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)) result := make([]liveapischema.NormalLiveStatusList, 0, len(ids))
for _, id := range ids { for _, id := range ids {
snapshot := snapshotMap[id]
result = append(result, liveapischema.NormalLiveStatusList{ result = append(result, liveapischema.NormalLiveStatusList{
LiveDifficultyID: id, LiveDifficultyID: id,
Status: 1, Status: snapshot.Status,
HiScore: 0, HiScore: snapshot.HiScore,
HiComboCount: 0, HiComboCount: snapshot.HiComboCount,
ClearCnt: 0, ClearCnt: snapshot.ClearCnt,
AchievedGoalIDList: []int{}, AchievedGoalIDList: snapshot.AchievedGoalIDList,
}) })
} }
return result 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)) result := make([]liveapischema.SpecialLiveStatusList, 0, len(ids))
for _, id := range ids { for _, id := range ids {
snapshot := snapshotMap[id]
result = append(result, liveapischema.SpecialLiveStatusList{ result = append(result, liveapischema.SpecialLiveStatusList{
LiveDifficultyID: id, LiveDifficultyID: id,
Status: 1, Status: snapshot.Status,
HiScore: 0, HiScore: snapshot.HiScore,
HiComboCount: 0, HiComboCount: snapshot.HiComboCount,
ClearCnt: 0, ClearCnt: snapshot.ClearCnt,
AchievedGoalIDList: []int{}, AchievedGoalIDList: snapshot.AchievedGoalIDList,
}) })
} }
return result 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)) result := make([]liveapischema.TrainingLiveStatusList, 0, len(ids))
for _, id := range ids { for _, id := range ids {
snapshot := snapshotMap[id]
result = append(result, liveapischema.TrainingLiveStatusList{ result = append(result, liveapischema.TrainingLiveStatusList{
LiveDifficultyID: id, LiveDifficultyID: id,
Status: 1, Status: snapshot.Status,
HiScore: 0, HiScore: snapshot.HiScore,
HiComboCount: 0, HiComboCount: snapshot.HiComboCount,
ClearCnt: 0, ClearCnt: snapshot.ClearCnt,
AchievedGoalIDList: []int{}, AchievedGoalIDList: snapshot.AchievedGoalIDList,
}) })
} }
return result return result
+11 -3
View File
@@ -24,12 +24,20 @@ func liveStatus(ctx *gin.Context) (res any, err error) {
if err != nil { if err != nil {
return nil, err 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{ res = liveapischema.StatusResp{
Result: liveapischema.StatusData{ Result: liveapischema.StatusData{
NormalLiveStatusList: buildNormalLiveStatusList(normalIDs), NormalLiveStatusList: buildNormalLiveStatusList(normalIDs, statusSnapshotMap),
SpecialLiveStatusList: buildSpecialLiveStatusList(specialIDs), SpecialLiveStatusList: buildSpecialLiveStatusList(specialIDs, statusSnapshotMap),
TrainingLiveStatusList: buildTrainingLiveStatusList(trainingIDs), TrainingLiveStatusList: buildTrainingLiveStatusList(trainingIDs, statusSnapshotMap),
MarathonLiveStatusList: []any{}, MarathonLiveStatusList: []any{},
FreeLiveStatusList: []any{}, FreeLiveStatusList: []any{},
CanResumeLive: true, CanResumeLive: true,
+76 -23
View File
@@ -1,34 +1,87 @@
package profile package profile
import ( import (
usermodel "honoka-chan/internal/model/user"
profileapischema "honoka-chan/internal/schema/api/profile" profileapischema "honoka-chan/internal/schema/api/profile"
"honoka-chan/internal/session"
"sort"
"time" "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{ res = profileapischema.LiveCntResp{
Result: []profileapischema.LiveCntData{ Result: result,
{
Difficulty: 1,
ClearCnt: 315,
},
{
Difficulty: 2,
ClearCnt: 310,
},
{
Difficulty: 3,
ClearCnt: 314,
},
{
Difficulty: 4,
ClearCnt: 455,
},
{
Difficulty: 6,
ClearCnt: 233,
},
},
Status: 200, Status: 200,
CommandNum: false, CommandNum: false,
TimeStamp: time.Now().Unix(), 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": case "cardRanking":
res, err = cardRanking() res, err = cardRanking()
case "liveCnt": case "liveCnt":
res, err = liveCnt() res, err = liveCnt(ctx, targetUserID)
case "profileInfo": case "profileInfo":
res, err = profileInfo(ctx, targetUserID) res, err = profileInfo(ctx, targetUserID)
default: default:
+42 -12
View File
@@ -42,6 +42,36 @@ func reward(ctx *gin.Context) {
} }
totalScore := playRewardReq.ScoreSmile + playRewardReq.ScoreCool + playRewardReq.ScoreCute 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{ playResp := liveschema.RewardResp{
ResponseData: liveschema.RewardData{ ResponseData: liveschema.RewardData{
LiveInfo: []liveschema.RewardLiveInfo{ LiveInfo: []liveschema.RewardLiveInfo{
@@ -53,8 +83,8 @@ func reward(ctx *gin.Context) {
}, },
}, },
TotalLove: 0, TotalLove: 0,
IsHighScore: true, IsHighScore: totalScore >= beforeLiveStatus.HiScore,
HiScore: totalScore, HiScore: afterLiveStatus.HiScore,
BaseRewardInfo: liveschema.BaseRewardInfo{ BaseRewardInfo: liveschema.BaseRewardInfo{
PlayerExp: 0, PlayerExp: 0,
PlayerExpUnitMax: liveschema.PlayerExpUnitMax{ PlayerExpUnitMax: liveschema.PlayerExpUnitMax{
@@ -93,8 +123,8 @@ func reward(ctx *gin.Context) {
}, },
}, },
GoalAccompInfo: liveschema.GoalAccompInfo{ GoalAccompInfo: liveschema.GoalAccompInfo{
AchievedIds: []any{}, AchievedIds: goalAccomplishedIDs,
Rewards: []any{}, Rewards: goalRewards,
}, },
SpecialRewardInfo: []any{}, SpecialRewardInfo: []any{},
EventInfo: []any{}, EventInfo: []any{},
@@ -123,25 +153,25 @@ func reward(ctx *gin.Context) {
StatusCode: 200, StatusCode: 200,
} }
if playRewardReq.MaxCombo > liveInfo.SRankCombo { if playRewardReq.MaxCombo >= liveInfo.SRankCombo {
playResp.ResponseData.ComboRank = 1 playResp.ResponseData.ComboRank = 1
} else if playRewardReq.MaxCombo > liveInfo.ARankCombo { } else if playRewardReq.MaxCombo >= liveInfo.ARankCombo {
playResp.ResponseData.ComboRank = 2 playResp.ResponseData.ComboRank = 2
} else if playRewardReq.MaxCombo > liveInfo.BRankCombo { } else if playRewardReq.MaxCombo >= liveInfo.BRankCombo {
playResp.ResponseData.ComboRank = 3 playResp.ResponseData.ComboRank = 3
} else if playRewardReq.MaxCombo > liveInfo.CRankCombo { } else if playRewardReq.MaxCombo >= liveInfo.CRankCombo {
playResp.ResponseData.ComboRank = 4 playResp.ResponseData.ComboRank = 4
} else { } else {
playResp.ResponseData.ComboRank = 5 playResp.ResponseData.ComboRank = 5
} }
if totalScore > liveInfo.SRankScore { if totalScore >= liveInfo.SRankScore {
playResp.ResponseData.Rank = 1 playResp.ResponseData.Rank = 1
} else if totalScore > liveInfo.ARankScore { } else if totalScore >= liveInfo.ARankScore {
playResp.ResponseData.Rank = 2 playResp.ResponseData.Rank = 2
} else if totalScore > liveInfo.BRankScore { } else if totalScore >= liveInfo.BRankScore {
playResp.ResponseData.Rank = 3 playResp.ResponseData.Rank = 3
} else if totalScore > liveInfo.CRankScore { } else if totalScore >= liveInfo.CRankScore {
playResp.ResponseData.Rank = 4 playResp.ResponseData.Rank = 4
} else { } else {
playResp.ResponseData.Rank = 5 playResp.ResponseData.Rank = 5
+4 -2
View File
@@ -4,9 +4,11 @@ import "honoka-chan/internal/constant"
type UserLiveGoal struct { type UserLiveGoal struct {
ID int `xorm:"id pk autoincr"` ID int `xorm:"id pk autoincr"`
LiveDifficultyID int `xorm:"live_difficulty_id"` LiveDifficultyID int `xorm:"live_difficulty_id index"`
UserID int `xorm:"user_id"` UserID int `xorm:"user_id index"`
LiveGoalRewardID int `xorm:"live_goal_reward_id index"`
GoalType constant.LiveGoalType `xorm:"goal_type"` GoalType constant.LiveGoalType `xorm:"goal_type"`
Rank int `xorm:"rank"`
CompletedAt int64 `xorm:"completed_at"` CompletedAt int64 `xorm:"completed_at"`
} }
+16
View File
@@ -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"
}
+4
View File
@@ -39,6 +39,10 @@ type LiveInfo struct {
BRankCombo int `json:"-"` BRankCombo int `json:"-"`
CRankCombo int `json:"-"` CRankCombo int `json:"-"`
SRankCombo int `json:"-"` SRankCombo int `json:"-"`
ARankComplete int `json:"-"`
BRankComplete int `json:"-"`
CRankComplete int `json:"-"`
SRankComplete int `json:"-"`
AcFlag int `json:"ac_flag"` AcFlag int `json:"ac_flag"`
SwingFlag int `json:"swing_flag"` SwingFlag int `json:"swing_flag"`
NotesList []NotesList `json:"notes_list"` NotesList []NotesList `json:"notes_list"`
+44 -27
View File
@@ -1,7 +1,6 @@
package session package session
import ( import (
"honoka-chan/internal/constant"
usermodel "honoka-chan/internal/model/user" usermodel "honoka-chan/internal/model/user"
liveschema "honoka-chan/internal/schema/live" liveschema "honoka-chan/internal/schema/live"
liverecordschema "honoka-chan/internal/schema/liverecord" 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"` BRankCombo int `xorm:"b_rank_combo"`
CRankCombo int `xorm:"c_rank_combo"` CRankCombo int `xorm:"c_rank_combo"`
SRankCombo int `xorm:"s_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"` AcFlag int `xorm:"ac_flag"`
SwingFlag int `xorm:"swing_flag"` SwingFlag int `xorm:"swing_flag"`
} }
has, err := ss.MainEng.Table("special_live_m").Alias("a"). 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"). Join("LEFT", "live_setting_m", "a.live_setting_id = live_setting_m.live_setting_id").
Where("live_difficulty_id = ?", LiveDifficultyID). 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) { if ss.CheckErr(err) {
return false, nil 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"). 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"). Join("LEFT", "live_setting_m", "a.live_setting_id = live_setting_m.live_setting_id").
Where("live_difficulty_id = ?", LiveDifficultyID). 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) { if ss.CheckErr(err) {
return false, nil return false, nil
} }
@@ -105,6 +142,10 @@ func (ss *Session) GetLiveInfo(LiveDifficultyID int) (bool, *liveschema.LiveInfo
BRankCombo: info.BRankCombo, BRankCombo: info.BRankCombo,
CRankCombo: info.CRankCombo, CRankCombo: info.CRankCombo,
SRankCombo: info.SRankCombo, SRankCombo: info.SRankCombo,
ARankComplete: info.ARankComplete,
BRankComplete: info.BRankComplete,
CRankComplete: info.CRankComplete,
SRankComplete: info.SRankComplete,
AcFlag: info.AcFlag, AcFlag: info.AcFlag,
SwingFlag: info.SwingFlag, SwingFlag: info.SwingFlag,
NotesList: []liveschema.NotesList{}, NotesList: []liveschema.NotesList{},
@@ -188,27 +229,3 @@ func (ss *Session) GetDeckInfo(deckID int) (bool, *liverecordschema.DeckInfo) {
UnitList: unitData, 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
}
+281
View File
@@ -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
}
+231
View File
@@ -1,12 +1,14 @@
package startup package startup
import ( import (
"honoka-chan/internal/constant"
ghomemodel "honoka-chan/internal/model/ghome" ghomemodel "honoka-chan/internal/model/ghome"
loginmodel "honoka-chan/internal/model/login" loginmodel "honoka-chan/internal/model/login"
unitmodel "honoka-chan/internal/model/unit" unitmodel "honoka-chan/internal/model/unit"
usermodel "honoka-chan/internal/model/user" usermodel "honoka-chan/internal/model/user"
"honoka-chan/pkg/db" "honoka-chan/pkg/db"
"log" "log"
"sort"
"time" "time"
"xorm.io/xorm" "xorm.io/xorm"
@@ -25,6 +27,7 @@ func CreateTables() {
db.UserEng.Sync2(new(usermodel.UserDeckUnit)) db.UserEng.Sync2(new(usermodel.UserDeckUnit))
db.UserEng.Sync2(new(usermodel.UserKey)) db.UserEng.Sync2(new(usermodel.UserKey))
db.UserEng.Sync2(new(usermodel.UserLiveGoal)) db.UserEng.Sync2(new(usermodel.UserLiveGoal))
db.UserEng.Sync2(new(usermodel.UserLiveStatus))
db.UserEng.Sync2(new(usermodel.UserLiveInProgress)) db.UserEng.Sync2(new(usermodel.UserLiveInProgress))
db.UserEng.Sync2(new(usermodel.UserLiveRecord)) db.UserEng.Sync2(new(usermodel.UserLiveRecord))
db.UserEng.Sync2(new(usermodel.UserFriend)) db.UserEng.Sync2(new(usermodel.UserFriend))
@@ -35,6 +38,7 @@ func CreateTables() {
db.UserEng.Sync2(new(usermodel.UserUnitSkillEquip)) db.UserEng.Sync2(new(usermodel.UserUnitSkillEquip))
MigrateUserPref() MigrateUserPref()
MigrateUserLiveData()
} }
func MigrateUserPref() { 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() { func LoadUnitData() {
userEng = db.UserEng.NewSession() userEng = db.UserEng.NewSession()
defer userEng.Close() defer userEng.Close()