From 4920620d2cfa237bf6533b0227a398b539de3c36 Mon Sep 17 00:00:00 2001 From: Yvan <8574526@qq,com> Date: Thu, 9 Apr 2026 15:37:40 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E9=AA=8C=E8=AF=81=E6=8A=A5?= =?UTF-8?q?=E5=90=8D=E4=BF=A1=E6=81=AF=E5=AF=B9=E8=AF=9D=E6=A1=86=E8=BD=A6?= =?UTF-8?q?=E7=89=8C=E5=8F=B7=E8=BE=93=E5=85=A5=E5=B8=83=E5=B1=80=EF=BC=8C?= =?UTF-8?q?=E4=BD=BF=E6=A0=87=E7=AD=BE=E5=92=8C=E8=BE=93=E5=85=A5=E6=A1=86?= =?UTF-8?q?=E5=9C=A8=E5=90=8C=E4=B8=80=E8=A1=8C=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .version | 1 + Dockerfile | 10 +- PRIZE-FEATURE.md | 0 backend/.env.example | 17 +- backend/init-admin.js | 26 +- backend/middleware/upload.js | 39 +- backend/migrations/0000-init-schema.js | 84 + backend/migrations/0001-add-danmaku-fields.js | 24 + .../migrations/0002-add-sponsor-sortorder.js | 18 + backend/migrations/0003-add-checkin-fields.js | 23 + backend/migrations/0004-add-prize-table.js | 31 + backend/migrations/README.md | 190 ++ backend/migrations/index.js | 141 ++ backend/models/Activity.js | 9 + backend/models/Prize.js | 35 + backend/models/Sponsor.js | 7 +- backend/models/db.js | 23 +- backend/models/index.js | 9 +- backend/package-lock.json | 724 ++++---- backend/package.json | 6 +- backend/routes/activity.js | 17 +- backend/routes/admin.js | 96 +- backend/server.js | 29 +- build.bat | 44 +- build.sh | 44 +- clean-db.bat | 19 + docker-compose.yml | 66 +- docker-entrypoint.sh | 18 + frontend/map-test.html | 201 --- frontend/public/avatars/wang.png | Bin 0 -> 223686 bytes frontend/src/api/index.js | 36 +- frontend/src/router/index.js | 7 +- frontend/src/views/ActivityDetail.vue | 1442 --------------- .../src/views/ActivityDetail/DanmakuDemo.vue | 292 +++ .../views/ActivityDetail/SponsorDanmaku.vue | 266 +++ frontend/src/views/ActivityDetail/index.vue | 638 +++++++ frontend/src/views/ActivityDetail/styles.css | 936 ++++++++++ .../views/ActivityDetail/useActivityDetail.js | 256 +++ frontend/src/views/ActivityDetail_fixed.vue | 1577 ----------------- frontend/src/views/LandingPage.vue | 62 +- frontend/src/views/Register.vue | 2 +- frontend/src/views/admin/ActivityManage.vue | 390 +++- .../src/views/admin/prize-manage-styles.css | 179 ++ frontend/src/views/admin/usePrizeManage.js | 153 ++ frontend/src/views/timeline-data.js | 417 +++++ migrate.bat | 33 + migrate.sh | 30 + 签到功能说明.md | 53 - 48 files changed, 4804 insertions(+), 3916 deletions(-) create mode 100644 .version create mode 100644 PRIZE-FEATURE.md create mode 100644 backend/migrations/0000-init-schema.js create mode 100644 backend/migrations/0001-add-danmaku-fields.js create mode 100644 backend/migrations/0002-add-sponsor-sortorder.js create mode 100644 backend/migrations/0003-add-checkin-fields.js create mode 100644 backend/migrations/0004-add-prize-table.js create mode 100644 backend/migrations/README.md create mode 100644 backend/migrations/index.js create mode 100644 backend/models/Prize.js create mode 100644 clean-db.bat delete mode 100644 frontend/map-test.html create mode 100644 frontend/public/avatars/wang.png delete mode 100644 frontend/src/views/ActivityDetail.vue create mode 100644 frontend/src/views/ActivityDetail/DanmakuDemo.vue create mode 100644 frontend/src/views/ActivityDetail/SponsorDanmaku.vue create mode 100644 frontend/src/views/ActivityDetail/index.vue create mode 100644 frontend/src/views/ActivityDetail/styles.css create mode 100644 frontend/src/views/ActivityDetail/useActivityDetail.js delete mode 100644 frontend/src/views/ActivityDetail_fixed.vue create mode 100644 frontend/src/views/admin/prize-manage-styles.css create mode 100644 frontend/src/views/admin/usePrizeManage.js create mode 100644 frontend/src/views/timeline-data.js create mode 100644 migrate.bat create mode 100644 migrate.sh delete mode 100644 签到功能说明.md diff --git a/.version b/.version new file mode 100644 index 0000000..7813681 --- /dev/null +++ b/.version @@ -0,0 +1 @@ +5 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 8049fbe..ae1a5e1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,14 +20,14 @@ FROM node:18-alpine WORKDIR /app -# 安装 dumb-init 和 envsubst -RUN apk add --no-cache dumb-init gettext +# 安装 dumb-init、envsubst 和 SQLite 编译依赖 +RUN apk add --no-cache dumb-init gettext python3 make g++ sqlite-dev # 复制后端依赖文件 COPY backend/package*.json ./ # 安装后端依赖(仅生产依赖) -RUN npm ci --only=production +RUN npm install --only=production # 复制后端源码 COPY backend/ ./ @@ -35,8 +35,8 @@ COPY backend/ ./ # 从前端构建阶段复制构建产物 COPY --from=frontend-builder /app/frontend/dist ./public -# 创建上传目录 -RUN mkdir -p uploads +# 创建上传和数据目录 +RUN mkdir -p uploads data # 复制启动脚本 COPY docker-entrypoint.sh /usr/local/bin/ diff --git a/PRIZE-FEATURE.md b/PRIZE-FEATURE.md new file mode 100644 index 0000000..e69de29 diff --git a/backend/.env.example b/backend/.env.example index 8ff80e7..9796360 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1,17 +1,6 @@ PORT=3000 -DB_HOST=localhost -DB_USER=root -DB_PASSWORD=your-db-password -DB_NAME=quanyoumi JWT_SECRET=your-secret-key-change-in-production -# MinIO 配置 -MINIO_ENDPOINT=localhost # 后端连接 MinIO 的地址 -MINIO_PORT=9000 -MINIO_USE_SSL=false -MINIO_ACCESS_KEY=jFywUsaCb4zNr2LV7hTP -MINIO_SECRET_KEY=minioadmin -MINIO_BUCKET=quanyoumi -# MINIO_PUBLIC_URL(可选):前端访问文件的公开地址,不填则自动用 MINIO_ENDPOINT 拼接 -# 生产环境如果用了反向代理(如 Nginx),填写外网地址,例如:https://cdn.yourdomain.com -# MINIO_PUBLIC_URL=http://localhost:9000 +# 高德地图配置 +AMAP_KEY=your-amap-key +AMAP_SECURITY_CODE=your-amap-security-code diff --git a/backend/init-admin.js b/backend/init-admin.js index f474dfa..9b60001 100644 --- a/backend/init-admin.js +++ b/backend/init-admin.js @@ -5,8 +5,21 @@ const { Admin, sequelize } = require('./models'); async function initAdmin() { try { + // 使用 force: false 避免 alter 表时的问题 + // 如果表不存在则创建,存在则跳过 await sequelize.sync({ force: false }); + // 检查是否已存在管理员账号 + const existingAdmin = await Admin.findOne({ where: { username: 'admin' } }); + + if (existingAdmin) { + console.log('✓ 管理员账号已存在'); + console.log(' 用户名: admin'); + console.log(' 提示: 如需重置密码,请删除数据库文件后重新初始化'); + process.exit(0); + return; + } + const hashedPassword = bcrypt.hashSync('admin123', 10); await Admin.create({ username: 'admin', @@ -14,12 +27,17 @@ async function initAdmin() { name: '管理员' }); - console.log('管理员账号创建成功'); - console.log('用户名: admin'); - console.log('密码: admin123'); + console.log('✓ 管理员账号创建成功'); + console.log(' 用户名: admin'); + console.log(' 密码: admin123'); process.exit(0); } catch (error) { - console.error('创建失败:', error.message); + console.error('✗ 初始化失败:', error.message); + if (error.name === 'SequelizeUniqueConstraintError') { + console.error(' 原因: 管理员账号已存在'); + } else { + console.error(' 详细错误:', error); + } process.exit(1); } } diff --git a/backend/middleware/upload.js b/backend/middleware/upload.js index b852c14..d51b460 100644 --- a/backend/middleware/upload.js +++ b/backend/middleware/upload.js @@ -1,11 +1,27 @@ const multer = require('multer'); const path = require('path'); -const { v4: uuidv4 } = require('uuid'); -const { uploadBuffer } = require('../utils/minio'); +const fs = require('fs'); + +// 确保上传目录存在 +const uploadDir = path.join(__dirname, '../uploads'); +if (!fs.existsSync(uploadDir)) { + fs.mkdirSync(uploadDir, { recursive: true }); +} + +// 配置本地存储 +const storage = multer.diskStorage({ + destination: (req, file, cb) => { + cb(null, uploadDir); + }, + filename: (req, file, cb) => { + const uniqueName = `${Date.now()}-${Math.random().toString(36).substring(7)}${path.extname(file.originalname)}`; + cb(null, uniqueName); + } +}); -// 使用内存存储,文件不落盘,不限制大小 const upload = multer({ - storage: multer.memoryStorage(), + storage, + limits: { fileSize: 50 * 1024 * 1024 }, // 50MB fileFilter: (req, file, cb) => { const extname = /jpeg|jpg|png|gif|mp4|mov|avi/.test( path.extname(file.originalname).toLowerCase() @@ -16,15 +32,14 @@ const upload = multer({ } }); -// 将 req.files 或 req.file 上传到 MinIO,返回 url 数组或单个 url -const uploadFilesToMinio = async (files) => { +// 返回文件访问URL +const getFileUrls = (files) => { const list = Array.isArray(files) ? files : [files]; - return Promise.all(list.map(async (file) => { - const ext = path.extname(file.originalname).toLowerCase(); - const filename = `${Date.now()}-${uuidv4()}${ext}`; - const url = await uploadBuffer(file.buffer, filename, file.mimetype); - return { ...file, filename, url }; + return list.map(file => ({ + url: `/uploads/${file.filename}`, + filename: file.filename, + mimetype: file.mimetype })); }; -module.exports = { upload, uploadFilesToMinio }; +module.exports = { upload, getFileUrls }; diff --git a/backend/migrations/0000-init-schema.js b/backend/migrations/0000-init-schema.js new file mode 100644 index 0000000..d5d1faa --- /dev/null +++ b/backend/migrations/0000-init-schema.js @@ -0,0 +1,84 @@ +/** + * 迁移: 初始化数据库结构(确保所有表和字段存在) + * 版本: 0000 + * 日期: 2025-01-09 + */ + +module.exports = { + async up(manager) { + console.log(' - 检查并创建所有必需的表和字段...'); + + // Activities 表的额外字段 + const activityFields = [ + { name: 'registrationEnabled', sql: 'ALTER TABLE Activities ADD COLUMN registrationEnabled INTEGER DEFAULT 1;' }, + { name: 'coverImage', sql: 'ALTER TABLE Activities ADD COLUMN coverImage TEXT;' }, + { name: 'price', sql: 'ALTER TABLE Activities ADD COLUMN price DECIMAL(10, 2) DEFAULT 0;' }, + { name: 'budget', sql: 'ALTER TABLE Activities ADD COLUMN budget DECIMAL(10, 2) DEFAULT 0;' }, + { name: 'locationLat', sql: 'ALTER TABLE Activities ADD COLUMN locationLat DECIMAL(10, 7);' }, + { name: 'locationLng', sql: 'ALTER TABLE Activities ADD COLUMN locationLng DECIMAL(10, 7);' } + ]; + + for (const field of activityFields) { + await manager.executeSql(field.sql); + } + + // Registrations 表的额外字段 + const registrationFields = [ + { name: 'peopleCount', sql: 'ALTER TABLE Registrations ADD COLUMN peopleCount INTEGER DEFAULT 1;' }, + { name: 'note', sql: 'ALTER TABLE Registrations ADD COLUMN note TEXT;' } + ]; + + for (const field of registrationFields) { + await manager.executeSql(field.sql); + } + + // Expenses 表的额外字段 + const expenseFields = [ + { name: 'category', sql: 'ALTER TABLE Expenses ADD COLUMN category TEXT;' }, + { name: 'receipt', sql: 'ALTER TABLE Expenses ADD COLUMN receipt TEXT;' }, + { name: 'note', sql: 'ALTER TABLE Expenses ADD COLUMN note TEXT;' }, + { name: 'media', sql: 'ALTER TABLE Expenses ADD COLUMN media TEXT;' } + ]; + + for (const field of expenseFields) { + await manager.executeSql(field.sql); + } + + // Photos 表的额外字段 + const photoFields = [ + { name: 'description', sql: 'ALTER TABLE Photos ADD COLUMN description TEXT;' }, + { name: 'uploadedBy', sql: 'ALTER TABLE Photos ADD COLUMN uploadedBy TEXT;' }, + { name: 'status', sql: 'ALTER TABLE Photos ADD COLUMN status TEXT DEFAULT "approved";' } + ]; + + for (const field of photoFields) { + await manager.executeSql(field.sql); + } + + // Sponsors 表的额外字段 + const sponsorFields = [ + { name: 'logo', sql: 'ALTER TABLE Sponsors ADD COLUMN logo TEXT;' }, + { name: 'type', sql: 'ALTER TABLE Sponsors ADD COLUMN type TEXT DEFAULT "sponsor";' }, + { name: 'link', sql: 'ALTER TABLE Sponsors ADD COLUMN link TEXT;' } + ]; + + for (const field of sponsorFields) { + await manager.executeSql(field.sql); + } + + // Admins 表的额外字段 + const adminFields = [ + { name: 'name', sql: 'ALTER TABLE Admins ADD COLUMN name TEXT;' } + ]; + + for (const field of adminFields) { + await manager.executeSql(field.sql); + } + + console.log(' - 初始化完成'); + }, + + async down(manager) { + console.log(' - 回滚不支持'); + } +}; diff --git a/backend/migrations/0001-add-danmaku-fields.js b/backend/migrations/0001-add-danmaku-fields.js new file mode 100644 index 0000000..3785689 --- /dev/null +++ b/backend/migrations/0001-add-danmaku-fields.js @@ -0,0 +1,24 @@ +/** + * 迁移: 添加弹幕功能字段 + * 版本: 0001 + * 日期: 2025-01-09 + */ + +module.exports = { + async up(manager) { + console.log(' - 添加 danmakuEnabled 字段...'); + await manager.executeSql(` + ALTER TABLE Activities ADD COLUMN danmakuEnabled INTEGER DEFAULT 0; + `); + + console.log(' - 添加 danmakuContent 字段...'); + await manager.executeSql(` + ALTER TABLE Activities ADD COLUMN danmakuContent TEXT; + `); + }, + + async down(manager) { + // SQLite 不支持 DROP COLUMN,需要重建表 + console.log(' - 回滚不支持,请手动处理'); + } +}; diff --git a/backend/migrations/0002-add-sponsor-sortorder.js b/backend/migrations/0002-add-sponsor-sortorder.js new file mode 100644 index 0000000..f9d36a9 --- /dev/null +++ b/backend/migrations/0002-add-sponsor-sortorder.js @@ -0,0 +1,18 @@ +/** + * 迁移: 添加赞助商排序字段 + * 版本: 0002 + * 日期: 2025-01-09 + */ + +module.exports = { + async up(manager) { + console.log(' - 添加 Sponsors.sortOrder 字段...'); + await manager.executeSql(` + ALTER TABLE Sponsors ADD COLUMN sortOrder INTEGER DEFAULT 0; + `); + }, + + async down(manager) { + console.log(' - 回滚不支持'); + } +}; diff --git a/backend/migrations/0003-add-checkin-fields.js b/backend/migrations/0003-add-checkin-fields.js new file mode 100644 index 0000000..16b408d --- /dev/null +++ b/backend/migrations/0003-add-checkin-fields.js @@ -0,0 +1,23 @@ +/** + * 迁移: 添加签到功能字段 + * 版本: 0003 + * 日期: 2025-01-09 + */ + +module.exports = { + async up(manager) { + console.log(' - 添加 Registrations.checkInStatus 字段...'); + await manager.executeSql(` + ALTER TABLE Registrations ADD COLUMN checkInStatus INTEGER DEFAULT 0; + `); + + console.log(' - 添加 Registrations.checkInTime 字段...'); + await manager.executeSql(` + ALTER TABLE Registrations ADD COLUMN checkInTime DATETIME; + `); + }, + + async down(manager) { + console.log(' - 回滚不支持'); + } +}; diff --git a/backend/migrations/0004-add-prize-table.js b/backend/migrations/0004-add-prize-table.js new file mode 100644 index 0000000..cfa4853 --- /dev/null +++ b/backend/migrations/0004-add-prize-table.js @@ -0,0 +1,31 @@ +/** + * 迁移: 添加奖品表 + * 版本: 0004 + * 日期: 2025-01-10 + */ + +module.exports = { + async up(manager) { + console.log(' - 创建 Prizes 表...'); + + await manager.executeSql(` + CREATE TABLE IF NOT EXISTS Prizes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + activityId INTEGER NOT NULL, + name TEXT NOT NULL, + level TEXT NOT NULL CHECK(level IN ('一等奖', '二等奖', '三等奖', '参与奖', '伴手礼')), + description TEXT, + image TEXT, + quantity INTEGER DEFAULT 1, + sortOrder INTEGER DEFAULT 0, + createdAt DATETIME DEFAULT CURRENT_TIMESTAMP, + updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP + ); + `); + }, + + async down(manager) { + console.log(' - 删除 Prizes 表...'); + await manager.executeSql('DROP TABLE IF EXISTS Prizes;'); + } +}; diff --git a/backend/migrations/README.md b/backend/migrations/README.md new file mode 100644 index 0000000..dd56b23 --- /dev/null +++ b/backend/migrations/README.md @@ -0,0 +1,190 @@ +# 数据库迁移系统 + +## 概述 + +本项目使用版本化的数据库迁移系统,支持增量更新和自动同步。 + +## 特性 + +- ✅ 版本管理:每个迁移有唯一版本号 +- ✅ 增量执行:只执行未执行的迁移 +- ✅ 自动运行:服务器启动时自动执行迁移 +- ✅ 幂等性:重复执行不会出错 +- ✅ 迁移记录:记录所有已执行的迁移 + +## 迁移文件命名规范 + +``` +XXXX-description.js +``` + +- `XXXX`: 4位数字版本号(如 0001, 0002) +- `description`: 迁移描述(使用短横线分隔) + +示例: +- `0001-add-danmaku-fields.js` +- `0002-add-user-avatar.js` + +## 创建新迁移 + +### 1. 创建迁移文件 + +在 `backend/migrations/` 目录下创建新文件: + +```javascript +/** + * 迁移: 添加用户头像字段 + * 版本: 0002 + * 日期: 2025-01-09 + */ + +module.exports = { + async up(manager) { + console.log(' - 添加 avatar 字段...'); + await manager.executeSql(` + ALTER TABLE Users ADD COLUMN avatar TEXT; + `); + }, + + async down(manager) { + // 回滚逻辑(可选) + console.log(' - 回滚不支持'); + } +}; +``` + +### 2. 迁移方法 + +#### `up(manager)` - 升级 +执行数据库变更的方法。 + +可用的 manager 方法: +- `executeSql(sql)`: 执行SQL语句 +- `db`: 直接访问数据库连接 + +#### `down(manager)` - 回滚(可选) +回滚数据库变更的方法。 + +注意:SQLite 不支持 DROP COLUMN,回滚可能需要重建表。 + +## 执行迁移 + +### 自动执行(推荐) + +服务器启动时会自动执行所有未执行的迁移: + +```bash +# 开发环境 +cd backend +npm start + +# 生产环境(Docker) +docker-compose up -d +# 或 +docker-compose restart +``` + +### 手动执行 + +#### 在容器内执行: + +```bash +# Windows +migrate.bat + +# Linux/Mac +chmod +x migrate.sh +./migrate.sh +``` + +#### 直接在容器中执行: + +```bash +docker exec quanyoumi-app node /app/backend/migrations/index.js +``` + +## 迁移状态 + +迁移执行记录保存在 `migrations` 表中: + +```sql +SELECT * FROM migrations; +``` + +输出示例: +``` +id | version | name | executed_at +---+---------+-----------------------+--------------------- +1 | 1 | add-danmaku-fields | 2025-01-09 10:30:00 +2 | 2 | add-user-avatar | 2025-01-09 11:00:00 +``` + +## 常见问题 + +### Q: 迁移失败怎么办? + +A: 检查错误日志,修复问题后重新启动服务器。已执行的迁移不会重复执行。 + +### Q: 如何回滚迁移? + +A: 目前不支持自动回滚。如需回滚,请手动修改数据库或创建新的迁移来撤销更改。 + +### Q: 可以修改已执行的迁移吗? + +A: 不建议。已执行的迁移不应修改。如需更改,请创建新的迁移。 + +### Q: 迁移在什么时候执行? + +A: 服务器启动时自动执行。也可以手动运行 `migrate.bat` 或 `migrate.sh`。 + +## 最佳实践 + +1. **版本号递增**:新迁移的版本号应大于所有现有迁移 +2. **描述清晰**:文件名和注释应清楚说明迁移目的 +3. **测试迁移**:在开发环境测试后再部署到生产环境 +4. **幂等性**:迁移应该可以安全地重复执行 +5. **小步迭代**:每个迁移只做一件事,便于追踪和回滚 + +## 示例 + +### 添加新字段 + +```javascript +module.exports = { + async up(manager) { + await manager.executeSql(` + ALTER TABLE Activities ADD COLUMN featured INTEGER DEFAULT 0; + `); + } +}; +``` + +### 创建新表 + +```javascript +module.exports = { + async up(manager) { + await manager.executeSql(` + CREATE TABLE IF NOT EXISTS Comments ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + activityId INTEGER NOT NULL, + userId INTEGER NOT NULL, + content TEXT NOT NULL, + createdAt DATETIME DEFAULT CURRENT_TIMESTAMP + ); + `); + } +}; +``` + +### 修改数据 + +```javascript +module.exports = { + async up(manager) { + await manager.executeSql(` + UPDATE Activities SET status = 'completed' WHERE endTime < datetime('now'); + `); + } +}; +``` diff --git a/backend/migrations/index.js b/backend/migrations/index.js new file mode 100644 index 0000000..0dbc3d9 --- /dev/null +++ b/backend/migrations/index.js @@ -0,0 +1,141 @@ +const sqlite3 = require('sqlite3').verbose(); +const path = require('path'); +const fs = require('fs'); + +const dbPath = path.join(__dirname, '..', 'data', 'database.sqlite'); +const migrationsDir = __dirname; + +class MigrationManager { + constructor() { + this.db = new sqlite3.Database(dbPath); + } + + // 初始化迁移表 + async initMigrationTable() { + return new Promise((resolve, reject) => { + this.db.run(` + CREATE TABLE IF NOT EXISTS migrations ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + version INTEGER NOT NULL UNIQUE, + name TEXT NOT NULL, + executed_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + `, (err) => { + if (err) reject(err); + else resolve(); + }); + }); + } + + // 获取已执行的迁移版本 + async getExecutedMigrations() { + return new Promise((resolve, reject) => { + this.db.all('SELECT version FROM migrations ORDER BY version', (err, rows) => { + if (err) reject(err); + else resolve(rows.map(r => r.version)); + }); + }); + } + + // 记录迁移执行 + async recordMigration(version, name) { + return new Promise((resolve, reject) => { + this.db.run( + 'INSERT INTO migrations (version, name) VALUES (?, ?)', + [version, name], + (err) => { + if (err) reject(err); + else resolve(); + } + ); + }); + } + + // 执行SQL语句 + async executeSql(sql) { + return new Promise((resolve, reject) => { + this.db.run(sql, (err) => { + if (err) { + // 忽略字段已存在的错误 + if (err.message.includes('duplicate column name')) { + resolve(); + } else { + reject(err); + } + } else { + resolve(); + } + }); + }); + } + + // 加载所有迁移文件 + loadMigrations() { + const files = fs.readdirSync(migrationsDir) + .filter(f => f.match(/^\d{4}-.*\.js$/) && f !== 'index.js') + .sort(); + + return files.map(file => { + const version = parseInt(file.split('-')[0]); + const name = file.replace(/^\d{4}-/, '').replace('.js', ''); + const migration = require(path.join(migrationsDir, file)); + return { version, name, file, migration }; + }); + } + + // 执行迁移 + async runMigrations() { + try { + console.log('🔄 开始数据库迁移...\n'); + + // 初始化迁移表 + await this.initMigrationTable(); + + // 获取已执行的迁移 + const executed = await this.getExecutedMigrations(); + console.log(`📊 已执行迁移: ${executed.length} 个`); + + // 加载所有迁移文件 + const migrations = this.loadMigrations(); + console.log(`📁 发现迁移文件: ${migrations.length} 个\n`); + + // 执行未执行的迁移 + let count = 0; + for (const { version, name, file, migration } of migrations) { + if (!executed.includes(version)) { + console.log(`⏳ 执行迁移 [${version}] ${name}...`); + + try { + await migration.up(this); + await this.recordMigration(version, name); + console.log(`✅ 迁移 [${version}] ${name} 完成\n`); + count++; + } catch (error) { + console.error(`❌ 迁移 [${version}] ${name} 失败:`, error.message); + throw error; + } + } else { + console.log(`⏭️ 跳过已执行的迁移 [${version}] ${name}`); + } + } + + console.log(`\n✨ 迁移完成!共执行 ${count} 个新迁移`); + + } catch (error) { + console.error('\n❌ 迁移失败:', error); + throw error; + } finally { + this.db.close(); + } + } +} + +// 如果直接运行此文件 +if (require.main === module) { + const manager = new MigrationManager(); + manager.runMigrations() + .then(() => process.exit(0)) + .catch(() => process.exit(1)); +} + +module.exports = MigrationManager; diff --git a/backend/models/Activity.js b/backend/models/Activity.js index c388c3d..877c962 100644 --- a/backend/models/Activity.js +++ b/backend/models/Activity.js @@ -38,6 +38,15 @@ const Activity = sequelize.define('Activity', { type: DataTypes.DECIMAL(10, 2), defaultValue: 0, comment: '活动预算' + }, + danmakuEnabled: { + type: DataTypes.BOOLEAN, + defaultValue: false, + comment: '弹幕开关' + }, + danmakuContent: { + type: DataTypes.TEXT, + comment: '弹幕内容JSON' } }); diff --git a/backend/models/Prize.js b/backend/models/Prize.js new file mode 100644 index 0000000..2a5d122 --- /dev/null +++ b/backend/models/Prize.js @@ -0,0 +1,35 @@ +const { DataTypes } = require('sequelize'); +const sequelize = require('./db'); + +const Prize = sequelize.define('Prize', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + activityId: { + type: DataTypes.INTEGER, + allowNull: false + }, + name: { + type: DataTypes.STRING, + allowNull: false + }, + level: { + type: DataTypes.ENUM('一等奖', '二等奖', '三等奖', '参与奖', '伴手礼'), + allowNull: false + }, + description: DataTypes.TEXT, + image: DataTypes.STRING, + quantity: { + type: DataTypes.INTEGER, + defaultValue: 1 + }, + sortOrder: { + type: DataTypes.INTEGER, + defaultValue: 0, + comment: '排序值,数字越小越靠前' + } +}); + +module.exports = Prize; diff --git a/backend/models/Sponsor.js b/backend/models/Sponsor.js index ab4bbf0..7d78e41 100644 --- a/backend/models/Sponsor.js +++ b/backend/models/Sponsor.js @@ -20,7 +20,12 @@ const Sponsor = sequelize.define('Sponsor', { type: DataTypes.ENUM('title', 'sponsor'), defaultValue: 'sponsor' }, - link: DataTypes.STRING + link: DataTypes.STRING, + sortOrder: { + type: DataTypes.INTEGER, + defaultValue: 0, + comment: '排序值,数字越小越靠前' + } }); module.exports = Sponsor; diff --git a/backend/models/db.js b/backend/models/db.js index 0fc3cc8..88b187e 100644 --- a/backend/models/db.js +++ b/backend/models/db.js @@ -1,14 +1,17 @@ const { Sequelize } = require('sequelize'); +const path = require('path'); +const fs = require('fs'); -const sequelize = new Sequelize( - process.env.DB_NAME || 'quanyoumi', - process.env.DB_USER || 'root', - process.env.DB_PASSWORD || '', - { - host: process.env.DB_HOST || 'localhost', - dialect: 'mysql', - logging: false - } -); +// 确保data目录存在 +const dataDir = path.join(__dirname, '../data'); +if (!fs.existsSync(dataDir)) { + fs.mkdirSync(dataDir, { recursive: true }); +} + +const sequelize = new Sequelize({ + dialect: 'sqlite', + storage: path.join(dataDir, 'database.sqlite'), + logging: false +}); module.exports = sequelize; diff --git a/backend/models/index.js b/backend/models/index.js index cffc31b..fb667fd 100644 --- a/backend/models/index.js +++ b/backend/models/index.js @@ -4,11 +4,15 @@ const Registration = require('./Registration'); const Expense = require('./Expense'); const Photo = require('./Photo'); const Sponsor = require('./Sponsor'); +const Prize = require('./Prize'); const Admin = require('./Admin'); // 同步数据库 -sequelize.sync({ alter: true }).then(() => { - console.log('数据库同步完成'); +// 使用 force: false 避免 SQLite alter 表时的约束冲突问题 +sequelize.sync({ force: false }).then(() => { + console.log('✓ 数据库同步完成'); +}).catch(err => { + console.error('✗ 数据库同步失败:', err.message); }); module.exports = { @@ -18,5 +22,6 @@ module.exports = { Expense, Photo, Sponsor, + Prize, Admin }; diff --git a/backend/package-lock.json b/backend/package-lock.json index f9035e2..823a4b7 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -9,15 +9,14 @@ "version": "1.0.0", "dependencies": { "bcryptjs": "^2.4.3", + "better-sqlite3": "^11.10.0", "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2", "jsonwebtoken": "^9.0.2", - "minio": "^8.0.7", "multer": "^1.4.5-lts.1", - "mysql2": "^3.6.0", "sequelize": "^6.33.0", - "uuid": "^9.0.1" + "sql.js": "^1.14.1" }, "devDependencies": { "nodemon": "^3.0.1" @@ -92,21 +91,6 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmmirror.com/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "license": "MIT" - }, - "node_modules/aws-ssl-profiles": { - "version": "1.1.2", - "resolved": "https://registry.npmmirror.com/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", - "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", - "license": "MIT", - "engines": { - "node": ">= 6.0.0" - } - }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-4.0.4.tgz", @@ -117,12 +101,43 @@ "node": "18 || 20 || >=22" } }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "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/bcryptjs": { "version": "2.4.3", "resolved": "https://registry.npmmirror.com/bcryptjs/-/bcryptjs-2.4.3.tgz", "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", "license": "MIT" }, + "node_modules/better-sqlite3": { + "version": "11.10.0", + "resolved": "https://registry.npmmirror.com/better-sqlite3/-/better-sqlite3-11.10.0.tgz", + "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -136,16 +151,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/block-stream2": { - "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/block-stream2/-/block-stream2-2.1.0.tgz", - "integrity": "sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg==", + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", "license": "MIT", "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, - "node_modules/block-stream2/node_modules/readable-stream": { + "node_modules/bl/node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", @@ -209,19 +235,28 @@ "node": ">=8" } }, - "node_modules/browser-or-node": { - "version": "2.1.1", - "resolved": "https://registry.npmmirror.com/browser-or-node/-/browser-or-node-2.1.1.tgz", - "integrity": "sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==", - "license": "MIT" - }, - "node_modules/buffer-crc32": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/buffer-crc32/-/buffer-crc32-1.0.0.tgz", - "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmmirror.com/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "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", - "engines": { - "node": ">=8.0.0" + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" } }, "node_modules/buffer-equal-constant-time": { @@ -393,22 +428,28 @@ "ms": "2.0.0" } }, - "node_modules/decode-uri-component": { - "version": "0.2.2", - "resolved": "https://registry.npmmirror.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz", - "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, "engines": { - "node": ">=0.10" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/denque": { - "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/denque/-/denque-2.1.0.tgz", - "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", - "license": "Apache-2.0", + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", "engines": { - "node": ">=0.10" + "node": ">=4.0.0" } }, "node_modules/depd": { @@ -430,6 +471,15 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/dotenv": { "version": "16.6.1", "resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-16.6.1.tgz", @@ -486,6 +536,15 @@ "node": ">= 0.8" } }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmmirror.com/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", @@ -531,11 +590,14 @@ "node": ">= 0.6" } }, - "node_modules/eventemitter3": { - "version": "5.0.4", - "resolved": "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-5.0.4.tgz", - "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", - "license": "MIT" + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } }, "node_modules/express": { "version": "4.22.1", @@ -583,40 +645,11 @@ "url": "https://opencollective.com/express" } }, - "node_modules/fast-xml-builder": { - "version": "1.1.4", - "resolved": "https://registry.npmmirror.com/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz", - "integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "dependencies": { - "path-expression-matcher": "^1.1.3" - } - }, - "node_modules/fast-xml-parser": { - "version": "5.5.10", - "resolved": "https://registry.npmmirror.com/fast-xml-parser/-/fast-xml-parser-5.5.10.tgz", - "integrity": "sha512-go2J2xODMc32hT+4Xr/bBGXMaIoiCwrwp2mMtAvKyvEFW6S/v5Gn2pBmE4nvbwNjGhpcAiOwEv7R6/GZ6XRa9w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "dependencies": { - "fast-xml-builder": "^1.1.4", - "path-expression-matcher": "^1.2.1", - "strnum": "^2.2.2" - }, - "bin": { - "fxparser": "src/cli/cli.js" - } + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" }, "node_modules/fill-range": { "version": "7.1.1", @@ -631,15 +664,6 @@ "node": ">=8" } }, - "node_modules/filter-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/filter-obj/-/filter-obj-1.1.0.tgz", - "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/finalhandler": { "version": "1.3.2", "resolved": "https://registry.npmmirror.com/finalhandler/-/finalhandler-1.3.2.tgz", @@ -676,6 +700,12 @@ "node": ">= 0.6" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", @@ -700,15 +730,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/generate-function": { - "version": "2.3.1", - "resolved": "https://registry.npmmirror.com/generate-function/-/generate-function-2.3.1.tgz", - "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", - "license": "MIT", - "dependencies": { - "is-property": "^1.0.2" - } - }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -746,6 +767,12 @@ "node": ">= 0.4" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmmirror.com/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, "node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", @@ -837,6 +864,26 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "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": "BSD-3-Clause" + }, "node_modules/ignore-by-default": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz", @@ -859,6 +906,12 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmmirror.com/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -914,12 +967,6 @@ "node": ">=0.12.0" } }, - "node_modules/is-property": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/is-property/-/is-property-1.0.2.tgz", - "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", - "license": "MIT" - }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz", @@ -1023,27 +1070,6 @@ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", "license": "MIT" }, - "node_modules/long": { - "version": "5.3.2", - "resolved": "https://registry.npmmirror.com/long/-/long-5.3.2.tgz", - "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", - "license": "Apache-2.0" - }, - "node_modules/lru.min": { - "version": "1.1.4", - "resolved": "https://registry.npmmirror.com/lru.min/-/lru.min-1.1.4.tgz", - "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", - "license": "MIT", - "engines": { - "bun": ">=1.0.0", - "deno": ">=1.30.0", - "node": ">=8.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wellwelwel" - } - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -1113,6 +1139,18 @@ "node": ">= 0.6" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "10.2.5", "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-10.2.5.tgz", @@ -1138,39 +1176,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/minio": { - "version": "8.0.7", - "resolved": "https://registry.npmmirror.com/minio/-/minio-8.0.7.tgz", - "integrity": "sha512-E737MgufW8CeQAsTAtnEMrxZ9scMSf29kkhZoXzDTKj/Jszzo2SfeZUH9wbDQH2Rsq6TCtl/yQL0+XdVKZansQ==", - "license": "Apache-2.0", - "dependencies": { - "async": "^3.2.4", - "block-stream2": "^2.1.0", - "browser-or-node": "^2.1.1", - "buffer-crc32": "^1.0.0", - "eventemitter3": "^5.0.1", - "fast-xml-parser": "^5.3.4", - "ipaddr.js": "^2.0.1", - "lodash": "^4.17.21", - "mime-types": "^2.1.35", - "query-string": "^7.1.3", - "stream-json": "^1.8.0", - "through2": "^4.0.2", - "xml2js": "^0.5.0 || ^0.6.2" - }, - "engines": { - "node": "^16 || ^18 || >=20" - } - }, - "node_modules/minio/node_modules/ipaddr.js": { - "version": "2.3.0", - "resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-2.3.0.tgz", - "integrity": "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==", - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, "node_modules/mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmmirror.com/mkdirp/-/mkdirp-0.5.6.tgz", @@ -1183,6 +1188,12 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmmirror.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, "node_modules/moment": { "version": "2.30.1", "resolved": "https://registry.npmmirror.com/moment/-/moment-2.30.1.tgz", @@ -1229,55 +1240,11 @@ "node": ">= 6.0.0" } }, - "node_modules/mysql2": { - "version": "3.20.0", - "resolved": "https://registry.npmmirror.com/mysql2/-/mysql2-3.20.0.tgz", - "integrity": "sha512-eCLUs7BNbgA6nf/MZXsaBO1SfGs0LtLVrJD3WeWq+jPLDWkSufTD+aGMwykfUVPdZnblaUK1a8G/P63cl9FkKg==", - "license": "MIT", - "dependencies": { - "aws-ssl-profiles": "^1.1.2", - "denque": "^2.1.0", - "generate-function": "^2.3.1", - "iconv-lite": "^0.7.2", - "long": "^5.3.2", - "lru.min": "^1.1.4", - "named-placeholders": "^1.1.6", - "sql-escaper": "^1.3.3" - }, - "engines": { - "node": ">= 8.0" - }, - "peerDependencies": { - "@types/node": ">= 8" - } - }, - "node_modules/mysql2/node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/named-placeholders": { - "version": "1.1.6", - "resolved": "https://registry.npmmirror.com/named-placeholders/-/named-placeholders-1.1.6.tgz", - "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", - "license": "MIT", - "dependencies": { - "lru.min": "^1.1.0" - }, - "engines": { - "node": ">=8.0.0" - } + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" }, "node_modules/negotiator": { "version": "0.6.3", @@ -1288,6 +1255,18 @@ "node": ">= 0.6" } }, + "node_modules/node-abi": { + "version": "3.89.0", + "resolved": "https://registry.npmmirror.com/node-abi/-/node-abi-3.89.0.tgz", + "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/nodemon": { "version": "3.1.14", "resolved": "https://registry.npmmirror.com/nodemon/-/nodemon-3.1.14.tgz", @@ -1385,6 +1364,15 @@ "node": ">= 0.8" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz", @@ -1394,21 +1382,6 @@ "node": ">= 0.8" } }, - "node_modules/path-expression-matcher": { - "version": "1.3.0", - "resolved": "https://registry.npmmirror.com/path-expression-matcher/-/path-expression-matcher-1.3.0.tgz", - "integrity": "sha512-tkolHg8cWjEFA8+TqYGk0w6aNEZVcb7pVxW8KXZpU+ebaBr3s1ogbLssjK1cL74TtEuKl/qy6cb90RnwTFt3kw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/path-to-regexp": { "version": "0.1.13", "resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-0.1.13.tgz", @@ -1434,6 +1407,33 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmmirror.com/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmmirror.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -1460,6 +1460,16 @@ "dev": true, "license": "MIT" }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/qs": { "version": "6.14.2", "resolved": "https://registry.npmmirror.com/qs/-/qs-6.14.2.tgz", @@ -1475,24 +1485,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/query-string": { - "version": "7.1.3", - "resolved": "https://registry.npmmirror.com/query-string/-/query-string-7.1.3.tgz", - "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", - "license": "MIT", - "dependencies": { - "decode-uri-component": "^0.2.2", - "filter-obj": "^1.1.0", - "split-on-first": "^1.0.0", - "strict-uri-encode": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmmirror.com/range-parser/-/range-parser-1.2.1.tgz", @@ -1517,6 +1509,21 @@ "node": ">= 0.8" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, "node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz", @@ -1583,15 +1590,6 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, - "node_modules/sax": { - "version": "1.6.0", - "resolved": "https://registry.npmmirror.com/sax/-/sax-1.6.0.tgz", - "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=11.0.0" - } - }, "node_modules/semver": { "version": "7.7.4", "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", @@ -1728,15 +1726,6 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "node_modules/sequelize/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmmirror.com/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/serve-static": { "version": "1.16.3", "resolved": "https://registry.npmmirror.com/serve-static/-/serve-static-1.16.3.tgz", @@ -1778,13 +1767,13 @@ } }, "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "object-inspect": "^1.13.4" }, "engines": { "node": ">= 0.4" @@ -1830,6 +1819,51 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "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/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "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", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -1843,29 +1877,11 @@ "node": ">=10" } }, - "node_modules/split-on-first": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/split-on-first/-/split-on-first-1.1.0.tgz", - "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/sql-escaper": { - "version": "1.3.3", - "resolved": "https://registry.npmmirror.com/sql-escaper/-/sql-escaper-1.3.3.tgz", - "integrity": "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw==", - "license": "MIT", - "engines": { - "bun": ">=1.0.0", - "deno": ">=2.0.0", - "node": ">=12.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/mysqljs/sql-escaper?sponsor=1" - } + "node_modules/sql.js": { + "version": "1.14.1", + "resolved": "https://registry.npmmirror.com/sql.js/-/sql.js-1.14.1.tgz", + "integrity": "sha512-gcj8zBWU5cFsi9WUP+4bFNXAyF1iRpA3LLyS/DP5xlrNzGmPIizUeBggKa8DbDwdqaKwUcTEnChtd2grWo/x/A==", + "license": "MIT" }, "node_modules/statuses": { "version": "2.0.2", @@ -1876,21 +1892,6 @@ "node": ">= 0.8" } }, - "node_modules/stream-chain": { - "version": "2.2.5", - "resolved": "https://registry.npmmirror.com/stream-chain/-/stream-chain-2.2.5.tgz", - "integrity": "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==", - "license": "BSD-3-Clause" - }, - "node_modules/stream-json": { - "version": "1.9.1", - "resolved": "https://registry.npmmirror.com/stream-json/-/stream-json-1.9.1.tgz", - "integrity": "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==", - "license": "BSD-3-Clause", - "dependencies": { - "stream-chain": "^2.2.5" - } - }, "node_modules/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmmirror.com/streamsearch/-/streamsearch-1.1.0.tgz", @@ -1899,15 +1900,6 @@ "node": ">=10.0.0" } }, - "node_modules/strict-uri-encode": { - "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", - "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz", @@ -1923,17 +1915,14 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, - "node_modules/strnum": { - "version": "2.2.3", - "resolved": "https://registry.npmmirror.com/strnum/-/strnum-2.2.3.tgz", - "integrity": "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT" + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } }, "node_modules/supports-color": { "version": "5.5.0", @@ -1948,16 +1937,41 @@ "node": ">=4" } }, - "node_modules/through2": { - "version": "4.0.2", - "resolved": "https://registry.npmmirror.com/through2/-/through2-4.0.2.tgz", - "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmmirror.com/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", "license": "MIT", "dependencies": { - "readable-stream": "3" + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" } }, - "node_modules/through2/node_modules/readable-stream": { + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream/node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", @@ -2009,6 +2023,18 @@ "nodetouch": "bin/nodetouch.js" } }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmmirror.com/type-is/-/type-is-1.6.18.tgz", @@ -2066,13 +2092,9 @@ } }, "node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmmirror.com/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], + "version": "8.3.2", + "resolved": "https://registry.npmmirror.com/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "license": "MIT", "bin": { "uuid": "dist/bin/uuid" @@ -2105,27 +2127,11 @@ "@types/node": "*" } }, - "node_modules/xml2js": { - "version": "0.6.2", - "resolved": "https://registry.npmmirror.com/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.npmmirror.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", - "license": "MIT", - "engines": { - "node": ">=4.0" - } + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" }, "node_modules/xtend": { "version": "4.0.2", diff --git a/backend/package.json b/backend/package.json index c4dc3e0..ca4000e 100644 --- a/backend/package.json +++ b/backend/package.json @@ -9,15 +9,15 @@ }, "dependencies": { "bcryptjs": "^2.4.3", + "better-sqlite3": "^11.10.0", "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2", "jsonwebtoken": "^9.0.2", - "minio": "^8.0.7", "multer": "^1.4.5-lts.1", - "mysql2": "^3.6.0", "sequelize": "^6.33.0", - "uuid": "^9.0.1" + "sql.js": "^1.14.1", + "sqlite3": "^5.1.7" }, "devDependencies": { "nodemon": "^3.0.1" diff --git a/backend/routes/activity.js b/backend/routes/activity.js index 3881fc3..a249cab 100644 --- a/backend/routes/activity.js +++ b/backend/routes/activity.js @@ -1,7 +1,7 @@ const express = require('express'); const router = express.Router(); -const { Activity, Photo, Sponsor, Expense, Registration } = require('../models'); -const { upload, uploadFilesToMinio } = require('../middleware/upload'); +const { Activity, Photo, Sponsor, Expense, Registration, Prize } = require('../models'); +const { upload, getFileUrls } = require('../middleware/upload'); // 获取活动列表 router.get('/activities', async (req, res) => { @@ -23,10 +23,17 @@ router.get('/activities/:id', async (req, res) => { const photos = await Photo.findAll({ where: { activityId: req.params.id, status: 'approved' } }); - const sponsors = await Sponsor.findAll({ where: { activityId: req.params.id } }); + const sponsors = await Sponsor.findAll({ + where: { activityId: req.params.id }, + order: [['sortOrder', 'ASC'], ['id', 'ASC']] + }); const expenses = await Expense.findAll({ where: { activityId: req.params.id } }); + const prizes = await Prize.findAll({ + where: { activityId: req.params.id }, + order: [['sortOrder', 'ASC'], ['id', 'ASC']] + }); - res.json({ activity, photos, sponsors, expenses }); + res.json({ activity, photos, sponsors, expenses, prizes }); } catch (error) { res.status(500).json({ error: error.message }); } @@ -46,7 +53,7 @@ router.get('/activities/:id/expenses', async (req, res) => { router.post('/activities/:id/photos', upload.array('photos', 10), async (req, res) => { try { const { uploadedBy } = req.body; - const uploaded = await uploadFilesToMinio(req.files); + const uploaded = getFileUrls(req.files); const photos = uploaded.map(file => ({ activityId: req.params.id, url: file.url, diff --git a/backend/routes/admin.js b/backend/routes/admin.js index fbdce2d..9640501 100644 --- a/backend/routes/admin.js +++ b/backend/routes/admin.js @@ -3,9 +3,10 @@ const router = express.Router(); const bcrypt = require('bcryptjs'); const jwt = require('jsonwebtoken'); const auth = require('../middleware/auth'); -const { upload, uploadFilesToMinio } = require('../middleware/upload'); -const { deleteFile } = require('../utils/minio'); -const { Admin, Activity, Registration, Expense, Photo, Sponsor } = require('../models'); +const { upload, getFileUrls } = require('../middleware/upload'); +const { Admin, Activity, Registration, Expense, Photo, Sponsor, Prize } = require('../models'); +const fs = require('fs'); +const path = require('path'); // 管理员登录 router.post('/login', async (req, res) => { @@ -100,7 +101,7 @@ router.post('/expenses', auth, upload.array('media', 5), async (req, res) => { let media = []; if (req.files && req.files.length > 0) { - const uploaded = await uploadFilesToMinio(req.files); + const uploaded = getFileUrls(req.files); media = uploaded.map(file => ({ url: file.url, type: file.mimetype.startsWith('video/') ? 'video' : 'image' @@ -152,7 +153,7 @@ router.put('/expenses/:id', auth, upload.array('media', 5), async (req, res) => // 处理新上传的媒体 if (req.files && req.files.length > 0) { - const uploaded = await uploadFilesToMinio(req.files); + const uploaded = getFileUrls(req.files); const newMedia = uploaded.map(file => ({ url: file.url, type: file.mimetype.startsWith('video/') ? 'video' : 'image' @@ -184,7 +185,7 @@ router.delete('/expenses/:id', auth, async (req, res) => { router.post('/photos', auth, upload.array('photos', 10), async (req, res) => { try { const { activityId } = req.body; - const uploaded = await uploadFilesToMinio(req.files); + const uploaded = getFileUrls(req.files); const photos = uploaded.map(file => ({ activityId, url: file.url, @@ -231,11 +232,12 @@ router.delete('/photos/:id', auth, async (req, res) => { return res.status(404).json({ error: '照片不存在' }); } - // 从URL提取文件名 + // 删除本地文件 const filename = photo.url.split('/').pop(); - - // 从MinIO删除文件 - await deleteFile(filename); + const filePath = path.join(__dirname, '../uploads', filename); + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + } // 从数据库删除记录 await photo.destroy(); @@ -249,13 +251,20 @@ router.delete('/photos/:id', auth, async (req, res) => { // 添加赞助商 router.post('/sponsors', auth, upload.single('logo'), async (req, res) => { try { - const { activityId, name, type, link } = req.body; + const { activityId, name, type, link, sortOrder } = req.body; let logo = null; if (req.file) { - const [uploaded] = await uploadFilesToMinio(req.file); + const [uploaded] = getFileUrls(req.file); logo = uploaded.url; } - const sponsor = await Sponsor.create({ activityId, name, logo, type, link }); + const sponsor = await Sponsor.create({ + activityId, + name, + logo, + type, + link, + sortOrder: sortOrder || 0 + }); res.json(sponsor); } catch (error) { res.status(500).json({ error: error.message }); @@ -265,11 +274,15 @@ router.post('/sponsors', auth, upload.single('logo'), async (req, res) => { // 更新赞助商 router.put('/sponsors/:id', auth, upload.single('logo'), async (req, res) => { try { - const { name, type, link } = req.body; + const { name, type, link, sortOrder } = req.body; const updateData = { name, type, link }; + if (sortOrder !== undefined) { + updateData.sortOrder = sortOrder; + } + if (req.file) { - const [uploaded] = await uploadFilesToMinio(req.file); + const [uploaded] = getFileUrls(req.file); updateData.logo = uploaded.url; } @@ -291,4 +304,57 @@ router.delete('/sponsors/:id', auth, async (req, res) => { } }); +// 添加奖品 +router.post('/prizes', auth, upload.single('image'), async (req, res) => { + try { + const { activityId, name, level, description, quantity, sortOrder } = req.body; + let image = null; + if (req.file) { + const [uploaded] = getFileUrls([req.file]); + image = uploaded.url; + } + const prize = await Prize.create({ + activityId, + name, + level, + description, + image, + quantity: quantity || 1, + sortOrder: sortOrder || 0 + }); + res.json(prize); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// 更新奖品 +router.put('/prizes/:id', auth, upload.single('image'), async (req, res) => { + try { + const { name, level, description, quantity, sortOrder } = req.body; + const updateData = { name, level, description, quantity, sortOrder }; + + if (req.file) { + const [uploaded] = getFileUrls([req.file]); + updateData.image = uploaded.url; + } + + await Prize.update(updateData, { where: { id: req.params.id } }); + const prize = await Prize.findByPk(req.params.id); + res.json(prize); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// 删除奖品 +router.delete('/prizes/:id', auth, async (req, res) => { + try { + await Prize.destroy({ where: { id: req.params.id } }); + res.json({ message: '删除成功' }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + module.exports = router; diff --git a/backend/server.js b/backend/server.js index 8812c16..deb2c3e 100644 --- a/backend/server.js +++ b/backend/server.js @@ -9,6 +9,9 @@ const app = express(); app.use(cors()); app.use(express.json()); +// 静态文件服务 - 提供上传的文件 +app.use('/uploads', express.static(path.join(__dirname, 'uploads'))); + // API 路由 app.use('/api', require('./routes/activity')); app.use('/api', require('./routes/registration')); @@ -26,11 +29,25 @@ if (process.env.NODE_ENV === 'production') { } const PORT = process.env.PORT || 3000; -app.listen(PORT, () => { - console.log(`服务器运行在 http://localhost:${PORT}`); - if (process.env.NODE_ENV === 'production') { - console.log('运行模式: 生产环境(前后端一体)'); - } else { - console.log('运行模式: 开发环境(仅后端API)'); + +// 启动服务器前运行数据库迁移 +const MigrationManager = require('./migrations/index'); +const runMigrations = async () => { + try { + const manager = new MigrationManager(); + await manager.runMigrations(); + } catch (error) { + console.error('数据库迁移失败,但服务器将继续启动'); } +}; + +runMigrations().then(() => { + app.listen(PORT, () => { + console.log(`服务器运行在 http://localhost:${PORT}`); + if (process.env.NODE_ENV === 'production') { + console.log('运行模式: 生产环境(前后端一体)'); + } else { + console.log('运行模式: 开发环境(仅后端API)'); + } + }); }); diff --git a/build.bat b/build.bat index baf8326..fb7f8bb 100644 --- a/build.bat +++ b/build.bat @@ -1,10 +1,26 @@ @echo off REM 泉友米车友会 - Docker 构建脚本 (Windows) -REM 版本: 0.0.1 + +REM ========================================== +REM 版本号配置(修改这里即可) +REM ========================================== +set VERSION_FILE=.version +set MAJOR=0 +set MINOR=0 + +REM 读取或初始化补丁版本号 +if exist %VERSION_FILE% ( + set /p PATCH=<%VERSION_FILE% +) else ( + set PATCH=1 +) + +REM 组合完整版本号 +set VERSION=%MAJOR%.%MINOR%.%PATCH% echo ========================================== echo 泉友米车友会 Docker 镜像构建 -echo 版本: 0.0.1 +echo 版本: %VERSION% echo ========================================== REM 检查 Docker 是否安装 @@ -17,7 +33,7 @@ if errorlevel 1 ( echo. echo 开始构建 Docker 镜像... -docker build -t quanyoumi-app:0.0.1 -t quanyoumi-app:latest . +docker build -t quanyoumi-app:%VERSION% -t quanyoumi-app:latest . if errorlevel 1 ( echo. @@ -26,23 +42,30 @@ if errorlevel 1 ( exit /b 1 ) +REM 构建成功后自动递增版本号 +set /a NEXT_PATCH=%PATCH%+1 +echo %NEXT_PATCH%>%VERSION_FILE% + echo. echo ========================================== -echo 构建完成! +echo 构建完成!版本号已更新 echo ========================================== echo. +echo 当前版本: %VERSION% +echo 下次版本: %MAJOR%.%MINOR%.%NEXT_PATCH% +echo. echo 镜像信息: docker images | findstr quanyoumi-app echo. REM 标记镜像到阿里云仓库 echo 正在标记镜像到阿里云仓库... -docker tag quanyoumi-app:0.0.1 registry.cn-hangzhou.aliyuncs.com/yvan329/quanyoumi-app:0.0.1 +docker tag quanyoumi-app:%VERSION% registry.cn-hangzhou.aliyuncs.com/yvan329/quanyoumi-app:%VERSION% docker tag quanyoumi-app:latest registry.cn-hangzhou.aliyuncs.com/yvan329/quanyoumi-app:latest echo. echo 正在推送镜像到阿里云仓库... -docker push registry.cn-hangzhou.aliyuncs.com/yvan329/quanyoumi-app:0.0.1 +docker push registry.cn-hangzhou.aliyuncs.com/yvan329/quanyoumi-app:%VERSION% docker push registry.cn-hangzhou.aliyuncs.com/yvan329/quanyoumi-app:latest if errorlevel 1 ( @@ -58,12 +81,17 @@ echo ========================================== echo 推送完成! echo ========================================== echo. +echo 当前版本: %VERSION% +echo 下次版本: %MAJOR%.%MINOR%.%NEXT_PATCH% +echo. echo 镜像地址: -echo registry.cn-hangzhou.aliyuncs.com/yvan329/quanyoumi-app:0.0.1 +echo registry.cn-hangzhou.aliyuncs.com/yvan329/quanyoumi-app:%VERSION% echo registry.cn-hangzhou.aliyuncs.com/yvan329/quanyoumi-app:latest echo. echo 在 NAS 上使用: -echo docker pull registry.cn-hangzhou.aliyuncs.com/yvan329/quanyoumi-app:0.0.1 +echo docker pull registry.cn-hangzhou.aliyuncs.com/yvan329/quanyoumi-app:%VERSION% echo docker-compose up -d echo. +echo 提示: 如需修改主版本号或次版本号,请编辑 build.bat 文件顶部的 MAJOR 和 MINOR 变量 +echo. pause diff --git a/build.sh b/build.sh index 4f4ef91..6a4d37f 100644 --- a/build.sh +++ b/build.sh @@ -1,13 +1,29 @@ #!/bin/bash # 泉友米车友会 - Docker 构建脚本 -# 版本: 0.0.1 set -e +# ========================================== +# 版本号配置(修改这里即可) +# ========================================== +VERSION_FILE=".version" +MAJOR=0 +MINOR=0 + +# 读取或初始化补丁版本号 +if [ -f "$VERSION_FILE" ]; then + PATCH=$(cat "$VERSION_FILE") +else + PATCH=1 +fi + +# 组合完整版本号 +VERSION="$MAJOR.$MINOR.$PATCH" + echo "==========================================" echo "泉友米车友会 Docker 镜像构建" -echo "版本: 0.0.1" +echo "版本: $VERSION" echo "==========================================" # 检查 Docker 是否安装 @@ -19,25 +35,32 @@ fi # 构建镜像 echo "" echo "开始构建 Docker 镜像..." -docker build -t quanyoumi-app:0.0.1 -t quanyoumi-app:latest . +docker build -t quanyoumi-app:$VERSION -t quanyoumi-app:latest . + +# 构建成功后自动递增版本号 +NEXT_PATCH=$((PATCH + 1)) +echo $NEXT_PATCH > "$VERSION_FILE" echo "" echo "==========================================" -echo "构建完成!" +echo "构建完成!版本号已更新" echo "==========================================" echo "" +echo "当前版本: $VERSION" +echo "下次版本: $MAJOR.$MINOR.$NEXT_PATCH" +echo "" echo "镜像信息:" docker images | grep quanyoumi-app echo "" # 标记镜像到阿里云仓库 echo "正在标记镜像到阿里云仓库..." -docker tag quanyoumi-app:0.0.1 registry.cn-hangzhou.aliyuncs.com/yvan329/quanyoumi-app:0.0.1 +docker tag quanyoumi-app:$VERSION registry.cn-hangzhou.aliyuncs.com/yvan329/quanyoumi-app:$VERSION docker tag quanyoumi-app:latest registry.cn-hangzhou.aliyuncs.com/yvan329/quanyoumi-app:latest echo "" echo "正在推送镜像到阿里云仓库..." -docker push registry.cn-hangzhou.aliyuncs.com/yvan329/quanyoumi-app:0.0.1 +docker push registry.cn-hangzhou.aliyuncs.com/yvan329/quanyoumi-app:$VERSION docker push registry.cn-hangzhou.aliyuncs.com/yvan329/quanyoumi-app:latest echo "" @@ -45,11 +68,16 @@ echo "==========================================" echo "推送完成!" echo "==========================================" echo "" +echo "当前版本: $VERSION" +echo "下次版本: $MAJOR.$MINOR.$NEXT_PATCH" +echo "" echo "镜像地址:" -echo " registry.cn-hangzhou.aliyuncs.com/yvan329/quanyoumi-app:0.0.1" +echo " registry.cn-hangzhou.aliyuncs.com/yvan329/quanyoumi-app:$VERSION" echo " registry.cn-hangzhou.aliyuncs.com/yvan329/quanyoumi-app:latest" echo "" echo "在 NAS 上使用:" -echo " docker pull registry.cn-hangzhou.aliyuncs.com/yvan329/quanyoumi-app:0.0.1" +echo " docker pull registry.cn-hangzhou.aliyuncs.com/yvan329/quanyoumi-app:$VERSION" echo " docker-compose up -d" echo "" +echo "提示: 如需修改主版本号或次版本号,请编辑 build.sh 文件顶部的 MAJOR 和 MINOR 变量" +echo "" diff --git a/clean-db.bat b/clean-db.bat new file mode 100644 index 0000000..947e950 --- /dev/null +++ b/clean-db.bat @@ -0,0 +1,19 @@ +@echo off +echo ========================================== +echo 清理数据库文件 +echo ========================================== + +echo 停止容器... +docker-compose down + +echo 删除数据库文件... +if exist backend\data\database.sqlite ( + del /f backend\data\database.sqlite + echo 数据库文件已删除 +) else ( + echo 数据库文件不存在 +) + +echo. +echo 清理完成!现在可以运行 start.bat 重新启动 +pause diff --git a/docker-compose.yml b/docker-compose.yml index 6797397..f2bb2f2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,7 +3,7 @@ version: '3.8' services: # 应用服务(前端+后端) app: - image: registry.cn-hangzhou.aliyuncs.com/yvan329/quanyoumi-app:0.0.1 + image: registry.cn-hangzhou.aliyuncs.com/yvan329/quanyoumi-app:0.0.2 container_name: quanyoumi-app restart: unless-stopped ports: @@ -11,28 +11,14 @@ services: environment: - NODE_ENV=production - PORT=3000 - # 数据库配置 - - DB_HOST=db - - DB_PORT=3306 - - DB_USER=root - - DB_PASSWORD=quanyoumi2024 - - DB_NAME=quanyoumi # JWT 密钥(请修改为随机字符串) - JWT_SECRET=quanyoumi_jwt_secret_change_this_in_production_2024 # 高德地图配置(必填) - AMAP_KEY=fd5b77136ba34fe6b4f0f86dca7782d9 - AMAP_SECURITY_CODE=7f61638e5635d944a534ff747dc615ac - # MinIO 配置 - - MINIO_ENDPOINT=minio - - MINIO_PORT=9000 - - MINIO_ACCESS_KEY=minioadmin - - MINIO_SECRET_KEY=minioadmin123 - - MINIO_BUCKET=quanyoumi volumes: + - app-data:/app/data - app-uploads:/app/uploads - depends_on: - - db - - minio networks: - quanyoumi-network healthcheck: @@ -42,54 +28,8 @@ services: retries: 3 start_period: 10s - # MySQL 数据库 - db: - image: mysql:8.0 - container_name: quanyoumi-db - restart: unless-stopped - environment: - - MYSQL_ROOT_PASSWORD=quanyoumi2024 - - MYSQL_DATABASE=quanyoumi - - MYSQL_CHARACTER_SET_SERVER=utf8mb4 - - MYSQL_COLLATION_SERVER=utf8mb4_unicode_ci - volumes: - - mysql-data:/var/lib/mysql - - ./database.sql:/docker-entrypoint-initdb.d/init.sql - networks: - - quanyoumi-network - healthcheck: - test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-pquanyoumi2024"] - interval: 10s - timeout: 5s - retries: 5 - start_period: 30s - - # MinIO 对象存储 - minio: - image: minio/minio:latest - container_name: quanyoumi-minio - restart: unless-stopped - command: server /data --console-address ":9001" - environment: - - MINIO_ROOT_USER=minioadmin - - MINIO_ROOT_PASSWORD=minioadmin123 - volumes: - - minio-data:/data - ports: - - "9001:9001" - networks: - - quanyoumi-network - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] - interval: 30s - timeout: 20s - retries: 3 - start_period: 10s - volumes: - mysql-data: - driver: local - minio-data: + app-data: driver: local app-uploads: driver: local diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 4511471..2686e46 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -5,6 +5,24 @@ echo "==========================================" echo "泉友米车友会 - 启动中..." echo "==========================================" +# 检查数据库文件 +DB_FILE="/app/data/database.sqlite" +if [ -f "$DB_FILE" ]; then + echo "✓ 发现已存在的数据库文件" + # 检查是否有备份表残留(这会导致sync失败) + if sqlite3 "$DB_FILE" "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '%_backup%';" 2>/dev/null | grep -q "_backup"; then + echo "⚠ 检测到备份表残留,清理中..." + sqlite3 "$DB_FILE" "SELECT 'DROP TABLE IF EXISTS ' || name || ';' FROM sqlite_master WHERE type='table' AND name LIKE '%_backup%';" | sqlite3 "$DB_FILE" 2>/dev/null || true + echo "✓ 备份表已清理" + fi +else + echo "✓ 首次启动,将创建新数据库" +fi + +# 初始化数据库 +echo "初始化数据库..." +node init-admin.js + # 替换前端 index.html 中的环境变量占位符 if [ -f "/app/public/index.html" ]; then echo "配置高德地图 Key..." diff --git a/frontend/map-test.html b/frontend/map-test.html deleted file mode 100644 index b6e73ed..0000000 --- a/frontend/map-test.html +++ /dev/null @@ -1,201 +0,0 @@ - - -
- - -YYiM9N?d;llbFA{g*r&Lx%6FknVJ*t0c~wzwODD(?wf$5E z$&G*A(%`xXC46kUarL?O?(Mln`_yt=qys>PO**DEQ47R`x+~rG8}G6Y98_F$5MJLh zX}mN=FuMXRjy2-HAO3Ec^r&|&M;JEfGRJ343`7hSJoyp?u1Z%Y1dImANW`@d&K_J3 z3FKcq`YPZ-k7ZTlA_fh(k8bFDAj6r~wGm4b3t!$1`}Qn&dF|;}=zV6<0*1}_(~Hic zForDsWls3m(?{2CWq(uM5^ZkejxZ%7v!h~GPK70wKxO+*Kr+9zsyc#U^P? z0z1qU>bbPt%07I!N{uv%VK~9#Z>>r3=qskYT9tY5xgx8=4(mM;;pkB| z=@+svtbYyTpEQXjISCiOvAf(c?{6hFl&&|eKz<^PGT~!dt|>hMN+ *k4WR>!uMMdb=D)IDuZaF_jn@ zqH8MsSc~Z(qlfzTCkQul$Qd%BaMq#vjMN9so&8}wgCjWn`W9A2z2SuZE+C>s|57P{ z7T2#vCL+JOFP@!yjoHf&CX4M&PVep|kW3UCq7mlO@M%3~JN5NB0DLk;5z2NBm#`D7 z6>^U04?LF2eyz1pu0yvZQL}|vEY}-XC%=-W^f7K3V~Fgo4ajwH-&@m(eN-80I}J2t zzGeYd)};6ULuusyZua~4>;E6jo#q$+hft#JA703L`C37l=)U|UZ*)5yfD~&10{>ZK z`1g$2ry~9351Tyy)_$$|D<}Wxe+-!9-|_fnjyil>`hv^f%B*s-ybfcs6ZoAspG7PG z8G!d^ht=VG %xz%aLEkpWl5Y{(w3A zh-Az-a{rmcRp6|xwcuZP0mu;cDNac&imOiy%s-LLOHz|+lN1ORX>TY*%uY_xmHJT4 zuOH6YHz~>;AleP4=tyot9%Fu!LCq20tv<@N3`p5(Y_GZ6crK+kCA@y(8`M+ej~&d# zmc1rPaihG<+Vx?J)OTsg$7bhab#ALz_pctbM!dqPf9j-UPKmG~civ^}Ul3*lE+mLH zB35pVw~wRA(UI_%V GHdb-Ksh&JuK)TNX|_ce-Nn}_dA1Dm+Gy`VV=sbS1q z2i)O+21RiO{^4RF|AhHIEynV@wims*ec9>vt?yGl_j-F2y7#m;MWF&?=w-{}o`^F^ zO^P}r?oemiWK^ZCoWEatDNh})%A1r#cH|i4zPoxV2;@ox_8}6xHTbbS@9aHG4r5>$ zgFNludey3BbOcMf1>BEB$pSBijFKjO41r<@hl{R#TZvOKoYd>AMXp{HyXdfm27@{w z0Zq77jRYH}P8tQqI3SfrBG7WEK}C#hBFCE{d?ZFwOj(5IL1OKV{&j4jNTmk7g{Jrt z(_x@x4d>B-$_9} 5f%*q+9_=c`g_FN@;Vb2tlmH^7I %~?2jK_)}>#VcZ?S#Qu zroG&ySF#7=7|f@W$PJ)YBnR3%61DEcNk}vh1H#_gq?n2bST6_#hS&4)>SVUn8MW!D z9AUyQ%WLb@VLvkmt_fj*2H0y2X^|m^iT1l<%>x3n{M*8XMhnar6ywsQ_x6S$9J>sf zLGFB9`?%rgdaxmUrkT#-`*R%9cFQYwuQG0goJT#nDVjMHaU w>|<;Wmj zxM2ml7{<5F=h$T#WZbcp9&c7u38))!H&iL-aT>$QEBo8B`hHC9hNgsiwSDRaN@Vvx zr5i>h=04*+5KIdTqJk?HY4tOAiV)8WMoOtz4-n;i6kr`EepkocTUn`hl8*g+Jhpe; zb%{4;pPK8&)G4 D2VX~7<)Vs3F3VSyfDkM*b(fHgZAPNezY4*(5@9~?C z$ZyDH6$$2Ac=EGfIabeYLw@9T&ro^oE9gT{*Ea^xa(vUY`Yq(I4p!Nhf86?MjV?_! znX9 w2%qjyr7^$Qy@=S@kP+sbQCagNOyQ15+3Wo6dz0oi zhRc>TBx ;>#0Y*4~ zI!GY%f=w+;_>>sQZ)t)Xj>L@{vU&xNS?M!hv*?efyz)crrY644&&ZC4oUfbPT^Y}e zsc!`re Y}ykdaSTu+X=0? c>ONT=nArVbb;2j)9gRt>ikF>2Y@}9?gB0YHRFPR3C}lI{mOtZVTY$AO zI^@B#+;h+JU>EQYVvlgzSxM$_KpvvDfZQV5Y{#cs@7s|*oeRx`B)5!kk 0eXoClHtKV}2`%Ri#~Tn#)jkD6Q2LafP+0|-%8l?A z7&s+-s`K6=0m4T^rTLauON+dRW!GOc$xe8oT**}eE?;Lf-+1w6rwP&+rM9KIvCKzR zuD)eNFaj|3ChYbSXpF$8mUNZ)Ws}?1mZKJ*ja{HVH5Ti#y!6%jEAtoMqV0vnjIO@3 zW2N_>Ii{kk$d#d$Jf;_Lx;o1r@=7%ETD*UM-}b{5#%1OoT*K92iqm0il{8*n{E+je zNvKqZVz+)xSjK*vP;tU^valv0k^w}j3gvw3i3iHP9^KmIp?1w|OI+Nf&bn|IBqHRs z2A#j>BKlBa`puspxBVd>UNQZ|Hqxx05VND{`dt1duTh+RgjNA}!H4c>vS^AuRCl6p z{zSg<_eenj6{$sSHuw;=maDzl+5ThZR@5%H`P@4nH~Ys`Y)G-Vkfayu6?fl5EFcVo zO}GX1eNP44B)-ny!HrOv&B^-nJDl8;;nxp=Ggb9iJfJBmrgdD zA0z!y*xsPh|CJl0VdWM2j5@mNEjl% 5-w2mPHp4}k9eq4M-1^u<|?WfTC&Zgy@#Hmq)6^CPnCZrZc~O^M#lWi3!9 z-$Qv`z+=yrKAe_+YG$SNH@Mew^Vz*I`^bU5P~V1YvH$-XO`qt#B>XZ dlviqWZ=bxE^Yte}U0-2|;d3@EPQ}B3F48UfM0ad6AAOrY! z`r_ZB{{C*E1_0c@zWQfZbkQ$Z_uS_r58e$BJ`d- 7p22sgizaLS;CXoJEW8num2wyDNXO_#O$ zZ;ww(j{O0zpJ62PagRk6)WPQ}#+BXGyYp`fu73%-voG#d5hS|491;zhAvil=!O#+g z`@0ev?b0)YM&0fVpZ)P6x!l+l8BxUJa9`!9xT}2X_ni|u5pQ40&QTxeb4GqTMZLi4 zvs# h4J^_(QV-c5d9pSAOx;e7|q{ov|~us0Aa(d$Wf39mxaLn3#& z?IaxG2SgXr^1^syPfD&W2UuHM>k&^P+3-OEdgiyDQ1(|NUrlb9?Kwu)!>b{<)aDB~ zhzgakY|_<3yN$mK%1B_xA}W@eCjg!s`wX*V(#R(hSadfiNH!e-BLr-!b-9|Kp3|8* zb(=FsMZSAqJ-N{&S;rxqBV7Y86tvQnD$kyc>xB3-h!5NJgtci2#rxFN+}r0Ze1S)U zm*=2vTL9{`8zdBrHx9&CM|o|(RT#Ehl}fBorV0iU{tOA&atHhrD3X6EfAFB^vlMBR z>l9#?MOu~PbaAVucdF{bKtXx$;^}3BNTaBub+J(`Pb9Zp%iWS>Mu(2r#8;YN=IWQ! zV(8W^PSgGehsu@bv2@HIuZa`L $xYyz0{BJU^Yra=(6(CXdZ)ljSz z?FL;vM);~4cX@Lz3(uzmefGTPpHv1O?$E~UP<4UK8smUuqU=d(k0NoS7Rr^-K;a4} zvo>#o5%ow5%-JO+{dRkg2Yc3aF8F83(ZMEh(@2UwecxVkNLp&&^UlwcxZQhF&g6Ae zo|*n}(e>^^au&oLYHbKijw_3=bdS5;P+KE5QyfFAp=JPH${MXtb&N9qkvSr*2u4PB z_C|EEhE_*To5R&(;yA>(Nkai6 Rkikl8v$&DLFSzvS>Y!<8zv9vtD*pHL+CJU~!S1)t@mgTRMc PT43foH_x(CsD=|FX!kDsPDDIlk**x1(@^WjXkDdldLUS zgG=g^atg4?#8KBB5F%Y~T#ZHQuDQMlGVgVVxQKH)P1w)9#+^2|a}}SY467yXvN7yY zm`?tI)gwJs)Q|XpRVs#V?8%HWJt4a*Yb|VN0)yzk99q}t|Mi1yqb0oJ3NwBGqUE9^ z%1g@n&bDpmaN!psTMIqx3k8}Ar$p}GO{EgdJJrhYt1ySPfL}*Zg7kr_TqRwxB3{oh zmAO0|bTj4}^
#wx-P$#zhJJcc+-IAfa}g!EqzGSt1zcVHy6xlT#5Ywq znDRGcNzmZZ#LN^2)v0g>&s^O#u?gp&l#l NR3b2WIC< zTPBu})btR?K9j~Ka65N#)vJR`;_pNBKi`nx-fIyqr ()ne{@ zh>+6H{R%dB4uzH`%pf^J6Fv&&F)gt1(gfAW=-2`><|N)@PZ z860s5 L7%y}m%?54zb_hWR>{FotBKo?4GeY|0v8naF&Y zMcv9`koM$6)KK`!vu9dv^5&!P_%C%E0ND)YHkt_hm>y4fXKfZsno@I<_47l!BHp5g z+HIi+?u)4!^s!7Epd@D}qs3QbyPM^0XpAFO28(9fY`Z6L^zbzWwws@z5rb178Pf$( zwoc?17Q5fNs;fT*&%t_w>Cc1Eq2GF6UKFcbqt}Ezz2F3c-qRdByMPpzSXjoP#lGP- ze5&BHhNdLz9;NOLU(Qey%Ua<4IKo|;vC9>F_jDV{Dd>1xyNEn)B7Q^ZT=VX;?sK#u z4&ROO>fzK#gQcuh#mLq^o_MU+{F)ijhAo3L66**-sCTM&(oHrStAv@jKjpIZTP8_> z0CuB(&T5+M_qLEX#`m`j@bA)Yxe#8m#sen({CyZ`M~^&$Xh+Mf$TlyhGoRA(cOx=A zdb&BwP&>>wthkBX9ID>v>+`R#y@FgwP}Sv!FIy&h2i=vAaJs2?NIe;Y5khr-a{l-4 zH;hpwZLW1<{?*AiKXB(u__T(=f6i98)dDpqAK#=E8bwCGnl@p_5jstYWH>V6C#Y1K zBJ)A&D0cJiw{ISO{}fa8vKb-cJO?g_l2-Zh*3MC6P&NuAKT{=fKDi~4-M)n!!9 0}y|d@&c26y=zZN z*CqL**Y4xCG==k^$uk1m???c5ePE)WD?BmV*H*UGn)1>vUu!EaJfEpTTY&Y{*yLWA zg0{~;ui};UJzbpwz*jkutYes0UFDfi%)7WZ7JD~EL({~-N$6lx;utmOWYboe1sb%2 zx}7?hHXv8Mg@qFPMv|N&{OAdOUi57)y5nK4()zmz$V%^l7`oMJhB!#Z%o*wAz;hEh zcfbDQb;>pXc>QJm`M(is{`cSecc0Kd(t&>afvPcVk?A3s6&Vkxz)>nfBAW`Ga7vdM zbRNzaxqd2KKxvT$#F3os@qe##{THGB_s`+xz~Q#jekLC~UgSh^ItMI_&6idt2LuOf zrS*Ol5pAVPYKT2Jjfvx|N|`?VP4Tl5-q&I?;myAbZ_i$w`aRxiJp0|5yn!djgQ6xq z?fz6{7MA}?<`M^Xf+@=@H%lEi17PUozbYmEdv7=h@Pi-ur&KdA`X8|Czh~foK)U~{ zrjtLV1iyZA@@%YJuxl>he;I>shs%ddA3e7Bz&%Tv;c^jQVEYu=hghX |2KhVGx*ZqiQ?Cokhn4X*zt9jwN`TC?0;z2O^ z0^tQ^kha(|2k{r4X6!0?w8y!$cb! aD$oukn6+USh82 z%h1 1=g!Ti8?h4!&R9xlR>N*s8(-1^*lDIiB@GHeF7!iIC|;5_*WbnwGe@$_r! z;1HLHP3|P}h@K!2(2??YlL*!N5xySJj_H59A1DjEao`DrcOlEaojuWJtSup`cTzng zcebLdctLqOJLwp$i*jB}++ o2T(Iqs@)&oQ+C+59#gKff>scNn3n zeo{=IjoFjtWRWawrfx`=kQqU{=dF!MXwu%$9|rrdok;NC*Z@R^O^RK*P<*of&Pj-C z5=g!52}L5UV+?N7+}luQXxL`%&s$XGaZY|OF7rTV-Rj(`q^?du5;)V?jMI;_Y!Vfr zY%*mSY1jF6=X@vrNyrQ!PO$a$ZyBqI-YA#a`LgigudDFjxC?qax(E`iLK+guTRN-% zV&0LWS2c>gX0$w0GO)2d2>m4~Rz0u}F0QUZia%*BW*T=W=I}h4C1eRbl-`_d3S*N1 z-GZZzhMo%x#HvtwaV45Vi~?p^8TO?OaVZw2G@;)<36~| mh9+P zhiJ>QW~;{TiV!xlh!Q!=h^yu$<`Ddr^=K PeN5B1% z8TUd*zdr9?b;`UF#rQ&WB!_8@?_b_F5~~wXD-__fWGJ_`PX*EMWZ2?rgDcM46H>*q zf`4FTZb_H$wxtd2h1|w(No(?ZLi$Yc{}wDDMy;d0_mGwPvT63%X1!Hi5(UC&bPcr2 z@YI~xQyi7a8LiECKE^#DZDalEv2N%+Z+$ExJLRqDv3pEd;E-ELGj48H$G d18p3~x EaB4}xq6lJ_-^G`WJsKx#WaodRI2+GQhLaM6 z`y@InR*K4Co~%3czyU>wv~ff-S3BXyE$+>8d5K5P5zSa+uHahcvoK75d*hxj^U)~9 zIzR~=D%3!I$VkUt*J+6ND>qzH23CK-%HddER`m{JZvd23@VF%vHEB$T%EKLR$NN{X zb@tjwG5(i1*A>&AQ;0pv9p3wuF3J%VU^qZk(vJ5UipajuXw=x1A(!*Y%5ntak~)Kr zGUM &O*u&pX^mUI;43|UZ+U;yW??%W*gdh1u!nf2zxMa%1NYh z&PtPcJiEv7KzD5d>Ha4s?s3ON4Xn@k-AwRU$9QCvxA%+VE!A(t*OrcKiUKWhCy0Ie zT3W}q_=cpaVmBsFOfUg|MPt-+@7ZVc3(42;&1?K} Eg-wXQ=N-y`AK5 zK0RBw4&(G)JPEN5&4S^>3mXZs9{M}>q<%q=#>38ka_p-}r~C*Pz~*P1<}N4f+-??Y zA!|?u1B1)_ENa4?UJgIfN<3#Wn2Os*H)Rq18CilA5!G2#ZyDvQ7XRj3n{FJjHaiM@ zE-nU_x&Q^(l52@v%aOfmJ+Dz4SgI~OD1m|~Y7L<&AN>*<-(lUG|BPy}{1%h5r{h9Z zPb&fm7C_fFdTBi~kZq*_YIGtN>1y9|y`V7@;#1eh_rp9=WJ~xN2Ra{did3p;|MJI9 z@63#8HW@y_`2puRO!7^Kl;WQ52u#LBp^mj#hqF{yY?W^X!%Hd3J>SE5SkiFt`A^T4 zPV@*Gr)RFamy|K)j@~r- s2U6Uq5kqp3R4HdI z&`eL{P3+sTm$~F8*(4*bLKPLJjrT-Vs=0pK#4;VkgpE~Ce#1wYZ#x%Kol>%T_)9R_ z`TFz>xBM1zSKJ@=m`Tfm(yv^uy=H$^l-Lk*n 4} zCVpV^^*jB2b~j+2xNWrA7i}mkn<2lP`~ 8Ryd~s|htxfrHAI47>Y6 zW3qEWs5dkxqYzb>xe2Frhd=fd7F-cb`8F@@Jfl{a_M=kskZ=V!YF8Fz 0RM9i{JTfjyN}>=c_YC<@ L5grO9g zI^0p!K94%a#77JgqlpbvseQY>`$=dlz?K&1WkIr2eCT9%-}dopS612ESerQb{=T+w zl{%ap%_nWn8839rcWTuIS~wM>hIM4%Y``?SuryPBYWV&m<&?<%DdrLGh<7BKDn@@; zWq rcB&TN)bsH-kegV9?VHX8+FBJBJ+g z-Y=<5ylA{N{~G2b<8<3~;6*cpRaS9q$GT;EJ3D%&b?qfaOVx6X$V)!M((Y_%Whg <-v_q0I@n*J>Rlr=IIByIJlT+^Qw66AY0CNxT>gqItfb6%`$R^!keP2@P;*Wo+>N z4=x8{2r%+dJ>JB<730N Y*8M67T8kgHPa`>N-oVvEfA;+0 zmi2V8Z|)?=Ps{sg&P}x`e{@YW7F3*HNuv%5pvn(Y8!ouT z?Bd(m{!CwwA>;hn$Jw7lhU9aXk9Qb4Y*oLxKEEX3`-aaEke?tgI~$=9k>n92p12DL z2t8LXAs@5}4!Bd4xNvZ?^N9Y+Mxk$)NIueT_PNmDEt4sAw6|zM>4M5NcbIXQNmfkS z&LcbhohPG!$Ss>9K6&<7J$kgDqqjvX<7QicQp-)7+FQC;?=-8f3~rd${Ua*dVs~+7 zL@0Nq*8Gsy{<5X;Td8*a69X-FJyGm#?zt2Gng2&4`v1uo|Ia`3XWb6@Kb&Bo{6Iia zwH>^gP)u0Wx>FNtJwV`6pcR1l^?|skp~pg$am=Y+-Ipe?eICbG;6GnhI{!b@3yb}2 zbI51>Ma5BnU$hl*efbG|@^7*YwO>i1-`oBv+}Ql5aN`AFAqLD`kDvIEC$ZTfCyWbJ zI8WrhoRIG=whZ}8IM#p73i#V*^}kFL0O_}Fxy#;QjquaM=TN$-Ykz_UU-VG%xwF&c zys&e=vWwI3so}jt=Il$$wM)lu>&1PG2R~jN%h+Q88cb7#p2pn(63k@k=u&*qNs{Bc zk}g=XqDISxL$gH^>+nYn@^OO)- MUF6DT`lkib`WpISTjZup*;6SDYg zu$0bZs>5r8eTVK`uI*roX&%aCJusX90y_};1`N^> !L8aehy2XF(e}WJ4@Z02l(QC8|mG*$6Xm;L~icaJCcaMggCMPF1?eZc`6^ TsTSswX#4DlfM491-rsf%CY5|k!i`%u5@6xXajx+uUr8nHzL=U? zHcB5AMR|=89$>`YbO%LHwM_KVca%=U0GWs)Yql*ayg<$)ue%kPeFrhOR}{8o&$xfV z)(Z= A?rPr3Hr~?Q+-4Ll*(7OPp?c%pgA;JefO)33Z d;xi2Byf0ZO^e2N ztG51-vQd84rM&(c|JXoIDrthzUm>Y@T~tnQPeE~4y`Fc?N8?XAXqz%)piX)`j>G+; z-8Fi`#5z&Qx1e8Z#IBQOQuI6v7-=D^`-801Q`yo}nVm5mJ*z7hk?@yros>3n*m=oe*d~*@Y;I@v}!;f;p)n6@i<}st# z?;>|~Y@ZfvySb17O*aF&Pe&F6MYBr{rcTLn;i}tCK=!s>Cd}0 xQ^xjwdP_3Ln) N1-2eB_U<*=s)*XxWygzs06_`P63gv yGy`NqaU*u)UfcqK>ZkqdrijFbWm~^Dv|<+@b!9UaJ~vbx|Bw=&84ytAYg GU iSQAk8{-*zuCyIRhF^-yN8$3r$d97|Vfvs@CWzyW}| zxZVBE;U*nR&HyFAC}sc`@p$fO=%Rz(3~;O0bMSh^-XOLi>q`c7vnID%HXLl&+yW8! zbLi~un*G7gy}Tmyuh({Wbgx3QA85Rvjfxs*xgy%WRI!*5@AE^`s@3R2Yf{5)mRj;x zGbowycnKjlye jEvvWQRl1H-E_+x_m3RXTe`jALjm zbI$ceP46AenTjIi99UM0LXiX>9ZHiQF+Dg(iViMLzHqkG)}=uW$F2=ibzw7bD;tC< zHGI~V#N1O Lbcc2G0rT_KGfOJXq$85drLl|WWf%BS&dkRKJC|dGy6Hk>Wj0zk)Ced z^=S(Zo?mC!T;CeW9%f4IQOCYLWNmHnF-nY$CsUnjq#|Rqr*^xCFii)dKg49HZAJq` z8XX44p5Z42r%TEAoP;WUa(_WHW2nz5v6L&X-+rB5-N&pdeaxX?`QCI3vAa2rjZOzc z9gDx(lWnPIOfB;MAM)Nbn(e-C`;F43gBq$@DyE{^Dm7FaLk#U%Lls3!&018=vk*fJ zEiGy+S|!n dmNvm>)DeOkO~YW+9Qg=@LOU= 1x%FZW z8dHZUQWuoMzjjO{MTdy-KE>}+88gjyH^NSv86RDh >5Dg_(l?;`DqyL(sD$ye@F15^_o<043?|Kt*a zltnS@EIeEn@U$(`M@yX@R}QxKQ)O5TX|U4`!;6LP^j8xB!#;wqGn_&M;7&2uI+3wH zbc5^J^Y>G(W!_Iar05sAmBhtu_9OJcasEoW6;m~QLieVI_4^TeMwPATE;_SWjKUE; z)#1}2pfUvzS3_Eav1ph~U)787@*JHfFJJi73U*I _H*7psG`!+P2UERYASxY zZ>cVa7O%-k#IHi~ T+jZ^hhW+v#B_QMgwhIdd%f5 zot>HEWzS4F+$E+&4oiwOlCiLeB5-`io9)ELjefywgTOwJITdHa#|j~5(;iQ|0hyBE za?A$s!VLAuZ8F)ci dqlQDbQZ zsq`4sMAaIcb#Y^@qDbicx@S&CTVzAM3n=Z7kMWIFI`RT=?Z$eJi{4D*Eq43We!x`! zbCS+V>~f#3c9M@8aY?h)d*n?kn%An2s6SMe-{dPK@O{bhd8@;rm9&s4zSdb^Tlst| zE=@?8)E9Ww5OsZa_2
Ac^rx%hn&gCvp#M^pPEGU#kG=tIARE*!mHRMK@v4+@neNuZ6e*N;cUWa-!Q zVDD=$H?1_Fvs^0QeJ~&B gqwLJp4cas$-xv&fNI%V1@sy6Y{pAo;8moED z#<3U@)Qb!OqW{_e^63!z!(;PF&M@N-vDxF|@$IWhvtM{VJmY+tWRW+^8|`xhGj(Rp z-l%y*hj6k?aCNg(xFBW~=S|t?-+vkbTM|7QA?8XPRxQYer>ZN4B*=UXIz0 _&}Aruv@3_^-iZu!Bp!x zhoDT*HcX}L4D%uIggUZ*1! cP+miMO<=#9317OB-9PX7_IXu;G>LU+KV#;uGLp|5zyGR=w1KY+*KC z1$@@Uz-%w`FGgFr50KmY&;6zU```atwbe;B^Z#aZqWRxxBLncpWs1S3VqxRH#q4tc z-#~kD{eJ}>*O+hlCn%+CX9!EZkFmg<9|rgr;WY34Q!+89ndcEbTU!2uxCIUD55nSK zPN8zW-&D1VoPSi09yIz_PSSN%)PC@HT?B%1tLNsI1;CSk?>_}g|5KF$;Q0Bw_5ANk z6QF?fAC%+2+1$&0{=PK%%Rlt5Xy|a3G$_cvVHJJr*7*Z{`b+vuSK!|v4Hgpx5Y8s= zb;F(2jT$ogla1{f*-)BUg@JlnUFvY)NS!?Vw*H0lGYrSpn4cP%5%JOkJSV;SV8gEm zH^D}v>Q*GViuBDnUOrRTF2fe qn-a*6+gOT2P# r%-fbw8B^*Ih!Sqo%;MIsDi7k z)ZRWgsofa7GU1zj_X%feT|49ePNqS+dXOAXPMa+il-cZNUDm&hdsAJ#@WA}YIINJa zxxA&BdaRd!?UL2BMV#p`DsqN{Lfl%$W3tDtKTLh+pxtc)UtCVp#dr;Eixf^LuXF_- z6MDw1K7RJ;gXk}B#iA7eTXnL@Nb^0)!BX8&=#jQMv&2n*oC*!UXv=o+quDw?vbKv* zmd%*8Vvp54FA?qEqA`KKV|;7>#(o-bqB(k5V0 CE#y zmhzz`l&ajG{I%Zgh)lC(^=@6@Eww$&`a<~Hjr9HwBjpc%gTU@i#M;Wt_zyxD@r!3= zoo7WIBQ=lJp;_TomoNL}@yLbR$d_B{RbXEz7=kk5QiZFqEAQg=9FVOrD*CA9*z&}| zO}&JG=iZ*YK5V;@LdLVcNiidh32l>=*E)`$p?@D? b8V zb!={>S$q~L;9exm&tQ!;&M{#d+YNB#iOkPznJO<%=b#Rc<|G0afMMlLaAG6Rh@+mP zNy_29JQtBTFD1MLD(_K~x?I4VGhVuS&1joI-iH(Bl(6^X;>N|5P9@Izab`2GCMScS?0Z|!P?Iahzb?Xe$sW% z*dqWKF*VB~lIdpGPZ1<)*zW0y(L|FJ=>CiL;E3;0j a`m;JuJ7P7c|$W7a3d zg!7_xIYi9}o9c<-s@b&+-a3_=k}e!kiB73~s 2SL9xD3#An^1lV-rvQ8J~ zNOM$2*aQgGv-SBB33aROOUQ%NaNP?f)-stADn><%sk<)1N-HI|ZrvIi_LOQW&us?q z{%o>m!jr$Q^M&0*jR^94KC%#G=dIR;a6a&Zu5nKxh~G7&DafzFey&`p&cS!mE6>)c zs7G_(7SZ)Cj9>(pZd-2r(JzSnl0V)(FP^gvCQK4Hx;J`nOiMp)l#!7{!0K+orj0Qw z_7Ss5O^iLNn`GYj$Fy)|TeC&va` bM`_JDt_0Mq>+vn;_6wjtEx2_%D=?EL8Bu{xYK1w6qB8Un~ zyX6Ngx2%qBjj@kl^GcL8WBhblZ)STh===yFeOk=%35O}!>CP-P3mVk6c|xuR#l-X( zDQb%RTC@!gBOIMbjo&SvjL2e@H Yvz&tCxvu4Y92%namsVz#6Pu_Sx9D!lqCBp=2EOj{8dhnRdQb^yC-=i}m32GJ zJG#;Qx+%{wxcD &6~L%}BAw#NN?s!G~?CicHXZ!*@1>+8E&--%Vh3p0C)Cp|H18xG~zP zgF`Tx>47JD{2JSAZInJl6p@+QC$ofqGQl0wE+yw2sU$BTqz}Uz8TsjH#MQ8Cu!n8w zFXWVYTR4WE?=&1?mW|F4@c1>2FB_L5-&aOoO@c`F98HvyKj8{?T+rMPBw?%K+I#km zTe9=5bym+>i3$1VtT`Zna*nd_qA5f3VLi{h;1+RxL7X7!AsBN5j{)f5B!D?dkJGBF z%H~hKEIz_!lbGoMu7+ybsacdjPG2#r%pC}4*kcXx2ks9S&@$74_I7@ hhbmaL4Q zE1$R3%N$eWX_>tuK2%E^YXmlhs=Mo^lvAG=Q8jhd&QFh+`pHl13IOO#(Wenq*5Krq z^89={`tDG`xY~s)X>}S#+K>&M0aZSrJZEyoS~JS(^k^zmyTCdVdTG`D2<*_zeOv18>n`6=09r`Jz2dFmhRm*%_PsplVaKH46ws2YHOAMS703$ZzoFqf2K zwK#x_dGSs91=-hcsCO~^N bJE>u_+M z=k!xZ{T3Ozy*io`S`$~M0dcoDl}-=B8D~5LUEy(o(5AH+6rxHQ${oCBb#$@gTvk!& zJ}v=*JsnNotvgv&zfd+*Q;5l5$9ngoM;h8jBSiH5!~7yTC3j!R){>I|XecLdTFt zI}zLIk9MjBdk)DYZWhh=DI#Ksi6UH~7F6Ja>L6qTRpGkvVP;)Vhbz 2267 z@mw4_Sm{X7a19v2zav@{<*VQLdQ5n5>sGu^pV17r#;&@f=nXu7>ieadda18)bAkb% zPHT+LSI7aV13=j{84TTK2;npiCg@i@ikBbt^E(axj5itmvCVX!@0lABmiPMzg!BBP zn57FJ?iE4L@2QO|=g+woWMSPi}B0`DRzRjx!pp`*6RThYwN1Y$&G+DcNm(q`RS| z##HC4v9@_Bc@AVOdm?;w5U4l#a&^Js0d5T^G3G++#%uwWqL?5Wf5LXmz&p; aLU#%vVe9{e>L&lE&DHteu)%-x;++ES{`;PP-HT0IqEK$0_I@BHCdXwYQjz9) zRrdP$@E@R6F{hnYVBB(%rp#c^VZAhe&5zDVKdE~5j&Q=&YQl%qbLsx7!x3RdYKo4I z+C0{7RxRHnl)On@TwJy>ex|PCTxxy0M*dRU&5KLlgjBj9yh*RY(sj!d&iv5Yris3h zqJfD9&C6;>Z!Jy2OIWt(Dtrr;CrqBg`jAyG`QxXd`qc3!mpp7)4o4G{bR|KKkGqAw zMj_8POFX`o;^r;;@sIWkk392MIks}(omv|EFl^H#9iS$y ~dDxv$>Jbj^vOCoyw2bW~Z$K>N|xq?$0N5O?~ENdNR? zR Pvfz1;bDuoetNFFC zpFT}_&XLrIPRrW0_r6?DUSdjSKs#(h$I3^W_z)9o#PDi@x&N6*ky%^C_qR4v!Z#6q zArJ ssLi$}r*?C0xnUmCvzG zoo$Pc&Na57nmk+wxM}+*y}f0H#L_d44 k@mmsPhY>1T7Kr9-M{>0H92B@Z zow)iC3^N8}ZwIa0tGcUNFYK%y8sKT*_wdn`^rw=V5gYEK`KG;cFGl;Z1m^e-f#iEd zvhaMvgzQemBxM3Pgte~%n%VdIJ%&F=YpMC-O`>*v0Yh6k_vA3V!SuA8mY4{0hmq76 zh#U0XE!xmo4O@Q1sd$vV3wPUrrLsz0b6+>qG;wu&t}no*B{|Cnbg^O8L+6|Qp%(tg z3GW`3(9Inb>vncPrQ5I-MkZ(}^k!0itep|G(+COjL!!$NNm0dbM~-xnq|tecH0m;) zh!4to8Z^BJQq+}@^x@Lv!rr1D7d3G{YQWdEzDb@GRA*57!k5W*B@Cryl3a3l)-D3* z8(pWa<;rI(cPino8{avs7H9X1sm;HTE%oV )NeJjQP<8tQG+8ZIq$=6vP6Z1QPJ zq>ia;lN5}rv5~#ezNvDl*nbeh)fi!^PFhCgFqG?Yl6pS{#R7zhAQx0zlIliy=`FQj zsYC@(j#Dx@mAa&f(kg#trRZ8F<$80}E16*1Y%jJUga%Jee9}te`6bY|rnZIJRlzNe z;+bX}=hp9PP27lVS=*m5hAu9ny>zR>TMMl$8^q5)p$bRjUf*IaH`Mg6%H1=`ziyr6 z0|P&TFp#w6p@PAe9t404bB)+VdQ*jV$JXPfiC>L>W&dFzZ+*(|2s9C81O*EUtS1Gl zZ0~yCr(c*UJEgM+dpYL^QiCKZMIVALj$(v?gyQkZwihy{4g)uZ4ylNLrcF7~aLB=7 z<;fMw^tj mn1-1mp zjh9;x{@jtK+?_Gz vUkLU^c zlWbodp#p-70DNoEBHx((alxEyW_Ni4VO@Q9{%3G^vCR8P+iU%k@0iIwhC);Ezs|~b zG!HZxob&TRY#8Z;`=WG}Cq}}qR#%99yr{!Atw5-jS0W^7leYFwchny*tU#YtHa!CR zkmxd+Ok@`$Ezh6sR0|M)2UEx}C*=u{Ccz?+A_raUIo}lwZ7&0elSyw~|K=pe)f2wP zcN?0R*vn?o{Y)um$g@>}E}S<3N-mm*s-L0tUem9PG6S)-k|TXPm7 G{sdk1AO1M%w(7oE zy19iVVH|%kWs0p^)y3@}-#M)!th9(WL#e1!F+~B^#olG&cWyX>W@+rpQTVNH%(E-x z$FGH*@DXuP23gWEis8slMYlIB%9%r%ZF(0$(oktw#cl-BzPRVzu0-%e-|!cB|6&EJ zY9$1^P-=IPIOomW4_zPM-L2_N2D{c*eV b99^MMuT=U^42%KE9A2IstGNA2+UI(?{1LTxOF2NZc>o z4lAG9x`DT8Ge5kXZ-T`UPr;IWee%E91qAwJX4ZMbAe79UKI@y!cM9gd`ay-0!oK9c zM {z(<8E?8;y)%HX2)>wbwe*2`yqy8Fi)Bf3>~?qs&20VKR8;m zbVDXujigDk3p%r9B-;)3n>6wi$#Pq+Avykhn{YSXNPTR0d}J7#ZnQJ8>}foguT6hI zIqJTdX^wa *v^*WIrkE*fPUWv5rN1_MTmWY((_Zfs$;hd1sbCR=lD zJ8ECgR0r+VJ=F5Kf4u3Z*J8A4AciDkSK08R(X+ht!cz%s ^C4I zFJ?-PJXLv>bVw1A<5(WDYVVqAoXL||f4=M &~9mH6@g^ z_??UlniVFqXbs8aamNLT2=5m$l^xaFBw)0>>o#0=J_>zD_Ld2Qn=<>Fjk1XBthg)g zEqhNG;RR3Qr2VfK% LT0RLVb%w}9f{hv- zr3;Z|*H$nTumL?`!@f;*;*Dw_!PVTzIygcyN+{=qpHi1*vP80s{L>tlNtO`hvw&+T zAz@-Q )`5V~8Is7NO=D)x_ALoRYYL) u;4K`g==P{ZD1{?|wYs`Tc8`_Zyn@cVFIlP+ny4mEXAa zCx UbV`2oan|uveYX88-@G&Gjp%4z8A$d z=?ZOKg6sU8aw4VDeY>HUX&{d`KD`Iyvu@bq42>9)B|t!In~$9B^3fT68qHPsdwzY2 z<~BX{cUShWR50xrWLZgcK2n-xu89&o?RF_uJ5XFI#;L-u9h}492__LXF!$)L7ne8M zc)tWihAeQ48uv8^(xX0nRCU+E9T!=NR^f2@;aC&~90$DLf6er8p&TYSMX*P*i{wM6 zM4HFxn9Ka9&-eanlB8NST`p{rE%INbTbwCzbnP_Lw^FD`t6HtF4#9Not+$W-k(mX0 zDz?fl0iw35hjJqcnZd*kGdiMoM{l=P;*t?a_>i9h%Xgy5bh!HzN9|F?Zvs<(Y_muO zI>r86Sjcdq-~Q{9(b^!+KncR}J;Y!NSejH2Bhv6@UHZGL|Q&XcS&24gMrZQ?9x)RGz(q zU?v~qZ)~zB^Nn9|HJ=|l-ECo5Ua-9oEG>#cup(S25^Y_c6Kmu8o&a>v?+95KigQFE zEUH{l5Y&i#P<=T9KspBswhD85guCnB-xwKp{r*LcK7K#Ii%S7lq`s-i+o^iNXc)e- zFIl!9j_aFwJYM6qL!dG1UCVdW9jD@Qy+e;TM!nokP48Y$MfvF3_(1VLM1RPtUomNu zN5;{n$P__Mdn0!e`se~>VdT5w#Z@5^P4sKNrVX4O>uCS@{)cWCPalOG@s&`v^j#MU zBYPTm`Eg+x06;7 55o!iZ|{e0;TC(S02rXmS!DVh-HULJTN zFbkYnx2*WLEU5)yt`VXhUb`&lg&PWGDKA`glFmv}g|N^`{;Zq9o?gxNZ`Zs+?PH~X zJ;Muka*Nvd1W%Frg8>vIDh64p;~x`wc#KV$4IGJV_Y5QC+g5sVzLs8#W?NrVbzxV* zhtV)L{WQM$bNWEz%e-i?NZs%1>`KIz&DurTMRLRrt4Yo)cfe)flxoW6^;s7|Na8F= z*JBe(TkkGQ2sdVnjo27F4BJZrI^h@eG3P1 od+{_!8fen-KS{{3@;I zzYs0eFj*K+4QsNgY^f#9(ApM>>7w<(B6LQ;4`DdFoKPwW*4$*6nSbS}k9BT-N!Q|# zHke7pT>O%8H--o k?fIx+Xr;#ei>=WS zlP_t$?%^JL@M^3YD$e^-;#AtWTFwrxmi_IT_A08ud~+q$C@3d7qzl#%v%0*R QJ=m542Wd3=;--PaYE7T N2rr6m3eQ?BF zm&4Mg46`?f`8--49HA2t-F(ZKBTSnT_geXDebkFr8P~r?)$S| Gj0g5#-0igYIsG8ol_ADE7yprlP(4# zZAWQ_njKg9wD~&DdqnkxEck`XBYc^0n!F2>>`VjaOa48QyJoeq-Rj}j{HYp6f|z3F zu_;>}JyJA0tbAzq3%3B?cNGOlqz)hGQu@dep1cdlbdd=Onbz|v_7ii>`=(fZI5X5O z{MhuW(Xrv1=6D&VZZW%X90})3!@O+-2YF)79@VoYfp`uf)wdTJj~e}3C%NBOq+0%T zjEUA-BvP2LTK@G3q5gq^HN_67L@jADJ?Y4Dy8X>8sGsDR+5FM~yk;WIPL*pb@!^Qk zx9%oB_jj=MuMGRq0O* &y5N*rpk(NJ2$WKzc0Yco>ZIB&&VX&^Pu%BWkyWgwagt z^j)@p;5H%q$H2`bzaOlXNIxGC0F40GfXGpc_ORe~1Cg^)B;NE|-ftVh5 x>JCoi z_{Pqc&W4rmsLdrX*+ 2$7eMgLuz3enTn zHj-nO#12!`Y7yo(Nm6QEwJk4@%HOsco&G}Xu_MPlZ~w*^UmEByn;01n*xI#Oy@WY& z$MgNYK%cw$k@wcUZuKb|>mL0cu&apbH(zceA`0%rDp=}oo$r(PgDIfob+{u`ElM+o z;!6*|_MU(Q4i7w_f_$Go_pd}>{AKWl{Hx*u_Q*R9{9EWlQ|)!F>67zhY2)RNJ5 z0BGCp^vr7j@?r8uNJsb?j7>6!lUzr_db?4Vi@l3#QZTQ0btO Cl)xL~n#iLkvgJLPJBKJ!v zdO~7Rsozk(sDsEt3#Jfx&&VdslFla@tL$C@TT4FJ==0iTQsR$ On5;cfp7Rf^-ouPNF}I1&i);+KbL zc7^76FvfBDPY{uXw9Dgu_a8Oyzt87Y{@EA3fz-5Z`LB)T=l{1w!vD90z`xQqZ;_2i z?pc|ie~E&t|Mgim{j$*BT@f*!vAeEJJ9q$_0yYJUldYgu MPY{U7 zLorUctPEEh|D}!pj>}=h5GRwpE&{rwdRdzymGeVM%ZOe5k51#}J0-9Na^3c<$bfzV zZ`2ceQ;l_vIE?Uz!!w!R=8xc{e1Zj`4Blj7jlKDFz`F48_BzoqU+(4Ggv4~;iUgxo z*)#jbOUI@Ty#Rbttybm@P0p?!GkkJW*T1FaJxI@S;`y3##DhFJ3pmh4Tem+27qmIp z%k-t{w+h~q%n!fs7tY<&)ml#2=Aic!=uYh!o;I!D1os?vm0~k_dG;Dd;4Er9k}4X& z)TS#>Bjbb8#ujVj!y9@O+f~_~vk7vDL^!rA`4fuCbN-OC+c{LyH-gu#5tuo42tT?B zX)}G>V{UZIBs#&{{T!wg-e0;BVQ9Ue>KU6NEL+3VC~xYMFARlHP_NV@l-1e%^OrP~ zVtX_0>z=K+u-9-$>6*>{Y5~`NBru(5t^5gku+F=*^{jER_cf$A T5B5yXS8Uu{rNkp~y?Atc|W8x#QU-)^MYuaYzO z;OQ2jn>%Z32;#FwHP2%=IUNIDygtIN_U_w@n5gd#QC)ja5h;ew%?Tb1r~a3mSD7*W zEfUhpIJ;N)JT(p8K`}~Dx@&;calh2k1NOeV7D7JY+YtF(9YrWv$nOTHuhGYbJDCSG z3T+@f{-ZE11cWHN!hEw=i0o#CC(H21&3az>NHeYv1*SN}>d@7jDBN;X0Zxom4kf z@ZS~nS9CPFi|q3QpvPRdmvM(1{Se%PWPxQH;fXhpooCa?1ZWFw#pjU!GJcvkNUa^7 z7?%3gb?j$SPSQ`2*V`Jj-h1O~ &gzFoWl-;WR_&cIcjO3$2!`6;D$0|Urls0G}y &M0{~Qshux_;;nTB~?smX(DdkvIVAvJp_v|ji&OYDXGAGPcPf?c$!Eu2yC zxrThZ{`lFdv_-l2@t9xO%geLj!bM9w@^@@p>hR^7_C1H{Wjsv ?itjC3)(eTOuy7Qpc&zc +UhAcb?|$ zh^j6(mgJrCGq!a$TZCx6;#fK3AZq PrVr6k#wm4}I|q>(P%k7OLXD zz4W|T*W(nh-L;-X+O^m^J;j}H8g**O!Lmp!B;NSDmO4#3QYj@|U!+!<5jZ6}@=(oF zZu~plTh_Sm=Pbwwrpn+=x6Nv97+a{hY!LP_Vk{VLywN+r+Fs&*6D=&^rA^Khgi4Dv za(BM32+s?S(0QEb8tYkjpjg6pO`&y)Wkbql1~=^21cY-VbBh(HXqEz`oIv}<)t?T* z@E6#rJXPDNmp4kspzUn2{