完善充值记录和短效套餐页面

This commit is contained in:
Eamon
2026-09-15 17:39:51 +08:00
parent aefbb8143e
commit a902f5be29
11 changed files with 374 additions and 59 deletions

View File

@@ -1,5 +1,5 @@
import type { ColumnDef } from "@tanstack/react-table"
import { useCallback, useEffect, useState } from "react"
import { useCallback, useEffect, useRef, useState } from "react"
import { toast } from "sonner"
import { fetchScoreList } from "@/api/funds"
import DataTable from "@/components/data-table"
@@ -58,18 +58,31 @@ export default function FundsPage() {
const [status, setStatus] = useState<"load" | "done" | "fail">("load")
const [page, setPage] = useState(1)
const [hasMore, setHasMore] = useState(false)
// 总数恰为 50 整数倍时最后一页也是 50 条,翻到空页后锁死最大页码
const [maxPage, setMaxPage] = useState<number | null>(null)
const seqRef = useRef(0)
const load = useCallback(async (targetPage: number) => {
const seq = ++seqRef.current
setStatus("load")
const res = await fetchScoreList(targetPage)
if (res.success) {
setData(res.data)
setHasMore(res.data.length >= SCORE_PAGE_SIZE)
setStatus("done")
} else {
console.log(res, 'datadatadatadatadata');
if (seq !== seqRef.current) return
if (!res.success) {
setStatus("fail")
toast.error(res.message)
return
}
if (res.data.length === 0 && targetPage > 1) {
setMaxPage(targetPage - 1)
setPage(targetPage - 1)
load(targetPage - 1)
return
}
setData(res.data)
setHasMore(res.data.length >= SCORE_PAGE_SIZE)
setStatus("done")
}, [])
useEffect(() => {
@@ -82,10 +95,13 @@ export default function FundsPage() {
load(nextPage)
}
// 接口不返回总条数:还有下一页时多加 1 条保证可翻页,否则按累计条数展示
const total = hasMore
? page * SCORE_PAGE_SIZE + 1
: (page - 1) * SCORE_PAGE_SIZE + data.length
const canNext = maxPage !== null ? page < maxPage : hasMore
const total =
maxPage !== null
? maxPage * SCORE_PAGE_SIZE
: canNext
? page * SCORE_PAGE_SIZE + 1
: (page - 1) * SCORE_PAGE_SIZE + data.length
return (
<Page>

View File

@@ -1,34 +1,56 @@
import type { ColumnDef } from "@tanstack/react-table"
import { useCallback, useEffect, useMemo, useState } from "react"
import { fetchHttpRechargeList } from "@/api/httpRecharge"
import DataTable from "@/components/data-table"
import Page from "@/components/page"
import type { HttpRechargeItem } from "@/lib/models/httpRecharge"
import {
formatPayStatus,
type HttpRechargeItem,
} from "@/lib/models/httpRecharge"
export default function HttpRechargePage() {
const [data, setData] = useState<HttpRechargeItem[]>([])
const [status, setStatus] = useState<"load" | "done" | "fail">("load")
const load = useCallback(async () => {
setStatus("load")
const res = await fetchHttpRechargeList()
console.log(res, 'fewewf');
if (!res.success) {
setStatus("fail")
return
}
setData(res.data)
setStatus("done")
}, [])
useEffect(() => {
load()
}, [load])
const columns = useMemo<ColumnDef<HttpRechargeItem>[]>(
() => [
{ accessorKey: "create_time", header: "交易时间" },
{ accessorKey: "ju_money", header: "充值H币" },
{ accessorKey: "pay_money", header: "支付金额" },
{ accessorKey: "pay_type", header: "充值渠道" },
{
accessorKey: "pay_status",
header: "支付状态",
cell: ({ row }) => formatPayStatus(row.original.pay_status),
},
{ accessorKey: "order_no", header: "订单编号" },
{ accessorKey: "ali_wx_no", header: "支付宝/微信订单号" },
],
[],
)
return (
<Page>
<div className="bg-white rounded-lg p-4">
<h3 className="font-semibold text-lg">HTTP 充值记录</h3>
<DataTable<HttpRechargeItem>
data={[]}
status="done"
columns={[
{
accessorKey: "create_time",
header: "交易时间",
},
{
accessorKey: "ju_money",
header: "充值H币",
},
{
accessorKey: "pay_type",
header: "充值渠道",
},
{
accessorKey: "pay_money",
header: "充值金额",
},
]}
/>
<h3 className="font-semibold text-lg mb-4">HTTP 充值记录</h3>
<DataTable data={data} status={status} columns={columns} />
</div>
</Page>
)

View File

@@ -13,6 +13,14 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import {
type ApiLinkConfig,
type DxPackage,
@@ -69,6 +77,151 @@ const SEPARATOR_OPTIONS = [
{ value: "6", label: "自定义:" },
]
const API_PARAM_DOCS: {
name: string
type: string
required: string
desc: string
}[] = [
{
name: "pt",
type: "int",
required: "是",
desc: "套餐类型,0或12-储值套餐,21-短效包天,22-短效包量,23-短效无限量,29-终身免费VIP套餐,31-长效套餐",
},
{ name: "num", type: "int", required: "是", desc: "提取IP数量" },
{
name: "gt",
type: "int",
required: "否",
desc: "提取时长 0-默认 1-1分钟以上 2-2分钟以上 3-3分钟以上 4-4分钟以上",
},
{
name: "regions",
type: "string",
required: "否",
desc: "多个用英文逗号换行,不传表示随机",
},
{ name: "isp", type: "int", required: "是", desc: "0:不限 4:联通 2:电信" },
{ name: "port", type: "int", required: "是", desc: "IP协议 1:HTTP 2:SOCKS5" },
{
name: "time",
type: "int",
required: "否",
desc: "有效时长 1-0.1至1分钟 5-1至5分钟 25-5至25分钟 180-25至180分钟 360-3至6小时",
},
{
name: "type",
type: "int",
required: "否",
desc: "数据格式:1:TXT 2:JSON 3:HTML",
},
{ name: "pack", type: "int", required: "否", desc: "用户套餐ID" },
{
name: "ts",
type: "int",
required: "否",
desc: "是否显示IP过期时间: 1显示 0不显示",
},
{
name: "lb",
type: "int",
required: "否",
desc: "换行符(1:\\r\\n 2:/br 3:\\r 4:\\n 5:\\t 6:自定义)",
},
{ name: "clb", type: "string", required: "否", desc: "自定义换行符" },
{
name: "sp",
type: "int",
required: "否",
desc: "分隔符(1:竖线| 2:斜线/ 3:冒号 6:自定义)",
},
{ name: "csp", type: "string", required: "否", desc: "自定义分隔符" },
{
name: "distinct",
type: "int",
required: "否",
desc: "去重选择(2:严格去重。0:不去重)",
},
{
name: "aw",
type: "int",
required: "否",
desc: "是否将调用者自动添加到IP白名单(0:不自动添加。1:自动添加)",
},
{
name: "at",
type: "int",
required: "否",
desc: "鉴权模式 1-白名单,2-白名单+账密严格鉴权,3-仅账密鉴权(无限量不可用)",
},
]
const RESPONSE_FIELD_DOCS: { name: string; type: string; desc: string }[] = [
{ name: "success", type: "bool", desc: "是否成功" },
{ name: "code", type: "int", desc: "0表示成功,其他参见《错误代码》表" },
{ name: "ip", type: "string", desc: "IP" },
{ name: "port", type: "int", desc: "端口号" },
]
const ERROR_CODE_DOCS: { code: string; msg: string; desc: string }[] = [
{
code: "111",
msg: "请在1秒后再次请求",
desc: "提取链接请求太过频繁,超出限制",
},
{
code: "113",
msg: "请将192.168.0.0设置为白名单!",
desc: "白名单未添加/白名单掉了",
},
{ code: "114", msg: "余额不足", desc: "账户金额消耗完毕" },
{
code: "115",
msg: "请更换地区等条件重新生成api链接地址",
desc: "没有资源或没有符合条件的数据",
},
{
code: "116",
msg: "您的套餐今日已到达上限!",
desc: "套餐内IP数量消耗完毕",
},
{
code: "117",
msg: "您的套餐pack传参有误!请检测您现在的ip是否在套餐所在账户!",
desc: "检测本地白名单是不是在账户下",
},
{
code: "118",
msg: "您的账户异常,请联系客服!",
desc: "账户处于被禁用状态",
},
{ code: "121", msg: "您的该套餐已经过期了!", desc: "套餐过期" },
{
code: "401",
msg: "IP:1.1.1.1:2000 Authentication failed!",
desc: "白名单错误/使用的IP已经过期",
},
{
code: "403",
msg: "Sorry!The site has been banned!",
desc: "客户目标网站异常,联系客服处理",
},
]
const RESPONSE_SAMPLE = `{
"code": 0,
"msg": "提示信息",
"success": true,
"data": [
{
"ip": "49.68.68.197", // 隧道ip(代理ip)
"port": 33220, // 代理端口
"expireTime": "2019-05-24 08:58:31"
}
]
}`
type FieldRowProps = {
label: string
children: React.ReactNode
@@ -179,8 +332,10 @@ export default function ShortApiLink({ packages }: { packages: DxPackage[] }) {
push("t", token.token)
}
push("at", config.at)
push("sp", config.sp)
push("csp", config.csp)
if (config.at !== 1) {
push("sp", config.sp)
push("csp", config.csp)
}
if (regions.length > 0) push("regions", regions.join(","))
return `${API_LINK_BASE}?${params.join("&")}`
}
@@ -431,7 +586,7 @@ export default function ShortApiLink({ packages }: { packages: DxPackage[] }) {
</FieldRow>
)}
{config.at !== 1 && !isUnlimited && (
{!isUnlimited && (
<FieldRow label="分隔符">
<RadioGroup
value={String(config.sp)}
@@ -485,6 +640,81 @@ export default function ShortApiLink({ packages }: { packages: DxPackage[] }) {
{currentPackage.durationType}
</p>
)}
<div className="rounded-lg border border-dashed border-gray-200 p-4 flex flex-col gap-3">
<h4 className="font-semibold">请求参数注释</h4>
<Table>
<TableHeader>
<TableRow>
<TableHead>名称</TableHead>
<TableHead>类型</TableHead>
<TableHead>必选</TableHead>
<TableHead>说明</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{API_PARAM_DOCS.map(param => (
<TableRow key={param.name}>
<TableCell className="font-mono">{param.name}</TableCell>
<TableCell>{param.type}</TableCell>
<TableCell>{param.required}</TableCell>
<TableCell className="whitespace-normal min-w-96">
{param.desc}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
<div className="rounded-lg border border-dashed border-gray-200 p-4 flex flex-col gap-3">
<h4 className="font-semibold">返回结果示例</h4>
<pre className="rounded-md bg-gray-50 border border-gray-200 p-3 text-xs overflow-x-auto text-gray-700">
<code>{RESPONSE_SAMPLE}</code>
</pre>
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-24">名称</TableHead>
<TableHead className="w-24">类型</TableHead>
<TableHead>说明</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{RESPONSE_FIELD_DOCS.map(field => (
<TableRow key={field.name}>
<TableCell className="font-mono">{field.name}</TableCell>
<TableCell>{field.type}</TableCell>
<TableCell className="whitespace-normal">
{field.desc}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
<div className="rounded-lg border border-dashed border-gray-200 p-4 flex flex-col gap-3">
<h4 className="font-semibold">错误代码</h4>
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-32">错误编码</TableHead>
<TableHead>错误提示</TableHead>
<TableHead>说明</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{ERROR_CODE_DOCS.map(err => (
<TableRow key={err.code}>
<TableCell className="font-mono">code:{err.code}</TableCell>
<TableCell>{err.msg}</TableCell>
<TableCell className="whitespace-normal">{err.desc}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
)
}

View File

@@ -33,6 +33,7 @@ export default function ShortPage() {
const loadPackages = useCallback(async () => {
setStatus("load")
const res = await fetchDxPackages()
console.log(res, 'resresresres');
if (!res.success) {
setStatus("fail")
return
@@ -44,6 +45,7 @@ export default function ShortPage() {
const loadOrders = useCallback(async () => {
setOrderStatus("load")
const res = await fetchDxOrders()
if (!res.success) {
setOrderStatus("fail")
return

View File

@@ -1,14 +1,21 @@
import type { ApiResponse } from "@/lib/api"
import type { ScoreItem } from "@/lib/models/funds"
import { phpPost } from "./http"
import { phpFetch } from "./http"
export async function fetchScoreList(
page: number,
): Promise<ApiResponse<ScoreItem[]>> {
const res = await phpPost<ScoreItem[] | unknown>("/user/UserScore/getList", {
cookie: document.cookie,
page,
})
const query = page > 1 ? `?page=${page}` : ""
const res = await phpFetch<ScoreItem[] | { data?: unknown }>(
"POST",
`/user/UserScore/getList${query}`,
{ cookie: document.cookie },
{ credentials: "include" },
)
if (!res.success) return res
return { success: true, data: Array.isArray(res.data) ? res.data : [] }
if (Array.isArray(res.data)) return { success: true, data: res.data }
return {
success: true,
data: Array.isArray(res.data?.data) ? res.data.data : [],
}
}

View File

@@ -61,7 +61,9 @@ export async function phpFetch<T = RawHttpResponse>(
message: "请求失败",
}
}
const data = (await response.json()) as T
const text = await response.text()
// 老 PHP 接口异常时可能返回空响应体,直接 json() 会抛错
const data = text ? (JSON.parse(text) as T) : ({} as T)
return { success: true, data }
} catch (e) {
if (controller.signal.aborted) {

View File

@@ -1,16 +1,20 @@
import type { ApiResponse } from "@/lib/api"
import type { HttpRechargeItem } from "@/lib/models/httpRecharge"
import { callByUser } from "./base"
import { isArray, phpPost } from "./http"
// ==================== HTTP 充值记录 ====================
// TODO: 接口路径待后端确认(旧后台为 php-api /http/* 充值列表接口)。
export async function fetchHttpRechargeList(
params: Record<string, unknown>,
): Promise<ApiResponse<HttpRechargeItem[]>> {
return await callByUser<HttpRechargeItem[]>(
"/api/course/v1/order/HttpRecharges",
params,
"GET",
export async function fetchHttpRechargeList(): Promise<
ApiResponse<HttpRechargeItem[]>
> {
const res = await phpPost<HttpRechargeItem[] | { data?: unknown }>(
"/http/user/charge_list",
{ cookie: document.cookie },
)
if (!res.success) return res
if (isArray<HttpRechargeItem>(res.data)) {
return { success: true, data: res.data }
}
const list = isArray<HttpRechargeItem>(res.data?.data) ? res.data.data : []
return { success: true, data: list }
}

View File

@@ -167,7 +167,7 @@ export async function createRechargeOrder(data: {
}
// 轮询充值支付状态:返回 true 表示支付成功
// 新后端 /user/IsPay 响应体:{ Code: 0|1, Message: "" },Code 本身即支付状态
// 响应体:{ Code: 0|1, Message: "" },Code 本身即支付状态
// 兼容旧格式 { Code: 10000, Data: 0|1 }
export async function checkRechargeStatus(
orderNo: string,

View File

@@ -16,4 +16,5 @@ export type ScoreItem = {
RestAmount2: string
}
// 服务端固定每页条数(接口不返回总条数)
export const SCORE_PAGE_SIZE = 50

View File

@@ -1,6 +1,22 @@
export type HttpRechargeItem = {
create_time: string
ju_money: number
id: string
user_id: string
ju_money: string | number
pay_money: string | number
pay_type: string
pay_money: number
pay_status: string | number
channel?: string | null
order_no: string
ali_wx_no: string
create_time: string
update_time: string
}
export const HTTP_RECHARGE_PAY_STATUS_LABELS: Record<number, string> = {
0: "未支付",
1: "支付成功",
}
export function formatPayStatus(value: string | number): string {
return HTTP_RECHARGE_PAY_STATUS_LABELS[Number(value)] ?? String(value)
}

View File

@@ -11,6 +11,11 @@ export type DxPackage = {
expireTime: string
isExpired?: boolean | number | string
ipAmount?: number
userId?: number
poolType?: number
usedAmount?: number
transferable?: number
createTime?: number
}
// 短效购买记录
@@ -21,6 +26,16 @@ export type DxOrder = {
durationType: string
money: number | string
create_time: string
id?: string
user_id?: string
order_status?: number | string
game_type?: string | null
ip_count?: number | string | null
order_id?: string
payCash?: string
serial?: string
status?: number | string
update_time?: string
}
// 长效套餐(已开通)
@@ -214,11 +229,11 @@ export const IP_STATE_LABELS: Record<number, string> = {
// ==================== 字段格式化 ====================
// 后端的 isExpired 可能返回 true/1/"1",统一转布尔
// 后端的 isExpired 可能返回 true/1/"1",也可能直接返回"已过期"/"未过期"字符串
export function parseExpired(
value: boolean | number | string | undefined,
): boolean {
return value === true || value === 1 || value === "1"
return value === true || value === 1 || value === "1" || value === "已过期"
}
export function formatExpired(