Files

173 lines
6.2 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
// +----------------------------------------------------------------------
// | YwxApp [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Author: ywxapp<admin@ywxapp.cn>
// +----------------------------------------------------------------------
declare (strict_types = 1);
namespace addon\wxchat\service;
use think\facade\Log;
/**
* 真人认证 AI 智能审核服务
*
* 内置两类审核器(provider),通过插件配置 wxchat.ai_audit_provider 切换:
* 1) local —— 本地启发式:按资料完整度打分(脱敏存储,仅作初筛,不作为最终结论)
* 2) http —— 真实服务:调用可配置 HTTPS 接口(OCR / 活体人脸 / 反欺诈),
* 失败或超时时自动回退 local,保证审核链路不中断。
*
* 审核结果约定:
* ['score'=>int(0-100), 'passed'=>bool, 'result'=>int(1通过/2不通过), 'detail'=>string, 'provider'=>string]
*/
class AiAuditService
{
/**
* 读取 wxchat 插件配置(存于 wxapp_addon_config,非框架 Config 命名空间)
*/
private static function cfg(string $key, $default = null)
{
try {
$conf = \ywxapp\service\AddonService::config('wxchat');
if (! is_array($conf)) {
return $default;
}
return $conf[$key] ?? $default;
} catch (\Throwable $e) {
return $default;
}
}
/**
* 统一入口
*/
public static function audit(array $data): array
{
$provider = (string) self::cfg('ai_audit_provider', 'local');
if ($provider === 'http' && self::cfg('ai_audit_url')) {
try {
return self::httpProvider($data);
} catch (\Throwable $e) {
Log::warning('AI审核真实服务调用失败,已回退本地启发式:' . $e->getMessage());
}
}
return self::localProvider($data);
}
/**
* 本地启发式:资料完整度打分
*/
private static function localProvider(array $data): array
{
$score = 0;
if (! empty($data['front_img'])) {
$score += 30;
}
if (! empty($data['back_img'])) {
$score += 20;
}
if (! empty($data['hold_img'])) {
$score += 20;
}
if (! empty($data['face_img'])) {
$score += 30;
}
// 真实姓名 + 证件号齐全(非脱敏校验,仅判断完整性)
if (empty($data['real_name']) || empty($data['id_card'])) {
$score = max(0, $score - 10);
}
$passScore = (int) self::cfg('ai_audit_pass_score', 80);
$passed = $score >= $passScore;
return [
'score' => $score,
'passed' => $passed,
'result' => $passed ? 1 : 2,
'detail' => '本地启发式审核(资料完整度=' . $score . ',阈值=' . $passScore . '',
'provider' => 'local',
];
}
/**
* 真实服务:调用可配置 HTTPS 接口
* 期望响应(常见结构,兼容多种返回):
* { "code":1, "message":"...", "data": { "score": 92, "passed": true } }
* 或
* { "score": 92, "passed": true, "msg":"..." }
*/
private static function httpProvider(array $data): array
{
$url = self::cfg('ai_audit_url', '');
$appcode = self::cfg('ai_audit_appcode', '');
$timeout = (int) self::cfg('ai_audit_timeout', 8);
$client = new \GuzzleHttp\Client([
'base_uri' => $url,
'timeout' => $timeout,
'verify' => (bool) self::cfg('ai_audit_ssl_verify', false),
]);
$headers = ['Accept' => 'application/json'];
if ($appcode) {
// 阿里云市场等常见鉴权头;如服务商不同,可在配置中改用自定义头
$headers['Authorization'] = 'APPCODE ' . $appcode;
}
$token = self::cfg('ai_audit_token', '');
if ($token) {
$headers['X-Api-Token'] = $token;
}
$resp = $client->post('', [
'headers' => $headers,
'json' => [
'uid' => $data['uid'] ?? 0,
'real_name' => $data['real_name'] ?? '',
'id_card' => $data['id_card_raw'] ?? ($data['id_card'] ?? ''),
'front_img' => $data['front_img'] ?? '',
'back_img' => $data['back_img'] ?? '',
'hold_img' => $data['hold_img'] ?? '',
'face_img' => $data['face_img'] ?? '',
],
]);
$body = json_decode($resp->getBody()->getContents(), true);
if (! is_array($body)) {
throw new \RuntimeException('AI审核服务返回非JSON');
}
$inner = $body['data'] ?? $body;
$score = (int) ($inner['score'] ?? ($body['score'] ?? 0));
$passed = ! empty($inner['passed']) || ! empty($body['passed'])
|| $score >= (int) self::cfg('ai_audit_pass_score', 80);
$detail = $inner['message'] ?? ($inner['msg'] ?? ($body['message'] ?? ($body['msg'] ?? '真实服务返回')));
return [
'score' => $score,
'passed' => (bool) $passed,
'result' => $passed ? 1 : 2,
'detail' => is_string($detail) ? $detail : json_encode($detail, JSON_UNESCAPED_UNICODE),
'provider' => 'http',
];
}
/**
* 18 位居民身份证号校验(GB 11643-1999 校验位算法)
* 仅在校验「原始」证件号时调用(落库为脱敏存储,无法直接校验)。
*/
public static function isValidIdCard(string $id): bool
{
if (! preg_match('/^\d{17}[\dXx]$/', $id)) {
return false;
}
$weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
$codes = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'];
$sum = 0;
for ($i = 0; $i < 17; $i++) {
$sum += (int) $id[$i] * $weights[$i];
}
return $codes[$sum % 11] === strtoupper($id[17]);
}
}