257 lines
7.8 KiB
TypeScript
257 lines
7.8 KiB
TypeScript
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<TreePricingConfigResponse> {
|
||
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<TreePricingConfigResponse> {
|
||
// 允许负数(降价对冲),但总价不能低于 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<TreePricingConfigResponse> {
|
||
const config = await this.getOrCreateConfig();
|
||
|
||
const data: Record<string, unknown> = {
|
||
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<boolean> {
|
||
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;
|
||
}
|
||
}
|