diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..f73344d --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/qym.iml b/.idea/qym.iml new file mode 100644 index 0000000..5e764c4 --- /dev/null +++ b/.idea/qym.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.version b/.version index 7813681..abac1ea 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -5 \ No newline at end of file +47 diff --git a/DEPLOY.md b/DEPLOY.md deleted file mode 100644 index 81233cb..0000000 --- a/DEPLOY.md +++ /dev/null @@ -1,279 +0,0 @@ -# 泉友米车友会 - 部署说明 - -版本: 0.0.1 - -## 🚀 快速部署(推荐) - -### Windows 用户 - -```cmd -# 1. 构建镜像 -build.bat - -# 2. 启动服务 -docker-compose up -d - -# 3. 初始化管理员 -docker exec -it quanyoumi-app sh -node init-admin.js -``` - -### Linux/Mac 用户 - -```bash -# 1. 构建镜像 -chmod +x build.sh -./build.sh - -# 2. 启动服务 -docker-compose up -d - -# 3. 初始化管理员 -docker exec -it quanyoumi-app sh -node init-admin.js -``` - -## 📝 部署前准备 - -### 1. 修改配置文件 - -编辑 `docker-compose.yml`,修改以下配置: - -```yaml -environment: - - DB_PASSWORD=your_mysql_password # 修改数据库密码 - - JWT_SECRET=your_jwt_secret_key # 修改JWT密钥(随机字符串) - - AMAP_KEY=your_amap_key # 填入高德地图Key -``` - -同时修改 MySQL 服务的密码: - -```yaml -db: - environment: - - MYSQL_ROOT_PASSWORD=your_mysql_password # 与上面保持一致 -``` - -### 2. 获取高德地图 Key - -1. 访问 https://console.amap.com/ -2. 注册/登录账号 -3. 创建应用,选择 Web 端(JS API) -4. 复制 Key 填入配置 - -## 🔧 详细步骤 - -### 步骤 1: 构建 Docker 镜像 - -镜像包含前端(Vue3)和后端(Node.js),版本号 0.0.1。 - -**Windows:** -```cmd -build.bat -``` - -**Linux/Mac:** -```bash -chmod +x build.sh -./build.sh -``` - -### 步骤 2: 启动服务 - -使用 docker-compose 启动完整环境(应用+MySQL+MinIO): - -```bash -docker-compose up -d -``` - -查看启动状态: -```bash -docker-compose ps -``` - -查看日志: -```bash -docker-compose logs -f app -``` - -### 步骤 3: 初始化数据库 - -数据库会自动初始化(使用 database.sql),等待约 30 秒。 - -检查数据库是否就绪: -```bash -docker-compose logs db | grep "ready for connections" -``` - -### 步骤 4: 创建管理员账号 - -进入应用容器: -```bash -docker exec -it quanyoumi-app sh -``` - -运行初始化脚本: -```bash -node init-admin.js -``` - -按提示输入: -- 用户名(例如:admin) -- 密码(至少6位) -- 确认密码 - -退出容器: -```bash -exit -``` - -### 步骤 5: 访问应用 - -- **前端页面**: http://localhost:3000 -- **后台管理**: http://localhost:3000/admin/login -- **MinIO 控制台**: http://localhost:9001 (minioadmin/minioadmin) - -## 📦 镜像信息 - -- **镜像名称**: quanyoumi-app -- **版本**: 0.0.1 -- **基础镜像**: node:18-alpine -- **大小**: 约 200MB -- **包含内容**: - - 前端构建产物(Vue3 + Vant4) - - 后端服务(Node.js + Express) - - 依赖包(仅生产环境) - -## 🔍 验证部署 - -### 1. 检查服务状态 - -```bash -docker-compose ps -``` - -应该看到 3 个服务都是 Up 状态: -- quanyoumi-app -- quanyoumi-db -- quanyoumi-minio - -### 2. 检查应用日志 - -```bash -docker-compose logs app -``` - -应该看到: -``` -服务器运行在 http://localhost:3000 -运行模式: 生产环境(前后端一体) -``` - -### 3. 测试访问 - -浏览器访问 http://localhost:3000,应该能看到首页。 - -## 🛠️ 常见问题 - -### 问题 1: 端口被占用 - -错误信息:`port is already allocated` - -解决方法: -```bash -# 修改 docker-compose.yml 中的端口映射 -ports: - - "3001:3000" # 改为其他端口 -``` - -### 问题 2: 数据库连接失败 - -错误信息:`ECONNREFUSED` 或 `Access denied` - -解决方法: -1. 检查数据库是否启动:`docker-compose ps db` -2. 检查密码是否一致(app 和 db 的密码要相同) -3. 等待数据库完全启动(约 30 秒) - -### 问题 3: 前端页面空白 - -解决方法: -1. 检查浏览器控制台是否有错误 -2. 确认后端 API 是否正常:访问 http://localhost:3000/api/activities -3. 重新构建镜像:`docker-compose build --no-cache` - -### 问题 4: 图片上传失败 - -解决方法: -1. 检查 MinIO 是否运行:`docker-compose ps minio` -2. 访问 MinIO 控制台创建 bucket:http://localhost:9001 -3. 检查环境变量配置是否正确 - -## 🔄 更新应用 - -```bash -# 1. 停止服务 -docker-compose down - -# 2. 拉取最新代码 -git pull - -# 3. 重新构建 -docker-compose build --no-cache - -# 4. 启动服务 -docker-compose up -d -``` - -## 🗑️ 清理 - -### 停止服务(保留数据) -```bash -docker-compose down -``` - -### 完全清理(删除数据) -```bash -docker-compose down -v -``` - -### 删除镜像 -```bash -docker rmi quanyoumi-app:0.0.1 -``` - -## 📊 生产环境建议 - -1. **使用反向代理** - - 推荐使用 Nginx 或 Caddy - - 配置 HTTPS 证书 - - 设置域名 - -2. **修改默认密码** - - MySQL root 密码 - - MinIO 访问密钥 - - JWT 密钥(长随机字符串) - -3. **数据备份** - - 定期备份 MySQL 数据 - - 备份 MinIO 存储 - - 备份上传文件 - -4. **监控告警** - - 配置日志收集 - - 设置资源监控 - - 配置告警通知 - -5. **安全加固** - - 限制数据库外部访问 - - 使用防火墙规则 - - 定期更新镜像 - -## 📞 技术支持 - -如有问题,请查看: -1. 应用日志:`docker-compose logs app` -2. 数据库日志:`docker-compose logs db` -3. MinIO 日志:`docker-compose logs minio` - -或联系技术支持团队。 diff --git a/PRIZE-FEATURE.md b/PRIZE-FEATURE.md deleted file mode 100644 index e69de29..0000000 diff --git a/backend/data/database.sqlite b/backend/data/database.sqlite new file mode 100644 index 0000000..546e04a Binary files /dev/null and b/backend/data/database.sqlite differ diff --git a/backend/fix-expense-media.js b/backend/fix-expense-media.js new file mode 100644 index 0000000..0442e24 --- /dev/null +++ b/backend/fix-expense-media.js @@ -0,0 +1,49 @@ +const { Expense } = require('./models'); + +async function fixExpenseMedia() { + try { + console.log('开始修复经费媒体数据...'); + + const expenses = await Expense.findAll(); + console.log(`找到 ${expenses.length} 条经费记录`); + + let fixedCount = 0; + + for (const expense of expenses) { + let needUpdate = false; + let media = expense.media || []; + + // 检查是否有异常数据 + if (Array.isArray(media) && media.length > 5) { + console.log(`经费 #${expense.id} 有 ${media.length} 个媒体文件,超过限制`); + media = media.slice(0, 5); + needUpdate = true; + } + + // 检查是否有无效的媒体项 + const validMedia = media.filter(item => { + return item && item.url && (item.type === 'image' || item.type === 'video'); + }); + + if (validMedia.length !== media.length) { + console.log(`经费 #${expense.id} 有无效媒体项,从 ${media.length} 清理到 ${validMedia.length}`); + media = validMedia; + needUpdate = true; + } + + if (needUpdate) { + await expense.update({ media }); + fixedCount++; + console.log(`✓ 修复经费 #${expense.id}`); + } + } + + console.log(`\n修复完成!共修复 ${fixedCount} 条记录`); + process.exit(0); + } catch (error) { + console.error('修复失败:', error); + process.exit(1); + } +} + +fixExpenseMedia(); diff --git a/backend/init-admin.js b/backend/init-admin.js index 9b60001..0ef2781 100644 --- a/backend/init-admin.js +++ b/backend/init-admin.js @@ -5,39 +5,66 @@ 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' } }); + // 检查role字段是否存在 + const [results] = await sequelize.query("PRAGMA table_info(Admins);"); + const hasRoleColumn = results.some(col => col.name === 'role'); - if (existingAdmin) { - console.log('✓ 管理员账号已存在'); - console.log(' 用户名: admin'); - console.log(' 提示: 如需重置密码,请删除数据库文件后重新初始化'); - process.exit(0); + if (!hasRoleColumn) { + console.log('⚠️ role字段不存在,请先运行迁移: node backend/migrations/index.js'); + console.log(' 或运行: npm run migrate (在backend目录)'); + process.exit(1); return; } - const hashedPassword = bcrypt.hashSync('admin123', 10); - await Admin.create({ - username: 'admin', - password: hashedPassword, - name: '管理员' - }); + // 检查yvan超级管理员 + const yvan = await Admin.findOne({ where: { username: 'yvan' } }); + if (!yvan) { + const hashedPassword = bcrypt.hashSync('Xu950329.', 10); + await Admin.create({ + username: 'yvan', + password: hashedPassword, + name: 'Yvan', + role: 'super' + }); + console.log('✓ 超级管理员yvan创建成功'); + } else { + // 确保yvan是super角色 + if (yvan.role !== 'super') { + await yvan.update({ role: 'super' }); + console.log('✓ 超级管理员yvan角色已更新'); + } else { + console.log('✓ 超级管理员yvan已存在'); + } + } - console.log('✓ 管理员账号创建成功'); - console.log(' 用户名: admin'); - console.log(' 密码: admin123'); + // 检查默认管理员 + const admin = await Admin.findOne({ where: { username: 'admin' } }); + if (!admin) { + const hashedPassword = bcrypt.hashSync('admin123', 10); + await Admin.create({ + username: 'admin', + password: hashedPassword, + name: '管理员', + role: 'admin' + }); + console.log('✓ 默认管理员admin创建成功'); + } else { + // 确保admin是admin角色 + if (!admin.role || admin.role === 'super') { + await admin.update({ role: 'admin' }); + console.log('✓ 默认管理员admin角色已更新'); + } else { + console.log('✓ 默认管理员admin已存在'); + } + } + + console.log('\n初始化完成!'); process.exit(0); } catch (error) { console.error('✗ 初始化失败:', error.message); - if (error.name === 'SequelizeUniqueConstraintError') { - console.error(' 原因: 管理员账号已存在'); - } else { - console.error(' 详细错误:', error); - } + console.error(' 详细错误:', error); process.exit(1); } } diff --git a/backend/middleware/authSuper.js b/backend/middleware/authSuper.js new file mode 100644 index 0000000..3c28cfa --- /dev/null +++ b/backend/middleware/authSuper.js @@ -0,0 +1,27 @@ +const jwt = require('jsonwebtoken'); +const { Admin } = require('../models'); + +const authSuper = async (req, res, next) => { + const token = req.header('Authorization')?.replace('Bearer ', ''); + + if (!token) { + return res.status(401).json({ error: '未授权访问' }); + } + + try { + const decoded = jwt.verify(token, process.env.JWT_SECRET); + const admin = await Admin.findByPk(decoded.id); + + if (!admin || admin.role !== 'super') { + return res.status(403).json({ error: '需要超级管理员权限' }); + } + + req.adminId = decoded.id; + req.admin = admin; + next(); + } catch (error) { + res.status(401).json({ error: '无效的token' }); + } +}; + +module.exports = authSuper; diff --git a/backend/migrations/0005-add-prize-source.js b/backend/migrations/0005-add-prize-source.js new file mode 100644 index 0000000..6fe149b --- /dev/null +++ b/backend/migrations/0005-add-prize-source.js @@ -0,0 +1,39 @@ +module.exports = { + async up(manager) { + console.log(' - 添加奖品来源字段...'); + + // 添加source字段,默认值为purchase + await manager.executeSql(` + ALTER TABLE Prizes ADD COLUMN source TEXT DEFAULT 'purchase'; + `); + + // 添加sponsor字段 + await manager.executeSql(` + ALTER TABLE Prizes ADD COLUMN sponsor TEXT; + `); + + // 为现有数据设置默认值 + await manager.executeSql(` + UPDATE Prizes SET source = 'purchase' WHERE source IS NULL; + `); + + console.log(' - 奖品来源字段添加完成'); + }, + + async down(manager) { + console.log(' - 移除奖品来源字段...'); + + // SQLite不支持DROP COLUMN,需要重建表 + await manager.executeSql(` + CREATE TABLE Prizes_backup AS SELECT + id, activityId, name, level, description, image, quantity, sortOrder, createdAt, updatedAt + FROM Prizes; + `); + + await manager.executeSql(`DROP TABLE Prizes;`); + + await manager.executeSql(`ALTER TABLE Prizes_backup RENAME TO Prizes;`); + + console.log(' - 奖品来源字段移除完成'); + } +}; diff --git a/backend/migrations/0006-add-grand-prize-level.js b/backend/migrations/0006-add-grand-prize-level.js new file mode 100644 index 0000000..502df9e --- /dev/null +++ b/backend/migrations/0006-add-grand-prize-level.js @@ -0,0 +1,74 @@ +module.exports = { + async up(manager) { + console.log(' - 添加特等奖等级...'); + + // SQLite不支持直接修改CHECK约束,需要重建表 + // 1. 创建新表结构(包含特等奖) + await manager.executeSql(` + CREATE TABLE Prizes_new ( + 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, + source TEXT DEFAULT 'purchase', + sponsor TEXT, + createdAt DATETIME NOT NULL, + updatedAt DATETIME NOT NULL + ); + `); + + // 2. 复制数据 + await manager.executeSql(` + INSERT INTO Prizes_new + SELECT * FROM Prizes; + `); + + // 3. 删除旧表 + await manager.executeSql(`DROP TABLE Prizes;`); + + // 4. 重命名新表 + await manager.executeSql(`ALTER TABLE Prizes_new RENAME TO Prizes;`); + + console.log(' - 特等奖等级添加完成'); + }, + + async down(manager) { + console.log(' - 移除特等奖等级...'); + + // 1. 创建旧表结构(不包含特等奖) + await manager.executeSql(` + CREATE TABLE Prizes_old ( + 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, + source TEXT DEFAULT 'purchase', + sponsor TEXT, + createdAt DATETIME NOT NULL, + updatedAt DATETIME NOT NULL + ); + `); + + // 2. 复制数据(排除特等奖) + await manager.executeSql(` + INSERT INTO Prizes_old + SELECT * FROM Prizes WHERE level != '特等奖'; + `); + + // 3. 删除新表 + await manager.executeSql(`DROP TABLE Prizes;`); + + // 4. 重命名旧表 + await manager.executeSql(`ALTER TABLE Prizes_old RENAME TO Prizes;`); + + console.log(' - 特等奖等级移除完成'); + } +}; diff --git a/backend/migrations/0007-add-admin-role.js b/backend/migrations/0007-add-admin-role.js new file mode 100644 index 0000000..7a9f6cf --- /dev/null +++ b/backend/migrations/0007-add-admin-role.js @@ -0,0 +1,12 @@ +// 添加管理员角色字段 +module.exports = { + async up(manager) { + console.log(' - 添加 Admins.role 字段...'); + await manager.executeSql('ALTER TABLE Admins ADD COLUMN role TEXT DEFAULT "admin";'); + console.log(' - 角色字段添加完成'); + }, + + async down(manager) { + await manager.executeSql('ALTER TABLE Admins DROP COLUMN role;'); + } +}; diff --git a/backend/migrations/0008-add-fourth-prize-level.js b/backend/migrations/0008-add-fourth-prize-level.js new file mode 100644 index 0000000..b69ba3f --- /dev/null +++ b/backend/migrations/0008-add-fourth-prize-level.js @@ -0,0 +1,75 @@ +// 添加四等奖等级 +module.exports = { + async up(manager) { + console.log(' - 添加四等奖等级...'); + + // SQLite不支持直接修改CHECK约束,需要重建表 + // 1. 创建新表结构(包含四等奖) + await manager.executeSql(` + CREATE TABLE Prizes_new ( + 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, + source TEXT DEFAULT 'purchase', + sponsor TEXT, + createdAt DATETIME NOT NULL, + updatedAt DATETIME NOT NULL + ); + `); + + // 2. 复制数据 + await manager.executeSql(` + INSERT INTO Prizes_new + SELECT * FROM Prizes; + `); + + // 3. 删除旧表 + await manager.executeSql(`DROP TABLE Prizes;`); + + // 4. 重命名新表 + await manager.executeSql(`ALTER TABLE Prizes_new RENAME TO Prizes;`); + + console.log(' - 四等奖等级添加完成'); + }, + + async down(manager) { + console.log(' - 移除四等奖等级...'); + + // 1. 创建旧表结构(不包含四等奖) + await manager.executeSql(` + CREATE TABLE Prizes_old ( + 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, + source TEXT DEFAULT 'purchase', + sponsor TEXT, + createdAt DATETIME NOT NULL, + updatedAt DATETIME NOT NULL + ); + `); + + // 2. 复制数据(排除四等奖) + await manager.executeSql(` + INSERT INTO Prizes_old + SELECT * FROM Prizes WHERE level != '四等奖'; + `); + + // 3. 删除新表 + await manager.executeSql(`DROP TABLE Prizes;`); + + // 4. 重命名旧表 + await manager.executeSql(`ALTER TABLE Prizes_old RENAME TO Prizes;`); + + console.log(' - 四等奖等级移除完成'); + } +}; diff --git a/backend/migrations/0009-add-group-status.js b/backend/migrations/0009-add-group-status.js new file mode 100644 index 0000000..89a52d6 --- /dev/null +++ b/backend/migrations/0009-add-group-status.js @@ -0,0 +1,20 @@ +/** + * 迁移 0009: 添加进群状态字段 + * 为 Registrations 表添加 groupStatus 字段 + */ + +module.exports = { + async up(manager) { + console.log(' - 添加 Registrations.groupStatus 字段...'); + await manager.executeSql(` + ALTER TABLE Registrations ADD COLUMN groupStatus INTEGER DEFAULT 0; + `); + }, + + async down(manager) { + console.log(' - 移除 Registrations.groupStatus 字段...'); + await manager.executeSql(` + ALTER TABLE Registrations DROP COLUMN groupStatus; + `); + } +}; diff --git a/backend/models/Admin.js b/backend/models/Admin.js index dc7eabe..6334b7b 100644 --- a/backend/models/Admin.js +++ b/backend/models/Admin.js @@ -16,7 +16,12 @@ const Admin = sequelize.define('Admin', { type: DataTypes.STRING, allowNull: false }, - name: DataTypes.STRING + name: DataTypes.STRING, + role: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: 'admin' + } }); module.exports = Admin; diff --git a/backend/models/Prize.js b/backend/models/Prize.js index 2a5d122..c05a221 100644 --- a/backend/models/Prize.js +++ b/backend/models/Prize.js @@ -16,7 +16,7 @@ const Prize = sequelize.define('Prize', { allowNull: false }, level: { - type: DataTypes.ENUM('一等奖', '二等奖', '三等奖', '参与奖', '伴手礼'), + type: DataTypes.ENUM('特等奖', '一等奖', '二等奖', '三等奖', '四等奖', '参与奖', '伴手礼'), allowNull: false }, description: DataTypes.TEXT, @@ -29,6 +29,15 @@ const Prize = sequelize.define('Prize', { type: DataTypes.INTEGER, defaultValue: 0, comment: '排序值,数字越小越靠前' + }, + source: { + type: DataTypes.ENUM('member', 'sponsor', 'purchase'), + defaultValue: 'purchase', + comment: '来源: member-群友赞助, sponsor-赞助商提供, purchase-采购' + }, + sponsor: { + type: DataTypes.STRING, + comment: '赞助者名称(群友或赞助商)' } }); diff --git a/backend/models/Registration.js b/backend/models/Registration.js index c36d62e..468f21e 100644 --- a/backend/models/Registration.js +++ b/backend/models/Registration.js @@ -34,7 +34,11 @@ const Registration = sequelize.define('Registration', { type: DataTypes.BOOLEAN, defaultValue: false }, - checkInTime: DataTypes.DATE + checkInTime: DataTypes.DATE, + groupStatus: { + type: DataTypes.BOOLEAN, + defaultValue: false + } }); module.exports = Registration; diff --git a/backend/models/VenueImage.js b/backend/models/VenueImage.js new file mode 100644 index 0000000..b8a07c5 --- /dev/null +++ b/backend/models/VenueImage.js @@ -0,0 +1,25 @@ +const { DataTypes } = require('sequelize'); +const sequelize = require('./db'); + +const VenueImage = sequelize.define('VenueImage', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + activityId: { + type: DataTypes.INTEGER, + allowNull: false + }, + image: { + type: DataTypes.STRING, + allowNull: false + }, + sortOrder: { + type: DataTypes.INTEGER, + defaultValue: 0, + comment: '排序值,数字越小越靠前' + } +}); + +module.exports = VenueImage; diff --git a/backend/models/index.js b/backend/models/index.js index fb667fd..0d2e6a8 100644 --- a/backend/models/index.js +++ b/backend/models/index.js @@ -6,6 +6,7 @@ const Photo = require('./Photo'); const Sponsor = require('./Sponsor'); const Prize = require('./Prize'); const Admin = require('./Admin'); +const VenueImage = require('./VenueImage'); // 同步数据库 // 使用 force: false 避免 SQLite alter 表时的约束冲突问题 @@ -23,5 +24,6 @@ module.exports = { Photo, Sponsor, Prize, - Admin + Admin, + VenueImage }; diff --git a/backend/package-lock.json b/backend/package-lock.json index 823a4b7..e203b6c 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -16,12 +16,69 @@ "jsonwebtoken": "^9.0.2", "multer": "^1.4.5-lts.1", "sequelize": "^6.33.0", - "sql.js": "^1.14.1" + "sql.js": "^1.14.1", + "sqlite3": "^5.1.7" }, "devDependencies": { "nodemon": "^3.0.1" } }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "license": "MIT", + "optional": true + }, + "node_modules/@npmcli/fs": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/@npmcli/fs/-/fs-1.1.1.tgz", + "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "@gar/promisify": "^1.0.1", + "semver": "^7.3.5" + } + }, + "node_modules/@npmcli/move-file": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/@npmcli/move-file/-/move-file-1.1.2.tgz", + "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "license": "MIT", + "optional": true, + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@npmcli/move-file/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/@types/debug": { "version": "4.1.13", "resolved": "https://registry.npmmirror.com/@types/debug/-/debug-4.1.13.tgz", @@ -52,6 +109,13 @@ "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==", "license": "MIT" }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC", + "optional": true + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz", @@ -65,6 +129,81 @@ "node": ">= 0.6" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "optional": true + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmmirror.com/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "license": "MIT", + "optional": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz", @@ -85,6 +224,43 @@ "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", "license": "MIT" }, + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "license": "ISC", + "optional": true + }, + "node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/are-we-there-yet/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==", + "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmmirror.com/array-flatten/-/array-flatten-1.1.1.tgz", @@ -291,6 +467,49 @@ "node": ">= 0.8" } }, + "node_modules/cacache": { + "version": "15.3.0", + "resolved": "https://registry.npmmirror.com/cacache/-/cacache-15.3.0.tgz", + "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "@npmcli/fs": "^1.0.0", + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^6.0.0", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/cacache/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -345,6 +564,42 @@ "fsevents": "~2.3.2" } }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "optional": true, + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT", + "optional": true + }, "node_modules/concat-stream": { "version": "1.6.2", "resolved": "https://registry.npmmirror.com/concat-stream/-/concat-stream-1.6.2.tgz", @@ -360,6 +615,13 @@ "typedarray": "^0.0.6" } }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC", + "optional": true + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-0.5.4.tgz", @@ -452,6 +714,13 @@ "node": ">=4.0.0" } }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT", + "optional": true + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz", @@ -527,6 +796,13 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT", + "optional": true + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz", @@ -536,6 +812,29 @@ "node": ">= 0.8" } }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmmirror.com/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/end-of-stream": { "version": "1.4.5", "resolved": "https://registry.npmmirror.com/end-of-stream/-/end-of-stream-1.4.5.tgz", @@ -545,6 +844,23 @@ "once": "^1.4.0" } }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmmirror.com/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "license": "MIT", + "optional": true + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", @@ -706,6 +1022,25 @@ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "license": "MIT" }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC", + "optional": true + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", @@ -730,6 +1065,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -773,6 +1129,28 @@ "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", "license": "MIT" }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", @@ -786,6 +1164,37 @@ "node": ">= 6" } }, + "node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT", + "optional": true + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.13", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "license": "MIT", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "optional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", @@ -798,6 +1207,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC", + "optional": true + }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-3.0.0.tgz", @@ -820,6 +1236,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC", + "optional": true + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", @@ -832,6 +1255,13 @@ "node": ">= 0.4" } }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "license": "BSD-2-Clause", + "optional": true + }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.1.tgz", @@ -852,6 +1282,95 @@ "url": "https://opencollective.com/express" } }, + "node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/http-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "optional": true + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "optional": true + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.0.0" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -891,6 +1410,33 @@ "dev": true, "license": "ISC" }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "license": "ISC", + "optional": true + }, "node_modules/inflection": { "version": "1.13.4", "resolved": "https://registry.npmmirror.com/inflection/-/inflection-1.13.4.tgz", @@ -900,6 +1446,18 @@ ], "license": "MIT" }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "optional": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", @@ -912,6 +1470,16 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "license": "ISC" }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmmirror.com/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -944,6 +1512,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", @@ -957,6 +1535,13 @@ "node": ">=0.10.0" } }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "license": "MIT", + "optional": true + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz", @@ -973,6 +1558,13 @@ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "license": "MIT" }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC", + "optional": true + }, "node_modules/jsonwebtoken": { "version": "9.0.3", "resolved": "https://registry.npmmirror.com/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", @@ -1070,6 +1662,47 @@ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", "license": "MIT" }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-fetch-happen": { + "version": "9.1.0", + "resolved": "https://registry.npmmirror.com/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", + "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", + "license": "ISC", + "optional": true, + "dependencies": { + "agentkeepalive": "^4.1.3", + "cacache": "^15.2.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^6.0.0", + "minipass": "^3.1.3", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^1.3.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.2", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^6.0.0", + "ssri": "^8.0.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -1176,6 +1809,101 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmmirror.com/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-fetch": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/minipass-fetch/-/minipass-fetch-1.4.1.tgz", + "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "license": "MIT", + "optional": true, + "dependencies": { + "minipass": "^3.1.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "optionalDependencies": { + "encoding": "^0.1.12" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/minipass-flush/-/minipass-flush-1.0.7.tgz", + "integrity": "sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==", + "license": "BlueOak-1.0.0", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmmirror.com/mkdirp/-/mkdirp-0.5.6.tgz", @@ -1267,6 +1995,37 @@ "node": ">=10" } }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, + "node_modules/node-gyp": { + "version": "8.4.1", + "resolved": "https://registry.npmmirror.com/node-gyp/-/node-gyp-8.4.1.tgz", + "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", + "license": "MIT", + "optional": true, + "dependencies": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^9.1.0", + "nopt": "^5.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": ">= 10.12.0" + } + }, "node_modules/nodemon": { "version": "3.1.14", "resolved": "https://registry.npmmirror.com/nodemon/-/nodemon-3.1.14.tgz", @@ -1321,6 +2080,22 @@ "dev": true, "license": "MIT" }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", @@ -1331,6 +2106,23 @@ "node": ">=0.10.0" } }, + "node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", @@ -1373,6 +2165,22 @@ "wrappy": "1" } }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz", @@ -1382,6 +2190,16 @@ "node": ">= 0.8" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.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", @@ -1440,6 +2258,27 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "license": "MIT" }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "license": "ISC", + "optional": true + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "license": "MIT", + "optional": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -1558,12 +2397,39 @@ "node": ">=8.10.0" } }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmmirror.com/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 4" + } + }, "node_modules/retry-as-promised": { "version": "7.1.1", "resolved": "https://registry.npmmirror.com/retry-as-promised/-/retry-as-promised-7.1.1.tgz", "integrity": "sha512-hMD7odLOt3LkTjcif8aRZqi/hybjpLNgSk5oF5FCowfCjok6LukpN2bDX7R5wDmbgBQFn7YoBxSagmtXHaJYJw==", "license": "MIT" }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "optional": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -1741,6 +2607,13 @@ "node": ">= 0.8.0" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC", + "optional": true + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -1819,6 +2692,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC", + "optional": true + }, "node_modules/simple-concat": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/simple-concat/-/simple-concat-1.0.1.tgz", @@ -1877,12 +2757,115 @@ "node": ">=10" } }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmmirror.com/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "license": "MIT", + "optional": true, + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "6.2.1", + "resolved": "https://registry.npmmirror.com/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", + "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/socks-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socks-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "optional": true + }, "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/sqlite3": { + "version": "5.1.7", + "resolved": "https://registry.npmmirror.com/sqlite3/-/sqlite3-5.1.7.tgz", + "integrity": "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "bindings": "^1.5.0", + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.1", + "tar": "^6.1.11" + }, + "optionalDependencies": { + "node-gyp": "8.x" + }, + "peerDependencies": { + "node-gyp": "8.x" + }, + "peerDependenciesMeta": { + "node-gyp": { + "optional": true + } + } + }, + "node_modules/ssri": { + "version": "8.0.1", + "resolved": "https://registry.npmmirror.com/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.2.tgz", @@ -1915,6 +2898,34 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz", @@ -1937,6 +2948,23 @@ "node": ">=4" } }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmmirror.com/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/tar-fs": { "version": "2.1.4", "resolved": "https://registry.npmmirror.com/tar-fs/-/tar-fs-2.1.4.tgz", @@ -1985,6 +3013,27 @@ "node": ">= 6" } }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -2067,6 +3116,26 @@ "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", "license": "MIT" }, + "node_modules/unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "unique-slug": "^2.0.0" + } + }, + "node_modules/unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "license": "ISC", + "optional": true, + "dependencies": { + "imurmurhash": "^0.1.4" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz", @@ -2118,6 +3187,32 @@ "node": ">= 0.8" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "optional": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmmirror.com/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "optional": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, "node_modules/wkx": { "version": "0.5.0", "resolved": "https://registry.npmmirror.com/wkx/-/wkx-0.5.0.tgz", @@ -2141,6 +3236,12 @@ "engines": { "node": ">=0.4" } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" } } } diff --git a/backend/routes/activity.js b/backend/routes/activity.js index a249cab..305b9fd 100644 --- a/backend/routes/activity.js +++ b/backend/routes/activity.js @@ -1,6 +1,6 @@ const express = require('express'); const router = express.Router(); -const { Activity, Photo, Sponsor, Expense, Registration, Prize } = require('../models'); +const { Activity, Photo, Sponsor, Expense, Registration, Prize, VenueImage } = require('../models'); const { upload, getFileUrls } = require('../middleware/upload'); // 获取活动列表 @@ -8,7 +8,11 @@ router.get('/activities', async (req, res) => { try { const { status } = req.query; const where = status ? { status } : {}; - const activities = await Activity.findAll({ where, order: [['startTime', 'DESC']] }); + const activities = await Activity.findAll({ + where, + order: [['startTime', 'DESC']], + raw: true + }); res.json(activities); } catch (error) { res.status(500).json({ error: error.message }); @@ -18,22 +22,42 @@ router.get('/activities', async (req, res) => { // 获取活动详情 router.get('/activities/:id', async (req, res) => { try { - const activity = await Activity.findByPk(req.params.id); + const activity = await Activity.findByPk(req.params.id, { raw: true }); + if (!activity) { + return res.status(404).json({ error: '活动不存在' }); + } + // 只返回已审核通过的照片 const photos = await Photo.findAll({ - where: { activityId: req.params.id, status: 'approved' } + where: { activityId: req.params.id, status: 'approved' }, + raw: true }); const sponsors = await Sponsor.findAll({ where: { activityId: req.params.id }, - order: [['sortOrder', 'ASC'], ['id', 'ASC']] + order: [['sortOrder', 'ASC'], ['id', 'ASC']], + raw: true }); - const expenses = await Expense.findAll({ where: { activityId: req.params.id } }); + const expenses = await Expense.findAll({ + where: { activityId: req.params.id }, + raw: true + }); + // 手动解析JSON字段 + const parsedExpenses = expenses.map(expense => ({ + ...expense, + media: typeof expense.media === 'string' ? JSON.parse(expense.media || '[]') : (expense.media || []) + })); const prizes = await Prize.findAll({ where: { activityId: req.params.id }, - order: [['sortOrder', 'ASC'], ['id', 'ASC']] + order: [['sortOrder', 'ASC'], ['id', 'ASC']], + raw: true + }); + const venueImages = await VenueImage.findAll({ + where: { activityId: req.params.id }, + order: [['sortOrder', 'ASC'], ['id', 'ASC']], + raw: true }); - res.json({ activity, photos, sponsors, expenses, prizes }); + res.json({ activity, photos, sponsors, expenses: parsedExpenses, prizes, venueImages: venueImages.map(img => img.image).filter(Boolean) }); } catch (error) { res.status(500).json({ error: error.message }); } @@ -42,8 +66,30 @@ router.get('/activities/:id', async (req, res) => { // 获取活动经费(公开接口) router.get('/activities/:id/expenses', async (req, res) => { try { - const expenses = await Expense.findAll({ where: { activityId: req.params.id } }); - res.json(expenses); + const expenses = await Expense.findAll({ + where: { activityId: req.params.id }, + raw: true + }); + // 手动解析JSON字段 + const parsedExpenses = expenses.map(expense => ({ + ...expense, + media: typeof expense.media === 'string' ? JSON.parse(expense.media || '[]') : (expense.media || []) + })); + res.json(parsedExpenses); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// 获取场地图片列表 +router.get('/activities/:id/venue-images', async (req, res) => { + try { + const images = await VenueImage.findAll({ + where: { activityId: req.params.id }, + order: [['sortOrder', 'ASC'], ['id', 'ASC']], + raw: true + }); + res.json(images.map(img => img.image).filter(Boolean)); } catch (error) { res.status(500).json({ error: error.message }); } diff --git a/backend/routes/admin.js b/backend/routes/admin.js index 2733384..6efdd2a 100644 --- a/backend/routes/admin.js +++ b/backend/routes/admin.js @@ -32,6 +32,20 @@ router.post('/login', async (req, res) => { } }); +// Token续期 +router.post('/refresh-token', auth, async (req, res) => { + try { + const admin = await Admin.findByPk(req.adminId); + if (!admin) { + return res.status(401).json({ error: '用户不存在' }); + } + const token = jwt.sign({ id: admin.id }, process.env.JWT_SECRET, { expiresIn: '7d' }); + res.json({ token }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + // ========== 管理员管理API(仅超级管理员) ========== // 获取管理员列表 @@ -173,10 +187,12 @@ router.put('/activities/:id', auth, async (req, res) => { // 审核报名 router.put('/registrations/:id', auth, async (req, res) => { try { - const { status, peopleCount } = req.body; + const { status, peopleCount, carNumber, groupStatus } = req.body; const updateData = {}; if (status) updateData.status = status; if (peopleCount !== undefined) updateData.peopleCount = peopleCount; + if (carNumber !== undefined) updateData.carNumber = carNumber; + if (groupStatus !== undefined) updateData.groupStatus = groupStatus; await Registration.update(updateData, { where: { id: req.params.id } }); const registration = await Registration.findByPk(req.params.id); @@ -207,6 +223,26 @@ router.put('/registrations/:id/checkin', auth, async (req, res) => { } }); +// 进群/取消进群 +router.put('/registrations/:id/group', auth, async (req, res) => { + try { + const registration = await Registration.findByPk(req.params.id); + if (!registration) { + return res.status(404).json({ error: '报名信息不存在' }); + } + + const newStatus = !registration.groupStatus; + await Registration.update({ + groupStatus: newStatus + }, { where: { id: req.params.id } }); + + const updated = await Registration.findByPk(req.params.id); + res.json(updated); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + // 获取所有报名(含待审核) router.get('/registrations', auth, async (req, res) => { try { @@ -264,8 +300,13 @@ router.get('/expenses', auth, async (req, res) => { try { const { activityId } = req.query; const where = activityId ? { activityId } : {}; - const expenses = await Expense.findAll({ where }); - res.json(expenses); + const expenses = await Expense.findAll({ where, raw: true }); + // 手动解析JSON字段 + const parsedExpenses = expenses.map(expense => ({ + ...expense, + media: typeof expense.media === 'string' ? JSON.parse(expense.media || '[]') : (expense.media || []) + })); + res.json(parsedExpenses); } catch (error) { res.status(500).json({ error: error.message }); } diff --git a/backend/routes/registration.js b/backend/routes/registration.js index 6cd8cd5..a0dd6d3 100644 --- a/backend/routes/registration.js +++ b/backend/routes/registration.js @@ -16,7 +16,7 @@ router.post('/registrations', async (req, res) => { activityId, name, phone, carModel, carNumber, note }); - res.json(registration); + res.json(registration.toJSON()); } catch (error) { res.status(500).json({ error: error.message }); } @@ -25,7 +25,7 @@ router.post('/registrations', async (req, res) => { // 查询报名状态 router.get('/registrations/:id', async (req, res) => { try { - const registration = await Registration.findByPk(req.params.id); + const registration = await Registration.findByPk(req.params.id, { raw: true }); res.json(registration); } catch (error) { res.status(500).json({ error: error.message }); @@ -36,7 +36,8 @@ router.get('/registrations/:id', async (req, res) => { router.get('/activities/:id/registrations', async (req, res) => { try { const registrations = await Registration.findAll({ - where: { activityId: req.params.id, status: 'approved' } + where: { activityId: req.params.id, status: 'approved' }, + raw: true }); res.json(registrations); } catch (error) { diff --git a/backend/routes/venue.js b/backend/routes/venue.js new file mode 100644 index 0000000..e7c5f9d --- /dev/null +++ b/backend/routes/venue.js @@ -0,0 +1,66 @@ +const express = require('express'); +const router = express.Router(); +const auth = require('../middleware/auth'); +const { upload, getFileUrls } = require('../middleware/upload'); +const { VenueImage } = require('../models'); + +// 获取场地图片列表 +router.get('/venue-images', auth, async (req, res) => { + try { + const { activityId } = req.query; + const images = await VenueImage.findAll({ + where: { activityId }, + order: [['sortOrder', 'ASC'], ['id', 'ASC']], + raw: true + }); + res.json(images); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// 上传场地图片 +router.post('/venue-images', auth, upload.single('image'), async (req, res) => { + try { + const { activityId, sortOrder } = req.body; + + if (!req.file) { + return res.status(400).json({ error: '请选择图片' }); + } + + const [uploaded] = getFileUrls([req.file]); + const image = await VenueImage.create({ + activityId, + image: uploaded.url, + sortOrder: sortOrder || 0 + }); + + res.json(image); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// 更新场地图片排序 +router.put('/venue-images/:id', auth, async (req, res) => { + try { + const { sortOrder } = req.body; + await VenueImage.update({ sortOrder }, { where: { id: req.params.id } }); + const image = await VenueImage.findByPk(req.params.id); + res.json(image); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +// 删除场地图片 +router.delete('/venue-images/:id', auth, async (req, res) => { + try { + await VenueImage.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 deb2c3e..24d23eb 100644 --- a/backend/server.js +++ b/backend/server.js @@ -16,6 +16,7 @@ app.use('/uploads', express.static(path.join(__dirname, 'uploads'))); app.use('/api', require('./routes/activity')); app.use('/api', require('./routes/registration')); app.use('/api/admin', require('./routes/admin')); +app.use('/api/admin', require('./routes/venue')); // 生产环境下提供前端静态文件 if (process.env.NODE_ENV === 'production') { diff --git a/database-migration-checkin.sql b/database-migration-checkin.sql deleted file mode 100644 index 176d76c..0000000 --- a/database-migration-checkin.sql +++ /dev/null @@ -1,4 +0,0 @@ --- 添加签到功能字段 -ALTER TABLE Registrations -ADD COLUMN checkInStatus BOOLEAN DEFAULT false COMMENT '是否已签到', -ADD COLUMN checkInTime DATETIME NULL COMMENT '签到时间'; diff --git a/database.sql b/database.sql deleted file mode 100644 index 07cb768..0000000 --- a/database.sql +++ /dev/null @@ -1,90 +0,0 @@ --- 创建数据库 -CREATE DATABASE IF NOT EXISTS quanyoumi CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - -USE quanyoumi; - --- 活动表 -CREATE TABLE IF NOT EXISTS Activities ( - id INT AUTO_INCREMENT PRIMARY KEY, - title VARCHAR(255) NOT NULL, - description TEXT, - location VARCHAR(255), - locationLat DECIMAL(10, 7), - locationLng DECIMAL(10, 7), - startTime DATETIME, - endTime DATETIME, - registrationDeadline DATETIME, - maxParticipants INT, - budget DECIMAL(10, 2) DEFAULT 0 COMMENT '活动预算', - status ENUM('upcoming', 'ongoing', 'completed') DEFAULT 'upcoming', - coverImage VARCHAR(255), - createdAt DATETIME DEFAULT CURRENT_TIMESTAMP, - updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -); - --- 报名表 -CREATE TABLE IF NOT EXISTS Registrations ( - id INT AUTO_INCREMENT PRIMARY KEY, - activityId INT NOT NULL, - name VARCHAR(100) NOT NULL, - phone VARCHAR(20) NOT NULL, - carModel VARCHAR(100), - carNumber VARCHAR(20), - peopleCount INT DEFAULT 1, - status ENUM('pending', 'approved', 'rejected') DEFAULT 'pending', - note TEXT, - createdAt DATETIME DEFAULT CURRENT_TIMESTAMP, - updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - FOREIGN KEY (activityId) REFERENCES Activities(id) ON DELETE CASCADE -); - --- 经费表 -CREATE TABLE IF NOT EXISTS Expenses ( - id INT AUTO_INCREMENT PRIMARY KEY, - activityId INT NOT NULL, - item VARCHAR(255) NOT NULL, - amount DECIMAL(10, 2) NOT NULL, - category VARCHAR(100), - receipt VARCHAR(255), - note TEXT, - createdAt DATETIME DEFAULT CURRENT_TIMESTAMP, - updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - FOREIGN KEY (activityId) REFERENCES Activities(id) ON DELETE CASCADE -); - --- 照片表 -CREATE TABLE IF NOT EXISTS Photos ( - id INT AUTO_INCREMENT PRIMARY KEY, - activityId INT NOT NULL, - url VARCHAR(255) NOT NULL, - type ENUM('image', 'video') DEFAULT 'image', - description VARCHAR(255), - uploadedBy VARCHAR(100), - status ENUM('pending', 'approved', 'rejected') DEFAULT 'approved', - createdAt DATETIME DEFAULT CURRENT_TIMESTAMP, - updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - FOREIGN KEY (activityId) REFERENCES Activities(id) ON DELETE CASCADE -); - --- 赞助商表 -CREATE TABLE IF NOT EXISTS Sponsors ( - id INT AUTO_INCREMENT PRIMARY KEY, - activityId INT NOT NULL, - name VARCHAR(255) NOT NULL, - logo VARCHAR(255), - type ENUM('title', 'sponsor') DEFAULT 'sponsor', - link VARCHAR(255), - createdAt DATETIME DEFAULT CURRENT_TIMESTAMP, - updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - FOREIGN KEY (activityId) REFERENCES Activities(id) ON DELETE CASCADE -); - --- 管理员表 -CREATE TABLE IF NOT EXISTS Admins ( - id INT AUTO_INCREMENT PRIMARY KEY, - username VARCHAR(50) NOT NULL UNIQUE, - password VARCHAR(255) NOT NULL, - name VARCHAR(100), - createdAt DATETIME DEFAULT CURRENT_TIMESTAMP, - updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -); diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 2686e46..b82ef30 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -21,6 +21,10 @@ fi # 初始化数据库 echo "初始化数据库..." +echo "[1/2] 运行数据库迁移..." +node migrations/index.js + +echo "[2/2] 初始化管理员账号..." node init-admin.js # 替换前端 index.html 中的环境变量占位符 diff --git a/fix-expense.bat b/fix-expense.bat new file mode 100644 index 0000000..da61e07 --- /dev/null +++ b/fix-expense.bat @@ -0,0 +1,8 @@ +@echo off +echo ========================================== +echo 修复经费媒体数据 +echo ========================================== +cd backend +node fix-expense-media.js +cd .. +pause diff --git a/frontend/public/cd/cd1.jpg b/frontend/public/cd/cd1.jpg new file mode 100644 index 0000000..a6e0509 Binary files /dev/null and b/frontend/public/cd/cd1.jpg differ diff --git a/frontend/public/cd/cd2.jpg b/frontend/public/cd/cd2.jpg new file mode 100644 index 0000000..6678a6a Binary files /dev/null and b/frontend/public/cd/cd2.jpg differ diff --git a/frontend/public/cd/cd3.jpg b/frontend/public/cd/cd3.jpg new file mode 100644 index 0000000..c69ced8 Binary files /dev/null and b/frontend/public/cd/cd3.jpg differ diff --git a/frontend/public/qun.jpg b/frontend/public/qun.jpg new file mode 100644 index 0000000..1e3abd4 Binary files /dev/null and b/frontend/public/qun.jpg differ diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js index 524609d..77addb3 100644 --- a/frontend/src/api/index.js +++ b/frontend/src/api/index.js @@ -5,10 +5,74 @@ const api = axios.create({ timeout: 10000 }); -api.interceptors.request.use(config => { +// Token续期状态管理 +let isRefreshing = false; +let refreshSubscribers = []; + +// 订阅token续期完成事件 +const subscribeTokenRefresh = (cb) => { + refreshSubscribers.push(cb); +}; + +// 通知所有订阅者token已更新 +const onTokenRefreshed = (token) => { + refreshSubscribers.forEach(cb => cb(token)); + refreshSubscribers = []; +}; + +// 检查token是否即将过期(剩余时间小于1天) +const shouldRefreshToken = (token) => { + if (!token) return false; + try { + const payload = JSON.parse(atob(token.split('.')[1])); + const exp = payload.exp * 1000; + const now = Date.now(); + const oneDay = 24 * 60 * 60 * 1000; + return (exp - now) < oneDay && (exp - now) > 0; + } catch { + return false; + } +}; + +// 续期token +const refreshToken = async () => { + try { + const { data } = await api.post('/admin/refresh-token', {}, { + _skipRefresh: true // 标记跳过续期检查 + }); + localStorage.setItem('token', data.token); + return data.token; + } catch (error) { + console.error('Token续期失败:', error); + return null; + } +}; + +api.interceptors.request.use(async config => { const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = `Bearer ${token}`; + + // 跳过续期请求本身的续期检查 + if (!config._skipRefresh && shouldRefreshToken(token)) { + if (!isRefreshing) { + isRefreshing = true; + const newToken = await refreshToken(); + isRefreshing = false; + if (newToken) { + config.headers.Authorization = `Bearer ${newToken}`; + onTokenRefreshed(newToken); + } + } else { + // 等待续期完成 + return new Promise(resolve => { + subscribeTokenRefresh(newToken => { + config.headers.Authorization = `Bearer ${newToken}`; + resolve(config); + }); + }); + } + } } return config; }); @@ -17,7 +81,15 @@ api.interceptors.request.use(config => { api.interceptors.response.use( response => response, error => { - // 统一错误处理 + // 401错误:token过期或无效,跳转登录页 + if (error.response?.status === 401) { + localStorage.removeItem('token'); + localStorage.removeItem('admin'); + // 避免在登录页重复跳转 + if (!window.location.pathname.includes('/admin/login')) { + window.location.href = '/admin/login'; + } + } console.error('API请求错误:', error); return Promise.reject(error); } @@ -40,6 +112,7 @@ export default { // 活动相关 getActivities: (params) => api.get('/activities', { params }), getActivity: (id) => api.get(`/activities/${id}`), + getVenueImages: (id) => api.get(`/activities/${id}/venue-images`), // 报名相关 submitRegistration: (data) => api.post('/registrations', data), @@ -57,11 +130,14 @@ export default { // 管理员相关 adminLogin: (data) => api.post('/admin/login', data), + refreshToken: () => api.post('/admin/refresh-token'), createActivity: (data) => api.post('/admin/activities', data), updateActivity: (id, data) => api.put(`/admin/activities/${id}`, data), approveRegistration: (id, status) => api.put(`/admin/registrations/${id}`, { status }), updateRegistration: (id, data) => api.put(`/admin/registrations/${id}`, data), checkInRegistration: (id) => api.put(`/admin/registrations/${id}/checkin`), + groupRegistration: (id) => api.put(`/admin/registrations/${id}/group`), + deleteRegistration: (id) => api.delete(`/admin/registrations/${id}`), getAllRegistrations: (params) => api.get('/admin/registrations', { params }), addExpense: (data) => api.post('/admin/expenses', data, { headers: { 'Content-Type': 'multipart/form-data' }, @@ -97,5 +173,21 @@ export default { headers: { 'Content-Type': 'multipart/form-data' }, timeout: 60000 }), - deletePrize: (id) => api.delete(`/admin/prizes/${id}`) + deletePrize: (id) => api.delete(`/admin/prizes/${id}`), + + // 管理员账号管理(仅超级管理员) + getAdmins: () => api.get('/admin/admins'), + createAdmin: (data) => api.post('/admin/admins', data), + updateAdmin: (id, data) => api.put(`/admin/admins/${id}`, data), + updateAdminPassword: (id, data) => api.put(`/admin/admins/${id}/password`, data), + deleteAdmin: (id) => api.delete(`/admin/admins/${id}`), + + // 场地图片管理 + getVenueImages: (activityId) => api.get('/admin/venue-images', { params: { activityId } }), + uploadVenueImage: (data) => api.post('/admin/venue-images', data, { + headers: { 'Content-Type': 'multipart/form-data' }, + timeout: 60000 + }), + deleteVenueImage: (id) => api.delete(`/admin/venue-images/${id}`), + updateVenueImageOrder: (id, sortOrder) => api.put(`/admin/venue-images/${id}`, { sortOrder }) }; diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index bff66b4..d291a8c 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -47,6 +47,12 @@ const routes = [ name: 'ActivityManage', component: () => import('../views/admin/ActivityManage.vue'), meta: { requiresAuth: true } + }, + { + path: '/admin/admins', + name: 'AdminManage', + component: () => import('../views/admin/AdminManage.vue'), + meta: { requiresAuth: true, requiresSuper: true } } ]; @@ -58,6 +64,13 @@ const router = createRouter({ router.beforeEach((to, from, next) => { if (to.meta.requiresAuth && !localStorage.getItem('token')) { next('/admin/login'); + } else if (to.meta.requiresSuper) { + const admin = JSON.parse(localStorage.getItem('admin') || '{}'); + if (admin.role !== 'super') { + next('/admin'); + } else { + next(); + } } else { next(); } diff --git a/frontend/src/views/ActivityDetail/SponsorDanmaku.vue b/frontend/src/views/ActivityDetail/SponsorDanmaku.vue index 20ddea5..3b9900f 100644 --- a/frontend/src/views/ActivityDetail/SponsorDanmaku.vue +++ b/frontend/src/views/ActivityDetail/SponsorDanmaku.vue @@ -1,28 +1,77 @@ +} diff --git a/frontend/src/views/ActivityDetail/index.vue b/frontend/src/views/ActivityDetail/index.vue index 7607414..2dd71d1 100644 --- a/frontend/src/views/ActivityDetail/index.vue +++ b/frontend/src/views/ActivityDetail/index.vue @@ -49,7 +49,7 @@ - +
@@ -106,7 +106,6 @@
🎯 签到墙合照 - 打卡留念
🏛️ 地标合影 - 记录活动地点
🎬 动态全员上车 - 拍摄车队启动瞬间
-
🔥 点火仪式 - 引擎轰鸣
✨ 拍摄"泉州"字样 - 城市印记
@@ -131,7 +130,7 @@
🍖 边烤肉边欣赏晚霞
🎵 轻松音乐伴随美食时光
-
- 💡 提示:分享美食照片到朋友圈并@活动官方账号可获得 +1分 -
@@ -183,11 +179,11 @@
- 19:00 - 20:00 + 19:00 - 21:00
篝火歌舞会
-
5个精彩互动游戏,点燃现场欢乐气氛
+
互动游戏、璀璨烟花、积分兑奖,欢乐不停歇
@@ -229,8 +225,6 @@
玩法:全员参与,齐唱经典歌曲
- 曲目:《朋友》《真心英雄》等
- 得分:参与即可获得+1分
氛围:点燃现场,凝聚团队
@@ -245,7 +239,6 @@
玩法:自愿上台展示才艺(唱歌/跳舞/表演)
准备:需提前到后台准备BGM
- 得分:参与表演+3分,精彩表演+5分
鼓励:展现自我,收获掌声!
@@ -261,7 +254,41 @@ 玩法:主持人邀请小朋友上台互动
内容:简单游戏/才艺展示/问答
礼品:参与即有小礼物
- 得分:家长陪同参与+2分 +
+
+
+ +
+
+
🎆 璀璨烟火秀
+ +
+
+
+ 环节:三重烟花盛宴,点亮夜空绽放精彩
+ 米兰之夜:大型专业烟火表演,绚丽夺目
+ 仙女棒:每车配发手持烟花,拍照打卡
+ 加特林压轴:震撼收尾,全场欢呼
+
+
+
+ +
+
+
🏆 积分兑换礼品
+ +
+
+
+ 奖项设置:
+ 🥇 一等奖 - 神秘大奖等你拿
+ 🥈 二等奖 - 精美礼品超值享
+ 🥉 三等奖 - 实用好物带回家
+ 🎖️ 四等奖 - 纪念礼品留回忆
+ 🎊 参与奖 - 人人有份不落空
+
+ ✨ 积分越高奖品越丰厚,还可能有惊喜等着你!
+ 📍 凭积分卡到兑奖处领取对应礼品
@@ -271,59 +298,23 @@ - -
+ +
- 20:00 - 20:30 + 21:00 以后
-
璀璨烟火秀
-
三重烟花盛宴,点亮夜空绽放精彩
+
自由交流 & 返程
+
自由合影留念,整理现场,安全返程
-
🎆 米兰之夜 - 大型专业烟火表演,绚丽夺目
-
✨ 仙女棒 - 每车配发手持烟花,拍照打卡
-
🔥 加特林压轴 - 震撼收尾,全场欢呼
-
📸 最佳拍摄时机 - 记录璀璨瞬间
-
-
- 💡 提示:拍摄烟花视频并分享可获得 +2分 -
-
-
-
-
- - -
-
-
-
- 20:30 以后 - -
-
积分兑奖 & 自由交流
-
凭积分兑换精美礼品,自由合影欢乐交流
-
-
-
-
🏆 积分兑换礼品
-
- 积分排行榜:
- • 第1名(20分以上):神秘大奖 🎁
- • 第2-3名(15-19分):精美礼品 🎀
- • 第4-10名(10-14分):纪念品 🎈
- • 参与奖(5-9分):小礼物 🎊
-
- 活动收尾:
- • 自由合影留念
- • 整理现场垃圾
- • 提醒安全驾驶返程
- • 期待下次相聚 -
+
📸 自由合影留念
+
🧹 整理现场垃圾
+
🚗 提醒安全驾驶返程
+
👋 期待下次相聚
@@ -332,6 +323,18 @@
+ +
+

