diff --git a/backend/services/docker-compose.yml b/backend/services/docker-compose.yml index 8083fd12..a75a776b 100644 --- a/backend/services/docker-compose.yml +++ b/backend/services/docker-compose.yml @@ -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 diff --git a/backend/services/planting-service/package-lock.json b/backend/services/planting-service/package-lock.json index a1ea0b8e..2a4dbf63 100644 --- a/backend/services/planting-service/package-lock.json +++ b/backend/services/planting-service/package-lock.json @@ -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", diff --git a/backend/services/planting-service/package.json b/backend/services/planting-service/package.json index e3bf7eac..f213b5cc 100644 --- a/backend/services/planting-service/package.json +++ b/backend/services/planting-service/package.json @@ -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", diff --git a/backend/services/planting-service/prisma/migrations/20241225000000_add_signed_pdf_url/migration.sql b/backend/services/planting-service/prisma/migrations/20241225000000_add_signed_pdf_url/migration.sql new file mode 100644 index 00000000..22959481 --- /dev/null +++ b/backend/services/planting-service/prisma/migrations/20241225000000_add_signed_pdf_url/migration.sql @@ -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); diff --git a/backend/services/planting-service/prisma/schema.prisma b/backend/services/planting-service/prisma/schema.prisma index 6add0adf..fc34c0ec 100644 --- a/backend/services/planting-service/prisma/schema.prisma +++ b/backend/services/planting-service/prisma/schema.prisma @@ -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) diff --git a/backend/services/planting-service/src/api/controllers/contract-signing.controller.ts b/backend/services/planting-service/src/api/controllers/contract-signing.controller.ts index fe560f0a..dd17f0ee 100644 --- a/backend/services/planting-service/src/api/controllers/contract-signing.controller.ts +++ b/backend/services/planting-service/src/api/controllers/contract-signing.controller.ts @@ -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, diff --git a/backend/services/planting-service/src/domain/aggregates/contract-signing-task.aggregate.ts b/backend/services/planting-service/src/domain/aggregates/contract-signing-task.aggregate.ts index a1d213cc..1aa336d7 100644 --- a/backend/services/planting-service/src/domain/aggregates/contract-signing-task.aggregate.ts +++ b/backend/services/planting-service/src/domain/aggregates/contract-signing-task.aggregate.ts @@ -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; diff --git a/backend/services/planting-service/src/infrastructure/infrastructure.module.ts b/backend/services/planting-service/src/infrastructure/infrastructure.module.ts index 653b5cf4..959a0d9e 100644 --- a/backend/services/planting-service/src/infrastructure/infrastructure.module.ts +++ b/backend/services/planting-service/src/infrastructure/infrastructure.module.ts @@ -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, ], diff --git a/backend/services/planting-service/src/infrastructure/persistence/repositories/contract-signing-task.repository.impl.ts b/backend/services/planting-service/src/infrastructure/persistence/repositories/contract-signing-task.repository.impl.ts index e44c7c37..84f59741 100644 --- a/backend/services/planting-service/src/infrastructure/persistence/repositories/contract-signing-task.repository.impl.ts +++ b/backend/services/planting-service/src/infrastructure/persistence/repositories/contract-signing-task.repository.impl.ts @@ -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, diff --git a/backend/services/planting-service/src/infrastructure/storage/minio-storage.service.ts b/backend/services/planting-service/src/infrastructure/storage/minio-storage.service.ts new file mode 100644 index 00000000..15288db7 --- /dev/null +++ b/backend/services/planting-service/src/infrastructure/storage/minio-storage.service.ts @@ -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('MINIO_ENDPOINT', 'localhost'); + const port = parseInt(this.configService.get('MINIO_PORT', '9000'), 10); + const useSSL = this.configService.get('MINIO_USE_SSL', 'false') === 'true'; + const accessKey = this.configService.get('MINIO_ACCESS_KEY', 'admin'); + const secretKey = this.configService.get('MINIO_SECRET_KEY', 'minio_secret_password'); + + this.bucketName = this.configService.get('MINIO_BUCKET_CONTRACTS', 'contracts'); + this.publicUrl = this.configService.get('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 { + 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 { + 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 { + 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 { + 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}`); + // 删除失败不抛出错误 + } + } +} diff --git a/frontend/mobile-app/lib/core/services/contract_signing_service.dart b/frontend/mobile-app/lib/core/services/contract_signing_service.dart index 183b16d1..10d5ef71 100644 --- a/frontend/mobile-app/lib/core/services/contract_signing_service.dart +++ b/frontend/mobile-app/lib/core/services/contract_signing_service.dart @@ -368,15 +368,17 @@ class ContractSigningService { if (response.statusCode == 200) { final responseData = response.data as Map; - if (responseData['success'] == true && responseData['data'] != null) { - final data = responseData['data'] as Map; - 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; + 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; - if (responseData['success'] == true && responseData['data'] != null) { - final data = responseData['data'] as Map; - 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; + debugPrint('[ContractSigningService] 返回任务数据,状态: ${data['status']}'); + return ContractSigningTask.fromJson(data); + } + // 如果没有返回 data,重新获取任务详情 + debugPrint('[ContractSigningService] 重新获取任务详情'); + return await getTask(orderNo); } throw Exception('补签合同失败: ${responseData['message'] ?? '响应格式错误'}'); }