# 用户手机号唯一性完善说明
## 📋 问题背景
在支持手机号登录后,手机号成为了用户登录的重要凭证。但经过检查发现,虽然**用户添加/修改**功能已有手机号唯一性校验,但在**用户导入**和**用户同步**功能中缺少该校验,可能导致手机号重复,造成登录混乱。
## ⚠️ 存在的风险
### 1. 登录冲突
- 多个用户使用相同手机号,导致手机号登录时无法确定登录的是哪个用户
- 可能造成用户登录到错误的账户
### 2. 数据一致性问题
- 手机号作为唯一标识,出现重复违背了数据设计原则
- 影响基于手机号的业务逻辑
### 3. 安全隐患
- 手机号重复可能被利用进行身份冒用
- 短信验证码等功能可能发送到错误的用户
## 🔧 完善方案
### 1. 用户导入功能增强
**文件**: `ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java`
**方法**: `importUser`
#### 新增功能
- ✅ 导入新用户前,校验手机号是否已被其他用户使用
- ✅ 更新用户时,校验手机号是否被其他用户(排除自己)占用
- ✅ 同时校验邮箱唯一性
- ✅ 详细的错误提示,包含冲突的用户名
#### 实现代码
```java
// 新增用户时
if (StringUtils.isNotEmpty(user.getPhonenumber()))
{
SysUser phoneCheck = userMapper.checkPhoneUnique(user.getPhonenumber());
if (StringUtils.isNotNull(phoneCheck))
{
failureNum++;
failureMsg.append("
" + failureNum + "、账号 " + user.getUserName()
+ " 导入失败:手机号码 " + user.getPhonenumber()
+ " 已被用户 " + phoneCheck.getUserName() + " 使用");
continue;
}
}
// 更新用户时
if (StringUtils.isNotEmpty(user.getPhonenumber()))
{
SysUser phoneCheck = userMapper.checkPhoneUnique(user.getPhonenumber());
if (StringUtils.isNotNull(phoneCheck) && !phoneCheck.getUserId().equals(u.getUserId()))
{
failureNum++;
failureMsg.append("
" + failureNum + "、账号 " + user.getUserName()
+ " 更新失败:手机号码 " + user.getPhonenumber()
+ " 已被用户 " + phoneCheck.getUserName() + " 使用");
continue;
}
}
```
### 2. 用户同步功能增强
**文件**: `ruoyi-system/src/main/java/com/ruoyi/system/service/impl/UserSyncServiceImpl.java`
#### 2.1 创建新用户 (`createNewUser`)
**增强点**:
- ✅ 创建前校验手机号唯一性
- ✅ 如手机号重复,记录警告日志并跳过创建
- ✅ 避免因手机号冲突导致同步失败
```java
if (StringUtils.isNotEmpty(dto.getPhonenumber()))
{
// 校验手机号是否已被其他用户使用
SysUser phoneCheck = sysUserMapper.checkPhoneUnique(dto.getPhonenumber());
if (StringUtils.isNotNull(phoneCheck))
{
log.warn("创建用户失败,手机号 {} 已被用户 {} 使用,跳过用户 {}",
dto.getPhonenumber(), phoneCheck.getUserName(), dto.getUserName());
return; // 跳过创建
}
newUser.setPhonenumber(dto.getPhonenumber());
}
```
#### 2.2 更新现有用户 (`updateExistingUser`)
**增强点**:
- ✅ 更新前校验手机号唯一性(排除自己)
- ✅ 如手机号被占用,记录警告日志并跳过手机号更新
- ✅ 继续更新其他字段,不影响整体同步流程
```java
if (StringUtils.isNotEmpty(dto.getPhonenumber()))
{
// 校验手机号是否已被其他用户使用(排除自己)
SysUser phoneCheck = sysUserMapper.checkPhoneUnique(dto.getPhonenumber());
if (StringUtils.isNotNull(phoneCheck) && !phoneCheck.getUserId().equals(existingUser.getUserId()))
{
log.warn("更新用户 {} 失败,手机号 {} 已被用户 {} 使用,跳过手机号更新",
existingUser.getUserName(), dto.getPhonenumber(), phoneCheck.getUserName());
}
else
{
existingUser.setPhonenumber(dto.getPhonenumber());
}
}
```
## 📊 完善覆盖范围
| 功能模块 | 原状态 | 完善后状态 | 说明 |
|---------|--------|-----------|------|
| 用户添加 | ✅ 已有校验 | ✅ 保持 | Controller层已实现 |
| 用户修改 | ✅ 已有校验 | ✅ 保持 | Controller层已实现 |
| 用户导入 | ❌ 缺少校验 | ✅ 已完善 | 新增+更新均校验 |
| 用户同步(新增) | ❌ 缺少校验 | ✅ 已完善 | 创建前校验 |
| 用户同步(更新) | ❌ 缺少校验 | ✅ 已完善 | 更新前校验 |
## 🔍 校验策略
### 新增用户
```
检查手机号 → 已存在? → 是 → 拒绝创建,记录错误
→ 否 → 允许创建
```
### 更新用户
```
检查手机号 → 已存在? → 是 → 是自己? → 是 → 允许更新
→ 否 → 拒绝更新,记录错误
→ 否 → 允许更新
```
## 📝 修改文件清单
| # | 文件 | 修改内容 | 行数变化 |
|---|------|---------|---------|
| 1 | `SysUserServiceImpl.java` | 用户导入增加手机号/邮箱唯一性校验 | +50 |
| 2 | `UserSyncServiceImpl.java` | 用户同步增加手机号唯一性校验 | +19 |
**总计**: +69 行
## ⚡ 处理策略差异
### 用户导入
- **策略**: 严格拒绝
- **原因**: 导入是批量操作,需要保证数据质量
- **行为**: 记录详细错误信息,终止该条记录导入
### 用户同步
- **策略**: 容错处理
- **原因**: 同步是自动化操作,不应因个别字段冲突影响整体
- **行为**: 记录警告日志,跳过冲突字段,继续其他字段同步
## 🎯 错误提示示例
### 用户导入
```
很抱歉,导入失败!共 2 条数据格式不正确,错误如下:
1、账号 zhangsan 导入失败:手机号码 13800138000 已被用户 lisi 使用
2、账号 wangwu 导入失败:邮箱 test@example.com 已被用户 zhaoliu 使用
```
### 用户同步
```log
2025-10-23 10:30:45 WARN 创建用户失败,手机号 13800138000 已被用户 lisi 使用,跳过用户 zhangsan
2025-10-23 10:30:46 WARN 更新用户 wangwu 失败,手机号 13900139000 已被用户 zhaoliu 使用,跳过手机号更新
```
## ✅ 测试场景
### 1. 用户导入测试
#### 场景1.1: 导入新用户,手机号重复
```
输入: 用户名=test001, 手机号=13800138000(已被admin使用)
预期: 导入失败,提示"手机号码 13800138000 已被用户 admin 使用"
```
#### 场景1.2: 导入新用户,手机号唯一
```
输入: 用户名=test001, 手机号=13900139999(未使用)
预期: 导入成功
```
#### 场景1.3: 更新用户,手机号改为其他人的
```
输入: 用户名=test001(已存在), 手机号=13800138000(被admin使用)
预期: 更新失败,提示"手机号码 13800138000 已被用户 admin 使用"
```
#### 场景1.4: 更新用户,手机号不变
```
输入: 用户名=test001(已存在,手机号=13900139999), 手机号=13900139999
预期: 更新成功
```
### 2. 用户同步测试
#### 场景2.1: 同步新用户,手机号重复
```
输入: OA用户(手机号=13800138000,已被系统用户使用)
预期: 跳过创建,记录警告日志
```
#### 场景2.2: 同步更新用户,手机号改为其他人的
```
输入: 更新用户手机号为已被占用的号码
预期: 跳过手机号更新,其他字段正常更新,记录警告日志
```
## 🔒 数据库约束建议
虽然代码层面已完善校验,但建议在数据库层面也添加约束:
```sql
-- 为手机号字段添加唯一索引
CREATE UNIQUE INDEX idx_sys_user_phonenumber
ON sys_user(phonenumber)
WHERE phonenumber IS NOT NULL AND phonenumber != '' AND del_flag = '0';
```
**注意**:
- 仅对非空、非空字符串、未删除的记录建立唯一约束
- 允许多条记录的手机号为NULL或空字符串
## 📚 相关文档
- [用户登录支持手机号功能说明](./用户登录支持手机号功能说明.md)
- [用户同步功能说明](./用户同步功能说明.md)
## 🎉 总结
通过本次完善:
1. ✅ 全面覆盖了所有用户创建/更新入口的手机号唯一性校验
2. ✅ 避免了手机号重复导致的登录混乱
3. ✅ 提升了数据一致性和系统安全性
4. ✅ 提供了清晰的错误提示和日志记录
5. ✅ 采用了合理的容错策略,保证自动化同步的稳定性
现在系统可以安全地使用手机号作为登录凭证!🎊