diff --git a/docker/Dockerfile b/docker/Dockerfile index f5fdfda..8a86a5c 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -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/* diff --git a/src/YhCore.Common/Config/Config.cs b/src/YhCore.Common/Config/Config.cs index e0ef7a9..03c7bb7 100644 --- a/src/YhCore.Common/Config/Config.cs +++ b/src/YhCore.Common/Config/Config.cs @@ -21,5 +21,8 @@ namespace YhCore.Common.Config /// 文件访问 URL 前缀(对应 Java FILE_WEB_PATH,由 Startup 注入) public static string FileWebPath { get; set; } + + /// wkhtmltopdf 可执行文件路径(审批单 PDF 用;空则用 PATH 中的 wkhtmltopdf,由 Startup 注入) + public static string WkHtmlToPdfPath { get; set; } } } diff --git a/src/YhCore.Service/Impl/FileService.cs b/src/YhCore.Service/Impl/FileService.cs index 98ccae2..4e013fe 100644 --- a/src/YhCore.Service/Impl/FileService.cs +++ b/src/YhCore.Service/Impl/FileService.cs @@ -224,7 +224,7 @@ namespace YhCore.Service.Impl } /// 存储根目录(旧 Server.MapPath("~/UploadFile/");Config.FilePath 缺省时退回进程目录/UploadFile)。 - private static string StorageRoot() + public static string StorageRoot() { if (!string.IsNullOrEmpty(Config.FilePath)) return Config.FilePath; diff --git a/src/YhCore.Service/Impl/WYSHRoleService.cs b/src/YhCore.Service/Impl/WYSHRoleService.cs index aeee58d..e8cc4cd 100644 --- a/src/YhCore.Service/Impl/WYSHRoleService.cs +++ b/src/YhCore.Service/Impl/WYSHRoleService.cs @@ -1,11 +1,14 @@ using System; using System.Collections.Generic; +using System.Data; using System.Globalization; using System.IO; using System.Linq; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Caching.Memory; using Yh.Platform.Abstractions; using Yh.SchClass.OpenApi; using YhCore.Common.Config; @@ -13,7 +16,9 @@ using YhCore.Entity; using YhCore.Model.Dto; using YhCore.Model.Vo; using YhCore.Repository; +using YhCore.Service.Excel; using YhCore.Service.Interfaces; +using YhCore.Service.Pdf; namespace YhCore.Service.Impl { @@ -25,7 +30,9 @@ namespace YhCore.Service.Impl /// 3. 签名存文件系统(DES 加密,Config.FilePath 替代旧 Server.MapPath("~/UploadFile/")); /// 4. 旧 ServiceResult 的 Data/OtherData/Page 三段合并为单个 Result 对象 /// (getshenheFlow / getStepInfo / getStepByJE / getdocOftable)。 - /// 延后:Excel 导入导出(UpfileExcelSHRole/downMBOfSHRole/downUpfileSHRoleFK/ExceltotalListSHRole/WYRDExceldaochu)、审批单 PDF(DwonWYSQD)。 + /// Excel 导入导出与审批单 PDF(DownMBOfSHRole/UpfileExcelSHRole/downUpfileSHRoleFK/ExceltotalListSHRole/ + /// WYRDExceldaochu/DwonWYSQD):NPOI/EPPlus 导出、wkhtmltopdf 转 PDF; + /// 导入反馈表存进程内 IMemoryCache(updateSHRole_{userId},单实例;多实例需改分布式缓存)。 /// public class WYSHRoleService : IWYSHRoleService { @@ -46,6 +53,7 @@ namespace YhCore.Service.Impl private readonly IRepository _senduser; private readonly IWYDocPrintService _wyDocPrint; private readonly SchClassOpenApi _sch; + private readonly IMemoryCache _cache; public WYSHRoleService( IRepository role, @@ -64,7 +72,8 @@ namespace YhCore.Service.Impl IRepository upfiles, IRepository senduser, IWYDocPrintService wyDocPrint, - SchClassOpenApi sch) + SchClassOpenApi sch, + IMemoryCache cache) { _role = role; _shTea = shTea; @@ -83,6 +92,7 @@ namespace YhCore.Service.Impl _senduser = senduser; _wyDocPrint = wyDocPrint; _sch = sch; + _cache = cache; } #region 审核角色管理 @@ -1975,7 +1985,7 @@ namespace YhCore.Service.Impl public async Task SaveSignatureAsync(string schSerId, string userId, string pic) { - var dir = Path.Combine(Config.FilePath, "Signature"); + var dir = Path.Combine(FileService.StorageRoot(), "Signature"); if (!Directory.Exists(dir)) Directory.CreateDirectory(dir); @@ -1987,7 +1997,7 @@ namespace YhCore.Service.Impl public async Task GetSignatureAsync(string schSerId, string userId) { - var dir = Path.Combine(Config.FilePath, "Signature"); + var dir = Path.Combine(FileService.StorageRoot(), "Signature"); if (!Directory.Exists(dir)) Directory.CreateDirectory(dir); @@ -2013,7 +2023,7 @@ namespace YhCore.Service.Impl public async Task DelSignatureAsync(string schSerId, string userId) { - var dir = Path.Combine(Config.FilePath, "Signature"); + var dir = Path.Combine(FileService.StorageRoot(), "Signature"); string path = Path.Combine(dir, userId + ".txt"); if (File.Exists(path)) { @@ -2299,6 +2309,1103 @@ namespace YhCore.Service.Impl #endregion + #region Excel 导入导出 / 审批单 PDF(延后批次补齐,移植旧 WY_SHRoleP 405/623/639/2495/4228/4725) + + /// 下载导入模板(旧 downMBOfSHRole:3 列 + 示例行,纯代码生成)。 + public Task DownMBOfSHRoleAsync() + { + var dt = new DataTable(); + foreach (var col in new List { "审核人", "审核角色名称", "被审核人" }) + dt.Columns.Add(new DataColumn(col, typeof(string))); + DataRow row = dt.NewRow(); + row[0] = "例:教师1,教师2,账号1,账号2"; + row[1] = ""; + row[2] = "例:教师1,教师2,账号1,账号2"; + dt.Rows.Add(row); + return Task.FromResult(ExcelRender.RenderToExcelBytes(dt, "批量导入审核角色.xls")); + } + + /// 下载最近一次导入的反馈 Excel(旧 downUpfileSHRoleFK,读导入时缓存的反馈表)。 + public Task DownUpfileSHRoleFKAsync(string userId) + { + var dt = _cache.Get("updateSHRole_" + userId); + return Task.FromResult(ExcelRender.RenderToExcelBytes(dt, "批量导入审核角色反馈.xls")); + } + + /// Excel 批量导入审核角色(旧 UpfileExcelSHRole)。 + public async Task UpfileExcelSHRoleAsync(string schSerId, string userId, IFormFile file) + { + string err = "ok"; + DataTable dt = ImportExcelToTable(file); + dt.Columns.Add(new DataColumn("状态", typeof(string))); + List listcol = new List { "审核人", "审核角色名称", "被审核人" }; + + var dtcol = new List(); + for (int i = 0; i < dt.Columns.Count; i++) + dtcol.Add(dt.Columns[i].ColumnName); + int n = listcol.Count(col => dtcol.Contains(col)); + if (n == listcol.Count) + { + // 教师匹配源:姓名或账号(旧 TeainfoBLL.GetSchByTeaList) + var teas = await _sch.TeaInfo.GetSchTeaInfoAsync(schSerId, null); + var teainfo = teas + .Where(t => int.TryParse(t.TeaSerId, out _)) + .Select(t => new { Name = t.Name ?? "", Account = t.Account ?? "", UserID = int.Parse(t.TeaSerId) }) + .ToList(); + + var listSHRoleinfo = await _role.FindAsync(a => a.SchSerId == schSerId); + List listSHRolename = new List(); + // 第一遍:逐行校验,收集合法角色名 + for (int i = 0; i < dt.Rows.Count; i++) + { + string errtp = ""; + string name = dt.Rows[i]["审核角色名称"].ToString(); + if (name == "") + { + errtp += "审核角色名称不能为空!"; + } + string SHTeastr = dt.Rows[i]["审核人"].ToString(); + if (SHTeastr == "") + { + errtp += "审核人不能为空!"; + } + else + { + List listSHTea = SHTeastr.Replace(",", ",").Split(',').ToList(); + int m = 0; + foreach (var t in listSHTea) + { + var fdtea = teainfo.FindAll(a => a.Name == t || a.Account == t); + if (fdtea.Count > 0) + m++; + } + if (m == 0) + errtp += "至少有一个正确的审核人!"; + } + if (errtp != "") + { + dt.Rows[i]["状态"] = errtp; + continue; + } + if (listSHRolename.FindAll(a => a == name).Count == 0) + listSHRolename.Add(name); + } + // 添加新角色(只增不改) + foreach (var name in listSHRolename) + { + if (listSHRoleinfo.FindAll(a => a.SHRoleName == name).Count == 0) + { + await _role.AddAsync(new WY_SHRole + { + SchSerId = schSerId, + SHRoleName = name, + inserttime = DateTime.Now, + writor = userId, + }); + } + } + await _role.SaveChangesAsync(); + listSHRoleinfo = await _role.FindAsync(a => a.SchSerId == schSerId); + + // 第二遍:逐行添加审核人/被审核人(只增不删) + for (int i = 0; i < dt.Rows.Count; i++) + { + string name = dt.Rows[i]["审核角色名称"].ToString(); + if (name == "") + continue; + string SHTeastr = dt.Rows[i]["审核人"].ToString(); + string BSHTeastr = dt.Rows[i]["被审核人"].ToString(); + + string errSHTea = ""; + string errBSHTea = ""; + var fdSHRoleinfo = listSHRoleinfo.FindAll(a => a.SHRoleName == name); + if (fdSHRoleinfo.Count > 0) + { + decimal SHRoleid = fdSHRoleinfo[0].SerID; + var fdSHTea = await _shTea.FindAsync(a => a.SchSerID == schSerId && a.SHRoleID == SHRoleid); + var fdBSHTea = await _bshTea.FindAsync(a => a.SchSerID == schSerId && a.SHRoleID == SHRoleid); + + if (SHTeastr != "") + { + // 审核人没有就添加,不做删除操作 + foreach (var item in SHTeastr.Replace(",", ",").Split(',').ToList()) + { + var fdtea = teainfo.FindAll(a => a.Name == item || a.Account == item); + if (fdtea.Count > 0) + { + if (fdSHTea.FindAll(a => a.SHTeaID == fdtea[0].UserID).Count == 0) + { + await _shTea.AddAsync(new WY_SHRole_SHTea + { + SchSerID = schSerId, + SHRoleID = fdSHRoleinfo[0].SerID, + SHTeaID = fdtea[0].UserID, + SHTeaName = fdtea[0].Name, + AndOr = "or", + inserttime = DateTime.Now, + writor = userId, + }); + } + } + else + { + errSHTea += errSHTea == "" ? item : "," + item; + } + } + if (errSHTea != "") + errSHTea = "审核人:" + errSHTea + "不存在;"; + } + + if (BSHTeastr != "") + { + // 被审核人没有就添加,不做删除操作 + foreach (var item in BSHTeastr.Replace(",", ",").Split(',').ToList()) + { + var fdtea = teainfo.FindAll(a => a.Name == item || a.Account == item); + if (fdtea.Count > 0) + { + if (fdBSHTea.FindAll(a => a.BSHTeaID == fdtea[0].UserID).Count == 0) + { + await _bshTea.AddAsync(new WY_SHRole_BSHTea + { + SchSerID = schSerId, + SHRoleID = fdSHRoleinfo[0].SerID, + BSHTeaID = fdtea[0].UserID, + BSHTeaName = fdtea[0].Name, + inserttime = DateTime.Now, + writor = userId, + }); + } + } + else + { + errBSHTea += errBSHTea == "" ? item : "," + item; + } + } + if (errBSHTea != "") + errBSHTea = "审核人:" + errBSHTea + "不存在;"; + } + } + dt.Rows[i]["状态"] = errSHTea + errBSHTea; + } + + await _shTea.SaveChangesAsync(); + await _bshTea.SaveChangesAsync(); + + // 保存导入反馈表,供 downUpfileSHRoleFK 下载 + string cachkey = "updateSHRole_" + userId; + _cache.Remove(cachkey); + _cache.Set(cachkey, dt); + } + else + { + err = "无法正常导入,请对照下载模板文件检查导入的Exce文件标题是否正确!"; + } + return err; + } + + /// 全部文印导出(旧 ExceltotalListSHRole,14 列 NPOI .xls)。 + public async Task<(byte[] Content, string FileName)> ExceltotalListSHRoleAsync( + string schSerId, string name, string stateid, string starttime, string endtime) + { + var table = new DataTable(); + table.Columns.Add("标题", typeof(string)); + table.Columns.Add("申请人", typeof(string)); + table.Columns.Add("单位", typeof(string)); + table.Columns.Add("组别", typeof(string)); + table.Columns.Add("印刷类型", typeof(string)); + table.Columns.Add("印刷内容", typeof(string)); + table.Columns.Add("备注", typeof(string)); + table.Columns.Add("印刷规格", typeof(string)); + table.Columns.Add("印刷份数", typeof(string)); + table.Columns.Add("原稿张数", typeof(string)); + table.Columns.Add("总张数", typeof(string)); + table.Columns.Add("总金额(元)", typeof(string)); + table.Columns.Add("预计领取时间", typeof(string)); + table.Columns.Add("审批状态", typeof(string)); + + DateTime? date1 = string.IsNullOrEmpty(starttime) ? (DateTime?)null : Convert.ToDateTime(starttime); + DateTime? date2 = string.IsNullOrEmpty(endtime) ? (DateTime?)null : Convert.ToDateTime(endtime).AddDays(1).AddMilliseconds(-1); // 防止跨天 + var list = await _doc.FindAsync(a => + a.SchSerID == schSerId + && (date1 == null || a.EditDate >= date1) + && (date2 == null || a.EditDate <= date2) + && (string.IsNullOrEmpty(stateid) || a.DocStatus == stateid) + && (string.IsNullOrEmpty(name) || a.DocContent.Contains(name))); + list = list.OrderBy(m => m.DocStatus).ThenByDescending(m => m.TakeDate).ToList(); + + // 旧版 join 教师表(内连接):申请人不在教师表的记录不导出 + var teas = await _sch.TeaInfo.GetSchTeaInfoAsync(schSerId, null); + var teaName = teas + .Where(t => !string.IsNullOrEmpty(t.TeaSerId)) + .GroupBy(t => t.TeaSerId) + .ToDictionary(g => g.Key, g => g.First().Name ?? ""); + list = list.Where(d => d.UserID != null && teaName.ContainsKey(d.UserID)).ToList(); + + foreach (var item in list) + { + string teaname = teaName.TryGetValue(item.UserID, out var tn) ? tn : ""; + string DWName = await GetDictNameByIDAsync(item.rid, schSerId); + string ZBName = await GetDictNameByIDAsync(item.reid, schSerId); + string DPTypeName = await GetDictNameWithTypeAsync("YZFL", item.DPType, schSerId); + string DocStyleName = await GetDictNameWithTypeAsync("YZBM", item.DocStyle, schSerId); + string DocStatusName = await GetStatusByCodeSHRoleAsync(schSerId, item.DocStatus, item.DocStep ?? 0, item.SerID, item.flowid); + table.Rows.Add( + item.DocTitle, teaname, DWName, ZBName, DPTypeName, item.DocContent, item.explain, + DocStyleName, + item.DocShare?.ToString() ?? "", // 印刷份数(旧 DocShare) + item.DocFix?.ToString() ?? "", // 原稿张数(旧 DocFix) + item.DocNum.ToString(), // 总张数(旧 DocNum) + item.Examtype, // 总金额(旧 Examtype) + item.TakeDate == null ? "" : item.TakeDate.Value.ToString("yyyy-MM-dd"), + DocStatusName); + } + byte[] content = ExcelRender.RenderToExcelBytes(table, "文印数量导出.xls"); + return (content, "文印数量导出.xls"); + } + + /// 文印记录导出(旧 WYRDExceldaochu:flag=page 简版 7 列 / 完整版固定列 + 动态规格列,EPPlus 导出)。 + public async Task<(byte[] Content, string FileName)> WYRDExceldaochuAsync( + string schSerId, int flowid, int dwid, string name, string stateid, string starttime, string endtime, string flag) + { + string filename = "文印记录导出"; + string filenameQB = "全部"; + DateTime? date1 = string.IsNullOrEmpty(starttime) ? (DateTime?)null : Convert.ToDateTime(starttime); + DateTime? date2 = string.IsNullOrEmpty(endtime) ? (DateTime?)null : Convert.ToDateTime(endtime).AddDays(1).AddMilliseconds(-1); // 防止跨天 + if (date1 != null) + { + filename += "开始" + date1.Value.ToString("yyyy-MM-dd"); + filenameQB = ""; + } + if (date2 != null) + { + filename += "结束" + date2.Value.ToString("yyyy-MM-dd"); + filenameQB = ""; + } + if (!string.IsNullOrEmpty(name)) + filenameQB = ""; + if (flowid > 0) + { + var flowinfo = await _flowinfo.FindAsync(a => a.SerID == flowid); + if (flowinfo.Count > 0) + filename += flowinfo[0].flowname; + filenameQB = ""; + } + if (dwid > 0) + { + var dwinfo = await _dic.FindAsync(a => a.SerID == dwid); + if (dwinfo.Count > 0) + filename += dwinfo[0].Name; + filenameQB = ""; + } + + var data = await _doc.FindAsync(a => + a.SchSerID == schSerId && a.DocStatus != "3" + && (date1 == null || a.EditDate >= date1) + && (date2 == null || a.EditDate <= date2) + && (string.IsNullOrEmpty(stateid) || a.DocStatus == stateid) + && (string.IsNullOrEmpty(name) || a.DocContent.Contains(name)) + && (flowid <= 0 || a.flowid == flowid) + && (dwid <= 0 || a.rid == dwid)); + data = data.OrderBy(m => m.DocStatus).ThenByDescending(m => m.TakeDate).ToList(); + + var teas = await _sch.TeaInfo.GetSchTeaInfoAsync(schSerId, null); + var teaName = teas + .Where(t => !string.IsNullOrEmpty(t.TeaSerId)) + .GroupBy(t => t.TeaSerId) + .ToDictionary(g => g.Key, g => g.First().Name ?? ""); + + var tb = await GetDocNumOftableAsync(schSerId, "3"); // 全部(印刷类型+规格)骨架,含单价 + var listdocid = data.Select(a => (int?)Convert.ToInt32(a.SerID)).ToList(); + var listdociddec = data.Select(a => (decimal?)a.SerID).ToList(); + var fileinfo = listdocid.Count > 0 + ? await _upfiles.FindAsync(a => listdocid.Contains(a.TopicID)) + : new List(); + var listflow = await _flowinfo.FindAsync(a => a.schSerID == schSerId); + var listdocnum = listdociddec.Count > 0 + ? await _docNum.FindAsync(a => listdociddec.Contains(a.DocPrintID)) + : new List(); + + DataTable dt = new DataTable(); + List colwidth; + if (flag == "page") + { + colwidth = new List { 17.71, 7.57, 13, 7.57, 19.71, 15, 8.29 }; + List fed = new List { "标题", "申请人", "使用单位", "组别", "文印内容", "状态", "合计金额" }; + foreach (var item in fed) + dt.Columns.Add(new DataColumn(item, typeof(string))); + + foreach (var d in data) + { + DataRow row = dt.NewRow(); + row["标题"] = d.DocTitle; + row["申请人"] = d.UserID != null && teaName.TryGetValue(d.UserID, out var un) ? un : ""; + row["使用单位"] = await GetDictNameByIDAsync(d.rid, schSerId); + row["组别"] = await GetDictNameByIDAsync(d.reid, schSerId); + row["文印内容"] = d.DocContent; + row["状态"] = await GetStatusByCodeSHRoleAsync(schSerId, d.DocStatus, d.DocStep ?? 0, d.SerID, d.flowid); + + double totalJE = 0; + var fdflow = listflow.FindAll(a => a.SerID == d.flowid); + if (fdflow.Count > 0) + { + if (fdflow[0].WYPageGG == "1") + { + // 单条:金额 = 骨架单价 × 总张数 + var fdgg = tb.FindAll(a => a.YSGGtypeID == d.DocStyle); + double unitprice = 0; + if (fdgg.Count > 0) + unitprice = Convert.ToDouble(fdgg[0].UnitPrice); + double je = unitprice * Convert.ToInt32(d.DocNum); + totalJE += Math.Round(je, 2); + } + else + { + // 多条:逐明细累加 + var fddocnum = listdocnum.FindAll(a => a.DocPrintID == d.SerID); + foreach (var num in fddocnum) + { + double je = Convert.ToDouble(num.TotalPrice); + totalJE += Math.Round(je, 2); + } + } + } + row["合计金额"] = totalJE; + dt.Rows.Add(row); + } + AddTotalRow(dt, "标题", "合计金额"); + } + else + { + colwidth = new List { 7.57, 17.71, 7.57, 13, 7.57, 19.71, 19.71, 15, 15, 8.29 }; + // 详细:固定列 + 每个(印刷类型+规格)动态 5 列 + List fed = new List { "序号", "标题", "申请人", "使用单位", "组别", "文印内容", "附件", "申请日期", "备注", "状态", "合计金额" }; + List fed2 = new List { "人数(份数)", "原稿页数(每份)", "张数(每份)", "总数", "金额" }; + foreach (var item in tb) + { + string str = item.YSLXName + item.YSGGName; + foreach (var fd in fed2) + fed.Add(str + "_" + fd); + } + foreach (var item in fed) + dt.Columns.Add(new DataColumn(item, typeof(string))); + + for (int i = 0; i < data.Count; i++) + { + var d = data[i]; + DataRow row = dt.NewRow(); + row["序号"] = i + 1; + row["标题"] = d.DocTitle; + row["申请人"] = d.UserID != null && teaName.TryGetValue(d.UserID, out var un) ? un : ""; + row["使用单位"] = await GetDictNameByIDAsync(d.rid, schSerId); + row["组别"] = await GetDictNameByIDAsync(d.reid, schSerId); + row["文印内容"] = d.DocContent; + row["附件"] = fileinfo.FirstOrDefault(a => a.TopicID == Convert.ToInt32(d.SerID))?.FileTrueName ?? ""; + row["申请日期"] = d.EditDate == null ? "" : d.EditDate.ToString("yyyy-MM-dd"); + row["备注"] = d.explain; + row["状态"] = await GetStatusByCodeSHRoleAsync(schSerId, d.DocStatus, d.DocStep ?? 0, d.SerID, d.flowid); + + double totalJE = 0; + var fdflow = listflow.FindAll(a => a.SerID == d.flowid); + if (fdflow.Count > 0) + { + if (fdflow[0].WYPageGG == "1") + { + // 单条:把主表数量填入骨架行,金额 = 单价 × 总张数 + var fdtb = tb.FindAll(a => a.YSGGtypeID == d.DocStyle); + if (fdtb.Count > 0) + { + double unitprice = Convert.ToDouble(fdtb[0].UnitPrice); + fdtb[0].DocShare = d.DocShare; + fdtb[0].DocNum = d.DocNum; + fdtb[0].DocFix = d.DocFix; + int pagenum = 0; + if (IsValidNumber(d.grade)) + pagenum = Convert.ToInt32(d.grade); + fdtb[0].DocPage = pagenum; + double totalprice = unitprice * Convert.ToInt32(d.DocNum); + fdtb[0].TotalPrice = totalprice; + totalJE += totalprice; + } + } + else + { + // 多条:明细回填骨架行 + var fddocnum = listdocnum.FindAll(a => a.DocPrintID == d.SerID); + foreach (var itemnum in fddocnum) + { + var fdtb = tb.FindAll(a => a.YSGGtypeID == itemnum.YSGGtypeID); + if (fdtb.Count > 0) + { + fdtb[0].DocShare = itemnum.DocShare; + fdtb[0].DocPage = itemnum.DocPage; + fdtb[0].DocFix = itemnum.DocFix; + fdtb[0].DocNum = itemnum.DocNum; + fdtb[0].TotalPrice = itemnum.TotalPrice; + totalJE += Convert.ToDouble(itemnum.TotalPrice); + } + } + } + } + foreach (var item in tb) + { + string str = item.YSLXName + item.YSGGName; + foreach (var fd in fed2) + { + string key = str + "_" + fd; + if (fd == "人数(份数)") + row[key] = item.DocShare > 0 ? item.DocShare.ToString() : ""; + if (fd == "原稿页数(每份)") + row[key] = item.DocPage > 0 ? item.DocPage.ToString() : ""; + if (fd == "张数(每份)") + row[key] = item.DocFix > 0 ? item.DocFix.ToString() : ""; + if (fd == "总数") + row[key] = item.DocNum > 0 ? item.DocNum.ToString() : ""; + if (fd == "金额") + row[key] = item.TotalPrice > 0 ? item.TotalPrice.ToString() : ""; + } + } + row["合计金额"] = totalJE; + dt.Rows.Add(row); + } + AddTotalRow(dt, "标题", "合计金额"); + } + filename += filenameQB; + byte[] content = ExcelHelper.RenderToExcelBytes(filename + ".xls", + new List { dt }, new List { filename }, + new List { filename }, colwidth); + return (content, filename + ".xls"); + } + + /// 追加"总计"汇总行(旧导出尾部逻辑)。 + private static void AddTotalRow(DataTable dt, string titleCol, string jeCol) + { + double allEJ = 0; + for (int i = 0; i < dt.Rows.Count; i++) + { + string num = dt.Rows[i][jeCol].ToString(); + if (IsValidNumber(num)) + allEJ += Convert.ToDouble(num); + } + DataRow newrow = dt.NewRow(); + newrow[jeCol] = allEJ; + newrow[titleCol] = "总计"; + dt.Rows.Add(newrow); + } + + /// 上传 Excel → DataTable(旧 common.EXEC:取第一个文件,支持 .xls/.xlsx,删全空行)。 + private static DataTable ImportExcelToTable(IFormFile file) + { + DataTable dt = null; + try + { + string fstype = Path.GetExtension(file.FileName).ToLowerInvariant(); + var bytes = ExcelRender.StreamToBytes(file.OpenReadStream()); + if (ExcelRender.HasData(new MemoryStream(bytes), fstype)) + dt = ExcelRender.RenderFromExcel(new MemoryStream(bytes), 0, 0, fstype); + } + catch + { + } + // 旧版解析失败返回 null 会在 removeEmpty 处 NRE;此处退回空表,走列头校验给出友好提示 + return RemoveEmptyRows(dt ?? new DataTable()); + } + + /// 删除全空行(旧 common.removeEmpty)。 + private static DataTable RemoveEmptyRows(DataTable dt) + { + var removelist = new List(); + for (int i = 0; i < dt.Rows.Count; i++) + { + bool rowdataisnull = true; + for (int j = 0; j < dt.Columns.Count; j++) + { + if (!string.IsNullOrEmpty(dt.Rows[i][j].ToString().Trim())) + rowdataisnull = false; + } + if (rowdataisnull) + removelist.Add(dt.Rows[i]); + } + foreach (var row in removelist) + dt.Rows.Remove(row); + return dt; + } + + /// 审批单 PDF 下载(旧 DwonWYSQD:逐单 HTML → wkhtmltopdf → 合并)。 + public async Task<(byte[] Content, string FileName)> DwonWYSQDAsync(string schSerId, string userId, string ids) + { + var listDocPrintID = new List(); + foreach (var id in (ids ?? "").Replace(",", ",").Split(',')) + { + if (IsValidNumber(id)) + listDocPrintID.Add(Convert.ToDecimal(id)); + } + + var (listCont, listdoctitle, listimg) = await DownFileHtmlAsync(schSerId, userId, listDocPrintID); + string filename = "文印审批单"; + byte[] arrPdf; + if (listCont.Count > 0) + { + filename += listCont.Count == 1 + ? "(" + listdoctitle[0] + ")" + : "(共有" + listCont.Count + "条记录)"; + arrPdf = PdfHelper.ConvertHtmlTextToPdfByList(listCont); + } + else + { + filename += "(无内容)"; + arrPdf = PdfHelper.ConvertHtmlTextToPdfByList(new List { "

无内容

" }); + } + + // 清理签名临时 PNG(每单一个 Ticks 目录) + foreach (var item in listimg) + { + string dir = Path.GetDirectoryName(item); + if (!string.IsNullOrEmpty(dir) && Directory.Exists(dir)) + Directory.Delete(dir, true); + } + + return (arrPdf, filename + ".pdf"); + } + + /// 逐单拼装审批单 HTML(旧 downfilehtml)。返回 HTML 列表、单据标题列表、签名临时图片路径列表。 + private async Task<(List HtmlList, List DocTitles, List TempImages)> DownFileHtmlAsync( + string schSerId, string userId, List listDocPrintID) + { + var listdoc = await _doc.FindAsync(a => a.SchSerID == schSerId); + var docById = listdoc.ToDictionary(a => a.SerID); + var listdoctype = await _dic.FindAsync(a => + (a.Schserid == schSerId || a.Schserid == null) && (a.typeID == "DW" || a.typeID == "ZBInfo")); // 单位 + 组别 + + var listdoctitle = new List(); + var listhtml = new List(); + var listimg = new List(); + foreach (var docID in listDocPrintID) + { + if (!docById.TryGetValue(docID, out var doc)) + continue; + + // 附件(旧版判空恒真,UpFile 为 null 时会 NRE;此处空值得空列表) + var listupfile = (doc.UpFile ?? "") + .Split(',') + .Where(IsValidNumber) + .Select(a => Convert.ToInt32(a)) + .ToList(); + var listFile = listupfile.Count > 0 + ? await _upfiles.FindAsync(a => listupfile.Contains(a.SerID)) + : new List(); + + var listflow = await _flowinfo.FindAsync(a => a.SerID == doc.flowid); + // 单选还是多选(flow 缺失时旧版直接跳过该单据) + if (listflow.Count == 0) + continue; + string f = listflow[0].WYPageGG; + + // 印刷类型:单条规格取该单 DPType 对应类型,多条取全部 Code=="2" 类型 + // (旧版在多单据间复用同一 listYZFL 变量逐次收窄,属潜在缺陷;此处每单据独立收窄) + var listYZFLAll = await GetDicInfoAsync(schSerId, "YZFLSHRole"); + List listYZFL = f == "1" + ? listYZFLAll.FindAll(a => a.Code != "2" && a.CodeID == doc.DPType) + : listYZFLAll.FindAll(a => a.Code == "2"); + + var listYZFLID = listYZFL.Select(a => a.CodeID).ToList(); + // 全部规格(含单价 Code) + var listggall = await _dic.FindAsync(a => + (a.Schserid == schSerId || a.Schserid == null) && listYZFLID.Contains(a.typeID)); + + // 规格骨架行 + var tbdata = new List(); + foreach (var item in listYZFL) + { + var listGG = await GetDicInfoAsync(schSerId, item.CodeID); + foreach (var d in listGG) + { + tbdata.Add(new WY_DocPrint_Num + { + YSLXName = item.Name, + YSLXtypeID = item.CodeID, + YSLXOrder = item.Sort, + YSGGName = d.Name, + YSGGtypeID = d.CodeID, + YSGGOrder = d.Sort, + }); + } + } + + double zje = 0; + if (f == "1") + { + // 单条 + if (IsValidNumber(doc.Examtype)) + zje = Math.Round(Convert.ToDouble(doc.Examtype), 2); + var fdgg = tbdata.FindAll(a => a.YSGGtypeID == doc.DocStyle); + if (fdgg.Count > 0) + { + fdgg[0].DocShare = doc.DocShare; // 份数 + fdgg[0].DocFix = doc.DocFix; // 张数 + fdgg[0].DocNum = doc.DocNum; // 总张数 + if (IsInteger(doc.grade)) + fdgg[0].DocPage = Convert.ToInt32(doc.grade); // 页数 + if (IsValidNumber(doc.Examtype)) + fdgg[0].TotalPrice = Math.Round(Convert.ToDouble(doc.Examtype)); // 总金额 + + var fdggbyall = listggall.FindAll(a => a.CodeID == doc.DocStyle); + if (fdggbyall.Count > 0 && IsValidNumber(fdggbyall[0].Code)) + fdgg[0].UnitPrice = Convert.ToDouble(fdggbyall[0].Code); + } + } + else + { + // 多条 + var listnum = await _docNum.FindAsync(a => a.DocPrintID == doc.SerID); + foreach (var item in listnum) + { + var fd = tbdata.FindAll(a => a.YSGGtypeID == item.YSGGtypeID); + if (fd.Count > 0) + { + fd[0].UnitPrice = item.UnitPrice; + fd[0].DocShare = item.DocShare; + fd[0].DocFix = item.DocFix; + fd[0].DocNum = item.DocNum; + fd[0].DocPage = item.DocPage; + fd[0].TotalPrice = item.TotalPrice; + zje += Convert.ToDouble(item.TotalPrice); + } + } + } + + listdoctitle.Add(doc.DocTitle); + + // 内容占位符 + var cont = new List<(string txt, string val)> + { + ("{标题}", doc.DocTitle), + ("{使用单位}", listdoctype.FindAll(a => a.SerID == (doc.rid ?? -1)).Select(a => a.Name).FirstOrDefault() ?? ""), + ("{申请日期}", doc.EditDate.ToString("d")), + ("{组别}", listdoctype.FindAll(a => a.SerID == (doc.reid ?? -1)).Select(a => a.Name).FirstOrDefault() ?? ""), + ("{申请人}", await GetUserNameAsync(doc.UserID)), + ("{文印内容}", doc.DocContent), + ("{合计金额}", IsValidNumber(doc.Examtype) + ? Math.Round(zje, 2) + "(元)  " + NumberToChineseConverter.ToChineseUppercase(zje) + : ""), + ("{附件}", listFile.Count > 0 + ? string.Join("", listFile.Select(file => + { + double size = Convert.ToDouble(file.FileSize); + size = Math.Round(size / 1024 / 1024, 6); + return file.FileTrueName + "(" + size + "M);"; + })) + : ""), + }; + + var (str, shImages) = await GetHtmlStrAsync(schSerId, userId, Convert.ToInt32(doc.flowid), Convert.ToInt32(doc.SerID), tbdata, cont); + listimg.AddRange(shImages); + listhtml.Add(str); + } + + return (listhtml, listdoctitle, listimg); + } + + /// 整页审批单 HTML(旧 GetHtmlStr:头部 + 信息区 + 规格表 + 金额/附件/签名表 + 收尾)。 + private async Task<(string Html, List TempImages)> GetHtmlStrAsync( + string schSerId, string userId, int flowid, int docprintid, + List tbdata, List<(string txt, string val)> contdata) + { + List head = GetHtmlHead(); + List body = GetHtmlCont(contdata); + var (shTable, shImages) = await GetHtmlContSHAsync(schSerId, userId, flowid, docprintid); + body[1] = body[1].Replace("{SHtable}", shTable); + string tb = GetHtmlTable(tbdata); + + string rlt = head[0] + body[0] + tb + body[1] + head[1]; + return (rlt, shImages); + } + + /// 审批签名表 HTML(旧 GetHtmlContSH):流程步骤 × 已审签名图片,签名转 PNG 临时文件内嵌本地路径。 + private async Task<(string Html, List TempImages)> GetHtmlContSHAsync( + string schSerId, string userId, int flowid, int docprintid) + { + string str = @"
+ + {td1} + + + {td2} + +
"; + string str2 = @" + + + + +
{title}:{img}
"; + int coltotalnum = 130 * 6; + var listflowstep = await _flowstep.FindAsync(a => a.flowid == flowid); + var listSH = await _flowtranct.FindAsync(a => a.dataid == docprintid && a.actionstatus == 1); + // 旧版无步骤时 coltotalnum/0 直接崩溃;此处退回 1 避免除零(循环不会执行,仅返回空壳表) + int colnum = listflowstep.Count > 0 ? coltotalnum / listflowstep.Count : coltotalnum; + string td1 = ""; + string td2 = ""; + int imgheight = 50; + int imgwidth; + var listimg = new List(); + string filepath = Path.Combine(FileService.StorageRoot(), "Signature", "temp", DateTime.Now.Ticks.ToString()) + Path.DirectorySeparatorChar; + + for (int i = 0; i < listflowstep.Count; i++) + { + string tb2 = str2; + td2 += @""; + td1 += @"{td1}"; + tb2 = tb2.Replace("{title}", listflowstep[i].StepName); + imgwidth = 80; + if (listflowstep[i].StepName.Length > 6) + { + imgwidth = 60; + } + + var fd = listSH.FindAll(a => a.stepid == listflowstep[i].SerID); + if (fd.Count > 0) + { + string img = ""; + string imgstrW = @""; + foreach (var item in fd) + { + string SHUserID = item.actionuserid.ToString(); + string imgbase64 = await GetSignatureAsync(schSerId, SHUserID); + if (imgbase64 != "") + { + // 生成 png 文件(旧版未显式建目录,依赖目录已存在) + Directory.CreateDirectory(filepath); + string filename = filepath + SHUserID + ".png"; + PdfHelper.ConvertBase64ToPng(imgbase64, filename); + listimg.Add(filename); + string tpstr = imgstrW; + tpstr = tpstr.Replace("{src}", filename); + img += tpstr; + } + } + tb2 = tb2.Replace("{img}", img); + td1 = td1.Replace("{td1}", tb2); + } + else + { + tb2 = tb2.Replace("{img}", ""); + td1 = td1.Replace("{td1}", tb2); + } + } + str = str.Replace("{td1}", td1).Replace("{td2}", td2); + + return (str, listimg); + } + + /// 审批单 HTML 头/尾与 CSS(旧 GetHtmlHead)。 + private static List GetHtmlHead() + { + List rlt = new List { "", "" }; + rlt[0] = @" + + + + + + 文印审批单 + + + +
+ "; + rlt[1] = "
"; + + return rlt; + } + + /// 规格明细表(类型列按 YSLXtypeID 跨行合并,旧 GetHtmlTable)。 + private static string GetHtmlTable(List data) + { + string rlt = ""; + string divd2st = "
"; + string divd2ed = "
"; + string tablest = ""; + string tableed = "
"; + string tbHead = @" + + + 类型 + 人数(份数) + 页数(每份) + 张数 + 总和 + + + "; + string tr = @" + + + + + + + + + "; + string tbodyst = ""; + string tbodyed = ""; + string trst = ""; + string tred = ""; + string tdst = ""; + string tded = ""; + string tdrowspancell = ""; + + var rowspan = new List(); + string front = ""; + int n = 0; + int m = 0; + for (int i = 0; i < data.Count; i++) + { + rowspan.Add(1); + if (i == 0) + { + front = data[i].YSLXtypeID; + n++; + } + else + { + if (front == data[i].YSLXtypeID) + { + n++; + } + else + { + front = data[i].YSLXtypeID; + + if (n > 1) + { + // 有合并 + for (int j = m; j < rowspan.Count; j++) + { + rowspan[j] = 0; + } + rowspan[m] = n; + } + m = i; + n = 1; + rowspan[m] = 1; + } + } + // 最后判断是否有合并 + if (i == data.Count - 1 && n > 1) + { + // 有合并 + for (int j = m; j < rowspan.Count; j++) + { + rowspan[j] = 0; + } + rowspan[m] = n; + } + } + string rowstr = ""; + for (int i = 0; i < data.Count; i++) + { + string tp = trst; + int row = rowspan[i]; + // 印刷类型列 + if (row == 0) + { + // 不添加单元格 + } + else if (row == 1) + { + // 正常单元格 + tp += tdst + data[i].YSLXName + tded; + } + else if (row > 1) + { + // 合并单元格 + tp += tdrowspancell.Replace("{n}", row.ToString()) + data[i].YSLXName + tded; + } + // 规格 + tp += tdst + data[i].YSGGName + tded; + // 人数份数 + tp += tdst + data[i].DocShare + tded; + // 页数每份 + tp += tdst + data[i].DocPage + tded; + // 张数每份 + tp += tdst + data[i].DocFix + tded; + // 总和 + tp += tdst + data[i].DocNum + tded; + + tp += tred; + + rowstr += tp; + } + rlt += divd2st; + rlt += tablest; + rlt += tbHead; + rlt += tbodyst; + rlt += rowstr; + rlt += tr; + rlt += tbodyed; + rlt += tableed; + rlt += divd2ed; + + return rlt; + } + + /// 审批单信息区 HTML(标题/单位/日期/组别/申请人/内容 + 金额/附件 + 签名表占位,旧 GetHtmlCont)。 + private static List GetHtmlCont(List<(string txt, string val)> data) + { + List rlt = new List { "", "" }; + string htmlstr = @" +

文印审批单

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
标题{标题}
使用单位{使用单位}申请日期{申请日期}
组别{组别}申请人{申请人}
文印内容{文印内容}
+
+ "; + + string htmlstr2 = @" +
+ + + + + + + + + + + + + + + + + + +
合计金额{合计金额}
附件{附件}
+ {SHtable} +
+ "; + + foreach (var item in data) + { + htmlstr = htmlstr.Replace(item.txt, item.val); + htmlstr2 = htmlstr2.Replace(item.txt, item.val); + } + + rlt[0] = htmlstr; + string datenow = DateTime.Now.ToString("yyy-MM-dd"); + rlt[1] = htmlstr2.Replace("{datenow}", datenow); + + return rlt; + } + + #endregion + #region 内部辅助 /// 获取学校教师映射(TeaSerId → (Name, Photo))。 diff --git a/src/YhCore.Service/Interfaces/IWYSHRoleService.cs b/src/YhCore.Service/Interfaces/IWYSHRoleService.cs index a5815df..0dc225a 100644 --- a/src/YhCore.Service/Interfaces/IWYSHRoleService.cs +++ b/src/YhCore.Service/Interfaces/IWYSHRoleService.cs @@ -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(单实例)。 /// public interface IWYSHRoleService { @@ -66,5 +70,24 @@ namespace YhCore.Service.Interfaces Task RegetJEAsync(string schSerId, string userId, List listDocPrintId, string type); Task> GetRegetJERdAsync(string schSerId, int pageSize, int pageIndex); Task SetDownFileTimesAsync(int serId); + + // ---- Excel 导入导出 / 审批单 PDF(延后批次补齐)---- + /// 下载导入模板(旧 downMBOfSHRole,代码生成)。 + Task DownMBOfSHRoleAsync(); + + /// Excel 批量导入审核角色(旧 UpfileExcelSHRole,multipart 第一个文件),返回反馈消息。 + Task UpfileExcelSHRoleAsync(string schSerId, string userId, IFormFile file); + + /// 下载最近一次导入的反馈 Excel(旧 downUpfileSHRoleFK)。 + Task DownUpfileSHRoleFKAsync(string userId); + + /// 全部文印导出(旧 ExceltotalListSHRole)。 + Task<(byte[] Content, string FileName)> ExceltotalListSHRoleAsync(string schSerId, string name, string stateid, string starttime, string endtime); + + /// 文印记录导出(旧 WYRDExceldaochu,flag=page 简版 / 其余完整版)。 + Task<(byte[] Content, string FileName)> WYRDExceldaochuAsync(string schSerId, int flowid, int dwid, string name, string stateid, string starttime, string endtime, string flag); + + /// 审批单 PDF 下载(旧 DwonWYSQD,IDS 逗号分隔的 WY_DocPrint.SerID)。 + Task<(byte[] Content, string FileName)> DwonWYSQDAsync(string schSerId, string userId, string ids); } } diff --git a/src/YhCore.Service/YhCore.Service.csproj b/src/YhCore.Service/YhCore.Service.csproj index 45131d3..3da8fec 100644 --- a/src/YhCore.Service/YhCore.Service.csproj +++ b/src/YhCore.Service/YhCore.Service.csproj @@ -27,4 +27,11 @@ + + + + + + +