/** * 只读盘点:白名单 / isTest / 限测商品 / 交叉风险 * * 用法: * cd server/dukang-api * pnpm inventory:test-data * pnpm inventory:test-data -- --json > inventory.json */ import '../src/load-env'; import { PrismaClient } from '@prisma/client'; import { writeFileSync } from 'fs'; import { resolve } from 'path'; import { SEED_TEST_PHONES, collectTestDataScope, countTestData, detectCrossRisks, } from './test-data-cleanup.shared'; const prisma = new PrismaClient(); const jsonMode = process.argv.includes('--json'); const outArg = process.argv.find((a) => a.startsWith('--out=')); const outPath = outArg ? outArg.slice('--out='.length) : ''; async function main() { const counts = await countTestData(prisma); const scope = await collectTestDataScope(prisma); const risks = await detectCrossRisks(prisma, scope); const [users, storeAccounts, partners, stores, orders, redeems, limitedProducts] = await Promise.all([ prisma.user.findMany({ where: { isTest: true }, select: { id: true, userNo: true, phone: true, nickname: true, createdAt: true }, orderBy: { createdAt: 'desc' }, }), prisma.storeAccount.findMany({ where: { isTest: true }, select: { id: true, phone: true, name: true, createdAt: true }, orderBy: { createdAt: 'desc' }, }), prisma.partnerAccount.findMany({ where: { isTest: true }, select: { id: true, phone: true, name: true, companyName: true, createdAt: true }, orderBy: { createdAt: 'desc' }, }), prisma.store.findMany({ where: { isTest: true }, select: { id: true, name: true, phone: true, cityName: true, status: true, createdAt: true }, orderBy: { createdAt: 'desc' }, }), prisma.order.findMany({ where: { id: { in: scope.testOrderIds } }, select: { id: true, orderNo: true, userId: true, payAmount: true, status: true, isTest: true, createdAt: true }, orderBy: { createdAt: 'desc' }, take: 200, }), prisma.redeemRecord.findMany({ where: { id: { in: scope.testRedeemIds } }, select: { id: true, redeemNo: true, storeId: true, userId: true, amount: true, isTest: true, createdAt: true }, orderBy: { createdAt: 'desc' }, take: 200, }), prisma.commonProductItem.findMany({ where: { visibilityWhitelistEnabled: true }, select: { id: true, name: true, skuCode: true, status: true }, orderBy: { sortOrder: 'asc' }, }), ]); const whitelistLinked = await Promise.all( scope.whitelistPhones.map(async (row) => { const phone = row.phone; const [user, storeAccount, partner, store] = await Promise.all([ prisma.user.findFirst({ where: { phone }, select: { id: true, userNo: true, isTest: true } }), prisma.storeAccount.findFirst({ where: { phone }, select: { id: true, name: true, isTest: true } }), prisma.partnerAccount.findFirst({ where: { phone }, select: { id: true, name: true, isTest: true } }), prisma.store.findFirst({ where: { phone }, select: { id: true, name: true, isTest: true } }), ]); return { ...row, user, storeAccount, partner, store }; }), ); const report = { generatedAt: new Date().toISOString(), databaseUrlHost: (process.env.DATABASE_URL ?? '').replace(/:([^:@/]+)@/, ':***@'), seedTestPhones: SEED_TEST_PHONES, counts, whitelist: whitelistLinked, testUsers: users, testStoreAccounts: storeAccounts, testPartners: partners, testStores: stores, testOrders: orders, testRedeems: redeems, limitedProducts, limitedProductsWithFormalOrders: scope.limitedProductsWithFormalOrders, crossRisks: risks, scopeSummary: { testUserIds: scope.testUserIds.map(String), testStoreAccountIds: scope.testStoreAccountIds.map(String), deletableTestStoreAccountIds: scope.deletableTestStoreAccountIds.map(String), promotedTestStoreAccountIds: scope.promotedTestStoreAccountIds.map(String), testPartnerIds: scope.testPartnerIds.map(String), deletableTestPartnerIds: scope.deletableTestPartnerIds.map(String), promotedTestPartnerIds: scope.promotedTestPartnerIds.map(String), testStoreIds: scope.testStoreIds.map(String), deletableTestStoreIds: scope.deletableTestStoreIds.map(String), promotedTestStoreIds: scope.promotedTestStoreIds.map(String), testOrderIds: scope.testOrderIds.map(String), testRedeemIds: scope.testRedeemIds.map(String), limitedProductIds: scope.limitedProductIds.map(String), deletableLimitedProductIds: scope.limitedProductIds .filter((id) => !scope.limitedProductsWithFormalOrders.some((p) => p.productId === id)) .map(String), }, }; const serialized = JSON.stringify( report, (_key, value) => (typeof value === 'bigint' ? value.toString() : value), 2, ); if (outPath) { const abs = resolve(outPath); writeFileSync(abs, serialized, 'utf8'); console.log(`已写入 ${abs}`); } if (jsonMode) { console.log(serialized); return; } console.log('=== 杜康好客 · 测试数据盘点 ==='); console.log(`时间: ${report.generatedAt}`); console.log(`库: ${report.databaseUrlHost}`); console.log(''); console.log('【计数】'); console.log(` 白名单手机号: ${counts.whitelistCount}`); console.log(` 测试用户: ${counts.testUsers}`); console.log(` 测试门店账号: ${counts.testStoreAccounts}`); console.log(` 测试合伙人: ${counts.testPartners}`); console.log(` 测试门店: ${counts.testStores}`); console.log(` 测试订单(isTest): ${counts.testOrders} / 待删订单(含测试用户): ${scope.testOrderIds.length}`); console.log(` 测试核销(isTest): ${counts.testRedeems} / 待删核销: ${scope.testRedeemIds.length}`); console.log(` 限测商品: ${counts.limitedProducts}`); console.log(` 仍开「仅白名单可见」的门店: ${counts.visibilityStores}`); console.log(''); console.log('【白名单手机号】'); for (const row of whitelistLinked) { const flags = [ row.user ? `user#${row.user.id}${row.user.isTest ? '(test)' : ''}` : null, row.storeAccount ? `storeAcct#${row.storeAccount.id}` : null, row.partner ? `partner#${row.partner.id}` : null, row.store ? `store#${row.store.id}` : null, ] .filter(Boolean) .join(', '); console.log(` ${row.phone} ${row.note ?? ''} ${flags || '(无关联账号)'}`); } console.log(''); console.log('【限测商品】'); for (const p of limitedProducts) { const blocked = scope.limitedProductsWithFormalOrders.find((x) => x.productId === p.id); console.log( ` #${p.id} ${p.name} (${p.skuCode})${blocked ? ` — 跳过删除,正式订单 ${blocked.formalOrderCount} 笔` : ' — 将删除'}`, ); } console.log(''); console.log('【交叉风险】'); if (!risks.length) { console.log(' (无)'); } else { for (const risk of risks) { console.log(` [${risk.code}] ${risk.message} (${risk.details.length} 条)`); } } console.log(''); console.log('确认后执行: pnpm cleanup:test-data -- --apply --yes'); } main() .catch((e) => { console.error(e); process.exit(1); }) .finally(() => prisma.$disconnect());