This commit is contained in:
Your Name
2026-09-07 12:30:42 +08:00
parent d5164b7369
commit 9ef6eb8d67
369 changed files with 11733 additions and 0 deletions
@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
function eventTagSnapshotExpect(bool $condition, string $message): void
{
if (!$condition) {
throw new RuntimeException($message);
}
}
$root = dirname(__DIR__);
$snapshotSource = file_get_contents(
$root . '/app/common/service/qywx/QywxExternalContactEventTagSnapshotService.php'
);
$customerSource = file_get_contents($root . '/app/adminapi/logic/qywx/CustomerLogic.php');
$callbackSource = file_get_contents($root . '/app/api/controller/QywxExternalContactCallbackController.php');
$storeSource = file_get_contents($root . '/app/common/service/qywx/QywxPromotionAutomationStore.php');
$datedMigration = file_get_contents($root . '/sql/1.9.20260904/add_qywx_external_contact_event_tag.sql');
$baseMigration = file_get_contents($root . '/database/migrations/create_qywx_external_contact_event_tag.sql');
foreach ([$snapshotSource, $customerSource, $callbackSource, $storeSource, $datedMigration, $baseMigration] as $source) {
eventTagSnapshotExpect(is_string($source), '无法读取事件标签快照实现或迁移文件');
}
eventTagSnapshotExpect(
str_contains($snapshotSource, "'tag_id' => ''")
&& str_contains($snapshotSource, 'Db::transaction(')
&& str_contains($snapshotSource, 'INSERT IGNORE INTO')
&& str_contains($snapshotSource, 'if ($inserted === 0)'),
'事件标签快照必须原子写入完成标记,并对重复回调保持幂等'
);
eventTagSnapshotExpect(
str_contains($snapshotSource, 'public static function installed(): bool')
&& str_contains($snapshotSource, "str_contains(\$message, '1146')"),
'快照服务必须能识别迁移缺表,供读取侧安全降级'
);
eventTagSnapshotExpect(
!str_contains($snapshotSource, '->delete(')
&& !str_contains($snapshotSource, '->update('),
'历史事件标签快照只能追加,不能随当前客户关系删除或改写'
);
eventTagSnapshotExpect(
str_contains($snapshotSource, "trim((string) (\$followUser['userid'] ?? '')) !== \$followUserId")
&& str_contains($snapshotSource, "['tags']['status']")
&& str_contains($snapshotSource, "\$tagStatus !== 'success'")
&& str_contains($snapshotSource, 'self::appendTags('),
'快照必须只读取事件员工标签,推广成功标签只能追加证据而不能提前冻结完整快照'
);
eventTagSnapshotExpect(
str_contains($customerSource, 'public static function recordExternalContactEvent(array $data): int')
&& str_contains($customerSource, 'QywxExternalContactEventTagSnapshotService::captureFromFollowUsers('),
'事件入库必须返回事件ID,并在客户详情同步后按员工保存快照'
);
eventTagSnapshotExpect(
str_contains($callbackSource, '$eventId = CustomerLogic::recordExternalContactEvent([')
&& str_contains($callbackSource, 'QywxExternalContactEventTagSnapshotService::captureForPromotionEvent($eventId)')
&& str_contains($callbackSource, "\$changeType === 'add_external_contact' ? \$eventId : 0"),
'普通新增与推广新增回调都必须把标签快照绑定到实际事件ID'
);
eventTagSnapshotExpect(
str_contains($storeSource, 'QywxExternalContactEventTagSnapshotService::captureFromPromotionTask($row)'),
'推广标签动作异步重试成功后必须补写事件标签快照'
);
foreach ([$datedMigration, $baseMigration] as $migration) {
foreach ([
'CREATE TABLE IF NOT EXISTS `zyt_qywx_external_contact_event_tag`',
'UNIQUE KEY `uk_event_user_tag` (`event_id`, `follow_user_id`, `tag_id`)',
'KEY `idx_tag_event_user` (`tag_id`, `event_id`, `follow_user_id`)',
"t.`change_type` = 'add_external_contact'",
"'$.tags.status'",
'current_tag.`follow_user_id` = e.`user_id`',
'existing_snapshot.`event_id` = e.`id`',
"existing_snapshot.`tag_id` = ''",
] as $needle) {
eventTagSnapshotExpect(str_contains($migration, $needle), '事件标签快照迁移缺少契约:' . $needle);
}
eventTagSnapshotExpect(
!str_contains($migration, "IN ('success', 'skipped', 'failed')"),
'推广标签未启用或失败不能生成空完成标记,否则会遮蔽客户实际渠道标签'
);
}
echo "QYWX_EXTERNAL_CONTACT_EVENT_TAG_SNAPSHOT_OK\n";
@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
use app\adminapi\logic\firstvisit\WecomPromotionLogic;
require dirname(__DIR__) . '/vendor/autoload.php';
$assert = static function (bool $condition, string $message): void {
if (!$condition) {
throw new RuntimeException($message);
}
};
$preview = new ReflectionMethod(WecomPromotionLogic::class, 'previewPoolMemberStatus');
$preview->setAccessible(true);
$assertDispatchReady = new ReflectionMethod(WecomPromotionLogic::class, 'assertMemberDispatchReady');
$assertDispatchReady->setAccessible(true);
$today = date('Y-m-d');
$members = [
['id' => 1, 'admin_id' => 11, 'userid' => 'A', 'enabled' => 1, 'daily_limit' => 0, 'today_count' => 0, 'today_date' => $today],
['id' => 2, 'admin_id' => 12, 'userid' => 'B', 'enabled' => 1, 'daily_limit' => 0, 'today_count' => 0, 'today_date' => $today],
];
$result = $preview->invoke(null, $members, [11], 0, []);
$assert($result['matched_ids'] === [1], '批量下线没有正确匹配员工规则');
$assert($result['update_ids'] === [1], '批量下线没有标记需要更新的规则');
$assert((int) $result['members'][0]['enabled'] === 0, '批量下线预览没有更新目标状态');
$assert((int) $result['members'][1]['enabled'] === 1, '批量下线错误修改了未选员工');
$result = $preview->invoke(null, [
[
'id' => 3,
'admin_id' => 13,
'userid' => 'C',
'enabled' => 0,
'daily_limit' => 0,
'today_count' => 0,
'today_date' => $today,
'active_start' => time() + 3600,
],
], [13], 1, []);
$assert(
$result['update_ids'] === [3] && (int) $result['members'][0]['enabled'] === 1,
'尚未到生效时间的员工也应允许提前批量上线'
);
try {
$preview->invoke(null, $members, [11, 12], 0, [], '全员方案');
throw new RuntimeException('批量下线不应允许方案变成零上线员工');
} catch (ReflectionException $error) {
throw $error;
} catch (Throwable $error) {
$assert(str_contains($error->getMessage(), '至少需要保留一名上线员工'), '零上线员工错误提示不正确');
}
try {
$preview->invoke(null, [
['id' => 4, 'admin_id' => 14, 'userid' => 'D', 'enabled' => 1, 'daily_limit' => 0, 'today_count' => 0, 'today_date' => $today],
['id' => 5, 'admin_id' => 15, 'userid' => 'E', 'enabled' => 1, 'daily_limit' => 1, 'today_count' => 1, 'today_date' => $today],
], [14], 0, [], '额度方案');
throw new RuntimeException('批量下线不应保留零个当前可用员工');
} catch (ReflectionException $error) {
throw $error;
} catch (Throwable $error) {
$assert(str_contains($error->getMessage(), '当前可用'), '零可用员工错误提示不正确');
}
$result = $preview->invoke(null, $members, [999], 0, []);
$assert($result['matched_ids'] === [] && $result['update_ids'] === [], '不存在的员工不应产生状态更新');
$assertDispatchReady->invoke(null, ['blocked' => false, 'queued' => true]);
try {
$assertDispatchReady->invoke(null, ['blocked' => true, 'queued' => false]);
throw new RuntimeException('同步阻塞时不应提交员工状态');
} catch (ReflectionException $error) {
throw $error;
} catch (Throwable $error) {
$assert(str_contains($error->getMessage(), '无法同步'), '同步阻塞错误提示不正确');
}
echo "WECOM_PROMOTION_BATCH_MEMBER_STATUS_OK\n";