yeg
2026-09-13 32f5e1a6aa30b3df020d58f0324f555705fd3812
bug更改
1个文件已添加
1个文件已修改
110 ■■■■■ 已修改文件
talent-card-server/scripts/repair-local-withdrawal-order-status.sql 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/test/java/com/talentcard/server/business/merchant/MerchantWithdrawalVisibilityTests.java 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/scripts/repair-local-withdrawal-order-status.sql
New file
@@ -0,0 +1,61 @@
-- 提现列表缺少订单状态字段、页面刷新待办接口缺少提现关联表时的本地库定向修复。
-- 在应用连接的数据库中执行(例如先 USE talent_card),不要执行在错误的 schema 中。
-- 根因:本地关闭 Flyway,订单表未同步 V132 的两个状态字段;查询失败时前端会显示空表。
-- 正常由 Flyway 管理的环境应执行正式迁移,而不是使用此脚本。
-- 本脚本用于已有迁移历史冲突的本地库:补订单状态、退款回填及缺失的提现关联表,支持重复执行。
-- 不改写 Flyway 历史,也不声称完成 V132 的核销唯一约束或后续自动提现的全部迁移。
-- 后续恢复 Flyway 前,仍需核对本地历史和已手工补齐的 DDL。
set @withdrawal_repair_ddl = (
    select if(count(*) = 0,
              'alter table benefit_order add column refund_status tinyint not null default 0 comment ''订单退款状态:0未退款 1退款中 2已退款''',
              'select 1')
    from information_schema.columns
    where table_schema = database()
      and table_name = 'benefit_order'
      and column_name = 'refund_status'
);
prepare withdrawal_repair_stmt from @withdrawal_repair_ddl;
execute withdrawal_repair_stmt;
deallocate prepare withdrawal_repair_stmt;
set @withdrawal_repair_ddl = (
    select if(count(*) = 0,
              'alter table benefit_order add column withdrawal_status tinyint not null default 0 comment ''提现锁定状态:0未进入提现记录 1已进入提现记录''',
              'select 1')
    from information_schema.columns
    where table_schema = database()
      and table_name = 'benefit_order'
      and column_name = 'withdrawal_status'
);
prepare withdrawal_repair_stmt from @withdrawal_repair_ddl;
execute withdrawal_repair_stmt;
deallocate prepare withdrawal_repair_stmt;
-- 与 V132 一致:退款成功优先于退款中,已退款订单不能因新增列默认 0 而重新参与提现。
update benefit_order o
    join (select order_id, max(case when refund_status = 2 then 1 else 0 end) refunded
          from order_refund
          where deleted = 0 and refund_status in (1, 2)
          group by order_id) r on r.order_id = o.id
set o.refund_status = case when r.refunded = 1 then 2 else 1 end
where o.refund_status = 0;
set @withdrawal_repair_ddl = null;
-- 页面刷新时 AdminLayout 还会请求 pending-notices;其中的失败提现提醒依赖这张表。
-- 缺表的旧库直接采用 V133 + V135 的最终结构,包含 released_at 和有效关联唯一约束。
-- 已有且完成迁移的表保持原样;历史手动提现没有来源订单记录,不凭金额猜测关联关系。
create table if not exists merchant_withdrawal_order (
    id bigint not null auto_increment,
    withdrawal_id bigint not null,
    order_id bigint not null,
    amount_cents bigint not null,
    created_at datetime not null default current_timestamp,
    released_at datetime default null comment '顺延解锁时间:解锁后该订单可以进入新的提现记录',
    active tinyint generated always as (if(released_at is null, 1, null)) stored
        comment '唯一键辅助列:未解锁为 1,已解锁为 NULL',
    primary key (id),
    unique key uk_withdrawal_order_active (order_id, active),
    key idx_withdrawal_order_assoc_withdrawal (withdrawal_id)
) engine=InnoDB default charset=utf8mb4 comment='自动提现记录与来源订单的内部关联';
talent-card-server/src/test/java/com/talentcard/server/business/merchant/MerchantWithdrawalVisibilityTests.java
@@ -6,6 +6,7 @@
import java.util.Map;
import com.talentcard.server.business.PageResult;
import com.talentcard.server.business.admin.AdminBusinessService;
import com.talentcard.server.business.admin.MerchantSettlementAdminService;
import com.talentcard.server.business.merchant.auth.MerchantPrincipal;
import org.junit.jupiter.api.BeforeEach;
@@ -14,6 +15,7 @@
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.transaction.annotation.Transactional;
/**
 * 提现状态可见性 seam:商户端和后台都只能看到「处理中 / 已到账 / 提现失败」三种状态,
@@ -31,6 +33,9 @@
    @Autowired
    private MerchantSettlementAdminService adminService;
    @Autowired
    private AdminBusinessService adminBusinessService;
    private Long merchantId;
    private Long storeId;
@@ -82,6 +87,50 @@
    }
    @Test
    void theAdminPageReturnsAnEmptyPageWhenNoWithdrawalsMatch() {
        PageResult<Map<String, Object>> page = adminService.withdrawals(
                1, 20, merchantName + " no matching merchant", null, null, null, null, null);
        assertThat(page.records()).isEmpty();
        assertThat(page.total()).isZero();
    }
    @Test
    @Transactional
    void theAdminRefreshLoadsWithdrawalsAndTracksStuckWithdrawalNotices() {
        // 刷新页面还会调用布局中的全局待办接口;只验证提现列表无法覆盖缺少提现关联表的错误。
        assertThat(adminService.withdrawals(1, 20, merchantName, null, null, null, null, null).records())
                .hasSize(4);
        Map<String, Object> originalNotice = stuckWithdrawalNotice();
        long originalCount = originalNotice.isEmpty() ? 0
                : Long.parseLong(String.valueOf(originalNotice.get("content")).replaceAll("\\D", ""));
        String orderNo = "NOTICE-" + suffix;
        jdbc.update("""
                insert into benefit_order(order_no, talent_id, merchant_id, store_id, order_type,
                                          pay_amount, order_status, verify_status, withdrawal_status, deleted)
                values (?, 9701, ?, ?, 'GROUP_BUY', 2000, 1, 1, 1, 0)
                """, orderNo, merchantId, storeId);
        Long orderId = jdbc.queryForObject("select id from benefit_order where order_no = ?", Long.class, orderNo);
        Long withdrawalId = jdbc.queryForObject(
                "select id from merchant_withdrawal where merchant_id = ? and status = 'failed'", Long.class, merchantId);
        jdbc.update("insert into merchant_withdrawal_order(withdrawal_id, order_id, amount_cents) values (?, ?, 2000)",
                withdrawalId, orderId);
        assertThat(stuckWithdrawalNotice())
                .containsEntry("content", "您有 %d 笔提现失败且金额已停止自动重试".formatted(originalCount + 1))
                .containsEntry("path", "/merchant-settlement?kind=withdrawals");
        jdbc.update("update merchant_withdrawal_order set released_at = now() where order_id = ?", orderId);
        assertThat(stuckWithdrawalNotice()).isEqualTo(originalNotice);
    }
    private Map<String, Object> stuckWithdrawalNotice() {
        return adminBusinessService.pendingNotices().stream()
                .filter(notice -> "merchant-withdrawal-stuck".equals(notice.get("id")))
                .findFirst().orElse(Map.of());
    }
    @Test
    void theAdminStatusFilterUsesTheThreeDisplayStates() {
        // 筛「处理中」要同时命中库里还没提交的 pending 和已提交的 processing。
        PageResult<Map<String, Object>> processing = adminService.withdrawals(