0%

EspoCRM定制第4篇 Dynamic Logic + BeforeSave Hook ——前后端双保险

适用版本:EspoCRM 9.2.2+

只做前端 required 的团队,迟早会在报表上被”脏数据”反噬。

TL;DR

  • Dynamic Logic 负责体验:用户当场知道”必须填”
  • BeforeSave Hook 负责底线:任何入口都无法绕过
  • 错误消息用可翻译 key 管理,默认英文输出便于排障
  • 测试必须覆盖 UI + API + 边界切换

1. 场景:为什么”只做前端必填”是自欺欺人

当业务规定”阶段 = Closed Lost 时必须填写失败原因”,很多人第一反应是:在前端把字段设成 required。

问题在于:前端不是唯一入口。

  • API PATCH/POST
  • 批量更新
  • 导入
  • 自动化脚本

这些路径都可能绕过前端。你最终会得到一堆”Closed Lost 但无失败原因”的脏数据,报表和复盘完全失真。

结论:体验靠前端,底线靠后端

2. 架构:双层校验

flowchart TD
  A[用户在UI切换 stage] --> B[Dynamic Logic: required]
  B -->|实时提示| C[用户填写字段]
  C --> D[保存]
  D --> E[BeforeSave Hook]
  E -->|ok| F[写入数据库]
  E -->|invalid| G[HTTP 400 Bad Request]

3. 前端:Dynamic Logic(实时必填)

Dynamic Logic 的价值:让用户”当场知道要填什么”,而不是保存时才被打回。

配置示例(公开版占位字段名):

  • 字段:cLostReasons
  • 条件:stage == "Closed Lost"
1
2
3
4
5
6
7
8
9
10
11
12
13
{
"dynamicLogic": {
"fields": {
"cLostReasons": {
"required": {
"conditionGroup": [
{ "type": "equals", "attribute": "stage", "value": "Closed Lost" }
]
}
}
}
}
}

4. 后端:BeforeSave Hook(强制校验,防绕过)

Hook 的价值:无论从哪里保存,规则都必须成立。

原则

  • 只做轻逻辑判断
  • 抛出明确错误(建议英文错误信息,便于排障)
  • 不做 HTTP、邮件、复杂计算

行为定义(伪代码):

1
2
if stage == "Closed Lost" and cLostReasons is empty:
throw BadRequest("Lost Reason is required when stage is set to Closed Lost")

代码骨架(公开版):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
namespace Espo\Modules\YourModule\Hooks\Opportunity;

use Espo\ORM\Entity;
use Espo\Core\Exceptions\BadRequest;

class BeforeSave
{
public function beforeSave(Entity $entity, array $options): void
{
if ($entity->get('stage') === 'Closed Lost'
&& !$entity->get('cLostReasons')) {
throw new BadRequest(
"Lost Reason is required when stage is set to Closed Lost"
);
}
}
}

5. 测试矩阵(别只点 UI)

最少要覆盖 3 类入口:

入口 测试内容 期望结果
UI 切换 stage → 保存 实时提示 + 拒绝保存
API PATCH /api/v1/Opportunity/{id} HTTP 400 + 错误信息
边界 从 Closed Lost 切回其它 stage 允许清空原因

API 测试示例(公开版占位符):

1
2
3
4
5
6
7
curl -X PATCH "https://crm.example.com/api/v1/Opportunity/OPP_ID" \
-H "Authorization: Bearer <ACCESS_TOKEN>" \
-H "Content-Type: application/json" \
-d '{"stage":"Closed Lost"}'

# 期望: HTTP 400
# 响应: {"error":"Lost Reason is required..."}

6. 你应该坚持的”坏人假设”

把自己当作攻击者/绕过者,永远问一句:

“如果我不用 UI,用 API 直接写入,会不会写出脏数据?”