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:
parent
a2f021fe94
commit
c657fb5a19
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
// 删除失败不抛出错误
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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'] ?? '响应格式错误'}');
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue