feat: 新增全局操作密码功能,将线下充值验证从登录密码改为统一操作密码
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
- 新增 RedisSystemOperationPasswordKey 常量函数(pkg/constants/redis.go) - 新增 operation_password Service(Set/IsSet/Verify,bcrypt 哈希存 Redis) - 新增 SuperAdminHandler 及两个接口: - POST /api/admin/super-admin/operation-password(设置/重置,仅超级管理员) - GET /api/admin/super-admin/operation-password/status(查询是否已设置) - AgentRecharge.OfflinePay 操作密码验证从"查当前用户登录密码"改为"全局操作密码 Verify" - Bootstrap/路由/文档生成器同步注册
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-04-17
|
||||
74
openspec/changes/super-admin-operation-password/design.md
Normal file
74
openspec/changes/super-admin-operation-password/design.md
Normal file
@@ -0,0 +1,74 @@
|
||||
## Context
|
||||
|
||||
当前代理充值线下确认接口(`OfflinePay`)通过将 `operation_password` 与操作人自己的登录密码(`account.Password`)做 bcrypt 对比来实现操作密码验证。这个机制存在两个问题:
|
||||
|
||||
1. **缺乏独立性**:操作密码与登录密码耦合,修改登录密码即等于修改操作密码,无法独立管控。
|
||||
2. **安全边界不清**:需要操作密码的敏感操作(如确认大额资金流转)应由超级管理员统一管控,而非各操作人分散管理。
|
||||
|
||||
设计决策:操作密码存 Redis,不开新表。原因:操作密码是全局单一配置,没有历史版本需求,Redis 的 SET/GET 完全满足需求,避免引入新表的维护成本。
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- 超级管理员可以设置/修改全局操作密码(存 Redis,bcrypt 哈希)
|
||||
- 提供查询接口返回操作密码是否已设置(不暴露密码本身)
|
||||
- `OfflinePay` 的操作密码验证从"当前用户登录密码"改为"全局操作密码"
|
||||
- 封装 `VerifyOperationPassword` 工具函数,供后续接口统一复用
|
||||
|
||||
**Non-Goals:**
|
||||
- 不做操作密码的过期机制(非当前需求)
|
||||
- 不做操作密码历史记录
|
||||
- 不修改除 `agent_recharge.OfflinePay` 以外的其他接口(其他接口暂无此需求)
|
||||
|
||||
## Decisions
|
||||
|
||||
### 决策1:存储介质选 Redis 而非数据库新表
|
||||
|
||||
**选择**:Redis `SET system:operation_password <bcrypt_hash>` 永久存储(无 TTL)。
|
||||
|
||||
**理由**:全局单一配置,无历史记录需求,Redis 的简单 KV 足够。Key 命名遵循项目规范,定义在 `pkg/constants/redis.go`。
|
||||
|
||||
**备选**:新建 `tb_system_config` 表 → 引入表管理、迁移、GORM 操作,过度设计。
|
||||
|
||||
### 决策2:新接口归属路径
|
||||
|
||||
**选择**:`POST /api/admin/super-admin/operation-password`(设置/修改)和 `GET /api/admin/super-admin/operation-password/status`(查询状态)。
|
||||
|
||||
**理由**:`/super-admin/` 路径前缀清晰表达"仅超级管理员"语义,与现有路由命名惯例一致。Handler 层检查 `userType == UserTypeSuperAdmin`,非超级管理员返回 403。
|
||||
|
||||
### 决策3:Service 层封装独立的 OperationPassword 服务
|
||||
|
||||
**选择**:新建 `internal/service/operation_password/service.go`,提供 `Set(ctx, password)` 和 `Verify(ctx, inputPassword) error` 两个方法。
|
||||
|
||||
**理由**:将操作密码逻辑从 `agent_recharge` Service 中解耦,后续接口可直接注入复用,避免逻辑散落多处。
|
||||
|
||||
### 决策4:未设置时的行为
|
||||
|
||||
**选择**:操作密码未设置时,`Verify` 返回"操作密码未设置,请联系超级管理员"错误,拒绝操作。
|
||||
|
||||
**理由**:不降级为登录密码(安全边界清晰)。初次上线前超级管理员需先设置操作密码。
|
||||
|
||||
### 决策5:bcrypt 哈希存 Redis
|
||||
|
||||
**选择**:Set 时用 `bcrypt.GenerateFromPassword` 哈希后存储,Verify 时用 `bcrypt.CompareHashAndPassword` 对比。
|
||||
|
||||
**理由**:即使 Redis 被访问,也无法还原明文密码。与现有登录密码处理方式一致。
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
**[风险] 初次上线前操作密码未设置**
|
||||
→ `OfflinePay` 接口会返回"操作密码未设置"错误。需在上线前由超级管理员设置初始操作密码。
|
||||
|
||||
**[风险] Redis 数据丢失**
|
||||
→ 如 Redis 数据被清空,操作密码丢失,所有需要操作密码的接口将拒绝服务。
|
||||
→ 缓解:Redis 持久化(AOF/RDB)已在生产环境启用;超级管理员可随时重设。
|
||||
|
||||
**[风险] bcrypt 计算开销**
|
||||
→ bcrypt 默认 cost=10,约 100ms/次。此接口调用频率极低(管理操作),不影响系统整体性能。
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. 部署代码变更(新接口 + Redis 验证逻辑)
|
||||
2. 超级管理员登录后台,调用 `POST /api/admin/super-admin/operation-password` 设置初始操作密码
|
||||
3. 验证 `GET /api/admin/super-admin/operation-password/status` 返回已设置
|
||||
4. 验证 `OfflinePay` 接口使用新操作密码可正常通过
|
||||
34
openspec/changes/super-admin-operation-password/proposal.md
Normal file
34
openspec/changes/super-admin-operation-password/proposal.md
Normal file
@@ -0,0 +1,34 @@
|
||||
## Why
|
||||
|
||||
系统中需要操作密码验证的接口(如代理充值线下确认)目前使用**当前登录用户自己的登录密码**作为操作密码,缺乏独立的操作层安全机制。需要引入一个**全局操作密码**,仅由超级管理员设置和修改,所有需要操作密码的接口统一验证这一密码,实现"操作密码"与"登录密码"的职责分离。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 新增 `POST /api/admin/super-admin/operation-password` 接口:超级管理员设置/修改全局操作密码
|
||||
- 新增 `GET /api/admin/super-admin/operation-password/status` 接口:查询操作密码是否已设置
|
||||
- 全局操作密码以 bcrypt 哈希值存储于 Redis,Key 定义在 `pkg/constants/redis.go`
|
||||
- `agent_recharge` `OfflinePay` 的操作密码验证逻辑从"当前用户登录密码"改为"全局操作密码"
|
||||
- 新增统一工具函数 `VerifyOperationPassword(ctx, inputPassword)` 供后续接口复用
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `global-operation-password`:全局操作密码的设置、修改、验证能力,存储于 Redis,仅超级管理员可维护
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
(无已有 spec 变更)
|
||||
|
||||
## Impact
|
||||
|
||||
| 层级 | 文件 | 变更类型 |
|
||||
|------|------|---------|
|
||||
| Handler | `internal/handler/admin/`(新建 `super_admin.go`) | 新建 |
|
||||
| Service | `internal/service/`(新建 `operation_password/service.go`) | 新建 |
|
||||
| Service | `internal/service/agent_recharge/service.go` | 修改验证逻辑 |
|
||||
| Constants | `pkg/constants/redis.go` | 新增 Redis Key 常量 |
|
||||
| Bootstrap | `internal/bootstrap/handlers.go`、`services.go` | 注册新组件 |
|
||||
| Routes | 路由注册文件 | 新增路由 |
|
||||
|
||||
**依赖**:Redis(已有,无需新增依赖)
|
||||
@@ -0,0 +1,58 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 超级管理员可设置和修改全局操作密码
|
||||
|
||||
系统 SHALL 提供 `POST /api/admin/super-admin/operation-password` 接口,仅允许 `user_type=1`(超级管理员)调用,用于设置或修改全局操作密码。密码以 bcrypt 哈希值存储于 Redis。
|
||||
|
||||
#### Scenario: 超级管理员首次设置操作密码
|
||||
|
||||
- **WHEN** 超级管理员调用 `POST /api/admin/super-admin/operation-password`,传入合法的 `password` 和 `confirm_password`(两者一致)
|
||||
- **THEN** 系统将 bcrypt 哈希后的密码写入 Redis,返回 200 成功
|
||||
|
||||
#### Scenario: 超级管理员修改已有操作密码
|
||||
|
||||
- **WHEN** 操作密码已存在,超级管理员重新调用设置接口传入新密码
|
||||
- **THEN** 系统覆盖 Redis 中的旧哈希值,返回 200 成功
|
||||
|
||||
#### Scenario: 非超级管理员调用被拒绝
|
||||
|
||||
- **WHEN** `user_type != 1` 的账号调用 `POST /api/admin/super-admin/operation-password`
|
||||
- **THEN** 系统返回 403,错误消息"仅超级管理员可设置操作密码"
|
||||
|
||||
#### Scenario: 两次密码不一致
|
||||
|
||||
- **WHEN** 超级管理员传入的 `password` 与 `confirm_password` 不一致
|
||||
- **THEN** 系统返回 400,错误消息"两次输入的密码不一致"
|
||||
|
||||
### Requirement: 可查询操作密码设置状态
|
||||
|
||||
系统 SHALL 提供 `GET /api/admin/super-admin/operation-password/status` 接口,返回操作密码是否已设置(布尔值),不返回密码本身,仅超级管理员可调用。
|
||||
|
||||
#### Scenario: 查询已设置状态
|
||||
|
||||
- **WHEN** 操作密码已设置,超级管理员调用状态查询接口
|
||||
- **THEN** 返回 `{"is_set": true}`
|
||||
|
||||
#### Scenario: 查询未设置状态
|
||||
|
||||
- **WHEN** 操作密码未设置(Redis 中无对应 key),超级管理员调用状态查询接口
|
||||
- **THEN** 返回 `{"is_set": false}`
|
||||
|
||||
### Requirement: 操作密码验证使用全局操作密码
|
||||
|
||||
系统 SHALL 将所有需要操作密码验证的接口(目前为 `agent-recharges` 线下充值确认)改为验证全局操作密码,而非当前登录用户的登录密码。
|
||||
|
||||
#### Scenario: 操作密码正确时通过验证
|
||||
|
||||
- **WHEN** 调用需要操作密码的接口,传入正确的全局操作密码
|
||||
- **THEN** 验证通过,接口正常执行后续业务逻辑
|
||||
|
||||
#### Scenario: 操作密码错误时拒绝
|
||||
|
||||
- **WHEN** 调用需要操作密码的接口,传入错误的密码
|
||||
- **THEN** 系统返回 400,错误消息"操作密码错误"
|
||||
|
||||
#### Scenario: 操作密码未设置时拒绝
|
||||
|
||||
- **WHEN** Redis 中无操作密码,调用需要操作密码的接口
|
||||
- **THEN** 系统返回 400,错误消息"操作密码未设置,请联系超级管理员"
|
||||
55
openspec/changes/super-admin-operation-password/tasks.md
Normal file
55
openspec/changes/super-admin-operation-password/tasks.md
Normal file
@@ -0,0 +1,55 @@
|
||||
## 1. Redis Key 常量
|
||||
|
||||
- [x] 1.1 在 `pkg/constants/redis.go` 中添加全局操作密码 Redis Key 生成函数 `RedisSystemOperationPasswordKey() string`,返回固定字符串 `"system:operation_password"`
|
||||
- [x] 1.2 运行 `lsp_diagnostics` 确认 `redis.go` 无错误
|
||||
|
||||
## 2. DTO 层
|
||||
|
||||
- [x] 2.1 新建 `internal/model/dto/super_admin_dto.go`,定义以下结构体:
|
||||
- `SetOperationPasswordRequest`:字段 `Password`(`validate:"required,min=6,max=50"`)、`ConfirmPassword`(`validate:"required"`),含 description 标签
|
||||
- `OperationPasswordStatusResponse`:字段 `IsSet bool`,含 description 标签
|
||||
- [x] 2.2 运行 `lsp_diagnostics` 确认 `super_admin_dto.go` 无错误
|
||||
|
||||
## 3. OperationPassword Service
|
||||
|
||||
- [x] 3.1 新建 `internal/service/operation_password/service.go`,实现以下方法:
|
||||
- `Set(ctx, password string) error`:bcrypt 哈希后写入 Redis(永久存储,无 TTL)
|
||||
- `IsSet(ctx) bool`:查询 Redis key 是否存在
|
||||
- `Verify(ctx, inputPassword string) error`:从 Redis 读取哈希值,用 `bcrypt.CompareHashAndPassword` 对比;key 不存在时返回"操作密码未设置,请联系超级管理员",密码错误时返回"操作密码错误"
|
||||
- Service 结构体通过构造函数注入 `*redis.Client`
|
||||
- [x] 3.2 运行 `lsp_diagnostics` 确认 `operation_password/service.go` 无错误
|
||||
|
||||
## 4. SuperAdmin Handler
|
||||
|
||||
- [x] 4.1 新建 `internal/handler/admin/super_admin.go`,实现以下接口:
|
||||
- `SetOperationPassword`(`POST /api/admin/super-admin/operation-password`):检查 `user_type == UserTypeSuperAdmin`,验证两次密码一致,调用 `operationPasswordService.Set()`
|
||||
- `GetOperationPasswordStatus`(`GET /api/admin/super-admin/operation-password/status`):检查 `user_type == UserTypeSuperAdmin`,调用 `IsSet()` 返回状态
|
||||
- 添加 Handler 方法中文注释(含 HTTP 方法和路径)
|
||||
- [x] 4.2 运行 `lsp_diagnostics` 确认 `super_admin.go` 无错误
|
||||
|
||||
## 5. Bootstrap 注册
|
||||
|
||||
- [x] 5.1 在 `internal/bootstrap/services.go` 中注册 `OperationPasswordService`(注入 Redis 客户端)
|
||||
- [x] 5.2 在 `internal/bootstrap/handlers.go` 中注册 `SuperAdminHandler`(注入 `OperationPasswordService`)
|
||||
- [x] 5.3 在路由文件中注册两个新接口(`POST /api/admin/super-admin/operation-password` 和 `GET /api/admin/super-admin/operation-password/status`),路由需经过 admin 认证中间件
|
||||
- [x] 5.4 运行 `lsp_diagnostics` 确认 bootstrap 和路由文件无错误
|
||||
|
||||
## 6. 文档生成器更新
|
||||
|
||||
- [x] 6.1 在 `cmd/api/docs.go` 和 `cmd/gendocs/main.go` 的 `Handlers` 结构体中注册 `SuperAdminHandler`(参考现有 handler 注册方式)
|
||||
|
||||
## 7. 修改 AgentRecharge 操作密码验证逻辑
|
||||
|
||||
- [x] 7.1 在 `internal/service/agent_recharge/service.go` 的 `OfflinePay` 方法中,将操作密码验证从"查询当前用户账号 + bcrypt 对比登录密码"改为"调用 `operationPasswordService.Verify(ctx, req.OperationPassword)`"
|
||||
- [x] 7.2 在 `agent_recharge/service.go` 的 Service 结构体中注入 `operationPasswordService`,在构造函数中传入
|
||||
- [x] 7.3 在 `internal/bootstrap/services.go` 中更新 `AgentRechargeService` 的构造调用,传入 `OperationPasswordService`
|
||||
- [x] 7.4 运行 `lsp_diagnostics` 确认 `agent_recharge/service.go` 和 bootstrap 相关文件无错误
|
||||
|
||||
## 8. 接口验证
|
||||
|
||||
- [ ] 8.1 调用 `GET /api/admin/super-admin/operation-password/status`,验证返回 `{"is_set": false}`(初始状态)
|
||||
- [ ] 8.2 调用 `POST /api/admin/super-admin/operation-password`(传入合法密码),验证返回 200
|
||||
- [ ] 8.3 调用 `GET /api/admin/super-admin/operation-password/status`,验证返回 `{"is_set": true}`
|
||||
- [ ] 8.4 调用 `OfflinePay` 接口传入旧登录密码,验证返回"操作密码错误"(旧密码已失效)
|
||||
- [ ] 8.5 调用 `OfflinePay` 接口传入新设置的操作密码,验证正常通过
|
||||
- [ ] 8.6 使用非超级管理员账号调用设置接口,验证返回 403
|
||||
Reference in New Issue
Block a user