| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386 |
- package com.etotem.cfc.aspect;
- import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
- import com.etotem.cfc.entity.AuditLog;
- import com.etotem.cfc.entity.CfEarnRule;
- import com.etotem.cfc.entity.CfRateTier;
- import com.etotem.cfc.entity.ServiceConfigItem;
- import com.etotem.cfc.entity.User;
- import com.etotem.cfc.mapper.CfEarnRuleMapper;
- import com.etotem.cfc.mapper.CfRateTierMapper;
- import com.etotem.cfc.mapper.ServiceConfigItemMapper;
- import com.etotem.cfc.mapper.UserMapper;
- import com.etotem.cfc.service.AuditLogService;
- import com.fasterxml.jackson.core.type.TypeReference;
- import com.fasterxml.jackson.databind.ObjectMapper;
- import org.aspectj.lang.ProceedingJoinPoint;
- import org.aspectj.lang.annotation.Around;
- import org.aspectj.lang.annotation.Aspect;
- import org.aspectj.lang.annotation.Pointcut;
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- import org.springframework.stereotype.Component;
- import org.springframework.web.context.request.RequestContextHolder;
- import org.springframework.web.context.request.ServletRequestAttributes;
- import javax.annotation.Resource;
- import javax.servlet.http.HttpServletRequest;
- import java.util.Arrays;
- import java.util.Date;
- import java.util.HashSet;
- import java.util.LinkedHashMap;
- import java.util.Map;
- import java.util.Set;
- /**
- * 管理端写操作统一审计切面(W0.3 审计日志统一)。
- *
- * 拦截范围:/api/admin/** 下 CF 相关管理端控制器({@code AdminCf*} + {@code AdminServiceConfigController})
- * 的写语义方法(save / update / toggle / delete / adjust 等),自动落 {@code audit_log} 并记录改前/改后值;
- * 审计落库成功后由 {@code AuditLogService} 触发 {@code CfConfigCache.invalidate()}。
- *
- * 约定:
- * - 读语义方法(list / history / balance / logs / invalidate 等)不拦截,不产生审计行;
- * - 业务方法抛异常时不落审计(避免为失败的写操作留痕),审计自身异常仅 warn,绝不阻塞业务请求;
- * - 新增 CF 管理端控制器时,类名以 {@code AdminCf} 开头即自动纳入拦截,无需修改本切面。
- *
- * 口径见 docs/superpowers/plans/2026-10-24-cf-value-unification-plan.md §3.7 W0.3
- */
- @Aspect
- @Component
- public class AuditLogAspect {
- private static final Logger log = LoggerFactory.getLogger(AuditLogAspect.class);
- static final String MODULE_CONFIG = "config";
- static final String MODULE_EARN_RULE = "earn_rule";
- static final String MODULE_TIER = "tier";
- static final String MODULE_BALANCE = "balance";
- private static final int MAX_SNAPSHOT_CHARS = 16000;
- private static final TypeReference<Map<String, Object>> MAP_TYPE =
- new TypeReference<Map<String, Object>>() {
- };
- /** 写语义动作白名单(读语义一律不审计) */
- private static final Set<String> WRITE_ACTIONS = new HashSet<>(Arrays.asList(
- "save", "update", "toggle", "delete", "adjust",
- "upsert", "enable", "disable", "create", "edit", "remove"));
- @Resource
- private AuditLogService auditLogService;
- @Resource
- private UserMapper userMapper;
- @Resource
- private ServiceConfigItemMapper serviceConfigItemMapper;
- @Resource
- private CfEarnRuleMapper cfEarnRuleMapper;
- @Resource
- private CfRateTierMapper cfRateTierMapper;
- @Resource
- private ObjectMapper objectMapper;
- /** CF 相关管理端控制器(AdminCf* + 服务配置中心) */
- @Pointcut("within(com.etotem.cfc.controller.admin.AdminCf*) "
- + "|| within(com.etotem.cfc.controller.admin.AdminServiceConfigController)")
- public void cfAdminController() {
- }
- @Around("cfAdminController()")
- public Object aroundAdminWrite(ProceedingJoinPoint pjp) throws Throwable {
- String module = null;
- String action = null;
- String targetId = null;
- String beforeJson = null;
- try {
- HttpServletRequest request = currentRequest();
- if (request != null) {
- module = resolveModule(request.getRequestURI());
- if (module != null) {
- action = resolveAction(pjp.getSignature().getName());
- if (action != null) {
- targetId = resolveTargetId(module, pjp.getArgs());
- beforeJson = snapshot(module, targetId);
- }
- }
- }
- } catch (Exception e) {
- log.warn("审计切面解析失败,跳过审计:{}", e.getMessage());
- }
- // 业务方法:审计不改变其返回值/异常语义
- Object result = pjp.proceed();
- if (module != null && action != null) {
- try {
- HttpServletRequest request = currentRequest();
- String afterJson = snapshot(module, targetId);
- if (afterJson == null) {
- afterJson = toJson(payload(pjp.getArgs()));
- }
- persist(request, module, action, targetId, beforeJson, afterJson);
- } catch (Exception e) {
- log.warn("审计落库异常(不影响业务):{}", e.getMessage());
- }
- }
- return result;
- }
- /** 按请求 URI 判定模块:config / earn_rule / tier / balance */
- private String resolveModule(String uri) {
- if (uri == null) {
- return null;
- }
- String path = uri.toLowerCase();
- if (path.contains("/service-config")) {
- return MODULE_CONFIG;
- }
- if (path.contains("/cf-earn-rule")) {
- return MODULE_EARN_RULE;
- }
- if (path.contains("/tier")) {
- return MODULE_TIER;
- }
- if (path.contains("/cf-balance") || path.contains("/cf-value")) {
- return MODULE_BALANCE;
- }
- return null;
- }
- private String resolveAction(String methodName) {
- if (methodName == null || methodName.isEmpty()) {
- return null;
- }
- return WRITE_ACTIONS.contains(methodName) ? methodName : null;
- }
- private String resolveTargetId(String module, Object[] args) {
- switch (module) {
- case MODULE_CONFIG:
- return firstValue(args, "configKey", "key");
- case MODULE_EARN_RULE:
- return firstValue(args, "code", "ruleCode");
- case MODULE_TIER:
- return firstValue(args, "id", "tierId");
- case MODULE_BALANCE:
- return firstValue(args, "memberId", "userId", "id");
- default:
- return null;
- }
- }
- private String targetTypeOf(String module) {
- switch (module) {
- case MODULE_CONFIG:
- return "service_config";
- case MODULE_EARN_RULE:
- return "cf_earn_rule";
- case MODULE_TIER:
- return "cf_rate_tier";
- case MODULE_BALANCE:
- return "cf_balance";
- default:
- return null;
- }
- }
- /**
- * 目标对象快照(DB 实时读取,新建为 null,删除后为 null)。
- * balance 模块暂无实体快照入口(F-2 接入 CfBalanceService 后补),回退为请求参数快照。
- */
- private String snapshot(String module, String targetId) {
- try {
- switch (module) {
- case MODULE_CONFIG:
- if (targetId == null) {
- return null;
- }
- ServiceConfigItem item = serviceConfigItemMapper.selectOne(
- new LambdaQueryWrapper<ServiceConfigItem>()
- .eq(ServiceConfigItem::getConfigKey, targetId)
- .last("LIMIT 1"));
- return toJson(item);
- case MODULE_EARN_RULE:
- if (targetId == null) {
- return null;
- }
- CfEarnRule rule = cfEarnRuleMapper.selectOne(
- new LambdaQueryWrapper<CfEarnRule>()
- .eq(CfEarnRule::getCode, targetId)
- .last("LIMIT 1"));
- return toJson(rule);
- case MODULE_TIER:
- Long tierId = toLong(targetId);
- if (tierId == null) {
- return null;
- }
- return toJson(cfRateTierMapper.selectById(tierId));
- default:
- return null;
- }
- } catch (Exception e) {
- log.warn("审计快照读取失败 module={} targetId={}:{}", module, targetId, e.getMessage());
- return null;
- }
- }
- private void persist(HttpServletRequest request, String module, String action, String targetId,
- String beforeJson, String afterJson) {
- Long operatorId = null;
- String operatorName = null;
- if (request != null) {
- Object uid = request.getAttribute("userId");
- if (uid instanceof Number && ((Number) uid).longValue() > 0) {
- operatorId = ((Number) uid).longValue();
- operatorName = resolveOperatorName(operatorId);
- }
- }
- if (operatorName == null && operatorId != null) {
- operatorName = "uid_" + operatorId;
- }
- AuditLog audit = new AuditLog();
- audit.setOperatorId(operatorId);
- audit.setOperatorName(operatorName);
- audit.setModule(module);
- audit.setAction(action);
- audit.setTargetType(targetTypeOf(module));
- audit.setTargetId(truncate(targetId, 128));
- audit.setBeforeValue(truncate(beforeJson));
- audit.setAfterValue(truncate(afterJson));
- audit.setIp(clientIp(request));
- audit.setCreatedAt(new Date());
- if (auditLogService.record(audit)) {
- log.info("审计已落库 module={} action={} targetId={} operator={}",
- module, action, audit.getTargetId(), operatorName);
- }
- }
- private String resolveOperatorName(Long userId) {
- try {
- User user = userMapper.selectById(userId);
- if (user != null) {
- if (user.getNickname() != null && !user.getNickname().trim().isEmpty()) {
- return user.getNickname();
- }
- if (user.getRealName() != null && !user.getRealName().trim().isEmpty()) {
- return user.getRealName();
- }
- }
- } catch (Exception e) {
- log.warn("查询审计操作人失败 userId={}:{}", userId, e.getMessage());
- }
- return null;
- }
- private String clientIp(HttpServletRequest request) {
- if (request == null) {
- return null;
- }
- String ip = request.getHeader("X-Forwarded-For");
- if (ip != null && ip.indexOf(',') >= 0) {
- ip = ip.substring(0, ip.indexOf(',')).trim();
- }
- if (ip == null || ip.trim().isEmpty() || "unknown".equalsIgnoreCase(ip.trim())) {
- ip = request.getHeader("X-Real-IP");
- }
- if (ip == null || ip.trim().isEmpty() || "unknown".equalsIgnoreCase(ip.trim())) {
- ip = request.getRemoteAddr();
- }
- return truncate(ip, 64);
- }
- private String firstValue(Object[] args, String... keys) {
- if (args == null) {
- return null;
- }
- for (Object arg : args) {
- Map<String, Object> map = toMap(arg);
- if (map == null) {
- continue;
- }
- for (String key : keys) {
- Object v = map.get(key);
- if (v != null && !String.valueOf(v).trim().isEmpty()) {
- return String.valueOf(v);
- }
- }
- }
- return null;
- }
- private Object payload(Object[] args) {
- if (args == null || args.length == 0) {
- return null;
- }
- for (Object arg : args) {
- Map<String, Object> map = toMap(arg);
- if (map != null && !map.isEmpty()) {
- return map;
- }
- }
- return null;
- }
- private Map<String, Object> toMap(Object arg) {
- if (arg == null) {
- return null;
- }
- try {
- if (arg instanceof Map) {
- Map<String, Object> out = new LinkedHashMap<>();
- for (Map.Entry<?, ?> entry : ((Map<?, ?>) arg).entrySet()) {
- if (entry.getKey() != null) {
- out.put(String.valueOf(entry.getKey()), entry.getValue());
- }
- }
- return out;
- }
- return objectMapper.convertValue(arg, MAP_TYPE);
- } catch (Exception e) {
- return null;
- }
- }
- private HttpServletRequest currentRequest() {
- ServletRequestAttributes attrs =
- (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
- return attrs == null ? null : attrs.getRequest();
- }
- private String toJson(Object obj) {
- if (obj == null) {
- return null;
- }
- try {
- return truncate(objectMapper.writeValueAsString(obj));
- } catch (Exception e) {
- log.warn("审计快照序列化失败:{}", e.getMessage());
- return null;
- }
- }
- private Long toLong(String value) {
- try {
- return value == null ? null : Long.valueOf(value.trim());
- } catch (NumberFormatException e) {
- return null;
- }
- }
- private String truncate(String value) {
- return truncate(value, MAX_SNAPSHOT_CHARS);
- }
- private String truncate(String value, int max) {
- if (value == null) {
- return null;
- }
- return value.length() > max ? value.substring(0, max) : value;
- }
- }
|