feat: 增加充值功能
This commit is contained in:
parent
129f3a2a30
commit
d065539707
@ -28,6 +28,7 @@
|
|||||||
"immer": "^9.0.19",
|
"immer": "^9.0.19",
|
||||||
"jsonwebtoken": "^9.0.0",
|
"jsonwebtoken": "^9.0.0",
|
||||||
"mongoose": "^6.10.0",
|
"mongoose": "^6.10.0",
|
||||||
|
"nanoid": "^4.0.1",
|
||||||
"next": "13.1.6",
|
"next": "13.1.6",
|
||||||
"nodemailer": "^6.9.1",
|
"nodemailer": "^6.9.1",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
@ -43,7 +44,6 @@
|
|||||||
"sass": "^1.58.3",
|
"sass": "^1.58.3",
|
||||||
"sharp": "^0.31.3",
|
"sharp": "^0.31.3",
|
||||||
"tunnel": "^0.0.6",
|
"tunnel": "^0.0.6",
|
||||||
"uuid": "^9.0.0",
|
|
||||||
"zustand": "^4.3.5"
|
"zustand": "^4.3.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
19
pnpm-lock.yaml
generated
19
pnpm-lock.yaml
generated
@ -32,6 +32,7 @@ specifiers:
|
|||||||
jsonwebtoken: ^9.0.0
|
jsonwebtoken: ^9.0.0
|
||||||
lint-staged: ^13.1.2
|
lint-staged: ^13.1.2
|
||||||
mongoose: ^6.10.0
|
mongoose: ^6.10.0
|
||||||
|
nanoid: ^4.0.1
|
||||||
next: 13.1.6
|
next: 13.1.6
|
||||||
nodemailer: ^6.9.1
|
nodemailer: ^6.9.1
|
||||||
nprogress: ^0.2.0
|
nprogress: ^0.2.0
|
||||||
@ -49,7 +50,6 @@ specifiers:
|
|||||||
sharp: ^0.31.3
|
sharp: ^0.31.3
|
||||||
tunnel: ^0.0.6
|
tunnel: ^0.0.6
|
||||||
typescript: 4.9.5
|
typescript: 4.9.5
|
||||||
uuid: ^9.0.0
|
|
||||||
zustand: ^4.3.5
|
zustand: ^4.3.5
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -70,6 +70,7 @@ dependencies:
|
|||||||
immer: registry.npmmirror.com/immer/9.0.19
|
immer: registry.npmmirror.com/immer/9.0.19
|
||||||
jsonwebtoken: registry.npmmirror.com/jsonwebtoken/9.0.0
|
jsonwebtoken: registry.npmmirror.com/jsonwebtoken/9.0.0
|
||||||
mongoose: registry.npmmirror.com/mongoose/6.10.0
|
mongoose: registry.npmmirror.com/mongoose/6.10.0
|
||||||
|
nanoid: registry.npmmirror.com/nanoid/4.0.1
|
||||||
next: registry.npmmirror.com/next/13.1.6_wiv434v7erz4aedd5whhdwmpv4
|
next: registry.npmmirror.com/next/13.1.6_wiv434v7erz4aedd5whhdwmpv4
|
||||||
nodemailer: registry.npmmirror.com/nodemailer/6.9.1
|
nodemailer: registry.npmmirror.com/nodemailer/6.9.1
|
||||||
nprogress: registry.npmmirror.com/nprogress/0.2.0
|
nprogress: registry.npmmirror.com/nprogress/0.2.0
|
||||||
@ -85,7 +86,6 @@ dependencies:
|
|||||||
sass: registry.npmmirror.com/sass/1.58.3
|
sass: registry.npmmirror.com/sass/1.58.3
|
||||||
sharp: registry.npmmirror.com/sharp/0.31.3
|
sharp: registry.npmmirror.com/sharp/0.31.3
|
||||||
tunnel: registry.npmmirror.com/tunnel/0.0.6
|
tunnel: registry.npmmirror.com/tunnel/0.0.6
|
||||||
uuid: registry.npmmirror.com/uuid/9.0.0
|
|
||||||
zustand: registry.npmmirror.com/zustand/4.3.5_immer@9.0.19+react@18.2.0
|
zustand: registry.npmmirror.com/zustand/4.3.5_immer@9.0.19+react@18.2.0
|
||||||
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
@ -8496,6 +8496,14 @@ packages:
|
|||||||
hasBin: true
|
hasBin: true
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
registry.npmmirror.com/nanoid/4.0.1:
|
||||||
|
resolution: {integrity: sha512-udKGtCCUafD3nQtJg9wBhRP3KMbPglUsgV5JVsXhvyBs/oefqb4sqMEhKBBgqZncYowu58p1prsZQBYvAj/Gww==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/nanoid/-/nanoid-4.0.1.tgz}
|
||||||
|
name: nanoid
|
||||||
|
version: 4.0.1
|
||||||
|
engines: {node: ^14 || ^16 || >=18}
|
||||||
|
hasBin: true
|
||||||
|
dev: false
|
||||||
|
|
||||||
registry.npmmirror.com/napi-build-utils/1.0.2:
|
registry.npmmirror.com/napi-build-utils/1.0.2:
|
||||||
resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz}
|
resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz}
|
||||||
name: napi-build-utils
|
name: napi-build-utils
|
||||||
@ -10335,13 +10343,6 @@ packages:
|
|||||||
dev: false
|
dev: false
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
registry.npmmirror.com/uuid/9.0.0:
|
|
||||||
resolution: {integrity: sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/uuid/-/uuid-9.0.0.tgz}
|
|
||||||
name: uuid
|
|
||||||
version: 9.0.0
|
|
||||||
hasBin: true
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
registry.npmmirror.com/uvu/0.5.6:
|
registry.npmmirror.com/uvu/0.5.6:
|
||||||
resolution: {integrity: sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/uvu/-/uvu-0.5.6.tgz}
|
resolution: {integrity: sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/uvu/-/uvu-0.5.6.tgz}
|
||||||
name: uvu
|
name: uvu
|
||||||
|
|||||||
BIN
public/imgs/wxcode.jpg
Normal file
BIN
public/imgs/wxcode.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 320 KiB |
1
public/qrcode.min.js
vendored
Normal file
1
public/qrcode.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -55,3 +55,12 @@ export const getUserBills = (data: RequestPaging) =>
|
|||||||
...res,
|
...res,
|
||||||
data: res.data.map((bill) => adaptBill(bill))
|
data: res.data.map((bill) => adaptBill(bill))
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export const getPayCode = (amount: number) =>
|
||||||
|
GET<{
|
||||||
|
codeUrl: string;
|
||||||
|
orderId: string;
|
||||||
|
}>(`/user/getPayCode?amount=${amount}`);
|
||||||
|
|
||||||
|
export const checkPayResult = (orderId: string) =>
|
||||||
|
GET<number>(`/user/checkPayResult?orderId=${orderId}`);
|
||||||
|
|||||||
1
src/components/Icon/icons/pay.svg
Normal file
1
src/components/Icon/icons/pay.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1679410564438" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2824" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32"><path d="M693.095316 281.760857l-131.632817 223.935003 103.718481 0 0 49.478312-120.846571 0 0 68.193688 120.846571 0 0 50.115659-120.846571 0 0 99.276514-62.164435 0L482.169975 673.483519 356.88022 673.483519l0-50.115659 125.289755 0 0-68.193688L356.88022 555.174172l0-49.478312 106.893053 0-130.364204-223.935003 70.099647 0c60.895822 111.230417 97.898433 181.748475 111.012698 211.562689l1.268612 0c4.441967-12.262847 16.596562-37.002611 36.474732-74.219292l74.536749-137.343396L693.095316 281.760857 693.095316 281.760857zM693.095316 281.760857" p-id="2825"></path><path d="M784.470674 621.448522c-15.061578 0-27.247797 12.187435-27.247797 27.247797s12.187435 27.247797 27.247797 27.247797l71.98128 0c-61.204765 128.843816-192.338895 217.986027-344.464118 217.986027-210.6687 0-381.478892-170.782216-381.478892-381.475243 0-210.696675 170.810191-381.465512 381.478892-381.465512 192.121175 0 350.635679 142.189179 377.137878 326.968701l55.08064 0C917.333181 242.953241 734.255278 76.493794 511.987837 76.493794 271.197197 76.493794 76.012135 271.688586 76.012135 512.456117c0 240.762665 195.185062 435.972053 435.975702 435.972053 164.236031 0 307.128238-90.894915 381.475243-225.064956l0 61.57574c0 15.061578 12.187435 27.247797 27.276989 27.247797 15.004412 0 27.247797-12.187435 27.247797-27.247797L947.987865 648.697535c0-3.297419 0-27.247797-27.247797-27.247797L784.470674 621.449738 784.470674 621.448522zM784.470674 621.448522" p-id="2826"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.7 KiB |
@ -7,7 +7,8 @@ const map = {
|
|||||||
model: require('./icons/model.svg').default,
|
model: require('./icons/model.svg').default,
|
||||||
share: require('./icons/share.svg').default,
|
share: require('./icons/share.svg').default,
|
||||||
home: require('./icons/home.svg').default,
|
home: require('./icons/home.svg').default,
|
||||||
menu: require('./icons/menu.svg').default
|
menu: require('./icons/menu.svg').default,
|
||||||
|
pay: require('./icons/pay.svg').default
|
||||||
};
|
};
|
||||||
|
|
||||||
export type IconName = keyof typeof map;
|
export type IconName = keyof typeof map;
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useToast } from '@chakra-ui/react';
|
import { useToast } from '@chakra-ui/react';
|
||||||
import { getTokenLogin } from '@/api/user';
|
|
||||||
import { useUserStore } from '@/store/user';
|
import { useUserStore } from '@/store/user';
|
||||||
import { useGlobalStore } from '@/store/global';
|
import { useGlobalStore } from '@/store/global';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
@ -19,7 +18,7 @@ const Auth = ({ children }: { children: JSX.Element }) => {
|
|||||||
position: 'top',
|
position: 'top',
|
||||||
status: 'warning'
|
status: 'warning'
|
||||||
});
|
});
|
||||||
const { userInfo, setUserInfo } = useUserStore();
|
const { userInfo, initUserInfo } = useUserStore();
|
||||||
const { setLoading } = useGlobalStore();
|
const { setLoading } = useGlobalStore();
|
||||||
|
|
||||||
useQuery(
|
useQuery(
|
||||||
@ -29,15 +28,10 @@ const Auth = ({ children }: { children: JSX.Element }) => {
|
|||||||
return setLoading(false);
|
return setLoading(false);
|
||||||
} else {
|
} else {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
return getTokenLogin();
|
return initUserInfo();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess(user) {
|
|
||||||
if (user) {
|
|
||||||
setUserInfo(user);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onError(error) {
|
onError(error) {
|
||||||
console.log('error->', error);
|
console.log('error->', error);
|
||||||
router.push('/login');
|
router.push('/login');
|
||||||
|
|||||||
@ -24,7 +24,7 @@ export const ModelList: ModelConstantsData[] = [
|
|||||||
trainName: 'turbo',
|
trainName: 'turbo',
|
||||||
maxToken: 4000,
|
maxToken: 4000,
|
||||||
maxTemperature: 2,
|
maxTemperature: 2,
|
||||||
price: 2
|
price: 5
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
serviceCompany: 'openai',
|
serviceCompany: 'openai',
|
||||||
@ -33,7 +33,7 @@ export const ModelList: ModelConstantsData[] = [
|
|||||||
trainName: 'davinci',
|
trainName: 'davinci',
|
||||||
maxToken: 4000,
|
maxToken: 4000,
|
||||||
maxTemperature: 2,
|
maxTemperature: 2,
|
||||||
price: 20
|
price: 50
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@ -39,6 +39,7 @@ export default function App({ Component, pageProps }: AppProps) {
|
|||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
</Head>
|
</Head>
|
||||||
<Script src="/iconfont.js" strategy="afterInteractive"></Script>
|
<Script src="/iconfont.js" strategy="afterInteractive"></Script>
|
||||||
|
<Script src="/qrcode.min.js" strategy="afterInteractive"></Script>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<ChakraProvider theme={theme}>
|
<ChakraProvider theme={theme}>
|
||||||
<ColorModeScript initialColorMode={theme.config.initialColorMode} />
|
<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 Markdown from '@/components/Markdown';
|
||||||
import { shareHint } from '@/constants/common';
|
import { shareHint } from '@/constants/common';
|
||||||
import { getChatSiteId } from '@/api/chat';
|
import { getChatSiteId } from '@/api/chat';
|
||||||
|
import Image from 'next/image';
|
||||||
|
|
||||||
const SlideBar = ({
|
const SlideBar = ({
|
||||||
name,
|
name,
|
||||||
@ -53,6 +54,7 @@ const SlideBar = ({
|
|||||||
const { chatHistory, removeChatHistoryByWindowId } = useChatStore();
|
const { chatHistory, removeChatHistoryByWindowId } = useChatStore();
|
||||||
const [hasReady, setHasReady] = useState(false);
|
const [hasReady, setHasReady] = useState(false);
|
||||||
const { isOpen: isOpenShare, onOpen: onOpenShare, onClose: onCloseShare } = useDisclosure();
|
const { isOpen: isOpenShare, onOpen: onOpenShare, onClose: onCloseShare } = useDisclosure();
|
||||||
|
const { isOpen: isOpenWx, onOpen: onOpenWx, onClose: onCloseWx } = useDisclosure();
|
||||||
|
|
||||||
const { isSuccess } = useQuery(['init'], getMyModels, {
|
const { isSuccess } = useQuery(['init'], getMyModels, {
|
||||||
cacheTime: 5 * 60 * 1000
|
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}>
|
<Box px={3} mb={3}>
|
||||||
<Flex
|
<Flex
|
||||||
alignItems={'center'}
|
alignItems={'center'}
|
||||||
@ -229,8 +237,17 @@ const SlideBar = ({
|
|||||||
分享
|
分享
|
||||||
</>
|
</>
|
||||||
</RenderButton>
|
</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
|
<IconButton
|
||||||
icon={colorMode === 'light' ? <MoonIcon /> : <SunIcon />}
|
icon={colorMode === 'light' ? <MoonIcon /> : <SunIcon />}
|
||||||
aria-label={''}
|
aria-label={''}
|
||||||
@ -242,7 +259,7 @@ const SlideBar = ({
|
|||||||
}}
|
}}
|
||||||
onClick={toggleColorMode}
|
onClick={toggleColorMode}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Flex>
|
||||||
|
|
||||||
{/* 分享提示modal */}
|
{/* 分享提示modal */}
|
||||||
<Modal isOpen={isOpenShare} onClose={onCloseShare}>
|
<Modal isOpen={isOpenShare} onClose={onCloseShare}>
|
||||||
@ -287,6 +304,35 @@ const SlideBar = ({
|
|||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</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>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import React, { Dispatch, useState, useCallback } from 'react';
|
import React, { Dispatch, useState, useCallback, useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
Modal,
|
Modal,
|
||||||
ModalOverlay,
|
ModalOverlay,
|
||||||
@ -12,12 +12,14 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
useToast,
|
useToast,
|
||||||
Input,
|
Input,
|
||||||
Select
|
Select,
|
||||||
|
Box
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { postCreateModel } from '@/api/model';
|
import { postCreateModel } from '@/api/model';
|
||||||
import type { ModelSchema } from '@/types/mongoSchema';
|
import type { ModelSchema } from '@/types/mongoSchema';
|
||||||
import { ModelList } from '@/constants/model';
|
import { ModelList } from '@/constants/model';
|
||||||
|
import { formatPrice } from '@/utils/user';
|
||||||
|
|
||||||
interface CreateFormType {
|
interface CreateFormType {
|
||||||
name: string;
|
name: string;
|
||||||
@ -37,6 +39,7 @@ const CreateModel = ({
|
|||||||
position: 'top'
|
position: 'top'
|
||||||
});
|
});
|
||||||
const {
|
const {
|
||||||
|
getValues,
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { errors }
|
formState: { errors }
|
||||||
@ -105,6 +108,12 @@ const CreateModel = ({
|
|||||||
{!!errors.serviceModelName && errors.serviceModelName.message}
|
{!!errors.serviceModelName && errors.serviceModelName.message}
|
||||||
</FormErrorMessage>
|
</FormErrorMessage>
|
||||||
</FormControl>
|
</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>
|
</ModalBody>
|
||||||
|
|
||||||
<ModalFooter>
|
<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 {
|
import {
|
||||||
Card,
|
Card,
|
||||||
Box,
|
Box,
|
||||||
@ -25,13 +25,18 @@ import { useUserStore } from '@/store/user';
|
|||||||
import { UserType } from '@/types/user';
|
import { UserType } from '@/types/user';
|
||||||
import { usePaging } from '@/hooks/usePaging';
|
import { usePaging } from '@/hooks/usePaging';
|
||||||
import type { UserBillType } from '@/types/user';
|
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 NumberSetting = () => {
|
||||||
const { userInfo, updateUserInfo } = useUserStore();
|
const { userInfo, updateUserInfo, initUserInfo } = useUserStore();
|
||||||
const { setLoading } = useGlobalStore();
|
const { setLoading } = useGlobalStore();
|
||||||
const { register, handleSubmit, control } = useForm<UserUpdateParams>({
|
const { register, handleSubmit, control } = useForm<UserUpdateParams>({
|
||||||
defaultValues: userInfo as UserType
|
defaultValues: userInfo as UserType
|
||||||
});
|
});
|
||||||
|
const [showPay, setShowPay] = useState(false);
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const {
|
const {
|
||||||
fields: accounts,
|
fields: accounts,
|
||||||
@ -43,9 +48,9 @@ const NumberSetting = () => {
|
|||||||
});
|
});
|
||||||
const { setPageNum, data: bills } = usePaging<UserBillType>({
|
const { setPageNum, data: bills } = usePaging<UserBillType>({
|
||||||
api: getUserBills,
|
api: getUserBills,
|
||||||
pageSize: 20
|
pageSize: 30
|
||||||
});
|
});
|
||||||
console.log(bills);
|
|
||||||
const onclickSave = useCallback(
|
const onclickSave = useCallback(
|
||||||
async (data: UserUpdateParams) => {
|
async (data: UserUpdateParams) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@ -62,6 +67,8 @@ const NumberSetting = () => {
|
|||||||
[setLoading, toast, updateUserInfo]
|
[setLoading, toast, updateUserInfo]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useQuery(['init'], initUserInfo);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Card px={6} py={4}>
|
<Card px={6} py={4}>
|
||||||
@ -80,9 +87,9 @@ const NumberSetting = () => {
|
|||||||
<Box>
|
<Box>
|
||||||
<strong>{userInfo?.balance}</strong> 元
|
<strong>{userInfo?.balance}</strong> 元
|
||||||
</Box>
|
</Box>
|
||||||
{/* <Button size={'sm'} w={'80px'} ml={5}>
|
<Button size={'sm'} w={'80px'} ml={5} onClick={() => setShowPay(true)}>
|
||||||
充值
|
充值
|
||||||
</Button> */}
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
</Card>
|
</Card>
|
||||||
@ -181,6 +188,7 @@ const NumberSetting = () => {
|
|||||||
</Table>
|
</Table>
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
</Card>
|
</Card>
|
||||||
|
{showPay && <PayModal onClose={() => setShowPay(false)} />}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -36,6 +36,6 @@ export const pushBill = async ({
|
|||||||
$inc: { balance: -price }
|
$inc: { balance: -price }
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Bill.findByIdAndDelete(billId);
|
billId && Bill.findByIdAndDelete(billId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
23
src/service/models/pay.ts
Normal file
23
src/service/models/pay.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { Schema, model, models } from 'mongoose';
|
||||||
|
|
||||||
|
const PaySchema = new Schema({
|
||||||
|
userId: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'user',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
time: {
|
||||||
|
type: Number,
|
||||||
|
default: () => Date.now()
|
||||||
|
},
|
||||||
|
price: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
orderId: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Pay = models['pay'] || model('pay', PaySchema);
|
||||||
@ -16,7 +16,7 @@ const UserSchema = new Schema({
|
|||||||
},
|
},
|
||||||
balance: {
|
balance: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 0
|
default: 0.5
|
||||||
},
|
},
|
||||||
accounts: [
|
accounts: [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -31,3 +31,4 @@ export * from './models/model';
|
|||||||
export * from './models/user';
|
export * from './models/user';
|
||||||
export * from './models/training';
|
export * from './models/training';
|
||||||
export * from './models/bill';
|
export * from './models/bill';
|
||||||
|
export * from './models/pay';
|
||||||
|
|||||||
@ -22,8 +22,12 @@ export const generateToken = (userId: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/* 校验 token */
|
/* 校验 token */
|
||||||
export const authToken = (token: string): Promise<string> => {
|
export const authToken = (token?: string): Promise<string> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!token) {
|
||||||
|
reject('缺少登录凭证');
|
||||||
|
return;
|
||||||
|
}
|
||||||
const key = process.env.TOKEN_KEY as string;
|
const key = process.env.TOKEN_KEY as string;
|
||||||
|
|
||||||
jwt.verify(token, key, function (err, decoded: any) {
|
jwt.verify(token, key, function (err, decoded: any) {
|
||||||
|
|||||||
@ -6,9 +6,11 @@ import type { ModelSchema } from '@/types/mongoSchema';
|
|||||||
import { setToken } from '@/utils/user';
|
import { setToken } from '@/utils/user';
|
||||||
import { getMyModels } from '@/api/model';
|
import { getMyModels } from '@/api/model';
|
||||||
import { formatPrice } from '@/utils/user';
|
import { formatPrice } from '@/utils/user';
|
||||||
|
import { getTokenLogin } from '@/api/user';
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
userInfo: UserType | null;
|
userInfo: UserType | null;
|
||||||
|
initUserInfo: () => Promise<null>;
|
||||||
setUserInfo: (user: UserType, token?: string) => void;
|
setUserInfo: (user: UserType, token?: string) => void;
|
||||||
updateUserInfo: (user: UserUpdateParams) => void;
|
updateUserInfo: (user: UserUpdateParams) => void;
|
||||||
myModels: ModelSchema[];
|
myModels: ModelSchema[];
|
||||||
@ -20,6 +22,11 @@ export const useUserStore = create<State>()(
|
|||||||
devtools(
|
devtools(
|
||||||
immer((set, get) => ({
|
immer((set, get) => ({
|
||||||
userInfo: null,
|
userInfo: null,
|
||||||
|
async initUserInfo() {
|
||||||
|
const res = await getTokenLogin();
|
||||||
|
get().setUserInfo(res);
|
||||||
|
return null;
|
||||||
|
},
|
||||||
setUserInfo(user: UserType, token?: string) {
|
setUserInfo(user: UserType, token?: string) {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
state.userInfo = {
|
state.userInfo = {
|
||||||
|
|||||||
1
src/types/index.d.ts
vendored
1
src/types/index.d.ts
vendored
@ -2,6 +2,7 @@ import type { Mongoose } from 'mongoose';
|
|||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
var mongodb: Mongoose | string | null;
|
var mongodb: Mongoose | string | null;
|
||||||
|
var QRCode: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PagingData<T> = {
|
export type PagingData<T> = {
|
||||||
|
|||||||
1
src/types/user.d.ts
vendored
1
src/types/user.d.ts
vendored
@ -14,6 +14,7 @@ export interface UserType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface UserUpdateParams {
|
export interface UserUpdateParams {
|
||||||
|
balance?: number;
|
||||||
accounts?: {
|
accounts?: {
|
||||||
type: string;
|
type: string;
|
||||||
value: string;
|
value: string;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user