feat(contract): 使用合同编号代替订单号

合同编号格式: accountSequence-yyyyMMddHHmm
例如: 10001-202512251003

修改内容:
- 数据库: 添加 contract_no 字段
- 后端: 聚合根、Repository、Service、PDF生成器支持 contractNo
- 前端: 显示合同编号代替订单号

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2025-12-25 04:58:39 -08:00
parent 9cf1bbbbd3
commit 0e93d2a343
10 changed files with 64 additions and 11 deletions

View File

@ -404,7 +404,8 @@
"Bash(frontend/mobile-app/lib/features/home/presentation/pages/home_shell_page.dart )", "Bash(frontend/mobile-app/lib/features/home/presentation/pages/home_shell_page.dart )",
"Bash(git branch:*)", "Bash(git branch:*)",
"Bash(echo \"docker exec rwa-planting-service npx prisma db execute --stdin <<< \"\"SELECT template_id, version, title, is_active FROM contract_templates;\"\"\")", "Bash(echo \"docker exec rwa-planting-service npx prisma db execute --stdin <<< \"\"SELECT template_id, version, title, is_active FROM contract_templates;\"\"\")",
"Bash(npm uninstall:*)" "Bash(npm uninstall:*)",
"Bash(git commit -m \"$\\(cat <<''EOF''\nfeat\\(contract\\): 使用合同编号代替订单号\n\n合同编号格式: accountSequence-yyyyMMddHHmm\n例如: 10001-202512251003\n\n修改内容:\n- 数据库: 添加 contract_no 字段\n- 后端: 聚合根、Repository、Service、PDF生成器支持 contractNo\n- 前端: 显示合同编号代替订单号\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")"
], ],
"deny": [], "deny": [],
"ask": [] "ask": []

View File

