Files
form-center-front/src/api/module/anon.ts
T
chenxing d1177a741c t18:前端代录与导入——管理员视图两个新动作接进同一条 submit 管线
- Fill.vue 加代录态:?proxy=1 且该表单在 managed 清单里才画「被记录人」选择器(复用 UserCheckModal +
  GetSchoolAll 本校组织树),提交体带 subjectId。三条口径写在注释里:proxy 只是界面开关、判权在后端
  requireSubject(非 ALL 档带字段即 403);subjectId 是被写入数据的字段而不是操作者身份(V17 不变);
  代录态不取也不显示"我的提交"段(那是本人视角的历史,混进来会让人以为已经给这个人记过)。
- AppContext 管理员视图每行加「添加(代录)」与「导入」;导入弹窗组件 components/admin/ImportPanel.vue:
  下载模板 → 选文件 → 逐行结果表(行号/结果/答卷ID/原因)+ 成功失败计数。整批拒(不是这张表的模板、
  label 重复、超行数上限)与坏行原因一律按后端 msg 原样亮出,前端不复制规则(同 AdminListPanel 口径)。
- api/module/data.ts 加 import(multipart,formUuid 走 query)与 importTemplateBlob(原生 fetch:
  文件流不能过 alova 的 response.json() 拦截器,理由同导出那条)。types/data.ts 加 ImportResult/ImportRowResult。
- api/module/anon.ts 加开发期会话开关:仅 conf.isMock(build 产物恒 false)时,地址带 mockSession=0
  即判"无会话"——否则 mock 的 GetUserInfo 永远返回 code=0,匿名通道在 mock 面演不出来(V67 前半句要实拍)。

门禁:npx vue-tsc --noEmit 0 错(s7/tsc-2.log);npm run build ✓ built in 20.99s(s7/build-1.log)。
2026-10-20 17:40:30 +08:00

90 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 匿名通道(契约 v2 §3.2/C6/C15,T15)。
*
* <p><b>两把探针必须走原生 fetch,不走 alova</b>:alova 实例的 302 拦截对全部 `/api/*` 生效且
* 没有 per-request 豁免位(C15 的代码事实,alova-core.ts:58-65),而引导期要问的问题恰恰是
* "这个人有没有会话"——用 alova 去问,无会话者当场被整页带去 CAS,双探针就没了意义。
* 探针只回数据,不跳转。</p>
*
* <p>提交回落 alova:`/anon` 面不存在 302 语义(无会话墙),业务错误提示链与登录面同一条;
* 上传端点由本模块给出 URL 串(附件控件按注入的 URL 走,见 Fill.vue 的 provide)。</p>
*/
import conf from "@/assets/config/host.config";
import Utils from "@/utils";
import { alovaInstance } from "@/utils/alova/alova-iview";
import type { ResolvedForm, SubmitRequest, SubmitView } from "@/types/answer";
/** 平台统一响应信封(探针只看 code,不套 alova 的错误处理) */
interface Envelope<T = any> {
code: number;
msg?: string;
data?: T;
}
/** 会话探针的结果:有没有会话 + 有会话时顺手把用户带回来(省一次重复请求) */
export interface SessionProbe {
logged: boolean;
user: any | null;
}
/**
* 探针请求。带 `yschapirequest: 1` 是给 CAS 过滤器认"这是 AJAX"的记号——不带它,墙内未登录会回
* HTML 跳转而不是 JSON 的 code=302 信封,fetch 会跟着跳到 CAS 域名然后撞 CORS,探针就变成了一次
* 无声的失败。`yschreferer` 同 alova 实例的头,保持两条通道对后端看起来是同一个前端。
*/
async function probe<T>(path: string): Promise<Envelope<T>> {
try {
const res = await fetch(`${conf.serviceHost}${path}`, {
credentials: "include",
headers: {
yschapirequest: "1",
yschreferer: Utils.strToHexCharCode(window.location.href)
}
});
if (!res.ok) return { code: -1, msg: `HTTP ${res.status}` };
return (await res.json()) as Envelope<T>;
} catch (e: any) {
return { code: -1, msg: e?.message || "探针请求失败" };
}
}
/**
* 探针 1(C15 第一跳):当前浏览器有没有 CAS 会话。非 0(含 302/网络错)一律按"无会话"回答。
*
* mock 面的开发期开关(`conf.isMock` 在 build 产物里恒 false,生产链路一个字都不走这支):
* 地址带 `mockSession=0` 即视为无会话——否则 mock 的 GetUserInfo 永远返回 code=0,
* 匿名通道在 mock 面就演不出来(V67 要的"匿名表单 page=直接可填"需要能在本机实拍)。
*/
export async function probeSession(): Promise<SessionProbe> {
if (conf.isMock && window.location.href.includes("mockSession=0")) {
return { logged: false, user: null };
}
const envelope = await probe("/api/base/GetUserInfo");
return { logged: envelope.code === 0, user: envelope.code === 0 ? envelope.data ?? null : null };
}
/**
* 探针 2(C15 第二跳):这张表是不是匿名表单。
* 后端对"非匿名表单"和"压根不存在"给同一条 PARAM 表单不存在(C6/V54 防探测),
* 所以这里只需要"是不是 0"一个判据,msg 不进 UI。
*/
export async function probeAnonForm(code: string, appCode?: string): Promise<ResolvedForm | null> {
const query = appCode ? `&appCode=${encodeURIComponent(appCode)}` : "";
const envelope = await probe<ResolvedForm>(`/anon/form/resolve?code=${encodeURIComponent(code)}${query}`);
return envelope.code === 0 && envelope.data ? envelope.data : null;
}
/** 匿名提交(响应形状与登录面同一条:{answerId, answerNo, replayed},C7 幂等命中 200+原 answerId) */
export function submitAnonAnswer(body: SubmitRequest) {
return alovaInstance.Post<SubmitView>("/anon/form/submit", body).send();
}
/**
* 匿名上传的端点寻址(`code` 必带:后端只有表单能定 sch_id,落错校提交时的附件归属就查不到这行)。
* 返回的是给附件控件用的完整请求路径,appCode 以 query 参数交出去。
*/
export function anonUploadPath(code: string, appCode?: string): string {
const query = appCode ? `&appCode=${encodeURIComponent(appCode)}` : "";
return `/anon/form/upload?code=${encodeURIComponent(code)}${query}`;
}