Files
SGGL_HBAZ/SGGL/BLL/HSSE/Hazard/SafetyPatrolWordService.cs
T
yangjl 192824ea90 安全巡检接入真实数据库:编码字段自动生成、公示牌二维码修复、安全问题统计排除作废
- 安全巡检表新增编码字段(Code,全局唯一 XJ-序号),WebAPI/桌面端新增时自动生成,不可重复
- 修复危大超危工程公示牌二维码不显示:urlName 含非法字符?导致 image.Save 抛异常,改用主键+内容哈希安全文件名
- 安全问题统计(mainProject/main3)排除作废数据(States=4),图表口径与顶部计数一致
- Word 导出文件名改为 监督巡视部位+编号
- Model.cs 由 SqlMetal 重新生成,补齐巡检人ID/签收人ID/发现问题/处理要求等字段;新增 HSSE_SafetyPatrol.Code.cs 补充 Code 列
- 新增安全巡检完整建表脚本(SGGLDB_V2026-08-25-001);删除已过时的接口文档 SafetyPatrol_API.md
2026-08-26 19:13:11 +08:00

501 lines
21 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Drawing;
using System.IO;
using System.Linq;
using Aspose.Words;
using Aspose.Words.Tables;
namespace BLL
{
/// <summary>
/// 安全巡检——导出监督记录表 Word
/// </summary>
public static class SafetyPatrolWordService
{
/// <summary>
/// 生成附件9《(危大工程类别)监督记录表》Word 文档(.docx)
/// </summary>
/// <param name="patrolId">巡检记录主键</param>
/// <returns>docx 字节流;记录不存在返回 null</returns>
public static byte[] Generate(string patrolId)
{
Model.HSSE_SafetyPatrol info = SafetyPatrolService.GetById(patrolId);
if (info == null)
{
return null;
}
Document doc = new Document();
DocumentBuilder b = new DocumentBuilder(doc);
AppendRecord(b, info);
ApplyFonts(doc);
ApplyLineSpacing(doc);
MemoryStream ms = new MemoryStream();
doc.Save(ms, SaveFormat.Docx);
return ms.ToArray();
}
/// <summary>
/// 写入单独一条(标题 + 记录表)
/// </summary>
/// <param name="b">文档构建器</param>
/// <param name="info">巡检记录</param>
private static void AppendRecord(DocumentBuilder b, Model.HSSE_SafetyPatrol info)
{
////全局默认字体
b.Font.Name = "微软雅黑";
b.Font.NameFarEast = "微软雅黑";
b.Font.Size = 10.5;
////固定页面为 A4,保证表格宽度不会被压缩(A4 内容宽 = 595 - 40 - 40 = 515 磅)
PageSetup ps = b.Document.FirstSection.PageSetup;
ps.PageWidth = 595;
ps.PageHeight = 842;
ps.LeftMargin = 40;
ps.RightMargin = 40;
ps.TopMargin = 40;
ps.BottomMargin = 40;
////标题(附件9
b.ParagraphFormat.Alignment = ParagraphAlignment.Center;
b.Font.Size = 16;
b.Font.Bold = true;
b.Write("附件9 (危大工程类别)监督记录表");
b.ParagraphFormat.SpaceAfter = 30; // 标题与下方「监督巡视部位」之间留 40px ≈ 30pt
b.Writeln();
b.Font.Bold = false;
b.Font.Size = 10.5;
b.ParagraphFormat.SpaceAfter = 0; // 还原,避免影响后续段落
b.ParagraphFormat.Alignment = ParagraphAlignment.Left;
////监督巡视部位 / 巡视日期:拆成左右两列、各占一行 50%(无边框),
////与下方记录表同宽(412 磅、居中,两列各 206 磅),保证与表格两列精确对齐。
Table head = b.StartTable();
b.CellFormat.VerticalAlignment = CellVerticalAlignment.Center;
b.CellFormat.Width = 206;
b.InsertCell();
b.Write("监督巡视部位:" + (info.HazardProjectName ?? ""));
b.CellFormat.Width = 206;
b.InsertCell();
b.Write("巡视日期:" + (info.PatrolDate.HasValue ? info.PatrolDate.Value.ToString("yyyy-MM-dd") : ""));
b.EndRow();
b.EndTable();
////无边框 + 定宽居中,使两列与下方记录表的起止完全对齐
head.AllowAutoFit = false;
head.PreferredWidth = PreferredWidth.FromPoints(412);
head.Alignment = TableAlignment.Center;
head.Rows[0].Cells[0].CellFormat.Width = 206;
head.Rows[0].Cells[1].CellFormat.Width = 206;
////默认表格无边框;个别环境下若带边框则显式清空
foreach (Cell c in head.Rows[0].Cells)
{
foreach (Border bd in c.CellFormat.Borders)
{
bd.LineStyle = LineStyle.None;
}
}
////记录表主体:统一两列(左=标签/内容区,右=发现问题/签收人/空),每行都是独立单元格。
////重要:不要用 VerticalMerge 或全宽 gridSpan —— DocumentBuilder 无法可靠地只合并右列,
////会把左列下部的「处理要求/整改后复查/备注」吞掉或把表格网格弄乱。统一两列最稳定。
Table table = b.StartTable();
b.CellFormat.VerticalAlignment = CellVerticalAlignment.Center;
////行1:表头 监督巡视内容 | 发现问题
b.CellFormat.Width = 340;
b.InsertCell();
b.Write("监督巡视内容");
b.CellFormat.Width = 175;
b.InsertCell();
b.Write("发现问题");
b.EndRow();
////行3:勾选项(固定4项+其它) | 发现问题内容
b.CellFormat.Width = 340;
b.InsertCell();
WriteContentLines(b, info.Content);
b.CellFormat.Width = 175;
b.InsertCell();
b.Write(info.Problems ?? "");
b.EndRow();
////行4:处理要求: | 签收人:
b.CellFormat.Width = 340;
b.InsertCell();
b.Write("处理要求:");
b.CellFormat.Width = 175;
b.InsertCell();
b.Write("签收人:" + (string.IsNullOrEmpty(info.Receiver) ? "____________" : info.Receiver));
b.EndRow();
////行5:处理要求内容区(占3行),整格横向合并(横跨两列)
b.CellFormat.Width = 340;
b.InsertCell();
WriteArea(b, info.HandleRequire, 3);
b.CellFormat.Width = 175;
b.InsertCell();
b.EndRow();
////行6-7:整改后复查情况 + 内容区,合并为一个整宽纵向单元格。
////重要:纵并组里只有 restart 格(上半行)的正文会显示,continue 格必须留空,
////因此把「标签:」与内容都写入 restart 格。
b.CellFormat.Width = 340;
b.InsertCell();
WriteLabelArea(b, "整改后复查情况:", info.RectifyReview, 5);
b.CellFormat.Width = 175;
b.InsertCell();
b.EndRow();
////行7:纵并 continue 格(留空)
b.CellFormat.Width = 340;
b.InsertCell();
b.Writeln();
b.CellFormat.Width = 175;
b.InsertCell();
b.Writeln();
b.EndRow();
////行8-9:备注 + 内容区,合并为一个整宽纵向单元格
b.CellFormat.Width = 340;
b.InsertCell();
WriteLabelArea(b, "备注:", info.Remarks, 2);
b.CellFormat.Width = 175;
b.InsertCell();
b.EndRow();
////行9:纵并 continue 格(留空)
b.CellFormat.Width = 340;
b.InsertCell();
b.Writeln();
b.CellFormat.Width = 175;
b.InsertCell();
b.Writeln();
b.EndRow();
b.EndTable();
////表格宽度:占内容区 80%515 × 0.8 = 412 磅),并水平居中
table.AllowAutoFit = false;
table.PreferredWidth = PreferredWidth.FromPoints(412);
table.Alignment = TableAlignment.Center;
double cellWidth = 412 / 2.0; // 两列等宽 = 206 磅
foreach (Row row in table.Rows)
{
row.Cells[0].CellFormat.Width = cellWidth; // 左列
row.Cells[1].CellFormat.Width = cellWidth; // 右列(与左列等宽)
}
////发现问题内容(行1右列):左上显示,不垂直居中。
////DocumentBuilder 中途设置 CellFormat.VerticalAlignment 对当前单元格不生效,
////必须在 post-build 阶段用 Cell 对象设置(与合并同一方式)。
table.Rows[1].Cells[1].CellFormat.VerticalAlignment = CellVerticalAlignment.Top;
////整宽 / 跨行合并:post-build 阶段用 Cell 对象合并,gridCol 仍保持两列。
////表头之下共 8 行:索引0=表头,1=发现问题,2=处理要求/签收人,
////索引3=处理要求内容区(横并),索引4-5=整改后复查(纵并+整宽),索引6-7=备注(纵并+整宽)。
HorizontalMergeRow(table.Rows[3]);
HorizontalMergeRow(table.Rows[4]); VerticalMergeTop(table.Rows[4]);
HorizontalMergeRow(table.Rows[5]); VerticalMergeBottom(table.Rows[5]);
HorizontalMergeRow(table.Rows[6]); VerticalMergeTop(table.Rows[6]);
HorizontalMergeRow(table.Rows[7]); VerticalMergeBottom(table.Rows[7]);
////表格样式:细黑色网格线(与图片一致)
table.StyleIdentifier = StyleIdentifier.TableGrid;
////底部:监督巡视人员(恢复正文大小,签收人已入表格第4行右列)
////右对齐到表格右缘:右缩进 51.5 磅(= 表格居中后右侧留白)
b.Font.Size = 10.5;
b.Font.Bold = false;
b.ParagraphFormat.Alignment = ParagraphAlignment.Right;
b.ParagraphFormat.LeftIndent = 51.5;
b.ParagraphFormat.RightIndent = 51.5;
b.Write("监督巡视人员:" + (info.PatrolMan ?? ""));
b.Writeln();
////注:左对齐到表格左缘(左缩进 51.5 磅),还原右对齐/右缩进
b.ParagraphFormat.Alignment = ParagraphAlignment.Left;
b.ParagraphFormat.RightIndent = 0;
b.Write("注:此表可优化调整,但管理要素不得少于上表。");
b.Writeln();
////巡检附件 / 整改附件:另起一页显示(第二页起)
b.InsertBreak(BreakType.PageBreak);
AppendAttachments(b, info.PatrolId);
}
/// <summary>
/// 在第二页写入巡检附件与整改附件图片(按记录遍历,逐张缩放嵌入)。
/// 附件图片均为磁盘文件,通过 localRoot + AttachUrl 定位;无附件则给出「无」。
/// </summary>
/// <param name="b">文档构建器</param>
/// <param name="patrolId">巡检记录主键</param>
private static void AppendAttachments(DocumentBuilder b, string patrolId)
{
if (string.IsNullOrEmpty(patrolId))
{
return;
}
string localRoot = ConfigurationManager.AppSettings["localRoot"] ?? string.Empty;
b.Font.Bold = false;
b.Font.Size = 10.5;
WriteAttachSection(b, "一、巡检附件", patrolId, Const.SafetyPatrolMenuId, localRoot);
WriteAttachSection(b, "二、整改附件", patrolId, Const.SafetyPatrolRectifyMenuId, localRoot);
}
/// <summary>
/// 写入一个附件分组:标题行 + 该分组下的所有附件图片(缩放至最大宽 412 磅保持原比例)。
/// 非图片文件仅列出文件名;分组无附件则写「无」。
/// </summary>
/// <param name="b">文档构建器</param>
/// <param name="title">分组标题(如「一、巡检附件」)</param>
/// <param name="patrolId">巡检记录主键</param>
/// <param name="menuId">附件菜单ID</param>
/// <param name="localRoot">附件磁盘根目录</param>
private static void WriteAttachSection(DocumentBuilder b, string title, string patrolId, string menuId, string localRoot)
{
b.ParagraphFormat.Alignment = ParagraphAlignment.Left;
b.ParagraphFormat.SpaceBefore = 6;
b.Write(title);
b.Writeln();
b.ParagraphFormat.SpaceBefore = 0;
List<Model.AttachFile> files = Funs.DB.AttachFile
.Where(p => p.ToKeyId == patrolId && p.MenuId == menuId)
.ToList();
if (files == null || files.Count == 0)
{
b.Write("无");
b.Writeln();
return;
}
foreach (Model.AttachFile f in files)
{
if (string.IsNullOrEmpty(f.AttachUrl))
{
continue;
}
string filePath = Path.Combine(localRoot, f.AttachUrl.Replace('/', '\\'));
if (!File.Exists(filePath))
{
continue;
}
string ext = Path.GetExtension(filePath).ToLowerInvariant();
if (ext == ".jpg" || ext == ".jpeg" || ext == ".png" || ext == ".gif" || ext == ".bmp")
{
b.ParagraphFormat.SpaceBefore = 4;
InsertImageScaled(b, filePath, 400, 260);
b.Writeln(); // 每张图独占一行,避免后续标题/图片内联到图右侧
b.ParagraphFormat.SpaceBefore = 0;
}
else
{
b.Write(Path.GetFileName(filePath));
b.Writeln();
}
}
}
/// <summary>
/// 插入图片:缩放至不超过指定宽高(磅),保持原始宽高比。
/// </summary>
/// <param name="b">文档构建器</param>
/// <param name="filePath">图片物理路径</param>
/// <param name="maxWidthPt">最大宽(磅)</param>
/// <param name="maxHeightPt">最大高(磅)</param>
private static void InsertImageScaled(DocumentBuilder b, string filePath, double maxWidthPt, double maxHeightPt)
{
try
{
using (Image img = Image.FromFile(filePath))
{
int w = img.Width;
int h = img.Height;
if (w <= 0 || h <= 0)
{
return;
}
double scale = Math.Min(maxWidthPt / w, maxHeightPt / h);
b.InsertImage(filePath, w * scale, h * scale);
}
}
catch
{
// 非图片或读取失败:忽略
}
}
/// <summary>
/// 写入内容区单元格:先写内容,再用空段落撑高到指定行数。
/// 无内容时也保证最小行高,便于手写填写。
/// </summary>
/// <param name="b">文档构建器</param>
/// <param name="text">内容</param>
/// <param name="minLines">最小行数(空段落数)</param>
private static void WriteArea(DocumentBuilder b, string text, int minLines)
{
if (!string.IsNullOrEmpty(text))
{
b.Write(text);
}
for (int i = 0; i < minLines; i++)
{
b.Writeln();
}
}
/// <summary>
/// 监督巡视内容固定项(与编辑页复选框 Text 一致)
/// </summary>
private static readonly string[] PatrolContentItems = new string[]
{
"1、施工条件保持情况",
"2、按方案施工情况,实体与方案参数的对比",
"3、现场安全防护状况",
"4、作业人员持证上岗情况"
};
/// <summary>
/// 将监督巡视内容写入单元格:始终保持 4 个固定项(写死默认值)+ 一行「其它」。
/// 其它:Content 字段以后只存「其它」内容,直接显示;若含「其它:」前缀则取后半段(兼容旧格式);无内容则下划线占位。
/// </summary>
/// <param name="b">文档构建器</param>
/// <param name="content">Content 文本(以后仅存其它内容,如:基坑围挡损坏;旧数据可能形如:1、…;4、…;其它:xxx</param>
private static void WriteContentLines(DocumentBuilder b, string content)
{
bool first = true;
////固定 4 项始终输出(写死默认值)
foreach (string item in PatrolContentItems)
{
if (!first)
{
b.Writeln();
}
b.Write(item);
first = false;
}
////其它行:优先取 Content 中「其它:」后半段(兼容旧格式);否则 Content 即其它内容
b.Writeln();
string other = ExtractOther(content);
if (other.Length == 0)
{
other = content ?? string.Empty;
}
b.Write("其它:" + (other.Length > 0 ? other : "____________"));
}
/// <summary>
/// 从 Content 文本中提取「其它」说明内容(无则返回空串)
/// </summary>
/// <param name="content">Content 文本</param>
/// <returns>其它说明;未包含返回空串</returns>
private static string ExtractOther(string content)
{
if (string.IsNullOrEmpty(content))
{
return string.Empty;
}
foreach (string seg in content.Split(''))
{
string s = seg.Trim();
if (s.Length == 0)
{
continue;
}
if (s.StartsWith("其它") || s.StartsWith("其他"))
{
int idx = s.IndexOf('');
return idx >= 0 ? s.Substring(idx + 1).Trim() : string.Empty;
}
}
return string.Empty;
}
/// <summary>
/// 将一行的左右两格横向合并为整宽单元格(gridSpan=2)。
/// 必须在 post-build 阶段用 Cell 对象合并,避免 DocumentBuilder 把表格网格弄乱。
/// </summary>
/// <param name="row">待合并的行</param>
private static void HorizontalMergeRow(Row row)
{
row.Cells[0].CellFormat.HorizontalMerge = CellMerge.First;
row.Cells[1].CellFormat.HorizontalMerge = CellMerge.Previous;
}
/// <summary>
/// 纵向合并的起始行(restart)。该行正文会被保留,并与其下方 continue 行合并为一个单元格。
/// </summary>
/// <param name="row">起始行</param>
private static void VerticalMergeTop(Row row)
{
row.Cells[0].CellFormat.VerticalMerge = CellMerge.First;
row.Cells[1].CellFormat.VerticalMerge = CellMerge.First;
}
/// <summary>
/// 纵向合并的延续行(continue)。该行正文不会显示,必须留空。
/// </summary>
/// <param name="row">延续行</param>
private static void VerticalMergeBottom(Row row)
{
row.Cells[0].CellFormat.VerticalMerge = CellMerge.Previous;
row.Cells[1].CellFormat.VerticalMerge = CellMerge.Previous;
}
/// <summary>
/// 写入「标签:」+ 内容区,标签独占一行,内容随后以内容/空段落撑高。
/// 用于纵向合并单元格(标签与内容需写入 restart 格)。
/// </summary>
/// <param name="b">文档构建器</param>
/// <param name="label">标签(如「整改后复查情况:」)</param>
/// <param name="text">内容</param>
/// <param name="minLines">最小行数(空段落数)</param>
private static void WriteLabelArea(DocumentBuilder b, string label, string text, int minLines)
{
b.Write(label);
b.Writeln();
WriteArea(b, text, minLines);
}
/// <summary>
/// 统一全文正文字体:仅标题「附件9 …监督记录表」加粗(16 磅),其余一律 10.5 磅、不加粗。
/// DocumentBuilder 在跨表格书写时会丢失字符格式状态(标题的加粗/16 磅曾泄漏到记录表与落款),
/// 故在 post-build 阶段对所有 Run 兜底逐一遍历设置,保证只有标题加粗。
/// </summary>
/// <param name="doc">文档</param>
private static void ApplyFonts(Document doc)
{
foreach (Run run in doc.GetChildNodes(NodeType.Run, true))
{
bool isTitle = run.Text == "附件9 (危大工程类别)监督记录表";
run.Font.Bold = isTitle;
run.Font.Size = isTitle ? 16 : 10.5;
}
}
/// <summary>
/// 为文档中所有段落(含表格单元格内)设置 1.5 倍行距。
/// Aspose 的 Multiple 行距以磅为单位:单倍 = 12 磅,1.5 倍 = 18 磅,
/// 生成 w:line="360" w:lineRule="auto"360/240 = 1.5 倍)。
/// </summary>
/// <param name="doc">文档</param>
private static void ApplyLineSpacing(Document doc)
{
foreach (Paragraph p in doc.GetChildNodes(NodeType.Paragraph, true))
{
p.ParagraphFormat.LineSpacingRule = LineSpacingRule.Multiple;
p.ParagraphFormat.LineSpacing = 14; // 1.5 倍行距(18 磅 → w:line="360"360/240=1.5
}
}
}
}