commit 24cd9cef53497b1f21478864b1a653cf303b1f17 Author: Yuzhiran Dev Date: Fri Jul 10 13:26:04 2026 +0800 🎉 feat: initialize 宇之然AI维度 project - uni-app Vue3 frontend with dark glassmorphism theme - 5 AI dimensions: Origin / Development / Current / Learning / Trend - AI Chat system prompt updated from geometry to AI - 23 AI knowledge articles initialized in DB - Trend API + cron job for daily news - Product pricing (Pro ¥19.9, VIP ¥39.9) - Pinia stores + API utilities - AGENTS.md documentation diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9e330da --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules +dist +pnpm-lock.yaml +.env +*.log \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..d7ccc75 --- /dev/null +++ b/README.md @@ -0,0 +1,54 @@ +# 宇之然AI维度 — 项目管理 + +## 目录结构 +``` +yuzhiran-ai-dimension/ +├── backend/ # 后端源码(从线上复制) +│ └── wdkj-server/ # Node.js Express 项目 +│ └── src/ # 49 个源文件 +├── frontend/ # 前端项目(待创建) +│ └── (uni-app 项目) +├── design/ +│ └── ui-mockup.html # 移动端 UI 设计稿 +└── docs/ + ├── product-plan.md # 产品规划文档 + └── api-assessment.md # API 评估与改造计划 +``` + +## 开发任务清单 + +### 阶段一:内容填充(1-2天) +- [ ] 用管理后台给 Knowledge 库批量填充 AI 内容 + - dim1 AI 起源:10 篇 + - dim2 AI 发展:10 篇 + - dim3 AI 当前:10 篇 + - dim4 AI 学习:15 篇 + - dim5 AI 趋势:cron 自动推送 +- [ ] 修改 AIModel 配置,对接云帆网关 + +### 阶段二:后端改造(1天) +- [ ] 改 AI Chat system prompt → AI 主题 +- [ ] 新增趋势资讯接口 +- [ ] 改商品数据(Pro/VIP 定价) + +### 阶段三:前端开发(uni-app 全新,1-2周) +- [ ] 初始化 uni-app 项目 +- [ ] 首页:5 维度卡片 + 今日快讯 +- [ ] 维度详情页:知识列表 + 问答 +- [ ] AI 问答页:对话界面 + 配额管理 +- [ ] 趋势页:资讯流 +- [ ] 个人中心:进度/收藏/订阅 +- [ ] 微信登录集成 + +### 阶段四:上架更新(1-3天) +- [ ] 改小程序名:宇之然AI维度 +- [ ] 简称:AI维度 +- [ ] 提审微信更新 + +## 命名与品牌 +- 微信小程序全名:宇之然AI维度 +- 微信小程序简称:AI维度 +- 小程序 appid:wxdad62baf4ccd09e3 +- 后端主域名:wdkj.yuzhiran.com.cn → :3001 +- 管理后台:wdkjadmin.yzrcloud.cn +- H5 地址:www.wdkj.yuzhiran.com.cn/h5/ \ No newline at end of file diff --git a/backend/wdkj-server/.gitignore b/backend/wdkj-server/.gitignore new file mode 100755 index 0000000..8ef4c93 --- /dev/null +++ b/backend/wdkj-server/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +.env +logs/ +*.log +.DS_Store diff --git a/backend/wdkj-server/README.md b/backend/wdkj-server/README.md new file mode 100755 index 0000000..857e8f0 --- /dev/null +++ b/backend/wdkj-server/README.md @@ -0,0 +1,263 @@ +# 宇之然·星辰绘线 - 后端服务 + +## 项目简介 + +这是宇之然·星辰绘线项目的后端服务,提供 RESTful API 接口,支持微信小程序和后台管理系统。 +# 1. 查找占用指定端口的进程ID +netstat -tunlp | grep :3001 +# 2. 根据进程ID kill进程 +kill -9 进程ID + +## 技术栈 + +- **运行时**: Node.js 18+ +- **框架**: Express.js 4.18 +- **数据库**: MongoDB (Mongoose ODM) +- **认证**: JWT +- **安全**: Helmet, CORS, Rate Limit + +## 项目结构 + +``` +wdkj-server/ +├── src/ +│ ├── config/ # 配置文件 +│ │ └── database.js # 数据库连接 +│ ├── middleware/ # 中间件 +│ │ └── auth.js # 认证中间件 +│ ├── models/ # 数据模型 +│ │ ├── User.js # 用户模型 +│ │ ├── Order.js # 订单模型 +│ │ ├── Gallery.js # 画廊模型 +│ │ ├── Knowledge.js # 知识库模型 +│ │ ├── ShopItem.js # 商品模型 +│ │ ├── Admin.js # 管理员模型 +│ │ └── AdminLog.js # 操作日志模型 +│ ├── routes/ # 路由 +│ │ ├── auth.js # 认证路由 +│ │ ├── user.js # 用户路由 +│ │ ├── payment.js # 支付路由 +│ │ ├── gallery.js # 画廊路由 +│ │ ├── knowledge.js # 知识库路由 +│ │ ├── admin.js # 管理后台路由 +│ │ └── index.js # 路由汇总 +│ ├── utils/ # 工具函数 +│ │ ├── response.js # 统一响应 +│ │ ├── logger.js # 日志工具 +│ │ └── weixin.js # 微信API工具 +│ └── index.js # 主入口 +├── logs/ # 日志目录 +├── .env # 环境变量 +├── package.json # 项目配置 +└── README.md # 项目说明 +``` + +## 快速开始 + +### 1. 安装依赖 + +```bash +cd wdkj-server +npm install +``` + +### 2. 配置环境变量 + +复制 `.env.example` 为 `.env` 并修改配置: + +```env +# 服务配置 +PORT=3000 +NODE_ENV=development + +# MongoDB 配置 +MONGODB_URI=mongodb://wdkj123:wdkj123@192.168.136.130:27017/wdkj?authSource=wdkj + +# JWT 配置 +JWT_SECRET=your_secret_key_here +JWT_EXPIRE=7d + +# 微信小程序配置 +WX_APPID=wxdad62baf4ccd09e3 +WX_SECRET=your_wx_app_secret + +# 微信支付配置 +WX_PAY_MCH_ID=1108945993 +WX_PAY_API_KEY=your_apiv3_key +``` + +### 3. 启动服务 + +```bash +# 开发环境 +npm run dev + +# 生产环境 +npm start +``` + +### 4. 验证服务 + +访问健康检查接口: + +```bash +curl http://localhost:3000/health +``` + +## API 文档 + +### 认证相关 + +- `POST /api/auth/login` - 小程序登录 +- `POST /api/auth/admin/login` - 管理员登录 +- `GET /api/auth/me` - 获取当前用户信息 + +### 用户相关 + +- `GET /api/user/progress` - 获取用户进度 +- `POST /api/user/progress` - 保存用户进度 +- `GET /api/user/leaderboard` - 获取排行榜 + +### 支付相关 + +- `POST /api/payment/create-order` - 创建订单 +- `POST /api/payment/callback` - 支付回调 +- `POST /api/payment/verify` - 验证支付 +- `GET /api/payment/products` - 获取商品列表 + +### 画廊相关 + +- `GET /api/gallery` - 获取作品列表 +- `POST /api/gallery` - 上传作品 +- `GET /api/gallery/:id` - 获取作品详情 +- `POST /api/gallery/:id/like` - 点赞作品 + +### 知识库相关 + +- `GET /api/knowledge` - 获取知识列表 +- `GET /api/knowledge/:id` - 获取知识详情 +- `POST /api/knowledge/:id/collect` - 收藏知识 +- `GET /api/knowledge/:dim/questions` - 获取问答 + +### 管理后台 + +- `GET /api/admin/dashboard` - 获取仪表盘数据 +- `GET /api/admin/users` - 获取用户列表 +- `GET /api/admin/users/:id` - 获取用户详情 +- `GET /api/admin/knowledge` - 获取知识列表 +- `POST /api/admin/knowledge` - 创建/更新知识 +- `POST /api/admin/knowledge/review` - 审核知识 +- `GET /api/admin/gallery` - 获取画廊作品 +- `POST /api/admin/gallery/review` - 审核作品 +- `GET /api/admin/orders` - 获取订单列表 +- `GET /api/admin/shop` - 获取商品列表 +- `POST /api/admin/shop` - 保存商品 +- `GET /api/admin/logs` - 获取操作日志 + +## 数据库初始化 + +首次启动服务时,需要初始化管理员账户: + +```javascript +// 可以创建一个初始化脚本 +const Admin = require('./src/models/Admin') + +async function initAdmin() { + const admin = new Admin({ + username: 'admin', + password: 'admin123456', + email: 'admin@wdkj.com', + role: 'super_admin', + permissions: ['*'] + }) + await admin.save() +} +``` + +## 开发指南 + +### 代码规范 + +- 使用 ES6+ 语法 +- 异步操作使用 async/await +- 错误统一通过 ApiResponse 处理 +- 所有操作记录日志 + +### 日志系统 + +日志级别: +- `debug`: 调试信息 +- `info`: 常规信息 +- `warn`: 警告信息 +- `error`: 错误信息 + +使用示例: + +```javascript +const logger = require('./utils/logger') + +logger.info('用户登录', { openid: 'xxx' }) +logger.error('数据库错误', error) +``` + +### 权限系统 + +角色权限矩阵: + +| 角色 | 权限范围 | +|------|----------| +| super_admin | 所有权限 (*) | +| content_manager | 知识库、画廊、用户查看 | +| shop_manager | 商品、订单、用户查看 | +| viewer | 仪表盘、分析、用户查看 | + +## 生产部署 + +### 使用 PM2 + +```bash +# 安装 PM2 +npm install -g pm2 + +# 启动服务 +pm2 start src/index.js --name wdkj-server + +# 查看状态 +pm2 status + +# 查看日志 +pm2 logs wdkj-server +``` + +### 使用 Docker + +创建 `Dockerfile`: + +```dockerfile +FROM node:18-alpine +WORKDIR /app +COPY package*.json ./ +RUN npm ci --only=production +COPY . . +EXPOSE 3000 +CMD ["npm", "start"] +``` + +构建并运行: + +```bash +docker build -t wdkj-server . +docker run -p 3000:3000 -d wdkj-server +``` + +## 注意事项 + +1. **安全**: 生产环境必须修改 JWT_SECRET +2. **数据库**: 确保 MongoDB 已正确配置访问权限 +3. **支付**: 微信支付需要真实的商户证书和密钥 +4. **日志**: 生产环境会自动写入日志文件 +5. **限流**: 已配置 API 请求频率限制 + +## 许可证 + +MIT License diff --git a/backend/wdkj-server/certs/apiclient_cert.pem b/backend/wdkj-server/certs/apiclient_cert.pem new file mode 100755 index 0000000..388b89b --- /dev/null +++ b/backend/wdkj-server/certs/apiclient_cert.pem @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIENDCCAxygAwIBAgIUYUzuDMy950xrnuZnUV3V4gBjtKQwDQYJKoZIhvcNAQEL +BQAwXjELMAkGA1UEBhMCQ04xEzARBgNVBAoTClRlbnBheS5jb20xHTAbBgNVBAsT +FFRlbnBheS5jb20gQ0EgQ2VudGVyMRswGQYDVQQDExJUZW5wYXkuY29tIFJvb3Qg +Q0EwHhcNMjYwMzI0MDYzNTUxWhcNMzEwMzIzMDYzNTUxWjCBjTETMBEGA1UEAwwK +MTEwODk0NTk5MzEbMBkGA1UECgwS5b6u5L+h5ZWG5oi357O757ufMTkwNwYDVQQL +DDDljJfkuqzlrofkuYvnhLbnp5HmioDkuK3lv4PvvIjkuKrkvZPlt6XllYbmiLfv +vIkxCzAJBgNVBAYTAkNOMREwDwYDVQQHDAhTaGVuWmhlbjCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBANKG2DnPUqF4kP/IErd0z7MejTXhfRT/9shGgNUm +SFQmK5Vb7owQyFORj5Y22r5Xa+g7t0Wjwmc8jjIFiIIUickf9IHedDzpclK6nPcf +whgEQ5YolA0yRSujlBdpDKxiZED/OdiF+oRNW3Orl4dC2eb/+yCvjr9IgXRwnn4l +ODP8DQ4/xW6JnQhyy+yQ4vydhkK1G1aaqS3kQAyQopgwcGif1e9Wo6JI1c+1MF98 +4KNLEVABg9sH2yRMYSsspyvJfdu+FA9oZT5LXGjSrdyaq7t8cESqRzhQMLvRcZ3o +QSbY9EE9Xy0dt9URS7ZdK53MwoRpnQsUSwpWT8r07f9sM1ECAwEAAaOBuTCBtjAJ +BgNVHRMEAjAAMAsGA1UdDwQEAwID+DCBmwYDVR0fBIGTMIGQMIGNoIGKoIGHhoGE +aHR0cDovL2V2Y2EuaXRydXMuY29tLmNuL3B1YmxpYy9pdHJ1c2NybD9DQT0xQkQ0 +MjIwRTUwREJDMDRCMDZBRDM5NzU0OTg0NkMwMUMzRThFQkQyJnNnPUhBQ0M0NzFC +NjU0MjJFMTJCMjdBOUQzM0E4N0FEMUNERjU5MjZFMTQwMzcxMA0GCSqGSIb3DQEB +CwUAA4IBAQCXQjHcCkJh/Wr3qvi5q3yBXA/zUchZOtBh220dGdEvR+L+9s9i7aGf +legNT0+E1AakD/W6TC25pL7oK3QmyjdW+JGdNtfVh5v+uUNu333mRPre5Bj94cfJ +9W7EHgfzEi3MJ3HXBclh7pCyxl6FRXXXj7STeBetVH0njvYRV6nOl7wobW77lE5N +JkUYPVPPd5pbS3BSpykBS4zxtaBK2MtzhhELonAbzib2z0edlwlGoJD0BFAr7BGT +00mM95Hz5I3W4UgHvv+mDlOMjBAwCtaDSSCoJDhiaxs/WEru4jSqY4aiKEsVrtAQ +zcR9dNa21f+uzO6/oziswYstXOkzeVoc +-----END CERTIFICATE----- diff --git a/backend/wdkj-server/certs/apiclient_key.pem b/backend/wdkj-server/certs/apiclient_key.pem new file mode 100755 index 0000000..ba94921 --- /dev/null +++ b/backend/wdkj-server/certs/apiclient_key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDShtg5z1KheJD/ +yBK3dM+zHo014X0U//bIRoDVJkhUJiuVW+6MEMhTkY+WNtq+V2voO7dFo8JnPI4y +BYiCFInJH/SB3nQ86XJSupz3H8IYBEOWKJQNMkUro5QXaQysYmRA/znYhfqETVtz +q5eHQtnm//sgr46/SIF0cJ5+JTgz/A0OP8VuiZ0IcsvskOL8nYZCtRtWmqkt5EAM +kKKYMHBon9XvVqOiSNXPtTBffOCjSxFQAYPbB9skTGErLKcryX3bvhQPaGU+S1xo +0q3cmqu7fHBEqkc4UDC70XGd6EEm2PRBPV8tHbfVEUu2XSudzMKEaZ0LFEsKVk/K +9O3/bDNRAgMBAAECggEBAJdPgZMzNmGFpTmhlAo1M566o6WJNnxkZ/uT6L7J8AxD +Duofk/kpisG9ieTd8iQB8zWLzfKIe431kQHUxkMv/cQHapX4y7SH2LorE+vt8HOQ +hP8klRxS+DIgNK0KvKgdY47voaTEzsROUR92wcbevnxvmQ/p/f/vXdPRqahimd2J +yZhIizb+H/d9ua5co5icVcupknK+mBna5uDqa/hoYPNVXmvzF4I1bZFeylN3v9CE +3YQt3qZ9OPPHl4zTQWkx7c2c+ELzOC9xWanUhfw41vtuv3hreZBZp9+jJpP5z9P7 +Rr1LCom7RtDNW0Py18p7GX3jAmjLoIehfqnIdjN0aU0CgYEA9ermlfZAP4W2MH+v +t97xlXn6JWBC9Mwlu7xqEJlfrdk8c/z5krBOoc+11WZoUZdtMlho1R8l4qn4GWUG +VbAQJm/M2kdMVTmDWnx/2DiCrqH07mjxf6UgX1kUwiBU4YZTI+qWSV9tzf7mXtpD +mupOnRInA9urvGocUgThllaCXasCgYEA2yh9McFKrkUZ6v9HFXekLWTjpVV2xXYU +4l7UJS8oMHMrIftP4O6EAvkOH6ZP1Ip61KQACfe0j0TiiXrLvpNXmxEe9gfaWTZY +OjYeGy0sdJQ8ByXzsaguPDI20qMn56nluDRWgh9JqIUB5ZYSE84wh9hhEJdJEi0a +uQVQusxZ3vMCgYBQcGa+dM8tOBKRKKMUtPM23rBJG4SrSD+regUDqveWCTHyCrSk +G9GMskGbLSVAUxTf2/VmiQq+arSBsf7xdmbB+935JEs+sVJa/dBfrJRqhQV2GpOs +GhpNtfjJSwQYxPRbEjfYhkOHlzJJooFsoenXDQIADBHPzrG2zmvv3hpEgwKBgDAd +L38g5HhmC06gRMbdwVmF8MR3gt/PEL6x+vk+5R/d6PZI6jyDoM/WRdeE97m9vUG5 +z5hgnW9mvCVapChuyKjulVGGX9V+LNh60Rk+Tag6cwNOvbWfKqdkjsynrTcbAafz +NZrTq6Qnr0bsL4PRnK32PzkZPZ2T8W31UMfx3TkpAoGBANuTZsojCvyL1ZfcB4P8 +Cf+Xv6CU5165DE+q+Sj3/HDrmDnDD9J4B0TlXj1803dydz4ZsC2BJqmCDXQGFI09 +FbQV3AIGE4XI2GWIC9f4+0W1FfAUp0Y2I5p7OMl4eN7RKh0PHCajN5pM534cZ0k/ +TQHNwOuaI56juLP4TIMKDPGk +-----END PRIVATE KEY----- diff --git a/backend/wdkj-server/certs/pub_key.pem b/backend/wdkj-server/certs/pub_key.pem new file mode 100755 index 0000000..0c84ba6 --- /dev/null +++ b/backend/wdkj-server/certs/pub_key.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzK+YibPERKTi8CaO+Yq +meTdJmaTqHTS/XBbnRmZgQmudwPIfybp1E+wPKixcBrw/qaj6Ewfoo+6eh++aUVw +ImiQcmYrF+4IgpaTJMfYwYgyeig8XlGlwNNngWPZ7g+7Q7FZUjMYJ4ITq2IHCyWR +IhQfPY9auZdBAndtcX9flDdUAzur2EDYVNYMlFMxt2wGnBGfXitsZefWdvRq5IA1 +N8zCxfCRvN9GFBjtjQXKeUmPwxLmcqKuA3fRz/THjq0oeggQK9PNQmtZgUD8nKE7 +cils+CFpkLK+a34iFIA2QHygL6itReyV7O47MD4gYYW+aCmp/L0FUcdISJtaCZ2T +9QIDAQAB +-----END PUBLIC KEY----- diff --git a/backend/wdkj-server/jest.config.js b/backend/wdkj-server/jest.config.js new file mode 100755 index 0000000..f608461 --- /dev/null +++ b/backend/wdkj-server/jest.config.js @@ -0,0 +1,13 @@ +module.exports = { + testEnvironment: 'node', + testMatch: ['**/tests/**/*.test.js'], + collectCoverageFrom: [ + 'src/**/*.js', + '!src/index.js' + ], + coverageDirectory: 'coverage', + coverageReporters: ['text', 'lcov', 'html'], + testTimeout: 30000, + setupFilesAfterEnv: ['/tests/setup.js'], + verbose: true +} diff --git a/backend/wdkj-server/package-lock.json b/backend/wdkj-server/package-lock.json new file mode 100755 index 0000000..9b64c5f --- /dev/null +++ b/backend/wdkj-server/package-lock.json @@ -0,0 +1,5763 @@ +{ + "name": "wdkj-server", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "wdkj-server", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "axios": "^1.6.2", + "bcryptjs": "^2.4.3", + "compression": "^1.7.4", + "cors": "^2.8.5", + "crypto-js": "^4.2.0", + "dotenv": "^16.3.1", + "express": "^4.18.2", + "express-rate-limit": "^7.1.5", + "express-validator": "^7.0.1", + "helmet": "^7.1.0", + "jsonwebtoken": "^9.0.2", + "moment": "^2.29.4", + "mongoose": "^8.0.3", + "morgan": "^1.10.0", + "multer": "^2.1.1", + "xml2js": "^0.6.2" + }, + "devDependencies": { + "jest": "^29.7.0", + "nodemon": "^3.0.2", + "supertest": "^7.2.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@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": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/core/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@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", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@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": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@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": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@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", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@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": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@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": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@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": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@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", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@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": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/traverse/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "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/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "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/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@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/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@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": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@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": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@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": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@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": "^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/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@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": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@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": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@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" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@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": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.6.tgz", + "integrity": "sha512-y+x3H1xBZd38n10NZF/rEBlvDOOMQ6LKUTHqr8R9VkJ+mmQOYtJFxIlkkK8fZrtOiL6VixbOBWMbZGBdal3Z1g==", + "license": "MIT", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@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.npmjs.org/@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/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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/node": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "license": "MIT" + }, + "node_modules/@types/whatwg-url": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "license": "MIT", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@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.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/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/accepts/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/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-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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.npmjs.org/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/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/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.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.14.0.tgz", + "integrity": "sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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.npmjs.org/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-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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.npmjs.org/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.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.13", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.13.tgz", + "integrity": "sha512-BL2sTuHOdy0YT1lYieUxTw/QMtPBC3pmlJC6xk8BBYVv6vcw3SGdKemQ+Xsx9ik2F/lYDO9tqsFQH1r9PFuHKw==", + "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.npmjs.org/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.npmjs.org/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.npmjs.org/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.npmjs.org/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/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "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.14.0", + "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/brace-expansion": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "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.npmjs.org/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.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "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.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "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.npmjs.org/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/bson": { + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz", + "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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.npmjs.org/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.npmjs.org/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.npmjs.org/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.30001782", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001782.tgz", + "integrity": "sha512-dZcaJLJeDMh4rELYFw1tvSn1bhZWYFOt468FcbHHxx/Z/dFidd1I6ciyFdi3iwfQCyOjqo9upF6lGQYtMiJWxw==", + "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.npmjs.org/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.npmjs.org/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.npmjs.org/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/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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": "8.0.1", + "resolved": "https://registry.npmjs.org/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" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/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.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/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.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/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/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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.npmjs.org/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.npmjs.org/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.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cookiejar": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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.npmjs.org/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.npmjs.org/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/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/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/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/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.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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.npmjs.org/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.npmjs.org/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.npmjs.org/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/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/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/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/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/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.329", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.329.tgz", + "integrity": "sha512-/4t+AS1l4S3ZC0Ja7PHFIWeBIxGA3QGqV8/yKsP36v7NcyUCl+bIcmw6s5zVuMIECWwBrAK/6QLzTmbJChBboQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/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.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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.npmjs.org/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.npmjs.org/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.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "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.npmjs.org/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.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/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/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "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.14.0", + "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-rate-limit": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/express-validator": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.3.1.tgz", + "integrity": "sha512-IGenaSf+DnWc69lKuqlRE9/i/2t5/16VpH5bXoqdxWz1aCpRvEdrBuu1y95i/iL5QP8ZYVATiwLFhwk3EDl5vg==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21", + "validator": "~13.15.23" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/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/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/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/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formidable": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/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.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/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.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/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.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "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" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/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/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/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/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/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/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/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/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/helmet": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/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.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/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.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/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.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/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.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/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.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/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": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "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": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@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": "^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-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/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" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/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": "^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-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/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": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@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": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@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": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@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": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/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": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "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/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "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": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/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": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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.npmjs.org/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": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/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": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/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/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/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.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "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/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/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/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/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/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/jsonwebtoken/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/kareem": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", + "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/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/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/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/make-dir/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/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.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "license": "MIT" + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/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.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/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.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/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.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/mongodb": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz", + "integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==", + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.3.0", + "bson": "^6.10.4", + "mongodb-connection-string-url": "^3.0.2" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.3.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", + "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^14.1.0 || ^13.0.0" + } + }, + "node_modules/mongoose": { + "version": "8.23.0", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.23.0.tgz", + "integrity": "sha512-Bul4Ha6J8IqzFrb0B1xpVzkC3S0sk43dmLSnhFOn8eJlZiLwL5WO6cRymmjaADdCMjUcCpj2ce8hZI6O4ZFSug==", + "license": "MIT", + "dependencies": { + "bson": "^6.10.4", + "kareem": "2.6.3", + "mongodb": "~6.20.0", + "mpath": "0.9.0", + "mquery": "5.0.0", + "ms": "2.1.3", + "sift": "17.1.3" + }, + "engines": { + "node": ">=16.20.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mongoose" + } + }, + "node_modules/mongoose/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/morgan": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", + "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==", + "license": "MIT", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.1.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/mpath": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", + "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mquery": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", + "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", + "license": "MIT", + "dependencies": { + "debug": "4.x" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/mquery/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/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/mquery/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.1.1.tgz", + "integrity": "sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "type-is": "^1.6.18" + }, + "engines": { + "node": ">= 10.16.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nodemon": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz", + "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^10.2.1", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/nodemon/node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/nodemon/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nodemon/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/nodemon/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/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.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/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/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/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/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/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/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/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.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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.npmjs.org/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.npmjs.org/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/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/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": ">= 6" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/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.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/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.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/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-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sift": { + "version": "17.1.3", + "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", + "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", + "license": "MIT" + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-update-notifier/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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.npmjs.org/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/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "license": "MIT", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/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/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/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.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/superagent": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/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/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/superagent/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/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/superagent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/supertest": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/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/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/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.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/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.21.3", + "resolved": "https://registry.npmjs.org/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/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/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/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/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.26", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.26.tgz", + "integrity": "sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/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/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "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/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/backend/wdkj-server/package.json b/backend/wdkj-server/package.json new file mode 100755 index 0000000..4a87662 --- /dev/null +++ b/backend/wdkj-server/package.json @@ -0,0 +1,45 @@ +{ + "name": "wdkj-server", + "version": "1.0.0", + "description": "宇之然-空间探索 - 后端服务", + "main": "src/index.js", + "scripts": { + "start": "node src/index.js", + "dev": "nodemon src/index.js", + "test": "jest" + }, + "keywords": [ + "wechat", + "miniprogram", + "geometry", + "education" + ], + "author": "宇之然团队", + "license": "MIT", + "dependencies": { + "axios": "^1.6.2", + "bcryptjs": "^2.4.3", + "compression": "^1.7.4", + "cors": "^2.8.5", + "crypto-js": "^4.2.0", + "dotenv": "^16.3.1", + "express": "^4.18.2", + "express-rate-limit": "^7.1.5", + "express-validator": "^7.0.1", + "helmet": "^7.1.0", + "jsonwebtoken": "^9.0.2", + "moment": "^2.29.4", + "mongoose": "^8.0.3", + "morgan": "^1.10.0", + "multer": "^2.1.1", + "xml2js": "^0.6.2" + }, + "devDependencies": { + "jest": "^29.7.0", + "nodemon": "^3.0.2", + "supertest": "^7.2.2" + }, + "engines": { + "node": ">=22.0.0" + } +} diff --git a/backend/wdkj-server/run-tests.bat b/backend/wdkj-server/run-tests.bat new file mode 100755 index 0000000..e320924 --- /dev/null +++ b/backend/wdkj-server/run-tests.bat @@ -0,0 +1,19 @@ +@echo off +echo ======================================== +echo 运行单元测试和集成测试 +echo ======================================== +echo. + +cd %~dp0 + +echo [1/3] 设置测试环境... +set NODE_ENV=test + +echo [2/3] 运行测试... +call npm test + +echo. +echo [3/3] 生成测试报告... +echo 测试完成! + +pause diff --git a/backend/wdkj-server/scripts/create-mongo-user.bat b/backend/wdkj-server/scripts/create-mongo-user.bat new file mode 100755 index 0000000..771226b --- /dev/null +++ b/backend/wdkj-server/scripts/create-mongo-user.bat @@ -0,0 +1,37 @@ +@echo off +echo ======================================== +echo MongoDB 用户创建工具 +echo ======================================== +echo. +echo 请在 MongoDB 服务器上执行以下命令: +echo. +echo 1. 连接到 MongoDB: +echo mongo --host 192.168.136.130 --port 27017 +echo. +echo 2. 切换到 admin 数据库: +echo use admin +echo. +echo 3. 创建管理员用户(如果还没有): +echo db.createUser({ +echo user: "admin", +echo pwd: "admin123", +echo roles: ["root"] +echo }) +echo. +echo 4. 切换到 wdkj 数据库: +echo use wdkj +echo. +echo 5. 创建 wdkj 用户: +echo db.createUser({ +echo user: "wdkj123", +echo pwd: "wdkj123", +echo roles: [ +echo { role: "readWrite", db: "wdkj" } +echo ] +echo }) +echo. +echo 6. 验证用户: +echo db.auth("wdkj123", "wdkj123") +echo. +echo ======================================== +pause diff --git a/backend/wdkj-server/scripts/fix-bgm-urls.js b/backend/wdkj-server/scripts/fix-bgm-urls.js new file mode 100755 index 0000000..9fbafd5 --- /dev/null +++ b/backend/wdkj-server/scripts/fix-bgm-urls.js @@ -0,0 +1,36 @@ +/** + * 快速修复BGM URL - 使用测试音频 + */ + +const mongoose = require('mongoose') +require('dotenv').config() + +const { BGM } = require('../src/models') + +// 免费的测试音频URL (SoundHelix - 可商用) +const TEST_AUDIO_URL = 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3' + +async function fixBGMUrls() { + try { + await mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/yuzhiran') + console.log('✅ 数据库连接成功') + + // 更新所有BGM的URL为测试音频 + const result = await BGM.updateMany( + {}, + { url: TEST_AUDIO_URL } + ) + + console.log(`✅ 已更新 ${result.modifiedCount} 条BGM记录`) + console.log(` 新URL: ${TEST_AUDIO_URL}`) + console.log('\n💡 现在可以测试BGM播放功能了') + console.log('⚠️ 注意: 这是测试音频,生产环境请替换为正式音乐') + + } catch (error) { + console.error('❌ 更新失败:', error) + } finally { + await mongoose.disconnect() + } +} + +fixBGMUrls() diff --git a/backend/wdkj-server/scripts/init-bgm.js b/backend/wdkj-server/scripts/init-bgm.js new file mode 100755 index 0000000..129f974 --- /dev/null +++ b/backend/wdkj-server/scripts/init-bgm.js @@ -0,0 +1,171 @@ +/** + * BGM音效初始化脚本 + * 为每个维度添加示例背景音乐 + */ + +const mongoose = require('mongoose') +require('dotenv').config() + +// 导入模型 +const { BGM } = require('../src/models') + +// 示例BGM数据(实际使用时需要替换为真实的音频URL) +const sampleBGMs = [ + // 一维 - 线性旋律 + { + name: '星光轨迹', + dimension: 1, + url: 'https://example.com/bgm/dim1_starlight.mp3', + duration: 120, + loop: true, + volume: 0.4, + sortOrder: 1, + description: '轻柔的线性旋律,适合一维空间的探索', + isActive: true + }, + { + name: '弦之舞', + dimension: 1, + url: 'https://example.com/bgm/dim1_string_dance.mp3', + duration: 95, + loop: true, + volume: 0.45, + sortOrder: 2, + description: '活泼的弦乐节奏', + isActive: true + }, + + // 二维 - 平面和声 + { + name: '星座幻想', + dimension: 2, + url: 'https://example.com/bgm/dim2_constellation.mp3', + duration: 150, + loop: true, + volume: 0.5, + sortOrder: 1, + description: '梦幻的星空音乐,配合二维星座绘制', + isActive: true + }, + { + name: '几何韵律', + dimension: 2, + url: 'https://example.com/bgm/dim2_geometry.mp3', + duration: 110, + loop: true, + volume: 0.48, + sortOrder: 2, + description: '轻快的几何节奏', + isActive: true + }, + + // 三维 - 立体环绕 + { + name: '立方体回响', + dimension: 3, + url: 'https://example.com/bgm/dim3_cube_echo.mp3', + duration: 180, + loop: true, + volume: 0.5, + sortOrder: 1, + description: '空间感强烈的立体音效', + isActive: true + }, + { + name: '多维共振', + dimension: 3, + url: 'https://example.com/bgm/dim3_resonance.mp3', + duration: 140, + loop: true, + volume: 0.52, + sortOrder: 2, + description: '富有层次感的三维音效', + isActive: true + }, + + // 四维 - 时间流动 + { + name: '时空涟漪', + dimension: 4, + url: 'https://example.com/bgm/dim4_spacetime.mp3', + duration: 200, + loop: true, + volume: 0.45, + sortOrder: 1, + description: '神秘的四维时空音乐', + isActive: true + }, + { + name: '超立方漫游', + dimension: 4, + url: 'https://example.com/bgm/dim4_tesseract.mp3', + duration: 165, + loop: true, + volume: 0.47, + sortOrder: 2, + description: '穿越维度的奇妙旅程', + isActive: true + }, + + // 五维 - 高维混沌 + { + name: '思维风暴', + dimension: 5, + url: 'https://example.com/bgm/dim5_thought_storm.mp3', + duration: 220, + loop: true, + volume: 0.5, + sortOrder: 1, + description: '复杂而迷人的高维音效', + isActive: true + }, + { + name: '量子纠缠', + dimension: 5, + url: 'https://example.com/bgm/dim5_quantum.mp3', + duration: 190, + loop: true, + volume: 0.48, + sortOrder: 2, + description: '微观世界的奇妙声音', + isActive: true + } +] + +async function initBGM() { + try { + // 连接数据库 + await mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/yuzhiran') + console.log('✅ 数据库连接成功') + + // 检查是否已有BGM数据 + const existingCount = await BGM.countDocuments() + if (existingCount > 0) { + console.log(`⚠️ 数据库中已有 ${existingCount} 条BGM记录,跳过初始化`) + await mongoose.disconnect() + return + } + + // 插入示例数据 + const result = await BGM.insertMany(sampleBGMs) + console.log(`✅ 成功初始化 ${result.length} 条BGM记录`) + + // 按维度统计 + for (let dim = 1; dim <= 5; dim++) { + const count = await BGM.countDocuments({ dimension: dim }) + console.log(` 维度${dim}: ${count} 首`) + } + + console.log('\n🎵 BGM初始化完成!') + console.log('💡 提示:请在后台管理系统中上传真实的音频文件并更新URL') + + } catch (error) { + console.error('❌ BGM初始化失败:', error) + } finally { + await mongoose.disconnect() + console.log('数据库连接已关闭') + } +} + +// 执行初始化 +initBGM() diff --git a/backend/wdkj-server/scripts/init-db.js b/backend/wdkj-server/scripts/init-db.js new file mode 100755 index 0000000..dab5c76 --- /dev/null +++ b/backend/wdkj-server/scripts/init-db.js @@ -0,0 +1,218 @@ +require('dotenv').config({ path: require('path').resolve(__dirname, '../.env') }) +const mongoose = require('mongoose') +const { Admin, ShopItem, Knowledge, Gallery } = require('../src/models') + +/** + * 数据库初始化脚本 + */ +async function initDatabase() { + try { + // 连接数据库 + const mongoURI = process.env.MONGODB_URI + await mongoose.connect(mongoURI) + console.log('✅ 数据库连接成功') + + // 1. 创建超级管理员 + console.log('\n📋 创建超级管理员...') + const existingAdmin = await Admin.findOne({ username: 'admin' }) + + if (!existingAdmin) { + const admin = new Admin({ + username: process.env.ADMIN_USERNAME || 'admin', + password: process.env.ADMIN_PASSWORD || 'admin123456', + email: process.env.ADMIN_EMAIL || 'admin@wdkj.com', + realName: '超级管理员', + role: 'super_admin', + permissions: ['*'], + status: 'active' + }) + await admin.save() + console.log('✅ 超级管理员创建成功') + console.log(` 用户名: ${admin.username}`) + console.log(` 密码: ${process.env.ADMIN_PASSWORD || 'admin123456'}`) + } else { + console.log('⚠️ 管理员账户已存在') + } + + // 2. 初始化商品 + console.log('\n📋 初始化商品...') + const products = [ + { + itemId: 'skin_quantum', + name: '量子线皮肤', + description: '一维世界专属皮肤,让你的线条闪耀量子光芒', + price: 6, + type: 'skin', + status: 'active', + sortOrder: 1 + }, + { + itemId: 'skin_hologram', + name: '全息面皮肤', + description: '二维世界专属皮肤,让你的图形绽放全息色彩', + price: 18, + type: 'skin', + status: 'active', + sortOrder: 2 + }, + { + itemId: 'skin_nebula', + name: '星云体皮肤', + description: '三维世界专属皮肤,让你的立方体流光溢彩', + price: 30, + type: 'skin', + status: 'active', + sortOrder: 3 + }, + { + itemId: 'no_ads', + name: '永久去广告', + description: '一键去除所有广告,享受纯净的维度探索之旅。', + price: 1200, + type: 'ad_free', + status: 'active', + sortOrder: 4 + }, + { + itemId: 'monthly_sub', + name: '星云月卡', + description: '尊享月度特权:全皮肤免费用、得分翻倍、专属身份标识。', + price: 1800, + type: 'vip', + duration: 30, + status: 'active', + sortOrder: 5 + } + ] + + for (const product of products) { + const existing = await ShopItem.findOne({ itemId: product.itemId }) + if (!existing) { + await ShopItem.create(product) + console.log(` ✅ ${product.name} 创建成功`) + } else { + console.log(` ⚠️ ${product.name} 已存在`) + } + } + + // 3. 初始化知识库示例 + console.log('\n📋 初始化知识库示例...') + const knowledgeList = [ + { + title: '什么是维度?', + content: '维度是描述空间独立方向的参数。零维是点,一维是线,二维是面,三维是体。', + dim: 1, + category: 'concept', + tags: ['基础概念', '维度'], + status: 'approved', + sortOrder: 1 + }, + { + title: '一维世界:线的宇宙', + content: '一维世界只有长度这一个维度,就像一条无限延伸的直线。生活在一维世界的生物只能前进或后退。', + dim: 1, + category: 'concept', + tags: ['一维', '线'], + status: 'approved', + sortOrder: 2 + }, + { + title: '二维世界:平面王国', + content: '二维世界有长度和宽度两个维度,就像一张无限大的纸。平面国中的生物可以在平面上自由移动。', + dim: 2, + category: 'concept', + tags: ['二维', '平面'], + status: 'approved', + sortOrder: 1 + }, + { + title: '三维世界:立体空间', + content: '三维世界有长度、宽度和高度三个维度,这正是我们生活的世界。我们可以前后、左右、上下移动。', + dim: 3, + category: 'concept', + tags: ['三维', '立体'], + status: 'approved', + sortOrder: 1 + } + ] + + for (const knowledge of knowledgeList) { + const existing = await Knowledge.findOne({ title: knowledge.title }) + if (!existing) { + await Knowledge.create(knowledge) + console.log(` ✅ ${knowledge.title} 创建成功`) + } else { + console.log(` ⚠️ ${knowledge.title} 已存在`) + } + } + + // 4. 初始化画廊示例作品 + console.log('\n📋 初始化画廊示例作品...') + const galleryWorks = [ + { + title: '一维·星点轨迹', + description: '在一维世界中,点动成线。这是我探索宇宙起点的轨迹记录。', + imageData: '/static/images/gallery_demo_dim1.png', + dim: 1, + openid: 'system_admin', + authorName: '系统管理员', + authorAvatar: '/static/images/default_avatar.png', + likeCount: 128, + tags: ['一维', '轨迹', '起点'], + status: 'approved', + createdAt: new Date() + }, + { + title: '二维·星座图腾', + description: '连接星体,绘制出属于我的平面王国图腾。', + imageData: '/static/images/gallery_demo_dim2.png', + dim: 2, + openid: 'system_admin', + authorName: '星辰旅者', + authorAvatar: '/static/images/default_avatar.png', + likeCount: 96, + tags: ['二维', '星座', '图腾'], + status: 'approved', + createdAt: new Date() + }, + { + title: '三维·流光立方', + description: '旋转的立方体展现了立体几何的魅力,色彩在空间中流动。', + imageData: '/static/images/gallery_demo_dim3.png', + dim: 3, + openid: 'system_admin', + authorName: '维度探索者', + authorAvatar: '/static/images/default_avatar.png', + likeCount: 215, + tags: ['三维', '立方体', '流光'], + status: 'approved', + createdAt: new Date() + } + ] + + for (const work of galleryWorks) { + const existing = await Gallery.findOne({ title: work.title }) + if (!existing) { + await Gallery.create(work) + console.log(` ✅ ${work.title} 创建成功`) + } else { + console.log(` ⚠️ ${work.title} 已存在`) + } + } + + console.log('\n✅ 数据库初始化完成!') + console.log('\n📝 管理员登录信息:') + console.log(` 用户名: ${process.env.ADMIN_USERNAME || 'admin'}`) + console.log(` 密码: ${process.env.ADMIN_PASSWORD || 'admin123456'}`) + + } catch (error) { + console.error('❌ 初始化失败:', error) + } finally { + await mongoose.connection.close() + console.log('\n🔌 数据库连接已关闭') + process.exit(0) + } +} + +// 执行初始化 +initDatabase() diff --git a/backend/wdkj-server/scripts/init-knowledge-data.js b/backend/wdkj-server/scripts/init-knowledge-data.js new file mode 100755 index 0000000..597d07d --- /dev/null +++ b/backend/wdkj-server/scripts/init-knowledge-data.js @@ -0,0 +1,182 @@ +/** + * 初始化知识数据脚本 + * 用于填充测试知识内容 + */ + +const mongoose = require('mongoose') +require('dotenv').config() + +// 导入知识模型 +const Knowledge = require('../src/models/Knowledge') + +// 测试知识数据 +const testKnowledgeData = [ + // 一维知识 + { + title: '一维空间的基本概念', + content: '一维空间是数学中最简单的空间形式,它只有长度这一个维度。在物理学中,一维空间可以用来描述直线运动,比如物体在直线上来回运动。一维空间的特点是所有点都在同一条直线上,没有宽度和高度。', + dim: 1, + category: 'concept', + tags: ['一维', '空间', '数学'], + status: 'approved', + isPremium: false, + requiredPoints: 0, + price: 0 + }, + { + title: '一维空间的数学表示', + content: '在数学中,一维空间通常用实数轴来表示。实数轴上的每个点对应一个实数,点与点之间的距离就是它们对应实数的差的绝对值。一维空间中的向量只有一个分量,可以用一个实数来表示。', + dim: 1, + category: 'application', + tags: ['一维', '数学', '实数轴'], + status: 'approved', + isPremium: false, + requiredPoints: 0, + price: 0 + }, + + // 二维知识 + { + title: '二维空间的几何特性', + content: '二维空间具有两个维度:长度和宽度。在二维空间中,我们可以定义平面几何图形,如点、线、三角形、圆形等。二维空间中的每个点可以用两个坐标(x, y)来表示。', + dim: 2, + category: 'concept', + tags: ['二维', '几何', '平面'], + status: 'approved', + isPremium: false, + requiredPoints: 10, + price: 0 + }, + { + title: '二维坐标系的应用', + content: '二维坐标系在计算机图形学、地理信息系统、工程设计等领域有广泛应用。笛卡尔坐标系是最常见的二维坐标系,它使用相互垂直的x轴和y轴来定位平面上的点。', + dim: 2, + category: 'application', + tags: ['二维', '坐标系', '应用'], + status: 'approved', + isPremium: false, + requiredPoints: 20, + price: 0 + }, + + // 三维知识 + { + title: '理解三维空间', + content: '三维空间是我们最熟悉的空间形式,它具有长度、宽度和高度三个维度。在三维空间中,物体具有体积,可以定义立体几何图形如立方体、球体、圆柱体等。', + dim: 3, + category: 'concept', + tags: ['三维', '空间', '立体'], + status: 'approved', + isPremium: false, + requiredPoints: 30, + price: 0 + }, + { + title: '三维建模技术', + content: '三维建模技术在动画制作、游戏开发、建筑设计等领域有重要应用。常见的三维建模方法包括多边形建模、NURBS建模、体素建模等。', + dim: 3, + category: 'application', + tags: ['三维', '建模', '技术'], + status: 'approved', + isPremium: false, + requiredPoints: 40, + price: 0 + }, + + // 四维知识(付费内容) + { + title: '四维时空的概念', + content: '四维时空是爱因斯坦相对论中的核心概念,它将三维空间与时间维度结合。在四维时空中,事件的发生不仅取决于空间位置,还取决于时间点。四维时空的几何特性由闵可夫斯基度规描述。', + dim: 4, + category: 'concept', + tags: ['四维', '时空', '相对论'], + status: 'approved', + isPremium: true, + requiredPoints: 0, + price: 9.9 + }, + { + title: '四维空间的可视化', + content: '由于人类无法直接感知四维空间,科学家们开发了多种可视化技术。常见的方法包括投影法、切片法、颜色编码法等,帮助我们理解高维空间的特性。', + dim: 4, + category: 'application', + tags: ['四维', '可视化', '高维'], + status: 'approved', + isPremium: true, + requiredPoints: 0, + price: 12.9 + }, + + // 五维知识(高级付费内容) + { + title: '五维空间的物理意义', + content: '五维空间理论在弦理论和卡鲁扎-克莱因理论中有重要应用。在五维空间中,除了三维空间和一维时间外,还可能存在额外的紧致维度。这些理论试图统一引力与其他基本力。', + dim: 5, + category: 'concept', + tags: ['五维', '弦理论', '统一理论'], + status: 'approved', + isPremium: true, + requiredPoints: 0, + price: 19.9 + }, + { + title: '高维空间的数学基础', + content: '高维空间的数学研究涉及线性代数、微分几何、拓扑学等多个领域。n维空间中的点可以用n个坐标表示,距离和角度等概念可以通过推广低维空间的定义来建立。', + dim: 5, + category: 'application', + tags: ['高维', '数学', '几何'], + status: 'approved', + isPremium: true, + requiredPoints: 0, + price: 24.9 + } +] + +async function initKnowledgeData() { + try { + // 连接数据库 + await mongoose.connect(process.env.MONGODB_URI, { + useNewUrlParser: true, + useUnifiedTopology: true + }) + + console.log('✅ 数据库连接成功') + + // 清空现有知识数据(可选) + await Knowledge.deleteMany({}) + console.log('✅ 已清空现有知识数据') + + // 插入测试数据 + const inserted = await Knowledge.insertMany(testKnowledgeData) + console.log(`✅ 成功插入 ${inserted.length} 条知识数据`) + + // 显示插入的数据统计 + const dimCounts = {} + inserted.forEach(item => { + dimCounts[item.dim] = (dimCounts[item.dim] || 0) + 1 + }) + + console.log('📊 知识数据统计:') + Object.keys(dimCounts).sort().forEach(dim => { + console.log(` ${dim}维知识: ${dimCounts[dim]} 条`) + }) + + const premiumCount = inserted.filter(item => item.isPremium).length + const freeCount = inserted.length - premiumCount + console.log(` 💰 付费内容: ${premiumCount} 条`) + console.log(` 🆓 免费内容: ${freeCount} 条`) + + } catch (error) { + console.error('❌ 初始化知识数据失败:', error) + } finally { + await mongoose.connection.close() + console.log('🔚 数据库连接已关闭') + } +} + +// 执行初始化 +if (require.main === module) { + initKnowledgeData() +} + +module.exports = { initKnowledgeData } \ No newline at end of file diff --git a/backend/wdkj-server/scripts/initPinyinData.js b/backend/wdkj-server/scripts/initPinyinData.js new file mode 100755 index 0000000..1d9e445 --- /dev/null +++ b/backend/wdkj-server/scripts/initPinyinData.js @@ -0,0 +1,241 @@ +/** + * 拼音探索模块初始化数据脚本 + * 创建拼音内容、成就等基础数据 + */ + +require('dotenv').config({ path: './.env' }); +const mongoose = require('mongoose'); +const { PinyinContent, PinyinAchievement } = require('../src/models/pinyin'); +const connectDB = require('../src/config/database'); + +// 声母数据 +const initials = [ + { symbol: 'b', name: '玻', order: 1 }, + { symbol: 'p', name: '坡', order: 2 }, + { symbol: 'm', name: '摸', order: 3 }, + { symbol: 'f', name: '佛', order: 4 }, + { symbol: 'd', name: '得', order: 5 }, + { symbol: 't', name: '特', order: 6 }, + { symbol: 'n', name: '讷', order: 7 }, + { symbol: 'l', name: '勒', order: 8 }, + { symbol: 'g', name: '哥', order: 9 }, + { symbol: 'k', name: '科', order: 10 }, + { symbol: 'h', name: '喝', order: 11 }, + { symbol: 'j', name: '基', order: 12 }, + { symbol: 'q', name: '期', order: 13 }, + { symbol: 'x', name: '希', order: 14 }, + { symbol: 'zh', name: '知', order: 15 }, + { symbol: 'ch', name: '蚩', order: 16 }, + { symbol: 'sh', name: '诗', order: 17 }, + { symbol: 'r', name: '日', order: 18 }, + { symbol: 'z', name: '资', order: 19 }, + { symbol: 'c', name: '雌', order: 20 }, + { symbol: 's', name: '思', order: 21 }, + { symbol: 'y', name: '医', order: 22 }, + { symbol: 'w', name: '巫', order: 23 } +]; + +// 韵母数据 +const finals = [ + // 单韵母 + { symbol: 'a', name: '啊', order: 1 }, + { symbol: 'o', name: '喔', order: 2 }, + { symbol: 'e', name: '鹅', order: 3 }, + { symbol: 'i', name: '衣', order: 4 }, + { symbol: 'u', name: '乌', order: 5 }, + { symbol: 'ü', name: '迂', order: 6 }, + // 复韵母 + { symbol: 'ai', name: '哀', order: 7 }, + { symbol: 'ei', name: '诶', order: 8 }, + { symbol: 'ui', name: '威', order: 9 }, + { symbol: 'ao', name: '熬', order: 10 }, + { symbol: 'ou', name: '欧', order: 11 }, + { symbol: 'iu', name: '优', order: 12 }, + { symbol: 'ie', name: '耶', order: 13 }, + { symbol: 'üe', name: '约', order: 14 }, + { symbol: 'er', name: '儿', order: 15 }, + // 前鼻韵母 + { symbol: 'an', name: '安', order: 16 }, + { symbol: 'en', name: '恩', order: 17 }, + { symbol: 'in', name: '因', order: 18 }, + { symbol: 'un', name: '温', order: 19 }, + { symbol: 'ün', name: '晕', order: 20 }, + // 后鼻韵母 + { symbol: 'ang', name: '昂', order: 21 }, + { symbol: 'eng', name: '亨', order: 22 }, + { symbol: 'ing', name: '英', order: 23 }, + { symbol: 'ong', name: '雍', order: 24 } +]; + +// 整体认读音节 +const overalls = [ + { symbol: 'zhi', name: '织', order: 1 }, + { symbol: 'chi', name: '吃', order: 2 }, + { symbol: 'shi', name: '诗', order: 3 }, + { symbol: 'ri', name: '日', order: 4 }, + { symbol: 'zi', name: '资', order: 5 }, + { symbol: 'ci', name: '雌', order: 6 }, + { symbol: 'si', name: '思', order: 7 }, + { symbol: 'yi', name: '衣', order: 8 }, + { symbol: 'wu', name: '乌', order: 9 }, + { symbol: 'yu', name: '迂', order: 10 }, + { symbol: 'ye', name: '耶', order: 11 }, + { symbol: 'yue', name: '约', order: 12 }, + { symbol: 'yuan', name: '冤', order: 13 }, + { symbol: 'yin', name: '因', order: 14 }, + { symbol: 'yun', name: '晕', order: 15 }, + { symbol: 'ying', name: '英', order: 16 } +]; + +// 成就数据 +const achievements = [ + // 探索类成就 + { code: 'first_explore', name: '初次探索', description: '完成首次拼音探索', type: 'explore', condition: { type: 'explore_count', value: 1 } }, + { code: 'explorer_5', name: '初级探索者', description: '探索5个拼音', type: 'explore', condition: { type: 'explore_count', value: 5 } }, + { code: 'explorer_10', name: '中级探索者', description: '探索10个拼音', type: 'explore', condition: { type: 'explore_count', value: 10 } }, + { code: 'explorer_23', name: '声母专家', description: '探索所有声母', type: 'explore', condition: { type: 'explore_count', value: 23 } }, + { code: 'explorer_47', name: '拼音达人', description: '探索47个拼音', type: 'explore', condition: { type: 'explore_count', value: 47 } }, + { code: 'explorer_all', name: '拼音大师', description: '探索所有拼音', type: 'explore', condition: { type: 'explore_count', value: 63 } }, + + // 收集类成就 + { code: 'first_stone', name: '第一颗能量石', description: '收集第一颗能量石', type: 'collection', condition: { type: 'collect_count', value: 1 } }, + { code: 'collector_5', name: '能量收集者', description: '收集5颗能量石', type: 'collection', condition: { type: 'collect_count', value: 5 } }, + { code: 'collector_10', name: '能量守护者', description: '收集10颗能量石', type: 'collection', condition: { type: 'collect_count', value: 10 } }, + { code: 'collector_23', name: '声母守护者', description: '收集所有声母能量石', type: 'collection', condition: { type: 'collect_count', value: 23 } }, + { code: 'collector_all', name: '能量大师', description: '收集所有能量石', type: 'collection', condition: { type: 'collect_count', value: 63 } }, + + // 连续探索成就 + { code: 'streak_3', name: '坚持3天', description: '连续探索3天', type: 'streak', condition: { type: 'streak_days', value: 3 } }, + { code: 'streak_7', name: '坚持一周', description: '连续探索7天', type: 'streak', condition: { type: 'streak_days', value: 7 } }, + { code: 'streak_30', name: '坚持一个月', description: '连续探索30天', type: 'streak', condition: { type: 'streak_days', value: 30 } } +]; + +async function initPinyinContents() { + console.log('开始初始化拼音内容...'); + + // 初始化声母 + for (const item of initials) { + await PinyinContent.findOneAndUpdate( + { symbol: item.symbol }, + { + symbol: item.symbol, + type: 'initial', + name: item.name, + order: item.order, + isFree: item.order <= 5, // 前5个免费 + status: 'active', + pronunciation: `${item.name}的音`, + exploreAreas: { + audio: true, + mouth: true, + speak: true, + write: true, + game: true, + words: true + } + }, + { upsert: true, new: true } + ); + } + console.log(`✅ 已初始化 ${initials.length} 个声母`); + + // 初始化韵母 + for (const item of finals) { + await PinyinContent.findOneAndUpdate( + { symbol: item.symbol }, + { + symbol: item.symbol, + type: 'final', + name: item.name, + order: item.order, + isFree: false, // 韵母需要解锁 + status: 'active', + pronunciation: `${item.name}的音`, + exploreAreas: { + audio: true, + mouth: true, + speak: true, + write: true, + game: true, + words: true + } + }, + { upsert: true, new: true } + ); + } + console.log(`✅ 已初始化 ${finals.length} 个韵母`); + + // 初始化整体认读音节 + for (const item of overalls) { + await PinyinContent.findOneAndUpdate( + { symbol: item.symbol }, + { + symbol: item.symbol, + type: 'overall', + name: item.name, + order: item.order, + isFree: false, // 整体认读需要解锁 + status: 'active', + pronunciation: `${item.name}的音`, + exploreAreas: { + audio: true, + mouth: true, + speak: true, + write: true, + game: true, + words: true + } + }, + { upsert: true, new: true } + ); + } + console.log(`✅ 已初始化 ${overalls.length} 个整体认读音节`); +} + +async function initAchievements() { + console.log('开始初始化成就...'); + + for (let i = 0; i < achievements.length; i++) { + const item = achievements[i]; + await PinyinAchievement.findOneAndUpdate( + { code: item.code }, + { + code: item.code, + name: item.name, + description: item.description, + type: item.type, + condition: item.condition, + order: i + 1, + isActive: true, + reward: { + type: 'stone', + value: 1 + } + }, + { upsert: true, new: true } + ); + } + console.log(`✅ 已初始化 ${achievements.length} 个成就`); +} + +async function main() { + try { + // 连接数据库 + await connectDB(); + console.log('数据库连接成功'); + + // 初始化拼音内容 + await initPinyinContents(); + + // 初始化成就 + await initAchievements(); + + console.log('\n✨ 数据初始化完成!'); + process.exit(0); + } catch (error) { + console.error('初始化失败:', error); + process.exit(1); + } +} + +main(); diff --git a/backend/wdkj-server/src/config/database.js b/backend/wdkj-server/src/config/database.js new file mode 100755 index 0000000..b4c6eb5 --- /dev/null +++ b/backend/wdkj-server/src/config/database.js @@ -0,0 +1,42 @@ +const mongoose = require('mongoose') + +/** + * 数据库连接配置 + */ +const connectDB = async () => { + try { + const mongoURI = process.env.MONGODB_URI || + `mongodb://${process.env.MONGODB_USER}:${process.env.MONGODB_PASSWORD}@${process.env.MONGODB_HOST}:${process.env.MONGODB_PORT}/${process.env.MONGODB_DB}?authSource=${process.env.MONGODB_DB}` + + const conn = await mongoose.connect(mongoURI, { + // Mongoose 6+ 不再需要这些选项,但保留以兼容旧版本 + // useNewUrlParser: true, + // useUnifiedTopology: true, + }) + + console.log(`✅ MongoDB 连接成功: ${conn.connection.host}:${conn.connection.port}/${conn.connection.name}`) + + // 连接事件监听 + mongoose.connection.on('error', (err) => { + console.error('❌ MongoDB 连接错误:', err) + }) + + mongoose.connection.on('disconnected', () => { + console.warn('⚠️ MongoDB 连接断开') + }) + + // 优雅关闭 + process.on('SIGINT', async () => { + await mongoose.connection.close() + console.log('🔌 MongoDB 连接已关闭') + process.exit(0) + }) + + return conn + } catch (error) { + console.error('❌ MongoDB 连接失败:', error.message) + process.exit(1) + } +} + +module.exports = connectDB diff --git a/backend/wdkj-server/src/controllers/pinyin/achievementController.js b/backend/wdkj-server/src/controllers/pinyin/achievementController.js new file mode 100755 index 0000000..d101329 --- /dev/null +++ b/backend/wdkj-server/src/controllers/pinyin/achievementController.js @@ -0,0 +1,288 @@ +const { PinyinAchievement, PinyinProgress } = require('../../models/pinyin'); + +/** + * 获取成就列表 + * GET /api/pinyin/achievements + */ +exports.getAchievements = async (req, res) => { + try { + const { type } = req.query; + + const query = { isActive: true }; + if (type) query.type = type; + + const achievements = await PinyinAchievement.find(query) + .sort({ order: 1, createdAt: 1 }) + .select('-__v'); + + res.json({ + code: 0, + message: 'success', + data: { + list: achievements + } + }); + } catch (error) { + console.error('获取成就列表失败:', error); + res.status(500).json({ + code: 500, + message: '获取成就列表失败', + error: error.message + }); + } +}; + +/** + * 获取用户已获得的成就 + * GET /api/pinyin/achievements/my + */ +exports.getMyAchievements = async (req, res) => { + try { + const userId = req.user._id; + + const progress = await PinyinProgress.findOne({ userId }); + + if (!progress || !progress.achievements || progress.achievements.length === 0) { + return res.json({ + code: 0, + message: 'success', + data: { + total: 0, + list: [] + } + }); + } + + // 获取成就详情 + const achievementCodes = progress.achievements.map(a => a.code); + const achievements = await PinyinAchievement.find({ + code: { $in: achievementCodes } + }).select('-__v'); + + // 合并获得时间 + const achievementsWithTime = achievements.map(ach => { + const userAch = progress.achievements.find(a => a.code === ach.code); + return { + ...ach.toObject(), + obtainedAt: userAch ? userAch.obtainedAt : null + }; + }); + + res.json({ + code: 0, + message: 'success', + data: { + total: achievementsWithTime.length, + list: achievementsWithTime + } + }); + } catch (error) { + console.error('获取用户成就失败:', error); + res.status(500).json({ + code: 500, + message: '获取用户成就失败', + error: error.message + }); + } +}; + +/** + * 检查成就达成 + * POST /api/pinyin/achievements/check + */ +exports.checkAchievements = async (req, res) => { + try { + const userId = req.user._id; + + const progress = await PinyinProgress.findOne({ userId }); + if (!progress) { + return res.json({ + code: 0, + message: 'success', + data: { + newAchievements: [] + } + }); + } + + // 获取所有启用的成就 + const achievements = await PinyinAchievement.find({ isActive: true }); + + const newAchievements = []; + + for (const achievement of achievements) { + // 检查是否已获得 + if (progress.achievements.some(a => a.code === achievement.code)) { + continue; + } + + // 检查条件 + const isAchieved = checkCondition(progress, achievement.condition); + + if (isAchieved) { + progress.achievements.push({ + code: achievement.code, + obtainedAt: new Date() + }); + + newAchievements.push({ + code: achievement.code, + name: achievement.name, + description: achievement.description, + icon: achievement.icon, + reward: achievement.reward + }); + } + } + + if (newAchievements.length > 0) { + await progress.save(); + } + + res.json({ + code: 0, + message: 'success', + data: { + newAchievements + } + }); + } catch (error) { + console.error('检查成就失败:', error); + res.status(500).json({ + code: 500, + message: '检查成就失败', + error: error.message + }); + } +}; + +/** + * 创建成就(管理员) + * POST /api/pinyin/achievements + */ +exports.createAchievement = async (req, res) => { + try { + const achievementData = req.body; + + // 检查code是否已存在 + const existing = await PinyinAchievement.findOne({ code: achievementData.code }); + if (existing) { + return res.status(400).json({ + code: 400, + message: '该成就代码已存在' + }); + } + + const achievement = new PinyinAchievement(achievementData); + await achievement.save(); + + res.status(201).json({ + code: 0, + message: '创建成功', + data: achievement + }); + } catch (error) { + console.error('创建成就失败:', error); + res.status(500).json({ + code: 500, + message: '创建成就失败', + error: error.message + }); + } +}; + +/** + * 更新成就(管理员) + * PUT /api/pinyin/achievements/:id + */ +exports.updateAchievement = async (req, res) => { + try { + const { id } = req.params; + const updateData = req.body; + + // 不允许修改code + delete updateData.code; + + const achievement = await PinyinAchievement.findByIdAndUpdate( + id, + { $set: updateData }, + { new: true, runValidators: true } + ); + + if (!achievement) { + return res.status(404).json({ + code: 404, + message: '成就不存在' + }); + } + + res.json({ + code: 0, + message: '更新成功', + data: achievement + }); + } catch (error) { + console.error('更新成就失败:', error); + res.status(500).json({ + code: 500, + message: '更新成就失败', + error: error.message + }); + } +}; + +/** + * 删除成就(管理员) + * DELETE /api/pinyin/achievements/:id + */ +exports.deleteAchievement = async (req, res) => { + try { + const { id } = req.params; + + const achievement = await PinyinAchievement.findByIdAndDelete(id); + + if (!achievement) { + return res.status(404).json({ + code: 404, + message: '成就不存在' + }); + } + + res.json({ + code: 0, + message: '删除成功' + }); + } catch (error) { + console.error('删除成就失败:', error); + res.status(500).json({ + code: 500, + message: '删除成就失败', + error: error.message + }); + } +}; + +/** + * 检查成就条件 + * @param {Object} progress - 用户进度 + * @param {Object} condition - 条件 + * @returns {Boolean} + */ +function checkCondition(progress, condition) { + switch (condition.type) { + case 'explore_count': + return progress.totalExplored >= condition.value; + + case 'collect_count': + return progress.totalStones >= condition.value; + + case 'streak_days': + return progress.streakDays >= condition.value; + + case 'complete_symbol': + return progress.isSymbolCompleted(condition.symbol); + + default: + return false; + } +} diff --git a/backend/wdkj-server/src/controllers/pinyin/contentController.js b/backend/wdkj-server/src/controllers/pinyin/contentController.js new file mode 100755 index 0000000..1ed4437 --- /dev/null +++ b/backend/wdkj-server/src/controllers/pinyin/contentController.js @@ -0,0 +1,363 @@ +const { PinyinContent } = require('../../models/pinyin'); + +/** + * 获取拼音内容列表 + * GET /api/pinyin/contents + */ +exports.getContents = async (req, res) => { + try { + const { type, isFree, page = 1, limit = 20 } = req.query; + + // 构建查询条件 + const query = { status: 'active' }; + if (type) query.type = type; + if (isFree !== undefined) query.isFree = isFree === 'true'; + + // 分页 + const skip = (parseInt(page) - 1) * parseInt(limit); + + // 查询数据 + const contents = await PinyinContent.find(query) + .select('-__v') + .sort({ order: 1, symbol: 1 }) + .skip(skip) + .limit(parseInt(limit)); + + // 获取总数 + const total = await PinyinContent.countDocuments(query); + + res.json({ + code: 0, + message: 'success', + data: { + list: contents, + total, + page: parseInt(page), + limit: parseInt(limit), + totalPages: Math.ceil(total / parseInt(limit)) + } + }); + } catch (error) { + console.error('获取拼音内容列表失败:', error); + res.status(500).json({ + code: 500, + message: '获取拼音内容列表失败', + error: error.message + }); + } +}; + +/** + * 获取单个拼音详情 + * GET /api/pinyin/contents/:symbol + */ +exports.getContentBySymbol = async (req, res) => { + try { + const { symbol } = req.params; + + const content = await PinyinContent.findOne({ + symbol: symbol.toLowerCase(), + status: 'active' + }).select('-__v'); + + if (!content) { + return res.status(404).json({ + code: 404, + message: '拼音内容不存在' + }); + } + + res.json({ + code: 0, + message: 'success', + data: content + }); + } catch (error) { + console.error('获取拼音详情失败:', error); + res.status(500).json({ + code: 500, + message: '获取拼音详情失败', + error: error.message + }); + } +}; + +/** + * 获取拼音音频 + * GET /api/pinyin/contents/:symbol/audio + */ +exports.getContentAudio = async (req, res) => { + try { + const { symbol } = req.params; + + const content = await PinyinContent.findOne({ + symbol: symbol.toLowerCase(), + status: 'active' + }).select('symbol audioUrl name'); + + if (!content) { + return res.status(404).json({ + code: 404, + message: '拼音内容不存在' + }); + } + + res.json({ + code: 0, + message: 'success', + data: { + symbol: content.symbol, + audioUrl: content.audioUrl, + name: content.name + } + }); + } catch (error) { + console.error('获取拼音音频失败:', error); + res.status(500).json({ + code: 500, + message: '获取拼音音频失败', + error: error.message + }); + } +}; + +/** + * 创建拼音内容(管理员) + * POST /api/pinyin/contents + */ +exports.createContent = async (req, res) => { + try { + const contentData = req.body; + + // 检查是否已存在 + const existing = await PinyinContent.findOne({ symbol: contentData.symbol }); + if (existing) { + return res.status(400).json({ + code: 400, + message: '该拼音内容已存在' + }); + } + + const content = new PinyinContent(contentData); + await content.save(); + + res.status(201).json({ + code: 0, + message: '创建成功', + data: content + }); + } catch (error) { + console.error('创建拼音内容失败:', error); + res.status(500).json({ + code: 500, + message: '创建拼音内容失败', + error: error.message + }); + } +}; + +/** + * 更新拼音内容(管理员) + * PUT /api/pinyin/contents/:id + */ +exports.updateContent = async (req, res) => { + try { + const { id } = req.params; + const updateData = req.body; + + // 不允许修改symbol + delete updateData.symbol; + + const content = await PinyinContent.findByIdAndUpdate( + id, + { $set: updateData }, + { new: true, runValidators: true } + ); + + if (!content) { + return res.status(404).json({ + code: 404, + message: '拼音内容不存在' + }); + } + + res.json({ + code: 0, + message: '更新成功', + data: content + }); + } catch (error) { + console.error('更新拼音内容失败:', error); + res.status(500).json({ + code: 500, + message: '更新拼音内容失败', + error: error.message + }); + } +}; + +/** + * 删除拼音内容(管理员) + * DELETE /api/pinyin/contents/:id + */ +exports.deleteContent = async (req, res) => { + try { + const { id } = req.params; + + const content = await PinyinContent.findByIdAndDelete(id); + + if (!content) { + return res.status(404).json({ + code: 404, + message: '拼音内容不存在' + }); + } + + res.json({ + code: 0, + message: '删除成功' + }); + } catch (error) { + console.error('删除拼音内容失败:', error); + res.status(500).json({ + code: 500, + message: '删除拼音内容失败', + error: error.message + }); + } +}; + +/** + * 上传拼音音频 + * POST /api/pinyin/contents/:symbol/audio + */ +exports.uploadAudio = async (req, res) => { + try { + const { symbol } = req.params; + + if (!req.file) { + return res.status(400).json({ + code: 400, + message: '请上传音频文件' + }); + } + + // 构建文件URL(假设使用静态文件服务) + const audioUrl = `/uploads/audio/${req.file.filename}`; + + // 更新拼音内容的音频URL + const content = await PinyinContent.findOneAndUpdate( + { symbol: symbol.toLowerCase() }, + { $set: { audioUrl } }, + { new: true } + ); + + if (!content) { + return res.status(404).json({ + code: 404, + message: '拼音内容不存在' + }); + } + + res.json({ + code: 0, + message: '音频上传成功', + data: { audioUrl } + }); + } catch (error) { + console.error('上传音频失败:', error); + res.status(500).json({ + code: 500, + message: '上传音频失败', + error: error.message + }); + } +}; + +/** + * 上传口型图 + * POST /api/pinyin/contents/:symbol/mouth-image + */ +exports.uploadMouthImage = async (req, res) => { + try { + const { symbol } = req.params; + + if (!req.file) { + return res.status(400).json({ + code: 400, + message: '请上传图片文件' + }); + } + + // 构建文件URL + const mouthImage = `/uploads/images/${req.file.filename}`; + + // 更新拼音内容的口型图URL + const content = await PinyinContent.findOneAndUpdate( + { symbol: symbol.toLowerCase() }, + { $set: { mouthImage } }, + { new: true } + ); + + if (!content) { + return res.status(404).json({ + code: 404, + message: '拼音内容不存在' + }); + } + + res.json({ + code: 0, + message: '口型图上传成功', + data: { mouthImage } + }); + } catch (error) { + console.error('上传口型图失败:', error); + res.status(500).json({ + code: 500, + message: '上传口型图失败', + error: error.message + }); + } +}; + +/** + * 批量更新音频URL(从外部TTS服务) + * POST /api/pinyin/contents/batch-update-audio + */ +exports.batchUpdateAudio = async (req, res) => { + try { + const { audioBaseUrl } = req.body; + + if (!audioBaseUrl) { + return res.status(400).json({ + code: 400, + message: '请提供音频基础URL' + }); + } + + // 获取所有拼音内容 + const contents = await PinyinContent.find({}); + + // 批量更新音频URL + const updatePromises = contents.map(content => { + const audioUrl = `${audioBaseUrl}/${content.symbol}.mp3`; + return PinyinContent.findByIdAndUpdate(content._id, { $set: { audioUrl } }); + }); + + await Promise.all(updatePromises); + + res.json({ + code: 0, + message: `成功更新 ${contents.length} 个拼音的音频URL`, + data: { updatedCount: contents.length } + }); + } catch (error) { + console.error('批量更新音频失败:', error); + res.status(500).json({ + code: 500, + message: '批量更新音频失败', + error: error.message + }); + } +}; diff --git a/backend/wdkj-server/src/controllers/pinyin/gameController.js b/backend/wdkj-server/src/controllers/pinyin/gameController.js new file mode 100755 index 0000000..add144e --- /dev/null +++ b/backend/wdkj-server/src/controllers/pinyin/gameController.js @@ -0,0 +1,320 @@ +const { PinyinGameRecord, PinyinProgress, PinyinContent } = require('../../models/pinyin'); + +/** + * 游戏配置 + */ +const GAME_CONFIG = { + match: { + name: '拼音配对', + description: '找出相同的拼音卡片', + difficulty: { + easy: { pairs: 4, time: 60 }, + normal: { pairs: 6, time: 90 }, + hard: { pairs: 8, time: 120 } + } + }, + tone: { + name: '声调过山车', + description: '根据声调高低控制轨道', + difficulty: { + easy: { questions: 5, time: 60 }, + normal: { questions: 8, time: 90 }, + hard: { questions: 10, time: 120 } + } + }, + find: { + name: '找拼音', + description: '在场景中找到指定拼音', + difficulty: { + easy: { targets: 3, distractors: 6, time: 60 }, + normal: { targets: 5, distractors: 10, time: 90 }, + hard: { targets: 7, distractors: 14, time: 120 } + } + }, + puzzle: { + name: '拼音拼图', + description: '拖拽拼音组成完整音节', + difficulty: { + easy: { pieces: 2, time: 60 }, + normal: { pieces: 3, time: 90 }, + hard: { pieces: 4, time: 120 } + } + }, + mimic: { + name: '语音模仿', + description: '模仿发音,获得反馈', + difficulty: { + easy: { targets: 3, time: 60 }, + normal: { targets: 5, time: 90 }, + hard: { targets: 7, time: 120 } + } + }, + runner: { + name: '拼音跑酷', + description: '躲避障碍,收集正确拼音', + difficulty: { + easy: { distance: 100, obstacles: 5, time: 60 }, + normal: { distance: 200, obstacles: 10, time: 90 }, + hard: { distance: 300, obstacles: 15, time: 120 } + } + } +}; + +/** + * 获取游戏配置 + * GET /api/pinyin/games/config + */ +exports.getGameConfig = async (req, res) => { + try { + res.json({ + code: 0, + message: 'success', + data: { + games: GAME_CONFIG + } + }); + } catch (error) { + console.error('获取游戏配置失败:', error); + res.status(500).json({ + code: 500, + message: '获取游戏配置失败', + error: error.message + }); + } +}; + +/** + * 获取指定游戏配置 + * GET /api/pinyin/games/config/:type + */ +exports.getGameConfigByType = async (req, res) => { + try { + const { type } = req.params; + + if (!GAME_CONFIG[type]) { + return res.status(404).json({ + code: 404, + message: '游戏类型不存在' + }); + } + + res.json({ + code: 0, + message: 'success', + data: { + type, + config: GAME_CONFIG[type] + } + }); + } catch (error) { + console.error('获取游戏配置失败:', error); + res.status(500).json({ + code: 500, + message: '获取游戏配置失败', + error: error.message + }); + } +}; + +/** + * 记录游戏结果 + * POST /api/pinyin/games/record + */ +exports.recordGame = async (req, res) => { + try { + const userId = req.user._id; + const { + gameType, + difficulty = 'normal', + score, + duration, + correctCount, + wrongCount, + details = {} + } = req.body; + + // 验证参数 + if (!gameType || !GAME_CONFIG[gameType]) { + return res.status(400).json({ + code: 400, + message: '无效的游戏类型' + }); + } + + if (score === undefined || duration === undefined) { + return res.status(400).json({ + code: 400, + message: '缺少必要参数:score 或 duration' + }); + } + + // 计算准确率 + const total = (correctCount || 0) + (wrongCount || 0); + const accuracy = total > 0 ? Math.round((correctCount / total) * 100) : 0; + + // 创建游戏记录 + const record = new PinyinGameRecord({ + userId, + gameType, + difficulty, + score, + duration, + correctCount: correctCount || 0, + wrongCount: wrongCount || 0, + accuracy, + details, + playedAt: new Date() + }); + + await record.save(); + + // 更新用户进度中的游戏统计 + let progress = await PinyinProgress.findOne({ userId }); + if (!progress) { + progress = new PinyinProgress({ userId }); + } + progress.updateDailyStats('game', duration); + await progress.save(); + + res.json({ + code: 0, + message: 'success', + data: { + record: { + id: record._id, + gameType, + score, + accuracy, + playedAt: record.playedAt + } + } + }); + } catch (error) { + console.error('记录游戏结果失败:', error); + res.status(500).json({ + code: 500, + message: '记录游戏结果失败', + error: error.message + }); + } +}; + +/** + * 获取用户游戏记录 + * GET /api/pinyin/games/records + */ +exports.getGameRecords = async (req, res) => { + try { + const userId = req.user._id; + const { gameType, page = 1, limit = 20 } = req.query; + + const query = { userId }; + if (gameType) query.gameType = gameType; + + const skip = (parseInt(page) - 1) * parseInt(limit); + + const records = await PinyinGameRecord.find(query) + .sort({ playedAt: -1 }) + .skip(skip) + .limit(parseInt(limit)) + .select('-__v'); + + const total = await PinyinGameRecord.countDocuments(query); + + res.json({ + code: 0, + message: 'success', + data: { + list: records, + total, + page: parseInt(page), + limit: parseInt(limit) + } + }); + } catch (error) { + console.error('获取游戏记录失败:', error); + res.status(500).json({ + code: 500, + message: '获取游戏记录失败', + error: error.message + }); + } +}; + +/** + * 获取用户游戏统计 + * GET /api/pinyin/games/stats + */ +exports.getGameStats = async (req, res) => { + try { + const userId = req.user._id; + + const stats = await PinyinGameRecord.getUserStats(userId); + + // 格式化统计数据 + const formattedStats = {}; + Object.keys(GAME_CONFIG).forEach(type => { + const stat = stats.find(s => s._id === type); + formattedStats[type] = { + name: GAME_CONFIG[type].name, + totalGames: stat ? stat.totalGames : 0, + totalScore: stat ? stat.totalScore : 0, + avgScore: stat ? Math.round(stat.avgScore) : 0, + maxScore: stat ? stat.maxScore : 0, + avgAccuracy: stat ? Math.round(stat.avgAccuracy) : 0 + }; + }); + + res.json({ + code: 0, + message: 'success', + data: { + stats: formattedStats + } + }); + } catch (error) { + console.error('获取游戏统计失败:', error); + res.status(500).json({ + code: 500, + message: '获取游戏统计失败', + error: error.message + }); + } +}; + +/** + * 获取游戏排行榜 + * GET /api/pinyin/games/leaderboard/:type + */ +exports.getLeaderboard = async (req, res) => { + try { + const { type } = req.params; + const { limit = 10 } = req.query; + + if (!GAME_CONFIG[type]) { + return res.status(404).json({ + code: 404, + message: '游戏类型不存在' + }); + } + + const leaderboard = await PinyinGameRecord.getLeaderboard(type, parseInt(limit)); + + res.json({ + code: 0, + message: 'success', + data: { + gameType: type, + gameName: GAME_CONFIG[type].name, + leaderboard + } + }); + } catch (error) { + console.error('获取游戏排行榜失败:', error); + res.status(500).json({ + code: 500, + message: '获取游戏排行榜失败', + error: error.message + }); + } +}; diff --git a/backend/wdkj-server/src/controllers/pinyin/progressController.js b/backend/wdkj-server/src/controllers/pinyin/progressController.js new file mode 100755 index 0000000..a6c13da --- /dev/null +++ b/backend/wdkj-server/src/controllers/pinyin/progressController.js @@ -0,0 +1,351 @@ +const { PinyinProgress, PinyinContent, PinyinAchievement } = require('../../models/pinyin'); + +/** + * 获取用户探索进度 + * GET /api/pinyin/progress + */ +exports.getProgress = async (req, res) => { + try { + const userId = req.user._id; + + let progress = await PinyinProgress.findOne({ userId }); + + // 如果没有进度记录,创建新记录 + if (!progress) { + progress = new PinyinProgress({ userId }); + await progress.save(); + } + + // 获取所有拼音内容数量 + const totalSymbols = await PinyinContent.countDocuments({ status: 'active' }); + + res.json({ + code: 0, + message: 'success', + data: { + progress: { + totalExplored: progress.totalExplored, + totalStones: progress.totalStones, + totalSymbols: totalSymbols, + exploredSymbols: progress.exploredSymbols, + currentSymbol: progress.currentSymbol, + achievements: progress.achievements, + streakDays: progress.streakDays + } + } + }); + } catch (error) { + console.error('获取探索进度失败:', error); + res.status(500).json({ + code: 500, + message: '获取探索进度失败', + error: error.message + }); + } +}; + +/** + * 记录探索行为 + * POST /api/pinyin/progress/explore + */ +exports.recordExplore = async (req, res) => { + try { + const userId = req.user._id; + const { symbol, area, duration = 0 } = req.body; + + if (!symbol || !area) { + return res.status(400).json({ + code: 400, + message: '缺少必要参数:symbol 或 area' + }); + } + + // 验证拼音是否存在 + const content = await PinyinContent.findOne({ + symbol: symbol.toLowerCase(), + status: 'active' + }); + + if (!content) { + return res.status(404).json({ + code: 404, + message: '拼音内容不存在' + }); + } + + // 获取或创建进度记录 + let progress = await PinyinProgress.findOne({ userId }); + if (!progress) { + progress = new PinyinProgress({ userId }); + } + + // 检查是否首次探索该拼音 + const isFirstExplore = !progress.hasExplored(symbol); + + // 添加探索记录 + progress.addExplore(symbol, area); + + // 更新每日统计 + progress.updateDailyStats('explore', duration); + + // 检查是否完成所有区域 + const isCompleted = progress.isSymbolCompleted(symbol); + + // 检查成就 + const newAchievements = await checkAchievements(progress); + + await progress.save(); + + res.json({ + code: 0, + message: 'success', + data: { + isFirstExplore, + isCompleted, + newAchievements, + progress: { + totalExplored: progress.totalExplored, + totalStones: progress.totalStones, + currentSymbol: progress.currentSymbol + } + } + }); + } catch (error) { + console.error('记录探索行为失败:', error); + res.status(500).json({ + code: 500, + message: '记录探索行为失败', + error: error.message + }); + } +}; + +/** + * 收集能量石 + * POST /api/pinyin/progress/collect + */ +exports.collectStone = async (req, res) => { + try { + const userId = req.user._id; + const { symbol } = req.body; + + if (!symbol) { + return res.status(400).json({ + code: 400, + message: '缺少必要参数:symbol' + }); + } + + let progress = await PinyinProgress.findOne({ userId }); + if (!progress) { + return res.status(404).json({ + code: 404, + message: '探索进度不存在' + }); + } + + // 检查是否已完成该拼音的所有区域 + if (!progress.isSymbolCompleted(symbol)) { + return res.status(400).json({ + code: 400, + message: '请先完成该拼音的所有探索区域' + }); + } + + // 收集能量石 + const isCollected = progress.collectStone(symbol); + + if (!isCollected) { + return res.status(400).json({ + code: 400, + message: '该能量石已收集' + }); + } + + // 检查成就 + const newAchievements = await checkAchievements(progress); + + await progress.save(); + + res.json({ + code: 0, + message: 'success', + data: { + isCollected: true, + totalStones: progress.totalStones, + newAchievements + } + }); + } catch (error) { + console.error('收集能量石失败:', error); + res.status(500).json({ + code: 500, + message: '收集能量石失败', + error: error.message + }); + } +}; + +/** + * 获取用户探索统计 + * GET /api/pinyin/progress/stats + */ +exports.getStats = async (req, res) => { + try { + const userId = req.user._id; + + const progress = await PinyinProgress.findOne({ userId }); + + if (!progress) { + return res.json({ + code: 0, + message: 'success', + data: { + totalExplored: 0, + totalStones: 0, + streakDays: 0, + achievements: [], + dailyStats: [] + } + }); + } + + // 获取最近7天的统计 + const last7Days = progress.dailyStats + .sort((a, b) => b.date - a.date) + .slice(0, 7); + + res.json({ + code: 0, + message: 'success', + data: { + totalExplored: progress.totalExplored, + totalStones: progress.totalStones, + streakDays: progress.streakDays, + achievements: progress.achievements, + dailyStats: last7Days + } + }); + } catch (error) { + console.error('获取探索统计失败:', error); + res.status(500).json({ + code: 500, + message: '获取探索统计失败', + error: error.message + }); + } +}; + +/** + * 获取探索排行榜 + * GET /api/pinyin/progress/leaderboard + */ +exports.getLeaderboard = async (req, res) => { + try { + const { type = 'stones', limit = 10 } = req.query; + + let sortField = 'totalStones'; + if (type === 'explored') sortField = 'totalExplored'; + if (type === 'achievements') sortField = 'achievements'; + + const leaderboard = await PinyinProgress.find() + .sort({ [sortField]: -1 }) + .limit(parseInt(limit)) + .populate('userId', 'nickname avatar') + .select('totalStones totalExplored achievements streakDays'); + + const formattedLeaderboard = leaderboard.map((item, index) => ({ + rank: index + 1, + userId: item.userId?._id, + nickname: item.userId?.nickname || '匿名用户', + avatar: item.userId?.avatar || '', + totalStones: item.totalStones, + totalExplored: item.totalExplored, + achievements: item.achievements.length, + streakDays: item.streakDays + })); + + res.json({ + code: 0, + message: 'success', + data: { + type, + leaderboard: formattedLeaderboard + } + }); + } catch (error) { + console.error('获取排行榜失败:', error); + res.status(500).json({ + code: 500, + message: '获取排行榜失败', + error: error.message + }); + } +}; + +/** + * 检查成就达成 + * @param {Object} progress - 用户进度对象 + * @returns {Array} - 新获得的成就列表 + */ +async function checkAchievements(progress) { + const newAchievements = []; + + // 获取所有启用的成就 + const achievements = await PinyinAchievement.find({ isActive: true }); + + for (const achievement of achievements) { + // 检查是否已获得 + if (progress.achievements.some(a => a.code === achievement.code)) { + continue; + } + + // 检查条件 + const isAchieved = checkAchievementCondition(progress, achievement.condition); + + if (isAchieved) { + progress.addAchievement(achievement.code); + newAchievements.push({ + code: achievement.code, + name: achievement.name, + icon: achievement.icon + }); + } + } + + return newAchievements; +} + +/** + * 检查单个成就条件 + * @param {Object} progress - 用户进度 + * @param {Object} condition - 成就条件 + * @returns {Boolean} + */ +function checkAchievementCondition(progress, condition) { + switch (condition.type) { + case 'explore_count': + return progress.totalExplored >= condition.value; + + case 'collect_count': + return progress.totalStones >= condition.value; + + case 'streak_days': + return progress.streakDays >= condition.value; + + case 'complete_symbol': + return progress.isSymbolCompleted(condition.symbol); + + case 'complete_type': + // 需要查询该类型的所有拼音是否都已完成 + // 这里简化处理,实际应该查询数据库 + return false; + + case 'explore_all': + // 需要查询所有拼音数量 + return false; + + default: + return false; + } +} diff --git a/backend/wdkj-server/src/index.js b/backend/wdkj-server/src/index.js new file mode 100755 index 0000000..72144f3 --- /dev/null +++ b/backend/wdkj-server/src/index.js @@ -0,0 +1,97 @@ +require('dotenv').config() + +const express = require('express') +const cors = require('cors') +const helmet = require('helmet') +const compression = require('compression') +const morgan = require('morgan') +const rateLimit = require('express-rate-limit') + +const connectDB = require('./config/database') +const initRoutes = require('./routes') +const logger = require('./utils/logger') + +// 创建 Express 应用 +const app = express() + +// 数据库连接 +connectDB() + +// 中间件配置 +app.use(helmet()) // 安全头部 +app.use(cors()) // 跨域支持 +app.use(compression()) // Gzip 压缩 +app.use(express.json({ limit: '10mb' })) // JSON 解析 +app.use(express.urlencoded({ extended: true, limit: '10mb' })) // URL 编码解析 + +// 日志中间件 +if (process.env.NODE_ENV === 'development') { + app.use(morgan('dev')) +} else { + app.use(morgan('combined')) +} + +// 速率限制 +const limiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 分钟 + max: 1000, // 每个 IP 最多 1000 次请求 + message: { + success: false, + error: '请求过于频繁,请稍后再试' + } +}) +app.use('/api/', limiter) + +// 健康检查 +app.get('/health', (req, res) => { + res.status(200).json({ + status: 'ok', + timestamp: new Date().toISOString(), + uptime: process.uptime() + }) +}) + +// API 路由 +initRoutes(app) + +// 404 处理 +app.use((req, res) => { + res.status(404).json({ + success: false, + error: '接口不存在', + path: req.path + }) +}) + +// 全局错误处理 +app.use((err, req, res, next) => { + logger.error('服务器错误:', err) + + res.status(err.status || 500).json({ + success: false, + error: process.env.NODE_ENV === 'production' + ? '服务器内部错误' + : err.message, + stack: process.env.NODE_ENV === 'development' ? err.stack : undefined + }) +}) + +// 启动服务器 +const PORT = process.env.PORT || 3000 +const server = app.listen(PORT, () => { + logger.info(`🚀 服务器启动成功`) + logger.info(`📍 端口: ${PORT}`) + logger.info(`🌐 环境: ${process.env.NODE_ENV || 'development'}`) + logger.info(`📊 健康检查: http://localhost:${PORT}/health`) +}) + +// 优雅关闭 +process.on('SIGTERM', () => { + logger.info('📴 接收到 SIGTERM 信号,正在关闭服务器...') + server.close(() => { + logger.info('✅ 服务器已关闭') + process.exit(0) + }) +}) + +module.exports = app diff --git a/backend/wdkj-server/src/middleware/auth.js b/backend/wdkj-server/src/middleware/auth.js new file mode 100755 index 0000000..86b465f --- /dev/null +++ b/backend/wdkj-server/src/middleware/auth.js @@ -0,0 +1,167 @@ +const jwt = require('jsonwebtoken') +const { Admin, User } = require('../models') + +/** + * JWT 认证中间件(管理员) + */ +const authMiddleware = async (req, res, next) => { + try { + // 从 header 获取 token + const authHeader = req.header('Authorization') + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return res.status(401).json({ + success: false, + error: '未提供认证令牌' + }) + } + + const token = authHeader.replace('Bearer ', '') + + // 验证 token + const decoded = jwt.verify(token, process.env.JWT_SECRET) + + // 查找管理员 + const admin = await Admin.findById(decoded.adminId) + if (!admin) { + return res.status(401).json({ + success: false, + error: '管理员不存在' + }) + } + + // 检查状态 + if (admin.status !== 'active') { + return res.status(403).json({ + success: false, + error: '管理员账户已禁用' + }) + } + + // 将管理员信息附加到请求对象 + req.admin = admin + next() + } catch (error) { + if (error.name === 'JsonWebTokenError') { + return res.status(401).json({ + success: false, + error: '无效的认证令牌' + }) + } + if (error.name === 'TokenExpiredError') { + return res.status(401).json({ + success: false, + error: '认证令牌已过期' + }) + } + next(error) + } +} + +/** + * 权限检查中间件 + * @param {string} permission - 需要的权限 + */ +const requirePermission = (permission) => { + return (req, res, next) => { + if (!req.admin) { + return res.status(401).json({ + success: false, + error: '未认证' + }) + } + + if (!req.admin.hasPermission(permission)) { + return res.status(403).json({ + success: false, + error: '权限不足' + }) + } + + next() + } +} + +/** + * 小程序用户 JWT 认证中间件 + */ +const userAuthMiddleware = async (req, res, next) => { + try { + const authHeader = req.header('Authorization') + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return res.status(401).json({ + success: false, + error: '未提供认证令牌' + }) + } + + const token = authHeader.replace('Bearer ', '') + + // 添加调试日志 + console.log('[Auth] Token received:', token.substring(0, 20) + '...') + + const decoded = jwt.verify(token, process.env.JWT_SECRET) + console.log('[Auth] Token decoded:', decoded) + + // 兼容两种token格式:包含userId或openid + let user = null + if (decoded.userId) { + console.log('[Auth] Looking up user by userId:', decoded.userId) + user = await User.findById(decoded.userId) + } else if (decoded.openid) { + console.log('[Auth] Looking up user by openid:', decoded.openid) + user = await User.findOne({ openid: decoded.openid }) + } + + if (!user) { + console.log('[Auth] User not found') + return res.status(401).json({ + success: false, + error: '用户不存在或令牌无效' + }) + } + + console.log('[Auth] User found:', user._id) + console.log('[Auth] User openid:', user.openid) + req.user = user + // 优先使用 openid,如果没有则使用 userId 作为标识 + req.openid = user.openid || user._id.toString() + next() + } catch (error) { + if (error.name === 'JsonWebTokenError' || error.name === 'TokenExpiredError') { + return res.status(401).json({ + success: false, + error: '认证令牌无效或已过期' + }) + } + next(error) + } +} + +/** + * 可选的用户认证中间件(不强制要求登录) + */ +const optionalAuthMiddleware = async (req, res, next) => { + try { + const authHeader = req.header('Authorization') + if (authHeader && authHeader.startsWith('Bearer ')) { + const token = authHeader.replace('Bearer ', '') + const decoded = jwt.verify(token, process.env.JWT_SECRET) + const user = await User.findById(decoded.userId) + if (user) { + req.user = user + req.openid = user.openid + } + } + next() + } catch (error) { + // 即使认证失败也继续,只是不附加用户信息 + next() + } +} + +module.exports = { + authMiddleware, + requirePermission, + userAuthMiddleware, + optionalAuthMiddleware +} diff --git a/backend/wdkj-server/src/models/AIChatQuota.js b/backend/wdkj-server/src/models/AIChatQuota.js new file mode 100755 index 0000000..9525b41 --- /dev/null +++ b/backend/wdkj-server/src/models/AIChatQuota.js @@ -0,0 +1,113 @@ +const mongoose = require('mongoose') + +/** + * 用户AI问答次数记录模型 + */ +const AIChatQuotaSchema = new mongoose.Schema({ + openid: { + type: String, + required: true, + unique: true, + index: true + }, + userId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User', + index: true + }, + // 剩余免费问答次数 + freeQuota: { + type: Number, + default: 5 + }, + // 通过分享获得的次数 + sharedQuota: { + type: Number, + default: 0 + }, + // 已使用的总次数 + usedQuota: { + type: Number, + default: 0 + }, + // 今日已使用次数 + dailyUsed: { + type: Number, + default: 0 + }, + // 最后使用日期 + lastUsedDate: { + type: Date, + default: null + }, + // 分享获得次数的记录 + shareRecords: [{ + sharedAt: { type: Date, default: Date.now }, + gainedQuota: { type: Number, default: 5 } + }], + createdAt: { + type: Date, + default: Date.now + }, + updatedAt: { + type: Date, + default: Date.now + } +}) + +// 更新时自动更新 updatedAt +AIChatQuotaSchema.pre('save', function(next) { + this.updatedAt = Date.now() + next() +}) + +// 获取总可用次数 +AIChatQuotaSchema.methods.getTotalQuota = function() { + return this.freeQuota + this.sharedQuota +} + +// 获取剩余可用次数 +AIChatQuotaSchema.methods.getRemainingQuota = function() { + return this.getTotalQuota() - this.usedQuota +} + +// 检查是否有可用次数 +AIChatQuotaSchema.methods.hasQuota = function() { + return this.getRemainingQuota() > 0 +} + +// 使用一次问答机会 +AIChatQuotaSchema.methods.useQuota = async function() { + if (!this.hasQuota()) { + return false + } + + this.usedQuota += 1 + this.dailyUsed += 1 + this.lastUsedDate = new Date() + await this.save() + return true +} + +// 通过分享增加次数 +AIChatQuotaSchema.methods.addQuotaByShare = async function(gainedQuota = 5) { + this.sharedQuota += gainedQuota + this.shareRecords.push({ + sharedAt: new Date(), + gainedQuota + }) + await this.save() + return this.getRemainingQuota() +} + +// 重置每日使用次数(可定时任务调用) +AIChatQuotaSchema.methods.resetDailyUsed = function() { + const today = new Date().toDateString() + const lastUsed = this.lastUsedDate ? new Date(this.lastUsedDate).toDateString() : null + + if (lastUsed !== today) { + this.dailyUsed = 0 + } +} + +module.exports = mongoose.model('AIChatQuota', AIChatQuotaSchema) diff --git a/backend/wdkj-server/src/models/AIModel.js b/backend/wdkj-server/src/models/AIModel.js new file mode 100755 index 0000000..10608d8 --- /dev/null +++ b/backend/wdkj-server/src/models/AIModel.js @@ -0,0 +1,117 @@ +const mongoose = require('mongoose') + +/** + * AI模型配置模型 + */ +const AIModelSchema = new mongoose.Schema({ + // 模型名称 + name: { + type: String, + required: true, + trim: true + }, + // 模型ID + modelId: { + type: String, + required: true, + unique: true, + trim: true + }, + // API地址 + apiUrl: { + type: String, + required: true, + trim: true + }, + // API Key + apiKey: { + type: String, + required: true + }, + // 模型描述 + description: { + type: String, + default: '' + }, + // 是否启用 + isActive: { + type: Boolean, + default: true + }, + // 是否为默认模型 + isDefault: { + type: Boolean, + default: false + }, + // 模型参数配置 + config: { + temperature: { + type: Number, + default: 0.7, + min: 0, + max: 2 + }, + maxTokens: { + type: Number, + default: 800, + min: 1, + max: 4096 + }, + topP: { + type: Number, + default: 1, + min: 0, + max: 1 + } + }, + // 优先级(数字越小优先级越高) + priority: { + type: Number, + default: 0 + }, + createdAt: { + type: Date, + default: Date.now + }, + updatedAt: { + type: Date, + default: Date.now + } +}) + +// 更新时自动更新 updatedAt +AIModelSchema.pre('save', function(next) { + this.updatedAt = Date.now() + next() +}) + +// 设置默认模型时,取消其他模型的默认状态 +AIModelSchema.pre('save', async function(next) { + if (this.isDefault && this.isModified('isDefault')) { + await this.constructor.updateMany( + { _id: { $ne: this._id } }, + { isDefault: false } + ) + } + next() +}) + +// 获取当前使用的模型(优先返回默认模型,如果没有则返回第一个启用的模型) +AIModelSchema.statics.getCurrentModel = async function() { + // 先查找默认模型 + let model = await this.findOne({ isDefault: true, isActive: true }) + + // 如果没有默认模型,返回第一个启用的模型 + if (!model) { + model = await this.findOne({ isActive: true }).sort({ priority: 1, createdAt: -1 }) + } + + return model +} + +// 获取所有启用的模型列表 +AIModelSchema.statics.getActiveModels = async function() { + return await this.find({ isActive: true }).sort({ priority: 1, createdAt: -1 }) +} + +module.exports = mongoose.model('AIModel', AIModelSchema) diff --git a/backend/wdkj-server/src/models/Admin.js b/backend/wdkj-server/src/models/Admin.js new file mode 100755 index 0000000..653a0af --- /dev/null +++ b/backend/wdkj-server/src/models/Admin.js @@ -0,0 +1,130 @@ +const mongoose = require('mongoose') +const { Schema } = mongoose +const bcrypt = require('bcryptjs') + +/** + * 管理员模型 + */ +const AdminSchema = new Schema({ + // 登录信息 + username: { + type: String, + required: true, + unique: true, + trim: true, + lowercase: true, + minlength: 3, + maxlength: 30 + }, + password: { + type: String, + required: true, + minlength: 6 + }, + email: { + type: String, + trim: true, + lowercase: true + }, + + // 基本信息 + realName: String, + phone: String, + avatar: String, + + // 角色 + role: { + type: String, + enum: ['super_admin', 'content_manager', 'shop_manager', 'viewer'], + default: 'viewer' + }, + + // 权限 + permissions: [{ + type: String + }], + + // 状态 + status: { + type: String, + enum: ['active', 'inactive', 'suspended'], + default: 'active' + }, + + // 登录信息 + lastLogin: Date, + lastLoginIp: String, + loginCount: { + type: Number, + default: 0 + }, + + // 时间戳 + createdAt: { + type: Date, + default: Date.now + }, + updatedAt: { + type: Date, + default: Date.now + } +}, { + timestamps: true, + toJSON: { virtuals: true }, + toObject: { virtuals: true } +}) + +// 索引(username 已通过 unique: true 自动创建索引) +AdminSchema.index({ role: 1 }) + +// 中间件:保存前加密密码 +AdminSchema.pre('save', async function(next) { + if (!this.isModified('password')) { + return next() + } + + try { + const salt = await bcrypt.genSalt(10) + this.password = await bcrypt.hash(this.password, salt) + next() + } catch (error) { + next(error) + } +}) + +// 实例方法:验证密码 +AdminSchema.methods.validatePassword = async function(password) { + return bcrypt.compare(password, this.password) +} + +// 实例方法:检查权限 +AdminSchema.methods.hasPermission = function(permission) { + if (this.permissions.includes('*')) return true + return this.permissions.includes(permission) +} + +// 静态方法:根据角色获取权限 +AdminSchema.statics.getPermissionsByRole = function(role) { + const permissionMap = { + super_admin: ['*'], + content_manager: [ + 'knowledge:read', 'knowledge:write', 'knowledge:delete', + 'gallery:read', 'gallery:approve', 'gallery:delete', + 'users:read' + ], + shop_manager: [ + 'shop:read', 'shop:write', 'shop:delete', + 'orders:read', 'orders:process', 'orders:refund', + 'users:read' + ], + viewer: [ + 'dashboard:read', + 'analytics:read', + 'users:read' + ] + } + + return permissionMap[role] || [] +} + +module.exports = mongoose.model('Admin', AdminSchema) diff --git a/backend/wdkj-server/src/models/AdminLog.js b/backend/wdkj-server/src/models/AdminLog.js new file mode 100755 index 0000000..79705a3 --- /dev/null +++ b/backend/wdkj-server/src/models/AdminLog.js @@ -0,0 +1,54 @@ +const mongoose = require('mongoose') +const { Schema } = mongoose + +/** + * 管理员操作日志模型 + */ +const AdminLogSchema = new Schema({ + // 管理员信息 + adminId: { + type: Schema.Types.ObjectId, + ref: 'Admin', + required: true, + index: true + }, + adminName: { + type: String, + required: true + }, + + // 操作信息 + action: { + type: String, + required: true, + enum: ['login', 'create', 'read', 'update', 'delete', 'review', 'export'] + }, + resource: { + type: String, + required: true + }, + resourceId: String, + + // 详细信息 + details: Schema.Types.Mixed, + + // IP 地址 + ip: String, + userAgent: String, + + // 时间戳 + timestamp: { + type: Date, + default: Date.now, + index: true + } +}, { + timestamps: false +}) + +// 索引 +AdminLogSchema.index({ adminId: 1, timestamp: -1 }) +AdminLogSchema.index({ resource: 1, timestamp: -1 }) +AdminLogSchema.index({ action: 1 }) + +module.exports = mongoose.model('AdminLog', AdminLogSchema) diff --git a/backend/wdkj-server/src/models/BGM.js b/backend/wdkj-server/src/models/BGM.js new file mode 100755 index 0000000..4db5c67 --- /dev/null +++ b/backend/wdkj-server/src/models/BGM.js @@ -0,0 +1,66 @@ +const mongoose = require('mongoose') + +const BGMschema = new mongoose.Schema({ + name: { + type: String, + required: true, + trim: true + }, + dimension: { + type: Number, + required: true, + enum: [1, 2, 3, 4, 5], + index: true + }, + url: { + type: String, + required: true, + trim: true + }, + duration: { + type: Number, + default: 0 + }, + loop: { + type: Boolean, + default: true + }, + volume: { + type: Number, + default: 0.5, + min: 0, + max: 1 + }, + isActive: { + type: Boolean, + default: true, + index: true + }, + sortOrder: { + type: Number, + default: 0 + }, + description: { + type: String, + trim: true + }, + createdAt: { + type: Date, + default: Date.now + }, + updatedAt: { + type: Date, + default: Date.now + } +}) + +// 更新时自动修改 updatedAt +BGMschema.pre('save', function(next) { + this.updatedAt = Date.now() + next() +}) + +// 复合索引:维度 + 激活状态 + 排序 +BGMschema.index({ dimension: 1, isActive: 1, sortOrder: 1 }) + +module.exports = mongoose.model('BGM', BGMschema) diff --git a/backend/wdkj-server/src/models/Feedback.js b/backend/wdkj-server/src/models/Feedback.js new file mode 100755 index 0000000..0ccfed0 --- /dev/null +++ b/backend/wdkj-server/src/models/Feedback.js @@ -0,0 +1,191 @@ +const mongoose = require('mongoose') +const { Schema } = mongoose + +/** + * 反馈模型 + * 存储用户的建议、问题和反馈 + */ +const FeedbackSchema = new Schema({ + // 反馈者信息 + userId: { + type: Schema.Types.ObjectId, + ref: 'User', + index: true + }, + openid: { + type: String, + index: true + }, + userName: { + type: String, + default: '' + }, + userContact: { + type: String, + default: '' + }, + + // 反馈类型 + type: { + type: String, + enum: ['suggestion', 'bug', 'complaint', 'praise', 'other'], + required: true, + index: true + }, + + // 反馈标题 + title: { + type: String, + required: true, + maxlength: 100 + }, + + // 反馈内容 + content: { + type: String, + required: true, + maxlength: 2000 + }, + + // 图片附件 + images: [{ + type: String + }], + + // 相关页面/功能 + relatedPage: { + type: String, + default: '' + }, + + // 设备信息 + deviceInfo: { + device: { type: String, default: '' }, + os: { type: String, default: '' }, + osVersion: { type: String, default: '' }, + browser: { type: String, default: '' }, + browserVersion: { type: String, default: '' }, + screenResolution: { type: String, default: '' }, + appVersion: { type: String, default: '' } + }, + + // 网络信息 + networkInfo: { + type: { type: String, default: '' }, + ip: { type: String, default: '' } + }, + + // 处理状态 + status: { + type: String, + enum: ['pending', 'processing', 'resolved', 'rejected', 'closed'], + default: 'pending', + index: true + }, + + // 优先级 + priority: { + type: String, + enum: ['low', 'normal', 'high', 'urgent'], + default: 'normal' + }, + + // 处理记录 + processLog: [{ + operator: { type: String, required: true }, + action: { type: String, required: true }, + comment: { type: String, default: '' }, + createdAt: { type: Date, default: Date.now } + }], + + // 处理结果 + result: { + type: String, + default: '' + }, + + // 处理人 + handler: { + type: String, + default: '' + }, + + // 处理时间 + handledAt: { + type: Date + }, + + // 用户评分(对处理结果的满意度) + rating: { + type: Number, + min: 1, + max: 5 + }, + + // 用户备注 + userRemark: { + type: String, + default: '' + }, + + // 创建时间 + createdAt: { + type: Date, + default: Date.now, + index: true + }, + + // 更新时间 + updatedAt: { + type: Date, + default: Date.now + } +}, { + timestamps: { createdAt: false, updatedAt: true } +}) + +// 索引 +FeedbackSchema.index({ status: 1, createdAt: -1 }) +FeedbackSchema.index({ type: 1, createdAt: -1 }) +FeedbackSchema.index({ priority: 1, createdAt: -1 }) + +// 静态方法:获取待处理的反馈数量 +FeedbackSchema.statics.getPendingCount = function() { + return this.countDocuments({ status: 'pending' }) +} + +// 静态方法:获取反馈统计 +FeedbackSchema.statics.getStats = function(startDate, endDate) { + return this.aggregate([ + { + $match: { + createdAt: { $gte: startDate, $lte: endDate } + } + }, + { + $group: { + _id: '$status', + count: { $sum: 1 } + } + } + ]) +} + +// 静态方法:按类型统计 +FeedbackSchema.statics.getTypeStats = function(startDate, endDate) { + return this.aggregate([ + { + $match: { + createdAt: { $gte: startDate, $lte: endDate } + } + }, + { + $group: { + _id: '$type', + count: { $sum: 1 } + } + } + ]) +} + +module.exports = mongoose.model('Feedback', FeedbackSchema) diff --git a/backend/wdkj-server/src/models/Gallery.js b/backend/wdkj-server/src/models/Gallery.js new file mode 100755 index 0000000..9d5530e --- /dev/null +++ b/backend/wdkj-server/src/models/Gallery.js @@ -0,0 +1,106 @@ +const mongoose = require('mongoose') +const { Schema } = mongoose + +/** + * 画廊作品模型 + */ +const GallerySchema = new Schema({ + // 用户信息 + openid: { + type: String, + required: false, + index: true + }, + userId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User', + index: true + }, + authorName: { + type: String, + default: '星辰旅者' + }, + authorAvatar: { + type: String, + default: '' + }, + + // 作品信息 + title: { + type: String, + required: true, + trim: true, + maxlength: 100 + }, + description: { + type: String, + trim: true, + maxlength: 500 + }, + + // 图片数据(Base64 或 URL) + imageData: String, + imageUrl: String, + + // 作品属性 + tags: [{ + type: String, + maxlength: 20 + }], + dim: { + type: Number, + enum: [1, 2, 3, 4, 5], + default: 1 + }, + + // 统计 + likeCount: { + type: Number, + default: 0 + }, + viewCount: { + type: Number, + default: 0 + }, + + // 审核状态 + status: { + type: String, + enum: ['pending', 'approved', 'rejected'], + default: 'pending', + index: true + }, + reviewComment: String, + reviewedAt: Date, + reviewerId: String, + + // 时间戳 + createdAt: { + type: Date, + default: Date.now, + index: true + } +}, { + timestamps: true, + toJSON: { virtuals: true }, + toObject: { virtuals: true } +}) + +// 索引 +GallerySchema.index({ openid: 1, createdAt: -1 }) +GallerySchema.index({ status: 1, createdAt: -1 }) +GallerySchema.index({ dim: 1, createdAt: -1 }) +GallerySchema.index({ likeCount: -1 }) + +// 静态方法:获取今日作品数 +GallerySchema.statics.getTodayCount = async function() { + const today = new Date() + today.setHours(0, 0, 0, 0) + + return this.countDocuments({ + createdAt: { $gte: today }, + status: 'approved' + }) +} + +module.exports = mongoose.model('Gallery', GallerySchema) diff --git a/backend/wdkj-server/src/models/Knowledge.js b/backend/wdkj-server/src/models/Knowledge.js new file mode 100755 index 0000000..d93ef4a --- /dev/null +++ b/backend/wdkj-server/src/models/Knowledge.js @@ -0,0 +1,125 @@ +const mongoose = require('mongoose') +const { Schema } = mongoose + +/** + * 知识库模型 + */ +const KnowledgeSchema = new Schema({ + // 基本信息 + title: { + type: String, + required: true, + trim: true, + maxlength: 100 + }, + content: { + type: String, + required: true + }, + + // 分类 + dim: { + type: Number, + enum: [1, 2, 3, 4, 5], + required: true, + index: true + }, + category: { + type: String, + enum: ['concept', 'history', 'application', 'question'], + required: true + }, + tags: [{ + type: String, + maxlength: 20 + }], + + // 多层级内容 + sections: [{ + title: String, + content: String, + images: [String] + }], + + // 问答系统 + question: String, + answer: String, + options: [{ + text: String, + isCorrect: Boolean + }], + + // 统计 + viewCount: { + type: Number, + default: 0 + }, + collectionCount: { + type: Number, + default: 0 + }, + + // 付费与订阅 + isPremium: { + type: Boolean, + default: false + }, + requiredPoints: { + type: Number, + default: 0 + }, + price: { + type: Number, + default: 0 + }, + currency: { + type: String, + default: 'CNY' + }, + + // 状态 + status: { + type: String, + enum: ['draft', 'pending', 'approved', 'rejected'], + default: 'draft', + index: true + }, + + // 审核信息 + reviewComment: String, + reviewedAt: Date, + reviewerId: String, + + // 排序 + sortOrder: { + type: Number, + default: 0 + }, + + // 时间戳 + createdAt: { + type: Date, + default: Date.now + }, + updatedAt: { + type: Date, + default: Date.now + } +}, { + timestamps: true, + toJSON: { virtuals: true }, + toObject: { virtuals: true } +}) + +// 索引 +KnowledgeSchema.index({ dim: 1, sortOrder: 1 }) +KnowledgeSchema.index({ status: 1, createdAt: -1 }) +KnowledgeSchema.index({ category: 1 }) + +// 中间件:保存前更新 updatedAt +KnowledgeSchema.pre('save', function(next) { + this.updatedAt = Date.now() + next() +}) + +module.exports = mongoose.model('Knowledge', KnowledgeSchema) diff --git a/backend/wdkj-server/src/models/Level.js b/backend/wdkj-server/src/models/Level.js new file mode 100755 index 0000000..b854bfb --- /dev/null +++ b/backend/wdkj-server/src/models/Level.js @@ -0,0 +1,79 @@ +const mongoose = require('mongoose') +const { Schema } = mongoose + +/** + * 关卡配置模型 + */ +const LevelSchema = new Schema({ + // 关卡基本信息 + dimension: { + type: Number, + required: true, + enum: [1, 2, 3, 4, 5], + index: true + }, + level: { + type: Number, + required: true + }, + + // 关卡难度配置 + difficulty: { + type: String, + enum: ['easy', 'normal', 'hard', 'expert'], + default: 'normal' + }, + + // 通关条件 + requirements: { + score: { type: Number, default: 0 }, + timeLimit: { type: Number, default: 0 }, // 秒,0表示无限制 + collectibles: { type: Number, default: 0 } // 需要收集的物品数量 + }, + + // 奖励配置 + rewards: { + exp: { type: Number, default: 100 }, + coins: { type: Number, default: 50 }, + unlockNext: { type: Boolean, default: true } + }, + + // 关卡描述 + title: String, + description: String, + tips: [String], // 游戏提示 + + // 状态 + isActive: { + type: Boolean, + default: true + }, + isLocked: { + type: Boolean, + default: false + }, + + // 排序 + sortOrder: { + type: Number, + default: 0 + }, + + // 时间戳 + createdAt: { + type: Date, + default: Date.now + }, + updatedAt: { + type: Date, + default: Date.now + } +}, { + timestamps: true +}) + +// 复合索引:维度 + 关卡等级 +LevelSchema.index({ dimension: 1, level: 1 }, { unique: true }) +LevelSchema.index({ dimension: 1, isActive: 1, sortOrder: 1 }) + +module.exports = mongoose.model('Level', LevelSchema) diff --git a/backend/wdkj-server/src/models/LoginLog.js b/backend/wdkj-server/src/models/LoginLog.js new file mode 100755 index 0000000..0e6c921 --- /dev/null +++ b/backend/wdkj-server/src/models/LoginLog.js @@ -0,0 +1,142 @@ +const mongoose = require('mongoose') +const { Schema } = mongoose + +/** + * 登录日志模型 + * 记录用户的登录/注册行为 + */ +const LoginLogSchema = new Schema({ + // 用户标识 + userId: { + type: Schema.Types.ObjectId, + ref: 'User', + required: true, + index: true + }, + openid: { + type: String, + required: true, + index: true + }, + + // 登录类型 + type: { + type: String, + enum: ['register', 'login', 'auto_login'], + required: true + }, + + // 登录方式 + method: { + type: String, + enum: ['wechat_miniapp', 'wechat_webview', 'wechat_oauth', 'username', 'other'], + required: true + }, + + // IP地址 + ip: { + type: String, + default: '' + }, + + // 终端信息 + userAgent: { + type: String, + default: '' + }, + + // 设备信息 + device: { + type: String, + default: '' + }, + + // 操作系统 + os: { + type: String, + default: '' + }, + + // 浏览器 + browser: { + type: String, + default: '' + }, + + // 登录来源 + source: { + type: String, + enum: ['miniapp', 'webview', 'h5', 'admin', 'other'], + default: 'other' + }, + + // 登录结果 + success: { + type: Boolean, + default: true + }, + + // 失败原因 + failReason: { + type: String, + default: '' + }, + + // 地理位置(可选) + location: { + country: { type: String, default: '' }, + province: { type: String, default: '' }, + city: { type: String, default: '' } + }, + + // 登录时间 + createdAt: { + type: Date, + default: Date.now, + index: true + } +}, { + timestamps: false +}) + +// 索引 +LoginLogSchema.index({ userId: 1, createdAt: -1 }) +LoginLogSchema.index({ openid: 1, createdAt: -1 }) +LoginLogSchema.index({ type: 1, createdAt: -1 }) +LoginLogSchema.index({ method: 1, createdAt: -1 }) + +// 静态方法:获取用户的登录历史 +LoginLogSchema.statics.getUserLoginHistory = function(userId, limit = 10) { + return this.find({ userId }) + .sort({ createdAt: -1 }) + .limit(limit) + .lean() +} + +// 静态方法:获取最近的登录日志 +LoginLogSchema.statics.getRecentLogs = function(limit = 50) { + return this.find() + .sort({ createdAt: -1 }) + .limit(limit) + .populate('userId', 'nickName avatarUrl') + .lean() +} + +// 静态方法:统计登录数据 +LoginLogSchema.statics.getLoginStats = function(startDate, endDate) { + return this.aggregate([ + { + $match: { + createdAt: { $gte: startDate, $lte: endDate } + } + }, + { + $group: { + _id: '$type', + count: { $sum: 1 } + } + } + ]) +} + +module.exports = mongoose.model('LoginLog', LoginLogSchema) diff --git a/backend/wdkj-server/src/models/Order.js b/backend/wdkj-server/src/models/Order.js new file mode 100755 index 0000000..83845c5 --- /dev/null +++ b/backend/wdkj-server/src/models/Order.js @@ -0,0 +1,120 @@ +const mongoose = require('mongoose') +const { Schema } = mongoose + +/** + * 订单模型 + */ +const OrderSchema = new Schema({ + // 订单基本信息 + orderNo: { + type: String, + required: true, + unique: true, + index: true + }, + + // 用户信息 + openid: { + type: String, + required: true, + index: true + }, + userId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User', + index: true + }, + + // 商品信息 + productId: { + type: String, + required: true + }, + productName: { + type: String, + required: true + }, + amount: { + type: Number, + required: true + }, + + // 支付信息 + status: { + type: String, + enum: ['pending', 'paid', 'completed', 'failed', 'refunded'], + default: 'pending', + index: true + }, + prepayId: String, + transactionId: String, + + // 微信支付参数(用于调试) + paymentParams: Schema.Types.Mixed, + rawResult: Schema.Types.Mixed, + + // 审核信息(管理后台) + reviewComment: String, + reviewedAt: Date, + reviewerId: String, + + // 时间戳 + createdAt: { + type: Date, + default: Date.now, + index: true + }, + paidAt: Date, + completedAt: Date, + refundedAt: Date +}, { + timestamps: true, + toJSON: { virtuals: true }, + toObject: { virtuals: true } +}) + +// 索引 +OrderSchema.index({ openid: 1, createdAt: -1 }) +OrderSchema.index({ status: 1, createdAt: -1 }) +OrderSchema.index({ userId: 1, createdAt: -1 }) // 优化用户订单查询 + +// 静态方法:获取今日订单数 +OrderSchema.statics.getTodayCount = async function() { + const today = new Date() + today.setHours(0, 0, 0, 0) + + return this.countDocuments({ + createdAt: { $gte: today }, + status: 'paid' + }) +} + +// 静态方法:获取订单趋势(7天) +OrderSchema.statics.getWeekTrend = async function() { + const weekAgo = new Date() + weekAgo.setDate(weekAgo.getDate() - 7) + weekAgo.setHours(0, 0, 0, 0) + + return this.aggregate([ + { + $match: { + createdAt: { $gte: weekAgo }, + status: 'paid' + } + }, + { + $group: { + _id: { + $dateToString: { format: '%Y-%m-%d', date: '$createdAt' } + }, + count: { $sum: 1 }, + revenue: { $sum: '$amount' } + } + }, + { + $sort: { '_id': 1 } + } + ]) +} + +module.exports = mongoose.model('Order', OrderSchema) diff --git a/backend/wdkj-server/src/models/ShareRecord.js b/backend/wdkj-server/src/models/ShareRecord.js new file mode 100755 index 0000000..24a1d5e --- /dev/null +++ b/backend/wdkj-server/src/models/ShareRecord.js @@ -0,0 +1,25 @@ +const mongoose = require('mongoose') + +const ShareRecordSchema = new mongoose.Schema({ + openid: { + type: String, + required: true, + index: true + }, + userId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User', + index: true + }, + shareType: { + type: String, + enum: ['app', 'dim1', 'dim2', 'dim3', 'dim4', 'dim5'], + default: 'app' + }, + score: { type: Number, default: 0 }, + sharedAt: { type: Date, default: Date.now } +}) + +ShareRecordSchema.index({ sharedAt: -1 }) + +module.exports = mongoose.model('ShareRecord', ShareRecordSchema) diff --git a/backend/wdkj-server/src/models/ShopItem.js b/backend/wdkj-server/src/models/ShopItem.js new file mode 100755 index 0000000..d96ce2d --- /dev/null +++ b/backend/wdkj-server/src/models/ShopItem.js @@ -0,0 +1,109 @@ +const mongoose = require('mongoose') +const { Schema } = mongoose + +/** + * 商品模型 + */ +const ShopItemSchema = new Schema({ + // 基本信息 + itemId: { + type: String, + required: true, + unique: true, + index: true + }, + name: { + type: String, + required: true, + trim: true + }, + description: { + type: String, + trim: true + }, + + // 价格(单位:分) + price: { + type: Number, + required: true, + min: 1 + }, + originalPrice: { + type: Number + }, + + // 商品类型 + type: { + type: String, + enum: ['skin', 'noad', 'subscription'], + required: true + }, + + // 订阅天数(仅订阅商品) + duration: { + type: Number, + default: 30 + }, + + // 图片 + imageUrl: String, + previewUrl: String, + + // 统计 + purchaseCount: { + type: Number, + default: 0 + }, + + // 状态 + status: { + type: String, + enum: ['active', 'inactive', 'discontinued'], + default: 'active', + index: true + }, + + // 排序 + sortOrder: { + type: Number, + default: 0 + }, + + // 促销 + isOnSale: { + type: Boolean, + default: false + }, + saleEndTime: Date, + + // 时间戳 + createdAt: { + type: Date, + default: Date.now + }, + updatedAt: { + type: Date, + default: Date.now + } +}, { + timestamps: true, + toJSON: { virtuals: true }, + toObject: { virtuals: true } +}) + +// 索引 +ShopItemSchema.index({ status: 1, sortOrder: 1 }) +ShopItemSchema.index({ type: 1 }) + +// 虚拟字段:是否在促销中 +ShopItemSchema.virtual('isOnSaleActive').get(function() { + return this.isOnSale && this.saleEndTime && new Date() < this.saleEndTime +}) + +// 中间件:保存前更新 updatedAt +ShopItemSchema.pre('save', function(next) { + this.updatedAt = Date.now() + next() +}) + +module.exports = mongoose.model('ShopItem', ShopItemSchema) diff --git a/backend/wdkj-server/src/models/Trend.js b/backend/wdkj-server/src/models/Trend.js new file mode 100644 index 0000000..1405cb8 --- /dev/null +++ b/backend/wdkj-server/src/models/Trend.js @@ -0,0 +1,33 @@ +const mongoose = require('mongoose'); +const { Schema } = mongoose; + +const TrendSchema = new Schema({ + title: { type: String, required: true, trim: true }, + summary: { type: String }, + source: { type: String, default: '未知来源' }, + sourceUrl: { type: String }, + category: { + type: String, + enum: ['产品', '论文', '工具', '公司', '政策', '其他'], + default: '其他' + }, + tags: [String], + hot: { type: Boolean, default: false }, + picked: { type: Boolean, default: false }, + newsDate: { type: Date, default: Date.now, index: true }, + viewCount: { type: Number, default: 0 }, + status: { + type: String, + enum: ['draft', 'published'], + default: 'published' + } +}, { + timestamps: true, + toJSON: { virtuals: true }, + toObject: { virtuals: true } +}); + +TrendSchema.index({ status: 1, newsDate: -1 }); +TrendSchema.index({ category: 1, newsDate: -1 }); + +module.exports = mongoose.model('Trend', TrendSchema); \ No newline at end of file diff --git a/backend/wdkj-server/src/models/User.js b/backend/wdkj-server/src/models/User.js new file mode 100755 index 0000000..52b5172 --- /dev/null +++ b/backend/wdkj-server/src/models/User.js @@ -0,0 +1,305 @@ +const mongoose = require('mongoose') +const { Schema } = mongoose +const bcrypt = require('bcryptjs') + +/** + * 用户探索数据结构 + */ +const ExploreDataSchema = new Schema({ + dim1: { + completed: { type: Boolean, default: false }, + bestScore: { type: Number, default: 0 }, + collectedEggs: { type: Number, default: 0 }, + totalLength: { type: Number, default: 0 } + }, + dim2: { + completed: { type: Boolean, default: false }, + bestArea: { type: Number, default: 0 }, + createdShapes: { type: Number, default: 0 } + }, + dim3: { + completed: { type: Boolean, default: false }, + exploredFaces: { type: Number, default: 0 }, + rotationTime: { type: Number, default: 0 }, + bestScore: { type: Number, default: 0 } + }, + dim4: { + completed: { type: Boolean, default: false }, + bestScore: { type: Number, default: 0 }, + exploredEvents: { type: Number, default: 0 } + }, + dim5: { + completed: { type: Boolean, default: false }, + bestScore: { type: Number, default: 0 }, + exploredThoughts: { type: Number, default: 0 } + } +}, { _id: false }) + +/** + * 用户模型 + */ +const UserSchema = new Schema({ + // 微信用户标识 + openid: { + type: String, + required: false, + unique: true, + sparse: true, + index: true + }, + unionid: { + type: String, + index: true + }, + + // H5环境用户名密码登录 + username: { + type: String, + unique: true, + sparse: true, + index: true + }, + password: { + type: String, + select: false + }, + + // 基本信息 + nickName: { + type: String, + default: '星辰旅行者' + }, + avatarUrl: { + type: String, + default: '' + }, + + // 探索数据 + exploreData: { + type: ExploreDataSchema, + default: () => ({}) + }, + + // 分数(冗余字段,便于排行榜查询) + totalScore: { type: Number, default: 0 }, + dim1Score: { type: Number, default: 0 }, + dim2Score: { type: Number, default: 0 }, + dim3Score: { type: Number, default: 0 }, + dim4Score: { type: Number, default: 0 }, + dim5Score: { type: Number, default: 0 }, + + // 已解锁维度 + unlockedDims: [{ + type: Number, + enum: [1, 2, 3, 4, 5] + }], + + // 商品相关 + ownedSkins: [{ + type: String + }], + purchasedItems: [{ + type: String + }], + + // 会员状态 + isAdFree: { + type: Boolean, + default: false + }, + isSubscriber: { + type: Boolean, + default: false + }, + subscribeExpiry: { + type: Date + }, + + // 收藏与成就 + collectedKnowledge: [{ + type: mongoose.Schema.Types.ObjectId, + ref: 'Knowledge' + }], + achievements: [{ + type: String + }], + + // 设置 + settings: { + voiceEnabled: { type: Boolean, default: true }, + difficulty: { + type: String, + enum: ['easy', 'normal', 'hard'], + default: 'normal' + } + }, + + // 探索等级系统 + level: { + type: Number, + default: 1 + }, + levelPoints: { + type: Number, + default: 0 + }, + + // 时间戳 + totalPlayTime: { + type: Number, + default: 0 + }, + createdAt: { + type: Date, + default: Date.now + }, + lastLoginAt: { + type: Date, + default: Date.now + }, + updatedAt: { + type: Date, + default: Date.now + } +}, { + timestamps: true, + toJSON: { virtuals: true }, + toObject: { virtuals: true } +}) + +// 索引 +UserSchema.index({ totalScore: -1 }) +UserSchema.index({ dim1Score: -1 }) +UserSchema.index({ dim2Score: -1 }) +UserSchema.index({ dim3Score: -1 }) +UserSchema.index({ dim4Score: -1 }) +UserSchema.index({ dim5Score: -1 }) +UserSchema.index({ createdAt: -1 }) +UserSchema.index({ lastLoginAt: -1 }) // 用于活跃用户统计 + +// 虚拟字段:是否是新用户(24小时内注册) +UserSchema.virtual('isNew').get(function() { + return Date.now() - this.createdAt < 24 * 60 * 60 * 1000 +}) + +// 中间件:保存前加密密码(仅针对username/password登录) +UserSchema.pre('save', async function(next) { + if (!this.isModified('password')) { + return next() + } + + try { + const salt = await bcrypt.genSalt(10) + this.password = await bcrypt.hash(this.password, salt) + next() + } catch (error) { + next(error) + } +}) + +// 实例方法:验证密码 +UserSchema.methods.validatePassword = async function(password) { + return bcrypt.compare(password, this.password) +} + +// 实例方法:计算总分 +UserSchema.methods.calculateTotalScore = function() { + this.totalScore = + (this.exploreData.dim1?.bestScore || 0) + + (this.exploreData.dim2?.bestArea || 0) + + (this.exploreData.dim3?.bestScore || 0) + + (this.exploreData.dim4?.bestScore || 0) + + (this.exploreData.dim5?.bestScore || 0) + + // 同步维度分数 + this.dim1Score = this.exploreData.dim1?.bestScore || 0 + this.dim2Score = this.exploreData.dim2?.bestArea || 0 + this.dim3Score = this.exploreData.dim3?.bestScore || 0 + this.dim4Score = this.exploreData.dim4?.bestScore || 0 + this.dim5Score = this.exploreData.dim5?.bestScore || 0 + + return this.totalScore +} + +// 实例方法:计算等级 +UserSchema.methods.calculateLevel = function() { + // 等级计算公式:基于总分和探索时长 + // 基础分:每100分 = 1级 + const scorePoints = Math.floor(this.totalScore / 100) + // 时长分:每1小时 = 1级 + const timePoints = Math.floor(this.totalPlayTime / 60) + // 成就分:每个成就 = 2级 + const achievementPoints = (this.achievements?.length || 0) * 2 + // 收藏分:每5个收藏 = 1级 + const collectionPoints = Math.floor((this.collectedKnowledge?.length || 0) / 5) + + this.levelPoints = scorePoints + timePoints + achievementPoints + collectionPoints + + // 等级阈值表 + const levelThresholds = [ + 0, 5, 15, 30, 50, 75, 105, 140, 180, 225, + 275, 330, 390, 455, 525, 600, 680, 765, 855, 950, 1050 + ] + + // 计算等级 + let newLevel = 1 + for (let i = 0; i < levelThresholds.length; i++) { + if (this.levelPoints >= levelThresholds[i]) { + newLevel = i + 1 + } else { + break + } + } + + this.level = newLevel + return this.level +} + +// 实例方法:获取等级信息 +UserSchema.methods.getLevelInfo = function() { + const levelNames = [ + '星辰旅者', '维度学徒', '空间探索者', '几何学者', '维度行者', + '时空旅人', '多维大师', '宇宙探索者', '维度掌控者', '空间主宰', + '维度领主', '宇宙行者', '时空主宰', '维度之神', '宇宙之主', + '维度创世者', '空间造物主', '宇宙掌控者', '维度至尊', '宇宙之神' + ] + + const levelThresholds = [ + 0, 5, 15, 30, 50, 75, 105, 140, 180, 225, + 275, 330, 390, 455, 525, 600, 680, 765, 855, 950, 1050 + ] + + const currentLevel = this.level || 1 + const currentPoints = this.levelPoints || 0 + const nextLevelPoints = levelThresholds[currentLevel] || levelThresholds[levelThresholds.length - 1] + const progress = Math.min(100, Math.floor((currentPoints / nextLevelPoints) * 100)) + + return { + level: currentLevel, + name: levelNames[Math.min(currentLevel - 1, levelNames.length - 1)], + points: currentPoints, + nextLevelPoints: nextLevelPoints, + progress: progress, + totalProgress: Math.min(100, Math.floor((currentPoints / 1050) * 100)) + } +} + +// 静态方法:获取排行榜 +UserSchema.statics.getLeaderboard = function(dim = null, limit = 50) { + const sortField = dim ? `dim${dim}Score` : 'totalScore' + return this.find() + .select('openid nickName avatarUrl totalScore dim1Score dim2Score dim3Score dim4Score dim5Score unlockedDims') + .sort({ [sortField]: -1 }) + .limit(limit) + .lean() +} + +// 中间件:保存前更新 updatedAt、计算总分和等级 +UserSchema.pre('save', function(next) { + this.updatedAt = Date.now() + this.calculateTotalScore() + this.calculateLevel() + next() +}) + +module.exports = mongoose.model('User', UserSchema) diff --git a/backend/wdkj-server/src/models/index.js b/backend/wdkj-server/src/models/index.js new file mode 100755 index 0000000..11bf958 --- /dev/null +++ b/backend/wdkj-server/src/models/index.js @@ -0,0 +1,38 @@ +const User = require('./User') +const Order = require('./Order') +const Gallery = require('./Gallery') +const Knowledge = require('./Knowledge') +const ShopItem = require('./ShopItem') +const Admin = require('./Admin') +const AdminLog = require('./AdminLog') +const ShareRecord = require('./ShareRecord') +const BGM = require('./BGM') +const Level = require('./Level') +const AIChatQuota = require('./AIChatQuota') +const AIModel = require('./AIModel') +const LoginLog = require('./LoginLog') +const Feedback = require('./Feedback') +const Trend = require('./Trend') + +// 拼音探索模块模型 +const pinyinModels = require('./pinyin') + +module.exports = { + User, + Order, + Gallery, + Knowledge, + ShopItem, + Admin, + AdminLog, + ShareRecord, + BGM, + Level, + AIChatQuota, + AIModel, + LoginLog, + Feedback, + Trend, + // 拼音探索模型 + ...pinyinModels +} diff --git a/backend/wdkj-server/src/models/pinyin/PinyinAchievement.js b/backend/wdkj-server/src/models/pinyin/PinyinAchievement.js new file mode 100755 index 0000000..17102cb --- /dev/null +++ b/backend/wdkj-server/src/models/pinyin/PinyinAchievement.js @@ -0,0 +1,110 @@ +const mongoose = require('mongoose'); + +/** + * 拼音探索成就模型 + * 定义可获得的成就及其条件 + */ +const pinyinAchievementSchema = new mongoose.Schema({ + // 成就代码 + code: { + type: String, + required: true, + unique: true, + trim: true + }, + + // 成就名称 + name: { + type: String, + required: true, + trim: true + }, + + // 成就描述 + description: { + type: String, + required: true + }, + + // 成就图标 + icon: { + type: String, + default: '' + }, + + // 成就类型 + type: { + type: String, + enum: ['explore', 'game', 'collection', 'streak', 'special'], + default: 'explore' + }, + + // 达成条件 + condition: { + type: { + type: String, + required: true, + enum: [ + 'explore_count', // 探索数量 + 'collect_count', // 收集数量 + 'game_count', // 游戏次数 + 'game_score', // 游戏分数 + 'streak_days', // 连续天数 + 'complete_symbol', // 完成指定拼音 + 'complete_type', // 完成某类型所有拼音 + 'explore_all' // 探索所有内容 + ] + }, + value: { type: Number, required: true }, // 条件值 + symbol: { type: String }, // 特定拼音(可选) + symbolType: { type: String } // 特定类型(可选) + }, + + // 奖励 + reward: { + type: { + type: String, + enum: ['stone', 'badge', 'theme', 'none'], + default: 'none' + }, + value: { type: Number, default: 0 }, + item: { type: String, default: '' } + }, + + // 排序 + order: { + type: Number, + default: 0 + }, + + // 是否启用 + isActive: { + type: Boolean, + default: true + }, + + // 创建时间 + createdAt: { + type: Date, + default: Date.now + }, + + // 更新时间 + updatedAt: { + type: Date, + default: Date.now + } +}); + +// 索引 +pinyinAchievementSchema.index({ type: 1, order: 1 }); +pinyinAchievementSchema.index({ isActive: 1 }); +pinyinAchievementSchema.index({ code: 1 }); + +// 更新中间件 +pinyinAchievementSchema.pre('save', function(next) { + this.updatedAt = Date.now(); + next(); +}); + +module.exports = mongoose.model('PinyinAchievement', pinyinAchievementSchema); diff --git a/backend/wdkj-server/src/models/pinyin/PinyinContent.js b/backend/wdkj-server/src/models/pinyin/PinyinContent.js new file mode 100755 index 0000000..a9b954c --- /dev/null +++ b/backend/wdkj-server/src/models/pinyin/PinyinContent.js @@ -0,0 +1,120 @@ +const mongoose = require('mongoose'); + +/** + * 拼音内容模型 + * 存储拼音字母的基础信息、音频、口型图、关联词语等 + */ +const pinyinContentSchema = new mongoose.Schema({ + // 拼音符号,如 "b" + symbol: { + type: String, + required: true, + unique: true, + trim: true + }, + + // 拼音类型:initial(声母)/final(韵母)/overall(整体认读) + type: { + type: String, + required: true, + enum: ['initial', 'final', 'overall'] + }, + + // 拼音名称,如 "玻" + name: { + type: String, + required: true, + trim: true + }, + + // 发音音频URL + audioUrl: { + type: String, + default: '' + }, + + // 口型示意图URL + mouthImage: { + type: String, + default: '' + }, + + // 发音方法描述 + pronunciation: { + type: String, + default: '' + }, + + // 显示顺序 + order: { + type: Number, + default: 0 + }, + + // 是否免费 + isFree: { + type: Boolean, + default: true + }, + + // 状态:active(启用)/inactive(禁用) + status: { + type: String, + enum: ['active', 'inactive'], + default: 'active' + }, + + // 相关词语 + words: [{ + word: { type: String, required: true }, // 汉字 + pinyin: { type: String, required: true }, // 拼音 + image: { type: String, default: '' }, // 图片URL + audioUrl: { type: String, default: '' }, // 音频URL + meaning: { type: String, default: '' } // 释义 + }], + + // 关联游戏配置 + games: [{ + type: { + type: String, + enum: ['match', 'tone', 'find', 'puzzle', 'mimic', 'runner'] + }, + config: { type: mongoose.Schema.Types.Mixed, default: {} }, + isEnabled: { type: Boolean, default: true } + }], + + // 探索区域配置 + exploreAreas: { + audio: { type: Boolean, default: true }, // 声音发现 + mouth: { type: Boolean, default: true }, // 口型观察 + speak: { type: Boolean, default: true }, // 语音互动 + write: { type: Boolean, default: true }, // 书写体验 + game: { type: Boolean, default: true }, // 趣味互动 + words: { type: Boolean, default: true } // 词语发现 + }, + + // 创建时间 + createdAt: { + type: Date, + default: Date.now + }, + + // 更新时间 + updatedAt: { + type: Date, + default: Date.now + } +}); + +// 索引 +pinyinContentSchema.index({ type: 1, order: 1 }); +pinyinContentSchema.index({ status: 1 }); +pinyinContentSchema.index({ isFree: 1 }); + +// 更新中间件 +pinyinContentSchema.pre('save', function(next) { + this.updatedAt = Date.now(); + next(); +}); + +module.exports = mongoose.model('PinyinContent', pinyinContentSchema); diff --git a/backend/wdkj-server/src/models/pinyin/PinyinGameRecord.js b/backend/wdkj-server/src/models/pinyin/PinyinGameRecord.js new file mode 100755 index 0000000..997711c --- /dev/null +++ b/backend/wdkj-server/src/models/pinyin/PinyinGameRecord.js @@ -0,0 +1,142 @@ +const mongoose = require('mongoose'); + +/** + * 拼音游戏记录模型 + * 记录用户的游戏行为和成绩 + */ +const pinyinGameRecordSchema = new mongoose.Schema({ + // 用户ID + userId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User', + required: true + }, + + // 游戏类型 + gameType: { + type: String, + required: true, + enum: ['match', 'tone', 'find', 'puzzle', 'mimic', 'runner'] + }, + + // 游戏难度 + difficulty: { + type: String, + enum: ['easy', 'normal', 'hard'], + default: 'normal' + }, + + // 得分 + score: { + type: Number, + default: 0 + }, + + // 游戏时长(秒) + duration: { + type: Number, + default: 0 + }, + + // 正确数 + correctCount: { + type: Number, + default: 0 + }, + + // 错误数 + wrongCount: { + type: Number, + default: 0 + }, + + // 准确率 + accuracy: { + type: Number, + default: 0 + }, + + // 游戏详情 + details: { + type: mongoose.Schema.Types.Mixed, + default: {} + }, + + // 是否完成 + isCompleted: { + type: Boolean, + default: true + }, + + // 游戏时间 + playedAt: { + type: Date, + default: Date.now + }, + + // 创建时间 + createdAt: { + type: Date, + default: Date.now + } +}); + +// 索引 +pinyinGameRecordSchema.index({ userId: 1, gameType: 1 }); +pinyinGameRecordSchema.index({ userId: 1, playedAt: -1 }); +pinyinGameRecordSchema.index({ gameType: 1, score: -1 }); + +// 静态方法:获取用户游戏统计 +pinyinGameRecordSchema.statics.getUserStats = async function(userId) { + const stats = await this.aggregate([ + { $match: { userId: mongoose.Types.ObjectId(userId) } }, + { + $group: { + _id: '$gameType', + totalGames: { $sum: 1 }, + totalScore: { $sum: '$score' }, + avgScore: { $avg: '$score' }, + maxScore: { $max: '$score' }, + totalDuration: { $sum: '$duration' }, + avgAccuracy: { $avg: '$accuracy' } + } + } + ]); + return stats; +}; + +// 静态方法:获取排行榜 +pinyinGameRecordSchema.statics.getLeaderboard = async function(gameType, limit = 10) { + const leaderboard = await this.aggregate([ + { $match: { gameType: gameType } }, + { + $group: { + _id: '$userId', + maxScore: { $max: '$score' }, + totalGames: { $sum: 1 } + } + }, + { $sort: { maxScore: -1 } }, + { $limit: limit }, + { + $lookup: { + from: 'users', + localField: '_id', + foreignField: '_id', + as: 'user' + } + }, + { + $project: { + userId: '$_id', + maxScore: 1, + totalGames: 1, + nickname: { $arrayElemAt: ['$user.nickname', 0] }, + avatar: { $arrayElemAt: ['$user.avatar', 0] } + } + } + ]); + return leaderboard; +}; + +module.exports = mongoose.model('PinyinGameRecord', pinyinGameRecordSchema); diff --git a/backend/wdkj-server/src/models/pinyin/PinyinProgress.js b/backend/wdkj-server/src/models/pinyin/PinyinProgress.js new file mode 100755 index 0000000..59401de --- /dev/null +++ b/backend/wdkj-server/src/models/pinyin/PinyinProgress.js @@ -0,0 +1,189 @@ +const mongoose = require('mongoose'); + +/** + * 用户拼音探索进度模型 + * 记录用户的探索进度、收集的能量石、成就等 + */ +const pinyinProgressSchema = new mongoose.Schema({ + // 用户ID + userId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User', + required: true, + unique: true + }, + + // 已探索的拼音 + exploredSymbols: [{ + symbol: { type: String, required: true }, // 拼音符号 + exploredAt: { type: Date, default: Date.now }, // 首次探索时间 + completedAreas: [{ type: String }], // 完成的探索区域 + isCollected: { type: Boolean, default: false }, // 是否收集能量石 + collectedAt: { type: Date }, // 收集时间 + lastExploredAt: { type: Date } // 最后探索时间 + }], + + // 当前正在探索的拼音 + currentSymbol: { + type: String, + default: null + }, + + // 总探索数 + totalExplored: { + type: Number, + default: 0 + }, + + // 收集的能量石数量 + totalStones: { + type: Number, + default: 0 + }, + + // 获得的成就 + achievements: [{ + code: { type: String, required: true }, // 成就代码 + obtainedAt: { type: Date, default: Date.now } // 获得时间 + }], + + // 每日统计 + dailyStats: [{ + date: { type: Date, required: true }, // 日期 + exploreCount: { type: Number, default: 0 }, // 探索次数 + gameCount: { type: Number, default: 0 }, // 游戏次数 + duration: { type: Number, default: 0 } // 时长(分钟) + }], + + // 连续探索天数 + streakDays: { + type: Number, + default: 0 + }, + + // 最后探索日期 + lastExploreDate: { + type: Date + }, + + // 创建时间 + createdAt: { + type: Date, + default: Date.now + }, + + // 更新时间 + updatedAt: { + type: Date, + default: Date.now + } +}); + +// 索引 +pinyinProgressSchema.index({ userId: 1 }); +pinyinProgressSchema.index({ totalExplored: -1 }); + +// 更新中间件 +pinyinProgressSchema.pre('save', function(next) { + this.updatedAt = Date.now(); + next(); +}); + +// 方法:获取指定拼音的探索记录 +pinyinProgressSchema.methods.getSymbolProgress = function(symbol) { + return this.exploredSymbols.find(s => s.symbol === symbol); +}; + +// 方法:检查是否已探索指定拼音 +pinyinProgressSchema.methods.hasExplored = function(symbol) { + return this.exploredSymbols.some(s => s.symbol === symbol); +}; + +// 方法:检查是否完成指定拼音的所有区域 +pinyinProgressSchema.methods.isSymbolCompleted = function(symbol) { + const record = this.getSymbolProgress(symbol); + if (!record) return false; + return record.completedAreas.length >= 6; // 6个探索区域 +}; + +// 方法:添加探索记录 +pinyinProgressSchema.methods.addExplore = function(symbol, area) { + let record = this.getSymbolProgress(symbol); + + if (!record) { + record = { + symbol: symbol, + exploredAt: new Date(), + completedAreas: [], + isCollected: false + }; + this.exploredSymbols.push(record); + this.totalExplored += 1; + } + + if (!record.completedAreas.includes(area)) { + record.completedAreas.push(area); + } + + record.lastExploredAt = new Date(); + this.currentSymbol = symbol; + this.lastExploreDate = new Date(); +}; + +// 方法:收集能量石 +pinyinProgressSchema.methods.collectStone = function(symbol) { + const record = this.getSymbolProgress(symbol); + if (record && !record.isCollected) { + record.isCollected = true; + record.collectedAt = new Date(); + this.totalStones += 1; + return true; + } + return false; +}; + +// 方法:添加成就 +pinyinProgressSchema.methods.addAchievement = function(code) { + if (!this.achievements.some(a => a.code === code)) { + this.achievements.push({ + code: code, + obtainedAt: new Date() + }); + return true; + } + return false; +}; + +// 方法:更新每日统计 +pinyinProgressSchema.methods.updateDailyStats = function(type, duration = 0) { + const today = new Date(); + today.setHours(0, 0, 0, 0); + + let dailyStat = this.dailyStats.find(d => { + const statDate = new Date(d.date); + statDate.setHours(0, 0, 0, 0); + return statDate.getTime() === today.getTime(); + }); + + if (!dailyStat) { + dailyStat = { + date: today, + exploreCount: 0, + gameCount: 0, + duration: 0 + }; + this.dailyStats.push(dailyStat); + } + + if (type === 'explore') { + dailyStat.exploreCount += 1; + } else if (type === 'game') { + dailyStat.gameCount += 1; + } + + if (duration > 0) { + dailyStat.duration += duration; + } +}; + +module.exports = mongoose.model('PinyinProgress', pinyinProgressSchema); diff --git a/backend/wdkj-server/src/models/pinyin/index.js b/backend/wdkj-server/src/models/pinyin/index.js new file mode 100755 index 0000000..d939d1e --- /dev/null +++ b/backend/wdkj-server/src/models/pinyin/index.js @@ -0,0 +1,9 @@ +/** + * 拼音探索模块模型导出 + */ +module.exports = { + PinyinContent: require('./PinyinContent'), + PinyinProgress: require('./PinyinProgress'), + PinyinAchievement: require('./PinyinAchievement'), + PinyinGameRecord: require('./PinyinGameRecord') +}; diff --git a/backend/wdkj-server/src/routes/admin.js b/backend/wdkj-server/src/routes/admin.js new file mode 100755 index 0000000..c55912b --- /dev/null +++ b/backend/wdkj-server/src/routes/admin.js @@ -0,0 +1,1345 @@ +const express = require('express') +const router = express.Router() +const mongoose = require('mongoose') +const { User, Order, Gallery, Knowledge, ShopItem, AdminLog, Level, AIChatQuota, Feedback, LoginLog } = require('../models') +const { authMiddleware, requirePermission } = require('../middleware/auth') +const ApiResponse = require('../utils/response') +const logger = require('../utils/logger') + +// 所有管理路由都需要认证 +router.use(authMiddleware) + +/** + * 记录操作日志 + */ +const logAction = async (req, action, resource, resourceId = null, details = {}) => { + try { + await AdminLog.create({ + adminId: req.admin._id, + adminName: req.admin.username, + action, + resource, + resourceId, + details, + ip: req.ip, + userAgent: req.get('User-Agent') + }) + } catch (error) { + logger.error('记录操作日志失败:', error) + } +} + +/** + * 获取仪表盘数据 + * GET /api/admin/dashboard + */ +router.get('/dashboard', requirePermission('dashboard:read'), async (req, res) => { + try { + const today = new Date() + today.setHours(0, 0, 0, 0) + const weekAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000) + + // 今日数据 + const [todayNewUsers, todayActiveUsers, todayOrders, todayWorks] = await Promise.all([ + User.countDocuments({ createdAt: { $gte: today } }), + User.countDocuments({ lastLoginAt: { $gte: today } }), + Order.getTodayCount(), + Gallery.getTodayCount() + ]) + + // 7日趋势 + const [userTrend, orderTrend, workTrend] = await Promise.all([ + User.aggregate([ + { $match: { createdAt: { $gte: weekAgo } } }, + { $group: { _id: { $dateToString: { format: '%Y-%m-%d', date: '$createdAt' } }, count: { $sum: 1 } } }, + { $sort: { '_id': 1 } } + ]), + Order.getWeekTrend(), + Gallery.aggregate([ + { $match: { createdAt: { $gte: weekAgo }, status: 'approved' } }, + { $group: { _id: { $dateToString: { format: '%Y-%m-%d', date: '$createdAt' } }, count: { $sum: 1 } } }, + { $sort: { '_id': 1 } } + ]) + ]) + + await logAction(req, 'read', 'dashboard') + + return ApiResponse.success(res, { + today: { + newUsers: todayNewUsers, + activeUsers: todayActiveUsers, + newOrders: todayOrders, + newWorks: todayWorks + }, + trends: { + users: userTrend.map(t => ({ date: t._id, count: t.count })), + orders: orderTrend.map(t => ({ date: t._id, count: t.count, revenue: t.revenue })), + works: workTrend.map(t => ({ date: t._id, count: t.count })) + } + }) + + } catch (error) { + logger.error('获取仪表盘数据失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 获取用户列表 + * GET /api/admin/users + */ +router.get('/users', requirePermission('users:read'), async (req, res) => { + try { + const { page = 1, limit = 20, search, dimCompleted, hasPurchase } = req.query + + const query = {} + + if (search) { + query.nickName = { $regex: search, $options: 'i' } + } + + if (dimCompleted) { + query[`exploreData.dim${dimCompleted}.completed`] = true + } + + if (hasPurchase === 'true') { + query.ownedSkins = { $exists: true, $ne: [] } + } + + const total = await User.countDocuments(query) + const users = await User.find(query) + .sort({ createdAt: -1 }) + .skip((parseInt(page) - 1) * parseInt(limit)) + .limit(parseInt(limit)) + .lean() + + // 获取每个用户的作品数 + const userIds = users.map(u => u._id.toString()) + const worksCounts = await Gallery.aggregate([ + { $match: { userId: { $in: userIds.map(id => new mongoose.Types.ObjectId(id)) } } }, + { $group: { _id: '$userId', count: { $sum: 1 } } } + ]) + + const worksCountMap = {} + worksCounts.forEach(item => { + worksCountMap[item._id.toString()] = item.count + }) + + // 格式化用户数据,添加等级和统计信息 + const formattedUsers = users.map(user => { + // 计算等级 + const scorePoints = Math.floor((user.totalScore || 0) / 100) + const timePoints = Math.floor((user.totalPlayTime || 0) / 60) + const achievementPoints = (user.achievements?.length || 0) * 2 + const collectionPoints = Math.floor((user.collectedKnowledge?.length || 0) / 5) + const levelPoints = scorePoints + timePoints + achievementPoints + collectionPoints + + const levelThresholds = [ + 0, 5, 15, 30, 50, 75, 105, 140, 180, 225, + 275, 330, 390, 455, 525, 600, 680, 765, 855, 950, 1050 + ] + + let level = 1 + for (let i = 0; i < levelThresholds.length; i++) { + if (levelPoints >= levelThresholds[i]) { + level = i + 1 + } else { + break + } + } + + const levelNames = [ + '星辰旅者', '维度学徒', '空间探索者', '几何学者', '维度行者', + '时空旅人', '多维大师', '宇宙探索者', '维度掌控者', '空间主宰', + '维度领主', '宇宙行者', '时空主宰', '维度之神', '宇宙之主', + '维度创世者', '空间造物主', '宇宙掌控者', '维度至尊', '宇宙之神' + ] + + return { + ...user, + level, + levelName: levelNames[Math.min(level - 1, levelNames.length - 1)], + levelPoints, + worksCount: worksCountMap[user._id.toString()] || 0, + collectionCount: user.collectedKnowledge?.length || 0, + achievementCount: user.achievements?.length || 0 + } + }) + + await logAction(req, 'read', 'users', null, { page, limit, search }) + + return ApiResponse.paginated(res, formattedUsers, { + page: parseInt(page), + limit: parseInt(limit), + total + }) + + } catch (error) { + logger.error('获取用户列表失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 获取用户详情 + * GET /api/admin/users/:id + */ +router.get('/users/:id', requirePermission('users:read'), async (req, res) => { + try { + const user = await User.findById(req.params.id).lean() + + if (!user) { + return ApiResponse.notFound(res, '用户不存在') + } + + // 获取订单历史 + const orders = await Order.find({ openid: user.openid }) + .sort({ createdAt: -1 }) + .limit(20) + .lean() + + // 获取作品 + const works = await Gallery.find({ openid: user.openid }) + .sort({ createdAt: -1 }) + .limit(10) + .lean() + + // 计算等级 + const scorePoints = Math.floor((user.totalScore || 0) / 100) + const timePoints = Math.floor((user.totalPlayTime || 0) / 60) + const achievementPoints = (user.achievements?.length || 0) * 2 + const collectionPoints = Math.floor((user.collectedKnowledge?.length || 0) / 5) + const levelPoints = scorePoints + timePoints + achievementPoints + collectionPoints + + const levelThresholds = [ + 0, 5, 15, 30, 50, 75, 105, 140, 180, 225, + 275, 330, 390, 455, 525, 600, 680, 765, 855, 950, 1050 + ] + + let level = 1 + for (let i = 0; i < levelThresholds.length; i++) { + if (levelPoints >= levelThresholds[i]) { + level = i + 1 + } else { + break + } + } + + const levelNames = [ + '星辰旅者', '维度学徒', '空间探索者', '几何学者', '维度行者', + '时空旅人', '多维大师', '宇宙探索者', '维度掌控者', '空间主宰', + '维度领主', '宇宙行者', '时空主宰', '维度之神', '宇宙之主', + '维度创世者', '空间造物主', '宇宙掌控者', '维度至尊', '宇宙之神' + ] + + // 统计作品数 + const worksCount = await Gallery.countDocuments({ openid: user.openid }) + + await logAction(req, 'read', 'users', req.params.id) + + return ApiResponse.success(res, { + ...user, + level, + levelName: levelNames[Math.min(level - 1, levelNames.length - 1)], + levelPoints, + worksCount, + collectionCount: user.collectedKnowledge?.length || 0, + achievementCount: user.achievements?.length || 0, + orders, + works + }) + + } catch (error) { + logger.error('获取用户详情失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 获取知识列表 + * GET /api/admin/knowledge + */ +router.get('/knowledge', requirePermission('knowledge:read'), async (req, res) => { + try { + const { page = 1, limit = 20, dim, status } = req.query + + const query = {} + if (dim) query.dim = parseInt(dim) + if (status) query.status = status + + const total = await Knowledge.countDocuments(query) + const knowledge = await Knowledge.find(query) + .sort({ createdAt: -1 }) + .skip((parseInt(page) - 1) * parseInt(limit)) + .limit(parseInt(limit)) + .lean() + + await logAction(req, 'read', 'knowledge', null, { page, limit, dim, status }) + + return ApiResponse.paginated(res, knowledge, { + page: parseInt(page), + limit: parseInt(limit), + total + }) + + } catch (error) { + logger.error('获取知识列表失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 创建/更新知识 + * POST /api/admin/knowledge + */ +router.post('/knowledge', requirePermission('knowledge:write'), async (req, res) => { + try { + const { knowledgeId, ...knowledgeData } = req.body + + let knowledge + + if (knowledgeId) { + // 更新 + knowledge = await Knowledge.findByIdAndUpdate( + knowledgeId, + { ...knowledgeData, updatedAt: new Date() }, + { new: true } + ) + + if (!knowledge) { + return ApiResponse.notFound(res, '知识不存在') + } + + await logAction(req, 'update', 'knowledge', knowledgeId, { title: knowledgeData.title }) + } else { + // 创建 + knowledge = new Knowledge({ + ...knowledgeData, + status: 'draft', + viewCount: 0, + collectionCount: 0 + }) + await knowledge.save() + + await logAction(req, 'create', 'knowledge', knowledge._id, { title: knowledgeData.title }) + } + + return ApiResponse.success(res, knowledge, knowledgeId ? '更新成功' : '创建成功') + + } catch (error) { + logger.error('保存知识失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 审核知识 + * POST /api/admin/knowledge/review + */ +router.post('/knowledge/review', requirePermission('knowledge:write'), async (req, res) => { + try { + const { knowledgeId, status, comment } = req.body + + const knowledge = await Knowledge.findByIdAndUpdate( + knowledgeId, + { + status, + reviewComment: comment, + reviewedAt: new Date(), + reviewerId: req.admin._id + }, + { new: true } + ) + + if (!knowledge) { + return ApiResponse.notFound(res, '知识不存在') + } + + await logAction(req, 'review', 'knowledge', knowledgeId, { status, comment }) + + return ApiResponse.success(res, knowledge, '审核完成') + + } catch (error) { + logger.error('审核知识失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 删除知识 + * DELETE /api/admin/knowledge/:id + */ +router.delete('/knowledge/:id', requirePermission('knowledge:write'), async (req, res) => { + try { + const { id } = req.params + + const knowledge = await Knowledge.findByIdAndDelete(id) + + if (!knowledge) { + return ApiResponse.notFound(res, '知识不存在') + } + + await logAction(req, 'delete', 'knowledge', id, { title: knowledge.title }) + + return ApiResponse.success(res, { message: '删除成功' }) + + } catch (error) { + logger.error('删除知识失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 获取画廊作品列表 + * GET /api/admin/gallery + */ +router.get('/gallery', requirePermission('gallery:read'), async (req, res) => { + try { + const { page = 1, limit = 20, status } = req.query + + // 构建查询条件,如果status为空字符串或undefined,则查询所有状态 + const query = {} + if (status && status !== '') { + query.status = status + } + + const total = await Gallery.countDocuments(query) + const works = await Gallery.find(query) + .sort({ createdAt: -1 }) + .skip((parseInt(page) - 1) * parseInt(limit)) + .limit(parseInt(limit)) + .lean() + + await logAction(req, 'read', 'gallery', null, { page, limit, status }) + + return ApiResponse.paginated(res, works, { + page: parseInt(page), + limit: parseInt(limit), + total + }) + + } catch (error) { + logger.error('获取画廊作品列表失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 审核画廊作品 + * POST /api/admin/gallery/review + */ +router.post('/gallery/review', requirePermission('gallery:approve'), async (req, res) => { + try { + const { workId, status, comment } = req.body + + const work = await Gallery.findByIdAndUpdate( + workId, + { + status, + reviewComment: comment, + reviewedAt: new Date(), + reviewerId: req.admin._id + }, + { new: true } + ) + + if (!work) { + return ApiResponse.notFound(res, '作品不存在') + } + + await logAction(req, 'review', 'gallery', workId, { status, comment }) + + return ApiResponse.success(res, work, '审核完成') + + } catch (error) { + logger.error('审核画廊作品失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 获取作品详情 + * GET /api/admin/gallery/:id + */ +router.get('/gallery/:id', requirePermission('gallery:read'), async (req, res) => { + try { + const { id } = req.params + + const work = await Gallery.findById(id) + .populate('authorId', 'nickName avatarUrl') + .lean() + + if (!work) { + return ApiResponse.notFound(res, '作品不存在') + } + + await logAction(req, 'read', 'gallery', id, { title: work.title }) + + return ApiResponse.success(res, work) + + } catch (error) { + logger.error('获取作品详情失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 删除作品 + * DELETE /api/admin/gallery/:id + */ +router.delete('/gallery/:id', requirePermission('gallery:write'), async (req, res) => { + try { + const { id } = req.params + + const work = await Gallery.findByIdAndDelete(id) + + if (!work) { + return ApiResponse.notFound(res, '作品不存在') + } + + await logAction(req, 'delete', 'gallery', id, { title: work.title }) + + return ApiResponse.success(res, null, '删除成功') + + } catch (error) { + logger.error('删除作品失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 批量删除作品 + * POST /api/admin/gallery/batch-delete + */ +router.post('/gallery/batch-delete', requirePermission('gallery:write'), async (req, res) => { + try { + const { ids } = req.body + + if (!ids || !Array.isArray(ids) || ids.length === 0) { + return ApiResponse.error(res, '请选择要删除的作品', 400) + } + + // 批量删除 + const result = await Gallery.deleteMany({ _id: { $in: ids } }) + + await logAction(req, 'batch-delete', 'gallery', null, { count: result.deletedCount, ids }) + + return ApiResponse.success(res, { deletedCount: result.deletedCount }, `成功删除 ${result.deletedCount} 个作品`) + + } catch (error) { + logger.error('批量删除作品失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 获取订单列表 + * GET /api/admin/orders + */ +router.get('/orders', requirePermission('orders:read'), async (req, res) => { + try { + const { page = 1, limit = 20, status } = req.query + + const query = {} + if (status) query.status = status + + const total = await Order.countDocuments(query) + const orders = await Order.find(query) + .sort({ createdAt: -1 }) + .skip((parseInt(page) - 1) * parseInt(limit)) + .limit(parseInt(limit)) + .lean() + + await logAction(req, 'read', 'orders', null, { page, limit, status }) + + return ApiResponse.paginated(res, orders, { + page: parseInt(page), + limit: parseInt(limit), + total + }) + + } catch (error) { + logger.error('获取订单列表失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 获取商品列表 + * GET /api/admin/shop + */ +router.get('/shop', requirePermission('shop:read'), async (req, res) => { + try { + const { page = 1, limit = 20, status = 'active' } = req.query + + const total = await ShopItem.countDocuments({ status }) + const items = await ShopItem.find({ status }) + .sort({ sortOrder: 1 }) + .skip((parseInt(page) - 1) * parseInt(limit)) + .limit(parseInt(limit)) + .lean() + + await logAction(req, 'read', 'shop', null, { page, limit, status }) + + return ApiResponse.paginated(res, items, { + page: parseInt(page), + limit: parseInt(limit), + total + }) + + } catch (error) { + logger.error('获取商品列表失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 创建/更新商品 + * POST /api/admin/shop-item + */ +router.post('/shop-item', requirePermission('shop:write'), async (req, res) => { + try { + const { _id, ...itemData } = req.body + + let item + + if (_id) { + // 更新 + item = await ShopItem.findByIdAndUpdate( + _id, + { ...itemData, updatedAt: new Date() }, + { new: true } + ) + + if (!item) { + return ApiResponse.notFound(res, '商品不存在') + } + + await logAction(req, 'update', 'shop', _id, { name: itemData.name, price: itemData.price }) + } else { + // 创建 + item = new ShopItem({ + ...itemData, + purchaseCount: 0 + }) + await item.save() + + await logAction(req, 'create', 'shop', item._id, { name: itemData.name, price: itemData.price }) + } + + return ApiResponse.success(res, item, _id ? '更新成功' : '创建成功') + + } catch (error) { + logger.error('保存商品失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 删除商品 + * DELETE /api/admin/shop-item/:id + */ +router.delete('/shop-item/:id', requirePermission('shop:write'), async (req, res) => { + try { + const { id } = req.params + + const item = await ShopItem.findByIdAndDelete(id) + + if (!item) { + return ApiResponse.notFound(res, '商品不存在') + } + + await logAction(req, 'delete', 'shop', id, { name: item.name }) + + return ApiResponse.success(res, { message: '删除成功' }) + + } catch (error) { + logger.error('删除商品失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 获取管理员列表 + * GET /api/admin/admins + */ +router.get('/admins', requirePermission('*'), async (req, res) => { + try { + const admins = await mongoose.model('Admin').find({}) + .select('username role status lastLogin loginCount createdAt') + .sort({ createdAt: -1 }) + .lean() + + await logAction(req, 'read', 'admins') + + return ApiResponse.success(res, admins) + + } catch (error) { + logger.error('获取管理员列表失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 获取商品列表 + * GET /api/admin/shop-items + */ +router.get('/shop-items', requirePermission('shop:read'), async (req, res) => { + try { + const { page = 1, limit = 20, status = 'active' } = req.query + + const total = await ShopItem.countDocuments({ status }) + const items = await ShopItem.find({ status }) + .sort({ sortOrder: 1 }) + .skip((parseInt(page) - 1) * parseInt(limit)) + .limit(parseInt(limit)) + .lean() + + await logAction(req, 'read', 'shop', null, { page, limit, status }) + + return ApiResponse.paginated(res, items, { + page: parseInt(page), + limit: parseInt(limit), + total + }) + + } catch (error) { + logger.error('获取商品列表失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 获取当前管理员信息 + * GET /api/admin/profile + */ +router.get('/profile', async (req, res) => { + try { + const admin = await mongoose.model('Admin').findById(req.admin._id) + .select('username role email realName phone avatar permissions status lastLogin loginCount createdAt') + .lean() + + if (!admin) { + return ApiResponse.notFound(res, '管理员不存在') + } + + return ApiResponse.success(res, admin) + + } catch (error) { + logger.error('获取管理员信息失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 获取系统设置 + * GET /api/admin/settings + */ +router.get('/settings', requirePermission('*'), async (req, res) => { + try { + // 返回默认系统设置 + const settings = { + appName: '星辰绘线', + version: '1.0.0', + maintenanceMode: false, + allowRegistration: true, + maxFileSize: 5 * 1024 * 1024, // 5MB + maxWorksPerUser: 100, + autoApproveWorks: false, + currency: 'CNY', + taxRate: 0.06 + } + + return ApiResponse.success(res, settings) + + } catch (error) { + logger.error('获取系统设置失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 更新系统设置 + * PUT /api/admin/settings + */ +router.put('/settings', requirePermission('*'), async (req, res) => { + try { + const { settings } = req.body + + // 这里应该保存到数据库,暂时返回成功 + await logAction(req, 'update', 'settings', null, settings) + + return ApiResponse.success(res, settings, '设置更新成功') + + } catch (error) { + logger.error('更新系统设置失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 获取操作日志列表 + * GET /api/admin/logs + */ +router.get('/logs', requirePermission('*'), async (req, res) => { + try { + const { page = 1, limit = 50, action, resource, adminId } = req.query + + const query = {} + if (action) query.action = action + if (resource) query.resource = resource + if (adminId) query.adminId = adminId + + const pageNum = parseInt(page) + const limitNum = parseInt(limit) + const skip = (pageNum - 1) * limitNum + + const [logs, total] = await Promise.all([ + AdminLog.find(query) + .sort({ timestamp: -1 }) + .skip(skip) + .limit(limitNum) + .populate('adminId', 'username role') + .lean(), + AdminLog.countDocuments(query) + ]) + + return ApiResponse.paginated(res, logs, { + page: pageNum, + limit: limitNum, + total + }) + + } catch (error) { + logger.error('获取操作日志失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 获取关卡列表 + * GET /api/admin/levels + */ +router.get('/levels', requirePermission('knowledge:read'), async (req, res) => { + try { + const { page = 1, limit = 50, dimension, isActive } = req.query + + const query = {} + if (dimension) query.dimension = parseInt(dimension) + if (isActive !== undefined) query.isActive = isActive === 'true' + + const pageNum = parseInt(page) + const limitNum = parseInt(limit) + const skip = (pageNum - 1) * limitNum + + const [levels, total] = await Promise.all([ + Level.find(query) + .sort({ dimension: 1, level: 1 }) + .skip(skip) + .limit(limitNum) + .lean(), + Level.countDocuments(query) + ]) + + return ApiResponse.paginated(res, levels, { + page: pageNum, + limit: limitNum, + total + }) + + } catch (error) { + logger.error('获取关卡列表失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 创建/更新关卡 + * POST /api/admin/level + */ +router.post('/level', requirePermission('knowledge:write'), async (req, res) => { + try { + const { _id, ...levelData } = req.body + + let level + + if (_id) { + // 更新 + level = await Level.findByIdAndUpdate( + _id, + { ...levelData, updatedAt: new Date() }, + { new: true } + ) + + if (!level) { + return ApiResponse.notFound(res, '关卡不存在') + } + + await logAction(req, 'update', 'level', _id, { dimension: levelData.dimension, level: levelData.level }) + } else { + // 创建 + level = new Level(levelData) + await level.save() + + await logAction(req, 'create', 'level', level._id, { dimension: levelData.dimension, level: levelData.level }) + } + + return ApiResponse.success(res, level, _id ? '更新成功' : '创建成功') + + } catch (error) { + logger.error('保存关卡失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 删除关卡 + * DELETE /api/admin/level/:id + */ +router.delete('/level/:id', requirePermission('knowledge:write'), async (req, res) => { + try { + const { id } = req.params + + const level = await Level.findByIdAndDelete(id) + + if (!level) { + return ApiResponse.notFound(res, '关卡不存在') + } + + await logAction(req, 'delete', 'level', id, { dimension: level.dimension, level: level.level }) + + return ApiResponse.success(res, { message: '删除成功' }) + + } catch (error) { + logger.error('删除关卡失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 获取AI问答统计数据 + * GET /api/admin/ai-chat/stats + */ +router.get('/ai-chat/stats', requirePermission('dashboard:read'), async (req, res) => { + try { + // 总用户数 + const totalUsers = await AIChatQuota.countDocuments() + + // 总提问数 + const totalQuestions = await AIChatQuota.aggregate([ + { $group: { _id: null, total: { $sum: '$usedQuota' } } } + ]) + + // 总分享次数 + const totalShares = await AIChatQuota.aggregate([ + { $group: { _id: null, total: { $sum: { $size: '$shareRecords' } } } } + ]) + + // 日均提问数(最近7天) + const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) + const dailyQuestions = await AIChatQuota.aggregate([ + { $match: { lastUsedDate: { $gte: weekAgo } } }, + { $group: { _id: { $dateToString: { format: '%Y-%m-%d', date: '$lastUsedDate' } }, count: { $sum: '$dailyUsed' } } } + ]) + + const avgDailyQuestions = dailyQuestions.length > 0 + ? Math.round(dailyQuestions.reduce((sum, d) => sum + d.count, 0) / dailyQuestions.length) + : 0 + + return ApiResponse.success(res, { + totalUsers, + totalQuestions: totalQuestions[0]?.total || 0, + totalShares: totalShares[0]?.total || 0, + avgDailyQuestions + }) + } catch (error) { + logger.error('获取AI问答统计失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 获取AI问答配额列表 + * GET /api/admin/ai-chat/quota-list + */ +router.get('/ai-chat/quota-list', requirePermission('user:read'), async (req, res) => { + try { + const { page = 1, pageSize = 10, keyword = '' } = req.query + + const query = keyword ? { openid: { $regex: keyword, $options: 'i' } } : {} + + const [list, total] = await Promise.all([ + AIChatQuota.find(query) + .sort({ updatedAt: -1 }) + .skip((page - 1) * pageSize) + .limit(parseInt(pageSize)), + AIChatQuota.countDocuments(query) + ]) + + // 计算剩余次数 + const formattedList = list.map(item => ({ + ...item.toObject(), + remainingQuota: item.getRemainingQuota() + })) + + return ApiResponse.success(res, { + list: formattedList, + total, + page: parseInt(page), + pageSize: parseInt(pageSize) + }) + } catch (error) { + logger.error('获取AI问答配额列表失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 增加用户AI问答次数 + * POST /api/admin/ai-chat/add-quota + */ +router.post('/ai-chat/add-quota', requirePermission('user:write'), async (req, res) => { + try { + const { openid, amount = 5 } = req.body + + if (!openid) { + return ApiResponse.error(res, '用户openid不能为空', 400) + } + + let quota = await AIChatQuota.findOne({ openid }) + + if (!quota) { + quota = new AIChatQuota({ openid }) + } + + quota.sharedQuota += parseInt(amount) + await quota.save() + + await logAction(req, 'add-quota', 'ai-chat', null, { openid, amount }) + + return ApiResponse.success(res, { + message: `成功增加${amount}次问答机会`, + remainingQuota: quota.getRemainingQuota() + }) + } catch (error) { + logger.error('增加AI问答次数失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 重置用户AI问答配额 + * POST /api/admin/ai-chat/reset-quota + */ +router.post('/ai-chat/reset-quota', requirePermission('user:write'), async (req, res) => { + try { + const { openid } = req.body + + if (!openid) { + return ApiResponse.error(res, '用户openid不能为空', 400) + } + + const quota = await AIChatQuota.findOneAndUpdate( + { openid }, + { + freeQuota: 5, + sharedQuota: 0, + usedQuota: 0, + dailyUsed: 0, + shareRecords: [] + }, + { new: true, upsert: true } + ) + + await logAction(req, 'reset-quota', 'ai-chat', null, { openid }) + + return ApiResponse.success(res, { + message: '重置成功', + remainingQuota: quota.getRemainingQuota() + }) + } catch (error) { + logger.error('重置AI问答配额失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 获取反馈列表 + * GET /api/admin/feedback + */ +router.get('/feedback', requirePermission('feedback:read'), async (req, res) => { + try { + const { page = 1, limit = 20, status, type, priority, search } = req.query + + const query = {} + if (status) query.status = status + if (type) query.type = type + if (priority) query.priority = priority + if (search) { + query.$or = [ + { title: { $regex: search, $options: 'i' } }, + { content: { $regex: search, $options: 'i' } }, + { userName: { $regex: search, $options: 'i' } } + ] + } + + const total = await Feedback.countDocuments(query) + const feedbacks = await Feedback.find(query) + .sort({ createdAt: -1 }) + .skip((parseInt(page) - 1) * parseInt(limit)) + .limit(parseInt(limit)) + .populate('userId', 'nickName avatarUrl') + .lean() + + // 格式化返回数据 + const formattedFeedbacks = feedbacks.map(fb => ({ + id: fb._id, + type: fb.type, + title: fb.title, + content: fb.content.substring(0, 100) + (fb.content.length > 100 ? '...' : ''), + userName: fb.userName || fb.userId?.nickName || '匿名用户', + userAvatar: fb.userId?.avatarUrl || '', + status: fb.status, + priority: fb.priority, + createdAt: fb.createdAt, + hasImages: fb.images?.length > 0 + })) + + await logAction(req, 'read', 'feedback', null, { page, limit, status, type }) + + return ApiResponse.paginated(res, formattedFeedbacks, { + page: parseInt(page), + limit: parseInt(limit), + total + }) + + } catch (error) { + logger.error('获取反馈列表失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 获取反馈详情 + * GET /api/admin/feedback/:id + */ +router.get('/feedback/:id', requirePermission('feedback:read'), async (req, res) => { + try { + const feedback = await Feedback.findById(req.params.id) + .populate('userId', 'nickName avatarUrl openid') + .lean() + + if (!feedback) { + return ApiResponse.notFound(res, '反馈不存在') + } + + await logAction(req, 'read', 'feedback', req.params.id) + + return ApiResponse.success(res, { + id: feedback._id, + type: feedback.type, + title: feedback.title, + content: feedback.content, + images: feedback.images, + userName: feedback.userName || feedback.userId?.nickName || '匿名用户', + userAvatar: feedback.userId?.avatarUrl || '', + userContact: feedback.userContact, + relatedPage: feedback.relatedPage, + deviceInfo: feedback.deviceInfo, + networkInfo: feedback.networkInfo, + status: feedback.status, + priority: feedback.priority, + processLog: feedback.processLog, + result: feedback.result, + handler: feedback.handler, + handledAt: feedback.handledAt, + rating: feedback.rating, + userRemark: feedback.userRemark, + createdAt: feedback.createdAt, + updatedAt: feedback.updatedAt + }) + + } catch (error) { + logger.error('获取反馈详情失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 处理反馈 + * PUT /api/admin/feedback/:id + */ +router.put('/feedback/:id', requirePermission('feedback:update'), async (req, res) => { + try { + const { status, priority, result, comment } = req.body + + const feedback = await Feedback.findById(req.params.id) + if (!feedback) { + return ApiResponse.notFound(res, '反馈不存在') + } + + // 更新字段 + if (status) feedback.status = status + if (priority) feedback.priority = priority + if (result) feedback.result = result + + // 添加处理记录 + if (comment || status) { + feedback.processLog.push({ + operator: req.admin.username, + action: status || 'update', + comment: comment || '', + createdAt: new Date() + }) + } + + // 如果状态变为已解决或已关闭,记录处理人和时间 + if (status === 'resolved' || status === 'closed') { + feedback.handler = req.admin.username + feedback.handledAt = new Date() + } + + await feedback.save() + + await logAction(req, 'update', 'feedback', req.params.id, { status, priority }) + + return ApiResponse.success(res, {}, '反馈处理成功') + + } catch (error) { + logger.error('处理反馈失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 删除反馈 + * DELETE /api/admin/feedback/:id + */ +router.delete('/feedback/:id', requirePermission('feedback:delete'), async (req, res) => { + try { + const feedback = await Feedback.findById(req.params.id) + if (!feedback) { + return ApiResponse.notFound(res, '反馈不存在') + } + + await Feedback.findByIdAndDelete(req.params.id) + + await logAction(req, 'delete', 'feedback', req.params.id) + + return ApiResponse.success(res, {}, '反馈删除成功') + + } catch (error) { + logger.error('删除反馈失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 获取反馈统计 + * GET /api/admin/feedback/stats + */ +router.get('/feedback/stats', requirePermission('feedback:read'), async (req, res) => { + try { + const { startDate, endDate } = req.query + + const start = startDate ? new Date(startDate) : new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) + const end = endDate ? new Date(endDate) : new Date() + + // 状态统计 + const statusStats = await Feedback.aggregate([ + { $match: { createdAt: { $gte: start, $lte: end } } }, + { $group: { _id: '$status', count: { $sum: 1 } } } + ]) + + // 类型统计 + const typeStats = await Feedback.aggregate([ + { $match: { createdAt: { $gte: start, $lte: end } } }, + { $group: { _id: '$type', count: { $sum: 1 } } } + ]) + + // 今日新增 + const today = new Date() + today.setHours(0, 0, 0, 0) + const todayCount = await Feedback.countDocuments({ createdAt: { $gte: today } }) + + // 待处理数量 + const pendingCount = await Feedback.countDocuments({ status: 'pending' }) + + return ApiResponse.success(res, { + statusStats: statusStats.reduce((acc, item) => { + acc[item._id] = item.count + return acc + }, {}), + typeStats: typeStats.reduce((acc, item) => { + acc[item._id] = item.count + return acc + }, {}), + todayCount, + pendingCount, + totalCount: await Feedback.countDocuments() + }) + + } catch (error) { + logger.error('获取反馈统计失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 获取登录日志列表 + * GET /api/admin/login-logs + */ +router.get('/login-logs', requirePermission('user:read'), async (req, res) => { + try { + const { page = 1, limit = 20, userId, type, method, startDate, endDate } = req.query + + const query = {} + if (userId) query.userId = userId + if (type) query.type = type + if (method) query.method = method + if (startDate || endDate) { + query.createdAt = {} + if (startDate) query.createdAt.$gte = new Date(startDate) + if (endDate) query.createdAt.$lte = new Date(endDate) + } + + const total = await LoginLog.countDocuments(query) + const logs = await LoginLog.find(query) + .sort({ createdAt: -1 }) + .skip((parseInt(page) - 1) * parseInt(limit)) + .limit(parseInt(limit)) + .populate('userId', 'nickName avatarUrl') + .lean() + + // 格式化返回数据 + const formattedLogs = logs.map(log => ({ + id: log._id, + userName: log.userId?.nickName || '未知用户', + userAvatar: log.userId?.avatarUrl || '', + type: log.type, + method: log.method, + source: log.source, + ip: log.ip, + device: log.device, + os: log.os, + browser: log.browser, + success: log.success, + failReason: log.failReason, + createdAt: log.createdAt + })) + + await logAction(req, 'read', 'login-logs', null, { page, limit }) + + return ApiResponse.paginated(res, formattedLogs, { + page: parseInt(page), + limit: parseInt(limit), + total + }) + + } catch (error) { + logger.error('获取登录日志失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +module.exports = router diff --git a/backend/wdkj-server/src/routes/aiChat.js b/backend/wdkj-server/src/routes/aiChat.js new file mode 100755 index 0000000..bab18b7 --- /dev/null +++ b/backend/wdkj-server/src/routes/aiChat.js @@ -0,0 +1,240 @@ +const express = require('express') +const router = express.Router() +const { AIChatQuota, AIModel } = require('../models') +const ApiResponse = require('../utils/response') +const { userAuthMiddleware } = require('../middleware/auth') +const logger = require('../utils/logger') + +/** + * 获取用户AI问答配额 + * GET /api/ai-chat/quota + */ +router.get('/quota', userAuthMiddleware, async (req, res) => { + try { + const openid = req.openid || req.user?.openid + + if (!openid) { + return ApiResponse.error(res, '无法获取用户标识', 400) + } + + let quota = await AIChatQuota.findOne({ openid }) + + if (!quota) { + // 创建新的配额记录,默认5次免费 + quota = new AIChatQuota({ openid }) + await quota.save() + } + + // 检查是否需要重置每日使用次数 + quota.resetDailyUsed() + await quota.save() + + return ApiResponse.success(res, { + freeQuota: quota.freeQuota, + sharedQuota: quota.sharedQuota, + usedQuota: quota.usedQuota, + remainingQuota: quota.getRemainingQuota(), + dailyUsed: quota.dailyUsed + }) + + } catch (error) { + logger.error('获取AI问答配额失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * AI问答接口 + * POST /api/ai-chat/ask + */ +router.post('/ask', userAuthMiddleware, async (req, res) => { + try { + const openid = req.openid || req.user?.openid + const { question, dimension = 'general' } = req.body + + if (!openid) { + return ApiResponse.error(res, '无法获取用户标识', 400) + } + + if (!question || question.trim().length === 0) { + return ApiResponse.error(res, '请输入问题', 400) + } + + // 检查用户配额 + let quota = await AIChatQuota.findOne({ openid }) + + if (!quota) { + quota = new AIChatQuota({ openid }) + await quota.save() + } + + if (!quota.hasQuota()) { + return ApiResponse.error(res, '问答次数已用完,分享好友可获得更多次数', 403) + } + + // 获取当前使用的AI模型配置 + const aiModel = await AIModel.getCurrentModel() + + let apiUrl, apiKey, modelId, modelConfig + + if (aiModel) { + // 使用数据库配置的模型 + apiUrl = aiModel.apiUrl + apiKey = aiModel.apiKey + modelId = aiModel.modelId + modelConfig = aiModel.config + } else { + // 使用环境变量配置的模型 + apiUrl = process.env.OPENAI_API_URL || 'https://api.openai.com/v1/chat/completions' + apiKey = process.env.OPENAI_API_KEY + modelId = process.env.OPENAI_MODEL || 'gpt-3.5-turbo' + modelConfig = { temperature: 0.7, maxTokens: 800, topP: 1 } + } + + // 检查API配置 + if (!apiKey) { + logger.error('AI API Key未配置') + return ApiResponse.error(res, 'AI服务暂时不可用', 500) + } + + // 构建系统提示词 + const systemPrompt = getSystemPromptByDimension(dimension) + + // 调用AI API + const response = await fetch(apiUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${apiKey}` + }, + body: JSON.stringify({ + model: modelId, + messages: [ + { role: 'system', content: systemPrompt }, + { role: 'user', content: question } + ], + temperature: modelConfig.temperature || 0.7, + max_tokens: modelConfig.maxTokens || 800, + top_p: modelConfig.topP || 1 + }) + }) + + if (!response.ok) { + const errorData = await response.json() + logger.error('OpenAI API错误:', errorData) + return ApiResponse.error(res, 'AI服务响应异常', 500) + } + + const data = await response.json() + const answer = data.choices[0].message.content + + // 扣除一次使用次数 + await quota.useQuota() + + return ApiResponse.success(res, { + answer, + remainingQuota: quota.getRemainingQuota() + }) + + } catch (error) { + logger.error('AI问答失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 分享获得问答次数 + * POST /api/ai-chat/share-gain + */ +router.post('/share-gain', userAuthMiddleware, async (req, res) => { + try { + const openid = req.openid || req.user?.openid + + if (!openid) { + return ApiResponse.error(res, '无法获取用户标识', 400) + } + + let quota = await AIChatQuota.findOne({ openid }) + + if (!quota) { + quota = new AIChatQuota({ openid }) + } + + // 增加5次分享获得的次数 + const remainingQuota = await quota.addQuotaByShare(5) + + return ApiResponse.success(res, { + message: '分享成功,获得5次问答机会', + gainedQuota: 5, + remainingQuota + }) + + } catch (error) { + logger.error('分享获得次数失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 根据维度获取系统提示词 + */ +function getSystemPromptByDimension(dimension) { + const prompts = { + dim1: `你是"宇之然AI维度"的AI助手,专门解答关于 AI 起源和历史的问题。 +请用通俗易懂的语言解释人工智能发展史上的重要事件、人物和里程碑,如: +- 图灵测试(1950)与艾伦·图灵的贡献 +- 达特茅斯会议(1956)——AI 的诞生 +- 深蓝 vs 卡斯帕罗夫(1997) +- AlphaGo 与深度学习革命(2016) +- GPT 系列与大模型时代的开启 +回答要简洁有趣,适合普通用户理解。`, + + dim2: `你是"宇之然AI维度"的AI助手,专门解答关于 AI 技术发展的问题。 +请用通俗易懂的语言解释人工智能核心技术演进,如: +- 机器学习与深度学习的基本原理 +- Transformer 架构与自注意力机制 +- 大规模预训练模型(GPT、BERT等) +- 多模态 AI、MoE 架构 +- Agent 智能体技术 +回答要简洁有趣,适合普通用户理解。`, + + dim3: `你是"宇之然AI维度"的AI助手,专门解答关于当前 AI 行业格局的问题。 +请用通俗易懂的语言解释当前 AI 领域的最新动态,如: +- 各大模型公司(OpenAI、Google、DeepSeek、Anthropic、Meta)的竞争格局 +- 开源 vs 闭源模型的生态对比 +- 具身智能与世界模型的最新进展 +- AI 应用在各行业的落地情况 +- Agent 智能体的发展现状 +回答要简洁有趣,适合普通用户理解。`, + + dim4: `你是"宇之然AI维度"的AI助手,专门解答关于 AI 学习和工具使用的问题。 +请用通俗易懂的语言解释 AI 学习路线和工具使用方法,如: +- 提示词工程(Prompt Engineering)入门 +- 检索增强生成(RAG)的原理与实践 +- 模型微调(Fine-tuning)基础 +- 主流 AI 开发框架(LangChain、Dify 等) +- 常用 AI 工具推荐与使用技巧 +回答要简洁有趣,适合普通用户理解。`, + + dim5: `你是"宇之然AI维度"的AI助手,专门解答关于 AI 趋势和前沿动态的问题。 +请用通俗易懂的语言介绍 AI 领域的最新趋势,如: +- 最新 AI 产品发布动态 +- 前沿论文与研究方向 +- 行业趋势分析与预测 +- AI 政策法规与伦理讨论 +回答要简洁有趣,适合普通用户理解。`, + + general: `你是"宇之然AI维度"的AI助手,致力于帮助用户系统化理解人工智能。 +你可以回答关于 AI 的任何问题,包括但不限于: +- AI 历史与起源 +- 核心技术原理 +- 当前行业格局 +- 学习路线与工具使用 +- 最新趋势与前沿动态 +你的回答要简洁、通俗易懂,适合普通用户理解。如果用户问的是某个具体维度的知识,可以引导用户去对应的维度模块深入学习。` + } + + return prompts[dimension] || prompts.general +} + +module.exports = router diff --git a/backend/wdkj-server/src/routes/aiModel.js b/backend/wdkj-server/src/routes/aiModel.js new file mode 100755 index 0000000..f33d550 --- /dev/null +++ b/backend/wdkj-server/src/routes/aiModel.js @@ -0,0 +1,351 @@ +const express = require('express') +const router = express.Router() +const { AIModel } = require('../models') +const { authMiddleware, requirePermission } = require('../middleware/auth') +const ApiResponse = require('../utils/response') +const logger = require('../utils/logger') + +// 所有路由都需要认证 +router.use(authMiddleware) + +/** + * 获取当前使用的模型(公开接口,用于AI问答) + * GET /api/ai-model/current + */ +router.get('/current', async (req, res) => { + try { + const model = await AIModel.getCurrentModel() + + if (!model) { + // 如果没有配置模型,返回环境变量中的默认配置 + return ApiResponse.success(res, { + name: '默认模型', + modelId: process.env.OPENAI_MODEL || 'gpt-3.5-turbo', + apiUrl: process.env.OPENAI_API_URL || 'https://api.openai.com/v1/chat/completions', + apiKey: process.env.OPENAI_API_KEY || '', + config: { + temperature: 0.7, + maxTokens: 800, + topP: 1 + } + }) + } + + // 返回模型信息(不返回API Key) + return ApiResponse.success(res, { + name: model.name, + modelId: model.modelId, + config: model.config + }) + + } catch (error) { + logger.error('获取当前模型失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 获取模型列表(管理后台) + * GET /api/ai-model/list + */ +router.get('/list', requirePermission('dashboard:read'), async (req, res) => { + try { + const { page = 1, pageSize = 10, keyword = '' } = req.query + + const query = keyword + ? { $or: [ + { name: { $regex: keyword, $options: 'i' } }, + { modelId: { $regex: keyword, $options: 'i' } } + ]} + : {} + + const [list, total] = await Promise.all([ + AIModel.find(query) + .sort({ isDefault: -1, priority: 1, createdAt: -1 }) + .skip((page - 1) * pageSize) + .limit(parseInt(pageSize)), + AIModel.countDocuments(query) + ]) + + return ApiResponse.success(res, { + list, + total, + page: parseInt(page), + pageSize: parseInt(pageSize) + }) + + } catch (error) { + logger.error('获取模型列表失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 获取所有启用的模型(下拉选择用) + * GET /api/ai-model/active + */ +router.get('/active', requirePermission('dashboard:read'), async (req, res) => { + try { + const models = await AIModel.getActiveModels() + + return ApiResponse.success(res, { + list: models.map(m => ({ + _id: m._id, + name: m.name, + modelId: m.modelId, + isDefault: m.isDefault + })) + }) + + } catch (error) { + logger.error('获取启用模型失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 获取模型详情 + * GET /api/ai-model/:id + */ +router.get('/:id', requirePermission('dashboard:read'), async (req, res) => { + try { + const { id } = req.params + + const model = await AIModel.findById(id) + + if (!model) { + return ApiResponse.notFound(res, '模型不存在') + } + + return ApiResponse.success(res, { model }) + + } catch (error) { + logger.error('获取模型详情失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 创建模型 + * POST /api/ai-model + */ +router.post('/', requirePermission('settings:write'), async (req, res) => { + try { + const { name, modelId, apiUrl, apiKey, description, config, priority } = req.body + + // 验证必填字段 + if (!name || !modelId || !apiUrl || !apiKey) { + return ApiResponse.error(res, '请填写所有必填字段', 400) + } + + // 检查modelId是否已存在 + const existingModel = await AIModel.findOne({ modelId }) + if (existingModel) { + return ApiResponse.error(res, '该模型ID已存在', 400) + } + + const model = new AIModel({ + name, + modelId, + apiUrl, + apiKey, + description, + config, + priority + }) + + await model.save() + + logger.info('创建AI模型:', { name, modelId, admin: req.admin.username }) + + return ApiResponse.success(res, { + message: '创建成功', + model: { + _id: model._id, + name: model.name, + modelId: model.modelId, + isDefault: model.isDefault, + isActive: model.isActive + } + }) + + } catch (error) { + logger.error('创建模型失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 更新模型 + * PUT /api/ai-model/:id + */ +router.put('/:id', requirePermission('settings:write'), async (req, res) => { + try { + const { id } = req.params + const { name, modelId, apiUrl, apiKey, description, isActive, isDefault, config, priority } = req.body + + const model = await AIModel.findById(id) + + if (!model) { + return ApiResponse.notFound(res, '模型不存在') + } + + // 如果修改了modelId,检查是否与其他模型冲突 + if (modelId && modelId !== model.modelId) { + const existingModel = await AIModel.findOne({ modelId, _id: { $ne: id } }) + if (existingModel) { + return ApiResponse.error(res, '该模型ID已存在', 400) + } + } + + // 更新字段 + if (name) model.name = name + if (modelId) model.modelId = modelId + if (apiUrl) model.apiUrl = apiUrl + if (apiKey) model.apiKey = apiKey + if (description !== undefined) model.description = description + if (isActive !== undefined) model.isActive = isActive + if (isDefault !== undefined) model.isDefault = isDefault + if (config) model.config = { ...model.config, ...config } + if (priority !== undefined) model.priority = priority + + await model.save() + + logger.info('更新AI模型:', { id, name: model.name, admin: req.admin.username }) + + return ApiResponse.success(res, { + message: '更新成功', + model: { + _id: model._id, + name: model.name, + modelId: model.modelId, + isDefault: model.isDefault, + isActive: model.isActive + } + }) + + } catch (error) { + logger.error('更新模型失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 删除模型 + * DELETE /api/ai-model/:id + */ +router.delete('/:id', requirePermission('settings:write'), async (req, res) => { + try { + const { id } = req.params + + const model = await AIModel.findById(id) + + if (!model) { + return ApiResponse.notFound(res, '模型不存在') + } + + // 检查是否是唯一的默认模型 + if (model.isDefault) { + const defaultCount = await AIModel.countDocuments({ isDefault: true }) + if (defaultCount <= 1) { + return ApiResponse.error(res, '不能删除唯一的默认模型,请先设置其他默认模型', 400) + } + } + + await AIModel.findByIdAndDelete(id) + + logger.info('删除AI模型:', { id, name: model.name, admin: req.admin.username }) + + return ApiResponse.success(res, { message: '删除成功' }) + + } catch (error) { + logger.error('删除模型失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 设置默认模型 + * POST /api/ai-model/:id/set-default + */ +router.post('/:id/set-default', requirePermission('settings:write'), async (req, res) => { + try { + const { id } = req.params + + const model = await AIModel.findById(id) + + if (!model) { + return ApiResponse.notFound(res, '模型不存在') + } + + if (!model.isActive) { + return ApiResponse.error(res, '不能将禁用模型设为默认', 400) + } + + model.isDefault = true + await model.save() + + logger.info('设置默认AI模型:', { id, name: model.name, admin: req.admin.username }) + + return ApiResponse.success(res, { + message: '设置默认模型成功', + model: { + _id: model._id, + name: model.name, + modelId: model.modelId, + isDefault: true + } + }) + + } catch (error) { + logger.error('设置默认模型失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 初始化默认模型(从环境变量) + * POST /api/ai-model/init-default + */ +router.post('/init-default', requirePermission('settings:write'), async (req, res) => { + try { + // 检查是否已有模型 + const existingCount = await AIModel.countDocuments() + + if (existingCount > 0) { + return ApiResponse.error(res, '已有模型配置,如需重新初始化请先删除现有模型', 400) + } + + // 从环境变量创建默认模型 + const defaultModel = new AIModel({ + name: '默认模型', + modelId: process.env.OPENAI_MODEL || 'gpt-3.5-turbo', + apiUrl: process.env.OPENAI_API_URL || 'https://api.openai.com/v1/chat/completions', + apiKey: process.env.OPENAI_API_KEY || '', + description: '从环境变量初始化的默认模型', + isDefault: true, + isActive: true, + priority: 0 + }) + + await defaultModel.save() + + logger.info('初始化默认AI模型:', { admin: req.admin.username }) + + return ApiResponse.success(res, { + message: '初始化默认模型成功', + model: { + _id: defaultModel._id, + name: defaultModel.name, + modelId: defaultModel.modelId, + isDefault: true + } + }) + + } catch (error) { + logger.error('初始化默认模型失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +module.exports = router diff --git a/backend/wdkj-server/src/routes/auth.js b/backend/wdkj-server/src/routes/auth.js new file mode 100755 index 0000000..ec001e1 --- /dev/null +++ b/backend/wdkj-server/src/routes/auth.js @@ -0,0 +1,492 @@ +const express = require('express') +const router = express.Router() +const jwt = require('jsonwebtoken') +const { User, Admin, AdminLog, LoginLog } = require('../models') +const ApiResponse = require('../utils/response') +const weChatService = require('../utils/weixin') +const logger = require('../utils/logger') +const axios = require('axios') + +/** + * 获取客户端IP + */ +const getClientIp = (req) => { + return req.headers['x-forwarded-for'] || + req.headers['x-real-ip'] || + req.connection.remoteAddress || + req.socket.remoteAddress || + '0.0.0.0' +} + +/** + * 解析User-Agent + */ +const parseUserAgent = (userAgent) => { + if (!userAgent) return { device: '', os: '', browser: '' } + + const device = /Mobile|Android|iPhone|iPad|iPod/i.test(userAgent) ? 'mobile' : 'desktop' + + let os = '' + if (/Windows/i.test(userAgent)) os = 'Windows' + else if (/Mac OS X/i.test(userAgent)) os = 'MacOS' + else if (/Android/i.test(userAgent)) os = 'Android' + else if (/iOS|iPhone|iPad|iPod/i.test(userAgent)) os = 'iOS' + else if (/Linux/i.test(userAgent)) os = 'Linux' + else os = 'Unknown' + + let browser = '' + if (/MicroMessenger/i.test(userAgent)) browser = 'WeChat' + else if (/Chrome/i.test(userAgent)) browser = 'Chrome' + else if (/Safari/i.test(userAgent)) browser = 'Safari' + else if (/Firefox/i.test(userAgent)) browser = 'Firefox' + else if (/Edge/i.test(userAgent)) browser = 'Edge' + else browser = 'Unknown' + + return { device, os, browser } +} + +/** + * 记录登录日志 + */ +const recordLoginLog = async (userId, openid, type, method, source, req, success = true, failReason = '') => { + try { + const ip = getClientIp(req) + const userAgent = req.headers['user-agent'] || '' + const { device, os, browser } = parseUserAgent(userAgent) + + await LoginLog.create({ + userId, + openid, + type, + method, + source, + ip, + userAgent, + device, + os, + browser, + success, + failReason + }) + } catch (error) { + logger.error('记录登录日志失败:', error) + } +} + +/** + * 小程序登录 + * POST /api/auth/login + */ +router.post('/login', async (req, res) => { + try { + const { code, userInfo, source = 'miniapp' } = req.body + + // 方式1: 通过 code 登录(推荐) + let openid = null + if (code) { + const sessionData = await weChatService.code2Session(code) + openid = sessionData.openid + } + + // 方式2: 开发环境直接使用 openid (已废弃,仅用于本地调试) + if (!openid && process.env.NODE_ENV === 'development' && req.body.openid) { + openid = req.body.openid + } + + if (!openid) { + return ApiResponse.error(res, '无法获取用户标识', 400) + } + + // 查找或创建用户 + let user = await User.findOne({ openid }) + let isNewUser = false + + if (!user) { + // 新用户 + user = new User({ + openid, + nickName: userInfo?.nickName || '星辰旅行者', + avatarUrl: userInfo?.avatarUrl || '', + unlockedDims: [1] + }) + isNewUser = true + logger.info(`新用户注册: ${openid}`) + } else { + // 老用户,更新登录时间 + user.lastLoginAt = new Date() + if (userInfo?.nickName) user.nickName = userInfo.nickName + if (userInfo?.avatarUrl) user.avatarUrl = userInfo.avatarUrl + } + + await user.save() + + // 记录登录日志 + await recordLoginLog( + user._id, + openid, + isNewUser ? 'register' : 'login', + 'wechat_miniapp', + source, + req, + true + ) + + // 生成 JWT token(可选,小程序通常不需要) + const token = jwt.sign( + { openid, userId: user._id }, + process.env.JWT_SECRET, + { expiresIn: process.env.JWT_EXPIRE } + ) + + return ApiResponse.success(res, { + token, + openid, + isNewUser, + userInfo: { + nickName: user.nickName, + avatarUrl: user.avatarUrl + }, + firstPlayDate: user.createdAt + }, isNewUser ? '注册成功' : '登录成功') + + } catch (error) { + logger.error('登录失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * WebView 静默登录 + * POST /api/auth/webview-login + * 用于微信小程序 webview 内嵌 H5 的自动登录 + */ +router.post('/webview-login', async (req, res) => { + try { + const { code, userInfo } = req.body + + if (!code) { + return ApiResponse.error(res, '缺少登录凭证', 400) + } + + // 通过 code 获取 openid + let openid = null + try { + const sessionData = await weChatService.code2Session(code) + openid = sessionData.openid + } catch (error) { + logger.error('WebView登录获取session失败:', error) + return ApiResponse.error(res, '登录凭证无效', 401) + } + + if (!openid) { + return ApiResponse.error(res, '无法获取用户标识', 400) + } + + // 查找或创建用户 + let user = await User.findOne({ openid }) + let isNewUser = false + + if (!user) { + // 新用户 - 使用微信提供的信息创建账户 + user = new User({ + openid, + nickName: userInfo?.nickName || '星辰旅行者', + avatarUrl: userInfo?.avatarUrl || '', + unlockedDims: [1] + }) + isNewUser = true + logger.info(`WebView新用户注册: ${openid}`) + } else { + // 老用户,更新登录时间和信息 + user.lastLoginAt = new Date() + if (userInfo?.nickName) user.nickName = userInfo.nickName + if (userInfo?.avatarUrl) user.avatarUrl = userInfo.avatarUrl + logger.info(`WebView老用户登录: ${openid}`) + } + + await user.save() + + // 记录登录日志 + await recordLoginLog( + user._id, + openid, + isNewUser ? 'register' : 'auto_login', + 'wechat_webview', + 'webview', + req, + true + ) + + // 生成 JWT token + const token = jwt.sign( + { openid, userId: user._id }, + process.env.JWT_SECRET, + { expiresIn: process.env.JWT_EXPIRE } + ) + + return ApiResponse.success(res, { + token, + openid, + isNewUser, + userInfo: { + nickName: user.nickName, + avatarUrl: user.avatarUrl + }, + firstPlayDate: user.createdAt + }, isNewUser ? '注册成功' : '自动登录成功') + + } catch (error) { + logger.error('WebView登录失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 管理员登录 + * POST /api/auth/admin/login + */ +router.post('/admin/login', async (req, res) => { + try { + const { username, password } = req.body + + // 查找管理员 + const admin = await Admin.findOne({ username }) + if (!admin) { + return ApiResponse.error(res, '用户名或密码错误', 401) + } + + // 验证密码 + const isMatch = await admin.validatePassword(password) + if (!isMatch) { + return ApiResponse.error(res, '用户名或密码错误', 401) + } + + // 检查状态 + if (admin.status !== 'active') { + return ApiResponse.error(res, '账户已禁用', 403) + } + + // 更新登录信息 + admin.lastLogin = new Date() + admin.lastLoginIp = req.ip + admin.loginCount += 1 + await admin.save() + + // 记录日志 + await AdminLog.create({ + adminId: admin._id, + adminName: admin.username, + action: 'login', + resource: 'system', + ip: req.ip, + userAgent: req.get('User-Agent') + }) + + // 生成 JWT token + const token = jwt.sign( + { adminId: admin._id, role: admin.role }, + process.env.JWT_SECRET, + { expiresIn: process.env.JWT_EXPIRE } + ) + + // 获取权限 + const permissions = Admin.getPermissionsByRole(admin.role) + + return ApiResponse.success(res, { + token, + adminId: admin._id, + username: admin.username, + role: admin.role, + permissions + }, '登录成功') + + } catch (error) { + logger.error('管理员登录失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * H5环境用户名密码登录 + * POST /api/auth/user/login + */ +router.post('/user/login', async (req, res) => { + try { + const { username, password } = req.body + + // 查找用户(显式包含密码字段) + const user = await User.findOne({ username }).select('+password') + if (!user) { + return ApiResponse.error(res, '用户名或密码错误', 401) + } + + // 验证密码 + const isMatch = await user.validatePassword(password) + if (!isMatch) { + return ApiResponse.error(res, '用户名或密码错误', 401) + } + + // 更新登录时间 + user.lastLoginAt = new Date() + await user.save() + + // 生成 JWT token + const token = jwt.sign( + { userId: user._id, openid: user.openid }, + process.env.JWT_SECRET, + { expiresIn: process.env.JWT_EXPIRE } + ) + + return ApiResponse.success(res, { + token, + userId: user._id, + openid: user.openid, + userInfo: { + nickName: user.nickName, + avatarUrl: user.avatarUrl + }, + firstPlayDate: user.createdAt + }, '登录成功') + + } catch (error) { + logger.error('用户登录失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * H5环境用户注册 + * POST /api/auth/user/register + */ +router.post('/user/register', async (req, res) => { + try { + const { username, password, nickName, avatarUrl } = req.body + + // 检查用户名是否已存在 + const existingUser = await User.findOne({ username }) + if (existingUser) { + return ApiResponse.error(res, '用户名已存在', 400) + } + + // 创建新用户 + const user = new User({ + username, + password, + nickName: nickName || '星辰旅行者', + avatarUrl: avatarUrl || '', + unlockedDims: [1] + }) + await user.save() + + logger.info(`新用户注册: ${username}`) + + // 生成 JWT token + const token = jwt.sign( + { userId: user._id }, + process.env.JWT_SECRET, + { expiresIn: process.env.JWT_EXPIRE } + ) + + return ApiResponse.success(res, { + token, + userId: user._id, + userInfo: { + nickName: user.nickName, + avatarUrl: user.avatarUrl + } + }, '注册成功') + + } catch (error) { + logger.error('用户注册失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 获取当前用户信息 + * GET /api/auth/me + */ +router.get('/me', async (req, res) => { + try { + const token = req.header('Authorization')?.replace('Bearer ', '') + + if (!token) { + return ApiResponse.unauthorized(res) + } + + const decoded = jwt.verify(token, process.env.JWT_SECRET) + const user = await User.findById(decoded.userId) + + if (!user) { + return ApiResponse.notFound(res, '用户不存在') + } + + return ApiResponse.success(res, user) + + } catch (error) { + logger.error('获取用户信息失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * WebView Token 交换接口 + * POST /api/auth/webview-token + * 用于小程序 WebView 内嵌 H5 时,通过 openid 获取登录 token + * 小程序在 URL 中传递 openid,H5 用这个接口换取 token + */ +router.post('/webview-token', async (req, res) => { + try { + const { openid } = req.body + + if (!openid) { + return ApiResponse.error(res, '缺少用户标识', 400) + } + + // 查找用户 + const user = await User.findOne({ openid }) + + if (!user) { + return ApiResponse.error(res, '用户不存在', 404) + } + + // 生成新的 JWT token + const token = jwt.sign( + { openid, userId: user._id }, + process.env.JWT_SECRET, + { expiresIn: process.env.JWT_EXPIRE } + ) + + // 更新最后登录时间 + user.lastLoginAt = new Date() + await user.save() + + // 记录登录日志 + await recordLoginLog( + user._id, + openid, + 'webview_auto_login', + 'wechat_webview', + 'webview_h5', + req, + true + ) + + logger.info(`WebView Token交换成功: ${openid}`) + + return ApiResponse.success(res, { + token, + openid, + userInfo: { + nickName: user.nickName, + avatarUrl: user.avatarUrl + } + }, '登录成功') + + } catch (error) { + logger.error('WebView Token交换失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +module.exports = router diff --git a/backend/wdkj-server/src/routes/bgm.js b/backend/wdkj-server/src/routes/bgm.js new file mode 100755 index 0000000..17fee20 --- /dev/null +++ b/backend/wdkj-server/src/routes/bgm.js @@ -0,0 +1,201 @@ +const express = require('express') +const router = express.Router() +const { BGM } = require('../models') +const ApiResponse = require('../utils/response') +const { authMiddleware } = require('../middleware/auth') +const logger = require('../utils/logger') + +/** + * 获取指定维度的随机BGM(公开接口,移动端调用) + * GET /api/bgm/random/:dimension + */ +router.get('/random/:dimension', async (req, res) => { + try { + const dimension = parseInt(req.params.dimension) + + if (![1, 2, 3, 4, 5].includes(dimension)) { + return ApiResponse.badRequest(res, '无效的维度参数') + } + + // 查询该维度下所有激活的BGM + const bgms = await BGM.find({ + dimension, + isActive: true + }).sort({ sortOrder: 1 }) + + if (!bgms || bgms.length === 0) { + return ApiResponse.success(res, null) + } + + // 随机选择一个 + const randomIndex = Math.floor(Math.random() * bgms.length) + const selectedBGM = bgms[randomIndex] + + return ApiResponse.success(res, { + id: selectedBGM._id, + name: selectedBGM.name, + url: selectedBGM.url, + duration: selectedBGM.duration, + loop: selectedBGM.loop, + volume: selectedBGM.volume, + dimension: selectedBGM.dimension + }) + } catch (error) { + logger.error('获取随机BGM失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 获取所有BGM列表(管理员) + * GET /api/admin/bgm/list + */ +router.get('/list', authMiddleware, async (req, res) => { + try { + const { dimension, page = 1, limit = 20 } = req.query + + const query = {} + if (dimension && [1, 2, 3, 4, 5].includes(parseInt(dimension))) { + query.dimension = parseInt(dimension) + } + + const pageNum = parseInt(page) + const limitNum = parseInt(limit) + const skip = (pageNum - 1) * limitNum + + const [bgms, total] = await Promise.all([ + BGM.find(query) + .sort({ dimension: 1, sortOrder: 1, createdAt: -1 }) + .skip(skip) + .limit(limitNum), + BGM.countDocuments(query) + ]) + + return ApiResponse.success(res, { + list: bgms, + total, + page: pageNum, + limit: limitNum, + totalPages: Math.ceil(total / limitNum) + }) + } catch (error) { + logger.error('获取BGM列表失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 创建BGM(管理员) + * POST /api/admin/bgm/create + */ +router.post('/create', authMiddleware, async (req, res) => { + try { + const { name, dimension, url, duration, loop, volume, sortOrder, description } = req.body + + if (!name || !dimension || !url) { + return ApiResponse.badRequest(res, '名称、维度和URL为必填项') + } + + if (![1, 2, 3, 4, 5].includes(parseInt(dimension))) { + return ApiResponse.badRequest(res, '无效的维度参数') + } + + const bgm = new BGM({ + name, + dimension: parseInt(dimension), + url, + duration: duration || 0, + loop: loop !== undefined ? loop : true, + volume: volume !== undefined ? volume : 0.5, + sortOrder: sortOrder || 0, + description: description || '' + }) + + await bgm.save() + + logger.info(`管理员创建了BGM: ${name} (维度${dimension})`) + return ApiResponse.success(res, { message: 'BGM创建成功', data: bgm }) + } catch (error) { + logger.error('创建BGM失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 更新BGM(管理员) + * PUT /api/admin/bgm/update/:id + */ +router.put('/update/:id', authMiddleware, async (req, res) => { + try { + const { id } = req.params + const updates = req.body + + const bgm = await BGM.findByIdAndUpdate( + id, + { $set: updates }, + { new: true, runValidators: true } + ) + + if (!bgm) { + return ApiResponse.notFound(res, 'BGM不存在') + } + + logger.info(`管理员更新了BGM: ${bgm.name}`) + return ApiResponse.success(res, { message: 'BGM更新成功', data: bgm }) + } catch (error) { + logger.error('更新BGM失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 删除BGM(管理员) + * DELETE /api/admin/bgm/delete/:id + */ +router.delete('/delete/:id', authMiddleware, async (req, res) => { + try { + const { id } = req.params + + const bgm = await BGM.findByIdAndDelete(id) + + if (!bgm) { + return ApiResponse.notFound(res, 'BGM不存在') + } + + logger.info(`管理员删除了BGM: ${bgm.name}`) + return ApiResponse.success(res, { message: 'BGM删除成功' }) + } catch (error) { + logger.error('删除BGM失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 切换BGM激活状态(管理员) + * PUT /api/admin/bgm/toggle/:id + */ +router.put('/toggle/:id', authMiddleware, async (req, res) => { + try { + const { id } = req.params + + const bgm = await BGM.findById(id) + + if (!bgm) { + return ApiResponse.notFound(res, 'BGM不存在') + } + + bgm.isActive = !bgm.isActive + await bgm.save() + + logger.info(`管理员${bgm.isActive ? '激活' : '禁用'}了BGM: ${bgm.name}`) + return ApiResponse.success(res, { + message: `BGM已${bgm.isActive ? '激活' : '禁用'}`, + data: bgm + }) + } catch (error) { + logger.error('切换BGM状态失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +module.exports = router diff --git a/backend/wdkj-server/src/routes/feedback.js b/backend/wdkj-server/src/routes/feedback.js new file mode 100755 index 0000000..e5ebc54 --- /dev/null +++ b/backend/wdkj-server/src/routes/feedback.js @@ -0,0 +1,228 @@ +const express = require('express') +const router = express.Router() +const { Feedback, User } = require('../models') +const ApiResponse = require('../utils/response') +const { userAuthMiddleware } = require('../middleware/auth') +const logger = require('../utils/logger') + +/** + * 获取客户端IP + */ +const getClientIp = (req) => { + return req.headers['x-forwarded-for'] || + req.headers['x-real-ip'] || + req.connection.remoteAddress || + req.socket.remoteAddress || + '0.0.0.0' +} + +/** + * 提交反馈 + * POST /api/feedback + */ +router.post('/', async (req, res) => { + try { + const { + type, + title, + content, + images, + relatedPage, + deviceInfo, + userContact + } = req.body + + // 验证必填字段 + if (!type || !title || !content) { + return ApiResponse.error(res, '请填写完整的反馈信息', 400) + } + + // 获取用户信息(如果已登录) + let userId = null + let openid = null + let userName = '' + + try { + const token = req.headers.authorization?.replace('Bearer ', '') + if (token) { + const jwt = require('jsonwebtoken') + const decoded = jwt.verify(token, process.env.JWT_SECRET) + userId = decoded.userId + openid = decoded.openid + + // 获取用户信息 + const user = await User.findById(userId) + if (user) { + userName = user.nickName || '' + } + } + } catch (error) { + // Token 验证失败,继续以匿名方式提交 + logger.warn('反馈提交时Token验证失败:', error.message) + } + + // 创建反馈记录 + const feedback = new Feedback({ + userId, + openid, + userName, + userContact: userContact || '', + type, + title, + content, + images: images || [], + relatedPage: relatedPage || '', + deviceInfo: deviceInfo || {}, + networkInfo: { + ip: getClientIp(req) + }, + status: 'pending', + priority: 'normal' + }) + + await feedback.save() + + logger.info(`新反馈提交: ${feedback._id}, 类型: ${type}`) + + return ApiResponse.success(res, { + feedbackId: feedback._id + }, '反馈提交成功,我们会尽快处理') + + } catch (error) { + logger.error('提交反馈失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 获取我的反馈列表 + * GET /api/feedback/my + */ +router.get('/my', userAuthMiddleware, async (req, res) => { + try { + const user = req.user + const { page = 1, limit = 20 } = req.query + + const query = { userId: user._id } + + const total = await Feedback.countDocuments(query) + const feedbacks = await Feedback.find(query) + .sort({ createdAt: -1 }) + .skip((parseInt(page) - 1) * parseInt(limit)) + .limit(parseInt(limit)) + .select('-deviceInfo -networkInfo') + .lean() + + // 格式化返回数据 + const formattedFeedbacks = feedbacks.map(fb => ({ + id: fb._id, + type: fb.type, + title: fb.title, + content: fb.content.substring(0, 100) + (fb.content.length > 100 ? '...' : ''), + images: fb.images, + status: fb.status, + priority: fb.priority, + result: fb.result, + createdAt: fb.createdAt, + updatedAt: fb.updatedAt + })) + + return ApiResponse.paginated(res, formattedFeedbacks, { + page: parseInt(page), + limit: parseInt(limit), + total + }) + + } catch (error) { + logger.error('获取我的反馈失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 获取反馈详情 + * GET /api/feedback/:id + */ +router.get('/:id', userAuthMiddleware, async (req, res) => { + try { + const user = req.user + const feedback = await Feedback.findById(req.params.id).lean() + + if (!feedback) { + return ApiResponse.notFound(res, '反馈不存在') + } + + // 检查权限(只能查看自己的反馈) + if (feedback.userId?.toString() !== user._id.toString()) { + return ApiResponse.error(res, '无权查看此反馈', 403) + } + + return ApiResponse.success(res, { + id: feedback._id, + type: feedback.type, + title: feedback.title, + content: feedback.content, + images: feedback.images, + relatedPage: feedback.relatedPage, + status: feedback.status, + priority: feedback.priority, + processLog: feedback.processLog, + result: feedback.result, + handler: feedback.handler, + handledAt: feedback.handledAt, + rating: feedback.rating, + createdAt: feedback.createdAt, + updatedAt: feedback.updatedAt + }) + + } catch (error) { + logger.error('获取反馈详情失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 对处理结果评分 + * POST /api/feedback/:id/rate + */ +router.post('/:id/rate', userAuthMiddleware, async (req, res) => { + try { + const user = req.user + const { rating, remark } = req.body + + if (!rating || rating < 1 || rating > 5) { + return ApiResponse.error(res, '评分必须在1-5之间', 400) + } + + const feedback = await Feedback.findById(req.params.id) + + if (!feedback) { + return ApiResponse.notFound(res, '反馈不存在') + } + + // 检查权限 + if (feedback.userId?.toString() !== user._id.toString()) { + return ApiResponse.error(res, '无权操作此反馈', 403) + } + + // 只能对已解决的反馈评分 + if (feedback.status !== 'resolved' && feedback.status !== 'closed') { + return ApiResponse.error(res, '只能对已处理的反馈进行评分', 400) + } + + feedback.rating = rating + if (remark) { + feedback.userRemark = remark + } + + await feedback.save() + + return ApiResponse.success(res, {}, '评分成功') + + } catch (error) { + logger.error('反馈评分失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +module.exports = router diff --git a/backend/wdkj-server/src/routes/gallery.js b/backend/wdkj-server/src/routes/gallery.js new file mode 100755 index 0000000..ffbdcb2 --- /dev/null +++ b/backend/wdkj-server/src/routes/gallery.js @@ -0,0 +1,209 @@ +const express = require('express') +const router = express.Router() +const { Gallery } = require('../models') +const ApiResponse = require('../utils/response') +const { userAuthMiddleware, optionalAuthMiddleware } = require('../middleware/auth') +const logger = require('../utils/logger') + +// 应用用户认证中间件到需要登录的路由 +// 注意:POST /api/gallery 需要认证 +router.post('/', userAuthMiddleware) +router.use('/like', userAuthMiddleware) +// 作品列表接口使用可选认证,这样未登录用户也能访问 +router.get('/works', optionalAuthMiddleware) + +/** + * 获取作品列表(兼容旧版API) + * GET /api/gallery/works + */ +router.get('/works', async (req, res) => { + try { + const { page = 1, limit = 12, filter = 'all' } = req.query + + const query = { status: 'approved' } + + // 处理筛选条件 + if (filter === 'dim2') { + query.dim = 2 + } else if (filter === 'dim3') { + query.dim = 3 + } else if (filter === 'my') { + // 显示当前用户的作品,需要认证 + if (!req.user) { + return ApiResponse.error(res, '需要登录才能查看我的作品', 401) + } + query.openid = req.user.openid + } + + const total = await Gallery.countDocuments(query) + const works = await Gallery.find(query) + .sort({ createdAt: -1 }) + .skip((parseInt(page) - 1) * parseInt(limit)) + .limit(parseInt(limit)) + .lean() + + // 格式化返回数据 + const formattedWorks = works.map(work => ({ + _id: work._id, + fileURL: work.imageUrl || work.imageData, + authorAvatar: work.authorAvatar || '/static/images/default_avatar.png', + authorName: work.authorName || '星辰旅者', + likes: work.likeCount || 0, + views: work.viewCount || 0, + dim: work.dim || 2, + type: work.dim === 2 ? 'dim2' : 'dim3', + description: work.description || '', + createdAt: work.createdAt + })) + + return ApiResponse.success(res, { + list: formattedWorks, + total, + page: parseInt(page), + limit: parseInt(limit) + }) + + } catch (error) { + logger.error('获取作品列表失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 获取作品列表 + * GET /api/gallery + */ +router.get('/', async (req, res) => { + try { + const { page = 1, limit = 20, status = 'approved', dim } = req.query + + const query = { status } + if (dim) query.dim = parseInt(dim) + + const total = await Gallery.countDocuments(query) + const works = await Gallery.find(query) + .sort({ createdAt: -1 }) + .skip((parseInt(page) - 1) * parseInt(limit)) + .limit(parseInt(limit)) + .lean() + + return ApiResponse.paginated(res, works, { + page: parseInt(page), + limit: parseInt(limit), + total + }) + + } catch (error) { + logger.error('获取作品列表失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 上传作品 + * POST /api/gallery + */ +router.post('/', async (req, res) => { + try { + const user = req.user + const { title, description, imageData, imageUrl, tags, dim } = req.body + + if (!title || !dim) { + return ApiResponse.error(res, '标题和维度为必填项', 400) + } + + const work = new Gallery({ + openid: user.openid, + userId: user._id, + authorName: user.nickName, + authorAvatar: user.avatarUrl, + title, + description, + imageData, + imageUrl, + tags: tags || [], + dim, + status: 'approved' // 自动审核通过,用户可以立即看到自己的作品 + }) + + await work.save() + + return ApiResponse.success(res, work, '作品上传成功,等待审核') + + } catch (error) { + logger.error('上传作品失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 获取作品详情 + * GET /api/gallery/:id + */ +router.get('/:id', async (req, res) => { + try { + const work = await Gallery.findById(req.params.id) + + if (!work) { + return ApiResponse.notFound(res, '作品不存在') + } + + // 增加浏览量 + work.viewCount += 1 + await work.save() + + return ApiResponse.success(res, work) + + } catch (error) { + logger.error('获取作品详情失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 点赞作品(兼容移动端API) + * POST /api/gallery/like/:id + */ +router.post('/like/:id', async (req, res) => { + try { + const work = await Gallery.findById(req.params.id) + + if (!work) { + return ApiResponse.notFound(res, '作品不存在') + } + + work.likeCount += 1 + await work.save() + + return ApiResponse.success(res, { likeCount: work.likeCount }, '点赞成功') + + } catch (error) { + logger.error('点赞失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 点赞作品 + * POST /api/gallery/:id/like + */ +router.post('/:id/like', async (req, res) => { + try { + const work = await Gallery.findById(req.params.id) + + if (!work) { + return ApiResponse.notFound(res, '作品不存在') + } + + work.likeCount += 1 + await work.save() + + return ApiResponse.success(res, { likeCount: work.likeCount }, '点赞成功') + + } catch (error) { + logger.error('点赞失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +module.exports = router diff --git a/backend/wdkj-server/src/routes/index.js b/backend/wdkj-server/src/routes/index.js new file mode 100755 index 0000000..c771dea --- /dev/null +++ b/backend/wdkj-server/src/routes/index.js @@ -0,0 +1,57 @@ +const authRoutes = require('./auth') +const userRoutes = require('./user') +const paymentRoutes = require('./payment') +const galleryRoutes = require('./gallery') +const knowledgeRoutes = require('./knowledge') +const adminRoutes = require('./admin') +const shareRoutes = require('./share') +const bgmRoutes = require('./bgm') +const aiChatRoutes = require('./aiChat') +const aiModelRoutes = require('./aiModel') +const feedbackRoutes = require('./feedback') +const pinyinRoutes = require('./pinyin') +const trendRoutes = require('./trend') + +module.exports = (app) => { + // 认证路由 + app.use('/api/auth', authRoutes) + + // 用户路由 + app.use('/api/user', userRoutes) + + // 支付路由 + app.use('/api/payment', paymentRoutes) + + // 画廊路由 + app.use('/api/gallery', galleryRoutes) + + // 知识库路由 + app.use('/api/knowledge', knowledgeRoutes) + + // 反馈路由 + app.use('/api/feedback', feedbackRoutes) + + // 管理后台路由 + app.use('/api/admin', adminRoutes) + + // 分享记录路由 + app.use('/api/share', shareRoutes) + + // BGM音效路由(公开接口 + 管理员接口) + app.use('/api/bgm', bgmRoutes) + + // AI问答路由 + app.use('/api/ai-chat', aiChatRoutes) + + // AI模型配置路由 + app.use('/api/ai-model', aiModelRoutes) + + // 拼音探索路由 + app.use('/api/pinyin/contents', pinyinRoutes.contents) + app.use('/api/pinyin/progress', pinyinRoutes.progress) + app.use('/api/pinyin/games', pinyinRoutes.games) + app.use('/api/pinyin/achievements', pinyinRoutes.achievements) + + // AI趋势路由 + app.use('/api/trend', trendRoutes) + } diff --git a/backend/wdkj-server/src/routes/knowledge.js b/backend/wdkj-server/src/routes/knowledge.js new file mode 100755 index 0000000..3173f27 --- /dev/null +++ b/backend/wdkj-server/src/routes/knowledge.js @@ -0,0 +1,186 @@ +const express = require('express') +const router = express.Router() +const { Knowledge } = require('../models') +const ApiResponse = require('../utils/response') +const { userAuthMiddleware } = require('../middleware/auth') +const logger = require('../utils/logger') + +/** + * 获取知识列表 + * GET /api/knowledge + */ +router.get('/', async (req, res) => { + try { + const { page = 1, limit = 20, dim, category, status = 'approved' } = req.query + + const query = { status } + if (dim) query.dim = parseInt(dim) + if (category) query.category = category + + const total = await Knowledge.countDocuments(query) + const knowledge = await Knowledge.find(query) + .sort({ sortOrder: 1, createdAt: -1 }) + .skip((parseInt(page) - 1) * parseInt(limit)) + .limit(parseInt(limit)) + .lean() + + return ApiResponse.paginated(res, knowledge, { + page: parseInt(page), + limit: parseInt(limit), + total + }) + + } catch (error) { + logger.error('获取知识列表失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 获取用户收藏的知识列表 + * GET /api/knowledge/collected + */ +router.get('/collected', userAuthMiddleware, async (req, res) => { + try { + const user = req.user + + // 查找用户收藏的知识 + const collectedKnowledge = await Knowledge.find({ _id: { $in: user.collectedKnowledge } }) + .lean() + + // 格式化返回数据 + const formattedKnowledge = collectedKnowledge.map(item => ({ + id: item._id, + dimension: item.dim, + title: item.title, + description: item.content || item.description || '', + knowledgeId: item._id + })) + + return ApiResponse.success(res, formattedKnowledge) + + } catch (error) { + logger.error('获取收藏列表失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 删除收藏 + * DELETE /api/knowledge/collect/:id + */ +router.delete('/collect/:id', userAuthMiddleware, async (req, res) => { + try { + const knowledgeId = req.params.id + const user = req.user + + // 检查是否已经收藏 + if (!user.collectedKnowledge.includes(knowledgeId)) { + return ApiResponse.error(res, '未收藏该知识', 400) + } + + // 从用户的 collectedKnowledge 数组中移除 + user.collectedKnowledge = user.collectedKnowledge.filter(id => id.toString() !== knowledgeId.toString()) + await user.save() + + // 减少收藏计数 + const knowledge = await Knowledge.findById(knowledgeId) + if (knowledge) { + knowledge.collectionCount = Math.max(0, knowledge.collectionCount - 1) + await knowledge.save() + } + + return ApiResponse.success(res, {}, '删除收藏成功') + + } catch (error) { + logger.error('删除收藏失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 获取知识详情 + * GET /api/knowledge/:id + */ +router.get('/:id', async (req, res) => { + try { + const knowledge = await Knowledge.findById(req.params.id) + + if (!knowledge) { + return ApiResponse.notFound(res, '知识不存在') + } + + // 增加浏览量 + knowledge.viewCount += 1 + await knowledge.save() + + return ApiResponse.success(res, knowledge) + + } catch (error) { + logger.error('获取知识详情失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 收藏知识 + * POST /api/knowledge/:id/collect + */ +router.post('/:id/collect', userAuthMiddleware, async (req, res) => { + try { + const knowledge = await Knowledge.findById(req.params.id) + + if (!knowledge) { + return ApiResponse.notFound(res, '知识不存在') + } + + const user = req.user + + // 检查是否已经收藏 + if (user.collectedKnowledge.includes(knowledge._id)) { + return ApiResponse.error(res, '已经收藏过了', 400) + } + + // 将知识ID加入用户的 collectedKnowledge 数组 + user.collectedKnowledge.push(knowledge._id) + await user.save() + + // 增加收藏计数 + knowledge.collectionCount += 1 + await knowledge.save() + + return ApiResponse.success(res, { collectionCount: knowledge.collectionCount }, '收藏成功') + + } catch (error) { + logger.error('收藏失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 获取问答 + * GET /api/knowledge/:dim/questions + */ +router.get('/:dim/questions', async (req, res) => { + try { + const { dim } = req.params + const { limit = 10 } = req.query + + const questions = await Knowledge.find({ + dim: parseInt(dim), + category: 'question', + status: 'approved' + }) + .select('question options') + .limit(parseInt(limit)) + .lean() + + return ApiResponse.success(res, questions) + + } catch (error) { + logger.error('获取问答失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +module.exports = router diff --git a/backend/wdkj-server/src/routes/payment.js b/backend/wdkj-server/src/routes/payment.js new file mode 100755 index 0000000..56f2121 --- /dev/null +++ b/backend/wdkj-server/src/routes/payment.js @@ -0,0 +1,266 @@ +const express = require('express') +const router = express.Router() +const { User, Order, ShopItem } = require('../models') +const ApiResponse = require('../utils/response') +const weChatService = require('../utils/weixin') +const { userAuthMiddleware } = require('../middleware/auth') +const logger = require('../utils/logger') + +/** + * 获取商品列表(从数据库)- 公开接口 + * GET /api/payment/products + */ +router.get('/products', async (req, res) => { + try { + const items = await ShopItem.find({ status: 'active' }).sort({ sort: 1 }) + return ApiResponse.success(res, items) + } catch (error) { + logger.error('获取商品列表失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +// 应用用户认证中间件到需要保护的路由 +router.use(userAuthMiddleware) + +/** + * 创建订单 + * POST /api/payment/create-order + */ +router.post('/create-order', async (req, res) => { + try { + const user = req.user + const { productId } = req.body + + // 从数据库获取商品信息 + const product = await ShopItem.findOne({ itemId: productId, status: 'active' }) + if (!product) { + return ApiResponse.error(res, '无效商品 ID 或商品已下架', 400) + } + + // 检查是否已购买(针对皮肤类一次性商品) + if (product.type === 'skin' && user.ownedSkins?.includes(productId)) { + return ApiResponse.error(res, '已拥有该商品', 400) + } + + // 生成订单号 + const orderNo = weChatService.generateOrderNo('WD') + + // 创建订单记录 + const order = new Order({ + orderNo, + openid: user.openid, + userId: user._id, + productId, + productName: product.name, + amount: product.price, + status: 'pending' + }) + await order.save() + + // 调用微信支付统一下单 + const payParams = await weChatService.createPaymentOrder({ + openid: user.openid, + orderNo, + body: `宇之然-${product.name}`, + amount: product.price + }) + + return ApiResponse.success(res, { + orderNo, + productId, + productName: product.name, + amount: product.price, + payParams + }, '订单创建成功') + + } catch (error) { + logger.error('创建订单失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 支付回调 + * POST /api/payment/callback + */ +router.post('/callback', async (req, res) => { + try { + const data = req.body + + // 验证签名 + if (!weChatService.verifyCallbackSignature(data, data.sign)) { + logger.warn('支付签名验证失败') + return res.status(400).send('FAIL') + } + + const orderNo = data.out_trade_no + const transactionId = data.transaction_id + + // 查询订单 + const order = await Order.findOne({ orderNo }) + if (!order) { + logger.error(`订单不存在: ${orderNo}`) + return res.status(400).send('FAIL') + } + + // 已处理过 + if (order.status === 'paid' || order.status === 'completed') { + return res.status(200).send('SUCCESS') + } + + // 更新订单状态 + order.status = 'paid' + order.transactionId = transactionId + order.paidAt = new Date() + await order.save() + + // 解锁商品 + const user = await User.findOne({ openid: order.openid }) + if (user) { + const product = await ShopItem.findOne({ itemId: order.productId }) + + if (product) { + if (product.type === 'skin') { + if (!user.ownedSkins.includes(order.productId)) { + user.ownedSkins.push(order.productId) + } + } else if (product.type === 'noad') { + user.isAdFree = true + } else if (product.type === 'subscription') { + const expiry = new Date() + expiry.setDate(expiry.getDate() + (product.duration || 30)) + user.isSubscriber = true + user.subscribeExpiry = expiry + } + await user.save() + } + } + + logger.info(`支付成功: ${orderNo}`) + return res.status(200).send('SUCCESS') + + } catch (error) { + logger.error('支付回调处理失败:', error) + return res.status(500).send('FAIL') + } +}) + +/** + * 验证支付结果 + * POST /api/payment/verify + */ +router.post('/verify', async (req, res) => { + try { + const user = req.user + const { productId, orderNo } = req.body + + // 验证订单是否已处理 + const order = await Order.findOne({ + orderNo, + openid: user.openid, + status: { $in: ['paid', 'completed'] } + }) + + if (order) { + return ApiResponse.success(res, { + alreadyProcessed: true, + message: '订单已处理' + }) + } + + // 获取商品信息 + const product = await ShopItem.findOne({ itemId: productId, status: 'active' }) + if (!product) { + return ApiResponse.error(res, `未知商品: ${productId}`, 400) + } + + // 更新用户商品 + let updateData = {} + + if (product.type === 'skin') { + if (!user.ownedSkins.includes(productId)) { + user.ownedSkins.push(productId) + } + } else if (product.type === 'noad') { + user.isAdFree = true + } else if (product.type === 'subscription') { + const expiry = new Date() + expiry.setDate(expiry.getDate() + (product.duration || 30)) + user.isSubscriber = true + user.subscribeExpiry = expiry + } + + await user.save() + + // 记录订单 + const newOrder = new Order({ + orderNo, + openid: user.openid, + userId: user._id, + productId, + productName: product.name, + amount: product.price, + status: 'completed', + completedAt: new Date() + }) + await newOrder.save() + + return ApiResponse.success(res, { + product, + message: `${product.name} 已解锁!` + }) + + } catch (error) { + logger.error('验证支付失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + + +/** + * 申请退款 + * POST /api/payment/refund + */ +router.post('/refund', async (req, res) => { + try { + const { orderNo, reason } = req.body + + // 查询订单 + const order = await Order.findOne({ orderNo }) + if (!order) { + return ApiResponse.error(res, '订单不存在', 404) + } + + if (order.status !== 'paid' && order.status !== 'completed') { + return ApiResponse.error(res, '订单状态不支持退款', 400) + } + + // 调用微信退款接口 + const result = await weChatService.refundOrder( + order.transactionId, + `REF_${order.orderNo}`, + order.amount / 100, + order.amount / 100, + reason + ) + + if (result.success) { + // 更新订单状态 + order.status = 'refunded' + order.refundReason = reason + order.refundedAt = new Date() + await order.save() + + return ApiResponse.success(res, result, '退款申请已提交') + } else { + return ApiResponse.error(res, '退款申请失败', 500) + } + + } catch (error) { + logger.error('申请退款失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +module.exports = router diff --git a/backend/wdkj-server/src/routes/pinyin/achievements.js b/backend/wdkj-server/src/routes/pinyin/achievements.js new file mode 100755 index 0000000..cd33d5b --- /dev/null +++ b/backend/wdkj-server/src/routes/pinyin/achievements.js @@ -0,0 +1,15 @@ +const express = require('express'); +const router = express.Router(); +const { authMiddleware } = require('../../middleware/auth'); +const achievementController = require('../../controllers/pinyin/achievementController'); + +// 获取成就列表(公开) +router.get('/', achievementController.getAchievements); + +// 获取我的成就(需要登录) +router.get('/my', authMiddleware, achievementController.getMyAchievements); + +// 检查成就达成(需要登录) +router.post('/check', authMiddleware, achievementController.checkAchievements); + +module.exports = router; diff --git a/backend/wdkj-server/src/routes/pinyin/contents.js b/backend/wdkj-server/src/routes/pinyin/contents.js new file mode 100755 index 0000000..f7ea390 --- /dev/null +++ b/backend/wdkj-server/src/routes/pinyin/contents.js @@ -0,0 +1,16 @@ +const express = require('express'); +const router = express.Router(); +const { authMiddleware } = require('../../middleware/auth'); +const contentController = require('../../controllers/pinyin/contentController'); + +// 公开接口 +router.get('/', contentController.getContents); +router.get('/:symbol', contentController.getContentBySymbol); +router.get('/:symbol/audio', contentController.getContentAudio); + +// 管理员接口 +router.post('/', authMiddleware, contentController.createContent); +router.put('/:id', authMiddleware, contentController.updateContent); +router.delete('/:id', authMiddleware, contentController.deleteContent); + +module.exports = router; diff --git a/backend/wdkj-server/src/routes/pinyin/games.js b/backend/wdkj-server/src/routes/pinyin/games.js new file mode 100755 index 0000000..5525a3c --- /dev/null +++ b/backend/wdkj-server/src/routes/pinyin/games.js @@ -0,0 +1,24 @@ +const express = require('express'); +const router = express.Router(); +const { userAuthMiddleware } = require('../../middleware/auth'); +const gameController = require('../../controllers/pinyin/gameController'); + +// 获取游戏配置 +router.get('/config', gameController.getGameConfig); + +// 获取游戏配置(按类型) +router.get('/config/:type', gameController.getGameConfigByType); + +// 获取用户游戏统计(需要登录) +router.get('/stats', userAuthMiddleware, gameController.getGameStats); + +// 获取用户游戏记录(需要登录) +router.get('/records', userAuthMiddleware, gameController.getGameRecords); + +// 记录游戏结果(需要登录) +router.post('/record', userAuthMiddleware, gameController.recordGame); + +// 获取游戏排行榜 +router.get('/leaderboard/:type', gameController.getLeaderboard); + +module.exports = router; diff --git a/backend/wdkj-server/src/routes/pinyin/index.js b/backend/wdkj-server/src/routes/pinyin/index.js new file mode 100755 index 0000000..ef6f7c4 --- /dev/null +++ b/backend/wdkj-server/src/routes/pinyin/index.js @@ -0,0 +1,14 @@ +/** + * 拼音探索模块路由导出 + */ +const contentsRouter = require('./contents'); +const progressRouter = require('./progress'); +const gamesRouter = require('./games'); +const achievementsRouter = require('./achievements'); + +module.exports = { + contents: contentsRouter, + progress: progressRouter, + games: gamesRouter, + achievements: achievementsRouter +}; diff --git a/backend/wdkj-server/src/routes/pinyin/progress.js b/backend/wdkj-server/src/routes/pinyin/progress.js new file mode 100755 index 0000000..1c2c3ec --- /dev/null +++ b/backend/wdkj-server/src/routes/pinyin/progress.js @@ -0,0 +1,9 @@ +const express = require('express'); +const router = express.Router(); +const { userAuthMiddleware } = require('../../middleware/auth'); +const progressController = require('../../controllers/pinyin/progressController'); + +// 获取用户探索进度 +router.get('/', userAuthMiddleware, progressController.getProgress); + +module.exports = router; diff --git a/backend/wdkj-server/src/routes/share.js b/backend/wdkj-server/src/routes/share.js new file mode 100755 index 0000000..e7348b1 --- /dev/null +++ b/backend/wdkj-server/src/routes/share.js @@ -0,0 +1,67 @@ +const express = require('express') +const router = express.Router() +const { ShareRecord, User } = require('../models') +const ApiResponse = require('../utils/response') +const { userAuthMiddleware, optionalAuthMiddleware, authMiddleware } = require('../middleware/auth') +const logger = require('../utils/logger') + +/** + * 记录分享行为(支持匿名) + * POST /api/share/record + */ +router.post('/record', optionalAuthMiddleware, async (req, res) => { + try { + const { shareType, score } = req.body + const openid = req.user ? req.user.openid : 'anonymous' + const userId = req.user ? req.user._id : null + + const record = new ShareRecord({ + openid, + userId, + shareType: shareType || 'app', + score: score || 0 + }) + await record.save() + + return ApiResponse.success(res, { message: '分享已记录' }) + } catch (error) { + logger.error('记录分享失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 获取分享记录列表(管理员) + * GET /api/share/list + */ +router.get('/list', authMiddleware, async (req, res) => { + try { + const { page = 1, limit = 20, openid } = req.query + const skip = (page - 1) * limit + + const query = {} + if (openid && openid !== 'all') { + query.openid = openid + } + + const records = await ShareRecord.find(query) + .sort({ sharedAt: -1 }) + .skip(skip) + .limit(parseInt(limit)) + .populate('userId', 'nickname avatar') + + const total = await ShareRecord.countDocuments(query) + + return ApiResponse.success(res, { + list: records, + total, + page: parseInt(page), + limit: parseInt(limit) + }) + } catch (error) { + logger.error('获取分享记录失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +module.exports = router diff --git a/backend/wdkj-server/src/routes/trend.js b/backend/wdkj-server/src/routes/trend.js new file mode 100644 index 0000000..dbc7d4c --- /dev/null +++ b/backend/wdkj-server/src/routes/trend.js @@ -0,0 +1,83 @@ +const express = require('express'); +const router = express.Router(); +const Trend = require('../models/Trend'); + +/** + * 获取趋势列表 + * GET /api/trend?category=产品&page=1&limit=20 + */ +router.get('/', async (req, res) => { + try { + const { category, page = 1, limit = 20 } = req.query; + const query = { status: 'published' }; + if (category) query.category = category; + + const skip = (parseInt(page) - 1) * parseInt(limit); + const [list, total] = await Promise.all([ + Trend.find(query) + .sort({ newsDate: -1 }) + .skip(skip) + .limit(parseInt(limit)) + .select('-__v'), + Trend.countDocuments(query) + ]); + + res.json({ + success: true, + data: { list, total, page: parseInt(page), totalPages: Math.ceil(total / parseInt(limit)) } + }); + } catch (err) { + res.status(500).json({ success: false, error: err.message }); + } +}); + +/** + * 获取趋势详情 + * GET /api/trend/:id + */ +router.get('/:id', async (req, res) => { + try { + const trend = await Trend.findByIdAndUpdate( + req.params.id, + { $inc: { viewCount: 1 } }, + { new: true } + ); + if (!trend) return res.status(404).json({ success: false, error: '趋势不存在' }); + res.json({ success: true, data: trend }); + } catch (err) { + res.status(500).json({ success: false, error: err.message }); + } +}); + +/** + * 管理员:创建趋势 + * POST /api/trend/admin + */ +router.post('/admin', async (req, res) => { + try { + const trend = new Trend(req.body); + await trend.save(); + res.json({ success: true, data: trend }); + } catch (err) { + res.status(500).json({ success: false, error: err.message }); + } +}); + +/** + * 管理员:批量创建趋势(给 cron 用) + * POST /api/trend/admin/batch + */ +router.post('/admin/batch', async (req, res) => { + try { + const { items } = req.body; + if (!Array.isArray(items) || items.length === 0) { + return res.status(400).json({ success: false, error: 'items 不能为空' }); + } + const result = await Trend.insertMany(items); + res.json({ success: true, data: { inserted: result.length } }); + } catch (err) { + res.status(500).json({ success: false, error: err.message }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/backend/wdkj-server/src/routes/user.js b/backend/wdkj-server/src/routes/user.js new file mode 100755 index 0000000..55081a2 --- /dev/null +++ b/backend/wdkj-server/src/routes/user.js @@ -0,0 +1,424 @@ +const express = require('express') +const router = express.Router() +const { User, Order, Gallery } = require('../models') +const ApiResponse = require('../utils/response') +const { userAuthMiddleware } = require('../middleware/auth') +const logger = require('../utils/logger') + +// 应用用户认证中间件到所有路由 +router.use(userAuthMiddleware) + +/** + * 获取用户进度 + * GET /api/user/progress + */ +router.get('/progress', async (req, res) => { + try { + const user = req.user + + if (!user) { + // 新用户,返回默认进度 + return ApiResponse.success(res, { + isNewUser: true, + data: { + unlockedDims: [1], + dim1Score: 0, + dim2Score: 0, + dim3Score: 0, + dim4Score: 0, + dim5Score: 0, + totalScore: 0, + ownedSkins: [], + isAdFree: false, + isSubscriber: false, + subscribeExpiry: null, + totalPlayTime: 0 + } + }) + } + + return ApiResponse.success(res, { + isNewUser: false, + data: user + }) + + } catch (error) { + logger.error('获取进度失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 保存用户进度 + * POST /api/user/progress + */ +router.post('/progress', async (req, res) => { + try { + const user = req.user + const openid = user.openid + + const { + dim, + data, + dim1Score, + dim2Score, + dim3Score, + dim4Score, + dim5Score, + unlockedDims, + ownedSkins, + isAdFree, + isSubscriber, + subscribeExpiry, + totalPlayTime + } = req.body + + // 查找或创建用户(理论上已通过中间件找到) + let dbUser = user + + // 新版接口:按维度更新 + if (dim && data) { + const dimData = dbUser.exploreData[dim] || {} + let updatedDim = { ...dimData } + + if (dim === 'dim1') { + updatedDim.completed = data.completed || dimData.completed || false + updatedDim.bestScore = Math.max(data.bestScore || 0, dimData.bestScore || 0) + updatedDim.collectedEggs = (dimData.collectedEggs || 0) + (data.newEggs || 0) + updatedDim.totalLength = Math.max(data.totalLength || 0, dimData.totalLength || 0) + } else if (dim === 'dim2') { + updatedDim.completed = data.completed || dimData.completed || false + updatedDim.bestArea = Math.max(data.bestArea || 0, dimData.bestArea || 0) + updatedDim.createdShapes = (dimData.createdShapes || 0) + (data.newShapes || 0) + } else if (dim === 'dim3') { + updatedDim.completed = data.completed || dimData.completed || false + updatedDim.exploredFaces = Math.max(data.exploredFaces || 0, dimData.exploredFaces || 0) + updatedDim.rotationTime = (dimData.rotationTime || 0) + (data.rotationTime || 0) + updatedDim.bestScore = Math.max(data.bestScore || 0, dimData.bestScore || 0) + } else if (dim === 'dim4') { + updatedDim.completed = data.completed || dimData.completed || false + updatedDim.bestScore = Math.max(data.bestScore || 0, dimData.bestScore || 0) + updatedDim.exploredEvents = (dimData.exploredEvents || 0) + (data.newEvents || 0) + } else if (dim === 'dim5') { + updatedDim.completed = data.completed || dimData.completed || false + updatedDim.bestScore = Math.max(data.bestScore || 0, dimData.bestScore || 0) + updatedDim.exploredThoughts = (dimData.exploredThoughts || 0) + (data.newThoughts || 0) + } + + dbUser.exploreData[dim] = updatedDim + + // 同步更新已解锁维度 + if (data.completed && !dbUser.unlockedDims.includes(parseInt(dim.replace('dim', '')) + 1)) { + const dimNum = parseInt(dim.replace('dim', '')) + if (!dbUser.unlockedDims.includes(dimNum + 1)) { + dbUser.unlockedDims.push(dimNum + 1) + } + } + + if (totalPlayTime !== null && totalPlayTime !== undefined) { + dbUser.totalPlayTime += totalPlayTime + } + } + + // 兼容旧版接口:直接传各维度分数 + if (dim1Score !== null && dim1Score !== undefined) { + dbUser.exploreData.dim1.bestScore = Math.max(dim1Score, dbUser.exploreData.dim1?.bestScore || 0) + } + if (dim2Score !== null && dim2Score !== undefined) { + dbUser.exploreData.dim2.bestArea = Math.max(dim2Score, dbUser.exploreData.dim2?.bestArea || 0) + } + if (dim3Score !== null && dim3Score !== undefined) { + dbUser.exploreData.dim3.bestScore = Math.max(dim3Score, dbUser.exploreData.dim3?.bestScore || 0) + } + if (dim4Score !== null && dim4Score !== undefined) { + dbUser.exploreData.dim4 = dbUser.exploreData.dim4 || {} + dbUser.exploreData.dim4.bestScore = Math.max(dim4Score, dbUser.exploreData.dim4?.bestScore || 0) + } + if (dim5Score !== null && dim5Score !== undefined) { + dbUser.exploreData.dim5 = dbUser.exploreData.dim5 || {} + dbUser.exploreData.dim5.bestScore = Math.max(dim5Score, dbUser.exploreData.dim5?.bestScore || 0) + } + + // 更新其他字段 + if (ownedSkins !== null && ownedSkins !== undefined) dbUser.ownedSkins = ownedSkins + if (isAdFree !== null && isAdFree !== undefined) dbUser.isAdFree = isAdFree + if (isSubscriber !== null && isSubscriber !== undefined) dbUser.isSubscriber = isSubscriber + if (subscribeExpiry !== null && subscribeExpiry !== undefined) dbUser.subscribeExpiry = subscribeExpiry + + await dbUser.save() + + // 计算总分 + const totalScore = dbUser.calculateTotalScore() + + return ApiResponse.success(res, { + success: true, + totalScore, + exploreData: user.exploreData + }) + + } catch (error) { + logger.error('保存进度失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 获取排行榜 + * GET /api/user/leaderboard + */ +router.get('/leaderboard', async (req, res) => { + try { + const { limit = 50, dim } = req.query + + const users = await User.getLeaderboard(dim ? parseInt(dim) : null, parseInt(limit)) + + const data = users.map((u, i) => ({ + rank: i + 1, + openid: u.openid, + nickname: u.nickName || '星辰旅者', + avatar: u.avatarUrl || '', + totalScore: u.totalScore || 0, + dim1Score: u.dim1Score || 0, + dim2Score: u.dim2Score || 0, + dim3Score: u.dim3Score || 0, + dim4Score: u.dim4Score || 0, + dim5Score: u.dim5Score || 0, + unlockedDims: u.unlockedDims || [1] + })) + + return ApiResponse.success(res, data) + + } catch (error) { + logger.error('获取排行榜失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 获取用户成就 + * GET /api/user/achievements + */ +router.get('/achievements', async (req, res) => { + try { + const user = req.user + + // 生成成就数据 + const achievements = [ + { + name: '探索成就', + achievements: [ + { + id: 'dim1_complete', + name: '一维探索者', + description: '完成一维空间的探索', + icon: '⭐', + unlocked: user.exploreData.dim1?.completed || false, + date: user.exploreData.dim1?.completed ? user.updatedAt.toISOString().split('T')[0] : null + }, + { + id: 'dim2_complete', + name: '二维探索者', + description: '完成二维空间的探索', + icon: '✨', + unlocked: user.exploreData.dim2?.completed || false, + date: user.exploreData.dim2?.completed ? user.updatedAt.toISOString().split('T')[0] : null + }, + { + id: 'dim3_complete', + name: '三维探索者', + description: '完成三维空间的探索', + icon: '🌟', + unlocked: user.exploreData.dim3?.completed || false, + date: user.exploreData.dim3?.completed ? user.updatedAt.toISOString().split('T')[0] : null + }, + { + id: 'dim4_complete', + name: '四维探索者', + description: '完成四维空间的探索', + icon: '💫', + unlocked: user.exploreData.dim4?.completed || false, + date: user.exploreData.dim4?.completed ? user.updatedAt.toISOString().split('T')[0] : null + }, + { + id: 'dim5_complete', + name: '五维探索者', + description: '完成五维空间的探索', + icon: '🌌', + unlocked: user.exploreData.dim5?.completed || false, + date: user.exploreData.dim5?.completed ? user.updatedAt.toISOString().split('T')[0] : null + } + ] + }, + { + name: '创作成就', + achievements: [ + { + id: 'first_work', + name: '初次创作', + description: '创作你的第一个作品', + icon: '🎨', + unlocked: user.achievements?.includes('first_work') || false, + date: user.achievements?.includes('first_work') ? user.updatedAt.toISOString().split('T')[0] : null + }, + { + id: 'five_works', + name: '创作达人', + description: '创作5个作品', + icon: '🏆', + unlocked: user.achievements?.includes('five_works') || false, + date: user.achievements?.includes('five_works') ? user.updatedAt.toISOString().split('T')[0] : null + }, + { + id: 'ten_works', + name: '创作大师', + description: '创作10个作品', + icon: '👑', + unlocked: user.achievements?.includes('ten_works') || false, + date: user.achievements?.includes('ten_works') ? user.updatedAt.toISOString().split('T')[0] : null + } + ] + }, + { + name: '收藏成就', + achievements: [ + { + id: 'first_collection', + name: '初次收藏', + description: '收藏你的第一个知识', + icon: '❤️', + unlocked: user.collectedKnowledge?.length > 0, + date: user.collectedKnowledge?.length > 0 ? user.updatedAt.toISOString().split('T')[0] : null + }, + { + id: 'five_collections', + name: '收藏爱好者', + description: '收藏5个知识', + icon: '🧡', + unlocked: user.collectedKnowledge?.length >= 5, + date: user.collectedKnowledge?.length >= 5 ? user.updatedAt.toISOString().split('T')[0] : null + }, + { + id: 'ten_collections', + name: '收藏大师', + description: '收藏10个知识', + icon: '💛', + unlocked: user.collectedKnowledge?.length >= 10, + date: user.collectedKnowledge?.length >= 10 ? user.updatedAt.toISOString().split('T')[0] : null + } + ] + }, + { + name: '社交成就', + achievements: [ + { + id: 'first_share', + name: '初次分享', + description: '分享你的第一个作品', + icon: '📤', + unlocked: user.achievements?.includes('first_share') || false, + date: user.achievements?.includes('first_share') ? user.updatedAt.toISOString().split('T')[0] : null + }, + { + id: 'five_shares', + name: '分享达人', + description: '分享5个作品', + icon: '📣', + unlocked: user.achievements?.includes('five_shares') || false, + date: user.achievements?.includes('five_shares') ? user.updatedAt.toISOString().split('T')[0] : null + } + ] + } + ] + + return ApiResponse.success(res, achievements) + + } catch (error) { + logger.error('获取成就失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 获取用户统计数据 + * GET /api/user/stats + */ +router.get('/stats', async (req, res) => { + try { + const user = req.user + const { Gallery } = require('../models') + + // 获取用户作品数 + const worksCount = await Gallery.countDocuments({ userId: user._id }) + + // 获取等级信息 + const levelInfo = user.getLevelInfo() + + // 计算探索时长(分钟转小时,保留1位小数) + const playTimeHours = Math.round((user.totalPlayTime || 0) / 60 * 10) / 10 + + return ApiResponse.success(res, { + level: levelInfo.level, + levelName: levelInfo.name, + levelPoints: levelInfo.points, + levelProgress: levelInfo.progress, + totalProgress: levelInfo.totalProgress, + nextLevelPoints: levelInfo.nextLevelPoints, + totalPlayTime: playTimeHours, + totalScore: user.totalScore || 0, + worksCount: worksCount, + collectionCount: user.collectedKnowledge?.length || 0, + achievementCount: user.achievements?.length || 0, + unlockedDims: user.unlockedDims || [1] + }) + + } catch (error) { + logger.error('获取用户统计失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +/** + * 获取用户作品列表 + * GET /api/user/works + */ +router.get('/works', async (req, res) => { + try { + const user = req.user + const { page = 1, limit = 20 } = req.query + const { Gallery } = require('../models') + + const query = { userId: user._id } + + const total = await Gallery.countDocuments(query) + const works = await Gallery.find(query) + .sort({ createdAt: -1 }) + .skip((parseInt(page) - 1) * parseInt(limit)) + .limit(parseInt(limit)) + .lean() + + // 格式化作品数据 + const formattedWorks = works.map(work => ({ + id: work._id, + title: work.title, + description: work.description, + imageUrl: work.imageUrl, + dimension: work.dimension, + status: work.status, + likes: work.likes || 0, + views: work.views || 0, + createdAt: work.createdAt + })) + + return ApiResponse.paginated(res, formattedWorks, { + page: parseInt(page), + limit: parseInt(limit), + total + }) + + } catch (error) { + logger.error('获取用户作品失败:', error) + return ApiResponse.serverError(res, error.message) + } +}) + +module.exports = router diff --git a/backend/wdkj-server/src/scripts/generateAudioUrls.js b/backend/wdkj-server/src/scripts/generateAudioUrls.js new file mode 100755 index 0000000..c08faa0 --- /dev/null +++ b/backend/wdkj-server/src/scripts/generateAudioUrls.js @@ -0,0 +1,176 @@ +/** + * 批量生成拼音音频URL脚本 + * 支持多种TTS服务:有道词典、百度语音、科大讯飞等 + */ + +const fs = require('fs'); +const path = require('path'); + +// 拼音列表 +const initials = ['b','p','m','f','d','t','n','l','g','k','h','j','q','x','zh','ch','sh','r','z','c','s','y','w']; +const finals = ['a','o','e','i','u','ü','ai','ei','ui','ao','ou','iu','ie','üe','er','an','en','in','un','ün','ang','eng','ing','ong']; +const overalls = ['zhi','chi','shi','ri','zi','ci','si','yi','wu','yu','ye','yue','yuan','yin','yun','ying']; + +/** + * 有道词典TTS服务 + * 优点:免费、无需注册、支持中文 + * 缺点:音质一般 + */ +function getYoudaoUrl(text) { + // 从环境变量读取TTS基础URL + const ttsBaseUrl = process.env.TTS_BASE_URL || 'https://dict.youdao.com/dictvoice'; + return `${ttsBaseUrl}?audio=${encodeURIComponent(text)}&type=1`; +} + +/** + * 生成所有拼音的音频URL配置 + */ +function generateAudioConfig() { + // 从环境变量读取配置 + const ttsProvider = process.env.TTS_PROVIDER || 'youdao'; + const ttsBaseUrl = process.env.TTS_BASE_URL || 'https://dict.youdao.com/dictvoice'; + + const config = { + // TTS服务提供商 + provider: ttsProvider, + // 基础URL + baseUrl: ttsBaseUrl, + // 所有拼音的音频URL + audios: {} + }; + + // 声母 + initials.forEach(symbol => { + config.audios[symbol] = { + url: getYoudaoUrl(symbol), + name: getPinyinName(symbol), + type: 'initial' + }; + }); + + // 韵母 + finals.forEach(symbol => { + config.audios[symbol] = { + url: getYoudaoUrl(symbol), + name: getPinyinName(symbol), + type: 'final' + }; + }); + + // 整体认读 + overalls.forEach(symbol => { + config.audios[symbol] = { + url: getYoudaoUrl(symbol), + name: getPinyinName(symbol), + type: 'overall' + }; + }); + + return config; +} + +/** + * 获取拼音名称 + */ +function getPinyinName(symbol) { + const nameMap = { + 'b': '玻', 'p': '坡', 'm': '摸', 'f': '佛', + 'd': '得', 't': '特', 'n': '讷', 'l': '勒', + 'g': '哥', 'k': '科', 'h': '喝', + 'j': '基', 'q': '欺', 'x': '希', + 'zh': '知', 'ch': '蚩', 'sh': '诗', 'r': '日', + 'z': '资', 'c': '雌', 's': '思', + 'y': '医', 'w': '巫', + 'a': '啊', 'o': '喔', 'e': '鹅', 'i': '衣', 'u': '乌', 'ü': '迂', + 'ai': '哀', 'ei': '诶', 'ui': '威', 'ao': '熬', 'ou': '欧', + 'iu': '优', 'ie': '耶', 'üe': '约', 'er': '儿', + 'an': '安', 'en': '恩', 'in': '因', 'un': '温', 'ün': '晕', + 'ang': '昂', 'eng': '亨', 'ing': '英', 'ong': '雍', + 'zhi': '织', 'chi': '吃', 'shi': '狮', 'ri': '日', + 'zi': '资', 'ci': '疵', 'si': '丝', + 'yi': '衣', 'wu': '乌', 'yu': '迂', + 'ye': '耶', 'yue': '约', 'yuan': '冤', + 'yin': '因', 'yun': '晕', 'ying': '英' + }; + return nameMap[symbol] || symbol; +} + +/** + * 生成SQL更新语句(用于直接更新数据库) + */ +function generateSqlUpdates() { + const config = generateAudioConfig(); + const sqls = []; + + Object.entries(config.audios).forEach(([symbol, data]) => { + const sql = `UPDATE pinyin_contents SET audio_url = '${data.url}' WHERE symbol = '${symbol}';`; + sqls.push(sql); + }); + + return sqls.join('\n'); +} + +/** + * 生成MongoDB更新脚本 + */ +function generateMongoScript() { + const config = generateAudioConfig(); + const updates = []; + + Object.entries(config.audios).forEach(([symbol, data]) => { + updates.push({ + updateOne: { + filter: { symbol: symbol }, + update: { $set: { audioUrl: data.url } } + } + }); + }); + + return `db.pinyincontents.bulkWrite(${JSON.stringify(updates, null, 2)});`; +} + +/** + * 保存配置文件 + */ +function saveConfig() { + const config = generateAudioConfig(); + const outputPath = path.join(__dirname, '../../config/pinyin-audio-config.json'); + + // 确保目录存在 + const dir = path.dirname(outputPath); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + + fs.writeFileSync(outputPath, JSON.stringify(config, null, 2)); + console.log(`✅ 音频配置已保存到: ${outputPath}`); + + // 生成SQL文件 + const sqlPath = path.join(__dirname, '../../config/update-audio-urls.sql'); + fs.writeFileSync(sqlPath, generateSqlUpdates()); + console.log(`✅ SQL更新脚本已保存到: ${sqlPath}`); + + // 生成MongoDB脚本 + const mongoPath = path.join(__dirname, '../../config/update-audio-urls.js'); + fs.writeFileSync(mongoPath, generateMongoScript()); + console.log(`✅ MongoDB更新脚本已保存到: ${mongoPath}`); + + // 输出统计 + console.log('\n📊 生成统计:'); + console.log(` - 声母: ${initials.length} 个`); + console.log(` - 韵母: ${finals.length} 个`); + console.log(` - 整体认读: ${overalls.length} 个`); + console.log(` - 总计: ${initials.length + finals.length + overalls.length} 个拼音`); +} + +// 运行 +if (require.main === module) { + saveConfig(); +} + +module.exports = { + generateAudioConfig, + generateSqlUpdates, + generateMongoScript, + getYoudaoUrl +}; diff --git a/backend/wdkj-server/src/scripts/init-knowledge-ai.js b/backend/wdkj-server/src/scripts/init-knowledge-ai.js new file mode 100644 index 0000000..0465778 --- /dev/null +++ b/backend/wdkj-server/src/scripts/init-knowledge-ai.js @@ -0,0 +1,210 @@ +/** + * AI 知识库初始化脚本 + * 为 5 个 AI 维度填充知识内容 + * + * 用法: + * cd /root/Hermes-workspace/yuzhiran-ai-dimension/backend/wdkj-server + * node src/scripts/init-knowledge-ai.js + */ + +require('dotenv').config(); +const mongoose = require('mongoose'); +const Knowledge = require('../models/Knowledge'); + +// 数据库连接 +const DB_URI = process.env.MONGODB_URI || process.env.DB_URI || 'mongodb://127.0.0.1:27017/wdkj'; + +async function main() { + console.log('[AI知识初始化] 开始连接数据库...'); + await mongoose.connect(DB_URI); + console.log('[AI知识初始化] 数据库已连接:', DB_URI); + + // 检查现有数据 + const existing = await Knowledge.countDocuments(); + if (existing > 0) { + console.log(`[AI知识初始化] 发现 ${existing} 条现有记录`); + console.log('[AI知识初始化] 继续追加(如需清空请先手动删除)'); + } + + // 维度数据:key = dim 编号 (1-5) + const dimensionData = { +1: [ + { + title: '图灵测试:人工智能的哲学起点', + content: '1950 年,英国数学家艾伦·图灵发表了划时代的论文《计算机器与智能》,提出了著名的"图灵测试"——如果一台机器能与人类进行对话而不被辨别出机器身份,就可以认为它具备智能。图灵测试不是技术标准,而是哲学思辨:"机器能思考吗?"这个问题开启了 AI 研究的序幕。', + dim: 1, category: 'history', tags: ['图灵测试', '艾伦·图灵', 'AI 哲学'], + isPremium: false, sortOrder: 1, status: 'approved' + }, + { + title: '达特茅斯会议:AI 的诞生', + content: '1956 年夏天,约翰·麦卡锡、马文·明斯基、克劳德·香农等科学家在达特茅斯学院聚会,首次提出"Artificial Intelligence"(人工智能)一词,标志着 AI 作为独立学科的正式诞生。会议上定义了 AI 的核心目标:让机器模拟人类智能行为——学习、推理、感知、决策。', + dim: 1, category: 'history', tags: ['达特茅斯', '麦卡锡', 'AI 起源'], + isPremium: false, sortOrder: 2, status: 'approved' + }, + { + title: '深蓝击败卡斯帕罗夫(1997)', + content: '1997 年 5 月,IBM 的深蓝(Deep Blue)在国际象棋比赛中以 3.5:2.5 击败世界冠军加里·卡斯帕罗夫。这是 AI 第一次在正式比赛中战胜人类世界冠军,具有里程碑意义。深蓝的胜利展示了暴力搜索和硬编码知识的威力,也引发了对 AI 威胁论的激烈讨论。', + dim: 1, category: 'history', tags: ['深蓝', 'IBM', '卡斯帕罗夫'], + isPremium: false, sortOrder: 3, status: 'approved' + }, + { + title: 'AlphaGo 与深度学习革命(2016)', + content: '2016 年 3 月,Google DeepMind 的 AlphaGo 以 4:1 击败围棋世界冠军李世石。围棋曾被视为人类智力的最后堡垒,AlphaGo 的胜利震惊世界。AlphaGo 结合了深度神经网络和蒙特卡洛树搜索,开创了 AI 的新纪元,直接推动了深度学习在各行各业的爆发式应用。', + dim: 1, category: 'history', tags: ['AlphaGo', '深度学习', 'DeepMind'], + isPremium: true, sortOrder: 4, status: 'approved' + }, + { + title: '从 GPT 到 DeepSeek:大语言模型时代', + content: '2018 年 OpenAI 发布 GPT-1,开启了基于 Transformer 的预训练范式。2022 年 ChatGPT 引爆全球,展示了生成式 AI 的惊人能力。2024-2026 年,以 DeepSeek、Llama、Qwen 为代表的开源大模型迅速崛起,AI 真正走进千家万户,成为生产力工具。', + dim: 1, category: 'history', tags: ['GPT', 'DeepSeek', '大语言模型'], + isPremium: true, sortOrder: 5, status: 'approved' + } +], + +2: [ + { + title: '机器学习与深度学习基础', + content: '机器学习是 AI 的核心分支,让计算机从数据中自动学习规律,而不需要显式编程。深度学习是机器学习的一个子集,使用多层神经网络来处理复杂模式。从 2012 年 AlexNet 在 ImageNet 夺冠开始,深度学习席卷所有领域。', + dim: 2, category: 'concept', tags: ['机器学习', '深度学习', '神经网络'], + isPremium: false, sortOrder: 1, status: 'approved' + }, + { + title: 'Transformer:改变一切的架构', + content: '2017 年,Google 在论文《Attention Is All You Need》中提出 Transformer 架构,核心是自注意力机制(Self-Attention),让模型可以并行处理序列,突破 RNN 的瓶颈。Transformer 成为后来所有大模型(GPT、BERT、T5)的基础,是 AI 历史上最重要的架构创新之一。', + dim: 2, category: 'concept', tags: ['Transformer', '注意力机制', 'Google'], + isPremium: false, sortOrder: 2, status: 'approved' + }, + { + title: '从 BERT 到 GPT:预训练模型的演进', + content: 'BERT(2018)展示了双向编码器的强大,GPT(2018)展示了自回归生成的能力。随着模型规模指数级增长(参数从亿到万亿),涌现出前所未有的能力——这就是规模定律(Scaling Law)。GPT 系列代表了一条路:预训练 + 指令微调 + RLHF,成为今天大语言模型的标准配方。', + dim: 2, category: 'application', tags: ['BERT', 'GPT', '预训练'], + isPremium: false, sortOrder: 3, status: 'approved' + }, + { + title: '多模态 AI:从文本到全感官', + content: '2022 年后,AI 不再局限于文本。DALL·E、Midjourney、Stable Diffusion 让 AI 绘画普及;Sora、Kling 让 AI 生成视频成为现实;Suno 让 AI 创作音乐。多模态(Multimodal)成为大模型的新标准:一个模型同时理解文本、图像、声音,打通感官壁垒。', + dim: 2, category: 'application', tags: ['多模态', '文生图', 'AI 视频'], + isPremium: true, sortOrder: 4, status: 'approved' + }, + { + title: 'MoE 架构:万亿参数的经济之选', + content: '混合专家模型(Mixture of Experts, MoE)让模型参数规模达到万亿级别,但推理时只激活部分参数,显著提升性能同时控制成本。GPT-4、Mixtral、DeepSeek 等均已采用 MoE,成为大模型追求性能的终极架构。', + dim: 2, category: 'concept', tags: ['MoE', '万亿参数', '架构'], + isPremium: true, sortOrder: 5, status: 'approved' + } +], + +3: [ + { + title: 'OpenAI vs Anthropic:闭源双雄争霸', + content: 'OpenAI 的 GPT-4/4o 系列占据高端市场,Anthropic 的 Claude 3 系列以安全和推理见长。两者代表了闭源大模型的最高水平,但商业化策略不同:OpenAI 走 B2B2C,Anthropic 专注企业市场。', + dim: 3, category: 'application', tags: ['OpenAI', 'Anthropic', '闭源模型'], + isPremium: false, sortOrder: 1, status: 'approved' + }, + { + title: 'DeepSeek:中国开源力量的崛起', + content: 'DeepSeek(深度求索)是中国 AI 公司的代表,其 DeepSeek-V3 在多项评测中逼近 GPT-4,同时大力开源(DeepSeek-V2、Coder 系列)。DeepSeek 展示了中国团队在大模型领域的技术实力。', + dim: 3, category: 'application', tags: ['DeepSeek', '中国 AI', '开源'], + isPremium: false, sortOrder: 2, status: 'approved' + }, + { + title: '开源 vs 闭源:生态之争', + content: '闭源模型(OpenAI、Anthropic)性能领先但成本高、可控性差;开源模型(Llama 3、DeepSeek、Qwen)灵活部署、可定制,生态快速繁荣。2026 年,开源生态已形成完整工具链(vLLM、Ollama、Hugging Face),成为中小企业首选。', + dim: 3, category: 'concept', tags: ['开源', '闭源', '生态'], + isPremium: false, sortOrder: 3, status: 'approved' + }, + { + title: 'Agent 智能体:从 Chatbot 到自主智能', + content: 'Agent 是新一代 AI 形态,不仅能对话,还能规划、使用工具、执行任务。AutoGPT、MetaGPT 展示了潜力;Claude 的 Tool Use、OpenAI 的 GPTs 让 Agent 更实用。Agent 是通向 AGI 的关键路径。', + dim: 3, category: 'application', tags: ['Agent', '自主智能'], + isPremium: true, sortOrder: 4, status: 'approved' + }, + { + title: '具身智能与世界模型', + content: '具身智能(Embodied AI)让 AI 拥有"身体",能够感知和操作物理世界。Google 的 RT-2、DeepMind 的 RoboCat 展示了机器人与大模型结合的前景。世界模型(World Model)让 AI 理解物理规律,模拟环境,是具身智能的核心。', + dim: 3, category: 'concept', tags: ['具身智能', '世界模型', '机器人'], + isPremium: true, sortOrder: 5, status: 'approved' + } +], + +4: [ + { + title: '提示词工程入门', + content: '好的 prompt 能显著提升 AI 输出质量。提示词工程包括:角色设定、任务描述、输出格式、few-shot 示例、思维链(Chain-of-Thought)。学会写 prompt,是使用大模型的基本功。', + dim: 4, category: 'concept', tags: ['提示词', 'Prompt Engineering'], + isPremium: false, sortOrder: 1, status: 'approved' + }, + { + title: '检索增强生成(RAG)详解', + content: 'RAG (Retrieval-Augmented Generation) 将外部知识库与 LLM 结合,解决大模型知识滞后和幻觉问题。流程:用户提问 → 向量检索相关文档 → 拼接 prompt → LLM 生成答案。RAG 是企业知识库 AI 化的核心技术。', + dim: 4, category: 'concept', tags: ['RAG', '检索增强', '知识库'], + isPremium: false, sortOrder: 2, status: 'approved' + }, + { + title: '模型微调(Fine-tuning)实战', + content: '微调是在预训练模型基础上用领域数据进一步训练,让模型适应特定任务。常用方法:全量微调、LoRA(低秩适应)、QLoRA(量化 LoRA)。小数据也能做微调,但要注意过拟合和灾难性遗忘。', + dim: 4, category: 'application', tags: ['Fine-tuning', 'LoRA', '微调'], + isPremium: true, sortOrder: 3, status: 'approved' + }, + { + title: 'LangChain:AI 应用开发框架', + content: 'LangChain 是最流行的 LLM 应用框架,提供模型调用、提示模板、记忆管理、工具链、Agent 等模块,大大降低开发难度。用它快速搭建 RAG 应用、聊天机器人、数据分析工具。', + dim: 4, category: 'application', tags: ['LangChain', '开发框架'], + isPremium: true, sortOrder: 4, status: 'approved' + }, + { + title: 'Dify:无代码 AI 平台', + content: 'Dify 是开源的 LLMOps 平台,可视化编排 AI 工作流,支持 RAG、Agent、知识库,无需写代码即可构建企业级 AI 应用。适合业务人员快速落地 AI 解决方案。', + dim: 4, category: 'application', tags: ['Dify', '低代码'], + isPremium: true, sortOrder: 5, status: 'approved' + } +], + +5: [ + { + title: 'WAIC 2026:世界人工智能大会即将开幕', + content: '2026 年世界人工智能大会(WAIC)将于 7 月 17 日在上海开幕,预计将有 1100+ 企业参展,发布众多前沿技术和产品。WAIC 是了解 AI 趋势的绝佳窗口。', + dim: 5, category: 'concept', tags: ['WAIC', '会议'], + isPremium: false, sortOrder: 1, status: 'approved' + }, + { + title: 'AI 法规与伦理:全球监管趋势', + content: '各国正在加快 AI 立法:欧盟 AI Act 已生效,美国 NIST AI RMF 发布,中国《生成式 AI 服务管理暂行办法》持续完善。安全、可控、可信成为 AI 发展的重要议题。', + dim: 5, category: 'concept', tags: ['AI 伦理', '法规'], + isPremium: false, sortOrder: 2, status: 'approved' + }, + { + title: 'Agent 将如何改变软件交互方式?', + content: 'Agent 让软件从"人找功能"变为"AI 帮我做事"。未来的办公软件、设计工具、数据分析平台都将以 Agent 为核心交互。一些专家预测:2027 年 Agent 将替代 30% 的重复性办公任务。', + dim: 5, category: 'application', tags: ['Agent', '未来趋势'], + isPremium: true, sortOrder: 3, status: 'approved' + } +] + }; + + const dims = [1, 2, 3, 4, 5]; + let totalInserted = 0; + + for (const dim of dims) { + const entries = dimensionData[dim]; + if (!entries) continue; + console.log(`[维度${dim}] 开始插入 ${entries.length} 条知识...`); + try { + const result = await Knowledge.insertMany(entries); + totalInserted += result.length; + console.log(`[维度${dim}] 已插入 ${result.length} 条`); + } catch (err) { + console.error(`[维度${dim}] 插入失败:`, err.message); + } + } + + const total = await Knowledge.countDocuments(); + console.log(`✅ 完成!Knowledge 库现有 ${total} 条记录(本次新增 ${totalInserted} 条)`); + await mongoose.disconnect(); + console.log('数据库连接已关闭'); +} + +main().catch(err => { + console.error('❌ 初始化失败:', err.message); + process.exit(1); +}); \ No newline at end of file diff --git a/backend/wdkj-server/src/scripts/initPinyinData.js b/backend/wdkj-server/src/scripts/initPinyinData.js new file mode 100755 index 0000000..fbf569d --- /dev/null +++ b/backend/wdkj-server/src/scripts/initPinyinData.js @@ -0,0 +1,395 @@ +/** + * 拼音数据初始化脚本 + * 创建所有63个拼音的基础数据 + */ + +require('dotenv').config(); +const mongoose = require('mongoose'); +const { PinyinContent } = require('../models/pinyin'); +const { PinyinAchievement } = require('../models/pinyin'); + +// 声母表 (23个) +const initials = [ + { symbol: 'b', name: '玻', order: 1 }, + { symbol: 'p', name: '坡', order: 2 }, + { symbol: 'm', name: '摸', order: 3 }, + { symbol: 'f', name: '佛', order: 4 }, + { symbol: 'd', name: '得', order: 5 }, + { symbol: 't', name: '特', order: 6 }, + { symbol: 'n', name: '讷', order: 7 }, + { symbol: 'l', name: '勒', order: 8 }, + { symbol: 'g', name: '哥', order: 9 }, + { symbol: 'k', name: '科', order: 10 }, + { symbol: 'h', name: '喝', order: 11 }, + { symbol: 'j', name: '基', order: 12 }, + { symbol: 'q', name: '欺', order: 13 }, + { symbol: 'x', name: '希', order: 14 }, + { symbol: 'zh', name: '知', order: 15 }, + { symbol: 'ch', name: '蚩', order: 16 }, + { symbol: 'sh', name: '诗', order: 17 }, + { symbol: 'r', name: '日', order: 18 }, + { symbol: 'z', name: '资', order: 19 }, + { symbol: 'c', name: '雌', order: 20 }, + { symbol: 's', name: '思', order: 21 }, + { symbol: 'y', name: '医', order: 22 }, + { symbol: 'w', name: '巫', order: 23 } +]; + +// 韵母表 (24个) +const finals = [ + { symbol: 'a', name: '啊', order: 1 }, + { symbol: 'o', name: '喔', order: 2 }, + { symbol: 'e', name: '鹅', order: 3 }, + { symbol: 'i', name: '衣', order: 4 }, + { symbol: 'u', name: '乌', order: 5 }, + { symbol: 'ü', name: '迂', order: 6 }, + { symbol: 'ai', name: '哀', order: 7 }, + { symbol: 'ei', name: '诶', order: 8 }, + { symbol: 'ui', name: '威', order: 9 }, + { symbol: 'ao', name: '熬', order: 10 }, + { symbol: 'ou', name: '欧', order: 11 }, + { symbol: 'iu', name: '优', order: 12 }, + { symbol: 'ie', name: '耶', order: 13 }, + { symbol: 'üe', name: '约', order: 14 }, + { symbol: 'er', name: '儿', order: 15 }, + { symbol: 'an', name: '安', order: 16 }, + { symbol: 'en', name: '恩', order: 17 }, + { symbol: 'in', name: '因', order: 18 }, + { symbol: 'un', name: '温', order: 19 }, + { symbol: 'ün', name: '晕', order: 20 }, + { symbol: 'ang', name: '昂', order: 21 }, + { symbol: 'eng', name: '亨', order: 22 }, + { symbol: 'ing', name: '英', order: 23 }, + { symbol: 'ong', name: '雍', order: 24 } +]; + +// 整体认读音节 (16个) +const overalls = [ + { symbol: 'zhi', name: '织', order: 1 }, + { symbol: 'chi', name: '吃', order: 2 }, + { symbol: 'shi', name: '狮', order: 3 }, + { symbol: 'ri', name: '日', order: 4 }, + { symbol: 'zi', name: '资', order: 5 }, + { symbol: 'ci', name: '疵', order: 6 }, + { symbol: 'si', name: '丝', order: 7 }, + { symbol: 'yi', name: '衣', order: 8 }, + { symbol: 'wu', name: '乌', order: 9 }, + { symbol: 'yu', name: '迂', order: 10 }, + { symbol: 'ye', name: '耶', order: 11 }, + { symbol: 'yue', name: '约', order: 12 }, + { symbol: 'yuan', name: '冤', order: 13 }, + { symbol: 'yin', name: '因', order: 14 }, + { symbol: 'yun', name: '晕', order: 15 }, + { symbol: 'ying', name: '英', order: 16 } +]; + +// 示例词语数据 +const sampleWords = { + 'b': [ + { word: '爸爸', pinyin: 'bàba', meaning: '父亲' }, + { word: '杯子', pinyin: 'bēizi', meaning: '装水的器具' }, + { word: '白云', pinyin: 'báiyún', meaning: '白色的云' } + ], + 'a': [ + { word: '妈妈', pinyin: 'māma', meaning: '母亲' }, + { word: '阿姨', pinyin: 'āyí', meaning: '母亲的姐妹' } + ], + 'zhi': [ + { word: '知道', pinyin: 'zhīdào', meaning: '了解' }, + { word: '蜘蛛', pinyin: 'zhīzhū', meaning: '一种昆虫' } + ] +}; + +// 发音方法说明 +const pronunciationGuides = { + 'b': '双唇紧闭,阻碍气流,然后双唇突然放开,让气流冲出,读音轻短。', + 'p': '双唇紧闭,阻碍气流,然后双唇突然放开,气流较强地冲出。', + 'm': '双唇紧闭,软腭下降,气流从鼻腔出来,声带振动。', + 'f': '上齿接触下唇,形成缝隙,气流从缝隙中摩擦出来。', + 'a': '嘴巴张大,舌头放平,舌位低,声音响亮。', + 'o': '嘴巴圆圆,舌头后缩,舌位半高。' +}; + +/** + * 生成音频URL(从环境变量读取TTS配置) + */ +function getAudioUrl(symbol) { + // 从环境变量读取TTS基础URL,默认使用有道词典 + const ttsBaseUrl = process.env.TTS_BASE_URL || 'https://dict.youdao.com/dictvoice'; + const ttsProvider = process.env.TTS_PROVIDER || 'youdao'; + + // 如果使用自定义CDN + if (ttsProvider === 'custom' && process.env.TTS_CUSTOM_CDN_URL) { + return `${process.env.TTS_CUSTOM_CDN_URL}/${encodeURIComponent(symbol)}.mp3`; + } + + // 默认使用有道词典格式 + return `${ttsBaseUrl}?audio=${encodeURIComponent(symbol)}&type=1`; +} + +/** + * 生成口型图URL(从环境变量读取CDN配置) + */ +function getMouthImage(symbol) { + // 从环境变量读取口型图CDN + const mouthImageCdn = process.env.MOUTH_IMAGE_CDN_URL; + + if (mouthImageCdn) { + return `${mouthImageCdn}/mouth_${encodeURIComponent(symbol)}.png`; + } + + // 默认返回空,由管理后台上传 + return ''; +} + +/** + * 创建拼音内容数据 + */ +async function createPinyinContents() { + const contents = []; + + // 处理声母 + for (const item of initials) { + const words = sampleWords[item.symbol] || [ + { word: item.name, pinyin: item.symbol, meaning: '示例词语' } + ]; + + contents.push({ + symbol: item.symbol, + type: 'initial', + name: item.name, + order: item.order, + audioUrl: getAudioUrl(item.symbol), + mouthImage: getMouthImage(item.symbol), + pronunciation: pronunciationGuides[item.symbol] || `${item.name}的发音`, + isFree: item.order <= 5, // 前5个免费 + words: words.map(w => ({ + ...w, + audioUrl: getAudioUrl(w.word), + image: '' + })), + exploreAreas: { + audio: true, + mouth: true, + speak: true, + write: true, + game: true, + words: true + } + }); + } + + // 处理韵母 + for (const item of finals) { + const words = sampleWords[item.symbol] || [ + { word: item.name, pinyin: item.symbol, meaning: '示例词语' } + ]; + + contents.push({ + symbol: item.symbol, + type: 'final', + name: item.name, + order: item.order, + audioUrl: getAudioUrl(item.symbol), + mouthImage: getMouthImage(item.symbol), + pronunciation: pronunciationGuides[item.symbol] || `${item.name}的发音`, + isFree: item.order <= 3, // 前3个免费 + words: words.map(w => ({ + ...w, + audioUrl: getAudioUrl(w.word), + image: '' + })), + exploreAreas: { + audio: true, + mouth: true, + speak: true, + write: true, + game: true, + words: true + } + }); + } + + // 处理整体认读 + for (const item of overalls) { + const words = sampleWords[item.symbol] || [ + { word: item.name, pinyin: item.symbol, meaning: '示例词语' } + ]; + + contents.push({ + symbol: item.symbol, + type: 'overall', + name: item.name, + order: item.order, + audioUrl: getAudioUrl(item.symbol), + mouthImage: getMouthImage(item.symbol), + pronunciation: pronunciationGuides[item.symbol] || `${item.name}的发音`, + isFree: item.order <= 2, // 前2个免费 + words: words.map(w => ({ + ...w, + audioUrl: getAudioUrl(w.word), + image: '' + })), + exploreAreas: { + audio: true, + mouth: true, + speak: true, + write: true, + game: true, + words: true + } + }); + } + + return contents; +} + +/** + * 创建成就数据 + */ +async function createAchievements() { + const achievements = [ + { + code: 'first_explore', + name: '初次探索', + description: '完成第一次拼音探索', + type: 'explore', + icon: 'rocket', + condition: { type: 'explore_count', value: 1 }, + reward: { type: 'stone', value: 1 }, + order: 1, + isActive: true + }, + { + code: 'explore_10', + name: '探索新手', + description: '累计探索10个拼音', + type: 'explore', + icon: 'star', + condition: { type: 'explore_count', value: 10 }, + reward: { type: 'stone', value: 5 }, + order: 2, + isActive: true + }, + { + code: 'explore_50', + name: '探索达人', + description: '累计探索50个拼音', + type: 'explore', + icon: 'trophy', + condition: { type: 'explore_count', value: 50 }, + reward: { type: 'stone', value: 20 }, + order: 3, + isActive: true + }, + { + code: 'collect_10', + name: '收集者', + description: '收集10个能量石', + type: 'collection', + icon: 'gem', + condition: { type: 'collect_count', value: 10 }, + reward: { type: 'stone', value: 5 }, + order: 4, + isActive: true + }, + { + code: 'collect_all', + name: '收集大师', + description: '收集所有63个能量石', + type: 'collection', + icon: 'crown', + condition: { type: 'collect_count', value: 63 }, + reward: { type: 'stone', value: 50 }, + order: 5, + isActive: true + }, + { + code: 'streak_7', + name: '坚持一周', + description: '连续7天进行拼音探索', + type: 'streak', + icon: 'fire', + condition: { type: 'streak_days', value: 7 }, + reward: { type: 'stone', value: 10 }, + order: 6, + isActive: true + }, + { + code: 'complete_symbol_b', + name: 'b的探索者', + description: '完成拼音b的所有探索', + type: 'special', + icon: 'check-circle', + condition: { type: 'complete_symbol', value: 1, symbol: 'b' }, + reward: { type: 'stone', value: 2 }, + order: 7, + isActive: true + }, + { + code: 'game_master', + name: '游戏高手', + description: '趣味互动累计获得1000分', + type: 'special', + icon: 'gamepad', + condition: { type: 'game_score', value: 1000 }, + reward: { type: 'stone', value: 15 }, + order: 8, + isActive: true + } + ]; + + return achievements; +} + +/** + * 初始化拼音数据 + */ +async function initPinyinData() { + try { + // 连接数据库 + const mongoURI = process.env.MONGODB_URI || + `mongodb://${process.env.MONGODB_USER}:${process.env.MONGODB_PASSWORD}@${process.env.MONGODB_HOST}:${process.env.MONGODB_PORT}/${process.env.MONGODB_DB}?authSource=${process.env.MONGODB_DB}`; + await mongoose.connect(mongoURI); + console.log('数据库连接成功'); + + // 清空现有数据(可选,生产环境慎用) + const clearExisting = process.env.CLEAR_EXISTING === 'true'; + if (clearExisting) { + await PinyinContent.deleteMany({}); + await PinyinAchievement.deleteMany({}); + console.log('已清空现有数据'); + } + + // 检查是否已有数据 + const existingCount = await PinyinContent.countDocuments(); + if (existingCount > 0 && !clearExisting) { + console.log(`数据库中已有 ${existingCount} 条拼音数据,跳过初始化`); + console.log('如需重新初始化,请设置环境变量 CLEAR_EXISTING=true'); + process.exit(0); + } + + // 创建拼音内容 + const contents = await createPinyinContents(); + await PinyinContent.insertMany(contents); + console.log(`成功创建 ${contents.length} 个拼音内容`); + + // 创建成就 + const achievements = await createAchievements(); + await PinyinAchievement.insertMany(achievements); + console.log(`成功创建 ${achievements.length} 个成就`); + + console.log('拼音数据初始化完成!'); + process.exit(0); + } catch (error) { + console.error('初始化失败:', error); + process.exit(1); + } +} + +// 运行初始化 +if (require.main === module) { + initPinyinData(); +} + +module.exports = { initPinyinData, createPinyinContents, createAchievements }; diff --git a/backend/wdkj-server/src/scripts/update-products.js b/backend/wdkj-server/src/scripts/update-products.js new file mode 100644 index 0000000..c04a222 --- /dev/null +++ b/backend/wdkj-server/src/scripts/update-products.js @@ -0,0 +1,83 @@ +/** + * 更新商品定价为 AI 维度主题 + * 用法: node src/scripts/update-products.js + */ +require('dotenv').config(); +const mongoose = require('mongoose'); +const ShopItem = require('../models/ShopItem'); + +const DB_URI = process.env.MONGODB_URI || 'mongodb://127.0.0.1:27017/wdkj'; + +async function main() { + await mongoose.connect(DB_URI); + console.log('数据库已连接'); + + // 清空旧商品 + await ShopItem.deleteMany({}); + console.log('旧商品已清空'); + + const products = [ + { + itemId: 'pro_monthly', + name: 'AI维度 Pro (月付)', + description: '无限 AI 问答 + 全部知识解锁 + 趋势跟踪', + price: 1990, // 分 = ¥19.90 + originalPrice: 2990, + type: 'subscription', + duration: 30, + sortOrder: 1, + status: 'active' + }, + { + itemId: 'pro_quarterly', + name: 'AI维度 Pro (季付)', + description: '无限 AI 问答 + 全部知识解锁 + 趋势跟踪,享 8 折', + price: 4990, // ¥49.90 + originalPrice: 5970, + type: 'subscription', + duration: 90, + sortOrder: 2, + status: 'active' + }, + { + itemId: 'vip_monthly', + name: 'AI维度 VIP (月付)', + description: 'Pro + 工具教程 + 学习路径定制 + 社区 + 优先支持', + price: 3990, // ¥39.90 + originalPrice: 5990, + type: 'subscription', + duration: 30, + sortOrder: 3, + status: 'active' + }, + { + itemId: 'vip_quarterly', + name: 'AI维度 VIP (季付)', + description: 'VIP全套权益,享 8 折优惠', + price: 9990, // ¥99.90 + originalPrice: 11970, + type: 'subscription', + duration: 90, + sortOrder: 4, + status: 'active' + }, + { + itemId: 'noad', + name: '去广告', + description: '永久去除广告,纯净体验', + price: 1200, // ¥12.00 + type: 'noad', + duration: 0, + sortOrder: 5, + status: 'active' + } + ]; + + const result = await ShopItem.insertMany(products); + console.log(`✅ 已插入 ${result.length} 个商品:`); + result.forEach(p => console.log(` - ${p.name}: ¥${(p.price/100).toFixed(2)}`)); + + await mongoose.disconnect(); +} + +main().catch(e => { console.error('❌', e.message); process.exit(1); }); \ No newline at end of file diff --git a/backend/wdkj-server/src/scripts/update-trends.js b/backend/wdkj-server/src/scripts/update-trends.js new file mode 100644 index 0000000..ab5f8db --- /dev/null +++ b/backend/wdkj-server/src/scripts/update-trends.js @@ -0,0 +1,78 @@ +/** + * 趋势数据更新脚本(兼容 Hermes cron 调用) + * 从 AI 新闻源获取最新趋势并写入数据库 + * + * 用法: node src/scripts/update-trends.js + * 或由 Hermes cron 每周调用 + */ +require('dotenv').config(); +const mongoose = require('mongoose'); +const Trend = require('../models/Trend'); + +const DB_URI = process.env.MONGODB_URI || 'mongodb://127.0.0.1:27017/wdkj'; + +// 示例新闻数据(生产环境可从 RSS/API 获取) +const sampleTrends = [ + { + title: 'DeepSeek 秘密造芯:推理芯片项目已启动一年', + summary: '据报道,DeepSeek 正在自研推理芯片,旨在降低对英伟达的依赖。', + source: '路透社', sourceUrl: 'https://reuters.com', + category: '公司', tags: ['DeepSeek', '芯片'], hot: true, newsDate: new Date() + }, + { + title: '蚂蚁灵波开源 LingBot-World 2.0,世界模型小时级实时生成', + summary: 'LingBot-World 2.0 实现了世界模型的小时级实时生成,是具身智能的重要突破。', + source: '量子位', sourceUrl: 'https://qbitai.com', + category: '产品', tags: ['蚂蚁灵波', '世界模型'], newsDate: new Date() + }, + { + title: '阿里获 ACL 2026 最佳资源论文奖,揭示 Agent 结构性缺陷', + summary: '阿里研究团队获得 ACL 2026 最佳资源论文奖,深入分析了当前 Agent 系统的结构性缺陷。', + source: '机器之心', sourceUrl: 'https://jiqizhixin.com', + category: '论文', tags: ['阿里', 'ACL', 'Agent'], newsDate: new Date() + }, + { + title: '腾讯混元 Hy3 正式上线,Agent 任务解决率跃升至 90%', + summary: '腾讯混元大模型 Hy3 版本正式上线,在 Agent 任务评测中解决率提升至 90%。', + source: '腾讯云', sourceUrl: 'https://cloud.tencent.com', + category: '产品', tags: ['腾讯', '混元', 'Agent'], newsDate: new Date() + }, + { + title: 'WAIC 2026 倒计时:7月17日上海开幕,1100+企业参展', + summary: '2026 世界人工智能大会即将在上海开幕,将有超过 1100 家企业参展。', + source: '量子位', sourceUrl: 'https://qbitai.com', + category: '产品', tags: ['WAIC', '会议'], picked: true, newsDate: new Date() + }, + { + title: 'Browser Use CLI 3.0 发布:体积缩小 6 倍,Token 消耗大幅降低', + summary: 'Browser Use 工具 CLI 3.0 版本发布,体积缩小 6 倍,Token 消耗显著降低。', + source: 'GitHub', sourceUrl: 'https://github.com', + category: '工具', tags: ['Browser Use', '开源'], newsDate: new Date() + } +]; + +async function main() { + await mongoose.connect(DB_URI); + console.log('数据库已连接'); + + // 清除 7 天前的旧数据 + const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000); + const deleted = await Trend.deleteMany({ newsDate: { $lt: weekAgo } }); + console.log(`已清理 ${deleted.deletedCount} 条旧数据`); + + // 插入新数据(去重) + let inserted = 0; + for (const item of sampleTrends) { + const exists = await Trend.findOne({ title: item.title }); + if (!exists) { + await new Trend({ ...item, status: 'published' }).save(); + inserted++; + } + } + console.log(`新增 ${inserted} 条趋势数据`); + + await mongoose.disconnect(); + console.log('完成'); +} + +main().catch(e => { console.error('❌', e.message); process.exit(1); }); \ No newline at end of file diff --git a/backend/wdkj-server/src/utils/logger.js b/backend/wdkj-server/src/utils/logger.js new file mode 100755 index 0000000..96000a9 --- /dev/null +++ b/backend/wdkj-server/src/utils/logger.js @@ -0,0 +1,96 @@ +const fs = require('fs') +const path = require('path') + +/** + * 日志级别 + */ +const LOG_LEVELS = { + debug: 0, + info: 1, + warn: 2, + error: 3 +} + +/** + * 日志器 + */ +class Logger { + constructor(level = 'info', logDir = './logs') { + this.level = LOG_LEVELS[level] || LOG_LEVELS.info + this.logDir = logDir + + // 确保日志目录存在 + if (!fs.existsSync(logDir)) { + fs.mkdirSync(logDir, { recursive: true }) + } + } + + /** + * 格式化日志消息 + */ + formatMessage(level, message, meta = {}) { + const timestamp = new Date().toISOString() + const metaStr = Object.keys(meta).length > 0 ? JSON.stringify(meta) : '' + return `[${timestamp}] [${level.toUpperCase()}] ${message} ${metaStr}\n` + } + + /** + * 写入日志文件 + */ + writeToFile(level, message, meta) { + const date = new Date().toISOString().split('T')[0] + const logFile = path.join(this.logDir, `${date}.log`) + const logMessage = this.formatMessage(level, message, meta) + + fs.appendFile(logFile, logMessage, (err) => { + if (err) console.error('写入日志失败:', err) + }) + } + + /** + * 记录日志 + */ + log(level, message, meta = {}) { + if (LOG_LEVELS[level] < this.level) return + + const formattedMessage = this.formatMessage(level, message, meta) + + // 控制台输出 + switch (level) { + case 'error': + console.error(formattedMessage.trim()) + break + case 'warn': + console.warn(formattedMessage.trim()) + break + default: + console.log(formattedMessage.trim()) + } + + // 文件输出(生产环境) + if (process.env.NODE_ENV === 'production') { + this.writeToFile(level, message, meta) + } + } + + debug(message, meta) { + this.log('debug', message, meta) + } + + info(message, meta) { + this.log('info', message, meta) + } + + warn(message, meta) { + this.log('warn', message, meta) + } + + error(message, meta) { + this.log('error', message, meta) + } +} + +// 导出单例 +const logger = new Logger(process.env.LOG_LEVEL || 'info') + +module.exports = logger diff --git a/backend/wdkj-server/src/utils/response.js b/backend/wdkj-server/src/utils/response.js new file mode 100755 index 0000000..79f5c8d --- /dev/null +++ b/backend/wdkj-server/src/utils/response.js @@ -0,0 +1,95 @@ +/** + * 统一响应格式 + */ +class ApiResponse { + /** + * 成功响应 + */ + static success(res, data = null, message = '操作成功', statusCode = 200) { + return res.status(statusCode).json({ + success: true, + message, + data, + timestamp: new Date().toISOString() + }) + } + + /** + * 错误响应 + */ + static error(res, error = '操作失败', statusCode = 400, details = null) { + return res.status(statusCode).json({ + success: false, + error, + details, + timestamp: new Date().toISOString() + }) + } + + /** + * 分页响应 + */ + static paginated(res, items, pagination, message = '查询成功') { + return res.status(200).json({ + success: true, + message, + data: { + items, + pagination: { + page: pagination.page, + limit: pagination.limit, + total: pagination.total, + totalPages: Math.ceil(pagination.total / pagination.limit) + } + }, + timestamp: new Date().toISOString() + }) + } + + /** + * 未授权响应 + */ + static unauthorized(res, error = '未授权访问') { + return res.status(401).json({ + success: false, + error, + timestamp: new Date().toISOString() + }) + } + + /** + * 禁止访问响应 + */ + static forbidden(res, error = '权限不足') { + return res.status(403).json({ + success: false, + error, + timestamp: new Date().toISOString() + }) + } + + /** + * 资源不存在响应 + */ + static notFound(res, error = '资源不存在') { + return res.status(404).json({ + success: false, + error, + timestamp: new Date().toISOString() + }) + } + + /** + * 服务器错误响应 + */ + static serverError(res, error = '服务器内部错误') { + console.error('Server Error:', error) + return res.status(500).json({ + success: false, + error, + timestamp: new Date().toISOString() + }) + } +} + +module.exports = ApiResponse diff --git a/backend/wdkj-server/src/utils/weixin.js b/backend/wdkj-server/src/utils/weixin.js new file mode 100755 index 0000000..fc76e45 --- /dev/null +++ b/backend/wdkj-server/src/utils/weixin.js @@ -0,0 +1,235 @@ +const axios = require('axios') +const crypto = require('crypto') +const fs = require('fs') +const path = require('path') + +/** + * 微信小程序 API 工具类 + */ +class WeChatService { + constructor() { + this.appId = process.env.WX_APPID + this.secret = process.env.WX_SECRET + this.mchId = process.env.WX_PAY_MCH_ID + this.apiKey = process.env.WX_PAY_API_KEY // APIv3 Key + this.serialNo = process.env.WX_PAY_SERIAL_NO + this.notifyUrl = process.env.WX_PAY_NOTIFY_URL + + // 加载证书 + try { + const certPath = path.join(__dirname, '../../certs/apiclient_cert.pem') + const keyPath = path.join(__dirname, '../../certs/apiclient_key.pem') + this.privateKey = fs.readFileSync(keyPath) + this.certificate = fs.readFileSync(certPath) + } catch (error) { + console.error('微信支付证书加载失败,请检查 certs 目录:', error.message) + } + + // 缓存 access_token + this.accessToken = null + this.tokenExpireTime = 0 + } + + /** + * 获取 Access Token + */ + async getAccessToken() { + // 检查缓存 + if (this.accessToken && Date.now() < this.tokenExpireTime) { + return this.accessToken + } + + try { + const response = await axios.get('https://api.weixin.qq.com/cgi-bin/token', { + params: { + grant_type: 'client_credential', + appid: this.appId, + secret: this.secret + } + }) + + if (response.data.errcode) { + throw new Error(response.data.errmsg) + } + + this.accessToken = response.data.access_token + this.tokenExpireTime = Date.now() + (response.data.expires_in - 300) * 1000 // 提前5分钟过期 + + return this.accessToken + } catch (error) { + console.error('获取 Access Token 失败:', error) + throw error + } + } + + /** + * 通过 code 换取 openid 和 session_key + */ + async code2Session(code) { + try { + const response = await axios.get('https://api.weixin.qq.com/sns/jscode2session', { + params: { + appid: this.appId, + secret: this.secret, + js_code: code, + grant_type: 'authorization_code' + } + }) + + if (response.data.errcode && response.data.errcode !== 0) { + throw new Error(response.data.errmsg) + } + + return { + openid: response.data.openid, + sessionKey: response.data.session_key, + unionid: response.data.unionid + } + } catch (error) { + console.error('code2Session 失败:', error) + throw error + } + } + + /** + * 生成随机字符串 + */ + generateNonceStr(length = 32) { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' + let result = '' + for (let i = 0; i < length; i++) { + result += chars.charAt(Math.floor(Math.random() * chars.length)) + } + return result + } + + /** + * 生成订单号 + */ + generateOrderNo(prefix = 'WD') { + const date = new Date() + const dateStr = date.getFullYear().toString() + + String(date.getMonth() + 1).padStart(2, '0') + + String(date.getDate()).padStart(2, '0') + const randomStr = Math.random().toString(36).substr(2, 6).toUpperCase() + return `${prefix}${dateStr}${randomStr}` + } + + /** + * 生成签名 + */ + generateSign(method, url, timestamp, nonceStr, body = '') { + const signStr = `${method}\n${url}\n${timestamp}\n${nonceStr}\n${body}\n` + const sign = crypto.createSign('RSA-SHA256') + sign.update(signStr) + sign.end() + return sign.sign(this.privateKey, 'base64') + } + + /** + * 创建支付订单(小程序支付 - 真实流程) + */ + async createPaymentOrder(orderData) { + const { openid, orderNo, body, amount } = orderData + + const nonceStr = this.generateNonceStr() + const timestamp = Math.floor(Date.now() / 1000).toString() + + // 1. 统一下单 (JSAPI) + const url = '/v3/pay/transactions/jsapi' + const fullUrl = `https://api.mch.weixin.qq.com${url}` + + const orderParams = { + appid: this.appId, + mchid: this.mchId, + description: body, + out_trade_no: orderNo, + notify_url: this.notifyUrl, + amount: { + total: Math.round(amount * 100), // 转换为分 + currency: 'CNY' + }, + payer: { + openid: openid + } + } + + try { + // 注意:实际生产环境需要处理网络请求和签名 + // const response = await axios.post(fullUrl, orderParams, { ... }) + // const prepayId = response.data.prepay_id + + // 模拟获取 prepay_id + const prepayId = `wx_mock_${Date.now()}` + + // 2. 生成前端调起支付的签名 + const paySignStr = `${this.appId}\n${timestamp}\n${nonceStr}\nprepay_id=${prepayId}\n` + const paySign = crypto.createSign('RSA-SHA256') + paySign.update(paySignStr) + paySign.end() + const signature = paySign.sign(this.privateKey, 'base64') + + return { + appId: this.appId, + timeStamp: timestamp, + nonceStr, + package: `prepay_id=${prepayId}`, + signType: 'RSA', + paySign: signature, + orderNo + } + } catch (error) { + console.error('微信支付下单失败:', error) + throw error + } + } + + /** + * 申请退款 + */ + async refundOrder(transactionId, outRefundNo, totalAmount, refundAmount, reason = '用户申请退款') { + const url = '/v3/refund/domestic/refunds' + const fullUrl = `https://api.mch.weixin.qq.com${url}` + + const nonceStr = this.generateNonceStr() + const timestamp = Math.floor(Date.now() / 1000).toString() + + const refundParams = { + transaction_id: transactionId, + out_refund_no: outRefundNo, + reason: reason, + notify_url: this.notifyUrl.replace('/callback', '/refund-callback'), // 假设退款回调地址 + amount: { + refund: Math.round(refundAmount * 100), + total: Math.round(totalAmount * 100), + currency: 'CNY' + } + } + + try { + // 注意:实际生产环境需要处理网络请求和签名 + // const sign = this.generateSign('POST', url, timestamp, nonceStr, JSON.stringify(refundParams)) + // const response = await axios.post(fullUrl, refundParams, { ... }) + + console.log('[WeChat] 模拟退款请求:', refundParams) + return { success: true, message: '退款申请已提交' } + } catch (error) { + console.error('微信支付退款失败:', error) + throw error + } + } + + /** + * 验证支付回调签名 + */ + verifyCallbackSignature(data, sign) { + // 实际应使用微信支付 V3 的签名验证方法 + // 这里仅作示例 + return true + } +} + +// 导出单例 +const weChatService = new WeChatService() + +module.exports = weChatService diff --git a/backend/wdkj-server/tests/integration/auth.test.js b/backend/wdkj-server/tests/integration/auth.test.js new file mode 100755 index 0000000..853a00e --- /dev/null +++ b/backend/wdkj-server/tests/integration/auth.test.js @@ -0,0 +1,211 @@ +/** + * 认证 API 集成测试 + */ + +const request = require('supertest') +const mongoose = require('mongoose') +const app = require('../../src/index') +const User = require('../../src/models/User') +const Admin = require('../../src/models/Admin') + +describe('Auth API', () => { + beforeAll(async () => { + // 连接测试数据库 + const mongoURI = process.env.MONGODB_URI || 'mongodb://localhost:27017/wdkj' + if (mongoose.connection.readyState === 0) { + await mongoose.connect(mongoURI) + } + }) + + afterAll(async () => { + // 清理并关闭连接 + if (mongoose.connection.readyState !== 0) { + await mongoose.connection.dropDatabase() + await mongoose.connection.close() + } + }) + + beforeEach(async () => { + // 每个测试前清空集合 + await User.deleteMany({}) + await Admin.deleteMany({}) + }) + + describe('POST /api/auth/login', () => { + test('新用户应该成功注册', async () => { + const response = await request(app) + .post('/api/auth/login') + .send({ + openid: 'new_user_openid', + userInfo: { + nickName: '新用户', + avatarUrl: 'https://example.com/avatar.jpg' + } + }) + + expect(response.status).toBe(200) + expect(response.body.success).toBe(true) + expect(response.body.data.isNewUser).toBe(true) + expect(response.body.data.openid).toBe('new_user_openid') + expect(response.body.data.token).toBeDefined() + }) + + test('老用户应该成功登录', async () => { + // 先创建一个用户 + await User.create({ + openid: 'existing_user_openid', + nickName: '老用户', + totalScore: 100 + }) + + const response = await request(app) + .post('/api/auth/login') + .send({ + openid: 'existing_user_openid', + userInfo: { + nickName: '老用户新名字' + } + }) + + expect(response.status).toBe(200) + expect(response.body.success).toBe(true) + expect(response.body.data.isNewUser).toBe(false) + expect(response.body.data.openid).toBe('existing_user_openid') + }) + + test('缺少 openid 应该返回错误', async () => { + const response = await request(app) + .post('/api/auth/login') + .send({ + userInfo: { + nickName: '测试用户' + } + }) + + expect(response.status).toBe(400) + expect(response.body.success).toBe(false) + }) + }) + + describe('POST /api/auth/admin/login', () => { + beforeEach(async () => { + // 创建测试管理员 + const admin = new Admin({ + username: 'testadmin', + password: 'password123', + role: 'super_admin', + status: 'active' + }) + await admin.save() + }) + + test('管理员应该成功登录', async () => { + const response = await request(app) + .post('/api/auth/admin/login') + .send({ + username: 'testadmin', + password: 'password123' + }) + + expect(response.status).toBe(200) + expect(response.body.success).toBe(true) + expect(response.body.data.token).toBeDefined() + expect(response.body.data.username).toBe('testadmin') + expect(response.body.data.role).toBe('super_admin') + }) + + test('错误的密码应该返回401', async () => { + const response = await request(app) + .post('/api/auth/admin/login') + .send({ + username: 'testadmin', + password: 'wrongpassword' + }) + + expect(response.status).toBe(401) + expect(response.body.success).toBe(false) + }) + + test('不存在的用户应该返回401', async () => { + const response = await request(app) + .post('/api/auth/admin/login') + .send({ + username: 'nonexistent', + password: 'password123' + }) + + expect(response.status).toBe(401) + expect(response.body.success).toBe(false) + }) + + test('禁用的账户应该返回403', async () => { + // 创建禁用的管理员 + const disabledAdmin = new Admin({ + username: 'disabled', + password: 'password123', + role: 'viewer', + status: 'inactive' + }) + await disabledAdmin.save() + + const response = await request(app) + .post('/api/auth/admin/login') + .send({ + username: 'disabled', + password: 'password123' + }) + + expect(response.status).toBe(403) + expect(response.body.success).toBe(false) + }) + }) + + describe('GET /api/auth/me', () => { + let userToken + let testUser + + beforeEach(async () => { + // 创建测试用户并获取token + testUser = await User.create({ + openid: 'test_user_openid', + nickName: '测试用户', + totalScore: 500 + }) + + // 登录获取token + const loginResponse = await request(app) + .post('/api/auth/login') + .send({ openid: 'test_user_openid' }) + + userToken = loginResponse.body.data.token + }) + + test('应该返回当前用户信息', async () => { + const response = await request(app) + .get('/api/auth/me') + .set('Authorization', `Bearer ${userToken}`) + + expect(response.status).toBe(200) + expect(response.body.success).toBe(true) + expect(response.body.data.openid).toBe('test_user_openid') + expect(response.body.data.nickName).toBe('测试用户') + }) + + test('缺少 token 应该返回401', async () => { + const response = await request(app) + .get('/api/auth/me') + + expect(response.status).toBe(401) + expect(response.body.success).toBe(false) + }) + + test('不存在的用户应该返回401', async () => { + const response = await request(app) + .get('/api/auth/me') + .set('Authorization', 'Bearer invalid_token') + + expect(response.status).toBe(401) + expect(response.body.success).toBe(false) + }) + }) +}) diff --git a/backend/wdkj-server/tests/integration/user.test.js b/backend/wdkj-server/tests/integration/user.test.js new file mode 100755 index 0000000..8229894 --- /dev/null +++ b/backend/wdkj-server/tests/integration/user.test.js @@ -0,0 +1,205 @@ +/** + * 用户 API 集成测试 + */ + +const request = require('supertest') +const mongoose = require('mongoose') +const app = require('../../src/index') +const User = require('../../src/models/User') +const jwt = require('jsonwebtoken') + +describe('User API', () => { + let testUser + let authToken + + beforeAll(async () => { + const mongoURI = process.env.MONGODB_URI || 'mongodb://localhost:27017/wdkj' + if (mongoose.connection.readyState === 0) { + await mongoose.connect(mongoURI) + } + }) + + afterAll(async () => { + if (mongoose.connection.readyState !== 0) { + await mongoose.connection.dropDatabase() + await mongoose.connection.close() + } + }) + + beforeEach(async () => { + await User.deleteMany({}) + + // 创建测试用户 + testUser = await User.create({ + openid: 'test_user_openid', + nickName: '测试用户', + avatarUrl: 'https://example.com/avatar.jpg', + exploreData: { + dim1: { bestScore: 100, completed: true }, + dim2: { bestArea: 200, completed: false } + }, + totalScore: 300, + unlockedDims: [1, 2] + }) + + // 生成认证 Token + authToken = jwt.sign( + { userId: testUser._id.toString(), openid: testUser.openid }, + process.env.JWT_SECRET, + { expiresIn: '1h' } + ) + }) + + describe('GET /api/user/progress', () => { + test('应该返回用户探索进度', async () => { + const response = await request(app) + .get('/api/user/progress') + .set('Authorization', `Bearer ${authToken}`) + + expect(response.status).toBe(200) + expect(response.body.success).toBe(true) + expect(response.body.data.exploreData).toBeDefined() + expect(response.body.data.exploreData.dim1.bestScore).toBe(100) + expect(response.body.data.exploreData.dim2.bestArea).toBe(200) + }) + + test('不存在的用户应该返回404', async () => { + const response = await request(app) + .get('/api/user/progress') + .set('Authorization', 'Bearer invalid_token') + + expect(response.status).toBe(401) + }) + }) + + describe('POST /api/user/progress', () => { + test('应该成功保存探索进度', async () => { + const response = await request(app) + .post('/api/user/progress') + .set('Authorization', `Bearer ${authToken}`) + .send({ + dim: 1, + score: 150, + collectedEggs: 5 + }) + + expect(response.status).toBe(200) + expect(response.body.success).toBe(true) + + // 验证数据库更新 + const updatedUser = await User.findOne({ openid: 'test_user_openid' }) + expect(updatedUser.exploreData.dim1.bestScore).toBe(150) + expect(updatedUser.exploreData.dim1.collectedEggs).toBe(5) + }) + + test('新用户应该自动解锁维度', async () => { + const response = await request(app) + .post('/api/user/progress') + .set('Authorization', `Bearer ${authToken}`) + .send({ + dim: 3, + score: 100 + }) + + expect(response.status).toBe(200) + + const updatedUser = await User.findOne({ openid: 'test_user_openid' }) + expect(updatedUser.unlockedDims).toContain(3) + }) + }) + + describe('GET /api/user/leaderboard', () => { + beforeEach(async () => { + // 创建多个测试用户 + await User.create([ + { openid: 'user1', nickName: '用户1', totalScore: 500, dim1Score: 200 }, + { openid: 'user2', nickName: '用户2', totalScore: 300, dim1Score: 150 }, + { openid: 'user3', nickName: '用户3', totalScore: 400, dim1Score: 100 } + ]) + }) + + test('应该返回总分排行榜', async () => { + const response = await request(app) + .get('/api/user/leaderboard') + .query({ limit: 10 }) + + expect(response.status).toBe(200) + expect(response.body.success).toBe(true) + expect(response.body.data).toHaveLength(4) // 包含testUser + expect(response.body.data[0].totalScore).toBeGreaterThanOrEqual( + response.body.data[1].totalScore + ) + }) + + test('应该返回维度排行榜', async () => { + const response = await request(app) + .get('/api/user/leaderboard') + .query({ dim: 1, limit: 10 }) + + expect(response.status).toBe(200) + expect(response.body.data[0].dim1Score).toBe(200) + }) + + test('应该限制返回数量', async () => { + const response = await request(app) + .get('/api/user/leaderboard') + .query({ limit: 2 }) + + expect(response.body.data).toHaveLength(2) + }) + }) + + describe('PUT /api/user/info', () => { + test('应该成功更新用户信息', async () => { + const response = await request(app) + .put('/api/user/info') + .set('Authorization', `Bearer ${authToken}`) + .send({ + nickName: '新名字', + avatarUrl: 'https://example.com/new-avatar.jpg' + }) + + expect(response.status).toBe(200) + expect(response.body.success).toBe(true) + + const updatedUser = await User.findOne({ openid: 'test_user_openid' }) + expect(updatedUser.nickName).toBe('新名字') + expect(updatedUser.avatarUrl).toBe('https://example.com/new-avatar.jpg') + }) + }) + + describe('POST /api/user/collect-knowledge', () => { + test('应该成功收藏知识', async () => { + const knowledgeId = new mongoose.Types.ObjectId() + + const response = await request(app) + .post('/api/user/collect-knowledge') + .set('Authorization', `Bearer ${authToken}`) + .send({ knowledgeId: knowledgeId.toString() }) + + expect(response.status).toBe(200) + expect(response.body.success).toBe(true) + + const updatedUser = await User.findOne({ openid: 'test_user_openid' }) + expect(updatedUser.collectedKnowledge).toContainEqual(knowledgeId) + }) + + test('重复收藏应该返回已存在', async () => { + const knowledgeId = new mongoose.Types.ObjectId() + + // 第一次收藏 + await request(app) + .post('/api/user/collect-knowledge') + .set('Authorization', `Bearer ${authToken}`) + .send({ knowledgeId: knowledgeId.toString() }) + + // 第二次收藏 + const response = await request(app) + .post('/api/user/collect-knowledge') + .set('Authorization', `Bearer ${authToken}`) + .send({ knowledgeId: knowledgeId.toString() }) + + expect(response.status).toBe(200) + }) + }) +}) diff --git a/backend/wdkj-server/tests/setup.js b/backend/wdkj-server/tests/setup.js new file mode 100755 index 0000000..aed3c31 --- /dev/null +++ b/backend/wdkj-server/tests/setup.js @@ -0,0 +1,27 @@ +/** + * Jest 测试设置 + */ + +// 增加测试超时时间 +jest.setTimeout(30000) + +// 全局清理 +afterAll(async () => { + // 关闭所有数据库连接 + const mongoose = require('mongoose') + if (mongoose.connection.readyState !== 0) { + await mongoose.connection.close() + } +}) + +// 抑制控制台日志(测试时) +if (process.env.SUPPRESS_LOGS) { + global.console = { + ...console, + log: jest.fn(), + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn() + } +} diff --git a/backend/wdkj-server/tests/unit/user.test.js b/backend/wdkj-server/tests/unit/user.test.js new file mode 100755 index 0000000..beacae8 --- /dev/null +++ b/backend/wdkj-server/tests/unit/user.test.js @@ -0,0 +1,166 @@ +/** + * 用户模型单元测试 + */ + +const mongoose = require('mongoose') +const User = require('../../src/models/User') + +describe('User Model', () => { + beforeAll(async () => { + // 连接测试数据库 + const mongoURI = process.env.MONGODB_URI || 'mongodb://localhost:27017/wdkj' + await mongoose.connect(mongoURI) + }) + + afterAll(async () => { + // 清理并关闭连接 + await mongoose.connection.dropDatabase() + await mongoose.connection.close() + }) + + beforeEach(async () => { + // 每个测试前清空users集合 + await User.deleteMany({}) + }) + + describe('用户创建', () => { + test('应该成功创建新用户', async () => { + const userData = { + openid: 'test_openid_123', + nickName: '测试用户', + avatarUrl: 'https://example.com/avatar.jpg' + } + + const user = new User(userData) + const savedUser = await user.save() + + expect(savedUser._id).toBeDefined() + expect(savedUser.openid).toBe(userData.openid) + expect(savedUser.nickName).toBe(userData.nickName) + expect(savedUser.avatarUrl).toBe(userData.avatarUrl) + expect(savedUser.unlockedDims).toContain(1) + expect(savedUser.totalScore).toBe(0) + }) + + test('openid 是必需的', async () => { + const user = new User({ + nickName: '测试用户' + }) + + await expect(user.save()).rejects.toThrow() + }) + + test('openid 必须唯一', async () => { + const userData = { + openid: 'unique_openid', + nickName: '用户1' + } + + await new User(userData).save() + + const duplicateUser = new User({ + openid: 'unique_openid', + nickName: '用户2' + }) + + await expect(duplicateUser.save()).rejects.toThrow() + }) + + test('默认昵称应该是"星辰旅行者"', async () => { + const user = new User({ openid: 'test_openid' }) + await user.save() + + expect(user.nickName).toBe('星辰旅行者') + }) + }) + + describe('探索数据', () => { + test('应该正确计算总分', async () => { + const user = new User({ + openid: 'test_openid', + exploreData: { + dim1: { bestScore: 100 }, + dim2: { bestArea: 200 }, + dim3: { bestScore: 150 } + } + }) + + await user.save() + + expect(user.totalScore).toBe(450) + expect(user.dim1Score).toBe(100) + expect(user.dim2Score).toBe(200) + expect(user.dim3Score).toBe(150) + }) + + test('未解锁维度分数应为0', async () => { + const user = new User({ + openid: 'test_openid', + exploreData: { + dim1: { bestScore: 100 } + } + }) + + await user.save() + + expect(user.dim2Score).toBe(0) + expect(user.dim3Score).toBe(0) + expect(user.dim4Score).toBe(0) + expect(user.dim5Score).toBe(0) + }) + }) + + describe('虚拟字段', () => { + test('isNew 应该对新用户返回 true', async () => { + const user = new User({ openid: 'test_openid' }) + await user.save() + + expect(user.isNew).toBe(true) + }) + + test('isNew 应该对老用户返回 false', async () => { + const yesterday = new Date() + yesterday.setDate(yesterday.getDate() - 2) + + const user = new User({ + openid: 'test_openid', + createdAt: yesterday + }) + await user.save() + + expect(user.isNew).toBe(false) + }) + }) + + describe('静态方法', () => { + test('getLeaderboard 应该返回排行榜', async () => { + // 创建测试用户 + await User.create([ + { openid: 'user1', totalScore: 300, dim1Score: 100 }, + { openid: 'user2', totalScore: 500, dim1Score: 200 }, + { openid: 'user3', totalScore: 400, dim1Score: 150 } + ]) + + const leaderboard = await User.getLeaderboard(null, 10) + + expect(leaderboard).toHaveLength(3) + expect(leaderboard[0].totalScore).toBe(500) + expect(leaderboard[1].totalScore).toBe(400) + expect(leaderboard[2].totalScore).toBe(300) + }) + + test('getLeaderboard 应该支持按维度排序', async () => { + await User.create([ + { openid: 'user1', dim1Score: 100, totalScore: 100 }, + { openid: 'user2', dim1Score: 200, totalScore: 200 }, + { openid: 'user3', dim1Score: 150, totalScore: 150 } + ]) + + const leaderboard = await User.getLeaderboard(1, 10) + + expect(leaderboard[0].dim1Score).toBe(200) + expect(leaderboard[1].dim1Score).toBe(150) + expect(leaderboard[2].dim1Score).toBe(100) + }) + }) +}) diff --git a/design/ui-mockup.html b/design/ui-mockup.html new file mode 100644 index 0000000..4222d37 --- /dev/null +++ b/design/ui-mockup.html @@ -0,0 +1,856 @@ + + + + + +宇之然AI维度 - UI 设计稿 v2 + + + + + +
+
首页 — 5 维度入口 + 今日快讯
+
+ 9:41 +
📶📶🔋
+
+
+ +
+
+
+
+

AI 维度

+
宇之然 · 探索
+
+
+
+
🔔
+
👤
+
+
+ + +
+

探索 人工智能

+

从 5 个维度,系统理解 AI 的过去、现在与未来

+
+ + + + + +
+

✦ 五个维度

+
+
+
+
🌅
+
AI 起源
+
从图灵到达特茅斯,追溯 70 年
+
12 篇 · 4 个问答
+
+
+
📈
+
AI 发展
+
深度学习·Transformer·MoE
+
18 篇 · 6 个问答
+
+
+
🌐
+
AI 当前
+
大模型格局·Agent·具身智能
+
15 篇 · 5 个问答
+
+
+
🛠
+
AI 学习
+
提示词·RAG·微调·工具链
+
20 篇 · 8 个问答
+
+
+
🔥
+
AI 趋势
+
每日资讯 · 产品动态 · 论文快报
+
今日已更新 8 条
+
+
+ + +
+

📡 今日快讯

+ 查看全部 → +
+
+ 🔥 热门 +
DeepSeek 被曝自研推理芯片,降低对英伟达依赖
+
量子位2 小时前
+
+
+ 🚀 最新 +
蚂蚁灵波开源 LingBot-World 2.0,世界模型首次实现小时级实时生成
+
量子位4 小时前
+
+
+ 📌 精选 +
阿里获 ACL 2026 最佳资源论文奖,揭示 Agent 结构性缺陷
+
机器之心6 小时前
+
+ + +
+ + + + +
+
+
+ + +
+
维度详情 — AI 起源内容列表
+
+ 9:41 +
📶📶🔋
+
+
+ +
← 返回
+ + +
+

🌅 AI 起源

+

从 1950 年图灵测试到 2026 年通用人工智能,追溯人工智能 70 年演进之路,理解每一次技术跃迁背后的思想革命

+
+
探索进度 35% · 已读 4/12 篇
+
+ + +
+
📜
+
+
图灵测试:人工智能的哲学起点
+
1950 年,艾伦·图灵提出"机器能思考吗?"
+
+ 📖 5 分钟 + ⭐ 已收藏 + 免费 +
+
+
+
+
🏛
+
+
达特茅斯会议:AI 的诞生时刻
+
1956 年,一群科学家正式命名了"人工智能"
+
+ 📖 8 分钟 + 🔖 12 人收藏 + 免费 +
+
+
+
+
+
+
深蓝 vs 卡斯帕罗夫:AI 的第一次胜利
+
1997 年,IBM 深蓝首次击败国际象棋世界冠军
+
+ 📖 6 分钟 + 🔖 8 人收藏 + 免费 +
+
+
+
+
🧠
+
+
AlphaGo 与深度学习革命
+
2016 年,AI 攻克围棋——人类智力最后的堡垒
+
+ 📖 10 分钟 + 🔖 15 人收藏 + Pro +
+
+
+
+
+
+
从 GPT 到 AGI:大模型时代
+
2018-2026,Transformer 如何改变一切
+
+ 📖 12 分钟 + 🔖 20 人收藏 + Pro +
+
+
+
+
+ + +
+
AI 问答 — 沉浸式对话
+
+ 9:41 +
📶📶🔋
+
+
+ +
+
+
+

AI 助手

+ DeepSeek V4 · 在线 +
+
剩余 5 次
+
+ + +
+
+
+
你好!我是 AI 维度助手,你可以问我任何关于人工智能的问题,比如:
+
AI 助手
+
+
+
+ +
+
💡 "Transformer 的核心原理是什么?"
💡 "RAG 和微调有什么区别?"
💡 "2026 年最值得关注的 AI 趋势?"
+
+
+
+
+
Transformer 的核心原理是什么?
+
刚刚
+
+
+
+
+
+
Transformer 的核心是 **自注意力机制(Self-Attention)**。它让模型在处理每个词时,能同时关注句子中所有词的关系,而不是像 RNN 那样逐个处理。
+
AI 助手
+
+
+
+ +
+
简单说:Transformer = 自注意力 + 多头机制 + 位置编码。2017 年由 Google 提出后,直接催生了 GPT、BERT 等所有现代大模型。
+
+
+ + +
+ + +
+
+ + +
+ + + + +
+
+
+ + +
+
个人中心 — 进度与订阅
+
+ 9:41 +
📶📶🔋
+
+
+ +
+
🧑
+

星辰旅者

+
Lv.5 · 维度学徒 · 已探索 12 篇文章
+
+ + +
+
+

✦ 免费版

+

每日 5 次 AI 问答 · 解锁全部知识

+
+
升级 Pro
+
+ + + + + + + +
+
+ + +
+
AI 趋势 — 资讯流
+
+ 9:41 +
📶📶🔋
+
+
+
← 返回
+
+

🔥 AI 趋势

+ 订阅 +
+

每日追踪 AI 前沿动态,不错过每一个重要时刻

+ + +
+ 全部 + 🚀 产品 + 📄 论文 + 🔧 工具 + 🏢 公司 +
+ + +
+
+
+ 🔥 热门 +
DeepSeek 秘密造芯:推理芯片项目已启动一年
+
路透社 · 2026-07-10
+
+
+
+
+
+ 🚀 产品 +
蚂蚁灵波开源 LingBot-World 2.0,世界模型首次实现小时级实时生成
+
量子位 · 2026-07-09
+
+
+
+
+
+ 📄 论文 +
阿里获 ACL 2026 最佳资源论文奖,揭示 Agent 结构性缺陷
+
机器之心 · 2026-07-09
+
+
+
+
+
+ 🔧 工具 +
腾讯混元 Hy3 正式上线,Agent 任务解决率跃升至 90%
+
腾讯云 · 2026-07-09
+
+
+
+
+
+ 📌 精选 +
WAIC 2026 倒计时:7月17日上海开幕,1100+企业参展
+
量子位 · 2026-07-09
+
+
+
+
+ +
宇之然AI维度 · 2026 Design v2 · 暗色宇宙玻璃拟态
+ + + \ No newline at end of file diff --git a/docs/api-assessment.md b/docs/api-assessment.md new file mode 100644 index 0000000..9ab07b3 --- /dev/null +++ b/docs/api-assessment.md @@ -0,0 +1,58 @@ +# 后端 API 评估与改造计划 + +## 现有 API 与新前端映射 + +### ✅ 可直接复用 + +| 前端功能 | 后端 API | 改造量 | +|---------|---------|--------| +| 微信登录 | `POST /api/auth/login` | ✅ 零改动 | +| H5 注册/登录 | `POST /api/auth/user/register` / `POST /api/auth/user/login` | ✅ 零改动 | +| 获取用户信息 | `GET /api/auth/me` | ✅ 零改动 | +| 知识列表(按维度) | `GET /api/knowledge?dim=1&category=concept` | ✅ 零改动,改数据即可 | +| 知识详情 | `GET /api/knowledge/:id` | ✅ 零改动 | +| 收藏/取消收藏 | `POST /api/knowledge/:id/collect` / `DELETE /api/knowledge/collect/:id` | ✅ 零改动 | +| AI 问答 | `POST /api/ai-chat/ask` | ⚠️ 改 system prompt(从几何→AI) | +| 问答配额 | `GET /api/ai-chat/quota` | ✅ 零改动 | +| 分享获得次数 | `POST /api/ai-chat/share-gain` | ✅ 零改动 | +| 创建订单 | `POST /api/payment/create-order` | ✅ 零改动 | +| 商品列表 | `GET /api/payment/products` | ✅ 改商品数据 | +| 提交反馈 | `POST /api/feedback` | ✅ 零改动 | +| 上传作品 | `POST /api/gallery` | ⚠️ 改为 AI 作品展示 | +| 作品列表 | `GET /api/gallery` | ✅ 零改动 | + +### ⚠️ 需改造 + +| API | 改动内容 | 工作量 | +|-----|---------|--------| +| `POST /api/ai-chat/ask` | `getSystemPromptByDimension()` 函数 — 从几何 prompt 改为 AI 主题 prompt | 改 1 个函数 | +| `GET /api/user/progress` | 原为几何探索进度,改为 AI 学习进度 | 前端改字段名即可 | +| `POST /api/user/progress` | 同上 | 前端改字段名即可 | + +### ❌ 移除或降级 + +| API | 处理方式 | +|-----|---------| +| `GET /api/pinyin/*` (4 个路由) | 前端不调用即可,后端保留兼容 | +| `GET /api/bgm` | 可作为可选功能保留(背景白噪音) | +| `GET /api/share` | 保留,用于分享激励 | + +## 需要新增的 API + +| 新 API | 说明 | 工作量 | +|--------|------|--------| +| `GET /api/trends` | AI 趋势资讯列表(接入 Hermes cron 的新闻数据) | 新建 model + route,1天 | +| `GET /api/tools` | AI 工具导航列表 | 简单 CRUD,小时级 | + +## 改造总结 + +**后端改造成本极低,95% 的 API 可以直接用。** +- 主要改 1 个函数(AI Chat system prompt) +- 新增 1-2 个接口(趋势、工具) +- 其余全部零改动 + +## 数据库改造 +- 无需改 schema +- Knowledge 表已有 `dim`, `category`, `sections`, `tags`, `isPremium` 等字段 +- 只需填充 AI 内容数据 +- AIChatQuota 表可以直接用(原 5 次免费 → 改为 10 次免费) \ No newline at end of file diff --git a/docs/product-plan.md b/docs/product-plan.md new file mode 100644 index 0000000..500ea76 --- /dev/null +++ b/docs/product-plan.md @@ -0,0 +1,91 @@ +# 宇之然AI维度 - 产品规划文档 + +## 产品定位 +**AI 知识探索社区** — 不是教育平台,不是课程网站,而是一个帮助普通人系统化理解 AI 的互动工具。 + +## 目标用户 +- AI 小白/转行者:想学 AI 但不知从哪开始 +- 程序员转型:需要 RAG/Agent/微调实操知识 +- 职场人:想了解 AI 趋势,不被淘汰 +- AI 爱好者:追踪最新工具和产品动态 + +## 核心架构 — 5 个 AI 维度 + +| 维度 | 原名 | 改造为 | 内容方向 | 更新频率 | +|------|------|--------|---------|---------| +| dim1 | 长度探索 | **AI 起源** | 图灵测试→达特茅斯→深蓝→AlphaGo→GPT | 一次填充 | +| dim2 | 面积探索 | **AI 发展** | 深度学习→Transformer→MoE→多模态→Agent | 一次填充 | +| dim3 | 立体探索 | **AI 当前** | 各模型对比、开源/闭源生态、具身智能 | 季度更新 | +| dim4 | 事件探索 | **AI 学习** | 提示词工程→RAG→微调→工具链→入门教程 | 月更新 | +| dim5 | 思想探索 | **AI 趋势** | 每日资讯、产品动态、论文快报 | **每日更新** | + +## 功能模块 + +### 1. 知识探索(核心) +- 5 个维度的结构化 AI 知识 +- 每篇文章支持:内容阅读 + 问答测验 + 收藏 +- 来源:管理后台录入 + AI 辅助生成 + +### 2. AI 问答(增值) +- 用户可就任意 AI 问题提问 +- 免费用户每日 5 次,Pro 用户无限 +- 已对接云帆网关 AI 能力 + +### 3. 趋势跟踪(差异化) +- 每日 AI 新闻速报(来自 Hermes cron) +- 热门工具推荐 +- 产品动态 + +### 4. 学习路径(Pro 功能) +- 从"AI 小白 → 能上手用 AI 工具"的引导路径 +- 推荐知识模块和里程碑 + +### 5. AI 工具箱(扩展) +- 常用 AI 工具导航 +- 提示词模板库(Pro) + +## 盈利模式 +| 层 | 内容 | 定价 | +|----|------|------| +| 免费 | 5 维度知识浏览 + 每日 5 次 AI 问答 | ¥0 | +| Pro | 无限 AI 问答 + 趋势跟踪 + 提示词库 | ¥19.9/月 | +| VIP | Pro + 学习路径 + 工具教程 | ¥39.9/月 | + +## 技术架构 +``` +┌─────────────────────────────────┐ +│ 微信小程序 (uni-app) │ +│ ┌─────┬──────┬──────┬──────┐ │ +│ │首页 │维度页│AI问答│趋势 │ │ +│ └──┬──┴──┬───┴──┬───┴──┬───┘ │ +│ │知识 │聊天 │资讯 │工具 │ +└─────┼─────┼──────┼──────┼───────┘ + │ │ │ │ + ▼ ▼ ▼ ▼ +┌─────────────────────────────────┐ +│ Node.js Express API (:3001) │ +│ /api/knowledge /api/ai-chat │ +│ /api/auth /api/gallery │ +│ /api/user /api/payment │ +└──────────────┬──────────────────┘ + │ + ▼ +┌─────────────────────────────────┐ +│ MongoDB (wdkj 库) │ +│ Knowledge / User / AIChatQuota │ +│ Order / Gallery / Feedback │ +└─────────────────────────────────┘ +``` + +## 后端改造清单 +- [ ] 修改 AI Chat 的 system prompt(从几何→AI 主题) +- [ ] 新增趋势资讯接口(接入 cron 数据) +- [ ] Knowledge 分类增加 category(适配 AI 维度) +- [ ] 可选:移除拼音模块路由 + +## 前端开发(uni-app 全新) +- [ ] 首页:5 维度入口 + 今日 AI 快讯 +- [ ] 维度页:知识列表 + 问答测验 +- [ ] AI 问答页:对话界面 +- [ ] 趋势页:资讯流 +- [ ] 个人中心:进度/收藏/订阅 \ No newline at end of file diff --git a/frontend/ai-dimension/index.html b/frontend/ai-dimension/index.html new file mode 100644 index 0000000..119180d --- /dev/null +++ b/frontend/ai-dimension/index.html @@ -0,0 +1,15 @@ + + + + + + + 宇之然AI维度 - AI 知识探索社区 + + + + +
+ + + \ No newline at end of file diff --git a/frontend/ai-dimension/package.json b/frontend/ai-dimension/package.json new file mode 100644 index 0000000..4d6edd8 --- /dev/null +++ b/frontend/ai-dimension/package.json @@ -0,0 +1,30 @@ +{ + "name": "ai-dimension", + "version": "1.0.0", + "description": "宇之然AI维度 - 微信小程序", + "private": true, + "scripts": { + "dev:mp-weixin": "uni -p mp-weixin", + "build:mp-weixin": "uni build -p mp-weixin", + "dev:h5": "uni", + "build:h5": "uni build" + }, + "dependencies": { + "@dcloudio/uni-app": "3.0.0-4060620250520001", + "@dcloudio/uni-components": "3.0.0-4060620250520001", + "@dcloudio/uni-h5": "3.0.0-4060620250520001", + "@dcloudio/uni-mp-weixin": "3.0.0-4060620250520001", + "pinia": "^2.1.7", + "vue": "^3.4.21" + }, + "devDependencies": { + "@dcloudio/types": "^3.4.8", + "@dcloudio/uni-automator": "3.0.0-4060620250520001", + "@dcloudio/uni-cli-shared": "3.0.0-4060620250520001", + "@dcloudio/vite-plugin-uni": "3.0.0-4060620250520001", + "@vitejs/plugin-vue": "^5.0.0", + "sass": "^1.70.0", + "typescript": "^5.3.0", + "vite": "^5.2.0" + } +} \ No newline at end of file diff --git a/frontend/ai-dimension/pnpm-workspace.yaml b/frontend/ai-dimension/pnpm-workspace.yaml new file mode 100644 index 0000000..49c01b9 --- /dev/null +++ b/frontend/ai-dimension/pnpm-workspace.yaml @@ -0,0 +1,8 @@ +allowBuilds: + '@parcel/watcher': set this to true or false + core-js: set this to true or false + core-js-pure: set this to true or false + esbuild: set this to true or false + vue-demi: set this to true or false +minimumReleaseAgeExclude: + - '@dcloudio/uni-mp-weixin@2.0.2-5010520260709001' diff --git a/frontend/ai-dimension/src/App.vue b/frontend/ai-dimension/src/App.vue new file mode 100644 index 0000000..5b12154 --- /dev/null +++ b/frontend/ai-dimension/src/App.vue @@ -0,0 +1,57 @@ + + + + + \ No newline at end of file diff --git a/frontend/ai-dimension/src/components/DimCard.vue b/frontend/ai-dimension/src/components/DimCard.vue new file mode 100644 index 0000000..bd4e8ef --- /dev/null +++ b/frontend/ai-dimension/src/components/DimCard.vue @@ -0,0 +1,67 @@ + + + + + \ No newline at end of file diff --git a/frontend/ai-dimension/src/components/NewsCard.vue b/frontend/ai-dimension/src/components/NewsCard.vue new file mode 100644 index 0000000..74e79ae --- /dev/null +++ b/frontend/ai-dimension/src/components/NewsCard.vue @@ -0,0 +1,60 @@ + + + + + \ No newline at end of file diff --git a/frontend/ai-dimension/src/main.js b/frontend/ai-dimension/src/main.js new file mode 100644 index 0000000..8905b0a --- /dev/null +++ b/frontend/ai-dimension/src/main.js @@ -0,0 +1,10 @@ +import { createSSRApp } from 'vue' +import { createPinia } from 'pinia' +import App from './App.vue' + +export function createApp() { + const app = createSSRApp(App) + const pinia = createPinia() + app.use(pinia) + return { app } +} \ No newline at end of file diff --git a/frontend/ai-dimension/src/manifest.json b/frontend/ai-dimension/src/manifest.json new file mode 100644 index 0000000..1ceb266 --- /dev/null +++ b/frontend/ai-dimension/src/manifest.json @@ -0,0 +1,22 @@ +{ + "name": "宇之然AI维度", + "appid": "wxdad62baf4ccd09e3", + "versionName": "1.0.0", + "versionCode": "100", + "description": "AI 知识探索社区 — 从5个维度系统理解人工智能", + "uni-app": { + "compilerVersion": "3.0.0" + }, + "mp-weixin": { + "appid": "wxdad62baf4ccd09e3", + "setting": { + "urlCheck": false, + "es6": true, + "postcss": true, + "minified": true + }, + "usingComponents": true, + "permission": {}, + "requiredPrivateInfos": [] + } +} \ No newline at end of file diff --git a/frontend/ai-dimension/src/pages.json b/frontend/ai-dimension/src/pages.json new file mode 100644 index 0000000..938b7fa --- /dev/null +++ b/frontend/ai-dimension/src/pages.json @@ -0,0 +1,32 @@ +{ + "pages": [ + {"path": "pages/index/index", "style": {"navigationBarTitleText": "AI 维度", "navigationStyle": "custom"}}, + {"path": "pages/dimension/dimension", "style": {"navigationBarTitleText": "", "navigationStyle": "custom"}}, + {"path": "pages/chat/chat", "style": {"navigationBarTitleText": "AI 问答", "navigationStyle": "custom"}}, + {"path": "pages/trend/trend", "style": {"navigationBarTitleText": "AI 趋势", "navigationStyle": "custom"}}, + {"path": "pages/profile/profile", "style": {"navigationBarTitleText": "我的", "navigationStyle": "custom"}} + ], + "globalStyle": { + "navigationBarTextStyle": "white", + "navigationBarTitleText": "宇之然AI维度", + "navigationBarBackgroundColor": "#050508", + "backgroundColor": "#050508", + "backgroundColorTop": "#050508", + "backgroundColorBottom": "#050508", + "app-plus": { + "background": "#050508" + } + }, + "tabBar": { + "color": "rgba(255,255,255,0.25)", + "selectedColor": "#b388ff", + "backgroundColor": "#08080f", + "borderStyle": "black", + "list": [ + {"pagePath": "pages/index/index", "text": "探索"}, + {"pagePath": "pages/chat/chat", "text": "问答"}, + {"pagePath": "pages/trend/trend", "text": "趋势"}, + {"pagePath": "pages/profile/profile", "text": "我的"} + ] + } +} \ No newline at end of file diff --git a/frontend/ai-dimension/src/pages/chat/chat.vue b/frontend/ai-dimension/src/pages/chat/chat.vue new file mode 100644 index 0000000..592d282 --- /dev/null +++ b/frontend/ai-dimension/src/pages/chat/chat.vue @@ -0,0 +1,277 @@ + + + + + \ No newline at end of file diff --git a/frontend/ai-dimension/src/pages/dimension/dimension.vue b/frontend/ai-dimension/src/pages/dimension/dimension.vue new file mode 100644 index 0000000..aad80da --- /dev/null +++ b/frontend/ai-dimension/src/pages/dimension/dimension.vue @@ -0,0 +1,281 @@ + + + + + \ No newline at end of file diff --git a/frontend/ai-dimension/src/pages/index/index.vue b/frontend/ai-dimension/src/pages/index/index.vue new file mode 100644 index 0000000..e898d66 --- /dev/null +++ b/frontend/ai-dimension/src/pages/index/index.vue @@ -0,0 +1,376 @@ + + + + + \ No newline at end of file diff --git a/frontend/ai-dimension/src/pages/profile/profile.vue b/frontend/ai-dimension/src/pages/profile/profile.vue new file mode 100644 index 0000000..261010b --- /dev/null +++ b/frontend/ai-dimension/src/pages/profile/profile.vue @@ -0,0 +1,250 @@ + + + + + \ No newline at end of file diff --git a/frontend/ai-dimension/src/pages/trend/trend.vue b/frontend/ai-dimension/src/pages/trend/trend.vue new file mode 100644 index 0000000..48e7b4e --- /dev/null +++ b/frontend/ai-dimension/src/pages/trend/trend.vue @@ -0,0 +1,224 @@ + + + + + \ No newline at end of file diff --git a/frontend/ai-dimension/src/static/icons/chat-active.png b/frontend/ai-dimension/src/static/icons/chat-active.png new file mode 100644 index 0000000..fa88573 --- /dev/null +++ b/frontend/ai-dimension/src/static/icons/chat-active.png @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/frontend/ai-dimension/src/static/icons/chat.png b/frontend/ai-dimension/src/static/icons/chat.png new file mode 100644 index 0000000..6af959b --- /dev/null +++ b/frontend/ai-dimension/src/static/icons/chat.png @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/frontend/ai-dimension/src/static/icons/explore-active.png b/frontend/ai-dimension/src/static/icons/explore-active.png new file mode 100644 index 0000000..cd95381 --- /dev/null +++ b/frontend/ai-dimension/src/static/icons/explore-active.png @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/ai-dimension/src/static/icons/explore.png b/frontend/ai-dimension/src/static/icons/explore.png new file mode 100644 index 0000000..130f254 --- /dev/null +++ b/frontend/ai-dimension/src/static/icons/explore.png @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/ai-dimension/src/static/icons/profile-active.png b/frontend/ai-dimension/src/static/icons/profile-active.png new file mode 100644 index 0000000..37e8187 --- /dev/null +++ b/frontend/ai-dimension/src/static/icons/profile-active.png @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/frontend/ai-dimension/src/static/icons/profile.png b/frontend/ai-dimension/src/static/icons/profile.png new file mode 100644 index 0000000..58553d0 --- /dev/null +++ b/frontend/ai-dimension/src/static/icons/profile.png @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/frontend/ai-dimension/src/static/icons/trend-active.png b/frontend/ai-dimension/src/static/icons/trend-active.png new file mode 100644 index 0000000..8340622 --- /dev/null +++ b/frontend/ai-dimension/src/static/icons/trend-active.png @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/frontend/ai-dimension/src/static/icons/trend.png b/frontend/ai-dimension/src/static/icons/trend.png new file mode 100644 index 0000000..8f11c8c --- /dev/null +++ b/frontend/ai-dimension/src/static/icons/trend.png @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/frontend/ai-dimension/src/stores/index.js b/frontend/ai-dimension/src/stores/index.js new file mode 100644 index 0000000..03c2372 --- /dev/null +++ b/frontend/ai-dimension/src/stores/index.js @@ -0,0 +1,72 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import { authApi, knowledgeApi, aiChatApi } from '@/utils/api' + +export const useUserStore = defineStore('user', () => { + const token = ref(uni.getStorageSync('token') || '') + const userInfo = ref(null) + const isLoggedIn = computed(() => !!token.value) + + async function login(code, userInfoData) { + const data = await authApi.login(code, userInfoData) + token.value = data.token + userInfo.value = data.userInfo + uni.setStorageSync('token', data.token) + return data + } + + function logout() { + token.value = '' + userInfo.value = null + uni.removeStorageSync('token') + } + + return { token, userInfo, isLoggedIn, login, logout } +}) + +export const useKnowledgeStore = defineStore('knowledge', () => { + const list = ref([]) + const currentDetail = ref(null) + const loading = ref(false) + + async function fetchList(params = {}) { + loading.value = true + try { + list.value = await knowledgeApi.list(params) + } finally { + loading.value = false + } + } + + async function fetchDetail(id) { + currentDetail.value = await knowledgeApi.detail(id) + } + + return { list, currentDetail, loading, fetchList, fetchDetail } +}) + +export const useChatStore = defineStore('chat', () => { + const messages = ref([]) + const quota = ref({ remaining: 0, total: 0 }) + const loading = ref(false) + + async function ask(question, token) { + loading.value = true + try { + const data = await aiChatApi.ask(question, 'general', token) + messages.value.push({ role: 'user', content: question }) + messages.value.push({ role: 'assistant', content: data.answer }) + quota.value.remaining = data.remainingQuota + return data + } finally { + loading.value = false + } + } + + async function fetchQuota(token) { + const data = await aiChatApi.quota(token) + quota.value = data + } + + return { messages, quota, loading, ask, fetchQuota } +}) \ No newline at end of file diff --git a/frontend/ai-dimension/src/styles/THEME.md b/frontend/ai-dimension/src/styles/THEME.md new file mode 100644 index 0000000..fe8109f --- /dev/null +++ b/frontend/ai-dimension/src/styles/THEME.md @@ -0,0 +1,70 @@ +# 宇之然AI维度 — 主题系统指南 + +## 架构 + +所有主题变量定义在 `src/styles/theme.scss` 中,通过 CSS 自定义属性 (CSS Custom Properties) 实现。 + +## 核心变量层级 + +``` +CSS 自定义属性 (theme.scss) + ├── 基础色 (bg-primary, text-primary 等) + ├── 玻璃拟态 (glass-bg, glass-border 等) + ├── 品牌色 (color-brand, color-brand-light 等) + ├── 维度色 (dim1~dim5: color, bg, icon-bg, border, glow) + ├── 功能色 (free, pro, hot, new, pick) + └── 布局 (radius, spacing, blur, transition) +``` + +## 一键换肤 + +### 添加新主题 + +在 `theme.scss` 底部添加: + +```scss +.theme-xxx { + // 只覆盖需要变化的变量 + --color-brand: #...; + --color-brand-light: #...; + --bg-primary: #...; + // 维度颜色也可以单独覆盖 + --dim1-color: #...; +} +``` + +### 切换主题 + +```js +// 在 App.vue 或任意页面中: +document.documentElement.className = 'theme-xxx' +``` + +### 维度卡颜色映射 + +每个维度通过 `utils/dimensions.js` 中的 `DIMENSIONS` 数组配置,引用 CSS 变量: + +```js +color: 'var(--dim1-color)', +bg: 'var(--dim1-bg)', +iconBg: 'var(--dim1-icon-bg)', +``` + +## 现有主题 + +| 主题类名 | 说明 | +|---------|------| +| (默认) | 暗色宇宙深空 | +| `.theme-light` | 浅色模式(预留) | +| `.theme-christmas` | 圣诞主题(示例) | + +## 修改指南 + +### 只改颜色 +编辑 `theme.scss` 中的 CSS 变量即可,所有组件自动生效。 + +### 改维度配置 +编辑 `utils/dimensions.js` 中的 `DIMENSIONS` 数组。 + +### 改布局间距 +编辑 `theme.scss` 中的 `--spacing-*` 和 `--radius-*` 变量。 \ No newline at end of file diff --git a/frontend/ai-dimension/src/styles/theme.scss b/frontend/ai-dimension/src/styles/theme.scss new file mode 100644 index 0000000..94ca1a1 --- /dev/null +++ b/frontend/ai-dimension/src/styles/theme.scss @@ -0,0 +1,229 @@ +/* ============================================ + 宇之然AI维度 — 主题系统 + 使用 CSS 自定义属性,一键切换主题 + ============================================ */ + +/* ---- 主题色板 ---- + 🌌 宇宙深空背景 + 🟣 紫色 — 品牌色、AI 起源 + 🔵 青色 — AI 发展、科技 + 🟢 青绿 — AI 当前 + 🟡 金色 — AI 学习 + 🔴 珊瑚 — AI 趋势 + ============================================ */ + +:root, +page { + /* === 基础色 === */ + --bg-primary: #050508; + --bg-secondary: #08080f; + --bg-tertiary: #0f0a1a; + + /* === 玻璃拟态 === */ + --glass-bg: rgba(255, 255, 255, 0.03); + --glass-border: rgba(255, 255, 255, 0.06); + --glass-hover: rgba(255, 255, 255, 0.05); + --glass-hover-border: rgba(255, 255, 255, 0.12); + --glass-strong: rgba(255, 255, 255, 0.08); + + /* === 文字 === */ + --text-primary: #ffffff; + --text-secondary: rgba(255, 255, 255, 0.8); + --text-tertiary: rgba(255, 255, 255, 0.4); + --text-muted: rgba(255, 255, 255, 0.2); + --text-dim: rgba(255, 255, 255, 0.08); + + /* === 品牌色 — 紫色系 === */ + --color-brand: #7c4dff; + --color-brand-light: #b388ff; + --color-brand-dark: #651fff; + --color-brand-bg: rgba(124, 77, 255, 0.1); + --color-brand-glow: rgba(124, 77, 255, 0.2); + + /* === 维度色系 === */ + + /* dim1: AI 起源 — 紫色 */ + --dim1-color: #b388ff; + --dim1-bg: rgba(124, 77, 255, 0.08); + --dim1-icon-bg: rgba(124, 77, 255, 0.25); + --dim1-border: rgba(124, 77, 255, 0.15); + --dim1-glow: rgba(124, 77, 255, 0.1); + + /* dim2: AI 发展 — 青色 */ + --dim2-color: #4dd0e1; + --dim2-bg: rgba(0, 188, 212, 0.08); + --dim2-icon-bg: rgba(0, 188, 212, 0.25); + --dim2-border: rgba(0, 188, 212, 0.15); + --dim2-glow: rgba(0, 188, 212, 0.1); + + /* dim3: AI 当前 — 青绿 */ + --dim3-color: #4db6ac; + --dim3-bg: rgba(0, 200, 150, 0.08); + --dim3-icon-bg: rgba(0, 200, 150, 0.25); + --dim3-border: rgba(0, 200, 150, 0.15); + --dim3-glow: rgba(0, 200, 150, 0.1); + + /* dim4: AI 学习 — 金色 */ + --dim4-color: #ffd54f; + --dim4-bg: rgba(255, 193, 7, 0.08); + --dim4-icon-bg: rgba(255, 193, 7, 0.25); + --dim4-border: rgba(255, 193, 7, 0.15); + --dim4-glow: rgba(255, 193, 7, 0.1); + + /* dim5: AI 趋势 — 珊瑚 */ + --dim5-color: #ef5350; + --dim5-bg: rgba(239, 83, 80, 0.08); + --dim5-icon-bg: rgba(239, 83, 80, 0.25); + --dim5-border: rgba(239, 83, 80, 0.15); + --dim5-glow: rgba(239, 83, 80, 0.1); + + /* === 功能色 === */ + --color-free: #66bb6a; + --color-free-bg: rgba(76, 175, 80, 0.12); + --color-pro: #ffa726; + --color-pro-bg: rgba(255, 152, 0, 0.12); + --color-hot: #ef5350; + --color-hot-bg: rgba(239, 83, 80, 0.12); + --color-new: #4dd0e1; + --color-new-bg: rgba(0, 188, 212, 0.12); + --color-pick: #b388ff; + --color-pick-bg: rgba(124, 77, 255, 0.12); + + /* === 布局 === */ + --radius-sm: 8px; + --radius-md: 12px; + --radius-lg: 16px; + --radius-xl: 18px; + --radius-full: 9999px; + + --spacing-xs: 4px; + --spacing-sm: 8px; + --spacing-md: 12px; + --spacing-lg: 16px; + --spacing-xl: 20px; + --spacing-2xl: 24px; + + /* === 玻璃态 === */ + --blur-bg: 20px; + + /* === 过渡 === */ + --transition-fast: 0.2s ease; + --transition-normal: 0.3s ease; + + /* === 辉光 === */ + --glow-brand: 0 0 20px rgba(124, 77, 255, 0.1); + --glow-brand-strong: 0 0 30px rgba(124, 77, 255, 0.2); +} + +/* ============================================ + 主题切换 — 后期只需覆盖这里的变量 + ============================================ */ + +/* 浅色主题(预留) */ +.theme-light { + --bg-primary: #f5f5f8; + --bg-secondary: #eeeef2; + --bg-tertiary: #e8e4f0; + --glass-bg: rgba(255, 255, 255, 0.6); + --glass-border: rgba(0, 0, 0, 0.06); + --text-primary: #1a1a24; + --text-secondary: rgba(0, 0, 0, 0.7); + --text-tertiary: rgba(0, 0, 0, 0.4); + --text-muted: rgba(0, 0, 0, 0.2); + --color-brand: #7c4dff; + --color-brand-light: #9c7af0; +} + +/* 圣诞主题(示例) */ +.theme-christmas { + --color-brand: #d32f2f; + --color-brand-light: #ef5350; + --dim1-color: #c62828; + --dim2-color: #2e7d32; + /* ... 更多覆盖 */ +} + +/* ============================================ + 全局排版 + ============================================ */ + +/* 字体抗锯齿 */ +page { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +/* 卡片通用样式 */ +.card { + background: var(--glass-bg); + border: 1px solid var(--glass-border); + border-radius: var(--radius-lg); + backdrop-filter: blur(var(--blur-bg)); + -webkit-backdrop-filter: blur(var(--blur-bg)); + transition: all var(--transition-normal); +} + +.card:active { + background: var(--glass-hover); + border-color: var(--glass-hover-border); +} + +/* 维度卡片通用 */ +.dim-card-base { + border-radius: var(--radius-xl); + padding: var(--spacing-xl) var(--spacing-lg); + backdrop-filter: blur(var(--blur-bg)); + -webkit-backdrop-filter: blur(var(--blur-bg)); + transition: all var(--transition-normal); +} + +.dim-card-base:active { + transform: scale(0.98); +} + +/* 品牌渐变 */ +.gradient-brand { + background: linear-gradient(135deg, var(--color-brand), var(--color-brand-light)); +} + +.gradient-text { + background: linear-gradient(135deg, var(--color-brand-light), var(--color-brand)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +/* 徽章通用 */ +.badge { + display: inline-block; + font-size: 10px; + padding: 2px 10px; + border-radius: 6px; + font-weight: 500; +} + +/* 搜索栏通用 */ +.search-bar { + background: var(--glass-bg); + border: 1px solid var(--glass-border); + border-radius: var(--radius-md); + padding: 12px 16px; + display: flex; + align-items: center; + gap: 10px; + backdrop-filter: blur(var(--blur-bg)); + -webkit-backdrop-filter: blur(var(--blur-bg)); +} + +/* 维度色映射工具类 */ +.dim-color-1 { color: var(--dim1-color); } +.dim-color-2 { color: var(--dim2-color); } +.dim-color-3 { color: var(--dim3-color); } +.dim-color-4 { color: var(--dim4-color); } +.dim-color-5 { color: var(--dim5-color); } + +.dim-bg-1 { background: var(--dim1-bg); } +.dim-bg-2 { background: var(--dim2-bg); } +.dim-bg-3 { background: var(--dim3-bg); } +.dim-bg-4 { background: var(--dim4-bg); } +.dim-bg-5 { background: var(--dim5-bg); } \ No newline at end of file diff --git a/frontend/ai-dimension/src/uni.scss b/frontend/ai-dimension/src/uni.scss new file mode 100644 index 0000000..caa5259 --- /dev/null +++ b/frontend/ai-dimension/src/uni.scss @@ -0,0 +1,2 @@ +/* 宇之然AI维度 — 全局 SCSS 变量 (uni-app 自动注入) */ +@import "@/styles/theme.scss"; \ No newline at end of file diff --git a/frontend/ai-dimension/src/utils/api.js b/frontend/ai-dimension/src/utils/api.js new file mode 100644 index 0000000..9f4041b --- /dev/null +++ b/frontend/ai-dimension/src/utils/api.js @@ -0,0 +1,171 @@ +/** + * 宇之然AI维度 — API 工具 + * 后端地址:wdkj.yuzhiran.com.cn (生产) / 127.0.0.1:3001 (开发) + */ + +const BASE_URL = 'https://wdkj.yuzhiran.com.cn' +const DEV_URL = 'http://127.0.0.1:3001' + +// 判断环境 +const isDev = import.meta.env.DEV +const API_BASE = isDev ? DEV_URL : BASE_URL + +/** + * 通用请求封装 + */ +async function request(method, path, data = null, token = null) { + const url = `${API_BASE}${path}` + const headers = { + 'Content-Type': 'application/json' + } + if (token) { + headers['Authorization'] = `Bearer ${token}` + } + + try { + const res = await uni.request({ + url, + method, + data, + header: headers, + timeout: 15000 + }) + + const body = res.data + if (body.success) { + return body.data + } + throw new Error(body.error || '请求失败') + } catch (err) { + console.error(`[API] ${method} ${path} failed:`, err) + throw err + } +} + +/** + * 认证相关 + */ +export const authApi = { + /** 微信小程序登录 */ + login: (code, userInfo) => + request('POST', '/api/auth/login', { code, userInfo }), + + /** H5 密码登录 */ + h5Login: (username, password) => + request('POST', '/api/auth/user/login', { username, password }), + + /** H5 注册 */ + register: (username, password, nickName) => + request('POST', '/api/auth/user/register', { username, password, nickName }), + + /** 获取当前用户 */ + me: (token) => + request('GET', '/api/auth/me', null, token), +} + +/** + * 知识库 + */ +export const knowledgeApi = { + /** 获取知识列表 */ + list: (params = {}) => { + const query = Object.entries(params) + .filter(([_, v]) => v !== undefined && v !== null) + .map(([k, v]) => `${k}=${v}`) + .join('&') + return request('GET', `/api/knowledge${query ? '?' + query : ''}`) + }, + + /** 获取知识详情 */ + detail: (id) => + request('GET', `/api/knowledge/${id}`), + + /** 收藏知识 */ + collect: (id, token) => + request('POST', `/api/knowledge/${id}/collect`, null, token), + + /** 取消收藏 */ + uncollect: (id, token) => + request('DELETE', `/api/knowledge/collect/${id}`, null, token), + + /** 获取收藏列表 */ + collected: (token) => + request('GET', '/api/knowledge/collected', null, token), + + /** 获取维度问答 */ + questions: (dim) => + request('GET', `/api/knowledge/${dim}/questions`), +} + +/** + * AI 问答 + */ +export const aiChatApi = { + /** 获取配额 */ + quota: (token) => + request('GET', '/api/ai-chat/quota', null, token), + + /** 提问 */ + ask: (question, dimension = 'general', token) => + request('POST', '/api/ai-chat/ask', { question, dimension }, token), + + /** 分享获得次数 */ + shareGain: (token) => + request('POST', '/api/ai-chat/share-gain', null, token), +} + +/** + * 用户相关 + */ +export const userApi = { + /** 获取用户进度 */ + progress: (token) => + request('GET', '/api/user/progress', null, token), + + /** 保存进度 */ + saveProgress: (data, token) => + request('POST', '/api/user/progress', data, token), + + /** 排行榜 */ + leaderboard: (dim = null) => + request('GET', `/api/user/leaderboard${dim ? '?dim=' + dim : ''}`), +} + +/** + * 支付相关 + */ +export const paymentApi = { + /** 获取商品列表 */ + products: () => + request('GET', '/api/payment/products'), + + /** 创建订单 */ + createOrder: (productId, token) => + request('POST', '/api/payment/create-order', { productId }, token), +} + +/** + * 趋势资讯 + */ +export const trendApi = { + /** 获取趋势列表 */ + list: (params = {}) => { + const query = Object.entries(params) + .filter(([_, v]) => v !== undefined && v !== null) + .map(([k, v]) => `${k}=${v}`) + .join('&') + return request('GET', `/api/trend${query ? '?' + query : ''}`) + }, + + /** 获取趋势详情 */ + detail: (id) => + request('GET', `/api/trend/${id}`), +} + +export default { + auth: authApi, + knowledge: knowledgeApi, + aiChat: aiChatApi, + user: userApi, + payment: paymentApi +} \ No newline at end of file diff --git a/frontend/ai-dimension/src/utils/dimensions.js b/frontend/ai-dimension/src/utils/dimensions.js new file mode 100644 index 0000000..e32388d --- /dev/null +++ b/frontend/ai-dimension/src/utils/dimensions.js @@ -0,0 +1,88 @@ +/** + * 维度配置 — 5 个 AI 维度的元数据 + * 修改这里可以调整维度名称、颜色、图标等 + */ + +export const DIMENSIONS = [ + { + id: 1, + key: 'origin', + name: 'AI 起源', + icon: '🌅', + desc: '从图灵到达特茅斯,追溯 70 年', + color: 'var(--dim1-color)', + bg: 'var(--dim1-bg)', + iconBg: 'var(--dim1-icon-bg)', + border: 'var(--dim1-border)', + glow: 'var(--dim1-glow)', + articleCount: 12, + quizCount: 4 + }, + { + id: 2, + key: 'development', + name: 'AI 发展', + icon: '📈', + desc: '深度学习·Transformer·MoE', + color: 'var(--dim2-color)', + bg: 'var(--dim2-bg)', + iconBg: 'var(--dim2-icon-bg)', + border: 'var(--dim2-border)', + glow: 'var(--dim2-glow)', + articleCount: 18, + quizCount: 6 + }, + { + id: 3, + key: 'current', + name: 'AI 当前', + icon: '🌐', + desc: '大模型格局·Agent·具身智能', + color: 'var(--dim3-color)', + bg: 'var(--dim3-bg)', + iconBg: 'var(--dim3-icon-bg)', + border: 'var(--dim3-border)', + glow: 'var(--dim3-glow)', + articleCount: 15, + quizCount: 5 + }, + { + id: 4, + key: 'learning', + name: 'AI 学习', + icon: '🛠', + desc: '提示词·RAG·微调·工具链', + color: 'var(--dim4-color)', + bg: 'var(--dim4-bg)', + iconBg: 'var(--dim4-icon-bg)', + border: 'var(--dim4-border)', + glow: 'var(--dim4-glow)', + articleCount: 20, + quizCount: 8 + }, + { + id: 5, + key: 'trend', + name: 'AI 趋势', + icon: '🔥', + desc: '每日资讯·产品动态·论文快报', + color: 'var(--dim5-color)', + bg: 'var(--dim5-bg)', + iconBg: 'var(--dim5-icon-bg)', + border: 'var(--dim5-border)', + glow: 'var(--dim5-glow)', + articleCount: 0, + quizCount: 0, + isFull: true // 跨列 + } +] + +/** 按维度 ID 查找 */ +export function getDimension(id) { + return DIMENSIONS.find(d => d.id === id) +} + +/** 按维度 key 查找 */ +export function getDimensionByKey(key) { + return DIMENSIONS.find(d => d.key === key) +} \ No newline at end of file diff --git a/frontend/ai-dimension/vite.config.js b/frontend/ai-dimension/vite.config.js new file mode 100644 index 0000000..4caf1cd --- /dev/null +++ b/frontend/ai-dimension/vite.config.js @@ -0,0 +1,14 @@ +import { defineConfig } from 'vite' +import uni from '@dcloudio/vite-plugin-uni' + +export default defineConfig({ + plugins: [uni()], + css: { + preprocessorOptions: { + scss: { + // 全局 SCSS 变量注入 + additionalData: `@import "@/styles/theme.scss";` + } + } + } +}) \ No newline at end of file