chore: 前置快照-assetUrl前缀与学校切换收敛会话遗留(执行方案B1~B5前既有未提交变更)

This commit is contained in:
chenxing
2026-09-08 15:14:07 +08:00
parent 67a7164897
commit c77be8b5d8
6 changed files with 232 additions and 137 deletions
+101
View File
@@ -0,0 +1,101 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
/**
* 路径参数型 POST 接口的 siteId 透传回归(publish 曾因 postJSON 丢第三参,
* 请求裸发 POST /api/article/publish/{id},后端报「缺少必填参数:siteId」)。
* alova 实例与 host.config(顶层 document 依赖)均 mock,node 环境纯断言。
* host.config 用 vi.hoisted 可变对象:assetUrl 用例需要切换 serviceHost 模拟生产 /yschcms。
*/
const confState = vi.hoisted(() => ({
conf: { baseHost: "", ssoHost: "", serviceHost: "", isMock: false, isDebug: false },
}));
vi.mock("@/assets/config/host.config", () => ({
default: confState.conf,
}));
vi.mock("@/utils/alova/alova-iview", () => ({
alovaInstance: {
Get: vi.fn(),
Post: vi.fn(() => ({})),
},
}));
import { articleApi, assetUrl } from "./cms";
import { alovaInstance } from "@/utils/alova/alova-iview";
const JSON_HEADERS = { "Content-Type": "application/json;charset=UTF-8" };
describe("cms.ts 路径参数型 POST 的 query 透传", () => {
beforeEach(() => {
vi.mocked(alovaInstance.Post).mockClear();
});
it("publish: siteId 以 query 参数随 Post config 发出", () => {
articleApi.publish(5, 2);
expect(alovaInstance.Post).toHaveBeenCalledWith("/api/article/publish/2", undefined, {
headers: JSON_HEADERS,
params: { siteId: 5 },
});
});
it("offline: 同款透传", () => {
articleApi.offline(5, 3);
expect(alovaInstance.Post).toHaveBeenCalledWith("/api/article/offline/3", undefined, {
headers: JSON_HEADERS,
params: { siteId: 5 },
});
});
it("topmost: siteId 与 enable 一并透传", () => {
articleApi.topmost(5, 3, true);
expect(alovaInstance.Post).toHaveBeenCalledWith("/api/article/topmost/3", undefined, {
headers: JSON_HEADERS,
params: { siteId: 5, enable: true },
});
});
it("save/delete: siteId 走 body,不带 params(守卫勿误伤)", () => {
articleApi.delete({ id: 2, siteId: 5 });
expect(alovaInstance.Post).toHaveBeenCalledWith("/api/article/delete", { id: 2, siteId: 5 }, {
headers: JSON_HEADERS,
params: undefined,
});
});
});
describe("assetUrl 站内资产路径补 serviceHost 前缀(线上主题截图破图回归)", () => {
afterEach(() => {
confState.conf.serviceHost = "";
confState.conf.isMock = false;
});
it("生产:/api 裸路径拼上 context-path(/yschcms)", () => {
confState.conf.serviceHost = "/yschcms";
expect(assetUrl("/api/theme/default/asset/screenshot.png")).toBe(
"/yschcms/api/theme/default/asset/screenshot.png"
);
});
it("dev/mock:serviceHost 为空串时原样返回", () => {
expect(assetUrl("/api/theme/default/asset/screenshot.png")).toBe(
"/api/theme/default/asset/screenshot.png"
);
});
it("绝对地址(http/data/blob)不拼前缀", () => {
confState.conf.serviceHost = "/yschcms";
expect(assetUrl("https://cdn.example.com/x.png")).toBe("https://cdn.example.com/x.png");
expect(assetUrl("data:image/png;base64,AAAA")).toBe("data:image/png;base64,AAAA");
expect(assetUrl("blob:http://localhost/uuid")).toBe("blob:http://localhost/uuid");
});
it("空值原样返回;isMock 时不拼 serviceHost", () => {
expect(assetUrl("")).toBe("");
confState.conf.serviceHost = "/yschcms";
confState.conf.isMock = true;
expect(assetUrl("/api/theme/default/asset/screenshot.png")).toBe(
"/api/theme/default/asset/screenshot.png"
);
});
});
+16 -11
View File
@@ -41,10 +41,11 @@ function get<T>(url: string, params?: IObj): Method {
return alovaInstance.Get<T>(url, { params });
}
/** POST JSON(@RequestBody) */
function postJSON<T>(url: string, data?: unknown): Method {
/** POST JSON(@RequestBody);params 为追加到 URL 的 query 参数(alova Post config 同 Get) */
function postJSON<T>(url: string, data?: unknown, params?: IObj): Method {
return alovaInstance.Post<T>(url, data, {
headers: { "Content-Type": "application/json;charset=UTF-8" },
params,
});
}
@@ -58,6 +59,16 @@ export function portalBase(siteCode: string): string {
return `${conf.isMock ? "" : conf.serviceHost}/portal/${siteCode}`;
}
/**
* 后端下发的站内资产路径补 serviceHost 前缀(如主题截图 /api/theme/{name}/asset/screenshot.png)。
* 生产 context-path=/yschcms,裸 /api 绝对路径会打到网关根上 404(破图);
* dev/mock 下 serviceHost 为空串,拼接后与原路径一致不受影响。
*/
export function assetUrl(url: string): string {
if (!url || /^(https?:|data:|blob:)/i.test(url)) return url;
return `${conf.isMock ? "" : conf.serviceHost}${url}`;
}
class AuthApi {
/** 登录态:用户 + 可管理站点 + 是否超管 */
getUser() {
@@ -137,21 +148,15 @@ class ArticleApi {
/** 发布(publishTime 为未来时间即定时发布) */
publish(siteId: number, id: number) {
return postJSON<void>(`${ARTICLE}/publish/${id}`, undefined, {
params: { siteId },
});
return postJSON<void>(`${ARTICLE}/publish/${id}`, undefined, { siteId });
}
offline(siteId: number, id: number) {
return postJSON<void>(`${ARTICLE}/offline/${id}`, undefined, {
params: { siteId },
});
return postJSON<void>(`${ARTICLE}/offline/${id}`, undefined, { siteId });
}
topmost(siteId: number, id: number, enable: boolean) {
return postJSON<void>(`${ARTICLE}/topmost/${id}`, undefined, {
params: { siteId, enable },
});
return postJSON<void>(`${ARTICLE}/topmost/${id}`, undefined, { siteId, enable });
}
delete(data: DeleteRequest) {