tea 3 сар өмнө
parent
commit
84d95f7219

+ 138 - 0
kxmall-admin/src/main/java/com/kxmall/web/controller/logback/WeComAppender.java

@@ -0,0 +1,138 @@
+package com.kxmall.web.controller.logback;
+
+import ch.qos.logback.core.OutputStreamAppender;
+import org.springframework.util.StringUtils;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * 企业微信日志添加器
+ */
+public class WeComAppender<E> extends OutputStreamAppender<E> {
+
+    private String webhookUrl;
+    private String msgFormat;
+    private String application;
+    private String instance;
+    private Boolean enabled;
+
+    public void setWebhookUrl(String webhookUrl) {
+        this.webhookUrl = webhookUrl;
+    }
+
+    public void setMsgFormat(String msgFormat) {
+        this.msgFormat = msgFormat;
+    }
+
+    public void setApplication(String application) {
+        this.application = application;
+    }
+
+    public void setInstance(String instance) {
+        this.instance = instance;
+    }
+
+    public void setEnabled(Boolean enabled) {
+        this.enabled = enabled;
+    }
+
+    //    protected ConsoleTarget target = ConsoleTarget.SystemOut;
+//    protected boolean withJansi = false;
+
+//    private final static String WindowsAnsiOutputStream_CLASS_NAME = "org.fusesource.jansi.WindowsAnsiOutputStream";
+
+//    /**
+//     * Sets the value of the <b>Target</b> option. Recognized values are
+//     * "System.out" and "System.err". Any other value will be ignored.
+//     */
+//    public void setTarget(String value) {
+//        ConsoleTarget t = ConsoleTarget.findByName(value.trim());
+//        if (t == null) {
+//            targetWarn(value);
+//        } else {
+//            target = t;
+//        }
+//    }
+//
+//    /**
+//     * Returns the current value of the <b>target</b> property. The default value
+//     * of the option is "System.out".
+//     * <p/>
+//     * See also {@link #setTarget}.
+//     */
+//    public String getTarget() {
+//        return target.getName();
+//    }
+//
+//    private void targetWarn(String val) {
+//        Status status = new WarnStatus("[" + val + "] should be one of " + Arrays.toString(ConsoleTarget.values()), this);
+//        status.add(new WarnStatus("Using previously set target, System.out by default.", this));
+//        addStatus(status);
+//    }
+
+    @Override
+    public void start() {
+        WeComOutputStream weComOutputStream = new WeComOutputStream();
+        weComOutputStream.setWebhookUrl(this.webhookUrl);
+        weComOutputStream.setFormat(this.msgFormat);
+        weComOutputStream.setApplication(this.application);
+        weComOutputStream.setInstance(this.instance);
+//        OutputStream targetStream = target.getStream();
+//        // enable jansi only on Windows and only if withJansi set to true
+//        if (EnvUtil.isWindows() && withJansi) {
+//            targetStream = getTargetStreamForWindows(targetStream);
+//        }
+        String enabled = System.getenv("LOGBACK_WECOM_ENABLED");
+        if (StringUtils.hasText(enabled)) {
+            try {
+                this.enabled = Boolean.valueOf(enabled);
+            } catch (Exception e) {
+                //
+            }
+        }
+        if (Boolean.FALSE.equals(this.enabled)) {
+            setOutputStream(new OutputStream() {
+                @Override
+                public void write(int b) throws IOException {
+                    //空输出
+                }
+            });
+        } else {
+            setOutputStream(weComOutputStream);
+        }
+        super.start();
+    }
+
+
+//    private OutputStream getTargetStreamForWindows(OutputStream targetStream) {
+//        try {
+//            addInfo("Enabling JANSI WindowsAnsiOutputStream for the console.");
+//            Object windowsAnsiOutputStream = OptionHelper.instantiateByClassNameAndParameter(WindowsAnsiOutputStream_CLASS_NAME, Object.class, context,
+//                    OutputStream.class, targetStream);
+//            return (OutputStream) windowsAnsiOutputStream;
+//        } catch (Exception e) {
+//            addWarn("Failed to create WindowsAnsiOutputStream. Falling back on the default stream.", e);
+//        }
+//        return targetStream;
+//    }
+
+//    /**
+//     * @return
+//     */
+//    public boolean isWithJansi() {
+//        return withJansi;
+//    }
+//
+//    /**
+//     * If true, this appender will output to a stream which
+//     *
+//     * @param withJansi
+//     * @since 1.0.5
+//     */
+//    public void setWithJansi(boolean withJansi) {
+//        this.withJansi = withJansi;
+//    }
+//
+
+}

