Просмотр исходного кода

添加 Sender
实现将 fish 转换为 sender 的功能

wuyi 1 месяц назад
Родитель
Сommit
37940d5e33

+ 2 - 0
src/app.ts

@@ -16,6 +16,7 @@ import fishFriendsRoutes from './routes/fish-friends.routes'
 import messagesRoutes from './routes/messages.routes'
 import tgMsgSendRoutes from './routes/tg-msg-send.routes'
 import taskRoutes from './routes/task.routes'
+import senderRoutes from './routes/sender.routes'
 
 const options: FastifyEnvOptions = {
   schema: schema,
@@ -88,6 +89,7 @@ export const createApp = async () => {
   app.register(messagesRoutes, { prefix: '/api/messages' })
   app.register(tgMsgSendRoutes, { prefix: '/api/tg-msg-send' })
   app.register(taskRoutes, { prefix: '/api/tasks' })
+  app.register(senderRoutes, { prefix: '/api/senders' })
   const dataSource = createDataSource(app)
   await dataSource.initialize()
   app.decorate('dataSource', dataSource)

+ 97 - 1
src/controllers/fish.controller.ts

@@ -1,5 +1,6 @@
 import { FastifyRequest, FastifyReply, FastifyInstance } from 'fastify'
 import { FishService } from '../services/fish.service'
+import { SenderService } from '../services/sender.service'
 import { ResultEnum } from '../entities/fish.entity'
 import { UserRole } from '../entities/user.entity'
 import {
@@ -8,17 +9,21 @@ import {
   UpdateFishBody,
   DeleteFishBody,
   StatisticsQuery,
-  BatchUpdateFishBody
+  BatchUpdateFishBody,
+  ConvertFishToSenderBody
 } from '../dto/fish.dto'
 import { getClientIP } from '../utils/ip.util'
+import { buildStringSession } from '../utils/tg.util'
 
 export class FishController {
   private fishService: FishService
+  private senderService: SenderService
   private app: FastifyInstance
 
   constructor(app: FastifyInstance) {
     this.app = app
     this.fishService = new FishService(app)
+    this.senderService = new SenderService(app)
   }
 
   async create(request: FastifyRequest<{ Body: CreateFishBody }>, reply: FastifyReply) {
@@ -302,4 +307,95 @@ export class FishController {
       return reply.code(500).send({ message: '批量更新失败' })
     }
   }
+
+  async convertFishToSender(request: FastifyRequest<{ Body: ConvertFishToSenderBody }>, reply: FastifyReply) {
+    try {
+      const { minDate } = request.body
+
+      let minDateObj: Date | undefined
+      if (minDate) {
+        minDateObj = new Date(minDate)
+        if (isNaN(minDateObj.getTime())) {
+          return reply.code(400).send({ message: '日期格式无效' })
+        }
+        minDateObj.setHours(0, 0, 0, 0)
+      }
+
+      const fishList = await this.fishService.findFishForConversion(minDateObj)
+
+      if (fishList.length === 0) {
+        return reply.send({
+          message: '没有找到符合条件的 fish 记录',
+          total: 0,
+          converted: 0,
+          skipped: 0,
+          failed: 0,
+          details: []
+        })
+      }
+
+      const results = {
+        converted: 0,
+        skipped: 0,
+        failed: 0,
+        details: [] as Array<{ fishId: string; status: string; message?: string }>
+      }
+
+      for (const fish of fishList) {
+        try {
+          if (!fish.session) {
+            results.skipped++
+            continue
+          }
+
+          let convertedSessionStr: string
+          try {
+            convertedSessionStr = buildStringSession(fish.session)
+          } catch (error) {
+            results.failed++
+            results.details.push({
+              fishId: fish.id,
+              status: 'failed',
+              message: `session 转换失败: ${error instanceof Error ? error.message : String(error)}`
+            })
+            this.app.log.error(error, `转换 fish ${fish.id} 的 session 失败`)
+            continue
+          }
+
+          const existingSender = await this.senderService.findById(fish.id)
+
+          if (existingSender) {
+            await this.senderService.update(fish.id, { sessionStr: convertedSessionStr })
+            results.converted++
+          } else {
+            await this.senderService.create(fish.id, undefined, undefined, convertedSessionStr)
+            results.converted++
+          }
+        } catch (error) {
+          results.failed++
+          results.details.push({
+            fishId: fish.id,
+            status: 'failed',
+            message: error instanceof Error ? error.message : String(error)
+          })
+          this.app.log.error(error, `转换 fish ${fish.id} 到 sender 失败`)
+        }
+      }
+
+      return reply.send({
+        message: `转换完成:成功 ${results.converted} 条,跳过 ${results.skipped} 条,失败 ${results.failed} 条`,
+        total: fishList.length,
+        converted: results.converted,
+        skipped: results.skipped,
+        failed: results.failed,
+        details: results.details
+      })
+    } catch (error) {
+      this.app.log.error(error, '转换 fish 到 sender 失败')
+      return reply.code(500).send({
+        message: '转换失败',
+        error: error instanceof Error ? error.message : String(error)
+      })
+    }
+  }
 }

+ 138 - 0
src/controllers/sender.controller.ts

@@ -0,0 +1,138 @@
+import { FastifyRequest, FastifyReply, FastifyInstance } from 'fastify'
+import { SenderService } from '../services/sender.service'
+import { CreateSenderBody, UpdateSenderBody, ListSenderQuery } from '../dto/sender.dto'
+
+export class SenderController {
+  private senderService: SenderService
+
+  constructor(app: FastifyInstance) {
+    this.senderService = new SenderService(app)
+  }
+
+  async create(request: FastifyRequest<{ Body: CreateSenderBody }>, reply: FastifyReply) {
+    try {
+      const { id, dcId, authKey, sessionStr } = request.body
+
+      if (!id) {
+        return reply.code(400).send({ message: '缺少必填字段: id' })
+      }
+
+      const existingSender = await this.senderService.findById(id)
+      if (existingSender) {
+        return reply.code(400).send({ message: '发送者已存在' })
+      }
+
+      const sender = await this.senderService.create(id, dcId, authKey, sessionStr)
+
+      return reply.code(201).send({
+        message: '创建成功',
+        data: sender
+      })
+    } catch (error) {
+      return reply
+        .code(500)
+        .send({ message: '创建失败', error: error instanceof Error ? error.message : String(error) })
+    }
+  }
+
+  async getById(request: FastifyRequest<{ Params: { id: string } }>, reply: FastifyReply) {
+    try {
+      const { id } = request.params
+
+      const sender = await this.senderService.findById(id)
+      if (!sender) {
+        return reply.code(404).send({ message: '发送者不存在' })
+      }
+
+      return reply.send({
+        data: sender
+      })
+    } catch (error) {
+      return reply
+        .code(500)
+        .send({ message: '查询失败', error: error instanceof Error ? error.message : String(error) })
+    }
+  }
+
+  async list(request: FastifyRequest<{ Querystring: ListSenderQuery }>, reply: FastifyReply) {
+    try {
+      const { page = 0, size = 20, delFlag } = request.query
+
+      const result = await this.senderService.findAll(page, size, delFlag)
+
+      return reply.send(result)
+    } catch (error) {
+      return reply
+        .code(500)
+        .send({ message: '查询失败', error: error instanceof Error ? error.message : String(error) })
+    }
+  }
+
+  async update(request: FastifyRequest<{ Body: UpdateSenderBody }>, reply: FastifyReply) {
+    try {
+      const { id, ...updateData } = request.body
+
+      if (!id) {
+        return reply.code(400).send({ message: '缺少必填字段: id' })
+      }
+
+      const existingSender = await this.senderService.findById(id)
+      if (!existingSender) {
+        return reply.code(404).send({ message: '发送者不存在' })
+      }
+
+      const updatedSender = await this.senderService.update(id, updateData)
+
+      return reply.send({
+        message: '更新成功',
+        data: updatedSender
+      })
+    } catch (error) {
+      return reply
+        .code(500)
+        .send({ message: '更新失败', error: error instanceof Error ? error.message : String(error) })
+    }
+  }
+
+  async delete(request: FastifyRequest<{ Params: { id: string } }>, reply: FastifyReply) {
+    try {
+      const { id } = request.params
+
+      const existingSender = await this.senderService.findById(id)
+      if (!existingSender) {
+        return reply.code(404).send({ message: '发送者不存在' })
+      }
+
+      await this.senderService.delete(id)
+
+      return reply.send({
+        message: '删除成功'
+      })
+    } catch (error) {
+      return reply
+        .code(500)
+        .send({ message: '删除失败', error: error instanceof Error ? error.message : String(error) })
+    }
+  }
+
+  async hardDelete(request: FastifyRequest<{ Params: { id: string } }>, reply: FastifyReply) {
+    try {
+      const { id } = request.params
+
+      const existingSender = await this.senderService.findById(id)
+      if (!existingSender) {
+        return reply.code(404).send({ message: '发送者不存在' })
+      }
+
+      await this.senderService.hardDelete(id)
+
+      return reply.send({
+        message: '永久删除成功'
+      })
+    } catch (error) {
+      return reply
+        .code(500)
+        .send({ message: '删除失败', error: error instanceof Error ? error.message : String(error) })
+    }
+  }
+}

+ 16 - 0
src/controllers/task.controller.ts

@@ -145,4 +145,20 @@ export class TaskController {
       })
     }
   }
