Initial commit: 全友米项目完整代码
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
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
|
||||
@@ -0,0 +1,27 @@
|
||||
// 初始化管理员账号
|
||||
require('dotenv').config();
|
||||
const bcrypt = require('bcryptjs');
|
||||
const { Admin, sequelize } = require('./models');
|
||||
|
||||
async function initAdmin() {
|
||||
try {
|
||||
await sequelize.sync({ force: false });
|
||||
|
||||
const hashedPassword = bcrypt.hashSync('admin123', 10);
|
||||
await Admin.create({
|
||||
username: 'admin',
|
||||
password: hashedPassword,
|
||||
name: '管理员'
|
||||
});
|
||||
|
||||
console.log('管理员账号创建成功');
|
||||
console.log('用户名: admin');
|
||||
console.log('密码: admin123');
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('创建失败:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
initAdmin();
|
||||
@@ -0,0 +1,19 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
const auth = (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);
|
||||
req.adminId = decoded.id;
|
||||
next();
|
||||
} catch (error) {
|
||||
res.status(401).json({ error: '无效的token' });
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = auth;
|
||||
@@ -0,0 +1,30 @@
|
||||
const multer = require('multer');
|
||||
const path = require('path');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const { uploadBuffer } = require('../utils/minio');
|
||||
|
||||
// 使用内存存储,文件不落盘,不限制大小
|
||||
const upload = multer({
|
||||
storage: multer.memoryStorage(),
|
||||
fileFilter: (req, file, cb) => {
|
||||
const extname = /jpeg|jpg|png|gif|mp4|mov|avi/.test(
|
||||
path.extname(file.originalname).toLowerCase()
|
||||
);
|
||||
const mimetype = /image|video/.test(file.mimetype);
|
||||
if (mimetype && extname) return cb(null, true);
|
||||
cb(new Error('只允许上传图片或视频文件'));
|
||||
}
|
||||
});
|
||||
|
||||
// 将 req.files 或 req.file 上传到 MinIO,返回 url 数组或单个 url
|
||||
const uploadFilesToMinio = async (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 };
|
||||
}));
|
||||
};
|
||||
|
||||
module.exports = { upload, uploadFilesToMinio };
|
||||
@@ -0,0 +1,44 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const sequelize = require('./db');
|
||||
|
||||
const Activity = sequelize.define('Activity', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
title: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
description: DataTypes.TEXT,
|
||||
location: DataTypes.STRING,
|
||||
locationLat: DataTypes.DECIMAL(10, 7),
|
||||
locationLng: DataTypes.DECIMAL(10, 7),
|
||||
startTime: DataTypes.DATE,
|
||||
endTime: DataTypes.DATE,
|
||||
registrationDeadline: DataTypes.DATE,
|
||||
maxParticipants: DataTypes.INTEGER,
|
||||
status: {
|
||||
type: DataTypes.ENUM('upcoming', 'ongoing', 'completed'),
|
||||
defaultValue: 'upcoming'
|
||||
},
|
||||
registrationEnabled: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: true,
|
||||
comment: '报名开关'
|
||||
},
|
||||
coverImage: DataTypes.STRING,
|
||||
price: {
|
||||
type: DataTypes.DECIMAL(10, 2),
|
||||
defaultValue: 0,
|
||||
comment: '人均费用'
|
||||
},
|
||||
budget: {
|
||||
type: DataTypes.DECIMAL(10, 2),
|
||||
defaultValue: 0,
|
||||
comment: '活动预算'
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Activity;
|
||||
@@ -0,0 +1,22 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const sequelize = require('./db');
|
||||
|
||||
const Admin = sequelize.define('Admin', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
username: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
unique: true
|
||||
},
|
||||
password: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
name: DataTypes.STRING
|
||||
});
|
||||
|
||||
module.exports = Admin;
|
||||
@@ -0,0 +1,32 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const sequelize = require('./db');
|
||||
|
||||
const Expense = sequelize.define('Expense', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
activityId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false
|
||||
},
|
||||
item: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
amount: {
|
||||
type: DataTypes.DECIMAL(10, 2),
|
||||
allowNull: false
|
||||
},
|
||||
category: DataTypes.STRING,
|
||||
receipt: DataTypes.STRING,
|
||||
note: DataTypes.TEXT,
|
||||
media: {
|
||||
type: DataTypes.JSON,
|
||||
defaultValue: [],
|
||||
comment: '经费相关照片/视频,最多5个'
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Expense;
|
||||
@@ -0,0 +1,30 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const sequelize = require('./db');
|
||||
|
||||
const Photo = sequelize.define('Photo', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
activityId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false
|
||||
},
|
||||
url: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
type: {
|
||||
type: DataTypes.ENUM('image', 'video'),
|
||||
defaultValue: 'image'
|
||||
},
|
||||
description: DataTypes.STRING,
|
||||
uploadedBy: DataTypes.STRING,
|
||||
status: {
|
||||
type: DataTypes.ENUM('pending', 'approved', 'rejected'),
|
||||
defaultValue: 'approved'
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Photo;
|
||||
@@ -0,0 +1,40 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const sequelize = require('./db');
|
||||
|
||||
const Registration = sequelize.define('Registration', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
activityId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
phone: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
carModel: DataTypes.STRING,
|
||||
carNumber: DataTypes.STRING,
|
||||
peopleCount: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 1
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('pending', 'approved', 'rejected'),
|
||||
defaultValue: 'pending'
|
||||
},
|
||||
note: DataTypes.TEXT,
|
||||
checkInStatus: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false
|
||||
},
|
||||
checkInTime: DataTypes.DATE
|
||||
});
|
||||
|
||||
module.exports = Registration;
|
||||
@@ -0,0 +1,26 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const sequelize = require('./db');
|
||||
|
||||
const Sponsor = sequelize.define('Sponsor', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
activityId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
logo: DataTypes.STRING,
|
||||
type: {
|
||||
type: DataTypes.ENUM('title', 'sponsor'),
|
||||
defaultValue: 'sponsor'
|
||||
},
|
||||
link: DataTypes.STRING
|
||||
});
|
||||
|
||||
module.exports = Sponsor;
|
||||
@@ -0,0 +1,14 @@
|
||||
const { Sequelize } = require('sequelize');
|
||||
|
||||
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
|
||||
}
|
||||
);
|
||||
|
||||
module.exports = sequelize;
|
||||
@@ -0,0 +1,22 @@
|
||||
const sequelize = require('./db');
|
||||
const Activity = require('./Activity');
|
||||
const Registration = require('./Registration');
|
||||
const Expense = require('./Expense');
|
||||
const Photo = require('./Photo');
|
||||
const Sponsor = require('./Sponsor');
|
||||
const Admin = require('./Admin');
|
||||
|
||||
// 同步数据库
|
||||
sequelize.sync({ alter: true }).then(() => {
|
||||
console.log('数据库同步完成');
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
sequelize,
|
||||
Activity,
|
||||
Registration,
|
||||
Expense,
|
||||
Photo,
|
||||
Sponsor,
|
||||
Admin
|
||||
};
|
||||
Generated
+2140
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "quanyoumi-backend",
|
||||
"version": "1.0.0",
|
||||
"description": "泉友米车友会管理系统后端",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "nodemon server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"bcryptjs": "^2.4.3",
|
||||
"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"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { Activity, Photo, Sponsor, Expense, Registration } = require('../models');
|
||||
const { upload, uploadFilesToMinio } = require('../middleware/upload');
|
||||
|
||||
// 获取活动列表
|
||||
router.get('/activities', async (req, res) => {
|
||||
try {
|
||||
const { status } = req.query;
|
||||
const where = status ? { status } : {};
|
||||
const activities = await Activity.findAll({ where, order: [['startTime', 'DESC']] });
|
||||
res.json(activities);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取活动详情
|
||||
router.get('/activities/:id', async (req, res) => {
|
||||
try {
|
||||
const activity = await Activity.findByPk(req.params.id);
|
||||
// 只返回已审核通过的照片
|
||||
const photos = await Photo.findAll({
|
||||
where: { activityId: req.params.id, status: 'approved' }
|
||||
});
|
||||
const sponsors = await Sponsor.findAll({ where: { activityId: req.params.id } });
|
||||
const expenses = await Expense.findAll({ where: { activityId: req.params.id } });
|
||||
|
||||
res.json({ activity, photos, sponsors, expenses });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取活动经费(公开接口)
|
||||
router.get('/activities/:id/expenses', async (req, res) => {
|
||||
try {
|
||||
const expenses = await Expense.findAll({ where: { activityId: req.params.id } });
|
||||
res.json(expenses);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 用户上传照片(需要审核)
|
||||
router.post('/activities/:id/photos', upload.array('photos', 10), async (req, res) => {
|
||||
try {
|
||||
const { uploadedBy } = req.body;
|
||||
const uploaded = await uploadFilesToMinio(req.files);
|
||||
const photos = uploaded.map(file => ({
|
||||
activityId: req.params.id,
|
||||
url: file.url,
|
||||
type: /\.(mp4|mov|avi)$/i.test(file.filename) ? 'video' : 'image',
|
||||
uploadedBy,
|
||||
status: 'pending'
|
||||
}));
|
||||
const created = await Photo.bulkCreate(photos);
|
||||
res.json(created);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 用户验证登录
|
||||
router.post('/auth/verify/:activityId', async (req, res) => {
|
||||
try {
|
||||
const { phone, carNumber } = req.body;
|
||||
const { activityId } = req.params;
|
||||
|
||||
console.log('验证请求:', { phone, carNumber, activityId });
|
||||
|
||||
if (!phone || !carNumber) {
|
||||
return res.status(400).json({ error: '请输入手机号和车牌号' });
|
||||
}
|
||||
|
||||
const registration = await Registration.findOne({
|
||||
where: {
|
||||
activityId,
|
||||
phone,
|
||||
carNumber
|
||||
}
|
||||
});
|
||||
|
||||
console.log('查询结果:', registration ? '找到' : '未找到');
|
||||
|
||||
if (!registration) {
|
||||
// 调试:查询该活动下该手机号的所有报名
|
||||
const allRegs = await Registration.findAll({
|
||||
where: { activityId, phone }
|
||||
});
|
||||
console.log('该手机号的所有报名:', allRegs.map(r => ({ id: r.id, carNumber: r.carNumber })));
|
||||
|
||||
return res.status(404).json({ error: '未找到报名信息,请检查车牌号是否正确' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
registration: {
|
||||
id: registration.id,
|
||||
name: registration.name,
|
||||
phone: registration.phone,
|
||||
carNumber: registration.carNumber,
|
||||
carModel: registration.carModel,
|
||||
status: registration.status,
|
||||
activityId: registration.activityId
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('验证错误:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,294 @@
|
||||
const express = require('express');
|
||||
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');
|
||||
|
||||
// 管理员登录
|
||||
router.post('/login', async (req, res) => {
|
||||
try {
|
||||
const { username, password } = req.body;
|
||||
const admin = await Admin.findOne({ where: { username } });
|
||||
if (!admin || !bcrypt.compareSync(password, admin.password)) {
|
||||
return res.status(401).json({ error: '用户名或密码错误' });
|
||||
}
|
||||
const token = jwt.sign({ id: admin.id }, process.env.JWT_SECRET, { expiresIn: '7d' });
|
||||
res.json({ token, admin: { id: admin.id, username: admin.username, name: admin.name } });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 创建活动
|
||||
router.post('/activities', auth, async (req, res) => {
|
||||
try {
|
||||
const activity = await Activity.create(req.body);
|
||||
res.json(activity);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 编辑活动
|
||||
router.put('/activities/:id', auth, async (req, res) => {
|
||||
try {
|
||||
await Activity.update(req.body, { where: { id: req.params.id } });
|
||||
const activity = await Activity.findByPk(req.params.id);
|
||||
res.json(activity);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 审核报名
|
||||
router.put('/registrations/:id', auth, async (req, res) => {
|
||||
try {
|
||||
const { status, peopleCount } = req.body;
|
||||
const updateData = {};
|
||||
if (status) updateData.status = status;
|
||||
if (peopleCount !== undefined) updateData.peopleCount = peopleCount;
|
||||
|
||||
await Registration.update(updateData, { where: { id: req.params.id } });
|
||||
const registration = await Registration.findByPk(req.params.id);
|
||||
res.json(registration);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 签到/取消签到
|
||||
router.put('/registrations/:id/checkin', auth, async (req, res) => {
|
||||
try {
|
||||
const registration = await Registration.findByPk(req.params.id);
|
||||
if (!registration) {
|
||||
return res.status(404).json({ error: '报名信息不存在' });
|
||||
}
|
||||
|
||||
const newStatus = !registration.checkInStatus;
|
||||
await Registration.update({
|
||||
checkInStatus: newStatus,
|
||||
checkInTime: newStatus ? new Date() : null
|
||||
}, { 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 {
|
||||
const { activityId } = req.query;
|
||||
const where = activityId ? { activityId } : {};
|
||||
const registrations = await Registration.findAll({ where, order: [['createdAt', 'DESC']] });
|
||||
res.json(registrations);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 上传经费明细
|
||||
router.post('/expenses', auth, upload.array('media', 5), async (req, res) => {
|
||||
try {
|
||||
const { activityId, item, amount, category, note } = req.body;
|
||||
let receipt = null;
|
||||
let media = [];
|
||||
|
||||
if (req.files && req.files.length > 0) {
|
||||
const uploaded = await uploadFilesToMinio(req.files);
|
||||
media = uploaded.map(file => ({
|
||||
url: file.url,
|
||||
type: file.mimetype.startsWith('video/') ? 'video' : 'image'
|
||||
}));
|
||||
}
|
||||
|
||||
const expense = await Expense.create({
|
||||
activityId,
|
||||
item,
|
||||
amount,
|
||||
category,
|
||||
receipt,
|
||||
note,
|
||||
media
|
||||
});
|
||||
res.json(expense);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取经费列表
|
||||
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);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 更新经费
|
||||
router.put('/expenses/:id', auth, upload.array('media', 5), async (req, res) => {
|
||||
try {
|
||||
const { item, amount, category, note, existingMedia } = req.body;
|
||||
const updateData = { item, amount, category, note };
|
||||
|
||||
// 处理现有媒体
|
||||
let media = [];
|
||||
if (existingMedia) {
|
||||
try {
|
||||
media = JSON.parse(existingMedia);
|
||||
} catch (e) {
|
||||
media = [];
|
||||
}
|
||||
}
|
||||
|
||||
// 处理新上传的媒体
|
||||
if (req.files && req.files.length > 0) {
|
||||
const uploaded = await uploadFilesToMinio(req.files);
|
||||
const newMedia = uploaded.map(file => ({
|
||||
url: file.url,
|
||||
type: file.mimetype.startsWith('video/') ? 'video' : 'image'
|
||||
}));
|
||||
media = [...media, ...newMedia].slice(0, 5); // 限制最多5个
|
||||
}
|
||||
|
||||
updateData.media = media;
|
||||
|
||||
await Expense.update(updateData, { where: { id: req.params.id } });
|
||||
const expense = await Expense.findByPk(req.params.id);
|
||||
res.json(expense);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 删除经费
|
||||
router.delete('/expenses/:id', auth, async (req, res) => {
|
||||
try {
|
||||
await Expense.destroy({ where: { id: req.params.id } });
|
||||
res.json({ message: '删除成功' });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 上传活动照片
|
||||
router.post('/photos', auth, upload.array('photos', 10), async (req, res) => {
|
||||
try {
|
||||
const { activityId } = req.body;
|
||||
const uploaded = await uploadFilesToMinio(req.files);
|
||||
const photos = uploaded.map(file => ({
|
||||
activityId,
|
||||
url: file.url,
|
||||
type: /\.(mp4|mov|avi)$/i.test(file.filename) ? 'video' : 'image',
|
||||
status: 'approved'
|
||||
}));
|
||||
const created = await Photo.bulkCreate(photos);
|
||||
res.json(created);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取待审核照片
|
||||
router.get('/photos/pending', auth, async (req, res) => {
|
||||
try {
|
||||
const photos = await Photo.findAll({
|
||||
where: { status: 'pending' },
|
||||
order: [['createdAt', 'DESC']]
|
||||
});
|
||||
res.json(photos);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 审核照片
|
||||
router.put('/photos/:id', auth, async (req, res) => {
|
||||
try {
|
||||
const { status } = req.body;
|
||||
await Photo.update({ status }, { where: { id: req.params.id } });
|
||||
const photo = await Photo.findByPk(req.params.id);
|
||||
res.json(photo);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 删除照片
|
||||
router.delete('/photos/:id', auth, async (req, res) => {
|
||||
try {
|
||||
const photo = await Photo.findByPk(req.params.id);
|
||||
if (!photo) {
|
||||
return res.status(404).json({ error: '照片不存在' });
|
||||
}
|
||||
|
||||
// 从URL提取文件名
|
||||
const filename = photo.url.split('/').pop();
|
||||
|
||||
// 从MinIO删除文件
|
||||
await deleteFile(filename);
|
||||
|
||||
// 从数据库删除记录
|
||||
await photo.destroy();
|
||||
|
||||
res.json({ message: '删除成功' });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 添加赞助商
|
||||
router.post('/sponsors', auth, upload.single('logo'), async (req, res) => {
|
||||
try {
|
||||
const { activityId, name, type, link } = req.body;
|
||||
let logo = null;
|
||||
if (req.file) {
|
||||
const [uploaded] = await uploadFilesToMinio(req.file);
|
||||
logo = uploaded.url;
|
||||
}
|
||||
const sponsor = await Sponsor.create({ activityId, name, logo, type, link });
|
||||
res.json(sponsor);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 更新赞助商
|
||||
router.put('/sponsors/:id', auth, upload.single('logo'), async (req, res) => {
|
||||
try {
|
||||
const { name, type, link } = req.body;
|
||||
const updateData = { name, type, link };
|
||||
|
||||
if (req.file) {
|
||||
const [uploaded] = await uploadFilesToMinio(req.file);
|
||||
updateData.logo = uploaded.url;
|
||||
}
|
||||
|
||||
await Sponsor.update(updateData, { where: { id: req.params.id } });
|
||||
const sponsor = await Sponsor.findByPk(req.params.id);
|
||||
res.json(sponsor);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 删除赞助商
|
||||
router.delete('/sponsors/:id', auth, async (req, res) => {
|
||||
try {
|
||||
await Sponsor.destroy({ where: { id: req.params.id } });
|
||||
res.json({ message: '删除成功' });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,47 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { Registration, Activity } = require('../models');
|
||||
|
||||
// 提交报名
|
||||
router.post('/registrations', async (req, res) => {
|
||||
try {
|
||||
const { activityId, name, phone, carModel, carNumber, note } = req.body;
|
||||
|
||||
const activity = await Activity.findByPk(activityId);
|
||||
if (!activity) {
|
||||
return res.status(404).json({ error: '活动不存在' });
|
||||
}
|
||||
|
||||
const registration = await Registration.create({
|
||||
activityId, name, phone, carModel, carNumber, note
|
||||
});
|
||||
|
||||
res.json(registration);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 查询报名状态
|
||||
router.get('/registrations/:id', async (req, res) => {
|
||||
try {
|
||||
const registration = await Registration.findByPk(req.params.id);
|
||||
res.json(registration);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取活动报名列表(已通过)
|
||||
router.get('/activities/:id/registrations', async (req, res) => {
|
||||
try {
|
||||
const registrations = await Registration.findAll({
|
||||
where: { activityId: req.params.id, status: 'approved' }
|
||||
});
|
||||
res.json(registrations);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,36 @@
|
||||
require('dotenv').config();
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const path = require('path');
|
||||
|
||||
const app = express();
|
||||
|
||||
// 中间件
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
// API 路由
|
||||
app.use('/api', require('./routes/activity'));
|
||||
app.use('/api', require('./routes/registration'));
|
||||
app.use('/api/admin', require('./routes/admin'));
|
||||
|
||||
// 生产环境下提供前端静态文件
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
// 静态文件目录
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
// 所有其他请求返回 index.html(支持前端路由)
|
||||
app.get('*', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, 'public', 'index.html'));
|
||||
});
|
||||
}
|
||||
|
||||
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)');
|
||||
}
|
||||
});
|
||||
Binary file not shown.
@@ -0,0 +1,88 @@
|
||||
const Minio = require('minio');
|
||||
|
||||
const config = {
|
||||
endPoint: process.env.MINIO_ENDPOINT || 'localhost',
|
||||
port: parseInt(process.env.MINIO_PORT) || 9000,
|
||||
useSSL: process.env.MINIO_USE_SSL === 'true',
|
||||
accessKey: process.env.MINIO_ACCESS_KEY || 'minioadmin',
|
||||
secretKey: process.env.MINIO_SECRET_KEY || 'minioadmin'
|
||||
};
|
||||
|
||||
console.log('MinIO 配置:', {
|
||||
...config,
|
||||
secretKey: '***' // 隐藏密钥
|
||||
});
|
||||
|
||||
const client = new Minio.Client(config);
|
||||
|
||||
const BUCKET = process.env.MINIO_BUCKET || 'quanyoumi';
|
||||
|
||||
// 确保 bucket 存在
|
||||
const ensureBucket = async () => {
|
||||
try {
|
||||
console.log(`检查 bucket: ${BUCKET}`);
|
||||
const exists = await client.bucketExists(BUCKET);
|
||||
console.log(`Bucket ${BUCKET} 存在:`, exists);
|
||||
|
||||
if (!exists) {
|
||||
console.log(`创建 bucket: ${BUCKET}`);
|
||||
await client.makeBucket(BUCKET);
|
||||
|
||||
// 设置公开读取策略
|
||||
const policy = JSON.stringify({
|
||||
Version: '2012-10-17',
|
||||
Statement: [{
|
||||
Effect: 'Allow',
|
||||
Principal: { AWS: ['*'] },
|
||||
Action: ['s3:GetObject'],
|
||||
Resource: [`arn:aws:s3:::${BUCKET}/*`]
|
||||
}]
|
||||
});
|
||||
await client.setBucketPolicy(BUCKET, policy);
|
||||
console.log(`Bucket ${BUCKET} 创建成功并设置为公开读取`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('ensureBucket 错误:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// 上传 buffer 到 MinIO,返回公开访问 URL
|
||||
const uploadBuffer = async (buffer, filename, mimetype) => {
|
||||
try {
|
||||
console.log(`开始上传文件: ${filename}, 大小: ${buffer.length} bytes`);
|
||||
await ensureBucket();
|
||||
|
||||
await client.putObject(BUCKET, filename, buffer, buffer.length, { 'Content-Type': mimetype });
|
||||
console.log(`文件上传成功: ${filename}`);
|
||||
|
||||
// 如果配置了公开 URL 就用,否则用 endpoint 拼接
|
||||
const publicUrl = process.env.MINIO_PUBLIC_URL;
|
||||
if (publicUrl) {
|
||||
return `${publicUrl}/${BUCKET}/${filename}`;
|
||||
}
|
||||
|
||||
const protocol = process.env.MINIO_USE_SSL === 'true' ? 'https' : 'http';
|
||||
const endpoint = process.env.MINIO_ENDPOINT || 'localhost';
|
||||
const port = process.env.MINIO_PORT || 9000;
|
||||
return `${protocol}://${endpoint}:${port}/${BUCKET}/${filename}`;
|
||||
} catch (error) {
|
||||
console.error('uploadBuffer 错误:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// 从 MinIO 删除文件
|
||||
const deleteFile = async (filename) => {
|
||||
try {
|
||||
console.log(`开始删除文件: ${filename}`);
|
||||
await client.removeObject(BUCKET, filename);
|
||||
console.log(`文件删除成功: ${filename}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('deleteFile 错误:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { client, BUCKET, uploadBuffer, deleteFile };
|
||||
Reference in New Issue
Block a user