+ 195 - 0
kxmall-admin/src/main/java/com/kxmall/web/controller/logback/WeComOutputStream.java

@@ -0,0 +1,195 @@
+package com.kxmall.web.controller.logback;
+
+import org.apache.commons.lang3.StringEscapeUtils;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
+import org.springframework.util.StringUtils;
+import org.springframework.web.client.RestTemplate;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.charset.StandardCharsets;
+import java.text.SimpleDateFormat;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class WeComOutputStream extends OutputStream {
+
+    private String webhookUrl;
+    private String format;
+    private String application;
+    private String instance;
+    private final RestTemplate restTemplate;
+    private final LinkedBlockingQueue<byte[]> queue;
+    private final ExecutorService executorService;
+    private final AtomicBoolean isClose;
+    private final static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+
+    public WeComOutputStream() {
+        HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
+        factory.setConnectionRequestTimeout(3000);
+        factory.setConnectTimeout(3000);
+        factory.setReadTimeout(2000);
+        this.restTemplate = new RestTemplate(factory);
+
+        this.format = "{\n" +
+                "    \"msgtype\":\"markdown\",\n" +
+                "    \"markdown\":{\n" +
+                "        \"content\":\"\n" +
+                "<font color=\\\"red\\\">**警报发生**</font> \n" +
+                "**主机地址:** {{instance}}\n" +
+                "**应用名称:** {{application}}\n" +
+                "**告警时间:** {{time}}\n" +
+                "**告警摘要:** {{msg}}" +
+                "        \"" +
+                "    }\n" +
+                "}}";
+
+        this.queue = new LinkedBlockingQueue<>(1000);
+        this.executorService = Executors.newSingleThreadExecutor();
+        this.executorService.submit(this::sendMsg);
+        this.isClose = new AtomicBoolean(false);
+    }
+
+    @Override
+    public void write(int b) throws IOException {
+        // 不需要处理
+    }
+
+    @Override
+    public void write(byte[] b) throws IOException {
+        if (b == null || b.length <= 0) {
+            return;
+        }
+
+        try {
+            if (queue.size() > 1000) {
+                return;
+            }
+            queue.put(b);
+        } catch (InterruptedException e) {
+            //
+        }
+    }
+
+    private void sendMsg() {
+        while (!isClose.get()) {
+            try {
+                byte[] msg = queue.poll(3, TimeUnit.SECONDS);
+                if (msg == null) {
+                    continue;
+                }
+                String payload = format;
+                payload = StringUtils.replace(payload, "{{time}}", sdf.format(System.currentTimeMillis()));
+                payload = StringUtils.replace(payload, "{{instance}}", this.instance());
+                payload = StringUtils.replace(payload, "{{application}}", this.application());
+                String msgStr = new String(msg, StandardCharsets.UTF_8);
+                payload = StringUtils.replace(payload, "{{msg}}",  StringEscapeUtils.escapeJson(msgStr.substring(0,Math.min(350,msgStr.length()))));
+                HttpHeaders headers = new HttpHeaders();
+                headers.set("Content-Type", "application/json;charset=utf-8");
+                HttpEntity<String> requestEntity = new HttpEntity<>(payload, headers);
+                restTemplate.exchange(webhookUrl, HttpMethod.POST, requestEntity, Void.class);
+            } catch (Exception e) {
+                System.out.println("企业微信日志处理失败: "+e.getMessage());
+                //不处理
+            }
+        }
+    }
+
+    private String application(){
+        if (StringUtils.hasText(this.application)) {
+            return this.application;
+        }
+        String application = System.getenv("spring.application.name");
+        if (StringUtils.hasText(application)) {
+            this.application = application;
+            return this.application;
+        }
+        application = System.getenv("APPLICATION");
+        if (StringUtils.hasText(application)) {
+            this.application = application;
+            return this.application;
+        }
+        this.application = "unknown";
+        return this.application;
+    }
+    
+    private String instance() {
+        if (StringUtils.hasText(this.instance)) {
+            return this.instance;
+        }
+        String instance = System.getenv("instance");
+        if (StringUtils.hasText(instance)) {
+            this.instance = instance;
+            return this.instance;
+        }
+        instance = System.getenv("INSTANCE");
+        if (StringUtils.hasText(instance)) {
+            this.instance = instance;
+            return this.instance;
+        }
+
+        try {
+            String hostAddress = InetAddress.getLocalHost().getHostAddress();
+            if (StringUtils.hasText(hostAddress)) {
+                this.instance = hostAddress;
+                return this.instance;
+            }
+        } catch (UnknownHostException e) {
+            //不处理
+        }
+        this.instance = "unknown";
+        return this.instance;
+    }
+
+    @Override
+    public void close() throws IOException {
+//        super.close();
+        this.isClose.set(true);
+        this.executorService.shutdown();
+        this.queue.clear();
+    }
+
+
+    @Override
+    public void flush() throws IOException {
+//        super.flush();
+    }
+
+    public void setWebhookUrl(String webhookUrl) {
+        this.webhookUrl = webhookUrl;
+    }
+
+    public void setFormat(String format) {
+        if (format == null || format.trim().equals("")) {
+            return;
+        }
+        this.format = format;
+    }
+
+    public void setApplication(String application) {
+        this.application = application;
+    }
+
+    public void setInstance(String instance) {
+        this.instance = instance;
+    }
+
+//    public static void main(String[] args) throws IOException, InterruptedException {
+//        WeComOutputStream outputStream = new WeComOutputStream();
+//        outputStream.setWebhookUrl("https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=20397e1e-827a-49e3-94ed-26674a4f88bb");
+//        outputStream.setApplication("buiness");
+//        System.out.println(outputStream.format);
+//
+//        outputStream.write("sdsfdsdf".getBytes(StandardCharsets.UTF_8));
+//
+//    }c
+
+}

