yourname 5 月之前
父節點
當前提交
01dac45801

+ 87 - 0
src/server/api/leads/[id]/delete.ts

@@ -0,0 +1,87 @@
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { z } from 'zod';
+import { AppDataSource } from '@/server/data-source';
+import { LeadService } from '@/server/modules/leads/lead.service';
+import { ErrorSchema } from '@/server/utils/errorHandler';
+import { authMiddleware } from '@/server/middleware/auth';
+import { AuthContext } from '@/server/types/context';
+import { logger } from '@/server/utils/logger';
+
+// 初始化服务
+const leadService = new LeadService(AppDataSource);
+
+// 路径参数Schema
+const DeleteLeadParams = z.object({
+  id: z.coerce.number().int().positive().openapi({
+    param: { name: 'id', in: 'path' },
+    example: 1,
+    description: '销售线索ID'
+  })
+});
+
+// 响应Schema
+const DeleteLeadResponse = z.object({
+  code: z.number().openapi({ example: 200 }),
+  message: z.string().openapi({ example: '删除成功' })
+});
+
+// 路由定义
+const routeDef = createRoute({
+  method: 'delete',
+  path: '/{id}',
+  middleware: [authMiddleware],
+  request: {
+    params: DeleteLeadParams
+  },
+  responses: {
+    200: {
+      description: '删除销售线索成功',
+      content: {
+        'application/json': { schema: DeleteLeadResponse }
+      }
+    },
+    404: {
+      description: '销售线索不存在',
+      content: {
+        'application/json': { schema: ErrorSchema }
+      }
+    },
+    500: {
+      description: '服务器内部错误',
+      content: {
+        'application/json': { schema: ErrorSchema }
+      }
+    }
+  }
+});
+
+// 创建路由实例
+const app = new OpenAPIHono<AuthContext>().openapi(routeDef, async (c) => {
+  const { id } = c.req.valid('param');
+  
+  try {
+    logger.api(`Deleting lead with id: ${id}`);
+    
+    const deleted = await leadService.delete(id);
+    
+    if (!deleted) {
+      return c.json({
+        code: 404,
+        message: '销售线索不存在'
+      }, 404);
+    }
+    
+    return c.json({
+      code: 200,
+      message: '删除成功'
+    }, 200);
+  } catch (error) {
+    logger.error(`Error deleting lead with id ${id}:`, error);
+    return c.json({
+      code: 500,
+      message: error instanceof Error ? error.message : '删除销售线索失败'
+    }, 500);
+  }
+});
+
+export default app;

+ 90 - 0
src/server/api/leads/[id]/get.ts

@@ -0,0 +1,90 @@
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { z } from 'zod';
+import { AppDataSource } from '@/server/data-source';
+import { LeadService } from '@/server/modules/leads/lead.service';
+import { LeadSchema } from '@/server/modules/leads/lead.entity';
+import { ErrorSchema } from '@/server/utils/errorHandler';
+import { authMiddleware } from '@/server/middleware/auth';
+import { AuthContext } from '@/server/types/context';
+import { logger } from '@/server/utils/logger';
+
+// 初始化服务
+const leadService = new LeadService(AppDataSource);
+
+// 路径参数Schema
+const GetLeadParams = z.object({
+  id: z.coerce.number().int().positive().openapi({
+    param: { name: 'id', in: 'path' },
+    example: 1,
+    description: '销售线索ID'
+  })
+});
+
+// 响应Schema
+const GetLeadResponse = z.object({
+  code: z.number().openapi({ example: 200 }),
+  message: z.string().openapi({ example: 'success' }),
+  data: LeadSchema
+});
+
+// 路由定义
+const routeDef = createRoute({
+  method: 'get',
+  path: '/{id}',
+  middleware: [authMiddleware],
+  request: {
+    params: GetLeadParams
+  },
+  responses: {
+    200: {
+      description: '获取销售线索详情成功',
+      content: {
+        'application/json': { schema: GetLeadResponse }
+      }
+    },
+    404: {
+      description: '销售线索不存在',
+      content: {
+        'application/json': { schema: ErrorSchema }
+      }
+    },
+    500: {
+      description: '服务器内部错误',
+      content: {
+        'application/json': { schema: ErrorSchema }
+      }
+    }
+  }
+});
+
+// 创建路由实例
+const app = new OpenAPIHono<AuthContext>().openapi(routeDef, async (c) => {
+  const { id } = c.req.valid('param');
+  
+  try {
+    logger.api(`Fetching lead with id: ${id}`);
+    
+    const lead = await leadService.findById(id);
+    
+    if (!lead) {
+      return c.json({
+        code: 404,
+        message: '销售线索不存在'
+      }, 404);
+    }
+    
+    return c.json({
+      code: 200,
+      message: 'success',
+      data: lead
+    }, 200);
+  } catch (error) {
+    logger.error(`Error fetching lead with id ${id}:`, error);
+    return c.json({
+      code: 500,
+      message: error instanceof Error ? error.message : '获取销售线索详情失败'
+    }, 500);
+  }
+});
+
+export default app;

+ 139 - 0
src/server/api/leads/[id]/put.ts

