// Package repository 数据访问层,定义数据仓储接口和 SQLite 实现 package repository import ( "crypto/aes" "crypto/cipher" "crypto/rand" "crypto/sha256" "database/sql" "encoding/base64" "encoding/json" "errors" "io" "log" "os" "regexp" "resume-platform/internal/model" "time" "github.com/google/uuid" "golang.org/x/crypto/pbkdf2" _ "modernc.org/sqlite" ) // formatDateTime 统一日期时间格式为 "2006-01-02 15:04:05" // @param dt 原始日期字符串 // @return string 格式化后的日期时间 // @author sunct func formatDateTime(dt string) string { if dt == "" { return "" } formats := []string{ "2006-01-02 15:04:05", time.RFC3339, "2006-01-02T15:04:05Z07:00", "2006-01-02", } for _, f := range formats { if t, err := time.Parse(f, dt); err == nil { return t.Format("2006-01-02 15:04:05") } } re := regexp.MustCompile(`^(\d{4}-\d{2}-\d{2})\s*T?\s*(\d{2}:\d{2}:\d{2})`) if matches := re.FindStringSubmatch(dt); len(matches) >= 3 { return matches[1] + " " + matches[2] } return dt } // sqliteRepository SQLite 数据访问层实现 type sqliteRepository struct { db *sql.DB // SQLite 数据库连接 cipher cipher.AEAD // AES 加密器(字段加密用) enabled bool // 是否启用字段加密 } // NewSQLiteRepository 创建 SQLite 数据访问层实例,初始化数据库连接和表结构 // @param dbPath 数据库文件路径 // @param dbPassword 数据库加密密码(空则不加密) // @return ResumeRepository 仓储接口实例 // @return error 初始化错误 // @author sunct func NewSQLiteRepository(dbPath string, dbPassword string) (ResumeRepository, error) { db, err := sql.Open("sqlite", dbPath) if err != nil { return nil, err } db.SetMaxOpenConns(1) db.SetMaxIdleConns(1) repo := &sqliteRepository{db: db, enabled: false} if dbPassword != "" { key := deriveKey(dbPassword) block, err := aes.NewCipher(key) if err != nil { return nil, err } gcm, err := cipher.NewGCM(block) if err != nil { return nil, err } repo.cipher = gcm repo.enabled = true } _, err = db.Exec(`PRAGMA timezone = 'localtime';`) if err != nil { return nil, err } err = initSchema(db) if err != nil { return nil, err } err = repo.populateResumeNames() if err != nil { return nil, err } return repo, nil } // deriveKey 使用 PBKDF2 从密码派生 AES 密钥 // @param password 原始密码 // @return []byte 32字节密钥 // @author sunct func deriveKey(password string) []byte { salt := []byte("resume-platform-salt-2024") return pbkdf2.Key([]byte(password), salt, 4096, 32, sha256.New) } // encrypt 使用 AES-GCM 加密字符串 // @param data 明文 // @return string Base64编码的密文 // @return error 加密错误 // @author sunct func (r *sqliteRepository) encrypt(data string) (string, error) { if !r.enabled || r.cipher == nil { return data, nil } nonce := make([]byte, r.cipher.NonceSize()) if _, err := io.ReadFull(rand.Reader, nonce); err != nil { return "", err } ciphertext := r.cipher.Seal(nil, nonce, []byte(data), nil) return base64.StdEncoding.EncodeToString(append(nonce, ciphertext...)), nil } // decrypt 使用 AES-GCM 解密字符串 // @param data Base64编码的密文 // @return string 明文 // @return error 解密错误 // @author sunct func (r *sqliteRepository) decrypt(data string) (string, error) { if !r.enabled || r.cipher == nil { return data, nil } raw, err := base64.StdEncoding.DecodeString(data) if err != nil { return data, nil } nonceSize := r.cipher.NonceSize() if len(raw) < nonceSize { return data, nil } nonce, ciphertext := raw[:nonceSize], raw[nonceSize:] plaintext, err := r.cipher.Open(nil, nonce, ciphertext, nil) if err != nil { return data, nil } return string(plaintext), nil } // initSchema 初始化数据库表结构,包含简历、用户、工作经历、教育背景、技能、项目、作品集等表 // 同时处理数据库迁移,为旧表添加缺失的字段 // initSchema 初始化数据库表结构,创建所有业务表 // @param db 数据库连接 // @return error 执行错误 // @author sunct func initSchema(db *sql.DB) error { schema := ` CREATE TABLE IF NOT EXISTS resumes ( id TEXT PRIMARY KEY, user_id TEXT, route TEXT UNIQUE, password TEXT, basic_info TEXT, name TEXT, template TEXT DEFAULT 'modern', show_in_home INTEGER DEFAULT 1, created_at TEXT, updated_at TEXT ); CREATE TABLE IF NOT EXISTS users ( id TEXT PRIMARY KEY, username TEXT UNIQUE, password_hash TEXT, email TEXT, name TEXT, phone TEXT, role TEXT DEFAULT 'user', created_at TEXT, updated_at TEXT ); CREATE TABLE IF NOT EXISTS experiences ( id INTEGER PRIMARY KEY AUTOINCREMENT, resume_id TEXT, company TEXT, position TEXT, start_date TEXT, end_date TEXT, description TEXT, highlights TEXT, platforms TEXT ); CREATE TABLE IF NOT EXISTS educations ( id INTEGER PRIMARY KEY AUTOINCREMENT, resume_id TEXT, school TEXT, degree TEXT, major TEXT, start_date TEXT, end_date TEXT, gpa TEXT ); CREATE TABLE IF NOT EXISTS skills ( id INTEGER PRIMARY KEY AUTOINCREMENT, resume_id TEXT, name TEXT, level TEXT, category TEXT ); CREATE TABLE IF NOT EXISTS projects ( id INTEGER PRIMARY KEY AUTOINCREMENT, resume_id TEXT, name TEXT, description TEXT, tech_stack TEXT, url TEXT, highlights TEXT, achievements TEXT, start_date TEXT, end_date TEXT, show_in_resume INTEGER DEFAULT 1 ); CREATE TABLE IF NOT EXISTS portfolio_items ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id TEXT, name TEXT, description TEXT, tech_stack TEXT, url TEXT, start_date TEXT, end_date TEXT, doc TEXT, show_in_resume INTEGER DEFAULT 0, resume_id TEXT, project_id INTEGER DEFAULT 0, password TEXT, hidden INTEGER DEFAULT 0 ); CREATE TABLE IF NOT EXISTS quiz_records ( id TEXT PRIMARY KEY, resume_id TEXT, resume_route TEXT, title TEXT, questions TEXT, user_answers TEXT, score REAL, correct_count INTEGER, total_count INTEGER, created_at TEXT ); CREATE TABLE IF NOT EXISTS favorite_questions ( id TEXT PRIMARY KEY, user_id TEXT, resume_id TEXT, resume_route TEXT, question TEXT, user_answer TEXT, is_correct INTEGER, is_favorite INTEGER, created_at TEXT ); CREATE TABLE IF NOT EXISTS documents ( id TEXT PRIMARY KEY, user_id TEXT, name TEXT, type TEXT, size INTEGER, mime_type TEXT, content TEXT, status TEXT DEFAULT 'uploading', error_msg TEXT, created_at TEXT DEFAULT (datetime('now', 'localtime')), updated_at TEXT DEFAULT (datetime('now', 'localtime')) ); CREATE TABLE IF NOT EXISTS document_chunks ( id TEXT PRIMARY KEY, document_id TEXT, chunk_index INTEGER, content TEXT, embedding TEXT, metadata TEXT, created_at TEXT DEFAULT (datetime('now', 'localtime')), FOREIGN KEY (document_id) REFERENCES documents(id) ); CREATE TABLE IF NOT EXISTS embeddings ( id TEXT PRIMARY KEY, content_hash TEXT UNIQUE, embedding TEXT, source_type TEXT, source_id TEXT, source_name TEXT, user_id TEXT, created_at TEXT DEFAULT (datetime('now', 'localtime')) ); CREATE TABLE IF NOT EXISTS chat_history ( id TEXT PRIMARY KEY, user_id TEXT, question TEXT, answer TEXT, context TEXT, created_at TEXT DEFAULT (datetime('now', 'localtime')) ); CREATE TABLE IF NOT EXISTS menus ( id TEXT PRIMARY KEY, name TEXT NOT NULL, icon TEXT, path TEXT, parent_id TEXT, sort_order INTEGER DEFAULT 0, is_fixed INTEGER DEFAULT 0, is_enabled INTEGER DEFAULT 1, created_at TEXT DEFAULT (datetime('now', 'localtime')), updated_at TEXT DEFAULT (datetime('now', 'localtime')) ); CREATE TABLE IF NOT EXISTS system_config ( id TEXT PRIMARY KEY, website_domain TEXT, website_logo TEXT, logo_path TEXT, website_title TEXT, website_desc TEXT, website_description TEXT, website_keywords TEXT, admin_email TEXT, admin_name TEXT, seo_keywords TEXT, seo_description TEXT, database_path TEXT, admin_page_size INTEGER DEFAULT 20, created_at TEXT DEFAULT (datetime('now', 'localtime')), updated_at TEXT DEFAULT (datetime('now', 'localtime')) ); CREATE TABLE IF NOT EXISTS login_history ( id TEXT PRIMARY KEY, user_id TEXT, user_name TEXT, ip TEXT, location TEXT, user_agent TEXT, success INTEGER DEFAULT 0, message TEXT, created_at TEXT DEFAULT (datetime('now', 'localtime')) ); CREATE TABLE IF NOT EXISTS notifications ( id TEXT PRIMARY KEY, type TEXT NOT NULL DEFAULT 'system', title TEXT NOT NULL, message TEXT, url TEXT, is_read INTEGER DEFAULT 0, created_at TEXT DEFAULT (datetime('now', 'localtime')) ); ` if _, err := db.Exec(schema); err != nil { return err } return migrateSchema(db) } // migrateSchema 数据库迁移,为旧表添加缺失的字段 // migrateSchema 数据库迁移,新增字段和表(兼容旧版本) // @param db 数据库连接 // @return error 执行错误 // @author sunct func migrateSchema(db *sql.DB) error { var count int err := db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('resumes') WHERE name = 'route'").Scan(&count) if err != nil { return err } if count == 0 { if _, err := db.Exec("ALTER TABLE resumes ADD COLUMN route TEXT"); err != nil { return err } if _, err := db.Exec("UPDATE resumes SET route = id WHERE route IS NULL"); err != nil { return err } if _, err := db.Exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_resumes_route ON resumes(route)"); err != nil { return err } } err = db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('resumes') WHERE name = 'name'").Scan(&count) if err != nil { return err } if count == 0 { if _, err := db.Exec("ALTER TABLE resumes ADD COLUMN name TEXT"); err != nil { return err } } err = db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('resumes') WHERE name = 'show_in_home'").Scan(&count) if err != nil { return err } if count == 0 { if _, err := db.Exec("ALTER TABLE resumes ADD COLUMN show_in_home INTEGER DEFAULT 1"); err != nil { return err } if _, err := db.Exec("UPDATE resumes SET show_in_home = 1 WHERE show_in_home IS NULL"); err != nil { return err } } err = db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('resumes') WHERE name = 'password'").Scan(&count) if err != nil { return err } if count == 0 { if _, err := db.Exec("ALTER TABLE resumes ADD COLUMN password TEXT"); err != nil { return err } } err = db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('resumes') WHERE name = 'user_id'").Scan(&count) if err != nil { return err } if count == 0 { if _, err := db.Exec("ALTER TABLE resumes ADD COLUMN user_id TEXT"); err != nil { return err } } err = db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('users') WHERE name = 'name'").Scan(&count) if err != nil { return err } if count == 0 { if _, err := db.Exec("ALTER TABLE users ADD COLUMN name TEXT"); err != nil { return err } } err = db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('users') WHERE name = 'phone'").Scan(&count) if err != nil { return err } if count == 0 { if _, err := db.Exec("ALTER TABLE users ADD COLUMN phone TEXT"); err != nil { return err } } err = db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('users') WHERE name = 'resume_id'").Scan(&count) if err != nil { return err } if count > 0 { if _, err := db.Exec("ALTER TABLE users DROP COLUMN resume_id"); err != nil { if _, err2 := db.Exec("UPDATE users SET resume_id = NULL WHERE resume_id IS NOT NULL"); err2 != nil { return err2 } } } err = db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('portfolio_items') WHERE name = 'user_id'").Scan(&count) if err != nil { return err } if count == 0 { if _, err := db.Exec("ALTER TABLE portfolio_items ADD COLUMN user_id TEXT"); err != nil { return err } } err = db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('portfolio_items') WHERE name = 'password'").Scan(&count) if err != nil { return err } if count == 0 { if _, err := db.Exec("ALTER TABLE portfolio_items ADD COLUMN password TEXT"); err != nil { return err } } err = db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('portfolio_items') WHERE name = 'hidden'").Scan(&count) if err != nil { return err } if count == 0 { if _, err := db.Exec("ALTER TABLE portfolio_items ADD COLUMN hidden INTEGER DEFAULT 0"); err != nil { return err } } err = db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('portfolio_items') WHERE name = 'resume_id'").Scan(&count) if err != nil { return err } if count == 0 { if _, err := db.Exec("ALTER TABLE portfolio_items ADD COLUMN resume_id TEXT"); err != nil { return err } } err = db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('portfolio_items') WHERE name = 'project_id'").Scan(&count) if err != nil { return err } if count == 0 { if _, err := db.Exec("ALTER TABLE portfolio_items ADD COLUMN project_id INTEGER DEFAULT 0"); err != nil { return err } } err = db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('quiz_records') WHERE name = 'title'").Scan(&count) if err != nil { return err } if count == 0 { if _, err := db.Exec("ALTER TABLE quiz_records ADD COLUMN title TEXT"); err != nil { return err } } err = db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('quiz_records') WHERE name = 'user_id'").Scan(&count) if err != nil { return err } if count == 0 { if _, err := db.Exec("ALTER TABLE quiz_records ADD COLUMN user_id TEXT"); err != nil { return err } if _, err := db.Exec(`UPDATE quiz_records q SET user_id = ( SELECT r.user_id FROM resumes r WHERE r.id = q.resume_id OR r.route = q.resume_route LIMIT 1 ) WHERE q.user_id IS NULL`); err != nil { return err } } err = db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('system_config') WHERE name = 'logo_path'").Scan(&count) if err != nil { return err } if count == 0 { if _, err := db.Exec("ALTER TABLE system_config ADD COLUMN logo_path TEXT"); err != nil { return err } } err = db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('system_config') WHERE name = 'admin_page_size'").Scan(&count) if err != nil { return err } if count == 0 { if _, err := db.Exec("ALTER TABLE system_config ADD COLUMN admin_page_size INTEGER DEFAULT 20"); err != nil { return err } } err = db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('system_config') WHERE name = 'admin_email'").Scan(&count) if err != nil { return err } if count == 0 { if _, err := db.Exec("ALTER TABLE system_config ADD COLUMN admin_email TEXT"); err != nil { return err } } err = db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('system_config') WHERE name = 'admin_name'").Scan(&count) if err != nil { return err } if count == 0 { if _, err := db.Exec("ALTER TABLE system_config ADD COLUMN admin_name TEXT"); err != nil { return err } } err = db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('system_config') WHERE name = 'website_description'").Scan(&count) if err != nil { return err } if count == 0 { if _, err := db.Exec("ALTER TABLE system_config ADD COLUMN website_description TEXT"); err != nil { return err } } err = db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('system_config') WHERE name = 'seo_keywords'").Scan(&count) if err != nil { return err } if count == 0 { if _, err := db.Exec("ALTER TABLE system_config ADD COLUMN seo_keywords TEXT"); err != nil { return err } } err = db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('system_config') WHERE name = 'seo_description'").Scan(&count) if err != nil { return err } if count == 0 { if _, err := db.Exec("ALTER TABLE system_config ADD COLUMN seo_description TEXT"); err != nil { return err } } err = db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('favorite_questions') WHERE name = 'user_id'").Scan(&count) if err != nil { return err } if count == 0 { if _, err := db.Exec("ALTER TABLE favorite_questions ADD COLUMN user_id TEXT"); err != nil { return err } } return nil } // GetResume 根据ID获取简历,包含关联的工作经历、教育背景、技能和项目 // GetResume 根据ID获取简历,包含所有关联数据 // @param id 简历ID // @return *model.Resume 简历实体 // @return error 查询错误 // @author sunct func (r *sqliteRepository) GetResume(id string) (*model.Resume, error) { var resume model.Resume var basicInfoJSON, createdAt, updatedAt, password string var showInHome int err := r.db.QueryRow(` SELECT id, COALESCE(user_id, ''), route, COALESCE(password, ''), basic_info, template, show_in_home, created_at, updated_at FROM resumes WHERE id = ?`, id).Scan( &resume.ID, &resume.UserID, &resume.Route, &password, &basicInfoJSON, &resume.Template, &showInHome, &createdAt, &updatedAt) if err != nil { return nil, err } resume.Password, _ = r.decrypt(password) resume.CreatedAt = formatDateTime(createdAt) resume.UpdatedAt = formatDateTime(updatedAt) if basicInfoJSON != "" { decryptedInfo, _ := r.decrypt(basicInfoJSON) json.Unmarshal([]byte(decryptedInfo), &resume.BasicInfo) } resume.ShowInHome = showInHome == 1 resume.Experience, _ = r.getExperiences(id) resume.Education, _ = r.getEducations(id) resume.Skills, _ = r.getSkills(id) resume.Projects, _ = r.getProjects(id) return &resume, nil } // getExperiences 根据简历ID获取工作经历列表 func (r *sqliteRepository) getExperiences(resumeID string) ([]model.Experience, error) { rows, err := r.db.Query(` SELECT id, company, position, start_date, end_date, description, highlights, platforms FROM experiences WHERE resume_id = ?`, resumeID) if err != nil { return nil, err } defer rows.Close() var experiences []model.Experience for rows.Next() { var e model.Experience var highlightsJSON, platformsJSON string err := rows.Scan(&e.ID, &e.Company, &e.Position, &e.StartDate, &e.EndDate, &e.Description, &highlightsJSON, &platformsJSON) if err != nil { return nil, err } e.ResumeID = resumeID if highlightsJSON != "" { json.Unmarshal([]byte(highlightsJSON), &e.Highlights) } if platformsJSON != "" { json.Unmarshal([]byte(platformsJSON), &e.Platforms) } experiences = append(experiences, e) } return experiences, nil } // getEducations 根据简历ID获取教育背景列表 func (r *sqliteRepository) getEducations(resumeID string) ([]model.Education, error) { rows, err := r.db.Query(` SELECT id, school, degree, major, start_date, end_date, gpa FROM educations WHERE resume_id = ?`, resumeID) if err != nil { return nil, err } defer rows.Close() var educations []model.Education for rows.Next() { var e model.Education err := rows.Scan(&e.ID, &e.School, &e.Degree, &e.Major, &e.StartDate, &e.EndDate, &e.GPA) if err != nil { return nil, err } e.ResumeID = resumeID educations = append(educations, e) } return educations, nil } // getSkills 根据简历ID获取技能列表 func (r *sqliteRepository) getSkills(resumeID string) ([]model.Skill, error) { rows, err := r.db.Query(` SELECT id, name, level, category FROM skills WHERE resume_id = ?`, resumeID) if err != nil { return nil, err } defer rows.Close() var skills []model.Skill for rows.Next() { var s model.Skill err := rows.Scan(&s.ID, &s.Name, &s.Level, &s.Category) if err != nil { return nil, err } s.ResumeID = resumeID skills = append(skills, s) } return skills, nil } // getProjects 根据简历ID获取项目列表 func (r *sqliteRepository) getProjects(resumeID string) ([]model.Project, error) { rows, err := r.db.Query(` SELECT id, name, description, tech_stack, url, highlights, achievements, start_date, end_date, show_in_resume FROM projects WHERE resume_id = ?`, resumeID) if err != nil { return nil, err } defer rows.Close() var projects []model.Project for rows.Next() { var p model.Project var techStackJSON, highlightsJSON, achievementsJSON string var showInResume int err := rows.Scan(&p.ID, &p.Name, &p.Description, &techStackJSON, &p.URL, &highlightsJSON, &achievementsJSON, &p.StartDate, &p.EndDate, &showInResume) if err != nil { return nil, err } p.ResumeID = resumeID p.ShowInResume = showInResume == 1 if techStackJSON != "" { json.Unmarshal([]byte(techStackJSON), &p.TechStack) } if highlightsJSON != "" { json.Unmarshal([]byte(highlightsJSON), &p.Highlights) } if achievementsJSON != "" { json.Unmarshal([]byte(achievementsJSON), &p.Achievements) } projects = append(projects, p) } return projects, nil } // CreateResume 创建简历,包含所有关联数据,使用事务保证数据一致性 func (r *sqliteRepository) CreateResume(resume *model.Resume) error { tx, err := r.db.Begin() if err != nil { return err } basicInfoJSON, _ := json.Marshal(resume.BasicInfo) encryptedInfo, _ := r.encrypt(string(basicInfoJSON)) encryptedPassword, _ := r.encrypt(resume.Password) showInHome := 0 if resume.ShowInHome { showInHome = 1 } name := "" if resume.BasicInfo.Name != "" { name = resume.BasicInfo.Name } _, err = tx.Exec(` INSERT INTO resumes (id, user_id, route, password, basic_info, name, template, show_in_home, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now', 'localtime'), datetime('now', 'localtime'))`, resume.ID, resume.UserID, resume.Route, encryptedPassword, encryptedInfo, name, resume.Template, showInHome) if err != nil { tx.Rollback() return err } for _, e := range resume.Experience { highlightsJSON, _ := json.Marshal(e.Highlights) platformsJSON, _ := json.Marshal(e.Platforms) _, err = tx.Exec(` INSERT INTO experiences (resume_id, company, position, start_date, end_date, description, highlights, platforms) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, resume.ID, e.Company, e.Position, e.StartDate, e.EndDate, e.Description, string(highlightsJSON), string(platformsJSON)) if err != nil { tx.Rollback() return err } } for _, e := range resume.Education { _, err = tx.Exec(` INSERT INTO educations (resume_id, school, degree, major, start_date, end_date, gpa) VALUES (?, ?, ?, ?, ?, ?, ?)`, resume.ID, e.School, e.Degree, e.Major, e.StartDate, e.EndDate, e.GPA) if err != nil { tx.Rollback() return err } } for _, s := range resume.Skills { _, err = tx.Exec(` INSERT INTO skills (resume_id, name, level, category) VALUES (?, ?, ?, ?)`, resume.ID, s.Name, s.Level, s.Category) if err != nil { tx.Rollback() return err } } for _, p := range resume.Projects { techStackJSON, _ := json.Marshal(p.TechStack) highlightsJSON, _ := json.Marshal(p.Highlights) achievementsJSON, _ := json.Marshal(p.Achievements) showInResume := 0 if p.ShowInResume { showInResume = 1 } _, err = tx.Exec(` INSERT INTO projects (resume_id, name, description, tech_stack, url, highlights, achievements, start_date, end_date, show_in_resume) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, resume.ID, p.Name, p.Description, string(techStackJSON), p.URL, string(highlightsJSON), string(achievementsJSON), p.StartDate, p.EndDate, showInResume) if err != nil { tx.Rollback() return err } } return tx.Commit() } // UpdateResume 更新简历,支持部分字段更新,使用事务保证数据一致性 func (r *sqliteRepository) UpdateResume(id string, update *model.ResumeUpdate) error { tx, err := r.db.Begin() if err != nil { return err } if update.BasicInfo != nil { basicInfoJSON, _ := json.Marshal(*update.BasicInfo) encryptedInfo, _ := r.encrypt(string(basicInfoJSON)) name := update.BasicInfo.Name _, err = tx.Exec(`UPDATE resumes SET basic_info = ?, name = ?, updated_at = datetime('now', 'localtime') WHERE id = ?`, encryptedInfo, name, id) if err != nil { tx.Rollback() return err } } if update.Template != nil { _, err = tx.Exec(`UPDATE resumes SET template = ?, updated_at = datetime('now', 'localtime') WHERE id = ?`, *update.Template, id) if err != nil { tx.Rollback() return err } } if update.ShowInHome != nil { showInHome := 0 if *update.ShowInHome { showInHome = 1 } _, err = tx.Exec(`UPDATE resumes SET show_in_home = ?, updated_at = datetime('now', 'localtime') WHERE id = ?`, showInHome, id) if err != nil { tx.Rollback() return err } } if update.Route != nil { _, err = tx.Exec(`UPDATE resumes SET route = ?, updated_at = datetime('now', 'localtime') WHERE id = ?`, *update.Route, id) if err != nil { tx.Rollback() return err } } if update.Password != nil { encryptedPassword, _ := r.encrypt(*update.Password) _, err = tx.Exec(`UPDATE resumes SET password = ?, updated_at = datetime('now', 'localtime') WHERE id = ?`, encryptedPassword, id) if err != nil { tx.Rollback() return err } } if update.Experience != nil { _, err = tx.Exec(`DELETE FROM experiences WHERE resume_id = ?`, id) if err != nil { tx.Rollback() return err } for _, e := range *update.Experience { highlightsJSON, _ := json.Marshal(e.Highlights) platformsJSON, _ := json.Marshal(e.Platforms) _, err = tx.Exec(` INSERT INTO experiences (resume_id, company, position, start_date, end_date, description, highlights, platforms) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, id, e.Company, e.Position, e.StartDate, e.EndDate, e.Description, string(highlightsJSON), string(platformsJSON)) if err != nil { tx.Rollback() return err } } } if update.Education != nil { _, err = tx.Exec(`DELETE FROM educations WHERE resume_id = ?`, id) if err != nil { tx.Rollback() return err } for _, e := range *update.Education { _, err = tx.Exec(` INSERT INTO educations (resume_id, school, degree, major, start_date, end_date, gpa) VALUES (?, ?, ?, ?, ?, ?, ?)`, id, e.School, e.Degree, e.Major, e.StartDate, e.EndDate, e.GPA) if err != nil { tx.Rollback() return err } } } if update.Skills != nil { _, err = tx.Exec(`DELETE FROM skills WHERE resume_id = ?`, id) if err != nil { tx.Rollback() return err } for _, s := range *update.Skills { _, err = tx.Exec(` INSERT INTO skills (resume_id, name, level, category) VALUES (?, ?, ?, ?)`, id, s.Name, s.Level, s.Category) if err != nil { tx.Rollback() return err } } } if update.Projects != nil { _, err = tx.Exec(`DELETE FROM projects WHERE resume_id = ?`, id) if err != nil { tx.Rollback() return err } for _, p := range *update.Projects { techStackJSON, _ := json.Marshal(p.TechStack) highlightsJSON, _ := json.Marshal(p.Highlights) achievementsJSON, _ := json.Marshal(p.Achievements) showInResume := 0 if p.ShowInResume { showInResume = 1 } _, err = tx.Exec(` INSERT INTO projects (resume_id, name, description, tech_stack, url, highlights, achievements, start_date, end_date, show_in_resume) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, id, p.Name, p.Description, string(techStackJSON), p.URL, string(highlightsJSON), string(achievementsJSON), p.StartDate, p.EndDate, showInResume) if err != nil { tx.Rollback() return err } } } return tx.Commit() } // DeleteResume 删除简历及其所有关联数据,使用事务保证数据一致性 func (r *sqliteRepository) DeleteResume(id string) error { tx, err := r.db.Begin() if err != nil { return err } _, err = tx.Exec(`DELETE FROM experiences WHERE resume_id = ?`, id) if err != nil { tx.Rollback() return err } _, err = tx.Exec(`DELETE FROM educations WHERE resume_id = ?`, id) if err != nil { tx.Rollback() return err } _, err = tx.Exec(`DELETE FROM skills WHERE resume_id = ?`, id) if err != nil { tx.Rollback() return err } _, err = tx.Exec(`DELETE FROM projects WHERE resume_id = ?`, id) if err != nil { tx.Rollback() return err } _, err = tx.Exec(`DELETE FROM resumes WHERE id = ?`, id) if err != nil { tx.Rollback() return err } return tx.Commit() } // UpdateResumeUserID 更新简历的用户绑定 func (r *sqliteRepository) UpdateResumeUserID(resumeID, userID string) error { _, err := r.db.Exec(`UPDATE resumes SET user_id = ?, updated_at = datetime('now', 'localtime') WHERE id = ?`, userID, resumeID) return err } // GetResumeByRoute 根据访问路由获取简历 func (r *sqliteRepository) GetResumeByRoute(route string) (*model.Resume, error) { var resume model.Resume var basicInfoJSON, createdAt, updatedAt, password string var showInHome int err := r.db.QueryRow(` SELECT id, COALESCE(user_id, ''), route, COALESCE(password, ''), basic_info, template, show_in_home, created_at, updated_at FROM resumes WHERE route = ?`, route).Scan( &resume.ID, &resume.UserID, &resume.Route, &password, &basicInfoJSON, &resume.Template, &showInHome, &createdAt, &updatedAt) if err != nil { if err == sql.ErrNoRows { err2 := r.db.QueryRow(` SELECT id, COALESCE(user_id, ''), route, COALESCE(password, ''), basic_info, template, show_in_home, created_at, updated_at FROM resumes WHERE id = ?`, route).Scan( &resume.ID, &resume.UserID, &resume.Route, &password, &basicInfoJSON, &resume.Template, &showInHome, &createdAt, &updatedAt) if err2 != nil { return nil, err2 } } else { return nil, err } } resume.Password, _ = r.decrypt(password) resume.CreatedAt = formatDateTime(createdAt) resume.UpdatedAt = formatDateTime(updatedAt) if basicInfoJSON != "" { decryptedInfo, _ := r.decrypt(basicInfoJSON) json.Unmarshal([]byte(decryptedInfo), &resume.BasicInfo) } resume.ShowInHome = showInHome == 1 resume.Experience, _ = r.getExperiences(resume.ID) resume.Education, _ = r.getEducations(resume.ID) resume.Skills, _ = r.getSkills(resume.ID) resume.Projects, _ = r.getProjects(resume.ID) return &resume, nil } // RouteExists 检查路由是否已存在 func (r *sqliteRepository) RouteExists(route string) bool { var count int r.db.QueryRow(`SELECT COUNT(*) FROM resumes WHERE route = ?`, route).Scan(&count) return count > 0 } // GetAllResumes 获取所有简历列表(不包含详细关联数据) func (r *sqliteRepository) GetAllResumes() ([]*model.Resume, error) { rows, err := r.db.Query(`SELECT id, COALESCE(user_id, ''), route, basic_info, template, show_in_home, created_at, updated_at FROM resumes`) if err != nil { return nil, err } defer rows.Close() var resumes []*model.Resume for rows.Next() { var resume model.Resume var basicInfoJSON, createdAt, updatedAt string var showInHome int err := rows.Scan(&resume.ID, &resume.UserID, &resume.Route, &basicInfoJSON, &resume.Template, &showInHome, &createdAt, &updatedAt) if err != nil { return nil, err } if basicInfoJSON != "" { decryptedInfo, _ := r.decrypt(basicInfoJSON) json.Unmarshal([]byte(decryptedInfo), &resume.BasicInfo) } resume.ShowInHome = showInHome == 1 resume.CreatedAt = formatDateTime(createdAt) resume.UpdatedAt = formatDateTime(updatedAt) resumes = append(resumes, &resume) } return resumes, nil } // GetResumesWithPagination 获取简历列表(分页) func (r *sqliteRepository) GetResumesWithPagination(page, pageSize int) ([]*model.Resume, int, error) { return r.GetResumesWithFilter(page, pageSize, "", "", "", nil) } // GetResumesWithFilter 带筛选条件的简历分页查询 func (r *sqliteRepository) GetResumesWithFilter(page, pageSize int, userID, keyword, template string, showInHome *bool) ([]*model.Resume, int, error) { offset := (page - 1) * pageSize where := "WHERE 1=1" args := []interface{}{} if userID != "" { where += " AND user_id = ?" args = append(args, userID) } if keyword != "" { where += " AND (route LIKE ? OR name LIKE ?)" kw := "%" + keyword + "%" args = append(args, kw, kw) } if template != "" { where += " AND template = ?" args = append(args, template) } if showInHome != nil { if *showInHome { where += " AND show_in_home = 1" } else { where += " AND show_in_home = 0" } } var total int countArgs := make([]interface{}, len(args)) copy(countArgs, args) err := r.db.QueryRow("SELECT COUNT(*) FROM resumes "+where, countArgs...).Scan(&total) if err != nil { return nil, 0, err } queryArgs := append(args, pageSize, offset) rows, err := r.db.Query("SELECT id, COALESCE(user_id, ''), route, basic_info, template, show_in_home, created_at, updated_at FROM resumes "+where+" ORDER BY updated_at DESC LIMIT ? OFFSET ?", queryArgs...) if err != nil { return nil, 0, err } defer rows.Close() var resumes []*model.Resume for rows.Next() { var resume model.Resume var basicInfoJSON, createdAt, updatedAt string var showInHome int err := rows.Scan(&resume.ID, &resume.UserID, &resume.Route, &basicInfoJSON, &resume.Template, &showInHome, &createdAt, &updatedAt) if err != nil { return nil, 0, err } if basicInfoJSON != "" { decryptedInfo, _ := r.decrypt(basicInfoJSON) json.Unmarshal([]byte(decryptedInfo), &resume.BasicInfo) } resume.ShowInHome = showInHome == 1 resume.CreatedAt = formatDateTime(createdAt) resume.UpdatedAt = formatDateTime(updatedAt) resumes = append(resumes, &resume) } return resumes, total, nil } // CreateUser 创建用户 func (r *sqliteRepository) CreateUser(user *model.User) error { _, err := r.db.Exec(` INSERT INTO users (id, username, password_hash, email, name, phone, role, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now', 'localtime'), datetime('now', 'localtime'))`, user.ID, user.Username, user.PasswordHash, user.Email, user.Name, user.Phone, user.Role) return err } // GetUserByUsername 根据用户名获取用户 func (r *sqliteRepository) GetUserByUsername(username string) (*model.User, error) { var user model.User var createdAt, updatedAt string err := r.db.QueryRow(` SELECT id, username, password_hash, email, name, phone, role, created_at, updated_at FROM users WHERE username = ?`, username).Scan( &user.ID, &user.Username, &user.PasswordHash, &user.Email, &user.Name, &user.Phone, &user.Role, &createdAt, &updatedAt) if err != nil { return nil, err } user.CreatedAt = formatDateTime(createdAt) user.UpdatedAt = formatDateTime(updatedAt) return &user, nil } // GetUserByID 根据ID获取用户 func (r *sqliteRepository) GetUserByID(id string) (*model.User, error) { var user model.User var createdAt, updatedAt string err := r.db.QueryRow(` SELECT id, username, password_hash, email, name, phone, role, created_at, updated_at FROM users WHERE id = ?`, id).Scan( &user.ID, &user.Username, &user.PasswordHash, &user.Email, &user.Name, &user.Phone, &user.Role, &createdAt, &updatedAt) if err != nil { return nil, err } user.CreatedAt = formatDateTime(createdAt) user.UpdatedAt = formatDateTime(updatedAt) return &user, nil } // GetAllUsers 获取所有用户列表 func (r *sqliteRepository) GetAllUsers() ([]*model.User, error) { rows, err := r.db.Query(`SELECT id, username, email, name, phone, role, created_at FROM users ORDER BY created_at DESC`) if err != nil { return nil, err } defer rows.Close() var users []*model.User for rows.Next() { var user model.User var createdAt string err := rows.Scan(&user.ID, &user.Username, &user.Email, &user.Name, &user.Phone, &user.Role, &createdAt) if err != nil { return nil, err } user.CreatedAt = formatDateTime(createdAt) users = append(users, &user) } return users, nil } // GetUsersWithPagination 获取用户列表(分页) func (r *sqliteRepository) GetUsersWithPagination(page, pageSize int) ([]*model.User, int, error) { offset := (page - 1) * pageSize var total int err := r.db.QueryRow(`SELECT COUNT(*) FROM users`).Scan(&total) if err != nil { return nil, 0, err } rows, err := r.db.Query(`SELECT id, username, email, name, phone, role, created_at FROM users ORDER BY created_at DESC LIMIT ? OFFSET ?`, pageSize, offset) if err != nil { return nil, 0, err } defer rows.Close() var users []*model.User for rows.Next() { var user model.User var createdAt string err := rows.Scan(&user.ID, &user.Username, &user.Email, &user.Name, &user.Phone, &user.Role, &createdAt) if err != nil { return nil, 0, err } user.CreatedAt = formatDateTime(createdAt) users = append(users, &user) } return users, total, nil } // UpdateUser 更新用户信息 func (r *sqliteRepository) UpdateUser(id string, user *model.User) error { // 若未传入 password_hash 或 role,则保留原值,避免被空字符串覆盖 if user.PasswordHash == "" || user.Role == "" { var existingPwd, existingRole string err := r.db.QueryRow("SELECT password_hash, role FROM users WHERE id = ?", id).Scan(&existingPwd, &existingRole) if err == nil { if user.PasswordHash == "" { user.PasswordHash = existingPwd } if user.Role == "" { user.Role = existingRole } } } _, err := r.db.Exec(` UPDATE users SET username = ?, password_hash = ?, email = ?, name = ?, phone = ?, role = ?, updated_at = datetime('now', 'localtime') WHERE id = ?`, user.Username, user.PasswordHash, user.Email, user.Name, user.Phone, user.Role, id) return err } // DeleteUser 删除用户 func (r *sqliteRepository) DeleteUser(id string) error { _, err := r.db.Exec(`DELETE FROM users WHERE id = ?`, id) return err } // GetResumesByUserID 根据用户ID获取简历列表 func (r *sqliteRepository) GetResumesByUserID(userID string) ([]*model.Resume, error) { rows, err := r.db.Query(`SELECT id, COALESCE(user_id, ''), route, basic_info, template, show_in_home, created_at, updated_at FROM resumes WHERE user_id = ?`, userID) if err != nil { return nil, err } defer rows.Close() var resumes []*model.Resume for rows.Next() { var resume model.Resume var basicInfoJSON, createdAt, updatedAt string var showInHome int err := rows.Scan(&resume.ID, &resume.UserID, &resume.Route, &basicInfoJSON, &resume.Template, &showInHome, &createdAt, &updatedAt) if err != nil { return nil, err } if basicInfoJSON != "" { decryptedInfo, _ := r.decrypt(basicInfoJSON) json.Unmarshal([]byte(decryptedInfo), &resume.BasicInfo) } resume.ShowInHome = showInHome == 1 resume.CreatedAt = formatDateTime(createdAt) resume.UpdatedAt = formatDateTime(updatedAt) resumes = append(resumes, &resume) } return resumes, nil } // GetPortfolioItemsByUserID 根据用户ID获取作品集列表 func (r *sqliteRepository) GetPortfolioItemsByUserID(userID string) ([]*model.PortfolioItem, error) { rows, err := r.db.Query(` SELECT id, user_id, name, description, tech_stack, url, start_date, end_date, doc, show_in_resume, resume_id, project_id, password, hidden FROM portfolio_items WHERE user_id = ? ORDER BY id DESC`, userID) if err != nil { return nil, err } defer rows.Close() var items []*model.PortfolioItem for rows.Next() { var item model.PortfolioItem var techStackJSON string var showInResume, hidden int err := rows.Scan(&item.ID, &item.UserID, &item.Name, &item.Description, &techStackJSON, &item.URL, &item.StartDate, &item.EndDate, &item.Doc, &showInResume, &item.ResumeID, &item.ProjectID, &item.Password, &hidden) if err != nil { return nil, err } item.ShowInResume = showInResume == 1 item.Hidden = hidden == 1 if techStackJSON != "" { json.Unmarshal([]byte(techStackJSON), &item.TechStack) } items = append(items, &item) } return items, nil } // Exists 检查简历是否存在 func (r *sqliteRepository) Exists(id string) bool { var count int r.db.QueryRow(`SELECT COUNT(*) FROM resumes WHERE id = ?`, id).Scan(&count) return count > 0 } // GetPortfolioItems 获取所有作品集项目列表 func (r *sqliteRepository) GetPortfolioItems() ([]*model.PortfolioItem, error) { rows, err := r.db.Query(` SELECT id, user_id, name, description, tech_stack, url, start_date, end_date, doc, show_in_resume, resume_id, project_id, password, hidden FROM portfolio_items ORDER BY id DESC`) if err != nil { return nil, err } defer rows.Close() var items []*model.PortfolioItem for rows.Next() { var item model.PortfolioItem var techStackJSON string var showInResume, hidden int err := rows.Scan(&item.ID, &item.UserID, &item.Name, &item.Description, &techStackJSON, &item.URL, &item.StartDate, &item.EndDate, &item.Doc, &showInResume, &item.ResumeID, &item.ProjectID, &item.Password, &hidden) if err != nil { return nil, err } item.ShowInResume = showInResume == 1 item.Hidden = hidden == 1 if techStackJSON != "" { json.Unmarshal([]byte(techStackJSON), &item.TechStack) } items = append(items, &item) } return items, nil } // GetPortfolioItemsWithPagination 获取作品集项目列表(分页) func (r *sqliteRepository) GetPortfolioItemsWithPagination(page, pageSize int) ([]*model.PortfolioItem, int, error) { offset := (page - 1) * pageSize var total int err := r.db.QueryRow(`SELECT COUNT(*) FROM portfolio_items`).Scan(&total) if err != nil { return nil, 0, err } rows, err := r.db.Query(` SELECT id, user_id, name, description, tech_stack, url, start_date, end_date, doc, show_in_resume, resume_id, project_id, password, hidden FROM portfolio_items ORDER BY id DESC LIMIT ? OFFSET ?`, pageSize, offset) if err != nil { return nil, 0, err } defer rows.Close() var items []*model.PortfolioItem for rows.Next() { var item model.PortfolioItem var techStackJSON string var showInResume, hidden int err := rows.Scan(&item.ID, &item.UserID, &item.Name, &item.Description, &techStackJSON, &item.URL, &item.StartDate, &item.EndDate, &item.Doc, &showInResume, &item.ResumeID, &item.ProjectID, &item.Password, &hidden) if err != nil { return nil, 0, err } item.ShowInResume = showInResume == 1 item.Hidden = hidden == 1 if techStackJSON != "" { json.Unmarshal([]byte(techStackJSON), &item.TechStack) } items = append(items, &item) } return items, total, nil } // GetPortfolioItem 根据ID获取单个作品集项目 func (r *sqliteRepository) GetPortfolioItem(id uint) (*model.PortfolioItem, error) { var item model.PortfolioItem var techStackJSON string var showInResume, hidden int err := r.db.QueryRow(` SELECT id, user_id, name, description, tech_stack, url, start_date, end_date, doc, show_in_resume, resume_id, project_id, password, hidden FROM portfolio_items WHERE id = ?`, id).Scan( &item.ID, &item.UserID, &item.Name, &item.Description, &techStackJSON, &item.URL, &item.StartDate, &item.EndDate, &item.Doc, &showInResume, &item.ResumeID, &item.ProjectID, &item.Password, &hidden) if err != nil { return nil, err } item.ShowInResume = showInResume == 1 item.Hidden = hidden == 1 if techStackJSON != "" { json.Unmarshal([]byte(techStackJSON), &item.TechStack) } return &item, nil } // CreatePortfolioItem 创建作品集项目 func (r *sqliteRepository) CreatePortfolioItem(item *model.PortfolioItem) error { techStackJSON, _ := json.Marshal(item.TechStack) showInResume := 0 if item.ShowInResume { showInResume = 1 } hidden := 0 if item.Hidden { hidden = 1 } _, err := r.db.Exec(` INSERT INTO portfolio_items (user_id, name, description, tech_stack, url, start_date, end_date, doc, show_in_resume, resume_id, project_id, password, hidden) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, item.UserID, item.Name, item.Description, string(techStackJSON), item.URL, item.StartDate, item.EndDate, item.Doc, showInResume, item.ResumeID, item.ProjectID, item.Password, hidden) return err } // UpdatePortfolioItem 更新作品集项目 func (r *sqliteRepository) UpdatePortfolioItem(id uint, item *model.PortfolioItem) error { techStackJSON, _ := json.Marshal(item.TechStack) showInResume := 0 if item.ShowInResume { showInResume = 1 } hidden := 0 if item.Hidden { hidden = 1 } _, err := r.db.Exec(` UPDATE portfolio_items SET user_id = ?, name = ?, description = ?, tech_stack = ?, url = ?, start_date = ?, end_date = ?, doc = ?, show_in_resume = ?, resume_id = ?, project_id = ?, password = ?, hidden = ? WHERE id = ?`, item.UserID, item.Name, item.Description, string(techStackJSON), item.URL, item.StartDate, item.EndDate, item.Doc, showInResume, item.ResumeID, item.ProjectID, item.Password, hidden, id) return err } // DeletePortfolioItem 删除作品集项目 func (r *sqliteRepository) DeletePortfolioItem(id uint) error { _, err := r.db.Exec(`DELETE FROM portfolio_items WHERE id = ?`, id) return err } // CreateQuizRecord 创建面试题历史记录 func (r *sqliteRepository) CreateQuizRecord(record *model.QuizRecord) error { questionsJSON, _ := json.Marshal(record.Questions) userAnswersJSON, _ := json.Marshal(record.UserAnswers) log.Printf("[DEBUG] CreateQuizRecord called with id: %s, resume_route: %s, resume_id: %s, title: %s, score: %f, correct_count: %d, total_count: %d", record.ID, record.ResumeRoute, record.ResumeID, record.Title, record.Score, record.CorrectCount, record.TotalCount) result, err := r.db.Exec(` INSERT INTO quiz_records (id, user_id, resume_id, resume_route, title, questions, user_answers, score, correct_count, total_count, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now', 'localtime'))`, record.ID, record.UserID, record.ResumeID, record.ResumeRoute, record.Title, string(questionsJSON), string(userAnswersJSON), record.Score, record.CorrectCount, record.TotalCount) if err != nil { log.Printf("[ERROR] CreateQuizRecord error: %v", err) return err } rowsAffected, err := result.RowsAffected() if err != nil { log.Printf("[ERROR] CreateQuizRecord rows affected error: %v", err) } else { log.Printf("[DEBUG] CreateQuizRecord success, rows affected: %d", rowsAffected) } var count int err = r.db.QueryRow("SELECT COUNT(*) FROM quiz_records").Scan(&count) if err != nil { log.Printf("[ERROR] CreateQuizRecord count error: %v", err) } else { log.Printf("[DEBUG] CreateQuizRecord total records: %d", count) } return nil } // GetQuizRecordsByResume 根据简历路由获取面试题历史记录 func (r *sqliteRepository) GetQuizRecordsByResume(resumeRoute string) ([]*model.QuizRecord, error) { rows, err := r.db.Query(` SELECT id, user_id, resume_id, resume_route, title, questions, user_answers, score, correct_count, total_count, created_at FROM quiz_records WHERE resume_route = ? ORDER BY created_at DESC`, resumeRoute) if err != nil { return nil, err } defer rows.Close() var records []*model.QuizRecord for rows.Next() { var record model.QuizRecord var questionsJSON, userAnswersJSON, createdAt string err := rows.Scan(&record.ID, &record.UserID, &record.ResumeID, &record.ResumeRoute, &record.Title, &questionsJSON, &userAnswersJSON, &record.Score, &record.CorrectCount, &record.TotalCount, &createdAt) if err != nil { return nil, err } if questionsJSON != "" { json.Unmarshal([]byte(questionsJSON), &record.Questions) } if userAnswersJSON != "" { json.Unmarshal([]byte(userAnswersJSON), &record.UserAnswers) } records = append(records, &record) } return records, nil } // GetQuizRecord 根据ID获取面试题记录 func (r *sqliteRepository) GetQuizRecord(id string) (*model.QuizRecord, error) { var record model.QuizRecord var questionsJSON, userAnswersJSON string var title sql.NullString err := r.db.QueryRow(` SELECT id, user_id, resume_id, resume_route, title, questions, user_answers, score, correct_count, total_count, created_at FROM quiz_records WHERE id = ?`, id).Scan( &record.ID, &record.UserID, &record.ResumeID, &record.ResumeRoute, &title, &questionsJSON, &userAnswersJSON, &record.Score, &record.CorrectCount, &record.TotalCount, &record.CreatedAt) if err != nil { return nil, err } if title.Valid { record.Title = title.String } if questionsJSON != "" { json.Unmarshal([]byte(questionsJSON), &record.Questions) } if userAnswersJSON != "" { json.Unmarshal([]byte(userAnswersJSON), &record.UserAnswers) } return &record, nil } // DeleteQuizRecord 删除面试题记录 func (r *sqliteRepository) DeleteQuizRecord(id string) error { _, err := r.db.Exec(`DELETE FROM quiz_records WHERE id = ?`, id) return err } // GetQuizRecordsByUserID 根据用户ID获取面试题记录 func (r *sqliteRepository) GetQuizRecordsByUserID(userID string) ([]*model.QuizRecord, error) { rows, err := r.db.Query(` SELECT id, user_id, resume_id, resume_route, title, questions, user_answers, score, correct_count, total_count, created_at FROM quiz_records WHERE user_id = ? ORDER BY created_at DESC`, userID) if err != nil { return nil, err } defer rows.Close() var records []*model.QuizRecord for rows.Next() { var record model.QuizRecord var questionsJSON, userAnswersJSON, createdAt string var title sql.NullString err := rows.Scan(&record.ID, &record.UserID, &record.ResumeID, &record.ResumeRoute, &title, &questionsJSON, &userAnswersJSON, &record.Score, &record.CorrectCount, &record.TotalCount, &createdAt) if err != nil { return nil, err } if title.Valid { record.Title = title.String } if questionsJSON != "" { json.Unmarshal([]byte(questionsJSON), &record.Questions) } if userAnswersJSON != "" { json.Unmarshal([]byte(userAnswersJSON), &record.UserAnswers) } records = append(records, &record) } return records, nil } // GetQuizRecordsByUserIDWithPagination 根据用户ID分页获取面试题记录 func (r *sqliteRepository) GetQuizRecordsByUserIDWithPagination(userID string, page, pageSize int) ([]*model.QuizRecord, int, error) { var total int err := r.db.QueryRow(` SELECT COUNT(*) FROM quiz_records WHERE user_id = ?`, userID).Scan(&total) if err != nil { return nil, 0, err } offset := (page - 1) * pageSize rows, err := r.db.Query(` SELECT id, user_id, resume_id, resume_route, title, questions, user_answers, score, correct_count, total_count, created_at FROM quiz_records WHERE user_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?`, userID, pageSize, offset) if err != nil { return nil, 0, err } defer rows.Close() var records []*model.QuizRecord for rows.Next() { var record model.QuizRecord var questionsJSON, userAnswersJSON, createdAt string var title sql.NullString err := rows.Scan(&record.ID, &record.UserID, &record.ResumeID, &record.ResumeRoute, &title, &questionsJSON, &userAnswersJSON, &record.Score, &record.CorrectCount, &record.TotalCount, &createdAt) if err != nil { return nil, 0, err } if title.Valid { record.Title = title.String } if questionsJSON != "" { json.Unmarshal([]byte(questionsJSON), &record.Questions) } if userAnswersJSON != "" { json.Unmarshal([]byte(userAnswersJSON), &record.UserAnswers) } records = append(records, &record) } return records, total, nil } // GetFavoriteQuestionsByUserID 根据用户ID获取收藏/错题记录 func (r *sqliteRepository) GetFavoriteQuestionsByUserID(userID string) ([]*model.FavoriteQuestion, error) { rows, err := r.db.Query(` SELECT id, user_id, resume_id, resume_route, question, user_answer, is_correct, is_favorite, created_at FROM favorite_questions WHERE user_id = ? ORDER BY created_at DESC`, userID) if err != nil { return nil, err } defer rows.Close() var favorites []*model.FavoriteQuestion for rows.Next() { var fav model.FavoriteQuestion var questionJSON, createdAt string var isCorrect, isFavorite int err := rows.Scan(&fav.ID, &fav.UserID, &fav.ResumeID, &fav.ResumeRoute, &questionJSON, &fav.UserAnswer, &isCorrect, &isFavorite, &createdAt) if err != nil { return nil, err } fav.IsCorrect = isCorrect == 1 fav.IsFavorite = isFavorite == 1 if questionJSON != "" { json.Unmarshal([]byte(questionJSON), &fav.Question) } favorites = append(favorites, &fav) } return favorites, nil } // CreateFavoriteQuestion 创建收藏/错题记录 func (r *sqliteRepository) CreateFavoriteQuestion(fav *model.FavoriteQuestion) error { questionJSON, _ := json.Marshal(fav.Question) isCorrect := 0 if fav.IsCorrect { isCorrect = 1 } isFavorite := 0 if fav.IsFavorite { isFavorite = 1 } _, err := r.db.Exec(` INSERT INTO favorite_questions (id, user_id, resume_id, resume_route, question, user_answer, is_correct, is_favorite, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now', 'localtime'))`, fav.ID, fav.UserID, fav.ResumeID, fav.ResumeRoute, string(questionJSON), fav.UserAnswer, isCorrect, isFavorite) return err } // GetFavoriteQuestions 获取收藏/错题记录 func (r *sqliteRepository) GetFavoriteQuestions(resumeRoute string, isFavorite, isCorrect *bool) ([]*model.FavoriteQuestion, error) { query := `SELECT id, user_id, resume_id, resume_route, question, user_answer, is_correct, is_favorite, created_at FROM favorite_questions WHERE resume_route = ?` params := []interface{}{resumeRoute} if isFavorite != nil { query += " AND is_favorite = ?" if *isFavorite { params = append(params, 1) } else { params = append(params, 0) } } if isCorrect != nil { query += " AND is_correct = ?" if *isCorrect { params = append(params, 1) } else { params = append(params, 0) } } query += " ORDER BY created_at DESC" rows, err := r.db.Query(query, params...) if err != nil { return nil, err } defer rows.Close() var favorites []*model.FavoriteQuestion for rows.Next() { var fav model.FavoriteQuestion var questionJSON, createdAt string var isCorrect, isFavorite int err := rows.Scan(&fav.ID, &fav.UserID, &fav.ResumeID, &fav.ResumeRoute, &questionJSON, &fav.UserAnswer, &isCorrect, &isFavorite, &createdAt) if err != nil { return nil, err } fav.IsCorrect = isCorrect == 1 fav.IsFavorite = isFavorite == 1 if questionJSON != "" { json.Unmarshal([]byte(questionJSON), &fav.Question) } favorites = append(favorites, &fav) } return favorites, nil } // DeleteFavoriteQuestion 删除收藏/错题记录 func (r *sqliteRepository) DeleteFavoriteQuestion(id string) error { _, err := r.db.Exec(`DELETE FROM favorite_questions WHERE id = ?`, id) return err } // ToggleFavorite 切换收藏状态 func (r *sqliteRepository) ToggleFavorite(id string) error { _, err := r.db.Exec(`UPDATE favorite_questions SET is_favorite = 1 - is_favorite WHERE id = ?`, id) return err } // CreateDocument 创建文档 func (r *sqliteRepository) CreateDocument(doc *model.Document) error { _, err := r.db.Exec(` INSERT INTO documents (id, user_id, name, type, size, mime_type, content, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now', 'localtime'), datetime('now', 'localtime'))`, doc.ID, doc.UserID, doc.Name, doc.Type, doc.Size, doc.MIMEType, doc.Content, doc.Status) return err } // GetDocument 根据ID和用户ID获取文档 func (r *sqliteRepository) GetDocument(id, userID string) (*model.Document, error) { var doc model.Document var createdAt, updatedAt string err := r.db.QueryRow(` SELECT id, user_id, name, type, size, mime_type, content, status, error_msg, created_at, updated_at FROM documents WHERE id = ? AND user_id = ?`, id, userID).Scan( &doc.ID, &doc.UserID, &doc.Name, &doc.Type, &doc.Size, &doc.MIMEType, &doc.Content, &doc.Status, &doc.ErrorMsg, &createdAt, &updatedAt) if err != nil { return nil, err } doc.CreatedAt = formatDateTime(createdAt) doc.UpdatedAt = formatDateTime(updatedAt) return &doc, nil } // GetDocumentByID 根据ID获取文档 func (r *sqliteRepository) GetDocumentByID(id string) (*model.Document, error) { var doc model.Document var createdAt, updatedAt string err := r.db.QueryRow(` SELECT id, user_id, name, type, size, mime_type, content, status, error_msg, created_at, updated_at FROM documents WHERE id = ?`, id).Scan( &doc.ID, &doc.UserID, &doc.Name, &doc.Type, &doc.Size, &doc.MIMEType, &doc.Content, &doc.Status, &doc.ErrorMsg, &createdAt, &updatedAt) if err != nil { return nil, err } doc.CreatedAt = formatDateTime(createdAt) doc.UpdatedAt = formatDateTime(updatedAt) return &doc, nil } // GetDocumentsByUserID 根据用户ID获取文档列表 func (r *sqliteRepository) GetDocumentsByUserID(userID string) ([]*model.Document, error) { rows, err := r.db.Query(` SELECT id, user_id, name, type, size, mime_type, content, status, error_msg, created_at, updated_at FROM documents WHERE user_id = ? ORDER BY created_at DESC`, userID) if err != nil { return nil, err } defer rows.Close() var docs []*model.Document for rows.Next() { var doc model.Document var createdAt, updatedAt string err := rows.Scan(&doc.ID, &doc.UserID, &doc.Name, &doc.Type, &doc.Size, &doc.MIMEType, &doc.Content, &doc.Status, &doc.ErrorMsg, &createdAt, &updatedAt) if err != nil { return nil, err } doc.CreatedAt = formatDateTime(createdAt) doc.UpdatedAt = formatDateTime(updatedAt) docs = append(docs, &doc) } return docs, nil } // UpdateDocument 更新文档 func (r *sqliteRepository) UpdateDocument(doc *model.Document) error { _, err := r.db.Exec(` UPDATE documents SET name = ?, type = ?, size = ?, mime_type = ?, content = ?, status = ?, error_msg = ?, updated_at = datetime('now', 'localtime') WHERE id = ?`, doc.Name, doc.Type, doc.Size, doc.MIMEType, doc.Content, doc.Status, doc.ErrorMsg, doc.ID) return err } // DeleteDocument 删除文档 func (r *sqliteRepository) DeleteDocument(id, userID string) error { _, err := r.db.Exec(`DELETE FROM documents WHERE id = ? AND user_id = ?`, id, userID) if err != nil { return err } _, err = r.db.Exec(`DELETE FROM document_chunks WHERE document_id = ?`, id) return err } // CreateDocumentChunk 创建文档切片 func (r *sqliteRepository) CreateDocumentChunk(chunk *model.DocumentChunk) error { _, err := r.db.Exec(` INSERT INTO document_chunks (id, document_id, chunk_index, content, embedding, metadata, created_at) VALUES (?, ?, ?, ?, ?, ?, datetime('now', 'localtime'))`, chunk.ID, chunk.DocumentID, chunk.ChunkIndex, chunk.Content, chunk.Embedding, chunk.Metadata) return err } // GetDocumentChunksByUserID 根据用户ID获取文档切片 func (r *sqliteRepository) GetDocumentChunksByUserID(userID string) ([]*model.DocumentChunk, error) { rows, err := r.db.Query(` SELECT dc.id, dc.document_id, dc.chunk_index, dc.content, dc.embedding, dc.metadata, dc.created_at FROM document_chunks dc JOIN documents d ON dc.document_id = d.id WHERE d.user_id = ?`, userID) if err != nil { return nil, err } defer rows.Close() var chunks []*model.DocumentChunk for rows.Next() { var chunk model.DocumentChunk var createdAt string err := rows.Scan(&chunk.ID, &chunk.DocumentID, &chunk.ChunkIndex, &chunk.Content, &chunk.Embedding, &chunk.Metadata, &createdAt) if err != nil { return nil, err } chunk.CreatedAt = formatDateTime(createdAt) chunks = append(chunks, &chunk) } return chunks, nil } // GetAllDocumentChunks 获取所有文档切片 func (r *sqliteRepository) GetAllDocumentChunks() ([]*model.DocumentChunk, error) { rows, err := r.db.Query(` SELECT id, document_id, chunk_index, content, embedding, metadata, created_at FROM document_chunks ORDER BY document_id, chunk_index`) if err != nil { return nil, err } defer rows.Close() var chunks []*model.DocumentChunk for rows.Next() { var chunk model.DocumentChunk var createdAt string err := rows.Scan(&chunk.ID, &chunk.DocumentID, &chunk.ChunkIndex, &chunk.Content, &chunk.Embedding, &chunk.Metadata, &createdAt) if err != nil { return nil, err } chunk.CreatedAt = formatDateTime(createdAt) chunks = append(chunks, &chunk) } return chunks, nil } // GetDocumentChunksByDocumentID 根据文档ID获取文档切片 func (r *sqliteRepository) GetDocumentChunksByDocumentID(documentID string) ([]*model.DocumentChunk, error) { rows, err := r.db.Query(` SELECT id, document_id, chunk_index, content, embedding, metadata, created_at FROM document_chunks WHERE document_id = ? ORDER BY chunk_index`, documentID) if err != nil { return nil, err } defer rows.Close() var chunks []*model.DocumentChunk for rows.Next() { var chunk model.DocumentChunk var createdAt string err := rows.Scan(&chunk.ID, &chunk.DocumentID, &chunk.ChunkIndex, &chunk.Content, &chunk.Embedding, &chunk.Metadata, &createdAt) if err != nil { return nil, err } chunk.CreatedAt = formatDateTime(createdAt) chunks = append(chunks, &chunk) } return chunks, nil } // CreateEmbedding 创建向量嵌入 func (r *sqliteRepository) CreateEmbedding(embedding *model.Embedding) error { _, err := r.db.Exec(` INSERT OR REPLACE INTO embeddings (id, content_hash, embedding, source_type, source_id, source_name, user_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now', 'localtime'))`, embedding.ID, embedding.ContentHash, embedding.Embedding, embedding.SourceType, embedding.SourceID, embedding.SourceName, embedding.UserID) return err } // GetEmbeddingByContentHash 根据内容哈希获取向量 func (r *sqliteRepository) GetEmbeddingByContentHash(contentHash string) (*model.Embedding, error) { var emb model.Embedding var createdAt string err := r.db.QueryRow(` SELECT id, content_hash, embedding, source_type, source_id, source_name, user_id, created_at FROM embeddings WHERE content_hash = ?`, contentHash).Scan( &emb.ID, &emb.ContentHash, &emb.Embedding, &emb.SourceType, &emb.SourceID, &emb.SourceName, &emb.UserID, &createdAt) if err != nil { return nil, err } emb.CreatedAt = formatDateTime(createdAt) return &emb, nil } // GetEmbeddingsByUserID 根据用户ID获取向量列表 func (r *sqliteRepository) GetEmbeddingsByUserID(userID string) ([]*model.Embedding, error) { rows, err := r.db.Query(` SELECT id, content_hash, embedding, source_type, source_id, source_name, user_id, created_at FROM embeddings WHERE user_id = ? ORDER BY created_at DESC`, userID) if err != nil { return nil, err } defer rows.Close() var embeddings []*model.Embedding for rows.Next() { var emb model.Embedding var createdAt string err := rows.Scan(&emb.ID, &emb.ContentHash, &emb.Embedding, &emb.SourceType, &emb.SourceID, &emb.SourceName, &emb.UserID, &createdAt) if err != nil { return nil, err } emb.CreatedAt = formatDateTime(createdAt) embeddings = append(embeddings, &emb) } return embeddings, nil } // GetAllEmbeddings 获取所有向量 func (r *sqliteRepository) GetAllEmbeddings() ([]*model.Embedding, error) { rows, err := r.db.Query(` SELECT id, content_hash, embedding, source_type, source_id, source_name, user_id, created_at FROM embeddings ORDER BY created_at DESC`) if err != nil { return nil, err } defer rows.Close() var embeddings []*model.Embedding for rows.Next() { var emb model.Embedding var createdAt string err := rows.Scan(&emb.ID, &emb.ContentHash, &emb.Embedding, &emb.SourceType, &emb.SourceID, &emb.SourceName, &emb.UserID, &createdAt) if err != nil { return nil, err } emb.CreatedAt = formatDateTime(createdAt) embeddings = append(embeddings, &emb) } return embeddings, nil } // DeleteEmbedding 删除向量 func (r *sqliteRepository) DeleteEmbedding(id string) error { _, err := r.db.Exec(`DELETE FROM embeddings WHERE id = ?`, id) return err } // DeleteEmbeddingsBySource 根据来源类型和来源ID删除向量 func (r *sqliteRepository) DeleteEmbeddingsBySource(sourceType, sourceID string) error { _, err := r.db.Exec(`DELETE FROM embeddings WHERE source_type = ? AND source_id = ?`, sourceType, sourceID) return err } // CreateChatHistory 创建对话历史 func (r *sqliteRepository) CreateChatHistory(history *model.ChatHistory) error { _, err := r.db.Exec(` INSERT INTO chat_history (id, user_id, question, answer, context, created_at) VALUES (?, ?, ?, ?, ?, datetime('now', 'localtime'))`, history.ID, history.UserID, history.Question, history.Answer, history.Context) return err } // GetChatHistoryByUserID 根据用户ID获取对话历史 func (r *sqliteRepository) GetChatHistoryByUserID(userID string) ([]*model.ChatHistory, error) { rows, err := r.db.Query(` SELECT id, user_id, question, answer, context, created_at FROM chat_history WHERE user_id = ? ORDER BY created_at DESC`, userID) if err != nil { return nil, err } defer rows.Close() var histories []*model.ChatHistory for rows.Next() { var history model.ChatHistory var createdAt string err := rows.Scan(&history.ID, &history.UserID, &history.Question, &history.Answer, &history.Context, &createdAt) if err != nil { return nil, err } history.CreatedAt = formatDateTime(createdAt) histories = append(histories, &history) } return histories, nil } func (r *sqliteRepository) GetAllMenus() ([]*model.Menu, error) { rows, err := r.db.Query(`SELECT id, name, icon, path, parent_id, sort_order, is_fixed, is_enabled, created_at, updated_at FROM menus ORDER BY sort_order ASC`) if err != nil { return nil, err } defer rows.Close() var menus []*model.Menu for rows.Next() { var menu model.Menu var isFixed, isEnabled int var createdAt, updatedAt string err := rows.Scan(&menu.ID, &menu.Name, &menu.Icon, &menu.Path, &menu.ParentID, &menu.SortOrder, &isFixed, &isEnabled, &createdAt, &updatedAt) if err != nil { return nil, err } menu.IsFixed = isFixed == 1 menu.IsEnabled = isEnabled == 1 menu.CreatedAt = formatDateTime(createdAt) menu.UpdatedAt = formatDateTime(updatedAt) menus = append(menus, &menu) } return menus, nil } func (r *sqliteRepository) CreateMenu(menu *model.Menu) error { _, err := r.db.Exec(` INSERT INTO menus (id, name, icon, path, parent_id, sort_order, is_fixed, is_enabled, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now', 'localtime'), datetime('now', 'localtime'))`, menu.ID, menu.Name, menu.Icon, menu.Path, menu.ParentID, menu.SortOrder, map[bool]int{true: 1, false: 0}[menu.IsFixed], map[bool]int{true: 1, false: 0}[menu.IsEnabled]) return err } func (r *sqliteRepository) UpdateMenu(id string, menu *model.Menu) error { _, err := r.db.Exec(` UPDATE menus SET name = ?, icon = ?, path = ?, parent_id = ?, sort_order = ?, is_fixed = ?, is_enabled = ?, updated_at = datetime('now', 'localtime') WHERE id = ?`, menu.Name, menu.Icon, menu.Path, menu.ParentID, menu.SortOrder, map[bool]int{true: 1, false: 0}[menu.IsFixed], map[bool]int{true: 1, false: 0}[menu.IsEnabled], id) return err } func (r *sqliteRepository) DeleteMenu(id string) error { var isFixed int err := r.db.QueryRow(`SELECT is_fixed FROM menus WHERE id = ?`, id).Scan(&isFixed) if err != nil { return err } if isFixed == 1 { return errors.New("cannot delete fixed menu") } _, err = r.db.Exec(`DELETE FROM menus WHERE id = ?`, id) return err } func (r *sqliteRepository) GetSystemConfig() (*model.SystemConfig, error) { var config model.SystemConfig var createdAt, updatedAt string err := r.db.QueryRow(`SELECT id, website_domain, website_logo, logo_path, website_title, website_desc, website_description, website_keywords, seo_keywords, seo_description, admin_email, admin_name, database_path, admin_page_size, created_at, updated_at FROM system_config LIMIT 1`).Scan( &config.ID, &config.WebsiteDomain, &config.WebsiteLogo, &config.LogoPath, &config.WebsiteTitle, &config.WebsiteDesc, &config.WebsiteDescription, &config.WebsiteKeywords, &config.SEOKeywords, &config.SEODescription, &config.AdminEmail, &config.AdminName, &config.DatabasePath, &config.AdminPageSize, &createdAt, &updatedAt) if err != nil { if err == sql.ErrNoRows { return nil, os.ErrNotExist } return nil, err } config.CreatedAt = formatDateTime(createdAt) config.UpdatedAt = formatDateTime(updatedAt) return &config, nil } func (r *sqliteRepository) CreateSystemConfig(config *model.SystemConfig) error { _, err := r.db.Exec(` INSERT INTO system_config (id, website_domain, website_logo, logo_path, website_title, website_desc, website_description, website_keywords, seo_keywords, seo_description, admin_email, admin_name, database_path, admin_page_size, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now', 'localtime'), datetime('now', 'localtime'))`, config.ID, config.WebsiteDomain, config.WebsiteLogo, config.LogoPath, config.WebsiteTitle, config.WebsiteDesc, config.WebsiteDescription, config.WebsiteKeywords, config.SEOKeywords, config.SEODescription, config.AdminEmail, config.AdminName, config.DatabasePath, config.AdminPageSize) return err } func (r *sqliteRepository) UpdateSystemConfig(id string, config *model.SystemConfig) error { _, err := r.db.Exec(` UPDATE system_config SET website_domain = ?, website_logo = ?, logo_path = ?, website_title = ?, website_desc = ?, website_description = ?, website_keywords = ?, seo_keywords = ?, seo_description = ?, admin_email = ?, admin_name = ?, database_path = ?, admin_page_size = ?, updated_at = datetime('now', 'localtime') WHERE id = ?`, config.WebsiteDomain, config.WebsiteLogo, config.LogoPath, config.WebsiteTitle, config.WebsiteDesc, config.WebsiteDescription, config.WebsiteKeywords, config.SEOKeywords, config.SEODescription, config.AdminEmail, config.AdminName, config.DatabasePath, config.AdminPageSize, id) return err } func (r *sqliteRepository) CleanSystemConfig() error { _, err := r.db.Exec("DELETE FROM system_config WHERE id != 'default'") return err } func (r *sqliteRepository) CreateLoginHistory(history *model.LoginHistory) error { _, err := r.db.Exec(` INSERT INTO login_history (id, user_id, user_name, ip, location, user_agent, success, message, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now', 'localtime'))`, history.ID, history.UserID, history.UserName, history.IP, history.Location, history.UserAgent, history.Success, history.Message) return err } func (r *sqliteRepository) GetLoginHistory(page, pageSize int) ([]*model.LoginHistory, int, error) { var total int err := r.db.QueryRow("SELECT COUNT(*) FROM login_history").Scan(&total) if err != nil { return nil, 0, err } offset := (page - 1) * pageSize rows, err := r.db.Query(` SELECT id, user_id, user_name, ip, location, user_agent, success, message, created_at FROM login_history ORDER BY created_at DESC LIMIT ? OFFSET ?`, pageSize, offset) if err != nil { return nil, 0, err } defer rows.Close() var histories []*model.LoginHistory for rows.Next() { var history model.LoginHistory var success int err := rows.Scan(&history.ID, &history.UserID, &history.UserName, &history.IP, &history.Location, &history.UserAgent, &success, &history.Message, &history.CreatedAt) if err != nil { return nil, 0, err } history.Success = success == 1 histories = append(histories, &history) } return histories, total, nil } // CreateNotification 创建通知 func (r *sqliteRepository) CreateNotification(n *model.Notification) error { if n.ID == "" { n.ID = uuid.NewString() } if n.CreatedAt == "" { n.CreatedAt = time.Now().Format("2006-01-02 15:04:05") } isRead := 0 if n.IsRead { isRead = 1 } _, err := r.db.Exec(` INSERT INTO notifications (id, type, title, message, url, is_read, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)`, n.ID, n.Type, n.Title, n.Message, n.URL, isRead, n.CreatedAt) return err } // GetNotifications 获取所有通知,按时间倒序 func (r *sqliteRepository) GetNotifications() ([]*model.Notification, error) { rows, err := r.db.Query(` SELECT id, type, title, message, url, is_read, created_at FROM notifications ORDER BY created_at DESC LIMIT 50`) if err != nil { return nil, err } defer rows.Close() var notifications []*model.Notification for rows.Next() { var n model.Notification var isRead int err := rows.Scan(&n.ID, &n.Type, &n.Title, &n.Message, &n.URL, &isRead, &n.CreatedAt) if err != nil { return nil, err } n.IsRead = isRead == 1 notifications = append(notifications, &n) } return notifications, nil } // GetUnreadNotificationCount 获取未读通知数量 func (r *sqliteRepository) GetUnreadNotificationCount() (int, error) { var count int err := r.db.QueryRow("SELECT COUNT(*) FROM notifications WHERE is_read = 0").Scan(&count) return count, err } // MarkNotificationAsRead 标记单条通知为已读 func (r *sqliteRepository) MarkNotificationAsRead(id string) error { _, err := r.db.Exec("UPDATE notifications SET is_read = 1 WHERE id = ?", id) return err } // MarkAllNotificationsAsRead 标记所有通知为已读 func (r *sqliteRepository) MarkAllNotificationsAsRead() error { _, err := r.db.Exec("UPDATE notifications SET is_read = 1 WHERE is_read = 0") return err } // DeleteAllNotifications 删除所有通知 func (r *sqliteRepository) DeleteAllNotifications() error { _, err := r.db.Exec("DELETE FROM notifications") return err } // DeleteNotification 删除单条通知 func (r *sqliteRepository) DeleteNotification(id string) error { _, err := r.db.Exec("DELETE FROM notifications WHERE id = ?", id) return err } // populateResumeNames 迁移数据:从加密的basic_info中提取name字段并填充到name列 func (r *sqliteRepository) populateResumeNames() error { var count int err := r.db.QueryRow("SELECT COUNT(*) FROM resumes WHERE name IS NULL AND basic_info IS NOT NULL").Scan(&count) if err != nil { return err } if count == 0 { return nil } rows, err := r.db.Query("SELECT id, basic_info FROM resumes WHERE name IS NULL AND basic_info IS NOT NULL") if err != nil { return err } type nameUpdate struct { id string name string } var updates []nameUpdate for rows.Next() { var id, basicInfoJSON string err := rows.Scan(&id, &basicInfoJSON) if err != nil { rows.Close() return err } var basicInfo model.BasicInfo if r.enabled && basicInfoJSON != "" { decrypted, decryptErr := r.decrypt(basicInfoJSON) if decryptErr == nil { json.Unmarshal([]byte(decrypted), &basicInfo) } } else if basicInfoJSON != "" { json.Unmarshal([]byte(basicInfoJSON), &basicInfo) } if basicInfo.Name != "" { updates = append(updates, nameUpdate{id: id, name: basicInfo.Name}) } } rows.Close() if len(updates) == 0 { return nil } tx, err := r.db.Begin() if err != nil { return err } for _, u := range updates { _, err := tx.Exec("UPDATE resumes SET name = ? WHERE id = ?", u.name, u.id) if err != nil { tx.Rollback() return err } } return tx.Commit() }