From b847a72a6acdc967b5c285e80489b46b5be3bd81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=82=B9=E6=96=B9=E6=88=90?= Date: Thu, 13 Nov 2025 14:26:54 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E9=93=BE=E5=B7=A5=E5=85=B7=E5=B9=B6=E4=BF=AE=E5=A4=8D=E5=B0=8F?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E7=8A=B6=E6=80=81=E7=9B=91=E6=8E=A7=E9=94=99?= =?UTF-8?q?=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加测试链工具用于验证活动和工会功能,修复小程序状态监控中的错误处理逻辑 --- cmd/testchain/main.go | 549 ++++++++++++++++++++++++++++++++++++ internal/cron/cron_start.go | 2 +- 2 files changed, 550 insertions(+), 1 deletion(-) create mode 100644 cmd/testchain/main.go diff --git a/cmd/testchain/main.go b/cmd/testchain/main.go new file mode 100644 index 0000000..547852e --- /dev/null +++ b/cmd/testchain/main.go @@ -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 + } +} diff --git a/internal/cron/cron_start.go b/internal/cron/cron_start.go index 9071299..07cb532 100644 --- a/internal/cron/cron_start.go +++ b/internal/cron/cron_start.go @@ -70,7 +70,7 @@ func (s *server) Start() { "updated_at": time.Now(), }); err != nil { s.logger.Error("更新小程序状态失败", zap.Error(err)) - continue + return } } }, "监控小程序状态")