feat: refactor member to pay-per-use gravity purchase; mv webview to clipboard+browser

- member.vue: rewrite from subscription plans (free/growth/sprint) to
  H5-only pay-per-use gravity purchase with quantity selector + QR code
- user.vue: gravity card replacing quota card, add share/contribute/H5-buy
  entry points, plus gravity acquisition modal (share/contribute/buy)
- share.vue: layout fix (flex column), smarter copyLink with cached URL,
  WeChat timeline hint instead of open-type
- share.controller.ts: add GET /:shareCode redirect route (IP record + 302)
- interview.vue: guest mode fix, H5 buy modal, clipboard copy instead of
  webview for mini-program
- App.vue: handleH5UrlParams for ?token=&buy=gravity auto-login
- composables/useGravityPurchase.ts: reusable gravity purchase composable
- remove webview.vue (no longer used), replace with clipboard+browser flow
- AGENTS.md: sync all above changes, fix duplicate numbering
This commit is contained in:
yuzhiran
2026-06-20 20:49:15 +08:00
parent a1e1f0b3c3
commit 8ee27fdd32
11 changed files with 648 additions and 593 deletions
@@ -6,7 +6,6 @@ import { VipExpiryService } from './vip-expiry.service'
import { GravityTopUpService } from './gravity-top-up.service'
import { DailyQuestion, DailyQuestionSchema } from '../schemas/daily-question.schema'
import { User, UserSchema } from '../user/user.schema'
import { PricingService } from '../schemas/pricing.service'
@Module({
imports: [
@@ -15,6 +14,6 @@ import { PricingService } from '../schemas/pricing.service'
{ name: User.name, schema: UserSchema },
]),
],
providers: [WechatTokenService, DailyQuestionPushService, VipExpiryService, GravityTopUpService, PricingService],
providers: [WechatTokenService, DailyQuestionPushService, VipExpiryService, GravityTopUpService],
})
export class ScheduleModule {}
+26 -1
View File
@@ -1,9 +1,11 @@
import { Controller, Get, Post, Body, Param, Query, HttpException, HttpStatus, UseGuards } from '@nestjs/common'
import { Controller, Get, Post, Body, Param, Query, HttpException, HttpStatus, UseGuards, Req, Res } from '@nestjs/common'
import { Request, Response } from 'express'
import { JwtService } from '@nestjs/jwt'
import { ShareService } from './share.service'
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'
import { CurrentUser } from '../../common/decorators/current-user.decorator'
import { Public } from '../../common/decorators/public.decorator'
import * as crypto from 'crypto'
@Controller('share')
export class ShareController {
@@ -66,4 +68,27 @@ export class ShareController {
) {
return this.shareService.visitors(userId, Number(page) || 1, Number(pageSize) || 20)
}
// 泛匹配路由放在最后,避免拦截 stats/records/visitors 等
@Public()
@Get(':shareCode')
async redirect(
@Param('shareCode') shareCode: string,
@Req() req: Request,
@Res() res: Response,
) {
try {
const ip = req.ip || req.socket?.remoteAddress || 'unknown'
const visitorId = crypto.createHash('md5').update(ip).digest('hex').slice(0, 16)
let visitorUserId: string | undefined
const token = req.query.token as string | undefined
if (token) {
try { const payload = this.jwtService.verify(token) as any; visitorUserId = payload.userId } catch {}
}
await this.shareService.visit(shareCode, visitorId, visitorUserId)
} catch (e) {
// 访问记录失败不影响跳转
}
res.redirect(HttpStatus.FOUND, `https://zhiyin.yzrcloud.cn/?share=${shareCode}`)
}
}