From c0fed06eaa86d4210f403afec96ffd5bb36b4190 Mon Sep 17 00:00:00 2001 From: cst61 Date: Mon, 25 May 2026 22:33:27 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8A=A5=E8=A1=A8=E5=AF=BC=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/rj/dto/hxr/HxrOrderRow.java | 2 + .../com/rj/entity/LbDailyUserTradeReport.java | 3 + src/main/java/com/rj/entity/LbOrderRow.java | 8 + .../java/com/rj/entity/LbPurchaseApply.java | 4 + .../LbDailyUserTradeReportServiceImpl.java | 114 +++- .../service/impl/LbOrderRowServiceImpl.java | 53 +- .../impl/LbPurchaseApplyServiceImpl.java | 141 +++++ .../LbDailyUserTradeReportExportSupport.java | 501 +++++++++++++++++- .../resources/mapper/LbOrderRowMapper.xml | 8 +- 9 files changed, 825 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/rj/dto/hxr/HxrOrderRow.java b/src/main/java/com/rj/dto/hxr/HxrOrderRow.java index b6909ca..6700fcd 100644 --- a/src/main/java/com/rj/dto/hxr/HxrOrderRow.java +++ b/src/main/java/com/rj/dto/hxr/HxrOrderRow.java @@ -10,6 +10,8 @@ public record HxrOrderRow( String sellerName, String sellerPhone, long buyerId, + String buyerName, + String buyerPhone, String orderSn, String totalMoney, String payTime, diff --git a/src/main/java/com/rj/entity/LbDailyUserTradeReport.java b/src/main/java/com/rj/entity/LbDailyUserTradeReport.java index d0050cd..bc4937f 100644 --- a/src/main/java/com/rj/entity/LbDailyUserTradeReport.java +++ b/src/main/java/com/rj/entity/LbDailyUserTradeReport.java @@ -78,13 +78,16 @@ public class LbDailyUserTradeReport implements Serializable { @Schema(description = "插入时间") @TableField("created_at") + @JsonFlexibleLocalDateTime private LocalDateTime createdAt; @Schema(description = "修改时间") @TableField("updated_at") + @JsonFlexibleLocalDateTime private LocalDateTime updatedAt; @Schema(description = "报表日期") @TableField("report_date") + @JsonFlexibleLocalDateTime private LocalDateTime reportDate; } diff --git a/src/main/java/com/rj/entity/LbOrderRow.java b/src/main/java/com/rj/entity/LbOrderRow.java index da68def..417c738 100644 --- a/src/main/java/com/rj/entity/LbOrderRow.java +++ b/src/main/java/com/rj/entity/LbOrderRow.java @@ -52,6 +52,14 @@ public class LbOrderRow implements Serializable { @Schema(description = "买家 id") private Long buyerId; + @TableField("buyer_name") + @Schema(description = "买家姓名") + private String buyerName; + + @TableField("buyer_phone") + @Schema(description = "买家电话") + private String buyerPhone; + @TableField("order_sn") @Schema(description = "订单号") private String orderSn; diff --git a/src/main/java/com/rj/entity/LbPurchaseApply.java b/src/main/java/com/rj/entity/LbPurchaseApply.java index f81e76d..e2229ee 100644 --- a/src/main/java/com/rj/entity/LbPurchaseApply.java +++ b/src/main/java/com/rj/entity/LbPurchaseApply.java @@ -83,4 +83,8 @@ public class LbPurchaseApply implements Serializable { @TableField(exist = false) @Schema(description = "推荐人用户信息(非数据库字段)") private LbUser referrerUser; + + @TableField(exist = false) + @Schema(description = "最近一次成交订单(非数据库字段,apply_phone 匹配 buyer_phone)") + private LbOrderRow latestTradeOrder; } diff --git a/src/main/java/com/rj/service/impl/LbDailyUserTradeReportServiceImpl.java b/src/main/java/com/rj/service/impl/LbDailyUserTradeReportServiceImpl.java index 6c3f104..a9da119 100644 --- a/src/main/java/com/rj/service/impl/LbDailyUserTradeReportServiceImpl.java +++ b/src/main/java/com/rj/service/impl/LbDailyUserTradeReportServiceImpl.java @@ -6,11 +6,15 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.rj.entity.LbDailyUserTrade; import com.rj.entity.LbDailyUserTradeReport; import com.rj.entity.LbDepartmentUser; +import com.rj.entity.LbOrderRow; +import com.rj.entity.LbUser; import com.rj.service.ILbDeductionAmountService; +import com.rj.service.ILbOrderRowService; import com.rj.mapper.LbDailyUserTradeMapper; import com.rj.mapper.LbDailyUserTradeReportMapper; import com.rj.service.ILbDailyUserTradeReportService; import com.rj.service.ILbDepartmentUserService; +import com.rj.service.ILbUserService; import com.rj.service.support.LbDailyUserTradeReportExportSupport; import com.rj.service.support.LbDailyUserTradeReportWriteSupport; import com.rj.tenant.TenantContextHolder; @@ -26,6 +30,7 @@ import org.springframework.transaction.support.TransactionTemplate; import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; @@ -48,6 +53,8 @@ public class LbDailyUserTradeReportServiceImpl extends ServiceImpl pageDailyUserTradeByUserIdAndDateRange( @@ -527,8 +538,18 @@ public class LbDailyUserTradeReportServiceImpl extends ServiceImpl rows = listReportsForExport(tenantIdTrim, parsedReportDate); Map deductionByUserName = lbDeductionAmountService.sumDikouAmtByUserNameForReportDate(tenantIdTrim, parsedReportDate); + Map userIdByNickname = buildUserIdByNickname(tenantIdTrim); + OrderTradeExportData orderTradeExportData = buildOrderTradeExportData(tenantIdTrim, parsedReportDate); LbDailyUserTradeReportExportSupport.writeExcel( - parsedReportDate, rows, deductionByUserName, response); + parsedReportDate, + rows, + deductionByUserName, + userIdByNickname, + orderTradeExportData.buyTotalByUserId(), + orderTradeExportData.buyDetailByUserId(), + orderTradeExportData.sellTotalByUserId(), + orderTradeExportData.sellDetailByUserId(), + response); } catch (Exception e) { LbDailyUserTradeReportExportSupport.writeExportError(response, e); } @@ -557,6 +578,97 @@ public class LbDailyUserTradeReportServiceImpl extends ServiceImpl buildUserIdByNickname(String tenantId) { + List users = lbUserService.list( + new LambdaQueryWrapper().eq(LbUser::getTenantId, tenantId)); + Map userIdByNickname = new HashMap<>(); + if (users == null) { + return userIdByNickname; + } + for (LbUser user : users) { + if (user.getNickname() == null || user.getNickname().trim().isEmpty()) { + continue; + } + if (user.getId() == null) { + continue; + } + userIdByNickname.putIfAbsent(user.getNickname().trim(), String.valueOf(user.getId())); + } + return userIdByNickname; + } + + /** 结果表检查表 P/Q/S/T:按租户与当日 buy_time 查询 lb_order_row,分别按 buyer_id、seller_id 汇总。 */ + private OrderTradeExportData buildOrderTradeExportData(String tenantId, LocalDate reportDate) { + String buyTimeStart = reportDate.atStartOfDay().format(ORDER_BUY_TIME_FMT); + String buyTimeEnd = reportDate.atTime(23, 59, 59).format(ORDER_BUY_TIME_FMT); + + List orderRows = lbOrderRowService.list( + new LambdaQueryWrapper() + .eq(LbOrderRow::getTenantId, tenantId) + .eq(LbOrderRow::getDataType, ORDER_DATA_TYPE_DETAIL) + .ge(LbOrderRow::getBuyTime, buyTimeStart) + .le(LbOrderRow::getBuyTime, buyTimeEnd) + .orderByAsc(LbOrderRow::getBuyTime) + .orderByAsc(LbOrderRow::getId)); + + Map buyTotalByUserId = new HashMap<>(); + Map> buyAmountsByUserId = new LinkedHashMap<>(); + Map sellTotalByUserId = new HashMap<>(); + Map> sellAmountsByUserId = new LinkedHashMap<>(); + if (orderRows == null) { + return new OrderTradeExportData(buyTotalByUserId, Map.of(), sellTotalByUserId, Map.of()); + } + for (LbOrderRow row : orderRows) { + BigDecimal money = row.getTotalMoney() == null ? BigDecimal.ZERO : row.getTotalMoney(); + if (row.getBuyerId() != null) { + String buyerId = String.valueOf(row.getBuyerId()); + buyTotalByUserId.merge(buyerId, money, BigDecimal::add); + buyAmountsByUserId.computeIfAbsent(buyerId, k -> new ArrayList<>()).add(money); + } + if (row.getSellerId() != null) { + String sellerId = String.valueOf(row.getSellerId()); + sellTotalByUserId.merge(sellerId, money, BigDecimal::add); + sellAmountsByUserId.computeIfAbsent(sellerId, k -> new ArrayList<>()).add(money); + } + } + + Map buyDetailByUserId = new HashMap<>(); + for (Map.Entry> entry : buyAmountsByUserId.entrySet()) { + buyDetailByUserId.put(entry.getKey(), formatOrderAmountDetail(entry.getValue())); + } + Map sellDetailByUserId = new HashMap<>(); + for (Map.Entry> entry : sellAmountsByUserId.entrySet()) { + sellDetailByUserId.put(entry.getKey(), formatOrderAmountDetail(entry.getValue())); + } + return new OrderTradeExportData( + buyTotalByUserId, buyDetailByUserId, sellTotalByUserId, sellDetailByUserId); + } + + private static String formatOrderAmountDetail(List amounts) { + if (amounts == null || amounts.isEmpty()) { + return ""; + } + StringBuilder sb = new StringBuilder(); + for (BigDecimal amount : amounts) { + if (amount == null) { + continue; + } + if (!sb.isEmpty()) { + sb.append('+'); + } + sb.append(amount.stripTrailingZeros().toPlainString()); + } + return sb.toString(); + } + + private record OrderTradeExportData( + Map buyTotalByUserId, + Map buyDetailByUserId, + Map sellTotalByUserId, + Map sellDetailByUserId) { + } + private static final class DetailRepairStat { final boolean repaired; final int updatedCount; diff --git a/src/main/java/com/rj/service/impl/LbOrderRowServiceImpl.java b/src/main/java/com/rj/service/impl/LbOrderRowServiceImpl.java index fe335ff..900ad78 100644 --- a/src/main/java/com/rj/service/impl/LbOrderRowServiceImpl.java +++ b/src/main/java/com/rj/service/impl/LbOrderRowServiceImpl.java @@ -7,11 +7,9 @@ import com.rj.dto.hxr.HxrOrderRow; import com.rj.dto.hxr.HxrOrderSelectResponse; import com.rj.entity.LbDepartmentUser; import com.rj.entity.LbOrderRow; +import com.rj.entity.LbUser; import com.rj.mapper.LbOrderRowMapper; -import com.rj.service.DingTalkRobotService; -import com.rj.service.HxrAdminOrderSelectService; -import com.rj.service.ILbDepartmentUserService; -import com.rj.service.ILbOrderRowService; +import com.rj.service.*; import com.rj.tenant.TenantContextHolder; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -303,6 +301,7 @@ public class LbOrderRowServiceImpl extends ServiceImpl entities, String tenantId) { + if (entities == null || entities.isEmpty()) { + return; + } + Set buyerIds = new HashSet<>(); + for (LbOrderRow entity : entities) { + if (entity.getBuyerId() != null) { + buyerIds.add(entity.getBuyerId()); + } + } + if (buyerIds.isEmpty()) { + return; + } + List userIdStrs = new ArrayList<>(buyerIds.size()); + for (Long buyerId : buyerIds) { + userIdStrs.add(String.valueOf(buyerId)); + } + log.info("buyerIds:{}", userIdStrs.toString()); + LambdaQueryWrapper w = new LambdaQueryWrapper<>(); + w.eq(LbUser::getTenantId, tenantId) + .in(LbUser::getId, userIdStrs); + List users = lbUserService.list(w); + Map byUserId = new LinkedHashMap<>(); + for (LbUser user : users) { + if (user.getId() != null && !byUserId.containsKey(user.getId())) { + byUserId.put(user.getId().toString(), user); + } + } + for (LbOrderRow entity : entities) { + if (entity.getBuyerId() == null) { + continue; + } + LbUser user = byUserId.get(String.valueOf(entity.getBuyerId())); + if (user != null) { + entity.setBuyerName(user.getNickname()); + entity.setBuyerPhone(user.getMobile()); + } + } + } private static LbOrderRow toLbOrderRow(HxrOrderRow row, String tenantId) { LbOrderRow e = new LbOrderRow(); @@ -699,6 +742,8 @@ public class LbOrderRowServiceImpl extends ServiceImpl page = this.page(new Page<>(current, size), queryWrapper); fillLinkedLbUsers(page.getRecords()); + fillLatestTradeOrders(page.getRecords()); result.put("success", true); result.put("message", "查询成功"); result.put("data", page.getRecords()); @@ -507,6 +521,133 @@ public class LbPurchaseApplyServiceImpl } } + /** + * 按租户 + 申请人手机号关联 {@link LbOrderRow}:{@code apply_phone} 匹配 {@code buyer_phone}, + * 取 pay_time 最近的一条已成交明细订单。 + */ + private void fillLatestTradeOrders(List records) { + if (records == null || records.isEmpty()) { + return; + } + Set buyerPhones = new HashSet<>(); + Set tenantIds = new HashSet<>(); + for (LbPurchaseApply record : records) { + String applyPhone = trimToNull(record.getApplyPhone()); + if (applyPhone != null) { + buyerPhones.add(applyPhone); + } + String tenantId = trimToNull(record.getTenantId()); + if (tenantId != null) { + tenantIds.add(tenantId); + } + } + if (buyerPhones.isEmpty()) { + return; + } + + LambdaQueryWrapper orderQuery = new LambdaQueryWrapper<>(); + orderQuery.eq(LbOrderRow::getDataType, ORDER_DATA_TYPE_DETAIL) + .in(LbOrderRow::getBuyerPhone, buyerPhones) + .isNotNull(LbOrderRow::getPayTime); + if (tenantIds.size() == 1) { + orderQuery.eq(LbOrderRow::getTenantId, tenantIds.iterator().next()); + } else if (tenantIds.size() > 1) { + orderQuery.in(LbOrderRow::getTenantId, tenantIds); + } + + List orderRows = lbOrderRowMapper.selectList(orderQuery); + if (orderRows == null || orderRows.isEmpty()) { + return; + } + + Map latestByTenantPhone = new HashMap<>(); + for (LbOrderRow row : orderRows) { + if (!isPaidOrderRow(row)) { + continue; + } + String buyerPhone = trimToNull(row.getBuyerPhone()); + if (buyerPhone == null) { + continue; + } + String key = buildTenantMobileKey(row.getTenantId(), buyerPhone); + LbOrderRow prev = latestByTenantPhone.get(key); + if (prev == null || isOrderPayTimeNewer(row, prev)) { + latestByTenantPhone.put(key, row); + } + } + + for (LbPurchaseApply record : records) { + String applyPhone = trimToNull(record.getApplyPhone()); + if (applyPhone == null) { + continue; + } + LbOrderRow latest = latestByTenantPhone.get(buildTenantMobileKey(record.getTenantId(), applyPhone)); + if (latest != null) { + record.setLatestTradeOrder(latest); + }else { + record.setLatestTradeOrder(new LbOrderRow()); + } + } + } + + private static boolean isPaidOrderRow(LbOrderRow row) { + String payTime = row.getPayTime(); + return payTime != null && !payTime.trim().isEmpty(); + } + + private static boolean isOrderPayTimeNewer(LbOrderRow order, LbOrderRow prev) { + LocalDateTime payTime = parseOrderTime(payTimeOrBuyTime(order)); + LocalDateTime prevPayTime = parseOrderTime(payTimeOrBuyTime(prev)); + if (payTime == null) { + return false; + } + if (prevPayTime == null) { + return true; + } + int cmp = payTime.compareTo(prevPayTime); + if (cmp != 0) { + return cmp > 0; + } + Long id = order.getId(); + Long prevId = prev.getId(); + if (id == null) { + return false; + } + if (prevId == null) { + return true; + } + return id > prevId; + } + + private static String payTimeOrBuyTime(LbOrderRow row) { + if (row.getPayTime() != null && !row.getPayTime().trim().isEmpty()) { + return row.getPayTime().trim(); + } + return row.getBuyTime() == null ? null : row.getBuyTime().trim(); + } + + private static LocalDateTime parseOrderTime(String timeText) { + if (timeText == null || timeText.isEmpty()) { + return null; + } + try { + return LocalDateTime.parse(timeText, ORDER_DAY_TIME_FMT); + } catch (DateTimeParseException ignored) { + // continue + } + try { + return LocalDate.parse(timeText.substring(0, Math.min(10, timeText.length())), ORDER_DAY_FMT) + .atStartOfDay(); + } catch (DateTimeParseException ignored) { + // continue + } + try { + return LocalDateTime.parse(timeText, DateTimeFormatter.ISO_LOCAL_DATE_TIME); + } catch (DateTimeParseException e) { + return null; + } + } + /** * 分页关联用户时仅暴露业务所需字段,其余置空或 0,避免泄露手机号、密码等隐私。 */ diff --git a/src/main/java/com/rj/service/support/LbDailyUserTradeReportExportSupport.java b/src/main/java/com/rj/service/support/LbDailyUserTradeReportExportSupport.java index 94d3658..11655d6 100644 --- a/src/main/java/com/rj/service/support/LbDailyUserTradeReportExportSupport.java +++ b/src/main/java/com/rj/service/support/LbDailyUserTradeReportExportSupport.java @@ -35,8 +35,11 @@ import java.nio.charset.StandardCharsets; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; /** * 当日用户交易报表 Excel 导出(POI 样式与写入逻辑)。 @@ -45,7 +48,7 @@ public final class LbDailyUserTradeReportExportSupport { /** 导出工作簿固定 8 个 sheet(顺序与业务模板一致)。 */ private static final String[] EXPORT_SHEET_NAMES = { - "总表", "抵扣表", "断货表", "11点表", "支付表", "支付表检查", "临时表2", "结果表" + "总表", "抵扣表", "断货表", "11点表", "支付表", "支付表检查表", "结果表", "结果表检查表" }; private static final int SUMMARY_SHEET_INDEX = 0; @@ -54,6 +57,20 @@ public final class LbDailyUserTradeReportExportSupport { private static final int ELEVEN_CLOCK_SHEET_INDEX = 3; private static final int PAYMENT_SHEET_INDEX = 4; private static final int PAYMENT_CHECK_SHEET_INDEX = 5; + private static final int RESULT_SHEET_INDEX = 6; + private static final int RESULT_CHECK_SHEET_INDEX = 7; + + /** 结果表检查表:M~O 留白;P 用户ID、Q 昵称、R 最终收付款;其后为买卖明细/汇总。 */ + private static final int RESULT_CHECK_COL_USER_ID = 15; + private static final int RESULT_CHECK_COL_NICKNAME = 16; + private static final int RESULT_CHECK_COL_FINAL_SETTLEMENT = 17; + private static final int RESULT_CHECK_COL_BUY_DETAIL = 18; + private static final int RESULT_CHECK_COL_BUY_TOTAL = 19; + private static final int RESULT_CHECK_COL_BUY_TOTAL_FEE = 20; + private static final int RESULT_CHECK_COL_SELL_DETAIL = 21; + private static final int RESULT_CHECK_COL_SELL_TOTAL = 22; + private static final int RESULT_CHECK_LAST_COL = RESULT_CHECK_COL_SELL_TOTAL; + private static final BigDecimal RESULT_CHECK_BUY_TOTAL_FEE_RATE = new BigDecimal("0.01"); private static final int PAYMENT_COL_NAME = 0; private static final int PAYMENT_COL_ACTUAL = 1; @@ -62,6 +79,14 @@ public final class LbDailyUserTradeReportExportSupport { private static final int PAYMENT_NEGATIVE_COL_ACTUAL = 5; private static final int PAYMENT_LAST_COL = PAYMENT_NEGATIVE_COL_ACTUAL; + /** 结果表:I–L 列为收付款贪心匹配明细(对应 VBA 自动匹配收付款) */ + private static final int RESULT_MATCH_NEG_NAME = 8; + private static final int RESULT_MATCH_FLAG = 9; + private static final int RESULT_MATCH_POS_NAME = 10; + private static final int RESULT_MATCH_AMT = 11; + private static final int RESULT_LAST_COL = RESULT_MATCH_AMT; + private static final int PAYMENT_MATCH_MAX_ROWS = 1000; + private static final int COL_NICKNAME = 0; private static final int COL_YESTODAY_BUY = 1; private static final int COL_DAILY_SELL = 2; @@ -87,6 +112,11 @@ public final class LbDailyUserTradeReportExportSupport { LocalDate reportDate, List rows, Map deductionByUserName, + Map userIdByNickname, + Map buyTotalByBuyerId, + Map buyDetailByBuyerId, + Map sellTotalBySellerId, + Map sellDetailBySellerId, HttpServletResponse response) throws IOException { try (Workbook workbook = new XSSFWorkbook()) { SheetStyles styles = SheetStyles.create(workbook); @@ -108,6 +138,17 @@ public final class LbDailyUserTradeReportExportSupport { Sheet paymentSheet = workbook.getSheetAt(PAYMENT_SHEET_INDEX); fillPaymentSheet(paymentSheet, exportRows, styles, deductionMap); fillPaymentCheckSheet(workbook.getSheetAt(PAYMENT_CHECK_SHEET_INDEX), paymentSheet, styles); + Sheet resultSheet = workbook.getSheetAt(RESULT_SHEET_INDEX); + fillResultSheetFromPayment(resultSheet, paymentSheet, styles); + fillResultCheckSheet( + workbook.getSheetAt(RESULT_CHECK_SHEET_INDEX), + resultSheet, + userIdByNickname, + buyTotalByBuyerId, + buyDetailByBuyerId, + sellTotalBySellerId, + sellDetailBySellerId, + styles); String filename = "当日汇总报表" + reportDate.format(DateTimeFormatter.ofPattern("yyyyMMdd")) + ".xlsx"; String encoded = URLEncoder.encode(filename, StandardCharsets.UTF_8).replaceAll("\\+", "%20"); @@ -269,7 +310,7 @@ public final class LbDailyUserTradeReportExportSupport { } /** - * 支付表检查:完整复制支付表,末尾追加一行「求和」,B、F 列为上方数据行之和。 + * 支付表检查表:完整复制支付表,末尾追加一行「求和」,B、F 列为上方数据行之和。 */ private static void fillPaymentCheckSheet(Sheet target, Sheet paymentSource, SheetStyles styles) { copyPaymentSheetContent(paymentSource, target); @@ -297,6 +338,462 @@ public final class LbDailyUserTradeReportExportSupport { autoSizeColumns(target, PAYMENT_LAST_COL); } + /** + * 结果表:从支付表读取 A/B 正值、E/F 负值,排序后贪心匹配,写入 A/B、E/F 副本及 I–L 匹配明细。 + * 逻辑对应 Excel 宏「自动匹配收付款」。 + */ + private static void fillResultSheetFromPayment(Sheet resultSheet, Sheet paymentSheet, SheetStyles styles) { + Row header = resultSheet.createRow(0); + header.setHeightInPoints(20 * styles.rowHeightFactor); + createHeaderCell(header, PAYMENT_COL_NAME, "昵称", styles.headerStyle); + createHeaderCell(header, PAYMENT_COL_ACTUAL, "实际收付", styles.headerStyle); + createHeaderCell(header, PAYMENT_NEGATIVE_COL_NAME, "昵称", styles.headerStyle); + createHeaderCell(header, PAYMENT_NEGATIVE_COL_ACTUAL, "实际收付", styles.headerStyle); + createHeaderCell(header, RESULT_MATCH_NEG_NAME, "昵称", styles.headerStyle); + createHeaderCell(header, RESULT_MATCH_FLAG, "F", styles.headerStyle); + createHeaderCell(header, RESULT_MATCH_POS_NAME, "昵称", styles.headerStyle); + createHeaderCell(header, RESULT_MATCH_AMT, "金额", styles.headerStyle); + + List positives = collectPaymentMatchEntries( + paymentSheet, PAYMENT_COL_NAME, PAYMENT_COL_ACTUAL, true); + List negatives = collectPaymentMatchEntries( + paymentSheet, PAYMENT_NEGATIVE_COL_NAME, PAYMENT_NEGATIVE_COL_ACTUAL, false); + + positives.sort(Comparator.comparing(PaymentMatchEntry::amount).reversed()); + negatives.sort(Comparator.comparing(PaymentMatchEntry::amount)); + + int listRow = 1; + for (PaymentMatchEntry entry : positives) { + writeResultListRow(resultSheet, listRow++, PAYMENT_COL_NAME, PAYMENT_COL_ACTUAL, entry, styles); + } + listRow = 1; + for (PaymentMatchEntry entry : negatives) { + writeResultListRow( + resultSheet, listRow++, PAYMENT_NEGATIVE_COL_NAME, PAYMENT_NEGATIVE_COL_ACTUAL, entry, styles); + } + + if (positives.isEmpty() || negatives.isEmpty()) { + writeResultMatchColumnLSum(resultSheet, styles); + autoSizeColumns(resultSheet, RESULT_LAST_COL); + return; + } + + int posIdx = 0; + int negIdx = 0; + int matchRow = 1; + while (posIdx < positives.size() && negIdx < negatives.size()) { + PaymentMatchEntry pos = positives.get(posIdx); + PaymentMatchEntry neg = negatives.get(negIdx); + + if (pos.amount().compareTo(BigDecimal.ZERO) <= 0) { + posIdx++; + continue; + } + BigDecimal negAbs = neg.amount().abs(); + if (negAbs.compareTo(BigDecimal.ZERO) <= 0) { + negIdx++; + continue; + } + + BigDecimal matchAmount = pos.amount().min(negAbs); + Row row = resultSheet.createRow(matchRow++); + applyRowHeight(row, styles); + createTextCell(row, RESULT_MATCH_NEG_NAME, neg.name(), styles.textStyle); + createTextCell(row, RESULT_MATCH_FLAG, "F", styles.textStyle); + createTextCell(row, RESULT_MATCH_POS_NAME, pos.name(), styles.textStyle); + createPaymentActualCell(row, RESULT_MATCH_AMT, matchAmount, paymentActualStyle(matchRow, styles)); + + pos.setAmount(pos.amount().subtract(matchAmount)); + neg.setAmount(neg.amount().add(matchAmount)); + } + + writeResultMatchColumnLSum(resultSheet, styles); + autoSizeColumns(resultSheet, RESULT_LAST_COL); + } + + /** 结果表 / 结果表检查表:L 列(匹配金额)求和,写入最后一行。 */ + private static void writeResultMatchColumnLSum(Sheet sheet, SheetStyles styles) { + int lastRow = sheet.getLastRowNum(); + if (lastRow < 1) { + return; + } + + int sumRowIdx; + boolean reuseSumRow = false; + Row trailingRow = sheet.getRow(lastRow); + if (trailingRow != null && isResultMatchSumLabelRow(trailingRow)) { + sumRowIdx = lastRow; + reuseSumRow = true; + } else { + sumRowIdx = lastRow + 1; + } + + int dataEndRow = reuseSumRow ? lastRow - 1 : lastRow; + BigDecimal sumL = BigDecimal.ZERO; + for (int rowIdx = 1; rowIdx <= dataEndRow; rowIdx++) { + Row row = sheet.getRow(rowIdx); + if (row == null || isResultMatchSumLabelRow(row)) { + continue; + } + sumL = sumL.add(readNumericCellValue(row.getCell(RESULT_MATCH_AMT))); + } + + Row sumRow = sheet.getRow(sumRowIdx); + if (sumRow == null) { + sumRow = sheet.createRow(sumRowIdx); + applyRowHeight(sumRow, styles); + } + createTextCell(sumRow, RESULT_MATCH_NEG_NAME, "求和", styles.summaryTextStyle); + createPaymentActualCell(sumRow, RESULT_MATCH_AMT, sumL, styles.summaryRoundedAmountStyle); + } + + private static boolean isResultMatchSumLabelRow(Row row) { + return "求和".equals(readPaymentCellText(row.getCell(RESULT_MATCH_NEG_NAME))); + } + + /** + * 结果表检查表:I/K 昵称各出现一次;P~V 为用户ID、昵称、最终收付款及买卖数据。 + */ + private static void fillResultCheckSheet( + Sheet target, + Sheet resultSource, + Map userIdByNickname, + Map buyTotalByBuyerId, + Map buyDetailByBuyerId, + Map sellTotalBySellerId, + Map sellDetailBySellerId, + SheetStyles styles) { + copyResultSheetContent(resultSource, target); + writeResultCheckExtraHeaders(target, styles); + Map nicknameToUserId = userIdByNickname == null ? Map.of() : userIdByNickname; + Map buyTotalMap = buyTotalByBuyerId == null ? Map.of() : buyTotalByBuyerId; + Map buyDetailMap = buyDetailByBuyerId == null ? Map.of() : buyDetailByBuyerId; + Map sellTotalMap = sellTotalBySellerId == null ? Map.of() : sellTotalBySellerId; + Map sellDetailMap = sellDetailBySellerId == null ? Map.of() : sellDetailBySellerId; + + int lastRow = target.getLastRowNum(); + Set usedSummaryRows = new HashSet<>(); + Set writtenNicknames = new HashSet<>(); + + for (int rowIdx = 1; rowIdx <= lastRow; rowIdx++) { + Row row = target.getRow(rowIdx); + if (row == null) { + continue; + } + String iName = normalizeResultCheckNickname( + readPaymentCellText(row.getCell(RESULT_MATCH_NEG_NAME))); + String kName = normalizeResultCheckNickname( + readPaymentCellText(row.getCell(RESULT_MATCH_POS_NAME))); + if (iName.isEmpty() && kName.isEmpty()) { + continue; + } + + if (!iName.isEmpty() && !writtenNicknames.contains(iName)) { + fillResultCheckUserColumns( + ensureResultCheckRow(target, rowIdx, styles), + iName, + nicknameToUserId, + buyTotalMap, + buyDetailMap, + sellTotalMap, + sellDetailMap, + styles); + usedSummaryRows.add(rowIdx); + writtenNicknames.add(iName); + continue; + } + if (!kName.isEmpty() && !writtenNicknames.contains(kName)) { + fillResultCheckUserColumns( + ensureResultCheckRow(target, rowIdx, styles), + kName, + nicknameToUserId, + buyTotalMap, + buyDetailMap, + sellTotalMap, + sellDetailMap, + styles); + usedSummaryRows.add(rowIdx); + writtenNicknames.add(kName); + } + } + + for (int rowIdx = 1; rowIdx <= lastRow; rowIdx++) { + Row row = target.getRow(rowIdx); + if (row == null) { + continue; + } + String kName = normalizeResultCheckNickname( + readPaymentCellText(row.getCell(RESULT_MATCH_POS_NAME))); + if (kName.isEmpty() || writtenNicknames.contains(kName)) { + continue; + } + int summaryRow = allocateResultCheckSummaryRow(target, usedSummaryRows, lastRow, styles); + fillResultCheckUserColumns( + ensureResultCheckRow(target, summaryRow, styles), + kName, + nicknameToUserId, + buyTotalMap, + buyDetailMap, + sellTotalMap, + sellDetailMap, + styles); + usedSummaryRows.add(summaryRow); + writtenNicknames.add(kName); + lastRow = Math.max(lastRow, summaryRow); + } + + writeResultMatchColumnLSum(target, styles); + autoSizeColumns(target, RESULT_CHECK_LAST_COL); + } + + /** 优先使用 1..lastRow 内未占用的行,否则在表尾追加。 */ + private static int allocateResultCheckSummaryRow( + Sheet sheet, Set usedSummaryRows, int lastRow, SheetStyles styles) { + for (int rowIdx = 1; rowIdx <= lastRow; rowIdx++) { + if (!usedSummaryRows.contains(rowIdx)) { + return rowIdx; + } + } + int appendRow = lastRow + 1; + while (usedSummaryRows.contains(appendRow)) { + appendRow++; + } + ensureResultCheckRow(sheet, appendRow, styles); + return appendRow; + } + + private static Row ensureResultCheckRow(Sheet sheet, int rowIdx, SheetStyles styles) { + Row row = sheet.getRow(rowIdx); + if (row == null) { + row = sheet.createRow(rowIdx); + applyRowHeight(row, styles); + } + return row; + } + + /** P~V:用户ID、昵称、最终收付款、买卖明细与汇总。 */ + private static void fillResultCheckUserColumns( + Row row, + String nickname, + Map userIdByNickname, + Map buyTotalByBuyerId, + Map buyDetailByBuyerId, + Map sellTotalBySellerId, + Map sellDetailBySellerId, + SheetStyles styles) { + String trimmed = normalizeResultCheckNickname(nickname); + if (trimmed.isEmpty()) { + return; + } + String userId = userIdByNickname.getOrDefault(trimmed, ""); + createTextCell(row, RESULT_CHECK_COL_USER_ID, userId, styles.textStyle); + createTextCell(row, RESULT_CHECK_COL_NICKNAME, trimmed, styles.textStyle); + if (!userId.isEmpty()) { + String buyDetail = buyDetailByBuyerId.getOrDefault(userId, ""); + BigDecimal buyTotal = buyTotalByBuyerId.getOrDefault(userId, BigDecimal.ZERO); + String sellDetail = sellDetailBySellerId.getOrDefault(userId, ""); + BigDecimal sellTotal = sellTotalBySellerId.getOrDefault(userId, BigDecimal.ZERO); + BigDecimal buyTotalFee = buyTotal + .multiply(RESULT_CHECK_BUY_TOTAL_FEE_RATE) + .setScale(2, RoundingMode.HALF_UP); + BigDecimal finalSettlement = sellTotal + .subtract(buyTotal) + .subtract(buyTotalFee) + .setScale(2, RoundingMode.HALF_UP); + + createDecimalCell(row, RESULT_CHECK_COL_FINAL_SETTLEMENT, finalSettlement, styles.amountStyle); + createTextCell(row, RESULT_CHECK_COL_BUY_DETAIL, buyDetail, styles.textStyle); + createDecimalCell(row, RESULT_CHECK_COL_BUY_TOTAL, buyTotal, styles.amountStyle); + createDecimalCell(row, RESULT_CHECK_COL_BUY_TOTAL_FEE, buyTotalFee, styles.amountStyle); + createTextCell(row, RESULT_CHECK_COL_SELL_DETAIL, sellDetail, styles.textStyle); + createDecimalCell(row, RESULT_CHECK_COL_SELL_TOTAL, sellTotal, styles.amountStyle); + } + } + + private static String normalizeResultCheckNickname(String nickname) { + return nickname == null ? "" : nickname.trim(); + } + + /** 结果表检查表 N 列表头(第 1 行)。 */ + private static void writeResultCheckExtraHeaders(Sheet sheet, SheetStyles styles) { + Row header = sheet.getRow(0); + if (header == null) { + header = sheet.createRow(0); + header.setHeightInPoints(20 * styles.rowHeightFactor); + } + createHeaderCell(header, RESULT_CHECK_COL_USER_ID, "用户ID", styles.headerStyle); + createHeaderCell(header, RESULT_CHECK_COL_NICKNAME, "昵称", styles.headerStyle); + createHeaderCell(header, RESULT_CHECK_COL_FINAL_SETTLEMENT, "最终收付款", styles.headerStyle); + createHeaderCell(header, RESULT_CHECK_COL_BUY_DETAIL, "订单金额明细", styles.headerStyle); + createHeaderCell(header, RESULT_CHECK_COL_BUY_TOTAL, "当日买入金额", styles.headerStyle); + createHeaderCell(header, RESULT_CHECK_COL_BUY_TOTAL_FEE, "1%金额", styles.headerStyle); + createHeaderCell(header, RESULT_CHECK_COL_SELL_DETAIL, "卖出金额明细", styles.headerStyle); + createHeaderCell(header, RESULT_CHECK_COL_SELL_TOTAL, "当日卖出金额", styles.headerStyle); + } + + private static void copyResultSheetContent(Sheet source, Sheet target) { + target.setDefaultRowHeight(source.getDefaultRowHeight()); + for (int col = 0; col <= RESULT_LAST_COL; col++) { + target.setColumnWidth(col, source.getColumnWidth(col)); + } + + for (int rowIdx = 0; rowIdx <= source.getLastRowNum(); rowIdx++) { + Row srcRow = source.getRow(rowIdx); + if (srcRow == null) { + continue; + } + Row destRow = target.createRow(rowIdx); + destRow.setHeight(srcRow.getHeight()); + for (int col = 0; col <= RESULT_LAST_COL; col++) { + Cell srcCell = srcRow.getCell(col); + if (srcCell == null) { + continue; + } + copyCellValueAndStyle(srcCell, destRow.createCell(col)); + } + } + } + + private static List collectPaymentMatchEntries( + Sheet paymentSheet, int nameCol, int amountCol, boolean positiveSide) { + List entries = new ArrayList<>(); + for (int rowIdx = 1; rowIdx <= PAYMENT_MATCH_MAX_ROWS; rowIdx++) { + Row row = paymentSheet.getRow(rowIdx); + if (row == null) { + if (positiveSide) { + if (isPaymentPairEmpty(paymentSheet, rowIdx, PAYMENT_COL_NAME, PAYMENT_COL_ACTUAL)) { + break; + } + } else if (isPaymentPairEmpty( + paymentSheet, rowIdx, PAYMENT_NEGATIVE_COL_NAME, PAYMENT_NEGATIVE_COL_ACTUAL) + && isPaymentPairEmpty( + paymentSheet, rowIdx + 1, PAYMENT_NEGATIVE_COL_NAME, PAYMENT_NEGATIVE_COL_ACTUAL)) { + break; + } + continue; + } + + String name = readPaymentCellText(row.getCell(nameCol)); + BigDecimal amount = parsePaymentSheetAmount(row.getCell(amountCol)); + boolean rowEmpty = name.isEmpty() && amount.compareTo(BigDecimal.ZERO) == 0; + if (rowEmpty) { + if (positiveSide) { + if (isPaymentPairEmpty(paymentSheet, rowIdx, PAYMENT_COL_NAME, PAYMENT_COL_ACTUAL)) { + break; + } + } else if (isPaymentPairEmpty( + paymentSheet, rowIdx, PAYMENT_NEGATIVE_COL_NAME, PAYMENT_NEGATIVE_COL_ACTUAL) + && isPaymentPairEmpty( + paymentSheet, rowIdx + 1, PAYMENT_NEGATIVE_COL_NAME, PAYMENT_NEGATIVE_COL_ACTUAL)) { + break; + } + continue; + } + + if (name.isEmpty()) { + continue; + } + if (positiveSide) { + if (amount.compareTo(BigDecimal.ZERO) > 0) { + entries.add(new PaymentMatchEntry(name, amount)); + } + } else if (amount.compareTo(BigDecimal.ZERO) < 0) { + entries.add(new PaymentMatchEntry(name, amount)); + } + } + return entries; + } + + private static boolean isPaymentPairEmpty(Sheet sheet, int rowIndex, int nameCol, int amountCol) { + if (rowIndex < 1 || rowIndex > PAYMENT_MATCH_MAX_ROWS) { + return true; + } + Row row = sheet.getRow(rowIndex); + if (row == null) { + return true; + } + return readPaymentCellText(row.getCell(nameCol)).isEmpty() + && parsePaymentSheetAmount(row.getCell(amountCol)).compareTo(BigDecimal.ZERO) == 0; + } + + private static void writeResultListRow( + Sheet sheet, + int rowIndex, + int nameCol, + int amountCol, + PaymentMatchEntry entry, + SheetStyles styles) { + Row row = sheet.getRow(rowIndex); + if (row == null) { + row = sheet.createRow(rowIndex); + applyRowHeight(row, styles); + } + createTextCell(row, nameCol, entry.name(), styles.textStyle); + createPaymentActualCell(row, amountCol, entry.amount(), paymentActualStyle(rowIndex, styles)); + } + + /** 支付表/结果表读数:去逗号与空格,兼容文本型数字 */ + private static BigDecimal parsePaymentSheetAmount(Cell cell) { + if (cell == null || cell.getCellType() == CellType.BLANK) { + return BigDecimal.ZERO; + } + if (cell.getCellType() == CellType.NUMERIC + || (cell.getCellType() == CellType.FORMULA + && cell.getCachedFormulaResultType() == CellType.NUMERIC)) { + return BigDecimal.valueOf(cell.getNumericCellValue()); + } + String text = readPaymentCellText(cell).replace(",", "").replace(" ", ""); + if (text.isEmpty()) { + return BigDecimal.ZERO; + } + try { + return new BigDecimal(text); + } catch (NumberFormatException ignored) { + return BigDecimal.ZERO; + } + } + + private static String readPaymentCellText(Cell cell) { + if (cell == null || cell.getCellType() == CellType.BLANK) { + return ""; + } + return switch (cell.getCellType()) { + case STRING -> { + String value = cell.getStringCellValue(); + yield value == null ? "" : value.trim(); + } + case NUMERIC -> BigDecimal.valueOf(cell.getNumericCellValue()).stripTrailingZeros().toPlainString(); + case BOOLEAN -> String.valueOf(cell.getBooleanCellValue()); + case FORMULA -> readPaymentCellText(switch (cell.getCachedFormulaResultType()) { + case STRING, NUMERIC -> cell; + default -> cell; + }); + default -> ""; + }; + } + + private static final class PaymentMatchEntry { + private final String name; + private BigDecimal amount; + + PaymentMatchEntry(String name, BigDecimal amount) { + this.name = name; + this.amount = amount; + } + + String name() { + return name; + } + + BigDecimal amount() { + return amount; + } + + void setAmount(BigDecimal amount) { + this.amount = amount; + } + } + private static void copyPaymentSheetContent(Sheet source, Sheet target) { target.setDefaultRowHeight(source.getDefaultRowHeight()); for (int col = 0; col <= PAYMENT_LAST_COL; col++) { diff --git a/src/main/resources/mapper/LbOrderRowMapper.xml b/src/main/resources/mapper/LbOrderRowMapper.xml index 23c16cc..26e5a06 100644 --- a/src/main/resources/mapper/LbOrderRowMapper.xml +++ b/src/main/resources/mapper/LbOrderRowMapper.xml @@ -7,7 +7,8 @@ INSERT INTO lb_order_row ( id, old_id, tenant_id, data_type, seller_id, seller_name, seller_phone, - buyer_id, order_sn, total_money, + buyer_id, buyer_name, buyer_phone, + order_sn, total_money, pay_time, pay_img, status, is_resell, is_show, consignee, phone, province, city, area, address, merchandise_id, confirm_time, buy_time, created_at, updated_at, @@ -17,7 +18,8 @@ ( #{item.id}, #{item.oldId}, #{item.tenantId}, #{item.dataType}, #{item.sellerId}, #{item.sellerName}, #{item.sellerPhone}, - #{item.buyerId}, #{item.orderSn}, #{item.totalMoney}, + #{item.buyerId}, #{item.buyerName}, #{item.buyerPhone}, + #{item.orderSn}, #{item.totalMoney}, #{item.payTime}, #{item.payImg}, #{item.status}, #{item.isResell}, #{item.isShow}, #{item.consignee}, #{item.phone}, #{item.province}, #{item.city}, #{item.area}, #{item.address}, #{item.merchandiseId}, #{item.confirmTime}, #{item.buyTime}, #{item.createdAt}, #{item.updatedAt}, @@ -32,6 +34,8 @@ seller_name = VALUES(seller_name), seller_phone = VALUES(seller_phone), buyer_id = VALUES(buyer_id), + buyer_name = VALUES(buyer_name), + buyer_phone = VALUES(buyer_phone), order_sn = VALUES(order_sn), total_money = VALUES(total_money), pay_time = VALUES(pay_time),