feat(data): add paywall payment APIs

This commit is contained in:
2026-06-18 12:53:06 +08:00
parent 567d3f9184
commit 35c30ac31e
38 changed files with 840 additions and 10 deletions
@@ -13,6 +13,10 @@ export class ChatHistoryResponse {
declare readonly total: number; declare readonly total: number;
declare readonly limit: number; declare readonly limit: number;
declare readonly offset: number; declare readonly offset: number;
declare readonly isVip: boolean;
declare readonly privateFreeLimit: number;
declare readonly privateUsedToday: number;
declare readonly privateCanViewFree: boolean;
private constructor(input: ChatHistoryResponseInput) { private constructor(input: ChatHistoryResponseInput) {
const data = ChatHistoryResponseSchema.parse(input); const data = ChatHistoryResponseSchema.parse(input);
+3
View File
@@ -12,6 +12,9 @@ export class ChatMessage {
declare readonly content: string; declare readonly content: string;
declare readonly id: string; declare readonly id: string;
declare readonly createdAt: string; declare readonly createdAt: string;
declare readonly isPrivate: boolean | null;
declare readonly privateLocked: boolean | null;
declare readonly privateHint: string | null;
private constructor(input: ChatMessageInput) { private constructor(input: ChatMessageInput) {
const data = ChatMessageSchema.parse(input); const data = ChatMessageSchema.parse(input);
+9 -1
View File
@@ -5,6 +5,7 @@ import {
ChatSendResponseSchema, ChatSendResponseSchema,
type ChatSendResponseInput, type ChatSendResponseInput,
type ChatSendResponseData, type ChatSendResponseData,
type ChatBlockDetailData,
} from "@/data/schemas/chat/chat_send_response"; } from "@/data/schemas/chat/chat_send_response";
export class ChatSendResponse { export class ChatSendResponse {
@@ -12,13 +13,20 @@ export class ChatSendResponse {
declare readonly reply: string; declare readonly reply: string;
declare readonly voiceUrl: string; declare readonly voiceUrl: string;
declare readonly audioUrl: string; declare readonly audioUrl: string;
declare readonly intimidadChange: number; declare readonly intimacyChange: number;
declare readonly newIntimacy: number; declare readonly newIntimacy: number;
declare readonly relationshipStage: string; declare readonly relationshipStage: string;
declare readonly currentMood: string; declare readonly currentMood: string;
declare readonly messageId: string; declare readonly messageId: string;
declare readonly isGuest: boolean; declare readonly isGuest: boolean;
declare readonly timestamp: number; declare readonly timestamp: number;
declare readonly blocked: boolean | null;
declare readonly blockReason: string | null;
declare readonly blockDetail: ChatBlockDetailData | null;
declare readonly paywallTriggered: boolean;
declare readonly showUpgrade: boolean;
declare readonly imageType: string | null;
declare readonly imageUrl: string | null;
private constructor(input: ChatSendResponseInput) { private constructor(input: ChatSendResponseInput) {
const data = ChatSendResponseSchema.parse(input); const data = ChatSendResponseSchema.parse(input);
+2
View File
@@ -14,3 +14,5 @@ export * from "./send_message_request";
export * from "./stt_data"; export * from "./stt_data";
export * from "./sync_message"; export * from "./sync_message";
export * from "./ui_message"; export * from "./ui_message";
export * from "./unlock_private_request";
export * from "./unlock_private_response";
@@ -0,0 +1,30 @@
/**
* 私密消息解锁请求 DTO
*/
import {
UnlockPrivateRequestSchema,
type UnlockPrivateRequestData,
type UnlockPrivateRequestInput,
} from "@/data/schemas/chat/unlock_private_request";
export class UnlockPrivateRequest {
declare readonly messageId: string;
private constructor(input: UnlockPrivateRequestInput) {
const data = UnlockPrivateRequestSchema.parse(input);
Object.assign(this, data);
Object.freeze(this);
}
static from(input: UnlockPrivateRequestInput): UnlockPrivateRequest {
return new UnlockPrivateRequest(input);
}
static fromJson(json: unknown): UnlockPrivateRequest {
return UnlockPrivateRequest.from(json as UnlockPrivateRequestInput);
}
toJson(): UnlockPrivateRequestData {
return UnlockPrivateRequestSchema.parse(this);
}
}
@@ -0,0 +1,37 @@
/**
* 私密消息解锁响应 DTO
*/
import {
UnlockPrivateResponseSchema,
type UnlockPrivateReason,
type UnlockPrivateResponseData,
type UnlockPrivateResponseInput,
} from "@/data/schemas/chat/unlock_private_response";
export class UnlockPrivateResponse {
declare readonly unlocked: boolean;
declare readonly content: string | null;
declare readonly showUpgrade: boolean;
declare readonly paywallTriggered: boolean;
declare readonly privateFreeLimit: number;
declare readonly privateUsedToday: number;
declare readonly reason: UnlockPrivateReason;
private constructor(input: UnlockPrivateResponseInput) {
const data = UnlockPrivateResponseSchema.parse(input);
Object.assign(this, data);
Object.freeze(this);
}
static from(input: UnlockPrivateResponseInput): UnlockPrivateResponse {
return new UnlockPrivateResponse(input);
}
static fromJson(json: unknown): UnlockPrivateResponse {
return UnlockPrivateResponse.from(json as UnlockPrivateResponseInput);
}
toJson(): UnlockPrivateResponseData {
return UnlockPrivateResponseSchema.parse(this);
}
}
@@ -0,0 +1,39 @@
/**
* 创建支付订单请求 DTO
*/
import {
CreatePaymentOrderRequestSchema,
type CreatePaymentOrderRequestData,
type CreatePaymentOrderRequestInput,
type PayChannel,
} from "@/data/schemas/payment/create_payment_order_request";
export type { PayChannel };
export class CreatePaymentOrderRequest {
declare readonly planId: string;
declare readonly payChannel: PayChannel;
declare readonly autoRenew: boolean;
private constructor(input: CreatePaymentOrderRequestInput) {
const data = CreatePaymentOrderRequestSchema.parse(input);
Object.assign(this, data);
Object.freeze(this);
}
static from(
input: CreatePaymentOrderRequestInput,
): CreatePaymentOrderRequest {
return new CreatePaymentOrderRequest(input);
}
static fromJson(json: unknown): CreatePaymentOrderRequest {
return CreatePaymentOrderRequest.from(
json as CreatePaymentOrderRequestInput,
);
}
toJson(): CreatePaymentOrderRequestData {
return CreatePaymentOrderRequestSchema.parse(this);
}
}
@@ -0,0 +1,35 @@
/**
* 创建支付订单响应 DTO
*/
import {
CreatePaymentOrderResponseSchema,
type CreatePaymentOrderResponseData,
type CreatePaymentOrderResponseInput,
} from "@/data/schemas/payment/create_payment_order_response";
export class CreatePaymentOrderResponse {
declare readonly orderId: string;
declare readonly payParams: Record<string, unknown>;
private constructor(input: CreatePaymentOrderResponseInput) {
const data = CreatePaymentOrderResponseSchema.parse(input);
Object.assign(this, data);
Object.freeze(this);
}
static from(
input: CreatePaymentOrderResponseInput,
): CreatePaymentOrderResponse {
return new CreatePaymentOrderResponse(input);
}
static fromJson(json: unknown): CreatePaymentOrderResponse {
return CreatePaymentOrderResponse.from(
json as CreatePaymentOrderResponseInput,
);
}
toJson(): CreatePaymentOrderResponseData {
return CreatePaymentOrderResponseSchema.parse(this);
}
}
+11
View File
@@ -0,0 +1,11 @@
/**
* @file Payment DTO barrel.
*/
export * from "./create_payment_order_request";
export * from "./create_payment_order_response";
export * from "./payment_order_status_response";
export * from "./payment_plan";
export * from "./payment_plans_response";
export * from "./payment_vip_status_response";
export * from "./payment_websocket_event";
@@ -0,0 +1,38 @@
/**
* 支付订单状态响应 DTO
*/
import {
PaymentOrderStatusResponseSchema,
type PaymentOrderStatus,
type PaymentOrderStatusResponseData,
type PaymentOrderStatusResponseInput,
} from "@/data/schemas/payment/payment_order_status_response";
export class PaymentOrderStatusResponse {
declare readonly orderId: string;
declare readonly status: PaymentOrderStatus;
declare readonly orderType: string;
declare readonly planId: string;
private constructor(input: PaymentOrderStatusResponseInput) {
const data = PaymentOrderStatusResponseSchema.parse(input);
Object.assign(this, data);
Object.freeze(this);
}
static from(
input: PaymentOrderStatusResponseInput,
): PaymentOrderStatusResponse {
return new PaymentOrderStatusResponse(input);
}
static fromJson(json: unknown): PaymentOrderStatusResponse {
return PaymentOrderStatusResponse.from(
json as PaymentOrderStatusResponseInput,
);
}
toJson(): PaymentOrderStatusResponseData {
return PaymentOrderStatusResponseSchema.parse(this);
}
}
+36
View File
@@ -0,0 +1,36 @@
/**
* 支付套餐 DTO
*/
import {
PaymentPlanSchema,
type PaymentPlanData,
type PaymentPlanInput,
} from "@/data/schemas/payment/payment_plan";
export class PaymentPlan {
declare readonly planId: string;
declare readonly planName: string;
declare readonly orderType: string;
declare readonly amountCents: number;
declare readonly currency: string;
declare readonly vipDays: number | null;
declare readonly dolAmount: number | null;
private constructor(input: PaymentPlanInput) {
const data = PaymentPlanSchema.parse(input);
Object.assign(this, data);
Object.freeze(this);
}
static from(input: PaymentPlanInput): PaymentPlan {
return new PaymentPlan(input);
}
static fromJson(json: unknown): PaymentPlan {
return PaymentPlan.from(json as PaymentPlanInput);
}
toJson(): PaymentPlanData {
return PaymentPlanSchema.parse(this);
}
}
@@ -0,0 +1,36 @@
/**
* 支付套餐列表响应 DTO
*/
import {
PaymentPlansResponseSchema,
type PaymentPlansResponseData,
type PaymentPlansResponseInput,
} from "@/data/schemas/payment/payment_plans_response";
import { PaymentPlan } from "./payment_plan";
export class PaymentPlansResponse {
declare readonly plans: PaymentPlan[];
private constructor(input: PaymentPlansResponseInput) {
const data = PaymentPlansResponseSchema.parse(input);
Object.assign(this, {
plans: data.plans.map((plan) => PaymentPlan.from(plan)),
});
Object.freeze(this);
}
static from(input: PaymentPlansResponseInput): PaymentPlansResponse {
return new PaymentPlansResponse(input);
}
static fromJson(json: unknown): PaymentPlansResponse {
return PaymentPlansResponse.from(json as PaymentPlansResponseInput);
}
toJson(): PaymentPlansResponseData {
return {
plans: this.plans.map((plan) => plan.toJson()),
};
}
}
@@ -0,0 +1,33 @@
/**
* VIP 状态响应 DTO
*/
import {
PaymentVipStatusResponseSchema,
type PaymentVipStatusResponseData,
type PaymentVipStatusResponseInput,
} from "@/data/schemas/payment/payment_vip_status_response";
export class PaymentVipStatusResponse {
declare readonly isVip: boolean;
declare readonly vipExpiresAt: string | null;
private constructor(input: PaymentVipStatusResponseInput) {
const data = PaymentVipStatusResponseSchema.parse(input);
Object.assign(this, data);
Object.freeze(this);
}
static from(input: PaymentVipStatusResponseInput): PaymentVipStatusResponse {
return new PaymentVipStatusResponse(input);
}
static fromJson(json: unknown): PaymentVipStatusResponse {
return PaymentVipStatusResponse.from(
json as PaymentVipStatusResponseInput,
);
}
toJson(): PaymentVipStatusResponseData {
return PaymentVipStatusResponseSchema.parse(this);
}
}
@@ -0,0 +1,33 @@
/**
* 支付 WebSocket 事件 DTO
*/
import {
PaymentWebSocketEventSchema,
type PaymentWebSocketEventData,
type PaymentWebSocketEventInput,
} from "@/data/schemas/payment/payment_websocket_event";
export class PaymentWebSocketEvent {
declare readonly type: PaymentWebSocketEventData["type"];
declare readonly orderId: string;
private constructor(input: PaymentWebSocketEventInput) {
const data = PaymentWebSocketEventSchema.parse(input);
Object.assign(this, data);
Object.freeze(this);
}
static from(input: PaymentWebSocketEventInput): PaymentWebSocketEvent {
return new PaymentWebSocketEvent(input);
}
static fromJson(json: unknown): PaymentWebSocketEvent {
return PaymentWebSocketEvent.from(json as PaymentWebSocketEventInput);
}
toJson(): PaymentWebSocketEventData {
return PaymentWebSocketEventSchema.parse(this);
}
}
export type { PaymentWebSocketEventData };
+11
View File
@@ -21,6 +21,8 @@ import {
ChatMessage, ChatMessage,
ChatSendResponse, ChatSendResponse,
SendMessageRequest, SendMessageRequest,
UnlockPrivateRequest,
UnlockPrivateResponse,
} from "@/data/dto/chat"; } from "@/data/dto/chat";
import { Result } from "@/utils"; import { Result } from "@/utils";
import type { IChatRepository } from "@/data/repositories/interfaces"; import type { IChatRepository } from "@/data/repositories/interfaces";
@@ -57,6 +59,15 @@ export class ChatRepository implements IChatRepository {
return Result.wrap(() => this.api.getHistory(limit, offset)); return Result.wrap(() => this.api.getHistory(limit, offset));
} }
/** 解锁私密消息。 */
async unlockPrivateMessage(
messageId: string,
): Promise<Result<UnlockPrivateResponse>> {
return Result.wrap(() =>
this.api.unlockPrivateMessage(UnlockPrivateRequest.from({ messageId })),
);
}
// ============ 本地(Dexie)操作 ============ // ============ 本地(Dexie)操作 ============
/** 把一条消息写入本地存储。 */ /** 把一条消息写入本地存储。 */
+2
View File
@@ -5,8 +5,10 @@
export * from "./auth_repository"; export * from "./auth_repository";
export * from "./chat_repository"; export * from "./chat_repository";
export * from "./metrics_repository"; export * from "./metrics_repository";
export * from "./payment_repository";
export * from "./user_repository"; export * from "./user_repository";
export * from "./interfaces/iauth_repository"; export * from "./interfaces/iauth_repository";
export * from "./interfaces/ichat_repository"; export * from "./interfaces/ichat_repository";
export * from "./interfaces/imetrics_repository"; export * from "./interfaces/imetrics_repository";
export * from "./interfaces/ipayment_repository";
export * from "./interfaces/iuser_repository"; export * from "./interfaces/iuser_repository";
@@ -16,6 +16,7 @@ import type {
ChatHistoryResponse, ChatHistoryResponse,
ChatMessage, ChatMessage,
ChatSendResponse, ChatSendResponse,
UnlockPrivateResponse,
} from "@/data/dto/chat"; } from "@/data/dto/chat";
export interface IChatRepository { export interface IChatRepository {
@@ -28,6 +29,9 @@ export interface IChatRepository {
/** 获取聊天历史,分页参数默认 limit=50, offset=0。 */ /** 获取聊天历史,分页参数默认 limit=50, offset=0。 */
getHistory(limit?: number, offset?: number): Promise<Result<ChatHistoryResponse>>; getHistory(limit?: number, offset?: number): Promise<Result<ChatHistoryResponse>>;
/** 解锁私密消息。 */
unlockPrivateMessage(messageId: string): Promise<Result<UnlockPrivateResponse>>;
/** 把一条消息写入本地存储。 */ /** 把一条消息写入本地存储。 */
saveMessageToLocal(message: ChatMessage): Promise<Result<void>>; saveMessageToLocal(message: ChatMessage): Promise<Result<void>>;
@@ -5,4 +5,5 @@
export * from "./iauth_repository"; export * from "./iauth_repository";
export * from "./ichat_repository"; export * from "./ichat_repository";
export * from "./imetrics_repository"; export * from "./imetrics_repository";
export * from "./ipayment_repository";
export * from "./iuser_repository"; export * from "./iuser_repository";
@@ -0,0 +1,31 @@
"use client";
/**
* IPaymentRepository 接口
*/
import type {
CreatePaymentOrderResponse,
PayChannel,
PaymentOrderStatusResponse,
PaymentPlansResponse,
PaymentVipStatusResponse,
} from "@/data/dto/payment";
import type { Result } from "@/utils/result";
export interface IPaymentRepository {
/** 获取套餐列表。 */
getPlans(): Promise<Result<PaymentPlansResponse>>;
/** 创建支付订单。 */
createOrder(
planId: string,
payChannel: PayChannel,
autoRenew: boolean,
): Promise<Result<CreatePaymentOrderResponse>>;
/** 查询支付订单状态。 */
getOrderStatus(orderId: string): Promise<Result<PaymentOrderStatusResponse>>;
/** 查询当前用户 VIP 状态。 */
getVipStatus(): Promise<Result<PaymentVipStatusResponse>>;
}
@@ -0,0 +1,56 @@
"use client";
/**
* PaymentRepository
*
* 支付 / 付费墙相关远程调用。
*/
import {
CreatePaymentOrderRequest,
CreatePaymentOrderResponse,
PayChannel,
PaymentOrderStatusResponse,
PaymentPlansResponse,
PaymentVipStatusResponse,
} from "@/data/dto/payment";
import { PaymentApi, paymentApi } from "@/data/services/api";
import type { IPaymentRepository } from "@/data/repositories/interfaces";
import { Result } from "@/utils/result";
export class PaymentRepository implements IPaymentRepository {
constructor(private readonly api: PaymentApi) {}
/** 获取套餐列表。 */
async getPlans(): Promise<Result<PaymentPlansResponse>> {
return Result.wrap(() => this.api.getPlans());
}
/** 创建支付订单。 */
async createOrder(
planId: string,
payChannel: PayChannel,
autoRenew: boolean,
): Promise<Result<CreatePaymentOrderResponse>> {
const request = CreatePaymentOrderRequest.from({
planId,
payChannel,
autoRenew,
});
return Result.wrap(() => this.api.createOrder(request));
}
/** 查询支付订单状态。 */
async getOrderStatus(
orderId: string,
): Promise<Result<PaymentOrderStatusResponse>> {
return Result.wrap(() => this.api.getOrderStatus(orderId));
}
/** 查询当前用户 VIP 状态。 */
async getVipStatus(): Promise<Result<PaymentVipStatusResponse>> {
return Result.wrap(() => this.api.getVipStatus());
}
}
/** 全局单例。 */
export const paymentRepository = new PaymentRepository(paymentApi);
@@ -10,6 +10,10 @@ export const ChatHistoryResponseSchema = z.object({
total: z.number().default(0), total: z.number().default(0),
limit: z.number(), limit: z.number(),
offset: z.number(), offset: z.number(),
isVip: z.boolean(),
privateFreeLimit: z.number(),
privateUsedToday: z.number(),
privateCanViewFree: z.boolean(),
}); });
export type ChatHistoryResponseInput = z.input<typeof ChatHistoryResponseSchema>; export type ChatHistoryResponseInput = z.input<typeof ChatHistoryResponseSchema>;
+11 -2
View File
@@ -4,12 +4,21 @@
*/ */
import { z } from "zod"; import { z } from "zod";
export const ChatMessageSchema = z.object({ export const ChatMessageSchema = z
.object({
role: z.string(), role: z.string(),
content: z.string(), content: z.string(),
id: z.string().default(""), id: z.string().default(""),
createdAt: z.string().default(""), createdAt: z.string().default(""),
}); created_at: z.string().optional(),
isPrivate: z.boolean().nullable().default(null),
privateLocked: z.boolean().nullable().default(null),
privateHint: z.string().nullable().default(null),
})
.transform(({ created_at, ...data }) => ({
...data,
createdAt: data.createdAt || created_at || "",
}));
export type ChatMessageInput = z.input<typeof ChatMessageSchema>; export type ChatMessageInput = z.input<typeof ChatMessageSchema>;
export type ChatMessageData = z.output<typeof ChatMessageSchema>; export type ChatMessageData = z.output<typeof ChatMessageSchema>;
@@ -4,6 +4,12 @@
*/ */
import { z } from "zod"; import { z } from "zod";
export const ChatBlockDetailSchema = z.object({
type: z.string(),
usedToday: z.number(),
limit: z.number(),
});
export const ChatSendResponseSchema = z.object({ export const ChatSendResponseSchema = z.object({
mode: z.string().default(""), mode: z.string().default(""),
reply: z.string(), reply: z.string(),
@@ -17,7 +23,15 @@ export const ChatSendResponseSchema = z.object({
isGuest: z.boolean().default(false), isGuest: z.boolean().default(false),
// 函数式 default —— 每次 parse 时重新调 Date.now()(后端不返回 timestamp 时用当下时间) // 函数式 default —— 每次 parse 时重新调 Date.now()(后端不返回 timestamp 时用当下时间)
timestamp: z.number().default(() => Date.now()), timestamp: z.number().default(() => Date.now()),
blocked: z.boolean().nullable().default(null),
blockReason: z.string().nullable().default(null),
blockDetail: ChatBlockDetailSchema.nullable().default(null),
paywallTriggered: z.boolean(),
showUpgrade: z.boolean(),
imageType: z.string().nullable().default(null),
imageUrl: z.string().nullable().default(null),
}); });
export type ChatSendResponseInput = z.input<typeof ChatSendResponseSchema>; export type ChatSendResponseInput = z.input<typeof ChatSendResponseSchema>;
export type ChatSendResponseData = z.output<typeof ChatSendResponseSchema>; export type ChatSendResponseData = z.output<typeof ChatSendResponseSchema>;
export type ChatBlockDetailData = z.output<typeof ChatBlockDetailSchema>;
+2
View File
@@ -12,3 +12,5 @@ export * from "./image_upload_response";
export * from "./send_message_request"; export * from "./send_message_request";
export * from "./stt_data"; export * from "./stt_data";
export * from "./sync_message"; export * from "./sync_message";
export * from "./unlock_private_request";
export * from "./unlock_private_response";
@@ -0,0 +1,15 @@
/**
* 私密消息解锁请求
*/
import { z } from "zod";
export const UnlockPrivateRequestSchema = z.object({
messageId: z.string(),
});
export type UnlockPrivateRequestInput = z.input<
typeof UnlockPrivateRequestSchema
>;
export type UnlockPrivateRequestData = z.output<
typeof UnlockPrivateRequestSchema
>;
@@ -0,0 +1,29 @@
/**
* 私密消息解锁响应
*/
import { z } from "zod";
export const UnlockPrivateReasonSchema = z.enum([
"ok",
"quota_exceeded",
"not_private",
"not_found",
]);
export const UnlockPrivateResponseSchema = z.object({
unlocked: z.boolean(),
content: z.string().nullable(),
showUpgrade: z.boolean(),
paywallTriggered: z.boolean(),
privateFreeLimit: z.number(),
privateUsedToday: z.number(),
reason: UnlockPrivateReasonSchema,
});
export type UnlockPrivateReason = z.output<typeof UnlockPrivateReasonSchema>;
export type UnlockPrivateResponseInput = z.input<
typeof UnlockPrivateResponseSchema
>;
export type UnlockPrivateResponseData = z.output<
typeof UnlockPrivateResponseSchema
>;
@@ -0,0 +1,20 @@
/**
* 创建支付订单请求
*/
import { z } from "zod";
export const PayChannelSchema = z.enum(["stripe", "ezpay"]);
export const CreatePaymentOrderRequestSchema = z.object({
planId: z.string(),
payChannel: PayChannelSchema,
autoRenew: z.boolean(),
});
export type PayChannel = z.output<typeof PayChannelSchema>;
export type CreatePaymentOrderRequestInput = z.input<
typeof CreatePaymentOrderRequestSchema
>;
export type CreatePaymentOrderRequestData = z.output<
typeof CreatePaymentOrderRequestSchema
>;
@@ -0,0 +1,16 @@
/**
* 创建支付订单响应
*/
import { z } from "zod";
export const CreatePaymentOrderResponseSchema = z.object({
orderId: z.string(),
payParams: z.record(z.string(), z.unknown()),
});
export type CreatePaymentOrderResponseInput = z.input<
typeof CreatePaymentOrderResponseSchema
>;
export type CreatePaymentOrderResponseData = z.output<
typeof CreatePaymentOrderResponseSchema
>;
+11
View File
@@ -0,0 +1,11 @@
/**
* @file Payment schema barrel.
*/
export * from "./create_payment_order_request";
export * from "./create_payment_order_response";
export * from "./payment_order_status_response";
export * from "./payment_plan";
export * from "./payment_plans_response";
export * from "./payment_vip_status_response";
export * from "./payment_websocket_event";
@@ -0,0 +1,21 @@
/**
* 支付订单状态响应
*/
import { z } from "zod";
export const PaymentOrderStatusSchema = z.enum(["pending", "paid", "failed"]);
export const PaymentOrderStatusResponseSchema = z.object({
orderId: z.string(),
status: PaymentOrderStatusSchema,
orderType: z.string(),
planId: z.string(),
});
export type PaymentOrderStatus = z.output<typeof PaymentOrderStatusSchema>;
export type PaymentOrderStatusResponseInput = z.input<
typeof PaymentOrderStatusResponseSchema
>;
export type PaymentOrderStatusResponseData = z.output<
typeof PaymentOrderStatusResponseSchema
>;
+44
View File
@@ -0,0 +1,44 @@
/**
* 支付套餐
*/
import { z } from "zod";
const PaymentPlanWireSchema = z.object({
plan_id: z.string(),
plan_name: z.string(),
order_type: z.string(),
amount_cents: z.number(),
currency: z.string(),
vip_days: z.number().nullable(),
dol_amount: z.number().nullable(),
});
export const PaymentPlanSchema = z
.preprocess((value) => {
if (!value || typeof value !== "object") return value;
const data = value as Record<string, unknown>;
if ("planId" in data) {
return {
plan_id: data.planId,
plan_name: data.planName,
order_type: data.orderType,
amount_cents: data.amountCents,
currency: data.currency,
vip_days: data.vipDays,
dol_amount: data.dolAmount,
};
}
return value;
}, PaymentPlanWireSchema)
.transform((data) => ({
planId: data.plan_id,
planName: data.plan_name,
orderType: data.order_type,
amountCents: data.amount_cents,
currency: data.currency,
vipDays: data.vip_days,
dolAmount: data.dol_amount,
}));
export type PaymentPlanInput = z.input<typeof PaymentPlanSchema>;
export type PaymentPlanData = z.output<typeof PaymentPlanSchema>;
@@ -0,0 +1,17 @@
/**
* 支付套餐列表响应
*/
import { z } from "zod";
import { PaymentPlanSchema } from "./payment_plan";
export const PaymentPlansResponseSchema = z.object({
plans: z.array(PaymentPlanSchema),
});
export type PaymentPlansResponseInput = z.input<
typeof PaymentPlansResponseSchema
>;
export type PaymentPlansResponseData = z.output<
typeof PaymentPlansResponseSchema
>;
@@ -0,0 +1,16 @@
/**
* VIP 状态响应
*/
import { z } from "zod";
export const PaymentVipStatusResponseSchema = z.object({
isVip: z.boolean(),
vipExpiresAt: z.string().nullable(),
});
export type PaymentVipStatusResponseInput = z.input<
typeof PaymentVipStatusResponseSchema
>;
export type PaymentVipStatusResponseData = z.output<
typeof PaymentVipStatusResponseSchema
>;
@@ -0,0 +1,52 @@
/**
* 支付 WebSocket 事件
*/
import { z } from "zod";
export const PaymentSuccessVipEventSchema = z.object({
type: z.literal("payment_success"),
orderId: z.string(),
payType: z.literal("vip"),
planName: z.string(),
vipExpiresAt: z.string().nullable(),
});
export const PaymentSuccessDolEventSchema = z.object({
type: z.literal("payment_success"),
orderId: z.string(),
payType: z.literal("dol"),
planName: z.string(),
dolAmount: z.number(),
dolBalance: z.number(),
});
export const PaymentFailedEventSchema = z.object({
type: z.literal("payment_failed"),
orderId: z.string(),
info: z.string(),
});
export const PaymentSuccessEventSchema = z.discriminatedUnion("payType", [
PaymentSuccessVipEventSchema,
PaymentSuccessDolEventSchema,
]);
export const PaymentWebSocketEventSchema = z.union([
PaymentSuccessEventSchema,
PaymentFailedEventSchema,
]);
export type PaymentSuccessVipEventData = z.output<
typeof PaymentSuccessVipEventSchema
>;
export type PaymentSuccessDolEventData = z.output<
typeof PaymentSuccessDolEventSchema
>;
export type PaymentFailedEventData = z.output<typeof PaymentFailedEventSchema>;
export type PaymentSuccessEventData = z.output<typeof PaymentSuccessEventSchema>;
export type PaymentWebSocketEventInput = z.input<
typeof PaymentWebSocketEventSchema
>;
export type PaymentWebSocketEventData = z.output<
typeof PaymentWebSocketEventSchema
>;
+12 -3
View File
@@ -71,13 +71,19 @@ export class ApiPath {
// ============ 支付相关 ============ // ============ 支付相关 ============
/** 创建充值订单 */ /** 创建充值订单 */
static readonly paymentCreateOrder = `${ApiPath._payment}/order/create`; static readonly paymentCreateOrder = `${ApiPath._payment}/create-order`;
/** 查询订单状态 */ /** 查询订单状态 */
static readonly paymentOrderStatus = `${ApiPath._payment}/order/status`; static readonly paymentOrderStatus = `${ApiPath._payment}/order-status`;
/** 获取商品套餐列表 */ /** 获取商品套餐列表 */
static readonly paymentProducts = `${ApiPath._payment}/products`; static readonly paymentPlans = `${ApiPath._payment}/plans`;
/** 查询当前用户 VIP 状态 */
static readonly paymentVipStatus = `${ApiPath._payment}/vip-status`;
/** @deprecated PAYWALL_API 使用 paymentPlans。 */
static readonly paymentProducts = ApiPath.paymentPlans;
/** 申请退款 */ /** 申请退款 */
static readonly paymentRefund = `${ApiPath._payment}/refund`; static readonly paymentRefund = `${ApiPath._payment}/refund`;
@@ -103,6 +109,9 @@ export class ApiPath {
/** 上传图片 */ /** 上传图片 */
static readonly chatUploadImage = `${ApiPath._chat}/upload-image`; static readonly chatUploadImage = `${ApiPath._chat}/upload-image`;
/** 解锁私密消息 */
static readonly chatUnlockPrivate = `${ApiPath._chat}/unlock-private`;
// ============ 数据看板相关 ============ // ============ 数据看板相关 ============
private static readonly _metrics = `${ApiPath._baseUrl}/metrics`; private static readonly _metrics = `${ApiPath._baseUrl}/metrics`;
+20
View File
@@ -16,6 +16,8 @@ import {
SendMessageRequest, SendMessageRequest,
SttData, SttData,
SyncMessage, SyncMessage,
UnlockPrivateRequest,
UnlockPrivateResponse,
} from "@/data/dto/chat"; } from "@/data/dto/chat";
export class ChatApi { export class ChatApi {
@@ -81,6 +83,24 @@ export class ChatApi {
unwrap(env) as Record<string, unknown> unwrap(env) as Record<string, unknown>
); );
} }
/**
* 解锁私密消息
*/
async unlockPrivateMessage(
body: UnlockPrivateRequest,
): Promise<UnlockPrivateResponse> {
const env = await httpClient<ApiEnvelope<unknown>>(
ApiPath.chatUnlockPrivate,
{
method: "POST",
body: body.toJson(),
},
);
return UnlockPrivateResponse.fromJson(
unwrap(env) as Record<string, unknown>,
);
}
} }
/** /**
+1
View File
@@ -8,5 +8,6 @@ export * from "./auth_api";
export * from "./chat_api"; export * from "./chat_api";
export * from "./http_client"; export * from "./http_client";
export * from "./metrics_api"; export * from "./metrics_api";
export * from "./payment_api";
export * from "./response_helper"; export * from "./response_helper";
export * from "./user_api"; export * from "./user_api";
+80
View File
@@ -0,0 +1,80 @@
/**
* Payment API
*
* 付费墙 / 充值相关接口
*/
import {
CreatePaymentOrderRequest,
CreatePaymentOrderResponse,
PaymentOrderStatusResponse,
PaymentPlansResponse,
PaymentVipStatusResponse,
} from "@/data/dto/payment";
import { ApiPath } from "./api_path";
import { httpClient } from "./http_client";
import { ApiEnvelope, unwrap } from "./response_helper";
export class PaymentApi {
/**
* 获取套餐列表
*/
async getPlans(): Promise<PaymentPlansResponse> {
const env = await httpClient<ApiEnvelope<unknown>>(ApiPath.paymentPlans);
return PaymentPlansResponse.fromJson(
unwrap(env) as Record<string, unknown>,
);
}
/**
* 创建支付订单
*/
async createOrder(
body: CreatePaymentOrderRequest,
): Promise<CreatePaymentOrderResponse> {
const env = await httpClient<ApiEnvelope<unknown>>(
ApiPath.paymentCreateOrder,
{
method: "POST",
body: body.toJson(),
},
);
return CreatePaymentOrderResponse.fromJson(
unwrap(env) as Record<string, unknown>,
);
}
/**
* 查询支付订单状态
*/
async getOrderStatus(
orderId: string,
): Promise<PaymentOrderStatusResponse> {
const env = await httpClient<ApiEnvelope<unknown>>(
ApiPath.paymentOrderStatus,
{
query: { order_id: orderId },
},
);
return PaymentOrderStatusResponse.fromJson(
unwrap(env) as Record<string, unknown>,
);
}
/**
* 查询当前用户 VIP 状态
*/
async getVipStatus(): Promise<PaymentVipStatusResponse> {
const env = await httpClient<ApiEnvelope<unknown>>(
ApiPath.paymentVipStatus,
);
return PaymentVipStatusResponse.fromJson(
unwrap(env) as Record<string, unknown>,
);
}
}
/**
* 全局单例
*/
export const paymentApi = new PaymentApi();