feat: 第五轮开发 - 14项未来路线图功能完整实现
W1-W14 全部完成: - W1: 消息搜索 (ILIKE全文检索 + SearchModal) - W2: 对话导出 (JSON/Markdown/TXT三格式) - W3: 记忆时间线 DevTools 可视化 - W4: 通知推送系统 (WebSocket + Browser Notification API) - W5: 定时提醒 (30s轮询 + 重复提醒 + WebSocket推送) - W6: 每日简报 (08:00自动生成: 天气+新闻+提醒+AI摘要) - W7: IoT场景自动化 (规则引擎 10s轮询 + 条件评估 + 场景执行) - W8: 语音输入 (浏览器 Speech Recognition API) - W9: STT服务 (voice-service + whisper.cpp) - W10: TTS服务 (浏览器 Speech Synthesis + edge-tts三档回退) - W11: 文件管理 (上传/下载/缩略图/纯Go bilinear缩放) - W12: 知识库RAG (PostgreSQL tsvector + 文档分块 + 检索) - W13: 多模态 (图片上传+分析: Vision API + 本地Go分析回退) - W14: PWA (Service Worker + 离线页 + install prompt) 总计: 6个Go微服务 + 10+前端组件 + 10+ PostgreSQL表 + 4个后台调度器
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
// File 文件元数据模型
|
||||
type File struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
Filename string `json:"filename"`
|
||||
StoredPath string `json:"stored_path"`
|
||||
MimeType string `json:"mime_type"`
|
||||
Size int64 `json:"size"`
|
||||
Hash string `json:"hash"`
|
||||
IsPublic bool `json:"is_public"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// FileStore 文件元数据持久化存储
|
||||
type FileStore struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// NewFileStore 使用已有数据库连接初始化文件存储并自动建表
|
||||
func NewFileStore(db *sql.DB) (*FileStore, error) {
|
||||
store := &FileStore{db: db}
|
||||
|
||||
if err := store.migrate(); err != nil {
|
||||
return nil, fmt.Errorf("文件表迁移失败: %w", err)
|
||||
}
|
||||
|
||||
log.Println("[FileStore] 文件持久化存储已初始化")
|
||||
return store, nil
|
||||
}
|
||||
|
||||
// migrate 自动创建文件表结构
|
||||
func (s *FileStore) migrate() error {
|
||||
queries := []string{
|
||||
`CREATE TABLE IF NOT EXISTS files (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
user_id VARCHAR(255) NOT NULL,
|
||||
filename VARCHAR(500) NOT NULL,
|
||||
stored_path VARCHAR(1000) NOT NULL,
|
||||
mime_type VARCHAR(255) NOT NULL DEFAULT 'application/octet-stream',
|
||||
size BIGINT NOT NULL DEFAULT 0,
|
||||
hash VARCHAR(64) NOT NULL DEFAULT '',
|
||||
is_public BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_files_user_id ON files(user_id)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_files_hash ON files(hash)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_files_created_at ON files(user_id, created_at DESC)`,
|
||||
}
|
||||
|
||||
for _, q := range queries {
|
||||
if _, err := s.db.Exec(q); err != nil {
|
||||
return fmt.Errorf("迁移SQL执行失败: %w\nSQL: %s", err, q)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateFile 创建文件元数据记录
|
||||
func (s *FileStore) CreateFile(f *File) error {
|
||||
_, err := s.db.Exec(
|
||||
`INSERT INTO files (id, user_id, filename, stored_path, mime_type, size, hash, is_public, created_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
|
||||
f.ID, f.UserID, f.Filename, f.StoredPath, f.MimeType, f.Size, f.Hash, f.IsPublic, f.CreatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建文件记录失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetFile 根据ID获取文件元数据
|
||||
func (s *FileStore) GetFile(id string) (*File, error) {
|
||||
var f File
|
||||
err := s.db.QueryRow(
|
||||
`SELECT id, user_id, filename, stored_path, mime_type, size, hash, is_public, created_at
|
||||
FROM files WHERE id = $1`,
|
||||
id,
|
||||
).Scan(&f.ID, &f.UserID, &f.Filename, &f.StoredPath, &f.MimeType, &f.Size, &f.Hash, &f.IsPublic, &f.CreatedAt)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("查询文件失败: %w", err)
|
||||
}
|
||||
return &f, nil
|
||||
}
|
||||
|
||||
// GetUserFiles 获取用户的所有文件,支持分页
|
||||
func (s *FileStore) GetUserFiles(userID string, page, limit int) ([]File, int, error) {
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if limit <= 0 || limit > 100 {
|
||||
limit = 20
|
||||
}
|
||||
offset := (page - 1) * limit
|
||||
|
||||
// 获取总数
|
||||
var total int
|
||||
if err := s.db.QueryRow(
|
||||
`SELECT COUNT(*) FROM files WHERE user_id = $1`,
|
||||
userID,
|
||||
).Scan(&total); err != nil {
|
||||
return nil, 0, fmt.Errorf("查询文件总数失败: %w", err)
|
||||
}
|
||||
|
||||
// 分页查询
|
||||
rows, err := s.db.Query(
|
||||
`SELECT id, user_id, filename, stored_path, mime_type, size, hash, is_public, created_at
|
||||
FROM files WHERE user_id = $1
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $2 OFFSET $3`,
|
||||
userID, limit, offset,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("查询用户文件失败: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var files []File
|
||||
for rows.Next() {
|
||||
var f File
|
||||
if err := rows.Scan(&f.ID, &f.UserID, &f.Filename, &f.StoredPath, &f.MimeType, &f.Size, &f.Hash, &f.IsPublic, &f.CreatedAt); err != nil {
|
||||
return nil, 0, fmt.Errorf("扫描文件行失败: %w", err)
|
||||
}
|
||||
files = append(files, f)
|
||||
}
|
||||
|
||||
if files == nil {
|
||||
files = []File{}
|
||||
}
|
||||
return files, total, rows.Err()
|
||||
}
|
||||
|
||||
// GetFileByHash 根据SHA256哈希查找文件(用于去重)
|
||||
func (s *FileStore) GetFileByHash(hash string) (*File, error) {
|
||||
if hash == "" {
|
||||
return nil, nil
|
||||
}
|
||||
var f File
|
||||
err := s.db.QueryRow(
|
||||
`SELECT id, user_id, filename, stored_path, mime_type, size, hash, is_public, created_at
|
||||
FROM files WHERE hash = $1
|
||||
ORDER BY created_at ASC LIMIT 1`,
|
||||
hash,
|
||||
).Scan(&f.ID, &f.UserID, &f.Filename, &f.StoredPath, &f.MimeType, &f.Size, &f.Hash, &f.IsPublic, &f.CreatedAt)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("按哈希查询文件失败: %w", err)
|
||||
}
|
||||
return &f, nil
|
||||
}
|
||||
|
||||
// DeleteFile 删除文件元数据记录
|
||||
func (s *FileStore) DeleteFile(id string) error {
|
||||
_, err := s.db.Exec(`DELETE FROM files WHERE id = $1`, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("删除文件记录失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user