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
@@ -71,6 +71,8 @@ type MembershipInfo struct {
EndAt *string `json:"end_at"`
ArticleGenerationLimit int `json:"article_generation_limit"`
ArticleQuotaCycle string `json:"article_quota_cycle"`
AIPointsMonthly int `json:"ai_points_monthly"`
AIPointBaseChars int `json:"ai_point_base_chars"`
CompanyLimit int `json:"company_limit"`
ImageStorageBytes int64 `json:"image_storage_bytes"`
ContactAdminOnExpiry bool `json:"contact_admin_on_expiry"`
@@ -271,6 +273,8 @@ func (s *AuthService) loadMembershipInfo(ctx context.Context, tenantID int64) (*
EndAt: formatTimeValuePtr(access.EndAt),
ArticleGenerationLimit: access.ArticleGenerationLimit(),
ArticleQuotaCycle: access.ArticleQuotaCycle(),
AIPointsMonthly: access.AIPointsMonthlyLimit(),
AIPointBaseChars: access.AIPointBaseChars(),
CompanyLimit: access.BrandLimit(),
ImageStorageBytes: access.ImageStorageBytes(),
ContactAdminOnExpiry: access.Policy.ContactAdminOnExpiry,
@@ -98,6 +98,38 @@ func (a *TenantPlanAccess) ArticleQuotaCycle() string {
}
}
func (a *TenantPlanAccess) AIPointsMonthlyLimit() int {
if a == nil {
return 0
}
return maxInt(a.Policy.AIPointsMonthly, 0)
}
func (a *TenantPlanAccess) AIPointBaseChars() int {
if a == nil || a.Policy.AIPointBaseChars <= 0 {
return 1000
}
return a.Policy.AIPointBaseChars
}
func (a *TenantPlanAccess) AIPointsWindow(now time.Time) (time.Time, time.Time, *time.Time) {
if a == nil {
return time.Time{}, time.Time{}, nil
}
windowStart := a.StartAt.UTC()
windowEnd := a.EndAt.UTC()
cycleStart, cycleEnd := monthlyCycleBounds(windowStart, now.UTC())
if cycleStart.Before(windowStart) {
cycleStart = windowStart
}
if !windowEnd.IsZero() && cycleEnd.After(windowEnd) {
cycleEnd = windowEnd
}
resetAt := cycleEnd
return cycleStart, cycleEnd, &resetAt
}
func (a *TenantPlanAccess) ArticleQuotaWindow(now time.Time) (time.Time, time.Time, *time.Time) {
if a == nil {
return time.Time{}, time.Time{}, nil