diff --git a/deploy/popadmin/popadmin.js b/deploy/popadmin/popadmin.js index babd7be..2ad6123 100644 --- a/deploy/popadmin/popadmin.js +++ b/deploy/popadmin/popadmin.js @@ -3087,45 +3087,122 @@ const pages = { }, async subPlans() { + // SleepHeart ASC App 6815533078 · Team 49S9UL5MN4 · Bundle health.wyrs365.com + const PLAN_DEFAULTS = { + week: { title: '周卡', subtitle: '灵活体验,随时可停', price_display: '$1/周', price_cents: 100, period_days: 7, badge: '', sort_order: 1, product_id: 'health.wyrs365.com.vip.week' }, + month: { title: '月卡', subtitle: '最受欢迎', price_display: '$3/月', price_cents: 300, period_days: 30, badge: '热门', sort_order: 2, product_id: 'health.wyrs365.com.vip.month' }, + quarter: { title: '季卡', subtitle: '平均每月更省', price_display: '$8/季', price_cents: 800, period_days: 90, badge: '', sort_order: 3, product_id: 'health.wyrs365.com.vip.quarter' }, + year: { title: '年卡', subtitle: '全年畅听会员曲库', price_display: '$12/年', price_cents: 1200, period_days: 365, badge: '超值', sort_order: 4, product_id: 'health.wyrs365.com.vip.year' } + }; let items = []; try { - const data = await api('/spark/subscriptions/plans?lang=zh-CN'); + const data = await api('/spark/subscriptions/plans'); items = (data && data.items) || []; } catch (e) { $('content').innerHTML = `
订阅配置 API:${esc(e.message)}
`; return; } + const fillForm = (p) => { + if (!p) return; + $('spPlan').value = p.plan_id || 'week'; + $('spLang').value = p.lang || 'zh-CN'; + $('spTitle').value = p.title || ''; + $('spSub').value = p.subtitle || ''; + $('spPrice').value = p.price_display || ''; + $('spCents').value = p.price_cents != null ? p.price_cents : ''; + $('spCurrency').value = p.currency || 'USD'; + $('spDays').value = p.period_days != null ? p.period_days : 7; + $('spBadge').value = p.badge || ''; + $('spProduct').value = p.product_id || ''; + $('spSort').value = p.sort_order != null ? p.sort_order : 1; + $('spEnabled').checked = p.enabled !== false && p.enabled !== 0; + }; + const applyPlanDefaults = () => { + const d = PLAN_DEFAULTS[$('spPlan').value] || PLAN_DEFAULTS.week; + if (!$('spTitle').value) $('spTitle').value = d.title; + if (!$('spSub').value) $('spSub').value = d.subtitle; + if (!$('spPrice').value) $('spPrice').value = d.price_display; + if (!$('spCents').value) $('spCents').value = d.price_cents; + if (!$('spDays').value) $('spDays').value = d.period_days; + if (!$('spBadge').value && d.badge) $('spBadge').value = d.badge; + if (!$('spProduct').value) $('spProduct').value = d.product_id; + if (!$('spSort').value) $('spSort').value = d.sort_order; + $('spCurrency').value = 'USD'; + }; $('content').innerHTML = ` -
SleepHeart 订阅引导 · /api/admin/spark/subscriptions/plans · 客户端 GET /v1/subscriptions/plans · plan_id=week|month|quarter|year
+
SleepHeart 订阅引导 · ASC App 6815533078 · Team 49S9UL5MN4 · Bundle health.wyrs365.com
+ Admin /api/admin/spark/subscriptions/plans · 客户端 GET /v1/subscriptions/plans · 默认 USD $1/周 · $3/月 · $12/年(季卡 $8 可保留)· IAP health.wyrs365.com.vip.{week|month|quarter|year}
- - + + + - + +
- + - ${items.length ? items.map((p)=>``).join('') : ''} + ${items.length ? items.map((p, i)=>``).join('') : ''}
planlang标题价格天数角标product启用
planlang标题价格币种天数角标product启用
${esc(p.plan_id)}${esc(p.lang)}${esc(p.title)}
${esc(p.subtitle||'')}
${esc(p.price_display)} (${esc(p.price_cents)})${esc(p.period_days)}${esc(p.badge||'')}${esc(p.product_id)}${esc(p.enabled)}
暂无套餐
${esc(p.plan_id)}${esc(p.lang)}${esc(p.title)}
${esc(p.subtitle||'')}
${esc(p.price_display)} (${esc(p.price_cents)})${esc(p.currency||'USD')}${esc(p.period_days)}${esc(p.badge||'')}${esc(p.product_id)}${esc(p.enabled)}
暂无套餐(可点「填入默认价」后保存)
`; + const seed = items.find((p) => p.plan_id === 'month' && p.lang === 'zh-CN') + || items.find((p) => p.lang === 'zh-CN') + || items[0]; + if (seed) fillForm(seed); + else applyPlanDefaults(); + $('spPlan').onchange = () => { + const hit = items.find((p) => p.plan_id === $('spPlan').value && p.lang === $('spLang').value); + if (hit) fillForm(hit); + else { + $('spTitle').value = ''; $('spSub').value = ''; $('spPrice').value = ''; + $('spCents').value = ''; $('spBadge').value = ''; $('spProduct').value = ''; + applyPlanDefaults(); + } + }; + $('spLang').onchange = () => { + const hit = items.find((p) => p.plan_id === $('spPlan').value && p.lang === $('spLang').value); + if (hit) fillForm(hit); + }; + $('spFillDefaults').onclick = () => { + const d = PLAN_DEFAULTS[$('spPlan').value] || PLAN_DEFAULTS.week; + $('spTitle').value = d.title; + $('spSub').value = d.subtitle; + $('spPrice').value = d.price_display; + $('spCents').value = d.price_cents; + $('spCurrency').value = 'USD'; + $('spDays').value = d.period_days; + $('spBadge').value = d.badge; + $('spProduct').value = d.product_id; + $('spSort').value = d.sort_order; + $('spEnabled').checked = true; + }; + $('content').querySelectorAll('[data-sp-idx]').forEach((tr) => { + tr.onclick = () => fillForm(items[parseInt(tr.getAttribute('data-sp-idx'), 10)]); + }); $('spSave').onclick = async () => { + const planId = $('spPlan').value; + const d = PLAN_DEFAULTS[planId] || PLAN_DEFAULTS.week; await api('/spark/subscriptions/plans', { method: 'PUT', body: JSON.stringify({ - plan_id: $('spPlan').value, lang: $('spLang').value, title: $('spTitle').value.trim(), - subtitle: $('spSub').value.trim(), price_display: $('spPrice').value.trim(), - price_cents: parseInt($('spCents').value,10)||0, currency: 'CNY', - period_days: parseInt($('spDays').value,10)||7, badge: $('spBadge').value.trim(), - sort_order: parseInt($('spSort').value,10)||1, enabled: !!$('spEnabled').checked, - product_id: $('spProduct').value.trim() + plan_id: planId, lang: $('spLang').value, title: $('spTitle').value.trim() || d.title, + subtitle: $('spSub').value.trim() || d.subtitle, + price_display: $('spPrice').value.trim() || d.price_display, + price_cents: parseInt($('spCents').value, 10) || d.price_cents, + currency: $('spCurrency').value || 'USD', + period_days: parseInt($('spDays').value, 10) || d.period_days, + badge: $('spBadge').value.trim(), + sort_order: parseInt($('spSort').value, 10) || d.sort_order, + enabled: !!$('spEnabled').checked, + product_id: $('spProduct').value.trim() || d.product_id })}); pages.subPlans(); }; diff --git a/docs/daily/2026-09-24-change-list-sleepheart.md b/docs/daily/2026-09-24-change-list-sleepheart.md index 26ef900..7f54b31 100644 --- a/docs/daily/2026-09-24-change-list-sleepheart.md +++ b/docs/daily/2026-09-24-change-list-sleepheart.md @@ -4,8 +4,9 @@ ### 概要 -- App 展示名统一为 **SleepHeart**;Bundle ID 保持 `health.wyrs365.com` +- App 展示名统一为 **SleepHeart**;Bundle ID 保持 `health.wyrs365.com`;ASC App `6815533078`;Team `49S9UL5MN4` - spark-server 新增法律文档 / 反馈 / 订阅套餐 / 密码登录 API,并在 popadmin 增加管理页 +- 订阅默认价 **USD $1/周 · $3/月 · $12/年**(保留季卡 $8);IAP `health.wyrs365.com.vip.{week|month|quarter|year}` - HealthiOS 将原先 mock 的密码登录、反馈、订阅配置改为对接真接口(失败仍可 mock 回退) ### 变更范围 @@ -19,10 +20,11 @@ ### 主要文件 - `migrations/0023_client_legal_feedback_subscription.sql` — 表与种子数据 +- `migrations/0025_subscription_plans_usd_pricing.sql` — USD 定价与 IAP product_id 对齐 - `internal/module/client/**` — 业务与仓储 - `internal/transport/http/handler/client`、`handler/admin/client.go`、`router*.go`、`auth.go` -- `deploy/popadmin/popadmin.js` — 三页管理 UI -- `HealthiOS` pbxproj / L10n / AuthService / APIModels +- `deploy/popadmin/popadmin.js` — 三页管理 UI(订阅配置可编辑默认价) +- `HealthiOS` pbxproj / L10n / AuthService / APIModels / PaywallView ### 验证 diff --git a/internal/module/client/entity/types.go b/internal/module/client/entity/types.go index 4032d9d..85bf2a6 100644 --- a/internal/module/client/entity/types.go +++ b/internal/module/client/entity/types.go @@ -42,6 +42,8 @@ type Feedback struct { } // 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). type SubscriptionPlan struct { PlanID string `json:"plan_id"` Lang string `json:"lang"` diff --git a/internal/module/client/logic/service.go b/internal/module/client/logic/service.go index 9b6c8c0..7872416 100644 --- a/internal/module/client/logic/service.go +++ b/internal/module/client/logic/service.go @@ -332,25 +332,39 @@ func subscriptionCopy(lang string) (headline, sub string) { } func defaultPlans(lang string) []entity.SubscriptionPlan { + const ( + pidWeek = "health.wyrs365.com.vip.week" + pidMonth = "health.wyrs365.com.vip.month" + pidQuarter = "health.wyrs365.com.vip.quarter" + pidYear = "health.wyrs365.com.vip.year" + ) mk := func(id, title, sub, price, badge, product string, cents, days, order int) entity.SubscriptionPlan { return entity.SubscriptionPlan{ PlanID: id, Lang: lang, Title: title, Subtitle: sub, PriceDisplay: price, - PriceCents: cents, Currency: "CNY", PeriodDays: days, Badge: badge, + PriceCents: cents, Currency: "USD", PeriodDays: days, Badge: badge, SortOrder: order, Enabled: true, ProductID: product, } } if lang == "en" { return []entity.SubscriptionPlan{ - mk("week", "Weekly", "Flexible", "$1.99", "", "health.sub.week", 199, 7, 1), - mk("month", "Monthly", "Most popular", "$4.99", "Popular", "health.sub.month", 499, 30, 2), - mk("quarter", "Quarterly", "Better value", "$12.99", "", "health.sub.quarter", 1299, 90, 3), - mk("year", "Yearly", "Best value", "$29.99", "Best", "health.sub.year", 2999, 365, 4), + mk("week", "Weekly", "Flexible, cancel anytime", "$1/week", "", pidWeek, 100, 7, 1), + mk("month", "Monthly", "Most popular", "$3/month", "Popular", pidMonth, 300, 30, 2), + mk("quarter", "Quarterly", "Better monthly value", "$8/quarter", "", pidQuarter, 800, 90, 3), + mk("year", "Yearly", "Best value all year", "$12/year", "Best", pidYear, 1200, 365, 4), + } + } + if lang == "zh-TW" { + return []entity.SubscriptionPlan{ + mk("week", "週卡", "靈活體驗,隨時可停", "$1/週", "", pidWeek, 100, 7, 1), + mk("month", "月卡", "最受歡迎", "$3/月", "熱門", pidMonth, 300, 30, 2), + mk("quarter", "季卡", "平均每月更省", "$8/季", "", pidQuarter, 800, 90, 3), + mk("year", "年卡", "全年暢聽會員曲庫", "$12/年", "超值", pidYear, 1200, 365, 4), } } return []entity.SubscriptionPlan{ - mk("week", "周卡", "灵活体验", "¥12", "", "health.sub.week", 1200, 7, 1), - mk("month", "月卡", "最受欢迎", "¥30", "热门", "health.sub.month", 3000, 30, 2), - mk("quarter", "季卡", "更省", "¥68", "", "health.sub.quarter", 6800, 90, 3), - mk("year", "年卡", "全年畅听", "¥198", "超值", "health.sub.year", 19800, 365, 4), + mk("week", "周卡", "灵活体验,随时可停", "$1/周", "", pidWeek, 100, 7, 1), + mk("month", "月卡", "最受欢迎", "$3/月", "热门", pidMonth, 300, 30, 2), + mk("quarter", "季卡", "平均每月更省", "$8/季", "", pidQuarter, 800, 90, 3), + mk("year", "年卡", "全年畅听会员曲库", "$12/年", "超值", pidYear, 1200, 365, 4), } } diff --git a/internal/module/client/model/repo.go b/internal/module/client/model/repo.go index c11eefd..225cc4f 100644 --- a/internal/module/client/model/repo.go +++ b/internal/module/client/model/repo.go @@ -219,7 +219,7 @@ func (r *Repo) UpsertPlan(ctx context.Context, p *entity.SubscriptionPlan) error Enabled: p.Enabled, ProductID: p.ProductID, UpdatedAt: time.Now(), } if row.Currency == "" { - row.Currency = "CNY" + row.Currency = "USD" } return r.db.WithContext(ctx).Save(&row).Error } diff --git a/migrations/0025_subscription_plans_usd_pricing.sql b/migrations/0025_subscription_plans_usd_pricing.sql new file mode 100644 index 0000000..f278c7b --- /dev/null +++ b/migrations/0025_subscription_plans_usd_pricing.sql @@ -0,0 +1,103 @@ +-- SleepHeart 订阅定价对齐:USD $1/周、$3/月、$12/年(保留季卡 $8);IAP product_id 占位。 +-- ASC App 6815533078 · Team 49S9UL5MN4 · Bundle health.wyrs365.com + +UPDATE t_subscription_plans +SET price_display = CASE plan_id + WHEN 'week' THEN '$1/周' + WHEN 'month' THEN '$3/月' + WHEN 'quarter' THEN '$8/季' + WHEN 'year' THEN '$12/年' + ELSE price_display + END, + price_cents = CASE plan_id + WHEN 'week' THEN 100 + WHEN 'month' THEN 300 + WHEN 'quarter' THEN 800 + WHEN 'year' THEN 1200 + ELSE price_cents + END, + currency = 'USD', + product_id = CASE plan_id + WHEN 'week' THEN 'health.wyrs365.com.vip.week' + WHEN 'month' THEN 'health.wyrs365.com.vip.month' + WHEN 'quarter' THEN 'health.wyrs365.com.vip.quarter' + WHEN 'year' THEN 'health.wyrs365.com.vip.year' + ELSE product_id + END, + updated_at = CURRENT_TIMESTAMP(3) +WHERE lang = 'zh-CN'; + +UPDATE t_subscription_plans +SET price_display = CASE plan_id + WHEN 'week' THEN '$1/week' + WHEN 'month' THEN '$3/month' + WHEN 'quarter' THEN '$8/quarter' + WHEN 'year' THEN '$12/year' + ELSE price_display + END, + price_cents = CASE plan_id + WHEN 'week' THEN 100 + WHEN 'month' THEN 300 + WHEN 'quarter' THEN 800 + WHEN 'year' THEN 1200 + ELSE price_cents + END, + currency = 'USD', + product_id = CASE plan_id + WHEN 'week' THEN 'health.wyrs365.com.vip.week' + WHEN 'month' THEN 'health.wyrs365.com.vip.month' + WHEN 'quarter' THEN 'health.wyrs365.com.vip.quarter' + WHEN 'year' THEN 'health.wyrs365.com.vip.year' + ELSE product_id + END, + updated_at = CURRENT_TIMESTAMP(3) +WHERE lang = 'en'; + +UPDATE t_subscription_plans +SET price_display = CASE plan_id + WHEN 'week' THEN '$1/週' + WHEN 'month' THEN '$3/月' + WHEN 'quarter' THEN '$8/季' + WHEN 'year' THEN '$12/年' + ELSE price_display + END, + price_cents = CASE plan_id + WHEN 'week' THEN 100 + WHEN 'month' THEN 300 + WHEN 'quarter' THEN 800 + WHEN 'year' THEN 1200 + ELSE price_cents + END, + currency = 'USD', + product_id = CASE plan_id + WHEN 'week' THEN 'health.wyrs365.com.vip.week' + WHEN 'month' THEN 'health.wyrs365.com.vip.month' + WHEN 'quarter' THEN 'health.wyrs365.com.vip.quarter' + WHEN 'year' THEN 'health.wyrs365.com.vip.year' + ELSE product_id + END, + updated_at = CURRENT_TIMESTAMP(3) +WHERE lang = 'zh-TW'; + +-- 若库中尚无种子,补齐三语默认档(幂等) +INSERT INTO t_subscription_plans +(plan_id, lang, title, subtitle, price_display, price_cents, currency, period_days, badge, sort_order, enabled, product_id, updated_at) +VALUES +('week', 'zh-CN', '周卡', '灵活体验,随时可停', '$1/周', 100, 'USD', 7, '', 1, 1, 'health.wyrs365.com.vip.week', CURRENT_TIMESTAMP(3)), +('month', 'zh-CN', '月卡', '最受欢迎', '$3/月', 300, 'USD', 30, '热门', 2, 1, 'health.wyrs365.com.vip.month', CURRENT_TIMESTAMP(3)), +('quarter', 'zh-CN', '季卡', '平均每月更省', '$8/季', 800, 'USD', 90, '', 3, 1, 'health.wyrs365.com.vip.quarter', CURRENT_TIMESTAMP(3)), +('year', 'zh-CN', '年卡', '全年畅听会员曲库', '$12/年', 1200, 'USD', 365, '超值', 4, 1, 'health.wyrs365.com.vip.year', CURRENT_TIMESTAMP(3)), +('week', 'en', 'Weekly', 'Flexible, cancel anytime', '$1/week', 100, 'USD', 7, '', 1, 1, 'health.wyrs365.com.vip.week', CURRENT_TIMESTAMP(3)), +('month', 'en', 'Monthly', 'Most popular', '$3/month', 300, 'USD', 30, 'Popular', 2, 1, 'health.wyrs365.com.vip.month', CURRENT_TIMESTAMP(3)), +('quarter', 'en', 'Quarterly', 'Better monthly value', '$8/quarter', 800, 'USD', 90, '', 3, 1, 'health.wyrs365.com.vip.quarter', CURRENT_TIMESTAMP(3)), +('year', 'en', 'Yearly', 'Best value all year', '$12/year', 1200, 'USD', 365, 'Best', 4, 1, 'health.wyrs365.com.vip.year', CURRENT_TIMESTAMP(3)), +('week', 'zh-TW', '週卡', '靈活體驗,隨時可停', '$1/週', 100, 'USD', 7, '', 1, 1, 'health.wyrs365.com.vip.week', CURRENT_TIMESTAMP(3)), +('month', 'zh-TW', '月卡', '最受歡迎', '$3/月', 300, 'USD', 30, '熱門', 2, 1, 'health.wyrs365.com.vip.month', CURRENT_TIMESTAMP(3)), +('quarter', 'zh-TW', '季卡', '平均每月更省', '$8/季', 800, 'USD', 90, '', 3, 1, 'health.wyrs365.com.vip.quarter', CURRENT_TIMESTAMP(3)), +('year', 'zh-TW', '年卡', '全年暢聽會員曲庫', '$12/年', 1200, 'USD', 365, '超值', 4, 1, 'health.wyrs365.com.vip.year', CURRENT_TIMESTAMP(3)) +ON DUPLICATE KEY UPDATE + price_display = VALUES(price_display), + price_cents = VALUES(price_cents), + currency = VALUES(currency), + product_id = VALUES(product_id), + updated_at = VALUES(updated_at);