package scoresaber import ( "database/sql" "encoding/json" "errors" "fmt" "io" "log" "net/http" "strconv" "strings" "time" "git.lxtend.com/qqbot/sqlite3" _ "github.com/mattn/go-sqlite3" ) func init() { createBindTableSQL := `CREATE TABLE IF NOT EXISTS ssBind ( id INTEGER PRIMARY KEY AUTOINCREMENT, qqid TEXT UNIQUE, ssid TEXT UNIQUE );` createScoreTableSQL := `CREATE TABLE IF NOT EXISTS ssData ( id TEXT, name TEXT, country TEXT, pp REAL, rank INTEGER, country_rank INTEGER, total_score INTEGER, total_ranked_score INTEGER, average_ranked_accuracy REAL, total_play_count INTEGER, ranked_play_count INTEGER, replays_watched INTEGER, generated_time TEXT );` createRecordTableSQL := `CREATE TABLE IF NOT EXISTS ssRecordData ( id INTEGER PRIMARY KEY AUTOINCREMENT, score_id INT, ss_id TEXT, name TEXT, country TEXT, song_name VARCHAR(255), song_sub_name VARCHAR(255), song_author_name VARCHAR(255), song_hash VARCHAR(64), cover_image TEXT, difficulty_raw VARCHAR(100), pp REAL, stars REAL, weight REAL, modifiers VARCHAR(255), multiplier REAL, rank INT, bad_cuts INT, missed_notes INT, max_combo INT, score INT, max_score INT, full_combo BOOLEAN, device_hmd VARCHAR(100), device_controller_left VARCHAR(100), device_controller_right VARCHAR(100), generated_time TEXT );` sqlite3.TryCreateTable(createBindTableSQL) sqlite3.TryCreateTable(createScoreTableSQL) sqlite3.TryCreateTable(createRecordTableSQL) } var SSQuery = &ssQuery{} type ssQuery struct { } func (ss *ssQuery) BindSS(qqId string, ssId string) (reply string) { tx, err := sqlite3.GetTran() if err != nil { log.Print(err) } defer tx.Rollback() // ssId为数字 if _, isNum := strconv.Atoi(ssId); isNum != nil { return "ssId格式错误,应当为一串数字(大部分情况下是你的steamID)" } data, err := FetchPlayerData(ssId) if data == nil { if err != nil { return "未找到玩家,请检查ID后重试:" + err.Error() } return "未找到玩家,请检查ID后重试" } //去重 if rows, err := tx.Query("SELECT * FROM ssBind WHERE qqid = ?", qqId); err == nil { if rows.Next() { return "您已绑定过ss账号,请先输入\"解绑ss\"解绑" } rows.Close() } // 检查是否已绑定 if rows, err := tx.Query("SELECT * FROM ssBind WHERE ssid = ?", ssId); err == nil { if rows.Next() { return "该ss账号已绑定至其他用户" } rows.Close() } _, err = tx.Exec("INSERT INTO ssBind(qqid, ssid) VALUES(?, ?)", qqId, ssId) if err != nil { return "绑定失败" } err = tx.Commit() if err != nil { return "无法提交事务" } return "和用户名为 " + data.Name + " 的用户绑定成功,输入\"查ss\"查看个人数据" } func (ss *ssQuery) UnbindSS(qqId string) (reply string) { tx, err := sqlite3.GetTran() if err != nil { log.Print(err) } defer tx.Rollback() //是否已绑定 if rows, err := tx.Query("SELECT * FROM ssBind WHERE qqid = ?", qqId); err == nil { if !rows.Next() { return "您未绑定ss账号,输入\"绑定ss [ssId]\"绑定" } rows.Close() } _, err = tx.Exec("DELETE FROM ssBind WHERE qqid = ?", qqId) if err != nil { return "解绑失败" } err = tx.Commit() if err != nil { return "无法提交事务" } return "解绑成功,重新绑定请输入\"绑定ss [ssId]\"" } func (ss *ssQuery) GetScore(ssId string) (reply string, err error) { db := sqlite3.GetDB() // 假设 sqlite3.GetDB() 返回 *sqlx.DB tx, err := db.Beginx() if err != nil { log.Print(err) return "数据库连接失败,请稍后重试", err } defer tx.Rollback() // 查询玩家数据 data, err := FetchPlayerData(ssId) if data == nil { return "查询出错,报错如下" + err.Error(), errors.New("查询出错,报错如下" + err.Error()) } // 构建 PlayerDataLite 结构体 dataLite := PlayerDataLite{ ID: data.ID, Name: data.Name, Country: data.Country, PP: data.PP, Rank: data.Rank, CountryRank: data.CountryRank, TotalScore: data.ScoreStats.TotalScore, TotalRankedScore: data.ScoreStats.TotalRankedScore, AverageRankedAccuracy: data.ScoreStats.AverageRankedAccuracy, TotalPlayCount: data.ScoreStats.TotalPlayCount, RankedPlayCount: data.ScoreStats.RankedPlayCount, ReplaysWatched: data.ScoreStats.ReplaysWatched, GeneratedTime: time.Now().Format("2006-01-02 15:04:05.999999999-07:00"), } // 查询最近的玩家数据 var lastDataLite PlayerDataLite err = tx.Get(&lastDataLite, "SELECT * FROM ssData WHERE id = ? ORDER BY generated_time DESC LIMIT 1", dataLite.ID) if err != nil && err != sql.ErrNoRows { log.Print(err) return "查询历史数据时出错", err } // 如果有新的数据,则插入 if lastDataLite.IsDiffFrom(dataLite) { _, err = tx.NamedExec(`INSERT INTO ssData (id, name, country, pp, rank, country_rank, total_score, total_ranked_score, average_ranked_accuracy, total_play_count, ranked_play_count, replays_watched, generated_time) VALUES (:id, :name, :country, :pp, :rank, :country_rank, :total_score, :total_ranked_score, :average_ranked_accuracy, :total_play_count, :ranked_play_count, :replays_watched, :generated_time)`, dataLite) if err != nil { log.Print(err) return "插入新数据时出错", err } // 提交事务 err = tx.Commit() if err != nil { log.Print(err) return "SQL事务提交失败,请重试", err } // 返回差异信息 return data.LastDiffToString(lastDataLite), nil } // 如果没有新数据,直接提交事务 err = tx.Commit() if err != nil { log.Print(err) return "SQL事务提交失败,请重试", err } // 返回当前数据的字符串表示 return data.ToString(), nil } func (ss *ssQuery) GetScoreWithoutUpdate(ssId string) (reply string, err error) { // 查询玩家数据 data, err := FetchPlayerData(ssId) if data == nil { return "查询出错,报错如下" + err.Error(), errors.New("查询出错,报错如下" + err.Error()) } // 返回当前数据的字符串表示 return data.ToString(), nil } func (ss *ssQuery) SaveRecord(cmdData CommandData) { db := sqlite3.GetDB() // 假设 sqlite3.GetDB() 返回 *sqlx.DB tx, err := db.Beginx() if err != nil { log.Print(err) return } defer tx.Rollback() // 创建 RecordDataLite 结构体实例 dataLite := RecordDataLite{ ScoreID: cmdData.Score.ID, SsID: cmdData.Score.LeaderboardPlayerInfo.ID, Name: cmdData.Score.LeaderboardPlayerInfo.Name, Country: cmdData.Score.LeaderboardPlayerInfo.Country, SongName: cmdData.Leaderboard.SongName, SongSubName: cmdData.Leaderboard.SongSubName, SongAuthorName: cmdData.Leaderboard.SongAuthorName, SongHash: cmdData.Leaderboard.SongHash, SongId: "", CoverImage: cmdData.Leaderboard.CoverImage, DifficultyRaw: cmdData.Leaderboard.Difficulty.DifficultyRaw, Stars: cmdData.Leaderboard.Stars, PP: cmdData.Score.Pp, Weight: cmdData.Score.Weight, Modifiers: cmdData.Score.Modifiers, Multiplier: cmdData.Score.Multiplier, Rank: cmdData.Score.Rank, BadCuts: cmdData.Score.BadCuts, Score: cmdData.Score.ModifiedScore, MaxScore: cmdData.Leaderboard.MaxScore, MissedNotes: cmdData.Score.MissedNotes, MaxCombo: cmdData.Score.MaxCombo, FullCombo: cmdData.Score.FullCombo, DeviceHmd: "", DeviceControllerLeft: "", DeviceControllerRight: "", GeneratedTime: time.Now().Format("2006-01-02 15:04:05.999999999-07:00"), } // 检查设备信息并设置 if cmdData.Score.DeviceHmd != nil { dataLite.DeviceHmd = *cmdData.Score.DeviceHmd } if cmdData.Score.DeviceControllerLeft != nil { dataLite.DeviceControllerLeft = *cmdData.Score.DeviceControllerLeft } if cmdData.Score.DeviceControllerRight != nil { dataLite.DeviceControllerRight = *cmdData.Score.DeviceControllerRight } // 使用 NamedExec 插入数据 _, err = tx.NamedExec(`INSERT INTO ssRecordData (score_id, ss_id, name, country, song_name, song_sub_name, song_author_name, song_hash, cover_image, difficulty_raw, pp, stars, weight, modifiers, multiplier,rank, bad_cuts, missed_notes, max_combo, score, max_score, full_combo, device_hmd, device_controller_left, device_controller_right, generated_time) VALUES (:score_id, :ss_id, :name, :country, :song_name, :song_sub_name, :song_author_name, :song_hash, :cover_image, :difficulty_raw, :pp, :stars, :weight, :modifiers, :multiplier, :rank,:bad_cuts, :missed_notes, :max_combo, :score, :max_score, :full_combo, :device_hmd, :device_controller_left, :device_controller_right, :generated_time)`, dataLite) if err != nil { log.Print(err) return } // 提交事务 err = tx.Commit() if err != nil { log.Print(err) } } func (ss *ssQuery) GetRecentScores(count int, qqId string) ([]RecordDataLite, error) { ssId, err := GetSSID(qqId) if err != nil { return nil, err } playerData, err := FetchPlayerData(ssId) if err != nil { return nil, err } historyUrl := "https://scoresaber.com/api/player/%s/scores?page=1&sort=recent" fullUrl := fmt.Sprintf(historyUrl, ssId) resp, err := http.Get(fullUrl) if err != nil { return nil, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } var response struct { Data []struct { Score Score `json:"score"` Leaderboard Leaderboard `json:"leaderboard"` } `json:"playerScores"` } err = json.Unmarshal(body, &response) if err != nil { return nil, err } scores := response.Data records := make([]RecordDataLite, 0) for k, score := range scores { if k > count { break } record := RecordDataLite{ ScoreID: score.Score.ID, SsID: ssId, Name: playerData.Name, Country: playerData.Country, SongName: score.Leaderboard.SongName, SongSubName: score.Leaderboard.SongSubName, SongAuthorName: score.Leaderboard.SongAuthorName, SongHash: score.Leaderboard.SongHash, SongId: "", CoverImage: score.Leaderboard.CoverImage, DifficultyRaw: score.Leaderboard.Difficulty.DifficultyRaw, Stars: score.Leaderboard.Stars, PP: score.Score.Pp, Weight: score.Score.Weight, Modifiers: score.Score.Modifiers, Multiplier: score.Score.Multiplier, Rank: score.Score.Rank, BadCuts: score.Score.BadCuts, Score: score.Score.ModifiedScore, MaxScore: score.Leaderboard.MaxScore, FullCombo: score.Score.FullCombo, DeviceHmd: "", DeviceControllerLeft: "", DeviceControllerRight: "", GeneratedTime: score.Score.TimeSet.Format("2006-01-02 15:04:05.999999999-07:00"), } // 检查设备信息并设置 if score.Score.DeviceHmd != nil { record.DeviceHmd = *score.Score.DeviceHmd } if score.Score.DeviceControllerLeft != nil { record.DeviceControllerLeft = *score.Score.DeviceControllerLeft } if score.Score.DeviceControllerRight != nil { record.DeviceControllerRight = *score.Score.DeviceControllerRight } records = append(records, record) } // 获取歌曲ID hashs := make([]string, 0) for _, record := range records { hashs = append(hashs, record.SongHash) } hashToSongId, err := GetSongIdsByHash(hashs) if err != nil { return nil, err } for i := 0; i < len(records); i++ { records[i].SongId = hashToSongId[records[i].SongHash] } return records, nil } func GetSongIdsByHash(hashs []string) (hashToSongId map[string]string, err error) { //每批最多49个 hashToSongId = make(map[string]string) batchSize := 49 for i := 0; i < len(hashs); i += batchSize { end := i + batchSize if end > len(hashs) { end = len(hashs) } batchHashs := hashs[i:end] queryUrl := "https://api.beatsaver.com/maps/hash/" + strings.Join(batchHashs, ",") resp, err := http.Get(queryUrl) if err != nil { return nil, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } var response map[string]struct { ID string `json:"id"` } err = json.Unmarshal(body, &response) if err != nil { return nil, err } for hash, data := range response { hashToSongId[hash] = data.ID } } return hashToSongId, nil }