809 lines
30 KiB
PHP
809 lines
30 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace app\common\service;
|
||
|
||
use app\adminapi\logic\dept\DeptLogic;
|
||
use app\common\model\auth\AdminDept;
|
||
use app\common\model\dept\Dept;
|
||
use app\common\model\ExpressTracking;
|
||
use app\common\model\ExpressTrace;
|
||
use app\common\model\ExpressStateLog;
|
||
use app\common\model\ExpressQueryLog;
|
||
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;
|
||
|
||
/**
|
||
* 物流追踪服务(存储和自动更新)
|
||
*/
|
||
class ExpressTrackingService
|
||
{
|
||
/**
|
||
* 创建或更新物流追踪记录
|
||
*
|
||
* @param array $params [
|
||
* 'order_id' => 订单ID,
|
||
* 'order_type' => 订单类型,
|
||
* 'tracking_number' => 快递单号,
|
||
* 'express_company' => 快递公司,
|
||
* 'recipient_phone' => 收件人手机,
|
||
* 'recipient_name' => 收件人姓名,
|
||
* 'recipient_address' => 收件地址,
|
||
* ]
|
||
* @return ExpressTracking|null
|
||
*/
|
||
public static function createOrUpdate(array $params): ?ExpressTracking
|
||
{
|
||
$trackingNumber = trim($params['tracking_number'] ?? '');
|
||
if ($trackingNumber === '') {
|
||
return null;
|
||
}
|
||
|
||
$tracking = ExpressTracking::where('tracking_number', $trackingNumber)
|
||
->whereNull('delete_time')
|
||
->find();
|
||
|
||
$now = time();
|
||
|
||
if (!$tracking) {
|
||
$tracking = new ExpressTracking();
|
||
$tracking->tracking_number = $trackingNumber;
|
||
$tracking->create_time = $now;
|
||
}
|
||
|
||
$tracking->order_id = (int) ($params['order_id'] ?? 0);
|
||
$tracking->order_type = (string) ($params['order_type'] ?? 'prescription');
|
||
$tracking->express_company = (string) ($params['express_company'] ?? 'auto');
|
||
$tracking->recipient_phone = (string) ($params['recipient_phone'] ?? '');
|
||
$tracking->recipient_name = (string) ($params['recipient_name'] ?? '');
|
||
$tracking->recipient_address = (string) ($params['recipient_address'] ?? '');
|
||
$tracking->update_time = $now;
|
||
|
||
// 设置下次更新时间(立即查询)
|
||
if ($tracking->next_update_time === 0) {
|
||
$tracking->next_update_time = $now;
|
||
}
|
||
|
||
$tracking->save();
|
||
|
||
// 立即查询一次物流信息
|
||
self::queryAndUpdate($tracking->id);
|
||
|
||
return $tracking;
|
||
}
|
||
|
||
/**
|
||
* 查询并更新物流信息
|
||
*
|
||
* @param int $trackingId
|
||
* @param bool $isAuto 是否自动查询
|
||
* @return array<string, mixed>|null 快递100 查询结果;自动任务时可能含 _assistant_sync(医助移除/跳过)
|
||
*/
|
||
public static function queryAndUpdate(int $trackingId, bool $isAuto = false): ?array
|
||
{
|
||
$tracking = ExpressTracking::where('id', $trackingId)
|
||
->whereNull('delete_time')
|
||
->find();
|
||
|
||
if (!$tracking) {
|
||
return null;
|
||
}
|
||
|
||
self::backfillRecipientPhoneFromPrescriptionOrder($tracking);
|
||
|
||
$startTime = microtime(true);
|
||
|
||
// 调用快递100查询(顺丰等单号需带收件人手机号后四位,见 ExpressTrackService)
|
||
$result = ExpressTrackService::query(
|
||
$tracking->express_company,
|
||
$tracking->tracking_number,
|
||
(string) $tracking->recipient_phone
|
||
);
|
||
|
||
$responseTime = (int) ((microtime(true) - $startTime) * 1000);
|
||
|
||
// 记录查询日志
|
||
self::logQuery(
|
||
$tracking->tracking_number,
|
||
$tracking->express_company,
|
||
$isAuto ? 'auto' : 'manual',
|
||
$result,
|
||
$responseTime
|
||
);
|
||
|
||
// 更新追踪记录
|
||
$now = time();
|
||
$oldState = $tracking->current_state;
|
||
|
||
$tracking->express_company_name = $result['carrier_label'] ?? '';
|
||
$tracking->current_state = $result['state'] ?? '0';
|
||
$tracking->current_state_text = $result['state_text'] ?? '';
|
||
$tracking->data_source = $result['source'] ?? '';
|
||
$tracking->last_query_time = $now;
|
||
$tracking->query_count = $tracking->query_count + 1;
|
||
|
||
// 更新预计信息
|
||
if (!empty($result['arrivalTime'])) {
|
||
$tracking->estimated_arrival_time = $result['arrivalTime'];
|
||
}
|
||
if (!empty($result['totalTime'])) {
|
||
$tracking->total_time = $result['totalTime'];
|
||
}
|
||
if (!empty($result['remainTime'])) {
|
||
$tracking->remain_time = $result['remainTime'];
|
||
}
|
||
|
||
// 更新路由信息
|
||
if (!empty($result['routeInfo'])) {
|
||
$tracking->route_info = json_encode($result['routeInfo'], JSON_UNESCAPED_UNICODE);
|
||
}
|
||
|
||
// 更新最新轨迹
|
||
if (!empty($result['traces']) && is_array($result['traces'])) {
|
||
$latestTrace = $result['traces'][0];
|
||
$tracking->latest_trace_time = $latestTrace['time'] ?? '';
|
||
$tracking->latest_trace_context = $latestTrace['context'] ?? '';
|
||
$tracking->latest_location = $latestTrace['location'] ?? '';
|
||
|
||
// 保存轨迹明细
|
||
self::saveTraces($tracking->id, $tracking->tracking_number, $result['traces']);
|
||
}
|
||
|
||
// 检查是否签收
|
||
if (ExpressTracking::isFinalState($tracking->current_state)) {
|
||
$tracking->is_signed = 1;
|
||
$tracking->sign_time = $now;
|
||
$tracking->auto_update = 0; // 停止自动更新
|
||
}
|
||
|
||
// 物流为「签收」(STATE_SIGNED) → 自动推进关联业务订单到「已签收」(fulfillment_status=6)
|
||
// 排除「退签/拒签」等其它终态,避免误把退/拒签的订单标成已签收
|
||
if ($tracking->current_state === ExpressTracking::STATE_SIGNED) {
|
||
self::autoMarkOrderAsSigned($tracking);
|
||
}
|
||
|
||
// 计算下次更新时间
|
||
if ($tracking->auto_update == 1 && !ExpressTracking::isFinalState($tracking->current_state)) {
|
||
$tracking->next_update_time = $now + $tracking->update_interval;
|
||
}
|
||
|
||
$tracking->update_time = $now;
|
||
$tracking->save();
|
||
|
||
// 记录状态变更
|
||
if ($oldState !== $tracking->current_state) {
|
||
self::logStateChange($tracking, $oldState);
|
||
}
|
||
|
||
// 定时拉轨迹:履约已发货/已签收时与甘草侧共用 applyAssistantReleaseForShippedPrescriptionOrder,释放诊单医助并写指派日志
|
||
$assistantSync = null;
|
||
if ($isAuto) {
|
||
$assistantSync = self::maybeClearDiagnosisAssistantWhenShippedPrescription($tracking);
|
||
}
|
||
|
||
return $assistantSync !== null ? array_merge($result, ['_assistant_sync' => $assistantSync]) : $result;
|
||
}
|
||
|
||
/**
|
||
* 物流签收时,自动将关联处方业务订单的履约状态推进到「已签收」(6)
|
||
*
|
||
* 仅在当前 fulfillment_status 属于「待双审通过(1)/待发货(2)/已发货(5)」时升级,
|
||
* 避免覆盖「已完成(3)/已取消(4)/已签收(6)/拒收(9)/退款(10)」等业务终态。
|
||
*/
|
||
private static function autoMarkOrderAsSigned(ExpressTracking $tracking): void
|
||
{
|
||
if ((string) $tracking->order_type !== 'prescription') {
|
||
return;
|
||
}
|
||
$orderId = (int) $tracking->order_id;
|
||
if ($orderId <= 0) {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
$order = PrescriptionOrder::where('id', $orderId)->whereNull('delete_time')->find();
|
||
if (!$order) {
|
||
return;
|
||
}
|
||
|
||
$currentFs = (int) $order->fulfillment_status;
|
||
if (!in_array($currentFs, [1, 2, 5], true)) {
|
||
return;
|
||
}
|
||
|
||
$order->fulfillment_status = 6;
|
||
$order->save();
|
||
|
||
try {
|
||
$log = new PrescriptionOrderLog();
|
||
$log->prescription_order_id = $orderId;
|
||
$log->admin_id = 0;
|
||
$log->admin_name = '系统';
|
||
$log->action = 'auto_sign';
|
||
$log->summary = mb_substr(
|
||
'物流签收(运单:' . (string) $tracking->tracking_number . '),订单状态自动更新为「已签收」',
|
||
0,
|
||
500
|
||
);
|
||
$log->create_time = time();
|
||
$log->save();
|
||
} catch (\Throwable $e) {
|
||
Log::warning('Auto sign order log save failed: ' . $e->getMessage(), [
|
||
'order_id' => $orderId,
|
||
]);
|
||
}
|
||
} catch (\Throwable $e) {
|
||
Log::warning('Auto sign order failed: ' . $e->getMessage(), [
|
||
'tracking_id' => (int) $tracking->id,
|
||
'order_id' => $orderId,
|
||
]);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处方业务订单履约「已发货(5) / 已签收(6)」时:清空关联诊单(患者)医助,并写入 tcm_diagnosis_assign_log(含关联业务订单 creator_id / create_time 快照,便于核对医助创建订单时间)。
|
||
* 快递 100 定时拉轨迹、甘草路由、甘草回调与履约核对共用同一规则。
|
||
* 例外:指派日志中若存在「从某医助改派到另一医助」(原、新医助 ID 均大于 0)的记录,视为人工指派链,自动释放跳过。
|
||
* 例外:患者当前医助在「二中心」及其全部子部门(与后台部门树一致)下任职时,不自动清空医助(按 admin_dept 与 dept 树判定)。
|
||
*
|
||
* @param array{tracking_id?:int,tracking_number?:string,source?:string} $meta
|
||
* @return array{
|
||
* action: 'cleared',
|
||
* source?: string,
|
||
* diagnosis_id: int,
|
||
* prescription_order_id: int,
|
||
* tracking_id: int,
|
||
* tracking_number: string,
|
||
* former_assistant_id?: string,
|
||
* fulfillment_status?: int
|
||
* }|null
|
||
*/
|
||
public static function applyAssistantReleaseForShippedPrescriptionOrder(PrescriptionOrder $order, array $meta = []): ?array
|
||
{
|
||
$orderId = (int) $order->id;
|
||
if ($orderId <= 0) {
|
||
return null;
|
||
}
|
||
$source = (string) ($meta['source'] ?? '');
|
||
$trackingId = (int) ($meta['tracking_id'] ?? 0);
|
||
$tn = (string) ($meta['tracking_number'] ?? $order->tracking_number ?? '');
|
||
|
||
try {
|
||
$fs = (int) $order->fulfillment_status;
|
||
if (!in_array($fs, [5, 6], true)) {
|
||
return null;
|
||
}
|
||
|
||
$diagnosisId = (int) ($order->diagnosis_id ?? 0);
|
||
if ($diagnosisId <= 0) {
|
||
return null;
|
||
}
|
||
|
||
$diag = Diagnosis::where('id', $diagnosisId)->whereNull('delete_time')->find();
|
||
if (!$diag) {
|
||
return null;
|
||
}
|
||
$fromAssistantId = (int) ($diag->getAttr('assistant_id') ?? 0);
|
||
if ($fromAssistantId <= 0) {
|
||
return null;
|
||
}
|
||
|
||
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,
|
||
'source' => $source,
|
||
'current_assistant_id' => $fromAssistantId,
|
||
]);
|
||
|
||
return null;
|
||
}
|
||
|
||
if (self::currentAssistantBelongsToErzhongxinDeptTree($fromAssistantId)) {
|
||
Log::info('Skipped auto assistant release (shipped/signed): assistant under 二中心 dept tree', [
|
||
'diagnosis_id' => $diagnosisId,
|
||
'prescription_order_id' => $orderId,
|
||
'source' => $source,
|
||
'current_assistant_id' => $fromAssistantId,
|
||
]);
|
||
|
||
return null;
|
||
}
|
||
|
||
$operatorName = match ($source) {
|
||
'express_auto_update' => '系统·物流自动同步',
|
||
'gancao_route', 'gancao_route_sync' => '系统·甘草路由同步',
|
||
'gancao_callback' => '系统·甘草回调',
|
||
'shipped_fulfillment_reconcile' => '系统·已发货履约核对',
|
||
default => '系统·订单已发货/签收',
|
||
};
|
||
|
||
$relatedPoCreatorId = (int) ($order->getAttr('creator_id') ?? 0);
|
||
$relatedPoCreateTime = (int) ($order->getAttr('create_time') ?? 0);
|
||
|
||
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' => '',
|
||
'related_po_creator_id' => $relatedPoCreatorId,
|
||
'related_po_create_time' => $relatedPoCreateTime,
|
||
'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' => $fromAssistantId,
|
||
'related_po_creator_id' => $relatedPoCreatorId,
|
||
'related_po_create_time' => $relatedPoCreateTime,
|
||
'assistant_created_order' => ($relatedPoCreatorId === $fromAssistantId && $relatedPoCreateTime > 0) ? 1 : 0,
|
||
]);
|
||
|
||
return [
|
||
'action' => 'cleared',
|
||
'source' => $source,
|
||
'diagnosis_id' => $diagnosisId,
|
||
'prescription_order_id' => $orderId,
|
||
'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,
|
||
'order_id' => $orderId,
|
||
]);
|
||
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 部门名称为「二中心」的根节点 id(启用、未删除的首条),无则 0。
|
||
* 与后台 adminapi 部门数据源一致(表 zyt_dept,树由 pid 构成)。
|
||
*/
|
||
private static function getErzhongxinRootDeptId(): int
|
||
{
|
||
static $cached = null;
|
||
if ($cached !== null) {
|
||
return $cached;
|
||
}
|
||
$id = (int) Dept::where('name', '二中心')
|
||
->where('status', 1)
|
||
->whereNull('delete_time')
|
||
->order('id', 'asc')
|
||
->value('id');
|
||
$cached = $id > 0 ? $id : 0;
|
||
|
||
return $cached;
|
||
}
|
||
|
||
/**
|
||
* 诊单当前医助(管理员)是否归属「二中心」或其任意下级部门(admin_dept.dept_id 落在该子树内)。
|
||
* 与 DeptLogic::getSelfAndDescendantIds 行为对齐于部门列表接口的树展开。
|
||
*/
|
||
private static function currentAssistantBelongsToErzhongxinDeptTree(int $assistantAdminId): bool
|
||
{
|
||
if ($assistantAdminId <= 0) {
|
||
return false;
|
||
}
|
||
$rootId = self::getErzhongxinRootDeptId();
|
||
if ($rootId <= 0) {
|
||
return false;
|
||
}
|
||
$treeIds = DeptLogic::getSelfAndDescendantIds($rootId);
|
||
if ($treeIds === []) {
|
||
return false;
|
||
}
|
||
$allowed = array_flip(array_map('intval', $treeIds));
|
||
$deptIds = AdminDept::where('admin_id', $assistantAdminId)->column('dept_id');
|
||
foreach ($deptIds as $did) {
|
||
if (isset($allowed[(int) $did])) {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* 是否存在「由原医助改派到新医助」的指派记录(两端 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') {
|
||
return null;
|
||
}
|
||
$orderId = (int) $tracking->order_id;
|
||
if ($orderId <= 0) {
|
||
return null;
|
||
}
|
||
|
||
try {
|
||
$order = PrescriptionOrder::where('id', $orderId)->whereNull('delete_time')->find();
|
||
if (!$order) {
|
||
return null;
|
||
}
|
||
|
||
return self::applyAssistantReleaseForShippedPrescriptionOrder($order, [
|
||
'tracking_id' => (int) $tracking->id,
|
||
'tracking_number' => (string) $tracking->tracking_number,
|
||
'source' => 'express_auto_update',
|
||
]);
|
||
} catch (\Throwable $e) {
|
||
Log::warning('maybeClearDiagnosisAssistantWhenShippedPrescription failed: ' . $e->getMessage(), [
|
||
'tracking_id' => (int) $tracking->id,
|
||
'order_id' => $orderId,
|
||
]);
|
||
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 物流表未存收件人手机时,从关联业务订单回填(顺丰/京东等查件必填后四位)
|
||
*/
|
||
private static function backfillRecipientPhoneFromPrescriptionOrder(ExpressTracking $tracking): void
|
||
{
|
||
$current = trim((string) $tracking->recipient_phone);
|
||
if ($current !== '') {
|
||
return;
|
||
}
|
||
if ((string) $tracking->order_type !== 'prescription' || (int) $tracking->order_id <= 0) {
|
||
return;
|
||
}
|
||
$po = PrescriptionOrder::where('id', (int) $tracking->order_id)
|
||
->whereNull('delete_time')
|
||
->find();
|
||
if ($po === null) {
|
||
return;
|
||
}
|
||
$p = trim((string) ($po->recipient_phone ?? ''));
|
||
if ($p !== '') {
|
||
$tracking->recipient_phone = $p;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 保存轨迹明细
|
||
*/
|
||
private static function saveTraces(int $trackingId, string $trackingNumber, array $traces): void
|
||
{
|
||
foreach ($traces as $trace) {
|
||
$traceTime = $trace['time'] ?? $trace['ftime'] ?? '';
|
||
if ($traceTime === '') {
|
||
continue;
|
||
}
|
||
|
||
// 检查是否已存在
|
||
$exists = ExpressTrace::where('tracking_id', $trackingId)
|
||
->where('trace_time', $traceTime)
|
||
->where('trace_context', $trace['context'] ?? '')
|
||
->count();
|
||
|
||
if ($exists > 0) {
|
||
continue;
|
||
}
|
||
|
||
$model = new ExpressTrace();
|
||
$model->tracking_id = $trackingId;
|
||
$model->tracking_number = $trackingNumber;
|
||
$model->trace_time = $traceTime;
|
||
$model->trace_time_stamp = strtotime($traceTime) ?: time();
|
||
$model->trace_context = $trace['context'] ?? '';
|
||
$model->status = $trace['status'] ?? '';
|
||
$model->status_code = $trace['statusCode'] ?? '';
|
||
$model->location = $trace['location'] ?? '';
|
||
$model->area_code = $trace['areaCode'] ?? '';
|
||
$model->area_name = $trace['areaName'] ?? '';
|
||
$model->area_center = $trace['areaCenter'] ?? '';
|
||
$model->extra_data = json_encode($trace, JSON_UNESCAPED_UNICODE);
|
||
$model->create_time = time();
|
||
$model->save();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 记录状态变更
|
||
*/
|
||
private static function logStateChange(ExpressTracking $tracking, string $oldState): void
|
||
{
|
||
$log = new ExpressStateLog();
|
||
$log->tracking_id = $tracking->id;
|
||
$log->tracking_number = $tracking->tracking_number;
|
||
$log->old_state = $oldState;
|
||
$log->old_state_text = ExpressTracking::getStateText($oldState);
|
||
$log->new_state = $tracking->current_state;
|
||
$log->new_state_text = $tracking->current_state_text;
|
||
$log->change_time = time();
|
||
$log->change_reason = $tracking->latest_trace_context;
|
||
$log->create_time = time();
|
||
$log->save();
|
||
|
||
// 检查是否需要通知
|
||
$shouldNotify = false;
|
||
|
||
// 签收通知
|
||
if ($tracking->notify_on_sign == 1 && ExpressTracking::isFinalState($tracking->current_state)) {
|
||
$shouldNotify = true;
|
||
}
|
||
|
||
// 异常通知
|
||
if ($tracking->notify_on_problem == 1 && ExpressTracking::isProblemState($tracking->current_state)) {
|
||
$shouldNotify = true;
|
||
}
|
||
|
||
if ($shouldNotify) {
|
||
self::sendNotification($tracking, $log);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 发送通知(可扩展对接企业微信、短信等)
|
||
*/
|
||
private static function sendNotification(ExpressTracking $tracking, ExpressStateLog $log): void
|
||
{
|
||
// TODO: 对接通知渠道
|
||
// 1. 企业微信消息
|
||
// 2. 短信通知
|
||
// 3. 系统内通知
|
||
|
||
Log::info('Express state changed notification', [
|
||
'tracking_number' => $tracking->tracking_number,
|
||
'old_state' => $log->old_state_text,
|
||
'new_state' => $log->new_state_text,
|
||
'context' => $log->change_reason,
|
||
]);
|
||
|
||
$log->is_notified = 1;
|
||
$log->notify_time = time();
|
||
$log->notify_result = 'logged';
|
||
$log->save();
|
||
}
|
||
|
||
/**
|
||
* 记录查询日志
|
||
*/
|
||
private static function logQuery(
|
||
string $trackingNumber,
|
||
string $expressCompany,
|
||
string $queryType,
|
||
array $result,
|
||
int $responseTime
|
||
): void {
|
||
$log = new ExpressQueryLog();
|
||
$log->tracking_number = $trackingNumber;
|
||
$log->express_company = $expressCompany;
|
||
$log->query_type = $queryType;
|
||
$log->query_source = $result['source'] ?? '';
|
||
$log->query_time = time();
|
||
$log->is_success = !empty($result['traces']) ? 1 : 0;
|
||
$log->error_code = $result['hint'] ? '400' : '';
|
||
$log->error_message = $result['hint'] ?? '';
|
||
$log->response_time = $responseTime;
|
||
$log->trace_count = count($result['traces'] ?? []);
|
||
$log->create_time = time();
|
||
$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,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 批量自动更新(定时任务调用)
|
||
*
|
||
* @param int $limit 每次处理数量
|
||
* @return array{
|
||
* total: int,
|
||
* success: int,
|
||
* failed: int,
|
||
* 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
|
||
{
|
||
$now = time();
|
||
$poTable = (new PrescriptionOrder())->getTable();
|
||
|
||
// 查询需要更新的记录
|
||
// 条件:
|
||
// 1. auto_update = 1(启用自动更新)
|
||
// 2. next_update_time <= now(到达更新时间)
|
||
// 3. is_signed = 0(未签收)
|
||
// 4. current_state 不是终态(排除已签收、退签、拒签)
|
||
// 5. 关联业务订单 tcm_prescription_order:已存在甘草单号 gancao_reciperl_order_no 的不再走本任务(由甘草侧物流拉取)
|
||
// 6. 关联业务订单 fulfillment_status 不在终态白名单:3/4/6/8/9/10/11/12(已完成/已取消/已签收/暂不制药/拒收/退款/保留药方/制药缓发)
|
||
$list = ExpressTracking::alias('et')
|
||
->leftJoin("{$poTable} po", "et.order_id = po.id AND et.order_type = 'prescription'")
|
||
->where('et.auto_update', 1)
|
||
->where('et.next_update_time', '<=', $now)
|
||
->where('et.is_signed', 0)
|
||
->whereNotIn('et.current_state', [
|
||
ExpressTracking::STATE_SIGNED,
|
||
ExpressTracking::STATE_RETURN_SIGNED,
|
||
ExpressTracking::STATE_REJECTED,
|
||
])
|
||
->whereNull('et.delete_time')
|
||
->whereRaw(
|
||
"(et.order_type <> ? OR TRIM(COALESCE(po.gancao_reciperl_order_no, '')) = '')",
|
||
['prescription']
|
||
)
|
||
->whereRaw(
|
||
"(et.order_type <> ? OR po.id IS NULL OR po.fulfillment_status NOT IN (3,4,6,8,9,10,11,12))",
|
||
['prescription']
|
||
)
|
||
->field('et.*')
|
||
->limit($limit)
|
||
->select();
|
||
|
||
$success = 0;
|
||
$failed = 0;
|
||
$assistantCleared = 0;
|
||
$assistantLines = [];
|
||
|
||
foreach ($list as $tracking) {
|
||
try {
|
||
$ret = self::queryAndUpdate($tracking->id, true);
|
||
$success++;
|
||
if (is_array($ret) && isset($ret['_assistant_sync']) && is_array($ret['_assistant_sync'])) {
|
||
$sync = $ret['_assistant_sync'];
|
||
$act = (string) ($sync['action'] ?? '');
|
||
if ($act === 'cleared') {
|
||
$assistantCleared++;
|
||
$assistantLines[] = 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)
|
||
);
|
||
}
|
||
}
|
||
} catch (\Throwable $e) {
|
||
$failed++;
|
||
Log::error('Auto update express tracking failed', [
|
||
'tracking_id' => $tracking->id,
|
||
'tracking_number' => $tracking->tracking_number,
|
||
'error' => $e->getMessage(),
|
||
]);
|
||
}
|
||
}
|
||
|
||
return [
|
||
'total' => count($list),
|
||
'success' => $success,
|
||
'failed' => $failed,
|
||
'assistant_cleared' => $assistantCleared,
|
||
'assistant_skipped_assign_log' => 0,
|
||
'assistant_lines' => $assistantLines,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 获取追踪详情(包含轨迹)
|
||
*/
|
||
public static function getDetail(int $trackingId): ?array
|
||
{
|
||
$tracking = ExpressTracking::with(['traces', 'stateLogs'])
|
||
->where('id', $trackingId)
|
||
->whereNull('delete_time')
|
||
->find();
|
||
|
||
if (!$tracking) {
|
||
return null;
|
||
}
|
||
|
||
$data = $tracking->toArray();
|
||
$data['traces'] = $tracking->traces ? $tracking->traces->toArray() : [];
|
||
$data['state_logs'] = $tracking->stateLogs ? $tracking->stateLogs->toArray() : [];
|
||
|
||
return $data;
|
||
}
|
||
|
||
/**
|
||
* 根据快递单号获取追踪详情(包含轨迹)
|
||
*/
|
||
public static function getDetailByTrackingNumber(string $trackingNumber): ?array
|
||
{
|
||
$tracking = ExpressTracking::with(['traces' => function($query) {
|
||
// 按时间倒序排列(最新的在前)
|
||
$query->order('trace_time_stamp', 'desc');
|
||
}])
|
||
->where('tracking_number', $trackingNumber)
|
||
->whereNull('delete_time')
|
||
->find();
|
||
|
||
if (!$tracking) {
|
||
return null;
|
||
}
|
||
|
||
$data = $tracking->toArray();
|
||
$data['traces'] = $tracking->traces ? $tracking->traces->toArray() : [];
|
||
|
||
return $data;
|
||
}
|
||
}
|