diff --git a/src/main/java/com/yunhai/userbase/aspect/OapiLogAspect.java b/src/main/java/com/yunhai/userbase/aspect/OapiLogAspect.java
new file mode 100644
index 0000000..965e96e
--- /dev/null
+++ b/src/main/java/com/yunhai/userbase/aspect/OapiLogAspect.java
@@ -0,0 +1,241 @@
+package com.yunhai.userbase.aspect;
+
+import com.alibaba.fastjson.JSON;
+import com.yunhai.platform.appauth.AppAuthFilter;
+import com.yunhai.userbase.common.annotation.OapiLog;
+import com.yunhai.userbase.entity.ApplicationInfo;
+import com.yunhai.userbase.entity.SysOapiLog;
+import com.yunhai.userbase.repository.ApplicationInfoRepository;
+import com.yunhai.userbase.service.ISysOapiLogService;
+import com.yunhai.userbase.utils.GzipUtil;
+import com.yunhai.userbase.utils.IpUtils;
+import com.yunhai.userbase.utils.OperationLogResolver;
+import com.yunhai.userbase.utils.SensitiveDataMasker;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 开放接口(/oapi/**)调用日志切面。
+ * 拦截 {@code controller.oapi} 包下所有方法,规则:
+ * - POST 写操作:全量自动记录(无需注解),级别/类型由 OperationLogResolver 关键词判定;
+ * - GET 查询:默认不记录(高频调用,防刷爆表),满足任一条件则记录:方法标注 {@code @OapiLog}、或配置 oapi.log-get=true;
+ * - 排除 {@code SignKeyInternalController}(/oapi/internal,响应含 appSecret 明文,绝不落库);
+ * - 请求参数与响应均经 {@link SensitiveDataMasker} 脱敏后入库;
+ * - finally 中异步落库(有界池 oapiLogExecutor),失败不影响主业务。
+ *
+ * @author chenx
+ * @date 2026/8/20
+ */
+@Slf4j
+@Aspect
+@Component
+public class OapiLogAspect {
+
+ /** 内部密钥端点控制器,排除(响应含 appSecret 明文)。 */
+ private static final String INTERNAL_CONTROLLER = "com.yunhai.userbase.controller.oapi.SignKeyInternalController";
+
+ /** GET 默认不记录,开启本配置后全量记录。 */
+ @Value("${oapi.log-get:false}")
+ private boolean logGet;
+
+ @Autowired
+ private ISysOapiLogService sysOapiLogService;
+
+ @Autowired
+ private ApplicationInfoRepository applicationInfoRepository;
+
+ @Around("execution(* com.yunhai.userbase.controller.oapi..*.*(..))")
+ public Object around(ProceedingJoinPoint point) throws Throwable {
+ if (INTERNAL_CONTROLLER.equals(point.getSignature().getDeclaringTypeName())) {
+ return point.proceed();
+ }
+
+ HttpServletRequest request = getRequest();
+ String httpMethod = request != null ? request.getMethod() : "";
+ boolean isGet = "GET".equalsIgnoreCase(httpMethod);
+
+ OapiLog oapiLog = findAnnotation(point);
+ // 决策:POST 全记;GET 仅当注解强制或配置开启才记
+ if (isGet && oapiLog == null && !logGet) {
+ return point.proceed();
+ }
+
+ long start = System.currentTimeMillis();
+ SysOapiLog entity = buildEntity(point, request, httpMethod, oapiLog);
+
+ boolean saveResponse = oapiLog != null ? oapiLog.saveResponse() : true;
+ int maxResponseSize = oapiLog != null ? oapiLog.maxResponseSize() : 10240;
+
+ Object result = null;
+ try {
+ result = point.proceed();
+ entity.setStatusCode(200);
+ fillResponse(entity, result, saveResponse, maxResponseSize);
+ } catch (Throwable e) {
+ entity.setStatusCode(500);
+ entity.setErrorMsg(truncate(e.getMessage(), 2000));
+ throw e;
+ } finally {
+ entity.setCostTime(System.currentTimeMillis() - start);
+ sysOapiLogService.saveAsync(entity);
+ }
+ return result;
+ }
+
+ private OapiLog findAnnotation(ProceedingJoinPoint point) {
+ try {
+ MethodSignature signature = (MethodSignature) point.getSignature();
+ Method method = signature.getMethod();
+ return method.getAnnotation(OapiLog.class);
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ private SysOapiLog buildEntity(ProceedingJoinPoint point, HttpServletRequest request,
+ String httpMethod, OapiLog oapiLog) {
+ SysOapiLog entity = new SysOapiLog();
+
+ // 调用方应用:验签 attribute → appKey → application_info 解析应用名(失败记 appKey 原值,不阻塞)
+ resolveCallerApp(request, entity);
+
+ String requestUri = "";
+ if (request != null) {
+ entity.setRequestIp(IpUtils.getIpAddr(request));
+ requestUri = request.getRequestURI();
+ entity.setUserAgent(request.getHeader("User-Agent"));
+ entity.setRequestParams(maskRequestParams(point, request));
+ } else {
+ entity.setRequestParams(maskRequestParams(point, null));
+ }
+ entity.setRequestUri(requestUri);
+ entity.setRequestMethod(httpMethod);
+
+ String methodName = point.getSignature().getName();
+ entity.setLogLevel(OperationLogResolver.resolveLevel(httpMethod, requestUri, methodName).getCode());
+ entity.setOperationType(OperationLogResolver.resolveOperationType(httpMethod, requestUri, methodName));
+ entity.setClassMethod(point.getTarget().getClass().getName() + "." + methodName);
+ if (oapiLog != null && StringUtils.isNotBlank(oapiLog.value())) {
+ entity.setOperationDesc(oapiLog.value());
+ }
+ return entity;
+ }
+
+ /**
+ * 从 request attribute 解析调用方 appKey,并映射应用名。
+ * 无 attribute / 解析失败:appKey 记 0、应用名记 unknown,仅 warn,不抛异常。
+ */
+ private void resolveCallerApp(HttpServletRequest request, SysOapiLog entity) {
+ entity.setCallerAppKey(0);
+ entity.setCallerAppName("unknown");
+ if (request == null) {
+ return;
+ }
+ Object attr = request.getAttribute(AppAuthFilter.APP_AUTH_ATTR);
+ if (attr == null) {
+ log.warn("[oapi-log] no AppAuth attr (should be blocked by RequireAppSignFilter), uri={}",
+ request.getRequestURI());
+ return;
+ }
+ String appKeyStr = (attr instanceof String) ? (String) attr : String.valueOf(attr);
+ if (StringUtils.isBlank(appKeyStr)) {
+ log.warn("[oapi-log] AppAuth attr is blank, uri={}", request.getRequestURI());
+ return;
+ }
+ Integer appKey;
+ try {
+ appKey = Integer.parseInt(appKeyStr.trim());
+ } catch (NumberFormatException e) {
+ log.warn("[oapi-log] invalid appKey format={}, uri={}", appKeyStr, request.getRequestURI());
+ return;
+ }
+ entity.setCallerAppKey(appKey);
+ ApplicationInfo app = applicationInfoRepository.findByAppKey(appKey);
+ entity.setCallerAppName(app != null && StringUtils.isNotBlank(app.getName())
+ ? app.getName() : String.valueOf(appKey));
+ }
+
+ /**
+ * 构建请求参数:GET 优先 queryString;其余序列化方法入参(过滤 Servlet 三大件),统一过脱敏。
+ */
+ private String maskRequestParams(ProceedingJoinPoint point, HttpServletRequest request) {
+ try {
+ if (request != null && "GET".equalsIgnoreCase(request.getMethod())) {
+ String qs = request.getQueryString();
+ if (StringUtils.isNotBlank(qs)) {
+ return SensitiveDataMasker.maskJson(qs);
+ }
+ }
+ Object[] args = point.getArgs();
+ if (args == null || args.length == 0) {
+ return "";
+ }
+ List