fix(chat): update unlock response contract
This commit is contained in:
@@ -0,0 +1,32 @@
|
|||||||
|
用途:解锁某一条历史里的付费/私密消息。
|
||||||
|
请求体:
|
||||||
|
{
|
||||||
|
"messageId": "消息ID"
|
||||||
|
}
|
||||||
|
需要带登录 token:
|
||||||
|
Authorization: Bearer <token>
|
||||||
|
Content-Type: application/json
|
||||||
|
返回示例:
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"message": "success",
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
如果积分不够,会返回 unlocked:false,并带 requiredCredits / shortfallCredits / creditBalance。
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,21 +1,23 @@
|
|||||||
/**
|
|
||||||
* 私密消息解锁响应 DTO
|
|
||||||
*/
|
|
||||||
import {
|
import {
|
||||||
UnlockPrivateResponseSchema,
|
UnlockPrivateResponseSchema,
|
||||||
type UnlockPrivateReason,
|
type UnlockPrivateReason,
|
||||||
type UnlockPrivateResponseData,
|
type UnlockPrivateResponseData,
|
||||||
type UnlockPrivateResponseInput,
|
type UnlockPrivateResponseInput,
|
||||||
} from "@/data/schemas/chat/unlock_private_response";
|
} from "@/data/schemas/chat/unlock_private_response";
|
||||||
|
import type { ChatLockDetailData } from "@/data/schemas/chat";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单条历史付费 / 私密消息解锁响应 DTO。
|
||||||
|
*/
|
||||||
export class UnlockPrivateResponse {
|
export class UnlockPrivateResponse {
|
||||||
declare readonly unlocked: boolean;
|
declare readonly unlocked: boolean;
|
||||||
declare readonly content: string | null;
|
declare readonly content: string;
|
||||||
declare readonly showUpgrade: boolean;
|
|
||||||
declare readonly paywallTriggered: boolean;
|
|
||||||
declare readonly privateFreeLimit: number;
|
|
||||||
declare readonly privateUsedToday: number;
|
|
||||||
declare readonly reason: UnlockPrivateReason;
|
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) {
|
private constructor(input: UnlockPrivateResponseInput) {
|
||||||
const data = UnlockPrivateResponseSchema.parse(input);
|
const data = UnlockPrivateResponseSchema.parse(input);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
UnlockPrivateRequest,
|
UnlockPrivateRequest,
|
||||||
UnlockPrivateResponse,
|
UnlockPrivateResponse,
|
||||||
} from "@/data/dto/chat";
|
} from "@/data/dto/chat";
|
||||||
|
import type { ChatLockDetailData } from "@/data/schemas/chat";
|
||||||
import { AuthStorage } from "@/data/storage/auth";
|
import { AuthStorage } from "@/data/storage/auth";
|
||||||
import { UserStorage } from "@/data/storage/user/user_storage";
|
import { UserStorage } from "@/data/storage/user/user_storage";
|
||||||
import { Logger, Result } from "@/utils";
|
import { Logger, Result } from "@/utils";
|
||||||
@@ -69,7 +70,7 @@ export class ChatRepository implements IChatRepository {
|
|||||||
return Result.wrap(() => this.api.getHistory(limit, offset));
|
return Result.wrap(() => this.api.getHistory(limit, offset));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 解锁私密消息。 */
|
/** 解锁单条历史付费 / 私密消息。 */
|
||||||
async unlockPrivateMessage(
|
async unlockPrivateMessage(
|
||||||
messageId: string,
|
messageId: string,
|
||||||
): Promise<Result<UnlockPrivateResponse>> {
|
): Promise<Result<UnlockPrivateResponse>> {
|
||||||
@@ -83,10 +84,11 @@ export class ChatRepository implements IChatRepository {
|
|||||||
return Result.wrap(() => this.api.unlockHistory());
|
return Result.wrap(() => this.api.unlockHistory());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 把本地缓存中的私密消息标记为已解锁。 */
|
/** 把本地缓存中的单条锁定消息标记为已解锁。 */
|
||||||
async markPrivateMessageUnlockedInLocal(
|
async markPrivateMessageUnlockedInLocal(
|
||||||
messageId: string,
|
messageId: string,
|
||||||
content: string,
|
content: string,
|
||||||
|
lockDetail?: ChatLockDetailData,
|
||||||
): Promise<Result<void>> {
|
): Promise<Result<void>> {
|
||||||
return Result.wrap(async () => {
|
return Result.wrap(async () => {
|
||||||
const localResult = await this.getLocalMessages();
|
const localResult = await this.getLocalMessages();
|
||||||
@@ -101,12 +103,14 @@ export class ChatRepository implements IChatRepository {
|
|||||||
return ChatMessage.from({
|
return ChatMessage.from({
|
||||||
...message.toJson(),
|
...message.toJson(),
|
||||||
content,
|
content,
|
||||||
lockDetail: {
|
lockDetail: lockDetail ?? {
|
||||||
...message.lockDetail,
|
...message.lockDetail,
|
||||||
locked: false,
|
locked: false,
|
||||||
showContent: true,
|
showContent: true,
|
||||||
showUpgrade: false,
|
showUpgrade: false,
|
||||||
|
reason: null,
|
||||||
hint: null,
|
hint: null,
|
||||||
|
detail: null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import type {
|
|||||||
UnlockHistoryResponse,
|
UnlockHistoryResponse,
|
||||||
UnlockPrivateResponse,
|
UnlockPrivateResponse,
|
||||||
} from "@/data/dto/chat";
|
} from "@/data/dto/chat";
|
||||||
|
import type { ChatLockDetailData } from "@/data/schemas/chat";
|
||||||
import type { LocalChatMediaRow } from "@/data/storage/chat";
|
import type { LocalChatMediaRow } from "@/data/storage/chat";
|
||||||
|
|
||||||
export interface ChatMediaLookupInput {
|
export interface ChatMediaLookupInput {
|
||||||
@@ -41,18 +42,17 @@ 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>>;
|
||||||
|
|
||||||
//TODO remove
|
/** 解锁单条历史付费 / 私密消息。 */
|
||||||
/** 解锁私密消息。 */
|
|
||||||
unlockPrivateMessage(messageId: string): Promise<Result<UnlockPrivateResponse>>;
|
unlockPrivateMessage(messageId: string): Promise<Result<UnlockPrivateResponse>>;
|
||||||
|
|
||||||
/** 一键解锁历史锁定消息。 */
|
/** 一键解锁历史锁定消息。 */
|
||||||
unlockHistory(): Promise<Result<UnlockHistoryResponse>>;
|
unlockHistory(): Promise<Result<UnlockHistoryResponse>>;
|
||||||
|
|
||||||
//TODO remove
|
/** 把本地缓存中的单条锁定消息标记为已解锁。 */
|
||||||
/** 把本地缓存中的私密消息标记为已解锁。 */
|
|
||||||
markPrivateMessageUnlockedInLocal(
|
markPrivateMessageUnlockedInLocal(
|
||||||
messageId: string,
|
messageId: string,
|
||||||
content: string,
|
content: string,
|
||||||
|
lockDetail?: ChatLockDetailData,
|
||||||
): Promise<Result<void>>;
|
): Promise<Result<void>>;
|
||||||
|
|
||||||
/** 把一条消息写入本地存储。 */
|
/** 把一条消息写入本地存储。 */
|
||||||
|
|||||||
@@ -1,23 +1,21 @@
|
|||||||
/**
|
|
||||||
* 私密消息解锁响应
|
|
||||||
*/
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
export const UnlockPrivateReasonSchema = z.enum([
|
import { ChatLockDetailSchema } from "./chat_payloads";
|
||||||
"ok",
|
|
||||||
"quota_exceeded",
|
/**
|
||||||
"not_private",
|
* 单条历史付费 / 私密消息解锁响应。
|
||||||
"not_found",
|
*/
|
||||||
]);
|
export const UnlockPrivateReasonSchema = z.string().default("");
|
||||||
|
|
||||||
export const UnlockPrivateResponseSchema = z.object({
|
export const UnlockPrivateResponseSchema = z.object({
|
||||||
unlocked: z.boolean(),
|
unlocked: z.boolean(),
|
||||||
content: z.string().nullable(),
|
content: z.string().default(""),
|
||||||
showUpgrade: z.boolean().default(false),
|
|
||||||
paywallTriggered: z.boolean().default(false),
|
|
||||||
privateFreeLimit: z.number().default(0),
|
|
||||||
privateUsedToday: z.number().default(0),
|
|
||||||
reason: UnlockPrivateReasonSchema,
|
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>;
|
export type UnlockPrivateReason = z.output<typeof UnlockPrivateReasonSchema>;
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ export class ChatApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解锁私密消息
|
* 解锁单条历史付费 / 私密消息
|
||||||
*/
|
*/
|
||||||
async unlockPrivateMessage(
|
async unlockPrivateMessage(
|
||||||
body: UnlockPrivateRequest,
|
body: UnlockPrivateRequest,
|
||||||
|
|||||||
Reference in New Issue
Block a user