This commit is contained in:
van
2026-03-24 01:09:01 +08:00
parent 6ecedf91b3
commit 175cd3ba01
6 changed files with 439 additions and 3 deletions

View File

@@ -2,13 +2,15 @@ package com.ruoyi.web.controller.system;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import com.ruoyi.jarvis.domain.OrderRows; import com.ruoyi.jarvis.domain.OrderRows;
import com.ruoyi.jarvis.service.impl.GroupRebateExcelImportService;
import com.ruoyi.jarvis.service.IOrderRowsService; import com.ruoyi.jarvis.service.IOrderRowsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import com.ruoyi.common.annotation.Log; import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.annotation.Anonymous; import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.core.controller.BaseController; import com.ruoyi.common.core.controller.BaseController;
@@ -34,11 +36,15 @@ public class JDOrderListController extends BaseController
private final IJDOrderService jdOrderService; private final IJDOrderService jdOrderService;
private final IOrderRowsService orderRowsService; private final IOrderRowsService orderRowsService;
private final IInstructionService instructionService; 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.jdOrderService = jdOrderService;
this.orderRowsService = orderRowsService; this.orderRowsService = orderRowsService;
this.instructionService = instructionService; this.instructionService = instructionService;
this.groupRebateExcelImportService = groupRebateExcelImportService;
} }
/** /**
@@ -75,6 +81,14 @@ public class JDOrderListController extends BaseController
query.getParams().put("orderSearch", orderSearch.trim()); 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<JDOrder> list; java.util.List<JDOrder> list;
if (orderBy != null && !orderBy.isEmpty()) { if (orderBy != null && !orderBy.isEmpty()) {
// 设置排序参数 // 设置排序参数
@@ -128,6 +142,24 @@ public class JDOrderListController extends BaseController
/** /**
* 导出JD订单列表 * 导出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<String, Object> 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) @Log(title = "JD订单", businessType = BusinessType.EXPORT)
@PostMapping("/export") @PostMapping("/export")
public void export(HttpServletResponse response, JDOrder jdOrder) throws IOException public void export(HttpServletResponse response, JDOrder jdOrder) throws IOException

View File

@@ -145,6 +145,15 @@ public class JDOrder extends BaseEntity {
@Excel(name = "评价日期", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") @Excel(name = "评价日期", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date reviewPostedDate; private Date reviewPostedDate;
/**
* 后返备注(多次导入跟团返现等 Excel 的记录JSON 数组字符串)
* @see com.ruoyi.jarvis.domain.dto.RebateRemarkItem
*/
private String rebateRemarkJson;
/** 后返备注中是否存在异常项1是 0否便于列表筛选 */
private Integer rebateRemarkHasAbnormal;
} }

View File

@@ -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;
}

View File

@@ -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<String, Object> importExcel(MultipartFile file, String documentTitle) throws Exception {
Map<String, Object> 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<String> notFound = new LinkedHashSet<>();
List<String> 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<RebateRemarkItem> 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<String, Integer> 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;
}
}

View File

@@ -35,13 +35,16 @@
<result property="invoiceOpenedDate" column="invoice_opened_date"/> <result property="invoiceOpenedDate" column="invoice_opened_date"/>
<result property="isReviewPosted" column="is_review_posted"/> <result property="isReviewPosted" column="is_review_posted"/>
<result property="reviewPostedDate" column="review_posted_date"/> <result property="reviewPostedDate" column="review_posted_date"/>
<result property="rebateRemarkJson" column="rebate_remark_json"/>
<result property="rebateRemarkHasAbnormal" column="rebate_remark_has_abnormal"/>
</resultMap> </resultMap>
<sql id="selectJDOrderBase"> <sql id="selectJDOrderBase">
select id, remark, distribution_mark, model_number, link, payment_amount, rebate_amount, 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, 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_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 from jd_order
</sql> </sql>
@@ -72,6 +75,10 @@
<if test="isPriceProtected != null"> and is_price_protected = #{isPriceProtected}</if> <if test="isPriceProtected != null"> and is_price_protected = #{isPriceProtected}</if>
<if test="isInvoiceOpened != null"> and is_invoice_opened = #{isInvoiceOpened}</if> <if test="isInvoiceOpened != null"> and is_invoice_opened = #{isInvoiceOpened}</if>
<if test="isReviewPosted != null"> and is_review_posted = #{isReviewPosted}</if> <if test="isReviewPosted != null"> and is_review_posted = #{isReviewPosted}</if>
<if test="rebateRemarkHasAbnormal != null"> and rebate_remark_has_abnormal = #{rebateRemarkHasAbnormal}</if>
<if test="params.hasRebateRemark != null and params.hasRebateRemark == true">
and rebate_remark_json is not null and char_length(trim(rebate_remark_json)) &gt; 2
</if>
<if test="params.beginTime != null and params.beginTime != ''"><!-- 开始时间检索 --> <if test="params.beginTime != null and params.beginTime != ''"><!-- 开始时间检索 -->
and date(order_time) &gt;= #{params.beginTime} and date(order_time) &gt;= #{params.beginTime}
</if> </if>
@@ -109,6 +116,10 @@
<if test="isPriceProtected != null"> and is_price_protected = #{isPriceProtected}</if> <if test="isPriceProtected != null"> and is_price_protected = #{isPriceProtected}</if>
<if test="isInvoiceOpened != null"> and is_invoice_opened = #{isInvoiceOpened}</if> <if test="isInvoiceOpened != null"> and is_invoice_opened = #{isInvoiceOpened}</if>
<if test="isReviewPosted != null"> and is_review_posted = #{isReviewPosted}</if> <if test="isReviewPosted != null"> and is_review_posted = #{isReviewPosted}</if>
<if test="rebateRemarkHasAbnormal != null"> and rebate_remark_has_abnormal = #{rebateRemarkHasAbnormal}</if>
<if test="params.hasRebateRemark != null and params.hasRebateRemark == true">
and rebate_remark_json is not null and char_length(trim(rebate_remark_json)) &gt; 2
</if>
<if test="params.beginTime != null and params.beginTime != ''"><!-- 开始时间检索 --> <if test="params.beginTime != null and params.beginTime != ''"><!-- 开始时间检索 -->
and date(order_time) &gt;= #{params.beginTime} and date(order_time) &gt;= #{params.beginTime}
</if> </if>
@@ -183,6 +194,8 @@
<if test="invoiceOpenedDate != null"> invoice_opened_date = #{invoiceOpenedDate},</if> <if test="invoiceOpenedDate != null"> invoice_opened_date = #{invoiceOpenedDate},</if>
<if test="isReviewPosted != null"> is_review_posted = #{isReviewPosted},</if> <if test="isReviewPosted != null"> is_review_posted = #{isReviewPosted},</if>
<if test="reviewPostedDate != null"> review_posted_date = #{reviewPostedDate},</if> <if test="reviewPostedDate != null"> review_posted_date = #{reviewPostedDate},</if>
<if test="rebateRemarkJson != null"> rebate_remark_json = #{rebateRemarkJson},</if>
<if test="rebateRemarkHasAbnormal != null"> rebate_remark_has_abnormal = #{rebateRemarkHasAbnormal},</if>
update_time = now() update_time = now()
</set> </set>
where id = #{id} where id = #{id}

View File

@@ -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无记录';