__DIR__ . '/../app/adminapi/logic/firstvisit/WecomPromotionLogic.php', 'controller' => __DIR__ . '/../app/adminapi/controller/firstvisit/WecomPromotionController.php', 'middleware' => __DIR__ . '/../app/adminapi/http/middleware/AuthMiddleware.php', 'api' => $root . '/admin/src/api/first_visit.ts', 'page' => $root . '/admin/src/views/first_visit/wecom_promotion/index.vue', 'automationForm' => $root . '/admin/src/views/first_visit/wecom_promotion/components/PromotionAutomationForm.vue', ]; $sources = []; foreach ($paths as $name => $path) { $source = file_get_contents($path); if (!is_string($source)) { throw new RuntimeException("无法读取 {$name}: {$path}"); } $sources[$name] = $source; } if (!str_contains($sources['controller'], 'public function batchUpdatePools()') || !str_contains($sources['controller'], 'WecomPromotionLogic::batchUpdatePools(')) { throw new RuntimeException('控制器缺少批量修改方案配置接口'); } if (!str_contains($sources['middleware'], "'firstvisit.wecompromotion/batchupdatepools'")) { throw new RuntimeException('批量修改方案接口未加入获客助手权限白名单'); } if (!str_contains($sources['api'], '/firstvisit.wecomPromotion/batchUpdatePools') || !str_contains($sources['api'], 'WecomPromotionBatchUpdatePoolsParams')) { throw new RuntimeException('前端缺少类型化批量修改 API'); } $logic = $sources['logic']; $start = strpos($logic, 'public static function batchUpdatePools('); $end = strpos($logic, 'public static function saveWidget(', $start === false ? 0 : $start); if ($start === false || $end === false) { throw new RuntimeException('无法定位 batchUpdatePools 方法'); } $method = substr($logic, $start, $end - $start); foreach ([ 'assertBasePagePermission($adminId, $adminInfo)', "array_key_exists('fallback_url', \$changes)", "array_key_exists('status', \$changes)", "array_key_exists('skip_verify', \$changes)", "array_key_exists('automation_config', \$changes)", "array_key_exists('member_status', \$changes)", 'array_replace($currentAutomation, $automationPatch)', 'self::savePool($saveParams, $adminId, $adminInfo, false)', 'self::updatePoolMemberStatuses(', 'self::previewPoolMemberStatus(', 'self::assertMemberDispatchReady(', 'if ($dispatch !== null && $status === 0)', '至少需要保留一名当前可用的上线员工', '员工上下线需要单独批量保存', "'sync_error_count' => \$syncErrorCount", "'sync_queued_count' => \$syncQueuedCount", "'member_updated' => \$memberUpdated", "'results' => \$results", ] as $needle) { if (!str_contains($method, $needle)) { throw new RuntimeException("批量修改方案逻辑缺少契约:{$needle}"); } } $permissionValidation = strpos($method, "self::assertScopedRow("); $writeLoop = strpos($method, '$results = []'); if ($permissionValidation === false || $writeLoop === false || $permissionValidation > $writeLoop) { throw new RuntimeException('批量修改必须在任何方案保存前完成全部方案权限校验'); } if (!str_contains($method, "false\n );")) { throw new RuntimeException('批量修改不得允许共享操作人绕过原管理范围批量编辑'); } if (!str_contains($method, "Db::name('qywx_promotion_range_sync')->where('pool_id', \$poolId)->lock(true)->find()") || !str_contains($sources['logic'], "'_status_only' => true") || !str_contains($sources['logic'], '$eligibleUserIds = self::eligibleSelectedUserIds(') || !str_contains($sources['logic'], '$createdRangeChanged = $createdRemote') || !str_contains($sources['logic'], '$needsQueuedSync = !$createdRemote || $createdRangeChanged') || !str_contains($sources['logic'], '$rangeShrank = array_diff($currentRange[\'userids\'], $nextRange[\'userids\']) !== []') || !str_contains($sources['logic'], 'if ($isGoingOffline || $rangeShrank)')) { throw new RuntimeException('单个与批量员工状态更新未使用一致的事务锁和字段保留策略'); } $page = $sources['page']; foreach ([ '批量修改方案', '全选可管理的分流方案', 'allManageablePoolsSelected', 'someManageablePoolsSelected', 'toggleAllPoolSelection', '仅勾选的项目会覆盖到所选方案', 'batchConfigApply.skip_verify', 'batchConfigApply.fallback_url', 'batchConfigApply.status', 'batchConfigApply.member_status', 'batchConfigForm.member_admin_ids', 'batchMemberStatusById', 'batchMemberDepartmentTree', 'batchMemberTreeDefaultExpandedKeys', 'selectableDepartments: true', '勾选部门可全选其下员工', 'batchMemberOfflineBlockedPools', '批量修改员工上线状态', '员工上下线需单独批量保存', 'batchConfigApply.reception', 'batchConfigApply.customer', 'batchConfigApply.welcome', 'batchSharedPrimaryMemberIds', 'batchPrimaryMemberUnion', 'wecomPromotionBatchUpdatePools', ] as $needle) { if (!str_contains($page, $needle)) { throw new RuntimeException("前端批量修改交互缺少 {$needle}"); } } if (!str_contains($sources['api'], 'member_status?: {') || !str_contains($sources['api'], 'member_updated: number')) { throw new RuntimeException('前端批量修改 API 缺少员工上下线契约'); } if (!str_contains($sources['automationForm'], 'disabledSections?: AutomationSection[]') || !str_contains($sources['automationForm'], 'backupExcludedMemberIds?: number[]')) { throw new RuntimeException('自动化表单未支持批量场景的分组禁用或主成员冲突约束'); } echo "WECOM_PROMOTION_BATCH_UPDATE_CONTRACT_OK\n";