seed-ads.ts 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. import {
  2. PrismaClient,
  3. AdType,
  4. ImageSource,
  5. Prisma,
  6. } from '@prisma/mongo/client';
  7. export function nowSecBigInt(): bigint {
  8. return BigInt(Math.floor(Date.now() / 1000));
  9. }
  10. const prisma = new PrismaClient();
  11. type SeedArgs = {
  12. clean: boolean;
  13. perType: number;
  14. adIdStart: number;
  15. };
  16. function parseArgs(argv: string[]): SeedArgs {
  17. const clean =
  18. argv.includes('--clean') ||
  19. argv.includes('-c') ||
  20. process.env.SEED_CLEAN === '1' ||
  21. process.env.SEED_CLEAN === 'true';
  22. const perType = (() => {
  23. const idx = argv.findIndex((a) => a === '--perType');
  24. if (idx >= 0) {
  25. const v = Number(argv[idx + 1]);
  26. if (Number.isFinite(v) && v > 0) return Math.floor(v);
  27. }
  28. const envValue = Number(process.env.SEED_PER_TYPE);
  29. if (Number.isFinite(envValue) && envValue > 0) return Math.floor(envValue);
  30. return 100;
  31. })();
  32. const adIdStart = (() => {
  33. const idx = argv.findIndex((a) => a === '--adIdStart');
  34. if (idx >= 0) {
  35. const v = Number(argv[idx + 1]);
  36. if (Number.isFinite(v) && v > 0) return Math.floor(v);
  37. }
  38. const envValue = Number(process.env.SEED_ADID_START);
  39. if (Number.isFinite(envValue) && envValue > 0) return Math.floor(envValue);
  40. return 1;
  41. })();
  42. return { clean, perType, adIdStart };
  43. }
  44. function nowEpochSec(): bigint {
  45. return nowSecBigInt();
  46. }
  47. function randInt(min: number, max: number): number {
  48. // inclusive range
  49. return Math.floor(Math.random() * (max - min + 1)) + min;
  50. }
  51. function pick<T>(arr: readonly T[]): T {
  52. return arr[randInt(0, arr.length - 1)];
  53. }
  54. function clampLen(s: string, maxLen: number): string {
  55. return s.length <= maxLen ? s : s.slice(0, maxLen);
  56. }
  57. function makeShortLabel(prefix: string, maxLen = 20): string {
  58. const tail = randInt(1, 9999).toString().padStart(4, '0');
  59. return clampLen(`${prefix}${tail}`, maxLen);
  60. }
  61. function makeCoverKey(adType: AdType, idx: number): string {
  62. const n = String(idx + 1).padStart(3, '0');
  63. return `ads/${adType.toLowerCase()}/img_${n}.png`;
  64. }
  65. function makeAdsUrl(adType: AdType): string {
  66. const id = randInt(100000, 999999);
  67. return `https://example.com/ads/${adType.toLowerCase()}?c=${id}`;
  68. }
  69. function makeContent(adType: AdType): string {
  70. const phrases = [
  71. '限时优惠',
  72. '新人专享',
  73. '立即领取',
  74. '官方推荐',
  75. '热卖爆款',
  76. '今日必看',
  77. '立刻参与',
  78. '福利放送',
  79. '超值精选',
  80. '品质保证',
  81. ] as const;
  82. const base = `${pick(phrases)} · ${adType}`;
  83. const extra = `|${pick(phrases)}|编号${randInt(1000, 9999)}`;
  84. return clampLen(base + extra, 500);
  85. }
  86. function makeTimeWindow(): { startDt: bigint; expiryDt: bigint } {
  87. const now = Number(nowEpochSec());
  88. const startOffsetDays = randInt(-30, 30);
  89. const start = now + startOffsetDays * 86400;
  90. const durationDays = randInt(7, 90);
  91. const expiry = start + durationDays * 86400;
  92. return { startDt: BigInt(start), expiryDt: BigInt(expiry) };
  93. }
  94. /**
  95. * Align the Mongo "counters" collection with the next adId we plan to use.
  96. * This ensures future runtime allocator (findAndModify $inc) won't collide.
  97. *
  98. * We set counters.seq = maxExistingAdId (or adIdStart-1 if empty),
  99. * so the next allocate will return max+1.
  100. */
  101. async function ensureAdIdCounterAligned(adIdStart: number) {
  102. const maxRow = await prisma.ads.findFirst({
  103. select: { adId: true },
  104. orderBy: { adId: 'desc' as any }, // Prisma Mongo sometimes needs 'as any' for orderBy on optional field
  105. });
  106. const maxExisting =
  107. typeof maxRow?.adId === 'number' ? maxRow.adId : undefined;
  108. const base = Math.max((adIdStart ?? 1) - 1, maxExisting ?? 0);
  109. await prisma.$runCommandRaw({
  110. update: 'counters',
  111. updates: [
  112. {
  113. q: { _id: 'ads_adId' },
  114. u: { $set: { seq: base } },
  115. upsert: true,
  116. },
  117. ],
  118. });
  119. console.log(`[seed:ads] counters.ads_adId aligned: seq=${base}`);
  120. }
  121. async function main() {
  122. const args = parseArgs(process.argv.slice(2));
  123. const tsNow = nowEpochSec();
  124. const adTypes = Object.values(AdType) as AdType[];
  125. if (args.clean) {
  126. await prisma.ads.deleteMany();
  127. console.log('[seed:ads] cleaned ads collection');
  128. }
  129. // Important: If your app uses the atomic allocator, align the counter to avoid collisions.
  130. await ensureAdIdCounterAligned(args.adIdStart);
  131. const advertiserPrefixes = [
  132. '广告商A',
  133. '广告商B',
  134. '广告商C',
  135. '品牌X',
  136. '品牌Y',
  137. '品牌Z',
  138. ] as const;
  139. const titlePrefixes = [
  140. '爆款推荐',
  141. '限时福利',
  142. '新品上线',
  143. '今日热推',
  144. '官方活动',
  145. '专属礼包',
  146. ] as const;
  147. const batch: Prisma.AdsCreateManyInput[] = [];
  148. let nextAdId = args.adIdStart;
  149. for (const adType of adTypes) {
  150. for (let idx = 0; idx < args.perType; idx++) {
  151. const { startDt, expiryDt } = makeTimeWindow();
  152. const advertiser = makeShortLabel(pick(advertiserPrefixes), 20);
  153. const title = makeShortLabel(pick(titlePrefixes), 20);
  154. const adsCoverImg =
  155. Math.random() < 0.85 ? makeCoverKey(adType, idx) : null;
  156. const adsUrl = Math.random() < 0.9 ? makeAdsUrl(adType) : null;
  157. const adsContent = Math.random() < 0.8 ? makeContent(adType) : null;
  158. batch.push({
  159. adId: nextAdId++, // ✅ NEW
  160. adType,
  161. advertiser,
  162. title,
  163. adsContent,
  164. adsCoverImg,
  165. adsUrl,
  166. imgSource: ImageSource.LOCAL_ONLY,
  167. startDt,
  168. expiryDt,
  169. seq: randInt(0, 9999),
  170. status: Math.random() < 0.9 ? 1 : 0,
  171. createAt: tsNow,
  172. updateAt: tsNow,
  173. });
  174. }
  175. }
  176. const result = await prisma.ads.createMany({ data: batch });
  177. console.log(
  178. `[seed:ads] adTypes=${adTypes.length} perType=${args.perType} inserted=${result.count} clean=${args.clean} adIdStart=${args.adIdStart}`,
  179. );
  180. }
  181. main()
  182. .catch((error) => {
  183. console.error('[seed:ads] failed:', error);
  184. process.exitCode = 1;
  185. })
  186. .finally(async () => {
  187. await prisma.$disconnect();
  188. });
  189. /**
  190. * Usage:
  191. * pnpm ts-node ./path/to/seed-ads.ts --clean --perType 50 --adIdStart 1
  192. *
  193. * Env:
  194. * SEED_CLEAN=true
  195. * SEED_PER_TYPE=100
  196. * SEED_ADID_START=1
  197. */