This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* 匿名通道三端点的本地 Mock(契约 v2 §3.2/C6/C7,vite-plugin-mock,仅 `npm run dev:mock` 生效)。
|
||||
*
|
||||
* 用途:本机没有 CAS 会话时,`/f/:code` 的**匿名通道**(双探针的第二跳 + 匿名 resolve/submit/upload)
|
||||
* 在 mock 面要可渲染、可提交、可实拍"窗口内二发被拒"。配合 `api/module/anon.ts` 里那条
|
||||
* "mock 模式地址带 `mockSession=0` 即视为无会话"的开发期开关使用——真环境的会话判定仍完全由
|
||||
* `probeSession()` 探 `/api/base/GetUserInfo` 决定,本文件不参与生产链路。
|
||||
*
|
||||
* 三处形状与后端逐字段对齐(前端取证只看对接形状,语义权威在 Java 侧的 AnonFillIT):
|
||||
* ① resolve 出参与登录面 `/api/form/resolve` 同构(audience 是数组、options 已内联);
|
||||
* ② 非匿名 code 一律回"表单不存在"(PARAM_ERROR=2 家族里的防探测文案,与不存在同一条);
|
||||
* ③ submit 命中防重回 CUSTOM(5) 且 msg 含剩余分钟;成功回 `{answerId, answerNo, replayed}`。
|
||||
*/
|
||||
import type { ResolvedForm } from "@/types/answer";
|
||||
|
||||
const ok = (data: unknown) => ({ code: 0, msg: "", data });
|
||||
const bizErr = (code: number, msg: string) => ({ code, msg, data: null });
|
||||
|
||||
/** 唯一一张"允许匿名"的种子表单;其余 code 都按不存在回答(防探测口径的 mock 面复演) */
|
||||
const ANON_CODE = "school_survey";
|
||||
const WINDOW_MINUTES = 10;
|
||||
|
||||
const GROUPS = [
|
||||
{
|
||||
key: "base",
|
||||
label: "基本信息",
|
||||
fields: [
|
||||
{ key: "nick", label: "称呼", type: "text", required: true, maxLength: 20 },
|
||||
{
|
||||
key: "grade", label: "年级", type: "select", required: true,
|
||||
options: [
|
||||
{ label: "高一", value: "g1" },
|
||||
{ label: "高二", value: "g2" },
|
||||
{ label: "高三", value: "g3" }
|
||||
]
|
||||
},
|
||||
{ key: "suggestion", label: "建议", type: "textarea", required: false }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
/** 防重窗口:code → 上次成功提交的毫秒时间戳(后端是 HMAC 签名的 cookie,mock 面用内存表复演) */
|
||||
const lastSubmitAt: Record<string, number> = {};
|
||||
const submitted: any[] = [];
|
||||
let seq = 1;
|
||||
|
||||
function queryValue(query: any, key: string): string {
|
||||
if (!query) return "";
|
||||
if (typeof query.get === "function") return query.get(key) || "";
|
||||
return String(query[key] ?? "");
|
||||
}
|
||||
|
||||
function resolveAnon(code: string): ResolvedForm {
|
||||
return {
|
||||
formUuid: "mock0000000000000000000000000an1",
|
||||
code,
|
||||
appCode: "form-center",
|
||||
name: "校园意见征集(匿名)",
|
||||
version: 1,
|
||||
audience: ["ANONYMOUS"],
|
||||
fillLimit: 0,
|
||||
answerNoEnabled: 1,
|
||||
deadline: null,
|
||||
deadlineReached: false,
|
||||
statusDefs: [
|
||||
{ code: "pending", label: "待处理", is_deleted: false },
|
||||
{ code: "processed", label: "已处理", is_deleted: false }
|
||||
],
|
||||
groups: GROUPS as any
|
||||
} as ResolvedForm;
|
||||
}
|
||||
|
||||
export default [
|
||||
{
|
||||
url: "/anon/form/resolve",
|
||||
method: "get",
|
||||
response: ({ query }: any) => {
|
||||
const code = queryValue(query, "code");
|
||||
if (code !== ANON_CODE) return bizErr(2, `表单不存在:form-center/${code}`);
|
||||
return ok(resolveAnon(code));
|
||||
}
|
||||
},
|
||||
{
|
||||
url: "/anon/form/submit",
|
||||
method: "post",
|
||||
response: ({ body }: any) => {
|
||||
const code = String(body?.formCode ?? "");
|
||||
if (code !== ANON_CODE) return bizErr(2, `表单不存在:form-center/${code}`);
|
||||
const elapsed = Date.now() - (lastSubmitAt[code] ?? 0);
|
||||
if (lastSubmitAt[code] && elapsed < WINDOW_MINUTES * 60_000) {
|
||||
const left = Math.max(1, Math.ceil((WINDOW_MINUTES * 60_000 - elapsed) / 60_000));
|
||||
return bizErr(5, `该表单 ${WINDOW_MINUTES} 分钟内只能提交一次,请约 ${left} 分钟后再提交:${code}`);
|
||||
}
|
||||
lastSubmitAt[code] = Date.now();
|
||||
submitted.push(body);
|
||||
const no = seq++;
|
||||
return ok({
|
||||
// 与真 answer_uuid 同长(32)——mock 面的 id 串一旦被别的调试脚本硬编码引用,长度漂移就是 P72 那类假失败
|
||||
answerId: `mockanon${String(no).padStart(4, "0")}${"0".repeat(20)}`,
|
||||
answerNo: `${code}-${new Date().toISOString().slice(0, 10).replace(/-/g, "")}-000${no}`,
|
||||
replayed: false
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
url: "/anon/form/upload",
|
||||
method: "post",
|
||||
response: ({ query }: any) => {
|
||||
const code = queryValue(query, "code");
|
||||
if (code !== ANON_CODE) return bizErr(2, `表单不存在:form-center/${code}`);
|
||||
// 与登录面同一份返回三件套(无 fileUrl,V24);id 是 mock 自增,真环境是 form_file 主键
|
||||
return ok({ id: seq, fileName: "匿名附件.jpg", key: `form/2026/10/mockanon${seq++}.jpg` });
|
||||
}
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* app 注册表(快应用)的本地 Mock(契约 v2 §5.2/C10/C17,vite-plugin-mock,仅 `npm run dev:mock` 生效)。
|
||||
*
|
||||
* 桩把后端四条规则也演一遍,让「应用设置 · 快应用」tab 在 mock 面能实拍到"前端没有把规则写丢":
|
||||
* ① C10 格式与保留字(小写 slug 2~64、`form-center` 不可注册);② code 全局唯一;
|
||||
* ③ 软删后编码不复用(`uk_app_code` 不含 is_deleted,问题单 P125 的那条口径);
|
||||
* ④ 在用禁删(名下有表单即 CUSTOM(5),msg 指点先迁先删)。
|
||||
* 零角色列(V59)在这里的形状证据就是 `AppRow` 里根本没有权限字段。
|
||||
*/
|
||||
import type { AppRow } from "@/types/data";
|
||||
|
||||
const ok = (data: unknown) => ({ code: 0, msg: "", data });
|
||||
const fail = (msg: string) => ({ code: 5, msg, data: null });
|
||||
const bad = (msg: string) => ({ code: 2, msg, data: null });
|
||||
|
||||
const CODE_RE = /^[a-z0-9]([a-z0-9-]{0,62}[a-z0-9])$/;
|
||||
const RESERVED = "form-center";
|
||||
|
||||
/** 名下还有未删表单的 app_code(后端由 form 表统计,这里用一张内存表复演) */
|
||||
const formsInUse = new Set<string>([RESERVED, "honor"]);
|
||||
|
||||
interface Row extends AppRow {
|
||||
deleted: boolean;
|
||||
}
|
||||
|
||||
const rows: Row[] = [
|
||||
{ appCode: "honor", name: "荣誉管理", schId: 3260, entryUrl: "/#/app/honor/", deleted: false },
|
||||
{ appCode: "affair-lib", name: "事项库", schId: 0, entryUrl: "/#/app/affair-lib/", deleted: false }
|
||||
];
|
||||
|
||||
function view(row: Row): AppRow {
|
||||
return { appCode: row.appCode, name: row.name, schId: row.schId, entryUrl: row.entryUrl };
|
||||
}
|
||||
|
||||
function live(): AppRow[] {
|
||||
return rows.filter((r) => !r.deleted).map(view);
|
||||
}
|
||||
|
||||
function pathParam(url: string): string {
|
||||
const tail = String(url ?? "").split("?")[0].split("/").pop() ?? "";
|
||||
try {
|
||||
return decodeURIComponent(tail);
|
||||
} catch {
|
||||
return tail;
|
||||
}
|
||||
}
|
||||
|
||||
function bodyOf(ctx: any): any {
|
||||
if (ctx?.body && Object.keys(ctx.body).length) return ctx.body;
|
||||
try {
|
||||
return JSON.parse(ctx?.bodyText || "{}");
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
export default [
|
||||
{ url: "/api/form/admin/apps", method: "get", response: () => ok(live()) },
|
||||
{
|
||||
url: "/api/form/admin/apps",
|
||||
method: "post",
|
||||
response: ({ body }: any) => {
|
||||
const req = bodyOf({ body });
|
||||
const code = String(req?.appCode ?? "").trim();
|
||||
const name = String(req?.name ?? "").trim();
|
||||
if (!code) return bad("appCode 必填");
|
||||
if (!name) return bad("应用名称 name 必填");
|
||||
if (code.toLowerCase() === RESERVED) return bad(`${RESERVED} 是中心保留的 app_code,不可注册`);
|
||||
if (!CODE_RE.test(code)) {
|
||||
return bad(`appCode 需为 2~64 位小写 slug(^[a-z0-9]([a-z0-9-]{0,62}[a-z0-9])$):${code}`);
|
||||
}
|
||||
const existing = rows.find((r) => r.appCode === code);
|
||||
if (existing) {
|
||||
return fail(existing.deleted
|
||||
? `app_code 曾被注册且已删除,编码不复用(uk_app_code 全库唯一,请换 code):${code}`
|
||||
: `app_code 已存在:${code}`);
|
||||
}
|
||||
const row: Row = { appCode: code, name, schId: 3260, entryUrl: `/#/app/${code}/`, deleted: false };
|
||||
rows.push(row);
|
||||
return ok(view(row));
|
||||
}
|
||||
},
|
||||
{
|
||||
url: "/api/form/admin/apps/:appCode",
|
||||
method: "put",
|
||||
response: ({ url, body }: any) => {
|
||||
const code = pathParam(url);
|
||||
const row = rows.find((r) => r.appCode === code && !r.deleted);
|
||||
if (!row) return bad(`app 不存在:${code}`);
|
||||
const name = String(bodyOf({ body })?.name ?? "").trim();
|
||||
if (!name) return bad("应用名称 name 必填");
|
||||
row.name = name;
|
||||
return ok(view(row));
|
||||
}
|
||||
},
|
||||
{
|
||||
url: "/api/form/admin/apps/:appCode",
|
||||
method: "delete",
|
||||
response: ({ url }: any) => {
|
||||
const code = pathParam(url);
|
||||
const row = rows.find((r) => r.appCode === code && !r.deleted);
|
||||
if (!row) return bad(`app 不存在:${code}`);
|
||||
if (formsInUse.has(code)) {
|
||||
return fail(`app_code「${code}」名下还有未删除表单,先把表单迁走或删除再删注册行`);
|
||||
}
|
||||
row.deleted = true;
|
||||
return ok(view(row));
|
||||
}
|
||||
}
|
||||
];
|
||||
Reference in New Issue
Block a user