Implement achievement list

Signed-off-by: Sean Du <do4suki@gmail.com>
This commit is contained in:
2026-06-13 06:54:32 +08:00
parent 5c2d68f979
commit 17336de1b7
10 changed files with 320 additions and 0 deletions
BIN
View File
Binary file not shown.
@@ -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)
}
@@ -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
}
+118
View File
@@ -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)
}
@@ -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
}
@@ -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
}
+3
View File
@@ -2,6 +2,7 @@ package api
import ( import (
"honoka-chan/internal/constant" "honoka-chan/internal/constant"
"honoka-chan/internal/handler/api/achievement"
"honoka-chan/internal/handler/api/album" "honoka-chan/internal/handler/api/album"
"honoka-chan/internal/handler/api/award" "honoka-chan/internal/handler/api/award"
"honoka-chan/internal/handler/api/background" "honoka-chan/internal/handler/api/background"
@@ -57,6 +58,8 @@ func api(ctx *gin.Context) {
switch v.Module { switch v.Module {
case "album": case "album":
result, err = album.AlbumApi(ctx, v.Action) result, err = album.AlbumApi(ctx, v.Action)
case "achievement":
result, err = achievement.AchievementApi(ctx, v.Action)
case "award": case "award":
result, err = award.AwardApi(ctx, v.Action) result, err = award.AwardApi(ctx, v.Action)
case "background": case "background":
+1
View File
@@ -1,6 +1,7 @@
package handler package handler
import ( import (
_ "honoka-chan/internal/handler/achievement"
_ "honoka-chan/internal/handler/album" _ "honoka-chan/internal/handler/album"
_ "honoka-chan/internal/handler/announce" _ "honoka-chan/internal/handler/announce"
_ "honoka-chan/internal/handler/api" _ "honoka-chan/internal/handler/api"
@@ -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"`
}
@@ -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"`
}