Compare commits

...
4 Commits
Author SHA1 Message Date
kate522 ca9dc7794f refactor(pay): 流水号扩至 22 字符,移除资金流水归档方案与删单守卫
四件事合在一笔,因为它们落在同一批文件里——OrderRepositoryCore 与流水契约测试
同时承载号码规则与订单清理逻辑,硬拆会产生引用不通的中间提交。

1) 号码熵。{YmdHis}{6 位随机} 在同一秒内只有 10^6 的取值空间,高并发下单会因
   随机后缀碰撞触发 uniq_order_number / uniq_flow_number 冲突(每秒 3 单约
   2.6 天撞一次,每秒 10 单约每天 4 次)。后缀扩到 8 位后空间升至 10^8
   (+6.6 bit),撞号频率下降两个数量级。
   选 8 位纯数字而非等长 base36:保持定长纯数字形态,号码仍可被运营从截图或
   邮件整段复制、电话口述;且表默认排序规则是 utf8mb4_*_ci(大小写不敏感),
   混入大小写字母会让唯一索引把 aB 与 Ab 判为重复行,反而凭空制造冲突。22 字符
   仍满足 VARCHAR(32) 与网关 out_trade_no ≤ 32,拼上重试后缀 P999 后为 26 字符。

2) 自带订单号形态校验。create() 此前对调用方传入的 order_number 不做检查,传数组
   会因 (string) 转换把字面量 "Array" 写进列。现收紧为「全数字、20 或 22 位」,
   不合规即弃用并回落到分配器(WP_DEBUG 下留痕)。放行 20 位是为了兼容扩位前
   分配的历史号码——该方法原有的注释就声明服务于导入、补单与幂等重放。

3) 流水表补 KEY idx_flow_created (created_at)。后台「资金流水」页默认视图是
   WHERE 1=1 ORDER BY created_at DESC, id DESC LIMIT n 加同条件 COUNT(*),界面
   上的日期区间筛选也只有 created_at 可用。此前没有任何以 created_at 为前导列的
   索引(idx_flow_event_time 的前导列是 event_type,筛选时可空),于是每次翻页
   都是全表扫描 + filesort,且与表大小无关地重复付出。

4) 移除归档方案与删单守卫。归档表 + 每日清理 cron 需要配套的归档浏览入口才闭环,
   未闭环时运营会误以为记录丢失;而第 3 条的索引已覆盖它要解决的主要问题,且
   不必在财务表上执行 DELETE。一并撤回删单 / 清理订单时对财务记录的拦截。

- BusinessNumber:后缀位数与上界收敛为 SUFFIX_DIGITS / SUFFIX_MAX,去掉三处
  重复的魔法数字;类文档写明选型依据,避免日后被改回字母方案
- OrderRepositoryCore:新增 ORDER_NUMBER_PATTERN 与 is_scalar 守卫;
  clear_closed_orders 与 delete_order 恢复单路签名(去掉 $force 与
  $include_orders_with_records)
- DBMigratorPart2:流水表新增 idx_flow_created,并删除两张归档表 DDL
- PayOrders:清理与删除订单回到单路处理,移除 orders_clear_blocked /
  order_delete_blocked 两条提示
- 删除 FinancialFlowRetention 类与 functions.php 中的引导注册
- 测试:BusinessNumberTest 期望值改 22 字符、越界用例改 10^8、补纯数字与 P999
  余量断言;流水契约测试并入索引断言、归档移除的反向断言、号码重试与形态断言

按用户要求不写双路逻辑与数据迁移。新索引依赖 dbDelta 在下次迁移时补到既有表;
若此前已跑过迁移,两张归档空表需手工 DROP(未写迁移,故不自动处理)。

