Files
2026-09-12 23:12:15 +08:00

313 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
:::tip 用户管理
用户管理是系统的核心模块,属于复杂业务模块。包含用户 CRUD、角色关联、密码加密、行政区划、Excel 导入导出等功能。与简单 CRUD 模块不同,用户管理使用自定义 service 函数而非继承 BaseService 的类方法。
:::
## 模块结构
```
src/modules/system/user/
├── models.py # 用户模型
├── schemas.py # 表单验证
├── repository.py # 数据访问层
└── service.py # 业务逻辑层(自定义函数)
src/modules/system/user_role/
├── models.py # 用户-角色关联模型
├── repository.py # 关联数据访问
└── service.py # 关联业务逻辑
```
## 用户模型
```python
# src/modules/system/user/models.py
# ============================================================
# 用户模型
# ============================================================
class User(base_model, base_db):
"""用户模型类"""
# ============================================================
# 表名配置
# ============================================================
__tablename__ = DB_PREFIX + "user"
__table_comment__ = "用户表"
# ============================================================
# 字段定义
# ============================================================
# 用户编码
code = Column(String(100), nullable=True, default='', server_default=text("''"), index=True, comment="用户编码")
# 用户姓名
realname = Column(String(150), nullable=False, index=True, comment="用户姓名")
# 性别:1-男 2-女 3-保密
gender = Column(Integer, default=1, server_default=text('1'), comment="性别:1-男 2-女 3-保密")
# 用户头像
avatar = Column(String(255), nullable=False, comment="用户头像")
# 手机号
mobile = Column(String(30), nullable=False, index=True, comment="手机号")
# 邮箱
email = Column(String(30), nullable=False, index=True, comment="邮箱")
# 部门ID
dept_id = Column(Integer, default=0, server_default=text('0'), index=True, comment="部门ID")
# 职级ID
level_id = Column(Integer, default=0, server_default=text('0'), comment="职级ID")
# 岗位ID
position_id = Column(Integer, default=0, server_default=text('0'), comment="岗位ID")
# 省份编码
province_code = Column(String(30), nullable=False, comment="省份编码")
# 城市编码
city_code = Column(String(30), nullable=False, comment="城市编码")
# 县区编码
district_code = Column(String(30), nullable=False, comment="县区编码")
# 街道编码
street_code = Column(String(30), nullable=False, comment="街道编码")
# 省市区信息
city_info = Column(String(255), nullable=True, comment="省市区信息")
# 详细地址
address = Column(String(255), nullable=False, comment="详细地址")
# 用户名
username = Column(String(30), nullable=True, index=True, comment="用户名")
# 密码
password = Column(String(255), nullable=True, comment="密码")
# 加密盐
salt = Column(String(30), nullable=True, comment="加密盐")
# 个人简介
intro = Column(String(255), nullable=True, comment="个人简介")
# 状态:1-正常 2-禁用
status = Column(Integer, default=1, server_default=text('1'), index=True, comment="状态:1-正常 2-禁用")
# 排序
sort = Column(Integer, default=0, server_default=text('0'), comment="排序")
# 个人备注
note = Column(String(255), nullable=True, comment="个人备注")
# ============================================================
# 内置方法
# ============================================================
def __str__(self):
"""返回用户ID作为字符串表示"""
return "用户{}".format(self.id)
```
## 密码加密
使用 bcrypt 双重加盐方案,由 `src/core/password.py` 提供:
```python
# src/core/password.py
# +======================================================================
# | 模块: 密码加密工具
# | 说明: 基于 bcrypt 的双重加盐方案
# +======================================================================
"""密码加密工具:基于 bcrypt 的双重加盐方案。"""
import secrets
import bcrypt
# bcrypt 工作因子,值越大计算越慢越安全,默认12(约250ms)
BCRYPT_ROUNDS = 12
# ============================================================
# 密码工具函数
# ============================================================
def generate_salt(length: int = 10) -> str:
"""生成随机盐值"""
return secrets.token_urlsafe(length)[:length]
def encrypt_password(password: str, salt: str) -> str:
"""使用 bcrypt 加密密码(外部 salt 双重加盐)"""
combined = (password + salt).encode('utf-8')
hashed = bcrypt.hashpw(combined, bcrypt.gensalt(rounds=BCRYPT_ROUNDS))
return hashed.decode('utf-8')
def verify_password(password: str, salt: str, hashed: str) -> bool:
"""验证密码是否正确"""
combined = (password + salt).encode('utf-8')
return bcrypt.checkpw(combined, hashed.encode('utf-8'))
```
:::tip 双重加盐
密码加密采用双重加盐:外部 salt(存储在 user 表)+ bcrypt 内部 salt。即使数据库泄露,攻击者也无法通过彩虹表破解密码。工作因子 12,单次加密约 250ms。
:::
## 角色关联
用户与角色通过 `fastapi_user_role` 关联表实现多对多关系:
```python
# src/modules/system/user_role/models.py
# ============================================================
# 用户角色关联模型
# ============================================================
class UserRole(base_model, base_db):
"""用户角色关联模型类"""
# ============================================================
# 表名配置
# ============================================================
__tablename__ = DB_PREFIX + "user_role"
__table_comment__ = "用户角色关联表"
# ============================================================
# 字段定义
# ============================================================
# 用户ID
user_id = Column(Integer, default=0, server_default=text('0'), index=True, comment="用户ID")
# 角色ID
role_id = Column(Integer, default=0, server_default=text('0'), index=True, comment="角色ID")
# ============================================================
# 内置方法
# ============================================================
def __str__(self):
"""返回用户ID作为字符串表示"""
return "用户角色表{}".format(self.user_id)
```
新增用户时同时创建角色关联,删除用户时硬删关联数据:
```python
# src/modules/system/user/service.py 关键片段
async def add_user(request, data: UserForm):
"""新增用户"""
def _add():
# 唯一性校验
if user_repo.get_one(username=data.username):
return R.failed("登录账号不能重复"), None
formData = data.model_dump(exclude={'id', 'city', 'roles'})
# 密码加密:未传密码时使用默认密码
pwd = data.password or DEFAULT_PASSWORD
salt = password.generate_salt()
formData['password'] = password.encrypt_password(pwd, salt)
formData['salt'] = salt
# 行政区划处理
citys = data.city or []
formData['provinceCode'] = str(citys[0]) if len(citys) > 0 and citys[0] else ''
# ... 其他字段处理
user = user_repo.create(**formData, create_user=get_realname(request))
user.code = f"{user.id:06d}" # 根据自增ID生成6位用户编码
return None, user.id
err, user_id = await asyncio.to_thread(_add)
if err:
return err
# 创建用户角色数据 + 清理权限缓存
if data.roles is not None:
await add_user_role(user_id, data.roles)
return R.ok(msg="添加成功")
```
## 关联数据富化
用户列表和详情需要富化部门、职级、岗位、角色等关联数据:
```python
# src/modules/system/user/service.py
def _enrich_user_records(records):
"""为用户记录补充关联数据"""
dept_map = {d.id: d.name for d in dept_repo.get_all()}
level_map = {l.id: l.name for l in level_repo.get_all()}
position_map = {p.id: p.name for p in position_repo.get_all()}
gender_map = get_dict_map('gender')
user_ids = [data.get("id") for data in records]
role_map = get_user_role_map(user_ids)
for data in records:
data["genderName"] = gender_map.get(str(data.get("gender")))
data["deptName"] = dept_map.get(data.get("deptId"))
data["levelName"] = level_map.get(data.get("levelId"))
data["positionName"] = position_map.get(data.get("positionId"))
data["roles"] = role_map.get(data.get("id"), [])
return records
```
## 权限缓存清理
用户增删改、角色变更、状态变更时需清理 Redis 中的权限和有效状态缓存:
```python
from utils.perm_cache import invalidate_user_perms, invalidate_user_active
# 角色变更后清理缓存
await invalidate_user_perms(user_id)
await invalidate_user_active(user_id)
# 状态变更后清理缓存(禁用即时踢出、启用即时放行)
await invalidate_user_perms(id)
await invalidate_user_active(id)
```
## Excel 导入导出
用户模块支持完整的 Excel 导入导出,导入列定义和导出列定义通过数组声明:
```python
# 导入列定义(顺序即 Excel 列序)
USER_IMPORT_COLUMNS = [
{"title": "登录账号", "key": "username", "required": True},
{"title": "用户姓名", "key": "realname", "required": True},
{"title": "性别", "key": "gender", "parser": _parse_gender},
{"title": "手机号", "key": "mobile", "required": True},
{"title": "邮箱", "key": "email", "required": True},
{"title": "部门", "key": "dept_id", "parser": _parse_dept},
{"title": "职级", "key": "level_id", "parser": _parse_level},
{"title": "岗位", "key": "position_id", "parser": _parse_position},
{"title": "用户角色", "key": "roles", "parser": _parse_roles},
{"title": "状态", "key": "status", "parser": _parse_status},
{"title": "城市信息", "key": "city_codes", "parser": _parse_city_info},
{"title": "详细地址", "key": "address", "required": True},
# ...
]
```
:::tip 导入解析器
每个导入列可声明 `parser` 函数,负责将 Excel 单元格值转换为目标类型。例如 `_parse_dept` 按部门名称查询部门 ID,`_parse_roles` 按角色名称(逗号分隔)查询角色 ID 列表。解析失败时返回行级错误信息,不影响其他行。
:::
## API 接口
| 接口 | 方法 | 权限节点 | 说明 |
|------|------|---------|------|
| `/api/v1/user/page` | GET | `sys:user:page` | 用户分页列表 |
| `/api/v1/user/detail/{id}` | GET | `sys:user:detail` | 用户详情 |
| `/api/v1/user/add` | POST | `sys:user:add` | 新增用户 |
| `/api/v1/user/update` | PUT | `sys:user:update` | 编辑用户 |
| `/api/v1/user/delete/{id}` | DELETE | `sys:user:delete` | 删除用户 |
| `/api/v1/user/batchDelete` | DELETE | `sys:user:batchDelete` | 批量删除 |
| `/api/v1/user/status` | PUT | `sys:user:status` | 设置状态 |
| `/api/v1/user/resetPwd` | PUT | `sys:user:resetPwd` | 重置密码 |
| `/api/v1/user/updatePassword` | PUT | - | 修改密码(本人) |
| `/api/v1/user/updateInfo` | PUT | - | 更新个人信息 |
| `/api/v1/user/import` | POST | `sys:user:import` | 导入用户 |
| `/api/v1/user/export` | GET | `sys:user:export` | 导出用户 |
:::warning 内置超管保护
用户 ID 为 1 的内置超管账号(`SUPERADMIN_ID`)禁止通过管理接口修改、删除、禁用、重置密码。这些操作会在 service 层提前拦截并返回失败提示。
:::
## 总结
用户管理模块具备以下特点:
```
1. 复杂业务:自定义 service 函数,异步操作用 asyncio.to_thread 包裹
2. 密码安全:bcrypt 双重加盐,工作因子12(约250ms)
3. 角色关联:多对多关系,通过 user_role 关联表,删除时硬删关联
4. 权限缓存:角色/状态变更后即时清理 Redis 缓存
5. 关联富化:列表/详情补充部门、职级、岗位、角色、性别等展示名称
6. 行政区划:省/市/区/街道四级编码,自动拼接城市信息
7. Excel 导入导出:列定义式导入解析,逐行校验,部分成功返回明细
8. 超管保护:ID=1 账号禁止修改/删除/禁用/重置密码
```