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