Kaynağa Gözat

集成sa-token

xudm 2 ay önce
ebeveyn
işleme
fcd8a5c801
46 değiştirilmiş dosya ile 2445 ekleme ve 92 silme
  1. 1 0
      .gitignore
  2. 60 0
      pom.xml
  3. 3 0
      src/main/java/com/xs/core/XsTgGameApplication.java
  4. 7 0
      src/main/java/com/xs/core/common/constant/ConstantConfig.java
  5. 54 0
      src/main/java/com/xs/core/common/constant/UiConstants.java
  6. 100 0
      src/main/java/com/xs/core/common/content/UserContext.java
  7. 112 0
      src/main/java/com/xs/core/common/content/UserContextHolder.java
  8. 13 0
      src/main/java/com/xs/core/common/enums/BaseEnum.java
  9. 59 0
      src/main/java/com/xs/core/common/enums/DataScopeEnum.java
  10. 46 0
      src/main/java/com/xs/core/common/enums/DisEnableStatusEnum.java
  11. 49 0
      src/main/java/com/xs/core/common/enums/GenderEnum.java
  12. 47 0
      src/main/java/com/xs/core/common/enums/SuccessFailureStatusEnum.java
  13. 21 0
      src/main/java/com/xs/core/common/exception/BusinessException.java
  14. 71 0
      src/main/java/com/xs/core/common/validation/CheckUtils.java
  15. 84 0
      src/main/java/com/xs/core/common/validation/Validator.java
  16. 51 0
      src/main/java/com/xs/core/config/FastJson2JsonRedisSerializer.java
  17. 39 0
      src/main/java/com/xs/core/config/RedisConfig.java
  18. 15 0
      src/main/java/com/xs/core/config/mybatis/CosIdKeyGenerator.java
  19. 13 0
      src/main/java/com/xs/core/config/mybatis/MybatisPlusConfig.java
  20. 43 0
      src/main/java/com/xs/core/config/satoken/SaTokenConfigure.java
  21. 7 6
      src/main/java/com/xs/core/controller/user/LoginController.java
  22. 71 0
      src/main/java/com/xs/core/filter/GlobalExceptionHandler.java
  23. 14 12
      src/main/java/com/xs/core/filter/RequestHandler.java
  24. 34 0
      src/main/java/com/xs/core/filter/UserContextInterceptor.java
  25. 23 0
      src/main/java/com/xs/core/init/DatabaseInitializer.java
  26. 16 0
      src/main/java/com/xs/core/mapper/user/TgUserMapper.java
  27. 233 0
      src/main/java/com/xs/core/model/user/entity/TgUser.java
  28. 0 3
      src/main/java/com/xs/core/model/user/req/AppLoginReq.java
  29. 104 0
      src/main/java/com/xs/core/service/Impl/user/AppLoginServiceImpl.java
  30. 20 0
      src/main/java/com/xs/core/service/Impl/user/TgUserServiceImpl.java
  31. 23 0
      src/main/java/com/xs/core/service/user/AppLoginService.java
  32. 16 0
      src/main/java/com/xs/core/service/user/ITgUserService.java
  33. 71 0
      src/main/java/com/xs/core/utils/ExceptionUtils.java
  34. 69 0
      src/main/java/com/xs/core/utils/InviteCodeGenerator.java
  35. 36 0
      src/main/java/com/xs/core/utils/IpUtils.java
  36. 186 0
      src/main/java/com/xs/core/utils/MyGenerator.java
  37. 46 0
      src/main/java/com/xs/core/utils/ServletUtils.java
  38. 87 0
      src/main/java/com/xs/core/utils/SpringWebUtils.java
  39. 68 0
      src/main/java/com/xs/core/utils/redis/RedissonLockUtil.java
  40. 150 0
      src/main/resources/ESAPI.properties
  41. 124 0
      src/main/resources/application-dev.yml
  42. 2 71
      src/main/resources/application.yml
  43. 6 0
      src/main/resources/esapi-java-logging.properties
  44. 99 0
      src/main/resources/logback-spring.xml
  45. 45 0
      src/main/resources/mapper/TgUserMapper.xml
  46. 7 0
      src/main/resources/validation.properties

+ 1 - 0
.gitignore

@@ -18,6 +18,7 @@ target/
 *.iws
 *.iml
 *.ipr
+logs
 
 ### NetBeans ###
 /nbproject/private/

+ 60 - 0
pom.xml

@@ -28,6 +28,9 @@
         <esapi.version>2.5.3.1</esapi.version>
         <bcprov-jdk15to18.version>1.76</bcprov-jdk15to18.version>
         <bcprov-jdk15on.version>1.70</bcprov-jdk15on.version>
+        <freemarket.version>2.3.33</freemarket.version>
+        <cosid.version>2.9.9</cosid.version>
+        <mica-ip2region.version>3.2.6</mica-ip2region.version>
     </properties>
     <dependencies>
         <dependency>
@@ -59,12 +62,40 @@
             <version>${mybatis-plus.version}</version>
         </dependency>
 
+        <!--  mybatis-plus 代码生成      -->
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-generator</artifactId>
+            <version>${mybatis-plus.version}</version>
+        </dependency>
+
+        <!-- FreeMarker(模板引擎) -->
+        <dependency>
+            <groupId>org.freemarker</groupId>
+            <artifactId>freemarker</artifactId>
+            <version>${freemarket.version}</version>
+        </dependency>
+
+
         <!-- mysql数据库驱动 -->
         <dependency>
             <groupId>com.mysql</groupId>
             <artifactId>mysql-connector-j</artifactId>
             <version>${mysql-connector.version}</version>
+        </dependency>
 
+        <!-- https://mvnrepository.com/artifact/me.ahoo.cosid/cosid-spring-boot-starter -->
+        <dependency>
+            <groupId>me.ahoo.cosid</groupId>
+            <artifactId>cosid-spring-boot-starter</artifactId>
+            <version>${cosid.version}</version>
+        </dependency>
+
+        <!-- https://mvnrepository.com/artifact/me.ahoo.cosid/cosid-spring-redis -->
+        <dependency>
+            <groupId>me.ahoo.cosid</groupId>
+            <artifactId>cosid-spring-redis</artifactId>
+            <version>${cosid.version}</version>
         </dependency>
 
         <!--分布式锁-->
@@ -89,6 +120,19 @@
             <version>${sa-token.version}</version>
         </dependency>
 
+        <!-- Sa-Token 整合 jwt -->
+        <dependency>
+            <groupId>cn.dev33</groupId>
+            <artifactId>sa-token-jwt</artifactId>
+            <version>${sa-token.version}</version>
+            <!--  排除hutool  -->
+            <exclusions>
+                <exclusion>
+                    <groupId>cn.hutool</groupId>
+                    <artifactId>hutool-jwt</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
 
         <!--  hutool  -->
         <dependency>
@@ -111,6 +155,21 @@
             <version>${knife4j.version}</version>
         </dependency>
 
+        <!-- mica-ip2region -->
+        <dependency>
+            <groupId>net.dreamlu</groupId>
+            <artifactId>mica-ip2region</artifactId>
+            <version>${mica-ip2region.version}</version>
+        </dependency>
+
+        <!-- mica-core -->
+        <dependency>
+            <groupId>net.dreamlu</groupId>
+            <artifactId>mica-core</artifactId>
+            <version>${mica-ip2region.version}</version>
+        </dependency>
+
+
         <dependency>
             <groupId>org.owasp.esapi</groupId>
             <artifactId>esapi</artifactId>
@@ -138,6 +197,7 @@
             <artifactId>spring-boot-starter-test</artifactId>
             <scope>test</scope>
         </dependency>
+
         <dependency>
             <groupId>org.projectlombok</groupId>
             <artifactId>lombok</artifactId>

+ 3 - 0
src/main/java/com/xs/core/XsTgGameApplication.java

@@ -1,9 +1,12 @@
 package com.xs.core;
 
+import org.mybatis.spring.annotation.MapperScan;
 import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 
 @SpringBootApplication
