更新
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use app\adminapi\lists\qywx\CustomerLists;
|
||||
|
||||
require dirname(__DIR__) . '/vendor/autoload.php';
|
||||
|
||||
function qywxChannelExpect(bool $condition, string $message): void
|
||||
{
|
||||
if (!$condition) {
|
||||
throw new RuntimeException($message);
|
||||
}
|
||||
}
|
||||
|
||||
final class QywxChannelFilterQueryFake
|
||||
{
|
||||
/** @var array<int, array{field:string,operator:string,value:string,logic:string}> */
|
||||
public array $likes = [];
|
||||
/** @var string[] */
|
||||
public array $raw = [];
|
||||
|
||||
public function where($field, $operator = null, $value = null): self
|
||||
{
|
||||
if ($field instanceof Closure) {
|
||||
$field($this);
|
||||
return $this;
|
||||
}
|
||||
$this->likes[] = [
|
||||
'field' => (string) $field,
|
||||
'operator' => (string) $operator,
|
||||
'value' => (string) $value,
|
||||
'logic' => 'and',
|
||||
];
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function whereOr($field, $operator = null, $value = null): self
|
||||
{
|
||||
$this->likes[] = [
|
||||
'field' => (string) $field,
|
||||
'operator' => (string) $operator,
|
||||
'value' => (string) $value,
|
||||
'logic' => 'or',
|
||||
];
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function whereRaw(string $condition): self
|
||||
{
|
||||
$this->raw[] = $condition;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
$method = new ReflectionMethod(CustomerLists::class, 'projectAddChannelEvents');
|
||||
$method->setAccessible(true);
|
||||
$addWayLabelMethod = new ReflectionMethod(CustomerLists::class, 'addWayLabel');
|
||||
$addWayLabelMethod->setAccessible(true);
|
||||
$normalizeAddWayMethod = new ReflectionMethod(CustomerLists::class, 'normalizeAddWay');
|
||||
$normalizeAddWayMethod->setAccessible(true);
|
||||
|
||||
$events = [
|
||||
['id' => 12, 'external_userid' => 'ext-a', 'user_id' => 'staff-2', 'state' => 'channel-b', 'event_time' => 300],
|
||||
['id' => 11, 'external_userid' => 'ext-a', 'user_id' => 'staff-1', 'state' => ' ', 'event_time' => 300],
|
||||
['id' => 10, 'external_userid' => 'ext-a', 'user_id' => 'staff-1', 'state' => 'zyt_pool:2', 'event_time' => 250],
|
||||
['id' => 9, 'external_userid' => 'ext-a', 'user_id' => 'staff-1', 'state' => 'channel-b', 'event_time' => 200],
|
||||
['id' => 8, 'external_userid' => 'ext-a', 'user_id' => 'staff-1', 'state' => 'channel-a', 'event_time' => 100],
|
||||
['id' => 7, 'external_userid' => 'ext-zero', 'user_id' => 'staff-1', 'state' => '0', 'event_time' => 100],
|
||||
['id' => 6, 'external_userid' => '', 'user_id' => 'staff-1', 'state' => 'ignored', 'event_time' => 100],
|
||||
];
|
||||
|
||||
/** @var array<string, array<int, array<string, mixed>>> $projected */
|
||||
$projected = $method->invoke(null, $events, [2 => '九月投放方案']);
|
||||
|
||||
qywxChannelExpect(
|
||||
array_column($projected['ext-a'] ?? [], 'state') === ['channel-b', 'zyt_pool:2', 'channel-a'],
|
||||
'渠道必须按最近事件排序、排除空值并按 state 去重'
|
||||
);
|
||||
qywxChannelExpect(($projected['ext-a'][0]['user_id'] ?? '') === 'staff-2', '重复渠道必须保留最近事件的员工');
|
||||
qywxChannelExpect(($projected['ext-a'][0]['event_time'] ?? 0) === 300, '重复渠道必须保留最近事件时间');
|
||||
qywxChannelExpect(($projected['ext-a'][1]['label'] ?? '') === '九月投放方案', '获客助手 state 必须映射方案名称');
|
||||
qywxChannelExpect(($projected['ext-a'][1]['source_type'] ?? '') === 'promotion_pool', '获客助手渠道类型错误');
|
||||
qywxChannelExpect(($projected['ext-a'][1]['pool_id'] ?? 0) === 2, '获客助手方案 ID 解析错误');
|
||||
qywxChannelExpect(array_column($projected['ext-zero'] ?? [], 'state') === ['0'], '字符串 0 是有效渠道,不能被 empty/filter 丢弃');
|
||||
qywxChannelExpect(!isset($projected['']), '空 external_userid 不得生成渠道投影');
|
||||
|
||||
$fallback = $method->invoke(null, [
|
||||
['id' => 1, 'external_userid' => 'ext-b', 'user_id' => '', 'state' => 'zyt_pool:99', 'event_time' => 1],
|
||||
], []);
|
||||
qywxChannelExpect(
|
||||
($fallback['ext-b'][0]['label'] ?? '') === '获客助手方案 #99',
|
||||
'已删除或缺失的获客助手方案应保留可读兜底名称'
|
||||
);
|
||||
|
||||
$knownAddWays = [
|
||||
0 => '未知添加方式',
|
||||
1 => '通过扫描二维码添加',
|
||||
2 => '通过搜索手机号添加',
|
||||
3 => '通过名片分享添加',
|
||||
4 => '通过群聊添加',
|
||||
5 => '通过手机通讯录添加',
|
||||
6 => '通过微信联系人添加',
|
||||
8 => '安装第三方应用时自动添加',
|
||||
9 => '通过搜索邮箱添加',
|
||||
10 => '通过视频号添加',
|
||||
11 => '通过日程参与人添加',
|
||||
12 => '通过会议参与人添加',
|
||||
13 => '通过微信好友添加',
|
||||
14 => '通过智慧硬件专属客服添加',
|
||||
15 => '通过上门服务客服添加',
|
||||
16 => '通过获客链接添加',
|
||||
17 => '通过定制开发添加',
|
||||
18 => '通过需求回复添加',
|
||||
21 => '通过第三方售前客服添加',
|
||||
22 => '通过可能的商务伙伴添加',
|
||||
24 => '通过接受微信好友申请添加',
|
||||
201 => '通过内部成员共享添加',
|
||||
202 => '通过管理员或负责人分配添加',
|
||||
];
|
||||
foreach ($knownAddWays as $addWay => $expectedLabel) {
|
||||
qywxChannelExpect(
|
||||
$addWayLabelMethod->invoke(null, $addWay) === $expectedLabel,
|
||||
"add_way={$addWay} 缺少正确的可读文案"
|
||||
);
|
||||
}
|
||||
qywxChannelExpect(
|
||||
$addWayLabelMethod->invoke(null, 999) === '其他添加方式(999)',
|
||||
'未知的新 add_way 必须保留编号作为可读兜底'
|
||||
);
|
||||
qywxChannelExpect($normalizeAddWayMethod->invoke(null, '16') === 16, '数字字符串 add_way 应被规范化');
|
||||
qywxChannelExpect($normalizeAddWayMethod->invoke(null, '1future') === null, '异常 add_way 不得被强转成错误来源');
|
||||
qywxChannelExpect($normalizeAddWayMethod->invoke(null, null) === null, '缺失 add_way 应保持未记录');
|
||||
|
||||
$filterMethod = new ReflectionMethod(CustomerLists::class, 'applyAddWayFilter');
|
||||
$filterMethod->setAccessible(true);
|
||||
$list = (new ReflectionClass(CustomerLists::class))->newInstanceWithoutConstructor();
|
||||
$paramsProperty = new ReflectionProperty(app\common\lists\BaseDataLists::class, 'params');
|
||||
$paramsProperty->setAccessible(true);
|
||||
|
||||
$paramsProperty->setValue($list, ['add_way' => 1]);
|
||||
$filterQuery = new QywxChannelFilterQueryFake();
|
||||
$filterMethod->invoke($list, $filterQuery);
|
||||
qywxChannelExpect(
|
||||
array_column($filterQuery->likes, 'value') === [
|
||||
'%"add_way":1,%',
|
||||
'%"add_way":1}%',
|
||||
'%"add_way":"1",%',
|
||||
'%"add_way":"1"}%',
|
||||
],
|
||||
'渠道筛选必须精确匹配数字或字符串 add_way,不能让 1 误命中 16'
|
||||
);
|
||||
|
||||
$paramsProperty->setValue($list, ['add_way' => 0]);
|
||||
$zeroFilterQuery = new QywxChannelFilterQueryFake();
|
||||
$filterMethod->invoke($list, $zeroFilterQuery);
|
||||
qywxChannelExpect(count($zeroFilterQuery->likes) === 4, 'add_way=0 是有效渠道筛选,不能按空值忽略');
|
||||
|
||||
$paramsProperty->setValue($list, ['add_way' => 'invalid']);
|
||||
$invalidFilterQuery = new QywxChannelFilterQueryFake();
|
||||
$filterMethod->invoke($list, $invalidFilterQuery);
|
||||
qywxChannelExpect($invalidFilterQuery->raw === ['1=0'], '非法渠道参数必须返回空结果,不能泄露全量客户');
|
||||
|
||||
$paramsProperty->setValue($list, ['add_way' => '']);
|
||||
$emptyFilterQuery = new QywxChannelFilterQueryFake();
|
||||
$filterMethod->invoke($list, $emptyFilterQuery);
|
||||
qywxChannelExpect($emptyFilterQuery->likes === [] && $emptyFilterQuery->raw === [], '空渠道参数应表示不限');
|
||||
|
||||
$source = file_get_contents(__DIR__ . '/../app/adminapi/lists/qywx/CustomerLists.php');
|
||||
qywxChannelExpect(is_string($source), '无法读取 CustomerLists.php');
|
||||
foreach ([
|
||||
"->where('change_type', 'add_external_contact')",
|
||||
"->where('state', '<>', '')",
|
||||
"->whereIn('external_userid', \$ids)",
|
||||
"->order('event_time', 'desc')",
|
||||
"->order('id', 'desc')",
|
||||
"\$item['add_channels']",
|
||||
"\$item['add_channel_states']",
|
||||
"\$fu['add_way_label'] = self::addWayLabel(\$addWay)",
|
||||
'$this->applyAddWayFilter($query)',
|
||||
] as $needle) {
|
||||
qywxChannelExpect(str_contains($source, $needle), "客户渠道投影缺少契约:{$needle}");
|
||||
}
|
||||
|
||||
echo "QYWX_CUSTOMER_CHANNEL_PROJECTION_OK\n";
|
||||
@@ -0,0 +1,60 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const currentDir = path.dirname(fileURLToPath(import.meta.url))
|
||||
const pagePath = path.resolve(currentDir, '../../admin/src/views/fans/qywx.vue')
|
||||
const source = fs.readFileSync(pagePath, 'utf8')
|
||||
|
||||
function expect(condition, message) {
|
||||
if (!condition) throw new Error(message)
|
||||
}
|
||||
|
||||
expect(source.includes('<el-table-column label="添加渠道"'), '客户主表缺少添加渠道列')
|
||||
expect(source.includes('<el-descriptions-item label="添加渠道"'), '客户详情缺少添加渠道')
|
||||
expect(source.includes('<el-form-item label="渠道"'), '客户检索区缺少渠道筛选')
|
||||
expect(source.includes('v-model="queryParams.add_way"'), '渠道筛选未绑定 add_way 参数')
|
||||
expect(source.includes('v-for="option in ADD_WAY_OPTIONS"'), '渠道筛选未复用完整添加方式枚举')
|
||||
expect(source.includes('placeholder="选择或搜索添加渠道"'), '渠道筛选必须支持按可读文案检索')
|
||||
expect(source.includes('queryParams.add_way ='), '重置操作未清空渠道筛选')
|
||||
expect(source.includes('customerAddSources(row)'), '客户主表未读取可读添加方式')
|
||||
expect(source.includes('customerAddSources(currentCustomer)'), '客户详情未读取可读添加方式')
|
||||
expect(source.includes(':key="source.key"'), '添加方式循环必须使用成员关系级稳定 key')
|
||||
expect(source.includes('add_channel_states'), '前端缺少原始渠道数组兼容逻辑')
|
||||
expect(source.includes('未记录'), '无渠道客户必须明确显示未记录')
|
||||
for (const [addWay, label] of [
|
||||
[0, '未知添加方式'],
|
||||
[1, '通过扫描二维码添加'],
|
||||
[2, '通过搜索手机号添加'],
|
||||
[3, '通过名片分享添加'],
|
||||
[4, '通过群聊添加'],
|
||||
[5, '通过手机通讯录添加'],
|
||||
[6, '通过微信联系人添加'],
|
||||
[8, '安装第三方应用时自动添加'],
|
||||
[9, '通过搜索邮箱添加'],
|
||||
[10, '通过视频号添加'],
|
||||
[11, '通过日程参与人添加'],
|
||||
[12, '通过会议参与人添加'],
|
||||
[13, '通过微信好友添加'],
|
||||
[14, '通过智慧硬件专属客服添加'],
|
||||
[15, '通过上门服务客服添加'],
|
||||
[16, '通过获客链接添加'],
|
||||
[17, '通过定制开发添加'],
|
||||
[18, '通过需求回复添加'],
|
||||
[21, '通过第三方售前客服添加'],
|
||||
[22, '通过可能的商务伙伴添加'],
|
||||
[24, '通过接受微信好友申请添加'],
|
||||
[201, '通过内部成员共享添加'],
|
||||
[202, '通过管理员或负责人分配添加']
|
||||
]) {
|
||||
expect(source.includes(`${addWay}: '${label}'`), `add_way=${addWay} 未显示可读来源`)
|
||||
}
|
||||
expect(source.includes('add_way_label'), '前端未优先使用后端返回的添加方式文案')
|
||||
expect(source.includes('添加方式:${source.label}'), '悬浮说明缺少直观添加方式')
|
||||
expect(source.includes('获客助手方案:${source.channel_label}'), '悬浮说明缺少获客助手方案')
|
||||
expect(source.includes('跟进人:${source.staff_name}'), '悬浮说明缺少对应跟进人')
|
||||
expect(source.includes('添加时间:${formatTime(source.event_time)}'), '悬浮说明缺少添加时间')
|
||||
expect(source.includes('渠道参数:${source.state}'), '原始 state 必须仅保留在悬浮说明')
|
||||
expect(!source.includes('{{ addChannelText(channel) }}'), '列表不得继续直接展示原始渠道参数')
|
||||
|
||||
console.log('QYWX_CUSTOMER_CHANNEL_UI_CONTRACT_OK')
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
$root = dirname(__DIR__, 2);
|
||||
$paths = [
|
||||
'controller' => __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";
|
||||
@@ -62,5 +62,9 @@ assert.match(previewTemplate('{customer_name}-{employee_name}-{add_time}', '小
|
||||
assert.equal(Array.from(previewTemplate('王'.repeat(30), '小陈', 20)).length, 20)
|
||||
assert.match(formSource, /不会写入企微获客链接详情中的“欢迎语\/客户标签”配置/)
|
||||
assert.match(formSource, /客户添加回调中立即发送渠道欢迎语并添加标签/)
|
||||
assert.match(formSource, /disabledSections\?: AutomationSection\[\]/)
|
||||
assert.match(formSource, /backupExcludedMemberIds\?: number\[\]/)
|
||||
assert.match(pageSource, /result\?\.automation_saved !== true/)
|
||||
assert.match(pageSource, /仅勾选的项目会覆盖到所选方案/)
|
||||
assert.match(pageSource, /batchSharedPrimaryMemberIds/)
|
||||
console.log('WECOM_PROMOTION_AUTOMATION_UI_OK')
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
$root = dirname(__DIR__, 2);
|
||||
$paths = [
|
||||
'logic' => __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_replace($currentAutomation, $automationPatch)',
|
||||
'self::savePool($saveParams, $adminId, $adminInfo, false)',
|
||||
"'sync_error_count' => \$syncErrorCount",
|
||||
"'sync_queued_count' => \$syncQueuedCount",
|
||||
"'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('批量修改不得允许共享操作人绕过原管理范围批量编辑');
|
||||
}
|
||||
|
||||
$page = $sources['page'];
|
||||
foreach ([
|
||||
'批量修改方案',
|
||||
'仅勾选的项目会覆盖到所选方案',
|
||||
'batchConfigApply.skip_verify',
|
||||
'batchConfigApply.fallback_url',
|
||||
'batchConfigApply.status',
|
||||
'batchConfigApply.reception',
|
||||
'batchConfigApply.customer',
|
||||
'batchConfigApply.welcome',
|
||||
'batchSharedPrimaryMemberIds',
|
||||
'batchPrimaryMemberUnion',
|
||||
'wecomPromotionBatchUpdatePools',
|
||||
] as $needle) {
|
||||
if (!str_contains($page, $needle)) {
|
||||
throw new RuntimeException("前端批量修改交互缺少 {$needle}");
|
||||
}
|
||||
}
|
||||
if (!str_contains($sources['automationForm'], 'disabledSections?: AutomationSection[]')
|
||||
|| !str_contains($sources['automationForm'], 'backupExcludedMemberIds?: number[]')) {
|
||||
throw new RuntimeException('自动化表单未支持批量场景的分组禁用或主成员冲突约束');
|
||||
}
|
||||
|
||||
echo "WECOM_PROMOTION_BATCH_UPDATE_CONTRACT_OK\n";
|
||||
Reference in New Issue
Block a user