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
|
||||
|
||||
# 百度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: {
|
||||
type: Date,
|
||||
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
|
||||
|
||||
@@ -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,
|
||||
nickname: user.nickname,
|
||||
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 { auth } = require('../middleware/auth');
|
||||
const User = require('../models/User');
|
||||
const { downloadAndSaveAvatar } = require('../services/avatarService');
|
||||
|
||||
router.get('/profile', auth, async (req, res, next) => {
|
||||
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(
|
||||
req.user._id,
|
||||
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