refactor(account): 统一账号管理API、完善权限检查和操作审计
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m17s

- 合并 customer_account 和 shop_account 路由到统一的 account 接口
- 新增统一认证接口 (auth handler)
- 实现越权防护中间件和权限检查工具函数
- 新增操作审计日志模型和服务
- 更新数据库迁移 (版本 39: account_operation_log 表)
- 补充集成测试覆盖权限检查和审计日志场景
This commit is contained in:
2026-02-02 17:23:20 +08:00
parent 5851cc6403
commit 80f560df33
58 changed files with 10743 additions and 4915 deletions

View File

@@ -0,0 +1,143 @@
# 账号管理接口规格
## ADDED Requirements
### Requirement: 统一账号管理路由结构
系统 SHALL 提供统一的账号管理路由,按账号类型分组。
#### Scenario: 平台账号管理路由
- **WHEN** 访问 /api/admin/accounts/platform/*
- **THEN** 提供平台账号的 CRUD + 角色管理功能
#### Scenario: 代理账号管理路由
- **WHEN** 访问 /api/admin/accounts/shop/*
- **THEN** 提供代理账号的 CRUD + 角色管理功能
#### Scenario: 企业账号管理路由
- **WHEN** 访问 /api/admin/accounts/enterprise/*
- **THEN** 提供企业账号的 CRUD + 角色管理功能
### Requirement: 所有账号类型支持完整的CRUD操作
系统 SHALL 为所有账号类型提供一致的 CRUD 功能。
#### Scenario: 创建账号
- **WHEN** POST /api/admin/accounts/{type}
- **THEN** 验证权限,创建账号,返回账号信息
#### Scenario: 查询账号列表
- **WHEN** GET /api/admin/accounts/{type}
- **THEN** 应用数据权限过滤,返回分页列表
#### Scenario: 查询账号详情
- **WHEN** GET /api/admin/accounts/{type}/:id
- **THEN** 验证权限,返回账号详情
#### Scenario: 更新账号
- **WHEN** PUT /api/admin/accounts/{type}/:id
- **THEN** 验证权限,更新账号,返回更新后信息
#### Scenario: 删除账号
- **WHEN** DELETE /api/admin/accounts/{type}/:id
- **THEN** 验证权限,软删除账号,返回成功
### Requirement: 所有账号类型支持密码和状态管理
系统 SHALL 为所有账号类型提供统一的密码和状态管理功能。
#### Scenario: 修改账号密码
- **WHEN** PUT /api/admin/accounts/{type}/:id/password
- **THEN** 验证权限更新密码bcrypt哈希返回成功
#### Scenario: 启用账号
- **WHEN** PUT /api/admin/accounts/{type}/:id/statusstatus=1
- **THEN** 验证权限,更新状态为启用,返回成功
#### Scenario: 禁用账号
- **WHEN** PUT /api/admin/accounts/{type}/:id/statusstatus=0
- **THEN** 验证权限,更新状态为禁用,返回成功
### Requirement: 所有账号类型支持角色管理
系统 SHALL 为所有账号类型提供统一的角色管理功能。
#### Scenario: 分配角色
- **WHEN** POST /api/admin/accounts/{type}/:id/rolesbody: {role_ids: [1,2]}
- **THEN** 验证权限,分配角色,返回成功
#### Scenario: 查询账号角色
- **WHEN** GET /api/admin/accounts/{type}/:id/roles
- **THEN** 验证权限,返回账号的所有角色列表
#### Scenario: 移除角色
- **WHEN** DELETE /api/admin/accounts/{type}/:id/roles/:role_id
- **THEN** 验证权限,软删除角色关联,返回成功
#### Scenario: 清空所有角色
- **WHEN** POST /api/admin/accounts/{type}/:id/rolesbody: {role_ids: []}
- **THEN** 验证权限,删除所有角色关联,返回成功
### Requirement: 删除旧路由避免冲突
系统 SHALL 删除旧的账号管理路由,避免与新路由冲突。
#### Scenario: 旧平台账号路由404
- **WHEN** 访问 POST /api/admin/platform-accounts
- **THEN** 返回 404 Not Found
#### Scenario: 旧代理账号路由404
- **WHEN** 访问 GET /api/admin/shop-accounts
- **THEN** 返回 404 Not Found
#### Scenario: 旧企业账号路由404
- **WHEN** 访问 POST /api/admin/customer-accounts
- **THEN** 返回 404 Not Found
### Requirement: 响应格式保持一致
系统 SHALL 为所有账号类型返回一致的响应格式。
#### Scenario: 创建响应包含完整账号信息
- **WHEN** 创建账号成功
- **THEN** 返回账号 ID、用户名、手机号、用户类型、状态、创建时间
#### Scenario: 列表响应包含分页信息
- **WHEN** 查询账号列表
- **THEN** 返回 {items, total, page, size}
#### Scenario: 错误响应使用统一格式
- **WHEN** 操作失败
- **THEN** 返回 {code, message, timestamp}
### Requirement: 支持按条件筛选账号列表
系统 SHALL 支持按多个条件筛选账号列表。
#### Scenario: 按用户名筛选
- **WHEN** GET /api/admin/accounts/{type}?username=张三
- **THEN** 返回用户名包含"张三"的账号列表
#### Scenario: 按手机号筛选
- **WHEN** GET /api/admin/accounts/{type}?phone=138
- **THEN** 返回手机号包含"138"的账号列表
#### Scenario: 按状态筛选
- **WHEN** GET /api/admin/accounts/{type}?status=1
- **THEN** 返回状态为启用的账号列表
#### Scenario: 按店铺ID筛选代理账号
- **WHEN** GET /api/admin/accounts/shop?shop_id=100
- **THEN** 返回 shop_id=100 的代理账号列表(需权限验证)
#### Scenario: 按企业ID筛选企业账号
- **WHEN** GET /api/admin/accounts/enterprise?enterprise_id=50
- **THEN** 返回 enterprise_id=50 的企业账号列表(需权限验证)
### Requirement: 统一Service层实现消除重复
系统 SHALL 使用单一 AccountService 处理所有账号类型,消除代码重复。
#### Scenario: AccountService处理所有账号类型
- **WHEN** 调用 AccountService.Create(ctx, req)
- **THEN** 根据 req.UserType 创建不同类型账号(平台、代理、企业)
#### Scenario: 删除ShopAccountService
- **WHEN** 系统重构完成
- **THEN** ShopAccountService 及相关文件应被删除
#### Scenario: 删除CustomerAccountService
- **WHEN** 系统重构完成
- **THEN** CustomerAccountService 及相关文件应被删除

View File

@@ -0,0 +1,105 @@
# 账号操作审计日志规格
## ADDED Requirements
### Requirement: 记录所有账号管理操作
系统 SHALL 记录所有账号管理操作,包括创建、更新、删除、角色分配和移除。
#### Scenario: 创建账号时记录审计日志
- **WHEN** 用户创建账号成功
- **THEN** 系统应异步写入审计日志包含操作人、目标账号、操作类型create、变更数据after_data
#### Scenario: 更新账号时记录变更前后数据
- **WHEN** 用户更新账号信息(用户名、手机号、状态等)
- **THEN** 系统应记录 before_data 和 after_data包含所有变更字段
#### Scenario: 删除账号时记录审计日志
- **WHEN** 用户软删除账号
- **THEN** 系统应记录删除操作包含被删除账号的完整信息before_data
#### Scenario: 分配角色时记录审计日志
- **WHEN** 用户为账号分配角色
- **THEN** 系统应记录 operation_type=assign_rolesafter_data 包含分配的角色 ID 列表
#### Scenario: 移除角色时记录审计日志
- **WHEN** 用户移除账号的角色
- **THEN** 系统应记录 operation_type=remove_role包含被移除的角色 ID
### Requirement: 审计日志包含完整的操作上下文
系统 SHALL 在审计日志中记录操作人、目标对象、变更内容和请求上下文。
#### Scenario: 记录操作人信息
- **WHEN** 记录审计日志
- **THEN** 日志应包含 operator_id、operator_type、operator_name
#### Scenario: 记录目标账号信息
- **WHEN** 记录审计日志
- **THEN** 日志应包含 target_account_id、target_username、target_user_type
#### Scenario: 记录变更数据JSON格式
- **WHEN** 记录更新操作
- **THEN** before_data 和 after_data 应为 JSONB 格式,包含完整的字段信息
#### Scenario: 记录请求上下文
- **WHEN** 记录审计日志
- **THEN** 日志应包含 request_id、ip_address、user_agent可关联访问日志
### Requirement: 异步写入不阻塞业务流程
系统 SHALL 使用 Goroutine 异步写入审计日志,确保业务操作不受审计日志性能影响。
#### Scenario: 异步写入审计日志
- **WHEN** AccountService.Create 创建账号成功
- **THEN** 主流程立即返回,审计日志在独立 Goroutine 中异步写入
#### Scenario: 写入失败只记录错误日志
- **WHEN** 审计日志写入数据库失败
- **THEN** 记录 Error 级别日志,包含完整审计信息,但不影响业务操作结果
#### Scenario: 业务响应时间不受影响
- **WHEN** 执行账号创建操作
- **THEN** API 响应时间不应因审计日志写入而增加(< 1ms
### Requirement: 操作描述使用中文
系统 SHALL 使用中文描述审计日志的操作类型和内容。
#### Scenario: 创建操作描述
- **WHEN** 记录创建账号操作
- **THEN** operation_desc 应为 "创建账号: {username}"
#### Scenario: 更新操作描述
- **WHEN** 记录更新账号操作
- **THEN** operation_desc 应为 "更新账号: {username}"
#### Scenario: 删除操作描述
- **WHEN** 记录删除账号操作
- **THEN** operation_desc 应为 "删除账号: {username}"
#### Scenario: 分配角色操作描述
- **WHEN** 记录分配角色操作
- **THEN** operation_desc 应为 "为账号 {username} 分配角色"
### Requirement: 支持按多维度查询审计日志
系统 SHALL 提供索引支持按操作人、目标账号、时间快速查询审计日志。
#### Scenario: 按操作人查询日志
- **WHEN** 查询特定操作人的所有操作记录
- **THEN** 使用 idx_account_log_operator 索引,查询时间 < 50ms
#### Scenario: 按目标账号查询日志
- **WHEN** 查询特定账号的所有操作记录
- **THEN** 使用 idx_account_log_target 索引,查询时间 < 50ms
#### Scenario: 按时间范围查询日志
- **WHEN** 查询最近7天的操作记录
- **THEN** 使用 idx_account_log_created 索引,支持倒序分页
### Requirement: 关联访问日志追溯完整请求链路
系统 SHALL 通过 request_id 关联审计日志和访问日志,支持完整链路追溯。
#### Scenario: 通过request_id关联日志
- **WHEN** 审计日志中记录 request_id="req-12345"
- **THEN** 可以在 access.log 中查询到对应的 HTTP 请求日志
#### Scenario: 追溯完整请求链路
- **WHEN** 运维人员调查某个账号创建操作
- **THEN** 通过 request_id 可以查询到:请求参数、权限检查、数据库操作、响应结果

View File

@@ -0,0 +1,127 @@
# 账号管理权限检查规格
## ADDED Requirements
### Requirement: 三层越权防护架构
系统 SHALL 实现三层越权防护机制,确保账号管理操作的安全性。
#### Scenario: 路由层中间件拦截企业账号
- **WHEN** 企业账号user_type=4访问账号管理接口/api/admin/accounts/*
- **THEN** 中间件应返回 403 错误:"无权限访问账号管理功能"
#### Scenario: Service层权限检查成功
- **WHEN** 代理账号创建自己店铺的账号
- **THEN** CanManageShop 检查应通过,账号创建成功
#### Scenario: GORM层自动过滤生效
- **WHEN** 代理账号查询账号列表
- **THEN** GORM Callback 应自动添加 `shop_id IN (当前店铺+下级店铺)` 过滤条件
### Requirement: 代理账号只能管理自己店铺及下级店铺的账号
系统 SHALL 验证代理账号对目标店铺的管理权限,禁止跨店铺越权操作。
#### Scenario: 代理创建自己店铺的账号成功
- **WHEN** 代理账号shop_id=100创建 shop_id=100 的账号
- **THEN** 权限检查通过,账号创建成功
#### Scenario: 代理创建下级店铺的账号成功
- **WHEN** 代理账号shop_id=100下级101,102创建 shop_id=101 的账号
- **THEN** GetSubordinateShopIDs 返回 [100,101,102],权限检查通过
#### Scenario: 代理创建其他店铺的账号失败
- **WHEN** 代理账号shop_id=100创建 shop_id=200 的账号
- **THEN** CanManageShop 返回错误:"无权限管理该店铺的账号",创建失败
#### Scenario: 代理创建平台账号失败
- **WHEN** 代理账号尝试创建 user_type=2 的平台账号
- **THEN** Service 层检查返回错误:"无权限创建平台账号",创建失败
### Requirement: 平台账号和超级管理员可以管理所有账号
系统 SHALL 允许平台账号和超级管理员跳过所有权限检查,管理所有账号。
#### Scenario: 平台账号创建任意类型账号
- **WHEN** 平台账号user_type=2创建代理账号user_type=3, shop_id=100
- **THEN** 权限检查跳过,账号创建成功
#### Scenario: 超级管理员创建任意类型账号
- **WHEN** 超级管理员user_type=1创建任意类型账号
- **THEN** 权限检查跳过,账号创建成功
#### Scenario: 平台账号查询所有账号
- **WHEN** 平台账号调用账号列表接口
- **THEN** GORM Callback 跳过过滤,返回所有账号
### Requirement: 企业账号禁止访问账号管理接口
系统 SHALL 禁止企业账号访问所有账号管理接口。
#### Scenario: 企业账号创建账号失败(路由层拦截)
- **WHEN** 企业账号user_type=4调用 POST /api/admin/accounts/enterprise
- **THEN** 路由层中间件返回 403 错误:"无权限访问账号管理功能"
#### Scenario: 企业账号更新账号失败Service层拦截
- **WHEN** 企业账号绕过路由层,直接调用 AccountService.Update
- **THEN** Service 层返回 403 错误:"企业账号不允许更新账号"
### Requirement: 统一错误返回防止信息泄露
系统 SHALL 在越权访问时统一返回模糊错误消息,防止攻击者判断资源是否存在。
#### Scenario: 查询不存在的账号返回模糊错误
- **WHEN** 用户查询不存在的账号 ID
- **THEN** 返回 403 错误:"无权限操作该资源或资源不存在"
#### Scenario: 查询越权的账号返回相同错误
- **WHEN** 代理账号shop_id=100查询 shop_id=200 的账号
- **THEN** 返回 403 错误:"无权限操作该资源或资源不存在"(与不存在的错误消息相同)
### Requirement: CanManageShop 权限检查函数
系统 SHALL 提供 CanManageShop 函数验证用户对目标店铺的管理权限。
#### Scenario: 验证代理对自己店铺的权限
- **WHEN** 调用 CanManageShop(ctx, 100, shopStore) 且当前用户 shop_id=100
- **THEN** 返回 nil有权限
#### Scenario: 验证代理对下级店铺的权限
- **WHEN** 调用 CanManageShop(ctx, 101, shopStore) 且当前用户 shop_id=100下级包含 101
- **THEN** GetSubordinateShopIDs 返回 [100,101,102],返回 nil有权限
#### Scenario: 验证代理对其他店铺的权限失败
- **WHEN** 调用 CanManageShop(ctx, 200, shopStore) 且当前用户 shop_id=100
- **THEN** 返回错误:"无权限管理该店铺的账号"
#### Scenario: 验证平台账号自动通过
- **WHEN** 调用 CanManageShop(ctx, 200, shopStore) 且当前用户 user_type=2平台
- **THEN** 不调用 GetSubordinateShopIDs直接返回 nil有权限
### Requirement: CanManageEnterprise 权限检查函数
系统 SHALL 提供 CanManageEnterprise 函数验证用户对目标企业的管理权限。
#### Scenario: 验证平台账号管理任意企业
- **WHEN** 调用 CanManageEnterprise(ctx, 50, enterpriseStore, shopStore) 且当前用户 user_type=2
- **THEN** 返回 nil有权限
#### Scenario: 验证代理对归属企业的权限
- **WHEN** 调用 CanManageEnterprise(ctx, 50, enterpriseStore, shopStore) 且企业 owner_shop_id=100当前用户 shop_id=100
- **THEN** 返回 nil有权限
#### Scenario: 验证代理对下级店铺企业的权限
- **WHEN** 调用 CanManageEnterprise(ctx, 50, enterpriseStore, shopStore) 且企业 owner_shop_id=101当前用户 shop_id=100下级包含 101
- **THEN** 返回 nil有权限
#### Scenario: 验证代理对其他店铺企业的权限失败
- **WHEN** 调用 CanManageEnterprise(ctx, 50, enterpriseStore, shopStore) 且企业 owner_shop_id=200当前用户 shop_id=100
- **THEN** 返回错误:"无权限管理该企业的账号"
### Requirement: 权限检查性能优化
系统 SHALL 使用 Redis 缓存优化权限检查性能,确保 API 响应时间 < 200ms。
#### Scenario: GetSubordinateShopIDs 命中缓存
- **WHEN** 调用 GetSubordinateShopIDs(ctx, 100) 且缓存存在
- **THEN** 从 Redis 读取缓存,不查询数据库,耗时 < 5ms
#### Scenario: GetSubordinateShopIDs 缓存未命中
- **WHEN** 调用 GetSubordinateShopIDs(ctx, 100) 且缓存不存在
- **THEN** 递归查询数据库,写入 Redis 缓存30分钟返回结果
#### Scenario: 权限检查总耗时 < 10ms
- **WHEN** 执行完整权限检查(包含 GetSubordinateShopIDs
- **THEN** 总耗时 < 10ms缓存命中时 < 5ms

View File

@@ -0,0 +1,86 @@
# 统一认证接口规格
## ADDED Requirements
### Requirement: 合并后台和H5认证接口
系统 SHALL 提供统一认证接口 /api/auth/*,支持后台和 H5 两种场景的认证。
#### Scenario: 后台用户登录
- **WHEN** 用户调用 POST /api/auth/loginuser_type IN (1,2,3,4)
- **THEN** 验证用户名+密码,返回 Access Token + Refresh Token
#### Scenario: H5用户登录
- **WHEN** H5 用户调用 POST /api/auth/loginuser_type IN (3,4)
- **THEN** 验证用户名+密码,返回 Access Token + Refresh Token
#### Scenario: 登出统一接口
- **WHEN** 用户调用 POST /api/auth/logout
- **THEN** 删除 Redis 中的 Token返回成功
#### Scenario: 刷新Token统一接口
- **WHEN** 用户调用 POST /api/auth/refresh-token
- **THEN** 验证 Refresh Token返回新的 Access Token
#### Scenario: 获取用户信息统一接口
- **WHEN** 用户调用 GET /api/auth/me
- **THEN** 返回当前用户信息,包含 menus 和 buttons
### Requirement: 保留个人客户认证接口
系统 SHALL 保持个人客户认证接口 /api/c/v1/* 独立,不与后台/H5认证合并。
#### Scenario: 个人客户微信授权登录
- **WHEN** 个人客户调用 POST /api/c/v1/wechat/auth
- **THEN** 使用微信 OAuth 流程,返回 JWT Token
#### Scenario: 个人客户手机号登录
- **WHEN** 个人客户调用 POST /api/c/v1/login
- **THEN** 验证手机号+验证码,返回 JWT Token
#### Scenario: 个人客户获取资料
- **WHEN** 个人客户调用 GET /api/c/v1/profile
- **THEN** 返回个人客户资料(独立数据结构)
### Requirement: 删除旧认证接口路由
系统 SHALL 删除 /api/admin/login、/api/h5/login 等旧路由,统一为 /api/auth/*。
#### Scenario: 旧后台登录接口404
- **WHEN** 用户调用 POST /api/admin/login
- **THEN** 返回 404 Not Found
#### Scenario: 旧H5登录接口404
- **WHEN** 用户调用 POST /api/h5/login
- **THEN** 返回 404 Not Found
#### Scenario: 新统一接口正常工作
- **WHEN** 用户调用 POST /api/auth/login
- **THEN** 正常认证,返回 200 OK
### Requirement: 认证逻辑保持不变
系统 SHALL 保持认证逻辑不变,只修改路由路径。
#### Scenario: Token生成逻辑不变
- **WHEN** 用户登录成功
- **THEN** 生成相同格式的 Access Token24小时和 Refresh Token7天
#### Scenario: Token存储在Redis
- **WHEN** 生成 Token
- **THEN** 存储在 RedisKey 格式为 "auth:token:{token}"
#### Scenario: 用户类型过滤不变
- **WHEN** 登录请求中包含 user_type
- **THEN** 验证用户类型是否与账号类型匹配
### Requirement: 响应格式保持兼容
系统 SHALL 保持登录响应格式兼容,包含 menus 和 buttons。
#### Scenario: 登录响应包含菜单
- **WHEN** 用户登录成功
- **THEN** 响应应包含 menus菜单树结构
#### Scenario: 登录响应包含按钮权限
- **WHEN** 用户登录成功
- **THEN** 响应应包含 buttons按钮权限列表
#### Scenario: 响应格式不变
- **WHEN** 用户登录成功
- **THEN** 响应格式应与旧接口完全一致,前端无需修改解析逻辑