执行方案 · v24 · 手机端合并方案(2026-07-08)

Nexora ERP
合并执行方案

把 4 套 fork ERP(龙凤 / 摆件 / 妈祖 / 囍铺)合并为一份 Nexora ERP 主干代码,配合一租户一库物理隔离和模块化配置,通过 3 个阶段完成收敛。本文档是直接可执行的作战地图,每一步任务都带完成命令和验证判据。

owner: mike · target repo: nexora/nexora-erp(GitLab gid=75, private, 空)· baseline: xylferp_php
01 / 概览

一句话说清楚要做什么

龙凤 ERP(xylferp_php)为骨架起点建立 nexora/nexora-erp 主干 · 通过 3 个阶段依次接入龙凤(未启用客户,验证用)、摆件(已停,验证加租户流程)、妈祖(主战场,深度并入并抽模块) · 每个租户独立一个 DB(物理隔离) · 老的 xipugold/* repo 在完成迁移前原样保留 · 迁移完成后归档 read-only。

3 条硬约束

一份代码
所有租户跑同一份 Nexora 主干代码。客户差异化通过「租户配置 + 可选模块」实现,禁止为某个客户复制/修改主干代码。
一租户一库
每个客户独立 MySQL DB(nexora_<slug>),物理隔离。业务表现有 aid 字段不动,值恒等于该库租户 id,退化为"防呆兜底"(错连别家库查不到)。
模块化差异
客户独有功能(贵金属 / 妈祖商城 / 阿里云图搜)拆为可选模块,通过中心库 tenant_modules 表按租户启用。关闭模块 = 该租户看不到任何相关菜单/接口/数据表。
02 / 现状

哪里出发

2.1 客户与主干仓库

客户 主干 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

2.2 关键实证数据

🔬 2026-07-10 血缘再核实 · 修正「妈祖独立演进 / 完全不同」旧结论

§2.2 上面那条「pyerp 独立演进 · 与其他家 md5 不同」+ §2.3「妈祖 DB schema 与其他 3 家完全不同」的结论 不完整、且误导了后续方向。当时只看到「表名/schema 不同」就下了「妈祖是异类」的判断,没做妈祖 vs 龙凤的函数级血缘对比。2026-07-10 补做后发现:

  • 妈祖(pyerp_php)与龙凤(xylferp_php)是同源 fork —— 铁证:两库有 54 个同名 controller(龙凤 75 / 妈祖 112 · 重合 54);Sell.php 12 个函数里 10 个同名(buildSre / examine / record / save / import 等独特业务函数)· 这种独特命名两边一致,不可能是巧合。
  • 「完全不同」是表象 —— 差异集中在:①表前缀(珠宝 xipunum_erp_ ↔ 妈祖 is_);②妈祖 fork 后加的贵金属扩展(is_ymerp_ 21 表);③各自演化的字段微调。核心业务模型(销售/采购/调拨/销退 = sell/buy/allot/sre)、通用层概念(user/menu/role/frame)两边同源同构。
  • schema 表名重合度低的真相 —— 龙凤 104 表 vs 妈祖 116 表,去前缀后仅重合 13 张,不是因为业务不同,而是 fork 后一边把表名从 xxx 演化成 erp_xxx(珠宝加了 erp_ 中缀)、一边保持 xxx(妈祖)。是机械的命名分叉,不是模型分叉。

对合并方向的意义:妈祖(原料线)接入 Nexora 主干 不是「异构 ERP 硬凑」· 是「同源 fork 的分支收敛」。所谓「表名债」(主干代码硬编码 erp_user/erp_type · 妈祖 is_ schema 无 erp_ 中缀 → 撞表)本质是 fork 分叉的机械前缀差异,正是 Tenant::table() 抽象要解决的(该抽象已就绪:珠宝返 erp_user · 妈祖返 user)· 系统性抹平即可,不是不可行。合并本身是有意义的(本来就该是一套 · 历史 fork 分叉了)。

教训升级:判「两个仓库是否同源」必须做函数级 / controller 级血缘,不能只看 DB schema 表名。表名可以在 fork 后各自演化到面目全非,但独特业务函数名(buildSre 这种)会留下同源指纹。§2.3「数据契约先于代码血缘」仍对(合并要看数据契约),但「代码血缘」这一步当年做浅了(只 controller 文件 md5 · 没到函数级),导致把同源 fork 误判成异构。

2.3 关键漏洞·数据契约差异(v9 补)

⚠️ 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 的痛苦教训。

03 / 目标架构

一张图看懂 Nexora

L3 · 外部入口 B 端管理后台 Vue 2 · 各租户独立域名 app_xiao_shou_kotlin UniApp 多客户 App 妈祖商城 pyerp_shop_php · 未来所有小程序基座 妈祖商城小程序 pyerp_shop_wx L2.5 · 统一 API 网关 鉴权 · 租户路由 · WS 转发 L2 · NEXORA ERP 核心 · 一份代码 PHP 主干 (ThinkPHP 6) 账号 · 权限 aid+bid+pt_type 功能开关 模块 · 菜单级 商品 · 库存 通用 54 ctrl 订单 · 财务 采购 · 销售 PHP 可选模块(按租户启用) ymetal 贵金属 50 ctrl myhw-shop 妈祖商城对接 aliyun-image 图搜 · OSS mall-sync 商城 API Kotlin 后端(变薄后) rn.rw.erp · WS 长连接 · 打印下发 · 消息推送 · ≤15 controller L2 · 中心库 nexora_platform tenants(租户目录 + DB 连接串) tenant_modules(启用清单) users · user_tenants(跨租户账号) schema_migrations(全局版本号) 全局 · 跨租户 · 只存目录/账号/开关,不存业务 L1 · 租户业务库 · 一租户一库 · 物理隔离 🗄️ nexora_xylferp 🗄️ nexora_baijian 🗄️ nexora_pyerp 🗄️ nexora_xipu(择机) 🗄️ ... 未来新客户 每库保留原 aid + bid + pt_type 字段作"防呆",值恒等于该库租户 id
04 / 仓库处置总表

34 个仓库 · 每个一个动作

执行时遇到"这个 repo 要不要动 / 怎么动"的问题,以本表为准。Action 列 = 强制约束

ERP 主干仓库

RepoAction阶段说明
xylferp_phpMERGEPhase 1骨架起点 · clone 为 nexora-erp
xylferp_webMERGEPhase 1前端骨架起点 · clone 为 nexora-erp-web
baijian_phpMERGEPhase 2验证「加租户」流程
baijian_webARCHIVEPhase 290 天 0 commits
pyerp_phpMERGEPhase 2主战场 · Ymetal / Ep* 抽模块 · pt_type 合入主干
pyerp_webMERGEPhase 2妈祖前端合入 nexora-erp-web
xipu_erp_phpLATERPhase 373/75 controller 在龙凤里 · 视客户情况
xipu_erp_webLATERPhase 3同 xipu_erp_php

商城仓库

RepoAction阶段说明
pyerp_shop_phpMERGEPhase 3唯一商城基座 · 合入 Nexora mall-sync 模块
pyerp_shop_wxMERGEPhase 3唯一小程序基座 · 未来所有小程序基于此
baijian_shop_phpARCHIVEPhase 320G · 客户已停

手机端仓库

RepoAction阶段说明
app_xiao_shou_kotlinKEEPPhase 3669 commits 多客户 UniApp 壳 · Phase 3 演化为 Nexora 统一手机端 · Phase 1-2 保留原样
pyerp_appKEEPPhase 3妈祖手机端(和 app_xiao_shou_kotlin 目前无代码合并)· Phase 3 把功能重写进后再归档
kotlinapi_uniappKEEPPhase 3囍铺原版手机端 · 已被 app_xiao_shou_kotlin 演化替代 · Phase 3 直接归档
sprin-boot-kotlin219KEEPPhase 3Kotlin 后端 · Phase 3 变薄:52 controller → ≤15
sprin-boot-kotlin219-xylfKEEPPhase 32 commits 分叉 · 合入主线
fastify_spring_app_apiARCHIVEPhase 3废弃分支
fastify_ts_app_apiARCHIVEPhase 3废弃分支

弃用仓库(不投入工作)

RepoAction说明
laiya_phpDROP莱雅商城主站 · 客户不再使用
laiya_wxDROP莱雅商城主小程序
laiya_jxs_wxDROP莱雅经销商小程序
laiya_lss_wxDROP莱雅零食师小程序
lyshop_wxDROP莱雅关联小程序
hjhsDROP黄金回收(CRMEB)· 客户已停 · 不同基因不合并

其他(战略无关)

RepoAction说明
pyerp_php_apiARCHIVE2 commits · API 抽出实验未成气候
pyerp_shop_h5_distOUT-OF-SCOPE前端构建产物
wl_php / wl_webOUT-OF-SCOPE未评估(疑似"物流")
waimao_phpOUT-OF-SCOPE未评估(疑似"外贸")
supererp / hjt / xipu_newOUT-OF-SCOPE历史产物 · 未评估
electron_exe / python-auto-print-desktop / passwordOUT-OF-SCOPE桌面工具
05 / 模块清单

Nexora 主干由核心+可选模块组成

5.1 核心模块(所有租户必装)

模块包含内容
account账号 / 角色 / 权限 · 复用现有 aid/bid/pt_type 上下文注入
feature-flag模块级 + 菜单级功能开关引擎 · 读中心库 tenant_modules / tenant_features
product商品 / SKU / 分类 · 通用 54 controller 里商品相关部分
inventory库存 / 库位 / 调拨 / 盘点 / 出入库 · 含龙凤零售出库特有的 outboundBack/queryIsTuihuo
order采购 / 销售 / 审核状态机 / Bill / 财务
store门店 / 仓库 / 收银
supplier-customer供应商 / 客户 / 联系人 / 会员

5.2 可选模块(按租户启用)

模块建于包含内容 · 目标租户
aliyun-imagePhase 1阿里云图搜 + OSS(囍铺 2 独有 controller)· 未来给囍铺租户用
ymetalPhase 2贵金属:50 个 Ymetal* controller + 40+ 张表 + 18884 行代码 · 妈祖专属
myhw-shopPhase 2妈祖商城对接:Epuser / Eprole / Epservice / Epxipu* · 妈祖专属
gold-pricePhase 3金价管理(get/save_gold_price + get_gold_code)· 囍铺独有,合入时抽出
mall-syncPhase 3ERP ↔ 妈祖商城对接:products.sync / orders.push / stock.query
mobile-apiPhase 3Kotlin 变薄后的 WS + 打印 + 推送对接

5.3 模块生命周期规范

每个模块目录 modules/<name>/ 自包含:

06 / Phase 1

Phase 1 · 龙凤先入

PHASE 1 骨架 + 首个租户

目标 · 以 xylferp_php 为骨架建立 nexora/nexora-erp 主干,把龙凤客户作为第一个租户接入,验证「一份代码 + 一租户一库」框架成立。

前置条件

  • GitLab nexora group 已建(gid=75, private)· 已完成
  • 4 个开发已加入 group Developer 权限(root / dev-alice / dev-bob / dev-simon)· 已完成

任务清单

1.1建 nexora-erp repo,以 xylferp_php 为起点
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 返回 200
· git log --oneline | wc -l ≥ 113(保留原始 git 历史)
1.2同步建 nexora-erp-web(前端骨架)
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 返回 200
1.3建中心库 schema
CREATE 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 张表
1.4建 nexora_xylferp DB,从 xylferp 生产迁数据
# 备份龙凤当前 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' = 1
· SELECT COUNT(*) FROM nexora_xylferp.is_menu 数量和原 xylferp DB 一致
1.5改造 BaseController 从中心库读租户上下文

原 xylferp 的 aid/bid/pt_type 从 token 回填。改造后:token 里带 tenant_slug,BaseController 先查中心库 tenants 拿到 db_dsn + aid/bid/pt_type,再切到对应租户库。

完成判据 · 用龙凤租户 token 请求 /api/user/info,返回数据来自 nexora_xylferp
· 用一个错误 tenant_slug 请求,返回 401
1.6抽第一个可选模块 aliyun-image(样板)

从 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
· 登录龙凤租户前台,搜"图搜"应搜不到菜单
1.7部署 Nexora 运行环境

选一台服务器(建议内网 dev 环境优先),部署 nginx + PHP + nexora-erp 代码,前端部署 nexora-erp-web。域名先用 nexora-dev.internal

完成判据 · curl -sI http://nexora-dev.internal/api/health 返回 200
· 前台用龙凤租户账号登录,能看到采购 / 销售 / 库存 / 门店 / 报表 / 零售所有主功能菜单
1.8落地运维套件最小可用版(见 §09)

写 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

Phase 1 出口标准(all 通过才进 Phase 2)

  • 功能验证:龙凤客户/内部人员用龙凤租户账号登录 Nexora,批发 + 零售所有关键单据可以走通一遍(采购单/销售单/入库出库/盘点/报表)
  • 数据一致:同一操作在 Nexora 和原 xylferp 生产上得到相同结果(抽 5 个业务场景对比)
  • 模块隔离:grep -rE "aliyun|Aliyun" app/controller/ 主干目录零命中
  • 租户切换:同样的接口传不同 tenant_slug token,返回不同租户库的数据
  • 运维套件:tenant-onboard / migrate-all / backup 三个脚本各跑通一次
07 / Phase 2

Phase 2 · 摆件 + 妈祖并入

PHASE 2A 摆件迁入(验证加租户流程)

目标 · 用 Phase 1 的运维套件把摆件作为第 2 个租户接入,证明「新增租户 = 一条命令」成立。

任务清单

2A.1用 tenant-onboard 脚本开摆件租户
bin/tenant-onboard.sh baijian --name "摆件" --aid 2
完成判据 · SELECT * FROM nexora_platform.tenants WHERE slug='baijian' 返回 1 行 · status=active
· SHOW DATABASES LIKE 'nexora_baijian' 存在
2A.2从 baijian_php 生产迁入历史业务数据(如需要)
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
完成判据 · 主要业务表行数和原 baijian DB 一致
· SELECT DISTINCT aid FROM nexora_baijian.is_user 只返回 2
2A.3摆件租户切换 · diff 主干缺失能力

用摆件账号登录 Nexora 前台,把主要业务流程走一遍。如果发现主干缺失能力(baijian 独有 0 个 controller,理论应完美复用),记录并补主干。

完成判据 · 摆件核心业务(采购/销售/库存)全部可用
· 修 1 个 bug(在 Nexora 主干)后,龙凤 + 摆件两租户都验证生效
· 整个流程 15 分钟内跑通(证明加租户已经工业化)

Phase 2A 出口

  • 2 个租户(龙凤 + 摆件)在同一份 Nexora 部署上并存,互不影响
  • tenant-onboard 脚本工业化:15 分钟内能加一个新租户

PHASE 2B 妈祖深度并入(主战场)

目标 · 把妈祖 pyerp_php 的 58 个独有 controller 分拆成 3 个可选模块(ymetal / myhw-shop / gold-price 里其一)+ 主干扩展,把 pt_type 三层租户合入主干。妈祖客户切换到 Nexora,老 pyerp_php 冻结。

任务清单

2B.1抽 ymetal 模块(贵金属)

把 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 = 50
· ls modules/ymetal/migrations/*.sql | wc -l ≥ 40
2B.2抽 myhw-shop 模块(妈祖商城对接)

把 pyerp_php 里 Epuser / Eprole / Epservice / Epxipu* 系列 controller 搬到 modules/myhw-shop/

完成判据 · grep -rE "^use.*Ep(user|role|service|xipu)" app/ 零命中
2B.3评估 Xipu.php(pyerp 里跨接喜铺的代码)

pyerp 里的 Xipu.php 里查 erp_category 表 —— 是否仍在生产使用?

  • 如仍用:合入 modules/myhw-shop/ 或单独模块 myhw-xipu-bridge
  • 如废弃:主干移除,归档决策
完成判据 · 决策记录在 modules/DECISIONS.md · Xipu.php 归位
2B.4合入 pt_type 三层租户到主干

pyerp 独有的 pt_type(产品线维度,1/2/3/4)合入主干 BaseController 的上下文注入。所有租户获得 pt_type 能力(默认值 1)。

完成判据 · 主干 BaseController grep define('pt_type' 有 1 处
· 龙凤 + 摆件两租户 pt_type 默认 1,不影响原有功能
2B.5合入 pyerp 的 BaseController / common.php 差异

pyerp 的 BaseController.phpcommon.php md5 已与其他家不同。做 3-way diff,把有价值的公共逻辑(sqlAuth / getBaseRoot / frameScope / erpPostInput 等)合入主干,妈祖专属逻辑放模块。

完成判据 · 主干 app/common.php 覆盖 pyerp / xylferp / xipu 三家的公共函数需求
· modules/myhw-shop/common.php 只放妈祖专属
2B.6开妈祖租户 + 迁入数据
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') = 2
· SELECT * FROM nexora_pyerp.Ymetal_原料库存 LIMIT 1 能查到数据(模块 migrations 生效)
2B.7妈祖客户切换(选低峰窗口)
  • 冻结原 pyerp_php 生产(nginx 加维护页)
  • 跑最后一次增量数据同步
  • DNS/反代切到 Nexora
  • 观察 48 小时
完成判据 · 切换窗口 ≤ 2 小时
· 切换后 48 小时内 妈祖客户零投诉
· 关键单据(采购/销售/贵金属日结/门店流水)当天正常
失败信号(触发回滚) · 妈祖客户 30 分钟内多次报错
· Ymetal 模块任何日结/结算流程出错
· 数据串到其他租户库(SELECT DISTINCT aid FROM nexora_pyerp.is_sell 出现非 1 的值)

Phase 2 出口标准

  • 3 个租户(龙凤 + 摆件 + 妈祖)全部在同一份 Nexora 主干代码 + 一租户一库跑
  • ymetal 模块可切换:摆件租户前台无任何贵金属菜单/接口/表
  • 妈祖客户业务无中断 · 老 pyerp_php 冻结成 read-only 归档
  • 主干代码 grep -rE "Ymetal|Ep(user|role|service)" app/ 零命中
08 / Phase 3

Phase 3 · 手机端 + 商城 + Kotlin变薄 + 囍铺择机

PHASE 3A Kotlin 后端变薄

目标 · sprin-boot-kotlin219 从 52 个 controller 精简到 ≤15,只保留 WebSocket / 打印下发 / 消息推送 / 定时任务。业务 CRUD 全部迁到 Nexora PHP 主干。
3A.1审计 52 个 Kotlin controller 分类
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)。

完成判据 · 审计报告分类完成 · 每个 controller 有明确 tag
· KEEP 数 ≤ 15 · MIGRATE 数 = 52 - KEEP
3A.2MIGRATE 类 controller 的 PHP 对等实现

对每个 MIGRATE controller,在 Nexora 主干或对应模块里实现相同的接口(路径 + 参数 + 返回结构)。

完成判据 · 对每个迁移接口写 diff 对比测试(Kotlin 版 vs PHP 版返回相同 JSON)
· 通过率 100%
3A.3app_xiao_shou_kotlin 切流量到 PHP 版接口

修改 SERVER_LIST,把囍铺/周锦记的 backend 从 kotlin 改为 php,请求走 Nexora 主干。

完成判据 · 抓包确认手机端所有业务请求打到 Nexora 主干
· Kotlin 后端只收 WS 长连接 + 打印 + 推送
3A.4Kotlin 后端下线业务 controller

MIGRATE 类 controller 从 Kotlin 后端删除 · 只保留 KEEP 类。

完成判据 · grep -rl "@RestController" sprin-boot-kotlin219 | wc -l ≤ 15
· Kotlin 后端连续跑一周,业务无异常
3A.5Kotlin 只读原则:禁止写业务表

Kotlin 后端要写数据时,通过 HTTP 调 Nexora PHP 主干 API。DB 连接改成只读账号。

完成判据 · Kotlin DB 连接串使用 read-only 账号
· 尝试 INSERT/UPDATE 应报权限错误
3A.6sprin-boot-kotlin219-xylf 合入主线

xylf 分叉只有 2 commits,cherry-pick 差异合入 sprin-boot-kotlin219,归档 -xylf。

完成判据 · -xylf repo GitLab 标 archived

PHASE 3B 手机端归口

目标 · 把 pyerp_app 的业务功能重写进 app_xiao_shou_kotlin,统一为 Nexora 手机端。kotlinapi_uniapp 直接归档(已被 app_xiao_shou_kotlin 覆盖)。
3B.1app_xiao_shou_kotlin 加租户切换到 Nexora

SERVER_LIST 里"妈祖"/"周锦记" 等 host 改为 Nexora 主干域名。

完成判据 · App 里选任一租户登录,请求打到 Nexora
3B.2把 pyerp_app 独有业务功能重写进 app_xiao_shou_kotlin

pyerp_app 独有页面/组件(妈祖手机端专属)逐个移植。

完成判据 · 妈祖客户用 app_xiao_shou_kotlin 打包的 App 能覆盖原 pyerp_app 所有功能
· 妈祖用户测试反馈无缺失
3B.3归档 pyerp_app + kotlinapi_uniapp

GitLab 上把两个 repo 标 archived。

完成判据 · 两 repo GitLab 状态 = archived · README 里指向 app_xiao_shou_kotlin

PHASE 3C 妈祖商城 + mall-sync 模块

目标 · pyerp_shop_php 作为唯一商城基座接入 Nexora · 通过 mall-sync 模块的 3 条 API 通信,消除商城对 ERP 库的直查。
3C.1建 mall-sync 模块

modules/mall-sync/ 定义 3 条 API:

  • POST /mall-sync/products.sync — ERP → 商城推商品/SKU/价格
  • POST /mall-sync/orders.push — 商城 → ERP 推订单
  • GET /mall-sync/stock.query — 商城 → ERP 查实时库存
完成判据 · 3 个 endpoint 用 curl 测通
· Postman collection 记录到 modules/mall-sync/docs/
3C.2pyerp_shop_php 改造为走 mall-sync API

移除 pyerp_shop_php 里对 ERP 库表的直查 · 全部改为 curl mall-sync API。

完成判据 · grep -rE "Db::connect|Db::name.*is_" pyerp_shop_php/application/ 应零命中
· 商城 + ERP 可以部署到不同服务器
3C.3合入 nexora-mall(可选)

如果决定把商城代码也纳入 nexora group,创建 nexora/nexora-mall · 从 pyerp_shop_php clone · 否则 pyerp_shop_php 原样保留在 xipugold。

完成判据 · 决策记录在 modules/mall-sync/DECISION.md

PHASE 3D 囍铺择机

目标 · Phase 3 末评估囍铺状态,做二选一决策。
3D.1评估囍铺客户状态

查最近 30 天囍铺业务活跃度(登录用户数 / 单据量 / 客户投诉) + 客户业务方向。

  • 方案 A · 合并:囍铺仍活跃 → 抽出 gold-price 模块(3 方法)+ 启用 aliyun-image 模块(Phase 1 已抽) · 用 tenant-onboard 开囍铺租户 · 迁数据 · 切流量
  • 方案 B · 自然退役:囍铺客户业务萎缩 → 老 xipu_erp_php 原样跑到自然退役 · 归档时打 tag retired
完成判据 · 决策记录在 DECISION-xipu.md

Phase 3 出口标准(战略完成)

  • 3-4 个租户全部在 Nexora 主干代码 + 一租户一库跑(囍铺看 3D 结果)
  • Kotlin 后端 controller 数 ≤ 15,只做实时/推送/打印
  • 手机端只有一份 App 打包(app_xiao_shou_kotlin)
  • 妈祖商城通过 mall-sync API 通信,商城代码无直查 ERP 库
  • 老 xipugold group repo 全部归档 read-only(除 xipu_erp_php 视 3D 决定)
  • 莱雅 6 个 repo(laiya_* + lyshop_wx + hjhs)全部 GitLab 标 archived
09 / 运维套件

一租户一库的必备基础设施

一租户一库的代价 = 运维成本随客户数线性增长。必须把每个环节脚本化。Phase 1 就要落地最小可用版,后续 Phase 持续增强。

9.1 tenant-onboard(新租户开通)

位置: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 秒内新租户能登录,前台菜单只显示启用的模块。

9.2 migrate-all(migrations 分发)

位置: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 都出现该表。

9.3 backup-tenants(备份轮询)

位置: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>/ 有当日备份文件。

9.4 monitor(部署监控)

位置:nexora-erp/bin/monitor.sh(cron 每 5 分钟)

monitor.sh

# 按租户视角检查:
# - 各租户 DB 可连接
# - 各租户前台 /api/health 返回 200
# - 备份状态(最近 24h 内有成功备份)
# - 慢查询(> 1s)计数
# - 磁盘用量 > 80% 报警
# 异常发飞书告警

验证:手动停一个租户 DB,5 分钟内收到告警。

10 / 明确不做的事

负范围 · 防止scope creep

执行时任何"顺便把 X 也做了"的问题,以本节为准。不在 §04 总表 + §06-08 路线图里的东西,默认不做。

关于弃用的仓库

  • 莱雅商城(laiya_php / laiya_wx / laiya_jxs_wx / laiya_lss_wx / lyshop_wx)· 客户已不用 · 不写归档脚本 · 不写数据导出 · 不跟客户沟通 · GitLab 标 archived 即完成
  • hjhs 黄金回收 · 独立 CRMEB 200+ controller · 不做合并 · 不做 SSO · 不做任何对接
  • baijian_shop_php · 客户已停 · 不做迁移 · 不做数据备份

关于囍铺

  • Phase 1-2 不动囍铺任何代码 · 客户业务不中断
  • Phase 3D 前 · 不做囍铺前端 diff · 不做囍铺数据库 schema diff
  • Phase 3D 决策方案 B(自然退役)· 不投入任何合并成本

关于数据迁移

  • Phase 1-3 期间 · 不做真实数据双向同步 · 新 Nexora 库和老库物理独立
  • 数据迁移只在客户切换时做单向 dump/restore
  • 不做增量同步中间件 · 不做双写
  • 迁移完成后的回迁策略:合并完 + 稳定 3 个月后再评估

关于手机端

  • Phase 1-2 手机端不动 · 老 App / 老后端原样跑
  • 不做客户 App 强制升级 · 不做旧版本兼容性回退
  • 不做 iOS 上架 / 华为渠道打包相关(维持原状)

关于代码风格与技术债

  • 不做代码风格重构 · 不做 ThinkPHP 6 → 7/8 升级 · 不做 Vue 2 → 3 升级
  • 不做 UI 层重设计 · 保持原有前端交互
  • 不做测试覆盖率补齐(除非验收标准明确需要)
  • 不做性能优化(除非影响业务可用性)

关于外部集成

  • 不做 SSO 打通(除 Nexora 内 PHP ↔ Kotlin 之间必须)
  • 不做飞书 OAuth 集成 · 不做微信开放平台账号打通
  • 不做第三方 ERP / SaaS 对接
11 / Phase 2B v2 修复方案

妈祖「以龙凤为准」合并方案

v10 决策:妈祖 pyerp 客户端账号迁到龙凤主干(schema 冲突时以龙凤为准)· 商城代码不动 · ERP 换 Nexora 前端。 拆分方案由 v11 §13 迭代 · 已由 §14 落地 (6 家独立租户)。

12 / 平台超管入口

Platform Admin · 从任一租户 admin 升级

v10 决策:平台超管入口复用龙凤 admin(platform_admin 标记)· 不建独立后台。 已由 §14/§15 落地 · PlatformAdmin 8 action controller · /platform-admin.html UI 完备。

13 / pyerp SaaS 拆租户

Multi-tenant unfold · v9 错 · v11 补

v9 假设 pyerp 单租户 · 建单库合并 → v10 review 发现 is_goods.aid 有 10 个租户 → v11 §13 决策拆 6 家独立租户。 拆分方案已由 §14 落地(Phase 2C 执行完成)· 详见 §14 现状快照。

14 / 现状快照

Live State · 2026-07-07 修正版

📸 v14 修正

v11 版曾声称有 10 家租户 · 那是基于错误的架构理解建的空壳。真实生产架构是两条产品线(见 §18)· Nexora 目前只承载珠宝线 3 家 · 原料线 6 家仍在原 myhw_py99999_com 生产库。

14.1 Nexora 已承载租户(珠宝线)

slug 品牌 URL nexora_ 库 生产库 数据一致?
xylferp 龙凤 xylferp.erp.nexora.restry.cn nexora_xylferp(4.8 MB) xylferp(4.8 MB) ✅ 100% schema 一致(0 张真差异 · 之前 dump 报的 116/118 是 AUTO_INCREMENT 假差异)
baijian 摆件 baijian.erp.nexora.restry.cn nexora_baijian(6.8 MB) baijianerp(6.8 MB) ✅ 100% schema 一致(0 张真差异 · 同龙凤)
xipu 囍铺 xipu.erp.nexora.restry.cn nexora_xipu(schema) xipu-mysql/xipuerp(独立容器) ✅ schema 一致(117 表 · 用生产 schema 重建 · admin/menu/frame 已灌 · 真登进 dashboard 品牌"囍铺黄金"+ 金价配置 · 数据未迁)

14.2 未承载租户(原料线 · 生产在 myhw_py99999_com)

这 6 家客户按 aid 共存于 myhw_py99999_com 单库(2026-06-14 唐润辉合并方案上线)· schema 是 is_*(原料 ERP)· 与珠宝线 xipunum_erp_* 完全不同。需要 §18 阶段 B 让 Nexora 平台接管这条线

slug 客户 aid 用户 商品
fjfl府见福礼377472
qywh清屿文化522351
ftgf凤天工坊50311
qyxs清屿销售3925
heian黑暗集团1933
?aid=21(妈祖 · 待确认)215-

14.3 已实现的平台能力

15 / URL 与后台管理

Subdomain Routing + 平台超管改租户信息

15.1 URL 格式

9 家全部使用子域名 · 每家独立 host · localStorage 天然按域名隔离(多 tab 不串号)。老 URL 保留兼容。

类型 URL
租户入口https://<slug>.erp.nexora.restry.cn
平台管理https://xylferp.erp.nexora.restry.cn/platform-admin.html
兼容老 URLhttps://nexora-erp.nexora.restry.cn/?tenant=<slug>

15.2 后台改租户信息

平台超管在「租户列表」每行「编辑」按钮打开 modal · 可改以下 6 字段。

字段 存储位置 显示在
租户名nexora_platform.tenants.name平台管理租户列表
品牌名<租户库>.xipunum_sys.name前端 header
公司全称<租户库>.xipunum_sys.company单据/报表落款
ICP 备案号<租户库>.xipunum_sys.icp页脚
公告<租户库>.xipunum_sys.notice前端首页顶部
状态nexora_platform.tenants.statusactive / archived

不可改:slug / db_dsn / table_prefix / aid(锚点字段 · 改了要迁库)。
硬约束:xylferp 不能归档(平台超管账号所在租户)。

15.3 涉及组件

组件 文件 配置/接口
frpc/etc/frp/frpc.inicustom_domains*.erp.nexora.restry.cn
Caddy/home/claw/lobster-platform/caddy/Caddyfile9 家 site block · (tenant_site) snippet import
Tenant.phpapp/nexora/Tenant.phpdetectSlug() 从 host parts[0] 取 slug
后端接口app/controller/PlatformAdmin.phpPOST /PlatformAdmin/tenantUpdate
GET /PlatformAdmin/tenantDetail?slug=
前端 UIdist/platform-admin.html租户列表「编辑」按钮 → modal 表单
16 / 经验总结

Lessons · 做 Nexora 学到的可复用经验

从 v9 假设错、v10 review、v11 §13 拆分、v11.2 URL 改造、v14 后台管理 一路走下来 · 这些经验可以直接用到别的项目(其他 ERP 合并 / SaaS 拆分 / 域名迁移)。

16.1 架构层

教训 具体
数据契约先于代码血缘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

16.2 工程层

教训 具体
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 内联 · 事务/错误处理全在同一个函数 · 好维护。

16.3 可复用到别的项目

17 / 后续安排

Next · 一份代码架构落地

🎉 Y1-Y5 完成(2026-07-08)· 6 家原料客户上 Nexora · e2e 通

遵循 "通用同 schema · 业务分模块" 原则 · 6 家全部在 Nexora 主干代码 + 独立 nexora_<slug> 库 + 标准 xipunum_ schema 下跑起来。**pymetal 独享 ymetal 模块**(左菜单多"贵金属")· 其他 5 家标准菜单。

17.1 Y1-Y5 已完成

动作 结果
Y1 建 6 家 nexora_<slug> · 标准 xipunum_ schema ✅ 6 × 119 表 · 从 nexora_xylferp clone
Y2 灌 metadata + admin + 品牌 ✅ 每家 85 菜单 · 26 岗位 · 2 frame · admin/123456 · sys.name 各异
Y3 中心库注册 + pymetal 启用 ymetal 模块 ✅ tenants 表 10 家(4 珠宝 · 6 metal)· tenant_modules 里 pymetal=ymetal enabled
Y4 metal-inventory 模块抽取 ⏭️ 跳过(通用 schema 已满足 6 家 UI · 有需求时再抽)
Y5 headless chrome + vision 验证 ✅ 6/6 dashboard 全通 · vision 判读:品牌各异 + pymetal 独有"贵金属"菜单
6 家原料客户 dashboard 拼图

6 家原料客户 dashboard · 一份代码 · 6 家独立 nexora_<slug> · pymetal 独有"贵金属"菜单

17.2 遇到的坑

现象 根因 + 修
heian.metal.nexora.restry.cn LE cert issue 挑战失败 · 30s timeout WAF 拦截 · heian(黑暗)中文含义敏感 · 阿里云边缘 reset 掉 http-01 挑战。**修**:slug 改成 darkgroup · 立刻通

17.3 Y6-Y7(仍未做 · 需客户切流量窗口)

17.4 珠宝线状态

18 / 一份代码架构(修订版)

One Codebase · 通用同 schema · 业务分模块

核心原则(vol 2026-07-08)

  • 通用功能(用户 / 登录 / 岗位 / 权限 / 菜单 / 平台超管)· 所有租户共享一份 schema · 主干代码硬编码
  • 业务功能(珠宝 / 原料 / 贵金属 / 商城)· 各自独立 schema · 走可选模块 · 按租户启用
  • 这不是"双 schema 兼容" · 是"通用一份 + 业务各自"

18.1 通用层(所有租户 · 一份 schema)

用途 状态
xipunum_erp_user用户 · password(md5+bwqinr) · department · google/google_secret(2FA)✅ 珠宝 3 家已有
xipunum_erp_role岗位 · 权限位
xipunum_menu菜单树
xipunum_frame组织框架
xipunum_warehouse仓库/门店
xipunum_erp_category分类(通用维度)
xipunum_sys租户信息(name/company/icp/logo)
nexora_platform.*中心库(tenants / tenant_modules / platform_admin_log)

18.2 业务模块层(按 tenant_modules 启用)

模块 product_line 业务
gold-pricejewelry金价管理(囍铺)
aliyun-imagejewelry阿里云图搜(囍铺)
ymetalmetal贵金属结价(妈祖 · 独立 is_ymerp_* 21 表)
mall-syncmetalERP ↔ 商城 API
myhw-shopmetal妈祖商城 · Ep* 9 controller + is_ep_* 6 表
tmp-ordersmetal临时单据 6 controller(Sell/Buy/Bor/Bre/Sor/Sre 的 _tmp 分支)
mobile-adminmetal妈祖 App 管理 3 controller
inventory-apimetal库存/店铺 API
pos-mall-bridgemetal门店 POS + 微信小程序码(pyerp Sell.php 3 独有 method)
report-async通用报表异步导出队列(async_queue 表)· 珠宝/原料都可用
mobile-apijewelryKotlin 手机端 API 样板
metal-inventory(未建)metal原料库存(is_room / is_summary / is_batch)· 未来 Y3

18.3 运行时决策

机制
Codebasenexora-erp 唯一主干 · pyerp_php 归档下线
租户识别Tenant::resolve() 从 host 拿 slug · 中心库拿 db_dsn + product_line
数据隔离一租户一库(nexora_<slug>)· 物理隔离 · 无需 aid 过滤
通用 schema所有 nexora_<slug> 库都用 xipunum_erp_user/menu/role/frame/warehouse · 主干代码硬编码
业务 schema按 tenant_modules 启用 · 模块自带 migration(如 ymetal 建 is_ymerp_* 21 表)
业务代码按模块拆到 modules/<name>/controller/ · 主干仅通用能力
URL珠宝 *.erp.nexora.restry.cn · 原料 *.metal.nexora.restry.cn
前端按 product_line 决定 SPA(珠宝 dist / 原料 dist)· 都用同一份用户/菜单 API

合并的价值:一份代码 · 一套通用能力 · 业务差异走模块开关。类比 Odoo:一份代码 · 装不同模块变成 CRM / ERP / 电商 / 库存 · 但用户/权限/公司都是同一套。不是"用一份代码兼容多种非标 schema"(那样在维护差异 · 不是消除差异)。

20 / 里程碑

Milestones · M1 → M6 交付节奏

规则:每个"建完"里程碑必有对应"测完"里程碑(外部独立子代理无本次会话记忆验证)· 避免自己干活自己测。

里程碑 状态 交付
M1-ERP-READY
git tag · 07177cc
✅ 完成
2026-07-08
ERP 主干建完 · 一份代码 · 通用同 schema · 业务分模块
  • 9 家客户 dashboard 均可登进(3 珠宝 + 6 原料)
  • pymetal 独享 ymetal · 其他 5 家标准菜单
  • Caddy 通配 *.erp + *.metal · 平台超管 UI
  • 11 个业务模块建立
M2-ERP-TEST-READY ✅ 完成
FULL 24/24
ERP e2e 深度业务测试 · 24/24 案例全过 · 独立子代理外部验证
  • 4 通用(登录 / store / dashboard / 品牌注入)
  • 4 架构(一份代码 / 通用 schema / 模块挂载 / 数据物理隔离)
  • 12 差异化菜单(龙凤平台管理 · 囍铺图搜/OSS/金价 · 妈祖贵金属)
  • 4 原料标准菜单(fjfl/qyxs/ftgf/qywh 等)
  • vision 判读 9 家品牌各异 + 菜单差异正确
M2.5-WEB-UNIFIED-READY ✅ 完成
adafd1a
前端 5 fork 合并统一 nexora-erp-web + 功能页系统审计修复
  • 5 前端 fork 合并(xylferp/baijian/xipu/merp/serp)· 850+ route
  • xipu 40 页 / pymetal 33 页 / fjfl 35 页 逐页审计全通过
  • 修 8 类 bug:打印模板缺组件 · 入库[9999] · 贵金属看板 · 4 模块菜单URL · 平台管理3页
  • 平台管理 5 子菜单全可用(租户列表/新建/模块启停/超管/监控)
M3-MOBILE-READY ⏳ 待做 手机端合并建完 · Kotlin + uniapp 双 schema 归一到主干
  • Kotlin(珠宝 app_xiao_shou_kotlin)接口归口 nexora-erp/modules/mobile-api
  • uniapp(原料 pyerp_uniapp)按业务模块拆到 modules/mobile-*
  • 通用能力(用户 / 登录 / 菜单)硬编码 · 业务差异走模块开关
  • 类似 ERP 后端:通用 schema 一份 · 业务模块可选
M4-MOBILE-TEST-READY ⏳ 待做 手机端 e2e 验证通过 · 独立子代理外部验证 M3
  • 各客户手机端 login + 主要页面截图 + vision 判读
  • 珠宝走 Kotlin 客户端 · 原料走 uniapp 客户端
  • 验证接口都归口 nexora-erp 主干 · 无双 codebase 残留
M5-MIGRATION-TEST-READY ⏳ 待做 数据迁移演练通过 · dry-run + 抽样比对
  • 字段转换脚本:pwd → password 加盐 · role → department · aid 过滤
  • dev 演练:fjfl 472 商品 + 324 采购 + 11621 销售 从 myhw_py99999_com 迁到 nexora_fjfl
  • 随机抽 20 条业务单据 · 对比 nexora vs pyerp 是否一致(金额 / 数量 / 关联客户)
  • 独立子代理跑 · 报告差异清单
M6-ALL-READY ⏳ 待做 生产全线 GA · 灰度切完 + pyerp-php 下线
  • 切换顺序:darkgroup(3 商品)→ qyxs(5)→ ftgf(11)→ qywh(351)→ fjfl(472)→ pymetal(贵金属)
  • 每家观察 3 天 · 全切完稳定 1 周 → pyerp-php 容器下线
  • 老 4 个 fork repo 归档 · 34 个仓库整理
  • Nexora 独家承载 10 家客户日常运营
21 / 功能页审计

Page Audit · 逐页可用性审计与修复

方法:登录每家 ERP · 逐个打开左侧菜单每个功能页 · headless chrome + vision 判读是否 404 / 空白 / 报错。 教训:审计脚本一度手动映射路由,掩盖了"从菜单点会 404"的真问题 —— 后续改为用菜单里的 resource 直接跳转(模拟真实点击),9 家全部重审。

✅ 9 家全部审计完成 · 318 个功能页全绿

租户 产品线 功能页 结果
xipu 囍铺珠宝40✅ 全通过
xylferp 龙凤珠宝37✅ 全通过
baijian 摆件珠宝33✅ 全通过
pymetal 妈祖贵金属原料33✅ 全通过
fjfl 府见福礼原料35✅ 全通过
darkgroup 黑暗集团原料35✅ 全通过
qyxs 清屿销售原料35✅ 全通过
ftgf 凤天工坊原料38✅ 全通过
qywh 清屿文化原料32✅ 全通过

本轮修复的 8 类 bug

问题 根因 → 修复 commit
打印模板空白M2.5 合并漏拷 PrintConfig.vue + src/print/ 引擎(37 文件)→ 补齐8c22544
入库订单表 [9999]建租户 admin data 字段 NULL → getWarehouse SQL 收到空数组崩 → 补 data=全部(7 家 + 建租户 SQL 固化)81b3284
贵金属看板"加载失败"Ymetalapi 无 dashboard 方法 + 模块化路由下 ApiAuth 用 controller()/action() 判权限失效 → 补方法 + pathinfo 白名单aa77b09
菜单点 OSS/图搜/金价/贵金属 4044 个模块菜单 resource 存后端接口路径而非前端 route → 全改前端 SPA routea0de735
平台管理 3 子页 404模块启停/平台超管/系统监控 只有菜单没前端页(后端 API 早齐全)→ 实装 3 个 Vue 页面adafd1a

系统性发现

两类反复出现的坑:①菜单 resource 存错 —— 模块菜单里存的是后端接口路径,前端 vue-router 不认识就 404,必须存前端 route。 ②ApiAuth 对模块化路由失效 —— request()->controller()/action() 在 modules/ 下返回空,导致 $url="/",白名单和菜单匹配全失效。已给只读接口加 pathinfo 白名单兜底,以后新加 modules 接口要注意。

22 / 手机端合并

Mobile Merge · 手机端合并实施方案

目标:把手机端收敛为唯一 App 单体 app_xiao_shou_kotlin + 唯一后端 nexora-erp PHP(多租户),与 ERP 后端"一份代码 + 通用 schema + X-Tenant-Slug 租户头分流"对齐。基准 commit 9b5f67d(Phase 3B.1)。

🚀 执行进展(2026-07-09)

  • 基座已建 · GitLab nexora/nexora-app(基于 app_xiao_shou_kotlin · 670 提交历史保留 · main 分支)· 老库不动
  • 后端选型敲定 = PHP · 实查证伪 Kotlin 三个"不可替代"能力:WebSocket 只是 HTTP 长连通道(routerRegistry.dispatch)· gridReport 就是查库(mybatisplus)· netweight 是净重统计非硬件秤。囍铺当年用 Kotlin 是"有 Java 工程师就用了",非技术必然。数据连接可靠性 PHP=Java(同一 MySQL)
  • P1 完成 ✅ · http.js 拦截器注入 X-Tenant-Slug(3faedd6)· 后端契约实测:xylferp→龙凤 / baijian→摆件 · 错/缺 slug 后端 TENANT_NOT_RESOLVED 拒绝(防串库)
  • P3 复核 = 已完成 ✅ · 实证 pyerp_app 全部 55 个 jerp/serp 业务页已逐字节(md5)并入 nexora-app + pages.json 注册 · merp 从来是空占位(侦察报告"merp 待搬"系误判)· 妈祖 pt_type=2/3 首页 + 请求栈均在
  • P2 待定 · 囍铺 150 个 Kotlin @WsRouter 接口重写进 nexora-erp PHP(60 表 / 2.7 万行 · 数据不迁 · 约 2-3 周)· 妈祖切 nexora backend 涉生产切流量,待窗口

22.1 现状定性 · 合并已完成大半

深度侦察(/tmp/mobile-recon/REPORT.md)结论:主力 app_xiao_shou_kotlin 已是多 ERP 分流单体,靠登录用户 pt_type 运行时分流到不同业务模块 + 不同后端。

App 身份 / 处置
app_xiao_shou_kotlin唯一活跃主力(670 提交,活跃到 2026-07-06)· 含 xerp囍铺/jerp+serp莆阳/merp妈祖占位 · 保留为唯一手机端
pyerp_app莆阳原料线 · jerp/serp 55 页已逐字节 md5 一致并入主力 · 已是历史快照,merp 待搬后归档
kotlinapi_uniapp被取代的旧双 tabbar 架构(停在 2026-04-13)· 归档

三大真缺口

  1. X-Tenant-Slug 请求头只有注释无实现 —— http.js 拦截器没注入租户头,Phase 3B 半成品
  2. 妈祖 merp 页面未搬入 —— pages/merp/ 只剩 appEvent.js 占位
  3. 后端仍多套并存 —— Kotlin 7782囍铺/7783龙凤 + 妈祖老 PHP + nexora-erp PHP,未收编

22.2 收敛终态

唯一 App: app_xiao_shou_kotlin
  ├─ 通用层: 登录(SERVER_LIST 选租户) / 首页分流 / 我的 / 设置
  └─ 业务模块(命名空间隔离): xerp囍铺 / jerp+serp莆阳 / merp妈祖
       ↓ 统一请求栈 uilts/http.js + X-Tenant-Slug
唯一后端: nexora-erp PHP (/api/*, X-Tenant-Slug 分库, 连 nexora_<slug>)
  └─ 退役: Kotlin 7782/7783 + fastify 7780 + 妈祖老 PHP

22.3 四阶段方案

阶段 核心动作 风险 里程碑 tag
P0 清理kotlinapi + pyerp_app 归档(先 diff 防丢)MOBILE-P0-CLEAN
P1 租户头http.js 注入 X-Tenant-Slug + 端到端验证(补 3B 半成品)中·串库MOBILE-P1-TENANT
P2 后端收编Kotlin 7782/7783 接口迁 nexora-erp PHP高·WS/报表/秤MOBILE-P2-BACKEND
P3 妈祖并入搬 merp 页面 + 统一请求栈中·回归MOBILE-P3-MAZU

22.4 各阶段交付物与验收标准

P0 · 清理归档

P1 · 打通租户头(最高优先级)

P2 · 珠宝线后端收编(最高工作量/风险)

P3 · 妈祖 merp 并入 + 请求栈统一

22.5 贯穿红线与执行顺序

  1. 禁 SERVER_LIST fallback —— 登录只用选中 host(loginUtils.js 铁律),防多客户串库灾难(有历史血泪)
  2. pt_type=1 囍铺行为字节级不变 —— 每阶段回归验证囍铺现网路径无变化
  3. 每个完成判据必须实测 —— curl/headless 抓真实响应,不看代码猜
  4. 验收用独立子代理(无本次会话记忆),只验不建,沿用 ERP 318 页审计法

建议执行顺序:P0 → P1 → P3 → P2(不是 P0→1→2→3)。理由:P2 后端收编工作量/风险最大(WebSocket / gridReport 报表 / netweight 网络秤 PHP 无对等实现),放最后持续演进;先做 P3 让"App 一份代码"先落地。

中心库统一认证 · Central Auth(2026-07-09/10 已落地)

登录认证从「查租户库验密码」上收到中心库 bcrypt 统一认证。网页端 + 手机端共用同一套登录。 核心设计原则:认证和「能进哪家」在中心库,进去后「能干什么」在租户库——中心库管门禁,租户库管店内工牌。

架构

pt_type · 业务线标识

pt_type(1 普通/珠宝 · 2 金属)是 xipunum_erp_user 表的一个字段,决定登录后进哪套业务界面。 已给所有租户库补上该字段并回填(jewelry=1 · metal 保留 myhw 源真实值)。

⚠️ 血泪教训 · 差点弄错的方向(2026-07-09)

做这次认证时,为了拿 pt_type,我一度把 metal 租户的 tenants.table_prefixxipunum_ 改成 is_(想「珠宝查 xipunum_erp_user / 原料查 is_user」双表分取)。 结果破坏了原本已合并好的 metal 网页端——metal 库里 103 个表全是 xipunum_ 前缀,改 prefix 后业务代码查 is_erp_user/is_menu(不存在)→ store/dashboard 全崩。

根本错误:违背了方案 §18 + Y1 早已钉死的决策——「一份代码 · 通用一份 schema · 所有租户统一 xipunum_」。metal 6 家 nexora 库当初就是从 nexora_xylferp clone 的 xipunum_ 结构,这是有意设计不是失误。

正确做法(已改回):不动 table_prefix(保持 xipunum_),pt_type 作为字段补进 xipunum_erp_user。妈祖 is_user老生产库(myhw/ylerp)的表,迁进 nexora 时做格式转换灌进 xipunum_erp_user(Y6:is_user.pwd→password 加盐 · is_user.role→department),不是原样保留 is_user 表。

一句话铁律:囍铺系 xipunum_erp_user / 妈祖系 is_user老系统的区别;迁进 nexora 后统一成 xipunum_erp_user 一张表禁止把 metal 租户 table_prefix 改成 is_。

已完成 · 端到端验证

未做 · 后续里程碑