现象:点确认收货 / 结算后,数据库其实已经写入成功,页面却弹出「结算失败:网络异常」,用户以为失败又点一次,造成重复操作和对账困惑。
根因:这是两条独立的路径叠加造成的假故障。
csetFetch / settleFetch 把 .catch 挂在处理回调 .then 的后面。
于是「回调函数内部的任何一个报错」(比如渲染时某个字段为空)都会掉进 .catch,被当成网络故障,
并二次调用回调弹出「网络异常」。真实情况是:请求成功了,只是渲染出了点小问题。php.ini 开着 display_errors,
任何一条 Warning 都会被打印在响应体最前面,导致前端 JSON.parse 解析失败,同样被归类成「网络异常」。
已实测复现:请求 ?company[]=x 会触发 Array to string conversion 警告,返回体污染。修复:
called 单次调用哨兵,回调保证只执行一次;先取 text() 再手动 JSON.parse,
解析失败时打印原始响应体到控制台并提示「服务端返回异常(HTTP xxx)」,不再一律甩锅网络;
.catch 只负责真正的网络故障;回调内部异常单独捕获并提示「页面处理出错,请刷新重试」。api.php 顶部加全局错误边界:本文件内强制 display_errors=0,
注册 set_error_handler / set_exception_handler / register_shutdown_function,
所有 Warning、未捕获异常、致命错误只写日志,响应体永远是合法 JSON。qparam() 安全取参:数组型入参(?company[]=x)直接按空值处理,从源头掐断该类警告。loadData 函数根本不存在loadData(...),但项目里从来只定义过 loadDB()。这些调用一执行就抛 ReferenceError,
再叠加上面的错误边界缺陷,就变成了用户看到的「网络异常」。loadData(cb),内部转调 loadDB() 后执行回调。openModal 原本只接受一个参数(HTML),但代码里有两处按 openModal(标题, 内容, 尺寸) 调用,
导致保存冲突提示弹窗和编辑销售单弹窗内容错乱 / 打不开。openModal(a, b, size),两种调用方式都支持,并在每次打开时重置弹窗宽度。editSale:原代码先取数组元素、后判断下标是否有效,越界时会抛错,现已调整判断顺序。seq 重置为 1,但历史结算单里仍记录着 N1 / N2 / N3 等来源批次号。
清空后新录入的批次又从 N1 开始,与历史单据撞号,会被幂等逻辑误判为重复而拒绝。seq,批次号继续往后排;同时清理遗留的 autoSettled 标记。load(整份台账,含全部账号)、cset_list、settle_list 三个读接口原本完全匿名可访问,
知道地址就能拉走全量经营数据。load 本版起强制校验 X-Api-Token(旧版前端本来就带令牌,升级无感知)。cset_list / settle_list 设置了宽限期:旧版前端调这两个接口时不带令牌,
若立刻强制校验,浏览器里缓存着旧页面的用户会直接 403 白屏。
因此本版只记录日志、暂不拦截,待日志确认无旧客户端残留后,
通过环境变量 PTX_ENFORCE_LIST_AUTH=1 正式开启(无需改代码)。$_SERVER['HTTP_X_API_TOKEN'] 兜底 —— HTTP/2 下请求头会全部小写,
不加这一步会在开启校验后把所有用户挡在门外。csetFetch / settleFetch 的 GET 分支原来不带令牌头,已同步补上;
e2e_settle.py 的读调用也已默认带令牌。load 下发(登录逻辑依赖它)。
真正的会话鉴权 + 按公司下发数据 + 凭据轮换安排在下一阶段,届时会一并改造登录流程。
updateLedgerData() 原来不检查 json_encode 是否失败,一旦序列化出错会把 false 当空串写入,整份台账被清空。lots 的完整台账结构;accounts / markups、而待写入数据里为空时,判定为误清空并中止;json_encode 返回 false 或结果长度异常时中止。loadLedger() 返回 null 时原代码会构造出只含 lots 的残缺对象再写回,
会抹掉账号、加价规则和批次序号。现已改为直接中止并提示,不做任何写入。$salesSplit = ($p['salesSplit'] ?? $salesSplit) 这种写法,
在结算单 payload 里存在但为空(空数组 / 空串)时,会用空值覆盖掉手动传入的拆分规则 ——
结果就是「确认收货时填的拆分数量没生效,全量转下游了」。!empty() 判定,payload 为空时才回退到手动传入值,并强制类型为数组。排查中发现部分「测试脚本」实际会直接写生产库、删生产数据,且无任何确认。已全部加护栏:
| 脚本 | 原风险 | 现在的护栏 |
|---|---|---|
dbhelper.py | pattern 为空时 LIKE '%%' 匹配全表,clean 等同删光结算数据 |
空 pattern 直接拒绝;clean 只允许测试前缀(SETTEST / SPLITTEST / E2E 等);越权删除须加 --i-know-this-is-prod 并输入 DELETE 确认;拒绝含引号分号的 pattern |
test_ptx_split.js | 硬编码生产地址,直接建单 + 调用 dbhelper 删库 | 必须显式传 TEST_BASE_URL;指向生产还须 TEST_ALLOW_PROD=1 |
e2e_settle.py | 默认在生产创建真实结算单并跨系统流转 | 生产运行须 TEST_ALLOW_PROD=1,可用 PTX_BT_API / PTX_SP_API 切测试环境 |
cleanup_e2e.py | PREFIX 若被改空则删除全部结算单 | 启动断言 PREFIX 非空且不含引号 / 通配符 |
顺带修掉 dbhelper.py 一个老问题:无匹配数据时 MySQL 的 SUM() 返回 NULL,旧代码在 float('NULL') 处崩溃。
上线后做目录巡检时发现,站点根目录残留的几个文件能被公网直接下载,且里面是明文凭据。 这比本次修的其它问题都严重,已当场封堵。
| 可下载的文件 | 泄露内容 |
|---|---|
api.php.bak.20260807125157api.php.bak.20260807125325 |
后端完整源码,含数据库密码明文回退值、SYNC_TOKEN、API_TOKEN |
__saas/data/control.sqlite |
SaaS 控制库:saas_keys 28 条(其中 9 条未吊销,含 api_key + api_secret)、
saas_admins 管理员口令哈希、saas_tenants 9 家租户的库名与库用户 |
进货联动管理系统.html |
8 月 7 日的旧版前端源码(内容与线上 app.html 同级,无额外机密,但属过期代码) |
处置:
.bak / .sql / .sqlite / .db / .env / .old 等后缀、
api.php.bak.<时间戳> 形式、.git / .svn / .ht、
/__saas/data/、/_rollback/ 一律返回 404/root/ptx_backups/(保留可追溯,不直接删).py / .sh / .sql / .env / .key / .md 等类型
即使误放进 dist/ 也会被拦截并告警——防止把带密码的脚本传上网站目录SYNC_TOKEN、9 条未吊销的租户 api_secret、SaaS 管理员口令。
轮换会影响正在对接的系统,需要协调停机窗口,暂未执行,等确认。
API_TOKEN 本身就写在前端页面里(app.html 明文常量),
任何打开页面的人都能拿到。本次给读接口加的令牌校验只能挡住不看源码的自动扫描,
挡不住有心之人。真正的解法是 v2.0.0 计划里的会话鉴权(登录换取有时效的令牌),届时一并处理。
php -l 服务端语法校验(PHP 8.3.27)通过node --check 前端主脚本语法校验(3 个脚本块)通过display_errors=On,响应体全部为合法 JSON,错误只进日志TEST_BASE_URL 运行、无 TEST_ALLOW_PROD 打生产
—— 四种危险操作全部被拦截api.php.bak.*、__saas/data/control.sqlite、
/_rollback/、/.git/config 全部返回 404;
首页 / 各公司入口 / 文档中心 / API 功能零误伤nginx -t 通过test_cset.js 137 项、test_v146.js 65 项、
test_auto_v147.js 41 项、test_v149.js 103 项
—— 合计 346 项断言全部通过pushToSales 改异步消费 settle_outbox、生产凭据轮换。lots 从单份 JSON 拆成独立物理表按行写入、
单点驱动一次操作生成全链路、整条链路包进一个数据库事务、每家公司只读自己的数据。