http-预储存/短效无限量/短效包天
This commit is contained in:
149
src/api/http.ts
149
src/api/http.ts
@@ -1,6 +1,8 @@
|
||||
import type { ApiResponse } from "@/lib/api"
|
||||
import type {
|
||||
CreateOrderResult,
|
||||
DxblData,
|
||||
DxbtData,
|
||||
DxwxlData,
|
||||
GameCity,
|
||||
GameItem,
|
||||
@@ -31,28 +33,68 @@ const DEFAULT_TIMEOUT = 30_000
|
||||
|
||||
type RawHttpResponse = Record<string, unknown>
|
||||
|
||||
// ==================== 统一 PHP 请求底层 ====================
|
||||
// 所有 /http/* 接口都带 cookie 走同一出口:超时、错误归一化在此层处理,
|
||||
// 各接口函数只关心业务数据,不再各自实现 fetch。
|
||||
// credentials 仅对需要登录态的 POST 开启;公开脚本 GET 接口(如线路表)
|
||||
// 不带 cookie,避免跨域时服务端 Access-Control-Allow-Origin 配置不匹配。
|
||||
|
||||
type PhpFetchOptions = {
|
||||
credentials?: RequestCredentials
|
||||
cache?: RequestCache
|
||||
encoding?: "json" | "form"
|
||||
}
|
||||
|
||||
// 表单编码:嵌套对象/数组展开为 data[key] 结构(PHP $_POST 规则)
|
||||
function toFormParams(params: URLSearchParams, prefix: string, value: unknown) {
|
||||
if (value === null || value === undefined) return
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((v, i) => {
|
||||
toFormParams(params, `${prefix}[${i}]`, v)
|
||||
})
|
||||
return
|
||||
}
|
||||
if (typeof value === "object") {
|
||||
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
||||
toFormParams(params, `${prefix}[${k}]`, v)
|
||||
}
|
||||
return
|
||||
}
|
||||
params.set(prefix, String(value))
|
||||
}
|
||||
|
||||
export async function phpFetch<T = RawHttpResponse>(
|
||||
method: "GET" | "POST",
|
||||
path: string,
|
||||
body?: unknown,
|
||||
options: { credentials?: RequestCredentials } = {},
|
||||
options: PhpFetchOptions = {},
|
||||
): Promise<ApiResponse<T>> {
|
||||
const { credentials = "same-origin" } = options
|
||||
const { credentials = "same-origin", cache, encoding = "json" } = options
|
||||
const isForm = encoding === "form" && body !== undefined
|
||||
const controller = new AbortController()
|
||||
const timer = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT)
|
||||
try {
|
||||
let payload: string | undefined
|
||||
if (body !== undefined) {
|
||||
if (isForm) {
|
||||
const params = new URLSearchParams()
|
||||
for (const [k, v] of Object.entries(body as Record<string, unknown>)) {
|
||||
toFormParams(params, k, v)
|
||||
}
|
||||
payload = params.toString()
|
||||
} else {
|
||||
payload = JSON.stringify(body)
|
||||
}
|
||||
}
|
||||
const response = await fetch(`${PHP_API_BASE_URL}${path}`, {
|
||||
method,
|
||||
headers:
|
||||
body !== undefined ? { "Content-Type": "application/json" } : undefined,
|
||||
headers: isForm
|
||||
? { "Content-Type": "application/x-www-form-urlencoded" }
|
||||
: body !== undefined
|
||||
? { "Content-Type": "application/json" }
|
||||
: undefined,
|
||||
credentials,
|
||||
body: body !== undefined ? JSON.stringify(body) : undefined,
|
||||
...(cache ? { cache } : {}),
|
||||
body: payload,
|
||||
signal: controller.signal,
|
||||
})
|
||||
if (!response.ok) {
|
||||
@@ -87,8 +129,9 @@ export async function phpFetch<T = RawHttpResponse>(
|
||||
export async function phpPost<T = RawHttpResponse>(
|
||||
path: string,
|
||||
body?: unknown,
|
||||
options: PhpFetchOptions = {},
|
||||
): Promise<ApiResponse<T>> {
|
||||
return phpFetch<T>("POST", path, body, { credentials: "include" })
|
||||
return phpFetch<T>("POST", path, body, { credentials: "include", ...options })
|
||||
}
|
||||
|
||||
// multipart 上传(手持身份证等文件场景),不设 Content-Type,由浏览器自动补全 boundary
|
||||
@@ -140,7 +183,7 @@ export async function phpGet<T = RawHttpResponse>(
|
||||
|
||||
// ==================== 响应解析工具 ====================
|
||||
|
||||
// 老 PHP 接口习惯把业务数据包在 d 字段里
|
||||
// 接口习惯把业务数据包在 d 字段里
|
||||
export function unwrapD(
|
||||
raw: RawHttpResponse | undefined,
|
||||
): Record<string, unknown> {
|
||||
@@ -177,7 +220,9 @@ export function parsePager(raw: unknown, itemCount: number): Pager {
|
||||
export async function fetchGameCities(
|
||||
game: GameOrderData,
|
||||
): Promise<ApiResponse<GameCity[]>> {
|
||||
const res = await phpPost<RawHttpResponse>("/http/product/city", game)
|
||||
const res = await phpPost<RawHttpResponse>("/http/product/city", game, {
|
||||
encoding: "form",
|
||||
})
|
||||
if (!res.success) return res
|
||||
const cities = unwrapD(res.data).cities
|
||||
return {
|
||||
@@ -189,7 +234,9 @@ export async function fetchGameCities(
|
||||
export async function fetchGameList(
|
||||
game: GameOrderData,
|
||||
): Promise<ApiResponse<GameItem[]>> {
|
||||
const res = await phpPost<RawHttpResponse>("/http/product/game", game)
|
||||
const res = await phpPost<RawHttpResponse>("/http/product/game", game, {
|
||||
encoding: "form",
|
||||
})
|
||||
if (!res.success) return res
|
||||
const games = unwrapD(res.data).games
|
||||
return {
|
||||
@@ -201,7 +248,9 @@ export async function fetchGameList(
|
||||
export async function fetchGameLineCount(
|
||||
game: GameOrderData,
|
||||
): Promise<ApiResponse<number>> {
|
||||
const res = await phpPost<RawHttpResponse>("/http/product/linecount", game)
|
||||
const res = await phpPost<RawHttpResponse>("/http/product/linecount", game, {
|
||||
encoding: "form",
|
||||
})
|
||||
if (!res.success) return res
|
||||
const count = unwrapD(res.data).count
|
||||
return { success: true, data: toNumber(count) }
|
||||
@@ -216,37 +265,83 @@ export async function calcGamePrice(
|
||||
export async function calcUnlimitedPrice(
|
||||
data: DxwxlData,
|
||||
): Promise<ApiResponse<number>> {
|
||||
const res = await phpPost<RawHttpResponse>("/http/product/calc_price", {
|
||||
order_type: 2,
|
||||
money: 1,
|
||||
pay_type: 1,
|
||||
data,
|
||||
})
|
||||
const res = await phpPost<RawHttpResponse>(
|
||||
"/http/product/calc_price",
|
||||
{
|
||||
order_type: 2,
|
||||
money: 1,
|
||||
pay_type: 1,
|
||||
data,
|
||||
},
|
||||
{ encoding: "form" },
|
||||
)
|
||||
if (!res.success) return res
|
||||
const raw = res.data ?? {}
|
||||
const priceValue =
|
||||
typeof raw.price === "number" ? raw.price : unwrapD(raw).price
|
||||
const d = unwrapD(raw)
|
||||
return { success: true, data: toNumber(raw.price ?? d.price) }
|
||||
}
|
||||
|
||||
// calc_price 原始返回:每 ipAmount 个 IP 价格为 price
|
||||
export type CalcPriceResult = {
|
||||
ipAmount: number
|
||||
price: number
|
||||
}
|
||||
|
||||
async function calcPriceWithIp(
|
||||
orderType: number,
|
||||
data: unknown,
|
||||
): Promise<ApiResponse<CalcPriceResult>> {
|
||||
const res = await phpPost<RawHttpResponse>(
|
||||
"/http/product/calc_price",
|
||||
{
|
||||
order_type: orderType,
|
||||
money: 1,
|
||||
pay_type: 1,
|
||||
data,
|
||||
},
|
||||
{ encoding: "form" },
|
||||
)
|
||||
if (!res.success) return res
|
||||
const raw = res.data ?? {}
|
||||
const d = unwrapD(raw)
|
||||
const rawIp = raw.ipAmount ?? d.ipAmount
|
||||
// ipAmount 为空时按 1 处理(每 1 个 IP 价格为 price)
|
||||
return {
|
||||
success: true,
|
||||
data: typeof priceValue === "number" ? priceValue : 0,
|
||||
data: {
|
||||
ipAmount: rawIp === null || rawIp === undefined ? 1 : toNumber(rawIp),
|
||||
price: toNumber(raw.price ?? d.price),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 短效包天:返回每 IP 每天单价(总价 = periodAmount * price * ipAmount)
|
||||
export async function calcDxbtPrice(
|
||||
data: DxbtData,
|
||||
): Promise<ApiResponse<CalcPriceResult>> {
|
||||
return calcPriceWithIp(3, data)
|
||||
}
|
||||
|
||||
// 短效包量:返回每 1 元可购 IP 数量(总 IP = money * ipAmount)
|
||||
export async function calcDxblPrice(
|
||||
data: DxblData,
|
||||
): Promise<ApiResponse<CalcPriceResult>> {
|
||||
return calcPriceWithIp(4, data)
|
||||
}
|
||||
|
||||
export async function calcOrderPrice(
|
||||
orderInfo: PackageOrderInfo,
|
||||
): Promise<ApiResponse<number>> {
|
||||
const res = await phpPost<RawHttpResponse>(
|
||||
"/http/product/calc_price",
|
||||
orderInfo,
|
||||
{ encoding: "form" },
|
||||
)
|
||||
if (!res.success) return res
|
||||
const raw = res.data ?? {}
|
||||
const priceValue =
|
||||
typeof raw.price === "number" ? raw.price : unwrapD(raw).price
|
||||
return {
|
||||
success: true,
|
||||
data: typeof priceValue === "number" ? priceValue : 0,
|
||||
}
|
||||
const d = unwrapD(raw)
|
||||
// price 可能为字符串(如 "9.9000"),统一 toNumber 解析
|
||||
return { success: true, data: toNumber(raw.price ?? d.price) }
|
||||
}
|
||||
|
||||
// ==================== 订单 ====================
|
||||
@@ -390,7 +485,6 @@ export async function fetchCxOrders(): Promise<ApiResponse<CxOrder[]>> {
|
||||
export async function fetchLangIpList(
|
||||
query: Record<string, unknown>,
|
||||
): Promise<ApiResponse<LangIpListResult>> {
|
||||
|
||||
const res = await phpPost<RawHttpResponse>("/http/user/lang_ip_list", {
|
||||
cookie: document.cookie,
|
||||
...query,
|
||||
@@ -407,7 +501,6 @@ export async function fetchLangIpList(
|
||||
export async function fetchSwitchList(
|
||||
query: Record<string, unknown>,
|
||||
): Promise<ApiResponse<SwitchListResult>> {
|
||||
|
||||
const res = await phpPost<RawHttpResponse>("/http/user/cx_switch", {
|
||||
cookie: document.cookie,
|
||||
...query,
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import type { ApiResponse } from "@/lib/api"
|
||||
import type {
|
||||
ShuidiGame,
|
||||
ShuidiOrderInfo,
|
||||
ShuidiOrderResult,
|
||||
ShuidiPackageList,
|
||||
ShuidiRegion,
|
||||
} from "@/lib/models/shuidi"
|
||||
import { phpFetch, phpGet, phpPost } from "./http"
|
||||
import { phpFetch, phpPost } from "./http"
|
||||
|
||||
// ==================== 水滴静态/固态(jinyou) ====================
|
||||
// - 游戏列表:POST /jinyou/api/gameList(不带 cookie)
|
||||
@@ -63,8 +65,12 @@ export async function fetchShuidiRegions(params: {
|
||||
is_unshar: String(params.is_unshar),
|
||||
game_id: String(params.game_id),
|
||||
})
|
||||
const res = await phpGet<JinyouResult<{ region?: ShuidiRegion }>>(
|
||||
// 余量数据时效性强,禁止浏览器缓存
|
||||
const res = await phpFetch<JinyouResult<{ region?: ShuidiRegion }>>(
|
||||
"GET",
|
||||
`/jinyou/api/regionSurplusOuts?${query.toString()}`,
|
||||
undefined,
|
||||
{ cache: "no-store" },
|
||||
)
|
||||
if (!res.success) return res
|
||||
const region = res.data?.data?.region
|
||||
@@ -82,3 +88,26 @@ export async function fetchShuidiRegions(params: {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 下单 ====================
|
||||
// 与 juipnet Index.cshtml pay() 一致:body 为 { cookie, order_info }
|
||||
// code 1-开通成功 2-返回支付宝支付主体 3-返回微信支付主体 -1-错误
|
||||
|
||||
export async function createShuidiOrder(
|
||||
orderInfo: ShuidiOrderInfo,
|
||||
): Promise<ApiResponse<ShuidiOrderResult>> {
|
||||
const res = await phpPost<Record<string, unknown>>(
|
||||
"/jinyou/jinyoujt/create_order",
|
||||
{ cookie: document.cookie, order_info: orderInfo },
|
||||
)
|
||||
if (!res.success) return res
|
||||
const raw = res.data ?? {}
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
code: Number(raw.code) || 0,
|
||||
msg: typeof raw.msg === "string" ? raw.msg : undefined,
|
||||
data: typeof raw.data === "string" ? raw.data : undefined,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,25 @@
|
||||
import { ChevronDownIcon } from "lucide-react"
|
||||
import { Popover } from "radix-ui"
|
||||
import { useCallback, useEffect, useMemo, useState } from "react"
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
import { useLocation, useNavigate } from "react-router-dom"
|
||||
import { toast } from "sonner"
|
||||
import { fetchApiIndex } from "@/api/dashboard"
|
||||
import {
|
||||
createShuidiOrder,
|
||||
fetchShuidiGameList,
|
||||
fetchShuidiPackages,
|
||||
fetchShuidiRegions,
|
||||
} from "@/api/shuidi"
|
||||
import { QrCodeImage } from "@/components/qrCodeImage"
|
||||
import RealNameCheck from "@/components/realNameCheck"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
||||
@@ -17,6 +27,7 @@ import Wrap from "@/components/wrap"
|
||||
import type {
|
||||
ShuidiCity,
|
||||
ShuidiGame,
|
||||
ShuidiOrderInfo,
|
||||
ShuidiPackage,
|
||||
ShuidiPackageList,
|
||||
ShuidiRegion,
|
||||
@@ -305,7 +316,14 @@ export default function BuyStaticPage() {
|
||||
const [payType, setPayType] = useState("100")
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
|
||||
// 初始加载:游戏列表 + 静态套餐(type=1),并生成随机账号密码
|
||||
// 微信支付二维码弹窗
|
||||
const [wechatOpen, setWechatOpen] = useState(false)
|
||||
const [wechatPayUrl, setWechatPayUrl] = useState("")
|
||||
|
||||
// 账户余额(.NET /user/ApiIndex -> UserModel.RestAmount)
|
||||
const [restAmount, setRestAmount] = useState(0)
|
||||
|
||||
// 初始加载:游戏列表 + 静态套餐(type=1)+ 账户余额,并生成随机账号密码
|
||||
useEffect(() => {
|
||||
setAccount(randomChars(2) + randomDigits(4))
|
||||
setPwd(randomDigits(3))
|
||||
@@ -318,16 +336,27 @@ export default function BuyStaticPage() {
|
||||
if (res.success) setPackages(res.data)
|
||||
else toast.error(res.message)
|
||||
})
|
||||
fetchApiIndex().then(res => {
|
||||
if (!res.success || !res.data) return
|
||||
const amount = res.data.UserModel?.RestAmount
|
||||
if (typeof amount === "number" && Number.isFinite(amount)) {
|
||||
setRestAmount(amount)
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
// 线路列表:跟随独享类型/项目变化重新拉取(与 juipnet select_game 逻辑一致)
|
||||
// 线路列表:跟随独享类型/项目变化重新拉取
|
||||
// 序列号防止快速切换时旧响应覆盖新数据
|
||||
const regionSeqRef = useRef(0)
|
||||
const loadRegions = useCallback(async () => {
|
||||
const seq = ++regionSeqRef.current
|
||||
setRegionLoading(true)
|
||||
const res = await fetchShuidiRegions({
|
||||
product_id: PRODUCT_ID,
|
||||
is_unshar: type === "project" ? 0 : 1,
|
||||
game_id: type === "project" ? gameId : "",
|
||||
})
|
||||
if (seq !== regionSeqRef.current) return
|
||||
if (res.success) {
|
||||
setRegion(res.data)
|
||||
const codes = Object.keys(res.data.province_surplus_outs)
|
||||
@@ -346,7 +375,7 @@ export default function BuyStaticPage() {
|
||||
loadRegions()
|
||||
}, [loadRegions])
|
||||
|
||||
// 切换项目独享/专线独享:重置套餐与分配选择(与 juipnet game()/line() 逻辑一致)
|
||||
// 切换项目独享/专线独享:重置套餐与分配选择
|
||||
const handleTypeChange = (t: "project" | "line") => {
|
||||
if (t === type) return
|
||||
setType(t)
|
||||
@@ -363,6 +392,24 @@ export default function BuyStaticPage() {
|
||||
[region],
|
||||
)
|
||||
|
||||
// 可用节点总数:优先接口返回的 surplus_out_total,缺失时按城市余量求和兜底
|
||||
const surplusOutTotal = useMemo(() => {
|
||||
if (!region) return 0
|
||||
if (
|
||||
typeof region.surplus_out_total === "number" &&
|
||||
region.surplus_out_total >= 0
|
||||
) {
|
||||
return region.surplus_out_total
|
||||
}
|
||||
return provinceCodes.reduce((sum, code) => {
|
||||
const cities = region.province_surplus_outs[code]?.citys ?? []
|
||||
return (
|
||||
sum +
|
||||
cities.reduce((s, c) => s + (Number.isFinite(c.total) ? c.total : 0), 0)
|
||||
)
|
||||
}, 0)
|
||||
}, [region, provinceCodes])
|
||||
|
||||
// 省市导航行:按拼音首字母 + 名称排序,附带字母索引
|
||||
const provinceRows = useMemo(() => {
|
||||
if (!region) return []
|
||||
@@ -407,8 +454,8 @@ export default function BuyStaticPage() {
|
||||
() =>
|
||||
keyword
|
||||
? provinceRows.filter(r =>
|
||||
r.province.en_name.toLowerCase().includes(keyword),
|
||||
)
|
||||
r.province.en_name.toLowerCase().includes(keyword),
|
||||
)
|
||||
: provinceRows,
|
||||
[provinceRows, keyword],
|
||||
)
|
||||
@@ -511,9 +558,9 @@ export default function BuyStaticPage() {
|
||||
setBuyNum(Math.max(Math.floor(value), sum))
|
||||
}
|
||||
|
||||
// 校验与 juipnet pay_page 一致;下单接口暂未提供,校验通过后提示联系客服
|
||||
// 校验与 juipnet pay_page 一致;下单与 juipnet pay() 一致
|
||||
const handlePay = async () => {
|
||||
if ((region?.surplus_out_total ?? 0) < buyNum) {
|
||||
if (surplusOutTotal < buyNum) {
|
||||
toast.error("节点数量不足!")
|
||||
return
|
||||
}
|
||||
@@ -533,9 +580,60 @@ export default function BuyStaticPage() {
|
||||
toast.error("密码为1-10位字母或数字!")
|
||||
return
|
||||
}
|
||||
|
||||
// product_info:省-市-rid-100 -> 数量(字符串),未选择时为空数组
|
||||
const productInfo: Record<string, string> = {}
|
||||
for (const [key, count] of Object.entries(cityCounts)) {
|
||||
productInfo[key] = String(count)
|
||||
}
|
||||
|
||||
// 免费(价格0)时固定余额支付,与 juipnet pay_page 一致
|
||||
const orderInfo: ShuidiOrderInfo = {
|
||||
pay_type: totalPrice <= 0 ? 10 : Number(payType),
|
||||
type: 0,
|
||||
package_id: packageId,
|
||||
game_id: type === "project" ? gameId : "",
|
||||
buy_num: buyNum,
|
||||
price: totalPrice,
|
||||
account: account.trim(),
|
||||
password: pwd.trim(),
|
||||
product_info: Object.keys(productInfo).length > 0 ? productInfo : [],
|
||||
}
|
||||
|
||||
setSubmitting(true)
|
||||
try {
|
||||
toast.info("该产品暂未开放线上购买,请联系客服购买")
|
||||
const result = await createShuidiOrder(orderInfo)
|
||||
if (!result.success) {
|
||||
toast.error(result.message)
|
||||
return
|
||||
}
|
||||
const data = result.data
|
||||
if (data.code === 1) {
|
||||
toast.success(data.msg || "账号开通成功!")
|
||||
navigate("/admin/channels")
|
||||
} else if (data.code === 2 && data.data) {
|
||||
// 支付宝:data 为支付宝支付主体(HTML 表单),挂载后自动提交
|
||||
const container = document.createElement("div")
|
||||
container.innerHTML = data.data
|
||||
const form = container.querySelector("form")
|
||||
if (form) {
|
||||
form.style.display = "none"
|
||||
document.body.appendChild(form)
|
||||
form.submit()
|
||||
} else if (/^https?:\/\//i.test(data.data)) {
|
||||
window.open(data.data, "_blank", "noopener,noreferrer")
|
||||
} else {
|
||||
toast.error("支付宝支付发起失败,请重试")
|
||||
}
|
||||
} else if (data.code === 3 && data.data) {
|
||||
// 微信:data 为支付二维码内容
|
||||
setWechatPayUrl(data.data)
|
||||
setWechatOpen(true)
|
||||
} else {
|
||||
toast.error(data.msg || "下单失败,请重试")
|
||||
}
|
||||
} catch {
|
||||
toast.error("网络错误,请重试")
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
@@ -637,7 +735,7 @@ export default function BuyStaticPage() {
|
||||
<p className="text-sm text-gray-600">
|
||||
可用节点总数:
|
||||
<span className="font-bold text-blue-600 ml-1">
|
||||
{region?.surplus_out_total ?? 0}
|
||||
{surplusOutTotal}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
@@ -899,7 +997,9 @@ export default function BuyStaticPage() {
|
||||
<div className="flex items-center gap-2 w-full md:w-auto md:flex-1">
|
||||
<span className="text-sm text-gray-600">
|
||||
当前账户余额
|
||||
<span className="font-bold text-orange-500 ml-1">¥28.00</span>
|
||||
<span className="font-bold text-orange-500 ml-1">
|
||||
¥{restAmount.toFixed(2)}
|
||||
</span>
|
||||
</span>
|
||||
<Button
|
||||
variant="link"
|
||||
@@ -942,7 +1042,7 @@ export default function BuyStaticPage() {
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<RadioGroupItem value="1" id="bs-balance" />
|
||||
<RadioGroupItem value="10" id="bs-balance" />
|
||||
<Label
|
||||
htmlFor="bs-balance"
|
||||
className="cursor-pointer text-sm text-gray-700 flex items-center gap-1.5"
|
||||
@@ -972,6 +1072,40 @@ export default function BuyStaticPage() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 微信扫码支付弹窗(与 juipnet staticBackdrop 一致) */}
|
||||
<Dialog open={wechatOpen} onOpenChange={setWechatOpen}>
|
||||
<DialogContent className="sm:max-w-sm">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-center">微信扫码支付</DialogTitle>
|
||||
<DialogDescription className="text-center">
|
||||
请使用微信扫描下方二维码完成支付
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
{wechatPayUrl && (
|
||||
<div className="rounded-lg border border-gray-200 p-2">
|
||||
<QrCodeImage text={wechatPayUrl} size={220} />
|
||||
</div>
|
||||
)}
|
||||
<p className="text-sm text-gray-600">
|
||||
应付金额:
|
||||
<span className="font-bold text-orange-500 ml-1">
|
||||
¥{totalPrice.toFixed(2)}
|
||||
</span>
|
||||
</p>
|
||||
<Button
|
||||
className="w-full bg-blue-600 hover:bg-blue-700"
|
||||
onClick={() => {
|
||||
setWechatOpen(false)
|
||||
navigate("/admin/channels")
|
||||
}}
|
||||
>
|
||||
我已付款
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</Wrap>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { Toaster, toast } from "sonner"
|
||||
import { fetchApiIndex } from "@/api/dashboard"
|
||||
import {
|
||||
calcDxblPrice,
|
||||
calcDxbtPrice,
|
||||
calcGamePrice,
|
||||
calcUnlimitedPrice,
|
||||
createHttpOrder,
|
||||
@@ -69,6 +72,13 @@ const PERIOD_OPTIONS = [
|
||||
{ value: "90", label: "按季" },
|
||||
]
|
||||
|
||||
const PERIOD_UNIT: Record<string, string> = {
|
||||
"1": "天",
|
||||
"7": "周",
|
||||
"30": "月",
|
||||
"90": "季",
|
||||
}
|
||||
|
||||
// 短效无限量:周期含按小时(1060),默认按小时
|
||||
const UNLIMITED_PERIOD_OPTIONS = [
|
||||
{ value: "1060", label: "按小时", unit: "小时" },
|
||||
@@ -139,14 +149,29 @@ export default function HttpPage() {
|
||||
const [volumeAmount, setVolumeAmount] = useState(1)
|
||||
|
||||
// 短效无限量:周期类型/购买时长/白名单数量 + 服务端单价
|
||||
const [unlimitedPeriod, setUnlimitedPeriod] = useState(
|
||||
UNLIMITED_PERIOD_OPTIONS[0].value,
|
||||
)
|
||||
// 默认"按天":当前后端不支持按小时档位(返回默认桩价格),按天/周/月/季均为有效档位
|
||||
const [unlimitedPeriod, setUnlimitedPeriod] = useState("1")
|
||||
const [unlimitedPeriodCount, setUnlimitedPeriodCount] = useState(1)
|
||||
const [unlimitedUnitPrice, setUnlimitedUnitPrice] = useState(0)
|
||||
const [unlimitedPriceLoading, setUnlimitedPriceLoading] = useState(false)
|
||||
const unlimitedPriceSeqRef = useRef(0)
|
||||
|
||||
// 短效包天:服务端每IP每天单价
|
||||
const [dxbtUnitPrice, setDxbtUnitPrice] = useState(0)
|
||||
const [dxbtPriceLoading, setDxbtPriceLoading] = useState(false)
|
||||
const dxbtPriceSeqRef = useRef(0)
|
||||
|
||||
// 短效包量:服务端每1元可购IP数量
|
||||
const [dxblIpsPerYuan, setDxblIpsPerYuan] = useState(0)
|
||||
const [dxblPriceLoading, setDxblPriceLoading] = useState(false)
|
||||
const dxblPriceSeqRef = useRef(0)
|
||||
|
||||
// 短效包天/包量 H币支付弹窗
|
||||
const [dxPayOpen, setDxPayOpen] = useState(false)
|
||||
const [dxPayKind, setDxPayKind] = useState<"daily" | "volume">("daily")
|
||||
const [dxPaying, setDxPaying] = useState(false)
|
||||
const [dxBalance, setDxBalance] = useState(0)
|
||||
|
||||
// 长效游戏
|
||||
const [game, _setGame] = useState<GameOrderData>(DEFAULT_GAME)
|
||||
const gameRef = useRef(game)
|
||||
@@ -158,6 +183,9 @@ export default function HttpPage() {
|
||||
const [gamePriceLoading, setGamePriceLoading] = useState(false)
|
||||
const gameSeqRef = useRef(0)
|
||||
|
||||
// IP数量滑杆:拖动即时更新数值,停止 300ms 后才触发算价(防抖)
|
||||
const ipSliderTimerRef = useRef<number | null>(null)
|
||||
|
||||
// 支付弹窗状态
|
||||
const [payDialogOpen, setPayDialogOpen] = useState(false)
|
||||
const [payAmount, setPayAmount] = useState(0)
|
||||
@@ -307,6 +335,33 @@ export default function HttpPage() {
|
||||
applyGame({ ...gameRef.current, gameId })
|
||||
}
|
||||
|
||||
// IP数量滑杆变化:即时更新显示,防抖后重新算价
|
||||
const handleIpSliderChange = (value: number) => {
|
||||
setGame({ ...gameRef.current, ipAmount: value })
|
||||
if (ipSliderTimerRef.current !== null) {
|
||||
window.clearTimeout(ipSliderTimerRef.current)
|
||||
}
|
||||
ipSliderTimerRef.current = window.setTimeout(() => {
|
||||
ipSliderTimerRef.current = null
|
||||
const seq = gameSeqRef.current + 1
|
||||
gameSeqRef.current = seq
|
||||
loadGamePrice(gameRef.current, seq)
|
||||
}, 300)
|
||||
}
|
||||
|
||||
// H币余额:优先走 .NET /user/ApiIndex 的 ju_money(登录会话内稳定可用),
|
||||
// 失败时回退 php-api 的 get_balance(依赖 php-api 域名下的 cookie)
|
||||
const loadHCoinBalance = async (): Promise<number | null> => {
|
||||
const apiRes = await fetchApiIndex()
|
||||
if (apiRes.success && apiRes.data) {
|
||||
const m = apiRes.data.UserModel?.ju_money
|
||||
if (typeof m === "number" && Number.isFinite(m)) return m
|
||||
}
|
||||
const phpRes = await fetchHttpBalance()
|
||||
if (phpRes.success) return phpRes.data
|
||||
return null
|
||||
}
|
||||
|
||||
const openGamePay = async () => {
|
||||
if (!ensureLogin()) return
|
||||
if (lineCountRef.current <= 0) {
|
||||
@@ -317,12 +372,12 @@ export default function HttpPage() {
|
||||
toast.error("价格获取失败,请稍后重试")
|
||||
return
|
||||
}
|
||||
const res = await fetchHttpBalance()
|
||||
if (!res.success) {
|
||||
toast.error(res.message)
|
||||
const hCoin = await loadHCoinBalance()
|
||||
if (hCoin === null) {
|
||||
toast.error("余额获取失败,请稍后重试")
|
||||
return
|
||||
}
|
||||
setBalance(res.data)
|
||||
setBalance(hCoin)
|
||||
setGamePayOpen(true)
|
||||
}
|
||||
|
||||
@@ -366,19 +421,19 @@ export default function HttpPage() {
|
||||
setGamePaying(false)
|
||||
}
|
||||
|
||||
// 短效无限量:打开 H币 支付弹窗(与 juipnet 一致)
|
||||
// 短效无限量:打开 H币 支付弹窗
|
||||
const openUnlimitedPay = async () => {
|
||||
if (!ensureLogin()) return
|
||||
if (unlimitedTotal <= 0) {
|
||||
toast.error("价格获取失败,请稍后重试")
|
||||
return
|
||||
}
|
||||
const res = await fetchHttpBalance()
|
||||
if (!res.success) {
|
||||
toast.error(res.message)
|
||||
const hCoin = await loadHCoinBalance()
|
||||
if (hCoin === null) {
|
||||
toast.error("余额获取失败,请稍后重试")
|
||||
return
|
||||
}
|
||||
setBalance(res.data)
|
||||
setBalance(hCoin)
|
||||
setUnlimitedPayOpen(true)
|
||||
}
|
||||
|
||||
@@ -427,6 +482,85 @@ export default function HttpPage() {
|
||||
setUnlimitedPaying(false)
|
||||
}
|
||||
|
||||
// 打开短效包天/包量 H币支付弹窗
|
||||
const openDxPay = async (kind: "daily" | "volume") => {
|
||||
if (!ensureLogin()) return
|
||||
if (kind === "daily") {
|
||||
if (effectiveDailyIpAmount < 500) {
|
||||
toast.error("ip数量最低500个")
|
||||
return
|
||||
}
|
||||
if (dxbtUnitPrice <= 0) {
|
||||
toast.error("价格计算中,请稍后重试")
|
||||
return
|
||||
}
|
||||
} else if (effectiveVolumeAmount <= 0) {
|
||||
toast.error("请输入有效的充值金额")
|
||||
return
|
||||
}
|
||||
setDxPayKind(kind)
|
||||
setDxPayOpen(true)
|
||||
const hCoin = await loadHCoinBalance()
|
||||
if (hCoin !== null) setDxBalance(hCoin)
|
||||
}
|
||||
|
||||
// 短效包天/包量下单(H币支付,与 payUnlimitedOrder 一致)
|
||||
const confirmDxPay = async () => {
|
||||
setDxPaying(true)
|
||||
const orderInfo =
|
||||
dxPayKind === "daily"
|
||||
? {
|
||||
order_type: 3,
|
||||
money: dxbtTotal,
|
||||
pay_type: 1,
|
||||
data: {
|
||||
durationType: Number(duration),
|
||||
ipAmount: effectiveDailyIpAmount,
|
||||
periodType: Number(period),
|
||||
periodAmount: periodCount,
|
||||
},
|
||||
}
|
||||
: {
|
||||
order_type: 4,
|
||||
money: effectiveVolumeAmount,
|
||||
pay_type: 1,
|
||||
data: {
|
||||
durationType: Number(duration),
|
||||
ipAmount: 500,
|
||||
},
|
||||
}
|
||||
const res = await createHttpOrder(orderInfo)
|
||||
if (!res.success) {
|
||||
toast.error(res.message)
|
||||
setDxPaying(false)
|
||||
return
|
||||
}
|
||||
const result = res.data
|
||||
if (result.code === 1) {
|
||||
setDxPayOpen(false)
|
||||
toast.success(result.msg || "支付成功")
|
||||
} else if (result.code === 2) {
|
||||
if (result.data && alipayBoxRef.current) {
|
||||
alipayBoxRef.current.innerHTML = result.data
|
||||
alipayBoxRef.current.querySelector("form")?.submit()
|
||||
setDxPayOpen(false)
|
||||
} else {
|
||||
toast.error(result.msg || "支付宝支付数据异常")
|
||||
}
|
||||
} else if (result.code === 3) {
|
||||
if (result.data) {
|
||||
setQrContent(result.data)
|
||||
setQrCodeOpen(true)
|
||||
setDxPayOpen(false)
|
||||
} else {
|
||||
toast.error(result.msg || "微信支付数据异常")
|
||||
}
|
||||
} else {
|
||||
toast.error(result.msg || "支付失败")
|
||||
}
|
||||
setDxPaying(false)
|
||||
}
|
||||
|
||||
const handleTabClick = (id: string) => {
|
||||
setActiveTab(id)
|
||||
if (id === "game") {
|
||||
@@ -439,7 +573,7 @@ export default function HttpPage() {
|
||||
}
|
||||
}
|
||||
|
||||
// 短效无限量:向服务端查询单价并计算总价(与 juipnet 一致)
|
||||
// 短效无限量:向服务端查询单价并计算总价
|
||||
useEffect(() => {
|
||||
if (activeTab !== "unlimited") return
|
||||
const seq = ++unlimitedPriceSeqRef.current
|
||||
@@ -470,6 +604,66 @@ export default function HttpPage() {
|
||||
).toFixed(2),
|
||||
)
|
||||
|
||||
// 短效包天有效IP数 / 短效包量有效金额(非法输入回退默认值)
|
||||
const parsedCustomIp = Number(customIpAmount)
|
||||
const effectiveDailyIpAmount =
|
||||
customIpAmount !== "" &&
|
||||
Number.isFinite(parsedCustomIp) &&
|
||||
parsedCustomIp > 0
|
||||
? parsedCustomIp
|
||||
: ipAmount
|
||||
const parsedCustomAmount = Number(customAmount)
|
||||
const effectiveVolumeAmount =
|
||||
customAmount !== "" &&
|
||||
Number.isFinite(parsedCustomAmount) &&
|
||||
parsedCustomAmount > 0
|
||||
? parsedCustomAmount
|
||||
: volumeAmount
|
||||
|
||||
// 短效包天:向服务端查询每IP每天单价
|
||||
useEffect(() => {
|
||||
if (activeTab !== "daily") return
|
||||
const seq = ++dxbtPriceSeqRef.current
|
||||
setDxbtPriceLoading(true)
|
||||
calcDxbtPrice({
|
||||
durationType: Number(duration),
|
||||
ipAmount: effectiveDailyIpAmount,
|
||||
periodType: Number(period),
|
||||
periodAmount: periodCount,
|
||||
}).then(res => {
|
||||
if (seq !== dxbtPriceSeqRef.current) return
|
||||
setDxbtPriceLoading(false)
|
||||
if (!res.success) {
|
||||
toast.error(res.message)
|
||||
return
|
||||
}
|
||||
setDxbtUnitPrice(res.data.price)
|
||||
})
|
||||
}, [activeTab, duration, effectiveDailyIpAmount, period, periodCount])
|
||||
|
||||
// 短效包量:向服务端查询每1元可购IP数量
|
||||
useEffect(() => {
|
||||
if (activeTab !== "volume") return
|
||||
const seq = ++dxblPriceSeqRef.current
|
||||
setDxblPriceLoading(true)
|
||||
calcDxblPrice({ durationType: Number(duration), ipAmount: 500 }).then(
|
||||
res => {
|
||||
if (seq !== dxblPriceSeqRef.current) return
|
||||
setDxblPriceLoading(false)
|
||||
if (!res.success) {
|
||||
toast.error(res.message)
|
||||
return
|
||||
}
|
||||
setDxblIpsPerYuan(res.data.ipAmount)
|
||||
},
|
||||
)
|
||||
}, [activeTab, duration])
|
||||
|
||||
// 短效包天总价:periodAmount * price * ipAmount(文档公式)
|
||||
const dxbtTotal = Number(
|
||||
(periodCount * dxbtUnitPrice * effectiveDailyIpAmount).toFixed(2),
|
||||
)
|
||||
|
||||
// 计算价格
|
||||
const calculatePrice = () => {
|
||||
// 根据不同的 tab 计算价格
|
||||
@@ -482,10 +676,10 @@ export default function HttpPage() {
|
||||
price = unlimitedTotal
|
||||
break
|
||||
case "daily":
|
||||
price = ipAmount * periodCount * 0.01
|
||||
price = dxbtTotal
|
||||
break
|
||||
case "volume":
|
||||
price = volumeAmount || Number(customAmount) || 0
|
||||
price = effectiveVolumeAmount
|
||||
break
|
||||
default:
|
||||
price = 0
|
||||
@@ -739,8 +933,11 @@ export default function HttpPage() {
|
||||
{IP_AMOUNT_OPTIONS.map(opt => (
|
||||
<OptionBtn
|
||||
key={opt.value}
|
||||
active={ipAmount === opt.value}
|
||||
onClick={() => setIpAmount(opt.value)}
|
||||
active={customIpAmount === "" && ipAmount === opt.value}
|
||||
onClick={() => {
|
||||
setIpAmount(opt.value)
|
||||
setCustomIpAmount("")
|
||||
}}
|
||||
className="justify-center"
|
||||
>
|
||||
{opt.label}
|
||||
@@ -752,13 +949,21 @@ export default function HttpPage() {
|
||||
每日最多使用
|
||||
<Input
|
||||
type="number"
|
||||
value={customIpAmount}
|
||||
value={
|
||||
customIpAmount === ""
|
||||
? String(ipAmount)
|
||||
: customIpAmount
|
||||
}
|
||||
onChange={e => setCustomIpAmount(e.target.value)}
|
||||
placeholder="IP数"
|
||||
min={500}
|
||||
className="w-28 h-7 inline-block text-center"
|
||||
/>
|
||||
个IP
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
最低 500 个IP
|
||||
</span>
|
||||
</div>
|
||||
</OptionBox>
|
||||
|
||||
@@ -793,12 +998,16 @@ export default function HttpPage() {
|
||||
<OptionBox label="购买时长">
|
||||
<Stepper
|
||||
value={periodCount}
|
||||
unit="天"
|
||||
unit={PERIOD_UNIT[period] ?? "天"}
|
||||
onChange={setPeriodCount}
|
||||
/>
|
||||
</OptionBox>
|
||||
|
||||
<PriceBar price={totalPrice} onPay={openPayDialog} />
|
||||
<PriceBar
|
||||
price={dxbtTotal}
|
||||
loading={dxbtPriceLoading}
|
||||
onPay={() => openDxPay("daily")}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -823,8 +1032,11 @@ export default function HttpPage() {
|
||||
{VOLUME_AMOUNTS.map(amount => (
|
||||
<OptionBtn
|
||||
key={amount}
|
||||
active={volumeAmount === amount}
|
||||
onClick={() => setVolumeAmount(amount)}
|
||||
active={customAmount === "" && volumeAmount === amount}
|
||||
onClick={() => {
|
||||
setVolumeAmount(amount)
|
||||
setCustomAmount("")
|
||||
}}
|
||||
className="justify-center"
|
||||
>
|
||||
{amount}元
|
||||
@@ -836,12 +1048,16 @@ export default function HttpPage() {
|
||||
自定义
|
||||
<Input
|
||||
type="number"
|
||||
value={customAmount}
|
||||
value={
|
||||
customAmount === ""
|
||||
? String(volumeAmount)
|
||||
: customAmount
|
||||
}
|
||||
onChange={e => setCustomAmount(e.target.value)}
|
||||
placeholder="金额"
|
||||
min={1}
|
||||
className="w-28 h-7 inline-block text-center"
|
||||
/>
|
||||
{/* 这个金额需要根据选项自动更新和也支持手动显示 */}
|
||||
元
|
||||
</span>
|
||||
</div>
|
||||
@@ -865,17 +1081,21 @@ export default function HttpPage() {
|
||||
<div className="text-sm text-muted-foreground">
|
||||
可使用{" "}
|
||||
<span className="text-xl font-bold text-blue-500">
|
||||
{Math.floor(
|
||||
(volumeAmount || customAmount
|
||||
? Number(customAmount || volumeAmount)
|
||||
: 0) * 100,
|
||||
)}
|
||||
{dxblPriceLoading
|
||||
? "计算中..."
|
||||
: Math.floor(
|
||||
effectiveVolumeAmount * dxblIpsPerYuan,
|
||||
).toLocaleString()}
|
||||
</span>{" "}
|
||||
个IP,有效期1年,提取免费,使用才扣费。
|
||||
</div>
|
||||
</OptionBox>
|
||||
|
||||
<PriceBar price={totalPrice} onPay={openPayDialog} />
|
||||
<PriceBar
|
||||
price={effectiveVolumeAmount}
|
||||
loading={dxblPriceLoading}
|
||||
onPay={() => openDxPay("volume")}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1032,17 +1252,38 @@ export default function HttpPage() {
|
||||
</OptionBox>
|
||||
|
||||
<OptionBox label="IP数量">
|
||||
<div className="flex items-center gap-4 flex-wrap">
|
||||
<Stepper
|
||||
value={game.ipAmount}
|
||||
unit="个"
|
||||
onChange={v => refreshGamePrice({ ...game, ipAmount: v })}
|
||||
min={1}
|
||||
max={lineCount}
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
最大可选数量:{lineCount}
|
||||
</span>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center gap-4 flex-wrap">
|
||||
<Stepper
|
||||
value={game.ipAmount}
|
||||
unit="个"
|
||||
onChange={v =>
|
||||
refreshGamePrice({ ...game, ipAmount: v })
|
||||
}
|
||||
min={1}
|
||||
max={lineCount}
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
最大可选数量:{lineCount}
|
||||
</span>
|
||||
</div>
|
||||
{/* 进度条滑杆:拖动即时更新,停顿后算价 */}
|
||||
<div className="flex items-center gap-3 w-full max-w-md">
|
||||
<input
|
||||
type="range"
|
||||
min={1}
|
||||
max={Math.max(1, lineCount)}
|
||||
step={1}
|
||||
value={game.ipAmount}
|
||||
onChange={e =>
|
||||
handleIpSliderChange(Number(e.target.value))
|
||||
}
|
||||
className="flex-1 accent-blue-500 cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
当前选择:{game.ipAmount} 个IP
|
||||
</p>
|
||||
</div>
|
||||
</OptionBox>
|
||||
|
||||
@@ -1193,6 +1434,16 @@ export default function HttpPage() {
|
||||
onPay={() => payUnlimitedOrder()}
|
||||
/>
|
||||
|
||||
{/* 短效包天/包量支付弹窗(H币支付) */}
|
||||
<HCoinPayDialog
|
||||
open={dxPayOpen}
|
||||
onOpenChange={setDxPayOpen}
|
||||
amount={dxPayKind === "daily" ? dxbtTotal : effectiveVolumeAmount}
|
||||
balance={dxBalance}
|
||||
paying={dxPaying}
|
||||
onPay={() => confirmDxPay()}
|
||||
/>
|
||||
|
||||
{/* 支付宝表单渲染容器 */}
|
||||
<div ref={alipayBoxRef} className="hidden" />
|
||||
|
||||
@@ -1494,7 +1745,7 @@ function PriceBar({
|
||||
<span className="text-sm font-medium text-gray-700">
|
||||
价格:
|
||||
<span className="text-lg font-bold">
|
||||
{loading ? "计算中..." : `¥${price.toFixed(2)}`}
|
||||
{loading ? "" : `¥${price.toFixed(2)}`}
|
||||
</span>
|
||||
</span>
|
||||
<Button
|
||||
|
||||
@@ -19,6 +19,20 @@ export type DxwxlData = {
|
||||
maxWhitelist: number
|
||||
}
|
||||
|
||||
// 短效包天:有效时长 5/25/180/360,周期 1060-小时 1-天 7-周 30-月 90-季
|
||||
export type DxbtData = {
|
||||
durationType: number
|
||||
ipAmount: number // 每日IP数量,最小500
|
||||
periodType: number
|
||||
periodAmount: number
|
||||
}
|
||||
|
||||
// 短效包量:有效时长 5/25/180/360
|
||||
export type DxblData = {
|
||||
durationType: number
|
||||
ipAmount: number
|
||||
}
|
||||
|
||||
export type GameCity = {
|
||||
code: number
|
||||
name: string
|
||||
@@ -34,7 +48,7 @@ export type HttpOrderInfo = {
|
||||
money: number
|
||||
pay_type: number
|
||||
// 预储值(order_type=1)下单时 data 为空对象
|
||||
data: GameOrderData | DxwxlData | Record<string, never>
|
||||
data: GameOrderData | DxwxlData | DxbtData | DxblData | Record<string, never>
|
||||
}
|
||||
|
||||
export type CreateOrderResult = {
|
||||
|
||||
@@ -73,3 +73,25 @@ export type ShuidiRegion = {
|
||||
surplus_out_total?: number
|
||||
province_surplus_outs: Record<string, ShuidiProvince>
|
||||
}
|
||||
|
||||
// ==================== 水滴静态:下单 ====================
|
||||
// POST /jinyou/jinyoujt/create_order -> { code, data, msg }
|
||||
|
||||
export type ShuidiOrderInfo = {
|
||||
pay_type: number // 10-余额 70-微信 100-支付宝
|
||||
type: 0 | 1 // 0-静态 1-固态
|
||||
package_id: string
|
||||
game_id: number | "" // 无项目时为空字符串
|
||||
buy_num: number
|
||||
price: number
|
||||
account: string
|
||||
password: string
|
||||
// 省-市-rid-100 -> 数量(字符串);未选择时为空数组
|
||||
product_info: Record<string, string> | []
|
||||
}
|
||||
|
||||
export type ShuidiOrderResult = {
|
||||
code: number // 1-开通成功 2-支付宝 3-微信 -1-失败
|
||||
msg?: string
|
||||
data?: string // 支付宝/微信支付主体
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user