mirror of
https://gitee.com/dromara/dax-pay
synced 2026-08-10 14:56:06 +08:00
feat(audit): 统一支付接口审计日志与公共参数 reqId
开放 API 公共参数增加必填 reqId 并响应回显;在验签切面异步记录调用审计(mchNo+reqId 索引),不落 appId/业务单号列;运营端日志管理增加支付接口日志菜单与查询能力。
This commit is contained in:
@@ -180,3 +180,4 @@ INSERT INTO "public"."iam_perm_menu" VALUES (31102, 311, 'system:sensitive-word-
|
||||
-- Primary Key structure for table iam_perm_menu
|
||||
-- ----------------------------
|
||||
ALTER TABLE "public"."iam_perm_menu" ADD CONSTRAINT "iam_perm_menu_pkey" PRIMARY KEY ("id");
|
||||
INSERT INTO "public"."iam_perm_menu" VALUES (30203, 302, 'system:log:unipay', 'admin', 'SystemUnipayApiLog', 'menu.system.log.unipay', 'lucide:webhook', 'f', 'f', '/system/log/unipay/UnipayApiLogList', '/system/log/unipay', NULL, 3, 'f', 't', 'f', 0, 1, 1, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-17 00:00:00+00', '2026-07-17 00:00:00+00');
|
||||
|
||||
@@ -103,3 +103,10 @@ INSERT INTO "public"."pay_gateway_cashier_item" (
|
||||
(9100000000000100026, 'M1781861282382', 'A7846150576259390', 'web', NULL,
|
||||
'微信 H5', 'wechat', 'f', 6, 'method', 'wechat_h5', NULL, NULL,
|
||||
1, '2026-07-17 00:00:00+00', 1, '2026-07-17 00:00:00+00', 0, 'f');
|
||||
|
||||
-- ----------------------------
|
||||
-- 日志管理:支付接口审计菜单 system:log:unipay
|
||||
-- ----------------------------
|
||||
DELETE FROM "public"."iam_perm_menu" WHERE "id" = 30203;
|
||||
INSERT INTO "public"."iam_perm_menu" VALUES (30203, 302, 'system:log:unipay', 'admin', 'SystemUnipayApiLog', 'menu.system.log.unipay', 'lucide:webhook', 'f', 'f', '/system/log/unipay/UnipayApiLogList', '/system/log/unipay', NULL, 3, 'f', 't', 'f', 0, 1, 1, 'f', 'menu', NULL, NULL, NULL, NULL, NULL, NULL, '2026-07-17 00:00:00+00', '2026-07-17 00:00:00+00');
|
||||
|
||||
|
||||
@@ -263,3 +263,67 @@ CREATE INDEX IF NOT EXISTS "idx_pay_gateway_cashier_item_bucket"
|
||||
ON "public"."pay_gateway_cashier_item" ("app_id", "cashier_type", "client_env");
|
||||
CREATE INDEX IF NOT EXISTS "idx_pay_gateway_cashier_item_mch"
|
||||
ON "public"."pay_gateway_cashier_item" ("mch_no", "app_id");
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for starter_audit_unipay_log(统一支付接口审计日志)
|
||||
-- 索引键:mch_no + req_id;不单独存 appId/业务单号(可在 body 中查看)
|
||||
-- ----------------------------
|
||||
CREATE TABLE IF NOT EXISTS "public"."starter_audit_unipay_log" (
|
||||
"id" int8 NOT NULL,
|
||||
"mch_no" varchar(32) COLLATE "pg_catalog"."default",
|
||||
"req_id" varchar(64) COLLATE "pg_catalog"."default",
|
||||
"api_path" varchar(256) COLLATE "pg_catalog"."default",
|
||||
"api_title" varchar(64) COLLATE "pg_catalog"."default",
|
||||
"request_method" varchar(16) COLLATE "pg_catalog"."default",
|
||||
"client_ip" varchar(64) COLLATE "pg_catalog"."default",
|
||||
"request_ip" varchar(64) COLLATE "pg_catalog"."default",
|
||||
"request_location" varchar(128) COLLATE "pg_catalog"."default",
|
||||
"success" bool,
|
||||
"error_code" int4,
|
||||
"error_msg" varchar(512) COLLATE "pg_catalog"."default",
|
||||
"duration_ms" int8,
|
||||
"trace_id" varchar(64) COLLATE "pg_catalog"."default",
|
||||
"req_param" jsonb,
|
||||
"res_body" jsonb,
|
||||
"operate_time" timestamptz(6)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE "public"."starter_audit_unipay_log" IS '统一支付接口审计日志';
|
||||
COMMENT ON COLUMN "public"."starter_audit_unipay_log"."id" IS '主键';
|
||||
COMMENT ON COLUMN "public"."starter_audit_unipay_log"."mch_no" IS '商户号';
|
||||
COMMENT ON COLUMN "public"."starter_audit_unipay_log"."req_id" IS '请求ID(商户传入,审计主索引)';
|
||||
COMMENT ON COLUMN "public"."starter_audit_unipay_log"."api_path" IS '接口路径';
|
||||
COMMENT ON COLUMN "public"."starter_audit_unipay_log"."api_title" IS '接口标题';
|
||||
COMMENT ON COLUMN "public"."starter_audit_unipay_log"."request_method" IS 'HTTP方法';
|
||||
COMMENT ON COLUMN "public"."starter_audit_unipay_log"."client_ip" IS '商户入参声明的客户端IP';
|
||||
COMMENT ON COLUMN "public"."starter_audit_unipay_log"."request_ip" IS '真实接入IP';
|
||||
COMMENT ON COLUMN "public"."starter_audit_unipay_log"."request_location" IS '接入IP归属地';
|
||||
COMMENT ON COLUMN "public"."starter_audit_unipay_log"."success" IS '是否成功';
|
||||
COMMENT ON COLUMN "public"."starter_audit_unipay_log"."error_code" IS '业务错误码';
|
||||
COMMENT ON COLUMN "public"."starter_audit_unipay_log"."error_msg" IS '错误信息';
|
||||
COMMENT ON COLUMN "public"."starter_audit_unipay_log"."duration_ms" IS '耗时毫秒';
|
||||
COMMENT ON COLUMN "public"."starter_audit_unipay_log"."trace_id" IS '链路追踪ID';
|
||||
COMMENT ON COLUMN "public"."starter_audit_unipay_log"."req_param" IS '请求参数(脱敏后)';
|
||||
COMMENT ON COLUMN "public"."starter_audit_unipay_log"."res_body" IS '响应体(脱敏后)';
|
||||
COMMENT ON COLUMN "public"."starter_audit_unipay_log"."operate_time" IS '操作时间UTC';
|
||||
|
||||
ALTER TABLE "public"."starter_audit_unipay_log" DROP CONSTRAINT IF EXISTS "starter_audit_unipay_log_pkey";
|
||||
ALTER TABLE "public"."starter_audit_unipay_log" ADD CONSTRAINT "starter_audit_unipay_log_pkey" PRIMARY KEY ("id");
|
||||
|
||||
-- 若已按旧版建表:补 req_id、删业务键列(审计表可重建,无历史依赖)
|
||||
ALTER TABLE "public"."starter_audit_unipay_log" ADD COLUMN IF NOT EXISTS "req_id" varchar(64);
|
||||
ALTER TABLE "public"."starter_audit_unipay_log" DROP COLUMN IF EXISTS "app_id";
|
||||
ALTER TABLE "public"."starter_audit_unipay_log" DROP COLUMN IF EXISTS "channel_mch_no";
|
||||
ALTER TABLE "public"."starter_audit_unipay_log" DROP COLUMN IF EXISTS "biz_order_no";
|
||||
ALTER TABLE "public"."starter_audit_unipay_log" DROP COLUMN IF EXISTS "order_no";
|
||||
ALTER TABLE "public"."starter_audit_unipay_log" DROP COLUMN IF EXISTS "trade_no";
|
||||
|
||||
DROP INDEX IF EXISTS "idx_starter_audit_unipay_log_biz_order";
|
||||
CREATE INDEX IF NOT EXISTS "idx_starter_audit_unipay_log_time" ON "public"."starter_audit_unipay_log" ("operate_time" DESC);
|
||||
CREATE INDEX IF NOT EXISTS "idx_starter_audit_unipay_log_mch_time" ON "public"."starter_audit_unipay_log" ("mch_no", "operate_time" DESC);
|
||||
CREATE INDEX IF NOT EXISTS "idx_starter_audit_unipay_log_mch_req" ON "public"."starter_audit_unipay_log" ("mch_no", "req_id");
|
||||
CREATE INDEX IF NOT EXISTS "idx_starter_audit_unipay_log_req" ON "public"."starter_audit_unipay_log" ("req_id");
|
||||
CREATE INDEX IF NOT EXISTS "idx_starter_audit_unipay_log_trace" ON "public"."starter_audit_unipay_log" ("trace_id");
|
||||
CREATE INDEX IF NOT EXISTS "idx_starter_audit_unipay_log_success_time" ON "public"."starter_audit_unipay_log" ("success", "operate_time" DESC);
|
||||
|
||||
-- ----------------------------
|
||||
|
||||
@@ -32,6 +32,10 @@ public class DaxResult<T>{
|
||||
@Schema(description = "响应时间(UTC)")
|
||||
private OffsetDateTime resTime;
|
||||
|
||||
/// 请求ID(回显入参 reqId)
|
||||
@Schema(description = "请求ID")
|
||||
private String reqId;
|
||||
|
||||
/// 追踪ID
|
||||
@Schema(description = "追踪ID")
|
||||
private String traceId;
|
||||
|
||||
@@ -3,6 +3,7 @@ package cn.daxpay.open.payment.unipay.param;
|
||||
import cn.daxpay.open.platform.core.validation.IpAddress;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
@@ -23,6 +24,12 @@ public abstract class PaymentCommonParam {
|
||||
@Size(max=64, message = "{validation.field.clientIp.size}")
|
||||
private String clientIp;
|
||||
|
||||
/// 请求ID(商户侧生成,调用追踪与审计索引;参与签名)
|
||||
@Schema(description = "请求ID")
|
||||
@NotBlank(message = "{validation.field.reqId.notBlank}")
|
||||
@Size(max = 64, message = "{validation.field.reqId.size}")
|
||||
private String reqId;
|
||||
|
||||
/// 随机数
|
||||
@Schema(description = "随机数")
|
||||
@Size(max = 32, message = "{validation.field.nonceStr.size}")
|
||||
|
||||
@@ -1,28 +1,43 @@
|
||||
package cn.daxpay.open.payment.unipay.aop;
|
||||
|
||||
import cn.daxpay.open.payment.common.context.MerchantContextLoader;
|
||||
import cn.daxpay.open.payment.common.result.DaxResult;
|
||||
import cn.daxpay.open.payment.unipay.param.MerchantPaymentCommonParam;
|
||||
import cn.daxpay.open.platform.common.json.util.JacksonUtil;
|
||||
import cn.daxpay.open.platform.common.request.context.RequestContextHolder;
|
||||
import cn.daxpay.open.platform.common.spring.util.WebServletUtil;
|
||||
import cn.daxpay.open.platform.core.code.CommonCode;
|
||||
import cn.daxpay.open.platform.core.code.CommonErrorCode;
|
||||
import cn.daxpay.open.platform.core.event.UnipayApiAccessLogEvent;
|
||||
import cn.daxpay.open.platform.core.exception.BizException;
|
||||
import cn.daxpay.open.platform.core.exception.BizInfoException;
|
||||
import cn.daxpay.open.platform.core.code.CommonErrorCode;
|
||||
import cn.daxpay.open.platform.core.util.ValidationUtil;
|
||||
import cn.daxpay.open.payment.unipay.param.MerchantPaymentCommonParam;
|
||||
import cn.daxpay.open.payment.common.result.DaxResult;
|
||||
import cn.daxpay.open.payment.common.context.MerchantContextLoader;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.extra.servlet.JakartaServletUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.slf4j.MDC;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/// # 开放支付验签切面([PaymentVerify])
|
||||
///
|
||||
/// 执行顺序: 过滤器 -> 拦截器 -> 切面 -> 方法。
|
||||
/// 切面负责**签名 API 身份源**:校验参数后 `initMch` + 验签;不负责应用解析。
|
||||
/// 装载后的 mchNo 供 TenantLine / 自动填充使用。
|
||||
///
|
||||
/// 同时在 finally 中发布 [UnipayApiAccessLogEvent],失败吞异常,不影响支付主流程。
|
||||
/// 审计业务索引:`mchNo` + `reqId`。
|
||||
@Aspect
|
||||
@Slf4j
|
||||
@Component
|
||||
@@ -32,6 +47,7 @@ public class PaymentVerifyAspect {
|
||||
private final PaymentSignService paymentSignService;
|
||||
private final PaymentReplayProtectService paymentReplayProtectService;
|
||||
private final MerchantContextLoader merchantContextLoader;
|
||||
private final ApplicationEventPublisher eventPublisher;
|
||||
|
||||
/// 处理方法上的@PaymentVerify注解
|
||||
@Around("@annotation(paymentVerify)")
|
||||
@@ -45,44 +61,192 @@ public class PaymentVerifyAspect {
|
||||
return doVerify(pjp);
|
||||
}
|
||||
|
||||
/// 支付签名校验逻辑
|
||||
/// 支付签名校验逻辑 + 接口审计发布
|
||||
private Object doVerify(ProceedingJoinPoint pjp) throws Throwable {
|
||||
Object[] args = pjp.getArgs();
|
||||
if (args.length == 0){
|
||||
// 支付方法至少有一个参数
|
||||
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "pay.error.verify.methodParamRequired");
|
||||
}
|
||||
Object param = args[0];
|
||||
if (param instanceof MerchantPaymentCommonParam paymentParam){
|
||||
// 参数校验
|
||||
ValidationUtil.validateParam(paymentParam);
|
||||
// 商户身份初始化(含状态校验), 使 mchNo 进入线程上下文供签名校验与自动填充
|
||||
merchantContextLoader.initMch(paymentParam.getMchNo());
|
||||
// 参数签名校验
|
||||
paymentSignService.signVerify(paymentParam);
|
||||
// 防重放校验(Nonce一次性消费 + 请求时间窗口,受平台API安全配置开关控制)
|
||||
paymentReplayProtectService.verify(paymentParam, paymentParam.getMchNo());
|
||||
} else {
|
||||
// 参数需要继承MerchantPaymentCommonParam
|
||||
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "pay.error.verify.paramExtendRequired");
|
||||
}
|
||||
Object proceed;
|
||||
long startNs = System.nanoTime();
|
||||
Object result = null;
|
||||
Throwable error = null;
|
||||
String reqId = null;
|
||||
try {
|
||||
proceed = pjp.proceed();
|
||||
} catch (BizException ex) {
|
||||
DaxResult<Void> result = new DaxResult<>(ex.getCode(), ex.getMessage());
|
||||
paymentSignService.sign(result);
|
||||
return result;
|
||||
Object[] args = pjp.getArgs();
|
||||
if (args.length == 0) {
|
||||
// 支付方法至少有一个参数
|
||||
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "pay.error.verify.methodParamRequired");
|
||||
}
|
||||
Object param = args[0];
|
||||
if (param instanceof MerchantPaymentCommonParam paymentParam) {
|
||||
reqId = paymentParam.getReqId();
|
||||
// 参数校验
|
||||
ValidationUtil.validateParam(paymentParam);
|
||||
// 商户身份初始化(含状态校验), 使 mchNo 进入线程上下文供签名校验与自动填充
|
||||
merchantContextLoader.initMch(paymentParam.getMchNo());
|
||||
// 参数签名校验
|
||||
paymentSignService.signVerify(paymentParam);
|
||||
// 防重放校验(Nonce一次性消费 + 请求时间窗口,受平台API安全配置开关控制)
|
||||
paymentReplayProtectService.verify(paymentParam, paymentParam.getMchNo());
|
||||
} else {
|
||||
// 参数需要继承MerchantPaymentCommonParam
|
||||
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "pay.error.verify.paramExtendRequired");
|
||||
}
|
||||
Object proceed;
|
||||
try {
|
||||
proceed = pjp.proceed();
|
||||
} catch (BizException ex) {
|
||||
DaxResult<Void> daxResult = new DaxResult<>(ex.getCode(), ex.getMessage());
|
||||
enrichDaxResult(daxResult, reqId);
|
||||
paymentSignService.sign(daxResult);
|
||||
result = daxResult;
|
||||
return daxResult;
|
||||
}
|
||||
// 对返回值添加响应时间、reqId/traceId 并进行签名
|
||||
if (proceed instanceof DaxResult<?> daxResult) {
|
||||
daxResult.setResTime(OffsetDateTime.now(ZoneOffset.UTC));
|
||||
enrichDaxResult(daxResult, reqId);
|
||||
paymentSignService.sign(daxResult);
|
||||
result = daxResult;
|
||||
} else {
|
||||
// 支付方法返回类型需要为 DaxResult
|
||||
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "pay.error.verify.returnTypeRequired");
|
||||
}
|
||||
return proceed;
|
||||
} catch (Throwable t) {
|
||||
error = t;
|
||||
throw t;
|
||||
} finally {
|
||||
// 审计发布:任何异常均吞掉,绝不影响支付
|
||||
try {
|
||||
publishAudit(pjp, startNs, result, error, reqId);
|
||||
} catch (Exception e) {
|
||||
log.warn("支付接口审计事件发布失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
// 对返回值添加响应时间并进行签名
|
||||
if (proceed instanceof DaxResult<?> result){
|
||||
result.setResTime(OffsetDateTime.now(ZoneOffset.UTC));
|
||||
paymentSignService.sign(result);
|
||||
} else {
|
||||
// 支付方法返回类型需要为 DaxResult
|
||||
throw new BizInfoException(CommonErrorCode.VALIDATE_PARAMETERS_ERROR, "pay.error.verify.returnTypeRequired");
|
||||
}
|
||||
return proceed;
|
||||
}
|
||||
|
||||
/// 回写请求ID与链路追踪ID
|
||||
private void enrichDaxResult(DaxResult<?> daxResult, String reqId) {
|
||||
if (StrUtil.isNotBlank(reqId)) {
|
||||
daxResult.setReqId(reqId);
|
||||
}
|
||||
String traceId = resolveTraceId();
|
||||
if (StrUtil.isNotBlank(traceId)) {
|
||||
daxResult.setTraceId(traceId);
|
||||
}
|
||||
}
|
||||
|
||||
/// 组装并发布审计事件
|
||||
private void publishAudit(ProceedingJoinPoint pjp, long startNs, Object result, Throwable error, String reqId) {
|
||||
long durationMs = (System.nanoTime() - startNs) / 1_000_000L;
|
||||
String apiPath = RequestContextHolder.getRequestUri();
|
||||
String requestMethod = RequestContextHolder.getMethod();
|
||||
|
||||
UnipayApiAccessLogEvent event = new UnipayApiAccessLogEvent()
|
||||
.setApiPath(apiPath)
|
||||
.setApiTitle(resolveApiTitle(apiPath))
|
||||
.setRequestMethod(requestMethod)
|
||||
.setReqId(reqId)
|
||||
.setDurationMs(durationMs)
|
||||
.setOperateTime(OffsetDateTime.now(ZoneOffset.UTC))
|
||||
.setTraceId(resolveTraceId());
|
||||
|
||||
// 真实接入 IP
|
||||
Optional.ofNullable(WebServletUtil.getRequest())
|
||||
.map(JakartaServletUtil::getClientIP)
|
||||
.ifPresent(event::setRequestIp);
|
||||
|
||||
// 请求参数
|
||||
Object[] args = pjp.getArgs();
|
||||
Object param = (args != null && args.length > 0) ? args[0] : null;
|
||||
if (param instanceof MerchantPaymentCommonParam paymentParam) {
|
||||
event.setMchNo(paymentParam.getMchNo());
|
||||
event.setClientIp(paymentParam.getClientIp());
|
||||
if (StrUtil.isBlank(event.getReqId())) {
|
||||
event.setReqId(paymentParam.getReqId());
|
||||
}
|
||||
try {
|
||||
event.setReqParam(JacksonUtil.toJson(param));
|
||||
} catch (Exception e) {
|
||||
event.setReqParam(null);
|
||||
}
|
||||
} else if (param != null) {
|
||||
try {
|
||||
event.setReqParam(JacksonUtil.toJson(param));
|
||||
} catch (Exception ignored) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
// 响应结果
|
||||
if (result instanceof DaxResult<?> daxResult) {
|
||||
boolean success = daxResult.getCode() == CommonCode.SUCCESS_CODE;
|
||||
event.setSuccess(success);
|
||||
event.setErrorCode(daxResult.getCode());
|
||||
if (!success) {
|
||||
event.setErrorMsg(daxResult.getMsg());
|
||||
}
|
||||
if (StrUtil.isBlank(event.getReqId()) && StrUtil.isNotBlank(daxResult.getReqId())) {
|
||||
event.setReqId(daxResult.getReqId());
|
||||
}
|
||||
try {
|
||||
event.setResBody(JacksonUtil.toJson(daxResult));
|
||||
} catch (Exception e) {
|
||||
event.setResBody(null);
|
||||
}
|
||||
} else if (error != null) {
|
||||
event.setSuccess(false);
|
||||
event.setErrorMsg(StrUtil.sub(error.getMessage(), 0, 512));
|
||||
try {
|
||||
Map<String, Object> errBody = new LinkedHashMap<>(2);
|
||||
errBody.put("exception", error.getClass().getSimpleName());
|
||||
errBody.put("message", error.getMessage());
|
||||
event.setResBody(JacksonUtil.toJson(errBody));
|
||||
} catch (Exception ignored) {
|
||||
// ignore
|
||||
}
|
||||
} else {
|
||||
event.setSuccess(true);
|
||||
}
|
||||
|
||||
eventPublisher.publishEvent(event);
|
||||
}
|
||||
|
||||
/// 解析链路 ID:优先 MDC,其次请求头
|
||||
private String resolveTraceId() {
|
||||
String fromMdc = MDC.get(CommonCode.TRACE_ID);
|
||||
if (StrUtil.isNotBlank(fromMdc)) {
|
||||
return fromMdc;
|
||||
}
|
||||
return RequestContextHolder.getTraceId();
|
||||
}
|
||||
|
||||
/// 按路径映射可读标题
|
||||
private String resolveApiTitle(String path) {
|
||||
if (StrUtil.isBlank(path)) {
|
||||
return "统一支付接口";
|
||||
}
|
||||
if (path.contains("/sync/") && path.endsWith("/pay")) {
|
||||
return "支付订单同步";
|
||||
}
|
||||
if (path.endsWith("/close")) {
|
||||
return "关单/撤销";
|
||||
}
|
||||
if (path.contains("/pay-order")) {
|
||||
return "支付订单查询";
|
||||
}
|
||||
if (path.contains("/gateway/pre-pay")) {
|
||||
return "网关预下单";
|
||||
}
|
||||
if (path.contains("/gateway/query")) {
|
||||
return "网关订单查询";
|
||||
}
|
||||
if (path.contains("/generate-auth-url")) {
|
||||
return "生成通道授权链接";
|
||||
}
|
||||
if (path.contains("/assist/channel/auth")) {
|
||||
return "通道授权";
|
||||
}
|
||||
if (path.endsWith("/pay")) {
|
||||
return "支付下单";
|
||||
}
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package cn.daxpay.open.platform.capability.audit.log.controller;
|
||||
|
||||
import cn.daxpay.open.platform.capability.audit.log.param.UnipayApiLogQuery;
|
||||
import cn.daxpay.open.platform.capability.audit.log.result.UnipayApiLogResult;
|
||||
import cn.daxpay.open.platform.capability.audit.log.service.log.UnipayApiLogService;
|
||||
import cn.daxpay.open.platform.core.annotation.PermCode;
|
||||
import cn.daxpay.open.platform.core.code.PermCodes;
|
||||
import cn.daxpay.open.platform.core.rest.Res;
|
||||
import cn.daxpay.open.platform.core.rest.param.PageParam;
|
||||
import cn.daxpay.open.platform.core.rest.result.PageResult;
|
||||
import cn.daxpay.open.platform.core.rest.result.Result;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/// # 统一支付接口审计日志
|
||||
///
|
||||
@PermCode(menuCode = PermCodes.System.Log.Unipay.MENU)
|
||||
@Validated
|
||||
@Tag(name = "支付接口日志")
|
||||
@RestController
|
||||
@RequestMapping("/log/unipay")
|
||||
@RequiredArgsConstructor
|
||||
public class UnipayApiLogController {
|
||||
|
||||
private final UnipayApiLogService unipayApiLogService;
|
||||
|
||||
@PermCode(code = PermCodes.Action.VIEW)
|
||||
@Operation(summary = "分页")
|
||||
@GetMapping("/page")
|
||||
public Result<PageResult<UnipayApiLogResult>> page(PageParam pageParam, UnipayApiLogQuery query) {
|
||||
return Res.ok(unipayApiLogService.page(pageParam, query));
|
||||
}
|
||||
|
||||
@PermCode(code = PermCodes.Action.VIEW)
|
||||
@Operation(summary = "获取")
|
||||
@GetMapping("/get")
|
||||
public Result<UnipayApiLogResult> findById(@NotNull(message = "{validation.field.id.notNull}") Long id) {
|
||||
return Res.ok(unipayApiLogService.findById(id));
|
||||
}
|
||||
|
||||
@PermCode(code = PermCodes.Action.MANAGE)
|
||||
@Operation(summary = "清除指定天数之前的日志")
|
||||
@PostMapping("/delete-by-day")
|
||||
public Result<Void> deleteByDay(@NotNull(message = "{validation.field.deleteDay.notNull}") Integer deleteDay) {
|
||||
unipayApiLogService.deleteByDay(deleteDay);
|
||||
return Res.ok();
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,13 @@ package cn.daxpay.open.platform.capability.audit.log.convert;
|
||||
|
||||
import cn.daxpay.open.platform.capability.audit.log.entity.LoginLogDb;
|
||||
import cn.daxpay.open.platform.capability.audit.log.entity.OperateLogDb;
|
||||
import cn.daxpay.open.platform.capability.audit.log.result.LoginLogResult;
|
||||
import cn.daxpay.open.platform.capability.audit.log.result.OperateLogResult;
|
||||
import cn.daxpay.open.platform.capability.audit.log.entity.UnipayApiLogDb;
|
||||
import cn.daxpay.open.platform.capability.audit.log.param.LoginLogParam;
|
||||
import cn.daxpay.open.platform.capability.audit.log.param.OperateLogParam;
|
||||
import cn.daxpay.open.platform.capability.audit.log.param.UnipayApiLogParam;
|
||||
import cn.daxpay.open.platform.capability.audit.log.result.LoginLogResult;
|
||||
import cn.daxpay.open.platform.capability.audit.log.result.OperateLogResult;
|
||||
import cn.daxpay.open.platform.capability.audit.log.result.UnipayApiLogResult;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.factory.Mappers;
|
||||
|
||||
@@ -20,8 +23,12 @@ public interface LogConvert {
|
||||
|
||||
LoginLogResult convert(LoginLogDb in);
|
||||
|
||||
UnipayApiLogResult convert(UnipayApiLogDb in);
|
||||
|
||||
OperateLogDb convert(OperateLogParam in);
|
||||
|
||||
LoginLogDb convert(LoginLogParam in);
|
||||
|
||||
UnipayApiLogDb convert(UnipayApiLogParam in);
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package cn.daxpay.open.platform.capability.audit.log.dao;
|
||||
|
||||
import cn.daxpay.open.platform.capability.audit.log.entity.UnipayApiLogDb;
|
||||
import cn.daxpay.open.platform.capability.audit.log.param.UnipayApiLogQuery;
|
||||
import cn.daxpay.open.platform.common.mybatisplus.impl.BaseManager;
|
||||
import cn.daxpay.open.platform.common.mybatisplus.query.generator.QueryGenerator;
|
||||
import cn.daxpay.open.platform.common.mybatisplus.util.MpUtil;
|
||||
import cn.daxpay.open.platform.core.rest.param.PageParam;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
/// # 统一支付接口审计日志 Manager
|
||||
///
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class UnipayApiLogDbManager extends BaseManager<UnipayApiLogDbMapper, UnipayApiLogDb> {
|
||||
|
||||
/// 分页
|
||||
public Page<UnipayApiLogDb> page(PageParam pageParam, UnipayApiLogQuery query) {
|
||||
QueryWrapper<UnipayApiLogDb> generator = QueryGenerator.generator(query);
|
||||
Page<UnipayApiLogDb> mpPage = MpUtil.getMpPage(pageParam);
|
||||
return this.page(mpPage, generator);
|
||||
}
|
||||
|
||||
/// 删除小于指定时间的日志
|
||||
public void deleteByOffset(OffsetDateTime offset) {
|
||||
lambdaUpdate()
|
||||
.le(UnipayApiLogDb::getOperateTime, offset)
|
||||
.remove();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package cn.daxpay.open.platform.capability.audit.log.dao;
|
||||
|
||||
import cn.daxpay.open.platform.capability.audit.log.entity.UnipayApiLogDb;
|
||||
import com.github.yulichang.base.MPJBaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/// # 统一支付接口审计日志 Mapper
|
||||
///
|
||||
@Mapper
|
||||
public interface UnipayApiLogDbMapper extends MPJBaseMapper<UnipayApiLogDb> {
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package cn.daxpay.open.platform.capability.audit.log.entity;
|
||||
|
||||
import cn.daxpay.open.platform.capability.audit.log.convert.LogConvert;
|
||||
import cn.daxpay.open.platform.capability.audit.log.result.UnipayApiLogResult;
|
||||
import cn.daxpay.open.platform.common.mybatisplus.base.MpIdEntity;
|
||||
import cn.daxpay.open.platform.common.mybatisplus.function.ToResult;
|
||||
import cn.daxpay.open.platform.common.mybatisplus.handler.type.JsonbStringTypeHandler;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
/// # 统一支付接口审计日志
|
||||
///
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@TableName(value = "starter_audit_unipay_log", autoResultMap = true)
|
||||
public class UnipayApiLogDb extends MpIdEntity implements ToResult<UnipayApiLogResult> {
|
||||
|
||||
/// 商户号
|
||||
private String mchNo;
|
||||
|
||||
/// 请求 ID
|
||||
private String reqId;
|
||||
|
||||
/// 接口路径
|
||||
private String apiPath;
|
||||
|
||||
/// 接口标题
|
||||
private String apiTitle;
|
||||
|
||||
/// HTTP 方法
|
||||
private String requestMethod;
|
||||
|
||||
/// 商户入参声明的客户端 IP
|
||||
private String clientIp;
|
||||
|
||||
/// 真实接入 IP
|
||||
private String requestIp;
|
||||
|
||||
/// 接入 IP 归属地
|
||||
private String requestLocation;
|
||||
|
||||
/// 是否成功
|
||||
private Boolean success;
|
||||
|
||||
/// 业务错误码
|
||||
private Integer errorCode;
|
||||
|
||||
/// 错误信息
|
||||
private String errorMsg;
|
||||
|
||||
/// 耗时(毫秒)
|
||||
private Long durationMs;
|
||||
|
||||
/// 链路追踪 ID
|
||||
private String traceId;
|
||||
|
||||
/// 请求参数(脱敏后)
|
||||
@TableField(typeHandler = JsonbStringTypeHandler.class)
|
||||
private String reqParam;
|
||||
|
||||
/// 响应体(脱敏后)
|
||||
@TableField(typeHandler = JsonbStringTypeHandler.class)
|
||||
private String resBody;
|
||||
|
||||
/// 操作时间 (UTC)
|
||||
private OffsetDateTime operateTime;
|
||||
|
||||
@Override
|
||||
public UnipayApiLogResult toResult() {
|
||||
return LogConvert.CONVERT.convert(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package cn.daxpay.open.platform.capability.audit.log.handler;
|
||||
|
||||
import cn.daxpay.open.platform.capability.audit.log.param.UnipayApiLogParam;
|
||||
import cn.daxpay.open.platform.capability.audit.log.service.ip2region.IpToRegionService;
|
||||
import cn.daxpay.open.platform.capability.audit.log.service.log.UnipayApiLogService;
|
||||
import cn.daxpay.open.platform.core.event.UnipayApiAccessLogEvent;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/// # 统一支付接口审计事件监听
|
||||
///
|
||||
/// 同步监听仅负责补全归属地并入队,禁止重业务逻辑。
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class UnipayApiLogEventListener {
|
||||
|
||||
private final UnipayApiLogService unipayApiLogService;
|
||||
private final IpToRegionService ipToRegionService;
|
||||
|
||||
@EventListener
|
||||
public void onUnipayApiAccess(UnipayApiAccessLogEvent event) {
|
||||
if (event == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
UnipayApiLogParam param = new UnipayApiLogParam()
|
||||
.setMchNo(event.getMchNo())
|
||||
.setReqId(event.getReqId())
|
||||
.setApiPath(event.getApiPath())
|
||||
.setApiTitle(event.getApiTitle())
|
||||
.setRequestMethod(event.getRequestMethod())
|
||||
.setClientIp(event.getClientIp())
|
||||
.setRequestIp(event.getRequestIp())
|
||||
.setSuccess(event.getSuccess())
|
||||
.setErrorCode(event.getErrorCode())
|
||||
.setErrorMsg(event.getErrorMsg())
|
||||
.setDurationMs(event.getDurationMs())
|
||||
.setTraceId(event.getTraceId())
|
||||
.setReqParam(event.getReqParam())
|
||||
.setResBody(event.getResBody())
|
||||
.setOperateTime(event.getOperateTime());
|
||||
|
||||
// IP 归属地
|
||||
if (StrUtil.isNotBlank(param.getRequestIp())) {
|
||||
try {
|
||||
param.setRequestLocation(ipToRegionService.getRegionStrByIp(param.getRequestIp()));
|
||||
} catch (Exception e) {
|
||||
param.setRequestLocation("未知");
|
||||
}
|
||||
}
|
||||
|
||||
unipayApiLogService.add(param);
|
||||
} catch (Exception e) {
|
||||
log.warn("处理支付接口审计事件失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package cn.daxpay.open.platform.capability.audit.log.param;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
/// # 统一支付接口审计日志写入参数
|
||||
///
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@Schema(description = "统一支付接口审计日志写入参数")
|
||||
public class UnipayApiLogParam {
|
||||
|
||||
@Schema(description = "商户号")
|
||||
private String mchNo;
|
||||
|
||||
@Schema(description = "请求ID")
|
||||
private String reqId;
|
||||
|
||||
@Schema(description = "接口路径")
|
||||
private String apiPath;
|
||||
|
||||
@Schema(description = "接口标题")
|
||||
private String apiTitle;
|
||||
|
||||
@Schema(description = "HTTP 方法")
|
||||
private String requestMethod;
|
||||
|
||||
@Schema(description = "商户入参声明的客户端 IP")
|
||||
private String clientIp;
|
||||
|
||||
@Schema(description = "真实接入 IP")
|
||||
private String requestIp;
|
||||
|
||||
@Schema(description = "接入 IP 归属地")
|
||||
private String requestLocation;
|
||||
|
||||
@Schema(description = "是否成功")
|
||||
private Boolean success;
|
||||
|
||||
@Schema(description = "业务错误码")
|
||||
private Integer errorCode;
|
||||
|
||||
@Schema(description = "错误信息")
|
||||
private String errorMsg;
|
||||
|
||||
@Schema(description = "耗时(毫秒)")
|
||||
private Long durationMs;
|
||||
|
||||
@Schema(description = "链路追踪 ID")
|
||||
private String traceId;
|
||||
|
||||
@Schema(description = "请求参数 JSON")
|
||||
private String reqParam;
|
||||
|
||||
@Schema(description = "响应体 JSON")
|
||||
private String resBody;
|
||||
|
||||
@Schema(description = "操作时间 (UTC)")
|
||||
private OffsetDateTime operateTime;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package cn.daxpay.open.platform.capability.audit.log.param;
|
||||
|
||||
import cn.daxpay.open.platform.core.annotation.QueryParam;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/// # 统一支付接口审计日志查询参数
|
||||
///
|
||||
@QueryParam(type = QueryParam.CompareTypeEnum.LIKE)
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@Schema(description = "统一支付接口审计日志查询参数")
|
||||
public class UnipayApiLogQuery {
|
||||
|
||||
@QueryParam(type = QueryParam.CompareTypeEnum.EQ)
|
||||
@Schema(description = "商户号")
|
||||
private String mchNo;
|
||||
|
||||
@QueryParam(type = QueryParam.CompareTypeEnum.EQ)
|
||||
@Schema(description = "请求ID")
|
||||
private String reqId;
|
||||
|
||||
@Schema(description = "接口路径")
|
||||
private String apiPath;
|
||||
|
||||
@Schema(description = "接口标题")
|
||||
private String apiTitle;
|
||||
|
||||
@QueryParam(type = QueryParam.CompareTypeEnum.EQ)
|
||||
@Schema(description = "是否成功")
|
||||
private Boolean success;
|
||||
|
||||
@QueryParam(type = QueryParam.CompareTypeEnum.EQ)
|
||||
@Schema(description = "链路追踪 ID")
|
||||
private String traceId;
|
||||
|
||||
@Schema(description = "真实接入 IP")
|
||||
private String requestIp;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package cn.daxpay.open.platform.capability.audit.log.result;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
/// # 统一支付接口审计日志结果
|
||||
///
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@Schema(description = "统一支付接口审计日志结果")
|
||||
public class UnipayApiLogResult {
|
||||
|
||||
@Schema(description = "日志 id")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "商户号")
|
||||
private String mchNo;
|
||||
|
||||
@Schema(description = "请求ID")
|
||||
private String reqId;
|
||||
|
||||
@Schema(description = "接口路径")
|
||||
private String apiPath;
|
||||
|
||||
@Schema(description = "接口标题")
|
||||
private String apiTitle;
|
||||
|
||||
@Schema(description = "HTTP 方法")
|
||||
private String requestMethod;
|
||||
|
||||
@Schema(description = "商户入参声明的客户端 IP")
|
||||
private String clientIp;
|
||||
|
||||
@Schema(description = "真实接入 IP")
|
||||
private String requestIp;
|
||||
|
||||
@Schema(description = "接入 IP 归属地")
|
||||
private String requestLocation;
|
||||
|
||||
@Schema(description = "是否成功")
|
||||
private Boolean success;
|
||||
|
||||
@Schema(description = "业务错误码")
|
||||
private Integer errorCode;
|
||||
|
||||
@Schema(description = "错误信息")
|
||||
private String errorMsg;
|
||||
|
||||
@Schema(description = "耗时(毫秒)")
|
||||
private Long durationMs;
|
||||
|
||||
@Schema(description = "链路追踪 ID")
|
||||
private String traceId;
|
||||
|
||||
@Schema(description = "请求参数")
|
||||
private String reqParam;
|
||||
|
||||
@Schema(description = "响应体")
|
||||
private String resBody;
|
||||
|
||||
@Schema(description = "操作时间 (UTC)")
|
||||
private OffsetDateTime operateTime;
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package cn.daxpay.open.platform.capability.audit.log.service.log;
|
||||
|
||||
import cn.daxpay.open.platform.capability.audit.log.convert.LogConvert;
|
||||
import cn.daxpay.open.platform.capability.audit.log.dao.UnipayApiLogDbManager;
|
||||
import cn.daxpay.open.platform.capability.audit.log.entity.UnipayApiLogDb;
|
||||
import cn.daxpay.open.platform.capability.audit.log.param.UnipayApiLogParam;
|
||||
import cn.daxpay.open.platform.capability.audit.log.param.UnipayApiLogQuery;
|
||||
import cn.daxpay.open.platform.capability.audit.log.result.UnipayApiLogResult;
|
||||
import cn.daxpay.open.platform.capability.audit.log.service.mask.AuditLogMaskService;
|
||||
import cn.daxpay.open.platform.common.mybatisplus.util.MpUtil;
|
||||
import cn.daxpay.open.platform.core.exception.DataNotExistException;
|
||||
import cn.daxpay.open.platform.core.rest.param.PageParam;
|
||||
import cn.daxpay.open.platform.core.rest.result.PageResult;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
|
||||
/// # 统一支付接口审计日志服务
|
||||
///
|
||||
/// 写入策略与操作日志类似,但队列满时**丢弃**(禁止同步写库,避免拖慢支付)。
|
||||
/// 请求/响应 body 强制脱敏并截断。
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class UnipayApiLogService {
|
||||
|
||||
/// 队列容量(高于操作日志,支付流量更大)
|
||||
private static final int DEFAULT_QUEUE_CAPACITY = 10000;
|
||||
|
||||
/// 单次最大批量保存条数
|
||||
private static final int BATCH_SIZE = 200;
|
||||
|
||||
/// body 最大长度(字符)
|
||||
private static final int PAYLOAD_MAX_LENGTH = 8192;
|
||||
|
||||
/// 支付接口强制全量脱敏键(sign 非敏感密钥,不脱敏)
|
||||
private static final String[] UNIPAY_FULL_MASK_KEYS = {
|
||||
"password", "token", "secret", "credential", "accesstoken", "refreshtoken",
|
||||
"idcard", "phone", "mobile", "bankcard", "cvv", "ssn", "passport",
|
||||
"authcode", "paybody"
|
||||
};
|
||||
|
||||
private final UnipayApiLogDbManager unipayApiLogManager;
|
||||
private final AuditLogMaskService maskService;
|
||||
|
||||
/// 内存缓冲队列
|
||||
private final BlockingQueue<UnipayApiLogParam> bufferQueue = new LinkedBlockingQueue<>(DEFAULT_QUEUE_CAPACITY);
|
||||
|
||||
/// 添加入队:强制脱敏 + 截断;队列满则丢弃
|
||||
public void add(UnipayApiLogParam param) {
|
||||
if (param == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 请求参数强制脱敏截断
|
||||
if (StrUtil.isNotEmpty(param.getReqParam())) {
|
||||
param.setReqParam(maskService.process(
|
||||
param.getReqParam(),
|
||||
true,
|
||||
UNIPAY_FULL_MASK_KEYS,
|
||||
null,
|
||||
PAYLOAD_MAX_LENGTH));
|
||||
}
|
||||
// 响应体强制脱敏截断
|
||||
if (StrUtil.isNotEmpty(param.getResBody())) {
|
||||
param.setResBody(maskService.process(
|
||||
param.getResBody(),
|
||||
true,
|
||||
UNIPAY_FULL_MASK_KEYS,
|
||||
null,
|
||||
PAYLOAD_MAX_LENGTH));
|
||||
}
|
||||
// 错误信息截断
|
||||
if (StrUtil.isNotEmpty(param.getErrorMsg()) && param.getErrorMsg().length() > 512) {
|
||||
param.setErrorMsg(param.getErrorMsg().substring(0, 512));
|
||||
}
|
||||
|
||||
boolean offered = bufferQueue.offer(param);
|
||||
if (!offered) {
|
||||
// 队列满:丢弃,绝不同步写库拖慢支付
|
||||
log.warn("支付接口审计队列已满,丢弃日志 path={} mchNo={}", param.getApiPath(), param.getMchNo());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("支付接口审计入队失败 path={}: {}", param.getApiPath(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/// 定时批量保存(每 1 秒)
|
||||
@Scheduled(fixedDelay = 1000)
|
||||
public void flushBuffer() {
|
||||
if (bufferQueue.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<UnipayApiLogParam> batch = new ArrayList<>(BATCH_SIZE);
|
||||
bufferQueue.drainTo(batch, BATCH_SIZE);
|
||||
if (batch.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
List<UnipayApiLogDb> entities = batch.stream()
|
||||
.map(LogConvert.CONVERT::convert)
|
||||
.toList();
|
||||
unipayApiLogManager.saveAll(entities);
|
||||
log.debug("支付接口审计批量保存成功,数量: {}", entities.size());
|
||||
} catch (Exception e) {
|
||||
log.error("支付接口审计批量保存失败,待处理条数: {}", batch.size(), e);
|
||||
for (UnipayApiLogParam item : batch) {
|
||||
try {
|
||||
unipayApiLogManager.save(LogConvert.CONVERT.convert(item));
|
||||
} catch (Exception ex) {
|
||||
log.error("支付接口审计逐条写入失败 path={}", item.getApiPath(), ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 应用关闭时刷空队列
|
||||
@PreDestroy
|
||||
public void shutdown() {
|
||||
log.info("正在刷空支付接口审计缓冲队列...");
|
||||
List<UnipayApiLogParam> remaining = new ArrayList<>();
|
||||
bufferQueue.drainTo(remaining);
|
||||
if (remaining.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
List<UnipayApiLogDb> entities = remaining.stream()
|
||||
.map(LogConvert.CONVERT::convert)
|
||||
.toList();
|
||||
unipayApiLogManager.saveAll(entities);
|
||||
log.info("应用关闭前保存剩余支付接口审计: {} 条", entities.size());
|
||||
} catch (Exception e) {
|
||||
log.error("应用关闭前保存剩余支付接口审计失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/// 详情
|
||||
public UnipayApiLogResult findById(Long id) {
|
||||
return unipayApiLogManager.findById(id)
|
||||
.map(UnipayApiLogDb::toResult)
|
||||
.orElseThrow(DataNotExistException::new);
|
||||
}
|
||||
|
||||
/// 分页
|
||||
public PageResult<UnipayApiLogResult> page(PageParam pageParam, UnipayApiLogQuery query) {
|
||||
return MpUtil.toPageResult(unipayApiLogManager.page(pageParam, query));
|
||||
}
|
||||
|
||||
/// 按天数清理
|
||||
public void deleteByDay(int deleteDay) {
|
||||
OffsetDateTime offset = OffsetDateTime.now(ZoneOffset.UTC).minusDays(deleteDay);
|
||||
unipayApiLogManager.deleteByOffset(offset);
|
||||
}
|
||||
|
||||
/// 当前队列大小(监控用)
|
||||
public int getQueueSize() {
|
||||
return bufferQueue.size();
|
||||
}
|
||||
}
|
||||
@@ -789,6 +789,10 @@
|
||||
},
|
||||
"riskCheckAfterPay": {
|
||||
"notNull": "Post-pay logging toggle cannot be empty"
|
||||
},
|
||||
"reqId": {
|
||||
"notBlank": "Request ID is required",
|
||||
"size": "Request ID must not exceed 64 characters"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -767,6 +767,10 @@
|
||||
},
|
||||
"timestampToleranceSeconds": {
|
||||
"min": "Toleransi stempel waktu tidak boleh kurang dari 1 detik"
|
||||
},
|
||||
"reqId": {
|
||||
"notBlank": "Request ID wajib diisi",
|
||||
"size": "Request ID maksimal 64 karakter"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -789,6 +789,10 @@
|
||||
},
|
||||
"riskCheckAfterPay": {
|
||||
"notNull": "決済後記録スイッチは必須です"
|
||||
},
|
||||
"reqId": {
|
||||
"notBlank": "リクエストIDは必須です",
|
||||
"size": "リクエストIDは64文字以内です"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -789,6 +789,10 @@
|
||||
},
|
||||
"riskCheckAfterPay": {
|
||||
"notNull": "결제 후 기록 스위치는 필수입니다"
|
||||
},
|
||||
"reqId": {
|
||||
"notBlank": "요청 ID는 필수입니다",
|
||||
"size": "요청 ID는 64자를 초과할 수 없습니다"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -767,6 +767,10 @@
|
||||
},
|
||||
"timestampToleranceSeconds": {
|
||||
"min": "Toleransi cap masa tidak boleh kurang daripada 1 saat"
|
||||
},
|
||||
"reqId": {
|
||||
"notBlank": "Request ID diperlukan",
|
||||
"size": "Request ID tidak boleh melebihi 64 aksara"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -767,6 +767,10 @@
|
||||
},
|
||||
"timestampToleranceSeconds": {
|
||||
"min": "ค่าคลาดเคลื่อนการประทับเวลาต้องไม่น้อยกว่า 1 วินาที"
|
||||
},
|
||||
"reqId": {
|
||||
"notBlank": "ต้องระบุ Request ID",
|
||||
"size": "Request ID ต้องไม่เกิน 64 ตัวอักษร"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -767,6 +767,10 @@
|
||||
},
|
||||
"timestampToleranceSeconds": {
|
||||
"min": "Dung sai dấu thời gian không được nhỏ hơn 1 giây"
|
||||
},
|
||||
"reqId": {
|
||||
"notBlank": "Request ID không được để trống",
|
||||
"size": "Request ID không quá 64 ký tự"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -789,6 +789,10 @@
|
||||
},
|
||||
"riskCheckAfterPay": {
|
||||
"notNull": "事后补录开关不能为空"
|
||||
},
|
||||
"reqId": {
|
||||
"notBlank": "请求ID不可为空",
|
||||
"size": "请求ID不可超过64位"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -789,6 +789,10 @@
|
||||
},
|
||||
"riskCheckAfterPay": {
|
||||
"notNull": "事後補錄開關不能為空"
|
||||
},
|
||||
"reqId": {
|
||||
"notBlank": "請求ID不可為空",
|
||||
"size": "請求ID不可超過64位"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -789,6 +789,10 @@
|
||||
},
|
||||
"riskCheckAfterPay": {
|
||||
"notNull": "事後補錄開關不能為空"
|
||||
},
|
||||
"reqId": {
|
||||
"notBlank": "請求ID不可為空",
|
||||
"size": "請求ID不可超過64位"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,6 +288,12 @@ public interface PermCodes {
|
||||
/// 菜单编码
|
||||
String MENU = "system:log:operate";
|
||||
}
|
||||
|
||||
/// 支付接口日志 menuCode=system:log:unipay
|
||||
interface Unipay {
|
||||
/// 菜单编码
|
||||
String MENU = "system:log:unipay";
|
||||
}
|
||||
}
|
||||
|
||||
/// 公告通知 menuCode=system:notify:notice
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package cn.daxpay.open.platform.core.event;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
/// # 统一支付接口访问审计事件
|
||||
///
|
||||
/// 由开放支付验签切面发布,审计模块异步落库。纯 POJO,无 Spring 依赖。
|
||||
/// 业务索引:`mchNo` + `reqId`;不单独索引 appId/业务单号。
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class UnipayApiAccessLogEvent {
|
||||
|
||||
/// 商户号
|
||||
private String mchNo;
|
||||
|
||||
/// 请求 ID(商户传入,审计主索引)
|
||||
private String reqId;
|
||||
|
||||
/// 接口路径
|
||||
private String apiPath;
|
||||
|
||||
/// 接口标题
|
||||
private String apiTitle;
|
||||
|
||||
/// HTTP 方法
|
||||
private String requestMethod;
|
||||
|
||||
/// 商户入参声明的客户端 IP
|
||||
private String clientIp;
|
||||
|
||||
/// 真实接入 IP
|
||||
private String requestIp;
|
||||
|
||||
/// 是否成功
|
||||
private Boolean success;
|
||||
|
||||
/// 业务错误码
|
||||
private Integer errorCode;
|
||||
|
||||
/// 错误信息
|
||||
private String errorMsg;
|
||||
|
||||
/// 耗时(毫秒)
|
||||
private Long durationMs;
|
||||
|
||||
/// 链路追踪 ID
|
||||
private String traceId;
|
||||
|
||||
/// 请求参数 JSON(未脱敏,由审计模块强制脱敏)
|
||||
private String reqParam;
|
||||
|
||||
/// 响应体 JSON(未脱敏,由审计模块强制脱敏)
|
||||
private String resBody;
|
||||
|
||||
/// 操作时间 (UTC)
|
||||
private OffsetDateTime operateTime;
|
||||
}
|
||||
Reference in New Issue
Block a user