diff --git a/sundynix-admin/src/App.tsx b/sundynix-admin/src/App.tsx index f47b0e6..bb2cb4c 100644 --- a/sundynix-admin/src/App.tsx +++ b/sundynix-admin/src/App.tsx @@ -8,6 +8,7 @@ import { me, clearToken, type AuthUser } from "./api"; import { SiteLayout } from "./site/components/layout/site-layout"; import HomePage from "./site/pages/home"; import DownloadPage from "./site/pages/download"; +import PricingPage from "./site/pages/pricing"; import NotFoundPage from "./site/pages/not-found"; // 后台鉴权门:只在访问 /admin 时跑 me()(官网公开、不打鉴权)。 @@ -54,6 +55,7 @@ export default function App() { {/* 官网:根 / + /download,公开;未知路径落官网 404(保留 header/footer) */} }> } /> + } /> } /> } /> diff --git a/sundynix-admin/src/site/components/layout/site-header.tsx b/sundynix-admin/src/site/components/layout/site-header.tsx index dcaa2c5..38efef7 100644 --- a/sundynix-admin/src/site/components/layout/site-header.tsx +++ b/sundynix-admin/src/site/components/layout/site-header.tsx @@ -10,6 +10,7 @@ const NAV_ITEMS = [ { label: '产品', to: '/', end: true }, { label: '功能', to: '/#features' }, { label: '架构', to: '/#architecture' }, + { label: '定价', to: '/pricing' }, ] export function SiteHeader() { diff --git a/sundynix-admin/src/site/pages/pricing.tsx b/sundynix-admin/src/site/pages/pricing.tsx new file mode 100644 index 0000000..29cd909 --- /dev/null +++ b/sundynix-admin/src/site/pages/pricing.tsx @@ -0,0 +1,151 @@ +import { useEffect, useState } from 'react' +import { GATEWAY } from '../../api' + +// 官网定价页。数据来自后台配置的「订阅套餐 / 积分包」,**不写死在前端**—— +// 运营改价、上下架、调发放节奏都在管理端完成,这里跟着变,不用发版。 +// +// 公开可见(/api/v1/pricing 不挂鉴权):让人在登录前就看得到价格, +// 这是购买转化的前提,也是官网存在的意义。 + +const MICRO = 1_000_000 +const credits = (m: number) => (m / MICRO).toLocaleString('zh-CN', { maximumFractionDigits: 0 }) +const yuan = (fen: number) => (fen / 100).toLocaleString('zh-CN', { minimumFractionDigits: fen % 100 === 0 ? 0 : 2 }) + +interface SubPlan { + id: string + name: string + price_fen: number + duration_days: number + refill_credits_micro: number + refill_interval_days: number +} + +interface Pack { + id: string + name: string + credits_micro: number + price_fen: number +} + +// 结账在 Web 面完成(那里有登录态、租户上下文与支付轮询)。官网只负责"看价 → 去买", +// 避免同一套支付流程在两处各实现一遍。 +const CHECKOUT = '/usage' + +export default function PricingPage() { + const [plans, setPlans] = useState([]) + const [packs, setPacks] = useState([]) + const [state, setState] = useState<'loading' | 'ready' | 'error'>('loading') + + useEffect(() => { + // GATEWAY 生产构建为空串(同源相对),开发时指向 :8080——写死相对路径会打到 vite + fetch(`${GATEWAY}/api/v1/pricing`) + .then((r) => (r.ok ? r.json() : Promise.reject(new Error(String(r.status))))) + .then((d: { plans?: SubPlan[]; packs?: Pack[] }) => { + setPlans(d.plans ?? []) + setPacks(d.packs ?? []) + setState('ready') + }) + .catch(() => setState('error')) + }, []) + + return ( +
+
+

PRICING

+

按用量计费,先付后用

+

+ 所有消耗以「积分」结算。订阅按周期自动发放积分,也可以单次购买积分包。 + 没有隐藏费用,用不完的积分不清零。 +

+
+ + {state === 'loading' &&

正在获取价格…

} + {state === 'error' && ( +

+ 价格暂时取不到,请稍后再试,或直接 + + 前往账单页 + + 。 +

+ )} + + {state === 'ready' && ( + <> + {plans.length > 0 && ( +
+

订阅

+

+ 购买一个周期,期内按节奏发放积分。到期即结束,不会自动续费、不会自动扣款。 +

+
+ {plans.map((p) => { + const times = p.refill_interval_days > 0 ? Math.floor(p.duration_days / p.refill_interval_days) : 0 + return ( +
+

{p.name}

+
+ ¥{yuan(p.price_fen)} + / {p.duration_days} 天 +
+
    +
  • + 每 {p.refill_interval_days} 天发放{' '} + {credits(p.refill_credits_micro)} 积分 +
  • +
  • + 周期内共 {times} 次,合计{' '} + {credits(p.refill_credits_micro * times)} 积分 +
  • +
  • 积分累加,用不完不清零
  • +
+ + 购买 + +
+ ) + })} +
+
+ )} + + {packs.length > 0 && ( +
+

积分包

+

一次性购买,用完再买,适合用量不规律的场景。

+
+ {packs.map((p) => ( +
+

{p.name}

+
¥{yuan(p.price_fen)}
+

{credits(p.credits_micro)} 积分

+ + 购买 → + +
+ ))} +
+
+ )} + + {plans.length === 0 && packs.length === 0 && ( +

价格方案即将上线。

+ )} + +

+ 支付由微信支付提供。购买需要先登录;企业采购与私有化部署请通过页脚联系方式与我们联系。 +

+ + )} +
+ ) +} diff --git a/sundynix-gateway/internal/handler/subscription.go b/sundynix-gateway/internal/handler/subscription.go index 636dc50..c28cc45 100644 --- a/sundynix-gateway/internal/handler/subscription.go +++ b/sundynix-gateway/internal/handler/subscription.go @@ -36,6 +36,18 @@ func (h *Handler) MySubscription(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"subscription": sub, "plan": h.db.GetSubPlan(ctx, sub.PlanID)}) } +// PublicPricing: GET /api/v1/pricing —— **公开**(无需登录):官网定价页用。 +// 官网要在用户登录前就能看到价格,所以这条不挂鉴权;只暴露在售项,且不含任何 +// 内部字段(成本、权重、租户信息一概不出去)。 +func (h *Handler) PublicPricing(c *gin.Context) { + ctx := c.Request.Context() + packs, _ := h.db.ActivePacks(ctx) + c.JSON(http.StatusOK, gin.H{ + "plans": h.db.ListSubPlans(ctx, true), + "packs": packs, + }) +} + // ---- 管理端 ---- // AdminSubPlans: GET /api/v1/admin/sub-plans —— 全部套餐(含下架)。 diff --git a/sundynix-gateway/internal/router/router.go b/sundynix-gateway/internal/router/router.go index ef24fd1..12fdef6 100644 --- a/sundynix-gateway/internal/router/router.go +++ b/sundynix-gateway/internal/router/router.go @@ -50,6 +50,7 @@ func New(db *store.Postgres, cache *store.Redis, bus *nats.Bus, blobStore *blob. api := r.Group("/api/v1") { // —— 公开:鉴权端点 / 健康 / 按 task_id 寻址的 SSE 与导出(EventSource/下载无法带 Bearer)—— + api.GET("/pricing", h.PublicPricing) // 公开定价(官网未登录也要能看价,故不挂鉴权) api.POST("/auth/register", h.Register) // 注册 + 签发 JWT api.POST("/auth/login", h.Login) // 登录 + 签发 JWT api.GET("/auth/me", h.Me) // 当前登录用户(无效令牌 → 401)