更新日志
版本发布逐版本列出变更说明,站点公告按置顶与时间倒序排列,共 93 条记录;升级前请先阅读对应版本的变更说明与升级步骤。
-
v0.4.14
修复:授权域名守卫把"本机/局域网/中文域名"访问误判为违规并锁定
随交付包发布性质:缺陷修复(安全守卫误伤运维访问)。无数据库变更;改动仅 includes/LicenseManager.php。
查看完整变更
性质:缺陷修复(安全守卫误伤运维访问)。无数据库变更;改动仅
includes/LicenseManager.php。 触发场景:在服务器本机或局域网打开站点、或用中文域名访问时,被站点自己的"域名授权守卫"记为违规。一、问题现象与根因
现象:
http://192.168.0.100/(局域网)、http://127.0.0.1/(本机)、http://10123.中国/(中文域名) 都会返回 403 + "停机维护"页面,而授权域名10123.xn--fiqs8s正常。根因(
includes/LicenseManager.php+includes/header.php):// header.php 每个页面都会执行 if (LicenseManager::isLicensed()) { $domainCheck = LicenseManager::checkDomain(); // 与授权域名逐字符比较 if (!$domainCheck['valid']) { $warning = LicenseManager::recordWarning(); // 记一次违规(5 天窗口内累计) if ($warning['shutdown']) { echo renderShutdownPage(); exit; } // ≥8 次 → 停机页 } }三处设计缺陷叠加:
# 缺陷 后果 1 getCurrentDomain()直接返回原始HTTP_HOST,不剥离端口、不做 IDN 归一化127.0.0.1:16500(宝塔反代)、10123.中国(中文形态)永远不等于绑定的 punycode10123.xn--fiqs8s2 本机/内网访问也计入违规 管理员在本机调试、局域网访问,每个页面都记一次违规;累计 8 次后本机/局域网访问被自己的停机页锁死(线上实际计数已达 106) 3 违规域名无白名单机制 自家其它域名(如 10123.vip)也被判违规实测线上违规计数文件:
{"count":106,"domains":["127.0.0.1","10123.vip","127.0.0.1:16500","192.168.0.100","10123.中国","39.96.90.85"]}—— 全部是运维访问地址,没有一个是真正的盗版域名。二、修复内容
改动 说明 新增 LicenseManager::normalizeHost()小写化、剥离端口(含 [::1]:8080)、非 ASCII 主机名用idn_to_ascii()转 punycode新增 LicenseManager::isLocalHost()放行且不计数: localhost/ `*.local.lan .home .internal /::1/127./10./172.16-31./192.168./169.254.*`新增 data/.license_allow白名单每行一个主机名或后缀( .example.com支持),#注释getBoundDomain()/getCurrentDomain()统一走 normalizeHost(),中文域名与 punycode 绑定可正确互相匹配checkDomain()命中本地/白名单 → 直接 valid(不进入违规计数)反盗版语义保留:外部未授权域名仍然被拦截并计数(8 次后对该主机显示停机页)。
三、验证(线上实测)
多 Host 访问(修复前 → 修复后):
Host 修复前 修复后 10123.xn--fiqs8s(授权域名)200 200 ✓ 192.168.0.100(局域网)403 停机维护 200 ✓ 127.0.0.1(本机)403 200 ✓ localhost— 200 ✓ 127.0.0.1:16500(宝塔反代带端口)403 200 ✓ 10123.中国(中文域名形态)403 200 ✓ 守卫语义 8/8 通过(CLI 直测
LicenseManager):[PASS] 10123.xn--fiqs8s 放行(计数 +0) [PASS] 192.168.0.100 放行(计数 +0) [PASS] 127.0.0.1 放行(计数 +0) [PASS] localhost 放行(计数 +0) [PASS] 127.0.0.1:16500 放行(计数 +0) [PASS] 10123.中国 放行(计数 +0) ← IDN 归一化生效 [PASS] evil-domain.example.com 拦截(计数 +1) ← 反盗版语义保留 [PASS] 白名单 10123.vip 放行且不计数历史计数已归零(
data/.license_warnings→count:0),并已备份原文件到D:\BtSoft\backup\qdcms-license-20261003-013150。四、给你的使用建议
- 本机 / 局域网 / 宝塔反代访问站点:现在不会再被记过或锁定 ✓
- 若还有自家其它域名(如
10123.vip)需要正常访问,把它写进data/.license_allow(一行一个)即可; - 该文件与计数文件都在
data/下,权限 0600,不随包分发。
-
v0.4.13
内置二维码解码器 + 动态码生成器:部署即用
随交付包发布性质:新能力(纯 PHP 实现)+ 修复"服务器未提供二维码渲染/解析能力"的降级状态。
查看完整变更
性质:新能力(纯 PHP 实现)+ 修复"服务器未提供二维码渲染/解析能力"的降级状态。 需求:"修复二维码解码器,写一个二维码解码器和动态码生成器入安装包内,实现部署即用"。 无数据库变更、无外部依赖(不需要 qrencode / zbarimg / Python)。
一、问题背景
此前两个能力都依赖外部命令行程序:
能力 原实现 在 Windows/宝塔上的实际结果 生成动态收款码 findQrEncoder()→ 只找qrencode找不到 → 前端「支付码」页显示「服务器未提供二维码渲染能力,请按右侧信息手工支付」 解析上传的收款码图片 detectQrDecoder()→ 只找zbarimg/python zxing/pyzbar找不到 → 后台显示「未检测到二维码解码器」 二、新增模块(包内自带,随包分发)
文件 说明 includes/qr/QrEncoder.php纯 PHP 二维码生成器:数字/字母数字/字节模式,版本 1~40 自适应,纠错 L/M/Q/H,8 种掩码按标准罚分择优;PNG(GD)或 SVG 输出 includes/qr/QrDecoder.php纯 PHP 二维码解码器:GD 读图 → Otsu 二值化 → 定位图形检测 → 仿射采样 → 格式/版本信息 BCH → 去交织 → Reed-Solomon 纠错(GF(256)/Berlekamp-Massey/Chien/Forney)→ 段解析;支持版本 1~10、四档纠错、任意 90° 旋转与缩放 install/qr-selftest.php自检脚本( php install/qr-selftest.php)docs/二维码模块说明.md使用说明、能力边界与常见问题 三、接入方式(系统内自动优先内置)
findQrEncoder()→ 返回'builtin';renderQrPngDataUri()用 GD 现场渲染 PNG(无 GD 自动退化 SVG)detectQrDecoder()→ 返回['type' => 'builtin'];decodeQrImage()用内置解码器解析;内置失败仍会回退外部解码器- 前台「支付码」页与后台「收款码与二维码解析」页无需改动,自动变为可用
四、验证(自检 7/7 全通过)
断言 结果 版本 1~40 × 四档纠错的容量表自洽 160/160 通过 ISO/IEC 18004 标准样例: "01234567"(v1-M)数据码字10 20 0C 56 61 80 EC 11 EC 11 EC 11 EC 11 EC 11逐字节一致格式信息 BCH(M/mask0) 101010000010010与标准一致编码 → 解码往返(版本 1~12、四档纠错、多长度) 18/18 通过 四向旋转(0/90/180/270) 4/4 通过 真实载荷(支付宝 / 微信 / 中文备注) 5/5 通过 缩放/纠错/边距组合扫描 96/96 通过 线上端到端实测(写入线上环境后,用临时探针调用系统自身函数):
findQrEncoder() = builtin detectQrDecoder() = type=builtin renderQrPngDataUri() → data:image/png;base64,iVBORw0K…(真实 PNG) decodeQrImage(生成的二维码) → 与原文完全一致(往返成功) 真实载荷 3 例(支付宝/微信/中文) → 全部解码成功 后台「收款码解析」页显示「已就绪:内置纯 PHP 解码器」;"未检测到解码器"告警消失五、开发过程中发现并修复的实现缺陷(自检驱动,逐条记录)
# 缺陷 影响 1 选版本时把位数当作码字数 "01234567"被编成版本 3 而非版本 12 格式信息 15 位摆放位置写反(两个副本都与标准不符) 解码器读不到合法格式信息 3 解码采样偏移写成 k-3.5(应为k-3.0)整体偏半个模块,采样落在模块边界 4 格式信息副本 2 的读取坐标与标准互换 副本 2 永远校验失败 5 字符计数指示符位宽未随版本组变化(字节模式 v10+ 应为 16 位) 版本 10 及以上数据头畸形,无法解析 6 定位图形纵向复核时,"下方浅色游程"起点用了最后一个深色行 定位图形 0 命中,全图无法解析 7 定位图形聚类窗口过宽(3 个模块)且无条件合并 真定位图形被邻近伪命中"吃掉"(实测 33×33 图真 TL 消失) 8 只按模块大小排序取前 3 个候选 假阳性被当成真定位图形 修复后另加两项鲁棒性措施:每模块 5 点多数表决采样、掩码回退重试(RS 自洽才认),最终缩放/纠错扫描 96/96 通过。
-
v0.4.12
后台:收款码与二维码解析独立成页
随交付包发布性质:后台结构整理(重构)。需求:"将后台 token 充值审核页面里二维码解析项目单独建立一个页面归纳"。
查看完整变更
性质:后台结构整理(重构)。需求:"将后台 token 充值审核页面里二维码解析项目单独建立一个页面归纳"。 无数据库变更、无前台影响、无支付口径变更。
改动前:
admin/token-orders.php(561 行)同时承担「订单审核」与「收款码/二维码解析」两件事, 二维码相关代码(POST 处理、配置读取、解析状态文案、两个上传解析表单、按套餐收款码表格)占了近 40% 篇幅。改动后:
文件 变化 admin/pay-qr.php(新增,352 行)独立页面「收款码与二维码解析」,归集:① 全局收款码内容(支付宝/微信)与动态码模板;② 上传图片自动解析二维码(不落盘);③ 按套餐固定金额收款码(含图片解析);④ 新增「二维码解码能力」面板——检测到解码器则显示路径,未检测到则给出 zLinux/Windows 安装指引,避免管理员反复换图重试 admin/token-orders.php(561 → 299 行)只保留「充值兑换比例」「订单审核(通过/拒绝)」「筛选」「订单列表」;二维码相关代码全部移出;新增一张引导卡片链接到新页;旧表单/旧书签提交 save_pay_qr_config会自动 302 跳转到新页(兼容)admin/header.php「用户运营」分组新增导航项 收款码解析(与 Token 订单/在线支付订单并列),并纳入分组自动展开条件 代码搬移方式:按行范围原样搬移(不重写逻辑),仅把表单
action从token-orders.php改为pay-qr.php; 所有解析函数(decodeQrImage/detectQrDecoder/normalizePayQrInput/parsePaymentQrPayload/payQrPackageSettingKey/getPayQrConfig/getPayQrPackageConfig)本就在includes/token-functions.php, 两页共用,无重复实现。保真核验(脚本逐项断言):新页 12 项要素齐全(两个渠道输入框、模板、两个图片解析入口、套餐输入与图片解析、 保存动作、解析函数调用、状态文案、表单目标);原页 8 项保留(汇率、审核通过/拒绝、兼容跳转、入口链接、筛选、列表) 且 5 项旧内容确认移除 ✓。
线上实测 23 / 23 通过:
[PASS] 新页 HTTP 200 完整渲染(18,316 字符)+ 全部区块/输入项/导航入口 [PASS] 原页 HTTP 200(17,150 字符):保留汇率/筛选/列表,新增入口卡片,旧区块与旧输入框已移除 [PASS] 旧动作提交 → HTTP 302 Location: pay-qr.php(兼容跳转) [PASS] 新页保存动作(合法 CSRF + 当前值)→「收款码设置已保存」 [PASS] 错误 CSRF → 「请求验证失败」且**未写入数据库**(已回查 settings) [PASS] 测试会话已删除;settings 未被改动(pay_qr_alipay / pay_qr_wechat 空、pay_qr_template={pay_url})过程中的一次误操作已修正:验证脚本中一项"错误 CSRF"用例误带了有效 token,把
WRONG_CSRF_TEST写入了pay_qr_alipay;发现后立即还原为空值并用独立用例(无效 token)重测,确认拒绝且不落库。 -
v0.4.11
充值系统审计与加固:支付表缺失兜底 + 账目/并发实测
随交付包发布性质:安全审计 + 缺陷修复 + 体验优化。需求:"优化充值系统,检测是否有bug"。
查看完整变更
性质:安全审计 + 缺陷修复 + 体验优化。需求:"优化充值系统,检测是否有bug"。 无既有表结构变更(仅新增 4 张支付表的自动创建能力);不改动任何支付金额口径。
一、审计与实测结论(有证据)
检查项 方法 结果 回调验签 伪造支付宝回调(无签名)POST 响应 failure✓ 被拒回调验签 伪造微信 APIv3 回调(乱报文) HTTP 500 + 失败应答 ✓ 被拒 回执上报 无 HMAC 伪造 payment-receipt.php被拒(需 IP 白名单 + 设备密钥 + 开关)✓ 入账幂等 同一订单连续两次 Ledger::credit()第一次入账、第二次 duplicate=true,仅 1 条流水 ✓并发重复入账 两个并行请求对同一订单入账 只入账一次( token_order_id相同,余额只 +10)✓金额不符 回调 999 分 vs 订单 100 分 ok=false+PAY_1012,未入账、转人工复核 ✓并发扣费 同内容 onceOnly双请求并行只扣一次,第二条返回 duplicate✓账目一致性 逐用户比对 余额 vs 已入账 − 已消费 3 个用户全部零漂移 ✓ 权限 admin/token-orders.php、admin/payment-orders.php经 admin/header.php → requireAdmin()+ CSRF ✓越权 payment-status.php、payment-receipt.phpGET、recharge-order.php全部 user_id = ?归属校验,遗留页只读 ✓客户端金额 payment-create.php金额/Token 一律服务端按套餐推导 + CSRF ✓ 结论:资金主链路(验签 / 幂等 / 并发 / 金额 / 账目)实测全部通过,未发现可被利用的入账漏洞。
二、发现并修复的真实缺陷
# 问题 影响 修复 1 🔴 支付数据表从未创建: includes/payment/schema.sql(4 张表)随包分发但没有任何入口执行;线上payment_orders等表缺失管理员一旦填好渠道密钥,充值页会显示"支付宝/微信",用户点下单直接 500「系统繁忙」(当前线上尚未配置密钥,故未触发) ① PaymentStore::schemaReady()/ensureSchema();②createForPackage()下单前自动补建,补建失败则返回明确错误码PAY_1021(不再 500);③availableChannels()表未就绪时不下发渠道(充值页不会显示会失败的入口);④ 后台「支付设置」新增一键初始化支付表按钮(CSRF 保护)+ 缺失告警;⑤ 全新安装install/auto-install.php自动执行 schema.sql2 🟠 addTokens()无条件返回 true用户行不存在时 UPDATE 影响 0 行,上层仍会把订单标为"已入账",而钱没加到任何人账上 改为按影响行数 + 用户存在性判定并记日志 3 🟠 支付表缺失没有可检测的判据 自检/后台无法提前发现 提供 PaymentStore::schemaReady(),后台实时显示状态三、实测验证(线上)
场景 结果 正常环境 schemaReady()true;4 张表齐备(每张 0 行)删掉一张空表 + schema.sql不可读 → 调用下单返回 {"code":"PAY_1021","error":"支付数据表未创建:请在后台「支付设置 → 初始化支付表」一键创建…"},无未捕获异常、不再 500 ✓删掉一张空表 + schema.sql可读 → 调用下单ensureSchema()自动重建成功(ready_after_ensure=true),随后正常进入渠道判断 ✓表数据 删表前 payment_refunds为 0 行(用空表做实验,无数据风险);实验后 4 张表全部重建 ✓四、其他优化
- 充值相关后台页面在支付表缺失时给出可点击的一键修复,不再只提示"请手动执行 SQL"
- 错误码体系新增
PAY_1021,与既有PAY_1001~PAY_1020保持一致 - 本次未改动金额口径、未调整任何订单/Tokens 数据
五、交付物
- 新增:
PaymentStore::schemaReady()/ensureSchema()、PaymentGateway::E_SCHEMA_MISSING - 修改:
PaymentService::createForPackage()、PaymentConfig::availableChannels()、admin/payment-settings.php、install/auto-install.php、includes/token-functions.php::addTokens() - 线上 DB:已执行
includes/payment/schema.sql(新增 4 张表,未动既有表)
-
v0.4.10
优化:聊天悬浮图标
随交付包发布性质:体验/性能/无障碍优化。无表结构变更、无依赖变更、无服务端逻辑变更(仅 tab-card.js + agent.css)。
查看完整变更
性质:体验/性能/无障碍优化。无表结构变更、无依赖变更、无服务端逻辑变更(仅
tab-card.js+agent.css)。 说明:本次未改 im-plugin 资源,故 IM_BUILD 不递增;tab-card.js、agent.css由 PHP 的assetVersion()(filemtime)自动破缓存,刷新即生效。审计发现 & 对应优化:
# 问题(代码位置实测) 优化 1 图标加载原图: renderTabIcon()直接<img src="basePath+iconUrl">,而悬浮键只有 48px、菜单项 26px,管理员上传的图标可能是 1024px 原图改走 XDImgUrl()→img-proxy.php(按需缩放 + WebP 缓存 + 30 天强缓存),代理不可用时自动回退原图;并补decoding="async"。实测生成:/img-proxy.php?src=uploads%2Fagent-icons%2Fchat.png&w=48&t=icon2 iPhone 安全区被压: .tab-drawer-group固定right:12px,底部按钮组固定bottom值,未考虑刘海/手势条加 env(safe-area-inset-right/bottom)(带0px兜底,老内核忽略该声明)3 键盘用户看不到焦点: .tab-trigger-btn { outline: none }且无:focus-visible补绿色焦点环(按钮、三键组、菜单项),深色模式换浅绿 4 键盘无法唤出聊天:只绑了 pointerdown/up,button 的 Enter/Space(触发click)无人处理补 click处理并默认进入聊天窗口;用lastPointerAt时间戳避开指针路径的重复触发(指针点击仍由onPressEnd处理)5 无障碍属性缺失:仅 title,无aria-label,菜单展开态不播报触发键补 aria-label/aria-haspopup/aria-expanded(show/hideMenu 内同步);菜单role="menu"+aria-label,菜单项role="menuitem"6 触屏"粘住"放大态: :hover缩放效果在触屏上会一直保留@media (hover: none)内中和 hover(transform/阴影/菜单底色),触屏只保留:active反馈7 移动端触控目标偏小:按钮 42px < 44px 推荐值,菜单项高度不足 移动端按钮 46px、菜单项 min-height 44px、菜单图标 26px、角标 11px 8 深色模式刺眼:菜单固定白底、角标白色描边 prefers-color-scheme: dark下菜单改深色底、文字#e8eaed、角标描边随底色9 不尊重"减弱动效" prefers-reduced-motion: reduce关闭过渡/动画与缩放10 角标数字跳动/挤压 角标加 font-variant-numeric: tabular-nums+letter-spacing11 拖拽手感 按钮 will-change: transform;.dragging时放大 1.06 且cursor: grabbing验证(新增
qdcms-release/test-floating-icon.js,Node + DOM 桩)—— 20 / 20 通过:[PASS] tab-card.js 初始化无异常 [PASS] 触发键图标走 img-proxy(/img-proxy.php?src=…&w=48&t=icon)+ decoding=async [PASS] aria-label=消息中心 / aria-haspopup=true / aria-expanded 初始 false [PASS] 菜单 role=menu、菜单项 role=menuitem [PASS] 键盘 click 可打开聊天(open 调用 ["chat"]) [PASS] 保存的拖拽位置在初始化时即恢复(left=520px top=240px right=auto,无跳位闪烁) [PASS] CSS 8 项静态断言:安全区 / :focus-visible / @media(hover:none) / ≥44px 触控 / prefers-reduced-motion / prefers-color-scheme:dark / tabular-nums / will-change过程中发现并修正了测试桩两处失真(未把
window.XMLHttpRequest镜像为全局导致初始化中断、 键盘用例未跳过 800ms 指针抑制窗口),保证断言确实在验证产品行为。既有回归同时全绿:
test-media-cache20/20、test-chat-upload23/23、test-chat-selfpush8/8、test-cn-browser32/32。 -
v0.4.09
新功能:聊天媒体缓存 —— 网络波动/刷新后视频不被清零重下
随交付包发布性质:功能增强。需求:"给聊天客户端增加缓存功能,预防网络波动时缓存视频后观看结果被刷新清空"。
查看完整变更
性质:功能增强。需求:"给聊天客户端增加缓存功能,预防网络波动时缓存视频后观看结果被刷新清空"。 无表结构变更、无依赖变更。
先定位"被刷新清空"的真实机制(两条都在前端):
# 机制 后果 1 messages.php的 4 秒轮询在收到新消息时执行 整屏重渲染(loadChatHistoryWithId→body.innerHTML = ...)正在播放的 <video>被整体销毁,已缓冲内容与播放进度全部丢失 → 重新从头缓冲2 插件与页面都没有任何媒体缓存/进度记忆 网络一抖动、或列表一刷新,就只能重新下载;长视频尤其明显 新增能力(
assets/im-plugin/core/media-cache.js,纯 ES5,10KB,含降级):层 能力 ① 字节缓存 用 Cache API 把看过的聊天视频存到本地(LRU 上限默认 120MB / 最多 60 条,超出按最久未用淘汰);命中后 <video>直接切到blob:源 —— 断网、刷新后仍可秒播,不必重下。仅在首次播放时后台抓取,不抢观看带宽② 进度记忆 播放位置按消息 ID 存本地(1 秒节流写入、播完自动清除),重新渲染或刷新页面后自动续播到原位置 ③ 播放器保活 与"增量追加"配合:页面轮询改为只追加新消息(不再整屏重渲染),正在播放的播放器节点不再被销毁; XDChatVideo.scan()幂等挂载,任何渲染路径后调用一次即可④ 优雅降级 Cache API 不可用(IE11 / 旧 X5 内核)时自动降级为"仅进度记忆",所有接口 no-op 不抛错(与 v0.4.07 的兼容层策略一致) 同时改动:
messages.php:轮询由整屏重渲染改为增量追加(含去重与兜底:节点对不上时才整屏重建);行包装新增data-msg-row;视频播放器新增data-msg-id/data-src;渲染后统一XDChatVideo.scan()(顺手修掉上一版遗留的一行重复 title 赋值)assets/im-plugin/modules/chat.js:视频气泡加data-msg-id/data-src;_loadMessages、_pollNewMessages(增量)、_uploadMedia(乐观气泡替换)三处渲染后均调用scan()includes/footer.php:media-cache.js紧随compat.js加载(早于img-lazy.js),所有页面可用;并加入插件jsFiles与资源预热清单chat-video-stream.php:新增 ETag + Last-Modified + 304(与前端 Cache API 互补,浏览器自身缓存更易命中)IM_BUILD20261002-8→20261002-9
回归测试(Node + DOM/缓存桩)
qdcms-release/test-media-cache.js—— 20 / 20 通过:[PASS] Cache API 可用时 supported=true;store() 写入索引并调用 fetch [PASS] savePos/getPos 往返、clearPos [PASS] scan 找到播放器且幂等(data-xd-attached=1,不重复绑定) [PASS] 重新渲染后恢复播放位置(33.3s)/播放中记录进度(55.5s)/播放结束清除 [PASS] 首次播放触发后台缓存 [PASS] LRU:超上限后淘汰至限制内(entries=2 bytes=600 limit=700)并真的从 Cache 删除 [PASS] 无 Cache API 时:加载不抛错、supported=false、接口安全降级、仍可记忆进度既有回归同时全绿:
test-chat-upload23/23、test-chat-selfpush8/8、test-cn-browser32/32。 -
v0.4.08
修复:发送视频看不到进度 / 上传完成后才显示消息
随交付包发布性质:体验缺陷修复(对应 v0.4.03 引入的 XHR 上传做的不彻底)。无表结构变更、无依赖变更、无服务端改动。
查看完整变更
性质:体验缺陷修复(对应 v0.4.03 引入的 XHR 上传做的不彻底)。无表结构变更、无依赖变更、无服务端改动。
两个根因(都在聊天插件
assets/im-plugin/modules/chat.js):# 现象 根因 1 发送方看不到进度 v0.4.03 的进度是用 showToast显示的,而每个 toast 2 秒后自动消失、且每次进度更新都会新建一个 → 上传超过 2 秒后进度提示就没了(messages.php页面同样只有按钮文字在变)2 上传完成后消息才"推回"到发送方窗口显示 媒体消息没有乐观气泡:上传期间聊天窗口里空无一物,成功后才通过 _loadMessages()从服务端整屏拉取渲染出来 —— 这就是"系统又推回给发送方显示"的观感修复:
# 改动 1 重构为 _uploadMedia(file):上传开始立即插入乐观气泡(图片用URL.createObjectURL本地预览、视频用首帧预览),气泡右下角带百分比 + 进度条,通过xhr.upload.onprogress就地更新(不再用会消失的 toast 刷进度)2 上传成功后用服务端返回的 data.message原地替换该气泡(outerHTML)并推进_lastMsgId—— 不再等服务端"推回";同时兜底:若视图已被重建(用户返回列表)则改走_pendingRefreshId+ 刷新联系人列表3 上传失败:气泡上标红显示失败原因,并给 重试(复用同一个 File 对象,不必重新选文件)与 删除 两个按钮; onerror/onabort同样落到气泡上4 messages.php页面同步改造:发送媒体时在#chatBody插入乐观气泡(预览 + 进度条 + 失败原因),成功后移除气泡由loadChatHistoryAjax()渲染真实消息5 IM_BUILD20261002-7→20261002-8回归测试(Node + DOM 桩)
qdcms-release/test-chat-upload.js—— 23 / 23 通过,新增覆盖:[PASS] 上传开始后立即出现乐观气泡(data-id=temp_up_…) [PASS] 气泡内含进度文案(初始 0% + 进度条元素) [PASS] 进度就地更新为 50%(气泡文字) [PASS] 进度条宽度同步 50% [PASS] 不再用 toast 刷进度(含"上传中"的 toast = 0 条) [PASS] 完成后清空 _upload 并解绑离开拦截 / 提示"已发送" / 无卡死标志 [PASS] 上传中返回列表:刷新联系人列表 + 记录待刷新会话(不误调 _loadMessages) [PASS] 仍在该会话:立即刷新消息、不刷新列表 [PASS] 失败展示服务端原因、解除上传态既有回归同时全绿:
test-chat-selfpush8/8、test-cn-browser32/32。 -
v0.4.07
修复:聊天插件不兼容国内浏览器
随交付包发布性质:兼容性修复。目标浏览器:IE11 / 360、QQ、搜狗、2345 等"兼容模式"、微信与 QQ 内置浏览器旧 X5 内核、UC、老版 Chromium(<53)。
查看完整变更
性质:兼容性修复。目标浏览器:IE11 / 360、QQ、搜狗、2345 等"兼容模式"、微信与 QQ 内置浏览器旧 X5 内核、UC、老版 Chromium(<53)。 无表结构变更、无依赖变更。
实测审计结果(先查证据再改):
项目 实测 判断 ES6+ 语法 逐行扫描(排除字符串里的中文省略号"…"与 script.async属性)→ 插件全部为纯 ES5 写法✅ 无需改写 attachShadowim-sdk.js直接调用且无兜底🔴 IE11 / 旧 X5 内核没有 Shadow DOM → 抛 TypeError → 面板直接打不开(本次最严重问题) 缺失 API fetch、Promise、Object.assign、Element.remove/closest/matches、String.endsWith/padStart、Array.from、requestAnimationFrame、IntersectionObserver;另有localStorage在 IE11 隐私模式下写入抛错🔴/🟠 插件与懒加载图片会失效 CSS 146 处 var(--im-*)全部没有字面量兜底;另有 flexgap×16、backdrop-filter×2、position:sticky×1🔴 IE11 不支持 CSS 变量 → 面板变成无样式裸 DOM 修复内容:
# 改动 文件 1 新增低版本浏览器兼容层(纯 ES5,14KB):Promise(含 then/catch/all 语义)、fetch(XHR 实现,支持 json/text/ok/status)、Object.assign、Array.from/includes/find/findIndex、String.trim/includes/startsWith/endsWith/padStart、Element.matches/closest/remove、NodeList.forEach、CustomEvent、requestAnimationFrame、IntersectionObserver(滚动探测版,保障图片懒加载)、 XDStorage(localStorage 不可写时回落内存);并提供window.XDCompat.summary()诊断assets/im-plugin/core/compat.js(新增)2 兼容层在所有页面最早期加载( footer.php中位于tab-card.js之前),并加入插件jsFiles首位与资源预热清单includes/footer.php、im-sdk.js、tab-card.js3 Shadow DOM 兜底渲染: attachShadow不可用时降级为"普通容器 + 样式注入<head>",宿主补im-shadow-fallback类与等价定位样式(不用 IE11 不支持的inset),CSS<link>改注入 head;可用时仍走 Shadow DOM,现代浏览器路径零变化assets/im-plugin/core/im-sdk.js4 CSS 字面量兜底:为全部 126 条含 var(--im-*)的声明前插入字面量等价声明(color:#07c160; color:var(--im-primary);)→ IE11 用字面量、现代浏览器仍走变量;覆盖 126/126(100%)im-container.css、im-modules.css、im-reset.css、im-dark.css5 localStorage调用点改用XDStorage(隐私模式不再抛错)im-sdk.js、tab-card.js6 IM_BUILD20261002-6→20261002-7assets/js/tab-card.js模拟旧内核回归测试(Node + 精简 DOM 桩,刻意移除 attachShadow/Promise/fetch/remove/closest/rAF/IO 并让 localStorage 抛错)——
qdcms-release/test-cn-browser.js:32 / 32 通过,关键断言:[PASS] attachShadow / Promise / fetch / Element.remove / rAF / IntersectionObserver 缺失(模拟生效) [PASS] compat.js 为纯 ES5 语法 [PASS] 已补齐:Promise、fetch、Object.assign、Element.remove/closest、String.endsWith/padStart、Array.from、rAF、IntersectionObserver [PASS] Promise.then / catch / all 语义正确 [PASS] fetch 返回 ok/status/json/text 且 json() 正常 [PASS] XDStorage 在 localStorage 写入抛错时仍可读回 [PASS] im-sdk.js 在旧内核环境执行无异常、window.IMPlugin 正常导出、IMPlugin.open() 无异常 [PASS] 降级渲染生效:<body> 出现 im-root im-shadow-fallback 宿主 [PASS] <head> 注入降级定位样式 1 个 + 样式表 <link> 5 个、容器内挂 im-shadow-scope 根节点 [PASS] attachShadow 可用时仍走 Shadow DOM 路径(现代浏览器不受影响)兼容层在测试中还暴露并修掉了自身两个缺陷:
XDStorage.get在"可读但不可写"的场景没有回落内存副本;fetchpolyfill 直接引用裸XMLHttpRequest(旧内核下应取window.XMLHttpRequest)。已知降级项(不影响可用性,仅外观):flex
gap(16 处)与backdrop-filter、position:sticky在旧内核下不生效 → 元素间距略紧凑、无毛玻璃与吸顶效果,布局与功能正常。 -
v0.4.06
修复:消息发出后又被系统推回给发送方
随交付包发布性质:缺陷修复。用户反馈"发消息出去后系统又推送给发送方"。
查看完整变更
性质:缺陷修复。用户反馈"发消息出去后系统又推送给发送方"。 无表结构变更、无依赖变更、无服务端接口变更(纯前端判定逻辑)。
根因链(三步):
- 增量接口
getChatMessagesSince()返回的是整个会话(WHERE (sender=我 AND receiver=对方) OR (sender=对方 AND receiver=我) AND id > since) —— 自己刚发出的那条也在返回结果里(这是设计如此,用于已读回执/多端同步)。 - 插件发送时用的是乐观气泡(
data-id="temp_<时间戳>"),发送成功后既没有替换成服务端真实记录,也没有推进_lastMsgId。 - 4 秒后的增量轮询拿到自己那条消息:按真实 id 去重匹配不上临时气泡 → 重复渲染一遍; 并且
if (document.hidden) self._notifyHiddenNewMessages(msgs.length)把自己发的也算作新消息 → 弹出"新消息"提醒。messages.php页面同理:会重渲染并闪出 "【新消息】" 标题。
修复:
位置 改动 assets/im-plugin/modules/chat.js_sendMessage()发送成功后用响应里的 data.message替换乐观气泡,并推进_lastMsgIdassets/im-plugin/modules/chat.js_pollNewMessages()逐条判断: sender_id === 我的消息只用于替换残留气泡,不计入新消息(不渲染、不提醒);_notifyHiddenNewMessages()只统计对方消息数messages.php页面 4 秒轮询统计时排除自己发的消息; incoming === 0直接返回 → 不再重渲染、不再闪"【新消息】"缓存 IM_BUILD20261002-5→20261002-6回归测试(Node + DOM 桩,跑真实模块代码) ——
qdcms-release/test-chat-selfpush.js:用例 新版 旧版(对照) A 增量返回自己发的消息 → 不渲染 PASS(渲染列表为空) FAIL(渲染了 101) A 同场景 → 不弹提醒 PASS(notify=0) FAIL(notify=1) ← 即"推送给发送方" B 增量返回对方消息 → 正常渲染 + 提醒 PASS(渲染 102、notify=1 数量=1) PASS / PASS C 同一批含自己+对方 → 只对对方计数 PASS(只渲染 104、提醒=1) FAIL(渲染 103+104、提醒=2) 合计 8/8 通过 4/8(4 项失败,精确复现用户反馈) - 增量接口
-
v0.4.05
修复:聊天视频播放容易失败
随交付包发布性质:缺陷修复。用户反馈"播放聊天视频容易失败"。实测线上 10 个视频后发现 4 个更底层的问题。
查看完整变更
性质:缺陷修复。用户反馈"播放聊天视频容易失败"。实测线上 10 个视频后发现 4 个更底层的问题。 无表结构变更、无依赖变更。
# 实测根因 修复 🔴 1 moov 在文件末尾(非 faststart):线上 10 个视频里 4 个如此(含用户当时新发的)。浏览器必须把整个文件下完才能起播,弱网/大文件即"播放失败",拖动更是直接卡死 上传后自动重封装为 faststart( ffmpeg -c copy -movflags +faststart,无损失);已对线上 4 个存量文件回填(备份在D:\BtSoft\backup\qdcms-video-normalize-*),现已 10/10 全部 faststart🔴 2 规范化代码在 Windows 上从未生效: compressVideoAsync()依赖nohup sh / wc -c / [ -s ](POSIX only);Windows 分支虽存在,但只在"产物更小"时才替换,而 faststart 重封装体积可能略增 → 直接被丢弃新增跨平台 normalizeVideoForWeb()(同步执行、失败保留原文件):① 已达标 → 不动;② HEVC/H.265/VC-1/MPEG-4 等浏览器不支持的编码 → 转 H.264/AAC + faststart(限 80MB/180s,超出仅重封装);③ 仅非 faststart → 无损重封装。compressVideoAsync()现在先保证 faststart,再做后台压缩🔴 3 播放走的 PHP 端点有三个硬伤:① Cache-Control: no-store→ 拖动/断流都要重下;② 每 8KBflush(),且max_execution_time=300s会腰斩长视频;③ Referer 全等校验 → 换域名/带 www/IDN 与 punycode 混用、浏览器不发 Referer(隐私设置)、企业代理剥离 Referer 时一律 403优先 X-Accel-Redirect 交给 nginx(sendfile 零拷贝、Range 由 nginx 处理、PHP worker 立即释放); Cache-Control: private, max-age=3600;set_time_limit(0);缓冲 8KB→64KB;Referer 规则放宽(空 Referer 放行 + 支持 www/裸域/IDN/punycode/SITE_URL 别名,外站仍 403 防盗链)。无 nginx 时自动退回 PHP 流式输出🟠 4 包内缺少 Windows ffmpeg 适配:线上 fn-upload.php比交付包多 184 行(detectFfmpegPathWindows():FFMPEG_PATH 环境变量 /config/ffmpeg.php/ 固定路径 / winget、scoop 目录 /where.exe),即 Windows 客户装包后封面与压缩功能全部失效以线上版本为基线合并回包内(线上是超集,diff 确认"包内独有 0 行"),Windows 客户开箱即用 🟠 5 播放器体验:无封面时黑屏、iOS 不内联播放、失败没有任何提示 messages.php与插件chat.js均补:poster 兜底(video-default-thumb.jpg)、playsinline、播放失败时给出下载观看引导实测验证(线上):
- 生成"moov 在尾部"的 MP4(77.7KB)走真实上传接口 → 落盘文件 faststart=是 ✓(自动规范化在真实 php-cgi 运行时生效)
chat-video-stream.php:正常 Referer → 200(Accept-Ranges: bytes);Range0-1023→ 206/1024B、500000-500999→ 206/1000B(nginx 直发);无 Referer → 200(历史为 403);外站 Referer → 403(防盗链保留)- 全部 10 个视频终检:非 faststart = 0
第 6 / 10 页 · 每页 10 条 · 共 93 条
升级前请确认
备份
备份站点目录与数据库,尤其是
uploads/与config/。阅读说明
在
docs/版本升级说明.md中找到对应版本,确认是否需要执行数据库迁移。执行升级
解压覆盖站点目录 → 执行 migration → 后台「系统 → 在线更新」复核版本状态。
复核功能
检查前台首页、播放、支付与后台工作面板等关键入口是否正常。
相关入口
- 文档中心查看随包技术文档(含版本升级说明、打包说明、安装配置说明)
- 插件市场查看插件版本与兼容性
- 授权续验升级后重新查询授权状态,确认未因换机失效
- 遇到问题在线留言,附上版本号与报错信息