feat(c2c): 订单详情增强、付款水单上传、买单逻辑修复及schema对齐
- 订单详情页显示完整手机号和收款账号信息 - 新增付款水单上传功能(前后端全链路) - 修复时间显示为本地时区格式 - 移除买单发布时的积分值余额验证(买方通过外部绿积分支付) - 前端买单发布页隐藏余额卡片和"全部"按钮 - 补齐 Prisma migration 与 schema 差异(payment_proof_url、 bot_purchased索引、market_maker_deposits/withdraws表) - docker-compose 新增 trading-uploads 卷用于水单持久化 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
4df23b02b8
commit
ce173451f5
|
|
@ -121,6 +121,8 @@ services:
|
||||||
FUSDT_MARKET_MAKER_ADDRESS: ${FUSDT_MARKET_MAKER_ADDRESS:-}
|
FUSDT_MARKET_MAKER_ADDRESS: ${FUSDT_MARKET_MAKER_ADDRESS:-}
|
||||||
EUSDT_MARKET_MAKER_USERNAME: ${EUSDT_MARKET_MAKER_USERNAME:-}
|
EUSDT_MARKET_MAKER_USERNAME: ${EUSDT_MARKET_MAKER_USERNAME:-}
|
||||||
EUSDT_MARKET_MAKER_ADDRESS: ${EUSDT_MARKET_MAKER_ADDRESS:-}
|
EUSDT_MARKET_MAKER_ADDRESS: ${EUSDT_MARKET_MAKER_ADDRESS:-}
|
||||||
|
volumes:
|
||||||
|
- trading-uploads:/app/uploads
|
||||||
ports:
|
ports:
|
||||||
- "3022:3022"
|
- "3022:3022"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
|
|
@ -349,6 +351,8 @@ services:
|
||||||
volumes:
|
volumes:
|
||||||
mining-admin-uploads:
|
mining-admin-uploads:
|
||||||
driver: local
|
driver: local
|
||||||
|
trading-uploads:
|
||||||
|
driver: local
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
rwa-network:
|
rwa-network:
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
-- ============================================================================
|
||||||
|
-- 补齐 schema 与 migration 的差异
|
||||||
|
-- 新增: payment_proof_url 字段, bot_purchased 索引,
|
||||||
|
-- 做市商充提记录表 (market_maker_deposits, market_maker_withdraws)
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
-- 1. c2c_orders: 补充 payment_proof_url 字段
|
||||||
|
ALTER TABLE "c2c_orders" ADD COLUMN "payment_proof_url" TEXT;
|
||||||
|
|
||||||
|
-- 2. c2c_orders: 补充 bot_purchased 索引
|
||||||
|
CREATE INDEX "c2c_orders_bot_purchased_idx" ON "c2c_orders"("bot_purchased");
|
||||||
|
|
||||||
|
-- 3. 创建 MarketMakerAssetType 枚举
|
||||||
|
CREATE TYPE "MarketMakerAssetType" AS ENUM ('EUSDT', 'FUSDT');
|
||||||
|
|
||||||
|
-- 4. 创建 MarketMakerDepositStatus 枚举
|
||||||
|
CREATE TYPE "MarketMakerDepositStatus" AS ENUM ('DETECTED', 'CONFIRMING', 'CONFIRMED', 'CREDITED', 'FAILED');
|
||||||
|
|
||||||
|
-- 5. 创建 MarketMakerWithdrawStatus 枚举
|
||||||
|
CREATE TYPE "MarketMakerWithdrawStatus" AS ENUM ('PENDING', 'SIGNING', 'SIGNED', 'BROADCAST', 'CONFIRMING', 'COMPLETED', 'FAILED', 'CANCELLED');
|
||||||
|
|
||||||
|
-- 6. 创建 market_maker_deposits 表
|
||||||
|
CREATE TABLE "market_maker_deposits" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"market_maker_id" TEXT NOT NULL,
|
||||||
|
"chain_type" VARCHAR(20) NOT NULL,
|
||||||
|
"tx_hash" VARCHAR(66) NOT NULL,
|
||||||
|
"block_number" BIGINT NOT NULL,
|
||||||
|
"block_timestamp" TIMESTAMP(3) NOT NULL,
|
||||||
|
"asset_type" "MarketMakerAssetType" NOT NULL,
|
||||||
|
"token_contract" VARCHAR(42) NOT NULL,
|
||||||
|
"from_address" VARCHAR(42) NOT NULL,
|
||||||
|
"to_address" VARCHAR(42) NOT NULL,
|
||||||
|
"amount" DECIMAL(36,8) NOT NULL,
|
||||||
|
"amount_raw" DECIMAL(78,0) NOT NULL,
|
||||||
|
"confirmations" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"required_confirms" INTEGER NOT NULL DEFAULT 12,
|
||||||
|
"status" "MarketMakerDepositStatus" NOT NULL DEFAULT 'DETECTED',
|
||||||
|
"credited_at" TIMESTAMP(3),
|
||||||
|
"credited_amount" DECIMAL(36,8),
|
||||||
|
"ledger_id" TEXT,
|
||||||
|
"error_message" TEXT,
|
||||||
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "market_maker_deposits_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "market_maker_deposits_tx_hash_key" ON "market_maker_deposits"("tx_hash");
|
||||||
|
CREATE INDEX "market_maker_deposits_market_maker_id_idx" ON "market_maker_deposits"("market_maker_id");
|
||||||
|
CREATE INDEX "market_maker_deposits_status_idx" ON "market_maker_deposits"("status");
|
||||||
|
CREATE INDEX "market_maker_deposits_chain_type_status_idx" ON "market_maker_deposits"("chain_type", "status");
|
||||||
|
CREATE INDEX "market_maker_deposits_asset_type_idx" ON "market_maker_deposits"("asset_type");
|
||||||
|
CREATE INDEX "market_maker_deposits_block_number_idx" ON "market_maker_deposits"("block_number");
|
||||||
|
CREATE INDEX "market_maker_deposits_created_at_idx" ON "market_maker_deposits"("created_at" DESC);
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "market_maker_deposits" ADD CONSTRAINT "market_maker_deposits_market_maker_id_fkey" FOREIGN KEY ("market_maker_id") REFERENCES "market_maker_configs"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- 7. 创建 market_maker_withdraws 表
|
||||||
|
CREATE TABLE "market_maker_withdraws" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"withdraw_no" TEXT NOT NULL,
|
||||||
|
"market_maker_id" TEXT NOT NULL,
|
||||||
|
"asset_type" "MarketMakerAssetType" NOT NULL,
|
||||||
|
"amount" DECIMAL(36,8) NOT NULL,
|
||||||
|
"chain_type" VARCHAR(20) NOT NULL,
|
||||||
|
"token_contract" VARCHAR(42) NOT NULL,
|
||||||
|
"from_address" VARCHAR(42) NOT NULL,
|
||||||
|
"to_address" VARCHAR(42) NOT NULL,
|
||||||
|
"tx_hash" VARCHAR(66),
|
||||||
|
"block_number" BIGINT,
|
||||||
|
"confirmations" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"gas_used" DECIMAL(36,0),
|
||||||
|
"gas_price" DECIMAL(36,0),
|
||||||
|
"status" "MarketMakerWithdrawStatus" NOT NULL DEFAULT 'PENDING',
|
||||||
|
"ledger_id" TEXT,
|
||||||
|
"error_message" TEXT,
|
||||||
|
"retry_count" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||||
|
"signed_at" TIMESTAMP(3),
|
||||||
|
"broadcast_at" TIMESTAMP(3),
|
||||||
|
"completed_at" TIMESTAMP(3),
|
||||||
|
"cancelled_at" TIMESTAMP(3),
|
||||||
|
|
||||||
|
CONSTRAINT "market_maker_withdraws_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "market_maker_withdraws_withdraw_no_key" ON "market_maker_withdraws"("withdraw_no");
|
||||||
|
CREATE INDEX "market_maker_withdraws_market_maker_id_idx" ON "market_maker_withdraws"("market_maker_id");
|
||||||
|
CREATE INDEX "market_maker_withdraws_status_idx" ON "market_maker_withdraws"("status");
|
||||||
|
CREATE INDEX "market_maker_withdraws_asset_type_idx" ON "market_maker_withdraws"("asset_type");
|
||||||
|
CREATE INDEX "market_maker_withdraws_tx_hash_idx" ON "market_maker_withdraws"("tx_hash");
|
||||||
|
CREATE INDEX "market_maker_withdraws_created_at_idx" ON "market_maker_withdraws"("created_at" DESC);
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "market_maker_withdraws" ADD CONSTRAINT "market_maker_withdraws_market_maker_id_fkey" FOREIGN KEY ("market_maker_id") REFERENCES "market_maker_configs"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
@ -679,6 +679,9 @@ model C2cOrder {
|
||||||
paymentDeadline DateTime? @map("payment_deadline") // 付款截止时间
|
paymentDeadline DateTime? @map("payment_deadline") // 付款截止时间
|
||||||
confirmDeadline DateTime? @map("confirm_deadline") // 确认收款截止时间
|
confirmDeadline DateTime? @map("confirm_deadline") // 确认收款截止时间
|
||||||
|
|
||||||
|
// 付款水单(买方上传的付款凭证图片)
|
||||||
|
paymentProofUrl String? @map("payment_proof_url")
|
||||||
|
|
||||||
// ============ Bot 自动购买相关 ============
|
// ============ Bot 自动购买相关 ============
|
||||||
// 卖家 Kava 地址(从 SyncedUser 表获取,用于接收 dUSDT)
|
// 卖家 Kava 地址(从 SyncedUser 表获取,用于接收 dUSDT)
|
||||||
sellerKavaAddress String? @map("seller_kava_address")
|
sellerKavaAddress String? @map("seller_kava_address")
|
||||||
|
|
|
||||||
|
|
@ -6,16 +6,25 @@ import {
|
||||||
Query,
|
Query,
|
||||||
Body,
|
Body,
|
||||||
Req,
|
Req,
|
||||||
|
Res,
|
||||||
HttpCode,
|
HttpCode,
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
|
UseInterceptors,
|
||||||
|
UploadedFile,
|
||||||
|
BadRequestException,
|
||||||
|
NotFoundException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
|
import { FileInterceptor } from '@nestjs/platform-express';
|
||||||
import {
|
import {
|
||||||
ApiTags,
|
ApiTags,
|
||||||
ApiOperation,
|
ApiOperation,
|
||||||
ApiResponse,
|
ApiResponse,
|
||||||
ApiParam,
|
ApiParam,
|
||||||
ApiBearerAuth,
|
ApiBearerAuth,
|
||||||
|
ApiConsumes,
|
||||||
} from '@nestjs/swagger';
|
} from '@nestjs/swagger';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as fs from 'fs';
|
||||||
import { C2cService } from '../../application/services/c2c.service';
|
import { C2cService } from '../../application/services/c2c.service';
|
||||||
import {
|
import {
|
||||||
CreateC2cOrderDto,
|
CreateC2cOrderDto,
|
||||||
|
|
@ -57,6 +66,8 @@ export class C2cController {
|
||||||
paymentAccount: order.paymentAccount || undefined,
|
paymentAccount: order.paymentAccount || undefined,
|
||||||
paymentQrCode: order.paymentQrCode || undefined,
|
paymentQrCode: order.paymentQrCode || undefined,
|
||||||
paymentRealName: order.paymentRealName || undefined,
|
paymentRealName: order.paymentRealName || undefined,
|
||||||
|
// 付款水单
|
||||||
|
paymentProofUrl: order.paymentProofUrl || undefined,
|
||||||
// 卖家 Kava 地址
|
// 卖家 Kava 地址
|
||||||
sellerKavaAddress: order.sellerKavaAddress || undefined,
|
sellerKavaAddress: order.sellerKavaAddress || undefined,
|
||||||
// 超时信息
|
// 超时信息
|
||||||
|
|
@ -250,4 +261,53 @@ export class C2cController {
|
||||||
const order = await this.c2cService.confirmReceived(orderNo, accountSequence);
|
const order = await this.c2cService.confirmReceived(orderNo, accountSequence);
|
||||||
return this.toResponseDto(order);
|
return this.toResponseDto(order);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('orders/:orderNo/upload-proof')
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@UseInterceptors(FileInterceptor('file', {
|
||||||
|
limits: { fileSize: 10 * 1024 * 1024 }, // 10MB
|
||||||
|
fileFilter: (_req: any, file: any, cb: any) => {
|
||||||
|
if (!file.mimetype.match(/^image\/(jpeg|png|gif|webp)$/)) {
|
||||||
|
cb(new BadRequestException('仅支持 JPEG/PNG/GIF/WebP 图片'), false);
|
||||||
|
} else {
|
||||||
|
cb(null, true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
@ApiOperation({ summary: '上传付款水单(买方操作)' })
|
||||||
|
@ApiConsumes('multipart/form-data')
|
||||||
|
@ApiParam({ name: 'orderNo', description: '订单号' })
|
||||||
|
@ApiResponse({ status: 200, description: '上传成功' })
|
||||||
|
async uploadPaymentProof(
|
||||||
|
@Param('orderNo') orderNo: string,
|
||||||
|
@UploadedFile() file: Express.Multer.File,
|
||||||
|
@Req() req: any,
|
||||||
|
): Promise<C2cOrderResponseDto> {
|
||||||
|
const accountSequence = req.user?.accountSequence;
|
||||||
|
if (!accountSequence) {
|
||||||
|
throw new Error('Unauthorized');
|
||||||
|
}
|
||||||
|
if (!file) {
|
||||||
|
throw new BadRequestException('请上传付款水单图片');
|
||||||
|
}
|
||||||
|
|
||||||
|
const order = await this.c2cService.uploadPaymentProof(orderNo, accountSequence, file);
|
||||||
|
return this.toResponseDto(order);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('proofs/:filename')
|
||||||
|
@ApiOperation({ summary: '获取付款水单图片' })
|
||||||
|
@ApiParam({ name: 'filename', description: '文件名' })
|
||||||
|
async getProofImage(
|
||||||
|
@Param('filename') filename: string,
|
||||||
|
@Res() res: any,
|
||||||
|
): Promise<void> {
|
||||||
|
// 防止路径遍历攻击
|
||||||
|
const safeName = path.basename(filename);
|
||||||
|
const filePath = path.resolve('./uploads/c2c-proofs', safeName);
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
throw new NotFoundException('文件不存在');
|
||||||
|
}
|
||||||
|
res.sendFile(filePath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -169,6 +169,8 @@ export class C2cOrderResponseDto {
|
||||||
paymentAccount?: string;
|
paymentAccount?: string;
|
||||||
paymentQrCode?: string;
|
paymentQrCode?: string;
|
||||||
paymentRealName?: string;
|
paymentRealName?: string;
|
||||||
|
// 付款水单
|
||||||
|
paymentProofUrl?: string;
|
||||||
// 卖家 Kava 地址(绿积分转账地址)
|
// 卖家 Kava 地址(绿积分转账地址)
|
||||||
sellerKavaAddress?: string;
|
sellerKavaAddress?: string;
|
||||||
// 超时信息
|
// 超时信息
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
import { Injectable, Logger, BadRequestException, NotFoundException, ForbiddenException } from '@nestjs/common';
|
import { Injectable, Logger, BadRequestException, NotFoundException, ForbiddenException } from '@nestjs/common';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
import { C2cOrderRepository, C2cOrderEntity } from '../../infrastructure/persistence/repositories/c2c-order.repository';
|
import { C2cOrderRepository, C2cOrderEntity } from '../../infrastructure/persistence/repositories/c2c-order.repository';
|
||||||
import { TradingAccountRepository } from '../../infrastructure/persistence/repositories/trading-account.repository';
|
import { TradingAccountRepository } from '../../infrastructure/persistence/repositories/trading-account.repository';
|
||||||
import { RedisService } from '../../infrastructure/redis/redis.service';
|
import { RedisService } from '../../infrastructure/redis/redis.service';
|
||||||
|
|
@ -138,12 +140,7 @@ export class C2cService {
|
||||||
|
|
||||||
// 检查余额并冻结资产
|
// 检查余额并冻结资产
|
||||||
if (type === C2C_ORDER_TYPE.BUY) {
|
if (type === C2C_ORDER_TYPE.BUY) {
|
||||||
// #16: 买入订单:验证积分值余额(买方通过外部系统支付绿积分)
|
// 买入订单:买方通过外部系统支付绿积分,不需要验证积分值余额也不冻结
|
||||||
const totalAmountMoney = new Money(totalAmount);
|
|
||||||
if (account.availableCash.isLessThan(totalAmountMoney)) {
|
|
||||||
throw new BadRequestException(`积分值余额不足,需要 ${totalAmount.toString()},可用 ${account.availableCash.toString()}`);
|
|
||||||
}
|
|
||||||
// #12: 不冻结(买方通过外部1.0系统支付绿积分,系统无法冻结外部资产)
|
|
||||||
} else {
|
} else {
|
||||||
// 卖出订单:需要冻结积分值(C2C交易的是积分值)
|
// 卖出订单:需要冻结积分值(C2C交易的是积分值)
|
||||||
const quantityMoney = new Money(quantityDecimal);
|
const quantityMoney = new Money(quantityDecimal);
|
||||||
|
|
@ -618,6 +615,52 @@ export class C2cService {
|
||||||
return order;
|
return order;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传付款水单(买方操作)
|
||||||
|
*/
|
||||||
|
async uploadPaymentProof(
|
||||||
|
orderNo: string,
|
||||||
|
accountSequence: string,
|
||||||
|
file: { buffer: Buffer; originalname: string; mimetype: string },
|
||||||
|
): Promise<C2cOrderEntity> {
|
||||||
|
const order = await this.c2cOrderRepository.findByOrderNo(orderNo);
|
||||||
|
if (!order) {
|
||||||
|
throw new NotFoundException('订单不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只有 MATCHED 或 PAID 状态可以上传水单
|
||||||
|
if (order.status !== C2C_ORDER_STATUS.MATCHED && order.status !== C2C_ORDER_STATUS.PAID) {
|
||||||
|
throw new BadRequestException('当前订单状态不允许上传水单');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确定买方身份
|
||||||
|
const buyerAccountSequence = order.type === C2C_ORDER_TYPE.BUY
|
||||||
|
? order.makerAccountSequence
|
||||||
|
: order.takerAccountSequence;
|
||||||
|
|
||||||
|
if (accountSequence !== buyerAccountSequence) {
|
||||||
|
throw new ForbiddenException('只有买方可以上传付款水单');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存文件到本地
|
||||||
|
const uploadDir = path.resolve('./uploads/c2c-proofs');
|
||||||
|
if (!fs.existsSync(uploadDir)) {
|
||||||
|
fs.mkdirSync(uploadDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const ext = path.extname(file.originalname) || '.jpg';
|
||||||
|
const filename = `${orderNo}-${Date.now()}${ext}`;
|
||||||
|
const filePath = path.join(uploadDir, filename);
|
||||||
|
fs.writeFileSync(filePath, file.buffer);
|
||||||
|
|
||||||
|
// 生成访问URL(通过 API 网关访问)
|
||||||
|
const proofUrl = `/api/v2/trading/c2c/proofs/${filename}`;
|
||||||
|
|
||||||
|
const updated = await this.c2cOrderRepository.updatePaymentProof(orderNo, proofUrl);
|
||||||
|
this.logger.log(`C2C订单上传付款水单: ${orderNo}, file: ${filename}`);
|
||||||
|
return updated!;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理超时订单(由定时任务调用)
|
* 处理超时订单(由定时任务调用)
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,8 @@ export interface C2cOrderEntity {
|
||||||
confirmTimeoutMinutes: number;
|
confirmTimeoutMinutes: number;
|
||||||
paymentDeadline?: Date | null;
|
paymentDeadline?: Date | null;
|
||||||
confirmDeadline?: Date | null;
|
confirmDeadline?: Date | null;
|
||||||
|
// 付款水单
|
||||||
|
paymentProofUrl?: string | null;
|
||||||
// Bot 自动购买相关
|
// Bot 自动购买相关
|
||||||
sellerKavaAddress?: string | null;
|
sellerKavaAddress?: string | null;
|
||||||
botPurchased: boolean;
|
botPurchased: boolean;
|
||||||
|
|
@ -153,6 +155,8 @@ export class C2cOrderRepository {
|
||||||
// #13: 部分成交时更新数量和金额
|
// #13: 部分成交时更新数量和金额
|
||||||
quantity: string;
|
quantity: string;
|
||||||
totalAmount: string;
|
totalAmount: string;
|
||||||
|
// 付款水单
|
||||||
|
paymentProofUrl: string;
|
||||||
// 超时时间
|
// 超时时间
|
||||||
paymentDeadline: Date;
|
paymentDeadline: Date;
|
||||||
confirmDeadline: Date;
|
confirmDeadline: Date;
|
||||||
|
|
@ -323,6 +327,17 @@ export class C2cOrderRepository {
|
||||||
return this.toEntity(record);
|
return this.toEntity(record);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新付款水单URL
|
||||||
|
*/
|
||||||
|
async updatePaymentProof(orderNo: string, paymentProofUrl: string): Promise<C2cOrderEntity | null> {
|
||||||
|
const record = await this.prisma.c2cOrder.update({
|
||||||
|
where: { orderNo },
|
||||||
|
data: { paymentProofUrl },
|
||||||
|
});
|
||||||
|
return this.toEntity(record);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将Prisma记录转为实体
|
* 将Prisma记录转为实体
|
||||||
*/
|
*/
|
||||||
|
|
@ -355,6 +370,8 @@ export class C2cOrderRepository {
|
||||||
confirmTimeoutMinutes: record.confirmTimeoutMinutes,
|
confirmTimeoutMinutes: record.confirmTimeoutMinutes,
|
||||||
paymentDeadline: record.paymentDeadline,
|
paymentDeadline: record.paymentDeadline,
|
||||||
confirmDeadline: record.confirmDeadline,
|
confirmDeadline: record.confirmDeadline,
|
||||||
|
// 付款水单
|
||||||
|
paymentProofUrl: record.paymentProofUrl,
|
||||||
// Bot 自动购买相关
|
// Bot 自动购买相关
|
||||||
sellerKavaAddress: record.sellerKavaAddress,
|
sellerKavaAddress: record.sellerKavaAddress,
|
||||||
botPurchased: record.botPurchased,
|
botPurchased: record.botPurchased,
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,7 @@ class ApiEndpoints {
|
||||||
static String c2cCancelOrder(String orderNo) => '/api/v2/trading/c2c/orders/$orderNo/cancel';
|
static String c2cCancelOrder(String orderNo) => '/api/v2/trading/c2c/orders/$orderNo/cancel';
|
||||||
static String c2cConfirmPayment(String orderNo) => '/api/v2/trading/c2c/orders/$orderNo/confirm-payment';
|
static String c2cConfirmPayment(String orderNo) => '/api/v2/trading/c2c/orders/$orderNo/confirm-payment';
|
||||||
static String c2cConfirmReceived(String orderNo) => '/api/v2/trading/c2c/orders/$orderNo/confirm-received';
|
static String c2cConfirmReceived(String orderNo) => '/api/v2/trading/c2c/orders/$orderNo/confirm-received';
|
||||||
|
static String c2cUploadProof(String orderNo) => '/api/v2/trading/c2c/orders/$orderNo/upload-proof';
|
||||||
|
|
||||||
// Contribution Service 2.0 (Kong路由: /api/v2/contribution -> /api/v1/contributions)
|
// Contribution Service 2.0 (Kong路由: /api/v2/contribution -> /api/v1/contributions)
|
||||||
static const String contributionStats = '/api/v2/contribution/stats';
|
static const String contributionStats = '/api/v2/contribution/stats';
|
||||||
|
|
|
||||||
|
|
@ -2,17 +2,17 @@ import 'package:intl/intl.dart';
|
||||||
|
|
||||||
String formatDateTime(DateTime? date) {
|
String formatDateTime(DateTime? date) {
|
||||||
if (date == null) return '-';
|
if (date == null) return '-';
|
||||||
return DateFormat('yyyy-MM-dd HH:mm:ss').format(date);
|
return DateFormat('yyyy-MM-dd HH:mm:ss').format(date.toLocal());
|
||||||
}
|
}
|
||||||
|
|
||||||
String formatDate(DateTime? date) {
|
String formatDate(DateTime? date) {
|
||||||
if (date == null) return '-';
|
if (date == null) return '-';
|
||||||
return DateFormat('yyyy-MM-dd').format(date);
|
return DateFormat('yyyy-MM-dd').format(date.toLocal());
|
||||||
}
|
}
|
||||||
|
|
||||||
String formatTime(DateTime? date) {
|
String formatTime(DateTime? date) {
|
||||||
if (date == null) return '-';
|
if (date == null) return '-';
|
||||||
return DateFormat('HH:mm:ss').format(date);
|
return DateFormat('HH:mm:ss').format(date.toLocal());
|
||||||
}
|
}
|
||||||
|
|
||||||
String formatRelative(DateTime? date) {
|
String formatRelative(DateTime? date) {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import '../../models/asset_display_model.dart';
|
||||||
import '../../models/kline_model.dart';
|
import '../../models/kline_model.dart';
|
||||||
import '../../models/p2p_transfer_model.dart';
|
import '../../models/p2p_transfer_model.dart';
|
||||||
import '../../models/p2p_transfer_fee_config_model.dart';
|
import '../../models/p2p_transfer_fee_config_model.dart';
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
import '../../models/c2c_order_model.dart';
|
import '../../models/c2c_order_model.dart';
|
||||||
import '../../../core/network/api_client.dart';
|
import '../../../core/network/api_client.dart';
|
||||||
import '../../../core/network/api_endpoints.dart';
|
import '../../../core/network/api_endpoints.dart';
|
||||||
|
|
@ -137,6 +138,9 @@ abstract class TradingRemoteDataSource {
|
||||||
|
|
||||||
/// 确认收款(卖方操作)
|
/// 确认收款(卖方操作)
|
||||||
Future<C2cOrderModel> confirmC2cReceived(String orderNo);
|
Future<C2cOrderModel> confirmC2cReceived(String orderNo);
|
||||||
|
|
||||||
|
/// 上传付款水单(买方操作)
|
||||||
|
Future<C2cOrderModel> uploadC2cPaymentProof(String orderNo, String filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
class TradingRemoteDataSourceImpl implements TradingRemoteDataSource {
|
class TradingRemoteDataSourceImpl implements TradingRemoteDataSource {
|
||||||
|
|
@ -581,4 +585,20 @@ class TradingRemoteDataSourceImpl implements TradingRemoteDataSource {
|
||||||
throw ServerException(e.toString());
|
throw ServerException(e.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<C2cOrderModel> uploadC2cPaymentProof(String orderNo, String filePath) async {
|
||||||
|
try {
|
||||||
|
final formData = FormData.fromMap({
|
||||||
|
'file': await MultipartFile.fromFile(filePath),
|
||||||
|
});
|
||||||
|
final response = await client.post(
|
||||||
|
ApiEndpoints.c2cUploadProof(orderNo),
|
||||||
|
data: formData,
|
||||||
|
);
|
||||||
|
return C2cOrderModel.fromJson(response.data);
|
||||||
|
} catch (e) {
|
||||||
|
throw ServerException(e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,8 @@ class C2cOrderModel {
|
||||||
final int confirmTimeoutMinutes; // 确认收款超时时间(分钟)
|
final int confirmTimeoutMinutes; // 确认收款超时时间(分钟)
|
||||||
final DateTime? paymentDeadline; // 付款截止时间
|
final DateTime? paymentDeadline; // 付款截止时间
|
||||||
final DateTime? confirmDeadline; // 确认收款截止时间
|
final DateTime? confirmDeadline; // 确认收款截止时间
|
||||||
|
// 付款水单
|
||||||
|
final String? paymentProofUrl; // 付款水单图片URL
|
||||||
// Bot 自动购买相关
|
// Bot 自动购买相关
|
||||||
final bool botPurchased; // 是否被 Bot 购买
|
final bool botPurchased; // 是否被 Bot 购买
|
||||||
final String? sellerKavaAddress; // 卖家 Kava 地址
|
final String? sellerKavaAddress; // 卖家 Kava 地址
|
||||||
|
|
@ -84,6 +86,8 @@ class C2cOrderModel {
|
||||||
this.confirmTimeoutMinutes = 60,
|
this.confirmTimeoutMinutes = 60,
|
||||||
this.paymentDeadline,
|
this.paymentDeadline,
|
||||||
this.confirmDeadline,
|
this.confirmDeadline,
|
||||||
|
// 付款水单
|
||||||
|
this.paymentProofUrl,
|
||||||
// Bot 自动购买相关
|
// Bot 自动购买相关
|
||||||
this.botPurchased = false,
|
this.botPurchased = false,
|
||||||
this.sellerKavaAddress,
|
this.sellerKavaAddress,
|
||||||
|
|
@ -127,6 +131,8 @@ class C2cOrderModel {
|
||||||
confirmDeadline: json['confirmDeadline'] != null
|
confirmDeadline: json['confirmDeadline'] != null
|
||||||
? DateTime.parse(json['confirmDeadline'])
|
? DateTime.parse(json['confirmDeadline'])
|
||||||
: null,
|
: null,
|
||||||
|
// 付款水单
|
||||||
|
paymentProofUrl: json['paymentProofUrl'],
|
||||||
// Bot 自动购买相关
|
// Bot 自动购买相关
|
||||||
botPurchased: json['botPurchased'] ?? false,
|
botPurchased: json['botPurchased'] ?? false,
|
||||||
sellerKavaAddress: json['sellerKavaAddress'],
|
sellerKavaAddress: json['sellerKavaAddress'],
|
||||||
|
|
|
||||||
|
|
@ -602,7 +602,8 @@ class _C2cMarketPageState extends ConsumerState<C2cMarketPage>
|
||||||
}
|
}
|
||||||
|
|
||||||
String _formatDateTime(DateTime dateTime) {
|
String _formatDateTime(DateTime dateTime) {
|
||||||
return '${dateTime.month}/${dateTime.day} ${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}';
|
final local = dateTime.toLocal();
|
||||||
|
return '${local.month}/${local.day} ${local.hour.toString().padLeft(2, '0')}:${local.minute.toString().padLeft(2, '0')}';
|
||||||
}
|
}
|
||||||
|
|
||||||
// #13 + #15: 接单对话框(支持部分成交数量输入 + BUY单收款信息输入)
|
// #13 + #15: 接单对话框(支持部分成交数量输入 + BUY单收款信息输入)
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
import '../../../core/utils/format_utils.dart';
|
import '../../../core/utils/format_utils.dart';
|
||||||
import '../../../data/models/c2c_order_model.dart';
|
import '../../../data/models/c2c_order_model.dart';
|
||||||
import '../../providers/c2c_providers.dart';
|
import '../../providers/c2c_providers.dart';
|
||||||
|
|
@ -133,6 +134,13 @@ class _C2cOrderDetailPageState extends ConsumerState<C2cOrderDetailPage> {
|
||||||
// 交易双方信息
|
// 交易双方信息
|
||||||
_buildParticipantsCard(order, isMaker, isTaker),
|
_buildParticipantsCard(order, isMaker, isTaker),
|
||||||
|
|
||||||
|
// 付款水单
|
||||||
|
if ((order.isMatched || order.isPaid || order.isCompleted) && !order.isBotPurchased)
|
||||||
|
...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_buildPaymentProofCard(order, isBuyer),
|
||||||
|
],
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// 操作说明
|
// 操作说明
|
||||||
|
|
@ -756,7 +764,8 @@ class _C2cOrderDetailPageState extends ConsumerState<C2cOrderDetailPage> {
|
||||||
label: order.isBuy ? '买方 (发布者)' : '卖方 (发布者)',
|
label: order.isBuy ? '买方 (发布者)' : '卖方 (发布者)',
|
||||||
name: order.makerNickname?.isNotEmpty == true
|
name: order.makerNickname?.isNotEmpty == true
|
||||||
? order.makerNickname!
|
? order.makerNickname!
|
||||||
: (order.makerPhone?.isNotEmpty == true ? _maskPhone(order.makerPhone!) : '未知用户'),
|
: (order.makerPhone?.isNotEmpty == true ? order.makerPhone! : '未知用户'),
|
||||||
|
phone: order.makerPhone,
|
||||||
isMe: isMaker,
|
isMe: isMaker,
|
||||||
),
|
),
|
||||||
const Divider(height: 24),
|
const Divider(height: 24),
|
||||||
|
|
@ -766,7 +775,8 @@ class _C2cOrderDetailPageState extends ConsumerState<C2cOrderDetailPage> {
|
||||||
label: order.isBuy ? '卖方 (接单者)' : '买方 (接单者)',
|
label: order.isBuy ? '卖方 (接单者)' : '买方 (接单者)',
|
||||||
name: order.takerNickname?.isNotEmpty == true
|
name: order.takerNickname?.isNotEmpty == true
|
||||||
? order.takerNickname!
|
? order.takerNickname!
|
||||||
: (order.takerPhone?.isNotEmpty == true ? _maskPhone(order.takerPhone!) : '未知用户'),
|
: (order.takerPhone?.isNotEmpty == true ? order.takerPhone! : '未知用户'),
|
||||||
|
phone: order.takerPhone,
|
||||||
isMe: isTaker,
|
isMe: isTaker,
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
|
|
@ -774,6 +784,13 @@ class _C2cOrderDetailPageState extends ConsumerState<C2cOrderDetailPage> {
|
||||||
'等待接单...',
|
'等待接单...',
|
||||||
style: TextStyle(fontSize: 14, color: _grayText),
|
style: TextStyle(fontSize: 14, color: _grayText),
|
||||||
),
|
),
|
||||||
|
// 对方收款信息
|
||||||
|
if (order.paymentAccount != null && order.takerAccountSequence != null) ...[
|
||||||
|
const Divider(height: 24),
|
||||||
|
_buildInfoRow('收款账号', order.paymentAccount!, canCopy: true),
|
||||||
|
if (order.paymentRealName != null)
|
||||||
|
_buildInfoRow('收款人', order.paymentRealName!),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -783,6 +800,7 @@ class _C2cOrderDetailPageState extends ConsumerState<C2cOrderDetailPage> {
|
||||||
required String label,
|
required String label,
|
||||||
required String name,
|
required String name,
|
||||||
required bool isMe,
|
required bool isMe,
|
||||||
|
String? phone,
|
||||||
}) {
|
}) {
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -844,6 +862,13 @@ class _C2cOrderDetailPageState extends ConsumerState<C2cOrderDetailPage> {
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
if (phone != null && phone.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
phone,
|
||||||
|
style: const TextStyle(fontSize: 12, color: _grayText),
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -851,6 +876,169 @@ class _C2cOrderDetailPageState extends ConsumerState<C2cOrderDetailPage> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildPaymentProofCard(C2cOrderModel order, bool isBuyer) {
|
||||||
|
final hasProof = order.paymentProofUrl != null && order.paymentProofUrl!.isNotEmpty;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.receipt_long, size: 20, color: _orange),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
const Text(
|
||||||
|
'付款水单',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: _darkText,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
if (isBuyer && (order.isMatched || order.isPaid))
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => _handleUploadProof(order),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _orange.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: const [
|
||||||
|
Icon(Icons.upload, size: 16, color: _orange),
|
||||||
|
SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
'上传水单',
|
||||||
|
style: TextStyle(fontSize: 12, color: _orange, fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
if (hasProof)
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => _showProofFullScreen(order.paymentProofUrl!),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
child: Image.network(
|
||||||
|
order.paymentProofUrl!,
|
||||||
|
height: 200,
|
||||||
|
width: double.infinity,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
errorBuilder: (_, __, ___) => Container(
|
||||||
|
height: 100,
|
||||||
|
color: _bgGray,
|
||||||
|
child: const Center(
|
||||||
|
child: Text('图片加载失败', style: TextStyle(color: _grayText)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Container(
|
||||||
|
height: 80,
|
||||||
|
width: double.infinity,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _bgGray,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: Colors.grey.shade300, style: BorderStyle.solid),
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.image_not_supported_outlined, size: 28, color: Colors.grey.shade400),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
isBuyer ? '请上传付款凭证' : '买方暂未上传水单',
|
||||||
|
style: TextStyle(fontSize: 12, color: Colors.grey.shade400),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _handleUploadProof(C2cOrderModel order) async {
|
||||||
|
final picker = ImagePicker();
|
||||||
|
final image = await picker.pickImage(
|
||||||
|
source: ImageSource.gallery,
|
||||||
|
maxWidth: 1920,
|
||||||
|
maxHeight: 1920,
|
||||||
|
imageQuality: 85,
|
||||||
|
);
|
||||||
|
if (image == null) return;
|
||||||
|
|
||||||
|
final notifier = ref.read(c2cTradingNotifierProvider.notifier);
|
||||||
|
final success = await notifier.uploadPaymentProof(order.orderNo, image.path);
|
||||||
|
|
||||||
|
if (success && mounted) {
|
||||||
|
ref.invalidate(c2cOrderDetailProvider(order.orderNo));
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('付款水单上传成功'),
|
||||||
|
backgroundColor: _green,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (mounted) {
|
||||||
|
final error = ref.read(c2cTradingNotifierProvider).error;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('上传失败: ${error ?? '未知错误'}'),
|
||||||
|
backgroundColor: _red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showProofFullScreen(String url) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => Dialog(
|
||||||
|
backgroundColor: Colors.black,
|
||||||
|
insetPadding: const EdgeInsets.all(0),
|
||||||
|
child: Stack(
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: [
|
||||||
|
InteractiveViewer(
|
||||||
|
child: Image.network(
|
||||||
|
url,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
errorBuilder: (_, __, ___) => const Center(
|
||||||
|
child: Text('图片加载失败', style: TextStyle(color: Colors.white)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
top: 40,
|
||||||
|
right: 16,
|
||||||
|
child: IconButton(
|
||||||
|
icon: const Icon(Icons.close, color: Colors.white, size: 28),
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildInstructionsCard(
|
Widget _buildInstructionsCard(
|
||||||
C2cOrderModel order,
|
C2cOrderModel order,
|
||||||
bool isMaker,
|
bool isMaker,
|
||||||
|
|
@ -1108,8 +1296,9 @@ class _C2cOrderDetailPageState extends ConsumerState<C2cOrderDetailPage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
String _formatDateTime(DateTime dateTime) {
|
String _formatDateTime(DateTime dateTime) {
|
||||||
return '${dateTime.year}-${dateTime.month.toString().padLeft(2, '0')}-${dateTime.day.toString().padLeft(2, '0')} '
|
final local = dateTime.toLocal();
|
||||||
'${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}';
|
return '${local.year}-${local.month.toString().padLeft(2, '0')}-${local.day.toString().padLeft(2, '0')} '
|
||||||
|
'${local.hour.toString().padLeft(2, '0')}:${local.minute.toString().padLeft(2, '0')}';
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _handleCancel(C2cOrderModel order) async {
|
Future<void> _handleCancel(C2cOrderModel order) async {
|
||||||
|
|
|
||||||
|
|
@ -192,9 +192,11 @@ class _C2cPublishPageState extends ConsumerState<C2cPublishPage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildBalanceCard(String availableShares, String availableCash) {
|
Widget _buildBalanceCard(String availableShares, String availableCash) {
|
||||||
// C2C交易对象是积分值,BUY和SELL都显示可用积分值
|
final isSell = _selectedType == 1;
|
||||||
const label = '可用积分值';
|
|
||||||
final balance = availableCash;
|
// 卖单:显示可用积分值(将被冻结)
|
||||||
|
// 买单:不需要显示余额(买方通过外部绿积分支付)
|
||||||
|
if (!isSell) return const SizedBox.shrink();
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
|
@ -206,12 +208,12 @@ class _C2cPublishPageState extends ConsumerState<C2cPublishPage> {
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
const Text(
|
||||||
label,
|
'可用积分值',
|
||||||
style: const TextStyle(fontSize: 14, color: _grayText),
|
style: TextStyle(fontSize: 14, color: _grayText),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
formatAmount(balance),
|
formatAmount(availableCash),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
|
@ -275,8 +277,9 @@ class _C2cPublishPageState extends ConsumerState<C2cPublishPage> {
|
||||||
String availableCash,
|
String availableCash,
|
||||||
String currentPrice,
|
String currentPrice,
|
||||||
) {
|
) {
|
||||||
// C2C交易的是积分值,BUY和SELL上限都是可用积分值
|
final isSell = _selectedType == 1;
|
||||||
final maxBalance = availableCash;
|
// 卖单:上限为可用积分值;买单:无上限(买方通过外部绿积分支付)
|
||||||
|
final maxBalance = isSell ? availableCash : null;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
|
@ -323,22 +326,24 @@ class _C2cPublishPageState extends ConsumerState<C2cPublishPage> {
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
borderSide: const BorderSide(color: _orange, width: 2),
|
borderSide: const BorderSide(color: _orange, width: 2),
|
||||||
),
|
),
|
||||||
suffixIcon: TextButton(
|
suffixIcon: maxBalance != null
|
||||||
onPressed: () {
|
? TextButton(
|
||||||
_isSyncingInput = true;
|
onPressed: () {
|
||||||
_quantityController.text = maxBalance;
|
_isSyncingInput = true;
|
||||||
_amountController.text = maxBalance;
|
_quantityController.text = maxBalance;
|
||||||
_isSyncingInput = false;
|
_amountController.text = maxBalance;
|
||||||
setState(() {});
|
_isSyncingInput = false;
|
||||||
},
|
setState(() {});
|
||||||
child: const Text(
|
},
|
||||||
'全部',
|
child: const Text(
|
||||||
style: TextStyle(
|
'全部',
|
||||||
color: _orange,
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.w500,
|
color: _orange,
|
||||||
),
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
suffixText: '积分值',
|
suffixText: '积分值',
|
||||||
suffixStyle: const TextStyle(color: _grayText, fontSize: 14),
|
suffixStyle: const TextStyle(color: _grayText, fontSize: 14),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -178,6 +178,19 @@ class C2cTradingNotifier extends StateNotifier<C2cTradingState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 上传付款水单(买方操作)
|
||||||
|
Future<bool> uploadPaymentProof(String orderNo, String filePath) async {
|
||||||
|
state = state.copyWith(isLoading: true, clearError: true);
|
||||||
|
try {
|
||||||
|
final order = await _dataSource.uploadC2cPaymentProof(orderNo, filePath);
|
||||||
|
state = state.copyWith(isLoading: false, lastOrder: order);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
state = state.copyWith(isLoading: false, error: e.toString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void clearError() {
|
void clearError() {
|
||||||
state = state.copyWith(clearError: true);
|
state = state.copyWith(clearError: true);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue