diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/JDOrderListController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/JDOrderListController.java index 8440bd5..beadd14 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/JDOrderListController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/JDOrderListController.java @@ -2,13 +2,15 @@ package com.ruoyi.web.controller.system; import java.io.IOException; import java.util.List; +import java.util.Map; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.ruoyi.jarvis.domain.OrderRows; +import com.ruoyi.jarvis.service.impl.GroupRebateExcelImportService; import com.ruoyi.jarvis.service.IOrderRowsService; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; import com.ruoyi.common.annotation.Log; import com.ruoyi.common.annotation.Anonymous; import com.ruoyi.common.core.controller.BaseController; @@ -34,11 +36,15 @@ public class JDOrderListController extends BaseController private final IJDOrderService jdOrderService; private final IOrderRowsService orderRowsService; private final IInstructionService instructionService; + private final GroupRebateExcelImportService groupRebateExcelImportService; - public JDOrderListController(IJDOrderService jdOrderService, IOrderRowsService orderRowsService, IInstructionService instructionService) { + public JDOrderListController(IJDOrderService jdOrderService, IOrderRowsService orderRowsService, + IInstructionService instructionService, + GroupRebateExcelImportService groupRebateExcelImportService) { this.jdOrderService = jdOrderService; this.orderRowsService = orderRowsService; this.instructionService = instructionService; + this.groupRebateExcelImportService = groupRebateExcelImportService; } /** @@ -74,6 +80,14 @@ public class JDOrderListController extends BaseController if (orderSearch != null && !orderSearch.trim().isEmpty()) { query.getParams().put("orderSearch", orderSearch.trim()); } + + String rebateRemarkAbnormal = request.getParameter("rebateRemarkHasAbnormal"); + if (rebateRemarkAbnormal != null && !rebateRemarkAbnormal.isEmpty()) { + query.setRebateRemarkHasAbnormal(Integer.valueOf(rebateRemarkAbnormal)); + } + if ("true".equalsIgnoreCase(request.getParameter("hasRebateRemark"))) { + query.getParams().put("hasRebateRemark", true); + } java.util.List list; if (orderBy != null && !orderBy.isEmpty()) { @@ -128,6 +142,24 @@ public class JDOrderListController extends BaseController /** * 导出JD订单列表 */ + /** + * 导入跟团返现类 Excel:按「单号/订单号」匹配系统订单,将「是否返现」「总共返现」等写入后返备注(可多次导入累加) + */ + @Log(title = "JD订单后返表导入", businessType = BusinessType.IMPORT) + @PostMapping("/importGroupRebateExcel") + public AjaxResult importGroupRebateExcel(@RequestParam("file") MultipartFile file, + @RequestParam(value = "documentTitle", required = false) String documentTitle) { + try { + Map data = groupRebateExcelImportService.importExcel(file, documentTitle); + if (Boolean.FALSE.equals(data.get("success"))) { + return AjaxResult.error(String.valueOf(data.get("message"))); + } + return AjaxResult.success(data); + } catch (Exception e) { + return AjaxResult.error("导入失败: " + e.getMessage()); + } + } + @Log(title = "JD订单", businessType = BusinessType.EXPORT) @PostMapping("/export") public void export(HttpServletResponse response, JDOrder jdOrder) throws IOException diff --git a/ruoyi-system/src/main/java/com/ruoyi/jarvis/domain/JDOrder.java b/ruoyi-system/src/main/java/com/ruoyi/jarvis/domain/JDOrder.java index 79bb548..1f35111 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/jarvis/domain/JDOrder.java +++ b/ruoyi-system/src/main/java/com/ruoyi/jarvis/domain/JDOrder.java @@ -145,6 +145,15 @@ public class JDOrder extends BaseEntity { @Excel(name = "评价日期", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") private Date reviewPostedDate; + /** + * 后返备注(多次导入跟团返现等 Excel 的记录,JSON 数组字符串) + * @see com.ruoyi.jarvis.domain.dto.RebateRemarkItem + */ + private String rebateRemarkJson; + + /** 后返备注中是否存在异常项(1是 0否),便于列表筛选 */ + private Integer rebateRemarkHasAbnormal; + } diff --git a/ruoyi-system/src/main/java/com/ruoyi/jarvis/domain/dto/RebateRemarkItem.java b/ruoyi-system/src/main/java/com/ruoyi/jarvis/domain/dto/RebateRemarkItem.java new file mode 100644 index 0000000..1fc0ce3 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/jarvis/domain/dto/RebateRemarkItem.java @@ -0,0 +1,28 @@ +package com.ruoyi.jarvis.domain.dto; + +import lombok.Data; + +/** + * 订单「后返备注」中单条记录(多次导入会追加多条) + */ +@Data +public class RebateRemarkItem { + + /** 文档标题,如:跟团+返现 260316 */ + private String documentTitle; + + /** Excel「是否返现」列原文 */ + private String whetherRebate; + + /** Excel 返现金额列(优先总共返现)展示值 */ + private String rebateAmount; + + /** 写入时间戳(毫秒) */ + private Long uploadTime; + + /** + * 是否判定为异常(空「是否返现」、待补/下次做表等关键词) + * 便于列表筛选与着色 + */ + private Boolean abnormal; +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/GroupRebateExcelImportService.java b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/GroupRebateExcelImportService.java new file mode 100644 index 0000000..704da9a --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/jarvis/service/impl/GroupRebateExcelImportService.java @@ -0,0 +1,348 @@ +package com.ruoyi.jarvis.service.impl; + +import com.alibaba.fastjson2.JSON; +import com.ruoyi.jarvis.domain.JDOrder; +import com.ruoyi.jarvis.domain.dto.RebateRemarkItem; +import com.ruoyi.jarvis.mapper.JDOrderMapper; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.DataFormatter; +import org.apache.poi.ss.usermodel.DateUtil; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.usermodel.WorkbookFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.Resource; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; + +/** + * 解析跟团返现类 Excel(表头含单号、是否返现、返现金额等),写入订单后返备注 JSON。 + */ +@Service +public class GroupRebateExcelImportService { + + private static final Logger log = LoggerFactory.getLogger(GroupRebateExcelImportService.class); + + private static final DataFormatter DATA_FORMATTER = new DataFormatter(); + + private static final Pattern ABNORMAL_HINT = Pattern.compile( + ".*(异常|待补|待返|下次|驳回|未返|不返|暂缓|缺|补表|重做|暂无|无$|-).*", Pattern.CASE_INSENSITIVE); + + private static final Pattern POSITIVE_HINT = Pattern.compile( + ".*(已返|已返现|正常|完成|通过|✓|√|到账).*", Pattern.CASE_INSENSITIVE); + + @Resource + private JDOrderMapper jdOrderMapper; + + @Transactional(rollbackFor = Exception.class) + public Map importExcel(MultipartFile file, String documentTitle) throws Exception { + Map result = new HashMap<>(); + if (file == null || file.isEmpty()) { + result.put("success", false); + result.put("message", "请选择文件"); + return result; + } + String title = documentTitle != null ? documentTitle.trim() : ""; + if (title.isEmpty()) { + String name = file.getOriginalFilename(); + if (name != null && name.contains(".")) { + title = name.substring(0, name.lastIndexOf('.')); + } else { + title = name != null ? name : "未命名文档"; + } + } + + try (InputStream is = file.getInputStream(); Workbook wb = WorkbookFactory.create(is)) { + Sheet sheet = wb.getNumberOfSheets() > 0 ? wb.getSheetAt(0) : null; + if (sheet == null) { + result.put("success", false); + result.put("message", "工作簿无工作表"); + return result; + } + + int headerRowIndex = findHeaderRowIndex(sheet); + if (headerRowIndex < 0) { + result.put("success", false); + result.put("message", "未找到表头(需包含「单号」或「订单号」列,以及「是否返现」或「总共返现」相关列)"); + return result; + } + + Row headerRow = sheet.getRow(headerRowIndex); + HeaderMap hm = buildHeaderMap(headerRow); + if (hm.orderCol < 0) { + result.put("success", false); + result.put("message", "未识别订单号列(表头需含「单号」或「订单号」,不含「第三方」)"); + return result; + } + if (hm.whetherRebateCol < 0 && hm.totalCashbackCol < 0) { + result.put("success", false); + result.put("message", "未识别「是否返现」或返现金额列(如「总共返现」)"); + return result; + } + + int dataRows = 0; + int updatedOrders = 0; + Set notFound = new LinkedHashSet<>(); + List errors = new ArrayList<>(); + + int lastRow = sheet.getLastRowNum(); + for (int r = headerRowIndex + 1; r <= lastRow; r++) { + Row row = sheet.getRow(r); + if (row == null) { + continue; + } + String orderNo = normalizeOrderNo(getCellText(row, hm.orderCol)); + if (orderNo.isEmpty()) { + continue; + } + dataRows++; + + String whether = hm.whetherRebateCol >= 0 ? trimToNull(getCellText(row, hm.whetherRebateCol)) : ""; + if (whether == null) { + whether = ""; + } + String amountStr = hm.totalCashbackCol >= 0 ? trimToNull(getCellText(row, hm.totalCashbackCol)) : ""; + if ((amountStr == null || amountStr.isEmpty()) && hm.fallbackAmountCol >= 0) { + amountStr = trimToNull(getCellText(row, hm.fallbackAmountCol)); + } + if (amountStr == null) { + amountStr = ""; + } + + JDOrder order = jdOrderMapper.selectJDOrderByOrderId(orderNo); + if (order == null) { + order = jdOrderMapper.selectJDOrderByThirdPartyOrderNo(orderNo); + } + if (order == null) { + notFound.add(orderNo); + continue; + } + + try { + appendRebateRemark(order, title, whether, amountStr); + updatedOrders++; + } catch (Exception e) { + log.warn("写入后返备注失败 orderNo={} id={}", orderNo, order.getId(), e); + errors.add(orderNo + ": " + e.getMessage()); + } + } + + result.put("success", true); + result.put("documentTitle", title); + result.put("dataRows", dataRows); + result.put("updatedOrders", updatedOrders); + result.put("notFoundOrderNos", new ArrayList<>(notFound)); + result.put("errors", errors); + result.put("message", String.format(Locale.ROOT, + "解析完成:有效数据行 %d,匹配并更新订单 %d,未找到订单 %d 条", + dataRows, updatedOrders, notFound.size())); + return result; + } + } + + private void appendRebateRemark(JDOrder order, String documentTitle, String whetherRebate, String rebateAmount) { + List list = new ArrayList<>(); + String existing = order.getRebateRemarkJson(); + if (existing != null && !existing.trim().isEmpty()) { + try { + list.addAll(JSON.parseArray(existing, RebateRemarkItem.class)); + } catch (Exception e) { + log.warn("解析已有 rebate_remark_json 失败 id={},将覆盖为新数组", order.getId()); + list.clear(); + } + } + + RebateRemarkItem item = new RebateRemarkItem(); + item.setDocumentTitle(documentTitle); + item.setWhetherRebate(whetherRebate); + item.setRebateAmount(rebateAmount); + item.setUploadTime(System.currentTimeMillis()); + item.setAbnormal(isAbnormalWhetherRebate(whetherRebate)); + list.add(item); + + boolean hasAbnormal = false; + for (RebateRemarkItem it : list) { + if (Boolean.TRUE.equals(it.getAbnormal())) { + hasAbnormal = true; + break; + } + } + + JDOrder upd = new JDOrder(); + upd.setId(order.getId()); + upd.setRebateRemarkJson(JSON.toJSONString(list)); + upd.setRebateRemarkHasAbnormal(hasAbnormal ? 1 : 0); + jdOrderMapper.updateJDOrder(upd); + } + + static boolean isAbnormalWhetherRebate(String text) { + if (text == null) { + return true; + } + String t = text.trim(); + if (t.isEmpty()) { + return true; + } + if ("-".equals(t) || "无".equals(t) || "暂无".equals(t) || "否".equals(t)) { + return true; + } + if (POSITIVE_HINT.matcher(t).matches() && !ABNORMAL_HINT.matcher(t).matches()) { + return false; + } + if (ABNORMAL_HINT.matcher(t).matches()) { + return true; + } + return false; + } + + private static int findHeaderRowIndex(Sheet sheet) { + int maxScan = Math.min(15, sheet.getLastRowNum() + 1); + for (int i = 0; i < maxScan; i++) { + Row row = sheet.getRow(i); + if (row == null) { + continue; + } + boolean hasOrder = false; + boolean hasWhether = false; + boolean hasAmount = false; + for (int c = 0; c <= row.getLastCellNum() && c < 64; c++) { + String h = normalizeHeader(getCellText(row, c)); + if (h.isEmpty()) { + continue; + } + if (isOrderHeader(h)) { + hasOrder = true; + } + if (h.contains("是否返现")) { + hasWhether = true; + } + if (h.contains("总共返现") || (h.contains("返现") && h.contains("金额"))) { + hasAmount = true; + } + } + if (hasOrder && (hasWhether || hasAmount)) { + return i; + } + } + return -1; + } + + private static boolean isOrderHeader(String h) { + if (h.contains("第三方")) { + return false; + } + if ("单号".equals(h) || h.startsWith("单号")) { + return true; + } + return h.contains("订单号"); + } + + private static HeaderMap buildHeaderMap(Row headerRow) { + HeaderMap hm = new HeaderMap(); + Map totalCashbackCandidates = new LinkedHashMap<>(); + for (int c = 0; c <= headerRow.getLastCellNum() && c < 128; c++) { + String raw = normalizeHeader(getCellText(headerRow, c)); + if (raw.isEmpty()) { + continue; + } + if (hm.orderCol < 0 && isOrderHeader(raw)) { + hm.orderCol = c; + } + if (hm.whetherRebateCol < 0 && raw.contains("是否返现")) { + hm.whetherRebateCol = c; + } + if (raw.contains("总共返现")) { + totalCashbackCandidates.put(raw, c); + } + if (hm.fallbackAmountCol < 0 && raw.contains("返现") && raw.contains("金额") + && !raw.contains("晒单") && hm.totalCashbackCol < 0) { + hm.fallbackAmountCol = c; + } + } + if (!totalCashbackCandidates.isEmpty()) { + hm.totalCashbackCol = totalCashbackCandidates.values().iterator().next(); + } + return hm; + } + + private static String normalizeHeader(String s) { + if (s == null) { + return ""; + } + return s.replace('\n', ' ').replace('\r', ' ').trim(); + } + + private static String getCellText(Row row, int col) { + if (row == null || col < 0) { + return ""; + } + Cell cell = row.getCell(col); + if (cell == null) { + return ""; + } + switch (cell.getCellType()) { + case STRING: + return cell.getStringCellValue(); + case NUMERIC: + if (DateUtil.isCellDateFormatted(cell)) { + return DATA_FORMATTER.formatCellValue(cell); + } + double d = cell.getNumericCellValue(); + if (d == Math.floor(d) && Math.abs(d) < 1e16) { + return String.valueOf((long) d); + } + return DATA_FORMATTER.formatCellValue(cell); + case BOOLEAN: + return Boolean.toString(cell.getBooleanCellValue()); + case FORMULA: + try { + return DATA_FORMATTER.formatCellValue(cell); + } catch (Exception e) { + return ""; + } + default: + return DATA_FORMATTER.formatCellValue(cell); + } + } + + private static String trimToNull(String s) { + if (s == null) { + return null; + } + String t = s.trim(); + return t.isEmpty() ? null : t; + } + + private static String normalizeOrderNo(String raw) { + if (raw == null) { + return ""; + } + String t = raw.trim(); + if (t.endsWith(".0") && t.matches("\\d+\\.0")) { + t = t.substring(0, t.length() - 2); + } + return t; + } + + private static class HeaderMap { + int orderCol = -1; + int whetherRebateCol = -1; + int totalCashbackCol = -1; + /** 无「总共返现」时的备选:含「返现」「金额」的列 */ + int fallbackAmountCol = -1; + } +} diff --git a/ruoyi-system/src/main/resources/mapper/jarvis/JDOrderMapper.xml b/ruoyi-system/src/main/resources/mapper/jarvis/JDOrderMapper.xml index a8caf4f..203656d 100644 --- a/ruoyi-system/src/main/resources/mapper/jarvis/JDOrderMapper.xml +++ b/ruoyi-system/src/main/resources/mapper/jarvis/JDOrderMapper.xml @@ -35,13 +35,16 @@ + + select id, remark, distribution_mark, model_number, link, payment_amount, rebate_amount, address, logistics_link, order_id, buyer, order_time, create_time, update_time, status, is_count_enabled, third_party_order_no, jingfen_actual_price, is_refunded, refund_date, is_refund_received, refund_received_date, is_rebate_received, rebate_received_date, - is_price_protected, price_protected_date, is_invoice_opened, invoice_opened_date, is_review_posted, review_posted_date + is_price_protected, price_protected_date, is_invoice_opened, invoice_opened_date, is_review_posted, review_posted_date, + rebate_remark_json, rebate_remark_has_abnormal from jd_order @@ -72,6 +75,10 @@ and is_price_protected = #{isPriceProtected} and is_invoice_opened = #{isInvoiceOpened} and is_review_posted = #{isReviewPosted} + and rebate_remark_has_abnormal = #{rebateRemarkHasAbnormal} + + and rebate_remark_json is not null and char_length(trim(rebate_remark_json)) > 2 + and date(order_time) >= #{params.beginTime} @@ -109,6 +116,10 @@ and is_price_protected = #{isPriceProtected} and is_invoice_opened = #{isInvoiceOpened} and is_review_posted = #{isReviewPosted} + and rebate_remark_has_abnormal = #{rebateRemarkHasAbnormal} + + and rebate_remark_json is not null and char_length(trim(rebate_remark_json)) > 2 + and date(order_time) >= #{params.beginTime} @@ -183,6 +194,8 @@ invoice_opened_date = #{invoiceOpenedDate}, is_review_posted = #{isReviewPosted}, review_posted_date = #{reviewPostedDate}, + rebate_remark_json = #{rebateRemarkJson}, + rebate_remark_has_abnormal = #{rebateRemarkHasAbnormal}, update_time = now() where id = #{id} diff --git a/sql/jd_order_rebate_remark.sql b/sql/jd_order_rebate_remark.sql new file mode 100644 index 0000000..4543091 --- /dev/null +++ b/sql/jd_order_rebate_remark.sql @@ -0,0 +1,6 @@ +-- 后返备注:多次导入跟团返现等 Excel 的记录(JSON 数组),及是否存在异常(便于列表筛选) +-- 执行前请确认 jd_order 表存在 + +ALTER TABLE jd_order +ADD COLUMN rebate_remark_json LONGTEXT NULL COMMENT '后返备注(JSON数组,每条含文档标题、是否返现、金额等)' AFTER review_posted_date, +ADD COLUMN rebate_remark_has_abnormal TINYINT(1) NULL DEFAULT NULL COMMENT '后返备注中是否存在异常项:1是 0否 NULL无记录';