__DIR__ . '/../app/adminapi/controller/qywx/CustomerController.php', 'logic' => __DIR__ . '/../app/adminapi/logic/qywx/CustomerLogic.php', 'validate' => __DIR__ . '/../app/adminapi/validate/qywx/CustomerValidate.php', 'api' => $root . '/admin/src/api/qywx.ts', 'page' => $root . '/admin/src/views/fans/qywx.vue', 'migration' => __DIR__ . '/../sql/1.9.20260902/add_qywx_customer_delete_menu.sql', ]; $sources = []; foreach ($paths as $name => $path) { $source = file_get_contents($path); if (!is_string($source)) { throw new RuntimeException("无法读取 {$name}: {$path}"); } $sources[$name] = $source; } function qywxDeleteExpect(bool $condition, string $message): void { if (!$condition) { throw new RuntimeException($message); } } $controller = $sources['controller']; $permissionCheck = strpos($controller, 'if (!$this->canDeleteCustomer())'); $deleteCall = strpos($controller, 'CustomerLogic::deleteCustomer('); qywxDeleteExpect(str_contains($controller, "private const DELETE_PERMISSION = 'qywx.customer/delete';"), '控制器缺少独立删除权限'); qywxDeleteExpect($permissionCheck !== false && $deleteCall !== false && $permissionCheck < $deleteCall, '控制器必须在删除前显式鉴权'); qywxDeleteExpect(str_contains($controller, "(int) (\$this->adminInfo['root'] ?? 0) === 1") && str_contains($controller, 'AuthLogic::getAuthByAdminId($this->adminId)') && str_contains($controller, 'in_array(self::DELETE_PERMISSION,'), '控制器删除权限必须仅放行 root 或显式授权账号'); qywxDeleteExpect(str_contains($sources['validate'], "'id' => 'require|integer|gt:0'") && str_contains($sources['validate'], 'public function sceneDelete()'), '删除请求缺少正整数 ID 校验'); $logic = $sources['logic']; $logicStart = strpos($logic, 'public static function deleteCustomer(int $id): bool'); $logicEnd = strpos($logic, 'public static function softDeleteExternalContactRow(', $logicStart === false ? 0 : $logicStart); qywxDeleteExpect($logicStart !== false && $logicEnd !== false, '无法定位客户删除逻辑'); $method = substr($logic, $logicStart, $logicEnd - $logicStart); foreach ([ "->where('id', \$id)", "->whereNull('delete_time')", "'delete_time' => \$now", "->where('external_userid', \$externalUserId)", 'if ($activeRows === 0)', "Db::name('qywx_external_contact_tag')", 'MediaChannelService::forgetCurrentTagCatalogCache()', ] as $needle) { qywxDeleteExpect(str_contains($method, $needle), "客户删除逻辑缺少契约:{$needle}"); } qywxDeleteExpect(!str_contains($method, 'WechatWorkService'), '后台删除不得调用企微接口删除外部客户关系'); qywxDeleteExpect(str_contains($sources['api'], '/qywx.customer/delete') && str_contains($sources['api'], 'qywxCustomerDelete'), '前端缺少客户删除 API'); foreach ([ "v-perms=\"['qywx.customer/delete']\"", 'handleDelete(row)', '仅删除系统内的同步记录', 'qywxCustomerDelete({ id })', 'Promise.all([getLists(), loadStats(), loadTagStats()])', ] as $needle) { qywxDeleteExpect(str_contains($sources['page'], $needle), "客户列表删除交互缺少:{$needle}"); } $migration = $sources['migration']; qywxDeleteExpect(str_contains($migration, "`component` = 'fans/qywx'") && str_contains($migration, "'qywx.customer/delete'") && str_contains($migration, "'A'") && str_contains($migration, 'NOT EXISTS'), '删除权限迁移必须按页面定位并可重复执行'); qywxDeleteExpect(!str_contains(strtolower($migration), 'system_role_menu'), '删除权限不得自动授予已有角色'); echo "QYWX_CUSTOMER_DELETE_PERMISSION_CONTRACT_OK\n";