feat(server/membership): add ai_points plan policy fields

Introduces ai_points_monthly and ai_point_base_chars on the membership
plan policy, exposes them through TenantPlanAccess and MembershipInfo,
and seeds Pro with a 1500-point monthly allowance across the local,
server, and deploy configs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-28 11:33:30 +08:00
parent f5254f6a27
commit 794a2d89db
7 changed files with 81 additions and 0 deletions
+21
View File
@@ -145,6 +145,8 @@ type MembershipPlanConfig struct {
Name string `mapstructure:"name"`
ArticleGeneration int `mapstructure:"article_generation"`
ArticleQuotaCycle string `mapstructure:"article_quota_cycle"`
AIPointsMonthly int `mapstructure:"ai_points_monthly"`
AIPointBaseChars int `mapstructure:"ai_point_base_chars"`
ImageStorageBytes int64 `mapstructure:"image_storage_bytes"`
CompanyLimit int `mapstructure:"company_limit"`
SubscriptionDuration time.Duration `mapstructure:"subscription_duration"`
@@ -154,6 +156,8 @@ type MembershipPlanConfig struct {
type PlanQuotaPolicy struct {
ArticleGeneration int `json:"article_generation"`
ArticleQuotaCycle string `json:"article_quota_cycle"`
AIPointsMonthly int `json:"ai_points_monthly"`
AIPointBaseChars int `json:"ai_point_base_chars"`
ImageStorageBytes int64 `json:"image_storage_bytes"`
BrandLimit int `json:"brand_limit"`
SubscriptionDurationSecs int64 `json:"subscription_duration_seconds,omitempty"`
@@ -174,6 +178,8 @@ func (p MembershipPlanConfig) QuotaPolicy() PlanQuotaPolicy {
policy := PlanQuotaPolicy{
ArticleGeneration: maxInt(p.ArticleGeneration, 0),
ArticleQuotaCycle: normalizeArticleQuotaCycle(p.ArticleQuotaCycle, p.Code),
AIPointsMonthly: maxInt(p.AIPointsMonthly, 0),
AIPointBaseChars: normalizeAIPointBaseChars(p.AIPointBaseChars),
ImageStorageBytes: maxInt64(p.ImageStorageBytes, 0),
BrandLimit: maxInt(p.CompanyLimit, 0),
ContactAdminOnExpiry: p.ContactAdminOnExpiry,
@@ -695,6 +701,10 @@ func normalizeMembershipConfig(cfg *MembershipConfig) {
if plan.ArticleGeneration < 0 {
plan.ArticleGeneration = 0
}
if plan.AIPointsMonthly < 0 {
plan.AIPointsMonthly = 0
}
plan.AIPointBaseChars = normalizeAIPointBaseChars(plan.AIPointBaseChars)
if plan.ImageStorageBytes < 0 {
plan.ImageStorageBytes = 0
}
@@ -725,6 +735,7 @@ func defaultMembershipPlans() []MembershipPlanConfig {
Name: "Free",
ArticleGeneration: 3,
ArticleQuotaCycle: ArticleQuotaCycleLifetime,
AIPointBaseChars: 1000,
ImageStorageBytes: 10 * 1024 * 1024,
CompanyLimit: 1,
SubscriptionDuration: 72 * time.Hour,
@@ -735,6 +746,7 @@ func defaultMembershipPlans() []MembershipPlanConfig {
Name: "Plus",
ArticleGeneration: 200,
ArticleQuotaCycle: ArticleQuotaCycleMonthly,
AIPointBaseChars: 1000,
ImageStorageBytes: 500 * 1024 * 1024,
CompanyLimit: 1,
SubscriptionDuration: 720 * time.Hour,
@@ -744,6 +756,8 @@ func defaultMembershipPlans() []MembershipPlanConfig {
Name: "Pro",
ArticleGeneration: 400,
ArticleQuotaCycle: ArticleQuotaCycleMonthly,
AIPointsMonthly: 1500,
AIPointBaseChars: 1000,
ImageStorageBytes: 1024 * 1024 * 1024,
CompanyLimit: 2,
SubscriptionDuration: 720 * time.Hour,
@@ -751,6 +765,13 @@ func defaultMembershipPlans() []MembershipPlanConfig {
}
}
func normalizeAIPointBaseChars(value int) int {
if value <= 0 {
return 1000
}
return value
}
func normalizeArticleQuotaCycle(value, planCode string) string {
switch strings.ToLower(strings.TrimSpace(value)) {
case ArticleQuotaCycleLifetime:
@@ -237,6 +237,12 @@ membership: {}
if pro.ArticleGeneration != 400 {
t.Fatalf("expected pro article generation 400, got %d", pro.ArticleGeneration)
}
if pro.AIPointsMonthly != 1500 {
t.Fatalf("expected pro ai points 1500, got %d", pro.AIPointsMonthly)
}
if pro.AIPointBaseChars != 1000 {
t.Fatalf("expected pro ai point base chars 1000, got %d", pro.AIPointBaseChars)
}
if pro.CompanyLimit != 2 {
t.Fatalf("expected pro company limit 2, got %d", pro.CompanyLimit)
}