Files
resume/internal/repository/sqlite.go
T

2446 lines
73 KiB
Go

// 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()
}