DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
commit
645b64fe21
4388 files changed
+1405457
No files matched your search
@@ -0,0 +1,205 @@
|
||||
package hotreload
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// DrainBarrier 在热切换前停止接收新调用,并等待已进入的调用自然结束。
|
||||
type DrainBarrier struct {
|
||||
mu sync.Mutex
|
||||
initialized bool
|
||||
accepting bool
|
||||
active int
|
||||
drainWaiters []chan struct{}
|
||||
enterWaiters []chan struct{}
|
||||
}
|
||||
|
||||
// BarrierSnapshot 描述热切换屏障当前的接入、活跃调用和等待队列状态。
|
||||
type BarrierSnapshot struct {
|
||||
Accepting bool `json:"accepting"`
|
||||
Active int `json:"active"`
|
||||
Waiters int `json:"waiters"`
|
||||
DrainWaiters int `json:"drain_waiters,omitempty"`
|
||||
EnterWaiters int `json:"enter_waiters,omitempty"`
|
||||
}
|
||||
|
||||
// NewDrainBarrier 创建默认允许进入的热切换屏障。
|
||||
func NewDrainBarrier() *DrainBarrier {
|
||||
return &DrainBarrier{initialized: true, accepting: true}
|
||||
}
|
||||
|
||||
// Enter 在屏障接受新调用时进入活跃区,并返回幂等的离开函数。
|
||||
func (b *DrainBarrier) Enter(ctx context.Context) (func(), error) {
|
||||
if b == nil {
|
||||
return func() {}, nil
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b.mu.Lock()
|
||||
b.ensureReadyLocked()
|
||||
defer b.mu.Unlock()
|
||||
if !b.accepting {
|
||||
return nil, ErrBarrierDraining
|
||||
}
|
||||
b.active++
|
||||
once := sync.Once{}
|
||||
return func() {
|
||||
once.Do(func() {
|
||||
b.mu.Lock()
|
||||
if b.active > 0 {
|
||||
b.active--
|
||||
}
|
||||
b.notifyDrainLocked()
|
||||
b.mu.Unlock()
|
||||
})
|
||||
}, nil
|
||||
}
|
||||
|
||||
// EnterWhenReady 会等待屏障恢复接受新调用后进入活跃区。
|
||||
func (b *DrainBarrier) EnterWhenReady(ctx context.Context) (func(), error) {
|
||||
if b == nil {
|
||||
return func() {}, nil
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
for {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b.mu.Lock()
|
||||
b.ensureReadyLocked()
|
||||
if b.accepting {
|
||||
b.active++
|
||||
b.mu.Unlock()
|
||||
once := sync.Once{}
|
||||
return func() {
|
||||
once.Do(func() {
|
||||
b.mu.Lock()
|
||||
if b.active > 0 {
|
||||
b.active--
|
||||
}
|
||||
b.notifyDrainLocked()
|
||||
b.mu.Unlock()
|
||||
})
|
||||
}, nil
|
||||
}
|
||||
waiter := make(chan struct{})
|
||||
b.enterWaiters = append(b.enterWaiters, waiter)
|
||||
b.mu.Unlock()
|
||||
select {
|
||||
case <-waiter:
|
||||
case <-ctx.Done():
|
||||
b.mu.Lock()
|
||||
b.enterWaiters = removeWaiter(b.enterWaiters, waiter)
|
||||
b.mu.Unlock()
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Drain 停止接收新调用,并等待已进入的调用全部离开。
|
||||
func (b *DrainBarrier) Drain(ctx context.Context) error {
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
b.mu.Lock()
|
||||
b.ensureReadyLocked()
|
||||
b.accepting = false
|
||||
for b.active > 0 {
|
||||
waiter := make(chan struct{})
|
||||
b.drainWaiters = append(b.drainWaiters, waiter)
|
||||
b.mu.Unlock()
|
||||
select {
|
||||
case <-waiter:
|
||||
case <-ctx.Done():
|
||||
b.mu.Lock()
|
||||
b.drainWaiters = removeWaiter(b.drainWaiters, waiter)
|
||||
b.mu.Unlock()
|
||||
return ctx.Err()
|
||||
}
|
||||
b.mu.Lock()
|
||||
}
|
||||
b.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Resume 重新允许调用进入,并唤醒等待恢复的调用方。
|
||||
func (b *DrainBarrier) Resume() {
|
||||
if b == nil {
|
||||
return
|
||||
}
|
||||
b.mu.Lock()
|
||||
b.ensureReadyLocked()
|
||||
b.accepting = true
|
||||
b.notifyEnterLocked()
|
||||
b.mu.Unlock()
|
||||
}
|
||||
|
||||
// Snapshot 返回屏障状态快照,便于管理端观测热切换过程。
|
||||
func (b *DrainBarrier) Snapshot() BarrierSnapshot {
|
||||
if b == nil {
|
||||
return BarrierSnapshot{Accepting: true}
|
||||
}
|
||||
b.mu.Lock()
|
||||
b.ensureReadyLocked()
|
||||
snapshot := BarrierSnapshot{
|
||||
Accepting: b.accepting,
|
||||
Active: b.active,
|
||||
DrainWaiters: len(b.drainWaiters),
|
||||
EnterWaiters: len(b.enterWaiters),
|
||||
}
|
||||
snapshot.Waiters = snapshot.DrainWaiters + snapshot.EnterWaiters
|
||||
b.mu.Unlock()
|
||||
return snapshot
|
||||
}
|
||||
|
||||
func (b *DrainBarrier) ensureReadyLocked() {
|
||||
if b.initialized {
|
||||
return
|
||||
}
|
||||
b.initialized = true
|
||||
b.accepting = true
|
||||
}
|
||||
|
||||
func (b *DrainBarrier) notifyDrainLocked() {
|
||||
if b.active > 0 {
|
||||
return
|
||||
}
|
||||
waiters := b.drainWaiters
|
||||
b.drainWaiters = nil
|
||||
for _, waiter := range waiters {
|
||||
close(waiter)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *DrainBarrier) notifyEnterLocked() {
|
||||
waiters := b.enterWaiters
|
||||
b.enterWaiters = nil
|
||||
for _, waiter := range waiters {
|
||||
close(waiter)
|
||||
}
|
||||
}
|
||||
|
||||
func removeWaiter(waiters []chan struct{}, target chan struct{}) []chan struct{} {
|
||||
for i, waiter := range waiters {
|
||||
if waiter != target {
|
||||
continue
|
||||
}
|
||||
copy(waiters[i:], waiters[i+1:])
|
||||
waiters[len(waiters)-1] = nil
|
||||
return waiters[:len(waiters)-1]
|
||||
}
|
||||
return waiters
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
package hotreload
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Registry 保存 route 到 handler 版本的映射,并负责原子切换当前版本。
|
||||
type Registry struct {
|
||||
mu sync.RWMutex
|
||||
routes map[string]*routeSlot
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
type routeSlot struct {
|
||||
mu sync.RWMutex
|
||||
activateMu sync.Mutex
|
||||
route string
|
||||
modules map[string]Module
|
||||
active string
|
||||
swaps int64
|
||||
updatedAt time.Time
|
||||
}
|
||||
|
||||
// NewRegistry 创建热重载版本注册表。
|
||||
func NewRegistry(options Options) *Registry {
|
||||
registry := &Registry{now: options.Now}
|
||||
_ = registry.ensureReady()
|
||||
return registry
|
||||
}
|
||||
|
||||
// Register 注册一个 route 的可切换实现版本。
|
||||
func (r *Registry) Register(module Module) error {
|
||||
module, err := normalizeModule(module)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.ensureReady(); err != nil {
|
||||
return err
|
||||
}
|
||||
r.mu.Lock()
|
||||
slot := r.routes[module.Route]
|
||||
if slot == nil {
|
||||
slot = &routeSlot{
|
||||
route: module.Route,
|
||||
modules: make(map[string]Module),
|
||||
}
|
||||
r.routes[module.Route] = slot
|
||||
}
|
||||
r.mu.Unlock()
|
||||
|
||||
slot.mu.Lock()
|
||||
if _, ok := slot.modules[module.Version]; ok {
|
||||
slot.mu.Unlock()
|
||||
return ErrVersionExists
|
||||
}
|
||||
slot.modules[module.Version] = module
|
||||
if slot.active == "" {
|
||||
slot.active = module.Version
|
||||
slot.updatedAt = r.now()
|
||||
}
|
||||
slot.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Activate 将指定 route 原子切换到目标版本。
|
||||
func (r *Registry) Activate(ctx context.Context, route, version string) (Result, error) {
|
||||
route, version, err := normRouteVersion(route, version)
|
||||
if err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
if err := r.ensureReady(); err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
slot := r.routeSlot(route)
|
||||
if slot == nil {
|
||||
return Result{}, ErrRouteNotFound
|
||||
}
|
||||
|
||||
var previous Module
|
||||
var hasPrevious bool
|
||||
unlock, err := slot.lockActivate(ctx)
|
||||
if err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
defer unlock()
|
||||
if err := ctx.Err(); err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
|
||||
slot.mu.RLock()
|
||||
module, ok := slot.modules[version]
|
||||
if !ok {
|
||||
slot.mu.RUnlock()
|
||||
return Result{}, ErrVersionNotFound
|
||||
}
|
||||
if slot.active == version {
|
||||
result := Result{Route: route, PreviousVersion: version, ActiveVersion: version, ActivatedAt: slot.updatedAt}
|
||||
slot.mu.RUnlock()
|
||||
return result, nil
|
||||
}
|
||||
if active := slot.active; active != "" {
|
||||
previous, hasPrevious = slot.modules[active]
|
||||
}
|
||||
slot.mu.RUnlock()
|
||||
|
||||
if module.Preflight != nil {
|
||||
if err := module.Preflight(ctx, module); err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
if module.Activate != nil {
|
||||
if err := module.Activate(ctx, module); err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
now := r.now()
|
||||
|
||||
slot.mu.Lock()
|
||||
result := Result{
|
||||
Route: route,
|
||||
PreviousVersion: slot.active,
|
||||
ActiveVersion: version,
|
||||
ActivatedAt: now,
|
||||
}
|
||||
slot.active = version
|
||||
slot.updatedAt = now
|
||||
slot.swaps++
|
||||
slot.mu.Unlock()
|
||||
|
||||
if hasPrevious && previous.Version != version && previous.Deactivate != nil {
|
||||
if err := previous.Deactivate(ctx, previous); err != nil {
|
||||
result.DeactivateError = err.Error()
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Rollback 通过重新激活旧版本完成 route 回滚。
|
||||
func (r *Registry) Rollback(ctx context.Context, route, version string) (Result, error) {
|
||||
return r.Activate(ctx, route, version)
|
||||
}
|
||||
|
||||
// Dispatch 将请求分发到 route 当前激活版本的 handler。
|
||||
func (r *Registry) Dispatch(ctx context.Context, route string, payload any, metadata map[string]any) (Response, error) {
|
||||
route = normalizeName(route)
|
||||
if route == "" {
|
||||
return Response{}, ErrRouteRequired
|
||||
}
|
||||
if err := r.ensureReady(); err != nil {
|
||||
return Response{}, err
|
||||
}
|
||||
slot := r.routeSlot(route)
|
||||
if slot == nil {
|
||||
return Response{}, ErrRouteNotFound
|
||||
}
|
||||
slot.mu.RLock()
|
||||
module, ok := slot.modules[slot.active]
|
||||
slot.mu.RUnlock()
|
||||
if !ok || module.Handler == nil {
|
||||
return Response{}, ErrHandlerUnavailable
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return Response{}, err
|
||||
}
|
||||
return module.Handler(ctx, Request{
|
||||
Route: route,
|
||||
Version: module.Version,
|
||||
Payload: payload,
|
||||
Metadata: copyMetadata(metadata),
|
||||
})
|
||||
}
|
||||
|
||||
// ActiveVersion 查询 route 当前激活版本。
|
||||
func (r *Registry) ActiveVersion(route string) (string, bool) {
|
||||
route = normalizeName(route)
|
||||
if route == "" {
|
||||
return "", false
|
||||
}
|
||||
if err := r.ensureReady(); err != nil {
|
||||
return "", false
|
||||
}
|
||||
slot := r.routeSlot(route)
|
||||
if slot == nil {
|
||||
return "", false
|
||||
}
|
||||
slot.mu.RLock()
|
||||
version := slot.active
|
||||
slot.mu.RUnlock()
|
||||
return version, version != ""
|
||||
}
|
||||
|
||||
// Snapshot 返回所有 route 的稳定排序快照。
|
||||
func (r *Registry) Snapshot() Snapshot {
|
||||
if err := r.ensureReady(); err != nil {
|
||||
return Snapshot{}
|
||||
}
|
||||
r.mu.RLock()
|
||||
slots := make([]*routeSlot, 0, len(r.routes))
|
||||
for _, slot := range r.routes {
|
||||
slots = append(slots, slot)
|
||||
}
|
||||
r.mu.RUnlock()
|
||||
sort.Slice(slots, func(i, j int) bool { return slots[i].route < slots[j].route })
|
||||
|
||||
snapshot := Snapshot{Routes: make([]RouteSnapshot, 0, len(slots))}
|
||||
for _, slot := range slots {
|
||||
slot.mu.RLock()
|
||||
versions := make([]string, 0, len(slot.modules))
|
||||
for version := range slot.modules {
|
||||
versions = append(versions, version)
|
||||
}
|
||||
sort.Strings(versions)
|
||||
snapshot.Routes = append(snapshot.Routes, RouteSnapshot{
|
||||
Route: slot.route,
|
||||
ActiveVersion: slot.active,
|
||||
Versions: versions,
|
||||
Swaps: slot.swaps,
|
||||
UpdatedAt: slot.updatedAt,
|
||||
})
|
||||
slot.mu.RUnlock()
|
||||
}
|
||||
return snapshot
|
||||
}
|
||||
|
||||
func (r *Registry) ensureReady() error {
|
||||
if r == nil {
|
||||
return ErrRouteNotFound
|
||||
}
|
||||
r.mu.Lock()
|
||||
if r.routes == nil {
|
||||
r.routes = make(map[string]*routeSlot)
|
||||
}
|
||||
if r.now == nil {
|
||||
r.now = func() time.Time { return time.Now().UTC() }
|
||||
}
|
||||
r.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Registry) routeSlot(route string) *routeSlot {
|
||||
r.mu.RLock()
|
||||
slot := r.routes[route]
|
||||
r.mu.RUnlock()
|
||||
return slot
|
||||
}
|
||||
|
||||
func (s *routeSlot) lockActivate(ctx context.Context) (func(), error) {
|
||||
if s.activateMu.TryLock() {
|
||||
return s.activateMu.Unlock, nil
|
||||
}
|
||||
timer := time.NewTimer(time.Millisecond)
|
||||
defer timer.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case <-timer.C:
|
||||
}
|
||||
if s.activateMu.TryLock() {
|
||||
return s.activateMu.Unlock, nil
|
||||
}
|
||||
timer.Reset(time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
// IsNotFound 判断错误是否属于热重载 route 或版本不存在。
|
||||
func IsNotFound(err error) bool {
|
||||
return errors.Is(err, ErrRouteNotFound) || errors.Is(err, ErrVersionNotFound)
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package hotreload
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrRouteRequired 表示热重载 route 为空。
|
||||
ErrRouteRequired = errors.New("hot reload route is required")
|
||||
|
||||
// ErrVersionRequired 表示热重载版本为空。
|
||||
ErrVersionRequired = errors.New("hot reload version is required")
|
||||
|
||||
// ErrHandlerRequired 表示模块缺少 handler。
|
||||
ErrHandlerRequired = errors.New("hot reload handler is required")
|
||||
|
||||
// ErrRouteNotFound 表示 route 尚未注册。
|
||||
ErrRouteNotFound = errors.New("hot reload route is not found")
|
||||
|
||||
// ErrVersionNotFound 表示目标版本尚未注册。
|
||||
ErrVersionNotFound = errors.New("hot reload version is not found")
|
||||
|
||||
// ErrVersionExists 表示同一 route 下的版本已经注册,不能被静默覆盖。
|
||||
ErrVersionExists = errors.New("hot reload version already exists")
|
||||
|
||||
// ErrHandlerUnavailable 表示当前激活版本没有可用 handler。
|
||||
ErrHandlerUnavailable = errors.New("hot reload handler is unavailable")
|
||||
|
||||
// ErrBarrierDraining 表示热切换屏障正在排空活跃调用。
|
||||
ErrBarrierDraining = errors.New("hot reload barrier is draining")
|
||||
|
||||
// ErrInvalidMetadataName 表示 metadata 字段名非法。
|
||||
ErrInvalidMetadataName = errors.New("hot reload metadata name is invalid")
|
||||
)
|
||||
|
||||
// Request 是安全热重载 handler 的最小调用上下文。
|
||||
type Request struct {
|
||||
Route string `json:"route"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Payload any `json:"payload,omitempty"`
|
||||
Metadata map[string]any `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// Response 是安全热重载 handler 的最小返回结构。
|
||||
type Response struct {
|
||||
Payload any `json:"payload,omitempty"`
|
||||
Metadata map[string]any `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// Handler 是热重载 route 的业务处理函数。
|
||||
type Handler func(context.Context, Request) (Response, error)
|
||||
|
||||
// Hook 是热重载模块激活、预检和停用时执行的钩子。
|
||||
type Hook func(context.Context, Module) error
|
||||
|
||||
// Module 表示一个可在线切换的 route 实现版本。
|
||||
type Module struct {
|
||||
Route string `json:"route"`
|
||||
Version string `json:"version"`
|
||||
Metadata map[string]any `json:"metadata,omitempty"`
|
||||
Handler Handler `json:"-"`
|
||||
Preflight Hook `json:"-"`
|
||||
Activate Hook `json:"-"`
|
||||
Deactivate Hook `json:"-"`
|
||||
}
|
||||
|
||||
// Options 定义热重载 registry 的可注入依赖。
|
||||
type Options struct {
|
||||
Now func() time.Time
|
||||
}
|
||||
|
||||
// Result 描述一次版本切换或回滚的结果。
|
||||
type Result struct {
|
||||
Route string `json:"route"`
|
||||
PreviousVersion string `json:"previous_version,omitempty"`
|
||||
ActiveVersion string `json:"active_version"`
|
||||
ActivatedAt time.Time `json:"activated_at"`
|
||||
DeactivateError string `json:"deactivate_error,omitempty"`
|
||||
}
|
||||
|
||||
// RouteSnapshot 描述单个 route 的当前版本和已注册版本。
|
||||
type RouteSnapshot struct {
|
||||
Route string `json:"route"`
|
||||
ActiveVersion string `json:"active_version,omitempty"`
|
||||
Versions []string `json:"versions,omitempty"`
|
||||
Swaps int64 `json:"swaps"`
|
||||
UpdatedAt time.Time `json:"updated_at,omitempty"`
|
||||
}
|
||||
|
||||
// Snapshot 描述热重载 registry 的整体快照。
|
||||
type Snapshot struct {
|
||||
Routes []RouteSnapshot `json:"routes,omitempty"`
|
||||
}
|
||||
|
||||
func normalizeModule(module Module) (Module, error) {
|
||||
module.Route = normalizeName(module.Route)
|
||||
if module.Route == "" {
|
||||
return Module{}, ErrRouteRequired
|
||||
}
|
||||
module.Version = normalizeName(module.Version)
|
||||
if module.Version == "" {
|
||||
return Module{}, ErrVersionRequired
|
||||
}
|
||||
if module.Handler == nil {
|
||||
return Module{}, ErrHandlerRequired
|
||||
}
|
||||
module.Metadata = copyMetadata(module.Metadata)
|
||||
return module, nil
|
||||
}
|
||||
|
||||
func normRouteVersion(route, version string) (string, string, error) {
|
||||
route = normalizeName(route)
|
||||
if route == "" {
|
||||
return "", "", ErrRouteRequired
|
||||
}
|
||||
version = normalizeName(version)
|
||||
if version == "" {
|
||||
return "", "", ErrVersionRequired
|
||||
}
|
||||
return route, version, nil
|
||||
}
|
||||
|
||||
func normalizeName(value string) string {
|
||||
return strings.TrimSpace(value)
|
||||
}
|
||||
|
||||
func copyMetadata(in map[string]any) map[string]any {
|
||||
if len(in) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string]any, len(in))
|
||||
for key, value := range in {
|
||||
key = strings.TrimSpace(key)
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
out[key] = value
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in new issue
Block a user