All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 5m6s
788 lines
25 KiB
Go
788 lines
25 KiB
Go
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)
|
|
}
|