+ 177 - 0
kxmall-admin/src/main/resources/logback-spring.xml

@@ -0,0 +1,177 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration scan="false" scanPeriod="10 seconds" debug="false">
+    <contextName>kxmall_logs</contextName>
+
+    <!-- 通用属性定义 -->
+    <property name="log.path" value="./logs/"/>
+
+    <!-- 彩色日志配置 -->
+    <conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter"/>
+    <conversionRule conversionWord="wex"
+                    converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter"/>
+    <conversionRule conversionWord="wEx"
+                    converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter"/>
+
+    <!-- 日志格式定义 -->
+    <property name="CONSOLE_LOG_PATTERN"
+              value="%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(%-5level) %clr([%15.15thread]){magenta} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}"/>
+    <property name="FILE_LOG_PATTERN"
+              value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"/>
+
+    <!-- 控制台输出 -->
+    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder>
+            <pattern>${CONSOLE_LOG_PATTERN}</pattern>
+            <charset>UTF-8</charset>
+        </encoder>
+    </appender>
+
+    <!-- 文件控制台输出 -->
+    <appender name="FILE_CONSOLE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${log.path}/sys-console.log</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <fileNamePattern>${log.path}/sys-console.%d{yyyy-MM-dd}.log</fileNamePattern>
+            <maxHistory>7</maxHistory>
+        </rollingPolicy>
+        <encoder>
+            <pattern>${FILE_LOG_PATTERN}</pattern>
+            <charset>UTF-8</charset>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
+            <level>DEBUG</level>
+        </filter>
+    </appender>
+
+    <!-- DEBUG级别日志 -->
+    <appender name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${log.path}/debug.log</file>
+        <encoder>
+            <pattern>${FILE_LOG_PATTERN}</pattern>
+            <charset>UTF-8</charset>
+        </encoder>
+        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+            <fileNamePattern>${log.path}/debug-%d{yyyy-MM-dd}-%i.log.gz</fileNamePattern>
+            <maxFileSize>1GB</maxFileSize>
+            <maxHistory>30</maxHistory>
+            <totalSizeCap>50GB</totalSizeCap>
+        </rollingPolicy>
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>debug</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+
+    <!-- INFO级别日志 -->
+    <appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${log.path}/info.log</file>
+        <encoder>
+            <pattern>${FILE_LOG_PATTERN}</pattern>
+            <charset>UTF-8</charset>
+        </encoder>
+        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+            <fileNamePattern>${log.path}/info-%d{yyyy-MM-dd}-%i.log.gz</fileNamePattern>
+            <maxFileSize>1GB</maxFileSize>
+            <maxHistory>30</maxHistory>
+            <totalSizeCap>60GB</totalSizeCap>
+        </rollingPolicy>
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>info</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+
+    <!-- WARN级别日志 -->
+    <appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${log.path}/warn.log</file>
+        <encoder>
+            <pattern>${FILE_LOG_PATTERN}</pattern>
+            <charset>UTF-8</charset>
+        </encoder>
+        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+            <fileNamePattern>${log.path}/warn-%d{yyyy-MM-dd}-%i.log.gz</fileNamePattern>
+            <maxFileSize>1GB</maxFileSize>
+            <maxHistory>15</maxHistory>
+            <totalSizeCap>10GB</totalSizeCap>
+        </rollingPolicy>
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>warn</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+
+    <!-- ERROR级别日志 -->
+    <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${log.path}/error.log</file>
+        <encoder>
+            <pattern>${FILE_LOG_PATTERN}</pattern>
+            <charset>UTF-8</charset>
+        </encoder>
+        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+            <fileNamePattern>${log.path}/error-%d{yyyy-MM-dd}-%i.log.gz</fileNamePattern>
+            <maxFileSize>1GB</maxFileSize>
+            <maxHistory>15</maxHistory>
+            <totalSizeCap>10GB</totalSizeCap>
+        </rollingPolicy>
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>ERROR</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+
+    <!-- 企业微信通知 -->
+    <appender name="WeCom" class="com.kxmall.web.controller.logback.WeComAppender">
+        <webhookUrl>https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=你的群聊机器人key</webhookUrl>
+        <application>kxmall</application>
+        <enabled>true</enabled>
+        <encoder>
+            <pattern>${FILE_LOG_PATTERN}</pattern>
+            <charset>UTF-8</charset>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>error</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+
+    <!-- 异步日志配置 -->
+    <appender name="ASYNC_INFO" class="ch.qos.logback.classic.AsyncAppender">
+        <discardingThreshold>0</discardingThreshold>
+        <queueSize>512</queueSize>
+        <appender-ref ref="INFO_FILE"/>
+    </appender>
+
+    <appender name="ASYNC_ERROR" class="ch.qos.logback.classic.AsyncAppender">
+        <discardingThreshold>0</discardingThreshold>
+        <queueSize>512</queueSize>
+        <appender-ref ref="ERROR_FILE"/>
+    </appender>
+
+    <!-- 开发环境配置 -->
+    <springProfile name="dev">
+        <root level="debug">
+            <appender-ref ref="CONSOLE"/>
+            <appender-ref ref="FILE_CONSOLE"/>
+            <appender-ref ref="DEBUG_FILE"/>
+            <appender-ref ref="ASYNC_INFO"/>
+            <appender-ref ref="WARN_FILE"/>
+            <appender-ref ref="ASYNC_ERROR"/>
+            <appender-ref ref="WeCom"/>
+        </root>
+    </springProfile>
+
+    <!-- 生产环境配置 -->
+    <root level="info">
+        <appender-ref ref="INFO_FILE" />
+        <appender-ref ref="FILE_CONSOLE"/>
+        <appender-ref ref="ASYNC_INFO"/>
+        <appender-ref ref="WARN_FILE"/>
+        <appender-ref ref="ASYNC_ERROR"/>
+        <appender-ref ref="WeCom"/>
+    </root>
+
+</configuration>

