批次生成/作废、C 端兑码延长会员;admin-h5 /codes。 Loop continuous。Next:ECR-016 UserIntelligence。 Co-authored-by: Cursor <cursoragent@cursor.com>
97 lines
2.5 KiB
Go
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
|
|
}
|