fix(chat): update unlock response contract

This commit is contained in:
2026-06-30 18:15:39 +08:00
parent bfc2c3cc1e
commit 5f94105bc6
7 changed files with 130 additions and 30 deletions
@@ -0,0 +1,64 @@
import { describe, expect, it } from "vitest";
import { UnlockPrivateResponse } from "@/data/dto/chat";
describe("UnlockPrivateResponse", () => {
it("parses a successful single-message unlock response", () => {
const response = UnlockPrivateResponse.from({
unlocked: true,
content: "完整消息内容",
reason: "ok",
creditBalance: 9180,
creditsCharged: 10,
requiredCredits: 10,
shortfallCredits: 0,
lockDetail: {
locked: false,
showContent: true,
showUpgrade: false,
reason: null,
hint: null,
detail: null,
},
});
expect(response.unlocked).toBe(true);
expect(response.content).toBe("完整消息内容");
expect(response.reason).toBe("ok");
expect(response.creditBalance).toBe(9180);
expect(response.creditsCharged).toBe(10);
expect(response.requiredCredits).toBe(10);
expect(response.shortfallCredits).toBe(0);
expect(response.lockDetail.locked).toBe(false);
});
it("parses an insufficient-balance unlock response", () => {
const response = UnlockPrivateResponse.from({
unlocked: false,
content: "",
reason: "insufficient_balance",
creditBalance: 5,
creditsCharged: 0,
requiredCredits: 10,
shortfallCredits: 5,
lockDetail: {
locked: true,
showContent: false,
showUpgrade: true,
reason: "insufficient_balance",
hint: "Insufficient credits.",
detail: {
messageId: "msg_001",
},
},
});
expect(response.unlocked).toBe(false);
expect(response.reason).toBe("insufficient_balance");
expect(response.creditBalance).toBe(5);
expect(response.creditsCharged).toBe(0);
expect(response.requiredCredits).toBe(10);
expect(response.shortfallCredits).toBe(5);
expect(response.lockDetail.showUpgrade).toBe(true);
});
});
+10 -8
View File
@@ -1,21 +1,23 @@
/**
* 私密消息解锁响应 DTO
*/
import {
UnlockPrivateResponseSchema,
type UnlockPrivateReason,
type UnlockPrivateResponseData,
type UnlockPrivateResponseInput,
} from "@/data/schemas/chat/unlock_private_response";
import type { ChatLockDetailData } from "@/data/schemas/chat";
/**
* 单条历史付费 / 私密消息解锁响应 DTO。
*/
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 content: string;
declare readonly reason: UnlockPrivateReason;
declare readonly creditBalance: number;
declare readonly creditsCharged: number;
declare readonly requiredCredits: number;
declare readonly shortfallCredits: number;
declare readonly lockDetail: ChatLockDetailData;
private constructor(input: UnlockPrivateResponseInput) {
const data = UnlockPrivateResponseSchema.parse(input);
+7 -3
View File
@@ -10,6 +10,7 @@ import {
UnlockPrivateRequest,
UnlockPrivateResponse,
} from "@/data/dto/chat";
import type { ChatLockDetailData } from "@/data/schemas/chat";
import { AuthStorage } from "@/data/storage/auth";
import { UserStorage } from "@/data/storage/user/user_storage";
import { Logger, Result } from "@/utils";
@@ -69,7 +70,7 @@ export class ChatRepository implements IChatRepository {
return Result.wrap(() => this.api.getHistory(limit, offset));
}
/** 解锁私密消息。 */
/** 解锁单条历史付费 / 私密消息。 */
async unlockPrivateMessage(
messageId: string,
): Promise<Result<UnlockPrivateResponse>> {
@@ -83,10 +84,11 @@ export class ChatRepository implements IChatRepository {
return Result.wrap(() => this.api.unlockHistory());
}
/** 把本地缓存中的私密消息标记为已解锁。 */
/** 把本地缓存中的单条锁定消息标记为已解锁。 */
async markPrivateMessageUnlockedInLocal(
messageId: string,
content: string,
lockDetail?: ChatLockDetailData,
): Promise<Result<void>> {
return Result.wrap(async () => {
const localResult = await this.getLocalMessages();
@@ -101,12 +103,14 @@ export class ChatRepository implements IChatRepository {
return ChatMessage.from({
...message.toJson(),
content,
lockDetail: {
lockDetail: lockDetail ?? {
...message.lockDetail,
locked: false,
showContent: true,
showUpgrade: false,
reason: null,
hint: null,
detail: null,
},
});
});
@@ -20,6 +20,7 @@ import type {
UnlockHistoryResponse,
UnlockPrivateResponse,
} from "@/data/dto/chat";
import type { ChatLockDetailData } from "@/data/schemas/chat";
import type { LocalChatMediaRow } from "@/data/storage/chat";
export interface ChatMediaLookupInput {
@@ -41,18 +42,17 @@ export interface IChatRepository {
/** 获取聊天历史,分页参数默认 limit=50, offset=0。 */
getHistory(limit?: number, offset?: number): Promise<Result<ChatHistoryResponse>>;
//TODO remove
/** 解锁私密消息。 */
/** 解锁单条历史付费 / 私密消息。 */
unlockPrivateMessage(messageId: string): Promise<Result<UnlockPrivateResponse>>;
/** 一键解锁历史锁定消息。 */
unlockHistory(): Promise<Result<UnlockHistoryResponse>>;
//TODO remove
/** 把本地缓存中的私密消息标记为已解锁。 */
/** 把本地缓存中的单条锁定消息标记为已解锁。 */
markPrivateMessageUnlockedInLocal(
messageId: string,
content: string,
lockDetail?: ChatLockDetailData,
): Promise<Result<void>>;
/** 把一条消息写入本地存储。 */
@@ -1,23 +1,21 @@
/**
* 私密消息解锁响应
*/
import { z } from "zod";
export const UnlockPrivateReasonSchema = z.enum([
"ok",
"quota_exceeded",
"not_private",
"not_found",
]);
import { ChatLockDetailSchema } from "./chat_payloads";
/**
* 单条历史付费 / 私密消息解锁响应。
*/
export const UnlockPrivateReasonSchema = z.string().default("");
export const UnlockPrivateResponseSchema = z.object({
unlocked: z.boolean(),
content: z.string().nullable(),
showUpgrade: z.boolean().default(false),
paywallTriggered: z.boolean().default(false),
privateFreeLimit: z.number().default(0),
privateUsedToday: z.number().default(0),
content: z.string().default(""),
reason: UnlockPrivateReasonSchema,
creditBalance: z.number().default(0),
creditsCharged: z.number().default(0),
requiredCredits: z.number().default(0),
shortfallCredits: z.number().default(0),
lockDetail: ChatLockDetailSchema,
});
export type UnlockPrivateReason = z.output<typeof UnlockPrivateReasonSchema>;
+1 -1
View File
@@ -86,7 +86,7 @@ export class ChatApi {
}
/**
* 解锁私密消息
* 解锁单条历史付费 / 私密消息
*/
async unlockPrivateMessage(
body: UnlockPrivateRequest,