把 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 路线图里的东西,默认不做。
v10 决策:妈祖 pyerp 客户端账号迁到龙凤主干(schema 冲突时以龙凤为准)· 商城代码不动 · ERP 换 Nexora 前端。 拆分方案由 v11 §13 迭代 · 已由 §14 落地 (6 家独立租户)。
v10 决策:平台超管入口复用龙凤 admin(platform_admin 标记)· 不建独立后台。
已由 §14/§15 落地 · PlatformAdmin 8 action controller · /platform-admin.html UI 完备。
v9 假设 pyerp 单租户 · 建单库合并 → v10 review 发现 is_goods.aid 有 10 个租户 → v11 §13 决策拆 6 家独立租户。
拆分方案已由 §14 落地(Phase 2C 执行完成)· 详见 §14 现状快照。
📸 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 表单 |
从 v9 假设错、v10 review、v11 §13 拆分、v11.2 URL 改造、v11.4 后台管理 一路走下来 · 这些经验可以直接用到别的项目(其他 ERP 合并 / SaaS 拆分 / 域名迁移)。
| 教训 | 具体 |
|---|---|
| 数据契约先于代码血缘 | v9 假设 pyerp 是单租户 · 直到 v10 review 才发现 is_goods 表按 aid 分 10 家客户。合并前必 SQL 里 GROUP BY tenant/aid 看多租户信号 · 光看 controller md5 是不够的。 |
| 一份代码 + 模块开关 | Nexora 主干 = 核心 + tenant_modules 表控制启停 · 4 家老客户 + 6 家新拆 = 一份代码(仅囍铺 3 模块 · pymetal 1 模块 · 龙凤 1 特权)。避免 fork 出 4 份代码维护地狱。 |
| 锚点字段禁改 | slug / db_dsn / table_prefix / aid 是数据锚点 · 改了要迁库。UI 层 disabled + 后端 tenantUpdate 硬拒 + 不能归档 xylferp(超管所在)。 |
| SaaS 用子域名 | v11.1 前用 ?tenant=xxx 参数 · 一浏览器只能登一家。v11.2 改子域名 · localStorage 天然按域名隔离 · 多 tab 无痕都不用。行业标准(Notion / Slack / Figma / Airtable 一致)。 |
| 禁写死名单 | Caddy 首版硬编码 9 家 slug · 建第 10 家立即崩。改成 *.erp 通配 + on_demand_tls + ask endpoint (校验中心库 slug 有效) · 建新租户 0 修改 Caddy。 |
| 教训 | 具体 |
|---|---|
| API 200 ≠ UI 可用 | 端到端验证必走 headless chrome + vision 判读。E2E 测试建租户时抓到了 Caddy 硬编码 bug · 光看 tenantCreate API 返 success 是发现不了的。 |
| 逐个测 · 不推断 | 9 家租户全部单独 login + dashboard 截图 · 别只测 1 家推 N 家。差异化模块(gold-price / ymetal / platform-admin)只有真登进去才能验。 |
| 老 URL 保留兼容 | v11.2 子域名改造保留了老 nexora-erp/?tenant= · 无破坏性 · 老书签/前端 hardcode 不会立即失效。 |
| frps SNI 白名单 | 235 通过 frpc 转发公网 · custom_domains 是白名单不是通配。调 TLS 问题先查全链路白名单(frps → Caddy → 后端) · 别死磕 Caddy。 |
| 建库脚本落 controller · 不 shell out | 首版 tenantCreate 用 shell_exec 跑 python · 不好调试。重构成 PHP PDO 内联 · 事务/错误处理全在同一个函数 · 好维护。 |
SELECT COUNT(DISTINCT tenant_key) FROM main_tables 判断是不是 SaaS<slug>.app.example.com + 通配 cert + on_demand_tls + ask endpointplatform_admin 标记 · 不建独立后台(避免账号双系统)tenant_modules(tenant_id, module, enabled) · ModuleRegistry 注入菜单/路由/守卫custom_domains 提前加 *.<subzone>.example.com · 不然每加一家改一次| 事项 | 阻塞点 |
|---|---|
| 老 pyerp_php 6 家客户业务数据迁到新库 | fjfl 472 商品 · qywh 351 · pymetal 18 贵金属流水 · ftgf 11 · qyxs 5 · heian 3 · 需客户切换窗口 |
| Phase 1 数据一致性验收 | 对齐龙凤 vs Nexora 5 类单据 · 需真业务数据 · 待客户切换 |
| 囍铺 615MB 数据回归测试 | 已迁完 · 但没有真跑一遍囍铺员工的日常单据流 |
| Kotlin 手机端接口归口 | 当前 xylf-kotlin/pyerp-kotlin 分别跑 · 归口到 nexora-erp 主干 · 变薄 |
| 商城 mall-sync 模块化 | 妈祖商城 / crmeb-mazu 对接 · 目前散在 pyerp_php · 抽成 nexora 模块 |
| migrate-all / backup-tenants 脚本真跑 | 脚本写了 · 但从没跑过 · 需要一次演练 |
| tenant 编辑操作日志 UI | 后端已写 platform_admin_log 表 · 前端还没显示 |
xylferp_web / baijian_php / xipu_erp_php / pyerp_php 容器下线(客户切流量后)erp.客户品牌.com 而不是 xxx.erp.nexora.restry.cn)关键路径:P0 三项都卡在客户切换窗口 · 需要和 fjfl/qywh/pymetal/ftgf/qyxs/heian 六家老 pyerp 客户约窗口(可分批 · 一家一家切) · 不需要一次搞定 9 家。