仰望星辰工作室

better-staridc-MNBT

Z zfhsh first commit 2 天前
1<?php
2/**
3 * realname 插件 - 核心函数库
4 *
5 * 功能:
6 * - 认证记录 CRUD(plg_realname_auth)
7 * - 身份证号 AES-256-CBC 加密存储 / 解密
8 * - 照片上传落盘(runtime/realname/{user_id}/,随机文件名 + .htaccess 防直接访问)
9 * - 自动审核(本地算法,无外部 API)
10 * - 支付拦截 filter(pay.dispatch.before)
11 * - 用户端视图渲染 / JSON 输出
12 * - 管理端 AJAX handler
13 */
14
15if (!defined('IN_CRONLITE')) {
16 exit;
17}
18
19require_once __DIR__ . '/idcard.php';
20
21define('REALNAME_ROOT', dirname(__DIR__)); // .../app_plugins/realname
22
23/* ============================================================
24 * 基础工具
25 * ============================================================ */
26
27/** 生成带站点 base path 前缀的 URL(查询参数路由,无需 rewrite)。 */
28function realname_url($path = '')
29{
30 $scriptName = isset($_SERVER['SCRIPT_NAME']) ? str_replace('\\', '/', $_SERVER['SCRIPT_NAME']) : '';
31 $basePath = rtrim(str_replace('\\', '/', dirname($scriptName)), '/');
32 if ($basePath === '.' || $basePath === '/') {
33 $basePath = '';
34 }
35 $p = ltrim($path, '/');
36 $qpos = strpos($p, '?');
37 if ($qpos !== false) {
38 $route = substr($p, 0, $qpos);
39 $query = substr($p, $qpos + 1);
40 return $basePath . '/index.php?_r=/' . $route . '&' . $query;
41 }
42 return $basePath . '/index.php?_r=/' . $p;
43}
44
45/** 插件静态资源 URL。 */
46function realname_asset_url($path = '')
47{
48 return mnbt_plugin_url('realname', 'assets/' . ltrim($path, '/'));
49}
50
51/** 输出 JSON 并退出。 */
52function realname_json($code, $extra = [])
53{
54 @header('Content-Type: application/json; charset=UTF-8');
55 $payload = ['code' => $code];
56 if (is_array($extra)) {
57 $payload = array_merge($payload, $extra);
58 }
59 echo json_encode($payload, JSON_UNESCAPED_UNICODE);
60 exit;
61}
62
63/** 当前时间字符串(兼容核心 $date)。 */
64function realname_now()
65{
66 global $date;
67 return $date ?: date('Y-m-d H:i:s');
68}
69
70/** 插件配置读取。 */
71function realname_opt($key, $default = null)
72{
73 return function_exists('mnbt_plugin_option_get') ? mnbt_plugin_option_get('realname', $key, $default) : $default;
74}
75
76/* ============================================================
77 * 加密 / 解密(身份证号敏感信息)
78 * ============================================================ */
79
80function realname_aes_key()
81{
82 $key = mnbt_plugin_option_get('realname', 'aes_key', '');
83 if ($key === '') {
84 $key = bin2hex(random_bytes(32));
85 mnbt_plugin_option_set('realname', 'aes_key', $key);
86 }
87 return $key;
88}
89
90/** AES-256-CBC 加密(返回 base64)。 */
91function realname_encrypt($plain)
92{
93 if ($plain === '') {
94 return '';
95 }
96 $key = substr(hash('sha256', realname_aes_key(), true), 0, 32);
97 $iv = random_bytes(16);
98 $cipher = openssl_encrypt($plain, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv);
99 if ($cipher === false) {
100 return '';
101 }
102 return base64_encode($iv . $cipher);
103}
104
105/** AES-256-CBC 解密(base64 → 明文),失败返回 ''。 */
106function realname_decrypt($encoded)
107{
108 if ($encoded === '') {
109 return '';
110 }
111 $raw = base64_decode((string)$encoded, true);
112 if ($raw === false || strlen($raw) <= 16) {
113 return '';
114 }
115 $iv = substr($raw, 0, 16);
116 $cipher = substr($raw, 16);
117 $key = substr(hash('sha256', realname_aes_key(), true), 0, 32);
118 $plain = openssl_decrypt($cipher, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv);
119 return $plain === false ? '' : $plain;
120}
121
122/* ============================================================
123 * 照片存储(runtime/realname/{user_id}/)
124 * ============================================================ */
125
126/** 项目根目录绝对路径。 */
127function realname_root_dir()
128{
129 return dirname(dirname(REALNAME_ROOT));
130}
131
132/** 照片数据根目录(项目根/runtime/realname)。 */
133function realname_data_root()
134{
135 $dir = realname_root_dir() . '/runtime/realname';
136 if (!is_dir($dir)) {
137 @mkdir($dir, 0755, true);
138 }
139 // 禁止 Web 直接访问
140 $ht = $dir . '/.htaccess';
141 if (!is_file($ht)) {
142 @file_put_contents($ht, "Deny from all\n");
143 }
144 return $dir;
145}
146
147/** 某用户的照片目录,不存在则创建。 */
148function realname_user_data_dir($user_id)
149{
150 $dir = realname_data_root() . '/' . (int)$user_id;
151 if (!is_dir($dir)) {
152 @mkdir($dir, 0755, true);
153 }
154 return $dir;
155}
156
157/**
158 * 处理单张上传照片,返回随机文件名(失败返回 '')。
159 * 校验:真实图片(getimagesize)、jpg/png、≤8MB。
160 */
161function realname_save_photo($user_id, $file_key)
162{
163 $file = $_FILES[$file_key] ?? null;
164 if (!$file || !is_array($file) || empty($file['tmp_name'])) {
165 return '';
166 }
167 if ((int)$file['error'] !== UPLOAD_ERR_OK) {
168 return '';
169 }
170 if ((int)$file['size'] > 8 * 1024 * 1024) {
171 return '';
172 }
173 $info = @getimagesize($file['tmp_name']);
174 if ($info === false) {
175 return '';
176 }
177 if (!in_array($info[2], [IMAGETYPE_JPEG, IMAGETYPE_PNG], true)) {
178 return '';
179 }
180 $ext = $info[2] === IMAGETYPE_PNG ? 'png' : 'jpg';
181 $name = bin2hex(random_bytes(16)) . '.' . $ext;
182 $dir = realname_user_data_dir($user_id);
183 if (!move_uploaded_file($file['tmp_name'], $dir . '/' . $name)) {
184 return '';
185 }
186 return $name;
187}
188
189/**
190 * 处理 base64 编码的照片(前端 canvas 压缩后传来),解码后落盘。
191 * @param int $user_id
192 * @param string $prefix 文件名前缀(front/back/hand)
193 * @param string $b64 完整的 data URL(data:image/jpeg;base64,...)
194 * @return string 随机文件名,失败返回 ''
195 */
196function realname_save_photo_base64($user_id, $prefix, $b64)
197{
198 if ($b64 === '' || strlen($b64) > 2 * 1024 * 1024) {
199 return ''; // base64 字符串超过 2MB 说明原图太大
200 }
201 // 解析 data:image/xxx;base64,...
202 if (!preg_match('#^data:image/(jpe?g|png);base64,(.+)$#i', $b64, $m)) {
203 return '';
204 }
205 $mimeType = strtolower($m[1]) === 'png' ? 'image/png' : 'image/jpeg';
206 $raw = base64_decode($m[2], true);
207 if ($raw === false || strlen($raw) > 8 * 1024 * 1024) {
208 return '';
209 }
210 // 验证解码后是有效图片
211 $tmp = tempnam(sys_get_temp_dir(), 'rn');
212 if ($tmp === false || file_put_contents($tmp, $raw) === false) {
213 return '';
214 }
215 $info = @getimagesize($tmp);
216 if ($info === false) {
217 @unlink($tmp);
218 return '';
219 }
220 if (!in_array($info[2], [IMAGETYPE_JPEG, IMAGETYPE_PNG], true)) {
221 @unlink($tmp);
222 return '';
223 }
224 $ext = $info[2] === IMAGETYPE_PNG ? 'png' : 'jpg';
225 $name = $prefix . '_' . bin2hex(random_bytes(12)) . '.' . $ext;
226 $dir = realname_user_data_dir($user_id);
227 if (!rename($tmp, $dir . '/' . $name)) {
228 @unlink($tmp);
229 return '';
230 }
231 return $name;
232}
233
234/** 根据相对文件名返回照片绝对路径。 */
235function realname_photo_path($user_id, $filename)
236{
237 if ($filename === '') {
238 return '';
239 }
240 $filename = basename((string)$filename); // 防路径穿越
241 return realname_user_data_dir($user_id) . '/' . $filename;
242}
243
244/* ============================================================
245 * 认证记录 CRUD
246 * ============================================================ */
247
248function realname_get_by_user($user_id)
249{
250 global $DB;
251 $row = $DB->get_row_prepare("SELECT * FROM plg_realname_auth WHERE user_id=? LIMIT 1", [(int)$user_id]);
252 return $row ?: null;
253}
254
255function realname_get_by_id($id)
256{
257 global $DB;
258 $row = $DB->get_row_prepare("SELECT * FROM plg_realname_auth WHERE id=? LIMIT 1", [(int)$id]);
259 return $row ?: null;
260}
261
262/**
263 * 保存(插入或覆盖)认证记录。
264 * @param array $user 当前插件用户数组
265 * @param array $data real_name/phone/id_card/front_img/back_img/hand_img/ocr_name/ocr_id_card
266 * @return array ['ok'=>bool, 'id'=>int, 'status'=>string, 'note'=>string]
267 */
268function realname_save_auth($user, $data)
269{
270 global $DB;
271 $user_id = (int)$user['id'];
272 $now = realname_now();
273
274 // 确保表存在
275 static $tableReady = false;
276 if (!$tableReady) {
277 $tableReady = true;
278 $ok = $DB->query(
279 "CREATE TABLE IF NOT EXISTS `plg_realname_auth` ("
280 . "`id` int(11) NOT NULL AUTO_INCREMENT,"
281 . "`user_id` int(11) NOT NULL,"
282 . "`username` varchar(64) NOT NULL DEFAULT '',"
283 . "`real_name` varchar(64) NOT NULL DEFAULT '',"
284 . "`phone` varchar(20) NOT NULL DEFAULT '',"
285 . "`id_card` varchar(255) NOT NULL DEFAULT '',"
286 . "`front_img` varchar(255) NOT NULL DEFAULT '',"
287 . "`back_img` varchar(255) NOT NULL DEFAULT '',"
288 . "`hand_img` varchar(255) NOT NULL DEFAULT '',"
289 . "`ocr_name` varchar(64) NOT NULL DEFAULT '',"
290 . "`ocr_id_card` varchar(255) NOT NULL DEFAULT '',"
291 . "`status` varchar(16) NOT NULL DEFAULT 'pending',"
292 . "`audit_note` varchar(255) NOT NULL DEFAULT '',"
293 . "`created_at` varchar(50) NOT NULL DEFAULT '',"
294 . "`updated_at` varchar(50) NOT NULL DEFAULT '',"
295 . "`audited_at` varchar(50) NOT NULL DEFAULT '',"
296 . "PRIMARY KEY (`id`),"
297 . "UNIQUE KEY `uk_user` (`user_id`),"
298 . "KEY `idx_status` (`status`)"
299 . ") ENGINE=MyISAM DEFAULT CHARSET=utf8"
300 );
301 if (!$ok) {
302 $msg = method_exists($DB, 'error') ? $DB->error() : 'unknown';
303 return ['ok' => false, 'id' => 0, 'status' => 'error', 'note' => '建表失败: ' . $msg];
304 }
305 }
306
307 $existing = realname_get_by_user($user_id);
308 $id_card_enc = realname_encrypt($data['id_card']);
309 $ocr_id_enc = realname_encrypt($data['ocr_id_card']);
310
311 // 自动审核
312 $audit = realname_auto_audit($data);
313 $status = $audit['status'];
314 $note = $audit['note'];
315
316 $e = function ($v) use ($DB) { return "'" . $DB->escape((string)$v) . "'"; };
317 if ($existing) {
318 $audited_at = ($status !== 'pending' ? $now : $existing['audited_at']);
319 $sql = "UPDATE plg_realname_auth SET "
320 . "real_name=" . $e($data['real_name'])
321 . ", phone=" . $e($data['phone'])
322 . ", id_card=" . $e($id_card_enc)
323 . ", front_img=" . $e($data['front_img'])
324 . ", back_img=" . $e($data['back_img'])
325 . ", hand_img=" . $e($data['hand_img'])
326 . ", ocr_name=" . $e($data['ocr_name'])
327 . ", ocr_id_card=" . $e($ocr_id_enc)
328 . ", status=" . $e($status)
329 . ", audit_note=" . $e($note)
330 . ", updated_at=" . $e($now)
331 . ", audited_at=" . $e($audited_at)
332 . " WHERE user_id=" . (int)$user_id;
333 $ok = (bool)$DB->query($sql);
334 $id = (int)$existing['id'];
335 } else {
336 $audited_at = ($status !== 'pending' ? $now : '');
337 $sql = "INSERT INTO plg_realname_auth "
338 . "(user_id, username, real_name, phone, id_card, front_img, back_img, hand_img, ocr_name, ocr_id_card, status, audit_note, created_at, updated_at, audited_at) VALUES ("
339 . (int)$user_id . ", " . $e($user['username']) . ", " . $e($data['real_name']) . ", " . $e($data['phone']) . ", "
340 . $e($id_card_enc) . ", " . $e($data['front_img']) . ", " . $e($data['back_img']) . ", " . $e($data['hand_img']) . ", "
341 . $e($data['ocr_name']) . ", " . $e($ocr_id_enc) . ", " . $e($status) . ", " . $e($note) . ", "
342 . $e($now) . ", " . $e($now) . ", " . $e($audited_at) . ")";
343 $ok = (bool)$DB->query($sql);
344 if ($ok) {
345 $row = $DB->get_row("SELECT id FROM plg_realname_auth WHERE user_id=" . (int)$user_id . " LIMIT 1");
346 $id = $row ? (int)$row['id'] : 0;
347 } else {
348 $id = 0;
349 }
350 }
351
352 if (!$ok) {
353 $err = method_exists($DB, 'error') ? $DB->error() : 'unknown';
354 $short = strlen($sql) > 300 ? (substr($sql, 0, 300) . '...') : $sql;
355 return ['ok' => false, 'id' => 0, 'status' => 'error', 'note' => '写入失败: ' . $err . ' | SQL前段: ' . $short];
356 }
357 return ['ok' => true, 'id' => $id, 'status' => $status, 'note' => $note];
358}
359
360/* ============================================================
361 * 自动审核(本地算法)
362 * ============================================================ */
363
364/**
365 * 自动审核:格式合法性 + 交叉一致性(本地三要素验证)。
366 * @param array $data real_name/phone/id_card/ocr_name/ocr_id_card
367 * @return array ['status'=>'approved'|'rejected'|'pending', 'note'=>string]
368 */
369function realname_auto_audit($data)
370{
371 $real_name = trim((string)($data['real_name'] ?? ''));
372 $phone = trim((string)($data['phone'] ?? ''));
373 $id_card = strtoupper(trim((string)($data['id_card'] ?? '')));
374 $ocr_name = trim((string)($data['ocr_name'] ?? ''));
375 $ocr_id_card = strtoupper(preg_replace('/\s+/', '', (string)($data['ocr_id_card'] ?? '')));
376
377 $min_age = (int)realname_opt('min_age', 18);
378 $allow_name_diff = (bool)realname_opt('allow_ocr_name_diff', false);
379
380 // 1. 姓名
381 $nameChk = realname_name_validate($real_name);
382 if (!$nameChk['ok']) {
383 return ['status' => 'rejected', 'note' => $nameChk['msg']];
384 }
385 // 2. 手机号
386 $phoneChk = realname_phone_validate($phone);
387 if (!$phoneChk['ok']) {
388 return ['status' => 'rejected', 'note' => $phoneChk['msg']];
389 }
390 // 3. 身份证
391 $idChk = realname_idcard_validate($id_card);
392 if (!$idChk['ok']) {
393 return ['status' => 'rejected', 'note' => $idChk['msg']];
394 }
395 // 4. 年龄
396 if ($min_age > 0 && $idChk['age'] < $min_age) {
397 return ['status' => 'rejected', 'note' => '未满 ' . $min_age . ' 周岁,暂不支持认证'];
398 }
399 // 5. OCR 一致性:身份证号
400 if ($ocr_id_card !== '' && $ocr_id_card !== $id_card) {
401 return ['status' => 'pending', 'note' => 'OCR 身份证号与填写不一致,已转人工复核'];
402 }
403 // 6. OCR 一致性:姓名
404 if ($allow_name_diff) {
405 if ($ocr_id_card === '') {
406 return ['status' => 'pending', 'note' => 'OCR 身份证号识别失败,转人工复核'];
407 }
408 return ['status' => 'approved', 'note' => '自动审核通过'];
409 }
410 if ($ocr_name === '') {
411 return ['status' => 'pending', 'note' => 'OCR 姓名识别失败,转人工复核'];
412 }
413 if ($ocr_name !== $real_name) {
414 return ['status' => 'pending', 'note' => 'OCR 姓名与填写不一致,已转人工复核'];
415 }
416 if ($ocr_id_card === '') {
417 return ['status' => 'pending', 'note' => 'OCR 身份证号识别失败,转人工复核'];
418 }
419 return ['status' => 'approved', 'note' => '自动审核通过'];
420}
421
422/* ============================================================
423 * 支付拦截(pay.dispatch.before filter)
424 * ============================================================ */
425
426/**
427 * filter 回调:未实名的插件用户禁止发起支付。
428 * @return string|null 返回 HTML 提示则拦截并终止;null 放行
429 */
430function realname_pay_guard($value, $type, $order_context)
431{
432 if (!function_exists('user_info_auth_current')) {
433 return null;
434 }
435 $user = user_info_auth_current();
436 if (!$user) {
437 return null; // 非插件用户(核心用户)→ 放行
438 }
439 $auth = realname_get_by_user((int)$user['id']);
440 if ($auth && $auth['status'] === 'approved') {
441 return null; // 已实名 → 放行
442 }
443
444 $isTdesign = function_exists('mnbt_theme_name') && mnbt_theme_name('user') === 'tdesign';
445 if ($isTdesign && function_exists('user_info_url')) {
446 $applyUrl = user_info_url('account') . '#/realname';
447 $statusUrl = $applyUrl;
448 } else {
449 $applyUrl = realname_url('realname/apply');
450 $statusUrl = realname_url('realname/status');
451 }
452 $tip = '尚未完成实名认证';
453 if ($auth) {
454 if ($auth['status'] === 'pending') {
455 $tip = '实名认证正在审核中';
456 } elseif ($auth['status'] === 'rejected') {
457 $tip = '实名认证未通过,请重新提交';
458 }
459 }
460 return '<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8">'
461 . '<meta name="viewport" content="width=device-width, initial-scale=1.0">'
462 . '<title>实名认证</title><style>'
463 . 'body{font-family:-apple-system,"PingFang SC","Microsoft YaHei",sans-serif;background:#f4f6fa;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0;}'
464 . '.rn-card{background:#fff;border-radius:16px;box-shadow:0 8px 30px rgba(0,0,0,.08);padding:44px 52px;max-width:420px;width:88%;text-align:center;}'
465 . '.rn-ico{width:64px;height:64px;border-radius:50%;background:#fef0ef;color:#e34d59;display:flex;align-items:center;justify-content:center;font-size:32px;margin:0 auto 20px;}'
466 . 'h2{font-size:20px;color:#1c2438;margin:0 0 10px;}'
467 . 'p{font-size:14px;color:#6b7a99;line-height:1.7;margin:0 0 26px;}'
468 . 'a.rn-btn{display:inline-block;background:#0052d9;color:#fff;text-decoration:none;padding:10px 30px;border-radius:8px;font-size:14px;transition:opacity .2s;}'
469 . 'a.rn-btn:hover{opacity:.85;}'
470 . '</style></head><body>'
471 . '<div class="rn-card"><div class="rn-ico">&#128100;</div>'
472 . '<h2>' . htmlspecialchars($tip) . '</h2>'
473 . '<p>根据平台要求,购买产品前需先完成实名认证(姓名 / 手机号 / 身份证号 + 证件照片,全程本地识别)。</p>'
474 . '<a class="rn-btn" href="' . htmlspecialchars($applyUrl) . '">前往实名认证</a>'
475 . '&nbsp;&nbsp;<a class="rn-btn" style="background:#6b7a99" href="' . htmlspecialchars($statusUrl) . '">查看状态</a>'
476 . '</div></body></html>';
477}
478
479/* ============================================================
480 * 视图渲染
481 * ============================================================ */
482
483/** 渲染用户端视图(带插件自带布局,不依赖外部 CDN)。 */
484function realname_render($view, $vars = [])
485{
486 $vars['current_user'] = $vars['current_user'] ?? (function_exists('user_info_auth_current') ? user_info_auth_current() : null);
487 $vars['asset_url'] = realname_asset_url();
488 $vars['url'] = 'realname_url';
489 extract($vars, EXTR_SKIP);
490
491 $viewFile = REALNAME_ROOT . '/views/' . $view . '.php';
492 if (!is_file($viewFile)) {
493 http_response_code(500);
494 echo 'View not found: ' . htmlspecialchars($view);
495 return;
496 }
497 include $viewFile;
498}
499
500/* ============================================================
501 * 管理端 AJAX handler(由 bootstrap.php 注册)
502 * ============================================================ */
503
504/** 审核列表(分页 + 筛选)。 */
505function realname_admin_list()
506{
507 global $DB;
508 mnbt_plugin_require_admin();
509 $page = max(1, (int)($_POST['page'] ?? 1));
510 $per = min(50, max(10, (int)($_POST['per_page'] ?? 20)));
511 $status = trim((string)($_POST['status'] ?? ''));
512 $kw = trim((string)($_POST['keyword'] ?? ''));
513
514 $where = 'WHERE 1';
515 $params = [];
516 if ($status !== '' && in_array($status, ['pending', 'approved', 'rejected'], true)) {
517 $where .= ' AND status=?';
518 $params[] = $status;
519 }
520 if ($kw !== '') {
521 $where .= ' AND (username LIKE ? OR real_name LIKE ?)';
522 $params[] = '%' . $kw . '%';
523 $params[] = '%' . $kw . '%';
524 }
525 $total = (int)($DB->get_row_prepare("SELECT COUNT(*) c FROM plg_realname_auth {$where}", $params)['c'] ?? 0);
526 $offset = ($page - 1) * $per;
527 $rows = $DB->get_all_prepare("SELECT * FROM plg_realname_auth {$where} ORDER BY id DESC LIMIT {$offset},{$per}", $params) ?: [];
528
529 $items = [];
530 foreach ($rows as $r) {
531 $idcard = realname_decrypt($r['id_card']);
532 $items[] = [
533 'id' => (int)$r['id'],
534 'user_id' => (int)$r['user_id'],
535 'username' => (string)$r['username'],
536 'real_name' => realname_mask_name($r['real_name']),
537 'phone' => realname_mask_phone($r['phone']),
538 'id_card' => realname_mask_idcard($idcard),
539 'status' => (string)$r['status'],
540 'audit_note' => (string)$r['audit_note'],
541 'created_at' => (string)$r['created_at'],
542 'audited_at' => (string)$r['audited_at'],
543 ];
544 }
545 realname_json('ok', ['items' => $items, 'total' => $total, 'page' => $page, 'pages' => max(1, (int)ceil($total / $per))]);
546}
547
548/** 审核通过。 */
549function realname_admin_approve()
550{
551 global $DB;
552 mnbt_plugin_require_admin();
553 $id = (int)($_POST['id'] ?? 0);
554 $note = trim((string)($_POST['note'] ?? ''));
555 if ($id <= 0) {
556 realname_json('参数错误');
557 }
558 $row = realname_get_by_id($id);
559 if (!$row) {
560 realname_json('记录不存在');
561 }
562 $now = realname_now();
563 $final = $note === '' ? '管理员审核通过' : '管理员审核通过:' . $note;
564 if (!$DB->query_prepare("UPDATE plg_realname_auth SET status='approved', audit_note=?, audited_at=? WHERE id=?", [$final, $now, $id])) {
565 realname_json('操作失败');
566 }
567 realname_json('ok', ['message' => '已通过']);
568}
569
570/** 审核驳回。 */
571function realname_admin_reject()
572{
573 global $DB;
574 mnbt_plugin_require_admin();
575 $id = (int)($_POST['id'] ?? 0);
576 $note = trim((string)($_POST['note'] ?? ''));
577 if ($id <= 0) {
578 realname_json('参数错误');
579 }
580 if ($note === '') {
581 realname_json('请填写驳回原因');
582 }
583 $row = realname_get_by_id($id);
584 if (!$row) {
585 realname_json('记录不存在');
586 }
587 $now = realname_now();
588 if (!$DB->query_prepare("UPDATE plg_realname_auth SET status='rejected', audit_note=?, audited_at=? WHERE id=?", [$note, $now, $id])) {
589 realname_json('操作失败');
590 }
591 realname_json('ok', ['message' => '已驳回']);
592}
593
594/** 解密查看身份证号(管理员二次密码验证)。 */
595function realname_admin_decrypt()
596{
597 global $DB, $conf;
598 mnbt_plugin_require_admin();
599 $id = (int)($_POST['id'] ?? 0);
600 $password = (string)($_POST['password'] ?? '');
601 if ($id <= 0) {
602 realname_json('参数错误');
603 }
604 if ($password === '' || $password !== ($conf['pwd'] ?? '')) {
605 realname_json('管理密码错误');
606 }
607 $row = realname_get_by_id($id);
608 if (!$row) {
609 realname_json('记录不存在');
610 }
611 realname_json('ok', [
612 'real_name' => (string)$row['real_name'],
613 'phone' => (string)$row['phone'],
614 'id_card' => realname_decrypt($row['id_card']),
615 'ocr_name' => (string)$row['ocr_name'],
616 'ocr_id_card' => realname_decrypt($row['ocr_id_card']),
617 ]);
618}