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>
126 lines
6.8 KiB
JavaScript
126 lines
6.8 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* 工单状态机契约:
|
|
* 1) 迁移规则集中在 TicketStatusService::can_transition(),上层不各自判合法性;
|
|
* 2) 回复会把工单推到「已回复 / 待回复」,管理员「接收」只能从待接单出发;
|
|
* 3) 已完结状态不可再流转,也不可从未接单一步跳到关闭;
|
|
* 4) 催单冷却用 transient,关闭走通知;
|
|
* 5) 默认状态在后台被拒绝改删,非默认状态带工单时也拒删。
|
|
*/
|
|
|
|
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');
|
|
|
|
test('transition rules live in one place', () => {
|
|
const service = read('src/Ticket/TicketStatusService.php');
|
|
const body = service.slice(service.indexOf('public static function can_transition'));
|
|
|
|
// 目标必须存在;同状态幂等;已完结不可流转;待接单不可直接关闭。
|
|
assert.match(body, /if \(\$to === '' \|\| !self::exists\(\$to\)\) \{\s*\n\s*return false;/);
|
|
assert.match(body, /if \(\$from === \$to\) \{\s*\n\s*return true;/);
|
|
assert.match(body, /if \(self::is_closed\(\$from\)\) \{\s*\n\s*return false;/);
|
|
assert.match(body, /if \(\$from === self::KEY_PENDING && \$to === self::KEY_CLOSED\) \{\s*\n\s*return false;/);
|
|
|
|
// 上层只调用 can_transition,不复制合法性判断。
|
|
for (const file of ['src/Ticket/TicketStatus.php', 'src/Ticket/TicketRestAdmin.php', 'src/Admin/Native/UsersMenusTicketsDetail.php']) {
|
|
const src = read(file);
|
|
assert.ok(src.includes('can_transition'), `${file} must delegate to can_transition`);
|
|
}
|
|
});
|
|
|
|
test('reply and receive drive the documented state changes', () => {
|
|
const status = read('src/Ticket/TicketStatus.php');
|
|
|
|
// 管理员回复 → 已回复;用户回复 → 待回复。
|
|
assert.match(status, /\$target = \$author_type === 'admin'\s*\n\s*\? TicketStatusService::KEY_ADMIN_REPLIED\s*\n\s*: TicketStatusService::KEY_USER_REPLIED;/);
|
|
// 同状态不重复迁移;已完结工单上的回复不改变状态。
|
|
assert.match(status, /if \(\$from === \$target\) \{\s*\n\s*return true;/);
|
|
assert.match(status, /if \(TicketStatusService::is_closed\(\$from\)\) \{\s*\n\s*return false;/);
|
|
|
|
// 「接收」只允许从未接单转处理中。
|
|
assert.match(status, /if \(\(string\) \(\$ticket\['status'\] \?\? ''\) !== TicketStatusService::KEY_PENDING\) \{\s*\n\s*return false;/);
|
|
assert.match(status, /return \$this->transition_ticket\(\$ticket_id, \$ticket, TicketStatusService::KEY_HANDLING, 'admin', get_current_user_id\(\)\);/);
|
|
|
|
// 「已解决」落到已关闭。
|
|
assert.match(status, /private function resolve_ticket\(int \$ticket_id, array \$ticket\): bool \{\s*\n\s*return \$this->transition_ticket\(\$ticket_id, \$ticket, TicketStatusService::KEY_CLOSED, 'admin', get_current_user_id\(\)\);/);
|
|
});
|
|
|
|
test('closing notifies the counterparty exactly once', () => {
|
|
const status = read('src/Ticket/TicketStatus.php');
|
|
assert.match(status, /if \(\$to === TicketStatusService::KEY_CLOSED\) \{\s*\n\s*TicketNotifier::closed\(\$ticket, \$actor\);/);
|
|
|
|
const notifier = read('src/Ticket/TicketNotifier.php');
|
|
// 关单双分支:管理员关 → 通知用户;用户关 → 通知跟进人。
|
|
assert.match(notifier, /if \(\$closer === 'admin'\) \{/);
|
|
assert.match(notifier, /self::send\(\$user_id, 'ticket_closed'/);
|
|
assert.match(notifier, /self::send\(\$admin_id, 'ticket_closed'/);
|
|
});
|
|
|
|
test('default status cannot be edited or deleted in the admin surface', () => {
|
|
// REST 侧的状态 CRUD 在 TicketConfigRest trait 里,由 TicketRestAdmin 组合。
|
|
// 配置入口已从原生后台迁至 SPA「工单中心」,兜底只在这一处。
|
|
const config = read('src/Ticket/TicketConfigRest.php');
|
|
assert.equal((config.match(/!empty\(\$row\['is_default'\]\)/g) || []).length, 2,
|
|
'both update and delete must honour is_default');
|
|
assert.match(config, /'jio_ticket_status_default'/);
|
|
|
|
// 原生后台不再维护状态定义,因此不再出现 is_default 判断。
|
|
const native = read('src/Admin/Native/UsersMenusTicketsCenter.php');
|
|
assert.ok(!native.includes("!empty(\$row['is_default'])"),
|
|
'status definition guards belong to TicketConfigRest only');
|
|
|
|
// 删除前还要确认没有工单挂在该状态/部门下(守卫在 TicketConfigRest trait 中)。
|
|
assert.match(config, /count_by_status\(\(string\) \$row\['status_key'\]\) > 0/);
|
|
assert.match(config, /count_by_type\(\$id\) > 0/);
|
|
assert.match(config, /'jio_ticket_status_in_use'/);
|
|
assert.match(config, /'jio_ticket_type_in_use'/);
|
|
|
|
const repo = read('src/Ticket/TicketRepository.php');
|
|
assert.match(repo, /public function count_by_status\(string \$status_key\): int/);
|
|
assert.match(repo, /public function count_by_type\(int \$type_id\): int/);
|
|
});
|
|
|
|
test('prereply reorder requires the full id set', () => {
|
|
const service = read('src/Ticket/TicketPrereplyService.php');
|
|
const body = service.slice(service.indexOf('public static function reorder'));
|
|
|
|
// 数量不符或含未知 ID 一律拒绝,避免漏排导致顺序错乱。
|
|
assert.match(body, /count\(\$ids\) !== count\(\$existing\) \|\| array_diff\(\$ids, \$existing\) !== \[\]/);
|
|
assert.match(body, /return false;/);
|
|
// 新建时追加到末尾。
|
|
assert.match(service, /return \(int\) \$max \+ 1;/);
|
|
});
|
|
|
|
test('ticket numbers are unique and settings-driven', () => {
|
|
const number = read('src/Ticket/TicketNumber.php');
|
|
assert.match(number, /TicketSettings::number_prefix\(\)/);
|
|
assert.match(number, /current_time\('Ymd'\)/);
|
|
assert.match(number, /wp_rand\(100000, 999999\)/);
|
|
assert.match(number, /if \(!self::exists\(\$candidate\)\) \{\s*\n\s*return \$candidate;/);
|
|
// 冲突重试有上限,极端情况追加随机后缀兜底。
|
|
assert.match(number, /private const MAX_ATTEMPTS = 12;/);
|
|
|
|
const settings = read('src/Ticket/TicketSettings.php');
|
|
assert.match(settings, /strtoupper\(\$raw\)/);
|
|
assert.match(settings, /sanitize_key/);
|
|
});
|
|
|
|
test('notifications go through the message inbox with deep links', () => {
|
|
const notifier = read('src/Ticket/TicketNotifier.php');
|
|
// 所有通知都走 Message::send(),持久化到 jio_notification。
|
|
assert.match(notifier, /\\Jio\\Messages\\Message::get_instance\(\)->send\(/);
|
|
assert.match(notifier, /class_exists\('\\Jio\\Messages\\Message'\)/);
|
|
// 新建工单通知全体管理员,催单只通知跟进人。
|
|
assert.match(notifier, /get_users\(\['role' => 'administrator', 'fields' => 'ID', 'number' => 20\]\)/);
|
|
assert.match(notifier, /\$admin_id = \(int\) \(\$ticket\['admin_id'\] \?\? 0\);/);
|
|
// 深链分别指向后台工单页与用户中心工单位。
|
|
assert.match(notifier, /admin_url\('users\.php\?page=jio_tickets'\)/);
|
|
assert.match(notifier, /jio_get_user_center_url\('tickets'\)/);
|
|
});
|