@ -0,0 +1,16 @@
-- AlterTable: Add contract_no column to contract_signing_tasks
-- 合同编号格式: accountSequence-yyyyMMddHHmm (例如: 10001-202512251003)
-- 1. 首先添加可空的 contract_no 列
ALTER TABLE "contract_signing_tasks" ADD COLUMN "contract_no" VARCHAR(30);
-- 2. 为现有记录生成合同编号(基于 account_sequence 和 created_at
UPDATE "contract_signing_tasks"
SET "contract_no" = "account_sequence" || '-' || TO_CHAR("created_at", 'YYYYMMDDHH24MI')
WHERE "contract_no" IS NULL;
-- 3. 设置为 NOT NULL
ALTER TABLE "contract_signing_tasks" ALTER COLUMN "contract_no" SET NOT NULL;
-- 4. 添加唯一索引
CREATE UNIQUE INDEX "contract_signing_tasks_contract_no_key" ON "contract_signing_tasks"("contract_no");

View File

@ -315,6 +315,7 @@ model ContractSigningTask {
// 关联信息 // 关联信息
orderNo String @unique @map("order_no") @db.VarChar(50) orderNo String @unique @map("order_no") @db.VarChar(50)
contractNo String @unique @map("contract_no") @db.VarChar(30) // 合同编号: accountSequence-yyyyMMddHHmm
userId BigInt @map("user_id") userId BigInt @map("user_id")
accountSequence String @map("account_sequence") @db.VarChar(20) accountSequence String @map("account_sequence") @db.VarChar(20)

View File

@ -227,7 +227,7 @@ export class ContractSigningController {
// 5. 生成带签名的 PDF // 5. 生成带签名的 PDF
const signingDate = new Date().toISOString().split('T')[0]; const signingDate = new Date().toISOString().split('T')[0];
let pdfBuffer = await this.pdfGeneratorService.generateContractPdf({ let pdfBuffer = await this.pdfGeneratorService.generateContractPdf({
orderNo: task.orderNo, contractNo: task.contractNo,
userRealName: task.userRealName || '未认证', userRealName: task.userRealName || '未认证',
userIdCard: task.userIdCardNumber || '', userIdCard: task.userIdCardNumber || '',
userPhone: task.userPhoneNumber || '', userPhone: task.userPhoneNumber || '',
@ -317,7 +317,7 @@ export class ContractSigningController {
// 5. 生成带签名的 PDF // 5. 生成带签名的 PDF
const signingDate = new Date().toISOString().split('T')[0]; const signingDate = new Date().toISOString().split('T')[0];
let pdfBuffer = await this.pdfGeneratorService.generateContractPdf({ let pdfBuffer = await this.pdfGeneratorService.generateContractPdf({
orderNo: task.orderNo, contractNo: task.contractNo,
userRealName: task.userRealName || '未认证', userRealName: task.userRealName || '未认证',
userIdCard: task.userIdCardNumber || '', userIdCard: task.userIdCardNumber || '',
userPhone: task.userPhoneNumber || '', userPhone: task.userPhoneNumber || '',
@ -413,7 +413,7 @@ export class ContractSigningController {
// 生成 PDF使用 pdf-lib 直接操作 PDF 模板) // 生成 PDF使用 pdf-lib 直接操作 PDF 模板)
const pdfBuffer = await this.pdfGeneratorService.generateContractPdf({ const pdfBuffer = await this.pdfGeneratorService.generateContractPdf({
orderNo: task.orderNo, contractNo: task.contractNo,
userRealName: task.userRealName || '未认证', userRealName: task.userRealName || '未认证',
userIdCard: task.userIdCardNumber || '', userIdCard: task.userIdCardNumber || '',
userPhone: task.userPhoneNumber || '', userPhone: task.userPhoneNumber || '',

View File

@ -35,6 +35,7 @@ export interface CreateSigningTaskParams {
* DTO * DTO
*/ */
export interface ContractSigningTaskDto { export interface ContractSigningTaskDto {
contractNo: string;
orderNo: string; orderNo: string;
contractVersion: string; contractVersion: string;
contractContent: string; contractContent: string;
@ -310,6 +311,7 @@ export class ContractSigningService {
private toDto(task: ContractSigningTask): ContractSigningTaskDto { private toDto(task: ContractSigningTask): ContractSigningTaskDto {
return { return {
orderNo: task.orderNo, orderNo: task.orderNo,
contractNo: task.contractNo,
contractVersion: task.contractVersion, contractVersion: task.contractVersion,
contractContent: task.contractContent, contractContent: task.contractContent,
status: task.status, status: task.status,

View File

@ -63,6 +63,7 @@ export interface SignContractParams {
export class ContractSigningTask { export class ContractSigningTask {
private _id?: bigint; private _id?: bigint;
private _orderNo: string; private _orderNo: string;
private _contractNo: string;
private _userId: bigint; private _userId: bigint;
private _accountSequence: string; private _accountSequence: string;
private _templateId: number; private _templateId: number;
@ -108,6 +109,8 @@ export class ContractSigningTask {
static create(params: CreateContractSigningTaskParams): ContractSigningTask { static create(params: CreateContractSigningTaskParams): ContractSigningTask {
const task = new ContractSigningTask(); const task = new ContractSigningTask();
task._orderNo = params.orderNo; task._orderNo = params.orderNo;
// 生成合同编号: accountSequence-yyyyMMddHHmm
task._contractNo = ContractSigningTask.generateContractNo(params.accountSequence);
task._userId = params.userId; task._userId = params.userId;
task._accountSequence = params.accountSequence; task._accountSequence = params.accountSequence;
task._templateId = params.templateId; task._templateId = params.templateId;
@ -133,6 +136,7 @@ export class ContractSigningTask {
static reconstitute(data: { static reconstitute(data: {
id: bigint; id: bigint;
orderNo: string; orderNo: string;
contractNo: string;
userId: bigint; userId: bigint;
accountSequence: string; accountSequence: string;
templateId: number; templateId: number;
@ -166,6 +170,7 @@ export class ContractSigningTask {
const task = new ContractSigningTask(); const task = new ContractSigningTask();
task._id = data.id; task._id = data.id;
task._orderNo = data.orderNo; task._orderNo = data.orderNo;
task._contractNo = data.contractNo;
task._userId = data.userId; task._userId = data.userId;
task._accountSequence = data.accountSequence; task._accountSequence = data.accountSequence;
task._templateId = data.templateId; task._templateId = data.templateId;
@ -210,6 +215,10 @@ export class ContractSigningTask {
return this._orderNo; return this._orderNo;
} }
get contractNo(): string {
return this._contractNo;
}
get userId(): bigint { get userId(): bigint {
return this._userId; return this._userId;
} }
@ -447,4 +456,22 @@ export class ContractSigningTask {
} }
this._updatedAt = new Date(); this._updatedAt = new Date();
} }
// ============================================
// 私有辅助方法
// ============================================
/**
*
* 格式: accountSequence-yyyyMMddHHmm
*/
private static generateContractNo(accountSequence: string): string {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const hour = String(now.getHours()).padStart(2, '0');
const minute = String(now.getMinutes()).padStart(2, '0');
return `${accountSequence}-${year}${month}${day}${hour}${minute}`;
}
} }

View File

@ -10,7 +10,7 @@ const fontkit = require('@pdf-lib/fontkit');
* PDF * PDF
*/ */
export interface ContractPdfData { export interface ContractPdfData {
orderNo: string; contractNo: string; // 合同编号: accountSequence-yyyyMMddHHmm
userRealName: string; userRealName: string;
userIdCard: string; userIdCard: string;
userPhone: string; userPhone: string;
@ -57,7 +57,7 @@ export class PdfGeneratorService {
* @returns PDF Buffer * @returns PDF Buffer
*/ */
async generateContractPdf(data: ContractPdfData): Promise<Buffer> { async generateContractPdf(data: ContractPdfData): Promise<Buffer> {
this.logger.log(`Generating PDF for order: ${data.orderNo}`); this.logger.log(`Generating PDF for contract: ${data.contractNo}`);
try { try {
// 1. 加载 PDF 模板 // 1. 加载 PDF 模板
@ -81,9 +81,9 @@ export class PdfGeneratorService {
const fontSize = 12; const fontSize = 12;
const textColor = rgb(0, 0, 0); const textColor = rgb(0, 0, 0);
// 5. 填充第1页 - 协议编号 // 5. 填充第1页 - 协议编号(使用合同编号)
// "协议编号:" 后面的位置(根据 PDF 布局调整坐标) // "协议编号:" 后面的位置(根据 PDF 布局调整坐标)
page1.drawText(data.orderNo, { page1.drawText(data.contractNo, {
x: 390, x: 390,
y: 95, y: 95,
size: fontSize, size: fontSize,
@ -156,11 +156,11 @@ export class PdfGeneratorService {
// 9. 保存 PDF // 9. 保存 PDF
const pdfBytes = await pdfDoc.save(); const pdfBytes = await pdfDoc.save();
this.logger.log(`PDF generated successfully for order: ${data.orderNo}`); this.logger.log(`PDF generated successfully for contract: ${data.contractNo}`);
return Buffer.from(pdfBytes); return Buffer.from(pdfBytes);
} catch (error) { } catch (error) {
this.logger.error( this.logger.error(
`Failed to generate PDF for order ${data.orderNo}:`, `Failed to generate PDF for contract ${data.contractNo}:`,
error, error,
); );
throw new Error(`PDF generation failed: ${error.message}`); throw new Error(`PDF generation failed: ${error.message}`);

View File

@ -59,6 +59,7 @@ export class ContractSigningTaskRepositoryImpl implements IContractSigningTaskRe
const result = await this.prisma.contractSigningTask.create({ const result = await this.prisma.contractSigningTask.create({
data: { data: {
orderNo: task.orderNo, orderNo: task.orderNo,
contractNo: task.contractNo,
userId: task.userId, userId: task.userId,
accountSequence: task.accountSequence, accountSequence: task.accountSequence,
templateId: task.templateId, templateId: task.templateId,
@ -188,6 +189,7 @@ export class ContractSigningTaskRepositoryImpl implements IContractSigningTaskRe
private mapToDomain(data: { private mapToDomain(data: {
id: bigint; id: bigint;
orderNo: string; orderNo: string;
contractNo: string;
userId: bigint; userId: bigint;
accountSequence: string; accountSequence: string;
templateId: number; templateId: number;
@ -230,6 +232,7 @@ export class ContractSigningTaskRepositoryImpl implements IContractSigningTaskRe
return ContractSigningTask.reconstitute({ return ContractSigningTask.reconstitute({
id: data.id, id: data.id,
orderNo: data.orderNo, orderNo: data.orderNo,
contractNo: data.contractNo,
userId: data.userId, userId: data.userId,
accountSequence: data.accountSequence, accountSequence: data.accountSequence,
templateId: data.templateId, templateId: data.templateId,

View File

@ -14,6 +14,7 @@ enum ContractSigningStatus {
/// ///
class ContractSigningTask { class ContractSigningTask {
final String orderNo; final String orderNo;
final String contractNo;
final String accountSequence; final String accountSequence;
final ContractSigningStatus status; final ContractSigningStatus status;
final String contractVersion; final String contractVersion;
@ -31,6 +32,7 @@ class ContractSigningTask {
ContractSigningTask({ ContractSigningTask({
required this.orderNo, required this.orderNo,
required this.contractNo,
required this.accountSequence, required this.accountSequence,
required this.status, required this.status,
required this.contractVersion, required this.contractVersion,
@ -50,6 +52,7 @@ class ContractSigningTask {
factory ContractSigningTask.fromJson(Map<String, dynamic> json) { factory ContractSigningTask.fromJson(Map<String, dynamic> json) {
return ContractSigningTask( return ContractSigningTask(
orderNo: json['orderNo'] ?? '', orderNo: json['orderNo'] ?? '',
contractNo: json['contractNo'] ?? '',
accountSequence: json['accountSequence'] ?? '', accountSequence: json['accountSequence'] ?? '',
status: _parseStatus(json['status']), status: _parseStatus(json['status']),
contractVersion: json['contractVersion'] ?? '', contractVersion: json['contractVersion'] ?? '',

View File

@ -772,7 +772,7 @@ class _ContractSigningPageState extends ConsumerState<ContractSigningPage> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
'订单: ${_task!.orderNo}', '合同编号: ${_task!.contractNo}',
style: const TextStyle( style: const TextStyle(
fontSize: 12, fontSize: 12,
color: Color(0xFF666666), color: Color(0xFF666666),