安全巡检接入真实数据库:编码字段自动生成、公示牌二维码修复、安全问题统计排除作废

- 安全巡检表新增编码字段(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
This commit is contained in:
2026-08-26 19:13:11 +08:00
parent 55944d732a
commit 192824ea90
27 changed files with 1601 additions and 309 deletions
@@ -0,0 +1,500 @@
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
}
}
}
}