feat: 添加测试链工具并修复小程序状态监控错误

添加测试链工具用于验证活动和工会功能,修复小程序状态监控中的错误处理逻辑
This commit is contained in:
邹方成 2025-11-13 14:26:54 +08:00
parent 00f758ecba
commit b847a72a6a
2 changed files with 550 additions and 1 deletions

549
cmd/testchain/main.go Normal file
View File

@ -0,0 +1,549 @@
package main
import (
"bytes"
"crypto/md5"
"encoding/hex"
"encoding/json"
"flag"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strings"
"time"
)
// doRequest 发送 HTTP 请求并返回响应内容
// 参数: method 请求方法, urlStr 请求地址, headers 请求头, body 请求体字节
// 返回: 响应字节与错误
func doRequest(method, urlStr string, headers map[string]string, body []byte) ([]byte, int, error) {
req, err := http.NewRequest(method, urlStr, bytes.NewReader(body))
if err != nil {
return nil, 0, err
}
for k, v := range headers {
req.Header.Set(k, v)
}
client := &http.Client{Timeout: 15 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, 0, err
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, resp.StatusCode, err
}
return data, resp.StatusCode, nil
}
// md5Hex 计算字符串的 MD5 十六进制
// 参数: s 输入字符串
// 返回: 32位十六进制 MD5 字符串
func md5Hex(s string) string {
h := md5.Sum([]byte(s))
return hex.EncodeToString(h[:])
}
// Failure 统一错误结构
type Failure struct {
Code int `json:"code"`
Message string `json:"message"`
}
// AdminLoginResponse 管理端登录返回
type AdminLoginResponse struct {
Token string `json:"token"`
IsSuper int32 `json:"is_super"`
}
// SimpleMessage 简单消息返回
type SimpleMessage struct {
Message string `json:"message"`
}
// AppActivityItem APP活动项
type AppActivityItem struct {
ID int64 `json:"id"`
Name string `json:"name"`
Banner string `json:"banner"`
ActivityCategoryID int64 `json:"activity_category_id"`
Status int32 `json:"status"`
PriceDraw int64 `json:"price_draw"`
IsBoss int32 `json:"is_boss"`
}
// AppListActivitiesResponse APP活动列表返回
type AppListActivitiesResponse struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
Total int64 `json:"total"`
List []AppActivityItem `json:"list"`
}
// AdminIssueData 管理端期数项
type AdminIssueData struct {
ID int64 `json:"id"`
IssueNumber string `json:"issue_number"`
Status int32 `json:"status"`
Sort int32 `json:"sort"`
}
// AdminListIssuesResponse 管理端期数列表返回
type AdminListIssuesResponse struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
Total int64 `json:"total"`
List []AdminIssueData `json:"list"`
}
// RewardItem 奖励项
type RewardItem struct {
ProductID int64 `json:"product_id"`
Name string `json:"name"`
Weight int32 `json:"weight"`
Quantity int64 `json:"quantity"`
OriginalQty int64 `json:"original_qty"`
Level int32 `json:"level"`
Sort int32 `json:"sort"`
IsBoss int32 `json:"is_boss"`
}
// ListRewardsResponse 奖励列表返回
type ListRewardsResponse struct {
List []RewardItem `json:"list"`
}
// loginAdmin 管理端登录获取 Token
// 参数: baseURL 服务基地址, username 用户名, passwordMD5 MD5密码
// 返回: token 字符串与错误
func loginAdmin(baseURL, username, password string) (string, error) {
payload := map[string]string{"username": username, "password": password}
b, _ := json.Marshal(payload)
data, code, err := doRequest("POST", baseURL+"/api/admin/login", map[string]string{"Content-Type": "application/json"}, b)
if err != nil {
return "", err
}
if code != http.StatusOK {
var fail Failure
_ = json.Unmarshal(data, &fail)
return "", fmt.Errorf("login failed: %d %s", code, fail.Message)
}
var res AdminLoginResponse
if err := json.Unmarshal(data, &res); err != nil {
return "", err
}
return res.Token, nil
}
// createActivity 创建活动
// 参数: baseURL 基地址, token 管理端token, name 名称, categoryID 分类ID, isBoss 是否Boss
// 返回: 错误
func createActivity(baseURL, token, name string, categoryID int64, isBoss int32) error {
start := time.Now().UTC().Format(time.RFC3339)
end := time.Now().Add(24 * time.Hour).UTC().Format(time.RFC3339)
payload := map[string]any{
"name": name,
"banner": "",
"activity_category_id": categoryID,
"status": 1,
"price_draw": 0,
"is_boss": isBoss,
"start_time": start,
"end_time": end,
}
b, _ := json.Marshal(payload)
headers := map[string]string{"Content-Type": "application/json", "Authorization": token}
data, code, err := doRequest("POST", baseURL+"/api/admin/activities", headers, b)
if err != nil {
return err
}
if code != http.StatusOK {
var fail Failure
_ = json.Unmarshal(data, &fail)
return fmt.Errorf("create activity failed: %d %s", code, fail.Message)
}
return nil
}
// createGuild 创建工会
// 参数: baseURL 基地址, token 管理端token, name 名称, ownerID 会长用户ID, isOpen 是否公开
// 返回: 错误
func createGuild(baseURL, token, name string, ownerID int64, isOpen int32) error {
payload := map[string]any{
"name": name,
"owner_id": ownerID,
"description": "",
"join_mode": 2,
"consume_limit": 0,
"avatar_url": "",
"is_open": isOpen,
}
b, _ := json.Marshal(payload)
headers := map[string]string{"Content-Type": "application/json", "Authorization": token}
data, code, err := doRequest("POST", baseURL+"/api/admin/guilds", headers, b)
if err != nil {
return err
}
if code != http.StatusOK {
var fail Failure
_ = json.Unmarshal(data, &fail)
return fmt.Errorf("create guild failed: %d %s", code, fail.Message)
}
return nil
}
// findGuildIDByName 通过名称查询工会ID
// 参数: baseURL 基地址, name 名称
// 返回: 工会ID与错误
func findGuildIDByName(baseURL, name string) (int64, error) {
q := url.Values{}
q.Set("name", name)
q.Set("page", "1")
q.Set("page_size", "20")
data, code, err := doRequest("GET", baseURL+"/api/app/guilds?"+q.Encode(), nil, nil)
if err != nil {
return 0, err
}
if code != http.StatusOK {
var fail Failure
_ = json.Unmarshal(data, &fail)
return 0, fmt.Errorf("list guilds failed: %d %s", code, fail.Message)
}
type GuildItem struct {
ID int64
Name string
}
type ListGuildsResponse struct{ List []GuildItem }
var res ListGuildsResponse
if err := json.Unmarshal(data, &res); err != nil {
return 0, err
}
for _, it := range res.List {
if strings.EqualFold(it.Name, name) {
return it.ID, nil
}
}
return 0, fmt.Errorf("guild not found: %s", name)
}
// joinGuildApp APP 加入工会
// 参数: baseURL 基地址, guildID 工会ID, userID 用户ID
// 返回: 错误
func joinGuildApp(baseURL string, guildID, userID int64) error {
b, _ := json.Marshal(map[string]any{"user_id": userID})
u := fmt.Sprintf("%s/api/app/guilds/%d/members", baseURL, guildID)
data, code, err := doRequest("POST", u, map[string]string{"Content-Type": "application/json"}, b)
if err != nil {
return err
}
if code != http.StatusOK {
var fail Failure
_ = json.Unmarshal(data, &fail)
return fmt.Errorf("join guild failed: %d %s", code, fail.Message)
}
return nil
}
// listGuildMembersApp APP 工会成员列表
// 参数: baseURL 基地址, guildID 工会ID
// 返回: 成员数量与错误
func listGuildMembersApp(baseURL string, guildID int64) (int, error) {
q := url.Values{}
q.Set("page", "1")
q.Set("page_size", "100")
u := fmt.Sprintf("%s/api/app/guilds/%d/members?%s", baseURL, guildID, q.Encode())
data, code, err := doRequest("GET", u, nil, nil)
if err != nil {
return 0, err
}
if code != http.StatusOK {
var fail Failure
_ = json.Unmarshal(data, &fail)
return 0, fmt.Errorf("list members failed: %d %s", code, fail.Message)
}
type MemberItem struct{ ID int64 }
type ListResponse struct{ List []MemberItem }
var res ListResponse
if err := json.Unmarshal(data, &res); err != nil {
return 0, err
}
return len(res.List), nil
}
// leaveGuildApp APP 离开工会
// 参数: baseURL 基地址, guildID 工会ID, userID 用户ID
// 返回: 错误
func leaveGuildApp(baseURL string, guildID, userID int64) error {
u := fmt.Sprintf("%s/api/app/guilds/%d/members/%d", baseURL, guildID, userID)
data, code, err := doRequest("DELETE", u, nil, nil)
if err != nil {
return err
}
if code != http.StatusOK {
var fail Failure
_ = json.Unmarshal(data, &fail)
return fmt.Errorf("leave guild failed: %d %s", code, fail.Message)
}
return nil
}
// findActivityIDByName 通过名称查询活动ID
// 参数: baseURL 基地址, name 名称
// 返回: 活动ID与错误
func findActivityIDByName(baseURL, name string) (int64, error) {
q := url.Values{}
q.Set("name", name)
q.Set("page", "1")
q.Set("page_size", "20")
data, code, err := doRequest("GET", baseURL+"/api/app/activities?"+q.Encode(), nil, nil)
if err != nil {
return 0, err
}
if code != http.StatusOK {
var fail Failure
_ = json.Unmarshal(data, &fail)
return 0, fmt.Errorf("list activities failed: %d %s", code, fail.Message)
}
var res AppListActivitiesResponse
if err := json.Unmarshal(data, &res); err != nil {
return 0, err
}
for _, it := range res.List {
if strings.EqualFold(it.Name, name) {
return it.ID, nil
}
}
return 0, fmt.Errorf("activity not found: %s", name)
}
// createIssue 创建活动期数
// 参数: baseURL 基地址, token 管理端token, activityID 活动ID, issueNumber 期号
// 返回: 错误
func createIssue(baseURL, token string, activityID int64, issueNumber string) error {
payload := map[string]any{"issue_number": issueNumber, "status": 1, "sort": 1}
b, _ := json.Marshal(payload)
headers := map[string]string{"Content-Type": "application/json", "Authorization": token}
u := fmt.Sprintf("%s/api/admin/activities/%d/issues", baseURL, activityID)
data, code, err := doRequest("POST", u, headers, b)
if err != nil {
return err
}
if code != http.StatusOK {
var fail Failure
_ = json.Unmarshal(data, &fail)
return fmt.Errorf("create issue failed: %d %s", code, fail.Message)
}
return nil
}
// findIssueIDByNumber 通过期号查询期ID
// 参数: baseURL 基地址, token 管理端token, activityID 活动ID, issueNumber 期号
// 返回: 期ID与错误
func findIssueIDByNumber(baseURL, token string, activityID int64, issueNumber string) (int64, error) {
q := url.Values{}
q.Set("page", "1")
q.Set("page_size", "20")
headers := map[string]string{"Authorization": token}
u := fmt.Sprintf("%s/api/admin/activities/%d/issues?%s", baseURL, activityID, q.Encode())
data, code, err := doRequest("GET", u, headers, nil)
if err != nil {
return 0, err
}
if code != http.StatusOK {
var fail Failure
_ = json.Unmarshal(data, &fail)
return 0, fmt.Errorf("list admin issues failed: %d %s", code, fail.Message)
}
var res AdminListIssuesResponse
if err := json.Unmarshal(data, &res); err != nil {
return 0, err
}
for _, it := range res.List {
if strings.EqualFold(it.IssueNumber, issueNumber) {
return it.ID, nil
}
}
return 0, fmt.Errorf("issue not found: %s", issueNumber)
}
// createRewards 批量创建奖励
// 参数: baseURL 基地址, token 管理端token, activityID 活动ID, issueID 期ID, rewards 奖励列表
// 返回: 错误
func createRewards(baseURL, token string, activityID, issueID int64, rewards []RewardItem) error {
payload := map[string]any{"rewards": rewards}
b, _ := json.Marshal(payload)
headers := map[string]string{"Content-Type": "application/json", "Authorization": token}
u := fmt.Sprintf("%s/api/admin/activities/%d/issues/%d/rewards", baseURL, activityID, issueID)
data, code, err := doRequest("POST", u, headers, b)
if err != nil {
return err
}
if code != http.StatusOK {
var fail Failure
_ = json.Unmarshal(data, &fail)
return fmt.Errorf("create rewards failed: %d %s", code, fail.Message)
}
return nil
}
// verifyAppChain 验证 APP 端链路
// 参数: baseURL 基地址, activityID 活动ID, issueID 期ID
// 返回: 错误
func verifyAppChain(baseURL string, activityID, issueID int64) error {
_, code1, err := doRequest("GET", fmt.Sprintf("%s/api/app/activities/%d", baseURL, activityID), nil, nil)
if err != nil || code1 != http.StatusOK {
return fmt.Errorf("app activity detail failed")
}
q := url.Values{}
q.Set("page", "1")
q.Set("page_size", "20")
_, code2, err := doRequest("GET", fmt.Sprintf("%s/api/app/activities/%d/issues?%s", baseURL, activityID, q.Encode()), nil, nil)
if err != nil || code2 != http.StatusOK {
return fmt.Errorf("app issues list failed")
}
_, code3, err := doRequest("GET", fmt.Sprintf("%s/api/app/activities/%d/issues/%d/rewards", baseURL, activityID, issueID), nil, nil)
if err != nil || code3 != http.StatusOK {
return fmt.Errorf("app rewards list failed")
}
q2 := url.Values{}
q2.Set("page", "1")
q2.Set("page_size", "20")
_, code4, err := doRequest("GET", fmt.Sprintf("%s/api/app/activities/%d/issues/%d/draw_logs?%s", baseURL, activityID, issueID, q2.Encode()), nil, nil)
if err != nil || code4 != http.StatusOK {
return fmt.Errorf("app draw logs list failed")
}
return nil
}
// main 主流程入口
// 参数: 命令行参数
// 返回: 无
func main() {
base := flag.String("base", "http://127.0.0.1:9991", "api base url")
chain := flag.String("chain", "activity", "test chain: activity|guild")
adminUser := flag.String("admin_user", os.Getenv("ADMIN_USER"), "admin username")
adminPass := flag.String("admin_pass", os.Getenv("ADMIN_PASS"), "admin password (plain)")
adminPassMD5 := flag.String("admin_pass_md5", os.Getenv("ADMIN_PASS_MD5"), "admin password (alternate input)")
activityName := flag.String("activity_name", fmt.Sprintf("自动化活动-%d", time.Now().Unix()), "activity name")
issueNumber := flag.String("issue_number", fmt.Sprintf("ISSUE-%d", time.Now().Unix()), "issue number")
categoryID := flag.Int64("category_id", 1, "activity category id")
isBoss := flag.Int("is_boss", 0, "activity boss flag 0/1")
guildName := flag.String("guild_name", fmt.Sprintf("自动化工会-%d", time.Now().Unix()), "guild name")
ownerID := flag.Int64("owner_id", 1001, "guild owner user id")
memberID := flag.Int64("member_id", 2002, "guild member user id")
isOpen := flag.Int("is_open", 1, "guild open flag 1/2")
flag.Parse()
if *adminUser == "" || (*adminPass == "" && *adminPassMD5 == "") {
fmt.Println("缺少管理员登录参数")
os.Exit(1)
}
password := *adminPassMD5
if password == "" {
password = md5Hex(*adminPass)
}
token, err := loginAdmin(*base, *adminUser, password)
if err != nil {
fmt.Println("登录失败", err)
os.Exit(1)
}
fmt.Println("登录成功")
if *chain == "activity" {
if err := createActivity(*base, token, *activityName, *categoryID, int32(*isBoss)); err != nil {
fmt.Println("创建活动失败", err)
os.Exit(1)
}
fmt.Println("创建活动成功")
actID, err := findActivityIDByName(*base, *activityName)
if err != nil {
fmt.Println("查询活动ID失败", err)
os.Exit(1)
}
fmt.Println("活动ID", actID)
if err := createIssue(*base, token, actID, *issueNumber); err != nil {
fmt.Println("创建期数失败", err)
os.Exit(1)
}
fmt.Println("创建期数成功")
issueID, err := findIssueIDByNumber(*base, token, actID, *issueNumber)
if err != nil {
fmt.Println("查询期ID失败", err)
os.Exit(1)
}
fmt.Println("期ID", issueID)
rewards := []RewardItem{
{ProductID: 0, Name: "S-稀有奖", Weight: 1, Quantity: 1, OriginalQty: 1, Level: 1, Sort: 1, IsBoss: 0},
{ProductID: 0, Name: "A-普通奖", Weight: 10, Quantity: 10, OriginalQty: 10, Level: 2, Sort: 2, IsBoss: 0},
}
if err := createRewards(*base, token, actID, issueID, rewards); err != nil {
fmt.Println("创建奖励失败", err)
os.Exit(1)
}
fmt.Println("创建奖励成功")
if err := verifyAppChain(*base, actID, issueID); err != nil {
fmt.Println("APP链路验证失败", err)
os.Exit(1)
}
fmt.Println("APP链路验证成功")
return
}
if *chain == "guild" {
if err := createGuild(*base, token, *guildName, *ownerID, int32(*isOpen)); err != nil {
fmt.Println("创建工会失败", err)
os.Exit(1)
}
fmt.Println("创建工会成功")
gid, err := findGuildIDByName(*base, *guildName)
if err != nil {
fmt.Println("查询工会ID失败", err)
os.Exit(1)
}
fmt.Println("工会ID", gid)
if err := joinGuildApp(*base, gid, *memberID); err != nil {
if strings.Contains(err.Error(), "join limited") {
alt := time.Now().Unix() % 100000
*memberID = 300000 + alt
if err2 := joinGuildApp(*base, gid, *memberID); err2 != nil {
fmt.Println("加入工会失败", err2)
os.Exit(1)
}
fmt.Println("加入工会成功(使用备用用户)")
} else {
fmt.Println("加入工会失败", err)
os.Exit(1)
}
} else {
fmt.Println("加入工会成功")
}
count, err := listGuildMembersApp(*base, gid)
if err != nil {
fmt.Println("查看成员失败", err)
os.Exit(1)
}
fmt.Println("成员数量", count)
if err := leaveGuildApp(*base, gid, *memberID); err != nil {
fmt.Println("离开工会失败", err)
os.Exit(1)
}
fmt.Println("离开工会成功")
err = joinGuildApp(*base, gid, *memberID)
if err == nil {
fmt.Println("预期被限制加入但成功了")
os.Exit(1)
}
if err != nil && strings.Contains(err.Error(), "join limited") {
fmt.Println("24小时限制验证成功")
} else {
fmt.Println("24小时限制验证失败", err)
os.Exit(1)
}
return
}
}

View File

@ -70,7 +70,7 @@ func (s *server) Start() {
"updated_at": time.Now(),
}); err != nil {
s.logger.Error("更新小程序状态失败", zap.Error(err))
continue
return
}
}
}, "监控小程序状态")