docs: annotate all shared frontend modules in Chinese

This commit is contained in:
cieanyau committed 2026-11-06 00:30:15 +08:00
1 parent 2be21c1c62
commit 408f8e5c98
55 files changed
+349

No files matched your search

+59
View File
@@ -1,7 +1,13 @@
/**
* 控制台统一请求与登录会话入口。
* 页面通过 useApi 访问 /console/v1,通过 useAuth 共享登录状态;本模块只在客户端使用。
* 后端业务成功必须同时满足 HTTP 成功和响应信封 status === 200,不能仅检查 HTTP 状态。
*/
import CryptoJS from "crypto-js";
import { defineStore } from "pinia";
import { navigateTo, useRuntimeConfig } from "#app";
import { computed, ref } from "vue";
// 将供应商/模块内部错误转成面向会员的提示,避免在 UI 中暴露上游实现细节。
var ce = [
[/产品类型错误.{0,30}安全组[^\n]*/g, `该商品未接入安全组`],
[/商品未关联.{0,20}模块接口/g, `该商品暂不可用`],
@@ -26,6 +32,7 @@ var le = [
];
var ue =
/模块接口|自定义配置|模块未关联|模块标识|上游|第三方云|[a-z]{2,}[_-][a-z0-9_]+|\bDCIM\b|whmcs|reserver|https?:\/\//i;
/** 非字符串输入视为空;替换后仍包含内部标识的消息统一降级,不将原始异常直接展示。 */
function W(e) {
let t = typeof e == `string` ? e : ``;
if (!t) return ``;
@@ -41,6 +48,7 @@ function W(e) {
n.length < 2 || ue.test(n) ? `操作失败,请稍后重试或联系客服` : n
);
}
// 操作日志中的熔断事件单独翻译;其余日志保留原文,由调用方决定展示方式。
function de(e) {
let t = typeof e == `string` ? e : ``;
return /^\s*上游熔断解除[::]/u.test(t)
@@ -49,18 +57,21 @@ function de(e) {
? `服务连接暂时异常,请稍后重试`
: t;
}
// 列表缓存最多 32 项,每项不超过 200 条、存活 60 秒;身份切换或写操作会使其失效。
var fe = 32;
var pe = 200;
var me = 6e4;
var G = new Map();
var he = Symbol(`uninitialized`);
var ge = 0;
// 递增代次,阻止清空之前发出的旧请求在完成后重新写回缓存。
function _e() {
(ge++, G.clear());
}
function ve() {
return ge;
}
/** 按身份/命名空间、接口及参数生成稳定键;对象键排序避免同义参数重复缓存。 */
function ye(e, t, n = ``) {
return JSON.stringify([e, n, t], (e, t) =>
t && typeof t == `object` && !Array.isArray(t)
@@ -72,15 +83,18 @@ function ye(e, t, n = ``) {
: t,
);
}
// 缓存只保存可 JSON 序列化的数据;读写均复制,避免页面修改共享对象。
function be(e) {
return JSON.parse(JSON.stringify(e));
}
/** e 为当前身份,t 为缓存键;切换身份清空缓存,命中时移动到 Map 尾部维护 LRU。 */
function xe(e, t, n = Date.now()) {
he !== e && (_e(), (he = e));
for (let [e, t] of G) t.expires <= n && G.delete(e);
let r = G.get(t);
if (r) return (G.delete(t), G.set(t, r), be(r.data));
}
/** 仅接受身份和请求开始时代次均未变化的列表结果;过大的结果或序列化失败不缓存。 */
function Se(e, t, n, r, i = Date.now()) {
if (e === he && r === ge) {
if (!Array.isArray(n.list) || n.list.length > pe) {
@@ -101,6 +115,7 @@ function Se(e, t, n, r, i = Date.now()) {
}
}
var Ce = 2147483647;
// 购买协议的整数范围与后端有符号 32 位整数一致;位置允许 0,商品和数量必须大于 0。
function K(e, t) {
let n =
typeof e == `number`
@@ -124,6 +139,10 @@ function Te(e) {
qty: K(t.qty, 1),
};
}
/**
* 从实际写请求提取限购检查所需字段;未匹配的接口返回 null,不进入购买预检。
* 购物车位置去重,批量添加最多 1000 项;此处校验不替代后端的最终授权。
*/
function Ee(e, t, n) {
if (e === `POST` && t === `/product/settle`)
return {
@@ -174,6 +193,7 @@ function Ee(e, t, n) {
}
return null;
}
// 预检必须明确允许/拒绝,拒绝时必须给出非空原因;缺失字段不能被当成放行。
function De(e) {
if (!e || typeof e != `object` || Array.isArray(e)) return !1;
let t = e;
@@ -183,6 +203,7 @@ function De(e) {
(t.allowed || !!t.message.trim())
);
}
/** 统一传递 HTTP/业务状态和可供安全验证等流程读取的数据,不丢失错误分类。 */
class ApiError extends Error {
status;
data;
@@ -201,6 +222,10 @@ function requestTimeoutError() {
return new ApiError(408, "请求超时,请稍后重试");
}
/**
* 截止时间覆盖整个异步操作(含预检和 JSON/Blob 解码),而非只覆盖收到响应头之前。
* 超时会中止 fetch;finally 清理计时器,调用方仍须在 await 后检查取消状态。
*/
async function withRequestDeadline(
operation,
timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS,
@@ -232,16 +257,19 @@ function assertRequestActive(signal) {
if (signal.aborted) throw requestTimeoutError();
}
// 已分类的业务错误原样传播;浏览器底层网络异常统一为状态 0,避免泄露内部信息。
function normalizeRequestError(error) {
return error instanceof ApiError
? error
: new ApiError(0, "网络异常,请检查网络后重试");
}
/** 在 Nuxt 上下文中创建客户端;apiBase 只配置后端 origin,本模块统一追加接口前缀。 */
function useApi() {
const config = useRuntimeConfig();
const auth = useAuth();
// 过期请求的 401 只能清理它所属的会话,不能把随后重新登录的新账号退出。
function clearUnauthorizedSession(requestToken) {
if (
!requestToken ||
@@ -269,6 +297,7 @@ function useApi() {
: {};
}
// HTTP 401 优先于业务信封;其他错误优先使用非 200 的业务状态,并脱敏错误消息。
function responseError(response, payload, requestToken, fallbackMessage) {
const envelope = responseObject(payload);
const status =
@@ -285,6 +314,7 @@ function useApi() {
);
}
/** 无 status 的 data 数组仅在调用方明确开启兼容项时接受,其他格式异常均拒绝。 */
async function decodeResponse(
response,
requestToken,
@@ -307,6 +337,7 @@ function useApi() {
throw responseError(response, payload, requestToken, fallbackMessage);
}
// 数组参数编码为 key[];默认省略 null/空串,少数筛选接口可显式保留空串。
function apiUrl(path, query, keepEmpty = false) {
let url = `${String(config.public.apiBase ?? "")}/console/v1${path}`;
const search = new URLSearchParams();
@@ -325,6 +356,7 @@ function useApi() {
return url;
}
/** JSON 请求主路径:固定本次身份和请求体 → 必要时限购预检 → 请求 → 验证信封。 */
async function request(method, path, options = {}) {
// Capture identity before the first await, including when logout clears it immediately.
const requestToken = auth.token;
@@ -349,6 +381,7 @@ function useApi() {
);
}
if (purchase) {
// 等待预检期间身份或表单可能变化;二次比较确保真正提交的是已核验的内容。
const authToken = auth.token;
const permission = await request("POST", "/purchase_limit/check", {
body: purchase,
@@ -391,6 +424,7 @@ function useApi() {
options.allowStatuslessDataArray,
);
assertRequestActive(signal);
// 普通写操作清空列表缓存;只读报价和限购预检不应让正在展示的列表无故失效。
if (
method !== "GET" &&
path !== "/purchase_limit/check" &&
@@ -405,6 +439,7 @@ function useApi() {
}
}
/** 文件使用 multipart/form-data,由浏览器生成 boundary;不要手动设置 Content-Type。 */
async function upload(file) {
const body = new FormData();
body.append("file", file);
@@ -434,6 +469,7 @@ function useApi() {
}
}
/** 下载仍校验 JSON 错误信封;成功后创建临时下载链接,并在点击后释放对象 URL。 */
async function download(path, data, filename = "export.xlsx", options = {}) {
const method = options.method ?? "GET";
const headers = { Accept: "*/*" };
@@ -529,6 +565,8 @@ function useApi() {
del: (path, query, body) => request("DELETE", path, { query, body }),
};
}
// 既有后端登录协议要求固定 AES-CBC 参数;这是传输字段兼容,不是安全密码存储。
// 修改必须与后端协商,HTTPS 仍不可省略,前端公开的固定参数不能视为秘密。
var ot = CryptoJS.enc.Utf8.parse(`idcsmart.finance`);
var st = CryptoJS.enc.Utf8.parse(`9311019310287172`);
function ct(e) {
@@ -538,6 +576,7 @@ function ct(e) {
iv: st,
}).toString();
}
/** 按安全验证、限流、超时和服务异常分类登录错误,屏蔽 HTML/SQL/堆栈等内部内容。 */
function lt(e, t = `登录失败,请稍后重试`) {
let n = e && typeof e == `object` ? e : {},
r = typeof n.message == `string` ? n.message.trim() : ``,
@@ -563,11 +602,13 @@ function lt(e, t = `登录失败,请稍后重试`) {
? t
: r;
}
// 这里只检查凭证形状,不验证签名和权限;真实性须由携带凭证的后端请求确认。
function ut(e) {
return (
typeof e == `string` && e.length > 0 && e.length <= 8192 && !/\s/.test(e)
);
}
/** 从接口成功/失败的常见包装中识别额外安全验证挑战,未找到时返回 null。 */
function dt(e) {
if (!e || typeof e != `object`) return null;
let t = e,
@@ -586,6 +627,7 @@ function dt(e) {
}
return null;
}
// 收到挑战不能被当成业务已完成;将挑战附在异常 data 上交给安全验证对话框。
function ft(e) {
let t = dt(e);
if (t)
@@ -599,6 +641,7 @@ var pt = {
phone_code: `手机验证码`,
certification: `实名认证`,
};
/** 将后端允许的验证方式转成选择项;未知方式不展示,逐项 token 优先于公共 token。 */
function mt(e) {
return (
Array.isArray(e.available_methods) ? e.available_methods : []
@@ -628,6 +671,10 @@ function mt(e) {
: [];
});
}
/**
* JWT 持久化到 localStorage 是现有会话约定,仍需防范 XSS;不在日志中输出凭证。
* sessionVersion 隔离跨登录的异步结果,overviewPromise 合并同一会话的并发首页请求。
*/
const useAuth = defineStore("auth", () => {
const token = ref(localStorage.getItem("jwt"));
const overview = ref(null);
@@ -638,6 +685,7 @@ const useAuth = defineStore("auth", () => {
let sessionVersion = 0;
let legacySession = null;
// 仅在登录或服务端核验成功后调用;先清理上一会话的缓存与未完成旧平台登录。
function loginWithJwt(jwt) {
if (!ut(jwt))
throw Error("登录响应无效,请重新登录;若持续失败,请联系客服。");
@@ -646,6 +694,7 @@ const useAuth = defineStore("auth", () => {
localStorage.setItem("jwt", jwt);
}
// 旧平台带来的凭证先暂存待验证,不立即写成已登录状态,避免路由守卫误放行。
function prepareLegacyLogin(jwt = localStorage.getItem("jwt")) {
clear();
legacySession = {
@@ -657,12 +706,17 @@ const useAuth = defineStore("auth", () => {
legacyLoginPending.value = true;
}
// 退出/切换身份会中止旧验证,并丢弃其结果。
function cancelLegacyLogin() {
legacySession?.controller?.abort();
legacySession = null;
legacyLoginPending.value = false;
}
/**
* 用 /index 实际核验旧平台 JWT;并发点击共享 Promise,失败后允许同一入口重试。
* no-store/omit/no-referrer 避免缓存、Cookie 和 Referer 携带额外身份信息。
*/
function verifyLegacyLogin() {
const session = legacySession;
if (!session?.jwt) {
@@ -731,6 +785,7 @@ const useAuth = defineStore("auth", () => {
return promise;
}
/** 密码/手机验证码登录共用入口;安全验证结果由调用方传入,成功后才保存 JWT。 */
async function login(
account,
password,
@@ -773,6 +828,7 @@ const useAuth = defineStore("auth", () => {
}
}
// 在请求完成前共享 Promise;会话版本不一致时拒绝回填,防止旧账号资料覆盖新账号。
function loadOverview() {
if (!overviewPromise) {
const version = sessionVersion;
@@ -791,6 +847,7 @@ const useAuth = defineStore("auth", () => {
return overviewPromise;
}
// 页面首次读取允许使用本会话已有资料;需要强制刷新余额等信息时调用 refreshOverview。
async function fetchOverview() {
return overview.value ?? loadOverview();
}
@@ -799,6 +856,7 @@ const useAuth = defineStore("auth", () => {
return loadOverview();
}
// 本地退出立即生效,不等待远端吊销成功;请求已同步捕获旧 Bearer,不会误用新身份。
function logout() {
// request() captures the original Bearer synchronously; remote revocation never owns local logout.
const revocation = useApi().post("/logout");
@@ -806,6 +864,7 @@ const useAuth = defineStore("auth", () => {
void revocation.catch(() => {});
}
// 所有会话切换走同一清理入口,使缓存、旧平台验证和账户资料同时失效。
function resetSessionState() {
_e();
cancelLegacyLogin();