refactor(auth): 优化设备登录逻辑,移除冗余代码并添加设备缓存
Build docker and publish / build (20.15.1) (push) Failing after 6m39s

feat(database): 添加用户算法和盐字段的迁移脚本

fix(subscribe): 修复服务器用户列表缓存问题,临时禁用缓存

style(model): 清理用户模型注释,简化代码结构

chore: 删除无用脚本和测试文件

docs: 添加用户绑定流程文档

perf(login): 优化设备登录性能,添加设备缓存键

fix(unbind): 修复设备解绑时的缓存清理逻辑

refactor(verify): 简化邮箱验证逻辑,移除冗余代码

build(docker): 更新Dockerfile配置,使用scratch基础镜像
This commit is contained in:
2025-10-28 20:46:21 -07:00
parent e6bd78aa76
commit 0f38b3fcd3
33 changed files with 226 additions and 1605 deletions
-149
View File
@@ -1,149 +0,0 @@
# 测试数据清理脚本
## 问题背景
在运行测试案例 `test/device.go` 时,可能会遇到以下错误:
```
[ERROR] 2025/01/28 11:40:59 [DeviceLoginLogic] FindOne Error: record not found
```
这个错误通常是由于测试数据清理不完整导致的,具体表现为:
- 设备记录存在,但对应的用户记录已被删除
- 数据库中存在孤立的记录
- 测试用户数据没有完全清理
## 解决方案
使用 `cleanup_test_data.go` 脚本来清理测试数据,确保数据一致性。
## 使用方法
### 1. 配置脚本
编辑 `cleanup_test_data.go` 文件,修改以下配置:
```go
const (
// 服务器地址
baseURL = "http://localhost:8080"
// 管理员认证信息
adminEmail = "admin@example.com"
adminPassword = "admin123"
// 测试用户邮箱前缀
testEmailPrefix = "test_"
)
```
### 2. 运行清理脚本
```bash
# 进入scripts目录
cd scripts
# 查看帮助信息
go run cleanup_test_data.go --help
# 运行清理脚本
go run cleanup_test_data.go
```
### 3. 脚本功能
脚本会自动执行以下操作:
1. **管理员登录**: 使用配置的管理员账户登录系统
2. **识别测试用户**: 根据邮箱规则识别测试用户:
- 邮箱以 `test_` 开头
- 邮箱包含 `test` 关键字
- 邮箱包含 `example.com` 域名
3. **批量删除**: 使用管理员API批量删除测试用户
4. **数据一致性检查**: 检查剩余用户数据的完整性
## API接口说明
脚本使用以下管理员API接口:
| 接口 | 方法 | 说明 |
|------|------|------|
| `/v1/auth/login` | POST | 管理员登录 |
| `/v1/admin/user` | GET | 获取用户列表 |
| `/v1/admin/user/batch` | DELETE | 批量删除用户 |
| `/v1/admin/user/{id}` | GET | 获取用户详情 |
## 清理规则
### 用户删除规则
脚本会删除符合以下条件的用户:
```go
isTestUser := strings.HasPrefix(user.Email, testEmailPrefix) ||
strings.Contains(user.Email, "test") ||
strings.Contains(user.Email, "example.com")
```
### 数据清理范围
根据 `internal/model/user/default.go` 中的 `Delete` 方法,删除用户时会自动清理:
- 用户基本信息 (`User` 表)
- 用户认证方式 (`AuthMethods` 表)
- 用户订阅信息 (`Subscribe` 表)
- 用户设备信息 (`Device` 表)
- 相关缓存数据
## 安全注意事项
⚠️ **重要提醒**
1. **仅在测试环境使用**: 此脚本会删除用户数据,请勿在生产环境运行
2. **备份数据**: 运行前建议备份数据库
3. **确认配置**: 确保管理员账户信息正确
4. **检查规则**: 确认测试用户识别规则符合预期
## 故障排除
### 常见错误
1. **登录失败**
```
❌ 管理员登录失败: 登录失败 (Code: 401): Invalid credentials
```
- 检查管理员邮箱和密码是否正确
- 确认服务器是否正在运行
2. **连接失败**
```
❌ 管理员登录失败: 登录请求失败: dial tcp [::1]:8080: connect: connection refused
```
- 检查服务器地址是否正确
- 确认服务器是否启动
3. **权限不足**
```
❌ 获取用户列表失败: 获取用户列表失败 (Code: 403): Forbidden
```
- 确认使用的是管理员账户
- 检查管理员权限配置
### 验证清理效果
清理完成后,可以重新运行测试脚本验证:
```bash
cd test
go run device.go
```
如果清理成功,应该不再出现 "record not found" 错误。
## 相关文件
- `test/device.go` - 测试脚本
- `internal/logic/admin/user/deleteUserLogic.go` - 单个用户删除逻辑
- `internal/logic/admin/user/batchDeleteUserLogic.go` - 批量用户删除逻辑
- `internal/model/user/default.go` - 用户模型删除方法
- `apis/admin/user.api` - 管理员用户API定义
-339
View File
@@ -1,339 +0,0 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
)
const (
// 服务器地址 - 根据实际情况修改
baseURL = "http://localhost:8080"
// 管理员认证信息 - 需要根据实际情况修改
adminEmail = "admin@example.com"
adminPassword = "admin123"
// 测试用户邮箱前缀,用于识别测试数据
testEmailPrefix = "test_"
)
// 管理员登录响应
type AdminLoginResponse struct {
Code int `json:"code"`
Data struct {
Token string `json:"token"`
} `json:"data"`
Message string `json:"message"`
}
// 用户列表响应
type UserListResponse struct {
Code int `json:"code"`
Data struct {
List []struct {
ID int64 `json:"id"`
Email string `json:"email"`
} `json:"list"`
Total int64 `json:"total"`
} `json:"data"`
Message string `json:"message"`
}
// 通用响应结构
type CommonResponse struct {
Code int `json:"code"`
Message string `json:"message"`
}
// HTTP客户端
var client = &http.Client{
Timeout: 30 * time.Second,
}
// 管理员token
var adminToken string
func main() {
// 检查是否显示帮助信息
if len(os.Args) > 1 && os.Args[1] == "--help" {
showHelp()
return
}
fmt.Println("=== 测试数据清理脚本 ===")
fmt.Printf("服务器地址: %s\n", baseURL)
fmt.Printf("管理员邮箱: %s\n", adminEmail)
fmt.Println()
// 1. 管理员登录获取token
fmt.Println("步骤 1: 管理员登录...")
if err := adminLogin(); err != nil {
fmt.Printf("❌ 管理员登录失败: %v\n", err)
fmt.Println("\n请检查:")
fmt.Println("- 服务器是否正在运行")
fmt.Println("- 管理员邮箱和密码是否正确")
fmt.Println("- 服务器地址是否正确")
return
}
fmt.Println("✅ 管理员登录成功")
// 2. 清理测试用户数据
fmt.Println("\n步骤 2: 清理测试用户...")
if err := cleanupTestUsers(); err != nil {
fmt.Printf("❌ 清理测试用户失败: %v\n", err)
}
// 3. 清理孤立的设备数据(如果有相应的API)
fmt.Println("\n步骤 3: 检查数据一致性...")
checkDataConsistency()
fmt.Println("\n=== 清理完成 ===")
}
// 显示帮助信息
func showHelp() {
fmt.Println(`测试数据清理脚本使用说明:
📋 功能说明:
本脚本用于清理测试过程中产生的用户数据,解决数据不一致问题
🔧 配置修改:
请在脚本中修改以下配置:
- baseURL: 服务器地址 (当前: http://localhost:8080)
- adminEmail: 管理员邮箱
- adminPassword: 管理员密码
- testEmailPrefix: 测试用户邮箱前缀 (当前: test_)
🚀 运行方式:
go run cleanup_test_data.go
📊 清理规则:
- 删除邮箱包含 "test" 的用户
- 删除邮箱包含 "example.com" 的用户
- 删除邮箱以 "test_" 开头的用户
- 检查数据一致性问题
🔗 使用的API接口:
- POST /v1/auth/login - 管理员登录
- GET /v1/admin/user - 获取用户列表
- DELETE /v1/admin/user/batch - 批量删除用户
- GET /v1/admin/user/{id} - 检查用户详情
⚠️ 安全提示:
- 仅在测试环境中使用
- 生产环境请谨慎操作
- 建议先备份数据库
- 确认管理员账户信息正确
🐛 问题排查:
如果遇到 "record not found" 错误:
1. 运行此脚本清理测试数据
2. 检查数据库外键约束
3. 确保测试流程的数据清理完整性`)
}
// 管理员登录
func adminLogin() error {
loginData := map[string]string{
"email": adminEmail,
"password": adminPassword,
}
jsonData, _ := json.Marshal(loginData)
resp, err := client.Post(baseURL+"/v1/auth/login", "application/json", bytes.NewBuffer(jsonData))
if err != nil {
return fmt.Errorf("登录请求失败: %v", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("读取响应失败: %v", err)
}
var loginResp AdminLoginResponse
if err := json.Unmarshal(body, &loginResp); err != nil {
return fmt.Errorf("解析登录响应失败: %v", err)
}
if loginResp.Code != 200 {
return fmt.Errorf("登录失败 (Code: %d): %s", loginResp.Code, loginResp.Message)
}
adminToken = loginResp.Data.Token
return nil
}
// 清理测试用户
func cleanupTestUsers() error {
// 获取用户列表
users, err := getUserList()
if err != nil {
return fmt.Errorf("获取用户列表失败: %v", err)
}
fmt.Printf("📊 总用户数: %d\n", len(users))
var testUserIDs []int64
var testUsers []string
for _, user := range users {
// 识别测试用户的规则
isTestUser := strings.HasPrefix(user.Email, testEmailPrefix) ||
strings.Contains(user.Email, "test") ||
strings.Contains(user.Email, "example.com")
if isTestUser {
testUserIDs = append(testUserIDs, user.ID)
testUsers = append(testUsers, fmt.Sprintf("ID=%d, Email=%s", user.ID, user.Email))
}
}
if len(testUserIDs) == 0 {
fmt.Println("✅ 未发现测试用户,数据已清理")
return nil
}
fmt.Printf("🔍 发现 %d 个测试用户:\n", len(testUserIDs))
for _, userInfo := range testUsers {
fmt.Printf(" - %s\n", userInfo)
}
// 批量删除测试用户
fmt.Printf("\n🗑️ 正在删除 %d 个测试用户...\n", len(testUserIDs))
if err := batchDeleteUsers(testUserIDs); err != nil {
return fmt.Errorf("批量删除用户失败: %v", err)
}
fmt.Printf("✅ 成功删除 %d 个测试用户\n", len(testUserIDs))
return nil
}
// 检查数据一致性
func checkDataConsistency() {
fmt.Println("🔍 检查数据一致性...")
// 获取用户列表,检查是否还有问题用户
users, err := getUserList()
if err != nil {
fmt.Printf("❌ 无法获取用户列表: %v\n", err)
return
}
problemUsers := 0
for _, user := range users {
// 检查用户详情是否可以正常获取
if !checkUserDetail(user.ID) {
problemUsers++
fmt.Printf("⚠️ 用户 ID=%d Email=%s 可能存在数据问题\n", user.ID, user.Email)
}
}
if problemUsers == 0 {
fmt.Println("✅ 数据一致性检查通过")
} else {
fmt.Printf("⚠️ 发现 %d 个可能有问题的用户记录\n", problemUsers)
}
}
// 获取用户列表
func getUserList() ([]struct {
ID int64 `json:"id"`
Email string `json:"email"`
}, error) {
req, err := http.NewRequest("GET", baseURL+"/v1/admin/user?page=1&size=1000", nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+adminToken)
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var userResp UserListResponse
if err := json.Unmarshal(body, &userResp); err != nil {
return nil, err
}
if userResp.Code != 200 {
return nil, fmt.Errorf("获取用户列表失败 (Code: %d): %s", userResp.Code, userResp.Message)
}
return userResp.Data.List, nil
}
// 批量删除用户
func batchDeleteUsers(userIDs []int64) error {
deleteData := map[string][]int64{
"ids": userIDs,
}
jsonData, _ := json.Marshal(deleteData)
req, err := http.NewRequest("DELETE", baseURL+"/v1/admin/user/batch", bytes.NewBuffer(jsonData))
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+adminToken)
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
if resp.StatusCode != 200 {
return fmt.Errorf("删除用户失败,状态码: %d, 响应: %s", resp.StatusCode, string(body))
}
var commonResp CommonResponse
if err := json.Unmarshal(body, &commonResp); err == nil && commonResp.Code != 200 {
return fmt.Errorf("删除用户失败 (Code: %d): %s", commonResp.Code, commonResp.Message)
}
return nil
}
// 检查用户详情
func checkUserDetail(userID int64) bool {
req, err := http.NewRequest("GET", baseURL+"/v1/admin/user/"+strconv.FormatInt(userID, 10), nil)
if err != nil {
return false
}
req.Header.Set("Authorization", "Bearer "+adminToken)
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return false
}
defer resp.Body.Close()
return resp.StatusCode == 200
}