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

347 lines
11 KiB
Go

package adminworkflow
import (
"context"
"errors"
"fmt"
"strings"
"time"
"longheng.io/server/internal/platform/admincmd"
"longheng.io/server/internal/platform/audit"
)
var (
ErrServiceRequired = errors.New("admin workflow service is required")
ErrCatalogRequired = errors.New("admin workflow catalog is required")
ErrRBACRequired = errors.New("admin workflow RBAC checker is required")
ErrAuditRequired = errors.New("admin workflow audit logger is required")
ErrRBACDenied = errors.New("admin workflow RBAC denied")
)
const (
ActionDryRun = "dry_run"
ActionSubmitApproval = "submit_approval"
ActionApprove = "approve"
ActionReject = "reject"
ActionExecute = "execute"
)
type RBACChecker interface {
CheckAdminWorkflow(context.Context, WorkflowTemplate, admincmd.Command, string, string) error
}
type RBACFunc func(context.Context, WorkflowTemplate, admincmd.Command, string, string) error
func (fn RBACFunc) CheckAdminWorkflow(ctx context.Context, template WorkflowTemplate, command admincmd.Command, action, actor string) error {
if fn == nil {
return ErrRBACRequired
}
return fn(ctx, template, command, action, actor)
}
type OrchestratorOptions struct {
Service *Service
Catalog Catalog
RBAC RBACChecker
Audit *audit.Logger
Now func() time.Time
}
type Orchestrator struct {
service *Service
catalog Catalog
rbac RBACChecker
audit *audit.Logger
now func() time.Time
}
type BeginRequest struct {
Workflow string
Command admincmd.Command
Previewer Previewer
SubmitApproval bool
}
type DecisionRequest struct {
ID string
Decision admincmd.ApprovalDecision
}
type ExecuteRequest struct {
ID string
Actor string
Executor Executor
}
func NewOrchestrator(options OrchestratorOptions) (*Orchestrator, error) {
if options.Service == nil {
return nil, ErrServiceRequired
}
if len(options.Catalog.Workflows) == 0 {
return nil, ErrCatalogRequired
}
if options.RBAC == nil {
return nil, ErrRBACRequired
}
if options.Audit == nil {
return nil, ErrAuditRequired
}
if err := ValidateCatalog(options.Catalog, CatalogValidateOptions{RequiredNames: WorkflowNames()}); err != nil {
return nil, err
}
now := options.Now
if now == nil {
now = time.Now
}
return &Orchestrator{
service: options.Service,
catalog: NormalizeCatalog(options.Catalog),
rbac: options.RBAC,
audit: options.Audit,
now: now,
}, nil
}
func (o *Orchestrator) Begin(ctx context.Context, request BeginRequest) (StartResult, error) {
if err := ctxErr(ctx); err != nil {
return StartResult{}, err
}
template, command, err := o.prepare(ctx, request.Workflow, request.Command, ActionDryRun, request.Command.Actor)
if err != nil {
return StartResult{}, err
}
if request.Previewer == nil {
return StartResult{}, ErrPreviewerRequired
}
if err := o.recordAudit(ctx, ActionDryRun+".requested", template, command, command.Actor, nil, ""); err != nil {
return StartResult{}, err
}
result, err := o.service.StartDryRun(ctx, command, request.Previewer)
if err != nil {
_ = o.recordAudit(ctx, ActionDryRun+".failed", template, command, command.Actor, nil, err.Error())
return StartResult{}, err
}
record := result.Record
record.Workflow = template.Name
record.UpdatedAt = o.now().UTC()
record, err = o.service.store.SaveWorkflow(ctx, record)
if err != nil {
return StartResult{}, err
}
result.Record = record
if err := o.recordAudit(ctx, ActionDryRun+".succeeded", template, command, command.Actor, &record, ""); err != nil {
return StartResult{}, err
}
if !request.SubmitApproval {
return result, nil
}
record, err = o.SubmitApproval(ctx, record.ID)
if err != nil {
return StartResult{}, err
}
result.Record = record
return result, nil
}
func (o *Orchestrator) SubmitApproval(ctx context.Context, id string) (Record, error) {
record, template, err := o.recordForAction(ctx, id, ActionSubmitApproval, "")
if err != nil {
return Record{}, err
}
if err := o.recordAudit(ctx, ActionSubmitApproval+".requested", template, record.Command, record.Command.Actor, &record, ""); err != nil {
return Record{}, err
}
record, err = o.service.SubmitApproval(ctx, id)
if err != nil {
_ = o.recordAudit(ctx, ActionSubmitApproval+".failed", template, record.Command, record.Command.Actor, &record, err.Error())
return Record{}, err
}
record.Workflow = template.Name
record, err = o.service.store.SaveWorkflow(ctx, record)
if err != nil {
return Record{}, err
}
if err := o.recordAudit(ctx, ActionSubmitApproval+".succeeded", template, record.Command, record.Command.Actor, &record, ""); err != nil {
return Record{}, err
}
return record, nil
}
func (o *Orchestrator) Approve(ctx context.Context, request DecisionRequest) (Record, error) {
record, template, err := o.recordForAction(ctx, request.ID, ActionApprove, request.Decision.Approver)
if err != nil {
return Record{}, err
}
if err := o.recordAudit(ctx, ActionApprove+".requested", template, record.Command, request.Decision.Approver, &record, ""); err != nil {
return Record{}, err
}
record, err = o.service.Approve(ctx, request.ID, request.Decision)
if err != nil {
_ = o.recordAudit(ctx, ActionApprove+".failed", template, record.Command, request.Decision.Approver, &record, err.Error())
return Record{}, err
}
record.Workflow = template.Name
record, err = o.service.store.SaveWorkflow(ctx, record)
if err != nil {
return Record{}, err
}
if err := o.recordAudit(ctx, ActionApprove+".succeeded", template, record.Command, request.Decision.Approver, &record, ""); err != nil {
return Record{}, err
}
return record, nil
}
func (o *Orchestrator) Reject(ctx context.Context, request DecisionRequest) (Record, error) {
record, template, err := o.recordForAction(ctx, request.ID, ActionReject, request.Decision.Approver)
if err != nil {
return Record{}, err
}
if err := o.recordAudit(ctx, ActionReject+".requested", template, record.Command, request.Decision.Approver, &record, ""); err != nil {
return Record{}, err
}
record, err = o.service.Reject(ctx, request.ID, request.Decision)
if err != nil {
_ = o.recordAudit(ctx, ActionReject+".failed", template, record.Command, request.Decision.Approver, &record, err.Error())
return Record{}, err
}
record.Workflow = template.Name
record, err = o.service.store.SaveWorkflow(ctx, record)
if err != nil {
return Record{}, err
}
if err := o.recordAudit(ctx, ActionReject+".succeeded", template, record.Command, request.Decision.Approver, &record, ""); err != nil {
return Record{}, err
}
return record, nil
}
func (o *Orchestrator) Execute(ctx context.Context, request ExecuteRequest) (Record, error) {
record, template, err := o.recordForAction(ctx, request.ID, ActionExecute, request.Actor)
if err != nil {
return Record{}, err
}
if request.Executor == nil {
return Record{}, ErrExecutorRequired
}
if err := o.recordAudit(ctx, ActionExecute+".requested", template, record.Command, request.Actor, &record, ""); err != nil {
return Record{}, err
}
record, err = o.service.Execute(ctx, request.ID, request.Executor)
if err != nil {
if latest, ok, getErr := o.service.Get(ctx, request.ID); getErr == nil && ok {
record = latest
}
_ = o.recordAudit(ctx, ActionExecute+".failed", template, record.Command, request.Actor, &record, err.Error())
return Record{}, err
}
record.Workflow = template.Name
record, err = o.service.store.SaveWorkflow(ctx, record)
if err != nil {
return Record{}, err
}
if err := o.recordAudit(ctx, ActionExecute+".succeeded", template, record.Command, request.Actor, &record, ""); err != nil {
return Record{}, err
}
return record, nil
}
func (o *Orchestrator) prepare(ctx context.Context, workflow string, command admincmd.Command, action, actor string) (WorkflowTemplate, admincmd.Command, error) {
if o == nil || o.service == nil {
return WorkflowTemplate{}, admincmd.Command{}, ErrServiceRequired
}
command = admincmd.Normalize(command)
template, err := o.templateFor(workflow, command)
if err != nil {
return WorkflowTemplate{}, admincmd.Command{}, err
}
if err := o.rbac.CheckAdminWorkflow(ctx, template, command, action, strings.TrimSpace(actor)); err != nil {
return WorkflowTemplate{}, admincmd.Command{}, fmt.Errorf("%w: %w", ErrRBACDenied, err)
}
return template, command, nil
}
func (o *Orchestrator) recordForAction(ctx context.Context, id, action, actor string) (Record, WorkflowTemplate, error) {
if o == nil || o.service == nil {
return Record{}, WorkflowTemplate{}, ErrServiceRequired
}
record, err := o.service.mustGet(ctx, id)
if err != nil {
return Record{}, WorkflowTemplate{}, err
}
if actor == "" {
actor = record.Command.Actor
}
template, command, err := o.prepare(ctx, record.Workflow, record.Command, action, actor)
if err != nil {
return Record{}, WorkflowTemplate{}, err
}
record.Command = command
if record.Workflow == "" {
record.Workflow = template.Name
}
return record, template, nil
}
func (o *Orchestrator) templateFor(workflow string, command admincmd.Command) (WorkflowTemplate, error) {
if o == nil || len(o.catalog.Workflows) == 0 {
return WorkflowTemplate{}, ErrCatalogRequired
}
names := []string{workflow}
if command.Operation != "" {
names = append(names, strings.ReplaceAll(command.Operation, ".", "_"))
}
if command.Scope != "" {
names = append(names, command.Scope)
}
for _, name := range names {
if template, ok := o.catalog.FindWorkflow(name); ok {
return template, nil
}
}
return WorkflowTemplate{}, ErrWorkflowNotFound
}
func (o *Orchestrator) recordAudit(ctx context.Context, action string, template WorkflowTemplate, command admincmd.Command, actor string, record *Record, errText string) error {
if o == nil || o.audit == nil {
return ErrAuditRequired
}
fields := map[string]string{
"workflow": template.Name,
"operation": command.Operation,
"scope": command.Scope,
"environment": command.Environment,
"shard_id": command.ShardID,
"idempotency_key": command.IdempotencyKey,
}
if record != nil {
fields["workflow_id"] = record.ID
fields["phase"] = string(record.Phase)
if record.Receipt != nil {
fields["receipt_id"] = record.Receipt.ID
fields["receipt_status"] = record.Receipt.Status
}
}
if errText != "" {
fields["error"] = errText
}
hash, err := admincmd.ParamsHash(command.Params)
if err != nil {
return err
}
if hash != "" {
fields["params_hash"] = hash
}
actor = strings.TrimSpace(actor)
if actor == "" {
actor = command.Actor
}
return o.audit.Record(ctx, audit.Event{
Time: o.now().UTC(),
Actor: actor,
Action: action,
Target: command.Target,
Fields: fields,
})
}