import com.talentcard.server.business.payment.huifu.HuifuPaymentProperties; import com.talentcard.server.business.payment.huifu.HuifuSettlementRecordGateway; import java.io.IOException; import java.io.Reader; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.Properties; /** * 只读排查探针:问通道「钱到底结了多少到这家商户的银行卡」,按日期区间直查结算记录。 * *
出款改由斗拱按结算周期自动完成之后,平台侧不再有提现记录,「每天结了多少」只有通道知道。 */ public class SettlementRecordProbe { /** * 通道对 {@code pageSize} 的硬上限,与 {@code HuifuSettlementRecordGateway.PROVIDER_MAX_PAGE_SIZE} 同一个值。 * *
实测超过 50 时接口直接返回 {@code 00000003} * 「基础参数校验失败:【pageSize-分页条数】不能为空,且最小值为0,最大值为50」,一条记录都拿不到。 * 后台此前默认传 100,于是「结算记录」页永远空白,而通道里其实有流水。本探针同样必须钉住这个上限, * 不是照原样透传调用方给的值。 */ private static final int PROVIDER_MAX_PAGE_SIZE = 50; private static final DateTimeFormatter REQUEST_DATE = DateTimeFormatter.ofPattern("yyyyMMdd"); /** * 回答什么问题:给定汇付号与日期区间,通道 {@code /v2/merchant/basicdata/settlement/query} 里这家商户 * 结了多少笔、多少钱、结算到哪张卡(卡号一律不打印)。 * *
什么时候用它:商户说「钱没到账」、或者后台「结算记录」页空白时——先确认通道侧到底有没有结算流水, * 再区分是「还没到结算周期」还是「通道里根本没有这笔钱的流水」(后者要去查支付与资金确认)。 * *
它不改任何状态:只调用通道的结算记录查询接口,不发起支付、不关单、不退款、不交易确认、 * 不下发业务开通、连本地数据库都不连。 * *
参数格式(全部从命令行传,缺参数直接打用法退出,不用任何默认值瞎猜): *
* --config=<汇付配置文件路径> 必填:通道查询要先有 system-id / product-id / 私钥 / 平台公钥
* --huifu-id=<斗拱商户号> 必填
* --begin-date=yyyyMMdd --end-date=yyyyMMdd 必填
* [--page-size=<1..50>] 默认 50(通道上限);越界值会被收敛,不会被发到通道
* [--huifu-environment=test|prod]
*
*/
public static void main(String[] args) {
String configPath = required(args, "config");
String huifuId = required(args, "huifu-id");
String beginDate = required(args, "begin-date");
String endDate = required(args, "end-date");
int requested = intValue(arg(args, "page-size"), PROVIDER_MAX_PAGE_SIZE);
int pageSize = Math.min(Math.max(requested, 1), PROVIDER_MAX_PAGE_SIZE);
requireBasicDate("--begin-date", beginDate);
requireBasicDate("--end-date", endDate);
HuifuPaymentProperties properties = loadProperties(configPath, arg(args, "huifu-environment"));
warnIfProduction(properties);
System.out.println("只读探针:只调用通道的结算记录查询接口。不发起支付、不关单、不退款、不交易确认、"
+ "不下发业务开通、不连本地数据库、不改任何状态。");
System.out.println();
System.out.println("== 请求 ==");
line("huifuId ", maskId(huifuId));
line("区间 ", beginDate + " ~ " + endDate);
line("pageSize", pageSize + (pageSize == requested ? "" : "(你要的 " + requested + " 已收敛到 1..50)"));
System.out.println(" 说明:通道对 pageSize 的硬上限就是 50,超了直接回 00000003 且一条都不返回,"
+ "所以这里不会原样透传。要看得更多只能缩小日期窗口,通道不返回总页数。");
HuifuSettlementRecordGateway.SettlementRecords records;
try {
records = new HuifuSettlementRecordGateway(properties).query(huifuId, beginDate, endDate, pageSize);
} catch (RuntimeException exception) {
System.out.println();
System.out.println("== 结果 ==");
System.out.println("查询失败:" + exception.getClass().getSimpleName() + " " + text(exception.getMessage()));
System.out.println("常见读法:00000003 是参数校验失败(先看参数,不一定是 pageSize);"
+ "通道说商户不存在就核对 --huifu-id 与 app.payment.huifu.environment 是否配套"
+ "(测试环境的产品号问不到生产商户)。");
System.out.println("查不通不代表没有结算流水:钱的问题仍以支付与资金确认那两处为准。");
return;
}
System.out.println();
System.out.println("== 结果 ==");
line("succeeded ", String.valueOf(records.succeeded()));
line("resp_code ", text(records.code()));
line("message ", text(records.message()));
line("result_count", String.valueOf(records.total()));
if (!records.succeeded()) {
System.out.println("通道没有给出成功回执。它可能什么都没查,也可能查了但整体被拒——"
+ "不要在这种时候下「没有结算流水」的结论。");
}
System.out.println();
System.out.println("结算记录(trans_log_result_list,已对商户号/卡号脱敏):");
System.out.println(sanitizeRaw(records.rawRecords()));
System.out.println();
System.out.println("读法:");
System.out.println(" * 通道里没有记录而商户说没到账 → 先看订单的 verify_status 与 funds_confirmation_status:"
+ "未核销的钱还在通道延迟户,不会进结算。");
System.out.println(" * result_count 打满 pageSize 时,这个窗口里很可能还有下一页——缩小日期窗口再查一次。");
System.out.println(" * 结算按商户(huifuId)维度、跨门店汇总;对不上时核对收款的子商户号"
+ "(benefit_order.collect_huifu_id / payment_transaction.provider_huifu_id)。");
}
// ---------- 参数、配置与输出 ----------
/** 读汇付配置。不读 application.yml:私钥与密钥只在本探针内部使用,从不打印。 */
private static HuifuPaymentProperties loadProperties(String path, String environmentOverride) {
Properties source = new Properties();
try (Reader reader = Files.newBufferedReader(Path.of(path), StandardCharsets.UTF_8)) {
source.load(reader);
} catch (IOException exception) {
System.out.println("读不了配置文件 " + path + ":" + exception.getMessage());
usage();
}
HuifuPaymentProperties properties = new HuifuPaymentProperties();
String enabled = value(source, "enabled");
properties.setEnabled(enabled.isBlank() || Boolean.parseBoolean(enabled));
properties.setEnvironment(firstNonBlank(environmentOverride, firstNonBlank(value(source, "environment"), "test")));
apply(value(source, "system-id"), properties::setSystemId);
apply(value(source, "product-id"), properties::setProductId);
apply(value(source, "merchant-id"), properties::setMerchantId);
apply(value(source, "private-key"), properties::setPrivateKey);
apply(value(source, "platform-public-key"), properties::setPlatformPublicKey);
apply(value(source, "notify-url"), properties::setNotifyUrl);
System.out.println("配置状态(只印有无,私钥与公钥从不打印):system-id=" + configured(properties.getSystemId())
+ " product-id=" + configured(properties.getProductId())
+ " private-key=" + configured(properties.getPrivateKey())
+ " platform-public-key=" + configured(properties.getPlatformPublicKey())
+ " enabled=" + properties.isEnabled()
+ " environment=" + properties.getEnvironment());
return properties;
}
private static String configured(String value) {
return value == null || value.isBlank() ? "未配置" : "已配置";
}
/** 键名兼容 {@code system-id} 与 {@code app.payment.huifu.system-id} 两种写法(后者照抄部署配置)。 */
private static String value(Properties source, String key) {
return firstNonBlank(source.getProperty("app.payment.huifu." + key), source.getProperty(key));
}
private static String firstNonBlank(String first, String second) {
if (first != null && !first.isBlank()) return first.trim();
return second == null ? "" : second.trim();
}
private static void apply(String value, java.util.function.Consumer