| | |
| | | public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { |
| | | ServerHttpRequest request = exchange.getRequest(); |
| | | String uri = request.getURI().getRawPath(); |
| | | if (uri.startsWith("/internal/audit-logs")) return chain.filter(exchange); |
| | | if (uri.startsWith("/internal/audit-logs") || !shouldAudit(request.getMethodValue(), uri)) { |
| | | return chain.filter(exchange); |
| | | } |
| | | long startedAt = System.currentTimeMillis(); |
| | | return chain.filter(exchange).then(Mono.<Void>fromRunnable(() -> writeAudit(exchange, startedAt)) |
| | | .subscribeOn(Schedulers.boundedElastic())); |
| | |
| | | String lower = uri.toLowerCase(); |
| | | if (lower.contains("login") || lower.contains("token")) return "LOGIN"; |
| | | if (lower.matches(".*(status|state|ticket|payment|finance|refund|withdraw|balance).*")) return "STATUS_CHANGE"; |
| | | if ("POST".equalsIgnoreCase(method)) return "CREATE"; |
| | | if (lower.matches(".*(add|create|save|insert|commit|submit|upload).*")) return "CREATE"; |
| | | if ("PUT".equalsIgnoreCase(method) || "PATCH".equalsIgnoreCase(method)) return "UPDATE"; |
| | | if ("DELETE".equalsIgnoreCase(method)) return "DELETE"; |
| | | if (lower.matches(".*(edit|update|modify|change|set|reset|freeze|unfreeze|enable|disable|audit|approve|reject).*")) return "UPDATE"; |
| | | if (lower.matches(".*(phone|mobile|idcard|identity|bank|wallet).*")) return "VIEW_SENSITIVE"; |
| | | return "READ"; |
| | | } |
| | | |
| | | /** |
| | | * 网关只记录外部用户真正触发的业务变更、登录和敏感数据访问。 |
| | | * POST 在本系统中大量用于列表/详情查询,不能再按 HTTP 方法一概当作新增。 |
| | | */ |
| | | private boolean shouldAudit(String method, String uri) { |
| | | String lower = uri == null ? "" : uri.toLowerCase(); |
| | | if (lower.matches(".*(login|token|phone|mobile|idcard|identity|bank|wallet).*")) return true; |
| | | if ("PUT".equalsIgnoreCase(method) || "PATCH".equalsIgnoreCase(method) |
| | | || "DELETE".equalsIgnoreCase(method)) return true; |
| | | return "POST".equalsIgnoreCase(method) |
| | | && lower.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 riskLevel(String uri, String type) { |
| | | String lower = uri.toLowerCase(); |
| | | if ("STATUS_CHANGE".equals(type) || lower.matches(".*(finance|payment|refund|withdraw|balance|ticket).*")) return "CRITICAL"; |
| | |
| | | import com.dsh.guns.core.log.factory.LogTaskFactory; |
| | | import com.dsh.guns.core.support.HttpKit; |
| | | import com.dsh.guns.core.util.Contrast; |
| | | import com.dsh.guns.modular.system.audit.ManagementAuditWriter; |
| | | import com.dsh.guns.modular.system.audit.AuditDataFormatter; |
| | | import com.dsh.guns.modular.system.model.User; |
| | | import com.dsh.guns.modular.system.service.IUserService; |
| | | import org.aspectj.lang.ProceedingJoinPoint; |
| | | import org.aspectj.lang.Signature; |
| | | import org.aspectj.lang.annotation.Around; |
| | |
| | | import org.slf4j.LoggerFactory; |
| | | import org.springframework.stereotype.Component; |
| | | |
| | | import javax.annotation.Resource; |
| | | import javax.servlet.http.HttpServletRequest; |
| | | import java.util.Date; |
| | | import java.util.HashMap; |
| | | import java.lang.reflect.Method; |
| | | import java.util.Map; |
| | | import java.util.UUID; |
| | | |
| | | /** |
| | | * 日志记录 |
| | |
| | | public class LogAop { |
| | | |
| | | private Logger log = LoggerFactory.getLogger(this.getClass()); |
| | | @Resource private ManagementAuditWriter auditWriter; |
| | | @Resource private IUserService userService; |
| | | |
| | | @Pointcut(value = "@annotation(com.dsh.guns.core.common.annotion.BussinessLog)") |
| | | public void cutService() { |
| | |
| | | |
| | | @Around("cutService()") |
| | | public Object recordSysLog(ProceedingJoinPoint point) throws Throwable { |
| | | |
| | | //先执行业务 |
| | | Object result = point.proceed(); |
| | | |
| | | String beforeData = beforeData(); |
| | | try { |
| | | handle(point); |
| | | } catch (Exception e) { |
| | | log.error("日志记录出错!", e); |
| | | Object result = point.proceed(); |
| | | appendUnifiedAudit(point, "SUCCESS", null, beforeData); |
| | | return result; |
| | | } catch (Throwable e) { |
| | | appendUnifiedAudit(point, "FAIL", e.getMessage(), beforeData); |
| | | throw e; |
| | | } |
| | | } |
| | | |
| | | return result; |
| | | /** |
| | | * @BussinessLog 是后台已有的“需要记录的用户操作”白名单。 |
| | | * 在这里写统一审计,能够覆盖 /mgr/add 等直接访问管理端的请求, |
| | | * 并避免将其内部 Feign 调用和 POST 查询误记为操作。 |
| | | */ |
| | | private void appendUnifiedAudit(ProceedingJoinPoint point, String result, String errorMessage, String beforeData) { |
| | | try { |
| | | Method method = ((MethodSignature) point.getSignature()).getMethod(); |
| | | BussinessLog annotation = method.getAnnotation(BussinessLog.class); |
| | | User user = UserExt.getUser(); |
| | | if (annotation == null || user == null) return; |
| | | |
| | | HttpServletRequest request = HttpKit.getRequest(); |
| | | Map<String, Object> audit = new HashMap<String, Object>(); |
| | | audit.put("traceId", UUID.randomUUID().toString().replace("-", "")); |
| | | audit.put("sourceService", "mb-cloud-management"); |
| | | audit.put("sourceType", "HTTP"); |
| | | audit.put("operationType", operationType(annotation.value())); |
| | | audit.put("riskLevel", "NORMAL"); |
| | | audit.put("operationName", annotation.value()); |
| | | audit.put("requestMethod", request.getMethod()); |
| | | audit.put("requestUri", request.getRequestURI()); |
| | | // 审计表字段为 varchar,必须在调用审计服务前序列化;直接传 Map 会导致 |
| | | // Spring 无法绑定到 SysAccountAuditLog.requestParams(String),从而整条日志写入失败。 |
| | | audit.put("requestParams", AuditDataFormatter.format(maskedParameters(HttpKit.getRequestParameters()), User.class)); |
| | | audit.put("beforeData", beforeData); |
| | | audit.put("afterData", afterData(beforeData)); |
| | | audit.put("userId", user.getId()); |
| | | audit.put("userAccount", user.getAccount()); |
| | | audit.put("clientIp", HttpKit.getIp()); |
| | | audit.put("result", result); |
| | | audit.put("errorMessage", maskText(errorMessage)); |
| | | audit.put("createdAt", new Date()); |
| | | auditWriter.append(audit); |
| | | } catch (Exception auditException) { |
| | | // 审计失败不能影响原有业务。 |
| | | log.error("统一操作审计记录出错", auditException); |
| | | } |
| | | } |
| | | |
| | | private String operationType(String operationName) { |
| | | String value = operationName == null ? "" : operationName; |
| | | if (value.contains("删除")) return "DELETE"; |
| | | if (value.contains("添加") || value.contains("新增")) return "CREATE"; |
| | | if (value.contains("冻结") || value.contains("解冻") || value.contains("审核") || value.contains("状态")) return "STATUS_CHANGE"; |
| | | return "UPDATE"; |
| | | } |
| | | |
| | | private Map<String, String> maskedParameters(Map<String, String> parameters) { |
| | | Map<String, String> masked = new HashMap<String, String>(); |
| | | for (Map.Entry<String, String> entry : parameters.entrySet()) { |
| | | String key = entry.getKey(); |
| | | masked.put(key, key != null && key.toLowerCase().matches(".*(password|token|secret|idcard|identity|bank|card|mobile|phone).*") |
| | | ? "***" : entry.getValue()); |
| | | } |
| | | return masked; |
| | | } |
| | | |
| | | private String maskText(String text) { |
| | | return text == null ? null : text.replaceAll("(?i)(password|token|authorization|idcard|bankCard)\\s*[:=]\\s*[^,\\s&}]+", "$1=***"); |
| | | } |
| | | |
| | | /** 员工修改前后均从数据库读取真实记录,避免将前端提交值误当作旧值。 */ |
| | | private String beforeData() { |
| | | try { |
| | | HttpServletRequest request = HttpKit.getRequest(); |
| | | if (!"/mgr/edit".equals(request.getRequestURI())) return null; |
| | | Integer userId = Integer.valueOf(request.getParameter("id")); |
| | | return userSnapshot(userService.getById(userId)); |
| | | } catch (Exception ignored) { |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | private String afterData(String beforeData) { |
| | | if (beforeData == null) return null; |
| | | try { |
| | | Integer userId = Integer.valueOf(HttpKit.getRequest().getParameter("id")); |
| | | return userSnapshot(userService.getById(userId)); |
| | | } catch (Exception ignored) { |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | private String userSnapshot(User target) { |
| | | if (target == null) return null; |
| | | Map<String, Object> data = new HashMap<String, Object>(); |
| | | data.put("id", target.getId()); |
| | | data.put("account", target.getAccount()); |
| | | data.put("name", target.getName()); |
| | | data.put("phone", target.getPhone()); |
| | | data.put("birthday", target.getBirthday()); |
| | | data.put("sex", target.getSex()); |
| | | data.put("email", target.getEmail()); |
| | | data.put("roleid", target.getRoleid()); |
| | | data.put("deptid", target.getDeptid()); |
| | | data.put("status", target.getStatus()); |
| | | data.put("objectType", target.getObjectType()); |
| | | data.put("objectId", target.getObjectId()); |
| | | data.put("idCard", target.getIdCard()); |
| | | data.put("avatar", target.getAvatar()); |
| | | return AuditDataFormatter.format(data, User.class); |
| | | } |
| | | |
| | | private void handle(ProceedingJoinPoint point) throws Exception { |
| New file |
| | |
| | | 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<String, Object> audit = new LinkedHashMap<String, Object>(); |
| | | 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<String, Object> data = new LinkedHashMap<String, Object>(); |
| | | Map<String, String[]> 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<String, Object> snapshot = new LinkedHashMap<String, Object>(); |
| | | 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; } |
| | | } |
| | | } |
| New file |
| | |
| | | package com.dsh.guns.modular.system.audit; |
| | | |
| | | import com.baomidou.mybatisplus.annotation.TableField; |
| | | import com.baomidou.mybatisplus.annotation.TableName; |
| | | import org.springframework.jdbc.core.JdbcTemplate; |
| | | import org.springframework.stereotype.Component; |
| | | |
| | | import javax.annotation.Resource; |
| | | import java.lang.reflect.Field; |
| | | import java.util.Collections; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.concurrent.ConcurrentHashMap; |
| | | |
| | | /** 从数据库字段备注自动解析审计展示名;每个实体只查询一次并缓存。 */ |
| | | @Component |
| | | public class AuditColumnCommentResolver { |
| | | @Resource private JdbcTemplate jdbcTemplate; |
| | | private final Map<Class<?>, Map<String, String>> cache = new ConcurrentHashMap<Class<?>, Map<String, String>>(); |
| | | |
| | | public Map<String, String> resolve(Class<?> entityType) { |
| | | if (entityType == null) return Collections.emptyMap(); |
| | | Map<String, String> cached = cache.get(entityType); |
| | | if (cached != null) return cached; |
| | | Map<String, String> labels = load(entityType); |
| | | cache.put(entityType, labels); |
| | | return labels; |
| | | } |
| | | |
| | | private Map<String, String> load(Class<?> entityType) { |
| | | TableName tableName = entityType.getAnnotation(TableName.class); |
| | | if (tableName == null || tableName.value().trim().isEmpty()) return Collections.emptyMap(); |
| | | try { |
| | | List<Map<String, Object>> columns = jdbcTemplate.queryForList( |
| | | "SELECT COLUMN_NAME, COLUMN_COMMENT FROM information_schema.COLUMNS " |
| | | + "WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?", tableName.value()); |
| | | Map<String, String> comments = new LinkedHashMap<String, String>(); |
| | | for (Map<String, Object> column : columns) { |
| | | Object name = column.get("COLUMN_NAME"); |
| | | Object comment = column.get("COLUMN_COMMENT"); |
| | | if (name != null && comment != null && !String.valueOf(comment).trim().isEmpty()) { |
| | | comments.put(String.valueOf(name), concise(String.valueOf(comment))); |
| | | } |
| | | } |
| | | Map<String, String> result = new LinkedHashMap<String, String>(); |
| | | for (Class<?> type = entityType; type != null && type != Object.class; type = type.getSuperclass()) { |
| | | for (Field field : type.getDeclaredFields()) { |
| | | TableField tableField = field.getAnnotation(TableField.class); |
| | | String column = tableField == null || tableField.value().trim().isEmpty() ? field.getName() : tableField.value(); |
| | | String comment = comments.get(column); |
| | | if (comment != null) result.put(field.getName(), comment); |
| | | } |
| | | } |
| | | return result; |
| | | } catch (Exception ignored) { |
| | | return Collections.emptyMap(); |
| | | } |
| | | } |
| | | |
| | | private String concise(String comment) { |
| | | int bracket = comment.indexOf('('); |
| | | int asciiBracket = comment.indexOf('('); |
| | | int cut = bracket >= 0 ? bracket : asciiBracket; |
| | | return cut > 0 ? comment.substring(0, cut).trim() : comment; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.dsh.guns.modular.system.audit; |
| | | |
| | | import com.alibaba.fastjson.JSON; |
| | | import com.alibaba.fastjson.JSONArray; |
| | | import com.alibaba.fastjson.JSONObject; |
| | | import io.swagger.annotations.ApiModelProperty; |
| | | import cn.mb.cloud.common.core.util.SpringContextHolder; |
| | | |
| | | import java.lang.reflect.Field; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.Map; |
| | | |
| | | /** 审计数据统一脱敏并将常用字段名转换为中文,供日志页面直接展示。 */ |
| | | public final class AuditDataFormatter { |
| | | private static final Map<String, String> FIELD_NAMES = new LinkedHashMap<String, String>(); |
| | | static { |
| | | FIELD_NAMES.put("id", "ID"); FIELD_NAMES.put("ids", "ID列表"); |
| | | FIELD_NAMES.put("name", "名称"); FIELD_NAMES.put("username", "用户名"); FIELD_NAMES.put("userName", "用户名"); |
| | | FIELD_NAMES.put("account", "账号"); FIELD_NAMES.put("phone", "手机号"); FIELD_NAMES.put("mobile", "手机号"); |
| | | FIELD_NAMES.put("password", "密码"); FIELD_NAMES.put("oldPwd", "旧密码"); FIELD_NAMES.put("newPwd", "新密码"); |
| | | FIELD_NAMES.put("idCard", "身份证号"); FIELD_NAMES.put("identity", "身份信息"); |
| | | FIELD_NAMES.put("status", "状态"); FIELD_NAMES.put("state", "状态"); FIELD_NAMES.put("type", "类型"); |
| | | FIELD_NAMES.put("birthday", "生日"); FIELD_NAMES.put("sex", "性别"); FIELD_NAMES.put("roleid", "角色ID"); |
| | | FIELD_NAMES.put("deptid", "部门ID"); FIELD_NAMES.put("avatar", "头像"); FIELD_NAMES.put("jstime", "入职时间"); |
| | | FIELD_NAMES.put("email", "邮箱"); FIELD_NAMES.put("objectType", "归属类型"); FIELD_NAMES.put("objectId", "归属对象ID"); |
| | | FIELD_NAMES.put("remark", "备注"); FIELD_NAMES.put("reason", "原因"); FIELD_NAMES.put("content", "内容"); |
| | | FIELD_NAMES.put("operatorId", "运营商ID"); FIELD_NAMES.put("storeId", "门店ID"); |
| | | FIELD_NAMES.put("userId", "用户ID"); FIELD_NAMES.put("roleId", "角色ID"); FIELD_NAMES.put("roleIds", "角色ID列表"); |
| | | FIELD_NAMES.put("cityId", "城市ID"); FIELD_NAMES.put("courseId", "课程ID"); |
| | | FIELD_NAMES.put("createdAt", "创建时间"); FIELD_NAMES.put("updateTime", "更新时间"); |
| | | } |
| | | |
| | | private AuditDataFormatter() { } |
| | | |
| | | public static String format(Object value) { |
| | | return format(value, null); |
| | | } |
| | | |
| | | /** entityType 非空时,字段中文名完全由实体注解决定。 */ |
| | | public static String format(Object value, Class<?> entityType) { |
| | | if (value == null) return null; |
| | | Object json = value instanceof String ? parse((String) value) : JSON.toJSON(value); |
| | | return mask(JSON.toJSONString(localize(json, entityType))); |
| | | } |
| | | |
| | | private static Object parse(String value) { |
| | | try { return JSON.parse(value); } catch (Exception ignored) { return value; } |
| | | } |
| | | |
| | | private static Object localize(Object value, Class<?> entityType) { |
| | | if (value instanceof JSONObject) { |
| | | JSONObject source = (JSONObject) value; |
| | | JSONObject target = new JSONObject(true); |
| | | for (Map.Entry<String, Object> entry : source.entrySet()) { |
| | | target.put(displayName(entry.getKey(), entityType), localize(entry.getValue(), null)); |
| | | } |
| | | return target; |
| | | } |
| | | if (value instanceof Map) { |
| | | JSONObject target = new JSONObject(true); |
| | | for (Object item : ((Map<?, ?>) value).entrySet()) { |
| | | Map.Entry<?, ?> entry = (Map.Entry<?, ?>) item; |
| | | target.put(displayName(String.valueOf(entry.getKey()), entityType), localize(entry.getValue(), null)); |
| | | } |
| | | return target; |
| | | } |
| | | if (value instanceof JSONArray) { |
| | | JSONArray target = new JSONArray(); |
| | | for (Object item : (JSONArray) value) target.add(localize(item, null)); |
| | | return target; |
| | | } |
| | | return value; |
| | | } |
| | | |
| | | private static String displayName(String field, Class<?> entityType) { |
| | | String annotated = annotationName(entityType, field); |
| | | if (annotated != null) return annotated; |
| | | String name = FIELD_NAMES.get(field); |
| | | return name == null ? field : name; |
| | | } |
| | | |
| | | private static String annotationName(Class<?> entityType, String fieldName) { |
| | | if (entityType == null) return null; |
| | | for (Class<?> type = entityType; type != null && type != Object.class; type = type.getSuperclass()) { |
| | | try { |
| | | Field field = type.getDeclaredField(fieldName); |
| | | AuditField audit = field.getAnnotation(AuditField.class); |
| | | if (audit != null) return audit.value(); |
| | | ApiModelProperty api = field.getAnnotation(ApiModelProperty.class); |
| | | if (api != null && api.value() != null && !api.value().trim().isEmpty()) return api.value(); |
| | | } catch (NoSuchFieldException ignored) { } |
| | | } |
| | | try { |
| | | String comment = SpringContextHolder.getBean(AuditColumnCommentResolver.class).resolve(entityType).get(fieldName); |
| | | if (comment != null && !comment.trim().isEmpty()) return comment; |
| | | } catch (Exception ignored) { |
| | | // 审计展示名解析失败时回退字段名,不能影响业务。 |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | private static String mask(String text) { |
| | | return text.replaceAll("(?i)(\\\"?(password|oldpwd|newpwd|token|authorization|secret|idcard|identity|bank(card)?|mobile|phone|密码|手机号|身份证号)\\\"?\\s*[:=]\\s*)\\\"[^\\\"]*\\\"", "$1\\\"***\\\""); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.dsh.guns.modular.system.audit; |
| | | |
| | | import java.lang.annotation.ElementType; |
| | | import java.lang.annotation.Retention; |
| | | import java.lang.annotation.RetentionPolicy; |
| | | import java.lang.annotation.Target; |
| | | |
| | | /** 实体字段在审计日志中的中文展示名。 */ |
| | | @Retention(RetentionPolicy.RUNTIME) |
| | | @Target(ElementType.FIELD) |
| | | public @interface AuditField { |
| | | String value(); |
| | | } |
| | |
| | | import feign.RequestTemplate; |
| | | import org.springframework.stereotype.Component; |
| | | |
| | | import javax.annotation.Resource; |
| | | import java.util.Date; |
| | | import java.util.HashMap; |
| | | import java.util.Map; |
| | | import java.util.UUID; |
| | | |
| | | /** Spring 会把该 RequestInterceptor 应用于管理端全部 Feign 客户端。 */ |
| | | /** 仅透传敏感数据访问所需的内部标识;Feign 调用不是用户操作,不写审计日志。 */ |
| | | @Component |
| | | public class ManagementFeignAuditInterceptor implements RequestInterceptor { |
| | | @Resource private ManagementAuditWriter auditWriter; |
| | | |
| | | @Override |
| | | public void apply(RequestTemplate template) { |
| | | try { |
| | | Map<String, Object> audit = new HashMap<String, Object>(); |
| | | String url = template.url(); |
| | | if (url != null && url.startsWith("/internal/sensitive/") |
| | | && UserExt.getUser() != null && "admin".equalsIgnoreCase(UserExt.getUser().getAccount())) { |
| | | template.header("X-Internal-Sensitive-Access", "true"); |
| | | } |
| | | audit.put("traceId", UUID.randomUUID().toString().replace("-", "")); |
| | | audit.put("sourceService", "mb-cloud-management"); |
| | | audit.put("sourceType", "FEIGN"); |
| | | audit.put("operationType", "FEIGN_CALL"); |
| | | audit.put("riskLevel", riskLevel(url)); |
| | | audit.put("operationName", template.feignTarget() == null ? "Feign调用" : template.feignTarget().name()); |
| | | audit.put("requestMethod", template.method()); |
| | | audit.put("requestUri", url); |
| | | audit.put("requestParams", template.body() == null ? null : "bodyBytes=" + template.body().length); |
| | | audit.put("userId", UserExt.getUser() == null ? null : UserExt.getUser().getId()); |
| | | audit.put("userAccount", UserExt.getUser() == null ? null : UserExt.getUser().getAccount()); |
| | | audit.put("result", "REQUESTED"); |
| | | audit.put("createdAt", new Date()); |
| | | auditWriter.append(audit); |
| | | } catch (Exception ignored) { |
| | | // 审计异常绝不能阻塞原 Feign 调用。 |
| | | } |
| | | } |
| | | |
| | | private String riskLevel(String url) { |
| | | String value = url == null ? "" : url.toLowerCase(); |
| | | return value.matches(".*(ticket|payment|finance|refund|withdraw|balance|status|state).*") ? "CRITICAL" : "NORMAL"; |
| | | } |
| | | } |
| | |
| | | TOperator byId = operatorService.getById(id); |
| | | // 运营商名称 |
| | | model.addAttribute(byId.getName()); |
| | | User byId1 = userService.getById(byId.getUserId()); |
| | | model.addAttribute("userName",byId1.getName()); |
| | | model.addAttribute("phone",byId1.getPhone()); |
| | | // 历史数据可能存在 t_operator.userId 未同步的情况,不能因此让编辑页手机号丢失。 |
| | | User byId1 = byId.getUserId() == null ? null : userService.getById(byId.getUserId()); |
| | | if (byId1 == null) { |
| | | byId1 = userService.getOne(new QueryWrapper<User>() |
| | | .eq("objectId", id).eq("objectType", 2)); |
| | | } |
| | | model.addAttribute("userName", byId1 == null ? "" : byId1.getName()); |
| | | model.addAttribute("phone", byId1 == null ? "" : byId1.getPhone()); |
| | | model.addAttribute("data",byId); |
| | | // 获取全部的省 |
| | | List<TOperatorCity> list = operatorCityService.list(new QueryWrapper<TOperatorCity>().eq("operatorId", id).eq("pid",0)); |
| | |
| | | package com.dsh.guns.modular.system.controller.system; |
| | | |
| | | import cn.hutool.crypto.SecureUtil; |
| | | import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; |
| | | import com.dsh.course.feignClient.account.AppUserClient; |
| | | import com.dsh.course.feignClient.account.model.TAppUser; |
| | |
| | | @PostMapping("/updatePassword") |
| | | @ResponseBody |
| | | // @ResponseBody |
| | | public ResultUtil update(String username,String sms_code,String passwordS) { |
| | | public ResultUtil update(String username, String sms_code, String password, String passwordS) { |
| | | if (ToolUtil.isEmpty(username) || ToolUtil.isEmpty(sms_code) |
| | | || ToolUtil.isEmpty(password) || ToolUtil.isEmpty(passwordS)) { |
| | | return ResultUtil.error("账号、验证码和新密码不能为空"); |
| | | } |
| | | |
| | | String newPassword = AESUtils.decrypt(password); |
| | | String confirmedPassword = AESUtils.decrypt(passwordS); |
| | | if (ToolUtil.isEmpty(newPassword) || ToolUtil.isEmpty(newPassword.trim())) { |
| | | return ResultUtil.error("新密码无效"); |
| | | } |
| | | if (!newPassword.equals(confirmedPassword)) { |
| | | return ResultUtil.error("两次输入的新密码不一致"); |
| | | } |
| | | |
| | | String value = redisUtil.getValue(username); |
| | | if (!"123456".equals(sms_code) && (ToolUtil.isEmpty(value) || !value.equals(sms_code))) { |
| | |
| | | // if (User.getCode() == null) { |
| | | // User.setCode(UUIDUtil.getNumberRandom(16)); |
| | | // } |
| | | User.setPassword(SecureUtil.md5(passwordS)); |
| | | userService.updateById(User); |
| | | User.setPassword(MD5.md5(newPassword)); |
| | | if (!userService.updateById(User)) { |
| | | return ResultUtil.error("密码修改失败,请稍后重试"); |
| | | } |
| | | return ResultUtil.success(); |
| | | |
| | | // return "/login.html"; |
| | |
| | | import com.baomidou.mybatisplus.annotation.TableId; |
| | | import com.baomidou.mybatisplus.annotation.TableName; |
| | | import com.baomidou.mybatisplus.extension.activerecord.Model; |
| | | import com.dsh.guns.modular.system.audit.AuditField; |
| | | |
| | | import java.io.Serializable; |
| | | import java.util.Date; |
| | |
| | | * 主键id |
| | | */ |
| | | @TableId(value="id", type= IdType.AUTO) |
| | | @AuditField("员工ID") |
| | | private Integer id; |
| | | /** |
| | | * 头像 |
| | | */ |
| | | private String avatar; |
| | | @AuditField("头像") private String avatar; |
| | | /** |
| | | * 账号 |
| | | */ |
| | | private String account; |
| | | @AuditField("账号") private String account; |
| | | /** |
| | | * 密码 |
| | | */ |
| | | private String password; |
| | | @AuditField("密码") private String password; |
| | | /** |
| | | * md5密码盐 |
| | | */ |
| | |
| | | /** |
| | | * 名字 |
| | | */ |
| | | private String name; |
| | | @AuditField("员工姓名") private String name; |
| | | /** |
| | | * 生日 |
| | | */ |
| | | private Date birthday; |
| | | @AuditField("生日") private Date birthday; |
| | | /** |
| | | * 性别(1:男 2:女) |
| | | */ |
| | | private Integer sex; |
| | | @AuditField("性别") private Integer sex; |
| | | /** |
| | | * 电子邮件 |
| | | */ |
| | | private String email; |
| | | @AuditField("邮箱") private String email; |
| | | /** |
| | | * 电话 |
| | | */ |
| | | private String phone; |
| | | @AuditField("手机号") private String phone; |
| | | /** |
| | | * 角色id |
| | | */ |
| | | private String roleid; |
| | | @AuditField("角色ID") private String roleid; |
| | | /** |
| | | * 部门id |
| | | */ |
| | | private Integer deptid; |
| | | @AuditField("部门ID") private Integer deptid; |
| | | /** |
| | | * 状态(1:启用 2:冻结 3:删除) |
| | | */ |
| | | private Integer status; |
| | | @AuditField("状态") private Integer status; |
| | | /** |
| | | * 创建时间 |
| | | */ |
| | |
| | | * 对象类型(1=平台,2=城市管理员,3=门店) |
| | | */ |
| | | @TableField("objectType") |
| | | private Integer objectType; |
| | | @AuditField("归属类型") private Integer objectType; |
| | | /** |
| | | * objectId |
| | | */ |
| | | @TableField("objectId") |
| | | private Integer objectId; |
| | | @AuditField("归属对象ID") private Integer objectId; |
| | | @TableField("idCard") |
| | | private String idCard; |
| | | @AuditField("身份证号") private String idCard; |
| | | |
| | | public String getIdCard() { |
| | | return idCard; |
| | |
| | | <script src="${ctxPath}/js/common/web-upload-object.js"></script> |
| | | <script src="${ctxPath}/js/common/ztree-object.js"></script> |
| | | <script src="${ctxPath}/js/common/Feng.js"></script> |
| | | <script src="${ctxPath}/js/common/sensitive-info.js"></script> |
| | | <script src="${ctxPath}/js/common/sensitive-info.js?v=20260922-employee-idcard"></script> |
| | | <script src="${ctxPath}/js/plugins/sweetalert/sweetalert.min.js"></script> |
| | | <script src="${ctxPath}/layui/layui.js"></script> |
| | | |
| | |
| | | <img style="margin-left: 44px" src="${ctxPath}/img/logo.png" width="100px" height="120px"/> |
| | | <br/> |
| | | <h4 style="color: red;">${tips!}</h4> |
| | | <form class="m-t" role="form" action="${ctxPath}/updatePassword" method="post"> |
| | | <form id="passwordForm" class="m-t" role="form" onsubmit="return update(event);"> |
| | | <div class="item"> |
| | | <img src="${ctxPath}/img/account.png" /> |
| | | <input class="itemInput" type="text" name="username" id="username" placeholder="请输入账号" required="" value=""> |
| | |
| | | <input class="itemInput" type="password" name="passwordS" id="passwordS" placeholder="请确认新密码" required="" value=""> |
| | | </div> |
| | | |
| | | <button onclick="update()" class="btn full-width m-b" style="background-color: #0573EA;color: white;">保存</button> |
| | | <button class="btn full-width m-b" style="background-color: #0573EA;color: white;" onclick="getBack()">关闭</button> |
| | | <button type="submit" id="submit" style="display: none;"></button> |
| | | <button id="saveButton" type="submit" class="btn full-width m-b" style="background-color: #0573EA;color: white;">保存</button> |
| | | <button type="button" class="btn full-width m-b" style="background-color: #0573EA;color: white;" onclick="getBack()">关闭</button> |
| | | </p> |
| | | </form> |
| | | </div> |
| | |
| | | return decryptedStr.toString(); |
| | | } |
| | | |
| | | function update() { |
| | | let password = $('#password').val(); |
| | | password = encrypt(password); |
| | | $('#password').val(password); |
| | | function update(event) { |
| | | event.preventDefault(); |
| | | |
| | | var username = $.trim($('#username').val()); |
| | | var smsCode = $.trim($('#sms_code').val()); |
| | | var password = $('#password').val(); |
| | | var passwordS = $('#passwordS').val(); |
| | | |
| | | if (!username) { |
| | | alert('请输入账号'); |
| | | $('#username').focus(); |
| | | return false; |
| | | } |
| | | if (!smsCode) { |
| | | alert('请输入验证码'); |
| | | $('#sms_code').focus(); |
| | | return false; |
| | | } |
| | | if (!password || !$.trim(password)) { |
| | | alert('请输入新密码'); |
| | | $('#password').focus(); |
| | | return false; |
| | | } |
| | | if (!passwordS || !$.trim(passwordS)) { |
| | | alert('请确认新密码'); |
| | | $('#passwordS').focus(); |
| | | return false; |
| | | } |
| | | if (password !== passwordS) { |
| | | alert('两次输入的新密码不一致'); |
| | | $('#passwordS').focus(); |
| | | return false; |
| | | } |
| | | |
| | | var $saveButton = $('#saveButton'); |
| | | $saveButton.prop('disabled', true).text('保存中...'); |
| | | $.ajax({ |
| | | url: 'your-submit-url', |
| | | url: Feng.ctxPath + '/updatePassword', |
| | | type: 'POST', |
| | | data: $('#submit').serialize(), |
| | | success: function(response) { |
| | | // 根据响应的msg字段设置对应的提示 |
| | | // 成功提示 |
| | | alert("修改成功"); |
| | | window.location.href = Feng.ctxPath+"/login"; |
| | | data: { |
| | | username: username, |
| | | sms_code: smsCode, |
| | | password: encrypt(password), |
| | | passwordS: encrypt(passwordS) |
| | | }, |
| | | error: function() { |
| | | // 错误处理 |
| | | alert('当前账号不存在'); |
| | | success: function(response) { |
| | | if (response && (response.status === 200 || response.code === 200)) { |
| | | alert(response.msg || '修改成功'); |
| | | window.location.href = Feng.ctxPath + '/login'; |
| | | return; |
| | | } |
| | | alert((response && response.msg) || '修改失败,请稍后重试'); |
| | | }, |
| | | error: function(xhr) { |
| | | var response = xhr.responseJSON; |
| | | alert((response && response.msg) || '修改失败,请检查网络后重试'); |
| | | }, |
| | | complete: function() { |
| | | $saveButton.prop('disabled', false).text('保存'); |
| | | } |
| | | }); |
| | | return false; |
| | | } |
| | | |
| | | |
| | |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <script src="${ctxPath}/modular/system/user/user.js"></script> |
| | | <script src="${ctxPath}/modular/system/user/user.js?v=20260922-sensitive-view"></script> |
| | | <script src="${ctxPath}/layui/layui.js" charset="utf-8"></script> |
| | | <script> |
| | | laydate.render({ |
| | |
| | | /** Admin-only sensitive-information viewer. Every request is audited by the server. */ |
| | | var SensitiveInfo = { |
| | | view: function (url) { |
| | | view: function (url, showIdCard) { |
| | | layer.confirm('查看完整敏感信息将记录操作日志,是否继续?', { |
| | | icon: 3, |
| | | title: '敏感信息查看' |
| | |
| | | Feng.info('未查询到手机号'); |
| | | return; |
| | | } |
| | | var content = '<div class="form-horizontal" style="padding:20px"><div class="form-group">' |
| | | var fields = '<div class="form-group">' |
| | | + '<label class="col-sm-4 control-label">手机号:</label><div class="col-sm-8">' |
| | | + '<p class="form-control-static">' + SensitiveInfo.escape(String(phone)) |
| | | + '</p></div></div></div>'; |
| | | + '</p></div></div>'; |
| | | if (showIdCard && data.idCard) { |
| | | fields += '<div class="form-group">' |
| | | + '<label class="col-sm-4 control-label">身份证号:</label><div class="col-sm-8">' |
| | | + '<p class="form-control-static">' + SensitiveInfo.escape(String(data.idCard)) |
| | | + '</p></div></div>'; |
| | | } |
| | | var content = '<div class="form-horizontal" style="padding:20px">' + fields + '</div>'; |
| | | layer.open({type: 1, title: '敏感信息', area: ['480px', 'auto'], content: content}); |
| | | }, |
| | | error: function (xhr) { |
| | |
| | | } |
| | | }; |
| | | |
| | | /** The button is rendered only for admin; the API repeats the permission check and records an audit event. */ |
| | | MgrUser.viewSensitiveData = function () { |
| | | if (!this.check()) return; |
| | | layer.confirm('查看完整手机号和身份证号将记录审计日志,是否继续?', {icon: 3, title: '敏感信息查看'}, function (index) { |
| | | $.ajax({ |
| | | url: Feng.ctxPath + '/system/sensitive/employee/' + MgrUser.seItem.id, |
| | | type: 'GET', |
| | | success: function (data) { |
| | | if (!data) { Feng.info('未查询到员工信息'); return; } |
| | | var html = '<div style="padding:20px;line-height:28px">' |
| | | + '<div>姓名:' + (data.name || '') + '</div>' |
| | | + '<div>手机号:' + (data.phone || '') + '</div>' |
| | | + '<div>身份证号:' + (data.idCard || '') + '</div></div>'; |
| | | layer.open({type: 1, title: '员工敏感信息(已记录审计日志)', area: ['420px', '260px'], content: html}); |
| | | }, |
| | | error: function () { Feng.error('无权查看敏感信息或查询失败'); } |
| | | }); |
| | | layer.close(index); |
| | | }); |
| | | if (this.check()) SensitiveInfo.view('/system/sensitive/employee/' + this.seItem.id, true); |
| | | }; |
| | | |
| | | /** |
| | |
| | | package com.dsh.other.audit; |
| | | |
| | | import org.springframework.context.annotation.Configuration; |
| | | import org.springframework.web.servlet.config.annotation.InterceptorRegistry; |
| | | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; |
| | | |
| | | import javax.annotation.Resource; |
| | | |
| | | /** |
| | | * 审计由网关的外部请求及管理端的业务注解统一产生。 |
| | | * 不再在 other 服务拦截全部 HTTP 请求,否则管理端 Feign 调用会被误记为用户操作。 |
| | | */ |
| | | @Configuration |
| | | public class AccountAuditWebConfig implements WebMvcConfigurer { |
| | | @Resource private AccountAuditInterceptor accountAuditInterceptor; |
| | | @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(accountAuditInterceptor).addPathPatterns("/**"); } |
| | | public class AccountAuditWebConfig { |
| | | } |
| | |
| | | package com.dsh.other.audit; |
| | | |
| | | import feign.Logger; |
| | | import com.dsh.other.service.SysAccountAuditLogService; |
| | | import org.springframework.context.annotation.Bean; |
| | | |
| | | /** 为本服务全部 OpenFeign 客户端启用审计;请求参数正文不会被落库。 */ |
| | | /** Feign 是服务间实现细节,不能作为用户操作写入审计表。 */ |
| | | public class FeignAuditConfiguration { |
| | | /** 创建由 Spring 注入审计服务的 Feign 日志器。 */ |
| | | @Bean |
| | | public Logger feignLogger(SysAccountAuditLogService auditLogService) { |
| | | return new FeignAuditLogger(auditLogService); |
| | | } |
| | | @Bean public Logger.Level feignLoggerLevel() { return Logger.Level.BASIC; } |
| | | @Bean public Logger.Level feignLoggerLevel() { return Logger.Level.NONE; } |
| | | } |