@@ -0,0 +1,139 @@
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { z } from 'zod';
+import { AppDataSource } from '@/server/data-source';
+import { LeadService } from '@/server/modules/leads/lead.service';
+import { LeadSchema } from '@/server/modules/leads/lead.entity';
+import { ErrorSchema } from '@/server/utils/errorHandler';
+import { authMiddleware } from '@/server/middleware/auth';
+import { AuthContext } from '@/server/types/context';
+import { logger } from '@/server/utils/logger';
+
+// 初始化服务
+const leadService = new LeadService(AppDataSource);
+
+// 路径参数Schema
+const UpdateLeadParams = z.object({
+  id: z.coerce.number().int().positive().openapi({
+    param: { name: 'id', in: 'path' },
+    example: 1,
+    description: '销售线索ID'
+  })
+});
+
+// 更新销售线索请求Schema
+const UpdateLeadSchema = z.object({
+  name: z.string().min(2).max(100).optional().openapi({
+    example: '张三',
+    description: '线索名称'
+  }),
+  email: z.string().email().optional().openapi({
+    example: 'example@test.com',
+    description: '电子邮箱'
+  }),
+  phone: z.string().min(8).max(20).optional().openapi({
+    example: '13800138000',
+    description: '联系电话'
+  }),
+  company: z.string().optional().openapi({
+    example: '测试公司',
+    description: '公司名称'
+  }),
+  position: z.string().optional().openapi({
+    example: '经理',
+    description: '职位'
+  }),
+  status: z.coerce.number().int().openapi({
+    example: 1,
+    description: '线索状态: 0-新线索, 1-已联系, 2-已转化, 3-已放弃'
+  }),
+  source: z.string().optional().openapi({
+    example: '网站',
+    description: '线索来源'
+  }),
+  notes: z.string().optional().openapi({
+    example: '客户对产品感兴趣',
+    description: '备注信息'
+  })
+});
+
+// 响应Schema
+const UpdateLeadResponse = z.object({
+  code: z.number().openapi({ example: 200 }),
+  message: z.string().openapi({ example: '更新成功' }),
+  data: LeadSchema
+});
+
+// 路由定义
+const routeDef = createRoute({
+  method: 'put',
+  path: '/{id}',
+  middleware: [authMiddleware],
+  request: {
+    params: UpdateLeadParams,
+    body: {
+      content: {
+        'application/json': { schema: UpdateLeadSchema }
+      }
+    }
+  },
+  responses: {
+    200: {
+      description: '更新销售线索成功',
+      content: {
+        'application/json': { schema: UpdateLeadResponse }
+      }
+    },
+    404: {
+      description: '销售线索不存在',
+      content: {
+        'application/json': { schema: ErrorSchema }
+      }
+    },
+    400: {
+      description: '请求参数错误',
+      content: {
+        'application/json': { schema: ErrorSchema }
+      }
+    },
+    500: {
+      description: '服务器内部错误',
+      content: {
+        'application/json': { schema: ErrorSchema }
+      }
+    }
+  }
+});
+
+// 创建路由实例
+const app = new OpenAPIHono<AuthContext>().openapi(routeDef, async (c) => {
+  const { id } = c.req.valid('param');
+  
+  try {
+    const leadData = c.req.valid('json');
+    
+    logger.api(`Updating lead with id: ${id}`, leadData);
+    
+    const updatedLead = await leadService.update(id, leadData);
+    
+    if (!updatedLead) {
+      return c.json({
+        code: 404,
+        message: '销售线索不存在'
+      }, 404);
+    }
+    
+    return c.json({
+      code: 200,
+      message: '更新成功',
+      data: updatedLead
+    }, 200);
+  } catch (error) {
+    logger.error(`Error updating lead with id ${id}:`, error);
+    return c.json({
+      code: 500,
+      message: error instanceof Error ? error.message : '更新销售线索失败'
+    }, 500);
+  }
+});
+
+export default app;

+ 122 - 0
src/server/api/leads/get.ts

@@ -0,0 +1,122 @@
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { z } from 'zod';
+import { AppDataSource } from '@/server/data-source';
+import { LeadService } from '@/server/modules/leads/lead.service';
+import { LeadSchema } from '@/server/modules/leads/lead.entity';
+import { ErrorSchema } from '@/server/utils/errorHandler';
+import { authMiddleware } from '@/server/middleware/auth';
+import { AuthContext } from '@/server/types/context';
+import { logger } from '@/server/utils/logger';
+
+// 初始化服务
+const leadService = new LeadService(AppDataSource);
+
+// 查询参数Schema
+const GetLeadsQuery = z.object({
+  page: z.coerce.number().int().positive().default(1).openapi({
+    example: 1,
+    description: '页码'
+  }),
+  pageSize: z.coerce.number().int().positive().default(10).openapi({
+    example: 10,
+    description: '每页数量'
+  }),
+  name: z.string().optional().openapi({
+    example: '张三',
+    description: '线索名称筛选'
+  }),
+  email: z.string().optional().openapi({
+    example: 'example@test.com',
+    description: '邮箱筛选'
+  }),
+  phone: z.string().optional().openapi({
+    example: '13800138000',
+    description: '电话筛选'
+  }),
+  status: z.string().optional().openapi({
+    example: 'new',
+    description: '状态筛选'
+  })
+});
+
+// 响应Schema
+const LeadListResponse = z.object({
+  code: z.number().openapi({ example: 200 }),
+  message: z.string().openapi({ example: 'success' }),
+  data: z.object({
+    data: z.array(LeadSchema),
+    pagination: z.object({
+      total: z.number().openapi({ example: 100, description: '总记录数' }),
+      current: z.number().openapi({ example: 1, description: '当前页码' }),
+      pageSize: z.number().openapi({ example: 10, description: '每页数量' })
+    })
+  })
+});
+
+// 路由定义
+const routeDef = createRoute({
+  method: 'get',
+  path: '/',
+  middleware: [authMiddleware],
+  request: {
+    query: GetLeadsQuery
+  },
+  responses: {
+    200: {
+      description: '获取销售线索列表成功',
+      content: {
+        'application/json': { schema: LeadListResponse }
+      }
+    },
+    400: {
+      description: '请求参数错误',
+      content: {
+        'application/json': { schema: ErrorSchema }
+      }
+    },
+    500: {
+      description: '服务器内部错误',
+      content: {
+        'application/json': { schema: ErrorSchema }
+      }
+    }
+  }
+});
+
+// 创建路由实例
+const app = new OpenAPIHono<AuthContext>().openapi(routeDef, async (c) => {
+  try {
+    const { page, pageSize, status, ...otherFilters } = c.req.valid('query');
+    
+    // 转换status为数字类型
+    const filters = {
+      ...otherFilters,
+      ...(status !== undefined && { status: parseInt(status, 10) })
+    };
+    
+    logger.api(`Fetching leads - page: ${page}, pageSize: ${pageSize}, filters: ${JSON.stringify(filters)}`);
+    
+    const result = await leadService.findAll(page, pageSize, filters);
+    
+    return c.json({
+      code: 200,
+      message: 'success',
+      data: {
+        data: result.data,
+        pagination: {
+          total: result.total,
+          current: result.current,
+          pageSize: result.pageSize
+        }
+      }
+    }, 200);
+  } catch (error) {
+    logger.error('Error fetching leads:', error);
+    return c.json({
+      code: 500,
+      message: error instanceof Error ? error.message : '获取销售线索列表失败'
+    }, 500);
+  }
+});
+
+export default app;