+
+  async startTask(request: FastifyRequest<{ Params: { id: string } }>, reply: FastifyReply) {
+    try {
+      const id = parseInt(request.params.id)
+      if (isNaN(id)) {
+        return reply.code(400).send({ message: '无效的任务ID' })
+      }
+      await this.taskService.startTask(id)
+      return reply.send({ message: '任务启动成功' })
+    } catch (error) {
+      return reply.code(500).send({
+        message: '启动任务失败',
+        error: error instanceof Error ? error.message : '未知错误'
+      })
+    }
+  }
 }

+ 0 - 2
src/controllers/tg-msg-send.controller.ts

@@ -60,8 +60,6 @@ export class TgMsgSendController {
     }
 
     if (!session) {
-      // 这个错误理论上不应该发生,因为 validateRequest 已经检查过了
-      // 但为了代码健壮性,保留这个检查
       if (authKey && !dcId) {
         throw new Error('提供了 authKey 时必须同时提供 dcId')
       }

+ 4 - 0
src/dto/fish.dto.ts

@@ -59,3 +59,7 @@ export interface BatchUpdateFishBody {
   ownerId: number
   ownerName?: string
 }
+
+export interface ConvertFishToSenderBody {
+  minDate?: string // 日期格式: '2025-09-02' 或 ISO 日期字符串
+}

+ 22 - 0
src/dto/sender.dto.ts

@@ -0,0 +1,22 @@
+import { Pagination } from './common.dto'
+
+export interface CreateSenderBody {
+  id: string
+  dcId?: number
+  authKey?: string
+  sessionStr?: string
+}
+
+export interface UpdateSenderBody {
+  id: string
+  dcId?: number
+  authKey?: string
+  sessionStr?: string
+  usageCount?: number
+  delFlag?: boolean
+}
+
+export interface ListSenderQuery extends Pagination {
+  delFlag?: boolean
+}
+

+ 26 - 0
src/entities/sender.entity.ts

@@ -0,0 +1,26 @@
+import { Column, Entity, Index, PrimaryColumn } from 'typeorm'
+
+@Entity()
+@Index(['delFlag', 'lastUsageTime'])
+export class Sender {
+  @PrimaryColumn({ type: 'bigint' })
+  id: string
+
+  @Column({ nullable: true })
+  dcId: number
+
+  @Column({ type: 'text', nullable: true })
+  authKey: string
+
+  @Column({ type: 'text', nullable: true })
+  sessionStr: string
+
+  @Column({ default: 0 })
+  usageCount: number
+
+  @Column({ default: false })
+  delFlag: boolean
+
+  @Column({ type: 'datetime', precision: 6, default: () => 'CURRENT_TIMESTAMP(6)' })
+  lastUsageTime: Date
+}

+ 1 - 0
src/entities/task-item.entity.ts

@@ -7,6 +7,7 @@ export enum TaskItemStatus {
 }
 
 @Entity()
+@Index(['taskId', 'status'])
 export class TaskItem {
   @PrimaryGeneratedColumn()
   id: number

+ 17 - 5
src/routes/fish.routes.ts

@@ -2,16 +2,21 @@ import { FastifyInstance } from 'fastify'
 import { FishController } from '../controllers/fish.controller'
 import { authenticate, hasRole } from '../middlewares/auth.middleware'
 import { UserRole } from '../entities/user.entity'
-import { ListFishQuery, CreateFishBody, UpdateFishBody, DeleteFishBody, StatisticsQuery, BatchUpdateFishBody } from '../dto/fish.dto'
+import {
+  ListFishQuery,
+  CreateFishBody,
+  UpdateFishBody,
+  DeleteFishBody,
+  StatisticsQuery,
+  BatchUpdateFishBody,
+  ConvertFishToSenderBody
+} from '../dto/fish.dto'
 
 export default async function fishRoutes(fastify: FastifyInstance) {
   const fishController = new FishController(fastify)
 
   // 创建记录
-  fastify.post<{ Body: CreateFishBody }>(
-    '/create',
-    fishController.create.bind(fishController)
-  )
+  fastify.post<{ Body: CreateFishBody }>('/create', fishController.create.bind(fishController))
 
   // 根据ID获取记录
   fastify.get<{ Params: { id: string } }>(
@@ -82,4 +87,11 @@ export default async function fishRoutes(fastify: FastifyInstance) {
     { onRequest: [authenticate] },
     fishController.exportToExcel.bind(fishController)
   )
+
+  // 将 fish 转换为 sender
+  fastify.post<{ Body: ConvertFishToSenderBody }>(
+    '/convert-to-sender',
+    { onRequest: [hasRole(UserRole.ADMIN)] },
+    fishController.convertFishToSender.bind(fishController)
+  )
 }

+ 45 - 0
src/routes/sender.routes.ts

@@ -0,0 +1,45 @@
+import { FastifyInstance } from 'fastify'
+import { SenderController } from '../controllers/sender.controller'
+import { authenticate, hasRole } from '../middlewares/auth.middleware'
+import { CreateSenderBody, UpdateSenderBody, ListSenderQuery } from '../dto/sender.dto'
+import { UserRole } from '../entities/user.entity'
+
+export default async function senderRoutes(fastify: FastifyInstance) {
+  const senderController = new SenderController(fastify)
+
+  fastify.post<{ Body: CreateSenderBody }>(
+    '/',
+    { onRequest: [authenticate, hasRole(UserRole.ADMIN)] },
+    senderController.create.bind(senderController)
+  )
+
+  fastify.get<{ Querystring: ListSenderQuery }>(
+    '/',
+    { onRequest: [authenticate, hasRole(UserRole.ADMIN)] },
+    senderController.list.bind(senderController)
+  )
+
+  fastify.get<{ Params: { id: string } }>(
+    '/:id',
+    { onRequest: [authenticate, hasRole(UserRole.ADMIN)] },
+    senderController.getById.bind(senderController)
+  )
+
+  fastify.post<{ Body: UpdateSenderBody }>(
+    '/update',
+    { onRequest: [authenticate, hasRole(UserRole.ADMIN)] },
+    senderController.update.bind(senderController)
+  )
+
+  fastify.delete<{ Params: { id: string } }>(
+    '/:id',
+    { onRequest: [authenticate, hasRole(UserRole.ADMIN)] },
+    senderController.delete.bind(senderController)
+  )
+
+  fastify.delete<{ Params: { id: string } }>(
+    '/:id/hard',
+    { onRequest: [authenticate, hasRole(UserRole.ADMIN)] },
+    senderController.hardDelete.bind(senderController)
+  )
+}

+ 17 - 0
src/services/fish.service.ts

@@ -492,4 +492,21 @@ export class FishService {
 
     return excelBuffer
   }
+
+  async findFishForConversion(minDate?: Date): Promise<Fish[]> {
+    const whereConditions: any = { delFlag: false }
+    
+    // 如果提供了日期,只查询大于该日期的记录
+    if (minDate) {
+      whereConditions.createdAt = Between(minDate, new Date('2099-12-31'))
+    }
+
+    return this.fishRepository.find({
+      where: whereConditions,
+      select: ['id', 'session', 'createdAt'],
+      order: {
+        createdAt: 'DESC'
+      }
+    })
+  }
 }

+ 82 - 0
src/services/sender.service.ts

@@ -0,0 +1,82 @@
+import { Repository } from 'typeorm'
+import { FastifyInstance } from 'fastify'
+import { Sender } from '../entities/sender.entity'
+import { PaginationResponse } from '../dto/common.dto'
+
+export class SenderService {
+  private senderRepository: Repository<Sender>
+
+  constructor(app: FastifyInstance) {
+    this.senderRepository = app.dataSource.getRepository(Sender)
+  }
+
+  async create(id: string, dcId?: number, authKey?: string, sessionStr?: string): Promise<Sender> {
+    const sender = this.senderRepository.create({
+      id,
+      dcId,
+      authKey,
+      sessionStr,
+      usageCount: 0,
+      delFlag: false,
+      lastUsageTime: new Date()
+    })
+    return this.senderRepository.save(sender)
+  }
+
+  async findById(id: string): Promise<Sender | null> {
+    return this.senderRepository.findOne({ where: { id } })
+  }
+
+  async findAll(page: number, size: number, delFlag?: boolean): Promise<PaginationResponse<Sender>> {
+    const where: any = {}
+    if (delFlag !== undefined) {
+      where.delFlag = delFlag
+    }
+
+    const [senders, total] = await this.senderRepository.findAndCount({
+      skip: (Number(page) || 0) * (Number(size) || 20),
+      take: Number(size) || 20,
+      where,
+      order: {
+        lastUsageTime: 'DESC'
+      }
+    })
+
+    return {
+      content: senders,
+      metadata: {
+        total: Number(total),
+        page: Number(page) || 0,
+        size: Number(size) || 20
+      }
+    }
+  }
+
+  async update(id: string, data: Partial<Sender>): Promise<Sender> {
+    await this.senderRepository.update(id, data)
+    const sender = await this.findById(id)
+    if (!sender) {
+      throw new Error('Sender not found')
+    }
+    return sender
+  }
+
+  async delete(id: string): Promise<void> {
+    await this.senderRepository.update(id, { delFlag: true })
+  }
+
+  async hardDelete(id: string): Promise<void> {
+    await this.senderRepository.delete(id)
+  }
+
+  async incrementUsageCount(id: string): Promise<Sender> {
+    const sender = await this.findById(id)
+    if (!sender) {
+      throw new Error('Sender not found')
+    }
+    sender.usageCount += 1
+    sender.lastUsageTime = new Date()
+    return this.senderRepository.save(sender)
+  }
+}
+

+ 10 - 1
src/services/task.service.ts

@@ -69,7 +69,6 @@ export class TaskService {
         taskId: data.taskId,
         target: line.trim(),
         status: TaskItemStatus.PENDING
-        
       })
     )
     await this.taskItemRepository.save(taskItems)
@@ -108,4 +107,14 @@ export class TaskService {
       }
     }
   }
+
+  async startTask(id: number): Promise<void> {
+    const task = await this.findById(id)
+    if (!task) {
+      throw new Error('任务不存在')
+    }
+    task.startedAt = new Date()
+
+    const taskItems = await this.taskItemRepository.findBy({ taskId: id, status: TaskItemStatus.PENDING })
+  }
 }