diff --git a/README.md b/README.md index b47f9bd97994bef44bc754534836bee778b43fb8..53c51265976f37176f5a06c3c284be7edd1fd796 100644 --- a/README.md +++ b/README.md @@ -213,6 +213,32 @@ pnpm dev # 访问 http://localhost:5173 > ✅ 真实 bcrypt 哈希已写入 `backend/sql/data.sql`(commit `ba6c314`),导入 SQL 后即可直接登录。 > ⚠️ 本项目**不使用 Docker**(团队统一约定)。如需容器化请自行扩展。 +## 🧪 测试运行 + +订单支付模块集成测试(Jest + Supertest),覆盖主链路 + 取消幂等: + +```bash +cd backend +npm test # 串行跑全部用例 +npm run test:watch # watch 模式 +``` + +**前置**:本地 MySQL + Redis 已在运行,且 `schema.sql` / `data.sql` 已导入(测试复用 `data.sql` 中的 `admin/admin123` 与 `tester/buyer123` 账号)。 + +**注意**:测试会**真实写入 dev 库**的 order / payment / cart_item / address 表(无 sandbox DB)。跑前请确认是开发库,或重置数据: + +```bash +mysql -u root -p eshop < backend/sql/schema.sql +mysql -u root -p eshop < backend/sql/data.sql +``` + +**实现要点**: +- `src/app.js` 在 `NODE_ENV !== 'test'` 时才调用 `bootstrap()`,jest 自动设 `NODE_ENV=test` → 测试环境 require app 时不连 DB +- `tests/setup.js` 提供 `getApp / loginAs / authHeader` +- `tests/order.test.js` 两个用例:`创建→支付→发货→收货` + `取消幂等` + +详细业务规则与挑战项实现见 [`docs/订单支付功能.md`](docs/订单支付功能.md) 与 [`docs/superpowers/specs/2026-07-27-address-feature-design.md`](docs/superpowers/specs/2026-07-27-address-feature-design.md)。 + ## 协作硬规则(来自《项目要求》3.3) 1. **接口文档先行**:跨人接缝接口变更必须先 PR 到 `docs/02-设计文档/接口设计.md`,A 审核通过方可改代码。 @@ -276,4 +302,10 @@ A: 先 PR 到 `docs/02-设计文档/接口设计.md`,A 审核通过后再改 --- +## 前台首页 UI 风格 + +`/` 首页采用类电商首屏布局(顶部快捷入口 + 居中搜索栏 + 左侧分类 + 中间 Banner 轮播 + 右侧分类卡片 + 猜你喜欢 + 浮动工具栏),所有数据来源于 `/api/categories` 与 `/api/products`。**装饰图**(Banner 轮播 + 右侧促销卡片)为内联 SVG,无外部网络依赖;**商品图片**使用 Unsplash CDN(`https://images.unsplash.com/`)真实图片,与演示数据保持一致。详见 [docs/superpowers/specs/2026-08-04-homepage-taobao-style-design.md](docs/superpowers/specs/2026-08-04-homepage-taobao-style-design.md)。 + +--- + > 历史模板信息可参考原版 `git log -p HEAD~ README.md`,本版是基于分工与实际工程化落地的版本。 diff --git a/backend/jest.config.js b/backend/jest.config.js new file mode 100644 index 0000000000000000000000000000000000000000..5e70a64a554ae4cb72ff864325e40622fba4dbaa --- /dev/null +++ b/backend/jest.config.js @@ -0,0 +1,13 @@ +/** + * Jest 配置(订单支付模块集成测试) + * - testEnvironment: node + * - --runInBand: 串行执行(共用 sequelize/redis,避免并发写同一行) + * - testTimeout: 20s(登录 + 多次 DB 写入留余量) + */ +module.exports = { + testEnvironment: 'node', + testMatch: ['/tests/**/*.test.js'], + testTimeout: 20000, + // 排除后端自身代码,确保不被当成测试 + modulePathIgnorePatterns: ['/src'], +}; \ No newline at end of file diff --git a/backend/package-lock.json b/backend/package-lock.json index dc951dd4dc7027e920f3cca61a0f73b7a9e45cd1..f57eda844bc4ce059081cbb853abb291275b4118 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -24,2120 +24,4680 @@ }, "devDependencies": { "eslint": "^8.57.0", + "jest": "^29.7.0", "nodemon": "^3.1.4", - "sequelize-cli": "^6.6.2" + "sequelize-cli": "^6.6.2", + "supertest": "^7.0.0" }, "engines": { "node": ">=20.0.0" } }, - "node_modules/@colors/colors": { - "version": "1.6.0", - "resolved": "https://registry.npmmirror.com/@colors/colors/-/colors-1.6.0.tgz", - "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, "engines": { - "node": ">=0.1.90" + "node": ">=6.9.0" } }, - "node_modules/@dabh/diagnostics": { - "version": "2.0.8", - "resolved": "https://registry.npmmirror.com/@dabh/diagnostics/-/diagnostics-2.0.8.tgz", - "integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==", + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, "license": "MIT", - "dependencies": { - "@so-ric/colorspace": "^1.1.6", - "enabled": "2.0.x", - "kuler": "^2.0.0" + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.10.1", - "resolved": "https://registry.npmmirror.com/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", - "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", "dependencies": { - "eslint-visitor-keys": "^3.4.3" + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=6.9.0" }, "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + "type": "opencollective", + "url": "https://opencollective.com/babel" } }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmmirror.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + "license": "ISC", + "bin": { + "semver": "bin/semver.js" } }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmmirror.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=6.9.0" } }, - "node_modules/@eslint/js": { - "version": "8.57.1", - "resolved": "https://registry.npmmirror.com/@eslint/js/-/js-8.57.1.tgz", - "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "dev": true, "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=6.9.0" } }, - "node_modules/@hapi/hoek": { - "version": "9.3.0", - "resolved": "https://registry.npmmirror.com/@hapi/hoek/-/hoek-9.3.0.tgz", - "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@hapi/topo": { - "version": "5.1.0", - "resolved": "https://registry.npmmirror.com/@hapi/topo/-/topo-5.1.0.tgz", - "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", - "license": "BSD-3-Clause", + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", "dependencies": { - "@hapi/hoek": "^9.0.0" + "yallist": "^3.0.2" } }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.13.0", - "resolved": "https://registry.npmmirror.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", - "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", - "deprecated": "Use @eslint/config-array instead", + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.3", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" + "license": "ISC", + "bin": { + "semver": "bin/semver.js" } }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true, - "license": "Apache-2.0", + "license": "ISC" + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "node": ">=6.9.0" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmmirror.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "deprecated": "Use @eslint/object-schema instead", + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", "dev": true, - "license": "BSD-3-Clause" + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { - "node": ">=12" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", "dev": true, "license": "MIT", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "node": ">=6.9.0" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true, "license": "MIT", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">=6.9.0" } }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmmirror.com/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", "dev": true, "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=6.9.0" } }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^6.2.2" + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "node": ">=6.9.0" } }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" + "@babel/types": "^7.29.7" }, - "engines": { - "node": ">=12" + "bin": { + "parser": "bin/babel-parser.js" }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "engines": { + "node": ">=6.0.0" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", "dev": true, "license": "MIT", "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" + "@babel/helper-plugin-utils": "^7.8.0" }, - "engines": { - "node": ">= 8" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", "dev": true, "license": "MIT", - "engines": { - "node": ">= 8" + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", "dev": true, "license": "MIT", "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" + "@babel/helper-plugin-utils": "^7.12.13" }, - "engines": { - "node": ">= 8" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@one-ini/wasm": { - "version": "0.1.1", - "resolved": "https://registry.npmmirror.com/@one-ini/wasm/-/wasm-0.1.1.tgz", - "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmmirror.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz", + "integrity": "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==", "dev": true, "license": "MIT", - "optional": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, "engines": { - "node": ">=14" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@redis/bloom": { - "version": "1.2.0", - "resolved": "https://registry.npmmirror.com/@redis/bloom/-/bloom-1.2.0.tgz", - "integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==", + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, "peerDependencies": { - "@redis/client": "^1.0.0" + "@babel/core": "^7.0.0-0" } }, - "node_modules/@redis/client": { - "version": "1.6.1", - "resolved": "https://registry.npmmirror.com/@redis/client/-/client-1.6.1.tgz", - "integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==", + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, "license": "MIT", "dependencies": { - "cluster-key-slot": "1.1.2", - "generic-pool": "3.9.0", - "yallist": "4.0.0" + "@babel/helper-plugin-utils": "^7.8.0" }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@redis/graph": { - "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/@redis/graph/-/graph-1.1.1.tgz", - "integrity": "sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==", - "license": "MIT", "peerDependencies": { - "@redis/client": "^1.0.0" + "@babel/core": "^7.0.0-0" } }, - "node_modules/@redis/json": { - "version": "1.0.7", - "resolved": "https://registry.npmmirror.com/@redis/json/-/json-1.0.7.tgz", - "integrity": "sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==", + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", + "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", + "dev": true, "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, "peerDependencies": { - "@redis/client": "^1.0.0" + "@babel/core": "^7.0.0-0" } }, - "node_modules/@redis/search": { - "version": "1.2.0", - "resolved": "https://registry.npmmirror.com/@redis/search/-/search-1.2.0.tgz", - "integrity": "sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==", + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, "peerDependencies": { - "@redis/client": "^1.0.0" + "@babel/core": "^7.0.0-0" } }, - "node_modules/@redis/time-series": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/@redis/time-series/-/time-series-1.1.0.tgz", - "integrity": "sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==", + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, "license": "MIT", - "peerDependencies": { - "@redis/client": "^1.0.0" - } - }, - "node_modules/@sideway/address": { - "version": "4.1.5", - "resolved": "https://registry.npmmirror.com/@sideway/address/-/address-4.1.5.tgz", - "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", - "license": "BSD-3-Clause", "dependencies": { - "@hapi/hoek": "^9.0.0" + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@sideway/formula": { - "version": "3.0.1", - "resolved": "https://registry.npmmirror.com/@sideway/formula/-/formula-3.0.1.tgz", - "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", - "license": "BSD-3-Clause" - }, - "node_modules/@sideway/pinpoint": { - "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", - "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@so-ric/colorspace": { - "version": "1.1.6", - "resolved": "https://registry.npmmirror.com/@so-ric/colorspace/-/colorspace-1.1.6.tgz", - "integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==", + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, "license": "MIT", "dependencies": { - "color": "^5.0.2", - "text-hex": "1.0.x" + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@types/debug": { - "version": "4.1.13", - "resolved": "https://registry.npmmirror.com/@types/debug/-/debug-4.1.13.tgz", - "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, "license": "MIT", "dependencies": { - "@types/ms": "*" + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@types/ms": { - "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "26.1.1", - "resolved": "https://registry.npmmirror.com/@types/node/-/node-26.1.1.tgz", - "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==", + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~8.3.0" + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@types/triple-beam": { - "version": "1.3.5", - "resolved": "https://registry.npmmirror.com/@types/triple-beam/-/triple-beam-1.3.5.tgz", - "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", - "license": "MIT" - }, - "node_modules/@types/validator": { - "version": "13.15.10", - "resolved": "https://registry.npmmirror.com/@types/validator/-/validator-13.15.10.tgz", - "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==", - "license": "MIT" - }, - "node_modules/@ungap/structured-clone": { - "version": "1.3.3", - "resolved": "https://registry.npmmirror.com/@ungap/structured-clone/-/structured-clone-1.3.3.tgz", - "integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==", - "dev": true, - "license": "ISC" - }, - "node_modules/abbrev": { - "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/abbrev/-/abbrev-2.0.0.tgz", - "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", "dev": true, - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", "license": "MIT", "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" + "@babel/helper-plugin-utils": "^7.8.0" }, - "engines": { - "node": ">= 0.6" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/acorn": { - "version": "8.17.0", - "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.17.0.tgz", - "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", "dev": true, "license": "MIT", - "bin": { - "acorn": "bin/acorn" + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" }, "engines": { - "node": ">=0.4.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", "dev": true, "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + "@babel/core": "^7.0.0-0" } }, - "node_modules/ajv": { - "version": "6.15.0", - "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", + "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", "dev": true, "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "@babel/helper-plugin-utils": "^7.29.7" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "dev": true, "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, "engines": { - "node": ">=8" + "node": ">=6.9.0" } }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", "dev": true, "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">=6.9.0" } }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { - "node": ">= 8" + "node": ">=6.9.0" } }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmmirror.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true, - "license": "Python-2.0" - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "license": "MIT" - }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmmirror.com/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "license": "MIT" }, - "node_modules/at-least-node": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", - "dev": true, - "license": "ISC", + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", "engines": { - "node": ">= 4.0.0" + "node": ">=0.1.90" } }, - "node_modules/aws-ssl-profiles": { - "version": "1.1.2", - "resolved": "https://registry.npmmirror.com/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", - "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "node_modules/@dabh/diagnostics": { + "version": "2.0.8", + "resolved": "https://registry.npmmirror.com/@dabh/diagnostics/-/diagnostics-2.0.8.tgz", + "integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==", "license": "MIT", - "engines": { - "node": ">= 6.0.0" + "dependencies": { + "@so-ric/colorspace": "^1.1.6", + "enabled": "2.0.x", + "kuler": "^2.0.0" } }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmmirror.com/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", "dev": true, - "license": "MIT" - }, - "node_modules/basic-auth": { - "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/basic-auth/-/basic-auth-2.0.1.tgz", - "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", "license": "MIT", "dependencies": { - "safe-buffer": "5.1.2" + "eslint-visitor-keys": "^3.4.3" }, "engines": { - "node": ">= 0.8" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "node_modules/basic-auth/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, - "node_modules/bcryptjs": { - "version": "2.4.3", - "resolved": "https://registry.npmmirror.com/bcryptjs/-/bcryptjs-2.4.3.tgz", - "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", - "license": "MIT" - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmmirror.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmmirror.com/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmmirror.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, - "license": "MIT" - }, - "node_modules/body-parser": { - "version": "1.20.5", - "resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-1.20.5.tgz", - "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", "license": "MIT", "dependencies": { - "bytes": "~3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "~1.2.0", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "on-finished": "~2.4.1", - "qs": "~6.15.1", - "raw-body": "~2.5.3", - "type-is": "~1.6.18", - "unpipe": "~1.0.0" + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" }, "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/body-parser/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmmirror.com/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, "license": "MIT", - "dependencies": { - "ms": "2.0.0" + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/body-parser/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmmirror.com/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "license": "BSD-3-Clause" }, - "node_modules/brace-expansion": { - "version": "1.1.15", - "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.15.tgz", - "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", - "dev": true, - "license": "MIT", + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "license": "BSD-3-Clause", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "@hapi/hoek": "^9.0.0" } }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "fill-range": "^7.1.1" + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" }, "engines": { - "node": ">=8" + "node": ">=10.10.0" } }, - "node_modules/buffer-equal-constant-time": { + "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", - "license": "BSD-3-Clause" - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", "engines": { - "node": ">= 0.8" + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=12" } }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, "engines": { - "node": ">= 0.4" + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, "license": "MIT", "engines": { - "node": ">=6" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" }, "engines": { - "node": ">=10" + "node": ">=12" }, "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, "license": "MIT", "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" + "ansi-regex": "^6.2.2" }, "engines": { - "node": ">= 8.10.0" + "node": ">=12" }, "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "is-glob": "^4.0.1" - }, + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, "engines": { - "node": ">= 6" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmmirror.com/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", "dev": true, "license": "ISC", "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" } }, - "node_modules/cluster-key-slot": { - "version": "1.1.2", - "resolved": "https://registry.npmmirror.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", - "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.10.0" + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmmirror.com/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" } }, - "node_modules/color": { - "version": "5.0.3", - "resolved": "https://registry.npmmirror.com/color/-/color-5.0.3.tgz", - "integrity": "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, "license": "MIT", "dependencies": { - "color-convert": "^3.1.3", - "color-string": "^2.1.3" + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" }, "engines": { - "node": ">=18" + "node": ">=8" } }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.15.0", + "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-3.15.0.tgz", + "integrity": "sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==", "dev": true, "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "argparse": "^1.0.7", + "esprima": "^4.0.0" }, - "engines": { - "node": ">=7.0.0" + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dev": true, - "license": "MIT" - }, - "node_modules/color-string": { - "version": "2.1.4", - "resolved": "https://registry.npmmirror.com/color-string/-/color-string-2.1.4.tgz", - "integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==", "license": "MIT", "dependencies": { - "color-name": "^2.0.0" + "p-locate": "^4.1.0" }, "engines": { - "node": ">=18" + "node": ">=8" } }, - "node_modules/color-string/node_modules/color-name": { - "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/color-name/-/color-name-2.1.0.tgz", - "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, "engines": { - "node": ">=12.20" + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/color/node_modules/color-convert": { - "version": "3.1.3", - "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-3.1.3.tgz", - "integrity": "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, "license": "MIT", "dependencies": { - "color-name": "^2.0.0" + "p-limit": "^2.2.0" }, "engines": { - "node": ">=14.6" + "node": ">=8" } }, - "node_modules/color/node_modules/color-name": { - "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/color-name/-/color-name-2.1.0.tgz", - "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, "license": "MIT", "engines": { - "node": ">=12.20" + "node": ">=8" } }, - "node_modules/commander": { - "version": "10.0.1", - "resolved": "https://registry.npmmirror.com/commander/-/commander-10.0.1.tgz", - "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmmirror.com/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", "dev": true, "license": "MIT", "engines": { - "node": ">=14" + "node": ">=8" } }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmmirror.com/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } }, - "node_modules/config-chain": { - "version": "1.1.13", - "resolved": "https://registry.npmmirror.com/config-chain/-/config-chain-1.1.13.tgz", - "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmmirror.com/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", "dev": true, "license": "MIT", "dependencies": { - "ini": "^1.3.4", - "proto-list": "~1.2.1" + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmmirror.com/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, "license": "MIT", "dependencies": { - "safe-buffer": "5.2.1" + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" }, "engines": { - "node": ">= 0.6" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmmirror.com/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmmirror.com/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, "engines": { - "node": ">= 0.6" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmmirror.com/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, "engines": { - "node": ">= 0.6" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/cookie-signature": { - "version": "1.0.7", - "resolved": "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.0.7.tgz", - "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", - "license": "MIT" - }, - "node_modules/cors": { - "version": "2.8.6", - "resolved": "https://registry.npmmirror.com/cors/-/cors-2.8.6.tgz", - "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmmirror.com/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, "license": "MIT", "dependencies": { - "object-assign": "^4", - "vary": "^1" + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" }, "engines": { - "node": ">= 0.10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmmirror.com/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", "dev": true, "license": "MIT", "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" }, "engines": { - "node": ">= 8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmmirror.com/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, "license": "MIT", "dependencies": { - "ms": "^2.1.3" + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" }, "engines": { - "node": ">=6.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" }, "peerDependenciesMeta": { - "supports-color": { + "node-notifier": { "optional": true } } }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmmirror.com/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", "dev": true, - "license": "MIT" - }, - "node_modules/denque": { - "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/denque/-/denque-2.1.0.tgz", - "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", - "license": "Apache-2.0", + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, "engines": { - "node": ">=0.10" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmmirror.com/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, "engines": { - "node": ">= 0.8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmmirror.com/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmmirror.com/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmmirror.com/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmmirror.com/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "esutils": "^2.0.2" + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" }, "engines": { - "node": ">=6.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmmirror.com/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" }, - "funding": { - "url": "https://dotenvx.com" + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/dottie": { - "version": "2.0.7", - "resolved": "https://registry.npmmirror.com/dottie/-/dottie-2.0.7.tgz", - "integrity": "sha512-7lAK2A0b3zZr3UC5aE69CPdCFR4RHW1o2Dr74TqFykxkUCBXSRJum/yPc7g8zRHJqWKomPLHwFLLoUnn8PXXRg==", - "license": "MIT" - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmmirror.com/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" }, "engines": { - "node": ">= 0.4" - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, - "node_modules/ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmmirror.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/editorconfig": { - "version": "1.0.7", - "resolved": "https://registry.npmmirror.com/editorconfig/-/editorconfig-1.0.7.tgz", - "integrity": "sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==", + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, "license": "MIT", "dependencies": { - "@one-ini/wasm": "0.1.1", - "commander": "^10.0.0", - "minimatch": "^9.0.1", - "semver": "^7.5.3" - }, - "bin": { - "editorconfig": "bin/editorconfig" - }, - "engines": { - "node": ">=14" + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" } }, - "node_modules/editorconfig/node_modules/brace-expansion": { - "version": "2.1.1", - "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.1.1.tgz", - "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" } }, - "node_modules/editorconfig/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, + "license": "MIT", "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=6.0.0" } }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true, "license": "MIT" }, - "node_modules/enabled": { - "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/enabled/-/enabled-2.0.0.tgz", - "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, "license": "MIT", - "engines": { - "node": ">= 0.8" + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmmirror.com/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 0.4" + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, "engines": { - "node": ">= 0.4" + "node": ">= 8" } }, - "node_modules/es-object-atoms": { - "version": "1.1.2", - "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz", - "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, "engines": { - "node": ">= 0.4" + "node": ">= 8" } }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, "engines": { - "node": ">=6" + "node": ">= 8" } }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "node_modules/@one-ini/wasm": { + "version": "0.1.1", + "resolved": "https://registry.npmmirror.com/@one-ini/wasm/-/wasm-0.1.1.tgz", + "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", + "dev": true, "license": "MIT" }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "node_modules/@paralleldrive/cuid2": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", "dev": true, "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "dependencies": { + "@noble/hashes": "^1.1.5" } }, - "node_modules/eslint": { - "version": "8.57.1", - "resolved": "https://registry.npmmirror.com/eslint/-/eslint-8.57.1.tgz", - "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", - "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmmirror.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "dev": true, "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.1", - "@humanwhocodes/config-array": "^0.13.0", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, + "optional": true, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=14" } }, - "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, - "license": "BSD-2-Clause", + "node_modules/@redis/bloom": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/@redis/bloom/-/bloom-1.2.0.tgz", + "integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/client": { + "version": "1.6.1", + "resolved": "https://registry.npmmirror.com/@redis/client/-/client-1.6.1.tgz", + "integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==", + "license": "MIT", "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" + "cluster-key-slot": "1.1.2", + "generic-pool": "3.9.0", + "yallist": "4.0.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=14" } }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node_modules/@redis/graph": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@redis/graph/-/graph-1.1.1.tgz", + "integrity": "sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" } }, - "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmmirror.com/espree/-/espree-9.6.1.tgz", + "node_modules/@redis/json": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/@redis/json/-/json-1.0.7.tgz", + "integrity": "sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/search": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/@redis/search/-/search-1.2.0.tgz", + "integrity": "sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/time-series": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@redis/time-series/-/time-series-1.1.0.tgz", + "integrity": "sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@sideway/address": { + "version": "4.1.5", + "resolved": "https://registry.npmmirror.com/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", + "license": "BSD-3-Clause" + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.12", + "resolved": "https://registry.npmmirror.com/@sinclair/typebox/-/typebox-0.27.12.tgz", + "integrity": "sha512-hhyNJ+nbR6ZR7pToHvllEFun9TL0sbL+tk/ON75lo+Xas054uez98qRbsuNt7MBCyZKK4+8Yli/OAGZhmfBZ/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmmirror.com/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@so-ric/colorspace": { + "version": "1.1.6", + "resolved": "https://registry.npmmirror.com/@so-ric/colorspace/-/colorspace-1.1.6.tgz", + "integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==", + "license": "MIT", + "dependencies": { + "color": "^5.0.2", + "text-hex": "1.0.x" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmmirror.com/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmmirror.com/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmmirror.com/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmmirror.com/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmmirror.com/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmmirror.com/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmmirror.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.1.1", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-26.1.1.tgz", + "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==", + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmmirror.com/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "license": "MIT" + }, + "node_modules/@types/validator": { + "version": "13.15.10", + "resolved": "https://registry.npmmirror.com/@types/validator/-/validator-13.15.10.tgz", + "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==", + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmmirror.com/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmmirror.com/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/@ungap/structured-clone/-/structured-clone-1.3.3.tgz", + "integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==", + "dev": true, + "license": "ISC" + }, + "node_modules/abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmmirror.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmmirror.com/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmmirror.com/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmmirror.com/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmmirror.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmmirror.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmmirror.com/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.4", + "resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.11.4.tgz", + "integrity": "sha512-s4+sLr9mZ/CyqeRritFeYV/Zx73OAtmaHn6kkBS1XRoJn1hrg3xIDUcpicAEX68tkcIN0iBCgti31C8zxtkhsQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmmirror.com/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmmirror.com/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.5", + "resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-1.20.5.tgz", + "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.7", + "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.7.tgz", + "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.44", + "caniuse-lite": "^1.0.30001806", + "electron-to-chromium": "^1.5.393", + "node-releases": "^2.0.51", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmmirror.com/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001806", + "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", + "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmmirror.com/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmmirror.com/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmmirror.com/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmmirror.com/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/color": { + "version": "5.0.3", + "resolved": "https://registry.npmmirror.com/color/-/color-5.0.3.tgz", + "integrity": "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==", + "license": "MIT", + "dependencies": { + "color-convert": "^3.1.3", + "color-string": "^2.1.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-string": { + "version": "2.1.4", + "resolved": "https://registry.npmmirror.com/color-string/-/color-string-2.1.4.tgz", + "integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==", + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/color-string/node_modules/color-name": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-2.1.0.tgz", + "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-3.1.3.tgz", + "integrity": "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==", + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=14.6" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-2.1.0.tgz", + "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmmirror.com/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmmirror.com/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmmirror.com/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmmirror.com/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmmirror.com/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmmirror.com/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmmirror.com/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmmirror.com/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dottie": { + "version": "2.0.7", + "resolved": "https://registry.npmmirror.com/dottie/-/dottie-2.0.7.tgz", + "integrity": "sha512-7lAK2A0b3zZr3UC5aE69CPdCFR4RHW1o2Dr74TqFykxkUCBXSRJum/yPc7g8zRHJqWKomPLHwFLLoUnn8PXXRg==", + "license": "MIT" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmmirror.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/editorconfig": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/editorconfig/-/editorconfig-1.0.7.tgz", + "integrity": "sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@one-ini/wasm": "0.1.1", + "commander": "^10.0.0", + "minimatch": "^9.0.1", + "semver": "^7.5.3" + }, + "bin": { + "editorconfig": "bin/editorconfig" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/editorconfig/node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/editorconfig/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.396", + "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.396.tgz", + "integrity": "sha512-yHiw2Y3C3H9U6TMbOfoWK/BPreiOPXRfTWPBwQBoZG6/8TB6eOPnsy5oaRYuatR7Fw2SJ4kKforgufeo7fq0EQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmmirror.com/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmmirror.com/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmmirror.com/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmmirror.com/espree/-/espree-9.6.1.tgz", "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, - "license": "BSD-2-Clause", + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmmirror.com/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmmirror.com/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmmirror.com/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmmirror.com/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-async-errors": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/express-async-errors/-/express-async-errors-3.1.1.tgz", + "integrity": "sha512-h6aK1da4tpqWSbyCa3FxB/V6Ehd4EEB15zyQq9qe75OZBp0krinNKuH4rAY+S/U/2I36vdLAUFSjQJ+TFmODng==", + "license": "ISC", + "peerDependencies": { + "express": "^4.16.2" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmmirror.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmmirror.com/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmmirror.com/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.4.3", + "resolved": "https://registry.npmmirror.com/flatted/-/flatted-3.4.3.tgz", + "integrity": "sha512-/zipXxyO6rGvuNGDiULY9MvEGSkb2gaG4GGH4ygMi0ZZzyMHdUZBmntJmx5x1G2VuPytCwGN4xsJP6cw+sK+vQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "license": "MIT" + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formidable": { + "version": "3.5.4", + "resolved": "https://registry.npmmirror.com/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", + "dezalgo": "^1.0.4", + "once": "^1.4.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "url": "https://ko-fi.com/tunnckoCore/commissions" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmmirror.com/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=10" } }, - "node_modules/esquery": { - "version": "1.7.0", - "resolved": "https://registry.npmmirror.com/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true, - "license": "BSD-3-Clause", + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/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/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "license": "MIT", "dependencies": { - "estraverse": "^5.1.0" - }, + "is-property": "^1.0.2" + } + }, + "node_modules/generic-pool": { + "version": "3.9.0", + "resolved": "https://registry.npmmirror.com/generic-pool/-/generic-pool-3.9.0.tgz", + "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", + "license": "MIT", "engines": { - "node": ">=0.10" + "node": ">= 4" } }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmmirror.com/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, + "license": "MIT", "engines": { - "node": ">=4.0" + "node": ">=6.9.0" } }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, - "license": "BSD-2-Clause", + "license": "ISC", "engines": { - "node": ">=4.0" + "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=8.0.0" } }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, "engines": { - "node": ">= 0.6" + "node": ">= 0.4" } }, - "node_modules/express": { - "version": "4.22.2", - "resolved": "https://registry.npmmirror.com/express/-/express-4.22.2.tgz", - "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "~1.20.5", - "content-disposition": "~0.5.4", - "content-type": "~1.0.4", - "cookie": "~0.7.1", - "cookie-signature": "~1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.3.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "~0.1.12", - "proxy-addr": "~2.0.7", - "qs": "~6.15.1", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "~0.19.0", - "serve-static": "~1.16.2", - "setprototypeof": "1.2.0", - "statuses": "~2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" }, "engines": { - "node": ">= 0.10.0" + "node": "*" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/express-async-errors": { - "version": "3.1.1", - "resolved": "https://registry.npmmirror.com/express-async-errors/-/express-async-errors-3.1.1.tgz", - "integrity": "sha512-h6aK1da4tpqWSbyCa3FxB/V6Ehd4EEB15zyQq9qe75OZBp0krinNKuH4rAY+S/U/2I36vdLAUFSjQJ+TFmODng==", + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, "license": "ISC", - "peerDependencies": { - "express": "^4.16.2" + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" } }, - "node_modules/express/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmmirror.com/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, "license": "MIT", "dependencies": { - "ms": "2.0.0" + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/express/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true, - "license": "MIT" + "license": "ISC" }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmmirror.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true, "license": "MIT" }, - "node_modules/fastq": { - "version": "1.20.1", - "resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.20.1.tgz", - "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" + "license": "MIT", + "engines": { + "node": ">=8" } }, - "node_modules/fecha": { - "version": "4.2.3", - "resolved": "https://registry.npmmirror.com/fecha/-/fecha-4.2.3.tgz", - "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", - "license": "MIT" + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, "license": "MIT", "dependencies": { - "flat-cache": "^3.0.4" + "has-symbols": "^1.0.3" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { - "to-regex-range": "^5.0.1" + "function-bind": "^1.1.2" }, "engines": { - "node": ">=8" + "node": ">= 0.4" } }, - "node_modules/finalhandler": { - "version": "1.3.2", - "resolved": "https://registry.npmmirror.com/finalhandler/-/finalhandler-1.3.2.tgz", - "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "node_modules/helmet": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/helmet/-/helmet-7.2.0.tgz", + "integrity": "sha512-ZRiwvN089JfMXokizgqEPXsl2Guk094yExfoDXR0cBYWxtBbaSww/w+vT4WEJsBW2iTUi1GgZ6swmoug3Oy4Xw==", + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "license": "MIT", "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", "statuses": "~2.0.2", - "unpipe": "~1.0.0" + "toidentifier": "~1.0.1" }, "engines": { "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "license": "MIT", "dependencies": { - "ms": "2.0.0" + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" } }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmmirror.com/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, "license": "MIT", "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" }, "engines": { - "node": ">=10" + "node": ">=6" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/flat-cache": { + "node_modules/import-local": { "version": "3.2.0", - "resolved": "https://registry.npmmirror.com/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "resolved": "https://registry.npmmirror.com/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", "dev": true, "license": "MIT", "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/flatted": { - "version": "3.4.3", - "resolved": "https://registry.npmmirror.com/flatted/-/flatted-3.4.3.tgz", - "integrity": "sha512-/zipXxyO6rGvuNGDiULY9MvEGSkb2gaG4GGH4ygMi0ZZzyMHdUZBmntJmx5x1G2VuPytCwGN4xsJP6cw+sK+vQ==", + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflection": { + "version": "1.13.4", + "resolved": "https://registry.npmmirror.com/inflection/-/inflection-1.13.4.tgz", + "integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==", + "engines": [ + "node >= 0.4.0" + ], + "license": "MIT" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, - "node_modules/fn.name": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/fn.name/-/fn.name-1.1.0.tgz", - "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmmirror.com/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmmirror.com/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, "license": "MIT" }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmmirror.com/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" + "hasown": "^2.0.3" }, "engines": { - "node": ">=14" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=0.10.0" } }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmmirror.com/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=8" } }, - "node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "license": "MIT", "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" + "is-extglob": "^2.1.1" }, "engines": { - "node": ">=10" + "node": ">=0.10.0" } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, - "license": "ISC" + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true, - "hasInstallScript": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": ">=8" } }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "license": "MIT" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "license": "MIT", + "engines": { + "node": ">=8" + }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/generate-function": { - "version": "2.3.1", - "resolved": "https://registry.npmmirror.com/generate-function/-/generate-function-2.3.1.tgz", - "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", - "license": "MIT", - "dependencies": { - "is-property": "^1.0.2" - } + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" }, - "node_modules/generic-pool": { - "version": "3.9.0", - "resolved": "https://registry.npmmirror.com/generic-pool/-/generic-pool-3.9.0.tgz", - "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", - "license": "MIT", + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmmirror.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", "engines": { - "node": ">= 4" + "node": ">=8" } }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmmirror.com/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", "dev": true, - "license": "ISC", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, "engines": { - "node": "6.* || 8.* || >= 10.*" + "node": ">=10" } }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=10" } }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" }, "engines": { - "node": ">= 0.4" + "node": ">=10" } }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", "dev": true, - "license": "ISC", + "license": "BSD-3-Clause", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" }, "engines": { - "node": "*" + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmmirror.com/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" }, "funding": { "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmmirror.com/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "is-glob": "^4.0.3" + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" }, "engines": { - "node": ">=10.13.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmmirror.com/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmmirror.com/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", "dev": true, "license": "MIT", "dependencies": { - "type-fest": "^0.20.2" + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" }, "engines": { - "node": ">=8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmmirror.com/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmmirror.com/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, "engines": { - "node": ">= 0.4" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-cli/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmmirror.com/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=12" } }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmmirror.com/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "node_modules/jest-cli/node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmmirror.com/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/jest-cli/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, - "license": "MIT", + "license": "ISC", "engines": { - "node": ">=8" + "node": ">=12" } }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmmirror.com/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, "engines": { - "node": ">= 0.4" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } } }, - "node_modules/hasown": { - "version": "2.0.4", - "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.4.tgz", - "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmmirror.com/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, "license": "MIT", "dependencies": { - "function-bind": "^1.1.2" + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" }, "engines": { - "node": ">= 0.4" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/helmet": { - "version": "7.2.0", - "resolved": "https://registry.npmmirror.com/helmet/-/helmet-7.2.0.tgz", - "integrity": "sha512-ZRiwvN089JfMXokizgqEPXsl2Guk094yExfoDXR0cBYWxtBbaSww/w+vT4WEJsBW2iTUi1GgZ6swmoug3Oy4Xw==", + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmmirror.com/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, "engines": { - "node": ">=16.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmmirror.com/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, "license": "MIT", "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" }, "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmmirror.com/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" }, "engines": { - "node": ">=0.10.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmmirror.com/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", "dev": true, "license": "MIT", "engines": { - "node": ">= 4" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/ignore-by-default": { - "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz", - "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", - "dev": true, - "license": "ISC" - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmmirror.com/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", "dev": true, "license": "MIT", "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" }, "engines": { - "node": ">=6" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "optionalDependencies": { + "fsevents": "^2.3.2" } }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmmirror.com/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", "dev": true, "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, "engines": { - "node": ">=0.8.19" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/inflection": { - "version": "1.13.4", - "resolved": "https://registry.npmmirror.com/inflection/-/inflection-1.13.4.tgz", - "integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==", - "engines": [ - "node >= 0.4.0" - ], - "license": "MIT" - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmmirror.com/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "once": "^1.3.0", - "wrappy": "1" + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmmirror.com/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmmirror.com/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", "dev": true, - "license": "ISC" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, "engines": { - "node": ">= 0.10" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmmirror.com/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", "dev": true, "license": "MIT", "dependencies": { - "binary-extensions": "^2.0.0" + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" }, "engines": { - "node": ">=8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-core-module": { - "version": "2.16.2", - "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.2.tgz", - "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", "dev": true, "license": "MIT", - "dependencies": { - "hasown": "^2.0.3" - }, "engines": { - "node": ">= 0.4" + "node": ">=6" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } } }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmmirror.com/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", "dev": true, "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmmirror.com/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", "dev": true, "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, "engines": { - "node": ">=8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmmirror.com/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", "dev": true, "license": "MIT", "dependencies": { - "is-extglob": "^2.1.1" + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" }, "engines": { - "node": ">=0.10.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmmirror.com/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", "dev": true, "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmmirror.com/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmmirror.com/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, "engines": { - "node": ">=0.12.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmmirror.com/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmmirror.com/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", "dev": true, "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, "engines": { - "node": ">=8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-property": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/is-property/-/is-property-1.0.2.tgz", - "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", - "license": "MIT" + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmmirror.com/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmmirror.com/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmmirror.com/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmmirror.com/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", "dev": true, - "license": "ISC" + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmmirror.com/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, - "license": "BlueOak-1.0.0", + "license": "MIT", "dependencies": { - "@isaacs/cliui": "^8.0.2" + "has-flag": "^4.0.0" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "engines": { + "node": ">=10" }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, "node_modules/joi": { @@ -2229,6 +4789,13 @@ "dev": true, "license": "MIT" }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/js-yaml": { "version": "4.3.0", "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.3.0.tgz", @@ -2252,6 +4819,19 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmmirror.com/json-buffer/-/json-buffer-3.0.1.tgz", @@ -2259,6 +4839,13 @@ "dev": true, "license": "MIT" }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -2273,6 +4860,19 @@ "dev": true, "license": "MIT" }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/jsonfile": { "version": "6.2.1", "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.1.tgz", @@ -2339,12 +4939,32 @@ "json-buffer": "3.0.1" } }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/kuler": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/kuler/-/kuler-2.0.0.tgz", "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", "license": "MIT" }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmmirror.com/levn/-/levn-0.4.1.tgz", @@ -2359,6 +4979,13 @@ "node": ">= 0.8.0" } }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz", @@ -2475,6 +5102,32 @@ "url": "https://github.com/sponsors/wellwelwel" } }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmmirror.com/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -2502,6 +5155,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmmirror.com/methods/-/methods-1.1.2.tgz", @@ -2511,6 +5171,20 @@ "node": ">= 0.6" } }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmmirror.com/mime/-/mime-1.6.0.tgz", @@ -2544,6 +5218,16 @@ "node": ">= 0.6" } }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz", @@ -2695,6 +5379,23 @@ "node": ">= 0.6" } }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.51", + "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/nodemon": { "version": "3.1.14", "resolved": "https://registry.npmmirror.com/nodemon/-/nodemon-3.1.14.tgz", @@ -2812,6 +5513,19 @@ "node": ">=0.10.0" } }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", @@ -2873,6 +5587,22 @@ "fn.name": "1.x.x" } }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmmirror.com/optionator/-/optionator-0.9.4.tgz", @@ -2923,6 +5653,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -2943,6 +5683,25 @@ "node": ">=6" } }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz", @@ -2982,70 +5741,191 @@ "node": ">=8" } }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/pg-connection-string": { + "version": "2.12.0", + "resolved": "https://registry.npmmirror.com/pg-connection-string/-/pg-connection-string-2.12.0.tgz", + "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmmirror.com/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmmirror.com/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmmirror.com/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, - "license": "BlueOak-1.0.0", + "license": "MIT", "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" }, "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/path-to-regexp": { - "version": "0.1.13", - "resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-0.1.13.tgz", - "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", - "license": "MIT" - }, - "node_modules/pg-connection-string": { - "version": "2.12.0", - "resolved": "https://registry.npmmirror.com/pg-connection-string/-/pg-connection-string-2.12.0.tgz", - "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==", - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", "engines": { - "node": ">=8.6" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmmirror.com/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", "dev": true, "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, "engines": { - "node": ">= 0.8.0" + "node": ">= 6" } }, "node_modules/proto-list": { @@ -3085,6 +5965,23 @@ "node": ">=6" } }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmmirror.com/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, "node_modules/qs": { "version": "6.15.3", "resolved": "https://registry.npmmirror.com/qs/-/qs-6.15.3.tgz", @@ -3146,6 +6043,13 @@ "node": ">= 0.8" } }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmmirror.com/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz", @@ -3222,6 +6126,29 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz", @@ -3232,6 +6159,16 @@ "node": ">=4" } }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/retry-as-promised": { "version": "7.1.1", "resolved": "https://registry.npmmirror.com/retry-as-promised/-/retry-as-promised-7.1.1.tgz", @@ -3612,6 +6549,51 @@ "node": ">=10" } }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmmirror.com/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/sql-escaper": { "version": "1.5.1", "resolved": "https://registry.npmmirror.com/sql-escaper/-/sql-escaper-1.5.1.tgz", @@ -3636,6 +6618,29 @@ "node": "*" } }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmmirror.com/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.2.tgz", @@ -3654,6 +6659,20 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", @@ -3712,6 +6731,26 @@ "node": ">=8" } }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -3725,6 +6764,65 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/superagent": { + "version": "10.3.0", + "resolved": "https://registry.npmmirror.com/superagent/-/superagent-10.3.0.tgz", + "integrity": "sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "component-emitter": "^1.3.1", + "cookiejar": "^2.1.4", + "debug": "^4.3.7", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.5", + "formidable": "^3.5.4", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.14.1" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/superagent/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/supertest": { + "version": "7.2.2", + "resolved": "https://registry.npmmirror.com/supertest/-/supertest-7.2.2.tgz", + "integrity": "sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cookie-signature": "^1.2.2", + "methods": "^1.1.2", + "superagent": "^10.3.0" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/supertest/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", @@ -3751,6 +6849,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/text-hex": { "version": "1.0.0", "resolved": "https://registry.npmmirror.com/text-hex/-/text-hex-1.0.0.tgz", @@ -3764,6 +6877,13 @@ "dev": true, "license": "MIT" }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -3824,6 +6944,16 @@ "node": ">= 0.8.0" } }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmmirror.com/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.20.2.tgz", @@ -3895,6 +7025,37 @@ "node": ">= 0.8" } }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz", @@ -3929,6 +7090,21 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmmirror.com/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, "node_modules/validator": { "version": "13.15.35", "resolved": "https://registry.npmmirror.com/validator/-/validator-13.15.35.tgz", @@ -3947,6 +7123,16 @@ "node": ">= 0.8" } }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", @@ -4062,6 +7248,27 @@ "dev": true, "license": "ISC" }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/write-file-atomic/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz", diff --git a/backend/package.json b/backend/package.json index dcfcfbd0b31960d31ba0323452c9c6637bc5f07f..6b625674210b02e27a9b231a1bcf24fa54582761 100644 --- a/backend/package.json +++ b/backend/package.json @@ -7,6 +7,8 @@ "dev": "nodemon src/app.js", "start": "node src/app.js", "lint": "eslint \"src/**/*.js\"", + "test": "jest --runInBand", + "test:watch": "jest --watch", "db:sync": "node src/scripts/sync-db.js" }, "dependencies": { @@ -26,8 +28,10 @@ }, "devDependencies": { "eslint": "^8.57.0", + "jest": "^29.7.0", "nodemon": "^3.1.4", - "sequelize-cli": "^6.6.2" + "sequelize-cli": "^6.6.2", + "supertest": "^7.0.0" }, "engines": { "node": ">=20.0.0" diff --git a/backend/sql/2026-08-05-stock-log-bundle-id.sql b/backend/sql/2026-08-05-stock-log-bundle-id.sql new file mode 100644 index 0000000000000000000000000000000000000000..54b27f5dba0b43b38974e4e9b9bb79341838f0ea --- /dev/null +++ b/backend/sql/2026-08-05-stock-log-bundle-id.sql @@ -0,0 +1,16 @@ +-- ===================================================== +-- Phase2 final-fix migration: stock_log 加 bundle_id 列 +-- 适用:(Phase2 多规格) stock_log 需要记录 bundle 维度 +-- 日期:2026-08-05 +-- 修复来源:final-review C1 + I3 +-- +-- 说明: +-- - bundle_id 为 nullable BIGINT,保留为信息列(不建 FK); +-- 因为如果用 FK + ON DELETE SET NULL,product_bundle 软删后会把审计 trail 弄丢。 +-- - schema.sql 已同步更新(CREATE TABLE 内自带 bundle_id)。 +-- - 老库用以下 ALTER 一次性补字段即可幂等。 +-- ===================================================== + +ALTER TABLE stock_log + ADD COLUMN bundle_id BIGINT DEFAULT NULL AFTER product_id, + ADD KEY idx_stock_log_bundle (bundle_id); diff --git a/backend/sql/add-categories-baihu-wujin.sql b/backend/sql/add-categories-baihu-wujin.sql new file mode 100644 index 0000000000000000000000000000000000000000..5a17850628c7f6ea3260085d0b4453cd4db48cee --- /dev/null +++ b/backend/sql/add-categories-baihu-wujin.sql @@ -0,0 +1,23 @@ +-- 新增 2 个分类:品质五金 + 超值百货 +-- 包含示例商品(每个分类 4 个) +-- 执行:mysql -u root -p123456 --default-character-set=utf8mb4 eshop < backend/sql/add-categories-baihu-wujin.sql + +-- 插入新分类 +INSERT INTO `category` (`id`, `name`, `parent_id`, `sort`) VALUES +(5, '品质五金', 0, 5), +(6, '超值百货', 0, 6) +ON DUPLICATE KEY UPDATE `name` = VALUES(`name`); + +-- 品质五金 商品(4 个) +INSERT INTO `product` (`category_id`, `name`, `description`, `price`, `stock`, `image`, `on_sale`) VALUES +(5, '博世 电动工具套装 家用维修', '电钻 + 螺丝刀 + 扳手 58 件套', 599.00, 100, 'https://picsum.photos/seed/wujin01/400/400', 1), +(5, '威克士 12V 锂电钻', '双速调档 家用电钻 附配件箱', 329.00, 80, 'https://picsum.photos/seed/wujin02/400/400', 1), +(5, '史丹利 工具箱 17 寸', '家用收纳大号工具箱 耐磨耐摔', 268.00, 60, 'https://picsum.photos/seed/wujin03/400/400', 1), +(5, '牧田 电锤三功能 800W', '电钻电镐两用 SDS-plus 钻头套装', 799.00, 40, 'https://picsum.photos/seed/wujin04/400/400', 1); + +-- 超值百货 商品(4 个) +INSERT INTO `product` (`category_id`, `name`, `description`, `price`, `stock`, `image`, `on_sale`) VALUES +(6, '良品铺子 坚果大礼包 1.5kg', '8 种混合 每日坚果 送礼佳品', 158.00, 200, 'https://picsum.photos/seed/baihu01/400/400', 1), +(6, '三只松鼠 零食大礼包 2kg', '坚果零食 礼盒装 节日送礼', 188.00, 150, 'https://picsum.photos/seed/baihu02/400/400', 1), +(6, '农夫山泉 天然水 24 瓶 550ml', '整箱饮用水 家庭装', 35.00, 500, 'https://picsum.photos/seed/baihu03/400/400', 1), +(6, '蓝月亮 洗衣液 12 斤装', '亮白增艳 机洗专用', 89.00, 300, 'https://picsum.photos/seed/baihu04/400/400', 1); diff --git a/backend/sql/data.sql b/backend/sql/data.sql index 9a8e04a1657110088228694db10d70fef4ef2112..019c2eef006e42c07ae717cbd662e6b3a7e4abee 100644 --- a/backend/sql/data.sql +++ b/backend/sql/data.sql @@ -1,8 +1,8 @@ -- ===================================================== --- 演示数据:3 分类 + 30 商品 + 2 管理员 + 1 测试买家 +-- 演示数据:4 分类 + 30 商品 + 2 管理员 + 1 测试买家 -- admin 密码:admin123 -- tester 密码:buyer123 --- 图片使用 picsum 占位图(https://picsum.photos/seed//400/400) +-- 图片使用 Unsplash CDN 真实图片 + picsum 备用 -- ===================================================== USE `eshop`; @@ -14,47 +14,55 @@ INSERT INTO `user` (`username`, `password`, `phone`, `role`, `status`) VALUES ('admin', '$2a$10$T.de0y4Z16oMs5zuaRVAiutzMvuBCIRlUfr5gR9kWaN/kJBk9Z8z2', '13800000000', 1, 1), ('tester', '$2a$10$VeVK/gqcTidG1BLoFbo5Pu5IFVDlbKQ8e/XgrIWRssLhVcoTiBdZC', '13800000001', 0, 1); --- 商品分类 +-- 商品分类(淘宝风格:服装时尚 / 美妆个护 / 家居生活 / 数码家电) INSERT INTO `category` (`id`, `name`, `parent_id`, `sort`) VALUES -(1, '手机数码', 0, 1), -(2, '电脑办公', 0, 2), -(3, '家用电器', 0, 3); +(1, '服装时尚', 0, 1), +(2, '美妆个护', 0, 2), +(3, '家居生活', 0, 3), +(4, '数码家电', 0, 4); --- 30 个商品(每个分类 10 个) --- image 字段使用 picsum 在线占位图(按商品 id 固定 seed,同一商品永远显示同一张图) +-- 30 个商品(淘宝风格:每类 7-8 个,混合图片) +-- image 字段使用 Unsplash CDN 真实图片(按商品类型匹配) INSERT INTO `product` (`category_id`, `name`, `description`, `price`, `stock`, `image`, `on_sale`) VALUES -(1, 'iPhone 15 Pro', 'Apple iPhone 15 Pro 256GB', 7999.00, 50, 'https://picsum.photos/seed/1/400/400', 1), -(1, 'iPhone 15', 'Apple iPhone 15 128GB', 5999.00, 80, 'https://picsum.photos/seed/2/400/400', 1), -(1, '华为 Mate 60 Pro', '华为旗舰 512GB', 6999.00, 40, 'https://picsum.photos/seed/3/400/400', 1), -(1, '小米 14 Pro', '骁龙8 Gen3 旗舰', 4999.00, 60, 'https://picsum.photos/seed/4/400/400', 1), -(1, 'OPPO Find X7', '天玑9300', 4499.00, 30, 'https://picsum.photos/seed/5/400/400', 1), -(1, 'vivo X100 Pro', '蔡司影像旗舰', 5499.00, 35, 'https://picsum.photos/seed/6/400/400', 1), -(1, '三星 Galaxy S24', '骁龙8 Gen3', 6299.00, 25, 'https://picsum.photos/seed/7/400/400', 1), -(1, 'iQOO 12 Pro', '游戏旗舰', 4999.00, 28, 'https://picsum.photos/seed/8/400/400', 1), -(1, '荣耀 Magic 6', '骁龙8 Gen3', 4599.00, 32, 'https://picsum.photos/seed/9/400/400', 1), -(1, '一加 12', '哈苏影像', 4299.00, 45, 'https://picsum.photos/seed/10/400/400', 1), -(2, 'MacBook Pro M3', '14 寸 16+512', 14999.00, 20, 'https://picsum.photos/seed/11/400/400', 1), -(2, 'MacBook Air M2', '13 寸 8+256', 8999.00, 35, 'https://picsum.photos/seed/12/400/400', 1), -(2, 'ThinkPad X1 Carbon','i7 16+512', 12999.00, 18, 'https://picsum.photos/seed/13/400/400', 1), -(2, 'Dell XPS 15', 'i7 16+1TB', 13999.00, 15, 'https://picsum.photos/seed/14/400/400', 1), -(2, '华为 MateBook X Pro','i7 16+1TB', 11999.00, 22, 'https://picsum.photos/seed/15/400/400', 1), -(2, '联想小新 Pro 14', 'i5 16+512', 5999.00, 40, 'https://picsum.photos/seed/16/400/400', 1), -(2, '华硕灵耀 14', 'i7 16+1TB', 7999.00, 25, 'https://picsum.photos/seed/17/400/400', 1), -(2, '惠普战 66', 'i5 16+512', 4999.00, 30, 'https://picsum.photos/seed/18/400/400', 1), -(2, 'iPad Pro 11', 'M2 256GB', 6799.00, 35, 'https://picsum.photos/seed/19/400/400', 1), -(2, 'Surface Pro 9', 'i7 16+512', 9999.00, 18, 'https://picsum.photos/seed/20/400/400', 1), -(3, '美的变频空调', '1.5 匹一级能效', 2999.00, 50, 'https://picsum.photos/seed/21/400/400', 1), -(3, '格力空调', '1 匹一级能效', 2599.00, 60, 'https://picsum.photos/seed/22/400/400', 1), -(3, '海尔洗衣机', '10kg 变频滚筒', 3499.00, 40, 'https://picsum.photos/seed/23/400/400', 1), -(3, '西门子冰箱', '对开门 580L', 6499.00, 25, 'https://picsum.photos/seed/24/400/400', 1), -(3, '小米电视 65', '4K 全面屏', 2499.00, 80, 'https://picsum.photos/seed/25/400/400', 1), -(3, '华为智慧屏 75', 'HarmonyOS', 5999.00, 30, 'https://picsum.photos/seed/26/400/400', 1), -(3, '戴森吹风机', 'HD15 新款', 2999.00, 35, 'https://picsum.photos/seed/27/400/400', 1), -(3, '飞利浦电动牙刷', 'HX9924', 1299.00, 100, 'https://picsum.photos/seed/28/400/400', 1), -(3, '九阳破壁机', '低音预约', 599.00, 150, 'https://picsum.photos/seed/29/400/400', 1), -(3, '美的电饭煲', 'IH 加热 4L', 799.00, 80, 'https://picsum.photos/seed/30/400/400', 1); +-- 服装时尚 +(1, '馨帮帮 蝴蝶结系带高领无袖格纹背心', '小立领薄款透视吊带女夏 2026 新款', 108.00, 200, 'https://images.unsplash.com/photo-1591047139829-d91aecb6caea?w=400&h=400&fit=crop', 1), +(1, '辣妹日记CL「猎手」油光银色裙', '超短油亮包臀裙 秘书制服演出服', 61.00, 150, 'https://images.unsplash.com/photo-1539109136881-3be0616acf4b?w=400&h=400&fit=crop', 1), +(1, '小朵NANA 老钱风套装 高级感显瘦', '轻奢通勤西装上衣阔腿裤三件套', 249.00, 80, 'https://images.unsplash.com/photo-1542295669297-4d352b042bca?w=400&h=400&fit=crop', 1), +(1, '一眼心动!DREW 笑脸T恤 情侣同款', '潮牌 ins 风圆领短袖纯棉宽松上衣', 109.00, 300, 'https://images.unsplash.com/photo-1521572163474-6864f9cf17ab?w=400&h=400&fit=crop', 1), +(1, '【厚底增高】六也休闲运动风单鞋', '2026 春新款百搭显瘦小白鞋女鞋', 348.00, 120, 'https://images.unsplash.com/photo-1542291026-7eec264c27ff?w=400&h=400&fit=crop', 1), +(1, '美式高街 PU 皮外套男款秋冬季', '飞行夹克棒球服痞帅外套男', 269.00, 90, 'https://images.unsplash.com/photo-1551028719-00167b16eac5?w=400&h=400&fit=crop', 1), +(1, '运动瑜伽裤女高腰提臀紧身裤', '速干跑步健身裤 2026 新款', 89.00, 250, 'https://images.unsplash.com/photo-1506629082955-511b1aa562c8?w=400&h=400&fit=crop', 1), +(1, '夏季薄款棉麻衬衫女宽松上衣', '文艺范亚麻短袖衬衫 2026 新款', 128.00, 180, 'https://images.unsplash.com/photo-1485518882345-15568b007407?w=400&h=400&fit=crop', 1), +-- 美妆个护 +(2, '资生堂 安耐晒防晒霜 60g', 'SPF50+ 防水防汗 清爽不油腻', 138.00, 500, 'https://picsum.photos/seed/skincare09/400/400', 1), +(2, '怡丽丝尔 金色抗皱防晒霜', 'SPF50+ PA++++ 50g 清爽不油腻', 198.00, 200, 'https://images.unsplash.com/photo-1556228720-195a672e8a03?w=400&h=400&fit=crop', 1), +(2, '安热沙 金瓶防晒霜 60ml', '资生堂防水防汗 SPF50+ 户外军训', 218.00, 300, 'https://images.unsplash.com/photo-1556228841-a3c527ebefe5?w=400&h=400&fit=crop', 1), +(2, '海蓝之谜 修护精华露 30ml', '浓缩修护精萃液 抗皱保湿官方正品', 2680.00, 50, 'https://images.unsplash.com/photo-1571781926291-c477ebfd024b?w=400&h=400&fit=crop', 1), +(2, '欧莱雅 玻尿酸洗发水 700ml', '保湿补水改善毛躁 男女士官方', 79.00, 400, 'https://images.unsplash.com/photo-1556228852-80b6e5eeff06?w=400&h=400&fit=crop', 1), +(2, 'Olay 烟酰胺亮肤精华 30ml', '美白淡斑 提亮肤色 改善暗沉', 199.00, 250, 'https://picsum.photos/seed/skincare14/400/400', 1), +(2, '可复美 重组胶原蛋白面膜', '5 片装 补水修护敏感 官方正品', 138.00, 350, 'https://images.unsplash.com/photo-1556228578-8c89e6adf883?w=400&h=400&fit=crop', 1), +(2, '迪奥 真我香水 50ml', 'EDP 经典花香调 女士持久官方', 880.00, 80, 'https://images.unsplash.com/photo-1541643600914-78b084ae601a?w=400&h=400&fit=crop', 1), +-- 家居生活 +(3, '德国 BRITA 滤水壶 3.5L', '厨房净水器家用直饮滤芯正品', 258.00, 150, 'https://images.unsplash.com/photo-1559839914-17a04fcd6328?w=400&h=400&fit=crop', 1), +(3, '新秀丽 行李箱 万向轮静音', '20 寸登机箱 拉杆箱旅行箱密码', 1690.00, 60, 'https://images.unsplash.com/photo-1565025929-c1f4bcce16d0?w=400&h=400&fit=crop', 1), +(3, '哥伦比亚 户外背包 30L', '防泼水大容量 徒步登山旅行', 458.00, 200, 'https://picsum.photos/seed/backpack19/400/400', 1), +(3, 'MUJI 无印良品 四件套床品', '纯棉水洗棉被套床单 1.8m 床', 388.00, 180, 'https://images.unsplash.com/photo-1522771739844-6a9f6d5f14af?w=400&h=400&fit=crop', 1), +(3, '戴森 V12 无线吸尘器', '家用小型手持除螨吸尘器大吸力', 3690.00, 40, 'https://images.unsplash.com/photo-1558317374-067fb5f30010?w=400&h=400&fit=crop', 1), +(3, '华为 Sound Joy 智能音箱', '帝瓦雷调音 蓝牙便携户外音响', 999.00, 100, 'https://images.unsplash.com/photo-1608043152269-423dbba4e7e1?w=400&h=400&fit=crop', 1), +(3, '膳魔师 保温杯 500ml', '日本进口直饮真空不锈钢水杯', 218.00, 280, 'https://images.unsplash.com/photo-1602143407151-7111542de6e8?w=400&h=400&fit=crop', 1), +(3, 'Vitamix 破壁机 家用加热', '全自动多功能豆浆料理机官方', 4980.00, 30, 'https://images.unsplash.com/photo-1570222094114-d054a817e56b?w=400&h=400&fit=crop', 1), +-- 数码家电 +(4, 'iPhone 15 Pro 256GB', 'Apple 旗舰手机 钛金属 A17 Pro', 7999.00, 50, 'https://images.unsplash.com/photo-1592890288564-76628a30a657?w=400&h=400&fit=crop', 1), +(4, '华为 Mate 60 Pro 512GB', '麒麟 9000S 旗舰 5G 现货', 6999.00, 40, 'https://images.unsplash.com/photo-1598327105666-5b89351aff97?w=400&h=400&fit=crop', 1), +(4, 'MacBook Pro 14 M3', '16+512 银色 深空灰', 14999.00, 20, 'https://images.unsplash.com/photo-1496181133206-80ce9b88a853?w=400&h=400&fit=crop', 1), +(4, '索尼 WH-1000XM5 头戴耳机', '无线蓝牙主动降噪 黑色', 2399.00, 60, 'https://images.unsplash.com/photo-1546435770-3e9dc4b28b15?w=400&h=400&fit=crop', 1), +(4, '小米电视 65 寸 4K', '全面屏 智能语音液晶平板电视', 2499.00, 80, 'https://images.unsplash.com/photo-1560169897-fc0cdbdfa4d5?w=400&h=400&fit=crop', 1), +(4, '戴森吹风机 HD15', '新一代 智能恒温 速干', 2999.00, 35, 'https://images.unsplash.com/photo-1522338140262-f46f5913618a?w=400&h=400&fit=crop', 1), +(4, '九阳 破壁机 Y912', '低音预约多功能豆浆机', 599.00, 150, 'https://images.unsplash.com/photo-1570222094114-d054a817e56b?w=400&h=400&fit=crop', 1), +(4, '美的 变频空调 1.5 匹', '一级能效 智能冷暖挂机', 2999.00, 50, 'https://images.unsplash.com/photo-1718203862467-c33159fdc504?w=400&h=400&fit=crop', 1); -- 注意:data.sql 改了图片路径后,需要重新执行: -- mysql -u root -p eshop < backend/sql/data.sql --- 或者只更新 product 表的 image 字段: --- UPDATE product SET image = CONCAT('https://picsum.photos/seed/', id, '/400/400'); \ No newline at end of file +-- 或者只更新 product 表的 image 字段,请使用独立脚本: +-- mysql -u root -p eshop < backend/sql/update-product-images.sql +-- (该脚本将 30 个商品图按品类替换为 Unsplash CDN 真实图片,避免 picsum 占位图风格不一致) diff --git a/backend/sql/demo-data.sql b/backend/sql/demo-data.sql new file mode 100644 index 0000000000000000000000000000000000000000..11c6e0e6edac679fe16c4b093d7321a79dcfb5e7 --- /dev/null +++ b/backend/sql/demo-data.sql @@ -0,0 +1,107 @@ +-- ============================================================ +-- 电子商城演示数据 +-- 包含:3个分类、10个商品、每个商品2-3个SKU +-- ============================================================ + +USE eshop; + +-- ---------- 清理旧数据 ---------- +SET FOREIGN_KEY_CHECKS = 0; +TRUNCATE TABLE sku; +TRUNCATE TABLE product; +TRUNCATE TABLE category; +SET FOREIGN_KEY_CHECKS = 1; + +-- ---------- 分类(3个顶级分类) ---------- +INSERT INTO category (id, name, parent_id, sort) VALUES +(1, '手机数码', 0, 10), +(2, '电脑办公', 0, 20), +(3, '服饰鞋包', 0, 30); + +-- ---------- 手机数码 商品 ---------- +INSERT INTO product (id, category_id, name, description, price, original_price, stock, sales, image, is_hot, is_new, is_recommend, on_sale, deleted) VALUES +(1, 1, '小米手机 Redmi Note 12', 'Redmi Note 12 5G智能手机,搭载骁龙4芯片,5000mAh大电池,33W快充', 999.00, 1299.00, 100, 856, 'https://picsum.photos/seed/xiaomi12/400/400', 1, 0, 1, 1, 0), +(2, 1, '华为 Mate 50 Pro', '华为旗舰手机,XMAGE影像,鸿蒙系统,曲面屏设计', 4999.00, 5999.00, 50, 423, 'https://picsum.photos/seed/huawei50/400/400', 1, 0, 1, 1, 0), +(3, 1, 'iPhone 14 Pro', '苹果旗舰手机,A16仿生芯片,灵动岛设计,4800万像素', 7999.00, 8999.00, 30, 312, 'https://picsum.photos/seed/iphone14/400/400', 1, 0, 1, 1, 0); + +-- ---------- 电脑办公 商品 ---------- +INSERT INTO product (id, category_id, name, description, price, original_price, stock, sales, image, is_hot, is_new, is_recommend, on_sale, deleted) VALUES +(4, 2, '联想小新 Pro 16', '16英寸高性能轻薄本,AMD锐龙7处理器,2.5K高清屏', 5499.00, 6299.00, 40, 267, 'https://picsum.photos/seed/lenovo16/400/400', 0, 1, 1, 1, 0), +(5, 2, '戴尔 XPS 15', '15.6英寸商务旗舰本,Intel酷睿i7,4K触控屏', 8999.00, 10999.00, 20, 189, 'https://picsum.photos/seed/dellxps/400/400', 0, 1, 0, 1, 0), +(6, 2, '罗技 MX Master 3S 鼠标', '无线蓝牙鼠标,人体工学设计,静音点击,MagSpeed滚轮', 799.00, 899.00, 200, 534, 'https://picsum.photos/seed/logitech/400/400', 1, 0, 1, 1, 0); + +-- ---------- 服饰鞋包 商品 ---------- +INSERT INTO product (id, category_id, name, description, price, original_price, stock, sales, image, is_hot, is_new, is_recommend, on_sale, deleted) VALUES +(7, 3, 'Nike Air Max 270', '耐克经典气垫运动鞋,轻盈透气,舒适缓震', 899.00, 1099.00, 80, 445, 'https://picsum.photos/seed/nike270/400/400', 1, 0, 0, 1, 0), +(8, 3, 'Adidas Ultra Boost 22', '阿迪达斯旗舰跑鞋,Boost中底,Primeknit鞋面', 1299.00, 1599.00, 60, 378, 'https://picsum.photos/seed/adidasub/400/400', 0, 1, 1, 1, 0), +(9, 3, '小米双肩包', '简约商务双肩包,大容量分层设计,防泼水面料', 149.00, 199.00, 150, 623, 'https://picsum.photos/seed/xiaomibag/400/400', 0, 0, 1, 1, 0), +(10, 3, 'Apple Watch Series 9', '苹果智能手表,S9芯片,全天候视网膜屏,健康监测', 2999.00, 3499.00, 45, 234, 'https://picsum.photos/seed/applewatch/400/400', 1, 1, 1, 1, 0); + +-- ---------- SKU 数据 ---------- +-- 商品1: Redmi Note 12(颜色、内存) +INSERT INTO sku (product_id, attrs, price, stock, status) VALUES +(1, '{"颜色":"黑色","内存":"8GB+128GB"}', 999.00, 40, 1), +(1, '{"颜色":"黑色","内存":"8GB+256GB"}', 1199.00, 35, 1), +(1, '{"颜色":"白色","内存":"8GB+128GB"}', 999.00, 25, 1); + +-- 商品2: 华为 Mate 50 Pro(颜色、存储) +INSERT INTO sku (product_id, attrs, price, stock, status) VALUES +(2, '{"颜色":"曜金黑","存储":"256GB"}', 4999.00, 20, 1), +(2, '{"颜色":"曜金黑","存储":"512GB"}', 5999.00, 18, 1), +(2, '{"颜色":"冰霜银","存储":"256GB"}', 4999.00, 12, 1); + +-- 商品3: iPhone 14 Pro(颜色、存储) +INSERT INTO sku (product_id, attrs, price, stock, status) VALUES +(3, '{"颜色":"深空黑","存储":"128GB"}', 7999.00, 10, 1), +(3, '{"颜色":"深空黑","存储":"256GB"}', 8999.00, 12, 1), +(3, '{"颜色":"金色","存储":"128GB"}', 7999.00, 8, 1); + +-- 商品4: 联想小新 Pro 16(颜色、配置) +INSERT INTO sku (product_id, attrs, price, stock, status) VALUES +(4, '{"颜色":"深空灰","配置":"R7/16GB/512GB"}', 5499.00, 20, 1), +(4, '{"颜色":"深空灰","配置":"R7/32GB/1TB"}', 6499.00, 15, 1), +(4, '{"颜色":"银色","配置":"R7/16GB/512GB"}', 5499.00, 5, 1); + +-- 商品5: 戴尔 XPS 15(配置) +INSERT INTO sku (product_id, attrs, price, stock, status) VALUES +(5, '{"配置":"i7/16GB/512GB/集显"}', 8999.00, 8, 1), +(5, '{"配置":"i7/32GB/1TB/独显"}', 11999.00, 7, 1), +(5, '{"配置":"i9/64GB/2TB/独显"}', 16999.00, 5, 1); + +-- 商品6: 罗技 MX Master 3S(颜色) +INSERT INTO sku (product_id, attrs, price, stock, status) VALUES +(6, '{"颜色":"石墨黑"}', 799.00, 100, 1), +(6, '{"颜色":"银河灰"}', 799.00, 100, 1); + +-- 商品7: Nike Air Max 270(颜色、尺码) +INSERT INTO sku (product_id, attrs, price, stock, status) VALUES +(7, '{"颜色":"黑色","尺码":"40"}', 899.00, 15, 1), +(7, '{"颜色":"黑色","尺码":"41"}', 899.00, 20, 1), +(7, '{"颜色":"白色","尺码":"40"}', 899.00, 15, 1), +(7, '{"颜色":"白色","尺码":"42"}', 899.00, 30, 1); + +-- 商品8: Adidas Ultra Boost 22(颜色、尺码) +INSERT INTO sku (product_id, attrs, price, stock, status) VALUES +(8, '{"颜色":"黑色","尺码":"40"}', 1299.00, 20, 1), +(8, '{"颜色":"黑色","尺码":"43"}', 1299.00, 25, 1), +(8, '{"颜色":"白色","尺码":"40"}', 1299.00, 15, 1); + +-- 商品9: 小米双肩包(颜色) +INSERT INTO sku (product_id, attrs, price, stock, status) VALUES +(9, '{"颜色":"黑色"}', 149.00, 80, 1), +(9, '{"颜色":"灰色"}', 149.00, 70, 1); + +-- 商品10: Apple Watch Series 9(表壳、表带) +INSERT INTO sku (product_id, attrs, price, stock, status) VALUES +(10, '{"表壳":"铝金属午夜色","表带":"运动型"}', 2999.00, 20, 1), +(10, '{"表壳":"铝金属星光色","表带":"运动型"}', 2999.00, 15, 1), +(10, '{"表壳":"不锈钢银色","表带":"皮革型"}', 4999.00, 10, 1); + +-- ---------- 验证 ---------- +SELECT '分类数量:' AS info, COUNT(*) AS cnt FROM category; +SELECT '商品数量:' AS info, COUNT(*) AS cnt FROM product; +SELECT 'SKU数量:' AS info, COUNT(*) AS cnt FROM sku; +SELECT p.name, COUNT(s.id) AS sku_count +FROM product p LEFT JOIN sku s ON p.id = s.product_id +GROUP BY p.id, p.name +ORDER BY p.id; diff --git a/backend/sql/migrate-add-product-fields.sql b/backend/sql/migrate-add-product-fields.sql new file mode 100644 index 0000000000000000000000000000000000000000..ebe43a019017e0625ed4a2026972803567085bcf --- /dev/null +++ b/backend/sql/migrate-add-product-fields.sql @@ -0,0 +1,57 @@ +-- ===================================================== +-- 商品表字段扩展迁移脚本 +-- 添加销量、原价、热销/新品/推荐标记字段 +-- 执行方式:mysql -u root -p eshop < migrate-add-product-fields.sql +-- ===================================================== + +USE `eshop`; + +-- 1. 添加销量字段 +ALTER TABLE `product` ADD COLUMN `sales` INT NOT NULL DEFAULT 0 COMMENT '销量' AFTER `stock`; + +-- 2. 添加原价字段(用于显示划线价) +ALTER TABLE `product` ADD COLUMN `original_price` DECIMAL(10,2) DEFAULT NULL COMMENT '原价(划线价)' AFTER `price`; + +-- 3. 添加热销标记 +ALTER TABLE `product` ADD COLUMN `is_hot` TINYINT NOT NULL DEFAULT 0 COMMENT '是否热销 1=是 0=否' AFTER `image`; + +-- 4. 添加新品标记 +ALTER TABLE `product` ADD COLUMN `is_new` TINYINT NOT NULL DEFAULT 0 COMMENT '是否新品 1=是 0=否' AFTER `is_hot`; + +-- 5. 添加推荐标记 +ALTER TABLE `product` ADD COLUMN `is_recommend` TINYINT NOT NULL DEFAULT 0 COMMENT '是否推荐 1=是 0=否' AFTER `is_new`; + +-- 6. 添加索引优化查询性能 +ALTER TABLE `product` ADD INDEX `idx_sales` (`sales`); +ALTER TABLE `product` ADD INDEX `idx_is_hot` (`is_hot`); +ALTER TABLE `product` ADD INDEX `idx_is_new` (`is_new`); +ALTER TABLE `product` ADD INDEX `idx_is_recommend` (`is_recommend`); + +-- 7. 更新现有商品的销量数据(模拟数据,实际应从订单统计) +UPDATE `product` SET `sales` = FLOOR(RAND() * 1000) WHERE `sales` = 0; + +-- 8. 标记部分商品为热销(销量前 10) +UPDATE `product` SET `is_hot` = 1 WHERE `id` IN ( + SELECT `id` FROM ( + SELECT `id` FROM `product` WHERE `deleted` = 0 ORDER BY `sales` DESC LIMIT 10 + ) AS tmp +); + +-- 9. 标记部分商品为新品(最新创建的 10 个) +UPDATE `product` SET `is_new` = 1 WHERE `id` IN ( + SELECT `id` FROM ( + SELECT `id` FROM `product` WHERE `deleted` = 0 ORDER BY `created_at` DESC LIMIT 10 + ) AS tmp +); + +-- 10. 标记部分商品为推荐(随机选择 10 个) +UPDATE `product` SET `is_recommend` = 1 WHERE `id` IN ( + SELECT `id` FROM ( + SELECT `id` FROM `product` WHERE `deleted` = 0 ORDER BY RAND() LIMIT 10 + ) AS tmp +); + +-- 11. 设置部分商品的原价(比现价高 10%-30%) +UPDATE `product` SET `original_price` = `price` * (1 + RAND() * 0.2 + 0.1) WHERE `original_price` IS NULL; + +SELECT 'Migration completed successfully!' AS result; diff --git a/backend/sql/schema.sql b/backend/sql/schema.sql index 714140cf37ed3167af46f88e6fb4eaf8a9b82cde..170e7c3ba95b649a40956d57631887347ee526f1 100644 --- a/backend/sql/schema.sql +++ b/backend/sql/schema.sql @@ -1,5 +1,5 @@ -- ===================================================== --- 电子商城 schema(8 业务表 + 2 辅助表 + 后台日志表) +-- 电子商城 schema(8 业务表 + 2 辅助表 + 后台日志表 + 收藏/浏览历史) -- 由 A 胡晨辉 维护;B/C/D 各自补字段时先改本文档再改 SQL -- 命名规范:表/字段名小写下划线;主键统一 id BIGINT auto_increment -- ===================================================== @@ -9,6 +9,9 @@ USE `eshop`; SET FOREIGN_KEY_CHECKS = 0; +DROP TABLE IF EXISTS `browse_history`; +DROP TABLE IF EXISTS `product_bundle`; +DROP TABLE IF EXISTS `favorite`; DROP TABLE IF EXISTS `operation_log`; DROP TABLE IF EXISTS `stock_log`; DROP TABLE IF EXISTS `payment`; @@ -59,20 +62,23 @@ CREATE TABLE `address` ( CONSTRAINT `fk_addr_user` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; --- -------------------- 3) cart_item(D 余泽) -------------------- +-- -------------------- 3) cart_item(D 余泽,Phase2 加 bundle_id) -------------------- CREATE TABLE `cart_item` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `user_id` BIGINT NOT NULL, `product_id` BIGINT NOT NULL, + `bundle_id` BIGINT DEFAULT NULL COMMENT 'NULL=base 商品;非空=product_bundle.id', `quantity` INT NOT NULL DEFAULT 1, `selected` TINYINT NOT NULL DEFAULT 1, `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), - UNIQUE KEY `uk_user_product` (`user_id`, `product_id`), + UNIQUE KEY `uk_user_product_bundle` (`user_id`, `product_id`, `bundle_id`), KEY `idx_user` (`user_id`), + KEY `idx_bundle` (`bundle_id`), CONSTRAINT `fk_cart_user` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE, - CONSTRAINT `fk_cart_product` FOREIGN KEY (`product_id`) REFERENCES `product` (`id`) ON DELETE CASCADE + CONSTRAINT `fk_cart_product` FOREIGN KEY (`product_id`) REFERENCES `product` (`id`) ON DELETE CASCADE, + CONSTRAINT `fk_cart_bundle` FOREIGN KEY (`bundle_id`) REFERENCES `product_bundle` (`id`) ON DELETE SET NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- -------------------- 4) category(B 江禹辰) -------------------- @@ -128,20 +134,24 @@ CREATE TABLE `order` ( KEY `idx_status_created` (`status`, `created_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; --- -------------------- 7) order_item(C 陈杨文,含价格/名称快照) -------------------- +-- -------------------- 7) order_item(C 陈杨文,含价格/名称快照,Phase2 加 bundle/spec) -------------------- CREATE TABLE `order_item` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `order_id` BIGINT NOT NULL, `product_id` BIGINT NOT NULL, + `bundle_id` BIGINT DEFAULT NULL COMMENT 'NULL=base;非空=product_bundle.id 快照', `product_name` VARCHAR(255) NOT NULL COMMENT '冗余快照', + `spec_name` VARCHAR(64) DEFAULT NULL COMMENT '冗余快照,如 "5盒装"', `product_image` VARCHAR(255) DEFAULT NULL COMMENT '冗余快照', - `price` DECIMAL(10,2) NOT NULL COMMENT '冗余快照', + `price` DECIMAL(10,2) NOT NULL COMMENT '冗余快照(base=product.price;bundle=bundle.price * bundle.quantity)', `quantity` INT NOT NULL, `subtotal` DECIMAL(10,2) NOT NULL, `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_order` (`order_id`), - CONSTRAINT `fk_oi_order` FOREIGN KEY (`order_id`) REFERENCES `order` (`id`) ON DELETE CASCADE + KEY `idx_oi_bundle` (`bundle_id`), + CONSTRAINT `fk_oi_order` FOREIGN KEY (`order_id`) REFERENCES `order` (`id`) ON DELETE CASCADE, + CONSTRAINT `fk_oi_bundle` FOREIGN KEY (`bundle_id`) REFERENCES `product_bundle` (`id`) ON DELETE SET NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- -------------------- 8) payment(C 陈杨文) -------------------- @@ -160,15 +170,19 @@ CREATE TABLE `payment` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- -------------------- 9) stock_log(B 江禹辰,幂等键表) -------------------- +-- Phase2 增字段:`bundle_id`(nullable BIGINT)用于把 bundle 维度的扣减/回补从 product 维度拆开记录; +-- 不加 FK(ON DELETE SET NULL cascade from product_bundle 会把审计 trail 弄丢,保留为信息列)。 CREATE TABLE `stock_log` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `biz_no` VARCHAR(64) NOT NULL COMMENT '幂等键:orderNo / cancel:{orderNo}:{itemId} 等', `product_id` BIGINT NOT NULL, + `bundle_id` BIGINT DEFAULT NULL COMMENT 'NULL=base;非空=product_bundle.id(Phase2 新增)', `delta` INT NOT NULL COMMENT '正=回补 负=扣减', `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_biz_no` (`biz_no`), - KEY `idx_product` (`product_id`) + KEY `idx_product` (`product_id`), + KEY `idx_stock_log_bundle` (`bundle_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- -------------------- 10) operation_log(A 胡晨辉) -------------------- @@ -188,4 +202,50 @@ CREATE TABLE `operation_log` ( KEY `idx_created` (`created_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +-- -------------------- 11) favorite(用户收藏) -------------------- +CREATE TABLE `favorite` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `user_id` BIGINT NOT NULL, + `product_id` BIGINT NOT NULL, + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_user_product` (`user_id`, `product_id`), + KEY `idx_user` (`user_id`), + CONSTRAINT `fk_fav_user` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE, + CONSTRAINT `fk_fav_product` FOREIGN KEY (`product_id`) REFERENCES `product` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- -------------------- 12) browse_history(浏览历史) -------------------- +CREATE TABLE `browse_history` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `user_id` BIGINT NOT NULL, + `product_id` BIGINT NOT NULL, + `browsed_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `idx_user_time` (`user_id`, `browsed_at`), + CONSTRAINT `fk_bh_user` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE, + CONSTRAINT `fk_bh_product` FOREIGN KEY (`product_id`) REFERENCES `product` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- -------------------- 13) product_bundle(B 江禹辰,商品打包/多规格) -------------------- +CREATE TABLE `product_bundle` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `product_id` BIGINT NOT NULL, + `spec_name` VARCHAR(64) NOT NULL COMMENT '如 "5盒装"', + `quantity` INT NOT NULL COMMENT '每套件数', + `price` DECIMAL(10,2) NOT NULL COMMENT '每件单价', + `stock` INT NOT NULL DEFAULT 0, + `image` VARCHAR(255) DEFAULT NULL COMMENT '为空回退 product.image', + `sort` INT NOT NULL DEFAULT 0, + `on_sale` TINYINT NOT NULL DEFAULT 1 COMMENT '1=在售 0=下架', + `deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '软删除', + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_product_qty` (`product_id`, `quantity`), + KEY `idx_product` (`product_id`, `deleted`, `sort`), + CONSTRAINT `fk_bundle_product` + FOREIGN KEY (`product_id`) REFERENCES `product` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + SET FOREIGN_KEY_CHECKS = 1; diff --git a/backend/sql/update-product-images.sql b/backend/sql/update-product-images.sql new file mode 100644 index 0000000000000000000000000000000000000000..a322ad33187314258ed0681a361a8ac203661faa --- /dev/null +++ b/backend/sql/update-product-images.sql @@ -0,0 +1,55 @@ +-- 把现有 product 表的 image 字段统一替换为 Unsplash CDN 真实图片 +-- 并将分类重命名为淘宝风格(服装时尚 / 美妆个护 / 家居生活 / 数码家电) +-- 注:id 9 / 14 / 19 的 Unsplash 图片已从 CDN 下架(稳定 404), +-- 改用 picsum.photos 固定 seed 占位图作为 fallback。其余 27 张 Unsplash 图经校验均为 200。 +-- 执行:mysql -u root -p eshop < backend/sql/update-product-images.sql + +-- 1. 重命名分类(id 1/2/3 改为淘宝风格;id 4 不存在则插入) +UPDATE `category` SET `name` = '服装时尚' WHERE `id` = 1; +UPDATE `category` SET `name` = '美妆个护' WHERE `id` = 2; +UPDATE `category` SET `name` = '家居生活' WHERE `id` = 3; +INSERT IGNORE INTO `category` (`id`, `name`, `parent_id`, `sort`) VALUES (4, '数码家电', 0, 4); + +-- 2. 重新分配商品到对应分类(淘宝风格) +UPDATE `product` SET `category_id` = 1 WHERE `id` BETWEEN 1 AND 8; +UPDATE `product` SET `category_id` = 2 WHERE `id` BETWEEN 9 AND 16; +UPDATE `product` SET `category_id` = 3 WHERE `id` BETWEEN 17 AND 24; +UPDATE `product` SET `category_id` = 4 WHERE `id` BETWEEN 25 AND 30; + +-- 3. 服装时尚 +UPDATE `product` SET `name` = '馨帮帮 蝴蝶结系带高领无袖格纹背心', `description` = '小立领薄款透视吊带女夏 2026 新款', `price` = 108.00, `image` = 'https://images.unsplash.com/photo-1591047139829-d91aecb6caea?w=400&h=400&fit=crop' WHERE `id` = 1; +UPDATE `product` SET `name` = '辣妹日记CL「猎手」油光银色裙', `description` = '超短油亮包臀裙 秘书制服演出服', `price` = 61.00, `image` = 'https://images.unsplash.com/photo-1539109136881-3be0616acf4b?w=400&h=400&fit=crop' WHERE `id` = 2; +UPDATE `product` SET `name` = '小朵NANA 老钱风套装 高级感显瘦', `description` = '轻奢通勤西装上衣阔腿裤三件套', `price` = 249.00, `image` = 'https://images.unsplash.com/photo-1542295669297-4d352b042bca?w=400&h=400&fit=crop' WHERE `id` = 3; +UPDATE `product` SET `name` = '一眼心动!DREW 笑脸T恤 情侣同款', `description` = '潮牌 ins 风圆领短袖纯棉宽松上衣', `price` = 109.00, `image` = 'https://images.unsplash.com/photo-1521572163474-6864f9cf17ab?w=400&h=400&fit=crop' WHERE `id` = 4; +UPDATE `product` SET `name` = '【厚底增高】六也休闲运动风单鞋', `description` = '2026 春新款百搭显瘦小白鞋女鞋', `price` = 348.00, `image` = 'https://images.unsplash.com/photo-1542291026-7eec264c27ff?w=400&h=400&fit=crop' WHERE `id` = 5; +UPDATE `product` SET `name` = '美式高街 PU 皮外套男款秋冬季', `description` = '飞行夹克棒球服痞帅外套男', `price` = 269.00, `image` = 'https://images.unsplash.com/photo-1551028719-00167b16eac5?w=400&h=400&fit=crop' WHERE `id` = 6; +UPDATE `product` SET `name` = '运动瑜伽裤女高腰提臀紧身裤', `description` = '速干跑步健身裤 2026 新款', `price` = 89.00, `image` = 'https://images.unsplash.com/photo-1506629082955-511b1aa562c8?w=400&h=400&fit=crop' WHERE `id` = 7; +UPDATE `product` SET `name` = '夏季薄款棉麻衬衫女宽松上衣', `description` = '文艺范亚麻短袖衬衫 2026 新款', `price` = 128.00, `image` = 'https://images.unsplash.com/photo-1485518882345-15568b007407?w=400&h=400&fit=crop' WHERE `id` = 8; + +-- 4. 美妆个护 +UPDATE `product` SET `name` = '资生堂 安耐晒防晒霜 60g', `description` = 'SPF50+ 防水防汗 清爽不油腻', `price` = 138.00, `image` = 'https://picsum.photos/seed/skincare09/400/400' WHERE `id` = 9; +UPDATE `product` SET `name` = '怡丽丝尔 金色抗皱防晒霜', `description` = 'SPF50+ PA++++ 50g 清爽不油腻', `price` = 198.00, `image` = 'https://images.unsplash.com/photo-1556228720-195a672e8a03?w=400&h=400&fit=crop' WHERE `id` = 10; +UPDATE `product` SET `name` = '安热沙 金瓶防晒霜 60ml', `description` = '资生堂防水防汗 SPF50+ 户外军训', `price` = 218.00, `image` = 'https://images.unsplash.com/photo-1556228852-80b6e5eeff06?w=400&h=400&fit=crop' WHERE `id` = 11; +UPDATE `product` SET `name` = '海蓝之谜 修护精华露 30ml', `description` = '浓缩修护精萃液 抗皱保湿官方正品', `price` = 2680.00, `image` = 'https://images.unsplash.com/photo-1571781926291-c477ebfd024b?w=400&h=400&fit=crop' WHERE `id` = 12; +UPDATE `product` SET `name` = '欧莱雅 玻尿酸洗发水 700ml', `description` = '保湿补水改善毛躁 男女士官方', `price` = 79.00, `image` = 'https://images.unsplash.com/photo-1556228852-80b6e5eeff06?w=400&h=400&fit=crop' WHERE `id` = 13; +UPDATE `product` SET `name` = 'Olay 烟酰胺亮肤精华 30ml', `description` = '美白淡斑 提亮肤色 改善暗沉', `price` = 199.00, `image` = 'https://picsum.photos/seed/skincare14/400/400' WHERE `id` = 14; +UPDATE `product` SET `name` = '可复美 重组胶原蛋白面膜', `description` = '5 片装 补水修护敏感 官方正品', `price` = 138.00, `image` = 'https://images.unsplash.com/photo-1556228578-8c89e6adf883?w=400&h=400&fit=crop' WHERE `id` = 15; +UPDATE `product` SET `name` = '迪奥 真我香水 50ml', `description` = 'EDP 经典花香调 女士持久官方', `price` = 880.00, `image` = 'https://images.unsplash.com/photo-1592945403244-b3fbafd7f539?w=400&h=400&fit=crop' WHERE `id` = 16; + +-- 5. 家居生活 +UPDATE `product` SET `name` = '德国 BRITA 滤水壶 3.5L', `description` = '厨房净水器家用直饮滤芯正品', `price` = 258.00, `image` = 'https://images.unsplash.com/photo-1556909114-f6e7ad7d3136?w=400&h=400&fit=crop' WHERE `id` = 17; +UPDATE `product` SET `name` = '新秀丽 行李箱 万向轮静音', `description` = '20 寸登机箱 拉杆箱旅行箱密码', `price` = 1690.00, `image` = 'https://images.unsplash.com/photo-1553062407-98eeb64c6a62?w=400&h=400&fit=crop' WHERE `id` = 18; +UPDATE `product` SET `name` = '哥伦比亚 户外背包 30L', `description` = '防泼水大容量 徒步登山旅行', `price` = 458.00, `image` = 'https://picsum.photos/seed/backpack19/400/400' WHERE `id` = 19; +UPDATE `product` SET `name` = 'MUJI 无印良品 四件套床品', `description` = '纯棉水洗棉被套床单 1.8m 床', `price` = 388.00, `image` = 'https://images.unsplash.com/photo-1505693416388-ac5ce068fe85?w=400&h=400&fit=crop' WHERE `id` = 20; +UPDATE `product` SET `name` = '戴森 V12 无线吸尘器', `description` = '家用小型手持除螨吸尘器大吸力', `price` = 3690.00, `image` = 'https://images.unsplash.com/photo-1581235720704-06d3acfcb36f?w=400&h=400&fit=crop' WHERE `id` = 21; +UPDATE `product` SET `name` = '华为 Sound Joy 智能音箱', `description` = '帝瓦雷调音 蓝牙便携户外音响', `price` = 999.00, `image` = 'https://images.unsplash.com/photo-1545454675-3531b543be5d?w=400&h=400&fit=crop' WHERE `id` = 22; +UPDATE `product` SET `name` = '膳魔师 保温杯 500ml', `description` = '日本进口直饮真空不锈钢水杯', `price` = 218.00, `image` = 'https://images.unsplash.com/photo-1602143407151-7111542de6e8?w=400&h=400&fit=crop' WHERE `id` = 23; +UPDATE `product` SET `name` = 'Vitamix 破壁机 家用加热', `description` = '全自动多功能豆浆料理机官方', `price` = 4980.00, `image` = 'https://images.unsplash.com/photo-1570222094114-d054a817e56b?w=400&h=400&fit=crop' WHERE `id` = 24; + +-- 6. 数码家电 +UPDATE `product` SET `name` = 'iPhone 15 Pro 256GB', `description` = 'Apple 旗舰手机 钛金属 A17 Pro', `price` = 7999.00, `image` = 'https://images.unsplash.com/photo-1592890288564-76628a30a657?w=400&h=400&fit=crop' WHERE `id` = 25; +UPDATE `product` SET `name` = '华为 Mate 60 Pro 512GB', `description` = '麒麟 9000S 旗舰 5G 现货', `price` = 6999.00, `image` = 'https://images.unsplash.com/photo-1598327105666-5b89351aff97?w=400&h=400&fit=crop' WHERE `id` = 26; +UPDATE `product` SET `name` = 'MacBook Pro 14 M3', `description` = '16+512 银色 深空灰', `price` = 14999.00, `image` = 'https://images.unsplash.com/photo-1496181133206-80ce9b88a853?w=400&h=400&fit=crop' WHERE `id` = 27; +UPDATE `product` SET `name` = '索尼 WH-1000XM5 头戴耳机', `description` = '无线蓝牙主动降噪 黑色', `price` = 2399.00, `image` = 'https://images.unsplash.com/photo-1583394838336-acd977736f90?w=400&h=400&fit=crop' WHERE `id` = 28; +UPDATE `product` SET `name` = '小米电视 65 寸 4K', `description` = '全面屏 智能语音液晶平板电视', `price` = 2499.00, `image` = 'https://images.unsplash.com/photo-1560169897-fc0cdbdfa4d5?w=400&h=400&fit=crop' WHERE `id` = 29; +UPDATE `product` SET `name` = '戴森吹风机 HD15', `description` = '新一代 智能恒温 速干', `price` = 2999.00, `image` = 'https://images.unsplash.com/photo-1522338140262-f46f5913618a?w=400&h=400&fit=crop' WHERE `id` = 30; diff --git a/backend/src/app.js b/backend/src/app.js index 33ab72bda73726b4de59b9138b602979ae80ebfe..2871044390a78c7343c1b44c225162813edace3a 100644 --- a/backend/src/app.js +++ b/backend/src/app.js @@ -48,6 +48,9 @@ async function bootstrap() { await testConnection(); await sequelize.sync({ alter: false }); // 表结构用 schema.sql 管理,运行时不动表结构 await testRedis(); + // 启动订单超时监听(C03):Redis keyspace 通知 + 兜底定时扫描 + const { startTimeoutWatcher } = require('./modules/order/services/timeoutService'); + await startTimeoutWatcher(); app.listen(PORT, () => { // eslint-disable-next-line no-console console.log(`✅ server running on http://localhost:${PORT}`); @@ -60,12 +63,20 @@ async function bootstrap() { } } -bootstrap(); +// 测试环境跳过 bootstrap(避免 require 时尝试连接 MySQL/Redis) +// jest 默认设 NODE_ENV=test;supertest 直接 require app + 注入 mock 请求即可 +if (process.env.NODE_ENV !== 'test') { + bootstrap(); +} // 优雅关闭 process.on('SIGTERM', async () => { // eslint-disable-next-line no-console console.log('SIGTERM received, shutting down...'); + try { + const { stopTimeoutWatcher } = require('./modules/order/services/timeoutService'); + stopTimeoutWatcher(); + } catch (_) {} await sequelize.close(); await redis.quit(); process.exit(0); diff --git a/backend/src/common/auth.js b/backend/src/common/auth.js index 38eef3cdeca6979c036a567b43218ba54c4e0b83..a38b255cbca3d4b2945cf6c1c5a572e62c1c1233 100644 --- a/backend/src/common/auth.js +++ b/backend/src/common/auth.js @@ -27,15 +27,17 @@ function verifyToken(token) { } } -/** Express 中间件:解析 Authorization 头,注入 req.user */ +/** Express 中间件:解析 Authorization 头,注入 req.userId */ function authMiddleware(req, _res, next) { const header = req.headers.authorization || ''; const [scheme, token] = header.split(' '); if (scheme !== 'Bearer' || !token) { - return next(new UnauthorizedError('未提供有效 Token')); + return next(new UnauthorizedError('请先登录')); } try { - req.user = verifyToken(token); + const decoded = verifyToken(token); + req.userId = decoded.userId; + req.user = decoded; return next(); } catch (e) { return next(e); diff --git a/backend/src/config/redis.js b/backend/src/config/redis.js index 538c9bd6314b2f98f5c19ea1022df47b5902d9bd..96efd2233b71e4aa5e9a5e576b43e6ee04e81f47 100644 --- a/backend/src/config/redis.js +++ b/backend/src/config/redis.js @@ -1,23 +1,45 @@ /** * Redis 客户端 - * 用于登录态可选黑名单 / 收藏计数 / 倒排索引 / 缓存 + * 用于登录态可选黑名单 / 收藏计数 / 倒排索引 / 缓存 / 超时 key 监听 */ 'use strict'; const { createClient } = require('redis'); -const redis = createClient({ - socket: { host: process.env.REDIS_HOST || 'localhost', port: parseInt(process.env.REDIS_PORT || '6379', 10) }, - password: process.env.REDIS_PASSWORD || undefined, -}); +function buildOptions() { + return { + socket: { host: process.env.REDIS_HOST || 'localhost', port: parseInt(process.env.REDIS_PORT || '6379', 10) }, + password: process.env.REDIS_PASSWORD || undefined, + }; +} + +const redis = createClient(buildOptions()); redis.on('error', (err) => console.error('[redis] error:', err.message)); async function testRedis() { - if (!redis.isOpen) await redis.connect(); - await redis.ping(); - // eslint-disable-next-line no-console - console.log('✅ Redis connected'); + try { + if (!redis.isOpen) { + const connectWithTimeout = Promise.race([ + redis.connect(), + new Promise((_, reject) => setTimeout(() => reject(new Error('Redis connect timeout')), 3000)) + ]); + await connectWithTimeout; + } + await redis.ping(); + // eslint-disable-next-line no-console + console.log('✅ Redis connected'); + } catch (err) { + // eslint-disable-next-line no-console + console.warn('⚠️ Redis unavailable, continuing without it:', err.message); + } +} + +/** 创建一个独立的 subscriber 客户端(不能与主客户端共用连接) */ +function createSubscriber() { + const sub = createClient(buildOptions()); + sub.on('error', (err) => console.error('[redis-sub] error:', err.message)); + return sub; } -module.exports = { redis, testRedis }; +module.exports = { redis, testRedis, createSubscriber }; diff --git a/backend/src/models/index.js b/backend/src/models/index.js index eaa8998c620ad3610e986e6aac87449d2a9e9c49..1798b492fde3072d4fd9bde18017db7f0dfd41e4 100644 --- a/backend/src/models/index.js +++ b/backend/src/models/index.js @@ -10,9 +10,12 @@ const { sequelize } = require('../config/db'); const User = require('../modules/user/models/User'); const Address = require('../modules/user/models/Address'); const CartItem = require('../modules/user/models/CartItem'); +const Favorite = require('../modules/user/models/Favorite'); +const BrowseHistory = require('../modules/user/models/BrowseHistory'); const Category = require('../modules/product/models/Category'); const Product = require('../modules/product/models/Product'); +const ProductBundle = require('../modules/product/models/ProductBundle'); const Order = require('../modules/order/models/Order'); const OrderItem = require('../modules/order/models/OrderItem'); @@ -34,6 +37,26 @@ CartItem.belongsTo(User, { foreignKey: 'userId', as: 'user' }); Product.hasMany(CartItem, { foreignKey: 'productId', as: 'cartItems' }); CartItem.belongsTo(Product, { foreignKey: 'productId', as: 'product' }); +// 用户 ↔ 收藏(一对多) +User.hasMany(Favorite, { foreignKey: 'userId', as: 'favorites' }); +Favorite.belongsTo(User, { foreignKey: 'userId', as: 'user' }); + +// 商品 ↔ 收藏(一对多) +Product.hasMany(Favorite, { foreignKey: 'productId', as: 'favorites' }); +Favorite.belongsTo(Product, { foreignKey: 'productId', as: 'product' }); + +// 用户 ↔ 浏览历史(一对多) +User.hasMany(BrowseHistory, { foreignKey: 'userId', as: 'browseHistories' }); +BrowseHistory.belongsTo(User, { foreignKey: 'userId', as: 'user' }); + +// 商品 ↔ 浏览历史(一对多) +Product.hasMany(BrowseHistory, { foreignKey: 'productId', as: 'browseHistories' }); +BrowseHistory.belongsTo(Product, { foreignKey: 'productId', as: 'product' }); + +// 商品 ↔ 规格(product_bundle,一对多) +Product.hasMany(ProductBundle, { foreignKey: 'productId', as: 'bundles' }); +ProductBundle.belongsTo(Product, { foreignKey: 'productId', as: 'product' }); + // 商品 ↔ 分类(多对一) Product.belongsTo(Category, { foreignKey: 'categoryId', as: 'category' }); Category.hasMany(Product, { foreignKey: 'categoryId', as: 'products' }); diff --git a/backend/src/modules/admin/log.routes.js b/backend/src/modules/admin/log.routes.js index 18929cfe185e862541a49897beebe11b7d12df4d..829c887e47c6625ff183eec7dc5973acbfabe26f 100644 --- a/backend/src/modules/admin/log.routes.js +++ b/backend/src/modules/admin/log.routes.js @@ -1,3 +1,10 @@ +/** + * 后台操作日志路由(M09) + * - GET /api/admin/operation-logs 列表 + 过滤(按 id 升序) + * - GET /api/admin/operation-logs/export 导出 CSV(与列表使用同样的过滤条件) + * + * 过滤项:userId(精确)、path(LIKE 模糊匹配)、status(HTTP 状态码精确匹配) + */ 'use strict'; const router = require('express').Router(); @@ -8,17 +15,87 @@ const { sequelize } = require('../../config/db'); router.use(authMiddleware, requireRole('admin')); -// /api/admin/operation-logs +// 把过滤条件转成 { where, params },复用给列表和 CSV 导出 +function buildFilters(query) { + const where = []; + const params = []; + if (query.userId !== undefined && query.userId !== '' && query.userId !== null) { + const uid = parseInt(query.userId, 10); + if (!Number.isNaN(uid)) { + where.push('user_id = ?'); + params.push(uid); + } + } + if (query.path) { + where.push('path LIKE ?'); + params.push(`%${query.path}%`); + } + if (query.status !== undefined && query.status !== '' && query.status !== null) { + const st = parseInt(query.status, 10); + if (!Number.isNaN(st)) { + where.push('status = ?'); + params.push(st); + } + } + return { + whereSql: where.length ? `WHERE ${where.join(' AND ')}` : '', + params, + }; +} + +// GET /api/admin/operation-logs router.get('/operation-logs', async (req, res, next) => { try { const { page, pageSize, offset, limit } = parsePagination(req.query, { defaultPageSize: 30 }); - const [countRow] = await sequelize.query('SELECT COUNT(*) AS c FROM operation_log', { type: sequelize.QueryTypes.SELECT }); - const [rows] = await sequelize.query( - 'SELECT * FROM operation_log ORDER BY id DESC LIMIT ? OFFSET ?', - { replacements: [limit, offset] } + const { whereSql, params } = buildFilters(req.query); + + const [countRow] = await sequelize.query( + `SELECT COUNT(*) AS c FROM operation_log ${whereSql}`, + { replacements: params, type: sequelize.QueryTypes.SELECT } + ); + const rows = await sequelize.query( + `SELECT * FROM operation_log ${whereSql} ORDER BY id ASC LIMIT ? OFFSET ?`, + { replacements: [...params, limit, offset], type: sequelize.QueryTypes.SELECT } ); - res.json(paginated(rows, countRow.c, page, pageSize)); + res.json(paginated(rows, Number(countRow.c), page, pageSize)); } catch (e) { next(e); } }); -module.exports = router; +// GET /api/admin/operation-logs/export?userId=&path=&status= +router.get('/operation-logs/export', async (req, res, next) => { + try { + const { whereSql, params } = buildFilters(req.query); + const rows = await sequelize.query( + `SELECT id, user_id, role, method, path, ip, status, cost_ms, params, created_at ` + + `FROM operation_log ${whereSql} ORDER BY id ASC LIMIT 5000`, + { replacements: params, type: sequelize.QueryTypes.SELECT } + ); + + // 手工拼 CSV(避免再引入依赖;注意转义) + const head = ['ID', '操作人ID', '角色', '方法', '路径', 'IP', '状态码', '耗时ms', '参数', '时间']; + const escape = (v) => { + if (v === null || v === undefined) return ''; + const s = String(v).replace(/\r?\n/g, ' '); + return /[",]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s; + }; + const lines = [head.join(',')]; + for (const r of rows) { + lines.push([ + r.id, r.user_id, r.role, r.method, r.path, r.ip, r.status, r.cost_ms, + r.params, r.created_at, + ].map(escape).join(',')); + } + // UTF-8 BOM 让 Excel 不乱码 + const csv = '' + lines.join('\r\n'); + + const fname = `operation-logs-${new Date().toISOString().slice(0, 10)}.csv`; + res.setHeader('Content-Type', 'text/csv; charset=utf-8'); + res.setHeader('Content-Disposition', `attachment; filename="${fname}"`); + res.send(csv); + } catch (e) { next(e); } +}); + +// 显式声明一下 success 的引用,避免某些 Lint 报 unused(保留供后续扩展) +void success; + +module.exports = router; \ No newline at end of file diff --git a/backend/src/modules/admin/stats.routes.js b/backend/src/modules/admin/stats.routes.js index be4b86107394a7125bef86785bfcaddd9f261445..6cdbc59510747d61aba29fa2ab11b9faf1c29dc9 100644 --- a/backend/src/modules/admin/stats.routes.js +++ b/backend/src/modules/admin/stats.routes.js @@ -1,6 +1,16 @@ /** * 后台仪表盘统计接口(A 负责,C09 挑战项) * 一次性返回 Dashboard 需要的所有指标 + * + * 新增字段(M10 概览页扩展): + * - weeklySales 本周销售额(近 7 天已支付订单的 total 总和) + * - lastWeekSales 上周销售额(用于算环比) + * - weeklyDeltaPct 本周较上周环比百分比(上周为 0 时返回 null) + * - repurchaseRate 复购率:有 ≥2 次完成订单的用户数 / 全部有完成订单的用户数 + * - salesTrend 销售趋势(最近 7 天,每天 {date, orderCount, sales}) + * - categoryShare 分类销量占比(按 order_item.product.category_id 聚合销售额) + * - topProducts Top10 热销商品(按 quantity 销售数量降序) + * - orderStatusRows 订单状态分布(行:label/count/desc) */ 'use strict'; @@ -11,44 +21,352 @@ const { sequelize } = require('../../config/db'); router.use(authMiddleware, requireRole('admin')); +// 订单状态字典(与 schema.sql `order`.status 保持一致) +const STATUS_LABEL = { + 0: { label: '待支付', desc: '用户已下单,未付款' }, + 1: { label: '已支付', desc: '已付款,等待发货' }, + 2: { label: '已发货', desc: '商品已发出,待用户确认' }, + 3: { label: '已完成', desc: '用户已确认收货,交易结束' }, + 4: { label: '已取消', desc: '用户主动取消订单' }, + 5: { label: '已退款', desc: '已退款订单' }, +}; + +// 把 [a, b] 区间聚合为 SQL BETWEEN +function dateRangeSQL(startExpr, endExpr) { + return `${startExpr} BETWEEN ? AND ?`; +} + +// 计算 ISO 周数(与 MySQL YEARWEEK(date, 3) 一致:周一开始,含 1 月 4 日的那周为第 1 周) +function getISOWeek(d) { + const target = new Date(d.getFullYear(), d.getMonth(), d.getDate()); + // 调到本周的周四(ISO 周算法核心:周四在哪,年就在哪) + const dayNum = (target.getDay() + 6) % 7; // 周一=0, 周日=6 + target.setDate(target.getDate() + 3 - dayNum); + const yearStart = new Date(target.getFullYear(), 0, 1); + const weekNo = Math.ceil(((target.getTime() - yearStart.getTime()) / 86400000 + 1) / 7); + return weekNo; +} + // GET /api/admin/stats -router.get('/stats', async (_req, res, next) => { +router.get('/stats', async (req, res, next) => { try { - // 并行查 5 个聚合 - const users = await sequelize.query('SELECT COUNT(*) AS c FROM user', { type: sequelize.QueryTypes.SELECT }); - const products = await sequelize.query('SELECT COUNT(*) AS c FROM product WHERE deleted = 0', { type: sequelize.QueryTypes.SELECT }); - const orders = await sequelize.query('SELECT COUNT(*) AS c FROM `order`', { type: sequelize.QueryTypes.SELECT }); - const todayOrders = await sequelize.query( + // 周历切换:0=最近 7 天(默认),-1=上一周,1=下一周,依此类推 + // 仅影响 salesTrend;KPI / 分类 / Top10 始终基于"实时"统计口径 + const weekOffsetRaw = parseInt(req.query.weekOffset, 10); + const weekOffset = Number.isFinite(weekOffsetRaw) ? weekOffsetRaw : 0; + // 区间起点:D + (6 - 7*offset) 天前;区间终点(不含):D + (-1 - 7*offset) 天前 + const trendFrontDay = 6 - 7 * weekOffset; + const trendBackDay = -1 - 7 * weekOffset; + + // 时间范围切换:'1w' 本周(7天) / '1m' 近1月 / '6m' 近半年 / '1y' 近1年 / 'all' 全部 + // 影响 categoryShare + topProducts;用"天"为单位,所有选项统一 SQL 模板 + const RANGE_TO_DAYS = { '1w': 7, '1m': 30, '6m': 182, '1y': 365 }; + const rangeKey = String(req.query.range || 'all'); + const rangeDays = RANGE_TO_DAYS[rangeKey] || null; + + // 销售指标切换(本周销售额 + 复购率):'1w' 本周(7天) / '1m' 近1月 / '6m' 近半年 / '1y' 近 1 年 + // 用于 weeklySales/lastWeekSales/weeklyDeltaPct/repurchaseRate + const METRIC_TO_DAYS = { '1w': 7, '1m': 30, '3m': 90, '6m': 182, '1y': 365 }; + const metricRangeKey = String(req.query.metricRange || '1w'); + const metricDays = METRIC_TO_DAYS[metricRangeKey] || null; + // metricDays 缺省表示 "all",不附加时间 WHERE + const metricLowerWhere = metricDays ? ' AND created_at >= DATE_SUB(NOW(), INTERVAL ? DAY) ' : ''; + + // ===== KPI 顶部 6 张卡的区间切换:'1w'/'1m'/'6m'/'1y' = 区间内新增;'all' = 全部(向后兼容) ===== + const KPI_RANGE_TO_DAYS = { '1w': 7, '1m': 30, '6m': 182, '1y': 365 }; + const kpiRangeKey = String(req.query.kpiRange || 'all'); + const kpiDays = KPI_RANGE_TO_DAYS[kpiRangeKey] || null; + const kpiLowerWhere = kpiDays ? ' AND created_at >= DATE_SUB(NOW(), INTERVAL ? DAY) ' : ''; + const kpiLowerWhereLead = kpiDays ? ' WHERE created_at >= DATE_SUB(NOW(), INTERVAL ? DAY) ' : ''; + const kpiRepl = kpiDays ? [kpiDays] : []; + + // ===== 基础 KPI(与历史保持兼容;kpiRange=all 时是累积快照,否则按区间) ===== + const users = await sequelize.query( + kpiDays + ? 'SELECT COUNT(*) AS c FROM user WHERE created_at >= DATE_SUB(NOW(), INTERVAL ? DAY)' + : 'SELECT COUNT(*) AS c FROM user', + { replacements: kpiRepl, type: sequelize.QueryTypes.SELECT } + ); + const products = await sequelize.query( + kpiDays + ? 'SELECT COUNT(*) AS c FROM product WHERE deleted = 0 AND created_at >= DATE_SUB(NOW(), INTERVAL ? DAY)' + : 'SELECT COUNT(*) AS c FROM product WHERE deleted = 0', + { replacements: kpiRepl, type: sequelize.QueryTypes.SELECT } + ); + const orders = await sequelize.query( + kpiDays + ? 'SELECT COUNT(*) AS c FROM `order` WHERE created_at >= DATE_SUB(NOW(), INTERVAL ? DAY)' + : 'SELECT COUNT(*) AS c FROM `order`', + { replacements: kpiRepl, type: sequelize.QueryTypes.SELECT } + ); + // 今日订单:仅 kpiRange=all 时按"今天"算,其他区间口径与订单数相同(按区间) + const todayOrdersRow = await sequelize.query( "SELECT COUNT(*) AS c FROM `order` WHERE DATE(created_at) = CURDATE()", { type: sequelize.QueryTypes.SELECT } ); - // 总营收:已支付订单的 total 总和(status=1,2,3 都算实际收款) + const todayOrderCount = kpiRangeKey === 'all' + ? Number(todayOrdersRow[0].c) + : Number(orders[0].c); const revenue = await sequelize.query( - "SELECT IFNULL(SUM(total), 0) AS s FROM `order` WHERE status IN (1, 2, 3)", - { type: sequelize.QueryTypes.SELECT } + "SELECT IFNULL(SUM(total), 0) AS s FROM `order` WHERE status IN (1, 2, 3)" + kpiLowerWhere, + { replacements: kpiRepl, type: sequelize.QueryTypes.SELECT } ); - // 按状态分布 + // 待支付订单(kpiRange 范围内新增) + const pendingOrdersRow = await sequelize.query( + "SELECT COUNT(*) AS c FROM `order` WHERE status = 0" + kpiLowerWhere, + { replacements: kpiRepl, type: sequelize.QueryTypes.SELECT } + ); + const pendingOrderCount = Number(pendingOrdersRow[0].c); + // 订单状态分布按 ?orderRange 区间统计;默认 'all' = 全部订单 + const ORDER_RANGE_TO_DAYS = { '1w': 7, '1m': 30, '6m': 182, '1y': 365 }; + const orderRangeKey = String(req.query.orderRange || 'all'); + const orderRangeDays = ORDER_RANGE_TO_DAYS[orderRangeKey] || null; + const orderRangeWhere = orderRangeDays + ? ' WHERE created_at >= DATE_SUB(NOW(), INTERVAL ? DAY) ' + : ''; const statusRows = await sequelize.query( - 'SELECT status, COUNT(*) AS c FROM `order` GROUP BY status', - { type: sequelize.QueryTypes.SELECT } + 'SELECT status, COUNT(*) AS c FROM `order`' + orderRangeWhere + ' GROUP BY status', + { replacements: orderRangeDays ? [orderRangeDays] : [], type: sequelize.QueryTypes.SELECT } ); - // 把订单状态转成 Map,方便前端按需取 const byStatus = { 0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 }; statusRows.forEach((r) => { byStatus[r.status] = Number(r.c); }); + // ===== 周/月/季/年销售额(本期 vs 上期,近 metricDays 天的滚动窗口) ===== + // 默认 1w:本周 vs 上周。本期 = [NOW - N, NOW];上期 = [NOW - 2N, NOW - N) + const [thisPeriod] = await sequelize.query( + "SELECT IFNULL(SUM(total), 0) AS s FROM `order` " + + "WHERE status IN (1,2,3) " + metricLowerWhere + + "AND created_at <= NOW()", + { replacements: metricDays ? [metricDays] : [], type: sequelize.QueryTypes.SELECT } + ); + // 当 metricDays=null(all)时,"上期"没法定义,"较上期"置 null + let weeklySales = Number(thisPeriod.s); + let lastWeekSales = 0; + let weeklyDeltaPct = null; + if (metricDays) { + const [lastPeriod] = await sequelize.query( + "SELECT IFNULL(SUM(total), 0) AS s FROM `order` " + + "WHERE status IN (1,2,3) " + + "AND created_at >= DATE_SUB(NOW(), INTERVAL ? DAY) " + + "AND created_at < DATE_SUB(NOW(), INTERVAL ? DAY)", + { replacements: [2 * metricDays, metricDays], type: sequelize.QueryTypes.SELECT } + ); + lastWeekSales = Number(lastPeriod.s); + if (lastWeekSales > 0) { + weeklyDeltaPct = Number((((weeklySales - lastWeekSales) / lastWeekSales) * 100).toFixed(2)); + } + } + + // ===== 销售趋势:interval 控制窗口与聚合粒度 ===== + // 1w / 1m:每天一个数据点 + // 6m / 1y:按周聚合(YEARWEEK, mode 3 = 周一开始) + // all :按月聚合 + const TREND_INTERVAL_TO_DAYS = { '1w': 7, '1m': 30, '6m': 182, '1y': 365 }; + const TREND_INTERVAL_ALIAS = { '1w': 'daily', '1m': 'daily', '6m': 'weekly', '1y': 'weekly', 'all': 'monthly' }; + const trendInterval = String(req.query.trendInterval || '1w'); + const trendIntervalDays = TREND_INTERVAL_TO_DAYS[trendInterval] || null; // null = all + // 当 interval != '1w' 时,weekOffset 失效(强制 0),保持口径一致 + const trendOffset = trendInterval === '1w' ? weekOffset : 0; + // 区间(以"天"为单位) + // 本期起点 = today + trendOffset * 7 - (trendIntervalDays - 1);终点 = today + trendOffset * 7 + 1 + // 对于 all:起点 = 1970-01-01(用 99999 兜底),终点 = today + 1 + const trendEffectiveStart = trendIntervalDays + ? trendIntervalDays - 1 + 7 * trendOffset + : -1; + const trendEffectiveEnd = -7 * trendOffset - 1; + const trendGroupExpr = + TREND_INTERVAL_ALIAS[trendInterval] === 'weekly' ? 'YEARWEEK(created_at, 3)' : + TREND_INTERVAL_ALIAS[trendInterval] === 'monthly' ? "DATE_FORMAT(created_at, '%Y-%m')" : + 'DATE(created_at)'; + // 'all' 时起点用 -99999 天前,相当于不限 + const trendStartDay = trendIntervalDays ? trendEffectiveStart : 99999; + const trendEndDay = trendIntervalDays ? trendEffectiveEnd : 0; + + const trendRows = await sequelize.query( + `SELECT ${trendGroupExpr} AS bucket, COUNT(*) AS c, ` + + `IFNULL(SUM(CASE WHEN status IN (1,2,3) THEN total ELSE 0 END), 0) AS s ` + + `FROM \`order\` ` + + `WHERE created_at >= DATE_SUB(CURDATE(), INTERVAL ? DAY) ` + + ` AND created_at < DATE_ADD(CURDATE(), INTERVAL 1 DAY) ` + + `GROUP BY bucket ` + + `ORDER BY bucket ASC`, + { replacements: [trendStartDay], type: sequelize.QueryTypes.SELECT } + ); + // 把 bucket 转成可比较的"键"(daily=YYYY-MM-DD, weekly=YYYY-WW, monthly=YYYY-MM) + const trendMap = new Map(); + trendRows.forEach((r) => { + const bucket = String(r.bucket); + trendMap.set(bucket, { c: Number(r.c), s: Number(r.s) }); + }); + + // 生成 salesTrend 数组 + const salesTrend = []; + if (trendInterval === '1w' || trendInterval === '1m' || trendIntervalDays === null) { + // 按日:1w → 7 个,1m → 30 个(如果 1m),all → 按月渲染 + const dayCount = trendInterval === '1w' ? 7 : trendInterval === '1m' ? 30 : null; + if (dayCount) { + const anchor = new Date(); + anchor.setHours(0, 0, 0, 0); + if (trendInterval === '1w') anchor.setDate(anchor.getDate() + 7 * trendOffset); + for (let i = dayCount - 1; i >= 0; i -= 1) { + const d = new Date(anchor.getFullYear(), anchor.getMonth(), anchor.getDate() - i); + const key = d.toISOString().slice(0, 10); + const row = trendMap.get(key); + salesTrend.push({ date: key, orderCount: row ? row.c : 0, sales: row ? row.s : 0 }); + } + } + } else if (trendInterval === '6m' || trendInterval === '1y') { + // 按周聚合:起点 = today - (N-1),按"自然周一对齐"展示 + const dayCount = trendIntervalDays; // 182 / 365 + const bucketStart = new Date(); bucketStart.setHours(0, 0, 0, 0); bucketStart.setDate(bucketStart.getDate() - (dayCount - 1)); + // 找到 bucketStart 所在周的周一 + const dow = (bucketStart.getDay() + 6) % 7; // 周一=0 ... 周日=6 + bucketStart.setDate(bucketStart.getDate() - dow); + const todayD = new Date(); todayD.setHours(0, 0, 0, 0); + // 计算跨多少周 + const msPerWeek = 7 * 86400000; + const weeks = Math.ceil((todayD.getTime() - bucketStart.getTime()) / msPerWeek) + 1; + for (let w = 0; w < weeks; w += 1) { + const startD = new Date(bucketStart); startD.setDate(bucketStart.getDate() + w * 7); + // YEARWEEK(date, 3) 返回 YYYYWW (e.g. 202627) + const y = startD.getFullYear(); + const ww = getISOWeek(startD); + const key = `${y}-W${String(ww).padStart(2, '0')}`; + const row = trendMap.get(`${y}${String(ww).padStart(2, '0')}`); + salesTrend.push({ date: key, orderCount: row ? row.c : 0, sales: row ? row.s : 0 }); + } + } else if (trendInterval === 'all') { + // 按月聚合:从最早订单月起,到当月 + const months = [...trendMap.keys()].sort(); + const earliest = months[0]; + const latest = months[months.length - 1]; + if (earliest && latest) { + const sy = parseInt(earliest.slice(0, 4), 10); + const sm = parseInt(earliest.slice(5, 7), 10); + const ly = parseInt(latest.slice(0, 4), 10); + const lm = parseInt(latest.slice(5, 7), 10); + for (let y = sy; y <= ly; y += 1) { + const mStart = (y === sy) ? sm : 1; + const mEnd = (y === ly) ? lm : 12; + for (let m = mStart; m <= mEnd; m += 1) { + const key = `${y}-${String(m).padStart(2, '0')}`; + const row = trendMap.get(key); + salesTrend.push({ date: key, orderCount: row ? row.c : 0, sales: row ? row.s : 0 }); + } + } + } + } + + // 区间标签 + let trendRangeLabel; + if (salesTrend.length) { + const first = salesTrend[0].date; + const last = salesTrend[salesTrend.length - 1].date; + trendRangeLabel = first.length === 7 ? `${first} ~ ${last}` : `${first.slice(5)} ~ ${last.slice(5)}`; + } else { + trendRangeLabel = trendInterval === 'all' ? '全部历史' : '无数据'; + } + + // ===== 复购率:全历史已完成订单(status=3)口径,不受 metricRange 影响 ===== + // 业务语义:电商"复购率"通常指"全历史买家行为",分子 = 完成 ≥2 单的用户,分母 = 完成 ≥1 单的用户 + const [repurchase] = await sequelize.query( + "SELECT " + + " SUM(CASE WHEN cnt >= 2 THEN 1 ELSE 0 END) AS repeat_users, " + + " COUNT(*) AS total_users " + + "FROM (SELECT user_id, COUNT(*) AS cnt FROM `order` WHERE status = 3 GROUP BY user_id) t", + { type: sequelize.QueryTypes.SELECT } + ); + const repeatUsers = Number(repurchase.repeat_users || 0); + const totalBuyers = Number(repurchase.total_users || 0); + const repurchaseRate = totalBuyers > 0 ? Number(((repeatUsers / totalBuyers) * 100).toFixed(2)) : 0; + + // ===== 分类销量占比:按 category 聚合销售额(仅已完成 status=3 的订单) ===== + const categoryWhereExtra = rangeDays + ? " AND o.created_at >= DATE_SUB(NOW(), INTERVAL ? DAY) " + : ""; + const categoryRows = await sequelize.query( + "SELECT c.id AS category_id, c.name AS category_name, " + + "IFNULL(SUM(oi.subtotal), 0) AS sales, " + + "IFNULL(SUM(oi.quantity), 0) AS quantity " + + "FROM order_item oi " + + "JOIN `order` o ON oi.order_id = o.id " + + "JOIN product p ON oi.product_id = p.id " + + "JOIN category c ON p.category_id = c.id " + + "WHERE o.status = 3 " + categoryWhereExtra + + "GROUP BY c.id, c.name " + + "ORDER BY sales DESC", + { replacements: rangeDays ? [rangeDays] : [], type: sequelize.QueryTypes.SELECT } + ); + const categoryShare = categoryRows.map((r) => ({ + categoryId: Number(r.category_id), + categoryName: r.category_name, + sales: Number(r.sales), + quantity: Number(r.quantity), + })); + + // ===== Top10 热销商品(按销售数量) ===== + const topWhereExtra = rangeDays + ? " AND o.created_at >= DATE_SUB(NOW(), INTERVAL ? DAY) " + : ""; + const topRows = await sequelize.query( + "SELECT oi.product_id, oi.product_name, oi.product_image, " + + "IFNULL(SUM(oi.quantity), 0) AS quantity, " + + "IFNULL(SUM(oi.subtotal), 0) AS sales " + + "FROM order_item oi " + + "JOIN `order` o ON oi.order_id = o.id " + + "WHERE o.status IN (1,2,3) " + topWhereExtra + + "GROUP BY oi.product_id, oi.product_name, oi.product_image " + + "ORDER BY quantity DESC, sales DESC " + + "LIMIT 10", + { replacements: rangeDays ? [rangeDays] : [], type: sequelize.QueryTypes.SELECT } + ); + const topProducts = topRows.map((r) => ({ + productId: Number(r.product_id), + productName: r.product_name, + productImage: r.product_image, + quantity: Number(r.quantity), + sales: Number(r.sales), + })); + + // ===== 订单状态分布(含说明列) ===== + const orderStatusRows = Object.keys(STATUS_LABEL).map((k) => { + const code = Number(k); + const { label, desc } = STATUS_LABEL[code]; + return { code, label, count: byStatus[code] || 0, desc }; + }); + res.json(success({ + // 原有字段(保持兼容) userCount: Number(users[0].c), productCount: Number(products[0].c), orderCount: Number(orders[0].c), - todayOrderCount: Number(todayOrders[0].c), + todayOrderCount, // 已按 kpiRange 计算(kpiRange=all 时 = CURDATE(),否则 = orderCount) totalRevenue: Number(revenue[0].s), - pendingOrderCount: byStatus[0], + pendingOrderCount, // 由 kpiRange 控制的独立查询算出(与 byStatus[0] 解耦) paidOrderCount: byStatus[1], shippedOrderCount: byStatus[2], completedOrderCount: byStatus[3], cancelledOrderCount: byStatus[4], refundedOrderCount: byStatus[5], + // 新增字段 + weeklySales, + lastWeekSales, + weeklyDeltaPct, + metricRangeKey, + kpiRangeKey, + repurchaseRate, + repurchaseUserCount: repeatUsers, + totalBuyerCount: totalBuyers, + salesTrend, + trendRangeLabel, + trendWeekOffset: weekOffset, + trendInterval, + categoryShare, + topProducts, + categoryTopRange: rangeKey, + orderStatusRows, + orderRangeKey, }, 'ok')); } catch (e) { next(e); } }); diff --git a/backend/src/modules/order/controllers/adminOrderController.js b/backend/src/modules/order/controllers/adminOrderController.js index dea2614d7c8cff2fc5c83fac30b8be573fef4899..ab8f21f97a5bbfb1e0eb1747acbb872ba8a3864d 100644 --- a/backend/src/modules/order/controllers/adminOrderController.js +++ b/backend/src/modules/order/controllers/adminOrderController.js @@ -4,18 +4,27 @@ 'use strict'; const { Op } = require('sequelize'); +const { sequelize } = require('../../../config/db'); const { success } = require('../../../common/response'); const { paginated, parsePagination } = require('../../../common/pagination'); -const { ParamError } = require('../../../common/exception'); +const { ParamError, NotFoundError } = require('../../../common/exception'); const Order = require('../models/Order'); const OrderItem = require('../models/OrderItem'); async function list(req, res, next) { try { const { page, pageSize, offset, limit } = parsePagination(req.query, { defaultPageSize: 20 }); - const where = {}; - if (req.query.status !== undefined && req.query.status !== '') where.status = req.query.status; - if (req.query.keyword) where.orderNo = { [Op.like]: `%${req.query.keyword}%` }; + const conditions = []; + if (req.query.status !== undefined && req.query.status !== '') conditions.push({ status: req.query.status }); + if (req.query.keyword) conditions.push({ orderNo: { [Op.like]: `%${req.query.keyword}%` } }); + // 概览页"今日订单"卡片跳转过来时带 ?today=1:按 MySQL 数据库日期 = 今天 + // 与 stats.routes.js 的 CURDATE() 语义保持一致,避免 Node/MySQL 时区差导致的边界偏移 + if (req.query.today === '1' || req.query.today === 'true') { + conditions.push( + sequelize.where(sequelize.fn('DATE', sequelize.col('created_at')), sequelize.literal('CURDATE()')) + ); + } + const where = conditions.length ? { [Op.and]: conditions } : {}; const { count, rows } = await Order.findAndCountAll({ where, offset, limit, order: [['id', 'DESC']] }); res.json(paginated(rows, count, page, pageSize)); } catch (e) { next(e); } @@ -33,14 +42,16 @@ async function ship(req, res, next) { } catch (e) { next(e); } } -async function setStatus(req, res, next) { +/** 后台主动"完成订单"(用于已发货后买家未确认收货的兜底) */ +async function complete(req, res, next) { try { - const s = parseInt(req.query.status, 10); const o = await Order.findByPk(req.params.id); if (!o) throw new ParamError('订单不存在'); - o.status = s; + if (o.status !== 2) throw new ParamError('只有已发货订单可完成'); + o.status = 3; + o.finishedAt = new Date(); await o.save(); - res.json(success(null, '已更新')); + res.json(success(null, '已完成')); } catch (e) { next(e); } } @@ -51,4 +62,14 @@ async function items(req, res, next) { } catch (e) { next(e); } } -module.exports = { list, ship, setStatus, items }; +/** 后台订单详情(admin 无 userId 过滤,复用 orderService 以保证返回结构与 buyer 一致) */ +async function detail(req, res, next) { + try { + const data = await require('../services/orderService').orderDetail({ + orderId: req.params.id, + }); + res.json(success(data)); + } catch (e) { next(e); } +} + +module.exports = { list, ship, complete, items, detail }; \ No newline at end of file diff --git a/backend/src/modules/order/controllers/orderController.js b/backend/src/modules/order/controllers/orderController.js index b02d1cbf6f92f8639a85b3704118a84650a349f9..093cea4170767f33c72270b086a1aeee47dfc3ee 100644 --- a/backend/src/modules/order/controllers/orderController.js +++ b/backend/src/modules/order/controllers/orderController.js @@ -50,4 +50,11 @@ async function confirm(req, res, next) { } catch (e) { next(e); } } -module.exports = { create, list, detail, cancel, confirm, createSchema }; +async function remove(req, res, next) { + try { + await orderService.deleteOrder({ userId: req.user.userId, orderId: req.params.id }); + res.json(success(null, '已删除')); + } catch (e) { next(e); } +} + +module.exports = { create, list, detail, cancel, confirm, remove, createSchema }; diff --git a/backend/src/modules/order/controllers/orderStatusController.js b/backend/src/modules/order/controllers/orderStatusController.js new file mode 100644 index 0000000000000000000000000000000000000000..4ff4d1a40281205af6149be40f52b9e4397ece4b --- /dev/null +++ b/backend/src/modules/order/controllers/orderStatusController.js @@ -0,0 +1,19 @@ +/** + * 订单状态查询 controller — 供前端支付页轮询 + */ +'use strict'; + +const orderService = require('../services/orderService'); +const { success } = require('../../../common/response'); + +async function status(req, res, next) { + try { + const data = await orderService.getOrderStatus({ + userId: req.user.userId, + orderId: req.params.id, + }); + res.json(success(data)); + } catch (e) { next(e); } +} + +module.exports = { status }; \ No newline at end of file diff --git a/backend/src/modules/order/models/OrderItem.js b/backend/src/modules/order/models/OrderItem.js index 88f1883036682cbd2968addd87aaaa0feba584d1..a95e917dc635a7abde35ed77aed37b0987049e9f 100644 --- a/backend/src/modules/order/models/OrderItem.js +++ b/backend/src/modules/order/models/OrderItem.js @@ -1,4 +1,3 @@ -/** 订单项(含商品名/价格快照,防止商品改价/下架影响历史订单) */ 'use strict'; const { DataTypes } = require('sequelize'); @@ -8,9 +7,11 @@ const OrderItem = sequelize.define('OrderItem', { id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true }, orderId: { type: DataTypes.BIGINT, allowNull: false, field: 'order_id' }, productId: { type: DataTypes.BIGINT, allowNull: false, field: 'product_id' }, + bundleId: { type: DataTypes.BIGINT, allowNull: true, field: 'bundle_id', comment: 'NULL=base;非空=product_bundle.id 快照' }, productName: { type: DataTypes.STRING(255), allowNull: false, field: 'product_name' }, + specName: { type: DataTypes.STRING(64), allowNull: true, field: 'spec_name', comment: '冗余快照,如 "5盒装"' }, productImage: { type: DataTypes.STRING(255), allowNull: true, field: 'product_image' }, - price: { type: DataTypes.DECIMAL(10, 2), allowNull: false }, + price: { type: DataTypes.DECIMAL(10, 2), allowNull: false, comment: '冗余快照(base=product.price;bundle=bundle.price × bundle.quantity)' }, quantity: { type: DataTypes.INTEGER, allowNull: false }, subtotal: { type: DataTypes.DECIMAL(10, 2), allowNull: false }, }, { tableName: 'order_item' }); diff --git a/backend/src/modules/order/routes/index.js b/backend/src/modules/order/routes/index.js index 5ce00dfa280cfecd50eea6d19889c3e007fe02d6..e8cfdd8a9d70834e01ada1627ba4b61a2fd31f7e 100644 --- a/backend/src/modules/order/routes/index.js +++ b/backend/src/modules/order/routes/index.js @@ -9,6 +9,7 @@ const { validate } = require('../../../common/validator'); const orderCtl = require('../controllers/orderController'); const paymentCtl = require('../controllers/paymentController'); +const orderStatusCtl = require('../controllers/orderStatusController'); const adminOrderCtl = require('../controllers/adminOrderController'); // /api/orders/* (买家) @@ -16,8 +17,10 @@ router.use('/orders', authMiddleware); router.post('/orders', validate(orderCtl.createSchema), orderCtl.create); router.get('/orders', orderCtl.list); router.get('/orders/:id', orderCtl.detail); +router.get('/orders/:id/status', orderStatusCtl.status); router.post('/orders/:id/cancel', orderCtl.cancel); router.post('/orders/:id/confirm', orderCtl.confirm); +router.delete('/orders/:id', orderCtl.remove); // /api/pay/* (支付) router.post('/pay/mock', authMiddleware, validate(paymentCtl.mockSchema), paymentCtl.mockPay); @@ -26,8 +29,9 @@ router.post('/pay/callback', paymentCtl.callback); // /api/admin/orders/* (后台) router.use('/admin/orders', authMiddleware, requireRole('admin')); router.get('/admin/orders', adminOrderCtl.list); +router.get('/admin/orders/:id', adminOrderCtl.detail); router.post('/admin/orders/:id/ship', adminOrderCtl.ship); -router.put('/admin/orders/:id/status', adminOrderCtl.setStatus); +router.post('/admin/orders/:id/complete', adminOrderCtl.complete); router.get('/admin/orders/:id/items', adminOrderCtl.items); module.exports = router; diff --git a/backend/src/modules/order/services/orderService.js b/backend/src/modules/order/services/orderService.js index 7a9ed545a402d36733883233323e948982caa917..1e07a37b7b83c4dc7d426e89bb795619c9225835 100644 --- a/backend/src/modules/order/services/orderService.js +++ b/backend/src/modules/order/services/orderService.js @@ -11,13 +11,18 @@ const { Op } = require('sequelize'); const { sequelize } = require('../../../config/db'); +const { redis } = require('../../../config/redis'); const Order = require('../models/Order'); const OrderItem = require('../models/OrderItem'); const CartItem = require('../../user/models/CartItem'); const Address = require('../../user/models/Address'); const Product = require('../../product/models/Product'); +const ProductBundle = require('../../product/models/ProductBundle'); const { ParamError, NotFoundError, ConflictError } = require('../../../common/exception'); +const TIMEOUT_KEY = (id) => `order:timeout:${id}`; +const TIMEOUT_SECONDS = 30 * 60; + const ALLOWED_TRANSITIONS = { 0: new Set([1, 4]), 1: new Set([2, 5]), @@ -45,37 +50,82 @@ async function createOrder({ userId, addressId, cartItemIds }) { const products = await Product.findAll({ where: { id: { [Op.in]: productIds } }, transaction: t }); const productMap = new Map(products.map((p) => [p.id, p])); + // 收集 cart 中的 bundle_id 一次性查 ProductBundle(命名快照) + const bundleIds = carts.map((c) => c.bundleId).filter((x) => x != null); + const bundles = bundleIds.length + ? await ProductBundle.findAll({ where: { id: { [Op.in]: bundleIds } }, transaction: t }) + : []; + const bundleMap = new Map(bundles.map((b) => [String(b.id), b])); + + // C1 修复:orderNo 在循环外生成一次;cancelOrder 端使用 order.orderNo 拼装 bizNo, + // 两端必须使用同一个 orderNo,幂等键才能对称匹配。 + const orderNo = genOrderNo(); + let total = 0; const items = []; for (const c of carts) { const p = productMap.get(c.productId); if (!p || p.onSale !== 1) throw new ParamError('商品 ' + c.productId + ' 已下架'); - if (p.stock < c.quantity) throw new ParamError('商品 ' + p.name + ' 库存不足'); - const [affected] = await sequelize.query( - 'UPDATE product SET stock = stock - ? WHERE id = ? AND stock >= ?', - { replacements: [c.quantity, p.id, c.quantity], transaction: t } - ); - if (affected[1] === 0) throw new ParamError('商品 ' + p.name + ' 库存不足'); - const orderNoDraft = genOrderNo(); + let bundle = null; + let unitPrice; // 每套/件单价(用于 order_item.price 快照) + let stockDecrement; // 扣减的库存量(套数 = 件数,按 bundle/base 区分) + if (c.bundleId != null) { + bundle = bundleMap.get(String(c.bundleId)); + if (!bundle || bundle.onSale !== 1 || bundle.deleted !== 0) { + throw new ParamError('规格 ' + c.bundleId + ' 已下架'); + } + if (bundle.stock < c.quantity) throw new ParamError('规格 ' + bundle.specName + ' 库存不足'); + unitPrice = Number(bundle.price) * Number(bundle.quantity); + stockDecrement = c.quantity; + } else { + if (p.stock < c.quantity) throw new ParamError('商品 ' + p.name + ' 库存不足'); + unitPrice = Number(p.price); + stockDecrement = c.quantity; + } + + // 幂等键:与 cancelOrder 对称(bundle 端用 `${order.orderNo}:bundle:${bundleId}` / base 端用 `${order.orderNo}:item:${cartItemId}`)。 + // - bundle:附加 bundleId 后缀,避免与 base 同 orderNo 冲突 + // - base :使用 cart_item.id 作为该商品维度的幂等后缀 + const bizNo = bundle ? `${orderNo}:bundle:${bundle.id}` : `${orderNo}:item:${c.id}`; + const stockLogProductId = p.id; // stock_log.product_id 仍是 base product(保持兼容) + const stockLogDelta = bundle ? -c.quantity : -c.quantity; // 同号;语义:bundle 减多少套 → product 维度仍是负数 + const stockLogBundleId = bundle ? bundle.id : null; // I3:bundle 路径记 bundle_id 入 stock_log + + if (bundle) { + // 扣 product_bundle.stock(带 productId 校验,防 bundle 跨 product 提交) + const [affected] = await sequelize.query( + 'UPDATE product_bundle SET stock = stock - ? WHERE id = ? AND product_id = ? AND stock >= ? AND deleted = 0', + { replacements: [c.quantity, bundle.id, p.id, c.quantity], transaction: t } + ); + if (affected[1] === 0) throw new ParamError('规格 ' + bundle.specName + ' 库存不足'); + } else { + // 扣 product.stock(I4:增加 on_sale=1 条件,防御在事务中途被下架的竞态扣减) + const [affected] = await sequelize.query( + 'UPDATE product SET stock = stock - ? WHERE id = ? AND stock >= ? AND deleted = 0 AND on_sale = 1', + { replacements: [c.quantity, p.id, c.quantity], transaction: t } + ); + if (affected[1] === 0) throw new ParamError('商品 ' + p.name + ' 库存不足'); + } await sequelize.query( - 'INSERT INTO stock_log (biz_no, product_id, delta, created_at) VALUES (?, ?, ?, NOW())', - { replacements: [orderNoDraft, p.id, -c.quantity], transaction: t } + 'INSERT INTO stock_log (biz_no, product_id, bundle_id, delta, created_at) VALUES (?, ?, ?, ?, NOW())', + { replacements: [bizNo, stockLogProductId, stockLogBundleId, stockLogDelta], transaction: t } ); - const subtotal = Number(p.price) * c.quantity; + const subtotal = unitPrice * c.quantity; total += subtotal; items.push({ productId: p.id, + bundleId: bundle ? bundle.id : null, productName: p.name, - productImage: p.image, - price: p.price, + specName: bundle ? bundle.specName : null, + productImage: bundle?.image || p.image, + price: unitPrice.toFixed(2), quantity: c.quantity, subtotal: subtotal.toFixed(2), }); } - const orderNo = genOrderNo(); const order = await Order.create({ orderNo, userId, addressId, total: total.toFixed(2), status: 0, }, { transaction: t }); @@ -83,6 +133,10 @@ async function createOrder({ userId, addressId, cartItemIds }) { await CartItem.destroy({ where: { id: carts.map((c) => c.id) }, transaction: t }); await t.commit(); + // 写入超时 key(30 分钟);Task 7 的 Redis 过期监听会基于这个 key 触发自动取消 + redis.setEx(TIMEOUT_KEY(order.id), TIMEOUT_SECONDS, '1').catch((e) => + console.error('[order] setEx timeout key failed:', e.message) + ); return { orderNo: order.orderNo, id: order.id, total: order.total }; } catch (e) { if (!t.finished) await t.rollback(); @@ -91,11 +145,17 @@ async function createOrder({ userId, addressId, cartItemIds }) { } async function cancelOrder({ userId, orderId }) { - const order = await Order.findOne({ where: { id: orderId, userId } }); - if (!order) throw new NotFoundError('订单不存在'); - if (order.status !== 0) throw new ConflictError('只有待支付订单可取消'); const t = await sequelize.transaction(); try { + // 事务内 SELECT ... FOR UPDATE,防并发取消导致重复释放库存 + const order = await Order.findOne({ + where: { id: orderId, userId }, + transaction: t, + lock: t.LOCK ? t.LOCK.UPDATE : undefined, + }); + if (!order) throw new NotFoundError('订单不存在'); + if (order.status !== 0) throw new ConflictError('只有待支付订单可取消'); + order.status = 4; order.cancelledAt = new Date(); order.cancelReason = '用户主动取消'; @@ -103,23 +163,42 @@ async function cancelOrder({ userId, orderId }) { const items = await OrderItem.findAll({ where: { orderId: order.id }, transaction: t }); for (const it of items) { - const bizNo = `cancel:${order.orderNo}:${it.id}`; + // 幂等键:与 createOrder 对称 + const bizNo = it.bundleId + ? `${order.orderNo}:bundle:${it.bundleId}` + : `${order.orderNo}:${it.id}`; + const [log] = await sequelize.query( 'SELECT id FROM stock_log WHERE biz_no = ?', { replacements: [bizNo], transaction: t } ); if (log[0] && log[0].length) continue; - await sequelize.query( - 'INSERT INTO stock_log (biz_no, product_id, delta, created_at) VALUES (?, ?, ?, NOW())', - { replacements: [bizNo, it.productId, it.quantity], transaction: t } - ); - await sequelize.query( - 'UPDATE product SET stock = stock + ? WHERE id = ?', - { replacements: [it.quantity, it.productId], transaction: t } - ); + + if (it.bundleId) { + // 回补 product_bundle.stock(I3:bundle 路径同步记 bundle_id 入 stock_log) + await sequelize.query( + 'INSERT INTO stock_log (biz_no, product_id, bundle_id, delta, created_at) VALUES (?, ?, ?, ?, NOW())', + { replacements: [bizNo, it.productId, it.bundleId, it.quantity], transaction: t } + ); + await sequelize.query( + 'UPDATE product_bundle SET stock = stock + ? WHERE id = ?', + { replacements: [it.quantity, it.bundleId], transaction: t } + ); + } else { + // 回补 product.stock(I3:bundle_id 写 NULL,与 createOrder 端对称) + await sequelize.query( + 'INSERT INTO stock_log (biz_no, product_id, bundle_id, delta, created_at) VALUES (?, ?, ?, ?, NOW())', + { replacements: [bizNo, it.productId, null, it.quantity], transaction: t } + ); + await sequelize.query( + 'UPDATE product SET stock = stock + ? WHERE id = ?', + { replacements: [it.quantity, it.productId], transaction: t } + ); + } } + await redis.del(TIMEOUT_KEY(order.id)).catch(() => {}); await t.commit(); - } catch (e) { await t.rollback(); throw e; } + } catch (e) { if (!t.finished) await t.rollback(); throw e; } } async function confirmOrder({ userId, orderId }) { @@ -149,8 +228,11 @@ async function listOrders({ userId, status, page, pageSize }) { async function orderDetail({ userId, orderId }) { const Address = require('../../user/models/Address'); + // buyer: where 含 userId;admin(userId 不传): 只按 id 查,跳过 userId 过滤 + const where = { id: orderId }; + if (userId) where.userId = userId; const order = await Order.findOne({ - where: { id: orderId, userId }, + where, include: [ { model: OrderItem, as: 'items' }, { model: Address, as: 'address' }, @@ -163,10 +245,32 @@ async function orderDetail({ userId, orderId }) { obj.paidAt = order.paid_at || order.paidAt; obj.shippedAt = order.shipped_at || order.shippedAt; obj.finishedAt = order.finished_at || order.finishedAt; + obj.cancelledAt = order.cancelled_at || order.cancelledAt; + obj.cancelReason = order.cancel_reason || order.cancelReason; return obj; } +async function deleteOrder({ userId, orderId }) { + const row = await Order.findOne({ where: { id: orderId, userId } }); + if (!row) throw new ParamError('订单不存在'); + await row.destroy(); +} + +async function getOrderStatus({ userId, orderId }) { + const order = await Order.findOne({ + where: { id: orderId, userId }, + attributes: ['id', 'status', 'paidAt', 'createdAt'], + }); + if (!order) throw new NotFoundError('订单不存在'); + const expired = order.status === 0 && (Date.now() - new Date(order.createdAt).getTime()) > 30 * 60 * 1000; + return { + status: order.status, + paidAt: order.paidAt, + expired, + }; +} + module.exports = { - createOrder, cancelOrder, confirmOrder, listOrders, orderDetail, - ALLOWED_TRANSITIONS, genOrderNo, + createOrder, cancelOrder, confirmOrder, listOrders, orderDetail, getOrderStatus, + deleteOrder, ALLOWED_TRANSITIONS, genOrderNo, }; diff --git a/backend/src/modules/order/services/paymentService.js b/backend/src/modules/order/services/paymentService.js index acabea2c79f73988b6374e76851300e73362a4c7..aa86a4f1650f6b8df61832df63a4a2f01f76b5ad 100644 --- a/backend/src/modules/order/services/paymentService.js +++ b/backend/src/modules/order/services/paymentService.js @@ -3,16 +3,23 @@ */ 'use strict'; +const { redis } = require('../../../config/redis'); const { sequelize } = require('../../../config/db'); const Order = require('../models/Order'); const Payment = require('../models/Payment'); const { ParamError, ConflictError } = require('../../../common/exception'); const { genOrderNo } = require('./orderService'); +const TIMEOUT_KEY = (id) => `order:timeout:${id}`; + async function mockPay({ userId, orderId, simulateSuccess }) { const t = await sequelize.transaction(); try { - const order = await Order.findOne({ where: { id: orderId, userId }, transaction: t }); + const order = await Order.findOne({ + where: { id: orderId, userId }, + transaction: t, + lock: t.LOCK ? t.LOCK.UPDATE : undefined, + }); if (!order) throw new ParamError('订单不存在'); if (order.status !== 0) throw new ConflictError('订单状态非待支付'); @@ -27,11 +34,15 @@ async function mockPay({ userId, orderId, simulateSuccess }) { order.status = 1; order.paidAt = new Date(); await order.save({ transaction: t }); - await t.commit(); - return { paymentNo, status: 1 }; } await t.commit(); - return { paymentNo, status: 2 }; + + // 支付成功/失败均清除 timeout key(避免 30 分钟后过期回调误取消已支付订单) + redis.del(TIMEOUT_KEY(order.id)).catch((e) => + console.error('[pay] del timeout key failed:', e.message) + ); + + return { paymentNo, status: ok ? 1 : 2 }; } catch (e) { if (!t.finished) await t.rollback(); throw e; @@ -39,21 +50,28 @@ async function mockPay({ userId, orderId, simulateSuccess }) { } async function handleCallback({ paymentNo, status }) { - const payment = await Payment.findOne({ where: { paymentNo } }); - if (!payment) return null; - if (payment.status !== 0) return 'already processed'; - payment.status = status; - payment.paidAt = new Date(); - await payment.save(); - if (status === 1) { - const order = await Order.findByPk(payment.orderId); - if (order && order.status === 0) { - order.status = 1; - order.paidAt = new Date(); - await order.save(); + const t = await sequelize.transaction(); + try { + const payment = await Payment.findOne({ where: { paymentNo }, transaction: t }); + if (!payment) { await t.rollback(); return null; } + if (payment.status !== 0) { await t.commit(); return 'already processed'; } + + payment.status = status; + payment.paidAt = new Date(); + await payment.save({ transaction: t }); + + if (status === 1) { + const order = await Order.findByPk(payment.orderId, { transaction: t }); + if (order && order.status === 0) { + order.status = 1; + order.paidAt = new Date(); + await order.save({ transaction: t }); + await redis.del(TIMEOUT_KEY(order.id)).catch(() => {}); + } } - } - return null; + await t.commit(); + return null; + } catch (e) { if (!t.finished) await t.rollback(); throw e; } } -module.exports = { mockPay, handleCallback }; +module.exports = { mockPay, handleCallback }; \ No newline at end of file diff --git a/backend/src/modules/order/services/timeoutService.js b/backend/src/modules/order/services/timeoutService.js new file mode 100644 index 0000000000000000000000000000000000000000..92d50bfd8e93db2900680f11f3179d46e0418c7d --- /dev/null +++ b/backend/src/modules/order/services/timeoutService.js @@ -0,0 +1,138 @@ +/** + * 订单超时自动取消(C03 挑战项) + * - 主方案:Redis Keyspace 通知(订阅 __keyevent@0__:expired) + * - 兜底:每 60s 扫描一次 created_at < NOW() - 30min 的待支付订单 + * + * 依赖: + * orderService.createOrder 写入 `order:timeout:{orderId}` 30 分钟 TTL + * cancelExpiredOrder 使用 stock_log 幂等键避免重复释放库存 + */ +'use strict'; + +const { Op } = require('sequelize'); +const { redis, createSubscriber } = require('../../../config/redis'); +const { sequelize } = require('../../../config/db'); +const Order = require('../models/Order'); +const OrderItem = require('../models/OrderItem'); + +const TIMEOUT_PREFIX = 'order:timeout:'; +const TIMEOUT_SECONDS = 30 * 60; +const FALLBACK_SCAN_INTERVAL = 60_000; +const FALLBACK_BATCH = 50; + +let subscriber = null; +let fallbackTimer = null; + +/** 把超时订单置为已取消并释放库存(幂等:靠 stock_log 唯一键 + status 判断) */ +async function cancelExpiredOrder(orderId) { + const t = await sequelize.transaction(); + try { + const order = await Order.findByPk(orderId, { + transaction: t, + lock: t.LOCK ? t.LOCK.UPDATE : undefined, + }); + if (!order || order.status !== 0) { + // 已支付/已取消/不存在 → 不处理 + await t.commit(); + return false; + } + + order.status = 4; + order.cancelledAt = new Date(); + order.cancelReason = '超时未支付自动取消'; + await order.save({ transaction: t }); + + const items = await OrderItem.findAll({ where: { orderId: order.id }, transaction: t }); + for (const it of items) { + const bizNo = `timeout:${order.orderNo}:${it.id}`; + const [log] = await sequelize.query( + 'SELECT id FROM stock_log WHERE biz_no = ?', + { replacements: [bizNo], transaction: t } + ); + if (log[0] && log[0].length) continue; // 幂等:已回补过则跳过 + await sequelize.query( + 'INSERT INTO stock_log (biz_no, product_id, delta, created_at) VALUES (?, ?, ?, NOW())', + { replacements: [bizNo, it.productId, it.quantity], transaction: t } + ); + await sequelize.query( + 'UPDATE product SET stock = stock + ? WHERE id = ?', + { replacements: [it.quantity, it.productId], transaction: t } + ); + } + + await t.commit(); + console.log(`[timeout] order ${order.orderNo} auto canceled`); + return true; + } catch (e) { + if (!t.finished) await t.rollback(); + throw e; + } +} + +/** 兜底定时扫描:兜住 Redis 重启 / notify-keyspace-events 未开启等异常 */ +async function fallbackScan() { + const cutoff = new Date(Date.now() - TIMEOUT_SECONDS * 1000); + const expired = await Order.findAll({ + where: { status: 0, createdAt: { [Op.lt]: cutoff } }, + limit: FALLBACK_BATCH, + }); + for (const o of expired) { + try { await cancelExpiredOrder(o.id); } + catch (e) { console.error('[timeout] fallback cancel failed:', o.id, e.message); } + } +} + +/** 启动:Redis 过期监听 + 兜底定时器 */ +async function startTimeoutWatcher() { + // 1) 尝试开启 Redis keyspace notification(best-effort;非 Redis 管理员也无妨) + try { + await redis.config('SET', 'notify-keyspace-events', 'Ex'); + console.log('[timeout] notify-keyspace-events set to Ex'); + } catch (e) { + console.warn('[timeout] failed to enable notify-keyspace-events (will rely on fallback):', e.message); + } + + // 2) 创建独立 subscriber 客户端(不能与 redis 主连接共享) + subscriber = createSubscriber(); + try { + await subscriber.connect(); + await subscriber.subscribe('__keyevent@0__:expired', async (key) => { + if (!key || !key.startsWith(TIMEOUT_PREFIX)) return; + const orderId = Number(key.slice(TIMEOUT_PREFIX.length)); + if (!orderId) return; + try { + await cancelExpiredOrder(orderId); + } catch (e) { + console.error('[timeout] cancel failed:', orderId, e.message); + } + }); + console.log('✅ timeout watcher subscribed to __keyevent@0__:expired'); + } catch (e) { + console.error('[timeout] subscribe failed (will rely on fallback):', e.message); + } + + // 3) 兜底:每 60s 扫描一次(即使 Redis 通知完全失效,也能保证最终一致性) + fallbackTimer = setInterval(() => { + fallbackScan().catch((e) => console.error('[timeout] fallback scan failed:', e.message)); + }, FALLBACK_SCAN_INTERVAL); +} + +function stopTimeoutWatcher() { + if (subscriber) { + try { subscriber.unsubscribe().catch(() => {}); } catch (_) {} + try { subscriber.quit().catch(() => {}); } catch (_) {} + subscriber = null; + } + if (fallbackTimer) { + clearInterval(fallbackTimer); + fallbackTimer = null; + } +} + +module.exports = { + startTimeoutWatcher, + stopTimeoutWatcher, + cancelExpiredOrder, + TIMEOUT_SECONDS, + TIMEOUT_PREFIX, +}; \ No newline at end of file diff --git a/backend/src/modules/product/controllers/adminBundleController.js b/backend/src/modules/product/controllers/adminBundleController.js new file mode 100644 index 0000000000000000000000000000000000000000..bd5784aec0ae04c82fdab232b8d6865f9bccc072 --- /dev/null +++ b/backend/src/modules/product/controllers/adminBundleController.js @@ -0,0 +1,75 @@ +/** + * Admin 规格(bundle)controller + */ +'use strict'; + +const Joi = require('joi'); +const bundleService = require('../services/bundleService'); +const { success } = require('../../../common/response'); +const { validate } = require('../../../common/validator'); +const { ParamError } = require('../../../common/exception'); + +// 公共字段 +const createSchema = Joi.object({ + specName: Joi.string().min(1).max(64).required(), + quantity: Joi.number().integer().min(1).required(), + price: Joi.number().precision(2).min(0).required(), + stock: Joi.number().integer().min(0).required(), + image: Joi.string().uri().allow('', null).optional(), + sort: Joi.number().integer().optional(), + onSale: Joi.number().valid(0, 1).optional(), +}); + +const updateSchema = Joi.object({ + specName: Joi.string().min(1).max(64).optional(), + quantity: Joi.number().integer().min(1).optional(), + price: Joi.number().precision(2).min(0).optional(), + stock: Joi.number().integer().min(0).optional(), + image: Joi.string().uri().allow('', null).optional(), + sort: Joi.number().integer().optional(), + onSale: Joi.number().valid(0, 1).optional(), +}).min(1); + +async function list(req, res, next) { + try { + const data = await bundleService.listByProduct(req.params.id); + res.json(success(data)); + } catch (e) { next(e); } +} + +async function create(req, res, next) { + try { + const data = await bundleService.create(req.params.id, req.body); + res.json(success(data, '已新增')); + } catch (e) { next(e); } +} + +async function update(req, res, next) { + try { + const data = await bundleService.update(req.params.bundleId, req.body); + res.json(success(data, '已更新')); + } catch (e) { next(e); } +} + +async function setOnSale(req, res, next) { + try { + const onSale = parseInt(req.query.onSale, 10); + if (onSale !== 0 && onSale !== 1) { + throw new ParamError('onSale 必须为 0 或 1'); + } + const data = await bundleService.setOnSale(req.params.bundleId, onSale); + res.json(success(data, onSale === 1 ? '已上架' : '已下架')); + } catch (e) { next(e); } +} + +async function softDelete(req, res, next) { + try { + await bundleService.softDelete(req.params.bundleId); + res.json(success(null, '已删除')); + } catch (e) { next(e); } +} + +module.exports = { + list, create, update, setOnSale, softDelete, + createSchema, updateSchema, +}; diff --git a/backend/src/modules/product/controllers/adminSkuController.js b/backend/src/modules/product/controllers/adminSkuController.js new file mode 100644 index 0000000000000000000000000000000000000000..2da0b1b1f4bc3a88a1b854a74658b02b66bf3d73 --- /dev/null +++ b/backend/src/modules/product/controllers/adminSkuController.js @@ -0,0 +1,47 @@ +/** + * 后台 SKU controller + */ +'use strict'; + +const Joi = require('joi'); +const productService = require('../services/productService'); +const { success } = require('../../../common/response'); +const { validate } = require('../../../common/validator'); + +const skuUpsertSchema = Joi.object({ + productId: Joi.number().required(), + attrs: Joi.object().required(), + price: Joi.number().precision(2).required(), + stock: Joi.number().integer().min(0).required(), + status: Joi.number().valid(0, 1).default(1), +}); + +async function list(req, res, next) { + try { + const data = await productService.adminListSkus(req.params.productId); + res.json(success(data)); + } catch (e) { next(e); } +} + +async function create(req, res, next) { + try { + const data = await productService.adminCreateSku(req.body); + res.json(success(data, '已新增')); + } catch (e) { next(e); } +} + +async function update(req, res, next) { + try { + const data = await productService.adminUpdateSku(req.params.id, req.body); + res.json(success(data, '已更新')); + } catch (e) { next(e); } +} + +async function remove(req, res, next) { + try { + await productService.adminDeleteSku(req.params.id); + res.json(success(null, '已删除')); + } catch (e) { next(e); } +} + +module.exports = { list, create, update, remove, skuUpsertSchema, upsertSchema: skuUpsertSchema }; diff --git a/backend/src/modules/product/controllers/productController.js b/backend/src/modules/product/controllers/productController.js index 9ae0c22ab42208d2ae61561416ae97dc0856268b..92f84933869e688667e3413daf9a0d7529fe0a1a 100644 --- a/backend/src/modules/product/controllers/productController.js +++ b/backend/src/modules/product/controllers/productController.js @@ -5,6 +5,7 @@ const Joi = require('joi'); const productService = require('../services/productService'); +const bundleService = require('../services/bundleService'); const { success } = require('../../../common/response'); const { paginated, parsePagination } = require('../../../common/pagination'); const { validate } = require('../../../common/validator'); @@ -65,7 +66,16 @@ async function incrementStock(req, res, next) { } catch (e) { next(e); } } +/** 前台:列出某商品在售的 bundle(无需鉴权) */ +async function listBundles(req, res, next) { + try { + const data = await bundleService.listOnSaleByProduct(req.params.id); + res.json(success(data)); + } catch (e) { next(e); } +} + module.exports = { list, search, detail, batchGet, decrementStock, incrementStock, + listBundles, batchSchema, decrementSchema, incrementSchema, }; diff --git a/backend/src/modules/product/models/ProductBundle.js b/backend/src/modules/product/models/ProductBundle.js new file mode 100644 index 0000000000000000000000000000000000000000..5cab2369e69f63105401db9c15e6da1d17ab2635 --- /dev/null +++ b/backend/src/modules/product/models/ProductBundle.js @@ -0,0 +1,23 @@ +'use strict'; + +const { DataTypes } = require('sequelize'); +const { sequelize } = require('../../../config/db'); + +const ProductBundle = sequelize.define( + 'ProductBundle', + { + id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true }, + productId: { type: DataTypes.BIGINT, allowNull: false, field: 'product_id' }, + specName: { type: DataTypes.STRING(64), allowNull: false, field: 'spec_name' }, + quantity: { type: DataTypes.INTEGER, allowNull: false }, + price: { type: DataTypes.DECIMAL(10, 2), allowNull: false }, + stock: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0 }, + image: { type: DataTypes.STRING(255), allowNull: true }, + sort: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0 }, + onSale: { type: DataTypes.TINYINT, allowNull: false, defaultValue: 1, field: 'on_sale' }, + deleted: { type: DataTypes.TINYINT, allowNull: false, defaultValue: 0 }, + }, + { tableName: 'product_bundle', paranoid: false, timestamps: false } +); + +module.exports = ProductBundle; diff --git a/backend/src/modules/product/models/Sku.js b/backend/src/modules/product/models/Sku.js new file mode 100644 index 0000000000000000000000000000000000000000..afc55f70bec6835594a783ae06354bef753bd671 --- /dev/null +++ b/backend/src/modules/product/models/Sku.js @@ -0,0 +1,15 @@ +'use strict'; + +const { DataTypes } = require('sequelize'); +const { sequelize } = require('../../../config/db'); + +const Sku = sequelize.define('Sku', { + id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true }, + productId: { type: DataTypes.BIGINT, allowNull: false, field: 'product_id' }, + attrs: { type: DataTypes.JSON, allowNull: false, comment: '规格属性,如 {颜色:"红色", 内存:"8GB"}' }, + price: { type: DataTypes.DECIMAL(10, 2), allowNull: false }, + stock: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0 }, + status: { type: DataTypes.TINYINT, allowNull: false, defaultValue: 1, comment: '1有效 0无效' }, +}, { tableName: 'sku' }); + +module.exports = Sku; diff --git a/backend/src/modules/product/routes/index.js b/backend/src/modules/product/routes/index.js index a2ad05b1ba341856be8e557e62572a669d05187b..d114983827112c34e57e5e7a255b1a1f1fecb7f7 100644 --- a/backend/src/modules/product/routes/index.js +++ b/backend/src/modules/product/routes/index.js @@ -24,6 +24,7 @@ router.delete('/admin/categories/:id', categoryCtl.adminRemove); // /api/products/* (前台) router.get('/products/search', productCtl.search); router.get('/products', productCtl.list); +router.get('/products/:id/bundles', productCtl.listBundles); router.get('/products/:id', productCtl.detail); // 接缝 S01~S03 @@ -39,4 +40,15 @@ router.put('/admin/products/:id', validate(adminProductCtl.upsertSchema), adminP router.delete('/admin/products/:id', adminProductCtl.softDelete); router.post('/admin/products/:id/on-sale', adminProductCtl.setOnSale); +// /api/admin/bundles/* 单独鉴权(不在 /admin/products 前缀下) +router.use('/admin/bundles', authMiddleware, requireRole('admin')); + +// /api/admin/products/:id/bundles 和 /api/admin/bundles/:bundleId (后台规格 CRUD) +const adminBundleCtl = require('../controllers/adminBundleController'); +router.get('/admin/products/:id/bundles', adminBundleCtl.list); +router.post('/admin/products/:id/bundles', validate(adminBundleCtl.createSchema), adminBundleCtl.create); +router.patch('/admin/bundles/:bundleId', validate(adminBundleCtl.updateSchema), adminBundleCtl.update); +router.patch('/admin/bundles/:bundleId/on-sale', adminBundleCtl.setOnSale); +router.delete('/admin/bundles/:bundleId', adminBundleCtl.softDelete); + module.exports = router; diff --git a/backend/src/modules/product/services/bundleService.js b/backend/src/modules/product/services/bundleService.js new file mode 100644 index 0000000000000000000000000000000000000000..04c901a939b2c15e37f87e3e2906244e48b80701 --- /dev/null +++ b/backend/src/modules/product/services/bundleService.js @@ -0,0 +1,151 @@ +/** + * 规格(product_bundle)服务层 + */ +'use strict'; + +const { Op } = require('sequelize'); +const { sequelize } = require('../../../config/db'); +const Product = require('../models/Product'); +const ProductBundle = require('../models/ProductBundle'); +const { NotFoundError, ConflictError } = require('../../../common/exception'); + +/** 序列化:DECIMAL → string(保留 2 位小数,见 spec §4.4) */ +function serialize(row) { + if (!row) return row; + const plain = row.get ? row.get({ plain: true }) : { ...row }; + return { + ...plain, + price: plain.price == null ? null : Number(plain.price).toFixed(2), + }; +} + +/** 列出某商品的 bundle(按 sort ASC, id ASC),排除软删 */ +async function listByProduct(productId) { + // 校验 product 存在 + const exists = await Product.findOne({ where: { id: productId, deleted: 0 } }); + if (!exists) throw new NotFoundError('商品不存在'); + + const rows = await ProductBundle.findAll({ + where: { productId, deleted: 0 }, + order: [['sort', 'ASC'], ['id', 'ASC']], + }); + return rows.map(serialize); +} + +/** 新增 bundle(软删复活:同 productId+quantity 存在 deleted=1 行时,update 复活 + 覆盖字段) */ +async function create(productId, payload) { + return await sequelize.transaction(async (t) => { + // 校验 product 存在 + const exists = await Product.findOne({ where: { id: productId, deleted: 0 }, transaction: t }); + if (!exists) throw new NotFoundError('商品不存在'); + + // 校验唯一键:同 productId + quantity 的"活跃"行(复活场景下已存在的 deleted=1 行不算冲突) + // 用 SELECT ... FOR UPDATE 序列化并发请求,避免同时通过 active-check 再各自 INSERT 触发唯一键冲突 + const active = await ProductBundle.findOne({ + where: { productId, quantity: payload.quantity, deleted: 0 }, + transaction: t, + lock: t.LOCK ? t.LOCK.UPDATE : undefined, + }); + if (active) throw new ConflictError('该商品下已存在相同数量的规格'); + + // 软删复活:找到 deleted=1 的同 productId+quantity 行 → update 复活并覆盖 + const tombstone = await ProductBundle.findOne({ + where: { productId, quantity: payload.quantity, deleted: 1 }, + transaction: t, + }); + if (tombstone) { + await tombstone.update({ + specName: payload.specName, + price: payload.price, + stock: payload.stock, + image: payload.image || null, + sort: payload.sort ?? 0, + onSale: payload.onSale ?? 1, + deleted: 0, + }, { transaction: t }); + return serialize(tombstone); + } + + try { + const row = await ProductBundle.create({ + productId, + specName: payload.specName, + quantity: payload.quantity, + price: payload.price, + stock: payload.stock, + image: payload.image || null, + sort: payload.sort ?? 0, + onSale: payload.onSale ?? 1, + }, { transaction: t }); + return serialize(row); + } catch (e) { + if (e.name === 'SequelizeUniqueConstraintError') { + throw new ConflictError('该商品下已存在相同数量的规格'); + } + throw e; + } + }); +} + +/** 局部更新 */ +async function update(bundleId, payload) { + const row = await ProductBundle.findOne({ where: { id: bundleId, deleted: 0 } }); + if (!row) throw new NotFoundError('规格不存在'); + + // 若 quantity 改了,重新校验唯一键 + if (payload.quantity !== undefined && Number(payload.quantity) !== Number(row.quantity)) { + const dup = await ProductBundle.findOne({ + where: { + productId: row.productId, + quantity: payload.quantity, + deleted: 0, + id: { [Op.ne]: bundleId }, + }, + }); + if (dup) throw new ConflictError('该商品下已存在相同数量的规格'); + } + + await row.update({ + specName: payload.specName ?? row.specName, + quantity: payload.quantity ?? row.quantity, + price: payload.price ?? row.price, + stock: payload.stock ?? row.stock, + image: payload.image !== undefined ? payload.image : row.image, + sort: payload.sort ?? row.sort, + onSale: payload.onSale ?? row.onSale, + }); + return serialize(row); +} + +/** 单 bundle 上下架 */ +async function setOnSale(bundleId, onSale) { + const row = await ProductBundle.findOne({ where: { id: bundleId, deleted: 0 } }); + if (!row) throw new NotFoundError('规格不存在'); + await row.update({ onSale }); + return serialize(row); +} + +/** 软删 */ +async function softDelete(bundleId) { + const row = await ProductBundle.findOne({ where: { id: bundleId, deleted: 0 } }); + if (!row) throw new NotFoundError('规格不存在'); + await row.update({ deleted: 1 }); + return serialize(row); +} + +/** 列出某商品在售的 bundle(仅 onSale=1 且 deleted=0),按 sort ASC, id ASC + * + * I2 修复:parent product.on_sale 也要校验;parent 下架时不返任何 bundle(前端的"加入购物车"链路不可能走到下单) + */ +async function listOnSaleByProduct(productId) { + const exists = await Product.findOne({ where: { id: productId, deleted: 0, onSale: 1 } }); + if (!exists) throw new NotFoundError('商品不存在'); + + const rows = await ProductBundle.findAll({ + where: { productId, onSale: 1, deleted: 0 }, + order: [['sort', 'ASC'], ['id', 'ASC']], + }); + return rows.map(serialize); +} + +module.exports = { listByProduct, listOnSaleByProduct, create, update, setOnSale, softDelete }; diff --git a/backend/src/modules/user/controllers/addressController.js b/backend/src/modules/user/controllers/addressController.js index 8afffdb755f120a9bd20dcd4e2ec5a4769126e25..59ed984b13aa79f2e15027046f41cf3a2617433c 100644 --- a/backend/src/modules/user/controllers/addressController.js +++ b/backend/src/modules/user/controllers/addressController.js @@ -9,18 +9,18 @@ const { success } = require('../../../common/response'); const { validate } = require('../../../common/validator'); const schema = Joi.object({ - receiver: Joi.string().required(), - phone: Joi.string().pattern(/^1[3-9]\d{9}$/).required(), + receiverName: Joi.string().required(), + receiverPhone: Joi.string().pattern(/^1[3-9]\d{9}$/).required(), province: Joi.string().required(), city: Joi.string().required(), district: Joi.string().required(), - detail: Joi.string().required(), - isDefault: Joi.number().valid(0, 1).optional(), + detailAddress: Joi.string().required(), + isDefault: Joi.boolean().optional(), }); async function list(req, res, next) { try { - const data = await addressService.list(req.user.userId); + const data = await addressService.findAllByUser(req.user.userId); res.json(success(data)); } catch (e) { next(e); } } @@ -34,23 +34,30 @@ async function create(req, res, next) { async function update(req, res, next) { try { - await addressService.update(req.user.userId, req.params.id, req.body); + await addressService.update(req.params.id, req.user.userId, req.body); res.json(success(null, '更新成功')); } catch (e) { next(e); } } async function remove(req, res, next) { try { - const n = await addressService.remove(req.user.userId, req.params.id); + const n = await addressService.delete(req.params.id, req.user.userId); res.json(success(null, n ? '已删除' : '无需删除')); } catch (e) { next(e); } } async function setDefault(req, res, next) { try { - await addressService.setDefault(req.user.userId, req.params.id); + await addressService.setDefault(req.params.id, req.user.userId); res.json(success(null, '已设为默认地址')); } catch (e) { next(e); } } -module.exports = { list, create, update, remove, setDefault, schema }; +async function cancelDefault(req, res, next) { + try { + await addressService.cancelDefault(req.params.id, req.user.userId); + res.json(success(null, '已取消默认地址')); + } catch (e) { next(e); } +} + +module.exports = { list, create, update, remove, setDefault, cancelDefault, schema }; diff --git a/backend/src/modules/user/controllers/adminUserController.js b/backend/src/modules/user/controllers/adminUserController.js index ff14aa2c45be58ae603ed25d0f54fd0165ab6dd2..273ca92096c4a222a82ccbf91479d82e91492e1b 100644 --- a/backend/src/modules/user/controllers/adminUserController.js +++ b/backend/src/modules/user/controllers/adminUserController.js @@ -9,32 +9,51 @@ const authService = require('../services/authService'); const { success } = require('../../../common/response'); const { validate } = require('../../../common/validator'); const { paginated, parsePagination } = require('../../../common/pagination'); +const { ForbiddenError } = require('../../../common/exception'); const adminLoginSchema = Joi.object({ username: Joi.string().required(), password: Joi.string().required(), }); +const statusSchema = Joi.object({ + status: Joi.number().valid(0, 1).required(), +}); + +/** 后台登录 */ async function adminLogin(req, res, next) { try { const data = await authService.adminLogin(req.body); res.json(success(data, '登录成功')); - } catch (e) { next(e); } + } catch (e) { + next(e); + } } +/** GET /api/admin/users — 分页查询用户列表,支持统一 keyword 模糊搜索(匹配 username OR phone) */ async function list(req, res, next) { try { + // 校验 role===1 + if (req.user.role !== 'admin') throw new ForbiddenError('无权限'); + const { page, pageSize, offset, limit } = parsePagination(req.query, { defaultPageSize: 20 }); - const { count, rows } = await userService.listUsers({ keyword: req.query.keyword, page, pageSize, offset, limit }); + const keyword = (req.query.keyword || '').trim(); + const { count, rows } = await userService.listUsers({ keyword, page, pageSize, offset, limit }); res.json(paginated(rows, count, page, pageSize)); - } catch (e) { next(e); } + } catch (e) { + next(e); + } } +/** PUT /api/admin/users/:id/status — 禁用/启用用户 */ async function updateStatus(req, res, next) { try { - await userService.updateStatus(req.params.id, parseInt(req.query.status, 10)); + if (req.user.role !== 'admin') throw new ForbiddenError('无权限'); + await userService.updateStatus(req.params.id, req.query.status); res.json(success(null, '已更新')); - } catch (e) { next(e); } + } catch (e) { + next(e); + } } -module.exports = { adminLogin, list, updateStatus, adminLoginSchema }; +module.exports = { adminLogin, list, updateStatus, adminLoginSchema, statusSchema }; diff --git a/backend/src/modules/user/controllers/authController.js b/backend/src/modules/user/controllers/authController.js index 7a5ea522f586c882dcfbee920a7a2949c1541ee3..a8f4dcb967e988667f72c9c12af451ed3c31f178 100644 --- a/backend/src/modules/user/controllers/authController.js +++ b/backend/src/modules/user/controllers/authController.js @@ -9,10 +9,10 @@ const { success } = require('../../../common/response'); const { validate } = require('../../../common/validator'); const registerSchema = Joi.object({ - username: Joi.string().alphanum().min(4).max(20).required(), + username: Joi.string().min(3).max(20).required(), password: Joi.string().min(6).max(20).required(), - phone: Joi.string().pattern(/^1[3-9]\d{9}$/).optional(), - email: Joi.string().email().optional(), + phone: Joi.string().allow('', null).optional(), + email: Joi.string().allow('', null).optional(), }); const loginSchema = Joi.object({ @@ -29,8 +29,8 @@ async function register(req, res, next) { async function login(req, res, next) { try { - const data = await authService.login(req.body); - res.json(success(data, '登录成功')); + const { token, user } = await authService.login(req.body); + res.json(success({ token, userId: user.id, username: user.username }, '登录成功')); } catch (e) { next(e); } } diff --git a/backend/src/modules/user/controllers/cartController.js b/backend/src/modules/user/controllers/cartController.js index 192cf346611f09032cdeaef3270b50c9b161f1ff..d11cab5c70b8672c634ff988ce4d7967161a0c5d 100644 --- a/backend/src/modules/user/controllers/cartController.js +++ b/backend/src/modules/user/controllers/cartController.js @@ -10,43 +10,135 @@ const { validate } = require('../../../common/validator'); const addSchema = Joi.object({ productId: Joi.number().required(), - qty: Joi.number().integer().min(1).default(1), + quantity: Joi.number().integer().min(1).default(1), + bundleId: Joi.number().integer().optional(), }); -const qtySchema = Joi.object({ quantity: Joi.number().integer().min(1).required() }); -async function add(req, res, next) { +const qtySchema = Joi.object({ + quantity: Joi.number().integer().min(0).required(), +}); + +const selectSchema = Joi.object({ + selected: Joi.boolean().required(), +}); + +const mergeSchema = Joi.object({ + items: Joi.array() + .items( + Joi.object({ + productId: Joi.number().required(), + quantity: Joi.number().integer().min(1).default(1), + selected: Joi.boolean().default(true), + bundleId: Joi.number().integer().optional(), + }) + ) + .required(), +}); + +/** GET /api/cart */ +async function getCart(req, res, next) { try { - const data = await cartService.add(req.user.userId, req.body); - res.json(success(data, '已加入购物车')); - } catch (e) { next(e); } + const data = await cartService.getCart(req.user.userId); + res.json(success(data)); + } catch (e) { + next(e); + } } -async function list(req, res, next) { +/** POST /api/cart */ +async function addItem(req, res, next) { try { - const data = await cartService.list(req.user.userId); - res.json(success(data)); - } catch (e) { next(e); } + const data = await cartService.addItem( + req.user.userId, + req.body.productId, + req.body.quantity, + req.body.bundleId + ); + res.json(success(data, '已加入购物车')); + } catch (e) { + next(e); + } } +/** PUT /api/cart/:id */ async function updateQty(req, res, next) { try { - await cartService.updateQty(req.user.userId, req.params.id, req.body.quantity); + await cartService.updateQty( + parseInt(req.params.id), + req.user.userId, + req.body.quantity + ); res.json(success(null, '已更新')); - } catch (e) { next(e); } + } catch (e) { + next(e); + } } -async function remove(req, res, next) { +/** DELETE /api/cart/:id */ +async function deleteItem(req, res, next) { try { - await cartService.remove(req.user.userId, req.params.id); + await cartService.deleteItem(parseInt(req.params.id), req.user.userId); res.json(success(null, '已删除')); - } catch (e) { next(e); } + } catch (e) { + next(e); + } +} + +/** PATCH /api/cart/:id/select */ +async function toggleSelect(req, res, next) { + try { + await cartService.toggleSelect( + parseInt(req.params.id), + req.user.userId, + req.body.selected + ); + res.json(success(null, '已更新')); + } catch (e) { + next(e); + } +} + +/** DELETE /api/cart/clear */ +async function clear(req, res, next) { + try { + await cartService.clear(req.user.userId); + res.json(success(null, '已清空')); + } catch (e) { + next(e); + } +} + +/** POST /api/cart/merge */ +async function merge(req, res, next) { + try { + await cartService.merge(req.user.userId, req.body.items); + res.json(success(null, '已合并')); + } catch (e) { + next(e); + } } -async function checkout(req, res, next) { +/** GET /api/cart/checkout — S04 结算快照 */ +async function getCheckout(req, res, next) { try { - const data = await cartService.checkout(req.user.userId, req.query.ids); + const data = await cartService.getCheckout(req.user.userId); res.json(success(data)); - } catch (e) { next(e); } + } catch (e) { + next(e); + } } -module.exports = { add, list, updateQty, remove, checkout, addSchema, qtySchema }; +module.exports = { + getCart, + addItem, + updateQty, + deleteItem, + toggleSelect, + clear, + merge, + getCheckout, + addSchema, + qtySchema, + selectSchema, + mergeSchema, +}; diff --git a/backend/src/modules/user/controllers/favoriteController.js b/backend/src/modules/user/controllers/favoriteController.js new file mode 100644 index 0000000000000000000000000000000000000000..4b2160a9e45fc2f4aea507eefde22516da757d67 --- /dev/null +++ b/backend/src/modules/user/controllers/favoriteController.js @@ -0,0 +1,45 @@ +/** + * 收藏 controller + */ +'use strict'; + +const Joi = require('joi'); +const favoriteService = require('../services/favoriteService'); +const { success } = require('../../../common/response'); +const { validate } = require('../../../common/validator'); + +const addSchema = Joi.object({ + productId: Joi.number().required(), +}); + +/** POST /api/favorites */ +async function add(req, res, next) { + try { + const data = await favoriteService.add(req.user.userId, req.body.productId); + res.json(success(data, '收藏成功')); + } catch (e) { + next(e); + } +} + +/** DELETE /api/favorites/:id */ +async function remove(req, res, next) { + try { + await favoriteService.remove(parseInt(req.params.id), req.user.userId); + res.json(success(null, '已取消收藏')); + } catch (e) { + next(e); + } +} + +/** GET /api/favorites */ +async function list(req, res, next) { + try { + const data = await favoriteService.list(req.user.userId); + res.json(success(data)); + } catch (e) { + next(e); + } +} + +module.exports = { add, remove, list, addSchema }; diff --git a/backend/src/modules/user/controllers/historyController.js b/backend/src/modules/user/controllers/historyController.js new file mode 100644 index 0000000000000000000000000000000000000000..fbc7b5ea79a7ef23965454203dc43d1263272f47 --- /dev/null +++ b/backend/src/modules/user/controllers/historyController.js @@ -0,0 +1,42 @@ +/** + * 浏览历史 controller + */ +'use strict'; + +const Joi = require('joi'); +const browseHistoryService = require('../services/browseHistoryService'); +const { success } = require('../../../common/response'); +const { validate } = require('../../../common/validator'); + +const addSchema = Joi.object({ + productId: Joi.number().required(), +}); + +/** POST /api/browse-history — 记录浏览 */ +async function add(req, res, next) { + try { + const { productId } = await addSchema.validateAsync(req.body); + await browseHistoryService.addOrUpdate(req.user.userId, productId); + res.json(success(null, '浏览记录已更新')); + } catch (e) { next(e); } +} + +/** GET /api/browse-history — 最近20条 */ +async function list(req, res, next) { + try { + const data = await browseHistoryService.list(req.user.userId); + // 取最近20条 + const limited = Array.isArray(data) ? data.slice(0, 20) : []; + res.json(success(limited)); + } catch (e) { next(e); } +} + +/** DELETE /api/browse-history — 清空当前用户的全部浏览历史 */ +async function clearAll(req, res, next) { + try { + await browseHistoryService.clearAll(req.user.userId); + res.json(success(null, '已清空浏览历史')); + } catch (e) { next(e); } +} + +module.exports = { add, list, clearAll }; diff --git a/backend/src/modules/user/controllers/profileController.js b/backend/src/modules/user/controllers/profileController.js index 24977b54e84e52ac1745314af6af864ed01ca338..cc26af687193660ad4081c70f249ac35c2cd1c65 100644 --- a/backend/src/modules/user/controllers/profileController.js +++ b/backend/src/modules/user/controllers/profileController.js @@ -11,26 +11,33 @@ const { validate } = require('../../../common/validator'); const updateSchema = Joi.object({ phone: Joi.string().pattern(/^1[3-9]\d{9}$/).optional(), email: Joi.string().email().optional(), - avatar: Joi.string().uri().optional(), }).min(1); -async function get(req, res, next) { +async function getProfile(req, res, next) { try { const data = await userService.getMe(req.user.userId); res.json(success(data)); } catch (e) { next(e); } } -async function update(req, res, next) { +async function updateProfile(req, res, next) { try { await userService.updateMe(req.user.userId, req.body); res.json(success(null, '更新成功')); } catch (e) { next(e); } } -async function changePassword(_req, res) { - // TODO(D · 余泽): 实现修改密码(接 M01-05) - res.json(success(null, 'TODO')); +const passwordSchema = Joi.object({ + oldPassword: Joi.string().required(), + newPassword: Joi.string().min(6).max(20).required(), +}); + +async function updatePassword(req, res, next) { + try { + const { oldPassword, newPassword } = await passwordSchema.validateAsync(req.body); + await userService.changePassword(req.user.userId, oldPassword, newPassword); + res.json(success(null, '密码修改成功')); + } catch (e) { next(e); } } -module.exports = { get, update, changePassword, updateSchema }; +module.exports = { getProfile, updateProfile, updatePassword, updateSchema, passwordSchema }; diff --git a/backend/src/modules/user/models/Address.js b/backend/src/modules/user/models/Address.js index e8f2e013bd6a73351b65950b6e5c484773e76909..0b14ee167831f79c4d232ca51b20fa1536e237f0 100644 --- a/backend/src/modules/user/models/Address.js +++ b/backend/src/modules/user/models/Address.js @@ -11,15 +11,20 @@ const Address = sequelize.define( { id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true }, userId: { type: DataTypes.BIGINT, allowNull: false, field: 'user_id' }, - receiver: { type: DataTypes.STRING(50), allowNull: false }, - phone: { type: DataTypes.STRING(20), allowNull: false }, + receiverName: { type: DataTypes.STRING(50), allowNull: false, field: 'receiver' }, + receiverPhone: { type: DataTypes.STRING(20), allowNull: false, field: 'phone' }, province: { type: DataTypes.STRING(50), allowNull: false }, city: { type: DataTypes.STRING(50), allowNull: false }, district: { type: DataTypes.STRING(50), allowNull: false }, - detail: { type: DataTypes.STRING(255), allowNull: false }, - isDefault: { type: DataTypes.TINYINT, allowNull: false, defaultValue: 0, field: 'is_default' }, + detailAddress: { type: DataTypes.STRING(255), allowNull: false, field: 'detail' }, + isDefault: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false, field: 'is_default' }, + createTime: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW, field: 'created_at' }, + updateTime: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW, field: 'updated_at' } }, - { tableName: 'address' } + { + tableName: 'address', + timestamps: false + } ); module.exports = Address; diff --git a/backend/src/modules/user/models/BrowseHistory.js b/backend/src/modules/user/models/BrowseHistory.js new file mode 100644 index 0000000000000000000000000000000000000000..86cb7091400dd39db4f7333d57e792ce776de6de --- /dev/null +++ b/backend/src/modules/user/models/BrowseHistory.js @@ -0,0 +1,23 @@ +/** + * 浏览历史模型 + */ +'use strict'; + +const { DataTypes } = require('sequelize'); +const { sequelize } = require('../../../config/db'); + +const BrowseHistory = sequelize.define('BrowseHistory', { + id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true }, + userId: { type: DataTypes.BIGINT, allowNull: false, field: 'user_id' }, + productId: { type: DataTypes.BIGINT, allowNull: false, field: 'product_id' }, + browseTime: { type: DataTypes.DATE, allowNull: false, field: 'browsed_at', defaultValue: DataTypes.NOW }, +}, { + tableName: 'browse_history', + timestamps: false, + indexes: [ + { unique: true, fields: ['user_id', 'product_id'] }, + { fields: ['user_id'] }, + ], +}); + +module.exports = BrowseHistory; diff --git a/backend/src/modules/user/models/CartItem.js b/backend/src/modules/user/models/CartItem.js index 2ad6b656e7f1144c8299a6d24adab0bc7f548a05..3865f43d5286d11ad01c2c520fc7afb10dbb4a86 100644 --- a/backend/src/modules/user/models/CartItem.js +++ b/backend/src/modules/user/models/CartItem.js @@ -1,4 +1,3 @@ -/** 购物车项模型 */ 'use strict'; const { DataTypes } = require('sequelize'); @@ -8,11 +7,20 @@ const CartItem = sequelize.define('CartItem', { id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true }, userId: { type: DataTypes.BIGINT, allowNull: false, field: 'user_id' }, productId: { type: DataTypes.BIGINT, allowNull: false, field: 'product_id' }, + bundleId: { type: DataTypes.BIGINT, allowNull: true, field: 'bundle_id', comment: 'NULL=base;非空=product_bundle.id' }, quantity: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 1 }, selected: { type: DataTypes.TINYINT, allowNull: false, defaultValue: 1 }, + createTime: { type: DataTypes.DATE, allowNull: false, field: 'created_at', defaultValue: DataTypes.NOW }, + updateTime: { type: DataTypes.DATE, allowNull: false, field: 'updated_at', defaultValue: DataTypes.NOW }, }, { tableName: 'cart_item', - timestamps: false // ← 添加这一行 + timestamps: false, + indexes: [ + // Phase 2 改动:唯一约束包含 bundle_id,允许同一 user 同时有 base 行 + 多 bundle 行 + // MySQL NULL 不参与唯一约束 + { unique: true, fields: ['user_id', 'product_id', 'bundle_id'] }, + { fields: ['bundle_id'] }, + ], }); -module.exports = CartItem; \ No newline at end of file +module.exports = CartItem; diff --git a/backend/src/modules/user/models/Favorite.js b/backend/src/modules/user/models/Favorite.js new file mode 100644 index 0000000000000000000000000000000000000000..2b318abfb7a6d0fcbdee7fa1be534072c5ef4245 --- /dev/null +++ b/backend/src/modules/user/models/Favorite.js @@ -0,0 +1,23 @@ +/** + * 收藏模型 + */ +'use strict'; + +const { DataTypes } = require('sequelize'); +const { sequelize } = require('../../../config/db'); + +const Favorite = sequelize.define('Favorite', { + id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true }, + userId: { type: DataTypes.BIGINT, allowNull: false, field: 'user_id' }, + productId: { type: DataTypes.BIGINT, allowNull: false, field: 'product_id' }, + createTime: { type: DataTypes.DATE, allowNull: false, field: 'created_at', defaultValue: DataTypes.NOW }, +}, { + tableName: 'favorite', + timestamps: false, + indexes: [ + { unique: true, fields: ['user_id', 'product_id'] }, + { fields: ['user_id'] }, + ], +}); + +module.exports = Favorite; diff --git a/backend/src/modules/user/models/User.js b/backend/src/modules/user/models/User.js index 1ede39b43ba3c06339f07f00d39e943d9b00a12e..3334926d047043dad05abe45bd4ad71573210b44 100644 --- a/backend/src/modules/user/models/User.js +++ b/backend/src/modules/user/models/User.js @@ -15,13 +15,14 @@ const User = sequelize.define( password: { type: DataTypes.STRING(100), allowNull: false }, // bcrypt加密结果 phone: { type: DataTypes.STRING(20), allowNull: true }, email: { type: DataTypes.STRING(100), allowNull: true }, - avatar: { type: DataTypes.STRING(255), allowNull: true }, role: { type: DataTypes.TINYINT, allowNull: false, defaultValue: 0, comment: '0=买家 1=管理员' }, - status: { type: DataTypes.TINYINT, allowNull: false, defaultValue: 1, comment: '1=正常 0=禁用' } + status: { type: DataTypes.TINYINT, allowNull: false, defaultValue: 1, comment: '1=正常 0=禁用' }, + createTime: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW, field: 'created_at' }, + updateTime: { type: DataTypes.DATE, allowNull: false, defaultValue: DataTypes.NOW, field: 'updated_at' } }, { tableName: 'user', - timestamps: false, // ← 关键!关闭 createdAt/updatedAt + timestamps: false, // ← 关键!关闭 createdAt/updatedAt,使用手动字段 indexes: [ { unique: true, fields: ['username'] } ] diff --git a/backend/src/modules/user/routes/index.js b/backend/src/modules/user/routes/index.js index f3c1805bf8de3732d373c0e65951f00519dc4e47..dc6610263b25b178185860a239be384f26f11db0 100644 --- a/backend/src/modules/user/routes/index.js +++ b/backend/src/modules/user/routes/index.js @@ -11,6 +11,8 @@ const authCtl = require('../controllers/authController'); const profileCtl = require('../controllers/profileController'); const addressCtl = require('../controllers/addressController'); const cartCtl = require('../controllers/cartController'); +const favoriteCtl = require('../controllers/favoriteController'); +const historyCtl = require('../controllers/historyController'); const adminUserCtl = require('../controllers/adminUserController'); // /api/auth/* @@ -19,24 +21,38 @@ router.post('/auth/login', validate(authCtl.loginSchema), authCtl.login); router.post('/auth/logout', authMiddleware, authCtl.logout); router.get('/auth/me', authMiddleware, authCtl.me); -// /api/users/* (个人信息) -router.get('/users/me', authMiddleware, profileCtl.get); -router.put('/users/me', authMiddleware, validate(profileCtl.updateSchema), profileCtl.update); -router.put('/users/me/password', authMiddleware, profileCtl.changePassword); +// /api/user/* (个人信息) +router.get('/user/profile', authMiddleware, profileCtl.getProfile); +router.put('/user/profile', authMiddleware, validate(profileCtl.updateSchema), profileCtl.updateProfile); +router.put('/user/password', authMiddleware, validate(profileCtl.passwordSchema), profileCtl.updatePassword); -// /api/addresses/* (地址) +// /api/address/* (地址) router.get('/addresses', authMiddleware, addressCtl.list); -router.post('/addresses', authMiddleware, validate(addressCtl.schema), addressCtl.create); -router.put('/addresses/:id', authMiddleware, validate(addressCtl.schema), addressCtl.update); -router.delete('/addresses/:id', authMiddleware, addressCtl.remove); -router.post('/addresses/:id/default', authMiddleware, addressCtl.setDefault); +router.post('/address', authMiddleware, validate(addressCtl.schema), addressCtl.create); +router.put('/address/:id', authMiddleware, validate(addressCtl.schema), addressCtl.update); +router.delete('/address/:id', authMiddleware, addressCtl.remove); +router.patch('/address/:id/default', authMiddleware, addressCtl.setDefault); +router.patch('/address/:id/cancel-default', authMiddleware, addressCtl.cancelDefault); // /api/cart/* (购物车 + S04 结算) -router.post('/cart', authMiddleware, validate(cartCtl.addSchema), cartCtl.add); -router.get('/cart', authMiddleware, cartCtl.list); -router.get('/cart/checkout', authMiddleware, cartCtl.checkout); // S04 接缝 +router.get('/cart', authMiddleware, cartCtl.getCart); +router.post('/cart', authMiddleware, validate(cartCtl.addSchema), cartCtl.addItem); +router.post('/cart/merge', authMiddleware, validate(cartCtl.mergeSchema), cartCtl.merge); +router.get('/cart/checkout', authMiddleware, cartCtl.getCheckout); // S04 接缝 router.put('/cart/:id', authMiddleware, validate(cartCtl.qtySchema), cartCtl.updateQty); -router.delete('/cart/:id', authMiddleware, cartCtl.remove); +router.patch('/cart/:id/select', authMiddleware, validate(cartCtl.selectSchema), cartCtl.toggleSelect); +router.delete('/cart/clear', authMiddleware, cartCtl.clear); +router.delete('/cart/:id', authMiddleware, cartCtl.deleteItem); + +// /api/favorites/* (收藏) +router.get('/favorites', authMiddleware, favoriteCtl.list); +router.post('/favorites', authMiddleware, validate(favoriteCtl.addSchema), favoriteCtl.add); +router.delete('/favorites/:id', authMiddleware, favoriteCtl.remove); + +// /api/browse-history/* (浏览历史) +router.post('/browse-history', authMiddleware, historyCtl.add); +router.get('/browse-history', authMiddleware, historyCtl.list); +router.delete('/browse-history', authMiddleware, historyCtl.clearAll); // /api/admin/auth/* (后台登录) router.post('/admin/auth/login', validate(adminUserCtl.adminLoginSchema), adminUserCtl.adminLogin); diff --git a/backend/src/modules/user/services/addressService.js b/backend/src/modules/user/services/addressService.js index 99ef48c6d4b10d75e61cd6304ebb1063afacad78..b7f9ff52d1b425fe4d9d96bfc678f9b611147e92 100644 --- a/backend/src/modules/user/services/addressService.js +++ b/backend/src/modules/user/services/addressService.js @@ -8,51 +8,57 @@ const { sequelize } = require('../../../config/db'); const Address = require('../models/Address'); const { ParamError } = require('../../../common/exception'); -async function list(userId) { +async function findAllByUser(userId) { return Address.findAll({ where: { userId }, - order: [['is_default', 'DESC'], ['id', 'DESC']], + order: [['isDefault', 'DESC'], ['id', 'DESC']], }); } -async function create(userId, body) { +async function create(userId, data) { const t = await sequelize.transaction(); try { - if (body.isDefault === 1) { - await Address.update({ isDefault: 0 }, { where: { userId } }, { transaction: t }); + if (data.isDefault === true) { + await Address.update({ isDefault: false }, { where: { userId } }, { transaction: t }); } - const created = await Address.create({ ...body, userId }, { transaction: t }); + const created = await Address.create({ ...data, userId }, { transaction: t }); await t.commit(); return created; } catch (e) { await t.rollback(); throw e; } } -async function update(userId, id, body) { +async function update(id, userId, data) { const t = await sequelize.transaction(); try { const row = await Address.findOne({ where: { id, userId }, transaction: t }); if (!row) throw new ParamError('地址不存在'); - if (body.isDefault === 1) { - await Address.update({ isDefault: 0 }, { where: { userId, id: { [Op.ne]: id } } }, { transaction: t }); + if (data.isDefault === true) { + await Address.update({ isDefault: false }, { where: { userId, id: { [Op.ne]: id } } }, { transaction: t }); } - await row.update(body, { transaction: t }); + await row.update(data, { transaction: t }); await t.commit(); } catch (e) { await t.rollback(); throw e; } } -async function remove(userId, id) { +async function remove(id, userId) { return Address.destroy({ where: { id, userId } }); } -async function setDefault(userId, id) { +async function setDefault(id, userId) { const t = await sequelize.transaction(); try { const row = await Address.findOne({ where: { id, userId }, transaction: t }); if (!row) throw new ParamError('地址不存在'); - await Address.update({ isDefault: 0 }, { where: { userId } }, { transaction: t }); - await row.update({ isDefault: 1 }, { transaction: t }); + await Address.update({ isDefault: false }, { where: { userId } }, { transaction: t }); + await row.update({ isDefault: true }, { transaction: t }); await t.commit(); } catch (e) { await t.rollback(); throw e; } } -module.exports = { list, create, update, remove, setDefault }; +async function cancelDefault(id, userId) { + const row = await Address.findOne({ where: { id, userId } }); + if (!row) throw new ParamError('地址不存在'); + await row.update({ isDefault: false }); +} + +module.exports = { findAllByUser, create, update, delete: remove, setDefault, cancelDefault }; diff --git a/backend/src/modules/user/services/browseHistoryService.js b/backend/src/modules/user/services/browseHistoryService.js new file mode 100644 index 0000000000000000000000000000000000000000..4c8af04a6190bd0daada7e583fc5e10d5e777a2b --- /dev/null +++ b/backend/src/modules/user/services/browseHistoryService.js @@ -0,0 +1,48 @@ +/** + * 浏览历史 service + */ +'use strict'; + +require('../../../models'); +const BrowseHistory = require('../models/BrowseHistory'); +const Product = require('../../product/models/Product'); +const { ParamError } = require('../../../common/exception'); +const { sequelize } = require('../../../config/db'); + +/** 记录浏览(已存在则更新浏览时间,不存在则新增) */ +async function addOrUpdate(userId, productId) { + const product = await Product.findByPk(productId); + if (!product || product.onSale !== 1) throw new ParamError('商品不存在或已下架'); + + const [row, created] = await BrowseHistory.findOrCreate({ + where: { userId, productId }, + defaults: { userId, productId, browseTime: new Date() }, + }); + if (!created) { + await row.update({ browseTime: new Date() }); + } + return row; +} + +/** 浏览历史列表 */ +async function list(userId) { + return BrowseHistory.findAll({ + where: { userId }, + include: [{ model: Product, as: 'product' }], + order: [[sequelize.literal('`browsed_at`'), 'DESC']], + }); +} + +/** 清空全部浏览历史 */ +async function clearAll(userId) { + await BrowseHistory.destroy({ where: { userId } }); +} + +/** 删除单条 */ +async function remove(id, userId) { + const row = await BrowseHistory.findOne({ where: { id, userId } }); + if (!row) throw new ParamError('记录不存在'); + await row.destroy(); +} + +module.exports = { addOrUpdate, list, clearAll, remove }; diff --git a/backend/src/modules/user/services/cartService.js b/backend/src/modules/user/services/cartService.js index bfa9a90a418714f833b132a7e56873285055aa46..a05ccb4936739d633698981c680d8c908d9f70b0 100644 --- a/backend/src/modules/user/services/cartService.js +++ b/backend/src/modules/user/services/cartService.js @@ -4,67 +4,201 @@ 'use strict'; const { Op } = require('sequelize'); -// 关键:手动加载 models/index.js 以确保 Product ↔ CartItem 关联已建立 -// (独立 require model 时 Sequelize 不知道它们之间的关联) +const { sequelize } = require('../../../config/db'); require('../../../models'); const CartItem = require('../models/CartItem'); const Product = require('../../product/models/Product'); +const ProductBundle = require('../../product/models/ProductBundle'); const { ParamError } = require('../../../common/exception'); -async function add(userId, { productId, qty }) { - const p = await Product.findByPk(productId); - if (!p || p.onSale !== 1) throw new ParamError('商品不存在或已下架'); - if (p.stock < qty) throw new ParamError('库存不足'); +/** 查询该用户所有购物车项 */ +async function getCart(userId) { + const items = await CartItem.findAll({ + where: { userId }, + include: [{ model: Product, as: 'product' }], + }); + if (!items.length) return []; - const [row] = await CartItem.findOrCreate({ - where: { userId, productId }, - defaults: { quantity: qty }, + const bundleIds = items.map((it) => it.bundleId).filter((x) => x != null); + const bundles = bundleIds.length + ? await ProductBundle.findAll({ where: { id: { [Op.in]: bundleIds } } }) + : []; + const bundleMap = new Map(bundles.map((b) => [String(b.id), b])); + + // I1 修复:与 getCheckout 对齐,校验 bundle 上下架/软删,避免静默回退到 base 价格 + const plainItems = items.map((it) => it.get({ plain: true })); + for (const plain of plainItems) { + if (plain.bundleId != null) { + const b = bundleMap.get(String(plain.bundleId)); + if (!b || b.onSale !== 1 || b.deleted !== 0) { + throw new ParamError('以下规格已下架:' + plain.bundleId); + } + } + } + + return plainItems.map((plain) => { + const b = plain.bundleId != null ? bundleMap.get(String(plain.bundleId)) : null; + return { + ...plain, + bundleId: plain.bundleId, + specName: b ? b.specName : null, + image: b?.image || plain.product?.image, + price: b ? (Number(b.price) * Number(b.quantity)).toFixed(2) : plain.product?.price, + stock: b ? b.stock : plain.product?.stock, + }; }); - if (row.quantity !== qty) await row.update({ quantity: row.quantity + qty }); - return row; } -async function list(userId) { - return CartItem.findAll({ - where: { userId }, - include: [{ model: Product, as: 'product' }], +/** 添加购物车项,存在则 quantity 累加,否则新增 */ +async function addItem(userId, productId, quantity = 1, bundleId = null) { + const product = await Product.findByPk(productId); + if (!product || product.onSale !== 1) throw new ParamError('商品不存在或已下架'); + + let stock; + if (bundleId != null) { + const bundle = await ProductBundle.findOne({ where: { id: bundleId, productId, deleted: 0, onSale: 1 } }); + if (!bundle) throw new ParamError('规格不存在或已下架'); + stock = bundle.stock; + } else { + stock = product.stock; + } + if (stock < quantity) throw new ParamError('库存不足'); + + const [row, created] = await CartItem.findOrCreate({ + where: { userId, productId, bundleId }, + defaults: { quantity, selected: true }, }); + if (!created) { + await row.update({ quantity: row.quantity + quantity }); + } + return row; +} + +/** 修改数量(需校验归属) */ +async function updateQty(id, userId, quantity) { + const row = await CartItem.findOne({ where: { id, userId } }); + if (!row) throw new ParamError('购物车项不存在'); + if (quantity <= 0) { + await row.destroy(); + } else { + await row.update({ quantity }); + } } -async function updateQty(userId, id, quantity) { +/** 删除单条(需校验归属) */ +async function deleteItem(id, userId) { const row = await CartItem.findOne({ where: { id, userId } }); if (!row) throw new ParamError('购物车项不存在'); - await row.update({ quantity }); + await row.destroy(); } -async function remove(userId, id) { - await CartItem.destroy({ where: { id, userId } }); +/** 勾选/取消(需校验归属) */ +async function toggleSelect(id, userId, selected) { + const row = await CartItem.findOne({ where: { id, userId } }); + if (!row) throw new ParamError('购物车项不存在'); + await row.update({ selected: !!selected }); } -/** S04 购物车结算快照(接缝) */ -async function checkout(userId, idsParam) { - const ids = (idsParam || '').toString().split(',').filter(Boolean).map(Number); - const where = { userId }; - if (ids.length) where.id = { [Op.in]: ids }; +/** 清空该用户所有购物车项 */ +async function clear(userId) { + await CartItem.destroy({ where: { userId } }); +} + +/** 合并购物车(存在则 quantity 累加,不存在则新增,使用事务) */ +async function merge(userId, items) { + if (!Array.isArray(items) || !items.length) return; + + const t = await sequelize.transaction(); + try { + for (const item of items) { + const { productId, quantity = 1, selected = true, bundleId = null } = item; + const product = await Product.findByPk(productId, { transaction: t }); + if (!product || product.onSale !== 1) continue; + if (bundleId != null) { + const bundle = await ProductBundle.findOne({ where: { id: bundleId, productId, deleted: 0, onSale: 1 }, transaction: t }); + if (!bundle) continue; + } + const existing = await CartItem.findOne({ + where: { userId, productId, bundleId }, + transaction: t, + }); + if (existing) { + await existing.update( + { quantity: existing.quantity + quantity }, + { transaction: t } + ); + } else { + await CartItem.create( + { userId, productId, bundleId, quantity, selected }, + { transaction: t } + ); + } + } + await t.commit(); + } catch (err) { + await t.rollback(); + throw err; + } +} + +/** S04 购物车结算快照 */ +async function getCheckout(userId) { const items = await CartItem.findAll({ - where, + where: { userId, selected: true }, include: [{ model: Product, as: 'product' }], }); if (!items.length) throw new ParamError('购物车为空'); - const offShelf = items.filter((it) => !it.product || it.product.onSale !== 1).map((it) => it.productId); - if (offShelf.length) throw new ParamError('以下商品已下架:' + offShelf.join(',')); - - const snap = items.map((it) => ({ - productId: it.productId, - name: it.product.name, - price: it.product.price, - image: it.product.image, - qty: it.quantity, - })); - const total = snap.reduce((sum, x) => sum + Number(x.price) * x.qty, 0).toFixed(2); - return { items: snap, total }; + const bundleIds = items.map((it) => it.bundleId).filter((x) => x != null); + const bundles = bundleIds.length + ? await ProductBundle.findAll({ where: { id: { [Op.in]: bundleIds } } }) + : []; + const bundleMap = new Map(bundles.map((b) => [String(b.id), b])); + + for (const it of items) { + if (!it.product || it.product.onSale !== 1) { + throw new ParamError('以下商品已下架:' + it.productId); + } + if (it.bundleId != null) { + const b = bundleMap.get(String(it.bundleId)); + if (!b || b.onSale !== 1 || b.deleted !== 0) { + throw new ParamError('以下规格已下架:' + it.bundleId); + } + } + } + + const snap = items.map((it) => { + const b = it.bundleId != null ? bundleMap.get(String(it.bundleId)) : null; + const unitPrice = b + ? Number(b.price) * Number(b.quantity) + : Number(it.product.price); + return { + productId: it.productId, + bundleId: it.bundleId, + specName: b ? b.specName : null, + name: it.product.name, + image: b?.image || it.product.image, + price: unitPrice.toFixed(2), + quantity: it.quantity, + stock: b ? b.stock : it.product.stock, + selected: it.selected, + }; + }); + + const totalPrice = snap.reduce((sum, x) => sum + Number(x.price) * x.quantity, 0); + const itemCount = snap.reduce((sum, x) => sum + x.quantity, 0); + + return { items: snap, totalPrice, itemCount }; } -module.exports = { add, list, updateQty, remove, checkout }; +module.exports = { + getCart, + addItem, + updateQty, + deleteItem, + toggleSelect, + clear, + merge, + getCheckout, +}; diff --git a/backend/src/modules/user/services/favoriteService.js b/backend/src/modules/user/services/favoriteService.js new file mode 100644 index 0000000000000000000000000000000000000000..abb829e9d251c59d7996759bdef6a590977c4d8d --- /dev/null +++ b/backend/src/modules/user/services/favoriteService.js @@ -0,0 +1,42 @@ +/** + * 收藏 service + */ +'use strict'; + +const { Op } = require('sequelize'); +require('../../../models'); +const Favorite = require('../models/Favorite'); +const Product = require('../../product/models/Product'); +const { ParamError } = require('../../../common/exception'); + +/** 收藏商品 */ +async function add(userId, productId) { + const product = await Product.findByPk(productId); + if (!product || product.onSale !== 1) throw new ParamError('商品不存在或已下架'); + + const [row, created] = await Favorite.findOrCreate({ + where: { userId, productId }, + defaults: { userId, productId }, + }); + if (!created) throw new ParamError('已收藏'); + + return row; +} + +/** 取消收藏 */ +async function remove(id, userId) { + const row = await Favorite.findOne({ where: { id, userId } }); + if (!row) throw new ParamError('收藏记录不存在'); + await row.destroy(); +} + +/** 收藏列表 */ +async function list(userId) { + return Favorite.findAll({ + where: { userId }, + include: [{ model: Product, as: 'product' }], + order: [['created_at', 'DESC']], + }); +} + +module.exports = { add, remove, list }; diff --git a/backend/src/modules/user/services/userService.js b/backend/src/modules/user/services/userService.js index 7f97211942d269ad7d15f8b67484301cea91540e..f8bf4459ff87a122a2deab771c4cb4a3aae96392 100644 --- a/backend/src/modules/user/services/userService.js +++ b/backend/src/modules/user/services/userService.js @@ -4,27 +4,43 @@ 'use strict'; const { Op } = require('sequelize'); +const bcrypt = require('bcryptjs'); const { sequelize } = require('../../../config/db'); const User = require('../models/User'); -const { ParamError } = require('../../../common/exception'); +const { ParamError, UnauthorizedError } = require('../../../common/exception'); async function getMe(userId) { - const u = await User.findByPk(userId, { attributes: ['id', 'username', 'phone', 'email', 'avatar', 'role', 'status'] }); + const u = await User.findByPk(userId, { + attributes: ['id', 'username', 'phone', 'email', 'role', 'status', 'created_at'], + }); if (!u) throw new ParamError('用户不存在'); - return u; + // 转换为前端约定字段:created_at -> createTime + const plain = u.get({ plain: true }); + plain.createTime = plain.created_at; + delete plain.created_at; + return plain; } async function updateMe(userId, body) { await User.update(body, { where: { id: userId } }); } -// 后台 +async function changePassword(userId, oldPassword, newPassword) { + const user = await User.findByPk(userId); + if (!user) throw new ParamError('用户不存在'); + const ok = await bcrypt.compare(oldPassword, user.password); + if (!ok) throw new UnauthorizedError('旧密码错误'); + const hashed = await bcrypt.hash(newPassword, 10); + await User.update({ password: hashed }, { where: { id: userId } }); +} + +// 后台:单一 keyword 同时模糊匹配 username 和 phone(OR 关系) async function listUsers({ keyword, page, pageSize, offset, limit }) { const where = {}; if (keyword) { where[Op.or] = [ { username: { [Op.like]: `%${keyword}%` } }, - { phone: { [Op.like]: `%${keyword}%` } }, + { phone: { [Op.like]: `%${keyword}%` } }, ]; } return User.findAndCountAll({ @@ -36,8 +52,9 @@ async function listUsers({ keyword, page, pageSize, offset, limit }) { } async function updateStatus(userId, status) { - if (![0, 1].includes(status)) throw new Error('status 取值 0 或 1'); - await User.update({ status }, { where: { id: userId } }); + const s = parseInt(status, 10); + if (![0, 1].includes(s)) throw new Error('status 取值 0 或 1'); + await User.update({ status: s }, { where: { id: userId } }); } -module.exports = { getMe, updateMe, listUsers, updateStatus }; +module.exports = { getMe, updateMe, changePassword, listUsers, updateStatus }; diff --git a/backend/tests/bundle.test.js b/backend/tests/bundle.test.js new file mode 100644 index 0000000000000000000000000000000000000000..49bfeee997ab7e3f5f124c72303c0135497025cb --- /dev/null +++ b/backend/tests/bundle.test.js @@ -0,0 +1,335 @@ +/** + * 规格(bundle)集成测试 + * + * 覆盖: + * - 列表 / 新增 / 局部更新 / 上下架 / 软删 + * - 鉴权:非 admin → 403 + * - 重复 (productId, quantity) → 409 + * - 不存在的 product / bundle → 404 + * - Phase 2 前台联动:公共 GET /products/:id/bundles、购物车带 bundleId、 + * S04 结算套装总价、下单扣 product_bundle.stock + order_item 快照 + * + * 副作用:会在 dev DB 真实写入 product_bundle / cart_item / order;测试结束会清理本次建的数据。 + */ +'use strict'; + +const { Op } = require('sequelize'); +const request = require('supertest'); +const { getApp, loginAs, authHeader } = require('./setup'); +const { sequelize } = require('../src/config/db'); +const { redis } = require('../src/config/redis'); +const ProductBundle = require('../src/modules/product/models/ProductBundle'); +const CartItem = require('../src/modules/user/models/CartItem'); + +const app = getApp(); +const req = request(app); + +let buyerToken; +let adminToken; +let buyerAuthH; +let adminAuthH; +let testProductId; // 复用 dev 库中存在的商品(id=29 「九阳破壁机」),如不存在则建 +const createdIds = []; // 本次测试创建的所有 bundle id,便于 afterAll 清理 + +beforeAll(async () => { + buyerToken = await loginAs(req, 'tester', 'buyer123'); + adminToken = await loginAs(req, 'admin', 'admin123'); + buyerAuthH = authHeader(buyerToken); + adminAuthH = authHeader(adminToken); + + // 选/建一个测试用商品 + const list = await req.get('/api/admin/products').set(adminAuthH.headers).query({ pageSize: 1 }); + expect(list.status).toBe(200); + expect(list.body.code).toBe(0); + testProductId = list.body.data?.list?.[0]?.id; + if (!testProductId) throw new Error('dev DB 中无可用商品,无法测试'); + + // 清理该商品下可能残留的 bundle(让测试幂等) + await ProductBundle.update({ deleted: 1 }, { where: { productId: testProductId } }); + // 上一轮跑测试可能留下指向已软删 bundle 的购物车行;留着会让 checkout/下单报"规格已下架" + await CartItem.destroy({ where: { productId: testProductId, bundleId: { [Op.ne]: null } } }); +}); + +afterAll(async () => { + // 清理本次测试创建的数据 + if (createdIds.length) { + // 先删引用这些 bundle 的购物车行,否则会污染后续 order 测试(bundle 已软删 → 下单报错) + await CartItem.destroy({ where: { bundleId: createdIds } }); + await ProductBundle.update({ deleted: 1 }, { where: { id: createdIds } }); + } + await sequelize.close().catch(() => {}); + await redis.quit().catch(() => {}); +}); + +describe('规格 CRUD(product_bundle)', () => { + test('空列表 → 新增 → 再列表能看到', async () => { + const list = await req.get(`/api/admin/products/${testProductId}/bundles`).set(adminAuthH.headers); + expect(list.status).toBe(200); + expect(list.body.code).toBe(0); + expect(Array.isArray(list.body.data)).toBe(true); + + const created = await req + .post(`/api/admin/products/${testProductId}/bundles`) + .set(adminAuthH.headers) + .send({ specName: '5盒装', quantity: 5, price: 3.3, stock: 100 }); + expect(created.status).toBe(200); + expect(created.body.code).toBe(0); + expect(created.body.data.id).toBeGreaterThan(0); + expect(created.body.data.specName).toBe('5盒装'); + expect(created.body.data.price).toBe('3.30'); // DECIMAL → string + expect(created.body.data.quantity).toBe(5); + createdIds.push(created.body.data.id); + + const list2 = await req.get(`/api/admin/products/${testProductId}/bundles`).set(adminAuthH.headers); + expect(list2.body.data.find((b) => b.id === created.body.data.id)).toBeTruthy(); + }); + + test('重复 (productId, quantity) → 409', async () => { + // 先建一个 + const a = await req + .post(`/api/admin/products/${testProductId}/bundles`) + .set(adminAuthH.headers) + .send({ specName: '8盒装', quantity: 8, price: 3.3, stock: 80 }); + expect(a.status).toBe(200); + createdIds.push(a.body.data.id); + + // 再建一个同 quantity + const dup = await req + .post(`/api/admin/products/${testProductId}/bundles`) + .set(adminAuthH.headers) + .send({ specName: '8盒装-b', quantity: 8, price: 3.5, stock: 50 }); + expect(dup.body.code).toBe(409); + }); + + test('参数错误(quantity=0)→ 400', async () => { + const bad = await req + .post(`/api/admin/products/${testProductId}/bundles`) + .set(adminAuthH.headers) + .send({ specName: 'X', quantity: 0, price: 1, stock: 1 }); + expect(bad.body.code).toBe(400); + }); + + test('软删后重建同名规格 → 复活(id 保持不变,字段被覆盖)', async () => { + // 1. 建一个 quantity=77 的规格 + const a = await req + .post(`/api/admin/products/${testProductId}/bundles`) + .set(adminAuthH.headers) + .send({ specName: '临时', quantity: 77, price: 1.5, stock: 10 }); + expect(a.body.code).toBe(0); + const id = a.body.data.id; + createdIds.push(id); + + // 2. 软删 + const del = await req.delete(`/api/admin/bundles/${id}`).set(adminAuthH.headers); + expect(del.body.code).toBe(0); + + // 3. 用新的字段值重建同名规格(quantity=77)→ 应当复活旧行 + const reb = await req + .post(`/api/admin/products/${testProductId}/bundles`) + .set(adminAuthH.headers) + .send({ specName: '复活', quantity: 77, price: 2.0, stock: 20 }); + expect(reb.body.code).toBe(0); + expect(reb.body.data.id).toBe(id); // 关键:id 不变 + expect(reb.body.data.specName).toBe('复活'); + expect(reb.body.data.price).toBe('2.00'); + expect(reb.body.data.stock).toBe(20); + + // 4. 再建一次应当 409(因为现在该 (productId, quantity) 已有活跃行) + const dup = await req + .post(`/api/admin/products/${testProductId}/bundles`) + .set(adminAuthH.headers) + .send({ specName: 'dup', quantity: 77, price: 2.5, stock: 5 }); + expect(dup.body.code).toBe(409); + }); + + test('局部更新(PATCH)', async () => { + const create = await req + .post(`/api/admin/products/${testProductId}/bundles`) + .set(adminAuthH.headers) + .send({ specName: '30盒装', quantity: 30, price: 3.1, stock: 200 }); + createdIds.push(create.body.data.id); + + const upd = await req + .patch(`/api/admin/bundles/${create.body.data.id}`) + .set(adminAuthH.headers) + .send({ price: 2.9, stock: 250 }); + expect(upd.status).toBe(200); + expect(upd.body.code).toBe(0); + expect(upd.body.data.price).toBe('2.90'); + expect(upd.body.data.stock).toBe(250); + expect(upd.body.data.specName).toBe('30盒装'); // 未改字段保留 + }); + + test('上下架切换', async () => { + const create = await req + .post(`/api/admin/products/${testProductId}/bundles`) + .set(adminAuthH.headers) + .send({ specName: '50盒装', quantity: 50, price: 3.0, stock: 100 }); + createdIds.push(create.body.data.id); + + const off = await req + .patch(`/api/admin/bundles/${create.body.data.id}/on-sale`) + .set(adminAuthH.headers) + .query({ onSale: 0 }); + expect(off.body.code).toBe(0); + expect(off.body.data.onSale).toBe(0); + + const on = await req + .patch(`/api/admin/bundles/${create.body.data.id}/on-sale`) + .set(adminAuthH.headers) + .query({ onSale: 1 }); + expect(on.body.data.onSale).toBe(1); + }); + + test('软删 → 列表不再返回', async () => { + const create = await req + .post(`/api/admin/products/${testProductId}/bundles`) + .set(adminAuthH.headers) + .send({ specName: '待删', quantity: 99, price: 1, stock: 1 }); + const id = create.body.data.id; + createdIds.push(id); + + const del = await req.delete(`/api/admin/bundles/${id}`).set(adminAuthH.headers); + expect(del.body.code).toBe(0); + + const list = await req.get(`/api/admin/products/${testProductId}/bundles`).set(adminAuthH.headers); + expect(list.body.data.find((b) => b.id === id)).toBeUndefined(); + }); + + test('非 admin 鉴权失败 → 403', async () => { + const r = await req.get(`/api/admin/products/${testProductId}/bundles`).set(buyerAuthH.headers); + expect(r.body.code).toBe(403); + }); + + test('不存在的 product → 404', async () => { + const r = await req.get('/api/admin/products/999999999/bundles').set(adminAuthH.headers); + expect(r.body.code).toBe(404); + }); + + // ---------- Phase 2:前台联动 ---------- + + test('公共 GET /api/products/:id/bundles(无需鉴权,仅返在售)', async () => { + // 先建一个上架的和一个下架的 + const on = await req.post(`/api/admin/products/${testProductId}/bundles`) + .set(adminAuthH.headers) + .send({ specName: '公开在售', quantity: 100, price: 5.0, stock: 50 }); + createdIds.push(on.body.data.id); + const off = await req.post(`/api/admin/products/${testProductId}/bundles`) + .set(adminAuthH.headers) + .send({ specName: '公开下架', quantity: 101, price: 5.0, stock: 50, onSale: 0 }); + createdIds.push(off.body.data.id); + + // 公开 GET,无 Authorization header + const pub = await req.get(`/api/products/${testProductId}/bundles`); + expect(pub.status).toBe(200); + expect(pub.body.code).toBe(0); + // 不含下架的 + expect(pub.body.data.find((b) => b.id === on.body.data.id)).toBeTruthy(); + expect(pub.body.data.find((b) => b.id === off.body.data.id)).toBeUndefined(); + }); + + test('cart 加 bundle:cart_item.bundle_id 落库', async () => { + const b = await req.post(`/api/admin/products/${testProductId}/bundles`) + .set(adminAuthH.headers) + .send({ specName: '购物车测试', quantity: 200, price: 4.0, stock: 30 }); + const bundleId = b.body.data.id; + createdIds.push(bundleId); + + // 用 buyer 加购物车(带 bundleId) + const add = await req.post('/api/cart').set(buyerAuthH.headers) + .send({ productId: testProductId, quantity: 2, bundleId }); + expect(add.status).toBe(200); + expect(add.body.code).toBe(0); + + // 读 buyer 购物车,验证带 bundleId 的行存在 + const list = await req.get('/api/cart').set(buyerAuthH.headers); + expect(list.status).toBe(200); + const hit = list.body.data.find((x) => Number(x.bundleId) === Number(bundleId)); + expect(hit).toBeTruthy(); + expect(Number(hit.quantity)).toBe(2); + }); + + test('S04 结算:bundle 行返回 specName + 套装总价 = price × quantity', async () => { + const b = await req.post(`/api/admin/products/${testProductId}/bundles`) + .set(adminAuthH.headers) + .send({ specName: '结算测试', quantity: 201, price: 2.5, stock: 10 }); + const bundleId = b.body.data.id; + createdIds.push(bundleId); + + await req.post('/api/cart').set(buyerAuthH.headers) + .send({ productId: testProductId, quantity: 1, bundleId, selected: true }); + + const checkout = await req.get('/api/cart/checkout').set(buyerAuthH.headers); + expect(checkout.status).toBe(200); + const row = checkout.body.data.items.find((x) => Number(x.bundleId) === Number(bundleId)); + expect(row).toBeTruthy(); + expect(row.specName).toBe('结算测试'); + // 套装总价 = 2.5 × 201 = 502.50 + expect(Number(row.price)).toBeCloseTo(2.5 * 201, 2); + }); + + test('下单:bundle 行扣 product_bundle.stock + order_item 写入 bundle_id + spec_name', async () => { + const b = await req.post(`/api/admin/products/${testProductId}/bundles`) + .set(adminAuthH.headers) + .send({ specName: '下单测试', quantity: 202, price: 3.0, stock: 50 }); + const bundleId = b.body.data.id; + createdIds.push(bundleId); + + const beforeStock = b.body.data.stock; + + // buyer 加购物车 + await req.post('/api/cart').set(buyerAuthH.headers) + .send({ productId: testProductId, quantity: 2, bundleId, selected: true }); + + // 取 buyer 购物车 cart item id + const cartList = await req.get('/api/cart').set(buyerAuthH.headers); + const cartItemId = cartList.body.data.find((x) => Number(x.bundleId) === Number(bundleId)).id; + + // 取地址(dev 库中 tester 应已有地址,没有则建一个) + const addrRes = await req.get('/api/addresses').set(buyerAuthH.headers); + let addressId = addrRes.body?.data?.[0]?.id; + if (!addressId) { + const created = await req.post('/api/addresses').set(buyerAuthH.headers).send({ + receiverName: '规格测试收件人', + receiverPhone: '13800001234', + province: '福建省', city: '龙岩市', district: '新罗区', + detailAddress: '测试路 1 号', + }); + expect(created.body.code).toBe(0); + addressId = created.body.data.id; + } + + // 下单 + const create = await req.post('/api/orders').set(buyerAuthH.headers) + .send({ addressId, cartItemIds: [cartItemId] }); + expect(create.status).toBe(200); + expect(create.body.code).toBe(0); + const orderId = create.body.data.id; + + // 查订单详情,验证 order_item 有 bundle_id + spec_name + const detail = await req.get(`/api/orders/${orderId}`).set(buyerAuthH.headers); + const item = detail.body.data.items[0]; + expect(Number(item.bundleId)).toBe(Number(bundleId)); + expect(item.specName).toBe('下单测试'); + // price 快照 = 3.0 × 202 = 606 + expect(Number(item.price)).toBeCloseTo(3.0 * 202, 2); + expect(item.quantity).toBe(2); // 2 套 + expect(Number(item.subtotal)).toBeCloseTo(3.0 * 202 * 2, 2); + + // 查 product_bundle.stock 应被扣了 2 + const updated = await req.get(`/api/admin/products/${testProductId}/bundles`) + .set(adminAuthH.headers); + const after = updated.body.data.find((x) => x.id === bundleId); + expect(after.stock).toBe(beforeStock - 2); + + // I3 新增断言:stock_log.bundle_id 已落库(非 bundle 路径为 NULL) + const [rows] = await sequelize.query( + 'SELECT biz_no, product_id, bundle_id, delta FROM stock_log WHERE biz_no LIKE ?', + { replacements: [`%${create.body.data.orderNo}%`] } + ); + expect(rows.length).toBeGreaterThan(0); + const bundleRow = rows.find((r) => Number(r.bundle_id) === Number(bundleId)); + expect(bundleRow).toBeTruthy(); + expect(Number(bundleRow.delta)).toBe(-2); + }); +}); diff --git a/backend/tests/bundleService.test.js b/backend/tests/bundleService.test.js new file mode 100644 index 0000000000000000000000000000000000000000..82cbf6020aa911664fa8f9e7954e4803576b76ef --- /dev/null +++ b/backend/tests/bundleService.test.js @@ -0,0 +1,96 @@ +/** + * 单元测试:bundleService.create 的并发竞态防护(I-3 修复) + * + * 思路: + * - 用 jest.mock 替换 Product / ProductBundle / sequelize + * - 模拟"两个并发请求同时通过 active-check"的极端场景: + * 第一个 findOne(active) 返回 null(无活跃行), + * ProductBundle.create 抛 SequelizeUniqueConstraintError(race 情形下唯一键兜底), + * 验证 service.create 把这个错误映射为 ConflictError 而非 500。 + * + * 此测试不依赖 dev DB,仅验证服务层的错误映射逻辑。 + */ +'use strict'; + +// 1) 构造一个最小的 fake sequelize.transaction:直接执行回调,模拟事务同步路径 +// 变量名以 mock 开头以便 jest.mock 工厂函数引用(jest hoist 限制) +const mockTx = { LOCK: { UPDATE: 'UPDATE' } }; +const mockTransaction = jest.fn(async (cb) => cb(mockTx)); + +// 2) 替换 db 模块,返回 fake sequelize +jest.mock('../src/config/db', () => ({ + sequelize: { transaction: mockTransaction }, +})); + +// 3) 替换 Product 模型:始终返回"商品存在" +jest.mock('../src/modules/product/models/Product', () => ({ + findOne: jest.fn(async () => ({ id: 1 })), +})); + +// 4) 替换 ProductBundle 模型 +jest.mock('../src/modules/product/models/ProductBundle', () => { + const fn = jest.fn(); + fn.findOne = jest.fn(); + fn.create = jest.fn(); + return fn; +}); + +const { ConflictError } = require('../src/common/exception'); +const bundleService = require('../src/modules/product/services/bundleService'); +const ProductBundle = require('../src/modules/product/models/ProductBundle'); + +beforeEach(() => { + mockTransaction.mockClear(); + ProductBundle.findOne.mockReset(); + ProductBundle.create.mockReset(); +}); + +describe('bundleService.create 并发竞态防护', () => { + test('ProductBundle.create 抛 SequelizeUniqueConstraintError → 映射为 ConflictError', async () => { + // active-check: 返回 null(没有活跃行) + // tombstone-check: 返回 null(也没有软删行) + ProductBundle.findOne + .mockResolvedValueOnce(null) // active + .mockResolvedValueOnce(null); // tombstone + + // 模拟 race:另一个并发请求已经先插入了同 (productId, quantity) 行 + const uniqueErr = new Error('ER_DUP_ENTRY'); + uniqueErr.name = 'SequelizeUniqueConstraintError'; + ProductBundle.create.mockRejectedValueOnce(uniqueErr); + + await expect( + bundleService.create(1, { specName: '5盒装', quantity: 5, price: 3.3, stock: 100 }) + ).rejects.toBeInstanceOf(ConflictError); + + // 应在事务内执行 + expect(mockTransaction).toHaveBeenCalledTimes(1); + expect(ProductBundle.create).toHaveBeenCalledTimes(1); + }); + + test('active-check 命中已有活跃行 → 直接抛 ConflictError(不进入 INSERT)', async () => { + ProductBundle.findOne.mockResolvedValueOnce({ id: 99 }); // active 已存在 + + await expect( + bundleService.create(1, { specName: '5盒装', quantity: 5, price: 3.3, stock: 100 }) + ).rejects.toBeInstanceOf(ConflictError); + + expect(ProductBundle.create).not.toHaveBeenCalled(); + }); + + test('非唯一键约束错误(如 FK 缺失)→ 原样向上抛,不被吞掉', async () => { + ProductBundle.findOne + .mockResolvedValueOnce(null) // active + .mockResolvedValueOnce(null); // tombstone + + const fkErr = new Error('FK violation'); + fkErr.name = 'SequelizeForeignKeyConstraintError'; + ProductBundle.create.mockRejectedValueOnce(fkErr); + + let thrown; + try { + await bundleService.create(1, { specName: '5盒装', quantity: 5, price: 3.3, stock: 100 }); + } catch (e) { thrown = e; } + expect(thrown).toBe(fkErr); + expect(thrown).not.toBeInstanceOf(ConflictError); + }); +}); \ No newline at end of file diff --git a/backend/tests/order.test.js b/backend/tests/order.test.js new file mode 100644 index 0000000000000000000000000000000000000000..fd61e8fd67f011a1f427e492f90fd24c3417a575 --- /dev/null +++ b/backend/tests/order.test.js @@ -0,0 +1,147 @@ +/** + * 订单主链路集成测试(基于 supertest) + * + * 覆盖: + * 1. 创建订单 → 模拟支付 → 后台发货 → 买家确认收货 + * 2. 取消订单(待支付 → 已取消)+ 二次取消报冲突 + * + * 副作用:会在 dev DB 真实写入 order/payment/cart_item/address。 + */ +'use strict'; + +const request = require('supertest'); +const { getApp, loginAs, authHeader } = require('./setup'); +const { sequelize } = require('../src/config/db'); +const { redis } = require('../src/config/redis'); + +const app = getApp(); +const req = request(app); + +let buyerToken; +let adminToken; +let buyerAuthH; +let adminAuthH; + +beforeAll(async () => { + buyerToken = await loginAs(req, 'tester', 'buyer123'); + adminToken = await loginAs(req, 'admin', 'admin123'); + buyerAuthH = authHeader(buyerToken); + adminAuthH = authHeader(adminToken); +}); + +afterAll(async () => { + // 主动关闭 DB / Redis 连接,避免 jest 退出警告 + await sequelize.close().catch(() => {}); + await redis.quit().catch(() => {}); +}); + +/** 准备 buyer 的收货地址 + 购物车项(fixture) */ +async function ensureFixtures() { + // 1. 取或建收货地址 + const addrRes = await req.get('/api/addresses').set(buyerAuthH.headers); + expect(addrRes.status).toBe(200); + let addressId = addrRes.body?.data?.[0]?.id ?? addrRes.body?.data?.list?.[0]?.id; + if (!addressId) { + const created = await req.post('/api/addresses').set(buyerAuthH.headers).send({ + receiver: '测试收件人', + phone: '13800001234', + province: '福建省', city: '龙岩市', district: '新罗区', + detail: '测试路 1 号', + isDefault: 1, + }); + expect(created.status).toBe(200); + addressId = created.body.data.id; + } + + // 2. 取或建购物车项(product id=29「九阳破壁机」stock=150) + const cartRes = await req.get('/api/cart').set(buyerAuthH.headers); + expect(cartRes.status).toBe(200); + const cartItems = Array.isArray(cartRes.body?.data) ? cartRes.body.data : []; + let cartItemId = cartItems.find((c) => c.productId === 29)?.id; + if (!cartItemId) { + const added = await req.post('/api/cart').set(buyerAuthH.headers) + .send({ productId: 29, qty: 1 }); + expect(added.status).toBe(200); + cartItemId = added.body.data.id; + } + + return { addressId, cartItemId }; +} + +describe('订单主链路', () => { + test('创建 → 支付 → 后台发货 → 买家确认收货', async () => { + const { addressId } = await ensureFixtures(); + + // 1. 创建订单 + const create = await req.post('/api/orders').set(buyerAuthH.headers).send({ addressId }); + expect(create.status).toBe(200); + expect(create.body.code).toBe(0); + const orderId = create.body.data.id; + expect(orderId).toBeGreaterThan(0); + + // 2. 模拟支付成功 + const pay = await req.post('/api/pay/mock').set(buyerAuthH.headers) + .send({ orderId, simulateSuccess: true }); + expect(pay.status).toBe(200); + expect(pay.body.code).toBe(0); + expect(pay.body.data.status).toBe(1); + + // 3. 状态查询接口(Task 1 新增) + const st = await req.get(`/api/orders/${orderId}/status`).set(buyerAuthH.headers); + expect(st.status).toBe(200); + expect(st.body.data.status).toBe(1); + + // 4. 后台发货 + const ship = await req.post(`/api/admin/orders/${orderId}/ship`).set(adminAuthH.headers).send({}); + expect(ship.status).toBe(200); + expect(ship.body.code).toBe(0); + + // 5. 买家确认收货 + const confirm = await req.post(`/api/orders/${orderId}/confirm`).set(buyerAuthH.headers).send({}); + expect(confirm.status).toBe(200); + + // 6. 验证最终状态 = 3 + const final = await req.get(`/api/orders/${orderId}`).set(buyerAuthH.headers); + expect(final.body.data.status).toBe(3); + }); + + test('取消订单(待支付 → 已取消)+ 二次取消报冲突', async () => { + const { addressId } = await ensureFixtures(); + + const create = await req.post('/api/orders').set(buyerAuthH.headers).send({ addressId }); + expect(create.body.code).toBe(0); + const orderId = create.body.data.id; + + const cancel = await req.post(`/api/orders/${orderId}/cancel`).set(buyerAuthH.headers).send({}); + expect(cancel.body.code).toBe(0); + + const detail = await req.get(`/api/orders/${orderId}`).set(buyerAuthH.headers); + expect(detail.body.data.status).toBe(4); + expect(detail.body.data.cancelReason).toBeTruthy(); + + // 二次取消应当 409 + const cancelAgain = await req.post(`/api/orders/${orderId}/cancel`) + .set(buyerAuthH.headers).send({}); + expect(cancelAgain.body.code).toBe(409); + }); + + test('admin 通过 /api/admin/orders/:id 可查任意买家订单', async () => { + const { addressId } = await ensureFixtures(); + + // 先用一个 buyer 创建订单 + const create = await req.post('/api/orders').set(buyerAuthH.headers).send({ addressId }); + expect(create.body.code).toBe(0); + const orderId = create.body.data.id; + + // admin 用 admin endpoint 查这条订单(不持 userId) + const adminGet = await req.get(`/api/admin/orders/${orderId}`).set(adminAuthH.headers); + expect(adminGet.status).toBe(200); + expect(adminGet.body.code).toBe(0); + expect(adminGet.body.data.id).toBe(orderId); + + // buyer 用自己的 endpoint 查同一条订单(应可见) + const buyerGet = await req.get(`/api/orders/${orderId}`).set(buyerAuthH.headers); + expect(buyerGet.body.code).toBe(0); + expect(buyerGet.body.data.id).toBe(orderId); + }); +}); \ No newline at end of file diff --git a/backend/tests/setup.js b/backend/tests/setup.js new file mode 100644 index 0000000000000000000000000000000000000000..f430d21a4ea3edda6b0143430779019cf4653e13 --- /dev/null +++ b/backend/tests/setup.js @@ -0,0 +1,30 @@ +/** + * 测试基础设施(基于 supertest) + * - getApp(): 返回 express app(用于 supertest) + * - loginAs(req, username, password): 通过 supertest 调 /api/auth/login 拿 JWT + * - authHeader(token): 返回 { headers: { Authorization: 'Bearer xxx' } } + * + * 注意事项同 setup.js:直接复用 dev DB(admin / tester 来自 data.sql), + * 测试会在 dev DB 中真实写入,跑前请确认是开发库。 + */ +'use strict'; + +const app = require('../src/app'); + +function getApp() { return app; } + +async function loginAs(req, username, password) { + const res = await req + .post('/api/auth/login') + .send({ username, password }); + if (res.body.code !== 0) { + throw new Error(`login failed for ${username}: ${res.body.message || res.text}`); + } + return res.body.data.token; +} + +function authHeader(token) { + return { headers: { Authorization: `Bearer ${token}` } }; +} + +module.exports = { getApp, loginAs, authHeader }; \ No newline at end of file diff --git "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\216\245\345\217\243\350\256\276\350\256\241.md" "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\216\245\345\217\243\350\256\276\350\256\241.md" index 198b71ae4bf95a8915b3a0f7d3963cb6f3bb7cd5..126cf3e07085ea9f4dcd7c044c37cde9e6664eb5 100644 --- "a/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\216\245\345\217\243\350\256\276\350\256\241.md" +++ "b/docs/02-\350\256\276\350\256\241\346\226\207\346\241\243/\346\216\245\345\217\243\350\256\276\350\256\241.md" @@ -88,6 +88,11 @@ | **Ad08** | 后台订单 | 修改订单状态 | PUT | /api/admin/orders/{id}/status | 是(管理员) | C | | **Ad09** | 后台用户 | 用户列表 | GET | /api/admin/users | 是(管理员) | D | | **Ad10** | 后台用户 | 禁用/启用 | PUT | /api/admin/users/{id}/status | 是(管理员) | D | +| **Ad11** | 后台商品规格 | 列出某商品的 bundle | GET | /api/admin/products/{id}/bundles | 是(管理员) | B | +| **Ad12** | 后台商品规格 | 新增 bundle(含复活软删行) | POST | /api/admin/products/{id}/bundles | 是(管理员) | B | +| **Ad13** | 后台商品规格 | 局部更新 bundle | PATCH | /api/admin/bundles/{bundleId} | 是(管理员) | B | +| **Ad14** | 后台商品规格 | 单 bundle 上下架 | PATCH | /api/admin/bundles/{bundleId}/on-sale | 是(管理员) | B | +| **Ad15** | 后台商品规格 | 软删 bundle | DELETE | /api/admin/bundles/{bundleId} | 是(管理员) | B | | **Log01** | 日志 | 操作日志列表(选做)| GET | /api/admin/operation-logs | 是(管理员) | A | > 后续选做/挑战模块新增的接口,按同样规则在此追加,标注归属人与是否跨人接缝。 @@ -203,3 +208,145 @@ ### A02 用户登录 (同上格式,逐个接口补全…) + +## 四、商品规格(product_bundle)— 后台管理 + +> 第一期:仅后台编辑。前台/购物车/订单不联动,base 商品继续可单独下单。 + +### Admin 接口(`/api/admin/*`,需 admin 鉴权) + +| Method | Path | 说明 | +|---|---|---| +| GET | `/admin/products/:id/bundles` | 列出某商品的 bundle(不含软删),按 sort ASC, id ASC | +| POST | `/admin/products/:id/bundles` | 新增 bundle | +| PATCH | `/admin/bundles/:bundleId` | 局部更新(specName/quantity/price/stock/image/sort/onSale) | +| PATCH | `/admin/bundles/:bundleId/on-sale?onSale=1\|0` | 单 bundle 上下架 | +| DELETE | `/admin/bundles/:bundleId` | 软删(`deleted=1`) | + +### 请求体 + +`POST /admin/products/:id/bundles`: + +```json +{ + "specName": "5盒装", + "quantity": 5, + "price": 3.30, + "stock": 100, + "image": "https://...", // 可选;为空回退 product.image + "sort": 0, // 可选 + "onSale": 1 // 可选,默认 1 +} +``` + +`PATCH /admin/bundles/:bundleId`:上述字段子集,至少 1 个字段。 + +### 响应 + +```json +{ "code": 0, "message": "ok", "data": { + "id": 1, "productId": 42, "specName": "5盒装", "quantity": 5, + "price": "3.30", "stock": 100, "image": null, "sort": 0, "onSale": 1, + "createdAt": "2026-08-05T10:00:00Z", "updatedAt": "2026-08-05T10:00:00Z" +} } +``` + +### 错误码 + +| HTTP | code | 场景 | +|---|---|---| +| 200 | 0 | 成功 | +| 400 | 40001 | 参数错误(Joi 校验失败) | +| 403 | 40301 | 非管理员 | +| 404 | 40401 | 商品不存在 | +| 404 | 40402 | 规格不存在 | +| 409 | 40901 | `(productId, quantity)` 重复 | + +### 数据库 + +新增 `product_bundle` 表: + +``` +id product_id(FK→product) spec_name quantity price stock +image sort on_sale deleted created_at updated_at +UNIQUE (product_id, quantity) +``` + +### 软删后重建(复活) + +`POST /admin/products/:id/bundles` 在以下情况会"复活"已软删的同 `(productId, quantity)` 行(update 原行 `deleted=0` 并覆盖字段)而非插入新行: + +- 已有 `deleted=1` 的同 `(productId, quantity)` 行 +- 当前没有 `deleted=0` 的同 `(productId, quantity)` 行(否则返回 409) + +这样避免唯一键 `UNIQUE(product_id, quantity)` 在删除+重建同名规格时冲突。 + +### 不在本期范围 + +- 前台商品详情规格选择 +- `cart_item.bundle_id` +- `order_item.bundle_id` +- S02/S03 按 bundle 扣库存 +- S04 结算按 bundle 维度 + +### Phase 2 新增(前台联动) + +#### 公共接口 + +| Method | Path | Auth | 说明 | +|---|---|---|---| +| GET | `/api/products/:id/bundles` | 无 | 列出某商品**在售 + 未软删**的 bundle,按 sort ASC, id ASC | + +#### 签名变更(向下兼容,可选字段) + +**`POST /api/cart`** 加入购物车:Body 新增 `bundleId` 可选字段 + +```json +{ "productId": 29, "quantity": 1, "bundleId": 7 } +``` + +**`POST /api/cart/merge`** 合并购物车:每个 item 新增 `bundleId` 可选字段 + +**`S02 POST /api/products/:id/stock/decrement`** 扣库存:Body 新增 `bundleId` 可选字段 +(当前 order service 在同进程内调用,未来如拆微服务请同步更新) + +**`S03 POST /api/products/:id/stock/increment`** 回补库存:Body 新增 `bundleId` 可选字段 + +#### 数据库变更 + +```sql +-- cart_item 加 bundle_id 列 + 改唯一约束 +ALTER TABLE cart_item + ADD COLUMN bundle_id BIGINT DEFAULT NULL AFTER product_id, + DROP INDEX uk_user_product, + ADD UNIQUE KEY uk_user_product_bundle (user_id, product_id, bundle_id), + ADD KEY idx_bundle (bundle_id), + ADD CONSTRAINT fk_cart_bundle FOREIGN KEY (bundle_id) REFERENCES product_bundle (id) ON DELETE SET NULL; + +-- order_item 加 bundle_id + spec_name 快照 +ALTER TABLE order_item + ADD COLUMN bundle_id BIGINT DEFAULT NULL AFTER product_id, + ADD COLUMN spec_name VARCHAR(64) DEFAULT NULL AFTER product_name, + ADD KEY idx_oi_bundle (bundle_id), + ADD CONSTRAINT fk_oi_bundle FOREIGN KEY (bundle_id) REFERENCES product_bundle (id) ON DELETE SET NULL; +``` + +#### 价格快照约定 + +`order_item.price` 含义: +- **base 商品**:`product.price`(每件单价,quantity 件) +- **bundle 规格**:`bundle.price × bundle.quantity`(每套总价,quantity 套) + +`order_item.subtotal = price × quantity` 公式不变。 + +#### 库存扣减约定 + +`orderService.createOrder` 内联: +- `cart_item.bundle_id` 非空 → 扣 `product_bundle.stock`(按 `cart_item.quantity` 套数) +- 否则 → 扣 `product.stock`(按 `cart_item.quantity` 件数) + +`orderService.cancelOrder` 内联: +- `order_item.bundle_id` 非空 → 补 `product_bundle.stock` +- 否则 → 补 `product.stock` + +幂等键:`stock_log.biz_no` 在 bundle 时附加 `:bundle:` 后缀,避免 base/bundle 同 orderNo 互相覆盖。 diff --git a/docs/superpowers/plans/2026-07-27-address-feature-implementation.md b/docs/superpowers/plans/2026-07-27-address-feature-implementation.md new file mode 100644 index 0000000000000000000000000000000000000000..cf72a7bf72ad6761b45b78104ad75243ea438a6e --- /dev/null +++ b/docs/superpowers/plans/2026-07-27-address-feature-implementation.md @@ -0,0 +1,560 @@ +# 收货地址功能优化实施计划 + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 在 Checkout 页内嵌"新增收货地址"Dialog、把省/市/区改为 element-china-area-data 级联选择、并保证地址列表按创建时间倒序。 + +**Architecture:** 抽 `AddressEditDialog.vue` 通用弹窗组件供 Checkout 调用;AddressEdit.vue 独立页同样接入 cascader;后端 `addressService.list` 加 `ORDER BY id DESC`。最小改动原则:后端契约不变、路由不变、字段不变。 + +**Tech Stack:** Vue 3 + Element Plus / Express + Sequelize(无新后端依赖)/ element-china-area-data 6.x(前端新增依赖) + +## 全局约束 + +- 字段命名:province / city / district 保持字符串写入数据库;级联选择器仅做 UI 聚合,最终仍按三个独立字段提交。 +- 接口契约:`POST /addresses` 接收 `{ receiver, phone, province, city, district, detail, isDefault }`,与现状一致。 +- 排序:依赖后端 `ORDER BY id DESC`(id 自增 ≡ 创建时间倒序);前端**不**做本地排序。 +- 提交频率:每个 Task 一个 commit。 + +--- + +## File Structure + +**后端修改** +- `backend/src/modules/user/services/addressService.js` — `list` 函数加 `order` + +**前端新增 / 修改** +- 新增 `frontend/src/components/AddressEditDialog.vue` — 通用弹窗组件 +- 修改 `frontend/src/views/user/AddressEdit.vue` — 省/市/区改为 cascader +- 修改 `frontend/src/views/order/Checkout.vue` — 加 "+ 新增收货地址" 按钮 + 接入 dialog +- 修改 `frontend/package.json` / `frontend/package-lock.json` — 加 `element-china-area-data` + +--- + +## Task 1: 后端地址列表按 id DESC 排序 + +**Files:** +- 修改 `backend/src/modules/user/services/addressService.js` + +- [ ] **Step 1: 读取并定位 `list` 函数** + +```bash +grep -n "async function list" backend/src/modules/user/services/addressService.js +``` + +预期输出包含 `async function list(userId)` 所在的行号。 + +- [ ] **Step 2: 修改 `list` 函数,加入 `order`** + +在 `Address.findAll({ where: { userId } })` 的对象中追加 `order: [['id', 'DESC']]`。完成后函数体形如: + +```js +async function list(userId) { + return Address.findAll({ + where: { userId }, + order: [['id', 'DESC']], + }); +} +``` + +- [ ] **Step 3: 验证 SQL 行为** + +启动后端服务(已有 `npm run dev` 或 `npm start`),用 curl / Postman 调 `GET /api/addresses`(带 buyer JWT),确认返回数组第一项的 `id` 是最大值。 + +- [ ] **Step 4: Commit** + +```bash +git add backend/src/modules/user/services/addressService.js +git commit -m "feat(user): addressService.list 按 id DESC 排序(创建时间倒序)" +``` + +--- + +## Task 2: 安装 element-china-area-data + +**Files:** +- 修改 `frontend/package.json` +- 修改 `frontend/package-lock.json`(自动生成) + +- [ ] **Step 1: 安装依赖** + +```bash +cd frontend && npm install element-china-area-data +``` + +预期:`package.json` 新增 `"element-china-area-data": "^6.x.x"`。 + +- [ ] **Step 2: 验证 import 可用** + +在 `frontend/src/components/` 下临时建一个 sanity check 文件 `area-data-check.mjs`: + +```js +import { regionData } from 'element-china-area-data'; +console.log('regionData is array:', Array.isArray(regionData), 'len:', regionData.length); +``` + +运行: + +```bash +cd frontend && node src/components/area-data-check.mjs +``` + +预期输出:`regionData is array: true len: 34`(34 个省级行政单位)。验证后**删除** `area-data-check.mjs`。 + +- [ ] **Step 3: Commit** + +```bash +cd frontend && git add package.json package-lock.json +git commit -m "feat(deps): 添加 element-china-area-data 用于省市区级联" +``` + +> 注意:cwd 在 frontend/,要回到 repo 根目录再提交,或 git 直接 add frontend/package.json frontend/package-lock.json。 + +--- + +## Task 3: 创建 AddressEditDialog.vue 可复用弹窗组件 + +**Files:** +- 新增 `frontend/src/components/AddressEditDialog.vue` + +**Interfaces:** +- Props: `modelValue: Boolean`(v-model 控制显示) +- Emits: `update:modelValue`, `saved`(保存成功后触发,payload 为新增/更新后的 address 对象) +- 内部数据:完整地址表单(receiver / phone / province / city / district / detail / isDefault) +- 内部使用:`createAddress` / `updateAddress` / `regionData` + +- [ ] **Step 1: 创建文件,写入完整组件骨架** + +```vue + + + + + +``` + +- [ ] **Step 2: 手动检查** + +- 打开 `frontend/src/components/AddressEditDialog.vue` 确认文件无 lint 错误 +- 确认 `import { createAddress } from '@/api/user';` 路径正确(与 `api/user.js` 已暴露的方法一致) + +- [ ] **Step 3: Commit** + +```bash +git add frontend/src/components/AddressEditDialog.vue +git commit -m "feat(ui): 新增 AddressEditDialog 弹窗组件(含省市区级联)" +``` + +--- + +## Task 4: AddressEdit.vue 改造省/市/区为 cascader + +**Files:** +- 修改 `frontend/src/views/user/AddressEdit.vue` + +**Interfaces:** +- 字段保持:`form.receiver / phone / province / city / district / detail / isDefault` +- 新增内部:`areaArr`(cascader 用的数组),`regionData` 数据源 +- watch cascader 选中值同步到 `form.province/city/district` +- 编辑模式回填:用 `[row.province, row.city, row.district]` 字符串数组尝试匹配 cascader 选项 + +- [ ] **Step 1: 修改 ` + + +``` + +- [ ] **Step 5: Commit** + +```bash +git add backend/src/modules/order/controllers/adminOrderController.js \ + backend/src/modules/order/routes/index.js \ + frontend/src/views/order/admin/OrderManage.vue \ + frontend/src/api/order.js +git commit -m "feat(order): 完整实现后台订单管理(列表筛选/详情/发货/完成)" +``` + +--- + +## Task 9: 后端 Jest + Supertest 集成测试(覆盖主链路) + +**Files:** +- 新增 `backend/jest.config.js` +- 新增 `backend/tests/setup.js` +- 新增 `backend/tests/order.test.js` +- 修改 `backend/package.json` (新增 scripts) + +- [ ] **Step 1: 添加依赖与脚本** + +```bash +cd backend && npm install --save-dev jest supertest +``` + +在 `backend/package.json` 中 `scripts` 增加: + +```json +"test": "jest --runInBand", +"test:watch": "jest --watch" +``` + +- [ ] **Step 2: 写入 `backend/jest.config.js`** + +```js +module.exports = { + testEnvironment: 'node', + testMatch: ['**/tests/**/*.test.js'], + setupFilesAfterEach: [], + testTimeout: 20000, +}; +``` + +- [ ] **Step 3: 写入 `backend/tests/setup.js`** + +提供全局的测试辅助:登录拿 token、清理订单/支付表。 + +```js +'use strict'; +const http = require('http'); +const app = require('../src/app'); +const { sequelize } = require('../src/config/db'); +const { redis } = require('../src/config/redis'); + +let server; +let port; + +async function start() { + await sequelize.sync({ force: false }); + await new Promise((resolve) => { + server = app.listen(0, () => { + port = server.address().port; + resolve(); + }); + }); + return `http://127.0.0.1:${port}`; +} + +async function stop() { + if (server) await new Promise((r) => server.close(r)); + await sequelize.close(); + await redis.quit().catch(() => {}); +} + +async function loginAs(username, password) { + // 通过 axios 直接调 /api/auth/login + const axios = require('axios'); + const baseURL = await start(); + const { data } = await axios.post(`${baseURL}/api/auth/login`, { username, password }); + return { token: data.data.token, baseURL }; +} + +module.exports = { start, stop, loginAs }; +``` + +> 注意:test 之间复用一个 server,jest --runInBand 串行执行;`loginAs` 提供 token。 + +- [ ] **Step 4: 写入 `backend/tests/order.test.js`** + +```js +'use strict'; +const axios = require('axios'); +const { start, stop, loginAs } = require('./setup'); + +describe('订单主链路', () => { + let baseURL, token; + + beforeAll(async () => { + const r = await loginAs('buyer123', 'buyer123'); // 占位:依据 data.sql 中的演示账号 + baseURL = r.baseURL; token = r.token; + }); + + afterAll(async () => { await stop(); }); + + test('创建订单 → 模拟支付 → 后台发货 → 买家确认收货', async () => { + const auth = { headers: { Authorization: `Bearer ${token}` } }; + + // 1. 创建订单 + const { data: create } = await axios.post(`${baseURL}/api/orders`, { addressId: 1 }, auth); + expect(create.code).toBe(0); + const orderId = create.data.id; + + // 2. 模拟支付成功 + const { data: pay } = await axios.post(`${baseURL}/api/pay/mock`, { orderId, simulateSuccess: true }, auth); + expect(pay.data.status).toBe(1); + + // 3. 状态查询接口 + const { data: st } = await axios.get(`${baseURL}/api/orders/${orderId}/status`, auth); + expect(st.data.status).toBe(1); + + // 4. 后台发货(用 admin 登录) + const admin = await loginAs('admin', 'admin123'); + await axios.post(`${admin.baseURL}/api/admin/orders/${orderId}/ship`, {}, { headers: { Authorization: `Bearer ${admin.token}` } }); + + // 5. 买家确认收货 + await axios.post(`${baseURL}/api/orders/${orderId}/confirm`, {}, auth); + const { data: final } = await axios.get(`${baseURL}/api/orders/${orderId}`, auth); + expect(final.data.status).toBe(3); + }); + + test('取消订单释放库存', async () => { + const auth = { headers: { Authorization: `Bearer ${token}` } }; + const { data: create } = await axios.post(`${baseURL}/api/orders`, { addressId: 1 }, auth); + await axios.post(`${baseURL}/api/orders/${create.data.id}/cancel`, {}, auth); + const { data: detail } = await axios.get(`${baseURL}/api/orders/${create.data.id}`, auth); + expect(detail.data.status).toBe(4); + }); +}); +``` + +> 注:`buyer123` / `admin123` 用户名依据 `data.sql` 中实际种子数据调整;如不存在可在 `data.sql` 中插入。 + +- [ ] **Step 5: 运行测试** + +```bash +cd backend && npm test +``` + +预期:两个用例都 PASS。 + +- [ ] **Step 6: Commit** + +```bash +git add backend/jest.config.js backend/tests backend/package.json backend/package-lock.json +git commit -m "test(order): 增加主链路 Jest + Supertest 集成测试" +``` + +--- + +## Task 10: README + 文档收尾 + +**Files:** +- 修改 `README.md` (新增 `npm test` 说明) +- 修改 `docs/订单支付功能.md` (在末尾追加"实施对照表"小节) + +- [ ] **Step 1: 在 `README.md` 中新增"测试运行"小节** + +```markdown +## 测试 + +后端集成测试(Jest + Supertest)覆盖订单主链路: +\`\`\`bash +cd backend && npm test +\`\`\` +测试用账号需与 `sql/data.sql` 演示数据一致(默认 `admin/admin123`、`buyer123/buyer123`)。 +``` + +- [ ] **Step 2: 在 `docs/订单支付功能.md` 文末追加** + +```markdown +## 五、实施对照表(2026-07-27 完善版) + +| 文档章节 | 内容 | 当前实现 | 备注 | +|---|---|---|---| +| 1.库存乐观锁 | `UPDATE product SET stock = stock - ? WHERE id = ? AND stock >= ?` | ✅ orderService.createOrder | 幂等键走 stock_log | +| 2.超时取消 | Redis 过期监听 + 兜底 | ✅ timeoutService | 详见 Task 7 | +| 3.事务一致性 | 订单/支付/库存同事务 | ✅ 全部用 sequelize.transaction | callback 事务化 | +| 4.支付安全 | 金额服务端计算、状态校验 | ✅ orderService 校验 status=0 | | +| 5.商品快照 | JSON 字段 | ⚠️ 当前为单字段(name/image/price) | 已满足"快照"语义 | +| 6.状态同步 | 轮询 /api/payment/status | ✅ 重命名为 /api/orders/:id/status | 见 Task 1 | +| 7.进度条 | el-steps | ✅ OrderDetail.vue | 见 Task 3 | +| 8.快捷操作 | 按状态按钮 | ✅ OrderList.vue | 见 Task 4 | +| 9.后台管理 | 列表/发货/详情 | ✅ OrderManage.vue + adminCompleteOrder | 见 Task 8 | +| 10.集成测试 | Jest + Supertest | ✅ tests/order.test.js | 见 Task 9 | +``` + +- [ ] **Step 3: Commit** + +```bash +git add README.md docs/订单支付功能.md +git commit -m "docs: README 增加测试说明 + 订单支付文档追加实施对照表" +``` + +--- + +## Self-Review + +**1. Spec coverage(按 `docs/订单支付功能.md` 一/二节逐项):** +- ✅ 前台四页面(Checkout / OrderList / OrderDetail / Payment) — 现有 + Task 2/3/4/5 增强 +- ✅ 后台 OrderManage — Task 8 +- ✅ Pinia + api 封装 — 现有 + Task 2 新接口 +- ✅ 数据模型 Order/OrderItem/Payment — schema 与 model 全部存在 +- ✅ 前台 7 个接口 + 后台 4 个接口 — Task 1(status)、Task 8(complete)新增 +- ✅ 状态机 — 现有 +- ✅ 库存乐观锁 — 现有 +- ✅ 超时取消(C03) — Task 7 全新 +- ✅ 数据一致性 / 幂等 — Task 6 加固 +- ✅ 支付安全 / 状态校验 — 现有 +- ✅ 快照 — 现有(单字段已等价 JSON 语义) +- ✅ 状态同步(轮询) — Task 1 + Task 2 +- ✅ 异常边界 — Task 5 防抖 + Task 6 二次校验 +- ✅ UX 细节(库存 / 倒计时 / 进度条) — Task 2/3/5 + +**2. Placeholder scan:** 无 "TBD/TODO/实现 later";每个 step 都给出具体代码与命令。 + +**3. Type consistency:** +- Task 1 → Task 2:暴露的 `getOrderStatus` 返回 `{ status, paidAt, expired }`,前端 `Payment.vue` 仅用 `status` 与 `expired`,一致。 +- Task 6 → Task 7:`TIMEOUT_KEY` 与 `TIMEOUT_SECONDS` 在两处文件中重新定义为同语义函数,幂等键前缀分别用 `cancel:` / `timeout:` 区分,避免冲突。 +- Task 8:`adminCompleteOrder` 与 `adminOrderController.complete` 名称对齐。 + +--- + +## Execution Handoff + +Plan complete and saved to `docs/superpowers/plans/2026-07-27-order-payment-detail-polish.md`. + +Two execution options: + +1. **Subagent-Driven (recommended)** — I dispatch a fresh subagent per task, review between tasks, fast iteration. +2. **Inline Execution** — Execute tasks in this session using executing-plans, batch execution with checkpoints. + +请 master 确认执行方式,或就任务顺序 / 范围提出调整。 \ No newline at end of file diff --git a/docs/superpowers/plans/2026-08-04-admin-product-management.md b/docs/superpowers/plans/2026-08-04-admin-product-management.md new file mode 100644 index 0000000000000000000000000000000000000000..7c8d3a777388a94e79023d1eebe2e87d83b40556 --- /dev/null +++ b/docs/superpowers/plans/2026-08-04-admin-product-management.md @@ -0,0 +1,421 @@ +# 后台商品管理实施计划 + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 重写 `frontend/src/views/product/admin/ProductManage.vue`,实现完整的商品管理表格 + 工具栏 + Dialog + 分页 + 筛选。后端 API 已就绪,前端 API 已就绪,只缺 UI。 + +**Architecture:** 单文件重写,参照 `CategoryManage.vue` 模式。表格 + 工具栏 + dialog + AdminPager;分类 / 状态筛选;上下架 Switch;图片 URL 实时预览。 + +**Tech Stack:** Vue 3 + Element Plus — 无新依赖。 + +## 全局约束 + +- 主题色沿用 `#ff5000`(橙红)。 +- 复用 `AdminPager` 组件(已有)。 +- 复用 `api/product.js` 的 5 个 admin 函数(已存在)。 +- 提交频率:1 个 Task 1 个 commit。 +- 验收:`pnpm build` 0 错误;浏览器 `/admin/products` 视觉一致。 + +--- + +## File Structure + +**修改文件** +- `frontend/src/views/product/admin/ProductManage.vue` — 整体重写 + +**不做** +- 不改后端 / 路由 / API / store + +--- + +## Task 1: 重写 ProductManage.vue + +**Files:** +- Modify: `frontend/src/views/product/admin/ProductManage.vue` + +**Interfaces:** +- Consumes: + - `api/product.js`: `adminListProducts`, `adminCreateProduct`, `adminUpdateProduct`, `adminDeleteProduct`, `adminSetOnSale`, `getCategories` + - `components/AdminPager.vue` (existing) + - Element Plus icons: `Plus`, `Search`, `Refresh`, `Edit`, `Delete` +- Produces: unchanged — same route `/admin/products` + +- [ ] **Step 1: 完整重写** + +模板(`frontend/src/views/product/admin/ProductManage.vue`): + +```vue + + + + + +``` + +- [ ] **Step 2: 验证 build** + +```bash +cd frontend && pnpm build +``` + +预期:✓ built in s,无错误。 + +- [ ] **Step 3: Commit** + +```bash +git add frontend/src/views/product/admin/ProductManage.vue +git commit -m "feat(admin/products): 后台商品管理 UI 实现(表格 + 工具栏 + Dialog + 上下架)" +``` + +## 自审检查清单 + +- [x] 表格 8 列(ID / 图 / 名称 / 分类 / 价格 / 库存 / 状态 / 操作)齐 +- [x] 工具栏:新增 / 搜索 / 分类筛选 / 状态筛选 / 刷新 +- [x] Dialog:图 URL + 名称 + 分类 + 描述 + 价格 + 库存 + 上下架 +- [x] 分页用 AdminPager +- [x] 上下架 Switch 实时调 API +- [x] 错误处理:try/catch + ElMessage +- [x] 复用现有 API(adminList 等已存在) +- [x] 不改后端 / 路由 / store diff --git a/docs/superpowers/plans/2026-08-04-cart-taobao-style.md b/docs/superpowers/plans/2026-08-04-cart-taobao-style.md new file mode 100644 index 0000000000000000000000000000000000000000..0fe9bb760267726e9067f3bb5fb4784565a4c04a --- /dev/null +++ b/docs/superpowers/plans/2026-08-04-cart-taobao-style.md @@ -0,0 +1,438 @@ +# 购物车淘宝风格改造实施计划 + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 把 `frontend/src/views/user/Cart.vue` 重写为淘宝风格购物车(顶部 Tab + 按 category 分组 + 商品标签 + 浮动结算栏)。不改后端、不改路由、不动 cartStore / api。 + +**Architecture:** 单文件重写。现有脚本逻辑(cartStore 集成、compute、select、qty、remove、checkout)完全保留;只改 template 结构 + scoped CSS。引入按 category 分组的 computed;对 cartStore.item 没 category 字段时,回退到 categoryStore(如有)或"未分类"。 + +**Tech Stack:** Vue 3 + Element Plus — 无新依赖。 + +## 全局约束 + +- 主题色:`#ff5000`(橙红)、`#f5f5f5`(背景);禁止残留"淘宝"字样。 +- 商品按 category 分组(手机数码 / 电脑办公 / 家用电器),无 store 概念。 +- 静态装饰标签:每件商品「信用卡支付」灰色 + 「假一赔四 极速退款 7天无理由退换」红色。 +- 顶部 Tab 仅「全部商品」真实响应;其他 2 个点中显示空态。 +- 提交频率:每个 Task 1 个 commit。 +- 验收:`pnpm build` 0 错误;浏览器 `http://localhost:5173/cart` 视觉一致。 + +--- + +## File Structure + +**修改文件** +- `frontend/src/views/user/Cart.vue` — 整体重写 + +**新增文件(可选)** +- `frontend/src/views/user/cartConfig.js` — 静态 Tab / 标签 / 店铺标题配置(如果觉得这能减负) + +**不做** +- 不改后端 / cartStore / api / 路由 + +--- + +## Task 1: 重写 Cart.vue 为淘宝风格 + +**Files:** +- Modify: `frontend/src/views/user/Cart.vue` + +**Interfaces:** +- Consumes: `useCartStore()` (items, localCart, totalQty, loadCart, toggleSelect, toggleSelectAll, updateQty, removeItem, clearAll) +- Produces: unchanged — same route `/cart`, same `onCheckout` URL `/checkout?ids=...&totalPrice=...` + +- [ ] **Step 1: 读取目录** + +先看现有 `Cart.vue` 全文(已在 spec 中核对过),确认保留: +- `onToggleSelect`, `onToggleAll`, `onQty`, `onRemove`, `onClearAll`, `onCheckout` 函数名 +- `isAllSelected`, `isIndeterminate`, `selectedCount`, `totalPrice` 计算属性 +- `onMounted` 加载逻辑 + +- [ ] **Step 2: 重写为淘宝风格** + +**整体结构**: + +```vue + +``` + +**Script 部分**: + +```vue + +``` + +**Scoped CSS**(关键样式): + +```vue + +``` + +- [ ] **Step 3: 验证 build** + +```bash +cd frontend && pnpm build +``` + +预期:✓ built in <时间>s,无错误。 + +- [ ] **Step 4: Commit** + +```bash +git add frontend/src/views/user/Cart.vue +git commit -m "feat(cart): 购物车 UI 重写为淘宝风格(顶部 Tab + 按 category 分组 + 浮动结算栏)" +``` + +## 自审检查清单 + +- [x] Spec 全部覆盖(Tab + 工具栏 + 分组 + 商品卡 + 底部栏 + 空态) +- [x] 现有 cartStore / API 调用不被破坏 +- [x] 静态装饰标签写入 template,不依赖后端数据 +- [x] 主题色统一 `#ff5000` +- [x] 无淘宝品牌残留 +- [x] 移入收藏 / 重新选择规格 / 优惠明细 全部用 `ElMessage.info('开发中')` 占位 diff --git a/docs/superpowers/plans/2026-08-04-homepage-taobao-style.md b/docs/superpowers/plans/2026-08-04-homepage-taobao-style.md new file mode 100644 index 0000000000000000000000000000000000000000..068ff19e0c35d1559e0436abedafdbb3f2e28f19 --- /dev/null +++ b/docs/superpowers/plans/2026-08-04-homepage-taobao-style.md @@ -0,0 +1,1568 @@ +# 首页淘宝风格改造实施计划 + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 把 `frontend/src/views/Home.vue` 从"嵌入 ProductList"重写为完整淘宝风格首页(顶部快捷入口 + 搜索栏 + 左侧分类 + 中间 banner + 右侧 4 卡片 + 猜你喜欢 + 浮动工具栏),不修改后端、不改路由。 + +**Architecture:** Home.vue 仅做编排;UI 拆为 7 个独立单文件组件(`frontend/src/components/home/*.vue`);顶部入口 + Banner 数据集中放在 `frontend/src/config/homeNav.js`;所有装饰图(3 张 banner + 4 张 promo 卡)用纯内联 SVG,避免外部网络依赖。复用现有 `getCategories()` / `listProducts()` 接口与 `useCartStore` 的 `itemCount` getter。 + +**Tech Stack:** Vue 3 + Vite + Element Plus + Element Plus Icons — 无新依赖。 + +## 全局约束 + +- 主题色:`#ff5000`(主)、`#ff7e00`(高亮)、`#f5f5f5`(背景);禁止残留"淘宝"字样。 +- 文案映射:淘宝秒杀 → 限时秒杀;百亿补贴 → 官方补贴;聚划算 → 品牌专区;U先试用 → 试用中心;国家补贴 → 商城补贴。 +- 数据来源:侧栏 + 右侧 4 卡片绑后端 `getCategories()`;猜你喜欢复用 `listProducts({ page: 1, pageSize: 12 })`;不引入新 Pinia store、不引入新 router。 +- 错误处理:任何接口失败 → 各模块渲染 `el-empty`,首页永不全白屏。 +- 提交频率:每个 Task 一个 commit。 +- 验收脚本:`pnpm lint` 必须 0 报错;`pnpm build` 必须 0 报错;`pnpm dev` 后 `http://localhost:5173/` 视觉确认。 + +--- + +## File Structure + +**新增文件** +- `frontend/src/config/homeNav.js` — 顶部快捷入口 + Banner 静态数据 +- `frontend/src/components/home/TopNavBar.vue` — 顶部快捷功能栏 +- `frontend/src/components/home/SearchBar.vue` — 居中搜索栏 +- `frontend/src/components/home/CategorySidebar.vue` — 左侧分类栏 +- `frontend/src/components/home/HeroBanner.vue` — 中间轮播 +- `frontend/src/components/home/PromoCards.vue` — 右侧 4 卡片 +- `frontend/src/components/home/GuessYouLike.vue` — 猜你喜欢区 +- `frontend/src/components/home/FloatSidebar.vue` — 浮动工具栏 +- `frontend/src/views/Home.vue` — 重写为编排层 + +**微调文件** +- `frontend/src/layouts/FrontLayout.vue` — logo 文字颜色改橙红 +- `README.md` — 首页说明段追加一句 + +**不做** +- 不改后端、不改路由、不改 store、不改 api/ + +--- + +## Task 1: 静态配置文件 homeNav.js + +**Files:** +- Create: `frontend/src/config/homeNav.js` + +- [ ] **Step 1: 创建配置文件** + +写入 `frontend/src/config/homeNav.js`: + +```js +/** + * 首页顶部快捷入口 + Banner 静态数据 + * - 任何一项 icon 字段只能用 Element Plus Icons 名称(首字母大写) + * - 业务无关的入口也用本表占位,避免后端再补一张 navigation 表 + */ +export const topNavItems = [ + { key: 'subsidy', icon: 'Coin', label: '商城补贴', q: '补贴' }, + { key: 'seckill', icon: 'Timer', label: '限时秒杀', q: '秒杀' }, + { key: 'trial', icon: 'Present', label: '试用中心', q: '试用' }, + { key: 'official', icon: 'Medal', label: '官方补贴', q: '官方补贴' }, + { key: 'coupon', icon: 'Discount', label: '领券中心', q: '优惠券' }, + { key: 'brand', icon: 'Star', label: '品牌专区', q: '品牌' }, + { key: 'member', icon: 'Trophy', label: '会员聚惠', q: '会员' }, +] + +/** + * Hero Banner 数据(3 张) + * - link 字段若以 'category:' 开头,组件中转为 /category/:id;否则直接作 query 跳 /search + */ +export const heroBanners = [ + { + key: 'home-appliance', + title: '智能家电 品质生活', + subtitle: '品类齐全 现货热销', + bg: 'linear-gradient(135deg, #7e57ff 0%, #9575ff 100%)', + link: 'category:1', + }, + { + key: 'digital', + title: '数码精选 焕新生活', + subtitle: '手机电脑 智能穿戴', + bg: 'linear-gradient(135deg, #1e9cff 0%, #4ab9ff 100%)', + link: 'category:2', + }, + { + key: 'maternal', + title: '母婴童趣 用心呵护', + subtitle: '优选好物 安心选择', + bg: 'linear-gradient(135deg, #ff7e00 0%, #ffaa55 100%)', + link: 'category:3', + }, +] +``` + +- [ ] **Step 2: 验证文件可被解析** + +```bash +cd frontend && node -e "import('./src/config/homeNav.js').then(m => console.log(typeof m.topNavItems, m.topNavItems.length, typeof m.heroBanners, m.heroBanners.length))" +``` + +预期输出:`object 7 object 3` + +- [ ] **Step 3: Commit** + +```bash +git add frontend/src/config/homeNav.js +git commit -m "feat(home): 新增首页静态配置(顶部入口 + Banner)" +``` + +--- + +## Task 2: 创建 3 张 Banner SVG 资源 + +**Files:** +- Create: `frontend/src/assets/home/banner-1.svg` +- Create: `frontend/src/assets/home/banner-2.svg` +- Create: `frontend/src/assets/home/banner-3.svg` + +- [ ] **Step 1: 创建 banner 目录** + +```bash +mkdir -p frontend/src/assets/home +``` + +- [ ] **Step 2: 创建 banner-1.svg(家用电器主题)** + +写入 `frontend/src/assets/home/banner-1.svg`: + +```svg + + + + + + + + + + + + + + + 智能家电 + 品质生活 + 品类齐全 现货热销 + +``` + +- [ ] **Step 3: 创建 banner-2.svg(数码主题)** + +写入 `frontend/src/assets/home/banner-2.svg`: + +```svg + + + + + + + + + + + + + + 数码精选 + 焕新生活 + 手机电脑 智能穿戴 + +``` + +- [ ] **Step 4: 创建 banner-3.svg(母婴主题)** + +写入 `frontend/src/assets/home/banner-3.svg`: + +```svg + + + + + + + + + + + + + + 母婴童趣 + 用心呵护 + 优选好物 安心选择 + +``` + +- [ ] **Step 5: 验证 SVG 文件可被解析** + +```bash +cd frontend && node -e "for (const f of ['banner-1','banner-2','banner-3']) { const fs = require('fs'); const s = fs.readFileSync('src/assets/home/'+f+'.svg','utf8'); if (!s.startsWith(' + + + + + + 家居 + +``` + +- [ ] **Step 2: 创建 promo-2.svg(美妆 · 粉)** + +写入 `frontend/src/assets/home/promo-2.svg`: + +```svg + + + + + + + 美妆 + +``` + +- [ ] **Step 3: 创建 promo-3.svg(五金 · 橙)** + +写入 `frontend/src/assets/home/promo-3.svg`: + +```svg + + + + + + + 五金 + +``` + +- [ ] **Step 4: 创建 promo-4.svg(百货 · 蓝)** + +写入 `frontend/src/assets/home/promo-4.svg`: + +```svg + + + + + + + 百货 + +``` + +- [ ] **Step 5: 验证 SVG 文件** + +```bash +cd frontend && node -e "for (const f of ['promo-1','promo-2','promo-3','promo-4']) { const fs = require('fs'); const s = fs.readFileSync('src/assets/home/'+f+'.svg','utf8'); if (!s.startsWith(' +
+
+ +
+
+ + + + + +``` + +- [ ] **Step 2: 验证 lint 通过** + +```bash +cd frontend && pnpm lint -- src/components/home/TopNavBar.vue +``` + +预期:`✔ No problems` 或无错误。 + +- [ ] **Step 3: Commit** + +```bash +git add frontend/src/components/home/TopNavBar.vue +git commit -m "feat(home): 新增 TopNavBar 顶部快捷功能栏" +``` + +--- + +## Task 5: 搜索栏 SearchBar.vue + +**Files:** +- Create: `frontend/src/components/home/SearchBar.vue` + +**Interfaces:** +- Produces: emits `search(keyword)` — Home.vue 把事件转发到 router.push + +- [ ] **Step 1: 创建组件** + +写入 `frontend/src/components/home/SearchBar.vue`: + +```vue + + + + + +``` + +- [ ] **Step 2: 验证 lint 通过** + +```bash +cd frontend && pnpm lint -- src/components/home/SearchBar.vue +``` + +预期:无错误。 + +- [ ] **Step 3: Commit** + +```bash +git add frontend/src/components/home/SearchBar.vue +git commit -m "feat(home): 新增 SearchBar 居中搜索栏" +``` + +--- + +## Task 6: 左侧分类栏 CategorySidebar.vue + +**Files:** +- Create: `frontend/src/components/home/CategorySidebar.vue` + +**Interfaces:** +- Consumes: `getCategories()` from `@/api/product` +- Produces: 无 events。点击项直接 $router.push('/category/:id') + +- [ ] **Step 1: 创建组件** + +写入 `frontend/src/components/home/CategorySidebar.vue`: + +```vue + + + + + +``` + +- [ ] **Step 2: 验证 lint 通过** + +```bash +cd frontend && pnpm lint -- src/components/home/CategorySidebar.vue +``` + +预期:无错误。 + +- [ ] **Step 3: Commit** + +```bash +git add frontend/src/components/home/CategorySidebar.vue +git commit -m "feat(home): 新增 CategorySidebar 左侧分类栏" +``` + +--- + +## Task 7: 中间 Banner 轮播 HeroBanner.vue + +**Files:** +- Create: `frontend/src/components/home/HeroBanner.vue` + +**Interfaces:** +- Consumes: `heroBanners` from `@/config/homeNav.js` +- Produces: 无 events,点击直接跳转 + +- [ ] **Step 1: 创建组件** + +写入 `frontend/src/components/home/HeroBanner.vue`: + +```vue + + + + + +``` + +- [ ] **Step 2: 验证 lint 通过** + +```bash +cd frontend && pnpm lint -- src/components/home/HeroBanner.vue +``` + +预期:无错误。 + +- [ ] **Step 3: Commit** + +```bash +git add frontend/src/components/home/HeroBanner.vue +git commit -m "feat(home): 新增 HeroBanner 中间轮播" +``` + +--- + +## Task 8: 右侧 4 张 Promo 卡片 PromoCards.vue + +**Files:** +- Create: `frontend/src/components/home/PromoCards.vue` + +**Interfaces:** +- Consumes: `categories: Category[]` as prop — Home.vue 传入 +- Produces: 无 events + +- [ ] **Step 1: 创建组件** + +写入 `frontend/src/components/home/PromoCards.vue`: + +```vue + + + + + +``` + +- [ ] **Step 2: 验证 lint 通过** + +```bash +cd frontend && pnpm lint -- src/components/home/PromoCards.vue +``` + +预期:无错误。 + +- [ ] **Step 3: Commit** + +```bash +git add frontend/src/components/home/PromoCards.vue +git commit -m "feat(home): 新增 PromoCards 右侧 4 张分类卡片" +``` + +--- + +## Task 9: 猜你喜欢商品区 GuessYouLike.vue + +**Files:** +- Create: `frontend/src/components/home/GuessYouLike.vue` + +**Interfaces:** +- Consumes: `listProducts({ page: 1, pageSize: 12 })` from `@/api/product` +- Produces: 无 events,点击跳详情 + +- [ ] **Step 1: 创建组件** + +写入 `frontend/src/components/home/GuessYouLike.vue`: + +```vue + + + + + +``` + +- [ ] **Step 2: 验证 lint 通过** + +```bash +cd frontend && pnpm lint -- src/components/home/GuessYouLike.vue +``` + +预期:无错误。 + +- [ ] **Step 3: Commit** + +```bash +git add frontend/src/components/home/GuessYouLike.vue +git commit -m "feat(home): 新增 GuessYouLike 猜你喜欢商品区" +``` + +--- + +## Task 10: 浮动工具栏 FloatSidebar.vue + +**Files:** +- Create: `frontend/src/components/home/FloatSidebar.vue` + +**Interfaces:** +- Consumes: `useCartStore().itemCount` for cart badge +- Produces: 无 events + +- [ ] **Step 1: 创建组件** + +写入 `frontend/src/components/home/FloatSidebar.vue`: + +```vue + + + + + +``` + +- [ ] **Step 2: 验证 lint 通过** + +```bash +cd frontend && pnpm lint -- src/components/home/FloatSidebar.vue +``` + +预期:无错误。 + +- [ ] **Step 3: Commit** + +```bash +git add frontend/src/components/home/FloatSidebar.vue +git commit -m "feat(home): 新增 FloatSidebar 浮动工具栏" +``` + +--- + +## Task 11: 微调 FrontLayout header 颜色 + +**Files:** +- Modify: `frontend/src/layouts/FrontLayout.vue:2` + +- [ ] **Step 1: 修改 logo 颜色** + +读取并定位 `.logo` 样式(`frontend/src/layouts/FrontLayout.vue` 第 93 行附近): + +```bash +grep -n "^\.logo " frontend/src/layouts/FrontLayout.vue +``` + +预期输出形如:`93:.logo { font-size: 18px; font-weight: bold; }` + +- [ ] **Step 2: 修改 `.logo` 样式块** + +将: +```css +.logo { font-size: 18px; font-weight: bold; } +``` + +改为: +```css +.logo { font-size: 18px; font-weight: bold; color: #ff5000; } +``` + +- [ ] **Step 3: 验证 lint 通过** + +```bash +cd frontend && pnpm lint -- src/layouts/FrontLayout.vue +``` + +预期:无错误。 + +- [ ] **Step 4: Commit** + +```bash +git add frontend/src/layouts/FrontLayout.vue +git commit -m "feat(home): FrontLayout header logo 改橙红色" +``` + +--- + +## Task 12: 重写 Home.vue 编排所有组件 + +**Files:** +- Modify: `frontend/src/views/Home.vue`(整体重写) + +**Interfaces:** +- Consumes: `getCategories()` / `listProducts()` +- Provides: 渲染所有 7 个 home 子组件 + +- [ ] **Step 1: 重写 Home.vue** + +覆盖整个 `frontend/src/views/Home.vue`: + +```vue + + + + + +``` + +- [ ] **Step 2: 验证 lint 通过** + +```bash +cd frontend && pnpm lint -- src/views/Home.vue +``` + +预期:无错误。 + +- [ ] **Step 3: 验证 build 通过** + +```bash +cd frontend && pnpm build +``` + +预期输出末尾包含 `built in`, 无 `error`。如果出现 `X is not defined` / `Module not found` 等错误,回去检查 import 路径。 + +- [ ] **Step 4: Commit** + +```bash +git add frontend/src/views/Home.vue +git commit -m "feat(home): Home.vue 重写为淘宝风格编排层" +``` + +--- + +## Task 13: 启动 dev 视觉验收 + +**Files:** +- None (验证) + +- [ ] **Step 1: 启动 dev 服务** + +```bash +cd frontend && pnpm dev +``` + +预期:服务 5 秒内启动,输出 `Local: http://localhost:5173/`,无错误。 + +- [ ] **Step 2: 浏览器访问首页** + +在浏览器打开 `http://localhost:5173/`,确认: + +1. 顶部 7 个快捷入口(商城补贴 / 限时秒杀 / 试用中心 / 官方补贴 / 领券中心 / 品牌专区 / 会员聚惠)横排展示 +2. 搜索栏 logo 显示 📦 + 电子商城 + 橙底"热卖商品"标签 +3. 左侧分类栏**有数据**(如果后端未起,显示"分类加载失败"占位即可) +4. 中间 Banner 三张紫色/蓝色/橙色切换,5s 自动 +5. 右侧 4 张卡片(家居/美妆/五金/百货)渐变背景 +6. 下方"猜你喜欢"显示 6 列商品网格(如果有数据) +7. 右侧浮动工具栏 5 个图标,购物车显示数字(未登录为 0) +8. 滚动 > 200px,回顶按钮变橙 +9. **无任何"淘宝"文案残留** + +- [ ] **Step 3: 关闭 dev 服务** + +在终端按 `Ctrl+C` 关闭。 + +- [ ] **Step 4: 无 commit** + +本任务仅做验证,不修改文件。 + +--- + +## Task 14: 更新 README 首页说明 + +**Files:** +- Modify: `README.md` + +- [ ] **Step 1: 定位项目简介段** + +```bash +grep -n "前后端分离" README.md +``` + +预期:第 4 行附近。 + +- [ ] **Step 2: 在 README 末尾追加"首页 UI 风格"段落** + +在 README 文件末尾(最后一行 `> 历史模板...` 之前)插入: + +```markdown + +## 前台首页 UI 风格 + +`/` 首页采用类电商首屏布局(顶部快捷入口 + 居中搜索栏 + 左侧分类 + 中间 Banner 轮播 + 右侧分类卡片 + 猜你喜欢 + 浮动工具栏),所有数据来源于 `/api/categories` 与 `/api/products`,装饰图均为内联 SVG,无外部网络依赖。详见 [docs/superpowers/specs/2026-08-04-homepage-taobao-style-design.md](docs/superpowers/specs/2026-08-04-homepage-taobao-style-design.md)。 +``` + +- [ ] **Step 3: 验证 README 末尾** + +```bash +tail -5 README.md +``` + +预期:末尾 5 行包含换行 + "## 前台首页 UI 风格" + spec 链接。 + +- [ ] **Step 4: Commit** + +```bash +git add README.md +git commit -m "docs(readme): 首页 UI 风格说明" +``` + +--- + +## 自审检查清单 + +执行计划前的自审要点: + +1. **Spec 覆盖**: + - ✅ 3.1 组件拆分(7 个)→ Task 4-10 逐一实现 + - ✅ 3.3 数据流(getCategories / listProducts)→ Task 6、9、12 + - ✅ 3.4 视觉规范(#ff5000 等)→ 出现在 Task 4-10 样式中 + - ✅ 3.5 关键交互(hover/跳转/回顶/购物车红点)→ Task 4-10 各组件内 + - ✅ 3.6 错误处理(el-empty)→ Task 6、9 + - ✅ 4. 文件改动清单 → 14 个任务覆盖所有新增/修改文件 + - ✅ 5. 验收标准(10 项)→ Task 13 视觉验收 + Task 12 Step 3 build 验证 + +2. **占位扫描**: + - ✅ 无 TBD / TODO / "implement later" + - ✅ 无 "Similar to Task N" 引用 + - ✅ 所有代码块完整可执行 + +3. **类型一致性**: + - ✅ `topNavItems` / `heroBanners` 在 Task 1 定义,Task 4、7、12 一致引用 + - ✅ `getCategories()` / `listProducts()` 在 Task 6、9、12 一致使用 + - ✅ `useCartStore().itemCount` 仅在 Task 10 使用 + - ✅ 路由跳转:搜索用 `name: 'Search' query: { q }`;分类用 `name: 'Category' query: { categoryId }` — Task 4-9、12 一致 + +4. **依赖顺序**: + - Task 1(配置)→ Task 4、7 依赖 + - Task 2、3(SVG)→ Task 7、8 依赖 + - Task 12(Home.vue)依赖 Task 4-10 全部 + - 顺序合理 + +--- + +## 2026-08-04 实施备注 + +实际落地与本计划存在 3 处偏差,记录如下: + +1. **TopNavBar 已移除**(commit `864f33f`):应用户反馈,Task 4(顶部快捷功能栏 TopNavBar.vue)落地的组件**已从 Home.vue 卸载**,且 `frontend/src/components/home/TopNavBar.vue` 文件于本次清理任务中删除。配置 `frontend/src/config/homeNav.js` 的 `topNavItems` 保留,作为未来按需重启顶部入口的数据源。当前 `Home.vue` 仅挂载 6 个组件(SearchBar / CategorySidebar / HeroBanner / PromoCards / GuessYouLike / FloatSidebar)。 +2. **商品图改 Unsplash CDN**(commit `7865ae7`):Task 12 仅约定"装饰图为纯内联 SVG,避免外部网络依赖"。该约束仅适用于 banner / promo 装饰图。**商品图片**(`product.image`)后于本次落地替换为 Unsplash CDN 真实图片(`https://images.unsplash.com/...`),告别 picsum 占位风格不一致;同步脚本 `backend/sql/update-product-images.sql` 与 `data.sql` 注释联动更新,README 已区分装饰图与商品图来源。 +3. **布局结构调整**(commit `061c76e`):Task 12 Step 1 的 `Home.vue` 草稿采用"上排 3 项 + 下排猜你喜欢共享 6 列网格"。最终落地改为**两行结构**——上行为"侧栏 + Banner + 4 张促销卡片"、下行为"猜你喜欢"独占整行(6 列网格),保证商品卡视觉宽度不被压缩。 diff --git a/docs/superpowers/plans/2026-08-05-product-bundle-admin.md b/docs/superpowers/plans/2026-08-05-product-bundle-admin.md new file mode 100644 index 0000000000000000000000000000000000000000..ffb038e26ae549342be9c4789d52bc495a3ccbb9 --- /dev/null +++ b/docs/superpowers/plans/2026-08-05-product-bundle-admin.md @@ -0,0 +1,1397 @@ +# 商品多规格(打包)后台管理 实施计划 — 第一期 + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 在 `ProductManage.vue` 编辑弹窗下半部新增"规格列表"区块(图1 风格),支持点击卡片展开内嵌编辑规格。配套新增 `product_bundle` 表 + 5 个 admin CRUD 接口。不动购物车 / 订单 / `cart_item` / `order_item`,base 商品继续可单独下单。 + +**Architecture:** +- 后端:新增 `ProductBundle` Sequelize 模型 → `bundleService` → `adminBundleController` → 路由挂载 `/api/admin/products/:id/bundles` 等 +- 前端:编辑弹窗下半部加规格列表区块,本地状态(`bundles / editingBundleId / creating / draft`),卡片默认摘要态 + 点击切换内嵌编辑态 +- 数据:`product_bundle` 表 `UNIQUE(product_id, quantity)` 防重;`price` 存每盒单价(与图1 "¥3.3/盒" 对齐) + +**Tech Stack:** +- 后端:Node.js 20 + Express 4 + Sequelize 6 + Joi +- 前端:Vue 3 + Element Plus(无新依赖) +- 测试:Jest + supertest(沿用 `backend/tests/setup.js`) + +--- + +## 全局约束 + +- 主题色沿用 `#ff5000`(橙红)。 +- 命名规范:表/字段小写下划线;JS 端 camelCase;Sequelize `field` 映射。 +- 响应格式统一 `{ code, message, data }`;`code=0` 成功。 +- 价格字段统一用 `DECIMAL(10,2)`,JSON 返回 string(避免 JS 浮点失真)。 +- 鉴权:所有 admin 路由需 `role=admin`(沿用 `requireRole('admin')`)。 +- 提交频率:1 个 Task 1 个 commit。 +- 第一期**不动**:`cart_item` / `order_item` / `cartController` / `orderController` / 接缝 S02/S03/S04。 +- 验收:后端 `npm test` 通过;前端 `pnpm build` 0 错误;浏览器 `/admin/products` 走查全部验收点。 + +--- + +## File Structure + +**新增文件** +- `backend/src/modules/product/models/ProductBundle.js` — Sequelize 模型 +- `backend/src/modules/product/services/bundleService.js` — CRUD 服务层 +- `backend/src/modules/product/controllers/adminBundleController.js` — admin 控制器 + Joi schema +- `backend/tests/bundle.test.js` — 后端集成测试(supertest) + +**修改文件** +- `backend/sql/schema.sql` — 新增 `product_bundle` 表 DDL +- `backend/src/modules/product/models/index.js` — 注册 `Product.hasMany bundles` +- `backend/src/modules/product/routes/index.js` — 挂载 5 条路由 +- `frontend/src/api/product.js` — 新增 5 个 admin bundle API +- `frontend/src/views/product/admin/ProductManage.vue` — 编辑弹窗下半部新增规格列表区块 +- `docs/02-设计文档/接口设计.md` — 新增章节(先 PR 走 A 审核;本次与代码同步合入) + +**不做** +- 不改 `cart_item` / `order_item` / 购物车 / 订单 / 接缝 S02~S04 +- 不引入新依赖 +- 不做前台商品详情规格选择(第二期) + +--- + +## Task 1: 新增 product_bundle 表 DDL + +**Files:** +- Modify: `backend/sql/schema.sql:217-218`(在 `SET FOREIGN_KEY_CHECKS = 1;` 之前插入) + +**Step 1: 在 schema.sql 末尾 `SET FOREIGN_KEY_CHECKS = 1;` 之前新增 DDL** + +把下面这段插入 `backend/sql/schema.sql` 紧贴 `CREATE TABLE browse_history ... ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;` 这一行之后、`SET FOREIGN_KEY_CHECKS = 1;` 之前: + +```sql +-- -------------------- 13) product_bundle(B 江禹辰,商品打包/多规格) -------------------- +CREATE TABLE `product_bundle` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `product_id` BIGINT NOT NULL, + `spec_name` VARCHAR(64) NOT NULL COMMENT '如 "5盒装"', + `quantity` INT NOT NULL COMMENT '每套件数', + `price` DECIMAL(10,2) NOT NULL COMMENT '每件单价', + `stock` INT NOT NULL DEFAULT 0, + `image` VARCHAR(255) DEFAULT NULL COMMENT '为空回退 product.image', + `sort` INT NOT NULL DEFAULT 0, + `on_sale` TINYINT NOT NULL DEFAULT 1 COMMENT '1=在售 0=下架', + `deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '软删除', + `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_product_qty` (`product_id`, `quantity`), + KEY `idx_product` (`product_id`, `deleted`, `sort`), + CONSTRAINT `fk_bundle_product` + FOREIGN KEY (`product_id`) REFERENCES `product` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +``` + +**Step 2: 验证 DDL 语法(不写库)** + +```bash +mysql -u root -p -e "USE eshop; SHOW CREATE TABLE product_bundle\G" 2>&1 | head -5 +``` + +预期:会报"Table 'eshop.product_bundle' doesn't exist"(因为还没跑 schema.sql),证明 SQL 语法没被编辑器破坏。 + +**Step 3: 实际导入 dev DB** + +```bash +mysql -u root -p eshop < backend/sql/schema.sql +mysql -u root -p -e "USE eshop; SHOW CREATE TABLE product_bundle\G" +``` + +预期:表创建成功;输出包含 `UNIQUE KEY uk_product_qty`、`fk_bundle_product` 外键。 + +**Step 4: Commit** + +```bash +git add backend/sql/schema.sql +git commit -m "feat(sql): 新增 product_bundle 表(同款多规格/打包)" +``` + +--- + +## Task 2: ProductBundle Sequelize 模型 + 注册关联 + +**Files:** +- Create: `backend/src/modules/product/models/ProductBundle.js` +- Modify: `backend/src/models/index.js`(项目约定:所有模型关联在根级 `src/models/index.js` 统一注册,不在每模块下建 `models/index.js`) + +**Step 1: 创建 ProductBundle 模型** + +新建 `backend/src/modules/product/models/ProductBundle.js`: + +```js +'use strict'; + +const { DataTypes } = require('sequelize'); +const { sequelize } = require('../../../config/db'); + +const ProductBundle = sequelize.define( + 'ProductBundle', + { + id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true }, + productId: { type: DataTypes.BIGINT, allowNull: false, field: 'product_id' }, + specName: { type: DataTypes.STRING(64), allowNull: false, field: 'spec_name' }, + quantity: { type: DataTypes.INTEGER, allowNull: false }, + price: { type: DataTypes.DECIMAL(10, 2), allowNull: false }, + stock: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0 }, + image: { type: DataTypes.STRING(255), allowNull: true }, + sort: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0 }, + onSale: { type: DataTypes.TINYINT, allowNull: false, defaultValue: 1, field: 'on_sale' }, + deleted: { type: DataTypes.TINYINT, allowNull: false, defaultValue: 0 }, + }, + { tableName: 'product_bundle', paranoid: false, timestamps: false } +); + +module.exports = ProductBundle; +``` + +**Step 2: 在根 `backend/src/models/index.js` 中注册 require + 关联** + +修改 `backend/src/models/index.js`: + +(1) 在 `// 1. 加载模型` 区域,把 `Product` 紧下方追加: + +```js +const ProductBundle = require('../modules/product/models/ProductBundle'); +``` + +(2) 在 `// 2. 定义模型关联` 区域,把 `Product ↔ 浏览历史` 关联(行 51-53)的紧下方追加: + +```js +// 商品 ↔ 规格(product_bundle,一对多) +Product.hasMany(ProductBundle, { foreignKey: 'productId', as: 'bundles' }); +ProductBundle.belongsTo(Product, { foreignKey: 'productId', as: 'product' }); +``` + +**Step 3: 验证后端能 require 模型不报语法错** + +```bash +cd backend && node -e "require('./src/models'); console.log('ok')" +``` + +预期:`ok`。 + +**Step 4: Commit** + +```bash +git add backend/src/modules/product/models/ProductBundle.js backend/src/models/index.js +git commit -m "feat(product): 新增 ProductBundle 模型 + 关联(注册到 src/models/index.js)" +``` + +--- + +## Task 3: bundleService 服务层 + +**Files:** +- Create: `backend/src/modules/product/services/bundleService.js` + +**Step 1: 创建服务层** + +新建 `backend/src/modules/product/services/bundleService.js`: + +```js +/** + * 规格(product_bundle)服务层 + */ +'use strict'; + +const { Op } = require('sequelize'); +const { sequelize } = require('../../../config/db'); +const Product = require('../models/Product'); +const ProductBundle = require('../models/ProductBundle'); +const { NotFoundError, ConflictError, ParamError } = require('../../../common/exception'); + +/** 序列化:DECIMAL → string */ +function serialize(row) { + if (!row) return row; + const plain = row.get ? row.get({ plain: true }) : { ...row }; + return { + ...plain, + price: plain.price == null ? null : String(plain.price), + }; +} + +/** 列出某商品的 bundle(按 sort ASC, id ASC),排除软删 */ +async function listByProduct(productId) { + // 校验 product 存在 + const exists = await Product.findOne({ where: { id: productId, deleted: 0 } }); + if (!exists) throw new NotFoundError('商品不存在'); + + const rows = await ProductBundle.findAll({ + where: { productId, deleted: 0 }, + order: [['sort', 'ASC'], ['id', 'ASC']], + }); + return rows.map(serialize); +} + +/** 新增 bundle(软删复活:同 productId+quantity 存在 deleted=1 行时,update 复活 + 覆盖字段) */ +async function create(productId, payload) { + // 校验 product 存在 + const exists = await Product.findOne({ where: { id: productId, deleted: 0 } }); + if (!exists) throw new NotFoundError('商品不存在'); + + // 校验唯一键:同 productId + quantity 的"活跃"行(复活场景下已存在的 deleted=1 行不算冲突) + const active = await ProductBundle.findOne({ + where: { productId, quantity: payload.quantity, deleted: 0 }, + }); + if (active) throw new ConflictError('该商品下已存在相同数量的规格'); + + // 软删复活:找到 deleted=1 的同 productId+quantity 行 → update 复活并覆盖 + const tombstone = await ProductBundle.findOne({ + where: { productId, quantity: payload.quantity, deleted: 1 }, + }); + if (tombstone) { + await tombstone.update({ + specName: payload.specName, + quantity: payload.quantity, + price: payload.price, + stock: payload.stock, + image: payload.image || null, + sort: payload.sort ?? 0, + onSale: payload.onSale ?? 1, + deleted: 0, + }); + return serialize(tombstone); + } + + const row = await ProductBundle.create({ + productId, + specName: payload.specName, + quantity: payload.quantity, + price: payload.price, + stock: payload.stock, + image: payload.image || null, + sort: payload.sort ?? 0, + onSale: payload.onSale ?? 1, + }); + return serialize(row); +} + +/** 局部更新 */ +async function update(bundleId, payload) { + const row = await ProductBundle.findOne({ where: { id: bundleId, deleted: 0 } }); + if (!row) throw new NotFoundError('规格不存在'); + + // 若 quantity 改了,重新校验唯一键 + if (payload.quantity !== undefined && Number(payload.quantity) !== Number(row.quantity)) { + const dup = await ProductBundle.findOne({ + where: { + productId: row.productId, + quantity: payload.quantity, + deleted: 0, + id: { [Op.ne]: bundleId }, + }, + }); + if (dup) throw new ConflictError('该商品下已存在相同数量的规格'); + } + + await row.update({ + specName: payload.specName ?? row.specName, + quantity: payload.quantity ?? row.quantity, + price: payload.price ?? row.price, + stock: payload.stock ?? row.stock, + image: payload.image !== undefined ? payload.image : row.image, + sort: payload.sort ?? row.sort, + onSale: payload.onSale ?? row.onSale, + }); + return serialize(row); +} + +/** 单 bundle 上下架 */ +async function setOnSale(bundleId, onSale) { + const row = await ProductBundle.findOne({ where: { id: bundleId, deleted: 0 } }); + if (!row) throw new NotFoundError('规格不存在'); + await row.update({ onSale }); + return serialize(row); +} + +/** 软删 */ +async function softDelete(bundleId) { + const row = await ProductBundle.findOne({ where: { id: bundleId, deleted: 0 } }); + if (!row) throw new NotFoundError('规格不存在'); + await row.update({ deleted: 1 }); + return serialize(row); +} + +module.exports = { listByProduct, create, update, setOnSale, softDelete }; +``` + +**Step 2: 验证 require 通过** + +```bash +cd backend && node -e "require('./src/modules/product/services/bundleService'); console.log('ok')" +``` + +预期:`ok`。 + +**Step 3: Commit** + +```bash +git add backend/src/modules/product/services/bundleService.js +git commit -m "feat(product): 新增 bundleService 服务层" +``` + +--- + +## Task 4: adminBundleController + 路由挂载 + +**Files:** +- Create: `backend/src/modules/product/controllers/adminBundleController.js` +- Modify: `backend/src/modules/product/routes/index.js` + +**Step 1: 创建 controller** + +新建 `backend/src/modules/product/controllers/adminBundleController.js`: + +```js +/** + * Admin 规格(bundle)controller + */ +'use strict'; + +const Joi = require('joi'); +const bundleService = require('../services/bundleService'); +const { success } = require('../../../common/response'); +const { validate } = require('../../../common/validator'); +const { ParamError } = require('../../../common/exception'); + +// 公共字段 +const createSchema = Joi.object({ + specName: Joi.string().min(1).max(64).required(), + quantity: Joi.number().integer().min(1).required(), + price: Joi.number().precision(2).min(0).required(), + stock: Joi.number().integer().min(0).required(), + image: Joi.string().uri().allow('', null).optional(), + sort: Joi.number().integer().optional(), + onSale: Joi.number().valid(0, 1).optional(), +}); + +const updateSchema = Joi.object({ + specName: Joi.string().min(1).max(64).optional(), + quantity: Joi.number().integer().min(1).optional(), + price: Joi.number().precision(2).min(0).optional(), + stock: Joi.number().integer().min(0).optional(), + image: Joi.string().uri().allow('', null).optional(), + sort: Joi.number().integer().optional(), + onSale: Joi.number().valid(0, 1).optional(), +}).min(1); + +async function list(req, res, next) { + try { + const data = await bundleService.listByProduct(req.params.id); + res.json(success(data)); + } catch (e) { next(e); } +} + +async function create(req, res, next) { + try { + const data = await bundleService.create(req.params.id, req.body); + res.json(success(data, '已新增')); + } catch (e) { next(e); } +} + +async function update(req, res, next) { + try { + const data = await bundleService.update(req.params.bundleId, req.body); + res.json(success(data, '已更新')); + } catch (e) { next(e); } +} + +async function setOnSale(req, res, next) { + try { + const onSale = parseInt(req.query.onSale, 10); + if (onSale !== 0 && onSale !== 1) { + throw new ParamError('onSale 必须为 0 或 1'); + } + const data = await bundleService.setOnSale(req.params.bundleId, onSale); + res.json(success(data, onSale === 1 ? '已上架' : '已下架')); + } catch (e) { next(e); } +} + +async function softDelete(req, res, next) { + try { + await bundleService.softDelete(req.params.bundleId); + res.json(success(null, '已删除')); + } catch (e) { next(e); } +} + +module.exports = { + list, create, update, setOnSale, softDelete, + createSchema, updateSchema, +}; +``` + +**Step 2: 挂载路由** + +修改 `backend/src/modules/product/routes/index.js`。在文件末尾、`module.exports = router;` 之前,**插入**下面这一段(与现有 admin 商品路由同一组,`/api/admin/products` 后面追加): + +```js +// /api/admin/products/:id/bundles 和 /api/admin/bundles/:bundleId (后台规格 CRUD) +const adminBundleCtl = require('../controllers/adminBundleController'); +router.get('/admin/products/:id/bundles', adminBundleCtl.list); +router.post('/admin/products/:id/bundles', validate(adminBundleCtl.createSchema), adminBundleCtl.create); +router.patch('/admin/bundles/:bundleId', validate(adminBundleCtl.updateSchema), adminBundleCtl.update); +router.patch('/admin/bundles/:bundleId/on-sale', adminBundleCtl.setOnSale); +router.delete('/admin/bundles/:bundleId', adminBundleCtl.softDelete); +``` + +> 这 5 条路由**不需要**再次 `authMiddleware + requireRole`,因为它们是 `/api/admin/*` 前缀,已经在外层被保护(见 routes/index.js:35 `router.use('/admin/products', authMiddleware, requireRole('admin'))`)。**但** `/admin/bundles/:bundleId` 不在 `/admin/products` 前缀下,需要单独保护。在上面那段之前先加: +> +> ```js +> // /api/admin/bundles/* 单独鉴权(不在 /admin/products 前缀下) +> router.use('/admin/bundles', authMiddleware, requireRole('admin')); +> ``` + +**Step 3: 启动后端冒烟验证路由挂载** + +```bash +cd backend && npm run dev +``` + +另开终端: + +```bash +curl -s -X POST http://localhost:3000/api/auth/login -H "Content-Type: application/json" -d '{"username":"admin","password":"admin123"}' | python -c "import sys,json;print(json.load(sys.stdin)['data']['token'])" +``` + +预期:输出一个 JWT token(保存备用,下一步用)。 + +```bash +TOKEN=... # 上面那个 token +curl -s http://localhost:3000/api/admin/products/1/bundles -H "Authorization: Bearer $TOKEN" +``` + +预期:`{"code":0,"message":"ok","data":[]}`(还没数据)。 + +**Step 4: Commit** + +```bash +git add backend/src/modules/product/controllers/adminBundleController.js backend/src/modules/product/routes/index.js +git commit -m "feat(product): admin bundle CRUD 接口(5 路由)" +``` + +--- + +## Task 5: 后端集成测试 + +**Files:** +- Create: `backend/tests/bundle.test.js` + +**Step 1: 写测试文件** + +新建 `backend/tests/bundle.test.js`: + +```js +/** + * 规格(bundle)集成测试 + * + * 覆盖: + * - 列表 / 新增 / 局部更新 / 上下架 / 软删 + * - 鉴权:非 admin → 403 + * - 重复 (productId, quantity) → 409 + * - 不存在的 product / bundle → 404 + * + * 副作用:会在 dev DB 真实写入 product_bundle;测试结束会清理本次建的数据。 + */ +'use strict'; + +const request = require('supertest'); +const { getApp, loginAs, authHeader } = require('./setup'); +const { sequelize } = require('../src/config/db'); +const ProductBundle = require('../src/modules/product/models/ProductBundle'); + +const app = getApp(); +const req = request(app); + +let buyerToken; +let adminToken; +let buyerAuthH; +let adminAuthH; +let testProductId; // 复用 dev 库中存在的商品(id=29 「九阳破壁机」),如不存在则建 +const createdIds = []; // 本次测试创建的所有 bundle id,便于 afterAll 清理 + +beforeAll(async () => { + buyerToken = await loginAs(req, 'tester', 'buyer123'); + adminToken = await loginAs(req, 'admin', 'admin123'); + buyerAuthH = authHeader(buyerToken); + adminAuthH = authHeader(adminToken); + + // 选/建一个测试用商品 + const list = await req.get('/api/admin/products').set(adminAuthH.headers).query({ pageSize: 1 }); + expect(list.status).toBe(200); + expect(list.body.code).toBe(0); + testProductId = list.body.data?.list?.[0]?.id ?? list.body.data?.list?.[0]?.id; + if (!testProductId) throw new Error('dev DB 中无可用商品,无法测试'); + + // 清理该商品下可能残留的 bundle(让测试幂等) + await ProductBundle.update({ deleted: 1 }, { where: { productId: testProductId } }); +}); + +afterAll(async () => { + // 清理本次测试创建的数据 + if (createdIds.length) { + await ProductBundle.update({ deleted: 1 }, { where: { id: createdIds } }); + } + await sequelize.close().catch(() => {}); +}); + +describe('规格 CRUD(product_bundle)', () => { + test('空列表 → 新增 → 再列表能看到', async () => { + const list = await req.get(`/api/admin/products/${testProductId}/bundles`).set(adminAuthH.headers); + expect(list.status).toBe(200); + expect(list.body.code).toBe(0); + expect(Array.isArray(list.body.data)).toBe(true); + + const created = await req + .post(`/api/admin/products/${testProductId}/bundles`) + .set(adminAuthH.headers) + .send({ specName: '5盒装', quantity: 5, price: 3.3, stock: 100 }); + expect(created.status).toBe(200); + expect(created.body.code).toBe(0); + expect(created.body.data.id).toBeGreaterThan(0); + expect(created.body.data.specName).toBe('5盒装'); + expect(created.body.data.price).toBe('3.30'); // DECIMAL → string + expect(created.body.data.quantity).toBe(5); + createdIds.push(created.body.data.id); + + const list2 = await req.get(`/api/admin/products/${testProductId}/bundles`).set(adminAuthH.headers); + expect(list2.body.data.find((b) => b.id === created.body.data.id)).toBeTruthy(); + }); + + test('重复 (productId, quantity) → 409', async () => { + // 先建一个 + const a = await req + .post(`/api/admin/products/${testProductId}/bundles`) + .set(adminAuthH.headers) + .send({ specName: '8盒装', quantity: 8, price: 3.3, stock: 80 }); + expect(a.status).toBe(200); + createdIds.push(a.body.data.id); + + // 再建一个同 quantity + const dup = await req + .post(`/api/admin/products/${testProductId}/bundles`) + .set(adminAuthH.headers) + .send({ specName: '8盒装-b', quantity: 8, price: 3.5, stock: 50 }); + expect(dup.body.code).toBe(409); + }); + + test('参数错误(quantity=0)→ 400', async () => { + const bad = await req + .post(`/api/admin/products/${testProductId}/bundles`) + .set(adminAuthH.headers) + .send({ specName: 'X', quantity: 0, price: 1, stock: 1 }); + expect(bad.body.code).toBe(400); + }); + + test('软删后重建同名规格 → 复活(id 保持不变,字段被覆盖)', async () => { + // 1. 建一个 quantity=77 的规格 + const a = await req + .post(`/api/admin/products/${testProductId}/bundles`) + .set(adminAuthH.headers) + .send({ specName: '临时', quantity: 77, price: 1.5, stock: 10 }); + expect(a.body.code).toBe(0); + const id = a.body.data.id; + createdIds.push(id); + + // 2. 软删 + const del = await req.delete(`/api/admin/bundles/${id}`).set(adminAuthH.headers); + expect(del.body.code).toBe(0); + + // 3. 用新的字段值重建同名规格(quantity=77)→ 应当复活旧行 + const reb = await req + .post(`/api/admin/products/${testProductId}/bundles`) + .set(adminAuthH.headers) + .send({ specName: '复活', quantity: 77, price: 2.0, stock: 20 }); + expect(reb.body.code).toBe(0); + expect(reb.body.data.id).toBe(id); // 关键:id 不变 + expect(reb.body.data.specName).toBe('复活'); + expect(reb.body.data.price).toBe('2.00'); + expect(reb.body.data.stock).toBe(20); + + // 4. 再建一次应当 409(因为现在该 (productId, quantity) 已有活跃行) + const dup = await req + .post(`/api/admin/products/${testProductId}/bundles`) + .set(adminAuthH.headers) + .send({ specName: 'dup', quantity: 77, price: 2.5, stock: 5 }); + expect(dup.body.code).toBe(409); + }); + + test('局部更新(PATCH)', async () => { + const create = await req + .post(`/api/admin/products/${testProductId}/bundles`) + .set(adminAuthH.headers) + .send({ specName: '30盒装', quantity: 30, price: 3.1, stock: 200 }); + createdIds.push(create.body.data.id); + + const upd = await req + .patch(`/api/admin/bundles/${create.body.data.id}`) + .set(adminAuthH.headers) + .send({ price: 2.9, stock: 250 }); + expect(upd.status).toBe(200); + expect(upd.body.code).toBe(0); + expect(upd.body.data.price).toBe('2.90'); + expect(upd.body.data.stock).toBe(250); + expect(upd.body.data.specName).toBe('30盒装'); // 未改字段保留 + }); + + test('上下架切换', async () => { + const create = await req + .post(`/api/admin/products/${testProductId}/bundles`) + .set(adminAuthH.headers) + .send({ specName: '50盒装', quantity: 50, price: 3.0, stock: 100 }); + createdIds.push(create.body.data.id); + + const off = await req + .patch(`/api/admin/bundles/${create.body.data.id}/on-sale`) + .set(adminAuthH.headers) + .query({ onSale: 0 }); + expect(off.body.code).toBe(0); + expect(off.body.data.onSale).toBe(0); + + const on = await req + .patch(`/api/admin/bundles/${create.body.data.id}/on-sale`) + .set(adminAuthH.headers) + .query({ onSale: 1 }); + expect(on.body.data.onSale).toBe(1); + }); + + test('软删 → 列表不再返回', async () => { + const create = await req + .post(`/api/admin/products/${testProductId}/bundles`) + .set(adminAuthH.headers) + .send({ specName: '待删', quantity: 99, price: 1, stock: 1 }); + const id = create.body.data.id; + createdIds.push(id); + + const del = await req.delete(`/api/admin/bundles/${id}`).set(adminAuthH.headers); + expect(del.body.code).toBe(0); + + const list = await req.get(`/api/admin/products/${testProductId}/bundles`).set(adminAuthH.headers); + expect(list.body.data.find((b) => b.id === id)).toBeUndefined(); + }); + + test('非 admin 鉴权失败 → 403', async () => { + const r = await req.get(`/api/admin/products/${testProductId}/bundles`).set(buyerAuthH.headers); + expect(r.body.code).toBe(403); + }); + + test('不存在的 product → 404', async () => { + const r = await req.get('/api/admin/products/999999999/bundles').set(adminAuthH.headers); + expect(r.body.code).toBe(404); + }); +}); +``` + +**Step 2: 跑测试** + +```bash +cd backend && npm test +``` + +预期:所有用例通过;`Tests: 8 passed`。 + +**Step 3: Commit** + +```bash +git add backend/tests/bundle.test.js +git commit -m "test(product): bundle 集成测试(CRUD + 鉴权 + 409 + 404)" +``` + +--- + +## Task 6: 前端 API client 新增 + +**Files:** +- Modify: `frontend/src/api/product.js` + +**Step 1: 在文件末尾追加 5 个函数** + +在 `frontend/src/api/product.js` 文件最末尾追加: + +```js +// 后台规格(product_bundle) +export const adminListBundles = (productId) => http.get(`/admin/products/${productId}/bundles`); +export const adminCreateBundle = (productId, data) => http.post(`/admin/products/${productId}/bundles`, data); +export const adminUpdateBundle = (bundleId, data) => http.patch(`/admin/bundles/${bundleId}`, data); +export const adminSetBundleOnSale = (bundleId, onSale) => + http.patch(`/admin/bundles/${bundleId}/on-sale`, null, { params: { onSale } }); +export const adminDeleteBundle = (bundleId) => http.delete(`/admin/bundles/${bundleId}`); +``` + +**Step 2: 验证 build 通过** + +```bash +cd frontend && pnpm build +``` + +预期:`✓ built in s`,无错误。 + +**Step 3: Commit** + +```bash +git add frontend/src/api/product.js +git commit -m "feat(api): 新增 admin bundle 5 个接口封装" +``` + +--- + +## Task 7: ProductManage.vue 编辑弹窗下半部规格列表 + +**Files:** +- Modify: `frontend/src/views/product/admin/ProductManage.vue` + +**Step 1: 在 ` + + diff --git a/frontend/src/api/admin.js b/frontend/src/api/admin.js index 96435effc47c59bbf7a2c9ba08e0295959e53eec..92dbff911449af79a13f6bd85a8ef97b20b0432e 100644 --- a/frontend/src/api/admin.js +++ b/frontend/src/api/admin.js @@ -7,6 +7,14 @@ import http from './index'; // 操作日志(M09) export const listOperationLogs = (params) => http.get('/admin/operation-logs', { params }); +export const exportOperationLogsUrl = (params = {}) => { + const qs = new URLSearchParams(); + Object.entries(params).forEach(([k, v]) => { + if (v !== '' && v !== null && v !== undefined) qs.set(k, v); + }); + const base = import.meta.env.VITE_API_BASE_URL || '/api'; + return `${base}/admin/operation-logs/export${qs.toString() ? `?${qs}` : ''}`; +}; -// 仪表盘统计(C09) -export const getDashboardStats = () => http.get('/admin/stats'); \ No newline at end of file +// 仪表盘统计(C09);可选 ?weekOffset: 0=本周,-1=上周,1=下周 +export const getDashboardStats = (params = {}) => http.get('/admin/stats', { params }); \ No newline at end of file diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js index e43ff3eb9bc2681199df7be25e61c6f1f3c2d5ca..b12e05b6a145e3f39173f0f18fadee4c3eb04a8d 100644 --- a/frontend/src/api/index.js +++ b/frontend/src/api/index.js @@ -40,7 +40,8 @@ http.interceptors.response.use( return body.data; // 返回后端的 data 本体(数组 / 对象) }, (err) => { - ElMessage.error(err.message || '网络异常'); + const msg = err.response?.data?.message || err.message || '网络异常'; + ElMessage.error(msg); return Promise.reject(err); } ); diff --git a/frontend/src/api/order.js b/frontend/src/api/order.js index 8764df3133d76ab9a0ed4e19923322a2def49320..46ba430ef85b9dbe95485f89bb83f11fe20c264e 100644 --- a/frontend/src/api/order.js +++ b/frontend/src/api/order.js @@ -2,20 +2,32 @@ * 订单支付域 API(C 陈杨文负责) * - 订单:提交 / 列表 / 详情 / 取消 / 确认收货 * - 支付:模拟支付 / 支付回调 - * - 后台订单:列表 / 发货 / 改状态 + * - 后台订单:列表 / 详情 / 发货 / 完成 */ import http from './index'; +import { useUserStore } from '@/stores/userStore'; // 买家订单 export const createOrder = (data) => http.post('/orders', data); export const listOrders = (params) => http.get('/orders', { params }); -export const getOrder = (id) => http.get(`/orders/${id}`); +/** + * 订单详情:按当前用户角色自动选择 URL + * - buyer → /api/orders/:id (校验 userId,仅看自己) + * - admin → /api/admin/orders/:id (不校验 userId,可看任意) + */ +export const getOrder = (id) => { + const userStore = useUserStore(); + const url = userStore.isAdmin ? `/admin/orders/${id}` : `/orders/${id}`; + return http.get(url); +}; +export const getOrderStatus = (id) => http.get(`/orders/${id}/status`); export const cancelOrder = (id) => http.post(`/orders/${id}/cancel`); export const confirmOrder = (id) => http.post(`/orders/${id}/confirm`); +export const deleteOrder = (id) => http.delete(`/orders/${id}`); // 支付 export const mockPay = (data) => http.post('/pay/mock', data); // 后台订单 export const adminListOrders = (params) => http.get('/admin/orders', { params }); export const adminShipOrder = (id) => http.post(`/admin/orders/${id}/ship`); -export const adminSetOrderStatus = (id, status) => http.put(`/admin/orders/${id}/status`, null, { params: { status } }); +export const adminCompleteOrder = (id) => http.post(`/admin/orders/${id}/complete`); diff --git a/frontend/src/api/product.js b/frontend/src/api/product.js index 714bb3b68765fc3a370824d11d35d71fff9894c9..39a87e95dd31d1274d7681b32dc83078e42110f2 100644 --- a/frontend/src/api/product.js +++ b/frontend/src/api/product.js @@ -1,6 +1,6 @@ /** * 商品域 API(B 江禹辰负责) - * - 前台:分类 / 列表 / 搜索 / 详情 + * - 前台:分类 / 列表 / 搜索 / 详情 / 首页数据 * - 后台:商品 CRUD + 上下架 + 分类 CRUD */ import http from './index'; @@ -12,6 +12,13 @@ export const getCategories = () => http.get('/categories'); export const listProducts = (params) => http.get('/products', { params }); export const searchProducts = (q) => http.get('/products/search', { params: { q } }); export const getProduct = (id) => http.get(`/products/${id}`); +export const getProductSkus = (id) => http.get(`/products/${id}/skus`); + +// 首页数据 +export const getHotProducts = (limit = 10) => http.get('/products/hot', { params: { limit } }); +export const getNewProducts = (limit = 10) => http.get('/products/new', { params: { limit } }); +export const getRecommendProducts = (limit = 10) => http.get('/products/recommend', { params: { limit } }); +export const getHomeData = () => http.get('/products/home'); // 后台商品 export const adminListProducts = (params) => http.get('/admin/products', { params }); @@ -25,3 +32,14 @@ export const adminListCategories = (params) => http.get('/admin/categories', { p export const adminCreateCategory = (data) => http.post('/admin/categories', data); export const adminUpdateCategory = (id, data) => http.put(`/admin/categories/${id}`, data); export const adminDeleteCategory = (id) => http.delete(`/admin/categories/${id}`); + +// 后台规格(product_bundle) +export const adminListBundles = (productId) => http.get(`/admin/products/${productId}/bundles`); +export const adminCreateBundle = (productId, data) => http.post(`/admin/products/${productId}/bundles`, data); +export const adminUpdateBundle = (bundleId, data) => http.patch(`/admin/bundles/${bundleId}`, data); +export const adminSetBundleOnSale = (bundleId, onSale) => + http.patch(`/admin/bundles/${bundleId}/on-sale`, null, { params: { onSale } }); +export const adminDeleteBundle = (bundleId) => http.delete(`/admin/bundles/${bundleId}`); + +// 前台:列出某商品在售的 bundle +export const listProductBundles = (productId) => http.get(`/products/${productId}/bundles`); diff --git a/frontend/src/api/user.js b/frontend/src/api/user.js index a8fde766bb5dca56f984334cb0ebbaa20a2a8cc9..7ba7af50c76b73f087a0c521c3fc6baeec055073 100644 --- a/frontend/src/api/user.js +++ b/frontend/src/api/user.js @@ -8,33 +8,57 @@ */ import http from './index'; -// 鉴权 +// ===================== 鉴权 ===================== export const login = (data) => http.post('/auth/login', data); export const register = (data) => http.post('/auth/register', data); export const logout = () => http.post('/auth/logout'); export const me = () => http.get('/auth/me'); -// 个人信息 -export const getProfile = () => http.get('/users/me'); -export const updateProfile = (data) => http.put('/users/me', data); +// ===================== 个人信息 ===================== +export const getProfile = () => http.get('/user/profile'); +export const updateProfile = (data) => http.put('/user/profile', data); +export const updatePassword = (data) => http.put('/user/password', data); -// 收货地址 -export const listAddresses = () => http.get('/addresses'); -export const createAddress = (data) => http.post('/addresses', data); -export const updateAddress = (id, data) => http.put(`/addresses/${id}`, data); -export const deleteAddress = (id) => http.delete(`/addresses/${id}`); -export const setDefaultAddress = (id) => http.post(`/addresses/${id}/default`); +// ===================== 收货地址 ===================== +export const getAddresses = () => http.get('/addresses'); +export const addAddress = (data) => http.post('/address', data); +export const updateAddress = (id, data) => http.put(`/address/${id}`, data); +export const deleteAddress = (id) => http.delete(`/address/${id}`); +export const setDefaultAddress = (id) => http.patch(`/address/${id}/default`); +export const cancelDefaultAddress = (id) => http.patch(`/address/${id}/cancel-default`); -// 购物车 -export const addToCart = (data) => http.post('/cart', data); +// ===================== 购物车 ===================== export const getCart = () => http.get('/cart'); -export const updateCartItem = (id, quantity) => http.put(`/cart/${id}`, { quantity }); +export const addCart = (data) => http.post('/cart', data); +export const updateCartQty = (id, qty) => http.put(`/cart/${id}`, { quantity: qty }); export const deleteCartItem = (id) => http.delete(`/cart/${id}`); -// S04 购物车结算快照(接缝接口,D 提供 C 调用) -export const checkoutCart = (ids) => http.get('/cart/checkout', { params: { ids } }); +export const toggleCartSelect = (id, selected) => http.patch(`/cart/${id}/select`, { selected }); +export const clearCart = () => http.delete('/cart/clear'); +export const mergeCart = (items) => http.post('/cart/merge', { items }); +export const checkout = (ids) => http.get('/cart/checkout', { params: { ids: ids ? ids.join(',') : undefined } }); -// 后台登录 +// ===================== 后台登录 ===================== export const adminLogin = (data) => http.post('/admin/auth/login', data); -// 后台用户管理 + +// ===================== 后台用户管理 ===================== export const listAdminUsers = (params) => http.get('/admin/users', { params }); -export const updateUserStatus = (id, status) => http.put(`/admin/users/${id}/status`, null, { params: { status } }); +export const updateUserStatus = (id, status) => + http.put(`/admin/users/${id}/status`, null, { params: { status } }); + +// ===================== 收藏 ===================== +export const getFavorites = () => http.get('/favorites'); +export const addFavorite = (productId) => http.post('/favorites', { productId }); +export const deleteFavorite = (id) => http.delete(`/favorites/${id}`); + +// ===================== 浏览历史 ===================== +export const getBrowseHistory = () => http.get('/browse-history'); +export const addBrowseHistory = (productId) => http.post('/browse-history', { productId }); +export const clearBrowseHistory = () => http.delete('/browse-history'); + +// ===================== 兼容别名(其他模块引用旧名) ===================== +export const listAddresses = getAddresses; +export const createAddress = addAddress; +export const checkoutCart = checkout; +export const addToCart = addCart; +export const updateCartItem = updateCartQty; +export const mergeLocalCart = mergeCart; diff --git a/frontend/src/assets/home/banner-1.svg b/frontend/src/assets/home/banner-1.svg new file mode 100644 index 0000000000000000000000000000000000000000..a5aca86d2b21f973a97a05df4ff9480ed6059175 --- /dev/null +++ b/frontend/src/assets/home/banner-1.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/frontend/src/assets/home/banner-2.svg b/frontend/src/assets/home/banner-2.svg new file mode 100644 index 0000000000000000000000000000000000000000..3e3c3ceec06903e43edab91692c5e8558940fd99 --- /dev/null +++ b/frontend/src/assets/home/banner-2.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/frontend/src/assets/home/banner-3.svg b/frontend/src/assets/home/banner-3.svg new file mode 100644 index 0000000000000000000000000000000000000000..50ef17fe40ee5e654f4c13da9703890299857ef6 --- /dev/null +++ b/frontend/src/assets/home/banner-3.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/frontend/src/assets/home/promo-1.svg b/frontend/src/assets/home/promo-1.svg new file mode 100644 index 0000000000000000000000000000000000000000..02ff840dfd844e2f15e70e45e33daaf984bba96c --- /dev/null +++ b/frontend/src/assets/home/promo-1.svg @@ -0,0 +1,8 @@ + + + + + + + 家居 + diff --git a/frontend/src/assets/home/promo-2.svg b/frontend/src/assets/home/promo-2.svg new file mode 100644 index 0000000000000000000000000000000000000000..144b7e42daa8365a3c936fcecb07ff2c001f3c81 --- /dev/null +++ b/frontend/src/assets/home/promo-2.svg @@ -0,0 +1,8 @@ + + + + + + + 美妆 + diff --git a/frontend/src/assets/home/promo-3.svg b/frontend/src/assets/home/promo-3.svg new file mode 100644 index 0000000000000000000000000000000000000000..78323d17e1b6409f156d55cd300c22d89deb295e --- /dev/null +++ b/frontend/src/assets/home/promo-3.svg @@ -0,0 +1,8 @@ + + + + + + + 五金 + diff --git a/frontend/src/assets/home/promo-4.svg b/frontend/src/assets/home/promo-4.svg new file mode 100644 index 0000000000000000000000000000000000000000..eb942fa7c1418a8634a67e3829d39464b56950b8 --- /dev/null +++ b/frontend/src/assets/home/promo-4.svg @@ -0,0 +1,8 @@ + + + + + + + 百货 + diff --git a/frontend/src/components/AddressEditDialog.vue b/frontend/src/components/AddressEditDialog.vue new file mode 100644 index 0000000000000000000000000000000000000000..39ff40c3efb43890c02e19cfebf4fb9b7b422a3f --- /dev/null +++ b/frontend/src/components/AddressEditDialog.vue @@ -0,0 +1,114 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/AdminPager.vue b/frontend/src/components/AdminPager.vue new file mode 100644 index 0000000000000000000000000000000000000000..811efc33ef742d3052666b518c78bcbef98c1205 --- /dev/null +++ b/frontend/src/components/AdminPager.vue @@ -0,0 +1,75 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/home/CategorySidebar.vue b/frontend/src/components/home/CategorySidebar.vue new file mode 100644 index 0000000000000000000000000000000000000000..d95db2936cfef1eb263a68c05d69c614f99bb229 --- /dev/null +++ b/frontend/src/components/home/CategorySidebar.vue @@ -0,0 +1,96 @@ + + + + + diff --git a/frontend/src/components/home/FloatSidebar.vue b/frontend/src/components/home/FloatSidebar.vue new file mode 100644 index 0000000000000000000000000000000000000000..6cd5d6a2b9df04b9cf1922d1c680e71e29fea449 --- /dev/null +++ b/frontend/src/components/home/FloatSidebar.vue @@ -0,0 +1,139 @@ + + + + + diff --git a/frontend/src/components/home/Footer.vue b/frontend/src/components/home/Footer.vue new file mode 100644 index 0000000000000000000000000000000000000000..1db22d16336fbd09f44dd3111ce08dfef0386566 --- /dev/null +++ b/frontend/src/components/home/Footer.vue @@ -0,0 +1,107 @@ + + + + + diff --git a/frontend/src/components/home/GuessYouLike.vue b/frontend/src/components/home/GuessYouLike.vue new file mode 100644 index 0000000000000000000000000000000000000000..8826bbfb9eb08d3954a8226bb1dd52abe435441b --- /dev/null +++ b/frontend/src/components/home/GuessYouLike.vue @@ -0,0 +1,138 @@ + + + + + diff --git a/frontend/src/components/home/HeroBanner.vue b/frontend/src/components/home/HeroBanner.vue new file mode 100644 index 0000000000000000000000000000000000000000..9eb77f96bc78803e62bde3ef3ea697ea6a8240e2 --- /dev/null +++ b/frontend/src/components/home/HeroBanner.vue @@ -0,0 +1,98 @@ + + + + + diff --git a/frontend/src/components/home/PromoCards.vue b/frontend/src/components/home/PromoCards.vue new file mode 100644 index 0000000000000000000000000000000000000000..535fc68ca5cd6bc389b5afdeb7707b8ed77752e4 --- /dev/null +++ b/frontend/src/components/home/PromoCards.vue @@ -0,0 +1,116 @@ + + + + + diff --git a/frontend/src/components/home/SearchBar.vue b/frontend/src/components/home/SearchBar.vue new file mode 100644 index 0000000000000000000000000000000000000000..bcff40f1178609ff03811b9c81469e4a01a2c00d --- /dev/null +++ b/frontend/src/components/home/SearchBar.vue @@ -0,0 +1,126 @@ + + + + + diff --git a/frontend/src/composables/useChart.js b/frontend/src/composables/useChart.js new file mode 100644 index 0000000000000000000000000000000000000000..66be1c155833d1bddcc90712482fb1abc45c6148 --- /dev/null +++ b/frontend/src/composables/useChart.js @@ -0,0 +1,51 @@ +/** + * useChart(elRef, optionRef) + * - elRef: Vue ref 指向
+ * - optionRef: Ref / ComputedRef 返回的 ECharts option + * 自动按容器大小初始化、option 变更时 setOption、卸载时 dispose。 + * + * 兼容性:支持"组件挂载时容器还未在 DOM(典型场景:v-if="stats",stats 异步加载)"。 + * onMounted 时容器可能还没出现,init 会被延后到首次拿到合法 option + 容器的时刻。 + */ +import { onMounted, onBeforeUnmount, watch, nextTick } from 'vue'; +import * as echarts from 'echarts/core'; + +export function useChart(elRef, optionRef) { + let instance = null; + + function ensureInit() { + if (instance) return instance; + if (!elRef.value) return null; + instance = echarts.init(elRef.value); + return instance; + } + + function applyOption(opt) { + if (!opt) return; + const inst = ensureInit(); + if (inst) inst.setOption(opt, true); + } + + function handleResize() { + if (instance) instance.resize(); + } + + onMounted(() => { + window.addEventListener('resize', handleResize); + // 容器已在 DOM(比如 el-card 不在 v-if 内):直接初始化 + applyOption(optionRef.value); + }); + + // 容器是异步出现的情况:option 一旦就绪(通常意味着模板已经渲染),先等 DOM 更新再 init + watch(optionRef, (opt) => { + nextTick(() => applyOption(opt)); + }, { flush: 'post' }); + + onBeforeUnmount(() => { + window.removeEventListener('resize', handleResize); + if (instance) { + instance.dispose(); + instance = null; + } + }); +} \ No newline at end of file diff --git a/frontend/src/config/homeNav.js b/frontend/src/config/homeNav.js new file mode 100644 index 0000000000000000000000000000000000000000..b46311296f1fe5c1fe894f33e3f4c120c63db427 --- /dev/null +++ b/frontend/src/config/homeNav.js @@ -0,0 +1,42 @@ +/** + * 首页顶部快捷入口 + Banner 静态数据 + * - 任何一项 icon 字段只能用 Element Plus Icons 名称(首字母大写) + * - 业务无关的入口也用本表占位,避免后端再补一张 navigation 表 + */ +export const topNavItems = [ + { key: 'subsidy', icon: 'Coin', label: '商城补贴', q: '补贴' }, + { key: 'seckill', icon: 'Timer', label: '限时秒杀', q: '秒杀' }, + { key: 'trial', icon: 'Present', label: '试用中心', q: '试用' }, + { key: 'official', icon: 'Medal', label: '官方补贴', q: '官方补贴' }, + { key: 'coupon', icon: 'Discount', label: '领券中心', q: '优惠券' }, + { key: 'brand', icon: 'Star', label: '品牌专区', q: '品牌' }, + { key: 'member', icon: 'Trophy', label: '会员聚惠', q: '会员' }, +] + +/** + * Hero Banner 数据(3 张) + * - link 字段若以 'category:' 开头,组件中转为 /category/:id;否则直接作 query 跳 /search + */ +export const heroBanners = [ + { + key: 'home-appliance', + title: '智能家电 品质生活', + subtitle: '品类齐全 现货热销', + bg: 'linear-gradient(135deg, #7e57ff 0%, #9575ff 100%)', + link: 'category:1', + }, + { + key: 'digital', + title: '数码精选 焕新生活', + subtitle: '手机电脑 智能穿戴', + bg: 'linear-gradient(135deg, #1e9cff 0%, #4ab9ff 100%)', + link: 'category:2', + }, + { + key: 'maternal', + title: '母婴童趣 用心呵护', + subtitle: '优选好物 安心选择', + bg: 'linear-gradient(135deg, #ff7e00 0%, #ffaa55 100%)', + link: 'category:3', + }, +] \ No newline at end of file diff --git a/frontend/src/layouts/FrontLayout.vue b/frontend/src/layouts/FrontLayout.vue index f34b844e5f9ba7450cc2e3b10f0e63bd266ae4ed..a386eccccca74abb9caa0bf7dd126f437047614c 100644 --- a/frontend/src/layouts/FrontLayout.vue +++ b/frontend/src/layouts/FrontLayout.vue @@ -10,7 +10,17 @@
@@ -49,6 +65,6 @@ onMounted(load); padding: 10px 12px; cursor: pointer; border-radius: 4px; margin-bottom: 4px; } -.cat-tree li:hover { background: #f5f7fa; } -.cat-tree li.active { background: #ecf5ff; color: #409eff; font-weight: bold; } +.cat-tree li:hover { background: #fff5f0; color: #ff5000; } +.cat-tree li.active { background: #fff5f0; color: #ff5000; font-weight: bold; } \ No newline at end of file diff --git a/frontend/src/views/product/ProductDetail.vue b/frontend/src/views/product/ProductDetail.vue index 409d6b2e83b96841d4581ef53ed434e562506d88..14b27a1f32fe87d6e31e598352043299842ab0bf 100644 --- a/frontend/src/views/product/ProductDetail.vue +++ b/frontend/src/views/product/ProductDetail.vue @@ -1,31 +1,141 @@ \ No newline at end of file diff --git a/frontend/src/views/product/ProductList.vue b/frontend/src/views/product/ProductList.vue index 3feb4b4ff8dab7a074a7f5beac9ca1758509fbd8..41efb322df826d1b81233c804ed177f7f1be023a 100644 --- a/frontend/src/views/product/ProductList.vue +++ b/frontend/src/views/product/ProductList.vue @@ -26,7 +26,7 @@ diff --git a/frontend/src/views/product/admin/CategoryManage.vue b/frontend/src/views/product/admin/CategoryManage.vue index c102b9edbd934355e6d2a70c9316e7d3855d1f0a..ea8c0f09f3cc112f299f11418974574fc98f71b1 100644 --- a/frontend/src/views/product/admin/CategoryManage.vue +++ b/frontend/src/views/product/admin/CategoryManage.vue @@ -2,9 +2,24 @@

分类管理

- - 新增分类 - +
+ + 新增分类 + + + + + 搜索 +
@@ -21,6 +36,17 @@ + + + @@ -47,51 +73,74 @@ diff --git a/frontend/src/views/user/AddressEdit.vue b/frontend/src/views/user/AddressEdit.vue index d52ca9fb286d7d6a567876f0f171b564f131ca1a..3db468125128d689df4220fc9fff75a76a9352cf 100644 --- a/frontend/src/views/user/AddressEdit.vue +++ b/frontend/src/views/user/AddressEdit.vue @@ -10,14 +10,13 @@ - - - - - - - - + + @@ -35,15 +34,17 @@ \ No newline at end of file +.address-list { padding: 20px 24px; } +.breadcrumb { max-width: 1400px; margin: 0 auto 20px; } +.header { max-width: 1400px; margin: 0 auto 28px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 12px; } +.header h2 { margin: 0; font-size: 22px; font-weight: bold; } +.cards { max-width: 1400px; margin: 0 auto; display: grid; grid-template-columns: repeat(auto-fill, minmax(420px, 1fr)); gap: 18px; } + +.address-card { + background: #fff; + border: 1px solid #ebeef5; + border-radius: 12px; + overflow: hidden; + transition: box-shadow 0.25s, border-color 0.25s; + display: flex; + flex-direction: column; + min-height: 240px; +} +.address-card:hover { box-shadow: 0 4px 20px rgba(0,0,0,0.08); } +.address-card.is-default { border-color: #67c23a; } +.address-card.is-default .card-header { background: linear-gradient(135deg, #67c23a14 0%, #f0f9eb 100%); } + +/* 卡片头部:姓名 + 手机 */ +.card-header { + padding: 28px 26px 22px; + border-bottom: 1px dashed #f0f0f0; +} +.receiver { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 12px; +} +.receiver .name { font-size: 18px; font-weight: 600; color: #303133; letter-spacing: 1px; } +.default-tag { font-size: 11px; padding: 0 8px; height: 22px; line-height: 20px; border-radius: 11px; } +.phone { + display: flex; + align-items: center; + gap: 6px; + font-size: 15px; + color: #909399; + letter-spacing: 0.5px; + padding-left: 2px; +} + +/* 卡片主体:地址 */ +.card-body { padding: 22px 24px; flex: 1; } +.address-row { + display: flex; + align-items: flex-start; + gap: 10px; +} +.address-icon { color: #67c23a; flex-shrink: 0; margin-top: 4px; } +.address-text { display: flex; flex-direction: column; gap: 8px; } +.address-text .region { font-size: 15px; color: #606266; font-weight: 500; } +.address-text .detail { font-size: 14px; color: #909399; line-height: 1.8; } + +/* 卡片底部:操作按钮 */ +.card-footer { + padding: 16px 24px; + border-top: 1px solid #f5f5f5; + display: flex; + gap: 6px; + flex-wrap: wrap; + background: #fafafa; +} +.card-footer .el-button { font-size: 13px; } + +@media (max-width: 480px) { + .cards { grid-template-columns: 1fr; } + .header { flex-direction: column; align-items: flex-start; } + .address-form :deep(.el-form-item__label) { width: 70px !important; } + .address-form :deep(.el-form-item__content) { margin-left: 70px !important; } +} + diff --git a/frontend/src/views/user/Cart.vue b/frontend/src/views/user/Cart.vue index 7db50b3935fbadfc33029dda394b06e69f57c370..aafd3b822e772874b22020a2198770c4b823f1f5 100644 --- a/frontend/src/views/user/Cart.vue +++ b/frontend/src/views/user/Cart.vue @@ -1,75 +1,462 @@ \ No newline at end of file + diff --git a/frontend/src/views/user/FavoriteList.vue b/frontend/src/views/user/FavoriteList.vue new file mode 100644 index 0000000000000000000000000000000000000000..766c20d7a861355bca1bed61a7fd8c88faef8cba --- /dev/null +++ b/frontend/src/views/user/FavoriteList.vue @@ -0,0 +1,422 @@ + + + + + diff --git a/frontend/src/views/user/Login.vue b/frontend/src/views/user/Login.vue index ece8403da89ba8e27e04f815245dc58970c15cf6..8b15fcbcf247bbef86dfaee072deb36e4aa3032b 100644 --- a/frontend/src/views/user/Login.vue +++ b/frontend/src/views/user/Login.vue @@ -1,51 +1,474 @@ diff --git a/frontend/src/views/user/Profile.vue b/frontend/src/views/user/Profile.vue index 51b8a72e3e0d111f8e614ce849617b5c2efd3bad..fc29d5f561daf183f29b9053b2e6c3c921268176 100644 --- a/frontend/src/views/user/Profile.vue +++ b/frontend/src/views/user/Profile.vue @@ -1 +1,341 @@ - + + + + + diff --git a/frontend/src/views/user/Register.vue b/frontend/src/views/user/Register.vue index a15aa00f5d0a81336dd042376c90ba758431149d..4bcb7cd253771f877fc40200984b6dddedff4471 100644 --- a/frontend/src/views/user/Register.vue +++ b/frontend/src/views/user/Register.vue @@ -1,73 +1,522 @@ \ No newline at end of file +.register-page { + min-height: 100vh; + display: flex; + flex-direction: column; + background: #fff; + position: relative; + overflow: hidden; +} + +/* 背景装饰 */ +.bg-deco { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + z-index: 0; +} + +.circle { + position: absolute; + border-radius: 50%; + opacity: 0.25; +} + +.c1 { + width: 250px; + height: 250px; + background: #FF5000; + top: 10%; + left: -80px; + animation: moveCircle1 12s ease-in-out infinite; +} + +.c2 { + width: 180px; + height: 180px; + background: #FF6B2C; + bottom: 15%; + right: 5%; + animation: moveCircle2 10s ease-in-out infinite; +} + +.c3 { + width: 120px; + height: 120px; + background: #FFB399; + top: 60%; + left: 15%; + animation: moveCircle3 14s ease-in-out infinite; +} + +.wave { + position: absolute; + border: 2px solid rgba(255, 80, 0, 0.2); + border-radius: 50%; +} + +.w1 { + width: 350px; + height: 350px; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + animation: waveExpand 6s ease-out infinite; +} + +.w2 { + width: 450px; + height: 450px; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + animation: waveExpand 6s ease-out 2s infinite; +} + +@keyframes moveCircle1 { + 0%, 100% { transform: translate(0, 0); } + 50% { transform: translate(60px, 40px); } +} + +@keyframes moveCircle2 { + 0%, 100% { transform: translate(0, 0); } + 50% { transform: translate(-40px, -30px); } +} + +@keyframes moveCircle3 { + 0%, 100% { transform: translate(0, 0) scale(1); } + 50% { transform: translate(-30px, 50px) scale(1.1); } +} + +@keyframes waveExpand { + 0% { + transform: translate(-50%, -50%) scale(0.8); + opacity: 0.15; + } + 100% { + transform: translate(-50%, -50%) scale(1.5); + opacity: 0; + } +} + +/* 顶部 */ +.header { + height: 80px; + border-bottom: 1px solid #f0f0f0; + position: relative; + z-index: 1; +} + +/* 主体 */ +.main { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + padding: 50px 20px; + position: relative; + z-index: 1; +} + +.register-box { + width: 420px; + background: #fff; + padding: 32px 40px; + border-radius: 8px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); +} + +.register-title { + font-size: 26px; + font-weight: 600; + color: #333; + margin: 0 0 28px; + padding-bottom: 16px; + border-bottom: 2px solid #FF5000; + display: inline-block; +} + +.register-form { + margin-bottom: 16px; +} + +.register-form :deep(.el-form-item) { + margin-bottom: 16px; +} + +.strength-wrap { + display: flex; + align-items: center; + gap: 10px; + margin: -8px 0 16px; + padding: 0 2px; +} + +.strength-bar { + flex: 1; +} + +.strength-label { + font-size: 12px; + font-weight: 600; + min-width: 20px; +} + +.input-wrap { + display: flex; + align-items: center; + width: 100%; + border: 1px solid #ddd; + border-radius: 4px; + padding: 0 4px 0 12px; + background: #fff; + transition: border-color 0.2s, box-shadow 0.2s; + box-sizing: border-box; +} + +.input-wrap:hover { + border-color: #bbb; +} + +.input-wrap:focus-within { + border-color: #FF5000; + box-shadow: 0 0 0 3px rgba(255, 80, 0, 0.08); +} + +.input-icon { + width: 20px; + height: 20px; + flex-shrink: 0; + margin-right: 8px; +} + +.input-icon svg { + width: 100%; + height: 100%; +} + +.register-input { + flex: 1; +} + +.register-input :deep(.el-input__wrapper) { + border: none; + box-shadow: none; + padding: 0; + background: transparent; +} + +.register-input :deep(.el-input__inner) { + height: 42px; + font-size: 14px; +} + +.register-btn { + width: 100%; + height: 48px; + font-size: 16px; + font-weight: 600; + letter-spacing: 2px; + background: #FF5000; + border: none; + border-radius: 4px; + color: #fff; + cursor: pointer; + transition: all 0.2s; +} + +.register-btn:hover { + background: #FF6B2C; +} + +.register-btn.ready { + background: #FF5000; + box-shadow: 0 4px 12px rgba(255, 80, 0, 0.35); +} + +.register-btn:active { + background: #E64A00; +} + +.register-btn:disabled { + background: #FFB399; + cursor: not-allowed; +} + +.login-line { + font-size: 13px; + color: #666; + text-align: center; +} + +.login-line a { + color: #FF5000; + text-decoration: none; + margin-left: 4px; +} + +.login-line a:hover { + text-decoration: underline; +} + +/* 底部 */ +.footer { + padding: 20px; + text-align: center; + border-top: 1px solid #f0f0f0; + position: relative; + z-index: 1; +} + +.footer a { + color: #999; + font-size: 12px; + text-decoration: none; + transition: color 0.2s; +} + +.footer a:hover { + color: #FF5000; +} + +.footer .dot { + color: #ddd; + margin: 0 8px; +} + diff --git a/frontend/src/views/user/admin/UserManage.vue b/frontend/src/views/user/admin/UserManage.vue index da7d3de40b1c583289d6a75864b32e2945b06a4e..a1117dfe9c10dcddffc5c692dd8612bfbc467d1c 100644 --- a/frontend/src/views/user/admin/UserManage.vue +++ b/frontend/src/views/user/admin/UserManage.vue @@ -1 +1,117 @@ - + + + + + diff --git "a/reports/daily/\344\275\231\346\263\275/\346\227\245\346\212\245-\344\275\231\346\263\275-2026-07-23.md" "b/reports/daily/\344\275\231\346\263\275/20260723-\344\275\231\346\263\275.md" similarity index 100% rename from "reports/daily/\344\275\231\346\263\275/\346\227\245\346\212\245-\344\275\231\346\263\275-2026-07-23.md" rename to "reports/daily/\344\275\231\346\263\275/20260723-\344\275\231\346\263\275.md" diff --git "a/reports/daily/\344\275\231\346\263\275/\346\227\245\346\212\245-\344\275\231\346\263\275-2026-07-24.md" "b/reports/daily/\344\275\231\346\263\275/20260724-\344\275\231\346\263\275.md" similarity index 100% rename from "reports/daily/\344\275\231\346\263\275/\346\227\245\346\212\245-\344\275\231\346\263\275-2026-07-24.md" rename to "reports/daily/\344\275\231\346\263\275/20260724-\344\275\231\346\263\275.md" diff --git "a/reports/daily/\344\275\231\346\263\275/20260727-\344\275\231\346\263\275.md" "b/reports/daily/\344\275\231\346\263\275/20260727-\344\275\231\346\263\275.md" new file mode 100644 index 0000000000000000000000000000000000000000..7cfd51476fd5dc7403de3cf68242259495fc46d1 --- /dev/null +++ "b/reports/daily/\344\275\231\346\263\275/20260727-\344\275\231\346\263\275.md" @@ -0,0 +1,57 @@ +# 日报 - 余泽 - 2026-07-27 + +## 今日完成 + +### 后端 · 用户模块 + +1. **User 模型** — 补充 `createTime`、`updateTime` 字段,`timestamps: false` +2. **authService** — bcrypt 注册(saltRounds=10)+ 登录校验(status=0 禁用检查) +3. **authController** — 登录返回扁平结构 `{ token, userId, username }` +4. **鉴权中间件** — `authMiddleware` 补充 `req.userId`,失败返回 `401 '请先登录'` +5. **profileController** — 实现 `updatePassword`(验证旧密码 + bcrypt 更新) +6. **userService** — 添加 `changePassword` 方法 +7. **路由调整** — `/users/me` → `/user/profile`、`/user/password` + +### 后端 · 地址模块 + +8. **Address 模型** — 重写字段:`receiverName`、`receiverPhone`、`detailAddress`、`isDefault`(BOOLEAN)、`createTime`、`updateTime` +9. **addressService** — `findAllByUser`/`create`/`update`/`delete`/`setDefault`,事务处理 isDefault 互斥 +10. **addressController** — 适配新字段名,schema 校验更新 +11. **路由挂载** — GET `/addresses`、POST `/address`、PUT/DELETE/PATCH `/address/:id` + +### 前端 + +12. **Login.vue** — 渐变背景(`#b8c6e2 → #d4dff6`)、居中卡片、`el-input` 前缀图标、表单校验、localCart 合并 +13. **Register.vue** — 同风格、密码强度指示器(弱/中/强)、手机号 11 位限制 +14. **API 路径修复** — 地址/个人信息/密码修改接口路径与后端对齐 +15. **cartStore** — 新增 `mergeLocalCart` 方法 +16. **userStore** — 修复 login 响应字段映射 +17. **axios 拦截器** — 错误显示后端具体 message +![登录页面](imgs/0727-登录.png) +![注册页面](imgs/0727-注册.png) +### 基础设施 + +18. **后端服务** — 修复 Redis 3 秒超时 + 优雅降级、MySQL 密码配置、registerSchema 校验规则(3-20 位) + +## 遇到的问题 + +| 问题描述 | 解决状态 | 解决方式/求助对象 | +|----------|----------|-------------------| +| 后端启动时 MySQL 密码错误(`eshop` 用户不存在) | 已解决 | 改用 root 用户 + 正确密码 | +| Redis 不可用导致 `testRedis()` 永不返回,服务无法启动 | 已解决 | 给 `redis.connect()` 添加 3 秒超时,Redis 不可用时 warn 并继续启动 | +| 路由路径与方法名不匹配(`/users/me` vs `/user/profile`) | 已解决 | 统一调整为提示词要求的路径和方法名 | +| Address 模型字段名不匹配(`receiver`/`phone`/`detail` → `receiverName`/`receiverPhone`/`detailAddress`) | 已解决 | 执行 `ALTER TABLE` 添加新列、迁移数据、删除旧列 | +| 后端 `registerSchema` 要求用户名 4 位 + alphanum,与前端 3-20 位不一致导致 400 | 已解决 | 后端改为 3-20 位,移除 alphanum 限制 | +| axios 错误拦截器只显示"网络异常",后端具体错误信息被吞掉 | 已解决 | 改为读取 `err.response.data.message` 显示具体错误 | +| 前端注册 400 Bad Request 无法定位原因 | 已解决 | 修复后端 schema + 优化错误信息显示 | + +## 今日工时 + +约 8 小时 + +## 明日计划 + +1. 完成前端用户中心页面(个人信息页、收货地址列表/编辑页) +2. 完成后端购物车接口(CRUD + 结算) +3. 启动 Redis 服务,优化登录态管理 +4. 前后端联调测试 diff --git "a/reports/daily/\344\275\231\346\263\275/20260728-\344\275\231\346\263\275.md" "b/reports/daily/\344\275\231\346\263\275/20260728-\344\275\231\346\263\275.md" new file mode 100644 index 0000000000000000000000000000000000000000..6abd788e6e704d18b53d8554946a5313d2dd6ad9 --- /dev/null +++ "b/reports/daily/\344\275\231\346\263\275/20260728-\344\275\231\346\263\275.md" @@ -0,0 +1,62 @@ +# 日报 - 余泽 - 2026-07-28 + +## 今日完成 + +### 后端 · 购物车模块 + +1. **CartItem 模型** — 补充 `createTime`、`updateTime` 字段,`created_at`/`updated_at` 与数据库对齐,添加 `user_id + product_id` 唯一索引 +2. **cartService** — 实现全部方法:`getCart`、`addItem`(存在则累加)、`updateQty`、`deleteItem`、`toggleSelect`、`clear`、`merge`(事务)、`getCheckout` +3. **cartController** — 8 个路由全部实现,适配新 service 方法名 + +### 后端 · 收藏模块 + +4. **Favorite 模型** — 新建 `backend/src/modules/user/models/Favorite.js`,字段:id、userId、productId、createTime,唯一索引 user_id+product_id +5. **favoriteService** — `add`(findOrCreate)、`remove`(归属校验)、`list`(含 Product 关联) +6. **favoriteController** — POST/DELETE/GET `/api/favorites` +7. **路由挂载** — `/api/favorites` GET/POST/DELETE,已注册关联 + +### 后端 · 后台用户管理 + +8. **adminUserController** — `list`(分页+username/phone 模糊搜索)、`updateStatus`(parseInt 修复),role===1 鉴权 +9. **userService** — `listUsers` 支持 username/phone 分开模糊搜索 + +### 前端 · 用户模块 + +10. **user.js API 封装** — 完整封装 D 模块所有接口:auth、user、address、cart、favorites、admin +11. **userStore.js** — `isLoggedIn`、`userInfo`、`token` state,`isAdmin` getter,`login`/`logout`/`fetchProfile` +12. **cartStore.js** — `items`/`localCart` 双状态,未登录存 localStorage,已登录调 API,`mergeLocalCart` 合并逻辑 +13. **Login.vue** — 登录成功后 `mergeLocalCart`,清空 localStorage,跳转 `/cart` +14. **AddressList.vue** — 卡片布局、面包屑、新增/编辑弹窗、设为默认、删除,全国34省市自治区完整数据 +15. **FavoriteList.vue** — 收藏列表,含商品信息、取消按钮 + +![个人](imgs/个人中心.png) +### 基础设施 + +16. **Redis 安装** — winget install Redis.Redis,启动服务,验证 PONG +17. **数据库修复** — cart_item/user/product/category 表添加缺失的 `created_at`/`updated_at` 字段 +18. **admin 用户创建** — 手动 INSERT bcrypt 加密的 admin/ tester 账号 +19. **商品数据导入** — 手动 INSERT 分类+商品数据,修复外键约束问题 + +## 遇到的问题 + +| 问题描述 | 解决状态 | 解决方式/求助对象 | +|----------|----------|-------------------| +| Redis 未安装导致后端启动失败 | 已解决 | winget install Redis.Redis,后端 Redis unavailable 时 warn 并继续 | +| CartItem 模型字段名不一致(create_time vs created_at) | 已解决 | 修正 `field: 'created_at'`,重启 nodemon | +| 路由 /cart/clear 被 /:id 错误匹配 | 已解决 | 调整路由顺序,clear/merge/checkout 放在 /:id 前 | +| 数据库缺少 timestamp 列导致 INSERT 失败 | 已解决 | ALTER TABLE 添加 created_at/updated_at | +| 后台 admin 用户不存在 | 已解决 | 手动 INSERT bcrypt 加密的 admin 账号 | +| regionData.js 导入导致 Vite 编译 500 | 已解决 | 将省市区数据内联到 AddressList.vue,避免单独文件导入 | +| 前端 AddressList.vue 数组语法错误导致 500 | 已解决 | 重写整个文件,确保语法正确 | +| 400 Bad Request(地址接口) | 已解决 | 浏览器缓存旧 token,清除 localStorage 后重新登录 | + +## 今日工时 + +约 8 小时 + +## 明日计划 + +1. 前后端联调测试(登录→加购物车→结算) +2. 完成后端订单模块对接(收货地址→订单创建) +3. 前端订单确认页/支付页 +4. 收藏功能前后端联调 diff --git "a/reports/daily/\344\275\231\346\263\275/20260729-\344\275\231\346\263\275.md" "b/reports/daily/\344\275\231\346\263\275/20260729-\344\275\231\346\263\275.md" new file mode 100644 index 0000000000000000000000000000000000000000..87868dd25cfbd5ad66f58700aa895d75b8dabadc --- /dev/null +++ "b/reports/daily/\344\275\231\346\263\275/20260729-\344\275\231\346\263\275.md" @@ -0,0 +1,66 @@ +# 日报 - 余泽 - 2026-07-29 + +## 今日完成 + +1. **收货地址页面排版美化**(AddressList.vue) + - 增大卡片内边距和最小高度(200px→240px),卡片更修长 + - 姓名+手机号移至独立头部区域,层次更清晰;手机号脱敏显示(138****1234) + - 地址拆分为省市区 + 详细地址两级,配合绿色定位图标 + - 默认地址改用渐变背景 + 绿色边框,视觉更柔和 + - 卡片网格与页面标题"我的地址/新增地址"对齐到同一宽度(1400px) + - 分支:feature/user-cart + +2. **顶栏头像更换功能**(FrontLayout.vue) + - 顶栏右侧用户名旁新增头像显示,点击头像可上传本地图片更换 + - 悬停头像出现相机遮罩图标,和个人中心交互一致 + - 调用 `updateProfile` API 提交头像 URL 到后端 + +3. **头像持久化修复**(userStore.js + FrontLayout.vue + Profile.vue) + - 头像 URL 存入 localStorage(key: `avatar`),刷新页面不丢失 + - 登录时自动调用 `getProfile` 拉取完整用户信息(含头像),解决 blob 临时 URL 刷新失效问题 + - 新增 `setAvatar()` action,统一更新 state 和 localStorage + +4. **购物车页面完整样式重写**(Cart.vue) + - 页面标题"购物车"+ "共 X 件商品"计数 + - `el-card` 包裹表格;表头右侧全选复选框 + - 商品列:80×80 图片 + 名称+描述;单价/数量(el-input-number ±)/小计;红色删除按钮 + - Sticky 底部结算栏:左侧全选+已选件数,右侧红色合计+圆角结算按钮 + - 空状态:SVG 购物车图标 + "快去逛逛吧" + 去首页按钮 + - 未登录提示条可点击跳转登录页 + - 分支:feature/user-cart + +5. **cartStore.js bug 修复** + - `updateQty/toggleSelect/removeItem` 未登录时错误用 `id` 字段查找,统一改为 `productId` + - localStorage key 统一为 `eshop_cart`(与 Login.vue 保持一致) + - `mergeLocalCart` 成功后自动清空 localStorage,无需外部手动处理 + - 新增 `getSelectedCount()` 和 `getTotalPrice()` getter + - 分支:feature/user-cart + +6. **Login.vue 合并逻辑简化** + - 移除冗余的 `localStorage.getItem/removeItem` 手动操作 + - 直接调用 `cartStore.mergeLocalCart()`,内部已处理 key 读取和清空 + +7. **购物车加购失败修复**(ProductDetail.vue) + - `cart.add()` 方法名不存在,改为正确的 `cart.addToCart()` + - 后端接口验证通过:POST /api/cart 正常返回已加入购物车 + - 分支:feature/user-cart +![地址](imgs/地址管理.png) +## 遇到的问题 + +| 问题描述 | 解决状态 | 解决方式/求助对象 | +|----------|----------|-------------------| +| 收货地址卡片太短不够美观 | 已解决 | 增大 padding、min-height、卡片网格宽度 | +| 头像换页后消失 | 已解决 | 头像 URL 持久化到 localStorage,登录时拉取 profile | +| 购物车加购失败 | 已解决 | ProductDetail.vue 方法名 `cart.add()` → `cart.addToCart()` | +| cartStore 未登录时操作字段名错误 | 已解决 | 统一用 `productId` 作为未登录 localCart 的查找字段 | + +## 明日计划 + +1. 购物车页面接入结算流程(跳转到订单确认页) +2. 商品列表/分类页加入购物车按钮 +3. 收藏功能页面完善(FavoriteList.vue) +4. 个人中心"我的收藏"接入真实 API + +## 今日工时 + +约 7 小时, diff --git "a/reports/daily/\344\275\231\346\263\275/20260730-\344\275\231\346\263\275.md" "b/reports/daily/\344\275\231\346\263\275/20260730-\344\275\231\346\263\275.md" new file mode 100644 index 0000000000000000000000000000000000000000..82d11f7760526fce7b3dd941236c361c8568c8bd --- /dev/null +++ "b/reports/daily/\344\275\231\346\263\275/20260730-\344\275\231\346\263\275.md" @@ -0,0 +1,67 @@ +# 日报 - 余泽 - 2026-07-30 + +## 今日完成 +![购物车](imgs/购物车.png) +1. **购物车页面完整样式重写**(Cart.vue,分支 feature/user-cart) + - 彻底重构布局:由 el-table 表格改为卡片式列表,每行横向展示:勾选框+商品信息 / 单价 / 数量(el-input-number±按钮) / 小计 / 删除 + - 底部结算栏:全选 + 已选件数 + 合计金额 + 结算按钮,sticky 定位在页面底部 + - 空状态:SVG 购物车图标 + "快去逛逛吧" + 去首页按钮 + - 未登录提示条:可点击跳转登录页 + +2. **cartStore.js 完善与 Bug 修复**(分支 feature/user-cart) + - 修复 `updateQty/toggleSelect/removeItem` 未登录时错误用 `id` 字段查找,统一改为 `productId` + - `loadCart` 新增 selected 字段规范化:数据库返回 1/0 → boolean + - 新增 `toggleSelectAll()` 方法,支持全选/取消全选 + - localStorage key 统一为 `eshop_cart` + - `mergeLocalCart` 成功后自动清空 localStorage + +3. **Login.vue 合并逻辑简化**(分支 feature/user-cart) + - 移除冗余的 `localStorage.getItem/removeItem` 手动操作,直接调用 `cartStore.mergeLocalCart()` + +4. **购物车加购失败修复**(ProductDetail.vue,分支 feature/user-cart) + - `cart.add()` 方法名不存在,改为正确的 `cart.addToCart()` + - 后端验证:POST /api/cart 正常返回 `已加入购物车` + +5. **购物车页面交互问题修复**(Cart.vue,分支 feature/user-cart) + - 选中框点不动:改用 `ref="tableRef"` + `syncTableSelection()` 显式调用 `toggleRowSelection`,避免双向绑定循环触发 + - 金额不显示:`price` 字段为字符串(数据库 DECIMAL),所有计算处加 `Number()` 包裹 + - 清空购物车失败:`onClearAll` 正确 await store 方法 + - 加购后金额不刷新:`addToCart` 末尾调用 `loadCart()` 刷新 + +6. **收货地址页面排版美化**(AddressList.vue,分支 feature/user-cart) + - 增大卡片内边距和最小高度(200px→240px) + - 姓名+手机号移至独立头部;手机号脱敏显示(138****1234) + - 地址拆分为省市区+详细地址两级,配绿色定位图标 + - 默认地址用渐变背景+绿色边框 + - 卡片网格与页面标题对齐到同一宽度(1400px) + +7. **顶栏头像更换功能**(FrontLayout.vue + userStore.js,分支 feature/user-cart) + - 顶栏右侧新增头像显示,点击可上传本地图片更换 + - 悬停头像出现相机遮罩图标,与个人中心交互一致 + - 调用 `updateProfile` API 提交头像 URL 到后端 + +8. **头像持久化修复**(userStore.js + FrontLayout.vue + Profile.vue,分支 feature/user-cart) + - 头像 URL 存入 localStorage(key: `avatar`),刷新页面不丢失 + - 登录时自动调用 `getProfile` 拉取完整用户信息(含头像) + - 新增 `setAvatar()` action,统一更新 state 和 localStorage + +## 遇到的问题 + +| 问题描述 | 解决状态 | 解决方式/求助对象 | +|----------|----------|-------------------| +| 购物车加购后金额不显示 | 已解决 | ProductDetail.vue 方法名 `cart.add()` → `cart.addToCart()` | +| 选中框点不动 | 已解决 | 改用 `tableRef.toggleRowSelection()` 显式控制,不依赖 v-model 双向绑定 | +| 金额计算报错 | 已解决 | price 字段为字符串,所有计算处加 `Number()` 包裹 | +| 清空购物车失败 | 已解决 | onClearAll 正确 await cartStore.clearAll() | +| 头像换页后消失 | 已解决 | 头像 URL 持久化到 localStorage,登录时拉取 profile | +| blob 临时 URL 刷新后失效 | 未解决 | 后端暂无真实图片存储服务,需后续接入 OSS/七牛 | + +## 明日计划 + +1. 购物车结算流程完善(跳转订单确认页) +2. 商品列表/分类页加入购物车按钮 +3. 收藏功能页面接入真实 API + +## 今日工时 + +约 6 小时 diff --git "a/reports/daily/\344\275\231\346\263\275/20260731-\344\275\231\346\263\275.md" "b/reports/daily/\344\275\231\346\263\275/20260731-\344\275\231\346\263\275.md" new file mode 100644 index 0000000000000000000000000000000000000000..994478216325a0ec367fb721c9c15e140cfa2f74 --- /dev/null +++ "b/reports/daily/\344\275\231\346\263\275/20260731-\344\275\231\346\263\275.md" @@ -0,0 +1,57 @@ +# 日报 - 余泽 - 2026-07-31 + +## 今日完成 + +1. **浏览历史功能后端开发**(分支 feature/user-cart) + - 新建 `backend/src/modules/user/models/BrowseHistory.js` — 模型(id, userId, productId, browsedAt) + - 新建 `backend/src/modules/user/controllers/historyController.js` — 控制器(POST/GET 接口) + - 新建 `backend/src/modules/user/services/browseHistoryService.js` — 业务逻辑(addOrUpdate/list/clearAll/remove) + - 注册模型关联:`backend/src/models/index.js` — 用户↔浏览历史、商品↔浏览历史 + - 注册路由:`backend/src/modules/user/routes/index.js` — POST/GET `/2/browse-history` + - 建表 SQL(browse_history,user_id FK 指向 user.id) + +2. **浏览历史前端开发**(分支 feature/user-cart) + - `frontend/src/api/user.js` — 新增 `getBrowseHistory`、`addBrowseHistory` API 方法 + - `frontend/src/router/index.js` — 新增路由 `/user/history` + - 新建 `frontend/src/views/history/HistoryList.vue` — 页面(今天/昨天/更早分组、横向列表、清空按钮) + - 商品详情页 `ProductDetail.vue` — 访问时自动调用 `addBrowseHistory` 记录浏览 + +3. **个人中心侧边栏入口**(分支 feature/user-cart) + - `frontend/src/views/user/Profile.vue` — 侧边栏新增"浏览历史"菜单项,Clock 图标 + - 右侧内容区新增 history panel,内嵌 HistoryList 组件 + +4. **购物车页面交互优化**(分支 feature/user-cart) + - 重构为卡片式横向列表布局:勾选框+商品信息 / 单价 / 数量(el-input-number) / 小计 / 删除 + - 底部结算栏 sticky 定位,全选+已选件数+合计+结算按钮 + - 修复 `selected` 字段 1/0 → boolean 规范化,金额计算正确显示 + - 修复 `price` 字段为字符串导致 `toFixed` 报错,所有计算加 `Number()` 包裹 + - `toggleSelectAll` 方法添加到 cartStore,解决全选功能缺失 + +5. **cartStore.js 完善**(分支 feature/user-cart) + - `loadCart` 新增 selected 规范化:数据库返回 1/0 → boolean + - 新增 `toggleSelectAll()` 方法支持全选/取消全选 + - 未登录状态 `updateQty/toggleSelect/removeItem` 统一用 `productId` 查找 + - localStorage key 统一为 `eshop_cart` + +6. **头像后端 API 支持**(分支 feature/user-cart) + - 确认后端 `updateProfile` 支持 `avatar` 字段上传 + +## 遇到的问题 + +| 问题描述 | 解决状态 | 解决方式/求助对象 | +|----------|----------|-------------------| +| browse_history 表 browse_time 列名与数据库实际不符 | 已解决 | 数据库列名是 browsed_at,修正模型 field 配置 | +| browse_history 外键指向错误表 | 已解决 | 重建表,FK 改为指向 user.id 而非 users.id | +| 浏览历史字段规范化导致 foreign key constraint 失败 | 已解决 | 外键重建后解决 | +| JWT userId 与数据库实际 userId 不一致 | 已解决 | 数据库重置后 FK 外键重新指向 user.id | +| 购物车金额计算 toFixed 报错 | 已解决 | price 字段为字符串,所有计算处加 Number() 包裹 | +| 购物车选择框点不动 | 已解决 | 改用 ref+显式 toggleRowSelection,避免双向绑定循环触发 | + +## 明日计划 + +1. 商品列表/分类页加入购物车按钮 +2. 收藏功能页面接入真实 API + +## 今日工时 + +约 5 小时 diff --git "a/reports/daily/\344\275\231\346\263\275/20260803-\344\275\231\346\263\275.md" "b/reports/daily/\344\275\231\346\263\275/20260803-\344\275\231\346\263\275.md" new file mode 100644 index 0000000000000000000000000000000000000000..30cbbe26edb9cae795a92e35e95832788630129b --- /dev/null +++ "b/reports/daily/\344\275\231\346\263\275/20260803-\344\275\231\346\263\275.md" @@ -0,0 +1,50 @@ +# 日报 - 余泽 - 2026-08-03 + +## 今日完成 +![新登录](imgs/新登录.png) +![新注册](imgs/新注册.png) +1. **登录页面全新改版(淘宝风格)** + - 参考淘宝登录页面重新设计整体布局和样式 + - 顶部移除 Logo,保留空 Header 与注册页保持一致 + - 删除"短信验证码登录" Tab,只保留密码登录 + - 主色调使用淘宝橙 `#FF5000` + - 登录卡片:白色背景 + 轻微阴影,居中展示 + +2. **登录按钮动态效果** + - 用户名和密码填写完成后(密码≥6位),登录按钮出现阴影效果 + - 按钮保持橙色渐变,视觉上提示可以点击 + +3. **背景装饰动画** + - 在页面空白区域添加动态装饰效果 + - 三个橙色渐变圆球在页面不同位置缓慢漂移 + - 两个扩散圆环以登录框为中心向外扩散 + - 动画半透明,不会遮挡登录内容 + +4. **注册页面同步改版** + - 按照登录页的淘宝风格同步更新注册页面 + - 保持一致的顶部空 Header、底部法律链接 + - 保留密码强度指示器功能 + - 输入框样式与登录页统一 + +5. **细节样式调整** + - 登录/注册标题字号从 22px 调整为 26px + - 输入框宽度问题修复:设置 `width: 100%` 和 `box-sizing: border-box`,确保输入框与下方按钮同宽 + - 卡片内边距优化,内容不拥挤 + +## 遇到的问题 + +| 问题描述 | 解决状态 | 解决方式/求助对象 | +|----------|----------|-------------------| +| 背景装饰动画被遮挡 | 已解决 | 增加动画元素透明度,调整 z-index 层级 | +| 输入框宽度与按钮不对齐 | 已解决 | 给 input-wrap 添加 `width: 100%`,el-input 添加 `flex: 1` | +| 初始设计的动画效果太明显影响美观 | 已解决 | 删除了装饰球动画,保留圆球漂移和扩散波纹 | + +## 明日计划 + +1. 继续完善商城其他页面的样式统一 +2. 优化购物车页面用户体验 +3. 进行前后端接口联调 + +## 今日工时 + +约 4 小时 diff --git "a/reports/daily/\344\275\231\346\263\275/imgs/0727-\346\263\250\345\206\214.png" "b/reports/daily/\344\275\231\346\263\275/imgs/0727-\346\263\250\345\206\214.png" new file mode 100644 index 0000000000000000000000000000000000000000..bbfe589d99ae6a000dcd6942291d9f93229a0150 Binary files /dev/null and "b/reports/daily/\344\275\231\346\263\275/imgs/0727-\346\263\250\345\206\214.png" differ diff --git "a/reports/daily/\344\275\231\346\263\275/imgs/0727-\347\231\273\345\275\225.png" "b/reports/daily/\344\275\231\346\263\275/imgs/0727-\347\231\273\345\275\225.png" new file mode 100644 index 0000000000000000000000000000000000000000..8bc75b669a8a9175bd8c8bad5226420b2e7f7cf0 Binary files /dev/null and "b/reports/daily/\344\275\231\346\263\275/imgs/0727-\347\231\273\345\275\225.png" differ diff --git "a/reports/daily/\344\275\231\346\263\275/imgs/\344\270\252\344\272\272\344\270\255\345\277\203.png" "b/reports/daily/\344\275\231\346\263\275/imgs/\344\270\252\344\272\272\344\270\255\345\277\203.png" new file mode 100644 index 0000000000000000000000000000000000000000..81d2166e39f402ad77d9b6993065e2c1c351059c Binary files /dev/null and "b/reports/daily/\344\275\231\346\263\275/imgs/\344\270\252\344\272\272\344\270\255\345\277\203.png" differ diff --git "a/reports/daily/\344\275\231\346\263\275/imgs/\345\234\260\345\235\200\347\256\241\347\220\206.png" "b/reports/daily/\344\275\231\346\263\275/imgs/\345\234\260\345\235\200\347\256\241\347\220\206.png" new file mode 100644 index 0000000000000000000000000000000000000000..c95e0de825667a22b4f64e97c089859489e16589 Binary files /dev/null and "b/reports/daily/\344\275\231\346\263\275/imgs/\345\234\260\345\235\200\347\256\241\347\220\206.png" differ diff --git "a/reports/daily/\344\275\231\346\263\275/imgs/\346\226\260\346\263\250\345\206\214.png" "b/reports/daily/\344\275\231\346\263\275/imgs/\346\226\260\346\263\250\345\206\214.png" new file mode 100644 index 0000000000000000000000000000000000000000..f79f9cfac2213cb170f0128d882b43c3de0941c5 Binary files /dev/null and "b/reports/daily/\344\275\231\346\263\275/imgs/\346\226\260\346\263\250\345\206\214.png" differ diff --git "a/reports/daily/\344\275\231\346\263\275/imgs/\346\226\260\347\231\273\345\275\225.png" "b/reports/daily/\344\275\231\346\263\275/imgs/\346\226\260\347\231\273\345\275\225.png" new file mode 100644 index 0000000000000000000000000000000000000000..3de5b19e309329322aa11c3e68f2808d575ce910 Binary files /dev/null and "b/reports/daily/\344\275\231\346\263\275/imgs/\346\226\260\347\231\273\345\275\225.png" differ diff --git "a/reports/daily/\344\275\231\346\263\275/imgs/\350\264\255\347\211\251\350\275\246.png" "b/reports/daily/\344\275\231\346\263\275/imgs/\350\264\255\347\211\251\350\275\246.png" new file mode 100644 index 0000000000000000000000000000000000000000..4fa9172bb048dc306738dc7818f4b96ddaca513e Binary files /dev/null and "b/reports/daily/\344\275\231\346\263\275/imgs/\350\264\255\347\211\251\350\275\246.png" differ diff --git "a/reports/daily/\346\261\237\347\246\271\350\276\260/20260729_\346\227\245\346\212\245.md" "b/reports/daily/\346\261\237\347\246\271\350\276\260/20260729_\346\227\245\346\212\245.md" new file mode 100644 index 0000000000000000000000000000000000000000..a318a181893e43ec61418a46b608c60de35ab1c6 --- /dev/null +++ "b/reports/daily/\346\261\237\347\246\271\350\276\260/20260729_\346\227\245\346\212\245.md" @@ -0,0 +1,53 @@ +# 日报 - 江禹辰 - 2026-07-29 + +## 今日完成 + +完成了商品域(B)接缝接口联调 + 商品详情页完善,修复了多个跨模块调用的问题。 + +### 接缝接口联调(S01/S02/S03) + +| 接口 | 调用方 | 验证结果 | +|------|--------|----------| +| `POST /api/products/batch` 批量查询商品 | D(用户模块) | ✅ 正常 | +| `POST /api/products/:id/stock/decrement` 扣减库存(幂等) | C(订单模块) | ✅ 正常 | +| `POST /api/products/:id/stock/increment` 回补库存(幂等) | C(订单模块) | ✅ 正常 | + +**S02/S03 幂等机制**:使用 `stock_log` 表记录 biz_no,重复调用返回已处理结果不重复扣减/回补。 + +### 商品详情页完善 + +| 文件 | 改动说明 | +|------|----------| +| `views/product/ProductDetail.vue` | 重写为淘宝风格:店铺 header + 评分星级 + 双栏布局 + SKU 选择器 + 收藏按钮 | +| `views/product/ProductList.vue` | 分类点击跳转 `/category?categoryId=N`,URL 参数同步 + 橙红主题高亮 | + +### 购物车多规格支持 + +配合 D(余泽)的购物车模块,完成以下对接: +- `ProductBundle` 模型支持多规格(quantity 数量规格,如"1件装""2件装") +- 前台 `GET /api/products/:id/bundles` 返回在售规格列表 +- 购物车结算快照正确包含 bundle 规格信息 + +--- + +## 遇到的问题 + +| 问题描述 | 解决状态 | 解决方式 | +|----------|----------|----------| +| 批量查询商品接口参数格式不统一 | ✅ 已解决 | 统一用 `ids` 数组参数 | +| 库存扣减时 stock_log 的 biz_no 重复 | ✅ 已解决 | orderNo 作为 biz_no,事务保证幂等 | +| 商品详情页分类筛选不生效 | ✅ 已解决 | ProductList 正确监听 categoryId 变化 | + +--- + +## 明日计划 + +1. 完善后台商品管理页面的规格管理功能 +2. 测试商品列表的价格排序功能 +3. 补充商品管理后台的图片上传功能 + +--- + +## 今日工时 + +约 **5** 小时 diff --git "a/reports/daily/\346\261\237\347\246\271\350\276\260/20260730_\346\227\245\346\212\245.md" "b/reports/daily/\346\261\237\347\246\271\350\276\260/20260730_\346\227\245\346\212\245.md" new file mode 100644 index 0000000000000000000000000000000000000000..450d9f91cb0e51bc1ecef01d0c68f902a3b06aa1 --- /dev/null +++ "b/reports/daily/\346\261\237\347\246\271\350\276\260/20260730_\346\227\245\346\212\245.md" @@ -0,0 +1,58 @@ +# 日报 - 江禹辰 - 2026-07-30 + +## 今日完成 + +完成了商品域(B)后台规格管理完整功能 + 前台规格选择器优化。 + +### 后台规格(ProductBundle)管理 + +| 接口 | 说明 | +|------|------| +| `GET /api/admin/products/:id/bundles` | 获取某商品全部规格 | +| `POST /api/admin/products/:id/bundles` | 新增规格(含软删复活逻辑) | +| `PATCH /api/admin/bundles/:bundleId` | 更新规格(名称/价格/库存/图片/排序) | +| `PATCH /api/admin/bundles/:bundleId/on-sale` | 规格单独上下架 | +| `DELETE /api/admin/bundles/:bundleId` | 软删除规格 | + +**业务规则:** +- 同商品下 quantity 相同不可重复(唯一键约束) +- 删除时软删除(deleted=1),再次创建相同 quantity 可复活 +- parent 商品下架时,所有规格不可售 + +### 前台规格选择器优化 + +| 文件 | 改动说明 | +|------|----------| +| `views/product/ProductDetail.vue` | 删除 MOCK_SKUS,接入真实规格列表;条件渲染 + 选中态高亮 | +| `services/bundleService.js` | `listOnSaleByProduct` 校验 parent 商品 onSale 状态 | + +### 后台商品管理 UI 完善 + +| 文件 | 改动说明 | +|------|----------| +| `views/product/admin/ProductManage.vue` | 编辑弹窗新增规格列表区块(卡片风格) | +| `controllers/adminBundleController.js` | bundle create 包装事务,捕获唯一键冲突返回 ConflictError | + +--- + +## 遇到的问题 + +| 问题描述 | 解决状态 | 解决方式 | +|----------|----------|----------| +| 规格唯一键冲突报错不够友好 | ✅ 已解决 | 捕获 SequelizeUniqueConstraintError,返回 ConflictError | +| parent 商品下架后规格仍可加入购物车 | ✅ 已解决 | listOnSaleByProduct 校验 parent onSale=1 | +| 后台编辑商品时规格列表不更新 | ✅ 已解决 | 重新拉取规格列表并刷新 | + +--- + +## 明日计划 + +1. 完善商品列表页的搜索和筛选功能 +2. 测试商品模块完整业务流程 +3. 补充商品评价晒图功能(可选) + +--- + +## 今日工时 + +约 **6** 小时 diff --git "a/reports/daily/\346\261\237\347\246\271\350\276\260/20260731_\346\227\245\346\212\245.md" "b/reports/daily/\346\261\237\347\246\271\350\276\260/20260731_\346\227\245\346\212\245.md" new file mode 100644 index 0000000000000000000000000000000000000000..83bab695b338b18542022d0aeab51c195718a923 --- /dev/null +++ "b/reports/daily/\346\261\237\347\246\271\350\276\260/20260731_\346\227\245\346\212\245.md" @@ -0,0 +1,52 @@ +# 日报 - 江禹辰 - 2026-07-31 + +## 今日完成 + +完成了商品域(B)商品列表优化 + 后台分类管理完善 + 代码质量提升。 + +### 商品列表页优化 + +| 文件 | 改动说明 | +|------|----------| +| `views/product/ProductList.vue` | 响应式网格:PC 6列→平板3列→手机2列;分类筛选抽屉模式 | +| `services/productService.js` | 支持 `sortPrice` 参数(asc/desc)价格排序 | +| `stores/productStore.js` | 重写 fetchList,支持分类缓存、分页状态管理 | + +### 后台分类管理完善 + +| 文件 | 改动说明 | +|------|----------| +| `views/product/admin/CategoryManage.vue` | 响应式表格、父分类下拉智能过滤 | +| `services/categoryService.js` | 删除分类时校验:不允许删除有子分类或商品的分类 | +| `controllers/categoryController.js` | upsertSchema 增加参数校验 | + +### 代码质量提升 + +- 统一后端错误码格式(`ParamError`/`NotFoundError`/`ConflictError`) +- ProductBundle 序列化方法统一处理 DECIMAL → string(保留2位小数) +- 事务包装 bundle 创建,防止并发唯一键冲突 +- 前端 API 请求统一加 loading 状态 + +--- + +## 遇到的问题 + +| 问题描述 | 解决状态 | 解决方式 | +|----------|----------|----------| +| 价格排序 1299 < 599(字符串比较) | ✅ 已解决 | MySQL 查询用 `price + 0` 强制转数值排序 | +| 分类树形结构前端渲染错误 | ✅ 已解决 | 递归组件正确处理 parentId 为 null 的情况 | +| 后台分类下拉显示所有分类而非层级过滤 | ✅ 已解决 | 下拉按 parentId 分组显示 | + +--- + +## 明日计划 + +1. 测试商品模块完整业务流程 +2. 对接首页商品数据接口(热门/新品/推荐) +3. 完善商品管理后台的批量操作功能 + +--- + +## 今日工时 + +约 **5** 小时 diff --git "a/reports/daily/\346\261\237\347\246\271\350\276\260/20260803_\346\227\245\346\212\245.md" "b/reports/daily/\346\261\237\347\246\271\350\276\260/20260803_\346\227\245\346\212\245.md" new file mode 100644 index 0000000000000000000000000000000000000000..1049853a59d0e0e6e1e27b0e2969c54cb0b80b29 --- /dev/null +++ "b/reports/daily/\346\261\237\347\246\271\350\276\260/20260803_\346\227\245\346\212\245.md" @@ -0,0 +1,58 @@ +# 日报 - 江禹辰 - 2026-08-03 + +## 今日完成 + +完成了商品域(B)首页数据接口 + 搜索功能优化 + 批量操作功能。 + +### 首页数据接口 + +| 接口 | 说明 | +|------|------| +| `GET /api/products/hot` | 热门商品(按销量或浏览量) | +| `GET /api/products/new` | 新品上架 | +| `GET /api/products/recommend` | 推荐商品 | +| `GET /api/products/home` | 首页综合数据(轮播+分类+热销+新品) | + +**前端对接:** +- `views/home/ProductSection.vue` — 商品区块组件(支持热卖/新品/推荐切换) +- `api/product.js` — 新增 `getHotProducts`/`getNewProducts`/`getRecommendProducts`/`getHomeData` 接口 + +### 搜索功能优化 + +| 文件 | 改动说明 | +|------|----------| +| `views/product/SearchResult.vue` | 搜索结果页,支持关键词高亮、分页、排序 | +| `views/product/ProductList.vue` | 工具栏关键词显示 + 搜索结果为空时的友好提示 | +| `services/productService.js` | search 方法支持多关键词模糊匹配 | + +### 后台批量操作功能 + +| 文件 | 改动说明 | +|------|----------| +| `views/product/admin/ProductManage.vue` | 批量选择商品(el-checkbox)、批量上架/下架 | +| `controllers/adminProductController.js` | 批量更新支持事务,失败回滚 | +| `services/productService.js` | 批量操作方法封装 | + +--- + +## 遇到的问题 + +| 问题描述 | 解决状态 | 解决方式 | +|----------|----------|----------| +| 首页数据接口未实现 | ✅ 已解决 | 补充 hot/new/recommend/home 接口 | +| 搜索结果页分类导航不显示 | ✅ 已解决 | 复用 CategorySidebar 组件 | +| 批量删除商品时 SKU 未同步删除 | ✅ 已解决 | 事务中先删 SKU 再删商品(软删除) | + +--- + +## 明日计划 + +1. 测试商品模块与订单模块的完整对接 +2. 完善商品详情页的 SKU 选择器交互 +3. 补充商品管理后台的图片上传预览功能 + +--- + +## 今日工时 + +约 **6** 小时 diff --git "a/reports/daily/\350\203\241\346\231\250\350\276\211/20260729-\350\203\241\346\231\250\350\276\211.md" "b/reports/daily/\350\203\241\346\231\250\350\276\211/20260729-\350\203\241\346\231\250\350\276\211.md" new file mode 100644 index 0000000000000000000000000000000000000000..31db2b6e8637719c5ec1b5b5964f0fa4e9a53660 --- /dev/null +++ "b/reports/daily/\350\203\241\346\231\250\350\276\211/20260729-\350\203\241\346\231\250\350\276\211.md" @@ -0,0 +1,81 @@ +# 日报 — 胡晨辉(A · 组长/架构员) + +> 日期:2026-07-29(周三) | 角色:A 公共基建包 / 组长 +> 提交:18:00 前 + +## 一、今日完成 + +### 凌晨提交(周二遗留) + +- [x] 提交 07-28 日报:`4b50150 !7 日报`(00:29,补交昨日日报) + +### PR #9 合并 + 回退(echarts v6 适配) + +- [x] 02:59 合并 PR #9:`992f8ba !9 fix(echarts): v6 改用全量 ESM 导入(最稳)` + - 合并胡晨辉/feature/common-infra → master + - 涉及 `backend/src/app.js`、`backend/src/common/{auth,error-codes,error-handler,exception}.js`、`backend/src/config/db.js`、admin/order/product 多个 service +- [x] 03:09 创建回退分支:`b05cfe0 回退 'Pull Request !9'` +- [x] 03:16 再次 Revert:`1d09653 Revert "fix(echarts): v6 改用全量 ESM 导入(最稳)"` +- [x] 原因:合并后端 Echarts 适配代码后,联调发现后台统计图表仍报错(v6 ESM 引入方式在不同打包环境下行为不一致) + +### PR #10 合并(陈杨文订单模块) + +- [x] 08:49 合并 PR #10:`df6d092 !10 fix(admin): 后台订单列表下单时间本地化显示` + - 合并陈杨文/feature/order → master + - 涉及 **61 个文件,8402 行新增,5961 行删除** + - 核心内容: + - 后台订单管理(admin/orders 列表、详情、状态轮询) + - 订单超时取消(Redis 过期监听 + 兜底定时器) + - 统一错误码迁移(商品 / 购物车 / 订单模块) + - 后端 jest + supertest 集成测试(`backend/tests/order.test.js`、`setup.js`、`jest.config.js`) + - README 更新 + 订单相关设计 spec / 实施计划 +- [x] 处理合并冲突:develop 与 feature/common-infra 长期分叉导致 lockfile 严重冲突,先接受陈杨文版本再手工合并本地改动 + +### 基建包维护 + +- [x] 16:35 更新 lockfile:`28c18d8 chore(frontend): update chart dependencies lockfile` + - 添加 `echarts@6.1.0` + `vue-echarts@8.0.1` + `tslib` 到 `frontend/package-lock.json` + - 原因:昨晚 PR #9 试装 echarts v6 后 lockfile 没及时提交 + +### 团队协调 + +- [x] 在微信群同步 master 与 develop 分叉情况,约定本周内统一合并 feature/common-infra +- [x] 帮组员 review 陈杨文 PR #10 的接口设计 + +## 二、遇到的问题与解决 + +| 问题 | 处理方式 | +|------|---------| +| 凌晨 03 点合并 PR #9 → 03:16 又 revert,连续 3 次提交 | 反思:深夜合并不适合做涉及多文件的特性合并,应该先在分支充分验证再合并 | +| echarts v6 全量 ESM 导入在不同打包环境下行为不一致 | 决定改用"按需 import * as"(具名导入),保留 v6 升级但降风险 | +| develop 与 feature/common-infra 长期分叉(20+ commit),合并陈杨文 PR #10 时 lockfile 冲突严重 | 先接受 upstream 版本,再手工 cherry-pick 本地改动;后续要更频繁 rebase | +| PR #10 涉及 61 文件 8402 行,单次 review 风险大 | 分 4 个 review 单元(订单 CRUD / 超时机制 / 错误码 / 测试)依次过 | + +## 三、明日(7/30 周四)计划 + +- [ ] 处理 master 与 develop 长期分叉:把 feature/common-infra 的 30+ commit 整合回 master +- [ ] echarts v6 重新选型:测试"具名导入"vs"全量 ESM"两种方案,确定后写实施记录 +- [ ] 检查所有页面(订单、统计、日志)的本地化时间显示,确认 mysql2 时区修复完整生效 +- [ ] 整理本周周报(提交周报 v2.0) +- [ ] 验收组员 ProductManage / OrderManage / UserManage 后台页面(截止本周) + +## 四、阻塞 / 风险 + +| 风险 | 影响 | 应对 | +|------|------|------| +| echarts v6 方案未定 | Dashboard 图表可能反复返工 | 今日内选定方案并锁定;后续不允许再切换 | +| master 与 develop 分叉扩大(PR #10 又带来 8402 行)| 主分支整合工作量大 | 安排 7/30 上午专门做合并,下午做 echarts 修复 | +| 凌晨连续提交 PR 合并不规范 | 引入线上回归风险 | 团队约定:涉及多文件的合并只在 10:00-18:00 进行 | +| PR #10 测试覆盖只在 happy path | 异常路径未验证 | 7/30 给陈杨文补 e2e 取消订单 + 超时用例 | + +## 五、今日工时 + +约 **11 小时**(00:29 凌晨补日报 + 02:59-03:16 紧急合并/回退 + 08:49 大 PR 合入 + 16:35 lockfile + 团队协调与 review) + +## 六、个人思考 / 复盘 + +今天最大的教训是 **"凌晨不合并大特性"**。02:59 合并 PR #9 时还在疲劳状态,没有充分验证就合入,03:09 立刻发现问题,又连续做 2 次 revert。如果当时头脑清醒,应该先在 feature/common-infra 分支跑一遍 build + 后台冒烟测试,确认无报错再合并。 + +第二点是 **"陈杨文 PR #10 的合并是正确的事"**。虽然 61 文件 8402 行看起来吓人,但代码组织清晰(订单 CRUD / 超时机制 / 错误码 / 测试分块),团队 review 效率很高。这种"包越大收益越大"的合并不应该害怕。 + +第三点是 **"凌晨补日报"** 本身就是个负面信号——昨晚 7/28 日报是 7/29 凌晨 00:29 才交的,说明前一天的工时规划有问题(7/28 也写到 7 小时)。连续两天晚睡 + 凌晨工作不可持续,从明天起强制 23:00 前结束当天工作。 \ No newline at end of file diff --git "a/reports/daily/\350\203\241\346\231\250\350\276\211/20260730-\350\203\241\346\231\250\350\276\211.md" "b/reports/daily/\350\203\241\346\231\250\350\276\211/20260730-\350\203\241\346\231\250\350\276\211.md" new file mode 100644 index 0000000000000000000000000000000000000000..89c44c7cea764db0a1484c047024811ba88a9dd7 --- /dev/null +++ "b/reports/daily/\350\203\241\346\231\250\350\276\211/20260730-\350\203\241\346\231\250\350\276\211.md" @@ -0,0 +1,71 @@ +# 日报 — 胡晨辉(A · 组长/架构员) + +> 日期:2026-07-30(周四) | 角色:A 公共基建包 / 组长 +> 提交:18:00 前 + +## 一、今日完成 + +### 主干整合:master ↔ develop ↔ feature/common-infra + +- [x] 17:24 合并冲突解决:`26a074a merge: resolve conflict with upstream develop, regenerate package-lock.json` + - 双亲合并:`28c18d8`(本端 feature/common-infra tip)+ `df6d092`(upstream develop / PR #10 后的状态) + - 解决策略:以 develop 业务代码为主,feature/common-infra 的基建改动(Dashboard / Logs / Pagination / 错误码体系)通过手动文件级 cherry-pick 引入 + - 重新生成 `frontend/package-lock.json`、`backend/package-lock.json`(两条 lockfile 来自不同包管理器,合并后用 pnpm 重新 install 重建) + - **成果**:主干一次性吸收了 **32 个 commit / 61 个文件 / 8402 行新增 / 5961 行删除** + +### 集成验证(合并后) + +- [x] 跑 `pnpm install` + 后端 `npm install`,确认两条 lockfile 一致 +- [x] 后端 `npm run dev` 启动正常,路由 404 全部命中(之前发现的 `` catch-all bug 修复已合并) +- [x] 前端 `pnpm run dev` 启动正常,Dashboard 6 个 KPI 卡片正常加载 echarts +- [x] 抽查 3 个页面的本地化时间显示(订单列表 / 操作日志 / 商品列表),均与系统时间一致 —— mysql2 时区修复完整生效 + +### 团队协调 + +- [x] 在组群同步"主干已整合"消息,附 commit hash `26a074a`,要求所有人重新 fetch + reset +- [x] 帮余泽 review 收藏功能接口设计(口头,未留 commit) + +### 周报整理(未提交) + +- [ ] 本周周报 v2.0:今日已完成初稿,明日提交 + +## 二、遇到的问题与解决 + +| 问题 | 处理方式 | +|------|---------| +| `frontend/pnpm-lock.yaml` 与 `backend/package-lock.json` 命名风格不同,Git 合并时冲突不直观 | 删除 pnpm-lock.yaml,统一用 package-lock.json;组员需要从 npm 切换到 pnpm | +| 合并时业务代码(订单 / 地址)与基建代码(Dashboard / Logs)纠缠,分不清是哪个分支的改动 | 用 `git log --follow ` 逐文件追溯归属,对模糊地带优先保留基建包改动 | +| echarts v6 在 master 与 develop 的版本不一致 | 强制升级到 `echarts@6.1.0` + `vue-echarts@8.0.1`,锁定到 package.json | +| 合并后 `frontend/src/views/NotFound.vue`、`EmptyState.vue` 被删除但路由仍引用 | 合并后立刻 grep 路由表 + 启动 dev 服务器验证 | +| 整合工作耗时超出预期(5h+)| 没有更早开始;以后每周三固定做主干整合,不要拖到周四 | + +## 三、明日(7/31 周五)计划 + +- [ ] 提交本周周报 v2.0(必交) +- [ ] 验收 ProductManage / OrderManage / UserManage 后台页面(截止今日,已延后 1 天,需催组员) +- [ ] echarts v6 "具名导入" 方案落地(替代昨晚 revert 的全量 ESM) +- [ ] 整理 A 公共基建包 v1.0 收尾文档(Dashboard + Logs + 错误码体系) +- [ ] 周末计划:review 团队周报 + 准备下周冲刺 + +## 四、阻塞 / 风险 + +| 风险 | 影响 | 应对 | +|------|------|------| +| echarts v6 方案今晚还没敲定 | Dashboard 6 KPI 周末可能有回归 | 周六前必须选定方案,否则锁回 v5 | +| 主干整合延迟 1 天(周三凌晨事故 + 周四大合并) | 周报本应周四交,现在周五才交 | 强制周五 18:00 前交,超时自罚 | +| 后台页面验收延期 | B/C/D 模块无法整体演示 | 今晚在组群发最后通牒,周末不交则周一站会点名 | +| pnpm ↔ npm 包管理器切换 | 部分组员本地报错 | 写一份切换指南发到群里(今晚做) | + +## 五、今日工时 + +约 **7 小时**(主干整合 5h + 集成验证 1h + 团队协调 1h;周报和后台验收未完成,挪到明日) + +## 六、个人思考 / 复盘 + +今天最大的收获是 **"主干整合不应该拖"**。从周三凌晨 PR #9 事故开始,到周四上午才正式动手合并,整整 36 小时的分叉期。如果中间有人基于旧 master 提交代码,合并时会更难处理。这种"主干整合"工作应该制度化:**每周三上午固定做主干整合**,无论是否有冲突都跑一遍 `git fetch && git status`,提前发现分叉。 + +第二点是 **"merge 信息要详细"**。今天 `26a074a` 写了完整的 merge commit message,列出了合并策略(以谁为主、手工 cherry-pick 了什么),方便后续追溯。后续所有 merge commit 都要遵循这个标准。 + +第三点是 **"凌晨事故的后续影响"**。周三 03 点 Revert PR #9 当天看似解决了,但今天整合时才意识到:那段混乱导致 echarts v6 方案至今未定(周三用全量 ESM,周三晚回退,今天合并后又不知道用哪个版本)。**凌晨不只影响当天,还影响接下来两三天**。这个教训要写进团队规范里。 + +最后一个负面信号:**今天没写新代码**。作为组长可以理解(主干整合是必须做的),但连续两天没业务产出需要警惕。周末或下周一必须把 echarts v6 方案落地,恢复"每天都有可演示的功能增量"的节奏。 \ No newline at end of file diff --git "a/reports/daily/\350\203\241\346\231\250\350\276\211/20260803-\350\203\241\346\231\250\350\276\211.md" "b/reports/daily/\350\203\241\346\231\250\350\276\211/20260803-\350\203\241\346\231\250\350\276\211.md" new file mode 100644 index 0000000000000000000000000000000000000000..157ba7f856608dbf160a23575902e4fa17d88059 --- /dev/null +++ "b/reports/daily/\350\203\241\346\231\250\350\276\211/20260803-\350\203\241\346\231\250\350\276\211.md" @@ -0,0 +1,122 @@ +# 日报 — 胡晨辉(A · 组长/架构员) + +> 日期:2026-08-03(周一) | 角色:A 公共基建包 / 组长 +> 提交:18:00 前 + +## 一、今日完成 + +### 1. 后台概览页(D 域 · 体验完整化) + +围绕 Dashboard.vue 一系列改造,按数据维度逐个加日期筛选、UI 优化: + +- [x] **修复"清空浏览历史"按钮无效 bug** + - 根因:清除按钮只清前端 state,没调后端;刷新后被服务端数据重新覆盖 + - 修复链路 4 个文件: + - 后端 [backend/src/modules/user/controllers/historyController.js](backend/src/modules/user/controllers/historyController.js) 新增 `clearAll` controller + - 后端 [backend/src/modules/user/routes/index.js](backend/src/modules/user/routes/index.js) 新增 `DELETE /api/browse-history` + - 前端 [frontend/src/api/user.js](frontend/src/api/user.js) 新增 `clearBrowseHistory()` + - 前端 [frontend/src/views/history/HistoryList.vue](frontend/src/views/history/HistoryList.vue) `onClearAll` 改为先调 API 再清 state + 失败兜底 + - service 层 `browseHistoryService.clearAll` 早已实现,本次只是把链路打通 + +- [x] **Dashboard 6 张 KPI 卡可点击跳转** + - 用户总数 → `/admin/users` + - 商品总数 → `/admin/products` + - 订单总数 → `/admin/orders` + - 今日订单 → `/admin/orders?today=1` + - 总营收 → `/admin/orders?status=1` + - 待支付订单 → `/admin/orders?status=0` + - 卡片加 `.kpi-card-clickable` 样式:hover 上浮 2px + 箭头图标 + +- [x] **后端 `adminOrderController.list` 接 `?today=1`** + - 用 SQL `DATE(created_at) = CURDATE()` 直接交给 MySQL 算今天(与现有 stats.routes 的 CURDATE 口径对齐,避免 Node/MySQL 时区漂移) + - 前端 OrderManage 接 query 自动填筛选器 + tag 提示"📅 仅今日订单" + 可关闭 + +- [x] **修复 echarts 三图(销售趋势/分类/Top10)一直空白的根因** + - 根因:`composables/useChart.js` 的 `init()` 只在 `onMounted` 跑一次,但 chart 容器在 `v-if="stats"` 内,stats 是异步加载 → onMounted 时容器未挂载 → init 永远 return → `instance` 一直是 null → watch(option) 触发但拿不到 instance,永远 setOption 不上 + - 修复:把 init 从"一次性"改成"懒加载 + 兼容容器异步出现":watch(option) 在 nextTick 兜底 ensureInit + setOption + - 端到端 Node 端模拟验证:mount 时 init=0 → stats 加载后 init=1 + setOption 拿数据成功 + +### 2. Dashboard 五维区间切换器全栈打通 + +5 个独立互不干扰的维度,逐个接: + +| 切换器 | 影响字段 | UI 位置 | +|---|---|---| +| 📅 KPI 统计区间(5 选项)| userCount/productCount/orderCount/todayOrderCount/totalRevenue/pendingOrderCount + 6 张卡标题动态化 | 顶部 KPI 卡上方 | +| 📈 销售趋势区间(5 选项)| salesTrend 数组长度与聚合粒度(1w/1m 每日,6m/1y 按周 ISO,all 按月) | 销售趋势卡 header | +| 📈 销售指标区间(5 选项)| weeklySales/weeklyDeltaPct/repurchaseRate + 上下期对比 | 销售/复购卡上方 | +| 📊 分类/Top10 区间(5 选项)| categoryShare/topProducts | 分类/Top10 卡上方 | +| 🧾 订单状态区间(5 选项)| orderStatusRows(按 status 0-5 各分组) | 订单状态分布卡内 | + +后端改动 [stats.routes.js](backend/src/modules/admin/stats.routes.js): +- `kpiRange` / `metricRange` / `range` / `orderRange` / `trendInterval` 5 个独立参数 +- 全部按"天"为单位生成 SQL(`WHERE created_at >= DATE_SUB(NOW(), INTERVAL ? DAY)`) +- salesTrend 的聚合粒度按 `trendInterval` 切换:`DATE(created_at)` / `YEARWEEK(created_at, 3)` / `DATE_FORMAT(created_at, '%Y-%m')` +- 新增 helper `getISOWeek(d)` 与 MySQL `YEARWEEK(date, 3)` 一致 +- 返回字段多加 `kpiRangeKey / metricRangeKey / trendInterval / orderRangeKey` 等让前端显示当前区间 + +前端 [Dashboard.vue](frontend/src/views/admin/Dashboard.vue) 改造: +- 5 个 `radio-group`(5 选项:1w / 1m / 6m / 1y / all) +- 6 张 KPI 卡标题按 `KPI_TITLE` 字典动态化("用户总数" ↔ "近 1 月新增用户") +- 销售指标卡标题按 `METRIC_TITLE` 字典动态化("本周销售额" ↔ "全部销售额") +- 切换器样式:从浅灰底 + label 压缩成纯 `el-radio-button` 紧凑组 +- `<` `今` `>` 周历切换器仅在 `trendInterval='1w'` 时出现 + +### 3. UI 细节完善 + +- [x] **"较上期"标签 UI 优化 —— 永远显示方向,0% 用"持平"提示** + - 新增三个 helper:`deltaArrow / deltaText / deltaTagType` + - 三态:↑ 绿(上涨)/ ↓ 红(下降)/ — 灰(持平) + - 兜底文案:"— 全量数据,无上期"(all 区间)和"— 上期无数据" + +### 4. 后端 500 Bug 修复 + +- [x] **修复 `todayOrders is not defined` ReferenceError** + - 上一轮 KPI 改造时把 `const todayOrders` 重命名为 `todayOrdersRow`,但响应 payload 第 344 行仍引用 `todayOrders[0].c`,触发后端 500 + - 改为统一使用预计算的 `todayOrderCount` 变量 + - 前端 `fetch()` 同时补 `catch` + `ElMessage.error` 防止 Vue 警告冒泡到 mounted hook + +## 二、git 状态 + +``` +3c526fb fix(user-store): avatar 完全独立于 profile,退出再登录不再丢失 +e0b5874 fix(layout): FrontLayout 头像上传改为压缩+base64,仅本地持久化 +d7ff2d8 fix(user): 模型字段与 schema 对齐 + 个人信息编辑/注册时间/头像持久化 +``` + +> 注:今日新增的 Dashboard 改造 commit 因涉及对话落地未即时 push,预期下个工作日内统一整理提交。 + +## 三、遇到的问题与解决 + +| 问题 | 处理方式 | +|------|---------| +| Dashboard 5 维度参数同步时容易 stale(变量名 / 字段名)| 出了 `todayOrders` 500 bug 后全 grep 验证 + Node 端 require + 表达式语法检测 | +| 时间维度参数太多 + 默认值不一致 | 全部默认 `all`(向后兼容老调用方),其余 4 个加默认 `1w` 兼容 | +| salesTrend 按周/按月聚合的 key 要与 MySQL `YEARWEEK` 完全一致 | 在 stats.routes.js 写 `getISOWeek()` JS 版,与 MySQL mode=3 算法对齐 | +| Echarts v6 在两个分支的 import 路径都不稳定 | 沿用上次确定的方案(`echarts/core/charts/components/renderers` 子路径),package.json 锁版本 | +| Dashboard 改造牵一发动全身 | 每加一个切换器都跑一遍 Node 端模拟 + 浏览器手动复测,避免上次的 500 重演 | + +## 四、明日(8/04 周二)计划 + +- [ ] 把今日 Dashboard 改动整理成 4~5 个 commit,按"功能/UI/fix"分类提交到 develop +- [ ] 给团队周报(第 2 周:7/27 ~ 7/31)做最后汇总,明日 18:00 前提交 +- [ ] 第 2 周内有几位组员功能延期未提交:在组群催一次,定本周内收齐 +- [ ] 准备第 3 周挑战项进度盘点(统计看板已基本完成 ✅,剩余 C03/C04/C06 看 B/C 进度) + +## 五、阻塞 / 风险 + +| 风险 | 影响 | 应对 | +|------|------|------| +| Dashboard 改造未 git commit | develop 上别人拉不到最新代码 | 明日整理后批量提交,建议拆成 4~5 个小 commit 便于 review | +| 5 个区间参数互相独立但用户可能混淆 | 上线后运营 / PM 可能误操作 | 之前预留:可考虑把 5 个切换器合并为单个全局区间(下次站会决断)| +| salesTrend 按周 / 按月聚合在新装数据库无足够数据时会全 0 | 截图评审被误认为"图 bug" | 已经在 empty 文案改成"所选区间暂无数据",不需再动 | + +## 六、今日工时 + +约 **8 小时**(Dashboard 区间切换器全套 5h + bug 修复 1h + UI 优化 1h + 文档整理 1h) + +## 七、个人复盘 + +今天最大的收获是 **"在重构前先做依赖图"**。Dashboard 5 个区间切换器全栈打通,如果不是一开始就在 docs/02-设计文档/接口设计.md 加了"5 维参数互不影响"的设计原则,而是直接在代码里堆 `if-else`,到了第三/第四个就一定会出现"我以为我是改了 KPI,但顺手把分类也改了"的隐蔽 bug。先把心智模型写下来,5 个一气呵成,每个 30 分钟就稳了。 + +第二点是 **"修复一个 bug 就写一段防回归"**。今天最后一次出现的 `todayOrders is not defined`,正是上周也犯过一次的"rename 后忘了改引用"——所以顺手给 `fetch()` 加了 `catch + ElMessage.error`,让这种 500 在前端有第一层兜底,不会再裸奔到 Vue mounted 警告里。 diff --git "a/reports/weekly/\347\254\2542\345\221\250-\345\221\250\346\212\245.md" "b/reports/weekly/\347\254\2542\345\221\250-\345\221\250\346\212\245.md" new file mode 100644 index 0000000000000000000000000000000000000000..37041d58aaa3423dae19fc836b96a0b6b8d8dfee --- /dev/null +++ "b/reports/weekly/\347\254\2542\345\221\250-\345\221\250\346\212\245.md" @@ -0,0 +1,133 @@ +# 第 2 周周报 + +> 组别:八组 组长:胡晨辉(A) 编写日期:2026/08/03 版本:v1.0 +> 周期:第 2 周(2026-07-27 ~ 2026-07-31) +> 截止:每周五 18:00 前提交(本次延后至下周一 8/03 提交,迟交说明见 §六) + +--- + +## 一、本周目标回顾 + +| # | 计划目标(来自第 1 周日报) | 完成度 | 说明 | +|:-:|---|:-:|---| +| 1 | **A**:完善公共组件(图片上传、操作日志);盯 4 条接缝联调;开始挑战模块预研 | ✅ 完成 | 完成图片上传(base64 + 压缩)、操作日志卡片化、4 条接缝接口全部跑通 | +| 2 | **B(江禹辰)**:补全商品域全部接口+页面;发布 S01/S02/S03;预置演示数据 | ✅ 完成 | 30+ 商品 + 3 分类已落库;S01 ~ S03 已通过 D/C 联调 | +| 3 | **C(陈杨文)**:补全订单支付域全部接口+页面;联调 S01~S04;状态机跑通 | ✅ 完成 | 主链路「登录→加车→下单→支付→发货→收货」完整 demo 通 | +| 4 | **D(余泽)**:补全用户购物车域全部接口+页面;发布 S01/S04;开始测试计划草案 | 🟡 部分 | 90% 完成;测试计划草案延后至第 3 周初 | +| 5 | 周末:环形 Code Review(B↔C↔D,A 兜底) | 🟡 部分 | 周六做了主干整合,周日部分 review 留到第 3 周初 | +| 6 | **A 挑战项**:统计看板(截止 7/31)| ✅ 完成 | Dashboard 6 KPI + 4 图表 + 5 维区间切换器全栈打通 | + +## 二、本周主要产出 + +### 1. 主干整合(关键里程碑) + +- 周三凌晨:处理 PR #9 echarts v6 误提交的回滚,连续做了 3 次 commit/revert: + - `992f8ba !9 fix(echarts): v6 改用全量 ESM 导入(最稳)` + - `1d09653 Revert "fix(echarts): v6 改用全量 ESM 导入(最稳)"` + - 最终方案:保留 `echarts/core + echarts/charts + echarts/components + echarts/renderers` 子路径导入(Vite tree-shake 友好),锁定 `echarts@6.1.0` 到 package.json +- 周四:`26a074a` / `c54595f` 双亲合并 develop → feature/common-infra,一次性吸收 32 个 commit / 61 个文件 / 8402 行新增 / 5961 行删除 +- 周四:`df6d092 !10 fix(admin): 后台订单列表下单时间本地化显示` —— mysql2 时区 `'+08:00'` 修复 + +### 2. 业务模块全量上线 + +- **B(商品域)**:`5fed97b 电子商城商品管理后端` + `32362cf !16 电子商城商品管理前端端` + - 后台商品 CRUD + 上下架 + 软删 + - 后台分类管理 + 批量导入 +- **C(订单域)**:订单状态机跑通 + 模拟支付 + 超时取消定时器 +- **D(用户域)**:7f3e3ca → 28e8127 → c804fa6 → a90de6b 一系列购物车 + 个人中心 + 报告插图 + +### 3. A 公共基建包 v1.0 完成 + +- **操作日志卡片化** (`77fb85e`):统一分页器 + 详情弹窗 + 导出 CSV +- **后台 Dashboard 统计看板**(A 挑战项 C09): + - 后端 `GET /api/admin/stats`:6 KPI + 销售趋势 + 周销售 + 复购率 + 分类销量占比 + Top10 热销商品 + 订单状态分布 + - 前端 Dashboard.vue:6 KPI 卡片 + 4 张 echarts 图 + 订单状态表 + 快捷入口 + +### 4. 本周日报 + +- 7-27 ~ 7-30 共 4 天全部提交(部分补交,最终齐套) +- 见 `reports/daily/胡晨辉/` + +## 三、成员工作量统计 + +| 姓名 | 负责模块 | 本周主要工作 | 提交次数(develop)| 日报提交(天) | +|------|----------|-------------|:-:|:-:| +| **A · 胡晨辉**(组长) | 公共基建 + 后台 + 部署 | 主干整合 + echarts v6 选型 + 操作日志 + Dashboard + 主线复测 | 11 | 4/4 | +| **B · 江禹辰** | 商品域 | 商品后台 CRUD + 分类管理 + 演示数据预置 + 接缝 S01/S02/S03 发布 | 6 | 4/4 | +| **C · 陈杨文** | 订单支付域 | 订单状态机 + 模拟支付 + 超时取消 + 主链路联调 | 7 | 4/4 | +| **D · 余泽** | 用户购物车域 | 购物车完整版本 + 个人中心 + 浏览历史 + 收藏 + 测试计划延后 | 8 | 4/4 | + +> 提交数按 `git log --author=... --since=2026-07-27 --until=2026-08-02 23:59 --oneline` 统计,仅含到达 develop / master 的合并 commit。 + +## 四、本周接缝接口与挑战项状态 + +### 4 条接缝接口(已冻结后第 1 周评审) + +| # | 接口 | 提供方 | 调用方 | 本周状态 | +|:-:|---|---|---|:-:| +| S01 | `POST /api/products/batch` | B | D、C | ✅ 已联调,无改动 | +| S02 | `POST /api/products/:id/stock/decrement` | B | C | ✅ 联调中发现幂等键未正确传递,已修 | +| S03 | `POST /api/products/:id/stock/increment` | B | C | ✅ 同 S02 | +| S04 | `GET /api/cart/checkout` | D | C | ✅ 联调通过 | + +### 挑战项 + +| 项 | 题目 | 负责人 | 本周状态 | +|:-:|---|---|:-:| +| **C09** | 后台统计看板 | A | ✅ **已完成** | +| C04 | 搜索进阶 | B | 🟡 进行中(约 70%)| +| C03 | 订单超时取消 | C | ✅ 已完成(定时器 + 回补库存)| +| C06 | 商品评价晒图 | B | 🟡 进行中(约 50%)| +| 选做 1 | 商品收藏 | D | ✅ 已完成 | +| 选做 2 | 浏览历史 | D | ✅ 已完成 | + +> 全组挑战+选做 ≥ 3 项最低门槛已达成 ✅。 + +## 五、问题与风险 + +| 问题/风险 | 影响 | 应对措施 | 是否需要老师协助 | +|-----------|------|----------|:-:| +| **主干整合延迟 1 天**(周三凌晨事故 + 周四大合并) | 周报迟交 1 天;D 测试计划延后 | 已完成;本周三上午制度化做主干整合 | 否 | +| echarts v6 方案一度震荡(全量 ESM → 回滚 → 具名导入) | Dashboard 6 KPI 中间有一版不可用 | 现版本已稳;锁定版本与导入路径 | 否 | +| B/C 后台部分 UX 细节延期到第 3 周初补齐 | 周一站会 demo 不完整 | 已通知,周二前自测完 | 否 | +| C09 统计看板口径文档未发布 | 后续接手人不知 KPI 计算口径 | 第 3 周初补 `docs/02-设计文档/统计看板口径.md` | 否 | +| D 测试计划延后 | 测试报告进度影响 | 第 3 周补完整版;接受评审延期 | 否 | +| pnpm ↔ npm 包管理器切换 | 部分组员本地报错 | 已发群切换指南 | 否 | + +## 六、迟交说明(v1.0 新增) + +本应在周五(7/31)18:00 提交,实际下周一(8/03)早上提交。延迟原因: + +1. 7/30 周四主干整合用满一天,**整合报告和风险盘点**延迟到周五凌晨,未赶上当日文档; +2. 7/31 周五用于补做 echarts v6 方案的最终选型和 Dashboard 集成验证; +3. A 周五晚已编辑 v0.9 版本草稿,但内容与实际 commit 历史有几处不一致,需要重新对照 git log 校准; +4. 8/01-8/02 周末:A 还在补小组挑战项 C09 的个别边界场景;周报同步调整; +5. 8/03 周一上午 8:00 完成最终 v1.0。 + +**改进措施**:下周(第 3 周)的周报启动时间改为周三上午 10:00,跨周三~周五分三段写,到周五 17:00 收口,避免再迟交。 + +## 七、第 3 周(2026-08-03 ~ 2026-08-07)计划 + +| 任务 | 负责人 | 预计完成 | +|------|--------|----------| +| 整理本周 Dashboard 区间切换器改动为 4~5 个小 commit | A | 周二 8/04 | +| 补 `docs/02-设计文档/统计看板口径.md` 给团队共享 | A | 周三 8/05 | +| 跟进 B/C 延期部分的最终交付 | A | 周五 8/07 前 | +| C04 搜索进阶收尾(搜索建议词 + 高亮)| B | 周三 8/05 | +| C06 商品评价晒图收尾 | B | 周五 8/07 | +| 测试计划 + 测试报告(19 个用例)| D | 周五 8/07 前 | +| 第 3 周挑战项盘点 + 演示数据准备 | 全员 | 周五 8/07 | +| 第 3 周周报 v1.0 | A | 周五 8/07 18:00 前(按改进措施提前写) | + +## 八、组长签字 + +| 角色 | 姓名 | 签字/确认 | +|------|------|------| +| 组长 | A | 胡晨辉 | +| 组员 | B | 江禹辰 | +| 组员 | C | 陈杨文 | +| 组员 | D | 余泽 | + +--- + +**附**:本周与第 1 周文档连贯,已同步至 `docs/02-设计文档/统计看板口径.md`(待补)、`docs/03-测试文档/测试计划.md`(D 主笔)。