package com.talentcard.server.business.admin;
|
|
import java.util.ArrayList;
|
import java.util.LinkedHashMap;
|
import java.util.List;
|
import java.util.Map;
|
|
import com.alibaba.fastjson.JSON;
|
import com.talentcard.server.business.DatabaseRows;
|
import com.talentcard.server.business.PageResult;
|
import com.talentcard.server.business.payment.huifu.HuifuSettlementRecordGateway;
|
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>出款由斗拱按结算周期自动完成(核销确认后的钱进商户基本户,次日自动结算到结算卡),
|
* 平台不再产生提现记录。后台保留进件、银行卡查询、收款能力快照刷新,以及向通道查询
|
* 真实结算记录用于监控每天结了多少到银行卡。
|
*/
|
@Service
|
public class MerchantSettlementAdminService {
|
private final JdbcTemplate jdbc;
|
private final SensitiveDataCrypto sensitiveDataCrypto;
|
private final MerchantPaymentReadinessService readinessService;
|
private final HuifuSettlementRecordGateway settlementRecordGateway;
|
public MerchantSettlementAdminService(JdbcTemplate jdbc, SensitiveDataCrypto sensitiveDataCrypto, MerchantPaymentReadinessService readinessService, HuifuSettlementRecordGateway settlementRecordGateway) { this.jdbc = jdbc; this.sensitiveDataCrypto = sensitiveDataCrypto; this.readinessService = readinessService; this.settlementRecordGateway = settlementRecordGateway; }
|
|
/**
|
* 结算记录:钱按结算周期自动划到结算卡之后,「每天结了多少」只能问通道。
|
*
|
* <p>按门店取该商户当前生效的斗拱子商户号,逐商户查询结算记录后合并成一条时间倒序的列表。
|
* 通道查询失败只影响这一条商户,不会让整页失败——监控页最重要的是「看得到」。
|
*
|
* <p>条数上限跟着通道走:这个接口的 {@code pageSize} 超过 50 会被判为基础参数校验失败,
|
* 传 100 时整页一条流水都查不出来。上限在网关里还有一道兜底,这里只是不要再主动越界。
|
*/
|
public Map<String, Object> settlements(String beginDate, String endDate, int size) {
|
List<Map<String, Object>> merchants = rows("""
|
select a.merchant_id, a.provider_huifu_id, m.merchant_name, 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.status = 'approved' and a.provider_huifu_id is not null and a.provider_huifu_id <> ''
|
and a.id = (select max(newest.id) from merchant_onboarding_application newest
|
where newest.merchant_id = a.merchant_id)
|
order by a.merchant_id asc
|
""");
|
List<Map<String, Object>> records = new ArrayList<>();
|
List<String> failures = new ArrayList<>();
|
int limit = Math.min(Math.max(size, 1), HuifuSettlementRecordGateway.PROVIDER_MAX_PAGE_SIZE);
|
for (Map<String, Object> merchant : merchants) {
|
String huifuId = String.valueOf(merchant.get("providerHuifuId"));
|
try {
|
HuifuSettlementRecordGateway.SettlementRecords response =
|
settlementRecordGateway.query(huifuId, beginDate, endDate, limit);
|
if (!response.succeeded()) {
|
failures.add(String.valueOf(merchant.get("merchantName")) + ":" + response.message());
|
continue;
|
}
|
records.addAll(decode(response.rawRecords(), merchant));
|
} catch (RuntimeException exception) {
|
failures.add(String.valueOf(merchant.get("merchantName")) + ":查询失败");
|
}
|
}
|
records.sort((left, right) -> String.valueOf(right.get("settleDate"))
|
.compareTo(String.valueOf(left.get("settleDate"))));
|
Map<String, Object> view = new LinkedHashMap<>();
|
view.put("beginDate", beginDate);
|
view.put("endDate", endDate);
|
view.put("records", records.size() > limit ? records.subList(0, limit) : records);
|
view.put("total", records.size());
|
view.put("failures", failures);
|
return view;
|
}
|
|
/** 把通道返回的结算记录 JSON 转成后台要展示的字段;只读展示,不做金额换算以外的加工。 */
|
private List<Map<String, Object>> decode(String rawRecords, Map<String, Object> merchant) {
|
List<Map<String, Object>> decoded = new ArrayList<>();
|
if (rawRecords == null || rawRecords.isBlank()) return decoded;
|
List<Map<String, Object>> parsed;
|
try {
|
parsed = JSON.parseObject(rawRecords, new com.alibaba.fastjson.TypeReference<>() { });
|
} catch (RuntimeException exception) {
|
return decoded;
|
}
|
if (parsed == null) return decoded;
|
for (Map<String, Object> record : parsed) {
|
Map<String, Object> row = new LinkedHashMap<>();
|
row.put("merchantId", merchant.get("merchantId"));
|
row.put("merchantName", merchant.get("merchantName"));
|
row.put("storeName", merchant.get("storeName"));
|
row.put("providerTransactionNo", record.get("trans_id"));
|
row.put("settleDate", record.get("trans_date"));
|
row.put("settleAmountCents", cents(record.get("trans_amt")));
|
row.put("feeAmountCents", cents(record.get("fee_amt")));
|
row.put("settleCycle", record.get("settle_cycle"));
|
row.put("status", record.get("trans_stat"));
|
row.put("cardMasked", record.get("card_no"));
|
row.put("cardHolder", record.get("card_name"));
|
decoded.add(row);
|
}
|
return decoded;
|
}
|
|
private Long cents(Object yuan) {
|
if (yuan == null) return null;
|
return com.talentcard.server.business.payment.huifu.HuifuPayload.amountInCent(String.valueOf(yuan));
|
}
|
|
@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;
|
}
|
/** 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)); }
|
}
|