import type { NextApiRequest, NextApiResponse } from 'next' import prismaClient from '@/lib/prisma' import fs from 'fs' import path from 'path' import archiver from 'archiver' import XLSX from 'xlsx' import XLSXStyle from 'xlsx-style'; import dayjs from 'dayjs' import AuthClient from '@/api/dingding/auth' import { getHanShuiAmount, getBuHanShuiAmount, getAllAmount } from '@/pages/api/approval/util' import { AGREEMENT_TYPE_CODE, AGREEMENT_TYPES, XIANGMU_ZUBIE_MAP } from '@/CONSTANTS' import { downloadAttachments, processInvoiceDataItem } from '@/lib/util' // 定义协议类型名称映射 const AGREEMENT_CODE_TO_NAME = { '1': '大协议', '2': '独立协议', '3': '大协议', '4': '独立协议' }; // 定义列宽配置 // 列宽:增加“开票作业序号”,移除“集团内外” const COLUMN_WIDTHS = { "开票作业序号": 15, "数据ID": 20, "项目编号": 15, "单位": 25, // 移除“集团内外” "票号": 15, "不含税金额": 15, "税额": 15, "含税金额": 15, "发票类型": 20, "申请人": 15, "专业组": 15, "开票日期": 15, "账期": 15, "回款日期": 15, "回款金额": 15, "统一社会信用代码": 25, "开户行名称": 25, "银行账号": 25, "业务内容": 30 }; // 定义表头样式 const HEADER_STYLE = { fill: { fgColor: { rgb: "bce672" } // 蓝色背景 }, font: { color: { rgb: "000000" }, // 白色字体 bold: true }, alignment: { horizontal: "center", vertical: "center" } }; // 定义数据单元格样式 const CELL_STYLE = { alignment: { horizontal: "left", vertical: "center" }, numFmt: "@" }; export default async function handler( req: NextApiRequest, res: NextApiResponse ) { if (req.method !== 'POST') { return res.status(405).json({ message: 'Method not allowed' }) } try { // 获取access token,实际实现中需要从请求上下文或认证系统获取 const accessToken = await AuthClient.getAccessToken(); const { ids } = req.body console.log('Received IDs:', ids); if (!ids || !Array.isArray(ids) || ids.length === 0) { return res.status(400).json({ message: 'Invalid or missing IDs array' }) } if (ids.length > 1000) { return res.status(400).json({ message: 'Too many IDs requested. Maximum 1000 IDs allowed.' }) } console.log('Fetching data for IDs:', ids); const invoiceData = await prismaClient.invoice_application_match_view.findMany({ where: { dataId: { in: ids } }, // 预排序(视图字段为数字时有效;为字符串时按字典序,下面还有兜底排序) orderBy: { invoiceJobNumber: 'asc' } }) // 提取大协议的dataId const largeAgreementIds = invoiceData .filter(item => item.agreementType === '1') .map(item => item.dataId); // 从invoice_amount_details获取大协议的业务内容 const largeAgreementDetails = await prismaClient.invoice_amount_details.findMany({ where: { dataid: { in: largeAgreementIds }, agreementType: AGREEMENT_TYPE_CODE[AGREEMENT_TYPES.LARGE] } }); // 创建一个映射,存储每个dataid对应的多个business_content const businessContentMap = new Map(); largeAgreementDetails.forEach((detail, idx) => { if (detail.dataid && detail.business_content) { if (!businessContentMap.has(detail.dataid)) { businessContentMap.set(detail.dataid, []); } businessContentMap.get(detail.dataid)?.push(`${idx}. [${detail.project_name}]${detail.business_content}`); } }); console.log('拉取开票申请数据数量:', invoiceData.length); const modifiedInvoiceData = invoiceData .map(item => { const processed = processInvoiceDataItem(item) const buHanShuiAmount = getBuHanShuiAmount(item) const hanShuiAmount = getHanShuiAmount(item) const AllAmount = getAllAmount(buHanShuiAmount, hanShuiAmount); // 业务内容统一取开票事由 const businessContent = item.kai_piao_shi_you || ''; // 根据新的协议类型显示逻辑 let agreementTypeName = ''; if (item.agreementType === '2' || item.agreementType === '4') { agreementTypeName = '独立协议'; } else if (item.agreementType === '1' || item.agreementType === '3') { agreementTypeName = '大协议'; } else { agreementTypeName = AGREEMENT_CODE_TO_NAME[item.agreementType as keyof typeof AGREEMENT_CODE_TO_NAME] || ''; } // 专业组逻辑:提取项目编号的所有字母部分映射,如果没有项目编号则为客户服务室 const getProjectGroupByProjectId = (projectId: string | null | undefined): string => { if (!projectId) { return '客户服务室'; } // 提取项目编号的所有字母部分(匹配连续的字母字符,不限位数) const letterMatch = projectId.match(/^[A-Za-z]+/); const prefix = letterMatch ? letterMatch[0] : ''; // 根据字母前缀映射专业组 const groupMapping: { [key: string]: string } = { 'YY': '运营室', 'ZP': '人员招聘项目组', 'ZW': '总务服务项目组', 'XC': '薪酬核算项目组', 'TX': '退休衔接服务项目组', 'TC': '统筹支持项目组', 'RZL': '人员入转离项目组', 'PJ': '人才评鉴项目组', 'HW': '会务服务项目组', 'GJ': '国际化服务项目组', 'GZ': '管理咨询项目组', 'KF': '客户服务室', 'FL': '福利业务项目组', 'FW': '法务合规中心', 'SJ': '数字化与交付项目组', 'KJ': '科技培训项目组', 'JG': '经营管理项目组', 'JN': '技能培训项目组', 'DJ': '党建培训项目组', 'JH': '计划核定项目组', 'LGB': '老干部服务项目组' }; return groupMapping[prefix] || '客户服务室'; }; const zhuan_ye_zu = getProjectGroupByProjectId(item.projectNumber); // 账期计算逻辑 let zhang_qi = ''; const kai_piao_ri_qi = item.kai_piao_ri_qi; const hui_kuan_ri_qi = item.hui_kuan_ri_qi; // 优先检查回款日期:如果有回款日期,则没有账期 if (hui_kuan_ri_qi) { zhang_qi = ''; } else if (kai_piao_ri_qi) { // 只有开票日期:账期 = 当前日期 - 开票日期(单位:天) const kp = dayjs(kai_piao_ri_qi); const now = dayjs(); if (kp.isValid()) { const dayDiff = now.diff(kp, 'day'); zhang_qi = dayDiff.toString(); } } return { "开票作业序号": item.invoiceJobNumber || '', "数据ID": item.dataId, "项目编号": item.projectNumber || '', "单位": processed.customerCompanyName || '', "票号": item.piao_hao || '', "不含税金额": buHanShuiAmount, "税额": AllAmount, "含税金额": hanShuiAmount, "发票类型": item.invoiceType || '', "申请人": item.applicant || '', "专业组": zhuan_ye_zu || '', "开票日期": item.kai_piao_ri_qi || '', "账期": zhang_qi, // 使用计算后的账期值 "回款日期": item.hui_kuan_ri_qi || '', "回款金额": item.hui_kuan_jin_e || '', "统一社会信用代码": processed.tong_yi_she_hui_xin_yong_dai_ma || '', "开户行名称": processed.bankName || '', "银行账号": processed.bankAccount || '', "业务内容": businessContent, "协议类型": agreementTypeName }; }) // 兜底排序:有效数字升序,空值/非数字排最后 .sort((a, b) => { const aRaw = a["开票作业序号"]; const bRaw = b["开票作业序号"]; const aEmpty = aRaw === '' || aRaw == null; const bEmpty = bRaw === '' || bRaw == null; if (aEmpty && bEmpty) return 0; if (aEmpty) return 1; if (bEmpty) return -1; const aNum = Number(aRaw); const bNum = Number(bRaw); const aNumValid = Number.isFinite(aNum); const bNumValid = Number.isFinite(bNum); if (aNumValid && bNumValid) return aNum - bNum; if (aNumValid) return -1; if (bNumValid) return 1; return String(aRaw).localeCompare(String(bRaw), 'zh-Hans-CN', { numeric: true }); }); // 转为工作表时的表头移除“集团内外”,统一“开票作业序号” const worksheet = XLSX.utils.json_to_sheet(modifiedInvoiceData, { header: [ "开票作业序号", "数据ID", "项目编号", "单位", // "集团内外" 已移除 "票号", "不含税金额", "税额", "含税金额", "发票类型", "申请人", "专业组", "开票日期", "账期", "回款日期", "回款金额", "统一社会信用代码", "开户行名称", "银行账号", "业务内容" ] }) // 设置列宽 const range = XLSX.utils.decode_range(worksheet['!ref'] || 'A1'); const cols = []; for (let i = 0; i <= range.e.c; i++) { const colHeader = worksheet[XLSX.utils.encode_cell({ r: 0, c: i })]?.v; cols.push({ wch: COLUMN_WIDTHS[colHeader] || 15 }); } worksheet['!cols'] = cols; // 设置表头样式和单元格格式 for (let R = range.s.r; R <= range.e.r; R++) { for (let C = range.s.c; C <= range.e.c; C++) { const cellAddress = XLSX.utils.encode_cell({ r: R, c: C }); if (R === 0) { // 表头行 worksheet[cellAddress].s = HEADER_STYLE; } else { // 数据行 worksheet[cellAddress].s = CELL_STYLE; } } } // Create a new workbook and append the worksheet const workbook = XLSX.utils.book_new() XLSX.utils.book_append_sheet(workbook, worksheet, '开票申请数据') const buffer = XLSXStyle.write(workbook, { type: 'buffer', bookType: 'xlsx', cellStyles: true }) // 创建日期时间戳文件夹 const now = dayjs(); const dateStr = now.format('YYYY-MM-DD'); const timeStr = now.format('HHmmss'); const folderName = `${dateStr}_${timeStr}`; const baseDir = path.join(process.cwd(), 'filebase'); const folderPath = path.join(baseDir, folderName); // 确保filebase目录存在 if (!fs.existsSync(baseDir)) { fs.mkdirSync(baseDir, { recursive: true }); } if (!fs.existsSync(folderPath)) { fs.mkdirSync(folderPath, { recursive: true }); } // 保存Excel文件到文件夹 const excelFileName = '开票申请数据.xlsx'; const excelFilePath = path.join(folderPath, excelFileName); fs.writeFileSync(excelFilePath, buffer); for (const item of invoiceData) { const invoiceFolderPath = path.join(folderPath, `${item.invoiceJobNumber}-${item.applicant}`) if (item.uploadedContractAttachment) { await downloadAttachments( item.uploadedContractAttachment, invoiceFolderPath, '上传合同及相关附件', item.dataId, accessToken, '' ); } // 处理uploadedSealedCostList if (item.uploadedSealedCostListDa) { await downloadAttachments( item.uploadedSealedCostListDa, invoiceFolderPath, '上传费用清单', item.dataId, accessToken, '' ); } // 处理uploadedSealedCostList if (item.uploadedSealedCostListDu) { await downloadAttachments( item.uploadedSealedCostListDu, invoiceFolderPath, '上传盖章费用清单(独)', item.dataId, accessToken, '' ); } } // 创建压缩文件 const zipFileName = `${folderName}.zip`; const zipFilePath = path.join(process.env.DOWNLOAD_BASE_DIR ?? '', zipFileName); const output = fs.createWriteStream(zipFilePath); const archive = archiver('zip', { zlib: { level: 9 } // 设置压缩级别 }); output.on('error', function (err) { console.error('Error writing zip file:', err); res.status(500).json({ message: 'Error writing zip file' }); }); // 当归档完成时发送响应 output.on('close', function () { console.log(archive.pointer() + ' total bytes'); console.log('archiver has been finalized and the output file descriptor has closed.'); console.log(`${process.env.DOWNLOAD_BASE_URL ?? ''}/${zipFileName}`) res.status(200).json({ code: 0, "error": null, message: '文件已经被妥善下载。', data: { zipFilePath: `${process.env.DOWNLOAD_BASE_URL ?? ''}/${zipFileName}` } }); }); // 处理归档错误 archive.on('error', function (err) { console.error('Error creating zip file:', err); res.status(500).json({ message: 'Error creating zip file' }); }); // 开始归档过程 archive.pipe(output); archive.directory(folderPath, false); // 将整个文件夹添加到归档中 TODO 这个foldername 是个啥 archive.finalize(); } catch (error) { console.error('Error exporting invoice data:', error) res.status(500).json({ message: 'Internal server error', error }) } }