feat(planting-service): 实现合同签名和PDF云存储功能

- 添加 MinIO 存储服务,支持上传签名图片和已签署 PDF
- 添加 signedPdfUrl 字段到数据库模型
- 修改签署流程:生成 PDF、嵌入签名、上传到云存储
- 修复前端签署 API 响应处理

🤖 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 03:35:15 -08:00
parent a2f021fe94
commit c657fb5a19
11 changed files with 733 additions and 29 deletions

View File

@ -273,6 +273,14 @@ services:
- WALLET_SERVICE_URL=http://rwa-wallet-service:3001
- IDENTITY_SERVICE_URL=http://rwa-identity-service:3000
- REFERRAL_SERVICE_URL=http://rwa-referral-service:3004
# MinIO Object Storage - 用于合同签名和PDF存储
- MINIO_ENDPOINT=${MINIO_ENDPOINT:-192.168.1.100}
- MINIO_PORT=${MINIO_PORT:-9000}
- MINIO_USE_SSL=${MINIO_USE_SSL:-false}
- MINIO_ACCESS_KEY=${MINIO_ACCESS_KEY:-admin}
- MINIO_SECRET_KEY=${MINIO_SECRET_KEY:-minio_secret_password}
- MINIO_BUCKET_CONTRACTS=${MINIO_BUCKET_CONTRACTS:-contracts}
- MINIO_PUBLIC_URL=${MINIO_PUBLIC_URL:-https://minio.szaiai.com}
depends_on:
postgres:
condition: service_healthy

View File

@ -25,6 +25,7 @@
"class-validator": "^0.14.0",
"jsonwebtoken": "^9.0.0",
"kafkajs": "^2.2.4",
"minio": "^8.0.6",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"pdf-lib": "^1.17.1",
@ -39,6 +40,7 @@
"@types/express": "^4.17.17",
"@types/jest": "^29.5.2",
"@types/jsonwebtoken": "^9.0.0",
"@types/minio": "^7.1.0",
"@types/node": "^20.3.1",
"@types/passport-jwt": "^4.0.0",
"@types/supertest": "^2.0.12",
@ -2573,6 +2575,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/minio": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@types/minio/-/minio-7.1.0.tgz",
"integrity": "sha512-yUaFE62KI7lhQLH7lQpT1bzDmTOS+8bD7wGN8ySk8teLv9afbL7RIT3tSbeijq6sqeyS9kGpj9Kmp4gHNQJWCw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/ms": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
@ -3117,6 +3129,13 @@
"dev": true,
"license": "Apache-2.0"
},
"node_modules/@zxing/text-encoding": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz",
"integrity": "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==",
"license": "(Unlicense OR Apache-2.0)",
"optional": true
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@ -3369,12 +3388,33 @@
"dev": true,
"license": "MIT"
},
"node_modules/async": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
"license": "MIT"
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/available-typed-arrays": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
"integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
"license": "MIT",
"dependencies": {
"possible-typed-array-names": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/axios": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
@ -3576,6 +3616,15 @@
"readable-stream": "^3.4.0"
}
},
"node_modules/block-stream2": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/block-stream2/-/block-stream2-2.1.0.tgz",
"integrity": "sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg==",
"license": "MIT",
"dependencies": {
"readable-stream": "^3.4.0"
}
},
"node_modules/body-parser": {
"version": "1.20.3",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
@ -3638,6 +3687,12 @@
"node": ">=8"
}
},
"node_modules/browser-or-node": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/browser-or-node/-/browser-or-node-2.1.1.tgz",
"integrity": "sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==",
"license": "MIT"
},
"node_modules/browserslist": {
"version": "4.28.0",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz",
@ -3721,6 +3776,15 @@
"ieee754": "^1.1.13"
}
},
"node_modules/buffer-crc32": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz",
"integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==",
"license": "MIT",
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
@ -3757,7 +3821,6 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
"integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.0",
@ -4328,6 +4391,15 @@
}
}
},
"node_modules/decode-uri-component": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz",
"integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==",
"license": "MIT",
"engines": {
"node": ">=0.10"
}
},
"node_modules/dedent": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz",
@ -4377,7 +4449,6 @@
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0",
@ -4965,6 +5036,12 @@
"node": ">= 0.6"
}
},
"node_modules/eventemitter3": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
"license": "MIT"
},
"node_modules/events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
@ -5165,6 +5242,24 @@
"integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==",
"license": "MIT"
},
"node_modules/fast-xml-parser": {
"version": "4.5.3",
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz",
"integrity": "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT",
"dependencies": {
"strnum": "^1.1.1"
},
"bin": {
"fxparser": "src/cli/cli.js"
}
},
"node_modules/fastq": {
"version": "1.19.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
@ -5261,6 +5356,15 @@
"node": ">=8"
}
},
"node_modules/filter-obj": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz",
"integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/finalhandler": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
@ -5353,6 +5457,21 @@
}
}
},
"node_modules/for-each": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
"integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
"license": "MIT",
"dependencies": {
"is-callable": "^1.2.7"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/foreground-child": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
@ -5526,6 +5645,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/generator-function": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz",
"integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/gensync": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@ -5781,7 +5909,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0"
@ -6005,6 +6132,22 @@
"node": ">= 0.10"
}
},
"node_modules/is-arguments": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz",
"integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"has-tostringtag": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-arrayish": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
@ -6025,6 +6168,18 @@
"node": ">=8"
}
},
"node_modules/is-callable": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
"integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-core-module": {
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
@ -6071,6 +6226,25 @@
"node": ">=6"
}
},
"node_modules/is-generator-function": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz",
"integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.4",
"generator-function": "^2.0.0",
"get-proto": "^1.0.1",
"has-tostringtag": "^1.0.2",
"safe-regex-test": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
@ -6114,6 +6288,24 @@
"node": ">=8"
}
},
"node_modules/is-regex": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
"integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"gopd": "^1.2.0",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-stream": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
@ -6127,6 +6319,21 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-typed-array": {
"version": "1.1.15",
"resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
"integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
"license": "MIT",
"dependencies": {
"which-typed-array": "^1.1.16"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-unicode-supported": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
@ -7501,6 +7708,40 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/minio": {
"version": "8.0.6",
"resolved": "https://registry.npmjs.org/minio/-/minio-8.0.6.tgz",
"integrity": "sha512-sOeh2/b/XprRmEtYsnNRFtOqNRTPDvYtMWh+spWlfsuCV/+IdxNeKVUMKLqI7b5Dr07ZqCPuaRGU/rB9pZYVdQ==",
"license": "Apache-2.0",
"dependencies": {
"async": "^3.2.4",
"block-stream2": "^2.1.0",
"browser-or-node": "^2.1.1",
"buffer-crc32": "^1.0.0",
"eventemitter3": "^5.0.1",
"fast-xml-parser": "^4.4.1",
"ipaddr.js": "^2.0.1",
"lodash": "^4.17.21",
"mime-types": "^2.1.35",
"query-string": "^7.1.3",
"stream-json": "^1.8.0",
"through2": "^4.0.2",
"web-encoding": "^1.1.5",
"xml2js": "^0.5.0 || ^0.6.2"
},
"engines": {
"node": "^16 || ^18 || >=20"
}
},
"node_modules/minio/node_modules/ipaddr.js": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz",
"integrity": "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==",
"license": "MIT",
"engines": {
"node": ">= 10"
}
},
"node_modules/minipass": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
@ -8104,6 +8345,15 @@
"node": ">=4"
}
},
"node_modules/possible-typed-array-names": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
"integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@ -8268,6 +8518,24 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/query-string": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz",
"integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==",
"license": "MIT",
"dependencies": {
"decode-uri-component": "^0.2.2",
"filter-obj": "^1.1.0",
"split-on-first": "^1.0.0",
"strict-uri-encode": "^2.0.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@ -8630,12 +8898,35 @@
],
"license": "MIT"
},
"node_modules/safe-regex-test": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
"integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"is-regex": "^1.2.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/sax": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.3.tgz",
"integrity": "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==",
"license": "BlueOak-1.0.0"
},
"node_modules/schema-utils": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
@ -8779,7 +9070,6 @@
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
"dev": true,
"license": "MIT",
"dependencies": {
"define-data-property": "^1.1.4",
@ -8955,6 +9245,15 @@
"node": ">=0.10.0"
}
},
"node_modules/split-on-first": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz",
"integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
@ -8994,6 +9293,21 @@
"node": ">= 0.8"
}
},
"node_modules/stream-chain": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-2.2.5.tgz",
"integrity": "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==",
"license": "BSD-3-Clause"
},
"node_modules/stream-json": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/stream-json/-/stream-json-1.9.1.tgz",
"integrity": "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==",
"license": "BSD-3-Clause",
"dependencies": {
"stream-chain": "^2.2.5"
}
},
"node_modules/streamsearch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
@ -9002,6 +9316,15 @@
"node": ">=10.0.0"
}
},
"node_modules/strict-uri-encode": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz",
"integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
@ -9116,6 +9439,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/strnum": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz",
"integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT"
},
"node_modules/strtok3": {
"version": "10.3.4",
"resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.4.tgz",
@ -9441,6 +9776,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/through2": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz",
"integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==",
"license": "MIT",
"dependencies": {
"readable-stream": "3"
}
},
"node_modules/tmp": {
"version": "0.0.33",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
@ -9883,6 +10227,19 @@
"punycode": "^2.1.0"
}
},
"node_modules/util": {
"version": "0.12.5",
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
"integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"is-arguments": "^1.0.4",
"is-generator-function": "^1.0.7",
"is-typed-array": "^1.1.3",
"which-typed-array": "^1.1.2"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@ -9985,6 +10342,18 @@
"defaults": "^1.0.3"
}
},
"node_modules/web-encoding": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/web-encoding/-/web-encoding-1.1.5.tgz",
"integrity": "sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==",
"license": "MIT",
"dependencies": {
"util": "^0.12.3"
},
"optionalDependencies": {
"@zxing/text-encoding": "0.9.0"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
@ -10131,6 +10500,27 @@
"node": ">= 8"
}
},
"node_modules/which-typed-array": {
"version": "1.1.19",
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz",
"integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==",
"license": "MIT",
"dependencies": {
"available-typed-arrays": "^1.0.7",
"call-bind": "^1.0.8",
"call-bound": "^1.0.4",
"for-each": "^0.3.5",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-tostringtag": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/word-wrap": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
@ -10210,6 +10600,28 @@
"dev": true,
"license": "ISC"
},
"node_modules/xml2js": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz",
"integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==",
"license": "MIT",
"dependencies": {
"sax": ">=0.6.0",
"xmlbuilder": "~11.0.0"
},
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/xmlbuilder": {
"version": "11.0.1",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
"license": "MIT",
"engines": {
"node": ">=4.0"
}
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",

View File

@ -40,6 +40,7 @@
"class-validator": "^0.14.0",
"jsonwebtoken": "^9.0.0",
"kafkajs": "^2.2.4",
"minio": "^8.0.6",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"pdf-lib": "^1.17.1",
@ -54,6 +55,7 @@
"@types/express": "^4.17.17",
"@types/jest": "^29.5.2",
"@types/jsonwebtoken": "^9.0.0",
"@types/minio": "^7.1.0",
"@types/node": "^20.3.1",
"@types/passport-jwt": "^4.0.0",
"@types/supertest": "^2.0.12",

View File

@ -0,0 +1,2 @@
-- AlterTable: Add signed_pdf_url column to contract_signing_tasks
ALTER TABLE "contract_signing_tasks" ADD COLUMN "signed_pdf_url" VARCHAR(500);

View File

@ -353,6 +353,7 @@ model ContractSigningTask {
// 签名数据
signatureCloudUrl String? @map("signature_cloud_url") @db.VarChar(500)
signatureHash String? @map("signature_hash") @db.VarChar(64) // SHA256
signedPdfUrl String? @map("signed_pdf_url") @db.VarChar(500) // 已签署合同PDF的云存储URL
// 法律合规证据链
signingIpAddress String? @map("signing_ip_address") @db.VarChar(50)

View File

@ -17,6 +17,8 @@ import { Response } from 'express';
import { JwtAuthGuard } from '../guards/jwt-auth.guard';
import { ContractSigningService } from '../../application/services/contract-signing.service';
import { PdfGeneratorService } from '../../infrastructure/pdf/pdf-generator.service';
import { MinioStorageService } from '../../infrastructure/storage/minio-storage.service';
import * as crypto from 'crypto';
/**
* DTO
@ -77,6 +79,7 @@ export class ContractSigningController {
constructor(
private readonly contractSigningService: ContractSigningService,
private readonly pdfGeneratorService: PdfGeneratorService,
private readonly minioStorageService: MinioStorageService,
) {}
/**
@ -197,13 +200,63 @@ export class ContractSigningController {
const userAgent = req.headers['user-agent'] || 'unknown';
try {
// TODO: 上传签名图片到云存储获取URL
// 目前暂时使用base64数据作为URL占位
const signatureCloudUrl = `data:image/png;base64,${dto.signatureBase64.slice(0, 100)}...`;
// 1. 获取任务详情
const task = await this.contractSigningService.getTask(orderNo, userId);
if (!task) {
throw new Error('签署任务不存在');
}
// 2. 解码签名图片
const signatureBuffer = Buffer.from(dto.signatureBase64, 'base64');
this.logger.log(`Signature size: ${signatureBuffer.length} bytes for order ${orderNo}`);
// 3. 计算签名哈希(如果前端没有提供)
const signatureHash = dto.signatureHash || crypto.createHash('sha256').update(signatureBuffer).digest('hex');
// 4. 上传签名图片到 MinIO
let signatureCloudUrl: string;
try {
signatureCloudUrl = await this.minioStorageService.uploadSignature(orderNo, signatureBuffer);
this.logger.log(`Signature uploaded to: ${signatureCloudUrl}`);
} catch (uploadError) {
this.logger.warn(`Failed to upload signature, using placeholder: ${uploadError.message}`);
// 如果上传失败,使用占位 URL允许服务继续工作
signatureCloudUrl = `data:image/png;base64,${dto.signatureBase64.slice(0, 100)}...`;
}
// 5. 生成带签名的 PDF
const signingDate = new Date().toISOString().split('T')[0];
let pdfBuffer = await this.pdfGeneratorService.generateContractPdf({
orderNo: task.orderNo,
userRealName: task.userRealName || '未认证',
userIdCard: task.userIdCardNumber || '',
userPhone: task.userPhoneNumber || '',
treeCount: task.treeCount,
signingDate,
});
// 6. 嵌入签名到 PDF
pdfBuffer = await this.pdfGeneratorService.embedSignature(pdfBuffer, {
signatureImagePng: signatureBuffer,
});
this.logger.log(`Signed PDF generated: ${pdfBuffer.length} bytes`);
// 7. 上传签署后的 PDF 到 MinIO
let signedPdfUrl: string;
try {
signedPdfUrl = await this.minioStorageService.uploadSignedPdf(orderNo, pdfBuffer);
this.logger.log(`Signed PDF uploaded to: ${signedPdfUrl}`);
} catch (uploadError) {
this.logger.warn(`Failed to upload signed PDF: ${uploadError.message}`);
// 如果上传失败,使用空 URL数据库允许 null
signedPdfUrl = '';
}
// 8. 完成签署
await this.contractSigningService.signContract(orderNo, userId, {
signatureCloudUrl,
signatureHash: dto.signatureHash,
signatureHash,
signedPdfUrl,
ipAddress,
deviceInfo: dto.deviceInfo,
userAgent,
@ -238,12 +291,61 @@ export class ContractSigningController {
const userAgent = req.headers['user-agent'] || 'unknown';
try {
// TODO: 上传签名图片到云存储获取URL
const signatureCloudUrl = `data:image/png;base64,${dto.signatureBase64.slice(0, 100)}...`;
// 1. 获取任务详情
const task = await this.contractSigningService.getTask(orderNo, userId);
if (!task) {
throw new Error('签署任务不存在');
}
// 2. 解码签名图片
const signatureBuffer = Buffer.from(dto.signatureBase64, 'base64');
this.logger.log(`Signature size: ${signatureBuffer.length} bytes for late-sign order ${orderNo}`);
// 3. 计算签名哈希(如果前端没有提供)
const signatureHash = dto.signatureHash || crypto.createHash('sha256').update(signatureBuffer).digest('hex');
// 4. 上传签名图片到 MinIO
let signatureCloudUrl: string;
try {
signatureCloudUrl = await this.minioStorageService.uploadSignature(orderNo, signatureBuffer);
this.logger.log(`Signature uploaded to: ${signatureCloudUrl}`);
} catch (uploadError) {
this.logger.warn(`Failed to upload signature, using placeholder: ${uploadError.message}`);
signatureCloudUrl = `data:image/png;base64,${dto.signatureBase64.slice(0, 100)}...`;
}
// 5. 生成带签名的 PDF
const signingDate = new Date().toISOString().split('T')[0];
let pdfBuffer = await this.pdfGeneratorService.generateContractPdf({
orderNo: task.orderNo,
userRealName: task.userRealName || '未认证',
userIdCard: task.userIdCardNumber || '',
userPhone: task.userPhoneNumber || '',
treeCount: task.treeCount,
signingDate,
});
// 6. 嵌入签名到 PDF
pdfBuffer = await this.pdfGeneratorService.embedSignature(pdfBuffer, {
signatureImagePng: signatureBuffer,
});
this.logger.log(`Signed PDF generated: ${pdfBuffer.length} bytes`);
// 7. 上传签署后的 PDF 到 MinIO
let signedPdfUrl: string;
try {
signedPdfUrl = await this.minioStorageService.uploadSignedPdf(orderNo, pdfBuffer);
this.logger.log(`Signed PDF uploaded to: ${signedPdfUrl}`);
} catch (uploadError) {
this.logger.warn(`Failed to upload signed PDF: ${uploadError.message}`);
signedPdfUrl = '';
}
// 8. 完成补签
await this.contractSigningService.lateSignContract(orderNo, userId, {
signatureCloudUrl,
signatureHash: dto.signatureHash,
signatureHash,
signedPdfUrl,
ipAddress,
deviceInfo: dto.deviceInfo,
userAgent,

View File

@ -53,6 +53,7 @@ export interface CreateContractSigningTaskParams {
export interface SignContractParams {
signatureCloudUrl: string;
signatureHash: string;
signedPdfUrl: string;
ipAddress: string;
deviceInfo: DeviceInfo;
userAgent: string;
@ -83,6 +84,7 @@ export class ContractSigningTask {
private _signedAt?: Date;
private _signatureCloudUrl?: string;
private _signatureHash?: string;
private _signedPdfUrl?: string;
private _signingIpAddress?: string;
private _signingDeviceInfo?: DeviceInfo;
private _signingUserAgent?: string;
@ -152,6 +154,7 @@ export class ContractSigningTask {
signedAt?: Date;
signatureCloudUrl?: string;
signatureHash?: string;
signedPdfUrl?: string;
signingIpAddress?: string;
signingDeviceInfo?: DeviceInfo;
signingUserAgent?: string;
@ -184,6 +187,7 @@ export class ContractSigningTask {
task._signedAt = data.signedAt;
task._signatureCloudUrl = data.signatureCloudUrl;
task._signatureHash = data.signatureHash;
task._signedPdfUrl = data.signedPdfUrl;
task._signingIpAddress = data.signingIpAddress;
task._signingDeviceInfo = data.signingDeviceInfo;
task._signingUserAgent = data.signingUserAgent;
@ -290,6 +294,10 @@ export class ContractSigningTask {
return this._signatureHash;
}
get signedPdfUrl(): string | undefined {
return this._signedPdfUrl;
}
get signingIpAddress(): string | undefined {
return this._signingIpAddress;
}
@ -391,6 +399,7 @@ export class ContractSigningTask {
this._signedAt = new Date();
this._signatureCloudUrl = params.signatureCloudUrl;
this._signatureHash = params.signatureHash;
this._signedPdfUrl = params.signedPdfUrl;
this._signingIpAddress = params.ipAddress;
this._signingDeviceInfo = params.deviceInfo;
this._signingUserAgent = params.userAgent;
@ -428,6 +437,7 @@ export class ContractSigningTask {
this._signedAt = new Date();
this._signatureCloudUrl = params.signatureCloudUrl;
this._signatureHash = params.signatureHash;
this._signedPdfUrl = params.signedPdfUrl;
this._signingIpAddress = params.ipAddress;
this._signingDeviceInfo = params.deviceInfo;
this._signingUserAgent = params.userAgent;

View File

@ -16,6 +16,7 @@ import { OutboxPublisherService } from './kafka/outbox-publisher.service';
import { EventAckController } from './kafka/event-ack.controller';
import { ContractSigningEventConsumer } from './kafka/contract-signing-event.consumer';
import { PdfGeneratorService } from './pdf/pdf-generator.service';
import { MinioStorageService } from './storage/minio-storage.service';
import { PLANTING_ORDER_REPOSITORY } from '../domain/repositories/planting-order.repository.interface';
import { PLANTING_POSITION_REPOSITORY } from '../domain/repositories/planting-position.repository.interface';
import { POOL_INJECTION_BATCH_REPOSITORY } from '../domain/repositories/pool-injection-batch.repository.interface';
@ -66,6 +67,7 @@ import { ContractSigningService } from '../application/services/contract-signing
PaymentCompensationService,
ContractSigningService,
PdfGeneratorService,
MinioStorageService,
WalletServiceClient,
ReferralServiceClient,
],
@ -83,6 +85,7 @@ import { ContractSigningService } from '../application/services/contract-signing
PaymentCompensationService,
ContractSigningService,
PdfGeneratorService,
MinioStorageService,
WalletServiceClient,
ReferralServiceClient,
],

View File

@ -28,6 +28,7 @@ export class ContractSigningTaskRepositoryImpl implements IContractSigningTaskRe
signedAt: task.signedAt,
signatureCloudUrl: task.signatureCloudUrl,
signatureHash: task.signatureHash,
signedPdfUrl: task.signedPdfUrl,
signingIpAddress: task.signingIpAddress,
signingDeviceInfo: deviceInfoJson,
signingUserAgent: task.signingUserAgent,
@ -193,6 +194,7 @@ export class ContractSigningTaskRepositoryImpl implements IContractSigningTaskRe
signedAt: Date | null;
signatureCloudUrl: string | null;
signatureHash: string | null;
signedPdfUrl: string | null;
signingIpAddress: string | null;
signingDeviceInfo: string | null;
signingUserAgent: string | null;
@ -234,6 +236,7 @@ export class ContractSigningTaskRepositoryImpl implements IContractSigningTaskRe
signedAt: data.signedAt ?? undefined,
signatureCloudUrl: data.signatureCloudUrl ?? undefined,
signatureHash: data.signatureHash ?? undefined,
signedPdfUrl: data.signedPdfUrl ?? undefined,
signingIpAddress: data.signingIpAddress ?? undefined,
signingDeviceInfo: deviceInfo,
signingUserAgent: data.signingUserAgent ?? undefined,

View File

@ -0,0 +1,157 @@
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as Minio from 'minio';
import * as crypto from 'crypto';
/**
* MinIO
*
* PDF MinIO
*/
@Injectable()
export class MinioStorageService implements OnModuleInit {
private readonly logger = new Logger(MinioStorageService.name);
private minioClient: Minio.Client;
private readonly bucketName: string;
private readonly publicUrl: string;
constructor(private readonly configService: ConfigService) {
const endpoint = this.configService.get<string>('MINIO_ENDPOINT', 'localhost');
const port = parseInt(this.configService.get<string>('MINIO_PORT', '9000'), 10);
const useSSL = this.configService.get<string>('MINIO_USE_SSL', 'false') === 'true';
const accessKey = this.configService.get<string>('MINIO_ACCESS_KEY', 'admin');
const secretKey = this.configService.get<string>('MINIO_SECRET_KEY', 'minio_secret_password');
this.bucketName = this.configService.get<string>('MINIO_BUCKET_CONTRACTS', 'contracts');
this.publicUrl = this.configService.get<string>('MINIO_PUBLIC_URL', `http://${endpoint}:${port}`);
this.minioClient = new Minio.Client({
endPoint: endpoint,
port: port,
useSSL: useSSL,
accessKey: accessKey,
secretKey: secretKey,
});
this.logger.log(`MinIO client configured: ${endpoint}:${port}, bucket: ${this.bucketName}`);
}
async onModuleInit() {
await this.ensureBucketExists();
}
/**
*
*/
private async ensureBucketExists(): Promise<void> {
try {
const exists = await this.minioClient.bucketExists(this.bucketName);
if (!exists) {
await this.minioClient.makeBucket(this.bucketName, 'cn-east-1');
this.logger.log(`Created bucket: ${this.bucketName}`);
// 设置桶策略为公开读取
const policy = {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Principal: { AWS: ['*'] },
Action: ['s3:GetObject'],
Resource: [`arn:aws:s3:::${this.bucketName}/*`],
},
],
};
await this.minioClient.setBucketPolicy(this.bucketName, JSON.stringify(policy));
this.logger.log(`Set public read policy for bucket: ${this.bucketName}`);
} else {
this.logger.log(`Bucket exists: ${this.bucketName}`);
}
} catch (error) {
this.logger.error(`Failed to ensure bucket exists: ${error.message}`);
// 不抛出错误,允许服务继续启动(可能 MinIO 暂时不可用)
}
}
/**
*
* @param orderNo
* @param signatureBuffer Buffer (PNG)
* @returns URL
*/
async uploadSignature(orderNo: string, signatureBuffer: Buffer): Promise<string> {
const timestamp = Date.now();
const hash = crypto.createHash('md5').update(signatureBuffer).digest('hex').slice(0, 8);
const objectName = `signatures/${orderNo}/${timestamp}-${hash}.png`;
try {
await this.minioClient.putObject(
this.bucketName,
objectName,
signatureBuffer,
signatureBuffer.length,
{
'Content-Type': 'image/png',
'x-amz-acl': 'public-read',
},
);
const url = `${this.publicUrl}/${this.bucketName}/${objectName}`;
this.logger.log(`Uploaded signature for order ${orderNo}: ${url}`);
return url;
} catch (error) {
this.logger.error(`Failed to upload signature for order ${orderNo}: ${error.message}`);
throw new Error(`签名图片上传失败: ${error.message}`);
}
}
/**
* PDF
* @param orderNo
* @param pdfBuffer PDF Buffer
* @returns PDF URL
*/
async uploadSignedPdf(orderNo: string, pdfBuffer: Buffer): Promise<string> {
const timestamp = Date.now();
const objectName = `contracts/${orderNo}/signed-contract-${timestamp}.pdf`;
try {
await this.minioClient.putObject(
this.bucketName,
objectName,
pdfBuffer,
pdfBuffer.length,
{
'Content-Type': 'application/pdf',
'x-amz-acl': 'public-read',
'Content-Disposition': `inline; filename="contract-${orderNo}.pdf"`,
},
);
const url = `${this.publicUrl}/${this.bucketName}/${objectName}`;
this.logger.log(`Uploaded signed PDF for order ${orderNo}: ${url}`);
return url;
} catch (error) {
this.logger.error(`Failed to upload signed PDF for order ${orderNo}: ${error.message}`);
throw new Error(`已签署合同上传失败: ${error.message}`);
}
}
/**
*
* @param objectUrl URL
*/
async deleteObject(objectUrl: string): Promise<void> {
try {
// 从 URL 中提取对象名称
const urlParts = objectUrl.replace(this.publicUrl, '').split('/');
const objectName = urlParts.slice(2).join('/'); // 去掉空字符串和桶名
await this.minioClient.removeObject(this.bucketName, objectName);
this.logger.log(`Deleted object: ${objectName}`);
} catch (error) {
this.logger.error(`Failed to delete object: ${error.message}`);
// 删除失败不抛出错误
}
}
}

