yeg
2026-09-12 d32e0dee609ab13220908ab28a11390bede07c84
talent-card-server/src/main/java/com/talentcard/server/business/admin/MerchantSettlementAdminService.java
@@ -7,22 +7,28 @@
import com.talentcard.server.business.DatabaseRows;
import com.talentcard.server.business.PageResult;
import com.talentcard.server.business.merchant.AutoWithdrawalResultService;
import com.talentcard.server.common.BusinessException;
import com.talentcard.server.common.ErrorCode;
import com.talentcard.server.common.SensitiveDataCrypto;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.talentcard.server.business.merchant.HuifuMerchantWithdrawalService;
import com.talentcard.server.business.merchant.MerchantPaymentReadinessService;
/**
 * 后台结算管理。
 *
 * <p>后台只能查看进件、银行卡和提现记录,不能修改提现状态:提现记录的状态只由斗拱的异步通知或主动
 * 查询收敛(见 {@code AutoWithdrawalResultService})。进件和银行卡的审核入口在这一版已经下线,
 * 后台保留查询与能力快照刷新。
 */
@Service
public class MerchantSettlementAdminService {
    private final JdbcTemplate jdbc;
    private final HuifuMerchantWithdrawalService withdrawalService;
    private final SensitiveDataCrypto sensitiveDataCrypto;
    private final MerchantPaymentReadinessService readinessService;
    public MerchantSettlementAdminService(JdbcTemplate jdbc, HuifuMerchantWithdrawalService withdrawalService, SensitiveDataCrypto sensitiveDataCrypto, MerchantPaymentReadinessService readinessService) { this.jdbc = jdbc; this.withdrawalService = withdrawalService; this.sensitiveDataCrypto = sensitiveDataCrypto; this.readinessService = readinessService; }
    public MerchantSettlementAdminService(JdbcTemplate jdbc, SensitiveDataCrypto sensitiveDataCrypto, MerchantPaymentReadinessService readinessService) { this.jdbc = jdbc; this.sensitiveDataCrypto = sensitiveDataCrypto; this.readinessService = readinessService; }
    @Transactional(readOnly = true)
    public PageResult<Map<String, Object>> onboardings(int page, int size, String merchantName, String contactMobile, String merchantType, String status, String startDate, String endDate) {
@@ -50,16 +56,33 @@
        });
        return result;
    }
    /**
     * 提现记录只读:状态由斗拱的通知或查询收敛,后台不能人工审核或改写。列表只展示三种对外状态,
     * 因此筛选值也按「处理中 / 已到账 / 提现失败」翻译成库里的原始状态。
     */
    @Transactional(readOnly = true)
    public PageResult<Map<String, Object>> withdrawals(int page, int size, String merchantName, String contactMobile, String merchantType, String status, String startDate, String endDate) {
        String available = "greatest(0, coalesce((select sum(o.pay_amount) from benefit_order o where o.merchant_id = w.merchant_id and (o.store_id is null or o.store_id = w.store_id) and o.order_status = 1 and o.verify_status = 1 and o.refund_status = 0 and o.withdrawal_status = 0 and o.deleted = 0), 0) - coalesce((select sum(w2.amount_cents) from merchant_withdrawal w2 where w2.merchant_id = w.merchant_id and w2.store_id = w.store_id and w2.status in ('pending', 'processing', 'paid')), 0))";
        String select = "select w.*, c.bank_name, c.card_masked, m.merchant_name, m.merchant_type, m.contact_mobile, s.store_name, " + available + " as available_amount_cents from merchant_withdrawal w join merchant_bank_card c on c.id = w.bank_card_id join merchant m on m.id = w.merchant_id join merchant_store s on s.id = w.store_id";
        String count = "select count(*) from merchant_withdrawal w join merchant_bank_card c on c.id = w.bank_card_id join merchant m on m.id = w.merchant_id join merchant_store s on s.id = w.store_id";
        return page(select, count, "w", page, size, merchantName, contactMobile, merchantType, status, startDate, endDate);
        PageResult<Map<String, Object>> result = page(select, count, "w", page, size, merchantName, contactMobile,
                merchantType, null, startDate, endDate, withdrawalStatusCondition(status));
        result.records().forEach(row -> row.put("status",
                AutoWithdrawalResultService.displayStatus(row.get("status") == null ? "" : String.valueOf(row.get("status")))));
        return result;
    }
    @Transactional public Map<String, Object> reviewBankCard(Long id, String status, String reason, String tokenNo) { return reviewBankCardInternal(id, status, reason, tokenNo); }
    public Map<String, Object> reviewWithdrawal(Long id, String status, String reason) { if ("paid".equals(normalizeWithdrawalStatus(status))) return withdrawalService.dispatch(id); return update("merchant_withdrawal", id, "failed", reason, "withdrawal"); }
    /**
     * 对外三种状态到库里原始状态的映射条件,由 {@link AutoWithdrawalResultService#rawStatuses} 推导。
     * 没有筛选时不加条件;筛选取值不认识是「筛不出记录」,不是「忽略筛选」。
     */
    private String withdrawalStatusCondition(String status) {
        if (status == null || status.isBlank()) return "";
        List<String> rawStatuses = AutoWithdrawalResultService.rawStatuses(status);
        if (rawStatuses.isEmpty()) return " and 1 = 0";
        return " and w.status in (" + rawStatuses.stream().map(value -> "'" + value + "'")
                .collect(java.util.stream.Collectors.joining(", ")) + ")";
    }
    /** Provider-confirmed settlement and encashment capability of one approved sub-merchant. */
    public Map<String, Object> paymentCapability(Long applicationId) { return readinessService.capability(applicationId); }
