AnQiCMS v3.8.1 发布:商城并入主干、飞书钉钉接管后台、四个支付回调重写

📅 2026-11-11 👁️ 2

这一版从 3.6.7 到 3.8.1 跨了三周,922 次提交,主线 116 条,落到更新日志里是 51 项。功能不少,但说白了,一半的力气花在同一件事上:把那些”接口回成功、实际上什么都没做”的地方找出来。

商城并进主干了

商城版此前是条独立分支,从 v3.5.8 分出去的。这次以 v3.6.7 为底座做整体移植,处理了 151 个冲突文件。

新增 24 个数据模型:SKU、购物车、愿望清单、优惠券、营销活动、批发规则、物流运费、询盘订单、商品同步、积分、汇率等。订单结算补上优惠券、运费、会员价、活动价、分销、发票和物流追踪,另外配了 15 套商城邮件模板和一套商城模板。

主干这段时间攒的东西一样没丢:AI 意图内核、Partial 语义、SQL 注入加固、支付账户体系都在。

内容站不受影响。shop_mode 默认是 0,商城前台 38 条路由在普通模式下统一返回 404,后台数据照样能管,只是菜单不显示。要开商城,去系统设置里切一下。

后台能接飞书、钉钉了

不是”有新留言给你发条通知”那种接入。管理员在飞书私聊,或者在授权的群里,直接跟 AI 对话干活:改内容、查订单、调设置。网站有新留言、新订单,也能按规则推到指定群或者管理员私聊。

两个平台都走长连接,飞书用 oapi-sdk,钉钉用 Stream 模式。不需要公网回调地址,也不用开放入站端口。内网部署的站点能不能用,基本就看这一条。

几个实现上的取舍:

· 收到消息先回一个表情。飞书贴 OnIt,钉钉发表情回复。等模型和工具跑完那段静默太久,先给个”我收到了”。

· 回复按 Markdown 渲染,钉钉的标题会跳过代码围栏再剥标记。

· 组织标识改成调平台接口自动取。那个值在平台后台根本没有展示入口,要求站长手填等于把接入门槛设在拿不到的地方。

权限只留一套。渠道能干什么,完全由站点的意图曝光和接口开放范围决定,渠道内部没有第二套授权,也没有审批状态机。第一版是带审批的,后来去掉了——同一件事在后台配一次就够了,两套规则只会互相打架。

一份源码,多个白标包

品牌收敛到一处:config/oem/{id}/brand.json,用 go:embed 编进二进制,构建期靠 -ldflags 注入。同一个仓库能出多个互不影响的安装包,不用给每个客户维护一个 fork。

品牌不从远端拉,这是刻意的。内网部署、或者官网正好不可达的时候,”拉不到就退回默认”等于把客户的付费后台做成竞品广告位。

标识拼错了会怎样?回落到中性品牌,展示名就是那个拼错的标识,绝不回落到官方品牌。整套改造里唯一”错了会很难看”的地方就在这儿。

验收也不能靠 grep 源码。品牌替换发生在运行出口,源文件里保留官方文案本来就是设计的一部分。所以加了个 make brand-check:抓运行中站点的真实响应,看首页、后台外壳、robots.txt、sitemap.xml,双向判定——不该出现的别出现,该出现的别缺。

· · ·

四个支付回调,原来全是坏的

这句不是修辞。改之前,微信、PayPal、Stripe 的异步回调全部静默失效,支付宝会把没付款的订单标成已付。

微信 验签函数的第一个参数被当成 appId 传了,实际要的是商户 apiKey;验签算法还写死 MD5,而通知报文不带 sign_type,下单用的又是 HMAC-SHA256。每一笔通知都验不过。

Stripe 拿 evt_ 开头的事件 ID 去查 payment_id,必然查不到,然后直接 return。Iris 默认回 200 空响应,Stripe 认为投递成功,不再重试。

PayPal 处理函数要求带 ?token= 参数。那是同步 return_url 上才有的,而注册的 webhook 地址不带参数——回调全被挡在门口。