+@MapperScan(basePackages = {"com.xs.core.mapper"})
 public class XsTgGameApplication {
 
     public static void main(String[] args) {

+ 7 - 0
src/main/java/com/xs/core/common/constant/ConstantConfig.java

@@ -0,0 +1,7 @@
+package com.xs.core.common.constant;
+
+public interface ConstantConfig {
+    String MESSAGE_ID_PREFIX = "MESSAGE_";
+
+    String USER_CONTENT_EXTRA_KEY = "userContext";
+}

+ 54 - 0
src/main/java/com/xs/core/common/constant/UiConstants.java

@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.xs.core.common.constant;
+
+/**
+ * UI 相关常量
+ *
+ * @author Charles7c
+ * @since 2023/9/17 14:12
+ */
+public class UiConstants {
+
+    /**
+     * 主色(极致蓝)
+     */
+    public static final String COLOR_PRIMARY = "arcoblue";
+
+    /**
+     * 成功色(仙野绿)
+     */
+    public static final String COLOR_SUCCESS = "green";
+
+    /**
+     * 警告色(活力橙)
+     */
+    public static final String COLOR_WARNING = "orangered";
+
+    /**
+     * 错误色(浪漫红)
+     */
+    public static final String COLOR_ERROR = "red";
+
+    /**
+     * 默认色(中性灰)
+     */
+    public static final String COLOR_DEFAULT = "gray";
+
+    private UiConstants() {
+    }
+}

+ 100 - 0
src/main/java/com/xs/core/common/content/UserContext.java

@@ -0,0 +1,100 @@
+package com.xs.core.common.content;
+
+import cn.hutool.core.net.Ipv4Util;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.extra.servlet.JakartaServletUtil;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.xs.core.utils.ExceptionUtils;
+import com.xs.core.utils.IpUtils;
+import com.xs.core.utils.ServletUtils;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.Builder;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+@Data
+public class UserContext implements Serializable {
+    @Serial
+    private static final long serialVersionUID = 1L;
+    /**
+     * ID
+     */
+    private Long id;
+
+    /**
+     * tg小程序id
+     */
+
+    private String tgId;
+
+    /**
+     * tg账号
+     */
+
+    private String userName;
+
+    /**
+     * 名
+     */
+
+    private String firstName;
+
+    /**
+     * 姓
+     */
+
+    private String lastName;
+
+    /**
+     * 昵称
+     */
+    private String nickname;
+
+    /**
+     * 真实姓名
+     */
+
+    private String realName;
+
+    /**
+     * 头像
+     */
+    private String avatar;
+
+
+    /**
+     * IP
+     */
+    private String ip;
+
+    /**
+     * IP 归属地
+     */
+    private String address;
+
+    /**
+     * 浏览器
+     */
+    private String browser;
+
+    /**
+     * 操作系统
+     */
+    private String os;
+
+    /**
+     * 登录时间
+     */
+    private LocalDateTime loginTime;
+
+    public void setExtraInfo(HttpServletRequest request) {
+        this.ip = JakartaServletUtil.getClientIP(request);
+        this.address = ExceptionUtils.exToNull(() -> IpUtils.getIpv4Address(this.ip));
+        this.setBrowser(ServletUtils.getBrowser(request));
+        this.setLoginTime(LocalDateTime.now());
+        this.setOs(StrUtil.subBefore(ServletUtils.getOs(request), " or", false));
+    }
+}

+ 112 - 0
src/main/java/com/xs/core/common/content/UserContextHolder.java

@@ -0,0 +1,112 @@
+/*
+ * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.xs.core.common.content;
+
+import cn.dev33.satoken.session.SaSession;
+import cn.dev33.satoken.stp.StpUtil;
+import com.xs.core.utils.ExceptionUtils;
+
+/**
+ * 用户上下文 Holder
+ *
+ * @author Charles7c
+ * @since 2022/12/24 12:58
+ */
+public class UserContextHolder {
+
+    private static final ThreadLocal<UserContext> CONTEXT_HOLDER = new ThreadLocal<>();
+
+    private UserContextHolder() {
+    }
+
+    /**
+     * 设置上下文
+     *
+     * @param context 上下文
+     */
+    public static void setContext(UserContext context) {
+        setContext(context, true);
+    }
+
+    /**
+     * 设置上下文
+     *
+     * @param context  上下文
+     * @param isUpdate 是否更新
+     */
+    public static void setContext(UserContext context, boolean isUpdate) {
+        CONTEXT_HOLDER.set(context);
+        if (isUpdate) {
+            StpUtil.getSessionByLoginId(context.getId()).set(SaSession.USER, context);
+        }
+    }
+
+    /**
+     * 获取上下文
+     *
+     * @return 上下文
+     */
+    public static UserContext getContext() {
+        UserContext context = CONTEXT_HOLDER.get();
+        if (null == context) {
+            context = StpUtil.getSession().getModel(SaSession.USER, UserContext.class);
+            CONTEXT_HOLDER.set(context);
+        }
+        return context;
+    }
+
+    /**
+     * 获取指定用户的上下文
+     *
+     * @param userId 用户 ID
+     * @return 上下文
+     */
+    public static UserContext getContext(Long userId) {
+        SaSession session = StpUtil.getSessionByLoginId(userId, false);
+        if (null == session) {
+            return null;
+        }
+        return session.getModel(SaSession.USER, UserContext.class);
+    }
+
+
+    /**
+     * 清除上下文
+     */
+    public static void clearContext() {
+        CONTEXT_HOLDER.remove();
+    }
+
+    /**
+     * 获取用户 ID
+     *
+     * @return 用户 ID
+     */
+    public static Long getUserId() {
+        return ExceptionUtils.exToNull(() -> getContext().getId());
+    }
+
+    /**
+     * 获取用户名
+     *
+     * @return 用户名
+     */
+    public static String getUsername() {
+        return ExceptionUtils.exToNull(() -> getContext().getUserName());
+    }
+
+}

+ 13 - 0
src/main/java/com/xs/core/common/enums/BaseEnum.java

@@ -0,0 +1,13 @@
+package com.xs.core.common.enums;
+
+import java.io.Serializable;
+
+public interface BaseEnum<T extends Serializable> {
+    T getValue();
+
+    String getDescription();
+
+    default String getColor() {
+        return null;
+    }
+}

+ 59 - 0
src/main/java/com/xs/core/common/enums/DataScopeEnum.java

@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.xs.core.common.enums;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * 数据权限枚举
+ *
+ * @author Charles7c
+ * @since 2023/2/8 22:58
+ */
+@Getter
+@RequiredArgsConstructor
+public enum DataScopeEnum implements BaseEnum<Integer> {
+
+    /**
+     * 全部数据权限
+     */
+    ALL(1, "全部数据权限"),
+
+    /**
+     * 本部门及以下数据权限
+     */
+    DEPT_AND_CHILD(2, "本部门及以下数据权限"),
+
+    /**
+     * 本部门数据权限
+     */
+    DEPT(3, "本部门数据权限"),
+
+    /**
+     * 仅本人数据权限
+     */
+    SELF(4, "仅本人数据权限"),
+
+    /**
+     * 自定义数据权限
+     */
+    CUSTOM(5, "自定义数据权限"),;
+
+    private final Integer value;
+    private final String description;
+}

+ 46 - 0
src/main/java/com/xs/core/common/enums/DisEnableStatusEnum.java

@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.xs.core.common.enums;
+
+import com.xs.core.common.constant.UiConstants;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * 启用/禁用状态枚举
+ *
+ * @author Charles7c
+ * @since 2022/12/29 22:38
+ */
+@Getter
+@RequiredArgsConstructor
+public enum DisEnableStatusEnum implements BaseEnum<Integer> {
+
+    /**
+     * 启用
+     */
+    ENABLE(0, "启用", UiConstants.COLOR_SUCCESS),
+
+    /**
+     * 禁用
+     */
+    DISABLE(1, "禁用", UiConstants.COLOR_ERROR);
+
+    private final Integer value;
+    private final String description;
+    private final String color;
+}

+ 49 - 0
src/main/java/com/xs/core/common/enums/GenderEnum.java

@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.xs.core.common.enums;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * 性别枚举
+ *
+ * @author Charles7c
+ * @since 2022/12/29 21:59
+ */
+@Getter
+@RequiredArgsConstructor
+public enum GenderEnum implements BaseEnum<Integer> {
+
+    /**
+     * 未知
+     */
+    UNKNOWN(0, "未知"),
+
+    /**
+     * 男
+     */
+    MALE(1, "男"),
+
+    /**
+     * 女
+     */
+    FEMALE(2, "女"),;
+
+    private final Integer value;
+    private final String description;
+}

+ 47 - 0
src/main/java/com/xs/core/common/enums/SuccessFailureStatusEnum.java

@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.xs.core.common.enums;
+
+import com.xs.core.common.constant.UiConstants;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * 成功/失败状态枚举
+ *
+ * @author Charles7c
+ * @since 2023/2/26 21:35
+ */
+@Getter
+@RequiredArgsConstructor
+public enum SuccessFailureStatusEnum implements BaseEnum<Integer> {
+
+    /**
+     * 成功
+     */
+    SUCCESS(1, "成功", UiConstants.COLOR_SUCCESS),
+
+    /**
+     * 失败
+     */
+    FAILURE(2, "失败", UiConstants.COLOR_ERROR),
+    ;
+
+    private final Integer value;
+    private final String description;
+    private final String color;
+}

+ 21 - 0
src/main/java/com/xs/core/common/exception/BusinessException.java

@@ -0,0 +1,21 @@
+package com.xs.core.common.exception;
+
+/**
+ * 自定义以业务异常
+ */
+public class BusinessException extends RuntimeException {
+    public BusinessException() {
+    }
+
+    public BusinessException(String message) {
+        super(message);
+    }
+
+    public BusinessException(Throwable cause) {
+        super(cause);
+    }
+
+    public BusinessException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}

+ 71 - 0
src/main/java/com/xs/core/common/validation/CheckUtils.java

@@ -0,0 +1,71 @@
+package com.xs.core.common.validation;
+
+import cn.hutool.core.text.CharSequenceUtil;
+import com.xs.core.common.exception.BusinessException;
+
+import java.util.function.BooleanSupplier;
+
+public class CheckUtils extends Validator {
+    private static final Class<BusinessException> EXCEPTION_TYPE = BusinessException.class;
+
+    private CheckUtils() {
+    }
+
+    public static void throwIfNotExists(Object obj, String entityName, String fieldName, Object fieldValue) {
+        String message = "%s 为 [%s] 的 %s 记录已不存在".formatted(fieldName, fieldValue, CharSequenceUtil.replace(entityName, "DO", ""));
+        throwIfNull(obj, message, EXCEPTION_TYPE);
+    }
+
+    public static void throwIfNull(Object obj, String template, Object... params) {
+        throwIfNull(obj, CharSequenceUtil.format(template, params), EXCEPTION_TYPE);
+    }
+
+    public static void throwIfNotNull(Object obj, String template, Object... params) {
+        throwIfNotNull(obj, CharSequenceUtil.format(template, params), EXCEPTION_TYPE);
+    }
+
+    public static void throwIfExists(Object obj, String entityName, String fieldName, Object fieldValue) {
+        String message = "%s 为 [%s] 的 %s 记录已存在".formatted(fieldName, fieldValue, entityName);
+        throwIfNotNull(obj, message, EXCEPTION_TYPE);
+    }
+
+    public static void throwIfEmpty(Object obj, String template, Object... params) {
+        throwIfEmpty(obj, CharSequenceUtil.format(template, params), EXCEPTION_TYPE);
+    }
+
+    public static void throwIfNotEmpty(Object obj, String template, Object... params) {
+        throwIfNotEmpty(obj, CharSequenceUtil.format(template, params), EXCEPTION_TYPE);
+    }
+
+    public static void throwIfBlank(CharSequence str, String template, Object... params) {
+        throwIfBlank(str, CharSequenceUtil.format(template, params), EXCEPTION_TYPE);
+    }
+
+    public static void throwIfNotBlank(CharSequence str, String template, Object... params) {
+        throwIfNotBlank(str, CharSequenceUtil.format(template, params), EXCEPTION_TYPE);
+    }
+
+    public static void throwIfEqual(Object obj1, Object obj2, String template, Object... params) {
+        throwIfEqual(obj1, obj2, CharSequenceUtil.format(template, params), EXCEPTION_TYPE);
+    }
+
+    public static void throwIfNotEqual(Object obj1, Object obj2, String template, Object... params) {
+        throwIfNotEqual(obj1, obj2, CharSequenceUtil.format(template, params), EXCEPTION_TYPE);
+    }
+
+    public static void throwIfEqualIgnoreCase(CharSequence str1, CharSequence str2, String template, Object... params) {
+        throwIfEqualIgnoreCase(str1, str2, CharSequenceUtil.format(template, params), EXCEPTION_TYPE);
+    }
+
+    public static void throwIfNotEqualIgnoreCase(CharSequence str1, CharSequence str2, String template, Object... params) {
+        throwIfNotEqualIgnoreCase(str1, str2, CharSequenceUtil.format(template, params), EXCEPTION_TYPE);
+    }
+
+    public static void throwIf(boolean condition, String template, Object... params) {
+        throwIf(condition, CharSequenceUtil.format(template, params), EXCEPTION_TYPE);
+    }
+
+    public static void throwIf(BooleanSupplier conditionSupplier, String template, Object... params) {
+        throwIf(conditionSupplier, CharSequenceUtil.format(template, params), EXCEPTION_TYPE);
+    }
+}

+ 84 - 0
src/main/java/com/xs/core/common/validation/Validator.java

@@ -0,0 +1,84 @@
+package com.xs.core.common.validation;
+
+import cn.hutool.core.text.CharSequenceUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.ReflectUtil;
+import cn.hutool.extra.spring.SpringUtil;
+import jakarta.validation.ConstraintViolation;
+import jakarta.validation.ConstraintViolationException;
+
+import java.util.Set;
+import java.util.function.BooleanSupplier;
+
+public class Validator {
+    private static jakarta.validation.Validator VALIDATOR;
+
+    private static jakarta.validation.Validator getValidator() {
+        if (VALIDATOR == null) {
+            VALIDATOR = SpringUtil.getBean(jakarta.validation.Validator.class);
+        }
+        return VALIDATOR;
+    }
+
+    protected Validator() {
+    }
+
+    protected static void throwIfNull(Object obj, String message, Class<? extends RuntimeException> exceptionType) {
+        throwIf(null == obj, message, exceptionType);
+    }
+
+    protected static void throwIfNotNull(Object obj, String message, Class<? extends RuntimeException> exceptionType) {
+        throwIf(null != obj, message, exceptionType);
+    }
+
+    protected static void throwIfEmpty(Object obj, String message, Class<? extends RuntimeException> exceptionType) {
+        throwIf(ObjectUtil.isEmpty(obj), message, exceptionType);
+    }
+
+    protected static void throwIfNotEmpty(Object obj, String message, Class<? extends RuntimeException> exceptionType) {
+        throwIf(ObjectUtil.isNotEmpty(obj), message, exceptionType);
+    }
+
+    protected static void throwIfBlank(CharSequence str, String message, Class<? extends RuntimeException> exceptionType) {
+        throwIf(CharSequenceUtil.isBlank(str), message, exceptionType);
+    }
+
+    protected static void throwIfNotBlank(CharSequence str, String message, Class<? extends RuntimeException> exceptionType) {
+        throwIf(CharSequenceUtil.isNotBlank(str), message, exceptionType);
+    }
+
+    protected static void throwIfEqual(Object obj1, Object obj2, String message, Class<? extends RuntimeException> exceptionType) {
+        throwIf(ObjectUtil.equal(obj1, obj2), message, exceptionType);
+    }
+
+    protected static void throwIfNotEqual(Object obj1, Object obj2, String message, Class<? extends RuntimeException> exceptionType) {
+        throwIf(ObjectUtil.notEqual(obj1, obj2), message, exceptionType);
+    }
+
+    protected static void throwIfEqualIgnoreCase(CharSequence str1, CharSequence str2, String message, Class<? extends RuntimeException> exceptionType) {
+        throwIf(CharSequenceUtil.equalsIgnoreCase(str1, str2), message, exceptionType);
+    }
+
+    protected static void throwIfNotEqualIgnoreCase(CharSequence str1, CharSequence str2, String message, Class<? extends RuntimeException> exceptionType) {
+        throwIf(!CharSequenceUtil.equalsIgnoreCase(str1, str2), message, exceptionType);
+    }
+
+    protected static void throwIf(boolean condition, String message, Class<? extends RuntimeException> exceptionType) {
+        if (condition) {
+            throw (RuntimeException) ReflectUtil.newInstance(exceptionType, new Object[]{message});
+        }
+    }
+
+    protected static void throwIf(BooleanSupplier conditionSupplier, String message, Class<? extends RuntimeException> exceptionType) {
+        if (null != conditionSupplier && conditionSupplier.getAsBoolean()) {
+            throw (RuntimeException) ReflectUtil.newInstance(exceptionType, new Object[]{message});
+        }
+    }
+
+    public static void validate(Object obj, Class<?>... groups) {
+        Set<ConstraintViolation<Object>> violations = VALIDATOR.validate(obj, groups);
+        if (!violations.isEmpty()) {
+            throw new ConstraintViolationException(violations);
+        }
+    }
+}

+ 51 - 0
src/main/java/com/xs/core/config/FastJson2JsonRedisSerializer.java

@@ -0,0 +1,51 @@
+
+package com.xs.core.config;
+
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONFactory;
+import com.alibaba.fastjson2.JSONWriter;
+import org.springframework.data.redis.serializer.RedisSerializer;
+import org.springframework.data.redis.serializer.SerializationException;
+import org.springframework.lang.Nullable;
+
+import java.nio.charset.StandardCharsets;
+
+public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T> {
+    private final Class<T> type;
+
+
+    static {
+        // fastjson升级文档 https://github.com/alibaba/fastjson2/wiki/fastjson_1_upgrade_cn
+        // ParserConfig添加autoType白名单的功能在ObjectReaderProvider中提供,可以如下的方式配置autoType白名单
+        JSONFactory.getDefaultObjectReaderProvider().addAutoTypeAccept("com.xs.core");
+    }
+
+    public FastJson2JsonRedisSerializer(Class<T> type) {
+        this.type = type;
+    }
+
+    public T deserialize(@Nullable byte[] bytes) throws SerializationException {
+        if (bytes == null || bytes.length == 0) {
+            return null;
+        } else {
+            try {
+                String str = new String(bytes, StandardCharsets.UTF_8);
+                return JSON.parseObject(str, type);
+            } catch (Exception var3) {
+                throw new SerializationException("Could not read JSON: " + var3.getMessage(), var3);
+            }
+        }
+    }
+
+    public byte[] serialize(@Nullable Object t) throws SerializationException {
+        if (t == null) {
+            return new byte[0];
+        } else {
+            try {
+                return JSON.toJSONString(t, JSONWriter.Feature.WriteClassName).getBytes(StandardCharsets.UTF_8);
+            } catch (Exception var3) {
+                throw new SerializationException("Could not write JSON: " + var3.getMessage(), var3);
+            }
+        }
+    }
+}

+ 39 - 0
src/main/java/com/xs/core/config/RedisConfig.java

@@ -0,0 +1,39 @@
+package com.xs.core.config;
+
+import org.redisson.Redisson;
+import org.redisson.api.RedissonClient;
+import org.redisson.codec.JsonJacksonCodec;
+import org.redisson.config.Config;
+import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
+import org.springframework.cache.annotation.CachingConfigurer;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+
+@Configuration
+public class RedisConfig implements CachingConfigurer {
+    @Bean
+    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
+
+        FastJson2JsonRedisSerializer<String> serializer = new FastJson2JsonRedisSerializer<>(String.class);
+        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
+        redisTemplate.setConnectionFactory(redisConnectionFactory);
+        redisTemplate.setKeySerializer(StringRedisSerializer.UTF_8);
+        redisTemplate.setValueSerializer(serializer);
+        redisTemplate.setHashKeySerializer(StringRedisSerializer.UTF_8);
+        redisTemplate.setHashValueSerializer(serializer);
+        redisTemplate.afterPropertiesSet();
+
+        return redisTemplate;
+    }
+
+    @Bean
+    public RedissonClient redissonClient(RedisProperties redisProperties) {
+        Config config = new Config();
+        config.useSingleServer().setPassword(redisProperties.getPassword()).setAddress("redis://" + redisProperties.getHost() + ":" + redisProperties.getPort()).setDatabase(redisProperties.getDatabase());
+        config.setCodec(new JsonJacksonCodec());
+        return Redisson.create(config);
+    }
+}

+ 15 - 0
src/main/java/com/xs/core/config/mybatis/CosIdKeyGenerator.java

@@ -0,0 +1,15 @@
+package com.xs.core.config.mybatis;
+
+
+import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
+import me.ahoo.cosid.IdGenerator;
+import me.ahoo.cosid.provider.DefaultIdGeneratorProvider;
+
+public class CosIdKeyGenerator implements IdentifierGenerator {
+
+    @Override
+    public Long nextId(Object entity) {
+        IdGenerator share = DefaultIdGeneratorProvider.INSTANCE.getShare();
+        return share.generate();
+    }
+}

+ 13 - 0
src/main/java/com/xs/core/config/mybatis/MybatisPlusConfig.java

@@ -0,0 +1,13 @@
+package com.xs.core.config.mybatis;
+
+import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class MybatisPlusConfig {
+    @Bean
+    public IdentifierGenerator idGenerator() {
+        return new CosIdKeyGenerator();
+    }
+}

+ 43 - 0
src/main/java/com/xs/core/config/satoken/SaTokenConfigure.java

@@ -0,0 +1,43 @@
+package com.xs.core.config.satoken;
+
+import cn.dev33.satoken.interceptor.SaInterceptor;
+import cn.dev33.satoken.jwt.StpLogicJwtForSimple;
+import cn.dev33.satoken.stp.SaTokenInfo;
+import cn.dev33.satoken.stp.StpLogic;
+import cn.dev33.satoken.stp.StpUtil;
+import cn.hutool.core.util.StrUtil;
+import com.alibaba.fastjson2.JSON;
+import com.xs.core.common.constant.ConstantConfig;
+import com.xs.core.common.content.UserContext;
+import com.xs.core.common.content.UserContextHolder;
+import com.xs.core.filter.UserContextInterceptor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+@Configuration
+@Slf4j
+public class SaTokenConfigure implements WebMvcConfigurer {
+    // Sa-Token 整合 jwt (Simple 简单模式)
+    @Bean
+    public StpLogic getStpLogicJwt() {
+        return new StpLogicJwtForSimple();
+    }
+
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        // 注册 Sa-Token 拦截器,校验规则为 StpUtil.checkLogin() 登录校验。
+        registry.addInterceptor(new SaInterceptor(handle -> {
+                    StpUtil.checkLogin();
+                    log.info("进入到Sa-Token 拦截器拦截器");
+                }))
+                .addPathPatterns("/**")
+                .excludePathPatterns("/login/coinLogin").order(-100);
+        registry.addInterceptor(new UserContextInterceptor())
+                .addPathPatterns("/**")
+                .excludePathPatterns("/login/coinLogin")
+                .order(-99);
+    }
+}

