fix(user): align dto with latest payload

This commit is contained in:
2026-06-25 17:40:41 +08:00
parent e01b0daaf3
commit 94fb3ed19b
4 changed files with 50 additions and 24 deletions
+5 -5
View File
@@ -5,18 +5,18 @@
* 流程: * 流程:
* 1. 用户在支付服务托管页完成结账 * 1. 用户在支付服务托管页完成结账
* 2. 支付服务重定向到 `/subscription/success` * 2. 支付服务重定向到 `/subscription/success`
* 3. 这个页面显示 "购买成功" + 调 `getCurrentUser` 刷新 user * 3. 这个页面显示 "购买成功" + 调 VIP 状态接口确认权益
* 4. 点 "返回订阅页" 跳回 `/subscription` * 4. 点 "返回订阅页" 跳回 `/subscription`
* *
* 注意:权益发放由后端支付服务链路异步完成。 * 注意:权益发放由后端支付服务链路异步完成。
* 此页面轮询几次 getCurrentUser 等待 user 状态完成更新。 * 此页面轮询几次 VIP 状态接口等待权益完成更新。
*/ */
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { MobileShell } from "@/app/_components/core/mobile-shell"; import { MobileShell } from "@/app/_components/core/mobile-shell";
import { authRepository } from "@/data/repositories/auth_repository"; import { paymentRepository } from "@/data/repositories/payment_repository";
import { ROUTES } from "@/router/routes"; import { ROUTES } from "@/router/routes";
export default function SubscriptionSuccessPage() { export default function SubscriptionSuccessPage() {
@@ -25,7 +25,7 @@ export default function SubscriptionSuccessPage() {
const [errorMessage, setErrorMessage] = useState<string | null>(null); const [errorMessage, setErrorMessage] = useState<string | null>(null);
const hasRefreshed = useRef(false); const hasRefreshed = useRef(false);
// 轮询 getCurrentUser 等待后端权益发放完成 // 轮询 VIP 状态接口等待后端权益发放完成
useEffect(() => { useEffect(() => {
if (hasRefreshed.current) return; if (hasRefreshed.current) return;
hasRefreshed.current = true; hasRefreshed.current = true;
@@ -33,7 +33,7 @@ export default function SubscriptionSuccessPage() {
let cancelled = false; let cancelled = false;
const tryRefresh = async (attemptsLeft: number) => { const tryRefresh = async (attemptsLeft: number) => {
if (cancelled) return; if (cancelled) return;
const r = await authRepository.getCurrentUser(); const r = await paymentRepository.getVipStatus();
if (cancelled) return; if (cancelled) return;
if (r.success) { if (r.success) {
if (r.data.isVip) { if (r.data.isVip) {
+45
View File
@@ -0,0 +1,45 @@
import { describe, expect, it } from "vitest";
import { User } from "@/data/dto/user/user";
describe("User", () => {
it("parses the latest backend user payload", () => {
const user = User.fromJson({
id: "d7bb2b3d-e9c5-474c-85fe-f503048f30dd",
username: "guest_bbc90a38",
email: "device_bbc90a38b996187348f54cf2cb0b789e@guest.local",
platform: "web",
intimacy: 0,
dolBalance: 0,
personalityTraits: {
caring: 0.5,
playful: 0.5,
serious: 0.5,
cheerful: 0.5,
romantic: 0.5,
},
preferredLanguage: "zh",
createdAt: "2026-06-12T08:08:27.378706+00:00",
lastMessageAt: "2026-06-24T11:00:23.156561+00:00",
});
expect(user.toJson()).toEqual({
id: "d7bb2b3d-e9c5-474c-85fe-f503048f30dd",
username: "guest_bbc90a38",
email: "device_bbc90a38b996187348f54cf2cb0b789e@guest.local",
platform: "web",
intimacy: 0,
dolBalance: 0,
personalityTraits: {
caring: 0.5,
playful: 0.5,
serious: 0.5,
cheerful: 0.5,
romantic: 0.5,
},
preferredLanguage: "zh",
createdAt: "2026-06-12T08:08:27.378706+00:00",
lastMessageAt: "2026-06-24T11:00:23.156561+00:00",
});
});
});
-9
View File
@@ -15,19 +15,10 @@ export class User {
declare readonly platform: string; declare readonly platform: string;
declare readonly intimacy: number; declare readonly intimacy: number;
declare readonly dolBalance: number; declare readonly dolBalance: number;
declare readonly relationshipStage: string;
declare readonly currentMood: string;
declare readonly personalityTraits: PersonalityTraits; declare readonly personalityTraits: PersonalityTraits;
declare readonly preferredLanguage: string; declare readonly preferredLanguage: string;
declare readonly createdAt: string; declare readonly createdAt: string;
declare readonly lastMessageAt: string; declare readonly lastMessageAt: string;
declare readonly avatarUrl: string;
declare readonly loginProvider: string;
declare readonly isGuest: boolean;
/** 是否为 VIP 会员 */
declare readonly isVip: boolean;
/** 语音聊天剩余分钟数 */
declare readonly voiceMinutesRemaining: number;
private constructor(input: UserInput) { private constructor(input: UserInput) {
const data = UserSchema.parse(input); const data = UserSchema.parse(input);
-10
View File
@@ -5,7 +5,6 @@
import { z } from "zod"; import { z } from "zod";
import { PersonalityTraitsSchema, PERSONALITY_TRAITS_DEFAULTS } from "./personality_traits"; import { PersonalityTraitsSchema, PERSONALITY_TRAITS_DEFAULTS } from "./personality_traits";
import { import {
booleanOrFalse,
numberOrZero, numberOrZero,
stringOrEmpty, stringOrEmpty,
} from "../nullable-defaults"; } from "../nullable-defaults";
@@ -17,20 +16,11 @@ export const UserSchema = z.object({
platform: stringOrEmpty, platform: stringOrEmpty,
intimacy: numberOrZero, intimacy: numberOrZero,
dolBalance: numberOrZero, dolBalance: numberOrZero,
relationshipStage: stringOrEmpty,
currentMood: stringOrEmpty,
personalityTraits: PersonalityTraitsSchema.default(PERSONALITY_TRAITS_DEFAULTS), personalityTraits: PersonalityTraitsSchema.default(PERSONALITY_TRAITS_DEFAULTS),
preferredLanguage: stringOrEmpty, preferredLanguage: stringOrEmpty,
createdAt: stringOrEmpty, createdAt: stringOrEmpty,
// 后端新游客返回 null("还没发过消息"),前端统一收敛为空字符串。 // 后端新游客返回 null("还没发过消息"),前端统一收敛为空字符串。
lastMessageAt: stringOrEmpty, lastMessageAt: stringOrEmpty,
avatarUrl: stringOrEmpty,
loginProvider: stringOrEmpty,
isGuest: booleanOrFalse,
/** 是否为 VIP 会员(订阅中或未过期) */
isVip: booleanOrFalse,
/** 语音聊天剩余分钟数(订阅生效后每天补充;用完为 0 不变) */
voiceMinutesRemaining: numberOrZero,
}); });
export type UserInput = z.input<typeof UserSchema>; export type UserInput = z.input<typeof UserSchema>;