+ 15 - 0
src/server/api/leads/index.ts

@@ -0,0 +1,15 @@
+import { OpenAPIHono } from '@hono/zod-openapi';
+import listRoute from './get';
+import createRoute from './post';
+import getByIdRoute from './[id]/get';
+import updateRoute from './[id]/put';
+import deleteRoute from './[id]/delete';
+
+const app = new OpenAPIHono()
+  .route('/', listRoute)
+  .route('/', createRoute)
+  .route('/', getByIdRoute)
+  .route('/', updateRoute)
+  .route('/', deleteRoute);
+
+export default app;

+ 114 - 0
src/server/api/leads/post.ts

@@ -0,0 +1,114 @@
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { z } from 'zod';
+import { AppDataSource } from '@/server/data-source';
+import { LeadService } from '@/server/modules/leads/lead.service';
+import { LeadSchema } from '@/server/modules/leads/lead.entity';
+import { ErrorSchema } from '@/server/utils/errorHandler';
+import { authMiddleware } from '@/server/middleware/auth';
+import { AuthContext } from '@/server/types/context';
+import { logger } from '@/server/utils/logger';
+
+// 初始化服务
+const leadService = new LeadService(AppDataSource);
+
+// 创建销售线索请求Schema
+const CreateLeadSchema = z.object({
+  name: z.string().min(2).max(100).openapi({
+    example: '张三',
+    description: '线索名称'
+  }),
+  email: z.string().email().openapi({
+    example: 'example@test.com',
+    description: '电子邮箱'
+  }),
+  phone: z.string().min(8).max(20).openapi({
+    example: '13800138000',
+    description: '联系电话'
+  }),
+  company: z.string().optional().openapi({
+    example: '测试公司',
+    description: '公司名称'
+  }),
+  position: z.string().optional().openapi({
+    example: '经理',
+    description: '职位'
+  }),
+  status: z.coerce.number().int().openapi({
+    example: 0,
+    description: '线索状态: 0-新线索, 1-已联系, 2-已转化, 3-已放弃'
+  }),
+  source: z.string().optional().openapi({
+    example: '网站',
+    description: '线索来源'
+  }),
+  notes: z.string().optional().openapi({
+    example: '客户对产品感兴趣',
+    description: '备注信息'
+  })
+});
+
+// 响应Schema
+const CreateLeadResponse = z.object({
+  code: z.number().openapi({ example: 200 }),
+  message: z.string().openapi({ example: '创建成功' }),
+  data: LeadSchema
+});
+
+// 路由定义
+const routeDef = createRoute({
+  method: 'post',
+  path: '/',
+  middleware: [authMiddleware],
+  request: {
+    body: {
+      content: {
+        'application/json': { schema: CreateLeadSchema }
+      }
+    }
+  },
+  responses: {
+    200: {
+      description: '创建销售线索成功',
+      content: {
+        'application/json': { schema: CreateLeadResponse }
+      }
+    },
+    400: {
+      description: '请求参数错误',
+      content: {
+        'application/json': { schema: ErrorSchema }
+      }
+    },
+    500: {
+      description: '服务器内部错误',
+      content: {
+        'application/json': { schema: ErrorSchema }
+      }
+    }
+  }
+});
+
+// 创建路由实例
+const app = new OpenAPIHono<AuthContext>().openapi(routeDef, async (c) => {
+  try {
+    const leadData = c.req.valid('json');
+    
+    logger.api('Creating new lead:', leadData);
+    
+    const newLead = await leadService.create(leadData);
+    
+    return c.json({
+      code: 200,
+      message: '创建成功',
+      data: newLead
+    }, 200);
+  } catch (error) {
+    logger.error('Error creating lead:', error);
+    return c.json({
+      code: 500,
+      message: error instanceof Error ? error.message : '创建销售线索失败'
+    }, 500);
+  }
+});
+
+export default app;

+ 111 - 0
src/server/api/opportunities/[id]/delete.ts

