This commit is contained in:
Your Name
2026-09-03 16:03:41 +08:00
parent b4c11881b4
commit 928f72ec3d
25 changed files with 1926 additions and 394 deletions
@@ -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";