From a05b86025cc091977f2b7ab53853d42b641cdb05 Mon Sep 17 00:00:00 2001 From: xudaotutou <13435638964@163.com> Date: Fri, 19 Apr 2024 18:15:00 +0800 Subject: [PATCH] refactor(costcenter): refactor invoice (#4694) --- .../src/pages/api/dev/migrateInvoice.ts | 90 +++++++++++++ .../costcenter/public/locales/zh/common.json | 1 + .../components/billing/RechargeTabPanel.tsx | 3 +- .../src/components/cost_overview/buget.tsx | 4 +- .../src/components/invoice/invoiceTable.tsx | 21 ++-- .../src/pages/api/invoice/billings.ts | 32 ----- .../costcenter/src/pages/api/invoice/sms.ts | 44 +++++-- .../src/pages/api/invoice/verify.ts | 20 ++- .../src/pages/create_invoice/InvoicdForm.tsx | 72 +++++------ .../src/pages/create_invoice/index.tsx | 119 +++++++----------- .../src/service/backend/db/invoice.ts | 9 +- .../src/service/backend/db/verifyCode.ts | 39 ++++-- .../costcenter/src/service/sendToBot.ts | 2 +- .../providers/costcenter/src/types/billing.ts | 6 + .../providers/costcenter/src/types/invoice.ts | 15 +-- .../providers/costcenter/src/utils/tools.ts | 18 +++ 16 files changed, 303 insertions(+), 192 deletions(-) create mode 100644 frontend/desktop/src/pages/api/dev/migrateInvoice.ts delete mode 100644 frontend/providers/costcenter/src/pages/api/invoice/billings.ts diff --git a/frontend/desktop/src/pages/api/dev/migrateInvoice.ts b/frontend/desktop/src/pages/api/dev/migrateInvoice.ts new file mode 100644 index 000000000..711918be6 --- /dev/null +++ b/frontend/desktop/src/pages/api/dev/migrateInvoice.ts @@ -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('invoices'); + const billings = await invoicesCollection + .aggregate([ + { $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'); + } +} diff --git a/frontend/providers/costcenter/public/locales/zh/common.json b/frontend/providers/costcenter/public/locales/zh/common.json index 14dbf41d2..a9a63a0c5 100644 --- a/frontend/providers/costcenter/public/locales/zh/common.json +++ b/frontend/providers/costcenter/public/locales/zh/common.json @@ -24,6 +24,7 @@ "Deduction": "扣费", "Total Cost": "总成本", "Cost Distribution": "成本分布", + "Income And Expense": "收支", "Expenditure": "支出", "Recharge Amount": "充值金额", "Select Amount": "选择金额", diff --git a/frontend/providers/costcenter/src/components/billing/RechargeTabPanel.tsx b/frontend/providers/costcenter/src/components/billing/RechargeTabPanel.tsx index 4bd8cfc6a..40840d95b 100644 --- a/frontend/providers/costcenter/src/components/billing/RechargeTabPanel.tsx +++ b/frontend/providers/costcenter/src/components/billing/RechargeTabPanel.tsx @@ -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, { diff --git a/frontend/providers/costcenter/src/components/cost_overview/buget.tsx b/frontend/providers/costcenter/src/components/cost_overview/buget.tsx index e196ca327..b18d75d7c 100644 --- a/frontend/providers/costcenter/src/components/cost_overview/buget.tsx +++ b/frontend/providers/costcenter/src/components/cost_overview/buget.tsx @@ -41,7 +41,9 @@ export function Buget() { return ( - {t('Income And Expense')} + + {t('Income And Expense')} + {list.map((v) => ( diff --git a/frontend/providers/costcenter/src/components/invoice/invoiceTable.tsx b/frontend/providers/costcenter/src/components/invoice/invoiceTable.tsx index 66e4f066b..6440e08ad 100644 --- a/frontend/providers/costcenter/src/components/invoice/invoiceTable.tsx +++ b/frontend/providers/costcenter/src/components/invoice/invoiceTable.tsx @@ -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({ > {t(item)} - {item !== 'Order Number' && } + {item === 'True Amount' && ( + <> + () + + )} ))} @@ -45,12 +50,12 @@ export function InvoiceTable({ {data.map((item) => ( - + {needSelect && ( 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} - {format(item.createdTime, 'MM-dd HH:mm')} - {item.amount} + {format(new Date(item.CreatedAt), 'MM-dd HH:mm')} + {item.Amount} ))} diff --git a/frontend/providers/costcenter/src/pages/api/invoice/billings.ts b/frontend/providers/costcenter/src/pages/api/invoice/billings.ts deleted file mode 100644 index 562bac94e..000000000 --- a/frontend/providers/costcenter/src/pages/api/invoice/billings.ts +++ /dev/null @@ -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 - }); - } -} diff --git a/frontend/providers/costcenter/src/pages/api/invoice/sms.ts b/frontend/providers/costcenter/src/pages/api/invoice/sms.ts index a8d67fd11..1d2d2006a 100644 --- a/frontend/providers/costcenter/src/pages/api/invoice/sms.ts +++ b/frontend/providers/costcenter/src/pages/api/invoice/sms.ts @@ -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 = {}; +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 diff --git a/frontend/providers/costcenter/src/pages/api/invoice/verify.ts b/frontend/providers/costcenter/src/pages/api/invoice/verify.ts index abfc96939..ef5e2ba2e 100644 --- a/frontend/providers/costcenter/src/pages/api/invoice/verify.ts +++ b/frontend/providers/costcenter/src/pages/api/invoice/verify.ts @@ -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) { diff --git a/frontend/providers/costcenter/src/pages/create_invoice/InvoicdForm.tsx b/frontend/providers/costcenter/src/pages/create_invoice/InvoicdForm.tsx index 458647293..8a61061ab 100644 --- a/frontend/providers/costcenter/src/pages/create_invoice/InvoicdForm.tsx +++ b/frontend/providers/costcenter/src/pages/create_invoice/InvoicdForm.tsx @@ -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 ( <> - {' '} - {' '} - {' '} - {t('orders.invoiceOrder')}({invoiceCount}){' '} - ¥ {invoiceAmount}{' '} - {' '} - {' '} + {t('orders.invoiceOrder')}({invoiceCount}) + ¥ {invoiceAmount} + + - {' '} - {' '} - {' '} - {' '} - {' '} - {t('orders.list')}{' '} - {' '} - {' '} + + {t('orders.list')} + + index <= pageSize * currentPage - 1 && index >= pageSize * (currentPage - 1) )} - >{' '} - {' '} + > + - {' '} - {t('Total')}: {billings?.current?.length || 0}{' '} + {t('Total')}: {billings?.current?.length || 0} - {' '} {' '} + + {' '} - {currentPage}/{totalPage}{' '} + + + + {currentPage}/{totalPage} + {' '} + + {' '} - {' '} - {pageSize} /{t('Page')}{' '} - {' '} - {' '} - {' '} + + + + {pageSize} /{t('Page')} + + + ); }; + function InvoicdForm({ invoiceAmount, invoiceCount, diff --git a/frontend/providers/costcenter/src/pages/create_invoice/index.tsx b/frontend/providers/costcenter/src/pages/create_invoice/index.tsx index 13580cb2d..be5643449 100644 --- a/frontend/providers/costcenter/src/pages/create_invoice/index.tsx +++ b/frontend/providers/costcenter/src/pages/create_invoice/index.tsx @@ -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([]); + const selectBillings = useRef([]); 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('/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( - '/api/billing', - { - method: 'POST', - data: { - spec - } - } - ); - - const tableResult = result.data.status.item - .filter((billing) => billing.type === 1) - .map((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>('/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 = (billing: T): boolean => - !(filterData?.data.billings || []).includes(billing.order_id); - let tableResult = data?.tableResult?.filter(billingFilter) || []; return ( @@ -164,7 +134,7 @@ function Invoice() { - {isSuccess && tableResult.length > 0 ? ( + {isSuccess ? ( <> { 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() { {t('Total')}: - {data.totalCount - (filterData?.data?.billings || []).length} + {data.length} - {currentPage}/{totalPage} + + {currentPage}/{totalPage} +