多租户 ERP · API 项目手册(ThinkPHP 8)


本文档是 API 项目(api.mpkerp.com) 的完整手册:框架学习、代码导览、开发规范、部署运维、数据库迁移。
你是什么角色看哪些部分
刚接手项目的新人第一部分(项目介绍)→ 第二部分(搭环境跑起来)→ 第三部分(框架速成)
要给系统加功能的开发者第四部分(代码导览)→ 第五部分(开发 SOP,照着模板抄)
要上线 / 维护服务器的运维第六部分(部署运维)→ 第七部分(数据库迁移)→ 附录 A(历史迁移记录)

第一部分 · 项目介绍

一、这是什么

一套多租户的 ERP 系统 API,服务法医试剂 / 检测行业的企业(客户、招标、试剂盒、订单、库存、出入库)。

三个组成部分:

  • API 平台 api.mpkerp.com —— ThinkPHP 8 写的后端,所有业务逻辑在这里。
  • 前端 erp.mpkerp.com —— 纯 HTML + JS + PHP 页面,通过 API 拿数据,自身不碰数据库。
  • 数据库 —— 一台 MySQL 服务器,包含「平台库」+ 每个租户独立一个「租户库」。

二、整体架构

   ┌───────────────────────────────────────┐
   │                              三端用户(同一套业务)                          │
   │                 网页端 ────── 手机端 ────── 桌面端               │
   └───────────────────┬───────────────────┘
                       │  HTTPS + JSON(跨域已开)
                       ▼
          ┌─────────────────────────────┐
          │  API 平台 api.mpkerp.com    │  ← ThinkPHP 8
          │     (多租户路由中枢)      │
          └──────────────┬──────────────┘
                         │  按 tenant_code 动态切库
          ┌──────────────┼──────────────┐
          ▼              ▼              ▼
      租户A独立库   租户B独立库   租户C独立库
    (erp_mpkerp)     (erp_xxx)      (erp_yyy)

核心设计(本项目最值钱的地方):

  • 多租户:多个企业共用一套系统,但每家独立一个数据库,互不干扰(数据天然隔离)。
  • 租户注册表:平台库 api_mpkerp_com.platform_tenant 表记录每个租户的 tenant_code(如 erp、cpa)和它自己的数据库连接信息(库名、账号、密码、主机、端口)。
  • 三端通:网页 / 手机 / 桌面共用同一套 API,前端只存一个 token。

三、技术栈

层技术说明
---------
后端框架ThinkPHP 8(TP8)国内主流 PHP 框架,MVC
数据库MySQL 8每租户一库,统一 utf8mb4
ORMthink-orm ^3 \^4TP 自带,Db::name() / 模型两种用法
运行时PHP >= 8.0建议 8.1 及以上
依赖管理Composercomposer.json 声明依赖
Web 服务器Nginx / Apache网站根目录指向 public/
前端原生 HTML + JS(无框架)assets/lib/mpk.lib.min.js 封装 API 调用

四、目录结构(先说最重要的)

api.mpkerp.com/
├── .env              ★ 环境变量(数据库账号密码等),启动时自动读取,绝不提交 git
├── composer.json     ★ 项目清单:依赖、命名空间(app\ 映射到 app/ 目录)
├── think             ★ 命令行入口(php think xxx)
│
├── app/              ★★★ 你的业务代码全在这
│   ├── controller/      控制器(接收请求、返回响应)—— 19 个
│   ├── model/           模型(每个模型对应一张表)—— 3 个
│   ├── middleware/      中间件(请求前后拦截)—— 3 个
│   ├── service/         服务层(复杂业务逻辑,如租户切换)—— 9 个
│   ├── common.php       ★ 全局函数(项目自定义辅助函数)
│   ├── BaseController.php  控制器基类(统一成功/失败返回)
│   ├── middleware.php      全局中间件注册表
│   └── ExceptionHandle.php 异常处理(出错时的兜底)
│
├── config/           ★★ 配置文件(database.php 数据库连接最关键)
├── route/            ★★ 路由定义(URL → 控制器方法)
├── public/           ★★ Web 根目录(唯一对外开放的目录)
│   ├── index.php         框架统一入口
│   ├── root.php          根路径分发器(开发 → 运维面板,生产 → 欢迎页)
│   ├── manual.php        本文档的网页版入口
│   ├── ops/              开发运维面板(查看日志 / API 清单 / 接口试调)
│   └── robots.txt
├── runtime/            运行时缓存 + 日志(apiLog 写的日志在这里)
├── vendor/             框架本体 + 依赖库(composer 管理,永远别手动改)
└── extend/             自写扩展类库(本项目没用到)

五、三个库分别放什么

平台库 api_mpkerp_com      ← 只有 1 张核心表 platform_tenant(租户注册表)
   └── platform_tenant     tenant_code / db_name / db_host / db_port / db_user / db_pass / web_domain ...

租户库 erp_mpkerp_com      ← 真正放业务数据,共 26 张表
   ├── 系统表   sys_user / sys_role / sys_token / sys_dept / sys_pstn / sys_dict
   ├── 基础表   base_prvc / base_city / base_locus / base_exps / base_supp / base_pduc
   └── 业务表   tender_main|item / kit_main|manu|locus / order_* / stock_* / stoin_* / stout_*

数据库备份文件在仓库根目录 erp.mpkerp.db/:

  • api_mpkerp_com.sql —— 平台库
  • erp_mpkerp_com.sql —— 租户库(模板租户 erp)

第二部分 · 本地开发环境搭建(ThinkPHP 8 的设置与使用)

目标:在一台新电脑上,把 API 跑起来、能登录、能调接口。

一、环境要求

软件版本检查命令
---------
PHP>= 8.0php -v
Composer最新composer -V
MySQL8.xmysql --version
扩展pdo_mysql、mbstringphp -m

PHP 需要开启的扩展:pdo_mysql、mbstring、openssl(TP8 必需)。

二、安装依赖

在项目根目录(api.mpkerp.com/)执行:

composer install
  • 会自动拉取 ThinkPHP 8 框架和所有依赖到 vendor/。
  • 如果未来修改了 composer.json,重新执行 composer update。

