Files
digital-psychology/apps/api/internal/service/admin/redemption.go
T
jackyu66gitandCursor 1eeb0b00e7 feat(ECR-015): RedemptionCode 兑换码并 Closed
批次生成/作废、C 端兑码延长会员;admin-h5 /codes。
Loop continuous。Next:ECR-016 UserIntelligence。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 18:16:13 +08:00

97 lines
2.5 KiB
Go

package admin
import (
"context"
"crypto/rand"
"encoding/hex"
"encoding/json"
"strings"
"github.com/google/uuid"
"github.com/yuxingu/digital-psychology/apps/api/internal/repository"
)
var (
ErrBadBatchQty = errString("quantity must be 1..100")
ErrCodeDisable = errString("code not unused")
ErrBatchNotFound = errString("batch not found")
)
// CreateRedemptionBatch generates codes for a membership plan.
func (s *Service) CreateRedemptionBatch(
ctx context.Context, adminID uuid.UUID, label, planCode string, qty int,
) (*repository.RedemptionBatch, []repository.RedemptionCodeRow, error) {
label = strings.TrimSpace(label)
planCode = strings.TrimSpace(planCode)
if label == "" || qty < 1 || qty > 100 {
return nil, nil, ErrBadBatchQty
}
if _, err := s.GetMembershipPlan(ctx, planCode); err != nil {
return nil, nil, err
}
codes := make([]string, 0, qty)
seen := map[string]struct{}{}
for len(codes) < qty {
c, err := newRedemptionCode()
if err != nil {
return nil, nil, err
}
if _, ok := seen[c]; ok {
continue
}
seen[c] = struct{}{}
codes = append(codes, c)
}
meta, _ := json.Marshal(map[string]any{"label": label, "plan": planCode, "quantity": qty})
return s.Repo.CreateRedemptionBatchWithCodes(ctx, adminID, label, planCode, codes, meta)
}
// ListRedemptionBatches lists recent batches.
func (s *Service) ListRedemptionBatches(ctx context.Context, limit int) ([]repository.RedemptionBatch, error) {
items, err := s.Repo.ListRedemptionBatches(ctx, limit)
if err != nil {
return nil, err
}
if items == nil {
items = []repository.RedemptionBatch{}
}
return items, nil
}
// ListRedemptionCodes lists codes in a batch.
func (s *Service) ListRedemptionCodes(ctx context.Context, batchID uuid.UUID) ([]repository.RedemptionCodeRow, error) {
ok, err := s.Repo.BatchExists(ctx, batchID)
if err != nil {
return nil, err
}
if !ok {
return nil, ErrBatchNotFound
}
items, err := s.Repo.ListRedemptionCodesByBatch(ctx, batchID)
if err != nil {
return nil, err
}
if items == nil {
items = []repository.RedemptionCodeRow{}
}
return items, nil
}
// DisableRedemptionCode voids an unused code.
func (s *Service) DisableRedemptionCode(ctx context.Context, adminID, codeID uuid.UUID) error {
err := s.Repo.DisableRedemptionCode(ctx, adminID, codeID)
if err != nil && err.Error() == "code not unused" {
return ErrCodeDisable
}
return err
}
func newRedemptionCode() (string, error) {
b := make([]byte, 6)
if _, err := rand.Read(b); err != nil {
return "", err
}
return "YXG-" + strings.ToUpper(hex.EncodeToString(b)), nil
}