本文目录导读:

这是一个很有挑战性的需求,要实现用户自定义反欺诈,核心在于将“决策逻辑”从“代码硬编码”中解放出来,交给用户通过后台界面进行配置。
这意味着你的PHP项目需要构建一个规则引擎,根据用户配置的规则实时计算风险分数。
下面是一套完整的实现方案,从架构设计到具体代码示例。
核心架构设计
采用 “黑白灰名单 + 规则矩阵 + 阈值评分” 的模型。
- 数据层:用户行为数据(IP、设备指纹、注册时间、下单频次等)。
- 配置层:用户自定义的规则列表(存储在数据库,例如MySQL或Redis)。
- 执行层:PHP规则引擎,加载用户配置,按优先级匹配规则,累加风险分数。
- 决策层:根据总分触发不同动作(放行、人工审核、拒绝、加入黑名单)。
graph TD
A[用户请求] --> B{规则引擎加载用户配置};
B --> C[遍历所有启用的自定义规则];
C --> D{条件匹配?};
D -- 是 --> E[累加风险分数/记录日志];
D -- 否 --> F[下一条规则];
E --> F;
F --> G{累计分数 > 阈值?};
G -- 是 --> H[执行动作: 拦截/审核/标记];
G -- 否 --> I[放行];
H --> J[记录到日志库];
数据库设计(用户可编辑)
你需要几张核心表来实现“自定义”:
表1:fraud_rules (规则定义表)
| 字段名 | 类型 | 说明 |
|---|---|---|
id |
INT (PK) | 规则ID |
user_id |
INT | 用户ID(实现多租户/自定义) |
rule_name |
VARCHAR | 规则名称(如:新注册用户高频下单) |
is_active |
TINYINT | 是否启用 |
priority |
INT | 优先级(数字越小越先执行) |
logic_connector |
ENUM(‘AND’,‘OR’) | 条件之间的连接词 |
表2:fraud_rule_conditions (规则条件表)
| 字段名 | 类型 | 说明 |
|---|---|---|
id |
INT (PK) | 条件ID |
rule_id |
INT | 关联规则ID |
field |
VARCHAR | 字段名(如:ip_country、user_agent、order_amount、register_minutes) |
operator |
ENUM(‘eq’,‘neq’,‘gt’,‘lt’,‘in’,‘not_in’,‘regex’) | 操作符 |
value |
TEXT | 比较值(如:CN; 100; Mozilla/5.0) |
表3:fraud_rule_actions (规则结果表)
| 字段名 | 类型 | 说明 |
|---|---|---|
id |
INT (PK) | 动作ID |
rule_id |
INT | 关联规则ID |
action_type |
ENUM(‘increase_score’, ‘block’, ‘tag’, ‘notify’) | 动作类型 |
action_value |
INT/TEXT | 分数值/标签名等 |
PHP核心代码实现(规则引擎)
这是最关键的模块,我们需要一个灵活的引擎来执行这些用户配置。
<?php
class FraudRuleEngine
{
private $db; // 数据库连接
private $userId;
private $requestData; // 当前请求的所有特征数据
public function __construct($db, $userId, array $requestData)
{
$this->db = $db;
$this->userId = $userId;
$this->requestData = $requestData;
}
/**
* 执行所有启用的规则,计算风险分数
* @return array ['score'=> 总分, 'matched_rules'=> [], 'actions'=> []]
*/
public function execute()
{
$totalScore = 0;
$matchedRules = [];
$finalActions = [];
// 1. 获取该用户所有的有效规则
$rules = $this->getActiveRules();
if (empty($rules)) {
return ['score' => 0, 'matched_rules' => [], 'actions' => []];
}
// 2. 遍历规则
foreach ($rules as $rule) {
$conditionsMet = $this->evaluateConditions($rule['id'], $rule['logic_connector']);
if ($conditionsMet) {
// 规则命中
$matchedRules[] = $rule['rule_name'];
$actions = $this->getRuleActions($rule['id']);
foreach ($actions as $action) {
if ($action['action_type'] === 'increase_score') {
$totalScore += (int)$action['action_value'];
} else {
// 如果是block, tag等,直接记录下来
$finalActions[] = $action;
}
}
}
}
return [
'score' => $totalScore,
'matched_rules' => $matchedRules,
'actions' => $finalActions
];
}
/**
* 评估单个规则的所有条件
*/
private function evaluateConditions(int $ruleId, string $connector): bool
{
$conditions = $this->getRuleConditions($ruleId);
if (empty($conditions)) {
// 没有条件的规则默认不触发
return false;
}
$results = [];
foreach ($conditions as $cond) {
$fieldValue = $this->requestData[$cond['field']] ?? null;
$result = $this->compare($fieldValue, $cond['operator'], $cond['value']);
$results[] = $result;
}
// 根据连接词(AND/OR)组合结果
if ($connector === 'AND') {
return !in_array(false, $results, true);
} else { // OR
return in_array(true, $results, true);
}
}
/**
* 基础比较器,支持多种操作符
*/
private function compare($fieldValue, string $operator, string $ruleValue): bool
{
switch ($operator) {
case 'eq':
return $fieldValue == $ruleValue;
case 'neq':
return $fieldValue != $ruleValue;
case 'gt':
return is_numeric($fieldValue) && $fieldValue > (float)$ruleValue;
case 'lt':
return is_numeric($fieldValue) && $fieldValue < (float)$ruleValue;
case 'in':
$values = explode(',', $ruleValue);
return in_array($fieldValue, $values);
case 'not_in':
$values = explode(',', $ruleValue);
return !in_array($fieldValue, $values);
case 'regex':
return preg_match('/' . preg_quote($ruleValue, '/') . '/', $fieldValue) === 1;
default:
return false;
}
}
// ---- 数据库查询方法(示例) ----
private function getActiveRules(): array
{
$stmt = $this->db->prepare("SELECT * FROM fraud_rules WHERE user_id = ? AND is_active = 1 ORDER BY priority ASC");
$stmt->execute([$this->userId]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
private function getRuleConditions(int $ruleId): array
{
$stmt = $this->db->prepare("SELECT * FROM fraud_rule_conditions WHERE rule_id = ?");
$stmt->execute([$ruleId]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
private function getRuleActions(int $ruleId): array
{
$stmt = $this->db->prepare("SELECT * FROM fraud_rule_actions WHERE rule_id = ?");
$stmt->execute([$ruleId]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
}
用户界面(如何配置?)
用户需要在后台看到这样一个配置面板:
基本信息
- 规则名称:
新注册用户大额订单 - 优先级:
1
条件配置(条件A AND 条件B)
| 字段 | 操作符 | 值 |
| :--- | :--- | :--- |
| register_minutes(注册时长) | < | 30 |
| order_amount(订单金额) | > | 5000 |
触发动作
increase_score->+50- 或直接
block(屏蔽)
生效范围
- 选择给哪些应用、哪些用户组生效。
关键优化与注意事项
-
性能陷阱:每次请求都从数据库拉取规则并循环,会造成巨大开销。
- 解决方案:使用 Redis 缓存用户的规则配置。
- 在保存规则时,主动清除该用户对应的Redis缓存。
- 引擎首先尝试从Redis获取规则。
-
字段扩展性:用户能选择的
field必须是你提前枚举好的。- 在后台管理下拉菜单中,动态提供选项(如
ip_country,user_agent,register_time,total_orders等)。 - 引擎的
requestData必须提前从请求中提取并填充好这些字段。
- 在后台管理下拉菜单中,动态提供选项(如
-
上下文变量:
- 有些规则不是简单的字段比较,过去1小时失败登录次数 > 3”。
- 这种情况下,需要在
requestData中预先计算好failed_logins_1h值,作为引擎可用的一个字段。
-
规则分租户:
- 如果你的项目是SaaS,每个商户的规则不同,务必使用
user_id或tenant_id隔离数据。
- 如果你的项目是SaaS,每个商户的规则不同,务必使用
-
决策日志:
- 每一次规则命中、执行的决定,一定要记录日志。
- 这在生产环境中排查误杀或漏杀至关重要。
你的实现步骤
- 确定可用字段:列出所有你能采集到的用户/行为数据(IP、设备、浏览器、金额、频率等)。
- 建表:创建
fraud_rules、fraud_rule_conditions、fraud_rule_actions。 - 编写引擎:复制或重构上面的
FraudRuleEngine类。 - 配置文件:在后台创建一个页面,允许用户通过拖拽或下拉菜单组合条件,存储到数据库。
- 集成:在你项目的核心入口(如下单、登录请求处)调用
engine->execute()。 - 测试:用不同规则反复测试,确保
AND/OR逻辑正确,评分准确。
这套方案给了用户极大的灵活性,同时将复杂的规则判断逻辑封装在引擎中,是大多数企业级反欺诈系统的标准做法。