Files
Jyotisha/frontend/src/lib/user-feedback.ts
T

71 lines
3.2 KiB
TypeScript

import { z } from "zod";
/**
* 反馈与投诉 (compliance round 2026-09-30). Shared by the account-menu dialog,
* POST /api/feedback and the admin list. The table holds the user's words, an
* optional contact and at most a session id — never conversation text.
*/
export const FEEDBACK_TYPES = ["problem", "content_report", "refund", "other"] as const;
export type FeedbackType = (typeof FEEDBACK_TYPES)[number];
export const FEEDBACK_TYPE_LABELS: Readonly<Record<FeedbackType, string>> = {
problem: "问题反馈",
content_report: "内容举报",
refund: "退款与扣点",
other: "其他",
};
export const FEEDBACK_STATUSES = ["new", "in_progress", "resolved", "rejected"] as const;
export type FeedbackStatus = (typeof FEEDBACK_STATUSES)[number];
export const FEEDBACK_STATUS_LABELS: Readonly<Record<FeedbackStatus, string>> = {
new: "待处理",
in_progress: "处理中",
resolved: "已解决",
rejected: "不予处理",
};
export const FEEDBACK_BODY_MIN = 10;
export const FEEDBACK_BODY_MAX = 2000;
export const FEEDBACK_CONTACT_MAX = 200;
/** Promised reply time on the success screen, in working days. */
export const FEEDBACK_RESPONSE_WORKING_DAYS = 3;
/** Mirrors submit_user_feedback(): five submissions per rolling hour. */
export const FEEDBACK_HOURLY_LIMIT = 5;
export const feedbackSubmissionSchema = z.object({
type: z.enum(FEEDBACK_TYPES),
body: z.string().trim().min(FEEDBACK_BODY_MIN).max(FEEDBACK_BODY_MAX),
contact: z.string().trim().max(FEEDBACK_CONTACT_MAX).optional().transform((value) => value || undefined),
sessionId: z.string().uuid().optional(),
});
export type FeedbackSubmission = z.infer<typeof feedbackSubmissionSchema>;
/** Client-side check with the same limits the server enforces; null when valid. */
export function feedbackBodyProblem(body: string): string | null {
const length = body.trim().length;
if (length < FEEDBACK_BODY_MIN) return `请至少写 ${FEEDBACK_BODY_MIN} 个字,说清楚发生了什么。`;
if (length > FEEDBACK_BODY_MAX) return `最多 ${FEEDBACK_BODY_MAX} 个字。`;
return null;
}
export const FEEDBACK_RATE_LIMITED_COPY = "提交得有点频繁,请一小时后再试;急事可以直接发邮件给我们。";
/** Maps the database's error text to an HTTP answer; unknown errors are 503. */
export function feedbackRpcErrorStatus(message: string | undefined): { status: number; error: string } {
if (message?.includes("feedback_rate_limited")) return { status: 429, error: FEEDBACK_RATE_LIMITED_COPY };
if (message?.includes("feedback_session_not_owned")) return { status: 400, error: "附上的对话不属于当前账号,请去掉勾选后再提交。" };
return { status: 503, error: "暂时没能提交,请稍后再试。" };
}
/** Admin list filter: a status, or `type:<feedback type>`. Unknown values filter nothing. */
export function parseFeedbackFilter(value: string | undefined): { type: string | null; status: string | null } {
if (!value) return { type: null, status: null };
if (value.startsWith("type:")) {
const type = value.slice(5);
return { type: (FEEDBACK_TYPES as readonly string[]).includes(type) ? type : null, status: null };
}
return { type: null, status: (FEEDBACK_STATUSES as readonly string[]).includes(value) ? value : null };
}