+ 41 - 0
kxmall-common/src/main/java/com/kxmall/common/annotation/Log.java

@@ -0,0 +1,41 @@
+package com.kxmall.common.annotation;
+
+import com.kxmall.common.enums.BusinessType;
+import com.kxmall.common.enums.OperatorType;
+
+import java.lang.annotation.*;
+
+/**
+ * 自定义操作日志记录注解
+ *
+ * @author kxmall
+ */
+@Target({ElementType.PARAMETER, ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface Log {
+    /**
+     * 模块
+     */
+    String title() default "";
+
+    /**
+     * 功能
+     */
+    BusinessType businessType() default BusinessType.OTHER;
+
+    /**
+     * 操作人类别
+     */
+    OperatorType operatorType() default OperatorType.MANAGE;
+
+    /**
+     * 是否保存请求的参数
+     */
+    boolean isSaveRequestData() default true;
+
+    /**
+     * 是否保存响应的参数
+     */
+    boolean isSaveResponseData() default true;
+}

+ 44 - 0
kxmall-common/src/main/java/com/kxmall/common/core/domain/event/LogininforEvent.java

@@ -0,0 +1,44 @@
+package com.kxmall.common.core.domain.event;
+
+import lombok.Data;
+
+import javax.servlet.http.HttpServletRequest;
+import java.io.Serializable;
+
+/**
+ * 登录事件
+ *
+ * @author kxmall
+ */
+
+@Data
+public class LogininforEvent implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 用户账号
+     */
+    private String username;
+
+    /**
+     * 登录状态 0成功 1失败
+     */
+    private String status;
+
+    /**
+     * 提示消息
+     */
+    private String message;
+
+    /**
+     * 请求体
+     */
+    private HttpServletRequest request;
+
+    /**
+     * 其他参数
+     */
+    private Object[] args;
+
+}

+ 42 - 0
kxmall-common/src/main/java/com/kxmall/common/core/domain/model/LoginBody.java

@@ -0,0 +1,42 @@
+package com.kxmall.common.core.domain.model;
+
+import com.kxmall.common.constant.UserConstants;
+import lombok.Data;
+import org.hibernate.validator.constraints.Length;
+
+import javax.validation.constraints.NotBlank;
+
+/**
+ * 用户登录对象
+ *
+ * @author kxmall
+ */
+
+@Data
+public class LoginBody {
+
+    /**
+     * 用户名
+     */
+    @NotBlank(message = "{user.username.not.blank}")
+    @Length(min = UserConstants.USERNAME_MIN_LENGTH, max = UserConstants.USERNAME_MAX_LENGTH, message = "{user.username.length.valid}")
+    private String username;
+
+    /**
+     * 用户密码
+     */
+    @NotBlank(message = "{user.password.not.blank}")
+    @Length(min = UserConstants.PASSWORD_MIN_LENGTH, max = UserConstants.PASSWORD_MAX_LENGTH, message = "{user.password.length.valid}")
+    private String password;
+
+    /**
+     * 验证码
+     */
+    private String code;
+
+    /**
+     * 唯一标识
+     */
+    private String uuid;
+
+}

+ 137 - 0
kxmall-common/src/main/java/com/kxmall/common/core/domain/model/LoginUser.java

@@ -0,0 +1,137 @@
+package com.kxmall.common.core.domain.model;
+
+import com.kxmall.common.core.domain.dto.RoleDTO;
+import com.kxmall.common.helper.LoginHelper;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * 登录用户身份权限
+ *
+ * @author kxmall
+ */
+
+@Data
+@NoArgsConstructor
+public class LoginUser implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 用户ID
+     */
+    private Long userId;
+
+    /**
+     * 部门ID
+     */
+    private Long deptId;
+
+    /**
+     * 部门名
+     */
+    private String deptName;
+
+    /**
+     * 用户唯一标识
+     */
+    private String token;
+
+    /**
+     * 用户类型
+     */
+    private String userType;
+
+    /**
+     * 登录时间
+     */
+    private Long loginTime;
+
+    /**
+     * 过期时间
+     */
+    private Long expireTime;
+
+    /**
+     * 登录IP地址
+     */
+    private String ipaddr;
+
+    /**
+     * 登录地点
+     */
+    private String loginLocation;
+
+    /**
+     * 浏览器类型
+     */
+    private String browser;
+
+    /**
+     * 操作系统
+     */
+    private String os;
+
+    /**
+     * 菜单权限
+     */
+    private Set<String> menuPermission;
+
+    /**
+     * 角色权限
+     */
+    private Set<String> rolePermission;
+
+    /**
+     * 仓库权限
+     */
+    private Set<Long> storagePermission;
+
+    /**
+     * 用户名
+     */
+    private String username;
+
+    /**
+     * 昵称
+     */
+    private String nickName;
+
+    /**
+     * 登录类型
+     */
+    private Integer loginType;
+
+    /**
+     * 根据登录方式设置不同的openid
+     */
+    private String openId;
+
+    /**
+     * 角色对象
+     */
+    private List<RoleDTO> roles;
+
+    /**
+     * 数据权限 当前角色ID
+     */
+    private Long roleId;
+
+    /**
+     * 获取登录id
+     */
+    public String getLoginId() {
+        if (userType == null) {
+            throw new IllegalArgumentException("用户类型不能为空");
+        }
+        if (userId == null) {
+            throw new IllegalArgumentException("用户ID不能为空");
+        }
+        return userType + LoginHelper.JOIN_CODE + userId;
+    }
+
+}

