Files
kate522andClaude Opus 5 35bb96e82f refactor: rename the theme brand and every identifier from AKSRT to Jio
Full-project rename. `git grep -i aksrt` is now empty apart from the
DBMigrator 3.3.0 changelog note that documents the rename itself.

Code identifiers
- PHP namespace AKSRT\ -> Jio\ (composer PSR-4 + 378 declarations)
- global constants AKSRT_* -> JIO_* (VERSION/DIR/URI, PAY_TYPE_*, CAPTCHA_*)
- procedural functions, hooks, options, meta keys, table names,
  cookies, query vars, ajax actions and nonces: aksrt_ -> jio_
- text domain aksrt -> jio; Theme Name -> Jio
- JS globals: aksrtData/aksrtAdminData/... -> jioData/jioAdminData/...,
  window.AKSRTCaptcha & friends -> window.JioCaptcha, Aksrt* -> Jio*
- Vue components Aks*.vue -> Jio*.vue (15 files renamed + imports)
- CSS classes/data-attrs/custom properties: aksrt- -> jio-
- REST namespace aksrt/v1 -> jio/v1 (incl. payment gateway callbacks)

Storage layer
- DB tables {prefix}aksrt_* -> {prefix}jio_*; SCHEMA_VERSION 3.2.0 -> 3.3.0
  so dbDelta creates the new names on install
- widget id_base aksrt_widget_* -> jio_widget_*
- uninstall.php drop list rebuilt from the actual migrator tables
  (dropped 3 stale entries, added the 17 missing ones)

Removed
- hardcoded https://api.aksrt.com/version.json default update endpoint;
  get_endpoint() now reads the 「更新端点」 setting and do_check() no-ops
  when it is empty

Files renamed: inc/helpers/aksrt-*.php -> jio-*.php (54),
assets/admin/src/components/form/Aks*.vue -> Jio*.vue (15).
Build artifacts regenerated: assets/css/build, assets/js/runtime,
assets/admin/build.

Verified: audit-psr4-paths errors=0, audit-admin-contracts errors=0
(warnings=56, unchanged baseline), 813/813 widget-system tests,
44/44 phpunit, php -l clean across all PHP.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-01 20:05:04 +08:00

