import { Injectable, Logger } from '@nestjs/common'; import { PrismaService } from '../infrastructure/persistence/prisma/prisma.service'; /** 基础价常量 */ const BASE_PRICE = 15831; // 正式认种基础价(不变) const BASE_PORTION_PRICE = 1887; // 预种基础价 [2026-03-01 调整] 9项 floor(18870/10) 取整 + 总部吸收余额 const PORTIONS_PER_TREE = 10; // [2026-03-01 调整] 5 → 10 份/棵 export interface TreePricingConfigResponse { basePrice: number; basePortionPrice: number; currentSupplement: number; totalPrice: number; totalPortionPrice: number; autoIncreaseEnabled: boolean; autoIncreaseAmount: number; autoIncreaseIntervalDays: number; lastAutoIncreaseAt: Date | null; nextAutoIncreaseAt: Date | null; updatedAt: Date; } export interface TreePriceChangeLogItem { id: string; changeType: string; previousSupplement: number; newSupplement: number; changeAmount: number; reason: string | null; operatorId: string | null; createdAt: Date; } @Injectable() export class TreePricingService { private readonly logger = new Logger(TreePricingService.name); constructor(private readonly prisma: PrismaService) {} /** * 获取当前定价配置,不存在则创建默认配置 */ async getConfig(): Promise { let config = await this.prisma.treePricingConfig.findFirst({ orderBy: { updatedAt: 'desc' }, }); if (!config) { config = await this.prisma.treePricingConfig.create({ data: { currentSupplement: 0, autoIncreaseEnabled: false }, }); this.logger.log('[TREE-PRICING] Default config created'); } const totalPrice = BASE_PRICE + config.currentSupplement; // 预种价格 = 基础预种价(1887) + floor(加价部分/10) const totalPortionPrice = BASE_PORTION_PRICE + Math.floor(config.currentSupplement / PORTIONS_PER_TREE); return { basePrice: BASE_PRICE, basePortionPrice: BASE_PORTION_PRICE, currentSupplement: config.currentSupplement, totalPrice, totalPortionPrice, autoIncreaseEnabled: config.autoIncreaseEnabled, autoIncreaseAmount: config.autoIncreaseAmount, autoIncreaseIntervalDays: config.autoIncreaseIntervalDays, lastAutoIncreaseAt: config.lastAutoIncreaseAt, nextAutoIncreaseAt: config.nextAutoIncreaseAt, updatedAt: config.updatedAt, }; } /** * 手动修改加价金额(事务:同时更新配置 + 写入审计日志) * 涨价原因:总部运营成本压力 */ async updateSupplement( newSupplement: number, reason: string, operatorId: string, ): Promise { // 允许负数(降价对冲),但总价不能低于 0 if (BASE_PRICE + newSupplement < 0) { throw new Error(`调价金额不能低于 -${BASE_PRICE},否则总价为负`); } const config = await this.getOrCreateConfig(); const previousSupplement = config.currentSupplement; const changeAmount = newSupplement - previousSupplement; if (changeAmount === 0) { return this.getConfig(); } await this.prisma.$transaction([ this.prisma.treePricingConfig.update({ where: { id: config.id }, data: { currentSupplement: newSupplement, updatedBy: operatorId, }, }), this.prisma.treePriceChangeLog.create({ data: { changeType: 'MANUAL', previousSupplement, newSupplement, changeAmount, reason: reason || '总部运营成本压力调价', operatorId, }, }), ]); this.logger.log( `[TREE-PRICING] Manual supplement update: ${previousSupplement} → ${newSupplement} (${changeAmount > 0 ? '+' : ''}${changeAmount}) by ${operatorId}, reason: ${reason}`, ); return this.getConfig(); } /** * 更新自动涨价设置 */ async updateAutoIncreaseSettings( enabled: boolean, amount?: number, intervalDays?: number, operatorId?: string, ): Promise { const config = await this.getOrCreateConfig(); const data: Record = { autoIncreaseEnabled: enabled, updatedBy: operatorId || null, }; if (amount !== undefined) { if (amount < 0) throw new Error('自动涨价金额不能为负数'); data.autoIncreaseAmount = amount; } if (intervalDays !== undefined) { if (intervalDays < 1) throw new Error('自动涨价间隔天数不能小于1'); data.autoIncreaseIntervalDays = intervalDays; } // 启用时计算下次涨价时间 if (enabled) { const interval = intervalDays ?? config.autoIncreaseIntervalDays; if (interval > 0) { const nextDate = new Date(); nextDate.setDate(nextDate.getDate() + interval); data.nextAutoIncreaseAt = nextDate; } } else { data.nextAutoIncreaseAt = null; } await this.prisma.treePricingConfig.update({ where: { id: config.id }, data, }); this.logger.log( `[TREE-PRICING] Auto-increase settings updated: enabled=${enabled}, amount=${amount ?? config.autoIncreaseAmount}, intervalDays=${intervalDays ?? config.autoIncreaseIntervalDays} by ${operatorId || 'unknown'}`, ); return this.getConfig(); } /** * 获取变更审计日志(分页) */ async getChangeLog( page: number = 1, pageSize: number = 20, ): Promise<{ items: TreePriceChangeLogItem[]; total: number }> { const [items, total] = await this.prisma.$transaction([ this.prisma.treePriceChangeLog.findMany({ orderBy: { createdAt: 'desc' }, skip: (page - 1) * pageSize, take: pageSize, }), this.prisma.treePriceChangeLog.count(), ]); return { items, total }; } /** * 执行自动涨价(由定时任务调用) * 涨价原因:系统自动涨价(总部运营成本压力) * @returns true 如果执行了涨价,false 如果未到时间或未启用 */ async executeAutoIncrease(): Promise { const config = await this.prisma.treePricingConfig.findFirst({ orderBy: { updatedAt: 'desc' }, }); if (!config) return false; if (!config.autoIncreaseEnabled) return false; if (!config.nextAutoIncreaseAt) return false; if (config.autoIncreaseAmount <= 0) return false; const now = new Date(); if (now < config.nextAutoIncreaseAt) return false; const previousSupplement = config.currentSupplement; const newSupplement = previousSupplement + config.autoIncreaseAmount; const nextDate = new Date(now); nextDate.setDate(nextDate.getDate() + config.autoIncreaseIntervalDays); await this.prisma.$transaction([ this.prisma.treePricingConfig.update({ where: { id: config.id }, data: { currentSupplement: newSupplement, lastAutoIncreaseAt: now, nextAutoIncreaseAt: nextDate, }, }), this.prisma.treePriceChangeLog.create({ data: { changeType: 'AUTO', previousSupplement, newSupplement, changeAmount: config.autoIncreaseAmount, reason: '系统自动涨价(总部运营成本压力)', operatorId: 'SYSTEM', }, }), ]); this.logger.log( `[TREE-PRICING] Auto-increase executed: ${previousSupplement} → ${newSupplement} (+${config.autoIncreaseAmount}), next: ${nextDate.toISOString()}`, ); return true; } /** 获取或创建配置(内部方法) */ private async getOrCreateConfig() { let config = await this.prisma.treePricingConfig.findFirst({ orderBy: { updatedAt: 'desc' }, }); if (!config) { config = await this.prisma.treePricingConfig.create({ data: { currentSupplement: 0, autoIncreaseEnabled: false }, }); } return config; } }