服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
122 lines
4.5 KiB
Go
122 lines
4.5 KiB
Go
// Package architecture_test 用 AST 静态断言 AGENTS.md 的 Source Boundaries。
|
||
// 测试只解析本模块源码 import,离线可运行、结果确定:
|
||
// 1. internal/modules/dnf/** 不得 import internal/services/**(领域层不依赖传输/装配层);
|
||
// 2. internal/services/dnfbridge 不得 import internal/platform 的数据库/仓储实现层
|
||
// (只允许经 internal/modules/dnf/repository 的 dnfrepo 接口路径);
|
||
// 3. internal/app/dnf90 之外不得 import internal/services/logic/dnf(组合根唯一),
|
||
// internal/services/logic/dnf/** 自身子包不受限,既有例外必须登记在 allowedExceptions。
|
||
package architecture_test
|
||
|
||
import (
|
||
"go/parser"
|
||
"go/token"
|
||
"os"
|
||
"path/filepath"
|
||
"runtime"
|
||
"strconv"
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
const modulePath = "longheng.io/server/"
|
||
|
||
// allowedExceptions 登记现状盘点发现的既有违规,格式为 文件相对路径 -> import 前缀 -> 保留原因。
|
||
// 断言规则:清单之外不得新增违规;清单内条目一旦不再被引用(stale)同样失败,
|
||
// 保证允许清单始终最小且与代码同步。新增例外必须在注释中说明原因。
|
||
var allowedExceptions = map[string]map[string]string{
|
||
"cmd/server/doctor/main.go": {
|
||
// doctor 是部署预检工具,需复用 logic/dnf 的配置解析与仓储装配做启动前校验(2026-07 盘点时的既有例外)。
|
||
modulePath + "internal/services/logic/dnf": "doctor preflight reuses logic/dnf config loading and repository assembly",
|
||
},
|
||
}
|
||
|
||
func TestSourceBoundaries(t *testing.T) {
|
||
root := moduleRoot(t)
|
||
matched := map[string]map[string]bool{}
|
||
err := filepath.WalkDir(root, func(path string, entry os.DirEntry, err error) error {
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if entry.IsDir() {
|
||
if entry.Name() == ".git" {
|
||
return filepath.SkipDir
|
||
}
|
||
return nil
|
||
}
|
||
if filepath.Ext(path) != ".go" || strings.HasSuffix(path, "_test.go") {
|
||
return nil
|
||
}
|
||
relative, err := filepath.Rel(root, path)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
relative = filepath.ToSlash(relative)
|
||
file, err := parser.ParseFile(token.NewFileSet(), path, nil, parser.ImportsOnly)
|
||
if err != nil {
|
||
t.Errorf("parse %s: %v", relative, err)
|
||
return nil
|
||
}
|
||
for _, imported := range file.Imports {
|
||
pathValue, err := strconv.Unquote(imported.Path.Value)
|
||
if err != nil {
|
||
t.Errorf("decode import in %s: %v", relative, err)
|
||
continue
|
||
}
|
||
reason := forbiddenImport(relative, pathValue)
|
||
if reason == "" {
|
||
continue
|
||
}
|
||
if _, ok := allowedExceptions[relative][pathValue]; ok {
|
||
if matched[relative] == nil {
|
||
matched[relative] = map[string]bool{}
|
||
}
|
||
matched[relative][pathValue] = true
|
||
continue
|
||
}
|
||
t.Errorf("%s imports %q: %s", relative, pathValue, reason)
|
||
}
|
||
return nil
|
||
})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
for file, imports := range allowedExceptions {
|
||
for prefix, why := range imports {
|
||
if !matched[file][prefix] {
|
||
t.Errorf("stale allowlist entry %s -> %q (%s): remove it or restore the import", file, prefix, why)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
func forbiddenImport(relative, imported string) string {
|
||
// 规则 1:DNF 领域层不依赖传输/装配层。
|
||
if strings.HasPrefix(relative, "internal/modules/dnf/") {
|
||
if strings.HasPrefix(imported, modulePath+"internal/services/") {
|
||
return "internal/modules/dnf owns domain rules and must not import internal/services"
|
||
}
|
||
}
|
||
// 规则 2:bridge 只允许经 dnfrepo 接口访问持久化,不得直连平台数据库/仓储实现层。
|
||
if strings.HasPrefix(relative, "internal/services/dnfbridge/") {
|
||
if imported == modulePath+"internal/platform/db" || strings.HasPrefix(imported, modulePath+"internal/platform/db/") {
|
||
return "internal/services/dnfbridge must reach persistence through internal/modules/dnf/repository interfaces, not platform db implementations"
|
||
}
|
||
}
|
||
// 规则 3:组合根唯一。logic/dnf 树内部(含 game 子包)不受限,其余例外须登记 allowedExceptions。
|
||
if !strings.HasPrefix(relative, "internal/app/dnf90/") && !strings.HasPrefix(relative, "internal/services/logic/dnf/") {
|
||
if imported == modulePath+"internal/services/logic/dnf" || strings.HasPrefix(imported, modulePath+"internal/services/logic/dnf/") {
|
||
return "only the internal/app/dnf90 composition root may import internal/services/logic/dnf"
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func moduleRoot(t *testing.T) string {
|
||
t.Helper()
|
||
_, file, _, ok := runtime.Caller(0)
|
||
if !ok {
|
||
t.Fatal("resolve architecture test path")
|
||
}
|
||
return filepath.Clean(filepath.Join(filepath.Dir(file), "..", ".."))
|
||
}
|