channel.service.ts 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. // box-mgnt-api/src/mgnt-backend/feature/channel/channel.service.ts
  2. import {
  3. Injectable,
  4. BadRequestException,
  5. NotFoundException,
  6. } from '@nestjs/common';
  7. import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';
  8. import { MongoPrismaService } from '@box/db/prisma/mongo-prisma.service';
  9. import { CacheSyncService } from '../../../cache-sync/cache-sync.service';
  10. import {
  11. CacheEntityType,
  12. CacheOperation,
  13. } from '../../../cache-sync/cache-sync.types';
  14. import {
  15. CreateChannelDto,
  16. ListChannelDto,
  17. UpdateChannelDto,
  18. } from './channel.dto';
  19. import { nowSecBigInt } from '@box/common/time/time.util';
  20. @Injectable()
  21. export class ChannelService {
  22. constructor(
  23. private readonly mongoPrismaService: MongoPrismaService,
  24. private readonly cacheSyncService: CacheSyncService,
  25. ) {}
  26. /**
  27. * Current epoch seconds (BigInt) for persisted timestamps.
  28. *
  29. * NOTE:
  30. * - We now keep `createAt`/`updateAt` as BigInt seconds.
  31. */
  32. private now(): bigint {
  33. return nowSecBigInt();
  34. }
  35. private trimOptional(value?: string | null) {
  36. if (value === undefined) return undefined;
  37. if (value === null) return null;
  38. return typeof value === 'string' ? value.trim() : value;
  39. }
  40. async create(dto: CreateChannelDto) {
  41. // Check for duplicate channel name
  42. const existingChannel = await this.mongoPrismaService.channel.findFirst({
  43. where: { name: dto.name },
  44. });
  45. if (existingChannel) {
  46. throw new BadRequestException(
  47. `Channel with name "${dto.name}" already exists`,
  48. );
  49. }
  50. // Check for duplicate channelId
  51. const existingChannelId = await this.mongoPrismaService.channel.findFirst({
  52. where: { channelId: dto.channelId },
  53. });
  54. if (existingChannelId) {
  55. throw new BadRequestException(
  56. `Channel with channelId "${dto.channelId}" already exists`,
  57. );
  58. }
  59. const isDefault = (await this.mongoPrismaService.channel.count()) === 0;
  60. const now = this.now();
  61. const channel = await this.mongoPrismaService.channel.create({
  62. data: {
  63. channelId: dto.channelId,
  64. name: dto.name,
  65. landingUrl: dto.landingUrl,
  66. videoCdn: this.trimOptional(dto.videoCdn) ?? null,
  67. coverCdn: this.trimOptional(dto.coverCdn) ?? null,
  68. clientName: this.trimOptional(dto.clientName) ?? null,
  69. clientNotice: this.trimOptional(dto.clientNotice) ?? null,
  70. remark: this.trimOptional(dto.remark) ?? null,
  71. isDefault,
  72. categories: (dto.categories as any) || null,
  73. tags: (dto.tags as any) || null,
  74. tagNames: dto.tagNames || [],
  75. createAt: now,
  76. updateAt: now,
  77. },
  78. });
  79. // Auto-schedule cache refresh
  80. await this.cacheSyncService.scheduleChannelRefreshAll();
  81. // Schedule channel-with-categories rebuild for this channel
  82. await this.cacheSyncService.scheduleAction({
  83. entityType: CacheEntityType.CHANNEL,
  84. operation: CacheOperation.REFRESH,
  85. payload: { channelId: channel.id },
  86. });
  87. return channel;
  88. }
  89. async update(dto: UpdateChannelDto) {
  90. // Check for duplicate channel name (excluding current channel)
  91. const duplicateChannel = await this.mongoPrismaService.channel.findFirst({
  92. where: {
  93. name: dto.name,
  94. id: { not: dto.id },
  95. },
  96. });
  97. if (duplicateChannel) {
  98. throw new BadRequestException(
  99. `Channel with name "${dto.name}" already exists`,
  100. );
  101. }
  102. // check for duplicate channelId (excluding current channel)
  103. const duplicateChannelId = await this.mongoPrismaService.channel.findFirst({
  104. where: {
  105. channelId: dto.channelId,
  106. id: { not: dto.id },
  107. },
  108. });
  109. if (duplicateChannelId) {
  110. throw new BadRequestException(
  111. `Channel with channelId "${dto.channelId}" already exists`,
  112. );
  113. }
  114. const now = this.now();
  115. try {
  116. const channel = await this.mongoPrismaService.channel.update({
  117. where: { id: dto.id },
  118. data: {
  119. channelId: dto.channelId,
  120. name: dto.name,
  121. landingUrl: dto.landingUrl,
  122. videoCdn: this.trimOptional(dto.videoCdn) ?? null,
  123. coverCdn: this.trimOptional(dto.coverCdn) ?? null,
  124. clientName: this.trimOptional(dto.clientName) ?? null,
  125. clientNotice: this.trimOptional(dto.clientNotice) ?? null,
  126. remark: this.trimOptional(dto.remark) ?? null,
  127. categories: (dto.categories as any) || null,
  128. tags: (dto.tags as any) || null,
  129. tagNames: dto.tagNames || [],
  130. updateAt: now,
  131. },
  132. });
  133. // Auto-schedule cache refresh
  134. await this.cacheSyncService.scheduleChannelRefreshAll();
  135. // Schedule channel-with-categories rebuild for this channel
  136. await this.cacheSyncService.scheduleAction({
  137. entityType: CacheEntityType.CHANNEL,
  138. operation: CacheOperation.REFRESH,
  139. payload: { channelId: channel.id },
  140. });
  141. return channel;
  142. } catch (e) {
  143. if (e instanceof PrismaClientKnownRequestError && e.code === 'P2025') {
  144. throw new NotFoundException('Channel not found');
  145. }
  146. throw e;
  147. }
  148. }
  149. async findOne(id: string) {
  150. const row = await this.mongoPrismaService.channel.findUnique({
  151. where: { id },
  152. });
  153. if (!row) {
  154. throw new NotFoundException('Channel not found');
  155. }
  156. return row;
  157. }
  158. async list(dto: ListChannelDto) {
  159. const where: any = {};
  160. if (dto.name) {
  161. where.name = { contains: dto.name, mode: 'insensitive' };
  162. }
  163. // Defensive guards for pagination
  164. const page = dto.page && dto.page > 0 ? dto.page : 1;
  165. const size = dto.size && dto.size > 0 ? dto.size : 10;
  166. const [total, data] = await this.mongoPrismaService.$transaction([
  167. this.mongoPrismaService.channel.count({ where }),
  168. this.mongoPrismaService.channel.findMany({
  169. where,
  170. orderBy: { updateAt: 'desc' },
  171. skip: (page - 1) * size,
  172. take: size,
  173. }),
  174. ]);
  175. return {
  176. total,
  177. data,
  178. totalPages: Math.ceil(total / size),
  179. page,
  180. size,
  181. };
  182. }
  183. async remove(id: string) {
  184. try {
  185. await this.mongoPrismaService.channel.delete({ where: { id } });
  186. // Auto-schedule cache refresh
  187. await this.cacheSyncService.scheduleChannelRefreshAll();
  188. return { message: 'Deleted' };
  189. } catch (e) {
  190. if (e instanceof PrismaClientKnownRequestError && e.code === 'P2025') {
  191. throw new NotFoundException('Channel not found');
  192. }
  193. throw e;
  194. }
  195. }
  196. }