Kaynağa Gözat

添加聊天群管理功能,包括创建、更新、查询和列表接口,更新相关 DTO 和服务,增强错误处理和日志记录。

wuyi 1 ay önce
ebeveyn
işleme
3d8d9be52c

+ 2 - 0
src/app.ts

@@ -17,6 +17,7 @@ import messagesRoutes from './routes/messages.routes'
 import tgMsgSendRoutes from './routes/tg-msg-send.routes'
 import tgMsgSendRoutes from './routes/tg-msg-send.routes'
 import taskRoutes from './routes/task.routes'
 import taskRoutes from './routes/task.routes'
 import senderRoutes from './routes/sender.routes'
 import senderRoutes from './routes/sender.routes'
+import chatGroupRoutes from './routes/chat-group.routes'
 
 
 const options: FastifyEnvOptions = {
 const options: FastifyEnvOptions = {
   schema: schema,
   schema: schema,
@@ -90,6 +91,7 @@ export const createApp = async () => {
   app.register(tgMsgSendRoutes, { prefix: '/api/msg' })
   app.register(tgMsgSendRoutes, { prefix: '/api/msg' })
   app.register(taskRoutes, { prefix: '/api/tasks' })
   app.register(taskRoutes, { prefix: '/api/tasks' })
   app.register(senderRoutes, { prefix: '/api/senders' })
   app.register(senderRoutes, { prefix: '/api/senders' })
+  app.register(chatGroupRoutes, { prefix: '/api/chat-groups' })
   const dataSource = createDataSource(app)
   const dataSource = createDataSource(app)
   await dataSource.initialize()
   await dataSource.initialize()
   app.decorate('dataSource', dataSource)
   app.decorate('dataSource', dataSource)

+ 81 - 0
src/controllers/chat-group.controller.ts

@@ -0,0 +1,81 @@
+import { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'
+import { ChatGroupService } from '../services/chat-group.service'
+import {
+  CreateChatGroupRecordBody,
+  ListChatGroupQuery,
+  UpdateChatGroupRecordBody
+} from '../dto/chat-group.dto'
+
+export class ChatGroupController {
+  private chatGroupService: ChatGroupService
+
+  constructor(app: FastifyInstance) {
+    this.chatGroupService = new ChatGroupService(app)
+  }
+
+  async createOrUpdate(
+    request: FastifyRequest<{ Body: CreateChatGroupRecordBody }>,
+    reply: FastifyReply
+  ): Promise<FastifyReply> {
+    try {
+      const payload = request.body
+      if (!payload.chatId || !payload.accessHash) {
+        return reply.code(400).send({ message: 'chatId 和 accessHash 为必填项' })
+      }
+      const saved = await this.chatGroupService.upsertGroup(payload)
+      return reply.code(201).send({ message: '保存成功', data: saved })
+    } catch (error) {
+      return reply
+        .code(500)
+        .send({ message: '保存失败', error: error instanceof Error ? error.message : String(error) })
+    }
+  }
+
+  async update(
+    request: FastifyRequest<{ Body: UpdateChatGroupRecordBody }>,
+    reply: FastifyReply
+  ): Promise<FastifyReply> {
+    try {
+      const payload = request.body
+      if (!payload.chatId) {
+        return reply.code(400).send({ message: 'chatId 不能为空' })
+      }
+      const updated = await this.chatGroupService.updateGroup(payload)
+      if (!updated) {
+        return reply.code(404).send({ message: '群组不存在' })
+      }
+      return reply.send({ message: '更新成功', data: updated })
+    } catch (error) {
+      return reply
+        .code(500)
+        .send({ message: '更新失败', error: error instanceof Error ? error.message : String(error) })
+    }
+  }
+
+  async getById(request: FastifyRequest<{ Params: { chatId: string } }>, reply: FastifyReply): Promise<FastifyReply> {
+    try {
+      const { chatId } = request.params
+      const data = await this.chatGroupService.findByChatId(chatId)
+      if (!data) {
+        return reply.code(404).send({ message: '群组不存在' })
+      }
+      return reply.send({ data })
+    } catch (error) {
+      return reply
+        .code(500)
+        .send({ message: '查询失败', error: error instanceof Error ? error.message : String(error) })
+    }
+  }
+
+  async list(request: FastifyRequest<{ Querystring: ListChatGroupQuery }>, reply: FastifyReply): Promise<FastifyReply> {
+    try {
+      const result = await this.chatGroupService.list(request.query)
+      return reply.send(result)
+    } catch (error) {
+      return reply
+        .code(500)
+        .send({ message: '查询失败', error: error instanceof Error ? error.message : String(error) })
+    }
+  }
+}
+

+ 3 - 2
src/controllers/task.controller.ts

@@ -204,7 +204,7 @@ export class TaskController {
   }
   }
 
 
   async testCreateChatGroup(request: FastifyRequest<{ Body: CreateChatGroupBody }>, reply: FastifyReply) {
   async testCreateChatGroup(request: FastifyRequest<{ Body: CreateChatGroupBody }>, reply: FastifyReply) {
-    const { senderId, session, dcId, authKey, groupName, groupDescription, groupType } = request.body
+    const { senderId, session, dcId, authKey, groupName, groupDescription, groupType, initMsg } = request.body
 
 
     if (!senderId || !groupName || !groupType) {
     if (!senderId || !groupName || !groupType) {
       return reply.code(400).send({
       return reply.code(400).send({
@@ -228,7 +228,8 @@ export class TaskController {
       groupType,
       groupType,
       session,
       session,
       dcId,
       dcId,
-      authKey
+      authKey,
+      initMsg
     )
     )
 
 
     if (result.success) {
     if (result.success) {

+ 26 - 0
src/dto/chat-group.dto.ts

@@ -0,0 +1,26 @@
+import { Pagination } from './common.dto'
+
+export interface CreateChatGroupRecordBody {
+  chatId: string
+  accessHash: string
+  name?: string
+  groupType?: string
+  inviteLink?: string
+  publicLink?: string
+  senderId?: string
+}
+
+export interface UpdateChatGroupRecordBody {
+  chatId: string
+  name?: string
+  groupType?: string
+  inviteLink?: string
+  publicLink?: string
+  delFlag?: boolean
+}
+
+export interface ListChatGroupQuery extends Pagination {
+  senderId?: string
+  delFlag?: boolean
+}
+

+ 1 - 0
src/dto/task.dto.ts

@@ -53,6 +53,7 @@ export interface CreateChatGroupBody {
   groupDescription: string
   groupDescription: string
   groupType: string
   groupType: string
   groupLink: string
   groupLink: string
+  initMsg?: string
 }
 }
 
 
 export interface InviteMembersBody {
 export interface InviteMembersBody {

+ 36 - 0
src/entities/chat-group.entity.ts

@@ -0,0 +1,36 @@
+import { Column, CreateDateColumn, Entity, Index, PrimaryColumn, UpdateDateColumn } from 'typeorm'
+
+@Entity()
+@Index(['senderId', 'delFlag'])
+@Index(['delFlag', 'createdAt'])
+export class ChatGroup {
+  @PrimaryColumn({ type: 'bigint' })
+  chatId: string
+
+  @Column({ type: 'bigint' })
+  accessHash: string
+
+  @Column({ type: 'bigint', nullable: true })
+  senderId: string
+
+  @Column({ nullable: true })
+  name: string
+
+  @Column({ nullable: true })
+  groupType: string
+
+  @Column({ nullable: true })
+  publicLink: string
+
+  @Column({ nullable: true })
+  inviteLink: string
+
+  @Column({ default: false })
+  delFlag: boolean
+
+  @CreateDateColumn()
+  createdAt: Date
+
+  @UpdateDateColumn()
+  updatedAt: Date
+}

+ 34 - 0
src/routes/chat-group.routes.ts

@@ -0,0 +1,34 @@
+import { FastifyInstance } from 'fastify'
+import { ChatGroupController } from '../controllers/chat-group.controller'
+import { hasRole } from '../middlewares/auth.middleware'
+import { UserRole } from '../entities/user.entity'
+import { CreateChatGroupRecordBody, ListChatGroupQuery, UpdateChatGroupRecordBody } from '../dto/chat-group.dto'
+
+export default async function chatGroupRoutes(fastify: FastifyInstance) {
+  const controller = new ChatGroupController(fastify)
+
+  fastify.post<{ Body: CreateChatGroupRecordBody }>(
+    '/',
+    { onRequest: [hasRole(UserRole.ADMIN)] },
+    controller.createOrUpdate.bind(controller)
+  )
+
+  fastify.put<{ Body: UpdateChatGroupRecordBody }>(
+    '/',
+    { onRequest: [hasRole(UserRole.ADMIN)] },
+    controller.update.bind(controller)
+  )
+
+  fastify.get<{ Params: { chatId: string } }>(
+    '/:chatId',
+    { onRequest: [hasRole(UserRole.ADMIN)] },
+    controller.getById.bind(controller)
+  )
+
+  fastify.get<{ Querystring: ListChatGroupQuery }>(
+    '/',
+    { onRequest: [hasRole(UserRole.ADMIN)] },
+    controller.list.bind(controller)
+  )
+}
+

+ 64 - 0
src/services/chat-group.service.ts

@@ -0,0 +1,64 @@
+import { FastifyInstance } from 'fastify'
+import { Repository } from 'typeorm'
+import { ChatGroup } from '../entities/chat-group.entity'
+import { PaginationResponse } from '../dto/common.dto'
+import { CreateChatGroupRecordBody, ListChatGroupQuery, UpdateChatGroupRecordBody } from '../dto/chat-group.dto'
+
+export class ChatGroupService {
+  private chatGroupRepository: Repository<ChatGroup>
+  private app: FastifyInstance
+
+  constructor(app: FastifyInstance) {
+    this.app = app
+    this.chatGroupRepository = app.dataSource.getRepository(ChatGroup)
+  }
+
+  async upsertGroup(payload: CreateChatGroupRecordBody): Promise<ChatGroup> {
+    const existing = await this.chatGroupRepository.findOne({ where: { chatId: payload.chatId } })
+    const data = {
+      ...existing,
+      ...payload,
+      chatId: payload.chatId,
+      accessHash: payload.accessHash
+    }
+    const entity = this.chatGroupRepository.create(data)
+    return await this.chatGroupRepository.save(entity)
+  }
+
+  async updateGroup(payload: UpdateChatGroupRecordBody): Promise<ChatGroup | null> {
+    const existing = await this.chatGroupRepository.findOne({ where: { chatId: payload.chatId } })
+    if (!existing) {
+      return null
+    }
+    await this.chatGroupRepository.update(payload.chatId, payload)
+    return await this.chatGroupRepository.findOne({ where: { chatId: payload.chatId } })
+  }
+
+  async findByChatId(chatId: string): Promise<ChatGroup | null> {
+    return await this.chatGroupRepository.findOne({ where: { chatId, delFlag: false } })
+  }
+
+  async list(query: ListChatGroupQuery): Promise<PaginationResponse<ChatGroup>> {
+    const { page = 0, size = 20, senderId, delFlag } = query
+    const where: any = {}
+    if (senderId !== undefined) where.senderId = senderId
+    if (delFlag !== undefined) where.delFlag = delFlag
+
+    const [content, total] = await this.chatGroupRepository.findAndCount({
+      where,
+      skip: (Number(page) || 0) * (Number(size) || 20),
+      take: Number(size) || 20,
+      order: { createdAt: 'DESC' }
+    })
+
+    return {
+      content,
+      metadata: {
+        total: Number(total),
+        page: Number(page) || 0,
+        size: Number(size) || 20
+      }
+    }
+  }
+}
+

+ 74 - 23
src/services/task.service.ts

@@ -4,6 +4,7 @@ import { Task } from '../entities/task.entity'
 import { TaskItem, TaskItemStatus } from '../entities/task-item.entity'
 import { TaskItem, TaskItemStatus } from '../entities/task-item.entity'
 import { PaginationResponse } from '../dto/common.dto'
 import { PaginationResponse } from '../dto/common.dto'
 import { Sender } from '../entities/sender.entity'
 import { Sender } from '../entities/sender.entity'
+import { ChatGroupService } from './chat-group.service'
 import { TgClientService } from './tgClient.service'
 import { TgClientService } from './tgClient.service'
 import { SenderService } from './sender.service'
 import { SenderService } from './sender.service'
 import { buildStringSession, buildStringSessionByDcIdAndAuthKey } from '../utils/tg.util'
 import { buildStringSession, buildStringSessionByDcIdAndAuthKey } from '../utils/tg.util'
@@ -15,6 +16,7 @@ export class TaskService {
   private senderRepository: Repository<Sender>
   private senderRepository: Repository<Sender>
   private senderService: SenderService
   private senderService: SenderService
   private tgClientService: TgClientService
   private tgClientService: TgClientService
+  private chatGroupService: ChatGroupService
   private app: FastifyInstance
   private app: FastifyInstance
 
 
   constructor(app: FastifyInstance) {
   constructor(app: FastifyInstance) {
@@ -24,6 +26,7 @@ export class TaskService {
     this.senderRepository = app.dataSource.getRepository(Sender)
     this.senderRepository = app.dataSource.getRepository(Sender)
     this.senderService = new SenderService(app)
     this.senderService = new SenderService(app)
     this.tgClientService = TgClientService.getInstance()
     this.tgClientService = TgClientService.getInstance()
+    this.chatGroupService = new ChatGroupService(app)
   }
   }
 
 
   async create(data: { name: string; message: string; userId: number; buffer: Buffer }): Promise<Task> {
   async create(data: { name: string; message: string; userId: number; buffer: Buffer }): Promise<Task> {
@@ -487,7 +490,8 @@ export class TaskService {
     groupType: string,
     groupType: string,
     session?: string,
     session?: string,
     dcId?: number,
     dcId?: number,
-    authKey?: string
+    authKey?: string,
+    initMsg?: string
   ): Promise<any> {
   ): Promise<any> {
     let client: TelegramClient | null = null
     let client: TelegramClient | null = null
 
 
@@ -549,18 +553,24 @@ export class TaskService {
         accessHash: accessHash
         accessHash: accessHash
       })
       })
 
 
-      // ⭐ 必须发送一条消息,让群出现在会话列表中
-      try {
-        await client.sendMessage(inputChannel, {
-          message: groupDescription || `Welcome to ${groupName}!`
-        })
-        this.app.log.info('已向群组发送欢迎消息')
-      } catch (e) {
-        this.app.log.warn('发送欢迎消息失败: ' + (e instanceof Error ? e.message : '未知错误'))
+      const shouldSendInit = typeof initMsg === 'string' && initMsg.trim().length > 0
+      if (shouldSendInit) {
+        // ⭐ 发送自定义初始化消息,让群出现在会话列表中
+        try {
+          await client.sendMessage(inputChannel, {
+            message: initMsg.trim()
+          })
+          this.app.log.info('已向群组发送自定义欢迎消息')
+        } catch (e) {
+          this.app.log.warn('发送欢迎消息失败: ' + (e instanceof Error ? e.message : '未知错误'))
+        }
+      } else {
+        this.app.log.info('未提供 initMsg,跳过欢迎消息发送')
       }
       }
 
 
-      // 获取链接
-      let groupLink: string | null = null
+      // 获取公开链接 + 永久邀请链接;若无用户名则为群配置一个
+      let publicLink: string | null = null
+      let inviteLinkPermanent: string | null = null
       try {
       try {
         const fullChannel = await client.invoke(
         const fullChannel = await client.invoke(
           new Api.channels.GetFullChannel({
           new Api.channels.GetFullChannel({
@@ -569,35 +579,75 @@ export class TaskService {
         )
         )
 
 
         const channel = fullChannel.chats?.[0] as any
         const channel = fullChannel.chats?.[0] as any
+        let channelUsername: string | undefined = channel?.username
 
 
-        // 若存在 username(公开群)
-        if (channel?.username) {
-          groupLink = `https://t.me/${channel.username}`
-          this.app.log.info(`群组公开链接(永久): ${groupLink}`)
-        } else {
-          // 创建私有邀请链接(有效期 1 天)
-          const expireDate = Math.floor(Date.now() / 1000) + 24 * 60 * 60
+        // 若无用户名,固定用 “group_时间戳”(长度限制 5-32)
+        if (!channelUsername) {
+          const candidate = `group_${Date.now()}`
+          let desiredHandle = candidate.toLowerCase()
+          if (desiredHandle.length > 32) desiredHandle = desiredHandle.slice(0, 32)
+          if (desiredHandle.length < 5) desiredHandle = desiredHandle.padEnd(5, '0')
+
+          try {
+            await client.invoke(
+              new Api.channels.UpdateUsername({
+                channel: inputChannel,
+                username: desiredHandle
+              })
+            )
+            channelUsername = desiredHandle
+            this.app.log.info(`已为群设置用户名: ${channelUsername}`)
+          } catch (setError) {
+            const msg = setError instanceof Error ? setError.message : '未知错误'
+            this.app.log.warn(`为群设置用户名失败: ${msg}`)
+          }
+        }
 
 
+        if (channelUsername) {
+          publicLink = `https://t.me/${channelUsername}`
+          this.app.log.info(`群组公开链接(永久): ${publicLink}`)
+        }
+
+        // 永久邀请链接(不设置 expireDate)
+        try {
           const inviteLink = await client.invoke(
           const inviteLink = await client.invoke(
             new Api.messages.ExportChatInvite({
             new Api.messages.ExportChatInvite({
-              peer: inputChannel,
-              expireDate: expireDate
+              peer: inputChannel
             })
             })
           )
           )
 
 
           const invite = inviteLink as any
           const invite = inviteLink as any
           if (invite?.link) {
           if (invite?.link) {
-            groupLink = invite.link
-            this.app.log.info(`群组邀请链接(1 天有效): ${groupLink}`)
+            inviteLinkPermanent = invite.link
+            this.app.log.info(`群组邀请链接(永久): ${inviteLinkPermanent}`)
           } else {
           } else {
             this.app.log.warn('未能获取群组邀请链接')
             this.app.log.warn('未能获取群组邀请链接')
           }
           }
+        } catch (inviteError) {
+          const msg = inviteError instanceof Error ? inviteError.message : '未知错误'
+          this.app.log.warn(`获取群组邀请链接失败: ${msg}`)
         }
         }
       } catch (error) {
       } catch (error) {
         const errorMessage = error instanceof Error ? error.message : '未知错误'
         const errorMessage = error instanceof Error ? error.message : '未知错误'
         this.app.log.warn(`获取群组链接失败: ${errorMessage}`)
         this.app.log.warn(`获取群组链接失败: ${errorMessage}`)
       }
       }
 
 
+      try {
+        await this.chatGroupService.upsertGroup({
+          chatId: chatId.toString(),
+          accessHash: accessHash?.toString() || '',
+          name: createdChat.title || groupName,
+          groupType,
+          publicLink: publicLink || undefined,
+          inviteLink: inviteLinkPermanent || undefined,
+          senderId
+        })
+        this.app.log.info('群组信息已保存到数据库')
+      } catch (saveError) {
+        const msg = saveError instanceof Error ? saveError.message : '未知错误'
+        this.app.log.warn(`保存群组信息失败: ${msg}`)
+      }
+
       return {
       return {
         success: true,
         success: true,
         message: '群组创建成功',
         message: '群组创建成功',
@@ -605,7 +655,8 @@ export class TaskService {
           chatId: chatId.toString(),
           chatId: chatId.toString(),
           chatTitle: groupName,
           chatTitle: groupName,
           chatType: groupType,
           chatType: groupType,
-          groupLink: groupLink || null
+            groupLinkPublic: publicLink || null,
+            groupInviteLink: inviteLinkPermanent || null
         }
         }
       }
       }
     } catch (error) {
     } catch (error) {