diff --git a/README.md b/README.md index ea04bc79..1360e870 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,7 @@ docker compose -f docker-compose.dev.yml down ```bash docker compose -f docker-compose.dev.yml exec -T backend npm test docker compose -f docker-compose.dev.yml exec -T backend npm run typecheck +docker compose -f docker-compose.dev.yml exec -T backend npm run build docker compose -f docker-compose.dev.yml exec -T frontend npm run typecheck docker compose -f docker-compose.dev.yml exec -T frontend npm run lint ``` @@ -93,7 +94,7 @@ docker compose -f docker-compose.dev.yml exec -T backend npm run db:migrate 开发版 Compose 会把源码挂载进容器: -- 修改 `apps/backend/src`:后端容器内 `node --watch` 自动重启 +- 修改 `apps/backend/src`:后端容器内 `tsx watch` 自动重启 - 修改 `apps/frontend/src`:Vite 自动 HMR - 修改 `apps/backend/subservices/ocr-worker`:下次 OCR 调用会使用挂载后的代码 - 修改数据库 migration:重启后端会自动执行迁移 diff --git a/apps/backend/README.md b/apps/backend/README.md index b5c30321..38264d93 100644 --- a/apps/backend/README.md +++ b/apps/backend/README.md @@ -36,22 +36,25 @@ Linux 部署: ```bash npm install +npm run build npm run browser:install:linux npm run start ``` -后端渐进 TypeScript 迁移已经开始,当前可执行: +后端渐进 TypeScript 迁移已经进入源码和构建双轨模式: ```bash npm run typecheck +npm run build +npm test ``` -第一阶段目前先检查: +当前约定: -- `src/config/runtime.js` -- `src/types/**/*.js` - -后续会逐步扩大到更多 routes / services / repositories,而不会先改成需要编译产物的运行模式。 +- 开发:`npm run dev` 使用 `tsx watch` 直接运行 `src`,并额外监听 `config` +- 数据库迁移:`npm run db:migrate` 使用 `tsx` +- 生产:`npm run build` 输出 `dist`,`npm run start` 运行 `dist/index.js` +- 新增或迁移后端模块优先使用 `.ts` ## 配置 @@ -122,6 +125,7 @@ npm run typecheck - 后端容器启动时执行 `npm install` 和 `npm run dev` - OCR 子服务会在容器里执行 `pip install -e /app/subservices/ocr-worker` - 所以平时改 `src/`、改 OCR Python、改 `.env`,一般都不需要重建镜像 +- 生产镜像会在 build 阶段编译后端并只复制 `dist`、`config` 和运行依赖 只有下面几类改动通常还需要 `docker compose -f docker-compose.dev.yml up -d --build backend`: diff --git a/apps/backend/package-lock.json b/apps/backend/package-lock.json index 49756c56..ee21cd9f 100644 --- a/apps/backend/package-lock.json +++ b/apps/backend/package-lock.json @@ -14,9 +14,453 @@ }, "devDependencies": { "@types/node": "^25.6.0", + "@types/pg": "^8.20.0", + "tsx": "^4.22.3", "typescript": "^6.0.2" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@types/node": { "version": "25.6.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", @@ -27,6 +471,18 @@ "undici-types": "~7.19.0" } }, + "node_modules/@types/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -227,6 +683,48 @@ "node": ">= 0.4" } }, + "node_modules/esbuild": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" + } + }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -981,6 +1479,40 @@ "node": ">=0.6" } }, + "node_modules/tsx": { + "version": "4.22.3", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.3.tgz", + "integrity": "sha512-mdoNxBC/cSQObGGVQ5Bpn5i+yv7j68gk3Nfm3wFjcJg3Z0Mix9jzAFfP12prmm5eVGmDKtp0yyArrs0Q+8gZHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/type-is": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", diff --git a/apps/backend/package.json b/apps/backend/package.json index 69055e27..61f2c02a 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -9,11 +9,13 @@ "agiso:auto-delivery:test": "node scripts/test-agiso-auto-delivery.js", "cleanup:dev-data": "node scripts/cleanup-dev-data.js", "seed:dev-data": "node scripts/seed-dev-data.js", - "db:migrate": "node src/db/migrate.js", - "dev": "node --watch-path=src --watch-path=config --watch-preserve-output src/index.js", - "test": "node --test $(find src -name '*.test.js' -print)", + "build": "tsc -p tsconfig.build.json && rm -rf dist/db/migrations && mkdir -p dist/db && cp -R src/db/migrations dist/db/migrations", + "db:migrate": "tsx src/db/migrate.js", + "dev": "tsx watch --clear-screen=false --include config src/index.js", + "test": "node --import tsx --test $(find src -name '*.test.js' -print)", "typecheck": "tsc -p tsconfig.json --noEmit", - "start": "node src/index.js" + "start": "node dist/index.js", + "start:src": "tsx src/index.js" }, "dependencies": { "express": "^5.1.0", @@ -22,6 +24,8 @@ }, "devDependencies": { "@types/node": "^25.6.0", + "@types/pg": "^8.20.0", + "tsx": "^4.22.3", "typescript": "^6.0.2" } } diff --git a/apps/backend/scripts/start-dev-container.sh b/apps/backend/scripts/start-dev-container.sh index 3facdc75..ec4373ea 100644 --- a/apps/backend/scripts/start-dev-container.sh +++ b/apps/backend/scripts/start-dev-container.sh @@ -21,6 +21,10 @@ start_browser_debug_services() { display_socket="/tmp/.X11-unix/X${display_number}" display_lock="/tmp/.X${display_number}-lock" + if [ -S "$display_socket" ] && [ ! -f "$display_lock" ]; then + rm -f "$display_socket" + fi + if [ -f "$display_lock" ]; then display_pid=$(cat "$display_lock" 2>/dev/null || true) if [ -z "$display_pid" ] || ! kill -0 "$display_pid" 2>/dev/null; then diff --git a/apps/backend/src/db/client.js b/apps/backend/src/db/client.ts similarity index 55% rename from apps/backend/src/db/client.js rename to apps/backend/src/db/client.ts index 923f6c89..af3df8e2 100644 --- a/apps/backend/src/db/client.js +++ b/apps/backend/src/db/client.ts @@ -1,10 +1,11 @@ import { Pool } from 'pg' +import type { PoolClient, QueryResult, QueryResultRow } from 'pg' import { runtimeConfig } from '../config/runtime.js' -let poolInstance = null +let poolInstance: Pool | null = null -export function getDb() { +export function getDb(): Pool { if (!poolInstance) { poolInstance = new Pool({ connectionString: String(runtimeConfig.database?.url || '').trim(), @@ -16,12 +17,20 @@ export function getDb() { return poolInstance } -export async function query(text, params = []) { +export async function query(text: string, params?: unknown[]): Promise> +export async function query( + text: string, + params?: unknown[], +): Promise> +export async function query( + text: string, + params: unknown[] = [], +): Promise> { const pool = getDb() - return pool.query(text, params) + return pool.query(text, params) } -export async function withTransaction(fn) { +export async function withTransaction(fn: (client: PoolClient) => Promise): Promise { const client = await getDb().connect() try { @@ -37,7 +46,7 @@ export async function withTransaction(fn) { } } -export async function closeDb() { +export async function closeDb(): Promise { if (!poolInstance) { return } diff --git a/apps/backend/src/repositories/admin-audit-log-repo.js b/apps/backend/src/repositories/admin-audit-log-repo.ts similarity index 56% rename from apps/backend/src/repositories/admin-audit-log-repo.js rename to apps/backend/src/repositories/admin-audit-log-repo.ts index 568b028b..e828d349 100644 --- a/apps/backend/src/repositories/admin-audit-log-repo.js +++ b/apps/backend/src/repositories/admin-audit-log-repo.ts @@ -1,7 +1,46 @@ import { query } from '../db/client.js' -export async function createAdminAuditLog(input) { - const result = await query( +type AdminAuditLogRow = { + [column: string]: unknown + id: number + actor_user_id: number | null + actor_username: string + actor_role: string + action: string + target_type: string + target_id: string + payload_json: string | Record + created_at: string +} + +type AdminAuditLogCreateInput = { + actorUserId?: number | string | null + actorUsername?: string + actorRole?: string + action: string + targetType: string + targetId: string + payloadJson?: string + createdAt: string +} + +type AdminAuditLogListInput = { + actorUsername?: string + action?: string + targetType?: string + dateFrom?: string + dateTo?: string + page?: number | string + pageSize?: number | string +} + +type AdminAuditLogListResult = { + items: AdminAuditLogRow[] + total: number +} + +export async function createAdminAuditLog(input: AdminAuditLogCreateInput): Promise { + const result = await query( ` INSERT INTO admin_audit_logs ( actor_user_id, @@ -30,14 +69,14 @@ export async function createAdminAuditLog(input) { return result.rows[0] || null } -export async function getAdminAuditLogById(logId) { - const result = await query('SELECT * FROM admin_audit_logs WHERE id = $1 LIMIT 1', [Number(logId)]) +export async function getAdminAuditLogById(logId: number | string): Promise { + const result = await query('SELECT * FROM admin_audit_logs WHERE id = $1 LIMIT 1', [Number(logId)]) return result.rows[0] || null } -export async function listAdminAuditLogs(queryInput = {}) { - const conditions = [] - const params = [] +export async function listAdminAuditLogs(queryInput: AdminAuditLogListInput = {}): Promise { + const conditions: string[] = [] + const params: unknown[] = [] if (queryInput.actorUsername) { params.push(queryInput.actorUsername) @@ -69,14 +108,14 @@ export async function listAdminAuditLogs(queryInput = {}) { const pageSize = Number(queryInput.pageSize) || 20 const offset = (page - 1) * pageSize - const totalResult = await query( + const totalResult = await query<{ [column: string]: unknown, total: number }>( `SELECT COUNT(*)::int AS total FROM admin_audit_logs ${whereClause}`, params, ) params.push(pageSize) params.push(offset) - const itemsResult = await query( + const itemsResult = await query( ` SELECT * FROM admin_audit_logs diff --git a/apps/backend/src/repositories/claim-token-repo.js b/apps/backend/src/repositories/claim-token-repo.ts similarity index 53% rename from apps/backend/src/repositories/claim-token-repo.js rename to apps/backend/src/repositories/claim-token-repo.ts index d063d11f..32bfcf78 100644 --- a/apps/backend/src/repositories/claim-token-repo.js +++ b/apps/backend/src/repositories/claim-token-repo.ts @@ -1,7 +1,38 @@ import { query } from '../db/client.js' -export async function createClaimToken(input) { - const result = await query( +type ClaimTokenRow = { + [column: string]: unknown + id: number + task_id: number + token: string + status: string + expired_at: string + used_at: string | null + max_use_count: number + used_count: number + created_at: string + updated_at: string +} + +type ClaimTokenCreateInput = { + taskId: number + token: string + status: string + expiredAt: string + usedAt?: string | null + maxUseCount: number + usedCount: number + createdAt: string + updatedAt: string +} + +type ClaimTokenPatch = Partial> + +export async function createClaimToken(input: ClaimTokenCreateInput): Promise { + const result = await query( ` INSERT INTO claim_tokens ( task_id, @@ -42,24 +73,24 @@ export async function createClaimToken(input) { return result.rows[0] || null } -export async function getClaimTokenById(tokenId) { - const result = await query('SELECT * FROM claim_tokens WHERE id = $1 LIMIT 1', [Number(tokenId)]) +export async function getClaimTokenById(tokenId: number | string): Promise { + const result = await query('SELECT * FROM claim_tokens WHERE id = $1 LIMIT 1', [Number(tokenId)]) return result.rows[0] || null } -export async function findClaimTokenByToken(token) { - const result = await query('SELECT * FROM claim_tokens WHERE token = $1 LIMIT 1', [String(token || '').trim()]) +export async function findClaimTokenByToken(token: string): Promise { + const result = await query('SELECT * FROM claim_tokens WHERE token = $1 LIMIT 1', [String(token || '').trim()]) return result.rows[0] || null } -export async function updateClaimToken(tokenId, patch) { +export async function updateClaimToken(tokenId: number | string, patch: ClaimTokenPatch): Promise { const current = await getClaimTokenById(tokenId) if (!current) { return null } const next = { ...current, ...patch } - const result = await query( + const result = await query( ` UPDATE claim_tokens SET diff --git a/apps/backend/src/repositories/task-event-repo.js b/apps/backend/src/repositories/task-event-repo.js deleted file mode 100644 index 2d07611b..00000000 --- a/apps/backend/src/repositories/task-event-repo.js +++ /dev/null @@ -1,38 +0,0 @@ -import { query } from '../db/client.js' - -export async function createTaskEvent(taskId, eventType, payload = {}, createdAt) { - const result = await query( - ` - INSERT INTO task_events ( - task_id, - event_type, - payload_json, - created_at - ) VALUES ($1, $2, $3::jsonb, $4) - RETURNING * - `, - [ - Number(taskId), - String(eventType || '').trim(), - JSON.stringify(payload || {}), - createdAt, - ], - ) - - return result.rows[0] || null -} - -export async function listTaskEventsByTaskId(taskId, { limit = 20 } = {}) { - const result = await query( - ` - SELECT * - FROM task_events - WHERE task_id = $1 - ORDER BY created_at DESC, id DESC - LIMIT $2 - `, - [Number(taskId), Math.max(1, Number(limit || 20))], - ) - - return result.rows -} diff --git a/apps/backend/src/repositories/task-event-repo.ts b/apps/backend/src/repositories/task-event-repo.ts new file mode 100644 index 00000000..c5507ada --- /dev/null +++ b/apps/backend/src/repositories/task-event-repo.ts @@ -0,0 +1,59 @@ +import { query } from '../db/client.js' + +type TaskEventRow = { + [column: string]: unknown + id: number + task_id: number + event_type: string + payload_json: string | Record + created_at: string +} + +type TaskEventListOptions = { + limit?: number | string +} + +export async function createTaskEvent( + taskId: number | string, + eventType: string, + payload: unknown = {}, + createdAt: string | undefined = undefined, +): Promise { + const result = await query( + ` + INSERT INTO task_events ( + task_id, + event_type, + payload_json, + created_at + ) VALUES ($1, $2, $3::jsonb, $4) + RETURNING * + `, + [ + Number(taskId), + String(eventType || '').trim(), + JSON.stringify(payload || {}), + createdAt, + ], + ) + + return result.rows[0] || null +} + +export async function listTaskEventsByTaskId( + taskId: number | string, + { limit = 20 }: TaskEventListOptions = {}, +): Promise { + const result = await query( + ` + SELECT * + FROM task_events + WHERE task_id = $1 + ORDER BY created_at DESC, id DESC + LIMIT $2 + `, + [Number(taskId), Math.max(1, Number(limit || 20))], + ) + + return result.rows +} diff --git a/apps/backend/src/utils/json.js b/apps/backend/src/utils/json.ts similarity index 71% rename from apps/backend/src/utils/json.js rename to apps/backend/src/utils/json.ts index 38b6879c..401ce6ee 100644 --- a/apps/backend/src/utils/json.js +++ b/apps/backend/src/utils/json.ts @@ -2,7 +2,17 @@ import { createHttpError } from './http.js' const LARGE_INTEGER_PATTERN = /(:\s*)(-?\d{16,})(\s*[,}\]])/g -export function parseJsonObject(rawText, { preserveLargeIntegers = false, throwOnError = false } = {}) { +type JsonObject = Record + +type ParseJsonObjectOptions = { + preserveLargeIntegers?: boolean + throwOnError?: boolean +} + +export function parseJsonObject( + rawText: unknown, + { preserveLargeIntegers = false, throwOnError = false }: ParseJsonObjectOptions = {}, +): JsonObject { const normalized = String(rawText || '').trim() if (!normalized) { @@ -26,7 +36,7 @@ export function parseJsonObject(rawText, { preserveLargeIntegers = false, throwO } } -export function wrapLargeIntegerLiterals(rawText) { +export function wrapLargeIntegerLiterals(rawText: unknown): string { return String(rawText || '').replace(LARGE_INTEGER_PATTERN, (_match, prefix, digits, suffix) => { if (!shouldPreserveIntegerToken(digits)) { return `${prefix}${digits}${suffix}` @@ -36,7 +46,7 @@ export function wrapLargeIntegerLiterals(rawText) { }) } -function shouldPreserveIntegerToken(rawDigits) { +function shouldPreserveIntegerToken(rawDigits: unknown): boolean { const digits = String(rawDigits || '').trim() const unsigned = digits.startsWith('-') ? digits.slice(1) : digits @@ -55,6 +65,6 @@ function shouldPreserveIntegerToken(rawDigits) { return unsigned > '9007199254740991' } -function isPlainObject(value) { +function isPlainObject(value: unknown): value is JsonObject { return Object.prototype.toString.call(value) === '[object Object]' } diff --git a/apps/backend/src/utils/money.js b/apps/backend/src/utils/money.ts similarity index 84% rename from apps/backend/src/utils/money.js rename to apps/backend/src/utils/money.ts index 5f8125d0..6c1eea6f 100644 --- a/apps/backend/src/utils/money.js +++ b/apps/backend/src/utils/money.ts @@ -1,4 +1,4 @@ -export function parseAmountToFen(value) { +export function parseAmountToFen(value: unknown): number { const normalized = normalizeAmountInput(value) if (!normalized) { @@ -30,7 +30,7 @@ export function parseAmountToFen(value) { return cents * sign } -export function formatFenToAmount(value) { +export function formatFenToAmount(value: unknown): string { const normalizedFen = normalizeFen(value) const sign = normalizedFen < 0 ? '-' : '' const absoluteFen = Math.abs(normalizedFen) @@ -39,12 +39,12 @@ export function formatFenToAmount(value) { return `${sign}${integerPart}.${fractionPart}` } -export function normalizeFen(value) { +export function normalizeFen(value: unknown): number { const parsed = Number(value) return Number.isFinite(parsed) ? Math.round(parsed) : 0 } -function normalizeAmountInput(value) { +function normalizeAmountInput(value: unknown): string { if (typeof value === 'number' && Number.isFinite(value)) { return String(value) } diff --git a/apps/backend/src/utils/random.js b/apps/backend/src/utils/random.ts similarity index 58% rename from apps/backend/src/utils/random.js rename to apps/backend/src/utils/random.ts index cd42557d..18781e8e 100644 --- a/apps/backend/src/utils/random.js +++ b/apps/backend/src/utils/random.ts @@ -1,9 +1,9 @@ import crypto from 'node:crypto' -export function randomToken(bytes = 24) { +export function randomToken(bytes = 24): string { return crypto.randomBytes(bytes).toString('hex') } -export function randomId(prefix, bytes = 6) { +export function randomId(prefix: string, bytes = 6): string { return `${prefix}${crypto.randomBytes(bytes).toString('hex')}` } diff --git a/apps/backend/src/utils/time.js b/apps/backend/src/utils/time.ts similarity index 59% rename from apps/backend/src/utils/time.js rename to apps/backend/src/utils/time.ts index f1cd944e..8e5a9fac 100644 --- a/apps/backend/src/utils/time.js +++ b/apps/backend/src/utils/time.ts @@ -1,8 +1,8 @@ -export function nowIso() { +export function nowIso(): string { return new Date().toISOString() } -export function addHours(baseIso, hours) { +export function addHours(baseIso: string | null | undefined, hours: number): string { const baseTime = baseIso ? new Date(baseIso).getTime() : Date.now() return new Date(baseTime + hours * 60 * 60 * 1000).toISOString() } diff --git a/apps/backend/tsconfig.build.json b/apps/backend/tsconfig.build.json new file mode 100644 index 00000000..00789ae5 --- /dev/null +++ b/apps/backend/tsconfig.build.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "outDir": "dist", + "rootDir": "src", + "declaration": false, + "sourceMap": true + }, + "exclude": [ + "node_modules", + "data", + "dist", + "src/**/*.test.js" + ] +} diff --git a/apps/backend/tsconfig.json b/apps/backend/tsconfig.json index f6d8f8cb..171105ce 100644 --- a/apps/backend/tsconfig.json +++ b/apps/backend/tsconfig.json @@ -12,7 +12,8 @@ "lib": ["ES2022"] }, "include": [ - "src/**/*.js" + "src/**/*.js", + "src/**/*.ts" ], "exclude": [ "node_modules", diff --git a/deploy/docker/backend.Dockerfile b/deploy/docker/backend.Dockerfile index 183f688c..b544ec48 100644 --- a/deploy/docker/backend.Dockerfile +++ b/deploy/docker/backend.Dockerfile @@ -33,9 +33,6 @@ COPY apps/backend/package.json apps/backend/package-lock.json ./ RUN --mount=type=cache,target=/root/.npm \ npm ci -# --- Prune devDependencies after install --- -RUN npm prune --omit=dev - # --- Playwright — install Chromium with system deps (builder has full apt) --- ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright RUN --mount=type=cache,target=/ms-playwright-cache,sharing=locked \ @@ -47,6 +44,10 @@ RUN --mount=type=cache,target=/ms-playwright-cache,sharing=locked \ # --- Copy application source --- COPY apps/backend/ ./ +# --- Build application and prune devDependencies --- +RUN npm run build \ + && npm prune --omit=dev + # --------------------------- Runtime Stage ----------------------------------- FROM node:22-bookworm-slim AS runtime @@ -87,9 +88,9 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ # --- Copy artefacts from builder --- # Pruned node_modules (no devDeps) COPY --from=builder /app/node_modules ./node_modules -# Application source (no build step — runs directly via `node src/index.js`) +# Compiled application COPY --from=builder /app/package.json ./package.json -COPY --from=builder /app/src ./src +COPY --from=builder /app/dist ./dist COPY --from=builder /app/config ./config # Playwright browsers COPY --from=builder /ms-playwright /ms-playwright diff --git a/docs/backend-typescript-migration-plan.md b/docs/backend-typescript-migration-plan.md index 09480265..0fbed9ba 100644 --- a/docs/backend-typescript-migration-plan.md +++ b/docs/backend-typescript-migration-plan.md @@ -1,6 +1,6 @@ # Backend TypeScript 迁移计划 -最后更新:2026-04-14 +最后更新:2026-05-21 ## 结论 @@ -49,14 +49,16 @@ ## 分阶段计划 -### 阶段 1:建立类型基础设施 +### 阶段 1:建立类型基础设施和构建链路 目标: - 引入后端 `tsconfig` +- 引入后端 `tsc -> dist` 构建链路 +- 开发和测试改为通过 `tsx` 支持 JS/TS 混合源码 - 增加 `npm run typecheck` - 先从最核心的共享配置和基础领域对象开始建类型 -- 暂不改变现有运行方式,不引入编译产物,不影响 Docker 启动 +- 生产 Docker 镜像运行编译后的 `dist` 范围: @@ -70,7 +72,9 @@ 验收标准: - 后端可以执行 `npm run typecheck` -- 现有 `npm run dev` / `npm run start` / Docker 启动流程不受影响 +- 后端可以执行 `npm run build` +- `npm run dev` 继续运行源码,`npm run start` 运行 `dist/index.js` +- Docker 开发环境继续热更新,生产镜像只复制编译产物和运行依赖 ### 阶段 2:按领域拆分超大 service @@ -189,19 +193,39 @@ 63. 新增 `src/services/admin/admin-order-read-helpers.js` 与 `src/services/admin/admin-webhook-read-helpers.js`,订单列表汇总与 webhook 事件映射已按领域独立 64. `admin-read-service / admin-write-service / admin-service` 已全部改为直接依赖 inventory / order / webhook / task / shared 五类 helper,读侧 helper 拆分阶段可以收口 -这一步的设计原则是“只加静态约束,不动运行链路”,所以不会影响: +## 2026-05-21 执行进展 -- `npm run dev` -- `npm run start` -- Docker 开发 / 部署 +本轮已落地: + +1. 新增 `tsconfig.build.json` +2. `npm run build` 输出 `dist`,并复制数据库 migration SQL +3. `npm run dev` 改为 `tsx watch --clear-screen=false --include config src/index.js` +4. `npm test` 改为 `node --import tsx --test ...` +5. `npm run db:migrate` 改为 `tsx src/db/migrate.js` +6. `npm run start` 改为 `node dist/index.js` +7. 生产 Dockerfile 改为 builder 阶段编译,runtime 阶段复制 `dist` +8. 新增 `tsx` 和 `@types/pg` +9. 首批真实源码迁移到 `.ts`: + - `src/db/client.ts` + - `src/utils/time.ts` + - `src/utils/random.ts` + - `src/utils/money.ts` + - `src/utils/json.ts` + - `src/repositories/claim-token-repo.ts` + - `src/repositories/task-event-repo.ts` + - `src/repositories/admin-audit-log-repo.ts` +10. Docker 内验证通过: + - `npm run typecheck` + - `npm run build` + - `npm test` ## 下一步建议 第一批继续推进时,建议按这个顺序: -1. 为 `admin` 路由层继续补响应模型与审计 payload 的共享类型,减少 `unknown` 到处就地断言 -2. 继续补 repository / service 的边界约束,但不急着改 `.ts` 扩展名 -3. 恢复并固定 `npm --prefix apps/backend run typecheck` 为日常校验步骤,确保本地与 CI 环境都能直接执行 +1. 将 `src/types/*.js` 从 JSDoc typedef 正式改写为 `export type` 的 `.ts` 文件,并逐步更新引用 +2. 继续迁移低风险 repository:`order-item-repo`、`message-delivery-repo`、`webhook-event-repo` +3. 拆分并迁移 `runtime.js`,把 env 解析、默认配置加载、配置合并分开 4. 为 webhook、库存换码、自动发货补测试 ## 执行原则