View File

@ -368,15 +368,17 @@ class ContractSigningService {
if (response.statusCode == 200) {
final responseData = response.data as Map<String, dynamic>;
if (responseData['success'] == true && responseData['data'] != null) {
final data = responseData['data'] as Map<String, dynamic>;
debugPrint('[ContractSigningService] 签署成功,任务状态: ${data['status']}');
return ContractSigningTask.fromJson(data);
}
//
if (responseData['orderNo'] != null) {
debugPrint('[ContractSigningService] 签署成功(直接返回数据)');
return ContractSigningTask.fromJson(responseData);
if (responseData['success'] == true) {
debugPrint('[ContractSigningService] 签署成功');
// data success + message
if (responseData['data'] != null) {
final data = responseData['data'] as Map<String, dynamic>;
debugPrint('[ContractSigningService] 返回任务数据,状态: ${data['status']}');
return ContractSigningTask.fromJson(data);
}
// data
debugPrint('[ContractSigningService] 重新获取任务详情');
return await getTask(orderNo);
}
throw Exception('签署合同失败: ${responseData['message'] ?? '响应格式错误'}');
}
@ -431,15 +433,17 @@ class ContractSigningService {
if (response.statusCode == 200) {
final responseData = response.data as Map<String, dynamic>;
if (responseData['success'] == true && responseData['data'] != null) {
final data = responseData['data'] as Map<String, dynamic>;
debugPrint('[ContractSigningService] 补签成功,任务状态: ${data['status']}');
return ContractSigningTask.fromJson(data);
}
//
if (responseData['orderNo'] != null) {
debugPrint('[ContractSigningService] 补签成功(直接返回数据)');
return ContractSigningTask.fromJson(responseData);
if (responseData['success'] == true) {
debugPrint('[ContractSigningService] 补签成功');
// data success + message
if (responseData['data'] != null) {
final data = responseData['data'] as Map<String, dynamic>;
debugPrint('[ContractSigningService] 返回任务数据,状态: ${data['status']}');
return ContractSigningTask.fromJson(data);
}
// data
debugPrint('[ContractSigningService] 重新获取任务详情');
return await getTask(orderNo);
}
throw Exception('补签合同失败: ${responseData['message'] ?? '响应格式错误'}');
}