支付宝 验签没问题,但完全不校验 trade_status。WAIT_BUYER_PAY 状态也会把订单标成已支付。

四条都按根因修了。顺手给记账加了道闸门:通知重试和模板轮询会并发进来,用 paid_time = 0 的条件更新做闸门,只有一条能真正落账。不加这个,财务流水会重复记。

另外新接了两个渠道。虎皮椒:签名按官方 V3 文档,回调要验签名、appid、status=OD、金额四项,退款只支持整单。NOWPayments:加密货币,发票模式,IPN 用 HMAC-SHA512 验签,只认 finished、confirmed、sending 三种状态——它没有退款 API,所以退款分支直接报错,而不是把订单标成已退款、实际一分没退。

几处安全修补

附件上传改白名单,默认拒绝。导火索是 public/uploads 里出现了 shell.php5 和 s2.phtml。原来的逻辑只拦 .php 和 .jsp,filetype 认不出的内容还会回落到用户文件名。水印和标题图的后缀也不再取文件名,改由 image.Decode 的结果决定。模板包导入顺手补了 Zip Slip。

找回密码的凭据换算法。原来是 Md5(email + password) 派生,空密码账号(Google、公众号、验证码注册)会退化成 Md5(email)——知道邮箱就能改密登录。

邮箱探测加限流。注册、GET/POST verify/email 三个端点都能探测某个邮箱是否已注册,原先各自没限流,轮换着用就绕过去了。现在共用一个预算:20 次 / 10 分钟 / IP。

/aigenerate 整组没有鉴权。7 个端点不带 token 就能读 AI 配置、触发批量生成。补上中间件后统一回 1001。

那些”点了没反应”的 bug

锚文本批量更新,3557 篇文章,失败 3557 篇,287 毫秒”跑完”。原因藏得比较深:CurrentSite 返回的是带请求 ctx 的副本,go currentSite.ReplaceAnchor(nil) 把它带进 goroutine,handler 一返回 ctx 立刻被 cancel,之后每篇都 context canceled,错误又被替换函数吞掉。加了 Website.Async() 摘掉请求 ctx,0 失败,3.065 秒跑完。

优惠券批量发码,循环里完全不看插入的错误。几十张全失败也照样返回成功,后台刷新列表是空的。改成逐张检查,整批发码走事务,要么全落库要么一张不留。

写端点更普遍:51 个在请求体为空时回”已保存”,24 个在 id 不存在时回成功。抽了个统一的入参护栏收口,空格载荷、缺目标、目标不存在一律拒绝并说明原因。

升级流程里的那个更阴。主程序被 rename 成 xxx.old 后进程还跑在旧 inode 上,Linux 会把 /proc/self/exe 也改成 xxx.old。升级完没重启又点一次升级,就把 xxx.old 备份成 xxx.old.old,真正的 xxx 一次都没换,界面提示成功。

还有一处判断:代码里有 5 个地方用 ModuleId == 2 认定”这是产品”。模块 id 是自增的,删了模型重建、或者迁移过站点,id 未必是 2。商品页照样能打开,价格区块、SKU 筛选、Product 结构化数据一起消失,没有任何报错。统计里的 module_id = 2 更直接,让”上新”图表恒为 0。现在这些地方都读 modules.kind。

· · ·

升级前,先看这四件事

  1. 数据库会删列,先备份

archives 和 archive_drafts 会删掉 29 个历史列,不可回滚。商品属性已经迁到 archive_products 侧表,自己写过插件直查这些列的必须一起改——WHERE product_type = ? 这类条件现在是 LEFT JOIN + IFNULL,不然会把”没有侧表行的普通商品”漏掉。

  1. 投稿权限默认是空的

用户组的”可投稿栏目”升级后为空,等于不能投稿。没有”空就是全部”的兜底,父子栏目也不继承。升级完去用户组里勾一遍,不然前台投稿会直接报权限错误。

  1. 附件后缀改白名单

