新增:安全技术数据

This commit is contained in:
2026-09-11 11:14:08 +08:00
parent 9bce0d340f
commit 5a2830d36a
24 changed files with 5065 additions and 292 deletions
+443
View File
@@ -0,0 +1,443 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
namespace BLL
{
/// <summary>
/// PDF 书籍模块 WebAPI 数据服务(移动端 / 第三方系统集成用)。
/// 对应 PC 页面 HSSE/PDFDOC/PdfBookView.aspx 的完整功能:书籍、多级章节目录、章节正文(富文本 HTML)的
/// 查询与维护(新增书籍 / 新增·编辑·删除章节 / 保存章节正文),并额外提供章节关键词检索。
/// 数据表:Sys_PdfBook / Sys_PdfChapter(正文存 [Text] 列,为 UMeditor 生成的 HTML)。
/// 说明:目录结构用 ParentChapterId + Level(1章 2节 3小节) 表达多级层级,SortNo 控制同级顺序。
/// </summary>
public static class APIPdfBookService
{
#region
/// <summary>书籍列表项。</summary>
public class BookItem
{
/// <summary>书籍主键。</summary>
public string BookId { get; set; }
/// <summary>书名。</summary>
public string Title { get; set; }
/// <summary>章节总数。</summary>
public int ChapterCount { get; set; }
/// <summary>登记时间。</summary>
public DateTime? CreateTime { get; set; }
}
/// <summary>章节目录树节点(多级嵌套)。</summary>
public class ChapterTreeNode
{
/// <summary>章节主键。</summary>
public string ChapterId { get; set; }
/// <summary>章节名称。</summary>
public string ChapterTitle { get; set; }
/// <summary>层级:1章 2节 3小节。</summary>
public int Level { get; set; }
/// <summary>起始页码(无则为0)。</summary>
public int PageStart { get; set; }
/// <summary>结束页码(无则为0)。</summary>
public int PageEnd { get; set; }
/// <summary>是否已有正文内容(用于客户端标识可读章节)。</summary>
public bool HasContent { get; set; }
/// <summary>子章节列表(叶子章节为空数组)。</summary>
public List<ChapterTreeNode> Children { get; set; }
}
/// <summary>章节正文详情。</summary>
public class ChapterDetailItem
{
/// <summary>章节主键。</summary>
public string ChapterId { get; set; }
/// <summary>所属书籍主键。</summary>
public string BookId { get; set; }
/// <summary>所属书籍名称。</summary>
public string BookTitle { get; set; }
/// <summary>章节名称。</summary>
public string ChapterTitle { get; set; }
/// <summary>父章节主键(顶级为 null)。</summary>
public string ParentChapterId { get; set; }
/// <summary>层级:1章 2节 3小节。</summary>
public int Level { get; set; }
/// <summary>起始页码(无则为0)。</summary>
public int PageStart { get; set; }
/// <summary>结束页码(无则为0)。</summary>
public int PageEnd { get; set; }
/// <summary>正文(富文本 HTML,可直接用 WebView 渲染)。</summary>
public string Content { get; set; }
/// <summary>正文纯文本(去掉 HTML 标签,便于全文展示/搜索)。</summary>
public string ContentText { get; set; }
}
/// <summary>章节关键词检索命中项。</summary>
public class SearchHitItem
{
/// <summary>书籍主键。</summary>
public string BookId { get; set; }
/// <summary>书籍名称。</summary>
public string BookTitle { get; set; }
/// <summary>章节主键。</summary>
public string ChapterId { get; set; }
/// <summary>章节名称。</summary>
public string ChapterTitle { get; set; }
/// <summary>起始页码(无则为0)。</summary>
public int PageStart { get; set; }
/// <summary>结束页码(无则为0)。</summary>
public int PageEnd { get; set; }
/// <summary>命中片段(关键词前后各约40字)。</summary>
public string Snippet { get; set; }
}
#endregion
#region
/// <summary>
/// 获取全部书籍(按登记时间升序,与页面下拉一致)。
/// </summary>
public static List<BookItem> GetBooks()
{
var list = new List<BookItem>();
DataTable dt = SQLHelper.GetDataTableRunText(
"SELECT b.BookId, b.Title, b.CreateTime, " +
"(SELECT COUNT(*) FROM dbo.Sys_PdfChapter c WHERE c.BookId = b.BookId) AS ChapterCount " +
"FROM dbo.Sys_PdfBook b ORDER BY b.CreateTime", new SqlParameter[] { });
if (dt == null)
return list;
foreach (DataRow r in dt.Rows)
{
list.Add(new BookItem
{
BookId = r["BookId"] == null ? "" : r["BookId"].ToString(),
Title = r["Title"] == null ? "" : r["Title"].ToString(),
ChapterCount = r["ChapterCount"] == DBNull.Value ? 0 : Convert.ToInt32(r["ChapterCount"]),
CreateTime = r["CreateTime"] == DBNull.Value ? (DateTime?)null : Convert.ToDateTime(r["CreateTime"])
});
}
return list;
}
#endregion
#region
/// <summary>
/// 获取某本书的多级章节目录树(嵌套 Children,顶级为根的数组;按 SortNo 排序)。
/// </summary>
public static List<ChapterTreeNode> GetChapterTree(string bookId)
{
if (string.IsNullOrWhiteSpace(bookId))
throw new Exception("bookId 不能为空");
var chapters = PdfBookService.GetChapters(bookId);
var result = new List<ChapterTreeNode>();
if (chapters == null || chapters.Count == 0)
return result;
// 有正文标记(一次查询)
var hasContentSet = new HashSet<string>();
DataTable dt = SQLHelper.GetDataTableRunText(
"SELECT ChapterId FROM dbo.Sys_PdfChapter " +
"WHERE BookId = @BookId AND [Text] IS NOT NULL AND LTRIM(RTRIM([Text])) <> N''",
new[] { new SqlParameter("@BookId", bookId) });
if (dt != null)
foreach (DataRow r in dt.Rows)
hasContentSet.Add(r["ChapterId"] == null ? "" : r["ChapterId"].ToString());
// 1) 先为每个章节建节点
var nodeMap = new Dictionary<string, ChapterTreeNode>();
foreach (PdfChapter c in chapters)
{
nodeMap[c.ChapterId] = new ChapterTreeNode
{
ChapterId = c.ChapterId,
ChapterTitle = c.Title,
Level = c.Level,
PageStart = c.PageStart,
PageEnd = c.PageEnd,
HasContent = hasContentSet.Contains(c.ChapterId),
Children = new List<ChapterTreeNode>()
};
}
// 2) 按父章节挂接(父不在列表中的作为顶级)
foreach (PdfChapter c in chapters)
{
var node = nodeMap[c.ChapterId];
if (!string.IsNullOrWhiteSpace(c.ParentChapterId) && nodeMap.ContainsKey(c.ParentChapterId))
nodeMap[c.ParentChapterId].Children.Add(node);
else
result.Add(node);
}
return result;
}
#endregion
#region
/// <summary>
/// 获取章节正文详情(含 HTML 与纯文本);章节不存在返回 null。
/// </summary>
public static ChapterDetailItem GetChapterDetail(string chapterId)
{
if (string.IsNullOrWhiteSpace(chapterId))
throw new Exception("chapterId 不能为空");
DataTable dt = SQLHelper.GetDataTableRunText(
"SELECT c.ChapterId, c.BookId, b.Title AS BookTitle, c.ParentChapterId, c.[Level], " +
"c.ChapterTitle, c.PageStart, c.PageEnd, c.[Text] " +
"FROM dbo.Sys_PdfChapter c INNER JOIN dbo.Sys_PdfBook b ON c.BookId = b.BookId " +
"WHERE c.ChapterId = @ChapterId",
new[] { new SqlParameter("@ChapterId", chapterId) });
if (dt == null || dt.Rows.Count == 0)
return null;
DataRow r = dt.Rows[0];
string html = r["Text"] == DBNull.Value ? "" : r["Text"].ToString();
var item = new ChapterDetailItem
{
ChapterId = r["ChapterId"] == null ? "" : r["ChapterId"].ToString(),
BookId = r["BookId"] == null ? "" : r["BookId"].ToString(),
BookTitle = r["BookTitle"] == null ? "" : r["BookTitle"].ToString(),
ParentChapterId = r["ParentChapterId"] == DBNull.Value ? null : r["ParentChapterId"].ToString(),
Level = r["Level"] == DBNull.Value ? 1 : Convert.ToInt32(r["Level"]),
ChapterTitle = r["ChapterTitle"] == null ? "" : r["ChapterTitle"].ToString(),
PageStart = r["PageStart"] == DBNull.Value ? 0 : Convert.ToInt32(r["PageStart"]),
PageEnd = r["PageEnd"] == DBNull.Value ? 0 : Convert.ToInt32(r["PageEnd"]),
Content = html,
ContentText = HtmlToPlainText(html)
};
return item;
}
#endregion
#region
/// <summary>
/// 章节关键词检索:在章节正文中按关键词做字面匹配(正文为 HTML,匹配前先剔除标签)。
/// bookId 可空:不传则全库检索。
/// </summary>
public static List<SearchHitItem> SearchChapter(string keyword, string bookId)
{
if (string.IsNullOrWhiteSpace(keyword))
throw new Exception("keyword 不能为空");
// LIKE 通配符转义(ESCAPE 字符为反斜杠)
string esc = keyword.Replace("\\", "\\\\").Replace("%", "\\%").Replace("_", "\\_");
var ps = new List<SqlParameter>
{
new SqlParameter("@Keyword", "%" + esc + "%")
};
string where = "c.[Text] LIKE @Keyword ESCAPE '\\'";
if (!string.IsNullOrWhiteSpace(bookId))
{
where += " AND c.BookId = @BookId";
ps.Add(new SqlParameter("@BookId", bookId));
}
DataTable dt = SQLHelper.GetDataTableRunText(
"SELECT c.ChapterId, c.ChapterTitle, c.BookId, b.Title AS BookTitle, " +
"c.PageStart, c.PageEnd, c.[Text] " +
"FROM dbo.Sys_PdfChapter c INNER JOIN dbo.Sys_PdfBook b ON c.BookId = b.BookId " +
"WHERE " + where + " ORDER BY b.Title, c.SortNo", ps.ToArray());
var list = new List<SearchHitItem>();
if (dt == null)
return list;
foreach (DataRow r in dt.Rows)
{
string html = r["Text"] == DBNull.Value ? "" : r["Text"].ToString();
string plain = HtmlToPlainText(html);
list.Add(new SearchHitItem
{
BookId = r["BookId"] == null ? "" : r["BookId"].ToString(),
BookTitle = r["BookTitle"] == null ? "" : r["BookTitle"].ToString(),
ChapterId = r["ChapterId"] == null ? "" : r["ChapterId"].ToString(),
ChapterTitle = r["ChapterTitle"] == null ? "" : r["ChapterTitle"].ToString(),
PageStart = r["PageStart"] == DBNull.Value ? 0 : Convert.ToInt32(r["PageStart"]),
PageEnd = r["PageEnd"] == DBNull.Value ? 0 : Convert.ToInt32(r["PageEnd"]),
Snippet = BuildSnippet(plain, keyword)
});
}
return list;
}
/// <summary>取关键词命中片段(前后各约40字)。</summary>
private static string BuildSnippet(string plain, string keyword)
{
if (string.IsNullOrEmpty(plain))
return "";
int idx = plain.IndexOf(keyword, StringComparison.OrdinalIgnoreCase);
if (idx < 0)
return plain.Length > 120 ? plain.Substring(0, 120) + "……" : plain;
int start = Math.Max(0, idx - 40);
int len = Math.Min(plain.Length - start, keyword.Length + 80);
return (start > 0 ? "……" : "") + plain.Substring(start, len) + (start + len < plain.Length ? "……" : "");
}
/// <summary>HTML 转纯文本(去标签、解码实体、压缩空白)。</summary>
private static string HtmlToPlainText(string html)
{
if (string.IsNullOrEmpty(html))
return "";
string s = Regex.Replace(html, "(?i)<(br|/p|/div|/li|/tr|/h[1-6])[^>]*>", "\n");
s = Regex.Replace(s, "<[^>]+>", "");
s = System.Net.WebUtility.HtmlDecode(s);
s = Regex.Replace(s, "[\r\n]+", "\n");
s = Regex.Replace(s, "[ \t]+", " ");
return s.Trim();
}
#endregion
#region
/// <summary>
/// 新增书籍(仅书名;FilePath/TotalPages/IndexStatus 与页面一致:NULL/0/2)。
/// </summary>
public static string AddBook(string title, string createUser)
{
if (string.IsNullOrWhiteSpace(title))
throw new Exception("书名不能为空");
if (string.IsNullOrWhiteSpace(createUser))
createUser = "api";
string bookId = NewId("PB");
SQLHelper.ExecutSql(
"INSERT INTO dbo.Sys_PdfBook (BookId, Title, FilePath, TotalPages, IndexStatus, CreateUser, CreateTime) VALUES ('"
+ SqlEsc(bookId) + "',N'" + SqlEsc(title.Trim()) + "',NULL,0,2,N'"
+ SqlEsc(createUser) + "',GETDATE())");
return bookId;
}
#endregion
#region / / /
/// <summary>
/// 新增章节:parentChapterId 为空则为顶级章节,否则挂到指定父章节下(层级=父级+1)。
/// SortNo 取全书当前最大值+1(与页面逻辑一致)。
/// </summary>
public static string AddChapter(string bookId, string parentChapterId, string chapterTitle, string createUser)
{
if (string.IsNullOrWhiteSpace(bookId))
throw new Exception("bookId 不能为空");
if (string.IsNullOrWhiteSpace(chapterTitle))
throw new Exception("章节名称不能为空");
int level = 1;
if (!string.IsNullOrWhiteSpace(parentChapterId))
{
int parentLevel = GetInt(
"SELECT ISNULL([Level],1) FROM dbo.Sys_PdfChapter WHERE ChapterId='" + SqlEsc(parentChapterId) + "'");
if (parentLevel <= 0)
throw new Exception("父章节不存在:" + parentChapterId);
level = parentLevel + 1;
}
int sort = GetInt(
"SELECT ISNULL(MAX(SortNo),0) FROM dbo.Sys_PdfChapter WHERE BookId='" + SqlEsc(bookId) + "'") + 1;
string chapterId = NewId("CH");
SQLHelper.ExecutSql(
"INSERT INTO dbo.Sys_PdfChapter (ChapterId, BookId, ParentChapterId, [Level], ChapterTitle, PageStart, PageEnd, [Text], SortNo) VALUES ('"
+ SqlEsc(chapterId) + "','" + SqlEsc(bookId) + "',"
+ (string.IsNullOrWhiteSpace(parentChapterId) ? "NULL" : "'" + SqlEsc(parentChapterId) + "'")
+ "," + level + ",N'" + SqlEsc(chapterTitle.Trim()) + "',NULL,NULL,N''," + sort + ")");
return chapterId;
}
/// <summary>
/// 编辑章节:仅修改章节名称。
/// </summary>
public static void EditChapter(string chapterId, string chapterTitle)
{
if (string.IsNullOrWhiteSpace(chapterId))
throw new Exception("chapterId 不能为空");
if (string.IsNullOrWhiteSpace(chapterTitle))
throw new Exception("章节名称不能为空");
if (GetInt("SELECT COUNT(*) FROM dbo.Sys_PdfChapter WHERE ChapterId='" + SqlEsc(chapterId) + "'") == 0)
throw new Exception("章节不存在:" + chapterId);
SQLHelper.ExecutSql("UPDATE dbo.Sys_PdfChapter SET ChapterTitle=N'" + SqlEsc(chapterTitle.Trim())
+ "' WHERE ChapterId='" + SqlEsc(chapterId) + "'");
}
/// <summary>
/// 删除章节及其全部子章节(递归收集自身+后代后一次性删除)。
/// </summary>
public static int DeleteChapter(string chapterId)
{
if (string.IsNullOrWhiteSpace(chapterId))
throw new Exception("chapterId 不能为空");
DataTable dt = SQLHelper.GetDataTableRunText(
"SELECT BookId FROM dbo.Sys_PdfChapter WHERE ChapterId=@ChapterId",
new[] { new SqlParameter("@ChapterId", chapterId) });
if (dt == null || dt.Rows.Count == 0)
throw new Exception("章节不存在:" + chapterId);
string bookId = dt.Rows[0]["BookId"].ToString();
var all = PdfBookService.GetChapters(bookId);
var ids = new List<string> { chapterId };
bool changed = true;
while (changed)
{
changed = false;
foreach (PdfChapter c in all)
{
if (!ids.Contains(c.ChapterId) && ids.Contains(c.ParentChapterId ?? ""))
{
ids.Add(c.ChapterId);
changed = true;
}
}
}
var sb = new StringBuilder();
foreach (string id in ids)
{
if (sb.Length > 0)
sb.Append(",");
sb.Append("'").Append(id.Replace("'", "''")).Append("'");
}
SQLHelper.ExecutSql("DELETE FROM dbo.Sys_PdfChapter WHERE ChapterId IN (" + sb + ")");
return ids.Count;
}
/// <summary>
/// 保存章节正文(富文本 HTML,与页面"保存内容"一致)。
/// </summary>
public static void SaveContent(string chapterId, string content)
{
if (string.IsNullOrWhiteSpace(chapterId))
throw new Exception("chapterId 不能为空");
if (GetInt("SELECT COUNT(*) FROM dbo.Sys_PdfChapter WHERE ChapterId='" + SqlEsc(chapterId) + "'") == 0)
throw new Exception("章节不存在:" + chapterId);
SQLHelper.ExecutSql("UPDATE dbo.Sys_PdfChapter SET [Text]=N'" + SqlEsc(content ?? "")
+ "' WHERE ChapterId='" + SqlEsc(chapterId) + "'");
}
#endregion
#region
/// <summary>SQL 字符串转义。</summary>
private static string SqlEsc(string s)
{
return (s ?? "").Replace("'", "''");
}
/// <summary>执行 SELECT 首行首列并转 int(查无返回0)。</summary>
private static int GetInt(string sql)
{
DataTable dt = SQLHelper.GetDataTableRunText(sql, new SqlParameter[] { });
if (dt == null || dt.Rows.Count == 0)
return 0;
object v = dt.Rows[0][0];
return v == DBNull.Value || v == null ? 0 : Convert.ToInt32(v);
}
/// <summary>生成业务主键(前缀+时间+随机,与页面一致)。</summary>
private static string NewId(string prefix)
{
return prefix + DateTime.Now.ToString("yyyyMMddHHmmss")
+ Guid.NewGuid().ToString("N").Substring(0, 4);
}
#endregion
}
}