| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231 |
- import {
- PrismaClient,
- AdType,
- ImageSource,
- Prisma,
- } from '@prisma/mongo/client';
- export function nowSecBigInt(): bigint {
- return BigInt(Math.floor(Date.now() / 1000));
- }
- const prisma = new PrismaClient();
- type SeedArgs = {
- clean: boolean;
- perType: number;
- adIdStart: number;
- };
- function parseArgs(argv: string[]): SeedArgs {
- const clean =
- argv.includes('--clean') ||
- argv.includes('-c') ||
- process.env.SEED_CLEAN === '1' ||
- process.env.SEED_CLEAN === 'true';
- const perType = (() => {
- const idx = argv.findIndex((a) => a === '--perType');
- if (idx >= 0) {
- const v = Number(argv[idx + 1]);
- if (Number.isFinite(v) && v > 0) return Math.floor(v);
- }
- const envValue = Number(process.env.SEED_PER_TYPE);
- if (Number.isFinite(envValue) && envValue > 0) return Math.floor(envValue);
- return 100;
- })();
- const adIdStart = (() => {
- const idx = argv.findIndex((a) => a === '--adIdStart');
- if (idx >= 0) {
- const v = Number(argv[idx + 1]);
- if (Number.isFinite(v) && v > 0) return Math.floor(v);
- }
- const envValue = Number(process.env.SEED_ADID_START);
- if (Number.isFinite(envValue) && envValue > 0) return Math.floor(envValue);
- return 1;
- })();
- return { clean, perType, adIdStart };
- }
- function nowEpochSec(): bigint {
- return nowSecBigInt();
- }
- function randInt(min: number, max: number): number {
- // inclusive range
- return Math.floor(Math.random() * (max - min + 1)) + min;
- }
- function pick<T>(arr: readonly T[]): T {
- return arr[randInt(0, arr.length - 1)];
- }
- function clampLen(s: string, maxLen: number): string {
- return s.length <= maxLen ? s : s.slice(0, maxLen);
- }
- function makeShortLabel(prefix: string, maxLen = 20): string {
- const tail = randInt(1, 9999).toString().padStart(4, '0');
- return clampLen(`${prefix}${tail}`, maxLen);
- }
- function makeCoverKey(adType: AdType, idx: number): string {
- const n = String(idx + 1).padStart(3, '0');
- return `ads/${adType.toLowerCase()}/img_${n}.png`;
- }
- function makeAdsUrl(adType: AdType): string {
- const id = randInt(100000, 999999);
- return `https://example.com/ads/${adType.toLowerCase()}?c=${id}`;
- }
- function makeContent(adType: AdType): string {
- const phrases = [
- '限时优惠',
- '新人专享',
- '立即领取',
- '官方推荐',
- '热卖爆款',
- '今日必看',
- '立刻参与',
- '福利放送',
- '超值精选',
- '品质保证',
- ] as const;
- const base = `${pick(phrases)} · ${adType}`;
- const extra = `|${pick(phrases)}|编号${randInt(1000, 9999)}`;
- return clampLen(base + extra, 500);
- }
- function makeTimeWindow(): { startDt: bigint; expiryDt: bigint } {
- const now = Number(nowEpochSec());
- const startOffsetDays = randInt(-30, 30);
- const start = now + startOffsetDays * 86400;
- const durationDays = randInt(7, 90);
- const expiry = start + durationDays * 86400;
- return { startDt: BigInt(start), expiryDt: BigInt(expiry) };
- }
- /**
- * Align the Mongo "counters" collection with the next adId we plan to use.
- * This ensures future runtime allocator (findAndModify $inc) won't collide.
- *
- * We set counters.seq = maxExistingAdId (or adIdStart-1 if empty),
- * so the next allocate will return max+1.
- */
- async function ensureAdIdCounterAligned(adIdStart: number) {
- const maxRow = await prisma.ads.findFirst({
- select: { adId: true },
- orderBy: { adId: 'desc' as any }, // Prisma Mongo sometimes needs 'as any' for orderBy on optional field
- });
- const maxExisting =
- typeof maxRow?.adId === 'number' ? maxRow.adId : undefined;
- const base = Math.max((adIdStart ?? 1) - 1, maxExisting ?? 0);
- await prisma.$runCommandRaw({
- update: 'counters',
- updates: [
- {
- q: { _id: 'ads_adId' },
- u: { $set: { seq: base } },
- upsert: true,
- },
- ],
- });
- console.log(`[seed:ads] counters.ads_adId aligned: seq=${base}`);
- }
- async function main() {
- const args = parseArgs(process.argv.slice(2));
- const tsNow = nowEpochSec();
- const adTypes = Object.values(AdType) as AdType[];
- if (args.clean) {
- await prisma.ads.deleteMany();
- console.log('[seed:ads] cleaned ads collection');
- }
- // Important: If your app uses the atomic allocator, align the counter to avoid collisions.
- await ensureAdIdCounterAligned(args.adIdStart);
- const advertiserPrefixes = [
- '广告商A',
- '广告商B',
- '广告商C',
- '品牌X',
- '品牌Y',
- '品牌Z',
- ] as const;
- const titlePrefixes = [
- '爆款推荐',
- '限时福利',
- '新品上线',
- '今日热推',
- '官方活动',
- '专属礼包',
- ] as const;
- const batch: Prisma.AdsCreateManyInput[] = [];
- let nextAdId = args.adIdStart;
- for (const adType of adTypes) {
- for (let idx = 0; idx < args.perType; idx++) {
- const { startDt, expiryDt } = makeTimeWindow();
- const advertiser = makeShortLabel(pick(advertiserPrefixes), 20);
- const title = makeShortLabel(pick(titlePrefixes), 20);
- const adsCoverImg =
- Math.random() < 0.85 ? makeCoverKey(adType, idx) : null;
- const adsUrl = Math.random() < 0.9 ? makeAdsUrl(adType) : null;
- const adsContent = Math.random() < 0.8 ? makeContent(adType) : null;
- batch.push({
- adId: nextAdId++, // ✅ NEW
- adType,
- advertiser,
- title,
- adsContent,
- adsCoverImg,
- adsUrl,
- imgSource: ImageSource.LOCAL_ONLY,
- startDt,
- expiryDt,
- seq: randInt(0, 9999),
- status: Math.random() < 0.9 ? 1 : 0,
- createAt: tsNow,
- updateAt: tsNow,
- });
- }
- }
- const result = await prisma.ads.createMany({ data: batch });
- console.log(
- `[seed:ads] adTypes=${adTypes.length} perType=${args.perType} inserted=${result.count} clean=${args.clean} adIdStart=${args.adIdStart}`,
- );
- }
- main()
- .catch((error) => {
- console.error('[seed:ads] failed:', error);
- process.exitCode = 1;
- })
- .finally(async () => {
- await prisma.$disconnect();
- });
- /**
- * Usage:
- * pnpm ts-node ./path/to/seed-ads.ts --clean --perType 50 --adIdStart 1
- *
- * Env:
- * SEED_CLEAN=true
- * SEED_PER_TYPE=100
- * SEED_ADID_START=1
- */
|