From 947da92a66f4243f13ab07a3c2e38d081eac3a58 Mon Sep 17 00:00:00 2001 From: Sean Du Date: Fri, 5 Jun 2026 03:33:17 +0800 Subject: [PATCH] webui: Add user birthday settings - Store birth month and day in user_pref with default 10/18 - Migrate existing user_pref rows to fill missing birthday fields - Expose birthday editing in the webui profile page - Replace hardcoded birthday values in user info, top info, and product list responses - Fix birthday month/day field alignment in the profile form Signed-off-by: Sean Du --- internal/handler/api/api.go | 2 +- internal/handler/api/login/login.go | 2 +- internal/handler/api/login/topinfo.go | 2 +- internal/handler/api/login/topinfoonce.go | 9 ++++- internal/handler/api/payment/payment.go | 10 +++-- internal/handler/api/payment/productlist.go | 9 ++++- internal/handler/api/user/info.go | 9 ++++- internal/handler/ghome/account/login.go | 2 + internal/handler/payment/productlist.go | 2 +- internal/handler/user/userinfo.go | 4 +- internal/handler/webui/profile.go | 20 +++++++++ internal/model/user/userpref.go | 45 ++++++++++++++++++++- internal/schema/api/user/info.go | 8 ++-- static/templates/admin/profile.html | 38 +++++++++++++++++ 14 files changed, 143 insertions(+), 19 deletions(-) diff --git a/internal/handler/api/api.go b/internal/handler/api/api.go index 4c9d0d2..20b6173 100644 --- a/internal/handler/api/api.go +++ b/internal/handler/api/api.go @@ -88,7 +88,7 @@ func api(ctx *gin.Context) { case "notice": result, err = notice.NoticeApi(v.Action) case "payment": - result, err = payment.PaymentApi(v.Action) + result, err = payment.PaymentApi(ctx, v.Action) case "profile": result, err = profile.ProfileApi(ctx, v.Action, v.UserID) case "scenario": diff --git a/internal/handler/api/login/login.go b/internal/handler/api/login/login.go index 1aeca3c..1ac476a 100644 --- a/internal/handler/api/login/login.go +++ b/internal/handler/api/login/login.go @@ -11,7 +11,7 @@ func LoginApi(ctx *gin.Context, action string) (res any, err error) { case "topInfo": res, err = loginTopInfo(ctx) case "topInfoOnce": - res, err = loginTopInfoOnce() + res, err = loginTopInfoOnce(ctx) default: err = fmt.Errorf("unimplemented action: login: %s", action) } diff --git a/internal/handler/api/login/topinfo.go b/internal/handler/api/login/topinfo.go index 95dc717..d6482ec 100644 --- a/internal/handler/api/login/topinfo.go +++ b/internal/handler/api/login/topinfo.go @@ -57,7 +57,7 @@ func loginTopInfo(ctx *gin.Context) (res any, err error) { NoticeMailDatetime: now.Format("2006-01-02 15:04:05"), FriendsApprovalWaitCnt: friendsApprovalWaitCnt, FriendsRequestCnt: friendsRequestCnt, - IsTodayBirthday: false, + IsTodayBirthday: ss.UserPref.IsBirthdayToday(now), LicenseInfo: loginapischema.LicenseInfo{ LicenseList: []any{}, LicensedInfo: []any{}, diff --git a/internal/handler/api/login/topinfoonce.go b/internal/handler/api/login/topinfoonce.go index f336a8b..d0af3ec 100644 --- a/internal/handler/api/login/topinfoonce.go +++ b/internal/handler/api/login/topinfoonce.go @@ -2,10 +2,15 @@ package login import ( loginapischema "honoka-chan/internal/schema/api/login" + "honoka-chan/internal/session" "time" + + "github.com/gin-gonic/gin" ) -func loginTopInfoOnce() (res any, err error) { +func loginTopInfoOnce(ctx *gin.Context) (res any, err error) { + ss := session.Get(ctx) + res = loginapischema.TopInfoOnceResp{ Result: loginapischema.TopInfoOnceData{ NewAchievementCnt: 0, @@ -22,7 +27,7 @@ func loginTopInfoOnce() (res any, err error) { Lbonus: false, Event: false, Secretbox: false, - Birthday: true, + Birthday: ss.UserPref.HasBirthDate(), }, OpenArena: true, CostumeStatus: true, diff --git a/internal/handler/api/payment/payment.go b/internal/handler/api/payment/payment.go index a2ca6cc..62b956a 100644 --- a/internal/handler/api/payment/payment.go +++ b/internal/handler/api/payment/payment.go @@ -1,11 +1,15 @@ package payment -import "fmt" +import ( + "fmt" -func PaymentApi(action string) (res any, err error) { + "github.com/gin-gonic/gin" +) + +func PaymentApi(ctx *gin.Context, action string) (res any, err error) { switch action { case "productList": - res, err = productList() + res, err = productList(ctx) default: err = fmt.Errorf("unimplemented action: payment: %s", action) } diff --git a/internal/handler/api/payment/productlist.go b/internal/handler/api/payment/productlist.go index a2e6fe5..436bedf 100644 --- a/internal/handler/api/payment/productlist.go +++ b/internal/handler/api/payment/productlist.go @@ -2,17 +2,22 @@ package payment import ( paymentapischema "honoka-chan/internal/schema/api/payment" + "honoka-chan/internal/session" "time" + + "github.com/gin-gonic/gin" ) -func productList() (res any, err error) { +func productList(ctx *gin.Context) (res any, err error) { + ss := session.Get(ctx) + res = paymentapischema.ProductListResp{ Result: paymentapischema.ProductListData{ RestrictionInfo: paymentapischema.RestrictionInfo{ Restricted: false, }, UnderAgeInfo: paymentapischema.UnderAgeInfo{ - BirthSet: false, + BirthSet: ss.UserPref.HasBirthDate(), HasLimit: false, LimitAmount: nil, MonthUsed: 0, diff --git a/internal/handler/api/user/info.go b/internal/handler/api/user/info.go index 975348e..3936b78 100644 --- a/internal/handler/api/user/info.go +++ b/internal/handler/api/user/info.go @@ -2,6 +2,7 @@ package user import ( userapischema "honoka-chan/internal/schema/api/user" + userschema "honoka-chan/internal/schema/user" "honoka-chan/internal/session" "time" @@ -12,7 +13,13 @@ func userInfo(ctx *gin.Context) (res any, err error) { ss := session.Get(ctx) res = userapischema.InfoResp{ - Result: ss.GetUserInfo(), + Result: userschema.UserInfoData{ + User: ss.GetUserInfo(), + Birth: userschema.Birth{ + BirthMonth: ss.UserPref.EffectiveBirthMonth(), + BirthDay: ss.UserPref.EffectiveBirthDay(), + }, + }, Status: 200, CommandNum: false, TimeStamp: time.Now().Unix(), diff --git a/internal/handler/ghome/account/login.go b/internal/handler/ghome/account/login.go index 219cb1d..331ba27 100644 --- a/internal/handler/ghome/account/login.go +++ b/internal/handler/ghome/account/login.go @@ -278,6 +278,8 @@ func addUser(dbSession *xorm.Session, phone, password string, isDefault bool) (g SnsCoin: usermodel.DefaultUserSnsCoin, EnergyMax: usermodel.DefaultUserEnergyMax, OverMaxEnergy: usermodel.DefaultUserOverMaxEnergy, + BirthMonth: usermodel.DefaultBirthMonth, + BirthDay: usermodel.DefaultBirthDay, ProfileVersion: usermodel.CurrentUserPrefProfileVersion, UpdateTime: time.Now().Unix(), } diff --git a/internal/handler/payment/productlist.go b/internal/handler/payment/productlist.go index 5a18999..bfaf23c 100644 --- a/internal/handler/payment/productlist.go +++ b/internal/handler/payment/productlist.go @@ -21,7 +21,7 @@ func productList(ctx *gin.Context) { Restricted: false, }, UnderAgeInfo: paymentapischema.UnderAgeInfo{ - BirthSet: true, + BirthSet: ss.UserPref.HasBirthDate(), HasLimit: false, LimitAmount: nil, MonthUsed: 0, diff --git a/internal/handler/user/userinfo.go b/internal/handler/user/userinfo.go index fdd5f5a..fbefd0e 100644 --- a/internal/handler/user/userinfo.go +++ b/internal/handler/user/userinfo.go @@ -18,8 +18,8 @@ func userInfo(ctx *gin.Context) { ResponseData: userschema.UserInfoData{ User: ss.GetUserInfo(), Birth: userschema.Birth{ - BirthMonth: 10, - BirthDay: 18, + BirthMonth: ss.UserPref.EffectiveBirthMonth(), + BirthDay: ss.UserPref.EffectiveBirthDay(), }, ServerTimestamp: time.Now().Unix(), }, diff --git a/internal/handler/webui/profile.go b/internal/handler/webui/profile.go index 4d5259b..a5e9d5c 100644 --- a/internal/handler/webui/profile.go +++ b/internal/handler/webui/profile.go @@ -89,6 +89,20 @@ func updateProfile(ctx *gin.Context) { ctx.JSON(http.StatusOK, webuischema.Msg{Code: 1, Message: "当前体力格式有误!"}) return } + birthMonth, err := parseInt("birth_month", 1) + if err != nil { + ctx.JSON(http.StatusOK, webuischema.Msg{Code: 1, Message: "生日月份格式有误!"}) + return + } + birthDay, err := parseInt("birth_day", 1) + if err != nil { + ctx.JSON(http.StatusOK, webuischema.Msg{Code: 1, Message: "生日日期格式有误!"}) + return + } + if !usermodel.IsValidBirthDate(birthMonth, birthDay) { + ctx.JSON(http.StatusOK, webuischema.Msg{Code: 1, Message: "生日日期无效!"}) + return + } pref := usermodel.UserPref{ UserName: userName, @@ -101,6 +115,8 @@ func updateProfile(ctx *gin.Context) { SnsCoin: snsCoin, EnergyMax: energyMax, OverMaxEnergy: overMaxEnergy, + BirthMonth: birthMonth, + BirthDay: birthDay, ProfileVersion: usermodel.CurrentUserPrefProfileVersion, UpdateTime: time.Now().Unix(), } @@ -117,6 +133,8 @@ func updateProfile(ctx *gin.Context) { "sns_coin", "energy_max", "over_max_energy", + "birth_month", + "birth_day", "profile_version", "update_time", ). @@ -163,6 +181,8 @@ func resetProfile(ctx *gin.Context) { "sns_coin", "energy_max", "over_max_energy", + "birth_month", + "birth_day", "profile_version", "update_time", ). diff --git a/internal/model/user/userpref.go b/internal/model/user/userpref.go index ff73016..b9a0776 100644 --- a/internal/model/user/userpref.go +++ b/internal/model/user/userpref.go @@ -3,9 +3,10 @@ package usermodel import ( "strconv" "strings" + "time" ) -const CurrentUserPrefProfileVersion = 1 +const CurrentUserPrefProfileVersion = 2 const ( DefaultAutoUserName = "音乃木坂学生" @@ -17,6 +18,8 @@ const ( DefaultUserSnsCoin = 10000 DefaultUserEnergyMax = 417 DefaultUserOverMaxEnergy = 0 + DefaultBirthMonth = 10 + DefaultBirthDay = 18 ) type UserPref struct { @@ -35,6 +38,8 @@ type UserPref struct { SnsCoin int `xorm:"sns_coin"` EnergyMax int `xorm:"energy_max"` OverMaxEnergy int `xorm:"over_max_energy"` + BirthMonth int `xorm:"birth_month"` + BirthDay int `xorm:"birth_day"` ProfileVersion int `xorm:"profile_version"` UpdateTime int64 `xorm:"update_time"` } @@ -64,6 +69,10 @@ func (u *UserPref) ApplyProfileDefaults() { if u.OverMaxEnergy < 0 { u.OverMaxEnergy = DefaultUserOverMaxEnergy } + if !IsValidBirthDate(u.BirthMonth, u.BirthDay) { + u.BirthMonth = DefaultBirthMonth + u.BirthDay = DefaultBirthDay + } u.ProfileVersion = CurrentUserPrefProfileVersion } @@ -77,6 +86,8 @@ func (u *UserPref) ResetProfileDefaults() { u.SnsCoin = DefaultUserSnsCoin u.EnergyMax = DefaultUserEnergyMax u.OverMaxEnergy = DefaultUserOverMaxEnergy + u.BirthMonth = DefaultBirthMonth + u.BirthDay = DefaultBirthDay if u.UserID > 0 { u.InviteCode = strconv.Itoa(u.UserID) } @@ -101,6 +112,36 @@ func (u UserPref) EffectiveCurrentEnergy() int { return u.EffectiveEnergyMax() } +func (u UserPref) EffectiveBirthMonth() int { + if IsValidBirthDate(u.BirthMonth, u.BirthDay) { + return u.BirthMonth + } + return DefaultBirthMonth +} + +func (u UserPref) EffectiveBirthDay() int { + if IsValidBirthDate(u.BirthMonth, u.BirthDay) { + return u.BirthDay + } + return DefaultBirthDay +} + +func (u UserPref) HasBirthDate() bool { + return IsValidBirthDate(u.BirthMonth, u.BirthDay) +} + +func (u UserPref) IsBirthdayToday(now time.Time) bool { + return int(now.Month()) == u.EffectiveBirthMonth() && now.Day() == u.EffectiveBirthDay() +} + +func IsValidBirthDate(month, day int) bool { + if month < 1 || month > 12 || day < 1 { + return false + } + maxDay := time.Date(2000, time.Month(month)+1, 0, 0, 0, 0, 0, time.UTC).Day() + return day <= maxDay +} + func UserPrefProfileColumns() []string { return []string{ "user_level", @@ -111,6 +152,8 @@ func UserPrefProfileColumns() []string { "sns_coin", "energy_max", "over_max_energy", + "birth_month", + "birth_day", "profile_version", } } diff --git a/internal/schema/api/user/info.go b/internal/schema/api/user/info.go index 7d82629..6eaaefa 100644 --- a/internal/schema/api/user/info.go +++ b/internal/schema/api/user/info.go @@ -3,8 +3,8 @@ package userapischema import userschema "honoka-chan/internal/schema/user" type InfoResp struct { - Result userschema.UserInfo `json:"result"` - Status int `json:"status"` - CommandNum bool `json:"commandNum"` - TimeStamp int64 `json:"timeStamp"` + Result userschema.UserInfoData `json:"result"` + Status int `json:"status"` + CommandNum bool `json:"commandNum"` + TimeStamp int64 `json:"timeStamp"` } diff --git a/static/templates/admin/profile.html b/static/templates/admin/profile.html index eb0f1f3..36a2cac 100644 --- a/static/templates/admin/profile.html +++ b/static/templates/admin/profile.html @@ -15,6 +15,22 @@ flex-wrap: wrap; } + .profile-birth-row { + display: flex; + gap: 10px; + align-items: center; + } + + .profile-birth-field { + flex: 1 1 0; + min-width: 0; + } + + .profile-birth-sep { + color: #666; + white-space: nowrap; + } + .profile-action-row .layui-btn+.layui-btn { margin-left: 0; } @@ -24,6 +40,10 @@ flex-direction: column; align-items: stretch; } + + .profile-birth-row { + gap: 8px; + } } @@ -93,6 +113,24 @@
填 0 时按体力上限返回给客户端。
+
+ +
+
+
+ +
+
+
+ +
+
+
+
格式为 月 / 日,默认 10 / 18。
+
+