Files
zyt/server/tests/FirstVisitConversionFanDetailSmokeTest.php
T
2026-08-31 15:17:34 +08:00

202 lines
8.2 KiB
PHP

<?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);
$canViewDeletedFans = ($admin['account'] ?? null) === 'admin';
if (($overview['meta']['can_view_deleted_fans'] ?? null) !== $canViewDeletedFans) {
throw new RuntimeException('Deleted-fan visibility must follow exact authenticated account, not root');
}
$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 (($detail['can_view_deleted_fans'] ?? null) !== $canViewDeletedFans) {
throw new RuntimeException('Fan detail visibility differs from overview');
}
if ($canViewDeletedFans && (int) ($detail['entity']['deleted_fans_count'] ?? -1) !== (int) ($row['deleted_fans_count'] ?? 0)) {
throw new RuntimeException('Fan detail entity metadata lost deleted_fans_count');
}
if (!$canViewDeletedFans && (array_key_exists('deleted_fans_count', $detail['entity']) || array_key_exists('deleted_fans_count', $row))) {
throw new RuntimeException('Other root accounts must not receive deleted_fans_count');
}
foreach ($detail['lists'] ?? [] as $fan) {
$requiredFields = ['external_userid', 'customer_name', 'wecom_userid', 'wecom_staff_name', 'add_time'];
if ($canViewDeletedFans) {
$requiredFields = array_merge($requiredFields, ['is_deleted', 'delete_time']);
}
foreach ($requiredFields as $field) {
if (!array_key_exists($field, $fan)) {
throw new RuntimeException("Fan detail row is missing {$field}");
}
}
if (!$canViewDeletedFans && (array_key_exists('is_deleted', $fan) || array_key_exists('delete_time', $fan))) {
throw new RuntimeException('Other accounts must not receive deletion state through fan detail');
}
}
$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)
|| ($canViewDeletedFans && (int) ($centerDetail['entity']['deleted_fans_count'] ?? -1)
!== (int) ($centerRow['deleted_fans_count'] ?? 0))
|| (!$canViewDeletedFans && array_key_exists('deleted_fans_count', $centerDetail['entity'] ?? []))
) {
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=%s 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'],
$canViewDeletedFans ? (string) ($detail['entity']['deleted_fans_count'] ?? 0) : 'hidden',
$parentChecked ? 1 : 0,
$overviewElapsedMs,
$detailElapsedMs,
$centerDetailCount,
$centerDetailElapsedMs
);