191 lines
10 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
/**
* 「工单中心」配置契约:
* 1) PHP SchemaUsersTicket 分区与 SPA JS schema 的键、顺序、类型、默认值 1:1 对齐;
* 2) SPA 页面已注册(/users/ticket 可达),承载 schema 表单 + 三块字典 CRUD;
* 3) 运行时 TicketSettings 缺键回退 schema 默认,数值边界被夹取;
* 4) 前台入口三处均已注册:Center.php 面板定义 + sidebar-nav.php 侧栏项 +
* jio-user-center.php 的 built_in_tabs 与 view map。
*/
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
const root = path.resolve(__dirname, '../..');
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8');
const FIELD_ORDER = [
'ticket_s',
'ticket_num_prefix',
'ticket_notice_open',
'ticket_notice_text',
'ticket_attachment_s',
'ticket_attachment_max',
'ticket_order_link_s',
'ticket_urge_interval',
'ticket_title_max',
'ticket_content_max',
'ticket_reply_max',
];
const GROUPS = [
{ key: 'main', title: '工单功能' },
{ key: 'form', title: '提交设置' },
];
test('ticket schema keys match between PHP and SPA JS', () => {
const php = read('src/Admin/Schema/Users/SchemaUsersTicket.php');
const js = read('assets/admin/src/schema/sections/users/ticket.js');
for (const key of FIELD_ORDER) {
assert.ok(php.includes(`'key' => '${key}'`), `PHP schema missing ${key}`);
assert.ok(js.includes(`key: '${key}'`), `JS schema missing ${key}`);
}
// 顺序:两组各自的字段顺序必须一致(前端分组渲染依赖它)。
for (const group of GROUPS) {
const phpGroup = php.slice(php.indexOf(`'key' => '${group.key}'`));
const jsGroup = js.slice(js.indexOf(`key: '${group.key}'`));
const phpKeys = [...phpGroup.matchAll(/'key' => '([a-z_]+)'/g)].map((m) => m[1]);
const jsKeys = [...jsGroup.matchAll(/key: '([a-z_]+)'/g)].map((m) => m[1]);
assert.deepEqual(jsKeys, phpKeys, `group ${group.key} field order drifted`);
}
// 类型逐字段对齐。PHP 里 textarea 带 sanitize_type,取值要跳过它,
// 因此显式匹配 'type' => '...'(前后都是引号边界,不会撞上 sanitize_type)。
for (const key of FIELD_ORDER) {
const phpType = new RegExp(`'key' => '${key}'[^\\]]*'type' => '([a-z]+)'`).exec(php);
const jsType = new RegExp(`key: '${key}'[^}]*?\\btype: '([a-z]+)'`).exec(js);
assert.ok(phpType && jsType, `type not found for ${key}`);
assert.equal(jsType[1], phpType[1], `${key} type drifted`);
}
// sanitize_type 也必须一致:公告内容允许 HTML,两侧都要声明。
assert.ok(php.includes("'key' => 'ticket_notice_text', 'label' => '公告内容', 'type' => 'textarea', 'sanitize_type' => 'html'"),
'PHP notice text must declare sanitize_type html');
assert.ok(js.includes("key: 'ticket_notice_text', label: '公告内容', type: 'textarea', sanitize_type: 'html'"),
'JS notice text must declare sanitize_type html');
});
test('ticket schema is registered under users', () => {
const js = read('assets/admin/src/schema/sections/users/ticket.js');
assert.equal(js.includes("key: 'users.ticket'"), true, 'schema key must be users.ticket');
const index = read('assets/admin/src/schema/sections/users/index.js');
assert.ok(index.includes("import { ticketSchema } from './ticket.js'"), 'schema not imported');
assert.ok(/ticket:\s*ticketSchema/.test(index), 'schema not exported in usersSchemas');
const constants = read('assets/admin/src/constants/sections/users.js');
assert.ok(constants.includes("key: 'users-ticket'"), 'menu page key missing');
assert.ok(constants.includes("route: '/users/ticket'"), 'menu route missing');
assert.ok(constants.includes("section: 'ticket'"), 'menu section missing');
assert.ok(constants.includes("canonicalDomain: 'users'"), 'menu canonical domain missing');
// PHP 侧:分区由 SchemaUsers 组装,SchemaUsersTicket 提供内容。
const users = read('src/Admin/Schema/SchemaUsers.php');
assert.ok(users.includes("'ticket' => Users\\SchemaUsersTicket::build()"), 'PHP section not composed');
// 组装位置必须在 'sections' => [ ... ] 内部。SchemaProvider::get_sections() 只读
// $schema['sections'],分区挂在同级会让 /settings/schema/users/ticket 404
// (SPA 报「未知的设置分组」)。定位 sections 开括号后用配平扫描找闭括号,
// 确认 compose 调用落在区间内——单纯 includes() 查不出层级错误。
const sectionsAt = users.indexOf("'sections' => [");
assert.ok(sectionsAt !== -1, 'sections array not found');
const composeAt = users.indexOf("'ticket' => Users\\SchemaUsersTicket::build()");
assert.ok(composeAt > sectionsAt, 'ticket must be composed after sections opens');
let depth = 0;
let sectionsEnd = -1;
for (let i = users.indexOf('[', sectionsAt); i < users.length; i++) {
if (users[i] === '[') depth++;
else if (users[i] === ']') {
depth--;
if (depth === 0) {
sectionsEnd = i;
break;
}
}
}
assert.ok(sectionsEnd !== -1, 'unbalanced sections array');
assert.ok(composeAt < sectionsEnd, 'ticket section must be INSIDE the sections array');
const ticket = read('src/Admin/Schema/Users/SchemaUsersTicket.php');
assert.ok(ticket.includes("'key' => 'ticket'"), 'PHP section key missing');
assert.ok(ticket.includes("'option_key' => 'jio_settings'"), 'PHP option key missing');
});
test('TicketSettings falls back to schema defaults and clamps numerics', () => {
const settings = read('src/Ticket/TicketSettings.php');
// 只取方法体,避免 docblock 与注释里的同形文字参与断言。
const body = settings.slice(settings.indexOf('public static function all()'));
// 缺键回退:enabled 只在显式关闭时为 false(与 schema default true 对齐)。
assert.match(body, /!isset\(\$s\['ticket_s'\]\) \|\| !empty\(\$s\['ticket_s'\]\)/);
assert.match(body, /self::all\(\)\['ticket_num_prefix'\] \?\? 'T'/);
assert.match(body, /self::all\(\)\['ticket_notice_text'\] \?\? ''\)/);
assert.match(body, /self::all\(\)\['ticket_attachment_max'\] \?\? 5\)/);
assert.match(body, /self::all\(\)\['ticket_urge_interval'\] \?\? 15\)/);
// 数值夹取:附件上限 1..9,催单冷却 1..1440。
assert.match(body, /max\(1, min\(9, \(int\)/);
assert.match(body, /max\(1, min\(1440, \(int\)/);
// 字数上限:标题 10..200(VARCHAR(200)),描述 / 回复 100..16000(TEXT 65535 字节 / utf8mb4)。
assert.match(body, /max\(10, min\(200, \(int\) \(self::all\(\)\['ticket_title_max'\] \?\? 200\)\)\)/);
assert.match(body, /max\(100, min\(16000, \(int\) \(self::all\(\)\['ticket_content_max'\] \?\? 5000\)\)\)/);
assert.match(body, /max\(100, min\(16000, \(int\) \(self::all\(\)\['ticket_reply_max'\] \?\? 3000\)\)\)/);
// 公告文案走 kses(后台允许 HTML)。
assert.match(body, /wp_kses_post/);
// 分区名与 schema key 一致(常量在 all() 之前声明,故对整份文件断言)。
assert.match(settings, /const SECTION = 'ticket'/);
assert.match(body, /read_domain\('users', self::SECTION\)/);
});
test('ticket panel is registered across the user-center wiring', () => {
const center = read('src/User/Center.php');
assert.ok(center.includes("'tickets' => ["), 'panel definition missing');
assert.ok(center.includes("'label' => __('工单中心', 'jio')"), 'panel label missing');
assert.match(center, /'tickets'\s*=>\s*\[[\s\S]*?'priority' => 49/, 'panel priority should sit next to orders (48)');
// 开关关闭时整块移除。
assert.match(center, /\$ticket_on = class_exists\('\\Jio\\Ticket\\TicketSettings'\)/);
assert.match(center, /if \(!\$ticket_on\) \{\s*\n\s*\$order = array_values\(array_filter\(\$order, static fn\(string \$key\): bool => \$key !== 'tickets'\)\);/);
assert.match(center, /if \(\$key === 'tickets' && !\$ticket_on\) \{\s*\n\s*continue;/);
const sidebar = read('inc/helpers/user-center-renderers/sidebar-nav.php');
assert.ok(sidebar.includes("'label' => __('工单中心', 'jio')"), 'sidebar label missing');
assert.ok(sidebar.includes("'icon' => 'comment-color'"), 'sidebar icon should use the built-in comment sprite');
assert.ok(sidebar.includes("'tab' => 'tickets'"), 'sidebar tab key missing');
const helper = read('inc/helpers/jio-user-center.php');
assert.ok(helper.includes("'tickets'"), 'built_in_tabs missing tickets');
assert.match(helper, /'tickets'\s*=> 'tickets',/, 'view map missing tickets => tickets');
const renderer = read('inc/helpers/user-center-renderers/profile-tickets.php');
assert.ok(renderer.includes('data-jio-ticket-center'), 'panel root hook missing');
assert.ok(renderer.includes('data-jio-ticket-create'), 'create button hook missing');
assert.ok(renderer.includes("jio_render_captcha('ticket_create')"), 'create captcha mount missing');
assert.ok(renderer.includes("jio_render_captcha('ticket_reply')"), 'reply captcha mount missing');
});
test('ticket frontend assets are enqueued only when the panel exists', () => {
const scripts = read('src/Core/ScriptsPart1b.php');
assert.match(scripts, /\$has_tickets_panel = false;/, 'panel flag not initialised');
assert.match(scripts, /\$has_tickets_panel = isset\(\$dashboard_tabs\['tickets'\]\);/, 'panel flag not derived');
assert.ok(scripts.includes("'jio-tickets'"), 'ticket CSS handle missing');
assert.ok(scripts.includes("'jio-ticket-center'"), 'ticket JS handle missing');
assert.ok(scripts.includes('/assets/js/ticket-center.js'), 'ticket JS path missing');
assert.ok(scripts.includes('/assets/css/routes/tickets.css'), 'ticket CSS path missing');
// 两个入队都必须在 $has_tickets_panel 条件内。
const cssIndex = scripts.indexOf("'jio-tickets'");
const jsIndex = scripts.indexOf("'jio-ticket-center'");
assert.ok(scripts.lastIndexOf('if ($has_tickets_panel) {', cssIndex) > scripts.indexOf("'jio-orders'"),
'ticket CSS must be gated on its own panel flag');
assert.ok(jsIndex > cssIndex, 'ticket JS should be enqueued alongside its CSS');
const localize = read('src/Core/ScriptsPart1c.php');
assert.ok(localize.includes("'ticket' => (function () {"), 'localize key missing');
assert.ok(!localize.includes("'afterSale'"), 'stale afterSale localize must be gone');
});