where('id', $localId)->lock(true)->find(); $remote = Db::name('ej_medicine_catalog')->where('medicine_code', $medicineCode)->lock(true)->find(); EjMedicineMappingPolicy::assertValid($local ?: [], $remote ?: []); $now = time(); $mapping = Db::name('ej_medicine_mapping') ->where('local_medicine_id', $localId) ->lock(true) ->find(); $values = [ 'medicine_code' => $medicineCode, 'status' => 1, 'operator_id' => $operatorId, 'operator_name' => mb_substr(trim($operatorName), 0, 80), 'update_time' => $now, 'delete_time' => null, ]; if ($mapping) { Db::name('ej_medicine_mapping')->where('id', (int) $mapping['id'])->update($values); return; } Db::name('ej_medicine_mapping')->insert(array_merge($values, [ 'local_medicine_id' => $localId, 'create_time' => $now, ])); }); return true; } catch (Throwable $exception) { self::setError(self::isDuplicateKey($exception) ? '该本地药材映射刚被其他操作更新,请刷新后重试' : $exception->getMessage()); return false; } } public static function unlink(int $localMedicineId, int $operatorId, string $operatorName): bool { self::$error = ''; try { Db::transaction(function () use ($localMedicineId, $operatorId, $operatorName): void { $local = Db::name('doctor_medicine')->where('id', $localMedicineId)->lock(true)->find(); $mapping = Db::name('ej_medicine_mapping') ->where('local_medicine_id', $localMedicineId) ->lock(true) ->find(); $decision = EjMedicineMappingPolicy::unlinkDecision($local ?: [], $mapping ?: null); if ($decision['already_unlinked']) { return; } $now = time(); Db::name('ej_medicine_mapping') ->where('id', $decision['mapping_id']) ->where('local_medicine_id', $localMedicineId) ->where('status', 1) ->whereNull('delete_time') ->update([ 'status' => 0, 'operator_id' => $operatorId, 'operator_name' => mb_substr(trim($operatorName), 0, 80), 'update_time' => $now, 'delete_time' => $now, ]); }); return true; } catch (Throwable $exception) { self::setError($exception->getMessage()); return false; } } /** @return array|false */ public static function sync() { self::$error = ''; try { return EjMedicineCatalogSyncService::sync(200); } catch (Throwable $exception) { self::setError($exception->getMessage()); return false; } } /** @return array */ public static function status(): array { $state = Db::name('ej_pharmacy_sync_state')->where('id', 1)->find() ?: []; $catalogTotal = (int) Db::name('ej_medicine_catalog')->count(); $catalogActive = (int) Db::name('ej_medicine_catalog') ->where('status', 1)->where('remote_deleted', 0)->count(); $unmappedLocal = (int) Db::name('doctor_medicine')->alias('l') ->leftJoin( 'ej_medicine_mapping m', 'm.local_medicine_id = l.id AND m.status = 1 AND m.delete_time IS NULL' ) ->where('l.status', 1) ->whereNull('l.delete_time') ->whereNull('m.id') ->count('l.id'); return [ 'sync_enabled' => (bool) Config::get('ej_pharmacy.catalog_sync_enabled', false), 'cursor' => (int) ($state['cursor'] ?? 0), 'last_success_time' => (int) ($state['last_success_time'] ?? 0), 'last_failure_time' => (int) ($state['last_failure_time'] ?? 0), 'last_error_summary' => (string) ($state['last_error_summary'] ?? ''), 'is_syncing' => !empty($state['lock_token']) && (int) ($state['lock_expires_at'] ?? 0) >= time(), 'catalog_total' => $catalogTotal, 'catalog_active' => $catalogActive, 'unmapped_local' => $unmappedLocal, ]; } /** @return array> */ public static function catalogOptions(string $keyword, int $limit = 30): array { $query = Db::name('ej_medicine_catalog') ->where('status', 1) ->where('remote_deleted', 0); $keyword = trim($keyword); if ($keyword !== '') { $query->where(function ($nested) use ($keyword): void { $nested->where('name', 'like', '%' . $keyword . '%') ->whereOr('medicine_code', 'like', '%' . $keyword . '%'); }); } return $query ->field('medicine_code,name,brand,unit,settlement_price,retail_price,catalog_version,status') ->order('catalog_version', 'desc') ->limit(min(max($limit, 1), 50)) ->select() ->toArray(); } private static function isDuplicateKey(Throwable $exception): bool { return (string) $exception->getCode() === '23000' || str_contains(strtolower($exception->getMessage()), 'duplicate'); } }