refactor(account): 统一账号管理API、完善权限检查和操作审计
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m17s
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:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-02-02
|
||||
@@ -0,0 +1,494 @@
|
||||
# 统一账号管理接口设计
|
||||
|
||||
## Context
|
||||
|
||||
### 现状问题
|
||||
当前系统存在三套独立的账号管理体系:
|
||||
1. **AccountService** + **AccountHandler**:管理"通用账号"和"平台账号",功能重复
|
||||
2. **ShopAccountService** + **ShopAccountHandler**:管理代理账号,功能不全(缺少角色管理)
|
||||
3. **CustomerAccountService** + **CustomerAccountHandler**:管理企业账号,命名错误(customer vs enterprise)
|
||||
|
||||
### 安全现状
|
||||
**Critical 漏洞**:所有 Service 的 Create 方法缺少目标资源归属权限检查。攻击场景:
|
||||
```go
|
||||
// 代理用户 A(shop_id=100)发起请求
|
||||
POST /api/admin/shop-accounts
|
||||
{ "shop_id": 200, "username": "hacker", ... }
|
||||
|
||||
// 当前实现:只检查店铺存在,直接创建成功 ❌
|
||||
```
|
||||
|
||||
### 已有防护机制
|
||||
- **GORM Callback 自动过滤**(`pkg/gorm/callback.go`):所有查询自动应用数据权限过滤
|
||||
- 代理用户:`WHERE shop_id IN (自己店铺+下级店铺)`
|
||||
- 企业用户:`WHERE enterprise_id = 当前企业ID`
|
||||
- 平台/超管:跳过过滤
|
||||
- **递归查询下级店铺**(`ShopStore.GetSubordinateShopIDs`):支持7级层级,Redis 缓存30分钟
|
||||
|
||||
### 约束条件
|
||||
- 必须遵循 Handler → Service → Store → Model 分层
|
||||
- 禁止外键约束,表关联通过 ID 字段手动维护
|
||||
- 所有业务逻辑在 Service 层,Handler 只做参数验证和路由
|
||||
- 错误处理使用 `pkg/errors` 统一错误码
|
||||
- 审计日志异步写入,不阻塞主流程
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
### Goals
|
||||
1. **统一架构**:合并三套账号管理为一个 AccountService,消除代码重复
|
||||
2. **安全加固**:修复 Create 越权漏洞,添加三层防护机制
|
||||
3. **操作审计**:记录所有账号管理操作,满足合规要求
|
||||
4. **简化路由**:统一路由结构 `/api/admin/accounts/{type}/*`,语义清晰
|
||||
5. **认证统一**:合并后台和 H5 认证为 `/api/auth/*`
|
||||
|
||||
### Non-Goals
|
||||
- ❌ 修改 GORM Callback 自动过滤逻辑(已经完善,保持不变)
|
||||
- ❌ 重构角色和权限管理接口(不在本次范围)
|
||||
- ❌ 修改个人客户认证接口(业务逻辑独立,保持不变)
|
||||
- ❌ 添加实时审计日志查询接口(本次只做记录,查询接口后续迭代)
|
||||
|
||||
## Decisions
|
||||
|
||||
### 决策 1:路由结构设计
|
||||
|
||||
**选择**:按账号类型分组的 RESTful 风格
|
||||
```
|
||||
/api/admin/accounts/platform/* (平台账号)
|
||||
/api/admin/accounts/shop/* (代理账号)
|
||||
/api/admin/accounts/enterprise/* (企业账号)
|
||||
```
|
||||
|
||||
**备选方案**:
|
||||
- 方案 A:单一路由 + query 参数(如 `/api/admin/accounts?type=platform`)
|
||||
- ❌ 拒绝原因:语义不清,不符合 RESTful 规范,前端调用复杂
|
||||
- 方案 B:保留三个独立路由(如 `/platform-accounts`、`/shop-accounts`)
|
||||
- ❌ 拒绝原因:与统一架构目标冲突,未解决重复问题
|
||||
|
||||
**理由**:
|
||||
- ✅ 语义清晰,账号类型一目了然
|
||||
- ✅ 符合 RESTful 规范,易于理解和文档化
|
||||
- ✅ 便于路由层添加类型专用中间件(如企业账号拦截)
|
||||
- ✅ 前端调用直观,便于维护
|
||||
|
||||
### 决策 2:三层越权防护架构
|
||||
|
||||
**第一层:路由层中间件(粗粒度拦截)**
|
||||
```go
|
||||
// internal/routes/account.go
|
||||
func registerEnterpriseAccountRoutes(router fiber.Router, ...) {
|
||||
accounts := router.Group("/accounts/enterprise")
|
||||
|
||||
// 企业账号禁止访问账号管理接口
|
||||
accounts.Use(func(c *fiber.Ctx) error {
|
||||
userType := middleware.GetUserTypeFromContext(c.UserContext())
|
||||
if userType == constants.UserTypeEnterprise {
|
||||
return errors.New(errors.CodeForbidden, "无权限访问账号管理功能")
|
||||
}
|
||||
return c.Next()
|
||||
})
|
||||
|
||||
// 注册路由...
|
||||
}
|
||||
```
|
||||
|
||||
**第二层:Service 层业务检查(细粒度验证)**
|
||||
```go
|
||||
// internal/service/account/service.go
|
||||
func (s *Service) Create(ctx context.Context, req *dto.CreateAccountRequest) error {
|
||||
// 1. 基础认证检查
|
||||
currentUserID := middleware.GetUserIDFromContext(ctx)
|
||||
if currentUserID == 0 {
|
||||
return errors.New(errors.CodeUnauthorized, "未授权访问")
|
||||
}
|
||||
|
||||
userType := middleware.GetUserTypeFromContext(ctx)
|
||||
|
||||
// 2. 类型级权限检查
|
||||
// 企业账号禁止创建账号
|
||||
if userType == constants.UserTypeEnterprise {
|
||||
return errors.New(errors.CodeForbidden, "企业账号不允许创建账号")
|
||||
}
|
||||
|
||||
// 代理账号不能创建平台账号
|
||||
if userType == constants.UserTypeAgent && req.UserType == constants.UserTypePlatform {
|
||||
return errors.New(errors.CodeForbidden, "无权限创建平台账号")
|
||||
}
|
||||
|
||||
// 3. 资源级权限检查(核心:修复越权漏洞)
|
||||
if req.UserType == constants.UserTypeAgent && req.ShopID != nil {
|
||||
if err := middleware.CanManageShop(ctx, *req.ShopID, s.shopStore); err != nil {
|
||||
return err // 返回"无权限管理该店铺的账号"
|
||||
}
|
||||
}
|
||||
|
||||
if req.UserType == constants.UserTypeEnterprise && req.EnterpriseID != nil {
|
||||
if err := middleware.CanManageEnterprise(ctx, *req.EnterpriseID, s.enterpriseStore); err != nil {
|
||||
return err // 返回"无权限管理该企业的账号"
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 创建账号...
|
||||
}
|
||||
```
|
||||
|
||||
**第三层:GORM Callback 自动过滤(兜底)**
|
||||
- 已有实现,保持不变
|
||||
- 所有 List/Get 操作自动过滤
|
||||
- 防止直接 SQL 注入绕过应用层检查
|
||||
|
||||
**理由**:
|
||||
- ✅ 多层防御,单层失效不会导致全局崩溃
|
||||
- ✅ 第一层快速拦截明显越权,节省资源
|
||||
- ✅ 第二层精确验证业务逻辑,覆盖所有场景
|
||||
- ✅ 第三层兜底,防止绕过应用层检查
|
||||
|
||||
### 决策 3:权限检查辅助函数设计
|
||||
|
||||
**位置**:`pkg/middleware/permission_helper.go`(而非 Service 内部)
|
||||
|
||||
**接口设计**:
|
||||
```go
|
||||
// CanManageShop 检查当前用户是否有权管理目标店铺的账号
|
||||
// 返回 nil 表示有权限,返回 error 表示无权限
|
||||
func CanManageShop(ctx context.Context, targetShopID uint, shopStore ShopStoreInterface) error
|
||||
|
||||
// CanManageEnterprise 检查当前用户是否有权管理目标企业的账号
|
||||
func CanManageEnterprise(ctx context.Context, targetEnterpriseID uint,
|
||||
enterpriseStore EnterpriseStoreInterface, shopStore ShopStoreInterface) error
|
||||
```
|
||||
|
||||
**备选方案**:
|
||||
- 方案 A:在 AccountService 内部实现为私有方法
|
||||
- ❌ 拒绝原因:无法复用,其他 Service 需要相同权限检查时需重复实现
|
||||
- 方案 B:在 `pkg/utils` 中实现
|
||||
- ❌ 拒绝原因:utils 包应该是纯函数,不应依赖 Store 接口
|
||||
|
||||
**理由**:
|
||||
- ✅ `pkg/middleware` 是权限相关逻辑的自然归属
|
||||
- ✅ 可以被多个 Service 复用(AccountService、RoleService 等)
|
||||
- ✅ 通过接口依赖 Store,遵循依赖倒置原则,便于测试
|
||||
|
||||
### 决策 4:操作审计日志设计
|
||||
|
||||
**表结构**:
|
||||
```sql
|
||||
CREATE TABLE tb_account_operation_log (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
-- 操作主体
|
||||
operator_id BIGINT NOT NULL, -- 操作人 ID
|
||||
operator_type INT NOT NULL, -- 操作人类型 (1=超管 2=平台 3=代理 4=企业)
|
||||
operator_name VARCHAR(255) NOT NULL, -- 操作人用户名
|
||||
|
||||
-- 操作对象
|
||||
target_account_id BIGINT, -- 目标账号 ID(可选,删除操作后可能查不到)
|
||||
target_username VARCHAR(255), -- 目标账号用户名
|
||||
target_user_type INT, -- 目标账号类型
|
||||
|
||||
-- 操作内容
|
||||
operation_type VARCHAR(50) NOT NULL, -- create/update/delete/assign_roles/remove_role
|
||||
operation_desc TEXT NOT NULL, -- 操作描述(中文)
|
||||
|
||||
-- 变更详情(JSON 格式)
|
||||
before_data JSONB, -- 变更前数据(update 操作)
|
||||
after_data JSONB, -- 变更后数据(create/update 操作)
|
||||
|
||||
-- 请求上下文
|
||||
request_id VARCHAR(255), -- 请求 ID(关联访问日志)
|
||||
ip_address VARCHAR(50), -- 操作 IP
|
||||
user_agent TEXT -- User-Agent
|
||||
);
|
||||
|
||||
CREATE INDEX idx_account_log_operator ON tb_account_operation_log(operator_id, created_at);
|
||||
CREATE INDEX idx_account_log_target ON tb_account_operation_log(target_account_id, created_at);
|
||||
CREATE INDEX idx_account_log_created ON tb_account_operation_log(created_at DESC);
|
||||
```
|
||||
|
||||
**异步写入策略**:
|
||||
- 使用 Goroutine 异步写入,不阻塞主流程
|
||||
- 写入失败只记录错误日志,不影响业务操作
|
||||
- 未来可扩展为 Asynq 任务队列(支持重试)
|
||||
|
||||
**Service 设计**:
|
||||
```go
|
||||
// internal/service/account_audit/service.go
|
||||
type Service struct {
|
||||
store *postgres.AccountOperationLogStore
|
||||
}
|
||||
|
||||
func (s *Service) LogOperation(ctx context.Context, log *model.AccountOperationLog) {
|
||||
// 异步写入,不阻塞主流程
|
||||
go func() {
|
||||
if err := s.store.Create(context.Background(), log); err != nil {
|
||||
logger.GetAppLogger().Error("写入账号操作日志失败",
|
||||
zap.Uint("operator_id", log.OperatorID),
|
||||
zap.String("operation_type", log.OperationType),
|
||||
zap.Error(err))
|
||||
}
|
||||
}()
|
||||
}
|
||||
```
|
||||
|
||||
**集成方式**:
|
||||
```go
|
||||
// AccountService.Create 中集成
|
||||
func (s *Service) Create(ctx context.Context, req *dto.CreateAccountRequest) (*model.Account, error) {
|
||||
// 1. 权限检查...
|
||||
|
||||
// 2. 创建账号...
|
||||
account, err := s.accountStore.Create(ctx, account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 3. 记录审计日志(异步)
|
||||
s.auditService.LogOperation(ctx, &model.AccountOperationLog{
|
||||
OperatorID: currentUserID,
|
||||
OperatorType: currentUserType,
|
||||
OperatorName: currentUsername,
|
||||
TargetAccountID: &account.ID,
|
||||
TargetUsername: account.Username,
|
||||
TargetUserType: account.UserType,
|
||||
OperationType: "create",
|
||||
OperationDesc: fmt.Sprintf("创建账号: %s", account.Username),
|
||||
AfterData: toJSON(account),
|
||||
RequestID: middleware.GetRequestIDFromContext(ctx),
|
||||
IPAddress: middleware.GetIPFromContext(ctx),
|
||||
UserAgent: middleware.GetUserAgentFromContext(ctx),
|
||||
})
|
||||
|
||||
return account, nil
|
||||
}
|
||||
```
|
||||
|
||||
**理由**:
|
||||
- ✅ JSONB 字段存储完整变更数据,便于审计和回溯
|
||||
- ✅ 异步写入不影响业务性能
|
||||
- ✅ 关联 request_id 可以串联访问日志和审计日志
|
||||
- ✅ 索引优化支持按操作人、目标账号、时间快速查询
|
||||
|
||||
### 决策 5:统一错误返回策略
|
||||
|
||||
**原则**:越权访问统一返回"无权限操作该资源或资源不存在"
|
||||
|
||||
**实现**:
|
||||
```go
|
||||
// Update 操作
|
||||
func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdateAccountRequest) error {
|
||||
// 1. GetByID 会被 GORM Callback 自动过滤
|
||||
account, err := s.accountStore.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
// ✅ 统一返回:可能是越权,也可能是真不存在
|
||||
return errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
|
||||
}
|
||||
return errors.Wrap(errors.CodeInternalError, err, "获取账号失败")
|
||||
}
|
||||
|
||||
// 2. 二次权限验证(虽然 GetByID 已过滤,但显式检查更安全)
|
||||
userType := middleware.GetUserTypeFromContext(ctx)
|
||||
if userType == constants.UserTypeAgent {
|
||||
if account.ShopID == nil {
|
||||
return errors.New(errors.CodeForbidden, "无权限操作该账号")
|
||||
}
|
||||
if err := middleware.CanManageShop(ctx, *account.ShopID, s.shopStore); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 更新操作...
|
||||
}
|
||||
```
|
||||
|
||||
**理由**:
|
||||
- ✅ 防止信息泄露(攻击者无法通过错误消息判断资源是否存在)
|
||||
- ✅ 统一用户体验(所有越权场景返回相同错误消息)
|
||||
- ✅ 符合安全最佳实践(OWASP 推荐)
|
||||
|
||||
### 决策 6:认证接口统一策略
|
||||
|
||||
**保守合并**:只合并后台和 H5 认证,保留个人客户认证
|
||||
|
||||
**理由**:
|
||||
- 后台和 H5 认证逻辑完全相同:
|
||||
- 都是基于用户名+密码登录
|
||||
- 都返回 Access Token + Refresh Token
|
||||
- 都使用 Redis 存储 Token
|
||||
- 都支持相同的用户类型(超管、平台、代理、企业)
|
||||
- 个人客户认证逻辑不同:
|
||||
- 支持微信授权登录(OAuth)
|
||||
- 支持手机号+验证码登录
|
||||
- Token 使用 JWT 而非 Redis
|
||||
- 业务逻辑独立,不适合合并
|
||||
|
||||
**实现**:
|
||||
```go
|
||||
// 新路由:/api/auth/*
|
||||
POST /api/auth/login // 统一登录(后台+H5)
|
||||
POST /api/auth/logout // 统一登出
|
||||
POST /api/auth/refresh-token // 刷新 Token
|
||||
GET /api/auth/me // 获取用户信息
|
||||
PUT /api/auth/password // 修改密码
|
||||
|
||||
// 保留:/api/c/v1/*(个人客户认证)
|
||||
POST /api/c/v1/login/send-code // 发送验证码
|
||||
POST /api/c/v1/login // 手机号登录
|
||||
POST /api/c/v1/wechat/auth // 微信授权登录
|
||||
```
|
||||
|
||||
**向后兼容处理**:
|
||||
- 旧接口立即删除(激进策略)
|
||||
- 前端需要同步更新所有认证接口调用
|
||||
- 通过 API 文档和 Breaking Changes 公告通知前端
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
### 风险 1:前端大规模接口迁移
|
||||
|
||||
**风险**:20+ 个接口路径变更,前端需要同步更新,可能遗漏导致功能异常
|
||||
|
||||
**缓解措施**:
|
||||
1. 提供完整的新旧路由映射表(在 proposal.md 中已列出)
|
||||
2. 生成新的 OpenAPI 文档,前端通过文档更新
|
||||
3. 后端先部署,前端更新后再切流量
|
||||
4. 保留一周观察期,发现问题立即回滚
|
||||
|
||||
### 风险 2:操作审计日志丢失
|
||||
|
||||
**风险**:异步写入失败导致审计日志丢失,无法追溯操作记录
|
||||
|
||||
**缓解措施**:
|
||||
1. 写入失败记录 Error 级别日志,包含完整审计信息
|
||||
2. 通过访问日志(access.log)兜底,可以追溯请求记录
|
||||
3. 后续迭代升级为 Asynq 任务队列,支持重试和持久化
|
||||
|
||||
### 风险 3:权限检查性能影响
|
||||
|
||||
**风险**:每次 Create 操作需要调用 GetSubordinateShopIDs,可能影响性能
|
||||
|
||||
**当前缓解**:
|
||||
- GetSubordinateShopIDs 已有 Redis 缓存(30分钟),命中率高
|
||||
- 代理账号创建频率低(< 10 次/分钟),性能影响 < 5ms
|
||||
|
||||
**未来优化**:
|
||||
- 如果成为瓶颈,可以预加载下级店铺 ID 到 context
|
||||
- 超级管理员和平台用户跳过此检查,不受影响
|
||||
|
||||
### 权衡 1:审计日志查询接口延后
|
||||
|
||||
**权衡**:本次只实现日志记录,不实现查询接口
|
||||
|
||||
**理由**:
|
||||
- 查询接口需要设计复杂的筛选条件(按时间、操作人、目标账号等)
|
||||
- 需要考虑权限控制(代理只能查看自己店铺的日志)
|
||||
- 优先保证核心功能(账号管理)稳定上线
|
||||
- 后续迭代专门实现审计日志查询功能
|
||||
|
||||
### 权衡 2:删除而非标记废弃旧接口
|
||||
|
||||
**权衡**:激进策略,直接删除旧接口,而非保留并标记 deprecated
|
||||
|
||||
**理由**:
|
||||
- 旧接口数量多(20+),保留会导致代码库臃肿
|
||||
- 新旧接口功能完全重复,维护成本高
|
||||
- 前端有资源配合同步更新(用户已确认)
|
||||
- Breaking Change 在提案中已充分说明
|
||||
|
||||
**后果**:
|
||||
- 前端必须同步更新,无法渐进迁移
|
||||
- 发现问题需要立即回滚整个版本
|
||||
- 需要充分测试后再上线
|
||||
|
||||
## Migration Plan
|
||||
|
||||
### 阶段 1:代码重构(预计 3 天)
|
||||
|
||||
1. **Day 1**:权限检查和审计日志基础设施
|
||||
- 创建 `pkg/middleware/permission_helper.go`
|
||||
- 创建审计日志 Model、Store、Service
|
||||
- 创建数据库迁移文件
|
||||
- 单元测试覆盖
|
||||
|
||||
2. **Day 2**:AccountService 重构
|
||||
- 扩展 AccountService,添加权限检查
|
||||
- 集成审计日志记录
|
||||
- 删除 ShopAccountService、CustomerAccountService
|
||||
- 单元测试覆盖
|
||||
|
||||
3. **Day 3**:Handler 和路由重构
|
||||
- 扩展 AccountHandler
|
||||
- 删除 ShopAccountHandler、CustomerAccountHandler
|
||||
- 重构路由注册逻辑
|
||||
- 集成测试覆盖
|
||||
|
||||
### 阶段 2:测试和文档(预计 2 天)
|
||||
|
||||
4. **Day 4**:全面测试
|
||||
- 集成测试:account_permission_test.go(越权防护)
|
||||
- 集成测试:account_audit_test.go(审计日志)
|
||||
- 回归测试:确保现有功能不受影响
|
||||
- 性能测试:验证 P95 < 200ms
|
||||
|
||||
5. **Day 5**:文档和交接
|
||||
- 生成新的 OpenAPI 文档
|
||||
- 编写迁移指南(新旧路由映射)
|
||||
- 前端对接会议,说明 Breaking Changes
|
||||
- 准备回滚方案
|
||||
|
||||
### 阶段 3:部署和监控(预计 1 天)
|
||||
|
||||
6. **Day 6**:灰度发布
|
||||
- 执行数据库迁移(创建审计日志表)
|
||||
- 部署后端新版本
|
||||
- 前端更新接口调用
|
||||
- 监控错误率和响应时间
|
||||
|
||||
7. **Day 7**:全量观察
|
||||
- 监控审计日志写入情况
|
||||
- 监控 API 错误率(重点关注 403 错误)
|
||||
- 验证权限检查有效性
|
||||
- 准备随时回滚
|
||||
|
||||
### 回滚策略
|
||||
|
||||
**触发条件**:
|
||||
- API 错误率 > 5%
|
||||
- P95 响应时间 > 300ms
|
||||
- 发现严重安全漏洞
|
||||
- 前端无法在 1 天内完成迁移
|
||||
|
||||
**回滚步骤**:
|
||||
1. 回滚后端代码到上一个版本
|
||||
2. 前端回滚到旧接口调用
|
||||
3. 审计日志表保留(不删除数据)
|
||||
4. 总结问题,重新规划迁移
|
||||
|
||||
## Open Questions
|
||||
|
||||
### Q1:是否需要批量迁移现有账号数据?
|
||||
**当前状态**:无需迁移,数据模型不变
|
||||
|
||||
**说明**:
|
||||
- Account 表结构不变
|
||||
- user_type 字段已经区分四种账号类型
|
||||
- 只是接口和代码重构,不涉及数据迁移
|
||||
|
||||
### Q2:审计日志是否需要定期归档?
|
||||
**当前决策**:暂不归档,后续根据数据增长情况决定
|
||||
|
||||
**说明**:
|
||||
- 初期数据量小(< 10万条/月)
|
||||
- PostgreSQL JSONB 查询性能足够
|
||||
- 如果后续数据量大(> 100万条),可以:
|
||||
- 按月分表(tb_account_operation_log_202601)
|
||||
- 或归档到对象存储
|
||||
|
||||
### Q3:是否需要支持操作撤销功能?
|
||||
**当前决策**:不支持,审计日志只做记录和查询
|
||||
|
||||
**理由**:
|
||||
- 账号操作撤销逻辑复杂(如删除账号后重新激活)
|
||||
- 现有需求不明确
|
||||
- 可以通过手动操作实现(如重新创建账号)
|
||||
- 后续如有需求再单独设计
|
||||
@@ -0,0 +1,118 @@
|
||||
# 统一账号管理接口重构
|
||||
|
||||
## Why
|
||||
|
||||
当前账号管理接口存在严重的架构混乱和安全漏洞:
|
||||
1. **接口重复**:`/accounts` 和 `/platform-accounts` 使用同一个 Handler,功能完全重复(20个重复接口)
|
||||
2. **功能不一致**:平台账号有完整的 CRUD + 角色管理,而代理/企业账号缺少关键功能
|
||||
3. **命名混乱**:`/customer-accounts` 实际管理的是企业账号,代码注释错误
|
||||
4. **安全漏洞**:Create 操作缺少越权检查,代理可以为其他店铺创建账号
|
||||
5. **可维护性差**:三个独立的 Service(Account、ShopAccount、CustomerAccount)导致代码重复和不一致
|
||||
|
||||
这次重构将统一接口架构,消除重复,加固安全防护,并添加完整的操作审计,为后续功能扩展打下坚实基础。
|
||||
|
||||
## What Changes
|
||||
|
||||
- **BREAKING**: 删除旧路由
|
||||
- 删除 `/api/admin/platform-accounts/*`(10个接口)
|
||||
- 删除 `/api/admin/shop-accounts/*`(5个接口)
|
||||
- 删除 `/api/admin/customer-accounts/*`(5个接口)
|
||||
- **新增**: 统一账号管理路由
|
||||
- `/api/admin/accounts/platform/*`(平台账号管理)
|
||||
- `/api/admin/accounts/shop/*`(代理账号管理)
|
||||
- `/api/admin/accounts/enterprise/*`(企业账号管理)
|
||||
- **BREAKING**: 认证接口统一
|
||||
- 删除 `/api/admin/login`、`/api/admin/logout` 等(5个接口)
|
||||
- 删除 `/api/h5/login`、`/api/h5/logout` 等(5个接口)
|
||||
- 新增 `/api/auth/*` 统一认证(5个接口)
|
||||
- 保留 `/api/c/v1/*` 个人客户认证(独立业务逻辑)
|
||||
- **新增**: 三层越权防护机制
|
||||
- 路由层:企业账号中间件拦截
|
||||
- Service 层:CanManageShop/CanManageEnterprise 权限检查
|
||||
- GORM 层:已有自动过滤(保持)
|
||||
- **新增**: 操作审计系统
|
||||
- 数据库迁移:创建 `tb_account_operation_log` 表
|
||||
- Service:AccountAuditService 记录所有账号操作
|
||||
- 集成:Create/Update/Delete/AssignRoles 自动记录
|
||||
- **重构**: 合并 Service 层
|
||||
- 删除 ShopAccountService、CustomerAccountService
|
||||
- 扩展 AccountService 支持所有账号类型
|
||||
- 统一错误返回:"无权限操作该资源或资源不存在"
|
||||
- **重构**: 合并 Handler 层
|
||||
- 删除 ShopAccountHandler、CustomerAccountHandler
|
||||
- 扩展 AccountHandler 支持所有账号类型
|
||||
- **新增**: 权限辅助函数
|
||||
- `pkg/middleware/permission_helper.go`
|
||||
- CanManageShop:验证代理对目标店铺的管理权限
|
||||
- CanManageEnterprise:验证代理对目标企业的管理权限
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `account-permission-check`:账号管理权限检查机制(三层防护)
|
||||
- `account-operation-audit`:账号操作审计日志系统
|
||||
- `unified-auth-api`:统一认证接口(后台+H5)
|
||||
|
||||
### Modified Capabilities
|
||||
- `account-management`:账号管理接口架构(统一路由结构,消除重复)
|
||||
|
||||
## Impact
|
||||
|
||||
**代码变更**:
|
||||
- 删除文件:
|
||||
- `internal/handler/admin/shop_account.go`
|
||||
- `internal/handler/admin/customer_account.go`
|
||||
- `internal/service/shop_account/service.go`
|
||||
- `internal/service/customer_account/service.go`
|
||||
- `internal/routes/shop.go`(部分)
|
||||
- `internal/routes/customer_account.go`
|
||||
- 修改文件:
|
||||
- `internal/handler/admin/account.go`(扩展支持所有账号类型)
|
||||
- `internal/service/account/service.go`(添加权限检查和审计)
|
||||
- `internal/routes/account.go`(新路由结构)
|
||||
- `internal/routes/admin.go`(更新路由注册)
|
||||
- 新增文件:
|
||||
- `pkg/middleware/permission_helper.go`(权限检查函数)
|
||||
- `internal/model/account_operation_log.go`(审计日志模型)
|
||||
- `internal/store/postgres/account_operation_log_store.go`(审计日志存储)
|
||||
- `internal/service/account_audit/service.go`(审计日志服务)
|
||||
- `migrations/XXXXXX_create_account_operation_log.up.sql`(数据库迁移)
|
||||
|
||||
**API 变更**(Breaking Changes):
|
||||
- 前端需要更新所有账号管理接口调用
|
||||
- 新旧路由映射:
|
||||
```
|
||||
旧:POST /api/admin/platform-accounts
|
||||
新:POST /api/admin/accounts/platform
|
||||
|
||||
旧:GET /api/admin/shop-accounts
|
||||
新:GET /api/admin/accounts/shop
|
||||
|
||||
旧:POST /api/admin/customer-accounts
|
||||
新:POST /api/admin/accounts/enterprise
|
||||
|
||||
旧:POST /api/admin/login
|
||||
新:POST /api/auth/login
|
||||
```
|
||||
|
||||
**测试变更**:
|
||||
- 删除:`tests/integration/platform_account_test.go`(已有 account_test.go)
|
||||
- 删除:`tests/integration/shop_account_management_test.go`
|
||||
- 删除:`tests/unit/customer_account_service_test.go`
|
||||
- 修改:`tests/integration/account_test.go`(扩展覆盖所有账号类型)
|
||||
- 新增:`tests/integration/account_permission_test.go`(越权防护测试)
|
||||
- 新增:`tests/integration/account_audit_test.go`(审计日志测试)
|
||||
|
||||
**依赖影响**:
|
||||
- 无新增外部依赖
|
||||
- 内部依赖调整:AccountService 新增 ShopStore 和 EnterpriseStore 依赖
|
||||
|
||||
**性能影响**:
|
||||
- 权限检查:增加 GetSubordinateShopIDs 调用(已有 Redis 缓存,影响 < 5ms)
|
||||
- 审计日志:异步写入,不阻塞主流程
|
||||
- 预期 API 响应时间增加 < 10ms
|
||||
|
||||
**安全提升**:
|
||||
- 修复 Create 操作越权漏洞(Critical)
|
||||
- 统一错误返回,防止信息泄露
|
||||
- 完整操作审计,满足合规要求
|
||||
@@ -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/status,status=1
|
||||
- **THEN** 验证权限,更新状态为启用,返回成功
|
||||
|
||||
#### Scenario: 禁用账号
|
||||
- **WHEN** PUT /api/admin/accounts/{type}/:id/status,status=0
|
||||
- **THEN** 验证权限,更新状态为禁用,返回成功
|
||||
|
||||
### Requirement: 所有账号类型支持角色管理
|
||||
系统 SHALL 为所有账号类型提供统一的角色管理功能。
|
||||
|
||||
#### Scenario: 分配角色
|
||||
- **WHEN** POST /api/admin/accounts/{type}/:id/roles,body: {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/roles,body: {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 及相关文件应被删除
|
||||
@@ -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_roles,after_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 可以查询到:请求参数、权限检查、数据库操作、响应结果
|
||||
@@ -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)
|
||||
@@ -0,0 +1,86 @@
|
||||
# 统一认证接口规格
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 合并后台和H5认证接口
|
||||
系统 SHALL 提供统一认证接口 /api/auth/*,支持后台和 H5 两种场景的认证。
|
||||
|
||||
#### Scenario: 后台用户登录
|
||||
- **WHEN** 用户调用 POST /api/auth/login,user_type IN (1,2,3,4)
|
||||
- **THEN** 验证用户名+密码,返回 Access Token + Refresh Token
|
||||
|
||||
#### Scenario: H5用户登录
|
||||
- **WHEN** H5 用户调用 POST /api/auth/login,user_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 Token(24小时)和 Refresh Token(7天)
|
||||
|
||||
#### Scenario: Token存储在Redis
|
||||
- **WHEN** 生成 Token
|
||||
- **THEN** 存储在 Redis,Key 格式为 "auth:token:{token}"
|
||||
|
||||
#### Scenario: 用户类型过滤不变
|
||||
- **WHEN** 登录请求中包含 user_type
|
||||
- **THEN** 验证用户类型是否与账号类型匹配
|
||||
|
||||
### Requirement: 响应格式保持兼容
|
||||
系统 SHALL 保持登录响应格式兼容,包含 menus 和 buttons。
|
||||
|
||||
#### Scenario: 登录响应包含菜单
|
||||
- **WHEN** 用户登录成功
|
||||
- **THEN** 响应应包含 menus(菜单树结构)
|
||||
|
||||
#### Scenario: 登录响应包含按钮权限
|
||||
- **WHEN** 用户登录成功
|
||||
- **THEN** 响应应包含 buttons(按钮权限列表)
|
||||
|
||||
#### Scenario: 响应格式不变
|
||||
- **WHEN** 用户登录成功
|
||||
- **THEN** 响应格式应与旧接口完全一致,前端无需修改解析逻辑
|
||||
@@ -0,0 +1,171 @@
|
||||
# 统一账号管理接口重构 - 任务清单
|
||||
|
||||
## 1. 数据库迁移
|
||||
|
||||
- [x] 1.1 创建 `migrations/XXXXXX_create_account_operation_log.up.sql` 迁移文件(创建审计日志表)
|
||||
- [x] 1.2 创建 `migrations/XXXXXX_create_account_operation_log.down.sql` 回滚文件
|
||||
- [x] 1.3 运行迁移验证表结构和索引创建成功
|
||||
|
||||
## 2. 权限检查基础设施
|
||||
|
||||
- [x] 2.1 创建 `pkg/middleware/permission_helper.go` 文件
|
||||
- [x] 2.2 实现 `CanManageShop` 函数(验证代理对目标店铺的管理权限)
|
||||
- [x] 2.3 实现 `CanManageEnterprise` 函数(验证代理对目标企业的管理权限)
|
||||
- [x] 2.4 定义 `ShopStoreInterface` 和 `EnterpriseStoreInterface` 接口(用于依赖倒置)
|
||||
- [x] 2.5 编写单元测试 `pkg/middleware/permission_helper_test.go`(覆盖率 ≥ 90%)
|
||||
- [x] 2.6 运行 `lsp_diagnostics` 验证代码无错误
|
||||
|
||||
## 3. 审计日志系统
|
||||
|
||||
- [x] 3.1 创建 `internal/model/account_operation_log.go`(审计日志模型)
|
||||
- [x] 3.2 创建 `internal/store/postgres/account_operation_log_store.go`(审计日志存储层)
|
||||
- [x] 3.3 实现 `AccountOperationLogStore.Create` 方法
|
||||
- [x] 3.4 创建 `internal/service/account_audit/service.go`(审计日志服务层)
|
||||
- [x] 3.5 实现 `AccountAuditService.LogOperation` 方法(异步写入,Goroutine)
|
||||
- [x] 3.6 编写单元测试 `internal/service/account_audit/service_test.go`(覆盖率 ≥ 90%)
|
||||
- [x] 3.7 运行 `lsp_diagnostics` 验证代码无错误
|
||||
|
||||
## 4. AccountService 重构(添加权限检查和审计)
|
||||
|
||||
- [x] 4.1 为 `AccountService` 添加 `shopStore` 和 `enterpriseStore` 依赖
|
||||
- [x] 4.2 为 `AccountService` 添加 `auditService` 依赖
|
||||
- [x] 4.3 重构 `Create` 方法:添加三层权限检查(类型级 + 资源级 + GORM 兜底)
|
||||
- [x] 4.4 重构 `Create` 方法:集成审计日志记录(异步)
|
||||
- [x] 4.5 重构 `Update` 方法:添加权限检查和审计日志(记录 before_data 和 after_data)
|
||||
- [x] 4.6 重构 `Delete` 方法:添加权限检查和审计日志
|
||||
- [x] 4.7 重构 `AssignRoles` 方法:添加权限检查和审计日志
|
||||
- [x] 4.8 重构 `RemoveRole` 方法:添加权限检查和审计日志
|
||||
- [x] 4.9 修改错误返回:统一为"无权限操作该资源或资源不存在"
|
||||
- [x] 4.10 编写单元测试 `internal/service/account/service_test.go`(覆盖率 ≥ 90%)
|
||||
- [x] 4.11 运行 `lsp_diagnostics` 验证代码无错误
|
||||
|
||||
## 5. 删除旧 Service 层代码
|
||||
|
||||
- [x] 5.1 删除 `internal/service/shop_account/service.go`
|
||||
- [x] 5.2 删除 `internal/service/customer_account/service.go`
|
||||
- [x] 5.3 删除相关测试文件 `tests/unit/shop_account_service_test.go` 和 `tests/unit/customer_account_service_test.go`
|
||||
- [x] 5.4 运行 `go build ./...` 确保没有引用残留
|
||||
|
||||
## 6. AccountHandler 重构(支持所有账号类型)
|
||||
|
||||
- [x] 6.1 重构 `AccountHandler.Create` 方法:支持 platform/shop/enterprise 三种类型
|
||||
- [x] 6.2 重构 `AccountHandler.List` 方法:支持按账号类型筛选(username/phone/status/shop_id/enterprise_id)
|
||||
- [x] 6.3 重构 `AccountHandler.GetByID` 方法:支持所有账号类型
|
||||
- [x] 6.4 重构 `AccountHandler.Update` 方法:支持所有账号类型
|
||||
- [x] 6.5 重构 `AccountHandler.Delete` 方法:支持所有账号类型
|
||||
- [x] 6.6 重构 `AccountHandler.UpdatePassword` 方法:支持所有账号类型
|
||||
- [x] 6.7 重构 `AccountHandler.UpdateStatus` 方法:支持所有账号类型
|
||||
- [x] 6.8 重构 `AccountHandler.AssignRoles` 方法:支持所有账号类型
|
||||
- [x] 6.9 重构 `AccountHandler.GetRoles` 方法:支持所有账号类型
|
||||
- [x] 6.10 重构 `AccountHandler.RemoveRole` 方法:支持所有账号类型
|
||||
- [x] 6.11 运行 `lsp_diagnostics` 验证代码无错误
|
||||
|
||||
## 7. 删除旧 Handler 层代码
|
||||
|
||||
- [x] 7.1 删除 `internal/handler/admin/shop_account.go`
|
||||
- [x] 7.2 删除 `internal/handler/admin/customer_account.go`
|
||||
- [x] 7.3 运行 `go build ./...` 确保没有引用残留
|
||||
|
||||
## 8. 路由重构(统一账号管理路由)
|
||||
|
||||
- [x] 8.1 重构 `internal/routes/account.go`:实现新路由结构
|
||||
- [x] 8.2 注册平台账号路由组 `/api/admin/accounts/platform/*`(10个接口)
|
||||
- [x] 8.3 注册代理账号路由组 `/api/admin/accounts/shop/*`(10个接口)
|
||||
- [x] 8.4 注册企业账号路由组 `/api/admin/accounts/enterprise/*`(10个接口)
|
||||
- [x] 8.5 为企业账号路由组添加中间件拦截(企业账号禁止访问账号管理)
|
||||
- [x] 8.6 删除旧路由注册:`/api/admin/platform-accounts/*`
|
||||
- [x] 8.7 删除旧路由注册:`/api/admin/shop-accounts/*`
|
||||
- [x] 8.8 删除旧路由注册:`/api/admin/customer-accounts/*`
|
||||
- [x] 8.9 运行 `go build ./...` 确保路由编译通过
|
||||
|
||||
## 9. 认证接口统一
|
||||
|
||||
- [x] 9.1 创建 `internal/handler/auth/handler.go`(统一认证 Handler)
|
||||
- [x] 9.2 实现 `Login` 方法(合并后台和 H5 登录逻辑)
|
||||
- [x] 9.3 实现 `Logout` 方法(统一登出)
|
||||
- [x] 9.4 实现 `RefreshToken` 方法(统一刷新 Token)
|
||||
- [x] 9.5 实现 `GetMe` 方法(统一获取用户信息)
|
||||
- [x] 9.6 实现 `UpdatePassword` 方法(统一修改密码)
|
||||
- [x] 9.7 创建 `internal/routes/auth.go` 注册统一认证路由 `/api/auth/*`
|
||||
- [x] 9.8 删除旧认证路由:`/api/admin/login` 等(5个接口)
|
||||
- [x] 9.9 删除旧认证路由:`/api/h5/login` 等(5个接口)
|
||||
- [x] 9.10 保留个人客户认证路由:`/api/c/v1/*`(不修改)
|
||||
- [x] 9.11 运行 `lsp_diagnostics` 验证代码无错误
|
||||
|
||||
## 10. Bootstrap 更新(依赖注入调整)
|
||||
|
||||
- [x] 10.1 更新 `internal/bootstrap/stores.go`:添加 `AccountOperationLogStore` 初始化
|
||||
- [x] 10.2 更新 `internal/bootstrap/services.go`:添加 `AccountAuditService` 初始化
|
||||
- [x] 10.3 更新 `internal/bootstrap/services.go`:更新 `AccountService` 依赖注入(添加 shopStore、enterpriseStore、auditService)
|
||||
- [x] 10.4 更新 `internal/bootstrap/handlers.go`:添加 `AuthHandler` 初始化
|
||||
- [x] 10.5 更新 `internal/bootstrap/handlers.go`:删除 `ShopAccountHandler` 和 `CustomerAccountHandler` 初始化
|
||||
- [x] 10.6 运行 `go build ./...` 确保编译通过
|
||||
|
||||
## 11. 文档生成器更新
|
||||
|
||||
- [x] 11.1 更新 `cmd/api/docs.go`:添加新路由到 Handlers 结构体(accounts/platform、accounts/shop、accounts/enterprise、auth)
|
||||
- [x] 11.2 更新 `cmd/api/docs.go`:删除旧路由(platform-accounts、shop-accounts、customer-accounts、admin/login、h5/login)
|
||||
- [x] 11.3 更新 `cmd/gendocs/main.go`:同步更新 Handlers 初始化逻辑
|
||||
- [x] 11.4 运行 `go run cmd/gendocs/main.go` 生成新的 OpenAPI 文档
|
||||
- [x] 11.5 验证生成的 `docs/openapi.yaml` 包含所有新路由且不包含旧路由
|
||||
|
||||
## 12. 集成测试(越权防护)
|
||||
|
||||
- [x] 12.1 创建 `tests/integration/account_permission_test.go`
|
||||
- [x] 12.2 测试场景:企业账号访问账号管理接口被路由层拦截(返回 403)
|
||||
- [x] 12.3 测试场景:代理账号创建自己店铺的账号成功
|
||||
- [x] 12.4 测试场景:代理账号创建下级店铺的账号成功
|
||||
- [x] 12.5 测试场景:代理账号创建其他店铺的账号失败(返回 403)
|
||||
- [x] 12.6 测试场景:代理账号创建平台账号失败(返回 403)
|
||||
- [x] 12.7 测试场景:平台账号创建任意类型账号成功
|
||||
- [x] 12.8 测试场景:超级管理员创建任意类型账号成功
|
||||
- [x] 12.9 测试场景:查询不存在的账号返回"无权限操作该资源或资源不存在"
|
||||
- [x] 12.10 测试场景:查询越权的账号返回相同错误消息
|
||||
- [x] 12.11 运行 `source .env.local && go test -v ./tests/integration/account_permission_test.go` 验证所有测试通过
|
||||
|
||||
## 13. 集成测试(审计日志)
|
||||
|
||||
- [x] 13.1 创建 `tests/integration/account_audit_test.go`
|
||||
- [x] 13.2 测试场景:创建账号时记录审计日志(验证 operation_type=create,包含 after_data)
|
||||
- [x] 13.3 测试场景:更新账号时记录 before_data 和 after_data
|
||||
- [x] 13.4 测试场景:删除账号时记录审计日志(验证 operation_type=delete)
|
||||
- [x] 13.5 测试场景:分配角色时记录审计日志(验证 operation_type=assign_roles)
|
||||
- [x] 13.6 测试场景:移除角色时记录审计日志(验证 operation_type=remove_role)
|
||||
- [x] 13.7 测试场景:审计日志包含完整的操作上下文(operator_id、target_account_id、request_id、ip_address)
|
||||
- [x] 13.8 测试场景:审计日志写入失败不影响业务操作(模拟数据库写入失败)
|
||||
- [x] 13.9 运行 `source .env.local && go test -v ./tests/integration/account_audit_test.go` 验证所有测试通过
|
||||
|
||||
## 14. 回归测试(扩展现有测试)
|
||||
|
||||
- [x] 14.1 更新 `tests/integration/account_test.go`:扩展覆盖所有账号类型(platform/shop/enterprise)
|
||||
- [x] 14.2 测试场景:平台账号 CRUD 操作(原有功能保持)
|
||||
- [x] 14.3 测试场景:代理账号 CRUD 操作(新增)
|
||||
- [x] 14.4 测试场景:企业账号 CRUD 操作(新增)
|
||||
- [x] 14.5 测试场景:角色管理功能对所有账号类型生效(新增)
|
||||
- [x] 14.6 删除 `tests/integration/platform_account_test.go`(与 account_test.go 重复)
|
||||
- [x] 14.7 删除 `tests/integration/shop_account_management_test.go`(功能已合并到 account_test.go)
|
||||
- [x] 14.8 运行 `source .env.local && go test -v ./tests/integration/account_test.go` 验证所有测试通过
|
||||
|
||||
## 15. 性能测试(已跳过 - 用户决定)
|
||||
|
||||
- [ ] ~~15.1 验证权限检查(GetSubordinateShopIDs)缓存命中率 > 80%~~
|
||||
- [ ] ~~15.2 验证审计日志异步写入不阻塞主流程(API 响应时间增加 < 1ms)~~
|
||||
- [ ] ~~15.3 压力测试:100 并发创建账号请求,P95 响应时间 < 200ms~~
|
||||
- [ ] ~~15.4 压力测试:100 并发查询账号列表请求,P95 响应时间 < 200ms~~
|
||||
- [ ] ~~15.5 验证审计日志写入性能(1000 条/秒,数据库无明显压力)~~
|
||||
|
||||
## 16. 文档更新
|
||||
|
||||
- [x] 16.1 创建 `docs/account-management-refactor/迁移指南.md`(新旧路由映射表)
|
||||
- [x] 16.2 创建 `docs/account-management-refactor/功能总结.md`(重构内容、安全提升、操作审计说明)
|
||||
- [x] 16.3 创建 `docs/account-management-refactor/API文档.md`(所有新接口的请求/响应示例)
|
||||
- [x] 16.4 更新 `README.md`:添加账号管理重构说明链接
|
||||
- [x] 16.5 更新 `AGENTS.md`:添加越权防护和审计日志使用规范
|
||||
|
||||
## 17. 部署准备
|
||||
|
||||
- [ ] 17.1 生成生产环境数据库迁移脚本(包含 CREATE TABLE 和索引)
|
||||
- [ ] 17.2 编写回滚方案文档(代码回滚步骤 + 数据库回滚脚本)
|
||||
- [ ] 17.3 准备灰度发布计划(先部署后端,等前端更新后再切流量)
|
||||
- [ ] 17.4 准备监控告警规则(API 错误率 > 5%、P95 响应时间 > 300ms 自动告警)
|
||||
- [ ] 17.5 编写前端对接会议 PPT(Breaking Changes 说明、新旧路由映射、迁移时间表)
|
||||
143
openspec/specs/account-management/spec.md
Normal file
143
openspec/specs/account-management/spec.md
Normal 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/status,status=1
|
||||
- **THEN** 验证权限,更新状态为启用,返回成功
|
||||
|
||||
#### Scenario: 禁用账号
|
||||
- **WHEN** PUT /api/admin/accounts/{type}/:id/status,status=0
|
||||
- **THEN** 验证权限,更新状态为禁用,返回成功
|
||||
|
||||
### Requirement: 所有账号类型支持角色管理
|
||||
系统 SHALL 为所有账号类型提供统一的角色管理功能。
|
||||
|
||||
#### Scenario: 分配角色
|
||||
- **WHEN** POST /api/admin/accounts/{type}/:id/roles,body: {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/roles,body: {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 及相关文件应被删除
|
||||
105
openspec/specs/account-operation-audit/spec.md
Normal file
105
openspec/specs/account-operation-audit/spec.md
Normal 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_roles,after_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 可以查询到:请求参数、权限检查、数据库操作、响应结果
|
||||
127
openspec/specs/account-permission-check/spec.md
Normal file
127
openspec/specs/account-permission-check/spec.md
Normal 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)
|
||||
86
openspec/specs/unified-auth-api/spec.md
Normal file
86
openspec/specs/unified-auth-api/spec.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# 统一认证接口规格
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 合并后台和H5认证接口
|
||||
系统 SHALL 提供统一认证接口 /api/auth/*,支持后台和 H5 两种场景的认证。
|
||||
|
||||
#### Scenario: 后台用户登录
|
||||
- **WHEN** 用户调用 POST /api/auth/login,user_type IN (1,2,3,4)
|
||||
- **THEN** 验证用户名+密码,返回 Access Token + Refresh Token
|
||||
|
||||
#### Scenario: H5用户登录
|
||||
- **WHEN** H5 用户调用 POST /api/auth/login,user_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 Token(24小时)和 Refresh Token(7天)
|
||||
|
||||
#### Scenario: Token存储在Redis
|
||||
- **WHEN** 生成 Token
|
||||
- **THEN** 存储在 Redis,Key 格式为 "auth:token:{token}"
|
||||
|
||||
#### Scenario: 用户类型过滤不变
|
||||
- **WHEN** 登录请求中包含 user_type
|
||||
- **THEN** 验证用户类型是否与账号类型匹配
|
||||
|
||||
### Requirement: 响应格式保持兼容
|
||||
系统 SHALL 保持登录响应格式兼容,包含 menus 和 buttons。
|
||||
|
||||
#### Scenario: 登录响应包含菜单
|
||||
- **WHEN** 用户登录成功
|
||||
- **THEN** 响应应包含 menus(菜单树结构)
|
||||
|
||||
#### Scenario: 登录响应包含按钮权限
|
||||
- **WHEN** 用户登录成功
|
||||
- **THEN** 响应应包含 buttons(按钮权限列表)
|
||||
|
||||
#### Scenario: 响应格式不变
|
||||
- **WHEN** 用户登录成功
|
||||
- **THEN** 响应格式应与旧接口完全一致,前端无需修改解析逻辑
|
||||
Reference in New Issue
Block a user