Files
links/application/admin/model/split/Ticket.php
T

398 lines
11 KiB
PHP
Executable File
Raw 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
declare(strict_types=1);
namespace app\admin\model\split;
use app\common\service\SplitSyncConfigService;
use app\common\service\SplitTicketRuleService;
use app\common\service\SplitTicketSyncLockService;
use think\Db;
use think\Model;
/**
* 分流工单模型
*/
class Ticket extends Model
{
protected $name = 'split_ticket';
protected static function init(): void
{
// 删除工单时:释放同步锁,并物理删除同步导入的号码(manual_manage=0
self::beforeDelete(function (self $ticket): void {
(new SplitTicketSyncLockService())->release((int) $ticket['id']);
(new SplitTicketRuleService())->deleteSyncedNumbersForTicket($ticket);
});
}
protected $autoWriteTimestamp = 'integer';
protected $createTime = 'createtime';
protected $updateTime = 'updatetime';
protected $deleteTime = false;
protected $append = [
'ticket_type_text',
'number_type_text',
'link_code_text',
'start_time_text',
'end_time_text',
'status_text',
'click_count',
'ticket_progress_text',
'inbound_ratio_text',
'sync_display_text',
'sync_success_text',
'sync_tooltip_text',
'sync_auto_paused',
];
/**
* 工单类型
*
* @return array<string, string>
*/
public function getTicketTypeList(): array
{
return [
'xinghe' => __('Ticket type xinghe'),
'haiwang' => __('Ticket type haiwang'),
'taiji' => __('Ticket type taiji'),
'huojian' => __('Ticket type huojian'),
'ss_channel' => __('Ticket type ss_channel'),
'ss_customer' => __('Ticket type ss_customer'),
'yifafa' => __('Ticket type yifafa'),
'a2c' => __('Ticket type a2c'),
'ceo_scrm' => __('Ticket type ceo_scrm'),
'chatknow' => __('Ticket type chatknow'),
'whatshub' => __('Ticket type whatshub'),
'sihai' => __('Ticket type sihai'),
];
}
/**
* @return array<string, string>
*/
public function getStatusList(): array
{
return [
'normal' => __('Status normal'),
'hidden' => __('Status hidden'),
];
}
/**
* @return array<string, string>
*/
public function getSyncStatusList(): array
{
return [
'success' => __('Sync status success'),
'error' => __('Sync status error'),
'pending' => __('Sync status pending'),
];
}
/**
* 号码类型
*
* @return array<string, string>
*/
public function getNumberTypeList(): array
{
return [
'whatsapp' => 'Whatsapp',
'telegram' => 'Telegram',
'line' => 'Line',
'custom' => __('Number type custom'),
];
}
/**
* 关联分流链接
*/
public function splitLink()
{
// IN 预载入:避免 eagerlyType=0(JOIN) 与列表 field 子查询冲突导致 SQL 1064
return $this->belongsTo(Link::class, 'split_link_id', 'id', [], 'LEFT')->setEagerlyType(1);
}
/**
* 列表子查询注入的点击数,无则按关联号码汇总
*
* @param mixed $value
* @param array<string, mixed> $data
*/
public function getClickCountAttr($value, $data): int
{
if (isset($data['click_count']) && $data['click_count'] !== '' && $data['click_count'] !== null) {
return (int) $data['click_count'];
}
return self::sumVisitCountForTicket($data);
}
/**
* 工单进度:完成数量 / 工单总量
*
* @param mixed $value
* @param array<string, mixed> $data
*/
public function getTicketProgressTextAttr($value, $data): string
{
$total = (int) ($data['ticket_total'] ?? 0);
$complete = (int) ($data['complete_count'] ?? 0);
if ($total <= 0) {
return '0%';
}
$percent = round($complete / $total * 100, 2);
return $percent . '%';
}
/**
* 进线比例:进线人数 / 点击数(点击数=关联号码 visit_count 之和)
*
* @param mixed $value
* @param array<string, mixed> $data
*/
public function getInboundRatioTextAttr($value, $data): string
{
$clicks = $this->getClickCountAttr(null, $data);
$inbound = (int) ($data['inbound_count'] ?? 0);
if ($clicks <= 0) {
return '—';
}
$percent = round($inbound / $clicks * 100, 2);
return $percent . '%';
}
/**
* 是否因连续失败暂停自动同步(工单仍为开启状态)
*
* @param array<string, mixed> $data
*/
public static function isAutoSyncPaused(array $data): bool
{
if ((string) ($data['status'] ?? '') !== 'normal') {
return false;
}
$threshold = SplitSyncConfigService::getFailPauseThreshold();
if ($threshold <= 0) {
return false;
}
return (int) ($data['sync_fail_count'] ?? 0) >= $threshold;
}
/**
* 同步状态展示文案(列表列默认显示,异常详情见 tooltip)
*
* @param mixed $value
* @param array<string, mixed> $data
*/
public function getSyncDisplayTextAttr($value, $data): string
{
if (self::isAutoSyncPaused($data)) {
$threshold = SplitSyncConfigService::getFailPauseThreshold();
return sprintf((string) __('Sync display auto paused'), $threshold);
}
$status = (string) ($data['sync_status'] ?? 'pending');
$online = (int) ($data['online_count'] ?? 0);
if ($status === 'success') {
return sprintf((string) __('Sync display success'), $online);
}
if ($status === 'pending') {
return (string) __('Sync display pending');
}
return (string) __('Sync display error');
}
/**
* 同步状态列 tooltip 完整说明
*
* @param mixed $value
* @param array<string, mixed> $data
*/
public function getSyncTooltipTextAttr($value, $data): string
{
$parts = [];
if (self::isAutoSyncPaused($data)) {
$threshold = SplitSyncConfigService::getFailPauseThreshold();
$parts[] = sprintf(
(string) __('Sync tooltip auto paused'),
(int) ($data['sync_fail_count'] ?? 0),
$threshold
);
}
$message = trim((string) ($data['sync_message'] ?? ''));
if ($message !== '') {
$parts[] = (string) __('Sync tooltip error prefix') . $message;
}
return implode("\n", $parts);
}
/**
* 是否暂停自动同步(供前端样式判断)
*
* @param mixed $value
* @param array<string, mixed> $data
*/
public function getSyncAutoPausedAttr($value, $data): bool
{
return self::isAutoSyncPaused($data);
}
/**
* 最近一次同步成功时间及方式
*
* @param mixed $value
* @param array<string, mixed> $data
*/
public function getSyncSuccessTextAttr($value, $data): string
{
$ts = (int) ($data['sync_success_time'] ?? 0);
if ($ts <= 0) {
return '';
}
$timeText = date('Y-m-d H:i:s', $ts);
$mode = (string) ($data['sync_success_mode'] ?? '');
if ($mode === 'auto') {
return $timeText . ' (' . (string) __('Sync mode auto') . ')';
}
if ($mode === 'manual') {
return $timeText . ' (' . (string) __('Sync mode manual') . ')';
}
return $timeText;
}
public function setStartTimeAttr($value): ?int
{
return self::parseTimeValue($value);
}
public function setEndTimeAttr($value): ?int
{
return self::parseTimeValue($value);
}
public function setNumberTypeCustomAttr($value): string
{
return trim((string) $value);
}
public function setTicketTotalAttr($value): int
{
return max(0, (int) $value);
}
public function getTicketTypeTextAttr($value, $data): string
{
$key = (string) ($data['ticket_type'] ?? '');
$list = $this->getTicketTypeList();
return $list[$key] ?? $key;
}
public function getNumberTypeTextAttr($value, $data): string
{
$type = (string) ($data['number_type'] ?? '');
if ($type === 'custom') {
$custom = trim((string) ($data['number_type_custom'] ?? ''));
return $custom !== '' ? $custom : (string) __('Number type custom');
}
$list = $this->getNumberTypeList();
return $list[$type] ?? $type;
}
public function getLinkCodeTextAttr($value, $data): string
{
if ($value !== '' && $value !== null) {
return (string) $value;
}
$linkId = (int) ($data['split_link_id'] ?? 0);
if ($linkId <= 0) {
return '';
}
$code = Link::where('id', $linkId)->value('link_code');
return (string) $code;
}
public function getStartTimeTextAttr($value, $data): string
{
return self::formatTimeText($data['start_time'] ?? null);
}
public function getEndTimeTextAttr($value, $data): string
{
return self::formatTimeText($data['end_time'] ?? null);
}
public function getStatusTextAttr($value, $data): string
{
$key = (string) ($data['status'] ?? '');
$list = $this->getStatusList();
return $list[$key] ?? $key;
}
/**
* 构建列表用 click_count 子查询 SQL 片段
*/
public static function buildClickCountSubQuery(string $ticketTableAlias = ''): string
{
$ticketTable = (new self())->getTable();
$numberTable = (new Number())->getTable();
$t = $ticketTableAlias !== '' ? $ticketTableAlias : $ticketTable;
return "(SELECT COALESCE(SUM(n.visit_count), 0) FROM `{$numberTable}` n "
. "WHERE n.ticket_name = `{$t}`.ticket_name "
. "AND n.split_link_id = `{$t}`.split_link_id "
. "AND n.admin_id = `{$t}`.admin_id) AS click_count";
}
/**
* @param array<string, mixed> $data
*/
private static function sumVisitCountForTicket(array $data): int
{
$ticketName = (string) ($data['ticket_name'] ?? '');
$linkId = (int) ($data['split_link_id'] ?? 0);
$adminId = (int) ($data['admin_id'] ?? 0);
if ($ticketName === '' || $linkId <= 0) {
return 0;
}
$sum = Db::name('split_number')
->where('ticket_name', $ticketName)
->where('split_link_id', $linkId)
->where('admin_id', $adminId)
->sum('visit_count');
return (int) $sum;
}
/**
* @param mixed $value
*/
private static function parseTimeValue($value): ?int
{
if ($value === '' || $value === null) {
return null;
}
if (is_numeric($value)) {
return (int) $value;
}
$ts = strtotime((string) $value);
return $ts === false ? null : $ts;
}
/**
* @param mixed $value
*/
private static function formatTimeText($value): string
{
if ($value === '' || $value === null || !is_numeric($value)) {
return '';
}
$ts = (int) $value;
return $ts > 0 ? date('Y-m-d H:i:s', $ts) : '';
}
}