feat: add auto-increment userId (8-digit padded) to User model and login response

This commit is contained in:
Developer
2026-05-13 20:41:58 +08:00
parent 67e7c251a6
commit 9f2f9ba7f6
2 changed files with 247 additions and 222 deletions
+98 -93
View File
@@ -1,93 +1,98 @@
const mongoose = require('mongoose'); const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({ const userSchema = new mongoose.Schema({
openid: { openid: {
type: String, type: String,
required: true, required: true,
unique: true, unique: true,
index: true index: true
}, },
unionid: { unionid: {
type: String, type: String,
sparse: true, sparse: true,
index: true index: true
}, },
nickname: { userId: {
type: String, type: String,
default: '' unique: true,
}, index: true
avatarUrl: { },
type: String, nickname: {
default: '' type: String,
}, default: ''
phoneNumber: { },
type: String, avatarUrl: {
default: '' type: String,
}, default: ''
profile: { },
gender: { phoneNumber: {
type: Number, type: String,
default: 0 default: ''
}, },
country: { profile: {
type: String, gender: {
default: '' type: Number,
}, default: 0
province: { },
type: String, country: {
default: '' type: String,
}, default: ''
city: { },
type: String, province: {
default: '' type: String,
} default: ''
}, },
status: { city: {
type: String, type: String,
enum: ['active', 'inactive', 'banned'], default: ''
default: 'active' }
}, },
lastLoginAt: { status: {
type: Date, type: String,
default: Date.now enum: ['active', 'inactive', 'banned'],
}, default: 'active'
isVip: { },
type: Boolean, lastLoginAt: {
default: false type: Date,
}, default: Date.now
vipExpireAt: { },
type: Date, isVip: {
default: null type: Boolean,
}, default: false
ocrCount: { },
type: Number, vipExpireAt: {
default: 10 type: Date,
}, default: null
ocrCountTotal: { },
type: Number, ocrCount: {
default: 10 type: Number,
}, default: 10
ocrCountResetAt: { },
type: Date, ocrCountTotal: {
default: Date.now type: Number,
}, default: 10
platformLimit: { },
type: Number, ocrCountResetAt: {
default: 15 type: Date,
}, default: Date.now
platformCount: { },
type: Number, platformLimit: {
default: 0 type: Number,
} default: 15
}, { },
timestamps: true platformCount: {
}); type: Number,
default: 0
userSchema.pre('save', function(next) { }
if (this.isModified('lastLoginAt')) { }, {
this.lastLoginAt = Date.now(); timestamps: true
} });
next();
}); userSchema.pre('save', function(next) {
if (this.isModified('lastLoginAt')) {
module.exports = mongoose.model('User', userSchema); this.lastLoginAt = Date.now();
}
next();
});
module.exports = mongoose.model('User', userSchema);
+149 -129
View File
@@ -1,129 +1,149 @@
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const axios = require('axios'); const axios = require('axios');
const jwt = require('jsonwebtoken'); const jwt = require('jsonwebtoken');
const User = require('../models/User'); const User = require('../models/User');
router.post('/wechat-login', async (req, res, next) => { async function generateUserId() {
try { const lastUser = await User.findOne({ userId: { $exists: true } })
const { code, userInfo } = req.body; .sort({ userId: -1 })
.select('userId')
if (!code) { .lean();
return res.status(400).json({
success: false, let nextNumber = 1;
error: '缺少微信登录code' if (lastUser && lastUser.userId) {
}); const lastNumber = parseInt(lastUser.userId, 10);
} if (!isNaN(lastNumber)) {
nextNumber = lastNumber + 1;
const appid = process.env.WECHAT_APPID; }
const secret = process.env.WECHAT_APPSECRET; }
const wxResponse = await axios.get('https://api.weixin.qq.com/sns/jscode2session', { return nextNumber.toString().padStart(8, '0');
params: { }
appid,
secret, router.post('/wechat-login', async (req, res, next) => {
js_code: code, try {
grant_type: 'authorization_code' const { code, userInfo } = req.body;
}
}); if (!code) {
return res.status(400).json({
const { openid, session_key, unionid, errcode, errmsg } = wxResponse.data; success: false,
error: '缺少微信登录code'
if (errcode) { });
return res.status(400).json({ }
success: false,
error: `微信登录失败: ${errmsg}`, const appid = process.env.WECHAT_APPID;
wxErrorCode: errcode const secret = process.env.WECHAT_APPSECRET;
});
} const wxResponse = await axios.get('https://api.weixin.qq.com/sns/jscode2session', {
params: {
let user = await User.findOne({ openid }); appid,
secret,
if (!user) { js_code: code,
user = await User.create({ grant_type: 'authorization_code'
openid, }
unionid: unionid || undefined, });
nickname: userInfo?.nickName || '',
avatarUrl: userInfo?.avatarUrl || '', const { openid, session_key, unionid, errcode, errmsg } = wxResponse.data;
profile: {
gender: userInfo?.gender || 0, if (errcode) {
country: userInfo?.country || '', return res.status(400).json({
province: userInfo?.province || '', success: false,
city: userInfo?.city || '' error: `微信登录失败: ${errmsg}`,
} wxErrorCode: errcode
}); });
} else { }
if (userInfo) {
user.nickname = userInfo.nickName || user.nickname; let user = await User.findOne({ openid });
user.avatarUrl = userInfo.avatarUrl || user.avatarUrl;
user.lastLoginAt = new Date(); if (!user) {
await user.save(); const userId = await generateUserId();
}
} user = await User.create({
openid,
const token = jwt.sign( unionid: unionid || undefined,
{ id: user._id, openid: user.openid }, userId,
process.env.JWT_SECRET, nickname: userInfo?.nickName || '',
{ expiresIn: process.env.JWT_EXPIRES_IN || '7d' } avatarUrl: userInfo?.avatarUrl || '',
); profile: {
gender: userInfo?.gender || 0,
res.json({ country: userInfo?.country || '',
success: true, province: userInfo?.province || '',
data: { city: userInfo?.city || ''
token, }
user: { });
id: user._id, } else {
nickname: user.nickname, if (userInfo) {
avatarUrl: user.avatarUrl, user.nickname = userInfo.nickName || user.nickname;
status: user.status, user.avatarUrl = userInfo.avatarUrl || user.avatarUrl;
isVip: user.isVip, user.lastLoginAt = new Date();
vipExpireAt: user.vipExpireAt, await user.save();
ocrCount: user.ocrCount, }
ocrCountTotal: user.ocrCountTotal, }
platformLimit: user.platformLimit,
platformCount: user.platformCount const token = jwt.sign(
} { id: user._id, openid: user.openid },
} process.env.JWT_SECRET,
}); { expiresIn: process.env.JWT_EXPIRES_IN || '7d' }
} catch (error) { );
next(error);
} res.json({
}); success: true,
data: {
router.post('/refresh-token', async (req, res, next) => { token,
try { user: {
const { token } = req.body; userId: user.userId,
nickname: user.nickname,
if (!token) { avatarUrl: user.avatarUrl,
return res.status(400).json({ status: user.status,
success: false, isVip: user.isVip,
error: '缺少token' vipExpireAt: user.vipExpireAt,
}); ocrCount: user.ocrCount,
} ocrCountTotal: user.ocrCountTotal,
platformLimit: user.platformLimit,
const decoded = jwt.verify(token, process.env.JWT_SECRET, { ignoreExpiration: true }); platformCount: user.platformCount
const user = await User.findById(decoded.id); }
}
if (!user || user.status !== 'active') { });
return res.status(401).json({ } catch (error) {
success: false, next(error);
error: '用户不存在或已被禁用' }
}); });
}
router.post('/refresh-token', async (req, res, next) => {
const newToken = jwt.sign( try {
{ id: user._id, openid: user.openid }, const { token } = req.body;
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRES_IN || '7d' } if (!token) {
); return res.status(400).json({
success: false,
res.json({ error: '缺少token'
success: true, });
data: { token: newToken } }
});
} catch (error) { const decoded = jwt.verify(token, process.env.JWT_SECRET, { ignoreExpiration: true });
next(error); const user = await User.findById(decoded.id);
}
}); if (!user || user.status !== 'active') {
return res.status(401).json({
module.exports = router; success: false,
error: '用户不存在或已被禁用'
});
}
const newToken = jwt.sign(
{ id: user._id, openid: user.openid },
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRES_IN || '7d' }
);
res.json({
success: true,
data: { token: newToken }
});
} catch (error) {
next(error);
}
});
module.exports = router;