@@ -0,0 +1,111 @@
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { z } from '@hono/zod-openapi';
+import { AppDataSource } from '@/server/data-source';
+import { OpportunityService } from '@/server/modules/opportunities/opportunity.service';
+import { ErrorSchema } from '@/server/utils/errorHandler';
+import { logger } from '@/server/utils/logger';
+import { authMiddleware } from '@/server/middleware/auth';
+import { AuthContext } from '@/server/types/context';
+
+// 定义路径参数Schema
+const DeleteParamsSchema = z.object({
+  id: z.coerce.number().int().positive().openapi({
+    param: { name: 'id', in: 'path' },
+    example: 1,
+    description: '销售机会ID'
+  })
+});
+
+// 创建响应Schema
+const DeleteOpportunityResponse = z.object({
+  code: z.number().openapi({ example: 200 }),
+  message: z.string().openapi({ example: '销售机会删除成功' })
+});
+
+// 定义路由
+const routeDef = createRoute({
+  method: 'delete',
+  path: '/{id}',
+  middleware: [authMiddleware],
+  request: {
+    params: DeleteParamsSchema
+  },
+  responses: {
+    200: {
+      description: '销售机会删除成功',
+      content: {
+        'application/json': {
+          schema: DeleteOpportunityResponse
+        }
+      }
+    },
+    400: {
+      description: '请求参数错误',
+      content: {
+        'application/json': {
+          schema: ErrorSchema
+        }
+      }
+    },
+    404: {
+      description: '销售机会不存在',
+      content: {
+        'application/json': {
+          schema: ErrorSchema
+        }
+      }
+    },
+    500: {
+      description: '服务器内部错误',
+      content: {
+        'application/json': {
+          schema: ErrorSchema
+        }
+      }
+    }
+  }
+});
+
+// 创建路由实例
+const app = new OpenAPIHono<AuthContext>().openapi(routeDef, async (c) => {
+  try {
+    const { id } = c.req.valid('param');
+    logger.api('Deleting opportunity with id: %d', id);
+
+    // 初始化服务
+    const opportunityService = new OpportunityService(AppDataSource);
+    
+    // 检查销售机会是否存在
+    const existingOpportunity = await opportunityService.findOne(id);
+    if (!existingOpportunity) {
+      logger.error('Opportunity not found with id: %d', id);
+      return c.json({
+        code: 404,
+        message: '销售机会不存在'
+      }, 404);
+    }
+    
+    // 删除销售机会(软删除)
+    const result = await opportunityService.remove(id);
+    if (!result) {
+      logger.error('Failed to delete opportunity with id: %d', id);
+      return c.json({
+        code: 500,
+        message: '销售机会删除失败'
+      }, 500);
+    }
+    
+    return c.json({
+      code: 200,
+      message: '销售机会删除成功'
+    }, 200);
+  } catch (error) {
+    logger.error('Error deleting opportunity: %o', error);
+    return c.json({
+      code: 500,
+      message: error instanceof Error ? error.message : '销售机会删除失败'
+    }, 500);
+  }
+});
+
+export default app;

+ 104 - 0
src/server/api/opportunities/[id]/get.ts

@@ -0,0 +1,104 @@
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { z } from '@hono/zod-openapi';
+import { AppDataSource } from '@/server/data-source';
+import { OpportunityService } from '@/server/modules/opportunities/opportunity.service';
+import { OpportunitySchema } from '@/server/modules/opportunities/opportunity.entity';
+import { ErrorSchema } from '@/server/utils/errorHandler';
+import { logger } from '@/server/utils/logger';
+import { authMiddleware } from '@/server/middleware/auth';
+import { AuthContext } from '@/server/types/context';
+
+// 定义路径参数Schema
+const GetParamsSchema = z.object({
+  id: z.coerce.number().int().positive().openapi({
+    param: { name: 'id', in: 'path' },
+    example: 1,
+    description: '销售机会ID'
+  })
+});
+
+// 创建响应Schema
+const GetOpportunityResponse = z.object({
+  code: z.number().openapi({ example: 200 }),
+  message: z.string().openapi({ example: '获取销售机会详情成功' }),
+  data: OpportunitySchema
+});
+
+// 定义路由
+const routeDef = createRoute({
+  method: 'get',
+  path: '/{id}',
+  middleware: [authMiddleware],
+  request: {
+    params: GetParamsSchema
+  },
+  responses: {
+    200: {
+      description: '获取销售机会详情成功',
+      content: {
+        'application/json': {
+          schema: GetOpportunityResponse
+        }
+      }
+    },
+    400: {
+      description: '请求参数错误',
+      content: {
+        'application/json': {
+          schema: ErrorSchema
+        }
+      }
+    },
+    404: {
+      description: '销售机会不存在',
+      content: {
+        'application/json': {
+          schema: ErrorSchema
+        }
+      }
+    },
+    500: {
+      description: '服务器内部错误',
+      content: {
+        'application/json': {
+          schema: ErrorSchema
+        }
+      }
+    }
+  }
+});
+
+// 创建路由实例
+const app = new OpenAPIHono<AuthContext>().openapi(routeDef, async (c) => {
+  try {
+    const { id } = c.req.valid('param');
+    logger.api('Fetching opportunity with id: %d', id);
+
+    // 初始化服务
+    const opportunityService = new OpportunityService(AppDataSource);
+    
+    // 获取销售机会详情
+    const opportunity = await opportunityService.findOne(id);
+    if (!opportunity) {
+      logger.error('Opportunity not found with id: %d', id);
+      return c.json({
+        code: 404,
+        message: '销售机会不存在'
+      }, 404);
+    }
+    
+    return c.json({
+      code: 200,
+      message: '获取销售机会详情成功',
+      data: opportunity
+    }, 200);
+  } catch (error) {
+    logger.error('Error fetching opportunity: %o', error);
+    return c.json({
+      code: 500,
+      message: error instanceof Error ? error.message : '获取销售机会详情失败'
+    }, 500);
+  }
+});
+
+export default app;

+ 132 - 0
src/server/api/opportunities/[id]/put.ts

