package main import ( "context" "encoding/json" "flag" "fmt" "os" "os/exec" "strings" "time" "github.com/hibiken/asynq" "github.com/perfect-panel/server/internal/config" authlogic "github.com/perfect-panel/server/internal/logic/auth" modelLog "github.com/perfect-panel/server/internal/model/log" modelOrder "github.com/perfect-panel/server/internal/model/order" modelSubscribe "github.com/perfect-panel/server/internal/model/subscribe" modelUser "github.com/perfect-panel/server/internal/model/user" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/pkg/conf" "github.com/perfect-panel/server/pkg/orm" "github.com/perfect-panel/server/pkg/uuidx" orderLogic "github.com/perfect-panel/server/queue/logic/order" queueTypes "github.com/perfect-panel/server/queue/types" "github.com/redis/go-redis/v9" "gorm.io/gorm" ) const marker = "codex-replay-business-bugs" func main() { var ( configPath = flag.String("config", "etc/ppanel.yaml", "ppanel config path for test server DB/Redis") dsn = flag.String("dsn", "", "optional MySQL DSN override: user:pass@tcp(host:3306)/db?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai") writeDB = flag.Bool("write-db", false, "create isolated test rows and execute activation replay against the configured test DB") force = flag.Bool("force", false, "allow -write-db even when the config name does not clearly look like test/dev/staging") keep = flag.Bool("keep", false, "keep replay rows for manual inspection") cleanupOnly = flag.Bool("cleanup-only", false, "delete leftover replay rows by marker and exit") skipCodeTests = flag.Bool("skip-code-tests", false, "skip go test checks") ) flag.Parse() ctx := context.Background() started := time.Now() fmt.Println("== replay business bug tests ==") fmt.Printf("marker: %s\n", marker) if !*skipCodeTests { must(runCodeTests()) } cfg := loadConfig(*configPath, *dsn) runEmailTrialAssertions(cfg) if *cleanupOnly { env := mustNewReplayEnv(ctx, cfg) env.cleanupByMarker(ctx) return } if !*writeDB { fmt.Println("\nDB replay skipped. Add -write-db to create isolated rows in the TEST database and run activation flows.") fmt.Println("Example:") fmt.Printf(" go run scripts/replay_business_bugs.go -config %s -write-db\n", *configPath) return } if looksLikeProduction(cfg) && !*force { fatalf("refusing to write DB because config does not look like a test environment: db=%s host=%s; add -force only on the test server", cfg.MySQL.Dbname, cfg.Site.Host) } env := mustNewReplayEnv(ctx, cfg) if !*keep { defer env.cleanup(ctx) } must(env.replaySingleSubscription(ctx)) must(env.replayInviteRulesMatrix(ctx)) must(env.replayFamilyInviteGiftToOwner(ctx)) fmt.Printf("\nPASS all replay checks in %s\n", time.Since(started).Round(time.Millisecond)) if *keep { fmt.Println("Replay rows kept for inspection. Delete rows with remark/name/order_no containing:", marker) } } func runCodeTests() error { fmt.Println("\n-- code-level tests --") args := []string{"test", "./internal/logic/auth", "./internal/logic/common", "./internal/logic/public/order", "./queue/logic/order", } cmd := exec.Command("go", args...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { return fmt.Errorf("go test failed: %w", err) } fmt.Println("PASS code-level tests") return nil } func loadConfig(path, dsn string) config.Config { var cfg config.Config conf.MustLoad(path, &cfg) if dsn != "" { cfg.MySQL = parseDSN(dsn) } return cfg } func parseDSN(dsn string) orm.Config { cfg := orm.ParseDSN(dsn) if cfg == nil { fatalf("invalid dsn") } return *cfg } func runEmailTrialAssertions(cfg config.Config) { fmt.Println("\n-- bug1 email trial whitelist assertions --") cfg.Register.EnableTrial = true cfg.Register.EnableTrialEmailWhitelist = true if cfg.Register.TrialEmailDomainWhitelist == "" { cfg.Register.TrialEmailDomainWhitelist = "gmail.com,163.com" } cases := []struct { email string want bool }{ {"1.2.3.4xxx@gmaial.com", false}, {"a.b.c@gmail.com", false}, {"user+tag@gmail.com", false}, {"user@fake.gmail.com", false}, {"normaluser@gmail.com", true}, } for _, tc := range cases { got := authlogic.ShouldGrantTrialForEmail(cfg.Register, tc.email) if got != tc.want { fatalf("email trial assertion failed: email=%s got=%v want=%v", tc.email, got, tc.want) } fmt.Printf("PASS %-32s grant=%v\n", tc.email, got) } } type replayEnv struct { db *gorm.DB rds *redis.Client cfg config.Config svcCtx *svc.ServiceContext ids struct { users []int64 subscribes []int64 plans []int64 orders []int64 logs []int64 } } func mustNewReplayEnv(ctx context.Context, cfg config.Config) *replayEnv { fmt.Println("\n-- connecting test DB/Redis --") db, err := orm.ConnectMysql(orm.Mysql{Config: cfg.MySQL}) must(err) rds := redis.NewClient(&redis.Options{ Addr: cfg.Redis.Host, Password: cfg.Redis.Pass, DB: cfg.Redis.DB, PoolSize: cfg.Redis.PoolSize, MinIdleConns: cfg.Redis.MinIdleConns, }) must(rds.Ping(ctx).Err()) svcCtx := &svc.ServiceContext{ DB: db, Redis: rds, Config: cfg, UserModel: modelUser.NewModel(db, rds), OrderModel: modelOrder.NewModel(db, rds), SubscribeModel: modelSubscribe.NewModel(db, rds), LogModel: modelLog.NewModel(db), } fmt.Printf("connected: mysql=%s/%s redis=%s\n", cfg.MySQL.Addr, cfg.MySQL.Dbname, cfg.Redis.Host) return &replayEnv{db: db, rds: rds, cfg: cfg, svcCtx: svcCtx} } func (e *replayEnv) replaySingleSubscription(ctx context.Context) error { fmt.Println("\n-- bug2 replay: paid purchase must reuse existing subscription --") planA, planB, err := e.createPlans(ctx, "bug2") if err != nil { return err } owner, err := e.createUser(ctx, "bug2-owner", 0, 0) if err != nil { return err } existing, err := e.createUserSubscribe(ctx, owner.Id, 0, planA.Id, time.Now().Add(7*24*time.Hour)) if err != nil { return err } order, err := e.createPaidOrder(ctx, owner.Id, owner.Id, planB.Id, true, "bug2") if err != nil { return err } payload, _ := json.Marshal(queueTypes.ForthwithActivateOrderPayload{OrderNo: order.OrderNo}) worker := orderLogic.NewActivateOrderLogic(e.svcCtx) if err = worker.ProcessTask(ctx, asynq.NewTask(queueTypes.ForthwithActivateOrder, payload)); err != nil { return err } if err = worker.ProcessTask(ctx, asynq.NewTask(queueTypes.ForthwithActivateOrder, payload)); err != nil { return err } var rows []modelUser.Subscribe if err = e.db.WithContext(ctx). Where("user_id = ? AND token <> '' AND status IN ?", owner.Id, []int{0, 1, 2, 3, 4}). Order("id ASC"). Find(&rows).Error; err != nil { return err } if len(rows) != 1 { return fmt.Errorf("bug2 failed: expected one visible subscription, got %d", len(rows)) } if rows[0].Id != existing.Id { return fmt.Errorf("bug2 failed: expected original subscription id=%d to be reused, got id=%d", existing.Id, rows[0].Id) } if rows[0].SubscribeId != planB.Id || rows[0].OrderId != order.Id { return fmt.Errorf("bug2 failed: reused subscription not updated, subscribe_id=%d order_id=%d", rows[0].SubscribeId, rows[0].OrderId) } fmt.Printf("PASS user=%d user_subscribe=%d plan %d -> %d order=%s\n", owner.Id, rows[0].Id, planA.Id, planB.Id, order.OrderNo) return nil } func (e *replayEnv) replayInviteGiftDays(ctx context.Context) error { fmt.Println("\n-- bug3 replay: commission=0 invite should grant gift days to both users --") giftDays := e.cfg.Invite.GiftDays if giftDays <= 0 { giftDays = 2 e.svcCtx.Config.Invite.GiftDays = giftDays } e.svcCtx.Config.Invite.ReferralPercentage = 0 e.svcCtx.Config.Invite.OnlyFirstPurchase = true planA, _, err := e.createPlans(ctx, "bug3") if err != nil { return err } referer, err := e.createUser(ctx, "bug3-referer", 0, 0) if err != nil { return err } referee, err := e.createUser(ctx, "bug3-referee", referer.Id, 0) if err != nil { return err } baseExpire := time.Now().Add(10 * 24 * time.Hour).Truncate(time.Millisecond) refererSub, err := e.createUserSubscribe(ctx, referer.Id, 0, planA.Id, baseExpire) if err != nil { return err } refereeSub, err := e.createUserSubscribe(ctx, referee.Id, 0, planA.Id, baseExpire) if err != nil { return err } order, err := e.createPaidOrder(ctx, referee.Id, referee.Id, planA.Id, true, "bug3") if err != nil { return err } payload, _ := json.Marshal(queueTypes.ForthwithActivateOrderPayload{OrderNo: order.OrderNo}) worker := orderLogic.NewActivateOrderLogic(e.svcCtx) if err = worker.ProcessTask(ctx, asynq.NewTask(queueTypes.ForthwithActivateOrder, payload)); err != nil { return err } if err = e.waitForGiftLogs(ctx, order.OrderNo, referer.Id, referee.Id); err != nil { return err } var refererAfter, refereeAfter modelUser.Subscribe if err = e.db.WithContext(ctx).First(&refererAfter, refererSub.Id).Error; err != nil { return err } if err = e.db.WithContext(ctx).First(&refereeAfter, refereeSub.Id).Error; err != nil { return err } minRefererExpire := baseExpire.Add(time.Duration(giftDays) * 24 * time.Hour) if refererAfter.ExpireTime.Before(minRefererExpire.Add(-time.Second)) { return fmt.Errorf("bug3 failed: referer expire not increased by gift days, got=%s want>=%s", refererAfter.ExpireTime, minRefererExpire) } if !refereeAfter.ExpireTime.After(baseExpire) { return fmt.Errorf("bug3 failed: referee expire did not increase, got=%s base=%s", refereeAfter.ExpireTime, baseExpire) } // Idempotency: repeat the same order task and make sure gift logs are still one per user. if err = worker.ProcessTask(ctx, asynq.NewTask(queueTypes.ForthwithActivateOrder, payload)); err != nil { return err } var giftCount int64 if err = e.db.WithContext(ctx).Model(&modelLog.SystemLog{}). Where("type = ? AND object_id IN ? AND content LIKE ?", modelLog.TypeGift.Uint8(), []int64{referer.Id, referee.Id}, "%"+order.OrderNo+"%"). Count(&giftCount).Error; err != nil { return err } if giftCount != 2 { return fmt.Errorf("bug3 failed: expected 2 gift logs after duplicate task, got %d", giftCount) } fmt.Printf("PASS referer=%d referee=%d order=%s gift_days=%d logs=%d\n", referer.Id, referee.Id, order.OrderNo, giftDays, giftCount) return nil } func (e *replayEnv) replayInviteRulesMatrix(ctx context.Context) error { fmt.Println("\n-- bug3 replay matrix: invite gift/commission rules --") giftDays := e.cfg.Invite.GiftDays if giftDays <= 0 { giftDays = 2 } e.svcCtx.Config.Invite.GiftDays = giftDays e.svcCtx.Config.Invite.OnlyFirstPurchase = false planA, _, err := e.createPlans(ctx, "bug3-matrix") if err != nil { return err } cases := []struct { name string hasReferer bool globalReferralPct int64 isNewOrder bool wantGiftLogs int64 wantCommissionLogs int64 wantCommission int64 }{ { name: "no invite relation first order no gift", hasReferer: false, isNewOrder: true, wantGiftLogs: 0, }, { name: "ordinary invite commission 0 first order gifts both", hasReferer: true, isNewOrder: true, wantGiftLogs: 2, }, { name: "ordinary invite commission 0 non-first order no gift", hasReferer: true, isNewOrder: false, wantGiftLogs: 0, }, { name: "channel commission positive first order gifts referee only", hasReferer: true, globalReferralPct: 10, isNewOrder: true, wantGiftLogs: 1, wantCommissionLogs: 1, wantCommission: 59, }, { name: "channel commission positive non-first order commission only", hasReferer: true, globalReferralPct: 10, isNewOrder: false, wantGiftLogs: 0, wantCommissionLogs: 1, wantCommission: 59, }, } for idx, tc := range cases { e.svcCtx.Config.Invite.ReferralPercentage = tc.globalReferralPct scope := fmt.Sprintf("bug3-rule-%d", idx+1) var referer *modelUser.User if tc.hasReferer { referer, err = e.createUser(ctx, scope+"-referer", 0, 0) if err != nil { return err } if _, err = e.createUserSubscribe(ctx, referer.Id, 0, planA.Id, time.Now().Add(10*24*time.Hour)); err != nil { return err } } var refererID int64 if referer != nil { refererID = referer.Id } referee, err := e.createUser(ctx, scope+"-referee", refererID, 0) if err != nil { return err } if _, err = e.createUserSubscribe(ctx, referee.Id, 0, planA.Id, time.Now().Add(10*24*time.Hour)); err != nil { return err } order, err := e.createPaidOrder(ctx, referee.Id, referee.Id, planA.Id, tc.isNewOrder, scope) if err != nil { return err } if err = e.activateOrderTwice(ctx, order.OrderNo); err != nil { return fmt.Errorf("%s: %w", tc.name, err) } if err = e.waitForLogCounts(ctx, order.OrderNo, tc.wantGiftLogs, tc.wantCommissionLogs); err != nil { return fmt.Errorf("%s: %w", tc.name, err) } giftLogs, err := e.countLogs(ctx, modelLog.TypeGift.Uint8(), order.OrderNo) if err != nil { return err } commissionLogs, err := e.countLogs(ctx, modelLog.TypeCommission.Uint8(), order.OrderNo) if err != nil { return err } if giftLogs != tc.wantGiftLogs { return fmt.Errorf("%s: expected gift logs=%d got=%d", tc.name, tc.wantGiftLogs, giftLogs) } if commissionLogs != tc.wantCommissionLogs { return fmt.Errorf("%s: expected commission logs=%d got=%d", tc.name, tc.wantCommissionLogs, commissionLogs) } if referer != nil && tc.wantCommission > 0 { var after modelUser.User if err = e.db.WithContext(ctx).First(&after, referer.Id).Error; err != nil { return err } if after.Commission != tc.wantCommission { return fmt.Errorf("%s: expected referer commission=%d got=%d", tc.name, tc.wantCommission, after.Commission) } } fmt.Printf("PASS %-58s gifts=%d commission_logs=%d\n", tc.name, giftLogs, commissionLogs) } return nil } func (e *replayEnv) replayFamilyInviteGiftToOwner(ctx context.Context) error { fmt.Println("\n-- bug3 family replay: member purchase gift days go to owner --") giftDays := e.cfg.Invite.GiftDays if giftDays <= 0 { giftDays = 2 } e.svcCtx.Config.Invite.GiftDays = giftDays e.svcCtx.Config.Invite.ReferralPercentage = 0 e.svcCtx.Config.Invite.OnlyFirstPurchase = false planA, _, err := e.createPlans(ctx, "bug3-family") if err != nil { return err } referer, err := e.createUser(ctx, "bug3-family-referer", 0, 0) if err != nil { return err } owner, err := e.createUser(ctx, "bug3-family-owner", 0, 0) if err != nil { return err } member, err := e.createUser(ctx, "bug3-family-member", referer.Id, 0) if err != nil { return err } if err = e.createFamily(ctx, owner.Id, member.Id); err != nil { return err } baseExpire := time.Now().Add(10 * 24 * time.Hour).Truncate(time.Millisecond) ownerSub, err := e.createUserSubscribe(ctx, owner.Id, 0, planA.Id, baseExpire) if err != nil { return err } memberSub, err := e.createUserSubscribe(ctx, member.Id, 0, planA.Id, baseExpire) if err != nil { return err } refererSub, err := e.createUserSubscribe(ctx, referer.Id, 0, planA.Id, baseExpire) if err != nil { return err } order, err := e.createPaidOrder(ctx, member.Id, owner.Id, planA.Id, true, "bug3-family") if err != nil { return err } if err = e.activateOrderTwice(ctx, order.OrderNo); err != nil { return err } if err = e.waitForLogCounts(ctx, order.OrderNo, 2, 0); err != nil { return err } var ownerAfter, memberAfter, refererAfter modelUser.Subscribe if err = e.db.WithContext(ctx).First(&ownerAfter, ownerSub.Id).Error; err != nil { return err } if err = e.db.WithContext(ctx).First(&memberAfter, memberSub.Id).Error; err != nil { return err } if err = e.db.WithContext(ctx).First(&refererAfter, refererSub.Id).Error; err != nil { return err } if !ownerAfter.ExpireTime.After(baseExpire) { return fmt.Errorf("family gift failed: owner expire not increased") } if !refererAfter.ExpireTime.After(baseExpire) { return fmt.Errorf("family gift failed: referer expire not increased") } if memberAfter.ExpireTime.After(baseExpire.Add(time.Second)) { return fmt.Errorf("family gift failed: member subscription should not receive gift days") } var memberGiftLogs int64 if err = e.db.WithContext(ctx).Model(&modelLog.SystemLog{}). Where("type = ? AND object_id = ? AND content LIKE ?", modelLog.TypeGift.Uint8(), member.Id, "%"+order.OrderNo+"%"). Count(&memberGiftLogs).Error; err != nil { return err } if memberGiftLogs != 0 { return fmt.Errorf("family gift failed: expected no member gift logs, got %d", memberGiftLogs) } fmt.Printf("PASS family member purchase gift target owner owner=%d member=%d referer=%d gift_days=%d\n", owner.Id, member.Id, referer.Id, giftDays) return nil } func (e *replayEnv) activateOrderTwice(ctx context.Context, orderNo string) error { payload, _ := json.Marshal(queueTypes.ForthwithActivateOrderPayload{OrderNo: orderNo}) worker := orderLogic.NewActivateOrderLogic(e.svcCtx) if err := worker.ProcessTask(ctx, asynq.NewTask(queueTypes.ForthwithActivateOrder, payload)); err != nil { return err } return worker.ProcessTask(ctx, asynq.NewTask(queueTypes.ForthwithActivateOrder, payload)) } func (e *replayEnv) waitForLogCounts(ctx context.Context, orderNo string, wantGiftLogs, wantCommissionLogs int64) error { deadline := time.Now().Add(8 * time.Second) for { giftLogs, err := e.countLogs(ctx, modelLog.TypeGift.Uint8(), orderNo) if err != nil { return err } commissionLogs, err := e.countLogs(ctx, modelLog.TypeCommission.Uint8(), orderNo) if err != nil { return err } if giftLogs >= wantGiftLogs && commissionLogs >= wantCommissionLogs { if wantGiftLogs == 0 && wantCommissionLogs == 0 { time.Sleep(500 * time.Millisecond) } return nil } if time.Now().After(deadline) { return fmt.Errorf("timed out waiting for logs: order=%s gift=%d/%d commission=%d/%d", orderNo, giftLogs, wantGiftLogs, commissionLogs, wantCommissionLogs) } time.Sleep(100 * time.Millisecond) } } func (e *replayEnv) countLogs(ctx context.Context, logType uint8, orderNo string) (int64, error) { var count int64 err := e.db.WithContext(ctx).Model(&modelLog.SystemLog{}). Where("type = ? AND content LIKE ?", logType, "%"+orderNo+"%"). Count(&count).Error return count, err } func (e *replayEnv) waitForGiftLogs(ctx context.Context, orderNo string, userIDs ...int64) error { deadline := time.Now().Add(5 * time.Second) for time.Now().Before(deadline) { var count int64 if err := e.db.WithContext(ctx).Model(&modelLog.SystemLog{}). Where("type = ? AND object_id IN ? AND content LIKE ?", modelLog.TypeGift.Uint8(), userIDs, "%"+orderNo+"%"). Count(&count).Error; err != nil { return err } if count == int64(len(userIDs)) { return nil } time.Sleep(100 * time.Millisecond) } return fmt.Errorf("timed out waiting for gift logs for order=%s", orderNo) } func (e *replayEnv) createPlans(ctx context.Context, scope string) (*modelSubscribe.Subscribe, *modelSubscribe.Subscribe, error) { a := &modelSubscribe.Subscribe{ Name: marker + "-" + scope + "-A", Language: "en", UnitPrice: 599, UnitTime: "Month", Traffic: 1024 * 1024 * 1024, Inventory: -1, Quota: 0, NodeGroupIds: modelSubscribe.JSONInt64Slice{}, } b := &modelSubscribe.Subscribe{ Name: marker + "-" + scope + "-B", Language: "en", UnitPrice: 699, UnitTime: "Month", Traffic: 2 * 1024 * 1024 * 1024, Inventory: -1, Quota: 0, NodeGroupIds: modelSubscribe.JSONInt64Slice{}, } if err := e.db.WithContext(ctx).Create(a).Error; err != nil { return nil, nil, err } if err := e.db.WithContext(ctx).Create(b).Error; err != nil { return nil, nil, err } e.ids.plans = append(e.ids.plans, a.Id, b.Id) return a, b, nil } func (e *replayEnv) createUser(ctx context.Context, scope string, refererID int64, referralPercentage uint8) (*modelUser.User, error) { onlyFirst := true enable := true isAdmin := false u := &modelUser.User{ Password: marker, Algo: "default", Salt: "default", RefererId: refererID, ReferralPercentage: referralPercentage, OnlyFirstPurchase: &onlyFirst, Enable: &enable, IsAdmin: &isAdmin, EnableBalanceNotify: &enable, EnableLoginNotify: &enable, EnableSubscribeNotify: &enable, EnableTradeNotify: &enable, Remark: marker + "-" + scope, } if err := e.db.WithContext(ctx).Create(u).Error; err != nil { return nil, err } u.ReferCode = uuidx.UserInviteCode(u.Id) if err := e.db.WithContext(ctx).Model(&modelUser.User{}).Where("id = ?", u.Id).Update("refer_code", u.ReferCode).Error; err != nil { return nil, err } e.ids.users = append(e.ids.users, u.Id) return u, nil } func (e *replayEnv) createFamily(ctx context.Context, ownerID, memberID int64) error { now := time.Now() family := &modelUser.UserFamily{ OwnerUserId: ownerID, MaxMembers: modelUser.DefaultFamilyMaxSize, Status: modelUser.FamilyStatusActive, } if err := e.db.WithContext(ctx).Create(family).Error; err != nil { return err } members := []modelUser.UserFamilyMember{ { FamilyId: family.Id, UserId: ownerID, Role: modelUser.FamilyRoleOwner, Status: modelUser.FamilyMemberActive, JoinSource: marker, JoinedAt: now, }, { FamilyId: family.Id, UserId: memberID, Role: modelUser.FamilyRoleMember, Status: modelUser.FamilyMemberActive, JoinSource: marker, JoinedAt: now, }, } return e.db.WithContext(ctx).Create(&members).Error } func (e *replayEnv) createUserSubscribe(ctx context.Context, userID, orderID, planID int64, expire time.Time) (*modelUser.Subscribe, error) { groupLocked := false sub := &modelUser.Subscribe{ UserId: userID, OrderId: orderID, SubscribeId: planID, GroupLocked: &groupLocked, StartTime: time.Now().Add(-time.Hour), ExpireTime: expire, Traffic: 1024 * 1024 * 1024, Token: marker + "-" + uuidx.NewUUID().String(), UUID: uuidx.NewUUID().String(), Status: 1, Note: marker, } if err := e.db.WithContext(ctx).Create(sub).Error; err != nil { return nil, err } e.ids.subscribes = append(e.ids.subscribes, sub.Id) return sub, nil } func (e *replayEnv) createPaidOrder(ctx context.Context, userID, subscriptionUserID, planID int64, isNew bool, scope string) (*modelOrder.Order, error) { orderNo := fmt.Sprintf("%s-%s-%d", marker, scope, time.Now().UnixNano()) order := &modelOrder.Order{ UserId: userID, SubscriptionUserId: subscriptionUserID, OrderNo: orderNo, Type: 1, Quantity: 1, Price: 599, Amount: 599, Status: 2, SubscribeId: planID, Method: "replay", IsNew: isNew, } if err := e.db.WithContext(ctx).Create(order).Error; err != nil { return nil, err } e.ids.orders = append(e.ids.orders, order.Id) return order, nil } func (e *replayEnv) cleanup(ctx context.Context) { fmt.Println("\n-- cleanup replay rows --") e.cleanupByMarker(ctx) if len(e.ids.subscribes) > 0 { _ = e.db.WithContext(ctx).Where("id IN ?", e.ids.subscribes).Delete(&modelUser.Subscribe{}).Error } if len(e.ids.orders) > 0 { _ = e.db.WithContext(ctx).Where("id IN ?", e.ids.orders).Delete(&modelOrder.Order{}).Error } if len(e.ids.plans) > 0 { _ = e.db.WithContext(ctx).Where("id IN ?", e.ids.plans).Delete(&modelSubscribe.Subscribe{}).Error } if len(e.ids.users) > 0 { _ = e.db.WithContext(ctx).Unscoped().Where("id IN ?", e.ids.users).Delete(&modelUser.User{}).Error } fmt.Println("cleanup done") } func (e *replayEnv) cleanupByMarker(ctx context.Context) { _ = e.db.WithContext(ctx). Where("join_source = ?", marker). Delete(&modelUser.UserFamilyMember{}).Error _ = e.db.WithContext(ctx). Where("owner_user_id IN (SELECT id FROM `user` WHERE remark LIKE ?)", marker+"%"). Delete(&modelUser.UserFamily{}).Error _ = e.db.WithContext(ctx). Where("type IN (33, 34) AND content LIKE ?", "%"+marker+"%"). Delete(&modelLog.SystemLog{}).Error _ = e.db.WithContext(ctx). Where("order_no LIKE ?", marker+"%"). Delete(&modelOrder.Order{}).Error _ = e.db.WithContext(ctx). Where("note = ? OR token LIKE ?", marker, marker+"%"). Delete(&modelUser.Subscribe{}).Error _ = e.db.WithContext(ctx). Where("name LIKE ?", marker+"%"). Delete(&modelSubscribe.Subscribe{}).Error _ = e.db.WithContext(ctx).Unscoped(). Where("remark LIKE ?", marker+"%"). Delete(&modelUser.User{}).Error } func looksLikeProduction(cfg config.Config) bool { joined := strings.ToLower(strings.Join([]string{cfg.MySQL.Dbname, cfg.Site.Host, cfg.Host}, " ")) if strings.Contains(joined, "prod") || strings.Contains(joined, "production") { return true } if cfg.Debug { return false } if strings.Contains(joined, "test") || strings.Contains(joined, "dev") || strings.Contains(joined, "staging") { return false } return true } func must(err error) { if err != nil { fatalf("%v", err) } } func fatalf(format string, args ...interface{}) { fmt.Fprintf(os.Stderr, "FAIL: "+format+"\n", args...) os.Exit(1) }