This commit is contained in:
Your Name
2026-05-05 18:25:14 +08:00
parent cd51f55701
commit de78f3b218
11 changed files with 559 additions and 97 deletions
@@ -10,11 +10,11 @@ use app\adminapi\logic\stats\YejiStatsLogic;
/**
* 业绩看板(部门 × 时间区间)控制器
*
* - GET stats.yeji-stats/overview 单区间
* - GET stats.yeji-stats/multi 多区间一次返回(默认 月/周/今日/昨日 四张表)
* - GET stats.yeji-stats/dept-options 部门下拉
* - GET stats.yeji-stats/leaderboard 医助排行榜(按展示部门分表)
* - GET stats.yeji-stats/unassignedBreakdown 「未归属中心」合计业绩按订单创建人拆解
* - GET stats.yejiStats/overview 单区间
* - GET stats.yejiStats/multi 多区间一次返回(默认 月/周/今日/昨日 四张表)
* - GET stats.yejiStats/deptOptions 部门下拉
* - GET stats.yejiStats/leaderboard 医助排行榜(按展示部门分表)
* - GET stats.yejiStats/leadLines 进线数据明细(add_external_contact 逐条)
*/
class YejiStatsController extends BaseAdminController
{
@@ -112,4 +112,13 @@ class YejiStatsController extends BaseAdminController
return $this->data(YejiStatsLogic::unassignedCenterBreakdown($params, $this->adminId, $this->adminInfo));
}
/** 进线数据明细:与看板「进线数据」同口径 */
public function leadLines()
{
@set_time_limit(120);
$params = $this->request->get();
return $this->data(YejiStatsLogic::leadLineList($params, $this->adminId, $this->adminInfo));
}
}
@@ -1445,6 +1445,203 @@ class YejiStatsLogic
return $byAdmin;
}
/**
* 进线事件查询公共条件(与 countLeads / countLeadsByAdmin 一致)。
*
* @param \think\db\BaseQuery $query
*/
private static function applyLeadEventBaseFilter($query, int $startTs, int $endTs, string $tagFilterId): void
{
$query->where('e.change_type', 'add_external_contact')
->where('e.event_time', 'between', [$startTs, $endTs])
->where('e.user_id', '<>', '');
if ($tagFilterId !== '') {
$tagTable = self::tableWithPrefix('qywx_external_contact_tag');
$query->whereRaw(
"EXISTS (SELECT 1 FROM {$tagTable} t WHERE t.external_userid = e.external_userid AND t.tag_id = ?)",
[$tagFilterId]
);
}
}
/**
* 归属到指定展示「中心」行部门的接待成员企微 userid 列表。
*
* @param array<int, int> $adminToPrimary
*
* @return list<string>
*/
private static function collectWorkWxUserIdsForYejiDeptRow(int $deptRowId, array $adminToPrimary): array
{
$adminIds = [];
foreach ($adminToPrimary as $aid => $pid) {
if ((int) $pid === $deptRowId) {
$adminIds[] = (int) $aid;
}
}
if ($adminIds === []) {
return [];
}
$wids = Db::name('admin')
->whereIn('id', $adminIds)
->whereNull('delete_time')
->column('work_wechat_userid');
$out = [];
foreach ($wids as $w) {
$w = trim((string) $w);
if ($w !== '') {
$out[] = $w;
}
}
return array_values(array_unique($out));
}
/**
* 进线明细:与看板「进线数据」同口径的 add_external_contact 逐条列表。
*
* @param array{
* start_date?: string,
* end_date?: string,
* dept_ids?: int[]|string,
* channel_code?: string,
* tag_id?: string,
* dept_id?: int|string,
* page?: int|string,
* page_size?: int|string
* } $params
*
* @return array{
* start_date: string,
* end_date: string,
* dept_id: int,
* dept_name: string,
* channel_code: string,
* count: int,
* lists: list<array<string, mixed>>,
* note: string
* }
*/
public static function leadLineList(array $params, int $viewerAdminId = 0, array $viewerAdminInfo = []): array
{
$c = self::resolveYejiContext($params);
if ($viewerAdminId > 0) {
self::applyYejiDataScope($c, $viewerAdminId, $viewerAdminInfo);
}
$deptId = (int) ($params['dept_id'] ?? 0);
$empty = static function (array $c, int $deptId, string $deptName, string $note) {
return [
'start_date' => (string) $c['startDate'],
'end_date' => (string) $c['endDate'],
'dept_id' => $deptId,
'dept_name' => $deptName,
'channel_code' => (string) $c['channelCode'],
'count' => 0,
'lists' => [],
'note' => $note,
];
};
if ($deptId <= 0) {
return $empty($c, 0, '', '未归属中心无进线明细(看板该行进线为 0)。');
}
if (!in_array($deptId, $c['tableRowDeptIds'], true)) {
return $empty(
$c,
$deptId,
self::formatYejiDeptRowDisplayName($deptId, $c['deptById']),
'该部门不在当前展示部门筛选或数据权限范围内。'
);
}
$deptNameMap = $c['deptNameMap'];
$channelCode = (string) $c['channelCode'];
if ($channelCode !== '' && self::isErCenterDisplayPrimaryName((string) ($deptNameMap[$deptId] ?? ''))) {
return $empty(
$c,
$deptId,
self::formatYejiDeptRowDisplayName($deptId, $c['deptById']),
'与看板一致:名称含「二中心」的展示部门在选定渠道下进线计 0,无明细。'
);
}
$startTs = (int) $c['startTs'];
$endTs = (int) $c['endTs'];
$tagFilterId = (string) $c['tagFilterId'];
$page = max(1, (int) ($params['page'] ?? 1));
$limit = min(100, max(1, (int) ($params['page_size'] ?? 20)));
$offset = ($page - 1) * $limit;
$wxUserIds = self::collectWorkWxUserIdsForYejiDeptRow($deptId, $c['adminToPrimary']);
$deptName = self::formatYejiDeptRowDisplayName($deptId, $c['deptById']);
$ecTable = self::tableWithPrefix('qywx_external_contact');
if ($wxUserIds === []) {
return [
'start_date' => (string) $c['startDate'],
'end_date' => (string) $c['endDate'],
'dept_id' => $deptId,
'dept_name' => $deptName,
'channel_code' => $channelCode,
'count' => 0,
'lists' => [],
'note' => '当前部门下没有可映射到接待企微成员的管理员,无进线流水。',
];
}
$countQuery = Db::name('qywx_external_contact_event')->alias('e');
self::applyLeadEventBaseFilter($countQuery, $startTs, $endTs, $tagFilterId);
$countQuery->whereIn('e.user_id', $wxUserIds);
$count = (int) $countQuery->count();
$listQuery = Db::name('qywx_external_contact_event')->alias('e')
->leftJoin('admin a', 'a.work_wechat_userid = e.user_id AND a.delete_time IS NULL')
->leftJoin("{$ecTable} ec", 'ec.external_userid = e.external_userid AND ec.delete_time IS NULL');
self::applyLeadEventBaseFilter($listQuery, $startTs, $endTs, $tagFilterId);
$listQuery->whereIn('e.user_id', $wxUserIds);
$rawList = $listQuery
->field(
'e.id,e.event_time,e.external_userid,e.user_id,e.state,'
. 'a.name AS reception_admin_name,ec.name AS external_contact_name'
)
->order('e.event_time', 'desc')
->order('e.id', 'desc')
->limit($offset, $limit)
->select()
->toArray();
$lists = [];
foreach ($rawList as $rw) {
$ts = (int) ($rw['event_time'] ?? 0);
$lists[] = [
'id' => (int) ($rw['id'] ?? 0),
'event_time' => $ts,
'event_time_text' => $ts > 0 ? date('Y-m-d H:i:s', $ts) : '',
'external_userid' => (string) ($rw['external_userid'] ?? ''),
'user_id' => (string) ($rw['user_id'] ?? ''),
'state' => (string) ($rw['state'] ?? ''),
'reception_admin_name' => (string) ($rw['reception_admin_name'] ?? ''),
'external_contact_name' => (string) ($rw['external_contact_name'] ?? ''),
];
}
$note = '与看板「进线数据」同口径:企微 change_type=add_external_contact,按接待成员归属当前部门行;'
. ($tagFilterId !== '' ? '已按所选渠道标签过滤客户。' : '未选渠道时不限制客户标签。');
return [
'start_date' => (string) $c['startDate'],
'end_date' => (string) $c['endDate'],
'dept_id' => $deptId,
'dept_name' => $deptName,
'channel_code' => $channelCode,
'count' => $count,
'lists' => $lists,
'note' => $note,
];
}
/**
* 与 sumPerformance 对应,按 admin 维度返回全量/渠道业绩金额(不按部门折叠)。
*
+5 -4
View File
@@ -15,7 +15,7 @@ use think\console\Output;
* 使用方法:
* php think express:auto-update
*
* 配置crontab(每10分钟执行一次):
* 配置 crontab(每 10 分钟):拉快递 100 + 按履约「已发货/已签收」核对释放诊单医助(与是否甘草单无关)
* 0,10,20,30,40,50 * * * * cd /path/to/server && php think express:auto-update >> /dev/null 2>&1
*/
class ExpressAutoUpdate extends Command
@@ -34,6 +34,7 @@ class ExpressAutoUpdate extends Command
try {
$result = ExpressTrackingService::autoUpdateBatch(200);
$recon = ExpressTrackingService::reconcileAssistantReleaseForShippedPrescriptionOrders(200);
$duration = round(microtime(true) - $startTime, 2);
@@ -41,9 +42,9 @@ class ExpressAutoUpdate extends Command
$output->writeln("总数: {$result['total']}");
$output->writeln("成功: {$result['success']}");
$output->writeln("失败: {$result['failed']}");
$output->writeln("医助已移除: " . (int) ($result['assistant_cleared'] ?? 0));
$output->writeln("医助保留(有指派记录): " . (int) ($result['assistant_skipped_assign_log'] ?? 0));
$lines = $result['assistant_lines'] ?? [];
$output->writeln("医助已移除(物流任务+指派日志): " . (int) ($result['assistant_cleared'] ?? 0));
$output->writeln("医助已移除(履约已发货/签收核对): " . (int) ($recon['cleared'] ?? 0) . " (扫描 " . (int) ($recon['scanned'] ?? 0) . " 单)");
$lines = array_merge($result['assistant_lines'] ?? [], $recon['lines'] ?? []);
if ($lines === []) {
$output->writeln('医助明细: (无)');
} else {
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace app\command;
use app\common\service\ExpressTrackingService;
use app\common\service\gancao\GancaoLogisticsRouteService;
use app\common\service\gancao\GancaoScmRecipelService;
use think\console\Command;
@@ -60,6 +61,10 @@ class GancaoSyncLogisticsRoute extends Command
try {
$stats = GancaoLogisticsRouteService::syncBatch($limit, $onlyOrderId);
$recon = ExpressTrackingService::reconcileAssistantReleaseForShippedPrescriptionOrders(200);
$stats['reconcile_cleared'] = (int) ($recon['cleared'] ?? 0);
$stats['reconcile_scanned'] = (int) ($recon['scanned'] ?? 0);
$stats['reconcile_lines'] = $recon['lines'] ?? [];
} catch (\Throwable $e) {
$output->error('同步异常:' . $e->getMessage());
return 1;
@@ -94,9 +99,9 @@ class GancaoSyncLogisticsRoute extends Command
$output->writeln('总数:' . $stats['total']);
$output->writeln('成功:' . $stats['success']);
$output->writeln('失败:' . $stats['failed']);
$output->writeln('医助已移除:' . (int) ($stats['assistant_cleared'] ?? 0));
$output->writeln('医助保留(有指派记录)' . (int) ($stats['assistant_skipped_assign_log'] ?? 0));
$lines = $stats['assistant_lines'] ?? [];
$output->writeln('医助已移除(甘草同步+指派日志)' . (int) ($stats['assistant_cleared'] ?? 0));
$output->writeln('医助已移除(履约已发货/签收核对)' . (int) ($stats['reconcile_cleared'] ?? 0) . '(扫描 ' . (int) ($stats['reconcile_scanned'] ?? 0) . ' 单)');
$lines = array_merge($stats['assistant_lines'] ?? [], $stats['reconcile_lines'] ?? []);
if ($lines === []) {
$output->writeln('医助明细:(无)');
} else {
@@ -12,6 +12,7 @@ use app\common\model\tcm\Diagnosis;
use app\common\model\tcm\DiagnosisAssignLog;
use app\common\model\tcm\PrescriptionOrder;
use app\common\model\tcm\PrescriptionOrderLog;
use think\facade\Db;
use think\facade\Log;
/**
@@ -176,7 +177,7 @@ class ExpressTrackingService
self::logStateChange($tracking, $oldState);
}
// 定时拉轨迹:业务订单已发货时释放诊单医助(有正式指派记录则跳过,与结案逻辑一致)
// 定时拉轨迹:履约已发货/已签收时与甘草侧共用 applyAssistantReleaseForShippedPrescriptionOrder,释放诊单医助并写指派日志
$assistantSync = null;
if ($isAuto) {
$assistantSync = self::maybeClearDiagnosisAssistantWhenShippedPrescription($tracking);
@@ -242,20 +243,19 @@ class ExpressTrackingService
}
/**
* 业务订单已发货(5)/已签收(6)清空关联诊单医助;存在 tcm_diagnosis_assign_log 则跳过
* 供 express:auto-update、gancao:sync-logistics、甘草回调等共用
* 处方业务订单履约「已发货(5) / 已签收(6)」时:清空关联诊单(患者)医助,并写入 tcm_diagnosis_assign_log。
* 快递 100 定时拉轨迹、甘草路由、甘草回调与履约核对共用同一规则
* 例外:指派日志中若存在「从某医助改派到另一医助」(原、新医助 ID 均大于 0)的记录,视为人工指派链,自动释放跳过。
*
* @param array{tracking_id?:int,tracking_number?:string,source?:string} $meta
* @return array{
* action: 'cleared'|'skipped_assign_log',
* action: 'cleared',
* source?: string,
* diagnosis_id: int,
* prescription_order_id: int,
* tracking_id: int,
* tracking_number: string,
* former_assistant_id?: string,
* assistant_id?: string,
* assign_log_count?: int,
* fulfillment_status?: int
* }|null
*/
@@ -284,52 +284,73 @@ class ExpressTrackingService
if (!$diag) {
return null;
}
$oldAssistant = (string) ($diag->getAttr('assistant_id') ?? '');
if ($oldAssistant === '' || $oldAssistant === '0') {
$fromAssistantId = (int) ($diag->getAttr('assistant_id') ?? 0);
if ($fromAssistantId <= 0) {
return null;
}
$assignLogCount = (int) DiagnosisAssignLog::where('diagnosis_id', $diagnosisId)
->whereNull('delete_time')
->count();
if ($assignLogCount > 0) {
return [
'action' => 'skipped_assign_log',
'source' => $source,
'diagnosis_id' => $diagnosisId,
if (self::diagnosisHasFormalAssistantReassignInAssignLog($diagnosisId)) {
Log::info('Skipped auto assistant release (shipped/signed): formal assistant reassign in assign log', [
'diagnosis_id' => $diagnosisId,
'prescription_order_id' => $orderId,
'tracking_id' => $trackingId,
'tracking_number' => $tn,
'assistant_id' => $oldAssistant,
'assign_log_count' => $assignLogCount,
'fulfillment_status' => $fs,
];
'source' => $source,
'current_assistant_id' => $fromAssistantId,
]);
return null;
}
Diagnosis::where('id', $diagnosisId)->whereNull('delete_time')->update(['assistant_id' => '']);
$operatorName = match ($source) {
'express_auto_update' => '系统·物流自动同步',
'gancao_route', 'gancao_route_sync' => '系统·甘草路由同步',
'gancao_callback' => '系统·甘草回调',
'shipped_fulfillment_reconcile' => '系统·已发货履约核对',
default => '系统·订单已发货/签收',
};
Log::info('Cleared diagnosis assistant (shipped/signed prescription order)', [
'source' => $source,
'diagnosis_id' => $diagnosisId,
Db::startTrans();
try {
Diagnosis::where('id', $diagnosisId)->whereNull('delete_time')->update(['assistant_id' => 0]);
DiagnosisAssignLog::create([
'diagnosis_id' => $diagnosisId,
'from_assistant_id' => $fromAssistantId,
'to_assistant_id' => 0,
'operator_admin_id' => 0,
'operator_name' => $operatorName,
'operator_account' => '',
'ip' => '',
'create_time' => time(),
]);
Db::commit();
} catch (\Throwable $e) {
Db::rollback();
throw $e;
}
Log::info('Cleared diagnosis assistant (prescription shipped/signed), assign log written', [
'source' => $source,
'diagnosis_id' => $diagnosisId,
'prescription_order_id' => $orderId,
'tracking_id' => $trackingId,
'fulfillment_status' => $fs,
'former_assistant_id' => $oldAssistant,
'tracking_id' => $trackingId,
'fulfillment_status' => $fs,
'former_assistant_id' => $fromAssistantId,
]);
return [
'action' => 'cleared',
'source' => $source,
'diagnosis_id' => $diagnosisId,
'action' => 'cleared',
'source' => $source,
'diagnosis_id' => $diagnosisId,
'prescription_order_id' => $orderId,
'tracking_id' => $trackingId,
'tracking_number' => $tn,
'former_assistant_id' => $oldAssistant,
'fulfillment_status' => $fs,
'tracking_id' => $trackingId,
'tracking_number' => $tn,
'former_assistant_id' => (string) $fromAssistantId,
'fulfillment_status' => $fs,
];
} catch (\Throwable $e) {
Log::warning('applyAssistantReleaseForShippedPrescriptionOrder failed: ' . $e->getMessage(), [
'source' => $source,
'source' => $source,
'order_id' => $orderId,
]);
@@ -338,19 +359,20 @@ class ExpressTrackingService
}
/**
* @return array{
* action: 'cleared'|'skipped_assign_log',
* source?: string,
* diagnosis_id: int,
* prescription_order_id: int,
* tracking_id: int,
* tracking_number: string,
* former_assistant_id?: string,
* assistant_id?: string,
* assign_log_count?: int,
* fulfillment_status?: int
* }|null
* 是否存在「由原医助改派到新医助」的指派记录(两端 admin 均大于 0)。有此记录则已发货自动释放医助不执行。
*/
private static function diagnosisHasFormalAssistantReassignInAssignLog(int $diagnosisId): bool
{
if ($diagnosisId <= 0) {
return false;
}
return DiagnosisAssignLog::where('diagnosis_id', $diagnosisId)
->where('from_assistant_id', '>', 0)
->where('to_assistant_id', '>', 0)
->count() > 0;
}
private static function maybeClearDiagnosisAssistantWhenShippedPrescription(ExpressTracking $tracking): ?array
{
if ((string) $tracking->order_type !== 'prescription') {
@@ -368,14 +390,14 @@ class ExpressTrackingService
}
return self::applyAssistantReleaseForShippedPrescriptionOrder($order, [
'tracking_id' => (int) $tracking->id,
'tracking_id' => (int) $tracking->id,
'tracking_number' => (string) $tracking->tracking_number,
'source' => 'express_auto_update',
'source' => 'express_auto_update',
]);
} catch (\Throwable $e) {
Log::warning('maybeClearDiagnosisAssistantWhenShippedPrescription failed: ' . $e->getMessage(), [
'tracking_id' => (int) $tracking->id,
'order_id' => $orderId,
'order_id' => $orderId,
]);
return null;
@@ -528,6 +550,54 @@ class ExpressTrackingService
$log->save();
}
/**
* 按处方订单履约核对:已发货(5)/已签收(6) 时诊单仍挂在医助的,一律释放并写指派日志。
* 与快递 100 / 甘草来自哪个物流渠道无关,保障「已发货」后患者侧医助被清掉。
*
* @return array{scanned: int, cleared: int, lines: list<string>}
*/
public static function reconcileAssistantReleaseForShippedPrescriptionOrders(int $limit = 200): array
{
$dTable = (new Diagnosis())->getTable();
$orders = PrescriptionOrder::alias('po')
->join($dTable . ' d', 'd.id = po.diagnosis_id')
->whereNull('po.delete_time')
->whereNull('d.delete_time')
->whereIn('po.fulfillment_status', [5, 6])
->where('po.diagnosis_id', '>', 0)
->where('d.assistant_id', '>', 0)
->order('po.id', 'asc')
->limit($limit)
->field('po.*')
->select();
$cleared = 0;
$lines = [];
foreach ($orders as $order) {
$sync = self::applyAssistantReleaseForShippedPrescriptionOrder($order, [
'source' => 'shipped_fulfillment_reconcile',
'tracking_number' => (string) ($order->tracking_number ?? ''),
]);
if (is_array($sync) && ($sync['action'] ?? '') === 'cleared') {
$cleared++;
$lines[] = sprintf(
'[履约核对·已发货/签收] 诊单=%d 业务订单=%d 运单=%s 原医助ID=%s 履约状态=%d',
(int) ($sync['diagnosis_id'] ?? 0),
(int) ($sync['prescription_order_id'] ?? 0),
(string) ($sync['tracking_number'] ?? ''),
(string) ($sync['former_assistant_id'] ?? ''),
(int) ($sync['fulfillment_status'] ?? 0)
);
}
}
return [
'scanned' => count($orders),
'cleared' => $cleared,
'lines' => $lines,
];
}
/**
* 批量自动更新(定时任务调用)
*
@@ -539,7 +609,7 @@ class ExpressTrackingService
* assistant_cleared: int,
* assistant_skipped_assign_log: int,
* assistant_lines: list<string>
* }
* } assistant_skipped_assign_log 保留兼容恒为 0(已发货/签收一律写指派日志并清空医助)
*/
public static function autoUpdateBatch(int $limit = 50): array
{
@@ -580,7 +650,6 @@ class ExpressTrackingService
$success = 0;
$failed = 0;
$assistantCleared = 0;
$assistantSkippedAssign = 0;
$assistantLines = [];
foreach ($list as $tracking) {
@@ -593,24 +662,13 @@ class ExpressTrackingService
if ($act === 'cleared') {
$assistantCleared++;
$assistantLines[] = sprintf(
'[移除医助] 诊单=%d 业务订单=%d 运单=%s 原医助ID=%s 履约状态=%d',
'[移除医助+指派日志] 诊单=%d 业务订单=%d 运单=%s 原医助ID=%s 履约状态=%d',
(int) ($sync['diagnosis_id'] ?? 0),
(int) ($sync['prescription_order_id'] ?? 0),
(string) ($sync['tracking_number'] ?? ''),
(string) ($sync['former_assistant_id'] ?? ''),
(int) ($sync['fulfillment_status'] ?? 0)
);
} elseif ($act === 'skipped_assign_log') {
$assistantSkippedAssign++;
$assistantLines[] = sprintf(
'[保留医助] 诊单=%d 业务订单=%d 运单=%s 医助ID=%s 指派记录=%d条 履约状态=%d',
(int) ($sync['diagnosis_id'] ?? 0),
(int) ($sync['prescription_order_id'] ?? 0),
(string) ($sync['tracking_number'] ?? ''),
(string) ($sync['assistant_id'] ?? ''),
(int) ($sync['assign_log_count'] ?? 0),
(int) ($sync['fulfillment_status'] ?? 0)
);
}
}
} catch (\Throwable $e) {
@@ -628,7 +686,7 @@ class ExpressTrackingService
'success' => $success,
'failed' => $failed,
'assistant_cleared' => $assistantCleared,
'assistant_skipped_assign_log' => $assistantSkippedAssign,
'assistant_skipped_assign_log' => 0,
'assistant_lines' => $assistantLines,
];
}
@@ -484,24 +484,13 @@ final class GancaoLogisticsRouteService
if ($act === 'cleared') {
$stats['assistant_cleared']++;
$stats['assistant_lines'][] = sprintf(
'[移除医助][甘草路由] 诊单=%d 业务订单=%d 运单=%s 原医助ID=%s 履约状态=%d',
'[移除医助+指派日志][甘草路由] 诊单=%d 业务订单=%d 运单=%s 原医助ID=%s 履约状态=%d',
(int) ($sync['diagnosis_id'] ?? 0),
(int) ($sync['prescription_order_id'] ?? 0),
(string) ($sync['tracking_number'] ?? ''),
(string) ($sync['former_assistant_id'] ?? ''),
(int) ($sync['fulfillment_status'] ?? 0)
);
} elseif ($act === 'skipped_assign_log') {
$stats['assistant_skipped_assign_log']++;
$stats['assistant_lines'][] = sprintf(
'[保留医助][甘草路由] 诊单=%d 业务订单=%d 运单=%s 医助ID=%s 指派记录=%d条 履约状态=%d',
(int) ($sync['diagnosis_id'] ?? 0),
(int) ($sync['prescription_order_id'] ?? 0),
(string) ($sync['tracking_number'] ?? ''),
(string) ($sync['assistant_id'] ?? ''),
(int) ($sync['assign_log_count'] ?? 0),
(int) ($sync['fulfillment_status'] ?? 0)
);
}
}
} else {
@@ -0,0 +1,15 @@
-- 业绩看板「进线数据」明细接口(fans/yeji · pid=293
-- 幂等:按 perms 判重
INSERT INTO `zyt_system_menu`
(`pid`, `type`, `name`, `icon`, `sort`, `perms`, `paths`, `component`, `selected`, `params`, `is_show`, `is_disable`, `create_time`, `update_time`)
SELECT
293, 'A', '业绩-进线明细', '', 10,
'stats.yejiStats/leadLines', '', '', '', '',
1, 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()
FROM DUAL
WHERE EXISTS (SELECT 1 FROM `zyt_system_menu` p WHERE p.id = 293)
AND NOT EXISTS (
SELECT 1 FROM `zyt_system_menu` m
WHERE m.perms = 'stats.yejiStats/leadLines'
);