新增:安全技术数据
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user