docs: project docs, scripts, deployment configs, and evidence
This commit is contained in:
886
docs/project-management/DESIGN_GAP_FIX_PLAN.md
Normal file
886
docs/project-management/DESIGN_GAP_FIX_PLAN.md
Normal file
@@ -0,0 +1,886 @@
|
||||
# 设计断链修复计划
|
||||
|
||||
**文档版本**: v1.0
|
||||
**编写日期**: 2026-04-01
|
||||
**目的**: 修复当前项目的设计断链问题,确保前后端设计闭环
|
||||
|
||||
---
|
||||
|
||||
## 一、当前设计断链清单
|
||||
|
||||
### 1.1 优先级分类
|
||||
|
||||
**P0 - 严重断链(必须立即修复)**
|
||||
|
||||
| ID | 断链类型 | 功能名称 | 影响 | 修复工作量 |
|
||||
|----|---------|---------|------|----------|
|
||||
| GAP-FE-001 | 前端缺失 | 管理员管理页 | 管理员无法通过后台管理管理员 | 3天 |
|
||||
| GAP-FE-002 | 前端缺失 | 系统设置页 | 系统配置无法管理 | 4天 |
|
||||
| GAP-FE-003 | 前端缺失 | 全局设备管理页 | 设备信息无法全局管理 | 3天 |
|
||||
| GAP-FE-004 | 前端缺失 | 登录日志导出 | 无法导出登录日志 | 1天 |
|
||||
| GAP-BE-001 | 后端缺失 | 系统设置API | 系统设置功能无法实现 | 3天 |
|
||||
| GAP-INT-001 | 接线缺失 | 设备信任检查 | 设备信任功能不生效 | 2天 |
|
||||
| GAP-INT-002 | 接线缺失 | 角色继承权限 | 角色继承功能不生效 | 2天 |
|
||||
|
||||
**P1 - 中等断链(当前Sprint修复)**
|
||||
|
||||
| ID | 断链类型 | 功能名称 | 影响 | 修复工作量 |
|
||||
|----|---------|---------|------|----------|
|
||||
| GAP-FE-005 | 前端缺失 | 批量操作(用户管理) | 批量删除/操作效率低 | 2天 |
|
||||
| GAP-INT-003 | 接线缺失 | 异常检测接入 | 异常检测功能不生效 | 2天 |
|
||||
| GAP-INT-004 | 接线缺失 | 密码历史记录检查 | 密码重复使用防护不生效 | 1天 |
|
||||
|
||||
**P2 - 轻微断链(下一Sprint修复)**
|
||||
|
||||
| ID | 断链类型 | 功能名称 | 影响 | 修复工作量 |
|
||||
|----|---------|---------|------|----------|
|
||||
| GAP-INT-005 | 接线缺失 | IP地理位置解析 | 异地登录检测不精确 | 1天 |
|
||||
| GAP-INT-006 | 接线缺失 | 设备指纹采集 | 设备识别不准确 | 1天 |
|
||||
|
||||
### 1.2 断链分布统计
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 设计断链分布统计 │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 前端缺失: 4个 (管理员管理、系统设置、全局设备管理、导出) │
|
||||
│ 后端缺失: 1个 (系统设置API) │
|
||||
│ 接线缺失: 6个 (设备信任、角色继承、异常检测等) │
|
||||
│ │
|
||||
│ P0断链: 7个 │
|
||||
│ P1断链: 3个 │
|
||||
│ P2断链: 2个 │
|
||||
│ │
|
||||
│ 总修复工作量: 约30天 │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、修复计划
|
||||
|
||||
### 2.1 修复优先级排序
|
||||
|
||||
**Sprint 12 (当前,剩余10天)**
|
||||
|
||||
| ID | 断链名称 | 优先级 | 负责人 | 计划完成 |
|
||||
|----|---------|--------|--------|---------|
|
||||
| GAP-BE-001 | 系统设置API | P0 | 后端A | 04-03 |
|
||||
| GAP-INT-001 | 设备信任检查 | P0 | 后端A | 04-04 |
|
||||
| GAP-INT-002 | 角色继承权限 | P0 | 后端A | 04-05 |
|
||||
| GAP-INT-004 | 密码历史检查 | P1 | 后端A | 04-06 |
|
||||
|
||||
**Sprint 13 (下周,14天)**
|
||||
|
||||
| ID | 断链名称 | 优先级 | 负责人 | 计划完成 |
|
||||
|----|---------|--------|--------|---------|
|
||||
| GAP-FE-001 | 管理员管理页 | P0 | 前端A | 04-08 |
|
||||
| GAP-FE-002 | 系统设置页 | P0 | 前端A | 04-10 |
|
||||
| GAP-FE-003 | 全局设备管理页 | P0 | 前端A | 04-12 |
|
||||
| GAP-FE-004 | 登录日志导出 | P0 | 前端A | 04-13 |
|
||||
| GAP-INT-003 | 异常检测接入 | P1 | 后端A | 04-15 |
|
||||
| GAP-FE-005 | 批量操作 | P1 | 前端A | 04-17 |
|
||||
|
||||
**Sprint 14 (下下周,14天)**
|
||||
|
||||
| ID | 断链名称 | 优先级 | 负责人 | 计划完成 |
|
||||
|----|---------|--------|--------|---------|
|
||||
| GAP-INT-005 | IP地理位置解析 | P2 | 后端A | 04-22 |
|
||||
| GAP-INT-006 | 设备指纹采集 | P2 | 前端A | 04-23 |
|
||||
|
||||
### 2.2 详细修复方案
|
||||
|
||||
#### GAP-BE-001: 系统设置API
|
||||
|
||||
**问题描述**
|
||||
- 前端系统设置页需要后端API支持
|
||||
- 当前后端无系统设置相关接口
|
||||
|
||||
**修复方案**
|
||||
|
||||
**1. 数据库设计**
|
||||
|
||||
```sql
|
||||
-- 系统设置表
|
||||
CREATE TABLE system_configs (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
config_key VARCHAR(100) NOT NULL UNIQUE COMMENT '配置键',
|
||||
config_value TEXT NOT NULL COMMENT '配置值',
|
||||
config_type ENUM('string', 'number', 'boolean', 'json') NOT NULL DEFAULT 'string' COMMENT '配置类型',
|
||||
category VARCHAR(50) NOT NULL COMMENT '配置分类',
|
||||
description VARCHAR(255) COMMENT '配置描述',
|
||||
is_public TINYINT(1) DEFAULT 0 COMMENT '是否公开(前端可见)',
|
||||
is_editable TINYINT(1) DEFAULT 1 COMMENT '是否可编辑',
|
||||
default_value TEXT COMMENT '默认值',
|
||||
validation_rule VARCHAR(255) COMMENT '验证规则',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
created_by BIGINT COMMENT '创建人ID',
|
||||
updated_by BIGINT COMMENT '更新人ID',
|
||||
INDEX idx_category (category),
|
||||
INDEX idx_key (config_key)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统配置表';
|
||||
|
||||
-- 初始化默认配置
|
||||
INSERT INTO system_configs (config_key, config_value, config_type, category, description, is_public, is_editable, default_value) VALUES
|
||||
('system.name', 'UMS', 'string', 'system', '系统名称', 1, 1, 'UMS'),
|
||||
('system.logo_url', '', 'string', 'system', '系统Logo URL', 1, 1, ''),
|
||||
('system.timezone', 'Asia/Shanghai', 'string', 'system', '系统时区', 1, 1, 'Asia/Shanghai'),
|
||||
('system.language', 'zh-CN', 'string', 'system', '系统语言', 1, 1, 'zh-CN'),
|
||||
('auth.password_min_length', '8', 'number', 'auth', '密码最小长度', 1, 1, '8'),
|
||||
('auth.password_max_age_days', '90', 'number', 'auth', '密码有效期(天)', 1, 1, '90'),
|
||||
('auth.session_timeout_minutes', '30', 'number', 'auth', '会话超时时间(分钟)', 1, 1, '30'),
|
||||
('auth.enable_totp', 'true', 'boolean', 'auth', '启用双因素认证', 1, 1, 'true'),
|
||||
('auth.enable_device_trust', 'true', 'boolean', 'auth', '启用设备信任', 1, 1, 'true'),
|
||||
('auth.device_trust_duration_days', '30', 'number', 'auth', '设备信任有效期(天)', 1, 1, '30'),
|
||||
('notification.email_enabled', 'true', 'boolean', 'notification', '启用邮件通知', 1, 1, 'true'),
|
||||
('notification.sms_enabled', 'false', 'boolean', 'notification', '启用短信通知', 1, 1, 'false'),
|
||||
('security.max_login_attempts', '5', 'number', 'security', '最大登录尝试次数', 1, 1, '5'),
|
||||
('security.login_lockout_minutes', '30', 'number', 'security', '登录锁定时间(分钟)', 1, 1, '30'),
|
||||
('logging.log_level', 'info', 'string', 'logging', '日志级别', 1, 1, 'info'),
|
||||
('logging.log_retention_days', '30', 'number', 'logging', '日志保留天数', 1, 1, '30');
|
||||
```
|
||||
|
||||
**2. Domain模型**
|
||||
|
||||
```go
|
||||
// internal/domain/system_config.go
|
||||
package domain
|
||||
|
||||
import "time"
|
||||
|
||||
type SystemConfig struct {
|
||||
ID int64 `json:"id"`
|
||||
ConfigKey string `json:"config_key"`
|
||||
ConfigValue string `json:"config_value"`
|
||||
ConfigType string `json:"config_type"` // string, number, boolean, json
|
||||
Category string `json:"category"`
|
||||
Description string `json:"description"`
|
||||
IsPublic bool `json:"is_public"`
|
||||
IsEditable bool `json:"is_editable"`
|
||||
DefaultValue string `json:"default_value"`
|
||||
ValidationRule string `json:"validation_rule"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
CreatedBy *int64 `json:"created_by,omitempty"`
|
||||
UpdatedBy *int64 `json:"updated_by,omitempty"`
|
||||
}
|
||||
|
||||
type SystemConfigUpdate struct {
|
||||
ConfigValue string `json:"config_value"`
|
||||
UpdatedBy int64 `json:"updated_by"`
|
||||
}
|
||||
```
|
||||
|
||||
**3. Repository层**
|
||||
|
||||
```go
|
||||
// internal/repository/system_config.go
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"d:/project/internal/domain"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type SystemConfigRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewSystemConfigRepository(db *gorm.DB) *SystemConfigRepository {
|
||||
return &SystemConfigRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *SystemConfigRepository) GetByCategory(ctx context.Context, category string) ([]*domain.SystemConfig, error) {
|
||||
var configs []*domain.SystemConfig
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("category = ?", category).
|
||||
Find(&configs).Error
|
||||
return configs, err
|
||||
}
|
||||
|
||||
func (r *SystemConfigRepository) GetByKey(ctx context.Context, key string) (*domain.SystemConfig, error) {
|
||||
var config domain.SystemConfig
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("config_key = ?", key).
|
||||
First(&config).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
func (r *SystemConfigRepository) GetAllPublic(ctx context.Context) ([]*domain.SystemConfig, error) {
|
||||
var configs []*domain.SystemConfig
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("is_public = ?", true).
|
||||
Order("category, config_key").
|
||||
Find(&configs).Error
|
||||
return configs, err
|
||||
}
|
||||
|
||||
func (r *SystemConfigRepository) GetAll(ctx context.Context) ([]*domain.SystemConfig, error) {
|
||||
var configs []*domain.SystemConfig
|
||||
err := r.db.WithContext(ctx).
|
||||
Order("category, config_key").
|
||||
Find(&configs).Error
|
||||
return configs, err
|
||||
}
|
||||
|
||||
func (r *SystemConfigRepository) Update(ctx context.Context, key string, update *domain.SystemConfigUpdate) error {
|
||||
return r.db.WithContext(ctx).
|
||||
Model(&domain.SystemConfig{}).
|
||||
Where("config_key = ? AND is_editable = ?", key, true).
|
||||
Updates(map[string]interface{}{
|
||||
"config_value": update.ConfigValue,
|
||||
"updated_by": update.UpdatedBy,
|
||||
"updated_at": "NOW()",
|
||||
}).Error
|
||||
}
|
||||
|
||||
func (r *SystemConfigRepository) GetByKeys(ctx context.Context, keys []string) (map[string]*domain.SystemConfig, error) {
|
||||
var configs []*domain.SystemConfig
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("config_key IN ?", keys).
|
||||
Find(&configs).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make(map[string]*domain.SystemConfig)
|
||||
for _, config := range configs {
|
||||
result[config.ConfigKey] = config
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
```
|
||||
|
||||
**4. Service层**
|
||||
|
||||
```go
|
||||
// internal/service/system_config.go
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"d:/project/internal/domain"
|
||||
"d:/project/internal/repository"
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type SystemConfigService struct {
|
||||
configRepo *repository.SystemConfigRepository
|
||||
}
|
||||
|
||||
func NewSystemConfigService(configRepo *repository.SystemConfigRepository) *SystemConfigService {
|
||||
return &SystemConfigService{
|
||||
configRepo: configRepo,
|
||||
}
|
||||
}
|
||||
|
||||
type ConfigCategory struct {
|
||||
Name string `json:"name"`
|
||||
Label string `json:"label"`
|
||||
Configs []*domain.SystemConfig `json:"configs"`
|
||||
}
|
||||
|
||||
func (s *SystemConfigService) GetPublicConfigs(ctx context.Context) ([]*ConfigCategory, error) {
|
||||
configs, err := s.configRepo.GetAllPublic(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.groupByCategory(configs), nil
|
||||
}
|
||||
|
||||
func (s *SystemConfigService) GetAllConfigs(ctx context.Context) ([]*ConfigCategory, error) {
|
||||
configs, err := s.configRepo.GetAll(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.groupByCategory(configs), nil
|
||||
}
|
||||
|
||||
func (s *SystemConfigService) GetConfig(ctx context.Context, key string) (*domain.SystemConfig, error) {
|
||||
return s.configRepo.GetByKey(ctx, key)
|
||||
}
|
||||
|
||||
func (s *SystemConfigService) GetConfigsByKeys(ctx context.Context, keys []string) (map[string]*domain.SystemConfig, error) {
|
||||
return s.configRepo.GetByKeys(ctx, keys)
|
||||
}
|
||||
|
||||
func (s *SystemConfigService) UpdateConfig(ctx context.Context, key string, value string, userID int64) error {
|
||||
config, err := s.configRepo.GetByKey(ctx, key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("配置不存在: %s", key)
|
||||
}
|
||||
|
||||
if !config.IsEditable {
|
||||
return fmt.Errorf("配置不允许编辑: %s", key)
|
||||
}
|
||||
|
||||
// 验证配置值
|
||||
if err := s.validateConfigValue(config, value); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
update := &domain.SystemConfigUpdate{
|
||||
ConfigValue: value,
|
||||
UpdatedBy: userID,
|
||||
}
|
||||
|
||||
return s.configRepo.Update(ctx, key, update)
|
||||
}
|
||||
|
||||
func (s *SystemConfigService) GetConfigValue(ctx context.Context, key string, defaultValue interface{}) (interface{}, error) {
|
||||
config, err := s.configRepo.GetByKey(ctx, key)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return defaultValue, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.parseConfigValue(config), nil
|
||||
}
|
||||
|
||||
func (s *SystemConfigService) GetString(ctx context.Context, key string, defaultValue string) (string, error) {
|
||||
value, err := s.GetConfigValue(ctx, key, defaultValue)
|
||||
if err != nil {
|
||||
return defaultValue, err
|
||||
}
|
||||
|
||||
if str, ok := value.(string); ok {
|
||||
return str, nil
|
||||
}
|
||||
return defaultValue, nil
|
||||
}
|
||||
|
||||
func (s *SystemConfigService) GetInt(ctx context.Context, key string, defaultValue int) (int, error) {
|
||||
value, err := s.GetConfigValue(ctx, key, defaultValue)
|
||||
if err != nil {
|
||||
return defaultValue, err
|
||||
}
|
||||
|
||||
if num, ok := value.(int); ok {
|
||||
return num, nil
|
||||
}
|
||||
return defaultValue, nil
|
||||
}
|
||||
|
||||
func (s *SystemConfigService) GetBool(ctx context.Context, key string, defaultValue bool) (bool, error) {
|
||||
value, err := s.GetConfigValue(ctx, key, defaultValue)
|
||||
if err != nil {
|
||||
return defaultValue, err
|
||||
}
|
||||
|
||||
if b, ok := value.(bool); ok {
|
||||
return b, nil
|
||||
}
|
||||
return defaultValue, nil
|
||||
}
|
||||
|
||||
// 辅助方法
|
||||
|
||||
func (s *SystemConfigService) groupByCategory(configs []*domain.SystemConfig) []*ConfigCategory {
|
||||
categoryMap := make(map[string]*ConfigCategory)
|
||||
categoryLabels := map[string]string{
|
||||
"system": "系统设置",
|
||||
"auth": "认证设置",
|
||||
"notification": "通知设置",
|
||||
"security": "安全设置",
|
||||
"logging": "日志设置",
|
||||
}
|
||||
|
||||
for _, config := range configs {
|
||||
if _, exists := categoryMap[config.Category]; !exists {
|
||||
categoryMap[config.Category] = &ConfigCategory{
|
||||
Name: config.Category,
|
||||
Label: categoryLabels[config.Category],
|
||||
}
|
||||
}
|
||||
categoryMap[config.Category].Configs = append(categoryMap[config.Category].Configs, config)
|
||||
}
|
||||
|
||||
var result []*ConfigCategory
|
||||
for _, category := range categoryMap {
|
||||
result = append(result, category)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *SystemConfigService) validateConfigValue(config *domain.SystemConfig, value string) error {
|
||||
switch config.ConfigType {
|
||||
case "number":
|
||||
_, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("配置值必须是数字: %s", value)
|
||||
}
|
||||
case "boolean":
|
||||
if value != "true" && value != "false" {
|
||||
return fmt.Errorf("配置值必须是 true 或 false: %s", value)
|
||||
}
|
||||
case "json":
|
||||
var js interface{}
|
||||
if err := json.Unmarshal([]byte(value), &js); err != nil {
|
||||
return fmt.Errorf("配置值必须是有效的JSON: %s", value)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: 实现 validation_rule 的验证逻辑
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SystemConfigService) parseConfigValue(config *domain.SystemConfig) interface{} {
|
||||
switch config.ConfigType {
|
||||
case "number":
|
||||
if num, err := strconv.Atoi(config.ConfigValue); err == nil {
|
||||
return num
|
||||
}
|
||||
case "boolean":
|
||||
return config.ConfigValue == "true"
|
||||
case "json":
|
||||
var js interface{}
|
||||
if err := json.Unmarshal([]byte(config.ConfigValue), &js); err == nil {
|
||||
return js
|
||||
}
|
||||
}
|
||||
|
||||
return config.ConfigValue
|
||||
}
|
||||
```
|
||||
|
||||
**5. Handler层**
|
||||
|
||||
```go
|
||||
// internal/api/handler/system_config_handler.go
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"d:/project/internal/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type SystemConfigHandler struct {
|
||||
configService *service.SystemConfigService
|
||||
}
|
||||
|
||||
func NewSystemConfigHandler(configService *service.SystemConfigService) *SystemConfigHandler {
|
||||
return &SystemConfigHandler{
|
||||
configService: configService,
|
||||
}
|
||||
}
|
||||
|
||||
type UpdateConfigRequest struct {
|
||||
ConfigValue string `json:"config_value" binding:"required"`
|
||||
}
|
||||
|
||||
// GetPublicConfigs 获取公开配置(前端可见)
|
||||
func (h *SystemConfigHandler) GetPublicConfigs(c *gin.Context) {
|
||||
configs, err := h.configService.GetPublicConfigs(c.Request.Context())
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"data": configs,
|
||||
})
|
||||
}
|
||||
|
||||
// GetAllConfigs 获取所有配置(管理员)
|
||||
func (h *SystemConfigHandler) GetAllConfigs(c *gin.Context) {
|
||||
configs, err := h.configService.GetAllConfigs(c.Request.Context())
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"data": configs,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateConfig 更新配置
|
||||
func (h *SystemConfigHandler) UpdateConfig(c *gin.Context) {
|
||||
key := c.Param("key")
|
||||
|
||||
var req UpdateConfigRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 从上下文获取用户ID
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "未认证"})
|
||||
return
|
||||
}
|
||||
|
||||
userIDInt, ok := userID.(int64)
|
||||
if !ok {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "用户ID类型错误"})
|
||||
return
|
||||
}
|
||||
|
||||
err := h.configService.UpdateConfig(c.Request.Context(), key, req.ConfigValue, userIDInt)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "配置更新成功",
|
||||
})
|
||||
}
|
||||
|
||||
// GetConfig 获取单个配置
|
||||
func (h *SystemConfigHandler) GetConfig(c *gin.Context) {
|
||||
key := c.Param("key")
|
||||
|
||||
config, err := h.configService.GetConfig(c.Request.Context(), key)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "配置不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"data": config,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
**6. 路由注册**
|
||||
|
||||
```go
|
||||
// internal/api/router/router.go
|
||||
// 添加系统设置路由
|
||||
configGroup := apiV1.Group("/system")
|
||||
configGroup.Use(middleware.RequireAuth())
|
||||
configGroup.Use(middleware.RequireAdmin()) // 管理员权限
|
||||
|
||||
systemConfigHandler := handler.NewSystemConfigHandler(systemConfigService)
|
||||
|
||||
configGroup.GET("/configs/public", systemConfigHandler.GetPublicConfigs)
|
||||
configGroup.GET("/configs", systemConfigHandler.GetAllConfigs)
|
||||
configGroup.GET("/configs/:key", systemConfigHandler.GetConfig)
|
||||
configGroup.PUT("/configs/:key", systemConfigHandler.UpdateConfig)
|
||||
```
|
||||
|
||||
**7. 启动注入**
|
||||
|
||||
```go
|
||||
// cmd/server/main.go
|
||||
// 初始化系统配置
|
||||
systemConfigRepo := repository.NewSystemConfigRepository(db.DB)
|
||||
systemConfigService := service.NewSystemConfigService(systemConfigRepo)
|
||||
|
||||
// 注入到其他需要使用配置的服务中
|
||||
// authService.SetConfigService(systemConfigService)
|
||||
```
|
||||
|
||||
**验收标准**
|
||||
- [ ] 数据库表创建成功
|
||||
- [ ] 默认配置初始化成功
|
||||
- [ ] GET /api/v1/system/configs/public 返回公开配置
|
||||
- [ ] GET /api/v1/system/configs 返回所有配置(需管理员权限)
|
||||
- [ ] PUT /api/v1/system/configs/:key 更新配置成功
|
||||
- [ ] 非可编辑配置不允许更新
|
||||
- [ ] 单元测试覆盖率达到80%
|
||||
- [ ] API文档完整
|
||||
|
||||
---
|
||||
|
||||
#### GAP-INT-001: 设备信任检查接线
|
||||
|
||||
**问题描述**
|
||||
- 设备信任的CRUD API已实现,但登录流程未使用
|
||||
- 用户信任设备后,登录时仍要求2FA验证
|
||||
|
||||
**修复方案**
|
||||
|
||||
**1. 修改登录请求结构**
|
||||
|
||||
```go
|
||||
// internal/service/auth.go
|
||||
type LoginRequest struct {
|
||||
Account string `json:"account" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
Remember bool `json:"remember"`
|
||||
DeviceID string `json:"device_id,omitempty"`
|
||||
DeviceName string `json:"device_name,omitempty"`
|
||||
DeviceOS string `json:"device_os,omitempty"`
|
||||
DeviceBrowser string `json:"device_browser,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
**2. 登录时自动注册设备**
|
||||
|
||||
```go
|
||||
// internal/service/auth.go
|
||||
func (s *AuthService) generateLoginResponse(ctx context.Context, user *domain.User, req *LoginRequest) (*LoginResponse, error) {
|
||||
// ... token生成逻辑 ...
|
||||
|
||||
// 自动注册/更新设备记录
|
||||
if s.deviceRepo != nil && req.DeviceID != "" {
|
||||
s.bestEffortRegisterDevice(ctx, user.ID, req)
|
||||
}
|
||||
|
||||
// ... 返回逻辑 ...
|
||||
}
|
||||
|
||||
func (s *AuthService) bestEffortRegisterDevice(ctx context.Context, userID int64, req *LoginRequest) {
|
||||
device := &domain.Device{
|
||||
UserID: userID,
|
||||
DeviceID: req.DeviceID,
|
||||
DeviceName: req.DeviceName,
|
||||
OS: req.DeviceOS,
|
||||
Browser: req.DeviceBrowser,
|
||||
LastSeenAt: time.Now(),
|
||||
IsTrusted: false,
|
||||
TrustExpiresAt: nil,
|
||||
}
|
||||
|
||||
// 尝试获取现有设备
|
||||
existing, err := s.deviceRepo.GetByDeviceID(ctx, userID, req.DeviceID)
|
||||
if err == nil && existing != nil {
|
||||
// 更新设备信息
|
||||
existing.DeviceName = req.DeviceName
|
||||
existing.OS = req.DeviceOS
|
||||
existing.Browser = req.DeviceBrowser
|
||||
existing.LastSeenAt = time.Now()
|
||||
s.deviceRepo.Update(ctx, existing)
|
||||
} else {
|
||||
// 创建新设备
|
||||
s.deviceRepo.Create(ctx, device)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**3. 2FA验证时检查设备信任**
|
||||
|
||||
```go
|
||||
// internal/service/auth.go
|
||||
func (s *AuthService) VerifyTOTP(ctx context.Context, req *VerifyTOTPRequest) error {
|
||||
// 检查设备是否已信任
|
||||
if req.DeviceID != "" && s.deviceRepo != nil {
|
||||
device, err := s.deviceRepo.GetByDeviceID(ctx, req.UserID, req.DeviceID)
|
||||
if err == nil && device != nil && device.IsTrusted {
|
||||
// 检查信任是否过期
|
||||
if device.TrustExpiresAt == nil || device.TrustExpiresAt.After(time.Now()) {
|
||||
// 设备已信任且未过期,跳过2FA验证
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 正常TOTP验证流程
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**验收标准**
|
||||
- [ ] 登录时自动创建/更新设备记录
|
||||
- [ ] 设备ID从登录请求中正确提取
|
||||
- [ ] 信任设备的2FA验证被跳过
|
||||
- [ ] 信任过期后重新要求2FA
|
||||
- [ ] 单元测试覆盖
|
||||
|
||||
---
|
||||
|
||||
#### GAP-INT-002: 角色继承权限接线
|
||||
|
||||
**问题描述**
|
||||
- 角色继承的Repository和Service已实现
|
||||
- 但auth middleware未使用继承权限
|
||||
|
||||
**修复方案**
|
||||
|
||||
**1. 修改auth middleware**
|
||||
|
||||
```go
|
||||
// internal/api/middleware/auth.go
|
||||
func (m *AuthMiddleware) getUserPermissions(ctx context.Context, userID int64) ([]string, error) {
|
||||
// 现状: 直接查询user_role_permissions表
|
||||
|
||||
// 修改: 调用roleService.GetRolePermissions(含继承)
|
||||
userRoles, err := m.userRepo.GetRoles(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var allPermissions []string
|
||||
seen := make(map[string]bool)
|
||||
|
||||
for _, role := range userRoles {
|
||||
permissions, err := m.roleService.GetRolePermissions(ctx, role.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, perm := range permissions {
|
||||
if !seen[perm.Code] {
|
||||
seen[perm.Code] = true
|
||||
allPermissions = append(allPermissions, perm.Code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return allPermissions, nil
|
||||
}
|
||||
```
|
||||
|
||||
**2. 修改JWT生成逻辑**
|
||||
|
||||
```go
|
||||
// internal/service/auth.go
|
||||
func (s *AuthService) generateLoginResponse(ctx context.Context, user *domain.User, req *LoginRequest) (*LoginResponse, error) {
|
||||
// ...
|
||||
|
||||
// 获取用户权限(含继承)
|
||||
permissions, err := s.getUserPermissions(ctx, user.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 生成JWT时包含继承权限
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"user_id": user.ID,
|
||||
"username": user.Username,
|
||||
"email": user.Email,
|
||||
"role_ids": user.RoleIDs,
|
||||
"permissions": permissions,
|
||||
"exp": time.Now().Add(tokenExpiry).Unix(),
|
||||
"iat": time.Now().Unix(),
|
||||
})
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**验收标准**
|
||||
- [ ] 用户持子角色能访问父角色权限
|
||||
- [ ] JWT中的permissions包含继承权限
|
||||
- [ ] auth middleware正确验证继承权限
|
||||
- [ ] 单元测试覆盖角色继承场景
|
||||
|
||||
---
|
||||
|
||||
### 2.3 前端断链修复(Sprint 13)
|
||||
|
||||
**将在专家评审后详细设计前端页面**
|
||||
|
||||
---
|
||||
|
||||
## 三、修复验收标准
|
||||
|
||||
### 3.1 通用验收标准
|
||||
|
||||
- [ ] 代码审查通过
|
||||
- [ ] 单元测试覆盖率 > 80%
|
||||
- [ ] 集成测试通过
|
||||
- [ ] API文档完整
|
||||
- [ ] 无已知安全漏洞
|
||||
- [ ] 性能测试达标
|
||||
|
||||
### 3.2 特定验收标准
|
||||
|
||||
**后端API修复**
|
||||
- [ ] API符合RESTful规范
|
||||
- [ ] 错误处理完整
|
||||
- [ ] 输入验证完整
|
||||
- [ ] 权限校验完整
|
||||
- [ ] 数据库索引合理
|
||||
|
||||
**前端页面修复**
|
||||
- [ ] UI/UX符合设计规范
|
||||
- [ ] 交互流程顺畅
|
||||
- [ ] 错误提示友好
|
||||
- [ ] 加载状态清晰
|
||||
- [ ] 响应式设计良好
|
||||
|
||||
---
|
||||
|
||||
## 四、风险与依赖
|
||||
|
||||
### 4.1 技术风险
|
||||
|
||||
| 风险 | 影响 | 概率 | 缓解措施 |
|
||||
|------|------|------|---------|
|
||||
| 系统设置API影响范围大 | 高 | 中 | 分阶段发布,先灰度 |
|
||||
| 设备信任逻辑复杂 | 中 | 高 | 充分测试,回滚方案 |
|
||||
| 角色继承权限链路长 | 中 | 中 | 详细测试,代码审查 |
|
||||
|
||||
### 4.2 资源风险
|
||||
|
||||
| 风险 | 影响 | 概率 | 缓解措施 |
|
||||
|------|------|------|---------|
|
||||
| 前端开发资源紧张 | 高 | 中 | 优先P0功能,延期P2 |
|
||||
| 测试资源不足 | 中 | 中 | 自动化测试,专家评审 |
|
||||
|
||||
### 4.3 依赖项
|
||||
|
||||
- [ ] Sprint 12后端开发需要2名后端工程师
|
||||
- [ ] Sprint 13前端开发需要1名前端工程师
|
||||
- [ ] 专家评审需要各领域专家参与
|
||||
|
||||
---
|
||||
|
||||
## 五、进度跟踪
|
||||
|
||||
### 5.1 每日跟踪
|
||||
|
||||
```markdown
|
||||
## 设计断链修复进度 - 2026-04-01
|
||||
|
||||
### 今日完成
|
||||
- [x] 完成系统设置API设计
|
||||
- [x] 完成设备信任检查设计
|
||||
|
||||
### 今日进行中
|
||||
- [ ] 系统设置API实现 (30%)
|
||||
- [ ] 角色继承权限修复 (20%)
|
||||
|
||||
### 今日计划
|
||||
- [ ] 完成系统设置API Repository层
|
||||
- [ ] 完成设备信任检查Handler层
|
||||
- [ ] 开始角色继承权限修复
|
||||
|
||||
### 阻碍
|
||||
- 无
|
||||
|
||||
### 明日计划
|
||||
- [ ] 完成系统设置API Service层
|
||||
- [ ] 完成系统设置API Handler层
|
||||
- [ ] 完成角色继承权限修复
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、总结
|
||||
|
||||
本修复计划旨在:
|
||||
1. ✅ **消除设计断链**: 修复12个设计断链问题
|
||||
2. ✅ **确保功能完整**: 补齐缺失的页面和API
|
||||
3. ✅ **提升用户体验**: 实现完整的设备信任和角色继承
|
||||
4. ✅ **提高系统安全性**: 加强密码和登录安全
|
||||
|
||||
预期成果:
|
||||
- 设计断链修复率: 100%
|
||||
- 功能完整性: 95%+
|
||||
- 用户满意度提升: 30%
|
||||
|
||||
---
|
||||
|
||||
*本文档由高级项目经理 Agent 编制,2026-04-01*
|
||||
Reference in New Issue
Block a user