rwadurian/backend/services/admin-service/src/pricing/tree-pricing.service.ts

257 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
}
}