三、配置 .env(两套数据库连接)

项目自带 .example.env 模板,复制一份命名为 .env:

Copy-Item .example.env .env

.env 里有两组连接配置,这是本项目最重要的配置:

# ① 默认连接 = 租户库连接(运行时会被 TenantService 按租户动态覆盖,见第四部分·二)
DB_HOST=127.0.0.1
DB_NAME=erp_mpkerp_com
DB_USER=erp_mpkerp_com
DB_PASS=你的密码
DB_PORT=3306

# ② 平台库连接 = 多租户路由中枢(只读 platform_tenant 注册表)
PLATFORM_DB_HOST=127.0.0.1
PLATFORM_DB_NAME=api_mpkerp_com
PLATFORM_DB_USER=api_mpkerp_com
PLATFORM_DB_PASS=你的密码
PLATFORM_DB_PORT=3306

# 调试开关
APP_DEBUG=true      # 开发必须 true;生产必须 false
SQL_TRACE=false     # true 时在页面/日志打印 SQL,调试用
记住:.env 绝不提交 git(里面有数据库密码)。

四、建库并导入数据

用 phpMyAdmin 或命令行:

# 平台库
mysql -u root -p < erp.mpkerp.db/api_mpkerp_com.sql
# 租户库
mysql -u root -p < erp.mpkerp.db/erp_mpkerp_com.sql

然后按第二部分·三里的账号建 MySQL 用户并授权(生产账号最小权限规范见第六部分·三)。

五、启动开发服务器

TP8 自带内置服务器(免配 Nginx):

php think run
# 默认监听 http://127.0.0.1:8000

浏览器访问:

  • http://127.0.0.1:8000/ → 开发环境会跳转到运维面板(public/ops/)
  • http://127.0.0.1:8000/manual.php → 本手册的网页版
  • http://127.0.0.1:8000/auth/login(POST)→ API 接口

六、验证安装(登录接口通)