@@ -0,0 +1,132 @@
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { z } from '@hono/zod-openapi';
+import { AppDataSource } from '@/server/data-source';
+import { OpportunityService } from '@/server/modules/opportunities/opportunity.service';
+import { OpportunitySchema } from '@/server/modules/opportunities/opportunity.entity';
+import { ErrorSchema } from '@/server/utils/errorHandler';
+import { logger } from '@/server/utils/logger';
+import { authMiddleware } from '@/server/middleware/auth';
+import { AuthContext } from '@/server/types/context';
+
+// 定义路径参数Schema
+const UpdateParamsSchema = z.object({
+  id: z.coerce.number().int().positive().openapi({
+    param: { name: 'id', in: 'path' },
+    example: 1,
+    description: '销售机会ID'
+  })
+});
+
+// 创建请求Schema(排除自动生成的字段)
+const UpdateOpportunitySchema = OpportunitySchema.omit({
+  id: true,
+  isDeleted: true,
+  createdAt: true,
+  updatedAt: true,
+  closedAt: true
+}).partial();
+
+// 创建响应Schema
+const UpdateOpportunityResponse = z.object({
+  code: z.number().openapi({ example: 200 }),
+  message: z.string().openapi({ example: '销售机会更新成功' }),
+  data: OpportunitySchema
+});
+
+// 定义路由
+const routeDef = createRoute({
+  method: 'put',
+  path: '/{id}',
+  middleware: [authMiddleware],
+  request: {
+    params: UpdateParamsSchema,
+    body: {
+      content: {
+        'application/json': {
+          schema: UpdateOpportunitySchema
+        }
+      }
+    }
+  },
+  responses: {
+    200: {
+      description: '销售机会更新成功',
+      content: {
+        'application/json': {
+          schema: UpdateOpportunityResponse
+        }
+      }
+    },
+    400: {
+      description: '请求参数错误',
+      content: {
+        'application/json': {
+          schema: ErrorSchema
+        }
+      }
+    },
+    404: {
+      description: '销售机会不存在',
+      content: {
+        'application/json': {
+          schema: ErrorSchema
+        }
+      }
+    },
+    500: {
+      description: '服务器内部错误',
+      content: {
+        'application/json': {
+          schema: ErrorSchema
+        }
+      }
+    }
+  }
+});
+
+// 创建路由实例
+const app = new OpenAPIHono<AuthContext>().openapi(routeDef, async (c) => {
+  try {
+    const { id } = c.req.valid('param');
+    const body = c.req.valid('json');
+    logger.api('Updating opportunity with id: %d, data: %o', id, body);
+
+    // 初始化服务
+    const opportunityService = new OpportunityService(AppDataSource);
+    
+    // 检查销售机会是否存在
+    const existingOpportunity = await opportunityService.findOne(id);
+    if (!existingOpportunity) {
+      logger.error('Opportunity not found with id: %d', id);
+      return c.json({
+        code: 404,
+        message: '销售机会不存在'
+      }, 404);
+    }
+    
+    // 更新销售机会
+    const updatedOpportunity = await opportunityService.update(id, body);
+    
+    if (!updatedOpportunity) {
+      logger.error('Failed to update opportunity with id: %d', id);
+      return c.json({
+        code: 500,
+        message: '销售机会更新失败'
+      }, 500);
+    }
+    
+    return c.json({
+      code: 200,
+      message: '销售机会更新成功',
+      data: updatedOpportunity
+    }, 200);
+  } catch (error) {
+    logger.error('Error updating opportunity: %o', error);
+    return c.json({
+      code: 500,
+      message: error instanceof Error ? error.message : '销售机会更新失败'
+    }, 500);
+  }
+});
+
+export default app;

+ 130 - 0
src/server/api/opportunities/[id]/stage.ts

@@ -0,0 +1,130 @@
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { z } from '@hono/zod-openapi';
+import { AppDataSource } from '@/server/data-source';
+import { OpportunityService } from '@/server/modules/opportunities/opportunity.service';
+import { OpportunitySchema } from '@/server/modules/opportunities/opportunity.entity';
+import { ErrorSchema } from '@/server/utils/errorHandler';
+import { logger } from '@/server/utils/logger';
+import { authMiddleware } from '@/server/middleware/auth';
+import { AuthContext } from '@/server/types/context';
+
+// 定义路径参数Schema
+const StageParamsSchema = z.object({
+  id: z.coerce.number().int().positive().openapi({
+    param: { name: 'id', in: 'path' },
+    example: 1,
+    description: '销售机会ID'
+  })
+});
+
+// 定义请求体Schema
+const StageBodySchema = z.object({
+  stage: z.enum(['prospecting', 'qualification', 'proposal', 'negotiation', 'closed_won', 'closed_lost']).openapi({
+    description: '销售阶段',
+    example: 'proposal'
+  })
+});
+
+// 创建响应Schema
+const StageResponse = z.object({
+  code: z.number().openapi({ example: 200 }),
+  message: z.string().openapi({ example: '销售阶段更新成功' }),
+  data: OpportunitySchema
+});
+
+// 定义路由
+const routeDef = createRoute({
+  method: 'put',
+  path: '/{id}/stage',
+  middleware: [authMiddleware],
+  request: {
+    params: StageParamsSchema,
+    body: {
+      content: {
+        'application/json': {
+          schema: StageBodySchema
+        }
+      }
+    }
+  },
+  responses: {
+    200: {
+      description: '销售阶段更新成功',
+      content: {
+        'application/json': {
+          schema: StageResponse
+        }
+      }
+    },
+    400: {
+      description: '请求参数错误',
+      content: {
+        'application/json': {
+          schema: ErrorSchema
+        }
+      }
+    },
+    404: {
+      description: '销售机会不存在',
+      content: {
+        'application/json': {
+          schema: ErrorSchema
+        }
+      }
+    },
+    500: {
+      description: '服务器内部错误',
+      content: {
+        'application/json': {
+          schema: ErrorSchema
+        }
+      }
+    }
+  }
+});
+
+// 创建路由实例
+const app = new OpenAPIHono<AuthContext>().openapi(routeDef, async (c) => {
+  try {
+    const { id } = c.req.valid('param');
+    const { stage } = c.req.valid('json');
+    logger.api('Changing opportunity stage with id: %d, new stage: %s', id, stage);
+
+    // 初始化服务
+    const opportunityService = new OpportunityService(AppDataSource);
+    
+    // 检查销售机会是否存在
+    const existingOpportunity = await opportunityService.findOne(id);
+    if (!existingOpportunity) {
+      logger.error('Opportunity not found with id: %d', id);
+      return c.json({
+        code: 404,
+        message: '销售机会不存在'
+      }, 404);
+    }
+    
+    // 转换销售阶段
+    const updatedOpportunity = await opportunityService.changeStage(id, stage);
+    if (!updatedOpportunity) {
+      logger.error('Failed to change stage for opportunity with id: %d', id);
+      return c.json({
+        code: 500,
+        message: '销售阶段更新失败'
+      }, 500);
+    }
+    
+    return c.json({
+      code: 200,
+      message: '销售阶段更新成功',
+      data: updatedOpportunity
+    }, 200);
+  } catch (error) {
+    logger.error('Error changing opportunity stage: %o', error);
+    return c.json({
+      code: 500,
+      message: error instanceof Error ? error.message : '销售阶段更新失败'
+    }, 500);
+  }
+});
+
+export default app;

