feat(ECR-014): MembershipPlan 套餐配置并 Closed

membership_plans 表、admin 套餐页、Grant/CreateOrder 读表;
Loop continuous 自动 Approve/Closed。Next:ECR-015 RedemptionCode。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jackyu66git
2026-08-07 18:03:38 +08:00
co-authored by Cursor
parent e25cd94b0c
commit 882c01d81a
31 changed files with 887 additions and 27 deletions
@@ -0,0 +1,86 @@
package admin
import (
"context"
"encoding/json"
"strings"
"github.com/google/uuid"
"github.com/yuxingu/digital-psychology/apps/api/internal/repository"
)
var (
ErrPlanNotFound = errString("plan not found")
ErrInvalidPlanU = errString("invalid plan update")
)
// ListMembershipPlans returns catalog.
func (s *Service) ListMembershipPlans(ctx context.Context) ([]repository.MembershipPlanRow, error) {
items, err := s.Repo.ListMembershipPlans(ctx)
if err != nil {
return nil, err
}
if items == nil {
items = []repository.MembershipPlanRow{}
}
return items, nil
}
// GetMembershipPlan returns one plan.
func (s *Service) GetMembershipPlan(ctx context.Context, code string) (*repository.MembershipPlanRow, error) {
p, err := s.Repo.GetMembershipPlan(ctx, code)
if err != nil {
return nil, err
}
if p == nil {
return nil, ErrPlanNotFound
}
return p, nil
}
// UpdateMembershipPlan updates mutable fields.
func (s *Service) UpdateMembershipPlan(
ctx context.Context, adminID uuid.UUID, code, title string, days, amount int, active bool,
) (*repository.MembershipPlanRow, error) {
code = strings.TrimSpace(code)
title = strings.TrimSpace(title)
if title == "" || days <= 0 || amount < 0 {
return nil, ErrInvalidPlanU
}
if _, err := s.GetMembershipPlan(ctx, code); err != nil {
return nil, err
}
meta, _ := json.Marshal(map[string]any{
"title": title, "duration_days": days, "amount_cents": amount, "active": active,
})
if err := s.Repo.UpdateMembershipPlanWithAudit(ctx, adminID, code, title, days, amount, active, meta); err != nil {
return nil, err
}
return s.GetMembershipPlan(ctx, code)
}
// PlanDurationDays resolves grant length from catalog with hardcoded fallback.
func (s *Service) PlanDurationDays(ctx context.Context, plan string) (int, error) {
p, err := s.Repo.GetMembershipPlan(ctx, plan)
if err != nil {
return 0, err
}
if p != nil && p.Active && p.DurationDays > 0 {
return p.DurationDays, nil
}
return planDaysFallback(plan)
}
func planDaysFallback(plan string) (int, error) {
switch plan {
case "month":
return 31, nil
case "quarter":
return 92, nil
case "year":
return 366, nil
default:
return 0, ErrInvalidPlan
}
}
+13 -11
View File
@@ -10,23 +10,25 @@ import (
// Permission catalog frozen in ECR-013A Spec.
const (
PermUsersRead = "admin.users.read"
PermMembershipGrant = "admin.users.membership.grant"
PermAskQuotaGrant = "admin.users.ask_quota.grant"
PermOrdersRead = "admin.orders.read"
PermAuditRead = "admin.audit.read"
PermAnalyticsRead = "admin.analytics.read"
PermContentWrite = "admin.content.write"
PermRolesRead = "admin.roles.read"
PermRolesWrite = "admin.roles.write"
PermUsersStatusWrite = "admin.users.status.write"
PermUsersRead = "admin.users.read"
PermMembershipGrant = "admin.users.membership.grant"
PermAskQuotaGrant = "admin.users.ask_quota.grant"
PermOrdersRead = "admin.orders.read"
PermAuditRead = "admin.audit.read"
PermAnalyticsRead = "admin.analytics.read"
PermContentWrite = "admin.content.write"
PermRolesRead = "admin.roles.read"
PermRolesWrite = "admin.roles.write"
PermUsersStatusWrite = "admin.users.status.write"
PermMembershipPlansRead = "admin.membership.plans.read"
PermMembershipPlansWrite = "admin.membership.plans.write"
)
var knownPermissions = map[string]struct{}{
PermUsersRead: {}, PermMembershipGrant: {}, PermAskQuotaGrant: {},
PermOrdersRead: {}, PermAuditRead: {}, PermAnalyticsRead: {},
PermContentWrite: {}, PermRolesRead: {}, PermRolesWrite: {},
PermUsersStatusWrite: {},
PermUsersStatusWrite: {}, PermMembershipPlansRead: {}, PermMembershipPlansWrite: {},
}
var (
+2 -11
View File
@@ -181,7 +181,7 @@ type GrantInput struct {
// GrantMembership extends membership and writes audit.
func (s *Service) GrantMembership(ctx context.Context, adminID, userID uuid.UUID, plan string) error {
days, err := planDays(plan)
days, err := s.PlanDurationDays(ctx, plan)
if err != nil {
return err
}
@@ -230,16 +230,7 @@ func (s *Service) ListAuditLogs(ctx context.Context, limit, offset int) ([]repos
}
func planDays(plan string) (int, error) {
switch plan {
case "month":
return 31, nil
case "quarter":
return 92, nil
case "year":
return 366, nil
default:
return 0, ErrInvalidPlan
}
return planDaysFallback(plan)
}
func newToken() (string, error) {