mirror of
https://github.com/labring/sealos.git
synced 2026-09-24 15:46:19 +08:00
refactor(costcenter): refactor invoice (#4694)
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { MongoClient, WithId } from 'mongodb';
|
||||
import { globalPrisma, prisma } from '@/services/backend/db/init';
|
||||
import { Prisma } from '@prisma/client/extension';
|
||||
|
||||
export type TInvoiceDetail = {
|
||||
title: string;
|
||||
tax: string;
|
||||
bank: string;
|
||||
bankAccount: string;
|
||||
address?: string;
|
||||
phone?: string;
|
||||
fax?: string;
|
||||
};
|
||||
export type TInvoiceContract = {
|
||||
person: string;
|
||||
phone: string;
|
||||
email: string;
|
||||
};
|
||||
|
||||
export type Tbilling = {
|
||||
order_id: string;
|
||||
amount: number;
|
||||
createdTime: Date;
|
||||
};
|
||||
|
||||
export type InvoicesCollection = {
|
||||
amount: number;
|
||||
detail: TInvoiceDetail;
|
||||
billings: Tbilling[];
|
||||
contract: TInvoiceContract;
|
||||
k8s_user: string;
|
||||
createdTime: Date;
|
||||
};
|
||||
const mongodb = new MongoClient(process.env.MONGODB_URI_COSTCENTER!);
|
||||
const client = mongodb.db();
|
||||
|
||||
async function pullInvoiceData() {
|
||||
const invoicesCollection = client.collection<InvoicesCollection>('invoices');
|
||||
const billings = await invoicesCollection
|
||||
.aggregate<Tbilling>([
|
||||
{ $unwind: '$billings' },
|
||||
{
|
||||
$replaceRoot: {
|
||||
newRoot: '$billings'
|
||||
}
|
||||
}
|
||||
])
|
||||
.toArray();
|
||||
const data = billings.map((b) => b.order_id);
|
||||
// console.log(data)
|
||||
const result = await globalPrisma.$transaction(async (tx) => {
|
||||
const existData = await tx.payment.findMany({
|
||||
where: {
|
||||
id: {
|
||||
in: data
|
||||
},
|
||||
invoiced_at: false
|
||||
},
|
||||
select: {
|
||||
id: true
|
||||
}
|
||||
});
|
||||
console.log(existData);
|
||||
return await tx.payment.updateMany({
|
||||
where: {
|
||||
id: {
|
||||
in: existData.map((d) => d.id)
|
||||
}
|
||||
},
|
||||
data: {
|
||||
invoiced_at: true
|
||||
}
|
||||
});
|
||||
});
|
||||
console.log(result);
|
||||
}
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
if (process.env.migrate !== 'true') return;
|
||||
console.log('pullInvoice');
|
||||
await pullInvoiceData();
|
||||
console.log('ok');
|
||||
return res.json('ok');
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return res.json('error');
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@
|
||||
"Deduction": "扣费",
|
||||
"Total Cost": "总成本",
|
||||
"Cost Distribution": "成本分布",
|
||||
"Income And Expense": "收支",
|
||||
"Expenditure": "支出",
|
||||
"Recharge Amount": "充值金额",
|
||||
"Select Amount": "选择金额",
|
||||
|
||||
@@ -91,7 +91,8 @@ export default function RechargeTabPanel() {
|
||||
id: TableHeaderID.TransactionTime,
|
||||
enablePinning: true,
|
||||
cell(props) {
|
||||
return format(parseISO(props.cell.getValue()), 'MM-dd HH:mm');
|
||||
const date = new Date(props.cell.getValue());
|
||||
return format(date, 'MM-dd HH:mm');
|
||||
}
|
||||
}),
|
||||
columnHelper.accessor((row) => row.Amount, {
|
||||
|
||||
@@ -41,7 +41,9 @@ export function Buget() {
|
||||
return (
|
||||
<Flex direction={'column'} mb={'34px'}>
|
||||
<Flex alignItems={'center'} justify="space-between">
|
||||
<Heading size="sm">{t('Income And Expense')}</Heading>
|
||||
<Text color={'#747F88'} mb={'5px'}>
|
||||
{t('Income And Expense')}
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex mt="20px" justify={'space-evenly'} gap="6">
|
||||
{list.map((v) => (
|
||||
|
||||
@@ -2,9 +2,10 @@ import { InvoiceTableHeaders } from '@/constants/billing';
|
||||
import { Checkbox, Flex, Table, TableContainer, Tbody, Td, Th, Thead, Tr } from '@chakra-ui/react';
|
||||
import { format } from 'date-fns';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { ReqGenInvoice } from '@/types';
|
||||
import { RechargeBillingItem, ReqGenInvoice } from '@/types';
|
||||
import CurrencySymbol from '../CurrencySymbol';
|
||||
import useEnvStore from '@/stores/env';
|
||||
import { formatMoney } from '@/utils/format';
|
||||
|
||||
export function InvoiceTable({
|
||||
data,
|
||||
@@ -13,7 +14,7 @@ export function InvoiceTable({
|
||||
}: {
|
||||
data: ReqGenInvoice['billings'];
|
||||
selectbillings: ReqGenInvoice['billings'];
|
||||
onSelect?: (type: boolean, item: ReqGenInvoice['billings'][0]) => void;
|
||||
onSelect?: (type: boolean, item: RechargeBillingItem) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const needSelect = !!onSelect;
|
||||
@@ -37,7 +38,11 @@ export function InvoiceTable({
|
||||
>
|
||||
<Flex display={'flex'}>
|
||||
{t(item)}
|
||||
{item !== 'Order Number' && <CurrencySymbol type={currency} />}
|
||||
{item === 'True Amount' && (
|
||||
<>
|
||||
(<CurrencySymbol type={currency} />)
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
</Th>
|
||||
))}
|
||||
@@ -45,12 +50,12 @@ export function InvoiceTable({
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{data.map((item) => (
|
||||
<Tr key={item.order_id} fontSize={'12px'}>
|
||||
<Tr key={item.ID} fontSize={'12px'}>
|
||||
<Td>
|
||||
<Flex align={'center'}>
|
||||
{needSelect && (
|
||||
<Checkbox
|
||||
isChecked={selectbillings.map((b) => b.order_id).includes(item.order_id)}
|
||||
isChecked={selectbillings.map((b) => b.ID).includes(item.ID)}
|
||||
onChange={(v) => {
|
||||
onSelect(v.target.checked, item);
|
||||
}}
|
||||
@@ -59,11 +64,11 @@ export function InvoiceTable({
|
||||
h="12px"
|
||||
/>
|
||||
)}
|
||||
{item.order_id}
|
||||
{item.ID}
|
||||
</Flex>
|
||||
</Td>
|
||||
<Td>{format(item.createdTime, 'MM-dd HH:mm')}</Td>
|
||||
<Td color={'#219BF4'}>{item.amount}</Td>
|
||||
<Td>{format(new Date(item.CreatedAt), 'MM-dd HH:mm')}</Td>
|
||||
<Td color={'#219BF4'}>{item.Amount}</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/backend/response';
|
||||
import { authSession } from '@/service/backend/auth';
|
||||
import { getAllInvoicesByK8sUser } from '@/service/backend/db/invoice';
|
||||
import { enableInvoice } from '@/service/enabled';
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
if (!enableInvoice()) {
|
||||
throw new Error('invoice is not enabled');
|
||||
}
|
||||
const kc = await authSession(req.headers);
|
||||
const user = kc.getCurrentUser();
|
||||
if (user === null) {
|
||||
return jsonRes(res, { code: 401, message: 'user null' });
|
||||
}
|
||||
const invoices = await getAllInvoicesByK8sUser({ k8s_user: user.name });
|
||||
|
||||
return jsonRes(res, {
|
||||
message: 'successfully',
|
||||
data: {
|
||||
billings: invoices.flatMap((invoice) => invoice.billings.map((billing) => billing.order_id))
|
||||
},
|
||||
code: 200
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
jsonRes(res, {
|
||||
message: 'Failed to get billings',
|
||||
code: 500
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,32 @@
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
// import twilio from 'twilio';
|
||||
//@ts-ignore
|
||||
import Dysmsapi, * as dysmsapi from '@alicloud/dysmsapi20170525';
|
||||
//@ts-ignore
|
||||
import * as OpenApi from '@alicloud/openapi-client';
|
||||
//@ts-ignore
|
||||
import * as Util from '@alicloud/tea-util';
|
||||
import { jsonRes } from '@/service/backend/response';
|
||||
import { addOrUpdateCode, checkSendable } from '@/service/backend/db/verifyCode';
|
||||
import { retrySerially } from '@/utils/tools';
|
||||
import { getClientIPFromRequest, retrySerially } from '@/utils/tools';
|
||||
import { authSession } from '@/service/backend/auth';
|
||||
import { enableInvoice } from '@/service/enabled';
|
||||
import * as process from 'process';
|
||||
const accessKeyId = process.env.ALI_ACCESS_KEY_ID;
|
||||
const accessKeySecret = process.env.ALI_ACCESS_KEY_SECRET;
|
||||
const templateCode = process.env.ALI_TEMPLATE_CODE;
|
||||
const signName = process.env.ALI_SIGN_NAME;
|
||||
const requestTimestamps: Record<string, number> = {};
|
||||
function checkRequestFrequency(ipAddress: string) {
|
||||
const currentTime = Date.now();
|
||||
const lastRequestTime = requestTimestamps[ipAddress] || 0;
|
||||
const timeDiff = currentTime - lastRequestTime;
|
||||
|
||||
const requestInterval = 60 * 1000;
|
||||
|
||||
if (timeDiff < requestInterval) {
|
||||
return false;
|
||||
} else {
|
||||
requestTimestamps[ipAddress] = currentTime;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
if (!enableInvoice()) {
|
||||
@@ -27,10 +38,25 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
return jsonRes(res, { code: 401, message: 'user null' });
|
||||
}
|
||||
const { phoneNumbers } = req.body;
|
||||
if (!(await checkSendable(phoneNumbers))) {
|
||||
let ip = getClientIPFromRequest(req);
|
||||
|
||||
if (!ip) {
|
||||
if (process.env.NODE_ENV === 'development') ip = '127.0.0.1';
|
||||
else
|
||||
return jsonRes(res, {
|
||||
message: 'The IP is null',
|
||||
code: 403
|
||||
});
|
||||
}
|
||||
if (
|
||||
!(await checkSendable({
|
||||
phone: phoneNumbers,
|
||||
ip
|
||||
}))
|
||||
) {
|
||||
return jsonRes(res, {
|
||||
message: 'code already sent',
|
||||
code: 400
|
||||
message: 'Code already sent',
|
||||
code: 429
|
||||
});
|
||||
}
|
||||
|
||||
@@ -76,7 +102,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
}, 3);
|
||||
|
||||
// update cache
|
||||
await addOrUpdateCode({ phone: phoneNumbers, code });
|
||||
await addOrUpdateCode({ phone: phoneNumbers, code, ip });
|
||||
return jsonRes(res, {
|
||||
message: 'successfully',
|
||||
code: 200
|
||||
|
||||
@@ -26,6 +26,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
if (user === null) {
|
||||
return jsonRes(res, { code: 401, message: 'user null' });
|
||||
}
|
||||
|
||||
const { detail, contract, billings } = req.body as ReqGenInvoice;
|
||||
if (
|
||||
!detail ||
|
||||
@@ -44,6 +45,16 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
code: 400
|
||||
});
|
||||
}
|
||||
const url = process.env.BILLING_URI + '/account/v1alpha1/payment/set-invoice';
|
||||
const setInvoiceRes = await fetch(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
kubeConfig: kc.exportConfig(),
|
||||
owner: user.name,
|
||||
paymentIDList: billings.map((b) => b.ID)
|
||||
})
|
||||
});
|
||||
if (!setInvoiceRes.ok) throw Error('setInvocice error');
|
||||
if (
|
||||
process.env.NODE_ENV !== 'development' &&
|
||||
!(await checkCode({ phone: contract.phone, code: contract.code }))
|
||||
@@ -70,8 +81,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
detail,
|
||||
contract,
|
||||
billings: billings.map((item) => ({
|
||||
...item,
|
||||
createdTime: new Date(item.createdTime)
|
||||
order_id: item.ID,
|
||||
amount: item.Amount,
|
||||
regionUID: item.RegionUID,
|
||||
userUID: item.UserUID,
|
||||
createdTime: new Date(item.CreatedAt)
|
||||
}))
|
||||
};
|
||||
|
||||
@@ -85,7 +99,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
code: 500
|
||||
});
|
||||
}
|
||||
retrySerially(async () => {
|
||||
await retrySerially(async () => {
|
||||
try {
|
||||
const result = await sendToBot(document);
|
||||
if (result.StatusCode !== 0) {
|
||||
|
||||
@@ -39,6 +39,7 @@ import arrow_icon from '@/assert/Vector.svg';
|
||||
import email_icon from '@/assert/mdi_email-receive-outline.svg';
|
||||
import listIcon from '@/assert/list.svg';
|
||||
import { InvoiceTable } from '@/components/invoice/invoiceTable';
|
||||
|
||||
const BillingModal = ({
|
||||
billings,
|
||||
t,
|
||||
@@ -55,17 +56,13 @@ const BillingModal = ({
|
||||
const [currentPage, setcurrentPage] = useState(1);
|
||||
return (
|
||||
<>
|
||||
{' '}
|
||||
<ModalContent w={'910px'} maxW="910px" h={'auto'} maxH="620px">
|
||||
{' '}
|
||||
<ModalHeader display={'flex'}>
|
||||
{' '}
|
||||
{t('orders.invoiceOrder')}({invoiceCount}){' '}
|
||||
<Text color="rgba(29, 140, 220, 1)">¥ {invoiceAmount}</Text>{' '}
|
||||
</ModalHeader>{' '}
|
||||
<ModalCloseButton />{' '}
|
||||
{t('orders.invoiceOrder')}({invoiceCount})
|
||||
<Text color="rgba(29, 140, 220, 1)">¥ {invoiceAmount}</Text>
|
||||
</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
{' '}
|
||||
<Box
|
||||
overflow={'auto'}
|
||||
fontFamily="PingFang SC"
|
||||
@@ -73,28 +70,23 @@ const BillingModal = ({
|
||||
fontWeight="500"
|
||||
lineHeight="20px"
|
||||
>
|
||||
{' '}
|
||||
<Flex mb={'16px'} align="center">
|
||||
{' '}
|
||||
<Flex align={'center'}>
|
||||
{' '}
|
||||
<Img src={listIcon.src} w={'20px'} h={'20px'} mr={'6px'} />{' '}
|
||||
<Text>{t('orders.list')}</Text>{' '}
|
||||
</Flex>{' '}
|
||||
</Flex>{' '}
|
||||
<Img src={listIcon.src} w={'20px'} h={'20px'} mr={'6px'} />
|
||||
<Text>{t('orders.list')}</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<InvoiceTable
|
||||
selectbillings={billings.current || []}
|
||||
data={(billings.current || []).filter(
|
||||
(item, index) =>
|
||||
index <= pageSize * currentPage - 1 && index >= pageSize * (currentPage - 1)
|
||||
)}
|
||||
></InvoiceTable>{' '}
|
||||
</Box>{' '}
|
||||
></InvoiceTable>
|
||||
</Box>
|
||||
<Flex w="370px" h="32px" align={'center'} mt={'20px'} mx="auto">
|
||||
{' '}
|
||||
<Text>{t('Total')}:</Text> <Flex w="40px">{billings?.current?.length || 0}</Flex>{' '}
|
||||
<Text>{t('Total')}:</Text> <Flex w="40px">{billings?.current?.length || 0}</Flex>
|
||||
<Flex gap={'8px'}>
|
||||
{' '}
|
||||
<Button
|
||||
variant={'switchPage'}
|
||||
isDisabled={currentPage === 1}
|
||||
@@ -103,9 +95,8 @@ const BillingModal = ({
|
||||
setcurrentPage(1);
|
||||
}}
|
||||
>
|
||||
{' '}
|
||||
<Img w="6px" h="6px" src={arrow_left_icon.src}></Img>{' '}
|
||||
</Button>{' '}
|
||||
<Img w="6px" h="6px" src={arrow_left_icon.src}></Img>
|
||||
</Button>
|
||||
<Button
|
||||
variant={'switchPage'}
|
||||
isDisabled={currentPage === 1}
|
||||
@@ -114,10 +105,11 @@ const BillingModal = ({
|
||||
setcurrentPage(currentPage - 1);
|
||||
}}
|
||||
>
|
||||
{' '}
|
||||
<Img src={arrow_icon.src} transform={'rotate(-90deg)'}></Img>{' '}
|
||||
</Button>{' '}
|
||||
<Text>{currentPage}</Text>/<Text>{totalPage}</Text>{' '}
|
||||
<Img src={arrow_icon.src} transform={'rotate(-90deg)'}></Img>
|
||||
</Button>
|
||||
<Flex my={'auto'}>
|
||||
<Text>{currentPage}</Text>/<Text>{totalPage}</Text>
|
||||
</Flex>
|
||||
<Button
|
||||
variant={'switchPage'}
|
||||
isDisabled={currentPage === totalPage}
|
||||
@@ -127,9 +119,8 @@ const BillingModal = ({
|
||||
setcurrentPage(currentPage + 1);
|
||||
}}
|
||||
>
|
||||
{' '}
|
||||
<Img src={arrow_icon.src} transform={'rotate(90deg)'}></Img>{' '}
|
||||
</Button>{' '}
|
||||
<Img src={arrow_icon.src} transform={'rotate(90deg)'}></Img>
|
||||
</Button>
|
||||
<Button
|
||||
variant={'switchPage'}
|
||||
isDisabled={currentPage === totalPage}
|
||||
@@ -140,22 +131,17 @@ const BillingModal = ({
|
||||
setcurrentPage(totalPage);
|
||||
}}
|
||||
>
|
||||
{' '}
|
||||
<Img
|
||||
w="6px"
|
||||
h="6px"
|
||||
src={arrow_left_icon.src}
|
||||
transform={'rotate(180deg)'}
|
||||
></Img>{' '}
|
||||
</Button>{' '}
|
||||
</Flex>{' '}
|
||||
<Text>{pageSize}</Text> <Text>/{t('Page')}</Text>{' '}
|
||||
</Flex>{' '}
|
||||
</ModalBody>{' '}
|
||||
</ModalContent>{' '}
|
||||
<Img w="6px" h="6px" src={arrow_left_icon.src} transform={'rotate(180deg)'}></Img>
|
||||
</Button>
|
||||
</Flex>
|
||||
<Text>{pageSize}</Text> <Text>/{t('Page')}</Text>
|
||||
</Flex>
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
function InvoicdForm({
|
||||
invoiceAmount,
|
||||
invoiceCount,
|
||||
|
||||
@@ -1,35 +1,34 @@
|
||||
import { InvoiceTable } from '@/components/invoice/invoiceTable';
|
||||
import { Box, Button, Flex, Heading, Img, Input, Text } from '@chakra-ui/react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { endOfDay, formatISO, parseISO } from 'date-fns';
|
||||
import receipt_icon from '@/assert/invoice-active.svg';
|
||||
import arrow_icon from '@/assert/Vector.svg';
|
||||
import arrow_left_icon from '@/assert/toleft.svg';
|
||||
import magnifyingGlass_icon from '@/assert/magnifyingGlass.svg';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import request from '@/service/request';
|
||||
import { BillingData, BillingSpec } from '@/types/billing';
|
||||
import {
|
||||
BillingData,
|
||||
BillingSpec,
|
||||
RechargeBillingData,
|
||||
RechargeBillingItem
|
||||
} from '@/types/billing';
|
||||
import SelectRange from '@/components/billing/selectDateRange';
|
||||
import useOverviewStore from '@/stores/overview';
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { getCookie } from '@/utils/cookieUtils';
|
||||
import NotFound from '@/components/notFound';
|
||||
import { formatMoney } from '@/utils/format';
|
||||
import listIcon from '@/assert/list.svg';
|
||||
import { ReqGenInvoice } from '@/types';
|
||||
import { ApiResp, ReqGenInvoice } from '@/types';
|
||||
import InvoicdForm from './InvoicdForm';
|
||||
import { enableInvoice } from '@/service/enabled';
|
||||
import { formatMoney } from '@/utils/format';
|
||||
|
||||
function Invoice() {
|
||||
const { t, i18n } = useTranslation();
|
||||
const cookie = getCookie('NEXT_LOCALE');
|
||||
useEffect(() => {
|
||||
i18n.changeLanguage(cookie);
|
||||
}, [cookie, i18n]);
|
||||
const startTime = useOverviewStore((state) => state.startTime);
|
||||
const endTime = useOverviewStore((state) => state.endTime);
|
||||
const selectBillings = useRef<ReqGenInvoice['billings']>([]);
|
||||
const selectBillings = useRef<RechargeBillingItem[]>([]);
|
||||
const [searchValue, setSearch] = useState('');
|
||||
const [orderID, setOrderID] = useState('');
|
||||
const [totalPage, setTotalPage] = useState(1);
|
||||
@@ -39,64 +38,35 @@ function Invoice() {
|
||||
const [invoiceAmount, setInvoiceAmount] = useState(0);
|
||||
const [processState, setProcessState] = useState(0);
|
||||
const [invoiceCount, setInvoiceCount] = useState(0);
|
||||
const { data: filterData } = useQuery(['billing', 'invoice'], () => {
|
||||
return request<any, { data: { billings: string[] } }>('/api/invoice/billings');
|
||||
});
|
||||
const { data, isLoading, isSuccess } = useQuery(
|
||||
['billing', { currentPage, startTime, endTime, orderID }],
|
||||
async () => {
|
||||
let spec = {} as BillingSpec;
|
||||
spec = {
|
||||
page: currentPage,
|
||||
pageSize: pageSize,
|
||||
type: 1,
|
||||
startTime: formatISO(startTime, { representation: 'complete' }),
|
||||
// startTime,
|
||||
endTime: formatISO(endOfDay(endTime), { representation: 'complete' }),
|
||||
// endTime,
|
||||
orderID
|
||||
};
|
||||
const result = await request<any, { data: BillingData }, { spec: BillingSpec }>(
|
||||
'/api/billing',
|
||||
{
|
||||
method: 'POST',
|
||||
data: {
|
||||
spec
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const tableResult = result.data.status.item
|
||||
.filter((billing) => billing.type === 1)
|
||||
.map<ReqGenInvoice['billings'][0]>((billing) => ({
|
||||
createdTime: parseISO(billing.time).getTime(),
|
||||
order_id: billing.order_id,
|
||||
amount: formatMoney(billing.payment?.amount || billing.amount)
|
||||
}));
|
||||
return {
|
||||
tableResult,
|
||||
pageLength: result.data.status.pageLength,
|
||||
totalCount: result.data.status.totalCount || tableResult.length
|
||||
};
|
||||
[
|
||||
'billing',
|
||||
'invoice',
|
||||
{
|
||||
startTime,
|
||||
endTime
|
||||
}
|
||||
],
|
||||
() => {
|
||||
return request<any, ApiResp<RechargeBillingData>>('/api/billing/recharge', {
|
||||
data: {
|
||||
startTime,
|
||||
endTime
|
||||
},
|
||||
method: 'POST'
|
||||
});
|
||||
},
|
||||
{
|
||||
onSuccess(data) {
|
||||
const totalPage = data.pageLength;
|
||||
if (totalPage === 0) {
|
||||
// 搜索时
|
||||
setTotalPage(1);
|
||||
return;
|
||||
}
|
||||
setTotalPage(totalPage);
|
||||
},
|
||||
staleTime: 1000,
|
||||
cacheTime: 0,
|
||||
enabled: filterData !== undefined
|
||||
select(data) {
|
||||
return ((data?.data?.payment || []) as RechargeBillingItem[])
|
||||
.filter((d) => !d.InvoicedAt)
|
||||
.map((d) => ({
|
||||
...d,
|
||||
Amount: formatMoney(d.Amount)
|
||||
}));
|
||||
}
|
||||
}
|
||||
);
|
||||
const billingFilter = <T extends { order_id: string }>(billing: T): boolean =>
|
||||
!(filterData?.data.billings || []).includes(billing.order_id);
|
||||
let tableResult = data?.tableResult?.filter(billingFilter) || [];
|
||||
|
||||
return (
|
||||
<Flex flexDirection="column" w="100%" h="100%" bg={'white'} p="24px" overflow={'auto'}>
|
||||
@@ -164,7 +134,7 @@ function Invoice() {
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
{isSuccess && tableResult.length > 0 ? (
|
||||
{isSuccess ? (
|
||||
<>
|
||||
<Box
|
||||
overflow={'auto'}
|
||||
@@ -205,17 +175,17 @@ function Invoice() {
|
||||
</Flex>
|
||||
<InvoiceTable
|
||||
selectbillings={selectBillings.current || []}
|
||||
data={[...tableResult]}
|
||||
data={data}
|
||||
onSelect={(checked, item) => {
|
||||
if (checked) {
|
||||
setInvoiceAmount(invoiceAmount + item.amount);
|
||||
setInvoiceAmount(invoiceAmount + item.Amount);
|
||||
setInvoiceCount(invoiceCount + 1);
|
||||
selectBillings.current.push({ ...item });
|
||||
} else {
|
||||
setInvoiceAmount(invoiceAmount - item.amount);
|
||||
setInvoiceAmount(invoiceAmount - item.Amount);
|
||||
setInvoiceCount(invoiceCount - 1);
|
||||
const idx = selectBillings.current.findIndex(
|
||||
(billing) => billing.order_id === item.order_id
|
||||
(billing) => billing.ID === item.ID
|
||||
);
|
||||
selectBillings.current.splice(idx, 1);
|
||||
}
|
||||
@@ -224,7 +194,7 @@ function Invoice() {
|
||||
</Box>
|
||||
<Flex w="370px" h="32px" align={'center'} mt={'20px'} mx="auto">
|
||||
<Text>{t('Total')}:</Text>
|
||||
<Flex w="40px">{data.totalCount - (filterData?.data?.billings || []).length}</Flex>
|
||||
<Flex w="40px">{data.length}</Flex>
|
||||
<Flex gap={'8px'}>
|
||||
<Button
|
||||
variant={'switchPage'}
|
||||
@@ -246,7 +216,9 @@ function Invoice() {
|
||||
>
|
||||
<Img src={arrow_icon.src} transform={'rotate(-90deg)'}></Img>
|
||||
</Button>
|
||||
<Text>{currentPage}</Text>/<Text>{totalPage}</Text>
|
||||
<Flex my={'auto'}>
|
||||
<Text>{currentPage}</Text>/<Text>{totalPage}</Text>
|
||||
</Flex>
|
||||
<Button
|
||||
variant={'switchPage'}
|
||||
isDisabled={currentPage === totalPage}
|
||||
@@ -317,9 +289,10 @@ function Invoice() {
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
export default Invoice;
|
||||
export async function getServerSideProps(content: any) {
|
||||
const locale = content?.req?.cookies?.NEXT_LOCALE || 'zh';
|
||||
|
||||
export async function getServerSideProps({ locale }: { locale: string }) {
|
||||
if (!enableInvoice()) {
|
||||
return {
|
||||
redirect: {
|
||||
@@ -330,7 +303,7 @@ export async function getServerSideProps(content: any) {
|
||||
}
|
||||
return {
|
||||
props: {
|
||||
...(await serverSideTranslations(locale, undefined, null, content.locales))
|
||||
...(await serverSideTranslations(locale, undefined, null, ['zh', 'en']))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { InvoicesCollection, Tbilling, TInvoiceContract, TInvoiceDetail } from '@/types';
|
||||
import {
|
||||
InvoicesCollection,
|
||||
RechargeBillingItem,
|
||||
Tbilling,
|
||||
TInvoiceContract,
|
||||
TInvoiceDetail
|
||||
} from '@/types';
|
||||
import { ObjectId } from 'mongodb';
|
||||
import { connectToDatabase } from './mongodb';
|
||||
|
||||
@@ -21,6 +27,7 @@ export async function addInvoice({
|
||||
k8s_user: string;
|
||||
}) {
|
||||
const invoices = await connectToUserCollection();
|
||||
|
||||
const result = await invoices.insertOne({
|
||||
detail,
|
||||
contract,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { connectToDatabase } from './mongodb';
|
||||
import { subMinutes } from 'date-fns';
|
||||
|
||||
type TVerification_Codes = {
|
||||
phone: string;
|
||||
code: string;
|
||||
createdTime: number;
|
||||
ip: string;
|
||||
createdTime: Date;
|
||||
};
|
||||
|
||||
async function connectToUserCollection() {
|
||||
@@ -15,7 +17,15 @@ async function connectToUserCollection() {
|
||||
}
|
||||
|
||||
// addOrUpdateCode
|
||||
export async function addOrUpdateCode({ phone, code }: { phone: string; code: string }) {
|
||||
export async function addOrUpdateCode({
|
||||
phone,
|
||||
code,
|
||||
ip
|
||||
}: {
|
||||
phone: string;
|
||||
code: string;
|
||||
ip: string;
|
||||
}) {
|
||||
const codes = await connectToUserCollection();
|
||||
const result = await codes.updateOne(
|
||||
{
|
||||
@@ -24,7 +34,9 @@ export async function addOrUpdateCode({ phone, code }: { phone: string; code: st
|
||||
{
|
||||
$set: {
|
||||
code,
|
||||
createdTime: new Date().getTime()
|
||||
phone,
|
||||
ip,
|
||||
createdTime: new Date()
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -33,19 +45,27 @@ export async function addOrUpdateCode({ phone, code }: { phone: string; code: st
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
// checkCode
|
||||
export async function checkSendable({ phone }: { phone: string }) {
|
||||
export async function checkSendable({ phone, ip }: { phone?: string; ip: string }) {
|
||||
const codes = await connectToUserCollection();
|
||||
const result = await codes.findOne({
|
||||
phone,
|
||||
$or: [
|
||||
{
|
||||
ip
|
||||
},
|
||||
{
|
||||
phone
|
||||
}
|
||||
],
|
||||
createdTime: {
|
||||
// 在区间范围内找到就是已经发送过了,不能再发了
|
||||
$gt: new Date().getTime() - 60 * 1000,
|
||||
$lt: new Date().getTime()
|
||||
$gt: subMinutes(new Date(), 1),
|
||||
$lt: new Date()
|
||||
}
|
||||
});
|
||||
return !result;
|
||||
}
|
||||
|
||||
// checkCode
|
||||
export async function checkCode({ phone, code }: { phone: string; code: string }) {
|
||||
const codes = await connectToUserCollection();
|
||||
@@ -53,8 +73,7 @@ export async function checkCode({ phone, code }: { phone: string; code: string }
|
||||
phone,
|
||||
code,
|
||||
createdTime: {
|
||||
// 5分钟内有效
|
||||
$gt: new Date().getTime() - 5 * 60 * 1000
|
||||
$gt: subMinutes(new Date(), 5)
|
||||
}
|
||||
});
|
||||
return !!result;
|
||||
|
||||
@@ -84,7 +84,7 @@ export const sendToBot = async ({
|
||||
...billings.map((item) => [
|
||||
{
|
||||
tag: 'text',
|
||||
text: `订单号: ${item.order_id}, 创建时间: ${item.createdTime}, 金额: ¥${item.amount}`
|
||||
text: `订单号: ${item.order_id}, 创建时间: ${item.createdTime}, 金额: ¥${item.amount}, 可用区UID: ${item.regionUID}, 用户UID ${item.userUID}`
|
||||
}
|
||||
])
|
||||
]
|
||||
|
||||
@@ -75,6 +75,12 @@ export type RechargeBillingItem = {
|
||||
CreatedAt: string;
|
||||
Amount: number;
|
||||
Gift: number;
|
||||
RegionUID: string;
|
||||
RegionUserOwner: string;
|
||||
Method: 'stripe';
|
||||
TradeNO: 'number';
|
||||
CodeURL: string;
|
||||
InvoicedAt: boolean;
|
||||
};
|
||||
export type RechargeBillingData = {
|
||||
payment: RechargeBillingItem[];
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { RechargeBillingItem } from '@/types/billing';
|
||||
|
||||
export type TInvoiceDetail = {
|
||||
title: string;
|
||||
tax: string;
|
||||
@@ -12,23 +14,16 @@ export type TInvoiceContract = {
|
||||
phone: string;
|
||||
email: string;
|
||||
};
|
||||
export type TInvoice = {
|
||||
detail: TInvoiceDetail;
|
||||
contract: TInvoiceContract;
|
||||
};
|
||||
export type ReqGenInvoice = {
|
||||
detail: TInvoiceDetail;
|
||||
contract: TInvoiceContract & { code: string };
|
||||
billings: {
|
||||
order_id: string;
|
||||
amount: number;
|
||||
// timeStamp
|
||||
createdTime: number;
|
||||
}[];
|
||||
billings: RechargeBillingItem[];
|
||||
};
|
||||
export type Tbilling = {
|
||||
order_id: string;
|
||||
amount: number;
|
||||
regionUID: string;
|
||||
userUID: string;
|
||||
createdTime: Date;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { NextApiRequest } from 'next';
|
||||
|
||||
export const retrySerially = <T>(fn: () => Promise<T>, times: number) =>
|
||||
new Promise((res, rej) => {
|
||||
let retries = 0;
|
||||
@@ -36,3 +38,19 @@ export const isValidEmail = (email: string) => {
|
||||
const regExp = /\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*/;
|
||||
return regExp.test(email);
|
||||
};
|
||||
export function getClientIPFromRequest(req: NextApiRequest) {
|
||||
// try to get ip from x-forwarded-for
|
||||
const ips_str = req.headers['x-forwarded-for'] as string;
|
||||
if (ips_str) {
|
||||
const ips = ips_str.split(',');
|
||||
return ips[0];
|
||||
}
|
||||
|
||||
// try to get ip from x-real-ip
|
||||
const ip = req.headers['x-real-ip'] as string;
|
||||
if (ip) {
|
||||
return ip;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user