fix(c2c): 积分股→积分值,C2C交易对象修正为积分值(cash)

C2C交易的产品是积分值,支付方式是绿积分(外部1.0系统),与积分股无关。

前端:
- c2c_publish_page: 余额卡、数量输入、提示文案、确认弹窗全部改为积分值
- c2c_market_page: 余额概览(availableShares→availableCash)、订单数量标签、接单弹窗
- c2c_order_detail_page: 订单详情、划转提示、确认收款弹窗

后端 c2c.service.ts:
- createOrder SELL: freezeShares→freezeCash
- takeOrder BUY(taker=卖方): freezeShares→freezeCash
- takeOrder SELL(taker=买方): 移除冻结(买方通过外部支付绿积分)
- cancelOrder SELL: unfreezeShares→unfreezeCash
- expireOrder: unfreezeShares→unfreezeCash,SELL不再解冻taker
- executeTransfer: 统一BUY/SELL为单向积分值划转,assetType改为CASH

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-30 12:41:23 -08:00
parent 49bcb96c4c
commit 94d4524ee3
4 changed files with 109 additions and 200 deletions

View File

@ -38,15 +38,16 @@ const DEFAULT_CONFIRM_TIMEOUT_MINUTES = 60;
/**
* C2C
*
*
* C2C交易的是积分值绿/1.0
* 1. 广createOrder-> 状态: PENDING
* - BUY 类型: 冻结用户的积分值cashBalance
* - SELL 类型: 冻结用户的积分股shareBalance
* - BUY 类型: 验证积分值余额绿
* - SELL 类型: 冻结卖方的积分值cashBalance
* 2. takeOrder-> 状态: MATCHED
* -
* - BUY单taker=: taker的积分值
* - SELL单taker=: 绿
* 3. confirmPayment-> 状态: PAID
* 4. confirmReceived-> 状态: COMPLETED
* -
* -
*/
@Injectable()
export class C2cService {
@ -144,13 +145,13 @@ export class C2cService {
}
// #12: 不冻结买方通过外部1.0系统支付绿积分,系统无法冻结外部资产)
} else {
// 卖出订单:需要冻结积分
// 卖出订单:需要冻结积分C2C交易的是积分值
const quantityMoney = new Money(quantityDecimal);
if (account.availableShares.isLessThan(quantityMoney)) {
throw new BadRequestException(`积分余额不足,需要 ${quantity},可用 ${account.availableShares.toString()}`);
if (account.availableCash.isLessThan(quantityMoney)) {
throw new BadRequestException(`积分余额不足,需要 ${quantity},可用 ${account.availableCash.toString()}`);
}
// 冻结积分
await this.tradingAccountRepository.freezeShares(accountSequence, quantityDecimal);
// 冻结积分
await this.tradingAccountRepository.freezeCash(accountSequence, quantityDecimal);
}
// 创建订单
@ -288,23 +289,17 @@ export class C2cService {
this.logger.log(`C2C部分成交: 原订单 ${orderNo} 拆分,剩余 ${remainingQuantity} 创建新订单 ${remainOrderNo}`);
}
// 接单方需要冻结对应资产
// 接单方需要冻结对应资产C2C交易的是积分值
if (order.type === C2C_ORDER_TYPE.BUY) {
// 挂单方要买入积分股,接单方需要有积分股来卖出
// 挂单方要买入积分值,接单方(卖方)需要有积分值来卖出
const quantityMoney = new Money(quantityDecimal);
if (takerAccount.availableShares.isLessThan(quantityMoney)) {
throw new BadRequestException(`积分股余额不足,需要 ${quantityDecimal.toString()},可用 ${takerAccount.availableShares.toString()}`);
}
// 冻结接单方的积分股
await this.tradingAccountRepository.freezeShares(takerAccountSequence, quantityDecimal);
} else {
// 挂单方要卖出积分股,接单方需要有积分值来买入
const totalAmountMoney = new Money(totalAmountDecimal);
if (takerAccount.availableCash.isLessThan(totalAmountMoney)) {
throw new BadRequestException(`积分值余额不足,需要 ${totalAmountDecimal.toString()},可用 ${takerAccount.availableCash.toString()}`);
if (takerAccount.availableCash.isLessThan(quantityMoney)) {
throw new BadRequestException(`积分值余额不足,需要 ${quantityDecimal.toString()},可用 ${takerAccount.availableCash.toString()}`);
}
// 冻结接单方的积分值
await this.tradingAccountRepository.freezeCash(takerAccountSequence, totalAmountDecimal);
await this.tradingAccountRepository.freezeCash(takerAccountSequence, quantityDecimal);
} else {
// 挂单方要卖出积分值,接单方(买方)通过外部支付绿积分,不冻结
}
// 计算超时时间
@ -369,7 +364,8 @@ export class C2cService {
if (order.type === C2C_ORDER_TYPE.BUY) {
// BUY 订单未冻结买方资产(买方通过外部支付绿积分),无需解冻
} else {
await this.tradingAccountRepository.unfreezeShares(accountSequence, quantityDecimal);
// SELL 订单:解冻卖方的积分值
await this.tradingAccountRepository.unfreezeCash(accountSequence, quantityDecimal);
}
// 更新订单状态
@ -494,146 +490,66 @@ export class C2cService {
sellerAccountSequence = order.makerAccountSequence;
}
// 使用事务执行转账
// 使用事务执行转账C2C交易的是积分值BUY和SELL都是单向划转积分值
await this.prisma.$transaction(async (tx) => {
if (order.type === C2C_ORDER_TYPE.BUY) {
// BUY订单买方通过外部支付绿积分系统只转移卖方积分股给买方单向
// 1. 解冻卖方积分股并扣除
await tx.tradingAccount.update({
where: { accountSequence: sellerAccountSequence },
data: {
frozenShares: { decrement: quantityDecimal.toNumber() },
shareBalance: { decrement: quantityDecimal.toNumber() },
totalSold: { increment: quantityDecimal.toNumber() },
},
});
// 1. 解冻卖方积分值并扣除
await tx.tradingAccount.update({
where: { accountSequence: sellerAccountSequence },
data: {
frozenCash: { decrement: quantityDecimal.toNumber() },
cashBalance: { decrement: quantityDecimal.toNumber() },
totalSold: { increment: quantityDecimal.toNumber() },
},
});
// 2. 买方获得积分股
await tx.tradingAccount.update({
where: { accountSequence: buyerAccountSequence },
data: {
shareBalance: { increment: quantityDecimal.toNumber() },
totalBought: { increment: quantityDecimal.toNumber() },
},
});
// 2. 买方获得积分值
await tx.tradingAccount.update({
where: { accountSequence: buyerAccountSequence },
data: {
cashBalance: { increment: quantityDecimal.toNumber() },
totalBought: { increment: quantityDecimal.toNumber() },
},
});
// 3. 记录交易流水(买方 — 积分股入账)
const buyerAccount = await tx.tradingAccount.findUnique({
where: { accountSequence: buyerAccountSequence },
});
await tx.tradingTransaction.create({
data: {
accountSequence: buyerAccountSequence,
type: 'C2C_BUY',
assetType: 'SHARE',
amount: quantityDecimal.toNumber(),
balanceBefore: new Decimal(buyerAccount!.shareBalance).minus(quantityDecimal).toNumber(),
balanceAfter: buyerAccount!.shareBalance,
referenceId: order.orderNo,
referenceType: 'C2C_ORDER',
counterpartyType: 'USER',
counterpartyAccountSeq: sellerAccountSequence,
memo: `C2C买入 ${order.quantity} 积分(绿积分支付)`,
},
});
// 3. 记录交易流水(买方 — 积分值入账)
const buyerAccount = await tx.tradingAccount.findUnique({
where: { accountSequence: buyerAccountSequence },
});
await tx.tradingTransaction.create({
data: {
accountSequence: buyerAccountSequence,
type: 'C2C_BUY',
assetType: 'CASH',
amount: quantityDecimal.toNumber(),
balanceBefore: new Decimal(buyerAccount!.cashBalance).minus(quantityDecimal).toNumber(),
balanceAfter: buyerAccount!.cashBalance,
referenceId: order.orderNo,
referenceType: 'C2C_ORDER',
counterpartyType: 'USER',
counterpartyAccountSeq: sellerAccountSequence,
memo: `C2C买入 ${order.quantity} 积分(绿积分支付)`,
},
});
// 4. 记录交易流水(卖方 — 积分股扣除)
const sellerAccount = await tx.tradingAccount.findUnique({
where: { accountSequence: sellerAccountSequence },
});
await tx.tradingTransaction.create({
data: {
accountSequence: sellerAccountSequence,
type: 'C2C_SELL',
assetType: 'SHARE',
amount: quantityDecimal.toNumber(),
balanceBefore: new Decimal(sellerAccount!.shareBalance).plus(quantityDecimal).toNumber(),
balanceAfter: sellerAccount!.shareBalance,
referenceId: order.orderNo,
referenceType: 'C2C_ORDER',
counterpartyType: 'USER',
counterpartyAccountSeq: buyerAccountSequence,
memo: `C2C卖出 ${order.quantity} 积分股(收到绿积分)`,
},
});
} else {
// SELL订单双向交换积分股 ↔ 积分值),保持现有逻辑
// 1. 解冻买方的积分值并扣除
await tx.tradingAccount.update({
where: { accountSequence: buyerAccountSequence },
data: {
frozenCash: { decrement: totalAmountDecimal.toNumber() },
cashBalance: { decrement: totalAmountDecimal.toNumber() },
},
});
// 2. 解冻卖方的积分股并扣除
await tx.tradingAccount.update({
where: { accountSequence: sellerAccountSequence },
data: {
frozenShares: { decrement: quantityDecimal.toNumber() },
shareBalance: { decrement: quantityDecimal.toNumber() },
totalSold: { increment: quantityDecimal.toNumber() },
},
});
// 3. 买方获得积分股
await tx.tradingAccount.update({
where: { accountSequence: buyerAccountSequence },
data: {
shareBalance: { increment: quantityDecimal.toNumber() },
totalBought: { increment: quantityDecimal.toNumber() },
},
});
// 4. 卖方获得积分值
await tx.tradingAccount.update({
where: { accountSequence: sellerAccountSequence },
data: {
cashBalance: { increment: totalAmountDecimal.toNumber() },
},
});
// 5. 记录交易流水(买方)
const buyerAccount = await tx.tradingAccount.findUnique({
where: { accountSequence: buyerAccountSequence },
});
await tx.tradingTransaction.create({
data: {
accountSequence: buyerAccountSequence,
type: 'C2C_BUY',
assetType: 'SHARE',
amount: quantityDecimal.toNumber(),
balanceBefore: new Decimal(buyerAccount!.shareBalance).minus(quantityDecimal).toNumber(),
balanceAfter: buyerAccount!.shareBalance,
referenceId: order.orderNo,
referenceType: 'C2C_ORDER',
counterpartyType: 'USER',
counterpartyAccountSeq: sellerAccountSequence,
memo: `C2C买入 ${order.quantity} 积分股,单价 ${order.price}`,
},
});
// 6. 记录交易流水(卖方)
const sellerAccount = await tx.tradingAccount.findUnique({
where: { accountSequence: sellerAccountSequence },
});
await tx.tradingTransaction.create({
data: {
accountSequence: sellerAccountSequence,
type: 'C2C_SELL',
assetType: 'CASH',
amount: totalAmountDecimal.toNumber(),
balanceBefore: new Decimal(sellerAccount!.cashBalance).minus(totalAmountDecimal).toNumber(),
balanceAfter: sellerAccount!.cashBalance,
referenceId: order.orderNo,
referenceType: 'C2C_ORDER',
counterpartyType: 'USER',
counterpartyAccountSeq: buyerAccountSequence,
memo: `C2C卖出 ${order.quantity} 积分股,单价 ${order.price}`,
},
});
}
// 4. 记录交易流水(卖方 — 积分值扣除)
const sellerAccount = await tx.tradingAccount.findUnique({
where: { accountSequence: sellerAccountSequence },
});
await tx.tradingTransaction.create({
data: {
accountSequence: sellerAccountSequence,
type: 'C2C_SELL',
assetType: 'CASH',
amount: quantityDecimal.toNumber(),
balanceBefore: new Decimal(sellerAccount!.cashBalance).plus(quantityDecimal).toNumber(),
balanceAfter: sellerAccount!.cashBalance,
referenceId: order.orderNo,
referenceType: 'C2C_ORDER',
counterpartyType: 'USER',
counterpartyAccountSeq: buyerAccountSequence,
memo: `C2C卖出 ${order.quantity} 积分值(收到绿积分)`,
},
});
});
this.logger.log(`C2C交易转账完成: ${order.orderNo}, 买方: ${buyerAccountSequence}, 卖方: ${sellerAccountSequence}`);
@ -740,18 +656,15 @@ export class C2cService {
const quantityDecimal = new Decimal(freshOrder.quantity);
const totalAmountDecimal = new Decimal(freshOrder.totalAmount);
// 解冻双方资产
// 解冻卖方的积分值C2C交易的是积分值买方不冻结
if (freshOrder.type === C2C_ORDER_TYPE.BUY) {
// BUY订单maker未冻结买方通过外部支付绿积分只解冻taker的积分股
// BUY订单maker买方未冻结只解冻taker卖方的积分值
if (freshOrder.takerAccountSequence) {
await this.tradingAccountRepository.unfreezeShares(freshOrder.takerAccountSequence, quantityDecimal);
await this.tradingAccountRepository.unfreezeCash(freshOrder.takerAccountSequence, quantityDecimal);
}
} else {
// SELL订单maker冻结了积分股taker冻结了积分值
await this.tradingAccountRepository.unfreezeShares(freshOrder.makerAccountSequence, quantityDecimal);
if (freshOrder.takerAccountSequence) {
await this.tradingAccountRepository.unfreezeCash(freshOrder.takerAccountSequence, totalAmountDecimal);
}
// SELL订单maker卖方冻结了积分值taker买方未冻结
await this.tradingAccountRepository.unfreezeCash(freshOrder.makerAccountSequence, quantityDecimal);
}
// 更新订单状态为过期

View File

@ -127,12 +127,12 @@ class _C2cMarketPageState extends ConsumerState<C2cMarketPage>
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'可用积分',
'可用积分',
style: TextStyle(fontSize: 12, color: AppColors.textSecondaryOf(context)),
),
const SizedBox(height: 4),
Text(
formatAmount(availableShares),
formatAmount(availableCash),
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
@ -389,7 +389,7 @@ class _C2cMarketPageState extends ConsumerState<C2cMarketPage>
Text('数量', style: TextStyle(fontSize: 12, color: AppColors.textSecondaryOf(context))),
const SizedBox(height: 4),
Text(
'${formatAmount(order.quantity)} 积分',
'${formatAmount(order.quantity)} 积分',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
@ -490,7 +490,7 @@ class _C2cMarketPageState extends ConsumerState<C2cMarketPage>
children: [
Expanded(
child: Text(
'${formatAmount(order.quantity)} 积分',
'${formatAmount(order.quantity)} 积分',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
@ -649,7 +649,7 @@ class _C2cMarketPageState extends ConsumerState<C2cMarketPage>
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('可用数量', style: TextStyle(fontSize: 13, color: AppColors.textSecondaryOf(context))),
Text('${formatAmount(order.quantity)} 积分', style: TextStyle(fontSize: 13, color: AppColors.textPrimaryOf(context))),
Text('${formatAmount(order.quantity)} 积分', style: TextStyle(fontSize: 13, color: AppColors.textPrimaryOf(context))),
],
),
],
@ -683,7 +683,7 @@ class _C2cMarketPageState extends ConsumerState<C2cMarketPage>
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: _orange, width: 2),
),
suffixText: '积分',
suffixText: '积分',
suffixStyle: TextStyle(color: AppColors.textSecondaryOf(context), fontSize: 14),
),
),
@ -830,8 +830,8 @@ class _C2cMarketPageState extends ConsumerState<C2cMarketPage>
),
child: Text(
isBuyAction
? '您将使用积分值购买对方的积分股'
: '您将出售积分,买方将向您支付绿积分',
? '您将使用绿积分购买对方的积分值'
: '您将出售积分,买方将向您支付绿积分',
style: TextStyle(fontSize: 12, color: AppColors.textSecondaryOf(context)),
),
),

