feat: sync backend with frontend - add all UserEquity fields (brandIcon, brandIconImage, brandColor, platformType, hasUsedBenefit, benefit sub-fields, etc.) and new routes
This commit is contained in:
@@ -15,3 +15,13 @@ JWT_EXPIRES_IN=7d
|
|||||||
|
|
||||||
# 日志配置
|
# 日志配置
|
||||||
LOG_LEVEL=info
|
LOG_LEVEL=info
|
||||||
|
|
||||||
|
# 百度OCR配置
|
||||||
|
BAIDU_OCR_API_KEY=IfYLOLzL6X60h5UOdnkX6OmT
|
||||||
|
BAIDU_OCR_SECRET_KEY=wGXbp6DwazDghJ1EXtjAT7XAFwJLqVD4
|
||||||
|
|
||||||
|
# 服务器地址
|
||||||
|
SERVER_URL=https://api.dxz99wyr.cn
|
||||||
|
|
||||||
|
# 数据导出加密密钥(建议设置一个复杂的密钥)
|
||||||
|
EXPORT_ENCRYPT_KEY=your_export_encrypt_key_here
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
const mongoose = require('mongoose');
|
||||||
|
|
||||||
|
const equityDetailSchema = new mongoose.Schema({
|
||||||
|
platform: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
trim: true,
|
||||||
|
unique: true
|
||||||
|
},
|
||||||
|
platformIcon: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
platformColor: {
|
||||||
|
type: String,
|
||||||
|
default: '#1890ff'
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
trim: true
|
||||||
|
},
|
||||||
|
typeLabel: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
summary: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
summaryGenerated: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
summaryApproved: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
detail: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
detailImages: [{
|
||||||
|
type: String
|
||||||
|
}],
|
||||||
|
benefits: [{
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
status: {
|
||||||
|
type: String,
|
||||||
|
enum: ['active', 'inactive'],
|
||||||
|
default: 'active'
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
timestamps: true
|
||||||
|
});
|
||||||
|
|
||||||
|
equityDetailSchema.index({ platform: 1 });
|
||||||
|
equityDetailSchema.index({ status: 1 });
|
||||||
|
|
||||||
|
module.exports = mongoose.model('EquityDetail', equityDetailSchema);
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
const mongoose = require('mongoose');
|
||||||
|
|
||||||
|
const benefitSchema = new mongoose.Schema({
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
trim: true
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: 'year'
|
||||||
|
},
|
||||||
|
typeLabel: {
|
||||||
|
type: String,
|
||||||
|
default: '年卡'
|
||||||
|
},
|
||||||
|
price: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
}, { _id: true });
|
||||||
|
|
||||||
|
const platformPresetSchema = new mongoose.Schema({
|
||||||
|
platform: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
trim: true
|
||||||
|
},
|
||||||
|
platformIcon: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
platformColor: {
|
||||||
|
type: String,
|
||||||
|
default: '#1890ff'
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
trim: true
|
||||||
|
},
|
||||||
|
typeLabel: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
benefits: [benefitSchema],
|
||||||
|
isHot: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
sortOrder: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: String,
|
||||||
|
enum: ['active', 'inactive'],
|
||||||
|
default: 'active'
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
timestamps: true
|
||||||
|
});
|
||||||
|
|
||||||
|
platformPresetSchema.index({ platform: 1 });
|
||||||
|
platformPresetSchema.index({ isHot: 1, sortOrder: 1 });
|
||||||
|
platformPresetSchema.index({ status: 1 });
|
||||||
|
|
||||||
|
module.exports = mongoose.model('PlatformPreset', platformPresetSchema);
|
||||||
@@ -50,6 +50,34 @@ const userSchema = new mongoose.Schema({
|
|||||||
lastLoginAt: {
|
lastLoginAt: {
|
||||||
type: Date,
|
type: Date,
|
||||||
default: Date.now
|
default: Date.now
|
||||||
|
},
|
||||||
|
isVip: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
vipExpireAt: {
|
||||||
|
type: Date,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
ocrCount: {
|
||||||
|
type: Number,
|
||||||
|
default: 10
|
||||||
|
},
|
||||||
|
ocrCountTotal: {
|
||||||
|
type: Number,
|
||||||
|
default: 10
|
||||||
|
},
|
||||||
|
ocrCountResetAt: {
|
||||||
|
type: Date,
|
||||||
|
default: Date.now
|
||||||
|
},
|
||||||
|
platformLimit: {
|
||||||
|
type: Number,
|
||||||
|
default: 15
|
||||||
|
},
|
||||||
|
platformCount: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
timestamps: true
|
timestamps: true
|
||||||
|
|||||||
@@ -0,0 +1,160 @@
|
|||||||
|
const mongoose = require('mongoose');
|
||||||
|
|
||||||
|
const benefitSchema = new mongoose.Schema({
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
trim: true
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
trim: true
|
||||||
|
},
|
||||||
|
typeLabel: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
expireDate: {
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
used: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
usedTime: {
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
times: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
usedTimes: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
usedAmount: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
totalAmount: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
rechargeAmount: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
price: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
currency: {
|
||||||
|
type: String,
|
||||||
|
default: '¥'
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
autoRenew: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
autoRenewCycle: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
renewReminderDismissed: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
}, { _id: true });
|
||||||
|
|
||||||
|
const userEquitySchema = new mongoose.Schema({
|
||||||
|
platform: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
trim: true
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
trim: true
|
||||||
|
},
|
||||||
|
platformType: {
|
||||||
|
type: String,
|
||||||
|
default: 'online'
|
||||||
|
},
|
||||||
|
expireDate: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
price: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
brandIcon: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
brandIconImage: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
brandColor: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
benefits: [benefitSchema],
|
||||||
|
owner: {
|
||||||
|
type: mongoose.Schema.Types.ObjectId,
|
||||||
|
ref: 'User',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: String,
|
||||||
|
enum: ['active', 'used', 'expired'],
|
||||||
|
default: 'active'
|
||||||
|
},
|
||||||
|
note: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
hasUsedBenefit: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
createTime: {
|
||||||
|
type: String,
|
||||||
|
default: () => new Date().toISOString()
|
||||||
|
},
|
||||||
|
updateTime: {
|
||||||
|
type: String,
|
||||||
|
default: () => new Date().toISOString()
|
||||||
|
},
|
||||||
|
syncedAt: {
|
||||||
|
type: String,
|
||||||
|
default: () => new Date().toISOString()
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
timestamps: false
|
||||||
|
});
|
||||||
|
|
||||||
|
userEquitySchema.index({ owner: 1, status: 1 });
|
||||||
|
userEquitySchema.index({ platform: 1 });
|
||||||
|
userEquitySchema.index({ expireDate: 1 });
|
||||||
|
|
||||||
|
userEquitySchema.pre('save', function(next) {
|
||||||
|
this.updateTime = new Date().toISOString();
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
userEquitySchema.pre('findOneAndUpdate', function(next) {
|
||||||
|
this.set({ updateTime: new Date().toISOString() });
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = mongoose.model('UserEquity', userEquitySchema);
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
const mongoose = require('mongoose');
|
||||||
|
|
||||||
|
const userSubscriptionSchema = new mongoose.Schema({
|
||||||
|
userId: {
|
||||||
|
type: mongoose.Schema.Types.ObjectId,
|
||||||
|
ref: 'User',
|
||||||
|
required: true,
|
||||||
|
index: true
|
||||||
|
},
|
||||||
|
openid: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
index: true
|
||||||
|
},
|
||||||
|
templateId: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
index: true
|
||||||
|
},
|
||||||
|
scene: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: String,
|
||||||
|
enum: ['active', 'expired', 'used'],
|
||||||
|
default: 'active'
|
||||||
|
},
|
||||||
|
subscribedAt: {
|
||||||
|
type: Date,
|
||||||
|
default: Date.now
|
||||||
|
},
|
||||||
|
expiredAt: {
|
||||||
|
type: Date,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
usedAt: {
|
||||||
|
type: Date,
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
timestamps: true
|
||||||
|
});
|
||||||
|
|
||||||
|
userSubscriptionSchema.index({ userId: 1, templateId: 1 });
|
||||||
|
|
||||||
|
module.exports = mongoose.model('UserSubscription', userSubscriptionSchema);
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
const mongoose = require('mongoose');
|
||||||
|
|
||||||
|
const versionLogSchema = new mongoose.Schema({
|
||||||
|
version: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
trim: true
|
||||||
|
},
|
||||||
|
versionName: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
releaseDate: {
|
||||||
|
type: Date,
|
||||||
|
default: Date.now
|
||||||
|
},
|
||||||
|
features: [{
|
||||||
|
type: String
|
||||||
|
}],
|
||||||
|
fixes: [{
|
||||||
|
type: String
|
||||||
|
}],
|
||||||
|
improvements: [{
|
||||||
|
type: String
|
||||||
|
}],
|
||||||
|
isPublished: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
sortOrder: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
timestamps: true
|
||||||
|
});
|
||||||
|
|
||||||
|
versionLogSchema.index({ version: 1 });
|
||||||
|
versionLogSchema.index({ isPublished: 1, sortOrder: -1 });
|
||||||
|
|
||||||
|
module.exports = mongoose.model('VersionLog', versionLogSchema);
|
||||||
+7
-1
@@ -75,7 +75,13 @@ router.post('/wechat-login', async (req, res, next) => {
|
|||||||
id: user._id,
|
id: user._id,
|
||||||
nickname: user.nickname,
|
nickname: user.nickname,
|
||||||
avatarUrl: user.avatarUrl,
|
avatarUrl: user.avatarUrl,
|
||||||
status: user.status
|
status: user.status,
|
||||||
|
isVip: user.isVip,
|
||||||
|
vipExpireAt: user.vipExpireAt,
|
||||||
|
ocrCount: user.ocrCount,
|
||||||
|
ocrCountTotal: user.ocrCountTotal,
|
||||||
|
platformLimit: user.platformLimit,
|
||||||
|
platformCount: user.platformCount
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const EquityDetail = require('../models/EquityDetail');
|
||||||
|
|
||||||
|
router.get('/', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { platform } = req.query;
|
||||||
|
const query = { status: 'active' };
|
||||||
|
|
||||||
|
if (platform) {
|
||||||
|
query.platform = platform;
|
||||||
|
}
|
||||||
|
|
||||||
|
const details = await EquityDetail.find(query)
|
||||||
|
.sort({ platform: 1 })
|
||||||
|
.lean();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: details
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/:platform', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { platform } = req.params;
|
||||||
|
|
||||||
|
const detail = await EquityDetail.findOne({
|
||||||
|
platform,
|
||||||
|
status: 'active'
|
||||||
|
}).lean();
|
||||||
|
|
||||||
|
if (!detail) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
error: '权益详情不存在'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: detail
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const { auth } = require('../middleware/auth');
|
||||||
|
const User = require('../models/User');
|
||||||
|
const UserEquity = require('../models/UserEquity');
|
||||||
|
|
||||||
|
router.get('/status', auth, async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const user = await User.findById(req.user._id);
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const resetDate = new Date(user.ocrCountResetAt);
|
||||||
|
const isNewMonth = now.getFullYear() !== resetDate.getFullYear() ||
|
||||||
|
now.getMonth() !== resetDate.getMonth();
|
||||||
|
|
||||||
|
if (isNewMonth) {
|
||||||
|
user.ocrCount = user.ocrCountTotal;
|
||||||
|
user.ocrCountResetAt = now;
|
||||||
|
await user.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
const platformCount = await UserEquity.countDocuments({ owner: req.user._id });
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
isVip: user.isVip,
|
||||||
|
vipExpireAt: user.vipExpireAt,
|
||||||
|
ocrCount: user.ocrCount,
|
||||||
|
ocrCountTotal: user.ocrCountTotal,
|
||||||
|
ocrCountResetAt: user.ocrCountResetAt,
|
||||||
|
platformLimit: user.platformLimit,
|
||||||
|
platformCount: platformCount,
|
||||||
|
remainingPlatforms: user.isVip ? -1 : Math.max(0, user.platformLimit - platformCount)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/upgrade', auth, async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { duration = 1 } = req.body;
|
||||||
|
|
||||||
|
const user = await User.findById(req.user._id);
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
let currentExpireAt = user.vipExpireAt ? new Date(user.vipExpireAt) : now;
|
||||||
|
|
||||||
|
if (currentExpireAt < now) {
|
||||||
|
currentExpireAt = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentExpireAt.setMonth(currentExpireAt.getMonth() + duration);
|
||||||
|
|
||||||
|
user.isVip = true;
|
||||||
|
user.vipExpireAt = currentExpireAt;
|
||||||
|
user.ocrCountTotal = 100;
|
||||||
|
user.ocrCount = 100;
|
||||||
|
user.platformLimit = -1;
|
||||||
|
await user.save();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
isVip: user.isVip,
|
||||||
|
vipExpireAt: user.vipExpireAt,
|
||||||
|
ocrCount: user.ocrCount,
|
||||||
|
ocrCountTotal: user.ocrCountTotal,
|
||||||
|
platformLimit: user.platformLimit
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const { auth } = require('../middleware/auth');
|
||||||
|
const User = require('../models/User');
|
||||||
|
const { recognizeText, extractMembershipInfo } = require('../services/ocrService');
|
||||||
|
|
||||||
|
router.post('/recognize', auth, async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { image } = req.body;
|
||||||
|
|
||||||
|
if (!image) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: '缺少图片数据'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await User.findById(req.user._id);
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const resetDate = new Date(user.ocrCountResetAt);
|
||||||
|
const isNewMonth = now.getFullYear() !== resetDate.getFullYear() ||
|
||||||
|
now.getMonth() !== resetDate.getMonth();
|
||||||
|
|
||||||
|
if (isNewMonth) {
|
||||||
|
user.ocrCount = user.ocrCountTotal;
|
||||||
|
user.ocrCountResetAt = now;
|
||||||
|
await user.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.ocrCount <= 0) {
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
error: '本月OCR次数已用完,请升级会员获取更多次数',
|
||||||
|
data: {
|
||||||
|
ocrCount: 0,
|
||||||
|
ocrCountTotal: user.ocrCountTotal,
|
||||||
|
isVip: user.isVip
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageBase64 = image.replace(/^data:image\/\w+;base64,/, '');
|
||||||
|
|
||||||
|
const ocrResult = await recognizeText(imageBase64, {
|
||||||
|
language_type: 'CHN_ENG'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (ocrResult.error_code) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: `OCR识别失败: ${ocrResult.error_msg}`,
|
||||||
|
data: ocrResult
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const extractedInfo = extractMembershipInfo(ocrResult);
|
||||||
|
|
||||||
|
user.ocrCount -= 1;
|
||||||
|
await user.save();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: extractedInfo,
|
||||||
|
meta: {
|
||||||
|
ocrCount: user.ocrCount,
|
||||||
|
ocrCountTotal: user.ocrCountTotal,
|
||||||
|
isVip: user.isVip
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/quota', auth, async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const user = await User.findById(req.user._id);
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const resetDate = new Date(user.ocrCountResetAt);
|
||||||
|
const isNewMonth = now.getFullYear() !== resetDate.getFullYear() ||
|
||||||
|
now.getMonth() !== resetDate.getMonth();
|
||||||
|
|
||||||
|
if (isNewMonth) {
|
||||||
|
user.ocrCount = user.ocrCountTotal;
|
||||||
|
user.ocrCountResetAt = now;
|
||||||
|
await user.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
ocrCount: user.ocrCount,
|
||||||
|
ocrCountTotal: user.ocrCountTotal,
|
||||||
|
isVip: user.isVip,
|
||||||
|
resetAt: user.ocrCountResetAt
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const { auth } = require('../middleware/auth');
|
||||||
|
const PlatformPreset = require('../models/PlatformPreset');
|
||||||
|
const UserEquity = require('../models/UserEquity');
|
||||||
|
const User = require('../models/User');
|
||||||
|
|
||||||
|
router.get('/', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const presets = await PlatformPreset.find({ status: 'active' })
|
||||||
|
.sort({ isHot: -1, sortOrder: 1 })
|
||||||
|
.lean();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: presets
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/hot', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const presets = await PlatformPreset.find({ status: 'active', isHot: true })
|
||||||
|
.sort({ sortOrder: 1 })
|
||||||
|
.lean();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: presets
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/import/:id', auth, async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { expireDate, price } = req.body;
|
||||||
|
|
||||||
|
const preset = await PlatformPreset.findById(id);
|
||||||
|
|
||||||
|
if (!preset) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
error: '预设平台不存在'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await User.findById(req.user._id);
|
||||||
|
|
||||||
|
if (!user.isVip) {
|
||||||
|
const userEquityCount = await UserEquity.countDocuments({ owner: req.user._id });
|
||||||
|
if (userEquityCount >= user.platformLimit) {
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
error: '非会员最多只能添加15个平台,请升级会员'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!expireDate) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: '缺少到期时间'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const equityData = {
|
||||||
|
platform: preset.platform,
|
||||||
|
type: preset.type,
|
||||||
|
expireDate,
|
||||||
|
price: parseFloat(price) || preset.price || 0,
|
||||||
|
benefits: preset.benefits.map(b => ({
|
||||||
|
name: b.name,
|
||||||
|
type: b.type,
|
||||||
|
typeLabel: b.typeLabel,
|
||||||
|
expireDate,
|
||||||
|
used: false,
|
||||||
|
usedTime: null
|
||||||
|
})),
|
||||||
|
owner: req.user._id,
|
||||||
|
status: 'active',
|
||||||
|
note: preset.description || ''
|
||||||
|
};
|
||||||
|
|
||||||
|
const equity = await UserEquity.create(equityData);
|
||||||
|
|
||||||
|
user.platformCount = await UserEquity.countDocuments({ owner: req.user._id });
|
||||||
|
await user.save();
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
success: true,
|
||||||
|
data: equity
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const { auth } = require('../middleware/auth');
|
||||||
|
const VersionLog = require('../models/VersionLog');
|
||||||
|
const crypto = require('crypto');
|
||||||
|
|
||||||
|
const ALGORITHM = 'aes-256-gcm';
|
||||||
|
const KEY_LENGTH = 32;
|
||||||
|
const IV_LENGTH = 16;
|
||||||
|
const SALT_LENGTH = 64;
|
||||||
|
const TAG_LENGTH = 16;
|
||||||
|
|
||||||
|
function getEncryptionKey() {
|
||||||
|
const envKey = process.env.EXPORT_ENCRYPT_KEY;
|
||||||
|
if (envKey) {
|
||||||
|
return crypto.scryptSync(envKey, 'salt', KEY_LENGTH);
|
||||||
|
}
|
||||||
|
return crypto.randomBytes(KEY_LENGTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
function encryptData(data) {
|
||||||
|
const key = getEncryptionKey();
|
||||||
|
const iv = crypto.randomBytes(IV_LENGTH);
|
||||||
|
const salt = crypto.randomBytes(SALT_LENGTH);
|
||||||
|
|
||||||
|
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
||||||
|
|
||||||
|
const jsonData = JSON.stringify(data);
|
||||||
|
let encrypted = cipher.update(jsonData, 'utf8', 'hex');
|
||||||
|
encrypted += cipher.final('hex');
|
||||||
|
|
||||||
|
const tag = cipher.getAuthTag();
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
salt: salt.toString('hex'),
|
||||||
|
iv: iv.toString('hex'),
|
||||||
|
tag: tag.toString('hex'),
|
||||||
|
data: encrypted
|
||||||
|
};
|
||||||
|
|
||||||
|
return Buffer.from(JSON.stringify(result)).toString('base64');
|
||||||
|
}
|
||||||
|
|
||||||
|
function decryptData(encryptedBase64) {
|
||||||
|
try {
|
||||||
|
const encryptedJson = Buffer.from(encryptedBase64, 'base64').toString('utf8');
|
||||||
|
const encrypted = JSON.parse(encryptedJson);
|
||||||
|
|
||||||
|
const key = getEncryptionKey();
|
||||||
|
const iv = Buffer.from(encrypted.iv, 'hex');
|
||||||
|
const tag = Buffer.from(encrypted.tag, 'hex');
|
||||||
|
|
||||||
|
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
|
||||||
|
decipher.setAuthTag(tag);
|
||||||
|
|
||||||
|
let decrypted = decipher.update(encrypted.data, 'hex', 'utf8');
|
||||||
|
decrypted += decipher.final('utf8');
|
||||||
|
|
||||||
|
return JSON.parse(decrypted);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error('数据解密失败,文件可能已损坏或被篡改');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
router.get('/versions', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const versions = await VersionLog.find({ isPublished: true })
|
||||||
|
.sort({ sortOrder: -1, releaseDate: -1 })
|
||||||
|
.lean();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: versions
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/versions/:version', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { version } = req.params;
|
||||||
|
|
||||||
|
const versionLog = await VersionLog.findOne({ version }).lean();
|
||||||
|
|
||||||
|
if (!versionLog) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
error: '版本记录不存在'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: versionLog
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/export', auth, async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const UserEquity = require('../models/UserEquity');
|
||||||
|
|
||||||
|
const equities = await UserEquity.find({ owner: req.user._id }).lean();
|
||||||
|
|
||||||
|
const exportData = {
|
||||||
|
exportAt: new Date().toISOString(),
|
||||||
|
userId: req.user._id.toString(),
|
||||||
|
version: '1.0',
|
||||||
|
equities: equities
|
||||||
|
};
|
||||||
|
|
||||||
|
const encrypted = encryptData(exportData);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
encryptedData: encrypted,
|
||||||
|
filename: `quanyi_backup_${new Date().getTime()}.json`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/import', auth, async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { encryptedData } = req.body;
|
||||||
|
|
||||||
|
if (!encryptedData) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: '缺少加密数据'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const decrypted = decryptData(encryptedData);
|
||||||
|
|
||||||
|
if (!decrypted.equities || !Array.isArray(decrypted.equities)) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: '数据格式错误'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserEquity = require('../models/UserEquity');
|
||||||
|
const User = require('../models/User');
|
||||||
|
const user = await User.findById(req.user._id);
|
||||||
|
|
||||||
|
const currentCount = await UserEquity.countDocuments({ owner: req.user._id });
|
||||||
|
const importCount = decrypted.equities.length;
|
||||||
|
|
||||||
|
if (!user.isVip && (currentCount + importCount) > user.platformLimit) {
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
error: `导入后平台数量将超过限制(${user.platformLimit}个),请升级会员`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = [];
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
for (const item of decrypted.equities) {
|
||||||
|
try {
|
||||||
|
const equityData = {
|
||||||
|
platform: item.platform,
|
||||||
|
type: item.type,
|
||||||
|
expireDate: item.expireDate,
|
||||||
|
price: item.price || 0,
|
||||||
|
benefits: item.benefits || [],
|
||||||
|
owner: req.user._id,
|
||||||
|
status: item.status || 'active',
|
||||||
|
note: item.note || '',
|
||||||
|
createTime: item.createTime || new Date().toISOString(),
|
||||||
|
updateTime: new Date().toISOString(),
|
||||||
|
syncedAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
const equity = await UserEquity.create(equityData);
|
||||||
|
results.push(equity);
|
||||||
|
} catch (itemError) {
|
||||||
|
errors.push({ item, error: itemError.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
user.platformCount = await UserEquity.countDocuments({ owner: req.user._id });
|
||||||
|
await user.save();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
imported: results.length,
|
||||||
|
failed: errors.length,
|
||||||
|
items: results,
|
||||||
|
errors: errors.length > 0 ? errors : undefined
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const UserSubscription = require('../models/UserSubscription');
|
||||||
|
const WechatSubscribeService = require('../services/wechatSubscribeService');
|
||||||
|
const { auth } = require('../middleware/auth');
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.post('/save', auth, async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { templateId, scene } = req.body;
|
||||||
|
|
||||||
|
if (!templateId) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'templateId 不能为空'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = req.user;
|
||||||
|
|
||||||
|
const existingSubscription = await UserSubscription.findOne({
|
||||||
|
userId: user._id,
|
||||||
|
templateId,
|
||||||
|
status: 'active'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingSubscription) {
|
||||||
|
return res.json({ success: true, message: '已存在有效的订阅' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const subscription = new UserSubscription({
|
||||||
|
userId: user._id,
|
||||||
|
openid: user.openid,
|
||||||
|
templateId,
|
||||||
|
scene: scene || ''
|
||||||
|
});
|
||||||
|
|
||||||
|
await subscription.save();
|
||||||
|
|
||||||
|
res.json({ success: true, message: '订阅保存成功' });
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/version-update', auth, async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { templateId, character_string1, thing3, thing6 } = req.body;
|
||||||
|
|
||||||
|
if (!templateId) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'templateId 不能为空'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!character_string1) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'character_string1 不能为空'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!thing3) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: 'thing3 不能为空'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = req.user;
|
||||||
|
|
||||||
|
const subscription = await UserSubscription.findOne({
|
||||||
|
userId: user._id,
|
||||||
|
templateId,
|
||||||
|
status: 'active'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!subscription) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: '未找到有效的订阅,用户可能未订阅该模板'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await WechatSubscribeService.sendVersionUpdateMessage({
|
||||||
|
openid: user.openid,
|
||||||
|
templateId,
|
||||||
|
character_string1,
|
||||||
|
thing3,
|
||||||
|
thing6
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
subscription.status = 'used';
|
||||||
|
subscription.usedAt = new Date();
|
||||||
|
await subscription.save();
|
||||||
|
|
||||||
|
res.json({ success: true, messageId: result.messageId, message: '消息发送成功' });
|
||||||
|
} else {
|
||||||
|
if (result.errcode === 43101 || result.errcode === 40037) {
|
||||||
|
subscription.status = 'expired';
|
||||||
|
subscription.expiredAt = new Date();
|
||||||
|
await subscription.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: result.errmsg || '消息发送失败',
|
||||||
|
errcode: result.errcode
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@@ -2,6 +2,7 @@ const express = require('express');
|
|||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const { auth } = require('../middleware/auth');
|
const { auth } = require('../middleware/auth');
|
||||||
const User = require('../models/User');
|
const User = require('../models/User');
|
||||||
|
const { downloadAndSaveAvatar } = require('../services/avatarService');
|
||||||
|
|
||||||
router.get('/profile', auth, async (req, res, next) => {
|
router.get('/profile', auth, async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
@@ -27,6 +28,13 @@ router.put('/profile', auth, async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (updates.avatarUrl && updates.avatarUrl.startsWith('http')) {
|
||||||
|
const savedAvatarUrl = await downloadAndSaveAvatar(updates.avatarUrl);
|
||||||
|
if (savedAvatarUrl) {
|
||||||
|
updates.avatarUrl = savedAvatarUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const user = await User.findByIdAndUpdate(
|
const user = await User.findByIdAndUpdate(
|
||||||
req.user._id,
|
req.user._id,
|
||||||
updates,
|
updates,
|
||||||
|
|||||||
@@ -0,0 +1,417 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const { auth } = require('../middleware/auth');
|
||||||
|
const UserEquity = require('../models/UserEquity');
|
||||||
|
|
||||||
|
router.get('/', auth, async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
page = 1,
|
||||||
|
limit = 100,
|
||||||
|
status,
|
||||||
|
platform,
|
||||||
|
sortBy = 'createTime',
|
||||||
|
order = 'desc'
|
||||||
|
} = req.query;
|
||||||
|
|
||||||
|
const query = { owner: req.user._id };
|
||||||
|
|
||||||
|
if (status) query.status = status;
|
||||||
|
if (platform) query.platform = platform;
|
||||||
|
|
||||||
|
const sortOrder = order === 'asc' ? 1 : -1;
|
||||||
|
const skip = (parseInt(page) - 1) * parseInt(limit);
|
||||||
|
|
||||||
|
const [equities, total] = await Promise.all([
|
||||||
|
UserEquity.find(query)
|
||||||
|
.sort({ [sortBy]: sortOrder })
|
||||||
|
.skip(skip)
|
||||||
|
.limit(parseInt(limit))
|
||||||
|
.lean(),
|
||||||
|
UserEquity.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('/all', auth, async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { since } = req.query;
|
||||||
|
const query = { owner: req.user._id };
|
||||||
|
|
||||||
|
if (since) {
|
||||||
|
query.updateTime = { $gt: since };
|
||||||
|
}
|
||||||
|
|
||||||
|
const equities = await UserEquity.find(query)
|
||||||
|
.sort({ updateTime: -1 })
|
||||||
|
.lean();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
list: equities,
|
||||||
|
count: equities.length
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/list', auth, async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const equities = await UserEquity.find({ owner: req.user._id })
|
||||||
|
.sort({ createTime: -1 })
|
||||||
|
.lean();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
list: equities,
|
||||||
|
count: equities.length
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/:id', auth, async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const equity = await UserEquity.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 {
|
||||||
|
platform,
|
||||||
|
type,
|
||||||
|
expireDate,
|
||||||
|
price,
|
||||||
|
benefits,
|
||||||
|
status,
|
||||||
|
note,
|
||||||
|
createTime,
|
||||||
|
updateTime,
|
||||||
|
platformType,
|
||||||
|
brandIcon,
|
||||||
|
brandIconImage,
|
||||||
|
brandColor,
|
||||||
|
hasUsedBenefit
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
if (!platform || !type || !expireDate) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: '缺少必填字段:平台名称、会员类型、到期时间'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const equityData = {
|
||||||
|
platform: platform.trim(),
|
||||||
|
type: type.trim(),
|
||||||
|
expireDate,
|
||||||
|
price: parseFloat(price) || 0,
|
||||||
|
benefits: benefits || [],
|
||||||
|
owner: req.user._id,
|
||||||
|
status: status || 'active',
|
||||||
|
note: note || '',
|
||||||
|
syncedAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
if (platformType !== undefined) equityData.platformType = platformType;
|
||||||
|
if (brandIcon !== undefined) equityData.brandIcon = brandIcon;
|
||||||
|
if (brandIconImage !== undefined) equityData.brandIconImage = brandIconImage;
|
||||||
|
if (brandColor !== undefined) equityData.brandColor = brandColor;
|
||||||
|
if (hasUsedBenefit !== undefined) equityData.hasUsedBenefit = hasUsedBenefit;
|
||||||
|
if (createTime) equityData.createTime = createTime;
|
||||||
|
if (updateTime) equityData.updateTime = updateTime;
|
||||||
|
|
||||||
|
const equity = await UserEquity.create(equityData);
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
success: true,
|
||||||
|
data: equity
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/batch', auth, async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { items } = req.body;
|
||||||
|
|
||||||
|
if (!Array.isArray(items) || items.length === 0) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: '缺少数据或格式错误'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = [];
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
platform,
|
||||||
|
type,
|
||||||
|
expireDate,
|
||||||
|
price,
|
||||||
|
benefits,
|
||||||
|
status,
|
||||||
|
note,
|
||||||
|
createTime,
|
||||||
|
updateTime,
|
||||||
|
platformType,
|
||||||
|
brandIcon,
|
||||||
|
brandIconImage,
|
||||||
|
brandColor,
|
||||||
|
hasUsedBenefit
|
||||||
|
} = item;
|
||||||
|
|
||||||
|
if (!platform || !type || !expireDate) {
|
||||||
|
errors.push({ item, error: '缺少必填字段' });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const equityData = {
|
||||||
|
platform: platform.trim(),
|
||||||
|
type: type.trim(),
|
||||||
|
expireDate,
|
||||||
|
price: parseFloat(price) || 0,
|
||||||
|
benefits: benefits || [],
|
||||||
|
owner: req.user._id,
|
||||||
|
status: status || 'active',
|
||||||
|
note: note || '',
|
||||||
|
syncedAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
if (platformType !== undefined) equityData.platformType = platformType;
|
||||||
|
if (brandIcon !== undefined) equityData.brandIcon = brandIcon;
|
||||||
|
if (brandIconImage !== undefined) equityData.brandIconImage = brandIconImage;
|
||||||
|
if (brandColor !== undefined) equityData.brandColor = brandColor;
|
||||||
|
if (hasUsedBenefit !== undefined) equityData.hasUsedBenefit = hasUsedBenefit;
|
||||||
|
if (createTime) equityData.createTime = createTime;
|
||||||
|
if (updateTime) equityData.updateTime = updateTime;
|
||||||
|
|
||||||
|
const equity = await UserEquity.create(equityData);
|
||||||
|
results.push(equity);
|
||||||
|
} catch (itemError) {
|
||||||
|
errors.push({ item, error: itemError.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
created: results.length,
|
||||||
|
failed: errors.length,
|
||||||
|
items: results,
|
||||||
|
errors: errors.length > 0 ? errors : undefined
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.put('/:id', auth, async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
platform,
|
||||||
|
type,
|
||||||
|
expireDate,
|
||||||
|
price,
|
||||||
|
benefits,
|
||||||
|
status,
|
||||||
|
note,
|
||||||
|
createTime,
|
||||||
|
updateTime,
|
||||||
|
platformType,
|
||||||
|
brandIcon,
|
||||||
|
brandIconImage,
|
||||||
|
brandColor,
|
||||||
|
hasUsedBenefit
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
const updates = {};
|
||||||
|
|
||||||
|
if (platform !== undefined) updates.platform = platform.trim();
|
||||||
|
if (type !== undefined) updates.type = type.trim();
|
||||||
|
if (expireDate !== undefined) updates.expireDate = expireDate;
|
||||||
|
if (price !== undefined) updates.price = parseFloat(price) || 0;
|
||||||
|
if (benefits !== undefined) updates.benefits = benefits;
|
||||||
|
if (status !== undefined) updates.status = status;
|
||||||
|
if (note !== undefined) updates.note = note;
|
||||||
|
if (platformType !== undefined) updates.platformType = platformType;
|
||||||
|
if (brandIcon !== undefined) updates.brandIcon = brandIcon;
|
||||||
|
if (brandIconImage !== undefined) updates.brandIconImage = brandIconImage;
|
||||||
|
if (brandColor !== undefined) updates.brandColor = brandColor;
|
||||||
|
if (hasUsedBenefit !== undefined) updates.hasUsedBenefit = hasUsedBenefit;
|
||||||
|
if (createTime !== undefined) updates.createTime = createTime;
|
||||||
|
if (updateTime !== undefined) updates.updateTime = updateTime;
|
||||||
|
|
||||||
|
updates.syncedAt = new Date().toISOString();
|
||||||
|
|
||||||
|
const equity = await UserEquity.findOneAndUpdate(
|
||||||
|
{ _id: req.params.id, owner: req.user._id },
|
||||||
|
updates,
|
||||||
|
{ 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.put('/:id/benefits', auth, async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { benefit, action } = req.body;
|
||||||
|
|
||||||
|
const equity = await UserEquity.findOne({
|
||||||
|
_id: req.params.id,
|
||||||
|
owner: req.user._id
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!equity) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
error: '权益不存在或无权限修改'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const benefits = equity.benefits || [];
|
||||||
|
|
||||||
|
if (action === 'use') {
|
||||||
|
const benefitIndex = benefits.findIndex(b => b.name === benefit.benefitName);
|
||||||
|
if (benefitIndex === -1) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
error: '权益项不存在'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
benefits[benefitIndex].used = true;
|
||||||
|
benefits[benefitIndex].usedTime = new Date().toISOString();
|
||||||
|
} else if (action === 'delete') {
|
||||||
|
const benefitIndex = benefits.findIndex(b => b.name === benefit.benefitName);
|
||||||
|
if (benefitIndex === -1) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
error: '权益项不存在'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
benefits.splice(benefitIndex, 1);
|
||||||
|
} else if (action === 'update') {
|
||||||
|
const benefitIndex = benefits.findIndex(b => b.name === benefit.oldBenefitName);
|
||||||
|
if (benefitIndex === -1) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
error: '权益项不存在'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
benefits[benefitIndex] = {
|
||||||
|
...benefits[benefitIndex].toObject(),
|
||||||
|
...benefit.newBenefitData,
|
||||||
|
updateTime: new Date().toISOString()
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
if (!benefit || !benefit.name || !benefit.type) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
error: '缺少必填字段:权益名称、权益类型'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
benefits.push({
|
||||||
|
name: benefit.name.trim(),
|
||||||
|
type: benefit.type,
|
||||||
|
typeLabel: benefit.typeLabel || '',
|
||||||
|
expireDate: benefit.expireDate || null,
|
||||||
|
used: false,
|
||||||
|
usedTime: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
equity.benefits = benefits;
|
||||||
|
equity.updateTime = new Date().toISOString();
|
||||||
|
equity.syncedAt = new Date().toISOString();
|
||||||
|
await equity.save();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: equity
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.delete('/:id', auth, async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const equity = await UserEquity.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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
const axios = require('axios');
|
||||||
|
|
||||||
|
let accessTokenCache = {
|
||||||
|
token: null,
|
||||||
|
expiresAt: null
|
||||||
|
};
|
||||||
|
|
||||||
|
class WechatSubscribeService {
|
||||||
|
static async getAccessToken() {
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
if (accessTokenCache.token && accessTokenCache.expiresAt && now < accessTokenCache.expiresAt) {
|
||||||
|
return accessTokenCache.token;
|
||||||
|
}
|
||||||
|
|
||||||
|
const appId = process.env.WECHAT_APPID;
|
||||||
|
const appSecret = process.env.WECHAT_APPSECRET;
|
||||||
|
|
||||||
|
const url = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appId}&secret=${appSecret}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get(url);
|
||||||
|
const { access_token, expires_in } = response.data;
|
||||||
|
|
||||||
|
accessTokenCache = {
|
||||||
|
token: access_token,
|
||||||
|
expiresAt: now + (expires_in - 300) * 1000
|
||||||
|
};
|
||||||
|
|
||||||
|
return access_token;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取微信 access_token 失败:', error.response?.data || error.message);
|
||||||
|
throw new Error('获取微信 access_token 失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async sendSubscribeMessage({ touser, templateId, page, data }) {
|
||||||
|
const accessToken = await this.getAccessToken();
|
||||||
|
const url = `https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=${accessToken}`;
|
||||||
|
|
||||||
|
const requestData = {
|
||||||
|
touser,
|
||||||
|
template_id: templateId,
|
||||||
|
page: page || 'pages/index/index',
|
||||||
|
data
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post(url, requestData);
|
||||||
|
|
||||||
|
if (response.data.errcode === 0) {
|
||||||
|
return { success: true, messageId: response.data.msgid };
|
||||||
|
} else {
|
||||||
|
console.error('发送订阅消息失败:', response.data);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
errcode: response.data.errcode,
|
||||||
|
errmsg: response.data.errmsg
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('发送订阅消息异常:', error.response?.data || error.message);
|
||||||
|
return { success: false, errmsg: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async sendVersionUpdateMessage({ openid, templateId, character_string1, thing3, thing6 }) {
|
||||||
|
const data = {
|
||||||
|
character_string1: { value: character_string1 },
|
||||||
|
thing3: { value: thing3 }
|
||||||
|
};
|
||||||
|
|
||||||
|
if (thing6) {
|
||||||
|
data.thing6 = { value: thing6 };
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.sendSubscribeMessage({
|
||||||
|
touser: openid,
|
||||||
|
templateId,
|
||||||
|
page: 'pages/index/index',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = WechatSubscribeService;
|
||||||
Reference in New Issue
Block a user