+ 142 - 0
src/server/api/opportunities/get.ts

@@ -0,0 +1,142 @@
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { z } from '@hono/zod-openapi';
+import { AppDataSource } from '@/server/data-source';
+import { OpportunityService } from '@/server/modules/opportunities/opportunity.service';
+import { OpportunitySchema } from '@/server/modules/opportunities/opportunity.entity';
+import { ErrorSchema } from '@/server/utils/errorHandler';
+import { logger } from '@/server/utils/logger';
+import { authMiddleware } from '@/server/middleware/auth';
+import { AuthContext } from '@/server/types/context';
+
+// 创建分页响应Schema
+const OpportunityListResponse = z.object({
+  code: z.number().openapi({ example: 200 }),
+  message: z.string().openapi({ example: '获取销售机会列表成功' }),
+  data: z.object({
+    data: z.array(OpportunitySchema),
+    pagination: z.object({
+      total: z.number().openapi({
+        example: 100,
+        description: '总记录数'
+      }),
+      current: z.number().openapi({
+        example: 1,
+        description: '当前页码'
+      }),
+      pageSize: z.number().openapi({
+        example: 10,
+        description: '每页数量'
+      })
+    })
+  })
+});
+
+// 定义查询参数Schema
+const GetQuerySchema = z.object({
+  page: z.coerce.number().int().positive().default(1).openapi({
+    description: '页码',
+    example: 1
+  }),
+  pageSize: z.coerce.number().int().positive().default(10).openapi({
+    description: '每页条数',
+    example: 10
+  }),
+  stage: z.string().optional().openapi({
+    description: '销售阶段筛选',
+    example: 'proposal'
+  }),
+  customerId: z.coerce.number().int().positive().optional().openapi({
+    description: '客户ID筛选',
+    example: 1
+  }),
+  minAmount: z.coerce.number().positive().optional().openapi({
+    description: '最小金额筛选',
+    example: 10000
+  }),
+  maxAmount: z.coerce.number().positive().optional().openapi({
+    description: '最大金额筛选',
+    example: 100000
+  })
+});
+
+// 定义路由
+const routeDef = createRoute({
+  method: 'get',
+  path: '/',
+  middleware: [authMiddleware],
+  request: {
+    query: GetQuerySchema
+  },
+  responses: {
+    200: {
+      description: '获取销售机会列表成功',
+      content: {
+        'application/json': {
+          schema: OpportunityListResponse
+        }
+      }
+    },
+    400: {
+      description: '请求参数错误',
+      content: {
+        'application/json': {
+          schema: ErrorSchema
+        }
+      }
+    },
+    500: {
+      description: '服务器内部错误',
+      content: {
+        'application/json': {
+          schema: ErrorSchema
+        }
+      }
+    }
+  }
+});
+
+// 创建路由实例
+const app = new OpenAPIHono<AuthContext>().openapi(routeDef, async (c) => {
+  try {
+    const query = c.req.valid('query');
+    logger.api('Fetching opportunities with query: %o', query);
+
+    // 初始化服务
+    const opportunityService = new OpportunityService(AppDataSource);
+    
+    // 构建筛选条件
+    const filters: any = {};
+    if (query.stage) filters.stage = query.stage;
+    if (query.customerId) filters.customerId = query.customerId;
+    if (query.minAmount) filters.minAmount = query.minAmount;
+    if (query.maxAmount) filters.maxAmount = query.maxAmount;
+    
+    // 获取数据
+    const { data, total } = await opportunityService.findAll(
+      query.page,
+      query.pageSize,
+      filters
+    );
+    
+    return c.json({
+      code: 200,
+      message: '获取销售机会列表成功',
+      data: {
+        data,
+        pagination: {
+          total,
+          current: query.page,
+          pageSize: query.pageSize
+        }
+      }
+    }, 200);
+  } catch (error) {
+    logger.error('Error fetching opportunities: %o', error);
+    return c.json({
+      code: 500,
+      message: error instanceof Error ? error.message : '获取销售机会列表失败'
+    }, 500);
+  }
+});
+
+export default app;

