Files
SGGL_HBAZ/SGGL/BLL/API/APIPdfBookService.cs
T
2026-09-11 11:14:08 +08:00

444 lines
20 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.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
}
}