Project Map · Architecture
每张图都能逐步播放。先看全景,再点「下一步」跟着数据走一遍。
gameboxclient 工作区快照(未提交状态,git HEAD
仍是旧基线)。所有
file:line
引用当时逐条核对过;一旦有人提交或改动,行号会漂移,
文件名和函数名仍然有效。
GameBox 是一个装在电脑上的桌面程序,策划和美术每天用它改游戏里的 NPC、物品、掉落、任务、地图,改完点发布,资源就上到测试服或正式服。
它不是网页,但它里面装着一个网页。Electron 干的就是这件事: 给你一个浏览器内核负责画界面,同时给你一个 Node.js 进程负责干浏览器干不了的活 —— 读写本地文件、调用 SVN 命令行、fork 子进程解压几个 G 的资源包。
所以看这个项目,第一件要分清的事永远是:我现在看的这段代码,跑在哪个进程里? 这决定了它能用什么 API、出错时去哪看日志、以及为什么有些代码在浏览器里根本没法调试。
这张图最该记住的是那个虚线空框:本项目没有 preload。 标准 Electron 应用靠 preload 脚本在两个进程之间开一道窄门, 只把明确列出的方法暴露给页面。这里没有,取而代之的是把整个 Node 环境直接交给了渲染层。
electron/preload/index.ts
这个文件是存在的,里面还老老实实写着
contextBridge.exposeInMainWorld。但它从来没被构建过 ——
vite.config.ts:58-72 把整个 preload 构建块注释掉了,
electron/main/config/options.ts 的
webPreferences 里也没有 preload 这个键。
结果是:想在渲染层用 Node,唯一的路就是直接 import。
所以你会在 .vue 文件里看到
import { ipcRenderer } from 'electron'、
import Fs from 'fs'、const SVN = require('node-svn-ultimate')
这些在正常 Web
项目里根本不该出现的东西。它们不是写错了,是这个项目的既定架构。
⚠️ 隔离
渲染层全开 Node 权限(options.ts:6-8)。
这是架构级决定,几百个文件都建立在它之上,你接手第一周绝不该动它。
但要清楚代价:渲染层加载的任何第三方内容都拥有读写整台机器的能力,
webSecurity: false 还关掉了同源策略。
规范做法是保持 contextIsolation: true, 用
preload + contextBridge 只暴露必要方法。见
Electron · Context Isolation
与
Electron · Security。
你现在该做的:新写的代码不要继续扩散
@electron/remote。要跟主进程说话就老实加一个 IPC 通道,
至少让边界是显式的、可数的。
从双击图标到看见登录框,中间有八步。搞清楚这八步, 你才知道「界面白屏」这类问题该往哪一步查。
排障用法:白屏且没报错,先看是卡在 3–4 步(窗口起了但页面没加载) 还是 5–6 步(页面加载了但 Vue 没装配)。前者去主进程日志找, 后者按 F12 看渲染层控制台。
⚠️ 隔离
generateRoutes() 名不副实。
src/store/modules/permission.ts:37-56 里它只做了
[...DynamicRouterMap] 加一条 404 兜底,然后全部
router.addRoute()。函数名和文件名都写着「权限」,
但没有任何一行按用户角色过滤。
这意味着:菜单里藏起来的页面(meta.hidden)
并不是「无权访问」,只是「不显示入口」。真正的授权只可能在服务端。
排障时如果有人说「他不该能进那个页面」——
前端确实没做角色过滤,但守卫会查 token
(src/permission.ts:129-137),
未登录仍会被重定向到 /login?redirect=…。 所以准确说法是:已登录用户之间没有权限区分。
这是整个项目最容易搞错的一条链路 —— 我自己第一版就搞错了,而且错得很典型。
直觉是:.env 放开发配置,真实凭据靠登录下发。
但这个项目不是那样。.env 里已经有一整套能跑的完整配置:42
个键,含 GM 地址、pcode、渠道、SVN 仓库地址、Jenkins 地址凭据, 以及 11 个
Jenkins job 名。
登录做的是覆盖,不是填充。
initEnv() 改写 14 个键 —— 而这 14 个在 .env
里全都已经有条目。
所以真正决定「能不能用」的不是「登录没登录」,而是那个键的兜底值是不是空的。
排障用法:「点了发布没反应」「SVN
提交报没权限」这类问题, 第一步永远是敲
JSON.parse(localStorage.getItem('GameENV'))。 但「有值」不等于「对」
—— 还要看它是登录下发的, 还是 .env 的兜底值。完整分类见环境变量速查卡。
❌ 别学
凭据既进 .env(且被 git 跟踪),又明文落
localStorage。
.env 里 VITE_JENKINS_TOKEN 有真值,
而这个文件在版本控制里;同时
GameEvn.ts:44-49 的深度 watch 会把整个
ENV (含登录下发的 svn_password)写进
localStorage, 没有加密、没有过期。而渲染层又是
webSecurity: false 的全 Node 环境。
规范做法是凭据只留在主进程,渲染层通过 IPC 请求主进程代为执行(「帮我提交这些文件」), 而不是把密码交给渲染层自己拼命令行;主进程侧用 Electron safeStorage 走系统钥匙串加密。构建期配置则不应把生产凭据写进受版本控制的文件。
你现在该做的:先别改,但把它记进风险清单 ——
.env 里那个 token 是你跟安全或运维对话时的第一个议题。
策划打开「NPC」页,搜一个 NPC,点进去改「刷新时间」,点保存。 这个动作在代码里要穿过两条并行的链路,最后在表单组件里汇合。
数据链负责「这个 NPC 现在的值是多少」,走网络。
描述链负责「这张表有哪些字段、每个字段该用什么控件、选项有哪些」, 走本地的
schema 文件。
分清这两条是理解本项目的关键:绝大多数「加个字段」「改个下拉选项」的需求,只动描述链,碰都不用碰页面组件。
最实用的一条:策划说「NPC 表里给我加个字段」,
你要改的是 src/store/config/Npc.ts(加字段定义)和
src/views/GameEditor/Npc/view.ts(决定它显示在哪个标签页), 不是
NpcEdit.vue。页面组件是通用的,它只负责按 schema 渲染。
这个后端有两种风格,别只记住一种
我第一版写的是「所有业务共用一个 /1000y/engine/cmd」——
这是错的,而且方向反了。真实分布(统计
src/api/ 下所有 url: 字面量):
| 风格 | 端点 | 处数 | 怎么看出它在干什么 |
|---|---|---|---|
| 具名端点(多数派) |
/1000y/sj/packServer ·
/1000y/sj/serverKickAll ·
/1000y/sj/gameNoticeAdd …
|
61 | 看 URL 就够了 |
cmd 分发 |
/1000y/engine/cmd |
4 |
必须展开 Payload 看 cmd, 全项目共 15 个不同的 cmd 值
|
/1000y/sj/cmd |
5 | ||
| 非业务 |
/user/login · /user/info ·
/document/* · /journal/create …
|
6 | 看 URL |
排障顺序:先看 Request URL。只有落在
.../cmd 的那 9 处才需要展开 Payload 看
cmd 字段。 读操作(查 NPC、查表)走
engine/cmd,
写操作(excel_modify)走 sj/cmd。
❌ 值班必知:「保存失败」不等于「没写进去」
excel_modify()(src/api/gm/index.ts:259-296)
一次调用会发两个请求:
const originalRequest = request.post({ url: '/1000y/sj/cmd', data: { …, cmd: 'excel_modify' } })
// 注意是 .finally —— 不管第一个成没成功,都会发第二个
const additionalRequest = originalRequest.finally(() => {
if (data.cmd === 'excel_modify') {
return request.post({ url: '/1000y/sj/cmd', data: { …, cmd: 'excel_save_to_oss' } })
}
})
return Promise.all([originalRequest, additionalRequest])
Promise.all 只要有一个 reject,整体就 reject。 所以界面弹「保存失败」时,实际有三种可能:
| ① 写表 | ② OSS 镜像 | 界面提示 | 真实状态 |
|---|---|---|---|
| ✅ 成功 | ✅ 成功 | 成功 | 都好了 |
| ✅ 成功 | ❌ 失败 | 失败 | 表已经改了,只是镜像没同步 —— 重试会二次写入 |
| ❌ 失败 | ❌ 失败 | 失败 | 确实没改 |
所以看到「保存失败」,正确动作不是重试,是先回读。 刷新页面或重新查一次那条数据,确认它到底改没改。
在 Network 面板怎么看:搜 cmd 会看到
成对出现的两个 POST /1000y/sj/cmd, 展开
Payload 分别是 excel_modify 和
excel_save_to_oss。看清是哪一个红了。
规范做法是把「主写入」和「镜像同步」的结果分开报告, 或者让镜像失败只降级为警告,而不是让整个保存显示为失败。
发布页在 /expert/release(专家模式 → 发布,组件是
src/views/Option/index.vue)。页面上是一排卡片,
但卡片背后其实只有两种任务类型,走完全不同的代码路径。
互斥规则只有一条,而且是全局的:只要 Jenkins
上有任何一个 job 在跑,所有发布卡片都点不动 —— 包括纯本地的 SVN
提交。
闸门在分叉之前(Option/index.vue:89-111),
所以「我只是想提交几个文件,为什么提示有打包任务」是符合预期的,不是
bug。
⚠️ 隔离
轮询用的是裸 setInterval,没有超时上限。
src/api/login/index.ts:105-107 起一个定时器反复查构建状态,
只在拿到明确结束态时才 clearInterval。 如果 Jenkins
中途不可达、返回结构变了、或者 job 被人在 Jenkins 侧删了,
这个定时器会一直转下去,界面上的任务永远停在进行中。
规范做法是给轮询加最大次数或截止时间,超时后主动置为「状态未知」 并给用户一个刷新入口,而不是无限等。
排障提示:用户报「发布卡住不动了」,先问 Jenkins
页面上那个 job 到底是什么状态。八成 Jenkins
那边早就结束了,是这边的轮询没收到。 重新登录会重置
taskStore,是最快的自救手段。
站点版本 319ca82 · 2026-08-13
内容基线 gameboxclient @ 2026-08-12