初次提交代码

This commit is contained in:
2026-05-27 14:12:10 +08:00
parent 321aa5b319
commit ca32aa87e7
88 changed files with 19000 additions and 1 deletions

View File

@@ -0,0 +1,402 @@
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<string, string[]>();
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 })
}
}