签入版本
This commit is contained in:
@@ -0,0 +1,312 @@
|
||||
:::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 账号禁止修改/删除/禁用/重置密码
|
||||
```
|
||||
Reference in New Issue
Block a user