fix(user): align dto with latest payload
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
Reference in New Issue
Block a user