场地展示

+ + +
+ +
+
+
+
+

活动经费 ¥{{ totalExpense }}

@@ -391,6 +394,12 @@
{{ prize.level }} {{ prize.name }} +
{{ prize.description }}
+
+ +
+
{{ approvedTotalPeople }}
+
参与人数
+
+
@@ -266,6 +273,20 @@
未签到
+
+ +
+
{{ registrations.filter(r => r.groupStatus).length }}
+
已进群
+
+
+
+ +
+
{{ registrations.filter(r => !r.groupStatus).length }}
+
未进群
+
+
@@ -285,11 +306,6 @@
{{ reg.name || '未知' }} -
- - 已签到 - -
+
+
+ + + +
+
+ +
+
+ +
+
+ + +
+ +
@@ -414,6 +464,47 @@ + + +
+
+
+
+ +
+
+ + 删除 +
+
+
+ + + + + +
+ +
+
+
+
@@ -579,7 +670,7 @@
@@ -603,16 +694,33 @@ :max-count="5" accept="image/*,video/*" :after-read="onExpenseMediaRead" + :disabled="expenseForm.media.length + expenseMediaFiles.length >= 5" > 上传 ({{ expenseForm.media.length + expenseMediaFiles.length }}/5) + +
-
- - - +
已有媒体
+
+
+ + + +
+
+
+ + +
+
新上传
+
+
+ + +
@@ -852,6 +960,82 @@
+ + +
+
+

