|
|
@@ -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);
|
|
|
+ }
|
|
|
+}
|