package com.talentcard.server.business.admin;
|
|
import java.time.LocalDate;
|
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.business.merchant.AutoWithdrawalAggregationService;
|
import com.talentcard.server.business.merchant.AutoWithdrawalResultService;
|
import com.talentcard.server.business.merchant.AutoWithdrawalScheduler;
|
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.MerchantPaymentReadinessService;
|
|
/**
|
* 后台结算管理。
|
*
|
* <p>后台只能查看进件、银行卡和提现记录,不能修改提现状态:提现记录的状态只由斗拱的异步通知或主动
|
* 查询收敛(见 {@code AutoWithdrawalResultService})。进件和银行卡的审核入口在这一版已经下线,
|
* 后台保留查询与能力快照刷新。
|
*/
|
@Service
|
public class MerchantSettlementAdminService {
|
private final JdbcTemplate jdbc;
|
private final SensitiveDataCrypto sensitiveDataCrypto;
|
private final MerchantPaymentReadinessService readinessService;
|
private final AutoWithdrawalScheduler autoWithdrawalScheduler;
|
public MerchantSettlementAdminService(JdbcTemplate jdbc, SensitiveDataCrypto sensitiveDataCrypto, MerchantPaymentReadinessService readinessService, AutoWithdrawalScheduler autoWithdrawalScheduler) { this.jdbc = jdbc; this.sensitiveDataCrypto = sensitiveDataCrypto; this.readinessService = readinessService; this.autoWithdrawalScheduler = autoWithdrawalScheduler; }
|
|
/**
|
* 按业务日补跑一次日汇总并立刻提交、收敛结果。
|
*
|
* <p>定时任务汇总的是「前一自然日」,当天核销的订单要等到第二天凌晨才会出账;排查和验收都等不起,
|
* 所以这里提供一次显式补跑。它不接受「不传日期」:隐式取当天容易误刷历史,日期必须由调用方给定。
|
*/
|
public Map<String, Object> runAutoWithdrawalAggregation(LocalDate businessDate) {
|
if (businessDate == null) throw new BusinessException(ErrorCode.BAD_REQUEST, "请指定业务日 businessDate(yyyyMMdd)");
|
AutoWithdrawalAggregationService.Result result = autoWithdrawalScheduler.runForBusinessDate(businessDate);
|
Map<String, Object> view = new LinkedHashMap<>();
|
view.put("businessDate", result.businessDate());
|
view.put("merchantsAggregated", result.merchantsAggregated());
|
view.put("merchantsSkipped", result.merchantsSkipped());
|
view.put("merchantsFailed", result.merchantsFailed());
|
view.put("ordersAggregated", result.ordersAggregated());
|
view.put("amountCents", result.amountCents());
|
return view;
|
}
|
|
@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) {
|
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<String, Object> onboarding(Long id) {
|
List<Map<String, Object>> rows = rows("select a.*, a.merchant_name as onboarding_merchant_name, 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<String, Object> 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<Map<String, Object>> bankCards(int page, int size, String merchantName, String contactMobile, String merchantType, String status, String startDate, String endDate) {
|
PageResult<Map<String, Object>> 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<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";
|
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;
|
}
|
|
/**
|
* 对外三种状态到库里原始状态的映射条件,由 {@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); }
|
|
/** Re-reads the provider configuration and stores a fresh capability snapshot. */
|
public Map<String, Object> refreshPaymentCapability(Long applicationId) { return readinessService.refresh(applicationId); }
|
|
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" + extraWhere);
|
List<Object> 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<Object> rowArgs = new ArrayList<>(args);
|
rowArgs.add(limit);
|
rowArgs.add((current - 1) * limit);
|
List<Map<String, Object>> 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<Object> 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<Object> 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<Object> 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 List<Map<String, Object>> rows(String sql, Object... args) { return DatabaseRows.normalize(jdbc.queryForList(sql, args)); }
|
}
|