feat(pricing): 认种树动态定价涨价系统(总部运营成本压力涨价)
基础价 15831 USDT/棵不变,新增 HQ_PRICE_SUPPLEMENT 加价项全额归总部(S0000000001)。 支持手动调价+自动周期涨价,所有变更可审计,移动端动态展示价格及涨价预告。 - admin-service: TreePricingConfig/ChangeLog 表 + Service + Controller + 定时任务 - planting-service: 正式认种和预种订单快照 priceSupplement,动态价格校验 - reward-service: HQ_PRICE_SUPPLEMENT 分配类型,涨价金额直接入总部账户 - admin-web: Settings 页面新增定价配置区间(手动调价/自动涨价/变更历史) - mobile-app: TreePricingService + 动态价格加载 + 涨价预告展示 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
023d71ac33
commit
ed6b48562a
|
|
@ -0,0 +1,41 @@
|
|||
-- CreateTable: 认种树定价配置
|
||||
-- 基础价 15831 USDT 不变,supplement 作为加价全额归总部 (S0000000001)
|
||||
-- 涨价原因:总部运营成本压力
|
||||
CREATE TABLE "tree_pricing_configs" (
|
||||
"id" TEXT NOT NULL,
|
||||
"current_supplement" INTEGER NOT NULL DEFAULT 0,
|
||||
"auto_increase_enabled" BOOLEAN NOT NULL DEFAULT false,
|
||||
"auto_increase_amount" INTEGER NOT NULL DEFAULT 0,
|
||||
"auto_increase_interval_days" INTEGER NOT NULL DEFAULT 0,
|
||||
"last_auto_increase_at" TIMESTAMP(3),
|
||||
"next_auto_increase_at" TIMESTAMP(3),
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
"updated_by" VARCHAR(50),
|
||||
|
||||
CONSTRAINT "tree_pricing_configs_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- 插入默认配置(加价为0,自动涨价关闭)
|
||||
INSERT INTO "tree_pricing_configs" ("id", "current_supplement", "auto_increase_enabled", "updated_at")
|
||||
VALUES (gen_random_uuid(), 0, false, NOW());
|
||||
|
||||
-- CreateTable: 认种树价格变更审计日志
|
||||
-- 每次价格变更(手动或自动)都会记录一条不可修改的日志,用于审计追踪
|
||||
CREATE TYPE "PriceChangeType" AS ENUM ('MANUAL', 'AUTO');
|
||||
|
||||
CREATE TABLE "tree_price_change_logs" (
|
||||
"id" TEXT NOT NULL,
|
||||
"change_type" "PriceChangeType" NOT NULL,
|
||||
"previous_supplement" INTEGER NOT NULL,
|
||||
"new_supplement" INTEGER NOT NULL,
|
||||
"change_amount" INTEGER NOT NULL,
|
||||
"reason" VARCHAR(500),
|
||||
"operator_id" VARCHAR(50),
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "tree_price_change_logs_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "tree_price_change_logs_created_at_idx" ON "tree_price_change_logs"("created_at");
|
||||
CREATE INDEX "tree_price_change_logs_change_type_idx" ON "tree_price_change_logs"("change_type");
|
||||
|
|
@ -1272,3 +1272,47 @@ model PrePlantingConfig {
|
|||
|
||||
@@map("pre_planting_configs")
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 认种树定价配置 (Tree Pricing Supplement)
|
||||
// 基础价 15831 USDT 不变,supplement 作为加价全额归总部 (S0000000001)
|
||||
// 正式认种总价 = 15831 + currentSupplement
|
||||
// 预种总价 = (15831 + currentSupplement) / 5
|
||||
// =============================================================================
|
||||
model TreePricingConfig {
|
||||
id String @id @default(uuid())
|
||||
currentSupplement Int @default(0) @map("current_supplement")
|
||||
autoIncreaseEnabled Boolean @default(false) @map("auto_increase_enabled")
|
||||
autoIncreaseAmount Int @default(0) @map("auto_increase_amount")
|
||||
autoIncreaseIntervalDays Int @default(0) @map("auto_increase_interval_days")
|
||||
lastAutoIncreaseAt DateTime? @map("last_auto_increase_at")
|
||||
nextAutoIncreaseAt DateTime? @map("next_auto_increase_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
updatedBy String? @map("updated_by") @db.VarChar(50)
|
||||
|
||||
@@map("tree_pricing_configs")
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 认种树价格变更审计日志 (Tree Price Change Audit Log)
|
||||
// 每次价格变更(手动或自动)都会记录一条不可修改的日志
|
||||
// =============================================================================
|
||||
model TreePriceChangeLog {
|
||||
id String @id @default(uuid())
|
||||
changeType PriceChangeType @map("change_type")
|
||||
previousSupplement Int @map("previous_supplement")
|
||||
newSupplement Int @map("new_supplement")
|
||||
changeAmount Int @map("change_amount")
|
||||
reason String? @db.VarChar(500)
|
||||
operatorId String? @map("operator_id") @db.VarChar(50)
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
@@index([createdAt])
|
||||
@@index([changeType])
|
||||
@@map("tree_price_change_logs")
|
||||
}
|
||||
|
||||
enum PriceChangeType {
|
||||
MANUAL // 管理员手动调整
|
||||
AUTO // 自动涨价任务
|
||||
}
|
||||
|
|
|
|||
|
|
@ -87,6 +87,10 @@ import { ContractService } from './application/services/contract.service';
|
|||
// [2026-02-17] 新增:预种计划开关管理
|
||||
import { PrePlantingConfigController, PublicPrePlantingConfigController } from './pre-planting/pre-planting-config.controller';
|
||||
import { PrePlantingConfigService } from './pre-planting/pre-planting-config.service';
|
||||
// [2026-02-26] 新增:认种树定价配置(总部运营成本压力涨价)
|
||||
import { AdminTreePricingController, PublicTreePricingController } from './pricing/tree-pricing.controller';
|
||||
import { TreePricingService } from './pricing/tree-pricing.service';
|
||||
import { AutoPriceIncreaseJob } from './infrastructure/jobs/auto-price-increase.job';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
|
@ -133,6 +137,9 @@ import { PrePlantingConfigService } from './pre-planting/pre-planting-config.ser
|
|||
// [2026-02-17] 新增:预种计划开关管理
|
||||
PrePlantingConfigController,
|
||||
PublicPrePlantingConfigController,
|
||||
// [2026-02-26] 新增:认种树定价配置(总部运营成本压力涨价)
|
||||
AdminTreePricingController,
|
||||
PublicTreePricingController,
|
||||
],
|
||||
providers: [
|
||||
PrismaService,
|
||||
|
|
@ -224,6 +231,9 @@ import { PrePlantingConfigService } from './pre-planting/pre-planting-config.ser
|
|||
ContractService,
|
||||
// [2026-02-17] 新增:预种计划开关管理
|
||||
PrePlantingConfigService,
|
||||
// [2026-02-26] 新增:认种树定价配置(总部运营成本压力涨价)
|
||||
TreePricingService,
|
||||
AutoPriceIncreaseJob,
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||
import { Cron } from '@nestjs/schedule';
|
||||
import { TreePricingService } from '../../pricing/tree-pricing.service';
|
||||
|
||||
/**
|
||||
* 自动涨价定时任务
|
||||
*
|
||||
* 每小时检查一次是否到达自动涨价时间。
|
||||
* 涨价间隔以天为单位,小时级精度足够。
|
||||
* 涨价原因:总部运营成本压力。
|
||||
*/
|
||||
@Injectable()
|
||||
export class AutoPriceIncreaseJob implements OnModuleInit {
|
||||
private readonly logger = new Logger(AutoPriceIncreaseJob.name);
|
||||
private isRunning = false;
|
||||
|
||||
constructor(private readonly pricingService: TreePricingService) {}
|
||||
|
||||
onModuleInit() {
|
||||
this.logger.log('AutoPriceIncreaseJob initialized');
|
||||
}
|
||||
|
||||
@Cron('0 * * * *') // 每小时第0分钟执行
|
||||
async checkAndExecuteAutoIncrease(): Promise<void> {
|
||||
if (this.isRunning) {
|
||||
this.logger.warn('Auto price increase check already running, skipping...');
|
||||
return;
|
||||
}
|
||||
|
||||
this.isRunning = true;
|
||||
try {
|
||||
const executed = await this.pricingService.executeAutoIncrease();
|
||||
if (executed) {
|
||||
this.logger.log('Auto price increase executed successfully');
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Auto price increase failed: ${error}`);
|
||||
} finally {
|
||||
this.isRunning = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Put,
|
||||
Body,
|
||||
Query,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
|
||||
import { TreePricingService } from './tree-pricing.service';
|
||||
|
||||
// ======================== DTO ========================
|
||||
|
||||
class UpdateSupplementDto {
|
||||
/** 新的加价金额(整数 USDT) */
|
||||
newSupplement: number;
|
||||
/** 变更原因 */
|
||||
reason: string;
|
||||
/** 操作人ID */
|
||||
operatorId?: string;
|
||||
}
|
||||
|
||||
class UpdateAutoIncreaseDto {
|
||||
/** 是否启用自动涨价 */
|
||||
enabled: boolean;
|
||||
/** 每次自动涨价金额(整数 USDT) */
|
||||
amount?: number;
|
||||
/** 自动涨价间隔天数 */
|
||||
intervalDays?: number;
|
||||
/** 操作人ID */
|
||||
operatorId?: string;
|
||||
}
|
||||
|
||||
class ChangeLogQueryDto {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
// ======================== Admin Controller ========================
|
||||
|
||||
@ApiTags('认种定价配置')
|
||||
@Controller('admin/tree-pricing')
|
||||
export class AdminTreePricingController {
|
||||
constructor(
|
||||
private readonly pricingService: TreePricingService,
|
||||
) {}
|
||||
|
||||
@Get('config')
|
||||
@ApiOperation({ summary: '获取当前定价配置' })
|
||||
@ApiResponse({ status: HttpStatus.OK, description: '定价配置信息' })
|
||||
async getConfig() {
|
||||
return this.pricingService.getConfig();
|
||||
}
|
||||
|
||||
@Post('supplement')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: '手动修改加价金额(总部运营成本压力涨价)' })
|
||||
@ApiResponse({ status: HttpStatus.OK, description: '更新成功,返回最新配置' })
|
||||
async updateSupplement(@Body() dto: UpdateSupplementDto) {
|
||||
return this.pricingService.updateSupplement(
|
||||
dto.newSupplement,
|
||||
dto.reason,
|
||||
dto.operatorId || 'admin',
|
||||
);
|
||||
}
|
||||
|
||||
@Put('auto-increase')
|
||||
@ApiOperation({ summary: '设置自动涨价(总部运营成本压力自动涨价)' })
|
||||
@ApiResponse({ status: HttpStatus.OK, description: '设置成功,返回最新配置' })
|
||||
async updateAutoIncrease(@Body() dto: UpdateAutoIncreaseDto) {
|
||||
return this.pricingService.updateAutoIncreaseSettings(
|
||||
dto.enabled,
|
||||
dto.amount,
|
||||
dto.intervalDays,
|
||||
dto.operatorId || 'admin',
|
||||
);
|
||||
}
|
||||
|
||||
@Get('change-log')
|
||||
@ApiOperation({ summary: '获取价格变更审计日志' })
|
||||
@ApiResponse({ status: HttpStatus.OK, description: '分页审计日志' })
|
||||
async getChangeLog(@Query() query: ChangeLogQueryDto) {
|
||||
return this.pricingService.getChangeLog(
|
||||
Number(query.page) || 1,
|
||||
Number(query.pageSize) || 20,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== Public Controller ========================
|
||||
|
||||
/**
|
||||
* 公开 API(供 planting-service 和 mobile-app 调用)
|
||||
* 不需要管理员认证
|
||||
*/
|
||||
@ApiTags('认种定价配置-公开API')
|
||||
@Controller('api/v1/tree-pricing')
|
||||
export class PublicTreePricingController {
|
||||
constructor(
|
||||
private readonly pricingService: TreePricingService,
|
||||
) {}
|
||||
|
||||
@Get('config')
|
||||
@ApiOperation({ summary: '获取当前定价配置(公开接口)' })
|
||||
async getConfig() {
|
||||
return this.pricingService.getConfig();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,252 @@
|
|||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { PrismaService } from '../infrastructure/persistence/prisma/prisma.service';
|
||||
|
||||
/** 基础价常量 */
|
||||
const BASE_PRICE = 15831;
|
||||
const BASE_PORTION_PRICE = 3171; // 预种基础价 = BASE_PRICE 的 1/5 取整
|
||||
|
||||
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;
|
||||
return {
|
||||
basePrice: BASE_PRICE,
|
||||
basePortionPrice: BASE_PORTION_PRICE,
|
||||
currentSupplement: config.currentSupplement,
|
||||
totalPrice,
|
||||
totalPortionPrice: Math.floor(totalPrice / 5),
|
||||
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> {
|
||||
if (newSupplement < 0) {
|
||||
throw new Error('加价金额不能为负数');
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
-- AlterTable: 认种订单新增总部运营成本压力涨价字段
|
||||
-- 每棵树加价金额,涨价部分全额归总部 (S0000000001)
|
||||
-- 默认值 0 表示不涨价,兼容所有历史订单
|
||||
ALTER TABLE "planting_orders" ADD COLUMN "price_supplement" INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
-- AlterTable: 预种订单新增总部运营成本压力涨价字段
|
||||
-- 每份加价金额 = Math.floor(每棵树加价金额 / 5),涨价部分全额归总部 (S0000000001)
|
||||
-- 默认值 0 表示不涨价,兼容所有历史订单
|
||||
ALTER TABLE "pre_planting_orders" ADD COLUMN "price_supplement" INTEGER NOT NULL DEFAULT 0;
|
||||
|
|
@ -19,6 +19,7 @@ model PlantingOrder {
|
|||
// 认种信息
|
||||
treeCount Int @map("tree_count")
|
||||
totalAmount Decimal @map("total_amount") @db.Decimal(20, 8)
|
||||
priceSupplement Int @default(0) @map("price_supplement") // 总部运营成本压力涨价(每棵树加价金额),归总部 (S0000000001)
|
||||
|
||||
// 转让锁定(纯新增,不影响现有逻辑)
|
||||
transferLockedCount Int @default(0) @map("transfer_locked_count")
|
||||
|
|
@ -408,6 +409,7 @@ model PrePlantingOrder {
|
|||
portionCount Int @default(1) @map("portion_count")
|
||||
pricePerPortion Decimal @default(3171) @map("price_per_portion") @db.Decimal(20, 8)
|
||||
totalAmount Decimal @map("total_amount") @db.Decimal(20, 8)
|
||||
priceSupplement Int @default(0) @map("price_supplement") // 总部运营成本压力涨价(每份加价金额),归总部 (S0000000001)
|
||||
|
||||
// 省市选择 (购买时即选择,后续复用)
|
||||
provinceCode String @map("province_code") @db.VarChar(10)
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import {
|
|||
CompensationType,
|
||||
FailureStage,
|
||||
} from '../../infrastructure/persistence/repositories/payment-compensation.repository';
|
||||
import { TreePricingAdminClient } from '../../infrastructure/external/tree-pricing-admin.client';
|
||||
|
||||
export interface CreateOrderResult {
|
||||
orderNo: string;
|
||||
|
|
@ -70,6 +71,7 @@ export class PlantingApplicationService {
|
|||
private readonly walletService: WalletServiceClient,
|
||||
private readonly referralService: ReferralServiceClient,
|
||||
private readonly compensationService: PaymentCompensationService,
|
||||
private readonly treePricingClient: TreePricingAdminClient,
|
||||
) {}
|
||||
|
||||
/**
|
||||
|
|
@ -82,17 +84,21 @@ export class PlantingApplicationService {
|
|||
): Promise<CreateOrderResult> {
|
||||
this.logger.log(`Creating order for user ${accountSequence}, treeCount: ${treeCount}`);
|
||||
|
||||
// 检查余额 - 使用 accountSequence 进行跨服务调用
|
||||
// 获取当前定价配置(含总部运营成本压力涨价)
|
||||
const pricingConfig = await this.treePricingClient.getTreePricingConfig();
|
||||
const priceSupplement = pricingConfig.currentSupplement; // 每棵树加价,归总部 (S0000000001)
|
||||
|
||||
// 检查余额 - 使用动态价格(基础价 + 涨价补充)
|
||||
const balance = await this.walletService.getBalance(accountSequence);
|
||||
const requiredAmount = treeCount * PRICE_PER_TREE;
|
||||
const requiredAmount = treeCount * (PRICE_PER_TREE + priceSupplement);
|
||||
if (balance.available < requiredAmount) {
|
||||
throw new Error(
|
||||
`余额不足: 需要 ${requiredAmount} USDT, 当前可用 ${balance.available} USDT`,
|
||||
);
|
||||
}
|
||||
|
||||
// 创建订单
|
||||
const order = PlantingOrder.create(userId, accountSequence, treeCount);
|
||||
// 创建订单(priceSupplement 快照到订单,确保后续涨价不影响已创建订单)
|
||||
const order = PlantingOrder.create(userId, accountSequence, treeCount, priceSupplement);
|
||||
await this.orderRepository.save(order);
|
||||
|
||||
this.logger.log(`Order created: ${order.orderNo}`);
|
||||
|
|
@ -481,6 +487,9 @@ export class PlantingApplicationService {
|
|||
// 新增:订单信息,供 referral-service 转发给 reward-service
|
||||
orderId: order.orderNo,
|
||||
paidAt: order.paidAt!.toISOString(),
|
||||
// [2026-02-26] 总部运营成本压力涨价(每棵树加价金额),归总部 (S0000000001)
|
||||
priceSupplement: order.priceSupplement,
|
||||
totalAmount: order.totalAmount,
|
||||
},
|
||||
},
|
||||
aggregateId: order.orderNo,
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ export interface PlantingOrderData {
|
|||
accountSequence: string;
|
||||
treeCount: number;
|
||||
totalAmount: number;
|
||||
priceSupplement?: number; // 总部运营成本压力涨价,归总部 (S0000000001)
|
||||
status: PlantingOrderStatus;
|
||||
selectedProvince?: string | null;
|
||||
selectedCity?: string | null;
|
||||
|
|
@ -40,6 +41,7 @@ export class PlantingOrder {
|
|||
private readonly _accountSequence: string;
|
||||
private readonly _treeCount: TreeCount;
|
||||
private readonly _totalAmount: number;
|
||||
private readonly _priceSupplement: number; // 总部运营成本压力涨价
|
||||
private _provinceCitySelection: ProvinceCitySelection | null;
|
||||
private _status: PlantingOrderStatus;
|
||||
private _fundAllocations: FundAllocation[];
|
||||
|
|
@ -61,6 +63,7 @@ export class PlantingOrder {
|
|||
accountSequence: string,
|
||||
treeCount: TreeCount,
|
||||
totalAmount: number,
|
||||
priceSupplement: number = 0,
|
||||
createdAt?: Date,
|
||||
) {
|
||||
this._id = null;
|
||||
|
|
@ -69,6 +72,7 @@ export class PlantingOrder {
|
|||
this._accountSequence = accountSequence;
|
||||
this._treeCount = treeCount;
|
||||
this._totalAmount = totalAmount;
|
||||
this._priceSupplement = priceSupplement;
|
||||
this._status = PlantingOrderStatus.CREATED;
|
||||
this._provinceCitySelection = null;
|
||||
this._fundAllocations = [];
|
||||
|
|
@ -101,6 +105,10 @@ export class PlantingOrder {
|
|||
get totalAmount(): number {
|
||||
return this._totalAmount;
|
||||
}
|
||||
/** 总部运营成本压力涨价(每棵树加价金额),归总部 (S0000000001) */
|
||||
get priceSupplement(): number {
|
||||
return this._priceSupplement;
|
||||
}
|
||||
get status(): PlantingOrderStatus {
|
||||
return this._status;
|
||||
}
|
||||
|
|
@ -143,17 +151,23 @@ export class PlantingOrder {
|
|||
|
||||
/**
|
||||
* 工厂方法:创建认种订单
|
||||
* @param priceSupplement 总部运营成本压力涨价(每棵树加价金额),默认 0 不影响现有逻辑
|
||||
*/
|
||||
static create(userId: bigint, accountSequence: string, treeCount: number): PlantingOrder {
|
||||
static create(
|
||||
userId: bigint,
|
||||
accountSequence: string,
|
||||
treeCount: number,
|
||||
priceSupplement: number = 0,
|
||||
): PlantingOrder {
|
||||
if (treeCount <= 0) {
|
||||
throw new Error('认种数量必须大于0');
|
||||
}
|
||||
|
||||
const orderNo = `PLT${Date.now()}${Math.random().toString(36).substring(2, 8).toUpperCase()}`;
|
||||
const tree = TreeCount.create(treeCount);
|
||||
const totalAmount = treeCount * PRICE_PER_TREE;
|
||||
const totalAmount = treeCount * (PRICE_PER_TREE + priceSupplement);
|
||||
|
||||
const order = new PlantingOrder(orderNo, userId, accountSequence, tree, totalAmount);
|
||||
const order = new PlantingOrder(orderNo, userId, accountSequence, tree, totalAmount, priceSupplement);
|
||||
|
||||
// 发布领域事件
|
||||
order._domainEvents.push(
|
||||
|
|
@ -372,6 +386,7 @@ export class PlantingOrder {
|
|||
data.accountSequence,
|
||||
TreeCount.create(data.treeCount),
|
||||
data.totalAmount,
|
||||
data.priceSupplement || 0,
|
||||
data.createdAt,
|
||||
);
|
||||
|
||||
|
|
@ -412,6 +427,7 @@ export class PlantingOrder {
|
|||
accountSequence: this._accountSequence,
|
||||
treeCount: this._treeCount.value,
|
||||
totalAmount: this._totalAmount,
|
||||
priceSupplement: this._priceSupplement,
|
||||
status: this._status,
|
||||
selectedProvince: this._provinceCitySelection?.provinceCode || null,
|
||||
selectedCity: this._provinceCitySelection?.cityCode || null,
|
||||
|
|
|
|||
|
|
@ -168,9 +168,21 @@ export class FundAllocationDomainService {
|
|||
),
|
||||
);
|
||||
|
||||
// 验证总额
|
||||
// [2026-02-26] 总部运营成本压力涨价:加价部分全额归总部 (S0000000001)
|
||||
if (order.priceSupplement > 0) {
|
||||
allocations.push(
|
||||
new FundAllocation(
|
||||
FundAllocationTargetType.HQ_PRICE_SUPPLEMENT,
|
||||
order.priceSupplement * treeCount,
|
||||
'SYSTEM_HEADQUARTERS', // 总部 S0000000001
|
||||
{ reason: '总部运营成本压力涨价' },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 验证总额(基础 15831 + 涨价补充)
|
||||
const total = allocations.reduce((sum, a) => sum + a.amount, 0);
|
||||
const expected = 15831 * treeCount;
|
||||
const expected = order.totalAmount;
|
||||
if (Math.abs(total - expected) > 0.01) {
|
||||
throw new Error(`资金分配计算错误: 总额 ${total} != ${expected}`);
|
||||
}
|
||||
|
|
@ -325,9 +337,21 @@ export class FundAllocationDomainService {
|
|||
),
|
||||
);
|
||||
|
||||
// 验证总额
|
||||
// [2026-02-26] 总部运营成本压力涨价:加价部分全额归总部 (S0000000001)
|
||||
if (order.priceSupplement > 0) {
|
||||
allocations.push(
|
||||
new FundAllocation(
|
||||
FundAllocationTargetType.HQ_PRICE_SUPPLEMENT,
|
||||
order.priceSupplement * treeCount,
|
||||
'SYSTEM:HEADQUARTERS', // 总部 S0000000001
|
||||
{ reason: '总部运营成本压力涨价' },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 验证总额(基础 15831 + 涨价补充)
|
||||
const total = allocations.reduce((sum, a) => sum + a.amount, 0);
|
||||
const expected = 15831 * treeCount;
|
||||
const expected = order.totalAmount;
|
||||
if (Math.abs(total - expected) > 0.01) {
|
||||
throw new Error(`资金分配计算错误: 总额 ${total} != ${expected}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,10 +9,16 @@ export enum FundAllocationTargetType {
|
|||
CITY_TEAM_RIGHTS = 'CITY_TEAM_RIGHTS', // 288 USDT - 市团队权益
|
||||
COMMUNITY_RIGHTS = 'COMMUNITY_RIGHTS', // 576 USDT - 社区权益
|
||||
RWAD_POOL = 'RWAD_POOL', // 5760 USDT - RWAD底池
|
||||
// [2026-02-26] 新增:总部运营成本压力涨价,加价部分全额归总部 (S0000000001)
|
||||
// 金额动态(由 admin-service TreePricingConfig 配置),不计入 FUND_ALLOCATION_AMOUNTS
|
||||
HQ_PRICE_SUPPLEMENT = 'HQ_PRICE_SUPPLEMENT',
|
||||
}
|
||||
|
||||
// 每棵树的资金分配规则 (总计 15831 USDT)
|
||||
export const FUND_ALLOCATION_AMOUNTS: Record<FundAllocationTargetType, number> =
|
||||
// 基础 10 类分配类型(不含动态加价 HQ_PRICE_SUPPLEMENT)
|
||||
type BaseAllocationTargetType = Exclude<FundAllocationTargetType, FundAllocationTargetType.HQ_PRICE_SUPPLEMENT>;
|
||||
|
||||
// 每棵树的资金分配规则 (总计 15831 USDT,不含涨价补充)
|
||||
export const FUND_ALLOCATION_AMOUNTS: Record<BaseAllocationTargetType, number> =
|
||||
{
|
||||
[FundAllocationTargetType.COST_ACCOUNT]: 2800,
|
||||
[FundAllocationTargetType.OPERATION_ACCOUNT]: 2100,
|
||||
|
|
|
|||
62
backend/services/planting-service/src/infrastructure/external/tree-pricing-admin.client.ts
vendored
Normal file
62
backend/services/planting-service/src/infrastructure/external/tree-pricing-admin.client.ts
vendored
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { HttpService } from '@nestjs/axios';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
|
||||
export interface TreePricingConfig {
|
||||
basePrice: number;
|
||||
basePortionPrice: number;
|
||||
currentSupplement: number;
|
||||
totalPrice: number;
|
||||
totalPortionPrice: number;
|
||||
autoIncreaseEnabled: boolean;
|
||||
autoIncreaseAmount: number;
|
||||
autoIncreaseIntervalDays: number;
|
||||
nextAutoIncreaseAt: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP 客户端:从 admin-service 获取当前树定价配置
|
||||
*
|
||||
* 涨价原因:总部运营成本压力,加价部分全额归总部 (S0000000001)
|
||||
* 遵循 PrePlantingAdminClient 模式
|
||||
*/
|
||||
@Injectable()
|
||||
export class TreePricingAdminClient {
|
||||
private readonly logger = new Logger(TreePricingAdminClient.name);
|
||||
private readonly baseUrl: string;
|
||||
|
||||
constructor(
|
||||
private readonly configService: ConfigService,
|
||||
private readonly httpService: HttpService,
|
||||
) {
|
||||
this.baseUrl =
|
||||
this.configService.get<string>('ADMIN_SERVICE_URL') ||
|
||||
'http://localhost:3010';
|
||||
}
|
||||
|
||||
async getTreePricingConfig(): Promise<TreePricingConfig> {
|
||||
try {
|
||||
const response = await firstValueFrom(
|
||||
this.httpService.get<TreePricingConfig>(
|
||||
`${this.baseUrl}/api/v1/tree-pricing/config`,
|
||||
),
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to get tree pricing config, using default (supplement=0)', error);
|
||||
// 安全降级:加价为 0,等同于原始价格 15831,不影响现有业务
|
||||
return {
|
||||
basePrice: 15831,
|
||||
basePortionPrice: 3171,
|
||||
currentSupplement: 0,
|
||||
totalPrice: 15831,
|
||||
totalPortionPrice: 3171,
|
||||
autoIncreaseEnabled: false,
|
||||
autoIncreaseAmount: 0,
|
||||
autoIncreaseIntervalDays: 0,
|
||||
nextAutoIncreaseAt: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ import { UnitOfWork, UNIT_OF_WORK } from './persistence/unit-of-work';
|
|||
import { WalletServiceClient } from './external/wallet-service.client';
|
||||
import { ReferralServiceClient } from './external/referral-service.client';
|
||||
import { IdentityServiceClient } from './external/identity-service.client';
|
||||
import { TreePricingAdminClient } from './external/tree-pricing-admin.client';
|
||||
import { KafkaModule } from './kafka/kafka.module';
|
||||
import { OutboxPublisherService } from './kafka/outbox-publisher.service';
|
||||
import { EventAckController } from './kafka/event-ack.controller';
|
||||
|
|
@ -85,6 +86,7 @@ import { ContractSigningService } from '../application/services/contract-signing
|
|||
WalletServiceClient,
|
||||
ReferralServiceClient,
|
||||
IdentityServiceClient,
|
||||
TreePricingAdminClient,
|
||||
],
|
||||
exports: [
|
||||
PrismaService,
|
||||
|
|
@ -104,6 +106,7 @@ import { ContractSigningService } from '../application/services/contract-signing
|
|||
WalletServiceClient,
|
||||
ReferralServiceClient,
|
||||
IdentityServiceClient,
|
||||
TreePricingAdminClient,
|
||||
],
|
||||
})
|
||||
export class InfrastructureModule {}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ export class PlantingOrderMapper {
|
|||
accountSequence: prismaOrder.accountSequence,
|
||||
treeCount: prismaOrder.treeCount,
|
||||
totalAmount: Number(prismaOrder.totalAmount),
|
||||
priceSupplement: prismaOrder.priceSupplement ?? 0,
|
||||
status: prismaOrder.status as PlantingOrderStatus,
|
||||
selectedProvince: prismaOrder.selectedProvince,
|
||||
selectedCity: prismaOrder.selectedCity,
|
||||
|
|
@ -81,6 +82,7 @@ export class PlantingOrderMapper {
|
|||
accountSequence: data.accountSequence,
|
||||
treeCount: data.treeCount,
|
||||
totalAmount: new Prisma.Decimal(data.totalAmount),
|
||||
priceSupplement: data.priceSupplement ?? 0,
|
||||
selectedProvince: data.selectedProvince || null,
|
||||
selectedCity: data.selectedCity || null,
|
||||
provinceCitySelectedAt: data.provinceCitySelectedAt || null,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { PrePlantingPositionRepository } from '../../infrastructure/repositories
|
|||
import { PrePlantingMergeRepository } from '../../infrastructure/repositories/pre-planting-merge.repository';
|
||||
import { PrePlantingRewardService } from './pre-planting-reward.service';
|
||||
import { PrePlantingAdminClient } from '../../infrastructure/external/pre-planting-admin.client';
|
||||
import { TreePricingAdminClient } from '../../../infrastructure/external/tree-pricing-admin.client';
|
||||
import { EventPublisherService } from '../../../infrastructure/kafka/event-publisher.service';
|
||||
|
||||
@Injectable()
|
||||
|
|
@ -29,6 +30,7 @@ export class PrePlantingApplicationService {
|
|||
private readonly mergeRepo: PrePlantingMergeRepository,
|
||||
private readonly rewardService: PrePlantingRewardService,
|
||||
private readonly adminClient: PrePlantingAdminClient,
|
||||
private readonly treePricingClient: TreePricingAdminClient,
|
||||
private readonly eventPublisher: EventPublisherService,
|
||||
) {}
|
||||
|
||||
|
|
@ -52,8 +54,13 @@ export class PrePlantingApplicationService {
|
|||
// Step 1: 前置校验
|
||||
await this.validatePurchase(userId, portionCount);
|
||||
|
||||
// 获取当前定价配置(含总部运营成本压力涨价)
|
||||
const pricingConfig = await this.treePricingClient.getTreePricingConfig();
|
||||
// 预种每份加价 = Math.floor(每棵树加价 / 5),涨价部分全额归总部 (S0000000001)
|
||||
const portionSupplement = Math.floor(pricingConfig.currentSupplement / 5);
|
||||
|
||||
const orderNo = this.generateOrderNo();
|
||||
const totalAmount = portionCount * PRE_PLANTING_PRICE_PER_PORTION;
|
||||
const totalAmount = portionCount * (PRE_PLANTING_PRICE_PER_PORTION + portionSupplement);
|
||||
|
||||
// Step 2: 冻结余额
|
||||
await this.walletClient.freezeForPlanting({
|
||||
|
|
@ -71,7 +78,7 @@ export class PrePlantingApplicationService {
|
|||
// Step 3-4: 事务内处理(创建订单 + 更新持仓 + 分配记录 + outbox)
|
||||
// 注意:事务内只做 DB 写入,不做 HTTP 调用
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
// 创建预种订单
|
||||
// 创建预种订单(portionSupplement 快照到订单,确保后续涨价不影响已创建订单)
|
||||
const order = PrePlantingOrder.create(
|
||||
orderNo,
|
||||
userId,
|
||||
|
|
@ -79,6 +86,7 @@ export class PrePlantingApplicationService {
|
|||
portionCount,
|
||||
provinceCode,
|
||||
cityCode,
|
||||
portionSupplement,
|
||||
);
|
||||
|
||||
// 获取或创建持仓
|
||||
|
|
@ -99,7 +107,7 @@ export class PrePlantingApplicationService {
|
|||
await this.orderRepo.save(tx, order);
|
||||
await this.positionRepo.save(tx, position);
|
||||
|
||||
// 分配 10 类权益 — 事务内只持久化记录,返回 allocations 供事务后转账
|
||||
// 分配 10 类权益 + 涨价补充 — 事务内只持久化记录,返回 allocations 供事务后转账
|
||||
rewardAllocations = await this.rewardService.prepareAndPersistRewards(
|
||||
tx,
|
||||
orderNo,
|
||||
|
|
@ -107,6 +115,7 @@ export class PrePlantingApplicationService {
|
|||
provinceCode,
|
||||
cityCode,
|
||||
portionCount,
|
||||
portionSupplement,
|
||||
);
|
||||
|
||||
// Outbox: 购买事件(包装为 { eventName, data } 格式,与现有 planting 事件一致)
|
||||
|
|
|
|||
|
|
@ -38,6 +38,9 @@ export class PrePlantingRewardService {
|
|||
*
|
||||
* 返回 allocations 供事务提交后执行转账(不在事务内做 HTTP 调用)
|
||||
*/
|
||||
/**
|
||||
* @param portionSupplement 总部运营成本压力涨价(每份加价金额),默认 0 不影响现有逻辑
|
||||
*/
|
||||
async prepareAndPersistRewards(
|
||||
tx: Prisma.TransactionClient,
|
||||
orderNo: string,
|
||||
|
|
@ -45,10 +48,11 @@ export class PrePlantingRewardService {
|
|||
provinceCode: string,
|
||||
cityCode: string,
|
||||
portionCount: number,
|
||||
portionSupplement: number = 0,
|
||||
): Promise<RewardAllocation[]> {
|
||||
this.logger.log(
|
||||
`[PRE-PLANTING] Preparing rewards for order ${orderNo}, ` +
|
||||
`${portionCount} portion(s), province=${provinceCode}, city=${cityCode}`,
|
||||
`${portionCount} portion(s), province=${provinceCode}, city=${cityCode}, supplement=${portionSupplement}`,
|
||||
);
|
||||
|
||||
// Step 3: 确定所有分配对象
|
||||
|
|
@ -57,6 +61,7 @@ export class PrePlantingRewardService {
|
|||
provinceCode,
|
||||
cityCode,
|
||||
portionCount,
|
||||
portionSupplement,
|
||||
);
|
||||
|
||||
// Step 4: 在事务内持久化分配记录
|
||||
|
|
@ -106,13 +111,15 @@ export class PrePlantingRewardService {
|
|||
}
|
||||
|
||||
/**
|
||||
* 确定 10 类权益分配对象
|
||||
* 确定 10 类权益分配对象 + 涨价补充(如有)
|
||||
* @param portionSupplement 总部运营成本压力涨价(每份加价金额),默认 0
|
||||
*/
|
||||
private async resolveAllocations(
|
||||
accountSequence: string,
|
||||
provinceCode: string,
|
||||
cityCode: string,
|
||||
portionCount: number,
|
||||
portionSupplement: number = 0,
|
||||
): Promise<RewardAllocation[]> {
|
||||
const allocations: RewardAllocation[] = [];
|
||||
const multiplier = portionCount;
|
||||
|
|
@ -234,6 +241,17 @@ export class PrePlantingRewardService {
|
|||
rewardStatus: PrePlantingRewardStatus.SETTLED,
|
||||
});
|
||||
|
||||
// [2026-02-26] 总部运营成本压力涨价:加价部分全额归总部 (S0000000001)
|
||||
if (portionSupplement > 0) {
|
||||
allocations.push({
|
||||
recipientAccountSequence: SYSTEM_ACCOUNTS.HEADQUARTERS,
|
||||
rightType: PrePlantingRightType.HQ_PRICE_SUPPLEMENT,
|
||||
amount: portionSupplement * multiplier,
|
||||
memo: '预种总部运营成本压力涨价',
|
||||
rewardStatus: PrePlantingRewardStatus.SETTLED,
|
||||
});
|
||||
}
|
||||
|
||||
return allocations;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export interface PrePlantingOrderData {
|
|||
portionCount: number;
|
||||
pricePerPortion: number;
|
||||
totalAmount: number;
|
||||
priceSupplement?: number; // 总部运营成本压力涨价(每份加价金额),归总部 (S0000000001)
|
||||
provinceCode: string;
|
||||
cityCode: string;
|
||||
status: PrePlantingOrderStatus;
|
||||
|
|
@ -28,6 +29,7 @@ export class PrePlantingOrder {
|
|||
private readonly _portionCount: number;
|
||||
private readonly _pricePerPortion: number;
|
||||
private readonly _totalAmount: number;
|
||||
private readonly _priceSupplement: number; // 总部运营成本压力涨价(每份加价金额)
|
||||
private readonly _provinceCode: string;
|
||||
private readonly _cityCode: string;
|
||||
private _status: PrePlantingOrderStatus;
|
||||
|
|
@ -45,6 +47,7 @@ export class PrePlantingOrder {
|
|||
portionCount: number,
|
||||
provinceCode: string,
|
||||
cityCode: string,
|
||||
priceSupplement: number = 0,
|
||||
createdAt?: Date,
|
||||
) {
|
||||
this._id = null;
|
||||
|
|
@ -52,8 +55,9 @@ export class PrePlantingOrder {
|
|||
this._userId = userId;
|
||||
this._accountSequence = accountSequence;
|
||||
this._portionCount = portionCount;
|
||||
this._pricePerPortion = PRE_PLANTING_PRICE_PER_PORTION;
|
||||
this._totalAmount = portionCount * PRE_PLANTING_PRICE_PER_PORTION;
|
||||
this._pricePerPortion = PRE_PLANTING_PRICE_PER_PORTION + priceSupplement;
|
||||
this._totalAmount = portionCount * (PRE_PLANTING_PRICE_PER_PORTION + priceSupplement);
|
||||
this._priceSupplement = priceSupplement;
|
||||
this._provinceCode = provinceCode;
|
||||
this._cityCode = cityCode;
|
||||
this._status = PrePlantingOrderStatus.CREATED;
|
||||
|
|
@ -66,6 +70,9 @@ export class PrePlantingOrder {
|
|||
/**
|
||||
* 创建新的预种订单
|
||||
*/
|
||||
/**
|
||||
* @param priceSupplement 总部运营成本压力涨价(每份加价金额),默认 0 不影响现有逻辑
|
||||
*/
|
||||
static create(
|
||||
orderNo: string,
|
||||
userId: bigint,
|
||||
|
|
@ -73,6 +80,7 @@ export class PrePlantingOrder {
|
|||
portionCount: number,
|
||||
provinceCode: string,
|
||||
cityCode: string,
|
||||
priceSupplement: number = 0,
|
||||
): PrePlantingOrder {
|
||||
if (portionCount < 1) {
|
||||
throw new Error('购买份数必须大于 0');
|
||||
|
|
@ -84,6 +92,7 @@ export class PrePlantingOrder {
|
|||
portionCount,
|
||||
provinceCode,
|
||||
cityCode,
|
||||
priceSupplement,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -98,6 +107,7 @@ export class PrePlantingOrder {
|
|||
data.portionCount,
|
||||
data.provinceCode,
|
||||
data.cityCode,
|
||||
data.priceSupplement || 0,
|
||||
data.createdAt,
|
||||
);
|
||||
order._id = data.id || null;
|
||||
|
|
@ -159,6 +169,8 @@ export class PrePlantingOrder {
|
|||
get portionCount(): number { return this._portionCount; }
|
||||
get pricePerPortion(): number { return this._pricePerPortion; }
|
||||
get totalAmount(): number { return this._totalAmount; }
|
||||
/** 总部运营成本压力涨价(每份加价金额),归总部 (S0000000001) */
|
||||
get priceSupplement(): number { return this._priceSupplement; }
|
||||
get provinceCode(): string { return this._provinceCode; }
|
||||
get cityCode(): string { return this._cityCode; }
|
||||
get status(): PrePlantingOrderStatus { return this._status; }
|
||||
|
|
@ -179,6 +191,7 @@ export class PrePlantingOrder {
|
|||
portionCount: this._portionCount,
|
||||
pricePerPortion: this._pricePerPortion,
|
||||
totalAmount: this._totalAmount,
|
||||
priceSupplement: this._priceSupplement,
|
||||
provinceCode: this._provinceCode,
|
||||
cityCode: this._cityCode,
|
||||
status: this._status,
|
||||
|
|
|
|||
|
|
@ -36,6 +36,9 @@ export enum PrePlantingRightType {
|
|||
CITY_AREA_RIGHT = 'CITY_AREA_RIGHT',
|
||||
CITY_TEAM_RIGHT = 'CITY_TEAM_RIGHT',
|
||||
COMMUNITY_RIGHT = 'COMMUNITY_RIGHT',
|
||||
// [2026-02-26] 新增:总部运营成本压力涨价,加价部分全额归总部 (S0000000001)
|
||||
// 金额动态(由 admin-service TreePricingConfig 配置),不计入 PRE_PLANTING_RIGHT_AMOUNTS
|
||||
HQ_PRICE_SUPPLEMENT = 'HQ_PRICE_SUPPLEMENT',
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ export class PrePlantingOrderRepository {
|
|||
portionCount: data.portionCount,
|
||||
pricePerPortion: new Prisma.Decimal(data.pricePerPortion),
|
||||
totalAmount: new Prisma.Decimal(data.totalAmount),
|
||||
priceSupplement: data.priceSupplement ?? 0,
|
||||
provinceCode: data.provinceCode,
|
||||
cityCode: data.cityCode,
|
||||
status: data.status,
|
||||
|
|
@ -91,6 +92,7 @@ export class PrePlantingOrderRepository {
|
|||
portionCount: record.portionCount,
|
||||
pricePerPortion: Number(record.pricePerPortion),
|
||||
totalAmount: Number(record.totalAmount),
|
||||
priceSupplement: record.priceSupplement ?? 0,
|
||||
provinceCode: record.provinceCode,
|
||||
cityCode: record.cityCode,
|
||||
status: record.status as PrePlantingOrderStatus,
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ export class RewardApplicationService {
|
|||
treeCount: number;
|
||||
provinceCode: string;
|
||||
cityCode: string;
|
||||
priceSupplement?: number; // [2026-02-26] 总部运营成本压力涨价(每棵树加价金额),归总部 (S0000000001)
|
||||
}): Promise<void> {
|
||||
this.logger.log(`Distributing rewards for order ${params.sourceOrderNo}, accountSequence=${params.sourceAccountSequence}`);
|
||||
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@ export class RewardCalculationService {
|
|||
treeCount: number;
|
||||
provinceCode: string;
|
||||
cityCode: string;
|
||||
priceSupplement?: number; // [2026-02-26] 总部运营成本压力涨价(每棵树加价金额),归总部 (S0000000001)
|
||||
}): Promise<RewardLedgerEntry[]> {
|
||||
this.logger.log(
|
||||
`[calculateRewards] START orderNo=${params.sourceOrderNo}, userId=${params.sourceUserId}, ` +
|
||||
|
|
@ -204,6 +205,18 @@ export class RewardCalculationService {
|
|||
);
|
||||
rewards.push(...communityRewards);
|
||||
|
||||
// [2026-02-26] 11. 总部运营成本压力涨价(动态金额,归总部 S0000000001)
|
||||
if (params.priceSupplement && params.priceSupplement > 0) {
|
||||
const supplementReward = this.calculateHqPriceSupplement(
|
||||
params.sourceOrderNo,
|
||||
params.sourceUserId,
|
||||
params.sourceAccountSequence,
|
||||
params.treeCount,
|
||||
params.priceSupplement,
|
||||
);
|
||||
rewards.push(supplementReward);
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`[calculateRewards] DONE orderNo=${params.sourceOrderNo}, totalRewards=${rewards.length}`,
|
||||
);
|
||||
|
|
@ -339,6 +352,38 @@ export class RewardCalculationService {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* [2026-02-26] 计算总部运营成本压力涨价
|
||||
* 动态金额,全额归总部 (S0000000001)
|
||||
* 不影响基础 10 类分配,纯增量
|
||||
*/
|
||||
private calculateHqPriceSupplement(
|
||||
sourceOrderNo: string,
|
||||
sourceUserId: bigint,
|
||||
sourceAccountSequence: string | undefined,
|
||||
treeCount: number,
|
||||
priceSupplement: number,
|
||||
): RewardLedgerEntry {
|
||||
const usdtAmount = Money.USDT(priceSupplement * treeCount);
|
||||
const hashpower = Hashpower.fromTreeCount(treeCount, 0); // 涨价部分无算力
|
||||
|
||||
const rewardSource = RewardSource.create(
|
||||
RightType.HQ_PRICE_SUPPLEMENT,
|
||||
sourceOrderNo,
|
||||
sourceUserId,
|
||||
sourceAccountSequence,
|
||||
);
|
||||
|
||||
return RewardLedgerEntry.createSettleable({
|
||||
userId: HEADQUARTERS_COMMUNITY_USER_ID,
|
||||
accountSequence: 'S0000000001',
|
||||
rewardSource,
|
||||
usdtAmount,
|
||||
hashpowerAmount: hashpower,
|
||||
memo: `总部运营成本压力涨价:来自用户${sourceAccountSequence || sourceUserId}的认种,${treeCount}棵树,每棵加价${priceSupplement}U`,
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 用户权益计算方法
|
||||
// ============================================
|
||||
|
|
@ -727,6 +772,7 @@ export class RewardCalculationService {
|
|||
treeCount: number;
|
||||
provinceCode: string;
|
||||
cityCode: string;
|
||||
priceSupplement?: number; // [2026-02-26] 总部运营成本压力涨价
|
||||
}): Promise<RewardLedgerEntry[]> {
|
||||
this.logger.log(
|
||||
`[calculateRewardsForExpiredContract] START orderNo=${params.sourceOrderNo}, userId=${params.sourceUserId}, ` +
|
||||
|
|
@ -827,6 +873,17 @@ export class RewardCalculationService {
|
|||
params.treeCount,
|
||||
));
|
||||
|
||||
// [2026-02-26] 11. 总部运营成本压力涨价(动态金额,归总部 S0000000001)
|
||||
if (params.priceSupplement && params.priceSupplement > 0) {
|
||||
rewards.push(this.calculateHqPriceSupplement(
|
||||
params.sourceOrderNo,
|
||||
params.sourceUserId,
|
||||
params.sourceAccountSequence,
|
||||
params.treeCount,
|
||||
params.priceSupplement,
|
||||
));
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`[calculateRewardsForExpiredContract] DONE orderNo=${params.sourceOrderNo}, totalRewards=${rewards.length}`,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -12,10 +12,17 @@ export enum RightType {
|
|||
CITY_AREA_RIGHT = 'CITY_AREA_RIGHT', // 市区域权益 252U + 2%算力
|
||||
CITY_TEAM_RIGHT = 'CITY_TEAM_RIGHT', // 市团队权益 288U
|
||||
COMMUNITY_RIGHT = 'COMMUNITY_RIGHT', // 社区权益 576U
|
||||
|
||||
// [2026-02-26] 新增:总部运营成本压力涨价,加价部分全额归总部 (S0000000001)
|
||||
// 金额动态(由 admin-service TreePricingConfig 配置),不计入基础 15831
|
||||
HQ_PRICE_SUPPLEMENT = 'HQ_PRICE_SUPPLEMENT',
|
||||
}
|
||||
|
||||
// 权益金额配置
|
||||
export const RIGHT_AMOUNTS: Record<RightType, { usdt: number; hashpowerPercent: number }> = {
|
||||
// 基础 10 类分配类型(不含动态加价 HQ_PRICE_SUPPLEMENT)
|
||||
type BaseRightType = Exclude<RightType, RightType.HQ_PRICE_SUPPLEMENT>;
|
||||
|
||||
// 权益金额配置(基础 10 类,不含动态加价)
|
||||
export const RIGHT_AMOUNTS: Record<BaseRightType, { usdt: number; hashpowerPercent: number }> = {
|
||||
// 系统费用类
|
||||
[RightType.COST_FEE]: { usdt: 2880, hashpowerPercent: 0 },
|
||||
[RightType.OPERATION_FEE]: { usdt: 2100, hashpowerPercent: 0 },
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@ interface PlantingOrderPaidEvent {
|
|||
provinceCode: string;
|
||||
cityCode: string;
|
||||
paidAt: string;
|
||||
// [2026-02-26] 总部运营成本压力涨价(每棵树加价金额),归总部 (S0000000001)
|
||||
priceSupplement?: number;
|
||||
totalAmount?: number;
|
||||
};
|
||||
// 兼容旧格式
|
||||
orderId?: string;
|
||||
|
|
@ -94,6 +97,7 @@ export class EventConsumerController {
|
|||
treeCount: eventData.treeCount,
|
||||
provinceCode: eventData.provinceCode,
|
||||
cityCode: eventData.cityCode,
|
||||
priceSupplement: eventData.priceSupplement || 0,
|
||||
});
|
||||
|
||||
// 2. 检查该用户是否有待领取奖励需要转为可结算
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import styles from './settings.module.scss';
|
|||
import { systemConfigService, type DisplaySettings } from '@/services/systemConfigService';
|
||||
import { appAssetService, type AppAsset, type AppAssetType } from '@/services/appAssetService';
|
||||
import { customerServiceContactService, type CustomerServiceContact, type ContactType } from '@/services/customerServiceContactService';
|
||||
import { treePricingService, type TreePricingConfig, type TreePriceChangeLogItem } from '@/services/treePricingService';
|
||||
|
||||
/**
|
||||
* 后台账号数据接口
|
||||
|
|
@ -103,6 +104,19 @@ export default function SettingsPage() {
|
|||
const [editingContact, setEditingContact] = useState<Partial<CustomerServiceContact> | null>(null);
|
||||
const [contactSaving, setContactSaving] = useState(false);
|
||||
|
||||
// 认种定价设置(总部运营成本压力涨价)
|
||||
const [pricingConfig, setPricingConfig] = useState<TreePricingConfig | null>(null);
|
||||
const [pricingLoading, setPricingLoading] = useState(false);
|
||||
const [pricingSaving, setPricingSaving] = useState(false);
|
||||
const [newSupplement, setNewSupplement] = useState('');
|
||||
const [supplementReason, setSupplementReason] = useState('');
|
||||
const [autoIncreaseEnabled, setAutoIncreaseEnabled] = useState(false);
|
||||
const [autoIncreaseAmount, setAutoIncreaseAmount] = useState('');
|
||||
const [autoIncreaseIntervalDays, setAutoIncreaseIntervalDays] = useState('');
|
||||
const [changeLog, setChangeLog] = useState<TreePriceChangeLogItem[]>([]);
|
||||
const [changeLogTotal, setChangeLogTotal] = useState(0);
|
||||
const [changeLogPage, setChangeLogPage] = useState(1);
|
||||
|
||||
// 后台账号与安全
|
||||
const [approvalCount, setApprovalCount] = useState('3');
|
||||
const [sensitiveOperations, setSensitiveOperations] = useState(['修改结算参数', '删除用户']);
|
||||
|
|
@ -282,12 +296,102 @@ export default function SettingsPage() {
|
|||
}
|
||||
}, [loadContacts]);
|
||||
|
||||
// 加载认种定价配置
|
||||
const loadPricingConfig = useCallback(async () => {
|
||||
setPricingLoading(true);
|
||||
try {
|
||||
const config = await treePricingService.getConfig();
|
||||
setPricingConfig(config);
|
||||
setNewSupplement(String(config.currentSupplement));
|
||||
setAutoIncreaseEnabled(config.autoIncreaseEnabled);
|
||||
setAutoIncreaseAmount(String(config.autoIncreaseAmount));
|
||||
setAutoIncreaseIntervalDays(String(config.autoIncreaseIntervalDays));
|
||||
// 加载变更日志
|
||||
const log = await treePricingService.getChangeLog(1, 10);
|
||||
setChangeLog(log.items);
|
||||
setChangeLogTotal(log.total);
|
||||
setChangeLogPage(1);
|
||||
} catch (error) {
|
||||
console.error('Failed to load pricing config:', error);
|
||||
} finally {
|
||||
setPricingLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 手动修改加价金额
|
||||
const handleSupplementUpdate = useCallback(async () => {
|
||||
const amount = parseInt(newSupplement, 10);
|
||||
if (isNaN(amount) || amount < 0) {
|
||||
alert('请输入有效的加价金额(非负整数)');
|
||||
return;
|
||||
}
|
||||
if (!supplementReason.trim()) {
|
||||
alert('请填写变更原因');
|
||||
return;
|
||||
}
|
||||
if (!confirm(`确认将加价金额修改为 ${amount} USDT?\n正式认种总价将变为 ${15831 + amount} USDT/棵\n预种总价将变为 ${Math.floor((15831 + amount) / 5)} USDT/份`)) return;
|
||||
setPricingSaving(true);
|
||||
try {
|
||||
await treePricingService.updateSupplement(amount, supplementReason.trim());
|
||||
setSupplementReason('');
|
||||
await loadPricingConfig();
|
||||
alert('加价金额修改成功');
|
||||
} catch (error) {
|
||||
console.error('Failed to update supplement:', error);
|
||||
alert('修改失败,请重试');
|
||||
} finally {
|
||||
setPricingSaving(false);
|
||||
}
|
||||
}, [newSupplement, supplementReason, loadPricingConfig]);
|
||||
|
||||
// 更新自动涨价设置
|
||||
const handleAutoIncreaseUpdate = useCallback(async () => {
|
||||
const amount = parseInt(autoIncreaseAmount, 10);
|
||||
const days = parseInt(autoIncreaseIntervalDays, 10);
|
||||
if (autoIncreaseEnabled && (isNaN(amount) || amount <= 0)) {
|
||||
alert('请输入有效的自动涨价金额');
|
||||
return;
|
||||
}
|
||||
if (autoIncreaseEnabled && (isNaN(days) || days < 1)) {
|
||||
alert('请输入有效的涨价间隔天数(至少1天)');
|
||||
return;
|
||||
}
|
||||
setPricingSaving(true);
|
||||
try {
|
||||
await treePricingService.updateAutoIncrease({
|
||||
enabled: autoIncreaseEnabled,
|
||||
amount: autoIncreaseEnabled ? amount : undefined,
|
||||
intervalDays: autoIncreaseEnabled ? days : undefined,
|
||||
});
|
||||
await loadPricingConfig();
|
||||
alert('自动涨价设置保存成功');
|
||||
} catch (error) {
|
||||
console.error('Failed to update auto-increase:', error);
|
||||
alert('保存失败,请重试');
|
||||
} finally {
|
||||
setPricingSaving(false);
|
||||
}
|
||||
}, [autoIncreaseEnabled, autoIncreaseAmount, autoIncreaseIntervalDays, loadPricingConfig]);
|
||||
|
||||
// 变更日志翻页
|
||||
const handleChangeLogPageChange = useCallback(async (page: number) => {
|
||||
try {
|
||||
const log = await treePricingService.getChangeLog(page, 10);
|
||||
setChangeLog(log.items);
|
||||
setChangeLogTotal(log.total);
|
||||
setChangeLogPage(page);
|
||||
} catch (error) {
|
||||
console.error('Failed to load change log:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 组件挂载时加载展示设置
|
||||
useEffect(() => {
|
||||
loadDisplaySettings();
|
||||
loadAppAssets();
|
||||
loadContacts();
|
||||
}, [loadDisplaySettings, loadAppAssets, loadContacts]);
|
||||
loadPricingConfig();
|
||||
}, [loadDisplaySettings, loadAppAssets, loadContacts, loadPricingConfig]);
|
||||
|
||||
// 切换货币选择
|
||||
const toggleCurrency = (currency: string) => {
|
||||
|
|
@ -1037,6 +1141,193 @@ export default function SettingsPage() {
|
|||
</div>
|
||||
</section>
|
||||
|
||||
{/* [2026-02-26] 认种定价设置(总部运营成本压力涨价) */}
|
||||
<section className={styles.settings__section}>
|
||||
<div className={styles.settings__sectionHeader}>
|
||||
<h2 className={styles.settings__sectionTitle}>认种定价设置</h2>
|
||||
{pricingLoading && <span className={styles.settings__hint}>加载中...</span>}
|
||||
</div>
|
||||
<div className={styles.settings__content}>
|
||||
{/* 当前价格展示 */}
|
||||
{pricingConfig && (
|
||||
<div className={styles.settings__quotaCard}>
|
||||
<div className={styles.settings__quotaHeader}>
|
||||
<span className={styles.settings__quotaTitle}>当前定价</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px', padding: '8px 0' }}>
|
||||
<div style={{ display: 'flex', gap: '24px', flexWrap: 'wrap' }}>
|
||||
<span>基础价: <strong>{pricingConfig.basePrice.toLocaleString()}</strong> USDT/棵</span>
|
||||
<span>加价: <strong>{pricingConfig.currentSupplement.toLocaleString()}</strong> USDT</span>
|
||||
<span>正式认种总价: <strong style={{ color: '#e74c3c' }}>{pricingConfig.totalPrice.toLocaleString()}</strong> USDT/棵</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '24px', flexWrap: 'wrap' }}>
|
||||
<span>预种基础价: <strong>{pricingConfig.basePortionPrice.toLocaleString()}</strong> USDT/份</span>
|
||||
<span>预种总价: <strong style={{ color: '#e74c3c' }}>{pricingConfig.totalPortionPrice.toLocaleString()}</strong> USDT/份</span>
|
||||
</div>
|
||||
{pricingConfig.autoIncreaseEnabled && pricingConfig.nextAutoIncreaseAt && (
|
||||
<div style={{ marginTop: '4px', color: '#8e44ad' }}>
|
||||
下次自动涨价: {new Date(pricingConfig.nextAutoIncreaseAt).toLocaleString('zh-CN')}
|
||||
(+{pricingConfig.autoIncreaseAmount} USDT → 届时总价 {(pricingConfig.totalPrice + pricingConfig.autoIncreaseAmount).toLocaleString()} USDT/棵)
|
||||
</div>
|
||||
)}
|
||||
<span className={styles.settings__hint}>
|
||||
最后更新: {new Date(pricingConfig.updatedAt).toLocaleString('zh-CN')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 手动调价 */}
|
||||
<div className={styles.settings__quotaCard}>
|
||||
<div className={styles.settings__quotaHeader}>
|
||||
<span className={styles.settings__quotaTitle}>手动调价(总部运营成本压力涨价)</span>
|
||||
</div>
|
||||
<div className={styles.settings__quotaInputRow}>
|
||||
<span className={styles.settings__quotaText}>加价金额</span>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
className={cn(styles.settings__input, styles['settings__input--medium'])}
|
||||
placeholder="加价金额"
|
||||
value={newSupplement}
|
||||
onChange={(e) => setNewSupplement(e.target.value)}
|
||||
/>
|
||||
<span className={styles.settings__inputUnit}>USDT</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginTop: '8px' }}>
|
||||
<span className={styles.settings__quotaText}>变更原因</span>
|
||||
<input
|
||||
type="text"
|
||||
className={styles.settings__input}
|
||||
placeholder="请输入变更原因(必填)"
|
||||
value={supplementReason}
|
||||
onChange={(e) => setSupplementReason(e.target.value)}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<button
|
||||
className={styles.settings__saveBtn}
|
||||
onClick={handleSupplementUpdate}
|
||||
disabled={pricingSaving}
|
||||
style={{ minWidth: '100px' }}
|
||||
>
|
||||
{pricingSaving ? '保存中...' : '确认修改'}
|
||||
</button>
|
||||
</div>
|
||||
<span className={styles.settings__hint}>
|
||||
加价部分全额归总部 (S0000000001),不影响现有 10 类分配金额。
|
||||
{newSupplement && !isNaN(parseInt(newSupplement, 10)) && parseInt(newSupplement, 10) >= 0 && (
|
||||
<> 修改后: 正式认种 {(15831 + parseInt(newSupplement, 10)).toLocaleString()} USDT/棵,预种 {Math.floor((15831 + parseInt(newSupplement, 10)) / 5).toLocaleString()} USDT/份</>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 自动涨价设置 */}
|
||||
<div className={styles.settings__quotaCard}>
|
||||
<div className={styles.settings__quotaHeader}>
|
||||
<span className={styles.settings__quotaTitle}>自动涨价</span>
|
||||
<Toggle checked={autoIncreaseEnabled} onChange={setAutoIncreaseEnabled} />
|
||||
</div>
|
||||
{autoIncreaseEnabled && (
|
||||
<div className={styles.settings__quotaInputRow}>
|
||||
<span className={styles.settings__quotaText}>每</span>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
className={cn(styles.settings__input, styles['settings__input--small'])}
|
||||
placeholder="天数"
|
||||
value={autoIncreaseIntervalDays}
|
||||
onChange={(e) => setAutoIncreaseIntervalDays(e.target.value)}
|
||||
/>
|
||||
<span className={styles.settings__quotaText}>天,自动加价</span>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
className={cn(styles.settings__input, styles['settings__input--small'])}
|
||||
placeholder="金额"
|
||||
value={autoIncreaseAmount}
|
||||
onChange={(e) => setAutoIncreaseAmount(e.target.value)}
|
||||
/>
|
||||
<span className={styles.settings__quotaText}>USDT</span>
|
||||
</div>
|
||||
)}
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
<button
|
||||
className={styles.settings__saveBtn}
|
||||
onClick={handleAutoIncreaseUpdate}
|
||||
disabled={pricingSaving}
|
||||
style={{ minWidth: '140px' }}
|
||||
>
|
||||
{pricingSaving ? '保存中...' : '保存自动涨价设置'}
|
||||
</button>
|
||||
</div>
|
||||
<span className={styles.settings__hint}>
|
||||
启用后系统将每隔指定天数自动执行一次涨价,涨价部分全额归总部。
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 变更历史 */}
|
||||
{changeLog.length > 0 && (
|
||||
<div style={{ marginTop: '16px' }}>
|
||||
<h3 className={styles.settings__logTitle}>定价变更历史</h3>
|
||||
<div className={styles.settings__logTable}>
|
||||
<div className={styles.settings__logTableInner}>
|
||||
<div className={styles.settings__logHead}>
|
||||
<div className={cn(styles.settings__logHeadCell, styles['settings__logHeadCell--time'])}>时间</div>
|
||||
<div className={cn(styles.settings__logHeadCell, styles['settings__logHeadCell--type'])}>类型</div>
|
||||
<div className={cn(styles.settings__logHeadCell, styles['settings__logHeadCell--desc'])}>变更前</div>
|
||||
<div className={cn(styles.settings__logHeadCell, styles['settings__logHeadCell--desc'])}>变更后</div>
|
||||
<div className={cn(styles.settings__logHeadCell, styles['settings__logHeadCell--type'])}>变更量</div>
|
||||
<div className={cn(styles.settings__logHeadCell, styles['settings__logHeadCell--desc'])}>原因</div>
|
||||
<div className={cn(styles.settings__logHeadCell, styles['settings__logHeadCell--account'])}>操作人</div>
|
||||
</div>
|
||||
<div className={styles.settings__logBody}>
|
||||
{changeLog.map((item) => (
|
||||
<div key={item.id} className={styles.settings__logRow}>
|
||||
<div className={cn(styles.settings__logCell, styles['settings__logCell--time'])}>{new Date(item.createdAt).toLocaleString('zh-CN')}</div>
|
||||
<div className={cn(styles.settings__logCell, styles['settings__logCell--type'])}>
|
||||
<span className={cn(
|
||||
styles.settings__logResult,
|
||||
item.changeType === 'AUTO' ? styles['settings__logResult--pending'] : styles['settings__logResult--pass']
|
||||
)}>
|
||||
{item.changeType === 'AUTO' ? '自动' : '手动'}
|
||||
</span>
|
||||
</div>
|
||||
<div className={cn(styles.settings__logCell, styles['settings__logCell--desc'])}>{item.previousSupplement}</div>
|
||||
<div className={cn(styles.settings__logCell, styles['settings__logCell--desc'])}>{item.newSupplement}</div>
|
||||
<div className={cn(styles.settings__logCell, styles['settings__logCell--type'])}>{item.changeAmount > 0 ? '+' : ''}{item.changeAmount}</div>
|
||||
<div className={cn(styles.settings__logCell, styles['settings__logCell--desc'])}>{item.reason || '-'}</div>
|
||||
<div className={cn(styles.settings__logCell, styles['settings__logCell--account'])}>{item.operatorId || 'SYSTEM'}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{changeLogTotal > 10 && (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', gap: '8px', marginTop: '8px' }}>
|
||||
<button
|
||||
className={styles.settings__resetBtn}
|
||||
disabled={changeLogPage <= 1}
|
||||
onClick={() => handleChangeLogPageChange(changeLogPage - 1)}
|
||||
>
|
||||
上一页
|
||||
</button>
|
||||
<span className={styles.settings__hint}>
|
||||
第 {changeLogPage} 页 / 共 {Math.ceil(changeLogTotal / 10)} 页
|
||||
</span>
|
||||
<button
|
||||
className={styles.settings__resetBtn}
|
||||
disabled={changeLogPage >= Math.ceil(changeLogTotal / 10)}
|
||||
onClick={() => handleChangeLogPageChange(changeLogPage + 1)}
|
||||
>
|
||||
下一页
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 页面底部操作按钮 */}
|
||||
<footer className={styles.settings__footer}>
|
||||
<button className={styles.settings__cancelBtn}>取消修改</button>
|
||||
|
|
|
|||
|
|
@ -312,4 +312,12 @@ export const API_ENDPOINTS = {
|
|||
STATS: '/v1/admin/transfers/stats',
|
||||
FORCE_CANCEL: (no: string) => `/v1/admin/transfers/${no}/force-cancel`,
|
||||
},
|
||||
// [2026-02-26] 新增:认种树定价配置 (admin-service / TreePricingModule)
|
||||
// 总部运营成本压力涨价管理:手动调价 + 自动涨价 + 审计日志
|
||||
TREE_PRICING: {
|
||||
CONFIG: '/v1/admin/tree-pricing/config',
|
||||
SUPPLEMENT: '/v1/admin/tree-pricing/supplement',
|
||||
AUTO_INCREASE: '/v1/admin/tree-pricing/auto-increase',
|
||||
CHANGE_LOG: '/v1/admin/tree-pricing/change-log',
|
||||
},
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
/**
|
||||
* 认种树定价配置服务
|
||||
* 总部运营成本压力涨价管理:手动调价 + 自动涨价 + 审计日志
|
||||
* [2026-02-26] 新增
|
||||
*/
|
||||
|
||||
import apiClient from '@/infrastructure/api/client';
|
||||
import { API_ENDPOINTS } from '@/infrastructure/api/endpoints';
|
||||
|
||||
/** 定价配置响应 */
|
||||
export interface TreePricingConfig {
|
||||
basePrice: number;
|
||||
basePortionPrice: number;
|
||||
currentSupplement: number;
|
||||
totalPrice: number;
|
||||
totalPortionPrice: number;
|
||||
autoIncreaseEnabled: boolean;
|
||||
autoIncreaseAmount: number;
|
||||
autoIncreaseIntervalDays: number;
|
||||
lastAutoIncreaseAt: string | null;
|
||||
nextAutoIncreaseAt: string | null;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
/** 变更日志项 */
|
||||
export interface TreePriceChangeLogItem {
|
||||
id: string;
|
||||
changeType: 'MANUAL' | 'AUTO';
|
||||
previousSupplement: number;
|
||||
newSupplement: number;
|
||||
changeAmount: number;
|
||||
reason: string | null;
|
||||
operatorId: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
/** 变更日志分页响应 */
|
||||
export interface TreePriceChangeLogResponse {
|
||||
items: TreePriceChangeLogItem[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 认种树定价配置服务
|
||||
*/
|
||||
export const treePricingService = {
|
||||
/** 获取当前定价配置 */
|
||||
async getConfig(): Promise<TreePricingConfig> {
|
||||
return apiClient.get(API_ENDPOINTS.TREE_PRICING.CONFIG);
|
||||
},
|
||||
|
||||
/** 手动修改加价金额(总部运营成本压力涨价) */
|
||||
async updateSupplement(newSupplement: number, reason: string): Promise<TreePricingConfig> {
|
||||
return apiClient.post(API_ENDPOINTS.TREE_PRICING.SUPPLEMENT, { newSupplement, reason });
|
||||
},
|
||||
|
||||
/** 更新自动涨价设置 */
|
||||
async updateAutoIncrease(params: {
|
||||
enabled: boolean;
|
||||
amount?: number;
|
||||
intervalDays?: number;
|
||||
}): Promise<TreePricingConfig> {
|
||||
return apiClient.put(API_ENDPOINTS.TREE_PRICING.AUTO_INCREASE, params);
|
||||
},
|
||||
|
||||
/** 获取变更审计日志 */
|
||||
async getChangeLog(page: number = 1, pageSize: number = 20): Promise<TreePriceChangeLogResponse> {
|
||||
return apiClient.get(API_ENDPOINTS.TREE_PRICING.CHANGE_LOG, {
|
||||
params: { page, pageSize },
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
@ -16,6 +16,8 @@ import '../services/transfer_service.dart';
|
|||
import '../services/reward_service.dart';
|
||||
import '../services/notification_service.dart';
|
||||
import '../services/system_config_service.dart';
|
||||
// [2026-02-26] 新增:认种树定价配置服务(总部运营成本压力涨价,动态价格)
|
||||
import '../services/tree_pricing_service.dart';
|
||||
import '../services/app_asset_service.dart';
|
||||
import '../services/customer_service_contact_service.dart';
|
||||
import '../services/leaderboard_service.dart';
|
||||
|
|
@ -131,6 +133,13 @@ final systemConfigServiceProvider = Provider<SystemConfigService>((ref) {
|
|||
return SystemConfigService(apiClient: apiClient);
|
||||
});
|
||||
|
||||
// [2026-02-26] Tree Pricing Service Provider (调用 admin-service 公开接口)
|
||||
// 认种树定价:动态获取当前价格(basePrice + supplement),5 分钟缓存
|
||||
final treePricingServiceProvider = Provider<TreePricingService>((ref) {
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
return TreePricingService(apiClient: apiClient);
|
||||
});
|
||||
|
||||
// App Asset Service Provider (调用 admin-service)
|
||||
final appAssetServiceProvider = Provider<AppAssetService>((ref) {
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,138 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import '../network/api_client.dart';
|
||||
|
||||
/// [2026-02-26] 认种树定价配置
|
||||
///
|
||||
/// 总部运营成本压力涨价:基础价不变,加价部分全额归总部。
|
||||
/// - 正式认种总价 = basePrice + currentSupplement
|
||||
/// - 预种总价 = totalPrice / 5(取整)
|
||||
class TreePricingConfig {
|
||||
/// 正式认种基础价(固定 15831)
|
||||
final int basePrice;
|
||||
|
||||
/// 预种基础价(固定 3171)
|
||||
final int basePortionPrice;
|
||||
|
||||
/// 当前加价金额
|
||||
final int currentSupplement;
|
||||
|
||||
/// 正式认种总价 = basePrice + currentSupplement
|
||||
final int totalPrice;
|
||||
|
||||
/// 预种总价 = totalPrice / 5
|
||||
final int totalPortionPrice;
|
||||
|
||||
/// 是否启用自动涨价
|
||||
final bool autoIncreaseEnabled;
|
||||
|
||||
/// 自动涨价金额
|
||||
final int autoIncreaseAmount;
|
||||
|
||||
/// 自动涨价间隔天数
|
||||
final int autoIncreaseIntervalDays;
|
||||
|
||||
/// 下次自动涨价时间
|
||||
final DateTime? nextAutoIncreaseAt;
|
||||
|
||||
TreePricingConfig({
|
||||
required this.basePrice,
|
||||
required this.basePortionPrice,
|
||||
required this.currentSupplement,
|
||||
required this.totalPrice,
|
||||
required this.totalPortionPrice,
|
||||
required this.autoIncreaseEnabled,
|
||||
required this.autoIncreaseAmount,
|
||||
required this.autoIncreaseIntervalDays,
|
||||
this.nextAutoIncreaseAt,
|
||||
});
|
||||
|
||||
factory TreePricingConfig.fromJson(Map<String, dynamic> json) {
|
||||
return TreePricingConfig(
|
||||
basePrice: json['basePrice'] ?? 15831,
|
||||
basePortionPrice: json['basePortionPrice'] ?? 3171,
|
||||
currentSupplement: json['currentSupplement'] ?? 0,
|
||||
totalPrice: json['totalPrice'] ?? 15831,
|
||||
totalPortionPrice: json['totalPortionPrice'] ?? 3171,
|
||||
autoIncreaseEnabled: json['autoIncreaseEnabled'] ?? false,
|
||||
autoIncreaseAmount: json['autoIncreaseAmount'] ?? 0,
|
||||
autoIncreaseIntervalDays: json['autoIncreaseIntervalDays'] ?? 0,
|
||||
nextAutoIncreaseAt: json['nextAutoIncreaseAt'] != null
|
||||
? DateTime.tryParse(json['nextAutoIncreaseAt'])
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
/// 默认配置(supplement=0,与原硬编码价格一致)
|
||||
factory TreePricingConfig.defaultConfig() {
|
||||
return TreePricingConfig(
|
||||
basePrice: 15831,
|
||||
basePortionPrice: 3171,
|
||||
currentSupplement: 0,
|
||||
totalPrice: 15831,
|
||||
totalPortionPrice: 3171,
|
||||
autoIncreaseEnabled: false,
|
||||
autoIncreaseAmount: 0,
|
||||
autoIncreaseIntervalDays: 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 认种树定价配置服务
|
||||
///
|
||||
/// 从 admin-service 公开接口获取当前定价,5 分钟缓存。
|
||||
/// 失败时安全降级返回默认配置(supplement=0)。
|
||||
class TreePricingService {
|
||||
final ApiClient _apiClient;
|
||||
|
||||
/// 缓存的定价配置
|
||||
TreePricingConfig? _cachedConfig;
|
||||
|
||||
/// 上次获取时间
|
||||
DateTime? _lastFetchTime;
|
||||
|
||||
/// 缓存有效期(5 分钟)
|
||||
static const Duration _cacheExpiration = Duration(minutes: 5);
|
||||
|
||||
TreePricingService({required ApiClient apiClient}) : _apiClient = apiClient;
|
||||
|
||||
/// 获取当前定价配置
|
||||
///
|
||||
/// [forceRefresh] 强制刷新,不使用缓存
|
||||
Future<TreePricingConfig> getConfig({bool forceRefresh = false}) async {
|
||||
// 检查缓存是否有效
|
||||
if (!forceRefresh && _cachedConfig != null && _lastFetchTime != null) {
|
||||
final cacheAge = DateTime.now().difference(_lastFetchTime!);
|
||||
if (cacheAge < _cacheExpiration) {
|
||||
return _cachedConfig!;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
final response = await _apiClient.get(
|
||||
'/tree-pricing/config',
|
||||
);
|
||||
|
||||
_cachedConfig = TreePricingConfig.fromJson(response.data);
|
||||
_lastFetchTime = DateTime.now();
|
||||
|
||||
debugPrint(
|
||||
'[TreePricingService] 获取定价成功: totalPrice=${_cachedConfig!.totalPrice}, '
|
||||
'totalPortionPrice=${_cachedConfig!.totalPortionPrice}, '
|
||||
'supplement=${_cachedConfig!.currentSupplement}',
|
||||
);
|
||||
|
||||
return _cachedConfig!;
|
||||
} catch (e) {
|
||||
debugPrint('[TreePricingService] 获取定价失败: $e');
|
||||
|
||||
// 有缓存则返回缓存,否则返回默认配置(安全降级)
|
||||
return _cachedConfig ?? TreePricingConfig.defaultConfig();
|
||||
}
|
||||
}
|
||||
|
||||
/// 清除缓存
|
||||
void clearCache() {
|
||||
_cachedConfig = null;
|
||||
_lastFetchTime = null;
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||
import 'package:go_router/go_router.dart';
|
||||
import '../../../../routes/route_paths.dart';
|
||||
import '../../../../core/di/injection_container.dart';
|
||||
import '../../../../core/services/tree_pricing_service.dart';
|
||||
import 'planting_location_page.dart';
|
||||
|
||||
/// 认种数量选择页面
|
||||
|
|
@ -16,8 +17,11 @@ class PlantingQuantityPage extends ConsumerStatefulWidget {
|
|||
}
|
||||
|
||||
class _PlantingQuantityPageState extends ConsumerState<PlantingQuantityPage> {
|
||||
/// 每棵树的价格 (USDT)
|
||||
static const double _pricePerTree = 15831.0;
|
||||
/// 每棵树的价格 (USDT) - 从 admin-service 动态获取
|
||||
double _pricePerTree = 15831.0;
|
||||
|
||||
/// 定价配置(用于展示下次涨价信息)
|
||||
TreePricingConfig? _pricingConfig;
|
||||
|
||||
/// 可用余额 (USDT) - 从 API 获取
|
||||
double _availableBalance = 0.0;
|
||||
|
|
@ -46,10 +50,29 @@ class _PlantingQuantityPageState extends ConsumerState<PlantingQuantityPage> {
|
|||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadPricingConfig();
|
||||
_loadBalance();
|
||||
_quantityController.addListener(_onQuantityChanged);
|
||||
}
|
||||
|
||||
/// 从 admin-service 加载动态定价配置
|
||||
Future<void> _loadPricingConfig() async {
|
||||
try {
|
||||
final treePricingService = ref.read(treePricingServiceProvider);
|
||||
final config = await treePricingService.getConfig();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_pricingConfig = config;
|
||||
_pricePerTree = config.totalPrice.toDouble();
|
||||
});
|
||||
debugPrint('[PlantingQuantity] 动态定价: ${config.totalPrice} USDT/棵 (supplement=${config.currentSupplement})');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[PlantingQuantity] 获取定价失败,使用默认 15831: $e');
|
||||
// 安全降级:保持 15831 默认值
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_quantityController.removeListener(_onQuantityChanged);
|
||||
|
|
@ -684,6 +707,40 @@ class _PlantingQuantityPageState extends ConsumerState<PlantingQuantityPage> {
|
|||
color: Color(0xFF000000),
|
||||
),
|
||||
),
|
||||
// 下次涨价预告
|
||||
if (_pricingConfig != null &&
|
||||
_pricingConfig!.autoIncreaseEnabled &&
|
||||
_pricingConfig!.nextAutoIncreaseAt != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFFF3E0),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.trending_up,
|
||||
color: Color(0xFFE65100),
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'预计涨价: ${_pricingConfig!.nextAutoIncreaseAt!.month}月${_pricingConfig!.nextAutoIncreaseAt!.day}日后每棵 +${_pricingConfig!.autoIncreaseAmount} 绿积分',
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontFamily: 'Inter',
|
||||
height: 1.4,
|
||||
color: Color(0xFFE65100),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart';
|
|||
import 'package:city_pickers/city_pickers.dart';
|
||||
import '../../../../core/di/injection_container.dart';
|
||||
import '../../../../core/services/pre_planting_service.dart';
|
||||
import '../../../../core/services/tree_pricing_service.dart';
|
||||
|
||||
// ============================================
|
||||
// [2026-02-17] 预种计划购买页面
|
||||
|
|
@ -41,12 +42,15 @@ class _PrePlantingPurchasePageState
|
|||
extends ConsumerState<PrePlantingPurchasePage> {
|
||||
// === 常量 ===
|
||||
|
||||
/// 每份预种价格(USDT)
|
||||
static const double _pricePerPortion = 3171.0;
|
||||
/// 每份预种价格(USDT)- 从 admin-service 动态获取
|
||||
double _pricePerPortion = 3171.0;
|
||||
|
||||
/// 合并所需份数
|
||||
static const int _portionsPerTree = 5;
|
||||
|
||||
/// 定价配置(用于展示下次涨价信息)
|
||||
TreePricingConfig? _pricingConfig;
|
||||
|
||||
// === 状态变量 ===
|
||||
|
||||
/// 可用余额(绿积分 / USDT)
|
||||
|
|
@ -126,6 +130,7 @@ class _PrePlantingPurchasePageState
|
|||
try {
|
||||
final walletService = ref.read(walletServiceProvider);
|
||||
final prePlantingService = ref.read(prePlantingServiceProvider);
|
||||
final treePricingService = ref.read(treePricingServiceProvider);
|
||||
|
||||
// 并行加载所有数据
|
||||
final results = await Future.wait([
|
||||
|
|
@ -140,12 +145,14 @@ class _PrePlantingPurchasePageState
|
|||
totalTreesMerged: 0,
|
||||
),
|
||||
),
|
||||
treePricingService.getConfig(), // [4] 定价配置
|
||||
]);
|
||||
|
||||
final walletResponse = results[0];
|
||||
final config = results[1] as PrePlantingConfig;
|
||||
final eligibility = results[2] as PrePlantingEligibility;
|
||||
final position = results[3] as PrePlantingPosition;
|
||||
final pricingConfig = results[4] as TreePricingConfig;
|
||||
|
||||
// 使用钱包中的 USDT 可用余额
|
||||
final balance = (walletResponse as dynamic).balances.usdt.available as double;
|
||||
|
|
@ -155,6 +162,8 @@ class _PrePlantingPurchasePageState
|
|||
_config = config;
|
||||
_eligibility = eligibility;
|
||||
_position = position;
|
||||
_pricingConfig = pricingConfig;
|
||||
_pricePerPortion = pricingConfig.totalPortionPrice.toDouble();
|
||||
_isLoading = false;
|
||||
|
||||
// 续购时自动填入已保存的省市
|
||||
|
|
@ -1310,6 +1319,40 @@ class _PrePlantingPurchasePageState
|
|||
color: Color(0xFF745D43),
|
||||
),
|
||||
),
|
||||
// 下次涨价预告
|
||||
if (_pricingConfig != null &&
|
||||
_pricingConfig!.autoIncreaseEnabled &&
|
||||
_pricingConfig!.nextAutoIncreaseAt != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFFF3E0),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.trending_up,
|
||||
color: Color(0xFFE65100),
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'预计涨价: ${_pricingConfig!.nextAutoIncreaseAt!.month}月${_pricingConfig!.nextAutoIncreaseAt!.day}日后每份 +${(_pricingConfig!.autoIncreaseAmount / 5).floor()} 绿积分',
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontFamily: 'Inter',
|
||||
height: 1.4,
|
||||
color: Color(0xFFE65100),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in New Issue