Compare commits
57 Commits
fc98ee0509
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| e487f021dc | |||
| 5ca3280ddc | |||
| b9ec1fcdb9 | |||
| a6a2e14abf | |||
| 18696f91a6 | |||
| 49e6cd2197 | |||
| f7f7b30813 | |||
| b043270e52 | |||
| f720adc2a8 | |||
| 2e51710e3a | |||
| 644188288a | |||
| d532f6717f | |||
| 4612155457 | |||
| 907439044a | |||
| 39d3c4c894 | |||
| c4177f167d | |||
| 2581a77f09 | |||
| a8896e4523 | |||
| 5f6df3dc26 | |||
| 3bacfe8b8f | |||
| 9fdc708bb8 | |||
| a02521e64b | |||
| 3ffd3255ee | |||
| 14f49e43dd | |||
| 73f0878fb2 | |||
| 34c6076068 | |||
| cb2f856ec7 | |||
| ac166e340d | |||
| 61d8c6317e | |||
| 7217325756 | |||
| c60cf13dd0 | |||
| 6bb36b1554 | |||
| dbe4f2962d | |||
| eb9bfd167e | |||
| 18db0f7d44 | |||
| b2487572a6 | |||
| 592345018e | |||
| e20503269d | |||
| 7f53b39c56 | |||
| 89571c81a4 | |||
| aa287ba655 | |||
| 4220de534a | |||
| 5e9dc385f5 | |||
| ba0e78d4c2 | |||
| aa7f3f7a6e | |||
| ce73d14d1c | |||
| c0c140fcdf | |||
| 54ee652584 | |||
| f8595dd2d5 | |||
| d49617a547 | |||
| 4ba69b2ce2 | |||
| b119a92d94 | |||
| 81ac636fb1 | |||
| 0aa65d0195 | |||
| 09ef6558ae | |||
| bb34831208 | |||
| b8c147c641 |
16
.workbuddy/memory/2026-06-28.md
Normal file
16
.workbuddy/memory/2026-06-28.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# 2026-06-28 工作记录
|
||||||
|
|
||||||
|
## AliOssController 上传接口优化
|
||||||
|
|
||||||
|
**文件**: `src/main/java/com/gxwebsoft/common/system/controller/AliOssController.java`
|
||||||
|
|
||||||
|
**问题**: 上传文件时,无论文件类型如何,都无条件拼接 `?x-oss-process=image/...` 图片处理参数,导致非图片文件的 thumbnail/url 无效。
|
||||||
|
|
||||||
|
**修改内容**:
|
||||||
|
1. 将 `FileServerUtil.getContentType(upload)` 调用移到设置 thumbnail/url 之前
|
||||||
|
2. 根据 contentType 判断文件类型,分别处理:
|
||||||
|
- **图片** (`image/*`): 照常拼接 OSS 图片处理参数(缩略图 100x100 + 预览图 750px宽)
|
||||||
|
- **视频** (`video/*`): 使用 OSS 视频截帧 `?x-oss-process=video/snapshot,t_0,f_jpg,w_100,h_100,m_fast` 生成缩略图
|
||||||
|
- **其他文件**: thumbnail 和 url 直接设为原始文件 URL,不加任何处理参数
|
||||||
|
|
||||||
|
**依赖**: 使用了已有的 `FileServerUtil.isImage(String contentType)` 方法判断图片类型。
|
||||||
15
.workbuddy/memory/2026-06-29.md
Normal file
15
.workbuddy/memory/2026-06-29.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# 2026-06-29
|
||||||
|
|
||||||
|
## fix: UserMapper.xml LEFT JOIN sys_company 导致分页查询记录重复
|
||||||
|
|
||||||
|
- 问题:`SELECT * FROM sys_user WHERE is_developer = 1` 只有5条记录,但 `/api/system/user/page?isDeveloper=true` 返回多条重复记录
|
||||||
|
- 原因:`LEFT JOIN sys_company g ON g.tenant_id = t.tenant_id` 中,一个 tenant_id 可能在 sys_company 中有多条公司记录,产生笛卡尔积
|
||||||
|
- 修复:将 `LEFT JOIN sys_company` 改为子查询,按 `tenant_id` 分组取第一条记录
|
||||||
|
```sql
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT tenant_id, MAX(company_name) as company_name, MAX(company_logo) as company_logo
|
||||||
|
FROM sys_company
|
||||||
|
GROUP BY tenant_id
|
||||||
|
) g ON g.tenant_id = t.tenant_id
|
||||||
|
```
|
||||||
|
- 同步修改了 `target/classes/` 下的编译版本
|
||||||
11
.workbuddy/memory/2026-07-11.md
Normal file
11
.workbuddy/memory/2026-07-11.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# 2026-07-11 工作日志
|
||||||
|
|
||||||
|
## 补充用户审核查询接口
|
||||||
|
- **问题**:`GET /api/system/user/audit/{userId}` 返回404,接口不存在
|
||||||
|
- **原因**:只有 `PUT /api/system/user/audit`(执行审核操作),缺少查询用户审核信息的GET接口
|
||||||
|
- **修复**:在 `UserController.java` 中新增 `getAuditInfo` 方法,返回 userId、auditStatus、rejectReason
|
||||||
|
|
||||||
|
## 修复前端审核接口请求方法不匹配
|
||||||
|
- **问题**:`Request method 'POST' not supported` 报错
|
||||||
|
- **原因**:前端 `auditUser` API 用 POST + 路径参数 `/audit/{id}`,后端是 PUT + body 参数 `/audit`
|
||||||
|
- **修复**:修改 `guilixu-admin/src/api/system/user/index.ts`,改为 PUT 请求、去掉路径参数、字段名映射(status→auditStatus, remark→rejectReason)、userId 放入 body
|
||||||
28
.workbuddy/memory/2026-07-21.md
Normal file
28
.workbuddy/memory/2026-07-21.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# 2026-07-21 工作记录
|
||||||
|
|
||||||
|
## 邀请成为开发者报错分析(UID:35739)
|
||||||
|
- 报错「该用户不是平台开发者,无法邀请为开发者角色」来源:`websopy-java` 项目(非 com.gxwebsoft.core)。
|
||||||
|
- 拦截点共 3 处,逻辑一致:邀请角色为 `developer` 时,跨库查 `gxwebsoft_core.sys_user.is_developer`,必须为 `1` 才放行:
|
||||||
|
- `com/gxwebsoft/app/service/impl/AppUserServiceImpl.java:168-172`(API: POST /api/app/developer/.../invite)
|
||||||
|
- `com/gxwebsoft/app/service/impl/AppInviteServiceImpl.java:257-261`
|
||||||
|
- `com/gxwebsoft/app/controller/AppMpInviteController.java:179-183`(小程序邀请)
|
||||||
|
- 「平台开发者」定义:
|
||||||
|
- `sys_user.type = 2`(见 AppUserController /check-access 判断)
|
||||||
|
- `sys_user.is_developer = 1`(邀请 developer 角色的硬性门槛,由 SysUserCrossDbMapper.selectIsDeveloperByUserId 查询)
|
||||||
|
- 关键发现:websopy-java 与 com.gxwebsoft.core 后端代码中**没有任何写入 is_developer=1 / type=2 的逻辑**,即「成为开发者」的赋权由 websopy-pc 前端开发者门户注册/认证流程或数据库操作完成(后端仅做读取校验)。
|
||||||
|
- 结论:UID:35739 的 `sys_user.is_developer` 为 0 或 NULL → 报错。需先让其完成「成为开发者」注册使 is_developer=1(并建议同步 type=2)。
|
||||||
|
- 潜在 Bug:邀请门槛用 is_developer,而 /check-access 用 type=2。若两字段不一致,会出现「能进开发者中心但无法被邀请为开发者」的矛盾。
|
||||||
|
|
||||||
|
## 统一开发者身份判定为 is_developer=1(已实施)
|
||||||
|
- 用户选定方案:把开发者身份判定统一为「sys_user.is_developer = 1」。
|
||||||
|
- 改动(websopy-java 项目):
|
||||||
|
- `AppUserController.checkAccess()`:原 `selectUserType()==2` → 改为 `selectIsDeveloperByUserId()==1`,同步更新 Javadoc。
|
||||||
|
- `AppProductServiceImpl` 转让应用所有权校验:原 `selectUserType()==2` → 改为 `selectIsDeveloperByUserId()==1`。
|
||||||
|
- 三处邀请门槛(AppUserServiceImpl / AppInviteServiceImpl / AppMpInviteController)本就使用 is_developer=1,无需改。
|
||||||
|
- 结果:全站「平台开发者」判定统一为 is_developer=1 单一标准。
|
||||||
|
- 注意:此变更使 is_developer 成为唯一依据;存量仅 type=2、无 is_developer 的开发者将失去权限。需数据回填:`UPDATE gxwebsoft_core.sys_user SET is_developer=1 WHERE type=2 AND deleted=0;`(先备份)。UID:35739 仍需 is_developer=1 才能被邀请。
|
||||||
|
|
||||||
|
## Tenant 实体补充 superAdminUserId 字段
|
||||||
|
- 用户已在 `TenantMapper.xml` 的 `selectSql` 中 SELECT `u.user_id as superAdminUserId`(LEFT JOIN gxwebsoft_core.sys_user u ON u.tenant_id=a.tenant_id AND u.is_super_admin=1 AND u.deleted=0)。
|
||||||
|
- 在 `Tenant.java` 中补充 `private Integer superAdminUserId;` 并标注 `@TableField(exist = false)`,使 MyBatis 能映射该衍生字段(不是 sys_tenant 真实列,不参与增改)。
|
||||||
|
- `selectPageRel` / `selectListRel` 用 resultType=Tenant,可直接返回该字段。
|
||||||
13
.workbuddy/memory/2026-07-23.md
Normal file
13
.workbuddy/memory/2026-07-23.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# 2026-07-23 工作记录
|
||||||
|
|
||||||
|
## 短信验证码接口 sendSmsCaptchaByAdmin 发送失败排查
|
||||||
|
- 现象:线上 `https://site.websoft.top/api/_server/sendSmsCaptchaByAdmin` 调用手机 15678895934 失败,返回 `401 请先登录`。
|
||||||
|
- 根因:`com/gxwebsoft/common/core/security/SecurityConfig.java` 的 `permitAll` 白名单里只有 `/api/sendSmsCaptcha`、`/api/sendEmailCaptcha`,没有 `/api/sendSmsCaptchaByAdmin`。该接口被 Spring Security 拦截,未登录请求直接 401,控制器逻辑未执行,验证码未发出。
|
||||||
|
- 已修复:在 SecurityConfig 的 permitAll antMatchers 中加入 `/api/sendSmsCaptchaByAdmin`(位于 `/api/sendSmsCaptcha` 之后)。
|
||||||
|
- 说明:接口内部已有 `getByPhoneAndAdmin` 管理员手机号校验,开放 permitAll 安全;线上代理前缀为 `/api/_server/**`,转发到后端 `/api/**`。
|
||||||
|
- 验证方式:用 curl 直接打 `/api/_server/sendSmsCaptchaByAdmin` 复现 401;同前缀的 `/api/sendSmsCaptcha` 因在白名单可正常进入(需确认部署后 ByAdmin 不再 401)。
|
||||||
|
|
||||||
|
## 关键文件
|
||||||
|
- SecurityConfig.java:`com/gxwebsoft/common/core/security/SecurityConfig.java`
|
||||||
|
- MainController.sendSmsCaptchaByAdmin:`com/gxwebsoft/common/system/controller/MainController.java:656`
|
||||||
|
- 管理员校验 SQL:`UserMapper.xml selectAdminByPhoneCrossTenant`(sys_user.deleted=0 AND phone=? AND (is_super_admin=1 OR is_admin=1))
|
||||||
32
pom.xml
32
pom.xml
@@ -26,7 +26,31 @@
|
|||||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
|
<dependencyManagement>
|
||||||
|
<dependencies>
|
||||||
|
<!-- 强制 okhttp/okio 版本,避免 minio-java(要求 okhttp>=4.11.0) 与微信/支付 SDK 引入的 okhttp3 旧版冲突 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.squareup.okhttp3</groupId>
|
||||||
|
<artifactId>okhttp</artifactId>
|
||||||
|
<version>4.12.0</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.squareup.okio</groupId>
|
||||||
|
<artifactId>okio</artifactId>
|
||||||
|
<version>3.6.0</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</dependencyManagement>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
<!-- 强制 okhttp 4.12.0:minio-java 要求 okhttp>=4.11.0,放在 dependencies 首位确保胜出,
|
||||||
|
避免运行时 NoSuchMethodError(okhttp3.RequestBody.create 等) -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.squareup.okhttp3</groupId>
|
||||||
|
<artifactId>okhttp</artifactId>
|
||||||
|
<version>4.12.0</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- spring-boot-devtools -->
|
<!-- spring-boot-devtools -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
@@ -297,6 +321,14 @@
|
|||||||
<version>3.17.4</version>
|
<version>3.17.4</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- MinIO 官方 Java SDK:用于对接 MinIO 兼容 S3 存储。
|
||||||
|
注意 aliyun OSS SDK 无 AWS4-HMAC-SHA256 签名器,无法对接 MinIO,故 minio 分支单独用此 SDK。 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.minio</groupId>
|
||||||
|
<artifactId>minio</artifactId>
|
||||||
|
<version>8.5.7</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- 阿里云 内容安全审核 -->
|
<!-- 阿里云 内容安全审核 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.aliyun</groupId>
|
<groupId>com.aliyun</groupId>
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ public class RedisConstants {
|
|||||||
public static final String SMS_CODE_KEY = "sms";
|
public static final String SMS_CODE_KEY = "sms";
|
||||||
// 验证码过期时间
|
// 验证码过期时间
|
||||||
public static final Long SMS_CODE_TTL = 5L;
|
public static final Long SMS_CODE_TTL = 5L;
|
||||||
|
// 邮箱验证码Key
|
||||||
|
public static final String EMAIL_CODE_KEY = "emailCode";
|
||||||
|
// 邮箱验证码过期时间(分钟)
|
||||||
|
public static final Long EMAIL_CODE_TTL = 5L;
|
||||||
// 微信凭证access-token
|
// 微信凭证access-token
|
||||||
public static final String ACCESS_TOKEN_KEY = "access-token";
|
public static final String ACCESS_TOKEN_KEY = "access-token";
|
||||||
// 空值防止击穿数据库
|
// 空值防止击穿数据库
|
||||||
|
|||||||
@@ -41,11 +41,13 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
|||||||
"/api/login",
|
"/api/login",
|
||||||
"/api/qr-login/**",
|
"/api/qr-login/**",
|
||||||
"/api/loginByUserId",
|
"/api/loginByUserId",
|
||||||
|
"/api/loginByEmail",
|
||||||
"/api/register",
|
"/api/register",
|
||||||
"/api/superAdminRegister",
|
"/api/superAdminRegister",
|
||||||
"/api/findAccountByPhone",
|
"/api/findAccountByPhone",
|
||||||
"/api/resetPassword",
|
"/api/resetPassword",
|
||||||
"/api/checkPhoneRegistered",
|
"/api/checkPhoneRegistered",
|
||||||
|
"/api/checkEmailRegistered",
|
||||||
"/api/existence",
|
"/api/existence",
|
||||||
"/api/oss/upload",
|
"/api/oss/upload",
|
||||||
"/druid/**",
|
"/druid/**",
|
||||||
@@ -53,8 +55,12 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
|||||||
"/webjars/**",
|
"/webjars/**",
|
||||||
"/hxz/v1/**",
|
"/hxz/v1/**",
|
||||||
"/api/sendSmsCaptcha",
|
"/api/sendSmsCaptcha",
|
||||||
|
"/api/sendSmsCaptchaByAdmin",
|
||||||
|
"/api/sendEmailCaptcha",
|
||||||
|
"/api/verifyEmailCaptcha",
|
||||||
"/api/loginBySms",
|
"/api/loginBySms",
|
||||||
"/api/loginBySuperAdminSms",
|
"/api/loginBySuperAdminSms",
|
||||||
|
"/api/loginBySelectTenant",
|
||||||
"/api/loginByDeveloperSms",
|
"/api/loginByDeveloperSms",
|
||||||
"/api/system/user/regByPhone",
|
"/api/system/user/regByPhone",
|
||||||
"/api/parseToken/*",
|
"/api/parseToken/*",
|
||||||
|
|||||||
@@ -290,4 +290,20 @@ public class CommonUtil {
|
|||||||
return pattern.matcher(phoneNumber).matches();
|
return pattern.matcher(phoneNumber).matches();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验邮箱格式是否有效
|
||||||
|
*
|
||||||
|
* @param email 要验证的邮箱字符串
|
||||||
|
* @return 如果字符串是有效的邮箱地址,则返回true;否则返回false
|
||||||
|
*/
|
||||||
|
public static boolean isValidEmail(String email) {
|
||||||
|
if (email == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// 邮箱格式正则:本地部分@域名.顶级域
|
||||||
|
String regex = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$";
|
||||||
|
Pattern pattern = Pattern.compile(regex);
|
||||||
|
return pattern.matcher(email).matches();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import com.aliyun.oss.ClientException;
|
|||||||
import com.aliyun.oss.OSS;
|
import com.aliyun.oss.OSS;
|
||||||
import com.aliyun.oss.OSSClientBuilder;
|
import com.aliyun.oss.OSSClientBuilder;
|
||||||
import com.aliyun.oss.OSSException;
|
import com.aliyun.oss.OSSException;
|
||||||
|
import com.aliyun.oss.ClientBuilderConfiguration;
|
||||||
|
import com.aliyun.oss.common.comm.SignVersion;
|
||||||
import com.aliyun.oss.common.auth.CredentialsProvider;
|
import com.aliyun.oss.common.auth.CredentialsProvider;
|
||||||
import com.aliyun.oss.common.auth.DefaultCredentialProvider;
|
import com.aliyun.oss.common.auth.DefaultCredentialProvider;
|
||||||
import com.aliyun.oss.common.utils.BinaryUtil;
|
import com.aliyun.oss.common.utils.BinaryUtil;
|
||||||
@@ -35,6 +37,8 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
|||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
import io.minio.MinioClient;
|
||||||
|
import io.minio.UploadObjectArgs;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
@@ -96,6 +100,12 @@ public class AliOssController extends BaseController {
|
|||||||
String bucketName = settingInfo.getString("bucketName");
|
String bucketName = settingInfo.getString("bucketName");
|
||||||
String accessKeyId = settingInfo.getString("accessKeyId");
|
String accessKeyId = settingInfo.getString("accessKeyId");
|
||||||
String accessKeySecret = settingInfo.getString("accessKeySecret");
|
String accessKeySecret = settingInfo.getString("accessKeySecret");
|
||||||
|
String uploadMethod = settingInfo.getString("uploadMethod");
|
||||||
|
|
||||||
|
// 规范化 endpoint:去掉结尾斜杠,避免 minio-java 拼接出双斜杠导致签名不匹配
|
||||||
|
if (StrUtil.isNotBlank(endpoint) && endpoint.endsWith("/")) {
|
||||||
|
endpoint = endpoint.substring(0, endpoint.length() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
// 判断是否登录
|
// 判断是否登录
|
||||||
String authorization = getAuthorization();
|
String authorization = getAuthorization();
|
||||||
@@ -112,9 +122,23 @@ public class AliOssController extends BaseController {
|
|||||||
|
|
||||||
// 上传文件结果
|
// 上传文件结果
|
||||||
FileRecord result;
|
FileRecord result;
|
||||||
CredentialsProvider credentialsProvider = new DefaultCredentialProvider(accessKeyId, accessKeySecret);
|
boolean isMinio = "minio".equals(uploadMethod);
|
||||||
// 创建OSSClient实例。
|
// MinIO 用官方 minio-java SDK(原生 AWS4-HMAC-SHA256 + path-style);
|
||||||
OSS ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider);
|
// aliyun OSS SDK 无 AWS4 签名器,对接不了 MinIO,因此 minio 分支独立使用 MinioClient。
|
||||||
|
OSS ossClient = null;
|
||||||
|
MinioClient minioClient = null;
|
||||||
|
if (isMinio) {
|
||||||
|
// 显式指定 region=us-east-1,跳过 minio-java 的 getBucketLocation 自动探测
|
||||||
|
// (该探测 GET 在 MinIO + 反代下会触发 SignatureDoesNotMatch)
|
||||||
|
minioClient = MinioClient.builder()
|
||||||
|
.endpoint(endpoint)
|
||||||
|
.credentials(accessKeyId, accessKeySecret)
|
||||||
|
.region("us-east-1")
|
||||||
|
.build();
|
||||||
|
} else {
|
||||||
|
CredentialsProvider credentialsProvider = new DefaultCredentialProvider(accessKeyId, accessKeySecret);
|
||||||
|
ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
@@ -123,16 +147,18 @@ public class AliOssController extends BaseController {
|
|||||||
String path = upload.getAbsolutePath().replace("\\", "/").substring(dir.length());
|
String path = upload.getAbsolutePath().replace("\\", "/").substring(dir.length());
|
||||||
String originalName = file.getOriginalFilename();
|
String originalName = file.getOriginalFilename();
|
||||||
|
|
||||||
// 创建PutObjectRequest对象。
|
// 上传文件到对象存储。
|
||||||
PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, path, upload);
|
if (isMinio) {
|
||||||
// 如果需要上传时设置存储类型和访问权限,请参考以下示例代码。
|
// minio-java:path 此时无前导 "/",作为对象名(如 2026/08/07/xxx.png),默认 region=us-east-1
|
||||||
// ObjectMetadata metadata = new ObjectMetadata();
|
minioClient.uploadObject(UploadObjectArgs.builder()
|
||||||
// metadata.setHeader(OSSHeaders.OSS_STORAGE_CLASS, StorageClass.Standard.toString());
|
.bucket(bucketName)
|
||||||
// metadata.setObjectAcl(CannedAccessControlList.Private);
|
.object(path)
|
||||||
// putObjectRequest.setMetadata(metadata);
|
.filename(upload.getAbsolutePath())
|
||||||
|
.build());
|
||||||
// 上传文件。
|
} else {
|
||||||
PutObjectResult ossResult = ossClient.putObject(putObjectRequest);
|
PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, path, upload);
|
||||||
|
ossClient.putObject(putObjectRequest);
|
||||||
|
}
|
||||||
|
|
||||||
// 保存记录并返回
|
// 保存记录并返回
|
||||||
result = new FileRecord();
|
result = new FileRecord();
|
||||||
@@ -155,11 +181,39 @@ public class AliOssController extends BaseController {
|
|||||||
result.setName(StrUtil.isBlank(originalName) ? upload.getName() : originalName);
|
result.setName(StrUtil.isBlank(originalName) ? upload.getName() : originalName);
|
||||||
result.setLength(upload.length());
|
result.setLength(upload.length());
|
||||||
result.setPath(bucketDomain + path);
|
result.setPath(bucketDomain + path);
|
||||||
result.setThumbnail(bucketDomain + path + "?x-oss-process=image/resize,m_fixed,w_100,h_100/quality,Q_90");
|
|
||||||
result.setUrl(bucketDomain + path + "?x-oss-process=image/resize,w_750/quality,Q_90");
|
|
||||||
result.setDownloadUrl(bucketDomain + path);
|
result.setDownloadUrl(bucketDomain + path);
|
||||||
|
|
||||||
|
// 获取文件类型
|
||||||
String contentType = FileServerUtil.getContentType(upload);
|
String contentType = FileServerUtil.getContentType(upload);
|
||||||
result.setContentType(contentType);
|
result.setContentType(contentType);
|
||||||
|
|
||||||
|
// 根据文件类型设置缩略图和预览图
|
||||||
|
// MinIO 不支持阿里云 x-oss-process 图片处理参数,统一返回原图 URL
|
||||||
|
isMinio = "minio".equals(uploadMethod);
|
||||||
|
if (FileServerUtil.isImage(contentType)) {
|
||||||
|
if (isMinio) {
|
||||||
|
result.setThumbnail(bucketDomain + path);
|
||||||
|
result.setUrl(bucketDomain + path);
|
||||||
|
} else {
|
||||||
|
// 图片:生成缩略图和压缩预览图
|
||||||
|
result.setThumbnail(bucketDomain + path + "?x-oss-process=image/resize,m_fixed,w_100,h_100/quality,Q_90");
|
||||||
|
result.setUrl(bucketDomain + path + "?x-oss-process=image/resize,w_750/quality,Q_90");
|
||||||
|
}
|
||||||
|
} else if (contentType != null && contentType.startsWith("video/")) {
|
||||||
|
if (isMinio) {
|
||||||
|
result.setThumbnail(bucketDomain + path);
|
||||||
|
result.setUrl(bucketDomain + path);
|
||||||
|
} else {
|
||||||
|
// 视频:使用OSS视频截帧生成缩略图(取首帧)
|
||||||
|
result.setThumbnail(bucketDomain + path + "?x-oss-process=video/snapshot,t_0,f_jpg,w_100,h_100,m_fast");
|
||||||
|
result.setUrl(bucketDomain + path);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 非图片/视频:直接使用原始URL
|
||||||
|
result.setThumbnail(bucketDomain + path);
|
||||||
|
result.setUrl(bucketDomain + path);
|
||||||
|
}
|
||||||
|
|
||||||
result.setTenantId(Integer.valueOf(tenantId));
|
result.setTenantId(Integer.valueOf(tenantId));
|
||||||
upload.delete();
|
upload.delete();
|
||||||
fileRecordService.save(result);
|
fileRecordService.save(result);
|
||||||
@@ -186,10 +240,15 @@ public class AliOssController extends BaseController {
|
|||||||
+ "a serious internal problem while trying to communicate with OSS, "
|
+ "a serious internal problem while trying to communicate with OSS, "
|
||||||
+ "such as not being able to access the network.");
|
+ "such as not being able to access the network.");
|
||||||
System.out.println("Error Message:" + ce.getMessage());
|
System.out.println("Error Message:" + ce.getMessage());
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 兼容 minio-java 抛出的异常(MinioException / IOException 等)
|
||||||
|
System.out.println("Caught an upload Exception: " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
} finally {
|
} finally {
|
||||||
if (ossClient != null) {
|
if (ossClient != null) {
|
||||||
ossClient.shutdown();
|
ossClient.shutdown();
|
||||||
}
|
}
|
||||||
|
// MinioClient 内部复用 HttpClient 连接池,无需显式关闭
|
||||||
}
|
}
|
||||||
return fail("上传失败", null);
|
return fail("上传失败", null);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,11 +61,11 @@ public class DictDataController extends BaseController {
|
|||||||
.eq(DictData::getDictDataName, dictData.getDictDataName())) > 0) {
|
.eq(DictData::getDictDataName, dictData.getDictDataName())) > 0) {
|
||||||
return fail("字典数据名称已存在");
|
return fail("字典数据名称已存在");
|
||||||
}
|
}
|
||||||
if (dictDataService.count(new LambdaQueryWrapper<DictData>()
|
// if (dictDataService.count(new LambdaQueryWrapper<DictData>()
|
||||||
.eq(DictData::getDictId, dictData.getDictId())
|
// .eq(DictData::getDictId, dictData.getDictId())
|
||||||
.eq(DictData::getDictDataCode, dictData.getDictDataCode())) > 0) {
|
// .eq(DictData::getDictDataCode, dictData.getDictDataCode())) > 0) {
|
||||||
return fail("字典数据标识已存在");
|
// return fail("字典数据标识已存在");
|
||||||
}
|
// }
|
||||||
// 自动添加字典
|
// 自动添加字典
|
||||||
final int count = dictService.count(new LambdaQueryWrapper<Dict>().eq(Dict::getDictCode, dictData.getDictCode()));
|
final int count = dictService.count(new LambdaQueryWrapper<Dict>().eq(Dict::getDictCode, dictData.getDictCode()));
|
||||||
if (dictData.getDictCode() != null && count == 0) {
|
if (dictData.getDictCode() != null && count == 0) {
|
||||||
@@ -95,12 +95,12 @@ public class DictDataController extends BaseController {
|
|||||||
.ne(DictData::getDictDataId, dictData.getDictDataId())) > 0) {
|
.ne(DictData::getDictDataId, dictData.getDictDataId())) > 0) {
|
||||||
return fail("字典数据名称已存在");
|
return fail("字典数据名称已存在");
|
||||||
}
|
}
|
||||||
if (dictDataService.count(new LambdaQueryWrapper<DictData>()
|
// if (dictDataService.count(new LambdaQueryWrapper<DictData>()
|
||||||
.eq(DictData::getDictId, dictData.getDictId())
|
// .eq(DictData::getDictId, dictData.getDictId())
|
||||||
.eq(DictData::getDictDataCode, dictData.getDictDataCode())
|
// .eq(DictData::getDictDataCode, dictData.getDictDataCode())
|
||||||
.ne(DictData::getDictDataId, dictData.getDictDataId())) > 0) {
|
// .ne(DictData::getDictDataId, dictData.getDictDataId())) > 0) {
|
||||||
return fail("字典数据标识已存在");
|
// return fail("字典数据标识已存在");
|
||||||
}
|
// }
|
||||||
if (dictDataService.updateById(dictData)) {
|
if (dictDataService.updateById(dictData)) {
|
||||||
return success("修改成功");
|
return success("修改成功");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,15 +33,21 @@ import com.gxwebsoft.common.system.entity.*;
|
|||||||
import com.gxwebsoft.common.system.mapper.CompanyMapper;
|
import com.gxwebsoft.common.system.mapper.CompanyMapper;
|
||||||
import com.gxwebsoft.common.system.param.LoginParam;
|
import com.gxwebsoft.common.system.param.LoginParam;
|
||||||
import com.gxwebsoft.common.system.param.SmsCaptchaParam;
|
import com.gxwebsoft.common.system.param.SmsCaptchaParam;
|
||||||
|
import com.gxwebsoft.common.system.param.ResetPayPasswordParam;
|
||||||
|
import com.gxwebsoft.common.system.param.EmailCaptchaParam;
|
||||||
|
import com.gxwebsoft.common.core.constants.RedisConstants;
|
||||||
import com.gxwebsoft.common.system.param.FindAccountByPhoneParam;
|
import com.gxwebsoft.common.system.param.FindAccountByPhoneParam;
|
||||||
import com.gxwebsoft.common.system.param.ResetPasswordParam;
|
import com.gxwebsoft.common.system.param.ResetPasswordParam;
|
||||||
import com.gxwebsoft.common.system.param.UpdatePasswordParam;
|
import com.gxwebsoft.common.system.param.UpdatePasswordParam;
|
||||||
import com.gxwebsoft.common.system.param.UserParam;
|
import com.gxwebsoft.common.system.param.UserParam;
|
||||||
import com.gxwebsoft.common.system.result.CaptchaResult;
|
import com.gxwebsoft.common.system.result.CaptchaResult;
|
||||||
import com.gxwebsoft.common.system.result.LoginResult;
|
import com.gxwebsoft.common.system.result.LoginResult;
|
||||||
|
import com.gxwebsoft.common.system.result.TenantOption;
|
||||||
import com.gxwebsoft.common.system.result.AccountInfoResult;
|
import com.gxwebsoft.common.system.result.AccountInfoResult;
|
||||||
|
import com.gxwebsoft.common.system.result.CheckEmailResult;
|
||||||
import com.gxwebsoft.common.system.result.CheckPhoneResult;
|
import com.gxwebsoft.common.system.result.CheckPhoneResult;
|
||||||
import com.gxwebsoft.common.system.service.*;
|
import com.gxwebsoft.common.system.service.*;
|
||||||
|
import com.gxwebsoft.common.system.mapper.UserMapper;
|
||||||
import com.gxwebsoft.common.system.util.EmailTemplateUtil;
|
import com.gxwebsoft.common.system.util.EmailTemplateUtil;
|
||||||
import com.wf.captcha.SpecCaptcha;
|
import com.wf.captcha.SpecCaptcha;
|
||||||
import io.jsonwebtoken.Claims;
|
import io.jsonwebtoken.Claims;
|
||||||
@@ -60,8 +66,10 @@ import java.net.URLEncoder;
|
|||||||
import java.text.MessageFormat;
|
import java.text.MessageFormat;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.ThreadLocalRandom;
|
import java.util.concurrent.ThreadLocalRandom;
|
||||||
|
|
||||||
@@ -84,6 +92,8 @@ public class MainController extends BaseController {
|
|||||||
@Resource
|
@Resource
|
||||||
private UserService userService;
|
private UserService userService;
|
||||||
@Resource
|
@Resource
|
||||||
|
private UserMapper userMapper;
|
||||||
|
@Resource
|
||||||
private RoleMenuService roleMenuService;
|
private RoleMenuService roleMenuService;
|
||||||
@Resource
|
@Resource
|
||||||
private LoginRecordService loginRecordService;
|
private LoginRecordService loginRecordService;
|
||||||
@@ -237,6 +247,77 @@ public class MainController extends BaseController {
|
|||||||
return success("登录成功", new LoginResult(access_token, user));
|
return success("登录成功", new LoginResult(access_token, user));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "邮箱登录(支持跨租户)")
|
||||||
|
@PostMapping("/loginByEmail")
|
||||||
|
public ApiResult<LoginResult> loginByEmail(@RequestBody LoginParam param, HttpServletRequest request) {
|
||||||
|
// 设置过期时间
|
||||||
|
Long tokenExpireTime = configProperties.getTokenExpireTime();
|
||||||
|
final String email = param.getEmail();
|
||||||
|
final String password = param.getPassword();
|
||||||
|
if (StrUtil.isBlank(email) || StrUtil.isBlank(password)) {
|
||||||
|
return fail("参数不能为空",null);
|
||||||
|
}
|
||||||
|
// 跨租户:按邮箱查询所有租户下的账号
|
||||||
|
List<User> users = userService.findAccountsByEmail(email);
|
||||||
|
if (users == null || users.isEmpty()) {
|
||||||
|
String message = "用户不存在";
|
||||||
|
loginRecordService.saveAsync(email, LoginRecord.TYPE_ERROR, message, null, request);
|
||||||
|
return fail(message, null);
|
||||||
|
}
|
||||||
|
User user;
|
||||||
|
if (users.size() == 1) {
|
||||||
|
user = users.get(0);
|
||||||
|
} else {
|
||||||
|
// 同一邮箱在多个租户下存在,需用户选择具体租户
|
||||||
|
if (param.getTenantId() == null) {
|
||||||
|
User first = users.get(0);
|
||||||
|
first.setHasAdminsByPhone(true);
|
||||||
|
return success("请选择登录用户", new LoginResult(null, first));
|
||||||
|
}
|
||||||
|
user = users.stream()
|
||||||
|
.filter(u -> u.getTenantId().equals(param.getTenantId()))
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null);
|
||||||
|
if (user == null) {
|
||||||
|
return fail("用户不存在", null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!user.getStatus().equals(0)) {
|
||||||
|
String message = "账号被冻结";
|
||||||
|
loginRecordService.saveAsync(email, LoginRecord.TYPE_ERROR, message, user.getTenantId(), request);
|
||||||
|
return fail(message, null);
|
||||||
|
}
|
||||||
|
if (!userService.comparePassword(user.getPassword(), password)) {
|
||||||
|
String message = "密码错误";
|
||||||
|
loginRecordService.saveAsync(email, LoginRecord.TYPE_ERROR, message, user.getTenantId(), request);
|
||||||
|
return fail(message, null);
|
||||||
|
}
|
||||||
|
// 登录成功
|
||||||
|
loginRecordService.saveAsync(email, LoginRecord.TYPE_LOGIN, null, user.getTenantId(), request);
|
||||||
|
|
||||||
|
final JSONObject register = cacheClient.getSettingInfo("register", user.getTenantId());
|
||||||
|
if (register != null) {
|
||||||
|
final String ExpireTime = register.getString("tokenExpireTime");
|
||||||
|
if (ExpireTime != null) {
|
||||||
|
tokenExpireTime = Long.valueOf(ExpireTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 签发token
|
||||||
|
String access_token = JwtUtil.buildToken(new JwtSubject(user.getUsername(), user.getTenantId()),
|
||||||
|
tokenExpireTime, configProperties.getTokenKey());
|
||||||
|
// 同步redis
|
||||||
|
redisUtil.set("access_token:" + user.getUserId(), access_token, tokenExpireTime, TimeUnit.SECONDS);
|
||||||
|
return success("登录成功", new LoginResult(access_token, user));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "查询邮箱下全部账号(跨租户,用于多账号选择)")
|
||||||
|
@GetMapping("/listAdminsByEmailAll")
|
||||||
|
public ApiResult<?> listAdminsByEmailAll(LoginParam param) {
|
||||||
|
final List<User> accounts = userService.findAccountsByEmail(param.getEmail());
|
||||||
|
return success(accounts);
|
||||||
|
}
|
||||||
|
|
||||||
@Operation(summary = "检查用户是否存在")
|
@Operation(summary = "检查用户是否存在")
|
||||||
@GetMapping("/existence")
|
@GetMapping("/existence")
|
||||||
public ApiResult<?> existence(ExistenceParam<User> param) {
|
public ApiResult<?> existence(ExistenceParam<User> param) {
|
||||||
@@ -261,11 +342,11 @@ public class MainController extends BaseController {
|
|||||||
}
|
}
|
||||||
final Company company = companyMapper.getByTenantId(tenantId);
|
final Company company = companyMapper.getByTenantId(tenantId);
|
||||||
// 是否过期
|
// 是否过期
|
||||||
if (company.getVersion() < 30) {
|
// if (company.getVersion() < 30) {
|
||||||
if (Instant.now().isAfter(company.getExpirationTime().toInstant())) {
|
// if (Instant.now().isAfter(company.getExpirationTime().toInstant())) {
|
||||||
return fail(MessageFormat.format("应用ID({0})已过期",company.getTenantId()),null);
|
// return fail(MessageFormat.format("应用ID({0})已过期",company.getTenantId()),null);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
company.setBusinessEntity(null);
|
company.setBusinessEntity(null);
|
||||||
company.setPhone(null);
|
company.setPhone(null);
|
||||||
// 配置信息
|
// 配置信息
|
||||||
@@ -319,14 +400,67 @@ public class MainController extends BaseController {
|
|||||||
update.setAvatar(user.getAvatar());
|
update.setAvatar(user.getAvatar());
|
||||||
update.setBgImage(user.getBgImage());
|
update.setBgImage(user.getBgImage());
|
||||||
update.setSex(user.getSex());
|
update.setSex(user.getSex());
|
||||||
update.setPhone(user.getPhone());
|
// 手机号变更:两步验证
|
||||||
update.setEmail(user.getEmail());
|
// 第一步:短信验证码发往原手机号(验证当前账户归属),校验通过才允许换绑
|
||||||
|
// 第二步:向新手机号发送验证码(验证新号归属);账号原本无手机号时,第一步的码已发往新号,无需第二步
|
||||||
|
if (StrUtil.isNotBlank(user.getSmsCode())) {
|
||||||
|
String newPhone = user.getPhone();
|
||||||
|
if (StrUtil.isBlank(newPhone) || !CommonUtil.isValidPhoneNumber(newPhone)) {
|
||||||
|
return fail("手机号格式不正确", null);
|
||||||
|
}
|
||||||
|
User currentUser = userService.getById(getLoginUserId());
|
||||||
|
String currentPhone = currentUser != null ? currentUser.getPhone() : null;
|
||||||
|
// 第一步:校验原手机号(或账号原本无手机号时的新号)验证码
|
||||||
|
String verifyPhone = StrUtil.isNotBlank(currentPhone) ? currentPhone : newPhone;
|
||||||
|
String oldKey = "code:" + verifyPhone;
|
||||||
|
String cachedOld = redisUtil.get(oldKey);
|
||||||
|
String devCode = redisUtil.get(CACHE_KEY_VERIFICATION_CODE_BY_DEV_SMS);
|
||||||
|
if (StrUtil.isBlank(cachedOld) || (!cachedOld.equals(user.getSmsCode()) && !user.getSmsCode().equals(devCode))) {
|
||||||
|
return fail("短信验证码不正确", null);
|
||||||
|
}
|
||||||
|
// 第二步:校验新手机号验证码(仅当账号原本已绑定手机号,才需要二次验证新号归属)
|
||||||
|
if (StrUtil.isNotBlank(currentPhone)) {
|
||||||
|
String newKey = "code:" + newPhone;
|
||||||
|
String cachedNew = redisUtil.get(newKey);
|
||||||
|
if (StrUtil.isBlank(cachedNew)
|
||||||
|
|| (!cachedNew.equals(user.getSmsCodeNew()) && !user.getSmsCodeNew().equals(devCode))) {
|
||||||
|
return fail("新手机号验证码不正确", null);
|
||||||
|
}
|
||||||
|
redisUtil.delete(newKey);
|
||||||
|
cacheClient.delete(newPhone);
|
||||||
|
}
|
||||||
|
// 新手机号唯一性校验:不可被其他账号占用
|
||||||
|
User existed = userService.getByPhone(newPhone);
|
||||||
|
if (existed != null && !existed.getUserId().equals(getLoginUserId())) {
|
||||||
|
return fail("该手机号已被其他账号绑定", null);
|
||||||
|
}
|
||||||
|
update.setPhone(newPhone);
|
||||||
|
redisUtil.delete(oldKey);
|
||||||
|
cacheClient.delete(verifyPhone);
|
||||||
|
}
|
||||||
|
// 邮箱变更:仅当提交邮箱验证码且校验通过时才更新,防止未经验证的邮箱被绑定
|
||||||
|
if (StrUtil.isNotBlank(user.getEmailCode())) {
|
||||||
|
String key = RedisConstants.EMAIL_CODE_KEY + ":" + user.getEmail();
|
||||||
|
String cached = redisUtil.get(key);
|
||||||
|
if (StrUtil.isBlank(cached) || !cached.equals(user.getEmailCode())) {
|
||||||
|
return fail("邮箱验证码不正确", null);
|
||||||
|
}
|
||||||
|
update.setEmail(user.getEmail());
|
||||||
|
redisUtil.delete(key);
|
||||||
|
}
|
||||||
|
// 未提交邮箱验证码时不更新邮箱(保持原值,禁止绕过校验)
|
||||||
update.setProvince(user.getProvince());
|
update.setProvince(user.getProvince());
|
||||||
update.setCity(user.getCity());
|
update.setCity(user.getCity());
|
||||||
update.setRegion(user.getRegion());
|
update.setRegion(user.getRegion());
|
||||||
update.setAddress(user.getAddress());
|
update.setAddress(user.getAddress());
|
||||||
update.setIntroduction(user.getIntroduction());
|
update.setIntroduction(user.getIntroduction());
|
||||||
|
|
||||||
|
// 支付密码变更(个人中心设置/修改支付密码时透传明文):加密后落库,不允许明文入库
|
||||||
|
// 注意:仅当本次提交携带非空 payPassword 才更新,避免清空已有密码
|
||||||
|
if (StrUtil.isNotBlank(user.getPayPassword())) {
|
||||||
|
update.setPayPassword(userService.encodePassword(user.getPayPassword()));
|
||||||
|
}
|
||||||
|
|
||||||
// MyBatis-Plus: 如果没有任何可更新字段,会生成 `UPDATE ... WHERE ...`(没有 SET)导致 SQL 报错
|
// MyBatis-Plus: 如果没有任何可更新字段,会生成 `UPDATE ... WHERE ...`(没有 SET)导致 SQL 报错
|
||||||
// 这里检测一下“确实有字段需要更新”,否则直接返回当前用户信息。
|
// 这里检测一下“确实有字段需要更新”,否则直接返回当前用户信息。
|
||||||
if (ObjectUtil.isAllEmpty(
|
if (ObjectUtil.isAllEmpty(
|
||||||
@@ -340,7 +474,8 @@ public class MainController extends BaseController {
|
|||||||
update.getCity(),
|
update.getCity(),
|
||||||
update.getRegion(),
|
update.getRegion(),
|
||||||
update.getAddress(),
|
update.getAddress(),
|
||||||
update.getIntroduction()
|
update.getIntroduction(),
|
||||||
|
update.getPayPassword()
|
||||||
)) {
|
)) {
|
||||||
return success("没有需要更新的字段", userService.getByIdRel(update.getUserId()));
|
return success("没有需要更新的字段", userService.getByIdRel(update.getUserId()));
|
||||||
}
|
}
|
||||||
@@ -352,20 +487,79 @@ public class MainController extends BaseController {
|
|||||||
return fail("保存失败", null);
|
return fail("保存失败", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("hasAuthority('sys:auth:user')")
|
||||||
|
@OperationLog
|
||||||
|
@Operation(summary = "提交注册地址(注册审核)")
|
||||||
|
@PostMapping("/auth/submitAddress")
|
||||||
|
public ApiResult<User> submitAddress(@RequestBody User param) {
|
||||||
|
Integer userId = getLoginUserId();
|
||||||
|
if (userId == null) {
|
||||||
|
return fail("用户未登录", null);
|
||||||
|
}
|
||||||
|
if (StrUtil.hasBlank(param.getRealName(), param.getShopName())) {
|
||||||
|
return fail("请填写注册人和店名", null);
|
||||||
|
}
|
||||||
|
if (StrUtil.hasBlank(param.getProvince(), param.getCity(), param.getRegion())) {
|
||||||
|
return fail("请选择所在地区", null);
|
||||||
|
}
|
||||||
|
User update = new User();
|
||||||
|
update.setUserId(userId);
|
||||||
|
update.setCountry("中国");
|
||||||
|
update.setRealName(param.getRealName());
|
||||||
|
update.setShopName(param.getShopName());
|
||||||
|
update.setProvince(param.getProvince());
|
||||||
|
update.setCity(param.getCity());
|
||||||
|
update.setRegion(param.getRegion());
|
||||||
|
update.setAddress(param.getAddress());
|
||||||
|
update.setAuditStatus(0); // 提交地址后标记为待审核
|
||||||
|
update.setRejectReason(null); // 清除之前的驳回原因
|
||||||
|
if (userService.updateById(update)) {
|
||||||
|
return success("提交成功", userService.getByIdRel(userId));
|
||||||
|
}
|
||||||
|
return fail("提交失败", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "查询当前用户审核状态")
|
||||||
|
@GetMapping("/auth/auditStatus")
|
||||||
|
public ApiResult<java.util.Map<String, Object>> getAuditStatus() {
|
||||||
|
Integer userId = getLoginUserId();
|
||||||
|
if (userId == null) {
|
||||||
|
return fail("未登录", null);
|
||||||
|
}
|
||||||
|
User user = userService.getByIdRel(userId);
|
||||||
|
java.util.Map<String, Object> result = new java.util.HashMap<>();
|
||||||
|
result.put("auditStatus", user.getAuditStatus());
|
||||||
|
result.put("rejectReason", user.getRejectReason());
|
||||||
|
result.put("hasAddress", StrUtil.isNotBlank(user.getRealName())
|
||||||
|
&& StrUtil.isNotBlank(user.getShopName())
|
||||||
|
&& StrUtil.isNotBlank(user.getProvince()));
|
||||||
|
return success(result);
|
||||||
|
}
|
||||||
|
|
||||||
@PreAuthorize("hasAuthority('sys:auth:password')")
|
@PreAuthorize("hasAuthority('sys:auth:password')")
|
||||||
@OperationLog
|
@OperationLog
|
||||||
@Operation(summary = "修改自己密码")
|
@Operation(summary = "修改自己密码")
|
||||||
@PutMapping("/auth/password")
|
@PutMapping("/auth/password")
|
||||||
public ApiResult<?> updatePassword(@RequestBody UpdatePasswordParam param) {
|
public ApiResult<?> updatePassword(@RequestBody UpdatePasswordParam param) {
|
||||||
if (StrUtil.hasBlank(param.getOldPassword(), param.getPassword())) {
|
if (StrUtil.isBlank(param.getPassword())) {
|
||||||
return fail("参数不能为空");
|
return fail("新密码不能为空");
|
||||||
}
|
}
|
||||||
Integer userId = getLoginUserId();
|
Integer userId = getLoginUserId();
|
||||||
if (userId == null) {
|
if (userId == null) {
|
||||||
return fail("未登录");
|
return fail("未登录");
|
||||||
}
|
}
|
||||||
if (!userService.comparePassword(userService.getById(userId).getPassword(), param.getOldPassword())) {
|
User dbUser = userService.getById(userId);
|
||||||
return fail("原密码输入不正确");
|
if (dbUser == null) {
|
||||||
|
return fail("用户不存在");
|
||||||
|
}
|
||||||
|
// 已设置过密码的必须验证旧密码;未设置过密码的(如验证码注册用户)直接设置新密码
|
||||||
|
if (StrUtil.isNotBlank(dbUser.getPassword())) {
|
||||||
|
if (StrUtil.isBlank(param.getOldPassword())) {
|
||||||
|
return fail("请输入当前密码");
|
||||||
|
}
|
||||||
|
if (!userService.comparePassword(dbUser.getPassword(), param.getOldPassword())) {
|
||||||
|
return fail("原密码输入不正确");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
User user = new User();
|
User user = new User();
|
||||||
user.setUserId(userId);
|
user.setUserId(userId);
|
||||||
@@ -403,6 +597,54 @@ public class MainController extends BaseController {
|
|||||||
return fail("修改失败");
|
return fail("修改失败");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("hasAuthority('sys:auth:user')")
|
||||||
|
@OperationLog
|
||||||
|
@Operation(summary = "短信验证码重置支付密码(忘记支付密码场景)")
|
||||||
|
@PutMapping("/auth/pay-password")
|
||||||
|
public ApiResult<?> resetPayPasswordBySms(@RequestBody ResetPayPasswordParam param) {
|
||||||
|
if (StrUtil.hasBlank(param.getPhone(), param.getCode(), param.getPayPassword())) {
|
||||||
|
return fail("参数不能为空");
|
||||||
|
}
|
||||||
|
if (!CommonUtil.isValidPhoneNumber(param.getPhone())) {
|
||||||
|
return fail("请输入有效的手机号码");
|
||||||
|
}
|
||||||
|
// 支付密码必须为 4 位数字
|
||||||
|
if (!param.getPayPassword().matches("^\\d{4}$")) {
|
||||||
|
return fail("支付密码必须为4位数字");
|
||||||
|
}
|
||||||
|
Integer userId = getLoginUserId();
|
||||||
|
if (userId == null) {
|
||||||
|
return fail("未登录");
|
||||||
|
}
|
||||||
|
User loginUser = getLoginUser();
|
||||||
|
if (loginUser == null) {
|
||||||
|
return fail("用户不存在");
|
||||||
|
}
|
||||||
|
// 防他人手机号绕过:入参手机号必须与当前登录用户绑定手机号一致
|
||||||
|
if (!param.getPhone().equals(loginUser.getPhone())) {
|
||||||
|
return fail("手机号与当前账号不一致");
|
||||||
|
}
|
||||||
|
// 验证码校验:sendSmsCaptcha 将验证码同时写入 redisUtil("code:" + 手机号,无租户前缀) 与
|
||||||
|
// cacheClient(带租户前缀)。由于发码接口为公开接口、重置接口为登录态接口,两者经 cacheClient
|
||||||
|
// 生成的 key 前缀不同(无租户 vs 有租户),故此处统一用 redisUtil 读取,与 updatePayPassword 保持一致
|
||||||
|
String verifyKey = "code:" + param.getPhone();
|
||||||
|
String cachedCode = redisUtil.get(verifyKey);
|
||||||
|
String devCode = redisUtil.get(CACHE_KEY_VERIFICATION_CODE_BY_DEV_SMS);
|
||||||
|
if (StrUtil.isBlank(cachedCode) || (!param.getCode().equals(cachedCode) && !param.getCode().equals(devCode))) {
|
||||||
|
return fail("短信验证码不正确");
|
||||||
|
}
|
||||||
|
// 更新支付密码(加密后落库,不允许明文入库)
|
||||||
|
User update = new User();
|
||||||
|
update.setUserId(userId);
|
||||||
|
update.setPayPassword(userService.encodePassword(param.getPayPassword()));
|
||||||
|
if (userService.updateById(update)) {
|
||||||
|
// 核销已使用的验证码
|
||||||
|
redisUtil.delete(verifyKey);
|
||||||
|
return success("支付密码重置成功");
|
||||||
|
}
|
||||||
|
return fail("重置失败");
|
||||||
|
}
|
||||||
|
|
||||||
@PreAuthorize("hasAnyAuthority('sys:auth:user')")
|
@PreAuthorize("hasAnyAuthority('sys:auth:user')")
|
||||||
@Operation(summary = "验证支付密码")
|
@Operation(summary = "验证支付密码")
|
||||||
@PostMapping("/auth/checkPayPassword")
|
@PostMapping("/auth/checkPayPassword")
|
||||||
@@ -447,13 +689,6 @@ public class MainController extends BaseController {
|
|||||||
if (param == null) {
|
if (param == null) {
|
||||||
return fail("参数不能为空");
|
return fail("参数不能为空");
|
||||||
}
|
}
|
||||||
// 默认配置(当租户未配置短信服务时使用)
|
|
||||||
String accessKeyId = "LTAI5t7jGTFTbpSLzzXY8HzP";
|
|
||||||
String accessKeySecret = "Z22EPJyUhQaIZfEEmZ4Hdbw6xZibCb";
|
|
||||||
String templateCode = "SMS_481670203";
|
|
||||||
String signName = "网宿信息";
|
|
||||||
String regionId = "cn-hangzhou";
|
|
||||||
|
|
||||||
if (!CommonUtil.isValidPhoneNumber(param.getPhone())) {
|
if (!CommonUtil.isValidPhoneNumber(param.getPhone())) {
|
||||||
return fail("请输入有效的手机号码");
|
return fail("请输入有效的手机号码");
|
||||||
}
|
}
|
||||||
@@ -486,13 +721,60 @@ public class MainController extends BaseController {
|
|||||||
return fail("租户ID格式不正确");
|
return fail("租户ID格式不正确");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return sendSmsCaptchaInternal(param, tenantId);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "发送短信验证码(后台管理员, 跨租户)")
|
||||||
|
@PostMapping("/sendSmsCaptchaByAdmin")
|
||||||
|
public ApiResult<?> sendSmsCaptchaByAdmin(@RequestBody SmsCaptchaParam param) {
|
||||||
|
if (param == null) {
|
||||||
|
return fail("参数不能为空");
|
||||||
|
}
|
||||||
|
if (!CommonUtil.isValidPhoneNumber(param.getPhone())) {
|
||||||
|
return fail("请输入有效的手机号码");
|
||||||
|
}
|
||||||
|
// 跨租户校验:只要某个租户下该手机号是超级管理员或管理员即可发送验证码
|
||||||
|
final User admin = userService.getByPhoneAndAdmin(param.getPhone());
|
||||||
|
if (ObjectUtil.isEmpty(admin)) {
|
||||||
|
return fail("该手机号码未注册管理员账号!");
|
||||||
|
}
|
||||||
|
// 使用管理员所在租户的短信配置发送
|
||||||
|
return sendSmsCaptchaInternal(param, admin.getTenantId());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 短信发送核心逻辑
|
||||||
|
*
|
||||||
|
* @param param 参数
|
||||||
|
* @param configTenantId 用于读取租户短信配置的租户ID,为 null 时使用默认配置
|
||||||
|
*/
|
||||||
|
private ApiResult<?> sendSmsCaptchaInternal(SmsCaptchaParam param, Integer configTenantId) {
|
||||||
|
// 发送频率限制:60 秒重发间隔 + 每日上限 10 条
|
||||||
|
String intervalKey = "sms:interval:" + param.getPhone();
|
||||||
|
if (StrUtil.isNotBlank(redisUtil.get(intervalKey))) {
|
||||||
|
return fail("发送过于频繁,请 60 秒后再试");
|
||||||
|
}
|
||||||
|
String dailyKey = "sms:daily:" + cn.hutool.core.date.DateUtil.today() + ":" + param.getPhone();
|
||||||
|
String dailyCountStr = redisUtil.get(dailyKey);
|
||||||
|
int dailyCount = StrUtil.isBlank(dailyCountStr) ? 0 : Integer.parseInt(dailyCountStr);
|
||||||
|
if (dailyCount >= 10) {
|
||||||
|
return fail("今日短信发送次数已达上限");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认配置(当租户未配置短信服务时使用)
|
||||||
|
String accessKeyId = "LTAI5t7jGTFTbpSLzzXY8HzP";
|
||||||
|
String accessKeySecret = "Z22EPJyUhQaIZfEEmZ4Hdbw6xZibCb";
|
||||||
|
String templateCode = "SMS_481670203";
|
||||||
|
String signName = "网宿信息";
|
||||||
|
String regionId = "cn-hangzhou";
|
||||||
|
|
||||||
// 读取租户的短信配置(SettingController写入的key格式为:sms:{tenantId})
|
// 读取租户的短信配置(SettingController写入的key格式为:sms:{tenantId})
|
||||||
if (tenantId != null) {
|
if (configTenantId != null) {
|
||||||
String settingJson = redisUtil.get("sms:" + tenantId);
|
String settingJson = redisUtil.get("sms:" + configTenantId);
|
||||||
// 兼容历史key
|
// 兼容历史key
|
||||||
if (StrUtil.isBlank(settingJson)) {
|
if (StrUtil.isBlank(settingJson)) {
|
||||||
settingJson = redisUtil.get("setting:sms:" + tenantId);
|
settingJson = redisUtil.get("setting:sms:" + configTenantId);
|
||||||
}
|
}
|
||||||
if (StrUtil.isNotBlank(settingJson)) {
|
if (StrUtil.isNotBlank(settingJson)) {
|
||||||
JSONObject jsonObject = JSONObject.parseObject(settingJson);
|
JSONObject jsonObject = JSONObject.parseObject(settingJson);
|
||||||
@@ -540,6 +822,11 @@ public class MainController extends BaseController {
|
|||||||
cacheClient.set(param.getPhone(), code, 5L, TimeUnit.MINUTES);
|
cacheClient.set(param.getPhone(), code, 5L, TimeUnit.MINUTES);
|
||||||
String key = "code:" + param.getPhone();
|
String key = "code:" + param.getPhone();
|
||||||
redisUtil.set(key, code, 5L, TimeUnit.MINUTES);
|
redisUtil.set(key, code, 5L, TimeUnit.MINUTES);
|
||||||
|
// 记录发送频率限制:60 秒间隔 + 每日计数
|
||||||
|
redisUtil.set("sms:interval:" + param.getPhone(), "1", 60L, TimeUnit.SECONDS);
|
||||||
|
String dailyNowStr = redisUtil.get("sms:daily:" + cn.hutool.core.date.DateUtil.today() + ":" + param.getPhone());
|
||||||
|
int dailyNow = StrUtil.isBlank(dailyNowStr) ? 0 : Integer.parseInt(dailyNowStr);
|
||||||
|
redisUtil.set("sms:daily:" + cn.hutool.core.date.DateUtil.today() + ":" + param.getPhone(), String.valueOf(dailyNow + 1), 1L, TimeUnit.DAYS);
|
||||||
return success("发送成功", result.get("Message"));
|
return success("发送成功", result.get("Message"));
|
||||||
} else {
|
} else {
|
||||||
log.warn("短信发送失败 phone={}, result={}", DesensitizedUtil.mobilePhone(param.getPhone()), result);
|
log.warn("短信发送失败 phone={}, result={}", DesensitizedUtil.mobilePhone(param.getPhone()), result);
|
||||||
@@ -564,6 +851,57 @@ public class MainController extends BaseController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "发送邮箱验证码")
|
||||||
|
@PostMapping("/sendEmailCaptcha")
|
||||||
|
public ApiResult<?> sendEmailCaptcha(@RequestBody EmailCaptchaParam param) {
|
||||||
|
if (param == null || StrUtil.isBlank(param.getEmail())) {
|
||||||
|
return fail("邮箱不能为空");
|
||||||
|
}
|
||||||
|
if (!CommonUtil.isValidEmail(param.getEmail())) {
|
||||||
|
return fail("请输入有效的邮箱地址");
|
||||||
|
}
|
||||||
|
// 生成6位邮箱验证码
|
||||||
|
String code = Integer.toString(ThreadLocalRandom.current().nextInt(100000, 1000000));
|
||||||
|
// 存储到Redis,5分钟有效期(key 与校验时保持一致)
|
||||||
|
String key = RedisConstants.EMAIL_CODE_KEY + ":" + param.getEmail();
|
||||||
|
redisUtil.set(key, code, RedisConstants.EMAIL_CODE_TTL, TimeUnit.MINUTES);
|
||||||
|
cacheClient.set(param.getEmail(), code, RedisConstants.EMAIL_CODE_TTL, TimeUnit.MINUTES);
|
||||||
|
Integer tenantId = getTenantId();
|
||||||
|
try {
|
||||||
|
emailTemplateUtil.sendCaptchaEmail(param.getEmail(), code, tenantId);
|
||||||
|
log.info("邮箱验证码发送成功 email={}", DesensitizedUtil.email(param.getEmail()));
|
||||||
|
return success("验证码已发送,请查收邮箱");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("邮箱验证码发送失败 email={}", param.getEmail(), e);
|
||||||
|
return fail("邮件发送失败,请稍后重试");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "校验邮箱验证码")
|
||||||
|
@PostMapping("/verifyEmailCaptcha")
|
||||||
|
public ApiResult<?> verifyEmailCaptcha(@RequestBody EmailCaptchaParam param) {
|
||||||
|
if (param == null || StrUtil.isBlank(param.getEmail())) {
|
||||||
|
return fail("邮箱不能为空");
|
||||||
|
}
|
||||||
|
if (!CommonUtil.isValidEmail(param.getEmail())) {
|
||||||
|
return fail("请输入有效的邮箱地址");
|
||||||
|
}
|
||||||
|
if (StrUtil.isBlank(param.getCode())) {
|
||||||
|
return fail("验证码不能为空");
|
||||||
|
}
|
||||||
|
// 与 sendEmailCaptcha 写入的 key 保持一致:emailCode:邮箱
|
||||||
|
String key = RedisConstants.EMAIL_CODE_KEY + ":" + param.getEmail();
|
||||||
|
String cachedCode = redisUtil.get(key);
|
||||||
|
String devCode = redisUtil.get(CACHE_KEY_VERIFICATION_CODE_BY_DEV_SMS);
|
||||||
|
if (cachedCode == null
|
||||||
|
|| (!param.getCode().equals(cachedCode) && !param.getCode().equals(devCode))) {
|
||||||
|
return fail("邮箱验证码不正确");
|
||||||
|
}
|
||||||
|
// 校验通过即删除,防止重复使用
|
||||||
|
redisUtil.delete(key);
|
||||||
|
return success("邮箱验证通过");
|
||||||
|
}
|
||||||
|
|
||||||
@OperationLog
|
@OperationLog
|
||||||
@Operation(summary = "重置密码")
|
@Operation(summary = "重置密码")
|
||||||
@PutMapping("/password")
|
@PutMapping("/password")
|
||||||
@@ -683,6 +1021,8 @@ public class MainController extends BaseController {
|
|||||||
final UserParam userParam = new UserParam();
|
final UserParam userParam = new UserParam();
|
||||||
userParam.setPhone(phone);
|
userParam.setPhone(phone);
|
||||||
userParam.setTenantId(tenantId);
|
userParam.setTenantId(tenantId);
|
||||||
|
// 前端传 auditStatus=0 时走审核流程(新用户待审核)
|
||||||
|
userParam.setAuditStatus(param.getAuditStatus());
|
||||||
user = userService.addUser(userParam);
|
user = userService.addUser(userParam);
|
||||||
}
|
}
|
||||||
if (!user.getStatus().equals(0)) {
|
if (!user.getStatus().equals(0)) {
|
||||||
@@ -732,37 +1072,217 @@ public class MainController extends BaseController {
|
|||||||
return fail(message, null);
|
return fail(message, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
User user = userService.getLastLoginSuperAdminByPhone(phone);
|
// 查询该手机号关联的所有超级管理员记录(跨租户)
|
||||||
if (user == null) {
|
List<User> superAdmins = userService.getSuperAdminsByPhone(phone);
|
||||||
|
if (superAdmins == null || superAdmins.isEmpty()) {
|
||||||
String message = "用户不存在";
|
String message = "用户不存在";
|
||||||
loginRecordService.saveAsync(phone, LoginRecord.TYPE_ERROR, message, null, request);
|
loginRecordService.saveAsync(phone, LoginRecord.TYPE_ERROR, message, null, request);
|
||||||
return fail(message, null);
|
return fail(message, null);
|
||||||
}
|
}
|
||||||
if (!Boolean.TRUE.equals(user.getIsSuperAdmin())) {
|
|
||||||
String message = "非超级管理员账号不允许登录";
|
// 单租户:直接登录(现有逻辑不变,单租户用户无感知)
|
||||||
loginRecordService.saveAsync(phone, LoginRecord.TYPE_ERROR, message, user.getTenantId(), request);
|
if (superAdmins.size() == 1) {
|
||||||
return fail(message, null);
|
User user = superAdmins.get(0);
|
||||||
|
// if (!Boolean.TRUE.equals(user.getIsSuperAdmin())) {
|
||||||
|
// String message = "非超级管理员账号不允许登录";
|
||||||
|
// loginRecordService.saveAsync(phone, LoginRecord.TYPE_ERROR, message, user.getTenantId(), request);
|
||||||
|
// return fail(message, null);
|
||||||
|
// }
|
||||||
|
if (!user.getStatus().equals(0)) {
|
||||||
|
String message = "账号被冻结";
|
||||||
|
loginRecordService.saveAsync(user.getUsername(), LoginRecord.TYPE_ERROR, message, user.getTenantId(), request);
|
||||||
|
return fail(message, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
String access_token = generateToken(user);
|
||||||
|
loginRecordService.saveAsync(user.getUsername(), LoginRecord.TYPE_LOGIN, null, user.getTenantId(), request);
|
||||||
|
return success("登录成功", new LoginResult(access_token, user));
|
||||||
}
|
}
|
||||||
if (!user.getStatus().equals(0)) {
|
|
||||||
String message = "账号被冻结";
|
// 多租户:返回租户列表,不生成 token,前端展示选择列表
|
||||||
loginRecordService.saveAsync(user.getUsername(), LoginRecord.TYPE_ERROR, message, user.getTenantId(), request);
|
List<TenantOption> tenants = superAdmins.stream()
|
||||||
|
.map(u -> new TenantOption(
|
||||||
|
u.getTenantId(),
|
||||||
|
u.getTenantName(),
|
||||||
|
u.getUserId(),
|
||||||
|
u.getUsername(),
|
||||||
|
u.getNickname(),
|
||||||
|
null,
|
||||||
|
u.getAvatar()
|
||||||
|
))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
return success("请选择要登录的租户", new LoginResult(tenants));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "选择租户后登录(多租户场景)")
|
||||||
|
@PostMapping("/loginBySelectTenant")
|
||||||
|
public ApiResult<LoginResult> loginBySelectTenant(@RequestBody LoginParam param, HttpServletRequest request) {
|
||||||
|
if (param == null) {
|
||||||
|
return fail("参数不能为空", null);
|
||||||
|
}
|
||||||
|
final String phone = param.getPhone();
|
||||||
|
if (!CommonUtil.isValidPhoneNumber(phone)) {
|
||||||
|
return fail("请输入有效的手机号码", null);
|
||||||
|
}
|
||||||
|
if (param.getTenantId() == null) {
|
||||||
|
return fail("请选择租户", null);
|
||||||
|
}
|
||||||
|
String code = param.getCode();
|
||||||
|
if (StrUtil.isBlank(code)) {
|
||||||
|
code = param.getSmsCode();
|
||||||
|
}
|
||||||
|
if (StrUtil.isBlank(code)) {
|
||||||
|
return fail("验证码不能为空", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
String smsCode = redisUtil.get("code:" + phone);
|
||||||
|
String devSmsCode = redisUtil.get(CACHE_KEY_VERIFICATION_CODE_BY_DEV_SMS);
|
||||||
|
if (!StrUtil.equals(code, smsCode) && !StrUtil.equals(code, devSmsCode)) {
|
||||||
|
String message = "验证码不正确";
|
||||||
|
loginRecordService.saveAsync(phone, LoginRecord.TYPE_ERROR, message, null, request);
|
||||||
return fail(message, null);
|
return fail(message, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 查询该手机号在指定租户下的超级管理员
|
||||||
|
User user = userService.getSuperAdminByPhoneAndTenantId(phone, param.getTenantId());
|
||||||
|
if (user == null) {
|
||||||
|
String message = "该租户下不存在此账号";
|
||||||
|
loginRecordService.saveAsync(phone, LoginRecord.TYPE_ERROR, message, param.getTenantId(), request);
|
||||||
|
return fail(message, null);
|
||||||
|
}
|
||||||
|
// if (!Boolean.TRUE.equals(user.getIsSuperAdmin())) {
|
||||||
|
// String message = "非超级管理员账号不允许登录";
|
||||||
|
// loginRecordService.saveAsync(phone, LoginRecord.TYPE_ERROR, message, user.getTenantId(), request);
|
||||||
|
// return fail(message, null);
|
||||||
|
// }
|
||||||
|
|
||||||
|
String access_token = generateToken(user);
|
||||||
|
loginRecordService.saveAsync(user.getUsername(), LoginRecord.TYPE_LOGIN, null, user.getTenantId(), request);
|
||||||
|
return success("登录成功", new LoginResult(access_token, user));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "获取当前登录用户可切换的租户列表(免登录切换前置)")
|
||||||
|
@GetMapping("/auth/switchableTenants")
|
||||||
|
public ApiResult<?> switchableTenants() {
|
||||||
|
User current = getLoginUser();
|
||||||
|
if (current == null) {
|
||||||
|
return fail("未登录或登录已过期", null);
|
||||||
|
}
|
||||||
|
List<TenantOption> options = new ArrayList<>();
|
||||||
|
// 按手机号聚合(仅管理员账号)
|
||||||
|
if (StrUtil.isNotBlank(current.getPhone())) {
|
||||||
|
LoginParam phoneParam = new LoginParam();
|
||||||
|
phoneParam.setPhone(current.getPhone());
|
||||||
|
List<User> byPhone = userService.getAdminsByPhone(phoneParam);
|
||||||
|
if (byPhone != null) {
|
||||||
|
for (User u : byPhone) {
|
||||||
|
options.add(toTenantOption(u));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 按邮箱聚合(管理员账号,按租户去重)
|
||||||
|
if (StrUtil.isNotBlank(current.getEmail())) {
|
||||||
|
List<User> byEmail = userService.findAccountsByEmail(current.getEmail());
|
||||||
|
if (byEmail != null) {
|
||||||
|
for (User u : byEmail) {
|
||||||
|
if (!Boolean.TRUE.equals(u.getIsAdmin())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final Integer tId = u.getTenantId();
|
||||||
|
if (tId != null && options.stream().noneMatch(o -> tId.equals(o.getTenantId()))) {
|
||||||
|
options.add(toTenantOption(u));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return success(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "免登录切换租户(需已登录,当前用户须为目标租户管理员)")
|
||||||
|
@PostMapping("/auth/switchTenant")
|
||||||
|
public ApiResult<LoginResult> switchTenant(@RequestBody LoginParam param, HttpServletRequest request) {
|
||||||
|
Integer targetTenantId = param.getTenantId();
|
||||||
|
if (targetTenantId == null) {
|
||||||
|
return fail("请选择租户", null);
|
||||||
|
}
|
||||||
|
User current = getLoginUser();
|
||||||
|
if (current == null) {
|
||||||
|
return fail("未登录或登录已过期", null);
|
||||||
|
}
|
||||||
|
// 切换到当前登录用户所属租户(如注册成功后刷新权限令牌)直接放行:
|
||||||
|
// 注册即时场景下可切换租户列表校验可能未能命中刚创建的新超管,自己切自己一定合法。
|
||||||
|
if (targetTenantId.equals(current.getTenantId())) {
|
||||||
|
String access_token = generateToken(current);
|
||||||
|
loginRecordService.saveAsync(current.getUsername(), LoginRecord.TYPE_LOGIN, null, current.getTenantId(), request);
|
||||||
|
return success("切换成功", new LoginResult(access_token, current));
|
||||||
|
}
|
||||||
|
// 校验目标租户在可切换范围内(与 switchableTenants 一致)
|
||||||
|
User target = findSwitchableUser(current, targetTenantId);
|
||||||
|
if (target == null) {
|
||||||
|
return fail("无权切换到该租户", null);
|
||||||
|
}
|
||||||
|
if (!target.getStatus().equals(0)) {
|
||||||
|
return fail("该租户账号已被冻结", null);
|
||||||
|
}
|
||||||
|
String access_token = generateToken(target);
|
||||||
|
loginRecordService.saveAsync(target.getUsername(), LoginRecord.TYPE_LOGIN, null, target.getTenantId(), request);
|
||||||
|
return success("切换成功", new LoginResult(access_token, target));
|
||||||
|
}
|
||||||
|
|
||||||
|
private User findSwitchableUser(User current, Integer targetTenantId) {
|
||||||
|
List<User> candidates = new ArrayList<>();
|
||||||
|
if (StrUtil.isNotBlank(current.getPhone())) {
|
||||||
|
LoginParam phoneParam = new LoginParam();
|
||||||
|
phoneParam.setPhone(current.getPhone());
|
||||||
|
List<User> byPhone = userService.getAdminsByPhone(phoneParam);
|
||||||
|
if (byPhone != null) {
|
||||||
|
candidates.addAll(byPhone);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (StrUtil.isNotBlank(current.getEmail())) {
|
||||||
|
List<User> byEmail = userService.findAccountsByEmail(current.getEmail());
|
||||||
|
if (byEmail != null) {
|
||||||
|
for (User u : byEmail) {
|
||||||
|
if (Boolean.TRUE.equals(u.getIsAdmin())) {
|
||||||
|
candidates.add(u);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return candidates.stream()
|
||||||
|
.filter(u -> targetTenantId.equals(u.getTenantId()))
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private TenantOption toTenantOption(User u) {
|
||||||
|
return new TenantOption(
|
||||||
|
u.getTenantId(),
|
||||||
|
u.getTenantName(),
|
||||||
|
u.getUserId(),
|
||||||
|
u.getUsername(),
|
||||||
|
u.getNickname(),
|
||||||
|
null,
|
||||||
|
u.getAvatar()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成 JWT token 并存入 Redis
|
||||||
|
*/
|
||||||
|
private String generateToken(User user) {
|
||||||
Long tokenExpireTime = configProperties.getTokenExpireTime();
|
Long tokenExpireTime = configProperties.getTokenExpireTime();
|
||||||
final JSONObject register = cacheClient.getSettingInfo("register", user.getTenantId());
|
final JSONObject register = cacheClient.getSettingInfo("register", user.getTenantId());
|
||||||
if (register != null) {
|
if (register != null) {
|
||||||
final String ExpireTime = register.getString("tokenExpireTime");
|
final String expireTime = register.getString("tokenExpireTime");
|
||||||
if (ExpireTime != null) {
|
if (expireTime != null) {
|
||||||
tokenExpireTime = Long.valueOf(ExpireTime);
|
tokenExpireTime = Long.valueOf(expireTime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loginRecordService.saveAsync(user.getUsername(), LoginRecord.TYPE_LOGIN, null, user.getTenantId(), request);
|
|
||||||
String access_token = JwtUtil.buildToken(new JwtSubject(user.getUsername(), user.getTenantId()),
|
String access_token = JwtUtil.buildToken(new JwtSubject(user.getUsername(), user.getTenantId()),
|
||||||
tokenExpireTime, configProperties.getTokenKey());
|
tokenExpireTime, configProperties.getTokenKey());
|
||||||
redisUtil.set("access_token:" + user.getUserId(), access_token, tokenExpireTime, TimeUnit.SECONDS);
|
redisUtil.set("access_token:" + user.getUserId(), access_token, tokenExpireTime, TimeUnit.SECONDS);
|
||||||
return success("登录成功", new LoginResult(access_token, user));
|
return access_token;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "开发者短信验证码登录")
|
@Operation(summary = "开发者短信验证码登录")
|
||||||
@@ -923,6 +1443,7 @@ public class MainController extends BaseController {
|
|||||||
final Company addCompany = tenantService.initialization(company);
|
final Company addCompany = tenantService.initialization(company);
|
||||||
final UserParam userParam = new UserParam();
|
final UserParam userParam = new UserParam();
|
||||||
userParam.setIsAdmin(true);
|
userParam.setIsAdmin(true);
|
||||||
|
userParam.setIsSuperAdmin(true);
|
||||||
userParam.setPhone(phone);
|
userParam.setPhone(phone);
|
||||||
userParam.setTemplateId(user.getTemplateId());
|
userParam.setTemplateId(user.getTemplateId());
|
||||||
userParam.setTenantId(addCompany.getTenantId()); // 使用新创建的租户ID
|
userParam.setTenantId(addCompany.getTenantId()); // 使用新创建的租户ID
|
||||||
@@ -1082,6 +1603,7 @@ public class MainController extends BaseController {
|
|||||||
company.setEmail(email);
|
company.setEmail(email);
|
||||||
company.setPhone(phone);
|
company.setPhone(phone);
|
||||||
company.setPassword(password);
|
company.setPassword(password);
|
||||||
|
company.setAppCode(user.getAppCode());
|
||||||
company.setTid(tenant.getTenantId());
|
company.setTid(tenant.getTenantId());
|
||||||
company.setShortName(tenantName);
|
company.setShortName(tenantName);
|
||||||
company.setCategoryId(661);
|
company.setCategoryId(661);
|
||||||
@@ -1167,14 +1689,20 @@ public class MainController extends BaseController {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 重置密码(找回密码功能)
|
* 重置密码(找回密码功能)
|
||||||
|
* 支持两种验证方式:手机号+短信验证码 / 邮箱+邮箱验证码
|
||||||
|
* 支持两种模式:
|
||||||
|
* 1. 传 userId + tenantId:精确重置指定账号密码
|
||||||
|
* 2. 不传 userId:通过手机号/邮箱查找所有正常账号,统一重置密码(忘记密码场景)
|
||||||
*/
|
*/
|
||||||
@Operation(summary = "重置密码")
|
@Operation(summary = "重置密码")
|
||||||
@PostMapping("/resetPassword")
|
@PostMapping("/resetPassword")
|
||||||
@Transactional(rollbackFor = Exception.class, isolation = Isolation.SERIALIZABLE)
|
@Transactional(rollbackFor = Exception.class, isolation = Isolation.SERIALIZABLE)
|
||||||
public ApiResult<?> resetPassword(@RequestBody ResetPasswordParam param) {
|
public ApiResult<?> resetPassword(@RequestBody ResetPasswordParam param) {
|
||||||
// 验证手机号
|
// 判断找回方式:邮箱 or 手机
|
||||||
if (!CommonUtil.isValidPhoneNumber(param.getPhone())) {
|
boolean byEmail = StrUtil.isNotBlank(param.getEmail());
|
||||||
return fail("请输入有效的手机号码");
|
boolean byPhone = StrUtil.isNotBlank(param.getPhone());
|
||||||
|
if (!byEmail && !byPhone) {
|
||||||
|
return fail("请提供手机号或邮箱");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证两次密码是否一致
|
// 验证两次密码是否一致
|
||||||
@@ -1187,45 +1715,103 @@ public class MainController extends BaseController {
|
|||||||
return fail("密码必须至少8位,且包含字母和数字");
|
return fail("密码必须至少8位,且包含字母和数字");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证短信验证码
|
// ===== 验证码校验 =====
|
||||||
String key = "code:" + param.getPhone();
|
String verifyKey;
|
||||||
String cachedCode = redisUtil.get(key);
|
String inputCode;
|
||||||
String devCode = redisUtil.get(CACHE_KEY_VERIFICATION_CODE_BY_DEV_SMS);
|
if (byEmail) {
|
||||||
|
if (!CommonUtil.isValidEmail(param.getEmail())) {
|
||||||
if (!param.getSmsCode().equals(cachedCode) && !param.getSmsCode().equals(devCode)) {
|
return fail("请输入有效的邮箱地址");
|
||||||
return fail("短信验证码不正确");
|
}
|
||||||
|
if (StrUtil.isBlank(param.getEmailCode())) {
|
||||||
|
return fail("邮箱验证码不能为空");
|
||||||
|
}
|
||||||
|
verifyKey = RedisConstants.EMAIL_CODE_KEY + ":" + param.getEmail();
|
||||||
|
inputCode = param.getEmailCode();
|
||||||
|
} else {
|
||||||
|
if (!CommonUtil.isValidPhoneNumber(param.getPhone())) {
|
||||||
|
return fail("请输入有效的手机号码");
|
||||||
|
}
|
||||||
|
if (StrUtil.isBlank(param.getSmsCode())) {
|
||||||
|
return fail("短信验证码不能为空");
|
||||||
|
}
|
||||||
|
verifyKey = "code:" + param.getPhone();
|
||||||
|
inputCode = param.getSmsCode();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证用户是否存在且手机号匹配
|
String cachedCode = redisUtil.get(verifyKey);
|
||||||
|
String devCode = redisUtil.get(CACHE_KEY_VERIFICATION_CODE_BY_DEV_SMS);
|
||||||
|
if (!inputCode.equals(cachedCode) && !inputCode.equals(devCode)) {
|
||||||
|
return fail(byEmail ? "邮箱验证码不正确" : "短信验证码不正确");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 模式判断:userId 为空时按手机号/邮箱查所有用户统一重置 =====
|
||||||
|
if (StrUtil.isBlank(param.getUserId())) {
|
||||||
|
List<User> userList = byEmail
|
||||||
|
? userMapper.selectAccountsByEmail(param.getEmail())
|
||||||
|
: userMapper.selectAccountsByPhone(param.getPhone());
|
||||||
|
if (userList == null || userList.isEmpty()) {
|
||||||
|
return fail(byEmail ? "该邮箱未注册" : "该手机号未注册");
|
||||||
|
}
|
||||||
|
// 过滤掉被冻结的账号
|
||||||
|
List<User> activeUsers = userList.stream()
|
||||||
|
.filter(u -> u.getStatus() == null || u.getStatus() == 0)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
if (activeUsers.isEmpty()) {
|
||||||
|
return fail("账号已被冻结,请联系管理员");
|
||||||
|
}
|
||||||
|
// 统一重置所有正常账号密码
|
||||||
|
String encodedPassword = userService.encodePassword(param.getNewPassword());
|
||||||
|
String methodDesc = byEmail ? "通过邮箱" : "通过手机号";
|
||||||
|
for (User u : activeUsers) {
|
||||||
|
User update = new User();
|
||||||
|
update.setUserId(u.getUserId());
|
||||||
|
update.setPassword(encodedPassword);
|
||||||
|
userMapper.updateByUserId(update);
|
||||||
|
// 记录登录日志
|
||||||
|
LoginRecord record = new LoginRecord();
|
||||||
|
record.setUsername(u.getUsername());
|
||||||
|
record.setNickname(u.getNickname());
|
||||||
|
record.setLoginType(5);
|
||||||
|
record.setComments("密码重置成功(" + methodDesc + ")");
|
||||||
|
record.setTenantId(u.getTenantId());
|
||||||
|
loginRecordService.save(record);
|
||||||
|
}
|
||||||
|
redisUtil.delete(verifyKey);
|
||||||
|
return success("密码重置成功");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 模式1:传了 userId,精确重置 =====
|
||||||
User user = userService.getByUserId(param.getUserId());
|
User user = userService.getByUserId(param.getUserId());
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
return fail("用户不存在");
|
return fail("用户不存在");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!param.getPhone().equals(user.getPhone())) {
|
if (byEmail) {
|
||||||
return fail("手机号与账号不匹配");
|
if (!param.getEmail().equals(user.getEmail())) {
|
||||||
|
return fail("邮箱与账号不匹配");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!param.getPhone().equals(user.getPhone())) {
|
||||||
|
return fail("手机号与账号不匹配");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!param.getTenantId().equals(user.getTenantId())) {
|
if (param.getTenantId() != null && !param.getTenantId().equals(user.getTenantId())) {
|
||||||
return fail("租户信息不匹配");
|
return fail("租户信息不匹配");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重置密码
|
// 重置密码
|
||||||
boolean success = userService.resetUserPassword(param.getUserId(), param.getTenantId(), param.getNewPassword());
|
boolean success = userService.resetUserPassword(param.getUserId(), user.getTenantId(), param.getNewPassword());
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
// 密码重置成功后删除验证码
|
redisUtil.delete(verifyKey);
|
||||||
redisUtil.delete(key);
|
|
||||||
|
|
||||||
// 记录登录日志(密码重置)
|
|
||||||
LoginRecord record = new LoginRecord();
|
LoginRecord record = new LoginRecord();
|
||||||
record.setUsername(user.getUsername());
|
record.setUsername(user.getUsername());
|
||||||
record.setNickname(user.getNickname());
|
record.setNickname(user.getNickname());
|
||||||
record.setLoginType(5); // 5表示密码重置(需要在LoginRecord中定义常量)
|
record.setLoginType(5);
|
||||||
record.setComments("密码重置成功");
|
record.setComments("密码重置成功");
|
||||||
record.setTenantId(user.getTenantId());
|
record.setTenantId(user.getTenantId());
|
||||||
loginRecordService.save(record);
|
loginRecordService.save(record);
|
||||||
|
|
||||||
return success("密码重置成功");
|
return success("密码重置成功");
|
||||||
} else {
|
} else {
|
||||||
return fail("密码重置失败,请稍后重试");
|
return fail("密码重置失败,请稍后重试");
|
||||||
@@ -1236,6 +1822,20 @@ public class MainController extends BaseController {
|
|||||||
* 检查手机号是否已注册(可选接口)
|
* 检查手机号是否已注册(可选接口)
|
||||||
*/
|
*/
|
||||||
@Operation(summary = "检查手机号是否已注册")
|
@Operation(summary = "检查手机号是否已注册")
|
||||||
|
@GetMapping("/checkEmailRegistered")
|
||||||
|
public ApiResult<?> checkEmailRegistered(@RequestParam("email") String email) {
|
||||||
|
// 邮箱格式校验
|
||||||
|
if (!CommonUtil.isValidEmail(email)) {
|
||||||
|
return fail("请输入有效的邮箱地址");
|
||||||
|
}
|
||||||
|
// 统计该邮箱注册的账号数量(跨租户,复用 findAccountsByEmail)
|
||||||
|
List<User> accounts = userService.findAccountsByEmail(email);
|
||||||
|
CheckEmailResult result = new CheckEmailResult();
|
||||||
|
result.setIsRegistered(accounts != null && !accounts.isEmpty());
|
||||||
|
result.setAccountCount(accounts != null ? accounts.size() : 0);
|
||||||
|
return success(result);
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/checkPhoneRegistered")
|
@GetMapping("/checkPhoneRegistered")
|
||||||
public ApiResult<?> checkPhoneRegistered(@RequestParam("phone") String phone) {
|
public ApiResult<?> checkPhoneRegistered(@RequestParam("phone") String phone) {
|
||||||
// 验证手机号
|
// 验证手机号
|
||||||
|
|||||||
@@ -226,6 +226,7 @@ public class MenuController extends BaseController {
|
|||||||
}
|
}
|
||||||
companyService.updateById(company);
|
companyService.updateById(company);
|
||||||
loginUser.setInstalled(true);
|
loginUser.setInstalled(true);
|
||||||
|
loginUser.setIsSuperAdmin(true);
|
||||||
userService.updateById(loginUser);
|
userService.updateById(loginUser);
|
||||||
return success("安装成功");
|
return success("安装成功");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import com.gxwebsoft.common.system.param.OrderParam;
|
|||||||
import com.gxwebsoft.common.system.service.MenuService;
|
import com.gxwebsoft.common.system.service.MenuService;
|
||||||
import com.gxwebsoft.common.system.service.OrderGoodsService;
|
import com.gxwebsoft.common.system.service.OrderGoodsService;
|
||||||
import com.gxwebsoft.common.system.service.OrderService;
|
import com.gxwebsoft.common.system.service.OrderService;
|
||||||
|
import com.gxwebsoft.common.system.service.UserService;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
@@ -47,6 +48,18 @@ public class OrderController extends BaseController {
|
|||||||
private MenuService menuService;
|
private MenuService menuService;
|
||||||
@Resource
|
@Resource
|
||||||
private MenuMapper menuMapper;
|
private MenuMapper menuMapper;
|
||||||
|
@Resource
|
||||||
|
private UserService userService;
|
||||||
|
|
||||||
|
/** 检查用户审核状态:auditStatus==null(老用户)或1(已通过)返回true,否则false */
|
||||||
|
private boolean checkUserAuditStatus(User loginUser) {
|
||||||
|
if (loginUser == null) return false;
|
||||||
|
User user = userService.getByIdRel(loginUser.getUserId());
|
||||||
|
if (user == null) return false;
|
||||||
|
Integer auditStatus = user.getAuditStatus();
|
||||||
|
// auditStatus == null 表示老用户(功能上线前注册的),默认放行
|
||||||
|
return auditStatus == null || auditStatus == 1;
|
||||||
|
}
|
||||||
|
|
||||||
@Operation(summary = "分页查询订单")
|
@Operation(summary = "分页查询订单")
|
||||||
@GetMapping("/page")
|
@GetMapping("/page")
|
||||||
@@ -94,6 +107,9 @@ public class OrderController extends BaseController {
|
|||||||
if(loginUser == null){
|
if(loginUser == null){
|
||||||
return fail("请先登录");
|
return fail("请先登录");
|
||||||
}
|
}
|
||||||
|
if (!checkUserAuditStatus(loginUser)) {
|
||||||
|
return fail("您的账号尚未通过审核,暂时无法下单");
|
||||||
|
}
|
||||||
if (ObjectUtil.isEmpty(order.getType())) {
|
if (ObjectUtil.isEmpty(order.getType())) {
|
||||||
return fail("订单类型不能为空");
|
return fail("订单类型不能为空");
|
||||||
}
|
}
|
||||||
@@ -184,6 +200,9 @@ public class OrderController extends BaseController {
|
|||||||
// 记录当前登录用户id
|
// 记录当前登录用户id
|
||||||
User loginUser = getLoginUser();
|
User loginUser = getLoginUser();
|
||||||
if (loginUser != null) {
|
if (loginUser != null) {
|
||||||
|
if (!checkUserAuditStatus(loginUser)) {
|
||||||
|
return fail("您的账号尚未通过审核,暂时无法下单");
|
||||||
|
}
|
||||||
// 封装订单数据
|
// 封装订单数据
|
||||||
final Order order = new Order();
|
final Order order = new Order();
|
||||||
order.setType(cart.getType());
|
order.setType(cart.getType());
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import com.gxwebsoft.common.core.web.BatchParam;
|
|||||||
import com.gxwebsoft.common.core.annotation.OperationLog;
|
import com.gxwebsoft.common.core.annotation.OperationLog;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
import org.springframework.util.CollectionUtils;
|
import org.springframework.util.CollectionUtils;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
@@ -24,6 +25,7 @@ import org.springframework.web.bind.annotation.*;
|
|||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@@ -49,6 +51,9 @@ public class TenantController extends BaseController {
|
|||||||
private RedisUtil redisUtil;
|
private RedisUtil redisUtil;
|
||||||
@Resource
|
@Resource
|
||||||
private UserService userService;
|
private UserService userService;
|
||||||
|
// cms权限参考租户, 默认取平台租户
|
||||||
|
@Value("${websoft.cms.ref-tenant-id:10257}")
|
||||||
|
private Integer cmsRefTenantId;
|
||||||
|
|
||||||
@Operation(summary = "分页查询租户")
|
@Operation(summary = "分页查询租户")
|
||||||
@GetMapping("/page")
|
@GetMapping("/page")
|
||||||
@@ -215,6 +220,40 @@ public class TenantController extends BaseController {
|
|||||||
return success(menus);
|
return success(menus);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OperationLog
|
||||||
|
@Operation(summary = "授予当前租户cms建站权限")
|
||||||
|
@PostMapping("/grantCmsPermission")
|
||||||
|
public ApiResult<?> grantCmsPermission(@RequestBody(required = false) Map<String, Object> body) {
|
||||||
|
final Integer curTenant = getTenantId();
|
||||||
|
if (curTenant == null) {
|
||||||
|
return fail("获取当前租户失败, 请重新登录");
|
||||||
|
}
|
||||||
|
// 参考租户优先级: 请求参数 > 企业当前使用的租户模板 > 配置项websoft.cms.ref-tenant-id
|
||||||
|
Integer refTenant = null;
|
||||||
|
final Object refTenantId = body == null ? null : body.get("refTenantId");
|
||||||
|
if (refTenantId instanceof Number) {
|
||||||
|
refTenant = ((Number) refTenantId).intValue();
|
||||||
|
} else if (refTenantId != null && refTenantId.toString().matches("\\d+")) {
|
||||||
|
refTenant = Integer.valueOf(refTenantId.toString());
|
||||||
|
}
|
||||||
|
if (refTenant == null) {
|
||||||
|
final Company company = getCompany();
|
||||||
|
if (company != null && company.getPlanId() != null) {
|
||||||
|
refTenant = company.getPlanId();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (refTenant == null) {
|
||||||
|
refTenant = cmsRefTenantId;
|
||||||
|
}
|
||||||
|
if (refTenant == null) {
|
||||||
|
return fail("未找到参考租户, 请配置websoft.cms.ref-tenant-id");
|
||||||
|
}
|
||||||
|
if (refTenant.equals(curTenant)) {
|
||||||
|
return fail("参考租户不能与当前租户相同");
|
||||||
|
}
|
||||||
|
return tenantService.grantCmsPermission(refTenant, curTenant);
|
||||||
|
}
|
||||||
|
|
||||||
@Operation(summary = "创建租户")
|
@Operation(summary = "创建租户")
|
||||||
@PostMapping("/saveByPhone")
|
@PostMapping("/saveByPhone")
|
||||||
public ApiResult<?> saveByPhone(@RequestBody Tenant tenant) {
|
public ApiResult<?> saveByPhone(@RequestBody Tenant tenant) {
|
||||||
|
|||||||
@@ -304,6 +304,49 @@ public class UserController extends BaseController {
|
|||||||
return fail("修改失败");
|
return fail("修改失败");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("hasAuthority('sys:auth:user')")
|
||||||
|
@Operation(summary = "查询用户审核信息")
|
||||||
|
@GetMapping("/audit/{userId}")
|
||||||
|
public ApiResult<Map<String, Object>> getAuditInfo(@PathVariable("userId") Integer userId) {
|
||||||
|
User user = userService.getByIdRel(userId);
|
||||||
|
if (user == null) {
|
||||||
|
return fail("用户不存在",null);
|
||||||
|
}
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
result.put("userId", user.getUserId());
|
||||||
|
result.put("auditStatus", user.getAuditStatus());
|
||||||
|
result.put("rejectReason", user.getRejectReason());
|
||||||
|
return success(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("hasAuthority('sys:user:update')")
|
||||||
|
@OperationLog
|
||||||
|
@Operation(summary = "审核用户注册(通过/拒绝)")
|
||||||
|
@PutMapping("/audit")
|
||||||
|
public ApiResult<?> auditUser(@RequestBody User user) {
|
||||||
|
if (user.getUserId() == null || user.getAuditStatus() == null
|
||||||
|
|| !Arrays.asList(0, 1, 2).contains(user.getAuditStatus())) {
|
||||||
|
return fail("参数不正确");
|
||||||
|
}
|
||||||
|
User update = new User();
|
||||||
|
update.setUserId(user.getUserId());
|
||||||
|
update.setAuditStatus(user.getAuditStatus());
|
||||||
|
// 拒绝时必须填写驳回原因
|
||||||
|
if (user.getAuditStatus() == 2) {
|
||||||
|
if (StrUtil.isBlank(user.getRejectReason())) {
|
||||||
|
return fail("驳回时请填写原因");
|
||||||
|
}
|
||||||
|
update.setRejectReason(user.getRejectReason());
|
||||||
|
} else {
|
||||||
|
// 通过或重新设为待审核时清除驳回原因
|
||||||
|
update.setRejectReason(null);
|
||||||
|
}
|
||||||
|
if (userService.updateById(update)) {
|
||||||
|
return success(user.getAuditStatus() == 1 ? "审核通过" : "已拒绝");
|
||||||
|
}
|
||||||
|
return fail("操作失败");
|
||||||
|
}
|
||||||
|
|
||||||
@PreAuthorize("hasAuthority('sys:user:update')")
|
@PreAuthorize("hasAuthority('sys:user:update')")
|
||||||
@OperationLog
|
@OperationLog
|
||||||
@Operation(summary = "修改推荐状态")
|
@Operation(summary = "修改推荐状态")
|
||||||
@@ -321,6 +364,20 @@ public class UserController extends BaseController {
|
|||||||
return fail("修改失败");
|
return fail("修改失败");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("hasAuthority('sys:userRole:save')")
|
||||||
|
@OperationLog
|
||||||
|
@Operation(summary = "设为开发者(审核通过时标记,不新增角色)")
|
||||||
|
@PutMapping("/developer/{userId}")
|
||||||
|
public ApiResult<?> setDeveloper(@PathVariable("userId") Integer userId) {
|
||||||
|
if (userId == null) {
|
||||||
|
return fail("参数不正确");
|
||||||
|
}
|
||||||
|
if (userService.markAsDeveloper(userId)) {
|
||||||
|
return success("已设为开发者");
|
||||||
|
}
|
||||||
|
return fail("操作失败");
|
||||||
|
}
|
||||||
|
|
||||||
@PreAuthorize("hasAuthority('sys:user:update')")
|
@PreAuthorize("hasAuthority('sys:user:update')")
|
||||||
@OperationLog
|
@OperationLog
|
||||||
@Operation(summary = "批量修改用户状态")
|
@Operation(summary = "批量修改用户状态")
|
||||||
|
|||||||
@@ -63,9 +63,9 @@ public class UserRoleController extends BaseController {
|
|||||||
@Operation(summary = "添加用户角色")
|
@Operation(summary = "添加用户角色")
|
||||||
@PostMapping()
|
@PostMapping()
|
||||||
public ApiResult<?> save(@RequestBody UserRole userRole) {
|
public ApiResult<?> save(@RequestBody UserRole userRole) {
|
||||||
// 记录当前登录用户id
|
// 记录当前登录用户id(请求体未指定 userId 时,回退到当前登录用户)
|
||||||
User loginUser = getLoginUser();
|
User loginUser = getLoginUser();
|
||||||
if (loginUser != null) {
|
if (userRole.getUserId() == null && loginUser != null) {
|
||||||
userRole.setUserId(loginUser.getUserId());
|
userRole.setUserId(loginUser.getUserId());
|
||||||
}
|
}
|
||||||
if (userRoleService.save(userRole)) {
|
if (userRoleService.save(userRole)) {
|
||||||
|
|||||||
@@ -134,11 +134,23 @@ public class WxLoginController extends BaseController {
|
|||||||
if (openid == null) {
|
if (openid == null) {
|
||||||
return fail("获取openid失败", null);
|
return fail("获取openid失败", null);
|
||||||
}
|
}
|
||||||
|
// 关键:把 openid/unionid 设置到 userParam,否则后续查库/注册都拿不到 openid
|
||||||
|
userParam.setOpenid(openid);
|
||||||
|
if (StrUtil.isNotBlank(unionid)) {
|
||||||
|
userParam.setUnionid(unionid);
|
||||||
|
}
|
||||||
// 2.通过openid查询用户是否已存在
|
// 2.通过openid查询用户是否已存在
|
||||||
User user = userService.getByOauthId(userParam);
|
User user = userService.getByOauthId(userParam);
|
||||||
// 3.存在则签发token并返回登录成功,不存在则注册新用户
|
// 3.存在则签发token并返回登录成功,不存在则注册新用户
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
user = addUser(userParam);
|
user = addUser(userParam);
|
||||||
|
} else if (StrUtil.isBlank(user.getOpenid())) {
|
||||||
|
// 老用户(比如手机号注册)没有 openid,补上
|
||||||
|
user.setOpenid(openid);
|
||||||
|
if (StrUtil.isNotBlank(unionid)) {
|
||||||
|
user.setUnionid(unionid);
|
||||||
|
}
|
||||||
|
userService.updateById(user);
|
||||||
}
|
}
|
||||||
// 4.签发token
|
// 4.签发token
|
||||||
loginRecordService.saveAsync(user.getUsername(), LoginRecord.TYPE_LOGIN, null, user.getTenantId(), request);
|
loginRecordService.saveAsync(user.getUsername(), LoginRecord.TYPE_LOGIN, null, user.getTenantId(), request);
|
||||||
@@ -505,6 +517,8 @@ public class WxLoginController extends BaseController {
|
|||||||
User addUser = new User();
|
User addUser = new User();
|
||||||
// 注册用户
|
// 注册用户
|
||||||
addUser.setStatus(0);
|
addUser.setStatus(0);
|
||||||
|
// 默认审核通过;前端传 auditStatus=0 时走审核流程,传其他值则按前端值
|
||||||
|
addUser.setAuditStatus(userParam.getAuditStatus() != null ? userParam.getAuditStatus() : 1);
|
||||||
addUser.setUsername(createUsername("wx_"));
|
addUser.setUsername(createUsername("wx_"));
|
||||||
addUser.setNickname("微信用户");
|
addUser.setNickname("微信用户");
|
||||||
addUser.setPlatform(MP_WEIXIN);
|
addUser.setPlatform(MP_WEIXIN);
|
||||||
|
|||||||
@@ -294,6 +294,10 @@ public class Company implements Serializable {
|
|||||||
@TableField(exist = false)
|
@TableField(exist = false)
|
||||||
private String password;
|
private String password;
|
||||||
|
|
||||||
|
@Schema(description = "应用代号(非数据库字段,注册时由前端传入,用于生成开通邮件中的域名,如 mp/shop/site)")
|
||||||
|
@TableField(exist = false)
|
||||||
|
private String appCode;
|
||||||
|
|
||||||
@Schema(description = "手机号(脱敏)")
|
@Schema(description = "手机号(脱敏)")
|
||||||
@TableField(exist = false)
|
@TableField(exist = false)
|
||||||
private String mobile;
|
private String mobile;
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ public class DictData implements Serializable {
|
|||||||
private String dictDataName;
|
private String dictDataName;
|
||||||
|
|
||||||
@Schema(description = "字段类型")
|
@Schema(description = "字段类型")
|
||||||
@TableField(exist = false)
|
|
||||||
private String type;
|
private String type;
|
||||||
|
|
||||||
@Schema(description = "字段名称")
|
@Schema(description = "字段名称")
|
||||||
|
|||||||
@@ -104,6 +104,10 @@ public class Tenant implements Serializable {
|
|||||||
@TableField(exist = false)
|
@TableField(exist = false)
|
||||||
private String phone;
|
private String phone;
|
||||||
|
|
||||||
|
@Schema(description = "超级管理员UID")
|
||||||
|
@TableField(exist = false)
|
||||||
|
private Integer ownerId;
|
||||||
|
|
||||||
@Schema(description = "管理地址")
|
@Schema(description = "管理地址")
|
||||||
@TableField(exist = false)
|
@TableField(exist = false)
|
||||||
private String adminUrl;
|
private String adminUrl;
|
||||||
|
|||||||
@@ -61,6 +61,22 @@ public class User implements UserDetails {
|
|||||||
@Schema(description = "邮箱")
|
@Schema(description = "邮箱")
|
||||||
private String email;
|
private String email;
|
||||||
|
|
||||||
|
@Schema(description = "应用代号(非数据库字段,注册时由前端传入,用于生成开通邮件中的域名,如 mp/shop/site)")
|
||||||
|
@TableField(exist = false)
|
||||||
|
private String appCode;
|
||||||
|
|
||||||
|
@Schema(description = "邮箱验证码(非数据库字段,仅用于绑定/修改邮箱时校验)")
|
||||||
|
@TableField(exist = false)
|
||||||
|
private String emailCode;
|
||||||
|
|
||||||
|
@Schema(description = "短信验证码(非数据库字段,仅用于绑定/修改手机号时校验)")
|
||||||
|
@TableField(exist = false)
|
||||||
|
private String smsCode;
|
||||||
|
|
||||||
|
@Schema(description = "新手机号短信验证码(非数据库字段,仅用于换绑手机号时二次验证新号归属)")
|
||||||
|
@TableField(exist = false)
|
||||||
|
private String smsCodeNew;
|
||||||
|
|
||||||
@Schema(description = "资质")
|
@Schema(description = "资质")
|
||||||
private String aptitude;
|
private String aptitude;
|
||||||
|
|
||||||
@@ -114,6 +130,9 @@ public class User implements UserDetails {
|
|||||||
@Schema(description = "街道地址")
|
@Schema(description = "街道地址")
|
||||||
private String address;
|
private String address;
|
||||||
|
|
||||||
|
@Schema(description = "店名")
|
||||||
|
private String shopName;
|
||||||
|
|
||||||
@Schema(description = "行业类型(父级)")
|
@Schema(description = "行业类型(父级)")
|
||||||
private String industryParent;
|
private String industryParent;
|
||||||
|
|
||||||
@@ -222,6 +241,12 @@ public class User implements UserDetails {
|
|||||||
@Schema(description = "专家角色")
|
@Schema(description = "专家角色")
|
||||||
private Integer expertType;
|
private Integer expertType;
|
||||||
|
|
||||||
|
@Schema(description = "审核状态: 0待审核, 1已通过, 2已拒绝")
|
||||||
|
private Integer auditStatus;
|
||||||
|
|
||||||
|
@Schema(description = "审核驳回原因")
|
||||||
|
private String rejectReason;
|
||||||
|
|
||||||
@Schema(description = "状态, 0正常, 1冻结")
|
@Schema(description = "状态, 0正常, 1冻结")
|
||||||
private Integer status;
|
private Integer status;
|
||||||
|
|
||||||
@@ -339,6 +364,10 @@ public class User implements UserDetails {
|
|||||||
@TableField(exist = false)
|
@TableField(exist = false)
|
||||||
private Boolean hasAdminsByPhone;
|
private Boolean hasAdminsByPhone;
|
||||||
|
|
||||||
|
@Schema(description = "是否已设置密码(未设置过密码的用户可免旧密码直接设置)")
|
||||||
|
@TableField(exist = false)
|
||||||
|
private Boolean hasPassword;
|
||||||
|
|
||||||
@Schema(description = "模板ID")
|
@Schema(description = "模板ID")
|
||||||
private Integer templateId;
|
private Integer templateId;
|
||||||
|
|
||||||
|
|||||||
@@ -36,4 +36,13 @@ public interface RoleMenuMapper extends BaseMapper<RoleMenu> {
|
|||||||
@InterceptorIgnore(tenantLine = "true")
|
@InterceptorIgnore(tenantLine = "true")
|
||||||
List<Menu> listMenuByRoleIds(@Param("roleIds") List<Integer> roleIds, @Param("menuType") Integer menuType);
|
List<Menu> listMenuByRoleIds(@Param("roleIds") List<Integer> roleIds, @Param("menuType") Integer menuType);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跨租户查询角色菜单(忽略租户拦截器)
|
||||||
|
*
|
||||||
|
* @param tenantId 租户id
|
||||||
|
* @return List<RoleMenu>
|
||||||
|
*/
|
||||||
|
@InterceptorIgnore(tenantLine = "true")
|
||||||
|
List<RoleMenu> selectListAll(@Param("tenantId") Integer tenantId);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,6 +81,15 @@ public interface UserMapper extends BaseMapper<User> {
|
|||||||
@InterceptorIgnore(tenantLine = "true")
|
@InterceptorIgnore(tenantLine = "true")
|
||||||
List<User> selectAccountsByPhone(@Param("phone") String phone);
|
List<User> selectAccountsByPhone(@Param("phone") String phone);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据邮箱查询所有账号(忽略租户隔离)
|
||||||
|
*
|
||||||
|
* @param email 邮箱
|
||||||
|
* @return List<User>
|
||||||
|
*/
|
||||||
|
@InterceptorIgnore(tenantLine = "true")
|
||||||
|
List<User> selectAccountsByEmail(@Param("email") String email);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据手机号统计账号数量(忽略租户隔离)
|
* 根据手机号统计账号数量(忽略租户隔离)
|
||||||
*
|
*
|
||||||
@@ -108,4 +117,34 @@ public interface UserMapper extends BaseMapper<User> {
|
|||||||
@InterceptorIgnore(tenantLine = "true")
|
@InterceptorIgnore(tenantLine = "true")
|
||||||
User selectLastLoginDeveloperByPhone(@Param("phone") String phone);
|
User selectLastLoginDeveloperByPhone(@Param("phone") String phone);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据手机号查询管理员账号(跨租户:超级管理员或普通管理员均可,忽略租户隔离)
|
||||||
|
*
|
||||||
|
* @param phone 手机号
|
||||||
|
* @return User
|
||||||
|
*/
|
||||||
|
@InterceptorIgnore(tenantLine = "true")
|
||||||
|
User selectAdminByPhoneCrossTenant(@Param("phone") String phone);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据手机号查询所有超级管理员账号(跨租户,忽略租户隔离)
|
||||||
|
* 用于多租户登录:手机号关联多个租户时返回列表供用户选择
|
||||||
|
*
|
||||||
|
* @param phone 手机号
|
||||||
|
* @return List<User>
|
||||||
|
*/
|
||||||
|
@InterceptorIgnore(tenantLine = "true")
|
||||||
|
List<User> selectSuperAdminsByPhone(@Param("phone") String phone);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据手机号和租户ID查询超级管理员账号(跨租户,忽略租户隔离)
|
||||||
|
* 用于多租户登录:用户选择租户后,查询该租户下的超级管理员
|
||||||
|
*
|
||||||
|
* @param phone 手机号
|
||||||
|
* @param tenantId 租户ID
|
||||||
|
* @return User
|
||||||
|
*/
|
||||||
|
@InterceptorIgnore(tenantLine = "true")
|
||||||
|
User selectSuperAdminByPhoneAndTenantId(@Param("phone") String phone, @Param("tenantId") Integer tenantId);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,4 +62,13 @@ public interface UserRoleMapper extends BaseMapper<UserRole> {
|
|||||||
*/
|
*/
|
||||||
List<UserRole> selectListRel(@Param("param") UserRoleParam param);
|
List<UserRole> selectListRel(@Param("param") UserRoleParam param);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跨租户查询用户角色(忽略租户拦截器)
|
||||||
|
*
|
||||||
|
* @param tenantId 租户id
|
||||||
|
* @return List<UserRole>
|
||||||
|
*/
|
||||||
|
@InterceptorIgnore(tenantLine = "true")
|
||||||
|
List<UserRole> selectListAll(@Param("tenantId") Integer tenantId);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,4 +39,9 @@
|
|||||||
ORDER BY a.sort_number
|
ORDER BY a.sort_number
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<!-- 跨租户查询角色菜单 -->
|
||||||
|
<select id="selectListAll" resultType="com.gxwebsoft.common.system.entity.RoleMenu">
|
||||||
|
SELECT * FROM sys_role_menu WHERE tenant_id = #{tenantId}
|
||||||
|
</select>
|
||||||
|
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<!-- 关联查询sql -->
|
<!-- 关联查询sql -->
|
||||||
<sql id="selectSql">
|
<sql id="selectSql">
|
||||||
SELECT a.*,b.company_name,b.company_logo as logo,b.admin_url,b.domain,b.free_domain,
|
SELECT a.*,b.company_name,b.company_logo as logo,b.admin_url,b.domain,b.free_domain,
|
||||||
u.phone,u.username
|
u.phone,u.username,u.user_id as ownerId
|
||||||
FROM sys_tenant a
|
FROM sys_tenant a
|
||||||
LEFT JOIN sys_company b ON a.tenant_id = b.tenant_id
|
LEFT JOIN sys_company b ON a.tenant_id = b.tenant_id
|
||||||
LEFT JOIN gxwebsoft_core.sys_user u ON u.tenant_id = a.tenant_id AND u.is_super_admin = 1 AND u.deleted = 0
|
LEFT JOIN gxwebsoft_core.sys_user u ON u.tenant_id = a.tenant_id AND u.is_super_admin = 1 AND u.deleted = 0
|
||||||
|
|||||||
@@ -110,6 +110,12 @@
|
|||||||
<if test="param.status != null">
|
<if test="param.status != null">
|
||||||
AND a.`status` = #{param.status}
|
AND a.`status` = #{param.status}
|
||||||
</if>
|
</if>
|
||||||
|
<if test="param.auditStatus != null">
|
||||||
|
AND a.`audit_status` = #{param.auditStatus}
|
||||||
|
</if>
|
||||||
|
<if test="param.auditPending != null and param.auditPending">
|
||||||
|
AND a.`audit_status` IN (0, 2)
|
||||||
|
</if>
|
||||||
<if test="param.createTimeStart != null">
|
<if test="param.createTimeStart != null">
|
||||||
AND a.create_time >= #{param.createTimeStart}
|
AND a.create_time >= #{param.createTimeStart}
|
||||||
</if>
|
</if>
|
||||||
@@ -365,6 +371,8 @@
|
|||||||
a.tenant_id,
|
a.tenant_id,
|
||||||
a.username,
|
a.username,
|
||||||
a.avatar,
|
a.avatar,
|
||||||
|
a.status,
|
||||||
|
a.password,
|
||||||
a.create_time,
|
a.create_time,
|
||||||
t.tenant_name
|
t.tenant_name
|
||||||
FROM sys_user a
|
FROM sys_user a
|
||||||
@@ -374,6 +382,24 @@
|
|||||||
ORDER BY a.create_time DESC
|
ORDER BY a.create_time DESC
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<!-- 根据邮箱查询所有账号(忽略租户隔离,用于跨租户邮箱登录) -->
|
||||||
|
<select id="selectAccountsByEmail" resultType="com.gxwebsoft.common.system.entity.User">
|
||||||
|
SELECT a.user_id,
|
||||||
|
a.tenant_id,
|
||||||
|
a.username,
|
||||||
|
a.nickname,
|
||||||
|
a.avatar,
|
||||||
|
a.status,
|
||||||
|
a.password,
|
||||||
|
a.create_time,
|
||||||
|
t.tenant_name
|
||||||
|
FROM sys_user a
|
||||||
|
LEFT JOIN sys_tenant t ON a.tenant_id = t.tenant_id
|
||||||
|
WHERE a.deleted = 0
|
||||||
|
AND a.email = #{email}
|
||||||
|
ORDER BY a.create_time DESC
|
||||||
|
</select>
|
||||||
|
|
||||||
<!-- 根据手机号统计账号数量(忽略租户隔离) -->
|
<!-- 根据手机号统计账号数量(忽略租户隔离) -->
|
||||||
<select id="countByPhone" resultType="java.lang.Integer">
|
<select id="countByPhone" resultType="java.lang.Integer">
|
||||||
SELECT COUNT(1)
|
SELECT COUNT(1)
|
||||||
@@ -430,4 +456,45 @@
|
|||||||
LIMIT 1
|
LIMIT 1
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<!-- 根据手机号查询管理员账号(跨租户:超级管理员或普通管理员均可,忽略租户隔离) -->
|
||||||
|
<select id="selectAdminByPhoneCrossTenant" resultType="com.gxwebsoft.common.system.entity.User">
|
||||||
|
SELECT u.*
|
||||||
|
FROM sys_user u
|
||||||
|
WHERE u.deleted = 0
|
||||||
|
AND u.phone = #{phone}
|
||||||
|
AND (u.is_super_admin = 1 OR u.is_admin = 1)
|
||||||
|
ORDER BY u.update_time DESC,
|
||||||
|
u.user_id DESC
|
||||||
|
LIMIT 1
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 根据手机号查询所有超级管理员账号(跨租户,忽略租户隔离,用于多租户登录选择) -->
|
||||||
|
<select id="selectSuperAdminsByPhone" resultType="com.gxwebsoft.common.system.entity.User">
|
||||||
|
SELECT u.*,
|
||||||
|
t.tenant_name
|
||||||
|
FROM sys_user u
|
||||||
|
LEFT JOIN sys_tenant t ON u.tenant_id = t.tenant_id
|
||||||
|
WHERE u.deleted = 0
|
||||||
|
AND u.status = 0
|
||||||
|
AND u.phone = #{phone}
|
||||||
|
AND (u.is_super_admin = 1 OR u.is_admin = 1)
|
||||||
|
ORDER BY u.update_time DESC,
|
||||||
|
u.create_time DESC,
|
||||||
|
u.user_id DESC
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 根据手机号和租户ID查询超级管理员账号(跨租户,忽略租户隔离,用于选择租户后登录) -->
|
||||||
|
<select id="selectSuperAdminByPhoneAndTenantId" resultType="com.gxwebsoft.common.system.entity.User">
|
||||||
|
SELECT u.*,
|
||||||
|
t.tenant_name
|
||||||
|
FROM sys_user u
|
||||||
|
LEFT JOIN sys_tenant t ON u.tenant_id = t.tenant_id
|
||||||
|
WHERE u.deleted = 0
|
||||||
|
AND u.status = 0
|
||||||
|
AND u.phone = #{phone}
|
||||||
|
AND u.is_super_admin = 1
|
||||||
|
AND u.tenant_id = #{tenantId}
|
||||||
|
LIMIT 1
|
||||||
|
</select>
|
||||||
|
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|||||||
@@ -64,4 +64,9 @@
|
|||||||
<include refid="selectSql"></include>
|
<include refid="selectSql"></include>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<!-- 跨租户查询用户角色 -->
|
||||||
|
<select id="selectListAll" resultType="com.gxwebsoft.common.system.entity.UserRole">
|
||||||
|
SELECT * FROM sys_user_role WHERE tenant_id = #{tenantId}
|
||||||
|
</select>
|
||||||
|
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.gxwebsoft.common.system.param;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送邮箱验证码参数
|
||||||
|
*
|
||||||
|
* @author WebSoft
|
||||||
|
* @since 2026-07-20
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
@Schema(description = "发送邮箱验证码参数")
|
||||||
|
public class EmailCaptchaParam implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Schema(description = "邮箱")
|
||||||
|
private String email;
|
||||||
|
|
||||||
|
@Schema(description = "邮箱验证码(校验时使用)")
|
||||||
|
private String code;
|
||||||
|
|
||||||
|
@Schema(description = "租户ID")
|
||||||
|
private String tenantId;
|
||||||
|
|
||||||
|
@Schema(description = "场景")
|
||||||
|
private String scene;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -28,6 +28,9 @@ public class LoginParam implements Serializable {
|
|||||||
@Schema(description = "手机号码")
|
@Schema(description = "手机号码")
|
||||||
private String phone;
|
private String phone;
|
||||||
|
|
||||||
|
@Schema(description = "邮箱")
|
||||||
|
private String email;
|
||||||
|
|
||||||
@Schema(description = "短信验证码")
|
@Schema(description = "短信验证码")
|
||||||
private String code;
|
private String code;
|
||||||
|
|
||||||
@@ -55,4 +58,7 @@ public class LoginParam implements Serializable {
|
|||||||
@Schema(description = "租户id")
|
@Schema(description = "租户id")
|
||||||
private Integer tenantId;
|
private Integer tenantId;
|
||||||
|
|
||||||
|
@Schema(description = "审核状态: 0待审核, 1已通过, 2已拒绝(前端传值决定注册是否走审核流程)")
|
||||||
|
private Integer auditStatus;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,20 +21,22 @@ import java.io.Serializable;
|
|||||||
public class ResetPasswordParam implements Serializable {
|
public class ResetPasswordParam implements Serializable {
|
||||||
private static final long serialVersionUID = 1L;
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
@NotBlank(message = "手机号不能为空")
|
@Schema(description = "手机号码(手机找回时必填)")
|
||||||
@Schema(description = "手机号码", required = true)
|
|
||||||
private String phone;
|
private String phone;
|
||||||
|
|
||||||
@NotBlank(message = "短信验证码不能为空")
|
@Schema(description = "短信验证码(手机找回时必填)")
|
||||||
@Schema(description = "短信验证码", required = true)
|
|
||||||
private String smsCode;
|
private String smsCode;
|
||||||
|
|
||||||
@NotBlank(message = "用户ID不能为空")
|
@Schema(description = "邮箱(邮箱找回时必填)")
|
||||||
@Schema(description = "用户ID", required = true)
|
private String email;
|
||||||
|
|
||||||
|
@Schema(description = "邮箱验证码(邮箱找回时必填)")
|
||||||
|
private String emailCode;
|
||||||
|
|
||||||
|
@Schema(description = "用户ID(未登录忘记密码场景可不传,后端按手机号查用户)")
|
||||||
private String userId;
|
private String userId;
|
||||||
|
|
||||||
@NotNull(message = "租户ID不能为空")
|
@Schema(description = "租户ID(未登录忘记密码场景可不传)")
|
||||||
@Schema(description = "租户ID", required = true)
|
|
||||||
private Integer tenantId;
|
private Integer tenantId;
|
||||||
|
|
||||||
@NotBlank(message = "新密码不能为空")
|
@NotBlank(message = "新密码不能为空")
|
||||||
@@ -47,7 +49,6 @@ public class ResetPasswordParam implements Serializable {
|
|||||||
@Schema(description = "确认密码", required = true)
|
@Schema(description = "确认密码", required = true)
|
||||||
private String confirmPassword;
|
private String confirmPassword;
|
||||||
|
|
||||||
@NotNull(message = "模板ID不能为空")
|
@Schema(description = "短信模板ID(可选)")
|
||||||
@Schema(description = "短信模板ID", required = true)
|
|
||||||
private Integer templateId;
|
private Integer templateId;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.gxwebsoft.common.system.param;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 短信验证码重置支付密码参数
|
||||||
|
*
|
||||||
|
* @author WebSoft
|
||||||
|
* @since 2026-08-03
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
@Schema(description = "短信验证码重置支付密码参数")
|
||||||
|
public class ResetPayPasswordParam implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Schema(description = "手机号码")
|
||||||
|
private String phone;
|
||||||
|
|
||||||
|
@Schema(description = "短信验证码")
|
||||||
|
private String code;
|
||||||
|
|
||||||
|
@Schema(description = "新的支付密码(4位数字,明文透传,后端加密落库)")
|
||||||
|
private String payPassword;
|
||||||
|
}
|
||||||
@@ -110,6 +110,14 @@ public class UserParam extends BaseParam {
|
|||||||
@QueryField(type = QueryType.EQ)
|
@QueryField(type = QueryType.EQ)
|
||||||
private Integer status;
|
private Integer status;
|
||||||
|
|
||||||
|
@Schema(description = "审核状态: 0待审核, 1已通过, 2已拒绝(前端传值决定注册是否走审核流程)")
|
||||||
|
@QueryField(type = QueryType.EQ)
|
||||||
|
private Integer auditStatus;
|
||||||
|
|
||||||
|
@Schema(description = "是否查询待处理审核用户(待审核0/被驳回2)")
|
||||||
|
@TableField(exist = false)
|
||||||
|
private Boolean auditPending;
|
||||||
|
|
||||||
@Schema(description = "是否删除, 0否, 1是")
|
@Schema(description = "是否删除, 0否, 1是")
|
||||||
@TableLogic
|
@TableLogic
|
||||||
private Integer deleted;
|
private Integer deleted;
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.gxwebsoft.common.system.result;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查邮箱返回结果
|
||||||
|
*
|
||||||
|
* @author WebSoft
|
||||||
|
* @since 2026-08-06
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Schema(description = "检查邮箱返回结果")
|
||||||
|
public class CheckEmailResult implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Schema(description = "是否已注册")
|
||||||
|
private Boolean isRegistered;
|
||||||
|
|
||||||
|
@Schema(description = "账号数量")
|
||||||
|
private Integer accountCount;
|
||||||
|
}
|
||||||
@@ -2,12 +2,11 @@ package com.gxwebsoft.common.system.result;
|
|||||||
|
|
||||||
import com.gxwebsoft.common.system.entity.User;
|
import com.gxwebsoft.common.system.entity.User;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 登录返回结果
|
* 登录返回结果
|
||||||
@@ -17,7 +16,6 @@ import java.io.Serializable;
|
|||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
|
||||||
@Schema(description = "登录返回结果")
|
@Schema(description = "登录返回结果")
|
||||||
public class LoginResult implements Serializable {
|
public class LoginResult implements Serializable {
|
||||||
private static final long serialVersionUID = 1L;
|
private static final long serialVersionUID = 1L;
|
||||||
@@ -28,4 +26,16 @@ public class LoginResult implements Serializable {
|
|||||||
@Schema(description = "用户信息")
|
@Schema(description = "用户信息")
|
||||||
private User user;
|
private User user;
|
||||||
|
|
||||||
|
@Schema(description = "租户列表(多租户场景下返回,此时不返回access_token)")
|
||||||
|
private List<TenantOption> tenants;
|
||||||
|
|
||||||
|
public LoginResult(String access_token, User user) {
|
||||||
|
this.access_token = access_token;
|
||||||
|
this.user = user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LoginResult(List<TenantOption> tenants) {
|
||||||
|
this.tenants = tenants;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package com.gxwebsoft.common.system.result;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 租户选项(多租户登录时返回,供前端展示选择列表)
|
||||||
|
*
|
||||||
|
* @author WebSoft
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Schema(description = "租户选项")
|
||||||
|
public class TenantOption implements Serializable {
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Schema(description = "租户ID")
|
||||||
|
private Integer tenantId;
|
||||||
|
|
||||||
|
@Schema(description = "租户名称")
|
||||||
|
private String tenantName;
|
||||||
|
|
||||||
|
@Schema(description = "用户ID")
|
||||||
|
private Integer userId;
|
||||||
|
|
||||||
|
@Schema(description = "用户名")
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
@Schema(description = "昵称")
|
||||||
|
private String nickname;
|
||||||
|
|
||||||
|
@Schema(description = "角色名称")
|
||||||
|
private String roleName;
|
||||||
|
|
||||||
|
@Schema(description = "头像")
|
||||||
|
private String avatar;
|
||||||
|
|
||||||
|
public TenantOption() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public TenantOption(Integer tenantId, String tenantName, Integer userId,
|
||||||
|
String username, String nickname, String roleName, String avatar) {
|
||||||
|
this.tenantId = tenantId;
|
||||||
|
this.tenantName = tenantName;
|
||||||
|
this.userId = userId;
|
||||||
|
this.username = username;
|
||||||
|
this.nickname = nickname;
|
||||||
|
this.roleName = roleName;
|
||||||
|
this.avatar = avatar;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.gxwebsoft.common.system.service;
|
package com.gxwebsoft.common.system.service;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.extension.service.IService;
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
import com.gxwebsoft.common.core.web.ApiResult;
|
||||||
import com.gxwebsoft.common.core.web.PageResult;
|
import com.gxwebsoft.common.core.web.PageResult;
|
||||||
import com.gxwebsoft.common.system.entity.Company;
|
import com.gxwebsoft.common.system.entity.Company;
|
||||||
import com.gxwebsoft.common.system.entity.Tenant;
|
import com.gxwebsoft.common.system.entity.Tenant;
|
||||||
@@ -45,4 +46,14 @@ public interface TenantService extends IService<Tenant> {
|
|||||||
boolean destructionAll(Integer tenantId);
|
boolean destructionAll(Integer tenantId);
|
||||||
|
|
||||||
Tenant getByCodeRel(String code);
|
Tenant getByCodeRel(String code);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 授予当前租户cms建站权限
|
||||||
|
* 从参考租户复制cms:*菜单到当前租户, 并绑定到当前租户的超级管理员角色
|
||||||
|
*
|
||||||
|
* @param refTenant 参考租户id(已配置好cms菜单的租户)
|
||||||
|
* @param curTenant 当前租户id
|
||||||
|
* @return ApiResult<?>
|
||||||
|
*/
|
||||||
|
ApiResult<?> grantCmsPermission(Integer refTenant, Integer curTenant);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,6 +76,14 @@ public interface UserService extends IService<User>, UserDetailsService {
|
|||||||
*/
|
*/
|
||||||
boolean updateUser(User user);
|
boolean updateUser(User user);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将用户标记为开发者(审核通过时调用,直接更新用户标记位,不新增角色)
|
||||||
|
*
|
||||||
|
* @param userId 用户id
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
boolean markAsDeveloper(Integer userId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 比较用户密码
|
* 比较用户密码
|
||||||
*
|
*
|
||||||
@@ -144,6 +152,33 @@ public interface UserService extends IService<User>, UserDetailsService {
|
|||||||
*/
|
*/
|
||||||
User getLastLoginDeveloperByPhone(String phone);
|
User getLastLoginDeveloperByPhone(String phone);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据手机号查询管理员账号(跨租户:超级管理员或普通管理员均可,忽略租户隔离)
|
||||||
|
*
|
||||||
|
* @param phone 手机号
|
||||||
|
* @return 用户信息
|
||||||
|
*/
|
||||||
|
User getByPhoneAndAdmin(String phone);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据手机号查询所有超级管理员账号(跨租户,忽略租户隔离)
|
||||||
|
* 用于多租户登录:手机号关联多个租户时返回列表供用户选择
|
||||||
|
*
|
||||||
|
* @param phone 手机号
|
||||||
|
* @return List<User>
|
||||||
|
*/
|
||||||
|
List<User> getSuperAdminsByPhone(String phone);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据手机号和租户ID查询超级管理员账号(跨租户,忽略租户隔离)
|
||||||
|
* 用于多租户登录:用户选择租户后,查询该租户下的超级管理员
|
||||||
|
*
|
||||||
|
* @param phone 手机号
|
||||||
|
* @param tenantId 租户ID
|
||||||
|
* @return User
|
||||||
|
*/
|
||||||
|
User getSuperAdminByPhoneAndTenantId(String phone, Integer tenantId);
|
||||||
|
|
||||||
List<User> pageAll(UserParam param);
|
List<User> pageAll(UserParam param);
|
||||||
|
|
||||||
User getByUserId(String userId);
|
User getByUserId(String userId);
|
||||||
@@ -156,6 +191,14 @@ public interface UserService extends IService<User>, UserDetailsService {
|
|||||||
*/
|
*/
|
||||||
List<User> findAccountsByPhone(String phone);
|
List<User> findAccountsByPhone(String phone);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据邮箱查询所有账号(忽略租户隔离,用于跨租户登录)
|
||||||
|
*
|
||||||
|
* @param email 邮箱
|
||||||
|
* @return List<User>
|
||||||
|
*/
|
||||||
|
List<User> findAccountsByEmail(String email);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据手机号统计账号数量
|
* 根据手机号统计账号数量
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import com.aliyun.oss.ClientException;
|
|||||||
import com.aliyun.oss.OSS;
|
import com.aliyun.oss.OSS;
|
||||||
import com.aliyun.oss.OSSClientBuilder;
|
import com.aliyun.oss.OSSClientBuilder;
|
||||||
import com.aliyun.oss.OSSException;
|
import com.aliyun.oss.OSSException;
|
||||||
|
import com.aliyun.oss.ClientBuilderConfiguration;
|
||||||
|
import com.aliyun.oss.common.comm.SignVersion;
|
||||||
import com.aliyun.oss.common.auth.CredentialsProvider;
|
import com.aliyun.oss.common.auth.CredentialsProvider;
|
||||||
import com.aliyun.oss.common.auth.DefaultCredentialProvider;
|
import com.aliyun.oss.common.auth.DefaultCredentialProvider;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
@@ -20,6 +22,8 @@ import com.gxwebsoft.common.system.param.FileRecordParam;
|
|||||||
import com.gxwebsoft.common.system.service.CompanyService;
|
import com.gxwebsoft.common.system.service.CompanyService;
|
||||||
import com.gxwebsoft.common.system.service.FileRecordService;
|
import com.gxwebsoft.common.system.service.FileRecordService;
|
||||||
import com.gxwebsoft.common.system.service.SettingService;
|
import com.gxwebsoft.common.system.service.SettingService;
|
||||||
|
import io.minio.MinioClient;
|
||||||
|
import io.minio.RemoveObjectArgs;
|
||||||
import org.springframework.scheduling.annotation.Async;
|
import org.springframework.scheduling.annotation.Async;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@@ -93,13 +97,38 @@ public class FileRecordServiceImpl extends ServiceImpl<FileRecordMapper, FileRec
|
|||||||
String bucketName = uploadConfig.getString("bucketName");
|
String bucketName = uploadConfig.getString("bucketName");
|
||||||
String accessKeyId = uploadConfig.getString("accessKeyId");
|
String accessKeyId = uploadConfig.getString("accessKeyId");
|
||||||
String accessKeySecret = uploadConfig.getString("accessKeySecret");
|
String accessKeySecret = uploadConfig.getString("accessKeySecret");
|
||||||
CredentialsProvider credentialsProvider = new DefaultCredentialProvider(accessKeyId, accessKeySecret);
|
String uploadMethod = uploadConfig.getString("uploadMethod");
|
||||||
OSS ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider);
|
// 规范化 endpoint:去掉结尾斜杠,避免 minio-java 拼接出双斜杠导致签名不匹配
|
||||||
|
if (StrUtil.isNotBlank(endpoint) && endpoint.endsWith("/")) {
|
||||||
|
endpoint = endpoint.substring(0, endpoint.length() - 1);
|
||||||
|
}
|
||||||
|
boolean isMinio = "minio".equals(uploadMethod);
|
||||||
|
OSS ossClient = null;
|
||||||
|
MinioClient minioClient = null;
|
||||||
|
if (isMinio) {
|
||||||
|
// MinIO 用官方 minio-java SDK(原生 AWS4 + path-style),aliyun OSS SDK 无法对接 MinIO
|
||||||
|
// 显式指定 region=us-east-1,跳过 getBucketLocation 自动探测(该探测 GET 会触发 SignatureDoesNotMatch)
|
||||||
|
minioClient = MinioClient.builder()
|
||||||
|
.endpoint(endpoint)
|
||||||
|
.credentials(accessKeyId, accessKeySecret)
|
||||||
|
.region("us-east-1")
|
||||||
|
.build();
|
||||||
|
} else {
|
||||||
|
CredentialsProvider credentialsProvider = new DefaultCredentialProvider(accessKeyId, accessKeySecret);
|
||||||
|
ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider);
|
||||||
|
}
|
||||||
for (FileRecord fileRecord : fileRecords) {
|
for (FileRecord fileRecord : fileRecords) {
|
||||||
fileRecord.setPath(StrUtil.replace(fileRecord.getPath(), bucketDomain.concat("/"), ""));
|
fileRecord.setPath(StrUtil.replace(fileRecord.getPath(), bucketDomain.concat("/"), ""));
|
||||||
try {
|
try {
|
||||||
// 删除远程文件
|
// 删除远程文件
|
||||||
ossClient.deleteObject(bucketName, fileRecord.getPath());
|
if (isMinio) {
|
||||||
|
minioClient.removeObject(RemoveObjectArgs.builder()
|
||||||
|
.bucket(bucketName)
|
||||||
|
.object(fileRecord.getPath())
|
||||||
|
.build());
|
||||||
|
} else {
|
||||||
|
ossClient.deleteObject(bucketName, fileRecord.getPath());
|
||||||
|
}
|
||||||
// 释放空间大小
|
// 释放空间大小
|
||||||
if (fileRecord.getCompanyId() > 0) {
|
if (fileRecord.getCompanyId() > 0) {
|
||||||
Company company = companyService.getById(fileRecord.getCompanyId());
|
Company company = companyService.getById(fileRecord.getCompanyId());
|
||||||
|
|||||||
@@ -2,21 +2,39 @@ package com.gxwebsoft.common.system.service.impl;
|
|||||||
|
|
||||||
import cn.hutool.core.date.DateUtil;
|
import cn.hutool.core.date.DateUtil;
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import com.gxwebsoft.common.core.Constants;
|
||||||
import com.gxwebsoft.common.core.utils.CommonUtil;
|
import com.gxwebsoft.common.core.utils.CommonUtil;
|
||||||
import com.gxwebsoft.common.core.utils.DomainUtil;
|
import com.gxwebsoft.common.core.utils.DomainUtil;
|
||||||
import com.gxwebsoft.common.core.utils.RedisUtil;
|
import com.gxwebsoft.common.core.utils.RedisUtil;
|
||||||
import com.gxwebsoft.common.system.entity.*;
|
import com.gxwebsoft.common.system.entity.*;
|
||||||
|
import com.gxwebsoft.common.system.mapper.MenuMapper;
|
||||||
|
import com.gxwebsoft.common.system.mapper.RoleMapper;
|
||||||
|
import com.gxwebsoft.common.system.mapper.RoleMenuMapper;
|
||||||
|
import com.gxwebsoft.common.system.mapper.UserRoleMapper;
|
||||||
import com.gxwebsoft.common.system.mapper.TenantMapper;
|
import com.gxwebsoft.common.system.mapper.TenantMapper;
|
||||||
import com.gxwebsoft.common.system.param.MenuParam;
|
import com.gxwebsoft.common.system.param.MenuParam;
|
||||||
|
import com.gxwebsoft.common.system.param.RoleParam;
|
||||||
import com.gxwebsoft.common.system.service.*;
|
import com.gxwebsoft.common.system.service.*;
|
||||||
import com.gxwebsoft.common.system.param.TenantParam;
|
import com.gxwebsoft.common.system.param.TenantParam;
|
||||||
|
import com.gxwebsoft.common.core.web.ApiResult;
|
||||||
import com.gxwebsoft.common.core.web.PageParam;
|
import com.gxwebsoft.common.core.web.PageParam;
|
||||||
import com.gxwebsoft.common.core.web.PageResult;
|
import com.gxwebsoft.common.core.web.PageResult;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 租户Service实现
|
* 租户Service实现
|
||||||
@@ -24,14 +42,24 @@ import java.util.List;
|
|||||||
* @author 科技小王子
|
* @author 科技小王子
|
||||||
* @since 2023-07-17 17:49:53
|
* @since 2023-07-17 17:49:53
|
||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
public class TenantServiceImpl extends ServiceImpl<TenantMapper, Tenant> implements TenantService {
|
public class TenantServiceImpl extends ServiceImpl<TenantMapper, Tenant> implements TenantService {
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private CompanyService companyService;
|
private CompanyService companyService;
|
||||||
@Resource
|
@Resource
|
||||||
private MenuService menuService;
|
private MenuService menuService;
|
||||||
@Resource
|
@Resource
|
||||||
|
private MenuMapper menuMapper;
|
||||||
|
@Resource
|
||||||
|
private RoleMapper roleMapper;
|
||||||
|
@Resource
|
||||||
|
private RoleMenuMapper roleMenuMapper;
|
||||||
|
@Resource
|
||||||
|
private UserRoleMapper userRoleMapper;
|
||||||
|
@Resource
|
||||||
|
private RoleMenuService roleMenuService;
|
||||||
|
@Resource
|
||||||
private RoleService roleService;
|
private RoleService roleService;
|
||||||
@Resource
|
@Resource
|
||||||
private UserRoleService userRoleService;
|
private UserRoleService userRoleService;
|
||||||
@@ -108,9 +136,10 @@ public class TenantServiceImpl extends ServiceImpl<TenantMapper, Tenant> impleme
|
|||||||
superAdmin.setUsername("superAdmin");
|
superAdmin.setUsername("superAdmin");
|
||||||
superAdmin.setNickname(company.getShortName());
|
superAdmin.setNickname(company.getShortName());
|
||||||
superAdmin.setPhone(company.getPhone());
|
superAdmin.setPhone(company.getPhone());
|
||||||
|
superAdmin.setIsSuperAdmin(true);
|
||||||
|
superAdmin.setIsAdmin(true);
|
||||||
superAdmin.setEmail(company.getEmail());
|
superAdmin.setEmail(company.getEmail());
|
||||||
superAdmin.setTemplateId(company.getTemplateId());
|
superAdmin.setTemplateId(company.getTemplateId());
|
||||||
superAdmin.setIsAdmin(true);
|
|
||||||
superAdmin.setRealName(company.getBusinessEntity());
|
superAdmin.setRealName(company.getBusinessEntity());
|
||||||
superAdmin.setPassword(userService.encodePassword("$2a$10$iMsEmh.rPlzwy/SVe6KW3.62vlwqMJpibhCF9jYN.fMqxdqymzMzu"));
|
superAdmin.setPassword(userService.encodePassword("$2a$10$iMsEmh.rPlzwy/SVe6KW3.62vlwqMJpibhCF9jYN.fMqxdqymzMzu"));
|
||||||
if (company.getPassword() != null) {
|
if (company.getPassword() != null) {
|
||||||
@@ -590,17 +619,38 @@ public class TenantServiceImpl extends ServiceImpl<TenantMapper, Tenant> impleme
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
// 发送邮件通知
|
// 发送邮件通知(按应用代号区分域名:前端注册时传入 appCode,未知默认 site)
|
||||||
String title = "恭喜!您的应用已创建成功";
|
// mp -> https://mp-{tid}.shoplnk.cn 后台 https://mp.websoft.top
|
||||||
String appUrl = "\r\n应用地址:" + DomainUtil.getSiteUrl(company.getTid().toString());
|
// shop -> https://shop-{tid}.shoplnk.cn 后台 https://shop.websoft.top
|
||||||
String appName = "\r\n应用名称:" + company.getShortName();
|
// site -> https://site-{tid}.sitelnk.cn 后台 https://site.websoft.top
|
||||||
String adminUrl = "\r\n后台管理:" + DomainUtil.getAdminUrl(company.getTid().toString());
|
final Map<String, String[]> appDomainMap = new HashMap<>(3);
|
||||||
String account = "\r\n账号:admin";
|
appDomainMap.put("mp", new String[]{"mp", "shoplnk.cn", "mp.websoft.top"});
|
||||||
String password = "\r\n密码:" + company.getPassword();
|
appDomainMap.put("shop", new String[]{"shop", "shoplnk.cn", "shop.websoft.top"});
|
||||||
String content = title + appUrl + appName + adminUrl + account + password;
|
appDomainMap.put("site", new String[]{"site", "sitelnk.cn", "site.websoft.top"});
|
||||||
|
final String appCode = StrUtil.isBlank(company.getAppCode()) ? "site" : company.getAppCode();
|
||||||
|
final String[] domainCfg = appDomainMap.getOrDefault(appCode, appDomainMap.get("site"));
|
||||||
|
final String sitePrefix = domainCfg[0];
|
||||||
|
final String siteDomain = domainCfg[1];
|
||||||
|
final String adminHost = domainCfg[2];
|
||||||
|
final String siteUrl = "https://" + sitePrefix + "-" + company.getTid() + "." + siteDomain;
|
||||||
|
final String adminUrl = "https://" + adminHost;
|
||||||
|
final String title = "您的企业官网已开通成功";
|
||||||
|
final StringBuilder content = new StringBuilder();
|
||||||
|
content.append("尊敬的用户:\r\n\r\n");
|
||||||
|
content.append("恭喜!您的企业官网「").append(company.getShortName()).append("」已开通成功。\r\n\r\n");
|
||||||
|
content.append("您可以使用以下信息登录管理后台,继续完善站点内容:\r\n\r\n");
|
||||||
|
content.append("• 官网地址:").append(siteUrl).append("\r\n");
|
||||||
|
content.append("• 后台地址:").append(adminUrl).append("\r\n");
|
||||||
|
content.append("• 登录账号:").append(company.getEmail()).append("\r\n");
|
||||||
|
content.append("• 登录密码:").append(company.getPassword()).append("\r\n\r\n");
|
||||||
|
content.append("安全提示:\r\n");
|
||||||
|
content.append("1. 请妥善保管登录密码,不要通过邮件、聊天工具发送给他人。\r\n");
|
||||||
|
content.append("2. 建议首次登录后立即进入「个人中心 > 修改密码」重置密码。\r\n\r\n");
|
||||||
|
content.append("点击登录后台:").append(adminUrl).append("\r\n\r\n");
|
||||||
|
content.append("如有疑问,请联系客服或访问帮助中心。");
|
||||||
// 发送邮件通知
|
// 发送邮件通知
|
||||||
if (company.getEmail() != null) {
|
if (company.getEmail() != null) {
|
||||||
emailRecordService.sendEmail(title, content, company.getEmail(), company.getTid());
|
emailRecordService.sendEmail(title, content.toString(), company.getEmail(), company.getTid());
|
||||||
}
|
}
|
||||||
return company;
|
return company;
|
||||||
}
|
}
|
||||||
@@ -631,4 +681,220 @@ public class TenantServiceImpl extends ServiceImpl<TenantMapper, Tenant> impleme
|
|||||||
return param.getOne(baseMapper.selectListRel(param));
|
return param.getOne(baseMapper.selectListRel(param));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = {Exception.class})
|
||||||
|
public ApiResult<?> grantCmsPermission(Integer refTenant, Integer curTenant) {
|
||||||
|
if (refTenant == null || curTenant == null) {
|
||||||
|
return new ApiResult<>(Constants.RESULT_ERROR_CODE, "参考租户或当前租户为空");
|
||||||
|
}
|
||||||
|
if (refTenant.equals(curTenant)) {
|
||||||
|
return new ApiResult<>(Constants.RESULT_ERROR_CODE, "参考租户不能与当前租户相同");
|
||||||
|
}
|
||||||
|
final Map<String, Object> data = new HashMap<>();
|
||||||
|
data.put("refTenantId", refTenant);
|
||||||
|
data.put("tenantId", curTenant);
|
||||||
|
|
||||||
|
// ===== 1. 克隆角色 sys_role(按 role_code 去重,superAdmin/admin/user 已存在则复用)=====
|
||||||
|
final RoleParam refRoleParam = new RoleParam();
|
||||||
|
refRoleParam.setTenantId(refTenant);
|
||||||
|
final List<Role> refRoles = roleMapper.selectListAll(refRoleParam);
|
||||||
|
if (CollectionUtils.isEmpty(refRoles)) {
|
||||||
|
return new ApiResult<>(Constants.RESULT_ERROR_CODE, "参考租户[" + refTenant + "]没有可复制的角色");
|
||||||
|
}
|
||||||
|
final RoleParam curRoleParam = new RoleParam();
|
||||||
|
curRoleParam.setTenantId(curTenant);
|
||||||
|
// role_code -> Role(当前租户已存在的角色)
|
||||||
|
final Map<String, Role> curRoleByCode = roleMapper.selectListAll(curRoleParam).stream()
|
||||||
|
.collect(Collectors.toMap(Role::getRoleCode, r -> r, (a, b) -> a));
|
||||||
|
// oldRoleId -> newRoleId 映射
|
||||||
|
final Map<Integer, Integer> roleIdMapping = new HashMap<>();
|
||||||
|
// 待新增角色(按 role_code 去重)
|
||||||
|
final Map<String, Role> toCreateByCode = new LinkedHashMap<>();
|
||||||
|
for (Role r : refRoles) {
|
||||||
|
final Role exist = curRoleByCode.get(r.getRoleCode());
|
||||||
|
if (exist != null) {
|
||||||
|
roleIdMapping.put(r.getRoleId(), exist.getRoleId());
|
||||||
|
} else if (!toCreateByCode.containsKey(r.getRoleCode())) {
|
||||||
|
final Role copy = new Role();
|
||||||
|
copy.setRoleCode(r.getRoleCode());
|
||||||
|
copy.setRoleName(r.getRoleName());
|
||||||
|
copy.setComments(r.getComments());
|
||||||
|
copy.setSortNumber(r.getSortNumber());
|
||||||
|
copy.setTenantId(curTenant);
|
||||||
|
toCreateByCode.put(r.getRoleCode(), copy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int roleCreated = 0;
|
||||||
|
if (!toCreateByCode.isEmpty()) {
|
||||||
|
roleService.saveBatch(new ArrayList<>(toCreateByCode.values()));
|
||||||
|
// 重新读取当前租户角色,回填 oldRoleId -> newRoleId 映射
|
||||||
|
final Map<String, Role> refreshed = roleMapper.selectListAll(curRoleParam).stream()
|
||||||
|
.collect(Collectors.toMap(Role::getRoleCode, r -> r, (a, b) -> a));
|
||||||
|
for (Role r : refRoles) {
|
||||||
|
final Role nr = refreshed.get(r.getRoleCode());
|
||||||
|
if (nr != null) {
|
||||||
|
roleIdMapping.put(r.getRoleId(), nr.getRoleId());
|
||||||
|
curRoleByCode.putIfAbsent(r.getRoleCode(), nr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
roleCreated = toCreateByCode.size();
|
||||||
|
}
|
||||||
|
// 当前租户全部角色:roleId -> Role(含新克隆的),供后续 role_code 对齐使用
|
||||||
|
final Map<Integer, Role> newRoleById = new HashMap<>();
|
||||||
|
curRoleByCode.values().forEach(r -> newRoleById.put(r.getRoleId(), r));
|
||||||
|
|
||||||
|
// ===== 2. 克隆菜单 sys_menu(按层级复制,父先于子,标题/权限去重)=====
|
||||||
|
final MenuParam refMenuParam = new MenuParam();
|
||||||
|
refMenuParam.setTenantId(refTenant);
|
||||||
|
refMenuParam.setDeleted(0);
|
||||||
|
final List<Menu> refMenus = menuMapper.getMenuByClone(refMenuParam);
|
||||||
|
if (CollectionUtils.isEmpty(refMenus)) {
|
||||||
|
return new ApiResult<>(Constants.RESULT_ERROR_CODE, "参考租户[" + refTenant + "]没有可复制的菜单");
|
||||||
|
}
|
||||||
|
final Map<Integer, Menu> refMenuMap = new HashMap<>();
|
||||||
|
for (Menu menu : refMenus) {
|
||||||
|
refMenuMap.put(menu.getMenuId(), menu);
|
||||||
|
}
|
||||||
|
final MenuParam curMenuParam = new MenuParam();
|
||||||
|
curMenuParam.setTenantId(curTenant);
|
||||||
|
curMenuParam.setDeleted(0);
|
||||||
|
final Map<String, Integer> curMenuKeys = new HashMap<>();
|
||||||
|
for (Menu menu : menuMapper.getMenuByClone(curMenuParam)) {
|
||||||
|
curMenuKeys.putIfAbsent(getMenuKey(menu), menu.getMenuId());
|
||||||
|
}
|
||||||
|
final List<Menu> sources = refMenus.stream()
|
||||||
|
.sorted(Comparator.comparingInt((Menu d) -> getMenuDepth(d, refMenuMap)))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
final Map<Integer, Integer> menuIdMapping = new HashMap<>();
|
||||||
|
int menuCount = 0;
|
||||||
|
for (Menu source : sources) {
|
||||||
|
final Integer existsMenuId = curMenuKeys.get(getMenuKey(source));
|
||||||
|
if (existsMenuId != null) {
|
||||||
|
menuIdMapping.put(source.getMenuId(), existsMenuId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final Menu menu = new Menu();
|
||||||
|
menu.setParentId(source.getParentId() == null ? 0 : menuIdMapping.getOrDefault(source.getParentId(), 0));
|
||||||
|
menu.setTitle(source.getTitle());
|
||||||
|
menu.setPath(source.getPath());
|
||||||
|
menu.setComponent(source.getComponent());
|
||||||
|
menu.setModules(source.getModules());
|
||||||
|
menu.setModulesUrl(source.getModulesUrl());
|
||||||
|
menu.setMenuType(source.getMenuType());
|
||||||
|
menu.setSortNumber(source.getSortNumber());
|
||||||
|
menu.setAuthority(source.getAuthority());
|
||||||
|
menu.setIcon(source.getIcon());
|
||||||
|
menu.setHide(source.getHide());
|
||||||
|
menu.setMeta(source.getMeta());
|
||||||
|
menu.setTenantId(curTenant);
|
||||||
|
menuService.save(menu);
|
||||||
|
menuIdMapping.put(source.getMenuId(), menu.getMenuId());
|
||||||
|
curMenuKeys.putIfAbsent(getMenuKey(menu), menu.getMenuId());
|
||||||
|
menuCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 3. 克隆角色菜单 sys_role_menu(roleId/menuId 用映射转换,去重)=====
|
||||||
|
final Set<String> curRmKeys = roleMenuMapper.selectListAll(curTenant).stream()
|
||||||
|
.map(rm -> rm.getRoleId() + "_" + rm.getMenuId())
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
final List<RoleMenu> rmsToSave = new ArrayList<>();
|
||||||
|
for (RoleMenu rm : roleMenuMapper.selectListAll(refTenant)) {
|
||||||
|
final Integer newRoleId = roleIdMapping.get(rm.getRoleId());
|
||||||
|
final Integer newMenuId = menuIdMapping.get(rm.getMenuId());
|
||||||
|
if (newRoleId == null || newMenuId == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final String key = newRoleId + "_" + newMenuId;
|
||||||
|
if (curRmKeys.contains(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final RoleMenu copy = new RoleMenu();
|
||||||
|
copy.setRoleId(newRoleId);
|
||||||
|
copy.setMenuId(newMenuId);
|
||||||
|
copy.setTenantId(curTenant);
|
||||||
|
rmsToSave.add(copy);
|
||||||
|
curRmKeys.add(key);
|
||||||
|
}
|
||||||
|
int roleMenuCreated = 0;
|
||||||
|
if (!rmsToSave.isEmpty()) {
|
||||||
|
roleMenuService.saveBatch(rmsToSave);
|
||||||
|
roleMenuCreated = rmsToSave.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 4. 克隆用户角色 sys_user_role(按 role_code 对齐当前租户用户,去重)=====
|
||||||
|
final List<UserRole> curUserRoles = userRoleMapper.selectListAll(curTenant);
|
||||||
|
final Set<String> curUrKeys = curUserRoles.stream()
|
||||||
|
.map(ur -> ur.getUserId() + "_" + ur.getRoleId())
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
// 当前租户 userId -> 其 role_code 集合
|
||||||
|
final Map<Integer, Set<String>> userRoleCodes = new HashMap<>();
|
||||||
|
for (UserRole ur : curUserRoles) {
|
||||||
|
final Role role = newRoleById.get(ur.getRoleId());
|
||||||
|
if (role == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
userRoleCodes.computeIfAbsent(ur.getUserId(), k -> new HashSet<>()).add(role.getRoleCode());
|
||||||
|
}
|
||||||
|
final List<UserRole> ursToSave = new ArrayList<>();
|
||||||
|
for (UserRole ur : userRoleMapper.selectListAll(refTenant)) {
|
||||||
|
final Integer newRoleId = roleIdMapping.get(ur.getRoleId());
|
||||||
|
if (newRoleId == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final Role newRole = newRoleById.get(newRoleId);
|
||||||
|
if (newRole == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final String roleCode = newRole.getRoleCode();
|
||||||
|
// 找当前租户中 role_code 相同的用户,建立绑定
|
||||||
|
for (Map.Entry<Integer, Set<String>> e : userRoleCodes.entrySet()) {
|
||||||
|
if (!e.getValue().contains(roleCode)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final String key = e.getKey() + "_" + newRoleId;
|
||||||
|
if (curUrKeys.contains(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final UserRole copy = new UserRole();
|
||||||
|
copy.setUserId(e.getKey());
|
||||||
|
copy.setRoleId(newRoleId);
|
||||||
|
copy.setTenantId(curTenant);
|
||||||
|
ursToSave.add(copy);
|
||||||
|
curUrKeys.add(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int userRoleCreated = 0;
|
||||||
|
if (!ursToSave.isEmpty()) {
|
||||||
|
userRoleService.saveBatch(ursToSave);
|
||||||
|
userRoleCreated = ursToSave.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
data.put("roleTotal", refRoles.size());
|
||||||
|
data.put("roleCreated", roleCreated);
|
||||||
|
data.put("menuTotal", menuIdMapping.size());
|
||||||
|
data.put("menuCreated", menuCount);
|
||||||
|
data.put("roleMenuCreated", roleMenuCreated);
|
||||||
|
data.put("userRoleCreated", userRoleCreated);
|
||||||
|
return new ApiResult<>(Constants.RESULT_OK_CODE, "授权成功", data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 菜单去重标识, 优先使用权限标识, 目录类菜单权限标识为空时按标题+路由匹配
|
||||||
|
private String getMenuKey(Menu menu) {
|
||||||
|
if (StrUtil.isNotBlank(menu.getAuthority())) {
|
||||||
|
return "authority:" + menu.getAuthority();
|
||||||
|
}
|
||||||
|
return "menu:" + StrUtil.nullToEmpty(menu.getTitle()) + "@" + StrUtil.nullToEmpty(menu.getPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 菜单层级, 用于保证父菜单先于子菜单插入
|
||||||
|
private int getMenuDepth(Menu menu, Map<Integer, Menu> menuMap) {
|
||||||
|
int depth = 0;
|
||||||
|
Menu current = menu;
|
||||||
|
while (current != null && current.getParentId() != null && current.getParentId() != 0 && depth < 20) {
|
||||||
|
current = menuMap.get(current.getParentId());
|
||||||
|
depth++;
|
||||||
|
}
|
||||||
|
return depth;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import com.gxwebsoft.common.core.web.PageResult;
|
|||||||
import com.gxwebsoft.common.system.entity.*;
|
import com.gxwebsoft.common.system.entity.*;
|
||||||
import com.gxwebsoft.common.system.mapper.UserMapper;
|
import com.gxwebsoft.common.system.mapper.UserMapper;
|
||||||
import com.gxwebsoft.common.system.param.LoginParam;
|
import com.gxwebsoft.common.system.param.LoginParam;
|
||||||
|
import com.gxwebsoft.common.system.param.RoleParam;
|
||||||
import com.gxwebsoft.common.system.param.UserParam;
|
import com.gxwebsoft.common.system.param.UserParam;
|
||||||
import com.gxwebsoft.common.mq.producer.SyncMessageProducer;
|
import com.gxwebsoft.common.mq.producer.SyncMessageProducer;
|
||||||
import com.gxwebsoft.common.system.service.*;
|
import com.gxwebsoft.common.system.service.*;
|
||||||
@@ -26,6 +27,7 @@ import org.springframework.transaction.annotation.Isolation;
|
|||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@@ -54,6 +56,8 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
|
|||||||
private OrganizationService organizationService;
|
private OrganizationService organizationService;
|
||||||
@Resource
|
@Resource
|
||||||
private UserRefereeService userRefereeService;
|
private UserRefereeService userRefereeService;
|
||||||
|
@Resource
|
||||||
|
private TenantService tenantService;
|
||||||
|
|
||||||
@Autowired(required = false)
|
@Autowired(required = false)
|
||||||
private SyncMessageProducer syncMessageProducer;
|
private SyncMessageProducer syncMessageProducer;
|
||||||
@@ -85,6 +89,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
|
|||||||
param.setUserId(userId);
|
param.setUserId(userId);
|
||||||
User user = param.getOne(baseMapper.selectListRel(param));
|
User user = param.getOne(baseMapper.selectListRel(param));
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
|
user.setHasPassword(StrUtil.isNotBlank(user.getPassword()));
|
||||||
user.setPassword(null);
|
user.setPassword(null);
|
||||||
user.setRoles(userRoleService.listByUserId(user.getUserId()));
|
user.setRoles(userRoleService.listByUserId(user.getUserId()));
|
||||||
user.setAuthorities(roleMenuService.listMenuByUserId(user.getUserId(), null));
|
user.setAuthorities(roleMenuService.listMenuByUserId(user.getUserId(), null));
|
||||||
@@ -211,6 +216,49 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public boolean markAsDeveloper(Integer userId) {
|
||||||
|
if (userId == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
User user = new User();
|
||||||
|
user.setUserId(userId);
|
||||||
|
user.setIsDeveloper(true);
|
||||||
|
boolean result = updateById(user);
|
||||||
|
if (result) {
|
||||||
|
// 替换用户角色为 developer:先按用户所在租户找到 developer 角色,删除该用户现有角色后
|
||||||
|
// 只绑定 developer,保证用户最终只有「开发者」单一角色(不与原角色并存)
|
||||||
|
try {
|
||||||
|
User dbUser = getById(userId);
|
||||||
|
Integer tenantId = dbUser != null ? dbUser.getTenantId() : null;
|
||||||
|
RoleParam roleParam = new RoleParam();
|
||||||
|
roleParam.setRoleCode("developer");
|
||||||
|
if (tenantId != null) {
|
||||||
|
roleParam.setTenantId(tenantId);
|
||||||
|
}
|
||||||
|
Role developerRole = roleService.getByRoleCode(roleParam);
|
||||||
|
if (developerRole != null && developerRole.getRoleId() != null) {
|
||||||
|
userRoleService.remove(new LambdaUpdateWrapper<UserRole>().eq(UserRole::getUserId, userId));
|
||||||
|
userRoleService.saveBatch(userId, Collections.singletonList(developerRole.getRoleId()));
|
||||||
|
log.info("用户设为开发者并替换角色为 developer: userId={}, roleId={}", userId, developerRole.getRoleId());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 角色替换失败不影响 is_developer 标记位落库
|
||||||
|
log.warn("替换用户角色为 developer 失败(不影响 is_developer 标记): userId={}", userId, e);
|
||||||
|
}
|
||||||
|
// 标记开发者后,通过MQ异步同步用户数据到 websopy
|
||||||
|
if (syncMessageProducer != null) {
|
||||||
|
User updatedUser = getAllByUserId(String.valueOf(userId));
|
||||||
|
if (updatedUser != null) {
|
||||||
|
syncMessageProducer.sendUserSyncMessage("websopy", "UPDATE", updatedUser);
|
||||||
|
log.info("用户设为开发者后同步到websopy: userId={}", userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean comparePassword(String dbPassword, String inputPassword) {
|
public boolean comparePassword(String dbPassword, String inputPassword) {
|
||||||
return bCryptPasswordEncoder.matches(inputPassword, dbPassword);
|
return bCryptPasswordEncoder.matches(inputPassword, dbPassword);
|
||||||
@@ -277,6 +325,8 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
|
|||||||
User addUser = new User();
|
User addUser = new User();
|
||||||
// 注册用户
|
// 注册用户
|
||||||
addUser.setStatus(0);
|
addUser.setStatus(0);
|
||||||
|
// 默认审核通过;前端传 auditStatus=0 时走审核流程
|
||||||
|
addUser.setAuditStatus(userParam.getAuditStatus() != null ? userParam.getAuditStatus() : 1);
|
||||||
if(userParam.getUsername() != null){
|
if(userParam.getUsername() != null){
|
||||||
addUser.setUsername(userParam.getUsername());
|
addUser.setUsername(userParam.getUsername());
|
||||||
}
|
}
|
||||||
@@ -344,6 +394,10 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
|
|||||||
role = roleService.getOne(roleQw, false);
|
role = roleService.getOne(roleQw, false);
|
||||||
// If the default "user" role is missing (fresh DB / incomplete init), create it to avoid empty roles.
|
// If the default "user" role is missing (fresh DB / incomplete init), create it to avoid empty roles.
|
||||||
if (role == null && addUser.getTenantId() != null && "user".equals(roleCode)) {
|
if (role == null && addUser.getTenantId() != null && "user".equals(roleCode)) {
|
||||||
|
// 校验租户是否存在,否则插入 sys_role 会因外键约束失败(tenant_id 引用 sys_tenant)
|
||||||
|
if (tenantService.getById(addUser.getTenantId()) == null) {
|
||||||
|
throw new BusinessException("租户不存在(tenantId=" + addUser.getTenantId() + "),无法创建默认角色");
|
||||||
|
}
|
||||||
Role defaultRole = new Role();
|
Role defaultRole = new Role();
|
||||||
defaultRole.setRoleName("注册用户");
|
defaultRole.setRoleName("注册用户");
|
||||||
defaultRole.setRoleCode("user");
|
defaultRole.setRoleCode("user");
|
||||||
@@ -402,6 +456,27 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
|
|||||||
return baseMapper.selectLastLoginDeveloperByPhone(phone);
|
return baseMapper.selectLastLoginDeveloperByPhone(phone);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public User getByPhoneAndAdmin(String phone) {
|
||||||
|
return baseMapper.selectAdminByPhoneCrossTenant(phone);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<User> getSuperAdminsByPhone(String phone) {
|
||||||
|
if (StrUtil.isBlank(phone)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return baseMapper.selectSuperAdminsByPhone(phone);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public User getSuperAdminByPhoneAndTenantId(String phone, Integer tenantId) {
|
||||||
|
if (StrUtil.isBlank(phone) || tenantId == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return baseMapper.selectSuperAdminByPhoneAndTenantId(phone, tenantId);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<User> pageAll(UserParam param) {
|
public List<User> pageAll(UserParam param) {
|
||||||
return baseMapper.pageRelAll(param);
|
return baseMapper.pageRelAll(param);
|
||||||
@@ -471,6 +546,14 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
|
|||||||
return baseMapper.selectAccountsByPhone(phone);
|
return baseMapper.selectAccountsByPhone(phone);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<User> findAccountsByEmail(String email) {
|
||||||
|
if (StrUtil.isBlank(email)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return baseMapper.selectAccountsByEmail(email);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Integer countAccountsByPhone(String phone) {
|
public Integer countAccountsByPhone(String phone) {
|
||||||
if (StrUtil.isBlank(phone)) {
|
if (StrUtil.isBlank(phone)) {
|
||||||
|
|||||||
@@ -196,4 +196,20 @@ public class EmailTemplateUtil {
|
|||||||
|
|
||||||
sendNotificationEmailWithAction(title, content, email, tenantId, actionUrl, actionText);
|
sendNotificationEmailWithAction(title, content, email, tenantId, actionUrl, actionText);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送邮箱验证码邮件
|
||||||
|
*
|
||||||
|
* @param email 收件人邮箱
|
||||||
|
* @param code 验证码
|
||||||
|
* @param tenantId 租户ID
|
||||||
|
*/
|
||||||
|
public void sendCaptchaEmail(String email, String code, Integer tenantId) {
|
||||||
|
if (email == null || email.trim().isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String title = "邮箱验证码";
|
||||||
|
String content = "您正在绑定或修改邮箱,验证码为:" + code + ",5 分钟内有效,请勿泄露给他人。如非本人操作,请忽略此邮件。";
|
||||||
|
sendNotificationEmail(title, content, email, tenantId, "尊敬的用户", "验证码用于确认邮箱真实性,请勿转发给他人。", null, null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user