Files
hanfuit 645b64fe21 DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
2026-11-04 14:31:55 +08:00

301 lines
6.0 KiB
Go

package scheduler
import (
"context"
"fmt"
"log/slog"
"math/rand"
"sort"
"strings"
"sync"
"time"
)
type Job struct {
Name string
Interval time.Duration
Jitter time.Duration
Run func(context.Context) error
}
type JobSnapshot struct {
Name string `json:"name"`
IntervalMS int64 `json:"interval_ms"`
JitterMS int64 `json:"jitter_ms,omitempty"`
Running bool `json:"running"`
Runs int64 `json:"runs"`
Errors int64 `json:"errors"`
LastStarted *time.Time `json:"last_started,omitempty"`
LastEnded *time.Time `json:"last_ended,omitempty"`
LastError string `json:"last_error,omitempty"`
NextRun *time.Time `json:"next_run,omitempty"`
}
type Scheduler struct {
name string
logger *slog.Logger
mu sync.Mutex
jobs map[string]*jobState
running bool
cancel context.CancelFunc
wg sync.WaitGroup
now func() time.Time
jitter func(time.Duration) time.Duration
}
type jobState struct {
job Job
running bool
runs int64
errors int64
lastStarted time.Time
lastEnded time.Time
lastError string
nextRun time.Time
}
func New(name string, logger *slog.Logger) *Scheduler {
if name == "" {
name = "scheduler"
}
return &Scheduler{
name: name,
logger: logger,
jobs: make(map[string]*jobState),
now: time.Now,
jitter: randomJitter,
}
}
func (s *Scheduler) Name() string {
if s == nil {
return "scheduler"
}
return s.name
}
func (s *Scheduler) Add(job Job) error {
if s == nil {
return fmt.Errorf("scheduler is nil")
}
job.Name = strings.TrimSpace(job.Name)
if job.Name == "" {
return fmt.Errorf("scheduler job name is required")
}
if job.Interval <= 0 {
return fmt.Errorf("scheduler job %q interval must be positive", job.Name)
}
if job.Jitter < 0 {
return fmt.Errorf("scheduler job %q jitter must not be negative", job.Name)
}
if job.Run == nil {
return fmt.Errorf("scheduler job %q run function is required", job.Name)
}
s.mu.Lock()
defer s.mu.Unlock()
if s.running {
return fmt.Errorf("scheduler job %q cannot be added after start", job.Name)
}
if _, exists := s.jobs[job.Name]; exists {
return fmt.Errorf("scheduler job %q already exists", job.Name)
}
s.jobs[job.Name] = &jobState{job: job}
return nil
}
func (s *Scheduler) Start(ctx context.Context) error {
if s == nil {
return nil
}
if ctx == nil {
ctx = context.Background()
}
s.mu.Lock()
if s.running {
s.mu.Unlock()
return nil
}
if err := ctx.Err(); err != nil {
s.mu.Unlock()
return err
}
runCtx, cancel := context.WithCancel(ctx)
jobs := make([]*jobState, 0, len(s.jobs))
for _, state := range s.jobs {
jobs = append(jobs, state)
}
// worker 必须在 running 对 Stop 可见前全部登记,禁止 Wait 与 Add 交错。
s.wg.Add(len(jobs))
s.cancel = cancel
s.running = true
s.mu.Unlock()
for _, state := range jobs {
go func() {
defer s.wg.Done()
s.loop(runCtx, state)
}()
}
return nil
}
func (s *Scheduler) Stop(ctx context.Context) error {
if s == nil {
return nil
}
if ctx == nil {
ctx = context.Background()
}
s.mu.Lock()
cancel := s.cancel
if cancel == nil && !s.running {
s.mu.Unlock()
return nil
}
s.mu.Unlock()
if cancel != nil {
cancel()
}
done := make(chan struct{})
go func() {
s.wg.Wait()
s.mu.Lock()
s.cancel = nil
s.running = false
s.mu.Unlock()
close(done)
}()
select {
case <-done:
return nil
case <-ctx.Done():
return ctx.Err()
}
}
func (s *Scheduler) Snapshot() []JobSnapshot {
if s == nil {
return nil
}
s.mu.Lock()
defer s.mu.Unlock()
out := make([]JobSnapshot, 0, len(s.jobs))
for _, state := range s.jobs {
out = append(out, state.snapshot())
}
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
return out
}
func (s *Scheduler) loop(ctx context.Context, state *jobState) {
interval := state.job.Interval
delay := s.nextDelay(state)
s.setNextRun(state, s.now().Add(delay))
timer := time.NewTimer(delay)
defer timer.Stop()
for {
select {
case <-ctx.Done():
return
case <-timer.C:
s.runJob(ctx, state)
delay = interval + s.jitterDelay(state)
next := s.now().Add(delay)
s.setNextRun(state, next)
timer.Reset(delay)
}
}
}
func (s *Scheduler) nextDelay(state *jobState) time.Duration {
delay := state.job.Interval + s.jitterDelay(state)
if delay <= 0 {
return time.Nanosecond
}
return delay
}
func (s *Scheduler) jitterDelay(state *jobState) time.Duration {
if state == nil || state.job.Jitter <= 0 || s.jitter == nil {
return 0
}
delay := s.jitter(state.job.Jitter)
if delay < 0 {
return 0
}
if delay > state.job.Jitter {
return state.job.Jitter
}
return delay
}
func (s *Scheduler) runJob(ctx context.Context, state *jobState) {
now := s.now().UTC()
s.mu.Lock()
if state.running {
s.mu.Unlock()
return
}
state.running = true
state.lastStarted = now
s.mu.Unlock()
err := state.job.Run(ctx)
ended := s.now().UTC()
s.mu.Lock()
state.running = false
state.runs++
state.lastEnded = ended
if err != nil {
state.errors++
state.lastError = err.Error()
if s.logger != nil {
s.logger.Error("scheduler job failed", "job", state.job.Name, "error", err)
}
} else {
state.lastError = ""
}
s.mu.Unlock()
}
func (s *Scheduler) setNextRun(state *jobState, next time.Time) {
s.mu.Lock()
state.nextRun = next.UTC()
s.mu.Unlock()
}
func (s *jobState) snapshot() JobSnapshot {
snap := JobSnapshot{
Name: s.job.Name,
IntervalMS: s.job.Interval.Milliseconds(),
JitterMS: s.job.Jitter.Milliseconds(),
Running: s.running,
Runs: s.runs,
Errors: s.errors,
LastError: s.lastError,
}
if !s.lastStarted.IsZero() {
t := s.lastStarted
snap.LastStarted = &t
}
if !s.lastEnded.IsZero() {
t := s.lastEnded
snap.LastEnded = &t
}
if !s.nextRun.IsZero() {
t := s.nextRun
snap.NextRun = &t
}
return snap
}
func randomJitter(max time.Duration) time.Duration {
if max <= 0 {
return 0
}
return time.Duration(rand.Int63n(int64(max) + 1)) // #nosec G404 -- 非安全用途:调度抖动,不用于凭证或加密。
}