Files
zyt/server/tests/RevisitCohortDatabaseTest.php
T
2026-10-17 16:08:24 +08:00

162 lines
13 KiB
PHP

<?php
declare(strict_types=1);
/** 仅测试显式指定的本机临时 MySQL;不 initialize 应用、不读取 .env。 */
require dirname(__DIR__) . '/vendor/autoload.php';
require dirname(__DIR__) . '/vendor/topthink/framework/src/helper.php';
use app\adminapi\logic\stats\RevisitCohortLogic as L;
use app\adminapi\logic\stats\RevisitRateLogic as Original;
use app\adminapi\controller\stats\RevisitRateController;
use app\adminapi\controller\stats\RevisitAssignRateController;
use app\adminapi\controller\stats\RevisitInheritRateController;
use think\Container;
$port = (int) getenv('REVISIT_COHORT_TEST_MYSQL_PORT');
if ($port < 1024 || $port === 3306 || getenv('REVISIT_COHORT_TEST_DISPOSABLE') !== '1') {
throw new RuntimeException('Set REVISIT_COHORT_TEST_DISPOSABLE=1 and an isolated loopback MySQL port');
}
date_default_timezone_set('Asia/Shanghai');
$database = 'revisit_cohort_' . bin2hex(random_bytes(6));
$pdo = new PDO("mysql:host=127.0.0.1;port={$port};charset=utf8mb4", 'root', '', [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
$pdo->exec("CREATE DATABASE `{$database}` CHARACTER SET utf8mb4");
$pdo->exec("USE `{$database}`");
new think\App();
$manager = new think\DbManager();
$manager->setConfig(['default' => 'mysql', 'connections' => ['mysql' => [
'type' => 'mysql', 'hostname' => '127.0.0.1', 'hostport' => $port, 'database' => $database,
'username' => 'root', 'password' => '', 'charset' => 'utf8mb4', 'prefix' => 'zyt_',
'fields_strict' => true, 'trigger_sql' => false,
]]]);
Container::getInstance()->instance('think\DbManager', $manager);
Container::getInstance()->instance('config', new think\Config());
$checks = 0;
$expect = static function (bool $condition, string $message) use (&$checks): void {
if (!$condition) {
throw new RuntimeException('FAIL: ' . $message);
}
$checks++;
};
try {
$pdo->exec('CREATE TABLE zyt_dept(id INT PRIMARY KEY,pid INT,name VARCHAR(50),sort INT DEFAULT 0,delete_time INT NULL)');
$pdo->exec("INSERT INTO zyt_dept(id,pid,name) VALUES(10,0,'一中心'),(11,10,'一组'),(20,0,'二中心'),(21,20,'二中心甲组'),(22,20,'乙组')");
$pdo->exec('CREATE TABLE zyt_admin(id INT PRIMARY KEY,name VARCHAR(50),delete_time INT NULL)');
$pdo->exec("INSERT INTO zyt_admin VALUES(1,'医助甲',NULL),(2,'医助乙',NULL),(3,'一中心员工',NULL),(4,'已删除',1)");
$pdo->exec('CREATE TABLE zyt_admin_dept(admin_id INT,dept_id INT,PRIMARY KEY(admin_id,dept_id))');
$pdo->exec('INSERT INTO zyt_admin_dept VALUES(1,21),(2,22),(3,11),(4,21)');
$pdo->exec('CREATE TABLE zyt_tcm_diagnosis(id INT PRIMARY KEY,patient_name VARCHAR(50),phone VARCHAR(20),revisit_slot_start_offset INT DEFAULT 0,delete_time INT NULL)');
for ($id = 10; $id <= 17; $id++) {
$pdo->prepare('INSERT INTO zyt_tcm_diagnosis(id,patient_name,phone,delete_time) VALUES(?,?,?,?)')
->execute([$id, '虚构患者' . $id, '13800000000', $id === 16 ? 1 : null]);
}
$pdo->exec('CREATE TABLE zyt_tcm_diagnosis_assign_log(id INT PRIMARY KEY AUTO_INCREMENT,diagnosis_id INT,from_assistant_id INT,to_assistant_id INT,operator_admin_id INT DEFAULT 7,create_time INT,is_inherit INT DEFAULT 0,inherit_visit_slot INT DEFAULT 0)');
$pdo->exec('CREATE TABLE zyt_tcm_diagnosis_auto_assign_log(diagnosis_id INT,rollback_time INT,rollback_admin_id INT)');
$pdo->exec('CREATE TABLE zyt_tcm_prescription_order(id INT PRIMARY KEY AUTO_INCREMENT,order_no VARCHAR(50),diagnosis_id INT,create_time INT,amount DECIMAL(10,2) DEFAULT 100,creator_id INT DEFAULT 1,fulfillment_status INT NULL,delete_time INT NULL)');
$lg = $pdo->prepare('INSERT INTO zyt_tcm_diagnosis_assign_log(diagnosis_id,from_assistant_id,to_assistant_id,create_time,is_inherit) VALUES(?,?,?,?,?)');
foreach ([[10,0,1,'2026-09-01',0],[10,1,2,'2026-09-20',1],[11,0,1,'2026-08-01',0],
[12,0,2,'2026-09-03',0],[13,0,3,'2026-09-03',0],[14,0,1,'2026-09-04',0],
[15,0,1,'2026-08-01',0],[15,1,2,'2026-09-15',1],[16,0,1,'2026-09-01',0],
[17,1,1,'2026-09-01',0]] as [$did,$from,$to,$date,$inherit]) {
$lg->execute([$did,$from,$to,strtotime($date),$inherit]);
}
$o = $pdo->prepare('INSERT INTO zyt_tcm_prescription_order(order_no,diagnosis_id,create_time,fulfillment_status) VALUES(?,?,?,?)');
foreach ([[10,'2026-09-02',3],[10,'2026-09-05',3],[10,'2026-10-01',3],[10,'2026-10-05',3],
[11,'2026-08-02',3],[11,'2026-10-01',3],[13,'2026-09-05',3],[13,'2026-09-06',3],
[14,'2026-09-05',3],[14,'2026-09-06',9],[15,'2026-08-02',3],[15,'2026-08-03',3],
[15,'2026-08-04',3],[15,'2026-09-16',3],[15,'2026-09-17',4],[15,'2026-10-02',3]] as $i => [$did,$date,$status]) {
$o->execute(['SYNTHETIC-' . $i,$did,strtotime($date),$status]);
}
$q = ['start_date' => '2026-09-01', 'end_date' => '2026-09-30'];
$invoke = static function (string $class, string $action, array $params): array {
$controller = (new ReflectionClass($class))->newInstanceWithoutConstructor();
$request = (new think\Request())->withGet($params);
$property = (new ReflectionClass(app\BaseController::class))->getProperty('request');
$property->setAccessible(true);
$property->setValue($controller, $request);
return $controller->$action()->getData()['data'];
};
foreach (['exclude', 'slot'] as $mode) {
$legacyParams = ['month' => '2026-09', 'inherit_mode' => $mode];
foreach (['overview', 'assignLines', 'visitOrderLines'] as $action) {
$params = $legacyParams + ['slot' => 0];
$expect($invoke(RevisitRateController::class, $action, $params) === Original::$action($params),
'原Controller实际响应与原算法一致:' . $mode . '/' . $action);
}
}
$legacyBaseline = Original::overview(['month' => '2026-09', 'inherit_mode' => 'exclude']);
$expect($legacyBaseline['total']['assigned_count'] === 3 && $legacyBaseline['total']['visit4_count'] === 1,
'原算法保留原自指派分母及当月继承订单分子规则');
$expect($invoke(RevisitRateController::class, 'deptOptions', []) === Original::deptOptions(), '原部门下拉契约不增加新报表字段');
$new = L::overview($q);
$expect($invoke(RevisitAssignRateController::class, 'overview', $q + ['cohort_type' => 'inherit'])['cohort_type'] === 'new', '新指派入口固定new且使用独立Controller');
$expect($invoke(RevisitInheritRateController::class, 'overview', $q + ['cohort_type' => 'new'])['cohort_type'] === 'inherit', '继承入口固定inherit');
$expect(Original::overview(['month' => '2026-09', 'inherit_mode' => 'exclude']) === $legacyBaseline,
'查询新报表不改变原报表响应');
$expect($new['total']['assigned_count'] === 2 && $new['total']['visit2_count'] === 1, '只圈定周期内新指派,排除区间外、继承、其他中心、自指派、软删、拒收');
$expect($new['total']['visit2_rate'] === 50.0 && $new['total']['inherit_count'] === 0, '新指派接诊率仅用新指派分母');
$inherit = L::overview($q, 'inherit');
$expect($inherit['total']['inherit_count'] === 2 && $inherit['total']['visit2_count'] === 2 && $inherit['total']['visit3_count'] === 2, '继承首单二诊、次单三诊,累计周期后订单且排除取消单');
$expect(L::overview($q + ['assistant_id' => 2])['total']['assigned_count'] === 1, '医助筛选作用于汇总');
$expect(L::overview($q + ['assistant_id' => 2])['total']['visit2_count'] === 0, '医助名下继承成交不进入其新指派分子');
$expect(L::overview($q + ['dept_ids' => '21'])['total']['assigned_count'] === 1, '部门子树过滤');
$expect(L::overview($q + ['dept_ids' => '20'])['total'] === $new['total'], '二中心父级包含各组');
$expect(L::overview($q + ['dept_ids' => '10'])['rows'] === [], '不能越过二中心范围');
$expect(L::overview($q + ['assistant_id' => 3])['rows'] === [], '其他中心医助不能绕过部门范围');
$expect(L::assignLines($q)['count'] === $new['total']['assigned_count'], '新指派患者明细与分母对账');
$expect(L::visitOrderLines($q + ['slot' => 2])['count'] === $new['total']['visit2_count'], '新指派订单明细与分子对账');
$expect(L::assignLines($q, 'inherit')['count'] === $inherit['total']['inherit_count'], '继承患者明细与分母对账');
$expect(L::visitOrderLines($q + ['slot' => 0], 'inherit')['count'] === $inherit['total']['visit_all_count'], '继承合计订单明细与汇总对账');
$expect(L::assignLines($q + ['assistant_id' => 2, 'dept_id' => 21])['count'] === 0, '下钻部门必须与页面医助取交集');
$expect(L::visitOrderLines($q + ['assistant_id' => 2, 'scope_assistant_id' => 1, 'slot' => 0])['count'] === 0, '下钻不能覆盖页面医助筛选');
$options = L::deptOptions();
$optionIds = array_column($options['assistants'], 'id');
sort($optionIds);
$expect($optionIds === [1, 2], '下拉排除其他中心及已删除账号');
$expect(count($options['rows']) === 3, '部门选项仍是二中心完整子树');
foreach ([18 => 0, 19 => 1] as $did => $isInherit) {
$pdo->prepare('INSERT INTO zyt_tcm_diagnosis(id,patient_name,phone) VALUES(?,?,?)')->execute([$did, '返还患者' . $did, '13800000000']);
$lg->execute([$did,0,1,strtotime('2026-08-01'),0]);
$lg->execute([$did,1,0,strtotime('2026-08-10'),0]);
$lg->execute([$did,1,1,strtotime('2026-09-01'),$isInherit]);
$o->execute(['RETURN-OLD-' . $did,$did,strtotime('2026-08-02'),3]);
$o->execute(['RETURN-NEW-' . $did,$did,strtotime('2026-09-02'),3]);
}
$expect(L::overview($q)['total']['assigned_count'] === 3 && L::overview($q)['total']['visit2_count'] === 2, '释放后重新新指派给原医助正常统计');
$expect(L::overview($q, 'inherit')['total']['inherit_count'] === 3 && L::overview($q, 'inherit')['total']['visit2_count'] === 3, '释放后重新继承给原医助正常统计');
// 在实际 MySQL 5.7 验证菜单脚本、幂等性与逐权限继承。
$pdo->exec('CREATE TABLE zyt_system_menu(id INT PRIMARY KEY AUTO_INCREMENT,pid INT,type VARCHAR(1),name VARCHAR(80),icon VARCHAR(80),sort INT,perms VARCHAR(120),paths VARCHAR(120),component VARCHAR(120),selected VARCHAR(120),params VARCHAR(120),is_cache INT,is_show INT,is_disable INT,create_time INT,update_time INT)');
$pdo->exec('CREATE TABLE zyt_system_role_menu(role_id INT,menu_id INT,PRIMARY KEY(role_id,menu_id))');
foreach (['overview','deptOptions','assignLines','visitOrderLines'] as $i => $action) {
$pdo->prepare('INSERT INTO zyt_system_menu(pid,type,name,perms) VALUES(100,?,?,?)')
->execute([$i === 0 ? 'C' : 'A', $action, 'stats.revisitRate/' . $action]);
}
$pdo->exec('INSERT INTO zyt_system_role_menu VALUES(9,1),(9,2),(9,3)');
$originalMenus = $pdo->query('SELECT * FROM zyt_system_menu ORDER BY id')->fetchAll(PDO::FETCH_ASSOC);
$migration = file_get_contents(dirname(__DIR__) . '/sql/1.9.20261016/01_add_revisit_inherit_rate_menu.sql');
$migration = preg_replace('/^--.*$/m', '', $migration);
for ($repeat = 0; $repeat < 2; $repeat++) {
foreach (explode(';', $migration) as $sql) {
if (trim($sql) !== '') { $pdo->exec($sql); }
}
}
$expect((int) $pdo->query("SELECT COUNT(*) FROM zyt_system_menu WHERE perms LIKE 'stats.revisitInheritRate/%'")->fetchColumn() === 4, '迁移可重复执行,页面及3个接口各登记一次');
$expect((int) $pdo->query('SELECT COUNT(*) FROM zyt_system_role_menu WHERE role_id=9')->fetchColumn() === 6, '仅继承原有的3项对应权限');
$expect((int) $pdo->query("SELECT COUNT(*) FROM zyt_system_role_menu rm JOIN zyt_system_menu m ON m.id=rm.menu_id WHERE rm.role_id=9 AND m.perms='stats.revisitInheritRate/visitOrderLines'")->fetchColumn() === 0, '不扩大原角色明细权限');
$assignMigration = preg_replace('/^--.*$/m', '', file_get_contents(dirname(__DIR__) . '/sql/1.9.20261017/01_add_revisit_assign_rate_menu.sql'));
for ($repeat = 0; $repeat < 2; $repeat++) {
foreach (explode(';', $assignMigration) as $sql) {
if (trim($sql) !== '') { $pdo->exec($sql); }
}
}
$expect((int) $pdo->query("SELECT COUNT(*) FROM zyt_system_menu WHERE perms LIKE 'stats.revisitAssignRate/%'")->fetchColumn() === 4, '独立指派周期页面及3个接口幂等登记');
$expect((int) $pdo->query('SELECT COUNT(*) FROM zyt_system_role_menu WHERE role_id=9')->fetchColumn() === 9, '两个独立报表均逐权限继承原角色授权');
$expect((int) $pdo->query("SELECT COUNT(*) FROM zyt_system_role_menu rm JOIN zyt_system_menu m ON m.id=rm.menu_id WHERE rm.role_id=9 AND m.perms='stats.revisitAssignRate/visitOrderLines'")->fetchColumn() === 0, '独立指派周期接口不扩大订单明细权限');
$expect($pdo->query('SELECT * FROM zyt_system_menu WHERE id<=4 ORDER BY id')->fetchAll(PDO::FETCH_ASSOC) === $originalMenus, '新增菜单不会改写原菜单及权限记录');
echo "RevisitCohortDatabaseTest OK ({$checks} checks)\n";
} finally {
$pdo->exec("DROP DATABASE `{$database}`");
}