Przeglądaj źródła

fix(首页统计): 乐收赢统计不再按企业筛 —— 去企业化后新单 enterprise_id 恒 NULL,卡片恒 0.00

用户报「银联收单的首页统计你没做」:卡片在、数不出来。根因是 2026-09-30 乐收赢
去企业化后新单 enterprise_id 恒为 NULL,而首页的企业下拉默认带着一个企业
(单个企业时 EnterpriseSwitcher 自动选中),statCollectedAmount 的
「AND enterprise_id = ?」把全部新单排除 ⇒ 三个窗口一起归零,且不报任何错。

- LsyOrderMapper#statCollectedAmount 去掉 enterpriseId 参数与 <if> 条件(只留租户条件)
- AccountService#statLsyCollectedAmount 只收 tenantId;汇总统计第三段同步
- AccountController /stat/lsy/amount 不再声明 enterprise_id(调用方多传也不会被使用)
- 前端 statLsyTradeAmount / fetchLsyAmount 不再带企业参数
- LsyOrderMapperSqlTest「企业条件传了也不出现」:先红(原 SQL 渲染出 AND enterprise_id = ?)
  后绿;配「租户条件传了仍出现」正面控制 —— 两向变异各被对应用例杀掉
- AccountServiceStatTest「租户参数仍然透传」:钉住去掉企业筛时没把租户筛一起删

验证:全量 mvn test 1038 绿(原 1037 + 新增 1);前端 pnpm ts:check 与改动前
逐字节相同(329 行基线),两文件各注入一条变异均被读到。
alphaH 1 tydzień temu
rodzic
commit
8e508171c5

+ 5 - 2
frontend/src/api/module_payment/account.ts

