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 { /// /// PDF 书籍模块 WebAPI 数据服务(移动端 / 第三方系统集成用)。 /// 对应 PC 页面 HSSE/PDFDOC/PdfBookView.aspx 的完整功能:书籍、多级章节目录、章节正文(富文本 HTML)的 /// 查询与维护(新增书籍 / 新增·编辑·删除章节 / 保存章节正文),并额外提供章节关键词检索。 /// 数据表:Sys_PdfBook / Sys_PdfChapter(正文存 [Text] 列,为 UMeditor 生成的 HTML)。 /// 说明:目录结构用 ParentChapterId + Level(1章 2节 3小节) 表达多级层级,SortNo 控制同级顺序。 /// public static class APIPdfBookService { #region 返回模型 /// 书籍列表项。 public class BookItem { /// 书籍主键。 public string BookId { get; set; } /// 书名。 public string Title { get; set; } /// 章节总数。 public int ChapterCount { get; set; } /// 登记时间。 public DateTime? CreateTime { get; set; } } /// 章节目录树节点(多级嵌套)。 public class ChapterTreeNode { /// 章节主键。 public string ChapterId { get; set; } /// 章节名称。 public string ChapterTitle { get; set; } /// 层级:1章 2节 3小节。 public int Level { get; set; } /// 起始页码(无则为0)。 public int PageStart { get; set; } /// 结束页码(无则为0)。 public int PageEnd { get; set; } /// 是否已有正文内容(用于客户端标识可读章节)。 public bool HasContent { get; set; } /// 子章节列表(叶子章节为空数组)。 public List Children { get; set; } } /// 章节正文详情。 public class ChapterDetailItem { /// 章节主键。 public string ChapterId { get; set; } /// 所属书籍主键。 public string BookId { get; set; } /// 所属书籍名称。 public string BookTitle { get; set; } /// 章节名称。 public string ChapterTitle { get; set; } /// 父章节主键(顶级为 null)。 public string ParentChapterId { get; set; } /// 层级:1章 2节 3小节。 public int Level { get; set; } /// 起始页码(无则为0)。 public int PageStart { get; set; } /// 结束页码(无则为0)。 public int PageEnd { get; set; } /// 正文(富文本 HTML,可直接用 WebView 渲染)。 public string Content { get; set; } /// 正文纯文本(去掉 HTML 标签,便于全文展示/搜索)。 public string ContentText { get; set; } } /// 章节关键词检索命中项。 public class SearchHitItem { /// 书籍主键。 public string BookId { get; set; } /// 书籍名称。 public string BookTitle { get; set; } /// 章节主键。 public string ChapterId { get; set; } /// 章节名称。 public string ChapterTitle { get; set; } /// 起始页码(无则为0)。 public int PageStart { get; set; } /// 结束页码(无则为0)。 public int PageEnd { get; set; } /// 命中片段(关键词前后各约40字)。 public string Snippet { get; set; } } #endregion #region 查询:书籍列表 /// /// 获取全部书籍(按登记时间升序,与页面下拉一致)。 /// public static List GetBooks() { var list = new List(); 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 查询:多级章节目录树 /// /// 获取某本书的多级章节目录树(嵌套 Children,顶级为根的数组;按 SortNo 排序)。 /// public static List GetChapterTree(string bookId) { if (string.IsNullOrWhiteSpace(bookId)) throw new Exception("bookId 不能为空"); var chapters = PdfBookService.GetChapters(bookId); var result = new List(); if (chapters == null || chapters.Count == 0) return result; // 有正文标记(一次查询) var hasContentSet = new HashSet(); 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(); 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() }; } // 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 查询:章节正文 /// /// 获取章节正文详情(含 HTML 与纯文本);章节不存在返回 null。 /// 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 查询:章节关键词检索 /// /// 章节关键词检索:在章节正文中按关键词做字面匹配(正文为 HTML,匹配前先剔除标签)。 /// bookId 可空:不传则全库检索。 /// public static List 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 { 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(); 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; } /// 取关键词命中片段(前后各约40字)。 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 ? "……" : ""); } /// HTML 转纯文本(去标签、解码实体、压缩空白)。 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 维护:书籍 /// /// 新增书籍(仅书名;FilePath/TotalPages/IndexStatus 与页面一致:NULL/0/2)。 /// 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 维护:章节(新增 / 改名 / 删除 / 保存正文) /// /// 新增章节:parentChapterId 为空则为顶级章节,否则挂到指定父章节下(层级=父级+1)。 /// SortNo 取全书当前最大值+1(与页面逻辑一致)。 /// 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; } /// /// 编辑章节:仅修改章节名称。 /// 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) + "'"); } /// /// 删除章节及其全部子章节(递归收集自身+后代后一次性删除)。 /// 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 { 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; } /// /// 保存章节正文(富文本 HTML,与页面"保存内容"一致)。 /// 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 工具 /// SQL 字符串转义。 private static string SqlEsc(string s) { return (s ?? "").Replace("'", "''"); } /// 执行 SELECT 首行首列并转 int(查无返回0)。 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); } /// 生成业务主键(前缀+时间+随机,与页面一致)。 private static string NewId(string prefix) { return prefix + DateTime.Now.ToString("yyyyMMddHHmmss") + Guid.NewGuid().ToString("N").Substring(0, 4); } #endregion } }