+ 17 - 0
src/server/api/opportunities/index.ts

@@ -0,0 +1,17 @@
+import { OpenAPIHono } from '@hono/zod-openapi';
+import listRoute from './get';
+import createRoute from './post';
+import getByIdRoute from './[id]/get';
+import updateRoute from './[id]/put';
+import deleteRoute from './[id]/delete';
+import stageRoute from './[id]/stage';
+
+const app = new OpenAPIHono()
+  .route('/', listRoute)
+  .route('/', createRoute)
+  .route('/', getByIdRoute)
+  .route('/', updateRoute)
+  .route('/', deleteRoute)
+  .route('/', stageRoute);
+
+export default app;

+ 107 - 0
src/server/api/opportunities/post.ts

@@ -0,0 +1,107 @@
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { z } from '@hono/zod-openapi';
+import { AppDataSource } from '@/server/data-source';
+import { OpportunityService } from '@/server/modules/opportunities/opportunity.service';
+import { OpportunitySchema } from '@/server/modules/opportunities/opportunity.entity';
+import { ErrorSchema } from '@/server/utils/errorHandler';
+import { logger } from '@/server/utils/logger';
+import { authMiddleware } from '@/server/middleware/auth';
+import { AuthContext } from '@/server/types/context';
+
+// 创建请求Schema(排除自动生成的字段)
+const CreateOpportunitySchema = OpportunitySchema.omit({
+  id: true,
+  isDeleted: true,
+  createdAt: true,
+  updatedAt: true,
+  closedAt: true
+});
+
+// 创建响应Schema
+const CreateOpportunityResponse = z.object({
+  code: z.number().openapi({ example: 201 }),
+  message: z.string().openapi({ example: '销售机会创建成功' }),
+  data: OpportunitySchema
+});
+
+// 定义路由
+const routeDef = createRoute({
+  method: 'post',
+  path: '/',
+  middleware: [authMiddleware],
+  request: {
+    body: {
+      content: {
+        'application/json': {
+          schema: CreateOpportunitySchema
+        }
+      }
+    }
+  },
+  responses: {
+    201: {
+      description: '销售机会创建成功',
+      content: {
+        'application/json': {
+          schema: CreateOpportunityResponse
+        }
+      }
+    },
+    400: {
+      description: '请求参数错误',
+      content: {
+        'application/json': {
+          schema: ErrorSchema
+        }
+      }
+    },
+    401: {
+      description: '未授权访问',
+      content: {
+        'application/json': {
+          schema: ErrorSchema
+        }
+      }
+    },
+    500: {
+      description: '服务器内部错误',
+      content: {
+        'application/json': {
+          schema: ErrorSchema
+        }
+      }
+    }
+  }
+});
+
+// 创建路由实例
+const app = new OpenAPIHono<AuthContext>().openapi(routeDef, async (c) => {
+  try {
+    const body = c.req.valid('json');
+    logger.api('Creating opportunity with data: %o', body);
+
+    // 初始化服务
+    const opportunityService = new OpportunityService(AppDataSource);
+    
+    // 创建销售机会
+    const newOpportunity = await opportunityService.create({
+      ...body,
+      // 设置默认值
+      isDeleted: 0
+    });
+    
+    return c.json({
+      code: 201,
+      message: '销售机会创建成功',
+      data: newOpportunity
+    }, 201);
+  } catch (error) {
+    logger.error('Error creating opportunity: %o', error);
+    return c.json({
+      code: 500,
+      message: error instanceof Error ? error.message : '销售机会创建失败'
+    }, 500);
+  }
+});
+
+export default app;

+ 77 - 0
src/server/modules/leads/lead.service.ts

@@ -0,0 +1,77 @@
+import { DataSource, Repository } from 'typeorm';
+import { Lead } from './lead.entity';
+import { logger } from '@/server/utils/logger';
+
+export class LeadService {
+  private leadRepository: Repository<Lead>;
+
+  constructor(dataSource: DataSource) {
+    this.leadRepository = dataSource.getRepository(Lead);
+  }
+
+  /**
+   * 获取销售线索列表,支持分页和条件筛选
+   */
+  async findAll(
+    page: number = 1,
+    pageSize: number = 10,
+    filters: Partial<Lead> = {}
+  ): Promise<{ data: Lead[], total: number, current: number, pageSize: number }> {
+    logger.api('Fetching leads with filters:', filters);
+    
+    const skip = (page - 1) * pageSize;
+    
+    // 构建查询
+    const query = this.leadRepository.createQueryBuilder('lead')
+      .where(filters)
+      .skip(skip)
+      .take(pageSize);
+    
+    const [data, total] = await query.getManyAndCount();
+    
+    return {
+      data,
+      total,
+      current: page,
+      pageSize
+    };
+  }
+
+  /**
+   * 根据ID获取销售线索详情
+   */
+  async findById(id: number): Promise<Lead | null> {
+    logger.api(`Fetching lead with id: ${id}`);
+    return this.leadRepository.findOneBy({ id });
+  }
+
+  /**
+   * 创建新销售线索
+   */
+  async create(leadData: Partial<Lead>): Promise<Lead> {
+    logger.api('Creating new lead:', leadData);
+    
+    const lead = this.leadRepository.create(leadData);
+    return this.leadRepository.save(lead);
+  }
+
+  /**
+   * 更新销售线索
+   */
+  async update(id: number, leadData: Partial<Lead>): Promise<Lead | null> {
+    logger.api(`Updating lead with id: ${id}`, leadData);
+    
+    await this.leadRepository.update(id, leadData);
+    return this.findById(id);
+  }
+
+  /**
+   * 删除销售线索
+   */
+  async delete(id: number): Promise<boolean> {
+    logger.api(`Deleting lead with id: ${id}`);
+    
+    const result = await this.leadRepository.delete(id);
+    return (result.affected ?? 0) > 0;
+  }
+}