+ 39 - 0
kxmall-common/src/main/java/com/kxmall/common/enums/LoginType.java

@@ -0,0 +1,39 @@
+package com.kxmall.common.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 登录类型
+ *
+ * @author kxmall
+ */
+@Getter
+@AllArgsConstructor
+public enum LoginType {
+
+    /**
+     * 密码登录
+     */
+    PASSWORD("user.password.retry.limit.exceed", "user.password.retry.limit.count"),
+
+    /**
+     * 短信登录
+     */
+    SMS("sms.code.retry.limit.exceed", "sms.code.retry.limit.count"),
+
+    /**
+     * 小程序登录
+     */
+    XCX("", "");
+
+    /**
+     * 登录重试超出限制提示
+     */
+    final String retryLimitExceed;
+
+    /**
+     * 登录重试限制计数提示
+     */
+    final String retryLimitCount;
+}

+ 143 - 0
kxmall-common/src/main/java/com/kxmall/common/helper/LoginHelper.java

@@ -0,0 +1,143 @@
+package com.kxmall.common.helper;
+
+import cn.dev33.satoken.context.SaHolder;
+import cn.dev33.satoken.stp.StpUtil;
+import cn.hutool.core.util.ObjectUtil;
+import com.kxmall.common.constant.UserConstants;
+import com.kxmall.common.exception.UtilException;
+import com.kxmall.common.core.domain.model.LoginUser;
+import com.kxmall.common.enums.DeviceType;
+import com.kxmall.common.enums.UserType;
+import com.kxmall.common.utils.StringUtils;
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+/**
+ * 登录鉴权助手
+ *
+ * user_type 为 用户类型 同一个用户表 可以有多种用户类型 例如 pc,app
+ * deivce 为 设备类型 同一个用户类型 可以有 多种设备类型 例如 web,ios
+ * 可以组成 用户类型与设备类型多对多的 权限灵活控制
+ *
+ * 多用户体系 针对 多种用户类型 但权限控制不一致
+ * 可以组成 多用户类型表与多设备类型 分别控制权限
+ *
+ * @author kxmall
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class LoginHelper {
+
+    public static final String JOIN_CODE = ":";
+    public static final String LOGIN_USER_KEY = "loginUser";
+
+    /**
+     * 登录系统
+     *
+     * @param loginUser 登录用户信息
+     */
+    public static void login(LoginUser loginUser) {
+        SaHolder.getStorage().set(LOGIN_USER_KEY, loginUser);
+        StpUtil.login(loginUser.getLoginId());
+        setLoginUser(loginUser);
+    }
+
+    /**
+     * 登录系统 基于 设备类型
+     * 针对相同用户体系不同设备
+     *
+     * @param loginUser 登录用户信息
+     */
+    public static void loginByDevice(LoginUser loginUser, DeviceType deviceType) {
+        SaHolder.getStorage().set(LOGIN_USER_KEY, loginUser);
+        StpUtil.login(loginUser.getLoginId(), deviceType.getDevice());
+        setLoginUser(loginUser);
+    }
+
+    /**
+     * 设置用户数据(多级缓存)
+     */
+    public static void setLoginUser(LoginUser loginUser) {
+        StpUtil.getTokenSession().set(LOGIN_USER_KEY, loginUser);
+    }
+
+    /**
+     * 获取用户(多级缓存)
+     */
+    public static LoginUser getLoginUser() {
+        LoginUser loginUser = (LoginUser) SaHolder.getStorage().get(LOGIN_USER_KEY);
+        if (loginUser != null) {
+            return loginUser;
+        }
+        loginUser = (LoginUser) StpUtil.getTokenSession().get(LOGIN_USER_KEY);
+        SaHolder.getStorage().set(LOGIN_USER_KEY, loginUser);
+        return loginUser;
+    }
+
+    /**
+     * 获取设备
+     */
+    public static DeviceType getDeviceType() {
+        String loginType = StpUtil.getLoginDevice();
+        return DeviceType.getDeviceType(loginType);
+    }
+
+    /**
+     * 获取用户id
+     */
+    public static Long getUserId() {
+        LoginUser loginUser = getLoginUser();
+        if (ObjectUtil.isNull(loginUser)) {
+            String loginId = StpUtil.getLoginIdAsString();
+            String userId = null;
+            for (UserType value : UserType.values()) {
+                if (StringUtils.contains(loginId, value.getUserType())) {
+                    String[] strs = StringUtils.split(loginId, JOIN_CODE);
+                    // 用户id在总是在最后
+                    userId = strs[strs.length - 1];
+                }
+            }
+            if (StringUtils.isBlank(userId)) {
+                throw new UtilException("登录用户: LoginId异常 => " + loginId);
+            }
+            return Long.parseLong(userId);
+        }
+        return loginUser.getUserId();
+    }
+
+    /**
+     * 获取部门ID
+     */
+    public static Long getDeptId() {
+        return getLoginUser().getDeptId();
+    }
+
+    /**
+     * 获取用户账户
+     */
+    public static String getUsername() {
+        return getLoginUser().getUsername();
+    }
+
+    /**
+     * 获取用户类型
+     */
+    public static UserType getUserType() {
+        String loginId = StpUtil.getLoginIdAsString();
+        return UserType.getUserType(loginId);
+    }
+
+    /**
+     * 是否为管理员
+     *
+     * @param userId 用户ID
+     * @return 结果
+     */
+    public static boolean isAdmin(Long userId) {
+        return UserConstants.ADMIN_ID.equals(userId);
+    }
+
+    public static boolean isAdmin() {
+        return isAdmin(getUserId());
+    }
+
+}

