>,next_cursor:int,has_more:bool} */ public static function parsePage(array $response, int $cursor): array { $body = is_array($response['body'] ?? null) ? $response['body'] : []; $httpStatus = (int) ($response['http_status'] ?? 0); if ($httpStatus < 200 || $httpStatus >= 300 || (int) ($body['code'] ?? -1) !== 0) { $message = trim((string) ($body['message'] ?? '')); throw new RuntimeException($message !== '' ? $message : '洛阳药房药材目录同步失败'); } $data = is_array($body['data'] ?? null) ? $body['data'] : []; $items = is_array($data['items'] ?? null) ? array_values(array_filter( $data['items'], static fn ($item): bool => is_array($item) )) : []; $nextCursor = max(0, (int) ($data['next_cursor'] ?? $cursor)); $hasMore = !empty($data['has_more']); if ($hasMore && $nextCursor <= $cursor) { throw new RuntimeException('洛阳药房药材目录游标未推进,已停止同步'); } return ['items' => $items, 'next_cursor' => $nextCursor, 'has_more' => $hasMore]; } /** * @param array|null $existing * @param array $remote * @return array{action:string,values:array,deactivated:int} */ public static function merge(?array $existing, array $remote): array { $code = trim((string) ($remote['medicine_code'] ?? '')); if ($code === '') { throw new RuntimeException('洛阳药房药材目录包含空 medicine_code'); } $deleted = !empty($remote['deleted']) || !empty($remote['remote_deleted']); $values = [ 'medicine_code' => $code, 'name' => trim((string) ($remote['name'] ?? '')), 'brand' => trim((string) ($remote['brand'] ?? '')), 'unit' => trim((string) ($remote['unit'] ?? '')), 'settlement_price' => self::decimal($remote['settlement_price'] ?? 0), 'retail_price' => self::decimal($remote['retail_price'] ?? 0), 'status' => $deleted ? 0 : (int) ($remote['status'] ?? 0), 'catalog_version' => max(0, (int) ($remote['catalog_version'] ?? 0)), 'remote_deleted' => $deleted ? 1 : 0, ]; if ($existing === null) { return [ 'action' => 'created', 'values' => $values, 'deactivated' => $values['status'] === 0 ? 1 : 0, ]; } $existingComparable = [ 'medicine_code' => trim((string) ($existing['medicine_code'] ?? '')), 'name' => trim((string) ($existing['name'] ?? '')), 'brand' => trim((string) ($existing['brand'] ?? '')), 'unit' => trim((string) ($existing['unit'] ?? '')), 'settlement_price' => self::decimal($existing['settlement_price'] ?? 0), 'retail_price' => self::decimal($existing['retail_price'] ?? 0), 'status' => (int) ($existing['status'] ?? 0), 'catalog_version' => max(0, (int) ($existing['catalog_version'] ?? 0)), 'remote_deleted' => (int) ($existing['remote_deleted'] ?? 0), ]; $deactivated = $existingComparable['status'] === 1 && $values['status'] === 0 ? 1 : 0; return [ 'action' => $existingComparable === $values ? 'unchanged' : 'updated', 'values' => $values, 'deactivated' => $deactivated, ]; } private static function decimal(mixed $value): string { return number_format(max(0.0, (float) $value), 4, '.', ''); } }