feat: 好友邀请

This commit is contained in:
archer
2023-04-21 22:23:19 +08:00
parent 4f51839026
commit 4397a0ad6b
22 changed files with 471 additions and 17 deletions

View File

@@ -2,9 +2,11 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, User, Pay } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import { PaySchema } from '@/types/mongoSchema';
import { PaySchema, UserModelSchema } from '@/types/mongoSchema';
import dayjs from 'dayjs';
import { getPayResult } from '@/service/utils/wxpay';
import { pushPromotionRecord } from '@/service/utils/promotion';
import { PRICE_SCALE } from '@/constants/common';
/* 校验支付结果 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
@@ -26,6 +28,17 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
throw new Error('订单已结算');
}
// 获取当前用户
const user = await User.findById(userId);
if (!user) {
throw new Error('找不到用户');
}
// 获取邀请者
let inviter: UserModelSchema | null = null;
if (user.inviterId) {
inviter = await User.findById(user.inviterId);
}
const payRes = await getPayResult(payOrder.orderId);
// 校验下是否超过一天
@@ -50,6 +63,16 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
await User.findByIdAndUpdate(userId, {
$inc: { balance: payOrder.price }
});
// 推广佣金发放
if (inviter) {
pushPromotionRecord({
userId: inviter._id,
objUId: userId,
type: 'invite',
// amount 单位为元,需要除以缩放比例,最后乘比例
amount: (payOrder.price / PRICE_SCALE) * inviter.promotion.rate * 0.01
});
}
jsonRes(res, {
data: '支付成功'
});

View File

@@ -15,7 +15,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
await connectToDatabase();
const records = await Pay.find({
userId
userId,
status: { $ne: 'CLOSED' }
}).sort({ createTime: -1 });
jsonRes(res, {

View File

@@ -0,0 +1,70 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, User, promotionRecord } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
import mongoose from 'mongoose';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { authorization } = req.headers;
if (!authorization) {
throw new Error('缺少登录凭证');
}
const userId = await authToken(authorization);
await connectToDatabase();
const invitedAmount = await User.countDocuments({
inviterId: userId
});
// 计算累计合
const countHistory: { totalAmount: number }[] = await promotionRecord.aggregate([
{ $match: { userId: new mongoose.Types.ObjectId(userId), amount: { $gt: 0 } } },
{
$group: {
_id: null, // 分组条件,这里使用 null 表示不分组
totalAmount: { $sum: '$amount' } // 计算 amount 字段的总和
}
},
{
$project: {
_id: false, // 排除 _id 字段
totalAmount: true // 只返回 totalAmount 字段
}
}
]);
// 计算剩余金额
const countResidue: { totalAmount: number }[] = await promotionRecord.aggregate([
{ $match: { userId: new mongoose.Types.ObjectId(userId) } },
{
$group: {
_id: null, // 分组条件,这里使用 null 表示不分组
totalAmount: { $sum: '$amount' } // 计算 amount 字段的总和
}
},
{
$project: {
_id: false, // 排除 _id 字段
totalAmount: true // 只返回 totalAmount 字段
}
}
]);
jsonRes(res, {
data: {
invitedAmount,
historyAmount: countHistory[0]?.totalAmount || 0,
residueAmount: countResidue[0]?.totalAmount || 0
}
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -0,0 +1,48 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, promotionRecord } from '@/service/mongo';
import { authToken } from '@/service/utils/tools';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { authorization } = req.headers;
let { pageNum = 1, pageSize = 10 } = req.query as { pageNum: string; pageSize: string };
pageNum = +pageNum;
pageSize = +pageSize;
if (!authorization) {
throw new Error('缺少登录凭证');
}
const userId = await authToken(authorization);
await connectToDatabase();
const data = await promotionRecord
.find(
{
userId
},
'_id createTime type amount'
)
.sort({ _id: -1 })
.skip((pageNum - 1) * pageSize)
.limit(pageSize);
jsonRes(res, {
data: {
pageNum,
pageSize,
data,
total: await promotionRecord.countDocuments({
userId
})
}
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -45,11 +45,12 @@ const BillTable = () => {
))}
</Tbody>
</Table>
<Box mt={4} mr={4} textAlign={'end'}>
<Pagination />
</Box>
<Loading loading={isLoading} fixed={false} />
</TableContainer>
<Box mt={4} mr={4} textAlign={'end'}>
<Pagination />
</Box>
</Card>
);
};

View File

@@ -7,7 +7,8 @@ import { useToast } from '@/hooks/useToast';
import { useGlobalStore } from '@/store/global';
import { useUserStore } from '@/store/user';
import { UserType } from '@/types/user';
import { clearToken } from '@/utils/user';
import { useRouter } from 'next/router';
import { useQuery } from '@tanstack/react-query';
import dynamic from 'next/dynamic';
@@ -16,6 +17,7 @@ const BilTable = dynamic(() => import('./components/BillTable'));
const PayModal = dynamic(() => import('./components/PayModal'));
const NumberSetting = () => {
const router = useRouter();
const { userInfo, updateUserInfo, initUserInfo } = useUserStore();
const { setLoading } = useGlobalStore();
const { register, handleSubmit } = useForm<UserUpdateParams>({
@@ -43,13 +45,23 @@ const NumberSetting = () => {
useQuery(['init'], initUserInfo);
const onclickLogOut = useCallback(() => {
clearToken();
router.replace('/login');
}, [router]);
return (
<>
{/* 核心信息 */}
<Card px={6} py={4}>
<Box fontSize={'xl'} fontWeight={'bold'}>
</Box>
<Flex justifyContent={'space-between'}>
<Box fontSize={'xl'} fontWeight={'bold'}>
</Box>
<Button variant={'outline'} size={'xs'} onClick={onclickLogOut}>
退
</Button>
</Flex>
<Flex mt={6} alignItems={'center'}>
<Box flex={'0 0 60px'}>:</Box>
<Box>{userInfo?.username}</Box>

