Persist live status and migrate legacy live data

- Add user_live_status to store hi score, hi combo count, and clear count per chart
- Expand user_live_goal to persist achieved live_goal_reward_id entries instead of only coarse goal types
- Update live reward handling to refresh live status and sync newly achieved live goals on every clear
- Return live status and achieved goal ids from stored user data in /api/live/status
- Aggregate profile liveCnt from stored user_live_status instead of hardcoded values
- Load complete-rank thresholds from live info for goal evaluation
- Migrate legacy user_live_record data into user_live_status and backfill achievable live goals on startup

Signed-off-by: Sean Du <do4suki@gmail.com>
This commit is contained in:
2026-06-06 16:25:06 +08:00
parent f8a0f2db13
commit f6466989c6
11 changed files with 731 additions and 86 deletions
+21 -18
View File
@@ -268,46 +268,49 @@ func buildRandomLiveList() []liveapischema.RandomLiveList {
}
}
func buildNormalLiveStatusList(ids []int) []liveapischema.NormalLiveStatusList {
func buildNormalLiveStatusList(ids []int, snapshotMap map[int]session.LiveStatusSnapshot) []liveapischema.NormalLiveStatusList {
result := make([]liveapischema.NormalLiveStatusList, 0, len(ids))
for _, id := range ids {
snapshot := snapshotMap[id]
result = append(result, liveapischema.NormalLiveStatusList{
LiveDifficultyID: id,
Status: 1,
HiScore: 0,
HiComboCount: 0,
ClearCnt: 0,
AchievedGoalIDList: []int{},
Status: snapshot.Status,
HiScore: snapshot.HiScore,
HiComboCount: snapshot.HiComboCount,
ClearCnt: snapshot.ClearCnt,
AchievedGoalIDList: snapshot.AchievedGoalIDList,
})
}
return result
}
func buildSpecialLiveStatusList(ids []int) []liveapischema.SpecialLiveStatusList {
func buildSpecialLiveStatusList(ids []int, snapshotMap map[int]session.LiveStatusSnapshot) []liveapischema.SpecialLiveStatusList {
result := make([]liveapischema.SpecialLiveStatusList, 0, len(ids))
for _, id := range ids {
snapshot := snapshotMap[id]
result = append(result, liveapischema.SpecialLiveStatusList{
LiveDifficultyID: id,
Status: 1,
HiScore: 0,
HiComboCount: 0,
ClearCnt: 0,
AchievedGoalIDList: []int{},
Status: snapshot.Status,
HiScore: snapshot.HiScore,
HiComboCount: snapshot.HiComboCount,
ClearCnt: snapshot.ClearCnt,
AchievedGoalIDList: snapshot.AchievedGoalIDList,
})
}
return result
}
func buildTrainingLiveStatusList(ids []int) []liveapischema.TrainingLiveStatusList {
func buildTrainingLiveStatusList(ids []int, snapshotMap map[int]session.LiveStatusSnapshot) []liveapischema.TrainingLiveStatusList {
result := make([]liveapischema.TrainingLiveStatusList, 0, len(ids))
for _, id := range ids {
snapshot := snapshotMap[id]
result = append(result, liveapischema.TrainingLiveStatusList{
LiveDifficultyID: id,
Status: 1,
HiScore: 0,
HiComboCount: 0,
ClearCnt: 0,
AchievedGoalIDList: []int{},
Status: snapshot.Status,
HiScore: snapshot.HiScore,
HiComboCount: snapshot.HiComboCount,
ClearCnt: snapshot.ClearCnt,
AchievedGoalIDList: snapshot.AchievedGoalIDList,
})
}
return result
+11 -3
View File
@@ -24,12 +24,20 @@ func liveStatus(ctx *gin.Context) (res any, err error) {
if err != nil {
return nil, err
}
allIDs := make([]int, 0, len(normalIDs)+len(specialIDs)+len(trainingIDs))
allIDs = append(allIDs, normalIDs...)
allIDs = append(allIDs, specialIDs...)
allIDs = append(allIDs, trainingIDs...)
statusSnapshotMap, err := ss.BuildLiveStatusSnapshotMap(allIDs)
if err != nil {
return nil, err
}
res = liveapischema.StatusResp{
Result: liveapischema.StatusData{
NormalLiveStatusList: buildNormalLiveStatusList(normalIDs),
SpecialLiveStatusList: buildSpecialLiveStatusList(specialIDs),
TrainingLiveStatusList: buildTrainingLiveStatusList(trainingIDs),
NormalLiveStatusList: buildNormalLiveStatusList(normalIDs, statusSnapshotMap),
SpecialLiveStatusList: buildSpecialLiveStatusList(specialIDs, statusSnapshotMap),
TrainingLiveStatusList: buildTrainingLiveStatusList(trainingIDs, statusSnapshotMap),
MarathonLiveStatusList: []any{},
FreeLiveStatusList: []any{},
CanResumeLive: true,
+76 -23
View File
@@ -1,34 +1,87 @@
package profile
import (
usermodel "honoka-chan/internal/model/user"
profileapischema "honoka-chan/internal/schema/api/profile"
"honoka-chan/internal/session"
"sort"
"time"
"github.com/gin-gonic/gin"
)
func liveCnt() (res any, err error) {
type profileLiveDifficultyRow struct {
LiveDifficultyID int `xorm:"live_difficulty_id"`
Difficulty int `xorm:"difficulty"`
}
func liveCnt(ctx *gin.Context, targetUserID int) (res any, err error) {
ss := session.Get(ctx)
targetPref, err := getTargetUserPref(ss, targetUserID)
if err != nil {
return nil, err
}
difficultyByLiveID := map[int]int{}
loadDifficultyRows := func(table string) error {
rows := []profileLiveDifficultyRow{}
err := ss.MainEng.Table(table).Alias("live").
Join("LEFT", "live_setting_m setting", "live.live_setting_id = setting.live_setting_id").
Select("live.live_difficulty_id, setting.difficulty").
Find(&rows)
if err != nil {
return err
}
for _, row := range rows {
difficultyByLiveID[row.LiveDifficultyID] = row.Difficulty
}
return nil
}
if err := loadDifficultyRows("normal_live_m"); err != nil {
return nil, err
}
if err := loadDifficultyRows("special_live_m"); err != nil {
return nil, err
}
statusRows := []usermodel.UserLiveStatus{}
err = ss.UserEng.Table(new(usermodel.UserLiveStatus)).
Where("user_id = ?", targetPref.UserID).
Where("clear_cnt > 0").
Cols("live_difficulty_id", "clear_cnt").
Find(&statusRows)
if err != nil {
return nil, err
}
difficultyCounts := map[int]int{}
for _, row := range statusRows {
difficulty, ok := difficultyByLiveID[row.LiveDifficultyID]
if !ok {
continue
}
if difficulty == 5 {
continue
}
difficultyCounts[difficulty]++
}
orderedDifficulties := []int{1, 2, 3, 4, 6}
result := make([]profileapischema.LiveCntData, 0, len(orderedDifficulties))
for _, difficulty := range orderedDifficulties {
result = append(result, profileapischema.LiveCntData{
Difficulty: difficulty,
ClearCnt: difficultyCounts[difficulty],
})
}
sort.Slice(result, func(i, j int) bool {
return result[i].Difficulty < result[j].Difficulty
})
res = profileapischema.LiveCntResp{
Result: []profileapischema.LiveCntData{
{
Difficulty: 1,
ClearCnt: 315,
},
{
Difficulty: 2,
ClearCnt: 310,
},
{
Difficulty: 3,
ClearCnt: 314,
},
{
Difficulty: 4,
ClearCnt: 455,
},
{
Difficulty: 6,
ClearCnt: 233,
},
},
Result: result,
Status: 200,
CommandNum: false,
TimeStamp: time.Now().Unix(),
+1 -1
View File
@@ -11,7 +11,7 @@ func ProfileApi(ctx *gin.Context, action string, targetUserID int) (res any, err
case "cardRanking":
res, err = cardRanking()
case "liveCnt":
res, err = liveCnt()
res, err = liveCnt(ctx, targetUserID)
case "profileInfo":
res, err = profileInfo(ctx, targetUserID)
default: