|
|
@@ -67,87 +67,47 @@ export class ChannelService {
|
|
|
}));
|
|
|
}
|
|
|
|
|
|
- private async generateNextChannelNo(): Promise<number> {
|
|
|
- const last = await this.mongoPrismaService.channel.findFirst({
|
|
|
- where: { channelNo: { isSet: true } },
|
|
|
- orderBy: { channelNo: 'desc' },
|
|
|
- select: { channelNo: true },
|
|
|
+ // private async generateNextChannelNo(): Promise<number> {
|
|
|
+ // const last = await this.mongoPrismaService.channel.findFirst({
|
|
|
+ // where: { channelNo: { isSet: true } },
|
|
|
+ // orderBy: { channelNo: 'desc' },
|
|
|
+ // select: { channelNo: true },
|
|
|
+ // });
|
|
|
+
|
|
|
+ // return (last?.channelNo ?? 0) + 1;
|
|
|
+ // }
|
|
|
+
|
|
|
+ private async generateNextChannelIdNo(): Promise<number> {
|
|
|
+ const res = await this.mongoPrismaService.$runCommandRaw({
|
|
|
+ aggregate: 'channel',
|
|
|
+ pipeline: [
|
|
|
+ // If you want extra safety, uncomment this:
|
|
|
+ // { $match: { channelId: { $regex: /^[0-9]+$/ } } },
|
|
|
+ { $match: { channelId: { $type: 'string' } } },
|
|
|
+ { $project: { n: { $toLong: '$channelId' } } },
|
|
|
+ { $group: { _id: null, maxN: { $max: '$n' } } },
|
|
|
+ ],
|
|
|
+ cursor: {},
|
|
|
});
|
|
|
|
|
|
- return (last?.channelNo ?? 0) + 1;
|
|
|
- }
|
|
|
-
|
|
|
- private async backfillIds(): Promise<void> {
|
|
|
- if (this.isBackfilling) {
|
|
|
- this.logger.warn('backfill is already running, skipping.');
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- this.isBackfilling = true;
|
|
|
- this.logger.log('Starting backfill of id...');
|
|
|
- try {
|
|
|
- const withoutId = await this.mongoPrismaService.channel.findMany({
|
|
|
- where: {
|
|
|
- OR: [{ channelNo: { isSet: false } }, { channelNo: null }],
|
|
|
- },
|
|
|
- orderBy: { createAt: 'asc' },
|
|
|
- select: { id: true },
|
|
|
- });
|
|
|
-
|
|
|
- if (withoutId.length === 0) {
|
|
|
- this.logger.log('No channels need backfilling channelNo.');
|
|
|
- return;
|
|
|
- }
|
|
|
+ const first = (res as any)?.cursor?.firstBatch?.[0];
|
|
|
+ const maxN = first?.maxN;
|
|
|
|
|
|
- this.logger.log(
|
|
|
- `Found ${withoutId.length} channels without channelNo. Starting backfill...`,
|
|
|
- );
|
|
|
+ const maxNum =
|
|
|
+ typeof maxN === 'number'
|
|
|
+ ? maxN
|
|
|
+ : typeof maxN?.toNumber === 'function'
|
|
|
+ ? maxN.toNumber()
|
|
|
+ : Number(maxN ?? 0);
|
|
|
|
|
|
- let nextUid = await this.generateNextChannelNo();
|
|
|
-
|
|
|
- for (const channel of withoutId) {
|
|
|
- let assigned = false;
|
|
|
-
|
|
|
- while (!assigned) {
|
|
|
- try {
|
|
|
- await this.mongoPrismaService.channel.update({
|
|
|
- where: { id: channel.id },
|
|
|
- data: { channelNo: nextUid },
|
|
|
- });
|
|
|
-
|
|
|
- this.logger.log(
|
|
|
- `Backfilled channelNo ${nextUid} for channel id ${channel.id}`,
|
|
|
- );
|
|
|
-
|
|
|
- nextUid += 1;
|
|
|
- assigned = true;
|
|
|
- } catch (e: any) {
|
|
|
- // Unique constraint violation → retry with a fresh number
|
|
|
- if (e?.code === 'P2002') {
|
|
|
- this.logger.warn(`Duplicate channelNo ${nextUid}, retrying...`);
|
|
|
- nextUid = await this.generateNextChannelNo();
|
|
|
- } else {
|
|
|
- throw e;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- this.logger.log(
|
|
|
- `Backfilled ${withoutId.length} channelNos successfully.`,
|
|
|
- );
|
|
|
- } finally {
|
|
|
- this.isBackfilling = false;
|
|
|
- this.logger.log('Finished backfilling process.');
|
|
|
+ if (!Number.isFinite(maxNum) || maxNum < 0) {
|
|
|
+ throw new Error(`Invalid max channelId computed: ${String(maxN)}`);
|
|
|
}
|
|
|
+
|
|
|
+ return maxNum + 1;
|
|
|
}
|
|
|
|
|
|
async create(dto: CreateChannelDto) {
|
|
|
- await this.backfillIds().catch((e) => {
|
|
|
- this.logger.error('Error during backfillIds:', e);
|
|
|
- });
|
|
|
-
|
|
|
- // Check for duplicate channel name
|
|
|
const existingChannel = await this.mongoPrismaService.channel.findFirst({
|
|
|
where: { name: dto.name },
|
|
|
select: { id: true },
|
|
|
@@ -171,8 +131,7 @@ export class ChannelService {
|
|
|
const isDefault = (await this.mongoPrismaService.channel.count()) === 0;
|
|
|
const now = this.now();
|
|
|
|
|
|
- // Generate and create with retry to avoid duplicate channelNo under concurrency
|
|
|
- let nextNo = await this.generateNextChannelNo();
|
|
|
+ let nextNo = await this.generateNextChannelIdNo();
|
|
|
let channel: Awaited<
|
|
|
ReturnType<typeof this.mongoPrismaService.channel.create>
|
|
|
> | null = null;
|
|
|
@@ -181,8 +140,7 @@ export class ChannelService {
|
|
|
try {
|
|
|
channel = await this.mongoPrismaService.channel.create({
|
|
|
data: {
|
|
|
- channelNo: nextNo,
|
|
|
- channelId: String(nextNo), // channelId mirrors channelNo on create
|
|
|
+ channelId: String(nextNo), // numeric string, generated
|
|
|
name: dto.name,
|
|
|
landingUrl: dto.landingUrl,
|
|
|
videoCdn: this.trimOptional(dto.videoCdn) ?? null,
|
|
|
@@ -204,17 +162,17 @@ export class ChannelService {
|
|
|
(e instanceof PrismaClientKnownRequestError && e.code === 'P2002');
|
|
|
|
|
|
if (isP2002) {
|
|
|
- this.logger.warn(`Duplicate channelNo ${nextNo}, retrying...`);
|
|
|
- nextNo = await this.generateNextChannelNo();
|
|
|
+ this.logger.warn(
|
|
|
+ `Duplicate channelId "${String(nextNo)}", retrying...`,
|
|
|
+ );
|
|
|
+ nextNo = await this.generateNextChannelIdNo();
|
|
|
continue;
|
|
|
}
|
|
|
throw e;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // Auto-schedule cache refresh
|
|
|
await this.cacheSyncService.scheduleChannelRefreshAll();
|
|
|
- // Schedule channel-with-categories rebuild for this channel
|
|
|
await this.cacheSyncService.scheduleAction({
|
|
|
entityType: CacheEntityType.CHANNEL,
|
|
|
operation: CacheOperation.REFRESH,
|
|
|
@@ -301,9 +259,6 @@ export class ChannelService {
|
|
|
}
|
|
|
|
|
|
async list(dto: ListChannelDto) {
|
|
|
- await this.backfillIds().catch((e) => {
|
|
|
- this.logger.error('Error during backfillIds:', e);
|
|
|
- });
|
|
|
const where: any = {};
|
|
|
|
|
|
if (dto.name) {
|