Add Android control app and embedded server
Inspired by HNKServer/honoka-chan-apk-server, but implemented as a JNI-based embedded runtime instead of the original approach. - Add an Android Compose controller app that starts and stops honoka-chan through JNI, shows runtime and health status, manages data directory mounting, and supports backup import/export for data.db - Add an embeddable Go server entrypoint plus JNI-exported status, health, and reload hooks so desktop and Android builds share the same startup and shutdown path - Add Android build scripts, runtime packaging, and project documentation, including generated default config.json content for honoka_runtime.zip - Add runtime bundle hash tracking so updated honoka_runtime.zip assets are automatically redeployed while preserving config.json and user data files - Add in-app service settings for unlock_all_special_rotation with immediate config persistence and automatic reload when the service is running - Split SQLite driver selection by platform, using go-sqlite3 on Android and modernc.org/sqlite elsewhere to avoid Android x86_64 seccomp crashes - Update startup, database initialization, and system health/reload handlers to support the embedded runtime and Android control flow Signed-off-by: Sean Du <do4suki@gmail.com>
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"honoka-chan/config"
|
||||
_ "honoka-chan/internal/handler"
|
||||
systemhandler "honoka-chan/internal/handler/system"
|
||||
"honoka-chan/internal/router"
|
||||
"honoka-chan/internal/startup"
|
||||
"honoka-chan/pkg/db"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const dateTimeFormat = "2006-01-02 15:04:05"
|
||||
|
||||
type Status struct {
|
||||
Running bool `json:"running"`
|
||||
WorkDir string `json:"work_dir"`
|
||||
ListenPort string `json:"listen_port"`
|
||||
LocalURL string `json:"local_url"`
|
||||
StartedAt string `json:"started_at,omitempty"`
|
||||
LastError string `json:"last_error,omitempty"`
|
||||
}
|
||||
|
||||
var (
|
||||
mu sync.Mutex
|
||||
httpServer *http.Server
|
||||
listener net.Listener
|
||||
state Status
|
||||
)
|
||||
|
||||
func Start(workDir string) error {
|
||||
if workDir == "" {
|
||||
workDir = "."
|
||||
}
|
||||
|
||||
absWorkDir, err := filepath.Abs(workDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
if state.Running {
|
||||
mu.Unlock()
|
||||
return errors.New("server is already running")
|
||||
}
|
||||
mu.Unlock()
|
||||
|
||||
if err := os.Chdir(absWorkDir); err != nil {
|
||||
setLastError(err)
|
||||
return err
|
||||
}
|
||||
|
||||
config.ReloadConfig()
|
||||
if err := db.Init("assets/main.db", "assets/data.db"); err != nil {
|
||||
setLastError(err)
|
||||
return err
|
||||
}
|
||||
if err := startup.StartUp(); err != nil {
|
||||
_ = db.Close()
|
||||
setLastError(err)
|
||||
return err
|
||||
}
|
||||
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
engine := gin.New()
|
||||
engine.Use(gin.LoggerWithConfig(gin.LoggerConfig{
|
||||
SkipPaths: []string{
|
||||
"/agreement/all",
|
||||
"/integration/appReport/initialize",
|
||||
"/report/ge/app",
|
||||
"/v1/account/reportRole",
|
||||
},
|
||||
}))
|
||||
router.SifRouter(engine)
|
||||
|
||||
ln, err := net.Listen("tcp", ":"+config.Conf.Settings.ListenPort)
|
||||
if err != nil {
|
||||
_ = db.Close()
|
||||
setLastError(err)
|
||||
return err
|
||||
}
|
||||
|
||||
srv := &http.Server{
|
||||
Handler: engine,
|
||||
}
|
||||
now := time.Now()
|
||||
systemhandler.MarkStarted(now)
|
||||
|
||||
mu.Lock()
|
||||
httpServer = srv
|
||||
listener = ln
|
||||
state = Status{
|
||||
Running: true,
|
||||
WorkDir: absWorkDir,
|
||||
ListenPort: config.Conf.Settings.ListenPort,
|
||||
LocalURL: fmt.Sprintf("http://127.0.0.1:%s", config.Conf.Settings.ListenPort),
|
||||
StartedAt: now.Format(dateTimeFormat),
|
||||
}
|
||||
mu.Unlock()
|
||||
|
||||
go serveLoop(srv, ln)
|
||||
return nil
|
||||
}
|
||||
|
||||
func serveLoop(srv *http.Server, ln net.Listener) {
|
||||
err := srv.Serve(ln)
|
||||
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
state.LastError = err.Error()
|
||||
}
|
||||
state.Running = false
|
||||
httpServer = nil
|
||||
listener = nil
|
||||
_ = db.Close()
|
||||
}
|
||||
|
||||
func Stop(ctx context.Context) error {
|
||||
mu.Lock()
|
||||
srv := httpServer
|
||||
running := state.Running
|
||||
mu.Unlock()
|
||||
|
||||
if !running || srv == nil {
|
||||
return nil
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
return srv.Shutdown(ctx)
|
||||
}
|
||||
|
||||
func GetStatus() Status {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
return state
|
||||
}
|
||||
|
||||
func GetStatusJSON() string {
|
||||
status := GetStatus()
|
||||
data, err := json.Marshal(status)
|
||||
if err != nil {
|
||||
return `{"running":false,"last_error":"failed to encode status"}`
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func setLastError(err error) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
state.LastError = err.Error()
|
||||
state.Running = false
|
||||
}
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
_ "honoka-chan/internal/handler/scenario"
|
||||
_ "honoka-chan/internal/handler/secretbox"
|
||||
_ "honoka-chan/internal/handler/subscenario"
|
||||
_ "honoka-chan/internal/handler/system"
|
||||
_ "honoka-chan/internal/handler/tos"
|
||||
_ "honoka-chan/internal/handler/unit"
|
||||
_ "honoka-chan/internal/handler/user"
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"honoka-chan/config"
|
||||
"honoka-chan/internal/router"
|
||||
systemschema "honoka-chan/internal/schema/system"
|
||||
"honoka-chan/pkg/db"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
var (
|
||||
startedAt = time.Now()
|
||||
lastReloadAt atomic.Int64
|
||||
)
|
||||
|
||||
const dateTimeFormat = "2006-01-02 15:04:05"
|
||||
|
||||
func MarkStarted(t time.Time) {
|
||||
startedAt = t
|
||||
}
|
||||
|
||||
func Health() (systemschema.HealthResp, int) {
|
||||
appName := config.Conf.AppName
|
||||
if appName == "" {
|
||||
appName = "honoka-chan"
|
||||
}
|
||||
|
||||
mainDBStatus := "not initialized"
|
||||
if db.MainEng != nil {
|
||||
mainDBStatus = "ok"
|
||||
if err := db.MainEng.Ping(); err != nil {
|
||||
mainDBStatus = err.Error()
|
||||
}
|
||||
}
|
||||
|
||||
userDBStatus := "not initialized"
|
||||
if db.UserEng != nil {
|
||||
userDBStatus = "ok"
|
||||
if err := db.UserEng.Ping(); err != nil {
|
||||
userDBStatus = err.Error()
|
||||
}
|
||||
}
|
||||
|
||||
status := "ok"
|
||||
statusCode := http.StatusOK
|
||||
if db.MainEng == nil || db.UserEng == nil {
|
||||
status = "stopped"
|
||||
}
|
||||
|
||||
resp := systemschema.HealthResp{
|
||||
Status: status,
|
||||
AppName: appName,
|
||||
Version: config.PackageVersion,
|
||||
StartedAt: startedAt.Format(dateTimeFormat),
|
||||
UptimeSeconds: int64(time.Since(startedAt).Seconds()),
|
||||
ListenPort: config.Conf.Settings.ListenPort,
|
||||
CdnServer: config.Conf.Settings.CdnServer,
|
||||
ReloadTokenConfigured: strings.TrimSpace(config.Conf.Settings.ReloadToken) != "",
|
||||
MainDB: mainDBStatus,
|
||||
UserDB: userDBStatus,
|
||||
}
|
||||
|
||||
if ts := lastReloadAt.Load(); ts > 0 {
|
||||
resp.LastReloadAt = time.Unix(ts, 0).Format(dateTimeFormat)
|
||||
}
|
||||
|
||||
if status == "ok" && (mainDBStatus != "ok" || userDBStatus != "ok") {
|
||||
resp.Status = "degraded"
|
||||
statusCode = http.StatusServiceUnavailable
|
||||
}
|
||||
|
||||
return resp, statusCode
|
||||
}
|
||||
|
||||
func HealthJSON() string {
|
||||
resp, _ := Health()
|
||||
data, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
return `{"status":"error","app_name":"honoka-chan"}`
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func Reload(token string, requireToken bool) (systemschema.ReloadResp, int, error) {
|
||||
expectedToken := strings.TrimSpace(config.Conf.Settings.ReloadToken)
|
||||
if requireToken && expectedToken == "" {
|
||||
return systemschema.ReloadResp{
|
||||
Status: "error",
|
||||
Message: "reload_token is not configured",
|
||||
}, http.StatusServiceUnavailable, errors.New("reload_token is not configured")
|
||||
}
|
||||
|
||||
if requireToken && strings.TrimSpace(token) != expectedToken {
|
||||
return systemschema.ReloadResp{
|
||||
Status: "error",
|
||||
Message: "invalid reload token",
|
||||
}, http.StatusUnauthorized, errors.New("invalid reload token")
|
||||
}
|
||||
|
||||
config.ReloadConfig()
|
||||
now := time.Now()
|
||||
lastReloadAt.Store(now.Unix())
|
||||
|
||||
return systemschema.ReloadResp{
|
||||
Status: "ok",
|
||||
Message: "configuration reloaded",
|
||||
ReloadedAt: now.Format(dateTimeFormat),
|
||||
}, http.StatusOK, nil
|
||||
}
|
||||
|
||||
func health(ctx *gin.Context) {
|
||||
resp, statusCode := Health()
|
||||
ctx.JSON(statusCode, resp)
|
||||
}
|
||||
|
||||
func reload(ctx *gin.Context) {
|
||||
token := strings.TrimSpace(ctx.GetHeader("X-Reload-Token"))
|
||||
if token == "" {
|
||||
token = strings.TrimSpace(ctx.Query("token"))
|
||||
}
|
||||
if token == "" {
|
||||
token = strings.TrimSpace(ctx.PostForm("token"))
|
||||
}
|
||||
|
||||
resp, statusCode, _ := Reload(token, true)
|
||||
ctx.JSON(statusCode, resp)
|
||||
}
|
||||
|
||||
func init() {
|
||||
router.AddHandler("/", "GET", "/system/health", health)
|
||||
router.AddHandler("/", "POST", "/system/reload", reload)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package systemschema
|
||||
|
||||
type HealthResp struct {
|
||||
Status string `json:"status"`
|
||||
AppName string `json:"app_name"`
|
||||
Version string `json:"version"`
|
||||
StartedAt string `json:"started_at"`
|
||||
UptimeSeconds int64 `json:"uptime_seconds"`
|
||||
LastReloadAt string `json:"last_reload_at,omitempty"`
|
||||
ListenPort string `json:"listen_port"`
|
||||
CdnServer string `json:"cdn_server"`
|
||||
ReloadTokenConfigured bool `json:"reload_token_configured"`
|
||||
MainDB string `json:"main_db"`
|
||||
UserDB string `json:"user_db"`
|
||||
}
|
||||
|
||||
type ReloadResp struct {
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
ReloadedAt string `json:"reloaded_at,omitempty"`
|
||||
}
|
||||
+239
-201
@@ -1,6 +1,7 @@
|
||||
package startup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"honoka-chan/internal/constant"
|
||||
ghomemodel "honoka-chan/internal/model/ghome"
|
||||
loginmodel "honoka-chan/internal/model/login"
|
||||
@@ -18,31 +19,43 @@ var (
|
||||
userEng *xorm.Session
|
||||
)
|
||||
|
||||
func CreateTables() {
|
||||
// db.UserEng.ShowSQL(true)
|
||||
db.UserEng.Sync2(new(ghomemodel.DeviceKey))
|
||||
db.UserEng.Sync2(new(loginmodel.AuthKey))
|
||||
db.UserEng.Sync2(new(usermodel.UserAccessoryWear))
|
||||
db.UserEng.Sync2(new(usermodel.UserDeck))
|
||||
db.UserEng.Sync2(new(usermodel.UserDeckUnit))
|
||||
db.UserEng.Sync2(new(usermodel.UserKey))
|
||||
db.UserEng.Sync2(new(usermodel.UserLiveGoal))
|
||||
db.UserEng.Sync2(new(usermodel.UserLiveStatus))
|
||||
db.UserEng.Sync2(new(usermodel.UserLiveInProgress))
|
||||
db.UserEng.Sync2(new(usermodel.UserLiveRandom))
|
||||
db.UserEng.Sync2(new(usermodel.UserLiveRecord))
|
||||
db.UserEng.Sync2(new(usermodel.UserFriend))
|
||||
db.UserEng.Sync2(new(usermodel.UserGreet))
|
||||
db.UserEng.Sync2(new(usermodel.UserPref))
|
||||
db.UserEng.Sync2(new(usermodel.Users))
|
||||
db.UserEng.Sync2(new(usermodel.UserUnit))
|
||||
db.UserEng.Sync2(new(usermodel.UserUnitSkillEquip))
|
||||
func CreateTables() error {
|
||||
models := []any{
|
||||
new(ghomemodel.DeviceKey),
|
||||
new(loginmodel.AuthKey),
|
||||
new(usermodel.UserAccessoryWear),
|
||||
new(usermodel.UserDeck),
|
||||
new(usermodel.UserDeckUnit),
|
||||
new(usermodel.UserKey),
|
||||
new(usermodel.UserLiveGoal),
|
||||
new(usermodel.UserLiveStatus),
|
||||
new(usermodel.UserLiveInProgress),
|
||||
new(usermodel.UserLiveRandom),
|
||||
new(usermodel.UserLiveRecord),
|
||||
new(usermodel.UserFriend),
|
||||
new(usermodel.UserGreet),
|
||||
new(usermodel.UserPref),
|
||||
new(usermodel.Users),
|
||||
new(usermodel.UserUnit),
|
||||
new(usermodel.UserUnitSkillEquip),
|
||||
}
|
||||
|
||||
MigrateUserPref()
|
||||
MigrateUserLiveData()
|
||||
for _, model := range models {
|
||||
if err := db.UserEng.Sync2(model); err != nil {
|
||||
return fmt.Errorf("同步表失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := MigrateUserPref(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := MigrateUserLiveData(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func MigrateUserPref() {
|
||||
func MigrateUserPref() error {
|
||||
session := db.UserEng.NewSession()
|
||||
defer session.Close()
|
||||
|
||||
@@ -52,7 +65,7 @@ func MigrateUserPref() {
|
||||
Or("profile_version IS NULL").
|
||||
Find(&prefList)
|
||||
if err != nil {
|
||||
log.Fatalln("迁移 user_pref 失败:", err.Error())
|
||||
return fmt.Errorf("迁移 user_pref 失败: %w", err)
|
||||
}
|
||||
|
||||
for _, pref := range prefList {
|
||||
@@ -62,9 +75,10 @@ func MigrateUserPref() {
|
||||
Cols(usermodel.UserPrefProfileColumns()...).
|
||||
Update(&pref)
|
||||
if err != nil {
|
||||
log.Fatalln("迁移 user_pref 失败:", err.Error())
|
||||
return fmt.Errorf("迁移 user_pref 失败: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type liveGoalRewardMigrationRow struct {
|
||||
@@ -105,18 +119,18 @@ func liveRankForMigration(value int, cRank int, bRank int, aRank int, sRank int)
|
||||
}
|
||||
}
|
||||
|
||||
func MigrateUserLiveData() {
|
||||
func MigrateUserLiveData() error {
|
||||
session := db.UserEng.NewSession()
|
||||
defer session.Close()
|
||||
|
||||
recordRows := []usermodel.UserLiveRecord{}
|
||||
if err := session.Table(new(usermodel.UserLiveRecord)).Find(&recordRows); err != nil {
|
||||
log.Fatalln("迁移 user_live_status 失败:", err.Error())
|
||||
return fmt.Errorf("迁移 user_live_status 失败: %w", err)
|
||||
}
|
||||
|
||||
existingStatusRows := []usermodel.UserLiveStatus{}
|
||||
if err := session.Table(new(usermodel.UserLiveStatus)).Find(&existingStatusRows); err != nil {
|
||||
log.Fatalln("迁移 user_live_status 失败:", err.Error())
|
||||
return fmt.Errorf("迁移 user_live_status 失败: %w", err)
|
||||
}
|
||||
|
||||
type liveStatusKey struct {
|
||||
@@ -144,7 +158,7 @@ func MigrateUserLiveData() {
|
||||
UpdateDate: now,
|
||||
}
|
||||
if _, err := session.Insert(&status); err != nil {
|
||||
log.Fatalln("迁移 user_live_status 失败:", err.Error())
|
||||
return fmt.Errorf("迁移 user_live_status 失败: %w", err)
|
||||
}
|
||||
statusMap[key] = status
|
||||
continue
|
||||
@@ -169,14 +183,14 @@ func MigrateUserLiveData() {
|
||||
Where("user_id = ? AND live_difficulty_id = ?", status.UserID, status.LiveDifficultyID).
|
||||
Cols("hi_score", "hi_combo_count", "clear_cnt", "update_date").
|
||||
Update(&status); err != nil {
|
||||
log.Fatalln("迁移 user_live_status 失败:", err.Error())
|
||||
return fmt.Errorf("迁移 user_live_status 失败: %w", err)
|
||||
}
|
||||
statusMap[key] = status
|
||||
}
|
||||
}
|
||||
|
||||
liveGoalInfoMap := map[int]liveGoalInfoMigrationRow{}
|
||||
loadLiveGoalInfoRows := func(table string) {
|
||||
loadLiveGoalInfoRows := func(table string) error {
|
||||
rows := []liveGoalInfoMigrationRow{}
|
||||
err := db.MainEng.Table(table).Alias("live").
|
||||
Join("LEFT", "live_setting_m setting", "live.live_setting_id = setting.live_setting_id").
|
||||
@@ -197,20 +211,25 @@ func MigrateUserLiveData() {
|
||||
`).
|
||||
Find(&rows)
|
||||
if err != nil {
|
||||
log.Fatalln("迁移 user_live_goal 失败:", err.Error())
|
||||
return fmt.Errorf("迁移 user_live_goal 失败: %w", err)
|
||||
}
|
||||
for _, row := range rows {
|
||||
liveGoalInfoMap[row.LiveDifficultyID] = row
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err := loadLiveGoalInfoRows("normal_live_m"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := loadLiveGoalInfoRows("special_live_m"); err != nil {
|
||||
return err
|
||||
}
|
||||
loadLiveGoalInfoRows("normal_live_m")
|
||||
loadLiveGoalInfoRows("special_live_m")
|
||||
|
||||
goalRewardRows := []liveGoalRewardMigrationRow{}
|
||||
if err := db.MainEng.Table("live_goal_reward_m").
|
||||
OrderBy("live_difficulty_id ASC, live_goal_type ASC, rank ASC, live_goal_reward_id ASC").
|
||||
Find(&goalRewardRows); err != nil {
|
||||
log.Fatalln("迁移 user_live_goal 失败:", err.Error())
|
||||
return fmt.Errorf("迁移 user_live_goal 失败: %w", err)
|
||||
}
|
||||
|
||||
goalRewardMap := map[int][]liveGoalRewardMigrationRow{}
|
||||
@@ -222,7 +241,7 @@ func MigrateUserLiveData() {
|
||||
if err := session.Table(new(usermodel.UserLiveGoal)).
|
||||
Where("live_goal_reward_id > 0").
|
||||
Find(&existingGoalRows); err != nil {
|
||||
log.Fatalln("迁移 user_live_goal 失败:", err.Error())
|
||||
return fmt.Errorf("迁移 user_live_goal 失败: %w", err)
|
||||
}
|
||||
|
||||
existingGoalMap := make(map[liveStatusKey]map[int]struct{})
|
||||
@@ -284,7 +303,7 @@ func MigrateUserLiveData() {
|
||||
Rank: reward.Rank,
|
||||
CompletedAt: now,
|
||||
}); err != nil {
|
||||
log.Fatalln("迁移 user_live_goal 失败:", err.Error())
|
||||
return fmt.Errorf("迁移 user_live_goal 失败: %w", err)
|
||||
}
|
||||
if existingGoalMap[key] == nil {
|
||||
existingGoalMap[key] = map[int]struct{}{}
|
||||
@@ -292,13 +311,29 @@ func MigrateUserLiveData() {
|
||||
existingGoalMap[key][reward.LiveGoalRewardID] = struct{}{}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func LoadUnitData() {
|
||||
func LoadUnitData() (err error) {
|
||||
userEng = db.UserEng.NewSession()
|
||||
defer userEng.Close()
|
||||
defer func() {
|
||||
if recovered := recover(); recovered != nil {
|
||||
if userEng != nil {
|
||||
_ = userEng.Rollback()
|
||||
}
|
||||
switch v := recovered.(type) {
|
||||
case error:
|
||||
err = fmt.Errorf("同步失败: %w", v)
|
||||
default:
|
||||
err = fmt.Errorf("同步失败: %v", v)
|
||||
}
|
||||
}
|
||||
if userEng != nil {
|
||||
userEng.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
err := userEng.Begin()
|
||||
err = userEng.Begin()
|
||||
CheckErr(err)
|
||||
|
||||
commonUnitExist, err := userEng.IsTableExist(new(unitmodel.CommonUnitData))
|
||||
@@ -306,183 +341,186 @@ func LoadUnitData() {
|
||||
userUnitExist, err := userEng.IsTableExist(new(usermodel.UserUnitData))
|
||||
CheckErr(err)
|
||||
|
||||
if !commonUnitExist || !userUnitExist {
|
||||
log.Println("卡片数据不存在,正在同步...")
|
||||
if commonUnitExist && userUnitExist {
|
||||
_ = userEng.Rollback()
|
||||
return nil
|
||||
}
|
||||
|
||||
userEng.DropTable(new(unitmodel.CommonUnitData))
|
||||
userEng.CreateTable(new(unitmodel.CommonUnitData))
|
||||
userEng.DropTable(new(usermodel.UserUnitData))
|
||||
userEng.CreateTable(new(usermodel.UserUnitData))
|
||||
log.Println("卡片数据不存在,正在同步...")
|
||||
|
||||
var unitData []unitmodel.UnitM
|
||||
err = db.MainEng.Table(new(unitmodel.UnitM)).OrderBy("unit_id ASC").Find(&unitData)
|
||||
CheckErr(userEng.DropTable(new(unitmodel.CommonUnitData)))
|
||||
CheckErr(userEng.CreateTable(new(unitmodel.CommonUnitData)))
|
||||
CheckErr(userEng.DropTable(new(usermodel.UserUnitData)))
|
||||
CheckErr(userEng.CreateTable(new(usermodel.UserUnitData)))
|
||||
|
||||
var unitData []unitmodel.UnitM
|
||||
err = db.MainEng.Table(new(unitmodel.UnitM)).OrderBy("unit_id ASC").Find(&unitData)
|
||||
CheckErr(err)
|
||||
|
||||
checked := false
|
||||
for _, u := range unitData {
|
||||
// 判断卡片最大等级
|
||||
var unitMaxLevel, nextExp, sumExp int
|
||||
_, err = db.MainEng.Table("unit_level_up_pattern_m").
|
||||
Where("unit_level_up_pattern_id = ?", u.UnitLevelUpPatternId).
|
||||
Select("MAX(unit_level),next_exp").Get(&unitMaxLevel, &nextExp)
|
||||
CheckErr(err)
|
||||
|
||||
checked := false
|
||||
for _, u := range unitData {
|
||||
// 判断卡片最大等级
|
||||
var unitMaxLevel, nextExp, sumExp int
|
||||
_, err = db.MainEng.Table("unit_level_up_pattern_m").
|
||||
Where("unit_level_up_pattern_id = ?", u.UnitLevelUpPatternId).
|
||||
Select("MAX(unit_level),next_exp").Get(&unitMaxLevel, &nextExp)
|
||||
// 计算突破前的经验总和
|
||||
_, err = db.MainEng.Table("unit_level_up_pattern_m").
|
||||
Where("unit_level_up_pattern_id = ?", u.UnitLevelUpPatternId).
|
||||
Where("unit_level = ?", unitMaxLevel-1).Cols("next_exp").Get(&sumExp)
|
||||
CheckErr(err)
|
||||
|
||||
// 计算突破前的属性
|
||||
var smileMax, pureMax, coolMax int
|
||||
smileMax = u.SmileMax
|
||||
pureMax = u.PureMax
|
||||
coolMax = u.CoolMax
|
||||
|
||||
// 如果 nexpExp 不为零,则说明卡片等级没有达到上限
|
||||
if nextExp != 0 {
|
||||
// 计算突破后的经验总和
|
||||
_, err = db.MainEng.Table("unit_level_limit_pattern_m").
|
||||
Where("unit_level_limit_id = 1 AND unit_level = 349").
|
||||
Cols("next_exp").Get(&sumExp)
|
||||
CheckErr(err)
|
||||
|
||||
// 计算突破前的经验总和
|
||||
_, err = db.MainEng.Table("unit_level_up_pattern_m").
|
||||
Where("unit_level_up_pattern_id = ?", u.UnitLevelUpPatternId).
|
||||
Where("unit_level = ?", unitMaxLevel-1).Cols("next_exp").Get(&sumExp)
|
||||
CheckErr(err)
|
||||
// 突破后最大等级
|
||||
unitMaxLevel = 350
|
||||
|
||||
// 计算突破前的属性
|
||||
var smileMax, pureMax, coolMax int
|
||||
smileMax = u.SmileMax
|
||||
pureMax = u.PureMax
|
||||
coolMax = u.CoolMax
|
||||
|
||||
// 如果 nexpExp 不为零,则说明卡片等级没有达到上限
|
||||
if nextExp != 0 {
|
||||
// 计算突破后的经验总和
|
||||
_, err = db.MainEng.Table("unit_level_limit_pattern_m").
|
||||
Where("unit_level_limit_id = 1 AND unit_level = 349").
|
||||
Cols("next_exp").Get(&sumExp)
|
||||
CheckErr(err)
|
||||
|
||||
// 突破后最大等级
|
||||
unitMaxLevel = 350
|
||||
|
||||
// 计算突破后的属性
|
||||
smileMax += 6000
|
||||
pureMax += 6000
|
||||
coolMax += 6000
|
||||
}
|
||||
|
||||
// 计算绊值、技能等级、技能经验
|
||||
var maxLove, skillLevel, skillExp, removableSkillCapacity, levelLimitID int
|
||||
switch u.Rarity {
|
||||
case 1:
|
||||
maxLove = 50
|
||||
skillExp = 0
|
||||
skillLevel = 0
|
||||
removableSkillCapacity = 0
|
||||
levelLimitID = 0
|
||||
case 2:
|
||||
maxLove = 200
|
||||
skillExp = 490
|
||||
skillLevel = 8
|
||||
removableSkillCapacity = 1
|
||||
levelLimitID = 0
|
||||
case 3:
|
||||
maxLove = 500
|
||||
skillExp = 4900
|
||||
skillLevel = 8
|
||||
removableSkillCapacity = 2
|
||||
levelLimitID = 0
|
||||
case 4:
|
||||
maxLove = 1000
|
||||
skillExp = 29900
|
||||
skillLevel = 8
|
||||
removableSkillCapacity = 8
|
||||
levelLimitID = 1
|
||||
case 5:
|
||||
maxLove = 750
|
||||
skillExp = 12700
|
||||
skillLevel = 8
|
||||
removableSkillCapacity = 3
|
||||
levelLimitID = 0
|
||||
}
|
||||
|
||||
// 针对技能卡等应援卡片
|
||||
if smileMax == 1 {
|
||||
maxLove = 0
|
||||
skillExp = 0
|
||||
skillLevel = 0
|
||||
removableSkillCapacity = 0
|
||||
}
|
||||
|
||||
// 检查是否签名卡
|
||||
var isSigned bool
|
||||
exist, err := db.MainEng.Table("unit_sign_asset_m").Where("unit_id = ?", u.UnitId).Exist()
|
||||
CheckErr(err)
|
||||
if exist {
|
||||
isSigned = true
|
||||
}
|
||||
|
||||
// 生成公共卡片
|
||||
unitCommon := unitmodel.CommonUnitData{
|
||||
UnitNumber: u.UnitNumber,
|
||||
UnitID: u.UnitId,
|
||||
UnitTypeID: u.UnitTypeId,
|
||||
Name: *u.NameEn,
|
||||
Eponym: u.EponymEn,
|
||||
Rarity: u.Rarity,
|
||||
Attribute: u.AttributeId,
|
||||
Smile: smileMax,
|
||||
Cute: pureMax,
|
||||
Cool: coolMax,
|
||||
Exp: sumExp,
|
||||
Level: unitMaxLevel,
|
||||
MaxLevel: unitMaxLevel,
|
||||
LevelLimitID: levelLimitID,
|
||||
Rank: u.RankMin,
|
||||
MaxRank: u.RankMax,
|
||||
Love: maxLove,
|
||||
MaxLove: maxLove,
|
||||
UnitSkillExp: skillExp,
|
||||
UnitSkillLevel: skillLevel,
|
||||
MaxHp: u.HpMax,
|
||||
UnitRemovableSkillCapacity: removableSkillCapacity,
|
||||
IsRankMax: true,
|
||||
IsLoveMax: true,
|
||||
IsLevelMax: true,
|
||||
IsSigned: isSigned,
|
||||
IsSkillLevelMax: true,
|
||||
IsRemovableSkillCapacityMax: true,
|
||||
InsertDate: time.Now().Unix(),
|
||||
}
|
||||
|
||||
_, err = userEng.Insert(&unitCommon)
|
||||
CheckErr(err)
|
||||
|
||||
var userID []int
|
||||
err = db.UserEng.Table(new(usermodel.Users)).Cols("user_id").Find(&userID)
|
||||
CheckErr(err)
|
||||
|
||||
for _, id := range userID {
|
||||
userUnit := usermodel.UserUnitData{
|
||||
UnitID: u.UnitId,
|
||||
FavoriteFlag: false,
|
||||
DisplayRank: u.RankMax,
|
||||
UserID: id,
|
||||
InsertDate: time.Now().Unix(),
|
||||
}
|
||||
|
||||
// 检查表里是否已经有数据
|
||||
if !checked {
|
||||
ct, err := userEng.Table(new(usermodel.UserUnitData)).Count()
|
||||
CheckErr(err)
|
||||
|
||||
if ct == 0 {
|
||||
userUnit.UnitOwningUserID = 38383
|
||||
}
|
||||
|
||||
checked = true
|
||||
}
|
||||
|
||||
_, err = userEng.Insert(&userUnit)
|
||||
CheckErr(err)
|
||||
}
|
||||
// 计算突破后的属性
|
||||
smileMax += 6000
|
||||
pureMax += 6000
|
||||
coolMax += 6000
|
||||
}
|
||||
|
||||
err = userEng.Commit()
|
||||
// 计算绊值、技能等级、技能经验
|
||||
var maxLove, skillLevel, skillExp, removableSkillCapacity, levelLimitID int
|
||||
switch u.Rarity {
|
||||
case 1:
|
||||
maxLove = 50
|
||||
skillExp = 0
|
||||
skillLevel = 0
|
||||
removableSkillCapacity = 0
|
||||
levelLimitID = 0
|
||||
case 2:
|
||||
maxLove = 200
|
||||
skillExp = 490
|
||||
skillLevel = 8
|
||||
removableSkillCapacity = 1
|
||||
levelLimitID = 0
|
||||
case 3:
|
||||
maxLove = 500
|
||||
skillExp = 4900
|
||||
skillLevel = 8
|
||||
removableSkillCapacity = 2
|
||||
levelLimitID = 0
|
||||
case 4:
|
||||
maxLove = 1000
|
||||
skillExp = 29900
|
||||
skillLevel = 8
|
||||
removableSkillCapacity = 8
|
||||
levelLimitID = 1
|
||||
case 5:
|
||||
maxLove = 750
|
||||
skillExp = 12700
|
||||
skillLevel = 8
|
||||
removableSkillCapacity = 3
|
||||
levelLimitID = 0
|
||||
}
|
||||
|
||||
// 针对技能卡等应援卡片
|
||||
if smileMax == 1 {
|
||||
maxLove = 0
|
||||
skillExp = 0
|
||||
skillLevel = 0
|
||||
removableSkillCapacity = 0
|
||||
}
|
||||
|
||||
// 检查是否签名卡
|
||||
var isSigned bool
|
||||
exist, err := db.MainEng.Table("unit_sign_asset_m").Where("unit_id = ?", u.UnitId).Exist()
|
||||
CheckErr(err)
|
||||
if exist {
|
||||
isSigned = true
|
||||
}
|
||||
|
||||
// 生成公共卡片
|
||||
unitCommon := unitmodel.CommonUnitData{
|
||||
UnitNumber: u.UnitNumber,
|
||||
UnitID: u.UnitId,
|
||||
UnitTypeID: u.UnitTypeId,
|
||||
Name: *u.NameEn,
|
||||
Eponym: u.EponymEn,
|
||||
Rarity: u.Rarity,
|
||||
Attribute: u.AttributeId,
|
||||
Smile: smileMax,
|
||||
Cute: pureMax,
|
||||
Cool: coolMax,
|
||||
Exp: sumExp,
|
||||
Level: unitMaxLevel,
|
||||
MaxLevel: unitMaxLevel,
|
||||
LevelLimitID: levelLimitID,
|
||||
Rank: u.RankMin,
|
||||
MaxRank: u.RankMax,
|
||||
Love: maxLove,
|
||||
MaxLove: maxLove,
|
||||
UnitSkillExp: skillExp,
|
||||
UnitSkillLevel: skillLevel,
|
||||
MaxHp: u.HpMax,
|
||||
UnitRemovableSkillCapacity: removableSkillCapacity,
|
||||
IsRankMax: true,
|
||||
IsLoveMax: true,
|
||||
IsLevelMax: true,
|
||||
IsSigned: isSigned,
|
||||
IsSkillLevelMax: true,
|
||||
IsRemovableSkillCapacityMax: true,
|
||||
InsertDate: time.Now().Unix(),
|
||||
}
|
||||
|
||||
_, err = userEng.Insert(&unitCommon)
|
||||
CheckErr(err)
|
||||
|
||||
log.Println("同步完成!")
|
||||
var userID []int
|
||||
err = db.UserEng.Table(new(usermodel.Users)).Cols("user_id").Find(&userID)
|
||||
CheckErr(err)
|
||||
|
||||
for _, id := range userID {
|
||||
userUnit := usermodel.UserUnitData{
|
||||
UnitID: u.UnitId,
|
||||
FavoriteFlag: false,
|
||||
DisplayRank: u.RankMax,
|
||||
UserID: id,
|
||||
InsertDate: time.Now().Unix(),
|
||||
}
|
||||
|
||||
// 检查表里是否已经有数据
|
||||
if !checked {
|
||||
ct, err := userEng.Table(new(usermodel.UserUnitData)).Count()
|
||||
CheckErr(err)
|
||||
|
||||
if ct == 0 {
|
||||
userUnit.UnitOwningUserID = 38383
|
||||
}
|
||||
|
||||
checked = true
|
||||
}
|
||||
|
||||
_, err = userEng.Insert(&userUnit)
|
||||
CheckErr(err)
|
||||
}
|
||||
}
|
||||
|
||||
err = userEng.Commit()
|
||||
CheckErr(err)
|
||||
|
||||
log.Println("同步完成!")
|
||||
return nil
|
||||
}
|
||||
|
||||
func CheckErr(err error) {
|
||||
if err != nil {
|
||||
userEng.Rollback()
|
||||
log.Fatalln("同步失败:", err.Error())
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package startup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"honoka-chan/internal/handler/ghome/account"
|
||||
usermodel "honoka-chan/internal/model/user"
|
||||
"log"
|
||||
@@ -10,16 +11,17 @@ const (
|
||||
defaultPassword = "klsbgames"
|
||||
)
|
||||
|
||||
func CreateDefaultUser() {
|
||||
func CreateDefaultUser() error {
|
||||
_, code, msg, created, err := account.AddUser(usermodel.DefaultSystemPhone, defaultPassword, true)
|
||||
if err != nil {
|
||||
log.Fatalln("默认用户创建失败:", err.Error())
|
||||
return fmt.Errorf("默认用户创建失败: %w", err)
|
||||
}
|
||||
if code != 0 {
|
||||
log.Fatalf("默认用户创建失败: code=%d msg=%s", code, msg)
|
||||
return fmt.Errorf("默认用户创建失败: code=%d msg=%s", code, msg)
|
||||
}
|
||||
if created {
|
||||
log.Printf("默认用户创建成功, 账号: %s, 密码: %s\n", usermodel.DefaultSystemPhone, defaultPassword)
|
||||
return
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
package startup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
usermodel "honoka-chan/internal/model/user"
|
||||
"honoka-chan/pkg/db"
|
||||
"log"
|
||||
)
|
||||
|
||||
func EnsureDefaultFriends() {
|
||||
func EnsureDefaultFriends() error {
|
||||
session := db.UserEng.NewSession()
|
||||
defer session.Close()
|
||||
|
||||
if err := session.Begin(); err != nil {
|
||||
log.Fatalln("初始化默认好友失败:", err.Error())
|
||||
return fmt.Errorf("初始化默认好友失败: %w", err)
|
||||
}
|
||||
|
||||
userIDs := []int{}
|
||||
@@ -20,19 +20,20 @@ func EnsureDefaultFriends() {
|
||||
Find(&userIDs)
|
||||
if err != nil {
|
||||
session.Rollback()
|
||||
log.Fatalln("初始化默认好友失败:", err.Error())
|
||||
return fmt.Errorf("初始化默认好友失败: %w", err)
|
||||
}
|
||||
|
||||
for _, userID := range userIDs {
|
||||
err = usermodel.EnsureDefaultFriendship(session, userID)
|
||||
if err != nil {
|
||||
session.Rollback()
|
||||
log.Fatalln("初始化默认好友失败:", err.Error())
|
||||
return fmt.Errorf("初始化默认好友失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := session.Commit(); err != nil {
|
||||
session.Rollback()
|
||||
log.Fatalln("初始化默认好友失败:", err.Error())
|
||||
return fmt.Errorf("初始化默认好友失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
package startup
|
||||
|
||||
func StartUp() {
|
||||
CreateTables()
|
||||
LoadUnitData()
|
||||
CreateDefaultUser()
|
||||
EnsureDefaultFriends()
|
||||
func StartUp() error {
|
||||
if err := CreateTables(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := LoadUnitData(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := CreateDefaultUser(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := EnsureDefaultFriends(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user