修改车牌号

+ +
+
+
+ {{ currentEditReg.name }} + 当前车牌号:{{ currentEditReg.carNumber || '未填写' }} +
+ +
+
+ 确定 +
+
+
+ + + +
+ + + + + + + + + + +
+ 提交报名 +
+
+
+
+
@@ -873,8 +1057,9 @@
车牌号 - + {{ currentReg.carNumber }} +
@@ -892,33 +1077,41 @@ 签到时间 {{ formatDate(currentReg.checkInTime) }}
-
- 审核状态 - - {{ currentReg.status === 'approved' ? '已通过' : currentReg.status === 'rejected' ? '已拒绝' : '待审核' }} - -
-
- 签到状态 - - {{ currentReg.checkInStatus ? '已签到' : '未签到' }} - +
+ 状态管理 +
+
+ 审核(是否交钱) + + {{ currentReg.status === 'approved' ? '已通过' : currentReg.status === 'rejected' ? '已拒绝' : '待审核' }} + +
+
+ 签到(是否到现场) + +
+
+ 进群(是否加微信群) + +
+
- - {{ currentReg.checkInStatus ? '取消签到' : '签到' }} -
- 通过 - 拒绝 + 通过审核 + 拒绝审核
删除报名 @@ -965,12 +1158,16 @@ const photoFiles = ref([]); const pendingPhotos = ref([]); const expenseMediaFiles = ref([]); const approvedPhotos = ref([]); +const venueImages = ref([]); +const venueFiles = ref([]); const showVideoPlayer = ref(false); const currentVideoUrl = ref(''); const regSearchKeyword = ref(''); const showPeopleCountDialog = ref(false); const currentEditReg = ref({}); const newPeopleCount = ref(1); +const showCarNumberDialog = ref(false); +const newCarNumber = ref(''); const showRegDetailDialog = ref(false); const currentReg = ref({}); const activeFilter = ref('all'); // 过滤器状态 @@ -978,6 +1175,16 @@ const activeFilter = ref('all'); // 过滤器状态 const showActivityForm = ref(false); const showExpenseForm = ref(false); const showSponsorForm = ref(false); +const showRegForm = ref(false); + +const regForm = ref({ + name: '', + phone: '', + carModel: '', + carNumber: '', + peopleCount: 1, + note: '' +}); // 奖品管理 const activityId = computed(() => route.params.id); @@ -1069,6 +1276,13 @@ const totalExpense = computed(() => { return expenses.value.reduce((sum, e) => sum + parseFloat(e.amount), 0).toFixed(2); }); +// 审核通过的总人数(车主+带的人) +const approvedTotalPeople = computed(() => { + return registrations.value + .filter(r => r.status === 'approved') + .reduce((sum, r) => sum + (r.peopleCount || 1), 0); +}); + // 过滤报名列表 const filteredRegistrations = computed(() => { let result = registrations.value; @@ -1084,6 +1298,10 @@ const filteredRegistrations = computed(() => { result = result.filter(r => r.checkInStatus); } else if (activeFilter.value === 'notCheckedIn') { result = result.filter(r => !r.checkInStatus); + } else if (activeFilter.value === 'grouped') { + result = result.filter(r => r.groupStatus); + } else if (activeFilter.value === 'notGrouped') { + result = result.filter(r => !r.groupStatus); } // 应用搜索关键词 @@ -1212,7 +1430,11 @@ const confirmCustomCategory = () => { const loadData = async () => { try { const { data } = await api.getActivity(route.params.id); - activity.value = data.activity; + activity.value = { + ...data.activity, + danmakuEnabled: Boolean(data.activity.danmakuEnabled), + registrationEnabled: Boolean(data.activity.registrationEnabled) + }; expenses.value = data.expenses || []; sponsors.value = data.sponsors || []; @@ -1226,6 +1448,9 @@ const loadData = async () => { // 加载奖品 await loadPrizes(); + + // 加载场地图片 + await loadVenueImages(); } catch (error) { showToast('加载失败'); } @@ -1327,8 +1552,8 @@ const toggleDanmaku = async (value) => { formData.append('category', expenseForm.value.category); formData.append('note', expenseForm.value.note); - // 添加现有媒体 - if (expenseForm.value.media && expenseForm.value.media.length > 0) { + // 只在编辑时添加现有媒体 + if (expenseForm.value.id && expenseForm.value.media && expenseForm.value.media.length > 0) { formData.append('existingMedia', JSON.stringify(expenseForm.value.media)); } @@ -1346,9 +1571,7 @@ const toggleDanmaku = async (value) => { await api.addExpense(formData); showToast('添加成功'); } - showExpenseForm.value = false; - expenseForm.value = { id: null, item: '', amount: '', category: '', note: '', media: [] }; - expenseMediaFiles.value = []; + closeExpenseForm(); loadData(); } catch (error) { showToast(expenseForm.value.id ? '更新失败' : '添加失败'); @@ -1374,13 +1597,76 @@ const addNewExpense = () => { }; const onExpenseMediaRead = (file) => { - // 文件读取后的处理,Vant会自动处理 + // 验证文件大小和类型 + const fileItem = Array.isArray(file) ? file[0] : file; + const maxSize = 50 * 1024 * 1024; // 50MB + + if (fileItem.file.size > maxSize) { + showToast('文件大小不能超过50MB'); + // 移除超大文件 + const index = expenseMediaFiles.value.findIndex(f => f.file === fileItem.file); + if (index > -1) { + expenseMediaFiles.value.splice(index, 1); + } + return; + } }; const removeExpenseMedia = (index) => { expenseForm.value.media.splice(index, 1); }; +// 添加新报名 +const addNewRegistration = () => { + regForm.value = { + name: '', + phone: '', + carModel: '', + carNumber: '', + peopleCount: 1, + note: '' + }; + showRegForm.value = true; +}; + +// 关闭报名表单 +const closeRegForm = () => { + showRegForm.value = false; + regForm.value = { + name: '', + phone: '', + carModel: '', + carNumber: '', + peopleCount: 1, + note: '' + }; +}; + +// 提交报名 +const submitRegistration = async () => { + try { + await api.submitRegistration({ + activityId: activity.value.id, + ...regForm.value + }); + showToast('报名成功'); + closeRegForm(); + loadData(); + } catch (error) { + showToast('报名失败'); + } +};; + +const removeNewExpenseMedia = (index) => { + expenseMediaFiles.value.splice(index, 1); +}; + +const closeExpenseForm = () => { + showExpenseForm.value = false; + expenseForm.value = { id: null, item: '', amount: '', category: '', note: '', media: [] }; + expenseMediaFiles.value = []; +}; + const deleteExpenseItem = async (id) => { try { await showConfirmDialog({ @@ -1592,6 +1878,24 @@ const confirmPeopleCount = async () => { } }; +// 修改车牌号 +const editCarNumber = (reg) => { + currentEditReg.value = reg; + newCarNumber.value = reg.carNumber || ''; + showCarNumberDialog.value = true; +}; + +const confirmCarNumber = async () => { + try { + await api.updateRegistration(currentEditReg.value.id, { carNumber: newCarNumber.value }); + showToast('修改成功'); + showCarNumberDialog.value = false; + loadData(); + } catch (error) { + showToast('修改失败'); + } +}; + // 显示报名详情 const showRegDetail = (reg) => { currentReg.value = reg; @@ -1600,13 +1904,19 @@ const showRegDetail = (reg) => { // 从详情弹窗签到 const toggleCheckInFromDetail = async () => { + const originalStatus = currentReg.value.checkInStatus; try { await api.checkInRegistration(currentReg.value.id); showToast('操作成功'); - showRegDetailDialog.value = false; - loadData(); + await loadData(); + // 重新打开详情弹窗,显示更新后的数据 + const updatedReg = registrations.value.find(r => r.id === currentReg.value.id); + if (updatedReg) { + currentReg.value = updatedReg; + } } catch (error) { showToast('操作失败'); + currentReg.value.checkInStatus = originalStatus; } }; @@ -1622,6 +1932,22 @@ const approveFromDetail = async (status) => { } }; +// 从详情弹窗切换进群状态 +const toggleGroupStatusFromDetail = async () => { + try { + await api.groupRegistration(currentReg.value.id); + showToast('操作成功'); + await loadData(); + // 重新打开详情弹窗,显示更新后的数据 + const updatedReg = registrations.value.find(r => r.id === currentReg.value.id); + if (updatedReg) { + currentReg.value = updatedReg; + } + } catch (error) { + showToast('操作失败'); + } +}; + // 删除报名 const deleteRegistration = async () => { try { @@ -1724,6 +2050,70 @@ const playVideo = (url) => { showVideoPlayer.value = true; }; +// 场地管理相关方法 +const loadVenueImages = async () => { + try { + const { data } = await api.getVenueImages(route.params.id); + venueImages.value = data; + } catch (error) { + console.error('加载场地图片失败:', error); + } +}; + +const uploadVenueImage = async (file) => { + const loadingToast = showToast({ + type: 'loading', + message: '正在上传...', + forbidClick: true, + duration: 0 + }); + + try { + const formData = new FormData(); + formData.append('image', file.file); + formData.append('activityId', route.params.id); + formData.append('sortOrder', venueImages.value.length); + + await api.uploadVenueImage(formData); + showToast('上传成功'); + venueFiles.value = []; + await loadVenueImages(); + } catch (error) { + console.error('上传失败:', error); + showToast('上传失败'); + } finally { + loadingToast.clear(); + } +}; + +const deleteVenueImage = async (id) => { + try { + await showConfirmDialog({ + title: '确认删除', + message: '删除后无法恢复,确定要删除吗?', + }); + await api.deleteVenueImage(id); + showToast('删除成功'); + await loadVenueImages(); + } catch (error) { + if (error !== 'cancel') { + console.error('删除失败:', error); + showToast('删除失败'); + } + } +}; + +const updateVenueOrder = async (id, sortOrder) => { + try { + await api.updateVenueImageOrder(id, sortOrder); + showToast('排序已更新'); + await loadVenueImages(); + } catch (error) { + console.error('更新失败:', error); + showToast('更新失败'); + } +}; + // 地图选点相关(与Dashboard相同) const initMapPicker = async () => { try { @@ -2400,6 +2790,71 @@ onMounted(loadData); font-size: 13px; } +.reg-right { + display: flex; + align-items: center; + gap: 12px; + flex-shrink: 0; +} + +.reg-right .van-icon { + font-size: 16px; +} + +.status-indicators { + display: flex; + gap: 6px; + align-items: center; +} + +.status-dot { + width: 28px; + height: 28px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + background: #f5f5f5; + transition: all 0.3s ease; +} + +.status-dot .van-icon { + font-size: 14px; + color: #999; +} + +.status-dot.status-approved { + background: #e8f5e9; +} + +.status-dot.status-approved .van-icon { + color: #07c160; +} + +.status-dot.status-rejected { + background: #ffebee; +} + +.status-dot.status-rejected .van-icon { + color: #ee0a24; +} + +.status-dot.status-pending { + background: #fff3e0; +} + +.status-dot.status-pending .van-icon { + color: #ffa940; +} + +.status-dot.status-active { + background: #e3f2fd; +} + +.status-dot.status-active .van-icon { + color: #1989fa; +} + .info-link, .info-text { display: inline-flex; @@ -2441,15 +2896,7 @@ onMounted(loadData); opacity: 0.7; } -.reg-right { - display: flex; - align-items: center; - flex-shrink: 0; -} - -.reg-right .van-icon { - font-size: 16px; -} +/* 移除旧的 reg-right 样式,已在上面重新定义 */ /* 弹窗 */ .form-popup { @@ -2759,6 +3206,48 @@ onMounted(loadData); border-radius: 12px; } +/* 场地管理 */ +.venue-list { + display: flex; + flex-direction: column; + gap: 12px; +} + +.venue-item { + background: #fff; + border-radius: 12px; + overflow: hidden; + box-shadow: 0 2px 8px rgba(0,0,0,0.04); + border: 1px solid #f0f0f0; +} + +.venue-thumb { + width: 100%; + height: 200px; + background: #f5f5f5; + cursor: pointer; +} + +.venue-thumb .van-image { + width: 100%; + height: 100%; +} + +.venue-actions { + padding: 12px; + display: flex; + gap: 8px; + align-items: center; +} + +.venue-actions .van-field { + flex: 1; +} + +.venue-actions .van-button { + flex-shrink: 0; +} + /* 分类选择器 */ .category-picker { padding: 20px; @@ -2887,9 +3376,19 @@ onMounted(loadData); } .existing-media { + margin-top: 12px; +} + +.media-section-title { + font-size: 13px; + color: #666; + margin-bottom: 8px; + font-weight: 500; +} + +.media-list { display: flex; gap: 8px; - margin-top: 12px; flex-wrap: wrap; } @@ -3026,6 +3525,33 @@ onMounted(loadData); border-bottom: 1px solid #f5f5f5; } +.detail-row.status-row { + flex-direction: column; + align-items: stretch; + gap: 12px; +} + +.status-controls { + display: flex; + flex-direction: column; + gap: 12px; + background: #f7f8fa; + padding: 12px; + border-radius: 8px; +} + +.status-item { + display: flex; + justify-content: space-between; + align-items: center; +} + +.status-name { + font-size: 14px; + color: #333; + font-weight: 500; +} + .detail-row:last-child { border-bottom: none; } diff --git a/frontend/src/views/admin/AdminManage.vue b/frontend/src/views/admin/AdminManage.vue new file mode 100644 index 0000000..0636a7a --- /dev/null +++ b/frontend/src/views/admin/AdminManage.vue @@ -0,0 +1,606 @@ + + + + + diff --git a/frontend/src/views/admin/Dashboard.vue b/frontend/src/views/admin/Dashboard.vue index 7fbc36a..7a678e9 100644 --- a/frontend/src/views/admin/Dashboard.vue +++ b/frontend/src/views/admin/Dashboard.vue @@ -5,9 +5,20 @@