View File

@@ -0,0 +1,179 @@
import React, { useState } from 'react';
import Link from 'next/link';
import {
Card,
Box,
Button,
Flex,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
TableContainer,
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalBody,
ModalCloseButton,
useColorModeValue,
ModalFooter,
useDisclosure
} from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import { useLoading } from '@/hooks/useLoading';
import dayjs from 'dayjs';
import { useCopyData } from '@/utils/tools';
import { useUserStore } from '@/store/user';
import MyIcon from '@/components/Icon';
import { getPromotionRecords } from '@/api/user';
import { usePagination } from '@/hooks/usePagination';
import { PromotionRecordType } from '@/api/response/user';
import { PromotionTypeMap } from '@/constants/user';
import { getPromotionInitData } from '@/api/user';
import Image from 'next/image';
const OpenApi = () => {
const { Loading } = useLoading();
const { userInfo, initUserInfo } = useUserStore();
const { copyData } = useCopyData();
const {
isOpen: isOpenWithdraw,
onClose: onCloseWithdraw,
onOpen: onOpenWithdraw
} = useDisclosure();
useQuery(['init'], initUserInfo);
const { data: { invitedAmount = 0, historyAmount = 0, residueAmount = 0 } = {} } = useQuery(
['getInvitedCountAmount'],
getPromotionInitData
);
const {
data: promotionRecords,
isLoading,
Pagination,
total
} = usePagination<PromotionRecordType>({
api: getPromotionRecords
});
return (
<>
<Card px={6} py={4} position={'relative'}>
<Box fontSize={'xl'} fontWeight={'bold'}>
</Box>
<Box my={2} color={'blackAlpha.600'} fontSize={'sm'}>
FastGpt FastGpt
</Box>
<Flex my={2} alignItems={'center'}>
<Box>: </Box>
<Box mx={2} fontSize={'xl'} lineHeight={1} fontWeight={'bold'}>
{residueAmount}
</Box>
</Flex>
<Flex>
<Button
mr={4}
variant={'outline'}
onClick={() => {
copyData(`${location.origin}?inviterId=${userInfo?._id}`, '已复制邀请链接');
}}
>
</Button>
<Button
leftIcon={<MyIcon name="withdraw" w={'22px'} />}
px={4}
title={residueAmount < 50 ? '最低提现额度为50元' : ''}
isDisabled={residueAmount < 50}
onClick={onOpenWithdraw}
>
</Button>
</Flex>
</Card>
<Card mt={4} px={6} py={4} position={'relative'}>
<Flex alignItems={'center'} mb={3} justifyContent={['space-between', 'flex-start']}>
<Box w={'120px'}></Box>
<Box fontWeight={'bold'}>{userInfo?.promotion.rate || 15}%</Box>
</Flex>
<Flex alignItems={'center'} mb={3} justifyContent={['space-between', 'flex-start']}>
<Box w={'120px'}></Box>
<Box fontWeight={'bold'}>{invitedAmount}</Box>
</Flex>
<Flex alignItems={'center'} justifyContent={['space-between', 'flex-start']}>
<Box w={'120px'}></Box>
<Box fontWeight={'bold'}> {historyAmount}</Box>
</Flex>
</Card>
<Card mt={4} px={6} py={4} position={'relative'}>
<Box fontSize={'xl'} fontWeight={'bold'}>
({total})
</Box>
<TableContainer position={'relative'}>
<Table>
<Thead>
<Tr>
<Th></Th>
<Th></Th>
<Th></Th>
</Tr>
</Thead>
<Tbody fontSize={'sm'}>
{promotionRecords.map((item) => (
<Tr key={item._id}>
<Td>
{item.createTime ? dayjs(item.createTime).format('YYYY/MM/DD HH:mm:ss') : '-'}
</Td>
<Td>{PromotionTypeMap[item.type]}</Td>
<Td>{item.amount}</Td>
</Tr>
))}
</Tbody>
</Table>
<Loading loading={isLoading} fixed={false} />
</TableContainer>
<Box mt={4} mr={4} textAlign={'end'}>
<Pagination />
</Box>
</Card>
<Modal isOpen={isOpenWithdraw} onClose={onCloseWithdraw}>
<ModalOverlay />
<ModalContent color={useColorModeValue('blackAlpha.700', 'white')}>
<ModalHeader></ModalHeader>
<ModalCloseButton />
<ModalBody textAlign={'center'}>
<Image
style={{ margin: 'auto' }}
src={'/imgs/wx300-2.jpg'}
width={200}
height={200}
alt=""
/>
<Box mt={2}>
:
<Box as={'span'} userSelect={'all'}>
YNyiqi
</Box>
</Box>
<Box></Box>
</ModalBody>
<ModalFooter>
<Button variant={'outline'} onClick={onCloseWithdraw}>
</Button>
</ModalFooter>
</ModalContent>
</Modal>
</>
);
};
export default OpenApi;