Merge branch 'master' of https://gitee.com/v_cms/zyt
This commit is contained in:
@@ -249,7 +249,7 @@ class ExpressTrackingService
|
||||
/**
|
||||
* 处方业务订单履约「已发货(5) / 已签收(6)」时:清空关联诊单(患者)医助,并写入 tcm_diagnosis_assign_log(含关联业务订单 creator_id / create_time 快照,便于核对医助创建订单时间)。
|
||||
* 快递 100 定时拉轨迹、甘草路由、甘草回调与履约核对共用同一规则。
|
||||
* 不自动清空的情形仅两种:(1)患者当前医助在「二中心」及其全部子部门(与后台部门树一致)下任职;(2)诊单已有 shipped_non_er_assistant_cleared_at 且 assistant_id 已为 0(视为已处理过,重新指派医助后可再次处理)。
|
||||
* 不自动清空的情形:(1)按业务订单 creator_id(代建场景)或回退身份判断是否在「二中心」部门子树;(2)诊单已有 shipped_non_er_assistant_cleared_at 且 assistant_id 已为 0(视为已处理);(3)指派日志存在后台人工指派:`operator_admin_id>0` 且 `to_assistant_id>0`(系统任务为 operator_admin_id=0);或 related_po_creator_id=from_assistant_id 且 to>0 的旧数据兼容。避免「系统·已发货履约核对」覆盖人工指派链。
|
||||
*
|
||||
* @param array{tracking_id?:int,tracking_number?:string,source?:string} $meta
|
||||
* @return array{
|
||||
@@ -288,36 +288,55 @@ class ExpressTrackingService
|
||||
if (!$diag) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 后台曾人工指派到新医助(operator_admin_id>0 且 to>0,与系统写入的 operator=0 区分)时不再自动清空;另保留快照 related_po=from 的旧数据兼容
|
||||
if (self::diagnosisShouldSkipAutoAssistantReleaseDueToManualAssignLog($diagnosisId)) {
|
||||
Log::info('Skipped auto assistant release (shipped/signed): manual assign history exists (human operator + to_assistant_id>0, or snapshot-tied pattern)', [
|
||||
'diagnosis_id' => $diagnosisId,
|
||||
'prescription_order_id' => $orderId,
|
||||
'source' => $source,
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$shippedClearedAt = (int) ($diag->getAttr('shipped_non_er_assistant_cleared_at') ?? 0);
|
||||
$diagAssistantIdNow = (int) ($diag->getAttr('assistant_id') ?? 0);
|
||||
if ($shippedClearedAt > 0 && $diagAssistantIdNow === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$poCreatorId = (int) ($order->getAttr('creator_id') ?? 0);
|
||||
$rxId = (int) ($order->getAttr('prescription_id') ?? 0);
|
||||
$rxCreator = 0;
|
||||
if ($rxId > 0) {
|
||||
$rxCreator = (int) Prescription::where('id', $rxId)->whereNull('delete_time')->value('creator_id');
|
||||
}
|
||||
|
||||
// 诊单 assistant_id 与列表「医助」筛选一致:医助常代建单但诊单未写 assistant_id,此时用创建人作为待释放身份(创建人即开方医生时不采用,避免误记指派日志)
|
||||
$fromAssistantId = (int) ($diag->getAttr('assistant_id') ?? 0);
|
||||
if ($fromAssistantId <= 0) {
|
||||
$creatorId = (int) ($order->getAttr('creator_id') ?? 0);
|
||||
if ($creatorId > 0) {
|
||||
$rxId = (int) ($order->getAttr('prescription_id') ?? 0);
|
||||
$rxCreator = 0;
|
||||
if ($rxId > 0) {
|
||||
$rxCreator = (int) Prescription::where('id', $rxId)->whereNull('delete_time')->value('creator_id');
|
||||
}
|
||||
if ($rxId <= 0 || $creatorId !== $rxCreator) {
|
||||
$fromAssistantId = $creatorId;
|
||||
}
|
||||
if ($poCreatorId > 0 && ($rxId <= 0 || $poCreatorId !== $rxCreator)) {
|
||||
$fromAssistantId = $poCreatorId;
|
||||
}
|
||||
}
|
||||
if ($fromAssistantId <= 0) {
|
||||
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,
|
||||
/** 是否因「二中心」跳过释放:优先看业务订单创建人 creator_id(代建场景);创建人即开方医生时用 fromAssistantId */
|
||||
$erZhongxinCheckAdminId = ($poCreatorId > 0 && ($rxId <= 0 || $poCreatorId !== $rxCreator))
|
||||
? $poCreatorId
|
||||
: $fromAssistantId;
|
||||
|
||||
if ($erZhongxinCheckAdminId > 0 && self::currentAssistantBelongsToErzhongxinDeptTree($erZhongxinCheckAdminId)) {
|
||||
Log::info('Skipped auto assistant release (shipped/signed): order creator (or fallback from-id) under 二中心 dept tree', [
|
||||
'diagnosis_id' => $diagnosisId,
|
||||
'prescription_order_id' => $orderId,
|
||||
'source' => $source,
|
||||
'er_zhongxin_check_admin_id' => $erZhongxinCheckAdminId,
|
||||
'prescription_order_creator_id' => $poCreatorId,
|
||||
'from_assistant_id_for_log' => $fromAssistantId,
|
||||
]);
|
||||
|
||||
return null;
|
||||
@@ -440,6 +459,39 @@ class ExpressTrackingService
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 若诊单曾被后台人工指派到具体医助,则不再执行发运/签收自动清空(避免「系统·已发货履约核对」覆盖人工链)。
|
||||
* 判定:operator_admin_id>0 且 to_assistant_id>0(人工);系统写入的释放/同步为 operator_admin_id=0。
|
||||
* 另保留 related_po_creator_id=from_assistant_id 且 to>0 的窄条件,兼容早期数据。
|
||||
*/
|
||||
private static function diagnosisShouldSkipAutoAssistantReleaseDueToManualAssignLog(int $diagnosisId): bool
|
||||
{
|
||||
if ($diagnosisId <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$tbl = 'tcm_diagnosis_assign_log';
|
||||
|
||||
$humanReassign = (int) Db::name($tbl)
|
||||
->where('diagnosis_id', $diagnosisId)
|
||||
->where('to_assistant_id', '>', 0)
|
||||
->where('operator_admin_id', '>', 0)
|
||||
->count();
|
||||
if ($humanReassign > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$snapshotTied = (int) Db::name($tbl)
|
||||
->where('diagnosis_id', $diagnosisId)
|
||||
->where('to_assistant_id', '>', 0)
|
||||
->where('from_assistant_id', '>', 0)
|
||||
->where('related_po_creator_id', '>', 0)
|
||||
->whereRaw('`related_po_creator_id` = `from_assistant_id`')
|
||||
->count();
|
||||
|
||||
return $snapshotTied > 0;
|
||||
}
|
||||
|
||||
private static function maybeClearDiagnosisAssistantWhenShippedPrescription(ExpressTracking $tracking): ?array
|
||||
{
|
||||
if ((string) $tracking->order_type !== 'prescription') {
|
||||
@@ -619,7 +671,7 @@ class ExpressTrackingService
|
||||
|
||||
/**
|
||||
* 按处方订单履约核对:已发货(5)/已签收(6) 时符合条件的订单调用 applyAssistantReleaseForShippedPrescriptionOrder。
|
||||
* 与快递 100 / 甘草来自哪个物流渠道无关;是否清空医助以该方法为准(仅二中心不释放;已 shipped 释放且 assistant_id=0 视为已处理)。
|
||||
* 与快递 100 / 甘草来自哪个物流渠道无关;是否清空医助以该方法为准(**二中心豁免看业务单创建人**参见 apply;已 shipped 释放且 assistant_id=0 视为已处理)。
|
||||
*
|
||||
* @return array{scanned: int, cleared: int, lines: list<string>}
|
||||
*/
|
||||
@@ -810,4 +862,237 @@ class ExpressTrackingService
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 与 PrescriptionOrderLogic::logisticsTrace 一致:优先库表 express_tracking + express_trace;
|
||||
* 库内算不出签收时(或库无记录),可选走快递100(与详情「刷新轨迹」同源)。
|
||||
*
|
||||
* 提成结算 orderLines 等对 SQL JOIN 无法覆盖「仅 API 有轨迹」的运单做补算。
|
||||
*
|
||||
* @param bool $queryKuaidiWhenDbUnusable true 时与 logisticsTrace 完全同路径;false 时仅数据库推导(overview 大批量避免 HTTP)
|
||||
*/
|
||||
public static function resolveSignUnixTimeForCommission(
|
||||
string $trackingNumber,
|
||||
string $expressCompany,
|
||||
string $recipientPhone,
|
||||
bool $queryKuaidiWhenDbUnusable = true
|
||||
): int {
|
||||
$num = trim($trackingNumber);
|
||||
if ($num === '') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$cacheKey = $num . '|' . ($queryKuaidiWhenDbUnusable ? '1' : '0');
|
||||
static $memo = [];
|
||||
if (isset($memo[$cacheKey])) {
|
||||
return $memo[$cacheKey];
|
||||
}
|
||||
|
||||
$detail = self::loadTrackingDetailNormalized($num);
|
||||
if (is_array($detail)) {
|
||||
$fromDb = self::effectiveSignUnixFromTrackingDetail($detail);
|
||||
if ($fromDb > 0) {
|
||||
$memo[$cacheKey] = $fromDb;
|
||||
|
||||
return $fromDb;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$queryKuaidiWhenDbUnusable) {
|
||||
$memo[$cacheKey] = 0;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
$ec = strtolower(trim($expressCompany));
|
||||
if ($ec === '') {
|
||||
$ec = 'auto';
|
||||
}
|
||||
$api = ExpressTrackService::query($ec, $num, $recipientPhone, '');
|
||||
$fromApi = self::effectiveSignUnixFromKuaidiPayload($api);
|
||||
$memo[$cacheKey] = $fromApi;
|
||||
|
||||
return $fromApi;
|
||||
}
|
||||
|
||||
/**
|
||||
* 单号精确匹配失败时尝试 TRIM(TrackingNumber)(历史数据首尾空格)。
|
||||
*
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
private static function loadTrackingDetailNormalized(string $trackingNumber): ?array
|
||||
{
|
||||
$n = trim($trackingNumber);
|
||||
if ($n === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$first = self::getDetailByTrackingNumber($n);
|
||||
if ($first !== null) {
|
||||
return $first;
|
||||
}
|
||||
|
||||
$tracking = ExpressTracking::with(['traces' => function ($query) {
|
||||
$query->order('trace_time_stamp', 'desc');
|
||||
}])
|
||||
->whereRaw('TRIM(`tracking_number`) = ?', [$n])
|
||||
->whereNull('delete_time')
|
||||
->find();
|
||||
|
||||
if (!$tracking) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$data = $tracking->toArray();
|
||||
$data['traces'] = $tracking->traces ? $tracking->traces->toArray() : [];
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 与 YejiStatsLogic 物流子查询中每条 express_tracking 的 GREATEST 语义对齐。
|
||||
*
|
||||
* @param array<string, mixed> $detail getDetail* / loadTrackingDetailNormalized 结果
|
||||
*/
|
||||
private static function effectiveSignUnixFromTrackingDetail(array $detail): int
|
||||
{
|
||||
$a = (int) ($detail['sign_time'] ?? 0);
|
||||
|
||||
$traces = $detail['traces'] ?? [];
|
||||
if (!is_array($traces)) {
|
||||
$traces = [];
|
||||
}
|
||||
|
||||
$b = 0;
|
||||
foreach ($traces as $tr) {
|
||||
if (!is_array($tr)) {
|
||||
continue;
|
||||
}
|
||||
if (!self::dbTraceRowLooksSigned($tr)) {
|
||||
continue;
|
||||
}
|
||||
$ts = self::traceRowToUnix($tr);
|
||||
if ($ts > 0) {
|
||||
$b = max($b, $ts);
|
||||
}
|
||||
}
|
||||
|
||||
$c = 0;
|
||||
$stateOk = ((string) ($detail['current_state'] ?? '') === ExpressTracking::STATE_SIGNED
|
||||
|| (int) ($detail['is_signed'] ?? 0) === 1);
|
||||
if ($stateOk) {
|
||||
foreach ($traces as $tr) {
|
||||
if (!is_array($tr)) {
|
||||
continue;
|
||||
}
|
||||
$ts = self::traceRowToUnix($tr);
|
||||
if ($ts > 0) {
|
||||
$c = max($c, $ts);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return max($a, $b, $c);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $tr express_trace 行
|
||||
*/
|
||||
private static function dbTraceRowLooksSigned(array $tr): bool
|
||||
{
|
||||
$code = (string) ($tr['status_code'] ?? '');
|
||||
if (in_array($code, ['3', '301', '302', '304'], true)) {
|
||||
return true;
|
||||
}
|
||||
$st = (string) ($tr['status'] ?? '');
|
||||
$ctx = (string) ($tr['trace_context'] ?? '');
|
||||
$hay = $st . ' ' . $ctx;
|
||||
foreach (['签收', '妥投', '已送达', '本人签收'] as $k) {
|
||||
if (mb_stripos($hay, $k) !== false) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $tr
|
||||
*/
|
||||
private static function traceRowToUnix(array $tr): int
|
||||
{
|
||||
$ts = (int) ($tr['trace_time_stamp'] ?? 0);
|
||||
if ($ts > 0) {
|
||||
return $ts;
|
||||
}
|
||||
$raw = trim((string) ($tr['trace_time'] ?? ''));
|
||||
if ($raw === '') {
|
||||
return 0;
|
||||
}
|
||||
$p = strtotime($raw);
|
||||
|
||||
return $p !== false ? (int) $p : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $api ExpressTrackService::query 返回值
|
||||
*/
|
||||
private static function effectiveSignUnixFromKuaidiPayload(array $api): int
|
||||
{
|
||||
$traces = $api['traces'] ?? [];
|
||||
if (!is_array($traces) || $traces === []) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$state = (string) ($api['state'] ?? '');
|
||||
$best = 0;
|
||||
|
||||
foreach ($traces as $row) {
|
||||
if (!is_array($row)) {
|
||||
continue;
|
||||
}
|
||||
$tstr = (string) ($row['time'] ?? '');
|
||||
$ctx = (string) ($row['context'] ?? '');
|
||||
$p = strtotime($tstr);
|
||||
$ts = $p !== false ? (int) $p : 0;
|
||||
if ($ts <= 0) {
|
||||
continue;
|
||||
}
|
||||
$signedLike = self::plainContextLooksSigned($ctx);
|
||||
if ($signedLike) {
|
||||
$best = max($best, $ts);
|
||||
}
|
||||
}
|
||||
|
||||
if ($best > 0) {
|
||||
return $best;
|
||||
}
|
||||
|
||||
if ($state === '3') {
|
||||
foreach ($traces as $row) {
|
||||
if (!is_array($row)) {
|
||||
continue;
|
||||
}
|
||||
$tstr = (string) ($row['time'] ?? '');
|
||||
$p = strtotime($tstr);
|
||||
$ts = $p !== false ? (int) $p : 0;
|
||||
if ($ts > 0) {
|
||||
$best = max($best, $ts);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $best;
|
||||
}
|
||||
|
||||
private static function plainContextLooksSigned(string $context): bool
|
||||
{
|
||||
foreach (['签收', '妥投', '已送达', '本人签收', '快件已送达'] as $k) {
|
||||
if (mb_stripos($context, $k) !== false) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user