@@ -0,0 +1,433 @@
< ? php
declare ( strict_types = 1 );
namespace app\mcp\service ;
use app\adminapi\logic\tcm\PrescriptionOrderLogic ;
use app\common\model\ExpressTracking ;
use app\common\service\ExpressTrackingService ;
use app\common\service\ExpressTrackService ;
use think\facade\Db ;
/**
* 物流查询(2026-09-24 增补):zyt_logistics_order 按业务订单号 / 快递单号查一张处方业务订单的物流,
* zyt_logistics_patient 按患者查其最近订单的物流。
* - 订单只从「处方业务订单」列表里找(以调用账号身份执行后台原列表),能查到哪些订单与后台列表完全一致;
* 后台逐单接口(详情、物流轨迹)的 canAccessOrder 对任一订单权限直接放行,这里不用它来判断能不能看。
* - 轨迹需要后台「物流轨迹」权限(tcm.prescriptionOrder/logisticsTrace);没有时只给快递公司、单号和订单状态。
* - 轨迹优先读系统已同步的数据(express_tracking / express_trace,由快递同步任务和甘草、洛阳药房回调写入),在只读事务里查询,
* 并写明这张运单最后一次同步的时间和来源。
* - 系统里还没有这张运单的轨迹时,按需实时查询(live,默认开启):与后台打开订单详情时自动查询的是同一路径
* ( PrescriptionOrderLogic::logisticsTrace → 快递100 / 京东官方接口,不写库),每个账号每小时有次数上限,
* .env [AI_MCP] LOGISTICS_LIVE = false 可关闭。
* - 收件人电话按权限脱敏,只给省市区、不给详细地址;轨迹文字里的手机号同样脱敏。
*/
class LogisticsTools
{
private const TOOLS = [ 'zyt_logistics_order' , 'zyt_logistics_patient' ];
private const ORDER_LIST = 'tcm.prescriptionOrder/lists' ;
private const TRACE_PERM = 'tcm.prescriptionOrder/logisticsTrace' ;
private const FULFILLMENT = [ 1 => '待双审通过' , 2 => '待发货' , 3 => '已完成' , 4 => '已取消' , 5 => '已发货' , 6 => '已签收' , 7 => '进行中' ,
8 => '暂不制药' , 9 => '拒收' , 10 => '退款' , 11 => '保留药方' , 12 => '制药缓发' ];
private const CARRIERS = [ 'sf' => '顺丰速运' , 'jd' => '京东快递' , 'jt' => '极兔速递' , 'yt' => '圆通速递' , 'zt' => '中通快递' , 'yd' => '韵达快递' ,
'st' => '申通快递' , 'yz' => '中国邮政' , 'ems' => 'EMS' ];
private const SOURCES = [ 'kuaidi100' => '快递100' , 'jd_official' => '京东物流官方接口' , 'official_only' => '仅有官网查询链接' , 'gancao' => '甘草药房' ,
'ej_pharmacy' => '洛阳药房' ];
private const SUPPLY = [ 'gancao' => '甘草药房代发' , 'direct' => '洛阳药房直发' ];
private const NOTE = '物流数据来自系统同步(快递接口、甘草/洛阳药房回调)或实时查询;updated_at 是这张运单最后一次同步的时间。'
. '订单已完成、取消或运单已签收后系统不再更新轨迹。tracking_page 是快递公司官网的查询页。' ;
public static function handles ( string $name ) : bool
{
return in_array ( $name , self :: TOOLS , true );
}
/** 能查处方业务订单列表的账号才有这两个工具 */
public static function definitions ( Identity $identity ) : array
{
if ( self :: orderList ( $identity ) === null ) {
return [];
}
$live = [ 'type' => 'boolean' , 'description' => '系统里还没有这张运单的轨迹时,是否实时向快递公司查询(默认是;每小时有次数上限)' ];
return [
Tools :: tool ( 'zyt_logistics_order' , '查一张处方业务订单的物流:快递公司、运单号、订单履约状态、物流状态(在途/派件中/已签收等)、签收时间和物流轨迹。'
. '用业务订单号(如 PO2026…)或快递单号查询;只能查到当前账号在后台「处方业务订单」里看得到的订单。轨迹优先用系统已同步的数据,结果里写明更新时间。' , [
'order_no' => [ 'type' => 'string' , 'description' => '业务订单号,如 PO20260921154848894908' ],
'tracking_no' => [ 'type' => 'string' , 'description' => '快递单号,如 JDVE19085931055(与 order_no 二选一)' ],
'live' => $live ,
'trace_limit' => [ 'type' => 'integer' , 'minimum' => 1 , 'maximum' => 100 , 'description' => '最多返回多少条轨迹,最新的在前(默认 30)' ],
]),
Tools :: tool ( 'zyt_logistics_patient' , '查一位患者最近的处方业务订单物流:每单的快递公司、运单号、履约状态、最新物流动态和是否签收,并给出最近一单已发货订单的完整轨迹。'
. '用患者姓名、手机号或患者ID查询,同名的不同患者会分开列出;只含当前账号在后台看得到的订单。' , [
'patient' => [ 'type' => 'string' , 'description' => '患者姓名或手机号(可以只写一部分)' ],
'patient_id' => [ 'type' => 'integer' , 'description' => '患者ID(诊单上的 patient_id;同名时用它区分)' ],
'start_date' => [ 'type' => 'string' , 'description' => '订单创建日期起 YYYY-MM-DD(可选)' ],
'end_date' => [ 'type' => 'string' , 'description' => '订单创建日期止 YYYY-MM-DD(可选)' ],
'limit' => [ 'type' => 'integer' , 'minimum' => 1 , 'maximum' => 20 , 'description' => '最多列出几单,最新的在前(默认 10)' ],
'live' => $live ,
]),
];
}
public static function call ( Identity $identity , string $name , array $args , array & $audit ) : array
{
$resource = self :: orderList ( $identity );
if ( $resource === null ) {
throw new McpException ( '当前账号不能查询处方业务订单,所以也查不了物流' , 'denied' );
}
$audit [ 'resource' ] = self :: ORDER_LIST ;
return $name === 'zyt_logistics_order' ? self :: order ( $identity , $resource , $args , $audit ) : self :: patient ( $identity , $resource , $args , $audit );
}
private static function orderList ( Identity $identity ) : ? array
{
$resource = Catalog :: get ( self :: ORDER_LIST );
return $resource !== null && Catalog :: denialFor ( $identity , $resource ) === null ? $resource : null ;
}
private static function order ( Identity $identity , array $resource , array $args , array & $audit ) : array
{
$orderNo = self :: code ( $args [ 'order_no' ] ? ? '' , '业务订单号' );
$trackingNo = self :: code ( $args [ 'tracking_no' ] ? ? '' , '快递单号' );
if ( $orderNo === '' && $trackingNo === '' ) {
throw new McpException ( '请提供业务订单号 order_no 或快递单号 tracking_no' , 'invalid' );
}
// 列表里订单号是模糊匹配、快递关键字同时匹配快递公司:取回后再按原值精确筛
$rows = self :: find ( $identity , $resource , $orderNo !== '' ? [ 'order_no' => $orderNo ] : [ 'express_keyword' => $trackingNo ], 20 );
$rows = array_values ( array_filter ( $rows , static fn ( array $row ) => $orderNo !== ''
? strcasecmp ( trim (( string ) ( $row [ 'order_no' ] ? ? '' )), $orderNo ) === 0
: strcasecmp ( trim (( string ) ( $row [ 'tracking_number' ] ? ? '' )), $trackingNo ) === 0 ));
$wanted = $orderNo !== '' ? '业务订单 ' . $orderNo : '快递单号 ' . $trackingNo ;
if ( ! $rows ) {
return self :: result ( $identity , $wanted . ':没有找到,或这张订单不在当前账号「处方业务订单」的可见范围内。' , [ 'found' => 0 ], $audit );
}
$canTrace = $identity -> can ( self :: TRACE_PERM );
$limit = max ( 1 , min ( 100 , ( int ) ( $args [ 'trace_limit' ] ? ? 30 )));
$orders = [];
foreach ( array_slice ( $rows , 0 , 3 ) as $row ) {
$orders [] = self :: withTrace ( $identity , $row , $canTrace , self :: liveWanted ( $args ), $limit );
}
$summary = implode ( " \n " , array_map ([ self :: class , 'line' ], $orders ));
if ( count ( $rows ) > 1 ) {
$summary .= " \n (这个快递单号关联了 " . count ( $rows ) . ' 张订单)' ;
}
return self :: result ( $identity , $summary , [ 'found' => count ( $rows ), 'orders' => $orders , 'note' => self :: NOTE ], $audit , $orders );
}
private static function patient ( Identity $identity , array $resource , array $args , array & $audit ) : array
{
$keyword = mb_substr ( trim (( string ) ( $args [ 'patient' ] ? ? '' )), 0 , 30 );
$patientId = max ( 0 , ( int ) ( $args [ 'patient_id' ] ? ? 0 ));
if ( $keyword === '' && $patientId === 0 ) {
throw new McpException ( '请提供患者姓名或手机号 patient,或患者ID patient_id' , 'invalid' );
}
$filter = $patientId > 0 ? [ 'patient_id' => $patientId ] : [ 'patient_keyword' => $keyword ];
$start = self :: date ( $args [ 'start_date' ] ? ? '' , 'start_date' );
$end = self :: date ( $args [ 'end_date' ] ? ? '' , 'end_date' );
if ( $start !== '' || $end !== '' ) {
// 列表按创建时间筛选时需要起止两端;只给一端时补成一年内
$end = $end !== '' ? $end : date ( 'Y-m-d' );
$start = $start !== '' ? $start : date ( 'Y-m-d' , ( int ) strtotime ( $end . ' -365 days' ));
$filter [ 'start_time' ] = $start . ' 00:00:00' ;
$filter [ 'end_time' ] = $end . ' 23:59:59' ;
}
$rows = self :: find ( $identity , $resource , $filter , max ( 1 , min ( 20 , ( int ) ( $args [ 'limit' ] ? ? 10 ))));
$who = $patientId > 0 ? '患者ID ' . $patientId : '「' . $keyword . '」' ;
if ( ! $rows ) {
return self :: result ( $identity , $who . ':没有找到处方业务订单,或不在当前账号「处方业务订单」的可见范围内。' , [ 'found' => 0 ], $audit );
}
$canTrace = $identity -> can ( self :: TRACE_PERM );
$diagnoses = self :: diagnoses ( array_map ( static fn ( array $row ) => ( int ) ( $row [ 'diagnosis_id' ] ? ? 0 ), $rows ));
$stored = $canTrace ? self :: storedTracking ( array_map ( static fn ( array $row ) => trim (( string ) ( $row [ 'tracking_number' ] ? ? '' )), $rows )) : [];
$groups = [];
$orders = [];
foreach ( $rows as $row ) {
$diagnosis = $diagnoses [( int ) ( $row [ 'diagnosis_id' ] ? ? 0 )] ? ? [];
$key = ( int ) ( $diagnosis [ 'patient_id' ] ? ? 0 ) ? : - ( int ) ( $row [ 'diagnosis_id' ] ? ? 0 );
$groups [ $key ] ? ? = [ 'patient_id' => max ( 0 , $key ), 'patient_name' => ( string ) ( $diagnosis [ 'patient_name' ] ? ? '' ),
'phone' => ( string ) ( $diagnosis [ 'phone' ] ? ? '' ), 'orders' => []];
$order = self :: basic ( $row );
$order [ 'logistics' ] = self :: latest ( $stored [ $order [ 'tracking_number' ]] ? ? null , $order , $canTrace );
$groups [ $key ][ 'orders' ][] = $order ;
$orders [] = $order ;
}
$groups = array_values ( $groups );
$data = [ 'found' => count ( $rows ), 'patients' => $groups , 'note' => self :: NOTE ];
$lines = [];
foreach ( $groups as $group ) {
$lines [] = sprintf ( '患者 %s( ID %s)最近 %d 单:' , $group [ 'patient_name' ] ? : '未知' , $group [ 'patient_id' ] ? : '无' , count ( $group [ 'orders' ]));
foreach ( $group [ 'orders' ] as $order ) {
$lines [] = '- ' . self :: line ( $order );
}
}
if ( count ( $groups ) > 1 ) {
$lines [] = '有 ' . count ( $groups ) . ' 位患者匹配' . $who . ',请确认是哪一位(可用 patient_id 查询)后再看完整轨迹。' ;
} else {
// 只有一位患者:给最近一张有快递单号的订单的完整轨迹
foreach ( $rows as $row ) {
if ( trim (( string ) ( $row [ 'tracking_number' ] ? ? '' )) !== '' ) {
$data [ 'latest_shipment' ] = self :: withTrace ( $identity , $row , $canTrace , self :: liveWanted ( $args ), 30 );
$lines [] = '最近一单的物流:' . self :: line ( $data [ 'latest_shipment' ]);
break ;
}
}
}
return self :: result ( $identity , implode ( " \n " , $lines ), $data , $audit , $orders );
}
/** 以调用账号身份查处方业务订单列表(权限、数据范围与后台一致) */
private static function find ( Identity $identity , array $resource , array $filter , int $size ) : array
{
Tools :: assertQuota ( $identity , $size );
$envelope = Dispatcher :: call ( $identity , $resource , Tools :: listParams ( $resource , Tools :: params ( $resource , $filter , true ), 1 , $size ));
if ( $envelope [ 'code' ] !== 1 ) {
throw new McpException ( Tools :: failText ( $resource , $envelope ), 'denied' );
}
return array_values ( array_filter (( array ) ( $envelope [ 'data' ][ 'lists' ] ? ? []), 'is_array' ));
}
private static function basic ( array $row ) : array
{
$status = ( int ) ( $row [ 'fulfillment_status' ] ? ? 0 );
$company = strtolower ( trim (( string ) ( $row [ 'express_company' ] ? ? '' )));
$carrier = $company !== '' ? ( self :: CARRIERS [ $company ] ? ? $company ) : trim (( string ) ( $row [ 'gancao_shipping_name' ] ? ? '' ));
$region = trim ( implode ( '' , array_map ( static fn ( $k ) => trim (( string ) ( $row [ $k ] ? ? '' )), [ 'shipping_province' , 'shipping_city' , 'shipping_district' ])));
return [
'order_id' => ( int ) ( $row [ 'id' ] ? ? 0 ),
'order_no' => ( string ) ( $row [ 'order_no' ] ? ? '' ),
'created_at' => self :: time ( $row [ 'create_time' ] ? ? 0 ),
'fulfillment_text' => self :: FULFILLMENT [ $status ] ? ? '未知' ,
'supply' => self :: SUPPLY [( string ) ( $row [ 'ship_mode' ] ? ? '' )] ? ? '' ,
'express_company' => $carrier ,
'tracking_number' => trim (( string ) ( $row [ 'tracking_number' ] ? ? '' )),
'recipient_name' => ( string ) ( $row [ 'recipient_name' ] ? ? '' ),
'recipient_phone' => ( string ) ( $row [ 'recipient_phone' ] ? ? '' ),
'recipient_region' => $region ,
];
}
/** 一张订单的完整物流:已同步的轨迹,没有时按需实时查询 */
private static function withTrace ( Identity $identity , array $row , bool $canTrace , bool $live , int $limit ) : array
{
$order = self :: basic ( $row );
$number = $order [ 'tracking_number' ];
if ( $number === '' ) {
$order [ 'logistics' ] = [ 'status' => '还没有快递单号' , 'note' => '订单状态:' . $order [ 'fulfillment_text' ]];
return $order ;
}
$order [ 'tracking_page' ] = self :: trackingPage ( $number , ( string ) ( $row [ 'express_company' ] ? ? '' ));
if ( ! $canTrace ) {
$order [ 'logistics' ] = [ 'status' => '无权查看物流轨迹' , 'note' => '当前账号没有后台「物流轨迹」权限,只能看到快递公司和单号' ];
return $order ;
}
$stored = Dispatcher :: readOnly ( static fn () => ExpressTrackingService :: getDetailByTrackingNumber ( $number ));
if ( $stored && ! empty ( $stored [ 'traces' ])) {
$order [ 'logistics' ] = self :: fromStored ( $stored , $limit );
return $order ;
}
if ( ! $live || ! McpConfig :: logisticsLive ()) {
$order [ 'logistics' ] = [ 'status' => '系统里暂无这张运单的物流记录' , 'note' => '可以打开快递公司官网查询' ];
return $order ;
}
if ( ! RateLimiter :: hit ( 'logistics_live_' . $identity -> adminId , McpConfig :: logisticsLivePerHour (), 3600 )) {
$order [ 'logistics' ] = [ 'status' => '系统里暂无这张运单的物流记录' ,
'note' => '本小时的实时查询次数已用完(每小时 ' . McpConfig :: logisticsLivePerHour () . ' 次),请稍后再试或打开快递公司官网查询' ];
return $order ;
}
// 与后台打开订单详情时相同:库里没有轨迹就向快递100 / 京东官方接口查询,不写库
$payload = Dispatcher :: readOnly ( static fn () => PrescriptionOrderLogic :: logisticsTrace ( $order [ 'order_id' ], '' , $identity -> adminId , $identity -> adminInfo ));
$order [ 'logistics' ] = $payload === null
? [ 'status' => '实时查询失败' , 'note' => PrescriptionOrderLogic :: getError () ? : '快递公司没有返回结果' ]
: self :: fromLive ( $payload , $limit );
return $order ;
}
private static function fromStored ( array $stored , int $limit ) : array
{
$state = ( string ) ( $stored [ 'current_state' ] ? ? '' );
$traces = self :: traces (( array ) $stored [ 'traces' ], 'trace_time' , 'trace_context' , $limit );
$delivered = $state === ExpressTracking :: STATE_SIGNED ;
return self :: compact ([
'status' => self :: stateText ( $state , ( string ) ( $stored [ 'current_state_text' ] ? ? '' )),
'delivered' => $delivered ,
'signed_at' => $delivered ? ( $traces [ 0 ][ 'time' ] ? ? '' ) : '' ,
'carrier' => ( string ) ( $stored [ 'express_company_name' ] ? ? '' ),
'trace_count' => count (( array ) $stored [ 'traces' ]),
'traces' => $traces ,
'updated_at' => self :: time ( $stored [ 'last_query_time' ] ? ? 0 ) ? : self :: time ( $stored [ 'update_time' ] ? ? 0 ),
'source' => self :: SOURCES [( string ) ( $stored [ 'data_source' ] ? ? '' )] ? ? '系统已同步' ,
]);
}
private static function fromLive ( array $payload , int $limit ) : array
{
$traces = self :: traces (( array ) ( $payload [ 'traces' ] ? ? []), 'time' , 'context' , $limit );
if ( ! $traces ) {
return self :: compact ([ 'status' => '快递公司暂无轨迹' , 'note' => trim (( string ) ( $payload [ 'hint' ] ? ? '' )) ? : '可能刚下单还没揽收,或单号与快递公司不匹配' ,
'source' => '实时查询' ]);
}
$state = ( string ) ( $payload [ 'state' ] ? ? '' );
$delivered = $state === ExpressTracking :: STATE_SIGNED ;
return self :: compact ([
'status' => self :: stateText ( $state , ( string ) ( $payload [ 'state_text' ] ? ? '' )) ? : '有轨迹' ,
'delivered' => $delivered ,
'signed_at' => $delivered ? ( $traces [ 0 ][ 'time' ] ? ? '' ) : '' ,
'carrier' => ( string ) ( $payload [ 'carrier_label' ] ? ? '' ),
'trace_count' => count (( array ) $payload [ 'traces' ]),
'traces' => $traces ,
'updated_at' => date ( 'Y-m-d H:i' ),
'source' => '实时查询' ,
]);
}
/** 列表里多张订单只看同步表的最新一条动态(不逐单查轨迹、不实时查询) */
private static function latest ( ? array $tracking , array $order , bool $canTrace ) : array
{
if ( $order [ 'tracking_number' ] === '' ) {
return [ 'status' => '还没有快递单号' ];
}
if ( ! $canTrace ) {
return [ 'status' => '无权查看物流轨迹' ];
}
if ( ! $tracking ) {
return [ 'status' => '系统里暂无这张运单的物流记录' ];
}
$state = ( string ) ( $tracking [ 'current_state' ] ? ? '' );
return self :: compact ([
'status' => self :: stateText ( $state , ( string ) ( $tracking [ 'current_state_text' ] ? ? '' )),
'delivered' => $state === ExpressTracking :: STATE_SIGNED ,
'latest_time' => ( string ) ( $tracking [ 'latest_trace_time' ] ? ? '' ),
'latest' => ( string ) ( $tracking [ 'latest_trace_context' ] ? ? '' ),
'location' => ( string ) ( $tracking [ 'latest_location' ] ? ? '' ),
'updated_at' => self :: time ( $tracking [ 'last_query_time' ] ? ? 0 ) ? : self :: time ( $tracking [ 'update_time' ] ? ? 0 ),
'source' => self :: SOURCES [( string ) ( $tracking [ 'data_source' ] ? ? '' )] ? ? '系统已同步' ,
]);
}
private static function storedTracking ( array $numbers ) : array
{
$numbers = array_values ( array_unique ( array_filter ( $numbers , static fn ( $n ) => $n !== '' )));
if ( ! $numbers ) {
return [];
}
return ( array ) Dispatcher :: readOnly ( static fn () => Db :: name ( 'express_tracking' ) -> whereIn ( 'tracking_number' , $numbers ) -> whereNull ( 'delete_time' )
-> column ( 'tracking_number,express_company_name,current_state,current_state_text,latest_trace_time,latest_trace_context,latest_location,last_query_time,update_time,data_source' , 'tracking_number' ));
}
/** 订单所属诊单上的患者(患者ID、姓名、手机号);这些诊单都属于账号在列表里看得到的订单 */
private static function diagnoses ( array $ids ) : array
{
$ids = array_values ( array_unique ( array_filter ( $ids )));
if ( ! $ids ) {
return [];
}
return ( array ) Dispatcher :: readOnly ( static fn () => Db :: name ( 'tcm_diagnosis' ) -> whereIn ( 'id' , $ids ) -> column ( 'id,patient_id,patient_name,phone' , 'id' ));
}
private static function traces ( array $rows , string $timeKey , string $textKey , int $limit ) : array
{
$out = [];
foreach ( array_slice ( $rows , 0 , $limit ) as $row ) {
if ( ! is_array ( $row )) {
continue ;
}
$out [] = self :: compact ([ 'time' => ( string ) ( $row [ $timeKey ] ? ? $row [ 'ftime' ] ? ? '' ), 'context' => ( string ) ( $row [ $textKey ] ? ? '' ),
'location' => ( string ) ( $row [ 'location' ] ? ? '' )]);
}
return $out ;
}
/** 摘要里的一行:订单号(履约状态);快递 单号;物流状态;签收时间或最新动态;数据更新时间 */
private static function line ( array $order ) : string
{
$logistics = ( array ) ( $order [ 'logistics' ] ? ? []);
$parts = [ $order [ 'order_no' ] . '( ' . $order [ 'fulfillment_text' ] . ') ' ];
if ( $order [ 'tracking_number' ] !== '' ) {
$parts [] = trim ( $order [ 'express_company' ] . ' ' . $order [ 'tracking_number' ]);
}
$parts [] = ( string ) ( $logistics [ 'status' ] ? ? '' );
$latest = $logistics [ 'traces' ][ 0 ] ? ? ( isset ( $logistics [ 'latest' ]) ? [ 'time' => $logistics [ 'latest_time' ] ? ? '' , 'context' => $logistics [ 'latest' ]] : null );
if ( ! empty ( $logistics [ 'signed_at' ])) {
$parts [] = '签收时间 ' . $logistics [ 'signed_at' ];
} elseif ( $latest ) {
$parts [] = '最新动态 ' . trim (( $latest [ 'time' ] ? ? '' ) . ' ' . mb_substr (( string ) ( $latest [ 'context' ] ? ? '' ), 0 , 80 ));
}
if ( ! empty ( $logistics [ 'note' ])) {
$parts [] = $logistics [ 'note' ];
}
if ( ! empty ( $logistics [ 'updated_at' ])) {
$parts [] = '数据更新于 ' . $logistics [ 'updated_at' ] . ( ! empty ( $logistics [ 'source' ]) ? '( ' . $logistics [ 'source' ] . ') ' : '' );
}
return implode ( '; ' , array_filter ( $parts , static fn ( $p ) => $p !== '' ));
}
private static function result ( Identity $identity , string $summary , array $data , array & $audit , array $orders = []) : array
{
$policy = FieldPolicy :: forIdentity ( $identity , 2000 );
$data = $policy -> apply ( $data );
$summary = $policy -> maskFreeText ( $summary );
$ids = array_values ( array_filter ( array_map ( static fn ( array $o ) => ( int ) ( $o [ 'order_id' ] ? ? 0 ), $orders )));
RateLimiter :: addRows ( $identity -> adminId , count ( $ids ));
$audit [ 'result_rows' ] = count ( $ids );
$audit [ 'record_ids' ] = $ids ;
return [
'content' => [[ 'type' => 'text' , 'text' => $summary . " \n " . json_encode ( $data , JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES )]],
'structuredContent' => $data ? : new \stdClass (),
'isError' => false ,
];
}
private static function trackingPage ( string $number , string $company ) : string
{
$urls = ExpressTrackService :: officialUrls ( $number );
$company = strtolower ( trim ( $company ));
if ( ! isset ( $urls [ $company ])) {
$prefix = strtoupper ( substr ( $number , 0 , 2 ));
$company = $prefix === 'SF' ? 'sf' : ( $prefix === 'JD' ? 'jd' : ( $prefix === 'JT' ? 'jt' : '' ));
}
return $urls [ $company ] ? ? '' ;
}
private static function stateText ( string $state , string $text ) : string
{
$text = trim ( $text );
return $text !== '' ? $text : ( $state !== '' ? ExpressTracking :: getStateText ( $state ) : '' );
}
private static function liveWanted ( array $args ) : bool
{
return ! array_key_exists ( 'live' , $args ) || filter_var ( $args [ 'live' ], FILTER_VALIDATE_BOOLEAN , FILTER_NULL_ON_FAILURE ) !== false ;
}
private static function code ( $value , string $label ) : string
{
$value = preg_replace ( '/\s+/u' , '' , ( string ) $value ) ? ? '' ;
if ( $value !== '' && ! preg_match ( '/^[A-Za-z0-9_\-]{4,64}$/' , $value )) {
throw new McpException ( $label . '格式不正确(只能包含字母、数字和短横线)' , 'invalid' );
}
return $value ;
}
private static function date ( $value , string $name ) : string
{
$value = trim (( string ) $value );
if ( $value !== '' && ( ! preg_match ( '/^\d{4}-\d{2}-\d{2}$/' , $value ) || strtotime ( $value ) === false )) {
throw new McpException ( $name . ' 需要 YYYY-MM-DD 格式' , 'invalid' );
}
return $value ;
}
private static function time ( $value ) : string
{
if ( is_numeric ( $value )) {
return ( int ) $value > 0 ? date ( 'Y-m-d H:i' , ( int ) $value ) : '' ;
}
return mb_substr ( trim (( string ) $value ), 0 , 16 );
}
/** 去掉空值(保留 false 和 0) */
private static function compact ( array $data ) : array
{
return array_filter ( $data , static fn ( $v ) => $v !== '' && $v !== null && $v !== []);
}
}