'use strict'; /** * 工单表结构契约: * 1) 6 张 jio_ticket* 表全部由 DBMigratorPart2 建出,且被 migrate_ticket_tables() 调用; * 2) SCHEMA_VERSION 已升到当前版本(dbDelta 只在版本变化时重跑); * 3) jio_order_aftersale 已从 schema 与表名映射中彻底移除; * 4) 状态与部门 seeds 幂等,默认状态被标记为不可改删。 */ 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 TABLES = [ 'jio_ticket', 'jio_ticket_reply', 'jio_ticket_status', 'jio_ticket_type', 'jio_ticket_prereply', 'jio_ticket_note', ]; test('schema version is bumped so dbDelta re-runs', () => { const migrator = read('src/Core/DBMigrator.php'); assert.match(migrator, /const SCHEMA_VERSION = '3\.4\.0';/, 'SCHEMA_VERSION must be bumped for new tables/columns'); assert.ok(migrator.includes('use DBMigratorTicket;'), 'DBMigratorTicket trait not composed'); // 版本注释要说明每次升版做了什么,否则后人看不懂升版原因。 assert.match(migrator, /3\.0\.0: 售后(order_aftersale)整体替换为工单系统/); assert.match(migrator, /3\.1\.0: jio_ticket 新增 urge_count \/ last_urge_time/); // 3.3.0 的品牌重命名(aksrt_ → jio_ 表前缀)也必须写进注释。 assert.match(migrator, /3\.3\.0: 主题品牌由 AKSRT 更名为 Jio/); const ticket = read('src/Core/DBMigratorTicket.php'); assert.match(ticket, /urge_count INT UNSIGNED NOT NULL DEFAULT 0,/); assert.match(ticket, /last_urge_time DATETIME DEFAULT NULL,/); }); test('order_aftersale is gone from schema and the table map', () => { const part2 = read('src/Core/DBMigratorPart2.php'); const part3 = read('src/Core/DBMigratorPart3.php'); assert.ok(!part2.includes('jio_order_aftersale'), 'order_aftersale CREATE must be removed'); assert.ok(!part3.includes("'order_aftersale'"), 'order_aftersale table map entry must be removed'); assert.ok(!part3.includes('jio_order_aftersale'), 'order_aftersale table name must be removed'); }); test('six ticket tables are declared and the migrator is invoked', () => { const part2 = read('src/Core/DBMigratorPart2.php'); // Part2 只负责调用;DDL 集中在 DBMigratorTicket,避免 run() 继续膨胀。 assert.ok(part2.includes('self::migrate_ticket_tables($prefix, $charset_collate);'), 'migrate_ticket_tables() must be called from run()'); assert.ok(!part2.includes('jio_order_aftersale'), 'Part2 must not carry the old table'); const ticket = read('src/Core/DBMigratorTicket.php'); for (const table of TABLES) { assert.ok(ticket.includes(`CREATE TABLE {$prefix}${table} (`), `${table} DDL missing in trait`); } assert.ok(ticket.includes('self::seed_ticket_statuses($prefix);'), 'status seeds not invoked'); assert.ok(ticket.includes('self::seed_ticket_types($prefix);'), 'type seeds not invoked'); }); test('ticket table map exposes all six names', () => { const part3 = read('src/Core/DBMigratorPart3.php'); for (const name of ['ticket', 'ticket_reply', 'ticket_status', 'ticket_type', 'ticket_prereply', 'ticket_note']) { assert.ok(part3.includes(`'${name}'`), `table map missing ${name}`); assert.ok(part3.includes(`'jio_${name}'`), `table map missing jio_${name}`); } }); test('ticket columns cover the state machine and optional order link', () => { const ticket = read('src/Core/DBMigratorTicket.php'); // 工单号对外唯一;状态是可自定义的 key;订单关联可空(默认空串)。 assert.match(ticket, /UNIQUE KEY uniq_ticket_num \(ticket_num\)/); assert.match(ticket, /status VARCHAR\(24\) NOT NULL DEFAULT 'pending'/); assert.match(ticket, /order_number VARCHAR\(32\) NOT NULL DEFAULT ''/); assert.match(ticket, /KEY idx_order \(order_number\)/); // 沟通记录独立成行,支持引用回复。 assert.match(ticket, /author_type VARCHAR\(16\) NOT NULL DEFAULT 'user'/); assert.match(ticket, /quote_reply_id BIGINT UNSIGNED NOT NULL DEFAULT 0/); // 状态表带完结/默认两个开关,默认状态不可改删由服务层兜住。 assert.match(ticket, /status_key VARCHAR\(24\) NOT NULL/); assert.match(ticket, /UNIQUE KEY uniq_status_key \(status_key\)/); assert.match(ticket, /is_closed TINYINT\(1\) NOT NULL DEFAULT 0/); assert.match(ticket, /is_default TINYINT\(1\) NOT NULL DEFAULT 0/); }); test('status and department seeds are idempotent', () => { const ticket = read('src/Core/DBMigratorTicket.php'); // 按 key / name 查重后跳过,重复执行不会写重复行。 assert.match(ticket, /\$exists = \$wpdb->get_col\("SELECT status_key FROM \{\$table\}"\)/); assert.match(ticket, /if \(in_array\(\$key, \$exists, true\)\) \{\s*\n\s*continue;/); assert.match(ticket, /\$exists = \$wpdb->get_col\("SELECT name FROM \{\$table\}"\)/); assert.match(ticket, /if \(in_array\(\$label, \$exists, true\)\) \{\s*\n\s*continue;/); // 五个内置状态与五个部门。 for (const key of ['pending', 'handling', 'user_replied', 'admin_replied', 'closed']) { assert.ok(ticket.includes(`['${key}',`), `status seed ${key} missing`); } for (const label of ['技术支持', '账号问题', '财务问题', '产品建议', '其他']) { assert.ok(ticket.includes(`'${label}'`), `department seed ${label} missing`); } // 默认状态是「待接单」,且是完结态的补集。 assert.match(ticket, /\['pending', '待接单', '#f59e0b', 0, 1\]/); assert.match(ticket, /\['closed', '已关闭', '#6b7280', 1, 0\]/); }); test('TicketStatusService seeds mirror the migrator and guard the default row', () => { const service = read('src/Ticket/TicketStatusService.php'); for (const key of ['pending', 'handling', 'user_replied', 'admin_replied', 'closed']) { assert.ok(service.includes(`KEY_${key.toUpperCase()}`), `KEY_${key.toUpperCase()} missing`); } // 状态机只挡确定非法的组合,其余交给「目标状态必须存在」。 assert.match(service, /if \(\$from === self::KEY_PENDING && \$to === self::KEY_CLOSED\) \{\s*\n\s*return false;/); assert.match(service, /if \(self::is_closed\(\$from\)\) \{\s*\n\s*return false;/); // 颜色只接受 #rgb / #rrggbb。 assert.match(service, /'#6b7280'/); assert.match(service, /\\Jio\\Core\\Color::sanitize\(\$color\)/); assert.doesNotMatch(service, /preg_match\('\/\^#/); });