Compare commits

...
17 Commits
Author SHA1 Message Date
ops 695ba1530e fix(ci): 构建容器名唯一化 + npm 走国内镜像源(避免并发互踩 exit 137) 2026-10-21 06:24:25 +00:00
ops c2f6f42a3c ci: apk 源统一走 aliyun(规避 dl-cdn 时段性被拦卡 60s) 2026-10-21 01:36:56 +00:00
wanghu 2b9389fcb8 附件
build / build-push (push) Successful in 1m55s
2026-09-09 20:30:15 +08:00
wanghu 44d3cd0326 去掉小数点
build / build-push (push) Successful in 40s
2026-09-09 19:01:08 +08:00
wanghu 3ff9b48a19 修改地址
build / build-push (push) Successful in 40s
2026-09-09 17:39:30 +08:00
wanghu 20325bed00 签名
build / build-push (push) Successful in 39s
2026-09-09 16:49:55 +08:00
wanghu 0efde94a74 附件
build / build-push (push) Successful in 1m21s
2026-09-09 16:27:17 +08:00
wanghu 4211b72041 Config.FilePath
build / build-push (push) Successful in 1m24s
2026-09-09 16:04:46 +08:00
wanghu e9e1c1722d uploud
build / build-push (push) Successful in 44s
2026-09-09 15:54:50 +08:00
wanghu 583d06f3d0 pdf
build / build-push (push) Successful in 1m55s
2026-09-09 12:07:17 +08:00
wanghu 83182cb538 插件
build / build-push (push) Failing after 58s
2026-09-09 12:02:05 +08:00
wanghu bbcb865055 插件
build / build-push (push) Failing after 21s
2026-09-09 12:01:45 +08:00
wanghu cf8d6da5b5 提交
build / build-push (push) Successful in 28s
2026-09-09 08:57:41 +08:00
wanghu ae1bd4cdb0 注入
build / build-push (push) Successful in 28s
2026-09-08 21:18:29 +08:00
wanghu f1059fd92b 大小写
build / build-push (push) Successful in 33s
2026-09-08 17:42:25 +08:00
wanghu 5785a2c10e 更新大小写
build / build-push (push) Successful in 28s
2026-09-08 17:29:56 +08:00
wanghu 10c791d034 编辑
build / build-push (push) Successful in 28s
2026-09-08 16:55:39 +08:00
22 changed files with 2122 additions and 138 deletions
+17 -8
View File
@@ -26,7 +26,10 @@ jobs:
options: --network infra-infra-net
steps:
- name: 安装 git / curl(alpine)
run: apk add --no-cache git curl
run: |
# 统一走 aliyun 源:dl-cdn 时段性被拦会卡满 60s×2(2026-10-20 事故)
sed -i 's#dl-cdn.alpinelinux.org#mirrors.aliyun.com#g' /etc/apk/repositories
apk add --no-cache git curl
- name: 拉取源码(内网无 GitHub,手动 clone 本仓库)
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -53,14 +56,20 @@ jobs:
- name: 构建 publish 产物(dotnet sdk 容器,复用宿主 nuget 缓存)
run: |
set -e
docker rm -f dotnetbuild 2>/dev/null || true
docker run -d --name dotnetbuild \
# 构建容器名必须全局唯一:runner 挂的是宿主 docker.sock,容器名在整个 daemon 上共享
# 命名空间。固定名会在多仓/多条 workflow 并发时互相 docker rm -f 掉对方正在构建的
# 容器,表现为无编译报错的 exit 137(SIGKILL,2026-10-21 实测复现)。
# GITHUB_RUN_ID 实例级唯一,再拼时间戳兜底。
BUILD_CONTAINER="dotnetbuild-${GITHUB_RUN_ID:-0}-$(date +%s)"
echo "BUILD_CONTAINER=${BUILD_CONTAINER}"
docker rm -f "$BUILD_CONTAINER" 2>/dev/null || true
docker run -d --name "$BUILD_CONTAINER" \
--network infra-infra-net \
-v /opt/docker/projman-dotnet-nuget:/root/.nuget/packages \
-w /build mcr.microsoft.com/dotnet/sdk:5.0 sleep 3600
docker cp "${PWD}/." dotnetbuild:/build/
docker exec dotnetbuild sh -c "find /build -maxdepth 2 -iname nuget.config -exec sed -i 's|//nexus.yunhaixingtong.com|//nexus:8081|g' {} +"
docker exec dotnetbuild sh -c '
docker cp "${PWD}/." "$BUILD_CONTAINER":/build/
docker exec "$BUILD_CONTAINER" sh -c "find /build -maxdepth 2 -iname nuget.config -exec sed -i 's|//nexus.yunhaixingtong.com|//nexus:8081|g' {} +"
docker exec "$BUILD_CONTAINER" sh -c '
set -e
root_sln=$(ls /build/*.sln 2>/dev/null | head -1 || true)
root_csproj=$(ls /build/*.csproj 2>/dev/null | head -1 || true)
@@ -88,8 +97,8 @@ jobs:
echo "publish target: $target"
'
mkdir -p "${PWD}/publish"
docker cp dotnetbuild:/app/. "${PWD}/publish/"
docker rm -f dotnetbuild
docker cp "$BUILD_CONTAINER":/app/. "${PWD}/publish/"
docker rm -f "$BUILD_CONTAINER"
# 入口脚本:runtimeconfig.json 是可执行项目唯一标志(X.runtimeconfig.json <-> X.dll),
# 类库 deps.json 无 runtimeconfig 兄弟文件,天然不会误选
entry=$(ls "${PWD}"/publish/*.runtimeconfig.json | head -1)
-53
View File
@@ -1,53 +0,0 @@
# DocprintService
> 本仓库由 projman 自动创建并注入 CI(`.gitea/workflows/build.yml`)与 Dockerfile(`docker/Dockerfile`)。
> 推送代码到 main 分支即自动构建镜像并推送到 Nexus,projman 收到通知后建档待部署,由运维在 projman「后端发布」手动点部署。
## 首次推送本地代码到本仓库
> 目标:把本地已有代码推上去,同时**保留** projman 注入的文件(`build.yml`、`Dockerfile`)。
```bash
# 1. 进入本地代码目录
cd /your/local/code/dir
# 2. 初始化 git(目录已有 .git 也安全,git init 幂等无副作用)
git init
git branch -M main
# 3. 先把本地代码提交成一个 commit(关键!不提交直接 merge 会丢失本地代码)
git add -A
git commit -m "init local code"
# 4. 关联远程仓库(幂等:已存在 origin 则改地址,不报错)
git remote add origin http://gitea.yunhaixingtong.com/docprint/DocprintService.git 2>/dev/null \
|| git remote set-url origin http://gitea.yunhaixingtong.com/docprint/DocprintService.git
# 5. 拉取远程信息(只下载,不合并)
git fetch origin
# 6. 合并远程 main(关键步骤)
# 本地与远程是两条独立历史,需允许合并不相关历史。
# 此步会把 projman 注入的 build.yml 与 Dockerfile 合并进你的本地工作区。
git merge origin/main --allow-unrelated-histories -m "merge projman injected files"
# 7. 查看状态(确认 .gitea/workflows/build.yml、docker/Dockerfile 已在本地)
git status
# 8. 提交并推送
git add -A
git commit -m "sync local code" 2>/dev/null || true
git push origin main
```
## 推送后验证
push 触发 CI 构建,前往 Gitea 仓库 → **Actions** 查看日志;构建成功后镜像推送到 Nexus,projman 建档待部署,在 projman 应用详情「后端发布」手动点部署。
## 禁止写法(会导致注入文件被删)
`git reset origin/main` + `git add -A` 会把远程已注入、但你本地没有的文件(`build.yml`、`Dockerfile`)当作"删除"提交并推送,导致这些文件被删、CI 失败。务必用 `git merge` 而非 `git reset`,且 merge 前先提交本地代码。
## 关于 Dockerfile
projman 注入的 Dockerfile 仅在仓库不存在时写入;你自备的 Dockerfile 优先,不会被覆盖。
+3 -1
View File
@@ -8,10 +8,12 @@ FROM mcr.microsoft.com/dotnet/aspnet:${DOTNET_VERSION}
# .NET 5 基础镜像基于 Debian 10(已 EOL),apt 源须切 archive.debian.org 才能装包
# curl 供 projman compose healthcheck(test: CMD curl -f ...)使用;tzdata 设上海时区
# wkhtmltopdf 供审批单 PDF 接口(DwonWYSQD)本地转换 HTML(Debian 源为 0.12.5,无需 --enable-local-file-access);
# fonts-wqy-zenhei 提供中文字体(缺失则 PDF 中文显示方块)
RUN sed -i 's|deb.debian.org|archive.debian.org|g; s|security.debian.org|archive.debian.org|g' /etc/apt/sources.list \
&& echo 'Acquire::Check-Valid-Until "false";' > /etc/apt/apt.conf.d/99no-valid-until \
&& apt-get update \
&& apt-get install -y --no-install-recommends curl tzdata \
&& apt-get install -y --no-install-recommends curl tzdata wkhtmltopdf fonts-wqy-zenhei \
&& ln -snf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo "Asia/Shanghai" > /etc/timezone \
&& rm -rf /var/lib/apt/lists/*
+3
View File
@@ -21,5 +21,8 @@ namespace YhCore.Common.Config
/// <summary>文件访问 URL 前缀(对应 Java FILE_WEB_PATH,由 Startup 注入)</summary>
public static string FileWebPath { get; set; }
/// <summary>wkhtmltopdf 可执行文件路径(审批单 PDF 用;空则用 PATH 中的 wkhtmltopdf,由 Startup 注入)</summary>
public static string WkHtmlToPdfPath { get; set; }
}
}
+1 -1
View File
@@ -17,7 +17,7 @@
<ItemGroup>
<!-- 平台模型与信封(DException/ErrorCode/BoaUser 等):从 Nexus nuget-group 引用(原为本地源项目引用) -->
<PackageReference Include="Yh.Platform.Abstractions" Version="1.0.0-preview.1" />
<PackageReference Include="Yh.Platform.Abstractions" Version="1.0.0-preview.2" />
</ItemGroup>
</Project>
+1 -13
View File
@@ -68,22 +68,10 @@ namespace YhCore.Model.Dto
/// <summary>全部字典类别(typeID, typeName),对齐旧 EnumConfig.Dictionary 的 Description。</summary>
public static readonly (string Id, string Name)[] AllTypes =
{
("DEPT", "部门类别"),
("BXBM", "报修部门"),
("WYFL", "文印分类"),
("YZBM", "印制版面"),
("YZFL", "印制分类"),
("LeaveType", "请假类别"),
("FoodType", "物品类别"),
("Unit", "单位"),
("VenueType", "场馆类别"),
("Vefacilities", "场馆设施"),
("Time", "场馆时间"),
("Repeat", "场馆重复"),
("LevelOrder", "奖次"),
("filetype", "文件类型"),
("doclevel", "公文级别"),
("XKCategory", "选课类型"),
("filetype", "文件类型")
};
/// <summary>GetSysDic 的 Type(int) → typeID;越界返回 null。</summary>
+2 -2
View File
@@ -2,7 +2,7 @@ namespace YhCore.Model.Dto
{
/// <summary>
/// 文件信息(旧 BestSchool.Entity.FileObj,GetEnclosure/FileSave/FileSaveImage/FileMergeSave 共用)。
/// fileurl 为相对路径(如 /UploadFile/docprint/20260101/{guid}.jpg,与旧库数据格式一致);
/// fileurl 为相对路径(如 /upload/docprint/20260101/{guid}.jpg,与旧库数据格式一致);
/// host 为文件访问主机前缀(旧 AuthConfig.weboa_url,新 Config.FileWebPath)。
/// </summary>
public class FileObj
@@ -19,7 +19,7 @@ namespace YhCore.Model.Dto
/// <summary>文件真实名称(含扩展名)。</summary>
public string filename { get; set; }
/// <summary>相对访问路径(如 /UploadFile/docprint/20260101/{guid}.jpg)。</summary>
/// <summary>相对访问路径(如 /upload/docprint/20260101/{guid}.jpg)。</summary>
public string fileurl { get; set; }
/// <summary>文件主机前缀(Config.FileWebPath)。</summary>
+149
View File
@@ -0,0 +1,149 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using OfficeOpenXml;
using OfficeOpenXml.Style;
namespace YhCore.Service.Excel
{
/// <summary>
/// EPPlus 多 Sheet 导出(移植自旧 Bsch.Common/DocPrint/ExcelHelper,
/// 保留 RenderToExcelBytes + ExportDynamicSheet:动态列、大标题、自定义列宽、打印优化)。
/// 注意:EPPlus 实际产出 xlsx 字节流,旧系统文件名沿用 .xls,Excel 可正常打开,保持原契约。
/// </summary>
public class ExcelHelper
{
/// <summary>导出多 Sheet Excel 字节流(listdt/listsheetname/listtitle 按下标一一对应)</summary>
public static byte[] RenderToExcelBytes(string fileName, List<DataTable> listdt, List<string> listsheetname, List<string> listtitle, List<double> listcolwidth = null)
{
try
{
var package = new ExcelPackage();
int n = 0;
foreach (var dt in listdt)
{
string sheetname = "Sheet" + (n + 1).ToString();
if (n < listsheetname.Count) sheetname = listsheetname[n];
string title = "";
if (n < listtitle.Count) title = listtitle[n];
var ws1 = package.Workbook.Worksheets.Add(sheetname);
ExportDynamicSheet(ws1, dt, title, "A", listcolwidth);
n++;
}
return package.GetAsByteArray();
}
catch (Exception) { return Array.Empty<byte>(); }
}
#region 动态列 Sheet 导出(通用方法)
/// <summary>
/// 导出 动态列 + 自定义表头 + 自定义页码 + 打印优化
/// </summary>
public static void ExportDynamicSheet(ExcelWorksheet ws, DataTable dt, string sheetTitle, string printMark, List<double> listcolwidth = null)
{
// -------------------------- 行定义 --------------------------
int titleRow = 1; // 大标题行
int headerRow = 2; // 列标题行
int dataStartRow = 3; // 数据开始行
int colCount = dt.Columns.Count;
int rowCount = dt.Rows.Count;
// -------------------------- 1. 自定义大标题(合并行) --------------------------
if (sheetTitle != "")
{
ws.Cells[titleRow, 1].Value = sheetTitle;
ws.Cells[titleRow, 1, titleRow, colCount].Merge = true; // 合并单元格
// 标题样式
var titleStyle = ws.Cells[titleRow, 1].Style;
titleStyle.Font.Size = 12;
titleStyle.Font.Bold = true;
titleStyle.HorizontalAlignment = ExcelHorizontalAlignment.Center;
titleStyle.VerticalAlignment = ExcelVerticalAlignment.Center;
ws.Row(titleRow).Height = 32;
}
else
{
headerRow = 1; // 列标题行
dataStartRow = 2; // 数据开始行
}
// -------------------------- 2. 动态列 自动填充表头 --------------------------
for (int c = 0; c < colCount; c++)
{
ws.Cells[headerRow, c + 1].Value = dt.Columns[c].ColumnName;
}
// -------------------------- 3. 动态列 自动填充数据 --------------------------
for (int r = 0; r < rowCount; r++)
{
for (int c = 0; c < colCount; c++)
{
ws.Cells[dataStartRow + r, c + 1].Value = dt.Rows[r][c];
}
}
// -------------------------- 4. 自动换行 + 行高自适应(关键) --------------------------
var dataRange = ws.Cells[headerRow, 1, dataStartRow + rowCount - 1, colCount];
dataRange.Style.Font.Size = 10;
dataRange.Style.WrapText = true;
dataRange.Style.VerticalAlignment = ExcelVerticalAlignment.Center;
dataRange.Style.HorizontalAlignment = ExcelHorizontalAlignment.Center;
// 加细边框
dataRange.Style.Border.Top.Style = ExcelBorderStyle.Thin;
dataRange.Style.Border.Bottom.Style = ExcelBorderStyle.Thin;
dataRange.Style.Border.Left.Style = ExcelBorderStyle.Thin;
dataRange.Style.Border.Right.Style = ExcelBorderStyle.Thin;
// 自动计算:所有列平均撑满 A4 宽度
if (listcolwidth != null)
{
int colnum = listcolwidth.Count > colCount ? colCount : listcolwidth.Count;
for (int i = 1; i <= colnum; i++)
{
ws.Column(i).Width = listcolwidth[i - 1];
}
}
else
{
double totalWidth = 58;
double perColWidth = (double)totalWidth / colCount;
for (int i = 1; i <= colCount; i++)
{
ws.Column(i).Width = perColWidth;
}
}
// 行高自适应(EPPlus 4.x 兼容)
for (int r = headerRow; r <= headerRow + rowCount - 1; r++)
{
ws.Row(r).CustomHeight = true;
}
// -------------------------- 5. 表头样式 --------------------------
var headerRange = ws.Cells[headerRow, 1, headerRow, colCount];
headerRange.Style.Font.Bold = true;
ws.Row(headerRow).Height = 25;
// -------------------------- 6. 打印:每页重复标题行 --------------------------
ws.PrinterSettings.RepeatRows = new ExcelAddress($"{headerRow}:{headerRow}");
// -------------------------- 7. 自定义页码(中文样式 + 字体大小) --------------------------
// &P=当前页 &N=总页 12号字体 居中
ws.HeaderFooter.OddFooter.CenteredText = "&14 第 &P 页 / 共 &N 页";
// -------------------------- 8. 打印格式优化 --------------------------
ws.PrinterSettings.PaperSize = ePaperSize.A4;
ws.PrinterSettings.Orientation = eOrientation.Portrait;
ws.PrinterSettings.LeftMargin = 0.4m;
ws.PrinterSettings.RightMargin = 0.4m;
ws.PrinterSettings.TopMargin = 0.8m;
ws.PrinterSettings.BottomMargin = 1.2m;
}
#endregion
}
}
+404
View File
@@ -0,0 +1,404 @@
using System;
using System.Data;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using NPOI.HSSF.UserModel;
using NPOI.SS.Formula.Eval;
using NPOI.SS.UserModel;
using NPOI.XSSF.UserModel;
namespace YhCore.Service.Excel
{
/// <summary>
/// 使用 NPOI 操作 Excel,无需 Office COM 组件(移植自旧 Bsch.Common/DocPrint/ExcelRender,
/// 仅保留 6 个 WYSHRole Excel 接口用到的导出/导入成员;控制器用 File() 包装下载)。
/// </summary>
public class ExcelRender
{
static ExcelRender()
{
// ExportDT 列宽按 GB2312 字节长计算:net5 需注册 CodePages 提供程序后才能 GetEncoding(936)
Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance);
}
#region DataTable 导出为 .xls 字节流
/// <summary>
/// DataTable 导出为 Excel 的 MemoryStream(HSSF .xls;65535 行自动分 sheet;列宽按 GB2312 字节长,上限 50)
/// </summary>
static MemoryStream ExportDT(DataTable dtSource, string strHeaderText)
{
HSSFWorkbook workbook = new HSSFWorkbook();
HSSFSheet sheet = workbook.CreateSheet() as HSSFSheet;
HSSFCellStyle dateStyle = workbook.CreateCellStyle() as HSSFCellStyle;
HSSFDataFormat format = workbook.CreateDataFormat() as HSSFDataFormat;
dateStyle.DataFormat = format.GetFormat("yyyy-mm-dd");
//取得列宽
int[] arrColWidth = new int[dtSource.Columns.Count];
foreach (DataColumn item in dtSource.Columns)
{
arrColWidth[item.Ordinal] = Encoding.GetEncoding(936).GetBytes(item.ColumnName.ToString()).Length;
}
for (int i = 0; i < dtSource.Rows.Count; i++)
{
for (int j = 0; j < dtSource.Columns.Count; j++)
{
int intTemp = Encoding.GetEncoding(936).GetBytes(dtSource.Rows[i][j].ToString()).Length;
if (intTemp > arrColWidth[j])
{
arrColWidth[j] = intTemp;
if (intTemp >= 50)
arrColWidth[j] = 50;
}
}
}
int rowIndex = 0;
foreach (DataRow row in dtSource.Rows)
{
#region 新建表,填充表头,填充列头,样式
if (rowIndex == 65535 || rowIndex == 0)
{
if (rowIndex != 0)
{
sheet = workbook.CreateSheet() as HSSFSheet;
}
#region 列头及样式
{
HSSFRow headerRow = sheet.CreateRow(0) as HSSFRow;
HSSFCellStyle headStyle = workbook.CreateCellStyle() as HSSFCellStyle;
headStyle.Alignment = NPOI.SS.UserModel.HorizontalAlignment.Center;
HSSFFont font = workbook.CreateFont() as HSSFFont;
font.FontHeightInPoints = 10;
font.Boldweight = 700;
headStyle.SetFont(font);
foreach (DataColumn column in dtSource.Columns)
{
headerRow.CreateCell(column.Ordinal).SetCellValue(column.ColumnName);
headerRow.GetCell(column.Ordinal).CellStyle = headStyle;
//设置列宽
sheet.SetColumnWidth(column.Ordinal, (arrColWidth[column.Ordinal] + 1) * 256);
}
}
#endregion
rowIndex = 1;
}
#endregion
#region 填充内容
HSSFRow dataRow = sheet.CreateRow(rowIndex) as HSSFRow;
foreach (DataColumn column in dtSource.Columns)
{
HSSFCell newCell = dataRow.CreateCell(column.Ordinal) as HSSFCell;
string drValue = row[column].ToString();
switch (column.DataType.ToString())
{
case "System.String": //字符串类型
newCell.SetCellValue(drValue);
break;
case "System.DateTime": //日期类型
DateTime dateV;
DateTime.TryParse(drValue, out dateV);
newCell.SetCellValue(dateV);
newCell.CellStyle = dateStyle; //格式化显示
break;
case "System.Boolean": //布尔型
bool boolV = false;
bool.TryParse(drValue, out boolV);
newCell.SetCellValue(boolV);
break;
case "System.Int16": //整型
case "System.Int32":
case "System.Int64":
case "System.Byte":
int intV = 0;
int.TryParse(drValue, out intV);
newCell.SetCellValue(intV);
break;
case "System.Decimal": //浮点型
case "System.Double":
double doubV = 0;
double.TryParse(drValue, out doubV);
newCell.SetCellValue(doubV);
break;
case "System.DBNull": //空值处理
newCell.SetCellValue("");
break;
default:
newCell.SetCellValue("");
break;
}
}
#endregion
rowIndex++;
}
using (MemoryStream ms = new MemoryStream())
{
workbook.Write(ms);
ms.Flush();
ms.Position = 0;
return ms;
}
}
/// <summary>导出 Excel 字节流(供 Service 层返回 byte[],控制器包 File 下载)</summary>
public static byte[] RenderToExcelBytes(DataTable table, string fileName)
{
try { return ExportDT(table, null).ToArray(); }
catch (Exception) { return Array.Empty<byte>(); }
}
#endregion
#region 读取 excel 到 DataTable
/// <summary>Excel 文档流是否有数据(默认第一个 sheet)</summary>
public static bool HasData(Stream excelFileStream, string fileExt)
{
return HasData(excelFileStream, 0, fileExt);
}
public static bool HasData(Stream excelFileStream, int sheetIndex, string fileExt)
{
using (excelFileStream)
{
IWorkbook workbook = null;
if (fileExt == ".xls")
{
workbook = new HSSFWorkbook(excelFileStream);
}
else if (fileExt == ".xlsx")
{
workbook = new XSSFWorkbook(excelFileStream);
}
{
if (workbook.NumberOfSheets > 0)
{
if (sheetIndex < workbook.NumberOfSheets)
{
ISheet sheet = workbook.GetSheetAt(sheetIndex);
{
return sheet.PhysicalNumberOfRows > 0;
}
}
}
}
}
return false;
}
/// <summary>
/// Excel 文档流转换成 DataTable
/// </summary>
/// <param name="excelFileStream">Excel 文档流</param>
/// <param name="sheetIndex">表索引号,如第一个表为 0</param>
/// <param name="headerRowIndex">标题行索引号,如第一行为 0</param>
public static DataTable RenderFromExcel(Stream excelFileStream, int sheetIndex, int headerRowIndex, string fileExt)
{
DataTable table = null;
using (excelFileStream)
{
IWorkbook workbook = null;
if (fileExt == ".xls")
{
workbook = new HSSFWorkbook(excelFileStream);
}
else if (fileExt == ".xlsx")
{
workbook = new XSSFWorkbook(excelFileStream);
}
ISheet sheet = workbook.GetSheetAt(sheetIndex);
{
table = ImportDt(sheet, headerRowIndex, true);
}
}
return table;
}
/// <summary>将指定 sheet 中的数据导出到 datatable 中(第一行为列头)</summary>
static DataTable ImportDt(ISheet sheet, int HeaderRowIndex, bool needHeader)
{
DataTable table = new DataTable();
IRow headerRow;
int cellCount;
try
{
if (HeaderRowIndex < 0 || !needHeader)
{
headerRow = sheet.GetRow(0);
cellCount = headerRow.LastCellNum;
for (int i = headerRow.FirstCellNum; i <= cellCount; i++)
{
DataColumn column = new DataColumn(Convert.ToString(i));
table.Columns.Add(column);
}
}
else
{
headerRow = sheet.GetRow(HeaderRowIndex);
cellCount = headerRow.LastCellNum;
for (int i = headerRow.FirstCellNum; i < cellCount; i++)
{
if (headerRow.GetCell(i) == null)
{
if (table.Columns.IndexOf(Convert.ToString(i)) > 0)
{
DataColumn column = new DataColumn(Convert.ToString("重复列名" + i));
table.Columns.Add(column);
}
else
{
DataColumn column = new DataColumn(Convert.ToString(i));
table.Columns.Add(column);
}
}
else if (table.Columns.IndexOf(headerRow.GetCell(i).ToString()) > 0)
{
DataColumn column = new DataColumn(Convert.ToString("重复列名" + i));
table.Columns.Add(column);
}
else
{
DataColumn column = new DataColumn(headerRow.GetCell(i).ToString());
table.Columns.Add(column);
}
}
}
int rowCount = sheet.LastRowNum;
for (int i = (HeaderRowIndex + 1); i <= sheet.LastRowNum; i++)
{
try
{
IRow row;
if (sheet.GetRow(i) == null)
{
row = sheet.CreateRow(i);
}
else
{
row = sheet.GetRow(i);
}
DataRow dataRow = table.NewRow();
for (int j = row.FirstCellNum; j < cellCount; j++)
{
try
{
if (row.GetCell(j) != null)
{
switch (row.GetCell(j).CellType)
{
case CellType.String:
string str = row.GetCell(j).StringCellValue;
if (str != null && str.Length > 0)
{
dataRow[j] = str.ToString();
}
else
{
dataRow[j] = null;
}
break;
case CellType.Numeric:
if (DateUtil.IsCellDateFormatted(row.GetCell(j)))
{
dataRow[j] = DateTime.FromOADate(row.GetCell(j).NumericCellValue);
}
else
{
dataRow[j] = Convert.ToDouble(row.GetCell(j).NumericCellValue);
}
break;
case CellType.Boolean:
dataRow[j] = Convert.ToString(row.GetCell(j).BooleanCellValue);
break;
case CellType.Error:
dataRow[j] = ErrorEval.GetText(row.GetCell(j).ErrorCellValue);
break;
case CellType.Formula:
switch (row.GetCell(j).CachedFormulaResultType)
{
case CellType.String:
string strFORMULA = row.GetCell(j).StringCellValue;
if (strFORMULA != null && strFORMULA.Length > 0)
{
dataRow[j] = strFORMULA.ToString();
}
else
{
dataRow[j] = null;
}
break;
case CellType.Numeric:
dataRow[j] = Convert.ToString(row.GetCell(j).NumericCellValue);
break;
case CellType.Boolean:
dataRow[j] = Convert.ToString(row.GetCell(j).BooleanCellValue);
break;
case CellType.Error:
dataRow[j] = ErrorEval.GetText(row.GetCell(j).ErrorCellValue);
break;
default:
dataRow[j] = "";
break;
}
break;
default:
dataRow[j] = "";
break;
}
}
}
catch (Exception)
{
}
}
table.Rows.Add(dataRow);
}
catch (Exception)
{
}
}
}
catch (Exception)
{
}
return table;
}
#endregion
public static byte[] StreamToBytes(Stream stream)
{
byte[] bytes = new byte[stream.Length];
stream.Read(bytes, 0, bytes.Length);
// 设置当前流的位置为流的开始
stream.Seek(0, SeekOrigin.Begin);
return bytes;
}
}
}
+15 -9
View File
@@ -68,10 +68,9 @@ namespace YhCore.Service.Impl
if (file == null || file.Length == 0)
throw new DException("上传失败!");
if (appType.ToLower() == "noticemsg") appType = "notice";
string dtStr = DateTime.Now.ToString("yyyyMMdd");
string physicalDir = Path.Combine(StorageRoot(), appType, dtStr);
string outFilePath = "/UploadFile/" + appType + "/" + dtStr + "/";
string outFilePath = appType + "/" + dtStr + "/";
if (!Directory.Exists(physicalDir))
Directory.CreateDirectory(physicalDir);
@@ -109,7 +108,7 @@ namespace YhCore.Service.Impl
if (appType.ToLower() == "noticemsg") appType = "notice";
string dtStr = DateTime.Now.ToString("yyyyMMdd");
string physicalDir = Path.Combine(StorageRoot(), appType, dtStr);
string outFilePath = "/UploadFile/" + appType + "/" + dtStr + "/";
string outFilePath = appType + "/" + dtStr + "/";
if (!Directory.Exists(physicalDir))
Directory.CreateDirectory(physicalDir);
@@ -169,7 +168,7 @@ namespace YhCore.Service.Impl
if (appType.ToLower() == "noticemsg") appType = "notice";
string dtStr = DateTime.Now.ToString("yyyyMMdd");
string physicalDir = Path.Combine(StorageRoot(), appType, dtStr);
string outFilePath = "/UploadFile/" + appType + "/" + dtStr + "/";
string outFilePath = appType + "/" + dtStr + "/";
if (!Directory.Exists(physicalDir))
Directory.CreateDirectory(physicalDir);
@@ -214,22 +213,29 @@ namespace YhCore.Service.Impl
if (string.IsNullOrEmpty(fileUrl))
return "";
// 相对路径如 /UploadFile/docprint/20260101/{guid}.jpg → Config.FilePath + 子路径
// 新数据:fileurl 以 FILE_PATH 开头(如 /data/upload/docprint/20260101/{guid}.jpg)→ 剥前缀映射回 FILE_PATH。
// 注意必须用未 TrimStart 的原始 fileUrl 比较(FILE_PATH 本身以 / 开头)
string fp = Config.FilePath;
if (!string.IsNullOrEmpty(fp) && fileUrl.StartsWith(fp, StringComparison.OrdinalIgnoreCase))
return Path.Combine(fp, fileUrl.Substring(fp.Length).TrimStart('/').Replace('/', Path.DirectorySeparatorChar));
// 旧数据:相对路径如 /upload/docprint/20260101/{guid}.jpg(不含 FILE_PATH 前缀)→ StorageRoot + 子路径
var relative = fileUrl.TrimStart('/');
const string prefix = "UploadFile/";
string prefix = Config.FilePath;
if (relative.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
relative = relative.Substring(prefix.Length);
return Path.Combine(StorageRoot(), relative.Replace('/', Path.DirectorySeparatorChar));
}
/// <summary>存储根目录(旧 Server.MapPath("~/UploadFile/");Config.FilePath 缺省时退回进程目录/UploadFile)。</summary>
private static string StorageRoot()
/// <summary>存储根目录(旧 Server.MapPath("~/upload/");Config.FilePath 缺省时退回进程目录/upload)。</summary>
public static string StorageRoot()
{
if (!string.IsNullOrEmpty(Config.FilePath))
return Config.FilePath;
return Path.Combine(AppContext.BaseDirectory, "UploadFile");
// 此分支仅在 FILE_PATH 未配置时执行(此时 Config.FilePath 为 null,不可再参与拼接)
return Path.Combine(AppContext.BaseDirectory, Config.FilePath);
}
}
}
File diff suppressed because it is too large Load Diff
@@ -7,8 +7,8 @@ namespace YhCore.Service.Interfaces
{
/// <summary>
/// 公共文件服务(移植旧 BestSchool.Service.Common.GetEnclosure/FileSaveEnclosure +
/// BestSchool.Common.UpLoadFile.FileSave/FileSaveImage/FileChunkSave/FileMergeSave)。
/// 存储约定:物理根目录 = Config.FilePath(≈旧 ~/UploadFile/),相对路径仍存 /UploadFile/{AppType}/{yyyyMMdd}/{guid}{ext}
/// BestSchool.Common.upload.FileSave/FileSaveImage/FileChunkSave/FileMergeSave)。
/// 存储约定:物理根目录 = Config.FilePath(≈旧 ~/upload/),相对路径仍存 /upload/{AppType}/{yyyyMMdd}/{guid}{ext}
/// (与旧库数据格式一致,兼容历史记录);host 取 Config.FileWebPath(≈旧 AuthConfig.weboa_url)。
/// 旧 ToOfficeHelper.OfficeConvertToPDF(Office COM 转 PDF)新项目缺失,降级:上传不做转 PDF,
/// 预览时本地无 .pdf 则降级 officeapps.live.com 在线预览。
@@ -21,10 +21,10 @@ namespace YhCore.Service.Interfaces
/// <summary>存储附件信息(旧 FileSaveEnclosure:Common_UpFiles 落库,返回 SerID;文件名非法字符替换为 _)。</summary>
Task<int> SaveEnclosureAsync(FileObj file, string appType);
/// <summary>单文件上传(旧 UpLoadFile.FileSave:扩展名白名单 + 保存 + 返回 FileObj)。</summary>
/// <summary>单文件上传(旧 upload.FileSave:扩展名白名单 + 保存 + 返回 FileObj)。</summary>
Task<FileObj> SaveFileAsync(string appType, IFormFile file);
/// <summary>图片多文件上传(旧 UpLoadFile.FileSaveImage:jpg/gif/png/jpeg)。</summary>
/// <summary>图片多文件上传(旧 upload.FileSaveImage:jpg/gif/png/jpeg)。</summary>
Task<List<FileObj>> SaveFileImageAsync(string appType, IFormFileCollection files);
/// <summary>保存分片(旧 FileChunkSave:临时目录 {guid}/ 下按索引存块)。</summary>
@@ -33,7 +33,7 @@ namespace YhCore.Service.Interfaces
/// <summary>合并分片(旧 FileMergeSave:按块合并 → 正式目录 + FileObj)。</summary>
Task<FileObj> MergeSaveAsync(string appType, string guid, string fileName);
/// <summary>相对路径 → 物理路径(fileurl 以 /UploadFile/ 开头 → Config.FilePath + 子路径)。</summary>
/// <summary>相对路径 → 物理路径(fileurl 以 /upload/ 开头 → Config.FilePath + 子路径)。</summary>
string MapPhysicalPath(string fileUrl);
}
}
@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using YhCore.Entity;
using YhCore.Model.Dto;
using YhCore.Model.Vo;
@@ -14,7 +16,9 @@ namespace YhCore.Service.Interfaces
/// 落库 WY_SHRole_SHTea.SHTeaID / WY_SHRole_BSHTea.BSHTeaID 仍为 decimal 用户 id;
/// 3. 签名存文件系统(DES 加密),非数据库表;
/// 4. 旧 ServiceResult 的 Data/OtherData/Page 三段合并为单个 Result 对象(getshenheFlow / getStepInfo / getStepByJE / getdocOftable)。
/// 延后:Excel 导入导出(UpfileExcelSHRole/downMBOfSHRole/downUpfileSHRoleFK/ExceltotalListSHRole/WYRDExceldaochu)、审批单 PDF(DwonWYSQD)。
/// Excel 导入导出(UpfileExcelSHRole/downMBOfSHRole/downUpfileSHRoleFK/ExceltotalListSHRole/WYRDExceldaochu)
/// 与审批单 PDF(DwonWYSQD):下载类接口返回 (byte[] Content, string FileName),
/// 控制器按扩展名包装 File();导入反馈表存进程内 IMemoryCache(单实例)。
/// </summary>
public interface IWYSHRoleService
{
@@ -66,5 +70,24 @@ namespace YhCore.Service.Interfaces
Task<string> RegetJEAsync(string schSerId, string userId, List<decimal> listDocPrintId, string type);
Task<PageVO<SHRoleRegetJERdItem>> GetRegetJERdAsync(string schSerId, int pageSize, int pageIndex);
Task<int> SetDownFileTimesAsync(int serId);
// ---- Excel 导入导出 / 审批单 PDF(延后批次补齐)----
/// <summary>下载导入模板(旧 downMBOfSHRole,代码生成)。</summary>
Task<byte[]> DownMBOfSHRoleAsync();
/// <summary>Excel 批量导入审核角色(旧 UpfileExcelSHRole,multipart 第一个文件),返回反馈消息。</summary>
Task<string> UpfileExcelSHRoleAsync(string schSerId, string userId, IFormFile file);
/// <summary>下载最近一次导入的反馈 Excel(旧 downUpfileSHRoleFK)。</summary>
Task<byte[]> DownUpfileSHRoleFKAsync(string userId);
/// <summary>全部文印导出(旧 ExceltotalListSHRole)。</summary>
Task<(byte[] Content, string FileName)> ExceltotalListSHRoleAsync(string schSerId, string name, string stateid, string starttime, string endtime);
/// <summary>文印记录导出(旧 WYRDExceldaochu,flag=page 简版 / 其余完整版)。</summary>
Task<(byte[] Content, string FileName)> WYRDExceldaochuAsync(string schSerId, int flowid, int dwid, string name, string stateid, string starttime, string endtime, string flag);
/// <summary>审批单 PDF 下载(旧 DwonWYSQD,IDS 逗号分隔的 WY_DocPrint.SerID)。</summary>
Task<(byte[] Content, string FileName)> DwonWYSQDAsync(string schSerId, string userId, string ids);
}
}
@@ -0,0 +1,123 @@
using System;
namespace YhCore.Service.Pdf
{
/// <summary>数字转中文大写金额(移植自旧 Bsch.Common/DocPrint/NumberToChineseConverter,审批单合计金额用)。</summary>
public class NumberToChineseConverter
{
// 中文大写数字
private static readonly char[] ChineseNumbers = { '零', '壹', '贰', '叁', '肆', '伍', '陆', '柒', '捌', '玖' };
// 中文大写单位
private static readonly string[] ChineseUnits = { "分", "角", "元", "拾", "佰", "仟", "万", "拾", "佰", "仟", "亿", "拾", "佰", "仟", "兆" };
/// <summary>
/// 将数字转换为中文大写金额
/// </summary>
/// <param name="number">需要转换的数字(支持整数和小数,建议保留两位小数)</param>
/// <returns>中文大写金额字符串</returns>
public static string ToChineseUppercase(decimal number)
{
// 处理负数
bool isNegative = number < 0;
if (isNegative)
{
number = Math.Abs(number);
}
// 四舍五入保留两位小数
number = Math.Round(number, 2);
long integerPart = (long)number; // 整数部分
int decimalPart = (int)((number - integerPart) * 100); // 小数部分(角分)
// 处理零值
if (integerPart == 0 && decimalPart == 0)
{
return "零元整";
}
string result = string.Empty;
// 处理整数部分
if (integerPart > 0)
{
string integerStr = integerPart.ToString();
int length = integerStr.Length;
// 标记是否需要加零
bool needZero = false;
for (int i = 0; i < length; i++)
{
int digit = int.Parse(integerStr[i].ToString());
int unitIndex = length - i - 1 + 2; // 对应到ChineseUnits的索引(从元开始)
if (digit == 0)
{
needZero = true;
// 处理万、亿等单位,避免漏单位
if (unitIndex == 6 || unitIndex == 10)
{
result += ChineseUnits[unitIndex];
needZero = false;
}
}
else
{
if (needZero)
{
result += ChineseNumbers[0];
needZero = false;
}
result += ChineseNumbers[digit] + ChineseUnits[unitIndex];
}
}
}
// 处理小数部分(角分)
if (decimalPart > 0)
{
int jiao = decimalPart / 10; // 角
int fen = decimalPart % 10; // 分
if (jiao > 0)
{
result += ChineseNumbers[jiao] + ChineseUnits[1];
}
else if (integerPart > 0) // 有整数部分但角为0,需要加零
{
result += ChineseNumbers[0];
}
if (fen > 0)
{
result += ChineseNumbers[fen] + ChineseUnits[0];
}
}
else
{
// 无小数部分,加"整"
result += "整";
}
// 处理负数
if (isNegative)
{
result = "负" + result;
}
return result;
}
// 重载方法,支持double类型
public static string ToChineseUppercase(double number)
{
return ToChineseUppercase((decimal)number);
}
// 重载方法,支持int类型
public static string ToChineseUppercase(int number)
{
return ToChineseUppercase((decimal)number);
}
}
}
+102
View File
@@ -0,0 +1,102 @@
// PDF helper(移植自旧 Bsch.Common/DocPrint/PdfHelper):
// HTML→PDF 由 wkhtmltopdf 外部进程完成(原 iTextSharp XMLWorker 无 Core 版),
// 多页合并用 iTextSharp.LGPLv2.Core;Base64→PNG 直接落盘。
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using iTextSharp.text;
using iTextSharp.text.pdf;
using Yh.Platform.Abstractions;
using YhCore.Common.Config;
namespace YhCore.Service.Pdf
{
public class PdfHelper
{
private static string WkHtmlToPdfPath =>
string.IsNullOrEmpty(Config.WkHtmlToPdfPath) ? "wkhtmltopdf" : Config.WkHtmlToPdfPath;
// --enable-local-file-access 为 wkhtmltopdf 0.12.6 新增开关(签名图片用本地绝对路径 <img> 必需);
// 0.12.5 及以下(如 Debian 10 源)不认识该参数会报 Unknown long argument,按版本探测决定是否追加
private static readonly Lazy<bool> NeedsLocalFileAccessFlag = new Lazy<bool>(() =>
{
try
{
using var probe = Process.Start(new ProcessStartInfo(WkHtmlToPdfPath, "--version")
{
RedirectStandardOutput = true,
CreateNoWindow = true,
UseShellExecute = false,
});
if (probe == null) return true;
string output = probe.StandardOutput.ReadToEnd();
probe.WaitForExit(10000);
var m = Regex.Match(output, @"(\d+)\.(\d+)\.(\d+)");
if (!m.Success) return true;
var version = new Version(int.Parse(m.Groups[1].Value), int.Parse(m.Groups[2].Value), int.Parse(m.Groups[3].Value));
return version >= new Version(0, 12, 6);
}
catch
{
return true;
}
});
/// <summary>逐页 HTML→PDF 后合并为单个 PDF 字节流(DwonWYSQD 用)</summary>
public static byte[] ConvertHtmlTextToPdfByList(List<string> htmlText, string HorV = "V")
=> MergePdf(htmlText.Select(HtmlToPdf).ToList());
private static byte[] HtmlToPdf(string html)
{
var tmpIn = Path.GetTempFileName() + ".html";
var tmpOut = Path.GetTempFileName() + ".pdf";
try
{
File.WriteAllText(tmpIn, html, new UTF8Encoding(false));
string localFlag = NeedsLocalFileAccessFlag.Value ? " --enable-local-file-access" : "";
var psi = new ProcessStartInfo(WkHtmlToPdfPath,
$" --encoding utf-8{localFlag} --disable-smart-shrinking \"{tmpIn}\" \"{tmpOut}\"")
{ CreateNoWindow = true, UseShellExecute = false };
using var proc = Process.Start(psi);
if (proc == null || !proc.WaitForExit(60000) || !File.Exists(tmpOut))
throw new DException("审批单 PDF 生成失败,请检查服务器是否已安装 wkhtmltopdf(或配置 wkhtmltopdf_path 指向其可执行文件)!");
return File.ReadAllBytes(tmpOut);
}
finally
{
if (File.Exists(tmpIn)) File.Delete(tmpIn);
if (File.Exists(tmpOut)) File.Delete(tmpOut);
}
}
public static byte[] MergePdf(List<byte[]> pdfBytes)
{
using var ms = new MemoryStream();
var doc = new Document();
var copy = new PdfCopy(doc, ms);
doc.Open();
foreach (var b in pdfBytes)
{
if (b == null || b.Length == 0) continue;
var reader = new PdfReader(b);
for (int i = 1; i <= reader.NumberOfPages; i++)
copy.AddPage(copy.GetImportedPage(reader, i));
copy.FreeReader(reader);
reader.Close();
}
doc.Close();
return ms.ToArray();
}
/// <summary>签名 base64 → PNG 落盘(审批单 HTML 内嵌本地路径图片用)</summary>
public static void ConvertBase64ToPng(string base64String, string outputFilePath)
{
var bytes = Convert.FromBase64String(base64String);
File.WriteAllBytes(outputFilePath, bytes);
}
}
}
+9 -2
View File
@@ -22,9 +22,16 @@
<ItemGroup>
<!-- 业务异常 DException / ErrorCode / IErrorCode:从 Nexus nuget-group 引用(原为本地源项目引用) -->
<PackageReference Include="Yh.Platform.Abstractions" Version="1.0.0-preview.1" />
<PackageReference Include="Yh.Platform.Abstractions" Version="1.0.0-preview.2" />
<!-- 教务(SchClass)开放接口 SDK:教师/学校查询(C1 reviewprocess 教师类接口):从 Nexus nuget-group 引用(原为本地源项目引用) -->
<PackageReference Include="Yh.SchClass.OpenApi" Version="1.0.0-preview.1" />
<PackageReference Include="Yh.SchClass.OpenApi" Version="1.0.0-preview.2" />
</ItemGroup>
<ItemGroup>
<!-- Excel 导入导出(NPOI .xls 读写 / EPPlus 多 Sheet 导出)与 PDF 合并:版本对齐旧 DocPrintCore/Bsch.Common -->
<PackageReference Include="NPOI" Version="2.6.2" />
<PackageReference Include="EPPlus" Version="4.5.3.3" />
<PackageReference Include="iTextSharp.LGPLv2.Core" Version="3.4.20" />
</ItemGroup>
</Project>
@@ -1,12 +1,14 @@
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using Yh.Platform.Abstractions;
using Yh.SchClass.OpenApi;
using Yh.SchClass.OpenApi.Model;
using YhCore.Common;
using YhCore.Common.Config;
using YhCore.Common.UserContext;
@@ -28,13 +30,24 @@ namespace YhCore.WebApi.Controllers.Api.Common
{
private readonly IDictionaryService _dict;
private readonly IFileService _file;
private readonly SchClassOpenApi _sch;
public CommonController(IDictionaryService dict, IFileService file)
public CommonController(IDictionaryService dict, IFileService file, SchClassOpenApi sch)
{
_dict = dict;
_file = file;
_sch = sch;
}
[HttpGet("GetSchoolAll")]
public async Task<List<TeaTreeVo>> GetSchoolAll([FromQuery] int type = 0)
{
return await _sch.SchTree.GetTeaTreeAsync(SchSerID(), null);
}
/// <summary>获取指定字典数据(Type 1~10)。</summary>
[HttpGet("GetSysDic")]
public async Task<List<DicOption>> GetSysDic([FromQuery] int type = 0)
@@ -222,42 +235,58 @@ namespace YhCore.WebApi.Controllers.Api.Common
/// <summary>
/// 公用附件上传(旧 FileUpload,[NoAuth])。
/// chunk 参数 → 分片保存返回 {code:1,type:"chunk",msg};fileName 参数 → 合并分片;否则单文件上传。
/// 分片(chunk/chunkIndex + totalChunks&gt;1)→ 分片保存返回 {code:1,type:"chunk",msg};
/// 只带 fileName 不带文件 → 合并分片;带文件 → 单文件直传
/// (前端单文件直传也会带 guid/chunkIndex=0/totalChunks=0/fileName,不得误入合并分支)。
/// guid/chunk/fileName 兼容 query 与 form 两种形态(旧版读 Query)。
/// 落库 Common_UpFiles(TopicType=AppCode),返回 {fileurl,host,fileid,name,type}。
/// </summary>
[HttpPost("FileUpload")]
public async Task<object> Upload()
{
string apptype = "docprint"; // 旧 WebHelper.AppType 固定 docprint
string guid = Request.Form["guid"];
string chunk = Request.Form["chunk"];
string fileName = Request.Form["fileName"];
string guid = FormOrQuery("guid");
string chunk = FormOrQuery("chunk"); // 旧契约分片序号
string chunkIndex = FormOrQuery("chunkIndex"); // 前端现用分片序号
string totalChunks = FormOrQuery("totalChunks");
string fileName = FormOrQuery("fileName");
if (!string.IsNullOrEmpty(chunk))
var file = Request.Form.Files.FirstOrDefault(f => f.Name == "file") ?? Request.Form.Files.FirstOrDefault();
bool multiChunk = int.TryParse(totalChunks, out var total) && total > 1;
bool isChunkReq = file != null
&& (!string.IsNullOrEmpty(chunk) || (!string.IsNullOrEmpty(chunkIndex) && multiChunk));
if (isChunkReq)
{
var chunkFile = Request.Form.Files.FirstOrDefault(f => f.Name == "file") ?? Request.Form.Files.FirstOrDefault();
await _file.SaveChunkAsync(guid, chunk, chunkFile);
await _file.SaveChunkAsync(guid, string.IsNullOrEmpty(chunk) ? chunkIndex : chunk, file);
return new { code = 1, type = "chunk", msg = "分片上传成功!" };
}
FileObj url;
string errMsg = "上传失败!";
if (string.IsNullOrEmpty(fileName))
if (!string.IsNullOrEmpty(fileName) && file == null)
{
url = await _file.SaveFileAsync(apptype, Request.Form.Files.FirstOrDefault());
url = await _file.MergeSaveAsync(apptype, guid, fileName); // 合并分片
}
else
{
url = await _file.MergeSaveAsync(apptype, guid, fileName);
url = await _file.SaveFileAsync(apptype, file); // 单文件直传
}
if (url == null)
throw new DException(errMsg);
throw new DException("上传失败!");
// 旧 WebHelper.AppCode == "1102" ? "1002" : AppCode → docprint 固定 1003
int serid = await _file.SaveEnclosureAsync(url, DocPrintConstants.AppCode.ToString());
return new { fileurl = url.fileurl, host = url.host, fileid = serid, name = url.filename, type = url.filetype };
}
/// <summary>取表单/URL 参数(旧版读 Request["x"] 同时覆盖 Query+Form)。</summary>
private string FormOrQuery(string name)
{
var v = Request.Form[name].ToString();
if (!string.IsNullOrEmpty(v)) return v;
return Request.Query[name].ToString();
}
/// <summary>公用图片上传(旧 FileUploadImage,[NoAuth];多图,返回数组)。</summary>
[HttpPost("FileUploadImage")]
public async Task<object> UploadImg()
@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
@@ -16,8 +17,7 @@ namespace YhCore.WebApi.Controllers.Api.DocPrint
{
/// <summary>
/// 审核角色 + 流程配置控制器(对应旧 Areas/DocPrint/WYSHRoleController,路由 api/docprint/wyshrole)。
/// 45 action 中本批落地 39 个;Excel 导入导出(ExceltotalListSHRole/WYRDExceldaochu/UpfileExcelSHRole/
/// downMBOfSHRole/downUpfileSHRoleFK)与审批单 PDF(DwonWYSQD)依赖 NPOI/PdfHelper,延后迁移。
/// 45 action 全部落地(Excel 导入导出 5 个走 NPOI/EPPlus,审批单 PDF 走 wkhtmltopdf)。
/// POST 动作一律 [FromForm]:前端 api/index.js 固定 x-www-form-urlencoded 提交([FromForm] 会 415);
/// 老契约"表单字段塞 JSON"的动作(WYSHRole/SHTeaTxt/listXxxTxt/data/docdata/docnumdata)
/// 以 [FromForm] string 接收后手动反序列化。
@@ -118,7 +118,7 @@ namespace YhCore.WebApi.Controllers.Api.DocPrint
#region 审核角色流程
/// <summary>审核流程列表(旧 ListFlow)。</summary>
/// <summary>审核流程列表(旧 ListFlow;分页返回 PageVO,Content 内对象字段与旧 Data 对齐)。</summary>
[HttpGet("ListFlow")]
public async Task<PageVO<SHRoleFlowListItem>> ListFlow(
[FromQuery] string name = null,
@@ -297,8 +297,74 @@ namespace YhCore.WebApi.Controllers.Api.DocPrint
return await _role.ShenheOkSHRoleAsync(model);
}
// 延后:ExceltotalListSHRole / WYRDExceldaochu / UpfileExcelSHRole / downMBOfSHRole / downUpfileSHRoleFK(Excel,NPOI)
// 延后:DwonWYSQD(审批单 PDF,PdfHelper + Server.MapPath)
#region Excel 导入导出 / 审批单 PDF
/// <summary>全部文印导出(旧 ExceltotalListSHRole,NPOI .xls)。</summary>
[HttpGet("ExceltotalListSHRole")]
public async Task<IActionResult> ExceltotalListSHRole(
[FromQuery] string Name,
[FromQuery] string stateid,
[FromQuery] string starttime,
[FromQuery] string endtime)
{
var (content, fileName) = await _role.ExceltotalListSHRoleAsync(SchSerID(), Name, stateid, starttime, endtime);
return File(content, ContentType(fileName), fileName);
}
/// <summary>文印记录导出(旧 WYRDExceldaochu,flag=page 简版 / 完整版,EPPlus)。</summary>
[HttpGet("WYRDExceldaochu")]
public async Task<IActionResult> WYRDExceldaochu(
[FromQuery] int flowid,
[FromQuery] int DWid,
[FromQuery] string Name,
[FromQuery] string stateid,
[FromQuery] string starttime,
[FromQuery] string endtime,
[FromQuery] string flag)
{
var (content, fileName) = await _role.WYRDExceldaochuAsync(SchSerID(), flowid, DWid, Name, stateid, starttime, endtime, flag);
return File(content, ContentType(fileName), fileName);
}
/// <summary>Excel 批量导入审核角色(旧 UpfileExcelSHRole,multipart 文件字段名不敏感取第一个)。</summary>
[HttpPost("UpfileExcelSHRole")]
public async Task<string> UpfileExcelSHRole()
{
var file = Request.Form.Files.Count > 0 ? Request.Form.Files[0] : null;
return await _role.UpfileExcelSHRoleAsync(SchSerID(), UserId(), file);
}
/// <summary>下载导入模板(旧 downMBOfSHRole,代码生成)。</summary>
[HttpGet("downMBOfSHRole")]
public async Task<IActionResult> DownMBOfSHRole()
{
var content = await _role.DownMBOfSHRoleAsync();
return File(content, "application/vnd.ms-excel", "批量导入审核角色.xls");
}
/// <summary>下载最近一次导入的反馈 Excel(旧 downUpfileSHRoleFK)。</summary>
[HttpGet("downUpfileSHRoleFK")]
public async Task<IActionResult> DownUpfileSHRoleFK()
{
var content = await _role.DownUpfileSHRoleFKAsync(UserId());
return File(content, "application/vnd.ms-excel", "批量导入审核角色反馈.xls");
}
/// <summary>审批单 PDF 下载(旧 DwonWYSQD,IDS 逗号分隔的 WY_DocPrint.SerID;依赖服务器安装 wkhtmltopdf)。</summary>
[HttpGet("DwonWYSQD")]
public async Task<IActionResult> DwonWYSQD([FromQuery] string IDS)
{
var (content, fileName) = await _role.DwonWYSQDAsync(SchSerID(), UserId(), IDS);
return File(content, "application/pdf", fileName);
}
/// <summary>按扩展名取下载 Content-Type(对齐 CommonController.GetContentType 约定)。</summary>
private static string ContentType(string fileName)
=> fileName != null && fileName.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase)
? "application/pdf"
: "application/vnd.ms-excel";
#endregion
#endregion
@@ -100,9 +100,8 @@ namespace YhCore.WebApi.Controllers.Api.DocPrint
[HttpPost("EditDictionaryType")]
public async Task EditDictionaryType([FromForm] Common_Dictionary model)
{
if (model == null || model.SerID <= 0 || string.IsNullOrWhiteSpace(model.typeID)
|| string.IsNullOrWhiteSpace(model.typeName) || string.IsNullOrWhiteSpace(model.Name))
throw new DException("参数错误:SerID、typeID、typeName、Name 均不能为空(注意表单字段名拼写,TypeId≠TypelD)");
if (model == null || model.SerID <= 0 || string.IsNullOrWhiteSpace(model.Name))
throw new DException("参数错误:SerID、Name 均不能为空(注意表单字段名拼写,TypeId≠TypelD)");
model.Schserid = SchSerID();
await _dict.EditDicAsync(model);
}
+17 -3
View File
@@ -122,6 +122,10 @@ namespace YhCore.WebApi
// Redis 缓存(RedisUtil 单例,连接串取 Redis:ConnectionString)
services.AddRedisCache(Configuration);
// 进程内内存缓存:WYSHRole Excel 导入的反馈表(updateSHRole_{userId},单实例;
// 多实例部署需改 Redis 支撑的分布式缓存)
services.AddMemoryCache();
// ===== 会话状态(Session)=====
// 对应 Java HttpSession / session.invalidate();旧 DocPrintCore 同款配置。
// 注意:YhAuth 的登录态走自定义短 Cookie(SessionTicketDataFormat,票据存进程内存),
@@ -171,9 +175,19 @@ namespace YhCore.WebApi
.Bind(Configuration.GetSection("Swagger"));
services.AddOptions<YhCore.Common.Config.FileConfig>()
.Bind(Configuration);
// 静态门面(对齐 Java Config):文件路径由配置注入
YhCore.Common.Config.Config.FilePath = Configuration["FILE_PATH"];
YhCore.Common.Config.Config.FileWebPath = Configuration["FILE_WEB_PATH"];
// 静态门面(对齐 Java Config):文件路径由配置注入。
// FILE_PATH 必须以 / 结尾(旧约定:FileService 多处 FilePath + "{appType}/..." 直接拼接,缺斜杠会拼出坏路径);
// FILE_WEB_PATH 不带尾 /(拼接目标 fileurl 均以 / 开头,带尾斜杠会产生 //)
var configFilePath = Configuration["FILE_PATH"];
YhCore.Common.Config.Config.FilePath = string.IsNullOrEmpty(configFilePath)
? configFilePath
: configFilePath.TrimEnd('/').Replace('\\', '/') + "/";
var configFileWebPath = Configuration["FILE_WEB_PATH"];
YhCore.Common.Config.Config.FileWebPath = string.IsNullOrEmpty(configFileWebPath)
? configFileWebPath
: configFileWebPath.TrimEnd('/');
// 审批单 PDF:wkhtmltopdf 可执行文件路径(空则用 PATH)
YhCore.Common.Config.Config.WkHtmlToPdfPath = Configuration["wkhtmltopdf_path"];
// 接口文档:对应 Java SpringDoc,按渠道分三组(api / mobiapi / oapi)
services.AddSwaggerGen(c =>
+5 -5
View File
@@ -36,11 +36,11 @@
<ItemGroup>
<!-- 平台基础(Abstractions/AppAuth/Auth/OpenApi + 教务 SDK):从 Nexus nuget-group 引用(原为本地源项目引用) -->
<PackageReference Include="Yh.Platform.Abstractions" Version="1.0.0-preview.1" />
<PackageReference Include="Yh.Platform.AppAuth" Version="1.0.0-preview.1" />
<PackageReference Include="Yh.Platform.Auth" Version="1.0.0-preview.1" />
<PackageReference Include="Yh.Platform.OpenApi" Version="1.0.0-preview.1" />
<PackageReference Include="Yh.SchClass.OpenApi" Version="1.0.0-preview.1" />
<PackageReference Include="Yh.Platform.Abstractions" Version="1.0.0-preview.2" />
<PackageReference Include="Yh.Platform.AppAuth" Version="1.0.0-preview.2" />
<PackageReference Include="Yh.Platform.Auth" Version="1.0.0-preview.2" />
<PackageReference Include="Yh.Platform.OpenApi" Version="1.0.0-preview.2" />
<PackageReference Include="Yh.SchClass.OpenApi" Version="1.0.0-preview.2" />
</ItemGroup>
</Project>
+4 -1
View File
@@ -4,12 +4,15 @@
"AutoCreateMissingTables": true
},
"ConnectionStrings": {
"SqlServer": "Server=localhost;Database=EduCloudDocPrint;User Id=sa;Password=Password01!;MultipleActiveResultSets=true",
"SqlServer": "Server=localhost;Database=EduCloudDocPrint2;User Id=sa;Password=Password01!;MultipleActiveResultSets=true",
"Mysql": "Server=localhost;Port=3306;Database=EduCloudDocPrint;User Id=root;Password=123123;CharSet=utf8mb4"
},
"Redis": {
"ConnectionString": "localhost:6379,defaultDatabase=0"
},
"FILE_PATH": "/data/upload/",
"FILE_WEB_PATH": "",
"wkhtmltopdf_path": "",
"yh": {
"auth": {
"Enabled": true,