把 4 套 fork ERP(龙凤 / 摆件 / 妈祖 / 囍铺)合并为一份 Nexora ERP 主干代码,配合一租户一库物理隔离和模块化配置,通过 3 个阶段完成收敛。本文档是直接可执行的作战地图,每一步任务都带完成命令和验证判据。
以 龙凤 ERP(xylferp_php)为骨架起点建立 nexora/nexora-erp 主干 · 通过 3 个阶段依次接入龙凤(未启用客户,验证用)、摆件(已停,验证加租户流程)、妈祖(主战场,深度并入并抽模块) · 每个租户独立一个 DB(物理隔离) · 老的 xipugold/* repo 在完成迁移前原样保留 · 迁移完成后归档 read-only。
nexora_<slug>),物理隔离。业务表现有 aid 字段不动,值恒等于该库租户 id,退化为"防呆兜底"(错连别家库查不到)。tenant_modules 表按租户启用。关闭模块 = 该租户看不到任何相关菜单/接口/数据表。| 客户 | 主干 repo | 首次 commit | Commits | Ctrl | 近 30 天活动 | 血缘 |
|---|---|---|---|---|---|---|
| 龙凤 | xylferp_php | 2025-11-22 | 113 | 75 | 0 commits · 客户尚未启用 | 原始仓 · 唯一有真实开发历史 · 业务面最广(批发+零售) |
| 摆件 | baijian_php | 2026-02-09 | 4 | 73 | 0 commits · 客户已停 | 从 xylferp squash · 几乎没改动 |
| 妈祖 | pyerp_php | 2026-02-09 | 104 | 112 | 27 commits · 极活跃 | 从 xylferp squash 后深度定制 · 独有 58 controller(Ymetal 贵金属 / Ep* 妈祖商城 / Xipu 跨接) · 加了 pt_type 第三层租户 |
| 囍铺 | xipu_erp_php | 2026-02-09 | 17 | 75 | 10 commits · 客户正在生产 | 从 xylferp squash · 独有金价 3 方法 + 阿里云图搜 2 controller |
where('aid') 用了 1896~2285 次(pyerp 最多)· BaseController 里 define('aid', $aid) 从 token 回填 · 妈祖还有 bid(门店/业务)+ pt_type(产品线)三层app_xiao_shou_kotlin(UniApp,669 commits)已经在做多客户 App 壳 · SERVER_LIST 已定义"周锦记(Kotlin 后端)"和"妈祖(PHP 后端)"两种类型 · 但 pyerp_app 和它 md5 完全不同,是两个独立项目sprin-boot-kotlin219 有 52 个 controller,6 模块(api / member / order / print / shop / user),命名空间 rn.rw.erp⚠️ v8 → v9 修订原因
v8 §2.2 只做了 controller 文件级 md5 diff,假设了 4 家 fork 血缘相同 = 数据契约相同。执行到 Phase 2B 时暴露:妈祖 DB schema 与其他 3 家完全不同,主干 login 逻辑覆盖不了妈祖。v9 补此对比表 + Phase 2B v2 修复方案(见 §11)。
| 契约字段 | 龙凤 xylferp | 摆件 baijian | 囍铺 xipu | 妈祖 pyerp |
|---|---|---|---|---|
| 表前缀 | xipunum_ | xipunum_ | xipunum_ | is_ |
| user 表名 | xipunum_erp_user | 同 | 同 | is_user |
| 密码字段 | password | 同 | 同 | pwd |
| 密码盐 | bwqinr | xipunum → 已迁 bwqinr | xipunum → 已迁 bwqinr | erpsm |
| 密码算法 | md5(pwd+salt) | 同 | 同 | md5(md5(pwd)+salt) 双重 |
| Login 入口 | Api::login | 同 | 同 | Api::login + Apiadmin |
| 用户过滤 | state=0 + aid + bid + department | 同 | 同 | 无 state · aid/bid header |
🎯 教训条(v9 新增)
数据契约 first · 代码血缘 second。任何跨仓合并战略,方案必须先做数据契约对比表(schema / 字段名 / 密码算法 / 认证过滤逻辑),再做代码血缘 md5 diff。反过来做 = v8 到 v9 的痛苦教训。
nexora_platform 存 tenants / modules / users(跨租户账号)nexora_<slug>)· 备份/升级/隔离独立执行时遇到"这个 repo 要不要动 / 怎么动"的问题,以本表为准。Action 列 = 强制约束。
nexora/nexora-erp,原 repo 完成后归档 read-only| Repo | Action | 阶段 | 说明 |
|---|---|---|---|
| xylferp_php | MERGE | Phase 1 | 骨架起点 · clone 为 nexora-erp |
| xylferp_web | MERGE | Phase 1 | 前端骨架起点 · clone 为 nexora-erp-web |
| baijian_php | MERGE | Phase 2 | 验证「加租户」流程 |
| baijian_web | ARCHIVE | Phase 2 | 90 天 0 commits |
| pyerp_php | MERGE | Phase 2 | 主战场 · Ymetal / Ep* 抽模块 · pt_type 合入主干 |
| pyerp_web | MERGE | Phase 2 | 妈祖前端合入 nexora-erp-web |
| xipu_erp_php | LATER | Phase 3 | 73/75 controller 在龙凤里 · 视客户情况 |
| xipu_erp_web | LATER | Phase 3 | 同 xipu_erp_php |
| Repo | Action | 阶段 | 说明 |
|---|---|---|---|
| pyerp_shop_php | MERGE | Phase 3 | 唯一商城基座 · 合入 Nexora mall-sync 模块 |
| pyerp_shop_wx | MERGE | Phase 3 | 唯一小程序基座 · 未来所有小程序基于此 |
| baijian_shop_php | ARCHIVE | Phase 3 | 20G · 客户已停 |
| Repo | Action | 阶段 | 说明 |
|---|---|---|---|
| app_xiao_shou_kotlin | KEEP | Phase 3 | 669 commits 多客户 UniApp 壳 · Phase 3 演化为 Nexora 统一手机端 · Phase 1-2 保留原样 |
| pyerp_app | KEEP | Phase 3 | 妈祖手机端(和 app_xiao_shou_kotlin 目前无代码合并)· Phase 3 把功能重写进后再归档 |
| kotlinapi_uniapp | KEEP | Phase 3 | 囍铺原版手机端 · 已被 app_xiao_shou_kotlin 演化替代 · Phase 3 直接归档 |
| sprin-boot-kotlin219 | KEEP | Phase 3 | Kotlin 后端 · Phase 3 变薄:52 controller → ≤15 |
| sprin-boot-kotlin219-xylf | KEEP | Phase 3 | 2 commits 分叉 · 合入主线 |
| fastify_spring_app_api | ARCHIVE | Phase 3 | 废弃分支 |
| fastify_ts_app_api | ARCHIVE | Phase 3 | 废弃分支 |
| Repo | Action | 说明 |
|---|---|---|
| laiya_php | DROP | 莱雅商城主站 · 客户不再使用 |
| laiya_wx | DROP | 莱雅商城主小程序 |
| laiya_jxs_wx | DROP | 莱雅经销商小程序 |
| laiya_lss_wx | DROP | 莱雅零食师小程序 |
| lyshop_wx | DROP | 莱雅关联小程序 |
| hjhs | DROP | 黄金回收(CRMEB)· 客户已停 · 不同基因不合并 |
| Repo | Action | 说明 |
|---|---|---|
| pyerp_php_api | ARCHIVE | 2 commits · API 抽出实验未成气候 |
| pyerp_shop_h5_dist | OUT-OF-SCOPE | 前端构建产物 |
| wl_php / wl_web | OUT-OF-SCOPE | 未评估(疑似"物流") |
| waimao_php | OUT-OF-SCOPE | 未评估(疑似"外贸") |
| supererp / hjt / xipu_new | OUT-OF-SCOPE | 历史产物 · 未评估 |
| electron_exe / python-auto-print-desktop / password | OUT-OF-SCOPE | 桌面工具 |
| 模块 | 包含内容 |
|---|---|
| account | 账号 / 角色 / 权限 · 复用现有 aid/bid/pt_type 上下文注入 |
| feature-flag | 模块级 + 菜单级功能开关引擎 · 读中心库 tenant_modules / tenant_features |
| product | 商品 / SKU / 分类 · 通用 54 controller 里商品相关部分 |
| inventory | 库存 / 库位 / 调拨 / 盘点 / 出入库 · 含龙凤零售出库特有的 outboundBack/queryIsTuihuo |
| order | 采购 / 销售 / 审核状态机 / Bill / 财务 |
| store | 门店 / 仓库 / 收银 |
| supplier-customer | 供应商 / 客户 / 联系人 / 会员 |
| 模块 | 建于 | 包含内容 · 目标租户 |
|---|---|---|
| aliyun-image | Phase 1 | 阿里云图搜 + OSS(囍铺 2 独有 controller)· 未来给囍铺租户用 |
| ymetal | Phase 2 | 贵金属:50 个 Ymetal* controller + 40+ 张表 + 18884 行代码 · 妈祖专属 |
| myhw-shop | Phase 2 | 妈祖商城对接:Epuser / Eprole / Epservice / Epxipu* · 妈祖专属 |
| gold-price | Phase 3 | 金价管理(get/save_gold_price + get_gold_code)· 囍铺独有,合入时抽出 |
| mall-sync | Phase 3 | ERP ↔ 妈祖商城对接:products.sync / orders.push / stock.query |
| mobile-api | Phase 3 | Kotlin 变薄后的 WS + 打印 + 推送对接 |
每个模块目录 modules/<name>/ 自包含:
nexora/nexora-erp 主干,把龙凤客户作为第一个租户接入,验证「一份代码 + 一租户一库」框架成立。
nexora group 已建(gid=75, private)· 已完成ssh://git@gitlab.nexora.restry.cn:18826/nexora/nexora-erp.git
# 从本地 clone xylferp,推到 nexora-erp
git clone ssh://git@gitlab.nexora.restry.cn:18826/xipugold/xylferp_php.git nexora-erp
cd nexora-erp
git remote set-url origin ssh://git@gitlab.nexora.restry.cn:18826/nexora/nexora-erp.git
git push -u origin main --tags
curl -s $URL/api/v4/projects/nexora%2Fnexora-erp 返回 200git log --oneline | wc -l ≥ 113(保留原始 git 历史)
git clone ssh://git@gitlab.nexora.restry.cn:18826/xipugold/xylferp_web.git nexora-erp-web
cd nexora-erp-web
git remote set-url origin ssh://git@gitlab.nexora.restry.cn:18826/nexora/nexora-erp-web.git
git push -u origin main --tags
curl -s $URL/api/v4/projects/nexora%2Fnexora-erp-web 返回 200CREATE DATABASE nexora_platform DEFAULT CHARSET utf8mb4;
USE nexora_platform;
CREATE TABLE tenants (
id INT PRIMARY KEY AUTO_INCREMENT,
slug VARCHAR(32) UNIQUE NOT NULL, -- xylferp / baijian / pyerp / xipu
name VARCHAR(128) NOT NULL, -- 龙凤 / 摆件 / 妈祖 / 囍铺
db_dsn VARCHAR(255) NOT NULL, -- mysql://user:pass@host:port/nexora_xylferp
aid INT NOT NULL, -- 该租户在业务库的 aid 值(防呆参考)
bid INT DEFAULT 0,
pt_type TINYINT DEFAULT 1,
status ENUM('active','paused','archived') DEFAULT 'active',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE tenant_modules (
tenant_id INT,
module VARCHAR(64), -- aliyun-image / ymetal / myhw-shop / gold-price / mall-sync / mobile-api
enabled TINYINT DEFAULT 1,
PRIMARY KEY (tenant_id, module)
);
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(64) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
name VARCHAR(128)
);
CREATE TABLE user_tenants (
user_id INT,
tenant_id INT,
role VARCHAR(32),
PRIMARY KEY (user_id, tenant_id)
);
CREATE TABLE schema_migrations (
version VARCHAR(64) PRIMARY KEY,
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
SHOW TABLES FROM nexora_platform 应返回 5 张表
# 备份龙凤当前 DB
mysqldump -u<user> -p xylferp | gzip > xylferp_$(date +%Y%m%d).sql.gz
# 恢复到 nexora_xylferp
gunzip -c xylferp_*.sql.gz | mysql -u<user> -p nexora_xylferp
# 中心库注册龙凤租户
INSERT INTO nexora_platform.tenants (slug, name, db_dsn, aid, bid, pt_type)
VALUES ('xylferp', '龙凤', 'mysql://...@.../nexora_xylferp', 1, 0, 1);
SELECT COUNT(*) FROM nexora_platform.tenants WHERE slug='xylferp' = 1SELECT COUNT(*) FROM nexora_xylferp.is_menu 数量和原 xylferp DB 一致
原 xylferp 的 aid/bid/pt_type 从 token 回填。改造后:token 里带 tenant_slug,BaseController 先查中心库 tenants 拿到 db_dsn + aid/bid/pt_type,再切到对应租户库。
/api/user/info,返回数据来自 nexora_xylferp从 xipu_erp_php 复制 Aliyunimagesearch.php + Aliyunoss.php 两个 controller,放到 modules/aliyun-image/,包括路由/菜单/权限声明。龙凤租户默认不启用。
grep -r "Aliyunimagesearch" app/controller/ 应无输出(不在主干)SELECT * FROM tenant_modules WHERE tenant_id=1 AND module='aliyun-image' 应返 0 或 enabled=0选一台服务器(建议内网 dev 环境优先),部署 nginx + PHP + nexora-erp 代码,前端部署 nexora-erp-web。域名先用 nexora-dev.internal。
curl -sI http://nexora-dev.internal/api/health 返回 200写 4 个脚本:tenant-onboard.sh / migrate-all.sh / backup-tenants.sh / monitor.sh。放到 nexora-erp/bin/。
bin/tenant-onboard.sh test 能建出 nexora_test DB 并注册到 tenants(用完删掉)bin/backup-tenants.sh 能在 /backup/tenant/xylferp/ 下生成 .sql.gz
grep -rE "aliyun|Aliyun" app/controller/ 主干目录零命中bin/tenant-onboard.sh baijian --name "摆件" --aid 2
SELECT * FROM nexora_platform.tenants WHERE slug='baijian' 返回 1 行 · status=activeSHOW DATABASES LIKE 'nexora_baijian' 存在
mysqldump -u<user> -p baijian > baijian.sql
mysql -u<user> -p nexora_baijian < baijian.sql
# 校验 aid 一致
UPDATE nexora_baijian.is_user SET aid=2 WHERE aid != 2; -- 全部对齐到租户 id
SELECT DISTINCT aid FROM nexora_baijian.is_user 只返回 2
用摆件账号登录 Nexora 前台,把主要业务流程走一遍。如果发现主干缺失能力(baijian 独有 0 个 controller,理论应完美复用),记录并补主干。
把 pyerp_php 里 50 个 Ymetal*.php controller + 相关 model + 40+ 张 Ymetal_* 表 migrations 全部搬到 modules/ymetal/。定义 Facade。清理主干里对 Ymetal 类的直接引用。
grep -rE "Ymetal|ymetal" app/(主干目录)零命中ls modules/ymetal/controller/ | wc -l = 50ls modules/ymetal/migrations/*.sql | wc -l ≥ 40
把 pyerp_php 里 Epuser / Eprole / Epservice / Epxipu* 系列 controller 搬到 modules/myhw-shop/。
grep -rE "^use.*Ep(user|role|service|xipu)" app/ 零命中pyerp 里的 Xipu.php 里查 erp_category 表 —— 是否仍在生产使用?
modules/myhw-shop/ 或单独模块 myhw-xipu-bridgemodules/DECISIONS.md · Xipu.php 归位pyerp 独有的 pt_type(产品线维度,1/2/3/4)合入主干 BaseController 的上下文注入。所有租户获得 pt_type 能力(默认值 1)。
define('pt_type' 有 1 处pyerp 的 BaseController.php 和 common.php md5 已与其他家不同。做 3-way diff,把有价值的公共逻辑(sqlAuth / getBaseRoot / frameScope / erpPostInput 等)合入主干,妈祖专属逻辑放模块。
app/common.php 覆盖 pyerp / xylferp / xipu 三家的公共函数需求modules/myhw-shop/common.php 只放妈祖专属
bin/tenant-onboard.sh pyerp --name "妈祖" --aid 1 \
--modules ymetal,myhw-shop
# 生产 pyerp DB → nexora_pyerp(选低峰,冻结原库)
mysqldump -u<user> -p pyerp | gzip > pyerp_$(date +%Y%m%d).sql.gz
gunzip -c pyerp_*.sql.gz | mysql -u<user> -p nexora_pyerp
SELECT COUNT(*) FROM nexora_platform.tenant_modules WHERE tenant_id=(SELECT id FROM tenants WHERE slug='pyerp') = 2SELECT * FROM nexora_pyerp.Ymetal_原料库存 LIMIT 1 能查到数据(模块 migrations 生效)
SELECT DISTINCT aid FROM nexora_pyerp.is_sell 出现非 1 的值)
grep -rE "Ymetal|Ep(user|role|service)" app/ 零命中cd sprin-boot-kotlin219
grep -rl "@RestController" --include="*.kt" | while read f; do
echo "== $f =="
grep -E "@GetMapping|@PostMapping" "$f" | head -20
done > kotlin-audit.md
每个 controller 分类:KEEP(WS / 打印 / 推送 / job)/ MIGRATE(业务 CRUD,迁到 PHP)。
对每个 MIGRATE controller,在 Nexora 主干或对应模块里实现相同的接口(路径 + 参数 + 返回结构)。
修改 SERVER_LIST,把囍铺/周锦记的 backend 从 kotlin 改为 php,请求走 Nexora 主干。
MIGRATE 类 controller 从 Kotlin 后端删除 · 只保留 KEEP 类。
grep -rl "@RestController" sprin-boot-kotlin219 | wc -l ≤ 15Kotlin 后端要写数据时,通过 HTTP 调 Nexora PHP 主干 API。DB 连接改成只读账号。
xylf 分叉只有 2 commits,cherry-pick 差异合入 sprin-boot-kotlin219,归档 -xylf。
-xylf repo GitLab 标 archivedSERVER_LIST 里"妈祖"/"周锦记" 等 host 改为 Nexora 主干域名。
pyerp_app 独有页面/组件(妈祖手机端专属)逐个移植。
GitLab 上把两个 repo 标 archived。
在 modules/mall-sync/ 定义 3 条 API:
POST /mall-sync/products.sync — ERP → 商城推商品/SKU/价格POST /mall-sync/orders.push — 商城 → ERP 推订单GET /mall-sync/stock.query — 商城 → ERP 查实时库存modules/mall-sync/docs/
移除 pyerp_shop_php 里对 ERP 库表的直查 · 全部改为 curl mall-sync API。
grep -rE "Db::connect|Db::name.*is_" pyerp_shop_php/application/ 应零命中如果决定把商城代码也纳入 nexora group,创建 nexora/nexora-mall · 从 pyerp_shop_php clone · 否则 pyerp_shop_php 原样保留在 xipugold。
modules/mall-sync/DECISION.md查最近 30 天囍铺业务活跃度(登录用户数 / 单据量 / 客户投诉) + 客户业务方向。
retiredDECISION-xipu.md一租户一库的代价 = 运维成本随客户数线性增长。必须把每个环节脚本化。Phase 1 就要落地最小可用版,后续 Phase 持续增强。
位置:nexora-erp/bin/tenant-onboard.sh
tenant-onboard.sh <slug> \
--name "<中文名>" \
--aid <租户 id> \
[--modules module1,module2,...]
# 一次做 4 件事:
# 1. 建 nexora_<slug> DB
# 2. 跑核心模块 migrations
# 3. 按 --modules 跑可选模块 migrations
# 4. 中心库 tenants + tenant_modules 注册
验证:执行完 30 秒内新租户能登录,前台菜单只显示启用的模块。
位置:nexora-erp/bin/migrate-all.sh
migrate-all.sh [--dry-run]
# 遍历中心库 tenants,对每个租户 DB:
# 1. 检查 schema_migrations 表
# 2. 执行未跑过的 migration
# 3. 失败即停,输出报错(不允许"跑了一半就走人")
# 4. 只跑该租户启用模块的 migrations
验证:主干加 1 张新表,跑 migrate-all,所有租户 DB 都出现该表。
位置:nexora-erp/bin/backup-tenants.sh(cron 每晚 03:00)
backup-tenants.sh
# 遍历中心库 tenants,每个租户:
# 1. mysqldump | gzip → /backup/tenant/<slug>/<date>.sql.gz
# 2. 保留策略:daily 7 天 · weekly 4 周 · monthly 6 月
# 3. 校验:大小比昨天缩水 >20% 报警
# 4. 校验:gunzip -t 能完整解压
验证:任一租户 ls /backup/tenant/<slug>/ 有当日备份文件。
位置:nexora-erp/bin/monitor.sh(cron 每 5 分钟)
monitor.sh
# 按租户视角检查:
# - 各租户 DB 可连接
# - 各租户前台 /api/health 返回 200
# - 备份状态(最近 24h 内有成功备份)
# - 慢查询(> 1s)计数
# - 磁盘用量 > 80% 报警
# 异常发飞书告警
验证:手动停一个租户 DB,5 分钟内收到告警。
执行时任何"顺便把 X 也做了"的问题,以本节为准。不在 §04 总表 + §06-08 路线图里的东西,默认不做。
🔧 v9 新增章节
v8 Phase 2B "妈祖深度并入" 假设主干 login 能覆盖妈祖,实际执行时因 schema 分歧失败。v9 明确「以龙凤为准 · 妈祖用户逻辑弃用」的修复策略,并给出可执行的三阶段 checklist。
DATE: 2026-07-06 · DECISION-MAKER: mike · CONFIRMED: 客户方接受
「以龙凤为准,妈祖也合并到龙凤里面来,只保留一份,如果用户相关的有冲突,那就放弃妈祖的用户部分的逻辑。使用龙凤的」
基于 pyerp_app(274 vue)+ pyerp_shop_wx/h5/php(5600 文件)+ pyerp_php/eperp SPA + pyerp_php/app 后端 四层真实引用扫描
| 模块 | Controller 数 | Controller 清单(节选) |
|---|---|---|
| ymetal(贵金属) | 34 | Ymetalapi / Ymetalbusiness / Ymetalcategory / ... |
| myhw-shop(电商桥接) | 7 | Epbusiness/Epmetal/Eprole/Epservice/Epuser/Epxipunumshop/Epxipunumstoreorder |
| mall-sync(商城通信) | 3 | 已有 |
| myhw-shop 追加(v9 新增) | 2 | AppReport + AppStat(妈祖 App 首页统计,pyerp_app 依赖) |
6 单据草稿模板:Bortmp · Bretmp · Buytmp · Selltmp · Sortmp · Sretmp
3 手机端遗留:Menuappinfo · Apiadmin · Prints
3 定时任务/店铺关联:StockApi · SyncApi · Usershop
2 电商菜单遗留:Epmenu · Epusershop
弃用理由:上述 12 个 controller 在妈祖 SPA / App / 商城 / 后端均零引用,是历史遗留死代码。
is_user 表(archive · 不 DROP)· 保留业务单据外键完整性pyerp_user_id_migration(old_is_user_id, new_xipunum_erp_user_id)bwqinr · 单重 md5理由:降低单次改造风险 · 商城/App 通过 mall-sync 桥接 · 不影响妈祖客户
| 应用 | 处置 |
|---|---|
| pyerp_shop_php / _h5 / _wx(商城客户端) | 不动 · 通过 mall-sync 与 Nexora ERP 通信 |
| pyerp_app(妈祖员工手机 App) | 暂不动 · 保留独立打包 · 只需前端换 X-Tenant-Slug + 走 Nexora 主干 login |
| Step | 时长 | 产出 | 验证 |
|---|---|---|---|
| Step 1 Diff 52 同名 controller |
2-4h | 妈祖独有改造清单 · 判断迁到 ymetal 或直接丢 | 生成 diff 报告 md 文档 |
| Step 2 主干代码改造 |
半天 | ① AppReport/AppStat 迁 myhw-shop 模块 ② 12 孤儿 controller 从主干删除 ③ 妈祖贵金属字段改造迁 ymetal 模块 |
php lint + syntax check |
| Step 3 妈祖租户数据迁移 |
半天 | ① archive is_user ② 建 27 新账号 + mapping 表 ③ 启用 ymetal + myhw-shop + mall-sync ④ 端到端 UI 验证 |
妈祖 admin 用新密码登进 Nexora dashboard · vision 判读看到「龙凤菜单 + 贵金属模块」 |
https://nexora-erp.nexora.restry.cn/?tenant=pyerp 用新密码登进 dashboard✅ v10 新增章节
v8/v9 隐含假设"平台超管 = Nexora 团队 SSH + SQL 运维",没有 Web UI。Mike 反馈"这个很重要,东西从哪来?任一租户 admin 应该都能升级成平台超管"。v10 补此章节 · 复用现有 ERP 登录页 · 通过中心库 role 判定注入平台管理菜单。
platform_admin 角色 → 侧栏多出"平台管理"顶级菜单nexora_platform.user_tenants (中心库) ├── user_id → nexora_platform.users.id │ (跨租户的全局账号池 · 目前只放平台超管) ├── tenant_id → nexora_platform.tenants.id │ (超管所属"总部租户" · 默认龙凤) └── role: 'platform_admin' | 'admin' | 'user'
判定逻辑:
user_tenants 是否有该用户的 platform_admin 记录| 顶级菜单 | 子菜单 | 功能 |
|---|---|---|
| 平台管理 | 租户列表 | 4 家 + 未来新建的租户清单 · 状态 · 启用模块 |
| 新建租户 | 表单页 · 相当于 web 版 bin/tenant-onboard.py |
|
| 模块启停 | 每租户一行 · 6 个模块开关 | |
| 平台超管 | 谁是平台超管 · 授权/收回 | |
| 系统监控 | 4 家在线状态 · DB 连接 · migration 版本 |
app/controller/PlatformAdmin.php · 5 个 action(tenantList / tenantCreate / moduleToggle / adminList / adminGrant)app/common.php · getBaseRoot() 里检查 platform_admin · 注入平台管理菜单app/nexora/PlatformAuth.php · 新工具类 · isPlatformAdmin(userId, tenantSlug)route/app.php · 平台管理路由前缀 /platform-admin/*src/views/platform/TenantList.vue · 租户列表页src/views/platform/TenantCreate.vue · 新建租户表单src/views/platform/ModuleMatrix.vue · 模块启停矩阵src/views/platform/AdminGrant.vue · 平台超管授权页src/router/index.js · 加 /platform/* 路由nexora_platform.users 新增一行:username=platform-admin-xylferp · 关联龙凤 adminnexora_platform.user_tenants 新增一行:role=platform_adminnexora_platform.platform_admin_log 新表 · 审计日志PlatformAuth::assert() · 通不过 return 403platform_admin_log 表(who + when + what + before/after)| Step | 时长 | 产出 | 验证 |
|---|---|---|---|
| Step 1 后端授权层 |
2h | ① PlatformAuth 工具类 ② 首次授权 SQL ③ getBaseRoot 注入菜单 ④ 4 家复测 |
curl service/store 拿 menu · vision 判读:龙凤有平台管理 · 其他 3 家无 |
| Step 2 后端 CRUD |
3h | ① PlatformAdmin.php controller ② route/app.php ③ audit log 表 ④ curl 单测 5 个接口 |
curl POST 每个接口 · 200 · 摆件 admin 调返 403 |
| Step 3 前端页面 |
4h | ① TenantList/Create/ModuleMatrix/AdminGrant ② 路由 ③ 菜单点击进入 |
headless chrome + vision 逐页判读 |
总计约 9h · 一天工作量。
🎯 "运维工具也是产品"
任何多租户系统的方案里,平台超管入口和业务功能一样重要,不能默认交给 SSH + SQL。v8/v9 因为方案作者(Mike & assistant)默认"我们自己是超管"跳过了这块 · v10 补齐。
🚨 v10 review 发现根本性错误
Mike 追问 "妈祖和贵金属是同一份源码 · 但也是不同的租户 · 这次合并考虑到了吗?" — v9 §11 假设 pyerp 是"妈祖单一客户" · 建了 nexora_pyerp 一个租户库把 26 员工全塞进去。真相:pyerp_php 本身就是一个 SaaS 平台 · 用 aid 字段做租户区分 · 里面挂着 10 个 aid · 至少 6 家真实客户。v9 相当于把 10 家客户的数据糊成一个租户 · 完全错。
扫 nexora_pyerp.is_user(v9 dump 自老 pyerp_php 生产)· 按 aid 聚合 · 关联 is_goods / is_ymerp_* 业务表 · 得真实图景:
| aid | 用户 | 商品 | 贵金属流水 | 最近登录 | 判定 |
|---|---|---|---|---|---|
| 37 | 府见福礼(7人) | 472 | 0 | 2026-04-29 | ✅ 活跃 · 主客户 |
| 52 | 清屿文化(2人) | 351 | 0 | 2026-01-29 | ✅ 活跃 |
| 21 | 门店组(5人) | 0 | 18 | 2025-11-30 | ✅ 贵金属主体 |
| 50 | 凤天工坊(3人) | 11 | 0 | 2026-01-22 | ✅ 少量活跃 |
| 39 | 清屿销售(2人) | 5 | 0 | 2026-03-28 | ✅ 少量 |
| 19 | 黑暗集团(3人) | 3 | 0 | 2025-11-30 | ⚠️ 冷 |
| 34 | 0 | 0 | NULL | ❌ 空壳 | |
| 41 | 0 | 0 | NULL | ❌ 空壳! | |
| 1/18 | 0 | 0 | NULL | ❌ 空壳 |
🔑 关键澄清
按 Mike 决策 "方便已经确认了 · 只要有活跃的租户":
| 新 tenant slug | 老 aid | 品牌名 | 迁移优先级 | ymetal 模块 |
|---|---|---|---|---|
fjfl | 37 | 府见福礼 / 兴化福礼 | P0 · 数据最多 | × |
qywh | 52 | 清屿文化 | P0 | × |
pymetal | 21 | pyerp 门店组 · 贵金属主体 | P0 · 唯一贵金属客户 | ✅ 启用 |
ftgf | 50 | 凤天工坊 | P1 | × |
qyxs | 39 | 清屿销售 | P1 | × |
heian | 19 | 黑暗集团 | P2(冷) | × |
空壳不建: aid=1/18/34/41 · 数据全 0 · 无用户实际登录 · 直接丢弃(老库继续保留归档)。
v9 建的 nexora_pyerp 一个库跑 10 aid 混合 · 不能直接用。选定选项 C(archive + 6 家全新建):
nexora_pyerp_archive_20260707 · 供追溯按 P0/P1/P2 顺序 · 每家 6 步:
mysqldump nexora_pyerp is_* --where "aid=<N>" → nexora_<slug>.sql💡 教训 · "SaaS 的 SaaS 陷阱"
合并"祖传 SaaS 系统"时必须先扫 aid / tenant_id 类字段的分布 · 才能判断它是单租户还是 SaaS。v9 §11 假设 pyerp = 一家客户 · 是没做数据契约扫描直接冲上去合并的典型错误。方案层的假设必须先用 SQL 验证 · 再动代码。
📸 2026-07-07 · Phase 2C 完成
v11 §13 拆分执行完毕:老 pyerp 归档 · 从 pyerp SaaS 拆出 6 家独立租户 · 9 家 active + 1 家 archived · 全部端到端 login + dashboard vision 判读通过。
| id | slug | 品牌名 sys.name | 状态 | 来源 |
|---|---|---|---|---|
| 1 | xylferp | 龍鳳珠寶 | ✅ active | Phase 1 · 龙凤主干 |
| 5 | baijian | 摆件工艺品 | ✅ active | Phase 2A · baijian_php |
| 6 | | 妈祖 (v9 混合库·已归档·勿用) | 🗄️ archived | v9 错误合并 · 10 aid 混合 |
| 7 | xipu | 囍铺黄金 | ✅ active | Phase 3D 提前 · xipu_erp_php |
| 9 | fjfl | 府见福礼 | ✅ active | Phase 2C · pyerp aid=37 拆 |
| 10 | qywh | 清屿文化 | ✅ active | Phase 2C · pyerp aid=52 拆 |
| 11 | pymetal | 妈祖贵金属门店 | ✅ active | Phase 2C · pyerp aid=21 拆 · 独启 ymetal |
| 12 | ftgf | 凤天工坊 | ✅ active | Phase 2C · pyerp aid=50 拆 |
| 13 | qyxs | 清屿销售 | ✅ active | Phase 2C · pyerp aid=39 拆 |
| 14 | heian | 黑暗集团 | ✅ active | Phase 2C · pyerp aid=19 拆 |
侧栏基础菜单(所有 active 租户共 8 项): 首页 / 账户管理 / 成品管理 / 销售管理 / 会员管理 / 资产管理 / 报表管理 / 系统配置。
以下是每家的 特殊菜单差异(启用模块后由 ModuleRegistry 注入):
| slug | aliyun-image (图搜+OSS) |
gold-price (金价) |
mall-sync (商城对接) |
mobile-api (手机端) |
myhw-shop (妈祖商城) |
ymetal (贵金属) |
platform-admin (超管) |
|---|---|---|---|---|---|---|---|
xylferp | × | × | × | × | × | × | 🔑 |
baijian | × | × | × | × | × | × | × |
xipu | ✅ | ✅ | × | × | × | × | × |
fjfl | × | × | × | × | × | × | × |
qywh | × | × | × | × | × | × | × |
pymetal | × | × | × | × | × | ✅ | × |
ftgf | × | × | × | × | × | × | × |
qyxs | × | × | × | × | × | × | × |
heian | × | × | × | × | × | × | × |
侧栏效果: 龙凤 9 项(基础 8 + 平台管理) · 摆件 8 项(纯基础) · 囍铺 11 项(基础 8 + 图像搜索/OSS/金价 3) · pymetal 9 项(基础 8 + 贵金属) · fjfl/qywh/ftgf/qyxs/heian 8 项(纯基础)
| slug | DB 大小 | 表数 | 员工 | 菜单行 | 库存件 | 会员 | 备注 |
|---|---|---|---|---|---|---|---|
xylferp | 4.8 MB | 119 | 18 | 85 | 0 | 0 | 主干母版 |
baijian | 6.8 MB | 110 | 4 | 204 | 130 | 1 | 从 baijian_php 迁 |
xipu | 615.3 MB | 116 | 77 | 205 | 136,416 | 1,170 | 最大 · 客户主力 |
pyerp | 50.2 MB | 244 | 26 | 85 | - | - | 🗄️ 归档 · 待迁 |
fjfl | 4.1 MB | 103 | 1 | 85 | 0 | 0 | 空壳 · 待迁 472 商品(aid=37) |
qywh | 4.1 MB | 103 | 1 | 85 | 0 | 0 | 空壳 · 待迁 351 商品(aid=52) |
pymetal | 4.1 MB | 103 | 1 | 85 | 0 | 0 | 空壳 · 待迁 18 贵金属流水(aid=21) |
ftgf | 4.1 MB | 103 | 1 | 85 | 0 | 0 | 空壳 · 待迁 11 商品(aid=50) |
qyxs | 4.1 MB | 103 | 1 | 85 | 0 | 0 | 空壳 · 待迁 5 商品(aid=39) |
heian | 4.1 MB | 103 | 1 | 85 | 0 | 0 | 空壳 · 待迁 3 商品(aid=19) |
登录入口 https://nexora-erp.nexora.restry.cn/?tenant=<slug> · 全部 admin/123456
| slug | 用户名 | 密码 | 密码盐 | 特权 |
|---|---|---|---|---|
xylferp | admin | 123456 | bwqinr (单 md5) | 🔑 platform_admin |
baijian | admin | 123456 | bwqinr | - |
xipu | admin | 123456 | bwqinr | - |
| - | - | - | 🗄️ 归档 · 无法登录 |
fjfl | admin | 123456 | bwqinr | - |
qywh | admin | 123456 | bwqinr | - |
pymetal | admin | 123456 | bwqinr | - |
ftgf | admin | 123456 | bwqinr | - |
qyxs | admin | 123456 | bwqinr | - |
heian | admin | 123456 | bwqinr | - |
/PlatformAdmin/tenantList → HTTP 403 · PLATFORM_ADMIN_REQUIREDTENANT_NOT_RESOLVED🎯 阶段小结
v9 假设 pyerp 是单租户 · v10 review 发现是 SaaS · v11 §13 拆分方案 · Phase 2C 执行完成 · 10 家中心库 + 9 家 active · 权限/模块/菜单差异化 vision 判读全过。剩下的是业务数据迁移(P0)和老容器下线(P4)·需客户切换窗口。
9 家全部使用子域名 · 每家独立 host · localStorage 天然按域名隔离(多 tab 不串号)。老 URL 保留兼容。
| 类型 | URL |
|---|---|
| 租户入口 | https://<slug>.erp.nexora.restry.cn |
| 平台管理 | https://xylferp.erp.nexora.restry.cn/platform-admin.html |
| 兼容老 URL | https://nexora-erp.nexora.restry.cn/?tenant=<slug> |
平台超管在「租户列表」每行「编辑」按钮打开 modal · 可改以下 6 字段。
| 字段 | 存储位置 | 显示在 |
|---|---|---|
| 租户名 | nexora_platform.tenants.name | 平台管理租户列表 |
| 品牌名 | <租户库>.xipunum_sys.name | 前端 header |
| 公司全称 | <租户库>.xipunum_sys.company | 单据/报表落款 |
| ICP 备案号 | <租户库>.xipunum_sys.icp | 页脚 |
| 公告 | <租户库>.xipunum_sys.notice | 前端首页顶部 |
| 状态 | nexora_platform.tenants.status | active / archived |
不可改:slug / db_dsn / table_prefix / aid(锚点字段 · 改了要迁库)。
硬约束:xylferp 不能归档(平台超管账号所在租户)。
| 组件 | 文件 | 配置/接口 |
|---|---|---|
| frpc | /etc/frp/frpc.ini | custom_domains 含 *.erp.nexora.restry.cn |
| Caddy | /home/claw/lobster-platform/caddy/Caddyfile | 9 家 site block · (tenant_site) snippet import |
| Tenant.php | app/nexora/Tenant.php | detectSlug() 从 host parts[0] 取 slug |
| 后端接口 | app/controller/PlatformAdmin.php | POST /PlatformAdmin/tenantUpdateGET /PlatformAdmin/tenantDetail?slug= |
| 前端 UI | dist/platform-admin.html | 租户列表「编辑」按钮 → modal 表单 |