From 1fc625ede83669576d5568ead0872faa6afe6e43 Mon Sep 17 00:00:00 2001 From: Eamon <17516219072@163.com> Date: Tue, 13 Oct 2026 13:32:53 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BA=A7=E5=93=81=E8=B4=AD=E4=B9=B0=20&=20?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E8=B4=A6=E5=8F=B7-=E5=85=8D=E8=B4=B9?= =?UTF-8?q?=E9=A2=86=E5=8F=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/account.ts | 4 +- src/api/base.ts | 59 +++- src/api/dashboard.ts | 4 +- src/api/user.ts | 6 +- src/home/product/buy.tsx | 571 +++++++++++-------------------- src/home/product/buyStatic.tsx | 11 +- src/home/product/productGrid.tsx | 2 +- src/lib/models/order.ts | 21 +- 8 files changed, 282 insertions(+), 396 deletions(-) diff --git a/src/api/account.ts b/src/api/account.ts index f7f3c51..a2fb368 100644 --- a/src/api/account.ts +++ b/src/api/account.ts @@ -1,11 +1,11 @@ import type { ApiResponse } from "@/lib/api" import type { MyAccountItem, MyAccountQueryParams } from "@/lib/models/account" -import { callPublic } from "./base" +import { callByUserCookie } from "./base" export async function fetchMyAccounts( params: MyAccountQueryParams, ): Promise> { - const res = await callPublic( + const res = await callByUserCookie( "/user/ApiMyAccounts", params, "GET", diff --git a/src/api/base.ts b/src/api/base.ts index 20b9df9..e15b007 100644 --- a/src/api/base.ts +++ b/src/api/base.ts @@ -6,21 +6,22 @@ import { } from "@/lib/api" import { clearCookie, clearSession, getUserToken } from "./token" -type CallMode = "public" | "device" | "user" +type CallMode = "public" | "device" | "user" | "cookie" // ==================== 登录态失效全局处理 ==================== -// user 模式请求未登录或收到 401(token 失效)时:清除本地会话并跳转登录页, -// 带 redirect 参数便于登录后回跳。与 logout() 及 juipnet 老项目行为保持一致。 +// user/cookie 模式请求未登录、收到 401(token 失效)或 302(后端跳登录页)时: +// 清除本地会话并跳转登录页,带 redirect 参数便于登录后回跳。 +// 与 logout() 及 juipnet 老项目行为保持一致。 let redirectingToLogin = false function handleUnauthorized() { if (redirectingToLogin) return - redirectingToLogin = true clearSession() clearCookie("token") clearCookie("userInfo") const current = window.location.pathname if (current !== "/login") { + redirectingToLogin = true window.location.href = `/login?redirect=${encodeURIComponent(current)}` } } @@ -62,8 +63,20 @@ async function request( headers, body: init.body, signal: controller.signal, + redirect: "manual", }) + // 登录态失效时后端会把受保护接口 302 到登录页(如 /User/WebLogin)。 + // manual 模式不自动跟随,此处识别重定向并返回 302, + // 由 call() 在 user/cookie 模式下统一触发 handleUnauthorized 跳登录页。 + if (response.type === "opaqueredirect") { + return { + success: false, + status: 302, + message: "登录状态已失效,请重新登录", + } + } + const responseHeaders = captureHeaders(response) const contentType = response.headers.get("Content-Type") ?? "text/plain" @@ -125,7 +138,13 @@ async function request( return { success: true, data: json, headers: responseHeaders } } - throw new Error(`无法解析响应数据: ${contentType}`) + // 其他内容类型(如 text/html 错误页)无法按 API 格式解析, + // 保留真实 HTTP 状态码返回,便于上层识别 401/3xx 等登录态问题 + return { + success: false, + status: response.status, + message: `无法解析响应数据: ${contentType}`, + } } catch (e) { if (controller.signal.aborted) { return { @@ -235,6 +254,18 @@ async function call( } } auth = `Bearer ${token}` + } else if (mode === "cookie") { + // cookie 模式:凭证走浏览器 cookie(老 /user/* 接口), + // 但登录态判断仍以本地 token 为准,缺失时同样触发跳登录 + const token = getUserToken() + if (!token) { + handleUnauthorized() + return { + success: false, + status: 401, + message: "未登录或会话已过期", + } + } } else if (mode === "device") { if (attempt > 1) { deviceToken = null @@ -262,7 +293,13 @@ async function call( continue } - if (!resp.success && resp.status === 401 && mode === "user") { + // 会话失效的三种表现:本地无 token(前面已处理)、后端 401、 + // 或后端 302 重定向到登录页(如 /user/* 过期跳 /User/WebLogin) + if ( + !resp.success && + (resp.status === 401 || resp.status === 302) && + (mode === "user" || mode === "cookie") + ) { handleUnauthorized() } return resp @@ -305,4 +342,12 @@ async function callByUser( return call("user", endpoint, data, method) } -export { callByDevice, callByUser, callPublic } +async function callByUserCookie( + endpoint: string, + data?: unknown, + method: "GET" | "POST" = "POST", +): Promise> { + return call("cookie", endpoint, data, method) +} + +export { callByDevice, callByUser, callByUserCookie, callPublic } diff --git a/src/api/dashboard.ts b/src/api/dashboard.ts index dbe87f4..7a88d42 100644 --- a/src/api/dashboard.ts +++ b/src/api/dashboard.ts @@ -1,11 +1,11 @@ import type { ApiResponse } from "@/lib/api" import type { ApiIndexItem } from "@/lib/models/dashboard" -import { callPublic } from "./base" +import { callByUserCookie } from "./base" export async function fetchApiIndex(): Promise< ApiResponse > { - const res = await callPublic( + const res = await callByUserCookie( "/user/ApiIndex", undefined, "GET", diff --git a/src/api/user.ts b/src/api/user.ts index 70ae840..459d5b0 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -6,7 +6,7 @@ import type { UserItem, UserQueryParams, } from "@/lib/models/user" -import { callByUser, callPublic } from "./base" +import { callByUser, callByUserCookie } from "./base" // ==================== 用户管理 ==================== @@ -54,11 +54,11 @@ export async function updateUserProfile( export async function updateUserInfo( data: UpdateUserInfoParams, ): Promise> { - return await callPublic("/user/ApiUpdateInfo", data) + return await callByUserCookie("/user/ApiUpdateInfo", data) } export async function updateUserPwd( data: UpdateUserPwdParams, ): Promise> { - return await callPublic("/user/UpdatePwd", data) + return await callByUserCookie("/user/UpdatePwd", data) } diff --git a/src/home/product/buy.tsx b/src/home/product/buy.tsx index a77a02c..3c1bc18 100644 --- a/src/home/product/buy.tsx +++ b/src/home/product/buy.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from "react" import { useLocation, useNavigate } from "react-router-dom" import { toast } from "sonner" -import { createOrder } from "@/api/order" +import { createOrder, createTestAccount } from "@/api/order" import RealNameCheck from "@/components/realNameCheck" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" @@ -18,7 +18,6 @@ import Wrap from "@/components/wrap" import type { CreateOrderRequest } from "@/lib/models/order" import type { Product } from "@/lib/models/product" import { clearPayPaid, PAY_PAID_KEY, saveOrderPay } from "@/lib/recharge" -import { cn } from "@/lib/utils" import alipay from "../_assets/alipay.svg" import balance from "../_assets/balance.svg" import wechat from "../_assets/wechat.svg" @@ -41,42 +40,6 @@ function randomDigits(length: number): string { return result } -// ============ 模式切换 Tab ============ -function ModeTabs({ - mode, - onModeChange, -}: { - mode: "single" | "batch" - onModeChange: (mode: "single" | "batch") => void -}) { - return ( -
- - -
- ) -} - // ============ 连接设备数选择器 ============ function ConnectCountSelector({ value, @@ -176,50 +139,15 @@ function CouponSelector() { ) } -// ============ 批量注册说明 ============ -function BatchInfo() { - return ( -
- -
-

- 批量注册的账号会使用【账号前缀】+【开始数】+【个数】顺序进行注册, -

-

- 如:注册账号前缀为【user】开始数为【2】个数为【10】,则注册的账号为: - - {" "} - user2,user3,user4,....user11 - -

-
-
- ) -} - // ============ 主页面 ============ export default function BuyPage() { const location = useLocation() const navigate = useNavigate() const product = location.state?.product as Product | undefined - // 模式 - const [mode, setMode] = useState<"single" | "batch">("single") - - // 单个注册 const [account, setAccount] = useState("") const [pwd, setPwd] = useState("") const [connectCount, setConnectCount] = useState(1) - - // 批量注册 - const [batchAccount, setBatchAccount] = useState("") - const [batchPwd, setBatchPwd] = useState("") - const [batchStart, setBatchStart] = useState(1) - const [batchCount, setBatchCount] = useState(1) - const [batchConnectCount, setBatchConnectCount] = useState(1) - const [payType, setPayType] = useState("100") const [submitting, setSubmitting] = useState(false) @@ -240,12 +168,8 @@ export default function BuyPage() { navigate("/product", { replace: true }) return } - const acct = randomChars(2) + randomDigits(4) - const pw = randomDigits(3) - setAccount(acct) - setPwd(pw) - setBatchAccount(randomChars(3)) - setBatchPwd(pw) + setAccount(randomChars(2) + randomDigits(4)) + setPwd(randomDigits(3)) }, [navigate, product]) if (!product) return null @@ -271,69 +195,63 @@ export default function BuyPage() { return total } - const singleTotal = clampMinPrice( + const total = clampMinPrice( product.price * displayConnectCount, displayConnectCount, ) - const batchTotal = clampMinPrice( - product.price * batchConnectCount * batchCount, - batchConnectCount * batchCount, - ) - const total = mode === "single" ? singleTotal : batchTotal - // 批量预览 - const batchPreview = (() => { - if (!batchAccount) return "" - const end = batchStart + batchCount - 1 - if (batchCount <= 4) { - const items: string[] = [] - for (let i = batchStart; i <= end; i++) { - items.push(`${batchAccount}${i}`) - } - return items.join(", ") - } - return `${batchAccount}${batchStart}, ${batchAccount}${batchStart + 1}, ${batchAccount}${batchStart + 2}...${batchAccount}${end}` - })() - - const handlePay = async () => { - if (mode === "single") { - if (!account.trim() || !pwd.trim()) { - toast.error("账号和密码不能为空") - return - } - } else { - if (!batchAccount.trim() || !batchPwd.trim()) { - toast.error("账号前缀和密码不能为空") - return - } - if (batchCount > 500) { - toast.error("一次最多注册500个") - return - } + // 测试套餐:免费领取 + const handleClaim = async () => { + if (!account.trim() || !pwd.trim()) { + toast.error("账号和密码不能为空") + return } setSubmitting(true) try { + const result = await createTestAccount({ + ProductId: product.productId, + PackageId: packageId, + Account: account.trim(), + Pwd: pwd.trim(), + }) + + if (result.success) { + toast.success("领取成功") + navigate("/admin/channels") + } else { + toast.error(result.message || "领取失败") + } + } catch { + toast.error("网络错误,请重试") + } finally { + setSubmitting(false) + } + } + + const handlePay = async () => { + if (!account.trim() || !pwd.trim()) { + toast.error("账号和密码不能为空") + return + } + + setSubmitting(true) + try { + // 支付类型/通道映射:1余额 100支付宝 70微信;30微信 50支付宝 70余额 const oPayType = Number(payType) - const useBalance = oPayType === 1 ? 1 : 0 - const payChannel = oPayType === 70 ? 30 : 50 + const useBalance = oPayType === 1 + const payChannel = oPayType === 1 ? 70 : oPayType === 70 ? 30 : 50 const params: CreateOrderRequest = { + Account: account.trim(), PackageId: packageId, - OrderType: mode === "single" ? 1 : 2, - Account: mode === "single" ? account.trim() : batchAccount.trim(), - Pwd: mode === "single" ? pwd.trim() : batchPwd.trim(), - ConnectCount: - mode === "single" ? displayConnectCount : batchConnectCount, + Pwd: pwd.trim(), + ConnectCount: displayConnectCount, + CouponAmount: 0, CouponId: 0, - UseAccountAmount: useBalance, OPayType: oPayType, PayChannel: payChannel, - Price: product.price, - ...(mode === "batch" && { - MinPostfix: batchStart, - MaxPostfix: batchCount, - }), + UseAccountAmount: useBalance, } const result = await createOrder(params) @@ -343,7 +261,6 @@ export default function BuyPage() { return } - // 参考 juipnet Product/buy.cshtml: // Data == "00" 为余额支付直接成功;否则需要微信/支付宝支付 const data = result.data if (typeof data === "string") { @@ -381,9 +298,6 @@ export default function BuyPage() { const handleBack = () => navigate(-1) - // 天卡首单(0.1元)不显示批量注册;天卡>0.1元时显示(juipnet:应付款!=0.1时显示批量tab) - const showTabs = !isTest && !isFirstOrderDayCard - return ( @@ -392,249 +306,162 @@ export default function BuyPage() {

