diff --git a/public/imgs/wx300-2.jpg b/public/imgs/wx300-2.jpg new file mode 100644 index 000000000..32623b171 Binary files /dev/null and b/public/imgs/wx300-2.jpg differ diff --git a/src/api/response/user.d.ts b/src/api/response/user.d.ts index 2a99aca3f..3bf8ab10c 100644 --- a/src/api/response/user.d.ts +++ b/src/api/response/user.d.ts @@ -1,5 +1,13 @@ import type { UserType } from '@/types/user'; +import type { PromotionRecordSchema } from '@/types/mongoSchema'; export interface ResLogin { token: string; user: UserType; } + +export interface PromotionRecordType { + _id: PromotionRecordSchema['_id']; + type: PromotionRecordSchema['type']; + createTime: PromotionRecordSchema['createTime']; + amount: PromotionRecordSchema['amount']; +} diff --git a/src/api/user.ts b/src/api/user.ts index e88dba03b..67fff885b 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -1,6 +1,6 @@ import { GET, POST, PUT } from './request'; import { createHashPassword, Obj2Query } from '@/utils/tools'; -import { ResLogin } from './response/user'; +import { ResLogin, PromotionRecordType } from './response/user'; import { UserAuthTypeEnum } from '@/constants/common'; import { UserType, UserUpdateParams } from '@/types/user'; import type { PagingData, RequestPaging } from '@/types'; @@ -17,6 +17,14 @@ export const sendAuthCode = ({ export const getTokenLogin = () => GET('/user/tokenLogin'); +/* get promotion init data */ +export const getPromotionInitData = () => + GET<{ + invitedAmount: number; + historyAmount: number; + residueAmount: number; + }>('/user/promotion/getPromotionData'); + export const postRegister = ({ username, password, @@ -73,3 +81,7 @@ export const getPayCode = (amount: number) => }>(`/user/getPayCode?amount=${amount}`); export const checkPayResult = (payId: string) => GET(`/user/checkPayResult?payId=${payId}`); + +/* promotion records */ +export const getPromotionRecords = (data: RequestPaging) => + GET(`/user/promotion/getPromotions?${Obj2Query(data)}`); diff --git a/src/components/Icon/icons/promotion.svg b/src/components/Icon/icons/promotion.svg new file mode 100644 index 000000000..b132c3ae4 --- /dev/null +++ b/src/components/Icon/icons/promotion.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/Icon/icons/withdraw.svg b/src/components/Icon/icons/withdraw.svg new file mode 100644 index 000000000..c7cc52da5 --- /dev/null +++ b/src/components/Icon/icons/withdraw.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/Icon/index.tsx b/src/components/Icon/index.tsx index a5be36b50..a9ba353d4 100644 --- a/src/components/Icon/index.tsx +++ b/src/components/Icon/index.tsx @@ -14,7 +14,9 @@ const map = { develop: require('./icons/develop.svg').default, user: require('./icons/user.svg').default, chatting: require('./icons/chatting.svg').default, - delete: require('./icons/delete.svg').default + promotion: require('./icons/promotion.svg').default, + delete: require('./icons/delete.svg').default, + withdraw: require('./icons/withdraw.svg').default }; export type IconName = keyof typeof map; diff --git a/src/components/Layout/index.tsx b/src/components/Layout/index.tsx index 3f3afdf44..aea56cfd4 100644 --- a/src/components/Layout/index.tsx +++ b/src/components/Layout/index.tsx @@ -32,6 +32,12 @@ const navbarList = [ link: '/number/setting', activeLink: ['/number/setting'] }, + { + label: '邀请', + icon: 'promotion', + link: '/promotion', + activeLink: ['/promotion'] + }, { label: '开发', icon: 'develop', diff --git a/src/constants/user.ts b/src/constants/user.ts index 11e039135..bd204bd59 100644 --- a/src/constants/user.ts +++ b/src/constants/user.ts @@ -20,3 +20,15 @@ export const BillTypeMap: Record<`${BillTypeEnum}`, string> = { [BillTypeEnum.vector]: '索引生成', [BillTypeEnum.return]: '退款' }; + +export enum PromotionEnum { + invite = 'invite', + shareModel = 'shareModel', + withdraw = 'withdraw' +} + +export const PromotionTypeMap = { + [PromotionEnum.invite]: '好友充值', + [PromotionEnum.shareModel]: '模型分享', + [PromotionEnum.withdraw]: '提现' +}; diff --git a/src/hooks/usePagination.tsx b/src/hooks/usePagination.tsx index 0f0439445..651db61d2 100644 --- a/src/hooks/usePagination.tsx +++ b/src/hooks/usePagination.tsx @@ -18,7 +18,7 @@ export const usePagination = ({ const [pageNum, setPageNum] = useState(1); const [total, setTotal] = useState(0); const [data, setData] = useState([]); - const maxPage = useMemo(() => Math.ceil(total / pageSize), [pageSize, total]); + const maxPage = useMemo(() => Math.ceil(total / pageSize) || 1, [pageSize, total]); const { mutate, isLoading } = useMutation({ mutationFn: async (num: number = pageNum) => { diff --git a/src/pages/api/user/checkPayResult.ts b/src/pages/api/user/checkPayResult.ts index 8fc6906a6..c4eb3678b 100644 --- a/src/pages/api/user/checkPayResult.ts +++ b/src/pages/api/user/checkPayResult.ts @@ -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: '支付成功' }); diff --git a/src/pages/api/user/getPayOrders.ts b/src/pages/api/user/getPayOrders.ts index 892fcd706..ebe00fa3c 100644 --- a/src/pages/api/user/getPayOrders.ts +++ b/src/pages/api/user/getPayOrders.ts @@ -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, { diff --git a/src/pages/api/user/promotion/getPromotionData.ts b/src/pages/api/user/promotion/getPromotionData.ts new file mode 100644 index 000000000..ec2f790a5 --- /dev/null +++ b/src/pages/api/user/promotion/getPromotionData.ts @@ -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 + }); + } +} diff --git a/src/pages/api/user/promotion/getPromotions.ts b/src/pages/api/user/promotion/getPromotions.ts new file mode 100644 index 000000000..536276d81 --- /dev/null +++ b/src/pages/api/user/promotion/getPromotions.ts @@ -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 + }); + } +} diff --git a/src/pages/number/components/BillTable.tsx b/src/pages/number/components/BillTable.tsx index 61eb80056..aa1ba74ba 100644 --- a/src/pages/number/components/BillTable.tsx +++ b/src/pages/number/components/BillTable.tsx @@ -45,11 +45,12 @@ const BillTable = () => { ))} - - - + + + + ); }; diff --git a/src/pages/number/setting.tsx b/src/pages/number/setting.tsx index 1650c5fb6..df646d654 100644 --- a/src/pages/number/setting.tsx +++ b/src/pages/number/setting.tsx @@ -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({ @@ -43,13 +45,23 @@ const NumberSetting = () => { useQuery(['init'], initUserInfo); + const onclickLogOut = useCallback(() => { + clearToken(); + router.replace('/login'); + }, [router]); + return ( <> {/* 核心信息 */} - - 账号信息 - + + + 账号信息 + + + 账号: {userInfo?.username} diff --git a/src/pages/promotion/index.tsx b/src/pages/promotion/index.tsx new file mode 100644 index 000000000..2563700dd --- /dev/null +++ b/src/pages/promotion/index.tsx @@ -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({ + api: getPromotionRecords + }); + + return ( + <> + + + 我的邀请 + + + 你可以通过邀请链接邀请好友注册 FastGpt 账号。好友在 FastGpt + 平台的每次充值,你都会获得一定比例的佣金。 + + + 当前剩余佣金: ¥ + + {residueAmount} + + + + + + + + + + 佣金比例 + {userInfo?.promotion.rate || 15}% + + + 已注册用户数 + {invitedAmount}人 + + + 累计佣金 + ¥ {historyAmount} + + + + + 佣金记录 ({total}) + + + + + + + + + + + + {promotionRecords.map((item) => ( + + + + + + ))} + +
时间类型金额
+ {item.createTime ? dayjs(item.createTime).format('YYYY/MM/DD HH:mm:ss') : '-'} + {PromotionTypeMap[item.type]}{item.amount}
+ + +
+ + + +
+ + + + 提现联系 + + + + + 微信号: + + YNyiqi + + + 发送你的邀请链接和需要提现的金额 + + + + + + + + + ); +}; + +export default OpenApi; diff --git a/src/service/models/promotionRecord.ts b/src/service/models/promotionRecord.ts new file mode 100644 index 000000000..6906c4432 --- /dev/null +++ b/src/service/models/promotionRecord.ts @@ -0,0 +1,31 @@ +import { Schema, model, models, Model } from 'mongoose'; +import { PromotionRecordSchema as PromotionRecordType } from '@/types/mongoSchema'; + +const PromotionRecordSchema = new Schema({ + userId: { + type: Schema.Types.ObjectId, + ref: 'user', + required: true + }, + objUId: { + type: Schema.Types.ObjectId, + ref: 'user', + required: false + }, + createTime: { + type: Date, + default: () => new Date() + }, + type: { + type: String, + required: true, + enum: ['invite', 'shareModel', 'withdraw'] + }, + amount: { + type: Number, + required: true + } +}); + +export const promotionRecord: Model = + models['promotionRecord'] || model('promotionRecord', PromotionRecordSchema); diff --git a/src/service/models/user.ts b/src/service/models/user.ts index 43878bcf4..34e389ab1 100644 --- a/src/service/models/user.ts +++ b/src/service/models/user.ts @@ -31,11 +31,6 @@ const UserSchema = new Schema({ // 返现比例 type: Number, default: 15 - }, - amount: { - // 推广金额 - type: Number, - default: 0 } }, openaiKey: { diff --git a/src/service/mongo.ts b/src/service/mongo.ts index 0aa947895..9902441e0 100644 --- a/src/service/mongo.ts +++ b/src/service/mongo.ts @@ -62,3 +62,4 @@ export * from './models/data'; export * from './models/dataItem'; export * from './models/splitData'; export * from './models/openapi'; +export * from './models/promotionRecord'; diff --git a/src/service/utils/promotion.ts b/src/service/utils/promotion.ts new file mode 100644 index 000000000..6790439e0 --- /dev/null +++ b/src/service/utils/promotion.ts @@ -0,0 +1,36 @@ +import { promotionRecord } from '../mongo'; + +export const pushPromotionRecord = async ({ + userId, + objUId, + type, + amount +}: { + userId: string; + objUId: string; + type: 'invite' | 'shareModel'; + amount: number; +}) => { + try { + await promotionRecord.create({ + userId, + objUId, + type, + amount + }); + } catch (error) { + console.log('创建推广记录异常', error); + } +}; + +export const withdrawRecord = async ({ userId, amount }: { userId: string; amount: number }) => { + try { + await promotionRecord.create({ + userId, + type: 'withdraw', + amount + }); + } catch (error) { + console.log('提现记录异常', error); + } +}; diff --git a/src/types/mongoSchema.d.ts b/src/types/mongoSchema.d.ts index 3ee2b7673..3103665f0 100644 --- a/src/types/mongoSchema.d.ts +++ b/src/types/mongoSchema.d.ts @@ -18,6 +18,9 @@ export interface UserModelSchema { promotionAmount: number; openaiKey: string; createTime: number; + promotion: { + rate: number; + }; } export interface AuthCodeSchema { @@ -162,3 +165,12 @@ export interface OpenApiSchema { lastUsedTime?: Date; apiKey: String; } + +export interface PromotionRecordSchema { + _id: string; + userId: string; // 收益人 + objUId?: string; // 目标对象(如果是withdraw则为空) + type: 'invite' | 'shareModel' | 'withdraw'; + createTime: Date; // 记录时间 + amount: number; +} diff --git a/src/types/user.d.ts b/src/types/user.d.ts index a17df20b3..a842ab16c 100644 --- a/src/types/user.d.ts +++ b/src/types/user.d.ts @@ -3,6 +3,9 @@ export interface UserType { username: string; openaiKey: string; balance: number; + promotion: { + rate: number; + }; } export interface UserUpdateParams {