- 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)。
90 lines
4.3 KiB
TypeScript
90 lines
4.3 KiB
TypeScript
/**
|
||
* 匿名通道(契约 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}`;
|
||
}
|