feat(client): 按平台拆分会员价格,并归一 health 包名

同一时长可按 ios、android、web 分别定价;health.wyrs.com 对齐已入库的 health.wyrs365.com。
This commit is contained in:
DESKTOP-L8OUL9F\gfl0034 2026-10-12 17:21:00 +08:00
parent 1bbf3b97c7
commit c4d1322cec
4 changed files with 50 additions and 20 deletions

View File

@ -1,6 +1,9 @@
package entity
import "time"
import (
"strings"
"time"
)
const (
DocPrivacy = "privacy"
@ -41,14 +44,33 @@ type Feedback struct {
CreatedAt time.Time `json:"created_at"`
}
// CanonicalHealthPackage maps the health.wyrs.com product name onto the iOS bundle already stored in admin.
func CanonicalHealthPackage(pkg string) string {
pkg = strings.TrimSpace(pkg)
if strings.EqualFold(pkg, "health.wyrs.com") {
return "health.wyrs365.com"
}
return pkg
}
const (
// PlatformIOS / PlatformAndroid / PlatformWeb split one duration into three prices.
// Empty platform is legacy data and still matches every client platform.
PlatformIOS = "ios"
PlatformAndroid = "android"
PlatformWeb = "web"
)
// SubscriptionPlan is one week/month/quarter/year card.
// IAP product_id placeholders: health.wyrs365.com.vip.{week|month|quarter|year}
// (ASC App 6815533078 · Team 49S9UL5MN4 · Bundle health.wyrs365.com).
// PackageName 为空表示默认兜底;客户端传 packageName / X-Wyrs-Package 时优先精确匹配。
// Platform 为 ios / android / web;空字符串是改平台前的旧行。
type SubscriptionPlan struct {
PlanID string `json:"plan_id"`
Lang string `json:"lang"`
PackageName string `json:"package_name,omitempty"`
Platform string `json:"platform,omitempty"`
Title string `json:"title"`
Subtitle string `json:"subtitle"`
PriceDisplay string `json:"price_display"`

View File

@ -183,30 +183,28 @@ func (s *Service) UpdateFeedbackStatus(ctx context.Context, id int64, status str
return s.repo.UpdateFeedbackStatus(ctx, id, status)
}
func (s *Service) SubscriptionPlans(ctx context.Context, packageName string) (*entity.SubscriptionConfig, error) {
func (s *Service) SubscriptionPlans(ctx context.Context, packageName, platform string) (*entity.SubscriptionConfig, error) {
lang := normalizeLang(reqctx.Language(ctx))
packageName = strings.TrimSpace(packageName)
packageName = canonicalHealthPackage(packageName)
platform = normalizePlatform(platform)
headline, sub := subscriptionCopy(lang)
if s.repo != nil {
plans, err := s.repo.ListPlans(ctx, lang, packageName)
if err == nil && len(plans) > 0 {
return &entity.SubscriptionConfig{Headline: headline, Subheadline: sub, Plans: plans}, nil
}
if lang != string(i18n.ZhCN) {
plans, err = s.repo.ListPlans(ctx, string(i18n.ZhCN), packageName)
if err == nil && len(plans) > 0 {
return &entity.SubscriptionConfig{Headline: headline, Subheadline: sub, Plans: plans}, nil
}
plans, err := s.clientPlans(ctx, lang, packageName, platform)
if err != nil {
return nil, err
}
return &entity.SubscriptionConfig{Headline: headline, Subheadline: sub, Plans: plans}, nil
}
return &entity.SubscriptionConfig{Headline: headline, Subheadline: sub, Plans: defaultPlans(lang)}, nil
return &entity.SubscriptionConfig{
Headline: headline, Subheadline: sub, Plans: filterPlansByPlatform(defaultPlans(lang), platform),
}, nil
}
func (s *Service) ListPlansAdmin(ctx context.Context, lang, packageName string) ([]entity.SubscriptionPlan, error) {
if s.repo == nil {
return defaultPlans(normalizeLang(lang)), nil
}
return s.repo.ListPlansAdmin(ctx, strings.TrimSpace(lang), strings.TrimSpace(packageName))
return s.repo.ListPlansAdmin(ctx, strings.TrimSpace(lang), canonicalHealthPackage(packageName))
}
func (s *Service) UpsertPlanAdmin(ctx context.Context, p *entity.SubscriptionPlan) error {
@ -218,7 +216,12 @@ func (s *Service) UpsertPlanAdmin(ctx context.Context, p *entity.SubscriptionPla
}
p.Lang = normalizeLang(p.Lang)
p.PlanID = strings.TrimSpace(p.PlanID)
p.PackageName = strings.TrimSpace(p.PackageName)
p.PackageName = canonicalHealthPackage(p.PackageName)
rawPlatform := strings.TrimSpace(p.Platform)
p.Platform = normalizePlatform(rawPlatform)
if rawPlatform != "" && p.Platform == "" {
return errs.ErrInvalidArgument.WithMsg("platform must be ios|android|web")
}
switch p.PlanID {
case "week", "month", "quarter", "year":
default:

View File

@ -52,6 +52,7 @@ type planRow struct {
PlanID string `gorm:"column:plan_id;primaryKey"`
Lang string `gorm:"column:lang;primaryKey"`
PackageName string `gorm:"column:package_name;primaryKey"`
Platform string `gorm:"column:platform;primaryKey"`
Title string `gorm:"column:title"`
Subtitle string `gorm:"column:subtitle"`
PriceDisplay string `gorm:"column:price_display"`
@ -204,7 +205,7 @@ func mapPlans(rows []planRow) []entity.SubscriptionPlan {
for i := range rows {
row := &rows[i]
out = append(out, entity.SubscriptionPlan{
PlanID: row.PlanID, Lang: row.Lang, PackageName: row.PackageName,
PlanID: row.PlanID, Lang: row.Lang, PackageName: row.PackageName, Platform: row.Platform,
Title: row.Title, Subtitle: row.Subtitle,
PriceDisplay: row.PriceDisplay, PriceCents: row.PriceCents, Currency: row.Currency,
PeriodDays: row.PeriodDays, Badge: row.Badge, SortOrder: row.SortOrder,
@ -230,11 +231,11 @@ func filterPlansByPackage(items []entity.SubscriptionPlan, packageName string) [
}
return items
}
want := strings.ToLower(packageName)
want := strings.ToLower(entity.CanonicalHealthPackage(packageName))
exact := make([]entity.SubscriptionPlan, 0)
fallback := make([]entity.SubscriptionPlan, 0)
for _, p := range items {
pkg := strings.TrimSpace(p.PackageName)
pkg := entity.CanonicalHealthPackage(p.PackageName)
if pkg != "" && strings.ToLower(pkg) == want {
exact = append(exact, p)
} else if pkg == "" {
@ -252,7 +253,7 @@ func (r *Repo) UpsertPlan(ctx context.Context, p *entity.SubscriptionPlan) error
return errors.New("client: nil subscription plan")
}
row := planRow{
PlanID: p.PlanID, Lang: p.Lang, PackageName: strings.TrimSpace(p.PackageName),
PlanID: p.PlanID, Lang: p.Lang, PackageName: strings.TrimSpace(p.PackageName), Platform: p.Platform,
Title: p.Title, Subtitle: p.Subtitle,
PriceDisplay: p.PriceDisplay, PriceCents: p.PriceCents, Currency: p.Currency,
PeriodDays: p.PeriodDays, Badge: p.Badge, SortOrder: p.SortOrder,

View File

@ -48,7 +48,11 @@ func (h *Handler) SubscriptionPlans(c *gin.Context) {
if pkg == "" {
pkg = strings.TrimSpace(c.GetHeader("X-Wyrs-Package"))
}
out, err := h.svc.SubscriptionPlans(c.Request.Context(), pkg)
platform := strings.TrimSpace(c.Query("platform"))
if platform == "" {
platform = strings.TrimSpace(c.GetHeader("X-Wyrs-Platform"))
}
out, err := h.svc.SubscriptionPlans(c.Request.Context(), pkg, platform)
if err != nil {
respond.JSONError(c, err)
return