webui: Make some user preferences configurable

This extends 09f0237560.

- Extend user_pref with configurable profile fields and automatic migration defaults
- Move user info responses to read from user_pref instead of hardcoded values
- Add admin profile page for editing UI values after login

Signed-off-by: Sean Du <do4suki@gmail.com>
This commit is contained in:
2026-06-04 19:31:27 +08:00
parent 9ad66c2dc6
commit fa2efe4298
14 changed files with 419 additions and 46 deletions
+7 -7
View File
@@ -5,6 +5,7 @@ import (
usermodel "honoka-chan/internal/model/user"
profileapischema "honoka-chan/internal/schema/api/profile"
"honoka-chan/internal/session"
"strconv"
"time"
"github.com/gin-gonic/gin"
@@ -12,11 +13,10 @@ import (
func profileInfo(ctx *gin.Context) (res any, err error) {
ss := session.Get(ctx)
pref := usermodel.UserPref{}
_, err = ss.UserEng.Table("user_pref").Where("user_id = ?", ss.UserID).Get(&pref)
if err != nil {
return nil, err
pref := ss.UserPref
displayUserID := pref.UserID
if inviteUserID, convErr := strconv.Atoi(pref.InviteCode); convErr == nil && inviteUserID > 0 {
displayUserID = inviteUserID
}
unitCount, err := ss.UserEng.Table(new(usermodel.UserUnitData)).
@@ -64,12 +64,12 @@ func profileInfo(ctx *gin.Context) (res any, err error) {
res = profileapischema.InfoResp{
Result: profileapischema.InfoData{
UserInfo: profileapischema.UserInfo{
UserID: pref.UserID,
UserID: displayUserID,
Name: pref.UserName,
Level: pref.UserLevel,
CostMax: 100,
UnitMax: 5000,
EnergyMax: 417,
EnergyMax: pref.EffectiveEnergyMax(),
FriendMax: 99,
UnitCnt: int(unitCount),
InviteCode: pref.InviteCode,
-7
View File
@@ -1,7 +1,6 @@
package user
import (
usermodel "honoka-chan/internal/model/user"
userapischema "honoka-chan/internal/schema/api/user"
"honoka-chan/internal/session"
"time"
@@ -12,12 +11,6 @@ import (
func userInfo(ctx *gin.Context) (res any, err error) {
ss := session.Get(ctx)
pref := usermodel.UserPref{}
_, err = ss.UserEng.Table("user_pref").Where("user_id = ?", ss.UserID).Get(&pref)
if err != nil {
return nil, err
}
res = userapischema.InfoResp{
Result: ss.GetUserInfo(),
Status: 200,
+8 -1
View File
@@ -269,9 +269,16 @@ func addUser(dbSession *xorm.Session, phone, password string, isDefault bool) (g
BackgroundID: backgroundID,
UnitOwningUserID: unitOwningUserID,
UserName: userName,
UserLevel: 1028,
UserLevel: usermodel.DefaultUserLevel,
UserDesc: userDesc,
InviteCode: strconv.Itoa(userID),
UserExp: usermodel.DefaultUserExp,
NextExp: usermodel.DefaultUserNextExp,
GameCoin: usermodel.DefaultUserGameCoin,
SnsCoin: usermodel.DefaultUserSnsCoin,
EnergyMax: usermodel.DefaultUserEnergyMax,
OverMaxEnergy: usermodel.DefaultUserOverMaxEnergy,
ProfileVersion: usermodel.CurrentUserPrefProfileVersion,
UpdateTime: time.Now().Unix(),
}
_, err = dbSession.Table("user_pref").Insert(&userPref)
+2 -2
View File
@@ -16,8 +16,8 @@ func continuee(ctx *gin.Context) {
ss.Respond(liveschema.ContinueResp{
ResponseData: liveschema.ContinueData{
BeforeSnsCoin: 100000,
AfterSnsCoin: 100000,
BeforeSnsCoin: ss.UserPref.SnsCoin,
AfterSnsCoin: ss.UserPref.SnsCoin,
ServerTimestamp: time.Now().Unix(),
},
ReleaseInfo: []any{},
+4 -10
View File
@@ -42,12 +42,6 @@ func reward(ctx *gin.Context) {
return
}
pref := usermodel.UserPref{}
_, err = ss.UserEng.Table("user_pref").Where("user_id = ?", ss.UserID).Get(&pref)
if ss.CheckErr(err) {
return
}
totalScore := playRewardReq.ScoreSmile + playRewardReq.ScoreCool + playRewardReq.ScoreCute
playResp := liveschema.RewardResp{
ResponseData: liveschema.RewardData{
@@ -73,8 +67,8 @@ func reward(ctx *gin.Context) {
After: 99,
},
PlayerExpLpMax: liveschema.PlayerExpLpMax{
Before: 417,
After: 417,
Before: ss.UserPref.EffectiveEnergyMax(),
After: ss.UserPref.EffectiveEnergyMax(),
},
GameCoin: 0,
GameCoinRewardBoxFlag: false,
@@ -95,8 +89,8 @@ func reward(ctx *gin.Context) {
AfterUserInfo: ss.GetUserInfo(),
NextLevelInfo: []liveschema.NextLevelInfo{
{
Level: pref.UserLevel,
FromExp: 1089696,
Level: ss.UserPref.UserLevel,
FromExp: ss.UserPref.UserExp,
},
},
GoalAccompInfo: liveschema.GoalAccompInfo{
-7
View File
@@ -2,7 +2,6 @@ package user
import (
"honoka-chan/internal/middleware"
usermodel "honoka-chan/internal/model/user"
"honoka-chan/internal/router"
userschema "honoka-chan/internal/schema/user"
"honoka-chan/internal/session"
@@ -15,12 +14,6 @@ func userInfo(ctx *gin.Context) {
ss := session.Get(ctx)
defer ss.Finalize()
pref := usermodel.UserPref{}
_, err := ss.UserEng.Table("user_pref").Where("user_id = ?", ss.UserID).Get(&pref)
if ss.CheckErr(err) {
return
}
userResp := userschema.UserInfoResp{
ResponseData: userschema.UserInfoData{
User: ss.GetUserInfo(),
+141
View File
@@ -0,0 +1,141 @@
package webui
import (
"honoka-chan/internal/middleware"
usermodel "honoka-chan/internal/model/user"
"honoka-chan/internal/router"
webuischema "honoka-chan/internal/schema/webui"
"honoka-chan/pkg/db"
"net/http"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
)
func profilePage(ctx *gin.Context) {
pref := usermodel.UserPref{}
exists, err := db.UserEng.Table(new(usermodel.UserPref)).
Where("user_id = ?", ctx.GetInt("userid")).Get(&pref)
if err != nil || !exists {
ctx.String(http.StatusInternalServerError, "用户资料读取失败")
return
}
ctx.HTML(http.StatusOK, "admin/profile.html", gin.H{
"menu": 2,
"url": strings.Split(ctx.Request.URL.String(), "?")[0],
"pref": pref,
})
}
func updateProfile(ctx *gin.Context) {
parseInt := func(key string, min int) (int, error) {
value := strings.TrimSpace(ctx.PostForm(key))
number, err := strconv.Atoi(value)
if err != nil {
return 0, err
}
if number < min {
return 0, strconv.ErrSyntax
}
return number, nil
}
userName := strings.TrimSpace(ctx.PostForm("user_name"))
inviteCode := strings.TrimSpace(ctx.PostForm("invite_code"))
userDesc := strings.TrimSpace(ctx.PostForm("user_desc"))
if userName == "" || inviteCode == "" {
ctx.JSON(http.StatusOK, webuischema.Msg{
Code: 1,
Message: "用户名和邀请码不能为空!",
})
return
}
userLevel, err := parseInt("user_level", 1)
if err != nil {
ctx.JSON(http.StatusOK, webuischema.Msg{Code: 1, Message: "等级格式有误!"})
return
}
userExp, err := parseInt("user_exp", 0)
if err != nil {
ctx.JSON(http.StatusOK, webuischema.Msg{Code: 1, Message: "当前经验格式有误!"})
return
}
nextExp, err := parseInt("next_exp", 0)
if err != nil {
ctx.JSON(http.StatusOK, webuischema.Msg{Code: 1, Message: "升级经验格式有误!"})
return
}
gameCoin, err := parseInt("game_coin", 0)
if err != nil {
ctx.JSON(http.StatusOK, webuischema.Msg{Code: 1, Message: "金币格式有误!"})
return
}
snsCoin, err := parseInt("sns_coin", 0)
if err != nil {
ctx.JSON(http.StatusOK, webuischema.Msg{Code: 1, Message: "爱心格式有误!"})
return
}
energyMax, err := parseInt("energy_max", 1)
if err != nil {
ctx.JSON(http.StatusOK, webuischema.Msg{Code: 1, Message: "体力上限格式有误!"})
return
}
overMaxEnergy, err := parseInt("over_max_energy", 0)
if err != nil {
ctx.JSON(http.StatusOK, webuischema.Msg{Code: 1, Message: "当前体力格式有误!"})
return
}
pref := usermodel.UserPref{
UserName: userName,
UserLevel: userLevel,
UserDesc: userDesc,
InviteCode: inviteCode,
UserExp: userExp,
NextExp: nextExp,
GameCoin: gameCoin,
SnsCoin: snsCoin,
EnergyMax: energyMax,
OverMaxEnergy: overMaxEnergy,
ProfileVersion: usermodel.CurrentUserPrefProfileVersion,
UpdateTime: time.Now().Unix(),
}
_, err = db.UserEng.Table(new(usermodel.UserPref)).
Where("user_id = ?", ctx.GetInt("userid")).
Cols(
"user_name",
"user_level",
"user_desc",
"invite_code",
"user_exp",
"next_exp",
"game_coin",
"sns_coin",
"energy_max",
"over_max_energy",
"profile_version",
"update_time",
).
Update(&pref)
if err != nil {
ctx.JSON(http.StatusOK, webuischema.Msg{
Code: 1,
Message: "保存失败!",
})
return
}
ctx.JSON(http.StatusOK, webuischema.Msg{
Code: 0,
Message: "保存成功!",
})
}
func init() {
router.AddHandler("admin", "GET", "/profile", middleware.WebAuth, profilePage)
router.AddHandler("admin", "POST", "/profile", middleware.WebAuth, updateProfile)
}