curl -X POST http://127.0.0.1:8000/auth/login `
  -H "Content-Type: application/json" `
  -d '{"tenant_code":"erp","username":"admin","password":"你的密码"}'

返回 {code:200, msg:..., data:{token: "erp:xxx", user: {...}}} 即成功。

七、开发常用命令

php think run            # 启动开发服务器
php think clear          # 清缓存(改路由/配置不生效时先跑这个)
php think make:controller Xxx   # 生成控制器骨架(本项目的控制器大多手写)
php think route:list     # 打印全部路由

八、调试三件套

  1. APP_DEBUG=true:页面直接显示错误堆栈。
  2. SQL_TRACE=true:数据库操作会把 SQL 打印出来。
  3. runtime/log/:apiLog() 写的日志按天分文件,如 runtime/log/202608/23.log。

第三部分 · ThinkPHP 8 框架速成(新手必读)

这章讲 TP8 的通用概念,每节都指向本项目对应代码,边看文档边开代码最有效。

一、一次请求怎么走完的(生命周期)

浏览器/手机/桌面
   │  POST https://api.mpkerp.com/auth/login
   ▼
public/index.php        ← 唯一入口文件(Web 根目录只有 public 对外开放)
   │
   ▼
think\App 启动          ← 读 .env、加载配置、注册路由
   │
   ▼
路由匹配                ← route/app.php 里找 "auth/login" → 谁处理
   │
   ▼
中间件(如果该路由挂了)   ← 本项目:Cors(跨域)、Auth(登录鉴权)、DeleteGuard(生产删除防护)
   │
   ▼
控制器方法               ← app/controller/Auth.php 的 login()
   │                      (操作数据库 → 组装数据)
   ▼
返回 JSON 响应            ← {code, msg, field, data}

一句话理解:入口 → 路由 → 中间件 → 控制器 → 数据库 → 返回 JSON。

二、路由(URL 怎么找到代码)

路由文件:route/app.php

// 公开接口(不需要登录)
Route::post('auth/login', 'auth/login');

// 需要登录的接口:统一挂 Auth 中间件
Route::group('api', function () {
    Route::get('user/list', 'user/list');
    Route::post('tender/create', 'tender/create');
    // ...
})->middleware(Auth::class);

要点:

  • 'auth/login' = 控制器 Auth 类的 login 方法(小写自动映射到 app/controller/Auth.php)。
  • 路由里 Auth::class 是中间件,不是控制器。分组下的接口全部要先过登录校验。
  • 本项目 URL 统一是 /模块/动作 风格,例如 POST /tender/create。

三、控制器(接收请求、干活、返回)

namespace app\controller;

use app\BaseController;
use think\facade\Db;
use think\Response;

class Tender extends BaseController
{
    // GET /tender/list
    public function list(): Response
    {
        $data = Db::name('tender_main')->order('tender_id', 'asc')->select()->toArray();
        return $this->success($data, '获取成功');
    }
}

要点:

  • 每个控制器都继承 BaseController,从而拿到 $this->success() / $this->error() 统一返回方法,以及 $this->request 请求对象。
  • 方法必须返回 Response($this->success() 内部就是返回 json)。
  • 控制器只做「收参数 → 校验 → 调数据 → 返回」,复杂逻辑尽量放 service 或模型。

四、数据库访问(两种方式)

方式 A:Db 门面(快捷,不用建模型) —— 本项目绝大多数接口用它

use think\facade\Db;

Db::name('tender_main')->select();                            // 查所有
Db::name('tender_main')->where('tender_id', $id)->find();     // 查一条
Db::name('tender_main')->insert([...]);                       // 新增
Db::name('tender_main')->where('tender_id', $id)->update([...]);// 修改
Db::name('tender_main')->where('tender_id', $id)->delete();   // 删除
  • Db::name('xxx') 会自动找到 config/database.php 里默认连接(mysql)对应的库。
  • 注意本项目默认连接是「租户库」,运行时会被动态切换(见第四部分·二 多租户)。

方式 B:模型(ORM,更"面向对象") —— 本项目用于用户/角色/Token

use app\model\SysUser;

$user = SysUser::where('username', $username)->find();  // 查用户
$user->save(['last_login' => $now]);                    // 更新
SysToken::create([...]);                                // 新增

模型文件长这样(app/model/SysUser.php):

class SysUser extends Model
{
    protected $name = 'sys_user';      // 表名
    protected $pk = 'user_id';         // 主键
    protected $createTime = 'created_at';  // 自动写入创建时间
    protected $updateTime = 'updated_at';  // 自动写入更新时间
}

两种方式的区别(新手容易懵):

  • Db::name():直接操作,像写 SQL,灵活。
  • Model:一个类对应一张表,可以自定义业务方法。
  • 选择标准:简单查表用 Db,涉及用户/权限/Token 这类核心实体建议用 Model。

五、门面 Facade(think\facade\XXX 是什么)

一句话:静态方式调用内部服务。TP8 把很多功能做成了"门面",让你不用 new 就能用:

use think\facade\Db;      // 数据库
use think\facade\Log;     // 日志
use think\facade\Config;  // 配置
use think\facade\Route;   // 路由
use think\facade\Cache;   // 缓存

例如 Db::name()、Log::write(),本质是访问框架的单例服务,但写起来像静态方法,省事。

六、中间件(请求的"关卡")

中间件在请求到达控制器之前执行,也可以放行后再做点事。本项目有三个:

中间件作用挂载位置
---------
Cors跨域处理:允许所有域名访问,返回 Access-Control-* 头全局(app/middleware.php)
Auth登录鉴权:校验 Authorization: Bearer {token},按 token 前缀切租户库,通过后挂载 $request->authUser / authToken / authTenant需要登录的路由分组
DeleteGuard生产环境删除防护:APP_DEBUG=false 时,所有以 /delete 结尾的请求直接返回 403,防止误删全局

DeleteGuard 判定规则(很重要):

  • APP_DEBUG = true(开发环境):不拦截,方便调试。
  • APP_DEBUG = false(生产环境):拦截所有 /xxx/delete 请求 → 403「生产环境已禁用删除操作」。
  • 所有删除接口路由都以 delete 结尾(当前 23 个),无其它接口以 delete 结尾,不会误伤。

七、配置文件速查(config/)

文件内容
------
database.php★ 两个连接:mysql(默认=租户库,运行时被覆盖)+ platform(平台库)
app.php应用级配置(debug、时区、异常处理等)
cors.php跨域规则(域名白名单等)
route.php路由全局配置
middleware.php全局中间件注册表(Cors、DeleteGuard 在这里)
log.php日志配置
cache.php缓存配置
session.php / view.php / lang.php / filesystem.php / trace.php / console.php各自对应功能

八、app 根目录速查

文件作用
------
BaseController.php控制器基类:统一 success() / error() 返回
common.php全局函数(启动时自动加载)
AppService.php应用服务注册(本项目基本没动)
ExceptionHandle.php异常兜底:把异常转成统一 JSON
middleware.php全局中间件列表

九、全局函数与统一响应格式

全局函数在 app/common.php,从旧项目迁移而来,函数名与逻辑保持一致:

函数作用
------
jsonReturn(code, msg, field, data)统一 JSON 返回,抛异常立即中断(等价旧项目的 exit)
apiLog(msg, type)写日志到 runtime/log/
apiCheckRequired($input, ['字段' => '中文名'])必填校验,缺失直接 400
apiCheckInt(val, label, key, required)整数校验
apiCheckStr(val, label, key, required, max)字符串校验(去空格、限长)
apiCheckDecimal(val, label, key)小数/数字校验
apiCheckDate(val, label, key)日期校验(返回 Y-m-d)

统一响应格式(全站所有接口一致,前端按这个解析):

{
  "code": 200,      // HTTP 风格:200=成功,400=参数错误,401=未登录,403=无权限/禁用,404=不存在,405=方法错误,500=服务器错误
  "msg": "获取成功", // 提示文字(前端可直接弹出)
  "field": "",      // 出错时标记是哪个字段(前端定位用)
  "data": {}        // 数据主体
}

第四部分 · 本项目代码导览(API 全貌)

一、app 四个子目录

app/
├── controller/   ← 19 个:接口动作层,一层一个文件
│                   主从表合并为一个控制器(如 Kit 管 kit_main + kit_locus + kit_manu)
├── model/        ← 3 个:数据表映射(SysUser / SysToken / SysRole)
├── service/      ← 9 个:复杂业务逻辑
│                   TenantService(租户中枢)/ BillService(出入库明细)/ OrderService /
│                   TenderService / StockService / StoinService / StoutService /
│                   PducService / KitService
└── middleware/   ← 3 个:请求拦截(Cors / Auth / DeleteGuard)

二、多租户机制(本项目灵魂,必须理解)

租户识别两种入口:

场景租户怎么来
------
登录前端提交 tenant_code → TenantService::findByCode() 查平台库 → 切库校验用户
已登录请求Token 明文格式 {tenant_code}:{64位随机} → 中间件按冒号前部分切库
旧 Token 兼容纯 64 位 hex 无冒号 → 视为 erp 租户(老用户不重登)

切库原理(app/service/TenantService.php 核心三行):

// TenantService::switchDb() 核心逻辑:
$default  = Db::getConfig('default', 'mysql');   // 1. 拿默认连接名
Config::set($database, 'database');              // 2. 整体改配置(不支持点号多级)
Db::connect($default, true);                     // 3. force=true 强制重建连接
  • 切库后,后续所有 Db::name() / 模型查询自动落到该租户库。
  • 平台库查询必须显式 Db::connect('platform')->name('platform_tenant')。

平台库 platform_tenant 关键字段:

字段说明
------
tenant_code对外代号,唯一(uk_tenant_code)
web_domain前端域名,唯一(uk_web_domain)
db_name独立库名,唯一(uk_db_name)
db_host / db_port / db_user / db_pass连该租户库的连接信息(可指向任意主机,这就是将来数据库迁移的突破口)
tenant_status1 启用 / 0 停用(只查启用)
expire_at到期日,NULL=永久

三、全部控制器与接口(按业务域)

3.1 认证与组织权限

Auth — 登录 / 登出 / 当前用户 / 改密

方法路由功能
---------
LoginPOST /auth/login登录:tenant_code + username + password → {user, role, token, tenant_code}
LogoutPOST /auth/logout退出:删 token
InfoGET /auth/info当前登录用户信息
RePwdPOST /auth/repwd修改密码(校验旧密码)

User — 用户管理(sys_user)

方法路由功能
---------
ListGET /user/list?keyword=用户列表(返回时隐藏 password 字段)
CreatePOST /user/create新增用户(用户名唯一、密码加密)
UpdatePOST /user/update修改用户(可重置密码、改角色)
DeletePOST /user/delete删除用户

Role / Dept / Pstn — 角色 / 部门(树形)/ 岗位

方法路由功能
---------
ListGET /role/list · /dept/list · /pstn/list列表
CreatePOST /role/create · /dept/create · /pstn/create新增
UpdatePOST /role/update · /dept/update · /pstn/update修改
DeletePOST /role/delete · /dept/delete · /pstn/delete删除(有下级/在用不可删)

Perm — 操作权限点(sys_perm,岗位维度)

方法路由功能
---------
ListGET /perm/list权限点列表(含允许岗位 pstn_codes,按分组排序)
SyncPOST /perm/sync同步权限点清单(前端 PERM_DEFS upsert,仅元数据不覆盖已配置岗位;body: {list:[{perm_key,perm_name,perm_group}]})
UpdatePOST /perm/update更新允许岗位(body: {perm_id, pstn_codes},逗号分隔,逐项校验 sys_pstn 存在性)

说明:权限点 = 页面按钮/操作,编码「模块.动作」;「允许岗位」由组织架构-岗位权限页配置;

总管理员 ADMIN 前端 can() 全开;未配置的权限点默认放行(保守不误藏)。

Dict — 系统字典(sys_dict,全局枚举,供各业务状态/类型字段翻译)

方法路由功能
---------
ListGET /dict/list?type=&status=字典列表(下拉选项数据)
CreatePOST /dict/create新增(类型 + 值联合唯一)
UpdatePOST /dict/update修改
DeletePOST /dict/delete删除

3.2 基础档案

Prvc — 省份(base_prvc)

方法路由功能
---------
ListGET /prvc/list?keyword=省份列表
UserPOST /prvc/user分配省级负责人
CreatePOST /prvc/create新增省份(名称唯一,可选负责人/排序/状态)
UpdatePOST /prvc/update修改省份(名称唯一)
DeletePOST /prvc/delete删除;名下存在区域(base_city)不可删

City — 区域(base_city)

方法路由功能
---------
ListGET /city/list?prvc_id=区域列表(可按省份筛选)
UserPOST /city/user分配区域负责人
CreatePOST /city/create新增区域(省份存在校验 + 同省内名称唯一)
UpdatePOST /city/update修改区域(同省内名称唯一)
DeletePOST /city/delete删除;已被客户/招标引用不可删

Exps — 物流公司(base_exps)

方法路由功能
---------
ListGET /exps/list?keyword=物流公司列表(出库单发货时用)
CreatePOST /exps/create新增
UpdatePOST /exps/update修改
DeletePOST /exps/delete删除

Supp — 供应商(base_supp)

方法路由功能
---------
ListGET /supp/list?keyword=供应商列表(采购入库时用)
CreatePOST /supp/create新增
UpdatePOST /supp/update修改
DeletePOST /supp/delete删除

Pduc — 产品档案(base_pduc)

方法路由功能
---------
ListGET /pduc/list?keyword=&gen_type=&use_type=产品列表(可筛选)
CreatePOST /pduc/create新增产品(编码唯一、金额小数校验)
UpdatePOST /pduc/update修改
DeletePOST /pduc/delete删除;已被订单/出入库引用不可删

Locus — 位点(base_locus 基因座基础信息总表)

方法路由功能
---------
ListGET /locus/list?keyword=&locus_type=位点列表(可筛选类型)
CreatePOST /locus/create新增位点(locus_type/chr_type/locus_level 枚举白名单校验)
UpdatePOST /locus/update修改位点(locus_length 小数校验)
DeletePOST /locus/delete删除;已被试剂盒位点(kit_locus)引用不可删

Client — 客户档案(base_client)

方法路由功能
---------
ListGET /client/list?keyword=客户列表(名称/电话搜索)
CreatePOST /client/create新增客户
UpdatePOST /client/update修改客户
DeletePOST /client/delete删除;已有订单不可删

3.3 业务单据

Tender — 招标项目(tender_main + tender_item 主从表)

方法路由功能
---------
ListGET /tender/list?keyword=&status=招标列表
DetailGET /tender/detail?tender_id=详情(主表 + 明细)
CreatePOST /tender/create新增主表(市场部发布)
UpdatePOST /tender/update修改主表
DeletePOST /tender/delete删除主表(级联删明细)
ItemListGET /tender/item/list?tender_id=明细列表
ItemCreatePOST /tender/item/create新增明细(销售分两次添加:招标时 / 中标后反馈)
ItemUpdatePOST /tender/item/update修改明细
ItemDeletePOST /tender/item/delete删除明细
业务说明:招标主表由市场部发布,明细由销售在招标时添加一次、中标后再继续添加(前端有单独的"反馈"按钮),所以主表接口和明细接口是分开的。

Order — 订单(order_main + order_item 主从表)

方法路由功能
---------
ListGET /order/list?keyword=&status=订单列表
DetailGET /order/detail?order_id=详情(主表 + 明细)
CreatePOST /order/create新增订单(生成单号,明细金额联动主表)
UpdatePOST /order/update修改主表(user_id 可改项目负责人,缺省保持原值)
DeletePOST /order/delete删除(被出库单引用不可删,级联删明细)
ItemList / ItemCreate / ItemUpdate / ItemDelete/order/item/*明细 CRUD

Stoin — 采购入库(stoin_main + stoin_item 主从表)

方法路由功能
---------
ListGET /stoin/list?keyword=&status=入库单列表
DetailGET /stoin/detail?stoin_id=详情(主表 + 明细)
CreatePOST /stoin/create新增入库单(生成单号,联动更新库存台账)
UpdatePOST /stoin/update修改主表
DeletePOST /stoin/delete删除(已过账/被引用不可删,级联删明细)
ItemList / ItemCreate / ItemUpdate / ItemDelete/stoin/item/*明细 CRUD

Stout — 销售出库(stout_main + stout_item 主从表)

方法路由功能
---------
ListGET /stout/list?keyword=&status=出库单列表
DetailGET /stout/detail?stout_id=详情(主表 + 明细,明细带订单单价 order_price)
CreatePOST /stout/create新增出库单(生成单号,联动扣减库存)
UpdatePOST /stout/update修改主表
DeletePOST /stout/delete删除(已过账不可删,级联删明细)
ItemList / ItemCreate / ItemUpdate / ItemDelete/stout/item/*明细 CRUD

Stock — 库存(stock_main 台账 + stock_log 流水)

方法路由功能
---------
ListGET /stock/list?keyword=&expire_status=&stock_status=台账列表(货品+批次唯一)
CreatePOST /stock/create新增台账
UpdatePOST /stock/update修改台账
DeletePOST /stock/delete删除台账(不删流水)
ItemListGET /stock/log/list?stock_id=&pduc_id=&stock_type=&ref_type=流水列表(只增不改不删)
ItemCreatePOST /stock/log/create手工盘点调整(stock_type=5,事务联动更新台账)

3.4 试剂盒域

Kit — 试剂盒(kit_main + kit_locus + kit_manu)

方法路由功能
---------
ListGET /kit/list?keyword=&sys_type=&status=试剂盒列表
DetailGET /kit/detail?kit_id=详情(主表 + 位点 + 厂家)
CreatePOST /kit/create新增试剂盒(型号唯一)
UpdatePOST /kit/update修改主表
DeletePOST /kit/delete删除(被订单引用不可删,级联删位点/厂家)
LocusList / LocusCreate / LocusUpdate / LocusDelete/kit/locus/*试剂盒位点 CRUD
ManuList / ManuCreate / ManuUpdate / ManuDelete/kit/manu/*竞品厂家 CRUD

四、数据库 26 张表速览

分类表用途
---------
系统sys_user用户
系统sys_role角色(role_auth 存权限字符串)
系统sys_token登录 token(只存 SHA256 哈希)
系统sys_dept / sys_pstn部门 / 岗位
系统sys_dict数据字典(全局枚举,供各业务状态/类型字段翻译)
基础base_prvc / base_city省份 / 区域
基础base_locus位点(基因座)标准库
基础base_exps物流公司
基础base_supp / base_pduc供应商档案 / 产品档案
业务tender_main / tender_item招标项目主表 / 明细
业务kit_main / kit_locus / kit_manu试剂盒 / 试剂盒基因座 / 竞品厂家
业务order_main / order_item / base_client订单主表 / 明细 / 客户档案
业务stock_main / stock_log库存台账 / 库存流水
业务stoin_main / stoin_item采购入库主表 / 明细
业务stout_main / stout_item销售出库主表 / 明细
26 张表全部已有对应 API(19 个控制器全覆盖),表结构可直接由对应接口维护。

五、接口 URL 速查(抄 URL 用)

控制器接口 URL说明
---------
AuthPOST /auth/login · POST /auth/logout · GET /auth/info · POST /auth/repwd认证
User/user/list · /user/create · /user/update · /user/delete用户管理
Role/role/list · /role/create · /role/update · /role/delete角色
Dept/dept/list · /dept/create · /dept/update · /dept/delete部门(树形)
Pstn/pstn/list · /pstn/create · /pstn/update · /pstn/delete岗位
Perm/perm/list · /perm/sync · /perm/update操作权限点(岗位维度)
Dict/dict/list · /dict/create · /dict/update · /dict/delete系统字典
Prvc/prvc/list · /prvc/user · /prvc/create · /prvc/update · /prvc/delete省份 + 负责人
City/city/list · /city/user · /city/create · /city/update · /city/delete区域 + 负责人
Exps/exps/list · /exps/create · /exps/update · /exps/delete物流公司
Supp/supp/list · /supp/create · /supp/update · /supp/delete供应商
Pduc/pduc/list · /pduc/create · /pduc/update · /pduc/delete产品档案
Locus/locus/list · /locus/create · /locus/update · /locus/delete位点(基因座)
Client/client/list · /client/create · /client/update · /client/delete客户档案
Tender/tender/list · /tender/detail · /tender/create · /tender/update · /tender/delete + /tender/item/list · /tender/item/create · /tender/item/update · /tender/item/delete招标主从表
Order/order/list · /order/detail · /order/create · /order/update · /order/delete + /order/item/*订单主从表
Stoin/stoin/list · /stoin/detail · /stoin/create · /stoin/update · /stoin/delete + /stoin/item/*采购入库主从表
Stout/stout/list · /stout/detail · /stout/create · /stout/update · /stout/delete + /stout/item/*销售出库主从表
Stock/stock/list · /stock/create · /stock/update · /stock/delete + /stock/log/list · /stock/log/create库存台账 + 流水
Kit/kit/list · /kit/detail · /kit/create · /kit/update · /kit/delete + /kit/locus/* · /kit/manu/*试剂盒 + 位点 + 厂家

六、关键业务设计

  1. 主从表模式:Tender / Order / Stoin / Stout / Kit 都是"主表 + 明细"两个表,合并在一个控制器里,接口是 /模块 和 /模块/item/* 两组。
  2. 单号自动生成:业务单据(订单/入库/出库)新增时自动生成单号。
  3. 删除保护:删除时先查引用(被下游单据引用不可删);生产环境还有 DeleteGuard 兜底,所有 /delete 接口直接 403。
  4. 审计日志:增删改操作都写 apiLog,并记录 created_by(当前登录用户)。
  5. 库存流水只增不改:库存变动(入库/出库/盘点)全部记入 stock_log 流水,台账由流水联动,保证可追溯。
  6. 唯一约束:省份名、同省内区域名、产品编码、用户用户名、租户 tenant_code/web_domain/db_name 等都有唯一校验。
  7. 外部键校验:所有外键字段(client_id、supp_id、prvc_id 等)更新前先查目标存在。

七、前端怎么调 API

前端是纯 HTML + JS(无框架),API 客户端在 erp.mpkerp.com/assets/lib/mpk.lib.min.js:

const API_BASE = 'https://api.mpkerp.com';   // 独立 API 服务器地址

apiRequest('tender/list', { method: 'GET' });          // GET 自动带 token
apiRequest('auth/login', { method: 'POST', body: {...} }); // POST 传对象自动 JSON
apiToken.get() / apiToken.set(token) / apiToken.clear();   // token 存 localStorage
getCurrentUserCache();                                      // 获取当前用户(有缓存)

要点:

  • 登录成功后把 data.token 存下来(apiToken.set()),之后所有请求自动带 Authorization: Bearer {token}。
  • 后端返回 code != 200 时,前端自动弹出 msg;401 自动跳登录页。
  • 手机端 / 桌面端(无浏览器环境)不受跨域影响,逻辑一致。

第五部分 · 日常开发 SOP(以后怎么弄)

照着模板抄,改改表名和字段就行。所有新代码都要保持全站风格:统一返回、参数校验、审计日志。

一、加一个"列表 + 详情"接口(最简单)

场景:给已有表 foo_main 加列表接口。五步:

① 建控制器 app/controller/Foo.php:

namespace app\controller;

use app\BaseController;
use think\facade\Db;
use think\Response;

class Foo extends BaseController
{
    // GET /foo/list?keyword=
    public function list(): Response
    {
        $input = $this->request->get();
        $query = Db::name('foo_main')->alias('f');

        if (!empty($input['keyword'])) {
            $query->whereLike('f.foo_name', '%' . trim($input['keyword']) . '%');
        }

        $list = $query->order('f.foo_id', 'desc')->select()->toArray();
        return $this->success($list, '获取成功');
    }
}

② 加路由 route/app.php(登录分组里):

Route::group('api', function () {
    // ... 现有路由
    Route::get('foo/list', 'foo/list');
})->middleware(Auth::class);

③ 验证:带 token 调 GET /foo/list,看返回。

④ 写日志:只读接口可不写;增删改必须 apiLog()。

⑤ 加详情(如果有):GET /foo/detail?foo_id=,主从表就再拼明细。

二、加一组 CRUD(增删改查)

新建 = 校验(apiCheckRequired)→ 查重(唯一字段)→ insert → 返回新 ID。

修改 = 先 find 确认存在 → 查重(排除自己)→ update → 返回。

删除 = 先查引用(有下游引用就拒绝)→ delete → 返回。

参考现成模板:app/controller/Exps.php(最简单的纯 CRUD)、app/controller/Locus.php(带枚举白名单校验)。

三、加一张新表(完整流程)

  1. SQL 建表:统一 utf8mb4,带 COMMENT 注释,主键 xxx_id 自增,加 created_by / created_at / updated_at。
  2. 导入测试库:mysql -u root -p < 新表.sql。
  3. 写控制器:按第五部分·一 / 第五部分·二模板。
  4. 加路由:route/app.php。
  5. 需要下拉选项就加字典:往 sys_dict 插入对应类型(dict/list?type=xxx 前端直接拿)。
  6. 同步备份:把新表 SQL 合并进 erp.mpkerp.db/erp_mpkerp_com.sql(保持备份库是模板库)。
  7. 更新本手册:第四部分·四 表速览、第四部分·三 控制器导览、第四部分·五 URL 速查。

四、接入新租户(Checklist,可复用)

  • [ ] 1. 复制模板库:CREATE DATABASE erp_新租户code_com ... → 导入 erp_mpkerp_com.sql
  • [ ] 2. 建独立账号并授权(只授权该库)
  • [ ] 3. 向 platform_tenant 插入一行(tenant_code / db_* / web_domain 均唯一)
  • [ ] 4. 清空 sys_token(旧 token 在新库不存在,直接失效)
  • [ ] 5. 重置管理员密码(或保留 md5 值迁移)
  • [ ] 6. API 代码无需改动,TenantService 按 tenant_code 自动切库
  • [ ] 7. 前端 assets/lib/mpk.lib.min.js 的 API_BASE 指向 https://api.mpkerp.com
  • [ ] 8. 登录传 tenant_code → 验证列表数据落在新库

五、常见坑(重点背下来)

5.1 数据库

  • 编码统一 utf8mb4,排序规则统一,避免乱码与 JOIN 报错。
  • tenant_code / web_domain / db_name 三个唯一索引别丢(平台库路由依赖)。
  • .env 绝不提交 git(含数据库密码);用 .example.env 当模板。
  • 每租户独立账号、最小权限;平台库账号生产建议只读。

5.2 API / 框架

  • 改路由/控制器不生效 → 清缓存:php think clear(TP 会缓存路由)。
  • Db::name() 查到错库 → 一定是租户没切对:检查 token 前缀、switchDb 是否执行。
  • Config::set() 第二参数不支持点号多级(如 database.connections.mysql.database),

必须整体读数组、修改后整体写回(TenantService::switchDb 已示范)。

  • 跨域由 Cors 中间件全局处理,一般不用动。

5.3 安全

  • 登录失败统一提示"用户名或密码错误",防用户名枚举。
  • Token 库中只存 SHA256,明文只在客户端和内存。
  • 每个租户独立库 → A 租户 token 在 B 库查不到,天然隔离。
  • 密码目前是 md5(历史兼容),新系统建议逐步换 password_hash() 并平滑过渡。

第六部分 · 部署与运维(当前:一台服务器)

一、当前部署拓扑

┌─────────────────────────────────────────────────────────────────┐
│                                                                 │
│   前端 erp.mpkerp.com             API api.mpkerp.com            │
│ (HTML/JS/PHP 静态页) ←→ (ThinkPHP 8 + Nginx/Apache)         │
│           │                               │                     │
│           └──────────── HTTPS ────────────┘                     │
│                                           ▼                     │
│                                  MySQL(同一台机器)            │
│                     ├── 平台库 api_mpkerp_com                   │
│                     └── 租户库 erp_mpkerp_com(及其他租户库)   │
└─────────────────────────────────────────────────────────────────┘

要点:

  • API、前端、MySQL 都在一台服务器。
  • 前端不碰数据库,只通过 HTTPS 调 API(跨域已开,域名为 *.mpkerp.com)。
  • API 通过两套连接访问数据库:平台库(.env 的 PLATFORM_DB_*)+ 租户库(.env 兜底 + platform_tenant 表逐租户覆盖)。

二、上线步骤(新服务器 / 重装)

  1. 准备环境:PHP 8.0+(开 pdo_mysql/mbstring/openssl)、MySQL 8、Nginx/Apache、Composer。
  2. 放代码:api.mpkerp.com/ 上传到服务器,composer install --no-dev(生产不要 dev 依赖)。
  3. 配置 .env:APP_DEBUG=false,填真实数据库账号(见第二部分·三)。
  4. 导入数据库:erp.mpkerp.db/ 两个 sql 导入。
  5. 配置 Web 服务器:网站根目录指向 public/,配置伪静态(TP8 标准 rewrite 规则)。
  6. 验证:

- POST /auth/login 返回 token;

- 带 token 访问 /tender/list 正常;

- 跨域正常(前端能调通)。

  1. 清缓存:php think clear(首次部署必做)。

三、生产安全清单

  • [ ] APP_DEBUG=false(不然错误堆栈会暴露数据库结构)
  • [ ] .env 不在 git 仓库里、文件权限收紧(如 600)
  • [ ] 数据库账号最小权限:

- 租户库:每租户独立账号,只授权自己的库;

- 平台库:独立账号,生产建议只读 platform_tenant 的 SELECT;

- 绝不给 API 账号 SUPER / GRANT 权限。

  • [ ] DeleteGuard 在生产自动拦截所有 /delete 接口(无需额外配置,靠 APP_DEBUG=false 触发)
  • [ ] 定期备份(见第六部分·四)
  • [ ] 登录失败统一提示,防用户名枚举

四、日常备份与日志

备份(建议每日):

# 平台库
mysqldump -u api_mpkerp_com -p api_mpkerp_com --default-character-set=utf8mb4 > api_mpkerp_com_备份.sql
# 租户库
mysqldump -u erp_mpkerp_com -p erp_mpkerp_com --default-character-set=utf8mb4 > erp_mpkerp_com_备份.sql

查日志:

  • 应用日志:runtime/log/,按天分文件(apiLog() 写的都在这里)。
  • Web 服务器错误:Nginx/Apache 的 error log。
  • 数据库慢查询:MySQL slow query log。

第七部分 · 数据库迁移方案(将来)

当前 API、前端、MySQL 都在一台服务器。将来系统变大,最常见的需求就是把 MySQL 搬到独立数据库服务器 / 云 RDS。
好消息:本项目的多租户设计天生支持数据库分布在任意主机——每个租户的连接信息都存在 platform_tenant 表里(含 db_host / db_port),平台库连接在 .env 里。迁移只需要改这两处"指向",代码一行不用动。

一、什么时候需要迁移

场景判断信号
------
数据库服务器太慢 / CPU 打满业务高峰期 API 响应明显变慢
磁盘快满数据库文件占用越来越大
需要高可用 / 自动备份想上云 RDS、主从、快照备份
安全合规数据库要求与 Web 分离、内网访问

二、核心原理:两套连接怎么指

API 进程启动时读 .env:
  ┌─ PLATFORM_DB_* → 平台库(api_mpkerp_com,存 platform_tenant 注册表)
  └─ DB_*          → 默认兜底租户库(实际每个租户的连接信息在 platform_tenant 表里)

登录/请求时 TenantService:
  1. 平台库(按 .env 的 PLATFORM_DB_* 连接)查 platform_tenant
  2. 取出该租户的 db_host/db_port/db_user/db_pass/db_name
  3. switchDb() 把默认连接改指向租户库 → 后续查询落到租户库

所以"数据库搬到哪里"= 改两个地方:.env(平台库)+ platform_tenant 表(每个租户库)。迁移后 API 代码零改动。

三、场景 A:同一台机器换 MySQL 实例 / 端口 / 账号

最轻量的场景(不搬机器,只换库实例或账号):

  1. 在新 MySQL 实例上建库并导入(步骤见第七部分·四的备份/导入)。
  2. 改 .env:DB_* 和 PLATFORM_DB_* 指向新实例 / 新账号。
  3. 若 platform_tenant 表里各租户的 db_host / db_port / db_user / db_pass 与原来不同,也要 UPDATE 该表。
  4. php think clear → 验证登录与列表。

四、场景 B:数据库搬到独立服务器 / 云 RDS(重点)

4.1 前置准备

  • 新数据库服务器安装 MySQL 8,开放 API 服务器 IP 的白名单(云 RDS 在安全组/白名单里加 API 服务器公网 IP)。
  • 确认 API 服务器到新库的网络连通(telnet 新库IP 3306)。

4.2 迁移步骤

① 全量备份(在旧服务器)

mysqldump -u root -p --all-databases --default-character-set=utf8mb4 --single-transaction > 全量备份.sql
# 或按库分别导:
mysqldump -u api_mpkerp_com -p api_mpkerp_com --default-character-set=utf8mb4 > api_mpkerp_com.sql
mysqldump -u erp_mpkerp_com  -p erp_mpkerp_com  --default-character-set=utf8mb4 > erp_mpkerp_com.sql

② 在目标服务器建库导入

mysql -u root -p < api_mpkerp_com.sql
mysql -u root -p < erp_mpkerp_com.sql
# 多租户就重复导入每个租户库

③ 建账号并授权(在目标服务器,账号规范见第六部分·三)

-- 平台库账号
CREATE USER 'api_mpkerp_com'@'API服务器IP' IDENTIFIED BY '密码';
GRANT SELECT ON api_mpkerp_com.* TO 'api_mpkerp_com'@'API服务器IP';

-- 每个租户库账号
CREATE USER 'erp_mpkerp_com'@'API服务器IP' IDENTIFIED BY '密码';
GRANT ALL ON erp_mpkerp_com.* TO 'erp_mpkerp_com'@'API服务器IP';

④ 改 API 服务器的 .env(平台库指向新服务器)

PLATFORM_DB_HOST=新数据库服务器IP
PLATFORM_DB_PORT=3306
PLATFORM_DB_USER=api_mpkerp_com
PLATFORM_DB_PASS=新密码

⑤ 改 platform_tenant 表(每个租户指向新服务器)

UPDATE platform_tenant
SET db_host = '新数据库服务器IP',
    db_port = 3306,
    db_user = 'erp_mpkerp_com',
    db_pass = '新密码'
WHERE tenant_code = 'erp';   -- 每个租户一行,都改
注意:platform_tenant 表在新库里(已随 ② 导入),直接在目标服务器上执行 UPDATE 即可。

⑥ 切换与验证

  1. php think clear 清缓存。
  2. 停旧库写入(或直接切,见第七部分·七 回滚)。
  3. 验证清单见第七部分·六。

4.3 迁移后架构

   API 服务器                 数据库服务器(独立 / 云 RDS)
┌────────────────┐          ┌──────────────────────────────┐
│ api.mpkerp.com │  ←────→  │ MySQL 8                      │
│ erp.mpkerp.com │  3306    │  ├── api_mpkerp_com(平台库)│
└────────────────┘          │  └── erp_mpkerp_com(租户库)│
                            └──────────────────────────────┘

五、场景 C:前端 / API 分离(可选)

如果将来前端流量大,想单独部署:

  1. 把 erp.mpkerp.com 目录复制到新前端服务器(纯静态 + PHP 轻页面)。
  2. 改 assets/lib/mpk.lib.min.js 里的 API_BASE 指向 API 域名。
  3. 改 API 的 Cors 配置允许新前端域名(见 config/cors.php)。
  4. 前端不碰数据库,无其他改动。

六、迁移后验证清单

  • [ ] POST /auth/login 返回 {user, role, token, tenant_code}
  • [ ] 带 Authorization: Bearer {token} 访问受保护接口正常
  • [ ] 多个租户分别登录,确认数据落在各自的库(跨库隔离仍生效)
  • [ ] 列表/详情/新增/修改都试一遍(覆盖主从表:如 tender/detail)
  • [ ] 网页端跨域正常、手机端无 Origin 也正常
  • [ ] runtime/log/ 正常写入日志
  • [ ] 平台库账号只读权限下,登录路由仍正常(只读 SELECT)

七、回滚方案

迁移失败随时可回滚(因为代码没动,只改了两处"指向"):

  1. .env 的 PLATFORM_DB_* 改回旧库。
  2. platform_tenant 的 db_host / db_* 改回旧值(或直接在旧库上改,因为旧库数据还在)。
  3. php think clear → 验证。
生产建议:迁移前先做全量备份(第七部分·四 ①),迁移后保留旧库至少一个备份周期再清理。

附录

A. 旧项目迁移历史(原散装 PHP → ThinkPHP 8 多租户)

这份是当初项目从旧代码迁移到新框架的历史记录,与第七部分"将来数据库迁移"不同。
留存备用:再次迁移其他旧项目 / 老接口时对照。

A.1 迁移背景

旧项目(erp.mpkerp.com 时代):

  • 接口都是独立 PHP 文件:api/get-all-tender-main.php、create-tender-main.php 等。
  • 公共函数在一个文件:api/api-common.php(jsonReturn、校验函数等)。
  • 原生 PDO + SQL 字符串拼接,直连单库,无框架、无路由、无统一鉴权。

新项目(api.mpkerp.com,ThinkPHP 8):

  • 统一入口 public/index.php,路由集中在 route/app.php。
  • 控制器继承 BaseController,中间件统一鉴权(Bearer Token)。
  • 多租户:平台库(注册表)+ 每租户一个独立业务库。
  • 统一返回 {code, msg, field, data}。

迁移三原则:

  1. URL 风格统一:/模块/动作。
  2. 返回格式兼容:{code, msg, field, data},旧前端尽量不改。
  3. Token 兼容:旧 token(纯 64 位 hex 无前缀)自动视为 erp 租户,老用户不重登。

A.2 旧接口 → 新接口对照(模板)

旧项目文件新路由控制器方法说明
------------
api/get-all-tender-main.phpGET /tender/listTender::list列表
api/create-tender-main.phpPOST /tender/createTender::create新增
api/update-tender-main.phpPOST /tender/updateTender::update修改
api/delete-tender-main.phpPOST /tender/deleteTender::delete删除
api/api-common.phpapp/common.phpjsonReturn / apiLog / apiCheck*公共函数
各文件里的登录校验app/middleware/Auth.php—统一鉴权

A.3 迁移五步套路(把旧接口搬到新框架)

  1. 建控制器:app/controller/Xxx.php,命名空间 app\controller,继承 BaseController。
  2. 加路由:route/app.php;需要登录的放 Auth 中间件分组。
  3. 搬逻辑:原生 PDO + SQL 拼接 → Db::name('表') 查询构造器;exit/json → 返回值。
  4. 加校验:apiCheckRequired / apiCheckInt / apiCheckDecimal / apiCheckDate。
  5. 统一返回:return $this->success($data, '获取成功');;出错 return $this->error('提示', 400, 'field');。

A.4 关键兼容点(迁移时最容易踩)

  • 返回结构:{code, msg, field, data};jsonReturn() 通过抛 HttpResponseException 立即中断。
  • Token:新格式明文 {tenant_code}:{64位hex};库中只存 SHA256。旧格式(纯 64 位 hex 无冒号)→ 视为 erp 租户。
  • 密码:sys_user.password 存 md5(旧数据直接兼容,未做二次迁移)。
  • 登录:tenant_code 缺省时自动用 LEGACY_TENANT_CODE(erp),旧前端不用传。
  • 鉴权:旧项目每个文件自己查 token → 新项目中间件统一做,通过后挂载 $request->authUser / authToken / authTenant。

B. 学习资料

  • ThinkPHP 8 官方文档:https://doc.thinkphp.cn/v8_0/
  • 本项目必读文件(按顺序):

1. route/app.php —— 看接口全貌

2. app/controller/Auth.php —— 看登录

3. app/middleware/Auth.php —— 看鉴权

4. app/service/TenantService.php —— 看多租户核心

5. config/database.php —— 看两个连接

C. 版本与维护记录

日期内容
------
2026-08手册按"项目介绍 → 搭建 → 框架速成 → 代码导览 → 开发 SOP → 部署运维 → 数据库迁移 → 附录"重排