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 <do4suki@gmail.com>
This commit is contained in:
@@ -88,7 +88,7 @@ func api(ctx *gin.Context) {
|
|||||||
case "notice":
|
case "notice":
|
||||||
result, err = notice.NoticeApi(v.Action)
|
result, err = notice.NoticeApi(v.Action)
|
||||||
case "payment":
|
case "payment":
|
||||||
result, err = payment.PaymentApi(v.Action)
|
result, err = payment.PaymentApi(ctx, v.Action)
|
||||||
case "profile":
|
case "profile":
|
||||||
result, err = profile.ProfileApi(ctx, v.Action, v.UserID)
|
result, err = profile.ProfileApi(ctx, v.Action, v.UserID)
|
||||||
case "scenario":
|
case "scenario":
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ func LoginApi(ctx *gin.Context, action string) (res any, err error) {
|
|||||||
case "topInfo":
|
case "topInfo":
|
||||||
res, err = loginTopInfo(ctx)
|
res, err = loginTopInfo(ctx)
|
||||||
case "topInfoOnce":
|
case "topInfoOnce":
|
||||||
res, err = loginTopInfoOnce()
|
res, err = loginTopInfoOnce(ctx)
|
||||||
default:
|
default:
|
||||||
err = fmt.Errorf("unimplemented action: login: %s", action)
|
err = fmt.Errorf("unimplemented action: login: %s", action)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ func loginTopInfo(ctx *gin.Context) (res any, err error) {
|
|||||||
NoticeMailDatetime: now.Format("2006-01-02 15:04:05"),
|
NoticeMailDatetime: now.Format("2006-01-02 15:04:05"),
|
||||||
FriendsApprovalWaitCnt: friendsApprovalWaitCnt,
|
FriendsApprovalWaitCnt: friendsApprovalWaitCnt,
|
||||||
FriendsRequestCnt: friendsRequestCnt,
|
FriendsRequestCnt: friendsRequestCnt,
|
||||||
IsTodayBirthday: false,
|
IsTodayBirthday: ss.UserPref.IsBirthdayToday(now),
|
||||||
LicenseInfo: loginapischema.LicenseInfo{
|
LicenseInfo: loginapischema.LicenseInfo{
|
||||||
LicenseList: []any{},
|
LicenseList: []any{},
|
||||||
LicensedInfo: []any{},
|
LicensedInfo: []any{},
|
||||||
|
|||||||
@@ -2,10 +2,15 @@ package login
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
loginapischema "honoka-chan/internal/schema/api/login"
|
loginapischema "honoka-chan/internal/schema/api/login"
|
||||||
|
"honoka-chan/internal/session"
|
||||||
"time"
|
"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{
|
res = loginapischema.TopInfoOnceResp{
|
||||||
Result: loginapischema.TopInfoOnceData{
|
Result: loginapischema.TopInfoOnceData{
|
||||||
NewAchievementCnt: 0,
|
NewAchievementCnt: 0,
|
||||||
@@ -22,7 +27,7 @@ func loginTopInfoOnce() (res any, err error) {
|
|||||||
Lbonus: false,
|
Lbonus: false,
|
||||||
Event: false,
|
Event: false,
|
||||||
Secretbox: false,
|
Secretbox: false,
|
||||||
Birthday: true,
|
Birthday: ss.UserPref.HasBirthDate(),
|
||||||
},
|
},
|
||||||
OpenArena: true,
|
OpenArena: true,
|
||||||
CostumeStatus: true,
|
CostumeStatus: true,
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
package payment
|
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 {
|
switch action {
|
||||||
case "productList":
|
case "productList":
|
||||||
res, err = productList()
|
res, err = productList(ctx)
|
||||||
default:
|
default:
|
||||||
err = fmt.Errorf("unimplemented action: payment: %s", action)
|
err = fmt.Errorf("unimplemented action: payment: %s", action)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,17 +2,22 @@ package payment
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
paymentapischema "honoka-chan/internal/schema/api/payment"
|
paymentapischema "honoka-chan/internal/schema/api/payment"
|
||||||
|
"honoka-chan/internal/session"
|
||||||
"time"
|
"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{
|
res = paymentapischema.ProductListResp{
|
||||||
Result: paymentapischema.ProductListData{
|
Result: paymentapischema.ProductListData{
|
||||||
RestrictionInfo: paymentapischema.RestrictionInfo{
|
RestrictionInfo: paymentapischema.RestrictionInfo{
|
||||||
Restricted: false,
|
Restricted: false,
|
||||||
},
|
},
|
||||||
UnderAgeInfo: paymentapischema.UnderAgeInfo{
|
UnderAgeInfo: paymentapischema.UnderAgeInfo{
|
||||||
BirthSet: false,
|
BirthSet: ss.UserPref.HasBirthDate(),
|
||||||
HasLimit: false,
|
HasLimit: false,
|
||||||
LimitAmount: nil,
|
LimitAmount: nil,
|
||||||
MonthUsed: 0,
|
MonthUsed: 0,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package user
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
userapischema "honoka-chan/internal/schema/api/user"
|
userapischema "honoka-chan/internal/schema/api/user"
|
||||||
|
userschema "honoka-chan/internal/schema/user"
|
||||||
"honoka-chan/internal/session"
|
"honoka-chan/internal/session"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -12,7 +13,13 @@ func userInfo(ctx *gin.Context) (res any, err error) {
|
|||||||
ss := session.Get(ctx)
|
ss := session.Get(ctx)
|
||||||
|
|
||||||
res = userapischema.InfoResp{
|
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,
|
Status: 200,
|
||||||
CommandNum: false,
|
CommandNum: false,
|
||||||
TimeStamp: time.Now().Unix(),
|
TimeStamp: time.Now().Unix(),
|
||||||
|
|||||||
@@ -278,6 +278,8 @@ func addUser(dbSession *xorm.Session, phone, password string, isDefault bool) (g
|
|||||||
SnsCoin: usermodel.DefaultUserSnsCoin,
|
SnsCoin: usermodel.DefaultUserSnsCoin,
|
||||||
EnergyMax: usermodel.DefaultUserEnergyMax,
|
EnergyMax: usermodel.DefaultUserEnergyMax,
|
||||||
OverMaxEnergy: usermodel.DefaultUserOverMaxEnergy,
|
OverMaxEnergy: usermodel.DefaultUserOverMaxEnergy,
|
||||||
|
BirthMonth: usermodel.DefaultBirthMonth,
|
||||||
|
BirthDay: usermodel.DefaultBirthDay,
|
||||||
ProfileVersion: usermodel.CurrentUserPrefProfileVersion,
|
ProfileVersion: usermodel.CurrentUserPrefProfileVersion,
|
||||||
UpdateTime: time.Now().Unix(),
|
UpdateTime: time.Now().Unix(),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ func productList(ctx *gin.Context) {
|
|||||||
Restricted: false,
|
Restricted: false,
|
||||||
},
|
},
|
||||||
UnderAgeInfo: paymentapischema.UnderAgeInfo{
|
UnderAgeInfo: paymentapischema.UnderAgeInfo{
|
||||||
BirthSet: true,
|
BirthSet: ss.UserPref.HasBirthDate(),
|
||||||
HasLimit: false,
|
HasLimit: false,
|
||||||
LimitAmount: nil,
|
LimitAmount: nil,
|
||||||
MonthUsed: 0,
|
MonthUsed: 0,
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ func userInfo(ctx *gin.Context) {
|
|||||||
ResponseData: userschema.UserInfoData{
|
ResponseData: userschema.UserInfoData{
|
||||||
User: ss.GetUserInfo(),
|
User: ss.GetUserInfo(),
|
||||||
Birth: userschema.Birth{
|
Birth: userschema.Birth{
|
||||||
BirthMonth: 10,
|
BirthMonth: ss.UserPref.EffectiveBirthMonth(),
|
||||||
BirthDay: 18,
|
BirthDay: ss.UserPref.EffectiveBirthDay(),
|
||||||
},
|
},
|
||||||
ServerTimestamp: time.Now().Unix(),
|
ServerTimestamp: time.Now().Unix(),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -89,6 +89,20 @@ func updateProfile(ctx *gin.Context) {
|
|||||||
ctx.JSON(http.StatusOK, webuischema.Msg{Code: 1, Message: "当前体力格式有误!"})
|
ctx.JSON(http.StatusOK, webuischema.Msg{Code: 1, Message: "当前体力格式有误!"})
|
||||||
return
|
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{
|
pref := usermodel.UserPref{
|
||||||
UserName: userName,
|
UserName: userName,
|
||||||
@@ -101,6 +115,8 @@ func updateProfile(ctx *gin.Context) {
|
|||||||
SnsCoin: snsCoin,
|
SnsCoin: snsCoin,
|
||||||
EnergyMax: energyMax,
|
EnergyMax: energyMax,
|
||||||
OverMaxEnergy: overMaxEnergy,
|
OverMaxEnergy: overMaxEnergy,
|
||||||
|
BirthMonth: birthMonth,
|
||||||
|
BirthDay: birthDay,
|
||||||
ProfileVersion: usermodel.CurrentUserPrefProfileVersion,
|
ProfileVersion: usermodel.CurrentUserPrefProfileVersion,
|
||||||
UpdateTime: time.Now().Unix(),
|
UpdateTime: time.Now().Unix(),
|
||||||
}
|
}
|
||||||
@@ -117,6 +133,8 @@ func updateProfile(ctx *gin.Context) {
|
|||||||
"sns_coin",
|
"sns_coin",
|
||||||
"energy_max",
|
"energy_max",
|
||||||
"over_max_energy",
|
"over_max_energy",
|
||||||
|
"birth_month",
|
||||||
|
"birth_day",
|
||||||
"profile_version",
|
"profile_version",
|
||||||
"update_time",
|
"update_time",
|
||||||
).
|
).
|
||||||
@@ -163,6 +181,8 @@ func resetProfile(ctx *gin.Context) {
|
|||||||
"sns_coin",
|
"sns_coin",
|
||||||
"energy_max",
|
"energy_max",
|
||||||
"over_max_energy",
|
"over_max_energy",
|
||||||
|
"birth_month",
|
||||||
|
"birth_day",
|
||||||
"profile_version",
|
"profile_version",
|
||||||
"update_time",
|
"update_time",
|
||||||
).
|
).
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ package usermodel
|
|||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const CurrentUserPrefProfileVersion = 1
|
const CurrentUserPrefProfileVersion = 2
|
||||||
|
|
||||||
const (
|
const (
|
||||||
DefaultAutoUserName = "音乃木坂学生"
|
DefaultAutoUserName = "音乃木坂学生"
|
||||||
@@ -17,6 +18,8 @@ const (
|
|||||||
DefaultUserSnsCoin = 10000
|
DefaultUserSnsCoin = 10000
|
||||||
DefaultUserEnergyMax = 417
|
DefaultUserEnergyMax = 417
|
||||||
DefaultUserOverMaxEnergy = 0
|
DefaultUserOverMaxEnergy = 0
|
||||||
|
DefaultBirthMonth = 10
|
||||||
|
DefaultBirthDay = 18
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserPref struct {
|
type UserPref struct {
|
||||||
@@ -35,6 +38,8 @@ type UserPref struct {
|
|||||||
SnsCoin int `xorm:"sns_coin"`
|
SnsCoin int `xorm:"sns_coin"`
|
||||||
EnergyMax int `xorm:"energy_max"`
|
EnergyMax int `xorm:"energy_max"`
|
||||||
OverMaxEnergy int `xorm:"over_max_energy"`
|
OverMaxEnergy int `xorm:"over_max_energy"`
|
||||||
|
BirthMonth int `xorm:"birth_month"`
|
||||||
|
BirthDay int `xorm:"birth_day"`
|
||||||
ProfileVersion int `xorm:"profile_version"`
|
ProfileVersion int `xorm:"profile_version"`
|
||||||
UpdateTime int64 `xorm:"update_time"`
|
UpdateTime int64 `xorm:"update_time"`
|
||||||
}
|
}
|
||||||
@@ -64,6 +69,10 @@ func (u *UserPref) ApplyProfileDefaults() {
|
|||||||
if u.OverMaxEnergy < 0 {
|
if u.OverMaxEnergy < 0 {
|
||||||
u.OverMaxEnergy = DefaultUserOverMaxEnergy
|
u.OverMaxEnergy = DefaultUserOverMaxEnergy
|
||||||
}
|
}
|
||||||
|
if !IsValidBirthDate(u.BirthMonth, u.BirthDay) {
|
||||||
|
u.BirthMonth = DefaultBirthMonth
|
||||||
|
u.BirthDay = DefaultBirthDay
|
||||||
|
}
|
||||||
u.ProfileVersion = CurrentUserPrefProfileVersion
|
u.ProfileVersion = CurrentUserPrefProfileVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,6 +86,8 @@ func (u *UserPref) ResetProfileDefaults() {
|
|||||||
u.SnsCoin = DefaultUserSnsCoin
|
u.SnsCoin = DefaultUserSnsCoin
|
||||||
u.EnergyMax = DefaultUserEnergyMax
|
u.EnergyMax = DefaultUserEnergyMax
|
||||||
u.OverMaxEnergy = DefaultUserOverMaxEnergy
|
u.OverMaxEnergy = DefaultUserOverMaxEnergy
|
||||||
|
u.BirthMonth = DefaultBirthMonth
|
||||||
|
u.BirthDay = DefaultBirthDay
|
||||||
if u.UserID > 0 {
|
if u.UserID > 0 {
|
||||||
u.InviteCode = strconv.Itoa(u.UserID)
|
u.InviteCode = strconv.Itoa(u.UserID)
|
||||||
}
|
}
|
||||||
@@ -101,6 +112,36 @@ func (u UserPref) EffectiveCurrentEnergy() int {
|
|||||||
return u.EffectiveEnergyMax()
|
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 {
|
func UserPrefProfileColumns() []string {
|
||||||
return []string{
|
return []string{
|
||||||
"user_level",
|
"user_level",
|
||||||
@@ -111,6 +152,8 @@ func UserPrefProfileColumns() []string {
|
|||||||
"sns_coin",
|
"sns_coin",
|
||||||
"energy_max",
|
"energy_max",
|
||||||
"over_max_energy",
|
"over_max_energy",
|
||||||
|
"birth_month",
|
||||||
|
"birth_day",
|
||||||
"profile_version",
|
"profile_version",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package userapischema
|
|||||||
import userschema "honoka-chan/internal/schema/user"
|
import userschema "honoka-chan/internal/schema/user"
|
||||||
|
|
||||||
type InfoResp struct {
|
type InfoResp struct {
|
||||||
Result userschema.UserInfo `json:"result"`
|
Result userschema.UserInfoData `json:"result"`
|
||||||
Status int `json:"status"`
|
Status int `json:"status"`
|
||||||
CommandNum bool `json:"commandNum"`
|
CommandNum bool `json:"commandNum"`
|
||||||
TimeStamp int64 `json:"timeStamp"`
|
TimeStamp int64 `json:"timeStamp"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,22 @@
|
|||||||
flex-wrap: wrap;
|
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 {
|
.profile-action-row .layui-btn+.layui-btn {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
@@ -24,6 +40,10 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.profile-birth-row {
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
@@ -93,6 +113,24 @@
|
|||||||
<div class="layui-form-mid layui-word-aux">填 0 时按体力上限返回给客户端。</div>
|
<div class="layui-form-mid layui-word-aux">填 0 时按体力上限返回给客户端。</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="layui-form-item">
|
||||||
|
<label class="layui-form-label">生日</label>
|
||||||
|
<div class="layui-input-block">
|
||||||
|
<div class="profile-birth-row">
|
||||||
|
<div class="profile-birth-field">
|
||||||
|
<input type="number" name="birth_month" min="1" max="12" required lay-verify="required"
|
||||||
|
autocomplete="off" class="layui-input" value="{{ .pref.EffectiveBirthMonth }}">
|
||||||
|
</div>
|
||||||
|
<div class="profile-birth-sep">月</div>
|
||||||
|
<div class="profile-birth-field">
|
||||||
|
<input type="number" name="birth_day" min="1" max="31" required lay-verify="required"
|
||||||
|
autocomplete="off" class="layui-input" value="{{ .pref.EffectiveBirthDay }}">
|
||||||
|
</div>
|
||||||
|
<div class="profile-birth-sep">日</div>
|
||||||
|
</div>
|
||||||
|
<div class="layui-form-mid layui-word-aux">格式为 月 / 日,默认 10 / 18。</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="layui-form-item">
|
<div class="layui-form-item">
|
||||||
<label class="layui-form-label">用户 ID</label>
|
<label class="layui-form-label">用户 ID</label>
|
||||||
<div class="layui-input-block">
|
<div class="layui-input-block">
|
||||||
|
|||||||
Reference in New Issue
Block a user