feat: 好友邀请
This commit is contained in:
@@ -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: '支付成功'
|
||||
});
|
||||
|
||||
@@ -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, {
|
||||
|
||||
70
src/pages/api/user/promotion/getPromotionData.ts
Normal file
70
src/pages/api/user/promotion/getPromotionData.ts
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
||||
48
src/pages/api/user/promotion/getPromotions.ts
Normal file
48
src/pages/api/user/promotion/getPromotions.ts
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
179
src/pages/promotion/index.tsx
Normal file
179
src/pages/promotion/index.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user