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 {
|
if authMethod != nil {
|
||||||
// 认证方法存在但设备记录不存在,可能是数据不一致,获取用户信息并重新创建设备记录
|
// 认证方法存在但设备记录不存在,可能是数据不一致,先检查用户是否存在
|
||||||
userInfo, err = l.svcCtx.UserModel.FindOne(l.ctx, authMethod.UserId)
|
userInfo, err = l.svcCtx.UserModel.FindOne(l.ctx, authMethod.UserId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Errorw("query user by auth method failed",
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
logger.Field("user_id", authMethod.UserId),
|
// 用户不存在,说明是孤立的认证方法记录,需要清理
|
||||||
logger.Field("identifier", req.Identifier),
|
l.Errorw("found orphaned auth method record, cleaning up",
|
||||||
logger.Field("error", err.Error()),
|
logger.Field("auth_method_id", authMethod.Id),
|
||||||
)
|
logger.Field("user_id", authMethod.UserId),
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query user failed: %v", err.Error())
|
logger.Field("identifier", req.Identifier),
|
||||||
}
|
)
|
||||||
|
|
||||||
// 重新创建缺失的设备记录
|
// 删除孤立的认证方法记录
|
||||||
deviceInfo := &user.Device{
|
if deleteErr := l.svcCtx.UserModel.DeleteUserAuthMethods(l.ctx, authMethod.UserId, authMethod.AuthType); deleteErr != nil {
|
||||||
Ip: req.IP,
|
l.Errorw("failed to delete orphaned auth method",
|
||||||
UserId: userInfo.Id,
|
logger.Field("auth_method_id", authMethod.Id),
|
||||||
UserAgent: req.UserAgent,
|
logger.Field("error", deleteErr.Error()),
|
||||||
Identifier: req.Identifier,
|
)
|
||||||
Enabled: true,
|
}
|
||||||
Online: false,
|
|
||||||
}
|
// 创建新用户和设备
|
||||||
if err := l.svcCtx.UserModel.InsertDevice(l.ctx, deviceInfo); err != nil {
|
userInfo, err = l.registerUserAndDevice(req)
|
||||||
l.Errorw("failed to recreate device record",
|
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("user_id", userInfo.Id),
|
||||||
logger.Field("identifier", req.Identifier),
|
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 {
|
} else {
|
||||||
// 设备和认证方法都不存在,创建新用户和设备
|
// 设备和认证方法都不存在,创建新用户和设备
|
||||||
userInfo, err = l.registerUserAndDevice(req)
|
userInfo, err = l.registerUserAndDevice(req)
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/perfect-panel/server/pkg/constant"
|
"github.com/perfect-panel/server/pkg/constant"
|
||||||
"github.com/perfect-panel/server/pkg/jwt"
|
"github.com/perfect-panel/server/pkg/jwt"
|
||||||
"github.com/perfect-panel/server/pkg/logger"
|
"github.com/perfect-panel/server/pkg/logger"
|
||||||
|
"github.com/perfect-panel/server/pkg/uuidx"
|
||||||
"github.com/perfect-panel/server/pkg/xerr"
|
"github.com/perfect-panel/server/pkg/xerr"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@ -127,13 +128,57 @@ func (l *BindEmailWithVerificationLogic) transferDeviceToEmailUser(deviceUserId,
|
|||||||
// 2. 在事务中执行设备转移
|
// 2. 在事务中执行设备转移
|
||||||
err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error {
|
err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error {
|
||||||
// 1. 检查目标邮箱用户状态
|
// 1. 检查目标邮箱用户状态
|
||||||
_, err := l.svcCtx.UserModel.FindOne(l.ctx, emailUserId)
|
emailUser, err := l.svcCtx.UserModel.FindOne(l.ctx, emailUserId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Errorw("查询邮箱用户失败", logger.Field("error", err.Error()), logger.Field("email_user_id", emailUserId))
|
l.Errorw("查询邮箱用户失败", logger.Field("error", err.Error()), logger.Field("email_user_id", emailUserId))
|
||||||
return err
|
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)
|
existingDevice, err := l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, deviceIdentifier)
|
||||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
l.Errorw("查询设备信息失败", logger.Field("error", err.Error()), logger.Field("device_identifier", deviceIdentifier))
|
l.Errorw("查询设备信息失败", logger.Field("error", err.Error()), logger.Field("device_identifier", deviceIdentifier))
|
||||||
@ -146,7 +191,7 @@ func (l *BindEmailWithVerificationLogic) transferDeviceToEmailUser(deviceUserId,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 处理设备冲突 - 删除目标用户的现有设备记录(如果存在)
|
// 6. 处理设备冲突 - 删除目标用户的现有设备记录(如果存在)
|
||||||
if existingDevice != nil && existingDevice.UserId != emailUserId {
|
if existingDevice != nil && existingDevice.UserId != emailUserId {
|
||||||
l.Infow("删除冲突的设备记录", logger.Field("existing_device_id", existingDevice.Id), logger.Field("existing_user_id", existingDevice.UserId))
|
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 {
|
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{}).
|
if err := db.Model(&user.AuthMethods{}).
|
||||||
Where("user_id = ? AND auth_type = ? AND auth_identifier = ?", deviceUserId, "device", deviceIdentifier).
|
Where("user_id = ? AND auth_type = ? AND auth_identifier = ?", deviceUserId, "device", deviceIdentifier).
|
||||||
Update("user_id", emailUserId).Error; err != nil {
|
Update("user_id", emailUserId).Error; err != nil {
|
||||||
@ -163,7 +208,7 @@ func (l *BindEmailWithVerificationLogic) transferDeviceToEmailUser(deviceUserId,
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. 更新user_device表 - 将设备记录转移到邮箱用户
|
// 8. 更新user_device表 - 将设备记录转移到邮箱用户
|
||||||
if err := db.Model(&user.Device{}).
|
if err := db.Model(&user.Device{}).
|
||||||
Where("user_id = ? AND identifier = ?", deviceUserId, deviceIdentifier).
|
Where("user_id = ? AND identifier = ?", deviceUserId, deviceIdentifier).
|
||||||
Update("user_id", emailUserId).Error; err != nil {
|
Update("user_id", emailUserId).Error; err != nil {
|
||||||
@ -171,7 +216,7 @@ func (l *BindEmailWithVerificationLogic) transferDeviceToEmailUser(deviceUserId,
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. 检查原始设备用户是否还有其他认证方式,如果没有则删除该用户
|
// 9. 检查原始设备用户是否还有其他认证方式,如果没有则删除该用户
|
||||||
var remainingAuthMethods []user.AuthMethods
|
var remainingAuthMethods []user.AuthMethods
|
||||||
if err := db.Where("user_id = ?", deviceUserId).Find(&remainingAuthMethods).Error; err != nil {
|
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))
|
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 创建新的邮箱用户
|
// createEmailUser 创建新的邮箱用户
|
||||||
func (l *BindEmailWithVerificationLogic) createEmailUser(email string) (int64, error) {
|
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
|
var newUserId int64
|
||||||
|
|
||||||
err := l.svcCtx.UserModel.Transaction(l.ctx, func(tx *gorm.DB) error {
|
err := l.svcCtx.UserModel.Transaction(l.ctx, func(tx *gorm.DB) error {
|
||||||
// 1. 创建新用户
|
// 1. 创建新用户
|
||||||
enabled := true
|
enabled := true
|
||||||
newUser := &user.User{
|
newUser := &user.User{
|
||||||
Enable: &enabled, // 启用状态
|
Enable: &enabled, // 启用状态
|
||||||
|
OnlyFirstPurchase: &l.svcCtx.Config.Invite.OnlyFirstPurchase,
|
||||||
}
|
}
|
||||||
if err := tx.Create(newUser).Error; err != nil {
|
if err := tx.Create(newUser).Error; err != nil {
|
||||||
l.Errorw("创建用户失败", logger.Field("error", err.Error()))
|
l.Errorw("创建用户失败", logger.Field("error", err.Error()))
|
||||||
@ -294,7 +348,17 @@ func (l *BindEmailWithVerificationLogic) createEmailUser(email string) (int64, e
|
|||||||
newUserId = newUser.Id
|
newUserId = newUser.Id
|
||||||
l.Infow("创建新用户成功", logger.Field("user_id", newUserId))
|
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{
|
emailAuth := &user.AuthMethods{
|
||||||
UserId: newUserId,
|
UserId: newUserId,
|
||||||
AuthType: "email",
|
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