From 6b32f254b1d3f2f750d2ee0ae1780f30fe960228 Mon Sep 17 00:00:00 2001 From: hjhan Date: Wed, 29 Jul 2026 22:33:45 +0800 Subject: [PATCH] =?UTF-8?q?test(fe):=20=E9=98=B6=E6=AE=B50=E6=AD=A2?= =?UTF-8?q?=E8=A1=80=E2=80=94=E2=80=94pre-commit=E5=AE=88=E5=8D=AB+postSaf?= =?UTF-8?q?e+jest.config+CI=E8=84=9A=E6=9C=AC+otcformat=E7=B2=BE=E5=BA=A6?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- YLErpWeb/App_Data/Config/otcformat.js | 2 +- YLErpWeb/fe-tests/hooks/pre-commit | 89 ++++++++++++ YLErpWeb/fe-tests/jest.config.js | 49 +++++++ YLErpWeb/fe-tests/main.postSafe.test.js | 144 +++++++++++++++++++ YLErpWeb/fe-tests/run-ci-checks.sh | 96 +++++++++++++ YLErpWeb/wwwroot/Scripts/base/main.js | 37 +++++ YLErpWeb/wwwroot/Statics/bundles/bundle.js | 49 +++++++ YLErpWeb/wwwroot/Statics/bundles/bundleV2.js | 49 +++++++ 8 files changed, 514 insertions(+), 1 deletion(-) create mode 100755 YLErpWeb/fe-tests/hooks/pre-commit create mode 100644 YLErpWeb/fe-tests/jest.config.js create mode 100644 YLErpWeb/fe-tests/main.postSafe.test.js create mode 100755 YLErpWeb/fe-tests/run-ci-checks.sh diff --git a/YLErpWeb/App_Data/Config/otcformat.js b/YLErpWeb/App_Data/Config/otcformat.js index 626aa7c8..92b1f226 100644 --- a/YLErpWeb/App_Data/Config/otcformat.js +++ b/YLErpWeb/App_Data/Config/otcformat.js @@ -1,2 +1,2 @@ var main = main || {}; -main.formatOptions = { "trading": { "umprice": { "trimTailZeros": true, "precision": 9, "grouping": true, "rounded": true, "percent": false, "minDecimals": 2, "maxDecimals": 9 }, "umpriceP": { "trimTailZeros": true, "percent": true, "precision": 9, "grouping": false, "rounded": true, "minDecimals": 2, "maxDecimals": 9 }, "umpricePR": { "trimTailZeros": true, "precision": 9, "grouping": false, "rounded": true, "percent": false, "minDecimals": 2, "maxDecimals": 9 }, "tradeSinglePrice": { "trimTailZeros": true, "precision": 2, "grouping": true, "rounded": true, "percent": false, "minDecimals": 2, "maxDecimals": 9 }, "premiumRateP": { "trimTailZeros": true, "percent": true, "precision": 9, "grouping": false, "rounded": true, "minDecimals": 2, "maxDecimals": 9 }, "premiumRate": { "trimTailZeros": true, "precision": 9, "grouping": false, "rounded": true, "percent": false, "minDecimals": 2, "maxDecimals": 9 }, "tradePrice": { "trimTailZeros": true, "precision": 9, "grouping": false, "rounded": true, "percent": false, "minDecimals": 2, "maxDecimals": 9 }, "StockEqvNotional": { "trimTailZeros": true, "precision": 9, "grouping": true, "rounded": true, "percent": false, "minDecimals": 2, "maxDecimals": 9 }, "notional": { "trimTailZeros": true, "precision": 9, "grouping": true, "rounded": true, "percent": false, "minDecimals": 2, "maxDecimals": 9 }, "notionalP": { "trimTailZeros": true, "percent": true, "precision": 9, "grouping": false, "rounded": true, "minDecimals": 2, "maxDecimals": 9 }, "volatility": { "trimTailZeros": true, "precision": 9, "grouping": false, "rounded": true, "percent": false, "minDecimals": 2, "maxDecimals": 9 }, "volatilityP": { "trimTailZeros": true, "percent": true, "precision": 9, "grouping": false, "rounded": true, "minDecimals": 2, "maxDecimals": 9 }, "greek": { "trimTailZeros": true, "precision": 9, "grouping": false, "rounded": true, "percent": false, "minDecimals": 2, "maxDecimals": 9 }, "marginRateP": { "trimTailZeros": true, "percent": true, "precision": 9, "grouping": false, "rounded": true, "minDecimals": 2, "maxDecimals": 9 }, "marginRate": { "trimTailZeros": true, "precision": 9, "grouping": false, "rounded": true, "percent": false, "minDecimals": 2, "maxDecimals": 9 } } }; \ No newline at end of file +main.formatOptions = { "trading": { "umprice": { "trimTailZeros": true, "precision": 9, "grouping": true, "rounded": true, "percent": false, "minDecimals": 2, "maxDecimals": 9 }, "umpriceP": { "trimTailZeros": true, "percent": true, "precision": 9, "grouping": false, "rounded": true, "minDecimals": 2, "maxDecimals": 9 }, "umpricePR": { "trimTailZeros": true, "precision": 9, "grouping": false, "rounded": true, "percent": false, "minDecimals": 2, "maxDecimals": 9 }, "tradeSinglePrice": { "trimTailZeros": true, "precision": 9, "grouping": true, "rounded": true, "percent": false, "minDecimals": 2, "maxDecimals": 9 }, "premiumRateP": { "trimTailZeros": true, "percent": true, "precision": 9, "grouping": false, "rounded": true, "minDecimals": 2, "maxDecimals": 9 }, "premiumRate": { "trimTailZeros": true, "precision": 9, "grouping": false, "rounded": true, "percent": false, "minDecimals": 2, "maxDecimals": 9 }, "tradePrice": { "trimTailZeros": true, "precision": 9, "grouping": false, "rounded": true, "percent": false, "minDecimals": 2, "maxDecimals": 9 }, "StockEqvNotional": { "trimTailZeros": true, "precision": 9, "grouping": true, "rounded": true, "percent": false, "minDecimals": 2, "maxDecimals": 9 }, "notional": { "trimTailZeros": true, "precision": 9, "grouping": true, "rounded": true, "percent": false, "minDecimals": 2, "maxDecimals": 9 }, "notionalP": { "trimTailZeros": true, "percent": true, "precision": 9, "grouping": false, "rounded": true, "minDecimals": 2, "maxDecimals": 9 }, "volatility": { "trimTailZeros": true, "precision": 9, "grouping": false, "rounded": true, "percent": false, "minDecimals": 2, "maxDecimals": 9 }, "volatilityP": { "trimTailZeros": true, "percent": true, "precision": 9, "grouping": false, "rounded": true, "minDecimals": 2, "maxDecimals": 9 }, "greek": { "trimTailZeros": true, "precision": 9, "grouping": false, "rounded": true, "percent": false, "minDecimals": 2, "maxDecimals": 9 }, "marginRateP": { "trimTailZeros": true, "percent": true, "precision": 9, "grouping": false, "rounded": true, "minDecimals": 2, "maxDecimals": 9 }, "marginRate": { "trimTailZeros": true, "precision": 9, "grouping": false, "rounded": true, "percent": false, "minDecimals": 2, "maxDecimals": 9 } } }; \ No newline at end of file diff --git a/YLErpWeb/fe-tests/hooks/pre-commit b/YLErpWeb/fe-tests/hooks/pre-commit new file mode 100755 index 00000000..a7e9f734 --- /dev/null +++ b/YLErpWeb/fe-tests/hooks/pre-commit @@ -0,0 +1,89 @@ +#!/usr/bin/env bash +# ============================================================================= +# pre-commit — 提交前自动守卫 +# ============================================================================= +# 做两件事(任一失败即阻断提交): +# 1. guard_arch.js — 扫描新增/修改行,禁止在 Vue 组件里内联金额计算 +# 2. jest — 跑前端单测,确保不引入回归 +# +# 安装方式(开发者只需执行一次): +# cd +# cp YLErpWeb/fe-tests/hooks/pre-commit .git/hooks/pre-commit +# chmod +x .git/hooks/pre-commit +# +# 跳过方式(紧急情况,不推荐): +# git commit --no-verify +# +# CI 也应调用本脚本(或等价的 npm test + node guard_arch.js)。 +# ============================================================================= +set -euo pipefail + +# 定位仓库根目录 +REPO_ROOT=$(git rev-parse --show-toplevel) +FE_TESTS_DIR="$REPO_ROOT/YLErpWeb/fe-tests" + +# 自动加载 nvm(非交互 shell 中 nvm 不会自动加载) +export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" +if [ -s "$NVM_DIR/nvm.sh" ] && ! command -v node &>/dev/null; then + source "$NVM_DIR/nvm.sh" 2>/dev/null || true + # 如果有 .nvmrc 就用它,否则用默认版本 + if [ -f "$REPO_ROOT/.nvmrc" ]; then + nvm use --silent 2>/dev/null || true + else + # 尝试用已安装的最新版本 + nvm use --silent --lts 2>/dev/null || nvm use --silent node 2>/dev/null || true + fi +fi + +# 颜色输出 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +echo -e "${YELLOW}[pre-commit] 开始提交前守卫检查...${NC}" + +# ----------------------------------------------------------------------------- +# 1. guard_arch.js — 架构闸门(零依赖,纯 Node) +# ----------------------------------------------------------------------------- +# 只在存在 node 时运行 +if command -v node &>/dev/null; then + echo -e "${YELLOW}[pre-commit] ① 架构闸门 (guard_arch.js)...${NC}" + if node "$FE_TESTS_DIR/guard_arch.js"; then + echo -e "${GREEN}[pre-commit] ✅ 架构闸门通过${NC}" + else + echo -e "${RED}[pre-commit] ❌ 架构闸门未通过,提交被阻断${NC}" + echo -e "${YELLOW} 请把金额/精度计算抽到 *Calc 模块(参考 swapCalc.js)${NC}" + exit 1 + fi +else + echo -e "${YELLOW}[pre-commit] ⚠️ 未找到 node,跳过架构闸门(建议安装 nvm + node 20)${NC}" +fi + +# ----------------------------------------------------------------------------- +# 2. jest — 前端单测(需要先 npm install) +# ----------------------------------------------------------------------------- +if [ -f "$FE_TESTS_DIR/node_modules/.bin/jest" ]; then + echo -e "${YELLOW}[pre-commit] ② 前端单测 (jest)...${NC}" + # 只在有 JS 源文件改动时才跑测试 + JS_CHANGED=$(git diff --cached --name-only --diff-filter=ACM -- "*.js" | grep "wwwroot/Scripts/" || true) + if [ -z "$JS_CHANGED" ]; then + echo -e "${GREEN}[pre-commit] ⏭️ 无前端源文件改动,跳过单测${NC}" + else + echo -e "${YELLOW} 改动的前端文件:${NC}" + echo "$JS_CHANGED" | sed 's/^/ /' + if (cd "$FE_TESTS_DIR" && npx jest --no-coverage --silent 2>&1); then + echo -e "${GREEN}[pre-commit] ✅ 前端单测通过${NC}" + else + echo -e "${RED}[pre-commit] ❌ 前端单测未通过,提交被阻断${NC}" + echo -e "${YELLOW} 修复测试后重新提交,或用 git commit --no-verify 跳过(不推荐)${NC}" + exit 1 + fi + fi +else + echo -e "${YELLOW}[pre-commit] ⚠️ 未安装 jest(node_modules 缺失),跳过单测${NC}" + echo -e "${YELLOW} 建议:cd YLErpWeb/fe-tests && npm install${NC}" +fi + +echo -e "${GREEN}[pre-commit] ✅ 所有守卫检查通过,继续提交${NC}" +exit 0 diff --git a/YLErpWeb/fe-tests/jest.config.js b/YLErpWeb/fe-tests/jest.config.js new file mode 100644 index 00000000..d60ebdc6 --- /dev/null +++ b/YLErpWeb/fe-tests/jest.config.js @@ -0,0 +1,49 @@ +/** + * jest.config.js — 前端测试配置 + * ============================================================================ + * 运行方式:cd YLErpWeb/fe-tests && npx jest + * + * 设计要点: + * - testEnvironment: jsdom — 模拟浏览器环境(jQuery / Vue 组件测试需要) + * - testMatch: 只跑 *.test.js,不跑 _shim_run.js / guard_arch.js + * - collectCoverage: 默认不开(影响速度),用 --coverage 手动开启 + * - coverageThreshold: 新代码覆盖率门槛,逐步提高 + * - moduleDirectories: 让测试文件能 require('jquery') 等npm包 + * ============================================================================ + */ +module.exports = { + // 保持 node 环境:部分测试文件自行 require('jsdom') 搭建 DOM, + // 若改为 jsdom 环境会与之冲突(TextEncoder 未定义等问题)。 + testEnvironment: 'node', + + // 只匹配 *.test.js + testMatch: ['**/*.test.js'], + + // 模块查找路径(node_modules + wwwroot/Scripts 便于 require 源文件) + moduleDirectories: ['node_modules', '../wwwroot/Scripts'], + + // 覆盖率配置(--coverage 时生效) + collectCoverageFrom: [ + '../wwwroot/Scripts/app/swaptrade/swapCalc.js', + '../wwwroot/Scripts/fast/fastVue.base.js', + // 逐步加入更多文件 + ], + + coverageDirectory: 'coverage', + + // 覆盖率门槛(初始宽松,逐步收紧) + coverageThreshold: { + global: { + branches: 40, + functions: 50, + lines: 50, + statements: 50, + }, + }, + + // 不转换 node_modules(本项目 JS 是原生 ES5,不需要 babel) + transform: {}, + + // verbose 输出 + verbose: false, +}; diff --git a/YLErpWeb/fe-tests/main.postSafe.test.js b/YLErpWeb/fe-tests/main.postSafe.test.js new file mode 100644 index 00000000..9edbe534 --- /dev/null +++ b/YLErpWeb/fe-tests/main.postSafe.test.js @@ -0,0 +1,144 @@ +/** + * main.postSafe.test.js — main.postSafe() 错误守卫封装测试 + * ============================================================================ + * 目的:验证 postSafe 正确将 jQuery Deferred 的 .done/.fail 映射到 Promise 的 resolve/reject, + * 防止「业务错误走 reject、.done 不触发」陷阱复发。 + * + * 覆盖场景: + * 1. 成功请求 → Promise resolve + * 2. 业务错误(resp.success === false)→ Promise reject + * 3. 网络异常 → Promise reject + * 4. async/await 语法兼容性 + * 5. 与 main.post 行为对比(陷阱复现) + * + * 运行:cd YLErpWeb/fe-tests && npx jest main.postSafe + * ============================================================================ + */ + +// 搭建最小 DOM 环境 +const { JSDOM } = require('jsdom'); +const dom = new JSDOM(''); +global.window = dom.window; +global.document = dom.window.document; +global.navigator = dom.window.navigator; + +// Mock main 对象(与 main.js 中的逻辑等价) +var main = {}; + +// main.post: 返回 jQuery Deferred promise,测试中通过 _lastDeferred 控制结果 +main.post = function (url, data, options) { + var doneCb = [], failCb = []; + var promise = { + done: function (cb) { doneCb.push(cb); return promise; }, + fail: function (cb) { failCb.push(cb); return promise; }, + }; + main._lastDeferred = { + resolve: function (resp) { doneCb.forEach(function (cb) { cb(resp); }); }, + reject: function (resp) { failCb.forEach(function (cb) { cb(resp); }); }, + promise: function () { return promise; }, + }; + return main._lastDeferred.promise(); +}; + +// main.postSafe: 与 main.js 中完全相同的实现 +main.postSafe = function (url, data, options) { + return new Promise(function (resolve, reject) { + main.post(url, data, options) + .done(function (resp) { resolve(resp); }) + .fail(function (resp) { reject(resp); }); + }); +}; + +describe('main.postSafe — Promise 封装守卫', function () { + + test('成功请求 → Promise resolve(resp)', function () { + var p = main.postSafe('/api/test', { foo: 1 }); + // postSafe 内部已调用 main.post,_lastDeferred 已就绪 + main._lastDeferred.resolve({ success: true, data: 'ok' }); + return p.then(function (resp) { + expect(resp.success).toBe(true); + expect(resp.data).toBe('ok'); + }); + }); + + test('业务错误(resp.success===false)→ Promise reject(resp)', function () { + var p = main.postSafe('/api/test', { foo: 1 }); + main._lastDeferred.reject({ success: false, msg: '算不出来' }); + return p.catch(function (resp) { + expect(resp.success).toBe(false); + expect(resp.msg).toBe('算不出来'); + }); + }); + + test('网络异常 → Promise reject', function () { + var p = main.postSafe('/api/test', { foo: 1 }); + main._lastDeferred.reject({ errcode: 500, msg: '请求失败' }); + return p.catch(function (resp) { + expect(resp.errcode).toBe(500); + }); + }); + + test('async/await 语法兼容 — 成功路径', async function () { + var p = main.postSafe('/api/test', { foo: 1 }); + main._lastDeferred.resolve({ success: true, value: 42 }); + var resp = await p; + expect(resp.success).toBe(true); + expect(resp.value).toBe(42); + }); + + test('async/await 语法兼容 — 失败路径', async function () { + var p = main.postSafe('/api/test', { foo: 1 }); + main._lastDeferred.reject({ success: false, msg: '计算失败' }); + + var caught = null; + try { + await p; + } catch (resp) { + caught = resp; + } + expect(caught).not.toBeNull(); + expect(caught.success).toBe(false); + expect(caught.msg).toBe('计算失败'); + }); +}); + +describe('main.postSafe — 与 main.post 行为对比(陷阱复现)', function () { + + test('main.post 的 .done 在业务错误时不触发(陷阱复现)', function () { + var doneCalled = false; + var failCalled = false; + + main.post('/api/test', {}).done(function () { + doneCalled = true; + }).fail(function () { + failCalled = true; + }); + + // 业务错误 → reject + main._lastDeferred.reject({ success: false }); + + expect(doneCalled).toBe(false); // .done 没触发!这就是陷阱 + expect(failCalled).toBe(true); // .fail 触发了 + }); + + test('main.postSafe 的 .then 在业务错误时不触发(安全)', function () { + var thenCalled = false; + var caught = null; + + var p = main.postSafe('/api/test', {}).then(function () { + thenCalled = true; + }).catch(function (resp) { + caught = resp; + }); + + // 业务错误 → reject + main._lastDeferred.reject({ success: false, msg: 'biz error' }); + + // Promise 是微任务,需要等一轮 + return p.then(function () { + expect(thenCalled).toBe(false); // .then 没触发 + expect(caught).not.toBeNull(); // .catch 触发了 + expect(caught.msg).toBe('biz error'); + }); + }); +}); diff --git a/YLErpWeb/fe-tests/run-ci-checks.sh b/YLErpWeb/fe-tests/run-ci-checks.sh new file mode 100755 index 00000000..b8c290fa --- /dev/null +++ b/YLErpWeb/fe-tests/run-ci-checks.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash +# ============================================================================= +# run-ci-checks.sh — CI 守卫入口脚本 +# ============================================================================= +# Jenkins / GitLab CI / GitHub Actions 等任一 CI 系统调用本脚本即可。 +# +# 做三件事(任一失败即 exit 1,阻断 CI): +# 1. guard_arch.js — 架构闸门:禁止新增内联金额计算 +# 2. jest — 前端单测:防止回归 +# 3. rebuild-bundles — bundle 校验:确保产物与源文件一致 +# +# 用法: +# bash YLErpWeb/fe-tests/run-ci-checks.sh +# +# 前置条件: +# - Node.js 18+ 已安装(CI 环境通常自带) +# - cd YLErpWeb/fe-tests && npm install 已执行 +# ============================================================================= +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +FE_TESTS_DIR="$REPO_ROOT/YLErpWeb/fe-tests" +YLERPWEB_DIR="$REPO_ROOT/YLErpWeb" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +FAILED=0 + +run_check() { + local name="$1" + local cmd="$2" + echo -e "\n${YELLOW}========================================${NC}" + echo -e "${YELLOW}CI Check: ${name}${NC}" + echo -e "${YELLOW}========================================${NC}" + if eval "$cmd"; then + echo -e "${GREEN}✅ ${name} PASSED${NC}" + else + echo -e "${RED}❌ ${name} FAILED${NC}" + FAILED=$((FAILED + 1)) + fi +} + +# ----------------------------------------------------------------------------- +# Check 1: 架构闸门 +# ----------------------------------------------------------------------------- +if command -v node &>/dev/null; then + run_check "guard_arch.js (架构闸门)" \ + "node '$FE_TESTS_DIR/guard_arch.js'" +else + echo -e "${YELLOW}⚠️ node 未安装,跳过架构闸门${NC}" +fi + +# ----------------------------------------------------------------------------- +# Check 2: 前端单测 +# ----------------------------------------------------------------------------- +if [ -f "$FE_TESTS_DIR/node_modules/.bin/jest" ]; then + run_check "jest (前端单测)" \ + "cd '$FE_TESTS_DIR' && npx jest --no-coverage --silent" +else + echo -e "${YELLOW}⚠️ jest 未安装,尝试自动安装...${NC}" + if command -v npm &>/dev/null; then + (cd "$FE_TESTS_DIR" && npm install --silent) + run_check "jest (前端单测)" \ + "cd '$FE_TESTS_DIR' && npx jest --no-coverage --silent" + else + echo -e "${RED}❌ npm 不可用,无法运行前端单测${NC}" + FAILED=$((FAILED + 1)) + fi +fi + +# ----------------------------------------------------------------------------- +# Check 3: Bundle 校验(可选,需要 python3) +# ----------------------------------------------------------------------------- +if [ -f "$YLERPWEB_DIR/rebuild-bundles.py" ] && command -v python3 &>/dev/null; then + run_check "bundle 校验 (rebuild-bundles.py --verify)" \ + "cd '$YLERPWEB_DIR' && python3 rebuild-bundles.py --verify" +else + echo -e "${YELLOW}⚠️ python3 或 rebuild-bundles.py 不可用,跳过 bundle 校验${NC}" +fi + +# ----------------------------------------------------------------------------- +# 汇总 +# ----------------------------------------------------------------------------- +echo -e "\n${YELLOW}========================================${NC}" +echo -e "${YELLOW}CI Checks Summary${NC}" +echo -e "${YELLOW}========================================${NC}" +if [ "$FAILED" -eq 0 ]; then + echo -e "${GREEN}✅ All checks passed${NC}" + exit 0 +else + echo -e "${RED}❌ ${FAILED} check(s) failed${NC}" + exit 1 +fi diff --git a/YLErpWeb/wwwroot/Scripts/base/main.js b/YLErpWeb/wwwroot/Scripts/base/main.js index f70a92f3..9c703d63 100644 --- a/YLErpWeb/wwwroot/Scripts/base/main.js +++ b/YLErpWeb/wwwroot/Scripts/base/main.js @@ -472,6 +472,43 @@ main.post = function (url, data, options) { return d.promise(); }; +/** + * main.postSafe — 安全版 main.post,返回标准 Promise 而非 jQuery Deferred。 + * ============================================================================ + * 解决 main.post 的「业务错误走 reject、.done 不触发」陷阱(见 __post 注释)。 + * + * 用法: + * main.postSafe('/api/calc', { price: 100 }) + * .then(function(resp) { /* 成功(含 resp.success !== false)*\/ }) + * .catch(function(resp) { /* 业务错误 / 网络异常 *\/ }); + * + * 或者 async/await: + * try { + * var resp = await main.postSafe('/api/calc', { price: 100 }); + * // 成功 + * } catch (resp) { + * // 业务错误 / 网络异常(resp 可能是 { success:false, msg:'...' } 或 Error) + * } + * + * 与 main.post 的区别: + * - main.post 返回 jQuery Deferred:.done = 成功, .fail = 失败 + * - main.postSafe 返回 Promise: .then = 成功, .catch = 失败 + * - 行为一致,但 Promise 更符合标准,async/await 更自然 + * + * @param {string} url -- post url + * @param {any} data -- post data + * @param {any} options -- 同 main.post + * @returns {Promise} -- 成功 resolve(resp),失败 reject(resp) + * ============================================================================ + */ +main.postSafe = function (url, data, options) { + return new Promise(function (resolve, reject) { + main.post(url, data, options) + .done(function (resp) { resolve(resp); }) + .fail(function (resp) { reject(resp); }); + }); +}; + //确认提交 main.confirmPost = function (confirmText, url, data, options) { var d = $.Deferred(); diff --git a/YLErpWeb/wwwroot/Statics/bundles/bundle.js b/YLErpWeb/wwwroot/Statics/bundles/bundle.js index 68a861c0..4a1820bd 100644 --- a/YLErpWeb/wwwroot/Statics/bundles/bundle.js +++ b/YLErpWeb/wwwroot/Statics/bundles/bundle.js @@ -15352,6 +15352,17 @@ function __getWaitMeFunc(options) { return waitMeFunc; } +/** + * ⚠️【全局陷阱 / 接手必读】__post 的成败路由: + * - 成功(resp.success 为真 且 非 errcode) -> deferred.resolve(resp) (.done 触发) + * - 业务错误(resp.success===false / errcode) -> deferred.reject(resp) (.fail 触发,.done 不触发!) + * - 网络异常(ajax error) -> deferred.reject(resp) (.fail 触发) + * 即:业务错误走的是 reject,不是 resolve。调用方若只在 .done(...) 里写「业务失败/算不出来」的处理, + * 业务失败时那段代码根本不会执行 —— 这是 bug 静默失效的高发地。 + * 典型实例:swapTradeEdit.js 的 calcBondForItem「约定2补充(计算器算不出→清另两字段+清标识)」曾因 + * applyBondCalcFailure 只写在 .done 内、长期不生效;后改到 .fail 才修复(见该文件注释)。 + * 凡要区分「成功 / 业务失败 / 网络异常」的逻辑,务必在 .done 之外再挂 .fail。 + */ function __post(url, data, deferred, options) { if (!url) throw "post url 不能为空"; @@ -15456,12 +15467,50 @@ main.ajax = function (options) { * @param {boolean} options.suppressError -- 禁止出错时提示 * @returns {jQuery} -- jquery延迟对象Deferred */ +// ⚠️ 见上方 __post 说明:本方法业务错误/网络异常走 reject,调用方需 .fail 才能捕获(只 .done 会漏掉失败分支)。 main.post = function (url, data, options) { var d = $.Deferred(); __post(url, data, d, options); return d.promise(); }; +/** + * main.postSafe — 安全版 main.post,返回标准 Promise 而非 jQuery Deferred。 + * ============================================================================ + * 解决 main.post 的「业务错误走 reject、.done 不触发」陷阱(见 __post 注释)。 + * + * 用法: + * main.postSafe('/api/calc', { price: 100 }) + * .then(function(resp) { /* 成功(含 resp.success !== false)*\/ }) + * .catch(function(resp) { /* 业务错误 / 网络异常 *\/ }); + * + * 或者 async/await: + * try { + * var resp = await main.postSafe('/api/calc', { price: 100 }); + * // 成功 + * } catch (resp) { + * // 业务错误 / 网络异常(resp 可能是 { success:false, msg:'...' } 或 Error) + * } + * + * 与 main.post 的区别: + * - main.post 返回 jQuery Deferred:.done = 成功, .fail = 失败 + * - main.postSafe 返回 Promise: .then = 成功, .catch = 失败 + * - 行为一致,但 Promise 更符合标准,async/await 更自然 + * + * @param {string} url -- post url + * @param {any} data -- post data + * @param {any} options -- 同 main.post + * @returns {Promise} -- 成功 resolve(resp),失败 reject(resp) + * ============================================================================ + */ +main.postSafe = function (url, data, options) { + return new Promise(function (resolve, reject) { + main.post(url, data, options) + .done(function (resp) { resolve(resp); }) + .fail(function (resp) { reject(resp); }); + }); +}; + //确认提交 main.confirmPost = function (confirmText, url, data, options) { var d = $.Deferred(); diff --git a/YLErpWeb/wwwroot/Statics/bundles/bundleV2.js b/YLErpWeb/wwwroot/Statics/bundles/bundleV2.js index e91583a9..c63811b3 100644 --- a/YLErpWeb/wwwroot/Statics/bundles/bundleV2.js +++ b/YLErpWeb/wwwroot/Statics/bundles/bundleV2.js @@ -1151,6 +1151,17 @@ function __getWaitMeFunc(options) { return waitMeFunc; } +/** + * ⚠️【全局陷阱 / 接手必读】__post 的成败路由: + * - 成功(resp.success 为真 且 非 errcode) -> deferred.resolve(resp) (.done 触发) + * - 业务错误(resp.success===false / errcode) -> deferred.reject(resp) (.fail 触发,.done 不触发!) + * - 网络异常(ajax error) -> deferred.reject(resp) (.fail 触发) + * 即:业务错误走的是 reject,不是 resolve。调用方若只在 .done(...) 里写「业务失败/算不出来」的处理, + * 业务失败时那段代码根本不会执行 —— 这是 bug 静默失效的高发地。 + * 典型实例:swapTradeEdit.js 的 calcBondForItem「约定2补充(计算器算不出→清另两字段+清标识)」曾因 + * applyBondCalcFailure 只写在 .done 内、长期不生效;后改到 .fail 才修复(见该文件注释)。 + * 凡要区分「成功 / 业务失败 / 网络异常」的逻辑,务必在 .done 之外再挂 .fail。 + */ function __post(url, data, deferred, options) { if (!url) throw "post url 不能为空"; @@ -1255,12 +1266,50 @@ main.ajax = function (options) { * @param {boolean} options.suppressError -- 禁止出错时提示 * @returns {jQuery} -- jquery延迟对象Deferred */ +// ⚠️ 见上方 __post 说明:本方法业务错误/网络异常走 reject,调用方需 .fail 才能捕获(只 .done 会漏掉失败分支)。 main.post = function (url, data, options) { var d = $.Deferred(); __post(url, data, d, options); return d.promise(); }; +/** + * main.postSafe — 安全版 main.post,返回标准 Promise 而非 jQuery Deferred。 + * ============================================================================ + * 解决 main.post 的「业务错误走 reject、.done 不触发」陷阱(见 __post 注释)。 + * + * 用法: + * main.postSafe('/api/calc', { price: 100 }) + * .then(function(resp) { /* 成功(含 resp.success !== false)*\/ }) + * .catch(function(resp) { /* 业务错误 / 网络异常 *\/ }); + * + * 或者 async/await: + * try { + * var resp = await main.postSafe('/api/calc', { price: 100 }); + * // 成功 + * } catch (resp) { + * // 业务错误 / 网络异常(resp 可能是 { success:false, msg:'...' } 或 Error) + * } + * + * 与 main.post 的区别: + * - main.post 返回 jQuery Deferred:.done = 成功, .fail = 失败 + * - main.postSafe 返回 Promise: .then = 成功, .catch = 失败 + * - 行为一致,但 Promise 更符合标准,async/await 更自然 + * + * @param {string} url -- post url + * @param {any} data -- post data + * @param {any} options -- 同 main.post + * @returns {Promise} -- 成功 resolve(resp),失败 reject(resp) + * ============================================================================ + */ +main.postSafe = function (url, data, options) { + return new Promise(function (resolve, reject) { + main.post(url, data, options) + .done(function (resp) { resolve(resp); }) + .fail(function (resp) { reject(resp); }); + }); +}; + //确认提交 main.confirmPost = function (confirmText, url, data, options) { var d = $.Deferred();