vr-shopxo-plugin/shopxo/app/plugins/vr_ticket/service/AuditService.php

275 lines
9.0 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<?php
/**
* VR票务插件 - 审计日志服务
*
* 记录所有敏感操作的防篡改审计日志
*
* @package vr_ticket\service
*/
namespace app\plugins\vr_ticket\service;
class AuditService
{
// ========================
// 操作类型常量(枚举)
// ========================
const ACTION_VERIFY = 'verify'; // 核销票
const ACTION_REFUND = 'refund'; // 退款
const ACTION_EXPORT = 'export'; // 批量导出
const ACTION_DISABLE_VERIFIER= 'disable_verifier'; // 禁用核销员
const ACTION_ENABLE_VERIFIER = 'enable_verifier'; // 启用核销员
const ACTION_DELETE_VERIFIER = 'delete_verifier'; // 删除核销员
const ACTION_DELETE_TEMPLATE = 'delete_template'; // 删除座位模板
const ACTION_DISABLE_TEMPLATE= 'disable_template'; // 禁用座位模板
const ACTION_ENABLE_TEMPLATE = 'enable_template'; // 启用座位模板
const ACTION_ISSUE_TICKET = 'issue_ticket'; // 补发票Phase 3
// ========================
// 对象类型常量
// ========================
const TARGET_TICKET = 'ticket';
const TARGET_VERIFIER = 'verifier';
const TARGET_TEMPLATE = 'seat_template';
const TARGET_GOODS = 'goods';
// ========================
// 日志记录入口(同步写入,异常不阻断主流程)
// ========================
/**
* 记录审计日志
*
* @param string $action 操作类型(使用常量)
* @param string $targetType 对象类型
* @param int $targetId 对象ID
* @param array $extra 附加数据before/after 状态等)
* @param string $targetDesc 对象描述(冗余字段,便于人工查询)
* @return int|false 写入成功返回日志ID失败返回 false
*/
public static function log($action, $targetType, $targetId, $extra = [], $targetDesc = '')
{
try {
$operatorId = self::getOperatorId();
$operatorName = self::getOperatorName();
$clientIp = self::getClientIp();
$userAgent = self::getUserAgent();
$requestId = self::getOrCreateRequestId();
$createdAt = BaseService::now();
$id = \Db::name(BaseService::table('audit_log'))->insertGetId([
'action' => $action,
'operator_id' => $operatorId,
'operator_name' => $operatorName,
'target_type' => $targetType,
'target_id' => $targetId,
'target_desc' => $targetDesc ?: self::buildTargetDesc($targetType, $targetId),
'client_ip' => $clientIp,
'user_agent' => mb_substr($userAgent, 0, 512),
'request_id' => $requestId,
'extra' => empty($extra) ? null : json_encode($extra, JSON_UNESCAPED_UNICODE),
'created_at' => $createdAt,
]);
return $id;
} catch (\Throwable $e) {
// 审计日志写入失败不阻断主业务流程,但记录警告
BaseService::log('AuditService::log failed', [
'action' => $action,
'targetType' => $targetType,
'targetId' => $targetId,
'error' => $e->getMessage(),
], 'warning');
return false;
}
}
// ========================
// 便捷包装方法(核销操作)
// ========================
/**
* 记录核销操作
*/
public static function logVerify($ticketId, $ticketCode, $verifierId, $verifierName, $result, $oldStatus)
{
return self::log(
self::ACTION_VERIFY,
self::TARGET_TICKET,
$ticketId,
[
'ticket_code' => $ticketCode,
'verifier_id' => $verifierId,
'verifier' => $verifierName,
'old_status' => $oldStatus,
'result' => $result,
],
"票码: {$ticketCode}"
);
}
/**
* 记录导出操作
*/
public static function logExport($goodsId, $filter, $count)
{
return self::log(
self::ACTION_EXPORT,
self::TARGET_GOODS,
$goodsId,
[
'filter' => $filter,
'count' => $count,
],
$goodsId > 0 ? "商品ID: {$goodsId}" : '全量导出'
);
}
// ========================
// 查询接口(供管理后台使用)
// ========================
/**
* 查询审计日志(分页)
*
* @param array $params 查询参数action, operator_id, target_type, target_id, date_from, date_to, page, limit
* @return array
*/
public static function search($params = [])
{
$where = [];
if (!empty($params['action'])) {
$where[] = ['action', '=', $params['action']];
}
if (!empty($params['operator_id'])) {
$where[] = ['operator_id', '=', intval($params['operator_id'])];
}
if (!empty($params['target_type'])) {
$where[] = ['target_type', '=', $params['target_type']];
}
if (!empty($params['target_id'])) {
$where[] = ['target_id', '=', intval($params['target_id'])];
}
if (!empty($params['date_from'])) {
$where[] = ['created_at', '>=', strtotime($params['date_from'])];
}
if (!empty($params['date_to'])) {
$where[] = ['created_at', '<=', strtotime($params['date_to'] . ' 23:59:59')];
}
$page = max(1, intval($params['page'] ?? 1));
$pageSize = min(100, max(10, intval($params['limit'] ?? 20)));
$result = \Db::name(BaseService::table('audit_log'))
->where($where)
->order('id', 'desc')
->paginate($pageSize)
->toArray();
// JSON 解析 extra 字段
if (!empty($result['data'])) {
foreach ($result['data'] as &$row) {
if (!empty($row['extra'])) {
$row['extra'] = json_decode($row['extra'], true);
}
}
unset($row);
}
return $result;
}
// ========================
// 内部工具方法
// ========================
/**
* 获取当前操作用户 ID
*/
private static function getOperatorId()
{
// ShopXO admin session: $this->admin['id'] 在控制器中
// 在服务层通过 session() 或 app() 获取
$admin = session('admin');
return isset($admin['id']) ? intval($admin['id']) : 0;
}
/**
* 获取当前操作用户名称
*/
private static function getOperatorName()
{
$admin = session('admin');
return $admin['username'] ?? ($admin['name'] ?? '');
}
/**
* 获取客户端真实 IP
*/
private static function getClientIp()
{
$ip = request()->ip(0, true); // true = 穿透代理
return $ip ?: '';
}
/**
* 获取 User-Agent
*/
private static function getUserAgent()
{
return request()->header('user-agent', '');
}
/**
* 获取或创建请求追踪 ID用于关联同一 HTTP 请求中的多个操作)
*/
private static function getOrCreateRequestId()
{
static $requestId = null;
if ($requestId === null) {
$requestId = session('vr_ticket_request_id');
if (empty($requestId)) {
$requestId = self::generateRequestId();
session('vr_ticket_request_id', $requestId);
}
}
return $requestId;
}
/**
* 生成唯一请求 ID
*/
private static function generateRequestId()
{
return sprintf(
'%s-%s-%04x-%04x-%04x',
date('YmdHis'),
substr(md5(uniqid((string) mt_rand(), true)), 0, 8),
mt_rand(0, 0xffff),
mt_rand(0, 0xffff),
mt_rand(0, 0xffff)
);
}
/**
* 根据对象类型和 ID 构建描述文本
*/
private static function buildTargetDesc($targetType, $targetId)
{
switch ($targetType) {
case self::TARGET_TICKET:
$ticket = \Db::name(BaseService::table('tickets'))->where('id', $targetId)->find();
return $ticket ? "票码: {$ticket['ticket_code']}" : "票ID: {$targetId}";
case self::TARGET_VERIFIER:
$verifier = \Db::name(BaseService::table('verifiers'))->where('id', $targetId)->find();
return $verifier ? "核销员: {$verifier['name']}" : "核销员ID: {$targetId}";
case self::TARGET_TEMPLATE:
$template = \Db::name(BaseService::table('seat_templates'))->where('id', $targetId)->find();
return $template ? "模板: {$template['name']}" : "模板ID: {$targetId}";
default:
return "{$targetType}:{$targetId}";
}
}
}