Bladeren bron

fix: 批量付款二轮审查修复 - enterprise_id空值拒绝与时间边界修正

- renderPay/batchQuery/batchClose/batchDetail 的 enterpriseId 为空时抛 400
  而非静默跳过租户过滤(防御 /pay /close body 路径零校验导致的跨租户操作)
- parseTimeFilter 10位纯日期按 endOfDay 补 00:00:00/23:59:59
  (修复前结束侧拼出 00:00:59,漏掉结束日整天数据)
- batchExport 删除 DateTimeParseException 死代码 catch
- +7 测试(空 enterpriseId 4 个 + 时间边界 3 个)
alphaH 6 dagen geleden
bovenliggende
commit
2fb8d2773f

+ 26 - 15
java/src/main/java/com/payment/platform/module/payment/batch/service/AlipayBatchPayService.java

@@ -353,10 +353,11 @@ public class AlipayBatchPayService {
 
     /** alipay.fund.trans.render.pay — 生成 PC 支付页链接 */
     public Map<String, String> renderPay(String enterpriseId, String outBatchNo) {
+        requireEnterpriseId(enterpriseId);
         BatchOrderEntity order = batchOrderMapper.selectOne(
                 new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<BatchOrderEntity>()
                         .eq(BatchOrderEntity::getOutBatchNo, outBatchNo)
-                        .eq(enterpriseId != null && !enterpriseId.isBlank(), BatchOrderEntity::getEnterpriseId, enterpriseId));
+                        .eq(BatchOrderEntity::getEnterpriseId, enterpriseId));
         if (order == null) throw new BusinessException(404, "批次不存在");
         if (!"INIT".equals(order.getStatus()))
             throw new BusinessException(400, "仅受理中的批次可支付,当前状态: " + order.getStatus());
@@ -381,10 +382,11 @@ public class AlipayBatchPayService {
 
     /** alipay.fund.batch.detail.query — 查询批次+明细状态并回写 DB */
     public Map<String, Object> batchQuery(String enterpriseId, String outBatchNo) {
+        requireEnterpriseId(enterpriseId);
         BatchOrderEntity order = batchOrderMapper.selectOne(
                 new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<BatchOrderEntity>()
                         .eq(BatchOrderEntity::getOutBatchNo, outBatchNo)
-                        .eq(enterpriseId != null && !enterpriseId.isBlank(), BatchOrderEntity::getEnterpriseId, enterpriseId));
+                        .eq(BatchOrderEntity::getEnterpriseId, enterpriseId));
         if (order == null) throw new BusinessException(404, "批次不存在");
         try {
             AlipayFundBatchDetailQueryModel model = new AlipayFundBatchDetailQueryModel();
@@ -437,10 +439,11 @@ public class AlipayBatchPayService {
 
     /** alipay.fund.batch.close — 主动关闭未支付批次 */
     public Map<String, String> batchClose(String enterpriseId, String outBatchNo) {
+        requireEnterpriseId(enterpriseId);
         BatchOrderEntity order = batchOrderMapper.selectOne(
                 new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<BatchOrderEntity>()
                         .eq(BatchOrderEntity::getOutBatchNo, outBatchNo)
-                        .eq(enterpriseId != null && !enterpriseId.isBlank(), BatchOrderEntity::getEnterpriseId, enterpriseId));
+                        .eq(BatchOrderEntity::getEnterpriseId, enterpriseId));
         if (order == null) throw new BusinessException(404, "批次不存在");
         // 与 renderPay 一致的前置守卫: 仅 INIT 状态可关闭(提前给友好提示而非透传支付宝原文案)
         if (!"INIT".equals(order.getStatus()))
@@ -499,10 +502,10 @@ public class AlipayBatchPayService {
                 .eq(status != null && !status.isBlank(), BatchOrderEntity::getStatus, status)
                 .orderByDesc(BatchOrderEntity::getId);
         if (startTime != null && !startTime.isBlank()) {
-            w.ge(BatchOrderEntity::getCreatedTime, parseTimeFilter(startTime, ":00+08:00"));
+            w.ge(BatchOrderEntity::getCreatedTime, parseTimeFilter(startTime, false));
         }
         if (endTime != null && !endTime.isBlank()) {
-            w.le(BatchOrderEntity::getCreatedTime, parseTimeFilter(endTime, ":59+08:00"));
+            w.le(BatchOrderEntity::getCreatedTime, parseTimeFilter(endTime, true));
         }
         var r = batchOrderMapper.selectPage(new Page<>(pageNo, pageSize), w);
         return PageResult.of(pageNo, pageSize, r.getTotal(), r.getRecords());
@@ -510,26 +513,36 @@ public class AlipayBatchPayService {
 
     /**
      * 时间参数解析: "yyyy-MM-dd HH:mm" 或纯日期 "yyyy-MM-dd" → OffsetDateTime;
-     * 10 位纯日期补当天零点(起始)/23:59(结束,与 AccountService.parseDateTime 行为对齐);
+     * 10 位纯日期按 endOfDay 补当天 00:00:00 / 23:59:59(与 AccountService.parseDateTime 行为对齐);
      * 非法格式抛 400(而非 DateTimeParseException→500)
      */
-    private static OffsetDateTime parseTimeFilter(String time, String suffix) {
+    static OffsetDateTime parseTimeFilter(String time, boolean endOfDay) {
         try {
-            // 10 位纯日期: 补 " 00:00",结束侧 suffix 会把秒替换为 :59 → 当天 23:59:59
-            String t = time.length() == 10 ? time + " 00:00" : time;
+            if (time.length() == 10) {
+                // 10 位纯日期: 起始补 " 00:00"+":00" → 00:00:00;结束补 " 23:59"+":59" → 23:59:59
+                String t = time + (endOfDay ? " 23:59" : " 00:00");
+                return OffsetDateTime.parse(t.replace(' ', 'T') + (endOfDay ? ":59+08:00" : ":00+08:00"));
+            }
             // "yyyy-MM-dd HH:mm" → "yyyy-MM-ddTHH:mm:00+08:00"(ISO 默认模式,无需自定义格式器)
-            return OffsetDateTime.parse(t.replace(' ', 'T') + suffix);
+            return OffsetDateTime.parse(time.replace(' ', 'T') + ":00+08:00");
         } catch (DateTimeParseException e) {
             throw new BusinessException(400, "时间参数格式错误:应形如 yyyy-MM-dd HH:mm");
         }
     }
 
+    /** 租户隔离: 企业 ID 是业务必需参数,为空直接拒绝(防御 Controller body 路径的零校验) */
+    private static void requireEnterpriseId(String enterpriseId) {
+        if (enterpriseId == null || enterpriseId.isBlank())
+            throw new BusinessException(400, "缺少企业ID");
+    }
+
     /** 批次详情 + 明细分页(enterprise_id 租户隔离,参照 batchList 过滤写法) */
     public Map<String, Object> batchDetail(String enterpriseId, String outBatchNo, int pageNo, int pageSize) {
+        requireEnterpriseId(enterpriseId);
         BatchOrderEntity order = batchOrderMapper.selectOne(
                 new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<BatchOrderEntity>()
                         .eq(BatchOrderEntity::getOutBatchNo, outBatchNo)
-                        .eq(enterpriseId != null && !enterpriseId.isBlank(), BatchOrderEntity::getEnterpriseId, enterpriseId));
+                        .eq(BatchOrderEntity::getEnterpriseId, enterpriseId));
         if (order == null) throw new BusinessException(404, "批次不存在");
         Page<BatchDetailEntity> detailPage = batchDetailMapper.selectPage(new Page<>(pageNo, pageSize),
                 new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<BatchDetailEntity>()
@@ -547,10 +560,10 @@ public class AlipayBatchPayService {
                     .eq(status != null && !status.isBlank(), BatchOrderEntity::getStatus, status)
                     .orderByDesc(BatchOrderEntity::getId);
             if (startTime != null && !startTime.isBlank()) {
-                w.ge(BatchOrderEntity::getCreatedTime, parseTimeFilter(startTime, ":00+08:00"));
+                w.ge(BatchOrderEntity::getCreatedTime, parseTimeFilter(startTime, false));
             }
             if (endTime != null && !endTime.isBlank()) {
-                w.le(BatchOrderEntity::getCreatedTime, parseTimeFilter(endTime, ":59+08:00"));
+                w.le(BatchOrderEntity::getCreatedTime, parseTimeFilter(endTime, true));
             }
             List<BatchOrderEntity> records = batchOrderMapper.selectList(w);
             // 列: 序号/批次号/支付宝批次号/标题/金额(元)/笔数/状态/创建时间/错误信息
@@ -595,8 +608,6 @@ public class AlipayBatchPayService {
         } catch (BusinessException e) {
             // parseTimeFilter 已抛 400(非法时间),不得被兜底包装成 500
             throw e;
-        } catch (DateTimeParseException e) {
-            throw new BusinessException(400, "时间参数格式错误:应形如 yyyy-MM-dd HH:mm");
         } catch (Exception e) {
             log.error("导出批次报表失败", e);
             throw new RuntimeException("导出批次报表失败: " + e.getMessage());

+ 54 - 0
java/src/test/java/com/payment/platform/module/payment/batch/service/AlipayBatchPayServiceTest.java

@@ -753,4 +753,58 @@ class AlipayBatchPayServiceTest {
         assertTrue(ex.getMessage().contains("INIT"), ex.getMessage());
         verify(alipayClient, never()).certificateExecute(any());
     }
+
+    // ==================== 二轮复核修复: 空 enterpriseId 拒绝(条件式 eq 的空值漏洞) ====================
+
+    @Test
+    void renderPay_blankEnterpriseId_throws() {
+        // enterprise_id 为空必须拒绝而非静默跳过租户过滤(防御 /pay body 零校验路径)
+        BusinessException ex = assertThrows(BusinessException.class, () -> service.renderPay("", "B1"));
+        assertEquals(400, ex.getCode());
+        verify(batchOrderMapper, never()).selectOne(any());
+    }
+
+    @Test
+    void batchQuery_nullEnterpriseId_throws() {
+        BusinessException ex = assertThrows(BusinessException.class, () -> service.batchQuery(null, "B1"));
+        assertEquals(400, ex.getCode());
+        verify(batchOrderMapper, never()).selectOne(any());
+    }
+
+    @Test
+    void batchClose_blankEnterpriseId_throws() {
+        BusinessException ex = assertThrows(BusinessException.class, () -> service.batchClose(" ", "B1"));
+        assertEquals(400, ex.getCode());
+        verify(batchOrderMapper, never()).selectOne(any());
+    }
+
+    @Test
+    void batchDetail_nullEnterpriseId_throws() {
+        BusinessException ex = assertThrows(BusinessException.class, () -> service.batchDetail(null, "B1", 1, 20));
+        assertEquals(400, ex.getCode());
+        verify(batchOrderMapper, never()).selectOne(any());
+    }
+
+    // ==================== 二轮复核修复: parseTimeFilter 边界精确断言 ====================
+
+    @Test
+    void parseTimeFilter_tenDigitDate_startOfDay() {
+        // 起始侧必须为当天 00:00:00(修复前拼出 00:00:59)。注: toString 对零秒省略 :00,用对象比较
+        assertEquals(OffsetDateTime.parse("2026-08-25T00:00:00+08:00"),
+                AlipayBatchPayService.parseTimeFilter("2026-08-25", false));
+    }
+
+    @Test
+    void parseTimeFilter_tenDigitDate_endOfDay() {
+        // 结束侧必须为当天 23:59:59(修复前拼出 00:00:59,漏掉结束日整天数据)
+        assertEquals(OffsetDateTime.parse("2026-08-25T23:59:59+08:00"),
+                AlipayBatchPayService.parseTimeFilter("2026-08-25", true));
+    }
+
+    @Test
+    void parseTimeFilter_withMinutes_keepsMinutes() {
+        // 带分钟输入不受 endOfDay 影响: 精确到分钟
+        assertEquals(OffsetDateTime.parse("2026-08-25T10:30:00+08:00"),
+                AlipayBatchPayService.parseTimeFilter("2026-08-25 10:30", true));
+    }
 }