Files
90CNServer/server/internal/services/dnfbridge/gm_activity_switches.go
T
hanfuit 0ced464168 启动器新增游戏设置页(频道开关+活动开关),窗口标题改名
- 服务端新增活动开关运行时配置与 GM 接口(GET/POST /dnf/gm/activity-switches),
  持久化到 Settings,接入月签到/周签到/PC房↔累计在线的实时下发
- 服务端新增频道开关 GM 接口(channel-switches),目录生成时过滤禁用频道
- channelcatalog/channelinfo 支持按禁用频道 ID 过滤目录与客户端脚本
- 启动器新增「游戏设置」页面:频道开关 Tab + 活动开关 Tab
- native.py/server.py 新增对应转发 API
- 窗口标题/页面 title/侧边栏「90轮回」统一改为「DNF90 灵狐小竹整合单机版」
2026-11-04 20:08:50 +08:00

284 lines
9.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package dnfbridge
// gm_activity_switches.go 提供「活动开关」的运行时配置:服主在启动器的
// 「游戏设置 → 活动开关」页里切换某个活动是否下发,这里负责持久化 + 缓存,
// 并让活动目录下发(buildCurrentOnlineEventInfoBodyAt)与各活动投影读取它。
//
// 为什么单独一份 scope 而不是塞进 gm-gameplay:
//
// gameplay 那一份 scope 管的是"数值旋钮"(掉落率、倍率、疲劳、燃烧),
// 活动开关管的是"这个活动要不要在客户端显示/可参与",语义与读写频率都不同。
// 分开后,启动器页面可以各自独立刷新,也不用担心一份 scope 里键越堆越多。
//
// 持久化与 gameplay 设置同模式:repositories.Settings + scope + mutex 缓存。
// 缓存未就绪时首次读取落一次仓储,之后走内存;写入即时更新缓存并落盘。
//
// 开关的默认值(defaults)来自当前部署的真实状态,不是"全开":
//
// spend_time 累计在线得好礼(2347) 默认关:本部署走 PC 房泡点(228)分支
// level_aiming 等级预约(222) 默认关:按运营要求已关闭
// joust 骑士马战(2365) 默认关:活动已取消
// monthly 月度签到(2370) 默认开
// weekly 周周签到(2208) 默认开
// pcroom PC 房时间点(228) 默认开:当前生效的泡点活动
import (
"context"
"fmt"
"strconv"
"strings"
"time"
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
)
const gmActivitySwitchesScope = "global:dnf90:gm-activity-switches"
// 活动开关键名。值与 true/false 对应("1"/"true" 为开)。
const (
gmActivitySpendTimeKey = "spend_time"
gmActivityLevelAimingKey = "level_aiming"
gmActivityJoustKey = "joust"
gmActivityMonthlyKey = "monthly_attendance"
gmActivityWeeklyKey = "weekly_attendance"
gmActivityPCRoomKey = "pcroom_timepoint"
)
// gmActivitySwitchDefinition 描述一个可切换的活动。
type gmActivitySwitchDefinition struct {
Key string // 持久化键名
Title string // 中文名
Description string // 中文说明
Default bool // 默认开关状态
}
// gmActivitySwitchDefinitions 全部可切换活动(顺序即启动器展示顺序)。
var gmActivitySwitchDefinitions = []gmActivitySwitchDefinition{
{
Key: gmActivityPCRoomKey,
Title: "PC房时间点奖励",
Description: "泡点/挂机得奖励(事件 228/561)。当前部署默认开启。",
Default: true,
},
{
Key: gmActivitySpendTimeKey,
Title: "累计在线得好礼",
Description: "按累计在线时长领取奖励(事件 2347)。与 PC 房泡点互斥,默认关闭。",
Default: false,
},
{
Key: gmActivityMonthlyKey,
Title: "月度签到",
Description: "每月推荐副本通关计数领奖(事件 2370)。默认开启。",
Default: true,
},
{
Key: gmActivityWeeklyKey,
Title: "周周签到",
Description: "每周签到领奖(事件 2208)。默认开启。",
Default: true,
},
{
Key: gmActivityLevelAimingKey,
Title: "等级预约(命运/决策)",
Description: "全职LV预约得好礼(事件 222)。按运营要求默认关闭。",
Default: false,
},
{
Key: gmActivityJoustKey,
Title: "骑士马战",
Description: "骑士马战竞猜活动(事件 2365)。已取消,默认关闭。",
Default: false,
},
}
// GMActivitySwitch 单个活动的当前开关状态。
type GMActivitySwitch struct {
Key string `json:"key"`
Title string `json:"title"`
Description string `json:"description"`
Enabled bool `json:"enabled"`
}
// GMActivitySwitches 活动开关总览(供 GM 接口 / 启动器展示)。
type GMActivitySwitches struct {
Switches []GMActivitySwitch `json:"switches"`
UpdatedAt time.Time `json:"updated_at,omitempty"`
}
// gmActivitySwitchesFromRecord 从设置记录解析全部活动开关。
// 缺失的键回落对应默认值;非法值回落默认值(不做硬失败,保持可恢复)。
func gmActivitySwitchesFromRecord(record dnfrepo.SettingsRecord, found bool) map[string]bool {
out := make(map[string]bool, len(gmActivitySwitchDefinitions))
for _, def := range gmActivitySwitchDefinitions {
out[def.Key] = def.Default
}
if !found {
return out
}
for _, def := range gmActivitySwitchDefinitions {
raw := strings.TrimSpace(record.Values[def.Key])
if raw == "" {
continue
}
out[def.Key] = raw == "1" || strings.EqualFold(raw, "true")
}
return out
}
// GMActivitySwitches 返回当前活动开关总览(带缓存)。
func (s *Service) GMActivitySwitches(ctx context.Context) (GMActivitySwitches, error) {
if s == nil {
return GMActivitySwitches{}, ErrGMUnavailable
}
s.gmActivitySwitchesMu.Lock()
defer s.gmActivitySwitchesMu.Unlock()
if s.gmActivitySwitchesReady {
return s.gmActivitySwitchesSnapshotLocked(), nil
}
repositories, ok := s.repositoryGroup()
if !ok || repositories.Settings == nil {
return GMActivitySwitches{}, ErrGMUnavailable
}
record, found, err := repositories.Settings.Load(ctx, gmActivitySwitchesScope)
if err != nil {
return GMActivitySwitches{}, err
}
s.gmActivitySwitches = gmActivitySwitchesFromRecord(record, found)
s.gmActivitySwitchesAt = record.UpdatedAt
s.gmActivitySwitchesReady = true
return s.gmActivitySwitchesSnapshotLocked(), nil
}
// GMSetActivitySwitch 切换某个活动的开关状态,返回更新后的总览。
func (s *Service) GMSetActivitySwitch(ctx context.Context, key string, enabled bool) (GMActivitySwitches, error) {
if s == nil {
return GMActivitySwitches{}, ErrGMUnavailable
}
key = strings.TrimSpace(strings.ToLower(key))
if !validGMActivitySwitchKey(key) {
return GMActivitySwitches{}, fmt.Errorf("%w: 未知的活动开关 %q", ErrGMInvalidRequest, key)
}
s.gmActivitySwitchesMu.Lock()
defer s.gmActivitySwitchesMu.Unlock()
repositories, ok := s.repositoryGroup()
if !ok || repositories.Settings == nil {
return GMActivitySwitches{}, ErrGMUnavailable
}
// 先确保缓存已就绪(读当前值),再改。
if !s.gmActivitySwitchesReady {
record, found, err := repositories.Settings.Load(ctx, gmActivitySwitchesScope)
if err != nil {
return GMActivitySwitches{}, err
}
s.gmActivitySwitches = gmActivitySwitchesFromRecord(record, found)
s.gmActivitySwitchesAt = record.UpdatedAt
s.gmActivitySwitchesReady = true
}
// 读最新记录用于落盘(避免覆盖其它并发写入)。
record, found, err := repositories.Settings.Load(ctx, gmActivitySwitchesScope)
if err != nil {
return GMActivitySwitches{}, err
}
if !found {
record = dnfrepo.SettingsRecord{Scope: gmActivitySwitchesScope}
} else {
record = dnfrepo.CloneSettings(record)
}
if record.Values == nil {
record.Values = make(map[string]string)
}
record.Values[key] = strconv.FormatBool(enabled)
record.UpdatedAt = time.Now().UTC()
if err := dnfrepo.SaveSettingsFields(ctx, repositories.Settings, record, dnfrepo.SettingsFieldValues); err != nil {
return GMActivitySwitches{}, err
}
s.gmActivitySwitches[key] = enabled
s.gmActivitySwitchesAt = record.UpdatedAt
return s.gmActivitySwitchesSnapshotLocked(), nil
}
// activitySwitchEnabled 读取某个活动的当前开关(内部无锁读取,调用方自行加锁)。
func (s *Service) activitySwitchEnabledLocked(key string) bool {
if s == nil || s.gmActivitySwitches == nil {
return false
}
enabled, ok := s.gmActivitySwitches[key]
return ok && enabled
}
// ActivitySwitchEnabled 读取某个活动的当前开关(公开、带锁)。
//
// 语义:开关「开启」意味着该活动照常下发/可参与;「关闭」意味着不下发、
// 客户端不显示该活动。未持久化过时回落定义里的默认值。
//
// 注意默认值的含义:本方法在缓存未就绪时会尽力加载一次(懒加载),
// 加载失败或仓储不可用时,回落"默认值"——即该活动在定义里的 Default。
// 这样即使 GM 接口从未被调用,玩法侧也能按部署默认(而非全关)工作。
func (s *Service) ActivitySwitchEnabled(ctx context.Context, key string) bool {
if s == nil {
return false
}
key = strings.TrimSpace(strings.ToLower(key))
s.gmActivitySwitchesMu.Lock()
defer s.gmActivitySwitchesMu.Unlock()
if !s.gmActivitySwitchesReady {
if repositories, ok := s.repositoryGroup(); ok && repositories.Settings != nil {
if record, found, err := repositories.Settings.Load(ctx, gmActivitySwitchesScope); err == nil {
s.gmActivitySwitches = gmActivitySwitchesFromRecord(record, found)
s.gmActivitySwitchesAt = record.UpdatedAt
s.gmActivitySwitchesReady = true
}
}
}
if s.gmActivitySwitches == nil {
for _, def := range gmActivitySwitchDefinitions {
if def.Key == key {
return def.Default
}
}
return false
}
if enabled, ok := s.gmActivitySwitches[key]; ok {
return enabled
}
for _, def := range gmActivitySwitchDefinitions {
if def.Key == key {
return def.Default
}
}
return false
}
// gmActivitySwitchesSnapshotLocked 组装总览(调用方须持 gmActivitySwitchesMu)。
func (s *Service) gmActivitySwitchesSnapshotLocked() GMActivitySwitches {
switches := make([]GMActivitySwitch, 0, len(gmActivitySwitchDefinitions))
for _, def := range gmActivitySwitchDefinitions {
enabled := def.Default
if s.gmActivitySwitches != nil {
if value, ok := s.gmActivitySwitches[def.Key]; ok {
enabled = value
}
}
switches = append(switches, GMActivitySwitch{
Key: def.Key,
Title: def.Title,
Description: def.Description,
Enabled: enabled,
})
}
return GMActivitySwitches{
Switches: switches,
UpdatedAt: s.gmActivitySwitchesAt,
}
}
func validGMActivitySwitchKey(key string) bool {
for _, def := range gmActivitySwitchDefinitions {
if def.Key == key {
return true
}
}
return false
}