chore(ops): 增加生产库备份/回滚与按 isTest 清理脚本
只入库通用运维能力,不提交写死手机号的一次性清理脚本与 dump。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,332 @@
|
||||
import { Prisma, PrismaClient } from '@prisma/client';
|
||||
|
||||
export const SEED_TEST_PHONES = [
|
||||
'13800000001',
|
||||
'13700000001',
|
||||
'13700000002',
|
||||
'13910000001',
|
||||
'13910000002',
|
||||
] as const;
|
||||
|
||||
export type TestDataScope = {
|
||||
whitelistPhones: { id: bigint; phone: string; note: string | null }[];
|
||||
testUserIds: bigint[];
|
||||
testStoreAccountIds: bigint[];
|
||||
deletableTestStoreAccountIds: bigint[];
|
||||
promotedTestStoreAccountIds: bigint[];
|
||||
testPartnerIds: bigint[];
|
||||
deletableTestPartnerIds: bigint[];
|
||||
promotedTestPartnerIds: bigint[];
|
||||
testStoreIds: bigint[];
|
||||
deletableTestStoreIds: bigint[];
|
||||
promotedTestStoreIds: bigint[];
|
||||
testOrderIds: bigint[];
|
||||
testRedeemIds: bigint[];
|
||||
limitedProductIds: bigint[];
|
||||
limitedProductsWithFormalOrders: { productId: bigint; name: string; formalOrderCount: number }[];
|
||||
};
|
||||
|
||||
export type CrossRisk = {
|
||||
code: string;
|
||||
message: string;
|
||||
details: unknown[];
|
||||
};
|
||||
|
||||
export async function collectTestDataScope(prisma: PrismaClient): Promise<TestDataScope> {
|
||||
const whitelistPhones = await prisma.commonTestWhitelistPhone.findMany({
|
||||
orderBy: { createdAt: 'asc' },
|
||||
select: { id: true, phone: true, note: true },
|
||||
});
|
||||
|
||||
const [testUsers, testStoreAccounts, testPartners, testStores] = await Promise.all([
|
||||
prisma.user.findMany({ where: { isTest: true }, select: { id: true } }),
|
||||
prisma.storeAccount.findMany({ where: { isTest: true }, select: { id: true } }),
|
||||
prisma.partnerAccount.findMany({ where: { isTest: true }, select: { id: true } }),
|
||||
prisma.store.findMany({ where: { isTest: true }, select: { id: true } }),
|
||||
]);
|
||||
|
||||
const testUserIds = testUsers.map((r) => r.id);
|
||||
const testStoreAccountIds = testStoreAccounts.map((r) => r.id);
|
||||
const testPartnerIds = testPartners.map((r) => r.id);
|
||||
const testStoreIds = testStores.map((r) => r.id);
|
||||
|
||||
const testOrders = await prisma.order.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ isTest: true },
|
||||
...(testUserIds.length ? [{ userId: { in: testUserIds } }] : []),
|
||||
],
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
let testOrderIds = testOrders.map((r) => r.id);
|
||||
if (testOrderIds.length) {
|
||||
const linkedReshipments = await prisma.order.findMany({
|
||||
where: { originOrderId: { in: testOrderIds } },
|
||||
select: { id: true },
|
||||
});
|
||||
testOrderIds = [...new Set([...testOrderIds, ...linkedReshipments.map((r) => r.id)])];
|
||||
}
|
||||
|
||||
const testRedeems = await prisma.redeemRecord.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ isTest: true },
|
||||
...(testUserIds.length ? [{ userId: { in: testUserIds } }] : []),
|
||||
],
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
const testRedeemIds = testRedeems.map((r) => r.id);
|
||||
|
||||
const storesWithFormalRedeems = testStoreIds.length
|
||||
? await prisma.redeemRecord.findMany({
|
||||
where: {
|
||||
storeId: { in: testStoreIds },
|
||||
isTest: false,
|
||||
...(testUserIds.length ? { userId: { notIn: testUserIds } } : {}),
|
||||
},
|
||||
select: { storeId: true },
|
||||
distinct: ['storeId'],
|
||||
})
|
||||
: [];
|
||||
const promotedTestStoreIds = storesWithFormalRedeems.map((r) => r.storeId);
|
||||
const promotedSet = new Set(promotedTestStoreIds.map(String));
|
||||
const deletableTestStoreIds = testStoreIds.filter((id) => !promotedSet.has(id.toString()));
|
||||
|
||||
const partnerStores = testPartnerIds.length
|
||||
? await prisma.store.findMany({
|
||||
where: { partnerAccountId: { in: testPartnerIds } },
|
||||
select: { id: true, partnerAccountId: true, isTest: true },
|
||||
})
|
||||
: [];
|
||||
const deletableTestPartnerIds: bigint[] = [];
|
||||
const promotedTestPartnerIds: bigint[] = [];
|
||||
for (const partnerId of testPartnerIds) {
|
||||
const stores = partnerStores.filter((s) => s.partnerAccountId === partnerId);
|
||||
const hasRetainedStore = stores.some((s) => !deletableTestStoreIds.some((id) => id === s.id));
|
||||
if (hasRetainedStore || stores.length === 0) promotedTestPartnerIds.push(partnerId);
|
||||
else deletableTestPartnerIds.push(partnerId);
|
||||
}
|
||||
|
||||
const accountBindings = testStoreAccountIds.length
|
||||
? await prisma.storeAccountStore.findMany({
|
||||
where: { storeAccountId: { in: testStoreAccountIds } },
|
||||
select: { storeAccountId: true, storeId: true },
|
||||
})
|
||||
: [];
|
||||
const deletableTestStoreAccountIds: bigint[] = [];
|
||||
const promotedTestStoreAccountIds: bigint[] = [];
|
||||
for (const accountId of testStoreAccountIds) {
|
||||
const bindings = accountBindings.filter((b) => b.storeAccountId === accountId);
|
||||
const hasRetainedStore = bindings.some((b) => !deletableTestStoreIds.some((id) => id === b.storeId));
|
||||
if (hasRetainedStore || bindings.length === 0) promotedTestStoreAccountIds.push(accountId);
|
||||
else deletableTestStoreAccountIds.push(accountId);
|
||||
}
|
||||
|
||||
const limitedProducts = await prisma.commonProductItem.findMany({
|
||||
where: { visibilityWhitelistEnabled: true },
|
||||
select: { id: true, name: true },
|
||||
});
|
||||
const limitedProductIds = limitedProducts.map((p) => p.id);
|
||||
|
||||
const limitedProductsWithFormalOrders: TestDataScope['limitedProductsWithFormalOrders'] = [];
|
||||
for (const product of limitedProducts) {
|
||||
const formalOrderCount = await prisma.order.count({
|
||||
where: {
|
||||
productId: product.id,
|
||||
isTest: false,
|
||||
...(testUserIds.length ? { userId: { notIn: testUserIds } } : {}),
|
||||
},
|
||||
});
|
||||
if (formalOrderCount > 0) {
|
||||
limitedProductsWithFormalOrders.push({
|
||||
productId: product.id,
|
||||
name: product.name,
|
||||
formalOrderCount,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
whitelistPhones,
|
||||
testUserIds,
|
||||
testStoreAccountIds,
|
||||
deletableTestStoreAccountIds,
|
||||
promotedTestStoreAccountIds,
|
||||
testPartnerIds,
|
||||
deletableTestPartnerIds,
|
||||
promotedTestPartnerIds,
|
||||
testStoreIds,
|
||||
deletableTestStoreIds,
|
||||
promotedTestStoreIds,
|
||||
testOrderIds,
|
||||
testRedeemIds,
|
||||
limitedProductIds,
|
||||
limitedProductsWithFormalOrders,
|
||||
};
|
||||
}
|
||||
|
||||
export async function detectCrossRisks(
|
||||
prisma: PrismaClient,
|
||||
scope: TestDataScope,
|
||||
): Promise<CrossRisk[]> {
|
||||
const risks: CrossRisk[] = [];
|
||||
|
||||
if (scope.testPartnerIds.length) {
|
||||
const formalStoresUnderTestPartner = await prisma.store.findMany({
|
||||
where: {
|
||||
partnerAccountId: { in: scope.testPartnerIds },
|
||||
isTest: false,
|
||||
},
|
||||
select: { id: true, name: true, phone: true, partnerAccountId: true },
|
||||
});
|
||||
if (formalStoresUnderTestPartner.length) {
|
||||
risks.push({
|
||||
code: 'TEST_PARTNER_FORMAL_STORES',
|
||||
message: '测试合伙人名下仍有正式门店',
|
||||
details: formalStoresUnderTestPartner,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (scope.testStoreIds.length) {
|
||||
const formalRedeemsAtTestStore = await prisma.redeemRecord.findMany({
|
||||
where: {
|
||||
storeId: { in: scope.testStoreIds },
|
||||
isTest: false,
|
||||
...(scope.testUserIds.length ? { userId: { notIn: scope.testUserIds } } : {}),
|
||||
},
|
||||
select: { id: true, redeemNo: true, storeId: true, userId: true, amount: true },
|
||||
take: 50,
|
||||
});
|
||||
if (formalRedeemsAtTestStore.length) {
|
||||
risks.push({
|
||||
code: 'TEST_STORE_FORMAL_REDEEMS',
|
||||
message: '测试门店上有正式用户核销;这些门店将改为取消测试标记并保留,不删除',
|
||||
details: formalRedeemsAtTestStore,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (scope.testOrderIds.length) {
|
||||
const paidWineryItems = await prisma.wineryBillItem.findMany({
|
||||
where: {
|
||||
orderId: { in: scope.testOrderIds },
|
||||
wineryBill: { status: 'PAID' },
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
orderNo: true,
|
||||
wineryBill: { select: { billNo: true, status: true } },
|
||||
},
|
||||
take: 50,
|
||||
});
|
||||
if (paidWineryItems.length) {
|
||||
risks.push({
|
||||
code: 'PAID_WINERY_BILL_TEST_ORDERS',
|
||||
message: '酒厂账单已打款且含测试订单',
|
||||
details: paidWineryItems,
|
||||
});
|
||||
}
|
||||
|
||||
const paidLogisticsItems = await prisma.logisticsBillItem.findMany({
|
||||
where: {
|
||||
orderId: { in: scope.testOrderIds },
|
||||
logisticsBill: { status: 'PAID' },
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
orderNo: true,
|
||||
logisticsBill: { select: { billNo: true, status: true } },
|
||||
},
|
||||
take: 50,
|
||||
});
|
||||
if (paidLogisticsItems.length) {
|
||||
risks.push({
|
||||
code: 'PAID_LOGISTICS_BILL_TEST_ORDERS',
|
||||
message: '物流账单已打款且含测试订单',
|
||||
details: paidLogisticsItems,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (scope.testRedeemIds.length) {
|
||||
const paidPayouts = await prisma.storePayout.findMany({
|
||||
where: {
|
||||
redeemRecordId: { in: scope.testRedeemIds },
|
||||
status: 'PAID',
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
redeemRecordId: true,
|
||||
storeBill: { select: { billNo: true, status: true } },
|
||||
},
|
||||
take: 50,
|
||||
});
|
||||
if (paidPayouts.length) {
|
||||
risks.push({
|
||||
code: 'PAID_STORE_PAYOUT_TEST_REDEEMS',
|
||||
message: '门店打款已支付且含测试核销',
|
||||
details: paidPayouts,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (scope.limitedProductsWithFormalOrders.length) {
|
||||
risks.push({
|
||||
code: 'LIMITED_PRODUCT_FORMAL_ORDERS',
|
||||
message: '限测商品已有正式订单,将跳过删除',
|
||||
details: scope.limitedProductsWithFormalOrders,
|
||||
});
|
||||
}
|
||||
|
||||
return risks;
|
||||
}
|
||||
|
||||
export async function countTestData(prisma: PrismaClient) {
|
||||
const [
|
||||
whitelistCount,
|
||||
testUsers,
|
||||
testStoreAccounts,
|
||||
testPartners,
|
||||
testStores,
|
||||
testOrders,
|
||||
testRedeems,
|
||||
limitedProducts,
|
||||
visibilityStores,
|
||||
visibilityProducts,
|
||||
] = await Promise.all([
|
||||
prisma.commonTestWhitelistPhone.count(),
|
||||
prisma.user.count({ where: { isTest: true } }),
|
||||
prisma.storeAccount.count({ where: { isTest: true } }),
|
||||
prisma.partnerAccount.count({ where: { isTest: true } }),
|
||||
prisma.store.count({ where: { isTest: true } }),
|
||||
prisma.order.count({ where: { isTest: true } }),
|
||||
prisma.redeemRecord.count({ where: { isTest: true } }),
|
||||
prisma.commonProductItem.count({ where: { visibilityWhitelistEnabled: true } }),
|
||||
prisma.store.count({ where: { visibilityWhitelistEnabled: true } }),
|
||||
prisma.commonProductItem.count({ where: { visibilityWhitelistEnabled: true } }),
|
||||
]);
|
||||
|
||||
return {
|
||||
whitelistCount,
|
||||
testUsers,
|
||||
testStoreAccounts,
|
||||
testPartners,
|
||||
testStores,
|
||||
testOrders,
|
||||
testRedeems,
|
||||
limitedProducts,
|
||||
visibilityStores,
|
||||
visibilityProducts,
|
||||
};
|
||||
}
|
||||
|
||||
export function idsEmpty(ids: bigint[]): boolean {
|
||||
return ids.length === 0;
|
||||
}
|
||||
|
||||
export function inIds(ids: bigint[]): Prisma.BigIntFilter | undefined {
|
||||
return ids.length ? { in: ids } : undefined;
|
||||
}
|
||||
Reference in New Issue
Block a user