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
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com> Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
155 lines
4.6 KiB
TypeScript
155 lines
4.6 KiB
TypeScript
/**
|
||
* 订阅/会员 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
|
||
}
|