feat: 添加测试数据清理脚本并改进设备登录逻辑
All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 7m9s
All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 7m9s
docs(scripts): 添加测试数据清理脚本的详细使用文档 fix(auth): 修复设备登录时处理孤立认证方法的问题 refactor(public): 改进邮箱绑定逻辑中的推荐码处理
This commit is contained in:
parent
cef7150aab
commit
1bcfa321b7
@ -86,40 +86,63 @@ func (l *DeviceLoginLogic) DeviceLogin(req *types.DeviceLoginRequest) (resp *typ
|
||||
}
|
||||
|
||||
if authMethod != nil {
|
||||
// 认证方法存在但设备记录不存在,可能是数据不一致,获取用户信息并重新创建设备记录
|
||||
// 认证方法存在但设备记录不存在,可能是数据不一致,先检查用户是否存在
|
||||
userInfo, err = l.svcCtx.UserModel.FindOne(l.ctx, authMethod.UserId)
|
||||
if err != nil {
|
||||
l.Errorw("query user by auth method failed",
|
||||
logger.Field("user_id", authMethod.UserId),
|
||||
logger.Field("identifier", req.Identifier),
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query user failed: %v", err.Error())
|
||||
}
|
||||
|
||||
// 重新创建缺失的设备记录
|
||||
deviceInfo := &user.Device{
|
||||
Ip: req.IP,
|
||||
UserId: userInfo.Id,
|
||||
UserAgent: req.UserAgent,
|
||||
Identifier: req.Identifier,
|
||||
Enabled: true,
|
||||
Online: false,
|
||||
}
|
||||
if err := l.svcCtx.UserModel.InsertDevice(l.ctx, deviceInfo); err != nil {
|
||||
l.Errorw("failed to recreate device record",
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
// 用户不存在,说明是孤立的认证方法记录,需要清理
|
||||
l.Errorw("found orphaned auth method record, cleaning up",
|
||||
logger.Field("auth_method_id", authMethod.Id),
|
||||
logger.Field("user_id", authMethod.UserId),
|
||||
logger.Field("identifier", req.Identifier),
|
||||
)
|
||||
|
||||
// 删除孤立的认证方法记录
|
||||
if deleteErr := l.svcCtx.UserModel.DeleteUserAuthMethods(l.ctx, authMethod.UserId, authMethod.AuthType); deleteErr != nil {
|
||||
l.Errorw("failed to delete orphaned auth method",
|
||||
logger.Field("auth_method_id", authMethod.Id),
|
||||
logger.Field("error", deleteErr.Error()),
|
||||
)
|
||||
}
|
||||
|
||||
// 创建新用户和设备
|
||||
userInfo, err = l.registerUserAndDevice(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
l.Errorw("query user by auth method failed",
|
||||
logger.Field("user_id", authMethod.UserId),
|
||||
logger.Field("identifier", req.Identifier),
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query user failed: %v", err.Error())
|
||||
}
|
||||
} else {
|
||||
// 用户存在,重新创建缺失的设备记录
|
||||
deviceInfo := &user.Device{
|
||||
Ip: req.IP,
|
||||
UserId: userInfo.Id,
|
||||
UserAgent: req.UserAgent,
|
||||
Identifier: req.Identifier,
|
||||
Enabled: true,
|
||||
Online: false,
|
||||
}
|
||||
if err := l.svcCtx.UserModel.InsertDevice(l.ctx, deviceInfo); err != nil {
|
||||
l.Errorw("failed to recreate device record",
|
||||
logger.Field("user_id", userInfo.Id),
|
||||
logger.Field("identifier", req.Identifier),
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "recreate device record failed: %v", err)
|
||||
}
|
||||
|
||||
l.Infow("found existing auth method without device record, recreated device record",
|
||||
logger.Field("user_id", userInfo.Id),
|
||||
logger.Field("identifier", req.Identifier),
|
||||
logger.Field("error", err.Error()),
|
||||
logger.Field("device_id", deviceInfo.Id),
|
||||
)
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "recreate device record failed: %v", err)
|
||||
}
|
||||
|
||||
l.Infow("found existing auth method without device record, recreated device record",
|
||||
logger.Field("user_id", userInfo.Id),
|
||||
logger.Field("identifier", req.Identifier),
|
||||
logger.Field("device_id", deviceInfo.Id),
|
||||
)
|
||||
} else {
|
||||
// 设备和认证方法都不存在,创建新用户和设备
|
||||
userInfo, err = l.registerUserAndDevice(req)
|
||||
|
||||
@ -12,6 +12,7 @@ import (
|
||||
"github.com/perfect-panel/server/pkg/constant"
|
||||
"github.com/perfect-panel/server/pkg/jwt"
|
||||
"github.com/perfect-panel/server/pkg/logger"
|
||||
"github.com/perfect-panel/server/pkg/uuidx"
|
||||
"github.com/perfect-panel/server/pkg/xerr"
|
||||
"github.com/pkg/errors"
|
||||
"gorm.io/gorm"
|
||||
@ -127,13 +128,57 @@ func (l *BindEmailWithVerificationLogic) transferDeviceToEmailUser(deviceUserId,
|
||||
// 2. 在事务中执行设备转移
|
||||
err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error {
|
||||
// 1. 检查目标邮箱用户状态
|
||||
_, err := l.svcCtx.UserModel.FindOne(l.ctx, emailUserId)
|
||||
emailUser, err := l.svcCtx.UserModel.FindOne(l.ctx, emailUserId)
|
||||
if err != nil {
|
||||
l.Errorw("查询邮箱用户失败", logger.Field("error", err.Error()), logger.Field("email_user_id", emailUserId))
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. 检查设备是否已经关联到目标用户
|
||||
// 2. 获取原设备用户信息
|
||||
deviceUser, err := l.svcCtx.UserModel.FindOne(l.ctx, deviceUserId)
|
||||
if err != nil {
|
||||
l.Errorw("查询设备用户失败", logger.Field("error", err.Error()), logger.Field("device_user_id", deviceUserId))
|
||||
return err
|
||||
}
|
||||
|
||||
// 3. 如果邮箱用户没有ReferCode,则从设备用户转移或生成新的
|
||||
if emailUser.ReferCode == "" {
|
||||
if deviceUser.ReferCode != "" {
|
||||
// 转移设备用户的ReferCode
|
||||
if err := db.Model(&user.User{}).Where("id = ?", emailUserId).Update("refer_code", deviceUser.ReferCode).Error; err != nil {
|
||||
l.Errorw("转移ReferCode失败", logger.Field("error", err.Error()))
|
||||
return err
|
||||
}
|
||||
l.Infow("已转移设备用户的ReferCode到邮箱用户",
|
||||
logger.Field("device_user_id", deviceUserId),
|
||||
logger.Field("email_user_id", emailUserId),
|
||||
logger.Field("refer_code", deviceUser.ReferCode))
|
||||
} else {
|
||||
// 为邮箱用户生成新的ReferCode
|
||||
newReferCode := uuidx.UserInviteCode(emailUserId)
|
||||
if err := db.Model(&user.User{}).Where("id = ?", emailUserId).Update("refer_code", newReferCode).Error; err != nil {
|
||||
l.Errorw("生成邮箱用户ReferCode失败", logger.Field("error", err.Error()))
|
||||
return err
|
||||
}
|
||||
l.Infow("已为邮箱用户生成新的ReferCode",
|
||||
logger.Field("email_user_id", emailUserId),
|
||||
logger.Field("refer_code", newReferCode))
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 如果邮箱用户没有RefererId,但设备用户有,则转移RefererId
|
||||
if emailUser.RefererId == 0 && deviceUser.RefererId != 0 {
|
||||
if err := db.Model(&user.User{}).Where("id = ?", emailUserId).Update("referer_id", deviceUser.RefererId).Error; err != nil {
|
||||
l.Errorw("转移RefererId失败", logger.Field("error", err.Error()))
|
||||
return err
|
||||
}
|
||||
l.Infow("已转移设备用户的RefererId到邮箱用户",
|
||||
logger.Field("device_user_id", deviceUserId),
|
||||
logger.Field("email_user_id", emailUserId),
|
||||
logger.Field("referer_id", deviceUser.RefererId))
|
||||
}
|
||||
|
||||
// 5. 检查设备是否已经关联到目标用户
|
||||
existingDevice, err := l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, deviceIdentifier)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
l.Errorw("查询设备信息失败", logger.Field("error", err.Error()), logger.Field("device_identifier", deviceIdentifier))
|
||||
@ -146,7 +191,7 @@ func (l *BindEmailWithVerificationLogic) transferDeviceToEmailUser(deviceUserId,
|
||||
return nil
|
||||
}
|
||||
|
||||
// 3. 处理设备冲突 - 删除目标用户的现有设备记录(如果存在)
|
||||
// 6. 处理设备冲突 - 删除目标用户的现有设备记录(如果存在)
|
||||
if existingDevice != nil && existingDevice.UserId != emailUserId {
|
||||
l.Infow("删除冲突的设备记录", logger.Field("existing_device_id", existingDevice.Id), logger.Field("existing_user_id", existingDevice.UserId))
|
||||
if err := db.Where("identifier = ? AND user_id = ?", deviceIdentifier, existingDevice.UserId).Delete(&user.Device{}).Error; err != nil {
|
||||
@ -155,7 +200,7 @@ func (l *BindEmailWithVerificationLogic) transferDeviceToEmailUser(deviceUserId,
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 更新user_auth_methods表 - 将设备认证方式转移到邮箱用户
|
||||
// 7. 更新user_auth_methods表 - 将设备认证方式转移到邮箱用户
|
||||
if err := db.Model(&user.AuthMethods{}).
|
||||
Where("user_id = ? AND auth_type = ? AND auth_identifier = ?", deviceUserId, "device", deviceIdentifier).
|
||||
Update("user_id", emailUserId).Error; err != nil {
|
||||
@ -163,7 +208,7 @@ func (l *BindEmailWithVerificationLogic) transferDeviceToEmailUser(deviceUserId,
|
||||
return err
|
||||
}
|
||||
|
||||
// 5. 更新user_device表 - 将设备记录转移到邮箱用户
|
||||
// 8. 更新user_device表 - 将设备记录转移到邮箱用户
|
||||
if err := db.Model(&user.Device{}).
|
||||
Where("user_id = ? AND identifier = ?", deviceUserId, deviceIdentifier).
|
||||
Update("user_id", emailUserId).Error; err != nil {
|
||||
@ -171,7 +216,7 @@ func (l *BindEmailWithVerificationLogic) transferDeviceToEmailUser(deviceUserId,
|
||||
return err
|
||||
}
|
||||
|
||||
// 6. 检查原始设备用户是否还有其他认证方式,如果没有则删除该用户
|
||||
// 9. 检查原始设备用户是否还有其他认证方式,如果没有则删除该用户
|
||||
var remainingAuthMethods []user.AuthMethods
|
||||
if err := db.Where("user_id = ?", deviceUserId).Find(&remainingAuthMethods).Error; err != nil {
|
||||
l.Errorw("查询原始用户剩余认证方式失败", logger.Field("error", err.Error()), logger.Field("device_user_id", deviceUserId))
|
||||
@ -278,13 +323,22 @@ func (l *BindEmailWithVerificationLogic) generateTokenForUser(userId int64) (str
|
||||
|
||||
// createEmailUser 创建新的邮箱用户
|
||||
func (l *BindEmailWithVerificationLogic) createEmailUser(email string) (int64, error) {
|
||||
// 检查是否启用了强制邀请码
|
||||
if l.svcCtx.Config.Invite.ForcedInvite {
|
||||
l.Errorw("邮箱绑定创建新用户时需要邀请码,但当前API不支持邀请码参数",
|
||||
logger.Field("email", email),
|
||||
logger.Field("forced_invite", true))
|
||||
return 0, xerr.NewErrMsg("创建新用户需要邀请码,请使用支持邀请码的注册方式")
|
||||
}
|
||||
|
||||
var newUserId int64
|
||||
|
||||
err := l.svcCtx.UserModel.Transaction(l.ctx, func(tx *gorm.DB) error {
|
||||
// 1. 创建新用户
|
||||
enabled := true
|
||||
newUser := &user.User{
|
||||
Enable: &enabled, // 启用状态
|
||||
Enable: &enabled, // 启用状态
|
||||
OnlyFirstPurchase: &l.svcCtx.Config.Invite.OnlyFirstPurchase,
|
||||
}
|
||||
if err := tx.Create(newUser).Error; err != nil {
|
||||
l.Errorw("创建用户失败", logger.Field("error", err.Error()))
|
||||
@ -294,7 +348,17 @@ func (l *BindEmailWithVerificationLogic) createEmailUser(email string) (int64, e
|
||||
newUserId = newUser.Id
|
||||
l.Infow("创建新用户成功", logger.Field("user_id", newUserId))
|
||||
|
||||
// 2. 创建邮箱认证方法
|
||||
// 2. 生成并设置用户的ReferCode
|
||||
newUser.ReferCode = uuidx.UserInviteCode(newUserId)
|
||||
if err := tx.Model(&user.User{}).Where("id = ?", newUserId).Update("refer_code", newUser.ReferCode).Error; err != nil {
|
||||
l.Errorw("更新用户ReferCode失败", logger.Field("error", err.Error()))
|
||||
return err
|
||||
}
|
||||
l.Infow("设置用户ReferCode成功",
|
||||
logger.Field("user_id", newUserId),
|
||||
logger.Field("refer_code", newUser.ReferCode))
|
||||
|
||||
// 3. 创建邮箱认证方法
|
||||
emailAuth := &user.AuthMethods{
|
||||
UserId: newUserId,
|
||||
AuthType: "email",
|
||||
|
||||
149
scripts/README.md
Normal file
149
scripts/README.md
Normal file
@ -0,0 +1,149 @@
|
||||
# 测试数据清理脚本
|
||||
|
||||
## 问题背景
|
||||
|
||||
在运行测试案例 `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
scripts/cleanup_test_data.go
Normal file
339
scripts/cleanup_test_data.go
Normal file
@ -0,0 +1,339 @@
|
||||
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
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user