From 1619e44207bd20f80595b632a70584dfb6df8da7 Mon Sep 17 00:00:00 2001 From: Sean Du Date: Sat, 6 Jun 2026 18:17:06 +0800 Subject: [PATCH] Implement live ranking endpoint - Add /main.php/ranking/live backed by user_live_status hi scores - Parse live_difficulty_id from the client string payload - Accept empty or numeric page values for the first-page request format Signed-off-by: Sean Du --- internal/handler/handler.go | 1 + internal/handler/ranking/live.go | 231 +++++++++++++++++++++++++++++++ internal/schema/ranking/live.go | 71 ++++++++++ 3 files changed, 303 insertions(+) create mode 100644 internal/handler/ranking/live.go create mode 100644 internal/schema/ranking/live.go diff --git a/internal/handler/handler.go b/internal/handler/handler.go index 8a79932..74871cc 100644 --- a/internal/handler/handler.go +++ b/internal/handler/handler.go @@ -24,6 +24,7 @@ import ( _ "honoka-chan/internal/handler/payment" _ "honoka-chan/internal/handler/personalnotice" _ "honoka-chan/internal/handler/profile" + _ "honoka-chan/internal/handler/ranking" _ "honoka-chan/internal/handler/reward" _ "honoka-chan/internal/handler/scenario" _ "honoka-chan/internal/handler/secretbox" diff --git a/internal/handler/ranking/live.go b/internal/handler/ranking/live.go new file mode 100644 index 0000000..66e8ca3 --- /dev/null +++ b/internal/handler/ranking/live.go @@ -0,0 +1,231 @@ +package ranking + +import ( + "honoka-chan/internal/middleware" + unitmodel "honoka-chan/internal/model/unit" + usermodel "honoka-chan/internal/model/user" + "honoka-chan/internal/router" + profileapischema "honoka-chan/internal/schema/api/profile" + rankingschema "honoka-chan/internal/schema/ranking" + "honoka-chan/internal/session" + honokautils "honoka-chan/internal/utils" + "strconv" + + "github.com/gin-gonic/gin" +) + +const livePageSize = 20 + +type liveRankingRow struct { + UserID int `xorm:"user_id"` + HiScore int `xorm:"hi_score"` + UserName string `xorm:"user_name"` + UserLevel int `xorm:"user_level"` + AwardID int `xorm:"award_id"` + CenterUnitOwningUserID int `xorm:"center_unit_owning_user_id"` +} + +func live(ctx *gin.Context) { + ss := session.Get(ctx) + defer ss.Finalize() + + req := rankingschema.LiveReq{} + err := honokautils.ParseRequestData(ctx, &req) + if ss.CheckErr(err) { + return + } + + page := max(int(req.Page), 0) + + liveDifficultyID, err := strconv.Atoi(req.LiveDifficultyID) + if ss.CheckErr(err) { + return + } + + totalCnt, err := ss.UserEng.Table(new(usermodel.UserLiveStatus)). + Where("live_difficulty_id = ?", liveDifficultyID). + Where("clear_cnt > 0"). + Count() + if ss.CheckErr(err) { + return + } + + rows := []liveRankingRow{} + if totalCnt > 0 { + err = ss.UserEng.Table(new(usermodel.UserLiveStatus)).Alias("uls"). + Join("LEFT", "user_pref up", "up.user_id = uls.user_id"). + Join("LEFT", "user_deck ud", "ud.user_id = uls.user_id AND ud.main_flag = 1"). + Join("LEFT", "user_deck_unit udu", "udu.user_deck_id = ud.id AND udu.position = 5"). + Select(` + uls.user_id, + uls.hi_score, + up.user_name, + up.user_level, + up.award_id, + COALESCE(udu.unit_owning_user_id, up.unit_owning_user_id) AS center_unit_owning_user_id + `). + Where("uls.live_difficulty_id = ?", liveDifficultyID). + Where("uls.clear_cnt > 0"). + OrderBy("uls.hi_score DESC, uls.user_id ASC"). + Limit(livePageSize, page*livePageSize). + Find(&rows) + if ss.CheckErr(err) { + return + } + } + + items := make([]rankingschema.LiveItem, 0, len(rows)) + for i, row := range rows { + centerUnitInfo, err := buildLiveCenterUnitInfo(ss, row.UserID, row.CenterUnitOwningUserID) + if ss.CheckErr(err) { + return + } + + items = append(items, rankingschema.LiveItem{ + Rank: page*livePageSize + i + 1, + Score: row.HiScore, + UserData: rankingschema.UserData{ + UserID: row.UserID, + Name: row.UserName, + Level: row.UserLevel, + }, + CenterUnitInfo: centerUnitInfo, + SettingAwardID: row.AwardID, + }) + } + + ss.Respond(rankingschema.LiveResp{ + ResponseData: rankingschema.LiveData{ + Rank: nil, + Items: items, + TotalCnt: totalCnt, + PresentCnt: 0, + Page: page, + }, + ReleaseInfo: []any{}, + StatusCode: 200, + }) +} + +func buildLiveCenterUnitInfo(ss *session.Session, targetUserID, unitOwningUserID int) (profileapischema.CenterUnitInfo, error) { + info := profileapischema.CenterUnitInfo{ + RemovableSkillIds: []int{}, + } + if unitOwningUserID <= 0 { + return info, nil + } + + unitData := unitmodel.UnitDataMap{} + has, err := ss.GetBasicUnitInfo(). + Where("a.user_id = ?", targetUserID). + Where("a.unit_owning_user_id = ?", unitOwningUserID). + Get(&unitData) + if err != nil { + return info, err + } + if !has { + return info, nil + } + + accessoryInfo, err := getRankingAccessoryInfo(ss, targetUserID, unitOwningUserID) + if err != nil { + return info, err + } + + removableSkillIDs := []int{} + err = ss.UserEng.Table(new(usermodel.UserUnitSkillEquip)). + Where("user_id = ? AND unit_owning_user_id = ?", targetUserID, unitOwningUserID). + Cols("unit_removable_skill_id"). + Find(&removableSkillIDs) + if err != nil { + return info, err + } + + return profileapischema.CenterUnitInfo{ + UnitOwningUserID: unitData.UnitOwningUserID, + UnitID: unitData.UnitID, + Exp: unitData.Exp, + NextExp: 0, + Level: unitData.Level, + LevelLimitID: unitData.LevelLimitID, + MaxLevel: unitData.MaxLevel, + Love: unitData.Love, + Rank: unitData.Rank, + MaxRank: unitData.MaxRank, + MaxLove: unitData.MaxLove, + UnitSkillLevel: unitData.UnitSkillLevel, + MaxHp: unitData.MaxHp, + FavoriteFlag: unitData.FavoriteFlag, + DisplayRank: unitData.DisplayRank, + Attribute: unitData.Attribute, + Smile: unitData.Smile, + Cute: unitData.Cute, + Cool: unitData.Cool, + IsLoveMax: unitData.IsLoveMax, + IsRankMax: unitData.IsRankMax, + IsLevelMax: unitData.IsLevelMax, + IsSigned: unitData.IsSigned, + IsSkillLevelMax: unitData.IsSkillLevelMax, + UnitSkillExp: unitData.UnitSkillExp, + RemovableSkillIds: removableSkillIDs, + UnitRemovableSkillCapacity: unitData.UnitRemovableSkillCapacity, + AccessoryInfo: accessoryInfo, + Costume: profileapischema.Costume{ + UnitID: unitData.UnitID, + IsRankMax: unitData.IsRankMax, + IsSigned: unitData.IsSigned, + }, + TotalSmile: unitData.Smile, + TotalCute: unitData.Cute, + TotalCool: unitData.Cool, + TotalHp: unitData.MaxHp, + }, nil +} + +func getRankingAccessoryInfo(ss *session.Session, targetUserID, unitOwningUserID int) (*profileapischema.AccessoryInfo, error) { + if unitOwningUserID <= 0 { + return nil, nil + } + + var accessoryOwningID int + has, err := ss.UserEng.Table("user_accessory_wear"). + Where("unit_owning_user_id = ? AND user_id = ?", unitOwningUserID, targetUserID). + Cols("accessory_owning_user_id"). + Get(&accessoryOwningID) + if err != nil { + return nil, err + } + if !has || accessoryOwningID <= 0 { + return nil, nil + } + + accessoryData := struct { + AccessoryID int `xorm:"accessory_id"` + Exp int `xorm:"exp"` + }{} + has, err = ss.MainEng.Table("common_accessory_m"). + Where("accessory_owning_user_id = ?", accessoryOwningID). + Cols("accessory_id,exp"). + Get(&accessoryData) + if err != nil { + return nil, err + } + if !has { + return nil, nil + } + + return &profileapischema.AccessoryInfo{ + AccessoryOwningUserID: accessoryOwningID, + AccessoryID: accessoryData.AccessoryID, + Exp: accessoryData.Exp, + NextExp: 0, + Level: 8, + MaxLevel: 8, + RankUpCount: 4, + FavoriteFlag: true, + }, nil +} + +func init() { + router.AddHandler("main.php", "POST", "/ranking/live", middleware.Common, live) +} diff --git a/internal/schema/ranking/live.go b/internal/schema/ranking/live.go new file mode 100644 index 0000000..825df53 --- /dev/null +++ b/internal/schema/ranking/live.go @@ -0,0 +1,71 @@ +package rankingschema + +import ( + "encoding/json" + profileapischema "honoka-chan/internal/schema/api/profile" + "strconv" +) + +type OptionalPage int + +func (p *OptionalPage) UnmarshalJSON(data []byte) error { + var intValue int + if err := json.Unmarshal(data, &intValue); err == nil { + *p = OptionalPage(intValue) + return nil + } + + var stringValue string + if err := json.Unmarshal(data, &stringValue); err != nil { + return err + } + if stringValue == "" { + *p = 0 + return nil + } + + parsed, err := strconv.Atoi(stringValue) + if err != nil { + return err + } + *p = OptionalPage(parsed) + return nil +} + +type LiveReq struct { + Module string `json:"module"` + Action string `json:"action"` + Mgd int `json:"mgd"` + Page OptionalPage `json:"page"` + LiveDifficultyID string `json:"live_difficulty_id"` + TimeStamp int64 `json:"timeStamp"` + CommandNum string `json:"commandNum"` +} + +type UserData struct { + UserID int `json:"user_id"` + Name string `json:"name"` + Level int `json:"level"` +} + +type LiveItem struct { + Rank int `json:"rank"` + Score int `json:"score"` + UserData UserData `json:"user_data"` + CenterUnitInfo profileapischema.CenterUnitInfo `json:"center_unit_info"` + SettingAwardID int `json:"setting_award_id"` +} + +type LiveData struct { + Rank *int `json:"rank"` + Items []LiveItem `json:"items"` + TotalCnt int64 `json:"total_cnt"` + PresentCnt int64 `json:"present_cnt"` + Page int `json:"page"` +} + +type LiveResp struct { + ResponseData LiveData `json:"response_data"` + ReleaseInfo []any `json:"release_info"` + StatusCode int `json:"status_code"` +}