+ 192 - 0
kxmall-framework/src/main/java/com/kxmall/framework/aspectj/LogAspect.java

@@ -0,0 +1,192 @@
+package com.kxmall.framework.aspectj;
+
+import cn.hutool.core.lang.Dict;
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.ObjectUtil;
+import com.kxmall.common.annotation.Log;
+import com.kxmall.common.core.domain.event.OperLogEvent;
+import com.kxmall.common.enums.BusinessStatus;
+import com.kxmall.common.enums.HttpMethod;
+import com.kxmall.common.helper.LoginHelper;
+import com.kxmall.common.utils.JsonUtils;
+import com.kxmall.common.utils.ServletUtils;
+import com.kxmall.common.utils.StringUtils;
+import com.kxmall.common.utils.spring.SpringUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.annotation.AfterReturning;
+import org.aspectj.lang.annotation.AfterThrowing;
+import org.aspectj.lang.annotation.Aspect;
+import org.springframework.stereotype.Component;
+import org.springframework.validation.BindingResult;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.util.Collection;
+import java.util.Map;
+
+/**
+ * 操作日志记录处理
+ *
+ * @author kxmall
+ */
+@Slf4j
+@Aspect
+@Component
+public class LogAspect {
+
+    /**
+     * 排除敏感属性字段
+     */
+    public static final String[] EXCLUDE_PROPERTIES = { "password", "oldPassword", "newPassword", "confirmPassword" };
+
+    /**
+     * 处理完请求后执行
+     *
+     * @param joinPoint 切点
+     */
+    @AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult")
+    public void doAfterReturning(JoinPoint joinPoint, Log controllerLog, Object jsonResult) {
+        handleLog(joinPoint, controllerLog, null, jsonResult);
+    }
+
+    /**
+     * 拦截异常操作
+     *
+     * @param joinPoint 切点
+     * @param e         异常
+     */
+    @AfterThrowing(value = "@annotation(controllerLog)", throwing = "e")
+    public void doAfterThrowing(JoinPoint joinPoint, Log controllerLog, Exception e) {
+        handleLog(joinPoint, controllerLog, e, null);
+    }
+
+    protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult) {
+        try {
+
+            // *========数据库日志=========*//
+            OperLogEvent operLog = new OperLogEvent();
+            operLog.setStatus(BusinessStatus.SUCCESS.ordinal());
+            // 请求的地址
+            String ip = ServletUtils.getClientIP();
+            operLog.setOperIp(ip);
+            operLog.setOperUrl(StringUtils.substring(ServletUtils.getRequest().getRequestURI(), 0, 255));
+            operLog.setOperName(LoginHelper.getUsername());
+
+            if (e != null) {
+                operLog.setStatus(BusinessStatus.FAIL.ordinal());
+                operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000));
+            }
+            // 设置方法名称
+            String className = joinPoint.getTarget().getClass().getName();
+            String methodName = joinPoint.getSignature().getName();
+            operLog.setMethod(className + "." + methodName + "()");
+            // 设置请求方式
+            operLog.setRequestMethod(ServletUtils.getRequest().getMethod());
+            // 处理设置注解上的参数
+            getControllerMethodDescription(joinPoint, controllerLog, operLog, jsonResult);
+            // 发布事件保存数据库
+            SpringUtils.context().publishEvent(operLog);
+        } catch (Exception exp) {
+            // 记录本地异常日志
+            log.error("异常信息:{}", exp.getMessage());
+            exp.printStackTrace();
+        }
+    }
+
+    /**
+     * 获取注解中对方法的描述信息 用于Controller层注解
+     *
+     * @param log     日志
+     * @param operLog 操作日志
+     * @throws Exception
+     */
+    public void getControllerMethodDescription(JoinPoint joinPoint, Log log, OperLogEvent operLog, Object jsonResult) throws Exception {
+        // 设置action动作
+        operLog.setBusinessType(log.businessType().ordinal());
+        // 设置标题
+        operLog.setTitle(log.title());
+        // 设置操作人类别
+        operLog.setOperatorType(log.operatorType().ordinal());
+        // 是否需要保存request,参数和值
+        if (log.isSaveRequestData()) {
+            // 获取参数的信息,传入到数据库中。
+            setRequestValue(joinPoint, operLog);
+        }
+        // 是否需要保存response,参数和值
+        if (log.isSaveResponseData() && ObjectUtil.isNotNull(jsonResult)) {
+            operLog.setJsonResult(StringUtils.substring(JsonUtils.toJsonString(jsonResult), 0, 2000));
+        }
+    }
+
+    /**
+     * 获取请求的参数,放到log中
+     *
+     * @param operLog 操作日志
+     * @throws Exception 异常
+     */
+    private void setRequestValue(JoinPoint joinPoint, OperLogEvent operLog) throws Exception {
+        String requestMethod = operLog.getRequestMethod();
+        if (HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod)) {
+            String params = argsArrayToString(joinPoint.getArgs());
+            operLog.setOperParam(StringUtils.substring(params, 0, 2000));
+        } else {
+            Map<String, String> paramsMap = ServletUtils.getParamMap(ServletUtils.getRequest());
+            MapUtil.removeAny(paramsMap, EXCLUDE_PROPERTIES);
+            operLog.setOperParam(StringUtils.substring(JsonUtils.toJsonString(paramsMap), 0, 2000));
+        }
+    }
+
+    /**
+     * 参数拼装
+     */
+    private String argsArrayToString(Object[] paramsArray) {
+        StringBuilder params = new StringBuilder();
+        if (paramsArray != null && paramsArray.length > 0) {
+            for (Object o : paramsArray) {
+                if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) {
+                    try {
+                        String str = JsonUtils.toJsonString(o);
+                        Dict dict = JsonUtils.parseMap(str);
+                        if (MapUtil.isNotEmpty(dict)) {
+                            MapUtil.removeAny(dict, EXCLUDE_PROPERTIES);
+                            str = JsonUtils.toJsonString(dict);
+                        }
+                        params.append(str).append(" ");
+                    } catch (Exception e) {
+                        e.printStackTrace();
+                    }
+                }
+            }
+        }
+        return params.toString().trim();
+    }
+
+    /**
+     * 判断是否需要过滤的对象。
+     *
+     * @param o 对象信息。
+     * @return 如果是需要过滤的对象,则返回true;否则返回false。
+     */
+    @SuppressWarnings("rawtypes")
+    public boolean isFilterObject(final Object o) {
+        Class<?> clazz = o.getClass();
+        if (clazz.isArray()) {
+            return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
+        } else if (Collection.class.isAssignableFrom(clazz)) {
+            Collection collection = (Collection) o;
+            for (Object value : collection) {
+                return value instanceof MultipartFile;
+            }
+        } else if (Map.class.isAssignableFrom(clazz)) {
+            Map map = (Map) o;
+            for (Object value : map.entrySet()) {
+                Map.Entry entry = (Map.Entry) value;
+                return entry.getValue() instanceof MultipartFile;
+            }
+        }
+        return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
+            || o instanceof BindingResult;
+    }
+}

+ 22 - 0
kxmall-system/src/main/java/com/kxmall/wechat/handler/LogHandler.java

@@ -0,0 +1,22 @@
+
+package com.kxmall.wechat.handler;
+
+import me.chanjar.weixin.common.session.WxSessionManager;
+import me.chanjar.weixin.mp.api.WxMpService;
+import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
+import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+
+@Component
+public class LogHandler extends AbstractHandler {
+    @Override
+    public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage,
+                                    Map<String, Object> context, WxMpService wxMpService,
+                                    WxSessionManager sessionManager) {
+        //this.logger.info("\n接收到请求消息,内容:{}", JsonUtils.toJson(wxMessage));
+        return null;
+    }
+
+}