feat: 增加充值功能

This commit is contained in:
archer
2023-03-21 23:14:28 +08:00
parent 129f3a2a30
commit d065539707
24 changed files with 389 additions and 35 deletions

View File

@@ -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} />

View 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
});
}
}

View 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
});
}
}

View File

@@ -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>
);
};

View File

@@ -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>

View 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;

View File

@@ -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)} />}
</>
);
};