Files
oil/server-tp6/app/common/service/QytInvoice.php
T
2026-09-18 20:55:21 +08:00

201 lines
8.5 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
// +----------------------------------------------------------------------
// | 企油通 · 企业发票(7.14)
// +----------------------------------------------------------------------
// | 7.14 的粒度是「企业 + 通道 + 月份(+ 司机手机号)」,返回的 invoice 只是一组地址,
// | 和具体订单之间【没有】对应关系。所以:
// | · 表按这个维度存(yoshop_qyt_invoice)
// | · 订单按「订单企业 + 消费月份 + 司机手机号」去关联 —— 同一司机当月多笔订单看到的是同一批发票
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace app\common\service;
use app\common\model\qyt\Enterprise as EnterpriseModel;
use think\facade\Db;
class QytInvoice
{
/** 已同步但暂无发票的记录,超过多久再次点开时自动重新同步(秒) */
const RESYNC_EMPTY_AFTER = 3600;
/**
* 调 7.14 同步某个维度的发票并入库
* @param EnterpriseModel|array $enterprise
* @param string $month YYYY-MM
* @param string $mobile 司机手机号,空串=企业全量
* @return array ['ok'=>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);
}
}
}