feat: registration gravity from DB config instead of hardcoded
- Add registrationGravity to pricing config (default 50, adjustable via admin) - Inject PricingService into UserService, read config for all 4 registration paths - Expose registrationGravity in /member/plans response for frontend - Login page fetches dynamic value from API, replaces hardcoded '50' - Update user.vue gravity card copy
This commit is contained in:
@@ -53,6 +53,7 @@ export class MemberController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
registrationGravity: pricing.registrationGravity,
|
||||||
interview: { dailyFreeLimit: FREE_DAILY_LIMIT, maxRoundsFree: 5, maxRoundsVip: 10 },
|
interview: { dailyFreeLimit: FREE_DAILY_LIMIT, maxRoundsFree: 5, maxRoundsVip: 10 },
|
||||||
gravityRates: pricing.gravityRates,
|
gravityRates: pricing.gravityRates,
|
||||||
products: {
|
products: {
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ export interface GravityRates {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface PricingConfig {
|
interface PricingConfig {
|
||||||
|
/** 新用户注册赠送引力值 */
|
||||||
|
registrationGravity: number
|
||||||
interview: { pricePerSession: number; creditsPerPurchase: number }
|
interview: { pricePerSession: number; creditsPerPurchase: number }
|
||||||
resumeOptimize: { freeLimit: number; pricePerOptimize: number; creditsPerPurchase: number }
|
resumeOptimize: { freeLimit: number; pricePerOptimize: number; creditsPerPurchase: number }
|
||||||
resumeDownload: { pricePerDownload: number; creditsPerPurchase: number }
|
resumeDownload: { pricePerDownload: number; creditsPerPurchase: number }
|
||||||
@@ -21,6 +23,7 @@ interface PricingConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_PRICING: PricingConfig = {
|
const DEFAULT_PRICING: PricingConfig = {
|
||||||
|
registrationGravity: 50,
|
||||||
interview: { pricePerSession: 600, creditsPerPurchase: 1 },
|
interview: { pricePerSession: 600, creditsPerPurchase: 1 },
|
||||||
resumeOptimize: { freeLimit: 3, pricePerOptimize: 400, creditsPerPurchase: 1 },
|
resumeOptimize: { freeLimit: 3, pricePerOptimize: 400, creditsPerPurchase: 1 },
|
||||||
resumeDownload: { pricePerDownload: 200, creditsPerPurchase: 1 },
|
resumeDownload: { pricePerDownload: 200, creditsPerPurchase: 1 },
|
||||||
@@ -62,6 +65,7 @@ export class PricingService {
|
|||||||
|
|
||||||
private mergeDefaults(value: any): PricingConfig {
|
private mergeDefaults(value: any): PricingConfig {
|
||||||
return {
|
return {
|
||||||
|
registrationGravity: value?.registrationGravity ?? DEFAULT_PRICING.registrationGravity,
|
||||||
interview: { ...DEFAULT_PRICING.interview, ...value?.interview },
|
interview: { ...DEFAULT_PRICING.interview, ...value?.interview },
|
||||||
resumeOptimize: { ...DEFAULT_PRICING.resumeOptimize, ...value?.resumeOptimize },
|
resumeOptimize: { ...DEFAULT_PRICING.resumeOptimize, ...value?.resumeOptimize },
|
||||||
resumeDownload: { ...DEFAULT_PRICING.resumeDownload, ...value?.resumeDownload },
|
resumeDownload: { ...DEFAULT_PRICING.resumeDownload, ...value?.resumeDownload },
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Model } from 'mongoose'
|
|||||||
import { JwtService } from '@nestjs/jwt'
|
import { JwtService } from '@nestjs/jwt'
|
||||||
import { User, UserDocument } from './user.schema'
|
import { User, UserDocument } from './user.schema'
|
||||||
import { EmailService } from '../email/email.service'
|
import { EmailService } from '../email/email.service'
|
||||||
|
import { PricingService } from '../schemas/pricing.service'
|
||||||
|
|
||||||
/** 通过 IP 查询粗略地理位置(ip-api.com 免费接口) */
|
/** 通过 IP 查询粗略地理位置(ip-api.com 免费接口) */
|
||||||
async function lookupIpLocation(ip: string): Promise<string> {
|
async function lookupIpLocation(ip: string): Promise<string> {
|
||||||
@@ -30,6 +31,7 @@ export class UserService {
|
|||||||
@InjectModel(User.name) private userModel: Model<UserDocument>,
|
@InjectModel(User.name) private userModel: Model<UserDocument>,
|
||||||
private jwtService: JwtService,
|
private jwtService: JwtService,
|
||||||
private emailService: EmailService,
|
private emailService: EmailService,
|
||||||
|
private pricingService: PricingService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async sendCode(phone: string) {
|
async sendCode(phone: string) {
|
||||||
@@ -58,7 +60,7 @@ export class UserService {
|
|||||||
|
|
||||||
let user = await this.userModel.findOne({ phone }).exec()
|
let user = await this.userModel.findOne({ phone }).exec()
|
||||||
if (!user) {
|
if (!user) {
|
||||||
user = await this.userModel.create({ phone, nickname: `用户${phone.slice(-4)}`, gravity: 5 })
|
user = await this.userModel.create({ phone, nickname: `用户${phone.slice(-4)}`, gravity: (await this.pricingService.getConfig()).registrationGravity })
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.recordLogin(user._id.toString(), ip)
|
await this.recordLogin(user._id.toString(), ip)
|
||||||
@@ -94,7 +96,7 @@ export class UserService {
|
|||||||
|
|
||||||
let user = await this.userModel.findOne({ wxOpenid: openid }).exec()
|
let user = await this.userModel.findOne({ wxOpenid: openid }).exec()
|
||||||
if (!user) {
|
if (!user) {
|
||||||
user = await this.userModel.create({ wxOpenid: openid, nickname: '微信用户', gravity: 5 })
|
user = await this.userModel.create({ wxOpenid: openid, nickname: '微信用户', gravity: (await this.pricingService.getConfig()).registrationGravity })
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.recordLogin(user._id.toString(), ip)
|
await this.recordLogin(user._id.toString(), ip)
|
||||||
@@ -170,7 +172,7 @@ export class UserService {
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
isNew = true
|
isNew = true
|
||||||
const nick = email.split('@')[0]
|
const nick = email.split('@')[0]
|
||||||
user = await this.userModel.create({ email, nickname: nick, remaining: 0, gravity: 5 })
|
user = await this.userModel.create({ email, nickname: nick, remaining: 0, gravity: (await this.pricingService.getConfig()).registrationGravity })
|
||||||
}
|
}
|
||||||
await this.recordLogin(user._id.toString(), ip)
|
await this.recordLogin(user._id.toString(), ip)
|
||||||
return { ...this.generateAuthResponse(user), isNew, hasPassword: !!user.password }
|
return { ...this.generateAuthResponse(user), isNew, hasPassword: !!user.password }
|
||||||
@@ -208,7 +210,7 @@ export class UserService {
|
|||||||
}
|
}
|
||||||
const nick = email.split('@')[0]
|
const nick = email.split('@')[0]
|
||||||
const hashed = await bcrypt.hash(password, 10)
|
const hashed = await bcrypt.hash(password, 10)
|
||||||
const user = await this.userModel.create({ email, nickname: nick, password: hashed, remaining: 0, gravity: 5 })
|
const user = await this.userModel.create({ email, nickname: nick, password: hashed, remaining: 0, gravity: (await this.pricingService.getConfig()).registrationGravity })
|
||||||
await this.recordLogin(user._id.toString(), ip)
|
await this.recordLogin(user._id.toString(), ip)
|
||||||
return this.generateAuthResponse(user)
|
return this.generateAuthResponse(user)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,6 +65,13 @@
|
|||||||
<view class="card" v-if="mainTab === 'register'">
|
<view class="card" v-if="mainTab === 'register'">
|
||||||
<text class="card-title">创建账号</text>
|
<text class="card-title">创建账号</text>
|
||||||
<text class="card-sub">注册后享受 AI 面试模拟服务</text>
|
<text class="card-sub">注册后享受 AI 面试模拟服务</text>
|
||||||
|
<view class="promo-banner">
|
||||||
|
<text class="promo-icon">🎁</text>
|
||||||
|
<view class="promo-texts">
|
||||||
|
<text class="promo-title">注册即送 {{ registrationGravity }} 引力值</text>
|
||||||
|
<text class="promo-desc">免费体验 AI 面试模拟</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
<view class="field">
|
<view class="field">
|
||||||
<text class="field-label">邮箱</text>
|
<text class="field-label">邮箱</text>
|
||||||
<input class="input" type="text" v-model="email" placeholder="请输入邮箱" />
|
<input class="input" type="text" v-model="email" placeholder="请输入邮箱" />
|
||||||
@@ -133,6 +140,7 @@ const agreed = ref(false)
|
|||||||
const mainTab = ref('login')
|
const mainTab = ref('login')
|
||||||
const loginMode = ref('password') // 'password' | 'code'
|
const loginMode = ref('password') // 'password' | 'code'
|
||||||
const isMp = ref(false)
|
const isMp = ref(false)
|
||||||
|
const registrationGravity = ref(50)
|
||||||
|
|
||||||
const email = ref('')
|
const email = ref('')
|
||||||
const password = ref('')
|
const password = ref('')
|
||||||
@@ -158,6 +166,17 @@ onMounted(() => {
|
|||||||
isMp.value = true
|
isMp.value = true
|
||||||
mainTab.value = 'wechat'
|
mainTab.value = 'wechat'
|
||||||
// #endif
|
// #endif
|
||||||
|
// 获取注册赠送引力值配置
|
||||||
|
uni.request({
|
||||||
|
url: api('/member/plans'),
|
||||||
|
method: 'GET',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.statusCode === 200 && res.data?.registrationGravity) {
|
||||||
|
registrationGravity.value = res.data.registrationGravity
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: () => {} // 使用默认值 50
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => { if (timer) { clearTimeout(timer); timer = null } })
|
onBeforeUnmount(() => { if (timer) { clearTimeout(timer); timer = null } })
|
||||||
@@ -355,6 +374,12 @@ const goPrivacy = () => uni.navigateTo({ url: '/pages/privacy/privacy' })
|
|||||||
.card-title { font-size: 30rpx; font-weight: 700; color: var(--color-text); display: block; }
|
.card-title { font-size: 30rpx; font-weight: 700; color: var(--color-text); display: block; }
|
||||||
.card-sub { font-size: 22rpx; color: var(--color-text-tertiary); margin-top: 6rpx; margin-bottom: 24rpx; display: block; }
|
.card-sub { font-size: 22rpx; color: var(--color-text-tertiary); margin-top: 6rpx; margin-bottom: 24rpx; display: block; }
|
||||||
|
|
||||||
|
/* ===== Promo Banner ===== */
|
||||||
|
.promo-banner { background: linear-gradient(135deg, #FEF3C7, #FDE68A); border-radius: var(--radius-md); padding: 20rpx 24rpx; margin-bottom: 24rpx; display: flex; align-items: center; gap: 16rpx; }
|
||||||
|
.promo-icon { font-size: 36rpx; }
|
||||||
|
.promo-title { font-size: 26rpx; font-weight: 700; color: #92400E; display: block; }
|
||||||
|
.promo-desc { font-size: 22rpx; color: #B45309; display: block; margin-top: 4rpx; }
|
||||||
|
|
||||||
/* ===== Fields ===== */
|
/* ===== Fields ===== */
|
||||||
.field { margin-bottom: 20rpx; }
|
.field { margin-bottom: 20rpx; }
|
||||||
.field-label { font-size: 22rpx; color: var(--color-text-secondary); margin-bottom: 8rpx; display: block; }
|
.field-label { font-size: 22rpx; color: var(--color-text-secondary); margin-bottom: 8rpx; display: block; }
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
</view>
|
</view>
|
||||||
<text class="gravity-num">{{ memberInfo.gravity ?? 0 }}</text>
|
<text class="gravity-num">{{ memberInfo.gravity ?? 0 }}</text>
|
||||||
</view>
|
</view>
|
||||||
<text class="gravity-hint">每次面试消耗 5 引力值 · 分享可获得更多</text>
|
<text class="gravity-hint">每次面试消耗 5 引力值 · 分享好友、贡献面经可获得更多</text>
|
||||||
<view class="gravity-actions">
|
<view class="gravity-actions">
|
||||||
<text class="gravity-btn share" @click="goSharePage">分享得引力值</text>
|
<text class="gravity-btn share" @click="goSharePage">分享得引力值</text>
|
||||||
<text class="gravity-btn contribute" @click="goContributePage">贡献面经</text>
|
<text class="gravity-btn contribute" @click="goContributePage">贡献面经</text>
|
||||||
@@ -150,7 +150,7 @@
|
|||||||
<view class="gp-method-icon-wrap"><text class="gp-method-icon">🛒</text></view>
|
<view class="gp-method-icon-wrap"><text class="gp-method-icon">🛒</text></view>
|
||||||
<view class="gp-method-info">
|
<view class="gp-method-info">
|
||||||
<text class="gp-method-name">购买引力值</text>
|
<text class="gp-method-name">购买引力值</text>
|
||||||
<text class="gp-method-desc">小程序内直接购买,微信支付安全便捷</text>
|
<text class="gp-method-desc">¥5 一份,微信支付安全便捷</text>
|
||||||
</view>
|
</view>
|
||||||
<text class="gp-method-arrow">›</text>
|
<text class="gp-method-arrow">›</text>
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
Reference in New Issue
Block a user