完成了基本调整调整
This commit is contained in:
@@ -108,8 +108,27 @@ class Qyt
|
||||
|
||||
$params['sign'] = $this->md5Sha1RsaSign($params);
|
||||
|
||||
// 文档示例形如 https://paytaskpage.worknoya.vip/#/?appId=..&mobile=..×tamp=..&sign=..#/
|
||||
return $base . '/?' . http_build_query($params) . '#/';
|
||||
// 最终形如 https://paytaskpage.worknoya.vip/#/?appId=..&mobile=..×tamp=..&sign=iwxE****g==
|
||||
//
|
||||
// ⚠️ 两个坑,都是实测踩出来的,改动前务必看完:
|
||||
//
|
||||
// 1) 不能用 http_build_query:它会把 base64 签名里的 + / = 编码成 %2B %2F %3D。
|
||||
// 参数挂在 # 之后的 fragment 里,浏览器不会自动解码,落地页若不做
|
||||
// decodeURIComponent,拿到的签名串就和我们签的不一致 -> 验签失败。
|
||||
// 文档示例里末尾是裸的 ==,对方期望的就是未编码的原始值。
|
||||
// appId / mobile / timestamp / creditcode 都是字母数字,本来也无需编码。
|
||||
// 若对接人日后确认页面会解码,把 h5_sign_urlencode 配成 true 即可切回。
|
||||
//
|
||||
// 2) 末尾【不能】再拼 '#/'。文档示例末尾是有 #/ 的,但实测:带上之后
|
||||
// 桌面浏览器能打开,微信里打开 / 扫码一律提示验签失败 —— 落地页会把
|
||||
// 这个 #/ 一起算进最后一个参数 sign 的值里。去掉后微信正常。
|
||||
// ⚠️ 别照着文档示例把 '#/' 加回来。
|
||||
$encode = !empty($this->conf['h5_sign_urlencode']);
|
||||
$pairs = [];
|
||||
foreach ($params as $k => $v) {
|
||||
$pairs[] = $k . '=' . ($encode ? rawurlencode((string) $v) : (string) $v);
|
||||
}
|
||||
return $base . '/?' . implode('&', $pairs);
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | 企油通 · 司机档案
|
||||
// +----------------------------------------------------------------------
|
||||
// | 按身份证号唯一。上游 7.12 / 7.13 / 7.17 都以「手机号 + 身份证号」定位司机,
|
||||
// | 7.7 下发前会做实名二要素核验,所以本地提前把会导致核验失败的数据标出来:
|
||||
// | · 同一身份证号出现不同姓名
|
||||
// | · 同一手机号对应多个身份证号
|
||||
// | · 同一身份证号换了手机号
|
||||
// | 累计发放 / 退款 / 订单数不存冗余,列表查询时从订单实时汇总,避免对不上账。
|
||||
// +----------------------------------------------------------------------
|
||||
declare (strict_types=1);
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
use think\facade\Db;
|
||||
|
||||
class QytDriver
|
||||
{
|
||||
/**
|
||||
* 下单成功后更新司机档案。任何异常都吞掉 —— 档案是辅助数据,不能影响建单
|
||||
*/
|
||||
public static function touch(array $order): void
|
||||
{
|
||||
$idCard = strtoupper(trim((string) ($order['id_card'] ?? '')));
|
||||
if ($idCard === '') {
|
||||
return;
|
||||
}
|
||||
$name = trim((string) ($order['driver_name'] ?? ''));
|
||||
$mobile = trim((string) ($order['mobile'] ?? ''));
|
||||
$storeId = (int) ($order['store_id'] ?? 0);
|
||||
$now = time();
|
||||
|
||||
try {
|
||||
$row = Db::name('qyt_driver')->where('store_id', $storeId)->where('id_card', $idCard)->find();
|
||||
if (!$row) {
|
||||
try {
|
||||
Db::name('qyt_driver')->insert([
|
||||
'id_card' => $idCard, 'name' => $name, 'mobile' => $mobile,
|
||||
'last_order_time' => $now, 'store_id' => $storeId, 'create_time' => $now, 'update_time' => $now,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
if (strpos($e->getMessage(), 'Duplicate') === false) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
$row = Db::name('qyt_driver')->where('store_id', $storeId)->where('id_card', $idCard)->find();
|
||||
}
|
||||
|
||||
$notes = self::detectConflicts($row, $name, $mobile);
|
||||
|
||||
// 同一手机号挂在别的身份证号上:两边都标记
|
||||
if ($mobile !== '') {
|
||||
$others = Db::name('qyt_driver')
|
||||
->where('store_id', $storeId)->where('mobile', $mobile)->where('id_card', '<>', $idCard)
|
||||
->column('driver_id');
|
||||
if ($others) {
|
||||
$notes[] = '同一手机号对应多个身份证号';
|
||||
Db::name('qyt_driver')->whereIn('driver_id', $others)
|
||||
->update(['conflict' => 1, 'conflict_text' => '同一手机号对应多个身份证号', 'update_time' => $now]);
|
||||
}
|
||||
}
|
||||
|
||||
$update = [
|
||||
'name' => $name !== '' ? $name : $row['name'],
|
||||
'mobile' => $mobile !== '' ? $mobile : $row['mobile'],
|
||||
'last_order_time' => $now,
|
||||
'update_time' => $now,
|
||||
];
|
||||
if ($notes) {
|
||||
$update['conflict'] = 1;
|
||||
$update['conflict_text'] = mb_substr(implode(';', array_unique($notes)), 0, 250);
|
||||
}
|
||||
Db::name('qyt_driver')->where('driver_id', $row['driver_id'])->update($update);
|
||||
} catch (\Throwable $e) {
|
||||
(new QytOil())->saveLog('司机档案更新失败 order=' . ($order['order_id'] ?? '') . ':' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 纯函数:对比已有档案与本次下单的姓名 / 手机号
|
||||
*/
|
||||
public static function detectConflicts(array $row, string $name, string $mobile): array
|
||||
{
|
||||
$notes = [];
|
||||
$oldName = (string) ($row['name'] ?? '');
|
||||
$oldMobile = (string) ($row['mobile'] ?? '');
|
||||
if ($oldName !== '' && $name !== '' && $oldName !== $name) {
|
||||
$notes[] = "同一身份证号出现不同姓名:{$oldName} / {$name}";
|
||||
}
|
||||
if ($oldMobile !== '' && $mobile !== '' && $oldMobile !== $mobile) {
|
||||
$notes[] = '手机号变更:' . QytOil::maskMobile($oldMobile) . ' → ' . QytOil::maskMobile($mobile);
|
||||
}
|
||||
return $notes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,13 @@ class QytOil extends BaseService
|
||||
/** @var EnterpriseModel|array|null 当前企业 */
|
||||
protected $enterprise = null;
|
||||
|
||||
/**
|
||||
* @var bool 最近一次请求是否属于「网络层异常」(超时、连不上、响应不是 JSON)。
|
||||
* 这类失败不能说明上游没处理——对 7.16 这种没有幂等键的写接口,必须和
|
||||
* 「上游明确返回失败」区分开,前者转人工确认,后者才可以放心重试。
|
||||
*/
|
||||
protected $transportError = false;
|
||||
|
||||
protected function initialize()
|
||||
{
|
||||
$this->conf = QytConfig::get();
|
||||
@@ -204,6 +211,132 @@ class QytOil extends BaseService
|
||||
return null;
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 7.17 获取支付凭证(须真实 RSA 验签)
|
||||
// ================================================================
|
||||
|
||||
/**
|
||||
* 查询某司机在当前企业下的支付凭证,退款(7.16)要用其中的 quota_id
|
||||
* @return array data.list[]: quota_id / total_amount / available_amount / used_amount / status ...(金额单位:元)
|
||||
*/
|
||||
public function getVoucherList(string $mobile, string $idCard, int $page = 1, int $pageSize = 50): array
|
||||
{
|
||||
if (($err = $this->requireEnterprise()) !== null) {
|
||||
return $err;
|
||||
}
|
||||
return $this->post('/enterprise/api/quota/getVoucherList', [
|
||||
'passageway' => (string) $this->channelCode(),
|
||||
'business_license' => $this->businessLicense(),
|
||||
'mobile' => $mobile,
|
||||
'id_card' => $idCard,
|
||||
'page' => $page,
|
||||
'page_size' => $pageSize,
|
||||
]);
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 7.16 司机额度调整(须真实 RSA 验签;⚠️ 无幂等键,重复调用会重复调额)
|
||||
// ================================================================
|
||||
|
||||
/**
|
||||
* @param int|string $quotaId 7.17 返回的凭证主键
|
||||
* @param string $changeType 02 增加额度 / 03 减少额度
|
||||
* @param float $amount 调整金额(元)
|
||||
* @param string $reason 操作原因(≤200 字);退款时写入退款单号,确认结果时靠它在 7.13 流水里定位
|
||||
* @param string $waybillNo 运单号(可选)
|
||||
*/
|
||||
public function adjustVoucher($quotaId, string $changeType, float $amount, string $reason = '', string $waybillNo = ''): array
|
||||
{
|
||||
if (($err = $this->requireEnterprise()) !== null) {
|
||||
return $err;
|
||||
}
|
||||
$params = [
|
||||
'passageway' => (string) $this->channelCode(),
|
||||
'business_license' => $this->businessLicense(),
|
||||
'quota_id' => (int) $quotaId,
|
||||
'change_type' => $changeType,
|
||||
'amount' => round($amount, 2),
|
||||
'operation_reason' => mb_substr($reason, 0, 200),
|
||||
];
|
||||
$waybillNo !== '' && $params['waybill_no'] = $waybillNo;
|
||||
return $this->post('/enterprise/api/quota/adjustVoucher', $params);
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 7.11 企业账户余额(仅平安银行通道支持,金额:分)
|
||||
// ================================================================
|
||||
|
||||
public function distributable(): array
|
||||
{
|
||||
if (($err = $this->requireEnterprise()) !== null) {
|
||||
return $err;
|
||||
}
|
||||
if ($this->channelCode() !== 2) {
|
||||
return $this->fail('企业账户余额查询仅支持平安银行通道,支付宝企业码没有独立资金账户');
|
||||
}
|
||||
return $this->post('/enterprise/api/zhyk/distributable', [
|
||||
'business_license' => $this->businessLicense(),
|
||||
'passageway' => (string) $this->channelCode(),
|
||||
]);
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 7.12 司机钱包金额 / 7.13 司机钱包明细(金额:分)
|
||||
// ================================================================
|
||||
|
||||
public function driverBalanceQuery(string $mobile, string $idCard, int $page = 1, int $pageSize = 20): array
|
||||
{
|
||||
if (($err = $this->requireEnterprise()) !== null) {
|
||||
return $err;
|
||||
}
|
||||
return $this->post('/enterprise/api/zhyk/driverBalanceQuery', [
|
||||
'business_license' => $this->businessLicense(),
|
||||
'passageway' => (string) $this->channelCode(),
|
||||
'mobile' => $mobile,
|
||||
'id_card' => $idCard,
|
||||
'page' => $page,
|
||||
'page_size' => $pageSize,
|
||||
]);
|
||||
}
|
||||
|
||||
public function driverWalletQuery(string $mobile, string $idCard, int $page = 1, int $pageSize = 20): array
|
||||
{
|
||||
if (($err = $this->requireEnterprise()) !== null) {
|
||||
return $err;
|
||||
}
|
||||
return $this->post('/enterprise/api/zhyk/driverWalletQuery', [
|
||||
'business_license' => $this->businessLicense(),
|
||||
'passageway' => (string) $this->channelCode(),
|
||||
'mobile' => $mobile,
|
||||
'id_card' => $idCard,
|
||||
'page' => $page,
|
||||
'page_size' => $pageSize,
|
||||
]);
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 7.14 企业发票下载(粒度:企业 + 通道 + 月份,可选按司机手机号收窄)
|
||||
// ================================================================
|
||||
|
||||
/**
|
||||
* @param string $month YYYY-MM
|
||||
* @param string $mobile 司机手机号;传了只返回该司机相关发票与加油明细
|
||||
* @return array data: business_license / invoice[] 发票地址集 / order_list[] 加油明细(分)
|
||||
*/
|
||||
public function invoice(string $month, string $mobile = ''): array
|
||||
{
|
||||
if (($err = $this->requireEnterprise()) !== null) {
|
||||
return $err;
|
||||
}
|
||||
$params = [
|
||||
'business_license' => $this->businessLicense(),
|
||||
'passageway' => (string) $this->channelCode(),
|
||||
'month' => $month,
|
||||
];
|
||||
$mobile !== '' && $params['mobile'] = $mobile;
|
||||
return $this->post('/enterprise/api/zhyk/invoice', $params);
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 7.2 企业申请创建
|
||||
// ================================================================
|
||||
@@ -295,12 +428,15 @@ class QytOil extends BaseService
|
||||
['企业' => $this->enterpriseName(), 'url' => $url, 'params' => self::maskDeep($bizParams)],
|
||||
JSON_UNESCAPED_UNICODE));
|
||||
|
||||
$this->transportError = false;
|
||||
$resp = $this->curlPost($url, json_encode($body, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
$this->saveLog("[响应] {$path} " . self::maskText($resp));
|
||||
|
||||
$json = json_decode($resp, true);
|
||||
if (!is_array($json)) {
|
||||
return ['code' => 'ERR', 'data' => [], 'message' => '响应解析失败', 'dup_batch' => false, 'raw' => ['body' => $resp]];
|
||||
// 拿不到可解析的响应,无法判断上游是否已处理
|
||||
return ['code' => 'ERR', 'data' => [], 'message' => '响应解析失败', 'dup_batch' => false,
|
||||
'transport_error' => true, 'raw' => ['body' => mb_substr((string) $resp, 0, 500)]];
|
||||
}
|
||||
|
||||
// 通用响应结构:{ status: ok|error, data, message, serialNo, sign }
|
||||
@@ -323,6 +459,8 @@ class QytOil extends BaseService
|
||||
'data' => $json['data'] ?? [],
|
||||
'message' => $message,
|
||||
'dup_batch' => $dupBatch,
|
||||
// cURL 失败时 curlPost 会伪造一个 {"status":"error"} 的 JSON,靠标记区分
|
||||
'transport_error' => $this->transportError,
|
||||
'raw' => $json,
|
||||
];
|
||||
}
|
||||
@@ -344,9 +482,15 @@ class QytOil extends BaseService
|
||||
if ($resp === false) {
|
||||
$err = curl_error($ch);
|
||||
curl_close($ch);
|
||||
$this->transportError = true;
|
||||
return json_encode(['status' => 'error', 'message' => 'cURL: ' . $err], JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
// 网关层 5xx:请求可能已到达上游业务层,也按结果未知处理
|
||||
if ($httpCode >= 500) {
|
||||
$this->transportError = true;
|
||||
}
|
||||
return (string) $resp;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,524 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | 企油通 · 订单退款(7.17 获取支付凭证 + 7.16 司机额度调整)
|
||||
// +----------------------------------------------------------------------
|
||||
// | 规则(已与业务确认):退「剩余未加油金额」,每单只退一次
|
||||
// | · 订单必须「发放成功」;一单可以分多次加油(7.10 逐笔累计到 used_amount)
|
||||
// | · 退款金额 = 订单金额 − 已加油金额,固定不可指定;为 0 时不能退
|
||||
// | · 额度在上游是按司机累计到凭证上的,退款只能「把凭证调低 X 元」。
|
||||
// | 还要求凭证可用余额 ≥ 退款金额(额度可能被该司机其他订单用掉),否则直接拒绝。
|
||||
// |
|
||||
// | 状态(yoshop_partners_order.refund_status):
|
||||
// | 0 未退款 -> 1 退款中 -> 2 已退款
|
||||
// | -> 3 退款失败(上游明确拒绝 / 凭证不足,可重新发起)
|
||||
// | -> 4 待人工确认(7.16 已发出但结果未知)
|
||||
// |
|
||||
// | ⚠️ 7.16 没有幂等键,同一请求发两次就调低两次。所以:
|
||||
// | · 一旦走到 7.16,任何「不确定」都进 4,绝不自动重试
|
||||
// | · 4 只能通过 confirm()(查上游证据)或 resolve()(人工判定)离开
|
||||
// | · 退款单号写进 7.16 的 operation_reason,confirm() 优先在 7.13 流水里找它
|
||||
// |
|
||||
// | 并发:RedisLock 串行同一订单;状态跃迁是条件 UPDATE,affected=1 才退合作商余额。
|
||||
// +----------------------------------------------------------------------
|
||||
declare (strict_types=1);
|
||||
|
||||
namespace app\common\service;
|
||||
|
||||
use app\common\lock\RedisLock;
|
||||
use app\common\model\qyt\Enterprise as EnterpriseModel;
|
||||
use think\facade\Db;
|
||||
|
||||
class QytRefund
|
||||
{
|
||||
const NONE = 0;
|
||||
const PROCESSING = 1;
|
||||
const SUCCESS = 2;
|
||||
const FAILED = 3;
|
||||
const CONFIRM = 4;
|
||||
|
||||
const STATUS_TEXT = [
|
||||
self::NONE => '未退款',
|
||||
self::PROCESSING => '退款中',
|
||||
self::SUCCESS => '已退款',
|
||||
self::FAILED => '退款失败',
|
||||
self::CONFIRM => '待人工确认',
|
||||
];
|
||||
|
||||
// 失败分类,开放接口据此映射错误码
|
||||
const REASON_NOT_FOUND = 'not_found';
|
||||
const REASON_STATE = 'state';
|
||||
const REASON_PROCESSING = 'processing';
|
||||
const REASON_UPSTREAM = 'upstream';
|
||||
|
||||
/** 元金额比较容差 */
|
||||
const EPS = 0.005;
|
||||
|
||||
// ================================================================
|
||||
// 发起退款
|
||||
// ================================================================
|
||||
|
||||
/**
|
||||
* @param string $source admin / api
|
||||
* @param string $operator 后台账号 或 合作商 appKey
|
||||
*/
|
||||
public static function apply(int $orderId, string $source, string $operator, string $reason = ''): array
|
||||
{
|
||||
$lock = new RedisLock('qyt:refund:' . $orderId, 120);
|
||||
if (!$lock->acquireLock()) {
|
||||
return self::result(false, self::PROCESSING, '该订单退款正在处理中,请勿重复提交', self::REASON_PROCESSING);
|
||||
}
|
||||
|
||||
$order = null;
|
||||
$refundId = 0;
|
||||
$refundNo = '';
|
||||
// before_adjust:还没调 7.16,出任何问题都可以安全地判失败
|
||||
// after_adjust :7.16 已发出,出问题只能进人工确认
|
||||
$stage = 'before_adjust';
|
||||
|
||||
try {
|
||||
$order = Db::name('partners_order')->where('order_id', $orderId)->find();
|
||||
if (!$order) {
|
||||
return self::result(false, self::NONE, '订单不存在', self::REASON_NOT_FOUND);
|
||||
}
|
||||
|
||||
$current = (int) $order['refund_status'];
|
||||
if ($current === self::SUCCESS) {
|
||||
// 幂等:已退过直接返回成功
|
||||
return self::result(true, self::SUCCESS, '该订单已退款', '', self::orderData($order));
|
||||
}
|
||||
if ($current === self::PROCESSING || $current === self::CONFIRM) {
|
||||
return self::result(false, $current,
|
||||
$current === self::CONFIRM ? '该订单退款结果待人工确认,请勿重复发起' : '该订单退款正在处理中',
|
||||
self::REASON_PROCESSING, self::orderData($order));
|
||||
}
|
||||
if (($err = self::checkRefundable($order)) !== '') {
|
||||
return self::result(false, $current, $err, self::REASON_STATE, self::orderData($order));
|
||||
}
|
||||
$enterprise = EnterpriseModel::detail((int) $order['qyt_enterprise_id']);
|
||||
if (!$enterprise) {
|
||||
return self::result(false, $current, '本单绑定的企油通企业不存在,无法退款', self::REASON_STATE, self::orderData($order));
|
||||
}
|
||||
|
||||
// 退款金额 = 剩余未加油金额(锁内重读订单后计算)
|
||||
$amount = self::remainAmount($order);
|
||||
|
||||
// 抢占:只有 0/3 能进入退款中
|
||||
$affected = Db::name('partners_order')
|
||||
->where('order_id', $orderId)
|
||||
->whereIn('refund_status', [self::NONE, self::FAILED])
|
||||
->update(['refund_status' => self::PROCESSING, 'refund_text' => '退款中', 'update_time' => time()]);
|
||||
if (!$affected) {
|
||||
return self::result(false, $current, '订单状态已变化,请刷新后重试', self::REASON_PROCESSING, self::orderData($order));
|
||||
}
|
||||
|
||||
$refundNo = 'RF' . $orderId . 'T' . date('YmdHis') . mt_rand(10, 99);
|
||||
$refundId = (int) Db::name('qyt_refund')->insertGetId([
|
||||
'refund_no' => $refundNo,
|
||||
'order_id' => $orderId,
|
||||
'partners_id' => (int) $order['partners_id'],
|
||||
'qyt_enterprise_id' => (int) $order['qyt_enterprise_id'],
|
||||
'source' => $source,
|
||||
'operator' => mb_substr($operator, 0, 64),
|
||||
'reason' => mb_substr($reason, 0, 200),
|
||||
'amount' => $amount,
|
||||
'status' => self::PROCESSING,
|
||||
'store_id' => (int) $order['store_id'],
|
||||
'create_time' => time(),
|
||||
'update_time' => time(),
|
||||
]);
|
||||
|
||||
$qyt = QytOil::forEnterprise($enterprise);
|
||||
|
||||
// 1) 7.17 找凭证(只读接口,失败不影响上游,可以放心判失败)
|
||||
$vres = $qyt->getVoucherList((string) $order['mobile'], (string) $order['id_card'], 1, 50);
|
||||
if ($vres['code'] !== '00') {
|
||||
return self::finishFailed($order, $refundId, $refundNo,
|
||||
'查询支付凭证失败:' . ($vres['message'] ?: '未知错误'), $vres);
|
||||
}
|
||||
$pick = self::pickVoucher((array) ($vres['data']['list'] ?? []), $amount);
|
||||
if (!$pick['voucher']) {
|
||||
return self::finishFailed($order, $refundId, $refundNo, $pick['message'], $vres);
|
||||
}
|
||||
$voucher = $pick['voucher'];
|
||||
Db::name('qyt_refund')->where('refund_id', $refundId)->update([
|
||||
'quota_id' => (string) $voucher['quota_id'],
|
||||
'total_before' => round((float) ($voucher['total_amount'] ?? 0), 2),
|
||||
'available_before' => round((float) ($voucher['available_amount'] ?? 0), 2),
|
||||
'update_time' => time(),
|
||||
]);
|
||||
|
||||
// 2) 7.16 调低额度
|
||||
$stage = 'after_adjust';
|
||||
$ares = $qyt->adjustVoucher(
|
||||
$voucher['quota_id'],
|
||||
'03',
|
||||
$amount,
|
||||
'订单退款 ' . $refundNo . ($reason !== '' ? ':' . $reason : ''),
|
||||
(string) $order['waybill_no']
|
||||
);
|
||||
|
||||
if (!empty($ares['transport_error'])) {
|
||||
return self::finishConfirm($order, $refundId, $refundNo,
|
||||
'退款请求已发出但结果未知(网络超时或响应异常),已转人工确认,切勿重复发起', $ares);
|
||||
}
|
||||
$success = $ares['code'] === '00' && !(isset($ares['data']['success']) && $ares['data']['success'] === false);
|
||||
if (!$success) {
|
||||
$msg = (string) ($ares['data']['message'] ?? '') ?: $ares['message'];
|
||||
return self::finishFailed($order, $refundId, $refundNo, '企油通调额失败:' . ($msg ?: '未知错误'), $ares);
|
||||
}
|
||||
return self::finishSuccess($order, $refundId, $refundNo, $amount, '退款成功', $ares);
|
||||
} catch (\Throwable $e) {
|
||||
(new QytOil())->saveLog("退款异常 order={$orderId} stage={$stage}:" . $e->getMessage() . ' @ ' . $e->getFile() . ':' . $e->getLine());
|
||||
if ($order && $refundId) {
|
||||
return $stage === 'after_adjust'
|
||||
? self::finishConfirm($order, $refundId, $refundNo, '退款过程发生异常且 7.16 可能已生效,已转人工确认:' . $e->getMessage(), [])
|
||||
: self::finishFailed($order, $refundId, $refundNo, '退款过程发生异常:' . $e->getMessage(), []);
|
||||
}
|
||||
return self::result(false, self::NONE, '退款处理异常:' . $e->getMessage(), self::REASON_STATE);
|
||||
} finally {
|
||||
$lock->releaseLock();
|
||||
}
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 确认「待人工确认」的退款:向上游取证
|
||||
// ================================================================
|
||||
|
||||
public static function confirm(int $orderId): array
|
||||
{
|
||||
$lock = new RedisLock('qyt:refund:' . $orderId, 120);
|
||||
if (!$lock->acquireLock()) {
|
||||
return self::result(false, self::CONFIRM, '该订单正在处理中,请稍后再试', self::REASON_PROCESSING);
|
||||
}
|
||||
try {
|
||||
[$order, $refund, $enterprise, $err] = self::loadConfirmContext($orderId);
|
||||
if ($err !== '') {
|
||||
return self::result(false, (int) ($order['refund_status'] ?? self::NONE), $err, self::REASON_STATE);
|
||||
}
|
||||
$qyt = QytOil::forEnterprise($enterprise);
|
||||
$amount = round((float) $refund['amount'], 2);
|
||||
|
||||
// 证据 1:7.13 钱包流水里带有退款单号的调额记录 —— 最可靠
|
||||
$inWallet = false;
|
||||
$wres = $qyt->driverWalletQuery((string) $order['mobile'], (string) $order['id_card'], 1, 50);
|
||||
if ($wres['code'] === '00') {
|
||||
foreach ((array) ($wres['data']['transactionDetails'] ?? []) as $t) {
|
||||
if (strpos((string) ($t['remark'] ?? ''), (string) $refund['refund_no']) !== false) {
|
||||
$inWallet = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 证据 2:7.17 同一张凭证的总额度 / 可用余额变化
|
||||
$vres = $qyt->getVoucherList((string) $order['mobile'], (string) $order['id_card'], 1, 50);
|
||||
$now = null;
|
||||
if ($vres['code'] === '00') {
|
||||
foreach ((array) ($vres['data']['list'] ?? []) as $v) {
|
||||
if ((string) ($v['quota_id'] ?? '') === (string) $refund['quota_id']) {
|
||||
$now = $v;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!$inWallet && $vres['code'] !== '00') {
|
||||
return self::result(false, self::CONFIRM, '暂时无法向企油通取证:' . ($vres['message'] ?: '查询失败') . ',请稍后再试', self::REASON_UPSTREAM);
|
||||
}
|
||||
|
||||
$totalNow = $now ? round((float) ($now['total_amount'] ?? 0), 2) : null;
|
||||
$availNow = $now ? round((float) ($now['available_amount'] ?? 0), 2) : null;
|
||||
if ($now) {
|
||||
Db::name('qyt_refund')->where('refund_id', $refund['refund_id'])->update([
|
||||
'total_after' => $totalNow, 'available_after' => $availNow, 'update_time' => time(),
|
||||
]);
|
||||
}
|
||||
|
||||
// 退款发起前后 10 分钟起,该司机在本企业是否有其他单在发放 / 发放成功(会干扰额度比对)
|
||||
$grantedSince = Db::name('partners_order')
|
||||
->where('id_card', $order['id_card'])
|
||||
->where('qyt_enterprise_id', $order['qyt_enterprise_id'])
|
||||
->where('order_id', '<>', $order['order_id'])
|
||||
->whereIn('oil_status_zsy', [1, 2])
|
||||
->where('update_time', '>=', (int) $refund['create_time'] - 600)
|
||||
->count() > 0;
|
||||
|
||||
$verdict = self::judgeConfirm(
|
||||
$inWallet,
|
||||
(float) $refund['total_before'], $totalNow,
|
||||
(float) $refund['available_before'], $availNow,
|
||||
$amount,
|
||||
$grantedSince
|
||||
);
|
||||
|
||||
if ($verdict === 'applied') {
|
||||
return self::finishSuccess($order, (int) $refund['refund_id'], (string) $refund['refund_no'], $amount,
|
||||
'人工确认:企油通已调低额度,退款成功', $vres);
|
||||
}
|
||||
if ($verdict === 'not_applied') {
|
||||
return self::finishFailed($order, (int) $refund['refund_id'], (string) $refund['refund_no'],
|
||||
'确认结果:企油通未调低额度,退款未生效,可重新发起', $vres);
|
||||
}
|
||||
return self::result(false, self::CONFIRM,
|
||||
'上游数据无法自动判定(期间可能有新的发放或加油消费),请到企油通后台核对该司机凭证后使用「人工判定」',
|
||||
self::REASON_UPSTREAM, self::orderData($order, (string) $refund['refund_no']));
|
||||
} catch (\Throwable $e) {
|
||||
return self::result(false, self::CONFIRM, '确认过程异常:' . $e->getMessage(), self::REASON_STATE);
|
||||
} finally {
|
||||
$lock->releaseLock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 人工判定「待人工确认」的退款(运营到企油通后台核对后操作)
|
||||
*/
|
||||
public static function resolve(int $orderId, bool $success, string $operator, string $remark): array
|
||||
{
|
||||
if (trim($remark) === '') {
|
||||
return self::result(false, self::CONFIRM, '请填写判定依据', self::REASON_STATE);
|
||||
}
|
||||
$lock = new RedisLock('qyt:refund:' . $orderId, 120);
|
||||
if (!$lock->acquireLock()) {
|
||||
return self::result(false, self::CONFIRM, '该订单正在处理中,请稍后再试', self::REASON_PROCESSING);
|
||||
}
|
||||
try {
|
||||
[$order, $refund, , $err] = self::loadConfirmContext($orderId);
|
||||
if ($err !== '') {
|
||||
return self::result(false, (int) ($order['refund_status'] ?? self::NONE), $err, self::REASON_STATE);
|
||||
}
|
||||
$text = "人工判定({$operator}):" . mb_substr($remark, 0, 100);
|
||||
return $success
|
||||
? self::finishSuccess($order, (int) $refund['refund_id'], (string) $refund['refund_no'], (float) $refund['amount'], $text, [])
|
||||
: self::finishFailed($order, (int) $refund['refund_id'], (string) $refund['refund_no'], $text, []);
|
||||
} finally {
|
||||
$lock->releaseLock();
|
||||
}
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 纯函数(无副作用,便于单测)
|
||||
// ================================================================
|
||||
|
||||
/**
|
||||
* 退款前置条件,返回空串表示可以退
|
||||
*/
|
||||
public static function checkRefundable(array $order): string
|
||||
{
|
||||
if ((int) $order['oil_status_zsy'] !== 2 || (int) $order['oil_status'] !== 1) {
|
||||
return '仅「发放成功」的订单可以退款';
|
||||
}
|
||||
if (self::remainAmount($order) <= self::EPS) {
|
||||
return '订单金额已全部加油,没有可退金额';
|
||||
}
|
||||
if (trim((string) $order['id_card']) === '') {
|
||||
return '订单缺少司机身份证号,无法查询支付凭证';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 剩余未加油金额(元)= 订单金额 − 已加油金额,不小于 0。这就是可退金额
|
||||
*/
|
||||
public static function remainAmount(array $order): float
|
||||
{
|
||||
return max(0.0, round((float) ($order['total_price'] ?? 0) - (float) ($order['used_amount'] ?? 0), 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 7.17 凭证列表里挑一张用于调低额度的凭证
|
||||
* 规则:只看生效中的凭证,取可用余额最大的一张;它不够就拒绝(整单全额退,不拆多张凭证)
|
||||
* @return array ['voucher' => array|null, 'message' => string]
|
||||
*/
|
||||
public static function pickVoucher(array $list, float $amount): array
|
||||
{
|
||||
$active = array_values(array_filter($list, function ($v) {
|
||||
return is_array($v) && (string) ($v['status'] ?? '') === 'active';
|
||||
}));
|
||||
if (!$active) {
|
||||
return ['voucher' => null, 'message' => '该司机在本企业下没有生效中的支付凭证,无法退款'];
|
||||
}
|
||||
usort($active, function ($a, $b) {
|
||||
return (float) ($b['available_amount'] ?? 0) <=> (float) ($a['available_amount'] ?? 0);
|
||||
});
|
||||
$top = $active[0];
|
||||
$avail = round((float) ($top['available_amount'] ?? 0), 2);
|
||||
if ($avail + self::EPS < $amount) {
|
||||
return ['voucher' => null, 'message' => sprintf(
|
||||
'司机凭证可用余额不足:可用 %.2f 元,需退 %.2f 元(凭证额度按司机累计,可能已被该司机其他订单加油用掉)', $avail, $amount)];
|
||||
}
|
||||
return ['voucher' => $top, 'message' => ''];
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据上游证据判定 7.16 是否已生效
|
||||
* · 7.13 流水里找到退款单号 -> applied
|
||||
* · 凭证总额度恰好减少了退款金额 -> applied
|
||||
* (加油消费只动可用/已用,不动总额度;调额才动总额度)
|
||||
* · 总额度没变 且 可用余额没有下降 -> not_applied
|
||||
* · 其余(凭证找不到、期间有新发放或消费导致对不上) -> unknown,交人工
|
||||
* @return string applied / not_applied / unknown
|
||||
*/
|
||||
public static function judgeConfirm(bool $inWallet, float $totalBefore, ?float $totalNow,
|
||||
float $availBefore, ?float $availNow, float $amount,
|
||||
bool $grantedSince = false): string
|
||||
{
|
||||
if ($inWallet) {
|
||||
return 'applied';
|
||||
}
|
||||
if ($totalNow === null || $availNow === null) {
|
||||
return 'unknown';
|
||||
}
|
||||
// $grantedSince:退款发起前后该司机在本企业有新的发放。此时额度比对不可信——
|
||||
// 「调低 100 + 新发 100」看起来就是没变,若据此判「未生效」,运营重新发起会重复扣减。
|
||||
if (abs(($totalBefore - $totalNow) - $amount) <= self::EPS) {
|
||||
return $grantedSince ? 'unknown' : 'applied';
|
||||
}
|
||||
if (abs($totalNow - $totalBefore) <= self::EPS && $availNow + self::EPS >= $availBefore) {
|
||||
return $grantedSince ? 'unknown' : 'not_applied';
|
||||
}
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 内部
|
||||
// ================================================================
|
||||
|
||||
protected static function finishSuccess(array $order, int $refundId, string $refundNo, float $amount, string $text, array $res): array
|
||||
{
|
||||
$amount = round($amount, 2);
|
||||
$won = false;
|
||||
Db::transaction(function () use ($order, $refundId, $amount, $text, $res, &$won) {
|
||||
$won = (bool) Db::name('partners_order')
|
||||
->where('order_id', $order['order_id'])
|
||||
->whereIn('refund_status', [self::PROCESSING, self::CONFIRM])
|
||||
->update([
|
||||
'refund_status' => self::SUCCESS,
|
||||
'refund_amount' => $amount,
|
||||
'refund_time' => time(),
|
||||
'refund_text' => mb_substr($text, 0, 250),
|
||||
'update_time' => time(),
|
||||
]);
|
||||
Db::name('qyt_refund')->where('refund_id', $refundId)->update([
|
||||
'status' => self::SUCCESS,
|
||||
'message' => mb_substr($text, 0, 250),
|
||||
'response' => self::dumpRes($res),
|
||||
'update_time' => time(),
|
||||
]);
|
||||
// 只有状态跃迁胜出者才回补合作商余额,保证只退一次
|
||||
if ($won) {
|
||||
PartnersBalance::refund((int) $order['partners_id'], $amount, (string) $order['partners_order_no'],
|
||||
(int) $order['order_id'], (int) $order['store_id']);
|
||||
}
|
||||
});
|
||||
|
||||
if ($won) {
|
||||
// 退款计算与加油推送之间有时间差:退款期间若又来了加油,已加油 + 已退款可能超过订单金额,告警人工核对
|
||||
$fresh = Db::name('partners_order')->where('order_id', $order['order_id'])->field('total_price,used_amount')->find();
|
||||
if ($fresh && (float) $fresh['used_amount'] + $amount > (float) $fresh['total_price'] + self::EPS) {
|
||||
(new QytOil())->saveLog("【告警】退款后 已加油+已退款 超出订单金额 order={$order['order_id']}"
|
||||
. " total={$fresh['total_price']} used={$fresh['used_amount']} refund={$amount}");
|
||||
}
|
||||
(new QueueService())->push('notify_queue', json_encode([
|
||||
'order_id' => $order['order_id'],
|
||||
'optType' => 'refund',
|
||||
'retry_count' => 0,
|
||||
], 320));
|
||||
(new QytOil())->saveLog("退款成功 order={$order['order_id']} refund_no={$refundNo} amount={$amount},已回补合作商余额");
|
||||
}
|
||||
$order['refund_status'] = self::SUCCESS;
|
||||
$order['refund_amount'] = $amount;
|
||||
return self::result(true, self::SUCCESS, $text, '', self::orderData($order, $refundNo));
|
||||
}
|
||||
|
||||
protected static function finishFailed(array $order, int $refundId, string $refundNo, string $text, array $res): array
|
||||
{
|
||||
Db::name('partners_order')
|
||||
->where('order_id', $order['order_id'])
|
||||
->whereIn('refund_status', [self::PROCESSING, self::CONFIRM])
|
||||
->update(['refund_status' => self::FAILED, 'refund_text' => mb_substr($text, 0, 250), 'update_time' => time()]);
|
||||
Db::name('qyt_refund')->where('refund_id', $refundId)->update([
|
||||
'status' => self::FAILED, 'message' => mb_substr($text, 0, 250), 'response' => self::dumpRes($res), 'update_time' => time(),
|
||||
]);
|
||||
(new QytOil())->saveLog("退款失败 order={$order['order_id']} refund_no={$refundNo}:{$text}");
|
||||
$order['refund_status'] = self::FAILED;
|
||||
return self::result(false, self::FAILED, $text, self::REASON_UPSTREAM, self::orderData($order, $refundNo));
|
||||
}
|
||||
|
||||
protected static function finishConfirm(array $order, int $refundId, string $refundNo, string $text, array $res): array
|
||||
{
|
||||
Db::name('partners_order')
|
||||
->where('order_id', $order['order_id'])
|
||||
->where('refund_status', self::PROCESSING)
|
||||
->update(['refund_status' => self::CONFIRM, 'refund_text' => mb_substr($text, 0, 250), 'update_time' => time()]);
|
||||
Db::name('qyt_refund')->where('refund_id', $refundId)->update([
|
||||
'status' => self::CONFIRM, 'message' => mb_substr($text, 0, 250), 'response' => self::dumpRes($res), 'update_time' => time(),
|
||||
]);
|
||||
(new QytOil())->saveLog("【告警】退款结果未知 order={$order['order_id']} refund_no={$refundNo}:{$text}");
|
||||
$order['refund_status'] = self::CONFIRM;
|
||||
return self::result(false, self::CONFIRM, $text, self::REASON_PROCESSING, self::orderData($order, $refundNo));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array [order, refund, enterprise, errorMessage]
|
||||
*/
|
||||
protected static function loadConfirmContext(int $orderId): array
|
||||
{
|
||||
$order = Db::name('partners_order')->where('order_id', $orderId)->find();
|
||||
if (!$order) {
|
||||
return [[], [], null, '订单不存在'];
|
||||
}
|
||||
if ((int) $order['refund_status'] !== self::CONFIRM) {
|
||||
return [$order, [], null, '只有「待人工确认」的退款需要确认,当前状态:' . (self::STATUS_TEXT[(int) $order['refund_status']] ?? '')];
|
||||
}
|
||||
$refund = Db::name('qyt_refund')->where('order_id', $orderId)->where('status', self::CONFIRM)
|
||||
->order('refund_id', 'desc')->find();
|
||||
if (!$refund) {
|
||||
return [$order, [], null, '未找到待确认的退款流水'];
|
||||
}
|
||||
$enterprise = EnterpriseModel::detail((int) $order['qyt_enterprise_id']);
|
||||
if (!$enterprise) {
|
||||
return [$order, $refund, null, '本单绑定的企油通企业不存在'];
|
||||
}
|
||||
return [$order, $refund, $enterprise, ''];
|
||||
}
|
||||
|
||||
protected static function orderData(array $order, string $refundNo = ''): array
|
||||
{
|
||||
if ($refundNo === '' && !empty($order['order_id'])) {
|
||||
$refundNo = (string) Db::name('qyt_refund')->where('order_id', $order['order_id'])
|
||||
->order('refund_id', 'desc')->value('refund_no');
|
||||
}
|
||||
return [
|
||||
'oilCardNo' => (string) ($order['oilCardNo'] ?? ''),
|
||||
'saleOrderNo' => (string) ($order['partners_order_no'] ?? ''),
|
||||
'refundNo' => $refundNo,
|
||||
// 已退款:实际退款金额;其余:当前剩余未加油金额(即本次会退的金额)
|
||||
'refundAmount' => number_format((int) ($order['refund_status'] ?? 0) === self::SUCCESS
|
||||
? (float) ($order['refund_amount'] ?? 0)
|
||||
: self::remainAmount($order), 2, '.', ''),
|
||||
'usedAmount' => number_format((float) ($order['used_amount'] ?? 0), 2, '.', ''),
|
||||
'refundStatus' => (int) ($order['refund_status'] ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
protected static function result(bool $ok, int $status, string $message, string $reason = '', array $data = []): array
|
||||
{
|
||||
return [
|
||||
'ok' => $ok,
|
||||
'status' => $status,
|
||||
'status_text' => self::STATUS_TEXT[$status] ?? '',
|
||||
'message' => $message,
|
||||
'reason' => $reason,
|
||||
'data' => $data,
|
||||
];
|
||||
}
|
||||
|
||||
protected static function dumpRes(array $res): string
|
||||
{
|
||||
if (!$res) {
|
||||
return '';
|
||||
}
|
||||
$keep = ['code' => $res['code'] ?? '', 'message' => $res['message'] ?? '',
|
||||
'transport_error' => !empty($res['transport_error']), 'data' => QytOil::maskDeep($res['data'] ?? [])];
|
||||
return mb_substr((string) json_encode($keep, JSON_UNESCAPED_UNICODE), 0, 60000);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user