管理后台

泉友米车友会

- - 退出 - +
+ + 管理员 + + + 退出 + +
@@ -230,6 +241,7 @@ import api from '../../api'; const router = useRouter(); const activities = ref([]); +const isSuper = ref(false); const showActivityForm = ref(false); const showMapPicker = ref(false); @@ -290,14 +302,29 @@ const formatDateTime = (dateTime) => { return `${year}-${month}-${day} ${hours}:${minutes}`; }; +// 安全解析datetime-local格式,确保以本地时区创建Date对象 +const parseDateTimeLocal = (dateTimeStr) => { + console.log('[时区转换] 输入的datetime-local字符串:', dateTimeStr); + const [datePart, timePart] = dateTimeStr.split('T'); + const [year, month, day] = datePart.split('-').map(Number); + const [hours, minutes] = timePart.split(':').map(Number); + const localDate = new Date(year, month - 1, day, hours, minutes); + console.log('[时区转换] 创建的本地Date对象:', localDate.toString()); + console.log('[时区转换] 转换为UTC (ISO):', localDate.toISOString()); + console.log('[时区转换] 时区偏移(分钟):', localDate.getTimezoneOffset()); + return localDate; +}; + // 开始时间确认 const confirmStartTime = () => { if (!startTimeInput.value) { showToast('请选择开始时间'); return; } - const selectedDate = new Date(startTimeInput.value); + console.log('[开始时间] 用户选择的时间:', startTimeInput.value); + const selectedDate = parseDateTimeLocal(startTimeInput.value); activityForm.value.startTime = selectedDate.toISOString(); + console.log('[开始时间] 保存到表单的UTC时间:', activityForm.value.startTime); showStartTimePicker.value = false; }; @@ -307,12 +334,14 @@ const confirmDeadline = () => { showToast('请选择报名截止时间'); return; } - const selectedDate = new Date(deadlineInput.value); + console.log('[截止时间] 用户选择的时间:', deadlineInput.value); + const selectedDate = parseDateTimeLocal(deadlineInput.value); if (activityForm.value.startTime && selectedDate > new Date(activityForm.value.startTime)) { showToast('报名截止时间不能晚于活动开始时间'); return; } activityForm.value.registrationDeadline = selectedDate.toISOString(); + console.log('[截止时间] 保存到表单的UTC时间:', activityForm.value.registrationDeadline); showDeadlinePicker.value = false; }; @@ -573,10 +602,17 @@ const submitActivity = async () => { const logout = () => { localStorage.removeItem('token'); + localStorage.removeItem('admin'); router.push('/admin/login'); }; +const goToAdminManage = () => { + router.push('/admin/admins'); +}; + onMounted(() => { + const admin = JSON.parse(localStorage.getItem('admin') || '{}'); + isSuper.value = admin.role === 'super'; loadActivities(); }); @@ -609,6 +645,22 @@ onMounted(() => { color: #666; } +.header-actions { + display: flex; + gap: 8px; +} + +.admin-btn { + background: #4dabf7; + border: none; + color: white; + font-weight: 500; +} + +.admin-btn:active { + background: #339af0; +} + .logout-btn { background: #f5f5f5; border: none; diff --git a/frontend/src/views/admin/Login.vue b/frontend/src/views/admin/Login.vue index 6c210f4..bd466d5 100644 --- a/frontend/src/views/admin/Login.vue +++ b/frontend/src/views/admin/Login.vue @@ -62,6 +62,7 @@ const onSubmit = async () => { try { const { data } = await api.adminLogin(form.value); localStorage.setItem('token', data.token); + localStorage.setItem('admin', JSON.stringify(data.admin)); showToast('登录成功'); router.push('/admin'); } catch (error) { diff --git a/frontend/src/views/admin/prize-manage-styles.css b/frontend/src/views/admin/prize-manage-styles.css index f7aeb5a..56406df 100644 --- a/frontend/src/views/admin/prize-manage-styles.css +++ b/frontend/src/views/admin/prize-manage-styles.css @@ -54,6 +54,11 @@ white-space: nowrap; } +.prize-level-badge.level-特等奖 { + background: linear-gradient(135deg, #FF0000 0%, #FF6B6B 100%); + color: #fff; +} + .prize-level-badge.level-一等奖 { background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%); color: #fff; @@ -69,6 +74,11 @@ color: #fff; } +.prize-level-badge.level-四等奖 { + background: linear-gradient(135deg, #8B7355 0%, #A0826D 100%); + color: #fff; +} + .prize-level-badge.level-参与奖 { background: linear-gradient(135deg, #FF6700 0%, #FF8533 100%); color: #fff; @@ -163,6 +173,10 @@ cursor: pointer; transition: all 0.2s; text-align: center; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; } .level-item:active { diff --git a/frontend/src/views/timeline-data.js b/frontend/src/views/timeline-data.js index 672c161..f34aed3 100644 --- a/frontend/src/views/timeline-data.js +++ b/frontend/src/views/timeline-data.js @@ -1,92 +1,6 @@ -// 活动流程数据 -export const timelineData = [ - { - title: '签到与集结', - time: '14:00 - 14:30', - brief: '车辆到场、签到登记、领取积分卡', - overview: ` -
-

