import java.nio.charset.StandardCharsets;
|
import java.nio.file.Files;
|
import java.nio.file.Path;
|
import java.sql.Connection;
|
import java.sql.DriverManager;
|
import java.sql.PreparedStatement;
|
import java.sql.ResultSet;
|
import java.sql.ResultSetMetaData;
|
import java.util.LinkedHashMap;
|
import java.util.List;
|
import java.util.Map;
|
import java.util.regex.Matcher;
|
import java.util.regex.Pattern;
|
|
import com.talentcard.server.business.BusinessIdGenerator;
|
import com.talentcard.server.business.payment.huifu.HuifuMiniProgramPaymentGateway;
|
import com.talentcard.server.business.payment.huifu.HuifuPaymentProperties;
|
import com.talentcard.server.business.payment.huifu.HuifuPaymentQueryCommand;
|
import com.talentcard.server.business.payment.huifu.HuifuRefundCommand;
|
import com.talentcard.server.business.payment.huifu.HuifuRefundGateway;
|
|
/**
|
* 一次性工具:把 2026-09-16 旧取消流程遗留在通道延迟户里的两笔 ¥0.02 退回来(订单 520/521)。
|
*
|
* <p>当时取消只看本地流水(写成 LOCAL_CLOSED),钱其实已经被通道收下并停在延迟户;本地既没有
|
* SUCCEEDED 流水,也没有退款记录,人才端显示「已取消」。这里按仓库自己的网关类调接口:
|
* 先查单确认钱在,再发起全额退款,成功后把本地账补成与通道一致(流水置 SUCCEEDED、补退款记录、
|
* 订单退出结算范围)。
|
*
|
* <p>默认只读(查单);加 {@code --execute} 才真正发起退款并落库。
|
* 用法:java -cp ... RefundStuckDelayedPayments [--execute] <yml路径> <jdbc-url>
|
*/
|
public class RefundStuckDelayedPayments {
|
|
/** 两笔目标:订单号 / 支付流水号 / 收款子商户 / 原交易请求日期(均为 2026-09-16)。 */
|
private static final List<Target> TARGETS = List.of(
|
new Target("P202609161320506589", "P202609161320512100", "6666000241804122", "20260916", 2L),
|
new Target("P202609161325126470", "P202609161325168095", "6666000241804122", "20260916", 2L));
|
|
private record Target(String orderNo, String paymentNo, String huifuId, String requestDate, long amountInCent) {}
|
|
public static void main(String[] args) throws Exception {
|
boolean execute = List.of(args).contains("--execute");
|
boolean check = List.of(args).contains("--check");
|
Path yml = Path.of(args[args.length - 2]);
|
String jdbcUrl = args[args.length - 1];
|
HuifuPaymentProperties properties = properties(yml);
|
System.out.println("环境=" + properties.getEnvironment() + " 系统号=" + properties.getSystemId()
|
+ " 产品号=" + properties.getProductId() + " 收款商户=" + properties.getMerchantId()
|
+ " 模式=" + (execute ? "执行退款" : check ? "查退款结果" : "只读查单"));
|
HuifuMiniProgramPaymentGateway queryGateway = new HuifuMiniProgramPaymentGateway(properties);
|
HuifuRefundGateway refundGateway = new HuifuRefundGateway(properties);
|
if (check) {
|
checkRefunds(refundGateway, jdbcUrl);
|
return;
|
}
|
|
try (Connection connection = DriverManager.getConnection(jdbcUrl, "root", "123456")) {
|
for (Target target : TARGETS) {
|
System.out.println();
|
System.out.println("==== 订单 " + target.orderNo() + " / 支付流水 " + target.paymentNo());
|
Map<String, Object> data = businessData(queryGateway.queryMiniProgramPayment(
|
new HuifuPaymentQueryCommand(target.paymentNo(), target.huifuId(), null, target.requestDate())));
|
print("查单", data);
|
String status = first(data, "trans_stat", "transStat");
|
String code = first(data, "sub_resp_code", "resp_code", "code");
|
String providerNo = first(data, "org_hf_seq_id", "orgHfSeqId", "hf_seq_id", "hfSeqId");
|
String amount = first(data, "trans_amt", "transAmt");
|
if (!"S".equalsIgnoreCase(status) || !successCode(code)) {
|
System.out.println("!! 通道未确认收款(trans_stat=" + status + " code=" + code + "),跳过退款");
|
continue;
|
}
|
if (!execute) {
|
System.out.println("-- 只读模式:确认收款 ¥" + amount + "(通道流水号 " + providerNo + "),加 --execute 才退款");
|
continue;
|
}
|
String refundNo = BusinessIdGenerator.next("R");
|
Map<String, Object> refundData = businessData(refundGateway.createRefund(new HuifuRefundCommand(
|
refundNo, target.huifuId(), target.paymentNo(), providerNo, target.requestDate(), target.amountInCent())));
|
print("退款", refundData);
|
String refundCode = first(refundData, "sub_resp_code", "resp_code", "code");
|
String refundTransStatus = first(refundData, "trans_stat", "transStat");
|
String refundProviderNo = first(refundData, "hf_seq_id", "hfSeqId");
|
// 与 HuifuRefundService 同一套判定:成功 / 处理中 / 失败,处理中留给对账任务收敛。
|
int localRefundStatus = successCode(refundCode) || "S".equalsIgnoreCase(refundTransStatus) ? 2
|
: ("P".equalsIgnoreCase(refundTransStatus) || "00000100".equals(refundCode) ? 1 : 3);
|
record(connection, target, refundNo, providerNo, refundData, refundProviderNo, localRefundStatus);
|
System.out.println("-- 本地已补齐:流水置 SUCCEEDED、退款记录 " + refundNo + "(本地按 "
|
+ (localRefundStatus == 2 ? "退款成功" : localRefundStatus == 1 ? "退款中,交对账收敛" : "退款失败") + " 落库)");
|
}
|
}
|
}
|
|
/** 查两笔退款的最新结果,并顺带看本地记录:退款成功后订单应退出结算范围。 */
|
private static void checkRefunds(HuifuRefundGateway refundGateway, String jdbcUrl) throws Exception {
|
try (Connection connection = DriverManager.getConnection(jdbcUrl, "root", "123456")) {
|
for (Target target : TARGETS) {
|
Map<String, Object> local = new LinkedHashMap<>();
|
try (PreparedStatement statement = connection.prepareStatement("""
|
select r.refund_no, r.refund_status, r.provider_refund_no, r.provider_response_code,
|
r.provider_response_message, o.order_no, o.refund_status order_refund_status,
|
p.status payment_status
|
from order_refund r join benefit_order o on o.id = r.order_id
|
join payment_transaction p on p.payment_no = r.provider_original_req_seq_id
|
where r.order_no = ? and r.deleted = 0 order by r.id desc limit 1
|
""")) {
|
statement.setString(1, target.orderNo());
|
try (ResultSet rows = statement.executeQuery()) {
|
ResultSetMetaData meta = rows.getMetaData();
|
if (rows.next()) {
|
for (int i = 1; i <= meta.getColumnCount(); i++) local.put(meta.getColumnLabel(i), rows.getObject(i));
|
}
|
}
|
}
|
String refundNo = String.valueOf(local.getOrDefault("refund_no", ""));
|
String providerRefundNo = String.valueOf(local.getOrDefault("provider_refund_no", ""));
|
System.out.println();
|
System.out.println("==== 订单 " + target.orderNo() + " / 退款 " + refundNo);
|
System.out.println("-- 本地:refund_status=" + local.get("refund_status") + " 订单 refund_status=" + local.get("order_refund_status")
|
+ " 支付流水=" + local.get("payment_status") + " 通道退款号=" + providerRefundNo);
|
if (refundNo.isBlank()) continue;
|
// 查询参数与 HuifuRefundService.query 一致:退款请求号 + 退款通道号 + 原交易日期都在记录里。
|
Map<String, Object> data = businessData(refundGateway.queryRefund(new HuifuRefundCommand(
|
refundNo, target.huifuId(), text(local.get("provider_original_req_seq_id")),
|
text(local.get("provider_original_no")), text(local.get("provider_original_req_date")),
|
target.amountInCent()), providerRefundNo));
|
print("退款查询", data);
|
}
|
}
|
}
|
|
/** 把本地账补成与通道一致:支付流水不再谎称 CLOSED,退款落到 order_refund,订单退出结算范围。 */
|
private static void record(Connection connection, Target target, String refundNo, String providerNo,
|
Map<String, Object> refundData, String refundProviderNo, int localRefundStatus) throws Exception {
|
connection.setAutoCommit(false);
|
try (PreparedStatement payment = connection.prepareStatement("""
|
update payment_transaction
|
set status = 'SUCCEEDED', provider_transaction_no = coalesce(nullif(?, ''), provider_transaction_no),
|
provider_response_code = ?, provider_response_message = ?
|
where payment_no = ? and status = 'CLOSED'
|
""");
|
PreparedStatement refund = connection.prepareStatement("""
|
insert into order_refund(refund_no, order_id, order_no, talent_id, card_id, benefit_id, merchant_id,
|
order_type, paid_amount, refund_amount, refundable_amount, refund_status,
|
provider, provider_huifu_id, provider_refund_no, provider_original_no,
|
provider_original_req_seq_id, provider_original_req_date,
|
provider_response_code, provider_response_message, reason, operator_name, refund_time)
|
select ?, o.id, o.order_no, o.talent_id, o.card_id, o.benefit_id, o.merchant_id, o.order_type,
|
?, ?, 0, ?, 'HUIFU', ?, ?, ?, ?, ?, ?, ?, ?, ?, now()
|
from benefit_order o where o.order_no = ? and o.deleted = 0
|
""");
|
PreparedStatement order = connection.prepareStatement(
|
"update benefit_order set refund_status = ? where order_no = ? and deleted = 0 and refund_status <> ?")) {
|
payment.setString(1, providerNo);
|
payment.setString(2, first(refundData, "sub_resp_code", "resp_code", "code"));
|
payment.setString(3, first(refundData, "sub_resp_desc", "resp_desc", "message"));
|
payment.setString(4, target.paymentNo());
|
System.out.println(" 支付流水更新 " + payment.executeUpdate() + " 行");
|
|
refund.setString(1, refundNo);
|
refund.setLong(2, target.amountInCent());
|
refund.setLong(3, target.amountInCent());
|
refund.setInt(4, localRefundStatus);
|
refund.setString(5, target.huifuId());
|
refund.setString(6, refundProviderNo);
|
refund.setString(7, providerNo);
|
refund.setString(8, target.paymentNo());
|
refund.setString(9, target.requestDate());
|
refund.setString(10, first(refundData, "sub_resp_code", "resp_code", "code"));
|
refund.setString(11, first(refundData, "sub_resp_desc", "resp_desc", "message"));
|
refund.setString(12, "取消订单后未退款的人工补退(旧取消流程遗留)");
|
refund.setString(13, "系统补退");
|
refund.setString(14, target.orderNo());
|
System.out.println(" 退款记录插入 " + refund.executeUpdate() + " 行(refund_status=" + localRefundStatus + ")");
|
// 退款成功才让订单退出结算范围;失败则把订单退款状态退回未退款(与 markRefundFailed 一致)。
|
int orderRefundStatus = localRefundStatus == 2 ? 2 : 0;
|
if (localRefundStatus != 1) {
|
order.setInt(1, orderRefundStatus);
|
order.setString(2, target.orderNo());
|
order.setInt(3, orderRefundStatus);
|
System.out.println(" 订单退款状态置为 " + orderRefundStatus + ":" + order.executeUpdate() + " 行(order_no=" + target.orderNo() + ")");
|
}
|
connection.commit();
|
} catch (Exception exception) {
|
connection.rollback();
|
throw exception;
|
}
|
}
|
|
/** 从 application.yml 读汇付配置:私钥不进命令行,也不写进这个文件。 */
|
private static HuifuPaymentProperties properties(Path yml) throws Exception {
|
String content = Files.readString(yml, StandardCharsets.UTF_8);
|
HuifuPaymentProperties properties = new HuifuPaymentProperties();
|
properties.setEnabled(true);
|
properties.setEnvironment(value(content, "environment"));
|
properties.setSystemId(value(content, "system-id"));
|
properties.setProductId(value(content, "product-id"));
|
properties.setMerchantId(value(content, "merchant-id"));
|
properties.setMiniProgramAppId(value(content, "mini-program-app-id"));
|
properties.setPrivateKey(value(content, "private-key"));
|
properties.setPlatformPublicKey(value(content, "platform-public-key"));
|
properties.setNotifyUrl(value(content, "notify-url"));
|
return properties;
|
}
|
|
private static String value(String content, String key) {
|
Matcher matcher = Pattern.compile("(?m)^\\s*" + Pattern.quote(key) + ":\\s*(.+?)\\s*$").matcher(content);
|
if (!matcher.find()) throw new IllegalStateException("application.yml 缺少 " + key);
|
String raw = matcher.group(1);
|
Matcher env = Pattern.compile("^\\$\\{[^:}]+:(.*)\\}$").matcher(raw);
|
return env.matches() ? env.group(1) : raw;
|
}
|
|
private static void print(String label, Map<String, Object> data) {
|
StringBuilder line = new StringBuilder("-- " + label + "回执:");
|
for (Map.Entry<String, Object> entry : data.entrySet()) line.append(entry.getKey()).append('=').append(entry.getValue()).append(' ');
|
System.out.println(line);
|
}
|
|
@SuppressWarnings("unchecked")
|
private static Map<String, Object> businessData(Map<String, Object> response) {
|
Object data = response.get("data");
|
if (data instanceof Map<?, ?> map) return new LinkedHashMap<>((Map<String, Object>) map);
|
return new LinkedHashMap<>(response);
|
}
|
|
private static String text(Object value) {
|
return value == null ? "" : String.valueOf(value).trim();
|
}
|
|
private static String first(Map<String, Object> data, String... keys) {
|
for (String key : keys) {
|
Object value = data.get(key);
|
if (value != null && !String.valueOf(value).isBlank()) return String.valueOf(value).trim();
|
}
|
return "";
|
}
|
|
private static boolean successCode(String code) {
|
return "00000000".equals(code) || "10000".equals(code) || "0".equals(code) || "SUCCESS".equalsIgnoreCase(code);
|
}
|
}
|