{product.title} - ¥{total.toFixed(2)} + + {isTest ? "免费领取" : `¥${total.toFixed(2)}`} +

-

- 请务必选好所需物品,换货会产生费用 -

- {isDayCard && ( -

天卡不支持退款,请谨慎购买

+ {isTest ? ( +

+ 测试套餐免费领取,领取成功后可在 IP账号管理 中查看 +

+ ) : ( + <> +

+ 请务必选好所需物品,换货会产生费用 +

+ {isDayCard && ( +

+ 天卡不支持退款,请谨慎购买 +

+ )} + )}
- {/* Tab 切换 */} - {showTabs && ( -
- -
- )} -
- {mode === "single" ? ( - // ====== 单个注册 ====== - <> - {/* IP产品账号 */} -
- - setAccount(e.target.value)} - placeholder="4-10位字母或数字" - className="h-10 rounded-xl border-gray-200 w-full md:w-auto md:flex-1" - /> - - 4至10位字母或数字或组合 - -
- - {/* IP产品密码 */} -
- - setPwd(e.target.value)} - placeholder="1-10位字母或数字" - className="h-10 rounded-xl border-gray-200 w-full md:w-auto md:flex-1" - /> - - 1至10位字母或数字或组合 - -
- - {!isTest && ( - - )} - - ) : ( - // ====== 批量注册 ====== - <> - {/* 批量说明 */} - - - {/* IP账号前缀 */} -
- - setBatchAccount(e.target.value)} - placeholder="3-8位字母或数字" - className="h-10 rounded-xl border-gray-200 w-full md:w-auto md:flex-1" - /> - - 3至8位字母或数字或组合 - -
- - {/* 开始号 */} -
- - - setBatchStart(Number.parseInt(e.target.value, 10) || 1) - } - className="h-10 rounded-xl border-gray-200 w-full md:w-auto md:flex-1 md:max-w-32" - /> - - 本批次账号的起始账号 - -
- - {/* 注册个数 */} -
- - - setBatchCount( - Math.min(500, Number.parseInt(e.target.value, 10) || 1), - ) - } - className="h-10 rounded-xl border-gray-200 w-full md:w-auto md:flex-1 md:max-w-32" - /> -
- - {/* 账号预览 */} - {batchPreview && ( -
- - - {batchPreview} - -
- )} - - {/* IP产品密码 */} -
- - setBatchPwd(e.target.value)} - placeholder="1-10位字母或数字" - className="h-10 rounded-xl border-gray-200 w-full md:w-auto md:flex-1" - /> - - 1至10位字母或数字或组合 - -
- - {/* 单账号连接设备数 */} - - - )} - - {/* 优惠券 */} - {!isTest && } - - {/* 余额 */} + {/* IP产品账号 */}
-
- - 当前账户余额 - ¥10.00 - - -
+ setAccount(e.target.value)} + placeholder="4-10位字母或数字" + className="h-10 rounded-xl border-gray-200 w-full md:w-auto md:flex-1" + /> + + 4至10位字母或数字或组合 +
- {/* 支付方式 */} -
+ {/* IP产品密码 */} +
- -
- - -
-
- - -
-
- - -
-
+ setPwd(e.target.value)} + placeholder="1-10位字母或数字" + className="h-10 rounded-xl border-gray-200 w-full md:w-auto md:flex-1" + /> + + 1至10位字母或数字或组合 +
- {/* 总金额 / 应付款 */} -
-
- - - ¥{total.toFixed(2)} - -
-
- - - ¥{total.toFixed(2)} - -
-
+ {!isTest && ( + + )} + + {!isTest && ( + <> + {/* 优惠券 */} + + + {/* 余额 */} +
+ +
+ + 当前账户余额 + + ¥10.00 + + + +
+
+ + {/* 支付方式 */} +
+ + +
+ + +
+
+ + +
+
+ + +
+
+
+ + {/* 总金额 / 应付款 */} +
+
+ + + ¥{total.toFixed(2)} + +
+
+ + + ¥{total.toFixed(2)} + +
+
+ + )}
- + {isTest ? ( + + ) : ( + + )}
diff --git a/src/home/product/buyStatic.tsx b/src/home/product/buyStatic.tsx index 1058f6e..5705428 100644 --- a/src/home/product/buyStatic.tsx +++ b/src/home/product/buyStatic.tsx @@ -324,16 +324,15 @@ export default function BuyStaticPage() { try { const oPayType = Number(payType) const params: CreateOrderRequest = { - PackageId: packageId, - OrderType: 1, Account: account.trim(), + PackageId: packageId, Pwd: pwd.trim(), ConnectCount: 1, + CouponAmount: 0, CouponId: 0, - UseAccountAmount: oPayType === 1 ? 1 : 0, + UseAccountAmount: oPayType === 1, OPayType: oPayType, - PayChannel: oPayType === 70 ? 30 : 50, - Price: Number(getPrice()), + PayChannel: oPayType === 1 ? 70 : oPayType === 70 ? 30 : 50, } const result = await createOrder(params) @@ -353,7 +352,7 @@ export default function BuyStaticPage() { saveOrderPay({ orderNo: data.OrderInfo.OrderNo, amount: Number(data.OrderInfo.OtherPayAmount) || Number(getPrice()), - payChannel: oPayType === 70 ? 30 : 50, + payChannel: oPayType === 1 ? 70 : oPayType === 70 ? 30 : 50, payData: data.PayData, }) const win = window.open( diff --git a/src/home/product/productGrid.tsx b/src/home/product/productGrid.tsx index 666af5f..0a50940 100644 --- a/src/home/product/productGrid.tsx +++ b/src/home/product/productGrid.tsx @@ -88,7 +88,7 @@ export default function ProductGrid({ products }: ProductGridProps) { className="w-full text-blue-500 border-blue-400 hover:bg-blue-50" onClick={() => handleBuy(product)} > - 立即购买 + {product.isTest ? "免费领取" : "立即购买"} diff --git a/src/lib/models/order.ts b/src/lib/models/order.ts index c98336f..501869d 100644 --- a/src/lib/models/order.ts +++ b/src/lib/models/order.ts @@ -39,19 +39,24 @@ export type OrderItem = { DeleteTag: number } +// 产品购买(不含水滴) export type CreateOrderRequest = { - PackageId: number - OrderType: number Account: string + PackageId: number Pwd: string ConnectCount: number - MinPostfix?: number - MaxPostfix?: number - CouponId?: number - UseAccountAmount?: number + // 优惠券减免金额 + CouponAmount: number + // 优惠券ID + CouponId: number + // 支付类型:1余额 100支付宝 70微信 OPayType: number - PayChannel?: number - Price?: number + // 支付通道:30微信 50支付宝 70余额 + PayChannel: number + // 是否使用余额 + UseAccountAmount: boolean + // 代理下用户会员号(可选) + phone?: string } export type CreateTestAccountRequest = {