📍 车辆到达指定地点后,工作人员将引导至临时停车点

-

✍️ 前往签到处签名确认,扫码加入活动群

-

🎫 领取专属积分卡,开启精彩旅程

-
- `, - content: ` -
-
📋 积分卡说明
-
- • 每人领取一张积分卡(红绿双色章)
- • 初始积分:0分
- • 游戏得分范围:1~5分
- • 游戏扣分范围:1~5分
- • 最终积分可兑换礼品 -
-
- ` - }, - { - title: '观赏素材拍摄', - time: '14:30 - 16:30', - brief: '多点位打卡拍照,记录精彩瞬间', - overview: ` -
-

📸 专业摄影师带队,前往多个精选点位

-

🚗 全员车前合影、签到墙打卡、地标合照

-

✨ 完成所有点位拍照可获得 +2分

-
- `, - items: [ - { - icon: '🏛️', - name: '宝船门口', - detail: ` -
-

拍摄内容:

-

• 全员车前合影

-

• 签到墙合照

-

• 个人打卡照

-
- ` - }, - { - icon: '🌉', - name: '隧道三点位', - detail: ` -
-

拍摄内容:

-

• 地标合影

-

• 动态全员上车

-

• 点火瞬间

-
- ` - }, - { - icon: '🎨', - name: '特色造型', - detail: ` -
-

