刘帅
2026-09-22 bc68f4393cafe39c4fce614c8128936067ea25f7
bug:登录问题和操作日志
4个文件已添加
13个文件已修改
764 ■■■■■ 已修改文件
cloud-server-gateway/src/main/java/cn/mb/cloud/gateway/filter/AccountAuditGlobalFilter.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cloud-server-management/src/main/java/com/dsh/guns/core/aop/LogAop.java 128 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cloud-server-management/src/main/java/com/dsh/guns/core/aop/ManagementOperationAuditAspect.java 198 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cloud-server-management/src/main/java/com/dsh/guns/modular/system/audit/AuditColumnCommentResolver.java 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cloud-server-management/src/main/java/com/dsh/guns/modular/system/audit/AuditDataFormatter.java 105 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cloud-server-management/src/main/java/com/dsh/guns/modular/system/audit/AuditField.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cloud-server-management/src/main/java/com/dsh/guns/modular/system/audit/ManagementFeignAuditInterceptor.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cloud-server-management/src/main/java/com/dsh/guns/modular/system/controller/code/OperatorController.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cloud-server-management/src/main/java/com/dsh/guns/modular/system/controller/system/LoginController.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cloud-server-management/src/main/java/com/dsh/guns/modular/system/model/User.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cloud-server-management/src/main/webapp/WEB-INF/view/common/_container.html 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cloud-server-management/src/main/webapp/WEB-INF/view/password.html 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cloud-server-management/src/main/webapp/WEB-INF/view/system/user/user.html 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cloud-server-management/src/main/webapp/static/js/common/sensitive-info.js 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cloud-server-management/src/main/webapp/static/modular/system/user/user.js 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cloud-server-other/src/main/java/com/dsh/other/audit/AccountAuditWebConfig.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cloud-server-other/src/main/java/com/dsh/other/audit/FeignAuditConfiguration.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
cloud-server-gateway/src/main/java/cn/mb/cloud/gateway/filter/AccountAuditGlobalFilter.java
@@ -44,7 +44,9 @@
    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()));
@@ -106,12 +108,28 @@
        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";
cloud-server-management/src/main/java/com/dsh/guns/core/aop/LogAop.java
@@ -8,7 +8,10 @@
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;
@@ -19,8 +22,13 @@
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;
/**
 * 日志记录
@@ -33,6 +41,8 @@
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() {
@@ -40,17 +50,119 @@
    @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 {
cloud-server-management/src/main/java/com/dsh/guns/core/aop/ManagementOperationAuditAspect.java
New file
@@ -0,0 +1,198 @@
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; }
    }
}
cloud-server-management/src/main/java/com/dsh/guns/modular/system/audit/AuditColumnCommentResolver.java
New file
@@ -0,0 +1,67 @@
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;
    }
}
cloud-server-management/src/main/java/com/dsh/guns/modular/system/audit/AuditDataFormatter.java
New file
@@ -0,0 +1,105 @@
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\\\"***\\\"");
    }
}
cloud-server-management/src/main/java/com/dsh/guns/modular/system/audit/AuditField.java
New file
@@ -0,0 +1,13 @@
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();
}
cloud-server-management/src/main/java/com/dsh/guns/modular/system/audit/ManagementFeignAuditInterceptor.java
@@ -5,47 +5,15 @@
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";
    }
}
cloud-server-management/src/main/java/com/dsh/guns/modular/system/controller/code/OperatorController.java
@@ -145,9 +145,14 @@
        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));
cloud-server-management/src/main/java/com/dsh/guns/modular/system/controller/system/LoginController.java
@@ -1,6 +1,5 @@
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;
@@ -99,8 +98,20 @@
    @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))) {
@@ -116,8 +127,10 @@
//        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";
cloud-server-management/src/main/java/com/dsh/guns/modular/system/model/User.java
@@ -6,6 +6,7 @@
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;
@@ -27,19 +28,20 @@
     * 主键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密码盐
     */
@@ -47,35 +49,35 @@
    /**
     * 名字
     */
    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;
    /**
     * 创建时间
     */
@@ -88,14 +90,14 @@
     * 对象类型(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;
cloud-server-management/src/main/webapp/WEB-INF/view/common/_container.html
@@ -64,7 +64,7 @@
    <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>
cloud-server-management/src/main/webapp/WEB-INF/view/password.html
@@ -60,7 +60,7 @@
        <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="">
@@ -79,9 +79,8 @@
                <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>
@@ -165,26 +164,68 @@
        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;
    }
cloud-server-management/src/main/webapp/WEB-INF/view/system/user/user.html
@@ -82,7 +82,7 @@
        </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({
cloud-server-management/src/main/webapp/static/js/common/sensitive-info.js
@@ -1,6 +1,6 @@
/** 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: '敏感信息查看'
@@ -20,10 +20,17 @@
                        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) {
cloud-server-management/src/main/webapp/static/modular/system/user/user.js
@@ -120,25 +120,8 @@
    }
};
/** 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);
};
/**
cloud-server-other/src/main/java/com/dsh/other/audit/AccountAuditWebConfig.java
@@ -1,13 +1,11 @@
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 {
}
cloud-server-other/src/main/java/com/dsh/other/audit/FeignAuditConfiguration.java
@@ -1,15 +1,9 @@
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; }
}