diff --git a/assets/main.db b/assets/main.db index 5dc965d..43d4ec8 100644 Binary files a/assets/main.db and b/assets/main.db differ diff --git a/internal/handler/achievement/pagingaccomplishedlist.go b/internal/handler/achievement/pagingaccomplishedlist.go new file mode 100644 index 0000000..3612137 --- /dev/null +++ b/internal/handler/achievement/pagingaccomplishedlist.go @@ -0,0 +1,45 @@ +package achievement + +import ( + apiachievement "honoka-chan/internal/handler/api/achievement" + "honoka-chan/internal/middleware" + "honoka-chan/internal/router" + achievementschema "honoka-chan/internal/schema/achievement" + "honoka-chan/internal/session" + honokautils "honoka-chan/internal/utils" + + "github.com/gin-gonic/gin" +) + +const pagingAccomplishedPageSize = 10 + +func pagingAccomplishedList(ctx *gin.Context) { + ss := session.Get(ctx) + defer ss.Finalize() + + req := achievementschema.PagingAccomplishedListReq{} + err := honokautils.ParseRequestData(ctx, &req) + if ss.CheckErr(err) { + return + } + + grouped, err := apiachievement.ListVisibleAccomplishedAchievements(ss) + if ss.CheckErr(err) { + return + } + + items := grouped[req.FilterCategoryID] + start := min(max(req.FromCount, 0), len(items)) + + end := min(start+pagingAccomplishedPageSize, len(items)) + + ss.Respond(achievementschema.PagingAccomplishedListResp{ + ResponseData: items[start:end], + ReleaseInfo: []any{}, + StatusCode: 200, + }) +} + +func init() { + router.AddHandler("main.php", "POST", "/achievement/pagingAccomplishedList", middleware.Common, pagingAccomplishedList) +} diff --git a/internal/handler/api/achievement/achievement.go b/internal/handler/api/achievement/achievement.go new file mode 100644 index 0000000..fd6161f --- /dev/null +++ b/internal/handler/api/achievement/achievement.go @@ -0,0 +1,30 @@ +package achievement + +import ( + honokautils "honoka-chan/internal/utils" + + "github.com/gin-gonic/gin" +) + +var achievementFilterCategoryList = []int{ + 1, // 重要 + 3, // 挑战 + 4, // 每日 + 5, // 期间限定 + 6, // 8周年限定/全世界5000万人突破纪念 + 7, // 回忆画廊 + 8, // 9周年限定 + 9, // 连携课题 +} + +func AchievementApi(ctx *gin.Context, action string) (res any, err error) { + switch action { + case "unaccomplishList": + res, err = unaccomplishList() + case "initialAccomplishedList": + res, err = initialAccomplishedList(ctx) + default: + err = honokautils.NewUnimplementedActionError("achievement", action) + } + return res, err +} diff --git a/internal/handler/api/achievement/common.go b/internal/handler/api/achievement/common.go new file mode 100644 index 0000000..3b2e71b --- /dev/null +++ b/internal/handler/api/achievement/common.go @@ -0,0 +1,118 @@ +package achievement + +import ( + "fmt" + achievementapischema "honoka-chan/internal/schema/api/achievement" + "honoka-chan/internal/session" + "sort" + "strings" + "time" +) + +type achievementRow struct { + AchievementID int `xorm:"achievement_id"` + AchievementFilterCategoryID int `xorm:"achievement_filter_category_id"` + DisplayFlag int `xorm:"display_flag"` + StartDate string + EndDate *string +} + +func ListVisibleAccomplishedAchievements(ss *session.Session) (map[int][]achievementapischema.AchievementListItem, error) { + var achievementList []achievementRow + err := ss.MainEng.Table("achievement_m"). + Cols("achievement_id,achievement_filter_category_id,display_flag,start_date,end_date"). + Where("display_flag = ?", 1). + Find(&achievementList) + if err != nil { + return nil, err + } + + now := time.Now() + grouped := make(map[int][]achievementapischema.AchievementListItem, len(achievementFilterCategoryList)) + for _, achievement := range achievementList { + if !isAchievementVisible(now, achievement.StartDate, achievement.EndDate) { + continue + } + + grouped[achievement.AchievementFilterCategoryID] = append( + grouped[achievement.AchievementFilterCategoryID], + achievementapischema.AchievementListItem{ + AchievementID: achievement.AchievementID, + Count: 1, + IsAccomplished: true, + InsertDate: normalizeAchievementDate(achievement.StartDate), + EndDate: normalizeAchievementDatePtr(achievement.EndDate), + RemainingTime: "", + IsNew: false, + ForDisplay: achievement.DisplayFlag != 0, + RewardList: []achievementapischema.AchievementRewardItem{ + { + AddType: 3001, + ItemID: 4, + Amount: 1, + }, + }, + }, + ) + } + + for filterCategoryID, items := range grouped { + sort.Slice(items, func(i, j int) bool { + return items[i].InsertDate > items[j].InsertDate + }) + grouped[filterCategoryID] = items + } + + return grouped, nil +} + +func normalizeAchievementDate(value string) string { + if value == "" { + return "1970-01-01 00:00:00" + } + + return strings.ReplaceAll(value, "/", "-") +} + +func normalizeAchievementDatePtr(value *string) *string { + if value == nil || *value == "" { + return nil + } + + normalized := normalizeAchievementDate(*value) + return &normalized +} + +func isAchievementVisible(now time.Time, startDate string, endDate *string) bool { + start, err := parseAchievementTime(startDate) + if err == nil && now.Before(start) { + return false + } + + if endDate == nil || *endDate == "" { + return true + } + + end, err := parseAchievementTime(*endDate) + if err != nil { + return true + } + + return !now.After(end) +} + +func parseAchievementTime(value string) (time.Time, error) { + layouts := []string{ + "2006/01/02 15:04:05", + "2006/1/2 15:04:05", + } + + for _, layout := range layouts { + parsed, err := time.ParseInLocation(layout, value, time.Local) + if err == nil { + return parsed, nil + } + } + + return time.Time{}, fmt.Errorf("invalid achievement time: %s", value) +} diff --git a/internal/handler/api/achievement/initialaccomplishedlist.go b/internal/handler/api/achievement/initialaccomplishedlist.go new file mode 100644 index 0000000..4fd4c5b --- /dev/null +++ b/internal/handler/api/achievement/initialaccomplishedlist.go @@ -0,0 +1,44 @@ +package achievement + +import ( + achievementapischema "honoka-chan/internal/schema/api/achievement" + "honoka-chan/internal/session" + "time" + + "github.com/gin-gonic/gin" +) + +func initialAccomplishedList(ctx *gin.Context) (res any, err error) { + ss := session.Get(ctx) + + grouped, err := ListVisibleAccomplishedAchievements(ss) + if err != nil { + return nil, err + } + + listData := make([]achievementapischema.UnaccomplishListData, 0, len(achievementFilterCategoryList)) + for _, filterCategoryID := range achievementFilterCategoryList { + achievementItems := grouped[filterCategoryID] + if achievementItems == nil { + achievementItems = []achievementapischema.AchievementListItem{} + } else { + if len(achievementItems) > 5 { + achievementItems = achievementItems[:5] + } + } + + listData = append(listData, achievementapischema.UnaccomplishListData{ + FilterCategoryID: filterCategoryID, + AchievementList: achievementItems, + Count: len(grouped[filterCategoryID]), + IsLast: true, + }) + } + + return achievementapischema.UnaccomplishListResp{ + Result: listData, + Status: 200, + CommandNum: false, + TimeStamp: time.Now().Unix(), + }, nil +} diff --git a/internal/handler/api/achievement/unaccomplishlist.go b/internal/handler/api/achievement/unaccomplishlist.go new file mode 100644 index 0000000..a78c7a0 --- /dev/null +++ b/internal/handler/api/achievement/unaccomplishlist.go @@ -0,0 +1,27 @@ +package achievement + +import ( + achievementapischema "honoka-chan/internal/schema/api/achievement" + "time" +) + +func unaccomplishList() (res any, err error) { + listData := make([]achievementapischema.UnaccomplishListData, 0, len(achievementFilterCategoryList)) + for i := range achievementFilterCategoryList { + listData = append(listData, achievementapischema.UnaccomplishListData{ + FilterCategoryID: achievementFilterCategoryList[i], + AchievementList: []achievementapischema.AchievementListItem{}, + Count: 0, + IsLast: true, + }) + } + + res = achievementapischema.UnaccomplishListResp{ + Result: listData, + Status: 200, + CommandNum: false, + TimeStamp: time.Now().Unix(), + } + + return res, nil +} diff --git a/internal/handler/api/api.go b/internal/handler/api/api.go index 624300d..776b9ec 100644 --- a/internal/handler/api/api.go +++ b/internal/handler/api/api.go @@ -2,6 +2,7 @@ package api import ( "honoka-chan/internal/constant" + "honoka-chan/internal/handler/api/achievement" "honoka-chan/internal/handler/api/album" "honoka-chan/internal/handler/api/award" "honoka-chan/internal/handler/api/background" @@ -57,6 +58,8 @@ func api(ctx *gin.Context) { switch v.Module { case "album": result, err = album.AlbumApi(ctx, v.Action) + case "achievement": + result, err = achievement.AchievementApi(ctx, v.Action) case "award": result, err = award.AwardApi(ctx, v.Action) case "background": diff --git a/internal/handler/handler.go b/internal/handler/handler.go index f703198..e5b8458 100644 --- a/internal/handler/handler.go +++ b/internal/handler/handler.go @@ -1,6 +1,7 @@ package handler import ( + _ "honoka-chan/internal/handler/achievement" _ "honoka-chan/internal/handler/album" _ "honoka-chan/internal/handler/announce" _ "honoka-chan/internal/handler/api" diff --git a/internal/schema/achievement/pagingaccomplishedlist.go b/internal/schema/achievement/pagingaccomplishedlist.go new file mode 100644 index 0000000..3bee977 --- /dev/null +++ b/internal/schema/achievement/pagingaccomplishedlist.go @@ -0,0 +1,19 @@ +package achievementschema + +import achievementapischema "honoka-chan/internal/schema/api/achievement" + +type PagingAccomplishedListReq struct { + Module string `json:"module"` + Action string `json:"action"` + TimeStamp int64 `json:"timeStamp"` + Mgd int `json:"mgd"` + FilterCategoryID int `json:"filter_category_id"` + CommandNum string `json:"commandNum"` + FromCount int `json:"from_count"` +} + +type PagingAccomplishedListResp struct { + ResponseData []achievementapischema.AchievementListItem `json:"response_data"` + ReleaseInfo []any `json:"release_info"` + StatusCode int `json:"status_code"` +} diff --git a/internal/schema/api/achievement/unaccomplishlist.go b/internal/schema/api/achievement/unaccomplishlist.go new file mode 100644 index 0000000..809103e --- /dev/null +++ b/internal/schema/api/achievement/unaccomplishlist.go @@ -0,0 +1,33 @@ +package achievementapischema + +type AchievementRewardItem struct { + AddType int `json:"add_type"` + ItemID int `json:"item_id"` + Amount int `json:"amount"` +} + +type AchievementListItem struct { + AchievementID int `json:"achievement_id"` + Count int `json:"count"` + IsAccomplished bool `json:"is_accomplished"` + InsertDate string `json:"insert_date"` + EndDate *string `json:"end_date"` + RemainingTime string `json:"remaining_time"` + IsNew bool `json:"is_new"` + ForDisplay bool `json:"for_display"` + RewardList []AchievementRewardItem `json:"reward_list"` +} + +type UnaccomplishListData struct { + FilterCategoryID int `json:"filter_category_id"` + AchievementList []AchievementListItem `json:"achievement_list"` + Count int `json:"count"` + IsLast bool `json:"is_last"` +} + +type UnaccomplishListResp struct { + Result []UnaccomplishListData `json:"result"` + Status int `json:"status"` + CommandNum bool `json:"commandNum"` + TimeStamp int64 `json:"timeStamp"` +}