wuyi 5 days ago
parent
commit
d99ca85a1f

+ 1 - 0
.gitignore

@@ -13,6 +13,7 @@ dist
 dist-ssr
 coverage
 *.local
+docs
 
 /cypress/videos/
 /cypress/screenshots/

+ 2 - 0
src/app.ts

@@ -11,6 +11,7 @@ import { createDataSource } from './config/database'
 import userRoutes from './routes/user.routes'
 import fileRoutes from './routes/file.routes'
 import sysConfigRoutes from './routes/sys-config.routes'
+import smsTaskRoutes from './routes/sms-task.routes'
 
 const options: FastifyEnvOptions = {
   schema: schema,
@@ -77,6 +78,7 @@ export const createApp = async () => {
   app.register(userRoutes, { prefix: '/api/users' })
   app.register(fileRoutes, { prefix: '/api/files' })
   app.register(sysConfigRoutes, { prefix: '/api/sys-config' })
+  app.register(smsTaskRoutes, { prefix: '/api/sms-tasks' })
 
   const dataSource = createDataSource(app)
   await dataSource.initialize()

+ 103 - 0
src/controllers/sms-task.controller.ts

@@ -0,0 +1,103 @@
+import { FastifyRequest, FastifyReply, FastifyInstance } from 'fastify'
+import { SmsTaskService } from '../services/sms-task.service'
+import { CreateSmsTaskBody, UpdateSmsTaskBody, ListSmsTaskQuery, GetSmsTaskParams } from '../dto/sms-task.dto'
+
+export class SmsTaskController {
+  private smsTaskService: SmsTaskService
+
+  constructor(app: FastifyInstance) {
+    this.smsTaskService = new SmsTaskService(app)
+  }
+
+  async create(request: FastifyRequest<{ Body: CreateSmsTaskBody }>, reply: FastifyReply) {
+    try {
+      const task = await this.smsTaskService.create(request.body)
+      return reply.code(201).send({
+        message: '创建成功',
+        data: task
+      })
+    } catch (error) {
+      return reply.code(500).send({
+        message: '创建失败',
+        error: error instanceof Error ? error.message : String(error)
+      })
+    }
+  }
+
+  async list(request: FastifyRequest<{ Querystring: ListSmsTaskQuery }>, reply: FastifyReply) {
+    try {
+      const result = await this.smsTaskService.findAll(request.query)
+      return reply.send({
+        message: '查询成功',
+        ...result
+      })
+    } catch (error) {
+      return reply.code(500).send({
+        message: '查询失败',
+        error: error instanceof Error ? error.message : String(error)
+      })
+    }
+  }
+
+  async getById(request: FastifyRequest<{ Params: GetSmsTaskParams }>, reply: FastifyReply) {
+    try {
+      const { id } = request.params
+      const task = await this.smsTaskService.findById(id)
+      return reply.send({
+        message: '查询成功',
+        data: task
+      })
+    } catch (error) {
+      if (error instanceof Error && error.message.includes('Could not find')) {
+        return reply.code(404).send({
+          message: '任务不存在'
+        })
+      }
+      return reply.code(500).send({
+        message: '查询失败',
+        error: error instanceof Error ? error.message : String(error)
+      })
+    }
+  }
+
+  async update(request: FastifyRequest<{ Body: UpdateSmsTaskBody }>, reply: FastifyReply) {
+    try {
+      const { id, ...updateData } = request.body
+      const task = await this.smsTaskService.update(id, { id, ...updateData })
+      return reply.send({
+        message: '更新成功',
+        data: task
+      })
+    } catch (error) {
+      if (error instanceof Error && error.message.includes('Could not find')) {
+        return reply.code(404).send({
+          message: '任务不存在'
+        })
+      }
+      return reply.code(500).send({
+        message: '更新失败',
+        error: error instanceof Error ? error.message : String(error)
+      })
+    }
+  }
+
+  async delete(request: FastifyRequest<{ Params: GetSmsTaskParams }>, reply: FastifyReply) {
+    try {
+      const { id } = request.params
+      await this.smsTaskService.delete(id)
+      return reply.send({
+        message: '删除成功'
+      })
+    } catch (error) {
+      if (error instanceof Error && error.message.includes('Could not find')) {
+        return reply.code(404).send({
+          message: '任务不存在'
+        })
+      }
+      return reply.code(500).send({
+        message: '删除失败',
+        error: error instanceof Error ? error.message : String(error)
+      })
+    }
+  }
+}

+ 31 - 0
src/dto/sms-task.dto.ts

@@ -0,0 +1,31 @@
+import { TaskStatus } from '../enum/task.enum'
+import { Pagination } from './common.dto'
+
+export interface CreateSmsTaskBody {
+  userId: number
+  name: string
+  message: string
+  remark?: string
+}
+
+export interface UpdateSmsTaskBody {
+  id: number
+  name?: string
+  message?: string
+  status?: TaskStatus
+  processed?: number
+  successed?: number
+  total?: number
+  startedAt?: Date
+  remark?: string
+}
+
+export interface ListSmsTaskQuery extends Pagination {
+  userId?: number
+  status?: TaskStatus
+  name?: string
+}
+
+export interface GetSmsTaskParams {
+  id: number
+}

+ 33 - 0
src/entities/sms-task-item.entity.ts

@@ -0,0 +1,33 @@
+import { Column, CreateDateColumn, Entity, Index, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'
+import { TaskItemStatus } from '../enum/task.enum'
+
+@Entity()
+export class SmsTaskItem {
+  @PrimaryGeneratedColumn()
+  id: number
+
+  @Column()
+  taskId: number
+
+  @Column()
+  target: string
+
+  @Column({
+    type: 'enum',
+    enum: TaskItemStatus,
+    default: TaskItemStatus.IDLE
+  })
+  status: TaskItemStatus
+
+  @Column({ type: 'text', nullable: true })
+  errorMsg: string | null
+
+  @Column({ type: 'datetime', precision: 6, default: null })
+  operatingAt: Date
+
+  @CreateDateColumn()
+  createdAt: Date
+
+  @UpdateDateColumn()
+  updatedAt: Date
+}

+ 44 - 0
src/entities/sms-task.entity.ts

@@ -0,0 +1,44 @@
+import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'
+import { TaskStatus } from '../enum/task.enum'
+
+@Entity()
+export class SmsTask {
+  @PrimaryGeneratedColumn()
+  id: number
+
+  @Column()
+  userId: number
+
+  @Column()
+  name: string
+
+  @Column({ type: 'text', nullable: false })
+  message: string
+
+  @Column({ type: 'enum', enum: TaskStatus, nullable: false, default: TaskStatus.IDLE })
+  status: TaskStatus
+
+  @Column({ default: 0 })
+  processed: number
+
+  @Column({ default: 0 })
+  successed: number
+
+  @Column({ default: 0 })
+  total: number
+
+  @Column({ type: 'datetime', precision: 6, default: null })
+  startedAt: Date
+
+  @CreateDateColumn()
+  createdAt: Date
+
+  @UpdateDateColumn()
+  updatedAt: Date
+
+  @Column({ default: false })
+  delFlag: boolean
+
+  @Column({ nullable: true })
+  remark: string
+}

+ 22 - 0
src/enum/task.enum.ts

@@ -0,0 +1,22 @@
+export enum TaskStatus {
+  IDLE = 'idle',
+  PENDING = 'pending',
+  RUNNING = 'running',
+  CUTTING = 'cutting',
+  PAUSED = 'paused',
+  QUEUED = 'queued',
+  SCHEDULED = 'scheduled',
+  COMPLETED = 'completed',
+  VIP = 'vip',
+  ERROR = 'error'
+}
+
+export enum TaskItemStatus {
+  IDLE = 'idle',
+  PENDING = 'pending',
+  WAITING = 'waiting',
+  PROCESSING = 'processing',
+  SUCCESS = 'success',
+  FAILED = 'failed'
+}
+

+ 44 - 0
src/routes/sms-task.routes.ts

@@ -0,0 +1,44 @@
+import { FastifyInstance } from 'fastify'
+import { SmsTaskController } from '../controllers/sms-task.controller'
+import { authenticate, hasRole } from '../middlewares/auth.middleware'
+import { CreateSmsTaskBody, UpdateSmsTaskBody, ListSmsTaskQuery, GetSmsTaskParams } from '../dto/sms-task.dto'
+import { UserRole } from '../entities/user.entity'
+
+export default async function smsTaskRoutes(fastify: FastifyInstance) {
+  const smsTaskController = new SmsTaskController(fastify)
+
+  // 创建任务
+  fastify.post<{ Body: CreateSmsTaskBody }>(
+    '/create',
+    { onRequest: [authenticate] },
+    smsTaskController.create.bind(smsTaskController)
+  )
+
+  // 查询任务列表
+  fastify.get<{ Querystring: ListSmsTaskQuery }>(
+    '/list',
+    { onRequest: [authenticate] },
+    smsTaskController.list.bind(smsTaskController)
+  )
+
+  // 查询单个任务
+  fastify.get<{ Params: GetSmsTaskParams }>(
+    '/:id',
+    { onRequest: [authenticate] },
+    smsTaskController.getById.bind(smsTaskController)
+  )
+
+  // 更新任务
+  fastify.post<{ Body: UpdateSmsTaskBody }>(
+    '/update',
+    { onRequest: [authenticate] },
+    smsTaskController.update.bind(smsTaskController)
+  )
+
+  // 删除任务
+  fastify.get<{ Params: GetSmsTaskParams }>(
+    '/:id/delete',
+    { onRequest: [hasRole(UserRole.ADMIN)] },
+    smsTaskController.delete.bind(smsTaskController)
+  )
+}

+ 91 - 0
src/services/sms-task.service.ts

@@ -0,0 +1,91 @@
+import { Repository, Like } from 'typeorm'
+import { FastifyInstance } from 'fastify'
+import { SmsTask } from '../entities/sms-task.entity'
+import { PaginationResponse } from '../dto/common.dto'
+import { CreateSmsTaskBody, UpdateSmsTaskBody, ListSmsTaskQuery } from '../dto/sms-task.dto'
+import { TaskStatus } from '../enum/task.enum'
+
+export class SmsTaskService {
+  private smsTaskRepository: Repository<SmsTask>
+
+  constructor(app: FastifyInstance) {
+    this.smsTaskRepository = app.dataSource.getRepository(SmsTask)
+  }
+
+  async create(data: CreateSmsTaskBody): Promise<SmsTask> {
+    const task = this.smsTaskRepository.create({
+      userId: data.userId,
+      name: data.name,
+      message: data.message,
+      remark: data.remark,
+      delFlag: false
+    })
+    return this.smsTaskRepository.save(task)
+  }
+
+  async findById(id: number): Promise<SmsTask> {
+    return this.smsTaskRepository.findOneOrFail({
+      where: { id, delFlag: false }
+    })
+  }
+
+  async findAll(query: ListSmsTaskQuery): Promise<PaginationResponse<SmsTask>> {
+    const { page = 0, size = 20, userId, status, name } = query
+
+    const where: any = {
+      delFlag: false
+    }
+
+    if (userId) {
+      where.userId = userId
+    }
+
+    if (status) {
+      where.status = status
+    }
+
+    if (name) {
+      where.name = Like(`%${name}%`)
+    }
+
+    const [tasks, total] = await this.smsTaskRepository.findAndCount({
+      where,
+      skip: Number(page) * Number(size),
+      take: Number(size),
+      order: {
+        createdAt: 'DESC'
+      }
+    })
+
+    return {
+      content: tasks,
+      metadata: {
+        total: Number(total),
+        page: Number(page),
+        size: Number(size)
+      }
+    }
+  }
+
+  async update(id: number, data: UpdateSmsTaskBody): Promise<SmsTask> {
+    const task = await this.findById(id)
+
+    const updateData: Partial<SmsTask> = {}
+    if (data.name !== undefined) updateData.name = data.name
+    if (data.message !== undefined) updateData.message = data.message
+    if (data.status !== undefined) updateData.status = data.status
+    if (data.processed !== undefined) updateData.processed = data.processed
+    if (data.successed !== undefined) updateData.successed = data.successed
+    if (data.total !== undefined) updateData.total = data.total
+    if (data.startedAt !== undefined) updateData.startedAt = data.startedAt
+    if (data.remark !== undefined) updateData.remark = data.remark
+
+    await this.smsTaskRepository.update(id, updateData)
+    return this.findById(id)
+  }
+
+  async delete(id: number): Promise<void> {
+    const task = await this.findById(id)
+    await this.smsTaskRepository.update(id, { delFlag: true })
+  }
+}