feat: 增加充值功能
This commit is contained in:
@@ -39,6 +39,7 @@ export default function App({ Component, pageProps }: AppProps) {
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<Script src="/iconfont.js" strategy="afterInteractive"></Script>
|
||||
<Script src="/qrcode.min.js" strategy="afterInteractive"></Script>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ChakraProvider theme={theme}>
|
||||
<ColorModeScript initialColorMode={theme.config.initialColorMode} />
|
||||
|
||||
63
src/pages/api/user/checkPayResult.ts
Normal file
63
src/pages/api/user/checkPayResult.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import axios from 'axios';
|
||||
import { connectToDatabase, User, Pay } from '@/service/mongo';
|
||||
import { authToken } from '@/service/utils/tools';
|
||||
import { formatPrice } from '@/utils/user';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { authorization } = req.headers;
|
||||
let { orderId } = req.query as { orderId: string };
|
||||
|
||||
const userId = await authToken(authorization);
|
||||
|
||||
const { data } = await axios.get(
|
||||
`https://sif268.laf.dev/wechat-order-query?order_number=${orderId}&api_key=${process.env.WXPAYCODE}`
|
||||
);
|
||||
|
||||
if (data.trade_state === 'SUCCESS') {
|
||||
await connectToDatabase();
|
||||
|
||||
// 重复记录校验
|
||||
const count = await Pay.count({
|
||||
orderId
|
||||
});
|
||||
|
||||
if (count > 0) {
|
||||
throw new Error('订单重复,请刷新');
|
||||
}
|
||||
|
||||
// 计算实际充值。把分转成数据库的值
|
||||
const price = data.amount.total * 0.01 * 100000;
|
||||
let payId;
|
||||
try {
|
||||
// 充值记录 +1
|
||||
const payRecord = await Pay.create({
|
||||
userId,
|
||||
price,
|
||||
orderId
|
||||
});
|
||||
payId = payRecord._id;
|
||||
// 充钱
|
||||
await User.findByIdAndUpdate(userId, {
|
||||
$inc: { balance: price }
|
||||
});
|
||||
} catch (error) {
|
||||
payId && Pay.findByIdAndDelete(payId);
|
||||
}
|
||||
|
||||
jsonRes(res, {
|
||||
data: 'success'
|
||||
});
|
||||
} else {
|
||||
throw new Error(data.trade_state_desc);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
45
src/pages/api/user/getPayCode.ts
Normal file
45
src/pages/api/user/getPayCode.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import axios from 'axios';
|
||||
import { authToken } from '@/service/utils/tools';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 20);
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
const { authorization } = req.headers;
|
||||
let { amount = 0 } = req.query as { amount: string };
|
||||
amount = +amount;
|
||||
|
||||
if (!authorization) {
|
||||
throw new Error('缺少登录凭证');
|
||||
}
|
||||
await authToken(authorization);
|
||||
|
||||
const id = nanoid();
|
||||
|
||||
const response = await axios({
|
||||
url: 'https://sif268.laf.dev/wechat-pay',
|
||||
method: 'POST',
|
||||
data: {
|
||||
trade_order_number: id,
|
||||
amount: amount * 100,
|
||||
api_key: process.env.WXPAYCODE
|
||||
}
|
||||
});
|
||||
|
||||
jsonRes(res, {
|
||||
data: {
|
||||
orderId: id,
|
||||
codeUrl: response.data?.code_url
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ import { useCopyData } from '@/utils/tools';
|
||||
import Markdown from '@/components/Markdown';
|
||||
import { shareHint } from '@/constants/common';
|
||||
import { getChatSiteId } from '@/api/chat';
|
||||
import Image from 'next/image';
|
||||
|
||||
const SlideBar = ({
|
||||
name,
|
||||
@@ -53,6 +54,7 @@ const SlideBar = ({
|
||||
const { chatHistory, removeChatHistoryByWindowId } = useChatStore();
|
||||
const [hasReady, setHasReady] = useState(false);
|
||||
const { isOpen: isOpenShare, onOpen: onOpenShare, onClose: onCloseShare } = useDisclosure();
|
||||
const { isOpen: isOpenWx, onOpen: onOpenWx, onClose: onCloseWx } = useDisclosure();
|
||||
|
||||
const { isSuccess } = useQuery(['init'], getMyModels, {
|
||||
cacheTime: 5 * 60 * 1000
|
||||
@@ -113,7 +115,13 @@ const SlideBar = ({
|
||||
</>
|
||||
);
|
||||
|
||||
const RenderButton = ({ onClick, children }: { onClick: () => void; children: JSX.Element }) => (
|
||||
const RenderButton = ({
|
||||
onClick,
|
||||
children
|
||||
}: {
|
||||
onClick: () => void;
|
||||
children: JSX.Element | string;
|
||||
}) => (
|
||||
<Box px={3} mb={3}>
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
@@ -229,8 +237,17 @@ const SlideBar = ({
|
||||
分享
|
||||
</>
|
||||
</RenderButton>
|
||||
<RenderButton onClick={() => router.push('/number/setting')}>
|
||||
<>
|
||||
<MyIcon name="pay" fill={'white'} w={'16px'} h={'16px'} mr={4} />
|
||||
充值
|
||||
</>
|
||||
</RenderButton>
|
||||
|
||||
<Box textAlign={'end'} mr={4}>
|
||||
<Flex alignItems={'center'} mr={4}>
|
||||
<Box flex={1}>
|
||||
<RenderButton onClick={onOpenWx}>交流群</RenderButton>
|
||||
</Box>
|
||||
<IconButton
|
||||
icon={colorMode === 'light' ? <MoonIcon /> : <SunIcon />}
|
||||
aria-label={''}
|
||||
@@ -242,7 +259,7 @@ const SlideBar = ({
|
||||
}}
|
||||
onClick={toggleColorMode}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
|
||||
{/* 分享提示modal */}
|
||||
<Modal isOpen={isOpenShare} onClose={onCloseShare}>
|
||||
@@ -287,6 +304,35 @@ const SlideBar = ({
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
{/* wx 联系 */}
|
||||
<Modal isOpen={isOpenWx} onClose={onCloseWx}>
|
||||
<ModalOverlay />
|
||||
<ModalContent color={useColorModeValue('blackAlpha.700', 'white')}>
|
||||
<ModalHeader>wx交流群</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody textAlign={'center'}>
|
||||
<Image
|
||||
style={{ margin: 'auto' }}
|
||||
src={'/imgs/wxcode.jpg'}
|
||||
width={200}
|
||||
height={200}
|
||||
alt=""
|
||||
/>
|
||||
<Box mt={2}>
|
||||
微信号:{' '}
|
||||
<Box as={'span'} userSelect={'all'}>
|
||||
YNyiqi
|
||||
</Box>
|
||||
</Box>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button variant={'outline'} onClick={onCloseWx}>
|
||||
关闭
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { Dispatch, useState, useCallback } from 'react';
|
||||
import React, { Dispatch, useState, useCallback, useMemo } from 'react';
|
||||
import {
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
@@ -12,12 +12,14 @@ import {
|
||||
Button,
|
||||
useToast,
|
||||
Input,
|
||||
Select
|
||||
Select,
|
||||
Box
|
||||
} from '@chakra-ui/react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { postCreateModel } from '@/api/model';
|
||||
import type { ModelSchema } from '@/types/mongoSchema';
|
||||
import { ModelList } from '@/constants/model';
|
||||
import { formatPrice } from '@/utils/user';
|
||||
|
||||
interface CreateFormType {
|
||||
name: string;
|
||||
@@ -37,6 +39,7 @@ const CreateModel = ({
|
||||
position: 'top'
|
||||
});
|
||||
const {
|
||||
getValues,
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors }
|
||||
@@ -105,6 +108,12 @@ const CreateModel = ({
|
||||
{!!errors.serviceModelName && errors.serviceModelName.message}
|
||||
</FormErrorMessage>
|
||||
</FormControl>
|
||||
<Box mt={3} textAlign={'center'} fontSize={'sm'} color={'blackAlpha.600'}>
|
||||
{formatPrice(
|
||||
ModelList.find((item) => item.model === getValues('serviceModelName'))?.price || 0
|
||||
) * 1000}
|
||||
元/1000字(包括上下文和标点符号)
|
||||
</Box>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
|
||||
138
src/pages/number/components/PayModal.tsx
Normal file
138
src/pages/number/components/PayModal.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import {
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalFooter,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
Button,
|
||||
Input,
|
||||
Box,
|
||||
Grid
|
||||
} from '@chakra-ui/react';
|
||||
import { getPayCode, checkPayResult } from '@/api/user';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useUserStore } from '@/store/user';
|
||||
|
||||
const PayModal = ({ onClose }: { onClose: () => void }) => {
|
||||
const { toast } = useToast();
|
||||
const { initUserInfo } = useUserStore();
|
||||
const [inputVal, setInputVal] = useState<number | ''>('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [orderId, setOrderId] = useState('');
|
||||
|
||||
const handleClickPay = useCallback(async () => {
|
||||
if (!inputVal || inputVal <= 0 || isNaN(+inputVal)) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
// 获取支付二维码
|
||||
const res = await getPayCode(inputVal);
|
||||
new QRCode(document.getElementById('payQRCode'), {
|
||||
text: res.codeUrl,
|
||||
width: 128,
|
||||
height: 128,
|
||||
colorDark: '#000000',
|
||||
colorLight: '#ffffff',
|
||||
correctLevel: QRCode.CorrectLevel.H
|
||||
});
|
||||
setOrderId(res.orderId);
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: '出现了一些意外...',
|
||||
status: 'error'
|
||||
});
|
||||
console.log(error);
|
||||
}
|
||||
setLoading(false);
|
||||
}, [inputVal, toast]);
|
||||
|
||||
useQuery(
|
||||
[orderId],
|
||||
() => {
|
||||
if (!orderId) return null;
|
||||
return checkPayResult(orderId);
|
||||
},
|
||||
{
|
||||
refetchInterval: 2000,
|
||||
onSuccess(res) {
|
||||
if (!res) return;
|
||||
onClose();
|
||||
initUserInfo();
|
||||
toast({
|
||||
title: '充值成功',
|
||||
status: 'success'
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
isOpen={true}
|
||||
onClose={() => {
|
||||
if (orderId) return;
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>充值</ModalHeader>
|
||||
{!orderId && <ModalCloseButton />}
|
||||
|
||||
<ModalBody>
|
||||
{!orderId && (
|
||||
<>
|
||||
<Grid gridTemplateColumns={'repeat(4,1fr)'} gridGap={5} mb={4}>
|
||||
{[5, 10, 20, 50].map((item) => (
|
||||
<Button
|
||||
key={item}
|
||||
variant={item === inputVal ? 'solid' : 'outline'}
|
||||
onClick={() => setInputVal(item)}
|
||||
>
|
||||
{item}元
|
||||
</Button>
|
||||
))}
|
||||
</Grid>
|
||||
<Box>
|
||||
<Input
|
||||
value={inputVal}
|
||||
type={'number'}
|
||||
step={1}
|
||||
placeholder={'其他金额,请取整数'}
|
||||
onChange={(e) => {
|
||||
setInputVal(Math.floor(+e.target.value));
|
||||
}}
|
||||
></Input>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
{/* 付费二维码 */}
|
||||
<Box textAlign={'center'}>
|
||||
{orderId && <Box mb={3}>请微信扫码支付: {inputVal}元,请勿关闭页面</Box>}
|
||||
<Box id={'payQRCode'} display={'inline-block'}></Box>
|
||||
</Box>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
{!orderId && (
|
||||
<>
|
||||
<Button colorScheme={'gray'} onClick={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button ml={3} isLoading={loading} onClick={handleClickPay}>
|
||||
获取充值二维码
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PayModal;
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import {
|
||||
Card,
|
||||
Box,
|
||||
@@ -25,13 +25,18 @@ import { useUserStore } from '@/store/user';
|
||||
import { UserType } from '@/types/user';
|
||||
import { usePaging } from '@/hooks/usePaging';
|
||||
import type { UserBillType } from '@/types/user';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
const PayModal = dynamic(() => import('./components/PayModal'));
|
||||
|
||||
const NumberSetting = () => {
|
||||
const { userInfo, updateUserInfo } = useUserStore();
|
||||
const { userInfo, updateUserInfo, initUserInfo } = useUserStore();
|
||||
const { setLoading } = useGlobalStore();
|
||||
const { register, handleSubmit, control } = useForm<UserUpdateParams>({
|
||||
defaultValues: userInfo as UserType
|
||||
});
|
||||
const [showPay, setShowPay] = useState(false);
|
||||
const { toast } = useToast();
|
||||
const {
|
||||
fields: accounts,
|
||||
@@ -43,9 +48,9 @@ const NumberSetting = () => {
|
||||
});
|
||||
const { setPageNum, data: bills } = usePaging<UserBillType>({
|
||||
api: getUserBills,
|
||||
pageSize: 20
|
||||
pageSize: 30
|
||||
});
|
||||
console.log(bills);
|
||||
|
||||
const onclickSave = useCallback(
|
||||
async (data: UserUpdateParams) => {
|
||||
setLoading(true);
|
||||
@@ -62,6 +67,8 @@ const NumberSetting = () => {
|
||||
[setLoading, toast, updateUserInfo]
|
||||
);
|
||||
|
||||
useQuery(['init'], initUserInfo);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card px={6} py={4}>
|
||||
@@ -80,9 +87,9 @@ const NumberSetting = () => {
|
||||
<Box>
|
||||
<strong>{userInfo?.balance}</strong> 元
|
||||
</Box>
|
||||
{/* <Button size={'sm'} w={'80px'} ml={5}>
|
||||
<Button size={'sm'} w={'80px'} ml={5} onClick={() => setShowPay(true)}>
|
||||
充值
|
||||
</Button> */}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Box>
|
||||
</Card>
|
||||
@@ -181,6 +188,7 @@ const NumberSetting = () => {
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Card>
|
||||
{showPay && <PayModal onClose={() => setShowPay(false)} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user