Files
YumeMichi f6466989c6 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>
2026-06-06 16:25:06 +08:00

282 lines
7.6 KiB
Go

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
}