+ 7 - 6
src/main/java/com/xs/core/controller/user/LoginController.java

@@ -1,11 +1,11 @@
 package com.xs.core.controller.user;
 
-import cn.dev33.satoken.annotation.SaIgnore;
 import cn.dev33.satoken.stp.SaTokenInfo;
-import com.alibaba.fastjson2.JSON;
 import com.xs.core.model.user.req.AppLoginReq;
+import com.xs.core.service.user.AppLoginService;
 import io.swagger.v3.oas.annotations.Operation;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
@@ -13,11 +13,12 @@ import org.springframework.web.bind.annotation.*;
 @RestController
 @RequestMapping("/login")
 public class LoginController {
-    @SaIgnore
+    @Autowired
+    private AppLoginService loginService;
+
     @PostMapping("/coinLogin")
     @Operation(summary = "tg账号登录", description = "根据tg用户id进行登录")
-    public AppLoginReq coinLogin(@Validated @RequestBody AppLoginReq req) {
-        log.info(JSON.toJSONString(req));
-        return req;
+    public SaTokenInfo coinLogin(@Validated @RequestBody AppLoginReq req) {
+        return loginService.tgLogin(req);
     }
 }

+ 71 - 0
src/main/java/com/xs/core/filter/GlobalExceptionHandler.java

@@ -0,0 +1,71 @@
+package com.xs.core.filter;
+
+import cn.dev33.satoken.exception.NotLoginException;
+import cn.dev33.satoken.exception.NotPermissionException;
+import cn.dev33.satoken.jwt.exception.SaJwtException;
+import cn.hutool.core.exceptions.ExceptionUtil;
+import com.xs.core.common.exception.BusinessException;
+import com.xs.core.model.ResponseResult;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.ConstraintViolationException;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.converter.HttpMessageNotReadableException;
+import org.springframework.util.CollectionUtils;
+import org.springframework.validation.BindException;
+import org.springframework.validation.FieldError;
+import org.springframework.web.HttpMediaTypeNotSupportedException;
+import org.springframework.web.HttpRequestMethodNotSupportedException;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.multipart.MaxUploadSizeExceededException;
+import org.springframework.web.multipart.support.MissingServletRequestPartException;
+
+import java.util.List;
+
+@RestControllerAdvice
+@Slf4j
+public class GlobalExceptionHandler {
+    @ExceptionHandler({Exception.class, NotLoginException.class, HttpMessageNotReadableException.class,
+            HttpMediaTypeNotSupportedException.class, MaxUploadSizeExceededException.class, NotPermissionException.class,
+            HttpRequestMethodNotSupportedException.class, MethodArgumentNotValidException.class, BindException.class,
+            ConstraintViolationException.class, MissingServletRequestPartException.class, SaJwtException.class})
+    @ResponseBody
+    ResponseResult<?> serverExceptionHandler(HttpServletRequest request, HttpServletResponse response, Exception e) {
+        if (e instanceof NotLoginException || e instanceof SaJwtException) {
+            return ResponseResult.unauthorized(null);
+        } else if (e instanceof HttpMessageNotReadableException || e instanceof HttpRequestMethodNotSupportedException) {
+            return ResponseResult.badRequest(null);
+        } else if (e instanceof MaxUploadSizeExceededException) {
+            return ResponseResult.requestTooLarge(null);
+        } else if (e instanceof HttpMediaTypeNotSupportedException) {
+            return ResponseResult.unsupportedMediaType(null);
+        } else if (e instanceof NotPermissionException) {
+            return ResponseResult.forbidden();
+        } else if (e instanceof BindException) {
+            StringBuilder message = new StringBuilder();
+            List<FieldError> fieldErrorList = ((BindException) e).getFieldErrors();
+            if (!CollectionUtils.isEmpty(fieldErrorList)) {
+                for (FieldError fieldError : fieldErrorList) {
+                    if (fieldError != null && fieldError.getDefaultMessage() != null) {
+                        message.append(fieldError.getDefaultMessage()).append(" ");
+                    }
+                }
+            }
+            log.error(message.toString());
+            return ResponseResult.failed(message.toString());
+        } else if (e instanceof ConstraintViolationException) {
+            return ResponseResult.badRequest(e.getMessage());
+        } else if (e instanceof BusinessException) {
+            //增加自定义内容响应
+            return ResponseResult.failed(e.getMessage());
+        } else if (e instanceof MissingServletRequestPartException) {
+            return ResponseResult.failed("预期的文件缺失");
+        } else {
+            log.error("请求:{} 异常,异常信息:{}", request.getRequestURI(), ExceptionUtil.stacktraceToString(e));
+            return ResponseResult.serverError(null);
+        }
+    }
+}

+ 14 - 12
src/main/java/com/xs/core/filter/RequestHandler.java

@@ -4,10 +4,13 @@ import cn.hutool.core.date.DatePattern;
 import cn.hutool.core.date.LocalDateTimeUtil;
 import com.alibaba.fastjson2.JSON;
 import com.alibaba.fastjson2.JSONObject;
+import com.xs.core.common.constant.ConstantConfig;
+import com.xs.core.common.exception.BusinessException;
 import com.xs.core.model.ResponseResult;
 import com.xs.core.request.BodyRequestWrapper;
 import com.xs.core.request.RequestWrapper;
 import com.xs.core.utils.SecurityUtil;
+import com.xs.core.utils.redis.RedissonLockUtil;
 import jakarta.servlet.*;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
@@ -135,17 +138,17 @@ public class RequestHandler implements Filter {
                     return;
                 }
                 // 验证请求唯一性
-                // --------
-                // 需要优化
-//                String key = String.format("%s%s", ConstantConfig.MESSAGE_ID_PREFIX, basicJson.getString("messageId"));
-//                if (redisService.hasKey(key)) {
-//                    log.error("加密解析===》已经接收过该请求!");
-//                    servletResponse.setContentType("application/json; charset=UTF-8");
-//                    servletResponse.getOutputStream().write(results);
-//                    return;
-//                }
-//                redisService.setNx(key, "0", 60 * 60 * 8);
-                // --------
+                String key = String.format("%s%s", ConstantConfig.MESSAGE_ID_PREFIX, basicJson.getString("messageId"));
+                try {
+                    boolean tryLock = RedissonLockUtil.tryLock(key);
+                    if (!tryLock) {
+                        servletResponse.setContentType("application/json; charset=UTF-8");
+                        servletResponse.getOutputStream().write(results);
+                        return;
+                    }
+                } catch (InterruptedException e) {
+                    throw new BusinessException("验证请求唯一性发生异常");
+                }
                 if (!requestJson.containsKey("bizData") ||
                         StringUtils.isBlank(requestJson.getString("bizData"))) {
                     request = new BodyRequestWrapper(request, "[]");
@@ -235,5 +238,4 @@ public class RequestHandler implements Filter {
 //        }
         return true;
     }
-
 }

+ 34 - 0
src/main/java/com/xs/core/filter/UserContextInterceptor.java

@@ -0,0 +1,34 @@
+package com.xs.core.filter;
+
+import cn.dev33.satoken.stp.StpUtil;
+import cn.hutool.core.util.StrUtil;
+import com.alibaba.fastjson2.JSON;
+import com.xs.core.common.constant.ConstantConfig;
+import com.xs.core.common.content.UserContext;
+import com.xs.core.common.content.UserContextHolder;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.servlet.HandlerInterceptor;
+
+/**
+ * 用户上下文拦截器
+ */
+@Slf4j
+public class UserContextInterceptor implements HandlerInterceptor {
+    @Override
+    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
+        log.info("UserContextInterceptor preHandle");
+        String extra = (String) StpUtil.getExtra(ConstantConfig.USER_CONTENT_EXTRA_KEY);
+        if (StrUtil.isNotBlank(extra)) {
+            UserContextHolder.setContext(JSON.parseObject(extra, UserContext.class));
+        }
+        return HandlerInterceptor.super.preHandle(request, response, handler);
+    }
+
+    @Override
+    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
+        //清除上下文防止内存溢出
+        UserContextHolder.clearContext();
+    }
+}

