初次提交代码

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

23
.env.development Normal file
View File

@@ -0,0 +1,23 @@
# 环境配置
ENVIRONMENT=local # 可选值: local/development/test/production
# 钉钉相关配置
# DINGDING_INVOICE_APPROVAL_PROCESS_CODE = PROC-4A680AF4-4853-4C30-A660-E702442E184A
# DINGDING_APP_KEY = dingjrkdm6lqfebrwrhs
# DINGDING_APP_SECRET = oxYwPO9F6-_TbjI-uKFpO1__Qc3jRPQ5MSInBvei2r6rscg_gZexbAqi-QR6ZNCz
DINGDING_INVOICE_APPROVAL_PROCESS_CODE = PROC-4FB03ECE-6F80-413F-90C3-CC9DCA903A2F
DINGDING_APP_KEY = ding3fdr9ulnhzlvnznb
DINGDING_APP_SECRET = Jk9Ph6iQ0gmkSxjOTZ7Sx3q_Z9HfJ97SbHoB2M6FFTNBrXqXAg-h1npfyAtznB7i
# 数据库配置
DATABASE_URL=mysql://root:root@localhost:3306/procurement_db?serverTimezone=Asia/Shanghai&allowMultiQueries=true&useSSL=false
DATABASE_NAME=your_database_name
DATABASE_USER=your_database_username
DATABASE_PASSWORD=your_database_password
PRISMA_ENGINES_MIRROR=https://registry.npmmirror.com/-/binary/prisma
# 下载文件的基础目录配置
# Windows示例: DOWNLOAD_BASE_DIR=E:\\nginx-1.29.1\\html\\download
# Linux示例: DOWNLOAD_BASE_DIR=/var/www/html/download
DOWNLOAD_BASE_DIR=D:\\nginx-1.29.1\\html\\download
DOWNLOAD_BASE_URL=http://localhost/download

4
.env.example Normal file
View File

@@ -0,0 +1,4 @@
# 下载文件的基础目录配置
# Windows示例: DOWNLOAD_BASE_DIR=E:\\nginx-1.29.1\\html\\download
# Linux示例: DOWNLOAD_BASE_DIR=/var/www/html/download
DOWNLOAD_BASE_DIR=./filebase

21
.env.production Normal file
View File

@@ -0,0 +1,21 @@
# 环境配置
ENVIRONMENT=production # 可选值: development/test/production
# 钉钉相关配置
DINGDING_INVOICE_APPROVAL_PROCESS_CODE = PROC-1FBC7990-CB26-45BA-8EE3-472EA70E4421
DINGDING_APP_KEY = ding3fdr9ulnhzlvnznb
DINGDING_APP_SECRET = Jk9Ph6iQ0gmkSxjOTZ7Sx3q_Z9HfJ97SbHoB2M6FFTNBrXqXAg-h1npfyAtznB7i
# 数据库配置
DATABASE_URL=mysql://mysqladmin:Hgt9908265!@@10.124.9.120:3306/procurement_db?serverTimezone=Asia/Shanghai&allowMultiQueries=true&useSSL=false
DATABASE_NAME=procurement_db
DATABASE_USER=mysqladmin # 后续考虑增加一个用户
DATABASE_PASSWORD=Hgt9908265!@
PRISMA_ENGINES_MIRROR=https://registry.npmmirror.com/-/binary/prisma
# 下载文件的基础目录配置
# Windows示例: DOWNLOAD_BASE_DIR=E:\\nginx-1.29.1\\html\\download
# Linux示例: DOWNLOAD_BASE_DIR=/var/www/html/download
DOWNLOAD_BASE_DIR=/usr/share/nginx/html/download
DOWNLOAD_BASE_URL=http://hrsscip.dfmc.com.cn/download

0
.env.test Normal file
View File

45
.gitignore vendored Normal file
View File

@@ -0,0 +1,45 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
# .env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
combined.log
filebase

8
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

9
.idea/dingding-oa-server.iml generated Normal file
View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

6
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/dingding-oa-server.iml" filepath="$PROJECT_DIR$/.idea/dingding-oa-server.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

45
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,45 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Next.js: 调试服务端",
"type": "node-terminal",
"request": "launch",
"command": "pnpm run dev:dev"
},
{
"name": "Next.js: 调试客户端",
"type": "chrome",
"request": "launch",
"url": "http://localhost:3000"
},
{
"name": "Next.js: 调试客户端 (Firefox)",
"type": "firefox",
"request": "launch",
"url": "http://localhost:3000",
"reAttach": true,
"pathMappings": [
{
"url": "webpack://_N_E",
"path": "${workspaceFolder}"
}
]
},
{
"name": "Next.js: 全栈调试",
"type": "node",
"request": "launch",
"program": "${workspaceFolder}/node_modules/next/dist/bin/next",
"runtimeArgs": ["--inspect"],
"skipFiles": ["<node_internals>/**"],
"serverReadyAction": {
"action": "debugWithEdge",
"killOnServerStop": true,
"pattern": "- Local:.+(https?://.+)",
"uriFormat": "%s",
"webRoot": "${workspaceFolder}"
}
}
]
}

View File

@@ -1,2 +1,93 @@
# dingding-oa-server
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
# 定时脚本
sudo crontab -l
0 */6 * * * curl http://localhost:3000/api/dingding/corn >> /var/log/cron_dingding.log 2>&1
每六个小时执行一次脚本将数据注入到数据库
# 通用Excel导入接口
项目提供了一个通用的Excel数据导入接口可以用于向不同的数据表导入数据。
## 接口地址
```
POST /api/utils/genericImport?table=<tableName>
```
## 支持的表名
- project (项目立项数据)
- purchase_contract_order_management (采购合同订单管理数据)
- purchase_contract_execution_maintenance (采购合同履约数据)
- expense_reimbursement (费用报销数据)
- actual_payment (实际支付数据)
## 使用方法
发送POST请求到上述接口地址将Excel文件作为请求体上传同时在查询参数中指定要导入的表名。
示例使用curl
```bash
# 导入项目立项数据
curl -X POST -F "file=@project_data.xlsx" "http://localhost:3000/api/utils/genericImport?table=project"
# 导入采购合同订单管理数据
curl -X POST -F "file=@purchase_contract_order.xlsx" "http://localhost:3000/api/utils/genericImport?table=purchase_contract_order_management"
```
## Excel文件格式要求
Excel文件应包含与目标数据表对应的列第一行为列标题。其中必须包含"数据id"列作为唯一标识。
## 注意事项
1. 接口在插入数据前会查询数据库中已存在的记录自动过滤掉重复的数据基于数据id字段只插入新数据
2. 导入完成后会返回具体插入的新数据条数
3. 导入过程中如遇到错误会返回500状态码及错误信息
4. 请确保Excel文件格式正确列名与模板一致
## 金融数据计算库
https://mikemcl.github.io/bignumber.js/
https://www.npmjs.com/package/bignumber.js
用例 1浮点数精度误差最常见问题
用例 2大整数精度丢失
用例 3toFixed()方法的舍入异常

16
eslint.config.mjs Normal file
View File

@@ -0,0 +1,16 @@
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
];
export default eslintConfig;

16
next.config.ts Normal file
View File

@@ -0,0 +1,16 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
typescript: {
ignoreBuildErrors: true
},
eslint: {
ignoreDuringBuilds: true
},
/* config options here */
api: {
bodyParser: false // 必须禁用默认解析器
}
};
export default nextConfig;

51
package.json Normal file
View File

@@ -0,0 +1,51 @@
{
"name": "dingding-oa-server",
"version": "0.1.0",
"private": true,
"scripts": {
"dev:prod": "cross-env NODE_ENV=production && next dev --turbopack",
"dev:dev-db": "cross-env NODE_ENV=development && dotenv -e .env.development -- npx prisma db pull && npx prisma generate && next dev --turbopack ",
"dev:dev": "cross-env NODE_ENV=development && next dev --turbopack ",
"build": " cross-env NODE_ENV=production && dotenv -e .env.production -- npx prisma db pull && npx prisma generate && next build",
"start": " cross-env NODE_ENV=production && next start",
"lint": "next lint",
"pulldb": "dotenv -e .env.development -- npx prisma db pull && npx prisma generate"
},
"dependencies": {
"@alicloud/dingtalk": "^2.2.21",
"@alicloud/openapi-client": "^0.4.14",
"@alicloud/tea-util": "^1.4.10",
"prisma": "6.19.0",
"@prisma/client": "6.19.0",
"archiver": "^7.0.1",
"bignumber.js": "^9.3.1",
"dayjs": "^1.11.13",
"dotenv-cli": "^10.0.0",
"formidable": "^3.5.4",
"lodash": "^4.17.21",
"next": "15.3.4",
"next-multiparty": "^0.7.0",
"p-limit": "^7.1.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"sax": "^1.4.1",
"winston": "^3.17.0",
"xlsx": "^0.18.5",
"xlsx-style": "^0.8.13"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4",
"@types/archiver": "^6.0.3",
"@types/formidable": "^3.4.5",
"@types/lodash": "^4.17.20",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"cross-env": "^7.0.3",
"eslint": "^9",
"eslint-config-next": "15.3.4",
"tailwindcss": "^4",
"typescript": "^5"
}
}

5052
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

5
postcss.config.mjs Normal file
View File

@@ -0,0 +1,5 @@
const config = {
plugins: ["@tailwindcss/postcss"],
};
export default config;

4106
procurement_db.sql Normal file

File diff suppressed because it is too large Load Diff

1
public/file.svg Normal file
View File

@@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 391 B

1
public/globe.svg Normal file
View File

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

1
public/next.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

1
public/vercel.svg Normal file
View File

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 128 B

1
public/window.svg Normal file
View File

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

After

Width:  |  Height:  |  Size: 385 B

426
schema.prisma Normal file
View File

@@ -0,0 +1,426 @@
generator client {
provider = "prisma-client-js"
previewFeatures = ["views"]
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
model dingding_oa_invoice_application {
invoiceJobNumber Int @default(autoincrement())
dataId String @db.VarChar(255)
applicationDate String? @db.VarChar(255)
applicant String? @db.VarChar(255)
applicationDepartment String? @db.VarChar(255)
agreementType String? @db.VarChar(255)
xiang_mu_zu_bie String? @db.VarChar(255)
yuJiBaoXiaoDiSanFangChengBenYuan String? @db.VarChar(255)
businessStartTime String? @db.VarChar(255)
businessEndTime String? @db.VarChar(255)
contractNumber String? @db.VarChar(255)
contractName String? @db.VarChar(255)
uploadedContractAttachment String? @db.Text
uploadedSealedCostListDa String? @db.Text
uploadedSealedCostListDu String? @db.Text
projectNumber String? @db.VarChar(255)
is_tax_inclusive String? @db.VarChar(255)
invoice_tax_rate String? @db.VarChar(255)
invoice_tax_category String? @db.VarChar(255)
is_multi_project_merge String? @db.VarChar(255)
customerCompanyName String? @db.VarChar(255)
customerCompanyNameOther String? @db.VarChar(255)
tong_yi_she_hui_xin_yong_dai_ma String? @db.VarChar(255)
bankName String? @db.VarChar(255)
bankAccount String? @db.VarChar(255)
kai_piao_shi_you String? @db.VarChar(255)
fa_piao_bei_zhu_lan_tian_xie_nei_rong String? @db.Text
invoiceType String? @db.VarChar(50)
invoiceCode Int? @db.UnsignedTinyInt
business_content_du String? @db.VarChar(255)
invoice_amount_yuan_du String? @db.VarChar(255)
invoice_amount_yuan_other String? @db.VarChar(255)
bu_han_shui_jin_e_yuan_du String? @db.VarChar(255)
han_shui_jin_e_yuan_du String? @db.VarChar(255)
bu_han_shui_jin_e_yuan_other String? @db.VarChar(255)
han_shui_jin_e_yuan_other String? @db.VarChar(255)
user String? @db.VarChar(255)
remark String? @db.VarChar(255)
cancelReason String? @db.VarChar(255)
canceldataid String? @db.VarChar(255)
han_shui_jin_e_yuan_duo String? @db.VarChar(255)
bu_han_shui_jin_e_yuan_da String? @db.VarChar(255)
han_shui_zong_jin_e_yuan_da String? @db.VarChar(255)
bu_han_shui_jin_e_yuan_da_du String? @db.VarChar(255)
han_shui_zong_jin_e_yuan_da_du String? @db.VarChar(255)
bu_han_shui_jin_e_yuan_duo String? @db.VarChar(255)
han_shui_jin_e_yuan_du_da_xie String? @db.VarChar(255)
bu_han_shui_jin_e_yuan_other_da_xie String? @db.VarChar(255)
han_shui_jin_e_yuan_other_da_xie String? @db.VarChar(255)
bu_han_shui_jin_e_yuan_duo_da_xie String? @db.VarChar(255)
approvalNumber String? @db.VarChar(255)
creationTime String? @db.VarChar(255)
creator String? @db.VarChar(255)
currentResponsiblePerson String? @db.VarChar(255)
approvalResult String? @db.VarChar(255)
approvalStatus String? @db.VarChar(255)
updateTime String? @db.VarChar(255)
creatorDepartment String? @db.VarChar(255)
approvalFormTitle String? @db.VarChar(255)
historicalApprovers String? @db.VarChar(255)
timeConsumed String? @db.VarChar(255)
approvalRecord String? @db.VarChar(255)
payee String? @db.VarChar(255)
invoice_amount_yuan String? @db.VarChar(255)
invoice_amount_yuan_da String? @db.VarChar(255)
BigInvoiceAmountList String? @db.Text
multiProjectMergeInvoice String? @db.Text
is_single_contract_multi_customer String? @db.VarChar(255)
single_contract_multi_customer_invoice_list String? @db.Text
hong_chong_piao_hao String? @db.VarChar(50)
@@id([invoiceJobNumber, dataId])
}
/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
model invoice_amount_details {
id Int @default(autoincrement())
dataid String @db.VarChar(255)
agreementType String? @db.VarChar(50)
project_number String? @db.VarChar(255)
business_group_name String? @db.VarChar(100)
project_name String? @db.VarChar(255)
customer_company_name String? @db.VarChar(255)
tong_yi_she_hui_xin_yong_dai_ma String? @db.VarChar(255)
bank_name String? @db.VarChar(255)
bank_account String? @db.VarChar(255)
business_content String? @db.VarChar(255)
invoice_amount String? @db.VarChar(255)
payee String? @db.VarChar(255)
updatedAt DateTime @default(now()) @db.DateTime(0)
yu_ji_bao_xiao_di_san_fang_cheng_ben String? @db.VarChar(255)
@@id([id, dataid])
}
/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
model invoice_data {
dataid String @db.VarChar(255)
piao_hao String @id @db.VarChar(50)
kai_piao_ri_qi String @db.VarChar(50)
hui_kuan_ri_qi String @db.VarChar(255)
hui_kuan_jin_e String @db.VarChar(255)
created_at DateTime @default(now()) @db.DateTime(0)
updated_at DateTime @default(now()) @db.DateTime(0)
invoice_type_enum Int? @default(0)
zhang_qi String? @db.VarChar(255)
customer_company_name String? @db.VarChar(255)
}
model test {
id Int @id
name String? @db.VarChar(255)
shcool String? @db.VarChar(255)
}
/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
model project {
data_id String @id @db.VarChar(255)
project_department String? @db.VarChar(255)
is_multi_client String? @db.VarChar(255)
multi_client_name String? @db.VarChar(255)
group_abbreviation String? @db.VarChar(255)
previous_project_id String? @db.VarChar(255)
project_id String? @db.VarChar(255)
project_name String? @db.VarChar(255)
contract_id String? @db.VarChar(255)
contract_name String? @db.VarChar(255)
client_name String? @db.VarChar(255)
unit_nature String? @db.VarChar(255)
project_leader String? @db.VarChar(255)
create_time String? @db.VarChar(255)
creator String? @db.VarChar(255)
creator_department String? @db.VarChar(255)
approval_title String? @db.VarChar(255)
update_time String? @db.VarChar(255)
project_group String? @db.VarChar(255)
delivery_area String? @db.VarChar(255)
}
/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
model purchase_contract_order_management {
data_id String @id @db.VarChar(255)
purchase_contract_number String? @db.VarChar(255)
purchase_purpose String? @db.VarChar(255)
project_number String? @db.VarChar(255)
project_name String? @db.VarChar(255)
associated_sales_contract_number String? @db.VarChar(255)
contract_name String? @db.VarChar(255)
purchase_approval_number String? @db.VarChar(255)
purchase_contract_name String? @db.VarChar(255)
xiang_dui_fang String? @db.VarChar(255)
he_tong_lei_bie String? @db.VarChar(255)
he_tong_zong_jin_e_HanShui String? @db.VarChar(255)
sheng_yu_jin_e_HanShui String? @db.VarChar(255)
he_tong_sheng_yu_jin_e_ti_xing String? @db.VarChar(255)
ben_ci_ding_dan_ming_cheng String? @db.VarChar(255)
ben_ci_ding_dan_jin_e_HanShui String? @db.VarChar(255)
ben_ci_ding_dan_lu_yue_kai_shi_shi_jian String? @db.VarChar(255)
ben_ci_ding_dan_lu_yue_jie_shu_shi_jian String? @db.VarChar(255)
ben_ci_ding_dan_fa_qi_bu_men String? @db.VarChar(255)
ben_ci_ding_dan_fa_qi_ren String? @db.VarChar(255)
ben_ci_ding_dan_fa_qi_shi_jian String? @db.VarChar(255)
ben_ci_ding_dan_guan_lian_he_tong_fujian String? @db.VarChar(255)
ben_ci_ding_dan_wen_ben String? @db.Text
approval_number String? @db.VarChar(255)
creation_time String? @db.VarChar(255)
creator String? @db.VarChar(255)
current_responsible_person String? @db.VarChar(255)
approval_result String? @db.VarChar(255)
approval_status String? @db.VarChar(255)
update_time String? @db.VarChar(255)
creator_department String? @db.VarChar(255)
approval_form_title String? @db.VarChar(255)
time_consumed String? @db.VarChar(255)
}
/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
model purchase_contract_execution_maintenance {
data_id String @id @db.VarChar(255)
initiator String? @db.VarChar(255)
contract_number String? @db.VarChar(255)
contract_name String? @db.VarChar(255)
lvyue_faqi_bumen String? @db.VarChar(255)
operator String? @db.VarChar(255)
xiangduifang String? @db.VarChar(255)
is_open String? @db.VarChar(255)
contract_amount String? @db.VarChar(255)
remarks String? @db.VarChar(255)
performance_start_time String? @db.VarChar(255)
hetong_yuanjian_saomiaojian_gaizhangban String? @db.Text
contract_attachment_scan String? @db.Text
dingding_purchase_approval_file String? @db.Text
creation_time String? @db.VarChar(255)
creator String? @db.VarChar(255)
creator_department String? @db.VarChar(255)
approval_form_title String? @db.VarChar(255)
update_time String? @db.VarChar(255)
}
/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
model actual_payment {
data_id String @id @db.VarChar(255)
project_number String? @db.VarChar(255)
payment_date String? @db.VarChar(255)
payment_year String? @db.VarChar(255)
pingzhenghao String? @db.VarChar(255)
hangxiangmu_wenben String? @db.VarChar(255)
business_line String? @db.VarChar(255)
qimo_yueyuan String? @db.VarChar(255)
qimo_yueyuan_daxie String? @db.VarChar(255)
payment_method String? @db.VarChar(255)
gongyingshang_mingcheng String? @db.VarChar(255)
created_time String? @db.VarChar(255)
creator String? @db.VarChar(255)
creator_department String? @db.VarChar(255)
approval_title String? @db.VarChar(255)
updated_time String? @db.VarChar(255)
}
/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
model expense_reimbursement {
data_id String @id @db.VarChar(255)
project_number String? @db.VarChar(255)
reimbursement_time String? @db.VarChar(255)
category String? @db.VarChar(255)
project_team String? @db.VarChar(255)
amount String? @db.VarChar(255)
sr_number String? @db.VarChar(255)
baoxiaoren String? @db.VarChar(255)
suoshu_zhangtao String? @db.VarChar(255)
baoxiao_yuefen String? @db.VarChar(255)
baoxiao_nianfen String? @db.VarChar(255)
expense_type String? @db.VarChar(255)
gongyingshang_guazhang_mingcheng String? @db.VarChar(255)
created_time String? @db.VarChar(255)
creator String? @db.VarChar(255)
creator_department String? @db.VarChar(255)
approval_title String? @db.VarChar(255)
updated_time String? @db.VarChar(255)
xuhao String? @db.VarChar(255)
date String? @db.VarChar(255)
voucher_number String? @db.VarChar(255)
entry_number String? @db.VarChar(255)
summary String? @db.VarChar(255)
account_code String? @db.VarChar(255)
account_name String? @db.VarChar(255)
auxiliary_items String? @db.VarChar(1000)
currency String? @db.VarChar(255)
debit_original_currency String? @db.VarChar(255)
debit_local_currency String? @db.VarChar(255)
credit_original_currency String? @db.VarChar(255)
credit_local_currency String? @db.VarChar(255)
shared_voucher_number String? @db.VarChar(255)
expense_budget_item String? @db.VarChar(255)
cost_center String? @db.VarChar(255)
standard_name String? @db.VarChar(255)
expense_type_level1 String? @db.VarChar(255)
expense_type_level2 String? @db.VarChar(255)
expense_type_level3 String? @db.VarChar(255)
}
/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
model procurement_contract_list {
data_id String @id @db.VarChar(255)
initiator String? @db.VarChar(255)
contract_number String? @db.VarChar(255)
contract_name String? @db.VarChar(255)
lvyue_faqi_bumen String? @db.VarChar(255)
operator String? @db.VarChar(255)
xiangduifang String? @db.VarChar(255)
is_open String? @db.VarChar(255)
contract_amount String? @db.VarChar(255)
remarks String? @db.VarChar(255)
performance_start_time String? @db.VarChar(255)
hetong_yuanjian_saomiaojian_gaizhangban String? @db.Text
dingtalk_procurement_approval_form String? @db.Text
contract_attachment_scan String? @db.Text
creation_time String? @db.VarChar(255)
creator String? @db.VarChar(255)
approval_result String? @db.VarChar(255)
approval_status String? @db.VarChar(255)
update_time String? @db.VarChar(255)
creator_department String? @db.VarChar(255)
approval_form_title String? @db.VarChar(255)
}
/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
model invoice_void_record {
logid Int @default(autoincrement())
dataId String @db.VarChar(255)
applicationDepartment String? @db.VarChar(255)
xiang_mu_zu_bie String? @db.VarChar(255)
applicationDate String? @db.VarChar(255)
applicant String? @db.VarChar(255)
piao_hao String? @db.VarChar(255)
kai_piao_ri_qi String? @db.VarChar(255)
agreementType String? @db.VarChar(255)
zuo_fei_shi_you String? @db.VarChar(255)
cancelReason String? @db.VarChar(255)
invoice_amount_yuan String? @db.VarChar(255)
is_tax_inclusive String? @db.VarChar(255)
feeBelongingTime String? @db.VarChar(255)
customerCompanyName String? @db.VarChar(255)
tong_yi_she_hui_xin_yong_dai_ma String? @db.VarChar(255)
bankName String? @db.VarChar(255)
bankAccount String? @db.VarChar(255)
uploadedVoidInvoiceAttachment String? @db.Text
handwrittenSignatureAttachment String? @db.Text
projectNumber String? @db.VarChar(255)
remark String? @db.VarChar(255)
@@id([logid, dataId])
}
/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
model customer_management_info {
data_id String @id @db.VarChar(255)
customer_unit_name String @db.VarChar(500)
customer_type String? @db.VarChar(100)
created_at DateTime? @default(now()) @db.Timestamp(0)
updated_at DateTime? @default(now()) @db.Timestamp(0)
}
/// This view or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
view invoice_application_match_inner_view {
dataId String @db.VarChar(255)
invoiceJobNumber Int @default(0)
agreementType String? @db.VarChar(255)
approvalNumber String? @db.VarChar(255)
salesContractNumber String? @db.VarChar(255)
applicationDepartment String? @db.VarChar(255)
yuJiBaoXiaoDiSanFangChengBenYuan String? @db.VarChar(255)
is_multi_project_merge String? @db.VarChar(255)
is_single_contract_multi_customer String? @db.VarChar(255)
projectNumber String? @db.VarChar(255)
customerCompanyName String? @db.VarChar(255)
customerCompanyNameOther String? @db.VarChar(255)
piao_hao String? @db.VarChar(50)
bu_han_shui_jin_e_yuan_du String? @db.VarChar(255)
han_shui_jin_e_yuan_du String? @db.VarChar(255)
bu_han_shui_jin_e_yuan_da String? @db.VarChar(255)
han_shui_jin_e_yuan_da String? @db.VarChar(255)
bu_han_shui_jin_e_yuan_da_du String? @db.VarChar(255)
han_shui_jin_e_yuan_da_du String? @db.VarChar(255)
bu_han_shui_jin_e_yuan_duo String? @db.VarChar(255)
han_shui_jin_e_yuan_duo String? @db.VarChar(255)
bu_han_shui_jin_e_yuan_other String? @db.VarChar(255)
han_shui_jin_e_yuan_other String? @db.VarChar(255)
applicant String? @db.VarChar(255)
business_content_du String? @db.VarChar(255)
uploadedContractAttachment String? @db.Text
uploadedSealedCostListDa String? @db.Text
uploadedSealedCostListDu String? @db.Text
invoiceType String? @db.VarChar(50)
invoiceCode Int? @db.UnsignedTinyInt
kai_piao_shi_you String? @db.VarChar(255)
fa_piao_bei_zhu_lan_tian_xie_nei_rong String? @db.Text
tong_yi_she_hui_xin_yong_dai_ma String? @db.VarChar(255)
bankName String? @db.VarChar(255)
bankAccount String? @db.VarChar(255)
invoice_type_enum Int?
kai_piao_ri_qi String? @db.VarChar(50)
hui_kuan_ri_qi String? @db.VarChar(255)
hui_kuan_jin_e String? @db.VarChar(255)
zhang_qi String? @db.VarChar(255)
unit_nature String? @db.VarChar(100)
hong_chong_piao_hao String? @db.VarChar(50)
}
/// This view or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments
view invoice_application_match_view {
dataId String @db.VarChar(255)
invoiceJobNumber Int @default(0)
agreementType String? @db.VarChar(255)
approvalNumber String? @db.VarChar(255)
salesContractNumber String? @db.VarChar(255)
applicationDepartment String? @db.VarChar(255)
yuJiBaoXiaoDiSanFangChengBenYuan String? @db.VarChar(255)
is_multi_project_merge String? @db.VarChar(255)
is_single_contract_multi_customer String? @db.VarChar(255)
projectNumber String? @db.VarChar(255)
customerCompanyName String? @db.VarChar(255)
customerCompanyNameOther String? @db.VarChar(255)
piao_hao String? @db.VarChar(50)
bu_han_shui_jin_e_yuan_du String? @db.VarChar(255)
han_shui_jin_e_yuan_du String? @db.VarChar(255)
bu_han_shui_jin_e_yuan_da String? @db.VarChar(255)
han_shui_jin_e_yuan_da String? @db.VarChar(255)
bu_han_shui_jin_e_yuan_da_du String? @db.VarChar(255)
han_shui_jin_e_yuan_da_du String? @db.VarChar(255)
bu_han_shui_jin_e_yuan_duo String? @db.VarChar(255)
han_shui_jin_e_yuan_duo String? @db.VarChar(255)
bu_han_shui_jin_e_yuan_other String? @db.VarChar(255)
han_shui_jin_e_yuan_other String? @db.VarChar(255)
applicant String? @db.VarChar(255)
business_content_du String? @db.VarChar(255)
uploadedContractAttachment String? @db.Text
uploadedSealedCostListDa String? @db.Text
uploadedSealedCostListDu String? @db.Text
invoiceType String? @db.VarChar(50)
invoiceCode Int? @db.UnsignedTinyInt
kai_piao_shi_you String? @db.VarChar(255)
fa_piao_bei_zhu_lan_tian_xie_nei_rong String? @db.Text
tong_yi_she_hui_xin_yong_dai_ma String? @db.VarChar(255)
bankName String? @db.VarChar(255)
bankAccount String? @db.VarChar(255)
invoice_type_enum Int?
kai_piao_ri_qi String? @db.VarChar(50)
hui_kuan_ri_qi String? @db.VarChar(255)
hui_kuan_jin_e String? @db.VarChar(255)
zhang_qi String? @db.VarChar(255)
unit_nature String? @db.VarChar(100)
hong_chong_piao_hao String? @db.VarChar(50)
}

View File

@@ -0,0 +1,93 @@
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
function splitProjectNumbers(value) {
return String(value ?? '')
.split(/[,,、\s]+/)
.map((s) => s.trim())
.filter(Boolean);
}
async function main() {
const project = 'DJ202500868';
const rows = await prisma.invoice_application_match_view.findMany({
where: { projectNumber: { contains: project } },
select: {
dataId: true,
invoiceJobNumber: true,
projectNumber: true,
yuJiBaoXiaoDiSanFangChengBenYuan: true,
kai_piao_ri_qi: true,
},
});
const filtered = rows.filter((r) => splitProjectNumbers(r.projectNumber).includes(project));
const dataIds = [...new Set(filtered.map((r) => r.dataId).filter(Boolean))];
const details =
dataIds.length > 0
? await prisma.invoice_amount_details.findMany({
where: {
dataid: { in: dataIds },
project_number: project,
},
select: {
dataid: true,
project_number: true,
invoice_amount: true,
yu_ji_bao_xiao_di_san_fang_cheng_ben: true,
},
})
: [];
const mainByDataId = dataIds.map((id) => {
const arr = filtered.filter((r) => r.dataId === id);
const uniqueMainThirdParty = [...new Set(arr.map((r) => String(r.yuJiBaoXiaoDiSanFangChengBenYuan ?? '')).filter(Boolean))];
const detailThirdPartyRaw = details
.filter((d) => d.dataid === id)
.map((d) => String(d.yu_ji_bao_xiao_di_san_fang_cheng_ben ?? ''))
.filter((s) => s !== '');
return {
dataId: id,
mainCount: arr.length,
uniqueMainThirdParty,
detailRows: details.filter((d) => d.dataid === id).length,
detailThirdPartyRaw,
};
});
const mainDedupByInvoiceKey = new Map();
filtered.forEach((r) => {
const key = `${r.dataId || ''}||${r.invoiceJobNumber || ''}`;
if (!mainDedupByInvoiceKey.has(key)) {
mainDedupByInvoiceKey.set(key, Number(r.yuJiBaoXiaoDiSanFangChengBenYuan || 0));
}
});
const mainTotal = [...mainDedupByInvoiceKey.values()].reduce((s, n) => s + n, 0);
const detailTotal = details.reduce((s, r) => s + Number(r.yu_ji_bao_xiao_di_san_fang_cheng_ben || 0), 0);
console.log(
JSON.stringify(
{
project,
filteredCount: filtered.length,
dataIds,
mainByDataId,
mainTotal,
detailTotal,
},
null,
2
)
);
}
main()
.catch((e) => {
console.error(e);
process.exitCode = 1;
})
.finally(async () => {
await prisma.$disconnect();
});

View File

@@ -0,0 +1,92 @@
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
function splitProjectNumbers(value) {
return String(value ?? '')
.split(/[,,、\s]+/)
.map((s) => s.trim())
.filter(Boolean);
}
async function main() {
const project = 'JN202600186';
const mainRows = await prisma.invoice_application_match_view.findMany({
where: { projectNumber: { contains: project } },
select: {
dataId: true,
invoiceJobNumber: true,
projectNumber: true,
bu_han_shui_jin_e_yuan_duo: true,
han_shui_jin_e_yuan_duo: true,
bu_han_shui_jin_e_yuan_du: true,
bu_han_shui_jin_e_yuan_other: true,
bu_han_shui_jin_e_yuan_da: true,
bu_han_shui_jin_e_yuan_da_du: true,
kai_piao_ri_qi: true,
},
});
const filteredRows = mainRows.filter((r) => splitProjectNumbers(r.projectNumber).includes(project));
const ids = [...new Set(filteredRows.map((r) => r.dataId).filter(Boolean))];
const detailRows =
ids.length > 0
? await prisma.invoice_amount_details.findMany({
where: {
dataid: { in: ids },
project_number: project,
},
select: {
dataid: true,
project_number: true,
invoice_amount: true,
yu_ji_bao_xiao_di_san_fang_cheng_ben: true,
},
})
: [];
const summaryByDataId = ids.map((dataId) => {
const mainForDataId = filteredRows.filter((r) => r.dataId === dataId);
const detailForDataId = detailRows.filter((r) => r.dataid === dataId);
const duoBuHan = Number(mainForDataId.find((r) => r.bu_han_shui_jin_e_yuan_duo)?.bu_han_shui_jin_e_yuan_duo || 0);
const duoHan = Number(mainForDataId.find((r) => r.han_shui_jin_e_yuan_duo)?.han_shui_jin_e_yuan_duo || 0);
const detailHanShuiSum = detailForDataId.reduce((s, r) => s + Number(r.invoice_amount || 0), 0);
const calcBuHan = duoHan > 0 ? (detailHanShuiSum * duoBuHan) / duoHan : detailHanShuiSum;
return {
dataId,
mainRowCount: mainForDataId.length,
detailRowCount: detailForDataId.length,
detailHanShuiSum,
duoBuHan,
duoHan,
calcBuHan,
detailSamples: detailForDataId.slice(0, 5),
};
});
const totalBuHan = summaryByDataId.reduce((s, r) => s + r.calcBuHan, 0);
console.log(
JSON.stringify(
{
project,
filteredCount: filteredRows.length,
ids,
summaryByDataId,
totalBuHan,
},
null,
2
)
);
}
main()
.catch((e) => {
console.error(e);
process.exitCode = 1;
})
.finally(async () => {
await prisma.$disconnect();
});

325
src/CONSTANTS.ts Normal file
View File

@@ -0,0 +1,325 @@
// 这里存放项目的常量配置
// oa 正式环境东风
// oa 测试环境 TODO 待建立
// 协议类型常量
export const INVOICE_TYPES = {
NORMAL: 0,
VOID_CURRENT_MONTH: 1,
VOID_REDEEM: 2
}
// 发票类型编码
export const INVOICE_TYPE_CODE = {
[INVOICE_TYPES.NORMAL]: '0',
[INVOICE_TYPES.VOID_CURRENT_MONTH]: '1',
[INVOICE_TYPES.VOID_REDEEM]: '2'
}
export const INVOICE_TYPE_NAME = {
'普通票': INVOICE_TYPES.NORMAL,
'作废当月': INVOICE_TYPES.VOID_CURRENT_MONTH,
'作废红冲': INVOICE_TYPES.VOID_REDEEM
}
export const AGREEMENT_TYPES = {
LARGE: 'large',
INDEPENDENT: 'independent',
LARGE_INDEPENDENT: 'large_independent',
OTHER: 'other'
}
// 协议类型编码
export const AGREEMENT_TYPE_CODE = {
[AGREEMENT_TYPES.LARGE]: '1',
[AGREEMENT_TYPES.INDEPENDENT]: '2',
[AGREEMENT_TYPES.LARGE_INDEPENDENT]: '3',
[AGREEMENT_TYPES.OTHER]: '4'
}
export const AGREEMENT_TYPE_NAME = {
'大协议(客户服务室专用)': AGREEMENT_TYPES.LARGE,//大协议改为大协议(客户服务室专用)
'独立协议': AGREEMENT_TYPES.INDEPENDENT,
'大协议+独立协议(客户服务室专用)': AGREEMENT_TYPES.LARGE_INDEPENDENT,//大协议+独立协议改为大协议+独立协议(客户服务室专用)
'其它': AGREEMENT_TYPES.OTHER
}
// 创建一个从协议类型编码到协议类型名称的映射
export const AGREEMENT_CODE_TO_NAME = Object.fromEntries(
Object.entries(AGREEMENT_TYPE_NAME).map(([name, code]) => [AGREEMENT_TYPE_CODE[code], name])
);
export const AGREEMENT_CODE_TO_TYPE = Object.fromEntries(
Object.entries(AGREEMENT_TYPE_CODE).map(([type, code]) => [code, type])
);
// 字段映射配置
export const FIELD_MAPPINGS = {
purchase_contract_execution_maintenance: {
'数据id': 'data_id',
'发起人': 'initiator',
'合同编号': 'contract_number',
'合同名称': 'contract_name',
'履约发起部门': 'lvyue_faqi_bumen',
'经办人': 'operator',
'相对方': 'xiangduifang',
'是否开口': 'is_open',
'合同总额': 'contract_amount',
'备注': 'remarks',
'履约开始时间': 'performance_start_time',
'合同原件扫描件(盖章版)': 'hetong_yuanjian_saomiaojian_gaizhangban',
'钉钉采购审批单': 'dingding_purchase_approval_file',
'合同附件扫描件': 'contract_attachment_scan',
'创建时间': 'creation_time',
'创建人': 'creator',
'更新时间': 'update_time',
'创建人部门': 'creator_department',
'审批单标题': 'approval_form_title',
'耗时(时:分:秒)': 'time_consumed',
},
'purchase_contract_order_management': {
'数据id': 'data_id',
'采购合同编号': 'purchase_contract_number',
'采购用途': 'purchase_purpose',
'项目编号': 'project_number',
'项目名称': 'project_name',
'关联销售合同编号': 'associated_sales_contract_number',
'合同名称': 'contract_name',
'采购审批单号(请先填写合同编号)': 'purchase_approval_number',
//'采购订单编号(请先填写合同编号)': 'purchase_approval_number',
'合同名称(***请再次确认合同名称***)': 'purchase_contract_name',
// '采购合同名称(***请再次确认合同名称***)': 'purchase_contract_name',
'相对方': 'xiang_dui_fang',
'合同类别(是否开口)': 'he_tong_lei_bie',
'合同总金额(含税)': 'he_tong_zong_jin_e_HanShui',
'剩余金额(含税)': 'sheng_yu_jin_e_HanShui',
'合同剩余金额提醒': 'he_tong_sheng_yu_jin_e_ti_xing',
'本次订单名称': 'ben_ci_ding_dan_ming_cheng',
'本次订单金额(含税)': 'ben_ci_ding_dan_jin_e_HanShui',
'本次订单履约开始时间': 'ben_ci_ding_dan_lu_yue_kai_shi_shi_jian',
'本次订单履约结束时间': 'ben_ci_ding_dan_lu_yue_jie_shu_shi_jian',
'本次订单发起部门': 'ben_ci_ding_dan_fa_qi_bu_men',
'本次订单发起人': 'ben_ci_ding_dan_fa_qi_ren',
'本次订单发起时间': 'ben_ci_ding_dan_fa_qi_shi_jian',
'本次订单文本(多文件请压缩后上传)': 'ben_ci_ding_dan_wen_ben',
'本次订单关联合同附件': 'ben_ci_ding_dan_guan_lian_he_tong_fujian',
'审批编号': 'approval_number',
'创建时间': 'creation_time',
'创建人': 'creator',
'当前负责人': 'current_responsible_person',
'审批结果': 'approval_result',
'审批状态': 'approval_status',
'更新时间': 'update_time',
'创建人部门': 'creator_department',
'审批单标题': 'approval_form_title',
'耗时(时:分:秒)': 'time_consumed'
},
"dingding_oa_invoice_application": {
'数据id': 'dataId',
'开票类型': 'agreementType',
'申请日期': 'applicationDate',
'申请人': 'applicant',
'申请部门': 'applicationDepartment',
'业务组名称': 'xiang_mu_zu_bie',//项目组别改为业务组名称
'预计报销第三方成本(不含税,元)': 'yuJiBaoXiaoDiSanFangChengBenYuan',
'业务开始时间': 'businessStartTime',
'业务结束时间': 'businessEndTime',
'销售合同编号': 'contractNumber',// 因为这里是关联的数据怀疑是 别名 --展示名称前面有销售
'销售合同名称': 'contractName',// 因为这里是关联的数据怀疑是 别名--展示名称前面有销售
'上传合同及相关附件': 'uploadedContractAttachment',
'上传合同及相关附件(含费用清单)': 'uploadedContractAttachment',
'项目编号': 'projectNumber',
'金额是否含税': 'is_tax_inclusive',//是否含税改为金额是否含税
'开票税率': 'invoice_tax_rate',
'是否多项目合并': 'is_multi_project_merge',
'上传费用清单': 'uploadedSealedCostListDa',//上传盖章费用清单(大)改为上传费用清单
'请上传费用清单': 'uploadedSealedCostListDa',//上传盖章费用清单(大)改为上传费用清单
'上传盖章费用清单(独)': 'uploadedSealedCostListDu',//上传盖章费用清单(独)弃用
'付款单位名称': 'customerCompanyName',//客户单位名称改为付款单位名称
'付款单位名称2': 'customerCompanyName',//客户单位名称改为付款单位名称
'付款单位名称3': 'customerCompanyName',//客户单位名称改为付款单位名称
'客户单位名称(其他)': 'customerCompanyNameOther',
'统一社会信用代码': 'tong_yi_she_hui_xin_yong_dai_ma',
'开户行名称': 'bankName',
'银行账号': 'bankAccount',
'付款人': 'payee',//新增付款人
'付款人2': 'payee',//新增付款人
'付款人3': 'payee',//新增付款人
'发票税目': 'invoice_tax_category',
'业务内容(摘要)': 'business_content_du',
'开票金额明细(大)': 'invoice_amount_yuan_da',//开票金额明细(大)
// "开票金额(元,不含税)": 'invoice_amount_yuan_bu_han_shui',//开票金额(元,不含税)
// "付款单位名称2": 'customerCompanyName2',//新增付款单位名称2
// "付款单位名称3": 'customerCompanyName3',//新增付款单位名称3
// "付款人3": 'payee3',//新增付款人3
// "付款人2": 'payee2',//新增付款人2
// "项目名称1": 'projectName1',//新增项目名称1
'开票金额(元,不含税)':'invoice_amount_yuan',
'开票金额(元)独': 'invoice_amount_yuan_du',
'开票金额(元)其他': 'invoice_amount_yuan_other',
'不含税金额(元)独': 'bu_han_shui_jin_e_yuan_du',
'不含税金额(元)独(大写)': 'bu_han_shui_jin_e_yuan_du_da_xie',
'含税金额(元)独': 'han_shui_jin_e_yuan_du',
'含税金额(元)独(大写)': 'han_shui_jin_e_yuan_du_da_xie',
'不含税金额(元)其他': 'bu_han_shui_jin_e_yuan_other',
'不含税金额(元)其他(大写)': 'bu_han_shui_jin_e_yuan_other_da_xie',
'含税金额(元)其他': 'han_shui_jin_e_yuan_other',
'含税金额(元)其他(大写)': 'han_shui_jin_e_yuan_other_da_xie',
'不含税金额(元)多': 'bu_han_shui_jin_e_yuan_duo',
'不含税金额(元)多项': 'bu_han_shui_jin_e_yuan_duo',
'不含税金额(元)多(大写)': 'bu_han_shui_jin_e_yuan_duo_da_xie',
'含税金额(元)多': 'han_shui_jin_e_yuan_duo',
'含税金额(元)多项': 'han_shui_jin_e_yuan_duo',
'含税金额(元)多(大写)': 'han_shui_jin_e_yuan_duo_da_xie',
'不含税金额(元)大': 'bu_han_shui_jin_e_yuan_da',
'含税总金额(元)大': 'han_shui_zong_jin_e_yuan_da',
'不含税金额(元)(大+独)': 'bu_han_shui_jin_e_yuan_da_du',
'不含税金额(元)(大+独)(大写)': 'bu_han_shui_jin_e_yuan_da_du_da_xie',
'含税总金额(元)(大+独)': 'han_shui_zong_jin_e_yuan_da_du',
'含税总金额(元)(大+独)(大写)': 'han_shui_zong_jin_e_yuan_da_du_da_xie',
"不含税金额(元)多家": 'bu_han_shui_jin_e_yuan_duo',//不含税金额(元)多项改为多家
"含税金额(元)多家": 'han_shui_jin_e_yuan_duo',//含税金额(元)多项改为多家
'审批编号': 'approvalNumber',
'创建时间': 'creationTime',
'创建人': 'creator',
'当前负责人': 'currentResponsiblePerson',
'审批结果': 'approvalResult',
'审批状态': 'approvalStatus',
'更新时间': 'updateTime',
'创建人部门': 'creatorDepartment',
'审批单标题': 'approvalFormTitle',
'历史审批人': 'historicalApprovers',
'耗时(时:分:秒)': 'timeConsumed',
'审批记录': 'approvalRecord',
"开票金额明细": 'BigInvoiceAmountList',
'多项目合并开票请添加': 'multiProjectMergeInvoice',//多项目合并开票请添加项目名称改为多项目合并开票请添加
// "多项目合并开票请添加项目名称": 'multiProjectMergeInvoice',
"[\"业务开始时间\",\"业务结束时间\"]": 'businessStartTime-BusinessEndTime',
// 新增字段映射
'开票事由': 'kai_piao_shi_you',
'发票备注栏填写内容': 'fa_piao_bei_zhu_lan_tian_xie_nei_rong',
'票据类型': 'invoiceType',
'发票开具类别': 'invoice_tax_category',//发票开具类别数据库表中发票类目
'是否单合同多家单位开票': 'is_single_contract_multi_customer',//是否单合同多家单位开票
'单合同多家单位开票清单': 'single_contract_multi_customer_invoice_list',//单合同多家单位开票清单
},
"invoice_void_record": {
'数据id': 'dataId',
'所在部门': 'applicationDepartment',
'申请业务组': 'xiang_mu_zu_bie',
'申请日期': 'applicationDate',
'申请人': 'applicant',
'发票号码': 'piao_hao',
'开票日期': 'kai_piao_ri_qi',
'协议类型': 'agreementType',
'作废事由': 'zuo_fei_shi_you',
'作废原因': 'cancelReason',
'发票金额': 'invoice_amount_yuan',
'金额是否含税': 'is_tax_inclusive',
'费用所属时间': 'feeBelongingTime',
'付款单位名称': 'customerCompanyName',
'付款单位纳税人识别号': 'tong_yi_she_hui_xin_yong_dai_ma',
'付款单位开户行': 'bankName',
'付款单位开户账号': 'bankAccount',
'上传作废发票附件': 'uploadedVoidInvoiceAttachment',
'手写签名': 'handwrittenSignatureAttachment',
'项目编号': 'projectNumber',
'备注信息': 'remark'
}
};
export const DATA_ID_FIELD_MAPPINGS = {
purchase_contract_execution_maintenance: 'data_id',
purchase_contract_order_management: 'data_id',
dingding_oa_invoice_application: 'dataId',
invoice_void_record: 'dataId',
}
// 受支持的表配置
export const SUPPORTED_TABLES = {
purchase_contract_execution_maintenance: '采购合同执行维护表',
purchase_contract_order_management: '采购合同订单管理',
dingding_oa_invoice_application: '钉钉OA发票申请表',
invoice_void_record: '发票作废记录表'
};
export const CONSTANTS = {
// 示例配置
// key: 'value'
};
export const XIANGMU_ZUBIE_MAP: {
[key: string]: string
} = {
'运营室': '运营',
'人员招聘项目组': '招聘',
'总务服务项目组': '总务',
'薪酬核算项目组': '薪酬',
'退休衔接服务项目组': '退休衔接',
'计划核定项目组': '统筹-计核',
'老干部服务项目组': '老干部服务',
'社保业务项目组': '统筹-社保',
'医保业务项目组': '统筹-医保',
'统筹支持项目组': '统筹-支持',
'人员入转离项目组': '入转离',
'人才评鉴项目组': '评鉴',
'会务服务项目组': '培训-会务',
'国际化服务项目组': '国际化服务',
'管理咨询项目组': '管理咨询',
'客户服务室': '客户服务',
'福利业务项目组': '福利',
'法务合规中心': '法务',
'数字化与交付项目组': '培训-数交',
'科技培训项目组': '培训-科技',
'经营管理项目组': '培训-经管',
'技能培训项目组': '培训-技能',
'党建培训项目组': '党校-党建',
'数字化室': '数字化',
'武汉站': '武汉站',
'十堰站': '十堰站',
'襄阳站': '襄阳站',
}
export const ABBR_ZUBIE_MAP: {
[key: string]: string
} = {
YY: '运营室',
ZP: '人员招聘项目组',
ZW: '总务服务项目组',
XC: '薪酬核算项目组',
TX: '退休衔接服务项目组',
TC: '统筹支持项目组',
RZL: '人员入转离项目组',
PJ: '人才评鉴项目组',
HW: '会务服务项目组',
GJ: '国际化服务项目组',
GZ: '管理咨询项目组',
KF: '客户服务室',
FL: '福利业务项目组',
FW: '法务合规中心',
SJ: '数字化与交付项目组',
KJ: '科技培训项目组',
JG: '经营管理项目组',
JN: '技能培训项目组',
DJ: '党建培训项目组',
JH: '计划核定项目组',
LGB: '老干部服务项目组'
}
// 新增:票据类型的双向映射(中文 <-> 代码)
export const INVOICE_CODE: Record<string, number> = {
'增值税专用发票': 1,
'增值税普通发票': 2,
};
export const INVOICE_NAME: Record<number, string> = {
1: '增值税专用发票',
2: '增值税普通发票',
};

186
src/api/dingding/auth.ts Normal file
View File

@@ -0,0 +1,186 @@
// This file is auto-generated, don't edit it
import Util from '@alicloud/tea-util';
import dingtalkoauth2_1_0, * as $dingtalkoauth2_1_0 from '@alicloud/dingtalk/oauth2_1_0';
import OpenApi, * as $OpenApi from '@alicloud/openapi-client';
import { logger } from '../winston/logger';
export default class AuthClient {
/**
* 使用 Token 初始化账号Client
* @return Client
* @throws Exception
*/
static createClient(): dingtalkoauth2_1_0 {
let config = new $OpenApi.Config({});
config.protocol = "https";
config.regionId = "central";
return new dingtalkoauth2_1_0(config);
}
static async getAccessToken(): Promise<string> {
let client = AuthClient.createClient();
// 测试环境
// let getAccessTokenRequest = new $dingtalkoauth2_1_0.GetAccessTokenRequest({
// appKey: "dingjrkdm6lqfebrwrhs",
// appSecret: "oxYwPO9F6-_TbjI-uKFpO1__Qc3jRPQ5MSInBvei2r6rscg_gZexbAqi-QR6ZNCz",
// });
// 正式环境
let getAccessTokenRequest = new $dingtalkoauth2_1_0.GetAccessTokenRequest({
appKey: process.env.DINGDING_APP_KEY,//"ding3fdr9ulnhzlvnznb",
appSecret: process.env.DINGDING_APP_SECRET,// "Jk9Ph6iQ0gmkSxjOTZ7Sx3q_Z9HfJ97SbHoB2M6FFTNBrXqXAg-h1npfyAtznB7i",
});
try {
const res = await client.getAccessToken(getAccessTokenRequest);
console.log("res", res)
console.log("拿到钉钉的accessToken", res?.body?.accessToken);
return res?.body?.accessToken;
} catch (error: any) {
console.error("AuthClient : getAccessToken", error)
logger.error("AuthClient : getAccessToken", error)
if (!Util.empty(error.code) && !Util.empty(error.message)) {
// err 中含有 code 和 message 属性,可帮助开发定位问题
}
return ""
}
}
}
// GetAccessTokenResponse {
// headers: {
// server: 'DingTalk/1.0.0',
// date: 'Mon, 30 Jun 2025 07:00:10 GMT',
// 'content-type': 'application/json;charset=utf-8',
// 'content-length': '66',
// connection: 'keep-alive',
// 'access-control-allow-origin': '*',
// 'access-control-expose-headers': '*',
// 'x-acs-request-id': '30D76DD0-4583-7A5A-A1F2-0846EEC1922D',
// 'x-acs-trace-id': 'e8076fdcf17402c1f1557baf1cec4b39',
// etag: '6F3iZV6GzSa1IL6X9PhSUVQ6',
// 'access-control-allow-headers': 'X-Requested-With, X-Sequence, _aop_secret, _aop_signature, x-acs-dingtalk-access-token'
// },
// statusCode: 200,
// body: GetAccessTokenResponseBody {
// accessToken: 'e12b5b336e103db2bca3bf6f120cf071',
// expireIn: 7200
// }
// }
// ----
// {
// headers: {
// server: 'DingTalk/1.0.0',
// date: 'Mon, 30 Jun 2025 09:21:30 GMT',
// 'content-type': 'application/json;charset=utf-8',
// 'content-length': '245',
// connection: 'keep-alive',
// 'access-control-allow-origin': '*',
// 'access-control-expose-headers': '*',
// 'x-acs-request-id': 'D08BBF0B-0C7A-777D-BEE0-E2481030660E',
// 'x-acs-trace-id': '4dd951ff3fdb1b880d8773ab8db4cc38',
// etag: '2sqH6LVyqyanwQEcOE52W+g5',
// 'access-control-allow-headers': 'X-Requested-With, X-Sequence, _aop_secret, _aop_signature, x-acs-dingtalk-access-token'
// },
// statusCode: 200,
// body: ListProcessInstanceIdsResponseBody {
// result: ListProcessInstanceIdsResponseBodyResult {
// list: [Array],
// nextToken: '2'
// },
// success: true
// }
// }
// {
// "headers": {
// "server": "DingTalk/1.0.0",
// "date": "Tue, 01 Jul 2025 01:42:36 GMT",
// "content-type": "application/json;charset=utf-8",
// "content-length": "1384",
// "connection": "keep-alive",
// "vary": "Accept-Encoding, Accept-Encoding",
// "access-control-allow-origin": "*",
// "access-control-expose-headers": "*",
// "x-acs-request-id": "B76B8B3D-19E2-7274-8511-8BE66A6BE486",
// "x-acs-trace-id": "d271717bd4706e5495c7bec3fe226723",
// "etag": "1uyNMfzsmzilvk+sq7asEUw0",
// "access-control-allow-headers": "X-Requested-With, X-Sequence, _aop_secret, _aop_signature, x-acs-dingtalk-access-token"
// },
// "statusCode": 200,
// "body": {
// "result": {
// "attachedProcessInstanceIds": [
// ],
// "bizAction": "NONE",
// "bizData": "{}",
// "businessId": "202506301703000580140",
// "createTime": "2025-06-30T17:03Z",
// "formComponentValues": [
// {
// "bizAlias": "",
// "componentType": "TextField",
// "id": "TextField-K2AD4O5B",
// "name": "测试表单1",
// "value": "<22>🐶🐩"
// },
// {
// "bizAlias": "",
// "componentType": "TextField",
// "id": "TextField_RJYCNJG9I680",
// "name": "测试表单2",
// "value": "🐕🐶🐶"
// },
// {
// "componentType": "NumberField",
// "id": "NumberField_1I5DKSPC42GW0",
// "name": "数字输入框",
// "value": "234565432"
// },
// {
// "componentType": "DDDateField",
// "id": "DDDateField_1JRPIPARZUKG0",
// "name": "日期",
// "value": "2025-07-25"
// }
// ],
// "operationRecords": [
// {
// "date": "2025-06-30T17:03Z",
// "images": [
// ],
// "result": "NONE",
// "showName": "提交申请",
// "type": "START_PROCESS_INSTANCE",
// "userId": "manager6697"
// }
// ],
// "originatorDeptId": "-1",
// "originatorDeptName": "猫咪食品厂",
// "originatorUserId": "manager6697",
// "result": "",
// "status": "RUNNING",
// "tasks": [
// {
// "activityId": "1918_5cd3",
// "createTime": "2025-06-30T17:03Z",
// "mobileUrl": "aflow.dingtalk.com?procInsId=c73YooUSTFW49EIXKjCPWA10021751274238&taskId=94657860987&businessId=202506301703000580140",
// "pcUrl": "aflow.dingtalk.com?procInsId=c73YooUSTFW49EIXKjCPWA10021751274238&taskId=94657860987&businessId=202506301703000580140",
// "result": "NONE",
// "status": "RUNNING",
// "taskId": 94657860987,
// "userId": "manager6697"
// }
// ],
// "title": "yatsov提交的数字化科室-测试表单"
// },
// "success": "true"
// }
// }

View File

@@ -0,0 +1,50 @@
// This file is auto-generated, don't edit it
import Util,{ RuntimeOptions } from '@alicloud/tea-util';
import dingtalkworkflow_1_0, * as $dingtalkworkflow_1_0 from '@alicloud/dingtalk/workflow_1_0';
import OpenApi, * as $OpenApi from '@alicloud/openapi-client';
import { logger } from '../winston/logger';
export default class DownLoadClient {
/**
* 使用 Token 初始化账号Client
* @return Client
* @throws Exception
*/
static createClient(): dingtalkworkflow_1_0 {
let config = new $OpenApi.Config({ });
config.protocol = "https";
config.regionId = "central";
return new dingtalkworkflow_1_0(config);
}
static async getFileURL({
processInstanceId,
fileId,
xAcsDingtalkAccessToken
}:{
processInstanceId: string,
fileId: string,
xAcsDingtalkAccessToken: string
}): Promise<void> {
let client = DownLoadClient.createClient();
let grantProcessInstanceForDownloadFileHeaders = new $dingtalkworkflow_1_0.GrantProcessInstanceForDownloadFileHeaders({ });
grantProcessInstanceForDownloadFileHeaders.xAcsDingtalkAccessToken = xAcsDingtalkAccessToken;
let grantProcessInstanceForDownloadFileRequest = new $dingtalkworkflow_1_0.GrantProcessInstanceForDownloadFileRequest({
processInstanceId,
fileId,
});
try {
return client.grantProcessInstanceForDownloadFileWithOptions(grantProcessInstanceForDownloadFileRequest, grantProcessInstanceForDownloadFileHeaders, new RuntimeOptions({ }));
} catch (err) {
console.error("DownLoadClient : getFileURL", err)
logger.error("DownLoadClient : getFileURL", err)
if (!Util.empty(err.code) && !Util.empty(err.message)) {
// err 中含有 code 和 message 属性,可帮助开发定位问题
}
}
}
}

View File

@@ -0,0 +1,46 @@
// This file is auto-generated, don't edit it
import Util,{ RuntimeOptions } from '@alicloud/tea-util';
import dingtalkworkflow_1_0, * as $dingtalkworkflow_1_0 from '@alicloud/dingtalk/workflow_1_0';
import dingtalkoauth2_1_0, * as $dingtalkoauth2_1_0 from '@alicloud/dingtalk/oauth2_1_0';
import OpenApi, * as $OpenApi from '@alicloud/openapi-client';
export default class FormInstancesClient {
/**
* 使用 Token 初始化账号Client
* @return Client
* @throws Exception
*/
static createClient(): dingtalkworkflow_1_0 {
let config = new $OpenApi.Config({});
config.protocol = "https";
config.regionId = "central";
return new dingtalkworkflow_1_0(config);
}
static async getFormInstances(args: {
dingtalkAccessToken: string;
nextToken: string;
maxResults: number;
appUuid: string;
formCode: string;
}) {
let client = FormInstancesClient.createClient();
let premiumGetFormInstancesHeaders = new $dingtalkworkflow_1_0.PremiumGetFormInstancesHeaders({});
premiumGetFormInstancesHeaders.xAcsDingtalkAccessToken = args.dingtalkAccessToken;
let premiumGetFormInstancesRequest = new $dingtalkworkflow_1_0.PremiumGetFormInstancesRequest({
nextToken: args.nextToken,//'100010',
maxResults: args.maxResults,//100,
appUuid: args.appUuid,//'SWAPP-dacdsa-example',
formCode: args.formCode,//'PROC-daccea-example',
});
try {
return client.premiumGetFormInstancesWithOptions(premiumGetFormInstancesRequest, premiumGetFormInstancesHeaders, new RuntimeOptions({}));
} catch (err) {
if (!Util.empty(err.code) && !Util.empty(err.message)) {
// err 中含有 code 和 message 属性,可帮助开发定位问题
}
return undefined
}
}
}

88
src/api/dingding/oa.ts Normal file
View File

@@ -0,0 +1,88 @@
// https://open.dingtalk.com/document/orgapp/obtain-an-approval-list-of-instance-ids
// This file is auto-generated, don't edit it
import Util, * as $Util from '@alicloud/tea-util';
import dingtalkworkflow_1_0, * as $dingtalkworkflow_1_0 from '@alicloud/dingtalk/workflow_1_0';
import OpenApi, * as $OpenApi from '@alicloud/openapi-client';
import { logger } from '../winston/logger';
export default class OAClient {
/**
* 使用 Token 初始化账号Client
* @return Client
* @throws Exception
*/
static createClient(): dingtalkworkflow_1_0 {
let config = new $OpenApi.Config({});
config.protocol = "https";
config.regionId = "central";
return new dingtalkworkflow_1_0(config);
}
static async listProcessInstanceIdsWithOptions(args: {
processCode: string;
startTime: number;
endTime?: number;
nextToken?: number;
maxResults: number;
userIds?: string[]
statuses?: string[]
dingtalkAccessToken: string;
}) {
let client = OAClient.createClient();
let listProcessInstanceIdsHeaders = new $dingtalkworkflow_1_0.ListProcessInstanceIdsHeaders({});
listProcessInstanceIdsHeaders.xAcsDingtalkAccessToken = args.dingtalkAccessToken;
let listProcessInstanceIdsRequest = new $dingtalkworkflow_1_0.ListProcessInstanceIdsRequest({
processCode: args.processCode,
startTime: args.startTime,
endTime: args.endTime,
nextToken: args.nextToken,
maxResults: args.maxResults ?? 20,
userIds: args.userIds ? args.userIds : [],
statuses: args.statuses ? args.statuses : [],
});
try {
const res = await client.listProcessInstanceIdsWithOptions(listProcessInstanceIdsRequest, listProcessInstanceIdsHeaders, new $Util.RuntimeOptions({}));
console.log("listProcessInstanceIdsWithOptions 结果", res)
return res;
} catch (err: any) {
logger.error("listProcessInstanceIdsWithOptions", err)
console.error("listProcessInstanceIdsWithOptions")
console.error(err)
if (!Util.empty(err.code) && !Util.empty(err.message)) {
// err 中含有 code 和 message 属性,可帮助开发定位问题
}
return null;
}
}
static async getProcessInstanceWithOptions(args: {
processInstanceId: string;
dingtalkAccessToken: string;
}) {
let client = OAClient.createClient();
let getProcessInstanceHeaders = new $dingtalkworkflow_1_0.GetProcessInstanceHeaders({});
getProcessInstanceHeaders.xAcsDingtalkAccessToken = args.dingtalkAccessToken;
let getProcessInstanceRequest = new $dingtalkworkflow_1_0.GetProcessInstanceRequest({
processInstanceId: args.processInstanceId,
});
try {
const res = await client.getProcessInstanceWithOptions(getProcessInstanceRequest, getProcessInstanceHeaders, new $Util.RuntimeOptions({}));
return {
...res,
processInstanceId: args.processInstanceId,
};
} catch (err: any) {
logger.error("getProcessInstanceWithOptions", err)
console.error("getProcessInstanceWithOptions")
console.error(err)
if (!Util.empty(err.code) && !Util.empty(err.message)) {
// err 中含有 code 和 message 属性,可帮助开发定位问题
}
return null;
}
}
}

29
src/api/winston/logger.ts Normal file
View File

@@ -0,0 +1,29 @@
import winston from 'winston';
export const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
defaultMeta: { service: 'user-service' },
transports: [
//
// - Write all logs with importance level of `error` or higher to `error.log`
// (i.e., error, fatal, but not other levels)
//
// new winston.transports.File({ filename: 'error.log', level: 'error' }),
//
// - Write all logs with importance level of `info` or higher to `combined.log`
// (i.e., fatal, error, warn, and info, but not trace)
//
new winston.transports.File({ filename: 'combined.log' }),
],
});
//
// If we're not in production then log to the `console` with the format:
// `${info.level}: ${info.message} JSON.stringify({ ...rest }) `
//
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.simple(),
}));
}

BIN
src/app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

26
src/app/globals.css Normal file
View File

@@ -0,0 +1,26 @@
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}

34
src/app/layout.tsx Normal file
View File

@@ -0,0 +1,34 @@
import type { Metadata } from "next";
// import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
// const geistSans = Geist({
// variable: "--font-geist-sans",
// subsets: ["latin"],
// });
// const geistMono = Geist_Mono({
// variable: "--font-geist-mono",
// subsets: ["latin"],
// });
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={` antialiased`}
>
{children}
</body>
</html>
);
}

103
src/app/page.tsx Normal file
View File

@@ -0,0 +1,103 @@
import Image from "next/image";
export default function Home() {
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>
<ol className="list-inside list-decimal text-sm/6 text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
<li className="mb-2 tracking-[-.01em]">
Get started by editing{" "}
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-[family-name:var(--font-geist-mono)] font-semibold">
src/app/page.tsx
</code>
.
</li>
<li className="tracking-[-.01em]">
Save and see your changes instantly.
</li>
</ol>
<div className="flex gap-4 items-center flex-col sm:flex-row">
<a
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Read our docs
</a>
</div>
</main>
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/file.svg"
alt="File icon"
width={16}
height={16}
/>
Learn
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to nextjs.org
</a>
</footer>
</div>
);
}

View File

@@ -0,0 +1,33 @@
/*
Navicat Premium Data Transfer
Source Server : 本地数据库
Source Server Type : MySQL
Source Server Version : 80042 (8.0.42)
Source Host : localhost:3306
Source Schema : procurement_db
Target Server Type : MySQL
Target Server Version : 80042 (8.0.42)
File Encoding : 65001
Date: 20/10/2025 14:15:36
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for customer_management_info
-- ----------------------------
DROP TABLE IF EXISTS `customer_management_info`;
CREATE TABLE `customer_management_info` (
`data_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '数据id',
`customer_unit_name` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '客户单位名称',
`customer_type` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '客户分类',
`created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`data_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '客户管理信息表' ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;

View File

@@ -0,0 +1,86 @@
/*
Navicat Premium Data Transfer
Source Server : 本地数据库
Source Server Type : MySQL
Source Server Version : 80042 (8.0.42)
Source Host : localhost:3306
Source Schema : procurement_db
Target Server Type : MySQL
Target Server Version : 80042 (8.0.42)
File Encoding : 65001
Date: 20/10/2025 14:15:43
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for dingding_oa_invoice_application
-- ----------------------------
DROP TABLE IF EXISTS `dingding_oa_invoice_application`;
CREATE TABLE `dingding_oa_invoice_application` (
`invoiceJobNumber` int NOT NULL AUTO_INCREMENT,
`dataId` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '数据id',
`applicationDate` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '申请日期',
`applicant` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '申请人',
`applicationDepartment` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '申请部门',
`agreementType` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '项目类型',
`xiang_mu_zu_bie` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '项目组别',
`yuJiBaoXiaoDiSanFangChengBenYuan` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '预计报销第三方成本(不含税,元)',
`businessStartTime` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '业务开始时间',
`businessEndTime` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '业务结束时间',
`contractNumber` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '销售合同编号',
`contractName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '销售合同名称',
`uploadedContractAttachment` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '上传合同及相关附件',
`uploadedSealedCostListDa` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '上传盖章费用清单(大)',
-- `uploadedSealedCostListDu` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '上传盖章费用清单(独)',
`projectNumber` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '项目编号',
`is_tax_inclusive` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '是否含税',
`invoice_tax_rate` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '开票税率',
`invoice_tax_category` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '发票税目',
`is_multi_project_merge` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '是否多项目合并',
`customerCompanyName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '客户单位名称',
`customerCompanyNameOther` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '客户单位名称(其他)',
`tong_yi_she_hui_xin_yong_dai_ma` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '统一社会信用代码',
`bankName` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '开户行名称',
`bankAccount` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '银行账号',
`business_content_du` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '业务内容(独)',
`invoice_amount_yuan_du` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '开票金额(元)独',
`invoice_amount_yuan_other` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '开票金额(元)其他',
`bu_han_shui_jin_e_yuan_du` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '不含税金额(元)独',
-- `bu_han_shui_jin_e_yuan_du_da_xie` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '不含税金额(元)独(大写)',
`han_shui_jin_e_yuan_du` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '含税金额(元)独',
-- `han_shui_jin_e_yuan_du_da_xie` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '含税金额(元)独(大写)',
`bu_han_shui_jin_e_yuan_other` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '不含税金额(元)其他',
-- `bu_han_shui_jin_e_yuan_other_da_xie` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '不含税金额(元)其他(大写)',
`han_shui_jin_e_yuan_other` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '含税金额(元)其他',
-- `han_shui_jin_e_yuan_other_da_xie` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '含税金额(元)其他(大写)',
`bu_han_shui_jin_e_yuan_duo` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '不含税金额(元)多',
-- `bu_han_shui_jin_e_yuan_duo_da_xie` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '不含税金额(元)多(大写)',
`han_shui_jin_e_yuan_duo` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '含税金额(元)多',
-- `han_shui_jin_e_yuan_duo_da_xie` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '含税金额(元)多(大写)',
`bu_han_shui_jin_e_yuan_da` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '不含税金额(元)大',
`han_shui_zong_jin_e_yuan_da` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '含税总金额(元)大',
`bu_han_shui_jin_e_yuan_da_du` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '不含税金额(元)(大+独)',
-- `bu_han_shui_jin_e_yuan_da_du_da_xie` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '不含税金额(元)(大+独)(大写)',
`han_shui_zong_jin_e_yuan_da_du` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '含税总金额(元)(大+独)',
-- `han_shui_zong_jin_e_yuan_da_du_da_xie` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '含税总金额(元)(大+独)(大写)',
`approvalNumber` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '审批编号',
`creationTime` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建时间',
`creator` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人',
`currentResponsiblePerson` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '当前负责人',
`approvalResult` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '审批结果',
`approvalStatus` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '审批状态',
`updateTime` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '更新时间',
`creatorDepartment` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人部门',
`approvalFormTitle` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '审批单标题',
`historicalApprovers` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '历史审批人',
`timeConsumed` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '耗时(时:分:秒)',
`approvalRecord` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '审批记录',
PRIMARY KEY (`invoiceJobNumber`, `dataId`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 74 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;

View File

@@ -0,0 +1,137 @@
-- 修复缺失的列
ALTER TABLE `dingding_oa_invoice_application`
ADD COLUMN IF NOT EXISTS `invoiceType` varchar(255) NULL COMMENT '发票类型',
ADD COLUMN IF NOT EXISTS `invoiceCode` tinyint NULL COMMENT '发票代码',
ADD COLUMN IF NOT EXISTS `kai_piao_shi_you` varchar(255) NULL COMMENT '开票事由',
ADD COLUMN IF NOT EXISTS `fa_piao_bei_zhu_lan_tian_xie_nei_rong` text NULL COMMENT '发票备注栏填写内容';
-- 补充:为明细表新增“预计报销第三方成本(不含税,元)”
ALTER TABLE `invoice_amount_details`
ADD COLUMN IF NOT EXISTS `yu_ji_bao_xiao_di_san_fang_cheng_ben_yuan` varchar(255) NULL COMMENT '预计报销第三方成本(不含税,元)';
-- 重新创建视图以解决权限问题
DROP VIEW IF EXISTS `invoice_application_match_view`;
CREATE VIEW `invoice_application_match_view` AS
SELECT
`da`.`dataId` AS `dataId`,
`da`.`invoiceJobNumber` AS `invoiceJobNumber`,
`da`.`agreementType` AS `agreementType`,
`da`.`approvalNumber` AS `approvalNumber`,
`da`.`contractNumber` AS `salesContractNumber`,
`da`.`yuJiBaoXiaoDiSanFangChengBenYuan` AS `yuJiBaoXiaoDiSanFangChengBenYuan`,
`da`.`is_multi_project_merge` AS `is_multi_project_merge`,
`da`.`projectNumber` AS `projectNumber`,
`da`.`customerCompanyName` AS `customerCompanyName`,
`da`.`customerCompanyNameOther` AS `customerCompanyNameOther`,
`id`.`piao_hao` AS `piao_hao`,
`da`.`bu_han_shui_jin_e_yuan_du` AS `bu_han_shui_jin_e_yuan_du`,
`da`.`han_shui_jin_e_yuan_du` AS `han_shui_jin_e_yuan_du`,
`da`.`bu_han_shui_jin_e_yuan_da` AS `bu_han_shui_jin_e_yuan_da`,
`da`.`han_shui_zong_jin_e_yuan_da` AS `han_shui_jin_e_yuan_da`,
`da`.`bu_han_shui_jin_e_yuan_da_du` AS `bu_han_shui_jin_e_yuan_da_du`,
`da`.`han_shui_zong_jin_e_yuan_da_du` AS `han_shui_jin_e_yuan_da_du`,
`da`.`bu_han_shui_jin_e_yuan_duo` AS `bu_han_shui_jin_e_yuan_duo`,
`da`.`han_shui_jin_e_yuan_duo` AS `han_shui_jin_e_yuan_duo`,
`da`.`bu_han_shui_jin_e_yuan_other` AS `bu_han_shui_jin_e_yuan_other`,
`da`.`han_shui_jin_e_yuan_other` AS `han_shui_jin_e_yuan_other`,
`da`.`applicant` AS `applicant`,
`da`.`business_content_du` AS `business_content_du`,
`da`.`uploadedContractAttachment` AS `uploadedContractAttachment`,
`da`.`uploadedSealedCostListDa` AS `uploadedSealedCostListDa`,
`da`.`invoiceType` AS `invoiceType`,
`da`.`invoiceCode` AS `invoiceCode`,
`da`.`kai_piao_shi_you` AS `kai_piao_shi_you`,
`da`.`fa_piao_bei_zhu_lan_tian_xie_nei_rong` AS `fa_piao_bei_zhu_lan_tian_xie_nei_rong`,
`id`.`invoice_type_enum` AS `invoice_type_enum`,
`id`.`kai_piao_ri_qi` AS `kai_piao_ri_qi`,
`id`.`hui_kuan_ri_qi` AS `hui_kuan_ri_qi`,
`id`.`hui_kuan_jin_e` AS `hui_kuan_jin_e`,
`id`.`zhang_qi` AS `zhang_qi`,
`cmi`.`customer_type` AS `unit_nature`
FROM
(
`dingding_oa_invoice_application` `da`
LEFT JOIN `invoice_data` `id` ON (
`da`.`dataId` = `id`.`dataid`
)
LEFT JOIN `customer_management_info` `cmi` ON (
`da`.`customerCompanyName` = `cmi`.`customer_unit_name`
))
SQL SECURITY INVOKER;
-- 同样修复内层视图
DROP VIEW IF EXISTS `invoice_application_match_inner_view`;
CREATE VIEW `invoice_application_match_inner_view` AS
SELECT
`da`.`dataId` AS `dataId`,
`da`.`invoiceJobNumber` AS `invoiceJobNumber`,
`da`.`agreementType` AS `agreementType`,
`da`.`approvalNumber` AS `approvalNumber`,
`da`.`contractNumber` AS `salesContractNumber`,
`da`.`applicationDepartment` AS `applicationDepartment`,
`da`.`yuJiBaoXiaoDiSanFangChengBenYuan` AS `yuJiBaoXiaoDiSanFangChengBenYuan`,
`da`.`is_multi_project_merge` AS `is_multi_project_merge`,
`da`.`projectNumber` AS `projectNumber`,
`da`.`customerCompanyName` AS `customerCompanyName`,
`da`.`customerCompanyNameOther` AS `customerCompanyNameOther`,
`id`.`piao_hao` AS `piao_hao`,
`da`.`bu_han_shui_jin_e_yuan_du` AS `bu_han_shui_jin_e_yuan_du`,
`da`.`han_shui_jin_e_yuan_du` AS `han_shui_jin_e_yuan_du`,
`da`.`bu_han_shui_jin_e_yuan_da` AS `bu_han_shui_jin_e_yuan_da`,
`da`.`han_shui_zong_jin_e_yuan_da` AS `han_shui_jin_e_yuan_da`,
`da`.`bu_han_shui_jin_e_yuan_da_du` AS `bu_han_shui_jin_e_yuan_da_du`,
`da`.`han_shui_jin_e_yuan_da_du` AS `han_shui_jin_e_yuan_da_du`,
`da`.`bu_han_shui_jin_e_yuan_duo` AS `bu_han_shui_jin_e_yuan_duo`,
`da`.`han_shui_jin_e_yuan_duo` AS `han_shui_jin_e_yuan_duo`,
`da`.`bu_han_shui_jin_e_yuan_other` AS `bu_han_shui_jin_e_yuan_other`,
`da`.`han_shui_jin_e_yuan_other` AS `han_shui_jin_e_yuan_other`,
`da`.`applicant` AS `applicant`,
`da`.`business_content_du` AS `business_content_du`,
`da`.`uploadedContractAttachment` AS `uploadedContractAttachment`,
`da`.`uploadedSealedCostListDa` AS `uploadedSealedCostListDa`,
`da`.`kai_piao_shi_you` AS `kai_piao_shi_you`,
`da`.`fa_piao_bei_zhu_lan_tian_xie_nei_rong` AS `fa_piao_bei_zhu_lan_tian_xie_nei_rong`,
`da`.`invoiceType` AS `invoiceType`,
`da`.`invoiceCode` AS `invoiceCode`,
`id`.`invoice_type_enum` AS `invoice_type_enum`,
`id`.`kai_piao_ri_qi` AS `kai_piao_ri_qi`,
`id`.`hui_kuan_ri_qi` AS `hui_kuan_ri_qi`,
`id`.`hui_kuan_jin_e` AS `hui_kuan_jin_e`,
`id`.`zhang_qi` AS `zhang_qi`,
`cmi`.`customer_type` AS `unit_nature`
FROM
(
(
`dingding_oa_invoice_application` `da`
LEFT JOIN `invoice_data` `id` ON((`da`.`dataId` = `id`.`dataid`))
)
LEFT JOIN `customer_management_info` `cmi` ON(
(
`da`.`customerCompanyName` = `cmi`.`customer_unit_name`
)
)
)
SQL SECURITY INVOKER;
-- 费用报销表新增字段(适配新版费用报销导入模板)
ALTER TABLE `expense_reimbursement`
ADD COLUMN IF NOT EXISTS `xuhao` varchar(255) NULL COMMENT '序号',
ADD COLUMN IF NOT EXISTS `date` varchar(255) NULL COMMENT '日期',
ADD COLUMN IF NOT EXISTS `voucher_number` varchar(255) NULL COMMENT '凭证号',
ADD COLUMN IF NOT EXISTS `entry_number` varchar(255) NULL COMMENT '分录号',
ADD COLUMN IF NOT EXISTS `summary` varchar(255) NULL COMMENT '摘要',
ADD COLUMN IF NOT EXISTS `account_code` varchar(255) NULL COMMENT '科目编码',
ADD COLUMN IF NOT EXISTS `account_name` varchar(255) NULL COMMENT '科目名称',
ADD COLUMN IF NOT EXISTS `auxiliary_items` varchar(1000) NULL COMMENT '辅助项',
ADD COLUMN IF NOT EXISTS `currency` varchar(255) NULL COMMENT '币种',
ADD COLUMN IF NOT EXISTS `debit_original_currency` varchar(255) NULL COMMENT '借方原币',
ADD COLUMN IF NOT EXISTS `debit_local_currency` varchar(255) NULL COMMENT '借方本币',
ADD COLUMN IF NOT EXISTS `credit_original_currency` varchar(255) NULL COMMENT '贷方原币',
ADD COLUMN IF NOT EXISTS `credit_local_currency` varchar(255) NULL COMMENT '贷方本币',
ADD COLUMN IF NOT EXISTS `shared_voucher_number` varchar(255) NULL COMMENT '共享凭证号',
ADD COLUMN IF NOT EXISTS `expense_budget_item` varchar(255) NULL COMMENT '费用预算项目',
ADD COLUMN IF NOT EXISTS `cost_center` varchar(255) NULL COMMENT '成本中心',
ADD COLUMN IF NOT EXISTS `standard_name` varchar(255) NULL COMMENT '标准名称',
ADD COLUMN IF NOT EXISTS `expense_type_level1` varchar(255) NULL COMMENT '一级费用类型',
ADD COLUMN IF NOT EXISTS `expense_type_level2` varchar(255) NULL COMMENT '二级费用类型',
ADD COLUMN IF NOT EXISTS `expense_type_level3` varchar(255) NULL COMMENT '三级费用类型';

View File

@@ -0,0 +1,159 @@
SELECT
`t`.`dataId` AS `dataId`,
`t`.`invoiceJobNumber` AS `invoiceJobNumber`,
`t`.`agreementType` AS `agreementType`,
`t`.`approvalNumber` AS `approvalNumber`,
`t`.`salesContractNumber` AS `salesContractNumber`,
`t`.`applicationDepartment` AS `applicationDepartment`,
`t`.`yuJiBaoXiaoDiSanFangChengBenYuan` AS `yuJiBaoXiaoDiSanFangChengBenYuan`,
`t`.`is_multi_project_merge` AS `is_multi_project_merge`,
`t`.`is_single_contract_multi_customer` AS `is_single_contract_multi_customer`,
`t`.`projectNumber` AS `projectNumber`,
`t`.`customerCompanyName` AS `customerCompanyName`,
`t`.`customerCompanyNameOther` AS `customerCompanyNameOther`,
`t`.`piao_hao` AS `piao_hao`,
`t`.`bu_han_shui_jin_e_yuan_du` AS `bu_han_shui_jin_e_yuan_du`,
`t`.`han_shui_jin_e_yuan_du` AS `han_shui_jin_e_yuan_du`,
`t`.`bu_han_shui_jin_e_yuan_da` AS `bu_han_shui_jin_e_yuan_da`,
`t`.`han_shui_jin_e_yuan_da` AS `han_shui_jin_e_yuan_da`,
`t`.`bu_han_shui_jin_e_yuan_da_du` AS `bu_han_shui_jin_e_yuan_da_du`,
`t`.`han_shui_jin_e_yuan_da_du` AS `han_shui_jin_e_yuan_da_du`,
`t`.`bu_han_shui_jin_e_yuan_duo` AS `bu_han_shui_jin_e_yuan_duo`,
`t`.`han_shui_jin_e_yuan_duo` AS `han_shui_jin_e_yuan_duo`,
`t`.`bu_han_shui_jin_e_yuan_other` AS `bu_han_shui_jin_e_yuan_other`,
`t`.`han_shui_jin_e_yuan_other` AS `han_shui_jin_e_yuan_other`,
`t`.`applicant` AS `applicant`,
`t`.`business_content_du` AS `business_content_du`,
`t`.`uploadedContractAttachment` AS `uploadedContractAttachment`,
`t`.`uploadedSealedCostListDa` AS `uploadedSealedCostListDa`,
`t`.`uploadedSealedCostListDu` AS `uploadedSealedCostListDu`,
`t`.`invoiceType` AS `invoiceType`,
`t`.`invoiceCode` AS `invoiceCode`,
`t`.`kai_piao_shi_you` AS `kai_piao_shi_you`,
`t`.`fa_piao_bei_zhu_lan_tian_xie_nei_rong` AS `fa_piao_bei_zhu_lan_tian_xie_nei_rong`,
`t`.`tong_yi_she_hui_xin_yong_dai_ma` AS `tong_yi_she_hui_xin_yong_dai_ma`,
`t`.`bankName` AS `bankName`,
`t`.`bankAccount` AS `bankAccount`,
`t`.`invoice_type_enum` AS `invoice_type_enum`,
`t`.`kai_piao_ri_qi` AS `kai_piao_ri_qi`,
`t`.`hui_kuan_ri_qi` AS `hui_kuan_ri_qi`,
`t`.`hui_kuan_jin_e` AS `hui_kuan_jin_e`,
`t`.`zhang_qi` AS `zhang_qi`,
`t`.`unit_nature` AS `unit_nature`,
`t`.`hong_chong_piao_hao` AS `hong_chong_piao_hao`
FROM
(
SELECT
`da`.`dataId` AS `dataId`,
`da`.`invoiceJobNumber` AS `invoiceJobNumber`,
`da`.`agreementType` AS `agreementType`,
`da`.`approvalNumber` AS `approvalNumber`,
`da`.`contractNumber` AS `salesContractNumber`,
`da`.`applicationDepartment` AS `applicationDepartment`,
`da`.`yuJiBaoXiaoDiSanFangChengBenYuan` AS `yuJiBaoXiaoDiSanFangChengBenYuan`,
`da`.`is_multi_project_merge` AS `is_multi_project_merge`,
`da`.`is_single_contract_multi_customer` AS `is_single_contract_multi_customer`,
`da`.`projectNumber` AS `projectNumber`,
`da`.`customerCompanyName` AS `customerCompanyName`,
`da`.`customerCompanyNameOther` AS `customerCompanyNameOther`,
`id`.`piao_hao` AS `piao_hao`,
`da`.`bu_han_shui_jin_e_yuan_du` AS `bu_han_shui_jin_e_yuan_du`,
`da`.`han_shui_jin_e_yuan_du` AS `han_shui_jin_e_yuan_du`,
`da`.`bu_han_shui_jin_e_yuan_da` AS `bu_han_shui_jin_e_yuan_da`,
`da`.`han_shui_zong_jin_e_yuan_da` AS `han_shui_jin_e_yuan_da`,
`da`.`bu_han_shui_jin_e_yuan_da_du` AS `bu_han_shui_jin_e_yuan_da_du`,
`da`.`han_shui_zong_jin_e_yuan_da_du` AS `han_shui_jin_e_yuan_da_du`,
`da`.`bu_han_shui_jin_e_yuan_duo` AS `bu_han_shui_jin_e_yuan_duo`,
`da`.`han_shui_jin_e_yuan_duo` AS `han_shui_jin_e_yuan_duo`,
`da`.`bu_han_shui_jin_e_yuan_other` AS `bu_han_shui_jin_e_yuan_other`,
`da`.`han_shui_jin_e_yuan_other` AS `han_shui_jin_e_yuan_other`,
`da`.`applicant` AS `applicant`,
`da`.`business_content_du` AS `business_content_du`,
`da`.`uploadedContractAttachment` AS `uploadedContractAttachment`,
`da`.`uploadedSealedCostListDa` AS `uploadedSealedCostListDa`,
`da`.`uploadedSealedCostListDu` AS `uploadedSealedCostListDu`,
`da`.`invoiceType` AS `invoiceType`,
`da`.`invoiceCode` AS `invoiceCode`,
`da`.`kai_piao_shi_you` AS `kai_piao_shi_you`,
`da`.`fa_piao_bei_zhu_lan_tian_xie_nei_rong` AS `fa_piao_bei_zhu_lan_tian_xie_nei_rong`,
`da`.`tong_yi_she_hui_xin_yong_dai_ma` AS `tong_yi_she_hui_xin_yong_dai_ma`,
`da`.`bankName` AS `bankName`,
`da`.`bankAccount` AS `bankAccount`,
`id`.`invoice_type_enum` AS `invoice_type_enum`,
`id`.`kai_piao_ri_qi` AS `kai_piao_ri_qi`,
`id`.`hui_kuan_ri_qi` AS `hui_kuan_ri_qi`,
`id`.`hui_kuan_jin_e` AS `hui_kuan_jin_e`,
`id`.`zhang_qi` AS `zhang_qi`,
`cmi`.`customer_type` AS `unit_nature`,
`da`.`hong_chong_piao_hao` AS `hong_chong_piao_hao`,
ROW_NUMBER() OVER (
PARTITION BY CASE
WHEN `da`.`is_single_contract_multi_customer` = '' THEN CONCAT(`da`.`dataId`, '#', `da`.`invoiceJobNumber`)
ELSE IFNULL(
`id`.`piao_hao`,
CONCAT(`da`.`dataId`, '#', `da`.`invoiceJobNumber`)
)
END
ORDER BY
`da`.`invoiceJobNumber` DESC
) AS `rn`
FROM
(
(
`dingding_oa_invoice_application` `da`
LEFT JOIN (
SELECT
`dataid`,
`customer_company_name`,
`piao_hao`,
MAX(`invoice_type_enum`) AS `invoice_type_enum`,
MAX(`kai_piao_ri_qi`) AS `kai_piao_ri_qi`,
MAX(`hui_kuan_ri_qi`) AS `hui_kuan_ri_qi`,
MAX(`hui_kuan_jin_e`) AS `hui_kuan_jin_e`,
MAX(`zhang_qi`) AS `zhang_qi`
FROM
`invoice_data`
GROUP BY
`dataid`,
`customer_company_name`,
`piao_hao`
) `id` ON(
(
(`da`.`dataId` = `id`.`dataid`)
AND (
`da`.`customerCompanyName` = `id`.`customer_company_name`
)
)
)
)
LEFT JOIN (
SELECT
`cmi1`.`customer_unit_name`,
MAX(`cmi1`.`customer_type`) AS `customer_type`
FROM
`customer_management_info` `cmi1`
INNER JOIN (
SELECT
`customer_unit_name`,
MAX(`updated_at`) AS `updated_at`
FROM
`customer_management_info`
GROUP BY
`customer_unit_name`
) `cmi_latest` ON(
(
(`cmi1`.`customer_unit_name` = `cmi_latest`.`customer_unit_name`)
AND (`cmi1`.`updated_at` = `cmi_latest`.`updated_at`)
)
)
GROUP BY
`cmi1`.`customer_unit_name`
) `cmi` ON(
(
`da`.`customerCompanyName` = `cmi`.`customer_unit_name`
)
)
)
) `t`
WHERE
`t`.`rn` = 1

37
src/db/invoice_data.sql Normal file
View File

@@ -0,0 +1,37 @@
/*
Navicat Premium Data Transfer
Source Server : 本地数据库
Source Server Type : MySQL
Source Server Version : 80042 (8.0.42)
Source Host : localhost:3306
Source Schema : procurement_db
Target Server Type : MySQL
Target Server Version : 80042 (8.0.42)
File Encoding : 65001
Date: 20/10/2025 14:15:27
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for invoice_data
-- ----------------------------
DROP TABLE IF EXISTS `invoice_data`;
CREATE TABLE `invoice_data` (
`dataid` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`piao_hao` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '票号',
`kai_piao_ri_qi` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '开票日期',
`hui_kuan_ri_qi` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '回款日期',
`hui_kuan_jin_e` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '回款金额',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`invoice_type_enum` int NULL DEFAULT 0 COMMENT '0 普通票\r\n1 作废-当月\r\n2 作废红冲\r\n',
`zhang_qi` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '账期,根据回款日期减去开票日期而得',
PRIMARY KEY (`dataid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '开票数据表' ROW_FORMAT = DYNAMIC;
SET FOREIGN_KEY_CHECKS = 1;

13
src/lib/prisma.ts Normal file
View File

@@ -0,0 +1,13 @@
import { PrismaClient } from '@prisma/client'
// 简化配置,直接实例化 PrismaClient
const globalForPrisma = globalThis as unknown as { prismaClient?: PrismaClient }
export const prismaClient = new PrismaClient({
datasources: {
db: {
url: process.env.DATABASE_URL
}
}
})
if (process.env.NODE_ENV !== 'production') globalForPrisma.prismaClient = prismaClient
export default prismaClient

215
src/lib/util.ts Normal file
View File

@@ -0,0 +1,215 @@
import { AGREEMENT_TYPES, AGREEMENT_TYPE_CODE, AGREEMENT_TYPE_NAME, AGREEMENT_CODE_TO_NAME, AGREEMENT_CODE_TO_TYPE, XIANGMU_ZUBIE_MAP } from '@/CONSTANTS';
import { InvoiceDataItem, ProcessedInvoiceDataItemFromFunction } from '@/pages/api/type';
import { getBuHanShuiAmount, getHanShuiAmount, getAllAmount } from '@/pages/api/approval/util';
import dayjs from 'dayjs';
import fs from 'fs';
import path from 'path';
import get from 'lodash/get';
import DownLoadClient from '@/api/dingding/download';
import { logger } from '@/api/winston/logger';
/**
* 处理发票数据,添加客户公司名称、账期和协议类型名称
* @param item 原始发票数据项
* @returns 处理后的发票数据项
*/
export function processInvoiceDataItem(item: any): any {
// 添加 customerCompanyName 字段
let customerCompanyName = item.customerCompanyName || "";
// 根据协议类型选择不含税金额字段
let bu_han_shui_jin_e: string = getBuHanShuiAmount(item);
// 根据协议类型选择含税金额字段
let han_shui_zong_jin_e = getHanShuiAmount(item);
// 计算税额(含税金额 - 不含税金额)
const shui_e = getAllAmount(bu_han_shui_jin_e, han_shui_zong_jin_e);
// 根据新的协议类型显示逻辑
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_TYPE_NAME] || '';
}
// 业务内容统一取开票事由
const businessContent = item.kai_piao_shi_you || '';
// 专业组映射逻辑 - 基于项目编号的字母前缀
const getProjectGroupByProjectId = (projectId: string | null | undefined): string => {
if (!projectId) {
return '客户服务室';
}
const match = projectId.match(/[A-Za-z]+/);
const prefix = match ? match[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();
}
}
// 返回包含新字段的对象
// const zhuan_ye_zu = XIANGMU_ZUBIE_MAP[item.applicationDepartment || ''] || '项目组别匹配失败';
// 发票类型:从开票申请中获取
const fa_piao_lei_xing = item.invoiceType || '';
return {
invoiceJobNumber: item.invoiceJobNumber || '',
dataId: item.dataId,
approvalNumber: item.approvalNumber || null,
// 去掉 unit_nature集团内外
projectNumber: item.projectNumber,
agreementType: item.agreementType,
agreementTypeName: agreementTypeName,
invoiceType: item.invoiceType || null,
customerCompanyName: customerCompanyName,
piao_hao: item.piao_hao,
bu_han_shui_jin_e: bu_han_shui_jin_e,
shui_e: shui_e,
han_shui_zong_jin_e: han_shui_zong_jin_e,
applicant: item.applicant,
zhuan_ye_zu,
kai_piao_ri_qi: item.kai_piao_ri_qi,
hui_kuan_ri_qi: item.hui_kuan_ri_qi,
hui_kuan_jin_e: item.hui_kuan_jin_e,
business_content: businessContent,
zhang_qi: zhang_qi, // 使用计算后的账期值
invoice_type_enum: item.invoice_type_enum,
// 透传新增字段
tong_yi_she_hui_xin_yong_dai_ma: item.tong_yi_she_hui_xin_yong_dai_ma || null,
bankName: item.bankName || null,
bankAccount: item.bankAccount || null,
};
}
/**
* 下载文件并保存到指定路径
* @param downloadUri 文件下载URL
* @param attachmentFileName 保存的文件名
* @param attachmentFilePath 保存的文件路径
* @returns Promise<boolean> 下载是否成功
*/
export async function downloadAndSaveFile(
downloadUri: string,
attachmentFilePath: string,
attachmentFileName: string
): Promise<boolean> {
try {
const response = await fetch(downloadUri);
if (response.ok) {
const arrayBuffer = await response.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
const fileFullPath = path.join(attachmentFilePath, attachmentFileName);
fs.writeFileSync(fileFullPath, buffer);
return true;
} else {
console.error(`Download failed: ${response.status} ${response.statusText}`);
throw new Error(`下载失败: ${response.status} ${response.statusText}`);
}
} catch (error) {
console.error(`Error downloading file from ${downloadUri}:`, error);
throw new Error(`文件下载失败: ${downloadUri} - ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* 下载附件功能 - 处理JSON格式的附件数据并下载保存
* @param attachmentJsonString 附件数据的JSON字符串
* @param folderPath 保存文件夹路径
* @param subFolderName 子文件夹名称
* @param dataId 数据ID
* @param accessToken 访问令牌
* @param fileNamePrefix 文件名前缀
* @returns Promise<void>
*/
export async function downloadAttachments(
attachmentJsonString: string,
folderPath: string,
subFolderName: string,
dataId: string,
accessToken: string,
fileNamePrefix: string = ''
): Promise<void> {
try {
console.log("attachmentJsonString",attachmentJsonString)
const attachmentData = JSON.parse(attachmentJsonString);
// 检查是否为数组
const attachments = Array.isArray(attachmentData) ? attachmentData : [attachmentData];
// 创建子文件夹
const subFolderPath = path.join(folderPath, subFolderName);
if (!fs.existsSync(subFolderPath)) {
fs.mkdirSync(subFolderPath, { recursive: true });
}
// 遍历所有附件
for (const attachment of attachments) {
if (attachment && attachment.fileId) {
// 调用下载接口获取文件URL
const fileUrl = await DownLoadClient.getFileURL({
processInstanceId: dataId,
fileId: attachment.fileId,
xAcsDingtalkAccessToken: accessToken
});
const downloadUri = get(fileUrl, 'body.result.downloadUri', '');
if (downloadUri) {
const fileName = `${fileNamePrefix}${attachment.fileName || attachment.fileId || 'file'}`;
await downloadAndSaveFile(downloadUri, subFolderPath, fileName);
}
}
}
} catch (error) {
console.error(error)
console.log(`${subFolderName} for ${dataId} processing failed:`, error instanceof Error ? error.message : String(error));
logger.error(`${subFolderName} for ${dataId} processing failed:`, error instanceof Error ? error.message : String(error));
throw new Error(`${subFolderName} for ${dataId} processing failed: ${error instanceof Error ? error.message : String(error)}`);
}
}

View File

@@ -0,0 +1,73 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { withFileUpload } from 'next-multiparty';
import * as XLSX from 'xlsx';
import { InvoiceApplicationItem } from '../type'
import prismaClient from '@/lib/prisma'
const ExcelToMySQLFieldMapping = {
'数据id': 'data_id',
'项目编号': 'project_number',
'支付日期': 'payment_date',
'年份': 'payment_year',
'凭证号': 'pingzhenghao',
'行项目文本': 'hangxiangmu_wenben',
'业务线': 'business_line',
'期末余额(元)(大写)': 'qimo_yueyuan_daxie',
'支付方式': 'payment_method',
'供应商名称': 'gongyingshang_mingcheng',
'创建时间': 'created_time',
'创建者': 'creator',
'创建人部门': 'creator_department',
'审批单标题': 'approval_title',
'更新时间': 'updated_time'
}
export const config = {
api: {
bodyParser: false // 必须禁用默认bodyParser
}
};
export default withFileUpload(async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
try {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
// 2. 读取Excel文件
const fileBuffer = await req?.file?.toBuffer()
const workbook = XLSX.read(fileBuffer, { type: 'buffer' });
const firstSheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[firstSheetName];
const rawData: InvoiceApplicationItem[] = XLSX.utils.sheet_to_json(worksheet);
// 3. 数据处理和去重
let actualPaymentDataList = [];
// TODO 可以查询一次搞重新修改 看业务需求
for (let i = 0; i < rawData.length; i++) {
const rawItem = rawData[i];
const actualPaymentData: any = {
data_id: rawItem['数据id'],
};
for (const [excelKey, dbKey] of Object.entries(ExcelToMySQLFieldMapping)) {
actualPaymentData[dbKey] = rawItem[excelKey];
}
actualPaymentDataList.push(actualPaymentData);
}
// 插入数据到actual_payment表
await prismaClient.actual_payment.createMany({
data: actualPaymentDataList,
skipDuplicates: true, // 跳过重复数据
});
res.status(200).json({
code: 0,
data: null,
message: '实际支付数据导入成功'
});
} catch (error) {
res.status(500).json({ error: 'Internal server error', stack: error, });
}
})

View File

@@ -0,0 +1,343 @@
import type { NextApiRequest, NextApiResponse } from 'next'
import prismaClient from '@/lib/prisma'
import XLSX from 'xlsx'
import dayjs from 'dayjs'
import XLSXStyle from 'xlsx-style';
import { getHanShuiAmount, getBuHanShuiAmount, getAllAmount } from '@/pages/api/approval/util'
import { processInvoiceDataItem } from '@/lib/util'
import { AGREEMENT_CODE_TO_NAME, AGREEMENT_TYPE_CODE, AGREEMENT_TYPES, XIANGMU_ZUBIE_MAP } from '@/CONSTANTS'
// 定义列宽配置
// 列宽:移除“集团内外”
const COLUMN_WIDTHS = {
"数据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
) {
try {
res.setTimeout(300000);
// Only allow POST requests
if (req.method !== 'POST') {
return res.status(405).json({ message: 'Method not allowed' })
}
const { ids } = req.body
console.log('Received IDs:', ids);
let invoiceData: Array<{ [k: string]: any }>
if (Array.isArray(ids) && ids.length > 0) {
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);
invoiceData = await prismaClient.invoice_application_match_view.findMany({
where: { dataId: { in: ids } },
orderBy: { invoiceJobNumber: 'asc' },
select: {
dataId: true,
invoiceJobNumber: true,
projectNumber: true,
customerCompanyName: true,
piao_hao: true,
kai_piao_ri_qi: true,
zhang_qi: true,
hui_kuan_ri_qi: true,
hui_kuan_jin_e: true,
applicant: true,
invoiceType: true,
tong_yi_she_hui_xin_yong_dai_ma: true,
bankName: true,
bankAccount: true,
kai_piao_shi_you: true,
agreementType: true,
is_multi_project_merge: true,
bu_han_shui_jin_e_yuan_da: true,
bu_han_shui_jin_e_yuan_du: true,
bu_han_shui_jin_e_yuan_duo: true,
bu_han_shui_jin_e_yuan_other: true,
han_shui_jin_e_yuan_da: true,
han_shui_jin_e_yuan_du: true,
han_shui_jin_e_yuan_duo: true,
han_shui_jin_e_yuan_other: true,
}
});
} else {
// 未传 ids 时导出全部(排除作废)
const voidRecords = await prismaClient.dingding_oa_invoice_application.findMany({
where: { remark: '发票作废' },
select: { dataId: true, invoiceJobNumber: true }
});
const voidPairs = voidRecords
.filter(r => r.dataId && r.invoiceJobNumber)
.map(r => ({ dataId: r.dataId as string, invoiceJobNumber: r.invoiceJobNumber as number }));
const whereForAll: any = {};
if (voidPairs.length > 0) {
whereForAll.AND = [
{
NOT: {
OR: voidPairs.map(pair => ({
dataId: pair.dataId,
invoiceJobNumber: pair.invoiceJobNumber
}))
}
}
];
}
console.log('Fetching all non-void data');
invoiceData = await prismaClient.invoice_application_match_view.findMany({
where: whereForAll,
orderBy: { invoiceJobNumber: 'asc' },
select: {
dataId: true,
invoiceJobNumber: true,
projectNumber: true,
customerCompanyName: true,
piao_hao: true,
kai_piao_ri_qi: true,
zhang_qi: true,
hui_kuan_ri_qi: true,
hui_kuan_jin_e: true,
applicant: true,
invoiceType: true,
tong_yi_she_hui_xin_yong_dai_ma: true,
bankName: true,
bankAccount: true,
kai_piao_shi_you: true,
agreementType: true,
is_multi_project_merge: true,
bu_han_shui_jin_e_yuan_da: true,
bu_han_shui_jin_e_yuan_du: true,
bu_han_shui_jin_e_yuan_duo: true,
bu_han_shui_jin_e_yuan_other: true,
han_shui_jin_e_yuan_da: true,
han_shui_jin_e_yuan_du: true,
han_shui_jin_e_yuan_duo: true,
han_shui_jin_e_yuan_other: true,
}
});
}
// 提取大协议的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('Fetched data count:', 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 || '';
// 专业组逻辑:提取项目编号的所有字母部分映射,如果没有项目编号则为客户服务室
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 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_TYPE_NAME] || '';
}
// 删除这行const zhuan_ye_zu = XIANGMU_ZUBIE_MAP[item.applicationDepartment || ''] || '项目组别匹配失败';
// 集团内外和专业组
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 || '',
"账期": item.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 // 添加协议类型字段
};
});
// Convert data to worksheet
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 })
// Set headers for file download
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
res.setHeader('Content-Disposition', 'attachment; filename=invoice-todolist.xlsx')
// Send the Excel file
res.write(buffer)
res.end()
} catch (error) {
console.error('Error exporting invoice data:', error)
res.status(500).json({ message: 'Internal server error' })
}
}

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 })
}
}

View File

@@ -0,0 +1,135 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { withFileUpload } from 'next-multiparty';
import * as XLSX from 'xlsx';
import { InvoiceApplicationItem, InvoiceApplicationDBItem, InvoiceApplicationAmountDetails } from '../type'
import prismaClient from '@/lib/prisma'
import { AGREEMENT_TYPE_CODE, AGREEMENT_TYPE_NAME } from '@/CONSTANTS';
import { ExcelToMySQLFieldMapping } from './util'
export const config = {
api: {
bodyParser: false // 必须禁用默认bodyParser
}
};
export default withFileUpload(async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
try {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
// 2. 读取Excel文件
const fileBuffer = await req?.file?.toBuffer()
const workbook = XLSX.read(fileBuffer, { type: 'buffer' });
const firstSheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[firstSheetName];
const rawData: InvoiceApplicationItem[] = XLSX.utils.sheet_to_json(worksheet);
// 3. 数据处理和去重
// 3.1 从数据库获取已存在的dataId
const existingRecords = await prismaClient.dingding_oa_invoice_application.findMany({
select: {
dataId: true
}
});
const existingDataIds = new Set(existingRecords.map(record => record.dataId));
let dataKV: {
[key: string]: InvoiceApplicationDBItem;
} = {};
const InvoiceAmountList = []
for (let i = 1; i < rawData.length; i++) {
const rawItem = rawData[i];
const key = rawItem['数据id'];
// 3.2 跳过数据库中已存在的数据
if (existingDataIds.has(key)) {
continue;
}
const agreementTypeCode = AGREEMENT_TYPE_CODE[AGREEMENT_TYPE_NAME[rawItem['开票类型']]]
if (!agreementTypeCode) {
throw new Error(`${i}行开票日期不识别`)
}
const bigInvoiceAmount: InvoiceApplicationAmountDetails = {
dataid: rawItem['数据id'],
agreementType: agreementTypeCode,
project_number: '',
business_group_name: rawItem['开票金额明细'],
project_name: rawItem['开票金额明细_1'],
business_content: rawItem['开票金额明细_2'],
invoice_amount: rawItem['开票金额明细_3'],
}
const multiProjectMergeInvoice: InvoiceApplicationAmountDetails = {
dataid: rawItem['数据id'],
agreementType: agreementTypeCode,
project_number: rawItem['多项目合并开票请添加'],
project_name: rawItem['多项目合并开票请添加_1'],
business_group_name: rawItem['多项目合并开票请添加_2'],
business_content: '',
invoice_amount: rawItem['多项目合并开票请添加_3'],
}
if (!dataKV[key]) {
dataKV[key] = {
agreementType: agreementTypeCode,
BigInvoiceAmountList: [], // InvoiceApplicationDBItem 特有字段
multiProjectMergeInvoice: [],
};
for (const [excelKey, dbKey] of Object.entries(ExcelToMySQLFieldMapping)) {
dataKV[key][dbKey] = rawItem[excelKey];
}
if (bigInvoiceAmount.business_group_name && bigInvoiceAmount.invoice_amount && bigInvoiceAmount.project_name) {
dataKV[key].BigInvoiceAmountList.push(bigInvoiceAmount)
InvoiceAmountList.push(bigInvoiceAmount)
}
if (multiProjectMergeInvoice.project_number && multiProjectMergeInvoice.invoice_amount && multiProjectMergeInvoice.project_name) {
dataKV[key].multiProjectMergeInvoice.push(multiProjectMergeInvoice)
InvoiceAmountList.push(multiProjectMergeInvoice)
}
} else {
if (bigInvoiceAmount.business_group_name && bigInvoiceAmount.invoice_amount && bigInvoiceAmount.project_name) {
dataKV[key].BigInvoiceAmountList.push(bigInvoiceAmount)
InvoiceAmountList.push(bigInvoiceAmount)
}
if (multiProjectMergeInvoice.project_number && multiProjectMergeInvoice.invoice_amount && multiProjectMergeInvoice.project_name) {
dataKV[key].multiProjectMergeInvoice.push(multiProjectMergeInvoice)
InvoiceAmountList.push(multiProjectMergeInvoice)
}
}
}
await prismaClient.$transaction([
prismaClient.invoice_amount_details.createMany({
data: InvoiceAmountList,
skipDuplicates: false,
}),
prismaClient.dingding_oa_invoice_application.createMany({
data: Object.values(dataKV).map(item => {
const mergedProjectNumbers = Array.from(new Set((item.multiProjectMergeInvoice || []).map(d => d.project_number).filter(Boolean)));
if (mergedProjectNumbers.length > 1) {
item.projectNumber = mergedProjectNumbers.join(',');
item.is_multi_project_merge = '是';
} else if (mergedProjectNumbers.length === 1) {
item.projectNumber = mergedProjectNumbers[0];
}
const { BigInvoiceAmountList, multiProjectMergeInvoice, ...rest } = item;
return rest;
}),
skipDuplicates: false,
})
]);
res.status(200).json({
code: 0,
data: null,
message: `钉钉开票申请数据导入成功,共导入${Object.values(dataKV).length}条数据,${InvoiceAmountList.length}条开票金额明细数据`,
});
} catch (error) {
res.status(500).json({ error: 'Internal server error', stack: error, });
}
})

View File

@@ -0,0 +1,156 @@
// 在文件顶部,导入语句之后添加
import { AGREEMENT_CODE_TO_TYPE, AGREEMENT_TYPE_NAME, AGREEMENT_TYPES } from '@/CONSTANTS'
import BigNumber from 'bignumber.js';
export const ExcelToMySQLFieldMapping: {
[k: string]: string
} = {
'数据id': 'dataId',
'申请日期': 'applicationDate',
'申请人': 'applicant',
'申请部门': 'applicationDepartment',
'项目组别': 'xiang_mu_zu_bie',
'预计报销第三方成本(不含税,元)': 'yuJiBaoXiaoDiSanFangChengBenYuan',
'业务开始时间': 'businessStartTime',
'业务结束时间': 'businessEndTime',
'销售合同编号': 'contractNumber',
'销售合同名称': 'contractName',
'合同原件扫描件(盖章版)': 'uploadedContractAttachment',
'项目编号': 'projectNumber',
'是否含税': 'is_tax_inclusive',
'开票税率': 'invoice_tax_rate',
'是否多项目合并': 'is_multi_project_merge',
'上传盖章费用清单': 'uploadedSealedCostList',
'客户单位名称': 'customerCompanyName',
'统一社会信用代码': 'tong_yi_she_hui_xin_yong_dai_ma',
'开户行名称': 'bankName',
'银行账号': 'bankAccount',
'开票税目': 'invoice_tax_category',
'业务内容(备注)': 'business_content_du',
'开票金额(元)独': 'invoice_amount_yuan_du',
'不含税金额(元)独': 'bu_han_shui_jin_e_yuan_du',
'不含税金额(元)独(大写)': 'bu_han_shui_jin_e_yuan_du_da_xie',
'含税金额(元)独': 'han_shui_jin_e_yuan_du',
'含税金额(元)独(大写)': 'han_shui_jin_e_yuan_du_da_xie',
'不含税金额(元)多': 'bu_han_shui_jin_e_yuan_duo',
'不含税金额(元)多(大写)': 'bu_han_shui_jin_e_yuan_duo_da_xie',
'含税金额(元)多': 'han_shui_jin_e_yuan_duo',
'含税金额(元)多(大写)': 'han_shui_jin_e_yuan_duo_da_xie',
'不含税金额(元)大': 'bu_han_shui_jin_e_yuan_da',
'含税总金额(元)大': 'han_shui_zong_jin_e_yuan_da',
'不含税金额(元)(大+独)': 'bu_han_shui_jin_e_yuan_da_du',
'不含税金额(元)(大+独)(大写)': 'bu_han_shui_jin_e_yuan_da_du_da_xie',
'含税总金额(元)(大+独)': 'han_shui_zong_jin_e_yuan_da_du',
'含税总金额(元)(大+独)(大写)': 'han_shui_zong_jin_e_yuan_da_du_da_xie',
'审批编号': 'approvalNumber',
'创建时间': 'creationTime',
'创建人': 'creator',
'当前负责人': 'currentResponsiblePerson',
'审批结果': 'approvalResult',
'审批状态': 'approvalStatus',
'更新时间': 'updateTime',
'创建人部门': 'creatorDepartment',
'审批单标题': 'approvalFormTitle',
'历史审批人': 'historicalApprovers',
'耗时(时:分:秒)': 'timeConsumed',
'审批记录': 'approvalRecord',
};
export function getBuHanShuiAmount(item: any) {
const normalize = (val: any): string => {
const s = String(val ?? '').trim()
if (!s) return ''
const neg = /^\(.+\)$/.test(s)
let cleaned = s.replace(/[,\s]/g, '')
cleaned = cleaned.replace(/[¥¥元]/g, '')
cleaned = cleaned.replace(/^[\(]|[\)]$/g, '')
const num = new BigNumber(cleaned)
if (!num.isFinite()) return ''
return neg ? num.negated().toString() : num.toString()
}
const raw = item?.agreementType
const code = raw == null ? '' : String(raw)
const agreementType = AGREEMENT_CODE_TO_TYPE[code] || AGREEMENT_TYPE_NAME[code] || code
const isSingleContractMultiCustomer = ['是', '1', 'true', 'TRUE', 'yes', 'YES'].includes(String(item?.is_single_contract_multi_customer ?? '').trim())
if (isSingleContractMultiCustomer) {
const singleAmount = normalize(item.bu_han_shui_jin_e_yuan_du)
if (singleAmount !== '') {
return singleAmount;
}
}
// 这个逻辑一定在前否则先过独立协议了,这个逻辑永远走不到
if (agreementType === AGREEMENT_TYPES.INDEPENDENT && item.is_multi_project_merge === '是') {
return normalize(item.bu_han_shui_jin_e_yuan_duo);
}
if (agreementType === AGREEMENT_TYPES.LARGE) {
return normalize(item.bu_han_shui_jin_e_yuan_da);
}
if (agreementType === AGREEMENT_TYPES.INDEPENDENT) {
return normalize(item.bu_han_shui_jin_e_yuan_du);
}
if (agreementType === AGREEMENT_TYPES.LARGE_INDEPENDENT) {
return normalize(item.bu_han_shui_jin_e_yuan_da_du);
}
if (agreementType === AGREEMENT_TYPES.OTHER) {
return normalize(item.bu_han_shui_jin_e_yuan_other);
}
return ''
}
export function getHanShuiAmount(item: any) {
const normalize = (val: any): string => {
const s = String(val ?? '').trim()
if (!s) return ''
const neg = /^\(.+\)$/.test(s)
let cleaned = s.replace(/[,\s]/g, '')
cleaned = cleaned.replace(/[¥¥元]/g, '')
cleaned = cleaned.replace(/^[\(]|[\)]$/g, '')
const num = new BigNumber(cleaned)
if (!num.isFinite()) return ''
return neg ? num.negated().toString() : num.toString()
}
const raw = item?.agreementType
const code = raw == null ? '' : String(raw)
const agreementType = AGREEMENT_CODE_TO_TYPE[code] || AGREEMENT_TYPE_NAME[code] || code
const isSingleContractMultiCustomer = ['是', '1', 'true', 'TRUE', 'yes', 'YES'].includes(String(item?.is_single_contract_multi_customer ?? '').trim())
if (isSingleContractMultiCustomer) {
const singleAmount = normalize(item.han_shui_jin_e_yuan_du)
if (singleAmount !== '') {
return singleAmount;
}
}
// 这个逻辑一定在前否则先过独立协议了,这个逻辑永远走不到
if (agreementType === AGREEMENT_TYPES.INDEPENDENT && item.is_multi_project_merge === '是') {
return normalize(item.han_shui_jin_e_yuan_duo);
}
if (agreementType === AGREEMENT_TYPES.LARGE) {
return normalize(item.han_shui_jin_e_yuan_da);
}
if (agreementType === AGREEMENT_TYPES.INDEPENDENT) {
return normalize(item.han_shui_jin_e_yuan_du);
}
if (agreementType === AGREEMENT_TYPES.LARGE_INDEPENDENT) {
return normalize(item.han_shui_jin_e_yuan_da_du);
}
if (agreementType === AGREEMENT_TYPES.OTHER) {
return normalize(item.han_shui_jin_e_yuan_other);
}
return ''
}
export function getAllAmount(buHanShuiAmount: string, hanShuiAmount: string): string {
// 使用bignumber.js处理金额计算避免浮点数精度问题
const buHanShuiNum = new BigNumber(buHanShuiAmount || '0');
const hanShuiNum = new BigNumber(hanShuiAmount || '0');
// 检查输入是否为有效数字
if (!buHanShuiNum.isFinite() || !hanShuiNum.isFinite()) {
return '';
}
// 执行减法运算并返回字符串结果(含税金额 - 不含税金额)
return hanShuiNum.minus(buHanShuiNum).toString();
}

View File

@@ -0,0 +1,100 @@
import type { NextApiRequest, NextApiResponse } from 'next'
import prismaClient from '@/lib/prisma'
import XLSX from 'xlsx'
import XLSXStyle from 'xlsx-style';
import { getBussinessDetailsList } from '../project/util'
import { getProjectIdsByInvoiceDate } from './util'
// 定义表头样式
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
) {
try {
if (req.method !== 'POST') {
return res.status(405).json({ message: 'Method not allowed' })
}
const { kai_piao_ri_qi, project_number } = req.body;
// 根据开票日期(支持单日或范围)与项目编号,确定要导出的项目集合
let projectIdsFromInvoice: string[] = [];
if (kai_piao_ri_qi) {
projectIdsFromInvoice = await getProjectIdsByInvoiceDate(kai_piao_ri_qi);
}
if (project_number) {
projectIdsFromInvoice.push(project_number);
}
// 查询项目
const projects = await prismaClient.project.findMany({
where: projectIdsFromInvoice.length > 0 ? { project_id: { in: projectIdsFromInvoice } } : undefined,
orderBy: {
project_id: 'desc'
}
});
// 生成业务明细数据Excel 行数据)
const excelDatas = await getBussinessDetailsList(projects);
// 若没有数据,返回空文件(也可改为返回提示)
const headerKeys = Object.keys(excelDatas?.[0] ?? {});
const worksheet = XLSX.utils.json_to_sheet(excelDatas, {
header: headerKeys.length > 0 ? headerKeys : undefined
});
// 设置样式
const range = XLSX.utils.decode_range(worksheet['!ref'] || 'A1');
// 统一列宽(可按需改为按列名定制)
const cols = [];
for (let i = 0; i <= range.e.c; i++) {
cols.push({ wch: 18 });
}
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 (worksheet[cellAddress]) {
worksheet[cellAddress].s = R === 0 ? HEADER_STYLE : CELL_STYLE;
}
}
}
// 生成工作簿并输出
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, '业务明细数据');
const buffer = XLSXStyle.write(workbook, { type: 'buffer', bookType: 'xlsx', cellStyles: true });
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
res.setHeader('Content-Disposition', 'attachment; filename=business-details.xlsx');
res.write(buffer);
res.end();
} catch (error) {
console.error('Error exporting business details:', error);
res.status(500).json({ message: 'Internal server error' });
}
}

View File

@@ -0,0 +1,150 @@
import type { NextApiRequest, NextApiResponse } from 'next'
import prismaClient from '@/lib/prisma'
import dayjs from 'dayjs'
import { Prisma } from '@prisma/client'
import { AGREEMENT_TYPES, AGREEMENT_TYPE_NAME, AGREEMENT_TYPE_CODE } from '@/CONSTANTS'
import type { InvoiceDataItem, ProcessedInvoiceDataItem } from '@/pages/api/type'
import { processInvoiceDataItem } from '@/lib/util'
import { getBussinessDetailsList } from '../project/util'
import { getProjectIdsByInvoiceDate } from './util'
type ResponseData = {
code: number
message: string
data?: {
data: any;
pagination?: {
total: number
page: number
pageSize: number
totalPages: number
}
}
error?: any
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<ResponseData>
) {
try {
// 这里是获取业务明细表的接口
const input: any = (req.body && Object.keys(req.body).length > 0) ? req.body : req.query;
const { page = 1, pageSize = 10, kai_piao_ri_qi } = input;
const project_number = typeof input?.project_number === 'string' ? input.project_number.trim() : input?.project_number;
const project_leader = typeof input?.project_leader === 'string' ? input.project_leader.trim() : input?.project_leader;
const project_name = typeof input?.project_name === 'string' ? input.project_name.trim() : input?.project_name;
const pageNum = Math.max(1, Number(page));
const size = Math.max(1, Number(pageSize));
const invoiceProjectIdsRaw: string[] = kai_piao_ri_qi ? await getProjectIdsByInvoiceDate(kai_piao_ri_qi) : [];
const invoiceProjectIds = Array.from(new Set((invoiceProjectIdsRaw || []).map(s => String(s).trim()).filter(Boolean)));
const projectIdsForFilter = [
...invoiceProjectIds,
...(project_number ? [String(project_number).trim()].filter(Boolean) : [])
];
if (kai_piao_ri_qi && invoiceProjectIds.length === 0 && !project_number) {
res.status(200).json({
code: 0,
message: '获取成功',
data: {
data: [],
pagination: {
total: 0,
page: pageNum,
pageSize: size,
totalPages: 0
}
}
});
return;
}
// 获取项目总数
const countWhere: any = {};
if (projectIdsForFilter.length > 0) {
countWhere.project_id = { in: projectIdsForFilter };
}
if (project_leader) {
countWhere.project_leader = project_leader;
}
if (project_name) {
countWhere.project_name = { contains: project_name };
}
const total = await prismaClient.project.count(Object.keys(countWhere).length > 0 ? { where: countWhere } : undefined);
// 分页查询项目
const conditions: any[] = [];
if (projectIdsForFilter.length > 0) {
conditions.push(Prisma.sql`project_id IN (${Prisma.join(projectIdsForFilter)})`);
}
if (project_leader) {
conditions.push(Prisma.sql`project_leader = ${project_leader}`);
}
if (project_name) {
conditions.push(Prisma.sql`project_name LIKE ${'%' + project_name + '%'}`);
}
let whereClause = Prisma.sql``;
if (conditions.length > 0) {
whereClause = Prisma.sql`WHERE ${conditions[0]}`;
for (let i = 1; i < conditions.length; i++) {
whereClause = Prisma.sql`${whereClause} AND ${conditions[i]}`;
}
}
const projects: any[] = await prismaClient.$queryRaw(Prisma.sql`
SELECT * FROM project
${whereClause}
ORDER BY CAST(
REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(
REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(
REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(
REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(
REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(
REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(
project_id,
'A',''),'B',''),'C',''),'D',''),'E',''),'F',''),'G',''),'H',''),'I',''),'J',''),
'K',''),'L',''),'M',''),'N',''),'O',''),'P',''),'Q',''),'R',''),'S',''),'T',''),
'U',''),'V',''),'W',''),'X',''),'Y',''),'Z',''),
'a',''),'b',''),'c',''),'d',''),'e',''),'f',''),'g',''),'h',''),'i',''),'j',''),
'k',''),'l',''),'m',''),'n',''),'o',''),'p',''),'q',''),'r',''),'s',''),'t',''),
'u',''),'v',''),'w',''),'x',''),'y',''),'z','')
AS UNSIGNED) DESC
LIMIT ${size} OFFSET ${pageNum - 1 >= 0 ? (pageNum - 1) * size : 0}
`);
// 后端已按项目编号倒序查询,无需再次排序
const excelDatas = await getBussinessDetailsList(projects, kai_piao_ri_qi);
const startIndex = (pageNum - 1) * size;
const excelDatasWithSeq = excelDatas.map((row, idx) => ({
...row,
'序号': startIndex + idx + 1
}));
// 构造分页信息
const totalPages = Math.ceil(total / size);
const pagination = {
total,
page: pageNum,
pageSize: size,
totalPages
};
res.status(200).json({
code: 0,
message: '获取成功',
data: {
data: excelDatasWithSeq,
pagination
}
});
} catch (error) {
console.error('handler:', error);
res.status(500).json({ message: '获取数据时出错' });
}
}

View File

@@ -0,0 +1,134 @@
import type { NextApiRequest, NextApiResponse } from 'next'
import prismaClient from '@/lib/prisma'
import dayjs from 'dayjs'
import { Prisma } from '@prisma/client'
import { AGREEMENT_TYPES, AGREEMENT_TYPE_NAME, AGREEMENT_TYPE_CODE } from '@/CONSTANTS'
import type { InvoiceDataItem, ProcessedInvoiceDataItem } from '@/pages/api/type'
import { processInvoiceDataItem } from '@/lib/util'
import { getBussinessDetailsList } from '../project/utilCopy'
import { getProjectIdsByInvoiceDate } from './util'
type ResponseData = {
code: number
message: string
data?: {
data: any;
pagination?: {
total: number
page: number
pageSize: number
totalPages: number
}
}
error?: any
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<ResponseData>
) {
try {
// 这里是获取业务明细表的接口
const { page = 1, pageSize = 10, kai_piao_ri_qi, project_number, project_leader, project_name } = req.body;
const pageNum = Math.max(1, Number(page));
const size = Math.max(1, Number(pageSize));
// 如果提供了开票日期,则验证格式并查询对应的项目号
let projectIdsFromInvoice: string[] = [];
if (kai_piao_ri_qi) {
projectIdsFromInvoice = await getProjectIdsByInvoiceDate(kai_piao_ri_qi);
}
// 如果提供了项目编号,则将其添加到筛选条件中
if (project_number) {
projectIdsFromInvoice.push(project_number);
}
// 获取项目总数
const countWhere: any = {};
if (projectIdsFromInvoice.length > 0) {
countWhere.project_id = { in: projectIdsFromInvoice };
}
if (project_leader) {
countWhere.project_leader = project_leader;
}
if (project_name) {
countWhere.project_name = { contains: project_name };
}
const total = await prismaClient.project.count(Object.keys(countWhere).length > 0 ? { where: countWhere } : undefined);
// 分页查询项目
const conditions: any[] = [];
if (projectIdsFromInvoice.length > 0) {
conditions.push(Prisma.sql`project_id IN (${Prisma.join(projectIdsFromInvoice)})`);
}
if (project_leader) {
conditions.push(Prisma.sql`project_leader = ${project_leader}`);
}
if (project_name) {
conditions.push(Prisma.sql`project_name LIKE ${'%' + project_name + '%'}`);
}
let whereClause = Prisma.sql``;
if (conditions.length > 0) {
whereClause = Prisma.sql`WHERE ${conditions[0]}`;
for (let i = 1; i < conditions.length; i++) {
whereClause = Prisma.sql`${whereClause} AND ${conditions[i]}`;
}
}
const projects: any[] = await prismaClient.$queryRaw(Prisma.sql`
SELECT * FROM project
${whereClause}
ORDER BY CAST(
REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(
REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(
REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(
REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(
REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(
REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(
project_id,
'A',''),'B',''),'C',''),'D',''),'E',''),'F',''),'G',''),'H',''),'I',''),'J',''),
'K',''),'L',''),'M',''),'N',''),'O',''),'P',''),'Q',''),'R',''),'S',''),'T',''),
'U',''),'V',''),'W',''),'X',''),'Y',''),'Z',''),
'a',''),'b',''),'c',''),'d',''),'e',''),'f',''),'g',''),'h',''),'i',''),'j',''),
'k',''),'l',''),'m',''),'n',''),'o',''),'p',''),'q',''),'r',''),'s',''),'t',''),
'u',''),'v',''),'w',''),'x',''),'y',''),'z','')
AS UNSIGNED) DESC
LIMIT ${size} OFFSET ${pageNum - 1 >= 0 ? (pageNum - 1) * size : 0}
`);
// 后端已按项目编号倒序查询,无需再次排序
const excelDatas = await getBussinessDetailsList(projects);
const startIndex = (pageNum - 1) * size;
const excelDatasWithSeq = excelDatas.map((row, idx) => ({
...row,
'序号': startIndex + idx + 1
}));
// 构造分页信息
const totalPages = Math.ceil(total / size);
const pagination = {
total,
page: pageNum,
pageSize: size,
totalPages
};
res.status(200).json({
code: 0,
message: '获取成功',
data: {
data: excelDatasWithSeq,
pagination
}
});
} catch (error) {
console.error('handler:', error);
res.status(500).json({ message: '获取数据时出错' });
}
}

View File

@@ -0,0 +1,81 @@
import type { NextApiRequest, NextApiResponse } from 'next'
import prismaClient from '@/lib/prisma'
import BigNumber from 'bignumber.js'
import { getBussinessDetailsList } from '../project/util'
type ResponseData = {
code: number
message: string
data?: {
totals: Record<string, string>
}
error?: any
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<ResponseData>
): Promise<void> {
try {
const projects = await prismaClient.project.findMany()
const rows = await getBussinessDetailsList(projects)
const amountKeyMap: Record<string, string> = {
'预计总支出 (不含税)': 'estimatedExpenseExclTax',
'开票收入(不含税)': 'invoiceRevenueExclTax',
'开票金额(含税)': 'invoiceAmountInclTax',
'已回款': 'receivedPayment',
'未回款金额': 'unreceivedAmount',
'已报销': 'reimbursed',
'预计未报销金额': 'estimatedUnreimbursed',
'已支付': 'paid',
}
const totals: Record<string, BigNumber> = Object.fromEntries(
Object.values(amountKeyMap).map(k => [k, new BigNumber(0)])
)
const parseAmount = (v: any): BigNumber => {
if (v === null || v === undefined) return new BigNumber(0)
if (BigNumber.isBigNumber(v)) return v
if (typeof v === 'number') {
const bn = new BigNumber(v)
return bn.isNaN() ? new BigNumber(0) : bn
}
if (typeof v === 'string') {
const s = v.trim()
if (s === '' || s === '-') return new BigNumber(0)
const s2 = s.replace(/,/g, '')
const bn = new BigNumber(s2)
return bn.isNaN() ? new BigNumber(0) : bn
}
return new BigNumber(0)
}
for (const row of rows) {
for (const [cnKey, enKey] of Object.entries(amountKeyMap)) {
const num = parseAmount(row[cnKey])
totals[enKey] = totals[enKey].plus(num)
}
}
const formatted: Record<string, string> = Object.fromEntries(
Object.entries(totals).map(([k, v]) => [k, v.toString()])
)
res.status(200).json({
code: 0,
message: '获取成功',
data: {
totals: formatted,
},
})
} catch (error) {
console.error('business_details/sum handler:', error)
res.status(500).json({
code: 500,
message: '获取数据时出错',
error,
})
}
}

View File

@@ -0,0 +1,133 @@
import { prismaClient } from '@/lib/prisma';
import dayjs from 'dayjs'
/**
* 根据开票日期获取对应的项目号
* @param kai_piao_ri_qi 开票日期,格式为 YYYY-MM-DD
* @returns 匹配的项目号数组
*/
/**
* 根据开票日期或日期范围获取对应的项目号
* 支持:
* - 单个日期字符串:'YYYY-MM-DD'
* - JSON数组字符串'["2025-09-01","2025-09-30",""]'
* - 直接数组:['2025-09-01','2025-09-30']
*/
export async function getProjectIdsByInvoiceDate(kai_piao_ri_qi: unknown): Promise<string[]> {
const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
const normalizeDate = (input: unknown): string | undefined => {
if (input === null || input === undefined) return undefined;
if (typeof input === 'number' && Number.isFinite(input)) {
const parsed = dayjs(input);
return parsed.isValid() ? parsed.format('YYYY-MM-DD') : undefined;
}
const s = String(input).trim();
if (!s) return undefined;
if (dateRegex.test(s)) return s;
const parsed = dayjs(s);
if (parsed.isValid()) return parsed.format('YYYY-MM-DD');
return undefined;
};
let start: string | undefined;
let end: string | undefined;
if (Array.isArray(kai_piao_ri_qi)) {
start = normalizeDate(kai_piao_ri_qi[0]);
end = normalizeDate(kai_piao_ri_qi[1]);
} else if (kai_piao_ri_qi && typeof kai_piao_ri_qi === 'object') {
const obj = kai_piao_ri_qi as any;
start = normalizeDate(obj?.start ?? obj?.startTime ?? obj?.begin ?? obj?.beginTime ?? obj?.[0]);
end = normalizeDate(obj?.end ?? obj?.endTime ?? obj?.finish ?? obj?.finishTime ?? obj?.[1]);
} else if (typeof kai_piao_ri_qi === 'string') {
const raw = kai_piao_ri_qi.trim();
if (dateRegex.test(raw)) {
start = normalizeDate(raw);
} else if (raw.startsWith('[')) {
// 尝试解析 JSON 数组字符串,例如 '["2025-09-03","2025-09-05",""]'
try {
const arr = JSON.parse(raw);
if (Array.isArray(arr)) {
start = normalizeDate(arr[0]);
end = normalizeDate(arr[1]);
}
} catch {
// 非法 JSON后续会走错误提示
}
} else {
const parts = raw.split(/[,,、\s]+/).map(s => s.trim()).filter(Boolean);
if (parts.length >= 2) {
start = normalizeDate(parts[0]);
end = normalizeDate(parts[1]);
}
}
}
// 组装 where 条件:区间或单点
// 由于数据库中日期可能以 YYYY-MM-DD 或 YYYY/MM/DD 存储,需要同时支持两种格式
let whereClause: any;
if (start && end) {
const startDash = start.replace(/\//g, '-');
const endExclusiveDash = dayjs(end).add(1, 'day').format('YYYY-MM-DD');
const startSlash = start.replace(/-/g, '/');
const endExclusiveSlash = dayjs(end).add(1, 'day').format('YYYY/MM/DD');
whereClause = {
OR: [
{ kai_piao_ri_qi: { gte: startDash, lt: endExclusiveDash } },
{ kai_piao_ri_qi: { gte: startSlash, lt: endExclusiveSlash } }
]
};
} else if (start) {
const startDash = start.replace(/\//g, '-');
const startSlash = start.replace(/-/g, '/');
whereClause = {
OR: [
{ kai_piao_ri_qi: { startsWith: startDash } },
{ kai_piao_ri_qi: { startsWith: startSlash } }
]
};
} else {
throw new Error('开票日期格式不正确,应为 YYYY-MM-DD 或 ["YYYY-MM-DD","YYYY-MM-DD",""]');
}
const voidRecords = await prismaClient.dingding_oa_invoice_application.findMany({
where: {
remark: {
in: ['发票作废', '作废']
}
},
select: { dataId: true }
});
const voidIds = voidRecords.map(r => r.dataId).filter(Boolean);
const invoiceRows = await prismaClient.invoice_data.findMany({
where: {
...whereClause
},
select: {
dataid: true
}
});
const dataIdsRaw = Array.from(new Set(invoiceRows.map(r => String(r.dataid ?? '').trim()).filter(Boolean)));
const voidIdSet = new Set(voidIds.map(id => String(id).trim()).filter(Boolean));
const dataIds = voidIdSet.size > 0 ? dataIdsRaw.filter(id => !voidIdSet.has(id)) : dataIdsRaw;
if (dataIds.length === 0) return [];
const apps = await prismaClient.dingding_oa_invoice_application.findMany({
where: {
dataId: { in: dataIds }
},
select: {
projectNumber: true
}
});
const projectIds = Array.from(new Set(
apps.flatMap(item => ((item.projectNumber || '') as string)
.split(/[,,、\s]+/)
.filter(Boolean))
));
return projectIds;
}

View File

@@ -0,0 +1,140 @@
import type { NextApiRequest, NextApiResponse } from 'next'
import prismaClient from '@/lib/prisma'
import AuthClient from '@/api/dingding/auth'
import OAClient from '@/api/dingding/oa'
import get from 'lodash/get'
import pLimit from 'p-limit'
type ResponseData = {
code: number
message: string
data?: {
totalTargets: number
updated: number
skipped: number
errors: Array<{ dataId: string; error: string }>
}
error?: any
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<ResponseData>
) {
try {
if (req.method !== 'POST') {
return res.status(405).json({ code: 405, message: 'Method not allowed' })
}
const body = req.body || {}
const inputIds: string[] = Array.isArray(body.dataIds) ? body.dataIds.filter(Boolean) : []
const limitNum = Number(body.limit || 200)
const concurrency = Math.max(1, Number(body.concurrency || 3))
const qps = Math.min(35, Math.max(1, Number(body.qps || 10)))
const whereClause = inputIds.length > 0
? { dataId: { in: inputIds } }
: { approvalNumber: null }
const targets = await prismaClient.dingding_oa_invoice_application.findMany({
where: whereClause,
select: { dataId: true },
take: limitNum
})
const ids = Array.from(new Set(targets.map(t => t.dataId).filter(Boolean)))
if (ids.length === 0) {
return res.status(200).json({
code: 0,
message: 'No records to backfill',
data: { totalTargets: 0, updated: 0, skipped: 0, errors: [] }
})
}
const accessToken = await AuthClient.getAccessToken()
const limit = pLimit(concurrency)
let updated = 0
let skipped = 0
const errors: Array<{ dataId: string; error: string }> = []
const window: number[] = []
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
const acquire = async () => {
const now = Date.now()
while (window.length > 0 && now - window[0] > 1000) {
window.shift()
}
if (window.length >= qps) {
const waitMs = 1000 - (now - window[0]) + 5
await sleep(waitMs)
}
window.push(Date.now())
}
const getBusinessIdWithRetry = async (dataId: string) => {
let attempt = 0
const maxAttempts = 5
while (attempt < maxAttempts) {
try {
await acquire()
const resp = await OAClient.getProcessInstanceWithOptions({
processInstanceId: dataId,
dingtalkAccessToken: accessToken
})
if (!resp) {
throw { message: 'processInstanceIdError', code: 'processInstanceIdError' }
}
const businessId: string = get(resp, 'body.result.businessId', '') || ''
if (businessId) return businessId
return ''
} catch (e: any) {
const msg: string = e?.message || ''
const code: string = e?.code || ''
if (code === 'Forbidden.AccessDenied.QpsLimitForAppkeyAndApi' || /QpsLimitForAppkeyAndApi/i.test(msg)) {
await sleep(1200 + attempt * 400)
attempt++
continue
}
throw e
}
}
return ''
}
const isValidId = (id: string) => {
if (!id || typeof id !== 'string') return false
const s = id.trim()
if (!s) return false
if (s.length < 10 || s.length > 128) return false
return /^[A-Za-z0-9\-\_]+$/.test(s)
}
const tasks = ids.map(id => limit(async () => {
try {
if (!isValidId(id)) {
skipped += 1
errors.push({ dataId: id, error: 'processInstanceIdError' })
return
}
const businessId = await getBusinessIdWithRetry(id)
if (!businessId) {
skipped += 1
return
}
await prismaClient.dingding_oa_invoice_application.updateMany({
where: { dataId: id },
data: { approvalNumber: businessId }
})
updated += 1
} catch (e: any) {
errors.push({ dataId: id, error: e?.message || 'unknown error' })
}
}))
await Promise.all(tasks)
return res.status(200).json({
code: 0,
message: 'Backfill completed',
data: {
totalTargets: ids.length,
updated,
skipped,
errors
}
})
} catch (error) {
return res.status(500).json({ code: 500, message: 'Internal server error', error })
}
}

View File

@@ -0,0 +1,219 @@
interface ImportConfig {
comment?: string;
tableName: string; // Prisma模型名称
fieldMapping: { [excelKey: string]: string }; // Excel列名到数据库字段的映射
idField: string; // ID字段名
}
// 支持的表配置
export const TABLE_CONFIGS: { [key: string]: ImportConfig } = {
project: {
tableName: 'project',
fieldMapping: {
'数据id': 'data_id',
'项目部门': 'project_department',
'项目名称': 'project_name',
'客户单位名称': 'client_name',
'是否多家客户单位': 'is_multi_client',
'客户单位名称(多)': 'multi_client_name',
'项目主担': 'project_leader',
'组别缩写': 'group_abbreviation',
'项目编号': 'project_id',
'创建时间': 'create_time',
'创建人': 'creator',
'创建人部门': 'creator_department',
'审批单标题': 'approval_title',
'更新时间': 'update_time',
},
idField: 'data_id'
},
procurement_contract_list: {
tableName: 'procurement_contract_list',
comment: '采购合同列表',
fieldMapping: {
'发起人': 'initiator',
'合同编号': 'contract_number',
'合同名称': 'contract_name',
'履约发起部门': 'lvyue_faqi_bumen',
'经办人': 'operator',
'相对方': 'xiangduifang',
'是否开口': 'is_open',
'合同总额': 'contract_amount',
'备注': 'remarks',
'履约开始时间': 'performance_start_time',
'合同原件扫描件(盖章版)': 'hetong_yuanjian_saomiaojian_gaizhangban',
'钉钉采购审批单': 'dingding_purchase_approval_file',
'合同附件扫描件': 'contract_attachment_scan',
'创建时间': 'creation_time',
'创建人': 'creator',
'更新时间': 'update_time',
'创建人部门': 'creator_department',
'审批单标题': 'approval_form_title',
'耗时(时:分:秒)': 'time_consumed',
},
idField: 'data_id'
},
purchase_contract_order_management: {
comment: '采购合同订单',
tableName: 'purchase_contract_order_management',
fieldMapping: {
'数据id': 'data_id',
'采购合同编号': 'purchase_contract_number',
'采购用途': 'purchase_purpose',
'项目编号': 'project_number',
'项目名称': 'project_name',
'关联销售合同编号': 'associated_sales_contract_number',
'合同名称': 'contract_name',
// '采购审批单号(请先填写合同编号)': 'purchase_approval_number',
'采购订单编号(请先填写合同编号)': 'purchase_approval_number',
// '合同名称(***请再次确认合同名称***)': 'purchase_contract_name',
'采购合同名称(***请再次确认合同名称***)': 'purchase_contract_name',
'相对方': 'xiang_dui_fang',
'合同类别(是否开口)': 'he_tong_lei_bie',
'合同总金额(含税)': 'he_tong_zong_jin_e_HanShui',
'剩余金额(含税)': 'sheng_yu_jin_e_HanShui',
'合同剩余金额提醒': 'he_tong_sheng_yu_jin_e_ti_xing',
'本次订单名称': 'ben_ci_ding_dan_ming_cheng',
'本次订单金额(含税)': 'ben_ci_ding_dan_jin_e_HanShui',
'本次订单履约开始时间': 'ben_ci_ding_dan_lu_yue_kai_shi_shi_jian',
'本次订单履约结束时间': 'ben_ci_ding_dan_lu_yue_jie_shu_shi_jian',
'本次订单发起部门': 'ben_ci_ding_dan_fa_qi_bu_men',
'本次订单发起人': 'ben_ci_ding_dan_fa_qi_ren',
'本次订单发起时间': 'ben_ci_ding_dan_fa_qi_shi_jian',
'本次订单文本(多文件请压缩后上传)': 'ben_ci_ding_dan_wen_ben',
'本次订单关联合同附件': 'ben_ci_ding_dan_guan_lian_he_tong_fujian',
'审批编号': 'approval_number',
'创建时间': 'creation_time',
'创建人': 'creator',
'当前负责人': 'current_responsible_person',
'审批结果': 'approval_result',
'审批状态': 'approval_status',
'更新时间': 'update_time',
'创建人部门': 'creator_department',
'审批单标题': 'approval_form_title',
'耗时(时:分:秒)': 'time_consumed'
},
idField: 'data_id'
},
purchase_contract_execution_maintenance: {
comment: '销售合同',
tableName: 'purchase_contract_execution_maintenance',
fieldMapping: {
'数据id': 'data_id',
'发起人': 'initiator',
'合同编号': 'contract_number',
'合同名称': 'contract_name',
'履约发起部门': 'lvyue_faqi_bumen',
'经办人': 'operator',
'相对方': 'xiangduifang',
'是否开口': 'is_open',
'合同总额': 'contract_amount',
'备注': 'remarks',
'履约开始时间': 'performance_start_time',
'合同原件扫描件(盖章版)': 'hetong_yuanjian_saomiaojian_gaizhangban',
'合同附件扫描件': 'contract_attachment_scan',
'创建时间': 'creation_time',
'创建者': 'creator',
'创建人部门': 'creator_department',
'审批单标题': 'approval_form_title',
'更新时间': 'update_time',
},
idField: 'data_id'
},
expense_reimbursement: { // 费用报销
tableName: 'expense_reimbursement',
fieldMapping: {
'序号': 'xuhao',
'数据id': 'data_id',
'项目号': 'project_number',
'日期': 'date',
'凭证号': 'voucher_number',
'分录号': 'entry_number',
'摘要': 'summary',
'科目编码': 'account_code',
'科目名称': 'account_name',
'辅助项': 'auxiliary_items',
'币种': 'currency',
'借方原币': 'debit_original_currency',
'借方本币': 'debit_local_currency',
'贷方原币': 'credit_original_currency',
'贷方本币': 'credit_local_currency',
'SR号': 'sr_number',
'共享凭证号': 'shared_voucher_number',
'费用预算项目': 'expense_budget_item',
'成本中心': 'cost_center',
'标准名称': 'standard_name',
'一级费用类型': 'expense_type_level1',
'二级费用类型': 'expense_type_level2',
'三级费用类型': 'expense_type_level3',
'金额': 'amount',
'创建时间': 'created_time',
'创建者': 'creator',
'创建人部门': 'creator_department',
'审批单标题': 'approval_title',
'更新时间': 'updated_time'
},
idField: 'data_id'
},
actual_payment: { // 实际支付
tableName: 'actual_payment',
fieldMapping: {
'数据id': 'data_id',
'项目编号': 'project_number',
'支付日期': 'payment_date',
'年份': 'payment_year',
'凭证号': 'pingzhenghao',
'行项目文本': 'hangxiangmu_wenben',
'业务线': 'business_line',
"期末余额(元)": 'qimo_yueyuan',
'期末余额(元)(大写)': 'qimo_yueyuan_daxie',
'支付方式': 'payment_method',
'供应商名称': 'gongyingshang_mingcheng',
'创建时间': 'created_time',
'创建者': 'creator',
'创建人部门': 'creator_department',
'审批单标题': 'approval_title',
'更新时间': 'updated_time'
},
idField: 'data_id'
},
// 客户管理
customer_management_info: {
tableName: 'customer_management_info',
fieldMapping: {
'数据id': 'data_id',
'客户单位名称': 'customer_unit_name',
'客户分类': 'customer_type',
},
idField: 'data_id'
},
invoice_void_record: {
comment: '发票作废记录表',
tableName: 'invoice_void_record',
fieldMapping: {
'数据id': 'dataId',
'所在部门': 'applicationDepartment',
'申请业务组': 'xiang_mu_zu_bie',
'申请日期': 'applicationDate',
'申请人': 'applicant',
'发票号码': 'piao_hao',
'开票日期': 'kai_piao_ri_qi',
'协议类型': 'agreementType',
'作废事由': 'zuo_fei_shi_you',
'作废原因': 'cancelReason',
'发票金额': 'invoice_amount_yuan',
'金额是否含税': 'is_tax_inclusive',
'费用所属时间': 'feeBelongingTime',
'付款单位名称': 'customerCompanyName',
'付款单位纳税人识别号': 'tong_yi_she_hui_xin_yong_dai_ma',
'付款单位开户行': 'bankName',
'付款单位开户账号': 'bankAccount',
'上传作废发票附件': 'uploadedVoidInvoiceAttachment',
'手写签名附件': 'handwrittenSignatureAttachment',
'项目编号': 'projectNumber',
'备注信息': 'remark'
},
idField: 'dataId'
}
};

View File

@@ -0,0 +1,99 @@
import { logger } from '@/api/winston/logger';
import type { NextApiRequest, NextApiResponse } from 'next'
type ResponseData = {
message: string
testjsonres?: string
}
// 定义请求配置接口
interface ImportRequestConfig {
tableName: string;
processCode: string;
daysFromNow: string;
}
// 通用的importData接口调用函数
async function callImportDataAPI(baseUrl: string, config: ImportRequestConfig) {
const url = new URL('/api/dingding/importData', baseUrl);
url.searchParams.append('tableName', config.tableName);
url.searchParams.append('PROCESS_CODE', config.processCode);
url.searchParams.append('daysFromNow', config.daysFromNow);
const response = await fetch(url.toString(), {
method: 'GET',
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
}
// 通用的importData接口调用函数
async function callImportInvoiceApprovalDataAPI(baseUrl: string, config: ImportRequestConfig) {
const url = new URL('/api/dingding/importInvoiceApprovalData', baseUrl);
url.searchParams.append('tableName', config.tableName);
url.searchParams.append('PROCESS_CODE', config.processCode);
url.searchParams.append('daysFromNow', config.daysFromNow);
const response = await fetch(url.toString(), {
method: 'GET',
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<ResponseData>
) {
try {
logger.info('调用corn接口')
// 使用 req.headers.host 获取主机域名
const host = req.headers.host || 'localhost:3000';
const protocol = req.headers.referer?.startsWith('https') ? 'https' : 'http';
const baseUrl = `${protocol}://${host}`;
// // 定义请求配置
// const requests: ImportRequestConfig[] = [
// {
// tableName: 'purchase_contract_execution_maintenance',
// processCode: 'PROC-870CF43E-EF32-4BCF-BC4B-D78FD12DADC4',
// daysFromNow: '3'
// },
// {
// tableName: 'purchase_contract_order_management',
// processCode: 'PROC-4FB03ECE-6F80-413F-90C3-CC9DCA903A2F',
// daysFromNow: '3'
// }
// ];
// // 执行所有请求
// const results = [];
// for (const config of requests) {
// const result = await callImportDataAPI(baseUrl, config);
// results.push(result);
// }
await callImportInvoiceApprovalDataAPI(baseUrl, {
tableName: 'dingding_oa_invoice_application',
processCode: 'PROC-C8B8B69F-2F2C-4B76-9831-0E68C89A367F',
daysFromNow: '15'
})
logger.info('调用corn接口-结束')
res.status(200).json({
message: `成功调用importData接口`,
testjsonres: JSON.stringify(['results'])
});
} catch (error) {
console.error('调用importData接口时出错:', error);
res.status(500).json({ message: `调用importData接口时出错: ${error.message}` });
}
}

View File

@@ -0,0 +1,103 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { withFileUpload } from 'next-multiparty';
import * as XLSX from 'xlsx';
import prismaClient from '@/lib/prisma';
import { TABLE_CONFIGS } from './config'
// 定义通用导入配置接口
export const config = {
api: {
bodyParser: false // 必须禁用默认bodyParser
}
};
export default withFileUpload(async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
try {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
// 从查询参数获取表名
const tableName = req?.fields?.tableName as string ?? '';
// 验证表名
if (!tableName || !TABLE_CONFIGS[tableName]) {
return res.status(400).json({
error: 'Invalid table name',
supportedTables: Object.keys(TABLE_CONFIGS)
});
}
const tableConfig = TABLE_CONFIGS[tableName];
// 读取Excel文件
const fileBuffer = await req?.file?.toBuffer();
if (!fileBuffer) {
return res.status(400).json({ error: 'File not provided' });
}
const workbook = XLSX.read(fileBuffer, { type: 'buffer' });
const firstSheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[firstSheetName];
const rawData: any[] = XLSX.utils.sheet_to_json(worksheet);
// 数据处理
const dataList: any[] = [];
for (let i = 0; i < rawData.length; i++) {
const rawItem = rawData[i];
const data: any = {};
// 设置ID字段
data[tableConfig.idField] = rawItem['数据id'];
// 映射字段
for (const [excelKey, dbKey] of Object.entries(tableConfig.fieldMapping)) {
data[dbKey] = rawItem[excelKey];
}
dataList.push(data);
}
// 查询数据库中已存在的记录
const existingRecords = await prismaClient[tableConfig.tableName].findMany({
where: {
[tableConfig.idField]: {
in: dataList.map(item => item[tableConfig.idField])
}
},
select: {
[tableConfig.idField]: true
}
});
const existingIdSet = new Set(existingRecords.map((record: any) => record[tableConfig.idField]));
const newDataList = dataList.filter(item => !existingIdSet.has(item[tableConfig.idField]));
// 如果没有新数据需要插入,则直接返回
if (newDataList.length === 0) {
return res.status(200).json({
code: 0,
data: null,
message: `${tableName}数据导入完成,无新数据需要插入`
});
}
// 使用Prisma动态调用对应模型的createMany方法
await prismaClient[tableConfig.tableName].createMany({
data: newDataList,
skipDuplicates: true, // 跳过重复数据
});
res.status(200).json({
code: 0,
data: null,
message: `${tableName}数据导入成功,共插入${newDataList.length}条新数据`
});
} catch (error) {
console.error('Generic import error:', error);
res.status(500).json({ error: 'Internal server error', stack: error });
}
});

View File

@@ -0,0 +1,166 @@
/**
* @description
* 技术方案:
* https://docs.dingtalk.com/i/nodes/Obva6QBXJw6YEvDyila56vb0Vn4qY5Pr
* 我现在想要搞得是自动化将钉钉oa的数据读取到mysql中的数据
* 配置项目1 code
* 配置项目2 表格名字
* 配置项目3 字段映射
* 1. 根据审批code 从接口中获得实例id列表
* 2. 根据实例id列表请求对应的实例
* 3. 将实例id根据表格映射拼接成对应的数据
* 4. 然后根据对应的表格和映射关系将其录入到数据库中
* 5. 完成操作
*/
import dayjs from 'dayjs'
import type { NextApiRequest, NextApiResponse } from 'next'
import { get } from 'lodash'
import AuthClient from '@/api/dingding/auth'
import OAClient from '@/api/dingding/oa'
import { FIELD_MAPPINGS, DATA_ID_FIELD_MAPPINGS } from '@/CONSTANTS'
import prismaClient from '@/lib/prisma'
import { logger } from '@/api/winston/logger'
import { getInstanceIdList } from '@/pages/api/dingding/util'
import pLimit from 'p-limit'
const DEFAULT_PROCESS_CODE = 'PROC-870CF43E-EF32-4BCF-BC4B-D78FD12DADC4' //process.env.DINGDING_INVOICE_APPROVAL_PROCESS_CODE ?? ''
type ResponseData = {
code: number
message: string
newInstanceIds?: string[]
error?: unknown
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<ResponseData>
) {
try {
const tableName = req.query.tableName as string ?? 'purchase_contract_execution_maintenance'
const PROCESS_CODE = req.query.processCode as string ?? DEFAULT_PROCESS_CODE
// 获取时间参数,支持 startTime 和 endTime 字符串格式
const startTimeStr = req.query.startTime as string
const endTimeStr = req.query.endTime as string
const daysFromNow = Number(req.query.daysFromNow) ?? 3;
const accessToken = await AuthClient.getAccessToken();
console.log("获得token", accessToken);
// 处理时间逻辑:优先使用传入的 startTime 和 endTime否则使用 daysFromNow
let startTime: number
let endTime: number | undefined
if (startTimeStr) {
// 如果传入了 startTime 字符串,转换为时间戳
startTime = dayjs(startTimeStr).valueOf()
} else {
// 否则使用 daysFromNow 计算开始时间
startTime = dayjs().subtract(daysFromNow, 'day').valueOf()
}
if (endTimeStr) {
// 如果传入了 endTime 字符串,转换为时间戳
endTime = dayjs(endTimeStr).valueOf()
} else {
// 否则不设置结束时间
endTime = undefined
}
// https://open.dingtalk.com/document/orgapp/obtain-an-approval-list-of-instance-ids
const instanceIdList = await getInstanceIdList({
accessToken: accessToken,
startTime,
endTime,
maxResults: 20,
processCode: PROCESS_CODE
})
const dataIdField = DATA_ID_FIELD_MAPPINGS[tableName]
const existingData = await prismaClient[tableName].findMany({
select: {
[dataIdField]: true
}
});
// 提取已存在的data_id
const existingDataIds = existingData.map(item => item[dataIdField]);
// 计算差集只处理不存在于数据库中的instanceId
const newInstanceIds = instanceIdList.filter(id => !existingDataIds.includes(id));
if (newInstanceIds.length > 0) {
// 创建一个限制并发数为5的限制器
const limit = pLimit(20);
// 使用 p-limit 来限制并发请求数并在每个请求完成后等待1秒
const promises = newInstanceIds.map(instanceId =>
limit(async () => {
const result = await OAClient.getProcessInstanceWithOptions({
processInstanceId: instanceId,
dingtalkAccessToken: accessToken
});
// 请求完成后等待1秒
await new Promise(resolve => setTimeout(resolve, 1000));
return result;
})
);
// 使用 Promise.allSettled 等待所有结果完成
const results = await Promise.allSettled(promises);
// 汇总成功的结果
const successfulResults = results
.filter(result => result.status === 'fulfilled')
.map(result => (result as PromiseFulfilledResult<any>).value);
const failedResults = results
.filter(result => result.status === 'rejected')
.map(result => (result as PromiseRejectedResult).reason);
if (failedResults.length > 0) {
console.error('有部分请求失败:', failedResults);
logger.error('有部分请求失败:', { failedResults })
}
const mappedDatas = successfulResults.map(item => {
const formComponentValues = get(item, 'body.result.formComponentValues', []);
const processInstanceId = get(item, 'processInstanceId', '');
const mappedData: any = {};
formComponentValues.forEach(fv => {
const fieldName = fv.name;
const fieldValue = fv.value;
// 检查是否有对应的字段映射
const tablekey = FIELD_MAPPINGS[tableName][fieldName]
if (tablekey) {
mappedData[tablekey] = fieldValue ?? '';
}
});
if (processInstanceId) {
mappedData.data_id = processInstanceId;
}
if (tableName === 'purchase_contract_order_management') {
mappedData['approval_result'] = '审批通过';
}
return {
...mappedData,
}
})
console.log("---")
try {
await prismaClient[tableName].createMany({
data: mappedDatas,
skipDuplicates: true, // 跳过重复数据
});
} catch (error) {
console.error('插入数据时出错:', error);
logger.error('插入数据时出错:', { error })
res.status(400).json({ code: 400, message: `数据插入到${tableName}表中失败`, error: error, })
}
}
logger.info(`数据已成功插入到${tableName}表中; count: ${newInstanceIds.length}; ${JSON.stringify(newInstanceIds)}`,)
res.status(200).json({ code: 0, message: `数据已成功插入到${tableName}表中`, newInstanceIds })
} catch (error) {
console.error('handler:', error);
logger.error('获取 token 时出错:', { error })
res.status(500).json({ code: 500, message: '获取 token 时出错' });
}
}

View File

@@ -0,0 +1,823 @@
/**
* @description
* 技术方案:
* https://docs.dingtalk.com/i/nodes/Obva6QBXJw6YEvDyila56vb0Vn4qY5Pr
* 我现在想要搞得是自动化将钉钉oa的数据读取到mysql中的数据
* 配置项目1 code
* 配置项目2 表格名字
* 配置项目3 字段映射
* 1. 根据审批code 从接口中获得实例id列表
* 2. 根据实例id列表请求对应的实例
* 3. 将实例id根据表格映射拼接成对应的数据
* 4. 然后根据对应的表格和映射关系将其录入到数据库中
* 5. 完成操作
*/
import dayjs from 'dayjs'
import type { NextApiRequest, NextApiResponse } from 'next'
import { get } from 'lodash'
import AuthClient from '@/api/dingding/auth'
import OAClient from '@/api/dingding/oa'
import prismaClient from '@/lib/prisma'
import { logger } from '@/api/winston/logger'
import { getInstanceIdList } from '@/pages/api/dingding/util'
import {
FIELD_MAPPINGS,
DATA_ID_FIELD_MAPPINGS,
AGREEMENT_TYPE_CODE,
AGREEMENT_TYPE_NAME,
INVOICE_CODE
} from '@/CONSTANTS';
import { InvoiceApplicationAmountDetails } from '../type'
import pLimit from 'p-limit'
const DEFAULT_PROCESS_CODE = 'PROC-C8B8B69F-2F2C-4B76-9831-0E68C89A367F' //process.env.DINGDING_INVOICE_APPROVAL_PROCESS_CODE ?? ''
type ResponseData = {
code: number
message: string
newInstanceIds?: string[]
error?: unknown
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<ResponseData>
) {
try {
const tableName = req.query.tableName as string ?? 'purchase_contract_execution_maintenance'
const PROCESS_CODE = req.query.processCode as string ?? DEFAULT_PROCESS_CODE
// 获取时间参数,支持 startTime 和 endTime 字符串格式
const startTimeStr = req.query.startTime as string
const endTimeStr = req.query.endTime as string
logger.info('importInvoiceApprovalData params', { tableName, processCode: PROCESS_CODE, startTimeStr, endTimeStr });
const accessToken = await AuthClient.getAccessToken();
console.log("获得token", accessToken);
// 处理时间逻辑:优先使用传入的 startTime 和 endTime否则使用 daysFromNow
let startTime: number
let endTime: number | undefined
if (startTimeStr) {
startTime = dayjs(startTimeStr).valueOf()
} else {
startTime = dayjs().subtract(60, 'day').valueOf()
}
if (endTimeStr) {
endTime = dayjs(endTimeStr).valueOf()
} else {
endTime = Date.now()
}
logger.info('importInvoiceApprovalData timeRange', { startTime, endTime });
const instanceIdList = await getInstanceIdList({
accessToken: accessToken,
startTime,
endTime,
maxResults: 20,
processCode: PROCESS_CODE
})
logger.info('importInvoiceApprovalData instanceIdList', { count: instanceIdList.length, sample: instanceIdList.slice(0, 5) })
const dataIdField = DATA_ID_FIELD_MAPPINGS[tableName]
const existingData = await prismaClient[tableName].findMany({
select: {
[dataIdField]: true
}
});
// 提取已存在的data_id
const existingDataIds = existingData.map(item => item[dataIdField]);
// 计算差集只处理不存在于数据库中的instanceId
const newInstanceIds = instanceIdList.filter(id => !existingDataIds.includes(id));
logger.info('importInvoiceApprovalData newInstanceIds', { totalFetched: instanceIdList.length, existed: existingDataIds.length, toFetch: newInstanceIds.length, sample: newInstanceIds.slice(0, 5) })
if (newInstanceIds.length > 0) {
// 创建一个限制并发数为5的限制器
const limit = pLimit(20);
// 使用 p-limit 来限制并发请求数并在每个请求完成后等待1秒
const promises = newInstanceIds.map(instanceId =>
limit(async () => {
const result = await OAClient.getProcessInstanceWithOptions({
processInstanceId: instanceId,
dingtalkAccessToken: accessToken
});
// 请求完成后等待1秒
await new Promise(resolve => setTimeout(resolve, 1000));
return result;
})
);
// 使用 Promise.allSettled 等待所有结果完成
const results = await Promise.allSettled(promises);
const successfulResults = results
.filter(result => result.status === 'fulfilled')
.map(result => (result as PromiseFulfilledResult<any>).value);
const failedResults = results
.filter(result => result.status === 'rejected')
.map(result => (result as PromiseRejectedResult).reason);
if (failedResults.length > 0) {
console.error('有部分请求失败:', failedResults);
logger.error('有部分请求失败:', { failedResults })
}
// 新增:严格筛选“审批完成且审批通过”
const approvedResults = successfulResults.filter(item => {
const status = (get(item, 'body.result.status', '') || '').toUpperCase();
const resultFlag = (get(item, 'body.result.result', '') || '').toUpperCase();
return status === 'COMPLETED'&& resultFlag === 'AGREE';
});
logger.info('importInvoiceApprovalData results', { successful: successfulResults.length, approved: approvedResults.length })
// 新增:安全 JSON 解析工具,统一归一化为数组
const safeParseArray = (val: any): any[] => {
try {
if (Array.isArray(val)) return val;
if (typeof val === 'string' && val.trim()) {
const parsed = JSON.parse(val);
return Array.isArray(parsed) ? parsed : [];
}
return [];
} catch {
return [];
}
};
// 映射为入库对象
const mappedDatas = approvedResults.map(item => {
const formComponentValues = get(item, 'body.result.formComponentValues', []);
const processInstanceId = get(item, 'processInstanceId', '');
const approvalStatus = get(item, 'body.result.status', '');
const businessId = get(item, 'body.result.businessId', '') || '';
const approvalNumberDD = get(item, 'body.result.approvalNumber', '') || get(item, 'body.result.approvalNo', '') || '';
const mappedData: any = {};
const tableMapping = FIELD_MAPPINGS[tableName as keyof typeof FIELD_MAPPINGS] || {};
const normalize = (s: any) => typeof s === 'string' ? s.replace(/\s+/g, '').replace(/[:]/g, '').replace(/.*?/g, '').replace(/\(.*?\)/g, '') : s;
const normalizedMap: Record<string, string> = Object.fromEntries(Object.entries(tableMapping).map(([k, v]) => [normalize(k), v as string]));
const unmappedFields: string[] = [];
const pickRowField = (rowValue: any[], candidates: string[]) => {
const norm = (s: string) => typeof s === 'string' ? s.replace(/\s+/g, '').replace(/[:]/g, '').replace(/.*?/g, '').replace(/\(.*?\)/g, '') : '';
const targets = candidates.map(norm);
for (const cell of rowValue || []) {
const lbl = norm(cell?.label || '');
if (targets.includes(lbl)) return cell?.value ?? '';
}
return '';
};
// 字段映射
formComponentValues.forEach((fv: any) => {
const fieldName = fv.name;
const fieldValue = fv.value;
const tablekey = (tableMapping as any)[fieldName] || normalizedMap[normalize(fieldName)];
if (fv.componentType === 'DDSelectField' && fv.extValue) {
try {
const extValueObj = JSON.parse(fv.extValue);
const finalValue = extValueObj.label || fieldValue;
if (tablekey && !mappedData[tablekey] && finalValue) {
mappedData[tablekey] = finalValue;
}
} catch (error) {
console.warn(`Failed to parse extValue for field ${fieldName}:`, error);
if (tablekey && !mappedData[tablekey] && fieldValue) {
mappedData[tablekey] = fieldValue;
}
}
} else if (fv.componentType === 'DepartmentField' && fv.extValue) {
try {
const arr = JSON.parse(fv.extValue);
const obj = Array.isArray(arr) ? arr[0] : null;
const finalValue = obj?.name || obj?.fullDeptName || fieldValue;
if (tablekey && !mappedData[tablekey] && finalValue) {
mappedData[tablekey] = finalValue;
}
} catch {
if (tablekey && !mappedData[tablekey] && fieldValue) {
mappedData[tablekey] = fieldValue;
}
}
} else if (fv.componentType === 'InnerContactField' && fv.extValue) {
try {
const arr = JSON.parse(fv.extValue);
const obj = Array.isArray(arr) ? arr[0] : null;
const finalValue = obj?.name || fieldValue;
if (tablekey && !mappedData[tablekey] && finalValue) {
mappedData[tablekey] = finalValue;
}
} catch {
if (tablekey && !mappedData[tablekey] && fieldValue) {
mappedData[tablekey] = fieldValue;
}
}
} else if (fv.componentType === 'SignatureField') {
const cleaned = typeof fieldValue === 'string' ? fieldValue.replace(/`/g, '').trim() : fieldValue;
if (tablekey && !mappedData[tablekey] && cleaned) {
mappedData[tablekey] = cleaned;
}
} else if (fv.componentType === 'DDAttachment') {
if (tablekey && !mappedData[tablekey] && fieldValue) {
mappedData[tablekey] = fieldValue;
}
} else if (fv.componentType === 'DDDateField' || fv.componentType === 'TextField') {
if (tablekey && !mappedData[tablekey] && fieldValue) {
mappedData[tablekey] = fieldValue;
}
} else {
if (tablekey && !mappedData[tablekey] && fieldValue) {
mappedData[tablekey] = fieldValue;
} else if (!tablekey) {
unmappedFields.push(fieldName);
}
}
});
// dataId
const idKeyName = DATA_ID_FIELD_MAPPINGS[tableName];
if (processInstanceId && idKeyName) {
mappedData[idKeyName] = processInstanceId;
}
// 新增:拆分并归一化业务开始/结束时间的区间字段
if ('businessStartTime-BusinessEndTime' in mappedData) {
const raw = mappedData['businessStartTime-BusinessEndTime'];
let start = '';
let end = '';
if (Array.isArray(raw)) {
start = raw[0] ?? '';
end = raw[1] ?? '';
} else if (typeof raw === 'string') {
const arr = safeParseArray(raw);
if (arr.length > 0) {
start = arr[0] ?? '';
end = arr[1] ?? '';
} else {
// 若不是 JSON 数组字符串,兜底将整值当作开始时间
start = raw || '';
end = '';
}
}
// 若已有单字段值,则优先保留已有值
if (!mappedData.businessStartTime) mappedData.businessStartTime = start;
if (!mappedData.businessEndTime) mappedData.businessEndTime = end;
// 入库前删除临时复合键,避免 Prisma 校验报错
delete mappedData['businessStartTime-BusinessEndTime'];
}
// 协议类型代码映射(不存在置 null
const agreementTypeCode = AGREEMENT_TYPE_CODE[AGREEMENT_TYPE_NAME[mappedData['agreementType']]] ?? null;
// 发票类型名称与代码
const invoiceTypeName: string = mappedData['invoiceType'] || '';
const invoiceCode = INVOICE_CODE[invoiceTypeName] ?? null;
// 解析两类明细表格(已存在)
const bigInvoiceAmountList = get(mappedData, 'invoice_amount_yuan_da', '');
const bigInvoiceAmountListStr = bigInvoiceAmountList ? JSON.parse(bigInvoiceAmountList) : [];
mappedData.BigInvoiceAmountList = bigInvoiceAmountListStr.map(items => {
const rowValue = items.rowValue;
// const thirdPartyCost = pickRowField(rowValue, ['预计报销第三方成本(不含税,元)','预计报销第三方成本(元)','预计报销第三方成本']);
const invoiceAmount = pickRowField(rowValue, ['开票金额(元)(不含税)', '开票金额(元)', '开票金额']) || (rowValue[1]?.value ?? '');
const bigInvoiceAmount: InvoiceApplicationAmountDetails = {
dataid: processInstanceId,
agreementType: agreementTypeCode,
project_number: '',
business_group_name: rowValue[0]?.value ?? '',
project_name: '',
business_content: '',
invoice_amount: invoiceAmount,
// yu_ji_bao_xiao_di_san_fang_cheng_ben: thirdPartyCost,
}
return bigInvoiceAmount;
});
const multiProjectMergeInvoice = get(mappedData, 'multiProjectMergeInvoice', '');
const multiProjectMergeInvoiceStr = multiProjectMergeInvoice ? JSON.parse(multiProjectMergeInvoice) : [];
// 同时保留明细记录用于明细表
mappedData.multiProjectMergeInvoice =multiProjectMergeInvoiceStr.map(row => {
const rowValue = row.rowValue;
const rowField = (candidates: string[], fallbackIndex?: number) => {
const valueByLabel = pickRowField(rowValue, candidates);
if (valueByLabel !== '' && valueByLabel !== undefined && valueByLabel !== null) {
return valueByLabel;
}
if (fallbackIndex !== undefined) {
return rowValue?.[fallbackIndex]?.value ?? '';
}
return '';
};
const thirdPartyCost = pickRowField(rowValue, ['预计报销第三方成本(不含税,元)','预计报销第三方成本(元)','预计报销第三方成本']);
const multiProjectMergeInvoice: InvoiceApplicationAmountDetails = {
dataid: processInstanceId,
agreementType: agreementTypeCode,
project_number: rowField(['项目编号'], 0),
project_name: rowField(['项目名称'], 1),
business_group_name: rowField(['业务组名称', '申请业务组', '业务组'], 5), // 业务组名称
business_content: rowField(['业务内容'], 6), // 业务内容
invoice_amount: rowField(['开票金额(元)', '开票金额(元)', '开票金额'], 3), // 开票金额
yu_ji_bao_xiao_di_san_fang_cheng_ben: thirdPartyCost,
}
return multiProjectMergeInvoice;
});
const mergedProjectNumbers = Array.from(new Set((multiProjectMergeInvoiceStr || []).map(row => row.rowValue[0]?.value).filter(Boolean)));
if (mergedProjectNumbers.length > 1) {
mappedData.projectNumber = mergedProjectNumbers.join(',');
mappedData.is_multi_project_merge = '是';
} else if (mergedProjectNumbers.length === 1) {
mappedData.projectNumber = mergedProjectNumbers[0];
}
// 计算多项目合并的总金额并写入主表字段(用于导出)
if (Array.isArray(multiProjectMergeInvoiceStr) && multiProjectMergeInvoiceStr.length > 0) {
const totalInvoiceAmount = multiProjectMergeInvoiceStr.reduce((sum, row) => {
const rowValue = row?.rowValue || [];
const rawAmount = pickRowField(rowValue, ['开票金额(元)', '开票金额(元)', '开票金额']) || (rowValue?.[3]?.value ?? '0');
const val = parseFloat(rawAmount);
return sum + (isNaN(val) ? 0 : val);
}, 0);
const taxRateNum = parseFloat(mappedData.invoice_tax_rate ?? '0');
if (totalInvoiceAmount > 0) {
if (!isNaN(taxRateNum) && taxRateNum > 0) {
const buHan = totalInvoiceAmount / (1 + taxRateNum);
const hanShui = totalInvoiceAmount;
mappedData.bu_han_shui_jin_e_yuan_duo = buHan.toFixed(2);
mappedData.han_shui_jin_e_yuan_duo = hanShui.toFixed(2);
} else {
// 无税率时,视为不含税=含税=开票金额
mappedData.bu_han_shui_jin_e_yuan_duo = totalInvoiceAmount.toFixed(2);
mappedData.han_shui_jin_e_yuan_duo = totalInvoiceAmount.toFixed(2);
}
}
}
// 新增:解析"单合同多家单位开票清单"
const singleContractMultiCustomer = get(mappedData, 'single_contract_multi_customer_invoice_list', '');
const singleContractMultiCustomerStr = singleContractMultiCustomer ? JSON.parse(singleContractMultiCustomer) : [];
// 仅主表记录“预计报销第三方成本(不含税,元)”字段:汇总三类明细的该字段
{
const extractThirdParty = (row: any) => {
const rv = row?.rowValue || []
const val = pickRowField(rv, ['预计报销第三方成本(不含税,元)','预计报销第三方成本(元)','预计报销第三方成本'])
const num = parseFloat((val ?? '').toString())
return Number.isNaN(num) ? 0 : num
}
const existingThirdPartyRaw = String(mappedData.yuJiBaoXiaoDiSanFangChengBenYuan ?? '').trim()
const existingThirdPartyNum = parseFloat(existingThirdPartyRaw)
const sumThirdParty =
(Array.isArray(bigInvoiceAmountListStr) ? bigInvoiceAmountListStr.reduce((s, r) => s + extractThirdParty(r), 0) : 0) +
(Array.isArray(multiProjectMergeInvoiceStr) ? multiProjectMergeInvoiceStr.reduce((s, r) => s + extractThirdParty(r), 0) : 0) +
(Array.isArray(singleContractMultiCustomerStr) ? singleContractMultiCustomerStr.reduce((s, r) => s + extractThirdParty(r), 0) : 0)
if (sumThirdParty > 0) {
mappedData.yuJiBaoXiaoDiSanFangChengBenYuan = sumThirdParty.toString()
} else if (existingThirdPartyRaw && !Number.isNaN(existingThirdPartyNum)) {
mappedData.yuJiBaoXiaoDiSanFangChengBenYuan = existingThirdPartyRaw
} else {
mappedData.yuJiBaoXiaoDiSanFangChengBenYuan = ''
}
}
// 为单合同多家单位开票生成主表记录
const singleContractMultiCustomerMainRecords = singleContractMultiCustomerStr.map(row => {
const rowValue = row.rowValue;
const rowField = (candidates: string[], fallbackIndex?: number) => {
const valueByLabel = pickRowField(rowValue, candidates);
if (valueByLabel !== '' && valueByLabel !== undefined && valueByLabel !== null) {
return valueByLabel;
}
if (fallbackIndex !== undefined) {
return rowValue?.[fallbackIndex]?.value ?? '';
}
return '';
};
const invoiceAmountRaw = rowField(['开票金额(元)', '开票金额(元)', '开票金额'], 7);
const invoiceAmount = parseFloat(invoiceAmountRaw ?? '0');
const taxRate = parseFloat(mappedData.invoice_tax_rate ?? '0');
const thirdPartyCost = pickRowField(rowValue, ['预计报销第三方成本(不含税,元)','预计报销第三方成本(元)','预计报销第三方成本']);
// 计算不含税金额和含税金额
let buHanShuiAmount = 0;
let hanShuiAmount = 0;
if (taxRate > 0) {
// 如果税率存在,根据税率计算
buHanShuiAmount = invoiceAmount / (1 + taxRate);
hanShuiAmount = invoiceAmount;
} else {
// 如果没有税率,假设开票金额就是不含税金额
buHanShuiAmount = invoiceAmount;
hanShuiAmount = invoiceAmount;
}
// 创建主表记录,复制原记录的所有字段,但替换客户相关字段
return {
...mappedData,
dataId: processInstanceId,
agreementType: agreementTypeCode,
invoiceType: invoiceTypeName || null,
invoiceCode: invoiceCode,
approvalStatus: approvalStatus,
approvalNumber: businessId,
approvalResult: '审批通过',
// 替换为明细的银行账号信息
projectNumber: rowField(['项目编号'], 0),
tong_yi_she_hui_xin_yong_dai_ma: rowField(['统一社会信用代码'], 3),
bankName: rowField(['开户行名称'], 4),
bankAccount: rowField(['银行账号'], 5),
customerCompanyName: rowField(['付款单位名称', '客户单位名称'], 2),
// customer_tax_number: rowValue[3]?.value ?? '',
// customer_bank_name: rowValue[4]?.value ?? '',
// customer_bank_account: rowValue[5]?.value ?? '',
// business_content_du: rowValue[6]?.value ?? '',
invoice_amount_yuan_du: invoiceAmount.toString(),
bu_han_shui_jin_e_yuan_du: buHanShuiAmount.toFixed(2),
han_shui_jin_e_yuan_du: hanShuiAmount.toFixed(2),
// 使用明细对应的第三方成本值,而不是汇总值
yuJiBaoXiaoDiSanFangChengBenYuan: thirdPartyCost,
is_single_contract_multi_customer: '是',
// 确保BigInvoiceAmountList字段存在且为null
BigInvoiceAmountList: null,
// 确保multiProjectMergeInvoice字段存在且为null
multiProjectMergeInvoice: null
};
});
// 提取项目编号到mappedData
if (singleContractMultiCustomerStr.length > 0) {
const firstRow = singleContractMultiCustomerStr[0];
const projectNumber = pickRowField(firstRow.rowValue || [], ['项目编号']) || firstRow.rowValue?.[0]?.value || '';
if (projectNumber) {
mappedData.projectNumber = projectNumber;
}
}
mappedData.singleContractMultiCustomerInvoice = singleContractMultiCustomerStr.map(row => {
const rv = row.rowValue;
const rowField = (candidates: string[], fallbackIndex?: number) => {
const valueByLabel = pickRowField(rv, candidates);
if (valueByLabel !== '' && valueByLabel !== undefined && valueByLabel !== null) {
return valueByLabel;
}
if (fallbackIndex !== undefined) {
return rv?.[fallbackIndex]?.value ?? '';
}
return '';
};
const thirdPartyCost = pickRowField(rv, ['预计报销第三方成本(不含税,元)','预计报销第三方成本(元)','预计报销第三方成本']);
const detail: InvoiceApplicationAmountDetails = {
dataid: processInstanceId,
agreementType: agreementTypeCode,
project_number: rowField(['项目编号'], 0),
project_name: rowField(['项目名称'], 1),
customer_company_name: rowField(['付款单位名称', '客户单位名称'], 2),
tong_yi_she_hui_xin_yong_dai_ma: rowField(['统一社会信用代码'], 3),
bank_name: rowField(['开户行名称'], 4),
bank_account: rowField(['银行账号'], 5),
business_content: rowField(['开票事由', '业务内容'], 6),
invoice_amount: rowField(['开票金额(元)', '开票金额(元)', '开票金额'], 7),
payee: rowField(['付款人'], 8),
yu_ji_bao_xiao_di_san_fang_cheng_ben: thirdPartyCost,
};
return detail;
});
// 将生成的主表记录保存到mappedData中
mappedData.singleContractMultiCustomerMainRecords = singleContractMultiCustomerMainRecords;
return {
...mappedData,
dataId: processInstanceId,
agreementType: agreementTypeCode,
invoiceType: invoiceTypeName || null,
invoiceCode: invoiceCode,
approvalStatus: approvalStatus,
approvalNumber: businessId,
approvalResult: '审批通过',
};
});
console.log("mappedDatas", mappedDatas)
logger.info('importInvoiceApprovalData mappedDatas', { count: mappedDatas.length, sampleKeys: mappedDatas[0] ? Object.keys(mappedDatas[0]).slice(0, 20) : [] })
try {
if (tableName === 'invoice_void_record') {
const recordsForInsert = mappedDatas.map(item => {
const { BigInvoiceAmountList, multiProjectMergeInvoice, singleContractMultiCustomerInvoice, multiProjectMergeMainRecords, singleContractMultiCustomerMainRecords, ...rest } = item;
const tableFieldMappings = FIELD_MAPPINGS[tableName as keyof typeof FIELD_MAPPINGS];
const idKeyNameLocal = DATA_ID_FIELD_MAPPINGS[tableName];
const filteredData: any = {};
// 只保留在字段映射中定义的字段
Object.keys(rest).forEach(key => {
if (Object.values(tableFieldMappings).includes(key)) {
filteredData[key] = rest[key];
}
});
// 确保 dataId/data_id 字段存在
if (!filteredData[idKeyNameLocal] && item[idKeyNameLocal]) {
filteredData[idKeyNameLocal] = item[idKeyNameLocal];
}
return filteredData;
});
logger.info('importInvoiceApprovalData void main records', { count: recordsForInsert.length, sampleKeys: recordsForInsert[0] ? Object.keys(recordsForInsert[0]).slice(0, 20) : [] })
await prismaClient[tableName].createMany({
data: recordsForInsert,
skipDuplicates: true,
});
const voidInfoByPiaoHao = new Map<string, { remark?: string; cancelReason?: string; canceldataid?: string; invoice_amount_yuan?: string; hong_chong_piao_hao?: string }>();
for (const r of recordsForInsert) {
if (r.piao_hao) {
voidInfoByPiaoHao.set(r.piao_hao, {
remark: r.remark,
cancelReason: r.cancelReason,
canceldataid: r.dataId,
invoice_amount_yuan: r.invoice_amount_yuan,
hong_chong_piao_hao: r.hong_chong_piao_hao
});
}
}
const piaoHaos = recordsForInsert.map(r => r.piao_hao).filter(Boolean);
const foundInvoices = piaoHaos.length > 0 ? await prismaClient.invoice_data.findMany({
where: { piao_hao: { in: piaoHaos } },
select: { dataid: true, piao_hao: true }
}) : [];
const updateOriginals: any[] = [];
const negativeRecords: any[] = [];
const normalizeAmount = (val: any): string => {
if (val === null || val === undefined) return '';
if (typeof val === 'number') {
if (isNaN(val)) return '';
return Number(val).toFixed(2);
}
const s = String(val).trim();
if (!s) return '';
const neg = /^\(.+\)$/.test(s);
let cleaned = s.replace(/[,\s]/g, '');
cleaned = cleaned.replace(/[¥¥元]/g, '');
cleaned = cleaned.replace(/^[\(]|[\)]$/g, '');
const num = Number(cleaned);
if (isNaN(num)) return '';
const v = neg ? -num : num;
return v.toFixed(2);
};
for (const fi of foundInvoices) {
const info = voidInfoByPiaoHao.get(fi.piao_hao);
// 从dingding_oa_invoice_application表获取发票金额信息
const invoiceApp = await prismaClient.dingding_oa_invoice_application.findFirst({
where: { dataId: fi.dataid },
select: {
invoice_amount_yuan: true,
invoice_amount_yuan_du: true,
invoice_amount_yuan_other: true,
invoice_amount_yuan_da: true,
han_shui_jin_e_yuan_du: true,
han_shui_jin_e_yuan_other: true,
han_shui_jin_e_yuan_duo: true,
han_shui_zong_jin_e_yuan_da: true,
han_shui_zong_jin_e_yuan_da_du: true,
bu_han_shui_jin_e_yuan_du: true,
bu_han_shui_jin_e_yuan_other: true,
bu_han_shui_jin_e_yuan_da: true,
bu_han_shui_jin_e_yuan_da_du: true,
bu_han_shui_jin_e_yuan_duo: true,
}
});
// 使用第一个非空的金额字段作为目标
const targetFields = [
invoiceApp?.invoice_amount_yuan,
invoiceApp?.invoice_amount_yuan_du,
invoiceApp?.invoice_amount_yuan_other,
invoiceApp?.invoice_amount_yuan_da,
invoiceApp?.han_shui_jin_e_yuan_du,
invoiceApp?.han_shui_jin_e_yuan_other,
invoiceApp?.han_shui_jin_e_yuan_duo,
invoiceApp?.han_shui_zong_jin_e_yuan_da,
invoiceApp?.han_shui_zong_jin_e_yuan_da_du,
invoiceApp?.bu_han_shui_jin_e_yuan_du,
invoiceApp?.bu_han_shui_jin_e_yuan_other,
invoiceApp?.bu_han_shui_jin_e_yuan_da,
invoiceApp?.bu_han_shui_jin_e_yuan_da_du,
invoiceApp?.bu_han_shui_jin_e_yuan_duo,
];
const target = targetFields.find(f => f && f.trim()) || '';
if (!target) continue;
const candidates = await prismaClient.dingding_oa_invoice_application.findMany({
where: { dataId: fi.dataid },
select: {
invoiceJobNumber: true,
dataId: true,
invoice_amount_yuan: true,
invoice_amount_yuan_du: true,
invoice_amount_yuan_other: true,
invoice_amount_yuan_da: true,
han_shui_jin_e_yuan_du: true,
han_shui_jin_e_yuan_other: true,
han_shui_jin_e_yuan_duo: true,
han_shui_zong_jin_e_yuan_da: true,
han_shui_zong_jin_e_yuan_da_du: true,
bu_han_shui_jin_e_yuan_du: true,
bu_han_shui_jin_e_yuan_other: true,
bu_han_shui_jin_e_yuan_da: true,
bu_han_shui_jin_e_yuan_da_du: true,
bu_han_shui_jin_e_yuan_duo: true,
}
});
const match = candidates.filter(c => {
const fields = [
c.invoice_amount_yuan,
c.invoice_amount_yuan_du,
c.invoice_amount_yuan_other,
c.invoice_amount_yuan_da,
c.han_shui_jin_e_yuan_du,
c.han_shui_jin_e_yuan_other,
c.han_shui_jin_e_yuan_duo,
c.han_shui_zong_jin_e_yuan_da,
c.han_shui_zong_jin_e_yuan_da_du,
c.bu_han_shui_jin_e_yuan_du,
c.bu_han_shui_jin_e_yuan_other,
c.bu_han_shui_jin_e_yuan_da,
c.bu_han_shui_jin_e_yuan_da_du,
c.bu_han_shui_jin_e_yuan_duo,
];
const hasMatch = fields.some(f => {
const normalized = normalizeAmount(f);
const targetNormalized = normalizeAmount(target);
const isMatch = normalized && targetNormalized && normalized === targetNormalized;
if (isMatch) {
console.log(`匹配成功: 字段值=${f}, 规范化后=${normalized}, 目标=${target}, 目标规范化后=${targetNormalized}`);
}
return isMatch;
});
if (!hasMatch && candidates.length > 0) {
console.log(`匹配失败: 目标金额=${target}, 目标规范化后=${normalizeAmount(target)}, 候选字段值=`, fields.map(f => `${f}->${normalizeAmount(f)}`));
}
return hasMatch;
});
if (match.length === 0) {
throw new Error(`不存在匹配记录: 票号=${fi.piao_hao}, 金额=${target}`);
} else if (match.length > 1) {
throw new Error(`存在多条匹配记录,无法唯一定位: 票号=${fi.piao_hao}, 金额=${target}`);
} else {
const m = match[0] as any;
const data: any = { remark: '发票作废' };
if (info?.cancelReason) data.cancelReason = info.cancelReason;
if (info?.canceldataid) data.canceldataid = info.canceldataid;
updateOriginals.push(
prismaClient.dingding_oa_invoice_application.update({
where: {
invoiceJobNumber_dataId: {
invoiceJobNumber: m.invoiceJobNumber,
dataId: m.dataId
}
},
data
})
);
const neg: any = { dataId: m.dataId };
// 复制所有可用字段,排除不需要的字段
const fieldsToExclude = new Set([
'invoiceJobNumber', 'dataId', 'BigInvoiceAmountList', 'multiProjectMergeInvoice',
'singleContractMultiCustomerInvoice', 'multiProjectMergeMainRecords',
'singleContractMultiCustomerMainRecords'
]);
// 复制所有字段
Object.keys(m).forEach(k => {
if (!fieldsToExclude.has(k) && m[k] !== undefined) {
neg[k] = m[k];
}
});
// 设置作废相关字段
neg['remark'] = '发票作废';
// 添加红冲发票号(从作废申请中获取)
if (info?.hong_chong_piao_hao) {
neg['hong_chong_piao_hao'] = info.hong_chong_piao_hao;
}
const amountFields = [
'invoice_amount_yuan','invoice_amount_yuan_du','invoice_amount_yuan_other',
'bu_han_shui_jin_e_yuan_du','han_shui_jin_e_yuan_du',
'bu_han_shui_jin_e_yuan_other','han_shui_jin_e_yuan_other',
'bu_han_shui_jin_e_yuan_duo','han_shui_jin_e_yuan_duo',
'bu_han_shui_jin_e_yuan_da','han_shui_zong_jin_e_yuan_da',
'bu_han_shui_jin_e_yuan_da_du','han_shui_zong_jin_e_yuan_da_du',
'yuJiBaoXiaoDiSanFangChengBenYuan'
];
amountFields.forEach(f => {
const raw = (m as any)[f];
const num = typeof raw === 'string' && raw.trim() !== '' ? parseFloat(raw.replace(/,/g, '')) : NaN;
if (!Number.isNaN(num)) {
const negVal = num > 0 ? -num : num;
(neg as any)[f] = negVal.toString();
}
});
negativeRecords.push(neg);
}
}
if (updateOriginals.length > 0 || negativeRecords.length > 0) {
await prismaClient.$transaction([
...updateOriginals,
...(negativeRecords.length > 0 ? [prismaClient.dingding_oa_invoice_application.createMany({
data: negativeRecords,
skipDuplicates: true
})] : [])
]);
}
} else {
const mainTableMappings = FIELD_MAPPINGS[tableName as keyof typeof FIELD_MAPPINGS] || {};
const mainAllowedKeys = new Set<string>([
...Object.values(mainTableMappings),
DATA_ID_FIELD_MAPPINGS[tableName as keyof typeof DATA_ID_FIELD_MAPPINGS]
].filter(Boolean) as string[]);
const sanitizeMain = (rec: any) => {
const out: any = {};
for (const key of mainAllowedKeys) {
if (rec && Object.prototype.hasOwnProperty.call(rec, key)) {
out[key] = rec[key];
}
}
const idKeyNameLocal = DATA_ID_FIELD_MAPPINGS[tableName as keyof typeof DATA_ID_FIELD_MAPPINGS];
if (idKeyNameLocal && rec?.[idKeyNameLocal] && !out[idKeyNameLocal]) {
out[idKeyNameLocal] = rec[idKeyNameLocal];
}
return out;
};
const mainRecords = [
...mappedDatas.filter(item => (
!item.singleContractMultiCustomerMainRecords || item.singleContractMultiCustomerMainRecords.length === 0
)).map(item => {
const { BigInvoiceAmountList, multiProjectMergeInvoice, singleContractMultiCustomerInvoice, multiProjectMergeMainRecords, singleContractMultiCustomerMainRecords, ...rest } = item;
return sanitizeMain(rest);
}),
...mappedDatas.flatMap(item => {
const records: any[] = [];
if (item.singleContractMultiCustomerMainRecords && item.singleContractMultiCustomerMainRecords.length > 0) {
records.push(...item.singleContractMultiCustomerMainRecords.map(record => {
const { BigInvoiceAmountList, multiProjectMergeInvoice, singleContractMultiCustomerInvoice, multiProjectMergeMainRecords, singleContractMultiCustomerMainRecords, ...filteredRecord } = record;
return sanitizeMain(filteredRecord);
}));
}
return records;
})
].filter(r => r && Object.keys(r).length > 0);
const detailAllowedKeys = new Set<string>([
'dataid','agreementType','project_number','business_group_name','project_name','customer_company_name','tong_yi_she_hui_xin_yong_dai_ma','bank_name','bank_account','business_content','invoice_amount','payee','yu_ji_bao_xiao_di_san_fang_cheng_ben'
]);
const detailRecords = mappedDatas.flatMap(item => [
...item.BigInvoiceAmountList,
...item.multiProjectMergeInvoice,
...(item.singleContractMultiCustomerInvoice || []),
]).map(rec => {
const out: any = {};
for (const key of detailAllowedKeys) {
if (rec && Object.prototype.hasOwnProperty.call(rec, key)) {
out[key] = rec[key];
}
}
return out;
}).filter(r => r && Object.keys(r).length > 0);
const txOps: any[] = [];
if (mainRecords.length > 0) {
txOps.push(prismaClient[tableName].createMany({
data: mainRecords,
skipDuplicates: true,
}));
}
if (detailRecords.length > 0) {
txOps.push(prismaClient.invoice_amount_details.createMany({
data: detailRecords,
skipDuplicates: true,
}));
}
if (txOps.length > 0) {
await prismaClient.$transaction(txOps);
}
}
} catch (error) {
console.error('插入数据时出错:', error);
logger.error('插入数据时出错:', { error })
return res.status(400).json({ code:400, message: `数据插入到${tableName}表中失败`, error: error, })
}
}
if (newInstanceIds.length === 0) {
logger.info(`本次无新增数据插入到${tableName}表; count: 0; []`,)
return res.status(200).json({code:0,message: `本次无新增数据`, newInstanceIds })
}
logger.info(`数据处理完成: ${tableName}; 待处理实例数: ${newInstanceIds.length}; ${JSON.stringify(newInstanceIds)}`,)
return res.status(200).json({code:0,message: `数据处理完成`, newInstanceIds })
} catch (error) {
console.error('handler:', error);
logger.error('获取 token 时出错:', { error })
return res.status(500).json({ code:500, message: '获取 token 时出错' });
}
}

View File

@@ -0,0 +1,53 @@
import { get } from 'lodash'
import OAClient from '@/api/dingding/oa'
export async function getInstanceIdList({
accessToken,
startTime,
endTime,
maxResults,
processCode
}: {
accessToken: string
startTime: number
endTime: number
maxResults: number
processCode: string
}) {
let allList = [];
let nextToken: number = 0;
const lpiRes = await OAClient.listProcessInstanceIdsWithOptions({
processCode: processCode,
startTime: startTime,
endTime: endTime,
nextToken: nextToken,
maxResults: maxResults,
dingtalkAccessToken: accessToken,
statuses: ['COMPLETED']
}) as any;
const list = get(lpiRes, 'body.result.list', [])
nextToken = get(lpiRes, 'body.result.nextToken', undefined)
allList.push(...list)
// 不为空表示有更多数据。
while (nextToken) {
const lpiRes = await OAClient.listProcessInstanceIdsWithOptions({
processCode: processCode,
startTime: startTime,
endTime: endTime,
nextToken: nextToken,
maxResults: maxResults,
dingtalkAccessToken: accessToken,
statuses: ['COMPLETED']
}) as any;
// TODO 如果有问题 这里可以尝试等待
// const { list = [], nextToken: newNextToken } = lpiRes?.body?.result ? lpiRes?.body?.result : {};
const list = get(lpiRes, 'body.result.list', [])
nextToken = get(lpiRes, 'body.result.nextToken', undefined)
allList = [...allList, ...list];
console.log("listProcessInstanceIdsWithOptions", list, nextToken);
}
return allList;
}

View File

@@ -0,0 +1,13 @@
import type { NextApiRequest, NextApiResponse } from 'next'
type ResponseData = {
data: boolean;
success: boolean;
};
export default function handler(
req: NextApiRequest,
res: NextApiResponse<ResponseData>
) {
res.status(200).json({ data: true, success: true });
}

View File

@@ -0,0 +1,87 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { withFileUpload } from 'next-multiparty';
import * as XLSX from 'xlsx';
import { InvoiceApplicationItem } from '../type'
import prismaClient from '@/lib/prisma'
const ExcelToMySQLFieldMapping = {
'序号': 'xuhao',
'数据id': 'data_id',
'项目号': 'project_number',
'日期': 'date',
'凭证号': 'voucher_number',
'分录号': 'entry_number',
'摘要': 'summary',
'科目编码': 'account_code',
'科目名称': 'account_name',
'辅助项': 'auxiliary_items',
'币种': 'currency',
'借方原币': 'debit_original_currency',
'借方本币': 'debit_local_currency',
'贷方原币': 'credit_original_currency',
'贷方本币': 'credit_local_currency',
'SR号': 'sr_number',
'共享凭证号': 'shared_voucher_number',
'费用预算项目': 'expense_budget_item',
'成本中心': 'cost_center',
'标准名称': 'standard_name',
'一级费用类型': 'expense_type_level1',
'二级费用类型': 'expense_type_level2',
'三级费用类型': 'expense_type_level3',
'金额': 'amount',
'创建时间': 'created_time',
'创建者': 'creator',
'创建人部门': 'creator_department',
'审批单标题': 'approval_title',
'更新时间': 'updated_time'
}
export const config = {
api: {
bodyParser: false // 必须禁用默认bodyParser
}
};
export default withFileUpload(async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
try {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
// 2. 读取Excel文件
const fileBuffer = await req?.file?.toBuffer()
const workbook = XLSX.read(fileBuffer, { type: 'buffer' });
const firstSheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[firstSheetName];
const rawData: InvoiceApplicationItem[] = XLSX.utils.sheet_to_json(worksheet);
// 3. 数据处理和去重
let expenseReimbursementDataList = [];
// TODO 可以查询一次搞重新修改 看业务需求
for (let i = 0; i < rawData.length; i++) {
const rawItem = rawData[i];
const expenseReimbursementData: any = {
data_id: rawItem['数据id'],
};
for (const [excelKey, dbKey] of Object.entries(ExcelToMySQLFieldMapping)) {
expenseReimbursementData[dbKey] = rawItem[excelKey];
}
expenseReimbursementDataList.push(expenseReimbursementData);
}
// 插入数据到expense_reimbursement表
await prismaClient.expense_reimbursement.createMany({
data: expenseReimbursementDataList,
skipDuplicates: true, // 跳过重复数据
});
res.status(200).json({
code: 0,
data: null,
message: '费用报销数据导入成功'
});
} catch (error) {
res.status(500).json({ error: 'Internal server error', stack: error, });
}
})

View File

@@ -0,0 +1,60 @@
/**
* @description
* 技术方案:
* https://docs.dingtalk.com/i/nodes/Obva6QBXJw6YEvDyila56vb0Vn4qY5Pr
*/
import dayjs from 'dayjs'
import type { NextApiRequest, NextApiResponse } from 'next'
import AuthClient from '../../api/dingding/auth'
import OAClient from '../../api/dingding/oa'
const PROCESS_CODE = process.env.DINGDING_INVOICE_APPROVAL_PROCESS_CODE ?? ''
type ResponseData = {
message: string
testjsonres?: string
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<ResponseData>
) {
try {
console.log("启动")
// 1 获得token
const accessToken = await AuthClient.getAccessToken();
console.log("获得token", accessToken);
// 2 拿到所有的实例id
const startTime = dayjs().subtract(60, 'day').valueOf();
const lpiRes = await OAClient.listProcessInstanceIdsWithOptions({
processCode: PROCESS_CODE,
startTime: startTime,
endTime: undefined,
nextToken: 0,
maxResults: 5,
dingtalkAccessToken: accessToken,
}) as any; // TODO
const { list = [], nextToken } = lpiRes?.body?.result ? lpiRes?.body?.result : {}
// TODO List
// 根据processid 拿到
let testjsonres = null
if (list[0]) {
const pinstance = await OAClient.getProcessInstanceWithOptions({
processInstanceId: list[0],
dingtalkAccessToken: accessToken
})
testjsonres = pinstance;
console.log("---------------------------------")
console.log("pinstance", JSON.stringify(pinstance))
console.log("---------------------------------")
}
console.log("listProcessInstanceIdsWithOptions", list, nextToken)
// 3
res.status(200).json({ message: `Hello from Next.js! `, testjsonres: testjsonres })
} catch (error) {
console.error('handler:', error);
res.status(500).json({ message: '获取 token 时出错' });
}
}

View File

@@ -0,0 +1,167 @@
import type { NextApiRequest, NextApiResponse } from 'next'
import prismaClient from '@/lib/prisma'
import { logger } from '@/api/winston/logger'
interface RequestBody {
cancelType: number
cancelReason: number
dataId: string
piaoHao: string
remarks: string
invoiceAmount?: string | number
}
interface ResponseData {
code: number
message: string
data?: any
error?: any
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<ResponseData>
) {
try {
// 验证请求方法
if (req.method !== 'POST') {
return res.status(405).json({
code: 405,
message: 'Method not allowed',
})
}
// 验证请求体参数
const { cancelType,cancelReason, dataId, piaoHao, remarks, invoiceAmount }: RequestBody = req.body
if (!piaoHao) {
return res.status(400).json({
code: 400,
message: 'piaoHao为必填项',
})
}
const normalizeAmount = (val: any): string => {
if (val === null || val === undefined) return ''
if (typeof val === 'number') {
if (isNaN(val)) return ''
return String(val)
}
const s = String(val).trim()
if (!s) return ''
const neg = /^\(.+\)$/.test(s)
let cleaned = s.replace(/[,\s]/g, '')
cleaned = cleaned.replace(/[¥¥元]/g, '')
cleaned = cleaned.replace(/^[\(]|[\)]$/g, '')
const num = Number(cleaned)
if (isNaN(num)) return ''
return String(neg ? -num : num)
}
const targetAmountStr = normalizeAmount(invoiceAmount)
if (!targetAmountStr) {
return res.status(400).json({
code: 400,
message: '缺少有效的发票金额,用于唯一定位主表记录'
})
}
// 开始数据库事务
await prismaClient.$transaction(async (prisma) => {
// 0. 如果未传 dataId则根据票号查询
let resolvedDataId = dataId
if (!resolvedDataId) {
const inv = await prisma.invoice_data.findUnique({
where: { piao_hao: piaoHao }
})
resolvedDataId = inv?.dataid || ''
}
if (!resolvedDataId) {
throw new Error('根据票号未找到对应的 dataId')
}
// 1. 更新invoice_data表中的invoice_type_enum字段按票号定位更稳妥
await prisma.invoice_data.update({
where: { piao_hao: piaoHao },
data: {
invoice_type_enum: cancelType,
updated_at: new Date(),
},
})
// 2. 在发票作废记录表中添加操作记录
await prisma.invoice_void_record.create({
data: {
dataId: resolvedDataId,
piao_hao: piaoHao,
cancelReason: String(cancelReason),
zuo_fei_shi_you: remarks || '',
},
})
// 3. 更新主表dataId 下可能多条记录,需要按发票金额匹配
const candidates = await prisma.dingding_oa_invoice_application.findMany({
where: { dataId: resolvedDataId },
select: {
invoiceJobNumber: true,
dataId: true,
invoice_amount_yuan_du: true
}
})
const normalizeToFixed2 = (s?: string | null) => {
const n = normalizeAmount(s ?? '')
if (!n) return ''
return Number(n).toFixed(2)
}
const targetFixed = Number(targetAmountStr).toFixed(2)
const isMatch = (c: any) => {
const fields = [
c.invoice_amount_yuan_du
]
return fields.some(f => normalizeToFixed2(f) === targetFixed)
}
const matches = candidates.filter(isMatch)
if (matches.length === 0) {
throw new Error('未找到与发票金额匹配的主表记录')
}
if (matches.length > 1) {
throw new Error('存在多条金额匹配记录,无法唯一定位,请提供更明确的金额或其他限定条件')
}
const m = matches[0]
await prisma.dingding_oa_invoice_application.update({
where: {
// 复合主键
invoiceJobNumber_dataId: {
invoiceJobNumber: m.invoiceJobNumber,
dataId: m.dataId
}
},
data: {
remark: '发票作废',
cancelReason: String(cancelReason)
}
})
logger.info(`发票作废成功: dataId=${resolvedDataId}, piaoHao=${piaoHao}, amount=${targetAmountStr}`)
})
return res.status(200).json({
code: 0,
message: '发票作废成功',
data: {
dataId: dataId,
piaoHao,
cancelType,
remarks,
invoiceAmount: invoiceAmount ?? null
},
})
} catch (error) {
logger.error('发票作废失败:', error)
return res.status(500).json({
code: 500,
message: '服务器内部错误',
error: error instanceof Error ? error.message : 'Unknown error',
})
}
}

View File

@@ -0,0 +1,2 @@
// 发票类型:从开票申请中获取
const fa_piao_lei_xing = item.invoiceType || '';

View File

@@ -0,0 +1,4 @@
// ... existing code ...
// 发票类型:从开票申请中获取
const fa_piao_lei_xing = item.invoiceType || '';
// ... existing code ...

View File

@@ -0,0 +1,241 @@
import type { NextApiRequest, NextApiResponse } from 'next'
import prismaClient from '@/lib/prisma'
import { AGREEMENT_TYPES, AGREEMENT_TYPE_CODE } from '@/CONSTANTS'
import { processInvoiceDataItem } from '@/lib/util'
type ResponseData = {
code: number
message: string
data?: {
data: any;
pagination?: {
total: number
pageNum: number
pageSize: number
totalPages: number
}
}
error?: any
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<ResponseData>
) {
try {
if (req.method != 'POST') {
return res.status(400).json({ message: '请求方法错误' });
}
// 从查询参数获取分页信息,设置默认值
const pageNum = parseInt(req.body.pageNum as string) || 1;
const pageSize = parseInt(req.body.pageSize as string) || 10;
const skip = (pageNum - 1) * pageSize;
const { agreementType, projectNumber, customerCompanyName, joinType = 'left', issueStatus = 'unissued' } = req.body;
const whereConditions: any = {};
if (agreementType) {
if (agreementType === '1') {
whereConditions.agreementType = { in: ['1', '3'] };
} else if (agreementType === '2') {
whereConditions.agreementType = { in: ['2', '4'] };
} else {
whereConditions.agreementType = agreementType as string;
}
}
if (projectNumber) {
whereConditions.projectNumber = {
contains: projectNumber as string,
};
}
if (customerCompanyName) {
whereConditions.customerCompanyName = {
contains: customerCompanyName as string,
};
}
// 仅查询非作废票
// whereConditions.invoice_type_enum = 0;
const voidRecords = await prismaClient.dingding_oa_invoice_application.findMany({
where: { remark: '发票作废' },
select: { dataId: true, invoiceJobNumber: true }
});
const voidPairs = voidRecords
.filter(r => r.dataId && r.invoiceJobNumber)
.map(r => ({ dataId: r.dataId as string, invoiceJobNumber: r.invoiceJobNumber as string }));
if (voidPairs.length > 0) {
whereConditions.AND = [
...(whereConditions.AND || []),
{
NOT: {
OR: voidPairs.map(pair => ({
dataId: pair.dataId,
invoiceJobNumber: pair.invoiceJobNumber
}))
}
}
];
}
// 已开票/未开票/全部
if (issueStatus === 'issued') {
whereConditions.AND = [
...(whereConditions.AND || []),
{ piao_hao: { not: '' } },
{ NOT: { piao_hao: null } }
];
} else if (issueStatus === 'unissued') {
whereConditions.OR = [
...(whereConditions.OR || []),
{ piao_hao: '' },
{ piao_hao: null }
];
}
if (isNaN(pageNum) || isNaN(pageSize) || pageNum < 1 || pageSize < 1) {
return res.status(400).json({ message: '无效的分页参数' });
}
// 如果是inner 那么是完成开票结果表,否则是全量数据
const viewModel = joinType === 'inner' ?
prismaClient.invoice_application_match_inner_view :
prismaClient.invoice_application_match_view;
// 获取分页数据
const invoiceData = await viewModel.findMany({
where: whereConditions,
skip,
take: pageSize,
orderBy: [
{ invoiceJobNumber: 'asc' }
],
});
// 统计总数:已开票按票号去重后的总数,其它保持原逻辑
const total = (issueStatus === 'issued')
? (await viewModel.findMany({
where: whereConditions,
select: { piao_hao: true },
distinct: ['piao_hao'],
})).length
: await viewModel.count({
where: whereConditions
});
// 提取大协议的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}`);
}
});
// 在这里增加逻辑
// 1 增加 customerCompanyName 取自 invoiceData.customerCompanyNameLargeAgreement如果协议类型是大协议 和 invoiceData.customerCompanyNameIndependentAgreement 如果协议类型是独立协议
// 2 账期 invoice_period 是 开票日期和回款日期两个字段都存在 那么账期= 回款日期 payment_date -开票日期 invoice_date
const mappedRows = invoiceData
.map((ele) => {
// 添加调试日志
// console.log('原始数据:', {
// agreementType: ele.agreementType,
// is_single_contract_multi_customer: ele.is_single_contract_multi_customer,
// bu_han_shui_jin_e_yuan_duo: ele.bu_han_shui_jin_e_yuan_duo,
// han_shui_jin_e_yuan_duo: ele.han_shui_jin_e_yuan_duo
// });
const result = processInvoiceDataItem(ele, businessContentMap);
if (String(ele.dataId || '') === 'ggxiANvuSHy_Vz0mOR_38Q08211767677530') {
console.log('[invoice-amount-debug]', {
dataId: ele.dataId,
invoiceJobNumber: ele.invoiceJobNumber,
agreementType: ele.agreementType,
is_single_contract_multi_customer: ele.is_single_contract_multi_customer,
is_multi_project_merge: ele.is_multi_project_merge,
bu_han_shui_jin_e_yuan_du: ele.bu_han_shui_jin_e_yuan_du,
bu_han_shui_jin_e_yuan_duo: ele.bu_han_shui_jin_e_yuan_duo,
han_shui_jin_e_yuan_du: ele.han_shui_jin_e_yuan_du,
han_shui_jin_e_yuan_duo: ele.han_shui_jin_e_yuan_duo,
result_bu_han_shui_jin_e: result.bu_han_shui_jin_e,
result_han_shui_zong_jin_e: result.han_shui_zong_jin_e
});
}
// console.log('处理结果:', {
// bu_han_shui_jin_e: result.bu_han_shui_jin_e,
// han_shui_zong_jin_e: result.han_shui_zong_jin_e
// });
return result;
})
// 添加去重逻辑:基于 projectNumber 去重,确保每个项目编号只出现一次
// .filter((item, index, array) =>
// array.findIndex(i => i.projectNumber === item.projectNumber) === index
// )
// 兜底:将没有票号的记录排在前面,其余按开票作业序号升序(防止字符串排序或空值干扰)
const dedupByPiaoHao = (rows: any[]) => {
const chosen = new Map<string, any>();
for (const r of rows) {
const key = String(r.piao_hao || '').trim();
if (!key) continue;
const prev = chosen.get(key);
const currNo = Number(r.invoiceJobNumber) || 0;
const prevNo = Number(prev?.invoiceJobNumber) || 0;
if (!prev || currNo > prevNo) {
chosen.set(key, r);
}
}
const withTicket = Array.from(chosen.values());
const noTicket = rows.filter(r => !String(r.piao_hao || '').trim());
return [...noTicket, ...withTicket];
};
const rowsForSort = issueStatus === 'issued' ? dedupByPiaoHao(mappedRows) : mappedRows;
const modifiedInvoiceData = rowsForSort.sort((a, b) => {
const aNoTicket = !a.piao_hao || String(a.piao_hao).trim() === '';
const bNoTicket = !b.piao_hao || String(b.piao_hao).trim() === '';
if (aNoTicket && !bNoTicket) return -1;
if (!aNoTicket && bNoTicket) return 1;
const numA = Number(a.invoiceJobNumber) || 0;
const numB = Number(b.invoiceJobNumber) || 0;
return numA - numB;
});
// 拉取到开票信息之后,再根据项目信息做统计来生成应收账款台账
res.status(200).json({
code: 0,
error: null,
message: '数据获取成功',
data: {
data: modifiedInvoiceData,
pagination: {
total,
pageNum,
pageSize,
totalPages: Math.ceil(total / pageSize)
}
},
});
} catch (error) {
console.error('handler:', error);
res.status(500).json({ message: '获取数据时出错' });
}
}

View File

@@ -0,0 +1,213 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { withFileUpload } from 'next-multiparty';
import * as XLSX from 'xlsx';
import { InvoiceDataDBItem } from '../type'
import prismaClient from '@/lib/prisma'
import dayjs from 'dayjs'
import { logger } from '@/api/winston/logger';
const normalizeAmount = (val: any): string => {
if (val === null || val === undefined) return '0'
if (typeof val === 'number') {
if (isNaN(val)) return '0'
return String(val)
}
const s = String(val).trim()
if (!s) return '0'
const neg = /^\(.+\)$/.test(s)
let cleaned = s.replace(/[,\s]/g, '')
cleaned = cleaned.replace(/[¥¥元]/g, '')
cleaned = cleaned.replace(/^[\(]|[\)]$/g, '')
const num = Number(cleaned)
if (isNaN(num)) return '0'
return String(neg ? -num : num)
}
// 添加日期转换函数
const convertExcelDate = (excelDate: any): string => {
if (!excelDate) return '';
// 如果是数字Excel序列号
if (typeof excelDate === 'number') {
// Excel日期从1900年1月1日开始但Excel有个bug1900年被当作闰年
const excelEpoch = new Date(1900, 0, 1);
const date = new Date(excelEpoch.getTime() + (excelDate - 1) * 24 * 60 * 60 * 1000);
return dayjs(date).format('YYYY/MM/DD');
}
// 如果是字符串,直接返回
return String(excelDate);
};
export const config = {
api: {
bodyParser: false // 必须禁用默认bodyParser
}
};
export default withFileUpload(async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
try {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
// 读取Excel文件
const fileBuffer = await req?.file?.toBuffer()
if (!fileBuffer) {
return res.status(400).json({ error: '未提供文件' });
}
const workbook = XLSX.read(fileBuffer, { type: 'buffer' });
const firstSheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[firstSheetName];
const rawData: {
数据ID: string;
项目编号: string;
单位: string;
集团内外: string;
票号: string;
不含税金额: string;
税额: string;
含税金额: string;
申请人: string;
专业组: string;
开票日期: string;
账期: string;
回款日期: string;
回款金额: string;
业务内容: string;
}[] = XLSX.utils.sheet_to_json(worksheet);
// 数据校验和转换
let datas: InvoiceDataDBItem[] = [];
const validationErrors: string[] = [];
for (let i = 0; i < rawData.length; i++) {
const rawItem = rawData[i];
const rowNumber = i; // Excel行号从2开始第1行是表头
// 基础数据校验
if (!rawItem['票号'] || rawItem['票号'].trim() === '') {
validationErrors.push(`${rowNumber}行:票号为必填项`);
continue;
}
// if (isNaN(Number(rawItem['回款金额']))) {
// validationErrors.push(`第${rowNumber}行:回款金额必须为数字`);
// continue;
// }
// if (isNaN(Number(rawItem['不含税金额']))) {
// validationErrors.push(`第${rowNumber}行:不含税金额必须为数字`);
// continue;
// }
// if (isNaN(Number(rawItem['税额']))) {
// validationErrors.push(`第${rowNumber}行:税额必须为数字`);
// continue;
// }
// if (isNaN(Number(rawItem['含税金额']))) {
// validationErrors.push(`第${rowNumber}行:含税金额必须为数字`);
// continue;
// }
const kai_piao_ri_qi = convertExcelDate(rawItem['开票日期'])
const hui_kuan_ri_qi = convertExcelDate(rawItem['回款日期'])
// 账期计算:优先使用导入文件中的"账期",否则按日期差计算
let zhang_qi = '';
const importedZhangQi = (rawItem['账期'] ?? '').toString().trim();
if (importedZhangQi) {
zhang_qi = importedZhangQi;
} else if (rawItem['开票日期'] && rawItem['回款日期']) {
// 两个日期都存在:有回款日期时,账期应该为空
zhang_qi = '';
} else if (rawItem['开票日期'] && !rawItem['回款日期']) {
// 只有开票日期:账期 = 当前日期 - 开票日期(单位:天)
const kp = dayjs(convertExcelDate(rawItem['开票日期']));
const now = dayjs();
if (kp.isValid()) {
const dayDiff = now.diff(kp, 'day');
zhang_qi = dayDiff.toString();
} else {
logger.error(`${rowNumber}行:开票日期格式错误,开票日期=${rawItem['开票日期']}`)
}
}
// 转换为数据库模型
datas.push({
dataid: `${rawItem['数据ID']}`,
piao_hao: `${rawItem['票号']}`,
kai_piao_ri_qi: `${kai_piao_ri_qi}`,
hui_kuan_ri_qi: `${hui_kuan_ri_qi}`,
hui_kuan_jin_e: normalizeAmount(rawItem['回款金额']),
zhang_qi: zhang_qi,
invoice_type_enum: 0,
customer_company_name: `${rawItem['单位']}`
});
}
// 返回校验错误
if (validationErrors.length > 0) {
return res.status(400).json({
message: '数据校验失败',
errors: validationErrors
});
}
let createCount = 0;
let updateCount = 0;
// 数据库存在性校验和upsert操作
await prismaClient.$transaction(async (prisma) => {
const dataIds = datas.map(item => item.dataid);
// 批量查询已存在记录
const existingRecords = await prisma.invoice_data.findMany({
where: {
OR: datas.map(item => ({
piao_hao: item.piao_hao
}))
}
});
// 创建唯一键集合
const existingIdSet = new Set(existingRecords.map(r => r.piao_hao));
// 分离需要创建和更新的数据
const createData = datas.filter(item => !existingIdSet.has(item.piao_hao));
const updateData = datas.filter(item => existingIdSet.has(item.piao_hao));
createCount = createData.length;
updateCount = updateData.length;
// 批量创建新数据
if (createData.length > 0) {
await prisma.invoice_data.createMany({
data: createData,
skipDuplicates: false
});
}
// 逐条更新已有数据updateMany不支持批量不同更新
// 逐条更新已有数据updateMany不支持批量不同更新
for (const item of updateData) {
await prisma.invoice_data.update(
{
where: {
piao_hao: item.piao_hao
},
data: item
}
);
}
}, {
maxWait: 30000, // 最大等待时间30秒
timeout: 60000, // 超时时间60秒
});
res.status(200).json({
code: 0,
data: {
createCount,
updateCount,
},
error: null,
message: '开票完成数据导入成功',
});
} catch (error) {
console.error(error)
res.status(500).json({ code: -1, error: 'Internal server error', stack: error });
}
})

View File

@@ -0,0 +1,41 @@
import { NextApiRequest, NextApiResponse } from 'next';
import prismaClient from '@/lib/prisma'
import XLSX from 'xlsx'
import { getBussinessDetailsList } from './util'
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
try {
if (req.method !== 'GET') {
return res.status(405).json({ error: 'Method not allowed' });
}
// 获取所有project数据
const projects = await prismaClient.project.findMany();
const excelDatas = await getBussinessDetailsList(projects)
// excelDatas 输出到excel文件并吐出去
const wb = XLSX.utils.book_new();
const ws = XLSX.utils.json_to_sheet(excelDatas, {
// header: [
// ...(excelDatas?.[0] ?? {}).key
// ]
});
XLSX.utils.book_append_sheet(wb, ws, "Sheet1");
XLSX.writeFile(wb, "output.xlsx");
// Create a new workbook and append the worksheet
const workbook = XLSX.utils.book_new()
XLSX.utils.book_append_sheet(workbook, ws, '开票申请数据')
const buffer = XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' })
// Set headers for file download
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
res.setHeader('Content-Disposition', 'attachment; filename=project-all-data.xlsx')
// Send the Excel file
res.write(buffer)
res.end()
} catch (error) {
res.status(500).json({ error: 'Internal server error', stack: error });
}
}

View File

@@ -0,0 +1,36 @@
import type { NextApiRequest, NextApiResponse } from 'next'
import prismaClient from '@/lib/prisma'
type ResponseData = {
code: number
message: string
data?: {
data: any
}
error?: any
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<ResponseData>
) {
try {
if (req.method !== 'POST') {
return res.status(400).json({ code: 400, message: '请求方法错误' })
}
const { project_number } = req.body || {}
if (!project_number || typeof project_number !== 'string') {
return res.status(400).json({ code: 400, message: '缺少项目编号' })
}
const records = await prismaClient.project.findMany({
where: { project_id: project_number }
})
return res.status(200).json({
code: 0,
message: '获取成功',
data: { data: records }
})
} catch (error) {
return res.status(500).json({ code: 500, message: '获取数据时出错', error })
}
}

View File

@@ -0,0 +1,75 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { withFileUpload } from 'next-multiparty';
import * as XLSX from 'xlsx';
import { InvoiceApplicationItem } from '../type'
import prismaClient from '@/lib/prisma'
const ExcelToMySQLFieldMapping = {
'数据id': 'data_id',
'组别缩写': 'group_abbreviation',
'上一项目编号': 'previous_project_id',
'项目编号': 'project_id',
'项目名称': 'project_name',
"关联合同编号": 'contract_id',
"合同名称": 'contract_name',
'客户名称': 'client_name',
'交付区域(省/市)': 'delivery_area',
'单位性质': 'unit_nature',
'项目主担': 'project_leader',
'项目组别': 'project_group',
'创建时间': 'create_time',
'创建者': 'creator',
'创建人部门': 'creator_department',
'审批单标题': 'approval_title',
'更新时间': 'update_time'
}
export const config = {
api: {
bodyParser: false // 必须禁用默认bodyParser
}
};
export default withFileUpload(async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
try {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
// 2. 读取Excel文件
const fileBuffer = await req?.file?.toBuffer()
const workbook = XLSX.read(fileBuffer, { type: 'buffer' });
const firstSheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[firstSheetName];
const rawData: InvoiceApplicationItem[] = XLSX.utils.sheet_to_json(worksheet);
// 3. 数据处理和去重
let projectDataList = [];
// TODO 可以查询一次搞重新修改 看业务需求
for (let i = 0; i < rawData.length; i++) {
const rawItem = rawData[i];
const projectData: any = {
data_id: rawItem['数据id'],
};
for (const [excelKey, dbKey] of Object.entries(ExcelToMySQLFieldMapping)) {
projectData[dbKey] = rawItem[excelKey];
}
projectDataList.push(projectData);
}
// 插入数据到project表
await prismaClient.project.createMany({
data: projectDataList,
skipDuplicates: true, // 跳过重复数据
});
res.status(200).json({
code: 0,
data: null,
message: '项目立项数据导入成功'
});
} catch (error) {
res.status(500).json({ error: 'Internal server error', stack: error, });
}
})

View File

@@ -0,0 +1,91 @@
import type { NextApiRequest, NextApiResponse } from 'next'
import prismaClient from '@/lib/prisma'
type ResponseData = {
code: number
message: string
data?: any[]
error?: any
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<ResponseData>
) {
try {
if (req.method !== 'GET') {
return res.status(400).json({ code: 400, message: '请求方法错误只支持GET请求' })
}
// 获取查询参数
const {
project_id,
project_name,
project_leader,
client_name,
project_department,
group_abbreviation
} = req.query
// 构建查询条件
const whereConditions: any = {}
if (project_id) {
whereConditions.project_id = {
contains: project_id as string
}
}
if (project_name) {
whereConditions.project_name = {
contains: project_name as string
}
}
if (project_leader) {
whereConditions.project_leader = {
contains: project_leader as string
}
}
if (client_name) {
whereConditions.client_name = {
contains: client_name as string
}
}
if (project_department) {
whereConditions.project_department = {
contains: project_department as string
}
}
if (group_abbreviation) {
whereConditions.group_abbreviation = {
contains: group_abbreviation as string
}
}
// 查询所有项目数据(不分页)
const records = await prismaClient.project.findMany({
where: whereConditions,
orderBy: {
create_time: 'desc'
}
})
return res.status(200).json({
code: 0,
message: '获取项目台账数据成功',
data: records
})
} catch (error) {
console.error('获取项目台账数据时出错:', error)
return res.status(500).json({
code: 500,
message: '获取项目台账数据时出错',
error: error instanceof Error ? error.message : '未知错误'
})
}
}

View File

@@ -0,0 +1,206 @@
import type { NextApiRequest, NextApiResponse } from 'next'
import prismaClient from '@/lib/prisma'
import { Prisma } from '@prisma/client'
type ResponseData = {
code: number
message: string
data?: {
data: any
pagination?: {
total: number
page: number
pageSize: number
totalPages: number
}
}
error?: any
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<ResponseData>
) {
try {
if (req.method !== 'POST') {
return res.status(400).json({ code: 400, message: '请求方法错误只支持POST请求' })
}
// 获取查询参数
const {
project_number,
project_leader,
project_name,
client_name,
project_department,
group_abbreviation
} = req.body
// 构建查询条件
const conditions: any[] = []
if (project_number) {
conditions.push(Prisma.sql`project_id LIKE ${'%' + project_number + '%'}`)
}
if (project_leader) {
conditions.push(Prisma.sql`project_leader LIKE ${'%' + project_leader + '%'}`)
}
if (project_name) {
conditions.push(Prisma.sql`project_name LIKE ${'%' + project_name + '%'}`)
}
if (client_name) {
conditions.push(Prisma.sql`client_name LIKE ${'%' + client_name + '%'}`)
}
if (project_department) {
conditions.push(Prisma.sql`project_department LIKE ${'%' + project_department + '%'}`)
}
if (group_abbreviation) {
conditions.push(Prisma.sql`group_abbreviation LIKE ${'%' + group_abbreviation + '%'}`)
}
// 构建WHERE子句
let whereClause = Prisma.sql``
if (conditions.length > 0) {
whereClause = Prisma.sql`WHERE ${conditions[0]}`
for (let i = 1; i < conditions.length; i++) {
whereClause = Prisma.sql`${whereClause} AND ${conditions[i]}`
}
}
// 查询所有项目数据(不分页)
const projects: any[] = await prismaClient.$queryRaw(Prisma.sql`
SELECT * FROM project
${whereClause}
ORDER BY CAST(
REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(
REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(
REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(
REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(
REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(
REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(
project_id,
'A',''),'B',''),'C',''),'D',''),'E',''),'F',''),'G',''),'H',''),'I',''),'J',''),
'K',''),'L',''),'M',''),'N',''),'O',''),'P',''),'Q',''),'R',''),'S',''),'T',''),
'U',''),'V',''),'W',''),'X',''),'Y',''),'Z',''),
'a',''),'b',''),'c',''),'d',''),'e',''),'f',''),'g',''),'h',''),'i',''),'j',''),
'k',''),'l',''),'m',''),'n',''),'o',''),'p',''),'q',''),'r',''),'s',''),'t',''),
'u',''),'v',''),'w',''),'x',''),'y',''),'z','')
AS UNSIGNED) DESC
`)
// 查询销售合同数据 - 通过合同编号关联
const contractIds = projects.map(p => p.contract_id).filter(Boolean)
const salesContracts = await prismaClient.procurement_contract_list.findMany({
where: {
contract_number: {
in: contractIds
}
},
select: {
contract_number: true,
contract_name: true,
contract_amount: true,
xiangduifang: true,
performance_start_time: true
}
})
// 构建返回数据
const ledgerData = projects.map((project, index) => {
const projectSalesContracts = salesContracts.filter(
contract => contract.project_number === project.project_id
)
return {
序号: index + 1,
项目编号: project.project_id || '',
项目名称: project.project_name || '',
客户名称: project.client_name || '',
交付区域: project.delivery_area || null,
单位性质: project.unit_nature || '',
项目主担: project.project_leader || '',
项目组别: project.project_group || '',
销售订单数量: projectSalesContracts.length,
销售合同金额: projectSalesContracts.reduce((sum, contract) => {
return sum + parseFloat(contract.contract_amount || '0')
}, 0).toString(),
采购订单数量: 0,
: '0',
_不含税: '0',
: '',
_不含税: '0',
: '否',
: '待定TODO',
_含税: '0',
: '0',
: '否',
: '0',
: '',
: '0',
SR号: '',
: '0',
: '0',
: '0',
: '0',
: '0',
: '0',
salesContracts: projectSalesContracts.map(contract => ({
data_id: contract.data_id,
initiator: contract.initiator || '',
contract_number: contract.contract_number || '',
contract_name: contract.contract_name || '',
lvyue_faqi_bumen: contract.lvyue_faqi_bumen || '',
operator: contract.operator || '',
xiangduifang: contract.xiangduifang || '',
is_open: contract.is_open || '',
contract_amount: contract.contract_amount || '0.00',
remarks: contract.remarks || '',
performance_start_time: contract.performance_start_time || '',
hetong_yuanjian_saomiaojian_gaizhangban: contract.hetong_yuanjian_saomiaojian_gaizhangban || '',
dingtalk_procurement_approval_form: contract.dingtalk_procurement_approval_form,
contract_attachment_scan: contract.contract_attachment_scan || '',
creation_time: contract.creation_time || '',
creator: contract.creator,
approval_result: contract.approval_result,
approval_status: contract.approval_status,
update_time: contract.update_time || '',
creator_department: contract.creator_department || '',
approval_form_title: contract.approval_form_title || ''
})),
purchaseContractsList: [],
modifiedProjectInvoiceData: []
}
})
// 构造分页信息(不分页,但保持结构一致)
const total = ledgerData.length
const pagination = {
total,
page: 1,
pageSize: total,
totalPages: 1
}
return res.status(200).json({
code: 0,
message: '获取项目台账数据成功',
data: {
data: ledgerData,
pagination
}
})
} catch (error) {
console.error('获取项目台账数据时出错:', error)
return res.status(500).json({
code: 500,
message: '获取项目台账数据时出错',
error: error instanceof Error ? error.message : '未知错误'
})
}
}

View File

@@ -0,0 +1,546 @@
import prismaClient from '@/lib/prisma'
import { processInvoiceDataItem } from '@/lib/util'
import { XIANGMU_ZUBIE_MAP, ABBR_ZUBIE_MAP } from '@/CONSTANTS'
import dayjs from 'dayjs'
import BigNumber from 'bignumber.js'
export async function getBussinessDetailsList(projects, kai_piao_ri_qi?: unknown) {
try {
const businessContentMap = new Map<string, string[]>();// <-- 一个空对象为了复用函数
// 1 拿到所有的项目id
const projectNumbers = projects.map(project => project.project_id).filter(Boolean);
const projectNumberSet = new Set(projectNumbers);
const splitProjectNumbers = (value: unknown): string[] => {
return String(value ?? '')
.split(/[,,、\s]+/)
.map(s => s.trim())
.filter(Boolean);
};
const normalizeDate = (input: unknown): dayjs.Dayjs | undefined => {
if (input === null || input === undefined) return undefined;
if (typeof input === 'number' && Number.isFinite(input)) {
const parsedNum = dayjs(input);
return parsedNum.isValid() ? parsedNum : undefined;
}
const s = String(input).trim();
if (!s) return undefined;
const cleaned = s.replace(/\//g, '-').split(' ')[0];
const parsed = dayjs(cleaned);
if (!parsed.isValid()) return undefined;
return parsed;
};
const parseInvoiceRange = (raw: unknown): { start?: dayjs.Dayjs, end?: dayjs.Dayjs } => {
if (!raw) return {};
if (Array.isArray(raw)) {
const s = normalizeDate(raw[0]);
const e = normalizeDate(raw[1] ?? raw[0]);
return {
start: s ? s.startOf('day') : undefined,
end: e ? e.endOf('day') : undefined
};
}
if (typeof raw === 'object') {
const obj: any = raw;
const s = normalizeDate(obj?.start ?? obj?.startTime ?? obj?.begin ?? obj?.beginTime ?? obj?.[0]);
const e = normalizeDate(obj?.end ?? obj?.endTime ?? obj?.finish ?? obj?.finishTime ?? obj?.[1] ?? obj?.start);
return {
start: s ? s.startOf('day') : undefined,
end: e ? e.endOf('day') : undefined
};
}
const text = String(raw).trim();
if (!text) return {};
if (text.startsWith('[')) {
try {
const arr = JSON.parse(text);
if (Array.isArray(arr)) {
const s = normalizeDate(arr[0]);
const e = normalizeDate(arr[1] ?? arr[0]);
return {
start: s ? s.startOf('day') : undefined,
end: e ? e.endOf('day') : undefined
};
}
} catch {}
}
const parts = text.split(/[,,、\s~~至到]+/).map(s => s.trim()).filter(Boolean);
if (parts.length > 0) {
const s = normalizeDate(parts[0]);
const e = normalizeDate(parts[1] ?? parts[0]);
return {
start: s ? s.startOf('day') : undefined,
end: e ? e.endOf('day') : undefined
};
}
return {};
};
const invoiceDateRange = parseInvoiceRange(kai_piao_ri_qi);
const isInInvoiceDateRange = (value: unknown): boolean => {
if (!invoiceDateRange.start && !invoiceDateRange.end) return true;
const d = normalizeDate(value);
if (!d) return false;
if (invoiceDateRange.start && d.isBefore(invoiceDateRange.start)) return false;
if (invoiceDateRange.end && d.isAfter(invoiceDateRange.end)) return false;
return true;
};
// 2 先拉取发票数据用于建立项目号与销售合同号映射
// 根据项目号 从 invoice_application_match_view 中拿到开票信息(排除作废)
const voidRecords = await prismaClient.dingding_oa_invoice_application.findMany({
where: { remark: '发票作废' },
select: { dataId: true }
});
const voidIds = voidRecords.map(r => r.dataId).filter(Boolean);
const invoiceData = await prismaClient.invoice_application_match_view.findMany({
where: {
OR: projectNumbers.map(p => ({ projectNumber: { contains: p } })),
...(voidIds.length > 0 ? { dataId: { notIn: voidIds } } : {})
}
});
const filteredInvoiceData = invoiceData.filter(item => {
const tokens = splitProjectNumbers(item.projectNumber);
return tokens.some(t => projectNumberSet.has(t));
});
const invoiceDataIds = Array.from(new Set(filteredInvoiceData.map(item => item.dataId).filter(Boolean)));
const invoiceAmountDetails = invoiceDataIds.length > 0 ? await prismaClient.invoice_amount_details.findMany({
where: {
dataid: {
in: invoiceDataIds
},
project_number: {
in: projectNumbers
}
},
select: {
dataid: true,
project_number: true,
invoice_amount: true,
yu_ji_bao_xiao_di_san_fang_cheng_ben: true
}
}) : [];
const detailInvoiceAmountByDataIdProject = new Map<string, BigNumber>();
const detailThirdPartyCostByDataIdProject = new Map<string, BigNumber>();
const detailThirdPartyCostProvidedKeys = new Set<string>();
invoiceAmountDetails.forEach(item => {
const key = `${item.dataid || ''}||${item.project_number || ''}`;
const raw = String(item.invoice_amount ?? '').replace(/,/g, '').trim();
const amount = raw ? new BigNumber(raw) : new BigNumber(0);
if (!amount.isFinite()) return;
const current = detailInvoiceAmountByDataIdProject.get(key) || new BigNumber(0);
detailInvoiceAmountByDataIdProject.set(key, current.plus(amount));
const thirdPartyRaw = String(item.yu_ji_bao_xiao_di_san_fang_cheng_ben ?? '').replace(/,/g, '').trim();
if (thirdPartyRaw !== '') {
const thirdPartyAmount = new BigNumber(thirdPartyRaw);
if (thirdPartyAmount.isFinite()) {
detailThirdPartyCostProvidedKeys.add(key);
const currentThirdParty = detailThirdPartyCostByDataIdProject.get(key) || new BigNumber(0);
detailThirdPartyCostByDataIdProject.set(key, currentThirdParty.plus(thirdPartyAmount));
}
}
});
const salesContractToProjectSetMap = new Map<string, Set<string>>();
for (const item of filteredInvoiceData) {
const contractNumber = item.salesContractNumber;
if (!contractNumber) continue;
const tokens = splitProjectNumbers(item.projectNumber);
if (tokens.length === 0) continue;
const existing = salesContractToProjectSetMap.get(contractNumber) ?? new Set<string>();
tokens.forEach(t => existing.add(t));
salesContractToProjectSetMap.set(contractNumber, existing);
}
const invoiceDataSalesContractNumbers = Array.from(salesContractToProjectSetMap.keys());
// 3 根据项目编号或关联销售合同编号查采购订单
const purchaseContractsOrder = await prismaClient.purchase_contract_order_management.findMany({
where: {
project_number: {
in: projectNumbers
}
}
});
// 根据销售合同编号在procurement_contract_list表中筛选出所有销售合同
const salesContracts = await prismaClient.procurement_contract_list.findMany({
where: {
contract_number: {
in: invoiceDataSalesContractNumbers
},
}
});
// 将销售合同对应的项目号塞入数组
const salesContractsWithProjectNumbers = [];
// 为每个项目单独处理其相关的销售合同
for (const project of projects) {
const projectNumber = project.project_id;
if (!projectNumber) continue;
// 找出与当前项目相关的销售合同编号
const projectSalesContractNumberSet = new Set<string>();
for (const [contractNumber, projectSet] of salesContractToProjectSetMap.entries()) {
if (projectSet.has(projectNumber)) {
projectSalesContractNumberSet.add(contractNumber);
}
}
if (projectSalesContractNumberSet.size === 0) continue;
// 筛选出当前项目相关的销售合同
const projectSalesContracts = salesContracts.filter(contract =>
projectSalesContractNumberSet.has(contract.contract_number || '')
);
// 将销售合同与项目编号关联
const projectSalesContractsWithNumbers = projectSalesContracts.map(contract => ({
...contract,
project_number: projectNumber
}));
salesContractsWithProjectNumbers.push(...projectSalesContractsWithNumbers);
}
// 根据项目号 从 expense_reimbursement 中拿到费用报销信息
const expenseReimbursementData = await prismaClient.expense_reimbursement.findMany({
where: {
project_number: {
in: projectNumbers
}
}
});
// 从 实际支付表内拿到项目号 对应的 期末余额
const actualPaymentData = await prismaClient.actual_payment.findMany({
where: {
project_number: {
in: projectNumbers
}
}
});
// 1 提取出 projects 表内所有的client_name 之后去 customer_management_info内提取对应的 customer_type 形成KV映射
const customerTypeMap = new Map<string, string>();
const customerNames = projects.map(project => project.client_name).filter(Boolean);
const customerManagementInfo = await prismaClient.customer_management_info.findMany({
where: {
customer_unit_name: {
in: customerNames
}
}
});
customerManagementInfo.forEach(item => {
customerTypeMap.set(item.customer_unit_name, item.customer_type || '');
});
// excelDatas 对象存储每一行的数据
const excelDatas = []
projects.map((project, idx) => {
// 专业组逻辑:提取项目编号的所有字母部分映射,如果没有项目编号则为客户服务室
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] || '客户服务室';
};
// 逻辑以 excel 为标准逻辑
// 1 项目类
const data = {
'序号': idx + 1,
'项目编号': project.project_id,
'项目名称': project.project_name,
'客户名称': project.client_name,
'交付区域': project.delivery_area,
'单位性质': customerTypeMap.get(project.client_name || '') || '', //
'项目主担': project.project_leader,
'项目组别': getProjectGroupByProjectId(project.project_id),
// '项目组别': XIANGMU_ZUBIE_MAP[ABBR_ZUBIE_MAP[project.group_abbreviation]], //
}
// 2 合同大类
// 销售合同 采购合同
// 销售合同/订单数量 =COUNTIFS(合同管理!D:D,C6,合同管理!C:C,$X$2)
// 销售合同号 =IF(X6=1,VLOOKUP($X$2&C6,合同管理!A:E,5,0),"-")
// 销售合同金额 =SUMIFS(合同管理!F:F,合同管理!D:D,C6,合同管理!C:C,$X$2)
// 客户名称 =IF(X6=1,VLOOKUP($X$2&C6,合同管理!A:H,8,0),"-")
//
// 采购合同/订单数量 =COUNTIFS(合同管理!D:D,C6,合同管理!C:C,$AB$2)
// 采购合同号 =IF(AB6=1,VLOOKUP(C6,合同管理!D:E,2,0),"-")
// 采购合同金额 =SUMIFS(合同管理!F:F,合同管理!D:D,C6,合同管理!C:C,$AB$2)
// 供应商名称 =IF(AB6=1,VLOOKUP($AB$2&C6,合同管理!A:H,8,0),"-")
const sales_contract = {
'销售订单数量': 0,//
// '销售合同号': '-',
'销售合同金额': new BigNumber(0),// string 或者 bignumber
// '客户名称': '-',
}
// 销售订单数量 这个变量等于 salesContractsWithProjectNumbers 里面项目号一致的合同的数量
const salesContractsObj = salesContractsWithProjectNumbers.filter(item => (item.project_number || '') === project.project_id);
sales_contract['销售订单数量'] = salesContractsObj.length
// sales_contract['销售合同号'] = salesContractsObj.length === 1 ? salesContractsObj[0].contract_name || '-' : '-';
sales_contract['销售合同金额'] = salesContractsObj.length > 0 ?
salesContractsObj.reduce((sum, contract) => (sum).plus(new BigNumber(contract?.contract_amount || 0)), new BigNumber(0)) : new BigNumber(0);
// sales_contract['客户名称'] = salesContractsObj.length === 1 ? salesContractsObj[0].xiangduifang || '-' : '-';
const purchase_contract = {
'采购订单数量': 0,
// '采购合同号': '-',
'采购合同金额': new BigNumber(0),
// '供应商名称': '-',
}
//
// 采购订单数量 这个变量等于 purchaseContractsOrder 里面项目号一致的合同的数量
const purchaseContractsList = purchaseContractsOrder.filter(item => item.project_number === project.project_id);
// 根据合同号去重
const purchaseContractsListDeduplication = purchaseContractsList.reduce((acc, current) => {
const existing = acc.find(item => item.associated_sales_contract_number === current.associated_sales_contract_number);
if (!existing) {
acc.push(current);
}
return acc;
}, []);
purchase_contract['采购订单数量'] = purchaseContractsList.length
// purchase_contract['采购合同号'] = purchaseContractsList.length === 1 ? purchaseContractsObj[0].contract_number || '-' : '-';
purchase_contract['采购合同金额'] = purchaseContractsListDeduplication.length > 0 ?
purchaseContractsListDeduplication.reduce((sum, contract) => (sum).plus(new BigNumber(contract?.he_tong_zong_jin_e_HanShui || 0)), new BigNumber(0)) : new BigNumber(0);
// purchase_contract['供应商名称'] = purchaseContractsList.length === 1 ? purchaseContractsObj[0].xiangduifang || '-' : '-';
// 提取项目号一致的开票数据
const projectInvoiceData = filteredInvoiceData.filter(item => {
const tokens = splitProjectNumbers(item.projectNumber);
return tokens.includes(project.project_id);
});
const modifiedProjectInvoiceData = projectInvoiceData.map((ele) => processInvoiceDataItem(ele, businessContentMap));
const projectInvoiceDataInRange = projectInvoiceData.filter(item => isInInvoiceDateRange(item.kai_piao_ri_qi));
const modifiedProjectInvoiceDataInRange = projectInvoiceDataInRange.map((ele) => processInvoiceDataItem(ele, businessContentMap));
// 接下来是金额组
// "预计总支出 (不含税)" 开票时间 " 开票收入(不含税) " 是否交付 " 划入交付组收入 " " 开票金额(含税) " 已回款 是否回款 未回款金额 回款时间
const invoiceExcelData = {
'预计总支出 (不含税)': new BigNumber(0),
'开票时间': '-', // =IF(AI74=0," ",VLOOKUP(C74,'发票台账23-25'!B:K,10,0))
'开票收入(不含税)': new BigNumber(0),
'是否交付': '-',
'划入交付组收入': '待定TODO',
'开票金额(含税)': new BigNumber(0),
'已回款': new BigNumber(0),
'是否回款': '-',
'未回款金额': new BigNumber(0),
'回款时间': '-',
}
// modifiedProjectInvoiceData 是为了拿到 税额因为他们要判断类型很复杂
// 预计总支出= 所有的预计支付第三方成本的和
const expenseByInvoiceKey = new Map<string, BigNumber>();
projectInvoiceDataInRange.forEach(item => {
const key = `${item.dataId || ''}||${item.invoiceJobNumber || ''}`;
if (!expenseByInvoiceKey.has(key)) {
expenseByInvoiceKey.set(key, new BigNumber(item.yuJiBaoXiaoDiSanFangChengBenYuan || 0));
}
});
const projectInvoiceDataIdsInRange = Array.from(new Set(
projectInvoiceDataInRange.map(item => String(item.dataId || '')).filter(Boolean)
));
const detailThirdPartyFromDetails = projectInvoiceDataIdsInRange.reduce((sum, dataId) => {
const key = `${dataId}||${project.project_id || ''}`;
return sum.plus(detailThirdPartyCostByDataIdProject.get(key) || new BigNumber(0));
}, new BigNumber(0));
const hasDetailThirdPartyValue = projectInvoiceDataIdsInRange.some(dataId =>
detailThirdPartyCostProvidedKeys.has(`${dataId}||${project.project_id || ''}`)
);
if (hasDetailThirdPartyValue) {
invoiceExcelData['预计总支出 (不含税)'] = detailThirdPartyFromDetails;
} else {
// 明细表未维护第三方成本时,回退主表原有取值逻辑
invoiceExcelData['预计总支出 (不含税)'] = Array.from(expenseByInvoiceKey.values()).reduce((sum, amt) => (sum).plus(amt), new BigNumber(0));
}
// 开票收入不函数 == SUMIF('发票台账23-25'!B:B,C21,'发票台账23-25'!F:F)
const nonDuoMainBuHanTotal = projectInvoiceDataInRange.reduce((sum, rawItem, idx) => {
const duoRaw = rawItem.bu_han_shui_jin_e_yuan_duo;
const isDuoMain = duoRaw !== null && duoRaw !== undefined && String(duoRaw).trim() !== '';
if (isDuoMain) return sum;
const processed = modifiedProjectInvoiceDataInRange[idx];
return sum.plus(new BigNumber(processed?.bu_han_shui_jin_e || 0));
}, new BigNumber(0));
const duoDataIdsForBuHan = Array.from(new Set(
projectInvoiceDataInRange
.filter(rawItem => rawItem.bu_han_shui_jin_e_yuan_duo !== null && rawItem.bu_han_shui_jin_e_yuan_duo !== undefined && String(rawItem.bu_han_shui_jin_e_yuan_duo).trim() !== '')
.map(rawItem => String(rawItem.dataId || ''))
.filter(Boolean)
));
const detailBuHanTotal = duoDataIdsForBuHan.reduce((sum, dataId) => {
const key = `${dataId}||${project.project_id || ''}`;
const detailHanShuiAmount = detailInvoiceAmountByDataIdProject.get(key) || new BigNumber(0);
const sourceRow = projectInvoiceDataInRange.find(rawItem => String(rawItem.dataId || '') === dataId && rawItem.bu_han_shui_jin_e_yuan_duo !== null && rawItem.bu_han_shui_jin_e_yuan_duo !== undefined && String(rawItem.bu_han_shui_jin_e_yuan_duo).trim() !== '');
const duoBuHanRaw = String(sourceRow?.bu_han_shui_jin_e_yuan_duo ?? '').replace(/,/g, '').trim();
const duoHanRaw = String(sourceRow?.han_shui_jin_e_yuan_duo ?? '').replace(/,/g, '').trim();
const duoBuHan = duoBuHanRaw ? new BigNumber(duoBuHanRaw) : new BigNumber(0);
const duoHan = duoHanRaw ? new BigNumber(duoHanRaw) : new BigNumber(0);
if (duoHan.gt(0) && duoBuHan.gte(0)) {
return sum.plus(detailHanShuiAmount.multipliedBy(duoBuHan).dividedBy(duoHan));
}
return sum.plus(detailHanShuiAmount);
}, new BigNumber(0));
invoiceExcelData['开票收入(不含税)'] = nonDuoMainBuHanTotal.plus(detailBuHanTotal).decimalPlaces(2, BigNumber.ROUND_HALF_UP);
// 开票时间
if (invoiceExcelData['开票收入(不含税)'].eq(0)) {
invoiceExcelData['开票时间'] = '';
} else {
const validInvoiceData = projectInvoiceDataInRange
.filter(item => item.kai_piao_ri_qi)
.sort((a, b) => {
const dateA = dayjs(a.kai_piao_ri_qi);
const dateB = dayjs(b.kai_piao_ri_qi);
return dateA.isBefore(dateB) ? -1 : dateA.isAfter(dateB) ? 1 : 0;
});
invoiceExcelData['开票时间'] = validInvoiceData.length > 0 ? validInvoiceData[0].kai_piao_ri_qi ?? '' : '-';
}
invoiceExcelData['是否交付'] = invoiceExcelData['开票收入(不含税)'].gt(0) ? '是' : '否'
const nonDuoMainHanShuiTotal = projectInvoiceDataInRange.reduce((sum, rawItem, idx) => {
const duoRaw = rawItem.han_shui_jin_e_yuan_duo;
const isDuoMain = duoRaw !== null && duoRaw !== undefined && String(duoRaw).trim() !== '';
if (isDuoMain) return sum;
const processed = modifiedProjectInvoiceDataInRange[idx];
return sum.plus(new BigNumber(processed?.han_shui_zong_jin_e || 0));
}, new BigNumber(0));
const duoDataIds = Array.from(new Set(
projectInvoiceDataInRange
.filter(rawItem => rawItem.han_shui_jin_e_yuan_duo !== null && rawItem.han_shui_jin_e_yuan_duo !== undefined && String(rawItem.han_shui_jin_e_yuan_duo).trim() !== '')
.map(rawItem => String(rawItem.dataId || ''))
.filter(Boolean)
));
const detailHanShuiTotal = duoDataIds.reduce((sum, dataId) => {
const key = `${dataId}||${project.project_id || ''}`;
return sum.plus(detailInvoiceAmountByDataIdProject.get(key) || new BigNumber(0));
}, new BigNumber(0));
invoiceExcelData['开票金额(含税)'] = nonDuoMainHanShuiTotal.plus(detailHanShuiTotal);
invoiceExcelData['已回款'] = modifiedProjectInvoiceDataInRange.reduce((sum, item) => (sum).plus(new BigNumber(item.hui_kuan_jin_e || 0)), new BigNumber(0));
invoiceExcelData['是否回款'] = invoiceExcelData['已回款'].gt(0) ? '是' : '否';
invoiceExcelData['未回款金额'] = new BigNumber(invoiceExcelData['开票金额(含税)']).minus(new BigNumber(invoiceExcelData['已回款']));
// invoiceExcelData['回款时间'] = invoiceExcelData['已回款'] === 0 ? '' :
// (projectInvoiceData.length > 0 ?
// projectInvoiceData.reduce((latest, current) =>
// !latest.hui_kuan_ri_qi || new Date(current.hui_kuan_ri_qi) > new Date(latest.hui_kuan_ri_qi) ? current : latest
// ).hui_kuan_ri_qi : '');
// 回款日期逻辑
if (invoiceExcelData['已回款'].eq(0)) {
invoiceExcelData['回款时间'] = '';
} else {
const validInvoiceData = projectInvoiceDataInRange.filter(item => item.hui_kuan_ri_qi);
const sortedInvoiceData = validInvoiceData.sort((a, b) => {
const dateA = dayjs(a.hui_kuan_ri_qi);
const dateB = dayjs(b.hui_kuan_ri_qi);
return dateA.isBefore(dateB) ? -1 : dateA.isAfter(dateB) ? 1 : 0;
});
// 最晚回款时间
invoiceExcelData['回款时间'] = sortedInvoiceData.length > 0 ? sortedInvoiceData[sortedInvoiceData.length - 1]?.hui_kuan_ri_qi ?? '' : '';
}
// 实际支出
const obj = {
'已报销': new BigNumber(0),
'SR号': '',
'预计未报销金额': new BigNumber(0),
'已支付': new BigNumber(0),
'预计项目收益': new BigNumber(0),
'系统项目收益': new BigNumber(0),
'现金流': new BigNumber(0),
'价值创造': new BigNumber(0),
}
// 提取项目号一致的费用报销数据
const projectExpenseReimbursementData = expenseReimbursementData.filter(item => item.project_number === project.project_id);
// 计算已报销金额 =SUMIF(费用报销!C:C,C6,费用报销!G:G)
obj['已报销'] = projectExpenseReimbursementData.reduce((sum, item) => (sum).plus(new BigNumber(item.amount || 0)), new BigNumber(0));
// 如果已报销是0 那么空字符串 如果不是0 那么找到第一个SR号
if (obj['已报销'].eq(0)) {
obj['SR号'] = '';
} else {
// 查找第一个有SR号的记录
const firstRecordWithSR = projectExpenseReimbursementData.find(item => item.sr_number && item.sr_number.trim() !== '');
obj['SR号'] = firstRecordWithSR ? firstRecordWithSR.sr_number : '';
}
// 预计未报销金额 = 预计总支出 - 已报销
obj['预计未报销金额'] = new BigNumber(invoiceExcelData['预计总支出 (不含税)']).minus(new BigNumber(obj['已报销']));
// 汇总实际支付表内的期末余额
obj['已支付'] = actualPaymentData.filter(item => item.project_number === project.project_id).reduce((sum, item) => (sum).plus(new BigNumber(item.qimo_yueyuan || 0)), new BigNumber(0));
// 开票收入 - 预计总支出
obj['预计项目收益'] = new BigNumber(invoiceExcelData['开票收入(不含税)']).minus(new BigNumber(invoiceExcelData['预计总支出 (不含税)']));
// 开票收入 - 已报销
obj['系统项目收益'] = new BigNumber(invoiceExcelData['开票收入(不含税)']).minus(new BigNumber(obj['已报销']));
// -
obj['现金流'] = new BigNumber(invoiceExcelData['已回款']).minus(new BigNumber(obj['已支付']));
// 价值创造 = 0.54 * 开票金额(含税)
obj['价值创造'] = new BigNumber(invoiceExcelData['开票金额(含税)']).times(new BigNumber(0.54));
// 对
const merged = {
...data, ...sales_contract, ...purchase_contract, ...invoiceExcelData, ...obj, salesContracts: salesContractsObj, purchaseContractsList, modifiedProjectInvoiceData: modifiedProjectInvoiceDataInRange
};
const finalObj = processFinalObject(merged);
excelDatas.push(finalObj)
})
return excelDatas;
} catch (error) {
console.log(error)
throw error;
}
}
function processFinalObject(
merged: Record<string, any>,
): Record<string, any> {
for (const key in merged) {
if (merged.hasOwnProperty(key)) {
const value = merged[key];
if (BigNumber.isBigNumber(value)) {
merged[key] = value.toString();
} else {
merged[key] = value;
}
}
}
return merged;
}

View File

@@ -0,0 +1,382 @@
import prismaClient from '@/lib/prisma'
import { processInvoiceDataItem } from '@/lib/util'
import { XIANGMU_ZUBIE_MAP, ABBR_ZUBIE_MAP } from '@/CONSTANTS'
import dayjs from 'dayjs'
import BigNumber from 'bignumber.js'
export async function getBussinessDetailsList(projects) {
try {
const businessContentMap = new Map<string, string[]>();// <-- 一个空对象为了复用函数
// 1 拿到所有的项目id
const projectNumbers = projects.map(project => project.project_id).filter(Boolean);
// 2 先拉取发票数据用于建立项目号与销售合同号映射
// 根据项目号 从 invoice_application_match_view 中拿到开票信息(排除作废)
const voidRecords = await prismaClient.dingding_oa_invoice_application.findMany({
where: { remark: '发票作废' },
select: { dataId: true }
});
const voidIds = voidRecords.map(r => r.dataId).filter(Boolean);
const invoiceData = await prismaClient.invoice_application_match_view.findMany({
where: {
OR: projectNumbers.map(p => ({ projectNumber: { contains: p } })),
...(voidIds.length > 0 ? { dataId: { notIn: voidIds } } : {})
}
});
// 从开票数据中提取项目号和销售合同编号
const salesContractToProjectMap: {
[k: string]: string;
} = {};
invoiceData.forEach(item => {
if (item.salesContractNumber && item.projectNumber) {
salesContractToProjectMap[item.salesContractNumber] = item.projectNumber;
}
});
const invoiceDataSalesContractNumbers = invoiceData.map(item => item.salesContractNumber).filter(ele => ele);
// 3 根据项目编号或关联销售合同编号查采购订单
const purchaseContractsOrder = await prismaClient.purchase_contract_order_management.findMany({
where: {
project_number: {
in: projectNumbers
}
}
});
// 创建销售合同编号到项目编号的映射(从采购订单数据中获取)
const salesContractToProjectMapFromPurchase = new Map<string, string>();
// 为每个项目单独查询相关的销售合同
const salesContractsWithProjectNumbers = [];
for (const project of projects) {
const projectNumber = project.project_id;
if (!projectNumber) continue;
// 根据项目编号从采购订单表中获取相关的销售合同编号
const purchaseContractsWithSalesContract = await prismaClient.purchase_contract_order_management.findMany({
where: {
project_number: {
contains: projectNumber
},
associated_sales_contract_number: {
not: null
}
},
select: {
associated_sales_contract_number: true,
project_number: true
}
});
// 提取当前项目相关的销售合同编号
const salesContractNumbersForProject = purchaseContractsWithSalesContract
.map(item => item.associated_sales_contract_number)
.filter(Boolean);
if (salesContractNumbersForProject.length === 0) continue;
// 根据销售合同编号在procurement_contract_list表中筛选出当前项目的销售合同
const salesContractsForProject = await prismaClient.procurement_contract_list.findMany({
where: {
contract_number: {
in: salesContractNumbersForProject
},
}
});
// 更新映射关系
purchaseContractsWithSalesContract.forEach(item => {
if (item.associated_sales_contract_number && item.project_number) {
salesContractToProjectMapFromPurchase.set(item.associated_sales_contract_number, item.project_number);
}
});
// 将当前项目的销售合同添加到结果数组
const salesContractsForProjectWithNumbers = salesContractsForProject.map(contract => {
const projectNumberFromPurchase = salesContractToProjectMapFromPurchase.get(contract?.contract_number || '');
const projectNumberFromInvoice = salesContractToProjectMap[contract?.contract_number] || null;
return {
...contract,
project_number: projectNumberFromPurchase || projectNumberFromInvoice || projectNumber
};
});
salesContractsWithProjectNumbers.push(...salesContractsForProjectWithNumbers);
}
// 根据项目号 从 expense_reimbursement 中拿到费用报销信息
const expenseReimbursementData = await prismaClient.expense_reimbursement.findMany({
where: {
project_number: {
in: projectNumbers
}
}
});
// 从 实际支付表内拿到项目号 对应的 期末余额
const actualPaymentData = await prismaClient.actual_payment.findMany({
where: {
project_number: {
in: projectNumbers
}
}
});
// 1 提取出 projects 表内所有的client_name 之后去 customer_management_info内提取对应的 customer_type 形成KV映射
const customerTypeMap = new Map<string, string>();
const customerNames = projects.map(project => project.client_name).filter(Boolean);
const customerManagementInfo = await prismaClient.customer_management_info.findMany({
where: {
customer_unit_name: {
in: customerNames
}
}
});
customerManagementInfo.forEach(item => {
customerTypeMap.set(item.customer_unit_name, item.customer_type || '');
});
// excelDatas 对象存储每一行的数据
const excelDatas = []
projects.map((project, idx) => {
// 专业组逻辑:提取项目编号的所有字母部分映射,如果没有项目编号则为客户服务室
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] || '客户服务室';
};
// 逻辑以 excel 为标准逻辑
// 1 项目类
const data = {
'序号': idx + 1,
'项目编号': project.project_id,
'项目名称': project.project_name,
'客户名称': project.client_name,
'交付区域': project.delivery_area,
'单位性质': customerTypeMap.get(project.client_name || '') || '', //
'项目主担': project.project_leader,
'项目组别': getProjectGroupByProjectId(project.project_id),
// '项目组别': XIANGMU_ZUBIE_MAP[ABBR_ZUBIE_MAP[project.group_abbreviation]], //
}
// 2 合同大类
// 销售合同 采购合同
// 销售合同/订单数量 =COUNTIFS(合同管理!D:D,C6,合同管理!C:C,$X$2)
// 销售合同号 =IF(X6=1,VLOOKUP($X$2&C6,合同管理!A:E,5,0),"-")
// 销售合同金额 =SUMIFS(合同管理!F:F,合同管理!D:D,C6,合同管理!C:C,$X$2)
// 客户名称 =IF(X6=1,VLOOKUP($X$2&C6,合同管理!A:H,8,0),"-")
//
// 采购合同/订单数量 =COUNTIFS(合同管理!D:D,C6,合同管理!C:C,$AB$2)
// 采购合同号 =IF(AB6=1,VLOOKUP(C6,合同管理!D:E,2,0),"-")
// 采购合同金额 =SUMIFS(合同管理!F:F,合同管理!D:D,C6,合同管理!C:C,$AB$2)
// 供应商名称 =IF(AB6=1,VLOOKUP($AB$2&C6,合同管理!A:H,8,0),"-")
const sales_contract = {
'销售订单数量': 0,//
// '销售合同号': '-',
'销售合同金额': new BigNumber(0),// string 或者 bignumber
// '客户名称': '-',
}
// 销售订单数量 这个变量等于 salesContractsWithProjectNumbers 里面项目号一致的合同的数量
const salesContractsObj = salesContractsWithProjectNumbers.filter(item => (item.project_number || '').includes(project.project_id));
sales_contract['销售订单数量'] = salesContractsObj.length
// sales_contract['销售合同号'] = salesContractsObj.length === 1 ? salesContractsObj[0].contract_name || '-' : '-';
sales_contract['销售合同金额'] = salesContractsObj.length > 0 ?
salesContractsObj.reduce((sum, contract) => (sum).plus(new BigNumber(contract?.contract_amount || 0)), new BigNumber(0)) : new BigNumber(0);
// sales_contract['客户名称'] = salesContractsObj.length === 1 ? salesContractsObj[0].xiangduifang || '-' : '-';
const purchase_contract = {
'采购订单数量': 0,
// '采购合同号': '-',
'采购合同金额': new BigNumber(0),
// '供应商名称': '-',
}
//
// 采购订单数量 这个变量等于 purchaseContractsOrder 里面项目号一致的合同的数量
const purchaseContractsList = purchaseContractsOrder.filter(item => item.project_number === project.project_id);
// 根据合同号去重
const purchaseContractsListDeduplication = purchaseContractsList.reduce((acc, current) => {
const existing = acc.find(item => item.associated_sales_contract_number === current.associated_sales_contract_number);
if (!existing) {
acc.push(current);
}
return acc;
}, []);
purchase_contract['采购订单数量'] = purchaseContractsList.length
// purchase_contract['采购合同号'] = purchaseContractsList.length === 1 ? purchaseContractsObj[0].contract_number || '-' : '-';
purchase_contract['采购合同金额'] = purchaseContractsListDeduplication.length > 0 ?
purchaseContractsListDeduplication.reduce((sum, contract) => (sum).plus(new BigNumber(contract?.he_tong_zong_jin_e_HanShui || 0)), new BigNumber(0)) : new BigNumber(0);
// purchase_contract['供应商名称'] = purchaseContractsList.length === 1 ? purchaseContractsObj[0].xiangduifang || '-' : '-';
// 提取项目号一致的开票数据
const projectInvoiceData = invoiceData.filter(item => (item.projectNumber || '').includes(project.project_id));
const modifiedProjectInvoiceData = projectInvoiceData.map((ele) => processInvoiceDataItem(ele, businessContentMap));
// 接下来是金额组
// "预计总支出 (不含税)" 开票时间 " 开票收入(不含税) " 是否交付 " 划入交付组收入 " " 开票金额(含税) " 已回款 是否回款 未回款金额 回款时间
const invoiceExcelData = {
'预计总支出 (不含税)': new BigNumber(0),
'开票时间': '-', // =IF(AI74=0," ",VLOOKUP(C74,'发票台账23-25'!B:K,10,0))
'开票收入(不含税)': new BigNumber(0),
'是否交付': '-',
'划入交付组收入': '待定TODO',
'开票金额(含税)': new BigNumber(0),
'已回款': new BigNumber(0),
'是否回款': '-',
'未回款金额': new BigNumber(0),
'回款时间': '-',
}
// modifiedProjectInvoiceData 是为了拿到 税额因为他们要判断类型很复杂
// 预计总支出= 所有的预计支付第三方成本的和
const uniqueExpenseAmounts = Array.from(new Set(projectInvoiceData.map(item => new BigNumber(item.yuJiBaoXiaoDiSanFangChengBenYuan || 0).toString())));
invoiceExcelData['预计总支出 (不含税)'] = uniqueExpenseAmounts.reduce((sum, amt) => (sum).plus(new BigNumber(amt)), new BigNumber(0));
// 开票收入不函数 == SUMIF('发票台账23-25'!B:B,C21,'发票台账23-25'!F:F)
invoiceExcelData['开票收入(不含税)'] = modifiedProjectInvoiceData.reduce((sum, item) => (sum).plus(new BigNumber(item.bu_han_shui_jin_e || 0)), new BigNumber(0));
// 开票时间
if (invoiceExcelData['开票收入(不含税)'].eq(0)) {
invoiceExcelData['开票时间'] = '';
} else {
const validInvoiceData = projectInvoiceData
.filter(item => item.kai_piao_ri_qi)
.sort((a, b) => {
const dateA = dayjs(a.kai_piao_ri_qi);
const dateB = dayjs(b.kai_piao_ri_qi);
return dateA.isBefore(dateB) ? -1 : dateA.isAfter(dateB) ? 1 : 0;
});
invoiceExcelData['开票时间'] = validInvoiceData.length > 0 ? validInvoiceData[0].kai_piao_ri_qi ?? '' : '-';
}
invoiceExcelData['是否交付'] = invoiceExcelData['开票收入(不含税)'].gt(0) ? '是' : '否'
invoiceExcelData['开票金额(含税)'] = modifiedProjectInvoiceData.reduce((sum, item) => (sum).plus(new BigNumber(item.han_shui_zong_jin_e || 0)), new BigNumber(0));
invoiceExcelData['已回款'] = modifiedProjectInvoiceData.reduce((sum, item) => (sum).plus(new BigNumber(item.hui_kuan_jin_e || 0)), new BigNumber(0));
invoiceExcelData['是否回款'] = invoiceExcelData['已回款'].gt(0) ? '是' : '否';
invoiceExcelData['未回款金额'] = new BigNumber(invoiceExcelData['开票金额(含税)']).minus(new BigNumber(invoiceExcelData['已回款']));
// invoiceExcelData['回款时间'] = invoiceExcelData['已回款'] === 0 ? '' :
// (projectInvoiceData.length > 0 ?
// projectInvoiceData.reduce((latest, current) =>
// !latest.hui_kuan_ri_qi || new Date(current.hui_kuan_ri_qi) > new Date(latest.hui_kuan_ri_qi) ? current : latest
// ).hui_kuan_ri_qi : '');
// 回款日期逻辑
if (invoiceExcelData['已回款'].eq(0)) {
invoiceExcelData['回款时间'] = '';
} else {
const validInvoiceData = projectInvoiceData.filter(item => item.hui_kuan_ri_qi);
const sortedInvoiceData = validInvoiceData.sort((a, b) => {
const dateA = dayjs(a.hui_kuan_ri_qi);
const dateB = dayjs(b.hui_kuan_ri_qi);
return dateA.isBefore(dateB) ? -1 : dateA.isAfter(dateB) ? 1 : 0;
});
// 最晚回款时间
invoiceExcelData['回款时间'] = sortedInvoiceData.length > 0 ? sortedInvoiceData[sortedInvoiceData.length - 1]?.hui_kuan_ri_qi ?? '' : '';
}
// 实际支出
const obj = {
'已报销': new BigNumber(0),
'SR号': '',
'预计未报销金额': new BigNumber(0),
'已支付': new BigNumber(0),
'预计项目收益': new BigNumber(0),
'系统项目收益': new BigNumber(0),
'现金流': new BigNumber(0),
'价值创造': new BigNumber(0),
}
// 提取项目号一致的费用报销数据
const projectExpenseReimbursementData = expenseReimbursementData.filter(item => item.project_number === project.project_id);
// 计算已报销金额 =SUMIF(费用报销!C:C,C6,费用报销!G:G)
obj['已报销'] = projectExpenseReimbursementData.reduce((sum, item) => (sum).plus(new BigNumber(item.amount || 0)), new BigNumber(0));
// 如果已报销是0 那么空字符串 如果不是0 那么找到第一个SR号
if (obj['已报销'].eq(0)) {
obj['SR号'] = '';
} else {
// 查找第一个有SR号的记录
const firstRecordWithSR = projectExpenseReimbursementData.find(item => item.sr_number && item.sr_number.trim() !== '');
obj['SR号'] = firstRecordWithSR ? firstRecordWithSR.sr_number : '';
}
// 预计未报销金额 = 预计总支出 - 已报销
obj['预计未报销金额'] = new BigNumber(invoiceExcelData['预计总支出 (不含税)']).minus(new BigNumber(obj['已报销']));
// 汇总实际支付表内的期末余额
obj['已支付'] = actualPaymentData.filter(item => item.project_number === project.project_id).reduce((sum, item) => (sum).plus(new BigNumber(item.qimo_yueyuan || 0)), new BigNumber(0));
// 开票收入 - 预计总支出
obj['预计项目收益'] = new BigNumber(invoiceExcelData['开票收入(不含税)']).minus(new BigNumber(invoiceExcelData['预计总支出 (不含税)']));
// 开票收入 - 已报销
obj['系统项目收益'] = new BigNumber(invoiceExcelData['开票收入(不含税)']).minus(new BigNumber(obj['已报销']));
// -
obj['现金流'] = new BigNumber(invoiceExcelData['已回款']).minus(new BigNumber(obj['已支付']));
// 价值创造 = 0.54 * 开票金额(含税)
obj['价值创造'] = new BigNumber(invoiceExcelData['开票金额(含税)']).times(new BigNumber(0.54));
// 对
const merged = {
...data, ...sales_contract, ...purchase_contract, ...invoiceExcelData, ...obj, salesContracts, purchaseContractsList, modifiedProjectInvoiceData
};
const finalObj = processFinalObject(merged);
excelDatas.push(finalObj)
})
return excelDatas;
} catch (error) {
console.log(error)
throw error;
}
}
function processFinalObject(
merged: Record<string, any>,
): Record<string, any> {
for (const key in merged) {
if (merged.hasOwnProperty(key)) {
const value = merged[key];
if (BigNumber.isBigNumber(value)) {
merged[key] = value.toString();
} else {
merged[key] = value;
}
}
}
return merged;
}

View File

@@ -0,0 +1,87 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { withFileUpload } from 'next-multiparty';
import * as XLSX from 'xlsx';
import prismaClient from '@/lib/prisma'
import { PurchaseContractExecutionMaintenanceItem } from '../type';
const ExcelToMySQLFieldMapping = {
'数据id': 'data_id',
'发起人': 'initiator',
'项目编号': 'project_number',
'合同编号': 'contract_number',
'合同名称': 'contract_name',
'履约发起部门': 'lvyue_faqi_bumen',
'经办人': 'operator',
'相对方': 'xiangduifang',
'是否开口': 'is_open',
'合同总额': 'contract_amount',
'备注': 'remarks',
'合同余额': 'contract_balance',
'履约开始时间': 'performance_start_time',
'合同原件扫描件(盖章版)': 'hetong_yuanjian_saomiaojian_gaizhangban',
'钉钉采购审批单': 'dingtalk_procurement_approval',
'合同附件扫描件': 'contract_attachment_scan',
'数据库索引': 'database_index',
'审批编号': 'approval_number',
'创建时间': 'creation_time',
'创建人': 'creator',
'当前负责人': 'current_responsible',
'审批结果': 'approval_result',
'审批状态': 'approval_status',
'更新时间': 'update_time',
'创建人部门': 'creator_department',
'审批单标题': 'approval_form_title',
'历史审批人': 'historical_approvers',
'耗时(时:分:秒)': 'time_consumed'
}
export const config = {
api: {
bodyParser: false // 必须禁用默认bodyParser
}
};
export default withFileUpload(async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
try {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
// 2. 读取Excel文件
const fileBuffer = await req?.file?.toBuffer()
const workbook = XLSX.read(fileBuffer, { type: 'buffer' });
const firstSheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[firstSheetName];
const rawData = XLSX.utils.sheet_to_json(worksheet);
// 3. 数据处理和去重
const projectDataList:PurchaseContractExecutionMaintenanceItem[] = [];
// TODO 可以查询一次搞重新修改 看业务需求
for (let i = 0; i < rawData.length; i++) {
const rawItem = rawData[i];
const projectData: any = {
data_id: rawItem['数据id'],
};
for (const [excelKey, dbKey] of Object.entries(ExcelToMySQLFieldMapping)) {
projectData[dbKey] = rawItem[excelKey];
}
projectDataList.push(projectData);
}
// 插入数据到purchase_contract_execution_maintenance表
await prismaClient.purchase_contract_execution_maintenance.createMany({
data: projectDataList,
skipDuplicates: true, // 跳过重复数据
});
res.status(200).json({
code: 0,
data: null,
message: '采购合同履约数据导入成功'
});
} catch (error) {
res.status(500).json({ error: 'Internal server error', stack: error, });
}
})

View File

@@ -0,0 +1,90 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { withFileUpload } from 'next-multiparty';
import * as XLSX from 'xlsx';
import { PurchaseContractOrderManagementItem, PurchaseContractOrderManagementDBItem } from '../type'
import prismaClient from '@/lib/prisma'
const ExcelToMySQLFieldMapping = {
'数据id': 'data_id',
'采购合同编号': 'purchase_contract_number',
'采购用途': 'purchase_purpose',
'项目编号': 'project_number',
'关联销售合同编号': 'associated_sales_contract_number',
'合同名称': 'contract_name',
'采购审批单号(请先填写合同编号)': 'purchase_approval_number',
'采购合同名称(***请再次确认合同名称***)': 'purchase_contract_name',
'相对方': 'xiang_dui_fang',
'合同类别(是否开口)': 'he_tong_lei_bie',
'合同总金额(含税)': 'he_tong_zong_jin_e_HanShui',
'剩余金额(含税)': 'sheng_yu_jin_e_HanShui',
'合同剩余金额提醒': 'he_tong_sheng_yu_jin_e_ti_xing',
'本次订单名称': 'ben_ci_ding_dan_ming_cheng',
'本次订单金额(含税)': 'ben_ci_ding_dan_jin_e_HanShui',
'本次订单履约开始时间': 'ben_ci_ding_dan_lu_yue_kai_shi_shi_jian',
'本次订单履约结束时间': 'ben_ci_ding_dan_lu_yue_jie_shu_shi_jian',
'本次订单发起部门': 'ben_ci_ding_dan_fa_qi_bu_men',
'本次订单发起人': 'ben_ci_ding_dan_fa_qi_ren',
'本次订单发起时间': 'ben_ci_ding_dan_fa_qi_shi_jian',
'审批编号': 'approval_number',
'创建时间': 'creation_time',
'创建人': 'creator',
'当前负责人': 'current_responsible_person',
'审批结果': 'approval_result',
'审批状态': 'approval_status',
'更新时间': 'update_time',
'创建人部门': 'creator_department',
'审批单标题': 'approval_form_title',
'耗时(时:分:秒)': 'time_consumed'
}
export const config = {
api: {
bodyParser: false // 必须禁用默认bodyParser
}
};
export default withFileUpload(async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
try {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
// 2. 读取Excel文件
const fileBuffer = await req?.file?.toBuffer()
const workbook = XLSX.read(fileBuffer, { type: 'buffer' });
const firstSheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[firstSheetName];
const rawData: PurchaseContractOrderManagementItem[] = XLSX.utils.sheet_to_json(worksheet);
// 3. 数据处理和去重
const projectDataList: PurchaseContractOrderManagementDBItem[] = [];
// TODO 可以查询一次搞重新修改 看业务需求
for (let i = 0; i < rawData.length; i++) {
const rawItem = rawData[i];
const projectData: any = {
data_id: rawItem['数据id'],
};
for (const [excelKey, dbKey] of Object.entries(ExcelToMySQLFieldMapping)) {
const val = rawItem[excelKey];
const str = val === undefined || val === null ? '' : String(val).trim();
projectData[dbKey] = str === '' ? null : str;
}
projectDataList.push(projectData as PurchaseContractOrderManagementDBItem);
}
// 插入数据到purchase_contract_order_management表
await prismaClient.purchase_contract_order_management.createMany({
data: projectDataList,
skipDuplicates: true, // 跳过重复数据
});
res.status(200).json({
code: 0,
data: null,
message: '采购合同订单管理数据导入成功'
});
} catch (error) {
res.status(500).json({ error: 'Internal server error', stack: error, });
}
})

View File

@@ -0,0 +1,158 @@
/**
* @description 这里是应收账款台账表格
* 参考的拉取开票信息的逻辑
*
*/
import type { NextApiRequest, NextApiResponse } from 'next'
import prismaClient from '@/lib/prisma'
import dayjs from 'dayjs'
import { AGREEMENT_TYPES, AGREEMENT_TYPE_NAME, AGREEMENT_TYPE_CODE } from '@/CONSTANTS'
import type { InvoiceDataItem, ProcessedInvoiceDataItem } from '@/pages/api/type'
import { processInvoiceDataItem } from '@/lib/util'
import { processReceivableAccounts } from '@/pages/api/receivable_accounts/util'
type ResponseData = {
code: number
message: string
data?: {
data: any;
pagination?: {
total: number
pageNum: number
pageSize: number
totalPages: number
}
}
error?: any
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<ResponseData>
) {
try {
if (req.method != 'POST') {
return res.status(400).json({ message: '请求方法错误' });
}
// 从查询参数获取分页信息,设置默认值
const pageNum = parseInt(req.body.pageNum as string) || 1;
const pageSize = parseInt(req.body.pageSize as string) || 10;
const skip = (pageNum - 1) * pageSize;
const { agreementType, projectNumber, customerCompanyName, joinType = 'left' } = req.body;
const whereConditions: any = {};
if (agreementType) {
whereConditions.agreementType = agreementType as string;
}
if (projectNumber) {
whereConditions.projectNumber = {
contains: projectNumber as string,
};
}
if (customerCompanyName) {
whereConditions.customerCompanyName = {
contains: customerCompanyName as string,
};
}
if (isNaN(pageNum) || isNaN(pageSize) || pageNum < 1 || pageSize < 1) {
return res.status(400).json({ code: 400, message: '无效的分页参数' });
}
// 如果是inner 那么是完成开票结果表,否则是全量数据
const viewModel = prismaClient.invoice_application_match_view
// 获取分页数据和总记录数
const [invoiceData, total] = await Promise.all([
viewModel.findMany({
where: whereConditions,
skip,
take: pageSize,
orderBy: {
projectNumber: 'asc',
},
}),
viewModel.count({
where: whereConditions
})
]);
// 提取大协议的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}`);
}
});
// 在这里增加逻辑
// 1 增加 customerCompanyName 取自 invoiceData.customerCompanyNameLargeAgreement如果协议类型是大协议 和 invoiceData.customerCompanyNameIndependentAgreement 如果协议类型是独立协议
// 2 账期 invoice_period 是 开票日期和回款日期两个字段都存在 那么账期= 回款日期 payment_date -开票日期 invoice_date
// 根据项目编号将project表拉取过来
const projectNumberList = invoiceData.map((ele) => ele.projectNumber);
// 过滤空值,确保传入 Prisma 的 in 参数合法
const projectIdList = projectNumberList
.filter((v): v is string => typeof v === 'string' && v.trim() !== '');
// 如果过滤后为空,跳过数据库查询
const projectList = projectIdList.length > 0
? await prismaClient.project.findMany({
where: {
project_id: {
in: projectIdList
}
}
})
: [];
// 构建 Map 时过滤掉空的 project_id
const projectMap = new Map(
projectList
.filter((ele) => typeof ele.project_id === 'string' && (ele.project_id as string).trim() !== '')
.map((ele) => [ele.project_id as string, ele])
);
const modifiedInvoiceData = invoiceData
.map((ele) => processInvoiceDataItem(ele, businessContentMap))
.map(ele => processReceivableAccounts(ele, projectMap));
res.status(200).json({
code: 0,
error: null,
message: '数据获取成功',
data: {
data: modifiedInvoiceData,
pagination: {
total,
pageNum,
pageSize,
totalPages: Math.ceil(total / pageSize)
}
},
});
} catch (error) {
console.error('handler:', error);
res.status(500).json({ code: 500, message: '获取数据时出错' });
}
}

View File

@@ -0,0 +1,48 @@
import { ProcessedInvoiceDataItemFromFunction } from '@/pages/api/type'
export const departmentList = [
"运营",
"招聘",
"总务",
"薪酬",
"退休衔接",
"统筹-计核",
"统筹-社保",
"统筹-医保",
"统筹-支持",
"入转离",
"评鉴",
"培训-会务",
"国际化服务",
"管理咨询",
"客户服务",
"福利",
"法务",
"培训-数交",
"培训-科技",
"培训-经管",
"培训-技能",
"党校-党建",
"数字化"
];
export function processReceivableAccounts(invoiceData: ProcessedInvoiceDataItemFromFunction, projectMap: Map<string, any>) {
// const projectInfo = projectMap.get(invoiceData.projectNumber)
const obj: {
[k: string]: any
} = {
...invoiceData,
// delivery_area: projectInfo?.delivery_area,
// project_group: obj?.zhuan_ye_zu,
// 将departmentList中的每个部门作为key值为空字符串
...Object.fromEntries(departmentList.map(department => [department, ""]))
}
// delivery_area 交付区域 对应负责地区
// project_group 项目组别 申请口
// departmentList
// 如果申请口在 departmentList 内,那么将值设置为申请口
if (departmentList.includes(obj.zhuan_ye_zu)) {
obj[obj.zhuan_ye_zu as string] = `${obj.bu_han_shui_jin_e}`;
}
return obj;
}

View File

@@ -0,0 +1,97 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import fs from 'fs';
import path from 'path';
import { createReadStream } from 'fs';
// 定义响应数据类型
interface ResponseData {
message: string;
error?: string;
}
// Excel文件存放目录相对于项目根目录
const EXCEL_DIR = path.join(process.cwd(), 'public', 'excel');
// 确保Excel目录存在
if (!fs.existsSync(EXCEL_DIR)) {
fs.mkdirSync(EXCEL_DIR, { recursive: true });
console.log(`创建Excel目录: ${EXCEL_DIR}`);
}
// TODO 此处代码没有测试以及优化
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<ResponseData>
) {
try {
// 仅允许GET请求
if (req.method !== 'GET') {
return res.status(405).json({ message: '不支持的请求方法' });
}
// 获取查询参数中的文件名
const { filename } = req.query;
if (!filename || typeof filename !== 'string') {
return res.status(400).json({ message: '缺少或无效的文件名参数' });
}
// 验证文件名,防止路径遍历攻击
if (filename.includes('..') || path.isAbsolute(filename)) {
return res.status(400).json({ message: '文件名不允许包含路径遍历字符' });
}
// 构建完整文件路径
const filePath = path.join(EXCEL_DIR, filename);
// 检查文件是否存在
if (!fs.existsSync(filePath)) {
return res.status(404).json({ message: '文件不存在' });
}
// 获取文件信息
const fileStats = fs.statSync(filePath);
// 设置响应头
res.setHeader('Content-Type', getContentType(filename));
res.setHeader('Content-Length', fileStats.size.toString());
res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(filename)}"`);
// 创建文件读取流并管道到响应
const fileStream = createReadStream(filePath);
fileStream.pipe(res);
// 处理流错误
fileStream.on('error', (error) => {
console.error('文件流错误:', error);
if (!res.headersSent) {
res.status(500).json({ message: '文件读取错误', error: error.message });
}
});
// 流结束时结束响应
fileStream.on('end', () => {
res.end();
});
} catch (error) {
console.error('处理文件错误:', error);
res.status(500).json({
message: '处理文件时发生错误',
error: error instanceof Error ? error.message : String(error)
});
}
}
// 根据文件名获取Content-Type
function getContentType(filename: string): string {
const ext = path.extname(filename).toLowerCase();
switch (ext) {
case '.xlsx':
return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
case '.xls':
return 'application/vnd.ms-excel';
case '.csv':
return 'text/csv';
default:
return 'application/octet-stream';
}
}

View File

@@ -0,0 +1,46 @@
import type { NextApiRequest, NextApiResponse } from 'next'
// import { PrismaClient } from '@prisma/client'
import prismaClient from '@/lib/prisma'
import dayjs from 'dayjs'
import BigNumber from "bignumber.js";
type ResponseData = {
message: string
env?: string
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<ResponseData>
) {
// 查询所有用户
// const users = await prismaClient.test.findMany()
try {
let x = new BigNumber(123.4567);
let a = new BigNumber('0.1');
let b = new BigNumber('0.2');
let y = new BigNumber(undefined);
let z = new BigNumber(x);
let o = a.plus(b);
const tax2 = new BigNumber(2.995);
console.log(tax2.toFixed(2)); // 输出:"3.00"(正确)
console.log()
// await prismaClient.test.create({
// data: {
// id: 2,
// name: 'Alice',
// shcool: 'alice@prisma.io',
// },
// })
} catch (err) {
console.error(err)
}
const timestamp = dayjs().subtract(1, 'day').valueOf();
console.log(timestamp);
res.status(200).json({ message: 'Hello from Next.js!', env: process.env.ENVIRONMENT })
}

410
src/pages/api/type.ts Normal file
View File

@@ -0,0 +1,410 @@
export interface InvoiceApplicationItem {
'序号': string;
'数据id': string;
'申请日期': string;
'申请人': string;
'申请部门': string;
'开票类型': string;
'项目组别': string; //
'预计报销第三方成本(元)': string;
'业务开始时间': string;
'业务结束时间': string;
'合同编号': string;
'合同名称': string;
'上传合同附件': string;
'项目编号': string;
'是否含税': string;
'开票税率': string;
'是否多项目合并': string;
'多项目合并开票请添加': string;
'多项目合并开票请添加_1': string;
'多项目合并开票请添加_2': string;
'多项目合并开票请添加_3': string;
'上传盖章费用清单': string;
'客户单位名称': string;
'统一社会信用代码': string;
'开户行名称': string;
'银行账号': string;
'发票税目': string;
'业务内容(备注)': string;
'开票金额明细': string;
'开票金额明细_1': string;
'开票金额明细_2': string;
'开票金额明细_3': string;
'开票金额(元)独': string;
'不含税金额(元)独': string;
'不含税金额(元)独(大写)': string;
'含税金额(元)独': string;
'含税金额(元)独(大写)': string;
'不含税金额(元)多': string;
'不含税金额(元)多(大写)': string;
'含税金额(元)多': string;
'含税金额(元)多(大写)': string;
'不含税金额(元)大': string;
'含税总金额(元)大': string;
'不含税金额(元)(大+独)': string;
'不含税金额(元)(大+独)(大写)': string;
'含税总金额(元)(大+独)': string;
'含税总金额(元)(大+独)(大写)': string;
'审批编号': string;
'创建时间': string;
'创建人': string;
'当前负责人': string;
'审批结果': string;
'审批状态': string;
'更新时间': string;
'创建人部门': string;
'审批单标题': string;
'历史审批人': string;
'耗时(时:分:秒)': string;
'审批记录': string;
}
export interface InvoiceApplicationDBItem {
dataId: string;
applicationDate: string;
applicant: string;
applicationDepartment: string;
agreementType: string;
xiang_mu_zu_bie: string; // 项目组别
yuJiBaoXiaoDiSanFangChengBenYuan: string;
businessStartTime: string;
businessEndTime: string;
contractNumber: string;
contractName: string;
uploadedContractAttachment: string;
projectNumber: string;
is_tax_inclusive: string;
invoice_tax_rate: string;
is_multi_project_merge: string; // 是否多项目合并
uploadedSealedCostList: string;
customerCompanyName: string;
tong_yi_she_hui_xin_yong_dai_ma: string;
bankName: string;
bankAccount: string;
invoice_tax_category: string;
organizationType: string; // 单位性质
business_content_du: string; // 业务内容(独)
invoice_amount_yuan_du: string;
bu_han_shui_jin_e_yuan_du: string;
// bu_han_shui_jin_e_yuan_du_da_xie: string;
han_shui_jin_e_yuan_du: string;
// han_shui_jin_e_yuan_du_da_xie: string;
bu_han_shui_jin_e_yuan_duo: string; // 不含税金额(元)多
// bu_han_shui_jin_e_yuan_duo_da_xie: string; // 不含税金额(元)多(大写)
han_shui_jin_e_yuan_duo: string; // 含税金额(元)多
// han_shui_jin_e_yuan_duo_da_xie: string; // 含税金额(元)多(大写)
bu_han_shui_jin_e_yuan_da: string;
han_shui_zong_jin_e_yuan_da: string;
bu_han_shui_jin_e_yuan_da_du: string;
// bu_han_shui_jin_e_yuan_da_du_da_xie: string;
han_shui_zong_jin_e_yuan_da_du: string;
// han_shui_zong_jin_e_yuan_da_du_da_xie: string;
approvalNumber: string;
creationTime: string;
creator: string;
currentResponsiblePerson: string;
approvalResult: string;
approvalStatus: string;
updateTime: string;
creatorDepartment: string;
approvalFormTitle: string;
historicalApprovers: string;
timeConsumed: string;
approvalRecord: string;
BigInvoiceAmountList: InvoiceApplicationAmountDetails[];
multiProjectMergeInvoice: InvoiceApplicationAmountDetails[];
// 新增字段
kai_piao_shi_you?: string;
fa_piao_bei_zhu_lan_tian_xie_nei_rong?: string;
invoiceType?: string; // 中文原值
invoiceCode?: number; // 1=专用 2=普通
}
//
export interface InvoiceApplicationAmountDetails {
id?: number;
dataid: string;
agreementType: string;
project_number: string;
business_group_name: string;
project_name: string;
business_content: string;
invoice_amount: string;
yu_ji_bao_xiao_di_san_fang_cheng_ben?: string;
// 新增:单合同多家单位开票清单的扩展字段
customer_company_name?: string;
tong_yi_she_hui_xin_yong_dai_ma?: string;
bank_name?: string;
bank_account?: string;
payee?: string;
}
export interface InvoiceDataDBItem {
dataid: string;
piao_hao: string;
kai_piao_ri_qi: string;
hui_kuan_ri_qi: string;
hui_kuan_jin_e: string;
zhang_qi: string;
invoice_type_enum: number;
customer_company_name: string;
}
// 定义发票数据项的类型
export type InvoiceDataItem = {
applicationDepartment?: string | null;
invoiceJobNumber: string | null;
dataId: string;
approvalNumber?: string | null;
unit_nature?: string | null;
agreementType?: string | null;
is_multi_project_merge?: string | null;
projectNumber?: string | null;
customerCompanyName?: string | null;
piao_hao?: string | null;
bu_han_shui_jin_e_yuan_du?: string | null;
han_shui_jin_e_yuan_du?: string | null;
bu_han_shui_jin_e_yuan_da?: string | null;
han_shui_jin_e_yuan_da?: string | null;
bu_han_shui_jin_e_yuan_da_du?: string | null;
han_shui_jin_e_yuan_da_du?: string | null;
bu_han_shui_jin_e_yuan_duo?: string | null;
han_shui_jin_e_yuan_duo?: string | null;
applicant?: string | null;
zhuan_ye_zu?: string | null;
kai_piao_ri_qi?: string | null;
hui_kuan_ri_qi?: string | null;
hui_kuan_jin_e?: string | null;
zhang_qi?: string | null;
business_content_du?: string | null;
invoice_type_enum?: number | null;
// 新增字段
kai_piao_shi_you?: string | null;
tong_yi_she_hui_xin_yong_dai_ma?: string | null;
bankName?: string | null;
bankAccount?: string | null;
};
// 定义处理后的发票数据项类型
export type ProcessedInvoiceDataItem = InvoiceDataItem & {
customerCompanyName: string;
agreementTypeName: string;
bu_han_shui_jin_e: number;
shui_e: number;
han_shui_zong_jin_e: number;
business_content: string;
xiang_mu_zu_bie: string;
is_multi_project_merge: string;
organizationType: string;
};
export interface ProcessedInvoiceDataItemFromFunction {
dataId: string;
projectNumber: string;
agreementType: string | null;
agreementTypeName: string;
invoiceType?: string | null;
customerCompanyName: string;
piao_hao: string | null;
bu_han_shui_jin_e: number;
shui_e: number;
han_shui_zong_jin_e: number;
applicant: string | null;
zhuan_ye_zu: string | null;
kai_piao_ri_qi: string | null;
hui_kuan_ri_qi: string | null;
hui_kuan_jin_e: string | null;
business_content: string;
zhang_qi: string | null;
// 新增:透传给前端使用
tong_yi_she_hui_xin_yong_dai_ma?: string | null;
bankName?: string | null;
bankAccount?: string | null;
}
// 通用导入接口配置类型
export interface ImportConfig {
tableName: string; // Prisma模型名称
fieldMapping: { [excelKey: string]: string }; // Excel列名到数据库字段的映射
idField: string; // ID字段名
}
// 通用导入接口支持的表配置
export interface TableConfigs {
[key: string]: ImportConfig;
}
export interface PurchaseContractOrderManagementItem {
'数据id': string;
'采购合同编号': string;
'采购用途': string;
'项目编号(测试使用,非必填项)': string;
'关联销售合同编号(测试使用,非必填项)': string;
'合同名称(测试使用,非必填项)': string;
'采购审批单号(请先填写合同编号)': string;
'采购合同名称(***请再次确认合同名称***)': string;
'相对方': string;
'合同类别(是否开口)': string;
'合同总金额(含税)': string;
'剩余金额(含税)': string;
'合同剩余金额提醒': string;
'本次订单名称': string;
'本次订单金额(含税)': string;
'本次订单履约开始时间': string;
'本次订单履约结束时间': string;
'本次订单发起部门': string;
'本次订单发起人': string;
'本次订单发起时间': string;
'审批编号': string;
'创建时间': string;
'创建人': string;
'当前负责人': string;
'审批结果': string;
'审批状态': string;
'更新时间': string;
'创建人部门': string;
'审批单标题': string;
'耗时(时:分:秒)': string;
}
export interface PurchaseContractOrderManagementDBItem {
data_id: string;
purchase_contract_number: string;
purchase_purpose: string;
project_number: string;
associated_sales_contract_number: string;
contract_name: string;
purchase_approval_number: string;
purchase_contract_name: string;
xiang_dui_fang: string;
he_tong_lei_bie: string;
he_tong_zong_jin_e_HanShui: string;
sheng_yu_jin_e_HanShui: string;
he_tong_sheng_yu_jin_e_ti_xing: string;
ben_ci_ding_dan_ming_cheng: string;
ben_ci_ding_dan_jin_e_HanShui: string;
ben_ci_ding_dan_lu_yue_kai_shi_shi_jian: string;
ben_ci_ding_dan_lu_yue_jie_shu_shi_jian: string;
ben_ci_ding_dan_fa_qi_bu_men: string;
ben_ci_ding_dan_fa_qi_ren: string;
ben_ci_ding_dan_fa_qi_shi_jian: string;
approval_number: string;
creation_time: string;
creator: string;
current_responsible_person: string;
approval_result: string;
approval_status: string;
update_time: string;
creator_department: string;
approval_form_title: string;
time_consumed: string;
}
export interface PurchaseContractExecutionMaintenanceItem {
'数据id': string;
'发起人': string;
'项目编号': string;
'合同编号': string;
'合同名称': string;
'履约发起部门': string;
'经办人': string;
'相对方': string;
'是否开口': string;
'合同总额': string;
'备注': string;
'合同余额': string;
'履约开始时间': string;
'合同原件扫描件(盖章版)': string;
'钉钉采购审批单': string;
'合同附件扫描件': string;
'数据库索引': string;
'审批编号': string;
'创建时间': string;
'创建人': string;
'当前负责人': string;
'审批结果': string;
'审批状态': string;
'更新时间': string;
'创建人部门': string;
'审批单标题': string;
'历史审批人': string;
'耗时(时:分:秒)': string;
}
export interface PurchaseContractExecutionMaintenanceDBItem {
data_id: string;
initiator: string;
project_number: string;
contract_number: string;
contract_name: string;
lvyue_faqi_bumen: string;
operator: string;
xiangduifang: string;
is_open: string;
contract_amount: string;
remarks: string;
contract_balance: string;
performance_start_time: string;
hetong_yuanjian_saomiaojian_gaizhangban: string;
dingtalk_procurement_approval: string;
contract_attachment_scan: string;
database_index: string;
approval_number: string;
creation_time: string;
creator: string;
current_responsible: string;
approval_result: string;
approval_status: string;
update_time: string;
creator_department: string;
approval_form_title: string;
historical_approvers: string;
time_consumed: string;
}
export interface ProjectItem {
'数据id': string;
'组别缩写': string;
'上一项目编号': string;
'项目编号': string;
'项目名称': string;
'关联合同编号': string;
'合同名称': string;
'客户名称': string;
'交付区域(省/市)': string;
'单位性质': string;
'项目主担': string;
'项目组别': string;
'创建时间': string;
'创建者': string;
'创建人部门': string;
'审批单标题': string;
'更新时间': string;
}
export interface ProjectDBItem {
data_id: string;
group_abbreviation: string;
previous_project_id: string;
project_id: string;
project_name: string;
contract_id: string;
contract_name: string;
client_name: string;
delivery_area: string;
unit_nature: string;
project_leader: string;
project_group: string;
create_time: string;
creator: string;
creator_department: string;
approval_title: string;
update_time: string;
}

View File

@@ -0,0 +1,315 @@
{
"message": "Hello from Next.js! ",
"testjsonres": {
"headers": {
"server": "DingTalk/1.0.0",
"date": "Thu, 17 Jul 2025 09:37:20 GMT",
"content-type": "application/json;charset=utf-8",
"content-length": "8541",
"connection": "keep-alive",
"vary": "Accept-Encoding, Accept-Encoding",
"access-control-allow-origin": "*",
"access-control-expose-headers": "*",
"x-acs-request-id": "AA91573E-F016-7F65-B8A5-F27505F8EA18",
"x-acs-trace-id": "8c641caa4da3bb4c9feb718781fdacec",
"etag": "7vNso+AtO0PfoeVdtZAIK0w1",
"access-control-allow-headers": "X-Requested-With, X-Sequence, _aop_secret, _aop_signature, x-acs-dingtalk-access-token"
},
"statusCode": 200,
"body": {
"result": {
"attachedProcessInstanceIds": [],
"bizAction": "NONE",
"bizData": "{}",
"businessId": "202507090915000418108",
"ccUserIds": [
"216969073232379411"
],
"createTime": "2025-07-09T09:15Z",
"finishTime": "2025-07-09T09:16Z",
"formComponentValues": [
{
"bizAlias": "",
"componentType": "DDDateField",
"id": "DDDateField_13V7MHUHVIIO0",
"name": "申请日期",
"value": "2025-07-09"
},
{
"bizAlias": "",
"componentType": "InnerContactField",
"extValue": "[{\"emplId\":\"216969073232379411\",\"itemId\":\"216969073232379411\",\"name\":\"胡光涛\",\"avatar\":\"https://static.dingtalk.com/media/lQLPM4IlQBbhggPNAyDNAyCwvNY1MzXKvZwFdbEHbuBPAA_800_800.png\"}]",
"id": "InnerContactField_OWGVHGJ10XS0",
"name": "申请人",
"value": "胡光涛"
},
{
"bizAlias": "",
"componentType": "DepartmentField",
"extValue": "[{\"itemId\":665065400,\"name\":\"运营室\",\"id\":665065400,\"fullDeptName\":\"运营室-运营室\"}]",
"id": "DepartmentField_1QNRM3JZDCLC0",
"name": "申请部门",
"value": "运营室"
},
{
"bizAlias": "",
"componentType": "DDSelectField",
"extValue": "{\"label\":\"大协议\",\"key\":\"option_0\"}",
"id": "DDSelectField_MA0M7TBUYWG0",
"name": "开票类型",
"value": "大协议"
},
{
"bizAlias": "",
"componentType": "MoneyField",
"id": "MoneyField_1E8XNBRX7TC00",
"name": "预计报销第三方成本(元)"
},
{
"bizAlias": "",
"componentType": "DDDateRangeField",
"id": "DDDateRangeField_1T26OKK5W2XS0",
"name": "[\"业务开始时间\",\"业务结束时间\"]"
},
{
"bizAlias": "",
"componentType": "DDSelectField",
"id": "DDSelectField_R8TB2L9FFDC0",
"name": "项目编号"
},
{
"bizAlias": "",
"componentType": "TextField",
"id": "TextField_1KFIQW86NZFK0",
"name": "合同编号"
},
{
"bizAlias": "",
"componentType": "TextField",
"id": "TextField_1AEB73361Y5C0",
"name": "合同名称"
},
{
"bizAlias": "",
"componentType": "FormRelateField",
"id": "FormRelateField_T0MH0PPK4PS0",
"name": "合同管理"
},
{
"bizAlias": "",
"componentType": "DDAttachment",
"id": "DDAttachment_1MEL4XXTWXVK0",
"name": "上传盖章费用清单"
},
{
"bizAlias": "",
"componentType": "DDSelectField",
"extValue": "{\"label\":\"凌悦数字科技(武汉)有限公司\",\"key\":\"twR6_C5bQTGFUq29EK18vQ08211744200895\"}",
"id": "DDSelectField_VYKH4VFBEQO0",
"name": "客户单位名称(大协议)",
"value": "凌悦数字科技(武汉)有限公司"
},
{
"bizAlias": "",
"componentType": "TextField",
"id": "TextField_103IX9MP82PC0",
"name": "统一社会信用代码(税号)",
"value": "91420100MA7MX5417J"
},
{
"bizAlias": "",
"componentType": "TextField",
"id": "TextField_64DY8T2JAK80",
"name": "开户行名称",
"value": "招商银行武汉分行经济技术开发区支行"
},
{
"bizAlias": "",
"componentType": "TextField",
"id": "TextField_1YDT2JKYGCIO0",
"name": "银行账号",
"value": "127916557910601"
},
{
"bizAlias": "",
"componentType": "AddressField",
"extValue": "{\"province\":{\"name\":\"湖北省\",\"id\":\"420000\"},\"city\":{\"name\":\"武汉市\",\"id\":\"420100\"},\"district\":{\"name\":\"武汉市\",\"id\":\"420100\"}}",
"id": "AddressField_UH33ZMYZ6EO0",
"name": "负责地区",
"value": "湖北省,武汉市"
},
{
"bizAlias": "",
"componentType": "DDSelectField",
"extValue": "{\"label\":\"*人力资源服务*其他人力资源服务\",\"key\":\"option_0\"}",
"id": "DDSelectField_ZXX2MGPMIXC0",
"name": "发票税目",
"value": "*人力资源服务*其他人力资源服务"
},
{
"bizAlias": "",
"componentType": "TableField",
"extValue": "{\"statValue\":[],\"componentName\":\"TableField\"}",
"id": "TableField_QEXZCW3ZPXS0",
"name": "开票金额明细(大协议)",
"value": "[{\"rowValue\":[{\"label\":\"业务组名称\",\"extendValue\":[{\"itemId\":665065400,\"name\":\"运营室\",\"id\":665065400,\"fullDeptName\":\"运营室-运营室\"}],\"source\":\"system\",\"value\":\"运营室\",\"key\":\"DepartmentField_1BBHYIYCFQJK0\"},{\"label\":\"项目名称\",\"value\":\"测试(项目名称)\",\"key\":\"TextField_12GMJ6Y62POW0\"},{\"label\":\"是否含税\",\"extendValue\":{\"label\":\"是 \",\"key\":\"option_0\"},\"value\":\"是 \",\"key\":\"DDSelectField_2Z2HLVP44SC0\"},{\"label\":\"开票税率\",\"extendValue\":{\"label\":\"0.06\",\"key\":\"option_0\"},\"value\":\"0.06\",\"key\":\"DDSelectField_1YFZ1Y6YSDOG0\"},{\"label\":\"不含税金额(元)\",\"value\":\"15000\",\"key\":\"MoneyField_7U5GR5MZ0UO0\"},{\"label\":\"税额(元)\",\"source\":\"system\",\"value\":\"900.00\",\"key\":\"MoneyField_YK6EMUSBTK00\"},{\"label\":\"总金额(元)\",\"source\":\"system\",\"value\":\"15900.00\",\"key\":\"MoneyField_1HPIZ8GLETOG0\"}],\"rowNumber\":\"TableField_QEXZCW3ZPXS0_21VBDXI9TFNK0\"},{\"rowValue\":[{\"label\":\"业务组名称\",\"extendValue\":[{\"deptName\":\"客户服务室\",\"itemId\":\"982357292\",\"name\":\"客户服务室\",\"id\":\"982357292\"}],\"value\":\"客户服务室\",\"key\":\"DepartmentField_1BBHYIYCFQJK0\"},{\"label\":\"项目名称\",\"value\":\"测试(项目名称客户)\",\"key\":\"TextField_12GMJ6Y62POW0\"},{\"label\":\"是否含税\",\"extendValue\":{\"label\":\"否\",\"key\":\"option_1\"},\"value\":\"否\",\"key\":\"DDSelectField_2Z2HLVP44SC0\"},{\"label\":\"不含税金额(元)\",\"value\":\"25000\",\"key\":\"MoneyField_7U5GR5MZ0UO0\"},{\"label\":\"总金额(元)\",\"source\":\"system\",\"value\":\"25000.00\",\"key\":\"MoneyField_1HPIZ8GLETOG0\"}],\"rowNumber\":\"TableField_QEXZCW3ZPXS0_167AI8YI64HS0\"}]"
},
{
"bizAlias": "",
"componentType": "TextField",
"id": "TextField_1UYLNSUA86TC0",
"name": "客户单位名称(独立协议)"
},
{
"bizAlias": "",
"componentType": "TextField",
"id": "TextField_1Z97XGZ345HC0",
"name": "统一社会领用代码(税号)"
},
{
"bizAlias": "",
"componentType": "TextField",
"id": "TextField_22RX2SVA40TC0",
"name": "开户银行名称"
},
{
"bizAlias": "",
"componentType": "TextField",
"id": "TextField_21S87YGUK0BK0",
"name": "银行账号"
},
{
"bizAlias": "",
"componentType": "TextField",
"id": "TextField_UYO8FRG98280",
"name": "负责地区"
},
{
"bizAlias": "",
"componentType": "DDSelectField",
"id": "DDSelectField_J6LHLQNUQ1S0",
"name": "发票税目"
},
{
"bizAlias": "",
"componentType": "MoneyField",
"id": "MoneyField_SUUWS7GXHVK0",
"name": "不含税金额(元)"
},
{
"bizAlias": "",
"componentType": "MoneyField",
"id": "MoneyField_1RPVEX0YUYLC0",
"name": "总金额(元)"
},
{
"bizAlias": "",
"componentType": "MoneyField",
"id": "MoneyField_1CA9FPSAZURK0",
"name": "不含税金额(元)大",
"value": "40000.00"
},
{
"bizAlias": "",
"componentType": "MoneyField",
"id": "MoneyField_IQ8O9U5HXRS0",
"name": "总金额(元)大",
"value": "40900.00"
},
{
"bizAlias": "",
"componentType": "MoneyField",
"id": "MoneyField_AJ2560YHU4K0",
"name": "不含税金额(元)(大+独)"
},
{
"bizAlias": "",
"componentType": "MoneyField",
"id": "MoneyField_183UEPMEZ4IO0",
"name": "含税总金额(元)(大+独)"
},
{
"bizAlias": "",
"componentType": "SeqNumberField",
"id": "SeqNumberField_1U5LXXTMOS8W0",
"name": "开票流水号",
"value": "202500001"
}
],
"operationRecords": [
{
"date": "2025-07-09T09:15Z",
"images": [],
"result": "NONE",
"showName": "提交申请",
"type": "START_PROCESS_INSTANCE",
"userId": "216969073232379411"
},
{
"activityId": "2927_72a8",
"date": "2025-07-09T09:15Z",
"images": [],
"result": "AGREE",
"showName": "直接主管",
"type": "EXECUTE_TASK_NORMAL",
"userId": "216969073232379411"
},
{
"activityId": "f3f7_8c89",
"date": "2025-07-09T09:16Z",
"images": [],
"remark": "",
"result": "AGREE",
"showName": "开票人员",
"type": "EXECUTE_TASK_NORMAL",
"userId": "216969073232379411"
},
{
"activityId": "9955_7e43",
"ccUserIds": [
"216969073232379411"
],
"date": "2025-07-09T09:16Z",
"images": [],
"remark": "",
"result": "NONE",
"showName": "抄送人",
"type": "PROCESS_CC",
"userId": "216969073232379411"
}
],
"originatorDeptId": "665065400",
"originatorDeptName": "运营室-运营室",
"originatorUserId": "216969073232379411",
"result": "agree",
"status": "COMPLETED",
"tasks": [
{
"activityId": "2927_72a8",
"createTime": "2025-07-09T09:15Z",
"finishTime": "2025-07-09T09:15Z",
"mobileUrl": "aflow.dingtalk.com?procInsId=Gx01t4YsSMiKyotJTTdcPw08211752023741&taskId=94820045176&businessId=202507090915000418108",
"pcUrl": "aflow.dingtalk.com?procInsId=Gx01t4YsSMiKyotJTTdcPw08211752023741&taskId=94820045176&businessId=202507090915000418108",
"result": "AGREE",
"status": "COMPLETED",
"taskId": 94820045176,
"userId": "216969073232379411"
},
{
"activityId": "f3f7_8c89",
"createTime": "2025-07-09T09:15Z",
"finishTime": "2025-07-09T09:16Z",
"mobileUrl": "aflow.dingtalk.com?procInsId=Gx01t4YsSMiKyotJTTdcPw08211752023741&taskId=94840972088&businessId=202507090915000418108",
"pcUrl": "aflow.dingtalk.com?procInsId=Gx01t4YsSMiKyotJTTdcPw08211752023741&taskId=94840972088&businessId=202507090915000418108",
"result": "AGREE",
"status": "COMPLETED",
"taskId": 94840972088,
"userId": "216969073232379411"
}
],
"title": "胡光涛提交的开票申请"
},
"success": "true"
}
}
}

4
src/types/dingtalk.d.ts vendored Normal file
View File

@@ -0,0 +1,4 @@
declare module '@alicloud/dingtalk' {
const dingtalk: any;
export = dingtalk;
}

27
tsconfig.json Normal file
View File

@@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "src/types/**/*.d.ts"],
"exclude": ["node_modules"]
}

View File

@@ -0,0 +1,271 @@
SELECT
`d`.`dataId` AS `dataId`,
`d`.`invoiceJobNumber` AS `invoiceJobNumber`,
`d`.`agreementType` AS `agreementType`,
`d`.`approvalNumber` AS `approvalNumber`,
`d`.`salesContractNumber` AS `salesContractNumber`,
`d`.`applicationDepartment` AS `applicationDepartment`,
`d`.`yuJiBaoXiaoDiSanFangChengBenYuan` AS `yuJiBaoXiaoDiSanFangChengBenYuan`,
`d`.`is_multi_project_merge` AS `is_multi_project_merge`,
`d`.`is_single_contract_multi_customer` AS `is_single_contract_multi_customer`,
`d`.`projectNumber` AS `projectNumber`,
`d`.`customerCompanyName` AS `customerCompanyName`,
`d`.`customerCompanyNameOther` AS `customerCompanyNameOther`,
`i`.`piao_hao` AS `piao_hao`,
`d`.`bu_han_shui_jin_e_yuan_du` AS `bu_han_shui_jin_e_yuan_du`,
`d`.`han_shui_jin_e_yuan_du` AS `han_shui_jin_e_yuan_du`,
`d`.`bu_han_shui_jin_e_yuan_da` AS `bu_han_shui_jin_e_yuan_da`,
`d`.`han_shui_zong_jin_e_yuan_da` AS `han_shui_jin_e_yuan_da`,
`d`.`bu_han_shui_jin_e_yuan_da_du` AS `bu_han_shui_jin_e_yuan_da_du`,
`d`.`han_shui_zong_jin_e_yuan_da_du` AS `han_shui_jin_e_yuan_da_du`,
`d`.`bu_han_shui_jin_e_yuan_duo` AS `bu_han_shui_jin_e_yuan_duo`,
`d`.`han_shui_jin_e_yuan_duo` AS `han_shui_jin_e_yuan_duo`,
`d`.`bu_han_shui_jin_e_yuan_other` AS `bu_han_shui_jin_e_yuan_other`,
`d`.`han_shui_jin_e_yuan_other` AS `han_shui_jin_e_yuan_other`,
`d`.`applicant` AS `applicant`,
`d`.`business_content_du` AS `business_content_du`,
`d`.`uploadedContractAttachment` AS `uploadedContractAttachment`,
`d`.`uploadedSealedCostListDa` AS `uploadedSealedCostListDa`,
`d`.`uploadedSealedCostListDu` AS `uploadedSealedCostListDu`,
`d`.`invoiceType` AS `invoiceType`,
`d`.`invoiceCode` AS `invoiceCode`,
`d`.`kai_piao_shi_you` AS `kai_piao_shi_you`,
`d`.`fa_piao_bei_zhu_lan_tian_xie_nei_rong` AS `fa_piao_bei_zhu_lan_tian_xie_nei_rong`,
`d`.`tong_yi_she_hui_xin_yong_dai_ma` AS `tong_yi_she_hui_xin_yong_dai_ma`,
`d`.`bankName` AS `bankName`,
`d`.`bankAccount` AS `bankAccount`,
`i`.`invoice_type_enum` AS `invoice_type_enum`,
`i`.`kai_piao_ri_qi` AS `kai_piao_ri_qi`,
`i`.`hui_kuan_ri_qi` AS `hui_kuan_ri_qi`,
`i`.`hui_kuan_jin_e` AS `hui_kuan_jin_e`,
`i`.`zhang_qi` AS `zhang_qi`,
`c`.`customer_type` AS `unit_nature`,
`d`.`hong_chong_piao_hao` AS `hong_chong_piao_hao`
FROM
(
(
(
(
SELECT
`da`.`dataId` AS `dataId`,
`da`.`invoiceJobNumber` AS `invoiceJobNumber`,
`da`.`agreementType` AS `agreementType`,
`da`.`approvalNumber` AS `approvalNumber`,
`da`.`contractNumber` AS `salesContractNumber`,
`da`.`applicationDepartment` AS `applicationDepartment`,
`da`.`yuJiBaoXiaoDiSanFangChengBenYuan` AS `yuJiBaoXiaoDiSanFangChengBenYuan`,
`da`.`is_multi_project_merge` AS `is_multi_project_merge`,
`da`.`is_single_contract_multi_customer` AS `is_single_contract_multi_customer`,
`da`.`projectNumber` AS `projectNumber`,
`da`.`customerCompanyName` AS `customerCompanyName`,
`da`.`customerCompanyNameOther` AS `customerCompanyNameOther`,
`da`.`bu_han_shui_jin_e_yuan_du` AS `bu_han_shui_jin_e_yuan_du`,
`da`.`han_shui_jin_e_yuan_du` AS `han_shui_jin_e_yuan_du`,
`da`.`bu_han_shui_jin_e_yuan_da` AS `bu_han_shui_jin_e_yuan_da`,
`da`.`han_shui_zong_jin_e_yuan_da` AS `han_shui_zong_jin_e_yuan_da`,
`da`.`bu_han_shui_jin_e_yuan_da_du` AS `bu_han_shui_jin_e_yuan_da_du`,
`da`.`han_shui_zong_jin_e_yuan_da_du` AS `han_shui_zong_jin_e_yuan_da_du`,
`da`.`bu_han_shui_jin_e_yuan_duo` AS `bu_han_shui_jin_e_yuan_duo`,
`da`.`han_shui_jin_e_yuan_duo` AS `han_shui_jin_e_yuan_duo`,
`da`.`bu_han_shui_jin_e_yuan_other` AS `bu_han_shui_jin_e_yuan_other`,
`da`.`han_shui_jin_e_yuan_other` AS `han_shui_jin_e_yuan_other`,
`da`.`applicant` AS `applicant`,
`da`.`business_content_du` AS `business_content_du`,
`da`.`uploadedContractAttachment` AS `uploadedContractAttachment`,
`da`.`uploadedSealedCostListDa` AS `uploadedSealedCostListDa`,
`da`.`uploadedSealedCostListDu` AS `uploadedSealedCostListDu`,
`da`.`invoiceType` AS `invoiceType`,
`da`.`invoiceCode` AS `invoiceCode`,
`da`.`kai_piao_shi_you` AS `kai_piao_shi_you`,
`da`.`fa_piao_bei_zhu_lan_tian_xie_nei_rong` AS `fa_piao_bei_zhu_lan_tian_xie_nei_rong`,
`da`.`tong_yi_she_hui_xin_yong_dai_ma` AS `tong_yi_she_hui_xin_yong_dai_ma`,
`da`.`bankName` AS `bankName`,
`da`.`bankAccount` AS `bankAccount`,
`da`.`hong_chong_piao_hao` AS `hong_chong_piao_hao`,
(
(
SELECT
count(1)
FROM
`procurement_db`.`dingding_oa_invoice_application` `da2`
WHERE
(
(`da2`.`dataId` = `da`.`dataId`)
AND (
`da2`.`customerCompanyName` = `da`.`customerCompanyName`
)
AND (
(
`da2`.`invoiceJobNumber` > `da`.`invoiceJobNumber`
)
OR (
(
`da2`.`invoiceJobNumber` = `da`.`invoiceJobNumber`
)
AND (`da2`.`dataId` > `da`.`dataId`)
)
)
)
) + 1
) AS `rn_da`,
(
SELECT
count(1)
FROM
`procurement_db`.`dingding_oa_invoice_application` `da3`
WHERE
(
(`da3`.`dataId` = `da`.`dataId`)
AND (
`da3`.`customerCompanyName` = `da`.`customerCompanyName`
)
)
) AS `cnt_da`
FROM
`procurement_db`.`dingding_oa_invoice_application` `da`
)
) `d`
LEFT JOIN (
SELECT
`ib`.`dataid` AS `dataid`,
`ib`.`customer_company_name` AS `customer_company_name`,
`ib`.`piao_hao` AS `piao_hao`,
`ib`.`invoice_type_enum` AS `invoice_type_enum`,
`ib`.`kai_piao_ri_qi` AS `kai_piao_ri_qi`,
`ib`.`hui_kuan_ri_qi` AS `hui_kuan_ri_qi`,
`ib`.`hui_kuan_jin_e` AS `hui_kuan_jin_e`,
`ib`.`zhang_qi` AS `zhang_qi`,
(
(
SELECT
count(1)
FROM
(
SELECT
`procurement_db`.`invoice_data`.`dataid` AS `dataid`,
`procurement_db`.`invoice_data`.`customer_company_name` AS `customer_company_name`,
`procurement_db`.`invoice_data`.`piao_hao` AS `piao_hao`,
max(
`procurement_db`.`invoice_data`.`invoice_type_enum`
) AS `invoice_type_enum`,
max(`procurement_db`.`invoice_data`.`kai_piao_ri_qi`) AS `kai_piao_ri_qi`,
max(`procurement_db`.`invoice_data`.`hui_kuan_ri_qi`) AS `hui_kuan_ri_qi`,
max(`procurement_db`.`invoice_data`.`hui_kuan_jin_e`) AS `hui_kuan_jin_e`,
max(`procurement_db`.`invoice_data`.`zhang_qi`) AS `zhang_qi`
FROM
`procurement_db`.`invoice_data`
GROUP BY
`procurement_db`.`invoice_data`.`dataid`,
`procurement_db`.`invoice_data`.`customer_company_name`,
`procurement_db`.`invoice_data`.`piao_hao`
) `ib2`
WHERE
(
(`ib2`.`dataid` = `ib`.`dataid`)
AND (
`ib2`.`customer_company_name` = `ib`.`customer_company_name`
)
AND (
(
coalesce(`ib2`.`kai_piao_ri_qi`, '9999-12-31') < coalesce(`ib`.`kai_piao_ri_qi`, '9999-12-31')
)
OR (
(
coalesce(`ib2`.`kai_piao_ri_qi`, '9999-12-31') = coalesce(`ib`.`kai_piao_ri_qi`, '9999-12-31')
)
AND (`ib2`.`piao_hao` < `ib`.`piao_hao`)
)
)
)
) + 1
) AS `rn_id`,
(
SELECT
count(1)
FROM
(
SELECT
`procurement_db`.`invoice_data`.`dataid` AS `dataid`,
`procurement_db`.`invoice_data`.`customer_company_name` AS `customer_company_name`,
`procurement_db`.`invoice_data`.`piao_hao` AS `piao_hao`
FROM
`procurement_db`.`invoice_data`
GROUP BY
`procurement_db`.`invoice_data`.`dataid`,
`procurement_db`.`invoice_data`.`customer_company_name`,
`procurement_db`.`invoice_data`.`piao_hao`
) `ib3`
WHERE
(
(`ib3`.`dataid` = `ib`.`dataid`)
AND (
`ib3`.`customer_company_name` = `ib`.`customer_company_name`
)
)
) AS `cnt_id`
FROM
(
SELECT
`procurement_db`.`invoice_data`.`dataid` AS `dataid`,
`procurement_db`.`invoice_data`.`customer_company_name` AS `customer_company_name`,
`procurement_db`.`invoice_data`.`piao_hao` AS `piao_hao`,
max(
`procurement_db`.`invoice_data`.`invoice_type_enum`
) AS `invoice_type_enum`,
max(`procurement_db`.`invoice_data`.`kai_piao_ri_qi`) AS `kai_piao_ri_qi`,
max(`procurement_db`.`invoice_data`.`hui_kuan_ri_qi`) AS `hui_kuan_ri_qi`,
max(`procurement_db`.`invoice_data`.`hui_kuan_jin_e`) AS `hui_kuan_jin_e`,
max(`procurement_db`.`invoice_data`.`zhang_qi`) AS `zhang_qi`
FROM
`procurement_db`.`invoice_data`
GROUP BY
`procurement_db`.`invoice_data`.`dataid`,
`procurement_db`.`invoice_data`.`customer_company_name`,
`procurement_db`.`invoice_data`.`piao_hao`
) `ib`
) `i` ON(
(
(`i`.`dataid` = `d`.`dataId`)
AND (
`i`.`customer_company_name` = `d`.`customerCompanyName`
)
AND (
`d`.`rn_da` = (((`i`.`rn_id` - 1) % `d`.`cnt_da`) + 1)
)
)
)
)
LEFT JOIN (
SELECT
`c1`.`customer_unit_name` AS `customer_unit_name`,
max(`c1`.`customer_type`) AS `customer_type`
FROM
(
`procurement_db`.`customer_management_info` `c1`
JOIN (
SELECT
`procurement_db`.`customer_management_info`.`customer_unit_name` AS `customer_unit_name`,
max(
`procurement_db`.`customer_management_info`.`updated_at`
) AS `updated_at`
FROM
`procurement_db`.`customer_management_info`
GROUP BY
`procurement_db`.`customer_management_info`.`customer_unit_name`
) `latest` ON(
(
(
`c1`.`customer_unit_name` = `latest`.`customer_unit_name`
)
AND (`c1`.`updated_at` = `latest`.`updated_at`)
)
)
)
GROUP BY
`c1`.`customer_unit_name`
) `c` ON(
(
`d`.`customerCompanyName` = `c`.`customer_unit_name`
)
)
)

View File

@@ -0,0 +1,271 @@
SELECT
`d`.`dataId` AS `dataId`,
`d`.`invoiceJobNumber` AS `invoiceJobNumber`,
`d`.`agreementType` AS `agreementType`,
`d`.`approvalNumber` AS `approvalNumber`,
`d`.`salesContractNumber` AS `salesContractNumber`,
`d`.`applicationDepartment` AS `applicationDepartment`,
`d`.`yuJiBaoXiaoDiSanFangChengBenYuan` AS `yuJiBaoXiaoDiSanFangChengBenYuan`,
`d`.`is_multi_project_merge` AS `is_multi_project_merge`,
`d`.`is_single_contract_multi_customer` AS `is_single_contract_multi_customer`,
`d`.`projectNumber` AS `projectNumber`,
`d`.`customerCompanyName` AS `customerCompanyName`,
`d`.`customerCompanyNameOther` AS `customerCompanyNameOther`,
`i`.`piao_hao` AS `piao_hao`,
`d`.`bu_han_shui_jin_e_yuan_du` AS `bu_han_shui_jin_e_yuan_du`,
`d`.`han_shui_jin_e_yuan_du` AS `han_shui_jin_e_yuan_du`,
`d`.`bu_han_shui_jin_e_yuan_da` AS `bu_han_shui_jin_e_yuan_da`,
`d`.`han_shui_zong_jin_e_yuan_da` AS `han_shui_jin_e_yuan_da`,
`d`.`bu_han_shui_jin_e_yuan_da_du` AS `bu_han_shui_jin_e_yuan_da_du`,
`d`.`han_shui_zong_jin_e_yuan_da_du` AS `han_shui_jin_e_yuan_da_du`,
`d`.`bu_han_shui_jin_e_yuan_duo` AS `bu_han_shui_jin_e_yuan_duo`,
`d`.`han_shui_jin_e_yuan_duo` AS `han_shui_jin_e_yuan_duo`,
`d`.`bu_han_shui_jin_e_yuan_other` AS `bu_han_shui_jin_e_yuan_other`,
`d`.`han_shui_jin_e_yuan_other` AS `han_shui_jin_e_yuan_other`,
`d`.`applicant` AS `applicant`,
`d`.`business_content_du` AS `business_content_du`,
`d`.`uploadedContractAttachment` AS `uploadedContractAttachment`,
`d`.`uploadedSealedCostListDa` AS `uploadedSealedCostListDa`,
`d`.`uploadedSealedCostListDu` AS `uploadedSealedCostListDu`,
`d`.`invoiceType` AS `invoiceType`,
`d`.`invoiceCode` AS `invoiceCode`,
`d`.`kai_piao_shi_you` AS `kai_piao_shi_you`,
`d`.`fa_piao_bei_zhu_lan_tian_xie_nei_rong` AS `fa_piao_bei_zhu_lan_tian_xie_nei_rong`,
`d`.`tong_yi_she_hui_xin_yong_dai_ma` AS `tong_yi_she_hui_xin_yong_dai_ma`,
`d`.`bankName` AS `bankName`,
`d`.`bankAccount` AS `bankAccount`,
`i`.`invoice_type_enum` AS `invoice_type_enum`,
`i`.`kai_piao_ri_qi` AS `kai_piao_ri_qi`,
`i`.`hui_kuan_ri_qi` AS `hui_kuan_ri_qi`,
`i`.`hui_kuan_jin_e` AS `hui_kuan_jin_e`,
`i`.`zhang_qi` AS `zhang_qi`,
`c`.`customer_type` AS `unit_nature`,
`d`.`hong_chong_piao_hao` AS `hong_chong_piao_hao`
FROM
(
(
(
(
SELECT
`da`.`dataId` AS `dataId`,
`da`.`invoiceJobNumber` AS `invoiceJobNumber`,
`da`.`agreementType` AS `agreementType`,
`da`.`approvalNumber` AS `approvalNumber`,
`da`.`contractNumber` AS `salesContractNumber`,
`da`.`applicationDepartment` AS `applicationDepartment`,
`da`.`yuJiBaoXiaoDiSanFangChengBenYuan` AS `yuJiBaoXiaoDiSanFangChengBenYuan`,
`da`.`is_multi_project_merge` AS `is_multi_project_merge`,
`da`.`is_single_contract_multi_customer` AS `is_single_contract_multi_customer`,
`da`.`projectNumber` AS `projectNumber`,
`da`.`customerCompanyName` AS `customerCompanyName`,
`da`.`customerCompanyNameOther` AS `customerCompanyNameOther`,
`da`.`bu_han_shui_jin_e_yuan_du` AS `bu_han_shui_jin_e_yuan_du`,
`da`.`han_shui_jin_e_yuan_du` AS `han_shui_jin_e_yuan_du`,
`da`.`bu_han_shui_jin_e_yuan_da` AS `bu_han_shui_jin_e_yuan_da`,
`da`.`han_shui_zong_jin_e_yuan_da` AS `han_shui_zong_jin_e_yuan_da`,
`da`.`bu_han_shui_jin_e_yuan_da_du` AS `bu_han_shui_jin_e_yuan_da_du`,
`da`.`han_shui_zong_jin_e_yuan_da_du` AS `han_shui_zong_jin_e_yuan_da_du`,
`da`.`bu_han_shui_jin_e_yuan_duo` AS `bu_han_shui_jin_e_yuan_duo`,
`da`.`han_shui_jin_e_yuan_duo` AS `han_shui_jin_e_yuan_duo`,
`da`.`bu_han_shui_jin_e_yuan_other` AS `bu_han_shui_jin_e_yuan_other`,
`da`.`han_shui_jin_e_yuan_other` AS `han_shui_jin_e_yuan_other`,
`da`.`applicant` AS `applicant`,
`da`.`business_content_du` AS `business_content_du`,
`da`.`uploadedContractAttachment` AS `uploadedContractAttachment`,
`da`.`uploadedSealedCostListDa` AS `uploadedSealedCostListDa`,
`da`.`uploadedSealedCostListDu` AS `uploadedSealedCostListDu`,
`da`.`invoiceType` AS `invoiceType`,
`da`.`invoiceCode` AS `invoiceCode`,
`da`.`kai_piao_shi_you` AS `kai_piao_shi_you`,
`da`.`fa_piao_bei_zhu_lan_tian_xie_nei_rong` AS `fa_piao_bei_zhu_lan_tian_xie_nei_rong`,
`da`.`tong_yi_she_hui_xin_yong_dai_ma` AS `tong_yi_she_hui_xin_yong_dai_ma`,
`da`.`bankName` AS `bankName`,
`da`.`bankAccount` AS `bankAccount`,
`da`.`hong_chong_piao_hao` AS `hong_chong_piao_hao`,
(
(
SELECT
count(1)
FROM
`procurement_db`.`dingding_oa_invoice_application` `da2`
WHERE
(
(`da2`.`dataId` = `da`.`dataId`)
AND (
`da2`.`customerCompanyName` = `da`.`customerCompanyName`
)
AND (
(
`da2`.`invoiceJobNumber` > `da`.`invoiceJobNumber`
)
OR (
(
`da2`.`invoiceJobNumber` = `da`.`invoiceJobNumber`
)
AND (`da2`.`dataId` > `da`.`dataId`)
)
)
)
) + 1
) AS `rn_da`,
(
SELECT
count(1)
FROM
`procurement_db`.`dingding_oa_invoice_application` `da3`
WHERE
(
(`da3`.`dataId` = `da`.`dataId`)
AND (
`da3`.`customerCompanyName` = `da`.`customerCompanyName`
)
)
) AS `cnt_da`
FROM
`procurement_db`.`dingding_oa_invoice_application` `da`
)
) `d`
LEFT JOIN (
SELECT
`ib`.`dataid` AS `dataid`,
`ib`.`customer_company_name` AS `customer_company_name`,
`ib`.`piao_hao` AS `piao_hao`,
`ib`.`invoice_type_enum` AS `invoice_type_enum`,
`ib`.`kai_piao_ri_qi` AS `kai_piao_ri_qi`,
`ib`.`hui_kuan_ri_qi` AS `hui_kuan_ri_qi`,
`ib`.`hui_kuan_jin_e` AS `hui_kuan_jin_e`,
`ib`.`zhang_qi` AS `zhang_qi`,
(
(
SELECT
count(1)
FROM
(
SELECT
`procurement_db`.`invoice_data`.`dataid` AS `dataid`,
`procurement_db`.`invoice_data`.`customer_company_name` AS `customer_company_name`,
`procurement_db`.`invoice_data`.`piao_hao` AS `piao_hao`,
max(
`procurement_db`.`invoice_data`.`invoice_type_enum`
) AS `invoice_type_enum`,
max(`procurement_db`.`invoice_data`.`kai_piao_ri_qi`) AS `kai_piao_ri_qi`,
max(`procurement_db`.`invoice_data`.`hui_kuan_ri_qi`) AS `hui_kuan_ri_qi`,
max(`procurement_db`.`invoice_data`.`hui_kuan_jin_e`) AS `hui_kuan_jin_e`,
max(`procurement_db`.`invoice_data`.`zhang_qi`) AS `zhang_qi`
FROM
`procurement_db`.`invoice_data`
GROUP BY
`procurement_db`.`invoice_data`.`dataid`,
`procurement_db`.`invoice_data`.`customer_company_name`,
`procurement_db`.`invoice_data`.`piao_hao`
) `ib2`
WHERE
(
(`ib2`.`dataid` = `ib`.`dataid`)
AND (
`ib2`.`customer_company_name` = `ib`.`customer_company_name`
)
AND (
(
coalesce(`ib2`.`kai_piao_ri_qi`, '9999-12-31') < coalesce(`ib`.`kai_piao_ri_qi`, '9999-12-31')
)
OR (
(
coalesce(`ib2`.`kai_piao_ri_qi`, '9999-12-31') = coalesce(`ib`.`kai_piao_ri_qi`, '9999-12-31')
)
AND (`ib2`.`piao_hao` < `ib`.`piao_hao`)
)
)
)
) + 1
) AS `rn_id`,
(
SELECT
count(1)
FROM
(
SELECT
`procurement_db`.`invoice_data`.`dataid` AS `dataid`,
`procurement_db`.`invoice_data`.`customer_company_name` AS `customer_company_name`,
`procurement_db`.`invoice_data`.`piao_hao` AS `piao_hao`
FROM
`procurement_db`.`invoice_data`
GROUP BY
`procurement_db`.`invoice_data`.`dataid`,
`procurement_db`.`invoice_data`.`customer_company_name`,
`procurement_db`.`invoice_data`.`piao_hao`
) `ib3`
WHERE
(
(`ib3`.`dataid` = `ib`.`dataid`)
AND (
`ib3`.`customer_company_name` = `ib`.`customer_company_name`
)
)
) AS `cnt_id`
FROM
(
SELECT
`procurement_db`.`invoice_data`.`dataid` AS `dataid`,
`procurement_db`.`invoice_data`.`customer_company_name` AS `customer_company_name`,
`procurement_db`.`invoice_data`.`piao_hao` AS `piao_hao`,
max(
`procurement_db`.`invoice_data`.`invoice_type_enum`
) AS `invoice_type_enum`,
max(`procurement_db`.`invoice_data`.`kai_piao_ri_qi`) AS `kai_piao_ri_qi`,
max(`procurement_db`.`invoice_data`.`hui_kuan_ri_qi`) AS `hui_kuan_ri_qi`,
max(`procurement_db`.`invoice_data`.`hui_kuan_jin_e`) AS `hui_kuan_jin_e`,
max(`procurement_db`.`invoice_data`.`zhang_qi`) AS `zhang_qi`
FROM
`procurement_db`.`invoice_data`
GROUP BY
`procurement_db`.`invoice_data`.`dataid`,
`procurement_db`.`invoice_data`.`customer_company_name`,
`procurement_db`.`invoice_data`.`piao_hao`
) `ib`
) `i` ON(
(
(`i`.`dataid` = `d`.`dataId`)
AND (
`i`.`customer_company_name` = `d`.`customerCompanyName`
)
AND (
`d`.`rn_da` = (((`i`.`rn_id` - 1) % `d`.`cnt_da`) + 1)
)
)
)
)
LEFT JOIN (
SELECT
`c1`.`customer_unit_name` AS `customer_unit_name`,
max(`c1`.`customer_type`) AS `customer_type`
FROM
(
`procurement_db`.`customer_management_info` `c1`
JOIN (
SELECT
`procurement_db`.`customer_management_info`.`customer_unit_name` AS `customer_unit_name`,
max(
`procurement_db`.`customer_management_info`.`updated_at`
) AS `updated_at`
FROM
`procurement_db`.`customer_management_info`
GROUP BY
`procurement_db`.`customer_management_info`.`customer_unit_name`
) `latest` ON(
(
(
`c1`.`customer_unit_name` = `latest`.`customer_unit_name`
)
AND (`c1`.`updated_at` = `latest`.`updated_at`)
)
)
)
GROUP BY
`c1`.`customer_unit_name`
) `c` ON(
(
`d`.`customerCompanyName` = `c`.`customer_unit_name`
)
)
)