fix(order): prevent duplicate subscriptions and repair invite gifts
Build docker and publish / build (20.15.1) (push) Successful in 5m6s
Build docker and publish / build (20.15.1) (push) Successful in 5m6s
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
func main() {
|
||||
dsn := flag.String("dsn", os.Getenv("PPANEL_MYSQL_DSN"), "MySQL DSN; defaults to PPANEL_MYSQL_DSN")
|
||||
flag.Parse()
|
||||
if strings.TrimSpace(*dsn) == "" {
|
||||
log.Fatal("missing DSN: pass -dsn or set PPANEL_MYSQL_DSN")
|
||||
}
|
||||
|
||||
db, err := sql.Open("mysql", *dsn)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
if err = db.Ping(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
mustPrintRows(db, "db/info", `
|
||||
SELECT NOW() AS db_now,
|
||||
(SELECT COUNT(*) FROM user) AS users,
|
||||
(SELECT COUNT(*) FROM user_subscribe) AS user_subscribes,
|
||||
(SELECT COUNT(*) FROM `+"`order`"+`) AS orders`)
|
||||
|
||||
mustPrintRows(db, "bug1/confusable-email-trials", `
|
||||
SELECT uam.user_id,
|
||||
uam.auth_identifier,
|
||||
us.id AS user_subscribe_id,
|
||||
us.order_id,
|
||||
us.status,
|
||||
us.expire_time,
|
||||
us.created_at
|
||||
FROM user_auth_methods uam
|
||||
JOIN user_subscribe us ON us.user_id = uam.user_id
|
||||
WHERE uam.auth_type = 'email'
|
||||
AND us.order_id = 0
|
||||
AND (
|
||||
uam.auth_identifier LIKE '%@gmaial.com'
|
||||
OR uam.auth_identifier LIKE '%@gmial.com'
|
||||
OR uam.auth_identifier LIKE '%@gamil.com'
|
||||
OR uam.auth_identifier LIKE '%+%@%'
|
||||
OR uam.auth_identifier REGEXP '^[^@]*\\.[^@]*@gmail\\.com$'
|
||||
)
|
||||
ORDER BY us.created_at DESC
|
||||
LIMIT 50`)
|
||||
|
||||
mustPrintRows(db, "bug2-visible-duplicate-subscriptions", `
|
||||
SELECT scoped.owner_user_id,
|
||||
COUNT(*) AS visible_subscribe_count,
|
||||
GROUP_CONCAT(scoped.user_subscribe_id ORDER BY scoped.expire_time DESC) AS user_subscribe_ids,
|
||||
GROUP_CONCAT(scoped.subscribe_id ORDER BY scoped.expire_time DESC) AS subscribe_ids,
|
||||
MAX(scoped.expire_time) AS max_expire_time
|
||||
FROM (
|
||||
SELECT us.id AS user_subscribe_id,
|
||||
us.user_id,
|
||||
COALESCE(uf.owner_user_id, us.user_id) AS owner_user_id,
|
||||
us.subscribe_id,
|
||||
us.status,
|
||||
us.expire_time,
|
||||
us.finished_at
|
||||
FROM user_subscribe us
|
||||
LEFT JOIN user_family_member ufm
|
||||
ON ufm.user_id = us.user_id AND ufm.deleted_at IS NULL AND ufm.status = 1
|
||||
LEFT JOIN user_family uf
|
||||
ON uf.id = ufm.family_id AND uf.deleted_at IS NULL AND uf.status = 1
|
||||
WHERE us.token <> ''
|
||||
AND us.status IN (0,1,2,3,4)
|
||||
AND (us.expire_time > NOW()
|
||||
OR us.finished_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)
|
||||
OR us.expire_time = FROM_UNIXTIME(0))
|
||||
) scoped
|
||||
GROUP BY scoped.owner_user_id
|
||||
HAVING COUNT(*) > 1
|
||||
ORDER BY visible_subscribe_count DESC, owner_user_id
|
||||
LIMIT 50`)
|
||||
|
||||
mustPrintRows(db, "bug2-order-subscription-owner-mismatch", `
|
||||
SELECT us.id AS user_subscribe_id,
|
||||
us.user_id AS subscribe_user_id,
|
||||
o.id AS order_id,
|
||||
o.order_no,
|
||||
o.user_id AS order_user_id,
|
||||
o.subscription_user_id,
|
||||
us.status,
|
||||
us.expire_time,
|
||||
us.created_at AS subscribe_created_at,
|
||||
o.created_at AS order_created_at
|
||||
FROM user_subscribe us
|
||||
JOIN `+"`order`"+` o ON o.id = us.order_id
|
||||
WHERE us.user_id <> o.subscription_user_id
|
||||
AND us.token <> ''
|
||||
AND us.status IN (0,1,2,3,4)
|
||||
ORDER BY us.updated_at DESC
|
||||
LIMIT 50`)
|
||||
|
||||
mustPrintRows(db, "bug3-invite-first-orders-missing-gift-days", `
|
||||
SELECT first_orders.user_id AS referee_id,
|
||||
referee.referer_id,
|
||||
first_orders.id AS order_id,
|
||||
first_orders.order_no,
|
||||
first_orders.amount,
|
||||
first_orders.created_at,
|
||||
referer.referral_percentage AS referer_referral_percentage,
|
||||
(SELECT COUNT(*) FROM system_logs sl
|
||||
WHERE sl.type = 34
|
||||
AND sl.object_id = first_orders.user_id
|
||||
AND sl.content LIKE CONCAT('%', first_orders.order_no, '%')) AS referee_gift_logs,
|
||||
(SELECT COUNT(*) FROM system_logs sl
|
||||
WHERE sl.type = 34
|
||||
AND sl.object_id = referee.referer_id
|
||||
AND sl.content LIKE CONCAT('%', first_orders.order_no, '%')) AS referer_gift_logs
|
||||
FROM (
|
||||
SELECT o.*
|
||||
FROM `+"`order`"+` o
|
||||
JOIN (
|
||||
SELECT user_id, MIN(id) AS first_order_id
|
||||
FROM `+"`order`"+`
|
||||
WHERE type IN (1,2)
|
||||
AND status IN (2,5)
|
||||
AND amount > 0
|
||||
GROUP BY user_id
|
||||
) fo ON fo.first_order_id = o.id
|
||||
) first_orders
|
||||
JOIN user referee ON referee.id = first_orders.user_id AND referee.referer_id <> 0
|
||||
JOIN user referer ON referer.id = referee.referer_id
|
||||
WHERE (
|
||||
referer.referral_percentage = 0
|
||||
AND (
|
||||
(SELECT COUNT(*) FROM system_logs sl
|
||||
WHERE sl.type = 34 AND sl.object_id = first_orders.user_id AND sl.content LIKE CONCAT('%', first_orders.order_no, '%')) = 0
|
||||
OR
|
||||
(SELECT COUNT(*) FROM system_logs sl
|
||||
WHERE sl.type = 34 AND sl.object_id = referee.referer_id AND sl.content LIKE CONCAT('%', first_orders.order_no, '%')) = 0
|
||||
)
|
||||
)
|
||||
OR (
|
||||
referer.referral_percentage > 0
|
||||
AND (SELECT COUNT(*) FROM system_logs sl
|
||||
WHERE sl.type = 34 AND sl.object_id = first_orders.user_id AND sl.content LIKE CONCAT('%', first_orders.order_no, '%')) = 0
|
||||
)
|
||||
ORDER BY first_orders.created_at DESC
|
||||
LIMIT 50`)
|
||||
}
|
||||
|
||||
func mustPrintRows(db *sql.DB, title string, query string) {
|
||||
fmt.Printf("\n== %s ==\n", title)
|
||||
rows, err := db.Query(query)
|
||||
if err != nil {
|
||||
log.Fatalf("%s: %v", title, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
cols, err := rows.Columns()
|
||||
if err != nil {
|
||||
log.Fatalf("%s columns: %v", title, err)
|
||||
}
|
||||
fmt.Println(strings.Join(cols, "\t"))
|
||||
|
||||
values := make([]sql.NullString, len(cols))
|
||||
args := make([]any, len(cols))
|
||||
for i := range values {
|
||||
args[i] = &values[i]
|
||||
}
|
||||
count := 0
|
||||
for rows.Next() {
|
||||
if err := rows.Scan(args...); err != nil {
|
||||
log.Fatalf("%s scan: %v", title, err)
|
||||
}
|
||||
out := make([]string, len(cols))
|
||||
for i, value := range values {
|
||||
if value.Valid {
|
||||
out[i] = value.String
|
||||
} else {
|
||||
out[i] = "NULL"
|
||||
}
|
||||
}
|
||||
fmt.Println(strings.Join(out, "\t"))
|
||||
count++
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
log.Fatalf("%s rows: %v", title, err)
|
||||
}
|
||||
if count == 0 {
|
||||
fmt.Println("(none)")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
type duplicateGroup struct {
|
||||
OwnerUserID int64 `json:"owner_user_id"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
type subscriptionRow struct {
|
||||
ID int64 `json:"id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
OrderID int64 `json:"order_id"`
|
||||
SubscribeID int64 `json:"subscribe_id"`
|
||||
ExpireTime time.Time `json:"expire_time"`
|
||||
Traffic int64 `json:"traffic"`
|
||||
Download int64 `json:"download"`
|
||||
Upload int64 `json:"upload"`
|
||||
ExpiredDownload int64 `json:"expired_download"`
|
||||
ExpiredUpload int64 `json:"expired_upload"`
|
||||
Status uint8 `json:"status"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type mergePlan struct {
|
||||
OwnerUserID int64 `json:"owner_user_id"`
|
||||
Keep subscriptionRow `json:"keep"`
|
||||
Merge []subscriptionRow `json:"merge"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
dsn := flag.String("dsn", os.Getenv("PPANEL_MYSQL_DSN"), "MySQL DSN; defaults to PPANEL_MYSQL_DSN")
|
||||
execute := flag.Bool("execute", false, "apply changes; default is dry-run")
|
||||
flag.Parse()
|
||||
|
||||
if strings.TrimSpace(*dsn) == "" {
|
||||
log.Fatal("missing DSN: pass -dsn or set PPANEL_MYSQL_DSN")
|
||||
}
|
||||
|
||||
db, err := sql.Open("mysql", *dsn)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
groups, err := findDuplicateGroups(db)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
plans := make([]mergePlan, 0, len(groups))
|
||||
for _, group := range groups {
|
||||
plan, err := buildPlan(db, group.OwnerUserID)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if len(plan.Merge) > 0 {
|
||||
plans = append(plans, plan)
|
||||
}
|
||||
}
|
||||
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
if err := enc.Encode(plans); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if !*execute {
|
||||
fmt.Fprintf(os.Stderr, "dry-run only: %d duplicate owner groups found\n", len(plans))
|
||||
return
|
||||
}
|
||||
|
||||
for _, plan := range plans {
|
||||
if err := applyPlan(db, plan); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "merged %d duplicate owner groups\n", len(plans))
|
||||
}
|
||||
|
||||
func findDuplicateGroups(db *sql.DB) ([]duplicateGroup, error) {
|
||||
rows, err := db.Query(`
|
||||
SELECT owner_user_id, COUNT(1) AS cnt
|
||||
FROM (
|
||||
SELECT us.id,
|
||||
COALESCE(uf.owner_user_id, us.user_id) AS owner_user_id
|
||||
FROM user_subscribe us
|
||||
LEFT JOIN user_family_member ufm
|
||||
ON ufm.user_id = us.user_id AND ufm.deleted_at IS NULL AND ufm.status = 1
|
||||
LEFT JOIN user_family uf
|
||||
ON uf.id = ufm.family_id AND uf.deleted_at IS NULL AND uf.status = 1
|
||||
WHERE us.token <> ''
|
||||
AND us.status IN (0, 1, 2, 3, 4)
|
||||
) scoped
|
||||
GROUP BY owner_user_id
|
||||
HAVING COUNT(1) > 1
|
||||
ORDER BY owner_user_id`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var groups []duplicateGroup
|
||||
for rows.Next() {
|
||||
var g duplicateGroup
|
||||
if err := rows.Scan(&g.OwnerUserID, &g.Count); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
groups = append(groups, g)
|
||||
}
|
||||
return groups, rows.Err()
|
||||
}
|
||||
|
||||
func buildPlan(db *sql.DB, ownerUserID int64) (mergePlan, error) {
|
||||
rows, err := db.Query(`
|
||||
SELECT us.id, us.user_id, us.order_id, us.subscribe_id, us.expire_time, us.traffic,
|
||||
us.download, us.upload, us.expired_download, us.expired_upload, us.status, us.updated_at
|
||||
FROM user_subscribe us
|
||||
LEFT JOIN user_family_member ufm
|
||||
ON ufm.user_id = us.user_id AND ufm.deleted_at IS NULL AND ufm.status = 1
|
||||
LEFT JOIN user_family uf
|
||||
ON uf.id = ufm.family_id AND uf.deleted_at IS NULL AND uf.status = 1
|
||||
WHERE COALESCE(uf.owner_user_id, us.user_id) = ?
|
||||
AND us.token <> ''
|
||||
AND us.status IN (0, 1, 2, 3, 4)
|
||||
ORDER BY us.expire_time DESC, us.updated_at DESC, us.id DESC`, ownerUserID)
|
||||
if err != nil {
|
||||
return mergePlan{}, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var all []subscriptionRow
|
||||
for rows.Next() {
|
||||
var r subscriptionRow
|
||||
if err := rows.Scan(&r.ID, &r.UserID, &r.OrderID, &r.SubscribeID, &r.ExpireTime, &r.Traffic, &r.Download, &r.Upload, &r.ExpiredDownload, &r.ExpiredUpload, &r.Status, &r.UpdatedAt); err != nil {
|
||||
return mergePlan{}, err
|
||||
}
|
||||
all = append(all, r)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return mergePlan{}, err
|
||||
}
|
||||
if len(all) == 0 {
|
||||
return mergePlan{OwnerUserID: ownerUserID}, nil
|
||||
}
|
||||
|
||||
keep := all[0]
|
||||
for _, r := range all[1:] {
|
||||
keep.Download += r.Download
|
||||
keep.Upload += r.Upload
|
||||
keep.ExpiredDownload += r.ExpiredDownload
|
||||
keep.ExpiredUpload += r.ExpiredUpload
|
||||
if r.Traffic > keep.Traffic {
|
||||
keep.Traffic = r.Traffic
|
||||
}
|
||||
}
|
||||
for _, r := range all {
|
||||
if r.UpdatedAt.After(keep.UpdatedAt) {
|
||||
keep.SubscribeID = r.SubscribeID
|
||||
}
|
||||
}
|
||||
|
||||
return mergePlan{OwnerUserID: ownerUserID, Keep: keep, Merge: all[1:]}, nil
|
||||
}
|
||||
|
||||
func applyPlan(db *sql.DB, plan mergePlan) error {
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
if _, err = tx.Exec(`
|
||||
UPDATE user_subscribe
|
||||
SET user_id = ?, subscribe_id = ?, traffic = ?, download = ?, upload = ?,
|
||||
expired_download = ?, expired_upload = ?, status = 1, note = CONCAT(COALESCE(note, ''), ' [merged duplicate subscriptions]')
|
||||
WHERE id = ?`,
|
||||
plan.OwnerUserID, plan.Keep.SubscribeID, plan.Keep.Traffic, plan.Keep.Download, plan.Keep.Upload,
|
||||
plan.Keep.ExpiredDownload, plan.Keep.ExpiredUpload, plan.Keep.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, r := range plan.Merge {
|
||||
if _, err = tx.Exec(`
|
||||
UPDATE user_subscribe
|
||||
SET status = 5, note = CONCAT(COALESCE(note, ''), ' [merged into subscription #', ?, ']')
|
||||
WHERE id = ?`, plan.Keep.ID, r.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
@@ -0,0 +1,787 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user