chore: 重写初始提交(清空历史,整理后全量提交)
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
<?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]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* 内容安全服务:敏感词过滤 + 风险评分
|
||||
* 敏感词存放于 wxapp_wxchat_banwords,缺失时使用内置兜底词库
|
||||
*/
|
||||
class SafetyService
|
||||
{
|
||||
private static $instance = null;
|
||||
|
||||
/** @var array|null [['word'=>, 'level'=>, 'replace'=>], ...] */
|
||||
private $words = null;
|
||||
|
||||
|
||||
public static function instance(): self
|
||||
{
|
||||
if (self::$instance === null) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载敏感词(带缓存 + 兜底)
|
||||
*/
|
||||
private function loadWords(): array
|
||||
{
|
||||
if ($this->words !== null) {
|
||||
return $this->words;
|
||||
}
|
||||
$this->words = [];
|
||||
try {
|
||||
$rows = \think\facade\Db::name('wxchat_banwords')
|
||||
->where('status', 1)
|
||||
->order('level', 'desc')
|
||||
->select();
|
||||
foreach ($rows as $r) {
|
||||
$this->words[] = [
|
||||
'word' => $r['word'],
|
||||
'level' => (int)$r['level'],
|
||||
'replace' => $r['replace_char'] ?: '**',
|
||||
];
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// 表可能未创建,忽略,使用兜底词库
|
||||
}
|
||||
// 兜底词库
|
||||
foreach (['fuck', 'shit', '赌博', '色情', '诈骗', '代开发票'] as $w) {
|
||||
$this->words[] = ['word' => $w, 'level' => 1, 'replace' => '**'];
|
||||
}
|
||||
return $this->words;
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤敏感词(level=1 替换为 **;level=2 命中则整段清空并返回命中标记)
|
||||
* @return string 过滤后的文本
|
||||
*/
|
||||
public function filter(string $text): string
|
||||
{
|
||||
if ($text === '') {
|
||||
return $text;
|
||||
}
|
||||
foreach ($this->loadWords() as $w) {
|
||||
if ($w['level'] >= 2) {
|
||||
if (mb_stripos($text, $w['word']) !== false) {
|
||||
// 高危词:整段清空(拦截)
|
||||
return '';
|
||||
}
|
||||
} else {
|
||||
$text = $this->mbStrReplace($w['word'], $w['replace'], $text);
|
||||
}
|
||||
}
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* 内容安全检测:返回是否命中高危词
|
||||
*/
|
||||
public function check(string $text): array
|
||||
{
|
||||
$hit = [];
|
||||
foreach ($this->loadWords() as $w) {
|
||||
if (mb_stripos($text, $w['word']) !== false) {
|
||||
$hit[] = $w['word'];
|
||||
if ($w['level'] >= 2) {
|
||||
return ['blocked' => true, 'hit' => $hit];
|
||||
}
|
||||
}
|
||||
}
|
||||
return ['blocked' => false, 'hit' => $hit];
|
||||
}
|
||||
|
||||
/**
|
||||
* 多字节安全的字符串替换
|
||||
*/
|
||||
private function mbStrReplace($search, $replace, $subject): string
|
||||
{
|
||||
if ($search === '') {
|
||||
return $subject;
|
||||
}
|
||||
$parts = mb_split(preg_quote($search), $subject);
|
||||
if ($parts === false) {
|
||||
return str_ireplace($search, $replace, $subject);
|
||||
}
|
||||
return implode($replace, $parts);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* Service 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class Service extends \think\Service
|
||||
{
|
||||
/**
|
||||
* 注册服务
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行服务
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
//'' php think wxchat:server start
|
||||
$this->commands([
|
||||
'wxchat:server' => \addon\wxchat\command\ChatServer::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user