更新
This commit is contained in:
@@ -13,7 +13,7 @@ function conversionFanRuleExpect(bool $condition, string $message): void
|
||||
}
|
||||
}
|
||||
|
||||
$method = new ReflectionMethod(ConversionLogic::class, 'loadFanRows');
|
||||
$method = new ReflectionMethod(ConversionLogic::class, 'loadFanDetailRows');
|
||||
$sourceLines = file($method->getFileName());
|
||||
if ($sourceLines === false) {
|
||||
throw new RuntimeException('无法读取加粉统计源码');
|
||||
@@ -33,7 +33,8 @@ conversionFanRuleExpect(
|
||||
'会话存档同意是独立能力,不能再次成为加粉统计的硬性条件'
|
||||
);
|
||||
conversionFanRuleExpect(
|
||||
str_contains($methodSource, "['del_external_contact', \$endTimestamp]"),
|
||||
str_contains($methodSource, "'del_external_contact'")
|
||||
&& str_contains($methodSource, 'surviving_del.event_time >= surviving_e.event_time'),
|
||||
'加粉统计必须继续识别区间内已删除客户'
|
||||
);
|
||||
conversionFanRuleExpect(
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use app\adminapi\logic\firstvisit\FirstVisitConversionLogic;
|
||||
use app\adminapi\logic\stats\ConversionLogic;
|
||||
use think\facade\Db;
|
||||
|
||||
require dirname(__DIR__) . '/vendor/autoload.php';
|
||||
|
||||
$app = new think\App();
|
||||
$app->initialize();
|
||||
|
||||
$admin = Db::name('admin')->where('root', 1)->whereNull('delete_time')->find();
|
||||
if (!$admin) {
|
||||
echo "FAN_DETAIL_DB_SMOKE_SKIP no_root\n";
|
||||
exit(0);
|
||||
}
|
||||
|
||||
$params = [
|
||||
'time_type' => 'custom',
|
||||
'start_date' => '2026-08-01',
|
||||
'end_date' => '2026-08-25',
|
||||
];
|
||||
$overviewStartedAt = microtime(true);
|
||||
$overview = FirstVisitConversionLogic::overview($params, (int) $admin['id'], $admin);
|
||||
$overviewElapsedMs = (microtime(true) - $overviewStartedAt) * 1000;
|
||||
$candidates = [];
|
||||
$collect = static function (array $rows, array $path = []) use (&$collect, &$candidates): void {
|
||||
foreach ($rows as $row) {
|
||||
if (!is_array($row)) {
|
||||
continue;
|
||||
}
|
||||
$rowPath = array_merge($path, [(string) ($row['name'] ?? $row['id'] ?? '')]);
|
||||
if ((int) ($row['add_fans_count'] ?? 0) > 0) {
|
||||
$row['_smoke_path'] = implode('/', $rowPath);
|
||||
$candidates[] = $row;
|
||||
}
|
||||
$collect(is_array($row['children'] ?? null) ? $row['children'] : [], $rowPath);
|
||||
}
|
||||
};
|
||||
$collect(is_array($overview['rows'] ?? null) ? $overview['rows'] : []);
|
||||
if ($candidates === []) {
|
||||
echo "FAN_DETAIL_DB_SMOKE_SKIP no_fans\n";
|
||||
exit(0);
|
||||
}
|
||||
|
||||
usort($candidates, static function (array $left, array $right): int {
|
||||
$leftCenter = str_contains((string) ($left['_smoke_path'] ?? ''), '郑州二中心') ? 0 : 1;
|
||||
$rightCenter = str_contains((string) ($right['_smoke_path'] ?? ''), '郑州二中心') ? 0 : 1;
|
||||
$leftMember = in_array((string) ($left['type'] ?? ''), ['member', 'unbound'], true) ? 0 : 1;
|
||||
$rightMember = in_array((string) ($right['type'] ?? ''), ['member', 'unbound'], true) ? 0 : 1;
|
||||
|
||||
return ($leftCenter <=> $rightCenter)
|
||||
?: ($leftMember <=> $rightMember)
|
||||
?: ((int) ($left['add_fans_count'] ?? 0) <=> (int) ($right['add_fans_count'] ?? 0));
|
||||
});
|
||||
|
||||
$row = $candidates[0];
|
||||
$entityType = in_array((string) ($row['type'] ?? ''), ['member', 'unbound'], true)
|
||||
? 'member'
|
||||
: 'dept';
|
||||
$detailStartedAt = microtime(true);
|
||||
$detail = FirstVisitConversionLogic::fansDetail($params + [
|
||||
'entity_type' => $entityType,
|
||||
'entity_id' => (string) ($row['id'] ?? ''),
|
||||
'admin_id' => (int) ($row['admin_id'] ?? 0),
|
||||
'page_no' => 1,
|
||||
'page_size' => 100,
|
||||
], (int) $admin['id'], $admin);
|
||||
$detailElapsedMs = (microtime(true) - $detailStartedAt) * 1000;
|
||||
|
||||
if ((int) ($detail['count'] ?? -1) !== (int) ($row['add_fans_count'] ?? 0)) {
|
||||
throw new RuntimeException('Fan detail count does not match the clicked add_fans_count row');
|
||||
}
|
||||
if ((int) ($detail['entity']['deleted_fans_count'] ?? -1) !== (int) ($row['deleted_fans_count'] ?? 0)) {
|
||||
throw new RuntimeException('Fan detail entity metadata lost deleted_fans_count');
|
||||
}
|
||||
|
||||
foreach ($detail['lists'] ?? [] as $fan) {
|
||||
foreach (['external_userid', 'customer_name', 'wecom_userid', 'wecom_staff_name', 'add_time', 'is_deleted', 'delete_time'] as $field) {
|
||||
if (!array_key_exists($field, $fan)) {
|
||||
throw new RuntimeException("Fan detail row is missing {$field}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$forgedDetail = FirstVisitConversionLogic::fansDetail($params + [
|
||||
'entity_type' => 'member',
|
||||
'entity_id' => 'M999999_-2',
|
||||
'admin_id' => 999999,
|
||||
'page_no' => 1,
|
||||
'page_size' => 1,
|
||||
], (int) $admin['id'], $admin);
|
||||
if ((int) ($forgedDetail['count'] ?? -1) !== 0 || ($forgedDetail['entity'] ?? null) !== null) {
|
||||
throw new RuntimeException('Forged fan-detail entity did not fail closed');
|
||||
}
|
||||
|
||||
$centerDetailElapsedMs = 0.0;
|
||||
$centerDetailCount = -1;
|
||||
$centerRows = array_values(array_filter($candidates, static fn (array $candidate): bool =>
|
||||
!in_array((string) ($candidate['type'] ?? ''), ['member', 'unbound'], true)
|
||||
&& str_contains((string) ($candidate['name'] ?? ''), '郑州二中心')
|
||||
));
|
||||
if ($centerRows !== []) {
|
||||
$centerRow = $centerRows[0];
|
||||
$centerStartedAt = microtime(true);
|
||||
$centerDetail = FirstVisitConversionLogic::fansDetail($params + [
|
||||
'entity_type' => 'dept',
|
||||
'entity_id' => (string) ($centerRow['id'] ?? ''),
|
||||
'page_no' => 1,
|
||||
'page_size' => 100,
|
||||
], (int) $admin['id'], $admin);
|
||||
$centerDetailElapsedMs = (microtime(true) - $centerStartedAt) * 1000;
|
||||
$centerDetailCount = (int) ($centerDetail['count'] ?? -1);
|
||||
if ($centerDetailCount !== (int) ($centerRow['add_fans_count'] ?? 0)
|
||||
|| (int) ($centerDetail['entity']['deleted_fans_count'] ?? -1)
|
||||
!== (int) ($centerRow['deleted_fans_count'] ?? 0)
|
||||
) {
|
||||
throw new RuntimeException('郑州二中心 detail count does not match overview');
|
||||
}
|
||||
}
|
||||
|
||||
$parentCandidates = array_values(array_filter($candidates, static function (array $candidate): bool {
|
||||
foreach (is_array($candidate['children'] ?? null) ? $candidate['children'] : [] as $child) {
|
||||
if (is_array($child) && !in_array((string) ($child['type'] ?? ''), ['member', 'unbound'], true)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}));
|
||||
$parentChecked = false;
|
||||
if ($parentCandidates !== []) {
|
||||
usort(
|
||||
$parentCandidates,
|
||||
static fn (array $left, array $right): int => ((int) ($left['add_fans_count'] ?? 0))
|
||||
<=> ((int) ($right['add_fans_count'] ?? 0))
|
||||
);
|
||||
$parentRow = $parentCandidates[0];
|
||||
$parentDeptIds = [];
|
||||
$collectDeptIds = static function (array $node) use (&$collectDeptIds, &$parentDeptIds): void {
|
||||
if (in_array((string) ($node['type'] ?? ''), ['member', 'unbound'], true)) {
|
||||
return;
|
||||
}
|
||||
$parentDeptIds[(int) ($node['id'] ?? 0)] = true;
|
||||
foreach (is_array($node['children'] ?? null) ? $node['children'] : [] as $child) {
|
||||
if (is_array($child)) {
|
||||
$collectDeptIds($child);
|
||||
}
|
||||
}
|
||||
};
|
||||
$collectDeptIds($parentRow);
|
||||
$parentDetail = ConversionLogic::fanDetails([
|
||||
'dimension' => 'dept',
|
||||
'time_type' => 'custom',
|
||||
'start_date' => (string) ($overview['meta']['start_date'] ?? ''),
|
||||
'end_date' => (string) ($overview['meta']['end_date'] ?? ''),
|
||||
'page_no' => 1,
|
||||
'page_size' => 1,
|
||||
], [
|
||||
'type' => 'dept',
|
||||
'dept_ids' => array_keys($parentDeptIds),
|
||||
], (int) $admin['id'], $admin);
|
||||
if ((int) ($parentDetail['count'] ?? -1) !== (int) ($parentRow['add_fans_count'] ?? 0)) {
|
||||
throw new RuntimeException('Parent department fan detail does not include the same descendant total as overview');
|
||||
}
|
||||
$parentChecked = true;
|
||||
}
|
||||
|
||||
echo sprintf(
|
||||
"FAN_DETAIL_DB_SMOKE_OK range=2026-08-01..2026-08-25 type=%s entity=%s path=%s count=%d deleted=%d parent=%d overview_ms=%.1f detail_ms=%.1f center_count=%d center_detail_ms=%.1f forged=0\n",
|
||||
$entityType,
|
||||
(string) ($row['name'] ?? $row['id'] ?? ''),
|
||||
(string) ($row['_smoke_path'] ?? ''),
|
||||
(int) $detail['count'],
|
||||
(int) ($detail['entity']['deleted_fans_count'] ?? 0),
|
||||
$parentChecked ? 1 : 0,
|
||||
$overviewElapsedMs,
|
||||
$detailElapsedMs,
|
||||
$centerDetailCount,
|
||||
$centerDetailElapsedMs
|
||||
);
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use app\adminapi\logic\firstvisit\FirstVisitConversionLogic;
|
||||
use app\adminapi\logic\stats\ConversionLogic;
|
||||
|
||||
require dirname(__DIR__) . '/vendor/autoload.php';
|
||||
|
||||
function conversionFanDetailExpect(bool $condition, string $message): void
|
||||
{
|
||||
if (!$condition) {
|
||||
throw new RuntimeException($message);
|
||||
}
|
||||
}
|
||||
|
||||
$firstVisitReflection = new ReflectionClass(FirstVisitConversionLogic::class);
|
||||
|
||||
$conversionSource = file_get_contents((new ReflectionClass(ConversionLogic::class))->getFileName());
|
||||
conversionFanDetailExpect(is_string($conversionSource), 'Unable to read ConversionLogic source');
|
||||
$buildDetailRows = (new ReflectionClass(ConversionLogic::class))->getMethod('buildFanDetailRows');
|
||||
$buildDetailRows->setAccessible(true);
|
||||
$detailRows = $buildDetailRows->invoke(null, [
|
||||
['user_id' => 'staff-a', 'external_userid' => 'readded', 'add_time' => 100],
|
||||
['user_id' => 'staff-a', 'external_userid' => 'readded', 'add_time' => 200],
|
||||
['user_id' => 'staff-b', 'external_userid' => 'deleted', 'add_time' => 300],
|
||||
], [
|
||||
['user_id' => 'staff-a', 'external_userid' => 'readded', 'add_time' => 200],
|
||||
]);
|
||||
conversionFanDetailExpect(
|
||||
count($detailRows) === 2
|
||||
&& (int) ($detailRows[0]['add_time'] ?? 0) === 100
|
||||
&& empty($detailRows[0]['is_deleted'])
|
||||
&& !empty($detailRows[1]['is_deleted']),
|
||||
'Delete/re-add details must retain the first counted add time and final deletion state'
|
||||
);
|
||||
conversionFanDetailExpect(
|
||||
str_contains($conversionSource, 'self::loadFanDetailRows($startTimestamp, $endTimestamp, $mediaChannel, $adminIds)')
|
||||
&& str_contains($conversionSource, 'MIN(e.event_time) AS add_time')
|
||||
&& str_contains($conversionSource, "->where('e.event_time', 'between', [\$startTimestamp, \$endTimestamp])")
|
||||
&& str_contains($conversionSource, 'EXISTS (SELECT 1 FROM `')
|
||||
&& str_contains($conversionSource, 'MAX(event_time) AS delete_time'),
|
||||
'Aggregate and detail results must share the same distinct fan-pair loader and expose event times'
|
||||
);
|
||||
conversionFanDetailExpect(
|
||||
str_contains($conversionSource, "'external_userid' => \$externalUserId")
|
||||
&& str_contains($conversionSource, "'customer_name'")
|
||||
&& str_contains($conversionSource, "'wecom_staff_name'")
|
||||
&& str_contains($conversionSource, "'is_deleted'")
|
||||
&& str_contains($conversionSource, "'delete_time'"),
|
||||
'Fan detail response must expose customer, employee, add and deletion fields'
|
||||
);
|
||||
|
||||
$firstVisitSource = file_get_contents($firstVisitReflection->getFileName());
|
||||
conversionFanDetailExpect(
|
||||
is_string($firstVisitSource)
|
||||
&& !str_contains($firstVisitSource, '$dashboard = self::overview($params, $adminId, $adminInfo);')
|
||||
&& str_contains($firstVisitSource, 'resolveFanDetailEntity(')
|
||||
&& str_contains($firstVisitSource, 'resolveFanDetailMemberDeptId(')
|
||||
&& str_contains($firstVisitSource, "['channel_dept_ids']")
|
||||
&& str_contains($firstVisitSource, "preg_match('/^M([1-9]\\d*)_(-?\\d+)$/', \$entityId")
|
||||
&& str_contains($firstVisitSource, "MediaChannelService::getCurrentTagChannelByCode"),
|
||||
'Fan detail must authorize the exact structural entity without recomputing overview and retain current-tag channel scope'
|
||||
);
|
||||
conversionFanDetailExpect(
|
||||
str_contains($conversionSource, '$targetWorkWechatUserIds')
|
||||
&& str_contains($conversionSource, 'fanDetailChannelDeptIds($mediaChannel)')
|
||||
&& str_contains($conversionSource, 'filterEntitiesByChannelDeptScope(')
|
||||
&& str_contains($conversionSource, 'NOT EXISTS (SELECT 1 FROM `')
|
||||
&& str_contains($conversionSource, "':wecom:'")
|
||||
&& str_contains($conversionSource, '$matched = $pairs;'),
|
||||
'Fan detail must narrow the event query to the clicked employees/WeCom user before sorting and pagination'
|
||||
);
|
||||
|
||||
$controllerSource = file_get_contents(dirname(__DIR__) . '/app/adminapi/controller/firstvisit/ConversionController.php');
|
||||
conversionFanDetailExpect(
|
||||
is_string($controllerSource)
|
||||
&& str_contains($controllerSource, 'public function fansDetail()')
|
||||
&& str_contains($controllerSource, 'FirstVisitConversionLogic::fansDetail('),
|
||||
'First-visit conversion controller must expose the fansDetail endpoint'
|
||||
);
|
||||
|
||||
echo "FirstVisitConversionFanDetailTest passed\n";
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use app\common\service\qywx\QywxPromotionMemberRange;
|
||||
use app\common\service\qywx\QywxPromotionMemberSchedulerService;
|
||||
|
||||
require dirname(__DIR__) . '/vendor/autoload.php';
|
||||
|
||||
$assert = static function (bool $condition, string $message): void {
|
||||
if (!$condition) {
|
||||
throw new RuntimeException($message);
|
||||
}
|
||||
};
|
||||
|
||||
$today = '2026-08-25';
|
||||
$now = strtotime($today . ' 12:00:00');
|
||||
$range = QywxPromotionMemberRange::evaluate([
|
||||
['id' => 1, 'userid' => 'A', 'enabled' => 1, 'weight' => 100, 'current_weight' => 9, 'daily_limit' => 0, 'today_count' => 0, 'today_date' => $today],
|
||||
['id' => 2, 'userid' => 'B', 'enabled' => 0, 'weight' => 1, 'current_weight' => -3, 'daily_limit' => 0, 'today_count' => 0, 'today_date' => $today],
|
||||
['id' => 3, 'userid' => 'C', 'enabled' => 1, 'weight' => 1, 'current_weight' => 4, 'daily_limit' => 3, 'today_count' => 3, 'today_date' => $today],
|
||||
['id' => 4, 'userid' => 'D', 'enabled' => 1, 'weight' => 1, 'current_weight' => 1, 'daily_limit' => 1, 'today_count' => 1, 'today_date' => '2026-08-24'],
|
||||
['id' => 5, 'userid' => 'E', 'enabled' => 1, 'weight' => 1, 'current_weight' => 0, 'daily_limit' => 0, 'today_count' => 0, 'today_date' => $today, 'active_start' => $now + 1],
|
||||
['id' => 6, 'userid' => 'F', 'enabled' => 1, 'weight' => 1, 'current_weight' => 0, 'daily_limit' => 0, 'today_count' => 0, 'today_date' => $today, 'active_end' => $now - 1],
|
||||
], $today, $now);
|
||||
$assert($range['userids'] === ['A', 'D'], '官方多人路由范围没有正确排除禁用或达到上限的成员');
|
||||
$assert($range['userids'] === ['A', 'D'], '逐成员权重不得改变企业微信原生多人范围');
|
||||
$assert($range['eligible_count'] === 2, '可用成员数量错误');
|
||||
$assert($range['members'][3]['today_count'] === 0, '跨日数量没有自动重置');
|
||||
$assert(array_column($range['members'], 'current_weight') === [0, 0, 0, 0, 0, 0], '旧版调度游标没有归零');
|
||||
$assert(QywxPromotionMemberRange::same(['B', 'A', 'A'], ['A', 'B']), '成员范围比较不应受顺序和重复值影响');
|
||||
$assert(!QywxPromotionMemberRange::same(['A'], ['A', 'B']), '不同成员范围被错误判定为相同');
|
||||
$assert(QywxPromotionMemberSchedulerService::poolIdFromState('zyt_pool:123') === 123, 'customer_channel 方案 ID 解析失败');
|
||||
$assert(QywxPromotionMemberSchedulerService::poolIdFromState('qywx_ca:123') === 0, '不应接管其他系统的 customer_channel');
|
||||
$empty = QywxPromotionMemberRange::evaluate([
|
||||
['id' => 7, 'userid' => 'G', 'enabled' => 0, 'today_date' => $today],
|
||||
], $today, $now);
|
||||
$assert($empty['userids'] === [] && $empty['eligible_count'] === 0, '零可用成员必须返回空范围并交由同步层阻止远端更新');
|
||||
|
||||
echo "QYWX_PROMOTION_MEMBER_RANGE_OK\n";
|
||||
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use app\adminapi\logic\firstvisit\WecomPromotionLogic;
|
||||
use app\common\service\qywx\QywxCustomerAcquisitionApiService;
|
||||
use think\facade\Db;
|
||||
|
||||
require dirname(__DIR__) . '/vendor/autoload.php';
|
||||
|
||||
$app = new think\App();
|
||||
$app->initialize();
|
||||
|
||||
final class PromotionDeleteApiFake extends QywxCustomerAcquisitionApiService
|
||||
{
|
||||
/** @var list<string> */
|
||||
public array $deleted = [];
|
||||
|
||||
public function __construct(private string $failLinkId = '')
|
||||
{
|
||||
}
|
||||
|
||||
public function deleteLink(string $linkId): void
|
||||
{
|
||||
$this->deleted[] = $linkId;
|
||||
if ($linkId === $this->failLinkId) {
|
||||
throw new RuntimeException('mock remote deletion failed');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$assert = static function (bool $condition, string $message): void {
|
||||
if (!$condition) {
|
||||
throw new RuntimeException($message);
|
||||
}
|
||||
};
|
||||
$admin = Db::name('admin')->where('root', 1)->whereNull('delete_time')->find();
|
||||
if (!$admin) {
|
||||
throw new RuntimeException('未找到 root 管理员,无法执行删除行为测试');
|
||||
}
|
||||
$now = time();
|
||||
$suffix = bin2hex(random_bytes(6));
|
||||
$firstRemoteId = 'contract_first_' . $suffix;
|
||||
$secondRemoteId = 'contract_second_' . $suffix;
|
||||
|
||||
Db::startTrans();
|
||||
try {
|
||||
$poolId = (int) Db::name('qywx_promotion_pool')->insertGetId([
|
||||
'name' => '删除契约测试',
|
||||
'public_key' => bin2hex(random_bytes(16)),
|
||||
'status' => 1,
|
||||
'owner_admin_id' => (int) $admin['id'],
|
||||
'create_time' => $now,
|
||||
'update_time' => $now,
|
||||
]);
|
||||
$firstLinkId = (int) Db::name('qywx_promotion_link')->insertGetId([
|
||||
'pool_id' => $poolId,
|
||||
'name' => '当前官方链接',
|
||||
'wecom_url' => 'https://work.weixin.qq.com/ca/' . $firstRemoteId,
|
||||
'remote_link_id' => $firstRemoteId,
|
||||
'remote_status' => 1,
|
||||
'status' => 1,
|
||||
'owner_admin_id' => (int) $admin['id'],
|
||||
'create_time' => $now,
|
||||
'update_time' => $now,
|
||||
]);
|
||||
$secondLinkId = (int) Db::name('qywx_promotion_link')->insertGetId([
|
||||
'pool_id' => $poolId,
|
||||
'name' => '历史已软删官方链接',
|
||||
'wecom_url' => 'https://work.weixin.qq.com/ca/' . $secondRemoteId,
|
||||
'remote_link_id' => $secondRemoteId,
|
||||
'remote_status' => 1,
|
||||
'status' => 0,
|
||||
'owner_admin_id' => (int) $admin['id'],
|
||||
'create_time' => $now,
|
||||
'update_time' => $now,
|
||||
'delete_time' => $now - 10,
|
||||
]);
|
||||
$memberId = (int) Db::name('qywx_promotion_pool_member')->insertGetId([
|
||||
'pool_id' => $poolId,
|
||||
'admin_id' => (int) $admin['id'],
|
||||
'userid' => 'contract_user_' . $suffix,
|
||||
'enabled' => 1,
|
||||
'create_time' => $now,
|
||||
'update_time' => $now,
|
||||
]);
|
||||
Db::name('qywx_promotion_range_sync')->insert([
|
||||
'pool_id' => $poolId,
|
||||
'promotion_link_id' => $firstLinkId,
|
||||
'desired_version' => 1,
|
||||
'applied_version' => 1,
|
||||
'status' => 0,
|
||||
'create_time' => $now,
|
||||
'update_time' => $now,
|
||||
]);
|
||||
|
||||
$firstAttempt = new PromotionDeleteApiFake($secondRemoteId);
|
||||
$message = '';
|
||||
try {
|
||||
WecomPromotionLogic::deletePool($poolId, (int) $admin['id'], $admin, $firstAttempt);
|
||||
} catch (RuntimeException $e) {
|
||||
$message = $e->getMessage();
|
||||
}
|
||||
$assert(str_contains($message, '本地方案已保留'), '远端部分删除失败没有明确保留本地方案');
|
||||
$assert($firstAttempt->deleted === [$firstRemoteId, $secondRemoteId], '首次删除没有按顺序处理全部远端链接');
|
||||
$assert(Db::name('qywx_promotion_pool')->where('id', $poolId)->whereNull('delete_time')->count() === 1, '远端失败时错误删除了本地方案');
|
||||
$assert((int) Db::name('qywx_promotion_link')->where('id', $firstLinkId)->value('remote_status') === 2, '部分成功的远端删除进度没有保存');
|
||||
$assert((int) Db::name('qywx_promotion_link')->where('id', $secondLinkId)->value('remote_status') === 1, '失败的远端链接被错误标记为已删除');
|
||||
$assert((int) Db::name('qywx_promotion_pool_member')->where('id', $memberId)->value('enabled') === 1, '远端失败时错误禁用了方案成员');
|
||||
|
||||
$retry = new PromotionDeleteApiFake();
|
||||
WecomPromotionLogic::deletePool($poolId, (int) $admin['id'], $admin, $retry);
|
||||
$assert($retry->deleted === [$secondRemoteId], '重试时重复删除了已经成功的远端链接');
|
||||
$assert(Db::name('qywx_promotion_pool')->where('id', $poolId)->whereNotNull('delete_time')->count() === 1, '远端全部成功后没有软删除本地方案');
|
||||
$assert((int) Db::name('qywx_promotion_link')->where('id', $secondLinkId)->value('remote_status') === 2, '历史软删链接没有同步删除企业微信端');
|
||||
$assert((int) Db::name('qywx_promotion_pool_member')->where('id', $memberId)->value('enabled') === 0, '方案删除后成员仍处于启用状态');
|
||||
$sync = Db::name('qywx_promotion_range_sync')->where('pool_id', $poolId)->find();
|
||||
$assert((int) ($sync['status'] ?? 0) === 4 && (string) ($sync['lock_token'] ?? '') === '', '方案删除后同步租约没有终止');
|
||||
} finally {
|
||||
Db::rollback();
|
||||
}
|
||||
|
||||
echo "WECOM_PROMOTION_DELETE_POOL_BEHAVIOR_OK\n";
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
$logicPath = dirname(__DIR__) . '/app/adminapi/logic/firstvisit/WecomPromotionLogic.php';
|
||||
$viewPath = dirname(__DIR__, 2) . '/admin/src/views/first_visit/wecom_promotion/index.vue';
|
||||
$logic = file_get_contents($logicPath);
|
||||
$view = file_get_contents($viewPath);
|
||||
if (!is_string($logic) || !is_string($view)) {
|
||||
throw new RuntimeException('无法读取获客助手删除链路源码');
|
||||
}
|
||||
|
||||
$start = strpos($logic, 'public static function deletePool(');
|
||||
$end = strpos($logic, 'public static function saveMember(', $start === false ? 0 : $start);
|
||||
if ($start === false || $end === false || $end <= $start) {
|
||||
throw new RuntimeException('无法定位 deletePool 方法');
|
||||
}
|
||||
$method = substr($logic, $start, $end - $start);
|
||||
$remoteDeleteAt = strpos($method, '$api->deleteLink($remoteLinkId)');
|
||||
$localDeleteAt = strpos($method, "Db::transaction(function () use (\$id, \$now)");
|
||||
if ($remoteDeleteAt === false || $localDeleteAt === false || $remoteDeleteAt >= $localDeleteAt) {
|
||||
throw new RuntimeException('删除方案必须先永久删除企业微信官方链接,再删除本地记录');
|
||||
}
|
||||
if (!str_contains($method, '本地方案已保留') || !str_contains($method, "'remote_status' => 2")) {
|
||||
throw new RuntimeException('企业微信删除失败保护或成功状态记录缺失');
|
||||
}
|
||||
if (!str_contains($view, '已投放的链接会失效且无法恢复') || !str_contains($view, "confirmButtonText: '永久删除'")) {
|
||||
throw new RuntimeException('前端没有明确提示官方链接将被永久删除');
|
||||
}
|
||||
|
||||
echo "WECOM_PROMOTION_DELETE_POOL_CONTRACT_OK\n";
|
||||
Reference in New Issue
Block a user