diff --git a/src/data/dto/chat/chat_history_response.ts b/src/data/dto/chat/chat_history_response.ts index 6b4abee6..90926da1 100644 --- a/src/data/dto/chat/chat_history_response.ts +++ b/src/data/dto/chat/chat_history_response.ts @@ -13,6 +13,10 @@ export class ChatHistoryResponse { declare readonly total: number; declare readonly limit: 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) { const data = ChatHistoryResponseSchema.parse(input); diff --git a/src/data/dto/chat/chat_message.ts b/src/data/dto/chat/chat_message.ts index 87dbc0e6..085844e5 100644 --- a/src/data/dto/chat/chat_message.ts +++ b/src/data/dto/chat/chat_message.ts @@ -12,6 +12,9 @@ export class ChatMessage { declare readonly content: string; declare readonly id: string; declare readonly createdAt: string; + declare readonly isPrivate: boolean | null; + declare readonly privateLocked: boolean | null; + declare readonly privateHint: string | null; private constructor(input: ChatMessageInput) { const data = ChatMessageSchema.parse(input); diff --git a/src/data/dto/chat/chat_send_response.ts b/src/data/dto/chat/chat_send_response.ts index a1523c4f..592b97d1 100644 --- a/src/data/dto/chat/chat_send_response.ts +++ b/src/data/dto/chat/chat_send_response.ts @@ -5,6 +5,7 @@ import { ChatSendResponseSchema, type ChatSendResponseInput, type ChatSendResponseData, + type ChatBlockDetailData, } from "@/data/schemas/chat/chat_send_response"; export class ChatSendResponse { @@ -12,13 +13,20 @@ export class ChatSendResponse { declare readonly reply: string; declare readonly voiceUrl: string; declare readonly audioUrl: string; - declare readonly intimidadChange: number; + declare readonly intimacyChange: number; declare readonly newIntimacy: number; declare readonly relationshipStage: string; declare readonly currentMood: string; declare readonly messageId: string; declare readonly isGuest: boolean; 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) { const data = ChatSendResponseSchema.parse(input); diff --git a/src/data/dto/chat/index.ts b/src/data/dto/chat/index.ts index abd3fbee..2d11b76e 100644 --- a/src/data/dto/chat/index.ts +++ b/src/data/dto/chat/index.ts @@ -14,3 +14,5 @@ export * from "./send_message_request"; export * from "./stt_data"; export * from "./sync_message"; export * from "./ui_message"; +export * from "./unlock_private_request"; +export * from "./unlock_private_response"; diff --git a/src/data/dto/chat/unlock_private_request.ts b/src/data/dto/chat/unlock_private_request.ts new file mode 100644 index 00000000..45ff81c6 --- /dev/null +++ b/src/data/dto/chat/unlock_private_request.ts @@ -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); + } +} diff --git a/src/data/dto/chat/unlock_private_response.ts b/src/data/dto/chat/unlock_private_response.ts new file mode 100644 index 00000000..fb4eeed7 --- /dev/null +++ b/src/data/dto/chat/unlock_private_response.ts @@ -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); + } +} diff --git a/src/data/dto/payment/create_payment_order_request.ts b/src/data/dto/payment/create_payment_order_request.ts new file mode 100644 index 00000000..064241c9 --- /dev/null +++ b/src/data/dto/payment/create_payment_order_request.ts @@ -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); + } +} diff --git a/src/data/dto/payment/create_payment_order_response.ts b/src/data/dto/payment/create_payment_order_response.ts new file mode 100644 index 00000000..15e57b24 --- /dev/null +++ b/src/data/dto/payment/create_payment_order_response.ts @@ -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; + + 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); + } +} diff --git a/src/data/dto/payment/index.ts b/src/data/dto/payment/index.ts new file mode 100644 index 00000000..058dbf94 --- /dev/null +++ b/src/data/dto/payment/index.ts @@ -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"; diff --git a/src/data/dto/payment/payment_order_status_response.ts b/src/data/dto/payment/payment_order_status_response.ts new file mode 100644 index 00000000..c9d95a01 --- /dev/null +++ b/src/data/dto/payment/payment_order_status_response.ts @@ -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); + } +} diff --git a/src/data/dto/payment/payment_plan.ts b/src/data/dto/payment/payment_plan.ts new file mode 100644 index 00000000..d5f933c1 --- /dev/null +++ b/src/data/dto/payment/payment_plan.ts @@ -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); + } +} diff --git a/src/data/dto/payment/payment_plans_response.ts b/src/data/dto/payment/payment_plans_response.ts new file mode 100644 index 00000000..03390e1d --- /dev/null +++ b/src/data/dto/payment/payment_plans_response.ts @@ -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()), + }; + } +} diff --git a/src/data/dto/payment/payment_vip_status_response.ts b/src/data/dto/payment/payment_vip_status_response.ts new file mode 100644 index 00000000..b90bd917 --- /dev/null +++ b/src/data/dto/payment/payment_vip_status_response.ts @@ -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); + } +} diff --git a/src/data/dto/payment/payment_websocket_event.ts b/src/data/dto/payment/payment_websocket_event.ts new file mode 100644 index 00000000..cf963b99 --- /dev/null +++ b/src/data/dto/payment/payment_websocket_event.ts @@ -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 }; diff --git a/src/data/repositories/chat_repository.ts b/src/data/repositories/chat_repository.ts index 7d5224b4..a7146450 100644 --- a/src/data/repositories/chat_repository.ts +++ b/src/data/repositories/chat_repository.ts @@ -21,6 +21,8 @@ import { ChatMessage, ChatSendResponse, SendMessageRequest, + UnlockPrivateRequest, + UnlockPrivateResponse, } from "@/data/dto/chat"; import { Result } from "@/utils"; import type { IChatRepository } from "@/data/repositories/interfaces"; @@ -57,6 +59,15 @@ export class ChatRepository implements IChatRepository { return Result.wrap(() => this.api.getHistory(limit, offset)); } + /** 解锁私密消息。 */ + async unlockPrivateMessage( + messageId: string, + ): Promise> { + return Result.wrap(() => + this.api.unlockPrivateMessage(UnlockPrivateRequest.from({ messageId })), + ); + } + // ============ 本地(Dexie)操作 ============ /** 把一条消息写入本地存储。 */ diff --git a/src/data/repositories/index.ts b/src/data/repositories/index.ts index a45833f1..84e43c4c 100644 --- a/src/data/repositories/index.ts +++ b/src/data/repositories/index.ts @@ -5,8 +5,10 @@ export * from "./auth_repository"; export * from "./chat_repository"; export * from "./metrics_repository"; +export * from "./payment_repository"; export * from "./user_repository"; export * from "./interfaces/iauth_repository"; export * from "./interfaces/ichat_repository"; export * from "./interfaces/imetrics_repository"; +export * from "./interfaces/ipayment_repository"; export * from "./interfaces/iuser_repository"; diff --git a/src/data/repositories/interfaces/ichat_repository.ts b/src/data/repositories/interfaces/ichat_repository.ts index 2074ea88..92b5dd43 100644 --- a/src/data/repositories/interfaces/ichat_repository.ts +++ b/src/data/repositories/interfaces/ichat_repository.ts @@ -16,6 +16,7 @@ import type { ChatHistoryResponse, ChatMessage, ChatSendResponse, + UnlockPrivateResponse, } from "@/data/dto/chat"; export interface IChatRepository { @@ -28,6 +29,9 @@ export interface IChatRepository { /** 获取聊天历史,分页参数默认 limit=50, offset=0。 */ getHistory(limit?: number, offset?: number): Promise>; + /** 解锁私密消息。 */ + unlockPrivateMessage(messageId: string): Promise>; + /** 把一条消息写入本地存储。 */ saveMessageToLocal(message: ChatMessage): Promise>; diff --git a/src/data/repositories/interfaces/index.ts b/src/data/repositories/interfaces/index.ts index 3d0eb5b3..4fb1821a 100644 --- a/src/data/repositories/interfaces/index.ts +++ b/src/data/repositories/interfaces/index.ts @@ -5,4 +5,5 @@ export * from "./iauth_repository"; export * from "./ichat_repository"; export * from "./imetrics_repository"; +export * from "./ipayment_repository"; export * from "./iuser_repository"; diff --git a/src/data/repositories/interfaces/ipayment_repository.ts b/src/data/repositories/interfaces/ipayment_repository.ts new file mode 100644 index 00000000..9fd43fa5 --- /dev/null +++ b/src/data/repositories/interfaces/ipayment_repository.ts @@ -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>; + + /** 创建支付订单。 */ + createOrder( + planId: string, + payChannel: PayChannel, + autoRenew: boolean, + ): Promise>; + + /** 查询支付订单状态。 */ + getOrderStatus(orderId: string): Promise>; + + /** 查询当前用户 VIP 状态。 */ + getVipStatus(): Promise>; +} diff --git a/src/data/repositories/payment_repository.ts b/src/data/repositories/payment_repository.ts new file mode 100644 index 00000000..62d94bcf --- /dev/null +++ b/src/data/repositories/payment_repository.ts @@ -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> { + return Result.wrap(() => this.api.getPlans()); + } + + /** 创建支付订单。 */ + async createOrder( + planId: string, + payChannel: PayChannel, + autoRenew: boolean, + ): Promise> { + const request = CreatePaymentOrderRequest.from({ + planId, + payChannel, + autoRenew, + }); + return Result.wrap(() => this.api.createOrder(request)); + } + + /** 查询支付订单状态。 */ + async getOrderStatus( + orderId: string, + ): Promise> { + return Result.wrap(() => this.api.getOrderStatus(orderId)); + } + + /** 查询当前用户 VIP 状态。 */ + async getVipStatus(): Promise> { + return Result.wrap(() => this.api.getVipStatus()); + } +} + +/** 全局单例。 */ +export const paymentRepository = new PaymentRepository(paymentApi); diff --git a/src/data/schemas/chat/chat_history_response.ts b/src/data/schemas/chat/chat_history_response.ts index 824405be..8a5757c0 100644 --- a/src/data/schemas/chat/chat_history_response.ts +++ b/src/data/schemas/chat/chat_history_response.ts @@ -10,6 +10,10 @@ export const ChatHistoryResponseSchema = z.object({ total: z.number().default(0), limit: z.number(), offset: z.number(), + isVip: z.boolean(), + privateFreeLimit: z.number(), + privateUsedToday: z.number(), + privateCanViewFree: z.boolean(), }); export type ChatHistoryResponseInput = z.input; diff --git a/src/data/schemas/chat/chat_message.ts b/src/data/schemas/chat/chat_message.ts index 3e4ab1b6..24d64353 100644 --- a/src/data/schemas/chat/chat_message.ts +++ b/src/data/schemas/chat/chat_message.ts @@ -4,12 +4,21 @@ */ import { z } from "zod"; -export const ChatMessageSchema = z.object({ - role: z.string(), - content: z.string(), - id: z.string().default(""), - createdAt: z.string().default(""), -}); +export const ChatMessageSchema = z + .object({ + role: z.string(), + content: z.string(), + id: 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; export type ChatMessageData = z.output; diff --git a/src/data/schemas/chat/chat_send_response.ts b/src/data/schemas/chat/chat_send_response.ts index 35ca13d9..6d7167ff 100644 --- a/src/data/schemas/chat/chat_send_response.ts +++ b/src/data/schemas/chat/chat_send_response.ts @@ -4,6 +4,12 @@ */ import { z } from "zod"; +export const ChatBlockDetailSchema = z.object({ + type: z.string(), + usedToday: z.number(), + limit: z.number(), +}); + export const ChatSendResponseSchema = z.object({ mode: z.string().default(""), reply: z.string(), @@ -17,7 +23,15 @@ export const ChatSendResponseSchema = z.object({ isGuest: z.boolean().default(false), // 函数式 default —— 每次 parse 时重新调 Date.now()(后端不返回 timestamp 时用当下时间) 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; export type ChatSendResponseData = z.output; +export type ChatBlockDetailData = z.output; diff --git a/src/data/schemas/chat/index.ts b/src/data/schemas/chat/index.ts index b11282a1..90049d31 100644 --- a/src/data/schemas/chat/index.ts +++ b/src/data/schemas/chat/index.ts @@ -12,3 +12,5 @@ export * from "./image_upload_response"; export * from "./send_message_request"; export * from "./stt_data"; export * from "./sync_message"; +export * from "./unlock_private_request"; +export * from "./unlock_private_response"; diff --git a/src/data/schemas/chat/unlock_private_request.ts b/src/data/schemas/chat/unlock_private_request.ts new file mode 100644 index 00000000..cd7d1de2 --- /dev/null +++ b/src/data/schemas/chat/unlock_private_request.ts @@ -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 +>; diff --git a/src/data/schemas/chat/unlock_private_response.ts b/src/data/schemas/chat/unlock_private_response.ts new file mode 100644 index 00000000..378d7936 --- /dev/null +++ b/src/data/schemas/chat/unlock_private_response.ts @@ -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; +export type UnlockPrivateResponseInput = z.input< + typeof UnlockPrivateResponseSchema +>; +export type UnlockPrivateResponseData = z.output< + typeof UnlockPrivateResponseSchema +>; diff --git a/src/data/schemas/payment/create_payment_order_request.ts b/src/data/schemas/payment/create_payment_order_request.ts new file mode 100644 index 00000000..396d1e0c --- /dev/null +++ b/src/data/schemas/payment/create_payment_order_request.ts @@ -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; +export type CreatePaymentOrderRequestInput = z.input< + typeof CreatePaymentOrderRequestSchema +>; +export type CreatePaymentOrderRequestData = z.output< + typeof CreatePaymentOrderRequestSchema +>; diff --git a/src/data/schemas/payment/create_payment_order_response.ts b/src/data/schemas/payment/create_payment_order_response.ts new file mode 100644 index 00000000..d134c632 --- /dev/null +++ b/src/data/schemas/payment/create_payment_order_response.ts @@ -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 +>; diff --git a/src/data/schemas/payment/index.ts b/src/data/schemas/payment/index.ts new file mode 100644 index 00000000..119c6cf2 --- /dev/null +++ b/src/data/schemas/payment/index.ts @@ -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"; diff --git a/src/data/schemas/payment/payment_order_status_response.ts b/src/data/schemas/payment/payment_order_status_response.ts new file mode 100644 index 00000000..2cf4fbfc --- /dev/null +++ b/src/data/schemas/payment/payment_order_status_response.ts @@ -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; +export type PaymentOrderStatusResponseInput = z.input< + typeof PaymentOrderStatusResponseSchema +>; +export type PaymentOrderStatusResponseData = z.output< + typeof PaymentOrderStatusResponseSchema +>; diff --git a/src/data/schemas/payment/payment_plan.ts b/src/data/schemas/payment/payment_plan.ts new file mode 100644 index 00000000..249e51f3 --- /dev/null +++ b/src/data/schemas/payment/payment_plan.ts @@ -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; + 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; +export type PaymentPlanData = z.output; diff --git a/src/data/schemas/payment/payment_plans_response.ts b/src/data/schemas/payment/payment_plans_response.ts new file mode 100644 index 00000000..b39c4248 --- /dev/null +++ b/src/data/schemas/payment/payment_plans_response.ts @@ -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 +>; diff --git a/src/data/schemas/payment/payment_vip_status_response.ts b/src/data/schemas/payment/payment_vip_status_response.ts new file mode 100644 index 00000000..65a31fc4 --- /dev/null +++ b/src/data/schemas/payment/payment_vip_status_response.ts @@ -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 +>; diff --git a/src/data/schemas/payment/payment_websocket_event.ts b/src/data/schemas/payment/payment_websocket_event.ts new file mode 100644 index 00000000..4d6b872d --- /dev/null +++ b/src/data/schemas/payment/payment_websocket_event.ts @@ -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; +export type PaymentSuccessEventData = z.output; +export type PaymentWebSocketEventInput = z.input< + typeof PaymentWebSocketEventSchema +>; +export type PaymentWebSocketEventData = z.output< + typeof PaymentWebSocketEventSchema +>; diff --git a/src/data/services/api/api_path.ts b/src/data/services/api/api_path.ts index 6a779246..c424e3b1 100644 --- a/src/data/services/api/api_path.ts +++ b/src/data/services/api/api_path.ts @@ -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`; @@ -103,6 +109,9 @@ export class ApiPath { /** 上传图片 */ static readonly chatUploadImage = `${ApiPath._chat}/upload-image`; + /** 解锁私密消息 */ + static readonly chatUnlockPrivate = `${ApiPath._chat}/unlock-private`; + // ============ 数据看板相关 ============ private static readonly _metrics = `${ApiPath._baseUrl}/metrics`; diff --git a/src/data/services/api/chat_api.ts b/src/data/services/api/chat_api.ts index ae780c88..11b720f8 100644 --- a/src/data/services/api/chat_api.ts +++ b/src/data/services/api/chat_api.ts @@ -16,6 +16,8 @@ import { SendMessageRequest, SttData, SyncMessage, + UnlockPrivateRequest, + UnlockPrivateResponse, } from "@/data/dto/chat"; export class ChatApi { @@ -81,6 +83,24 @@ export class ChatApi { unwrap(env) as Record ); } + + /** + * 解锁私密消息 + */ + async unlockPrivateMessage( + body: UnlockPrivateRequest, + ): Promise { + const env = await httpClient>( + ApiPath.chatUnlockPrivate, + { + method: "POST", + body: body.toJson(), + }, + ); + return UnlockPrivateResponse.fromJson( + unwrap(env) as Record, + ); + } } /** diff --git a/src/data/services/api/index.ts b/src/data/services/api/index.ts index 3768108e..d2bad177 100644 --- a/src/data/services/api/index.ts +++ b/src/data/services/api/index.ts @@ -8,5 +8,6 @@ export * from "./auth_api"; export * from "./chat_api"; export * from "./http_client"; export * from "./metrics_api"; +export * from "./payment_api"; export * from "./response_helper"; export * from "./user_api"; diff --git a/src/data/services/api/payment_api.ts b/src/data/services/api/payment_api.ts new file mode 100644 index 00000000..41153cc7 --- /dev/null +++ b/src/data/services/api/payment_api.ts @@ -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 { + const env = await httpClient>(ApiPath.paymentPlans); + return PaymentPlansResponse.fromJson( + unwrap(env) as Record, + ); + } + + /** + * 创建支付订单 + */ + async createOrder( + body: CreatePaymentOrderRequest, + ): Promise { + const env = await httpClient>( + ApiPath.paymentCreateOrder, + { + method: "POST", + body: body.toJson(), + }, + ); + return CreatePaymentOrderResponse.fromJson( + unwrap(env) as Record, + ); + } + + /** + * 查询支付订单状态 + */ + async getOrderStatus( + orderId: string, + ): Promise { + const env = await httpClient>( + ApiPath.paymentOrderStatus, + { + query: { order_id: orderId }, + }, + ); + return PaymentOrderStatusResponse.fromJson( + unwrap(env) as Record, + ); + } + + /** + * 查询当前用户 VIP 状态 + */ + async getVipStatus(): Promise { + const env = await httpClient>( + ApiPath.paymentVipStatus, + ); + return PaymentVipStatusResponse.fromJson( + unwrap(env) as Record, + ); + } +} + +/** + * 全局单例 + */ +export const paymentApi = new PaymentApi();