CRUD 开发指南
本文以租户后台新增「产品管理」为例,说明普通业务模块的开发流程。插件应用建议优先阅读 插件开发指南。
1. 创建数据表
租户级业务表必须包含 tenant_id,否则无法进入自动租户隔离。
sql
CREATE TABLE `products` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`tenant_id` int unsigned NOT NULL DEFAULT 0 COMMENT '租户ID',
`name` varchar(100) NOT NULL COMMENT '产品名称',
`category_id` int unsigned DEFAULT 0 COMMENT '分类ID',
`price` decimal(10,2) DEFAULT 0 COMMENT '价格',
`cover` varchar(255) DEFAULT '' COMMENT '封面图',
`description` text COMMENT '描述',
`status` tinyint DEFAULT 1 COMMENT '状态 1启用 0禁用',
`sort` int DEFAULT 0 COMMENT '排序',
`created_at` datetime DEFAULT NULL,
`updated_at` datetime DEFAULT NULL,
`deleted_at` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_tenant_id` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='产品表';WARNING
Repository 查询必须使用 $this->query(),不能直接用 $this->model 起查询。query() 会自动注入当前租户的 tenant_id 条件。
2. 生成 CRUD 代码
bash
cd server
php think make:crud products --module=product --model=Product自动生成文件:
后端
app/model/product/Product.phpapp/repository/product/ProductRepository.phpapp/service/product/ProductService.phpapp/tenantapi/controller/v1/product/ProductController.phpapp/tenantapi/validate/v1/product/ProductValidate.phpapp/tenantapi/route/product.php
前端
当前代码生成器的前端输出路径仍保留旧后台目录约定,这是历史兼容行为。SaaS 项目中应将生成结果迁移到 tenant/src/...,或在生成器模板完成迁移后直接生成到 tenant/。
tenant/src/api/product.tstenant/src/views/product/product/index.vuetenant/src/views/product/product/components/ProductForm.vue
3. 注册路由
检查 app/tenantapi/route/product.php 是否存在。ThinkPHP 多应用模式会自动加载 route/ 目录下的路由文件。
生成的租户端接口通常形如:
text
GET /tenantapi/product/product
GET /tenantapi/product/product/:id
POST /tenantapi/product/product
PUT /tenantapi/product/product/:id
DELETE /tenantapi/product/product/:id具体以生成的路由文件为准。
4. 添加菜单和权限
在租户后台「系统管理 → 菜单管理」中添加:
- 目录菜单:产品管理。
- 页面菜单:产品列表,组件路径指向生成的页面。
- 按钮权限:新增、编辑、删除、状态切换等。
权限标识要和 Controller 中的 #[Permission(...)] 保持一致。
5. 自定义查询
在 Repository 中使用 $this->query():
php
public function getSearchList(array $params, int $page = 1, int $size = 20): array
{
$query = $this->query();
if (!empty($params['keyword'])) {
$query->whereLike('name', "%{$params['keyword']}%");
}
if (isset($params['status']) && $params['status'] !== '') {
$query->where('status', (int) $params['status']);
}
if (!empty($params['category_id'])) {
$query->where('category_id', (int) $params['category_id']);
}
return $query->order('sort', 'asc')
->order('id', 'desc')
->paginate(['page' => $page, 'list_rows' => $size])
->toArray();
}6. 前端 API
租户后台 API 使用 /tenantapi 前缀:
ts
import { myRequest } from '@/utils/request'
export const productApi = {
list: (params: any) => myRequest.get('/tenantapi/product/product', { params }),
show: (id: number) => myRequest.get(`/tenantapi/product/product/${id}`),
create: (data: any) => myRequest.post('/tenantapi/product/product', data),
update: (id: number, data: any) => myRequest.put(`/tenantapi/product/product/${id}`, data),
destroy: (id: number) => myRequest.delete(`/tenantapi/product/product/${id}`),
}7. 常见扩展
文件上传字段
前端表单使用上传组件:
vue
<Upload v-model="formData.cover" type="image" />上传前后端会走租户存储配额校验,超限时返回 403 或业务错误。
字典值关联
vue
<DictValue :options="dictOptions.product_type" :value="row.type" />ts
const { optionsData: dictOptions } = useDictOptions<{
product_type: any[]
}>(['product_type'])树形数据
树形数据仍然要基于 $this->query() 取列表,再做树转换:
php
public function getTree(): array
{
$list = $this->query()
->order('sort', 'asc')
->select()
->toArray();
return $this->listToTree($list);
}8. 验证
- 表包含
tenant_id并建立索引。 - Repository 查询使用
$this->query()。 - 路由在
tenantapi下可访问。 - 菜单和按钮权限已分配到角色。
- 使用两个租户分别创建数据,确认列表不会互相看到。
