- 新 api/module/anon.ts:两把探针走原生 fetch(alova 的 302 拦截对全部 /api/* 生效且没有
per-request 豁免位,用它问"有没有会话"会当场整页跳 CAS,C15 的代码事实);带 yschapirequest
头让墙内未登录回 JSON 的 code=302 信封而不是 HTML 跳转。提交回落 alova(/anon 面无 302 语义)。
- Fill.vue 引导改双探针:probeSession → 有会话走登录通道并取"我的提交"段;无会话再 probeAnonForm
→ 命中即匿名渲染(匿名人无任何查询能力,编辑/删除段整条不出现);两跳都落空才 guideToLogin。
- F1 顺修(取"改判通道"那一支,不动全局 errorHandle):旧判据 e?.code===4 是死分支——alova 抛的是
不带 code 的 Error,embed 的 needLogin 从来没发出去过;现在未登录引导只由探针结论触发。
- F2 顺修:slot desc → #desc 两处(Vue 3 下旧写法静默不进 Alert 描述插槽,报名号那行从未渲染)。
- App.vue:/f/ 前缀路由先以原生探针判会话,无会话则裸渲染且一个鉴权请求都不发(P124——否则无会话者
还没走到填报页的探针就被带去 CAS,"匿名表单 page=直接可填"根本兑现不了);探到会话时结果直接复用,
登录面不多发一次请求。其余路由引导一字未动。
- upload/image 两个附件控件的上传端点改 inject("fcUploadUrl", 默认 /api/form/upload),由 Fill.vue
按通道 provide;设计器预览等调用点行为不变。vite.config.ts 代理加 /anon → 8086。
门禁:npx vue-tsc --noEmit exit 0;npm run build ✓ built in 11.61s。
81 lines
3.8 KiB
TypeScript
81 lines
3.8 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/网络错)一律按"无会话"回答 */
|
||
export async function probeSession(): Promise<SessionProbe> {
|
||
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}`;
|
||
}
|