Files
wangairnan 5bac272f6d feat: 前台标签接入 antd 组件、列表封面图、favicon 随站点 Logo 及侧栏收窄
- 前台列表/详情/标签页标签统一 antd Tag;标签页改 CheckableTagGroup 单选
  (选中态由组件承接,再点已选标签回到全部);移除首页标签筛选条,
  标签入口收敛至顶部「标签」页
- 公开列表新增 cover 字段(取正文第一张站内图,store.FirstImageRef),
  首页条目左侧显示封面缩略图,无图条目保持纯文字
- favicon 随站点 Logo:webui.Meta.Icon 服务端注入 index.html,
  前端 useSiteTitle 兜底(dev 模板占位符场景)并即时生效;
  设置页「重置默认」清空 Logo 后回退内置 /favicon.svg
- 后台侧栏改 antd Menu(inline 分组),宽度 256→208、窄侧栏档 224→184
2026-09-09 11:51:06 +08:00

147 lines
4.2 KiB
Go

package store
import (
"database/sql"
"errors"
"regexp"
)
// Image 图片元信息(不含 BLOB)。
type Image struct {
ID int64 `json:"id"`
SHA256 string `json:"sha256"`
MIME string `json:"mime"`
Size int `json:"size"`
CreatedAt int64 `json:"created_at"`
}
// UpsertImage 按 sha256 去重入库,返回图片 id。
func (s *Store) UpsertImage(sha256Hex, mime string, size int, data []byte, createdAt int64) (int64, error) {
if _, err := s.db.Exec(
`INSERT OR IGNORE INTO images (sha256, mime, size, data, created_at) VALUES (?, ?, ?, ?, ?)`,
sha256Hex, mime, size, data, createdAt); err != nil {
return 0, err
}
var id int64
err := s.db.QueryRow(`SELECT id FROM images WHERE sha256=?`, sha256Hex).Scan(&id)
return id, err
}
// GetImageMeta 取图片元信息。
func (s *Store) GetImageMeta(id int64) (*Image, error) {
row := s.db.QueryRow(`SELECT id, sha256, mime, size, created_at FROM images WHERE id=?`, id)
var img Image
err := row.Scan(&img.ID, &img.SHA256, &img.MIME, &img.Size, &img.CreatedAt)
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
return &img, err
}
// GetImageData 取图片字节。
func (s *Store) GetImageData(id int64) ([]byte, error) {
var data []byte
err := s.db.QueryRow(`SELECT data FROM images WHERE id=?`, id).Scan(&data)
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
return data, err
}
// ImageIsPublic 并集语义(§6.2):任一引用笔记 public 且未删除 ⇒ 匿名可读。
func (s *Store) ImageIsPublic(id int64) (bool, error) {
var one int
err := s.db.QueryRow(`
SELECT 1 FROM image_refs r JOIN notes n ON n.id = r.note_id
WHERE r.image_id = ? AND n.status='public' AND n.deleted_at IS NULL
LIMIT 1`, id).Scan(&one)
if errors.Is(err, sql.ErrNoRows) {
return false, nil
}
return err == nil, err
}
// imageRefRe 扫描 content 中对 /api/images/{id} 的引用(§6.2 引用维护)。
var imageRefRe = regexp.MustCompile(`/api/images/(\d+)`)
// FirstImageRef 返回 content 中按出现顺序的第一张站内图片路径(无则空串)。
// 供公开列表推导封面图使用。
func FirstImageRef(content string) string {
m := imageRefRe.FindStringSubmatch(content)
if len(m) < 2 {
return ""
}
return m[0]
}
// RebuildImageRefs 在**同一事务**内重建笔记的图片引用:
// 删除该笔记全部 refs → 正则扫描 content → 重建(§6.2)。
func (s *Store) RebuildImageRefs(noteID int64, content string) error {
tx, err := s.db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
if err := rebuildRefsTx(tx, noteID, content); err != nil {
return err
}
return tx.Commit()
}
func rebuildRefsTx(tx *sql.Tx, noteID int64, content string) error {
if _, err := tx.Exec(`DELETE FROM image_refs WHERE note_id=?`, noteID); err != nil {
return err
}
seen := map[int64]bool{}
for _, m := range imageRefRe.FindAllStringSubmatch(content, -1) {
var imgID int64
if _, err := fmtSscanInt(m[1], &imgID); err != nil {
continue
}
if seen[imgID] {
continue
}
seen[imgID] = true
// 图片不存在时忽略(引用悬空无害)
if _, err := tx.Exec(`INSERT OR IGNORE INTO image_refs (image_id, note_id) VALUES (?, ?)`, imgID, noteID); err != nil {
return err
}
}
return nil
}
// ListOrphanImages 零引用图片清单(GET /api/admin/images?orphan=1 与 gc 检视用)。
// 站点 Logo 引用的图片虽无笔记引用仍受保护,不视为孤儿。
func (s *Store) ListOrphanImages() ([]Image, error) {
rows, err := s.db.Query(`
SELECT i.id, i.sha256, i.mime, i.size, i.created_at FROM images i
WHERE NOT EXISTS (SELECT 1 FROM image_refs r WHERE r.image_id = i.id)
AND i.id <> ?
ORDER BY i.created_at DESC`, s.logoImageID())
if err != nil {
return nil, err
}
defer rows.Close()
out := []Image{}
for rows.Next() {
var img Image
if err := rows.Scan(&img.ID, &img.SHA256, &img.MIME, &img.Size, &img.CreatedAt); err != nil {
return nil, err
}
out = append(out, img)
}
return out, rows.Err()
}
// DeleteImage 物理删除单张图片(refs 级联)。
func (s *Store) DeleteImage(id int64) error {
res, err := s.db.Exec(`DELETE FROM images WHERE id=?`, id)
if err != nil {
return err
}
if rows, _ := res.RowsAffected(); rows == 0 {
return ErrNotFound
}
return nil
}