From f6466989c68b85c83513c7feac9ca966da74954c Mon Sep 17 00:00:00 2001 From: Sean Du Date: Sat, 6 Jun 2026 16:17:49 +0800 Subject: [PATCH] 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 --- internal/handler/api/live/common.go | 39 ++-- internal/handler/api/live/status.go | 14 +- internal/handler/api/profile/livecnt.go | 99 +++++++-- internal/handler/api/profile/profile.go | 2 +- internal/handler/live/reward.go | 54 ++++- internal/model/user/userlivegoal.go | 6 +- internal/model/user/userlivestatus.go | 16 ++ internal/schema/live/play.go | 4 + internal/session/live.go | 71 +++--- internal/session/livestatus.go | 281 ++++++++++++++++++++++++ internal/startup/database.go | 231 +++++++++++++++++++ 11 files changed, 731 insertions(+), 86 deletions(-) create mode 100644 internal/model/user/userlivestatus.go create mode 100644 internal/session/livestatus.go diff --git a/internal/handler/api/live/common.go b/internal/handler/api/live/common.go index e2dfba1..5ebb396 100644 --- a/internal/handler/api/live/common.go +++ b/internal/handler/api/live/common.go @@ -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 diff --git a/internal/handler/api/live/status.go b/internal/handler/api/live/status.go index a831837..20b6c38 100644 --- a/internal/handler/api/live/status.go +++ b/internal/handler/api/live/status.go @@ -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, diff --git a/internal/handler/api/profile/livecnt.go b/internal/handler/api/profile/livecnt.go index 6eb0cfe..9859101 100644 --- a/internal/handler/api/profile/livecnt.go +++ b/internal/handler/api/profile/livecnt.go @@ -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(), diff --git a/internal/handler/api/profile/profile.go b/internal/handler/api/profile/profile.go index ca2d0f5..7c53804 100644 --- a/internal/handler/api/profile/profile.go +++ b/internal/handler/api/profile/profile.go @@ -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: diff --git a/internal/handler/live/reward.go b/internal/handler/live/reward.go index 24bfcce..de36c71 100644 --- a/internal/handler/live/reward.go +++ b/internal/handler/live/reward.go @@ -42,6 +42,36 @@ func reward(ctx *gin.Context) { } totalScore := playRewardReq.ScoreSmile + playRewardReq.ScoreCool + playRewardReq.ScoreCute + beforeLiveStatus, afterLiveStatus, err := ss.UpdateUserLiveStatus(difficultyID, totalScore, playRewardReq.MaxCombo) + if ss.CheckErr(err) { + return + } + newGoalRewardIDs, err := ss.SyncUserLiveGoals(difficultyID, afterLiveStatus.HiScore, afterLiveStatus.HiComboCount, afterLiveStatus.ClearCnt) + if ss.CheckErr(err) { + return + } + + goalAccomplishedIDs := make([]any, 0, len(newGoalRewardIDs)) + goalRewards := make([]any, 0, len(newGoalRewardIDs)) + if len(newGoalRewardIDs) > 0 { + goalRewardRows, err := ss.GetLiveGoalRewardRowsByIDs(newGoalRewardIDs) + if ss.CheckErr(err) { + return + } + for _, goalRewardID := range newGoalRewardIDs { + goalAccomplishedIDs = append(goalAccomplishedIDs, goalRewardID) + } + for _, row := range goalRewardRows { + goalRewards = append(goalRewards, liveschema.PlayRewardList{ + ItemID: row.ItemID, + AddType: row.AddType, + Amount: row.Amount, + ItemCategoryID: row.ItemCategoryID, + RewardBoxFlag: false, + }) + } + } + playResp := liveschema.RewardResp{ ResponseData: liveschema.RewardData{ LiveInfo: []liveschema.RewardLiveInfo{ @@ -53,8 +83,8 @@ func reward(ctx *gin.Context) { }, }, TotalLove: 0, - IsHighScore: true, - HiScore: totalScore, + IsHighScore: totalScore >= beforeLiveStatus.HiScore, + HiScore: afterLiveStatus.HiScore, BaseRewardInfo: liveschema.BaseRewardInfo{ PlayerExp: 0, PlayerExpUnitMax: liveschema.PlayerExpUnitMax{ @@ -93,8 +123,8 @@ func reward(ctx *gin.Context) { }, }, GoalAccompInfo: liveschema.GoalAccompInfo{ - AchievedIds: []any{}, - Rewards: []any{}, + AchievedIds: goalAccomplishedIDs, + Rewards: goalRewards, }, SpecialRewardInfo: []any{}, EventInfo: []any{}, @@ -123,25 +153,25 @@ func reward(ctx *gin.Context) { StatusCode: 200, } - if playRewardReq.MaxCombo > liveInfo.SRankCombo { + if playRewardReq.MaxCombo >= liveInfo.SRankCombo { playResp.ResponseData.ComboRank = 1 - } else if playRewardReq.MaxCombo > liveInfo.ARankCombo { + } else if playRewardReq.MaxCombo >= liveInfo.ARankCombo { playResp.ResponseData.ComboRank = 2 - } else if playRewardReq.MaxCombo > liveInfo.BRankCombo { + } else if playRewardReq.MaxCombo >= liveInfo.BRankCombo { playResp.ResponseData.ComboRank = 3 - } else if playRewardReq.MaxCombo > liveInfo.CRankCombo { + } else if playRewardReq.MaxCombo >= liveInfo.CRankCombo { playResp.ResponseData.ComboRank = 4 } else { playResp.ResponseData.ComboRank = 5 } - if totalScore > liveInfo.SRankScore { + if totalScore >= liveInfo.SRankScore { playResp.ResponseData.Rank = 1 - } else if totalScore > liveInfo.ARankScore { + } else if totalScore >= liveInfo.ARankScore { playResp.ResponseData.Rank = 2 - } else if totalScore > liveInfo.BRankScore { + } else if totalScore >= liveInfo.BRankScore { playResp.ResponseData.Rank = 3 - } else if totalScore > liveInfo.CRankScore { + } else if totalScore >= liveInfo.CRankScore { playResp.ResponseData.Rank = 4 } else { playResp.ResponseData.Rank = 5 diff --git a/internal/model/user/userlivegoal.go b/internal/model/user/userlivegoal.go index f11814b..9a7a033 100644 --- a/internal/model/user/userlivegoal.go +++ b/internal/model/user/userlivegoal.go @@ -4,9 +4,11 @@ import "honoka-chan/internal/constant" type UserLiveGoal struct { ID int `xorm:"id pk autoincr"` - LiveDifficultyID int `xorm:"live_difficulty_id"` - UserID int `xorm:"user_id"` + LiveDifficultyID int `xorm:"live_difficulty_id index"` + UserID int `xorm:"user_id index"` + LiveGoalRewardID int `xorm:"live_goal_reward_id index"` GoalType constant.LiveGoalType `xorm:"goal_type"` + Rank int `xorm:"rank"` CompletedAt int64 `xorm:"completed_at"` } diff --git a/internal/model/user/userlivestatus.go b/internal/model/user/userlivestatus.go new file mode 100644 index 0000000..7ee9b26 --- /dev/null +++ b/internal/model/user/userlivestatus.go @@ -0,0 +1,16 @@ +package usermodel + +type UserLiveStatus struct { + ID int `xorm:"id pk autoincr"` + UserID int `xorm:"user_id unique(user_live_status_pair) index"` + LiveDifficultyID int `xorm:"live_difficulty_id unique(user_live_status_pair) index"` + HiScore int `xorm:"hi_score"` + HiComboCount int `xorm:"hi_combo_count"` + ClearCnt int `xorm:"clear_cnt"` + InsertDate int64 `xorm:"insert_date"` + UpdateDate int64 `xorm:"update_date"` +} + +func (UserLiveStatus) TableName() string { + return "user_live_status" +} diff --git a/internal/schema/live/play.go b/internal/schema/live/play.go index 1e415fc..c141064 100644 --- a/internal/schema/live/play.go +++ b/internal/schema/live/play.go @@ -39,6 +39,10 @@ type LiveInfo struct { BRankCombo int `json:"-"` CRankCombo int `json:"-"` SRankCombo int `json:"-"` + ARankComplete int `json:"-"` + BRankComplete int `json:"-"` + CRankComplete int `json:"-"` + SRankComplete int `json:"-"` AcFlag int `json:"ac_flag"` SwingFlag int `json:"swing_flag"` NotesList []NotesList `json:"notes_list"` diff --git a/internal/session/live.go b/internal/session/live.go index ef96d50..12d6286 100644 --- a/internal/session/live.go +++ b/internal/session/live.go @@ -1,7 +1,6 @@ package session import ( - "honoka-chan/internal/constant" usermodel "honoka-chan/internal/model/user" liveschema "honoka-chan/internal/schema/live" liverecordschema "honoka-chan/internal/schema/liverecord" @@ -64,13 +63,34 @@ func (ss *Session) GetLiveInfo(LiveDifficultyID int) (bool, *liveschema.LiveInfo BRankCombo int `xorm:"b_rank_combo"` CRankCombo int `xorm:"c_rank_combo"` SRankCombo int `xorm:"s_rank_combo"` + ARankComplete int `xorm:"a_rank_complete"` + BRankComplete int `xorm:"b_rank_complete"` + CRankComplete int `xorm:"c_rank_complete"` + SRankComplete int `xorm:"s_rank_complete"` AcFlag int `xorm:"ac_flag"` SwingFlag int `xorm:"swing_flag"` } has, err := ss.MainEng.Table("special_live_m").Alias("a"). Join("LEFT", "live_setting_m", "a.live_setting_id = live_setting_m.live_setting_id"). Where("live_difficulty_id = ?", LiveDifficultyID). - Cols("a.live_difficulty_id,live_setting_m.*").Get(&info) + Select(` + a.live_difficulty_id, + a.c_rank_complete, + a.b_rank_complete, + a.a_rank_complete, + a.s_rank_complete, + live_setting_m.notes_setting_asset, + live_setting_m.a_rank_score, + live_setting_m.b_rank_score, + live_setting_m.c_rank_score, + live_setting_m.s_rank_score, + live_setting_m.a_rank_combo, + live_setting_m.b_rank_combo, + live_setting_m.c_rank_combo, + live_setting_m.s_rank_combo, + live_setting_m.ac_flag, + live_setting_m.swing_flag + `).Get(&info) if ss.CheckErr(err) { return false, nil } @@ -79,7 +99,24 @@ func (ss *Session) GetLiveInfo(LiveDifficultyID int) (bool, *liveschema.LiveInfo has, err = ss.MainEng.Table("normal_live_m").Alias("a"). Join("LEFT", "live_setting_m", "a.live_setting_id = live_setting_m.live_setting_id"). Where("live_difficulty_id = ?", LiveDifficultyID). - Cols("a.live_difficulty_id,live_setting_m.*").Get(&info) + Select(` + a.live_difficulty_id, + a.c_rank_complete, + a.b_rank_complete, + a.a_rank_complete, + a.s_rank_complete, + live_setting_m.notes_setting_asset, + live_setting_m.a_rank_score, + live_setting_m.b_rank_score, + live_setting_m.c_rank_score, + live_setting_m.s_rank_score, + live_setting_m.a_rank_combo, + live_setting_m.b_rank_combo, + live_setting_m.c_rank_combo, + live_setting_m.s_rank_combo, + live_setting_m.ac_flag, + live_setting_m.swing_flag + `).Get(&info) if ss.CheckErr(err) { return false, nil } @@ -105,6 +142,10 @@ func (ss *Session) GetLiveInfo(LiveDifficultyID int) (bool, *liveschema.LiveInfo BRankCombo: info.BRankCombo, CRankCombo: info.CRankCombo, SRankCombo: info.SRankCombo, + ARankComplete: info.ARankComplete, + BRankComplete: info.BRankComplete, + CRankComplete: info.CRankComplete, + SRankComplete: info.SRankComplete, AcFlag: info.AcFlag, SwingFlag: info.SwingFlag, NotesList: []liveschema.NotesList{}, @@ -188,27 +229,3 @@ func (ss *Session) GetDeckInfo(deckID int) (bool, *liverecordschema.DeckInfo) { UnitList: unitData, } } - -func (ss *Session) GetLiveGoalList(LiveDifficultyID int) []constant.LiveGoalType { - var goal []constant.LiveGoalType - err := ss.MainEng.Table("live_goal_reward_m"). - Where("live_difficulty_id = ?", LiveDifficultyID).Cols("live_goal_type").Find(&goal) - if ss.CheckErr(err) { - return []constant.LiveGoalType{} - } - return goal -} - -func (ss *Session) GetUserLiveGoalList(LiveDifficultyID int) []constant.LiveGoalType { - var goal []constant.LiveGoalType - var goalData []usermodel.UserLiveGoal - err := ss.UserEng.Table(new(usermodel.UserLiveGoal)). - Where("user_id = ? AND live_difficulty_id = ?", ss.UserID, LiveDifficultyID).Find(&goalData) - if ss.CheckErr(err) { - return []constant.LiveGoalType{} - } - for _, g := range goalData { - goal = append(goal, g.GoalType) - } - return goal -} diff --git a/internal/session/livestatus.go b/internal/session/livestatus.go new file mode 100644 index 0000000..f1b26a4 --- /dev/null +++ b/internal/session/livestatus.go @@ -0,0 +1,281 @@ +package session + +import ( + "honoka-chan/internal/constant" + usermodel "honoka-chan/internal/model/user" + "sort" + "time" +) + +type liveGoalRewardRow struct { + LiveGoalRewardID int `xorm:"live_goal_reward_id"` + LiveDifficultyID int `xorm:"live_difficulty_id"` + LiveGoalType constant.LiveGoalType `xorm:"live_goal_type"` + Rank int `xorm:"rank"` + AddType int `xorm:"add_type"` + ItemID int `xorm:"item_id"` + ItemCategoryID int `xorm:"item_category_id"` + Amount int `xorm:"amount"` +} + +type LiveStatusSnapshot struct { + Status int + HiScore int + HiComboCount int + ClearCnt int + AchievedGoalIDList []int +} + +func liveRank(value int, cRank int, bRank int, aRank int, sRank int) int { + switch { + case value >= sRank: + return 1 + case value >= aRank: + return 2 + case value >= bRank: + return 3 + case value >= cRank: + return 4 + default: + return 5 + } +} + +func (ss *Session) GetUserLiveStatus(liveDifficultyID int) (bool, *usermodel.UserLiveStatus, error) { + status := usermodel.UserLiveStatus{} + has, err := ss.UserEng.Table(new(usermodel.UserLiveStatus)). + Where("user_id = ? AND live_difficulty_id = ?", ss.UserID, liveDifficultyID). + Get(&status) + if err != nil { + return false, nil, err + } + return has, &status, nil +} + +func (ss *Session) GetUserLiveStatusMap(liveDifficultyIDs []int) (map[int]usermodel.UserLiveStatus, error) { + result := map[int]usermodel.UserLiveStatus{} + if len(liveDifficultyIDs) == 0 { + return result, nil + } + + rows := []usermodel.UserLiveStatus{} + err := ss.UserEng.Table(new(usermodel.UserLiveStatus)). + In("live_difficulty_id", liveDifficultyIDs). + Where("user_id = ?", ss.UserID). + Find(&rows) + if err != nil { + return nil, err + } + + for _, row := range rows { + result[row.LiveDifficultyID] = row + } + return result, nil +} + +func (ss *Session) GetUserLiveGoalRewardIDMap(liveDifficultyIDs []int) (map[int][]int, error) { + result := map[int][]int{} + if len(liveDifficultyIDs) == 0 { + return result, nil + } + + rows := []usermodel.UserLiveGoal{} + err := ss.UserEng.Table(new(usermodel.UserLiveGoal)). + In("live_difficulty_id", liveDifficultyIDs). + Where("user_id = ?", ss.UserID). + Where("live_goal_reward_id > 0"). + OrderBy("live_goal_reward_id ASC"). + Find(&rows) + if err != nil { + return nil, err + } + + for _, row := range rows { + result[row.LiveDifficultyID] = append(result[row.LiveDifficultyID], row.LiveGoalRewardID) + } + return result, nil +} + +func (ss *Session) BuildLiveStatusSnapshotMap(liveDifficultyIDs []int) (map[int]LiveStatusSnapshot, error) { + statusMap, err := ss.GetUserLiveStatusMap(liveDifficultyIDs) + if err != nil { + return nil, err + } + goalMap, err := ss.GetUserLiveGoalRewardIDMap(liveDifficultyIDs) + if err != nil { + return nil, err + } + + result := make(map[int]LiveStatusSnapshot, len(liveDifficultyIDs)) + for _, liveDifficultyID := range liveDifficultyIDs { + status := LiveStatusSnapshot{ + Status: 1, + HiScore: 0, + HiComboCount: 0, + ClearCnt: 0, + AchievedGoalIDList: []int{}, + } + + if row, ok := statusMap[liveDifficultyID]; ok { + status.HiScore = row.HiScore + status.HiComboCount = row.HiComboCount + status.ClearCnt = row.ClearCnt + if row.ClearCnt > 0 { + status.Status = 2 + } + } + + if ids := goalMap[liveDifficultyID]; len(ids) > 0 { + status.AchievedGoalIDList = ids + } + + result[liveDifficultyID] = status + } + return result, nil +} + +func (ss *Session) UpdateUserLiveStatus(liveDifficultyID int, totalScore int, maxCombo int) (before usermodel.UserLiveStatus, after usermodel.UserLiveStatus, err error) { + now := time.Now().Unix() + has, current, err := ss.GetUserLiveStatus(liveDifficultyID) + if err != nil { + return before, after, err + } + if current == nil { + current = &usermodel.UserLiveStatus{} + } + + before = *current + if !has { + after = usermodel.UserLiveStatus{ + UserID: ss.UserID, + LiveDifficultyID: liveDifficultyID, + HiScore: totalScore, + HiComboCount: maxCombo, + ClearCnt: 1, + InsertDate: now, + UpdateDate: now, + } + _, err = ss.UserEng.Insert(&after) + return before, after, err + } + + after = before + if totalScore > after.HiScore { + after.HiScore = totalScore + } + if maxCombo > after.HiComboCount { + after.HiComboCount = maxCombo + } + after.ClearCnt++ + after.UpdateDate = now + + _, err = ss.UserEng.Table(new(usermodel.UserLiveStatus)). + Where("user_id = ? AND live_difficulty_id = ?", ss.UserID, liveDifficultyID). + Cols("hi_score", "hi_combo_count", "clear_cnt", "update_date"). + Update(&after) + return before, after, err +} + +func (ss *Session) GetAchievedLiveGoalRewardRows(liveDifficultyID int, hiScore int, hiComboCount int, clearCnt int) ([]liveGoalRewardRow, error) { + has, liveInfo := ss.GetLiveInfo(liveDifficultyID) + if !has || liveInfo == nil { + return []liveGoalRewardRow{}, nil + } + + rows := []liveGoalRewardRow{} + err := ss.MainEng.Table("live_goal_reward_m"). + Where("live_difficulty_id = ?", liveDifficultyID). + OrderBy("live_goal_type ASC, rank ASC, live_goal_reward_id ASC"). + Find(&rows) + if err != nil { + return nil, err + } + + scoreRank := liveRank(hiScore, liveInfo.CRankScore, liveInfo.BRankScore, liveInfo.ARankScore, liveInfo.SRankScore) + comboRank := liveRank(hiComboCount, liveInfo.CRankCombo, liveInfo.BRankCombo, liveInfo.ARankCombo, liveInfo.SRankCombo) + clearRank := liveRank(clearCnt, liveInfo.CRankComplete, liveInfo.BRankComplete, liveInfo.ARankComplete, liveInfo.SRankComplete) + + result := make([]liveGoalRewardRow, 0, len(rows)) + for _, row := range rows { + switch row.LiveGoalType { + case constant.LiveGoalTypeScore: + if scoreRank <= row.Rank { + result = append(result, row) + } + case constant.LiveGoalTypeCombo: + if comboRank <= row.Rank { + result = append(result, row) + } + case constant.LiveGoalTypeClear: + if clearRank <= row.Rank { + result = append(result, row) + } + } + } + return result, nil +} + +func (ss *Session) SyncUserLiveGoals(liveDifficultyID int, hiScore int, hiComboCount int, clearCnt int) ([]int, error) { + achievedRows, err := ss.GetAchievedLiveGoalRewardRows(liveDifficultyID, hiScore, hiComboCount, clearCnt) + if err != nil { + return nil, err + } + if len(achievedRows) == 0 { + return []int{}, nil + } + + existingRows := []usermodel.UserLiveGoal{} + err = ss.UserEng.Table(new(usermodel.UserLiveGoal)). + Where("user_id = ? AND live_difficulty_id = ?", ss.UserID, liveDifficultyID). + Where("live_goal_reward_id > 0"). + Find(&existingRows) + if err != nil { + return nil, err + } + + existing := make(map[int]struct{}, len(existingRows)) + for _, row := range existingRows { + existing[row.LiveGoalRewardID] = struct{}{} + } + + now := time.Now().Unix() + newlyAchieved := make([]int, 0) + for _, row := range achievedRows { + if _, ok := existing[row.LiveGoalRewardID]; ok { + continue + } + + _, err = ss.UserEng.Insert(&usermodel.UserLiveGoal{ + UserID: ss.UserID, + LiveDifficultyID: liveDifficultyID, + LiveGoalRewardID: row.LiveGoalRewardID, + GoalType: row.LiveGoalType, + Rank: row.Rank, + CompletedAt: now, + }) + if err != nil { + return nil, err + } + + newlyAchieved = append(newlyAchieved, row.LiveGoalRewardID) + } + + sort.Ints(newlyAchieved) + return newlyAchieved, nil +} + +func (ss *Session) GetLiveGoalRewardRowsByIDs(goalRewardIDs []int) ([]liveGoalRewardRow, error) { + if len(goalRewardIDs) == 0 { + return []liveGoalRewardRow{}, nil + } + + rows := []liveGoalRewardRow{} + err := ss.MainEng.Table("live_goal_reward_m"). + In("live_goal_reward_id", goalRewardIDs). + OrderBy("live_goal_reward_id ASC"). + Find(&rows) + if err != nil { + return nil, err + } + return rows, nil +} diff --git a/internal/startup/database.go b/internal/startup/database.go index 72b3127..d5fb8fe 100644 --- a/internal/startup/database.go +++ b/internal/startup/database.go @@ -1,12 +1,14 @@ package startup import ( + "honoka-chan/internal/constant" ghomemodel "honoka-chan/internal/model/ghome" loginmodel "honoka-chan/internal/model/login" unitmodel "honoka-chan/internal/model/unit" usermodel "honoka-chan/internal/model/user" "honoka-chan/pkg/db" "log" + "sort" "time" "xorm.io/xorm" @@ -25,6 +27,7 @@ func CreateTables() { db.UserEng.Sync2(new(usermodel.UserDeckUnit)) db.UserEng.Sync2(new(usermodel.UserKey)) db.UserEng.Sync2(new(usermodel.UserLiveGoal)) + db.UserEng.Sync2(new(usermodel.UserLiveStatus)) db.UserEng.Sync2(new(usermodel.UserLiveInProgress)) db.UserEng.Sync2(new(usermodel.UserLiveRecord)) db.UserEng.Sync2(new(usermodel.UserFriend)) @@ -35,6 +38,7 @@ func CreateTables() { db.UserEng.Sync2(new(usermodel.UserUnitSkillEquip)) MigrateUserPref() + MigrateUserLiveData() } func MigrateUserPref() { @@ -62,6 +66,233 @@ func MigrateUserPref() { } } +type liveGoalRewardMigrationRow struct { + LiveGoalRewardID int `xorm:"live_goal_reward_id"` + LiveDifficultyID int `xorm:"live_difficulty_id"` + LiveGoalType int `xorm:"live_goal_type"` + Rank int `xorm:"rank"` +} + +type liveGoalInfoMigrationRow struct { + LiveDifficultyID int `xorm:"live_difficulty_id"` + CRankScore int `xorm:"c_rank_score"` + BRankScore int `xorm:"b_rank_score"` + ARankScore int `xorm:"a_rank_score"` + SRankScore int `xorm:"s_rank_score"` + CRankCombo int `xorm:"c_rank_combo"` + BRankCombo int `xorm:"b_rank_combo"` + ARankCombo int `xorm:"a_rank_combo"` + SRankCombo int `xorm:"s_rank_combo"` + CRankComplete int `xorm:"c_rank_complete"` + BRankComplete int `xorm:"b_rank_complete"` + ARankComplete int `xorm:"a_rank_complete"` + SRankComplete int `xorm:"s_rank_complete"` +} + +func liveRankForMigration(value int, cRank int, bRank int, aRank int, sRank int) int { + switch { + case value >= sRank: + return 1 + case value >= aRank: + return 2 + case value >= bRank: + return 3 + case value >= cRank: + return 4 + default: + return 5 + } +} + +func MigrateUserLiveData() { + session := db.UserEng.NewSession() + defer session.Close() + + recordRows := []usermodel.UserLiveRecord{} + if err := session.Table(new(usermodel.UserLiveRecord)).Find(&recordRows); err != nil { + log.Fatalln("迁移 user_live_status 失败:", err.Error()) + } + + existingStatusRows := []usermodel.UserLiveStatus{} + if err := session.Table(new(usermodel.UserLiveStatus)).Find(&existingStatusRows); err != nil { + log.Fatalln("迁移 user_live_status 失败:", err.Error()) + } + + type liveStatusKey struct { + UserID int + LiveDifficultyID int + } + + statusMap := make(map[liveStatusKey]usermodel.UserLiveStatus, len(existingStatusRows)) + for _, row := range existingStatusRows { + statusMap[liveStatusKey{UserID: row.UserID, LiveDifficultyID: row.LiveDifficultyID}] = row + } + + now := time.Now().Unix() + for _, row := range recordRows { + key := liveStatusKey{UserID: row.UserID, LiveDifficultyID: row.LiveDifficultyID} + status, ok := statusMap[key] + if !ok { + status = usermodel.UserLiveStatus{ + UserID: row.UserID, + LiveDifficultyID: row.LiveDifficultyID, + HiScore: row.TotalScore, + HiComboCount: row.MaxCombo, + ClearCnt: 1, + InsertDate: now, + UpdateDate: now, + } + if _, err := session.Insert(&status); err != nil { + log.Fatalln("迁移 user_live_status 失败:", err.Error()) + } + statusMap[key] = status + continue + } + + updated := false + if row.TotalScore > status.HiScore { + status.HiScore = row.TotalScore + updated = true + } + if row.MaxCombo > status.HiComboCount { + status.HiComboCount = row.MaxCombo + updated = true + } + if status.ClearCnt <= 0 { + status.ClearCnt = 1 + updated = true + } + if updated { + status.UpdateDate = now + if _, err := session.Table(new(usermodel.UserLiveStatus)). + Where("user_id = ? AND live_difficulty_id = ?", status.UserID, status.LiveDifficultyID). + Cols("hi_score", "hi_combo_count", "clear_cnt", "update_date"). + Update(&status); err != nil { + log.Fatalln("迁移 user_live_status 失败:", err.Error()) + } + statusMap[key] = status + } + } + + liveGoalInfoMap := map[int]liveGoalInfoMigrationRow{} + loadLiveGoalInfoRows := func(table string) { + rows := []liveGoalInfoMigrationRow{} + err := db.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.c_rank_score, + setting.b_rank_score, + setting.a_rank_score, + setting.s_rank_score, + setting.c_rank_combo, + setting.b_rank_combo, + setting.a_rank_combo, + setting.s_rank_combo, + live.c_rank_complete, + live.b_rank_complete, + live.a_rank_complete, + live.s_rank_complete + `). + Find(&rows) + if err != nil { + log.Fatalln("迁移 user_live_goal 失败:", err.Error()) + } + for _, row := range rows { + liveGoalInfoMap[row.LiveDifficultyID] = row + } + } + loadLiveGoalInfoRows("normal_live_m") + loadLiveGoalInfoRows("special_live_m") + + goalRewardRows := []liveGoalRewardMigrationRow{} + if err := db.MainEng.Table("live_goal_reward_m"). + OrderBy("live_difficulty_id ASC, live_goal_type ASC, rank ASC, live_goal_reward_id ASC"). + Find(&goalRewardRows); err != nil { + log.Fatalln("迁移 user_live_goal 失败:", err.Error()) + } + + goalRewardMap := map[int][]liveGoalRewardMigrationRow{} + for _, row := range goalRewardRows { + goalRewardMap[row.LiveDifficultyID] = append(goalRewardMap[row.LiveDifficultyID], row) + } + + existingGoalRows := []usermodel.UserLiveGoal{} + if err := session.Table(new(usermodel.UserLiveGoal)). + Where("live_goal_reward_id > 0"). + Find(&existingGoalRows); err != nil { + log.Fatalln("迁移 user_live_goal 失败:", err.Error()) + } + + existingGoalMap := make(map[liveStatusKey]map[int]struct{}) + for _, row := range existingGoalRows { + key := liveStatusKey{UserID: row.UserID, LiveDifficultyID: row.LiveDifficultyID} + if existingGoalMap[key] == nil { + existingGoalMap[key] = map[int]struct{}{} + } + existingGoalMap[key][row.LiveGoalRewardID] = struct{}{} + } + + statusKeys := make([]liveStatusKey, 0, len(statusMap)) + for key := range statusMap { + statusKeys = append(statusKeys, key) + } + sort.Slice(statusKeys, func(i, j int) bool { + if statusKeys[i].UserID == statusKeys[j].UserID { + return statusKeys[i].LiveDifficultyID < statusKeys[j].LiveDifficultyID + } + return statusKeys[i].UserID < statusKeys[j].UserID + }) + + for _, key := range statusKeys { + status := statusMap[key] + liveInfo, ok := liveGoalInfoMap[key.LiveDifficultyID] + if !ok { + continue + } + + scoreRank := liveRankForMigration(status.HiScore, liveInfo.CRankScore, liveInfo.BRankScore, liveInfo.ARankScore, liveInfo.SRankScore) + comboRank := liveRankForMigration(status.HiComboCount, liveInfo.CRankCombo, liveInfo.BRankCombo, liveInfo.ARankCombo, liveInfo.SRankCombo) + clearRank := liveRankForMigration(status.ClearCnt, liveInfo.CRankComplete, liveInfo.BRankComplete, liveInfo.ARankComplete, liveInfo.SRankComplete) + + for _, reward := range goalRewardMap[key.LiveDifficultyID] { + achieved := false + switch reward.LiveGoalType { + case 1: + achieved = scoreRank <= reward.Rank + case 2: + achieved = comboRank <= reward.Rank + case 3: + achieved = clearRank <= reward.Rank + } + if !achieved { + continue + } + + if existingGoalMap[key] != nil { + if _, ok := existingGoalMap[key][reward.LiveGoalRewardID]; ok { + continue + } + } + + if _, err := session.Insert(&usermodel.UserLiveGoal{ + UserID: key.UserID, + LiveDifficultyID: key.LiveDifficultyID, + LiveGoalRewardID: reward.LiveGoalRewardID, + GoalType: constant.LiveGoalType(reward.LiveGoalType), + Rank: reward.Rank, + CompletedAt: now, + }); err != nil { + log.Fatalln("迁移 user_live_goal 失败:", err.Error()) + } + if existingGoalMap[key] == nil { + existingGoalMap[key] = map[int]struct{}{} + } + existingGoalMap[key][reward.LiveGoalRewardID] = struct{}{} + } + } +} + func LoadUnitData() { userEng = db.UserEng.NewSession() defer userEng.Close()