refactor(costcenter): refactor invoice (#4694)

This commit is contained in:
xudaotutou
2024-04-19 18:15:00 +08:00
committed by GitHub
parent 3b044f328d
commit a05b86025c
16 changed files with 303 additions and 192 deletions
@@ -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;
}