package com.dsh.guns.core.aop; import com.alibaba.fastjson.JSON; import com.dsh.guns.config.UserExt; import com.dsh.guns.core.common.annotion.BussinessLog; import com.dsh.guns.core.support.HttpKit; import com.dsh.guns.modular.system.audit.ManagementAuditWriter; import com.dsh.guns.modular.system.audit.AuditDataFormatter; import com.dsh.guns.modular.system.model.TOperator; import com.dsh.guns.modular.system.model.User; import com.dsh.guns.modular.system.service.TOperatorService; 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.stereotype.Component; import org.springframework.ui.Model; import javax.annotation.Resource; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Date; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.UUID; /** * 管理后台的兜底操作审计。旧的 @BussinessLog 仍由 LogAop 记录;这里补齐 * 各业务页面未标注注解的新增、修改、删除、审核、状态变更等操作。 */ @Aspect @Component public class ManagementOperationAuditAspect { @Resource private ManagementAuditWriter auditWriter; @Resource private TOperatorService operatorService; @Around("execution(public * com.dsh.guns.modular.system.controller..*.*(..))") public Object recordOperation(ProceedingJoinPoint point) throws Throwable { HttpServletRequest request = currentRequest(); Method method = ((MethodSignature) point.getSignature()).getMethod(); boolean shouldRecord = request != null && method.getAnnotation(BussinessLog.class) == null && shouldAudit(request.getMethod(), request.getRequestURI()); String beforeData = shouldRecord ? beforeData(request) : null; try { Object result = point.proceed(); if (shouldRecord) append(point, request, "SUCCESS", null, beforeData); return result; } catch (Throwable ex) { if (shouldRecord) append(point, request, "FAIL", ex.getMessage(), beforeData); throw ex; } } private void append(ProceedingJoinPoint point, HttpServletRequest request, String result, String error, String beforeData) { try { User user = UserExt.getUser(); if (user == null) return; String uri = request.getRequestURI(); Map audit = new LinkedHashMap(); audit.put("traceId", UUID.randomUUID().toString().replace("-", "")); audit.put("sourceService", "mb-cloud-management"); audit.put("sourceType", "HTTP"); audit.put("operationType", operationType(uri, request.getMethod())); audit.put("riskLevel", riskLevel(uri)); audit.put("operationName", operationName(uri, request.getMethod())); audit.put("requestMethod", request.getMethod()); audit.put("requestUri", uri); audit.put("requestParams", requestData(request)); audit.put("beforeData", beforeData); audit.put("afterData", afterData(request, beforeData)); audit.put("userId", user.getId()); audit.put("userAccount", user.getAccount()); audit.put("clientIp", request.getRemoteAddr()); audit.put("result", result); audit.put("errorMessage", mask(error)); audit.put("createdAt", new Date()); auditWriter.append(audit); } catch (Exception ignored) { // 审计链路不能影响后台业务。 } } private boolean shouldAudit(String method, String uri) { String value = uri == null ? "" : uri.toLowerCase(); if (value.startsWith("/accountauditlog") || value.startsWith("/system/sensitive")) return false; if ("PUT".equalsIgnoreCase(method) || "PATCH".equalsIgnoreCase(method) || "DELETE".equalsIgnoreCase(method)) return true; return "POST".equalsIgnoreCase(method) && value.matches(".*(add|create|save|insert|edit|update|modify|delete|remove|del|change|set|reset|freeze|unfreeze|enable|disable|commit|submit|upload|audit|approve|reject|refund|withdraw|payment).*" ); } private String operationType(String uri, String method) { String value = uri.toLowerCase(); if ("DELETE".equalsIgnoreCase(method) || value.matches(".*(delete|remove|del).*")) return "DELETE"; if (value.matches(".*(add|create|save|insert|commit|submit|upload).*")) return "CREATE"; if (value.matches(".*(change|state|status|freeze|unfreeze|enable|disable|audit|approve|reject|refund|withdraw|payment).*")) return "STATUS_CHANGE"; return "UPDATE"; } /** 将管理端接口翻译为日志中可直接阅读的中文说明。 */ private String operationName(String uri, String method) { String value = uri == null ? "" : uri.toLowerCase(); String action; if ("DELETE".equalsIgnoreCase(method) || value.matches(".*(delete|remove|del).*")) action = "删除"; else if (value.matches(".*(add|create|save|insert|commit|submit|upload).*")) action = "新增"; else if (value.matches(".*(audit|approve|reject).*")) action = "审核"; else if (value.matches(".*(freeze|unfreeze|enable|disable|change|state|status).*")) action = "变更状态"; else if (value.matches(".*reset.*")) action = "重置"; else action = "修改"; return action + resourceName(value) + (resourceName(value).startsWith("接口") ? "(" + uri + ")" : ""); } private String resourceName(String uri) { String[][] names = { {"operatoruser", "运营商员工"}, {"operator", "运营商"}, {"store", "门店"}, {"coursepackage", "课程包"}, {"course", "课程"}, {"coach", "教练"}, {"student", "学员"}, {"referee", "裁判"}, {"competition", "赛事"}, {"ticket", "门票"}, {"coupon", "优惠券"}, {"goods", "商品"}, {"pointmerchar", "积分商品"}, {"site", "场地"}, {"city", "城市"}, {"notice", "公告"}, {"question", "题目"}, {"advertisement", "广告"}, {"finance", "财务"}, {"vip", "会员"}, {"welfare", "福利"}, {"gate", "闸机"}, {"agreement", "协议"}, {"discount", "折扣"}, {"record", "记录"} }; for (String[] item : names) if (uri.contains(item[0])) return item[1]; return "接口"; } private String riskLevel(String uri) { return uri.toLowerCase().matches(".*(finance|payment|refund|withdraw|balance).*" ) ? "CRITICAL" : "NORMAL"; } private String requestData(HttpServletRequest request) { Map data = new LinkedHashMap(); Map parameters = request.getParameterMap(); if (parameters != null && !parameters.isEmpty()) data.put("params", parameters); return AuditDataFormatter.format(data); } /** * 可按资源逐步扩展真实快照读取。运营商管理是当前明确缺失的场景, * 在执行更新前/后直接从数据库读取,避免把前端提交值误写成“修改前”。 */ private String beforeData(HttpServletRequest request) { if (!request.getRequestURI().toLowerCase().contains("/operator/")) return null; Integer id = integerParameter(request, "id", "operatorId", "operatorIdOne"); if (id == null) return null; TOperator operator = operatorService.getById(id); return operatorSnapshot(operator); } private String afterData(HttpServletRequest request, String beforeData) { if (beforeData != null && request.getRequestURI().toLowerCase().contains("/operator/")) { Integer id = integerParameter(request, "id", "operatorId", "operatorIdOne"); TOperator operator = id == null ? null : operatorService.getById(id); return operatorSnapshot(operator); } return requestData(request); } private Integer integerParameter(HttpServletRequest request, String... names) { for (String name : names) { try { String value = request.getParameter(name); if (value != null && !value.trim().isEmpty()) return Integer.valueOf(value); } catch (NumberFormatException ignored) { // 尝试下一个可能的主键参数名。 } } return null; } /** 显式字段快照,避免 ActiveRecord/代理对象序列化不稳定,并确保日志字段全为中文。 */ private String operatorSnapshot(TOperator operator) { if (operator == null) return null; Map snapshot = new LinkedHashMap(); snapshot.put("运营商ID", operator.getId()); snapshot.put("运营商名称", operator.getName()); snapshot.put("关联用户ID", operator.getUserId()); snapshot.put("运营范围类型", operator.getType()); snapshot.put("商户绑定状态", operator.getStatus()); snapshot.put("运营商状态", operator.getState()); snapshot.put("微信商户号", operator.getMerchantNumber()); snapshot.put("支付宝商户号", operator.getMerchantNumberAli()); snapshot.put("微信商户全称", operator.getMerchantName()); return AuditDataFormatter.format(snapshot); } private String mask(String text) { if (text == null) return null; return text.replaceAll("(?i)(\\\"?(password|token|authorization|secret|idcard|identity|bank(card)?|mobile|phone)\\\"?\\s*[:=]\\s*)\\\"[^\\\"]*\\\"", "$1\\\"***\\\""); } private HttpServletRequest currentRequest() { try { return HttpKit.getRequest(); } catch (Exception ignored) { return null; } } }