View File

@ -338,8 +338,8 @@ class _C2cOrderDetailPageState extends ConsumerState<C2cOrderDetailPage> {
),
const SizedBox(height: 16),
_buildInfoRow('订单编号', order.orderNo, canCopy: true),
_buildInfoRow('单价', '${formatPrice(order.price)} 积分值/股'),
_buildInfoRow('数量', '${formatAmount(order.quantity)} 积分'),
_buildInfoRow('单价', '${formatPrice(order.price)} 积分值'),
_buildInfoRow('数量', '${formatAmount(order.quantity)} 积分'),
_buildInfoRow('总金额', '${formatAmount(order.totalAmount)} 积分值'),
_buildInfoRow('创建时间', _formatDateTime(order.createdAt)),
if (order.remark != null && order.remark!.isNotEmpty)
@ -880,12 +880,12 @@ class _C2cOrderDetailPageState extends ConsumerState<C2cOrderDetailPage> {
? [
'您已确认付款',
'请等待卖方确认收款',
'卖方确认后积分将自动划转到您的账户',
'卖方确认后积分将自动划转到您的账户',
]
: [
'买方已确认付款',
'请检查您是否已收到转账',
'确认收款后积分将自动划转给买方',
'确认收款后积分将自动划转给买方',
];
break;
default:
@ -1215,7 +1215,7 @@ class _C2cOrderDetailPageState extends ConsumerState<C2cOrderDetailPage> {
Text('金额: ${formatAmount(order.totalAmount)} 积分值'),
const SizedBox(height: 16),
const Text(
'确认后,您的积分将自动划转给买方,此操作不可撤销。',
'确认后,您的积分将自动划转给买方,此操作不可撤销。',
style: TextStyle(fontSize: 12, color: _red),
),
],

View File

@ -192,11 +192,9 @@ class _C2cPublishPageState extends ConsumerState<C2cPublishPage> {
}
Widget _buildBalanceCard(String availableShares, String availableCash) {
final isBuy = _selectedType == 0;
// BUY: #16
// SELL:
final label = isBuy ? '可用积分值' : '可用积分股';
final balance = isBuy ? availableCash : availableShares;
// C2C交易对象是积分值BUY和SELL都显示可用积分值
const label = '可用积分值';
final balance = availableCash;
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
@ -271,16 +269,14 @@ class _C2cPublishPageState extends ConsumerState<C2cPublishPage> {
);
}
// #14: 1:1
// #14: 1:1
Widget _buildQuantityInput(
String availableShares,
String availableCash,
String currentPrice,
) {
final isBuy = _selectedType == 0;
// BUY:
// SELL:
final maxBalance = isBuy ? availableCash : availableShares;
// C2C交易的是积分值BUY和SELL上限都是可用积分值
final maxBalance = availableCash;
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
@ -302,12 +298,12 @@ class _C2cPublishPageState extends ConsumerState<C2cPublishPage> {
),
const SizedBox(height: 4),
Text(
'价格1:1积分数量 = 积分值金额',
'价格1:1积分数量 = 积分值金额',
style: const TextStyle(fontSize: 12, color: _grayText),
),
const SizedBox(height: 12),
// 1:
// 1:
TextField(
controller: _quantityController,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
@ -315,7 +311,7 @@ class _C2cPublishPageState extends ConsumerState<C2cPublishPage> {
FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d{0,4}')),
],
decoration: InputDecoration(
hintText: '请输入积分数量',
hintText: '请输入积分数量',
hintStyle: const TextStyle(color: _grayText),
filled: true,
fillColor: _bgGray,
@ -343,7 +339,7 @@ class _C2cPublishPageState extends ConsumerState<C2cPublishPage> {
),
),
),
suffixText: '积分',
suffixText: '积分',
suffixStyle: const TextStyle(color: _grayText, fontSize: 14),
),
onChanged: (val) {
@ -679,9 +675,9 @@ class _C2cPublishPageState extends ConsumerState<C2cPublishPage> {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('积分数量', style: TextStyle(fontSize: 14, color: _grayText)),
const Text('积分数量', style: TextStyle(fontSize: 14, color: _grayText)),
Text(
'${formatAmount(quantity.toString())} 积分',
'${formatAmount(quantity.toString())} 积分',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
@ -783,11 +779,11 @@ class _C2cPublishPageState extends ConsumerState<C2cPublishPage> {
),
const SizedBox(height: 8),
const Text(
'1. 发布卖出广告后,您的积分将被冻结直到交易完成或取消\n'
'1. 发布卖出广告后,您的积分将被冻结直到交易完成或取消\n'
'2. 发布买入广告不会冻结您的资产\n'
'3. 其他用户接单后,需在规定时间内完成交易\n'
'4. 买方需在1.0系统中向卖方转账绿积分\n'
'5. 卖方确认收到绿积分后,积分自动划转给买方\n'
'5. 卖方确认收到绿积分后,积分自动划转给买方\n'
'6. 如遇问题,请联系客服处理',
style: TextStyle(
fontSize: 12,
@ -871,7 +867,7 @@ class _C2cPublishPageState extends ConsumerState<C2cPublishPage> {
const SizedBox(height: 16),
Text(
_selectedType == 1
? '发布后,您的积分将被冻结'
? '发布后,您的积分将被冻结'
: '发布后不会冻结您的资产',
style: const TextStyle(fontSize: 12, color: _grayText),
),