拍摄内容:

-

• 摆"泉州"两个字造型

-

• 创意合影

-
- ` - } - ] - }, - { - title: '互动游戏时间', - time: '16:30 - 18:00', - brief: '10+趣味游戏项目,赢取积分', - overview: ` -
-

🎮 精心准备10+款趣味游戏

-

🏆 每个游戏都有积分奖励,表现越好得分越高

-

⚡ 特别设置复活挑战赛,给你翻盘机会

-

💡 点击下方游戏查看详细规则和得分标准

-
- `, - items: [ - { +// 游戏项目数据 +export const gameItems = [ + { icon: '🎯', name: '套圈圈', detail: ` @@ -98,8 +12,8 @@ export const timelineData = [

• 套中5个:+5分

` - }, - { + }, + { icon: '🏺', name: '投壶', detail: ` @@ -111,8 +25,8 @@ export const timelineData = [

• 全部投中:+5分

` - }, - { + }, + { icon: '🎲', name: '摇骰子(067玩法)', detail: ` @@ -124,8 +38,8 @@ export const timelineData = [

• 摇出5个正豹子:+5分(直接判赢)

` - }, - { + }, + { icon: '🎲', name: '摇骰子(单骰比大小)', detail: ` @@ -136,8 +50,8 @@ export const timelineData = [

• 玩家输庄家:-1分

` - }, - { + }, + { icon: '🐂', name: '牛牛', detail: ` @@ -149,8 +63,8 @@ export const timelineData = [

• 连赢5局:+5分

` - }, - { + }, + { icon: '✊', name: '闽南划拳(两拳倒)', detail: ` @@ -164,8 +78,8 @@ export const timelineData = [

💡 投资机制:可叠加投资,赢得越多分数越高!

` - }, - { + }, + { icon: '⚽', name: '足球射门', detail: ` @@ -177,8 +91,8 @@ export const timelineData = [

• 进5球:+5分

` - }, - { + }, + { icon: '🎮', name: '王者荣耀', detail: ` @@ -193,8 +107,8 @@ export const timelineData = [

💡 团队配合最重要,选好英雄!

` - }, - { + }, + { icon: '💧', name: '抛水瓶', detail: ` @@ -206,8 +120,8 @@ export const timelineData = [

• 立住5个:+5分

` - }, - { + }, + { icon: '🎴', name: '扑克游戏', detail: ` @@ -218,8 +132,8 @@ export const timelineData = [

• 输家:-1分

` - }, - { + }, + { icon: '🐊', name: '鳄鱼牙齿/插剑玩具', detail: ` @@ -233,8 +147,8 @@ export const timelineData = [

💡 胆大心细,步步为营!

` - }, - { + }, + { icon: '🪑', name: '最终复活挑战 - 抢椅子', detail: ` @@ -248,170 +162,5 @@ export const timelineData = [

⚡ 这是翻盘的最佳机会!

` - } - ] - }, - { - title: '落日BBQ', - time: '18:00 - 19:30', - brief: '落日余晖下享受美食,拍摄氛围大片', - overview: ` -
-

🌅 在落日余晖下,车辆排成一排

-

🚗 齐开大灯,营造浪漫氛围

-

🍖 边烤肉边欣赏晚霞,拍摄氛围感大片

-
- `, - content: ` -
-
- 💡 提示:分享美食照片到朋友圈并@活动官方账号可获得 +1分 -
-
- ` - }, - { - title: '篝火歌舞会', - time: '19:00 - 20:00', - brief: '围炉夜话,互动游戏,才艺表演', - overview: ` -
-

🔥 点燃篝火,围坐一圈

-

🎤 击鼓传花、喊数抱团等互动游戏

-

🎭 才艺表演,展示个人魅力

-

💡 点击下方查看各个游戏详细规则

-
- `, - items: [ - { - icon: '🥁', - name: '击鼓传花(10轮)', - detail: ` -
-

游戏规则:音乐停止时持花者上台,吃柠檬或苦瓜

-

得分标准:

-

• 勇敢完成:+2分

-

• 拒绝挑战:-3分

-
- ` - }, - { - icon: '🤝', - name: '喊数抱团(10轮)', - detail: ` -
-

游戏规则:主持人喊数字,参与者按数字抱团,落单者淘汰

-

惩罚:上台自我介绍(1分钟)

-

得分标准:

-

• 成功抱团:+1分

-

• 落单但完成自我介绍:+2分

-

• 拒绝:-2分

-
- ` - }, - { - icon: '🎵', - name: '大合唱', - detail: ` -
-

活动内容:全员参与,气氛热烈

-

得分标准:

-

• 每人:+1分

-
- ` - }, - { - icon: '🎭', - name: '个人才艺表演', - detail: ` -
-

活动内容:自愿上台唱歌、跳舞、表演才艺

-

提示:需提前到后台准备BGM

-

得分标准:

-

• 参与表演:+3分

-

• 精彩表演:+5分

-
- ` - }, - { - icon: '👶', - name: '小朋友互动', - detail: ` -
-

活动内容:主持人邀请小朋友上台互动,参与即有礼物

-

得分标准:

-

• 家长陪同参与:+2分

-
- ` - } - ] - }, - { - title: '璀璨烟火秀', - time: '20:00 - 20:30', - brief: '专业烟火表演,点亮夜空', - overview: ` -
-

🎆 大型专业烟火表演

-

✨ 每车配发手持仙女棒

-

🔥 加特林压轴,震撼收尾

-
- `, - items: [ - { - icon: '🌃', - name: '米兰之夜', - detail: ` -
-

表演内容:大型专业烟火表演,点亮夜空

-
- ` - }, - { - icon: '✨', - name: '仙女棒', - detail: ` -
-

活动内容:每车配发手持烟花,拍照打卡

-
- ` - }, - { - icon: '🔥', - name: '加特林压轴', - detail: ` -
-

表演内容:震撼收尾,全场欢呼

-

💡 拍摄烟花视频并分享可获得 +2分

-
- ` - } - ] - }, - { - title: '积分兑奖 & 自由交流', - time: '20:30 以后', - brief: '积分兑换礼品,自由合影交流', - overview: ` -
-

🏆 根据积分排行榜兑换礼品

-

📸 自由合影,交流心得

-

🚗 整理垃圾,安全驾驶返程

-
- `, - content: ` -
-
🏆 积分兑换礼品
-
- 积分排行榜:
- • 第1名(20分以上):神秘大奖
- • 第2-3名(15-19分):精美礼品
- • 第4-10名(10-14分):纪念品
- • 参与奖(5-9分):小礼物
-
- 随后自由合影、整理垃圾、提醒安全驾驶返程 -
-
- ` - } + } ]; diff --git a/init-all.bat b/init-all.bat new file mode 100644 index 0000000..4b2dbda --- /dev/null +++ b/init-all.bat @@ -0,0 +1,35 @@ +@echo off +echo ======================================== +echo 泉友米系统初始化 +echo ======================================== +echo. + +echo [1/2] 运行数据库迁移... +node backend/migrations/index.js +if %errorlevel% neq 0 ( + echo. + echo ✗ 迁移失败,请检查错误信息 + pause + exit /b 1 +) + +echo. +echo [2/2] 初始化管理员账号... +node backend/init-admin.js +if %errorlevel% neq 0 ( + echo. + echo ✗ 初始化失败,请检查错误信息 + pause + exit /b 1 +) + +echo. +echo ======================================== +echo ✓ 初始化完成! +echo ======================================== +echo. +echo 管理员账号信息: +echo 超级管理员: yvan / Xu950329. +echo 普通管理员: admin / admin123 +echo. +pause diff --git a/init-all.sh b/init-all.sh new file mode 100644 index 0000000..7e0c244 --- /dev/null +++ b/init-all.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +echo "========================================" +echo "泉友米系统初始化" +echo "========================================" +echo "" + +echo "[1/2] 运行数据库迁移..." +node backend/migrations/index.js +if [ $? -ne 0 ]; then + echo "" + echo "✗ 迁移失败,请检查错误信息" + exit 1 +fi + +echo "" +echo "[2/2] 初始化管理员账号..." +node backend/init-admin.js +if [ $? -ne 0 ]; then + echo "" + echo "✗ 初始化失败,请检查错误信息" + exit 1 +fi + +echo "" +echo "========================================" +echo "✓ 初始化完成!" +echo "========================================" +echo "" +echo "管理员账号信息:" +echo " 超级管理员: yvan / Xu950329." +echo " 普通管理员: admin / admin123" +echo "" diff --git a/migrate-group-status.bat b/migrate-group-status.bat new file mode 100644 index 0000000..3e6a0c7 --- /dev/null +++ b/migrate-group-status.bat @@ -0,0 +1,7 @@ +@echo off +echo Running migration: Add group status field... +cd backend +node migrations/index.js +cd .. +echo Migration completed! +pause diff --git a/migrate-prize-source.bat b/migrate-prize-source.bat new file mode 100644 index 0000000..7194ac8 --- /dev/null +++ b/migrate-prize-source.bat @@ -0,0 +1,7 @@ +@echo off +echo 正在运行数据库迁移... +cd backend +node migrations/index.js +cd .. +echo 迁移完成! +pause