.psd、.ai、.apk 这些以前能传的现在会被拒。有需要就补表,判定逻辑只有一个入口,不用改多处。

  1. 商城默认是关的

要用得在系统设置里把 shop_mode 切到 1 并保存。

完整的 51 条变更在仓库的 CHANGELOG.md 里,每条都写了为什么改、改之前是什么样。这一版动的地方多,升级前把备份做完再动手。

如果升级过程中遇到问题,把版本号、日志片段和复现步骤发到仓库 Issues,我们会跟到底。

相关文章

表单模块的模板替换为什么能变成代码执行入口

表单模块把提交内容交给模板渲染时,如果没有把可替换标记限制在安全来源内,字段值就可能被当成指令执行,形成代码执行入口。防线分两段:输入侧用验证码拦住自动化提交与可疑内容,渲染侧只允许白名单标记。外购模块要进版本清单,公告一出就能定位到自己的站点。

2026-11-01

内容 API 的关系过滤没做净化,接口会多返回哪些数据

内容接口把关系过滤条件直接拼进查询而不做净化时,调用方能用过滤语法读到本不该公开的关联字段,比如作者、订单、内部标识这类跨表数据。自查要覆盖参数白名单、关联深度与可读字段清单,并在接口文档里写清每个端点返回什么。鉴权与最小权限决定暴露面,升级前先跑一遍回归用例。

2026-11-01

把页面做成 HTML 缓存,为什么会变成可执行代码的入口

把前台页面做成 HTML 静态缓存能提速,但带参数的内容若被原样反射进缓存文件,而缓存产物又交给了脚本解析引擎,恶意代码就能在缓存里被执行。边界有两条:写入时剥离可执行标签,解析时不把缓存目录当脚本目录。修复之后还要清掉历史缓存,再核对伪静态规则与页面结构。

2026-11-01

改了域名持有人邮箱之后两个月内为什么不能转注册商

改动域名持有人的名称、机构或邮箱会触发一段转移锁,期内不能把域名转到别家注册商,这与域名是否到期无关。做建站或换主机的排期时要倒推这段周期:先确认持有人信息稳定,再申请转移码,最后才动解析。集中管理多个站点的域名时,要先查有没有刚改过记录的域名。

2026-11-01

静态站点托管按人头收席位费,五人团队一年要付多少

静态站点托管的免费档免掉的是部署与带宽,一到团队协作就换成按人头计的席位费,再叠加构建分钟与流量超额。五人团队的年度账单要把席位、超额、构建三项分开算。自托管的内容管理按资源计费而不按人头,多站点共用一套后台还能摊薄单位成本,内存占用也低约八成。

2026-11-01

关掉生成式 AI 特性的开关之后,还收得到 AI 引荐流量吗

关掉生成式 AI 特性的开关与关掉自然搜索是两套账:前者影响那部分特性带来的展现与流量,后者影响常规收录与引荐。判断前先看两个日期,报告数据起始日早于开关生效日,观察窗口必须在生效之后。引荐流量要按代理来源分组核对,改动保留可回退记录。

2026-11-01

生成式平台的流量份额一年里重排,外贸站该盯哪几个入口

生成式 AI 平台之间的流量份额在一年内明显重排,把某一个入口当作全部来源已经不可靠。外贸站应当把每个入口单独记账,比较带引荐会话的转化差异,并让多语言站点与多站点各自对应落地页。季度回看一次口径,避免用上一季度的份额结构做这一季度的判断。

2026-11-01

先看抓取日志再决定拦谁,代理到底在访问哪些路径

决定拦截哪些 AI 代理之前,先用服务器抓取日志把访问量按代理标识与访问路径分组统计。多数站点放置的 llms.txt 几乎没人来读,真正来读的几家也集中在少数路径上。看清真实访问分布再写 robots 规则,比照抄别人的拦截清单更省带宽,改动也更容易回退和复核。

2026-11-01