From 7a231c6501f1c892c80d7b3a86b883dc29369f26 Mon Sep 17 00:00:00 2001 From: archer <545436317@qq.com> Date: Sun, 20 Aug 2023 20:35:48 +0800 Subject: [PATCH] invite url --- client/public/locales/en/common.json | 9 ++ client/public/locales/zh/common.json | 15 +- client/src/api/user.ts | 16 +- .../components/Icon/icons/light/promotion.svg | 8 + client/src/components/Icon/index.tsx | 3 +- client/src/components/SideTabs/index.tsx | 6 +- client/src/components/Tabs/index.tsx | 3 + client/src/constants/user.ts | 11 +- client/src/pages/_app.tsx | 5 + .../pages/account/components/Promotion.tsx | 148 ++++++++++++++++++ client/src/pages/account/index.tsx | 39 +++-- client/src/pages/api/user/account/gitLogin.ts | 10 +- .../api/user/promotion/getPromotionData.ts | 51 ++++++ .../pages/api/user/promotion/getPromotions.ts | 44 ++++++ .../pages/login/components/RegisterForm.tsx | 5 +- client/src/pages/login/provider.tsx | 5 +- client/src/service/models/promotionRecord.ts | 2 +- client/src/service/models/user.ts | 5 +- client/src/types/mongoSchema.d.ts | 2 +- client/src/types/user.d.ts | 1 + 20 files changed, 345 insertions(+), 43 deletions(-) create mode 100644 client/src/components/Icon/icons/light/promotion.svg create mode 100644 client/src/pages/account/components/Promotion.tsx create mode 100644 client/src/pages/api/user/promotion/getPromotionData.ts create mode 100644 client/src/pages/api/user/promotion/getPromotions.ts diff --git a/client/public/locales/en/common.json b/client/public/locales/en/common.json index b83cef9b2..a9bb9495e 100644 --- a/client/public/locales/en/common.json +++ b/client/public/locales/en/common.json @@ -119,17 +119,26 @@ }, "user": { "Account": "Account", + "Amount of earnings": "Earnings", + "Amount of inviter": "Inviter", "Application Name": "Application Name", "Avatar": "Avatar", "Balance": "Balance", "Bill Detail": "Bill Detail", "Change": "Change", + "Copy invite url": "Copy invitation link", + "Invite Url": "Invite Url", + "Invite url tip": "Friends who register through this link will be permanently bound to you, and you will get a certain balance reward when they recharge. In addition, when friends register with their mobile phone number, you will get 5 yuan reward immediately.", "Notice": "Notice", "Old password is error": "Old password is error", "OpenAI Account Setting": "OpenAI Account Setting", "Password": "Password", "Pay": "Pay", "Personal Information": "Personal", + "Promotion": "Promotion", + "Promotion Rate": "Promotion Rate", + "Promotion Record": "Promotion", + "Promotion rate tip": "You will be rewarded with a percentage of the balance when your friends top up", "Recharge Record": "Recharge", "Replace": "Replace", "Set OpenAI Account Failed": "Set OpenAI account failed", diff --git a/client/public/locales/zh/common.json b/client/public/locales/zh/common.json index 9ad05aacd..2022da5fa 100644 --- a/client/public/locales/zh/common.json +++ b/client/public/locales/zh/common.json @@ -119,17 +119,26 @@ }, "user": { "Account": "账号", + "Amount of earnings": "收益(¥)", + "Amount of inviter": "累计邀请人数", "Application Name": "应用名", "Avatar": "头像", "Balance": "余额", "Bill Detail": "账单详情", "Change": "变更", + "Copy invite url": "复制邀请链接", + "Invite Url": "邀请链接", + "Invite url tip": "通过该链接注册的好友将永久与你绑定,其充值时你会获得一定余额奖励。\n此外,好友使用手机号注册时,你将立即获得 5 元奖励。", "Notice": "通知", "Old password is error": "旧密码错误", "OpenAI Account Setting": "OpenAI 账号配置", "Password": "密码", "Pay": "充值", "Personal Information": "个人信息", + "Promotion": "", + "Promotion Rate": "返现比例", + "Promotion Record": "推广记录", + "Promotion rate tip": "好友充值时你将获得一定比例的余额奖励", "Recharge Record": "充值记录", "Replace": "更换", "Set OpenAI Account Failed": "设置 OpenAI 账号异常", @@ -140,6 +149,10 @@ "Update Password": "修改密码", "Update password failed": "修改密码异常", "Update password succseful": "修改密码成功", - "Usage Record": "使用记录" + "Usage Record": "使用记录", + "promption": { + "register": "好友注册", + "pay": "好友充值" + } } } diff --git a/client/src/api/user.ts b/client/src/api/user.ts index bab130666..91b639396 100644 --- a/client/src/api/user.ts +++ b/client/src/api/user.ts @@ -1,6 +1,6 @@ import { GET, POST, PUT } from './request'; import { createHashPassword } from '@/utils/tools'; -import { ResLogin } from './response/user'; +import type { ResLogin, PromotionRecordType } from './response/user'; import { UserAuthTypeEnum } from '@/constants/common'; import { UserBillType, UserType, UserUpdateParams } from '@/types/user'; import type { PagingData, RequestPaging } from '@/types'; @@ -13,7 +13,8 @@ export const sendAuthCode = (data: { }) => POST('/user/sendAuthCode', data); export const getTokenLogin = () => GET('/user/account/tokenLogin'); -export const gitLogin = (code: string) => GET('/user/account/gitLogin', { code }); +export const gitLogin = (params: { code: string; inviterId?: string }) => + GET('/user/account/gitLogin', params); export const postRegister = ({ username, @@ -82,3 +83,14 @@ export const getInforms = (data: RequestPaging) => export const getUnreadCount = () => GET(`/user/inform/countUnread`); export const readInform = (id: string) => GET(`/user/inform/read`, { id }); + +/* get promotion init data */ +export const getPromotionInitData = () => + GET<{ + invitedAmount: number; + earningsAmount: number; + }>('/user/promotion/getPromotionData'); + +/* promotion records */ +export const getPromotionRecords = (data: RequestPaging) => + POST(`/user/promotion/getPromotions`, data); diff --git a/client/src/components/Icon/icons/light/promotion.svg b/client/src/components/Icon/icons/light/promotion.svg new file mode 100644 index 000000000..ef2fc589e --- /dev/null +++ b/client/src/components/Icon/icons/light/promotion.svg @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/client/src/components/Icon/index.tsx b/client/src/components/Icon/index.tsx index f3abe9da9..6f68bc736 100644 --- a/client/src/components/Icon/index.tsx +++ b/client/src/components/Icon/index.tsx @@ -75,7 +75,8 @@ const map = { outlink_iframe: require('./icons/outlink/iframe.svg').default, addCircle: require('./icons/circle/add.svg').default, playFill: require('./icons/fill/play.svg').default, - courseLight: require('./icons/light/course.svg').default + courseLight: require('./icons/light/course.svg').default, + promotionLight: require('./icons/light/promotion.svg').default }; export type IconName = keyof typeof map; diff --git a/client/src/components/SideTabs/index.tsx b/client/src/components/SideTabs/index.tsx index 698ee679f..4a42084ac 100644 --- a/client/src/components/SideTabs/index.tsx +++ b/client/src/components/SideTabs/index.tsx @@ -1,8 +1,7 @@ import React, { useMemo } from 'react'; -import { Box, Flex, useTheme } from '@chakra-ui/react'; +import { Box, Flex } from '@chakra-ui/react'; import type { GridProps } from '@chakra-ui/react'; import MyIcon, { type IconName } from '../Icon'; -import { useTranslation } from 'next-i18next'; // @ts-ignore export interface Props extends GridProps { @@ -13,7 +12,6 @@ export interface Props extends GridProps { } const SideTabs = ({ list, size = 'md', activeId, onChange, ...props }: Props) => { - const { t } = useTranslation(); const sizeMap = useMemo(() => { switch (size) { case 'sm': @@ -63,7 +61,7 @@ const SideTabs = ({ list, size = 'md', activeId, onChange, ...props }: Props) => }} > - {t(item.label)} + {item.label} ))} diff --git a/client/src/components/Tabs/index.tsx b/client/src/components/Tabs/index.tsx index d84e1c04e..fd090b2e6 100644 --- a/client/src/components/Tabs/index.tsx +++ b/client/src/components/Tabs/index.tsx @@ -42,6 +42,7 @@ const Tabs = ({ list, size = 'md', activeId, onChange, ...props }: Props) => { p={sizeMap.outP} borderRadius={'sm'} fontSize={sizeMap.fontSize} + overflowX={'auto'} {...props} > {list.map((item) => ( @@ -50,6 +51,8 @@ const Tabs = ({ list, size = 'md', activeId, onChange, ...props }: Props) => { py={sizeMap.inlineP} textAlign={'center'} borderBottom={'2px solid transparent'} + px={3} + whiteSpace={'nowrap'} {...(activeId === item.id ? { color: 'myBlue.700', diff --git a/client/src/constants/user.ts b/client/src/constants/user.ts index 9d6d22dfb..b49f67e64 100644 --- a/client/src/constants/user.ts +++ b/client/src/constants/user.ts @@ -16,17 +16,10 @@ export const BillSourceMap: Record<`${BillSourceEnum}`, string> = { }; export enum PromotionEnum { - invite = 'invite', - shareModel = 'shareModel', - withdraw = 'withdraw' + register = 'register', + pay = 'pay' } -export const PromotionTypeMap = { - [PromotionEnum.invite]: '好友充值', - [PromotionEnum.shareModel]: '应用分享', - [PromotionEnum.withdraw]: '提现' -}; - export enum InformTypeEnum { system = 'system' } diff --git a/client/src/pages/_app.tsx b/client/src/pages/_app.tsx index 3bb826757..7fe3d1812 100644 --- a/client/src/pages/_app.tsx +++ b/client/src/pages/_app.tsx @@ -35,6 +35,7 @@ const queryClient = new QueryClient({ function App({ Component, pageProps }: AppProps) { const router = useRouter(); + const { hiId } = router.query as { hiId?: string }; const { i18n } = useTranslation(); const [scripts, setScripts] = useState([]); @@ -50,6 +51,10 @@ function App({ Component, pageProps }: AppProps) { })(); }, []); + useEffect(() => { + hiId && localStorage.setItem('inviterId', hiId); + }, [hiId]); + useEffect(() => { const lang = getLangStore() || 'zh'; i18n?.changeLanguage?.(lang); diff --git a/client/src/pages/account/components/Promotion.tsx b/client/src/pages/account/components/Promotion.tsx new file mode 100644 index 000000000..a2d8148d4 --- /dev/null +++ b/client/src/pages/account/components/Promotion.tsx @@ -0,0 +1,148 @@ +import React from 'react'; +import { + Grid, + Box, + Flex, + BoxProps, + useTheme, + Button, + Table, + Thead, + Tbody, + Tr, + Th, + Td, + TableContainer +} from '@chakra-ui/react'; +import { useTranslation } from 'next-i18next'; +import { useQuery } from '@tanstack/react-query'; +import { getPromotionInitData, getPromotionRecords } from '@/api/user'; +import { useUserStore } from '@/store/user'; +import { useLoading } from '@/hooks/useLoading'; + +import MyTooltip from '@/components/MyTooltip'; +import { QuestionOutlineIcon } from '@chakra-ui/icons'; +import { useCopyData } from '@/utils/tools'; +import { usePagination } from '@/hooks/usePagination'; +import { PromotionRecordType } from '@/api/response/user'; +import MyIcon from '@/components/Icon'; +import dayjs from 'dayjs'; + +const Promotion = () => { + const { t } = useTranslation(); + const theme = useTheme(); + const { copyData } = useCopyData(); + const { userInfo } = useUserStore(); + const { Loading } = useLoading(); + + const { + data: promotionRecords, + isLoading, + total, + pageSize, + Pagination + } = usePagination({ + api: getPromotionRecords + }); + + const { data: { invitedAmount = 0, earningsAmount = 0 } = {} } = useQuery( + ['getPromotionInitData'], + getPromotionInitData + ); + + const statisticsStyles: BoxProps = { + p: [4, 5], + border: theme.borders.base, + textAlign: 'center', + fontSize: ['md', 'xl'], + borderRadius: 'md' + }; + const titleStyles: BoxProps = { + mt: 2, + fontSize: ['lg', '28px'], + fontWeight: 'bold' + }; + + return ( + + + + {t('user.Amount of inviter')} + {invitedAmount} + + + {t('user.Amount of earnings')} + {earningsAmount} + + + + {t('user.Promotion Rate')} + + + + + {userInfo?.promotionRate || 15}% + + + + {t('user.Invite Url')} + + + + + + + + + + + + + + + + + + + {promotionRecords.map((item) => ( + + + + + + ))} + +
时间类型金额
+ {item.createTime ? dayjs(item.createTime).format('YYYY/MM/DD HH:mm:ss') : '-'} + {t(`user.promotion.${item.type}`)}{item.amount}
+
+ + {!isLoading && promotionRecords.length === 0 && ( + + + + 无邀请记录~ + + + )} + {total > pageSize && ( + + + + )} + +
+
+ ); +}; + +export default Promotion; diff --git a/client/src/pages/account/index.tsx b/client/src/pages/account/index.tsx index f1f2e0ff3..d15e4b71a 100644 --- a/client/src/pages/account/index.tsx +++ b/client/src/pages/account/index.tsx @@ -12,7 +12,11 @@ import Tabs from '@/components/Tabs'; import UserInfo from './components/Info'; import { serviceSideProps } from '@/utils/i18n'; import { feConfigs } from '@/store/static'; +import { useTranslation } from 'react-i18next'; +const Promotion = dynamic(() => import('./components/Promotion'), { + ssr: false +}); const BillTable = dynamic(() => import('./components/BillTable'), { ssr: false }); @@ -25,6 +29,7 @@ const InformTable = dynamic(() => import('./components/InformTable'), { enum TabEnum { 'info' = 'info', + 'promotion' = 'promotion', 'bill' = 'bill', 'pay' = 'pay', 'inform' = 'inform', @@ -32,40 +37,42 @@ enum TabEnum { } const Account = ({ currentTab }: { currentTab: `${TabEnum}` }) => { + const { t } = useTranslation(); const tabList = useRef([ { icon: 'meLight', - label: 'user.Personal Information', - id: TabEnum.info, - Component: + label: t('user.Personal Information'), + id: TabEnum.info }, + { icon: 'billRecordLight', - label: 'user.Usage Record', - id: TabEnum.bill, - Component: + label: t('user.Usage Record'), + id: TabEnum.bill }, ...(feConfigs?.show_userDetail ? [ + { + icon: 'promotionLight', + label: t('user.Promotion Record'), + id: TabEnum.promotion + }, { icon: 'payRecordLight', - label: 'user.Recharge Record', - id: TabEnum.pay, - Component: + label: t('user.Recharge Record'), + id: TabEnum.pay } ] : []), { icon: 'informLight', - label: 'user.Notice', - id: TabEnum.inform, - Component: + label: t('user.Notice'), + id: TabEnum.inform }, { icon: 'loginoutLight', - label: 'user.Sign Out', - id: TabEnum.loginout, - Component: () => <> + label: t('user.Sign Out'), + id: TabEnum.loginout } ]); @@ -122,7 +129,6 @@ const Account = ({ currentTab }: { currentTab: `${TabEnum}` }) => { ({ id: item.id, @@ -136,6 +142,7 @@ const Account = ({ currentTab }: { currentTab: `${TabEnum}` }) => { {currentTab === TabEnum.info && } + {currentTab === TabEnum.promotion && } {currentTab === TabEnum.bill && } {currentTab === TabEnum.pay && } {currentTab === TabEnum.inform && } diff --git a/client/src/pages/api/user/account/gitLogin.ts b/client/src/pages/api/user/account/gitLogin.ts index 8bf16eb46..5ec281764 100644 --- a/client/src/pages/api/user/account/gitLogin.ts +++ b/client/src/pages/api/user/account/gitLogin.ts @@ -24,7 +24,7 @@ type GithubUserType = { export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { - const { code } = req.query as { code: string }; + const { code, inviterId } = req.query as { code: string; inviterId?: string }; const { data: gitAccessToken } = await axios.post( `https://github.com/login/oauth/access_token?client_id=${global.feConfigs.gitLoginKey}&client_secret=${global.systemEnv.gitLoginSecret}&code=${code}` @@ -51,7 +51,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< } catch (err: any) { if (err?.code === 500) { jsonRes(res, { - data: await registerUser({ username, avatar: avatar_url, res }) + data: await registerUser({ username, avatar: avatar_url, res, inviterId }) }); return; } @@ -88,17 +88,21 @@ export async function loginByUsername({ export async function registerUser({ username, avatar, + inviterId, res }: { username: string; avatar?: string; + inviterId?: string; res: NextApiResponse; }) { const response = await User.create({ username, avatar, - password: nanoid() + password: nanoid(), + inviterId }); + console.log(response, '-=-=-='); // 根据 id 获取用户信息 const user = await User.findById(response._id); diff --git a/client/src/pages/api/user/promotion/getPromotionData.ts b/client/src/pages/api/user/promotion/getPromotionData.ts new file mode 100644 index 000000000..489def930 --- /dev/null +++ b/client/src/pages/api/user/promotion/getPromotionData.ts @@ -0,0 +1,51 @@ +// 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 { authUser } from '@/service/utils/auth'; +import mongoose from 'mongoose'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + await connectToDatabase(); + const { userId } = await authUser({ req, authToken: true }); + + 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 字段 + } + } + ]); + + jsonRes(res, { + data: { + invitedAmount, + historyAmount: countHistory[0]?.totalAmount || 0 + } + }); + } catch (err) { + jsonRes(res, { + code: 500, + error: err + }); + } +} diff --git a/client/src/pages/api/user/promotion/getPromotions.ts b/client/src/pages/api/user/promotion/getPromotions.ts new file mode 100644 index 000000000..854abe373 --- /dev/null +++ b/client/src/pages/api/user/promotion/getPromotions.ts @@ -0,0 +1,44 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { jsonRes } from '@/service/response'; +import { connectToDatabase, promotionRecord } from '@/service/mongo'; +import { authUser } from '@/service/utils/auth'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + let { pageNum = 1, pageSize = 10 } = req.body as { + pageNum: number; + pageSize: number; + }; + + const { userId } = await authUser({ req, authToken: true }); + + 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/client/src/pages/login/components/RegisterForm.tsx b/client/src/pages/login/components/RegisterForm.tsx index e2c5f236e..20f4a25b5 100644 --- a/client/src/pages/login/components/RegisterForm.tsx +++ b/client/src/pages/login/components/RegisterForm.tsx @@ -24,7 +24,6 @@ interface RegisterType { } const RegisterForm = ({ setPageType, loginSuccess }: Props) => { - const { inviterId = '' } = useRouter().query as { inviterId: string }; const { toast } = useToast(); const { register, @@ -58,7 +57,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => { username, code, password, - inviterId: inviterId || localStorage.getItem('inviterId') || '' + inviterId: localStorage.getItem('inviterId') || '' }) ); toast({ @@ -81,7 +80,7 @@ const RegisterForm = ({ setPageType, loginSuccess }: Props) => { } setRequesting(false); }, - [inviterId, loginSuccess, toast] + [loginSuccess, toast] ); return ( diff --git a/client/src/pages/login/provider.tsx b/client/src/pages/login/provider.tsx index 3cac7c062..ef3a3e7b2 100644 --- a/client/src/pages/login/provider.tsx +++ b/client/src/pages/login/provider.tsx @@ -44,7 +44,10 @@ const provider = ({ code }: { code: string }) => { try { const res = await (async () => { if (loginStore.provider === 'git') { - return gitLogin(code); + return gitLogin({ + code, + inviterId: localStorage.getItem('inviterId') || '' + }); } return null; })(); diff --git a/client/src/service/models/promotionRecord.ts b/client/src/service/models/promotionRecord.ts index 6906c4432..c73163ac3 100644 --- a/client/src/service/models/promotionRecord.ts +++ b/client/src/service/models/promotionRecord.ts @@ -19,7 +19,7 @@ const PromotionRecordSchema = new Schema({ type: { type: String, required: true, - enum: ['invite', 'shareModel', 'withdraw'] + enum: ['invite', 'register'] }, amount: { type: Number, diff --git a/client/src/service/models/user.ts b/client/src/service/models/user.ts index baec31964..293ca1c6d 100644 --- a/client/src/service/models/user.ts +++ b/client/src/service/models/user.ts @@ -26,7 +26,6 @@ const UserSchema = new Schema({ default: '/icon/human.png' }, balance: { - // 平台余额,不可提现 type: Number, default: 2 * PRICE_SCALE }, @@ -35,6 +34,10 @@ const UserSchema = new Schema({ type: Schema.Types.ObjectId, ref: 'user' }, + promotionRate: { + type: Number, + default: 15 + }, limit: { exportKbTime: { // Every half hour diff --git a/client/src/types/mongoSchema.d.ts b/client/src/types/mongoSchema.d.ts index 57c15d509..8eb3d638a 100644 --- a/client/src/types/mongoSchema.d.ts +++ b/client/src/types/mongoSchema.d.ts @@ -13,8 +13,8 @@ export interface UserModelSchema { password: string; avatar: string; balance: number; + promotionRate: number; inviterId?: string; - promotionAmount: number; openaiKey: string; createTime: number; openaiAccount?: { diff --git a/client/src/types/user.d.ts b/client/src/types/user.d.ts index f8e8d6f60..751a04a41 100644 --- a/client/src/types/user.d.ts +++ b/client/src/types/user.d.ts @@ -5,6 +5,7 @@ export interface UserType { username: string; avatar: string; balance: number; + promotionRate: UserModelSchema['promotionRate']; openaiAccount: UserModelSchema['openaiAccount']; }