更新若干功能

This commit is contained in:
李琦
2026-08-15 17:18:00 +08:00
parent 4954295961
commit 6a81e479ef
77 changed files with 8165 additions and 2372 deletions

View File

@@ -1,15 +1,11 @@
package main
// filestorage.go 全局「文件存储方式」配置:由管理员(云端账号 id=1)在设置页配置,
// 权威数据存远端 MySQL 的 sync_settings 表(user_id=1, name='file_storage' 行)。
// 保存时直接写库立即生效;各端上传图片时实时拉取该配置(短 TTL 缓存),
// 权威数据经 nl-pms-api settings(全局键 file_storage)。
// 保存时直接写 API 立即生效;各端上传图片时实时拉取该配置(短 TTL 缓存),
// 拉取失败(离线/未配置同步)回退本地缓存副本(由同步循环下发,见 syncFileStorageConfig)。
// mode=local 时一切维持本地行为;mode=server 时内容图与头像选图会上传到
// nl-pms-api 换取 http URL(远程/跨设备场景无法使用本地路径)。
import (
"context"
"database/sql"
"encoding/json"
"errors"
"net/http"
@@ -23,7 +19,6 @@ const (
fileStorageTTL = 30 * time.Second // 实时配置的内存缓存时长,避免连续上传反复查库
)
// normalizeFileStorage 收敛非法值:mode 只认 local/server,地址去尾斜杠。
func normalizeFileStorage(c FileStorageConfig) FileStorageConfig {
if c.Mode != "server" {
c.Mode = "local"
@@ -33,37 +28,35 @@ func normalizeFileStorage(c FileStorageConfig) FileStorageConfig {
return c
}
// fileStorageConfig 读本地缓存的全局配置(未配置返回 local 零值)。
// defaultFileStorage 未配置时的默认:服务器模式,地址与打包同步 API 基址一致。
func (a *App) defaultFileStorage() FileStorageConfig {
return normalizeFileStorage(FileStorageConfig{
Mode: "server",
BaseURL: a.apiBaseURL(),
})
}
func (a *App) fileStorageConfig() FileStorageConfig {
var c FileStorageConfig
if raw := a.store.Meta(fileStorageKey); raw != "" {
_ = json.Unmarshal([]byte(raw), &c)
return normalizeFileStorage(c)
}
return normalizeFileStorage(c)
return a.defaultFileStorage()
}
// fetchRemoteFileStorage 从远端 MySQL 读权威配置。第二个返回值表示"远端可用且结果权威"
// (无行记录视作权威的 local);成功时顺带刷新本地缓存副本,供离线兜底。
// fetchRemoteFileStorage 从 API 读权威配置。第二个返回值表示"远端可用且结果权威"
// (无行记录时回落默认服务器配置);成功时顺带刷新本地缓存副本,供离线兜底。
func (a *App) fetchRemoteFileStorage() (FileStorageConfig, bool) {
base := a.ctx
if base == nil {
base = context.Background()
if a.syncUserID() <= 0 || a.store.Meta("sync_access_token") == "" || a.apiBaseURL() == "" {
return FileStorageConfig{}, false
}
ctx, cancel := context.WithTimeout(base, 5*time.Second)
defer cancel()
db, e := a.openRemote(ctx)
val, at, ok, e := a.apiGetGlobalSetting(fileStorageKey)
if e != nil {
return FileStorageConfig{}, false
}
defer db.Close()
var val, at string
e = db.QueryRowContext(ctx, `SELECT value,updated_at FROM sync_settings WHERE user_id=? AND name=?`,
festivalAdminID, fileStorageKey).Scan(&val, &at)
if e == sql.ErrNoRows {
return FileStorageConfig{}, true // 管理员尚未配置 → 权威的 local
}
if e != nil {
return FileStorageConfig{}, false
if !ok {
return a.defaultFileStorage(), true
}
var c FileStorageConfig
if json.Unmarshal([]byte(val), &c) != nil {
@@ -77,9 +70,6 @@ func (a *App) fetchRemoteFileStorage() (FileStorageConfig, bool) {
return c, true
}
// currentFileStorage 返回上传时应采用的全局配置:优先远端实时值(TTL 内存缓存),
// 远端不可达时回退本地缓存副本。管理员改完配置后各端最迟 TTL 内生效,无需等同步轮。
// 失败结果同样缓存 TTL,避免离线时每次上传都白等一轮连接超时。
func (a *App) currentFileStorage() FileStorageConfig {
a.fsMu.Lock()
if !a.fsCfgAt.IsZero() && time.Since(a.fsCfgAt) < fileStorageTTL {
@@ -98,15 +88,12 @@ func (a *App) currentFileStorage() FileStorageConfig {
return c
}
// invalidateFileStorageCache 让下一次读取强制回源(保存配置后调用)。
func (a *App) invalidateFileStorageCache() {
a.fsMu.Lock()
a.fsCfgAt = time.Time{}
a.fsMu.Unlock()
}
// GetFileStorageConfig 返回全局文件存储配置(远端优先,离线回本地缓存)。
// 所有账号可读:界面据此决定头像/内容图的存储走向与素材库可用性。
func (a *App) GetFileStorageConfig() (FileStorageConfig, error) {
if e := a.ready(); e != nil {
return FileStorageConfig{}, e
@@ -114,8 +101,7 @@ func (a *App) GetFileStorageConfig() (FileStorageConfig, error) {
return a.currentFileStorage(), nil
}
// SaveFileStorageConfig 管理员(id=1)保存全局配置:直接写远端 MySQL 立即全员生效,
// 同时更新本地缓存副本与 LWW 时间戳(同步循环不会再把旧值推回)。要求在线。
// SaveFileStorageConfig 管理员(id=1)保存全局配置:直接写 API 立即全员生效。
func (a *App) SaveFileStorageConfig(c FileStorageConfig) error {
if e := a.ready(); e != nil {
return e
@@ -131,18 +117,9 @@ func (a *App) SaveFileStorageConfig(c FileStorageConfig) error {
if e != nil {
return e
}
ctx, cancel := context.WithTimeout(a.ctx, syncTimeout)
defer cancel()
db, e := a.openRemote(ctx)
if e != nil {
return e
}
defer db.Close()
now := nowRFC()
if _, e = db.ExecContext(ctx, `INSERT INTO sync_settings(user_id,name,value,updated_at) VALUES(?,?,?,?)
ON DUPLICATE KEY UPDATE value=VALUES(value), updated_at=VALUES(updated_at)`,
festivalAdminID, fileStorageKey, string(b), now); e != nil {
return mapSyncErr(e)
if e := a.apiPutSettingWithStepUp(fileStorageKey, string(b), now, true); e != nil {
return e
}
if e := a.store.SetMeta(fileStorageKey, string(b)); e != nil {
return e
@@ -153,7 +130,7 @@ func (a *App) SaveFileStorageConfig(c FileStorageConfig) error {
return nil
}
// TestFileStorage 探测服务器连通性(GET {baseURL}/healthz),配置界面「测试连接」用。
// TestFileStorage 探测服务器连通性(GET {baseURL}/healthz)。
func (a *App) TestFileStorage(c FileStorageConfig) error {
if e := a.ready(); e != nil {
return e