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 package entity
import "time" import (
"strings"
"time"
)
const ( const (
DocPrivacy = "privacy" DocPrivacy = "privacy"
@ -41,14 +44,33 @@ type Feedback struct {
CreatedAt time.Time `json:"created_at"` 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. // SubscriptionPlan is one week/month/quarter/year card.
// IAP product_id placeholders: health.wyrs365.com.vip.{week|month|quarter|year} // IAP product_id placeholders: health.wyrs365.com.vip.{week|month|quarter|year}
// (ASC App 6815533078 · Team 49S9UL5MN4 · Bundle health.wyrs365.com). // (ASC App 6815533078 · Team 49S9UL5MN4 · Bundle health.wyrs365.com).
// PackageName 为空表示默认兜底;客户端传 packageName / X-Wyrs-Package 时优先精确匹配。 // PackageName 为空表示默认兜底;客户端传 packageName / X-Wyrs-Package 时优先精确匹配。
// Platform 为 ios / android / web;空字符串是改平台前的旧行。
type SubscriptionPlan struct { type SubscriptionPlan struct {
PlanID string `json:"plan_id"` PlanID string `json:"plan_id"`
Lang string `json:"lang"` Lang string `json:"lang"`
PackageName string `json:"package_name,omitempty"` PackageName string `json:"package_name,omitempty"`
Platform string `json:"platform,omitempty"`
Title string `json:"title"` Title string `json:"title"`
Subtitle string `json:"subtitle"` Subtitle string `json:"subtitle"`
PriceDisplay string `json:"price_display"` 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) 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)) lang := normalizeLang(reqctx.Language(ctx))
packageName = strings.TrimSpace(packageName) packageName = canonicalHealthPackage(packageName)
platform = normalizePlatform(platform)
headline, sub := subscriptionCopy(lang) headline, sub := subscriptionCopy(lang)
if s.repo != nil { if s.repo != nil {
plans, err := s.repo.ListPlans(ctx, lang, packageName) plans, err := s.clientPlans(ctx, lang, packageName, platform)
if err == nil && len(plans) > 0 { if err != nil {
return &entity.SubscriptionConfig{Headline: headline, Subheadline: sub, Plans: plans}, nil return nil, err
}
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
}
} }
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) { func (s *Service) ListPlansAdmin(ctx context.Context, lang, packageName string) ([]entity.SubscriptionPlan, error) {
if s.repo == nil { if s.repo == nil {
return defaultPlans(normalizeLang(lang)), 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 { 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.Lang = normalizeLang(p.Lang)
p.PlanID = strings.TrimSpace(p.PlanID) 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 { switch p.PlanID {
case "week", "month", "quarter", "year": case "week", "month", "quarter", "year":
default: default:

View File

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

View File

@ -48,7 +48,11 @@ func (h *Handler) SubscriptionPlans(c *gin.Context) {
if pkg == "" { if pkg == "" {
pkg = strings.TrimSpace(c.GetHeader("X-Wyrs-Package")) 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 { if err != nil {
respond.JSONError(c, err) respond.JSONError(c, err)
return return