关键在于域名级索引命名、es客户端单例按域隔离、mapping独立维护、搜索结果必须回查mysql并校验租户上下文,严禁共用索引或绕过域名约束。

ThinkPHP 6 做二级域名 + Elasticsearch,关键不在“能不能连上”,而在“不同域名的数据和索引能不能真正隔开”。否则容易出现:admin.example.com 搜到 user.example.com 的草稿、API 域名写入的文档被前台域名查出来、清缓存时误删其他租户的搜索索引——这些都不是配置没通,而是隔离没做实。
域名级索引命名必须显式绑定
Elasticsearch 本身不认域名,所有索引名都由你定义。若所有子域名共用一个 index(比如 articles),那数据就天然混在一起。必须把域名信息作为索引名的一部分,且在业务层强制约束。
- 推荐格式:{domain}_{type}_{env},例如
admin_example_com_posts_prod、shop_example_com_products_prod - 不要靠运行时拼接字符串,应在模型或搜索服务类中封装方法:
getIndexName(),内部取request()->host()并标准化(替换点号为下划线、转小写) - 禁止在控制器里手写
client->search(['index' => 'articles']),必须走封装层,避免遗漏
ES 客户端连接需按域名复用,而非新建
每个请求都 new 一次 Elasticsearch\Client,不仅性能差,更会导致连接池混乱、认证头错乱、甚至跨域名复用连接——尤其在 Swoole 或多应用常驻场景下风险极高。
- 用 ThinkPHP 容器注册单例,key 名带域名标识,例如:
elasticsearch.admin_example_com和elasticsearch.shop_example_com - 在中间件(如 CheckTenant)识别完域名后,动态 bind 对应客户端实例:
App::bind('es_client', $container->get('elasticsearch.' . $tenantDomain)) - 确保
shared => true,并验证每次app()->make('es_client')返回的是同一对象(可用 spl_object_id 判断)
索引 mapping 必须按域名独立创建与维护
mapping 是索引的“身份证”,一旦建错,后期改字段类型代价极大。多域名环境下,不同业务对同一类数据(如“商品”)的字段定义可能不同:后台要 price_raw,前台要 price_display;管理员要 status_code,用户端只看 is_visible。
- 每个域名对应一套 mapping 文件,存放于
config/elasticsearch/mapping/{domain}/,例如admin_example_com/product.php - 索引创建统一走命令行任务:
php think es:setup --domain=admin.example.com --type=product,任务内读取对应 mapping 并调用 PUT /{index_name} + body - 上线前必须校验:
GET /{index_name}/_mapping,确认字段类型、analyzer、是否启用 norms 等与预期一致
搜索逻辑必须先路由再查询,不能绕过域名上下文
ES 只负责检索 ID,真实数据仍来自 MySQL。如果搜索结果直接返回 ES 文档,就等于把索引结构暴露给了前端,也失去了租户/权限控制能力。
- 标准流程:ES 检索 → 提取
_id或业务主键 → 走 Model::whereIn('id', [...]) 查 MySQL → 补全关联、权限过滤、字段脱敏 - 务必在 Model 查询前检查当前租户上下文(如
App::instance()->tenant),防止 admin 域名查出 user 域名的数据 - 禁用
Model::search(...)->get()这类封装(除非你完全掌控其内部逻辑),优先手写两步流程,可控性更强
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











