完善支付功能,添加支付宝的准备与创建逻辑
This commit is contained in:
@@ -21,28 +21,47 @@ async function allResource(){
|
||||
return callByUser<Resource[]>('/api/resource/all')
|
||||
}
|
||||
|
||||
async function createResourceByBalance(props: {
|
||||
type CreateResourceReq = {
|
||||
type: number
|
||||
live: number
|
||||
quota: number
|
||||
expire: number
|
||||
daily_limit: number
|
||||
}) {
|
||||
}
|
||||
|
||||
type CreateResourceResp = {
|
||||
trade_no: string
|
||||
pay_url: string
|
||||
}
|
||||
|
||||
async function createResourceByBalance(props: CreateResourceReq) {
|
||||
return await callByUser('/api/resource/create/balance', props)
|
||||
}
|
||||
|
||||
async function createResourceByAlipay() {
|
||||
throw new Error('Not implemented')
|
||||
async function prepareResourceByAlipay(props: CreateResourceReq) {
|
||||
return await callByUser<CreateResourceResp>('/api/resource/prepare/alipay', props)
|
||||
}
|
||||
|
||||
async function createResourceByWechat() {
|
||||
throw new Error('Not implemented')
|
||||
async function prepareResourceByWechat(props: CreateResourceReq) {
|
||||
return await callByUser<CreateResourceResp>('/api/resource/prepare/wechat', props)
|
||||
}
|
||||
|
||||
async function createResourceByAlipay(props: CreateResourceReq) {
|
||||
return await callByUser('/api/resource/create/alipay', props)
|
||||
}
|
||||
|
||||
async function createResourceByWechat(props: CreateResourceReq) {
|
||||
return await callByUser('/api/resource/create/wechat', props)
|
||||
}
|
||||
|
||||
export {
|
||||
listResourcePss,
|
||||
allResource,
|
||||
prepareResourceByAlipay,
|
||||
prepareResourceByWechat,
|
||||
createResourceByBalance,
|
||||
createResourceByAlipay,
|
||||
createResourceByWechat,
|
||||
type CreateResourceReq,
|
||||
type CreateResourceResp,
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
'use server'
|
||||
|
||||
import { callByUser } from "@/actions/base"
|
||||
|
||||
export async function tradeRecharge(props: {
|
||||
amount: number
|
||||
method: string
|
||||
}) {
|
||||
|
||||
let method: number
|
||||
switch (props.method) {
|
||||
case 'alipay':
|
||||
method = 1
|
||||
break
|
||||
case 'wechat':
|
||||
method = 2
|
||||
break
|
||||
default:
|
||||
throw new Error(`${props.method} is not a valid method`)
|
||||
}
|
||||
|
||||
return await callByUser('/api/trade/create', {
|
||||
subject: '余额充值',
|
||||
amount: Number(props.amount * 100),
|
||||
method: method,
|
||||
})
|
||||
}
|
||||
@@ -247,7 +247,7 @@ export default function BillsPage(props: BillsPageProps) {
|
||||
<div className={`flex flex-col gap-1`}>
|
||||
<span>{row.original.info}</span>
|
||||
|
||||
{row.original.type === 1 && (
|
||||
{row.original.type === 1 && row.original.trade.status === 1 && (
|
||||
<Link
|
||||
href={`/admin/resources?resource_no=${row.original.resource.resource_no}`}
|
||||
className={`text-sm text-blue-500 hover:underline`}>
|
||||
|
||||
@@ -270,7 +270,7 @@ export default function ResourcesPage(props: ResourcesPageProps) {
|
||||
<div className={`flex gap-1`}>
|
||||
{isAfter(row.original.pss.expire, new Date())
|
||||
? <span className={`text-green-500`}>正常</span>
|
||||
: <span className={`text-red-500`}>已过期</span>}
|
||||
: <span className={`text-red-500`}>过期</span>}
|
||||
<span>|</span>
|
||||
<span>今日限额:{row.original.pss.daily_used} / {row.original.pss.daily_limit}</span>
|
||||
<span>|</span>
|
||||
|
||||
@@ -27,6 +27,7 @@ export type Schema = z.infer<typeof schema>
|
||||
|
||||
type PurchaseFormContextType = {
|
||||
form: UseFormReturn<Schema>
|
||||
onSubmit?: () => void
|
||||
}
|
||||
|
||||
export const PurchaseFormContext = createContext<PurchaseFormContextType | undefined>(undefined)
|
||||
@@ -35,7 +36,6 @@ export type PurchaseFormProps = {}
|
||||
|
||||
export default function PurchaseForm(props: PurchaseFormProps) {
|
||||
console.log('PurchaseForm render')
|
||||
const authCtx = useContext(AuthContext)
|
||||
|
||||
const form = useForm<Schema>({
|
||||
resolver: zodResolver(schema),
|
||||
@@ -49,48 +49,9 @@ export default function PurchaseForm(props: PurchaseFormProps) {
|
||||
},
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const toExtract = () => {
|
||||
router.push('/admin/extract')
|
||||
}
|
||||
|
||||
const onSubmit = async (value: Schema) => {
|
||||
try {
|
||||
const resp = await createResourceByBalance({
|
||||
type: Number(value.type),
|
||||
live: Number(value.live) * 60,
|
||||
quota: Number(value.quota),
|
||||
expire: Number(value.expire) * 24 * 3600,
|
||||
daily_limit: Number(value.daily_limit),
|
||||
})
|
||||
|
||||
if (!resp.success) {
|
||||
throw new Error(resp.message)
|
||||
}
|
||||
|
||||
toast.success('购买成功', {
|
||||
duration: 10 * 1000,
|
||||
closeButton: true,
|
||||
action: {
|
||||
label: `去提取`,
|
||||
onClick: toExtract,
|
||||
},
|
||||
})
|
||||
|
||||
await authCtx.refreshProfile()
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e)
|
||||
toast.error('购买失败', {
|
||||
description: (e as Error).message,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section role={`tabpanel`} className={`bg-white rounded-lg`}>
|
||||
<Form form={form} onSubmit={onSubmit} className={`flex flex-row`}>
|
||||
<Form form={form} className={`flex flex-row`}>
|
||||
<PurchaseFormContext.Provider value={{form}}>
|
||||
<Left/>
|
||||
<Center/>
|
||||
|
||||
210
src/components/composites/purchase/_client/pay.tsx
Normal file
210
src/components/composites/purchase/_client/pay.tsx
Normal file
@@ -0,0 +1,210 @@
|
||||
'use client'
|
||||
import {Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger} from '@/components/ui/dialog'
|
||||
import {Button} from '@/components/ui/button'
|
||||
import alipay from '../_assets/alipay.svg'
|
||||
import wechat from '../_assets/wechat.svg'
|
||||
import balance from '../_assets/balance.svg'
|
||||
import Image from 'next/image'
|
||||
import {useContext, useState} from 'react'
|
||||
import {AuthContext} from '@/components/providers/AuthProvider'
|
||||
import {Alert, AlertDescription} from '@/components/ui/alert'
|
||||
import {
|
||||
prepareResourceByAlipay,
|
||||
prepareResourceByWechat,
|
||||
CreateResourceReq,
|
||||
CreateResourceResp,
|
||||
createResourceByBalance,
|
||||
createResourceByAlipay,
|
||||
createResourceByWechat,
|
||||
} from '@/actions/resource'
|
||||
import {ApiResponse} from '@/lib/api'
|
||||
import {toast} from 'sonner'
|
||||
import {Loader} from 'lucide-react'
|
||||
import {useRouter} from 'next/navigation'
|
||||
|
||||
export type PayProps = {
|
||||
method: 'alipay' | 'wechat' | 'balance'
|
||||
amount: number
|
||||
resource: CreateResourceReq
|
||||
|
||||
}
|
||||
|
||||
export default function Pay(props: PayProps) {
|
||||
|
||||
const ctx = useContext(AuthContext)
|
||||
const [open, setOpen] = useState(false)
|
||||
const [payInfo, setPayInfo] = useState<CreateResourceResp | undefined>()
|
||||
|
||||
const onOpen = async () => {
|
||||
setOpen(true)
|
||||
|
||||
if (props.method === 'balance') {
|
||||
return
|
||||
}
|
||||
|
||||
let resp: ApiResponse<CreateResourceResp>
|
||||
switch (props.method) {
|
||||
case 'alipay':
|
||||
resp = await prepareResourceByAlipay(props.resource)
|
||||
break
|
||||
case 'wechat':
|
||||
resp = await prepareResourceByWechat(props.resource)
|
||||
break
|
||||
}
|
||||
if (!resp.success) {
|
||||
toast.error(`创建订单失败: ${resp.message}`)
|
||||
setOpen(false)
|
||||
return
|
||||
}
|
||||
|
||||
setPayInfo(resp.data)
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
const onSubmit = async () => {
|
||||
let resp: ApiResponse
|
||||
try {
|
||||
switch (props.method) {
|
||||
case 'alipay':
|
||||
resp = await createResourceByAlipay(props.resource)
|
||||
break
|
||||
case 'wechat':
|
||||
resp = await createResourceByWechat(props.resource)
|
||||
break
|
||||
case 'balance':
|
||||
resp = await createResourceByBalance(props.resource)
|
||||
break
|
||||
}
|
||||
|
||||
if (!resp.success) {
|
||||
throw new Error(resp.message)
|
||||
}
|
||||
|
||||
toast.success('购买成功', {
|
||||
duration: 10 * 1000,
|
||||
closeButton: true,
|
||||
action: {
|
||||
label: `去提取`,
|
||||
onClick: () => router.push('/admin/extract'),
|
||||
},
|
||||
})
|
||||
|
||||
setOpen(false)
|
||||
await ctx.refreshProfile()
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e)
|
||||
toast.error('购买失败', {
|
||||
description: (e as Error).message,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className={`mt-4 h-12`} onClick={onOpen}>
|
||||
立即支付
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle className={`flex gap-2 items-center`}>
|
||||
{props.method === 'alipay' && (<>
|
||||
<Image src={alipay} alt={`支付宝`} width={20} height={20}/>
|
||||
<span>支付宝</span>
|
||||
</>)}
|
||||
{props.method === 'wechat' && (<>
|
||||
<Image src={wechat} alt={`微信`} width={20} height={20}/>
|
||||
<span>微信</span>
|
||||
</>)}
|
||||
{props.method === 'balance' && (<>
|
||||
<Image src={balance} alt={`余额`} width={20} height={20}/>
|
||||
<span>余额支付</span>
|
||||
</>)}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{props.method === 'balance' ? (
|
||||
ctx.profile && (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-weak text-sm">账户余额</span>
|
||||
<span className={`text-lg`}>{ctx.profile.balance}元</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-weak text-sm">支付金额</span>
|
||||
<span className="text-lg text-accent">- {props.amount}元</span>
|
||||
</div>
|
||||
<hr className="my-2"/>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-weak text-sm">支付后余额</span>
|
||||
<span className={`text-lg ${ctx.profile.balance > props.amount ? 'text-done' : `text-fail`}`}>
|
||||
{ctx.profile.balance - props.amount}元
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{ctx.profile.balance < props.amount && (
|
||||
<Alert variant="fail">
|
||||
<AlertDescription>
|
||||
余额不足,请先充值或选择其他支付方式
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{ctx.profile.balance >= props.amount && (
|
||||
<Alert>
|
||||
<AlertDescription>
|
||||
检查无误后,点击确认支付按钮完成支付
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<div className="bg-gray-100 w-52 h-52 flex items-center justify-center">
|
||||
{payInfo ? (
|
||||
<iframe
|
||||
src={payInfo.pay_url}
|
||||
className="w-full h-full"
|
||||
title="支付二维码"
|
||||
/>
|
||||
) : (
|
||||
<Loader size={40} className={`animate-spin`}/>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 text-center">
|
||||
请使用{props.method === 'alipay' ? '支付宝' : '微信'}扫码支付
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-center space-y-1">
|
||||
<p className="font-medium">支付金额: <span className="text-accent">{props.amount}元</span></p>
|
||||
<p className="text-xs text-gray-500">订单号: {payInfo?.trade_no || '创建订单中...'}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
disabled={props.method === 'balance' && !!ctx.profile && ctx.profile.balance < props.amount}
|
||||
onClick={onSubmit}
|
||||
>
|
||||
{props.method === 'balance' ? '确认支付' : '已完成支付'}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
theme="outline"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -13,12 +13,11 @@ import zod from 'zod'
|
||||
import FormOption from '@/components/composites/purchase/_client/option'
|
||||
import {RadioGroup} from '@/components/ui/radio-group'
|
||||
import Image from 'next/image'
|
||||
import wechat from '@/components/composites/purchase/_assets/wechat.svg'
|
||||
import alipay from '@/components/composites/purchase/_assets/alipay.svg'
|
||||
import {zodResolver} from '@hookform/resolvers/zod'
|
||||
import {tradeRecharge} from '@/actions/trade'
|
||||
import {toast} from 'sonner'
|
||||
import {useRouter} from 'next/navigation'
|
||||
import wechat from '@/components/composites/purchase/_assets/wechat.svg'
|
||||
import alipay from '@/components/composites/purchase/_assets/alipay.svg'
|
||||
|
||||
const schema = zod.object({
|
||||
method: zod.enum(['alipay', 'wechat']),
|
||||
@@ -43,10 +42,10 @@ export default function RechargeModal(props: RechargeModelProps) {
|
||||
|
||||
const onSubmit = async (data: Schema) => {
|
||||
try {
|
||||
const resp = await tradeRecharge(data)
|
||||
if (!resp.success) {
|
||||
throw new Error(resp.message)
|
||||
}
|
||||
// const resp = await tradeRecharge(data)
|
||||
// if (!resp.success) {
|
||||
// throw new Error(resp.message)
|
||||
// }
|
||||
|
||||
// todo 跳转支付页
|
||||
router.push('/pay')
|
||||
|
||||
@@ -11,6 +11,7 @@ import balance from '@/components/composites/purchase/_assets/balance.svg'
|
||||
import {Button} from '@/components/ui/button'
|
||||
import {AuthContext} from '@/components/providers/AuthProvider'
|
||||
import RechargeModal from '@/components/composites/purchase/_client/recharge'
|
||||
import Pay from '@/components/composites/purchase/_client/pay'
|
||||
|
||||
export type RightProps = {}
|
||||
|
||||
@@ -30,6 +31,7 @@ export default function Right(props: RightProps) {
|
||||
const watchQuota = form.watch('quota')
|
||||
const watchExpire = form.watch('expire')
|
||||
const watchDailyLimit = form.watch('daily_limit')
|
||||
const payType = form.watch('pay_type')
|
||||
|
||||
return (
|
||||
<div className={`flex-none basis-80 p-6 flex flex-col gap-6`}>
|
||||
@@ -119,9 +121,13 @@ export default function Right(props: RightProps) {
|
||||
</RadioGroup>
|
||||
)}
|
||||
</FormField>
|
||||
<Button className={`mt-4 h-12`} type="submit">
|
||||
立即支付
|
||||
</Button>
|
||||
<Pay method={payType} amount={100} resource={{
|
||||
type: Number(watchType),
|
||||
live: Number(watchLive) * 60,
|
||||
quota: watchQuota,
|
||||
expire: Number(watchExpire) * 24 * 3600,
|
||||
daily_limit: watchDailyLimit,
|
||||
}}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ type ApiResponse<T = undefined> = {
|
||||
data: T
|
||||
}
|
||||
|
||||
|
||||
type PageRecord<T = unknown> = {
|
||||
total: number
|
||||
page: number
|
||||
@@ -22,6 +23,8 @@ type PageRecord<T = unknown> = {
|
||||
list: T[]
|
||||
}
|
||||
|
||||
type ExtractData<T extends (...args: never) => unknown> = Awaited<ReturnType<T>> extends ApiResponse<infer D> ? D : never
|
||||
|
||||
// 预定义错误
|
||||
const UnauthorizedError = new Error('未授权访问')
|
||||
|
||||
@@ -31,5 +34,6 @@ export {
|
||||
CLIENT_SECRET,
|
||||
type ApiResponse,
|
||||
type PageRecord,
|
||||
type ExtractData,
|
||||
UnauthorizedError,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user