From 2230a95b45e6fbb5e4e429cd2ac696eced6c26aa Mon Sep 17 00:00:00 2001 From: yuzhiran Date: Sat, 4 Jul 2026 11:12:55 +0800 Subject: [PATCH] feat: gravity transaction logging + user-facing history popup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New GravityTransaction schema tracks all gravity changes (registration, interview/optimize/download deduction, purchase, monthly topup, migration) - GravityTopUpService: bulk log for monthly VIP topup - PaymentController.activateMembership: log plan_set transactions - QuotaService: add logTransaction, wire into all gravity-modifying methods - UserService: log registration grants (phone/wx/email/password) - GET /user/gravity-transactions?page=&limit= API - Frontend: user.vue '明细' button + paginated popup - Docs: update PROJECT-STATUS v4.10, FEATURE-LIST, DEPLOYMENT --- backend/src/app.module.ts | 2 + .../payment/payment.controller.spec.ts | 1 + .../src/modules/payment/payment.controller.ts | 10 +++ .../schedule/gravity-top-up.service.ts | 56 ++++++++----- .../schemas/gravity-transaction.module.ts | 10 +++ .../schemas/gravity-transaction.schema.ts | 43 ++++++++++ backend/src/modules/user/quota.service.ts | 24 +++++- backend/src/modules/user/user.controller.ts | 11 ++- backend/src/modules/user/user.service.spec.ts | 3 + backend/src/modules/user/user.service.ts | 37 ++++++++- docs/DEPLOYMENT.md | 5 +- docs/FEATURE-LIST.md | 10 ++- docs/PROJECT-STATUS.md | 15 ++-- zhiyin-app/src/pages/user/user.vue | 79 ++++++++++++++++++- 14 files changed, 268 insertions(+), 38 deletions(-) create mode 100644 backend/src/modules/schemas/gravity-transaction.module.ts create mode 100644 backend/src/modules/schemas/gravity-transaction.schema.ts diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 92e2c8c..f5fbe71 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -25,6 +25,7 @@ import { DailyQuestionModule } from './modules/daily-question/daily-question.mod import { ScheduleModule } from './modules/schedule/schedule.module' import { TtsModule } from './modules/tts/tts.module' import { PricingModule } from './modules/schemas/pricing.module' +import { GravityTransactionModule } from './modules/schemas/gravity-transaction.module' import { ShareModule } from './modules/share/share.module' import { InterviewReviewModule } from './modules/interview-review/interview-review.module' import { CareerAdviceModule } from './modules/career-advice/career-advice.module' @@ -66,6 +67,7 @@ const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/zhiyin InterviewReviewModule, CareerAdviceModule, VirtualPaymentModule, + GravityTransactionModule, ], providers: [ JwtStrategy, diff --git a/backend/src/modules/payment/payment.controller.spec.ts b/backend/src/modules/payment/payment.controller.spec.ts index 63ae7c7..e77ad6d 100644 --- a/backend/src/modules/payment/payment.controller.spec.ts +++ b/backend/src/modules/payment/payment.controller.spec.ts @@ -51,6 +51,7 @@ describe('PaymentController', () => { providers: [ { provide: getModelToken('User'), useValue: mockUserModel }, { provide: getModelToken('PaymentOrder'), useValue: mockOrderModel }, + { provide: getModelToken('GravityTransaction'), useValue: { create: jest.fn() } }, { provide: WechatPayService, useValue: mockWechatPay }, { provide: QuotaService, useValue: mockQuotaService }, { provide: PricingService, useValue: mockPricingService }, diff --git a/backend/src/modules/payment/payment.controller.ts b/backend/src/modules/payment/payment.controller.ts index 7937833..9e828dc 100644 --- a/backend/src/modules/payment/payment.controller.ts +++ b/backend/src/modules/payment/payment.controller.ts @@ -9,6 +9,7 @@ import { WechatPayService } from './wechat-pay.service' import { QuotaService } from '../user/quota.service' import { PricingService } from '../schemas/pricing.service' import { Public } from '../../common/decorators/public.decorator' +import { GravityTransaction } from '../schemas/gravity-transaction.schema' @Controller('payment') export class PaymentController { @@ -17,6 +18,7 @@ export class PaymentController { constructor( @InjectModel(User.name) private userModel: Model, @InjectModel(PaymentOrder.name) private orderModel: Model, + @InjectModel(GravityTransaction.name) private gravityTxModel: Model, private wechatPay: WechatPayService, private quotaService: QuotaService, private pricingService: PricingService, @@ -226,6 +228,14 @@ export class PaymentController { user.gravity = planCfg.gravityPerMonth user.freeOptimizeUsed = 3 await user.save() + await this.gravityTxModel.create({ + userId: order.userId, + amount: planCfg.gravityPerMonth, + balance: planCfg.gravityPerMonth, + type: 'plan_set', + description: `开通${isSprint ? '冲刺版' : '成长版'}会员,获得 ${planCfg.gravityPerMonth} 引力值`, + refId: order.outTradeNo, + }) } private async activateProduct(order: PaymentOrderDocument) { diff --git a/backend/src/modules/schedule/gravity-top-up.service.ts b/backend/src/modules/schedule/gravity-top-up.service.ts index 671abf5..6639e98 100644 --- a/backend/src/modules/schedule/gravity-top-up.service.ts +++ b/backend/src/modules/schedule/gravity-top-up.service.ts @@ -4,6 +4,7 @@ import { InjectModel } from '@nestjs/mongoose' import { Model } from 'mongoose' import { User, UserDocument } from '../user/user.schema' import { PricingService } from '../schemas/pricing.service' +import { GravityTransaction } from '../schemas/gravity-transaction.schema' @Injectable() export class GravityTopUpService { @@ -11,9 +12,24 @@ export class GravityTopUpService { constructor( @InjectModel(User.name) private userModel: Model, + @InjectModel(GravityTransaction.name) private gravityTxModel: Model, private pricingService: PricingService, ) {} + private async logBulkTopUp(userIds: string[], amount: number, type: string) { + const users = await this.userModel.find({ _id: { $in: userIds } }).select('gravity').exec() + const docs = users.map(u => ({ + userId: u._id.toString(), + amount, + balance: u.gravity, + type, + description: `月度补给 ${amount} 引力值`, + })) + if (docs.length > 0) { + await this.gravityTxModel.insertMany(docs) + } + } + @Cron(CronExpression.EVERY_DAY_AT_2AM) async topUpVipGravity() { this.logger.log('Topping up gravity for active VIP members...') @@ -22,28 +38,32 @@ export class GravityTopUpService { // 成长版 —— vipExpireAt 未过期 const growthPlan = pricing.plans.growth - const growthResult = await this.userModel.updateMany( - { - plan: 'growth', - vipExpireAt: { $gt: now }, - }, - { $inc: { gravity: growthPlan.gravityPerMonth } }, - ).exec() - if (growthResult.modifiedCount > 0) { - this.logger.log(`Growth plan: topped up ${growthResult.modifiedCount} users with ${growthPlan.gravityPerMonth} gravity each`) + const growthUsers = await this.userModel.find( + { plan: 'growth', vipExpireAt: { $gt: now } }, + ).select('_id').exec() + const growthIds = growthUsers.map(u => u._id.toString()) + if (growthIds.length > 0) { + await this.userModel.updateMany( + { _id: { $in: growthIds } }, + { $inc: { gravity: growthPlan.gravityPerMonth } }, + ).exec() + await this.logBulkTopUp(growthIds, growthPlan.gravityPerMonth, 'monthly_topup') + this.logger.log(`Growth plan: topped up ${growthIds.length} users with ${growthPlan.gravityPerMonth} gravity each`) } // 冲刺版 —— sprintExpireAt 未过期 const sprintPlan = pricing.plans.sprint - const sprintResult = await this.userModel.updateMany( - { - plan: 'sprint', - sprintExpireAt: { $gt: now }, - }, - { $inc: { gravity: sprintPlan.gravityPerMonth } }, - ).exec() - if (sprintResult.modifiedCount > 0) { - this.logger.log(`Sprint plan: topped up ${sprintResult.modifiedCount} users with ${sprintPlan.gravityPerMonth} gravity each`) + const sprintUsers = await this.userModel.find( + { plan: 'sprint', sprintExpireAt: { $gt: now } }, + ).select('_id').exec() + const sprintIds = sprintUsers.map(u => u._id.toString()) + if (sprintIds.length > 0) { + await this.userModel.updateMany( + { _id: { $in: sprintIds } }, + { $inc: { gravity: sprintPlan.gravityPerMonth } }, + ).exec() + await this.logBulkTopUp(sprintIds, sprintPlan.gravityPerMonth, 'monthly_topup') + this.logger.log(`Sprint plan: topped up ${sprintIds.length} users with ${sprintPlan.gravityPerMonth} gravity each`) } } } diff --git a/backend/src/modules/schemas/gravity-transaction.module.ts b/backend/src/modules/schemas/gravity-transaction.module.ts new file mode 100644 index 0000000..649e079 --- /dev/null +++ b/backend/src/modules/schemas/gravity-transaction.module.ts @@ -0,0 +1,10 @@ +import { Module, Global } from '@nestjs/common' +import { MongooseModule } from '@nestjs/mongoose' +import { GravityTransaction, GravityTransactionSchema } from './gravity-transaction.schema' + +@Global() +@Module({ + imports: [MongooseModule.forFeature([{ name: GravityTransaction.name, schema: GravityTransactionSchema }])], + exports: [MongooseModule], +}) +export class GravityTransactionModule {} diff --git a/backend/src/modules/schemas/gravity-transaction.schema.ts b/backend/src/modules/schemas/gravity-transaction.schema.ts new file mode 100644 index 0000000..23a6f94 --- /dev/null +++ b/backend/src/modules/schemas/gravity-transaction.schema.ts @@ -0,0 +1,43 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose' +import { Document } from 'mongoose' + +export type GravityTransactionDocument = GravityTransaction & Document + +export type GravityTransactionType = + | 'registration' // 注册赠送 + | 'interview_deduct' // AI 面试消耗 + | 'optimize_deduct' // 简历优化消耗 + | 'download_deduct' // 简历下载消耗 + | 'purchase' // 充值购买 + | 'monthly_topup' // 月度补给 + | 'migration' // 旧额度迁移 + | 'plan_set' // 套餐设置 + | 'share' // 分享所得 + | 'contribution' // 面经贡献奖励 + | 'share_credits_fallback' // 分享币后备抵扣 + | 'admin_adjust' // 管理员调整 + +@Schema({ timestamps: true }) +export class GravityTransaction { + @Prop({ required: true, index: true }) + userId: string + + @Prop({ required: true }) + amount: number // 变动数量(正=增加,负=减少) + + @Prop({ required: true }) + balance: number // 变动后余额 + + @Prop({ required: true }) + type: GravityTransactionType + + @Prop({ default: '' }) + description: string // 描述(如 "AI 模拟面试消耗") + + @Prop() + refId?: string // 关联 ID(订单号、面试ID 等) +} + +export const GravityTransactionSchema = SchemaFactory.createForClass(GravityTransaction) + +GravityTransactionSchema.index({ userId: 1, createdAt: -1 }) diff --git a/backend/src/modules/user/quota.service.ts b/backend/src/modules/user/quota.service.ts index 592c7a0..a8c779a 100644 --- a/backend/src/modules/user/quota.service.ts +++ b/backend/src/modules/user/quota.service.ts @@ -3,6 +3,7 @@ import { InjectModel } from '@nestjs/mongoose' import { Model } from 'mongoose' import { User, UserDocument } from './user.schema' import { PricingService } from '../schemas/pricing.service' +import { GravityTransaction, GravityTransactionType } from '../schemas/gravity-transaction.schema' const FREE_OPTIMIZE_LIMIT = 3 @@ -12,9 +13,16 @@ export class QuotaService { constructor( @InjectModel(User.name) private userModel: Model, + @InjectModel(GravityTransaction.name) private gravityTxModel: Model, private pricingService: PricingService, ) {} + private async logTransaction(userId: string, amount: number, type: GravityTransactionType, description: string, refId?: string) { + const u = await this.userModel.findById(userId).select('gravity').exec() + const balance = u?.gravity ?? 0 + await this.gravityTxModel.create({ userId, amount, balance, type, description, refId }) + } + /** 仅检查面试引力值是否充足,不扣除(用于先 AI 后扣款模式) */ async checkInterview(userId: string): Promise { const user = await this.userModel.findById(userId).exec() @@ -106,19 +114,23 @@ export class QuotaService { /** 从 gravity 扣除,后备从 shareCredits 扣除(兼容旧数据) */ private async deductGravityOrFallback(userId: string, cost: number): Promise { - // 主路径:gravity const gravResult = await this.userModel.findOneAndUpdate( { _id: userId, gravity: { $gte: cost } }, { $inc: { gravity: -cost } }, ).exec() - if (gravResult) return true + if (gravResult) { + await this.logTransaction(userId, -cost, 'interview_deduct', `AI 模拟面试消耗 ${cost} 引力值`) + return true + } - // 后备:旧 shareCredits const shareResult = await this.userModel.findOneAndUpdate( { _id: userId, shareCredits: { $gt: 0 } }, { $inc: { shareCredits: -1 } }, ).exec() - if (shareResult) return true + if (shareResult) { + await this.logTransaction(userId, 0, 'share_credits_fallback', '分享币后备抵扣 1 次') + return true + } return false } @@ -131,6 +143,7 @@ export class QuotaService { { $inc: { gravity: amount } }, ).exec() if (!result) throw new HttpException('用户不存在', HttpStatus.NOT_FOUND) + await this.logTransaction(userId, amount, 'purchase', `充值获得 ${amount} 引力值`) } /** 设置 VIP 套餐引力值额度 */ @@ -142,6 +155,7 @@ export class QuotaService { }, }).exec() if (!result) throw new HttpException('用户不存在', HttpStatus.NOT_FOUND) + await this.logTransaction(userId, gravityAmount, 'plan_set', `套餐开通,获得 ${gravityAmount} 引力值`) } /** 是否为非会员用户授予初始引力值 */ @@ -149,6 +163,7 @@ export class QuotaService { await this.userModel.findByIdAndUpdate(userId, { $set: { interviewCredits: 1, gravity: 5 }, }).exec() + await this.logTransaction(userId, 5, 'registration', '注册赠送 5 引力值') } /** 判断是否有旧额度需要迁移 */ @@ -180,5 +195,6 @@ export class QuotaService { shareCredits: 0, }, }).exec() + await this.logTransaction(userId, total, 'migration', `旧额度迁移,获得 ${total} 引力值`) } } diff --git a/backend/src/modules/user/user.controller.ts b/backend/src/modules/user/user.controller.ts index 286b75d..f2c4fe9 100644 --- a/backend/src/modules/user/user.controller.ts +++ b/backend/src/modules/user/user.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Post, Get, Put, Body, Req, HttpCode, HttpStatus, UseGuards } from '@nestjs/common' +import { Controller, Post, Get, Put, Query, Body, Req, HttpCode, HttpStatus, UseGuards } from '@nestjs/common' import { UserService } from './user.service' import { Public } from '../../common/decorators/public.decorator' import { CurrentUser } from '../../common/decorators/current-user.decorator' @@ -88,4 +88,13 @@ export class UserController { async setPassword(@CurrentUser('userId') userId: string, @Body('password') password: string) { return this.userService.setPassword(userId, password) } + + @Get('gravity-transactions') + async getGravityTransactions( + @CurrentUser('userId') userId: string, + @Query('page') page = '1', + @Query('limit') limit = '20', + ) { + return this.userService.getGravityTransactions(userId, parseInt(page), parseInt(limit)) + } } diff --git a/backend/src/modules/user/user.service.spec.ts b/backend/src/modules/user/user.service.spec.ts index 0d6645d..6f5d8ba 100644 --- a/backend/src/modules/user/user.service.spec.ts +++ b/backend/src/modules/user/user.service.spec.ts @@ -4,6 +4,7 @@ import { JwtService } from '@nestjs/jwt' import { HttpException } from '@nestjs/common' import { UserService } from './user.service' import { EmailService } from '../email/email.service' +import { PricingService } from '../schemas/pricing.service' describe('UserService', () => { let service: UserService @@ -44,8 +45,10 @@ describe('UserService', () => { providers: [ UserService, { provide: getModelToken('User'), useValue: mockUserModel }, + { provide: getModelToken('GravityTransaction'), useValue: { create: jest.fn() } }, { provide: JwtService, useValue: mockJwtService }, { provide: EmailService, useValue: mockEmailService }, + { provide: PricingService, useValue: { getConfig: jest.fn().mockResolvedValue({ registrationGravity: 50, gravityRates: { interviewPerUse: 5, optimizePerUse: 3, downloadPerUse: 2 }, plans: { growth: { gravityPerMonth: 80 }, sprint: { gravityPerMonth: 200 } } }) } }, ], }).compile() diff --git a/backend/src/modules/user/user.service.ts b/backend/src/modules/user/user.service.ts index 597ab3b..e4309c3 100644 --- a/backend/src/modules/user/user.service.ts +++ b/backend/src/modules/user/user.service.ts @@ -2,10 +2,11 @@ import { Injectable, HttpException, HttpStatus, Logger } from '@nestjs/common' import { InjectModel } from '@nestjs/mongoose' import { Model } from 'mongoose' -import { JwtService } from '@nestjs/jwt' import { User, UserDocument } from './user.schema' +import { JwtService } from '@nestjs/jwt' import { EmailService } from '../email/email.service' import { PricingService } from '../schemas/pricing.service' +import { GravityTransaction, GravityTransactionType } from '../schemas/gravity-transaction.schema' /** 通过 IP 查询粗略地理位置(ip-api.com 免费接口) */ async function lookupIpLocation(ip: string): Promise { @@ -29,11 +30,18 @@ export class UserService { constructor( @InjectModel(User.name) private userModel: Model, + @InjectModel(GravityTransaction.name) private gravityTxModel: Model, private jwtService: JwtService, private emailService: EmailService, private pricingService: PricingService, ) {} + private async logTransaction(userId: string, amount: number, type: GravityTransactionType, description: string) { + const u = await this.userModel.findById(userId).select('gravity').exec() + const balance = u?.gravity ?? 0 + await this.gravityTxModel.create({ userId, amount, balance, type, description }) + } + async sendCode(phone: string) { const code = process.env.NODE_ENV === 'production' ? String(Math.floor(100000 + Math.random() * 900000)) @@ -59,11 +67,17 @@ export class UserService { codeStore.delete(phone) let user = await this.userModel.findOne({ phone }).exec() + let isNew = false if (!user) { + isNew = true user = await this.userModel.create({ phone, nickname: `用户${phone.slice(-4)}`, gravity: (await this.pricingService.getConfig()).registrationGravity }) } await this.recordLogin(user._id.toString(), ip) + if (isNew) { + const amount = (await this.pricingService.getConfig()).registrationGravity + await this.logTransaction(user._id.toString(), amount, 'registration', '注册赠送引力值') + } return this.generateAuthResponse(user) } @@ -95,11 +109,17 @@ export class UserService { } let user = await this.userModel.findOne({ wxOpenid: openid }).exec() + let isNew = false if (!user) { + isNew = true user = await this.userModel.create({ wxOpenid: openid, nickname: '微信用户', gravity: (await this.pricingService.getConfig()).registrationGravity }) } await this.recordLogin(user._id.toString(), ip) + if (isNew) { + const amount = (await this.pricingService.getConfig()).registrationGravity + await this.logTransaction(user._id.toString(), amount, 'registration', '注册赠送引力值') + } return this.generateAuthResponse(user) } @@ -175,6 +195,10 @@ export class UserService { user = await this.userModel.create({ email, nickname: nick, remaining: 0, gravity: (await this.pricingService.getConfig()).registrationGravity }) } await this.recordLogin(user._id.toString(), ip) + if (isNew) { + const amount = (await this.pricingService.getConfig()).registrationGravity + await this.logTransaction(user._id.toString(), amount, 'registration', '注册赠送引力值') + } return { ...this.generateAuthResponse(user), isNew, hasPassword: !!user.password } } @@ -211,6 +235,8 @@ 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: 0, gravity: (await this.pricingService.getConfig()).registrationGravity }) + const amount = (await this.pricingService.getConfig()).registrationGravity + await this.logTransaction(user._id.toString(), amount, 'registration', '注册赠送引力值') await this.recordLogin(user._id.toString(), ip) return this.generateAuthResponse(user) } @@ -265,6 +291,15 @@ export class UserService { await user.save() } + async getGravityTransactions(userId: string, page = 1, limit = 20) { + const skip = (page - 1) * limit + const [items, total] = await Promise.all([ + this.gravityTxModel.find({ userId }).sort({ createdAt: -1 }).skip(skip).limit(limit).exec(), + this.gravityTxModel.countDocuments({ userId }), + ]) + return { items, total, page, limit, totalPages: Math.ceil(total / limit) } + } + private generateAuthResponse(user: UserDocument) { const payload = { userId: user._id.toString(), phone: user.phone || '', role: user.role || 'user' } return { diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index e72ad14..7b4ce92 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -1,6 +1,6 @@ # 职引 - 部署文档 -> **最后更新**: 2026-06-21 +> **最后更新**: 2026-07-04 > **生产环境**: 已部署(服务器已购 + 域名已配) ## 目录 @@ -227,7 +227,7 @@ node scripts/upload-mp.js ``` ### 版本号 -当前线上版本:**1.0.17**(git tag v1.0.16,脚本自动末位自增 → 上传版本 1.0.17) +当前线上版本:**1.0.21**(git tag v1.0.20,脚本自动末位自增 → 上传版本 1.0.21) --- @@ -255,3 +255,4 @@ node scripts/upload-mp.js | 2026-06-09 | 更新生产域名:zhiyinwx.yzrcloud.cn(API :3006)、zhiyin.yzrcloud.cn(H5 静态目录) | 小之 | | 2026-06-21 | 更新部署版本至 v1.0.16;小程序上传工具使用 git tag 自动获取版本号 | 小之 | | 2026-06-21 | v4.8 SEO + 分享全面优化:部署新增 robots.txt、sitemap.xml、static/ 目录;版本号自动注入(Vite define);13 页面微信分享全部开启;上传脚本版本号末位自增 1 | AI | +| 2026-07-04 | v4.10 引力值变动记录系统 + 前端 401/错误处理完善;v1.0.21 发布;H5 + 小程序全量部署 | AI | diff --git a/docs/FEATURE-LIST.md b/docs/FEATURE-LIST.md index 668ca00..8de4c23 100644 --- a/docs/FEATURE-LIST.md +++ b/docs/FEATURE-LIST.md @@ -1,8 +1,8 @@ -# 职引 · 完整功能清单 v4.7 +# 职引 · 完整功能清单 v4.10 -> **版本**: v4.7 -> **日期**: 2026-06-21 -> **状态**: Phase 1.5 按量购买引力值 + 全量生产部署 +> **版本**: v4.10 +> **日期**: 2026-07-04 +> **状态**: Phase 1.5 引力值变动记录 + 前端错误处理完善 > **定位**: 应届生/实习生 AI 面试教练 --- @@ -86,6 +86,7 @@ | 简历管理 | ✅ 完成 | 多份简历 CRUD + AI 分析 | | 面试复盘 | ✅ 完成 | 音频上传 → ASR → AI 评析 → 口语分析 | | 会员中心 | ✅ 完成 | 套餐对比 + 支付 | +| 引力值明细 | ✅ 完成 | 分页查看引力值变动记录(注册/消耗/购买/补给/迁移) | --- @@ -193,3 +194,4 @@ | 2026-06-16 | **v4.2**:新增面试复盘功能(whisper.cpp ASR + AI 评析 + 口语分析) | AI | | 2026-06-17 | **v4.3**:新增 AI 择业顾问功能(专业分析 + 岗位匹配 + 多轮对话) | AI | | 2026-06-21 | **v4.7**:按量购买引力值重构(¥5/份取代月订阅);微信小程序剪贴板购买链路;客服按钮;管理后台全面完善;生产环境全量部署上线 | AI | +| 2026-07-04 | **v4.10**:引力值变动记录系统(GravityTransaction schema + 全埋点 + 用户端分页明细弹窗);前端 401/错误处理完善;面试创建 201 兼容;AI 错误友好提示 | AI | diff --git a/docs/PROJECT-STATUS.md b/docs/PROJECT-STATUS.md index 1953253..e313d80 100644 --- a/docs/PROJECT-STATUS.md +++ b/docs/PROJECT-STATUS.md @@ -1,8 +1,8 @@ -# 职引项目 · 状态报告 v4.9 +# 职引项目 · 状态报告 v4.10 -> **项目版本**: v4.9 -> **更新时间**: 2026-06-22 -> **项目状态**: ✅ Mongoose 8 兼容修复 + v1.0.17 发布 +> **项目版本**: v4.10 +> **更新时间**: 2026-07-04 +> **项目状态**: ✅ 引力值变动记录系统 + 前端 401/错误处理完善 + v1.0.21 发布 --- @@ -16,7 +16,7 @@ | 定价 | 免费版 / 按量购买引力值(¥5/份) | | AI 模型 | DeepSeek V4-Flash(主) + Step-3.5-Flash(备) | | ASR | whisper.cpp(本地部署,tiny/base 模型,无需 API Key) | -| 后端模块 | user, interview, resume, member, payment, positions, ai, analyze, upload, admin, email, progress, contribution, daily-question, schedule, interview-review, career-advice | +| 后端模块 | user, interview, resume, member, payment, positions, ai, analyze, upload, admin, email, progress, contribution, daily-question, schedule, interview-review, career-advice, gravity-transaction | --- @@ -29,6 +29,7 @@ | AI 面试模拟 | **95%** | 多轮对话 + 评分 + 报告 + 进度追踪 | | 简历诊断/优化 | **95%** | 文件上传 + AI 分析 + 下载 | | 支付系统(微信) | **95%** | API v3 完整对接,含真实证书,H5 扫码支付可用 | +| 引力值变动记录 | **100%** | 全量日志(注册/消耗/购买/补给/迁移)+ 前端分页查看 | | 会员系统 | **100%** | 改为按量购买引力值体系(¥5/份),免费版注册送 5 引力值 | | 护城河 P0-P5 | **100%** | AI 结构化 / 行业基准 / VIP 过期 / 分享卡片 / 打卡积分 / 岗位匹配 | | 面试复盘 | **100%** | 音频上传 → whisper.cpp ASR → AI 评析 → 口语分析 | @@ -182,6 +183,7 @@ | `interview-review` | controller + service + schema + asr service | ✅ | 面试复盘:音频 ASR + AI 评析 + 口语分析 | | `career-advice` | controller + service + module | ✅ | AI 择业顾问:专业分析 + 岗位匹配 + 多轮对话 | | `admin` | controller + module | ✅ | 管理后台 | +| `gravity-transaction` | schema (shared) | ✅ | 引力值变动全量日志:注册/面试消耗/购买/月度补给/迁移等 | | `email` | module + service | ✅ | 邮件发送 | | `upload` | controller + module | ✅ | 文件上传 | @@ -196,7 +198,7 @@ | 面试模拟 | interview/interview | ✅ 多轮对话 + 计时 | | 面试报告 | report/report | ✅ 评分/分析/全文回放/分享卡片 | | 历史记录 | history/history | ✅ 筛选/统计 | -| 个人中心 | user/user | ✅ 引力值卡片 + 信息/统计/管理员入口 + 面试复盘入口 + 择业顾问入口 + 客服按钮 | +| 个人中心 | user/user | ✅ 引力值卡片(含明细弹窗)+ 信息/统计/管理员入口 + 面试复盘入口 + 择业顾问入口 + 客服按钮 | | 会员中心 | member/member | ✅ 引力值按量购买(H5 扫码支付/小程序剪贴板链路) | | 进步轨迹 | progress/progress | ✅ 雷达图 + 打卡日历 | | 面经贡献 | contribute/contribute | ✅ 表单提交 | @@ -224,6 +226,7 @@ | 日期 | 版本 | 变更内容 | 操作者 | |------|------|----------|--------| +| 2026-07-04 | **v4.10** | **引力值变动记录系统**:新建 GravityTransaction schema + 全量日志埋点(注册/面试消耗/购买/月度补给/迁移/套餐设置等);新增 `GET /user/gravity-transactions` 接口;用户端「引力值明细」弹窗(分页);前端 401/错误处理完善(checkAuth 全局检测);面试创建 201 状态码兼容 fix;AI 错误友好提示 | AI | | 2026-06-22 | **v4.9** | **Mongoose 8 兼容修复**(pre-save hook 回调→async);v1.0.17 tag 发布;测试账号 test@yzrcloud.cn 重建 | AI | | 2026-06-21 | **v4.8** | **SEO 全量优化**(canonical URL、robots.txt、sitemap.xml、结构化数据);**微信分享全面开启**(13 个页面 onShareAppMessage + onShareTimeline);**版本号自动注入**(Vite define __APP_VERSION__);**导航栏/Tab标题关键词优化**;manifest 描述更新;页面描述统一增强 | AI | | 2026-06-21 | v4.7 | 按量购买引力值体系重构(¥5/份取代月订阅);member.vue 完全重写;微信小程序剪贴板购买链路;客服按钮;管理后台字段全面完善;代码清理;测试数据清理;后端/H5/小程序全量部署上线 | AI | diff --git a/zhiyin-app/src/pages/user/user.vue b/zhiyin-app/src/pages/user/user.vue index bd6a8a2..6fe3539 100644 --- a/zhiyin-app/src/pages/user/user.vue +++ b/zhiyin-app/src/pages/user/user.vue @@ -53,7 +53,8 @@ 贡献面经 - 购买引力值 + 购买引力值 + 明细 @@ -158,11 +159,37 @@ 关闭 + + + + + 引力值明细 + 购买、消耗、补给的引力值变动记录 + + 暂无记录 + + + {{ tx.description }} + {{ formatTime(tx.createdAt) }} + + + {{ tx.amount > 0 ? '+' : '' }}{{ tx.amount }} + + + + + 上一页 + {{ gravityTxPage }} / {{ gravityTxTotalPages }} + 下一页 + + 关闭 + +