bool, 'message'=>string, 'row'=>array] */ public static function sync($enterprise, string $month, string $mobile = ''): array { if (!preg_match('/^\d{4}-(0[1-9]|1[0-2])$/', $month)) { return ['ok' => false, 'message' => '月份格式应为 YYYY-MM', 'row' => []]; } if ($mobile !== '' && !preg_match('/^1[3-9]\d{9}$/', $mobile)) { return ['ok' => false, 'message' => '司机手机号格式不正确', 'row' => []]; } $ent = $enterprise instanceof EnterpriseModel ? $enterprise->toArray() : (array) $enterprise; if (empty($ent['enterprise_id'])) { return ['ok' => false, 'message' => '企业不存在', 'row' => []]; } $res = QytOil::forEnterprise($ent)->invoice($month, $mobile); $key = [ 'qyt_enterprise_id' => (int) $ent['enterprise_id'], 'passageway' => (string) $ent['passageway'], 'month' => $month, 'mobile' => $mobile, ]; $now = time(); if ($res['code'] !== '00') { $message = '同步失败:' . ($res['message'] ?: '未知错误'); self::upsert($key, [ 'business_license' => (string) $ent['business_license'], 'sync_status' => 2, 'sync_time' => $now, 'sync_text' => mb_substr($message, 0, 250), 'store_id' => (int) ($ent['store_id'] ?? 0), ]); return ['ok' => false, 'message' => $message, 'row' => self::format(self::find($key))]; } $urls = self::normalizeUrls($res['data']['invoice'] ?? []); $orders = (array) ($res['data']['order_list'] ?? []); $text = $urls ? '同步成功,共 ' . count($urls) . ' 张发票' : '同步成功,企油通暂未出具该月发票'; self::upsert($key, [ 'business_license' => (string) $ent['business_license'], 'invoice_urls' => json_encode($urls, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), 'invoice_count' => count($urls), 'order_count' => count($orders), 'raw' => mb_substr((string) json_encode(QytOil::maskDeep((array) $res['data']), JSON_UNESCAPED_UNICODE), 0, 1000000), 'sync_status' => 1, 'sync_time' => $now, 'sync_text' => $text, 'store_id' => (int) ($ent['store_id'] ?? 0), ]); return ['ok' => true, 'message' => $text, 'row' => self::format(self::find($key))]; } /** * 取某个订单关联的发票。本地没有、上次同步失败、或「暂无发票」且已超过 1 小时,会自动去同步一次 */ public static function forOrder(array $order, bool $refresh = false): array { $enterprise = EnterpriseModel::detail((int) $order['qyt_enterprise_id']); if (!$enterprise) { return ['ok' => false, 'message' => '本单绑定的企油通企业不存在', 'month' => '', 'row' => []]; } $month = self::orderMonth($order); $mobile = (string) $order['mobile']; $key = [ 'qyt_enterprise_id' => (int) $enterprise['enterprise_id'], 'passageway' => (string) $enterprise['passageway'], 'month' => $month, 'mobile' => $mobile, ]; $row = self::find($key); $stale = !$row || (int) $row['sync_status'] !== 1 || ((int) $row['invoice_count'] === 0 && time() - (int) $row['sync_time'] > self::RESYNC_EMPTY_AFTER); $message = $row ? (string) $row['sync_text'] : ''; if ($refresh || $stale) { $r = self::sync($enterprise, $month, $mobile); $message = $r['message']; $row = self::find($key); if (!$r['ok'] && !$row) { return ['ok' => false, 'message' => $message, 'month' => $month, 'row' => []]; } } return ['ok' => true, 'message' => $message, 'month' => $month, 'row' => self::format($row)]; } /** * 订单的消费月份:优先取加油流水的支付时间,没有再依次退到消费时间 / 发放时间 / 下单时间 */ public static function orderMonth(array $order): string { $t = (int) Db::name('qyt_consume')->where('order_id', $order['order_id'])->max('pay_time'); foreach (['consume_time', 'oil_time', 'create_time'] as $field) { if ($t > 0) { break; } $t = is_numeric($order[$field] ?? null) ? (int) $order[$field] : (int) strtotime((string) ($order[$field] ?? '')); } return date('Y-m', $t > 0 ? $t : time()); } /** * 7.14 的 invoice 文档只写了「发票地址集」,没给元素结构。 * 兼容两种形态:直接是 URL 字符串;或对象(取常见的 url 字段,取不到就找第一个 http 开头的值) */ public static function normalizeUrls($invoice): array { $out = []; foreach ((array) $invoice as $item) { $url = ''; if (is_string($item)) { $url = trim($item); } elseif (is_array($item)) { foreach (['url', 'invoice_url', 'file_url', 'pdf_url', 'download_url', 'path'] as $k) { if (!empty($item[$k]) && is_string($item[$k])) { $url = trim($item[$k]); break; } } if ($url === '') { foreach ($item as $v) { if (is_string($v) && preg_match('#^https?://#i', trim($v))) { $url = trim($v); break; } } } } if ($url !== '' && preg_match('#^https?://#i', $url)) { $out[] = $url; } } return array_values(array_unique($out)); } /** 列表 / 详情输出格式 */ public static function format($row): array { if (!$row) { return []; } $row = (array) $row; unset($row['raw']); $urls = json_decode((string) ($row['invoice_urls'] ?? ''), true); $row['invoice_urls'] = is_array($urls) ? $urls : []; $row['mobile_mask'] = $row['mobile'] !== '' ? QytOil::maskMobile((string) $row['mobile']) : '企业全量'; $row['sync_time_text'] = !empty($row['sync_time']) ? date('Y-m-d H:i:s', (int) $row['sync_time']) : ''; return $row; } protected static function find(array $key): ?array { $row = Db::name('qyt_invoice')->where($key)->find(); return $row ?: null; } protected static function upsert(array $key, array $data): void { $data['update_time'] = time(); if (Db::name('qyt_invoice')->where($key)->update($data)) { return; } try { Db::name('qyt_invoice')->insert($key + $data + ['create_time' => time()]); } catch (\Throwable $e) { // 并发下另一请求刚插入:唯一键冲突,改为更新 if (strpos($e->getMessage(), 'Duplicate') === false) { throw $e; } Db::name('qyt_invoice')->where($key)->update($data); } } }