201 lines
8.5 KiB
PHP
201 lines
8.5 KiB
PHP
<?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);
|
||
}
|
||
}
|
||
}
|