Initial backend code
This commit is contained in:
@@ -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
@@ -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元优惠券
|
||||
退货包运费
|
||||
@@ -0,0 +1,26 @@
|
||||
# 权益小助手后端服务
|
||||
|
||||
## 项目简介
|
||||
|
||||
本项目是「权益小助手」的后端服务,为前端应用提供 API 接口支持。当前处于项目初始化阶段,尚未开始正式开发。
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
backend/
|
||||
├── project.md # 项目信息、里程碑及开发规范
|
||||
└── README.md # 本文件
|
||||
```
|
||||
|
||||
## 当前状态
|
||||
|
||||
- 项目已创建基础文档
|
||||
- 等待后续技术选型与框架搭建
|
||||
|
||||
## 技术栈(待定)
|
||||
|
||||
待需求分析完成后确定。
|
||||
|
||||
## 开发规范
|
||||
|
||||
详见 [project.md](./project.md)。
|
||||
Generated
+6220
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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
@@ -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;
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user