feat: 添加测试数据清理脚本并改进设备登录逻辑
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:
shanshanzhong 2025-10-27 22:20:18 -07:00
parent cef7150aab
commit 1bcfa321b7
5 changed files with 611 additions and 36 deletions

View File

@ -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)

View File

@ -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
View 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定义

View 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
}

BIN
server

Binary file not shown.