优化验证报名信息对话框车牌号输入布局,使标签和输入框在同一行显示

This commit is contained in:
Yvan
2026-04-09 15:37:40 +08:00
parent 89960b1cb8
commit 4920620d2c
48 changed files with 4804 additions and 3916 deletions
+3 -14
View File
@@ -1,17 +1,6 @@
PORT=3000
DB_HOST=localhost
DB_USER=root
DB_PASSWORD=your-db-password
DB_NAME=quanyoumi
JWT_SECRET=your-secret-key-change-in-production
# MinIO 配置
MINIO_ENDPOINT=localhost # 后端连接 MinIO 的地址
MINIO_PORT=9000
MINIO_USE_SSL=false
MINIO_ACCESS_KEY=jFywUsaCb4zNr2LV7hTP
MINIO_SECRET_KEY=minioadmin
MINIO_BUCKET=quanyoumi
# MINIO_PUBLIC_URL(可选):前端访问文件的公开地址,不填则自动用 MINIO_ENDPOINT 拼接
# 生产环境如果用了反向代理(如 Nginx),填写外网地址,例如:https://cdn.yourdomain.com
# MINIO_PUBLIC_URL=http://localhost:9000
# 高德地图配置
AMAP_KEY=your-amap-key
AMAP_SECURITY_CODE=your-amap-security-code
+22 -4
View File
@@ -5,8 +5,21 @@ const { Admin, sequelize } = require('./models');
async function initAdmin() {
try {
// 使用 force: false 避免 alter 表时的问题
// 如果表不存在则创建,存在则跳过
await sequelize.sync({ force: false });
// 检查是否已存在管理员账号
const existingAdmin = await Admin.findOne({ where: { username: 'admin' } });
if (existingAdmin) {
console.log('✓ 管理员账号已存在');
console.log(' 用户名: admin');
console.log(' 提示: 如需重置密码,请删除数据库文件后重新初始化');
process.exit(0);
return;
}
const hashedPassword = bcrypt.hashSync('admin123', 10);
await Admin.create({
username: 'admin',
@@ -14,12 +27,17 @@ async function initAdmin() {
name: '管理员'
});
console.log('管理员账号创建成功');
console.log('用户名: admin');
console.log('密码: admin123');
console.log('✓ 管理员账号创建成功');
console.log(' 用户名: admin');
console.log(' 密码: admin123');
process.exit(0);
} catch (error) {
console.error('创建失败:', error.message);
console.error('✗ 初始化失败:', error.message);
if (error.name === 'SequelizeUniqueConstraintError') {
console.error(' 原因: 管理员账号已存在');
} else {
console.error(' 详细错误:', error);
}
process.exit(1);
}
}
+27 -12
View File
@@ -1,11 +1,27 @@
const multer = require('multer');
const path = require('path');
const { v4: uuidv4 } = require('uuid');
const { uploadBuffer } = require('../utils/minio');
const fs = require('fs');
// 确保上传目录存在
const uploadDir = path.join(__dirname, '../uploads');
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
// 配置本地存储
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, uploadDir);
},
filename: (req, file, cb) => {
const uniqueName = `${Date.now()}-${Math.random().toString(36).substring(7)}${path.extname(file.originalname)}`;
cb(null, uniqueName);
}
});
// 使用内存存储,文件不落盘,不限制大小
const upload = multer({
storage: multer.memoryStorage(),
storage,
limits: { fileSize: 50 * 1024 * 1024 }, // 50MB
fileFilter: (req, file, cb) => {
const extname = /jpeg|jpg|png|gif|mp4|mov|avi/.test(
path.extname(file.originalname).toLowerCase()
@@ -16,15 +32,14 @@ const upload = multer({
}
});
// 将 req.files 或 req.file 上传到 MinIO,返回 url 数组或单个 url
const uploadFilesToMinio = async (files) => {
// 返回文件访问URL
const getFileUrls = (files) => {
const list = Array.isArray(files) ? files : [files];
return Promise.all(list.map(async (file) => {
const ext = path.extname(file.originalname).toLowerCase();
const filename = `${Date.now()}-${uuidv4()}${ext}`;
const url = await uploadBuffer(file.buffer, filename, file.mimetype);
return { ...file, filename, url };
return list.map(file => ({
url: `/uploads/${file.filename}`,
filename: file.filename,
mimetype: file.mimetype
}));
};
module.exports = { upload, uploadFilesToMinio };
module.exports = { upload, getFileUrls };
+84
View File
@@ -0,0 +1,84 @@
/**
* 迁移: 初始化数据库结构(确保所有表和字段存在)
* 版本: 0000
* 日期: 2025-01-09
*/
module.exports = {
async up(manager) {
console.log(' - 检查并创建所有必需的表和字段...');
// Activities 表的额外字段
const activityFields = [
{ name: 'registrationEnabled', sql: 'ALTER TABLE Activities ADD COLUMN registrationEnabled INTEGER DEFAULT 1;' },
{ name: 'coverImage', sql: 'ALTER TABLE Activities ADD COLUMN coverImage TEXT;' },
{ name: 'price', sql: 'ALTER TABLE Activities ADD COLUMN price DECIMAL(10, 2) DEFAULT 0;' },
{ name: 'budget', sql: 'ALTER TABLE Activities ADD COLUMN budget DECIMAL(10, 2) DEFAULT 0;' },
{ name: 'locationLat', sql: 'ALTER TABLE Activities ADD COLUMN locationLat DECIMAL(10, 7);' },
{ name: 'locationLng', sql: 'ALTER TABLE Activities ADD COLUMN locationLng DECIMAL(10, 7);' }
];
for (const field of activityFields) {
await manager.executeSql(field.sql);
}
// Registrations 表的额外字段
const registrationFields = [
{ name: 'peopleCount', sql: 'ALTER TABLE Registrations ADD COLUMN peopleCount INTEGER DEFAULT 1;' },
{ name: 'note', sql: 'ALTER TABLE Registrations ADD COLUMN note TEXT;' }
];
for (const field of registrationFields) {
await manager.executeSql(field.sql);
}
// Expenses 表的额外字段
const expenseFields = [
{ name: 'category', sql: 'ALTER TABLE Expenses ADD COLUMN category TEXT;' },
{ name: 'receipt', sql: 'ALTER TABLE Expenses ADD COLUMN receipt TEXT;' },
{ name: 'note', sql: 'ALTER TABLE Expenses ADD COLUMN note TEXT;' },
{ name: 'media', sql: 'ALTER TABLE Expenses ADD COLUMN media TEXT;' }
];
for (const field of expenseFields) {
await manager.executeSql(field.sql);
}
// Photos 表的额外字段
const photoFields = [
{ name: 'description', sql: 'ALTER TABLE Photos ADD COLUMN description TEXT;' },
{ name: 'uploadedBy', sql: 'ALTER TABLE Photos ADD COLUMN uploadedBy TEXT;' },
{ name: 'status', sql: 'ALTER TABLE Photos ADD COLUMN status TEXT DEFAULT "approved";' }
];
for (const field of photoFields) {
await manager.executeSql(field.sql);
}
// Sponsors 表的额外字段
const sponsorFields = [
{ name: 'logo', sql: 'ALTER TABLE Sponsors ADD COLUMN logo TEXT;' },
{ name: 'type', sql: 'ALTER TABLE Sponsors ADD COLUMN type TEXT DEFAULT "sponsor";' },
{ name: 'link', sql: 'ALTER TABLE Sponsors ADD COLUMN link TEXT;' }
];
for (const field of sponsorFields) {
await manager.executeSql(field.sql);
}
// Admins 表的额外字段
const adminFields = [
{ name: 'name', sql: 'ALTER TABLE Admins ADD COLUMN name TEXT;' }
];
for (const field of adminFields) {
await manager.executeSql(field.sql);
}
console.log(' - 初始化完成');
},
async down(manager) {
console.log(' - 回滚不支持');
}
};
@@ -0,0 +1,24 @@
/**
* 迁移: 添加弹幕功能字段
* 版本: 0001
* 日期: 2025-01-09
*/
module.exports = {
async up(manager) {
console.log(' - 添加 danmakuEnabled 字段...');
await manager.executeSql(`
ALTER TABLE Activities ADD COLUMN danmakuEnabled INTEGER DEFAULT 0;
`);
console.log(' - 添加 danmakuContent 字段...');
await manager.executeSql(`
ALTER TABLE Activities ADD COLUMN danmakuContent TEXT;
`);
},
async down(manager) {
// SQLite 不支持 DROP COLUMN,需要重建表
console.log(' - 回滚不支持,请手动处理');
}
};
@@ -0,0 +1,18 @@
/**
* 迁移: 添加赞助商排序字段
* 版本: 0002
* 日期: 2025-01-09
*/
module.exports = {
async up(manager) {
console.log(' - 添加 Sponsors.sortOrder 字段...');
await manager.executeSql(`
ALTER TABLE Sponsors ADD COLUMN sortOrder INTEGER DEFAULT 0;
`);
},
async down(manager) {
console.log(' - 回滚不支持');
}
};
@@ -0,0 +1,23 @@
/**
* 迁移: 添加签到功能字段
* 版本: 0003
* 日期: 2025-01-09
*/
module.exports = {
async up(manager) {
console.log(' - 添加 Registrations.checkInStatus 字段...');
await manager.executeSql(`
ALTER TABLE Registrations ADD COLUMN checkInStatus INTEGER DEFAULT 0;
`);
console.log(' - 添加 Registrations.checkInTime 字段...');
await manager.executeSql(`
ALTER TABLE Registrations ADD COLUMN checkInTime DATETIME;
`);
},
async down(manager) {
console.log(' - 回滚不支持');
}
};
@@ -0,0 +1,31 @@
/**
* 迁移: 添加奖品表
* 版本: 0004
* 日期: 2025-01-10
*/
module.exports = {
async up(manager) {
console.log(' - 创建 Prizes 表...');
await manager.executeSql(`
CREATE TABLE IF NOT EXISTS Prizes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
activityId INTEGER NOT NULL,
name TEXT NOT NULL,
level TEXT NOT NULL CHECK(level IN ('一等奖', '二等奖', '三等奖', '参与奖', '伴手礼')),
description TEXT,
image TEXT,
quantity INTEGER DEFAULT 1,
sortOrder INTEGER DEFAULT 0,
createdAt DATETIME DEFAULT CURRENT_TIMESTAMP,
updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP
);
`);
},
async down(manager) {
console.log(' - 删除 Prizes 表...');
await manager.executeSql('DROP TABLE IF EXISTS Prizes;');
}
};
+190
View File
@@ -0,0 +1,190 @@
# 数据库迁移系统
## 概述
本项目使用版本化的数据库迁移系统,支持增量更新和自动同步。
## 特性
- ✅ 版本管理:每个迁移有唯一版本号
- ✅ 增量执行:只执行未执行的迁移
- ✅ 自动运行:服务器启动时自动执行迁移
- ✅ 幂等性:重复执行不会出错
- ✅ 迁移记录:记录所有已执行的迁移
## 迁移文件命名规范
```
XXXX-description.js
```
- `XXXX`: 4位数字版本号(如 0001, 0002)
- `description`: 迁移描述(使用短横线分隔)
示例:
- `0001-add-danmaku-fields.js`
- `0002-add-user-avatar.js`
## 创建新迁移
### 1. 创建迁移文件
在 `backend/migrations/` 目录下创建新文件:
```javascript
/**
* 迁移: 添加用户头像字段
* 版本: 0002
* 日期: 2025-01-09
*/
module.exports = {
async up(manager) {
console.log(' - 添加 avatar 字段...');
await manager.executeSql(`
ALTER TABLE Users ADD COLUMN avatar TEXT;
`);
},
async down(manager) {
// 回滚逻辑(可选)
console.log(' - 回滚不支持');
}
};
```
### 2. 迁移方法
#### `up(manager)` - 升级
执行数据库变更的方法。
可用的 manager 方法:
- `executeSql(sql)`: 执行SQL语句
- `db`: 直接访问数据库连接
#### `down(manager)` - 回滚(可选)
回滚数据库变更的方法。
注意:SQLite 不支持 DROP COLUMN,回滚可能需要重建表。
## 执行迁移
### 自动执行(推荐)
服务器启动时会自动执行所有未执行的迁移:
```bash
# 开发环境
cd backend
npm start
# 生产环境(Docker)
docker-compose up -d
# 或
docker-compose restart
```
### 手动执行
#### 在容器内执行:
```bash
# Windows
migrate.bat
# Linux/Mac
chmod +x migrate.sh
./migrate.sh
```
#### 直接在容器中执行:
```bash
docker exec quanyoumi-app node /app/backend/migrations/index.js
```
## 迁移状态
迁移执行记录保存在 `migrations` 表中:
```sql
SELECT * FROM migrations;
```
输出示例:
```
id | version | name | executed_at
---+---------+-----------------------+---------------------
1 | 1 | add-danmaku-fields | 2025-01-09 10:30:00
2 | 2 | add-user-avatar | 2025-01-09 11:00:00
```
## 常见问题
### Q: 迁移失败怎么办?
A: 检查错误日志,修复问题后重新启动服务器。已执行的迁移不会重复执行。
### Q: 如何回滚迁移?
A: 目前不支持自动回滚。如需回滚,请手动修改数据库或创建新的迁移来撤销更改。
### Q: 可以修改已执行的迁移吗?
A: 不建议。已执行的迁移不应修改。如需更改,请创建新的迁移。
### Q: 迁移在什么时候执行?
A: 服务器启动时自动执行。也可以手动运行 `migrate.bat` 或 `migrate.sh`。
## 最佳实践
1. **版本号递增**:新迁移的版本号应大于所有现有迁移
2. **描述清晰**:文件名和注释应清楚说明迁移目的
3. **测试迁移**:在开发环境测试后再部署到生产环境
4. **幂等性**:迁移应该可以安全地重复执行
5. **小步迭代**:每个迁移只做一件事,便于追踪和回滚
## 示例
### 添加新字段
```javascript
module.exports = {
async up(manager) {
await manager.executeSql(`
ALTER TABLE Activities ADD COLUMN featured INTEGER DEFAULT 0;
`);
}
};
```
### 创建新表
```javascript
module.exports = {
async up(manager) {
await manager.executeSql(`
CREATE TABLE IF NOT EXISTS Comments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
activityId INTEGER NOT NULL,
userId INTEGER NOT NULL,
content TEXT NOT NULL,
createdAt DATETIME DEFAULT CURRENT_TIMESTAMP
);
`);
}
};
```
### 修改数据
```javascript
module.exports = {
async up(manager) {
await manager.executeSql(`
UPDATE Activities SET status = 'completed' WHERE endTime < datetime('now');
`);
}
};
```
+141
View File
@@ -0,0 +1,141 @@
const sqlite3 = require('sqlite3').verbose();
const path = require('path');
const fs = require('fs');
const dbPath = path.join(__dirname, '..', 'data', 'database.sqlite');
const migrationsDir = __dirname;
class MigrationManager {
constructor() {
this.db = new sqlite3.Database(dbPath);
}
// 初始化迁移表
async initMigrationTable() {
return new Promise((resolve, reject) => {
this.db.run(`
CREATE TABLE IF NOT EXISTS migrations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
version INTEGER NOT NULL UNIQUE,
name TEXT NOT NULL,
executed_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`, (err) => {
if (err) reject(err);
else resolve();
});
});
}
// 获取已执行的迁移版本
async getExecutedMigrations() {
return new Promise((resolve, reject) => {
this.db.all('SELECT version FROM migrations ORDER BY version', (err, rows) => {
if (err) reject(err);
else resolve(rows.map(r => r.version));
});
});
}
// 记录迁移执行
async recordMigration(version, name) {
return new Promise((resolve, reject) => {
this.db.run(
'INSERT INTO migrations (version, name) VALUES (?, ?)',
[version, name],
(err) => {
if (err) reject(err);
else resolve();
}
);
});
}
// 执行SQL语句
async executeSql(sql) {
return new Promise((resolve, reject) => {
this.db.run(sql, (err) => {
if (err) {
// 忽略字段已存在的错误
if (err.message.includes('duplicate column name')) {
resolve();
} else {
reject(err);
}
} else {
resolve();
}
});
});
}
// 加载所有迁移文件
loadMigrations() {
const files = fs.readdirSync(migrationsDir)
.filter(f => f.match(/^\d{4}-.*\.js$/) && f !== 'index.js')
.sort();
return files.map(file => {
const version = parseInt(file.split('-')[0]);
const name = file.replace(/^\d{4}-/, '').replace('.js', '');
const migration = require(path.join(migrationsDir, file));
return { version, name, file, migration };
});
}
// 执行迁移
async runMigrations() {
try {
console.log('🔄 开始数据库迁移...\n');
// 初始化迁移表
await this.initMigrationTable();
// 获取已执行的迁移
const executed = await this.getExecutedMigrations();
console.log(`📊 已执行迁移: ${executed.length} 个`);
// 加载所有迁移文件
const migrations = this.loadMigrations();
console.log(`📁 发现迁移文件: ${migrations.length} 个\n`);
// 执行未执行的迁移
let count = 0;
for (const { version, name, file, migration } of migrations) {
if (!executed.includes(version)) {
console.log(`⏳ 执行迁移 [${version}] ${name}...`);
try {
await migration.up(this);
await this.recordMigration(version, name);
console.log(`✅ 迁移 [${version}] ${name} 完成\n`);
count++;
} catch (error) {
console.error(`❌ 迁移 [${version}] ${name} 失败:`, error.message);
throw error;
}
} else {
console.log(`⏭️ 跳过已执行的迁移 [${version}] ${name}`);
}
}
console.log(`\n✨ 迁移完成!共执行 ${count} 个新迁移`);
} catch (error) {
console.error('\n❌ 迁移失败:', error);
throw error;
} finally {
this.db.close();
}
}
}
// 如果直接运行此文件
if (require.main === module) {
const manager = new MigrationManager();
manager.runMigrations()
.then(() => process.exit(0))
.catch(() => process.exit(1));
}
module.exports = MigrationManager;
+9
View File
@@ -38,6 +38,15 @@ const Activity = sequelize.define('Activity', {
type: DataTypes.DECIMAL(10, 2),
defaultValue: 0,
comment: '活动预算'
},
danmakuEnabled: {
type: DataTypes.BOOLEAN,
defaultValue: false,
comment: '弹幕开关'
},
danmakuContent: {
type: DataTypes.TEXT,
comment: '弹幕内容JSON'
}
});
+35
View File
@@ -0,0 +1,35 @@
const { DataTypes } = require('sequelize');
const sequelize = require('./db');
const Prize = sequelize.define('Prize', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
activityId: {
type: DataTypes.INTEGER,
allowNull: false
},
name: {
type: DataTypes.STRING,
allowNull: false
},
level: {
type: DataTypes.ENUM('一等奖', '二等奖', '三等奖', '参与奖', '伴手礼'),
allowNull: false
},
description: DataTypes.TEXT,
image: DataTypes.STRING,
quantity: {
type: DataTypes.INTEGER,
defaultValue: 1
},
sortOrder: {
type: DataTypes.INTEGER,
defaultValue: 0,
comment: '排序值,数字越小越靠前'
}
});
module.exports = Prize;
+6 -1
View File
@@ -20,7 +20,12 @@ const Sponsor = sequelize.define('Sponsor', {
type: DataTypes.ENUM('title', 'sponsor'),
defaultValue: 'sponsor'
},
link: DataTypes.STRING
link: DataTypes.STRING,
sortOrder: {
type: DataTypes.INTEGER,
defaultValue: 0,
comment: '排序值,数字越小越靠前'
}
});
module.exports = Sponsor;
+13 -10
View File
@@ -1,14 +1,17 @@
const { Sequelize } = require('sequelize');
const path = require('path');
const fs = require('fs');
const sequelize = new Sequelize(
process.env.DB_NAME || 'quanyoumi',
process.env.DB_USER || 'root',
process.env.DB_PASSWORD || '',
{
host: process.env.DB_HOST || 'localhost',
dialect: 'mysql',
logging: false
}
);
// 确保data目录存在
const dataDir = path.join(__dirname, '../data');
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true });
}
const sequelize = new Sequelize({
dialect: 'sqlite',
storage: path.join(dataDir, 'database.sqlite'),
logging: false
});
module.exports = sequelize;
+7 -2
View File
@@ -4,11 +4,15 @@ const Registration = require('./Registration');
const Expense = require('./Expense');
const Photo = require('./Photo');
const Sponsor = require('./Sponsor');
const Prize = require('./Prize');
const Admin = require('./Admin');
// 同步数据库
sequelize.sync({ alter: true }).then(() => {
console.log('数据库同步完成');
// 使用 force: false 避免 SQLite alter 表时的约束冲突问题
sequelize.sync({ force: false }).then(() => {
console.log('✓ 数据库同步完成');
}).catch(err => {
console.error('✗ 数据库同步失败:', err.message);
});
module.exports = {
@@ -18,5 +22,6 @@ module.exports = {
Expense,
Photo,
Sponsor,
Prize,
Admin
};
+365 -359
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -9,15 +9,15 @@
},
"dependencies": {
"bcryptjs": "^2.4.3",
"better-sqlite3": "^11.10.0",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"jsonwebtoken": "^9.0.2",
"minio": "^8.0.7",
"multer": "^1.4.5-lts.1",
"mysql2": "^3.6.0",
"sequelize": "^6.33.0",
"uuid": "^9.0.1"
"sql.js": "^1.14.1",
"sqlite3": "^5.1.7"
},
"devDependencies": {
"nodemon": "^3.0.1"
+12 -5
View File
@@ -1,7 +1,7 @@
const express = require('express');
const router = express.Router();
const { Activity, Photo, Sponsor, Expense, Registration } = require('../models');
const { upload, uploadFilesToMinio } = require('../middleware/upload');
const { Activity, Photo, Sponsor, Expense, Registration, Prize } = require('../models');
const { upload, getFileUrls } = require('../middleware/upload');
// 获取活动列表
router.get('/activities', async (req, res) => {
@@ -23,10 +23,17 @@ router.get('/activities/:id', async (req, res) => {
const photos = await Photo.findAll({
where: { activityId: req.params.id, status: 'approved' }
});
const sponsors = await Sponsor.findAll({ where: { activityId: req.params.id } });
const sponsors = await Sponsor.findAll({
where: { activityId: req.params.id },
order: [['sortOrder', 'ASC'], ['id', 'ASC']]
});
const expenses = await Expense.findAll({ where: { activityId: req.params.id } });
const prizes = await Prize.findAll({
where: { activityId: req.params.id },
order: [['sortOrder', 'ASC'], ['id', 'ASC']]
});
res.json({ activity, photos, sponsors, expenses });
res.json({ activity, photos, sponsors, expenses, prizes });
} catch (error) {
res.status(500).json({ error: error.message });
}
@@ -46,7 +53,7 @@ router.get('/activities/:id/expenses', async (req, res) => {
router.post('/activities/:id/photos', upload.array('photos', 10), async (req, res) => {
try {
const { uploadedBy } = req.body;
const uploaded = await uploadFilesToMinio(req.files);
const uploaded = getFileUrls(req.files);
const photos = uploaded.map(file => ({
activityId: req.params.id,
url: file.url,
+81 -15
View File
@@ -3,9 +3,10 @@ const router = express.Router();
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const auth = require('../middleware/auth');
const { upload, uploadFilesToMinio } = require('../middleware/upload');
const { deleteFile } = require('../utils/minio');
const { Admin, Activity, Registration, Expense, Photo, Sponsor } = require('../models');
const { upload, getFileUrls } = require('../middleware/upload');
const { Admin, Activity, Registration, Expense, Photo, Sponsor, Prize } = require('../models');
const fs = require('fs');
const path = require('path');
// 管理员登录
router.post('/login', async (req, res) => {
@@ -100,7 +101,7 @@ router.post('/expenses', auth, upload.array('media', 5), async (req, res) => {
let media = [];
if (req.files && req.files.length > 0) {
const uploaded = await uploadFilesToMinio(req.files);
const uploaded = getFileUrls(req.files);
media = uploaded.map(file => ({
url: file.url,
type: file.mimetype.startsWith('video/') ? 'video' : 'image'
@@ -152,7 +153,7 @@ router.put('/expenses/:id', auth, upload.array('media', 5), async (req, res) =>
// 处理新上传的媒体
if (req.files && req.files.length > 0) {
const uploaded = await uploadFilesToMinio(req.files);
const uploaded = getFileUrls(req.files);
const newMedia = uploaded.map(file => ({
url: file.url,
type: file.mimetype.startsWith('video/') ? 'video' : 'image'
@@ -184,7 +185,7 @@ router.delete('/expenses/:id', auth, async (req, res) => {
router.post('/photos', auth, upload.array('photos', 10), async (req, res) => {
try {
const { activityId } = req.body;
const uploaded = await uploadFilesToMinio(req.files);
const uploaded = getFileUrls(req.files);
const photos = uploaded.map(file => ({
activityId,
url: file.url,
@@ -231,11 +232,12 @@ router.delete('/photos/:id', auth, async (req, res) => {
return res.status(404).json({ error: '照片不存在' });
}
// 从URL提取文件名
// 删除本地文件
const filename = photo.url.split('/').pop();
// 从MinIO删除文件
await deleteFile(filename);
const filePath = path.join(__dirname, '../uploads', filename);
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
}
// 从数据库删除记录
await photo.destroy();
@@ -249,13 +251,20 @@ router.delete('/photos/:id', auth, async (req, res) => {
// 添加赞助商
router.post('/sponsors', auth, upload.single('logo'), async (req, res) => {
try {
const { activityId, name, type, link } = req.body;
const { activityId, name, type, link, sortOrder } = req.body;
let logo = null;
if (req.file) {
const [uploaded] = await uploadFilesToMinio(req.file);
const [uploaded] = getFileUrls(req.file);
logo = uploaded.url;
}
const sponsor = await Sponsor.create({ activityId, name, logo, type, link });
const sponsor = await Sponsor.create({
activityId,
name,
logo,
type,
link,
sortOrder: sortOrder || 0
});
res.json(sponsor);
} catch (error) {
res.status(500).json({ error: error.message });
@@ -265,11 +274,15 @@ router.post('/sponsors', auth, upload.single('logo'), async (req, res) => {
// 更新赞助商
router.put('/sponsors/:id', auth, upload.single('logo'), async (req, res) => {
try {
const { name, type, link } = req.body;
const { name, type, link, sortOrder } = req.body;
const updateData = { name, type, link };
if (sortOrder !== undefined) {
updateData.sortOrder = sortOrder;
}
if (req.file) {
const [uploaded] = await uploadFilesToMinio(req.file);
const [uploaded] = getFileUrls(req.file);
updateData.logo = uploaded.url;
}
@@ -291,4 +304,57 @@ router.delete('/sponsors/:id', auth, async (req, res) => {
}
});
// 添加奖品
router.post('/prizes', auth, upload.single('image'), async (req, res) => {
try {
const { activityId, name, level, description, quantity, sortOrder } = req.body;
let image = null;
if (req.file) {
const [uploaded] = getFileUrls([req.file]);
image = uploaded.url;
}
const prize = await Prize.create({
activityId,
name,
level,
description,
image,
quantity: quantity || 1,
sortOrder: sortOrder || 0
});
res.json(prize);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// 更新奖品
router.put('/prizes/:id', auth, upload.single('image'), async (req, res) => {
try {
const { name, level, description, quantity, sortOrder } = req.body;
const updateData = { name, level, description, quantity, sortOrder };
if (req.file) {
const [uploaded] = getFileUrls([req.file]);
updateData.image = uploaded.url;
}
await Prize.update(updateData, { where: { id: req.params.id } });
const prize = await Prize.findByPk(req.params.id);
res.json(prize);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// 删除奖品
router.delete('/prizes/:id', auth, async (req, res) => {
try {
await Prize.destroy({ where: { id: req.params.id } });
res.json({ message: '删除成功' });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
module.exports = router;
+23 -6
View File
@@ -9,6 +9,9 @@ const app = express();
app.use(cors());
app.use(express.json());
// 静态文件服务 - 提供上传的文件
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
// API 路由
app.use('/api', require('./routes/activity'));
app.use('/api', require('./routes/registration'));
@@ -26,11 +29,25 @@ if (process.env.NODE_ENV === 'production') {
}
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`服务器运行在 http://localhost:${PORT}`);
if (process.env.NODE_ENV === 'production') {
console.log('运行模式: 生产环境(前后端一体)');
} else {
console.log('运行模式: 开发环境(仅后端API)');
// 启动服务器前运行数据库迁移
const MigrationManager = require('./migrations/index');
const runMigrations = async () => {
try {
const manager = new MigrationManager();
await manager.runMigrations();
} catch (error) {
console.error('数据库迁移失败,但服务器将继续启动');
}
};
runMigrations().then(() => {
app.listen(PORT, () => {
console.log(`服务器运行在 http://localhost:${PORT}`);
if (process.env.NODE_ENV === 'production') {
console.log('运行模式: 生产环境(前后端一体)');
} else {
console.log('运行模式: 开发环境(仅后端API)');
}
});
});