feat: unified gravity system - VIP members consume gravity instead of unlimited; add monthly gravity top-up cron

This commit is contained in:
yuzhiran
2026-06-19 22:43:52 +08:00
parent c2ba810a02
commit 2fbab1072f
22 changed files with 956 additions and 216 deletions
+50 -6
View File
@@ -46,14 +46,13 @@ export class UserService {
let user = await this.userModel.findOne({ phone }).exec()
if (!user) {
user = await this.userModel.create({ phone, nickname: `用户${phone.slice(-4)}` })
user = await this.userModel.create({ phone, nickname: `用户${phone.slice(-4)}`, gravity: 5 })
}
return this.generateAuthResponse(user)
}
async loginByWx(code: string) {
// WeChat silent login - exchange code for openid
async loginByWx(code: string, userId?: string) {
const appid = process.env.WX_APPID
const secret = process.env.WX_SECRET
if (!appid || !secret) {
@@ -70,15 +69,59 @@ export class UserService {
}
const openid = wxData.openid
if (userId) {
const user = await this.userModel.findById(userId).exec()
if (!user) throw new HttpException('用户不存在', HttpStatus.NOT_FOUND)
if (user.wxOpenid) throw new HttpException('该账号已绑定微信', HttpStatus.CONFLICT)
user.wxOpenid = openid
await user.save()
return this.generateAuthResponse(user)
}
let user = await this.userModel.findOne({ wxOpenid: openid }).exec()
if (!user) {
user = await this.userModel.create({ wxOpenid: openid, nickname: '微信用户' })
user = await this.userModel.create({ wxOpenid: openid, nickname: '微信用户', gravity: 5 })
}
return this.generateAuthResponse(user)
}
// 📧 邮箱验证码
async bindWxOpenid(userId: string, code: string) {
this.logger.log(`[bindWx] userId=${userId}, code=${code ? '已提供' : '空'}`)
const appid = process.env.WX_APPID
const secret = process.env.WX_SECRET
if (!appid || !secret) {
this.logger.error(`[bindWx] 微信配置不完整`)
throw new HttpException('微信登录未配置', HttpStatus.SERVICE_UNAVAILABLE)
}
const wxRes = await fetch(
`https://api.weixin.qq.com/sns/jscode2session?appid=${appid}&secret=${secret}&js_code=${code}&grant_type=authorization_code`,
)
const wxData: any = await wxRes.json()
this.logger.log(`[bindWx] 微信接口返回: ${JSON.stringify(wxData)}`)
if (wxData.errcode) {
this.logger.error(`[bindWx] 微信登录失败: ${wxData.errmsg}, rid: ${wxData.rid || '无'}`)
throw new HttpException(`微信登录失败: ${wxData.errmsg}`, HttpStatus.UNAUTHORIZED)
}
const openid = wxData.openid
this.logger.log(`[bindWx] 获取到openid=${openid}`)
const existing = await this.userModel.findOne({ wxOpenid: openid }).exec()
if (existing) {
this.logger.warn(`[bindWx] openid=${openid} 已绑定到其他用户 ${existing._id}`)
throw new HttpException('该微信号已绑定其他账号', HttpStatus.CONFLICT)
}
const user = await this.userModel.findByIdAndUpdate(userId, { wxOpenid: openid }, { new: true }).exec()
if (!user) { this.logger.error(`[bindWx] 用户不存在 userId=${userId}`); throw new HttpException('用户不存在', HttpStatus.NOT_FOUND) }
this.logger.log(`[bindWx] openid=${openid} 绑定到用户 ${userId} 成功`)
return { message: '微信绑定成功', wxOpenid: openid }
}
async sendEmailCode(email: string) {
if (!email || !email.includes('@')) {
throw new HttpException('请输入正确的邮箱地址', HttpStatus.BAD_REQUEST)
@@ -113,7 +156,7 @@ export class UserService {
if (!user) {
isNew = true
const nick = email.split('@')[0]
user = await this.userModel.create({ email, nickname: nick, remaining: 3 })
user = await this.userModel.create({ email, nickname: nick, remaining: 0, gravity: 5 })
}
return { ...this.generateAuthResponse(user), isNew, hasPassword: !!user.password }
}
@@ -148,7 +191,7 @@ export class UserService {
}
const nick = email.split('@')[0]
const hashed = await bcrypt.hash(password, 10)
const user = await this.userModel.create({ email, nickname: nick, password: hashed, remaining: 3 })
const user = await this.userModel.create({ email, nickname: nick, password: hashed, remaining: 0, gravity: 5 })
return this.generateAuthResponse(user)
}
@@ -216,6 +259,7 @@ export class UserService {
resumeDownloadCredits: user.resumeDownloadCredits ?? 0,
freeOptimizeUsed: user.freeOptimizeUsed ?? 0,
shareCredits: user.shareCredits ?? 0,
gravity: user.gravity ?? 0,
}
}
}