feat(data): add paywall payment APIs
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
@@ -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<Result<UnlockPrivateResponse>> {
|
||||
return Result.wrap(() =>
|
||||
this.api.unlockPrivateMessage(UnlockPrivateRequest.from({ messageId })),
|
||||
);
|
||||
}
|
||||
|
||||
// ============ 本地(Dexie)操作 ============
|
||||
|
||||
/** 把一条消息写入本地存储。 */
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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<Result<ChatHistoryResponse>>;
|
||||
|
||||
/** 解锁私密消息。 */
|
||||
unlockPrivateMessage(messageId: string): Promise<Result<UnlockPrivateResponse>>;
|
||||
|
||||
/** 把一条消息写入本地存储。 */
|
||||
saveMessageToLocal(message: ChatMessage): Promise<Result<void>>;
|
||||
|
||||
|
||||
@@ -5,4 +5,5 @@
|
||||
export * from "./iauth_repository";
|
||||
export * from "./ichat_repository";
|
||||
export * from "./imetrics_repository";
|
||||
export * from "./ipayment_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),
|
||||
limit: z.number(),
|
||||
offset: z.number(),
|
||||
isVip: z.boolean(),
|
||||
privateFreeLimit: z.number(),
|
||||
privateUsedToday: z.number(),
|
||||
privateCanViewFree: z.boolean(),
|
||||
});
|
||||
|
||||
export type ChatHistoryResponseInput = z.input<typeof ChatHistoryResponseSchema>;
|
||||
|
||||
@@ -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<typeof ChatMessageSchema>;
|
||||
export type ChatMessageData = z.output<typeof ChatMessageSchema>;
|
||||
|
||||
@@ -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<typeof ChatSendResponseSchema>;
|
||||
export type ChatSendResponseData = z.output<typeof ChatSendResponseSchema>;
|
||||
export type ChatBlockDetailData = z.output<typeof ChatBlockDetailSchema>;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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
|
||||
>;
|
||||
@@ -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
|
||||
>;
|
||||
@@ -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
|
||||
>;
|
||||
@@ -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`;
|
||||
|
||||
|
||||
@@ -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<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>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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();
|
||||
Reference in New Issue
Block a user