diff --git a/server/internal/codes/admin_support.go b/server/internal/codes/admin_support.go index 9ca8940..4823e41 100644 --- a/server/internal/codes/admin_support.go +++ b/server/internal/codes/admin_support.go @@ -31,7 +31,7 @@ func (s *Store) ListBatches(ctx context.Context, limit, offset int) ([]BatchInfo } var total int - if err := s.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM code_batches`).Scan(&total); err != nil { + if err := s.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM codes_batches`).Scan(&total); err != nil { return nil, 0, fmt.Errorf("store.ListBatches count: %w", err) } @@ -41,7 +41,7 @@ func (s *Store) ListBatches(ctx context.Context, limit, offset int) ([]BatchInfo SUM(c.status = 'redeemed'), SUM(c.status = 'void'), SUM(c.status = 'unused') - FROM code_batches b + FROM codes_batches b LEFT JOIN codes c ON c.batch_id = b.id GROUP BY b.id, b.channel, b.created_by, b.note, b.created_at ORDER BY b.id DESC @@ -73,7 +73,7 @@ func (s *Store) ListBatches(ctx context.Context, limit, offset int) ([]BatchInfo // number of codes voided. Already-redeemed codes are left untouched. func (s *Store) VoidBatch(ctx context.Context, batchID int64) (int64, error) { res, err := s.db.ExecContext(ctx, - `UPDATE codes SET status = 'void' WHERE batch_id = ? AND status = 'unused'`, batchID) + `UPDATE codes SET status = 'void', void_reason = 'batch_void' WHERE batch_id = ? AND status = 'unused'`, batchID) if err != nil { return 0, fmt.Errorf("store.VoidBatch: %w", err) } diff --git a/server/internal/codes/admin_support_sqlite_test.go b/server/internal/codes/admin_support_sqlite_test.go new file mode 100644 index 0000000..5f05157 --- /dev/null +++ b/server/internal/codes/admin_support_sqlite_test.go @@ -0,0 +1,50 @@ +package codes_test + +import ( + "context" + "testing" + "time" + + "github.com/wangjia/pangolin/server/internal/codes" +) + +func TestListBatchesAndVoidBatchOnLibTables(t *testing.T) { + ctx := context.Background() + db := openMigratedSQLite(t) + store := codes.NewStore(db) + svc := codes.NewService(store, nil, 5, time.Hour) + + res, err := svc.CreateBatch(ctx, codes.BatchRequest{ + PlanCode: codes.PlanPro, DurationDays: 30, Count: 3, + Channel: codes.ChannelManual, Note: "n", CreatedBy: "admin:1", + }) + if err != nil { + t.Fatalf("mint: %v", err) + } + + infos, total, err := store.ListBatches(ctx, 50, 0) + if err != nil { + t.Fatalf("ListBatches: %v", err) + } + if total != 1 || len(infos) != 1 { + t.Fatalf("total=%d len=%d", total, len(infos)) + } + bi := infos[0] + if bi.ID != res.BatchID || bi.Channel != codes.ChannelManual || bi.CreatedBy != "admin:1" || + bi.Note != "n" || bi.Total != 3 || bi.Unused != 3 || bi.Redeemed != 0 || bi.Void != 0 { + t.Fatalf("BatchInfo = %+v", bi) + } + + n, err := store.VoidBatch(ctx, res.BatchID) + if err != nil || n != 3 { + t.Fatalf("VoidBatch = %d, %v", n, err) + } + infos, _, err = store.ListBatches(ctx, 50, 0) + if err != nil || infos[0].Void != 3 || infos[0].Unused != 0 { + t.Fatalf("after void: %+v (err=%v)", infos[0], err) + } + // 二次 void:0 行(只动 unused)。 + if n, err := store.VoidBatch(ctx, res.BatchID); err != nil || n != 0 { + t.Fatalf("re-void = %d, %v", n, err) + } +} diff --git a/server/internal/codes/store.go b/server/internal/codes/store.go index 2d6c36e..455634b 100644 --- a/server/internal/codes/store.go +++ b/server/internal/codes/store.go @@ -43,21 +43,12 @@ const ( // SubscriptionRow mirrors the `subscriptions` DB row. type SubscriptionRow struct { - ID int64 - UserID int64 - PlanID int64 - PlanCode PlanCode - ExpiresAt time.Time - Source string -} - -// BatchRow mirrors the `code_batches` DB row. -type BatchRow struct { ID int64 - Channel BatchChannel - CreatedBy string - Note sql.NullString - CreatedAt time.Time + UserID int64 + PlanID int64 + PlanCode PlanCode + ExpiresAt time.Time + Source string } // Store wraps a *sql.DB and exposes all database operations needed by the @@ -248,4 +239,3 @@ func (s *Store) MintOne(ctx context.Context, codeHash string, ent libcodes.Entit func (s *Store) BeginTx(ctx context.Context) (*sql.Tx, error) { return s.db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelReadCommitted}) } -