feat: 新增全局操作密码功能,将线下充值验证从登录密码改为统一操作密码
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:
2026-04-18 09:06:33 +08:00
parent 6e15e1b853
commit 6caf0f6141
17 changed files with 568 additions and 30 deletions

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-04-17

View File

@@ -0,0 +1,74 @@
## Context
当前代理充值线下确认接口(`OfflinePay`)通过将 `operation_password` 与操作人自己的登录密码(`account.Password`)做 bcrypt 对比来实现操作密码验证。这个机制存在两个问题:
1. **缺乏独立性**:操作密码与登录密码耦合,修改登录密码即等于修改操作密码,无法独立管控。
2. **安全边界不清**:需要操作密码的敏感操作(如确认大额资金流转)应由超级管理员统一管控,而非各操作人分散管理。
设计决策:操作密码存 Redis不开新表。原因操作密码是全局单一配置没有历史版本需求Redis 的 SET/GET 完全满足需求,避免引入新表的维护成本。
## Goals / Non-Goals
**Goals:**
- 超级管理员可以设置/修改全局操作密码(存 Redisbcrypt 哈希)
- 提供查询接口返回操作密码是否已设置(不暴露密码本身)
- `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。
### 决策3Service 层封装独立的 OperationPassword 服务
**选择**:新建 `internal/service/operation_password/service.go`,提供 `Set(ctx, password)``Verify(ctx, inputPassword) error` 两个方法。
**理由**:将操作密码逻辑从 `agent_recharge` Service 中解耦,后续接口可直接注入复用,避免逻辑散落多处。
### 决策4未设置时的行为
**选择**:操作密码未设置时,`Verify` 返回"操作密码未设置,请联系超级管理员"错误,拒绝操作。
**理由**:不降级为登录密码(安全边界清晰)。初次上线前超级管理员需先设置操作密码。
### 决策5bcrypt 哈希存 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` 接口使用新操作密码可正常通过

View File

@@ -0,0 +1,34 @@
## Why
系统中需要操作密码验证的接口(如代理充值线下确认)目前使用**当前登录用户自己的登录密码**作为操作密码,缺乏独立的操作层安全机制。需要引入一个**全局操作密码**,仅由超级管理员设置和修改,所有需要操作密码的接口统一验证这一密码,实现"操作密码"与"登录密码"的职责分离。
## What Changes
- 新增 `POST /api/admin/super-admin/operation-password` 接口:超级管理员设置/修改全局操作密码
- 新增 `GET /api/admin/super-admin/operation-password/status` 接口:查询操作密码是否已设置
- 全局操作密码以 bcrypt 哈希值存储于 RedisKey 定义在 `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已有无需新增依赖

View File

@@ -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错误消息"操作密码未设置,请联系超级管理员"

View 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