Files
xiaoxia-saas/apps/web/src/api/subscription/subscription.ts
T
xiaoxia 6155b4d21e
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 0s
CI/CD Pipeline / Check push changed paths (push) Successful in 2s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m56s
CI/CD Pipeline / Build Staging API Image (push) Successful in 2m33s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 2m59s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 3m37s
CI/CD Pipeline / Validate - Style (push) Successful in 3m58s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m33s
CI/CD Pipeline / Integration Tests (push) Successful in 5m27s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m6s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m48s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m10s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 4m13s
CI/CD Pipeline / Validate - Security (push) Successful in 9m25s
CI/CD Pipeline / Unit Tests (push) Successful in 10m29s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Failing after 4h19m11s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 4h26m39s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 4h28m51s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 4h25m45s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 4h25m46s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 4h28m48s
CI/CD Pipeline / PR Build Web Image (push) Failing after 4h28m48s
CI/CD Pipeline / PR Build API Image (push) Failing after 4h28m48s
CI/CD Pipeline / Deploy Production (push) Failing after 4h18m14s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 4h18m19s
CI/CD Pipeline / Build Production Web Image (push) Failing after 4h18m19s
CI/CD Pipeline / Canary Release to Production (push) Failing after 4h18m14s
CI/CD Pipeline / Frontend Lint (push) Failing after 4h28m48s
CI/CD Pipeline / Build Production API Image (push) Failing after 4h54m12s
feat(points): 对齐后端积分/订阅 API 最终契约(types + API 层改造) (#1956)
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-16 10:53:58 +08:00

155 lines
4.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 订阅/会员 API 封装
* 对齐后端 staging 实测最终契约(2026-09-16)
*
* Base path: /api/v1/
* 所有请求走 apiClient(已配置 baseURL=/api/v1 和 token 拦截器)。
*/
import apiClient from "../client"
import type {
SubscriptionInfo,
SubscriptionPlan,
SubscriptionPlansResponse,
BillingRecord,
ChangePlanRequest,
ChangePlanResponse,
ToggleAutoRenewRequest,
} from "./types"
const MOCK_DELAY = 500
const MOCK_SUBSCRIPTION: SubscriptionInfo = {
id: "sub_mock_001",
plan_id: "free",
plan_name: "免费版",
status: "active",
billing_cycle: "monthly",
current_period_start: new Date(Date.now() - 30 * 86400_000).toISOString(),
current_period_end: new Date(Date.now() + 30 * 86400_000).toISOString(),
amount: 0,
auto_renew: false,
created_at: new Date(Date.now() - 30 * 86400_000).toISOString(),
}
const MOCK_PLANS: SubscriptionPlan[] = [
{
plan_id: "free",
name: "免费版",
price_cents: 0,
monthly_price_cents: 0,
duration_days: 0,
points_discount: 1,
features: { max_resolution: "720p", free_clips_daily: 3 },
},
{
plan_id: "monthly",
name: "月度会员",
price_cents: 1990,
monthly_price_cents: 1990,
duration_days: 30,
points_discount: 0.9,
features: { max_resolution: "1080p", free_clips_daily: 10 },
},
{
plan_id: "quarterly",
name: "季度会员",
price_cents: 3990,
monthly_price_cents: 1330,
duration_days: 90,
points_discount: 0.85,
features: { max_resolution: "1080p", free_clips_daily: 15 },
},
{
plan_id: "yearly",
name: "年度会员",
price_cents: 15900,
monthly_price_cents: 1325,
duration_days: 365,
points_discount: 0.8,
features: { max_resolution: "4k", free_clips_daily: 30 },
},
]
const MOCK_BILLING: BillingRecord[] = []
const isMock = () => (process.env.POINTS_API_MOCK as string | undefined) === "true"
/** 获取当前订阅 */
export const getCurrentSubscription = async (): Promise<SubscriptionInfo> => {
if (isMock()) {
await new Promise((r) => setTimeout(r, MOCK_DELAY))
return { ...MOCK_SUBSCRIPTION }
}
const { data } = await apiClient.get("/subscription/current")
return data
}
/** 获取所有订阅档位 */
export const getSubscriptionPlans = async (): Promise<SubscriptionPlansResponse> => {
if (isMock()) {
await new Promise((r) => setTimeout(r, MOCK_DELAY))
return { plans: MOCK_PLANS.map((p) => ({ ...p, features: { ...p.features } })) }
}
const { data } = await apiClient.get("/subscription/plans")
return data
}
/** 获取账单记录 */
export const getBillingRecords = async (): Promise<BillingRecord[]> => {
if (isMock()) {
await new Promise((r) => setTimeout(r, MOCK_DELAY))
return MOCK_BILLING.map((r) => ({ ...r }))
}
const { data } = await apiClient.get("/subscription/billing-records")
return data
}
/** 升级/降级套餐 */
export const changePlan = async (request: ChangePlanRequest): Promise<ChangePlanResponse> => {
if (isMock()) {
await new Promise((r) => setTimeout(r, MOCK_DELAY * 2))
const plan = MOCK_PLANS.find((p) => p.plan_id === request.target_plan_id)
if (!plan) return { success: false, message: "套餐不存在" }
const newSub: SubscriptionInfo = {
...MOCK_SUBSCRIPTION,
plan_id: plan.plan_id,
plan_name: plan.name,
billing_cycle: request.billing_cycle,
amount: plan.price_cents,
status: "pending",
current_period_start: new Date().toISOString(),
current_period_end: new Date(Date.now() + plan.duration_days * 86400_000).toISOString(),
auto_renew: true,
}
return {
success: true,
message: "订阅变更成功(mock,支付通道待接入)",
new_subscription: newSub,
}
}
const { data } = await apiClient.post("/subscription/change-plan", request)
return data
}
/** 取消订阅(到期后失效) */
export const cancelSubscription = async (): Promise<{ success: boolean; message: string }> => {
if (isMock()) {
await new Promise((r) => setTimeout(r, MOCK_DELAY))
return { success: true, message: "已取消订阅,到期后将不再续费" }
}
const { data } = await apiClient.post("/subscription/cancel")
return data
}
/** 切换自动续费 */
export const toggleAutoRenew = async (
req: ToggleAutoRenewRequest,
): Promise<{ success: boolean; message: string }> => {
if (isMock()) {
await new Promise((r) => setTimeout(r, MOCK_DELAY))
return { success: true, message: req.enabled ? "已开启自动续费" : "已关闭自动续费" }
}
const { data } = await apiClient.post("/subscription/toggle-auto-renew", req)
return data
}