+ 14 - 2
src/server/modules/opportunities/opportunity.entity.ts

@@ -112,6 +112,14 @@ export class Opportunity {
   })
   updatedAt!: Date;
 
+  @Column({
+    name: 'closed_at',
+    type: 'timestamp',
+    nullable: true,
+    comment: '关闭时间'
+  })
+  closedAt!: Date | null;
+
   // 关系定义
   @ManyToOne(() => Customer, customer => customer.opportunities)
   customer!: Customer;
@@ -169,8 +177,12 @@ export const OpportunitySchema = z.object({
     description: '创建时间',
     example: '2023-01-01T00:00:00Z' 
   }),
-  updatedAt: z.date().openapi({ 
+  updatedAt: z.date().openapi({
     description: '更新时间',
-    example: '2023-01-01T00:00:00Z' 
+    example: '2023-01-01T00:00:00Z'
+  }),
+  closedAt: z.date().nullable().openapi({
+    description: '关闭时间',
+    example: '2023-06-30T00:00:00Z'
   })
 });

+ 126 - 0
src/server/modules/opportunities/opportunity.service.ts

@@ -0,0 +1,126 @@
+import { DataSource, Repository } from 'typeorm';
+import { Opportunity } from './opportunity.entity';
+import { logger } from '@/server/utils/logger';
+
+export class OpportunityService {
+  private repository: Repository<Opportunity>;
+
+  constructor(dataSource: DataSource) {
+    this.repository = dataSource.getRepository(Opportunity);
+  }
+
+  /**
+   * 创建销售机会
+   */
+  async create(data: Partial<Opportunity>): Promise<Opportunity> {
+    logger.api('Creating opportunity with data: %o', data);
+    
+    const opportunity = this.repository.create(data);
+    return this.repository.save(opportunity);
+  }
+
+  /**
+   * 获取销售机会列表(支持分页和筛选)
+   */
+  async findAll(
+    page: number = 1, 
+    pageSize: number = 10, 
+    filters: any = {}
+  ): Promise<{ data: Opportunity[], total: number }> {
+    logger.api('Finding opportunities with page: %d, pageSize: %d, filters: %o', page, pageSize, filters);
+    
+    const skip = (page - 1) * pageSize;
+    
+    // 构建查询条件
+    const query = this.repository.createQueryBuilder('opportunity')
+      .where('opportunity.isDeleted = 0');
+    
+    // 添加筛选条件
+    if (filters.stage) {
+      query.andWhere('opportunity.stage = :stage', { stage: filters.stage });
+    }
+    
+    if (filters.customerId) {
+      query.andWhere('opportunity.customerId = :customerId', { customerId: filters.customerId });
+    }
+    
+    if (filters.minAmount || filters.maxAmount) {
+      if (filters.minAmount) {
+        query.andWhere('opportunity.amount >= :minAmount', { minAmount: filters.minAmount });
+      }
+      
+      if (filters.maxAmount) {
+        query.andWhere('opportunity.amount <= :maxAmount', { maxAmount: filters.maxAmount });
+      }
+    }
+    
+    // 执行查询
+    const [data, total] = await query
+      .skip(skip)
+      .take(pageSize)
+      .orderBy('opportunity.updatedAt', 'DESC')
+      .getManyAndCount();
+    
+    return { data, total };
+  }
+
+  /**
+   * 获取销售机会详情
+   */
+  async findOne(id: number): Promise<Opportunity | null> {
+    logger.api('Finding opportunity with id: %d', id);
+    
+    return this.repository.findOne({
+      where: { id, isDeleted: 0 }
+    });
+  }
+
+  /**
+   * 更新销售机会
+   */
+  async update(id: number, data: Partial<Opportunity>): Promise<Opportunity | null> {
+    logger.api('Updating opportunity with id: %d, data: %o', id, data);
+    
+    await this.repository.update(id, data);
+    return this.findOne(id);
+  }
+
+  /**
+   * 删除销售机会(软删除)
+   */
+  async remove(id: number): Promise<boolean> {
+    logger.api('Deleting opportunity with id: %d', id);
+    
+    const result = await this.repository.update(id, { isDeleted: 1 });
+    return result.affected === 1;
+  }
+
+  /**
+   * 转换销售机会阶段
+   */
+  async changeStage(id: number, stage: string): Promise<Opportunity | null> {
+    logger.api('Changing opportunity stage with id: %d, stage: %s', id, stage);
+    
+    // 获取当前销售机会
+    const opportunity = await this.findOne(id);
+    if (!opportunity) {
+      logger.error('Opportunity not found with id: %d', id);
+      return null;
+    }
+    
+    // 阶段转换业务逻辑和状态验证
+    const validStages = ['prospecting', 'qualification', 'proposal', 'negotiation', 'closed_won', 'closed_lost'];
+    if (!validStages.includes(stage)) {
+      logger.error('Invalid stage: %s for opportunity id: %d', stage, id);
+      throw new Error('Invalid sales stage');
+    }
+    
+    // 如果是转换为已赢单或已输单,添加相应的处理逻辑
+    if (stage === 'closed_won' || stage === 'closed_lost') {
+      opportunity.closedAt = new Date();
+    }
+    
+    opportunity.stage = stage;
+    return this.repository.save(opportunity);
+  }
+}