|
|
@@ -0,0 +1,246 @@
|
|
|
+import { DataSource, Repository, FindOptionsWhere } from 'typeorm';
|
|
|
+import { Contract, ContractStatus as EntityContractStatus } from './contract.entity';
|
|
|
+import { logger } from '@/server/utils/logger';
|
|
|
+import { ErrorSchema } from '@/server/utils/errorHandler';
|
|
|
+import { z } from 'zod';
|
|
|
+
|
|
|
+// 合同状态枚举
|
|
|
+// 扩展实体中定义的合同状态枚举
|
|
|
+export enum ContractStatus {
|
|
|
+ DRAFT = EntityContractStatus.DRAFT,
|
|
|
+ REVIEWING = EntityContractStatus.REVIEWING,
|
|
|
+ ACTIVE = EntityContractStatus.ACTIVE,
|
|
|
+ TERMINATED = EntityContractStatus.TERMINATED,
|
|
|
+ EXPIRED = EntityContractStatus.EXPIRED
|
|
|
+}
|
|
|
+
|
|
|
+// 合同状态流转规则
|
|
|
+const STATUS_TRANSITIONS: Record<ContractStatus, ContractStatus[]> = {
|
|
|
+ [ContractStatus.DRAFT]: [ContractStatus.REVIEWING, ContractStatus.TERMINATED],
|
|
|
+ [ContractStatus.REVIEWING]: [ContractStatus.ACTIVE, ContractStatus.DRAFT, ContractStatus.TERMINATED],
|
|
|
+ [ContractStatus.ACTIVE]: [ContractStatus.TERMINATED, ContractStatus.EXPIRED],
|
|
|
+ [ContractStatus.TERMINATED]: [],
|
|
|
+ [ContractStatus.EXPIRED]: []
|
|
|
+};
|
|
|
+
|
|
|
+export class ContractService {
|
|
|
+ private contractRepository: Repository<Contract>;
|
|
|
+
|
|
|
+ constructor(private dataSource: DataSource) {
|
|
|
+ this.contractRepository = dataSource.getRepository(Contract);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成合同编号
|
|
|
+ * 格式: CONTRACT-{年份}{月份}{日期}-{4位随机数}
|
|
|
+ */
|
|
|
+ private async generateContractNumber(): Promise<string> {
|
|
|
+ const date = new Date();
|
|
|
+ const year = date.getFullYear().toString().slice(-2);
|
|
|
+ const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
|
|
+ const day = date.getDate().toString().padStart(2, '0');
|
|
|
+ const random = Math.floor(Math.random() * 10000).toString().padStart(4, '0');
|
|
|
+
|
|
|
+ // 确保合同编号唯一
|
|
|
+ let contractNumber = `CONTRACT-${year}${month}${day}-${random}`;
|
|
|
+ const existingContract = await this.contractRepository.findOne({
|
|
|
+ where: { contract_number: contractNumber } as FindOptionsWhere<Contract>
|
|
|
+ });
|
|
|
+
|
|
|
+ if (existingContract) {
|
|
|
+ // 如果编号已存在,递归生成新编号
|
|
|
+ return this.generateContractNumber();
|
|
|
+ }
|
|
|
+
|
|
|
+ return contractNumber;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 检查合同状态变更是否合法
|
|
|
+ */
|
|
|
+ private isValidStatusTransition(currentStatus: ContractStatus, newStatus: ContractStatus): boolean {
|
|
|
+ return STATUS_TRANSITIONS[currentStatus].includes(newStatus);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 创建合同
|
|
|
+ */
|
|
|
+ async createContract(data: Partial<Contract>): Promise<Contract> {
|
|
|
+ logger.api('Creating new contract');
|
|
|
+
|
|
|
+ try {
|
|
|
+ const contract = new Contract();
|
|
|
+ Object.assign(contract, data);
|
|
|
+
|
|
|
+ // 生成合同编号
|
|
|
+ contract.contract_number = await this.generateContractNumber();
|
|
|
+
|
|
|
+ // 默认状态为草稿
|
|
|
+ contract.status = ContractStatus.DRAFT;
|
|
|
+
|
|
|
+ // 设置创建时间和更新时间
|
|
|
+ contract.createdAt = new Date();
|
|
|
+ contract.updatedAt = new Date();
|
|
|
+
|
|
|
+ const savedContract = await this.contractRepository.save(contract);
|
|
|
+ logger.api(`Contract created with ID: ${savedContract.id}`);
|
|
|
+ return savedContract;
|
|
|
+ } catch (error) {
|
|
|
+ logger.error('Error creating contract:', error);
|
|
|
+ throw new Error('创建合同失败');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取合同列表(支持分页和筛选)
|
|
|
+ */
|
|
|
+ async getContracts(
|
|
|
+ page: number = 1,
|
|
|
+ pageSize: number = 10,
|
|
|
+ filters: Partial<Contract> = {}
|
|
|
+ ): Promise<{ data: Contract[], total: number, current: number, pageSize: number }> {
|
|
|
+ logger.api(`Fetching contracts with filters: ${JSON.stringify(filters)}, page: ${page}, pageSize: ${pageSize}`);
|
|
|
+
|
|
|
+ try {
|
|
|
+ const [data, total] = await this.contractRepository.findAndCount({
|
|
|
+ where: filters as FindOptionsWhere<Contract>,
|
|
|
+ skip: (page - 1) * pageSize,
|
|
|
+ take: pageSize,
|
|
|
+ order: { createdAt: 'DESC' }
|
|
|
+ });
|
|
|
+
|
|
|
+ return {
|
|
|
+ data,
|
|
|
+ total,
|
|
|
+ current: page,
|
|
|
+ pageSize
|
|
|
+ };
|
|
|
+ } catch (error) {
|
|
|
+ logger.error('Error fetching contracts:', error);
|
|
|
+ throw new Error('获取合同列表失败');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取合同详情
|
|
|
+ */
|
|
|
+ async getContractById(id: number): Promise<Contract | null> {
|
|
|
+ logger.api(`Fetching contract with ID: ${id}`);
|
|
|
+
|
|
|
+ try {
|
|
|
+ const contract = await this.contractRepository.findOne({
|
|
|
+ where: { id }
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!contract) {
|
|
|
+ logger.api(`Contract with ID ${id} not found`);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ return contract;
|
|
|
+ } catch (error) {
|
|
|
+ logger.error(`Error fetching contract with ID ${id}:`, error);
|
|
|
+ throw new Error('获取合同详情失败');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 更新合同
|
|
|
+ */
|
|
|
+ async updateContract(id: number, data: Partial<Contract>): Promise<Contract | null> {
|
|
|
+ logger.api(`Updating contract with ID: ${id}`);
|
|
|
+
|
|
|
+ try {
|
|
|
+ const contract = await this.contractRepository.findOne({
|
|
|
+ where: { id }
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!contract) {
|
|
|
+ logger.api(`Contract with ID ${id} not found`);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果尝试更新状态,需要验证状态流转是否合法
|
|
|
+ if (data.status && data.status !== contract.status) {
|
|
|
+ if (data.status !== undefined && !this.isValidStatusTransition(contract.status, data.status)) {
|
|
|
+ logger.error(`Invalid status transition from ${contract.status} to ${data.status}`);
|
|
|
+ throw new Error(`不允许从${contract.status}状态变更为${data.status}状态`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ Object.assign(contract, data);
|
|
|
+ contract.updatedAt = new Date();
|
|
|
+
|
|
|
+ const updatedContract = await this.contractRepository.save(contract);
|
|
|
+ logger.api(`Contract with ID ${id} updated successfully`);
|
|
|
+ return updatedContract;
|
|
|
+ } catch (error) {
|
|
|
+ logger.error(`Error updating contract with ID ${id}:`, error);
|
|
|
+ throw new Error('更新合同失败');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 删除合同
|
|
|
+ */
|
|
|
+ async deleteContract(id: number): Promise<boolean> {
|
|
|
+ logger.api(`Deleting contract with ID: ${id}`);
|
|
|
+
|
|
|
+ try {
|
|
|
+ const contract = await this.contractRepository.findOne({
|
|
|
+ where: { id }
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!contract) {
|
|
|
+ logger.api(`Contract with ID ${id} not found`);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查合同状态,如果不是草稿状态,不允许删除
|
|
|
+ if (contract.status !== ContractStatus.DRAFT) {
|
|
|
+ logger.error(`Cannot delete contract with ID ${id} because it's not in draft status`);
|
|
|
+ throw new Error('只有草稿状态的合同可以删除');
|
|
|
+ }
|
|
|
+
|
|
|
+ await this.contractRepository.remove(contract);
|
|
|
+ logger.api(`Contract with ID ${id} deleted successfully`);
|
|
|
+ return true;
|
|
|
+ } catch (error) {
|
|
|
+ logger.error(`Error deleting contract with ID ${id}:`, error);
|
|
|
+ throw new Error('删除合同失败');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 变更合同状态
|
|
|
+ */
|
|
|
+ async changeContractStatus(id: number, newStatus: ContractStatus): Promise<Contract | null> {
|
|
|
+ logger.api(`Changing contract status for ID ${id} to ${newStatus}`);
|
|
|
+
|
|
|
+ try {
|
|
|
+ const contract = await this.contractRepository.findOne({
|
|
|
+ where: { id }
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!contract) {
|
|
|
+ logger.api(`Contract with ID ${id} not found`);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!this.isValidStatusTransition(contract.status, newStatus)) {
|
|
|
+ logger.error(`Invalid status transition from ${contract.status} to ${newStatus}`);
|
|
|
+ throw new Error(`不允许从${contract.status}状态变更为${newStatus}状态`);
|
|
|
+ }
|
|
|
+
|
|
|
+ contract.status = newStatus;
|
|
|
+ contract.updatedAt = new Date();
|
|
|
+
|
|
|
+ const updatedContract = await this.contractRepository.save(contract);
|
|
|
+ logger.api(`Contract status for ID ${id} changed to ${newStatus} successfully`);
|
|
|
+ return updatedContract;
|
|
|
+ } catch (error) {
|
|
|
+ logger.error(`Error changing contract status for ID ${id}:`, error);
|
|
|
+ throw new Error('变更合同状态失败');
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|