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(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(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": [],
"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)
contractNo String @unique @map("contract_no") @db.VarChar(30) // 合同编号: accountSequence-yyyyMMddHHmm
userId BigInt @map("user_id")
accountSequence String @map("account_sequence") @db.VarChar(20)

View File

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

View File

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

View File

@ -63,6 +63,7 @@ export interface SignContractParams {
export class ContractSigningTask {
private _id?: bigint;
private _orderNo: string;
private _contractNo: string;
private _userId: bigint;
private _accountSequence: string;
private _templateId: number;
@ -108,6 +109,8 @@ export class ContractSigningTask {
static create(params: CreateContractSigningTaskParams): ContractSigningTask {
const task = new ContractSigningTask();
task._orderNo = params.orderNo;
// 生成合同编号: accountSequence-yyyyMMddHHmm
task._contractNo = ContractSigningTask.generateContractNo(params.accountSequence);
task._userId = params.userId;
task._accountSequence = params.accountSequence;
task._templateId = params.templateId;
@ -133,6 +136,7 @@ export class ContractSigningTask {
static reconstitute(data: {
id: bigint;
orderNo: string;
contractNo: string;
userId: bigint;
accountSequence: string;
templateId: number;
@ -166,6 +170,7 @@ export class ContractSigningTask {
const task = new ContractSigningTask();
task._id = data.id;
task._orderNo = data.orderNo;
task._contractNo = data.contractNo;
task._userId = data.userId;
task._accountSequence = data.accountSequence;
task._templateId = data.templateId;
@ -210,6 +215,10 @@ export class ContractSigningTask {
return this._orderNo;
}
get contractNo(): string {
return this._contractNo;
}
get userId(): bigint {
return this._userId;
}
@ -447,4 +456,22 @@ export class ContractSigningTask {
}
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
*/
export interface ContractPdfData {
orderNo: string;
contractNo: string; // 合同编号: accountSequence-yyyyMMddHHmm
userRealName: string;
userIdCard: string;
userPhone: string;
@ -57,7 +57,7 @@ export class PdfGeneratorService {
* @returns PDF 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 {
// 1. 加载 PDF 模板
@ -81,9 +81,9 @@ export class PdfGeneratorService {
const fontSize = 12;
const textColor = rgb(0, 0, 0);
// 5. 填充第1页 - 协议编号
// 5. 填充第1页 - 协议编号(使用合同编号)
// "协议编号:" 后面的位置(根据 PDF 布局调整坐标)
page1.drawText(data.orderNo, {
page1.drawText(data.contractNo, {
x: 390,
y: 95,
size: fontSize,
@ -156,11 +156,11 @@ export class PdfGeneratorService {
// 9. 保存 PDF
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);
} catch (error) {
this.logger.error(
`Failed to generate PDF for order ${data.orderNo}:`,
`Failed to generate PDF for contract ${data.contractNo}:`,
error,
);
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({
data: {
orderNo: task.orderNo,
contractNo: task.contractNo,
userId: task.userId,
accountSequence: task.accountSequence,
templateId: task.templateId,
@ -188,6 +189,7 @@ export class ContractSigningTaskRepositoryImpl implements IContractSigningTaskRe
private mapToDomain(data: {
id: bigint;
orderNo: string;
contractNo: string;
userId: bigint;
accountSequence: string;
templateId: number;
@ -230,6 +232,7 @@ export class ContractSigningTaskRepositoryImpl implements IContractSigningTaskRe
return ContractSigningTask.reconstitute({
id: data.id,
orderNo: data.orderNo,
contractNo: data.contractNo,
userId: data.userId,
accountSequence: data.accountSequence,
templateId: data.templateId,

View File

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

View File

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