refactor(chat): rely on server message limits
This commit is contained in:
@@ -1,82 +0,0 @@
|
||||
/**
|
||||
* 游客每日聊天配额数据 DTO
|
||||
*/
|
||||
import {
|
||||
GuestChatQuotaSchema,
|
||||
type GuestChatQuotaInput,
|
||||
type GuestChatQuotaData,
|
||||
} from "@/data/schemas/chat/guest_chat_quota";
|
||||
import { AppEnvUtil } from "@/utils";
|
||||
|
||||
export class GuestChatQuota {
|
||||
// 静态常量
|
||||
static readonly maxQuotaPerDay = 30;
|
||||
static readonly maxQuotaPerDayTest = 4;
|
||||
|
||||
static readonly defaultTotalQuota = 50;
|
||||
static readonly defaultTotalQuotaTest = 5;
|
||||
static readonly quotaExhausted = 0;
|
||||
|
||||
declare readonly remaining: number;
|
||||
declare readonly date: string;
|
||||
|
||||
private constructor(input: GuestChatQuotaInput) {
|
||||
const data = GuestChatQuotaSchema.parse(input);
|
||||
Object.assign(this, data);
|
||||
Object.freeze(this);
|
||||
}
|
||||
|
||||
static from(input: GuestChatQuotaInput): GuestChatQuota {
|
||||
return new GuestChatQuota(input);
|
||||
}
|
||||
|
||||
static fromJson(json: unknown): GuestChatQuota {
|
||||
return GuestChatQuota.from(json as GuestChatQuotaInput);
|
||||
}
|
||||
|
||||
/**
|
||||
* 配额耗尽状态值
|
||||
*/
|
||||
static get exhausted(): number {
|
||||
return GuestChatQuota.quotaExhausted;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前环境的每日最大配额
|
||||
*/
|
||||
static get threshold(): number {
|
||||
return AppEnvUtil.isProduction()
|
||||
? GuestChatQuota.maxQuotaPerDay
|
||||
: GuestChatQuota.maxQuotaPerDayTest;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前环境的总配额默认值
|
||||
*/
|
||||
static get totalQuotaDefault(): number {
|
||||
return AppEnvUtil.isProduction()
|
||||
? GuestChatQuota.defaultTotalQuota
|
||||
: GuestChatQuota.defaultTotalQuotaTest;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建初始配额实例
|
||||
*/
|
||||
static initial(todayString: string): GuestChatQuota {
|
||||
return new GuestChatQuota({
|
||||
remaining: GuestChatQuota.threshold,
|
||||
date: todayString,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否需要重置(跨天)
|
||||
*/
|
||||
needsReset(todayString: string): boolean {
|
||||
return this.date !== todayString;
|
||||
}
|
||||
|
||||
toJson(): GuestChatQuotaData {
|
||||
return GuestChatQuotaSchema.parse(this);
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@ export * from "./chat_message";
|
||||
export * from "./chat_send_response";
|
||||
export * from "./chat_sync_data";
|
||||
export * from "./chat_sync_request";
|
||||
export * from "./guest_chat_quota";
|
||||
export * from "./image_upload_response";
|
||||
export * from "./send_message_request";
|
||||
export * from "./stt_data";
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
/**
|
||||
* 游客每日聊天配额数据
|
||||
* 原始 Dart: GuestChatQuota (lib/data/models/chat/guest_chat_quota.dart)
|
||||
*/
|
||||
import { z } from "zod";
|
||||
|
||||
export const GuestChatQuotaSchema = z.object({
|
||||
remaining: z.number(),
|
||||
date: z.string(),
|
||||
});
|
||||
|
||||
export type GuestChatQuotaInput = z.input<typeof GuestChatQuotaSchema>;
|
||||
export type GuestChatQuotaData = z.output<typeof GuestChatQuotaSchema>;
|
||||
@@ -7,7 +7,6 @@ export * from "./chat_message";
|
||||
export * from "./chat_send_response";
|
||||
export * from "./chat_sync_data";
|
||||
export * from "./chat_sync_request";
|
||||
export * from "./guest_chat_quota";
|
||||
export * from "./image_upload_response";
|
||||
export * from "./send_message_request";
|
||||
export * from "./stt_data";
|
||||
|
||||
@@ -35,7 +35,6 @@ export * from "../dto/chat/chat_message";
|
||||
export * from "../dto/chat/chat_send_response";
|
||||
export * from "../dto/chat/chat_sync_data";
|
||||
export * from "../dto/chat/chat_sync_request";
|
||||
export * from "../dto/chat/guest_chat_quota";
|
||||
export * from "../dto/chat/image_upload_response";
|
||||
export * from "../dto/chat/send_message_request";
|
||||
export * from "../dto/chat/stt_data";
|
||||
@@ -69,7 +68,6 @@ export * from "../schemas/chat/chat_message";
|
||||
export * from "../schemas/chat/chat_send_response";
|
||||
export * from "../schemas/chat/chat_sync_data";
|
||||
export * from "../schemas/chat/chat_sync_request";
|
||||
export * from "../schemas/chat/guest_chat_quota";
|
||||
export * from "../schemas/chat/image_upload_response";
|
||||
export * from "../schemas/chat/send_message_request";
|
||||
export * from "../schemas/chat/stt_data";
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { createStorage } from "unstorage";
|
||||
import memoryDriver from "unstorage/drivers/memory";
|
||||
|
||||
import { GuestChatQuota } from "@/data/dto/chat/guest_chat_quota";
|
||||
import { StorageKeys } from "@/data/storage/storage_keys";
|
||||
import { todayString, SpAsyncUtil } from "@/utils";
|
||||
|
||||
import { ChatStorage } from "../chat_storage";
|
||||
|
||||
describe("ChatStorage guest quota", () => {
|
||||
beforeEach(() => {
|
||||
SpAsyncUtil.setStorage(createStorage({ driver: memoryDriver() }));
|
||||
ChatStorage._resetForTests();
|
||||
});
|
||||
|
||||
it("keeps an existing daily quota for the current day", async () => {
|
||||
const storage = ChatStorage.getInstance();
|
||||
await storage.setGuestDailyChatQuota(2, todayString());
|
||||
|
||||
const result = await storage.getGuestDailyChatQuota();
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.success ? result.data : null).toEqual({
|
||||
remaining: 2,
|
||||
date: todayString(),
|
||||
});
|
||||
});
|
||||
|
||||
it("resets daily quota only when the stored date is not today", async () => {
|
||||
const storage = ChatStorage.getInstance();
|
||||
await storage.setGuestDailyChatQuota(1, "2000-01-01");
|
||||
|
||||
const result = await storage.getGuestDailyChatQuota();
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.success ? result.data : null).toEqual({
|
||||
remaining: GuestChatQuota.threshold,
|
||||
date: todayString(),
|
||||
});
|
||||
});
|
||||
|
||||
it("does not overwrite invalid daily quota data with the default value", async () => {
|
||||
await SpAsyncUtil.setString(StorageKeys.guestChatQuota, "{not-json");
|
||||
|
||||
const result = await ChatStorage.getInstance().getGuestDailyChatQuota();
|
||||
const stored = await SpAsyncUtil.getString(StorageKeys.guestChatQuota);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(stored.success ? stored.data : null).toBe("{not-json");
|
||||
});
|
||||
|
||||
it("does not overwrite invalid total quota data with the default value", async () => {
|
||||
await SpAsyncUtil.setString(StorageKeys.guestTotalQuotaRemaining, "{not-json");
|
||||
|
||||
const result = await ChatStorage.getInstance().getGuestTotalQuota();
|
||||
const stored = await SpAsyncUtil.getString(StorageKeys.guestTotalQuotaRemaining);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(stored.success ? stored.data : null).toBe("{not-json");
|
||||
});
|
||||
});
|
||||
@@ -1,17 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
GuestChatQuotaSchema,
|
||||
type GuestChatQuotaData,
|
||||
} from "@/data/schemas/chat/guest_chat_quota";
|
||||
import { GuestChatQuota as GuestChatQuotaDto } from "@/data/dto/chat/guest_chat_quota";
|
||||
import { z } from "zod";
|
||||
|
||||
import { type Result as ResultT, Result, todayString, SpAsyncUtil } from "@/utils";
|
||||
import { StorageKeys } from "../storage_keys";
|
||||
import type { IChatStorage } from "./ichat_storage";
|
||||
|
||||
export class ChatStorage implements IChatStorage {
|
||||
export class ChatStorage {
|
||||
private static _instance: ChatStorage | null = null;
|
||||
|
||||
static getInstance(): ChatStorage {
|
||||
@@ -24,65 +13,6 @@ export class ChatStorage implements IChatStorage {
|
||||
ChatStorage._instance = null;
|
||||
}
|
||||
|
||||
// ---- guest daily chat quota ----
|
||||
|
||||
async getGuestDailyChatQuota(): Promise<ResultT<GuestChatQuotaData | null>> {
|
||||
const existing = await SpAsyncUtil.getJson(
|
||||
StorageKeys.guestChatQuota,
|
||||
GuestChatQuotaSchema,
|
||||
);
|
||||
if (Result.isErr(existing)) return existing;
|
||||
|
||||
const today = todayString();
|
||||
if (existing.data != null) {
|
||||
const quota = GuestChatQuotaDto.from(existing.data);
|
||||
if (!quota.needsReset(today)) return existing;
|
||||
}
|
||||
|
||||
const initResult = await this.setGuestDailyChatQuota(
|
||||
GuestChatQuotaDto.threshold,
|
||||
today,
|
||||
);
|
||||
if (Result.isErr(initResult)) return existing;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: { remaining: GuestChatQuotaDto.threshold, date: today },
|
||||
};
|
||||
}
|
||||
|
||||
setGuestDailyChatQuota(remaining: number, date: string): Promise<ResultT<void>> {
|
||||
const value: GuestChatQuotaData = { remaining, date };
|
||||
return SpAsyncUtil.setJson(StorageKeys.guestChatQuota, value, GuestChatQuotaSchema);
|
||||
}
|
||||
|
||||
clearGuestDailyChatQuota(): Promise<ResultT<void>> {
|
||||
return SpAsyncUtil.remove(StorageKeys.guestChatQuota);
|
||||
}
|
||||
|
||||
// ---- guest total quota ----
|
||||
|
||||
async getGuestTotalQuota(): Promise<ResultT<number | null>> {
|
||||
const existing = await SpAsyncUtil.getJson(
|
||||
StorageKeys.guestTotalQuotaRemaining,
|
||||
z.number(),
|
||||
);
|
||||
if (Result.isErr(existing)) return existing;
|
||||
if (existing.data != null) return existing;
|
||||
|
||||
const initResult = await this.setGuestTotalQuota(
|
||||
GuestChatQuotaDto.totalQuotaDefault,
|
||||
);
|
||||
if (Result.isErr(initResult)) return existing;
|
||||
|
||||
return { success: true, data: GuestChatQuotaDto.totalQuotaDefault };
|
||||
}
|
||||
|
||||
setGuestTotalQuota(remaining: number): Promise<ResultT<void>> {
|
||||
return SpAsyncUtil.setJson(StorageKeys.guestTotalQuotaRemaining, remaining, z.number());
|
||||
}
|
||||
|
||||
clearGuestTotalQuota(): Promise<ResultT<void>> {
|
||||
return SpAsyncUtil.remove(StorageKeys.guestTotalQuotaRemaining);
|
||||
}
|
||||
// 本地游客消息数量额度逻辑已停用:
|
||||
// 游客 / 非游客的消息数量限制统一由后端接口返回 blocked/daily_limit。
|
||||
}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* IChatStorage 接口
|
||||
*
|
||||
* 对齐 Dart 端 `IChatStorage`(lib/data/services/storage/chat/ichat_storage.dart):
|
||||
* - 游客每日聊天配额 + 游客总配额 两个独立字段的 CRUD
|
||||
*
|
||||
* 注:`GuestChatQuota` 模型见 `src/data/dto/chat/guest_chat_quota.ts`,
|
||||
* 数据形态 `{ remaining: number, date: string }`。
|
||||
* 跨天判断逻辑(`needsReset`)保留在 `GuestChatQuota` 类上。
|
||||
*/
|
||||
|
||||
import type { Result } from "@/utils";
|
||||
import type { GuestChatQuotaData } from "@/data/schemas/chat/guest_chat_quota";
|
||||
|
||||
export interface IChatStorage {
|
||||
getGuestDailyChatQuota(): Promise<Result<GuestChatQuotaData | null>>;
|
||||
setGuestDailyChatQuota(
|
||||
remaining: number,
|
||||
date: string,
|
||||
): Promise<Result<void>>;
|
||||
clearGuestDailyChatQuota(): Promise<Result<void>>;
|
||||
|
||||
getGuestTotalQuota(): Promise<Result<number | null>>;
|
||||
setGuestTotalQuota(remaining: number): Promise<Result<void>>;
|
||||
clearGuestTotalQuota(): Promise<Result<void>>;
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
*/
|
||||
|
||||
export * from "./chat_storage";
|
||||
export * from "./ichat_storage";
|
||||
export * from "./local_chat_db";
|
||||
export * from "./local_chat_storage";
|
||||
export * from "./local_message";
|
||||
|
||||
@@ -14,7 +14,6 @@ export * from "./app/app_storage";
|
||||
export * from "./auth/auth_storage";
|
||||
export * from "./auth/iauth_storage";
|
||||
export * from "./chat/chat_storage";
|
||||
export * from "./chat/ichat_storage";
|
||||
export * from "./chat/local_chat_db";
|
||||
export * from "./chat/local_chat_storage";
|
||||
export * from "./chat/local_message";
|
||||
|
||||
@@ -21,8 +21,6 @@ export const StorageKeys = {
|
||||
|
||||
// chat
|
||||
chatHistory: "chat_history",
|
||||
guestChatQuota: "guest_chat_quota",
|
||||
guestTotalQuotaRemaining: "guest_total_quota_remaining",
|
||||
|
||||
// pwa / app info
|
||||
lastPwaDialogShown: "last_pwa_dialog_shown",
|
||||
|
||||
Reference in New Issue
Block a user