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
+17
View File
@@ -0,0 +1,17 @@
# 服务器配置
PORT=3000
NODE_ENV=development
# 微信小程序配置
WECHAT_APPID=your_app_id_here
WECHAT_APPSECRET=your_app_secret_here
# MongoDB数据库配置
MONGODB_URI=mongodb://localhost:27017/quanyixiaozhushou
# JWT配置
JWT_SECRET=your_jwt_secret_key_here
JWT_EXPIRES_IN=7d
# 日志配置
LOG_LEVEL=info
+42
View File
@@ -0,0 +1,42 @@
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Logs
logs/
*.log
# Runtime data
pids/
*.pid
*.seed
*.pid.lock
# Coverage directory used by tools like istanbul
coverage/
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Build
dist/
build/
# MongoDB local data
mongodb/
@@ -0,0 +1,7 @@
优酷/芒果年卡(二选一),年卡价值258元
网易云音乐年卡,年卡价值216元
夸克网盘年卡,买年卡价值198元
高德打车直达Lv6 会员
飞猪直达F4会员
淘票票,每月2张4元优惠券
退货包运费
+26
View File
@@ -0,0 +1,26 @@
# 权益小助手后端服务
## 项目简介
本项目是「权益小助手」的后端服务,为前端应用提供 API 接口支持。当前处于项目初始化阶段,尚未开始正式开发。
## 目录结构
```
backend/
├── project.md # 项目信息、里程碑及开发规范
└── README.md # 本文件
```
## 当前状态
- 项目已创建基础文档
- 等待后续技术选型与框架搭建
## 技术栈(待定)
待需求分析完成后确定。
## 开发规范
详见 [project.md](./project.md)。
+6220
View File
File diff suppressed because it is too large Load Diff
+44
View File
@@ -0,0 +1,44 @@
{
"name": "quanyixiaozhushou-backend",
"version": "1.0.0",
"description": "权益小助手后端服务 - 微信小程序后台API",
"main": "src/app.js",
"scripts": {
"start": "node src/app.js",
"dev": "nodemon src/app.js",
"test": "jest",
"lint": "eslint src/",
"typecheck": "tsc --noEmit"
},
"keywords": [
"wechat",
"miniprogram",
"equity",
"backend",
"api"
],
"author": "",
"license": "MIT",
"dependencies": {
"express": "^4.18.2",
"cors": "^2.8.5",
"helmet": "^7.1.0",
"morgan": "^1.10.0",
"dotenv": "^16.3.1",
"mongoose": "^8.0.3",
"jsonwebtoken": "^9.0.2",
"bcryptjs": "^2.4.3",
"express-validator": "^7.0.1",
"axios": "^1.6.2",
"crypto-js": "^4.2.0"
},
"devDependencies": {
"nodemon": "^3.0.2",
"jest": "^29.7.0",
"eslint": "^8.56.0",
"@types/node": "^20.10.6"
},
"engines": {
"node": ">=18.0.0"
}
}
+38
View File
@@ -0,0 +1,38 @@
# 项目信息
## 项目概述
- **项目名称**:权益小助手后端服务
- **项目类型**:后端服务(纯后端项目)
- **工作目录**`d:\003_Project\WeixinProject\QuanYiXiaoZhuShou\backend`
- **当前状态**:项目初始化阶段,尚未开始开发
## 项目开发里程碑
| 里程碑 | 状态 | 计划开始时间 | 计划完成时间 | 实际完成时间 | 备注 |
|--------|------|-------------|-------------|-------------|------|
| 项目初始化 | 进行中 | 2026-05-01 | 2026-05-01 | - | 创建项目基础结构、配置和文档 |
| 需求分析与设计 | 未开始 | - | - | - | 待初始化完成后进行 |
| 数据库设计 | 未开始 | - | - | - | - |
| 核心功能开发 | 未开始 | - | - | - | - |
| 接口联调测试 | 未开始 | - | - | - | - |
| 部署上线 | 未开始 | - | - | - | - |
## 强制要求
1. **上下文加载**:每次都需要在上下文中加载 `project.md`
2. **意图确认**:每次接受到任务后,都需要仔细分析用户意图;如果有不理解或者认为意图模糊的时候,可以反问用户,确认好意图。
3. **Agent 协同**:执行任务要在当前合适的 Agent 配置中选取 Agent 调用;如果能同时协同多个 Agent,则协同调用多个 Agent 完成任务,提升任务效率。
4. **Git 本地提交**:每次修改都需要维护 git 仓库,提交代码修改记录到本地仓库。
5. **禁止擅自推送远程**:用户未主动要求,不得擅自提交到远程仓库。
## 核心原则
这是一个纯粹的后端项目,在遇到调用前端问题后,需要严格按照后端的 SDK 文档进行排查:
- 如果发现后端 SDK 有问题,则直接原因在后端需要进行修改。
- 如果排查前端调用不符合 SDK 文档规范,则直接给出结论,不强行修改后端进行适配。
+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;
+45
View File
@@ -0,0 +1,45 @@
@echo off
chcp 65001 >nul
title 权益小助手 - 一键启动脚本
echo ========================================
echo 权益小助手 - 一键启动脚本
echo ========================================
echo.
set MONGO_HOME=D:\001_software\012_MongoDB\mongodb-win32-x86_64-windows-8.0.16
set DATA_DIR=D:\003_Project\WeixinProject\QuanYiXiaoZhuShou\backend\mongodb\data
set LOG_DIR=D:\003_Project\WeixinProject\QuanYiXiaoZhuShou\backend\mongodb\log
set BACKEND_DIR=D:\003_Project\WeixinProject\QuanYiXiaoZhuShou\backend
if not exist "%MONGO_HOME%\bin\mongod.exe" (
echo [错误] 找不到 mongod.exe,请检查 MONGO_HOME 路径
pause
exit /b 1
)
if not exist "%DATA_DIR%" mkdir "%DATA_DIR%"
if not exist "%LOG_DIR%" mkdir "%LOG_DIR%"
echo [信息] 正在启动 MongoDB 服务...
start "MongoDB - 权益小助手" cmd /k "echo [MongoDB] 正在运行... && \"%MONGO_HOME%\bin\mongod.exe\" --dbpath \"%DATA_DIR%\" --port 27017 --bind_ip 127.0.0.1"
echo [信息] 等待 MongoDB 初始化...
timeout /t 3 /nobreak >nul
echo [信息] 正在启动后端服务...
cd /d "%BACKEND_DIR%"
start "后端服务 - 权益小助手" cmd /k "npm start"
echo.
echo ========================================
echo [成功] 所有服务已启动!
echo ========================================
echo.
echo 服务状态:
echo - MongoDB: 127.0.0.1:27017
echo - 后端API: http://localhost:3000
echo - 健康检查: http://localhost:3000/health
echo.
echo 按任意键关闭此窗口(服务将继续在后台运行)
pause >nul
+38
View File
@@ -0,0 +1,38 @@
@echo off
chcp 65001 >nul
title MongoDB 启动脚本 - 权益小助手
echo ========================================
echo 权益小助手 - MongoDB 启动脚本
echo ========================================
echo.
set MONGO_HOME=D:\001_software\012_MongoDB\mongodb-win32-x86_64-windows-8.0.16
set DATA_DIR=D:\003_Project\WeixinProject\QuanYiXiaoZhuShou\backend\mongodb\data
set LOG_DIR=D:\003_Project\WeixinProject\QuanYiXiaoZhuShou\backend\mongodb\log
if not exist "%MONGO_HOME%\bin\mongod.exe" (
echo [错误] 找不到 mongod.exe,请检查 MONGO_HOME 路径
echo 当前路径: %MONGO_HOME%
pause
exit /b 1
)
if not exist "%DATA_DIR%" mkdir "%DATA_DIR%"
if not exist "%LOG_DIR%" mkdir "%LOG_DIR%"
echo [信息] MongoDB 路径: %MONGO_HOME%
echo [信息] 数据目录: %DATA_DIR%
echo [信息] 日志目录: %LOG_DIR%
echo.
echo [信息] 正在启动 MongoDB 服务...
echo [信息] 按 Ctrl+C 可以停止服务
echo.
"%MONGO_HOME%\bin\mongod.exe" --dbpath "%DATA_DIR%" --port 27017 --bind_ip 127.0.0.1
echo.
echo [信息] MongoDB 服务已停止
echo.
pause