import com.alibaba.fastjson.JSON;
import com.talentcard.server.business.payment.huifu.HuifuMiniProgramPaymentGateway;
import com.talentcard.server.business.payment.huifu.HuifuPayload;
import com.talentcard.server.business.payment.huifu.HuifuPayload.TransStatus;
import com.talentcard.server.business.payment.huifu.HuifuPaymentCloseQueryCommand;
import com.talentcard.server.business.payment.huifu.HuifuPaymentProperties;
import com.talentcard.server.business.payment.huifu.HuifuPaymentQueryCommand;
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.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
/**
* 只读排查探针:一笔支付在本地库里是什么结论,在通道侧又是什么结论,两者差在哪。
*
*
通道侧的支付状态是权威事实,本地状态只是它的副本;两者不一致时,先看清差在哪里,再决定要不要让
* 对账去收敛它(后台订单列表的「立即对账」跑的是同一套真实逻辑)。
*/
public class PaymentStateProbe {
/** 公网 IP 只留主机段前两段:回调/查询地址是公开可达的,整串贴出来等于把入口交出去。 */
private static final Pattern AUTHORITY_IPV4 = Pattern.compile("^(\\d{1,3})\\.(\\d{1,3})\\.\\d{1,3}\\.\\d{1,3}(:\\d+)?$");
private static final DateTimeFormatter REQUEST_DATE = DateTimeFormatter.ofPattern("yyyyMMdd");
/**
* 关单在通道侧是异步的,结论要另问一次关单查询(汇付文档:关单处理中应继续调用关单查询)。
* 只有这两个本地状态值得问:其余状态下本地已经有结论,问了也多不出信息。
*/
private static final Set CLOSE_QUERY_STATUSES = Set.of("CLOSING", "CANCEL_UNCONFIRMED");
/** 通道「交易不存在」:它不认识这个请求流水号,也就不可能为它收过钱。 */
private static final String TRADE_MISSING = "23000001";
/**
* 回答什么问题:给定订单号或支付流水号,本地 {@code payment_transaction} 是什么状态,通道侧这笔
* 交易现在是什么状态,两者差在哪(含金额、收款子商户、关单结论)。
*
* 什么时候用它:用户说「钱扣了订单没变」「取消了还能付」「一直显示支付处理中」,而回调审计表
* ({@code payment_callback_event})看不出结论时——它把本地与通道摆在一起,差异一眼可见。
*
*
它不改任何状态:只做 SELECT,通道侧只调查单与关单查询,不发起支付、不关单、不退款、不交易确认、
* 不下发业务开通、不写库。查询本身会落在通道的调用记录里,是唯一的「痕迹」。
*
*
参数格式(全部从命令行传,缺参数直接打用法退出,不用任何默认值瞎猜):
*
* --db-url=jdbc:mysql://<host>:<port>/<db>?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false
* --db-user=<用户> --db-password=<口令>
* --order-no=<订单号> 与 --payment-no 二选一
* --payment-no=<支付流水号>
* [--config=<汇付配置文件路径>] 不给就只查本地、不问通道
* [--huifu-environment=test|prod] 覆盖配置文件里的 environment
*
*/
public static void main(String[] args) {
String dbUrl = required(args, "db-url");
String dbUser = required(args, "db-user");
String dbPassword = required(args, "db-password");
String orderNo = arg(args, "order-no");
String paymentNo = arg(args, "payment-no");
if (blank(orderNo) == blank(paymentNo)) {
System.out.println("必须且只能给一个:--order-no=<订单号> 或 --payment-no=<支付流水号>。");
usage();
}
String configPath = arg(args, "config");
HuifuPaymentProperties properties = blank(configPath) ? null : loadProperties(configPath, arg(args, "huifu-environment"));
warnIfProduction(properties, "app.payment.huifu.environment");
System.out.println("只读探针:只做 SELECT 与通道的查询类接口(查单 / 关单查询)。不发起支付、不关单、不退款、"
+ "不交易确认、不下发业务开通、不改任何数据库行。");
System.out.println("数据库目标:" + maskHost(dbUrl));
if (!dbUrl.toLowerCase(Locale.ROOT).contains("servertimezone")) {
System.out.println("! 提示:--db-url 上没有 serverTimezone。org_req_date 由 created_at 推出来,"
+ "时区解释不同会整体偏一天,通道那边只会回「交易不存在」——那会看起来像「钱丢了」。");
}
JdbcTemplate jdbc = connect(dbUrl, dbUser, dbPassword);
String key = !blank(orderNo) ? orderNo.trim() : paymentNo.trim();
List