Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca9dc7794f | ||
|
|
c06aadb9a9 | ||
|
|
3bec2a5777 | ||
|
|
a6819ec7a5 |
No files matched your search
@@ -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
@@ -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: '通过推广链接购买,额外优惠的金额;需开启推广返佣功能,且不能超过实际购买价' },
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Vendored
+1
-1
@@ -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
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)); }
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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">';
|
||||
|
||||
@@ -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>';
|
||||
}
|
||||
|
||||
@@ -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>';
|
||||
|
||||
@@ -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) :
|
||||
|
||||
@@ -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', [
|
||||
|
||||
@@ -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' => '通过推广链接购买,额外优惠的金额;需开启推广返佣功能,且不能超过实际购买价'],
|
||||
|
||||
@@ -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>';
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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\)/);
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user