chore(ops): 增加生产库备份/回滚与按 isTest 清理脚本
只入库通用运维能力,不提交写死手机号的一次性清理脚本与 dump。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
/**
|
||||
* 只读盘点:白名单 / 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());
|
||||
Reference in New Issue
Block a user