From 0695c7958bb48decc473018a4e1fa2e12981bf8a Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Mon, 13 Jul 2026 07:11:17 +0800 Subject: [PATCH] =?UTF-8?q?feat(server/codes):=20GrantRewardTx=20=E5=A4=8D?= =?UTF-8?q?=E7=94=A8=20applySubscription=20=E5=8F=91=E5=A5=96=E5=8A=B1?= =?UTF-8?q?=E4=BC=9A=E5=91=98=E5=A4=A9=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/internal/codes/paygrant.go | 18 +++++++ .../codes/reward_grant_sqlite_test.go | 48 +++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 server/internal/codes/reward_grant_sqlite_test.go diff --git a/server/internal/codes/paygrant.go b/server/internal/codes/paygrant.go index 64b3a54..d3185a4 100644 --- a/server/internal/codes/paygrant.go +++ b/server/internal/codes/paygrant.go @@ -29,3 +29,21 @@ func (svc *Service) GrantPaidSubscriptionTx( _ = svc.store.WriteAuditLog(ctx, tx, fmt.Sprintf("user:%d", userID), "pay_grant", ref, string(meta)) return subID, expiresAt, nil } + +// GrantRewardTx 发放奖励会员天数(Pro,source∈{invite,task}),与付费/兑换码同一条 +// applySubscription 延时逻辑(max(到期,now)+days;已有活跃 pro 则原地顺延)。审计走 audit_log。 +func (svc *Service) GrantRewardTx( + ctx context.Context, tx *sql.Tx, userID int64, days int, source, auditAction, ref string, +) (int64, time.Time, error) { + planID, err := svc.store.GetPlanIDTx(ctx, tx, PlanPro) + if err != nil { + return 0, time.Time{}, err + } + subID, expiresAt, err := svc.applySubscription(ctx, tx, userID, planID, days, source) + if err != nil { + return 0, time.Time{}, err + } + meta, _ := json.Marshal(map[string]any{"days": days, "source": source, "sub_id": subID}) + _ = svc.store.WriteAuditLog(ctx, tx, fmt.Sprintf("user:%d", userID), auditAction, ref, string(meta)) + return subID, expiresAt, nil +} diff --git a/server/internal/codes/reward_grant_sqlite_test.go b/server/internal/codes/reward_grant_sqlite_test.go new file mode 100644 index 0000000..6bbe93a --- /dev/null +++ b/server/internal/codes/reward_grant_sqlite_test.go @@ -0,0 +1,48 @@ +package codes_test + +import ( + "context" + "testing" + "time" + + "github.com/wangjia/pangolin/server/internal/codes" +) + +// 无活跃 pro 的新用户:GrantRewardTx 新建一行 source='invite',并写一条 audit_log。 +func TestGrantRewardTx_FreshUserCreatesInviteSub(t *testing.T) { + db := openMigratedSQLite(t) + seedUser(t, db, 7) + store := codes.NewStore(db) + svc := codes.NewService(store, nil, 5, time.Hour) + ctx := context.Background() + + tx, err := store.BeginTx(ctx) + if err != nil { + t.Fatalf("BeginTx: %v", err) + } + subID, exp, err := svc.GrantRewardTx(ctx, tx, 7, 3, "invite", "invite_reward", "ref-1") + if err != nil { + t.Fatalf("grant: %v", err) + } + if err := tx.Commit(); err != nil { + t.Fatal(err) + } + if subID == 0 || exp.Before(time.Now()) { + t.Fatalf("bad sub %d exp %v", subID, exp) + } + + var src string + if err := db.QueryRow(`SELECT source FROM subscriptions WHERE id=?`, subID).Scan(&src); err != nil { + t.Fatalf("query source: %v", err) + } + if src != "invite" { + t.Fatalf("source = %q, want invite", src) + } + var n int + if err := db.QueryRow(`SELECT COUNT(*) FROM audit_log WHERE action='invite_reward' AND target='ref-1'`).Scan(&n); err != nil { + t.Fatalf("query audit_log: %v", err) + } + if n != 1 { + t.Fatalf("audit rows = %d, want 1", n) + } +}