yeg
2026-09-17 2aabed512ca44772d16d31bce833f31184676867
fix(merchant): 首页交易总额只统计已核销订单,退款受理后立即回落

根因:首页卡片 SQL 只过滤 order_status = 1(已支付),漏了核销条件,已支付未核销的订单也被求和。
「退款后金额不回落」是它的推论而非缓存:退款受理成功只把订单置为 refund_status = 1,
order_status 仍是 1,通道结果要等对账任务(最长十分钟)或下一次读订单才收敛,
这段时间它一直计入;商户首页统计的读路径本身也不做退款收敛。
口径改为 order_status = 1 且 verify_status = 1,与 selectPeriodSummaries、后台工作台一致,
退款中的订单因必然未核销被同时挡住,金额在退款受理那一刻就回落,不再依赖通道收敛。

新增 MerchantSettlementStatisticsServiceTests(真 MySQL、真 SQL):改前红
(expected: 1000L but was: 7000L,未核销 2000 + 退款中 4000 被计入),改后绿。
1个文件已添加
2个文件已修改
83 ■■■■■ 已修改文件
talent-card-server/src/main/java/com/talentcard/server/mapper/StatisticsQuerySql.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/service/MerchantSettlementStatisticsService.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/test/java/com/talentcard/server/service/MerchantSettlementStatisticsServiceTests.java 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/mapper/StatisticsQuerySql.java
@@ -382,10 +382,13 @@
        return sql("""
            select
                coalesce((
                    -- 只算已核销的实付金额:未核销的钱还扣在通道的延迟户里,核销后才分账给商户;
                    -- 退款中的订单在通道结果收敛前仍是 order_status = 1,只有「已核销」能立刻挡住它。
                    select sum(o.pay_amount)
                    from benefit_order o
                    where o.deleted = 0
                      and o.order_status = 1
                      and o.verify_status = 1
                      and o.merchant_id = #{merchantId}
                ), 0) as transactionAmountCents,
                coalesce((
talent-card-server/src/main/java/com/talentcard/server/service/MerchantSettlementStatisticsService.java
@@ -130,7 +130,7 @@
        result.put("subsidyAmountCents", subsidyAmount);
        result.put("subsidyAmount", yuan(subsidyAmount));
        result.put("hasData", transactionAmount > 0 || todayVerificationCount > 0 || weekVerificationCount > 0 || subsidyAmount > 0);
        result.put("statisticsBasis", "交易总额统计已支付订单实付金额;补贴总额统计政府承担的订单补贴、政府补贴券及酒店免费入住线下补贴,商家自行让利不进入政府补贴结算");
        result.put("statisticsBasis", "交易总额统计已支付且已核销订单的实付金额;补贴总额统计政府承担的订单补贴、政府补贴券及酒店免费入住线下补贴,商家自行让利不进入政府补贴结算");
        return result;
    }
talent-card-server/src/test/java/com/talentcard/server/service/MerchantSettlementStatisticsServiceTests.java
New file
@@ -0,0 +1,78 @@
package com.talentcard.server.service;
import static org.assertj.core.api.Assertions.assertThat;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.util.Map;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.ActiveProfiles;
/**
 * 商户首页统计卡片的取数口径。
 *
 * <p>「交易总额」是商户首页最显眼的数字,口径必须是**已支付且已核销**的订单实付金额:
 * 未核销的订单钱还扣在通道的延迟户里,核销后才分账给商户,把它算进交易总额等于告诉商户
 * 他已经收到了还没到手的钱。
 *
 * <p>另一个必守的性质是**退款受理后首页立刻回落**。人才端的退款受理成功只把订单置为「退款中」,
 * 通道的最终结果要等对账任务或下一次读订单才收敛(最长十分钟),中间这段时间订单还是
 * {@code order_status = 1}。所以统计口径不能依赖订单状态收敛,必须靠「是否核销」把这类订单挡在外面。
 */
@SpringBootTest
@ActiveProfiles("test")
class MerchantSettlementStatisticsServiceTests {
    @Autowired
    private JdbcTemplate jdbc;
    @Autowired
    private MerchantSettlementStatisticsService service;
    private Long merchantId;
    private Long talentId;
    @BeforeEach
    void prepareMerchant() {
        String suffix = String.valueOf(System.nanoTime());
        jdbc.update("insert into merchant(merchant_name, status, can_login_merchant_app, deleted) values (?, 1, 1, 0)",
                "首页统计测试商户 " + suffix);
        merchantId = jdbc.queryForObject("select max(id) from merchant", Long.class);
        jdbc.update("insert into talent_user(openid, name, mobile, status, deleted) values (?, ?, ?, 1, 0)",
                "dashboard-test-" + suffix, "首页统计测试人才", "13800000000");
        talentId = jdbc.queryForObject("select max(id) from talent_user", Long.class);
    }
    @Test
    void countsOnlyVerifiedOrdersInDashboardTransactionAmount() {
        long verifiedPayAmount = 1000L;
        // 已支付且已核销:钱已经确认分账给商户,计入。
        insertOrder("VER", 1, 1, 0, verifiedPayAmount);
        // 已支付但未核销:钱还在延迟户,未核销就不算。
        insertOrder("UNV", 1, 0, 0, 2000L);
        // 已支付、未核销、退款受理中:人才端刚点了退款,通道结果还没收敛,首页必须立刻回落。
        insertOrder("REF", 1, 0, 1, 4000L);
        Map<String, Object> cards = service.dashboardCards(merchantId);
        assertThat(((Number) cards.get("transactionAmountCents")).longValue()).isEqualTo(verifiedPayAmount);
    }
    private void insertOrder(String marker, int orderStatus, int verifyStatus, int refundStatus, long payAmount) {
        LocalDateTime now = LocalDateTime.now();
        jdbc.update("""
                insert into benefit_order(order_no, order_type, merchant_id, talent_id, product_name_snapshot,
                                          order_amount, pay_amount, subsidy_amount, order_status, verify_status,
                                          refund_status, created_at, updated_at, deleted)
                values (?, 'GROUP_BUY', ?, ?, ?, ?, ?, 0, ?, ?, ?, ?, ?, 0)
                """,
                "DASHBOARD-" + marker + "-" + System.nanoTime(), merchantId, talentId, "首页统计测试商品",
                payAmount, payAmount, orderStatus, verifyStatus, refundStatus,
                Timestamp.valueOf(now), Timestamp.valueOf(now));
    }
}