package com.talentcard.server.business.admin; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import com.talentcard.server.business.DatabaseRows; import com.talentcard.server.business.PageResult; 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; @Service public class MerchantSettlementAdminService { private final JdbcTemplate jdbc; private final HuifuMerchantWithdrawalService withdrawalService; private final SensitiveDataCrypto sensitiveDataCrypto; public MerchantSettlementAdminService(JdbcTemplate jdbc, HuifuMerchantWithdrawalService withdrawalService, SensitiveDataCrypto sensitiveDataCrypto) { this.jdbc = jdbc; this.withdrawalService = withdrawalService; this.sensitiveDataCrypto = sensitiveDataCrypto; } @Transactional(readOnly = true) public PageResult> onboardings(int page, int size, String merchantName, String contactMobile, String merchantType, String status, String startDate, String endDate) { return page("select a.*, m.merchant_name, m.merchant_type, m.contact_mobile, s.store_name from merchant_onboarding_application a join merchant m on m.id = a.merchant_id join merchant_store s on s.id = a.store_id", "select count(*) from merchant_onboarding_application a join merchant m on m.id = a.merchant_id join merchant_store s on s.id = a.store_id", "a", page, size, merchantName, contactMobile, merchantType, status, startDate, endDate); } @Transactional(readOnly = true) public Map onboarding(Long id) { List> rows = rows("select a.*, m.merchant_name, m.merchant_type, m.contact_mobile, s.store_name from merchant_onboarding_application a join merchant m on m.id = a.merchant_id join merchant_store s on s.id = a.store_id where a.id = ?", id); if (rows.isEmpty()) throw new BusinessException(ErrorCode.NOT_FOUND, "商户进件不存在"); Map result = rows.get(0); // 个人主体的历史记录可能使用 individual、personal 或中文类型值; // 只要已保存加密身份证号,后台详情均应解密后完整展示。 String encrypted = String.valueOf(result.get("subjectIdNoEncrypted")); if (!encrypted.isBlank() && !"null".equals(encrypted)) result.put("subjectIdNo", sensitiveDataCrypto.decrypt(encrypted)); return result; } @Transactional(readOnly = true) public PageResult> bankCards(int page, int size, String merchantName, String contactMobile, String merchantType, String status, String startDate, String endDate) { PageResult> result = page("select c.*, m.merchant_name, m.merchant_type, m.contact_mobile, s.store_name from merchant_bank_card c join merchant m on m.id = c.merchant_id join merchant_store s on s.id = c.store_id", "select count(*) from merchant_bank_card c join merchant m on m.id = c.merchant_id join merchant_store s on s.id = c.store_id", "c", page, size, merchantName, contactMobile, merchantType, status, startDate, endDate); result.records().forEach(row -> { String encrypted = String.valueOf(row.remove("cardNoEncrypted")); if (!encrypted.isBlank() && !"null".equals(encrypted)) row.put("cardNo", sensitiveDataCrypto.decrypt(encrypted)); else row.put("cardNo", row.get("cardMasked")); row.put("cityName", bankCardCityName(row.get("cityCode"))); }); return result; } @Transactional(readOnly = true) public PageResult> 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.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); } @Transactional public Map reviewBankCard(Long id, String status, String reason, String tokenNo) { return reviewBankCardInternal(id, status, reason, tokenNo); } public Map reviewWithdrawal(Long id, String status, String reason) { if ("paid".equals(normalizeWithdrawalStatus(status))) return withdrawalService.dispatch(id); return update("merchant_withdrawal", id, "failed", reason, "withdrawal"); } private Map 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> page(String query, String count, String tableAlias, int page, int size, String merchantName, String contactMobile, String merchantType, String status, String startDate, String endDate) { int current = Math.max(1, page); int limit = Math.min(Math.max(1, size), 100); StringBuilder where = new StringBuilder(" where 1 = 1"); List args = new ArrayList<>(); addLike(where, args, "m.merchant_name", merchantName); addLike(where, args, "m.contact_mobile", contactMobile); addEqual(where, args, "m.merchant_type", merchantType); addEqual(where, args, tableAlias + ".status", status); addDate(where, args, tableAlias + ".created_at", startDate, true); addDate(where, args, tableAlias + ".created_at", endDate, false); List rowArgs = new ArrayList<>(args); rowArgs.add(limit); rowArgs.add((current - 1) * limit); List> values = rows(query + where + " order by " + tableAlias + ".created_at desc, " + tableAlias + ".id desc limit ? offset ?", rowArgs.toArray()); Long total = jdbc.queryForObject(count + where, Long.class, args.toArray()); return new PageResult<>(values, current, limit, total == null ? 0 : total); } private void addLike(StringBuilder where, List args, String column, String value) { if (value != null && !value.trim().isEmpty()) { where.append(" and ").append(column).append(" like ?"); args.add("%" + value.trim() + "%"); } } private void addEqual(StringBuilder where, List args, String column, String value) { if (value != null && !value.trim().isEmpty()) { where.append(" and ").append(column).append(" = ?"); args.add(value.trim()); } } private void addDate(StringBuilder where, List args, String column, String value, boolean start) { if (value != null && value.matches("\\d{4}-\\d{2}-\\d{2}")) { where.append(" and ").append(column).append(start ? " >= ?" : " < date_add(?, interval 1 day)"); args.add(value); } } private String bankCardCityName(Object cityCode) { return switch (cityCode == null ? "" : String.valueOf(cityCode).trim()) { case "510100" -> "四川省|成都市"; case "510300" -> "四川省|自贡市"; case "510400" -> "四川省|攀枝花市"; case "510500" -> "四川省|泸州市"; case "510600" -> "四川省|德阳市"; case "510700" -> "四川省|绵阳市"; case "510800" -> "四川省|广元市"; case "510900" -> "四川省|遂宁市"; case "511000" -> "四川省|内江市"; case "511100" -> "四川省|乐山市"; case "511300" -> "四川省|南充市"; case "511400" -> "四川省|眉山市"; case "511500" -> "四川省|宜宾市"; case "511600" -> "四川省|广安市"; case "511700" -> "四川省|达州市"; case "511800" -> "四川省|雅安市"; case "511900" -> "四川省|巴中市"; case "512000" -> "四川省|资阳市"; case "513200" -> "四川省|阿坝藏族羌族自治州"; case "513300" -> "四川省|甘孜藏族自治州"; case "513400" -> "四川省|凉山彝族自治州"; default -> cityCode == null ? "" : String.valueOf(cityCode).trim(); }; } private Map 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> rows(String sql, Object... args) { return DatabaseRows.normalize(jdbc.queryForList(sql, args)); } }