验证:PHPUnit 44 项 / 106 断言全部通过;node --test(已追踪用例)333 项 /
333 通过,含本地专用用例共 370 项全部通过;php -l 全部改动的 PHP 文件通过;
残留引用扫描确认 FinancialFlowRetention、两张归档表名、has_financial_records、
count_closed_orders_with_records、financial_records_exists_sql、两条 blocked
提示、$force 与 $include_orders_with_records 均已无残留。
2026-09-18 23:17:36 +08:00
kate522 c06aadb9a9 fix(css): 弹窗在 portal 内重建共享令牌基座,修复样式静默失效
弹窗外壳由 runtime portal 到 <body>,已经位于任何页面级 token 作用域之外。此前
路由样式把语义别名挂在页面容器上:

  .aksrt-orders-page { --orders-ink: var(--color-ink, #333); }

弹窗内部再消费 var(--orders-ink) 时该变量不存在,整条声明被判为无效值丢弃。
CSS 自定义属性解析失败不会抛错,只表现为边框消失、次级文字发灰发透明,排查
成本很高——订单详情弹窗此前「样式没做完」的观感就来自这里。

修法是在弹窗外壳上重建基座,而不是逐个给弹窗补别名:外壳永远位于 portal 内,
任何弹窗都能直接消费。

- modal-runtime.css 新增 portal token bridge,在 .aksrt-modal-runtime 上声明
  --modal-ink / -ink-2 / -muted / -line / -line-soft / -surface / -surface-2 /
  -surface-3 / -primary / -on-primary / -warning / -success / -danger,
  全部以 var(--color-*, 回退值) 兜底
- orders.css 的 --detail-* 由直接推导调色板值改为映射 var(--modal-*),路由文件
  不再自行持有颜色定义,避免同类问题在下一个弹窗上重演
- 重建 assets/css/build/public.css 与 public.min.css

验证:node --test(已追踪用例)333 项 / 333 通过;确认重建后的 public.min.css
已包含 --modal-ink。视觉核对未在本次提交内完成(需要可运行的站点),如需可在
真实环境按订单详情弹窗逐项比对边框、分隔线与次级文字色。
2026-09-18 23:17:18 +08:00
kate522 3bec2a5777 fix(icons): 图标类名统一为 FA5 语法并修正一处失效图标名
主题打包的是 Font Awesome 5,solid 字形必须带样式类 fas。全站有 18 处仍写成
FA4 的裸 fa fa-*,FA5 下并不存在对应字形,表现为图标位置空白;而 FA5-only
契约此前只扫描部分运行时文件,没有覆盖模板与渲染器,这些写法一直没被拦下。

同时发现一处真实的失效图标名:profile-level.php 用的是 fa-line-chart。这是
FA4 名称,FA5 把该系列重命名为 chart-*,正确写法是 fa-chart-line。

- 18 处 "fa fa-*" 补上 fas:首页模板、组件标记、评论表单、用户等级页、菜单
  装饰、SchemaPayment 促销标签默认值、VIP 服务弹窗、ContentRenderer /
  GeneralRenderer、后台卡片样式校验、pay-vip.js
- profile-level.php 的 fa-line-chart 修正为 fa-chart-line
- 三处注释里的示例值同步为合法形态("fa fas fa-star"),避免被当成正确写法
  抄走;credits.css 注释中直接写出旧字体族名的一处改为描述性表述,否则会被
  FA5-only 契约当作违规命中
- legacy-navigation-contract:FA 资产断言改为跟随 aksrt_enqueue_fontawesome()
  这一间接层(注册点已从脚本文件移到 inc/helpers/aksrt-icons.php),并把
  fa-zibsvg-<key> 识别为项目自有 SVG sprite 族,而非 Font Awesome 字形
- vip-points-exchange-modal 契约同步断言 fa fas fa-angle-right

验证:改动前在干净 HEAD 上跑 node --test(已追踪用例)为 327 项 / 325 通过 /
2 失败,两项失败正是本次修复的失效图标名断言与 FA 版本号断言;改动后 333 项 /
333 通过(含 5 个本地专用用例共 370 项全部通过)。php -l 全部改动的 PHP 文件
通过。
2026-09-18 23:17:05 +08:00
kate522 a6819ec7a5 chore(repo): 显式声明行尾规则并恢复 tests 目录追踪
仓库此前没有 .gitattributes,行尾完全依赖 core.autocrlf=true。源文件在工作区
是 CRLF,而 esbuild 产物(public.css / public.min.css、运行时 .min.js)是 LF,
两种行尾混在同一套自动转换策略下:每次构建都会把产物整文件标记为已修改,真正的
改动反而被行尾噪音淹没。

- 新增 .gitattributes:源文件(php / js / cjs / vue / json / css / md 等)统一
  text eol=crlf,与既有工作区一致;构建产物(assets/css/build/**、
  assets/css/main.min.css、assets/js/runtime/*.min.js、assets/js/build/**)
  强制 text eol=lf,与 esbuild 输出一致;.gitattributes / .gitignore 自身用
  eol=lf,避免规则文件自己先被转换
- .gitignore 改为反向规则:先以 !/tests/ 放行整个目录,再逐个排除仅本地使用的
  条目(visual-results、widget-golden 夹具、5 个 widget-system 用例、run.php、
  scratch harness)。此前 tests/navigation/ 从未被放行,只靠历史追踪才没有从
  仓库里消失——一旦有人重新 clone,相关用例就会缺文件
- 顺带纳入 /.workbuddy/

验证:git ls-files --eol 确认源文件为 CRLF、构建产物为 LF;git status --ignored
确认仅本地条目仍处于忽略状态,反向规则没有意外放行本地用例。改动不触及运行时
代码与测试断言,node --test tests/**/*.test.cjs 370 项全部通过。
2026-09-18 23:16:01 +08:00
29 changed files with 420 additions and 132 deletions

No files matched your search

+81
View File
@@ -0,0 +1,81 @@
# 换行规范
#
# 背景:本仓库历史索引(index)内全部为 LF(`git ls-files --eol` 显示 i/lf),
# 而 Windows 工作区长期依赖 core.autocrlf=true 做 CRLF↔LF 转换。这种隐式转换
# 会让「工具以 LF 写盘」的改动在提交时产生大量换行噪声与 "LF will be replaced
# by CRLF" 警告。此处把规则显式化,使任何机器、任何 core.autocrlf 设置下行为一致。
#
# 注意:不要为已跟踪文件添加 `-text`。索引为 LF 而工作区为 CRLF 的文件一旦
# 被标记为 `-text`,git 会认为工作区内容与索引不一致,从而产生虚假改动。
# 默认:按内容自动识别是否为文本,文本一律在仓库内规范化为 LF。
* text=auto
# 源码与配置(文本)
#
# 这里必须同时给出 `eol=crlf`。只写 `* text=auto` 时,工作区的换行形态仍由每个
# 开发者的 core.autocrlf 决定——工具(编辑器、脚本)以 LF 写盘时不会报错,只会
# 在提交时冒出 "LF will be replaced by CRLF" 警告,也就是本仓库此前的漂移来源。
# 声明 `eol=crlf` 后,git 会把工作区自动纠正为 CRLF,漂移自愈。
# 索引侧仍然是 LF,因此不会产生任何既有文件的改写。
*.php text eol=crlf
*.css text eol=crlf
*.scss text eol=crlf
*.js text eol=crlf
*.cjs text eol=crlf
*.mjs text eol=crlf
*.vue text eol=crlf
*.json text eol=crlf
*.html text eol=crlf
*.svg text eol=crlf
*.txt text eol=crlf
*.md text eol=crlf
*.yml text eol=crlf
*.yaml text eol=crlf
*.xml text eol=crlf
*.dist text eol=crlf
*.htaccess text eol=crlf
# gettext 目录按 LF 维护,避免与 xgettext/msgmerge 的默认输出冲突。
*.pot text
*.po text
# 构建产物必须声明为 LF。
#
# esbuild 恒定输出 LF,而上面的 `*.css eol=crlf` 会让工作区期待 CRLF,
# 于是每次执行 tools/build-public-assets.mjs 都会重新引入换行漂移。
# 这些路径必须放在上面各条之后:.gitattributes 以最后匹配的规则为准。
/assets/css/build/** text eol=lf
/assets/css/main.min.css text eol=lf
/assets/js/runtime/*.min.js text eol=lf
/assets/js/build/** text eol=lf
# 二进制资源(不做换行转换,也不做 diff)
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.webp binary
*.avif binary
*.ico binary
*.bmp binary
*.woff binary
*.woff2 binary
*.ttf binary
*.otf binary
*.eot binary
*.zip binary
*.gz binary
*.tgz binary
*.tar binary
*.pdf binary
*.mp3 binary
*.mp4 binary
*.webm binary
*.psd binary
*.ai binary
*.sketch binary
# 这些文件本身必须是 LF,否则 git 解析 `.gitattributes` 时可能把 CR 读进模式串。
.gitattributes text eol=lf
.gitignore text eol=lf
+23 -66
View File
@@ -4,80 +4,37 @@
/.compare/
/.zcode/
/.mimosa/
/.workbuddy/
# Project planning, internal docs, and local-only development files
/.aksrt-chrome-profile/
/.specs/
/docs/
# tools/: 只版本化两个共享入口,其余为本地脚本。
/tools/*
!/tools/build-public-assets.mjs
/tests/*
!/tools/wp-cli-visual-fixtures.php
!/tests/fixtures/
!/tests/visual/
/tests/fixtures/*
!/tests/fixtures/page-visual-matrix-v1.json
!/tests/widget-system/
/tests/widget-system/*
!/tests/widget-system/post-editor-reference-contract.test.cjs
!/tests/widget-system/post-visual-contract.test.cjs
!/tests/widget-system/lazyload-contract.test.cjs
!/tests/widget-system/toast-contract.test.cjs
!/tests/widget-system/page-display-contract.test.cjs
!/tests/widget-system/icon-card-contract.test.cjs
!/tests/widget-system/page-display-save-harness.php
!/tests/widget-system/admin-ui-contract.test.cjs
!/tests/widget-system/email-system-contract.test.cjs
!/tests/widget-system/auth-source-contract.test.cjs
!/tests/widget-system/captcha-business-contract.test.cjs
!/tests/widget-system/captcha-security-harness.php
!/tests/widget-system/captcha-runtime-harness.test.cjs
!/tests/widget-system/oauth-security-contract.test.cjs
!/tests/widget-system/oauth-security-harness.php
!/tests/widget-system/user-badges-contract.test.cjs
!/tests/widget-system/user-features-contract.test.cjs
!/tests/widget-system/zibll-modern-widgets-contract.test.cjs
!/tests/widget-system/user-card-runtime-contract.test.cjs
!/tests/widget-system/user-card-runtime-harness.php
!/tests/widget-system/notification-preferences-contract.test.cjs
!/tests/widget-system/message-center-contract.test.cjs
!/tests/widget-system/message-center-url-harness.php
!/tests/widget-system/message-center-pagination-harness.php
!/tests/widget-system/asset-ledger-contract.test.cjs
!/tests/widget-system/financial-flow-contract.test.cjs
!/tests/widget-system/user-assets-layout-contract.test.cjs
!/tests/widget-system/pay-orders-filter-contract.test.cjs
!/tests/widget-system/front-submit-editor-contract.test.cjs
!/tests/widget-system/front-submit-attachment-contract.test.cjs
!/tests/widget-system/comment-smilies-removal-contract.test.cjs
!/tests/widget-system/comment-children-list-reset-contract.test.cjs
!/tests/widget-system/comment-image-upload-contract.test.cjs
!/tests/widget-system/pay-card-contract.test.cjs
!/tests/widget-system/circular-button-icon-margin-contract.test.cjs
!/tests/widget-system/single-cover-height-contract.test.cjs
!/tests/widget-system/frontend-lifecycle-contract.test.cjs
!/tests/widget-system/card-pass-universal-redeem.test.cjs
!/tests/widget-system/card-pass-modal-redeem.test.cjs
!/tests/widget-system/balance-charge-modal.test.cjs
!/tests/widget-system/vip-points-exchange-modal.test.cjs
!/tests/widget-system/pay-sdk-order-contract.test.cjs
!/tests/admin/
/tests/admin/*
!/tests/admin/charge-cards-page-contract.test.cjs
!/tests/admin/charge-cards-dependency.test.cjs
!/tests/admin/charge-cards-pagination.test.cjs
!/tests/admin/charge-cards-form-constraints.test.cjs
!/tests/admin/card-pass-order-numbers.test.cjs
!/tests/front-submit/
/tests/front-submit/*
!/tests/front-submit/video-cover-contract.test.cjs
!/tests/front-submit/featured-buttons-contract.test.cjs
!/tests/front-submit/editor-fullscreen-contract.test.cjs
!/tests/asset-ledger/
!/tests/asset-ledger/*.php
!/tests/visual/docker-compose.zibll.yml
!/tests/visual/zibll-Caddyfile
!/tests/visual/README.md
# tests/: 放行整个套件,按名字排除本地专有产物。
#
# 这里刻意从「逐文件白名单」改为「先放行目录、再逐个排除」的反向写法:
# 白名单要求每新增一个 *.test.cjs 都同步改本文件,一旦漏改,该测试文件会静默
# 不被跟踪,在 CI 上表现为「测试数量莫名变少」,且极难排查。反向写法下新增测试
# 自动纳入版本控制,只有确需本地专有的文件才需要追加排除。
#
# 下列 10 个路径是切换前就已处于「忽略且未跟踪」状态的本地专有文件,逐一保留原状。
!/tests/
/tests/visual-results/
/tests/fixtures/widget-golden-v1.json
/tests/widget-system/a11y-contract.test.cjs
/tests/widget-system/admin-core.test.cjs
/tests/widget-system/captcha-cloud-contract.test.cjs
/tests/widget-system/comment-hard-cut-contract.test.cjs
/tests/widget-system/download-config-contract.test.cjs
/tests/widget-system/run.php
/tests/widget-system/scratch-term-aggregation-harness.php
/CLAUDE.md
/AGENTS.md
ask-*.md
@@ -17,7 +17,7 @@
<script setup>
/**
* IconValue — renders any icon-picker value ("fa fas fa-x", "zibsvg-key",
* IconValue — renders any icon-picker value ("fa fas fa-star", "zibsvg-key",
* "customsvg-<svg>") as inline preview markup.
*/
import { computed } from 'vue'
@@ -42,7 +42,7 @@ export const knowledgeSchema = {
vip_2_points_default: 0,
pay_price_default: '0.01',
pay_original_price_default: '',
pay_promotion_tag_default: '<i class="fa fa-fw fa-bolt"></i> 限时特惠',
pay_promotion_tag_default: '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠',
vip_1_price_default: '0',
vip_2_price_default: '0',
pay_rebate_discount: '0',
@@ -213,7 +213,7 @@ export const knowledgeSchema = {
{ key: 'vip_2_points_default', label: '二级会员积分售价(积分)', type: 'number', default: 0, min: 0 },
{ key: 'pay_price_default', label: '执行价', type: 'input', sanitize_type: 'money_decimal', default: '0.01', suffix: '元' },
{ key: 'pay_original_price_default', label: '原价', type: 'input', sanitize_type: 'money_decimal', default: '', suffix: '元', help: '显示在执行价格前面,并划掉' },
{ key: 'pay_promotion_tag_default', label: '促销标签', type: 'textarea', sanitize_type: 'html', default: '<i class="fa fa-fw fa-bolt"></i> 限时特惠', rows: 1 },
{ key: 'pay_promotion_tag_default', label: '促销标签', type: 'textarea', sanitize_type: 'html', default: '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', rows: 1 },
{ key: 'vip_1_price_default', label: '一级会员价格', type: 'input', sanitize_type: 'money_decimal', default: '0', suffix: '元', help: '填0则为一级会员免费' },
{ key: 'vip_2_price_default', label: '二级会员价格', type: 'input', sanitize_type: 'money_decimal', default: '0', suffix: '元', help: '填0则为二级会员免费' },
{ key: 'pay_rebate_discount', label: '推广折扣', type: 'input', sanitize_type: 'money_decimal', default: '0', suffix: '元', help: '通过推广链接购买,额外优惠的金额;需开启推广返佣功能,且不能超过实际购买价' },
+29
View File
@@ -6986,6 +6986,35 @@ body.aksrt-body-scroll .aksrt-show-on-scroll {
* @package AKSRT
*/
/* ---- portal token bridge ----
*
* 弹窗由 runtime portal 到 <body>,因此它位于任何「页面级 token 作用域」之外。
* 典型陷阱:路由样式把语义别名挂在页面上,例如
*
* .aksrt-orders-page { --orders-ink: var(--color-ink, #333); }
*
* 弹窗内部再去 `var(--orders-ink)` 时该变量不存在,整条声明会被静默丢弃——
* 表现为边框消失、次级文字变透明,而不是抛错,排查成本很高。
*
* 因此共享调色板在这里声明:本壳层永远在 portal 内,任何弹窗都能直接消费
* `--modal-*`。路由样式应把自己的局部别名映射到这里,而不是映射到页面级变量。
*/
.aksrt-modal-runtime {
--modal-ink: var(--color-ink, #333333);
--modal-ink-2: var(--color-ink-2, #4e5358);
--modal-muted: var(--color-ink-3, #777777);
--modal-line: var(--color-line-stronger, rgba(50, 50, 50, 0.12));
--modal-line-soft: var(--color-line, rgba(50, 50, 50, 0.06));
--modal-surface: var(--surface-1, #ffffff);
--modal-surface-2: var(--surface-2, #eeeeee);
--modal-surface-3: var(--surface-4, #f8f8f8);
--modal-primary: var(--color-primary, #f04494);
--modal-on-primary: var(--color-on-primary, #ffffff);
--modal-warning: var(--color-warning, #f59e0b);
--modal-success: var(--color-success, #10b981);
--modal-danger: var(--color-danger, #ef4444);
}
/* ---- scroll lock ---- */
html.aksrt-modal-runtime-open,
body.aksrt-modal-runtime-open {
+1 -1
View File
@@ -1 +1 @@
:root{--color-primary: #f04494;--color-primary-hover: #e0307e;--color-primary-active: #c9206d;--color-primary-soft: rgba(240, 68, 148, .1);--color-primary-softer: rgba(240, 68, 148, .05);--color-on-primary: #FFFFFF;--color-ink: #333333;--color-ink-2: #4e5358;--color-ink-3: #777777;--color-ink-4: #999999;--surface-page: #f5f6f7;--surface-1: #FFFFFF;--surface-2: #eeeeee;--surface-3: #e9ecef;--surface-4: #f8f8f8;--color-line: rgba(50, 50, 50, .06);--color-line-soft: rgba(0, 0, 0, .03);--color-line-stronger: rgba(50, 50, 50, .12);--color-success: #10B981;--color-warning: #F59E0B;--color-error: #EF4444;--color-info: #3B82F6;--color-accent: #18a058;--color-accent-soft: rgba(24, 160, 88, .12);--color-accent-shadow: rgba(24, 160, 88, .28);--font-display: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;--font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;--font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;--font-mono: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;--text-xs: .75rem;--text-sm: .875rem;--text-base: 1rem;--text-md: 1.125rem;--text-lg: 1.25rem;--text-xl: 1.5rem;--text-2xl: 1.875rem;--text-3xl: 2.25rem;--text-4xl: 3rem;--text-5xl: 3.75rem;--leading-tight: 1.2;--leading-snug: 1.4;--leading-normal: 1.6;--leading-relaxed: 1.6;--space-1: 4px;--space-2: 8px;--space-3: 12px;--space-4: 16px;--space-5: 20px;--space-6: 24px;--space-7: 32px;--space-8: 40px;--space-9: 48px;--space-10: 64px;--space-11: 80px;--space-12: 96px;--radius-xs: 3px;--radius-sm: 4px;--radius: 8px;--radius-lg: 8px;--radius-xl: 12px;--radius-2xl: 16px;--radius-pill: 100px;--radius-avatar: 50%;--shadow-xs: 0 1px 2px rgba(0, 0, 0, .03);--shadow-sm: 0 0 10px rgba(116, 116, 116, .08);--shadow: 0 0 10px rgba(116, 116, 116, .12);--shadow-lg: 0 12px 32px rgba(18, 18, 18, .1);--shadow-drawer: 0 18px 60px rgba(0, 0, 0, .2);--nav-height: 60px;--container: 1200px;--container-wide: 1440px;--container-read: 760px;--container-page: 880px;--sidebar-width: 311px;--content-gap: 24px;--section-gap: 24px;--sidebar-bp: 992px;--ease: cubic-bezier(.4, 0, .2, 1);--ease-out: cubic-bezier(.16, 1, .3, 1);--ease-spring: cubic-bezier(.34, 1.56, .64, 1);--dur-fast: .15s;--dur: .25s;--dur-slow: .4s;--focus-ring: 0 0 2px 3px rgba(253, 83, 161, .4);--z-dropdown: 100;--z-sticky: 200;--z-fixed: 300;--z-overlay: 400;--z-modal: 500;--z-popover: 600;--z-toast: 700;--z-tooltip: 800;--radius-btn: 4px;--btn-sm-h: 28px;--btn-md-h: 34px;--btn-lg-h: 44px;--card-padding: 15px;--card-padding-sm: 10px;--card-gap: 16px;--card-gap-lg: 20px;--skeleton-base: var(--surface-2);--skeleton-shine: var(--surface-1);--color-primary-light: var(--color-primary-soft);--color-surface: var(--surface-1);--color-line-strong: var(--color-line);--color-bg-hover: var(--surface-2);--color-border-light: var(--color-line-soft);--color-fill: var(--surface-2);--color-warning-bg: #fffbe6;--color-warning-text: #ad6800;--text-secondary: var(--color-ink-3);--text-tertiary: var(--color-ink-4);--surface-active: var(--surface-2);--surface-card: var(--surface-1);--surface-hover: var(--surface-2);--surface-secondary: var(--surface-2);--radius-code: var(--radius);--radius-full: 100px;--shadow-xl: 0 0 10px rgba(116, 116, 116, .12);--space-16: 64px;--z-floating: 900;--transition-fast: .15s ease;--transition-base: .25s ease;--header-bg: var(--blur-bg);--header-color: var(--color-ink-2);--footer-bg: var(--surface-1);--footer-color: var(--color-ink-4);--blur-bg: rgba(255, 255, 255, .8);--float-btn-bg: rgba(200, 200, 200, .4);--content-max: var(--container);--font-heading: var(--font-ui);--radius-card: var(--radius);--radius-md: var(--radius);--color-primary-50: var(--color-primary-soft);--color-primary-dark: var(--color-primary-active);--shadow-md: var(--shadow);--mini-radius: calc(var(--radius) / 1.4);--aksrt-avatar-size-xs: 24px;--aksrt-avatar-size-sm: 32px;--aksrt-avatar-size: 40px;--aksrt-avatar-size-lg: 56px;--aksrt-avatar-size-xl: 80px;--aksrt-card-thumb-scale: 56.25%;--aksrt-rose: #f43f5e;--aksrt-rose-soft: rgba(244, 63, 94, .12);--aksrt-blue: #3b82f6;--aksrt-blue-soft: rgba(59, 130, 246, .12);--aksrt-green: #10b981;--aksrt-green-soft: rgba(16, 185, 129, .12);--aksrt-orange: #f97316;--aksrt-orange-soft:rgba(249, 115, 22, .12);--aksrt-purple: #8b5cf6;--aksrt-purple-soft:rgba(139, 92, 246, .12);--aksrt-cyan: #06b6d4;--aksrt-cyan-soft: rgba(6, 182, 212, .12);--aksrt-yellow: #eab308;--aksrt-yellow-soft:rgba(234, 179, 8, .12);--aksrt-red: #ef4444;--aksrt-red-soft: rgba(239, 68, 68, .12);--aksrt-teal: #14b8a6;--aksrt-teal-soft: rgba(20, 184, 166, .12);--aksrt-indigo: #6366f1;--aksrt-indigo-soft:rgba(99, 102, 241, .12);--aksrt-pink: #ec4899;--aksrt-pink-soft: rgba(236, 72, 153, .12);--aksrt-vip-gold-1: #f6d365;--aLine truncated
:root{--color-primary: #f04494;--color-primary-hover: #e0307e;--color-primary-active: #c9206d;--color-primary-soft: rgba(240, 68, 148, .1);--color-primary-softer: rgba(240, 68, 148, .05);--color-on-primary: #FFFFFF;--color-ink: #333333;--color-ink-2: #4e5358;--color-ink-3: #777777;--color-ink-4: #999999;--surface-page: #f5f6f7;--surface-1: #FFFFFF;--surface-2: #eeeeee;--surface-3: #e9ecef;--surface-4: #f8f8f8;--color-line: rgba(50, 50, 50, .06);--color-line-soft: rgba(0, 0, 0, .03);--color-line-stronger: rgba(50, 50, 50, .12);--color-success: #10B981;--color-warning: #F59E0B;--color-error: #EF4444;--color-info: #3B82F6;--color-accent: #18a058;--color-accent-soft: rgba(24, 160, 88, .12);--color-accent-shadow: rgba(24, 160, 88, .28);--font-display: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;--font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;--font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;--font-mono: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;--text-xs: .75rem;--text-sm: .875rem;--text-base: 1rem;--text-md: 1.125rem;--text-lg: 1.25rem;--text-xl: 1.5rem;--text-2xl: 1.875rem;--text-3xl: 2.25rem;--text-4xl: 3rem;--text-5xl: 3.75rem;--leading-tight: 1.2;--leading-snug: 1.4;--leading-normal: 1.6;--leading-relaxed: 1.6;--space-1: 4px;--space-2: 8px;--space-3: 12px;--space-4: 16px;--space-5: 20px;--space-6: 24px;--space-7: 32px;--space-8: 40px;--space-9: 48px;--space-10: 64px;--space-11: 80px;--space-12: 96px;--radius-xs: 3px;--radius-sm: 4px;--radius: 8px;--radius-lg: 8px;--radius-xl: 12px;--radius-2xl: 16px;--radius-pill: 100px;--radius-avatar: 50%;--shadow-xs: 0 1px 2px rgba(0, 0, 0, .03);--shadow-sm: 0 0 10px rgba(116, 116, 116, .08);--shadow: 0 0 10px rgba(116, 116, 116, .12);--shadow-lg: 0 12px 32px rgba(18, 18, 18, .1);--shadow-drawer: 0 18px 60px rgba(0, 0, 0, .2);--nav-height: 60px;--container: 1200px;--container-wide: 1440px;--container-read: 760px;--container-page: 880px;--sidebar-width: 311px;--content-gap: 24px;--section-gap: 24px;--sidebar-bp: 992px;--ease: cubic-bezier(.4, 0, .2, 1);--ease-out: cubic-bezier(.16, 1, .3, 1);--ease-spring: cubic-bezier(.34, 1.56, .64, 1);--dur-fast: .15s;--dur: .25s;--dur-slow: .4s;--focus-ring: 0 0 2px 3px rgba(253, 83, 161, .4);--z-dropdown: 100;--z-sticky: 200;--z-fixed: 300;--z-overlay: 400;--z-modal: 500;--z-popover: 600;--z-toast: 700;--z-tooltip: 800;--radius-btn: 4px;--btn-sm-h: 28px;--btn-md-h: 34px;--btn-lg-h: 44px;--card-padding: 15px;--card-padding-sm: 10px;--card-gap: 16px;--card-gap-lg: 20px;--skeleton-base: var(--surface-2);--skeleton-shine: var(--surface-1);--color-primary-light: var(--color-primary-soft);--color-surface: var(--surface-1);--color-line-strong: var(--color-line);--color-bg-hover: var(--surface-2);--color-border-light: var(--color-line-soft);--color-fill: var(--surface-2);--color-warning-bg: #fffbe6;--color-warning-text: #ad6800;--text-secondary: var(--color-ink-3);--text-tertiary: var(--color-ink-4);--surface-active: var(--surface-2);--surface-card: var(--surface-1);--surface-hover: var(--surface-2);--surface-secondary: var(--surface-2);--radius-code: var(--radius);--radius-full: 100px;--shadow-xl: 0 0 10px rgba(116, 116, 116, .12);--space-16: 64px;--z-floating: 900;--transition-fast: .15s ease;--transition-base: .25s ease;--header-bg: var(--blur-bg);--header-color: var(--color-ink-2);--footer-bg: var(--surface-1);--footer-color: var(--color-ink-4);--blur-bg: rgba(255, 255, 255, .8);--float-btn-bg: rgba(200, 200, 200, .4);--content-max: var(--container);--font-heading: var(--font-ui);--radius-card: var(--radius);--radius-md: var(--radius);--color-primary-50: var(--color-primary-soft);--color-primary-dark: var(--color-primary-active);--shadow-md: var(--shadow);--mini-radius: calc(var(--radius) / 1.4);--aksrt-avatar-size-xs: 24px;--aksrt-avatar-size-sm: 32px;--aksrt-avatar-size: 40px;--aksrt-avatar-size-lg: 56px;--aksrt-avatar-size-xl: 80px;--aksrt-card-thumb-scale: 56.25%;--aksrt-rose: #f43f5e;--aksrt-rose-soft: rgba(244, 63, 94, .12);--aksrt-blue: #3b82f6;--aksrt-blue-soft: rgba(59, 130, 246, .12);--aksrt-green: #10b981;--aksrt-green-soft: rgba(16, 185, 129, .12);--aksrt-orange: #f97316;--aksrt-orange-soft:rgba(249, 115, 22, .12);--aksrt-purple: #8b5cf6;--aksrt-purple-soft:rgba(139, 92, 246, .12);--aksrt-cyan: #06b6d4;--aksrt-cyan-soft: rgba(6, 182, 212, .12);--aksrt-yellow: #eab308;--aksrt-yellow-soft:rgba(234, 179, 8, .12);--aksrt-red: #ef4444;--aksrt-red-soft: rgba(239, 68, 68, .12);--aksrt-teal: #14b8a6;--aksrt-teal-soft: rgba(20, 184, 166, .12);--aksrt-indigo: #6366f1;--aksrt-indigo-soft:rgba(99, 102, 241, .12);--aksrt-pink: #ec4899;--aksrt-pink-soft: rgba(236, 72, 153, .12);--aksrt-vip-gold-1: #f6d365;--aLine truncated
+29
View File
@@ -4,6 +4,35 @@
* @package AKSRT
*/
/* ---- portal token bridge ----
*
* 弹窗由 runtime portal 到 <body>,因此它位于任何「页面级 token 作用域」之外。
* 典型陷阱:路由样式把语义别名挂在页面上,例如
*
* .aksrt-orders-page { --orders-ink: var(--color-ink, #333); }
*
* 弹窗内部再去 `var(--orders-ink)` 时该变量不存在,整条声明会被静默丢弃——
* 表现为边框消失、次级文字变透明,而不是抛错,排查成本很高。
*
* 因此共享调色板在这里声明:本壳层永远在 portal 内,任何弹窗都能直接消费
* `--modal-*`。路由样式应把自己的局部别名映射到这里,而不是映射到页面级变量。
*/
.aksrt-modal-runtime {
--modal-ink: var(--color-ink, #333333);
--modal-ink-2: var(--color-ink-2, #4e5358);
--modal-muted: var(--color-ink-3, #777777);
--modal-line: var(--color-line-stronger, rgba(50, 50, 50, 0.12));
--modal-line-soft: var(--color-line, rgba(50, 50, 50, 0.06));
--modal-surface: var(--surface-1, #ffffff);
--modal-surface-2: var(--surface-2, #eeeeee);
--modal-surface-3: var(--surface-4, #f8f8f8);
--modal-primary: var(--color-primary, #f04494);
--modal-on-primary: var(--color-on-primary, #ffffff);
--modal-warning: var(--color-warning, #f59e0b);
--modal-success: var(--color-success, #10b981);
--modal-danger: var(--color-danger, #ef4444);
}
/* ---- scroll lock ---- */
html.aksrt-modal-runtime-open,
body.aksrt-modal-runtime-open {
+12 -11
View File
@@ -93,18 +93,19 @@
.aksrt-orders-page__empty h3 { margin: 0 0 7px; font-size: 15px; }.aksrt-orders-page__empty p { margin: 0 0 16px; color: var(--orders-muted); font-size: 13px; }
/* ---- Order detail modal (front-end order center) ---- */
/* The dialog is portalled to <body>, so it cannot inherit the page-scoped
`--orders-*` aliases; it re-declares the few tokens it needs on the shell. */
/* 弹窗 portal 到 <body>,读不到页面上的 `--orders-*` 别名,因此把本模块的局部
别名映射到 modal-runtime.css 声明的共享 portal 基座 `--modal-*`;
路由文件不应再自行推导调色板值。 */
.aksrt-order-detail-modal {
--detail-ink: var(--color-ink, #333333);
--detail-muted: var(--color-ink-3, #777777);
--detail-line: var(--color-line-stronger, rgba(50, 50, 50, .12));
--detail-surface: var(--surface-1, #fff);
--detail-surface-2: var(--surface-2, #eee);
--detail-surface-3: var(--surface-4, #f8f8f8);
--detail-primary: var(--color-primary, #f04494);
--detail-warning: var(--color-warning, #f59e0b);
--detail-success: var(--color-success, #10b981);
--detail-ink: var(--modal-ink);
--detail-muted: var(--modal-muted);
--detail-line: var(--modal-line);
--detail-surface: var(--modal-surface);
--detail-surface-2: var(--modal-surface-2);
--detail-surface-3: var(--modal-surface-3);
--detail-primary: var(--modal-primary);
--detail-warning: var(--modal-warning);
--detail-success: var(--modal-success);
}
.aksrt-order-detail-modal:not(.aksrt-modal-runtime--bottom) .aksrt-modal-runtime__dialog { width: min(480px, calc(100vw - 32px)); }
+2 -2
View File
@@ -225,8 +225,8 @@
}
.balance-charge-form .vip-product.active::before {
content: '\f058';
/* 本主题打包的是 FA5(family "Font Awesome 5 Free",solid 字形需 900),
Zibll 的 FA4 family "FontAwesome" 在本主题不存在 → 豆腐块(被误认破图)。 */
/* 本主题打包的是 FA5(族名 'Font Awesome 5 Free',solid 字形需 900),
Zibll 遗留的 FA4 族名在本主题不存在 → 豆腐块(被误认破图)。 */
font: normal normal 900 16px/1 'Font Awesome 5 Free';
color: var(--focus-color);
position: absolute;
+1 -1
View File
@@ -1,6 +1,6 @@
/**
* AKSRT universal icon picker — shared admin modal for FA5, the built-in
* SVG sprite and inline custom SVG values ("fa fas fa-x" / "zibsvg-key" /
* SVG sprite and inline custom SVG values ("fa fas fa-star" / "zibsvg-key" /
* "customsvg-<svg>").
*
* Any admin screen: call aksrt_icon_picker_assets() (PHP) and render fields
+2 -2
View File
@@ -446,7 +446,7 @@
append(tab, miniCard);
append(tabItem, tab);
var activeIcon = node("div", "abs-right active-icon");
append(activeIcon, node("i", "fa fa-check-circle"));
append(activeIcon, node("i", "fa fas fa-check-circle"));
activeIcon.setAttribute("aria-hidden", "true");
append(tabItem, activeIcon);
append(tabs, tabItem);
@@ -494,7 +494,7 @@
pointsLink.setAttribute("data-aksrt-vip-points-open", "");
pointsLink.innerHTML = '<svg class="icon" aria-hidden="true"><use xlink:href="#icon-points-color"></use></svg>'
+ "积分兑换会员"
+ '<i class="fa fa-angle-right em12 ml10" aria-hidden="true"></i>';
+ '<i class="fa fas fa-angle-right em12 ml10" aria-hidden="true"></i>';
append(products, pointsLink);
}
if (catalog.more) append(products, node("div", "muted-2-color mb10 em09 muted-box padding-10 mini-radius", catalog.more));
+1 -1
View File
@@ -303,7 +303,7 @@ if (!function_exists('aksrt_homepage_get_option_orderby_but')) {
$is_dropdown = ($option['dropdown'] && $dropdown_but) ? true : false;
$d_but = $is_dropdown
? '<a href="javascript:;" data-toggle="dropdown"><span name="cat">' . esc_html__('排序', 'aksrt') . '</span><i class="fa fa-fw fa-sort opacity5" aria-hidden="true"></i></a>'
? '<a href="javascript:;" data-toggle="dropdown"><span name="cat">' . esc_html__('排序', 'aksrt') . '</span><i class="fa fas fa-fw fa-sort opacity5" aria-hidden="true"></i></a>'
: esc_html__('排序', 'aksrt');
$html = '<div class="flex ac">';
+5 -5
View File
@@ -157,7 +157,7 @@ if (!function_exists('aksrt_widget_hot_posts')) :
if ((string) $data['primary'] !== '') {
$thumb = '<img' . aksrt_get_lazy_attr('lazy_posts_thumb', (string) $data['primary'], 'fit-cover radius8') . ' alt="' . esc_attr((string) $data['title']) . '">';
}
$time_ago = '<i class="fa fa-clock mr3" aria-hidden="true"></i>' . esc_html((string) $data['time_ago']);
$time_ago = '<i class="fa fas fa-clock mr3" aria-hidden="true"></i>' . esc_html((string) $data['time_ago']);
$permalink = esc_url((string) $data['permalink']);
if ($i === 1) {
@@ -214,7 +214,7 @@ if (!function_exists('aksrt_widget_term_aggregation')) :
}
$href = esc_url((string) $href);
$icon = '<i class="fa fa-folder-open mr6" aria-hidden="true"></i>';
$icon = '<i class="fa fas fa-folder-open mr6" aria-hidden="true"></i>';
$img = aksrt_widget_term_cover_url((int) $term->term_id, true);
$img_html = aksrt_graphic_card([
'class' => '',
@@ -241,8 +241,8 @@ if (!function_exists('aksrt_widget_term_aggregation')) :
];
$query = \AKSRT\WidgetSystem\PostQuery::build($query_args, 1, []);
$meta = '<span class="px12 muted-3-color text-ellipsis"><i class="fa fa-file-alt fa-fw" aria-hidden="true"></i>' . sprintf(esc_html__('%s篇文章', 'aksrt'), esc_html(aksrt_format_number(max(0, (int) $query->found_posts)))) . '</span>';
$meta .= '<a class="but p2-10 px12 c-blue hide-sm shrink0"' . $target . ' href="' . $href . '"><i class="fa fa-angle-right" aria-hidden="true"></i>' . esc_html__('更多文章', 'aksrt') . '</a>';
$meta = '<span class="px12 muted-3-color text-ellipsis"><i class="fa fas fa-file-alt fa-fw" aria-hidden="true"></i>' . sprintf(esc_html__('%s篇文章', 'aksrt'), esc_html(aksrt_format_number(max(0, (int) $query->found_posts)))) . '</span>';
$meta .= '<a class="but p2-10 px12 c-blue hide-sm shrink0"' . $target . ' href="' . $href . '"><i class="fa fas fa-angle-right" aria-hidden="true"></i>' . esc_html__('更多文章', 'aksrt') . '</a>';
$meta = '<div class="term-meta flex jsb ac">' . $meta . '</div>';
$posts_html = '';
@@ -271,7 +271,7 @@ if (!function_exists('aksrt_widget_term_aggregation')) :
break;
}
if ($post_meta === '') {
$post_meta = '<i class="fa fa-clock mr3" aria-hidden="true"></i>' . esc_html((string) $data['time_ago']);
$post_meta = '<i class="fa fas fa-clock mr3" aria-hidden="true"></i>' . esc_html((string) $data['time_ago']);
}
$posts_html .= '<div class="mt10 flex jsb ac">' . $title . '<div class="em09 muted-3-color flex0 ml10">' . $post_meta . '</div></div>';
}
+1 -1
View File
@@ -87,7 +87,7 @@ if (!function_exists('aksrt_get_input_expand_but')) :
if ($upload) {
$placeholder = get_template_directory_uri() . '/assets/icons/upload-add.svg';
$dropdown .= '<div class="tab-pane fade dropdown-image" id="image-tab-' . esc_attr($upload_id) . '-2">';
$dropdown .= '<p><a class="muted-color input-image-address-tab" data-image-tab="#image-tab-' . esc_attr($upload_id) . '-1" href="javascript:;"><i class="fa fa-angle-left mr6" aria-hidden="true"></i>' . esc_html__('填写图片地址', 'aksrt') . '</a></p>';
$dropdown .= '<p><a class="muted-color input-image-address-tab" data-image-tab="#image-tab-' . esc_attr($upload_id) . '-1" href="javascript:;"><i class="fa fas fa-angle-left mr6" aria-hidden="true"></i>' . esc_html__('填写图片地址', 'aksrt') . '</a></p>';
$dropdown .= '<div class="form-upload comment-image-upload">';
$dropdown .= '<label class="pointer comment-image-upload-label">';
$dropdown .= '<div class="preview text-center mb6"><img src="' . esc_url($placeholder) . '" alt="" /></div>';
+1 -1
View File
@@ -54,7 +54,7 @@ function aksrt_render_menu_icon(string $icon_class, bool $enabled = true): strin
}
// Sprite/inline values must reach aksrt_icon_markup untouched — the legacy
// fa-prefix padding below would corrupt them (zibsvg-hot → fa fas fa-zibsvg-hot).
// FA class padding below would corrupt them (zibsvg-hot → fa fas fa-zibsvg-hot).
if (str_starts_with($icon_class, 'zibsvg-')) {
return aksrt_icon_markup($icon_class, 'icon mr3 menu-item-icon');
}
@@ -78,10 +78,10 @@ $percentage = max(0, min(100, (int) ($progress['progress'] ?? 0)));
<div class="aksrt-level-card__growth-inner">
<div class="aksrt-level-card__growth-head">
<div>
<strong><i class="fa fas fa-line-chart" aria-hidden="true"></i><?php esc_html_e('成长体系', 'aksrt'); ?></strong>
<strong><i class="fa fas fa-chart-line" aria-hidden="true"></i><?php esc_html_e('成长体系', 'aksrt'); ?></strong>
<a class="muted-2-color" data-aksrt-level-tab="tab_integral_my" href="#tab_integral_my"><?php printf(esc_html__('我的经验值:%d', 'aksrt'), $integral); ?></a>
</div>
<a id="tab-integral-date-link" class="muted-2-color" data-aksrt-level-tab="tab_integral_date" href="#tab_integral_date"><?php echo $today_integral > 0 ? esc_html(sprintf(__('今日 +%d', 'aksrt'), $today_integral)) : esc_html__('查看详情', 'aksrt'); ?><i class="fa fa-angle-right ml6" aria-hidden="true"></i></a>
<a id="tab-integral-date-link" class="muted-2-color" data-aksrt-level-tab="tab_integral_date" href="#tab_integral_date"><?php echo $today_integral > 0 ? esc_html(sprintf(__('今日 +%d', 'aksrt'), $today_integral)) : esc_html__('查看详情', 'aksrt'); ?><i class="fa fas fa-angle-right ml6" aria-hidden="true"></i></a>
</div>
<div class="swiper-container swiper-scroll pillar-box aksrt-level-growth" scroll-nogroup="1">
<div class="swiper-wrapper">
@@ -141,7 +141,7 @@ $percentage = max(0, min(100, (int) ($progress['progress'] ?? 0)));
</div>
<div class="tab-pane fade" id="tab_integral_my" role="tabpanel" aria-labelledby="tab-integral-my" hidden>
<?php if ($today_integral > 0) : ?>
<div class="border-bottom padding-h10 aksrt-level-list-row"><div class="flex jsb ac"><div class="flex1 mr20"><div class="font-bold mb6"><?php printf(esc_html__('今日累计 +%d', 'aksrt'), $today_integral); ?></div><div class="muted-2-color em09"><?php echo $day_max > $today_integral ? esc_html(sprintf(__('每日最多获得 %d 经验', 'aksrt'), $day_max)) : esc_html__('今日累计已达到上限', 'aksrt'); ?></div></div><a class="muted-2-color shrink0" data-aksrt-level-tab="tab_integral_date" href="#tab_integral_date" role="tab" aria-controls="tab_integral_date"><?php esc_html_e('每日详情', 'aksrt'); ?><i class="fa fa-angle-right ml6" aria-hidden="true"></i></a></div></div>
<div class="border-bottom padding-h10 aksrt-level-list-row"><div class="flex jsb ac"><div class="flex1 mr20"><div class="font-bold mb6"><?php printf(esc_html__('今日累计 +%d', 'aksrt'), $today_integral); ?></div><div class="muted-2-color em09"><?php echo $day_max > $today_integral ? esc_html(sprintf(__('每日最多获得 %d 经验', 'aksrt'), $day_max)) : esc_html__('今日累计已达到上限', 'aksrt'); ?></div></div><a class="muted-2-color shrink0" data-aksrt-level-tab="tab_integral_date" href="#tab_integral_date" role="tab" aria-controls="tab_integral_date"><?php esc_html_e('每日详情', 'aksrt'); ?><i class="fa fas fa-angle-right ml6" aria-hidden="true"></i></a></div></div>
<?php endif; ?>
<?php if ($details) : ?>
<?php foreach ($details as $row) :
+5 -3
View File
@@ -384,7 +384,8 @@ class PayOrders {
$days = isset($_POST['days']) ? max(1, min(3650, (int) $_POST['days'])) : 14;
$deleted = 0;
if (class_exists('\AKSRT\Payment\OrderRepository')) {
$deleted = \AKSRT\Payment\OrderRepository::get_instance()->clear_closed_orders($days);
$repo = \AKSRT\Payment\OrderRepository::get_instance();
$deleted = $repo->clear_closed_orders($days);
}
wp_safe_redirect($this->page_url('aksrt_orders', [
'aksrt_pay_msg' => $deleted > 0 ? 'orders_cleared' : 'orders_clear_empty',
@@ -401,11 +402,12 @@ class PayOrders {
check_admin_referer('aksrt_pay_delete_order_' . $order_number);
$ok = false;
if ($order_number !== '' && class_exists('\AKSRT\Payment\OrderRepository')) {
$order = \AKSRT\Payment\OrderRepository::get_instance()->get($order_number);
$repo = \AKSRT\Payment\OrderRepository::get_instance();
$order = $repo->get($order_number);
$status = is_array($order) ? (string) ($order['status'] ?? '') : '';
// Only allow hard-delete of closed rows (safety).
if ($status === 'closed') {
$ok = \AKSRT\Payment\OrderRepository::get_instance()->delete_order($order_number);
$ok = $repo->delete_order($order_number);
}
}
wp_safe_redirect($this->page_url('aksrt_orders', [
+1 -1
View File
@@ -102,7 +102,7 @@ class SchemaPayment {
['key' => 'vip_2_points_default', 'label' => '二级会员积分售价(积分)', 'type' => 'number', 'default' => 0, 'min' => 0],
['key' => 'pay_price_default', 'label' => '执行价', 'type' => 'input', 'sanitize_type' => 'money_decimal', 'default' => '0.01', 'suffix' => '元'],
['key' => 'pay_original_price_default', 'label' => '原价', 'type' => 'input', 'sanitize_type' => 'money_decimal', 'default' => '', 'suffix' => '元', 'help' => '显示在执行价格前面,并划掉'],
['key' => 'pay_promotion_tag_default', 'label' => '促销标签', 'type' => 'textarea', 'sanitize_type' => 'html', 'default' => '<i class="fa fa-fw fa-bolt"></i> 限时特惠', 'rows' => 1],
['key' => 'pay_promotion_tag_default', 'label' => '促销标签', 'type' => 'textarea', 'sanitize_type' => 'html', 'default' => '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', 'rows' => 1],
['key' => 'vip_1_price_default', 'label' => '一级会员价格', 'type' => 'input', 'sanitize_type' => 'money_decimal', 'default' => '0', 'suffix' => '元', 'help' => '填0则为一级会员免费'],
['key' => 'vip_2_price_default', 'label' => '二级会员价格', 'type' => 'input', 'sanitize_type' => 'money_decimal', 'default' => '0', 'suffix' => '元', 'help' => '填0则为二级会员免费'],
['key' => 'pay_rebate_discount', 'label' => '推广折扣', 'type' => 'input', 'sanitize_type' => 'money_decimal', 'default' => '0', 'suffix' => '元', 'help' => '通过推广链接购买,额外优惠的金额;需开启推广返佣功能,且不能超过实际购买价'],
+2 -2
View File
@@ -251,7 +251,7 @@ trait VipServiceModal {
. '<input type="hidden" name="order_type" value="4">'
. '<input type="hidden" name="action" value="points_initiate_pay">'
. '<button type="submit" class="mt6 but jb-yellow btn-block radius padding-lg" data-aksrt-vip-points-submit>'
. '<i class="fa fa-check" aria-hidden="true"></i>' . esc_html__('立即兑换', 'aksrt')
. '<i class="fa fas fa-check" aria-hidden="true"></i>' . esc_html__('立即兑换', 'aksrt')
. '</button></div>';
}
@@ -268,7 +268,7 @@ trait VipServiceModal {
. '<svg class="icon" aria-hidden="true"><use xlink:href="#icon-points"></use></svg> '
. esc_html__('我的积分', 'aksrt') . '</span>'
. '<span class="font-bold ml6 em14"><span class="c-green mr10">' . $user_points . '</span>'
. '<i class="fa fa-angle-right" aria-hidden="true"></i></span></a></div>';
. '<i class="fa fas fa-angle-right" aria-hidden="true"></i></span></a></div>';
$lists = '<div class="muted-2-color mb6 em09">' . esc_html__('请选择需兑换的会员', 'aksrt') . '</div>'
. '<div class="flex hh exchange-card-box">' . $cards . '</div>';
+5
View File
@@ -70,6 +70,10 @@ trait DBMigratorPart2 {
) ENGINE=InnoDB {$charset_collate};";
dbDelta($sql_asset_entry);
// 后台「资金流水」页的默认查询是 WHERE 1=1 ORDER BY created_at DESC, id DESC LIMIT n
// 外加同条件的 COUNT(*),且界面上的日期区间筛选也只有 created_at 可用。
// 索引必须能以 created_at 为前导列,否则每次翻页都是全表扫描 + filesort,
// 而 idx_flow_event_time 的前导列是 event_type(筛选时可空),救不了这两条查询。
$sql_financial_flow = "CREATE TABLE {$prefix}aksrt_financial_flow (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
flow_number VARCHAR(32) NOT NULL,
@@ -87,6 +91,7 @@ trait DBMigratorPart2 {
UNIQUE KEY uniq_flow_number (flow_number),
UNIQUE KEY uniq_flow_operation (operation_key),
UNIQUE KEY uniq_flow_ledger_transaction (ledger_transaction_id),
KEY idx_flow_created (created_at),
KEY idx_flow_event_time (event_type, created_at),
KEY idx_flow_order (order_number),
KEY idx_flow_withdrawal (withdrawal_number),
+22 -5
View File
@@ -11,24 +11,41 @@ defined('ABSPATH') || exit;
* 统一业务流水号分配器。
*
* 订单号、资金流水号、提现单号共用同一套号码规则,不再按业务类型区分,
* 也不再存在任何可配置前缀:{YmdHis}{6 位随机数},共 20 字符。
* 也不再存在任何可配置前缀:{YmdHis}{8 位随机数},共 22 字符。
* 该长度同时满足数据库 VARCHAR(32) 列与各网关 out_trade_no ≤32 字符的限制。
*
* 后缀为什么是 8 位纯数字(而不是等长的 base36):
* - 同一秒内的取值空间从 10^6 提升到 10^8(+6.6 bit),撞号频率随之下降
* 两个数量级,规避高并发下单时的重试噪音;
* - 保持「纯数字、定长」形态,号码仍可被运营从截图或邮件里整段复制、
* 电话口述,不引入 0/O、1/I 之类的大小写与字形歧义;
* - 表默认排序规则是 utf8mb4_*_ci(大小写不敏感),一旦混入大小写字母,
* 唯一索引会把 aB 与 Ab 判为重复行,反而凭空制造冲突;
* - 22 字符拼上重试后缀(P999)后 26 字符,仍远低于 32 的上限。
* 碰撞最终由调用侧的重试循环兜底(FinancialFlowService::record 与
* OrderRepositoryCore::create 均为 5 次),此处只负责把概率压低。
*/
final class BusinessNumber {
/** 随机后缀的位数。 */
private const SUFFIX_DIGITS = 8;
/** 随机后缀的上界,等于 10^SUFFIX_DIGITS - 1。 */
private const SUFFIX_MAX = 99999999;
/**
* 生成一个统一流水号。
*
* @param \DateTimeInterface|null $now 分配时刻,默认取站内时区的当前时间。
* @param int|null $random 0-999999 的随机后缀,默认随机取值。
* @param int|null $random 0-99999999 的随机后缀,默认随机取值。
*/
public static function allocate(?\DateTimeInterface $now = null, ?int $random = null): string {
if ($now === null) {
$now = new \DateTimeImmutable('now', wp_timezone());
}
$random = $random ?? random_int(0, 999999);
$random = max(0, min(999999, $random));
$random = $random ?? random_int(0, self::SUFFIX_MAX);
$random = max(0, min(self::SUFFIX_MAX, $random));
return $now->format('YmdHis') . str_pad((string) $random, 6, '0', STR_PAD_LEFT);
return $now->format('YmdHis') . str_pad((string) $random, self::SUFFIX_DIGITS, '0', STR_PAD_LEFT);
}
}
+38 -3
View File
@@ -69,9 +69,16 @@ final class FinancialFlowQuery {
$params = [];
$keyword = trim(sanitize_text_field((string) ($args['keyword'] ?? '')));
if ($keyword !== '') {
$like = '%' . $wpdb->esc_like($keyword) . '%';
$where[] = '(f.flow_number LIKE %s OR f.operation_key LIKE %s OR f.order_number LIKE %s OR f.withdrawal_number LIKE %s OR f.provider_transaction_id LIKE %s OR f.ref_key LIKE %s)';
array_push($params, $like, $like, $like, $like, $like, $like);
if ($this->keyword_hits_identifier($keyword)) {
// 命中完整标识符:走等值查询。各列都有索引,避免 6 路 LIKE '%kw%' 全表扫描。
$where[] = '(f.flow_number = %s OR f.operation_key = %s OR f.order_number = %s OR f.withdrawal_number = %s OR f.provider_transaction_id = %s OR f.ref_key = %s)';
array_push($params, $keyword, $keyword, $keyword, $keyword, $keyword, $keyword);
} else {
// 未精确命中(含前缀、部分匹配、含中文或短关键字):退回模糊匹配。
$like = '%' . $wpdb->esc_like($keyword) . '%';
$where[] = '(f.flow_number LIKE %s OR f.operation_key LIKE %s OR f.order_number LIKE %s OR f.withdrawal_number LIKE %s OR f.provider_transaction_id LIKE %s OR f.ref_key LIKE %s)';
array_push($params, $like, $like, $like, $like, $like, $like);
}
}
$event_type = sanitize_key((string) ($args['event_type'] ?? ''));
if ($event_type !== '') {
@@ -153,6 +160,34 @@ final class FinancialFlowQuery {
return $map;
}
/**
* 关键字是否精确命中某个标识符列。
*
* 业务流水号统一为 {YmdHis}{8 位随机数},运营通常是从截图或邮件里整段复制,
* 此时等值查询可以走索引。若没有精确命中,则退回 LIKE 模糊匹配,保证
* 「只输入前缀也能搜到」的既有行为不变。
*/
private function keyword_hits_identifier(string $keyword): bool {
global $wpdb;
// 只对像标识符的输入启用:纯 ASCII 字母数字、长度 8-191。
if (!preg_match('/^[A-Za-z0-9_-]{8,191}$/', $keyword)) {
return false;
}
$found = $wpdb->get_var($wpdb->prepare(
"SELECT 1 FROM {$this->flow_table()}
WHERE flow_number = %s OR operation_key = %s OR order_number = %s
OR withdrawal_number = %s OR provider_transaction_id = %s OR ref_key = %s
LIMIT 1",
$keyword,
$keyword,
$keyword,
$keyword,
$keyword,
$keyword
));
return $found !== null;
}
private function flow_table(): string {
return \AKSRT\Core\DBMigrator::table('financial_flow');
}
+61 -9
View File
@@ -9,6 +9,27 @@ defined('ABSPATH') || exit;
trait OrderRepositoryCore {
/**
* 建单时对「订单号唯一键冲突」的重试次数。
*
* 统一流水号规则为 {YmdHis}{8 位随机数},同一秒内的取值空间为 10^8,
* 高并发下单仍可能因随机后缀碰撞触发 uniq_order_number 冲突。资金流水侧
* (FinancialFlowService::record)已有等价的 5 次重试,订单侧此前缺失,
* 碰撞时会直接把错误抛给用户,故在此对齐。
*/
private const ORDER_NUMBER_MAX_ATTEMPTS = 5;
/**
* 调用方自带订单号的合法形态:全数字,14 位时间前缀 + 定长数字后缀。
*
* 号码只有一种来源(本类的 allocate_order_number()),所以自带号必须落在
* 分配器能产出的范围内:22 位是当前规则(见 BusinessNumber),20 位是扩位
* 之前的历史规则——导入旧数据、旧单重放仍可能带它进来,故一并放行。
* 带前缀、UUID、非数字、超长等一律视为不受控输入:弃用并回落到分配器,
* 避免把异常长度写进 VARCHAR(32) 列后再去污染检索与对账。
*/
private const ORDER_NUMBER_PATTERN = '/^\d{20}$|^\d{22}$/';
private function table(): string {
@@ -27,7 +48,18 @@ trait OrderRepositoryCore {
public function create(array $data): string {
global $wpdb;
$order_number = $data['order_number'] ?? $this->allocate_order_number();
// 自带订单号先过形态校验:只接受分配器能产出的「全数字、20 或 22 位」。
// 不合规的一律弃用并回落到分配器(WP_DEBUG 下留痕),既不把非法长度写进库,
// 也不因此抛错中断调用方。
$declared_number = isset($data['order_number']) && is_scalar($data['order_number'])
? trim((string) $data['order_number'])
: '';
$caller_supplied = $declared_number !== ''
&& (bool) preg_match(self::ORDER_NUMBER_PATTERN, $declared_number);
if ($declared_number !== '' && !$caller_supplied && defined('WP_DEBUG') && WP_DEBUG) {
error_log(sprintf('[AKSRT Order] 弃用不合规的自带订单号:%s', $declared_number));
}
$order_number = $caller_supplied ? $declared_number : $this->allocate_order_number();
// Preserve JSON pay meta (VIP/content/coupon). Only strip tags for plain text notes.
$note_raw = (string) ($data['note'] ?? '');
@@ -84,10 +116,27 @@ trait OrderRepositoryCore {
$formats[] = '%s';
}
$wpdb->insert($this->table(), $row, $formats);
// 只有本方法自己分配的号码才允许换号重试。调用方显式传入的合规订单号
// (导入、补单、幂等重放)必须原样使用,冲突就应失败,不得悄悄换号。
$max_attempts = $caller_supplied ? 1 : self::ORDER_NUMBER_MAX_ATTEMPTS;
if ($wpdb->insert_id) {
return $order_number;
for ($attempt = 1; $attempt <= $max_attempts; $attempt++) {
$row['order_number'] = $order_number;
$wpdb->insert($this->table(), $row, $formats);
if ($wpdb->insert_id) {
return $order_number;
}
// 只对订单号唯一键冲突换号重试;request_key 冲突等属于调用方的
// 幂等语义,换号重试会掩盖重复提交,必须原样失败。
if ($attempt >= $max_attempts
|| stripos((string) $wpdb->last_error, 'uniq_order_number') === false
) {
return '';
}
$order_number = $this->allocate_order_number();
}
return '';
@@ -586,11 +635,14 @@ trait OrderRepositoryCore {
$days = max(1, min(3650, $days));
$table = $this->table();
$cutoff = gmdate('Y-m-d H:i:s', time() - ($days * DAY_IN_SECONDS));
$sql = "DELETE FROM {$table}
WHERE status = 'closed'
AND cancelled_at IS NOT NULL
AND cancelled_at < %s";
$deleted = $wpdb->query($wpdb->prepare($sql, $cutoff));
$deleted = $wpdb->query($wpdb->prepare(
"DELETE FROM {$table}
WHERE status = 'closed'
AND cancelled_at IS NOT NULL
AND cancelled_at < %s",
$cutoff
));
return $deleted === false ? 0 : (int) $deleted;
}
@@ -125,7 +125,7 @@ final class ContentRenderer {
if (is_wp_error($href)) {
continue;
}
$icon = '<i class="fa fa-folder-open mr6" aria-hidden="true"></i>';
$icon = '<i class="fa fas fa-folder-open mr6" aria-hidden="true"></i>';
$card = [
'type' => (string) ($instance['type'] ?? 'style-1'),
'class' => 'mb10',
@@ -407,7 +407,7 @@ final class GeneralRenderer {
}
$target = $link['target'] === '_blank' ? ' target="_blank" rel="noopener noreferrer"' : '';
$cta_html = $cta_text !== '' && $url !== ''
? '<a class="zci-cta inflex ac gap6 ' . esc_attr(sanitize_html_class($cta_color)) . '" href="' . $url . '"' . $target . '><span>' . esc_html($cta_text) . '</span><i class="fa fa-arrow-right zci-arrow" aria-hidden="true"></i></a>'
? '<a class="zci-cta inflex ac gap6 ' . esc_attr(sanitize_html_class($cta_color)) . '" href="' . $url . '"' . $target . '><span>' . esc_html($cta_text) . '</span><i class="fa fas fa-arrow-right zci-arrow" aria-hidden="true"></i></a>'
: '';
$inner = '<div class="zci-body flex xx flex1">' . $icon_html;
+12 -7
View File
@@ -4,24 +4,29 @@ use AKSRT\Finance\BusinessNumber;
use PHPUnit\Framework\TestCase;
final class BusinessNumberTest extends TestCase {
public function test_allocate_uses_site_time_and_six_digit_random_suffix(): void {
public function test_allocate_uses_site_time_and_eight_digit_random_suffix(): void {
$time = new DateTimeImmutable('2026-08-25 13:14:15', new DateTimeZone('Asia/Shanghai'));
self::assertSame('20260825131415000007', BusinessNumber::allocate($time, 7));
self::assertSame('20260825131415999999', BusinessNumber::allocate($time, 999999));
self::assertSame('2026082513141500000007', BusinessNumber::allocate($time, 7));
self::assertSame('2026082513141599999999', BusinessNumber::allocate($time, 99999999));
}
public function test_allocate_is_prefix_free_and_within_gateway_limit(): void {
$time = new DateTimeImmutable('2026-08-25 13:14:15', new DateTimeZone('Asia/Shanghai'));
$number = BusinessNumber::allocate($time, 0);
self::assertSame('20260825131415000000', $number);
self::assertSame('2026082513141500000000', $number);
// 订单号 / 流水号 / 提现单号共用同一长度,须同时满足 VARCHAR(32) 与网关 out_trade_no ≤32。
self::assertSame(20, strlen($number));
self::assertSame(22, strlen($number));
self::assertLessThanOrEqual(32, strlen($number));
// 必须保持「纯数字定长」:号码会被整段复制与口述,混入字母会带来字形歧义,
// 且表默认排序规则大小写不敏感,字母会让唯一索引把 aB 与 Ab 判为重复。
self::assertTrue(ctype_digit($number));
// 拼上重试后缀(P999)后仍须落在微信等网关的 32 字符限制内。
self::assertLessThanOrEqual(32, strlen($number) + strlen('P999'));
}
public function test_allocate_clamps_out_of_range_random_suffix(): void {
$time = new DateTimeImmutable('2026-01-01 00:00:00', new DateTimeZone('Asia/Shanghai'));
self::assertSame('20260101000000999999', BusinessNumber::allocate($time, 1000000));
self::assertSame('20260101000000000000', BusinessNumber::allocate($time, -5));
self::assertSame('2026010100000099999999', BusinessNumber::allocate($time, 100000000));
self::assertSame('2026010100000000000000', BusinessNumber::allocate($time, -5));
}
}
@@ -28,6 +28,13 @@ const scripts = join([
'src/Core/ScriptsPart1b.php',
'src/Core/ScriptsPart1c.php',
]);
// The Font Awesome handle is registered inside aksrt_enqueue_fontawesome(), so the
// 5.15.4 asset contract has to follow that indirection rather than the enqueue call site.
const iconRuntime = join([
'src/Core/ScriptsPart1b.php',
'src/Core/ScriptsPart1c.php',
'inc/helpers/aksrt-icons.php',
]);
const header = join([
'header.php',
'inc/helpers/header/aksrt-header-render.php',
@@ -42,7 +49,8 @@ test('the public dependency graph uses WordPress jQuery with the local runtime s
assert.doesNotMatch(scripts, /wp_deregister_script\('jquery'\)/);
assert.doesNotMatch(scripts, /jquery\.min\.js'[\s\S]*?'1\.9\.1'/);
assert.match(scripts, /bootstrap\.min\.css'[\s\S]*?'3\.4\.1'/);
assert.match(scripts, /fontawesome5'[\s\S]*?all\.min\.css'[\s\S]*?'5\.15\.4'/);
assert.match(scripts, /aksrt_enqueue_fontawesome\(/);
assert.match(iconRuntime, /fontawesome5\/css\/all\.min\.css'[\s\S]*?'5\.15\.4'/);
assert.match(scripts, /main\.min\.css/);
assert.match(scripts, /loader\.js'[\s\S]*?\['jquery', 'bootstrap'\]/);
assert.match(read('assets/js/runtime/loader.js'), /tbquire\(\['main'\]\)/);
@@ -107,6 +115,9 @@ test('Font Awesome 5 is the only icon contract across every runtime path', () =>
'fa-chevron', 'fa-icon', 'fa-icon-picker', 'fa-icons', 'fa-input',
'fa-mask', 'fa-spin-hover', 'fa-toggle-theme', 'fa-xxx',
]);
// The theme ships its own SVG sprite family, so fa-zibsvg-<key> is a project
// icon rather than a Font Awesome glyph — allow the prefix instead of every key.
const projectFamily = /^fa-zibsvg-/;
assert.doesNotMatch(runtime, /\bfa fa-/);
assert.doesNotMatch(runtime, /FontAwesome|font-awesome\.min\.css|Font Awesome 4|4\.7\.0/);
@@ -114,7 +125,7 @@ test('Font Awesome 5 is the only icon contract across every runtime path', () =>
assert.doesNotMatch(iconCss, /font:\s*normal\s+normal\s+normal\s+[^;]+Font Awesome 5 Free/);
assert.equal(exists('assets/css/font-awesome.min.css'), false);
for (const token of iconTokens) {
if (utility.test(token) || custom.has(token)) continue;
if (utility.test(token) || custom.has(token) || projectFamily.test(token)) continue;
assert.ok(fa5.includes(`.${token}:before`), `${token} is not available in Font Awesome 5.15.4`);
}
});
@@ -104,3 +104,65 @@ test('user withdrawal history uses one paginated ten-item endpoint', () => {
assert.match(styles, /\.uc-withdraw-history__item\s*\{[\s\S]*?display: flex/)
assert.match(styles, /\.uc-withdraw-history-modal__dialog\s*\{[\s\S]*?overflow: hidden/)
})
test('flow table is indexed for the admin listing and date range filters', () => {
const schema = read('src/Core/DBMigratorPart2.php')
// 后台默认视图是 WHERE 1=1 ORDER BY created_at DESC, id DESC LIMIT n,外加同条件的
// COUNT(*),界面上的日期区间筛选也只有 created_at 可用;created_at 必须是索引前导列,
// 否则每次翻页都是全表扫描 + filesort。
assert.match(schema, /KEY idx_flow_created \(created_at\),/)
// 复合索引前导列是 event_type(筛选时可空),替代不了上面那条。
assert.match(schema, /KEY idx_flow_event_time \(event_type, created_at\),/)
})
test('flow retention and archive were deliberately removed', () => {
// 归档方案(归档表 + 每日清理 cron)已移除:一条 created_at 索引覆盖了它要解决的问题,
// 而它会在财务表上执行 DELETE,并需要额外的归档浏览入口才闭环。
assert.doesNotMatch(read('src/Core/DBMigratorPart2.php'), /financial_flow_archive/)
assert.doesNotMatch(read('src/Core/DBMigratorPart3.php'), /financial_flow_archive/)
assert.doesNotMatch(read('functions.php'), /FinancialFlowRetention/)
assert.equal(fs.existsSync(path.join(root, 'src/Finance/FinancialFlowRetention.php')), false)
})
test('admin flow search uses an equality lookup for full identifiers', () => {
const query = read('src/Finance/FinancialFlowQuery.php')
assert.match(query, /private function keyword_hits_identifier\(string \$keyword\): bool/)
assert.match(query, /if \(\$this->keyword_hits_identifier\(\$keyword\)\)/)
// 精确分支走等值(各列有索引),模糊分支保留 LIKE 兜底,前缀搜索行为不变。
assert.match(query, /f\.flow_number = %s OR f\.operation_key = %s/)
assert.match(query, /f\.flow_number LIKE %s OR f\.operation_key LIKE %s/)
})
test('order creation retries only on order-number unique key collisions', () => {
const repository = read('src/Payment/OrderRepositoryCore.php')
assert.match(repository, /private const ORDER_NUMBER_MAX_ATTEMPTS = \d+;/)
assert.match(repository, /for \(\$attempt = 1; \$attempt <= \$max_attempts; \$attempt\+\+\)/)
// 只对订单号唯一键冲突重试;request_key 等冲突属于调用方幂等语义,必须原样失败。
assert.match(repository, /stripos\(\(string\) \$wpdb->last_error, 'uniq_order_number'\)/)
// 调用方显式传入的合规订单号不得换号重试。
assert.match(repository, /\$max_attempts = \$caller_supplied \? 1 : self::ORDER_NUMBER_MAX_ATTEMPTS;/)
})
test('caller-supplied order numbers are constrained to allocator shapes', () => {
const repository = read('src/Payment/OrderRepositoryCore.php')
// 22 位是当前规则,20 位兼容扩位前的历史号码(导入旧数据 / 旧单重放)。
// 用精确字符串断言:正则字面量里转义 \d 与花括号极易写错,会静默失配。
assert.ok(repository.includes("ORDER_NUMBER_PATTERN = '/^\\d{20}$|^\\d{22}$/';"))
// 非标量(数组 / 对象)不得进入 (string) 转换,否则会写入 "Array"。
assert.match(repository, /is_scalar\(\$data\['order_number'\]\)/)
// 不合规时必须回落到分配器,而不是原样写库。
assert.match(repository, /\$order_number = \$caller_supplied \? \$declared_number : \$this->allocate_order_number\(\);/)
// 弃用不受控号码时在 WP_DEBUG 下留痕。
assert.match(repository, /error_log\(sprintf\('\[AKSRT Order\] 弃用不合规的自带订单号:%s'/)
})
test('order cleanup no longer guards on financial records', () => {
const repository = read('src/Payment/OrderRepositoryCore.php')
const payOrders = read('src/Admin/Native/PayOrders.php')
assert.match(repository, /public function clear_closed_orders\(int \$days = 14\): int/)
assert.match(repository, /public function delete_order\(string \$order_number\): bool/)
for (const source of [repository, payOrders]) {
assert.doesNotMatch(source, /has_financial_records|count_closed_orders_with_records|financial_records_exists_sql/)
assert.doesNotMatch(source, /orders_clear_blocked|order_delete_blocked/)
}
})
@@ -95,7 +95,9 @@ test('会员弹窗内的入口为参考主题同款按钮并开本地弹窗', ()
// 参考主题 zibpay_get_pay_uservip_modal 的 class 与图标顺序。
assert.match(vipJs, /node\("a", "btn-block but mb10 padding-lg c-green mini-radius"\)/);
assert.match(vipJs, /#icon-points-color/);
assert.match(vipJs, /fa fa-angle-right em12 ml10/);
// 图标顺序与 class 串保持参考主题一致;FA5 样式类 `fas` 由图标契约(
// legacy-navigation-contract 的 FA5-only 约束)强制要求,故在此一并断言。
assert.match(vipJs, /fa fas fa-angle-right em12 ml10/);
assert.match(vipJs, /data-aksrt-vip-points-open/);
assert.match(vipJs, /openPointsModal\(pointsLink\)/);
});