@@ -46,14 +46,17 @@ export const AccountAPI = {
   /**
    * 乐收赢收款统计(**净额** = 实收 − 已退)—— 首页「支付宝收款」卡的数据源。
    * 单位已是元(后端把库里的分换好才出网),键名与旁边三块统计一致。
+   *
+   * 【不带企业参数】乐收赢无企业维度(2026-09-30 去企业化后新单 enterprise_id 恒 NULL)
+   * ⇒ 按企业筛只会把新单整批排除、卡片恒 0.00 且不报错。后端端点也不再收这个参数。
    */
-  statLsyTradeAmount(tenantId?: number, enterpriseId?: string) {
+  statLsyTradeAmount(tenantId?: number) {
     return request<
       ApiResponse<{ amount_of_today: string; amount_of_7days: string; amount_of_all: string }>
     >({
       url: `${API_PATH}/stat/lsy/amount`,
       method: "get",
-      params: { tenant_id: tenantId, enterprise_id: enterpriseId },
+      params: { tenant_id: tenantId },
     });
   },
 

+ 3 - 1
frontend/src/views/dashboard/tenant.vue

@@ -222,9 +222,11 @@ async function fetchConsumeAmount() {
   }
 }
 
+// 乐收赢(银联收单)统计只按租户取,**不随「企业」下拉变** —— 乐收赢无企业维度
+// (去企业化后新单 enterprise_id 恒 NULL,带企业筛会把新单全排除、卡片恒 0.00)。
 async function fetchLsyAmount() {
   try {
-    const res = await AccountAPI.statLsyTradeAmount(currentTenantId.value, selectedEnterpriseId.value);
+    const res = await AccountAPI.statLsyTradeAmount(currentTenantId.value);
     lsyAmount.value = res.data.data || {};
   } catch (error) {
     console.error("获取乐收赢收款统计失败:", error);

+ 6 - 3
java/src/main/java/com/payment/platform/module/payment/account/controller/AccountController.java

@@ -64,6 +64,10 @@ public class AccountController {
     /**
      * 乐收赢收款统计(净额,元)—— 首页「支付宝收款」卡片的数据源(2026-10-05 起)。
      *
+     * <p>【没有 enterprise_id 参数,故意的】乐收赢无企业维度(2026-09-30 去企业化:
+     * 新单 enterprise_id 恒 NULL)⇒ 按企业筛只会把新单整批排除、卡片恒 0.00,
+     * 而且不报错。相邻三个统计仍收这个参数;本端点不声明它 ⇒ 调用方多传也不会被使用。
+     *
      * <p>权限串**故意复用**上面三个统计的 {@code module_payment:account:transfer}:
      * 这张卡与它们在同一个页面、同一批人看,新开一个串就得配菜单 + 角色授权 SQL,
      * 还要使用者重登才生效 —— 对一次「换数据源」来说是白加一次部署风险。
@@ -71,9 +75,8 @@ public class AccountController {
     @PreAuthorize("@perm.hasAny('module_payment:account:transfer')")
     @GetMapping("/stat/lsy/amount")
     public Result<Map<String, String>> statLsyCollectedAmount(
-            @RequestParam(name = "tenant_id", required = false) Long tenantId,
-            @RequestParam(name = "enterprise_id", required = false) String enterpriseId) {
-        return Result.ok(accountService.statLsyCollectedAmount(tenantId, enterpriseId));
+            @RequestParam(name = "tenant_id", required = false) Long tenantId) {
+        return Result.ok(accountService.statLsyCollectedAmount(tenantId));
     }
 
     @PreAuthorize("@perm.hasAny('module_payment:account:transfer')")

+ 9 - 3
java/src/main/java/com/payment/platform/module/payment/account/service/AccountService.java

@@ -160,15 +160,19 @@ public class AccountService {
      * {@link LsyOrderMapper#statCollectedAmount} 的注释里,这里只做两件事:
      * 把 {@link LsyOrderStatus#PAY_RECORDED} 传下去,把**分换成元**。
      *
+     * <p>【只按租户,没有企业参数】乐收赢无企业维度(2026-09-30 去企业化:新单
+     * enterprise_id 恒 NULL)⇒ 按企业筛就是把新单整批排除、卡片恒 0.00。
+     * 相邻两块统计仍按企业筛,这一块是租户级 —— 首页的企业下拉对它不生效。
+     *
      * <p>时间窗口由应用层计算(北京时间),与旁边三块统计同一口径;
      * 窗口键名也一模一样(amount_of_today / amount_of_7days / amount_of_all),
      * 前端四块卡片才能共用一个渲染形状。
      */
-    public Map<String, String> statLsyCollectedAmount(Long tenantId, String enterpriseId) {
+    public Map<String, String> statLsyCollectedAmount(Long tenantId) {
         OffsetDateTime today = LocalDate.now().atStartOfDay(ZoneOffset.ofHours(8)).toOffsetDateTime();
         OffsetDateTime weekAgo = today.minusDays(7);
         Map<String, BigDecimal> fen = lsyOrderMapper.statCollectedAmount(
-                tenantId, enterpriseId, LsyOrderStatus.PAY_RECORDED, today, weekAgo);
+                tenantId, LsyOrderStatus.PAY_RECORDED, today, weekAgo);
         return Map.of(
                 "amount_of_today", fenToYuan(fen.get("amount_of_today")),
                 "amount_of_7days", fenToYuan(fen.get("amount_of_7days")),
@@ -190,11 +194,13 @@ public class AccountService {
      * ★ 2026-10-05:第三段从「当面付」换成「乐收赢」—— 首页那张卡不再统计当面付了,
      *   汇总若还带着当面付,卡片上的数与汇总对不上(用户裁定,见 tenant.vue)。
      *   当面付的 statF2fTradeAmount 保留不删(现在没有调用方;切回去是一行)。
+     *   注意三段的口径差:前两段按企业筛,第三段(乐收赢)是租户级 —— 汇总=上面三张
+     *   卡片直接相加,卡片上是什么数、汇总里就是什么数。
      */
     public Map<String, String> statSummaryAmount(Long tenantId, String enterpriseId, String payeeType) {
         Map<String, String> transfer = statTransferAmount(tenantId, enterpriseId, payeeType);
         Map<String, String> consume = statConsumeAmount(tenantId, enterpriseId, payeeType);
-        Map<String, String> lsy = statLsyCollectedAmount(tenantId, enterpriseId);
+        Map<String, String> lsy = statLsyCollectedAmount(tenantId);
 
         return Map.of(
                 "amount_of_today", add(add(transfer.get("amount_of_today"), consume.get("amount_of_today")),

+ 6 - 4
java/src/main/java/com/payment/platform/module/payment/leshouying/mapper/LsyOrderMapper.java

@@ -36,6 +36,12 @@ public interface LsyOrderMapper extends BaseMapper<LsyOrderEntity> {
      * 已登录租户用户由拦截器自动追加;平台用户(tenantId 为 null)拦截器跳过,
      * 走这里的 {@code <if>} 显式条件(首页「选择商户」下拉就是这条路)。
      *
+     * <p>【没有企业条件,是故意的】2026-09-30 乐收赢去企业化后**新单 enterprise_id 恒为
+     * NULL**({@code LeshouyingCashierController} 的 javadoc 表:该列只留着历史订单的值)。
+     * 一旦按企业筛,全部新单被排除、三个窗口一起归零,而且**不报任何错** ——
+     * 2026-10-05 那张「首页乐收赢统计恒 0.00」就是这么来的(首页的企业下拉默认带着一个企业)。
+     * 乐收赢与企业概念无关是既有裁定(同上游开放 API 对老参数 enterprise_id 响亮拒绝那条)。
+     *
      * <p>SQL 形状由 {@code LsyOrderMapperSqlTest} 用 MyBatis 真求值钉住 ——
      * 服务层测试把本接口 mock 掉,注解里的 SQL 在那里完全不可见。
      */
@@ -55,13 +61,9 @@ public interface LsyOrderMapper extends BaseMapper<LsyOrderEntity> {
             <if test="tenantId != null">
             AND tenant_id = #{tenantId}
             </if>
-            <if test="enterpriseId != null and enterpriseId != ''">
-            AND enterprise_id = #{enterpriseId}
-            </if>
             </script>
             """)
     Map<String, BigDecimal> statCollectedAmount(@Param("tenantId") Long tenantId,
-                                                @Param("enterpriseId") String enterpriseId,
                                                 @Param("statuses") Collection<String> statuses,
                                                 @Param("today") OffsetDateTime today,
                                                 @Param("weekAgo") OffsetDateTime weekAgo);

+ 34 - 11
java/src/test/java/com/payment/platform/module/payment/account/service/AccountServiceStatTest.java

@@ -52,6 +52,11 @@ import static org.mockito.Mockito.when;
  * <p>SQL 本身的形状(净额公式逐窗口、finish_time 窗口列、占位符状态集合)由
  * {@code LsyOrderMapperSqlTest} 用 MyBatis 真求值来钉 —— 这里 mapper 是 mock,
  * 注解 SQL 在这个测试里**完全不可见**。
+ *
+ * <p>【企业参数不存在,是这张卡能出数的前提】乐收赢无企业维度(2026-09-30 去企业化后
+ * 新单 enterprise_id 恒 NULL)⇒ 服务层签名里就没有 enterpriseId,只透传 tenantId
+ * (见 {@link #乐收赢统计_租户参数仍然透传()})。谁把企业筛选加回来,
+ * {@code LsyOrderMapperSqlTest.企业条件传了也不出现} 会红。
  */
 @ExtendWith(MockitoExtension.class)
 class AccountServiceStatTest {
@@ -83,13 +88,13 @@ class AccountServiceStatTest {
     /** 库里是分,卡片上是元:12345 分 = 123.45 元;整百分位保留两位(100000 分 = 1000.00 元)。 */
     @Test
     void 乐收赢统计_分转元并保留两位() {
-        when(lsyOrderMapper.statCollectedAmount(any(), any(), any(), any(), any()))
+        when(lsyOrderMapper.statCollectedAmount(any(), any(), any(), any()))
                 .thenReturn(Map.of(
                         "amount_of_today", new BigDecimal("12345"),
                         "amount_of_7days", new BigDecimal("100000"),
                         "amount_of_all", new BigDecimal("0")));
 
-        Map<String, String> r = service.statLsyCollectedAmount(null, null);
+        Map<String, String> r = service.statLsyCollectedAmount(null);
 
         assertEquals("123.45", r.get("amount_of_today"));
         assertEquals("1000.00", r.get("amount_of_7days"));
@@ -99,10 +104,10 @@ class AccountServiceStatTest {
     /** 空表 / 键缺失时是 0.00 而不是 null 或异常 —— 首页任何一块统计都不该让整页报错。 */
     @Test
     void 乐收赢统计_键缺失时按零() {
-        when(lsyOrderMapper.statCollectedAmount(any(), any(), any(), any(), any()))
+        when(lsyOrderMapper.statCollectedAmount(any(), any(), any(), any()))
                 .thenReturn(Map.of("amount_of_all", new BigDecimal("250")));
 
-        Map<String, String> r = service.statLsyCollectedAmount(null, null);
+        Map<String, String> r = service.statLsyCollectedAmount(null);
 
         assertEquals("0.00", r.get("amount_of_today"));
         assertEquals("0.00", r.get("amount_of_7days"));
@@ -115,30 +120,48 @@ class AccountServiceStatTest {
      */
     @Test
     void 乐收赢统计_状态集合取自PAY_RECORDED() {
-        when(lsyOrderMapper.statCollectedAmount(any(), any(), any(), any(), any()))
+        when(lsyOrderMapper.statCollectedAmount(any(), any(), any(), any()))
                 .thenReturn(Map.of("amount_of_today", BigDecimal.ZERO,
                         "amount_of_7days", BigDecimal.ZERO, "amount_of_all", BigDecimal.ZERO));
 
-        service.statLsyCollectedAmount(null, null);
+        service.statLsyCollectedAmount(null);
 
         @SuppressWarnings("unchecked")
         ArgumentCaptor<Collection<String>> captor = ArgumentCaptor.forClass(Collection.class);
-        verify(lsyOrderMapper).statCollectedAmount(isNull(), isNull(), captor.capture(), any(), any());
+        verify(lsyOrderMapper).statCollectedAmount(isNull(), captor.capture(), any(), any());
         assertEquals(Set.copyOf(LsyOrderStatus.PAY_RECORDED), Set.copyOf(captor.getValue()));
     }
 
+    /**
+     * 租户参数**照旧透传**(正面控制)—— 去掉企业筛选时不能顺手把租户筛也去掉:
+     * 这条与 {@code LsyOrderMapperSqlTest} 的「企业条件传了也不出现」是一对,
+     * 一个钉「不许少」,一个钉「不许多」。
+     */
+    @Test
+    void 乐收赢统计_租户参数仍然透传() {
+        when(lsyOrderMapper.statCollectedAmount(any(), any(), any(), any()))
+                .thenReturn(Map.of("amount_of_today", BigDecimal.ZERO,
+                        "amount_of_7days", BigDecimal.ZERO, "amount_of_all", BigDecimal.ZERO));
+
+        service.statLsyCollectedAmount(7L);
+
+        ArgumentCaptor<Long> tenant = ArgumentCaptor.forClass(Long.class);
+        verify(lsyOrderMapper).statCollectedAmount(tenant.capture(), any(), any(), any());
+        assertEquals(7L, tenant.getValue());
+    }
+
     /** 时间窗:北京时间当日零点起的「今日」,近 7 日 = 今日减 7 天(与另外三块统计同一口径)。 */
     @Test
     void 乐收赢统计_时间窗是北京时间当日零点() {
-        when(lsyOrderMapper.statCollectedAmount(any(), any(), any(), any(), any()))
+        when(lsyOrderMapper.statCollectedAmount(any(), any(), any(), any()))
                 .thenReturn(Map.of("amount_of_today", BigDecimal.ZERO,
                         "amount_of_7days", BigDecimal.ZERO, "amount_of_all", BigDecimal.ZERO));
 
-        service.statLsyCollectedAmount(null, null);
+        service.statLsyCollectedAmount(null);
 
         ArgumentCaptor<OffsetDateTime> today = ArgumentCaptor.forClass(OffsetDateTime.class);
         ArgumentCaptor<OffsetDateTime> weekAgo = ArgumentCaptor.forClass(OffsetDateTime.class);
-        verify(lsyOrderMapper).statCollectedAmount(isNull(), isNull(), any(),
+        verify(lsyOrderMapper).statCollectedAmount(isNull(), any(),
                 today.capture(), weekAgo.capture());
         assertEquals(ZoneOffset.ofHours(8), today.getValue().getOffset());
         assertEquals(0, today.getValue().getHour());
@@ -165,7 +188,7 @@ class AccountServiceStatTest {
                 "amount_of_today", new BigDecimal("50"),
                 "amount_of_7days", new BigDecimal("60"),
                 "amount_of_all", new BigDecimal("70")));
-        when(lsyOrderMapper.statCollectedAmount(any(), any(), any(), any(), any())).thenReturn(Map.of(
+        when(lsyOrderMapper.statCollectedAmount(any(), any(), any(), any())).thenReturn(Map.of(
                 "amount_of_today", new BigDecimal("2500"),
                 "amount_of_7days", new BigDecimal("3000"),
                 "amount_of_all", new BigDecimal("4000")));

+ 20 - 5
java/src/test/java/com/payment/platform/module/payment/leshouying/mapper/LsyOrderMapperSqlTest.java

@@ -34,7 +34,9 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
  * {@link CCJSqlParserUtil} 解一遍 —— MyBatis-Plus 的租户行拦截器走的就是这支解析器。
  *
  * <p>这里钉的是**口径**,不是排版:净额公式在三个窗口里各出现一次、状态集合走占位符
- * (不手抄字面量)、窗口列是 {@code finish_time}、租户/企业条件按参数有无出现。
+ * (不手抄字面量)、窗口列是 {@code finish_time}、租户条件按参数有无出现、
+ * 而**企业条件一次都不许出现**(2026-10-05 那张卡恒 0.00 的根因,见
+ * {@link #企业条件传了也不出现()})。
  */
 class LsyOrderMapperSqlTest {
 
@@ -48,6 +50,10 @@ class LsyOrderMapperSqlTest {
      * <p>归一化只做三件事:空白折叠、逗号两侧留一个空格、**括号内侧不保留空格** ——
      * 后两者都是 MyBatis foreach 拼串的产物(实测渲染 `status IN ( ?, ?, ? )`),
      * 不是被测对象;占位符**有几个、什么顺序**必须由断言管。
+     *
+     * <p>{@code enterpriseId} 现在**已不是**这个接口的参数 —— 它仍然进 param map,
+     * 是为了让「按企业筛选」这个变异体看得见:谁把那条 {@code <if>} 加回来,
+     * 传非空值的用例立刻红(param map 里多余的键 MyBatis 不报错)。
      */
     private static String boundSql(Long tenantId, String enterpriseId) {
         Method m = Arrays.stream(LsyOrderMapper.class.getDeclaredMethods())
@@ -115,7 +121,7 @@ class LsyOrderMapperSqlTest {
                 && sql.contains("AS amount_of_all"), sql);
     }
 
-    /** 不传租户/企业时两条条件都不出现(已登录租户用户由拦截器自动追加租户条件)。 */
+    /** 不传租户时条件不出现(已登录租户用户由拦截器自动追加租户条件)。 */
     @Test
     void 缺省参数不生成条件() {
         String sql = boundSql(null, null);
@@ -123,12 +129,21 @@ class LsyOrderMapperSqlTest {
         assertFalse(sql.contains("enterprise_id"), sql);
     }
 
-    /** 传了租户/企业时两条条件都在(平台用户在首页选商户/企业就是这条路)。 */
+    /**
+     * 租户条件照旧按参数出现,**企业条件传了也不出现** —— 这条是 2026-10-05
+     * 「首页银联收单(乐收赢)统计恒 0.00」的回归钉。
+     *
+     * <p>为什么企业列必须不参与筛选:2026-09-30 乐收赢去企业化后**新单 enterprise_id
+     * 恒为 NULL**(该列只留着历史订单的值)。首页那只企业下拉默认就带着一个企业
+     * (只有一个企业时 `EnterpriseSwitcher` 自动选中)⇒ {@code AND enterprise_id = ?}
+     * 把全部新单排除,三个窗口一起归零,且**不报任何错**。
+     * 「乐收赢与企业概念无关」是既有裁定(同上游开放 API 响亮拒绝老参数那条)。
+     */
     @Test
-    void 传了参数条件成对出现() {
+    void 企业条件传了也不出现() {
         String sql = boundSql(7L, "ENT-1");
         assertTrue(sql.contains("tenant_id = ?"), sql);
-        assertTrue(sql.contains("enterprise_id = ?"), sql);
+        assertFalse(sql.contains("enterprise_id"), sql);
     }
 
     /** 解析器解得动这条 SQL —— 租户拦截器线上就是拿它解,解不动整个查询 500。 */