Private
Public Access
1
0

新增:Markdown编辑器/数据库优化/安全修复

- Markdown 编辑器:实时预览、PDF 导出、独立查看器
- 数据库优化:动态连接池、查询缓存、Redis Pipeline
- 窗口置顶功能
- 文件系统增强:右键菜单、编辑器集成、收藏夹重构
- 安全修复:XSS 防护、路径穿越、HTML 注入
- 代码质量:正则预编译、缓存锁优化、死代码清理
This commit is contained in:
2026-03-31 09:18:06 +08:00
parent 5f94ccf13b
commit e5dbe89a6f
59 changed files with 5289 additions and 1316 deletions

379
internal/api/pdf_api.go Normal file
View File

@@ -0,0 +1,379 @@
package api
import (
"context"
"fmt"
"html"
"os"
"path/filepath"
"strings"
"time"
"github.com/chromedp/cdproto/page"
"github.com/chromedp/chromedp"
"github.com/yuin/goldmark"
"u-desk/internal/common"
)
// PdfExportRequest PDF导出请求结构体
type PdfExportRequest struct {
Content string `json:"content"` // Markdown/HTML内容
Title string `json:"title"` // PDF标题
FileName string `json:"fileName"` // 文件名(不含扩展名)
FontSize int `json:"fontSize"` // 字体大小
PageWidth int `json:"pageWidth"` // 页面宽度mm
PageHeight int `json:"pageHeight"` // 页面高度mm
}
// PdfExportResponse PDF导出响应结构体
type PdfExportResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
Path string `json:"path"` // PDF文件保存路径
Size int64 `json:"size"` // 文件大小(字节)
}
// PdfAPI PDF导出API
type PdfAPI struct {
// 可以在这里添加依赖,如文件系统服务等
}
// NewPdfAPI 创建PDF导出API
func NewPdfAPI() (*PdfAPI, error) {
return &PdfAPI{}, nil
}
// ExportMarkdownToPDF 将Markdown内容导出为PDF - 使用chromedp实现
func (api *PdfAPI) ExportMarkdownToPDF(req PdfExportRequest) (*PdfExportResponse, error) {
// 验证参数
if strings.TrimSpace(req.Content) == "" {
return nil, fmt.Errorf("内容不能为空")
}
if strings.TrimSpace(req.FileName) == "" {
req.FileName = "document_" + time.Now().Format("20060102_150405")
}
if req.FontSize <= 0 {
req.FontSize = 12
}
// 设置默认页面尺寸A4
if req.PageWidth <= 0 {
req.PageWidth = 210
}
if req.PageHeight <= 0 {
req.PageHeight = 297
}
// 将Markdown转换为HTML
htmlContent := api.markdownToHTML(req.Content, req.Title, req.FontSize)
// 使用chromedp生成PDF
pdfBuffer, err := api.generatePDFFromHTML(htmlContent, req.Title, req.PageWidth, req.PageHeight)
if err != nil {
return nil, fmt.Errorf("生成PDF失败: %v", err)
}
// 生成文件名
if !strings.HasSuffix(strings.ToLower(req.FileName), ".pdf") {
req.FileName += ".pdf"
}
// 获取用户桌面目录作为默认保存位置
saveDir := api.getDesktopDirectory()
// 确保目录存在
if err := os.MkdirAll(saveDir, 0755); err != nil {
return nil, fmt.Errorf("创建目录失败: %v", err)
}
// 完整保存路径
savePath := filepath.Join(saveDir, filepath.Base(req.FileName))
// 保存PDF文件
err = os.WriteFile(savePath, pdfBuffer, 0644)
if err != nil {
return nil, fmt.Errorf("保存PDF文件失败: %v", err)
}
// 获取文件信息
fileInfo, err := os.Stat(savePath)
if err != nil {
return nil, fmt.Errorf("获取文件信息失败: %v", err)
}
// 返回成功响应
return &PdfExportResponse{
Success: true,
Message: "PDF生成成功",
Path: savePath,
Size: fileInfo.Size(),
}, nil
}
// markdownToHTML 将Markdown转换为HTML
func (api *PdfAPI) markdownToHTML(markdownContent string, title string, fontSize int) string {
// 基础HTML模板
htmlTemplate := `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
line-height: 1.6;
color: #333;
max-width: 800px;
margin: 0 auto;
padding: 40px 20px;
font-size: %dpx;
}
h1, h2, h3, h4, h5, h6 {
margin-top: 24px;
margin-bottom: 16px;
font-weight: 600;
line-height: 1.25;
}
h1 {
font-size: 2em;
border-bottom: 1px solid #eaecef;
padding-bottom: 0.3em;
}
h2 {
font-size: 1.5em;
border-bottom: 1px solid #eaecef;
padding-bottom: 0.3em;
}
h3 {
font-size: 1.25em;
}
h4 {
font-size: 1em;
}
h5 {
font-size: 0.875em;
}
h6 {
font-size: 0.85em;
color: #6a737d;
}
p {
margin-bottom: 16px;
}
blockquote {
margin: 0 0 16px;
padding: 0 1em;
color: #6a737d;
border-left: 0.25em solid #dfe2e5;
}
ul, ol {
padding-left: 2em;
margin-bottom: 16px;
}
li {
margin-bottom: 4px;
}
code {
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
background-color: rgba(27,31,35,0.05);
border-radius: 3px;
font-size: 85%;
margin: 0;
padding: 0.2em 0.4em;
}
pre {
background-color: #f6f8fa;
border-radius: 3px;
padding: 16px;
overflow: auto;
margin-bottom: 16px;
}
pre code {
background-color: transparent;
padding: 0;
margin: 0;
font-size: 100%;
}
table {
border-collapse: collapse;
width: 100%;
margin-bottom: 16px;
border: 1px solid #dfe2e5;
}
th, td {
padding: 8px 12px;
border: 1px solid #dfe2e5;
text-align: left;
}
th {
background-color: #f6f8fa;
font-weight: 600;
}
img {
max-width: 100%;
height: auto;
margin: 16px 0;
}
hr {
height: 0.25em;
padding: 0;
margin: 24px 0;
background-color: #e1e4e8;
border: 0;
}
.title {
text-align: center;
margin-bottom: 32px;
font-size: 1.5em;
color: #2c3e50;
}
</style>
</head>
<body>
<div class="title">%s</div>
%s
</body>
</html>`
// 标题处理
docTitle := ""
if title != "" {
docTitle = html.EscapeString(title)
} else {
docTitle = "文档"
}
// Markdown转HTML使用goldmark
var htmlContent string
var htmlBuf strings.Builder
if err := goldmark.Convert([]byte(markdownContent), &htmlBuf); err != nil {
htmlContent = "<p>Markdown 解析失败</p>"
} else {
htmlContent = htmlBuf.String()
}
// 生成完整的HTML
fullHTML := fmt.Sprintf(htmlTemplate, fontSize, docTitle, htmlContent)
return fullHTML
}
// generatePDFFromHTML 使用chromedp从HTML生成PDF
func (api *PdfAPI) generatePDFFromHTML(htmlContent, title string, pageWidth, pageHeight int) ([]byte, error) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// 配置chromedp选项
opts := []chromedp.ExecAllocatorOption{
chromedp.Flag("headless", true),
chromedp.Flag("disable-gpu", true),
chromedp.Flag("no-sandbox", true),
chromedp.Flag("disable-dev-shm-usage", true),
chromedp.Flag("disable-software-rasterizer", true),
chromedp.Flag("disable-extensions", true),
chromedp.Flag("disable-notifications", true),
}
// 在Windows上设置Chrome路径
if common.IsWindows() {
// 常见的Windows Chrome路径
chromePaths := []string{
"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
"C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
"C:\\Users\\" + os.Getenv("USERNAME") + "\\AppData\\Local\\Google\\Chrome\\Application\\chrome.exe",
}
for _, path := range chromePaths {
if _, err := os.Stat(path); err == nil {
opts = append(opts, chromedp.ExecPath(path))
break
}
}
}
// 创建执行分配器上下文
allocCtx, allocCancel := chromedp.NewExecAllocator(ctx, opts...)
defer allocCancel()
// 创建chromedp上下文
chromeCtx, chromeCancel := chromedp.NewContext(allocCtx)
defer chromeCancel()
// 创建一个临时的目录用于PDF生成
tempDir, err := os.MkdirTemp("", "pdf_gen")
if err != nil {
return nil, fmt.Errorf("创建临时目录失败: %v", err)
}
defer os.RemoveAll(tempDir)
// 将HTML写入临时文件
htmlFile := filepath.Join(tempDir, "document.html")
if err := os.WriteFile(htmlFile, []byte(htmlContent), 0644); err != nil {
return nil, fmt.Errorf("写入HTML文件失败: %v", err)
}
var buf []byte
// 使用 file URL 加载本地HTML文件
err = chromedp.Run(chromeCtx,
// 导航到HTML文件
chromedp.Navigate("file://"+htmlFile),
// 等待页面加载完成
chromedp.WaitReady("body"),
// 打印到PDF
chromedp.ActionFunc(func(ctx context.Context) error {
// 设置页面打印参数
printToPDF := page.PrintToPDF().
WithPrintBackground(true).
WithLandscape(false).
WithMarginTop(0).
WithMarginBottom(0).
WithMarginLeft(0).
WithMarginRight(0).
WithPaperWidth(float64(pageWidth) / 25.4). // mm to inches
WithPaperHeight(float64(pageHeight) / 25.4) // mm to inches
// 执行打印并获取PDF数据
var err error
buf, _, err = printToPDF.Do(ctx)
return err
}),
)
if err != nil {
return nil, fmt.Errorf("chromedp执行失败: %v", err)
}
return buf, nil
}
// getDesktopDirectory 获取用户桌面目录
func (api *PdfAPI) getDesktopDirectory() string {
// Windows系统
if common.IsWindows() {
home := os.Getenv("USERPROFILE")
if home != "" {
return filepath.Join(home, "Desktop")
}
}
// Linux/Mac系统
home := os.Getenv("HOME")
if home != "" {
return filepath.Join(home, "Desktop")
}
// 备用:当前目录
return "."
}
// SelectDirectory 选择保存目录简化版实际应该使用Wails runtime
func (api *PdfAPI) SelectDirectory() (string, error) {
// 简化版:直接返回桌面目录
desktop := api.getDesktopDirectory()
if desktop == "." {
return "", fmt.Errorf("无法确定默认目录")
}
return desktop, nil
}