Initial backend code

This commit is contained in:
Developer
2026-05-02 12:49:41 +08:00
commit 8bab5d67b2
21 changed files with 7616 additions and 0 deletions
+53
View File
@@ -0,0 +1,53 @@
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
require('dotenv').config();
const connectDB = require('./config/database');
const errorHandler = require('./middleware/errorHandler');
const { notFound } = require('./middleware/notFound');
const authRoutes = require('./routes/auth');
const equityRoutes = require('./routes/equity');
const userRoutes = require('./routes/user');
const tradeRoutes = require('./routes/trade');
const app = express();
connectDB();
app.use(helmet());
app.use(cors());
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));
if (process.env.NODE_ENV === 'development') {
app.use(morgan('dev'));
}
app.get('/health', (req, res) => {
res.json({
status: 'ok',
timestamp: new Date().toISOString(),
service: 'quanyixiaozhushou-backend'
});
});
app.use('/api/auth', authRoutes);
app.use('/api/equity', equityRoutes);
app.use('/api/user', userRoutes);
app.use('/api/trade', tradeRoutes);
app.use(notFound);
app.use(errorHandler);
const PORT = process.env.PORT || 3000;
app.listen(PORT, '0.0.0.0', () => {
console.log(`🚀 权益小助手后端服务运行在端口 ${PORT}`);
console.log(`📊 环境: ${process.env.NODE_ENV || 'development'}`);
console.log(`🌐 访问地址: http://192.168.3.250:${PORT}`);
});
module.exports = app;
+26
View File
@@ -0,0 +1,26 @@
const mongoose = require('mongoose');
const connectDB = async () => {
try {
const conn = await mongoose.connect(process.env.MONGODB_URI, {
// Mongoose 6+ 不需要这些选项,但保留以备不时之需
// useNewUrlParser: true,
// useUnifiedTopology: true,
});
console.log(`✅ MongoDB 连接成功: ${conn.connection.host}`);
} catch (error) {
console.error(`❌ MongoDB 连接失败: ${error.message}`);
process.exit(1);
}
};
mongoose.connection.on('error', (err) => {
console.error(`MongoDB 连接错误: ${err}`);
});
mongoose.connection.on('disconnected', () => {
console.warn('MongoDB 连接已断开');
});
module.exports = connectDB;
+42
View File
@@ -0,0 +1,42 @@
const jwt = require('jsonwebtoken');
const User = require('../models/User');
const auth = async (req, res, next) => {
try {
let token;
if (req.headers.authorization && req.headers.authorization.startsWith('Bearer')) {
token = req.headers.authorization.split(' ')[1];
}
if (!token) {
return res.status(401).json({
success: false,
error: '未授权,请先登录'
});
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = await User.findById(decoded.id);
if (!req.user) {
return res.status(401).json({
success: false,
error: '用户不存在'
});
}
next();
} catch (error) {
return res.status(401).json({
success: false,
error: '未授权,token无效'
});
}
} catch (error) {
next(error);
}
};
module.exports = { auth };
+39
View File
@@ -0,0 +1,39 @@
const errorHandler = (err, req, res, next) => {
let error = { ...err };
error.message = err.message;
console.error('错误详情:', err);
if (err.name === 'CastError') {
const message = '资源未找到';
error = { message, statusCode: 404 };
}
if (err.code === 11000) {
const message = '重复字段值 entered';
error = { message, statusCode: 400 };
}
if (err.name === 'ValidationError') {
const message = Object.values(err.errors).map(val => val.message).join(', ');
error = { message, statusCode: 400 };
}
if (err.name === 'JsonWebTokenError') {
const message = '无效的token';
error = { message, statusCode: 401 };
}
if (err.name === 'TokenExpiredError') {
const message = 'token已过期';
error = { message, statusCode: 401 };
}
res.status(error.statusCode || 500).json({
success: false,
error: error.message || '服务器内部错误',
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
});
};
module.exports = errorHandler;
+7
View File
@@ -0,0 +1,7 @@
const notFound = (req, res, next) => {
const error = new Error(`未找到路由 - ${req.originalUrl}`);
res.status(404);
next(error);
};
module.exports = { notFound };
+80
View File
@@ -0,0 +1,80 @@
const mongoose = require('mongoose');
const equitySchema = new mongoose.Schema({
title: {
type: String,
required: true,
trim: true
},
description: {
type: String,
default: ''
},
type: {
type: String,
enum: ['coupon', 'membership', 'discount', 'gift', 'other'],
required: true
},
platform: {
type: String,
required: true,
enum: ['taobao', 'jd', 'pdd', 'meituan', 'eleme', 'douyin', 'kuaishou', 'other']
},
value: {
type: Number,
default: 0
},
unit: {
type: String,
default: '元'
},
validStart: {
type: Date,
required: true
},
validEnd: {
type: Date,
required: true
},
status: {
type: String,
enum: ['active', 'used', 'expired', 'transferred'],
default: 'active'
},
owner: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
source: {
type: String,
enum: ['manual', 'import', 'purchase', 'transfer'],
default: 'manual'
},
images: [{
type: String
}],
tags: [{
type: String
}],
isTransferable: {
type: Boolean,
default: true
},
transferPrice: {
type: Number,
default: 0
},
metadata: {
type: mongoose.Schema.Types.Mixed,
default: {}
}
}, {
timestamps: true
});
equitySchema.index({ owner: 1, status: 1 });
equitySchema.index({ platform: 1, type: 1 });
equitySchema.index({ validEnd: 1 });
module.exports = mongoose.model('Equity', equitySchema);
+84
View File
@@ -0,0 +1,84 @@
const mongoose = require('mongoose');
const tradeSchema = new mongoose.Schema({
equity: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Equity',
required: true
},
seller: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
buyer: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
default: null
},
price: {
type: Number,
required: true,
min: 0
},
originalPrice: {
type: Number,
default: 0
},
status: {
type: String,
enum: ['pending', 'paid', 'completed', 'cancelled', 'disputed'],
default: 'pending'
},
tradeType: {
type: String,
enum: ['sale', 'auction', 'exchange'],
default: 'sale'
},
description: {
type: String,
default: ''
},
paymentMethod: {
type: String,
enum: ['wechat_pay', 'alipay', 'balance', 'other'],
default: 'wechat_pay'
},
paidAt: {
type: Date,
default: null
},
completedAt: {
type: Date,
default: null
},
cancelledAt: {
type: Date,
default: null
},
cancelReason: {
type: String,
default: ''
},
rating: {
score: {
type: Number,
min: 1,
max: 5,
default: null
},
comment: {
type: String,
default: ''
}
}
}, {
timestamps: true
});
tradeSchema.index({ seller: 1, status: 1 });
tradeSchema.index({ buyer: 1, status: 1 });
tradeSchema.index({ equity: 1 });
tradeSchema.index({ status: 1, createdAt: -1 });
module.exports = mongoose.model('Trade', tradeSchema);
+65
View File
@@ -0,0 +1,65 @@
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
openid: {
type: String,
required: true,
unique: true,
index: true
},
unionid: {
type: String,
sparse: true,
index: true
},
nickname: {
type: String,
default: ''
},
avatarUrl: {
type: String,
default: ''
},
phoneNumber: {
type: String,
default: ''
},
profile: {
gender: {
type: Number,
default: 0
},
country: {
type: String,
default: ''
},
province: {
type: String,
default: ''
},
city: {
type: String,
default: ''
}
},
status: {
type: String,
enum: ['active', 'inactive', 'banned'],
default: 'active'
},
lastLoginAt: {
type: Date,
default: Date.now
}
}, {
timestamps: true
});
userSchema.pre('save', function(next) {
if (this.isModified('lastLoginAt')) {
this.lastLoginAt = Date.now();
}
next();
});
module.exports = mongoose.model('User', userSchema);
+123
View File
@@ -0,0 +1,123 @@
const express = require('express');
const router = express.Router();
const axios = require('axios');
const jwt = require('jsonwebtoken');
const User = require('../models/User');
router.post('/wechat-login', async (req, res, next) => {
try {
const { code, userInfo } = req.body;
if (!code) {
return res.status(400).json({
success: false,
error: '缺少微信登录code'
});
}
const appid = process.env.WECHAT_APPID;
const secret = process.env.WECHAT_APPSECRET;
const wxResponse = await axios.get('https://api.weixin.qq.com/sns/jscode2session', {
params: {
appid,
secret,
js_code: code,
grant_type: 'authorization_code'
}
});
const { openid, session_key, unionid, errcode, errmsg } = wxResponse.data;
if (errcode) {
return res.status(400).json({
success: false,
error: `微信登录失败: ${errmsg}`,
wxErrorCode: errcode
});
}
let user = await User.findOne({ openid });
if (!user) {
user = await User.create({
openid,
unionid: unionid || undefined,
nickname: userInfo?.nickName || '',
avatarUrl: userInfo?.avatarUrl || '',
profile: {
gender: userInfo?.gender || 0,
country: userInfo?.country || '',
province: userInfo?.province || '',
city: userInfo?.city || ''
}
});
} else {
if (userInfo) {
user.nickname = userInfo.nickName || user.nickname;
user.avatarUrl = userInfo.avatarUrl || user.avatarUrl;
user.lastLoginAt = new Date();
await user.save();
}
}
const token = jwt.sign(
{ id: user._id, openid: user.openid },
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRES_IN || '7d' }
);
res.json({
success: true,
data: {
token,
user: {
id: user._id,
nickname: user.nickname,
avatarUrl: user.avatarUrl,
status: user.status
}
}
});
} catch (error) {
next(error);
}
});
router.post('/refresh-token', async (req, res, next) => {
try {
const { token } = req.body;
if (!token) {
return res.status(400).json({
success: false,
error: '缺少token'
});
}
const decoded = jwt.verify(token, process.env.JWT_SECRET, { ignoreExpiration: true });
const user = await User.findById(decoded.id);
if (!user || user.status !== 'active') {
return res.status(401).json({
success: false,
error: '用户不存在或已被禁用'
});
}
const newToken = jwt.sign(
{ id: user._id, openid: user.openid },
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRES_IN || '7d' }
);
res.json({
success: true,
data: { token: newToken }
});
} catch (error) {
next(error);
}
});
module.exports = router;
+197
View File
@@ -0,0 +1,197 @@
const express = require('express');
const router = express.Router();
const { auth } = require('../middleware/auth');
const Equity = require('../models/Equity');
router.get('/', auth, async (req, res, next) => {
try {
const {
page = 1,
limit = 10,
status,
platform,
type,
sortBy = 'createdAt',
order = 'desc'
} = req.query;
const query = { owner: req.user._id };
if (status) query.status = status;
if (platform) query.platform = platform;
if (type) query.type = type;
const sortOrder = order === 'asc' ? 1 : -1;
const skip = (parseInt(page) - 1) * parseInt(limit);
const [equities, total] = await Promise.all([
Equity.find(query)
.sort({ [sortBy]: sortOrder })
.skip(skip)
.limit(parseInt(limit))
.lean(),
Equity.countDocuments(query)
]);
res.json({
success: true,
data: {
list: equities,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total,
pages: Math.ceil(total / parseInt(limit))
}
}
});
} catch (error) {
next(error);
}
});
router.get('/:id', auth, async (req, res, next) => {
try {
const equity = await Equity.findOne({
_id: req.params.id,
owner: req.user._id
});
if (!equity) {
return res.status(404).json({
success: false,
error: '权益不存在'
});
}
res.json({
success: true,
data: equity
});
} catch (error) {
next(error);
}
});
router.post('/', auth, async (req, res, next) => {
try {
const {
title,
description,
type,
platform,
value,
unit,
validStart,
validEnd,
images,
tags,
isTransferable,
transferPrice,
metadata
} = req.body;
if (!title || !type || !platform || !validStart || !validEnd) {
return res.status(400).json({
success: false,
error: '缺少必填字段'
});
}
const equity = await Equity.create({
title,
description,
type,
platform,
value,
unit,
validStart: new Date(validStart),
validEnd: new Date(validEnd),
owner: req.user._id,
images,
tags,
isTransferable,
transferPrice,
metadata
});
res.status(201).json({
success: true,
data: equity
});
} catch (error) {
next(error);
}
});
router.put('/:id', auth, async (req, res, next) => {
try {
const equity = await Equity.findOneAndUpdate(
{ _id: req.params.id, owner: req.user._id },
{ ...req.body, updatedAt: new Date() },
{ new: true, runValidators: true }
);
if (!equity) {
return res.status(404).json({
success: false,
error: '权益不存在或无权限修改'
});
}
res.json({
success: true,
data: equity
});
} catch (error) {
next(error);
}
});
router.delete('/:id', auth, async (req, res, next) => {
try {
const equity = await Equity.findOneAndDelete({
_id: req.params.id,
owner: req.user._id
});
if (!equity) {
return res.status(404).json({
success: false,
error: '权益不存在或无权限删除'
});
}
res.json({
success: true,
message: '权益已删除'
});
} catch (error) {
next(error);
}
});
router.get('/platforms/summary', auth, async (req, res, next) => {
try {
const summary = await Equity.aggregate([
{ $match: { owner: req.user._id } },
{
$group: {
_id: '$platform',
count: { $sum: 1 },
totalValue: { $sum: '$value' }
}
},
{ $sort: { count: -1 } }
]);
res.json({
success: true,
data: summary
});
} catch (error) {
next(error);
}
});
module.exports = router;
+352
View File
@@ -0,0 +1,352 @@
const express = require('express');
const router = express.Router();
const { auth } = require('../middleware/auth');
const Trade = require('../models/Trade');
const Equity = require('../models/Equity');
router.get('/', auth, async (req, res, next) => {
try {
const {
page = 1,
limit = 10,
status,
role,
sortBy = 'createdAt',
order = 'desc'
} = req.query;
const query = {};
if (role === 'seller') {
query.seller = req.user._id;
} else if (role === 'buyer') {
query.buyer = req.user._id;
} else {
query.$or = [
{ seller: req.user._id },
{ buyer: req.user._id }
];
}
if (status) query.status = status;
const sortOrder = order === 'asc' ? 1 : -1;
const skip = (parseInt(page) - 1) * parseInt(limit);
const [trades, total] = await Promise.all([
Trade.find(query)
.populate('equity', 'title type platform value')
.populate('seller', 'nickname avatarUrl')
.populate('buyer', 'nickname avatarUrl')
.sort({ [sortBy]: sortOrder })
.skip(skip)
.limit(parseInt(limit))
.lean(),
Trade.countDocuments(query)
]);
res.json({
success: true,
data: {
list: trades,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total,
pages: Math.ceil(total / parseInt(limit))
}
}
});
} catch (error) {
next(error);
}
});
router.get('/market', auth, async (req, res, next) => {
try {
const {
page = 1,
limit = 10,
platform,
type,
minPrice,
maxPrice,
sortBy = 'createdAt',
order = 'desc'
} = req.query;
const query = {
status: 'pending',
seller: { $ne: req.user._id }
};
if (platform) query.platform = platform;
if (type) query.type = type;
if (minPrice || maxPrice) {
query.price = {};
if (minPrice) query.price.$gte = parseFloat(minPrice);
if (maxPrice) query.price.$lte = parseFloat(maxPrice);
}
const equityQuery = { isTransferable: true };
if (platform) equityQuery.platform = platform;
if (type) equityQuery.type = type;
const sortOrder = order === 'asc' ? 1 : -1;
const skip = (parseInt(page) - 1) * parseInt(limit);
const equities = await Equity.find(equityQuery)
.populate('owner', 'nickname avatarUrl')
.sort({ [sortBy]: sortOrder })
.skip(skip)
.limit(parseInt(limit))
.lean();
const total = await Equity.countDocuments(equityQuery);
res.json({
success: true,
data: {
list: equities,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total,
pages: Math.ceil(total / parseInt(limit))
}
}
});
} catch (error) {
next(error);
}
});
router.get('/:id', auth, async (req, res, next) => {
try {
const trade = await Trade.findById(req.params.id)
.populate('equity')
.populate('seller', 'nickname avatarUrl')
.populate('buyer', 'nickname avatarUrl');
if (!trade) {
return res.status(404).json({
success: false,
error: '交易不存在'
});
}
const isParticipant =
trade.seller._id.toString() === req.user._id.toString() ||
(trade.buyer && trade.buyer._id.toString() === req.user._id.toString());
if (!isParticipant) {
return res.status(403).json({
success: false,
error: '无权查看此交易'
});
}
res.json({
success: true,
data: trade
});
} catch (error) {
next(error);
}
});
router.post('/', auth, async (req, res, next) => {
try {
const { equityId, price, description, tradeType } = req.body;
if (!equityId || !price) {
return res.status(400).json({
success: false,
error: '缺少必填字段'
});
}
const equity = await Equity.findOne({
_id: equityId,
owner: req.user._id,
status: 'active',
isTransferable: true
});
if (!equity) {
return res.status(404).json({
success: false,
error: '权益不存在或不可转让'
});
}
const trade = await Trade.create({
equity: equityId,
seller: req.user._id,
price,
originalPrice: equity.value,
description,
tradeType: tradeType || 'sale'
});
await Trade.findById(trade._id)
.populate('equity', 'title type platform value')
.populate('seller', 'nickname avatarUrl');
res.status(201).json({
success: true,
data: trade
});
} catch (error) {
next(error);
}
});
router.post('/:id/purchase', auth, async (req, res, next) => {
try {
const trade = await Trade.findOne({
_id: req.params.id,
status: 'pending',
seller: { $ne: req.user._id }
});
if (!trade) {
return res.status(404).json({
success: false,
error: '交易不存在或不可购买'
});
}
trade.buyer = req.user._id;
trade.status = 'paid';
trade.paidAt = new Date();
await trade.save();
await Equity.findByIdAndUpdate(trade.equity, {
status: 'transferred',
owner: req.user._id
});
const updatedTrade = await Trade.findById(trade._id)
.populate('equity')
.populate('seller', 'nickname avatarUrl')
.populate('buyer', 'nickname avatarUrl');
res.json({
success: true,
data: updatedTrade
});
} catch (error) {
next(error);
}
});
router.put('/:id/cancel', auth, async (req, res, next) => {
try {
const { reason } = req.body;
const trade = await Trade.findOne({
_id: req.params.id,
$or: [
{ seller: req.user._id },
{ buyer: req.user._id }
],
status: { $in: ['pending', 'paid'] }
});
if (!trade) {
return res.status(404).json({
success: false,
error: '交易不存在或无法取消'
});
}
trade.status = 'cancelled';
trade.cancelledAt = new Date();
trade.cancelReason = reason || '用户取消';
await trade.save();
if (trade.status === 'paid') {
await Equity.findByIdAndUpdate(trade.equity, {
status: 'active',
owner: trade.seller
});
}
res.json({
success: true,
message: '交易已取消',
data: trade
});
} catch (error) {
next(error);
}
});
router.put('/:id/complete', auth, async (req, res, next) => {
try {
const trade = await Trade.findOne({
_id: req.params.id,
buyer: req.user._id,
status: 'paid'
});
if (!trade) {
return res.status(404).json({
success: false,
error: '交易不存在或无法完成'
});
}
trade.status = 'completed';
trade.completedAt = new Date();
await trade.save();
res.json({
success: true,
message: '交易已完成',
data: trade
});
} catch (error) {
next(error);
}
});
router.put('/:id/rate', auth, async (req, res, next) => {
try {
const { score, comment } = req.body;
if (!score || score < 1 || score > 5) {
return res.status(400).json({
success: false,
error: '评分必须在1-5之间'
});
}
const trade = await Trade.findOne({
_id: req.params.id,
buyer: req.user._id,
status: 'completed'
});
if (!trade) {
return res.status(404).json({
success: false,
error: '交易不存在或无法评价'
});
}
trade.rating = { score, comment };
await trade.save();
res.json({
success: true,
message: '评价已提交',
data: trade
});
} catch (error) {
next(error);
}
});
module.exports = router;
+71
View File
@@ -0,0 +1,71 @@
const express = require('express');
const router = express.Router();
const { auth } = require('../middleware/auth');
const User = require('../models/User');
router.get('/profile', auth, async (req, res, next) => {
try {
const user = await User.findById(req.user._id).select('-__v');
res.json({
success: true,
data: user
});
} catch (error) {
next(error);
}
});
router.put('/profile', auth, async (req, res, next) => {
try {
const allowedUpdates = ['nickname', 'avatarUrl', 'phoneNumber', 'profile'];
const updates = {};
Object.keys(req.body).forEach(key => {
if (allowedUpdates.includes(key)) {
updates[key] = req.body[key];
}
});
const user = await User.findByIdAndUpdate(
req.user._id,
updates,
{ new: true, runValidators: true }
).select('-__v');
res.json({
success: true,
data: user
});
} catch (error) {
next(error);
}
});
router.get('/stats', auth, async (req, res, next) => {
try {
const Equity = require('../models/Equity');
const Trade = require('../models/Trade');
const [totalEquities, activeEquities, totalTrades, sellingTrades] = await Promise.all([
Equity.countDocuments({ owner: req.user._id }),
Equity.countDocuments({ owner: req.user._id, status: 'active' }),
Trade.countDocuments({ $or: [{ seller: req.user._id }, { buyer: req.user._id }] }),
Trade.countDocuments({ seller: req.user._id, status: 'pending' })
]);
res.json({
success: true,
data: {
totalEquities,
activeEquities,
totalTrades,
sellingTrades
}
});
} catch (error) {
next(error);
}
});
module.exports = router;