Initial backend code
This commit is contained in:
+53
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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 };
|
||||
@@ -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;
|
||||
@@ -0,0 +1,7 @@
|
||||
const notFound = (req, res, next) => {
|
||||
const error = new Error(`未找到路由 - ${req.originalUrl}`);
|
||||
res.status(404);
|
||||
next(error);
|
||||
};
|
||||
|
||||
module.exports = { notFound };
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user