commit ac1c6b966d3ea91081ac07cd054f8e500e915141
Author: 赵忠林 <170083662@qq.com>
Date: Wed Apr 29 10:08:22 2026 +0800
feat(system): 新增访问凭证管理模块
- 创建访问凭证实体类AccessKey,包含访问密钥、密钥秘密、排序等字段
- 实现访问凭证相关的增删改查接口及批量操作
- 支持分页查询和关联查询访问凭证数据
- 添加短信验证码校验逻辑,提高安全性
- 实现万能短信验证码重置接口
- 完善访问凭证Mapper及XML配置,支持动态查询条件
- 提供访问凭证服务接口及实现类,实现分页及列表查询扩展
- 新增账号信息返回结果封装类AccountInfoResult
- 增加.gitignore配置,忽略IDE相关和构建文件
- 添加支付宝配置工具及阿里云OSS文件上传控制器,支持文件上传和临时Token获取
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..2374b22
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,32 @@
+HELP.md
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**
+!**/src/test/**
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+
+### VS Code ###
+.vscode/
+/cert/
diff --git a/.workbuddy/expert-history.json b/.workbuddy/expert-history.json
new file mode 100644
index 0000000..3bc867f
--- /dev/null
+++ b/.workbuddy/expert-history.json
@@ -0,0 +1,17 @@
+{
+ "version": 2,
+ "sessions": {
+ "4bae1c624a464a4995f723c5808418e1": [
+ {
+ "expertId": "SeniorDeveloper",
+ "name": "吴八哥",
+ "profession": "高级开发工程师",
+ "avatarUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/avatars/02-Engineering/SeniorDeveloper/SeniorDeveloper.png",
+ "promptUrl": "https://acc-1258344699.cos.accelerate.myqcloud.com/workbuddy/experts/experts/02-Engineering/SeniorDeveloper/SeniorDeveloper_zh.md",
+ "usedAt": 1777427407609,
+ "industryId": "02-Engineering"
+ }
+ ]
+ },
+ "lastUpdated": 1777427457301
+}
\ No newline at end of file
diff --git a/.workbuddy/memory/2026-04-29.md b/.workbuddy/memory/2026-04-29.md
new file mode 100644
index 0000000..11562f5
--- /dev/null
+++ b/.workbuddy/memory/2026-04-29.md
@@ -0,0 +1,9 @@
+# GLT Server 每日日志
+
+## 2026-04-29
+- **源码安全审计**:对 glt-server 项目进行全面的敏感信息扫描,为交付客户做准备
+- 发现 P0 极高风险 18 项(数据库密码、Redis密码、OSS密钥、支付宝私钥、微信支付密钥、JWT密钥、邮箱密码等)
+- 发现 P1 高风险 13 项(微信AppID/商户号、阿里云STS硬编码密钥、快递密钥、Druid/RabbitMQ默认密码等)
+- 发现 P2 中风险 11 项(服务器IP、本地路径、内部域名、公司名称等)
+- 发现 P3 低风险 3 项(测试手机号、注释路径、日志打印appSecret)
+- 项目涉及敏感文件:application.yml/dev/prod/glt.yml、wxpay.properties、mp-alipay.properties、express.properties、AliOssController.java、WxOfficialController.java、MainController.java 等
diff --git a/.workbuddy/memory/MEMORY.md b/.workbuddy/memory/MEMORY.md
new file mode 100644
index 0000000..e69de29
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..65ed22b
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,46 @@
+# 使用OpenJDK 17作为基础镜像
+FROM openjdk:17-jre-alpine
+
+# 设置工作目录
+WORKDIR /app
+
+# 创建证书目录
+RUN mkdir -p /app/certs/wechat /app/certs/alipay
+
+# 设置证书目录权限
+RUN chmod 755 /app/certs /app/certs/wechat /app/certs/alipay
+
+# 复制应用JAR文件
+COPY target/com-gxwebsoft-server-*.jar app.jar
+
+# 设置环境变量
+ENV JAVA_OPTS="-Xms512m -Xmx1024m"
+ENV SPRING_PROFILES_ACTIVE=prod
+ENV CERTIFICATE_LOAD_MODE=VOLUME
+ENV CERTIFICATE_CERT_ROOT_PATH=/app/certs
+
+# 暴露端口
+EXPOSE 8080
+
+# 健康检查
+HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
+ CMD wget --no-verbose --tries=1 --spider http://localhost:8080/actuator/health || exit 1
+
+# 启动应用
+ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar app.jar"]
+
+# 证书挂载点说明
+# 在运行容器时,需要将证书目录挂载到 /app/certs
+# 例如:docker run -v /host/certs:/app/certs your-image
+#
+# 证书目录结构应该如下:
+# /app/certs/
+# ├── wechat/
+# │ ├── apiclient_key.pem
+# │ ├── apiclient_cert.pem
+# │ └── wechatpay_cert.pem
+# └── alipay/
+# ├── app_private_key.pem
+# ├── appCertPublicKey.crt
+# ├── alipayCertPublicKey.crt
+# └── alipayRootCert.crt
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..3dbc925
--- /dev/null
+++ b/README.md
@@ -0,0 +1,286 @@
+
+
🚀 WebSoft API
+
基于 Spring Boot + MyBatis Plus 的企业级后端API服务
+
+
+
+
+
+
+
+
+
+
+
+## 📖 项目简介
+
+WebSoft API 是一个基于 **Spring Boot + MyBatis Plus** 构建的现代化企业级后端API服务,采用最新的Java技术栈:
+
+- **核心框架**:Spring Boot 2.5.4 + Spring Security + Spring AOP
+- **数据访问**:MyBatis Plus 3.4.3 + Druid 连接池
+- **数据库**:MySQL + Redis
+- **文档工具**:Swagger 3.0 + Knife4j
+- **工具库**:Hutool、Lombok、FastJSON
+
+
+
+## 项目演示
+| 后台管理系统 | https://mp.websoft.top |
+|--------|-------------------------------------------------------------------------------------------------------------------------------------|
+| 测试账号 | 13800010123,123456
+| 正式账号 | [立即注册](https://mp.websoft.top/register/?inviteCode=github) |
+| 关注公众号 |  |
+
+
+
+
+## 🛠️ 技术栈
+
+### 核心框架
+| 技术 | 版本 | 说明 |
+|------|-------|------|
+| Java | 17+ | 编程语言 |
+| Spring Boot | 2.5.4 | 微服务框架 |
+| Spring Security | 5.5.x | 安全框架 |
+| MyBatis Plus | 3.4.3 | ORM框架 |
+| MySQL | 8.0+ | 关系型数据库 |
+| Redis | 6.0+ | 缓存数据库 |
+| Druid | 1.2.6 | 数据库连接池 |
+
+### 功能组件
+- **Swagger 3.0 + Knife4j** - API文档生成与测试
+- **JWT** - 用户认证与授权
+- **Hutool** - Java工具类库
+- **EasyPOI** - Excel文件处理
+- **阿里云OSS** - 对象存储服务
+- **微信支付/支付宝** - 支付集成
+- **Socket.IO** - 实时通信
+- **MQTT** - 物联网消息传输
+
+## 📋 环境要求
+
+### 基础环境
+- ☕ **Java 1.8+**
+- 🗄️ **MySQL 8.0+**
+- 🔴 **Redis 6.0+**
+- 📦 **Maven 3.6+**
+
+### 开发工具
+- **推荐**:IntelliJ IDEA / Eclipse
+- **插件**:Lombok Plugin、MyBatis Plugin
+
+## 🚀 快速开始
+
+### 1. 克隆项目
+```bash
+git clone https://github.com/websoft-top/mp-java.git
+cd mp-java
+```
+
+### 2. 数据库配置
+```sql
+-- 创建数据库
+CREATE DATABASE websoft_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
+
+-- 导入数据库脚本(如果有的话)
+-- source /path/to/database.sql
+```
+
+### 3. 配置文件
+编辑 `src/main/resources/application-dev.yml` 文件,配置数据库连接:
+```yaml
+spring:
+ datasource:
+ url: jdbc:mysql://localhost:3306/websoft_db?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8
+ username: your_username
+ password: your_password
+ redis:
+ host: localhost
+ port: 6379
+ password: your_redis_password
+```
+
+### 4. 启动项目
+```bash
+# 使用 Maven 启动
+mvn spring-boot:run
+
+# 或者使用 IDE 直接运行 WebSoftApplication.java
+```
+
+访问 `http://localhost:9200` 即可看到API服务。
+
+### 5. API文档
+启动项目后,访问以下地址查看API文档:
+- Swagger UI: `http://localhost:9200/swagger-ui/index.html`
+- Knife4j: `http://localhost:9200/doc.html`
+
+## ⚙️ 配置说明
+
+### 数据库配置
+在 `application-dev.yml` 中配置数据库连接:
+```yaml
+spring:
+ datasource:
+ url: jdbc:mysql://localhost:3306/websoft_db
+ username: root
+ password: your_password
+ driver-class-name: com.mysql.cj.jdbc.Driver
+```
+
+### Redis配置
+```yaml
+spring:
+ redis:
+ host: localhost
+ port: 6379
+ password: your_redis_password
+ database: 0
+```
+
+### 阿里云OSS配置
+```yaml
+config:
+ endpoint: https://oss-cn-shenzhen.aliyuncs.com
+ accessKeyId: your_access_key_id
+ accessKeySecret: your_access_key_secret
+ bucketName: your_bucket_name
+ bucketDomain: https://your-domain.com
+```
+
+### 其他配置
+- **JWT密钥**:`config.token-key` 用于JWT令牌加密
+- **文件上传路径**:`config.upload-path` 本地文件存储路径
+- **邮件服务**:配置SMTP服务器用于发送邮件
+
+## 🎯 核心功能
+
+### 🔐 用户认证与授权
+- **JWT认证**:基于JSON Web Token的用户认证
+- **Spring Security**:完整的安全框架集成
+- **角色权限**:基于RBAC的权限控制
+- **图形验证码**:防止恶意登录
+
+### 📝 内容管理系统(CMS)
+- **文章管理**:支持富文本内容管理
+- **媒体文件**:图片/视频文件上传与管理
+- **分类管理**:内容分类与标签管理
+- **SEO优化**:搜索引擎优化支持
+
+### 🛒 电商系统
+- **商品管理**:商品信息、规格、库存管理
+- **订单系统**:完整的订单流程管理
+- **支付集成**:支持微信支付、支付宝
+- **物流跟踪**:快递100物流查询集成
+
+### 🔧 系统管理
+- **用户管理**:用户信息维护与管理
+- **系统配置**:动态配置管理
+- **日志监控**:系统操作日志记录
+- **数据备份**:数据库备份与恢复
+
+### 📊 数据分析
+- **统计报表**:业务数据统计分析
+- **图表展示**:数据可视化展示
+- **导出功能**:Excel数据导出
+- **实时监控**:系统性能监控
+
+## 🏗️ 项目结构
+
+```
+src/main/java/com/gxwebsoft/
+├── WebSoftApplication.java # 启动类
+├── cms/ # 内容管理模块
+│ ├── controller/ # 控制器层
+│ ├── service/ # 业务逻辑层
+│ ├── mapper/ # 数据访问层
+│ └── entity/ # 实体类
+├── shop/ # 商城模块
+│ ├── controller/
+│ ├── service/
+│ ├── mapper/
+│ └── entity/
+├── common/ # 公共模块
+│ ├── core/ # 核心配置
+│ ├── utils/ # 工具类
+│ └── exception/ # 异常处理
+└── resources/
+ ├── application.yml # 主配置文件
+ ├── application-dev.yml # 开发环境配置
+ └── application-prod.yml# 生产环境配置
+```
+
+## 🔧 开发规范
+
+### 代码结构
+- **Controller层**:处理HTTP请求,参数验证
+- **Service层**:业务逻辑处理,事务管理
+- **Mapper层**:数据访问,SQL映射
+- **Entity层**:数据实体,数据库表映射
+
+### 命名规范
+- **类名**:使用大驼峰命名法(PascalCase)
+- **方法名**:使用小驼峰命名法(camelCase)
+- **常量**:使用全大写,下划线分隔
+- **包名**:使用小写字母,点分隔
+
+## 📚 API文档
+
+项目集成了Swagger和Knife4j,提供完整的API文档:
+
+### 访问地址
+- **Swagger UI**: `http://localhost:9200/swagger-ui/index.html`
+- **Knife4j**: `http://localhost:9200/doc.html`
+
+### 主要接口模块
+- **用户认证**: `/api/auth/**` - 登录、注册、权限验证
+- **用户管理**: `/api/user/**` - 用户CRUD操作
+- **内容管理**: `/api/cms/**` - 文章、媒体文件管理
+- **商城管理**: `/api/shop/**` - 商品、订单管理
+- **系统管理**: `/api/system/**` - 系统配置、日志管理
+
+## 🚀 部署指南
+
+### 开发环境部署
+```bash
+# 1. 启动MySQL和Redis服务
+# 2. 创建数据库并导入初始数据
+# 3. 修改配置文件
+# 4. 启动应用
+mvn spring-boot:run
+```
+
+### 生产环境部署
+```bash
+# 1. 打包应用
+mvn clean package -Dmaven.test.skip=true
+
+# 2. 运行jar包
+java -jar target/com-gxwebsoft-modules-1.5.0.jar --spring.profiles.active=prod
+
+# 3. 使用Docker部署(可选)
+docker build -t websoft-api .
+docker run -d -p 9200:9200 websoft-api
+```
+
+## 🤝 贡献指南
+
+1. Fork 本仓库
+2. 创建特性分支 (`git checkout -b feature/AmazingFeature`)
+3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
+4. 推送到分支 (`git push origin feature/AmazingFeature`)
+5. 打开 Pull Request
+
+## 📄 许可证
+
+本项目采用 MIT 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情
+
+## 📞 联系我们
+
+- 官网:https://websoft.top
+- 邮箱:170083662@qq.top
+- QQ群:479713884
+
+---
+
+⭐ 如果这个项目对您有帮助,请给我们一个星标!
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..ea39557
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,105 @@
+version: '3.8'
+
+services:
+ gxwebsoft-app:
+ build: .
+ container_name: gxwebsoft-server
+ ports:
+ - "8080:8080"
+ environment:
+ - SPRING_PROFILES_ACTIVE=prod
+ - CERTIFICATE_LOAD_MODE=VOLUME
+ - CERTIFICATE_CERT_ROOT_PATH=/app/certs
+ - JAVA_OPTS=-Xms512m -Xmx1024m
+ volumes:
+ # 证书挂载卷 - 将主机的证书目录挂载到容器
+ - ./certs:/app/certs:ro
+ # 日志挂载卷
+ - ./logs:/app/logs
+ # 上传文件挂载卷
+ - ./uploads:/app/uploads
+ networks:
+ - gxwebsoft-network
+ depends_on:
+ - mysql
+ - redis
+ restart: unless-stopped
+ healthcheck:
+ test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/actuator/health"]
+ interval: 30s
+ timeout: 10s
+ retries: 3
+ start_period: 60s
+
+ mysql:
+ image: mysql:8.0
+ container_name: gxwebsoft-mysql
+ environment:
+ MYSQL_ROOT_PASSWORD: root123456
+ MYSQL_DATABASE: gxwebsoft
+ MYSQL_USER: gxwebsoft
+ MYSQL_PASSWORD: gxwebsoft123
+ ports:
+ - "3306:3306"
+ volumes:
+ - mysql_data:/var/lib/mysql
+ - ./mysql/conf:/etc/mysql/conf.d
+ - ./mysql/init:/docker-entrypoint-initdb.d
+ networks:
+ - gxwebsoft-network
+ restart: unless-stopped
+ command: --default-authentication-plugin=mysql_native_password
+
+ redis:
+ image: redis:6.2-alpine
+ container_name: gxwebsoft-redis
+ ports:
+ - "6379:6379"
+ volumes:
+ - redis_data:/data
+ - ./redis/redis.conf:/etc/redis/redis.conf
+ networks:
+ - gxwebsoft-network
+ restart: unless-stopped
+ command: redis-server /etc/redis/redis.conf
+
+networks:
+ gxwebsoft-network:
+ driver: bridge
+
+volumes:
+ mysql_data:
+ driver: local
+ redis_data:
+ driver: local
+
+# 证书目录结构说明
+# 在项目根目录创建 certs 目录,结构如下:
+# ./certs/
+# ├── wechat/
+# │ ├── apiclient_key.pem # 微信支付商户私钥
+# │ ├── apiclient_cert.pem # 微信支付商户证书
+# │ └── wechatpay_cert.pem # 微信支付平台证书
+# └── alipay/
+# ├── app_private_key.pem # 支付宝应用私钥
+# ├── appCertPublicKey.crt # 支付宝应用公钥证书
+# ├── alipayCertPublicKey.crt # 支付宝公钥证书
+# └── alipayRootCert.crt # 支付宝根证书
+#
+# 证书文件权限设置:
+# chmod -R 444 certs/ # 设置证书文件为只读
+# chmod 755 certs/ # 设置目录权限
+# chmod 755 certs/wechat/
+# chmod 755 certs/alipay/
+#
+# 启动命令:
+# docker-compose up -d
+#
+# 查看日志:
+# docker-compose logs -f gxwebsoft-app
+#
+# 停止服务:
+# docker-compose down
+#
+# 重新构建并启动:
+# docker-compose up -d --build
diff --git a/mvnw b/mvnw
new file mode 100755
index 0000000..a16b543
--- /dev/null
+++ b/mvnw
@@ -0,0 +1,310 @@
+#!/bin/sh
+# ----------------------------------------------------------------------------
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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
+#
+# https://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.
+# ----------------------------------------------------------------------------
+
+# ----------------------------------------------------------------------------
+# Maven Start Up Batch script
+#
+# Required ENV vars:
+# ------------------
+# JAVA_HOME - location of a JDK home dir
+#
+# Optional ENV vars
+# -----------------
+# M2_HOME - location of maven2's installed home dir
+# MAVEN_OPTS - parameters passed to the Java VM when running Maven
+# e.g. to debug Maven itself, use
+# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+# ----------------------------------------------------------------------------
+
+if [ -z "$MAVEN_SKIP_RC" ] ; then
+
+ if [ -f /etc/mavenrc ] ; then
+ . /etc/mavenrc
+ fi
+
+ if [ -f "$HOME/.mavenrc" ] ; then
+ . "$HOME/.mavenrc"
+ fi
+
+fi
+
+# OS specific support. $var _must_ be set to either true or false.
+cygwin=false;
+darwin=false;
+mingw=false
+case "`uname`" in
+ CYGWIN*) cygwin=true ;;
+ MINGW*) mingw=true;;
+ Darwin*) darwin=true
+ # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
+ # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
+ if [ -z "$JAVA_HOME" ]; then
+ if [ -x "/usr/libexec/java_home" ]; then
+ export JAVA_HOME="`/usr/libexec/java_home`"
+ else
+ export JAVA_HOME="/Library/Java/Home"
+ fi
+ fi
+ ;;
+esac
+
+if [ -z "$JAVA_HOME" ] ; then
+ if [ -r /etc/gentoo-release ] ; then
+ JAVA_HOME=`java-config --jre-home`
+ fi
+fi
+
+if [ -z "$M2_HOME" ] ; then
+ ## resolve links - $0 may be a link to maven's home
+ PRG="$0"
+
+ # need this for relative symlinks
+ while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG="`dirname "$PRG"`/$link"
+ fi
+ done
+
+ saveddir=`pwd`
+
+ M2_HOME=`dirname "$PRG"`/..
+
+ # make it fully qualified
+ M2_HOME=`cd "$M2_HOME" && pwd`
+
+ cd "$saveddir"
+ # echo Using m2 at $M2_HOME
+fi
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched
+if $cygwin ; then
+ [ -n "$M2_HOME" ] &&
+ M2_HOME=`cygpath --unix "$M2_HOME"`
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
+fi
+
+# For Mingw, ensure paths are in UNIX format before anything is touched
+if $mingw ; then
+ [ -n "$M2_HOME" ] &&
+ M2_HOME="`(cd "$M2_HOME"; pwd)`"
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
+fi
+
+if [ -z "$JAVA_HOME" ]; then
+ javaExecutable="`which javac`"
+ if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
+ # readlink(1) is not available as standard on Solaris 10.
+ readLink=`which readlink`
+ if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
+ if $darwin ; then
+ javaHome="`dirname \"$javaExecutable\"`"
+ javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
+ else
+ javaExecutable="`readlink -f \"$javaExecutable\"`"
+ fi
+ javaHome="`dirname \"$javaExecutable\"`"
+ javaHome=`expr "$javaHome" : '\(.*\)/bin'`
+ JAVA_HOME="$javaHome"
+ export JAVA_HOME
+ fi
+ fi
+fi
+
+if [ -z "$JAVACMD" ] ; then
+ if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ else
+ JAVACMD="`which java`"
+ fi
+fi
+
+if [ ! -x "$JAVACMD" ] ; then
+ echo "Error: JAVA_HOME is not defined correctly." >&2
+ echo " We cannot execute $JAVACMD" >&2
+ exit 1
+fi
+
+if [ -z "$JAVA_HOME" ] ; then
+ echo "Warning: JAVA_HOME environment variable is not set."
+fi
+
+CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
+
+# traverses directory structure from process work directory to filesystem root
+# first directory with .mvn subdirectory is considered project base directory
+find_maven_basedir() {
+
+ if [ -z "$1" ]
+ then
+ echo "Path not specified to find_maven_basedir"
+ return 1
+ fi
+
+ basedir="$1"
+ wdir="$1"
+ while [ "$wdir" != '/' ] ; do
+ if [ -d "$wdir"/.mvn ] ; then
+ basedir=$wdir
+ break
+ fi
+ # workaround for JBEAP-8937 (on Solaris 10/Sparc)
+ if [ -d "${wdir}" ]; then
+ wdir=`cd "$wdir/.."; pwd`
+ fi
+ # end of workaround
+ done
+ echo "${basedir}"
+}
+
+# concatenates all lines of a file
+concat_lines() {
+ if [ -f "$1" ]; then
+ echo "$(tr -s '\n' ' ' < "$1")"
+ fi
+}
+
+BASE_DIR=`find_maven_basedir "$(pwd)"`
+if [ -z "$BASE_DIR" ]; then
+ exit 1;
+fi
+
+##########################################################################################
+# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+# This allows using the maven wrapper in projects that prohibit checking in binary data.
+##########################################################################################
+if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Found .mvn/wrapper/maven-wrapper.jar"
+ fi
+else
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
+ fi
+ if [ -n "$MVNW_REPOURL" ]; then
+ jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+ else
+ jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+ fi
+ while IFS="=" read key value; do
+ case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
+ esac
+ done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Downloading from: $jarUrl"
+ fi
+ wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
+ if $cygwin; then
+ wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"`
+ fi
+
+ if command -v wget > /dev/null; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Found wget ... using wget"
+ fi
+ if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+ wget "$jarUrl" -O "$wrapperJarPath"
+ else
+ wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath"
+ fi
+ elif command -v curl > /dev/null; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Found curl ... using curl"
+ fi
+ if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+ curl -o "$wrapperJarPath" "$jarUrl" -f
+ else
+ curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f
+ fi
+
+ else
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Falling back to using Java to download"
+ fi
+ javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
+ # For Cygwin, switch paths to Windows format before running javac
+ if $cygwin; then
+ javaClass=`cygpath --path --windows "$javaClass"`
+ fi
+ if [ -e "$javaClass" ]; then
+ if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo " - Compiling MavenWrapperDownloader.java ..."
+ fi
+ # Compiling the Java class
+ ("$JAVA_HOME/bin/javac" "$javaClass")
+ fi
+ if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
+ # Running the downloader
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo " - Running MavenWrapperDownloader.java ..."
+ fi
+ ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
+ fi
+ fi
+ fi
+fi
+##########################################################################################
+# End of extension
+##########################################################################################
+
+export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
+if [ "$MVNW_VERBOSE" = true ]; then
+ echo $MAVEN_PROJECTBASEDIR
+fi
+MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin; then
+ [ -n "$M2_HOME" ] &&
+ M2_HOME=`cygpath --path --windows "$M2_HOME"`
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
+ [ -n "$MAVEN_PROJECTBASEDIR" ] &&
+ MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
+fi
+
+# Provide a "standardized" way to retrieve the CLI args that will
+# work with both Windows and non-Windows executions.
+MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@"
+export MAVEN_CMD_LINE_ARGS
+
+WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+exec "$JAVACMD" \
+ $MAVEN_OPTS \
+ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
+ "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
+ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
diff --git a/mvnw.cmd b/mvnw.cmd
new file mode 100644
index 0000000..c8d4337
--- /dev/null
+++ b/mvnw.cmd
@@ -0,0 +1,182 @@
+@REM ----------------------------------------------------------------------------
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements. See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership. The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License. You may obtain a copy of the License at
+@REM
+@REM https://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied. See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM ----------------------------------------------------------------------------
+
+@REM ----------------------------------------------------------------------------
+@REM Maven Start Up Batch script
+@REM
+@REM Required ENV vars:
+@REM JAVA_HOME - location of a JDK home dir
+@REM
+@REM Optional ENV vars
+@REM M2_HOME - location of maven2's installed home dir
+@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
+@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
+@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
+@REM e.g. to debug Maven itself, use
+@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+@REM ----------------------------------------------------------------------------
+
+@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
+@echo off
+@REM set title of command window
+title %0
+@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
+@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
+
+@REM set %HOME% to equivalent of $HOME
+if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
+
+@REM Execute a user defined script before this one
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
+@REM check for pre script, once with legacy .bat ending and once with .cmd ending
+if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
+if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd"
+:skipRcPre
+
+@setlocal
+
+set ERROR_CODE=0
+
+@REM To isolate internal variables from possible post scripts, we use another setlocal
+@setlocal
+
+@REM ==== START VALIDATION ====
+if not "%JAVA_HOME%" == "" goto OkJHome
+
+echo.
+echo Error: JAVA_HOME not found in your environment. >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+:OkJHome
+if exist "%JAVA_HOME%\bin\java.exe" goto init
+
+echo.
+echo Error: JAVA_HOME is set to an invalid directory. >&2
+echo JAVA_HOME = "%JAVA_HOME%" >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+@REM ==== END VALIDATION ====
+
+:init
+
+@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
+@REM Fallback to current working directory if not found.
+
+set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
+IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
+
+set EXEC_DIR=%CD%
+set WDIR=%EXEC_DIR%
+:findBaseDir
+IF EXIST "%WDIR%"\.mvn goto baseDirFound
+cd ..
+IF "%WDIR%"=="%CD%" goto baseDirNotFound
+set WDIR=%CD%
+goto findBaseDir
+
+:baseDirFound
+set MAVEN_PROJECTBASEDIR=%WDIR%
+cd "%EXEC_DIR%"
+goto endDetectBaseDir
+
+:baseDirNotFound
+set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
+cd "%EXEC_DIR%"
+
+:endDetectBaseDir
+
+IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
+
+@setlocal EnableExtensions EnableDelayedExpansion
+for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
+@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
+
+:endReadAdditionalConfig
+
+SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
+set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
+set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+
+FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
+ IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
+)
+
+@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
+if exist %WRAPPER_JAR% (
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Found %WRAPPER_JAR%
+ )
+) else (
+ if not "%MVNW_REPOURL%" == "" (
+ SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+ )
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Couldn't find %WRAPPER_JAR%, downloading it ...
+ echo Downloading from: %DOWNLOAD_URL%
+ )
+
+ powershell -Command "&{"^
+ "$webclient = new-object System.Net.WebClient;"^
+ "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
+ "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
+ "}"^
+ "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^
+ "}"
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Finished downloading %WRAPPER_JAR%
+ )
+)
+@REM End of extension
+
+@REM Provide a "standardized" way to retrieve the CLI args that will
+@REM work with both Windows and non-Windows executions.
+set MAVEN_CMD_LINE_ARGS=%*
+
+%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
+if ERRORLEVEL 1 goto error
+goto end
+
+:error
+set ERROR_CODE=1
+
+:end
+@endlocal & set ERROR_CODE=%ERROR_CODE%
+
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost
+@REM check for post script, once with legacy .bat ending and once with .cmd ending
+if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
+if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd"
+:skipRcPost
+
+@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
+if "%MAVEN_BATCH_PAUSE%" == "on" pause
+
+if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE%
+
+exit /B %ERROR_CODE%
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..77b3ea5
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,393 @@
+
+
+ 4.0.0
+
+ com.gxwebsoft
+ server-api
+ 1.0
+
+ server-api
+ WebSoftApi project for Spring Boot
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+
+ 2.5.15
+
+
+
+
+ 17
+ 17
+ 17
+ UTF-8
+ UTF-8
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-devtools
+ runtime
+ true
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+
+ com.fasterxml.jackson.datatype
+ jackson-datatype-jsr310
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-aop
+
+
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+
+ com.mysql
+ mysql-connector-j
+ 8.2.0
+ runtime
+
+
+
+
+ com.alibaba
+ druid-spring-boot-starter
+ 1.2.20
+
+
+
+
+ com.baomidou
+ mybatis-plus-boot-starter
+ 3.4.3.3
+
+
+
+
+ com.github.yulichang
+ mybatis-plus-join-boot-starter
+ 1.4.5
+
+
+
+
+ com.baomidou
+ mybatis-plus-generator
+ 3.4.1
+
+
+
+
+ cn.hutool
+ hutool-core
+ 5.8.25
+
+
+ cn.hutool
+ hutool-extra
+ 5.8.25
+
+
+ cn.hutool
+ hutool-http
+ 5.8.25
+
+
+ cn.hutool
+ hutool-crypto
+ 5.8.25
+
+
+
+
+ cn.afterturn
+ easypoi-base
+ 4.4.0
+
+
+
+
+ org.apache.tika
+ tika-core
+ 2.9.1
+
+
+
+
+ com.github.livesense
+ jodconverter-core
+ 1.0.5
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-mail
+
+
+
+
+ com.ibeetl
+ beetl
+ 3.15.10.RELEASE
+
+
+
+
+ io.springfox
+ springfox-boot-starter
+ 3.0.0
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+
+
+ io.jsonwebtoken
+ jjwt-api
+ 0.11.5
+
+
+ io.jsonwebtoken
+ jjwt-impl
+ 0.11.5
+ runtime
+
+
+ io.jsonwebtoken
+ jjwt-jackson
+ 0.11.5
+ runtime
+
+
+
+
+ com.github.whvcse
+ easy-captcha
+ 1.6.2
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-redis
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+
+
+ com.aliyun
+ aliyun-java-sdk-core
+ 4.4.3
+
+
+
+ com.alipay.sdk
+ alipay-sdk-java
+ 4.35.0.ALL
+
+
+
+ org.bouncycastle
+ bcprov-jdk18on
+ 1.77
+
+
+
+ commons-logging
+ commons-logging
+ 1.3.0
+
+
+
+ com.alibaba
+ fastjson
+ 2.0.43
+
+
+
+
+ com.google.zxing
+ core
+ 3.5.2
+
+
+
+
+ com.google.code.gson
+ gson
+ 2.10.1
+
+
+
+ com.vaadin.external.google
+ android-json
+ 0.0.20131108.vaadin1
+ compile
+
+
+
+
+ com.corundumstudio.socketio
+ netty-socketio
+ 2.0.3
+
+
+
+
+ com.github.wechatpay-apiv3
+ wechatpay-java
+ 0.2.17
+
+
+
+
+ com.github.binarywang
+ weixin-java-miniapp
+ 4.6.0
+
+
+
+
+ com.aliyun.oss
+ aliyun-sdk-oss
+ 3.17.4
+
+
+
+
+ com.aliyun
+ green20220302
+ 1.0.8
+
+
+
+ org.springframework.boot
+ spring-boot-starter-freemarker
+
+
+
+
+ com.getui.push
+ restful-sdk
+ 1.0.0.14
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-amqp
+
+
+
+
+ javax.annotation
+ javax.annotation-api
+ 1.3.2
+
+
+
+
+ com.github.xiaoymin
+ knife4j-spring-boot-starter
+ 3.0.3
+
+
+
+
+
+
+
+ src/main/java
+
+ **/*Mapper.xml
+
+
+
+ src/main/resources
+
+ **
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+ 2.5.15
+
+
+
+ org.projectlombok
+ lombok
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+ 17
+ 17
+
+
+
+
+
+
+
+ aliYunMaven
+ https://maven.aliyun.com/repository/public
+
+
+ com.e-iceblue
+ e-iceblue
+ https://repo.e-iceblue.cn/repository/maven-public/
+
+
+
+
diff --git a/src/main/java/com/gxwebsoft/WebSoftApplication.java b/src/main/java/com/gxwebsoft/WebSoftApplication.java
new file mode 100644
index 0000000..80c9a42
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/WebSoftApplication.java
@@ -0,0 +1,28 @@
+package com.gxwebsoft;
+
+import com.gxwebsoft.common.core.config.ConfigProperties;
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.scheduling.annotation.EnableAsync;
+import org.springframework.scheduling.annotation.EnableScheduling;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+
+/**
+ * 启动类
+ * Created by WebSoft on 2018-02-22 11:29:03
+ */
+@EnableAsync
+@EnableTransactionManagement
+@MapperScan("com.gxwebsoft.**.mapper")
+@EnableConfigurationProperties(ConfigProperties.class)
+@SpringBootApplication
+@EnableScheduling
+public class WebSoftApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(WebSoftApplication.class, args);
+ }
+
+}
diff --git a/src/main/java/com/gxwebsoft/auto/controller/QrLoginController.java b/src/main/java/com/gxwebsoft/auto/controller/QrLoginController.java
new file mode 100644
index 0000000..9535c80
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/auto/controller/QrLoginController.java
@@ -0,0 +1,147 @@
+package com.gxwebsoft.auto.controller;
+
+import com.gxwebsoft.auto.dto.QrLoginBindPhoneRequest;
+import com.gxwebsoft.auto.dto.QrLoginConfirmRequest;
+import com.gxwebsoft.auto.dto.QrLoginGenerateResponse;
+import com.gxwebsoft.auto.dto.QrLoginStatusResponse;
+import com.gxwebsoft.auto.dto.WechatScanRequest;
+import com.gxwebsoft.auto.dto.WechatScanResponse;
+import com.gxwebsoft.auto.service.QrLoginService;
+import com.gxwebsoft.common.core.web.BaseController;
+import com.gxwebsoft.common.core.web.ApiResult;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import javax.validation.Valid;
+
+/**
+ * 认证模块
+ *
+ * @author 科技小王子
+ * @since 2025-03-06 22:50:25
+ */
+@Tag(name = "认证模块")
+@RestController
+@RequestMapping("/api/qr-login")
+public class QrLoginController extends BaseController {
+
+ @Autowired
+ private QrLoginService qrLoginService;
+
+ @Autowired
+ private com.gxwebsoft.common.system.service.WxService wxService;
+
+ @Autowired
+ private javax.servlet.http.HttpServletRequest request;
+
+ /**
+ * 生成扫码登录token
+ */
+ @Operation(summary = "生成扫码登录token")
+ @PostMapping("/generate")
+ public ApiResult> generateQrLoginToken() {
+ try {
+ QrLoginGenerateResponse response = qrLoginService.generateQrLoginToken(getTenantId());
+ return success("生成成功", response);
+ } catch (Exception e) {
+ return fail(e.getMessage());
+ }
+ }
+
+ /**
+ * 检查扫码登录状态
+ */
+ @Operation(summary = "检查扫码登录状态")
+ @GetMapping("/status/{token}")
+ public ApiResult> checkQrLoginStatus(
+ @Parameter(description = "扫码登录token") @PathVariable String token) {
+ try {
+ QrLoginStatusResponse response = qrLoginService.checkQrLoginStatus(token);
+ return success("查询成功", response);
+ } catch (Exception e) {
+ return fail(e.getMessage());
+ }
+ }
+
+ /**
+ * 确认扫码登录
+ */
+ @Operation(summary = "确认扫码登录")
+ @PostMapping("/confirm")
+ public ApiResult> confirmQrLogin(@Valid @RequestBody QrLoginConfirmRequest request) {
+ try {
+ QrLoginStatusResponse response = qrLoginService.confirmQrLogin(request);
+ return success("确认成功", response);
+ } catch (Exception e) {
+ return fail(e.getMessage());
+ }
+ }
+
+ /**
+ * 扫码操作(可选接口,用于移动端扫码后更新状态)
+ */
+ @Operation(summary = "扫码操作")
+ @PostMapping("/scan/{token}")
+ public ApiResult> scanQrCode(@Parameter(description = "扫码登录token") @PathVariable String token) {
+ try {
+ boolean result = qrLoginService.scanQrCode(token);
+ return success("操作成功", result);
+ } catch (Exception e) {
+ return fail(e.getMessage());
+ }
+ }
+
+ /**
+ * 公众号关注注册后绑定手机号
+ */
+ @Operation(summary = "绑定手机号并完成扫码登录")
+ @PostMapping("/bind-phone")
+ public ApiResult> bindPhone(@Valid @RequestBody QrLoginBindPhoneRequest request) {
+ try {
+ QrLoginStatusResponse response = qrLoginService.bindPhone(request);
+ return success("绑定成功", response);
+ } catch (Exception e) {
+ return fail(e.getMessage());
+ }
+ }
+
+ /**
+ * 微信扫码登录确认(H5页面调用)
+ */
+ @Operation(summary = "微信扫码登录确认")
+ @PostMapping("/wechat-scan")
+ public ApiResult> wechatScanConfirm(@Valid @RequestBody WechatScanRequest request) {
+ try {
+ WechatScanResponse response = qrLoginService.wechatScanConfirm(request);
+ return success("操作成功", response);
+ } catch (Exception e) {
+ return fail(e.getMessage());
+ }
+ }
+
+ /**
+ * 获取微信网页授权 URL(用于 H5 扫码页面重定向)
+ */
+ @Operation(summary = "获取微信网页授权URL")
+ @GetMapping("/wechat-oauth-url")
+ public ApiResult> getWechatOAuthUrl(@Parameter(description = "扫码登录token") @RequestParam String token) {
+ try {
+ String appId = wxService.getOfficialAppId(getTenantId());
+ // 回调地址,指向 H5 扫码确认页面
+ String redirectUri = java.net.URLEncoder.encode(
+ "https://" + request.getHeader("Host") + "/wx-scan?token=" + token,
+ java.nio.charset.StandardCharsets.UTF_8);
+ // 构造微信 OAuth 授权 URL
+ String oauthUrl = String.format(
+ "https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=snsapi_userinfo&state=%s#wechat_redirect",
+ appId, redirectUri, token);
+ return success("获取成功", oauthUrl);
+ } catch (Exception e) {
+ return fail(e.getMessage());
+ }
+ }
+
+}
diff --git a/src/main/java/com/gxwebsoft/auto/dto/QrLoginBindPhoneRequest.java b/src/main/java/com/gxwebsoft/auto/dto/QrLoginBindPhoneRequest.java
new file mode 100644
index 0000000..ccdfb40
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/auto/dto/QrLoginBindPhoneRequest.java
@@ -0,0 +1,30 @@
+package com.gxwebsoft.auto.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+
+/**
+ * 扫码登录绑定手机号请求
+ *
+ * @author 科技小王子
+ * @since 2026-04-06
+ */
+@Data
+@Schema(description = "扫码登录绑定手机号请求")
+public class QrLoginBindPhoneRequest {
+
+ @Schema(description = "扫码登录token")
+ @NotBlank(message = "token不能为空")
+ private String token;
+
+ @Schema(description = "手机号")
+ @NotBlank(message = "手机号不能为空")
+ private String phone;
+
+ @Schema(description = "短信验证码")
+ @NotBlank(message = "验证码不能为空")
+ private String code;
+
+}
diff --git a/src/main/java/com/gxwebsoft/auto/dto/QrLoginConfirmRequest.java b/src/main/java/com/gxwebsoft/auto/dto/QrLoginConfirmRequest.java
new file mode 100644
index 0000000..73244f0
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/auto/dto/QrLoginConfirmRequest.java
@@ -0,0 +1,25 @@
+package com.gxwebsoft.auto.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+
+/**
+ * 扫码登录确认请求
+ *
+ * @author 科技小王子
+ * @since 2025-08-31
+ */
+@Data
+@Schema(description = "扫码登录确认请求")
+public class QrLoginConfirmRequest {
+
+ @Schema(description = "扫码登录token")
+ @NotBlank(message = "token不能为空")
+ private String token;
+
+ @Schema(description = "用户ID")
+ private Integer userId;
+
+}
diff --git a/src/main/java/com/gxwebsoft/auto/dto/QrLoginData.java b/src/main/java/com/gxwebsoft/auto/dto/QrLoginData.java
new file mode 100644
index 0000000..69b131b
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/auto/dto/QrLoginData.java
@@ -0,0 +1,68 @@
+package com.gxwebsoft.auto.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 扫码登录数据模型
+ *
+ * @author 科技小王子
+ * @since 2025-08-31
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class QrLoginData {
+
+ /**
+ * 扫码登录token
+ */
+ private String token;
+
+ /**
+ * 状态: pending-等待扫码, scanned-已扫码, confirmed-已确认, bind_phone-待绑定手机号, expired-已过期
+ */
+ private String status;
+
+ /**
+ * 用户ID(扫码确认后设置)
+ */
+ private Integer userId;
+
+ /**
+ * 用户名(扫码确认后设置)
+ */
+ private String username;
+
+ /**
+ * 创建时间
+ */
+ private String createTime;
+
+ /**
+ * 过期时间
+ */
+ private String expireTime;
+
+ /**
+ * JWT访问令牌(确认后生成)
+ */
+ private String accessToken;
+
+ /**
+ * 租户ID
+ */
+ private Integer tenantId;
+
+ /**
+ * 是否需要绑定手机号
+ */
+ private Boolean needBindPhone;
+
+ /**
+ * 状态提示信息
+ */
+ private String message;
+
+}
diff --git a/src/main/java/com/gxwebsoft/auto/dto/QrLoginGenerateResponse.java b/src/main/java/com/gxwebsoft/auto/dto/QrLoginGenerateResponse.java
new file mode 100644
index 0000000..5837631
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/auto/dto/QrLoginGenerateResponse.java
@@ -0,0 +1,54 @@
+package com.gxwebsoft.auto.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 扫码登录生成响应
+ *
+ * @author 科技小王子
+ * @since 2025-08-31
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Schema(description = "扫码登录生成响应")
+public class QrLoginGenerateResponse {
+
+ @Schema(description = "扫码登录token")
+ private String token;
+
+ @Schema(description = "二维码内容(APP扫码使用)")
+ private String qrCodeContent;
+
+ @Schema(description = "微信小程序页面路径")
+ private String miniprogramPath;
+
+ @Schema(description = "微信小程序码图片URL(已废弃,改用base64)")
+ private String miniprogramQrCodeUrl;
+
+ @Schema(description = "微信小程序码图片Base64(扫码后直接打开小程序,优先使用)")
+ private String miniprogramQrCode;
+
+ @Schema(description = "过期时间(秒)")
+ private Long expiresIn;
+
+ @Schema(description = "微信扫码登录H5页面URL")
+ private String wechatScanUrl;
+
+ @Schema(description = "微信公众号AppID")
+ private String wechatAppId;
+
+ @Schema(description = "微信公众号带参数二维码图片URL")
+ private String wechatQrCodeUrl;
+
+ // 保持向后兼容的构造函数
+ public QrLoginGenerateResponse(String token, String qrCodeContent, Long expiresIn) {
+ this.token = token;
+ this.qrCodeContent = qrCodeContent;
+ this.expiresIn = expiresIn;
+ }
+
+}
diff --git a/src/main/java/com/gxwebsoft/auto/dto/QrLoginStatusResponse.java b/src/main/java/com/gxwebsoft/auto/dto/QrLoginStatusResponse.java
new file mode 100644
index 0000000..b833615
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/auto/dto/QrLoginStatusResponse.java
@@ -0,0 +1,57 @@
+package com.gxwebsoft.auto.dto;
+
+import com.gxwebsoft.common.system.entity.User;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 扫码登录状态响应
+ *
+ * @author 科技小王子
+ * @since 2025-08-31
+ */
+@Data
+@NoArgsConstructor
+@Schema(description = "扫码登录状态响应")
+public class QrLoginStatusResponse {
+
+ @Schema(description = "状态: pending-等待扫码, scanned-已扫码, confirmed-已确认, bind_phone-待绑定手机号, expired-已过期")
+ private String status;
+
+ @Schema(description = "JWT访问令牌(仅在confirmed状态时返回)")
+ private String accessToken;
+
+ @Schema(description = "用户信息")
+ private User userInfo;
+
+ @Schema(description = "剩余过期时间(秒)")
+ private Long expiresIn;
+
+ @Schema(description = "租户ID")
+ private Integer tenantId;
+
+ @Schema(description = "是否需要绑定手机号")
+ private Boolean needBindPhone;
+
+ @Schema(description = "状态提示信息")
+ private String message;
+
+ @Schema(description = "下一步操作:bind_phone-绑定手机号, redirect-跳转, login-直接登录")
+ private String nextAction;
+
+ @Schema(description = "跳转URL(当nextAction为redirect时使用)")
+ private String redirectUrl;
+
+ @Schema(description = "成功消息")
+ private String successMessage;
+
+ public QrLoginStatusResponse(String status, String accessToken, User userInfo, Long expiresIn, Integer tenantId) {
+ this.status = status;
+ this.accessToken = accessToken;
+ this.userInfo = userInfo;
+ this.expiresIn = expiresIn;
+ this.tenantId = tenantId;
+ }
+
+}
diff --git a/src/main/java/com/gxwebsoft/auto/dto/WechatScanRequest.java b/src/main/java/com/gxwebsoft/auto/dto/WechatScanRequest.java
new file mode 100644
index 0000000..b8cc295
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/auto/dto/WechatScanRequest.java
@@ -0,0 +1,31 @@
+package com.gxwebsoft.auto.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+
+/**
+ * 微信扫码登录请求(用于 H5 页面回调)
+ *
+ * @author 科技小王子
+ * @since 2026-04-06
+ */
+@Data
+@Schema(description = "微信扫码登录请求")
+public class WechatScanRequest {
+
+ @Schema(description = "扫码登录token")
+ @NotBlank(message = "token不能为空")
+ private String token;
+
+ @Schema(description = "微信公众号授权code")
+ private String code;
+
+ @Schema(description = "微信unionId(如果已获取)")
+ private String unionId;
+
+ @Schema(description = "微信openId")
+ private String openId;
+
+}
diff --git a/src/main/java/com/gxwebsoft/auto/dto/WechatScanResponse.java b/src/main/java/com/gxwebsoft/auto/dto/WechatScanResponse.java
new file mode 100644
index 0000000..36a21ac
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/auto/dto/WechatScanResponse.java
@@ -0,0 +1,47 @@
+package com.gxwebsoft.auto.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 微信扫码登录响应
+ *
+ * @author 科技小王子
+ * @since 2026-04-06
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@Schema(description = "微信扫码登录响应")
+public class WechatScanResponse {
+
+ @Schema(description = "状态:success-登录成功,bind_required-需要绑定账号,not_bound-账号未绑定")
+ private String status;
+
+ @Schema(description = "JWT访问令牌")
+ private String accessToken;
+
+ @Schema(description = "用户信息")
+ private Object userInfo;
+
+ @Schema(description = "提示信息")
+ private String message;
+
+ @Schema(description = "租户ID")
+ private Integer tenantId;
+
+ public static WechatScanResponse success(String accessToken, Object userInfo, Integer tenantId) {
+ return new WechatScanResponse("success", accessToken, userInfo, "登录成功", tenantId);
+ }
+
+ public static WechatScanResponse needBind(String message) {
+ return new WechatScanResponse("bind_required", null, null, message, null);
+ }
+
+ public static WechatScanResponse notBound(String message) {
+ return new WechatScanResponse("not_bound", null, null, message, null);
+ }
+
+}
diff --git a/src/main/java/com/gxwebsoft/auto/service/QrLoginService.java b/src/main/java/com/gxwebsoft/auto/service/QrLoginService.java
new file mode 100644
index 0000000..538bed5
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/auto/service/QrLoginService.java
@@ -0,0 +1,65 @@
+package com.gxwebsoft.auto.service;
+
+import com.gxwebsoft.auto.dto.QrLoginBindPhoneRequest;
+import com.gxwebsoft.auto.dto.QrLoginConfirmRequest;
+import com.gxwebsoft.auto.dto.QrLoginGenerateResponse;
+import com.gxwebsoft.auto.dto.QrLoginStatusResponse;
+import com.gxwebsoft.auto.dto.WechatScanRequest;
+import com.gxwebsoft.auto.dto.WechatScanResponse;
+
+/**
+ * 扫码登录服务接口
+ *
+ * @author 科技小王子
+ * @since 2025-08-31
+ */
+public interface QrLoginService {
+
+ /**
+ * 生成扫码登录token
+ *
+ * @return QrLoginGenerateResponse
+ */
+ QrLoginGenerateResponse generateQrLoginToken(Integer tenantId);
+
+ /**
+ * 检查扫码登录状态
+ *
+ * @param token 扫码登录token
+ * @return QrLoginStatusResponse
+ */
+ QrLoginStatusResponse checkQrLoginStatus(String token);
+
+ /**
+ * 确认扫码登录
+ *
+ * @param request 确认请求
+ * @return QrLoginStatusResponse
+ */
+ QrLoginStatusResponse confirmQrLogin(QrLoginConfirmRequest request);
+
+ /**
+ * 扫码操作(更新状态为已扫码)
+ *
+ * @param token 扫码登录token
+ * @return boolean
+ */
+ boolean scanQrCode(String token);
+
+ /**
+ * 关注后绑定手机号并完成登录
+ *
+ * @param request 绑定手机号请求
+ * @return QrLoginStatusResponse
+ */
+ QrLoginStatusResponse bindPhone(QrLoginBindPhoneRequest request);
+
+ /**
+ * 微信扫码登录确认(H5页面调用)
+ *
+ * @param request 微信扫码登录请求
+ * @return WechatScanResponse
+ */
+ WechatScanResponse wechatScanConfirm(WechatScanRequest request);
+
+}
diff --git a/src/main/java/com/gxwebsoft/auto/service/impl/QrLoginServiceImpl.java b/src/main/java/com/gxwebsoft/auto/service/impl/QrLoginServiceImpl.java
new file mode 100644
index 0000000..4cd3e76
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/auto/service/impl/QrLoginServiceImpl.java
@@ -0,0 +1,645 @@
+package com.gxwebsoft.auto.service.impl;
+
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.lang.UUID;
+import cn.hutool.core.util.DesensitizedUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.http.HttpRequest;
+import cn.hutool.http.HttpUtil;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.gxwebsoft.auto.dto.*;
+import com.gxwebsoft.auto.service.QrLoginService;
+import com.gxwebsoft.common.core.config.ConfigProperties;
+import com.gxwebsoft.common.core.security.JwtSubject;
+import com.gxwebsoft.common.core.security.JwtUtil;
+import com.gxwebsoft.common.core.utils.CommonUtil;
+import com.gxwebsoft.common.core.utils.RedisUtil;
+import com.gxwebsoft.common.mq.producer.SyncMessageProducer;
+import com.gxwebsoft.common.system.entity.User;
+import com.gxwebsoft.common.system.entity.UserOauth;
+import com.gxwebsoft.common.system.service.UserOauthService;
+import com.gxwebsoft.common.system.service.UserService;
+import com.gxwebsoft.common.system.service.WxService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.concurrent.TimeUnit;
+
+import static com.gxwebsoft.common.core.constants.PlatformConstants.MP_OFFICIAL;
+import static com.gxwebsoft.common.core.constants.RedisConstants.*;
+import static com.gxwebsoft.common.core.constants.WebsiteConstants.CACHE_KEY_VERIFICATION_CODE_BY_DEV_SMS;
+
+/**
+ * 扫码登录服务实现
+ *
+ * @author 科技小王子
+ * @since 2025-08-31
+ */
+@Slf4j
+@Service
+public class QrLoginServiceImpl implements QrLoginService {
+
+ @Autowired
+ private RedisUtil redisUtil;
+
+ @Autowired
+ private UserService userService;
+
+ @Autowired
+ private ConfigProperties configProperties;
+
+ @Autowired
+ private WxService wxService;
+
+ @Autowired(required = false)
+ private UserOauthService userOauthService;
+
+ @Autowired(required = false)
+ private SyncMessageProducer syncMessageProducer;
+
+ @Override
+ public QrLoginGenerateResponse generateQrLoginToken(Integer tenantId) {
+ String token = UUID.randomUUID().toString(true);
+
+ QrLoginData qrLoginData = new QrLoginData();
+ qrLoginData.setToken(token);
+ qrLoginData.setStatus(QR_LOGIN_STATUS_PENDING);
+ qrLoginData.setTenantId(tenantId);
+ qrLoginData.setNeedBindPhone(false);
+ qrLoginData.setMessage("等待微信扫码");
+ qrLoginData.setCreateTime(DateUtil.formatDateTime(DateUtil.date()));
+ qrLoginData.setExpireTime(DateUtil.formatDateTime(DateUtil.offsetSecond(DateUtil.date(), QR_LOGIN_TOKEN_TTL.intValue())));
+
+ String redisKey = QR_LOGIN_TOKEN_KEY + token;
+ redisUtil.set(redisKey, qrLoginData, QR_LOGIN_TOKEN_TTL, TimeUnit.SECONDS);
+
+ log.info("生成扫码登录token: {}", token);
+
+ QrLoginGenerateResponse response = new QrLoginGenerateResponse();
+ response.setToken(token);
+ response.setExpiresIn(QR_LOGIN_TOKEN_TTL);
+ // 二维码内容:使用自定义协议,前端据此生成base64二维码
+ response.setQrCodeContent("websopy://login?token=" + token);
+ // 小程序路径(用于小程序扫码直接打开)
+ response.setMiniprogramPath("/pages/qr-login?token=" + token);
+
+ // 扫码跳转URL(前端生成二维码时使用此URL)
+ try {
+ String baseUrl = configProperties.getWechatScanUrl();
+ if (StrUtil.isBlank(baseUrl)) {
+ baseUrl = "https://websopy.websoft.top";
+ }
+ String wechatScanUrl = baseUrl + "/wx-scan?token=" + token;
+ response.setWechatScanUrl(wechatScanUrl);
+ log.info("扫码跳转URL: {}", wechatScanUrl);
+ } catch (Exception e) {
+ log.warn("获取扫码跳转URL失败: {}", e.getMessage());
+ // 降级:使用默认域名
+ response.setWechatScanUrl("https://websopy.websoft.top/wx-scan?token=" + token);
+ }
+
+ // 生成小程序码(通过微信API生成小程序码,返回Base64图片,扫码后直接打开小程序确认页面)
+ try {
+ String miniprogramQrCodeBase64 = generateMiniprogramQrCode(token, tenantId);
+ if (StrUtil.isNotBlank(miniprogramQrCodeBase64)) {
+ response.setMiniprogramQrCode(miniprogramQrCodeBase64);
+ log.info("生成小程序码成功(Base64,长度: {})", miniprogramQrCodeBase64.length());
+ }
+ } catch (Exception e) {
+ log.error("生成小程序码失败: {}", e.getMessage(), e);
+ // 生成失败不影响主流程,继续使用H5方式
+ }
+
+ return response;
+ }
+
+ /**
+ * 生成小程序码(用于PC端扫码登录)
+ * 调用微信API生成无限制小程序码,返回Base64图片,扫码后直接打开小程序确认页面
+ * 具备自动重试机制:首次失败后清理缓存并重试一次
+ *
+ * @param token 扫码登录token
+ * @param tenantId 租户ID
+ * @return 小程序码图片Base64字符串
+ */
+ private String generateMiniprogramQrCode(String token, Integer tenantId) {
+ // 构建 access_token 的 Redis key(与 WxService 保持一致)
+ String accessTokenKey = "WX_ACCESS_TOKEN:" + (tenantId != null ? tenantId : 10048);
+
+ // 第一次尝试生成
+ String result = doGenerateMiniprogramQrCode(token, tenantId, accessTokenKey, false);
+ if (result != null) {
+ return result;
+ }
+
+ // 第一次失败,清理缓存并重试(确保下次能拿到最新的 access_token)
+ log.info("小程序码首次生成失败,清理缓存后重试...");
+ clearAccessTokenCache(accessTokenKey, tenantId);
+
+ // 第二次尝试生成(强制刷新 token)
+ return doGenerateMiniprogramQrCode(token, tenantId, accessTokenKey, true);
+ }
+
+ /**
+ * 执行小程序码生成
+ *
+ * @param token 扫码登录token
+ * @param tenantId 租户ID
+ * @param accessTokenKey access_token 的 Redis key
+ * @param forceRefresh 是否强制刷新 access_token
+ * @return 小程序码 Base64 字符串,失败返回 null
+ */
+ private String doGenerateMiniprogramQrCode(String token, Integer tenantId, String accessTokenKey, boolean forceRefresh) {
+ try {
+ // 获取小程序access_token
+ String accessToken = forceRefresh
+ ? wxService.getAccessTokenForcibly(tenantId) // 强制从微信获取新token
+ : wxService.getAccessToken(tenantId);
+
+ if (StrUtil.isBlank(accessToken)) {
+ log.warn("获取小程序access_token失败,跳过生成小程序码");
+ return null;
+ }
+
+ // 调用微信API生成小程序码
+ String apiUrl = "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=" + accessToken;
+
+ HashMap params = new HashMap<>();
+ // scene 必须是字符串,最大 32 字符,直接传 token(32位UUID)刚好满足限制
+ // 小程序端通过 router.params.scene 获取此 token
+ params.put("scene", token);
+ params.put("page", "passport/qr-confirm/index"); // 小程序确认页面路径(子包)
+ params.put("env_version", "release"); // release/trial/develop
+ params.put("width", 280); // 二维码宽度
+ params.put("auto_color", false); // 不自动配置颜色
+
+ // 发送请求并获取二进制响应
+ byte[] imageBytes = HttpRequest.post(apiUrl)
+ .body(JSON.toJSONString(params))
+ .timeout(15000)
+ .execute().bodyBytes();
+
+ // 判断是否返回图片(二进制)或错误(JSON)
+ if (imageBytes == null || imageBytes.length == 0) {
+ log.error("生成小程序码API返回空数据");
+ return null;
+ }
+
+ // 检查是否返回JSON错误(微信API错误时会返回JSON)
+ if (imageBytes.length < 100 && new String(imageBytes).startsWith("{")) {
+ JSONObject errorResult = JSON.parseObject(new String(imageBytes));
+ Integer errCode = errorResult.getInteger("errcode");
+ String errMsg = errorResult.getString("errmsg");
+
+ log.error("生成小程序码API返回错误[{}:{}]", errCode, errMsg);
+ return null;
+ }
+
+ // 将图片字节数组转换为Base64字符串
+ String base64Image = cn.hutool.core.codec.Base64.encode(imageBytes);
+ // 添加Data URI前缀,使前端可以直接使用
+ return "data:image/png;base64," + base64Image;
+ } catch (Exception e) {
+ log.error("生成小程序码异常: {}", e.getMessage(), e);
+ return null;
+ }
+ }
+
+ /**
+ * 判断是否是 token 相关的错误码,需要清理缓存
+ * 常见微信 API 错误码:
+ * - 40001: 获取access_token时AppSecret错误
+ * - 40013: appid无效
+ * - 40125: appsecret无效
+ * - 42001: access_token超时
+ * - 42002: refresh_token超时
+ * - 42003: code超时
+ * - 44002: post body太长
+ * - 44003: 图片太大
+ * - 41002: appid不正确
+ * - 41008: 缺少access_token参数
+ */
+ private boolean isTokenRelatedError(Integer errCode, String errMsg) {
+ if (errCode == null) {
+ return false;
+ }
+ // token 相关错误码
+ return errCode == 40001 // AppSecret错误
+ || errCode == 40013 // appid无效
+ || errCode == 40125 // appsecret无效
+ || errCode == 42001 // access_token超时
+ || errCode == 42002 // refresh_token超时
+ || errCode == 42003 // code超时
+ || errCode == 41002 // appid不正确
+ || errCode == 41008 // 缺少access_token参数
+ || errCode == 40014 // 不合法的access_token
+ || errCode == 40097; // invalid page
+ }
+
+ /**
+ * 清理 access_token 缓存
+ */
+ private void clearAccessTokenCache(String accessTokenKey, Integer tenantId) {
+ try {
+ redisUtil.delete(accessTokenKey);
+ log.info("清理微信access_token缓存[{}], tenantId={}", accessTokenKey, tenantId);
+ } catch (Exception e) {
+ log.error("清理access_token缓存失败: {}", e.getMessage());
+ }
+ }
+
+ @Override
+ public QrLoginStatusResponse checkQrLoginStatus(String token) {
+ if (StrUtil.isBlank(token)) {
+ return buildExpiredResponse();
+ }
+
+ String redisKey = QR_LOGIN_TOKEN_KEY + token;
+ QrLoginData qrLoginData = redisUtil.get(redisKey, QrLoginData.class);
+ if (qrLoginData == null) {
+ return buildExpiredResponse();
+ }
+
+ Date expireAt = parseExpireTime(qrLoginData.getExpireTime());
+ if (expireAt == null || DateUtil.date().after(expireAt)) {
+ redisUtil.delete(redisKey);
+ return buildExpiredResponse();
+ }
+
+ long expiresIn = calculateExpiresIn(expireAt);
+
+ if (QR_LOGIN_STATUS_CONFIRMED.equals(qrLoginData.getStatus())
+ && StrUtil.isBlank(qrLoginData.getAccessToken())
+ && qrLoginData.getUserId() != null) {
+ try {
+ User user = userService.getAllByUserId(String.valueOf(qrLoginData.getUserId()));
+ if (user != null) {
+ qrLoginData.setUsername(user.getUsername());
+ if (StrUtil.isBlank(user.getPhone())) {
+ qrLoginData.setStatus(QR_LOGIN_STATUS_BIND_PHONE);
+ qrLoginData.setNeedBindPhone(true);
+ qrLoginData.setAccessToken(null);
+ qrLoginData.setMessage("请先绑定手机号完成登录");
+ } else {
+ qrLoginData.setStatus(QR_LOGIN_STATUS_CONFIRMED);
+ qrLoginData.setNeedBindPhone(false);
+ qrLoginData.setAccessToken(buildAccessToken(user));
+ qrLoginData.setMessage(StrUtil.blankToDefault(qrLoginData.getMessage(), "登录成功"));
+ }
+ long refreshedTtl = Math.max(expiresIn, 120L);
+ persistQrLoginData(redisKey, qrLoginData, refreshedTtl, true);
+ expiresIn = refreshedTtl;
+ }
+ } catch (Exception e) {
+ log.error("补全扫码登录状态失败,token={}", token, e);
+ }
+ }
+
+ return buildStatusResponse(qrLoginData, expiresIn);
+ }
+
+ @Override
+ public QrLoginStatusResponse confirmQrLogin(QrLoginConfirmRequest request) {
+ String token = request.getToken();
+ Integer userId = request.getUserId();
+
+ if (StrUtil.isBlank(token) || userId == null) {
+ throw new RuntimeException("参数不能为空");
+ }
+
+ String redisKey = QR_LOGIN_TOKEN_KEY + token;
+ QrLoginData qrLoginData = redisUtil.get(redisKey, QrLoginData.class);
+ if (qrLoginData == null) {
+ throw new RuntimeException("扫码登录token不存在或已过期");
+ }
+
+ Date expireAt = parseExpireTime(qrLoginData.getExpireTime());
+ if (expireAt == null || DateUtil.date().after(expireAt)) {
+ redisUtil.delete(redisKey);
+ throw new RuntimeException("扫码登录token已过期");
+ }
+
+ User user = userService.getAllByUserId(String.valueOf(userId));
+ if (user == null) {
+ throw new RuntimeException("用户不存在");
+ }
+ if (user.getStatus() != null && user.getStatus() != 0) {
+ throw new RuntimeException("用户已被冻结");
+ }
+
+ String accessToken = buildAccessToken(user);
+ qrLoginData.setStatus(QR_LOGIN_STATUS_CONFIRMED);
+ qrLoginData.setUserId(userId);
+ qrLoginData.setUsername(user.getUsername());
+ qrLoginData.setAccessToken(accessToken);
+ qrLoginData.setTenantId(user.getTenantId());
+ qrLoginData.setNeedBindPhone(false);
+ qrLoginData.setMessage("登录成功");
+ persistQrLoginData(redisKey, qrLoginData, 120L, true);
+
+ log.info("用户 {} 确认扫码登录,token: {}", user.getUsername(), token);
+ return buildStatusResponse(qrLoginData, 120L);
+ }
+
+ @Override
+ public boolean scanQrCode(String token) {
+ if (StrUtil.isBlank(token)) {
+ return false;
+ }
+
+ String redisKey = QR_LOGIN_TOKEN_KEY + token;
+ QrLoginData qrLoginData = redisUtil.get(redisKey, QrLoginData.class);
+ if (qrLoginData == null) {
+ return false;
+ }
+
+ Date expireAt = parseExpireTime(qrLoginData.getExpireTime());
+ if (expireAt == null || DateUtil.date().after(expireAt)) {
+ redisUtil.delete(redisKey);
+ return false;
+ }
+
+ if (QR_LOGIN_STATUS_PENDING.equals(qrLoginData.getStatus())) {
+ qrLoginData.setStatus(QR_LOGIN_STATUS_SCANNED);
+ qrLoginData.setMessage("已识别扫码,等待公众号回调");
+ long remainingSeconds = Math.max(1L,
+ (expireAt.getTime() - DateUtil.date().getTime()) / 1000);
+ redisUtil.set(redisKey, qrLoginData, remainingSeconds, TimeUnit.SECONDS);
+ log.info("扫码登录token {} 状态更新为已扫码", token);
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public QrLoginStatusResponse bindPhone(QrLoginBindPhoneRequest request) {
+ if (request == null || StrUtil.isBlank(request.getToken()) || StrUtil.isBlank(request.getPhone()) || StrUtil.isBlank(request.getCode())) {
+ throw new RuntimeException("参数不能为空");
+ }
+ if (!CommonUtil.isValidPhoneNumber(request.getPhone())) {
+ throw new RuntimeException("请输入有效的手机号码");
+ }
+
+ String redisKey = QR_LOGIN_TOKEN_KEY + request.getToken();
+ QrLoginData qrLoginData = redisUtil.get(redisKey, QrLoginData.class);
+ if (qrLoginData == null) {
+ throw new RuntimeException("二维码已过期,请刷新后重试");
+ }
+ Date expireAt = parseExpireTime(qrLoginData.getExpireTime());
+ if (expireAt == null || DateUtil.date().after(expireAt)) {
+ redisUtil.delete(redisKey);
+ throw new RuntimeException("二维码已过期,请刷新后重试");
+ }
+ if (!QR_LOGIN_STATUS_BIND_PHONE.equals(qrLoginData.getStatus()) && !Boolean.TRUE.equals(qrLoginData.getNeedBindPhone())) {
+ throw new RuntimeException("当前二维码无需绑定手机号");
+ }
+ if (qrLoginData.getUserId() == null) {
+ throw new RuntimeException("绑定账号不存在,请重新扫码");
+ }
+
+ String codeKey = "code:" + request.getPhone();
+ String smsCode = redisUtil.get(codeKey);
+ String devCode = redisUtil.get(CACHE_KEY_VERIFICATION_CODE_BY_DEV_SMS);
+ if (StrUtil.isBlank(smsCode) && StrUtil.isBlank(devCode)) {
+ throw new RuntimeException("验证码已过期,请重新获取");
+ }
+ if (!StrUtil.equals(request.getCode(), smsCode) && !StrUtil.equals(request.getCode(), devCode)) {
+ throw new RuntimeException("验证码不正确");
+ }
+
+ User user = userService.getAllByUserId(String.valueOf(qrLoginData.getUserId()));
+ if (user == null) {
+ throw new RuntimeException("用户不存在");
+ }
+ if (user.getStatus() != null && user.getStatus() != 0) {
+ throw new RuntimeException("账号已被冻结");
+ }
+
+ User existed = userService.getByPhone(request.getPhone());
+ if (existed != null && !existed.getUserId().equals(user.getUserId())) {
+ throw new RuntimeException("该手机号已绑定其他账号");
+ }
+
+ user.setPhone(request.getPhone());
+ if (StrUtil.isBlank(user.getNickname()) || "微信公众号用户".equals(user.getNickname())) {
+ user.setNickname(DesensitizedUtil.mobilePhone(request.getPhone()));
+ }
+ if (StrUtil.isBlank(user.getUsername()) || user.getUsername().startsWith("wxoff_")) {
+ user.setUsername(request.getPhone());
+ }
+ userService.updateUser(user);
+ redisUtil.delete(codeKey);
+
+ // 绑定手机号成功后,通过MQ异步同步用户数据到 websopy
+ if (syncMessageProducer != null) {
+ User updatedUser = userService.getAllByUserId(String.valueOf(user.getUserId()));
+ if (updatedUser != null) {
+ syncMessageProducer.sendUserSyncMessage("websopy", "UPDATE", updatedUser);
+ log.info("扫码绑定手机号后发送MQ消息同步用户到websopy: userId={}, phone={}", user.getUserId(), user.getPhone());
+ }
+ }
+
+ String accessToken = buildAccessToken(user);
+ qrLoginData.setStatus(QR_LOGIN_STATUS_CONFIRMED);
+ qrLoginData.setUserId(user.getUserId());
+ qrLoginData.setUsername(user.getUsername());
+ qrLoginData.setTenantId(user.getTenantId());
+ qrLoginData.setAccessToken(accessToken);
+ qrLoginData.setNeedBindPhone(false);
+ qrLoginData.setMessage("手机号绑定成功,正在登录");
+ persistQrLoginData(redisKey, qrLoginData, 120L, true);
+
+ return buildStatusResponse(qrLoginData, 120L);
+ }
+
+ private String buildAccessToken(User user) {
+ JwtSubject jwtSubject = new JwtSubject(user.getUsername(), user.getTenantId());
+ return JwtUtil.buildToken(jwtSubject, configProperties.getTokenExpireTime(), configProperties.getTokenKey());
+ }
+
+ private Date parseExpireTime(String expireTime) {
+ if (StrUtil.isBlank(expireTime)) {
+ return null;
+ }
+ try {
+ return DateUtil.parseDateTime(expireTime);
+ } catch (Exception e) {
+ log.warn("扫码登录 expireTime 解析失败: {}", expireTime, e);
+ return null;
+ }
+ }
+
+ private long calculateExpiresIn(Date expireAt) {
+ if (expireAt == null) {
+ return 0L;
+ }
+ return Math.max(0L, (expireAt.getTime() - DateUtil.date().getTime()) / 1000);
+ }
+
+ private void persistQrLoginData(String redisKey, QrLoginData qrLoginData, long ttlSeconds, boolean refreshExpireTime) {
+ if (refreshExpireTime) {
+ qrLoginData.setExpireTime(DateUtil.formatDateTime(DateUtil.offsetSecond(DateUtil.date(), (int) ttlSeconds)));
+ }
+ redisUtil.set(redisKey, qrLoginData, ttlSeconds, TimeUnit.SECONDS);
+ }
+
+ private QrLoginStatusResponse buildExpiredResponse() {
+ QrLoginStatusResponse response = new QrLoginStatusResponse(QR_LOGIN_STATUS_EXPIRED, null, null, 0L, null);
+ response.setNeedBindPhone(false);
+ response.setMessage("二维码已过期,请刷新后重试");
+ return response;
+ }
+
+ private QrLoginStatusResponse buildStatusResponse(QrLoginData qrLoginData, Long expiresIn) {
+ QrLoginStatusResponse response = new QrLoginStatusResponse();
+ response.setStatus(qrLoginData.getStatus());
+ response.setAccessToken(qrLoginData.getAccessToken());
+ response.setExpiresIn(expiresIn);
+ response.setTenantId(qrLoginData.getTenantId());
+ response.setNeedBindPhone(Boolean.TRUE.equals(qrLoginData.getNeedBindPhone())
+ || QR_LOGIN_STATUS_BIND_PHONE.equals(qrLoginData.getStatus()));
+ response.setMessage(qrLoginData.getMessage());
+
+ // 设置下一步操作逻辑
+ if (QR_LOGIN_STATUS_BIND_PHONE.equals(qrLoginData.getStatus()) || Boolean.TRUE.equals(qrLoginData.getNeedBindPhone())) {
+ response.setNextAction("bind_phone");
+ response.setRedirectUrl(null);
+ } else if (QR_LOGIN_STATUS_CONFIRMED.equals(qrLoginData.getStatus()) && StrUtil.isNotBlank(qrLoginData.getAccessToken())) {
+ response.setNextAction("redirect");
+ response.setRedirectUrl("/console");
+ response.setSuccessMessage("登录成功,即将跳转到控制台");
+ } else {
+ response.setNextAction("wait");
+ }
+
+ if (qrLoginData.getUserId() != null) {
+ try {
+ User user = userService.getAllByUserId(String.valueOf(qrLoginData.getUserId()));
+ if (user != null) {
+ user.setPassword(null);
+ response.setUserInfo(user);
+ }
+ } catch (Exception e) {
+ log.error("构建扫码登录状态响应时查询用户失败,userId={}", qrLoginData.getUserId(), e);
+ }
+ }
+ return response;
+ }
+
+ @Override
+ public WechatScanResponse wechatScanConfirm(WechatScanRequest request) {
+ String token = request.getToken();
+ if (StrUtil.isBlank(token)) {
+ return WechatScanResponse.notBound("二维码参数错误");
+ }
+
+ String redisKey = QR_LOGIN_TOKEN_KEY + token;
+ QrLoginData qrLoginData = redisUtil.get(redisKey, QrLoginData.class);
+ if (qrLoginData == null) {
+ return WechatScanResponse.notBound("二维码已过期,请刷新重试");
+ }
+ Date expireAt = parseExpireTime(qrLoginData.getExpireTime());
+ if (expireAt == null || DateUtil.date().after(expireAt)) {
+ redisUtil.delete(redisKey);
+ return WechatScanResponse.notBound("二维码已过期,请刷新重试");
+ }
+
+ String unionId = request.getUnionId();
+ String openId = request.getOpenId();
+ Integer tenantId = qrLoginData.getTenantId();
+
+ if (StrUtil.isBlank(unionId) && StrUtil.isNotBlank(request.getCode())) {
+ try {
+ JSONObject userAccessToken = wxService.getOfficialUserAccessToken(request.getCode(), tenantId);
+ unionId = userAccessToken.getString("unionid");
+ openId = userAccessToken.getString("openid");
+ log.info("通过授权码获取到 unionId: {}, openId: {}", unionId, openId);
+ } catch (Exception e) {
+ log.error("通过授权码获取用户信息失败: {}", e.getMessage());
+ return WechatScanResponse.notBound("微信授权失败,请重试");
+ }
+ }
+
+ if (StrUtil.isBlank(unionId) && StrUtil.isBlank(openId)) {
+ return WechatScanResponse.notBound("无法获取微信用户信息");
+ }
+
+ User user = null;
+ if (StrUtil.isNotBlank(unionId)) {
+ user = userService.getOne(new LambdaQueryWrapper()
+ .eq(User::getUnionid, unionId)
+ .eq(User::getDeleted, 0)
+ .last("limit 1"));
+ log.info("通过 unionId {} 查找用户: {}", unionId, user != null ? user.getUsername() : "未找到");
+ }
+
+ if (user == null && StrUtil.isNotBlank(openId)) {
+ user = userService.getOne(new LambdaQueryWrapper()
+ .eq(User::getOpenid, openId)
+ .eq(User::getDeleted, 0)
+ .last("limit 1"));
+ log.info("通过 openId {} 查找用户: {}", openId, user != null ? user.getUsername() : "未找到");
+ }
+
+ if (user == null && (StrUtil.isNotBlank(unionId) || StrUtil.isNotBlank(openId))) {
+ try {
+ LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
+ if (StrUtil.isNotBlank(unionId)) {
+ wrapper.eq(UserOauth::getUnionid, unionId);
+ } else {
+ wrapper.eq(UserOauth::getOauthId, openId);
+ }
+ wrapper.eq(UserOauth::getDeleted, 0);
+
+ UserOauth userOauth = null;
+ if (userOauthService != null) {
+ userOauth = userOauthService.getOne(wrapper);
+ }
+
+ if (userOauth != null && userOauth.getUserId() != null) {
+ user = userService.getAllByUserId(String.valueOf(userOauth.getUserId()));
+ log.info("通过 UserOauth 查找到用户: {}", user != null ? user.getUsername() : "未找到");
+ }
+ } catch (Exception e) {
+ log.error("通过 UserOauth 查找用户失败: {}", e.getMessage());
+ }
+ }
+
+ if (user == null) {
+ return WechatScanResponse.notBound("该微信未绑定平台账号,请先在平台注册并绑定微信");
+ }
+ if (user.getStatus() != null && user.getStatus() != 0) {
+ return WechatScanResponse.notBound("账号已被冻结");
+ }
+
+ if (StrUtil.isBlank(user.getPhone())) {
+ qrLoginData.setStatus(QR_LOGIN_STATUS_BIND_PHONE);
+ qrLoginData.setUserId(user.getUserId());
+ qrLoginData.setUsername(user.getUsername());
+ qrLoginData.setTenantId(user.getTenantId());
+ qrLoginData.setAccessToken(null);
+ qrLoginData.setNeedBindPhone(true);
+ qrLoginData.setMessage("请先绑定手机号完成登录");
+ persistQrLoginData(redisKey, qrLoginData, 120L, true);
+ return WechatScanResponse.needBind("请先绑定手机号完成登录");
+ }
+
+ String accessToken = buildAccessToken(user);
+ qrLoginData.setStatus(QR_LOGIN_STATUS_CONFIRMED);
+ qrLoginData.setUserId(user.getUserId());
+ qrLoginData.setUsername(user.getUsername());
+ qrLoginData.setAccessToken(accessToken);
+ qrLoginData.setTenantId(user.getTenantId());
+ qrLoginData.setNeedBindPhone(false);
+ qrLoginData.setMessage("登录成功");
+ persistQrLoginData(redisKey, qrLoginData, 120L, true);
+
+ user.setPassword(null);
+ return WechatScanResponse.success(accessToken, user, user.getTenantId());
+ }
+}
diff --git a/src/main/java/com/gxwebsoft/common/core/Constants.java b/src/main/java/com/gxwebsoft/common/core/Constants.java
new file mode 100644
index 0000000..be48387
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/common/core/Constants.java
@@ -0,0 +1,93 @@
+package com.gxwebsoft.common.core;
+
+/**
+ * 系统常量
+ * Created by WebSoft on 2019-10-29 15:55
+ */
+public class Constants {
+ /**
+ * 默认成功码
+ */
+ public static final int RESULT_OK_CODE = 0;
+
+ /**
+ * 默认失败码
+ */
+ public static final int RESULT_ERROR_CODE = 1;
+
+ /**
+ * 默认成功信息
+ */
+ public static final String RESULT_OK_MSG = "操作成功";
+
+ /**
+ * 默认失败信息
+ */
+ public static final String RESULT_ERROR_MSG = "操作失败";
+
+ /**
+ * 无权限错误码
+ */
+ public static final int UNAUTHORIZED_CODE = 403;
+
+ /**
+ * 无权限提示信息
+ */
+ public static final String UNAUTHORIZED_MSG = "没有访问权限";
+
+ /**
+ * 未认证错误码
+ */
+ public static final int UNAUTHENTICATED_CODE = 401;
+
+ /**
+ * 未认证提示信息
+ */
+ public static final String UNAUTHENTICATED_MSG = "请先登录";
+
+ /**
+ * 登录过期错误码
+ */
+ public static final int TOKEN_EXPIRED_CODE = 401;
+
+ /**
+ * 登录过期提示信息
+ */
+ public static final String TOKEN_EXPIRED_MSG = "登录已过期";
+
+ /**
+ * 非法token错误码
+ */
+ public static final int BAD_CREDENTIALS_CODE = 401;
+
+ /**
+ * 非法token提示信息
+ */
+ public static final String BAD_CREDENTIALS_MSG = "请退出重新登录";
+
+ /**
+ * 表示升序的值
+ */
+ public static final String ORDER_ASC_VALUE = "asc";
+
+ /**
+ * 表示降序的值
+ */
+ public static final String ORDER_DESC_VALUE = "desc";
+
+ /**
+ * token通过header传递的名称
+ */
+ public static final String TOKEN_HEADER_NAME = "Authorization";
+
+ /**
+ * token通过参数传递的名称
+ */
+ public static final String TOKEN_PARAM_NAME = "access_token";
+
+ /**
+ * token认证类型
+ */
+ public static final String TOKEN_TYPE = "Bearer";
+
+}
diff --git a/src/main/java/com/gxwebsoft/common/core/annotation/OperationLog.java b/src/main/java/com/gxwebsoft/common/core/annotation/OperationLog.java
new file mode 100644
index 0000000..87bdf2c
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/common/core/annotation/OperationLog.java
@@ -0,0 +1,41 @@
+package com.gxwebsoft.common.core.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 操作日志记录注解
+ *
+ * @author WebSoft
+ * @since 2020-03-21 17:03:08
+ */
+@Documented
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface OperationLog {
+
+ /**
+ * 操作功能
+ */
+ String value() default "";
+
+ /**
+ * 操作模块
+ */
+ String module() default "";
+
+ /**
+ * 备注
+ */
+ String comments() default "";
+
+ /**
+ * 是否记录请求参数
+ */
+ boolean param() default true;
+
+ /**
+ * 是否记录返回结果
+ */
+ boolean result() default true;
+
+}
diff --git a/src/main/java/com/gxwebsoft/common/core/annotation/OperationModule.java b/src/main/java/com/gxwebsoft/common/core/annotation/OperationModule.java
new file mode 100644
index 0000000..60ab018
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/common/core/annotation/OperationModule.java
@@ -0,0 +1,21 @@
+package com.gxwebsoft.common.core.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 操作日志模块注解
+ *
+ * @author WebSoft
+ * @since 2021-09-01 20:48:16
+ */
+@Documented
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface OperationModule {
+
+ /**
+ * 模块名称
+ */
+ String value();
+
+}
diff --git a/src/main/java/com/gxwebsoft/common/core/annotation/QueryField.java b/src/main/java/com/gxwebsoft/common/core/annotation/QueryField.java
new file mode 100644
index 0000000..9377b9b
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/common/core/annotation/QueryField.java
@@ -0,0 +1,22 @@
+package com.gxwebsoft.common.core.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 查询条件注解
+ *
+ * @author WebSoft
+ * @since 2021-09-01 20:48:16
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE})
+public @interface QueryField {
+
+ // 字段名称
+ String value() default "";
+
+ // 查询方式
+ QueryType type() default QueryType.LIKE;
+
+}
diff --git a/src/main/java/com/gxwebsoft/common/core/annotation/QueryType.java b/src/main/java/com/gxwebsoft/common/core/annotation/QueryType.java
new file mode 100644
index 0000000..3eb540e
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/common/core/annotation/QueryType.java
@@ -0,0 +1,42 @@
+package com.gxwebsoft.common.core.annotation;
+
+/**
+ * 查询方式
+ *
+ * @author WebSoft
+ * @since 2021-09-01 20:48:16
+ */
+public enum QueryType {
+ // 等于
+ EQ,
+ // 不等于
+ NE,
+ // 大于
+ GT,
+ // 大于等于
+ GE,
+ // 小于
+ LT,
+ // 小于等于
+ LE,
+ // 包含
+ LIKE,
+ // 不包含
+ NOT_LIKE,
+ // 结尾等于
+ LIKE_LEFT,
+ // 开头等于
+ LIKE_RIGHT,
+ // 为NULL
+ IS_NULL,
+ // 不为空
+ IS_NOT_NULL,
+ // IN
+ IN,
+ // NOT IN
+ NOT_IN,
+ // IN条件解析逗号分割
+ IN_STR,
+ // NOT IN条件解析逗号分割
+ NOT_IN_STR
+}
diff --git a/src/main/java/com/gxwebsoft/common/core/aspect/OperationLogAspect.java b/src/main/java/com/gxwebsoft/common/core/aspect/OperationLogAspect.java
new file mode 100644
index 0000000..2e71792
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/common/core/aspect/OperationLogAspect.java
@@ -0,0 +1,211 @@
+package com.gxwebsoft.common.core.aspect;
+
+import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.extra.servlet.ServletUtil;
+import cn.hutool.http.useragent.UserAgent;
+import cn.hutool.http.useragent.UserAgentUtil;
+import com.gxwebsoft.common.core.annotation.OperationLog;
+import com.gxwebsoft.common.core.annotation.OperationModule;
+import com.gxwebsoft.common.core.utils.JSONUtil;
+import com.gxwebsoft.common.system.entity.OperationRecord;
+import com.gxwebsoft.common.system.entity.User;
+import com.gxwebsoft.common.system.service.OperationRecordService;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import io.swagger.v3.oas.annotations.Operation;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.annotation.*;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.stereotype.Component;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.lang.reflect.Method;
+import java.util.Map;
+
+/**
+ * 操作日志记录
+ *
+ * @author WebSoft
+ * @since 2020-03-21 16:58:16:05
+ */
+@Aspect
+@Component
+public class OperationLogAspect {
+ @Resource
+ private OperationRecordService operationRecordService;
+
+ // 参数、返回结果、错误信息等最大保存长度
+ private static final int MAX_LENGTH = 1000;
+ // 用于记录请求耗时
+ private final ThreadLocal startTime = new ThreadLocal<>();
+
+ @Pointcut("@annotation(com.gxwebsoft.common.core.annotation.OperationLog)")
+ public void operationLog() {
+ }
+
+ @Before("operationLog()")
+ public void doBefore(JoinPoint joinPoint) throws Throwable {
+ startTime.set(System.currentTimeMillis());
+ }
+
+ @AfterReturning(pointcut = "operationLog()", returning = "result")
+ public void doAfterReturning(JoinPoint joinPoint, Object result) {
+ saveLog(joinPoint, result, null);
+ }
+
+ @AfterThrowing(value = "operationLog()", throwing = "e")
+ public void doAfterThrowing(JoinPoint joinPoint, Exception e) {
+ saveLog(joinPoint, null, e);
+ }
+
+ /**
+ * 保存操作记录
+ */
+ private void saveLog(JoinPoint joinPoint, Object result, Exception e) {
+ OperationRecord record = new OperationRecord();
+ // 记录操作耗时
+ if (startTime.get() != null) {
+ record.setSpendTime(System.currentTimeMillis() - startTime.get());
+ }
+ // 记录当前登录用户id、租户id
+ User user = getLoginUser();
+ if (user != null) {
+ record.setUserId(user.getUserId());
+ record.setTenantId(user.getTenantId());
+ }
+ // 记录请求地址、请求方式、ip
+ ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
+ HttpServletRequest request = (attributes == null ? null : attributes.getRequest());
+ if (request != null) {
+ record.setUrl(request.getRequestURI());
+ record.setRequestMethod(request.getMethod());
+ UserAgent ua = UserAgentUtil.parse(ServletUtil.getHeaderIgnoreCase(request, "User-Agent"));
+ record.setOs(ua.getPlatform().toString());
+ record.setDevice(ua.getOs().toString());
+ record.setBrowser(ua.getBrowser().toString());
+ record.setIp(ServletUtil.getClientIP(request));
+ }
+ // 记录异常信息
+ if (e != null) {
+ record.setStatus(1);
+ record.setError(StrUtil.sub(e.toString(), 0, MAX_LENGTH));
+ }
+ // 记录模块名、操作功能、请求方法、请求参数、返回结果
+ MethodSignature signature = (MethodSignature) joinPoint.getSignature();
+ record.setMethod(joinPoint.getTarget().getClass().getName() + "." + signature.getName());
+ Method method = signature.getMethod();
+ if (method != null) {
+ OperationLog ol = method.getAnnotation(OperationLog.class);
+ if (ol != null) {
+ // 记录操作功能
+ record.setDescription(getDescription(method, ol));
+ // 记录操作模块
+ record.setModule(getModule(joinPoint, ol));
+ // 记录备注
+ if (StrUtil.isNotEmpty(ol.comments())) {
+ record.setComments(ol.comments());
+ }
+ // 记录请求参数
+ if (ol.param() && request != null) {
+ record.setParams(StrUtil.sub(getParams(joinPoint, request), 0, MAX_LENGTH));
+ }
+ // 记录请求结果
+ if (ol.result() && result != null) {
+ record.setResult(StrUtil.sub(JSONUtil.toJSONString(result), 0, MAX_LENGTH));
+ }
+ }
+ }
+ operationRecordService.saveAsync(record);
+ }
+
+ /**
+ * 获取当前登录用户
+ */
+ private User getLoginUser() {
+ Authentication subject = SecurityContextHolder.getContext().getAuthentication();
+ if (subject != null) {
+ Object object = subject.getPrincipal();
+ if (object instanceof User) {
+ return (User) object;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * 获取请求参数
+ *
+ * @param joinPoint JoinPoint
+ * @param request HttpServletRequest
+ * @return String
+ */
+ private String getParams(JoinPoint joinPoint, HttpServletRequest request) {
+ String params;
+ Map paramsMap = ServletUtil.getParamMap(request);
+ if (paramsMap.keySet().size() > 0) {
+ params = JSONUtil.toJSONString(paramsMap);
+ } else {
+ StringBuilder sb = new StringBuilder();
+ for (Object arg : joinPoint.getArgs()) {
+ if (ObjectUtil.isNull(arg)
+ || arg instanceof MultipartFile
+ || arg instanceof HttpServletRequest
+ || arg instanceof HttpServletResponse) {
+ continue;
+ }
+ sb.append(JSONUtil.toJSONString(arg)).append(" ");
+ }
+ params = sb.toString();
+ }
+ return params;
+ }
+
+ /**
+ * 获取操作模块
+ *
+ * @param joinPoint JoinPoint
+ * @param ol OperationLog
+ * @return String
+ */
+ private String getModule(JoinPoint joinPoint, OperationLog ol) {
+ if (StrUtil.isNotEmpty(ol.module())) {
+ return ol.module();
+ }
+ OperationModule om = joinPoint.getTarget().getClass().getAnnotation(OperationModule.class);
+ if (om != null && StrUtil.isNotEmpty(om.value())) {
+ return om.value();
+ }
+ Tag tag = joinPoint.getTarget().getClass().getAnnotation(Tag.class);
+ if (tag != null && StrUtil.isNotEmpty(tag.name())) {
+ return tag.name();
+ }
+ return null;
+ }
+
+ /**
+ * 获取操作功能
+ *
+ * @param method Method
+ * @param ol OperationLog
+ * @return String
+ */
+ private String getDescription(Method method, OperationLog ol) {
+ if (StrUtil.isNotEmpty(ol.value())) {
+ return ol.value();
+ }
+ Operation operation = method.getAnnotation(Operation.class);
+ if (operation != null && StrUtil.isNotEmpty(operation.summary())) {
+ return operation.summary();
+ }
+ return null;
+ }
+
+}
diff --git a/src/main/java/com/gxwebsoft/common/core/config/CertificateProperties.java b/src/main/java/com/gxwebsoft/common/core/config/CertificateProperties.java
new file mode 100644
index 0000000..ab09a24
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/common/core/config/CertificateProperties.java
@@ -0,0 +1,197 @@
+package com.gxwebsoft.common.core.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+/**
+ * 证书配置属性类
+ * 支持开发环境从classpath加载证书,生产环境从Docker挂载卷加载证书
+ *
+ * @author 科技小王子
+ * @since 2024-07-26
+ */
+@Data
+@Component
+@ConfigurationProperties(prefix = "certificate")
+public class CertificateProperties {
+
+ /**
+ * 证书加载模式
+ * CLASSPATH: 从classpath加载(开发环境)
+ * FILESYSTEM: 从文件系统加载(生产环境)
+ * VOLUME: 从Docker挂载卷加载(容器环境)
+ */
+ private LoadMode loadMode = LoadMode.CLASSPATH;
+
+ /**
+ * Docker挂载卷证书根路径
+ */
+ private String certRootPath = "/app/certs";
+
+ /**
+ * 开发环境证书路径前缀
+ */
+ private String devCertPath = "certs/dev";
+
+ /**
+ * 微信支付证书配置
+ */
+ private WechatPayConfig wechatPay = new WechatPayConfig();
+
+ /**
+ * 支付宝证书配置
+ */
+ private AlipayConfig alipay = new AlipayConfig();
+
+ /**
+ * 证书加载模式枚举
+ */
+ public enum LoadMode {
+ CLASSPATH, // 从classpath加载
+ FILESYSTEM, // 从文件系统加载
+ VOLUME // 从Docker挂载卷加载
+ }
+
+ /**
+ * 微信支付证书配置
+ */
+ @Data
+ public static class WechatPayConfig {
+ /**
+ * 开发环境配置
+ */
+ private DevConfig dev = new DevConfig();
+
+ /**
+ * 生产环境基础路径
+ */
+ private String prodBasePath = "/file";
+
+ /**
+ * 微信支付证书目录名
+ */
+ private String certDir = "wechat";
+
+ @Data
+ public static class DevConfig {
+ /**
+ * APIv3密钥
+ */
+ private String apiV3Key;
+
+ /**
+ * 商户私钥证书文件名
+ */
+ private String privateKeyFile = "apiclient_key.pem";
+
+ /**
+ * 商户证书文件名
+ */
+ private String apiclientCertFile = "apiclient_cert.pem";
+
+ /**
+ * 微信支付平台证书文件名
+ */
+ private String wechatpayCertFile = "wechatpay_cert.pem";
+ }
+ }
+
+ /**
+ * 支付宝证书配置
+ */
+ @Data
+ public static class AlipayConfig {
+ /**
+ * 支付宝证书目录名
+ */
+ private String certDir = "alipay";
+
+ /**
+ * 应用私钥文件名
+ */
+ private String appPrivateKeyFile = "app_private_key.pem";
+
+ /**
+ * 应用公钥证书文件名
+ */
+ private String appCertPublicKeyFile = "appCertPublicKey.crt";
+
+ /**
+ * 支付宝公钥证书文件名
+ */
+ private String alipayCertPublicKeyFile = "alipayCertPublicKey.crt";
+
+ /**
+ * 支付宝根证书文件名
+ */
+ private String alipayRootCertFile = "alipayRootCert.crt";
+ }
+
+ /**
+ * 获取证书文件的完整路径
+ *
+ * @param certType 证书类型(wechat/alipay)
+ * @param fileName 文件名
+ * @return 完整路径
+ */
+ public String getCertificatePath(String certType, String fileName) {
+ switch (loadMode) {
+ case CLASSPATH:
+ return devCertPath + "/" + certType + "/" + fileName;
+ case FILESYSTEM:
+ return System.getProperty("user.dir") + "/certs/" + certType + "/" + fileName;
+ case VOLUME:
+ return certRootPath + "/" + certType + "/" + fileName;
+ default:
+ throw new IllegalArgumentException("不支持的证书加载模式: " + loadMode);
+ }
+ }
+
+ /**
+ * 获取微信支付证书路径
+ *
+ * @param fileName 文件名
+ * @return 完整路径
+ */
+ public String getWechatPayCertPath(String fileName) {
+ return getCertificatePath(wechatPay.getCertDir(), fileName);
+ }
+
+ /**
+ * 获取支付宝证书路径
+ *
+ * @param fileName 文件名
+ * @return 完整路径
+ */
+ public String getAlipayCertPath(String fileName) {
+ return getCertificatePath(alipay.getCertDir(), fileName);
+ }
+
+ /**
+ * 检查证书加载模式是否为classpath模式
+ *
+ * @return true if classpath mode
+ */
+ public boolean isClasspathMode() {
+ return LoadMode.CLASSPATH.equals(loadMode);
+ }
+
+ /**
+ * 检查证书加载模式是否为文件系统模式
+ *
+ * @return true if filesystem mode
+ */
+ public boolean isFilesystemMode() {
+ return LoadMode.FILESYSTEM.equals(loadMode);
+ }
+
+ /**
+ * 检查证书加载模式是否为挂载卷模式
+ *
+ * @return true if volume mode
+ */
+ public boolean isVolumeMode() {
+ return LoadMode.VOLUME.equals(loadMode);
+ }
+}
diff --git a/src/main/java/com/gxwebsoft/common/core/config/ConfigProperties.java b/src/main/java/com/gxwebsoft/common/core/config/ConfigProperties.java
new file mode 100644
index 0000000..c8afd9c
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/common/core/config/ConfigProperties.java
@@ -0,0 +1,115 @@
+package com.gxwebsoft.common.core.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * 系统配置属性
+ *
+ * @author WebSoft
+ * @since 2021-08-30 17:58:16
+ */
+@Data
+@ConfigurationProperties(prefix = "config")
+public class ConfigProperties {
+ /**
+ * 文件上传磁盘位置
+ */
+ private Integer uploadLocation = 0;
+
+ /**
+ * 文件上传是否使用uuid命名
+ */
+ private Boolean uploadUuidName = true;
+
+ /**
+ * 文件上传生成缩略图的大小(kb)
+ */
+ private Integer thumbnailSize = 60;
+
+ /**
+ * OpenOffice的安装目录
+ */
+ private String openOfficeHome;
+
+ /**
+ * swagger扫描包
+ */
+ private String swaggerBasePackage;
+
+ /**
+ * swagger文档标题
+ */
+ private String swaggerTitle;
+
+ /**
+ * swagger文档描述
+ */
+ private String swaggerDescription;
+
+ /**
+ * swagger文档版本号
+ */
+ private String swaggerVersion;
+
+ /**
+ * swagger地址
+ */
+ private String swaggerHost;
+
+ /**
+ * token过期时间, 单位秒
+ */
+ private Long tokenExpireTime = 60 * 60 * 365 * 24L;
+
+ /**
+ * token快要过期自动刷新时间, 单位分钟
+ */
+ private int tokenRefreshTime = 30;
+
+ /**
+ * 生成token的密钥Key的base64字符
+ */
+ private String tokenKey;
+
+ /**
+ * 文件上传目录
+ */
+ private String uploadPath;
+
+ /**
+ * 本地文件上传目录(开发环境)
+ */
+ private String localUploadPath;
+
+ /**
+ * 文件服务器
+ */
+ private String fileServer;
+
+ /**
+ * 网关地址
+ */
+ private String serverUrl;
+
+ /**
+ * websopy 服务地址(用于同步用户数据)
+ */
+ private String websopyUrl;
+
+ /**
+ * 微信扫码H5页面访问地址(用于微信扫码登录跳转)
+ */
+ private String wechatScanUrl;
+
+ /**
+ * 阿里云存储 OSS
+ * Endpoint
+ */
+ private String endpoint;
+ private String accessKeyId;
+ private String accessKeySecret;
+ private String bucketName;
+ private String bucketDomain;
+
+}
diff --git a/src/main/java/com/gxwebsoft/common/core/config/HttpMessageConverter.java b/src/main/java/com/gxwebsoft/common/core/config/HttpMessageConverter.java
new file mode 100644
index 0000000..6bae59d
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/common/core/config/HttpMessageConverter.java
@@ -0,0 +1,15 @@
+package com.gxwebsoft.common.core.config;
+
+import org.springframework.http.MediaType;
+import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class HttpMessageConverter extends MappingJackson2HttpMessageConverter {
+ public HttpMessageConverter(){
+ List mediaTypes = new ArrayList<>();
+ mediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED);
+ setSupportedMediaTypes(mediaTypes);
+ }
+}
diff --git a/src/main/java/com/gxwebsoft/common/core/config/JacksonConfig.java b/src/main/java/com/gxwebsoft/common/core/config/JacksonConfig.java
new file mode 100644
index 0000000..0c73720
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/common/core/config/JacksonConfig.java
@@ -0,0 +1,40 @@
+package com.gxwebsoft.common.core.config;
+
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
+
+/**
+ * Jackson配置类
+ * 解决Java 8时间类型序列化问题
+ *
+ * @author WebSoft
+ * @since 2024-08-28
+ */
+@Configuration
+public class JacksonConfig {
+
+ @Bean
+ @Primary
+ public ObjectMapper objectMapper() {
+ ObjectMapper mapper = new ObjectMapper();
+
+ // 注册JavaTimeModule
+ mapper.registerModule(new JavaTimeModule());
+
+ // 禁用将日期写为时间戳
+ mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
+
+ // 禁用将日期时间戳写为纳秒
+ mapper.disable(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS);
+
+ // 忽略未知字段,避免反序列化时出现 "Unrecognized field" 错误
+ mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+
+ return mapper;
+ }
+}
diff --git a/src/main/java/com/gxwebsoft/common/core/config/MqttProperties.java b/src/main/java/com/gxwebsoft/common/core/config/MqttProperties.java
new file mode 100644
index 0000000..cfc882d
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/common/core/config/MqttProperties.java
@@ -0,0 +1,72 @@
+package com.gxwebsoft.common.core.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+/**
+ * MQTT配置属性
+ *
+ * @author 科技小王子
+ * @since 2025-07-02
+ */
+@Data
+@Component
+@ConfigurationProperties(prefix = "mqtt")
+public class MqttProperties {
+
+ /**
+ * 是否启用MQTT服务
+ */
+ private boolean enabled = false;
+
+ /**
+ * MQTT服务器地址
+ */
+ private String host = "tcp://127.0.0.1:1883";
+
+ /**
+ * 用户名
+ */
+ private String username = "";
+
+ /**
+ * 密码
+ */
+ private String password = "";
+
+ /**
+ * 客户端ID前缀
+ */
+ private String clientIdPrefix = "mqtt_client_";
+
+ /**
+ * 订阅主题
+ */
+ private String topic = "/SW_GPS/#";
+
+ /**
+ * QoS等级
+ */
+ private int qos = 2;
+
+ /**
+ * 连接超时时间(秒)
+ */
+ private int connectionTimeout = 10;
+
+ /**
+ * 心跳间隔(秒)
+ */
+ private int keepAliveInterval = 20;
+
+ /**
+ * 是否自动重连
+ */
+ private boolean autoReconnect = true;
+
+ /**
+ * 是否清除会话
+ */
+ private boolean cleanSession = false;
+}
diff --git a/src/main/java/com/gxwebsoft/common/core/config/MybatisPlusConfig.java b/src/main/java/com/gxwebsoft/common/core/config/MybatisPlusConfig.java
new file mode 100644
index 0000000..2259142
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/common/core/config/MybatisPlusConfig.java
@@ -0,0 +1,139 @@
+package com.gxwebsoft.common.core.config;
+
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.annotation.DbType;
+import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
+import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
+import com.gxwebsoft.common.core.utils.RedisUtil;
+import com.gxwebsoft.common.system.entity.User;
+import net.sf.jsqlparser.expression.Expression;
+import net.sf.jsqlparser.expression.LongValue;
+import net.sf.jsqlparser.expression.NullValue;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletRequest;
+import java.util.Arrays;
+
+/**
+ * MybatisPlus配置
+ *
+ * @author WebSoft
+ * @since 2018-02-22 11:29:28
+ */
+@Configuration
+public class MybatisPlusConfig {
+ @Resource
+ private RedisUtil redisUtil;
+
+ @Bean
+ public MybatisPlusInterceptor mybatisPlusInterceptor(HttpServletRequest request) {
+ MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
+
+ // 多租户插件配置
+ TenantLineHandler tenantLineHandler = new TenantLineHandler() {
+
+ @Override
+ public Expression getTenantId() {
+ String tenantId;
+ // 从请求头拿ID
+ tenantId = request.getHeader("tenantId");
+ if(tenantId != null){
+ return new LongValue(tenantId);
+ }
+ // 从域名拿ID
+ String Domain = request.getHeader("Domain");
+ if (StrUtil.isNotBlank(Domain)) {
+ String key = "Domain:" + Domain;
+ tenantId = redisUtil.get(key);
+ if(tenantId != null){
+ System.out.println("授权域名" + Domain + " => " + tenantId);
+ return new LongValue(tenantId);
+ }
+ }
+ return getLoginUserTenantId();
+ }
+
+ @Override
+ public boolean ignoreTable(String tableName) {
+ return Arrays.asList(
+ "sys_tenant",
+ "sys_dictionary",
+ "sys_dictionary_data",
+ "sys_user_oauth",
+ "sys_email_record",
+ "sys_plug",
+ "sys_version",
+ "sys_order",
+ "sys_modules",
+ "sys_environment",
+ "sys_components",
+ "sys_website_field",
+// "sys_company",
+ "sys_domain",
+ "sys_white_domain"
+// "cms_domain"
+// "cms_website",
+// "cms_website_field",
+// "cms_navigation",
+// "cms_design",
+// "cms_design_record",
+// "cms_article",
+// "cms_article_content",
+// "cms_article_category",
+// "cms_article_comment",
+// "cms_article_count",
+// "cms_article_like",
+// "cms_form",
+// "cms_form_record",
+// "cms_link",
+// "oa_app",
+// "oa_app_field",
+// "oa_app_renew",
+// "oa_app_url",
+// "oa_app_user",
+// "shop_goods",
+// "cms_product",
+// "cms_product_url",
+// "cms_product_spec",
+// "cms_product_spec_value",
+// "sys_company_content"
+ ).contains(tableName);
+ }
+ };
+ TenantLineInnerInterceptor tenantLineInnerInterceptor = new TenantLineInnerInterceptor(tenantLineHandler);
+ interceptor.addInnerInterceptor(tenantLineInnerInterceptor);
+
+ // 分页插件配置
+ PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
+ interceptor.addInnerInterceptor(paginationInnerInterceptor);
+
+ return interceptor;
+ }
+
+ /**
+ * 获取当前登录用户的租户id
+ *
+ * @return Integer
+ */
+ public Expression getLoginUserTenantId() {
+ try {
+ Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+ if (authentication != null) {
+ Object object = authentication.getPrincipal();
+ if (object instanceof User) {
+ return new LongValue(((User) object).getTenantId());
+ }
+ }
+ } catch (Exception e) {
+ System.out.println(e.getMessage());
+ }
+ return new NullValue();
+ }
+
+}
diff --git a/src/main/java/com/gxwebsoft/common/core/config/OpenApiConfig.java b/src/main/java/com/gxwebsoft/common/core/config/OpenApiConfig.java
new file mode 100644
index 0000000..3f2a255
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/common/core/config/OpenApiConfig.java
@@ -0,0 +1,46 @@
+package com.gxwebsoft.common.core.config;
+
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.info.Contact;
+import io.swagger.v3.oas.models.info.Info;
+import io.swagger.v3.oas.models.security.SecurityRequirement;
+import io.swagger.v3.oas.models.security.SecurityScheme;
+import io.swagger.v3.oas.models.Components;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import javax.annotation.Resource;
+
+/**
+ * OpenAPI 配置
+ *
+ * @author WebSoft
+ * @since 2025-09-11
+ */
+@Configuration
+public class OpenApiConfig {
+
+ @Resource
+ private ConfigProperties config;
+
+ @Bean
+ public OpenAPI customOpenAPI() {
+ return new OpenAPI()
+ .info(new Info()
+ .title(config.getSwaggerTitle())
+ .description(config.getSwaggerDescription())
+ .version(config.getSwaggerVersion())
+ .contact(new Contact()
+ .name("科技小王子")
+ .url("https://websoft.top")
+ .email("170083662@qq.com")))
+ .addSecurityItem(new SecurityRequirement().addList("Authorization"))
+ .components(new Components()
+ .addSecuritySchemes("Authorization",
+ new SecurityScheme()
+ .type(SecurityScheme.Type.HTTP)
+ .scheme("bearer")
+ .bearerFormat("JWT")
+ .description("JWT 认证")));
+ }
+}
diff --git a/src/main/java/com/gxwebsoft/common/core/config/RestTemplateConfig.java b/src/main/java/com/gxwebsoft/common/core/config/RestTemplateConfig.java
new file mode 100644
index 0000000..786798f
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/common/core/config/RestTemplateConfig.java
@@ -0,0 +1,29 @@
+package com.gxwebsoft.common.core.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.client.ClientHttpRequestFactory;
+import org.springframework.http.client.SimpleClientHttpRequestFactory;
+import org.springframework.web.client.RestTemplate;
+
+
+@Configuration
+public class RestTemplateConfig {
+
+ @Bean
+ public RestTemplate restTemplate(ClientHttpRequestFactory factory) {
+ RestTemplate restTemplate = new RestTemplate(factory);
+ restTemplate.getMessageConverters().add(new HttpMessageConverter());
+ return restTemplate;
+ }
+ @Bean
+ public ClientHttpRequestFactory simpleClientHttpRequestFactory() {
+ SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
+ // ms
+ factory.setReadTimeout(60000);
+ // ms
+ factory.setConnectTimeout(60000);
+
+ return factory;
+ }
+}
diff --git a/src/main/java/com/gxwebsoft/common/core/config/SpringContextUtil.java b/src/main/java/com/gxwebsoft/common/core/config/SpringContextUtil.java
new file mode 100644
index 0000000..4e6d883
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/common/core/config/SpringContextUtil.java
@@ -0,0 +1,62 @@
+package com.gxwebsoft.common.core.config;
+
+import org.springframework.beans.BeansException;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.stereotype.Component;
+
+/**
+ * @Author ds
+ * @Date 2022-05-05
+ */
+@Component
+public class SpringContextUtil implements ApplicationContextAware {
+ /**
+ * spring的应用上下文
+ */
+ private static ApplicationContext applicationContext;
+
+ /**
+ * 初始化时将应用上下文设置进applicationContext
+ * @param applicationContext
+ * @throws BeansException
+ */
+ @Override
+ public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
+ SpringContextUtil.applicationContext=applicationContext;
+ }
+
+ public static ApplicationContext getApplicationContext(){
+ return applicationContext;
+ }
+
+ /**
+ * 根据bean名称获取某个bean对象
+ *
+ * @param name bean名称
+ * @return Object
+ * @throws BeansException
+ */
+ public static Object getBean(String name) throws BeansException {
+ return applicationContext.getBean(name);
+ }
+
+ /**
+ * 根据bean的class获取某个bean对象
+ * @param beanClass
+ * @param
+ * @return
+ * @throws BeansException
+ */
+ public static T getBean(Class beanClass) throws BeansException {
+ return applicationContext.getBean(beanClass);
+ }
+
+ /**
+ * 获取spring.profiles.active
+ * @return
+ */
+ public static String getProfile(){
+ return getApplicationContext().getEnvironment().getActiveProfiles()[0];
+ }
+}
diff --git a/src/main/java/com/gxwebsoft/common/core/config/WebMvcConfig.java b/src/main/java/com/gxwebsoft/common/core/config/WebMvcConfig.java
new file mode 100644
index 0000000..2ed9d8b
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/common/core/config/WebMvcConfig.java
@@ -0,0 +1,31 @@
+package com.gxwebsoft.common.core.config;
+
+import com.gxwebsoft.common.core.Constants;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.CorsRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+/**
+ * WebMvc配置, 拦截器、资源映射等都在此配置
+ *
+ * @author WebSoft
+ * @since 2019-06-12 10:11:16
+ */
+@Configuration
+public class WebMvcConfig implements WebMvcConfigurer {
+
+ /**
+ * 支持跨域访问
+ */
+ @Override
+ public void addCorsMappings(CorsRegistry registry) {
+ registry.addMapping("/**")
+ .allowedOriginPatterns("*")
+ .allowedHeaders("*")
+ .exposedHeaders(Constants.TOKEN_HEADER_NAME)
+ .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")
+ .allowCredentials(true)
+ .maxAge(3600);
+ }
+
+}
diff --git a/src/main/java/com/gxwebsoft/common/core/constants/AppUserConstants.java b/src/main/java/com/gxwebsoft/common/core/constants/AppUserConstants.java
new file mode 100644
index 0000000..538e295
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/common/core/constants/AppUserConstants.java
@@ -0,0 +1,8 @@
+package com.gxwebsoft.common.core.constants;
+
+public class AppUserConstants {
+ // 成员角色
+ public static final Integer TRIAL = 10; // 体验成员
+ public static final Integer DEVELOPER = 20; // 开发者
+ public static final Integer ADMINISTRATOR = 30; // 管理员
+}
diff --git a/src/main/java/com/gxwebsoft/common/core/constants/ArticleConstants.java b/src/main/java/com/gxwebsoft/common/core/constants/ArticleConstants.java
new file mode 100644
index 0000000..62a38cc
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/common/core/constants/ArticleConstants.java
@@ -0,0 +1,6 @@
+package com.gxwebsoft.common.core.constants;
+
+public class ArticleConstants extends BaseConstants {
+ public static final String[] ARTICLE_STATUS = {"已发布","待审核","已驳回","违规内容"};
+ public static final String CACHE_KEY_ARTICLE = "Article:";
+}
diff --git a/src/main/java/com/gxwebsoft/common/core/constants/BalanceConstants.java b/src/main/java/com/gxwebsoft/common/core/constants/BalanceConstants.java
new file mode 100644
index 0000000..6857250
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/common/core/constants/BalanceConstants.java
@@ -0,0 +1,10 @@
+package com.gxwebsoft.common.core.constants;
+
+public class BalanceConstants {
+ // 余额变动场景
+ public static final Integer BALANCE_RECHARGE = 10; // 用户充值
+ public static final Integer BALANCE_USE = 20; // 用户消费
+ public static final Integer BALANCE_RE_LET = 21; // 续租
+ public static final Integer BALANCE_ADMIN = 30; // 管理员操作
+ public static final Integer BALANCE_REFUND = 40; // 订单退款
+}
diff --git a/src/main/java/com/gxwebsoft/common/core/constants/BaseConstants.java b/src/main/java/com/gxwebsoft/common/core/constants/BaseConstants.java
new file mode 100644
index 0000000..66cf4c0
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/common/core/constants/BaseConstants.java
@@ -0,0 +1,5 @@
+package com.gxwebsoft.common.core.constants;
+
+public class BaseConstants {
+ public static final String[] STATUS = {"未定义","显示","隐藏"};
+}
diff --git a/src/main/java/com/gxwebsoft/common/core/constants/DomainConstants.java b/src/main/java/com/gxwebsoft/common/core/constants/DomainConstants.java
new file mode 100644
index 0000000..c101769
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/common/core/constants/DomainConstants.java
@@ -0,0 +1,10 @@
+package com.gxwebsoft.common.core.constants;
+
+public class DomainConstants {
+ public static final String ROOT_DOMAIN = "websoft.top"; // 根域名
+ public static final String PREFIX = "https://"; // 域名前缀
+ public static final String ADMIN_SUFFIX = ".".concat(ROOT_DOMAIN); // 后台管理域名拼接
+ public static final String WEB_SUFFIX = ".wsdns.cn"; // 应用域名拼接
+ public static final String DOMAIN = PREFIX.concat(ROOT_DOMAIN); // 完整域名
+
+}
diff --git a/src/main/java/com/gxwebsoft/common/core/constants/OrderConstants.java b/src/main/java/com/gxwebsoft/common/core/constants/OrderConstants.java
new file mode 100644
index 0000000..e866654
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/common/core/constants/OrderConstants.java
@@ -0,0 +1,37 @@
+package com.gxwebsoft.common.core.constants;
+
+public class OrderConstants {
+ // 支付方式
+ public static final String PAY_METHOD_BALANCE = "10"; // 余额支付
+ public static final String PAY_METHOD_WX = "20"; // 微信支付
+ public static final String PAY_METHOD_ALIPAY = "30"; // 支付宝支付
+ public static final String PAY_METHOD_OTHER = "40"; // 其他支付
+
+ // 付款状态
+ public static final Integer PAY_STATUS_NO_PAY = 10; // 未付款
+ public static final Integer PAY_STATUS_SUCCESS = 20; // 已付款
+
+ // 发货状态
+ public static final Integer DELIVERY_STATUS_NO = 10; // 未发货
+ public static final Integer DELIVERY_STATUS_YES = 20; // 已发货
+ public static final Integer DELIVERY_STATUS_30 = 30; // 部分发货
+
+ // 收货状态
+ public static final Integer RECEIPT_STATUS_NO = 10; // 未收货
+ public static final Integer RECEIPT_STATUS_YES = 20; // 已收货
+ public static final Integer RECEIPT_STATUS_RETURN = 30; // 已退货
+
+ // 订单状态
+ public static final Integer ORDER_STATUS_DOING = 10; // 进行中
+ public static final Integer ORDER_STATUS_CANCEL = 20; // 已取消
+ public static final Integer ORDER_STATUS_TO_CANCEL = 21; // 待取消
+ public static final Integer ORDER_STATUS_COMPLETED = 30; // 已完成
+
+ // 订单结算状态
+ public static final Integer ORDER_SETTLED_YES = 1; // 已结算
+ public static final Integer ORDER_SETTLED_NO = 0; // 未结算
+
+
+
+
+}
diff --git a/src/main/java/com/gxwebsoft/common/core/constants/PlatformConstants.java b/src/main/java/com/gxwebsoft/common/core/constants/PlatformConstants.java
new file mode 100644
index 0000000..896f8e3
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/common/core/constants/PlatformConstants.java
@@ -0,0 +1,12 @@
+package com.gxwebsoft.common.core.constants;
+
+public class PlatformConstants {
+ public static final String MP_OFFICIAL = "MP-OFFICIAL"; // 微信公众号
+ public static final String MP_WEIXIN = "MP-WEIXIN"; // 微信小程序
+ public static final String MP_ALIPAY = "MP-ALIPAY"; // 支付宝小程序
+ public static final String WEB = "WEB"; // web(同H5)
+ public static final String H5 = "H5"; // H5(推荐使用 WEB)
+ public static final String APP = "APP"; // App
+ public static final String MP_BAIDU = "MP-BAIDU"; // 百度小程序
+ public static final String MP_TOUTIAO = "MP-TOUTIAO"; // 百度小程序
+}
diff --git a/src/main/java/com/gxwebsoft/common/core/constants/ProfitConstants.java b/src/main/java/com/gxwebsoft/common/core/constants/ProfitConstants.java
new file mode 100644
index 0000000..2cb60fd
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/common/core/constants/ProfitConstants.java
@@ -0,0 +1,9 @@
+package com.gxwebsoft.common.core.constants;
+
+public class ProfitConstants {
+ // 收益类型
+ public static final Integer PROFIT_TYPE10 = 10; // 推广收益
+ public static final Integer PROFIT_TYPE20 = 20; // 团队收益
+ public static final Integer PROFIT_TYPE30 = 30; // 门店收益
+ public static final Integer PROFIT_TYPE40 = 30; // 区域收益
+}
diff --git a/src/main/java/com/gxwebsoft/common/core/constants/QRCodeConstants.java b/src/main/java/com/gxwebsoft/common/core/constants/QRCodeConstants.java
new file mode 100644
index 0000000..1b30868
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/common/core/constants/QRCodeConstants.java
@@ -0,0 +1,10 @@
+package com.gxwebsoft.common.core.constants;
+
+public class QRCodeConstants {
+ // 二维码类型
+ public static final String USER_QRCODE = "user"; // 用户二维码
+ public static final String TASK_QRCODE = "task"; // 工单二维码
+ public static final String ARTICLE_QRCODE = "article"; // 文章二维码
+ public static final String GOODS_QRCODE = "goods"; // 商品二维码
+ public static final String DIY_QRCODE = "diy"; // 工单二维码
+}
diff --git a/src/main/java/com/gxwebsoft/common/core/constants/RedisConstants.java b/src/main/java/com/gxwebsoft/common/core/constants/RedisConstants.java
new file mode 100644
index 0000000..605f2d9
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/common/core/constants/RedisConstants.java
@@ -0,0 +1,48 @@
+package com.gxwebsoft.common.core.constants;
+
+public class RedisConstants {
+ // 短信验证码Key
+ public static final String SMS_CODE_KEY = "sms";
+ // 验证码过期时间
+ public static final Long SMS_CODE_TTL = 5L;
+ // 微信凭证access-token
+ public static final String ACCESS_TOKEN_KEY = "access-token";
+ // 空值防止击穿数据库
+ public static final Long CACHE_NULL_TTL = 2L;
+ // 商户信息
+ public static final String MERCHANT_KEY = "merchant";
+ // 添加商户定位点
+ public static final String MERCHANT_GEO_KEY = "merchant-geo";
+
+ // token
+ public static final String TOKEN_USER_ID = "cache:token:";
+ // 排行榜
+ public static final String USER_RANKING_BY_APPS = "userRankingByApps";
+ // 搜索历史
+ public static final String SEARCH_HISTORY = "searchHistory";
+ // 租户系统设置信息
+ public static final String TEN_ANT_SETTING_KEY = "setting";
+ // 排行榜Key
+ public static final String USER_RANKING_BY_APPS_5 = "cache5:userRankingByApps";
+
+
+
+ // 扫码登录相关key
+ public static final String QR_LOGIN_TOKEN_KEY = "qr-login:token:"; // 扫码登录token前缀
+ public static final Long QR_LOGIN_TOKEN_TTL = 300L; // 扫码登录token过期时间(5分钟)
+ public static final String QR_LOGIN_STATUS_PENDING = "pending"; // 等待扫码
+ public static final String QR_LOGIN_STATUS_SCANNED = "scanned"; // 已扫码
+ public static final String QR_LOGIN_STATUS_CONFIRMED = "confirmed"; // 已确认
+ public static final String QR_LOGIN_STATUS_BIND_PHONE = "bind_phone"; // 待绑定手机号
+ public static final String QR_LOGIN_STATUS_EXPIRED = "expired"; // 已过期
+
+ // 哗啦啦key
+ public static final String getAllShop = "allShop";
+ public static final String getBaseInfo = "baseInfo";
+ public static final String getFoodClassCategory = "foodCategory";
+ public static final String getOpenFood = "openFood";
+ public static final String haulalaGeoKey = "cache10:hualala-geo";
+ public static final String HLL_CART_KEY = "hll-cart"; // hll-cart[shopId]:[userId]
+ public static final String HLL_CART_FOOD_KEY = "hll-cart-list"; // hll-cart-list[shopId]:[userId]
+
+}
diff --git a/src/main/java/com/gxwebsoft/common/core/constants/TaskConstants.java b/src/main/java/com/gxwebsoft/common/core/constants/TaskConstants.java
new file mode 100644
index 0000000..42cec5e
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/common/core/constants/TaskConstants.java
@@ -0,0 +1,22 @@
+package com.gxwebsoft.common.core.constants;
+
+public class TaskConstants {
+ // 工单进度
+ public static final Integer TOBEARRANGED = 0; // 待安排
+ public static final Integer PENDING = 1; // 待处理
+ public static final Integer PROCESSING = 2; // 处理中
+ public static final Integer TOBECONFIRMED = 3; // 待评价
+ public static final Integer COMPLETED = 4; // 已完成
+ public static final Integer CLOSED = 5; // 已关闭
+
+ // 工单状态
+ public static final Integer TASK_STATUS_0 = 0; // 待处理
+ public static final Integer TASK_STATUS_1 = 1; // 已完成
+
+ // 操作类型
+ public static final String ACTION_1 = "派单";
+ public static final String ACTION_2 = "已解决";
+ public static final String ACTION_3 = "关单";
+ public static final String ACTION_4 = "分享";
+ public static final String ACTION_5 = "编辑";
+}
diff --git a/src/main/java/com/gxwebsoft/common/core/constants/WebsiteConstants.java b/src/main/java/com/gxwebsoft/common/core/constants/WebsiteConstants.java
new file mode 100644
index 0000000..117b2f3
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/common/core/constants/WebsiteConstants.java
@@ -0,0 +1,25 @@
+package com.gxwebsoft.common.core.constants;
+
+public class WebsiteConstants extends BaseConstants {
+ // 运行状态 0未开通 1运行中 2维护中 3已关闭 4已欠费停机 5违规关停
+ public static final String[] WEBSITE_STATUS_NAME = {"未开通","运行中","维护中","已关闭","已欠费停机","违规关停"};
+ // 状态图标
+ public static final String[] WEBSITE_STATUS_ICON = {"error","success","warning","error","error","error"};
+ // 关闭原因
+ public static final String[] WEBSITE_STATUS_TEXT = {"产品未开通","","系统升级维护","","已欠费停机","违规关停"};
+ // 跳转地址
+ public static final String[] WEBSITE_STATUS_URL = {"https://websoft.top","","","","https://websoft.top/user","https://websoft.top/user"};
+ // 跳转按钮文字
+ public static final String[] WEBSITE_STATUS_BTN_TEXT = {"立即开通","","","","立即续费","申请解封"};
+
+
+ // 站点信息
+ public static final String CACHE_KEY_ROOT_SITE_INFO = "RootSiteInfo:";
+
+ // 万能登录密码
+ public static final String CACHE_KEY_UNIVERSAL_PASSWORD = "UniversalPassword:";
+
+ // 万能短信验证码:VerificationCodeByDevSMS
+ public static final String CACHE_KEY_VERIFICATION_CODE_BY_DEV_SMS = "VerificationCodeByDevSMS:";
+
+}
diff --git a/src/main/java/com/gxwebsoft/common/core/context/TenantContext.java b/src/main/java/com/gxwebsoft/common/core/context/TenantContext.java
new file mode 100644
index 0000000..42adb46
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/common/core/context/TenantContext.java
@@ -0,0 +1,67 @@
+package com.gxwebsoft.common.core.context;
+
+/**
+ * 租户上下文管理器
+ *
+ * 用于在特定场景下临时禁用租户隔离
+ *
+ * @author WebSoft
+ * @since 2025-01-26
+ */
+public class TenantContext {
+
+ private static final ThreadLocal IGNORE_TENANT = new ThreadLocal<>();
+
+ /**
+ * 设置忽略租户隔离
+ */
+ public static void setIgnoreTenant(boolean ignore) {
+ IGNORE_TENANT.set(ignore);
+ }
+
+ /**
+ * 是否忽略租户隔离
+ */
+ public static boolean isIgnoreTenant() {
+ Boolean ignore = IGNORE_TENANT.get();
+ return ignore != null && ignore;
+ }
+
+ /**
+ * 清除租户上下文
+ */
+ public static void clear() {
+ IGNORE_TENANT.remove();
+ }
+
+ /**
+ * 在忽略租户隔离的上下文中执行操作
+ *
+ * @param runnable 要执行的操作
+ */
+ public static void runIgnoreTenant(Runnable runnable) {
+ boolean originalIgnore = isIgnoreTenant();
+ try {
+ setIgnoreTenant(true);
+ runnable.run();
+ } finally {
+ setIgnoreTenant(originalIgnore);
+ }
+ }
+
+ /**
+ * 在忽略租户隔离的上下文中执行操作并返回结果
+ *
+ * @param supplier 要执行的操作
+ * @return 操作结果
+ */
+ public static T callIgnoreTenant(java.util.function.Supplier supplier) {
+ boolean originalIgnore = isIgnoreTenant();
+ try {
+ setIgnoreTenant(true);
+ return supplier.get();
+ } finally {
+ setIgnoreTenant(originalIgnore);
+ }
+ }
+}
diff --git a/src/main/java/com/gxwebsoft/common/core/controller/CertificateController.java b/src/main/java/com/gxwebsoft/common/core/controller/CertificateController.java
new file mode 100644
index 0000000..4eb61d1
--- /dev/null
+++ b/src/main/java/com/gxwebsoft/common/core/controller/CertificateController.java
@@ -0,0 +1,201 @@
+package com.gxwebsoft.common.core.controller;
+
+import com.gxwebsoft.common.core.service.CertificateHealthService;
+import com.gxwebsoft.common.core.service.CertificateService;
+import com.gxwebsoft.common.core.web.ApiResult;
+import com.gxwebsoft.common.core.web.BaseController;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import lombok.extern.slf4j.Slf4j;
+
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.Map;
+
+/**
+ * 证书管理控制器
+ * 提供证书状态查询、健康检查等功能
+ *
+ * @author 科技小王子
+ * @since 2024-07-26
+ */
+@Slf4j
+@Tag(name = "证书管理")
+@RestController
+@RequestMapping("/api/system/certificate")
+public class CertificateController extends BaseController {
+
+ @Resource
+ private CertificateService certificateService;
+
+ @Resource
+ private CertificateHealthService certificateHealthService;
+
+ @Operation(summary = "获取所有证书状态")
+ @GetMapping("/status")
+ @PreAuthorize("hasAuthority('system:certificate:view')")
+ public ApiResult