+ 23 - 0
src/main/java/com/xs/core/init/DatabaseInitializer.java

@@ -0,0 +1,23 @@
+package com.xs.core.init;
+
+import lombok.AllArgsConstructor;
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.stereotype.Component;
+
+import javax.sql.DataSource;
+import java.sql.Connection;
+
+@Component
+@AllArgsConstructor
+public class DatabaseInitializer implements CommandLineRunner {
+    private final DataSource dataSource;
+
+    @Override
+    public void run(String... args) throws Exception {
+        try (Connection connection = dataSource.getConnection()) {
+            // 执行一个简单的查询来初始化连接
+            connection.createStatement().execute("SELECT 1");
+            System.out.println("Database connection initialized");
+        }
+    }
+}

+ 16 - 0
src/main/java/com/xs/core/mapper/user/TgUserMapper.java

@@ -0,0 +1,16 @@
+package com.xs.core.mapper.user;
+
+import com.xs.core.model.user.entity.TgUser;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+
+/**
+ * <p>
+ * tg用户 Mapper 接口
+ * </p>
+ *
+ * @author xudm
+ * @since 2024-12-18
+ */
+public interface TgUserMapper extends BaseMapper<TgUser> {
+
+}

+ 233 - 0
src/main/java/com/xs/core/model/user/entity/TgUser.java

@@ -0,0 +1,233 @@
+package com.xs.core.model.user.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.baomidou.mybatisplus.extension.activerecord.Model;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+import lombok.Builder;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * <p>
+ * tg用户
+ * </p>
+ *
+ * @author xudm
+ * @since 2024-12-18
+ */
+@Getter
+@Setter
+@TableName("b_tg_user")
+@Builder
+public class TgUser extends Model<TgUser> {
+
+    /**
+     * id
+     */
+    @TableId(value = "id", type = IdType.ASSIGN_ID)
+    private Long id;
+
+    /**
+     * tg小程序id
+     */
+    @TableField("tg_id")
+    private String tgId;
+
+    /**
+     * tg账号
+     */
+    @TableField("tg_account")
+    private String tgAccount;
+
+    /**
+     * 名
+     */
+    @TableField("first_name")
+    private String firstName;
+
+    /**
+     * 姓
+     */
+    @TableField("last_name")
+    private String lastName;
+
+    /**
+     * 昵称
+     */
+    @TableField("nickname")
+    private String nickname;
+
+    /**
+     * 真实姓名
+     */
+    @TableField("real_name")
+    private String realName;
+
+    /**
+     * 头像
+     */
+    @TableField("avatar")
+    private String avatar;
+
+    /**
+     * 用户的语言的编码
+     */
+    @TableField("language_code")
+    private String languageCode;
+
+    /**
+     * 钱包地址
+     */
+    @TableField("wallet_address")
+    private String walletAddress;
+
+    /**
+     * 年限
+     */
+    @TableField("age_limit")
+    private Integer ageLimit;
+
+    /**
+     * 是否老用户[0=新用户, 1=老用户]
+     */
+    @TableField("old_user")
+    private Integer oldUser;
+
+    /**
+     * 选择的语言
+     */
+    @TableField("check_language")
+    private String checkLanguage;
+
+    /**
+     * 最后登录的IP
+     */
+    @TableField("login_ip")
+    private String loginIp;
+
+    /**
+     * 最后登录时间
+     */
+    @TableField("login_time")
+    private LocalDateTime loginTime;
+
+    /**
+     * ip转换实际地址
+     */
+    @TableField("ip_address_convert")
+    private String ipAddressConvert;
+
+    /**
+     * 是否禁用: [0=否, 1=是]
+     */
+    @TableField("disable_flag")
+    private Integer disableFlag;
+
+    /**
+     * 注册渠道: [1-微信小程序 2-微信公众号 3-手机H5 4-电脑PC 5-苹果APP 6-安卓APP]
+     */
+    @TableField("channel")
+    private Integer channel;
+
+    /**
+     * 客流途径 1 新增 2 邀请
+     */
+    @TableField("passenger_flow_way")
+    private Integer passengerFlowWay;
+
+    /**
+     * 用户授权telegram登录后的唯一标识
+     */
+    @TableField("login_telegram")
+    private String loginTelegram;
+
+    /**
+     * 币账户进入的唯一标识地址
+     */
+    @TableField("coin_address")
+    private String coinAddress;
+
+    /**
+     * 用户密码
+     */
+    @TableField("password")
+    private String password;
+
+    /**
+     * 用户电话
+     */
+    @TableField("mobile")
+    private String mobile;
+
+    /**
+     * 用户性别: [1=男, 2=女]
+     */
+    @TableField("sex")
+    private String sex;
+
+    /**
+     * 邀请码
+     */
+    @TableField("invite_code")
+    private String inviteCode;
+
+    /**
+     * 介绍人_id
+     */
+    @TableField("referrer_id")
+    private String referrerId;
+
+    /**
+     * 空投数量
+     */
+    @TableField("airdrop_coin")
+    private Integer airdropCoin;
+
+    /**
+     * 金币余额
+     */
+    @TableField("gold_coin_amount")
+    private BigDecimal goldCoinAmount;
+
+    /**
+     * 金币总数量
+     */
+    @TableField("gold_coin_total_his")
+    private BigDecimal goldCoinTotalHis;
+
+    /**
+     * 在线时间 单位秒
+     */
+    @TableField("online_time")
+    private Integer onlineTime;
+
+    /**
+     * 用户余额
+     */
+    @TableField("user_amount")
+    private BigDecimal userAmount;
+
+    /**
+     * 创建时间
+     */
+    @TableField("created_time")
+    private LocalDateTime createdTime;
+
+    /**
+     * 更新时间
+     */
+    @TableField("updated_time")
+    private LocalDateTime updatedTime;
+
+    @Override
+    public Serializable pkVal() {
+        return this.id;
+    }
+}

+ 0 - 3
src/main/java/com/xs/core/model/user/req/AppLoginReq.java