@@ -67,14 +90,14 @@
    /** Re-reads the provider configuration and stores a fresh capability snapshot. */
    public Map<String, Object> refreshPaymentCapability(Long applicationId) { return readinessService.refresh(applicationId); }
    private Map<String,Object> reviewBankCardInternal(Long id,String status,String reason,String tokenNo) { String value=reviewStatus(status); if("approved".equals(value) && (tokenNo==null || !tokenNo.trim().matches("[A-Za-z0-9]{1,32}"))) throw new BusinessException(ErrorCode.BAD_REQUEST,"Huifu withdrawal card token is required when approving a bank card"); int affected=jdbc.update("update merchant_bank_card set status=?, reject_reason=?, provider_token_no=?, reviewed_at=now() where id=? and status='pending'",value,reason==null?"":reason.trim(),"approved".equals(value)?tokenNo.trim():null,id); if(affected==0)throw new BusinessException(ErrorCode.CONFLICT,"Bank card is already handled or does not exist");return Map.of("id",id,"status",value); }
    private String reviewStatus(String status) { String value=status==null?"":status.trim().toLowerCase(); if(!List.of("approved","rejected").contains(value))throw new BusinessException(ErrorCode.BAD_REQUEST,"Invalid review status");return value; }
    private String normalizeWithdrawalStatus(String status) { String value=status==null?"":status.trim().toLowerCase(); if(!List.of("paid","failed").contains(value))throw new BusinessException(ErrorCode.BAD_REQUEST,"Invalid withdrawal status");return value; }
    private PageResult<Map<String, Object>> page(String query, String count, String tableAlias, int page, int size, String merchantName, String contactMobile, String merchantType, String status, String startDate, String endDate) {
        return page(query, count, tableAlias, page, size, merchantName, contactMobile, merchantType, status, startDate, endDate, "");
    }
    private PageResult<Map<String, Object>> page(String query, String count, String tableAlias, int page, int size, String merchantName, String contactMobile, String merchantType, String status, String startDate, String endDate, String extraWhere) {
        int current = Math.max(1, page);
        int limit = Math.min(Math.max(1, size), 100);
        StringBuilder where = new StringBuilder(" where 1 = 1");
        StringBuilder where = new StringBuilder(" where 1 = 1" + extraWhere);
        List<Object> args = new ArrayList<>();
        addLike(where, args, "m.merchant_name", merchantName);
        addLike(where, args, "m.contact_mobile", contactMobile);
@@ -117,17 +140,6 @@
            case "513400" -> "四川省|凉山彝族自治州";
            default -> cityCode == null ? "" : String.valueOf(cityCode).trim();
        };
    }
    private Map<String, Object> update(String table, Long id, String status, String reason, String label) {
        String value = status == null ? "" : status.trim().toLowerCase();
        if (!List.of("approved", "rejected", "paid", "failed").contains(value)) throw new BusinessException(ErrorCode.BAD_REQUEST, "审核状态无效");
        if ("merchant_onboarding_application".equals(table) && !List.of("approved", "rejected").contains(value)) throw new BusinessException(ErrorCode.BAD_REQUEST, "进件状态无效");
        if ("merchant_bank_card".equals(table) && !List.of("approved", "rejected").contains(value)) throw new BusinessException(ErrorCode.BAD_REQUEST, "银行卡状态无效");
        int affected;
        if ("merchant_withdrawal".equals(table)) affected = jdbc.update("update merchant_withdrawal set status = ?, failure_reason = ?, reviewed_at = now(), paid_at = case when ? = 'paid' then now() else paid_at end where id = ? and status in ('pending','processing')", value, reason == null ? "" : reason.trim(), value, id);
        else affected = jdbc.update("update " + table + " set status = ?, reject_reason = ?, reviewed_at = now() where id = ? and status = 'pending'", value, reason == null ? "" : reason.trim(), id);
        if (affected == 0) throw new BusinessException(ErrorCode.CONFLICT, "该" + label + "已处理或不存在");
        return Map.of("id", id, "status", value);
    }
    private List<Map<String, Object>> rows(String sql, Object... args) { return DatabaseRows.normalize(jdbc.queryForList(sql, args)); }
}