mirror of
https://github.com/woodchen-ink/Q58Bot.git
synced 2025-07-18 05:42:06 +08:00
- 扩展了数据库模式,为关键词添加了新字段:is_link、is_auto_added和added_at。 - 实现了新的关键词管理方法,包括AddKeyword的更新,使其能够设置链接和自动添加标记。 - 开发了CleanupExpiredLinks方法来删除过期的自动添加链接。 - 导入了time包以支持新字段的Timestamp默认值。 - 进行了迁移脚本的开发和执行,以无缝过渡到新的数据库结构。 - 更新了关键词检索方法,以支持新的关键词属性。 - 在消息处理和链接过滤服务中调整了关键词的添加逻辑。 - 重构了/list命令的响应,以区分手动添加的关键词和自动添加的链接。 - 优化了/add命令,以正确处理新的关键词属性。 注意:这些更改需要在已有的数据库中执行适当的迁移脚本,以避免数据丢失或结构冲突。
445 lines
10 KiB
Go
445 lines
10 KiB
Go
package core
|
|
|
|
//数据库处理
|
|
import (
|
|
"database/sql"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
_ "modernc.org/sqlite"
|
|
)
|
|
|
|
type Database struct {
|
|
db *sql.DB
|
|
keywordsCache []string
|
|
whitelistCache []string
|
|
promptRepliesCache map[string]string
|
|
keywordsCacheTime time.Time
|
|
whitelistCacheTime time.Time
|
|
promptRepliesCacheTime time.Time
|
|
mu sync.Mutex
|
|
}
|
|
|
|
func NewDatabase() (*Database, error) {
|
|
os.MkdirAll(filepath.Dir(DB_FILE), os.ModePerm)
|
|
db, err := sql.Open("sqlite", DB_FILE)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
database := &Database{
|
|
db: db,
|
|
}
|
|
|
|
if err := database.createTables(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return database, nil
|
|
}
|
|
|
|
func (d *Database) createTables() error {
|
|
queries := []string{
|
|
`CREATE TABLE IF NOT EXISTS keywords
|
|
(id INTEGER PRIMARY KEY, keyword TEXT UNIQUE, is_link BOOLEAN, is_auto_added BOOLEAN, added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)`,
|
|
`CREATE INDEX IF NOT EXISTS idx_keyword ON keywords(keyword)`,
|
|
`CREATE INDEX IF NOT EXISTS idx_added_at ON keywords(added_at)`,
|
|
`CREATE INDEX IF NOT EXISTS idx_is_link ON keywords(is_link)`,
|
|
`CREATE INDEX IF NOT EXISTS idx_is_auto_added ON keywords(is_auto_added)`,
|
|
`CREATE TABLE IF NOT EXISTS whitelist
|
|
(id INTEGER PRIMARY KEY, domain TEXT UNIQUE)`,
|
|
`CREATE INDEX IF NOT EXISTS idx_domain ON whitelist(domain)`,
|
|
`CREATE TABLE IF NOT EXISTS prompt_replies
|
|
(prompt TEXT PRIMARY KEY, reply TEXT NOT NULL)`,
|
|
}
|
|
|
|
for _, query := range queries {
|
|
_, err := d.db.Exec(query)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (d *Database) executeQuery(query string, args ...interface{}) ([]string, error) {
|
|
rows, err := d.db.Query(query, args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var results []string
|
|
for rows.Next() {
|
|
var result string
|
|
if err := rows.Scan(&result); err != nil {
|
|
return nil, err
|
|
}
|
|
results = append(results, result)
|
|
}
|
|
|
|
return results, nil
|
|
}
|
|
|
|
func (d *Database) AddKeyword(keyword string, isLink bool, isAutoAdded bool) error {
|
|
_, err := d.db.Exec("INSERT OR IGNORE INTO keywords (keyword, is_link, is_auto_added, added_at) VALUES (?, ?, ?, ?)",
|
|
keyword, isLink, isAutoAdded, time.Now())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
d.invalidateCache("keywords")
|
|
return nil
|
|
}
|
|
|
|
func (d *Database) RemoveKeyword(keyword string) (bool, error) {
|
|
result, err := d.db.Exec("DELETE FROM keywords WHERE keyword = ?", keyword)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
rowsAffected, err := result.RowsAffected()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
d.invalidateCache("keywords")
|
|
return rowsAffected > 0, nil
|
|
}
|
|
|
|
func (d *Database) CleanupExpiredLinks() error {
|
|
twoMonthsAgo := time.Now().AddDate(0, -2, 0)
|
|
_, err := d.db.Exec("DELETE FROM keywords WHERE is_link = TRUE AND is_auto_added = TRUE AND added_at < ?", twoMonthsAgo)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
d.invalidateCache("keywords")
|
|
return nil
|
|
}
|
|
|
|
func (d *Database) GetAllKeywords() ([]string, error) {
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
|
|
if d.keywordsCache == nil || time.Since(d.keywordsCacheTime) > 5*time.Minute {
|
|
keywords, err := d.executeQuery("SELECT keyword FROM keywords")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
d.keywordsCache = keywords
|
|
d.keywordsCacheTime = time.Now()
|
|
}
|
|
|
|
return d.keywordsCache, nil
|
|
}
|
|
func (d *Database) GetAllManualKeywords() ([]string, error) {
|
|
return d.executeQuery("SELECT keyword FROM keywords WHERE is_auto_added = ?", false)
|
|
}
|
|
|
|
func (d *Database) GetAllAutoAddedLinks() ([]string, error) {
|
|
return d.executeQuery("SELECT keyword FROM keywords WHERE is_link = ? AND is_auto_added = ?", true, true)
|
|
}
|
|
|
|
func (d *Database) RemoveKeywordsContaining(substring string) ([]string, error) {
|
|
// 首先获取要删除的关键词列表
|
|
rows, err := d.db.Query("SELECT keyword FROM keywords WHERE keyword LIKE ?", "%"+substring+"%")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var removedKeywords []string
|
|
for rows.Next() {
|
|
var keyword string
|
|
if err := rows.Scan(&keyword); err != nil {
|
|
return nil, err
|
|
}
|
|
removedKeywords = append(removedKeywords, keyword)
|
|
}
|
|
|
|
// 执行删除操作
|
|
_, err = d.db.Exec("DELETE FROM keywords WHERE keyword LIKE ?", "%"+substring+"%")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
d.invalidateCache("keywords")
|
|
return removedKeywords, nil
|
|
}
|
|
|
|
func (d *Database) AddWhitelist(domain string) error {
|
|
_, err := d.db.Exec("INSERT OR IGNORE INTO whitelist (domain) VALUES (?)", domain)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
d.invalidateCache("whitelist")
|
|
return nil
|
|
}
|
|
|
|
func (d *Database) RemoveWhitelist(domain string) error {
|
|
_, err := d.db.Exec("DELETE FROM whitelist WHERE domain = ?", domain)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
d.invalidateCache("whitelist")
|
|
return nil
|
|
}
|
|
|
|
func (d *Database) GetAllWhitelist() ([]string, error) {
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
|
|
if d.whitelistCache == nil || time.Since(d.whitelistCacheTime) > 5*time.Minute {
|
|
whitelist, err := d.executeQuery("SELECT domain FROM whitelist")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
d.whitelistCache = whitelist
|
|
d.whitelistCacheTime = time.Now()
|
|
}
|
|
|
|
return d.whitelistCache, nil
|
|
}
|
|
|
|
func (d *Database) SearchKeywords(pattern string) ([]string, error) {
|
|
return d.executeQuery("SELECT keyword FROM keywords WHERE keyword LIKE ?", "%"+pattern+"%")
|
|
}
|
|
|
|
func (d *Database) KeywordExists(keyword string) (bool, error) {
|
|
var count int
|
|
err := d.db.QueryRow("SELECT COUNT(*) FROM keywords WHERE keyword = ?", keyword).Scan(&count)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return count > 0, nil
|
|
}
|
|
|
|
func (d *Database) WhitelistExists(domain string) (bool, error) {
|
|
var count int
|
|
err := d.db.QueryRow("SELECT COUNT(*) FROM whitelist WHERE domain = ?", domain).Scan(&count)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return count > 0, nil
|
|
}
|
|
|
|
func (d *Database) AddPromptReply(prompt, reply string) error {
|
|
tx, err := d.db.Begin()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
_, err = tx.Exec("INSERT OR REPLACE INTO prompt_replies (prompt, reply) VALUES (?, ?)", strings.ToLower(prompt), reply)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = tx.Commit(); err != nil {
|
|
return err
|
|
}
|
|
|
|
d.invalidateCache("promptReplies")
|
|
return nil
|
|
}
|
|
|
|
func (d *Database) DeletePromptReply(prompt string) error {
|
|
tx, err := d.db.Begin()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
_, err = tx.Exec("DELETE FROM prompt_replies WHERE prompt = ?", strings.ToLower(prompt))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = tx.Commit(); err != nil {
|
|
return err
|
|
}
|
|
|
|
d.invalidateCache("promptReplies")
|
|
return nil
|
|
}
|
|
|
|
func (d *Database) fetchAllPromptReplies() (map[string]string, error) {
|
|
rows, err := d.db.Query("SELECT prompt, reply FROM prompt_replies")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
promptReplies := make(map[string]string)
|
|
for rows.Next() {
|
|
var prompt, reply string
|
|
if err := rows.Scan(&prompt, &reply); err != nil {
|
|
return nil, err
|
|
}
|
|
promptReplies[prompt] = reply
|
|
}
|
|
return promptReplies, nil
|
|
}
|
|
|
|
func (d *Database) GetAllPromptReplies() (map[string]string, error) {
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
|
|
// 强制刷新缓存
|
|
promptReplies, err := d.fetchAllPromptReplies()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
d.promptRepliesCache = promptReplies
|
|
d.promptRepliesCacheTime = time.Now()
|
|
|
|
// 返回一个副本
|
|
result := make(map[string]string, len(d.promptRepliesCache))
|
|
for k, v := range d.promptRepliesCache {
|
|
result[k] = v
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (d *Database) invalidateCache(cacheType string) {
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
switch cacheType {
|
|
case "keywords":
|
|
d.keywordsCache = nil
|
|
d.keywordsCacheTime = time.Time{}
|
|
case "whitelist":
|
|
d.whitelistCache = nil
|
|
d.whitelistCacheTime = time.Time{}
|
|
case "promptReplies":
|
|
d.promptRepliesCache = nil
|
|
d.promptRepliesCacheTime = time.Time{}
|
|
default:
|
|
// 清除所有缓存
|
|
d.keywordsCache = nil
|
|
d.whitelistCache = nil
|
|
d.promptRepliesCache = nil
|
|
d.keywordsCacheTime = time.Time{}
|
|
d.whitelistCacheTime = time.Time{}
|
|
d.promptRepliesCacheTime = time.Time{}
|
|
}
|
|
}
|
|
|
|
func (d *Database) Close() error {
|
|
return d.db.Close()
|
|
}
|
|
|
|
// 迁移现有关键词
|
|
//
|
|
//
|
|
//
|
|
//
|
|
//
|
|
//
|
|
//
|
|
//
|
|
//
|
|
//
|
|
//
|
|
//
|
|
//
|
|
//
|
|
|
|
func (d *Database) MigrateExistingKeywords() error {
|
|
// 检查是否已经执行过迁移
|
|
var migrationDone bool
|
|
err := d.db.QueryRow("SELECT value FROM config WHERE key = 'keywords_migrated'").Scan(&migrationDone)
|
|
if err != nil && err != sql.ErrNoRows {
|
|
return err
|
|
}
|
|
|
|
if migrationDone {
|
|
return nil // 迁移已经完成,无需再次执行
|
|
}
|
|
|
|
// 开始事务
|
|
tx, err := d.db.Begin()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
// 获取所有现有的关键词
|
|
rows, err := tx.Query("SELECT keyword FROM keywords")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer rows.Close()
|
|
|
|
// 准备插入语句
|
|
stmt, err := tx.Prepare("INSERT INTO keywords (keyword, is_link, is_auto_added) VALUES (?, ?, ?)")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer stmt.Close()
|
|
|
|
// 迁移现有关键词
|
|
for rows.Next() {
|
|
var keyword string
|
|
if err := rows.Scan(&keyword); err != nil {
|
|
return err
|
|
}
|
|
|
|
// 这里我们假设所有现有的关键词都是手动添加的非链接关键词
|
|
_, err = stmt.Exec(keyword, false, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// 删除旧的关键词表
|
|
_, err = tx.Exec("DROP TABLE IF EXISTS old_keywords")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// 重命名现有的关键词表
|
|
_, err = tx.Exec("ALTER TABLE keywords RENAME TO old_keywords")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// 创建新的关键词表
|
|
_, err = tx.Exec(`CREATE TABLE keywords (
|
|
id INTEGER PRIMARY KEY,
|
|
keyword TEXT UNIQUE,
|
|
is_link BOOLEAN,
|
|
is_auto_added BOOLEAN,
|
|
added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
)`)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// 将数据从旧表复制到新表
|
|
_, err = tx.Exec("INSERT INTO keywords SELECT id, keyword, is_link, is_auto_added, added_at FROM old_keywords")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// 删除旧表
|
|
_, err = tx.Exec("DROP TABLE old_keywords")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// 更新配置,标记迁移已完成
|
|
_, err = tx.Exec("INSERT OR REPLACE INTO config (key, value) VALUES ('keywords_migrated', 'true')")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// 提交事务
|
|
if err := tx.Commit(); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|