@@ -8,9 +8,6 @@ import lombok.Data;
 @Data
 public class AppLoginReq {
 
-    @Schema(description = "", example = "true")
-    private boolean allowsWriteToPm;
-
     @Schema(description = "名", example = "st")
     private String firstName;
     @Schema(description = "姓", example = "prpr")

+ 104 - 0
src/main/java/com/xs/core/service/Impl/user/AppLoginServiceImpl.java

@@ -0,0 +1,104 @@
+package com.xs.core.service.Impl.user;
+
+import cn.dev33.satoken.stp.SaLoginConfig;
+import cn.dev33.satoken.stp.SaTokenInfo;
+import cn.dev33.satoken.stp.StpUtil;
+import cn.hutool.core.util.ObjUtil;
+import cn.hutool.core.util.StrUtil;
+import com.alibaba.fastjson2.JSON;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.xs.core.common.constant.ConstantConfig;
+import com.xs.core.common.content.UserContext;
+import com.xs.core.common.enums.DisEnableStatusEnum;
+import com.xs.core.common.validation.CheckUtils;
+import com.xs.core.model.user.entity.TgUser;
+import com.xs.core.model.user.req.AppLoginReq;
+import com.xs.core.service.user.AppLoginService;
+import com.xs.core.service.user.ITgUserService;
+import com.xs.core.utils.InviteCodeGenerator;
+import com.xs.core.utils.SpringWebUtils;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.BeanUtils;
+import org.springframework.stereotype.Service;
+
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.time.LocalDateTime;
+import java.util.concurrent.CompletableFuture;
+
+@Service
+@AllArgsConstructor
+@Slf4j
+public class AppLoginServiceImpl implements AppLoginService {
+    private final ITgUserService userService;
+
+    @Override
+    public SaTokenInfo tgLogin(AppLoginReq loginReq) {
+        LambdaQueryWrapper<TgUser> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(TgUser::getTgId, loginReq.getId());
+        TgUser user = userService.getOne(queryWrapper);
+        if (ObjUtil.isNull(user)) {
+            user = registerToSys(loginReq);
+        }
+        //校验是否被禁用
+        checkUserStatus(user);
+        return this.login(user);
+    }
+
+    @Override
+    public TgUser registerToSys(AppLoginReq loginReq) {
+        TgUser user = TgUser.builder()
+                .tgAccount(loginReq.getUsername())
+                .tgId(loginReq.getId())
+                .avatar(URLDecoder.decode(loginReq.getPhotoUrl(), StandardCharsets.UTF_8))
+                .firstName(loginReq.getFirstName())
+                .lastName(loginReq.getLastName())
+                //注册时指定用户的唯一邀请码
+                .inviteCode(InviteCodeGenerator.generateInviteCode())
+                .languageCode(loginReq.getLanguageCode())
+                .createdTime(LocalDateTime.now())
+                .updatedTime(LocalDateTime.now())
+                .build();
+        if (StrUtil.isNotBlank(loginReq.getAge_limit())) {
+            user.setAgeLimit(Integer.parseInt(loginReq.getAge_limit()));
+        }
+        if (StrUtil.isNotBlank(loginReq.getShare_code())) {
+            //如邀请码不为空 则写入邀请人信息
+            LambdaQueryWrapper<TgUser> queryWrapper = new LambdaQueryWrapper<>();
+            queryWrapper.eq(TgUser::getInviteCode, loginReq.getShare_code());
+            TgUser inviteUser = userService.getOne(queryWrapper);
+            if (ObjUtil.isNotNull(inviteUser)) {
+                user.setReferrerId(String.valueOf(inviteUser.getId()));
+            }
+            user.setPassengerFlowWay(2);
+        }
+        userService.save(user);
+        return user;
+    }
+
+    /**
+     * 检查用户状态
+     *
+     * @param user 用户信息
+     */
+    private void checkUserStatus(TgUser user) {
+        CheckUtils.throwIfEqual(DisEnableStatusEnum.DISABLE.getValue(), user.getDisableFlag(), "此账号已被禁用,如有疑问,请联系管理员");
+    }
+
+    private SaTokenInfo login(TgUser user) {
+        Long userId = user.getId();
+        user.setLoginTime(LocalDateTime.now());
+        UserContext userContext = new UserContext();
+        BeanUtils.copyProperties(user, userContext);
+        userContext.setId(userId);
+        // 记录登录时间以及ip等信息
+        userContext.setExtraInfo(SpringWebUtils.getRequest());
+        // 登录并缓存用户信息
+        StpUtil.login(userId, SaLoginConfig.setExtra(ConstantConfig.USER_CONTENT_EXTRA_KEY, JSON.toJSONString(userContext)));
+        CompletableFuture.runAsync(() -> {
+            userService.updateById(user);
+        });
+        return StpUtil.getTokenInfo();
+    }
+}

+ 20 - 0
src/main/java/com/xs/core/service/Impl/user/TgUserServiceImpl.java

@@ -0,0 +1,20 @@
+package com.xs.core.service.Impl.user;
+
+import com.xs.core.model.user.entity.TgUser;
+import com.xs.core.mapper.user.TgUserMapper;
+import com.xs.core.service.user.ITgUserService;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.springframework.stereotype.Service;
+
+/**
+ * <p>
+ * tg用户 服务实现类
+ * </p>
+ *
+ * @author xudm
+ * @since 2024-12-18
+ */
+@Service
+public class TgUserServiceImpl extends ServiceImpl<TgUserMapper, TgUser> implements ITgUserService {
+
+}

+ 23 - 0
src/main/java/com/xs/core/service/user/AppLoginService.java

@@ -0,0 +1,23 @@
+package com.xs.core.service.user;
+
+import cn.dev33.satoken.stp.SaTokenInfo;
+import com.xs.core.model.user.entity.TgUser;
+import com.xs.core.model.user.req.AppLoginReq;
+
+public interface AppLoginService {
+    /**
+     * tg小程序登录
+     *
+     * @param loginReq 用户信息
+     * @return
+     */
+    SaTokenInfo tgLogin(AppLoginReq loginReq);
+
+    /**
+     * 注册tg用户到到系统
+     *
+     * @param loginReq
+     * @return
+     */
+    TgUser registerToSys(AppLoginReq loginReq);
+}

+ 16 - 0
src/main/java/com/xs/core/service/user/ITgUserService.java

@@ -0,0 +1,16 @@
+package com.xs.core.service.user;
+
+import com.xs.core.model.user.entity.TgUser;
+import com.baomidou.mybatisplus.extension.service.IService;
+
+/**
+ * <p>
+ * tg用户 服务类
+ * </p>
+ *
+ * @author xudm
+ * @since 2024-12-18
+ */
+public interface ITgUserService extends IService<TgUser> {
+
+}

+ 71 - 0
src/main/java/com/xs/core/utils/ExceptionUtils.java

@@ -0,0 +1,71 @@
+package com.xs.core.utils;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.function.Consumer;
+
+public class ExceptionUtils {
+    private static final Logger log = LoggerFactory.getLogger(ExceptionUtils.class);
+
+    private ExceptionUtils() {
+    }
+
+    public static void printException(Runnable runnable, Throwable throwable) {
+        if (null == throwable && runnable instanceof Future<?> future) {
+            try {
+                if (future.isDone()) {
+                    future.get();
+                }
+            } catch (CancellationException e) {
+                throwable = e;
+            } catch (ExecutionException e) {
+                throwable = e.getCause();
+            } catch (InterruptedException var6) {
+                Thread.currentThread().interrupt();
+            }
+        }
+
+        if (null != throwable) {
+            log.error(throwable.getMessage(), throwable);
+        }
+
+    }
+
+    public static <T> T exToNull(ExSupplier<T> exSupplier) {
+        return (T) exToDefault(exSupplier, null);
+    }
+
+    public static <T> T exToNull(ExSupplier<T> supplier, Consumer<Exception> exConsumer) {
+        return (T) exToDefault(supplier, null, exConsumer);
+    }
+
+    public static String exToBlank(ExSupplier<String> exSupplier) {
+        return (String) exToDefault(exSupplier, "");
+    }
+
+    public static <T> T exToDefault(ExSupplier<T> exSupplier, T defaultValue) {
+        return (T) exToDefault(exSupplier, defaultValue, (Consumer) null);
+    }
+
+    public static <T> T exToDefault(ExSupplier<T> exSupplier, T defaultValue, Consumer<Exception> exConsumer) {
+        try {
+            return exSupplier.get();
+        } catch (Exception e) {
+            if (null != exConsumer) {
+                exConsumer.accept(e);
+            }
+
+            return defaultValue;
+        }
+    }
+
+    @FunctionalInterface
+    public interface ExSupplier<T> {
+        T get() throws Exception;
+    }
+}
+

+ 69 - 0
src/main/java/com/xs/core/utils/InviteCodeGenerator.java

@@ -0,0 +1,69 @@
+package com.xs.core.utils;
+
+import cn.hutool.crypto.digest.DigestUtil;
+import jakarta.xml.bind.DatatypeConverter;
+
+import java.security.SecureRandom;
+
+public class InviteCodeGenerator {
+    private static final int CODE_LENGTH = 32; // 邀请码长度
+    private static final int RANDOM_CODE_LENGTH = 8; // 随机码长度
+    private static final SecureRandom RANDOM = new SecureRandom();
+
+    private static final String[] CHARACTERS = new String[]{"a", "b", "c", "d", "e", "f", "g", "h",
+            "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t",
+            "u", "v", "w", "x", "y", "z", "0", "1", "2", "3", "4", "5",
+            "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H",
+            "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T",
+            "U", "V", "W", "X", "Y", "Z"
+    };
+
+
+    public static String generateInviteCode() {
+        StringBuilder code = new StringBuilder();
+        StringBuilder randomCode = new StringBuilder();
+
+        for (int i = 0; i < CODE_LENGTH; i++) {
+            int index = RANDOM.nextInt(CHARACTERS.length);
+            code.append(CHARACTERS[index]);
+        }
+        for (int i = 0; i < RANDOM_CODE_LENGTH; i++) {
+            int index = RANDOM.nextInt(CHARACTERS.length);
+            randomCode.append(CHARACTERS[index]);
+        }
+
+        //邀请码+随机码组成一个字符
+        String tempCode = code.toString() + randomCode.toString();
+        byte[] digest = DigestUtil.md5(tempCode);
+        String sMD5EncryptResult = DatatypeConverter.printHexBinary(digest).toUpperCase();
+        String[] resUrl = new String[4];
+        //得到 4组短链接字符串
+        for (int i = 0; i < 4; i++) {
+            // 把加密字符按照 8 位一组 16 进制与 0x3FFFFFFF 进行位与运算
+            String sTempSubString = sMD5EncryptResult.substring(i * 8, i * 8 + 8);
+            // 这里需要使用 long 型来转换,因为 Inteper .parseInt() 只能处理 31 位 , 首位为符号位 , 如果不用 long ,则会越界
+            long lHexLong = 0x3FFFFFFF & Long.parseLong(sTempSubString, 16);
+            String outChars = "";
+            //循环获得每组6位的字符串
+            for (int j = 0; j < 6; j++) {
+                // 把得到的值与 0x0000003D 进行位与运算,取得字符数组 chars 索引(具体需要看chars数组的长度   以防下标溢出,注意起点为0)
+                long index = 0x0000003D & lHexLong;
+                // 把取得的字符相加
+                outChars += CHARACTERS[(int) index];
+                // 每次循环按位右移 5 位
+                lHexLong = lHexLong >> 5;
+            }
+            // 把字符串存入对应索引的输出数组
+            resUrl[i] = outChars;
+        }
+
+        return resUrl[0];
+    }
+
+    public static void main(String[] args) {
+        // 生成5个邀请码示例
+        for (int i = 0; i < 5; i++) {
+            System.out.println("邀请码 " + (i + 1) + ": " + generateInviteCode());
+        }
+    }
+}

+ 36 - 0
src/main/java/com/xs/core/utils/IpUtils.java

@@ -0,0 +1,36 @@
+package com.xs.core.utils;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.net.NetUtil;
+import cn.hutool.extra.spring.SpringUtil;
+import cn.hutool.http.HtmlUtil;
+import net.dreamlu.mica.ip2region.core.Ip2regionSearcher;
+import net.dreamlu.mica.ip2region.core.IpInfo;
+
+import java.util.Objects;
+import java.util.Set;
+
+public class IpUtils {
+    private IpUtils() {
+    }
+
+    public static String getIpv4Address(String ip) {
+        if (isInnerIpv4(ip)) {
+            return "内网IP";
+        } else {
+            Ip2regionSearcher ip2regionSearcher = (Ip2regionSearcher) SpringUtil.getBean(Ip2regionSearcher.class);
+            IpInfo ipInfo = ip2regionSearcher.memorySearch(ip);
+            if (null == ipInfo) {
+                return null;
+            } else {
+                Set<String> regionSet = CollUtil.newLinkedHashSet(new String[]{ipInfo.getCountry(), ipInfo.getRegion(), ipInfo.getProvince(), ipInfo.getCity(), ipInfo.getIsp()});
+                regionSet.removeIf(Objects::isNull);
+                return String.join("|", regionSet);
+            }
+        }
+    }
+
+    public static boolean isInnerIpv4(String ip) {
+        return NetUtil.isInnerIP("0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : HtmlUtil.cleanHtmlTag(ip));
+    }
+}

+ 186 - 0
src/main/java/com/xs/core/utils/MyGenerator.java

@@ -0,0 +1,186 @@
+package com.xs.core.utils;
+
+import com.baomidou.mybatisplus.annotation.FieldFill;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.baomidou.mybatisplus.generator.FastAutoGenerator;
+import com.baomidou.mybatisplus.generator.config.OutputFile;
+import com.baomidou.mybatisplus.generator.config.rules.DbColumnType;
+import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
+import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
+import com.baomidou.mybatisplus.generator.engine.VelocityTemplateEngine;
+import com.baomidou.mybatisplus.generator.fill.Column;
+import org.apache.commons.lang3.ClassUtils;
+
+import java.io.File;
+import java.sql.Types;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author zoutao
+ * @date 2023/09/13
+ */
+public class MyGenerator {
+
+    //数据库连接信息
+    static String url = "jdbc:mysql://192.168.241.130:3306/continew?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai",
+            username = "continew",
+            password = "continew123456";
+
+    // 设置作者
+    static String author = "xudm";
+    // 指定输出目录
+    static String outputDir = "D:\\DevelopmentSpace\\MyWorkSpace\\OutPutDir\\tg-game-app";
+
+    // 设置父包名
+    static String parent = "com.xs.core";
+    // 表前缀
+    static String[] tablePrefix = {"t_", "b_"};
+    // 字段前缀
+    static String[] fieldPrefix = {"f_"};
+    //自动填充字段
+    static Column[] fillColumns = new Column[]{
+            new Column("f_updated_time", FieldFill.INSERT_UPDATE),
+            new Column("f_created_time", FieldFill.INSERT),
+            new Column("f_corp_id", FieldFill.INSERT),
+            new Column("f_corp_name", FieldFill.INSERT),
+            new Column("f_created_user_business_id", FieldFill.INSERT),
+            new Column("f_created_user_name", FieldFill.INSERT),
+            new Column("f_updated_user_business_id", FieldFill.INSERT_UPDATE),
+            new Column("f_updated_user_name", FieldFill.INSERT_UPDATE),
+            new Column("f_delete_flag", FieldFill.INSERT),
+            new Column("f_status", FieldFill.INSERT),
+    };
+
+
+    public static void main(String[] args) {
+        // 项目名 例如app  web
+        generatorByBusinessModule("user", new String[]{"b_tg_user"});
+    }
+
+    /**
+     * @param businessModuleName 业务模块名称,可以为空
+     * @param generatorTables    需要生成的表名称
+     */
+    private static void generatorByBusinessModule(String businessModuleName, String[] generatorTables) {
+
+
+        //设置 entity 包名
+        String entityPackageName = "model." + businessModuleName + ".entity";
+        //设置 controller 包名
+        String controllerPackageName = "controller." + businessModuleName;
+        //设置 service 包名
+        String servicePackageName = "service." + businessModuleName;
+        //设置 serviceImpl 包名
+        String serviceImplPackageName = "service.Impl." + businessModuleName;
+        //设置 mapper 包名
+        String mapperPackageName = "mapper." + businessModuleName;
+
+        // 设置mapperXml生成路径
+        String mapperXmlPath = outputDir + "/mapper/";
+
+        // 设置 service 父类
+        String superServiceName = "com.baomidou.mybatisplus.extension.service.IService";
+        // 设置 serviceImpl 父类
+        String superServiceImplName = "com.baomidou.mybatisplus.extension.service.impl.ServiceImpl";
+
+        FastAutoGenerator.create(url, username, password)
+                .globalConfig(builder -> {
+                    builder.author(author)
+                            .outputDir(outputDir);
+                })
+                .dataSourceConfig(builder -> builder.typeConvertHandler((globalConfig, typeRegistry, metaInfo) -> {
+                    int typeCode = metaInfo.getJdbcType().TYPE_CODE;
+                    if (typeCode == Types.SMALLINT) {
+                        // 自定义类型转换
+                        return DbColumnType.INTEGER;
+                    }
+                    if (typeCode == Types.BIGINT) {
+                        return DbColumnType.LONG;
+                    }
+                    if (typeCode == Types.TINYINT) {
+                        return DbColumnType.BOOLEAN;
+                    }
+                    return typeRegistry.getColumnType(metaInfo);
+
+                }))
+
+                .packageConfig(builder -> {
+                    builder.entity(entityPackageName)
+                            .controller(controllerPackageName)
+                            .mapper(mapperPackageName)
+                            .service(servicePackageName)
+                            .serviceImpl(serviceImplPackageName)
+                            .parent(parent) // 设置父包名
+                            .pathInfo(Collections.singletonMap(OutputFile.xml, mapperXmlPath)); // 设置mapperXml生成路径
+                })
+                .strategyConfig(builder -> {
+
+                    builder.controllerBuilder()
+                            .enableFileOverride()
+                            .enableRestStyle();
+
+                    builder.serviceBuilder()
+                            .enableFileOverride()
+                            .superServiceClass(superServiceName)
+                            .superServiceImplClass(superServiceImplName)
+                            .build();
+
+                    builder.mapperBuilder()
+                            .enableBaseColumnList()
+                            .enableBaseResultMap()
+                            .enableFileOverride();
+                    builder.entityBuilder()
+                            .addTableFills(fillColumns)
+                            .enableFileOverride()
+                            .enableLombok()
+                            .idType(IdType.ASSIGN_ID) // 主键类型,生成策略
+                            .enableTableFieldAnnotation()
+                            .naming(NamingStrategy.underline_to_camel)
+                            .disableSerialVersionUID()
+                            .enableActiveRecord();// 开启 ActiveRecord 模式
+                    builder
+                            .addInclude(generatorTables)
+                            .addTablePrefix(tablePrefix)// 设置过滤表前缀
+                            .addFieldPrefix(fieldPrefix)
+                    ;
+                }).templateEngine(new FreemarkerTemplateEngine())
+                .execute();
+    }
+
+    static class ModuleTableGenerator {
+        public static Map<String, String[]> moduleTablesMap = new HashMap<>();
+
+        static {
+            moduleTablesMap.put("user", new String[]{"t_cmttg_human_role", "t_human_user", "t_test_user", "t_cmttg_user_basic", "t_cmttg_user_business", "t_cmttg_user_business_rel", "t_cmttg_user_platform"});
+            moduleTablesMap.put("role", new String[]{"t_cmttg_role_inc", "t_cmttg_role_type", "t_cmttg_role_type_rel", "t_cmttg_sys_role"});
+
+            moduleTablesMap.put("center", new String[]{"t_human_dept", "t_cmttg_human_org",
+                    "t_human_position", "t_cmttg_sys_base_org", "t_cmttg_sys_base_org_region",
+                    "t_cmttg_sys_base_org_role", "t_cmttg_sys_org", "t_cmttg_sys_org_role"
+            });
+            moduleTablesMap.put("menu", new String[]{"t_cmttg_app_info", "t_cmttg_app_menu", "t_cmttg_role_app_menu", "t_cmttg_role_web_menu", "t_cmttg_user_app_menu", "t_cmttg_user_web_menu", "t_cmttg_web_menu"
+            });
+            moduleTablesMap.put("system", new String[]{"t_cmttg_dict", "t_cmttg_sys_region",
+            });
+        }
+
+
+        private static void getFileTableNames() throws ClassNotFoundException {
+            String moduleName = "system";
+            String path = "C:\\Users\\Administrator\\IdeaProjects\\cmttg\\cmttg-common\\src\\main\\java\\com\\xwtec\\cmttg\\common\\pojo\\" + moduleName;
+
+            String packageName = "com.xwtec.cmttg.common.pojo." + moduleName;
+            File file = new File(path);
+            for (File listFile : file.listFiles()) {
+                String className = packageName + "." + listFile.getName().replace(".java", "");
+                Class<?> aClass = ClassUtils.getClass(className);
+                TableName annotation = aClass.getAnnotation(TableName.class);
+                System.err.print(",\"" + annotation.value() + "\"");
+            }
+        }
+
+    }
+}

+ 46 - 0
src/main/java/com/xs/core/utils/ServletUtils.java

@@ -0,0 +1,46 @@
+package com.xs.core.utils;
+
+
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.http.useragent.UserAgent;
+import cn.hutool.http.useragent.UserAgentUtil;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import java.util.Collection;
+import java.util.Map;
+
+public class ServletUtils {
+    private ServletUtils() {
+    }
+
+    public static String getBrowser(HttpServletRequest request) {
+        return null == request ? null : getBrowser(request.getHeader("User-Agent"));
+    }
+
+    public static String getBrowser(String userAgentString) {
+        UserAgent userAgent = UserAgentUtil.parse(userAgentString);
+        String var10000 = userAgent.getBrowser().getName();
+        return var10000 + " " + userAgent.getVersion();
+    }
+
+    public static String getOs(HttpServletRequest request) {
+        return null == request ? null : getOs(request.getHeader("User-Agent"));
+    }
+
+    public static String getOs(String userAgentString) {
+        UserAgent userAgent = UserAgentUtil.parse(userAgentString);
+        return userAgent.getOs().getName();
+    }
+
+    public static Map<String, String> getHeaderMap(HttpServletResponse response) {
+        Collection<String> headerNames = response.getHeaderNames();
+        Map<String, String> headerMap = MapUtil.newHashMap(headerNames.size(), true);
+
+        for (String name : headerNames) {
+            headerMap.put(name, response.getHeader(name));
+        }
+
+        return headerMap;
+    }
+}

+ 87 - 0
src/main/java/com/xs/core/utils/SpringWebUtils.java

@@ -0,0 +1,87 @@
+package com.xs.core.utils;
+
+import cn.hutool.core.text.CharSequenceUtil;
+import cn.hutool.core.util.ReflectUtil;
+import cn.hutool.extra.spring.SpringUtil;
+import jakarta.servlet.ServletContext;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.context.ApplicationContext;
+import org.springframework.http.server.PathContainer;
+import org.springframework.web.accept.ContentNegotiationManager;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+import org.springframework.web.servlet.HandlerMapping;
+import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
+import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping;
+import org.springframework.web.util.UrlPathHelper;
+import org.springframework.web.util.pattern.PathPattern;
+import org.springframework.web.util.pattern.PathPatternParser;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+public class SpringWebUtils {
+    private SpringWebUtils() {
+    }
+
+    public static HttpServletRequest getRequest() {
+        return getServletRequestAttributes().getRequest();
+    }
+
+    public static HttpServletResponse getResponse() {
+        return getServletRequestAttributes().getResponse();
+    }
+
+    public static boolean isMatch(String path, List<String> patterns) {
+        return patterns.stream().anyMatch((pattern) -> isMatch(path, pattern));
+    }
+
+    public static boolean isMatch(String path, String... patterns) {
+        return Arrays.stream(patterns).anyMatch((pattern) -> isMatch(path, pattern));
+    }
+
+    public static boolean isMatch(String path, String pattern) {
+        PathPattern pathPattern = PathPatternParser.defaultInstance.parse(pattern);
+        PathContainer pathContainer = PathContainer.parsePath(path);
+        return pathPattern.matches(pathContainer);
+    }
+
+    public static void deRegisterResourceHandler(Map<String, String> handlerMap) {
+        ApplicationContext applicationContext = SpringUtil.getApplicationContext();
+        HandlerMapping resourceHandlerMapping = (HandlerMapping) applicationContext.getBean("resourceHandlerMapping", HandlerMapping.class);
+        Map<String, Object> oldHandlerMap = (Map) ReflectUtil.getFieldValue(resourceHandlerMapping, "handlerMap");
+
+        for (Map.Entry<String, String> entry : handlerMap.entrySet()) {
+            String pathPattern = CharSequenceUtil.appendIfMissing((CharSequence) entry.getKey(), "/**", new CharSequence[0]);
+            oldHandlerMap.remove(pathPattern);
+        }
+
+    }
+
+    public static void registerResourceHandler(Map<String, String> handlerMap) {
+        ApplicationContext applicationContext = SpringUtil.getApplicationContext();
+        HandlerMapping resourceHandlerMapping = (HandlerMapping) applicationContext.getBean("resourceHandlerMapping", HandlerMapping.class);
+        Map<String, Object> oldHandlerMap = (Map) ReflectUtil.getFieldValue(resourceHandlerMapping, "handlerMap");
+        ServletContext servletContext = (ServletContext) applicationContext.getBean(ServletContext.class);
+        ContentNegotiationManager contentNegotiationManager = (ContentNegotiationManager) applicationContext.getBean("mvcContentNegotiationManager", ContentNegotiationManager.class);
+        UrlPathHelper urlPathHelper = (UrlPathHelper) applicationContext.getBean("mvcUrlPathHelper", UrlPathHelper.class);
+        ResourceHandlerRegistry resourceHandlerRegistry = new ResourceHandlerRegistry(applicationContext, servletContext, contentNegotiationManager, urlPathHelper);
+
+        for (Map.Entry<String, String> entry : handlerMap.entrySet()) {
+            String pathPattern = CharSequenceUtil.appendIfMissing((CharSequence) entry.getKey(), "/**", new CharSequence[0]);
+            oldHandlerMap.remove(pathPattern);
+            String resourceLocations = CharSequenceUtil.appendIfMissing((CharSequence) entry.getValue(), "/", new CharSequence[0]);
+            resourceHandlerRegistry.addResourceHandler(new String[]{pathPattern}).addResourceLocations(new String[]{"file:" + resourceLocations});
+        }
+
+        Map<String, ?> additionalUrlMap = ((SimpleUrlHandlerMapping) ReflectUtil.invoke(resourceHandlerRegistry, "getHandlerMapping", new Object[0])).getUrlMap();
+        ReflectUtil.invoke(resourceHandlerMapping, "registerHandlers", new Object[]{additionalUrlMap});
+    }
+
+    private static ServletRequestAttributes getServletRequestAttributes() {
+        return (ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes());
+    }
+}

+ 68 - 0
src/main/java/com/xs/core/utils/redis/RedissonLockUtil.java

@@ -0,0 +1,68 @@
+package com.xs.core.utils.redis;
+
+import lombok.extern.slf4j.Slf4j;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
+import org.springframework.stereotype.Component;
+
+import java.util.concurrent.TimeUnit;
+
+@Component
+@Slf4j
+public class RedissonLockUtil {
+    public static RedissonLockUtil lockUtil;
+    private final RedissonClient redissonClient;
+
+    private static final Long PRO_LOCK_RELEASE_TIME = 65L;//释放锁时间
+    private static final Long PROC_LOCK_WAIT_TIME = 2L;//获取锁等待时间
+
+    public RedissonLockUtil(RedissonClient redissonClient) {
+        this.redissonClient = redissonClient;
+        lockUtil = this;
+    }
+
+    /**
+     * 根据name对进行上锁操作,redisson Lock 一直等待获取锁
+     */
+    public static void lock(String lockName, long time) {
+        RLock lock = lockUtil.redissonClient.getLock(lockName);
+        log.info("lock.isLocked:{}", lock.isLocked());
+        //lock提供带timeout参数,timeout结束强制解锁,防止死锁
+        lock.lock(time, TimeUnit.SECONDS);
+    }
+
+
+    /**
+     * 根据name对进行上锁操作,redisson tryLock  根据第一个参数,一定时间(waitTime)内未获取到锁,则不再等待直接返回boolean
+     *
+     * @param lockName  锁名
+     * @param waitTime  等待获取锁时间
+     * @param leaseTime 上锁后释放锁时间
+     */
+    public static boolean tryLock(String lockName, long waitTime, long leaseTime) throws InterruptedException {
+        RLock lock = lockUtil.redissonClient.getLock(lockName);
+        //tryLock,第一个参数是等待时间,waitTime内获取不到锁,则直接返回。 第二个参数 释放锁时间
+        return lock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS);
+    }
+
+    /**
+     * 尝试获取锁 默认等待2秒  65秒自动释放锁
+     *
+     * @param lockName 锁名称
+     * @return
+     * @throws InterruptedException
+     */
+    public static boolean tryLock(String lockName) throws InterruptedException {
+        RLock lock = lockUtil.redissonClient.getLock(lockName);
+        //tryLock,第一个参数是等待时间,waitTime内获取不到锁,则直接返回。 第二个参数 释放锁时间
+        return lock.tryLock(PROC_LOCK_WAIT_TIME, PRO_LOCK_RELEASE_TIME, TimeUnit.SECONDS);
+    }
+
+    /**
+     * 释放锁操作
+     */
+    public static void unLock(String lockName) {
+        RLock lock = lockUtil.redissonClient.getLock(lockName);
+        lock.forceUnlock();
+    }
+}

+ 150 - 0
src/main/resources/ESAPI.properties

@@ -0,0 +1,150 @@
+# 是否要打印配置属性,默认为true
+ESAPI.printProperties=true
+ESAPI.AccessControl=org.owasp.esapi.reference.DefaultAccessController
+ESAPI.Authenticator=org.owasp.esapi.reference.FileBasedAuthenticator
+ESAPI.Encoder=org.owasp.esapi.reference.DefaultEncoder
+ESAPI.Encryptor=org.owasp.esapi.reference.crypto.JavaEncryptor
+ESAPI.Executor=org.owasp.esapi.reference.DefaultExecutor
+ESAPI.HTTPUtilities=org.owasp.esapi.reference.DefaultHTTPUtilities
+ESAPI.IntrusionDetector=org.owasp.esapi.reference.DefaultIntrusionDetector
+ESAPI.Logger=org.owasp.esapi.logging.java.JavaLogFactory
+ESAPI.Randomizer=org.owasp.esapi.reference.DefaultRandomizer
+ESAPI.Validator=org.owasp.esapi.reference.DefaultValidator
+
+
+#===========================================================================
+# ESAPI Encoder
+Encoder.AllowMultipleEncoding=false
+Encoder.AllowMixedEncoding=false
+Encoder.DefaultCodecList=HTMLEntityCodec,PercentCodec,JavaScriptCodec
+
+
+#===========================================================================
+# ESAPI 加密模块
+Encryptor.PreferredJCEProvider=
+Encryptor.EncryptionAlgorithm=AES
+Encryptor.CipherTransformation=AES/CBC/PKCS5Padding
+Encryptor.cipher_modes.combined_modes=GCM,CCM,IAPM,EAX,OCB,CWC
+Encryptor.cipher_modes.additional_allowed=CBC
+Encryptor.EncryptionKeyLength=128
+Encryptor.ChooseIVMethod=random
+Encryptor.fixedIV=0x000102030405060708090a0b0c0d0e0f
+Encryptor.CipherText.useMAC=true
+Encryptor.PlainText.overwrite=true
+Encryptor.HashAlgorithm=SHA-512
+Encryptor.HashIterations=1024
+Encryptor.DigitalSignatureAlgorithm=SHA1withDSA
+Encryptor.DigitalSignatureKeyLength=1024
+Encryptor.RandomAlgorithm=SHA1PRNG
+Encryptor.CharacterEncoding=UTF-8
+Encryptor.KDF.PRF=HmacSHA256
+
+#===========================================================================
+# ESAPI Http工具
+
+HttpUtilities.UploadDir=C:\\ESAPI\\testUpload
+HttpUtilities.UploadTempDir=C:\\temp
+# Force flags on cookies, if you use HttpUtilities to set cookies
+HttpUtilities.ForceHttpOnlySession=false
+HttpUtilities.ForceSecureSession=false
+HttpUtilities.ForceHttpOnlyCookies=true
+HttpUtilities.ForceSecureCookies=true
+# Maximum size of HTTP headers
+HttpUtilities.MaxHeaderSize=4096
+# File upload configuration
+HttpUtilities.ApprovedUploadExtensions=.zip,.pdf,.doc,.docx,.ppt,.pptx,.tar,.gz,.tgz,.rar,.war,.jar,.ear,.xls,.rtf,.properties,.java,.class,.txt,.xml,.jsp,.jsf,.exe,.dll
+HttpUtilities.MaxUploadFileBytes=500000000
+# Using UTF-8 throughout your stack is highly recommended. That includes your database driver,
+# container, and any other technologies you may be using. Failure to do this may expose you
+# to Unicode transcoding injection attacks. Use of UTF-8 does not hinder internationalization.
+HttpUtilities.ResponseContentType=text/html; charset=UTF-8
+# This is the name of the cookie used to represent the HTTP session
+# Typically this will be the default "JSESSIONID"
+HttpUtilities.HttpSessionIdName=JSESSIONID
+
+
+
+#===========================================================================
+# ESAPI Executor
+Executor.WorkingDirectory=
+Executor.ApprovedExecutables=
+
+
+#===========================================================================
+# ESAPI Logging
+# Set the application name if these logs are combined with other applications
+Logger.ApplicationName=ExampleApplication
+# If you use an HTML log viewer that does not properly HTML escape log data, you can set LogEncodingRequired to true
+Logger.LogEncodingRequired=false
+# Determines whether ESAPI should log the application name. This might be clutter in some single-server/single-app environments.
+Logger.LogApplicationName=true
+# Determines whether ESAPI should log the server IP and port. This might be clutter in some single-server environments.
+Logger.LogServerIP=true
+# LogFileName, the name of the logging file. Provide a full directory path (e.g., C:\\ESAPI\\ESAPI_logging_file) if you
+# want to place it in a specific directory.
+Logger.LogFileName=ESAPI_logging_file
+# MaxLogFileSize, the max size (in bytes) of a single log file before it cuts over to a new one (default is 10,000,000)
+Logger.MaxLogFileSize=10000000
+Logger.UserInfo=true
+Logger.ClientInfo=true
+
+
+#===========================================================================
+# ESAPI Intrusion Detection
+IntrusionDetector.Disable=false
+IntrusionDetector.event.test.count=2
+IntrusionDetector.event.test.interval=10
+IntrusionDetector.event.test.actions=disable,log
+
+IntrusionDetector.org.owasp.esapi.errors.IntrusionException.count=1
+IntrusionDetector.org.owasp.esapi.errors.IntrusionException.interval=1
+IntrusionDetector.org.owasp.esapi.errors.IntrusionException.actions=log,disable,logout
+IntrusionDetector.org.owasp.esapi.errors.IntegrityException.count=10
+IntrusionDetector.org.owasp.esapi.errors.IntegrityException.interval=5
+IntrusionDetector.org.owasp.esapi.errors.IntegrityException.actions=log,disable,logout
+IntrusionDetector.org.owasp.esapi.errors.AuthenticationHostException.count=2
+IntrusionDetector.org.owasp.esapi.errors.AuthenticationHostException.interval=10
+IntrusionDetector.org.owasp.esapi.errors.AuthenticationHostException.actions=log,logout
+
+
+#===========================================================================
+# ESAPI 校验器
+#校验器的配置文件
+Validator.ConfigurationFile=validation.properties
+
+# Validators used by ESAPI
+Validator.AccountName=^[a-zA-Z0-9]{3,20}$
+Validator.SystemCommand=^[a-zA-Z\\-\\/]{1,64}$
+Validator.RoleName=^[a-z]{1,20}$
+
+#the word TEST below should be changed to your application
+#name - only relative URL's are supported
+Validator.Redirect=^\\/test.*$
+
+# Global HTTP Validation Rules
+# Values with Base64 encoded data (e.g. encrypted state) will need at least [a-zA-Z0-9\/+=]
+Validator.HTTPScheme=^(http|https)$
+Validator.HTTPServerName=^[a-zA-Z0-9_.\\-]*$
+Validator.HTTPParameterName=^[a-zA-Z0-9_]{1,32}$
+Validator.HTTPParameterValue=^[a-zA-Z0-9.\\-\\/+=@_ ]*$
+Validator.HTTPCookieName=^[a-zA-Z0-9\\-_]{1,32}$
+Validator.HTTPCookieValue=^[a-zA-Z0-9\\-\\/+=_ ]*$
+
+# Note that max header name capped at 150 in SecurityRequestWrapper!
+Validator.HTTPHeaderName=^[a-zA-Z0-9\\-_]{1,50}$
+Validator.HTTPHeaderValue=^[a-zA-Z0-9()\\-=\\*\\.\\?;,+\\/:&_ ]*$
+Validator.HTTPContextPath=^\\/?[a-zA-Z0-9.\\-\\/_]*$
+Validator.HTTPServletPath=^[a-zA-Z0-9.\\-\\/_]*$
+Validator.HTTPPath=^[a-zA-Z0-9.\\-_]*$
+Validator.HTTPQueryString=^[a-zA-Z0-9()\\-=\\*\\.\\?;,+\\/:&_ %]*$
+Validator.HTTPURI=^[a-zA-Z0-9()\\-=\\*\\.\\?;,+\\/:&_ ]*$
+Validator.HTTPURL=^.*$
+Validator.HTTPJSESSIONID=^[A-Z0-9]{10,30}$
+
+# Validation of file related input
+Validator.FileName=^[a-zA-Z0-9!@#$%^&{}\\[\\]()_+\\-=,.~'` ]{1,255}$
+Validator.DirectoryName=^[a-zA-Z0-9:/\\\\!@#$%^&{}\\[\\]()_+\\-=,.~'` ]{1,255}$
+
+# Validation of dates. Controls whether or not 'lenient' dates are accepted.
+# See DataFormat.setLenient(boolean flag) for further details.
+Validator.AcceptLenientDates=false

+ 124 - 0
src/main/resources/application-dev.yml

@@ -0,0 +1,124 @@
+spring:
+  data:
+    redis:
+      host: 192.168.241.130
+      port: 6379
+      password: xudm5200
+      database: 6
+      lettuce:
+        pool:
+          # 连接池最大连接数
+          max-active: 200
+          # 连接池最大阻塞等待时间(使用负值表示没有限制)
+          max-wait: -1ms
+          # 连接池中的最大空闲连接
+          max-idle: 10
+          # 连接池中的最小空闲连接
+          min-idle: 2
+  datasource:
+    driver-class-name: com.mysql.cj.jdbc.Driver
+    url: jdbc:mysql://192.168.241.130:3306/continew?useUnicode=true&characterEncoding=utf-8&useSSL=false
+    username: continew
+    password: continew123456
+    hikari:
+      # 最大连接数量(默认 10,根据实际环境调整)
+      # 注意:当连接达到上限,并且没有空闲连接可用时,获取连接将在超时前阻塞最多 connectionTimeout 毫秒
+      maximum-pool-size: 20
+      minimum-idle: 5
+      connection-test-query: SELECT 1
+      # 获取连接超时时间(默认 30000 毫秒,30 秒)
+      connection-timeout: 30000
+      # 空闲连接最大存活时间(默认 600000 毫秒,10 分钟)
+      idle-timeout: 600000
+      # 保持连接活动的频率,以防止它被数据库或网络基础设施超时。该值必须小于 maxLifetime(默认 0,禁用)
+      keepaliveTime: 30000
+      # 连接最大生存时间(默认 1800000 毫秒,30 分钟)
+      max-lifetime: 1800000
+      # 启动时建立连接
+      initialization-fail-timeout: 60000
+
+
+#myabtis-plus
+mybatis-plus:
+  # Mapper XML 文件目录配置
+  mapper-locations: classpath*:/mapper/**/*.xml
+  # 类型别名扫描包配置
+  type-aliases-package: com.xs.core.model
+  ## MyBatis 配置
+  configuration:
+    # MyBatis 自动映射策略
+    # NONE:不启用 PARTIAL:只对非嵌套 resultMap 自动映射 FULL:对所有 resultMap 自动映射
+    auto-mapping-behavior: PARTIAL
+  ## 全局配置
+  global-config:
+    banner: true
+    db-config:
+      # 主键类型(默认 assign_id,表示自行赋值)
+      # auto 代表使用数据库自增策略(需要在表中设置好自增约束)
+      id-type: ASSIGN_ID
+      # 逻辑删除字段
+      logic-delete-field: deleteFlag
+      # 逻辑删除全局值(默认 1,表示已删除)
+      logic-delete-value: 1
+      # 逻辑未删除全局值(默认 0,表示未删除)
+      logic-not-delete-value: 0
+
+--- ### CosId 配置
+cosid:
+  namespace: ${spring.application.name}
+  machine:
+    enabled: true
+    # 机器号分配器
+    distributor:
+      type: REDIS
+    guarder:
+      # 开启机器号守护
+      enabled: true
+  snowflake:
+    enabled: true
+    zone-id: Asia/Shanghai
+    epoch: 1577203200000
+    share:
+      # 开启时钟回拨同步
+      clock-sync: true
+      friendly: true
+    provider:
+      safe-js:
+        machine-bit: 7
+        sequence-bit: 9
+
+############## Sa-Token 配置 (文档: https://sa-token.cc) ##############
+sa-token:
+  # token 名称(同时也是 cookie 名称)
+  token-name: Authorization
+  # token前缀
+  token-prefix: Bearer
+  # token 有效期(单位:秒) 默认30天,-1 代表永久有效
+  timeout: 86400
+  # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
+  active-timeout: -1
+  # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
+  is-concurrent: false
+  # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)
+  is-share: true
+  # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)
+  token-style: tik
+  # 是否输出操作日志
+  is-log: false
+  jwt-secret-key: asdaefghifhueuiwyurfewbfjsdafjk
+
+# 配置请求是否加密
+encryption:
+  isEncryption: false
+  requestPrivateKey: 14FDF4948E60F856524FBB1175E83716558B8CBF71A68318875F506B9C6D2A4A
+  requestPublicKey: 04F36CD10986CE214D0E5C540C30E0552DC8499B64E5B2709245D03BF2CADAA0CCA3C2BC2C8DB511012A50FAA1E43FCD4B8ABC521418EAB2D96F0075AD940EB25F
+  responsePrivateKey: 00C83135E19EBD958593091F42A3442DE3D03D975A5DBD4CE19F85C9FBF2D364B7
+  responsePublicKey: 040D10F8CA3AAC83345DA54472CCE5AB495BBB15E21E960B2A2EEE1D9EEC2E9EB1BE3902606904BD767FF056F59CC1AD237D3074A3F8D452AE376FFE84113640C0
+
+
+# 防止xss攻击配置
+xssFilter:
+  # 是否开启防止xss攻击
+  isOpenXssFilter: false
+  # 不拦截路径,多个用“,”分隔,例如/userBusiness/*,/sysRole/*
+  xssFilterWhileUrl: /process/*

+ 2 - 71
src/main/resources/application.yml

@@ -6,74 +6,5 @@ server:
 spring:
   application:
     name: xs-tg-game
-  data:
-    redis:
-      host: 192.168.241.130
-      port: 6379
-      password: xudm5200
-      database: 5
-      lettuce:
-        pool:
-          # 连接池最大连接数
-          max-active: 200
-          # 连接池最大阻塞等待时间(使用负值表示没有限制)
-          max-wait: -1ms
-          # 连接池中的最大空闲连接
-          max-idle: 10
-          # 连接池中的最小空闲连接
-          min-idle: 2
-  datasource:
-    type: com.zaxxer.hikari.HikariDataSource
-    driver-class-name: com.mysql.cj.jdbc.Driver
-    url: jdbc:p6spy:mysql://192.168.241.130:3306/continew?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&useSSL=false&allowMultiQueries=true&rewriteBatchedStatements=true&autoReconnect=true&maxReconnects=10&failOverReadOnly=false
-    username: continew
-    password: continew123456
-    hikari:
-      # 最大连接数量(默认 10,根据实际环境调整)
-      # 注意:当连接达到上限,并且没有空闲连接可用时,获取连接将在超时前阻塞最多 connectionTimeout 毫秒
-      maximum-pool-size: 20
-      # 获取连接超时时间(默认 30000 毫秒,30 秒)
-      connection-timeout: 30000
-      # 空闲连接最大存活时间(默认 600000 毫秒,10 分钟)
-      idle-timeout: 600000
-      # 保持连接活动的频率,以防止它被数据库或网络基础设施超时。该值必须小于 maxLifetime(默认 0,禁用)
-      keepaliveTime: 30000
-      # 连接最大生存时间(默认 1800000 毫秒,30 分钟)
-      max-lifetime: 1800000
-
-
-############## Sa-Token 配置 (文档: https://sa-token.cc) ##############
-sa-token:
-  # token 名称(同时也是 cookie 名称)
-  token-name: Authorization
-  # token前缀
-  token-prefix: Bearer
-  # token 有效期(单位:秒) 默认30天,-1 代表永久有效
-  timeout: 86400
-  # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
-  active-timeout: -1
-  # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
-  is-concurrent: false
-  # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)
-  is-share: true
-  # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)
-  token-style: tik
-  # 是否输出操作日志
-  is-log: false
-  jwt-secret-key: asdasdasifhueuiwyurfewbfjsdafjk
-
-# 配置请求是否加密
-encryption:
-  isEncryption: true
-  requestPrivateKey: 14FDF4948E60F856524FBB1175E83716558B8CBF71A68318875F506B9C6D2A4A
-  requestPublicKey: 04F36CD10986CE214D0E5C540C30E0552DC8499B64E5B2709245D03BF2CADAA0CCA3C2BC2C8DB511012A50FAA1E43FCD4B8ABC521418EAB2D96F0075AD940EB25F
-  responsePrivateKey: 00C83135E19EBD958593091F42A3442DE3D03D975A5DBD4CE19F85C9FBF2D364B7
-  responsePublicKey: 040D10F8CA3AAC83345DA54472CCE5AB495BBB15E21E960B2A2EEE1D9EEC2E9EB1BE3902606904BD767FF056F59CC1AD237D3074A3F8D452AE376FFE84113640C0
-
-
-# 防止xss攻击配置
-xssFilter:
-  # 是否开启防止xss攻击
-  isOpenXssFilter: false
-  # 不拦截路径,多个用“,”分隔,例如/userBusiness/*,/sysRole/*
-  xssFilterWhileUrl: /process/*
+  profiles:
+    active: dev

+ 6 - 0
src/main/resources/esapi-java-logging.properties

@@ -0,0 +1,6 @@
+handlers= java.util.logging.ConsoleHandler
+.level= INFO
+java.util.logging.ConsoleHandler.level = INFO
+java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
+java.util.logging.SimpleFormatter.format=[%1$tF %1$tT] [%3$-7s] %5$s %n
+#https://www.logicbig.com/tutorials/core-java-tutorial/logging/customizing-default-format.html

+ 99 - 0
src/main/resources/logback-spring.xml

@@ -0,0 +1,99 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration scan="true" scanPeriod="60 seconds" debug="false">
+    <!-- 生成一个日志文件,超出限制后会重新生成1个 -->
+    <!-- Springboot配置文件(yml/yml/properties) 中定义参数  -->
+    <springProperty scope="context" name="logPath" source="logging.file.path" defaultValue="logs"/>
+    <springProperty scope="context" name="appName" source="spring.application.name"/>
+
+    <!--  定义日志文件的存储地址  -->
+    <property name="LOG_HOME" value="${logPath}"/>
+
+    <!-- 应用名称:和统一配置中的项目代码保持一致(小写) -->
+    <property name="APP_NAME" value="${appName}"/>
+
+    <!--日志文件保留天数 -->
+    <property name="LOG_MAX_HISTORY" value="30"/>
+
+    <!--单日志文件大小,单位M -->
+    <property name="MAX_FILE_SIZE" value="100MB"/>
+
+    <!--定义日志文件的存储地址 勿在 LogBack 的配置中使用相对路径 -->
+    <!--应用日志文件保存路径 -->
+    <property name="LOG_APP_HOME" value="${LOG_HOME}"/>
+
+    <!-- 彩色日志依赖的渲染类 -->
+    <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="${APP_NAME} >> ${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(LN:%L){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
+
+    <!-- 控制台输出 -->
+    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+            <!--格式化输出:%d表示日期,%-5level:级别从左显示5个字符宽度,%t表示线程名,%msg:日志消息,%n是换行符-->
+            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level ${PID:-} --- [%t] %logger{50} - %msg%n</pattern>
+        </encoder>
+    </appender>
+
+    <!-- info级别日志文件输出 -->
+    <appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <!-- 日志文件输出的文件名 -->
+        <File>${LOG_HOME}/${APP_NAME}/info.log</File>
+        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+            <!-- 每日生成日志文件或日志文件大小超出限制后输出的文件名模板 -->
+            <fileNamePattern>${LOG_APP_HOME}/info-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
+            <!-- 日志文件的最大保留时间,超过设定时间后会自动删除 -->
+            <maxHistory>${LOG_MAX_HISTORY}</maxHistory>
+            <!-- 每份日志文件的最大限制,超出限制后会重新生成,并将旧的日志文件按照fileNamePattern设定的日志命名方式进行命名 -->
+            <maxFileSize>${MAX_FILE_SIZE}</maxFileSize>
+        </rollingPolicy>
+        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level ${PID:-} --- [%t] %logger{50} - %msg%n</pattern>
+        </encoder>
+    </appender>
+
+    <!-- error级别日志文件输出 -->
+    <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <!-- 日志输出级别,优先级 > '<root level>' -->
+        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
+            <level>ERROR</level>
+        </filter>
+        <File>${LOG_APP_HOME}/${APP_NAME}/error.log</File>
+        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+            <fileNamePattern>${LOG_APP_HOME}/error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
+            <maxHistory>${LOG_MAX_HISTORY}</maxHistory>
+            <maxFileSize>${MAX_FILE_SIZE}</maxFileSize>
+        </rollingPolicy>
+        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level ${PID:-} --- [%t] %logger{50} - %msg%n</pattern>
+        </encoder>
+    </appender>
+
+    <appender name="fileAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${LOG_APP_HOME}/project-id-execution-detail-info.log</file>
+        <encoder>
+            <charset>UTF-8</charset>
+            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level ${PID:-} -- [%t] %logger{50} - %msg%n</pattern>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
+            <level>INFO</level>
+        </filter>
+    </appender>
+
+    <!-- 显示形成的sql、使用的参数、结果集 生产环境需要删掉 -->
+    <logger name="java.sql" level="info"/>
+    <logger name="org.springframework.jdbc" level="info"/>
+
+    <!-- 默认日志输出级别 -->
+    <root level="INFO">
+        <appender-ref ref="STDOUT"/>
+        <appender-ref ref="INFO_FILE"/>
+        <appender-ref ref="ERROR_FILE"/>
+    </root>
+
+</configuration>

+ 45 - 0
src/main/resources/mapper/TgUserMapper.xml

@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.xs.core.mapper.user.TgUserMapper">
+    <!-- 通用查询映射结果 -->
+    <resultMap id="BaseResultMap" type="com.xs.core.model.user.entity.TgUser">
+        <id column="id" property="id"/>
+        <result column="tg_id" property="tgId"/>
+        <result column="tg_account" property="tgAccount"/>
+        <result column="first_name" property="firstName"/>
+        <result column="last_name" property="lastName"/>
+        <result column="nickname" property="nickname"/>
+        <result column="real_name" property="realName"/>
+        <result column="avatar" property="avatar"/>
+        <result column="language_code" property="languageCode"/>
+        <result column="wallet_address" property="walletAddress"/>
+        <result column="age_limit" property="ageLimit"/>
+        <result column="old_user" property="oldUser"/>
+        <result column="check_language" property="checkLanguage"/>
+        <result column="login_ip" property="loginIp"/>
+        <result column="login_time" property="loginTime"/>
+        <result column="ip_address_convert" property="ipAddressConvert"/>
+        <result column="disable_flag" property="disableFlag"/>
+        <result column="channel" property="channel"/>
+        <result column="passenger_flow_way" property="passengerFlowWay"/>
+        <result column="login_telegram" property="loginTelegram"/>
+        <result column="coin_address" property="coinAddress"/>
+        <result column="password" property="password"/>
+        <result column="mobile" property="mobile"/>
+        <result column="sex" property="sex"/>
+        <result column="invite_code" property="inviteCode"/>
+        <result column="referrer_id" property="referrerId"/>
+        <result column="airdrop_coin" property="airdropCoin"/>
+        <result column="gold_coin_amount" property="goldCoinAmount"/>
+        <result column="gold_coin_total_his" property="goldCoinTotalHis"/>
+        <result column="online_time" property="onlineTime"/>
+        <result column="user_amount" property="userAmount"/>
+        <result column="created_time" property="createdTime"/>
+        <result column="updated_time" property="updatedTime"/>
+    </resultMap>
+
+    <!-- 通用查询结果列 -->
+    <sql id="Base_Column_List">
+        id, tg_id, tg_account, first_name, last_name, nickname, real_name, avatar, language_code, wallet_address, age_limit, old_user, check_language, login_ip, login_time, ip_address_convert, disable_flag, channel, passenger_flow_way, login_telegram, coin_address, password, mobile, sex, invite_code, referrer_id, airdrop_coin, gold_coin_amount, gold_coin_total_his, online_time, user_amount, created_time, updated_time
+    </sql>
+</mapper>

+ 7 - 0
src/main/resources/validation.properties

@@ -0,0 +1,7 @@
+# 校验某个字段的正则表达式
+Validator.SafeString=^[.\\p{Alnum}\\p{Space}]{0,1024}$
+Validator.Email=^[A-Za-z0-9._%'-]+@[A-Za-z0-9.-]+\\.[a-zA-Z]{2,4}$
+Validator.IPAddress=^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$
+Validator.URL=^(ht|f)tp(s?)\\:\\/\\/[0-9a-zA-Z]([-.\\w]*[0-9a-zA-Z])*(:(0-9)*)*(\\/?)([a-zA-Z0-9\\-\\.\\?\\,\\:\\'\\/\\\\\\+=&amp;%\\$#_]*)?$
+Validator.CreditCard=^(\\d{4}[- ]?){3}\\d{4}$
+Validator.SSN=^(?!000)([0-6]\\d{2}|7([0-6]\\d|7[012]))([ -]?)(?!00)\\d\\d\\3(?!0000)\\d{4}$validation.properties