完成了基本调整调整
This commit is contained in:
@@ -18,7 +18,9 @@ use app\common\model\Partners as PartnersModel;
|
||||
use app\common\crypto\RsaUtil;
|
||||
use app\common\service\PartnersBalance;
|
||||
use app\common\model\qyt\Enterprise as EnterpriseModel;
|
||||
use app\common\service\QytDriver;
|
||||
use app\common\service\QytOil;
|
||||
use app\common\service\QytRefund;
|
||||
use app\store\model\order\QrCode as QrCodeModel;
|
||||
use cores\exception\BaseException;
|
||||
use think\facade\Request;
|
||||
@@ -168,6 +170,14 @@ class OilCard extends Controller
|
||||
if($created){
|
||||
// 回填流水的订单ID(建单前尚不知 order_id)
|
||||
PartnersBalance::bindOrder($balanceLogId, (int)$OrderModel->order_id);
|
||||
// 司机档案(按身份证号归档并标记信息冲突;内部吞异常,不影响建单)
|
||||
QytDriver::touch([
|
||||
'order_id' => $OrderModel->order_id,
|
||||
'id_card' => $idCard,
|
||||
'driver_name' => $driverName,
|
||||
'mobile' => $mobile,
|
||||
'store_id' => $this->storeId,
|
||||
]);
|
||||
// 存入队列处理
|
||||
$queueService = new QueueService();
|
||||
$queueService->push('partners_queue', json_encode(['order_id'=>$OrderModel->order_id],320));
|
||||
@@ -186,6 +196,76 @@ class OilCard extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单退款(整单全额)
|
||||
* 企油通侧:7.17 查司机支付凭证 -> 7.16 按订单金额调低额度;成功后回补合作商余额,并推送油卡变动通知(optType=refund)
|
||||
* @return Json
|
||||
*/
|
||||
public function refund()
|
||||
{
|
||||
$headers = Request::header();
|
||||
$data = json_decode(Request::instance()->getContent(), true);
|
||||
$appKey = $headers['appkey'] ?? '';
|
||||
$requestId = $headers['requestid'] ?? '';
|
||||
$method = $headers['method'] ?? '';
|
||||
if($method != 'syb.oil.oilCard.refund'){
|
||||
return $this->oilRenderError('12', '0005');
|
||||
}
|
||||
try {
|
||||
Db::name('request_log')->insert([
|
||||
'request_id' => $requestId,
|
||||
'app_key' => $appKey,
|
||||
'create_time' => time()
|
||||
]);
|
||||
} catch (\think\Exception $e) {
|
||||
if (strpos($e->getMessage(), 'Duplicate entry') !== false) {
|
||||
return $this->oilRenderError('12', '0012');
|
||||
}
|
||||
return $this->oilRenderError('99', '9999');
|
||||
}
|
||||
$partners = PartnersModel::detail(['appKey' => $appKey]);
|
||||
if(!$partners){
|
||||
return $this->oilRenderError('02','0004');
|
||||
}
|
||||
$crypto = new RsaUtil($partners);
|
||||
$result = $crypto->processResponse(['header'=>$headers,'encryptData'=>$data]);
|
||||
if ($result['code'] !== '00') {
|
||||
return $this->oilRenderError($result['code'], $result['errCode'], '', $result['errMessage'] ?? '');
|
||||
}
|
||||
$body = $result['data'];
|
||||
if($partners['orgNo'] != ($body['orgNo'] ?? '')){
|
||||
return $this->oilRenderError('12','0014');
|
||||
}
|
||||
$oilCardNo = trim((string)($body['oilCardNo'] ?? ''));
|
||||
if($oilCardNo === ''){
|
||||
return $this->oilRenderError('12','0001', '', '缺少必填请求参数 oilCardNo');
|
||||
}
|
||||
$reason = mb_substr(trim((string)($body['reason'] ?? '')), 0, 100);
|
||||
|
||||
// 只能退自己的订单
|
||||
$order = OrderModel::detail(['oilCardNo' => $oilCardNo, 'partners_id' => $partners['partners_id']]);
|
||||
if(!$order){
|
||||
return $this->oilRenderError('01','2036');
|
||||
}
|
||||
|
||||
$res = QytRefund::apply((int)$order['order_id'], 'api', (string)$appKey, $reason);
|
||||
// 已退款(含重复调用的幂等返回)或结果确认中:都按受理成功返回,由 refundStatus 区分
|
||||
// 确认中说明 7.16 已发出、上游可能已调额,不能告诉下游「失败」让它重试
|
||||
if ($res['ok'] || $res['status'] === QytRefund::CONFIRM) {
|
||||
return $this->oilRenderSuccess($res['data'], '00', $res['message']);
|
||||
}
|
||||
switch ($res['reason']) {
|
||||
case QytRefund::REASON_NOT_FOUND:
|
||||
return $this->oilRenderError('01', '2036', '', $res['message']);
|
||||
case QytRefund::REASON_PROCESSING:
|
||||
return $this->oilRenderError('01', '2032', '', $res['message']);
|
||||
case QytRefund::REASON_STATE:
|
||||
return $this->oilRenderError('01', '2048', '', $res['message']);
|
||||
default:
|
||||
return $this->oilRenderError('01', '2049', '', $res['message']);
|
||||
}
|
||||
}
|
||||
|
||||
public static function createOrderNo(): string
|
||||
{
|
||||
// 获取当前时间戳到毫秒(13位)
|
||||
@@ -469,7 +549,11 @@ class OilCard extends Controller
|
||||
return $this->oilRenderError('12','0013', '', $validate->getError());
|
||||
}
|
||||
|
||||
$detail = OrderModel::detail(['oilCardNo' => $data['oilCardNo']]);
|
||||
// 只能查自己的订单(原先不校验归属,一个合作商可以查到别家的单)
|
||||
$detail = OrderModel::detail(['oilCardNo' => $data['oilCardNo'], 'partners_id' => $partners['partners_id']]);
|
||||
if(!$detail){
|
||||
return $this->oilRenderError('01','2036');
|
||||
}
|
||||
// 组装返回格式
|
||||
$result = [
|
||||
'mobile' => $detail['mobile'],
|
||||
@@ -482,6 +566,15 @@ class OilCard extends Controller
|
||||
'oil_time' => $detail['oil_time'],
|
||||
'use_status' => $detail['use_status'],
|
||||
'use_time' => $detail['use_time'],
|
||||
// 一单可多次加油:已加油金额 / 加油次数 / 剩余可加油(即可退)金额
|
||||
'used_amount' => number_format((float)$detail['used_amount'], 2, '.', ''),
|
||||
'used_num' => (int)$detail['used_num'],
|
||||
'remain_amount' => number_format(max(0, (float)$detail['total_price'] - (float)$detail['used_amount']
|
||||
- ((int)$detail['refund_status'] === 2 ? (float)$detail['refund_amount'] : 0)), 2, '.', ''),
|
||||
// 退款:0未退款 1退款中 2已退款 3退款失败 4退款结果确认中
|
||||
'refund_status' => (int)$detail['refund_status'],
|
||||
'refund_amount' => number_format((float)$detail['refund_amount'], 2, '.', ''),
|
||||
'refund_time' => $detail['refund_time'] ? date('Y-m-d H:i:s', (int)$detail['refund_time']) : '',
|
||||
];
|
||||
return $this->oilRenderSuccess($result);
|
||||
} else {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -340,58 +340,91 @@ class CallbackQueue extends Command
|
||||
continue;
|
||||
}
|
||||
$payTime = !empty($c['pay_time']) ? (strtotime($c['pay_time']) ?: time()) : time();
|
||||
// 落消费流水(uk_consume 去重:重复回调直接忽略)
|
||||
// 一单可以分多次加油:每条推送是一次加油,consume_amount = 本次占用运单金额(分)
|
||||
$consumeFen = (int)($c['consume_amount'] ?? 0);
|
||||
$consumeYuan = number_format($consumeFen / 100, 2, '.', '');
|
||||
$orderCode = trim((string)($c['order_code'] ?? ''));
|
||||
// 去重键:文档里 order_code 是选填,缺失时用本笔消费的特征字段做指纹,
|
||||
// 否则同一运单的第二笔加油会被唯一键误判为重复推送而丢掉(口径与迁移脚本回填一致)
|
||||
$consumeKey = $orderCode !== '' ? $orderCode : 'FP' . md5(implode('|', [
|
||||
$c['quota_id'] ?? '', $c['pay_time'] ?? '', $c['consume_amount'] ?? '',
|
||||
$c['pay_fee'] ?? '', $c['station_code'] ?? '', $c['litter'] ?? '',
|
||||
]));
|
||||
$consumeId = 0;
|
||||
try {
|
||||
Db::name('qyt_consume')->insert([
|
||||
'order_id' => $order['order_id'],
|
||||
'waybill_no' => $c['waybill_no'] ?? '',
|
||||
'waybill_no_code' => $code,
|
||||
'order_code' => $c['order_code'] ?? '',
|
||||
'station_code' => $c['station_code'] ?? '',
|
||||
'station_name' => $c['station_name'] ?? '',
|
||||
'oil_name' => $c['oil_name'] ?? '',
|
||||
'litter' => $c['litter'] ?? '',
|
||||
'oil_price' => (int)($c['oil_price'] ?? 0),
|
||||
'actual_oil_price' => (int)($c['actual_oil_price'] ?? 0),
|
||||
'pay_fee' => (int)($c['pay_fee'] ?? 0),
|
||||
'consume_amount' => (int)($c['consume_amount'] ?? 0),
|
||||
'pay_time' => $payTime,
|
||||
'raw' => json_encode($c, 320),
|
||||
'store_id' => $order['store_id'],
|
||||
'create_time' => time(),
|
||||
]);
|
||||
// 流水落库与订单累计放在同一事务:唯一键挡住重推,重推既不重复累计也不重复通知
|
||||
Db::transaction(function () use ($order, $c, $code, $orderCode, $consumeKey, $payTime, $consumeFen, $consumeYuan, &$consumeId) {
|
||||
$consumeId = (int)Db::name('qyt_consume')->insertGetId([
|
||||
'order_id' => $order['order_id'],
|
||||
'waybill_no' => $c['waybill_no'] ?? '',
|
||||
'waybill_no_code' => $code,
|
||||
'order_code' => $orderCode,
|
||||
'consume_key' => $consumeKey,
|
||||
'station_code' => $c['station_code'] ?? '',
|
||||
'station_name' => $c['station_name'] ?? '',
|
||||
'oil_name' => $c['oil_name'] ?? '',
|
||||
'litter' => $c['litter'] ?? '',
|
||||
'oil_price' => (int)($c['oil_price'] ?? 0),
|
||||
'actual_oil_price' => (int)($c['actual_oil_price'] ?? 0),
|
||||
'pay_fee' => (int)($c['pay_fee'] ?? 0),
|
||||
'consume_amount' => $consumeFen,
|
||||
'pay_time' => $payTime,
|
||||
'raw' => json_encode($c, 320),
|
||||
'store_id' => $order['store_id'],
|
||||
'create_time' => time(),
|
||||
]);
|
||||
// 累计已加油金额 / 次数,并记下最近一次加油信息(原子自增,不读后写)
|
||||
Db::name('partners_order')->where('order_id', $order['order_id'])->update([
|
||||
'used_amount' => Db::raw('`used_amount` + ' . $consumeYuan),
|
||||
'used_num' => Db::raw('`used_num` + 1'),
|
||||
'use_text' => '加油',
|
||||
'qyt_order_code' => $orderCode,
|
||||
'station_name' => $c['station_name'] ?? '',
|
||||
'oil_name' => $c['oil_name'] ?? '',
|
||||
'litter' => $c['litter'] ?? '',
|
||||
'pay_fee' => (int)($c['pay_fee'] ?? 0),
|
||||
'consume_time' => $payTime,
|
||||
'update_time' => time(),
|
||||
]);
|
||||
// use_status 语义不变(0 未加油 / 1 有加油记录):只在第一笔加油时置 1 并记首次加油时间
|
||||
Db::name('partners_order')->where('order_id', $order['order_id'])->where('use_status', 0)->update([
|
||||
'use_status' => 1,
|
||||
'use_time' => time(),
|
||||
'shareNo' => $orderCode !== '' ? $orderCode : $order['shareNo'],
|
||||
]);
|
||||
});
|
||||
} catch (\Exception $e) {
|
||||
if (strpos($e->getMessage(), 'Duplicate') === false) {
|
||||
throw $e;
|
||||
}
|
||||
$SybOil->saveLogStore('企油通消费回调重复,忽略流水:' . $code . '/' . ($c['order_code'] ?? ''));
|
||||
$SybOil->saveLogStore('企油通消费回调重复,忽略(不重复累计、不重复通知):' . $code . '/' . $consumeKey);
|
||||
continue;
|
||||
}
|
||||
// 条件跃迁 use_status 0 -> 1(要求已发放),仅胜出者通知下单平台
|
||||
$affected = Db::name('partners_order')->where('order_id', $order['order_id'])
|
||||
->where('use_status', 0)
|
||||
->where('oil_status', 1)
|
||||
->update([
|
||||
'use_status' => 1,
|
||||
'use_time' => time(),
|
||||
'use_text' => '加油',
|
||||
'qyt_order_code' => $c['order_code'] ?? '',
|
||||
'shareNo' => $c['order_code'] ?: $order['shareNo'],
|
||||
'station_name' => $c['station_name'] ?? '',
|
||||
'oil_name' => $c['oil_name'] ?? '',
|
||||
'litter' => $c['litter'] ?? '',
|
||||
'pay_fee' => (int)($c['pay_fee'] ?? 0),
|
||||
'consume_time' => $payTime,
|
||||
'update_time' => time(),
|
||||
]);
|
||||
if ($affected) {
|
||||
$queueService = new QueueService();
|
||||
$queueService->push('notify_queue', json_encode([
|
||||
'order_id' => $order['order_id'],
|
||||
'optType' => 'add_oil',
|
||||
'retry_count' => 0,
|
||||
], 320));
|
||||
$SybOil->saveLogStore('企油通消费回调处理成功:' . json_encode($c, 320));
|
||||
|
||||
// 超额告警:累计加油 + 已退款 > 订单金额。上游额度按司机凭证累计,
|
||||
// 归属到本单的消费可能与本地可退金额算出的不一致,需要人工核对
|
||||
$fresh = Db::name('partners_order')->where('order_id', $order['order_id'])
|
||||
->field('total_price,used_amount,refund_amount,oil_status_zsy')->find();
|
||||
if ($fresh && (float)$fresh['used_amount'] + (float)$fresh['refund_amount'] > (float)$fresh['total_price'] + 0.005) {
|
||||
$SybOil->saveLogStore('【告警】订单已加油+已退款超出订单金额 order=' . $order['order_id']
|
||||
. ' total=' . $fresh['total_price'] . ' used=' . $fresh['used_amount'] . ' refund=' . $fresh['refund_amount']);
|
||||
}
|
||||
|
||||
// 每笔加油都通知下单平台(amount = 本次占用金额)
|
||||
$notify = json_encode([
|
||||
'order_id' => $order['order_id'],
|
||||
'optType' => 'add_oil',
|
||||
'consume_id' => $consumeId,
|
||||
'retry_count' => 0,
|
||||
], 320);
|
||||
$queueService = new QueueService();
|
||||
if ((int)($fresh['oil_status_zsy'] ?? 2) === 2) {
|
||||
$queueService->push('notify_queue', $notify);
|
||||
} else {
|
||||
// 7.8 发放结果还没确认就先收到了加油:延后 60 秒再通知,尽量让「分油成功」通知先到下游
|
||||
$queueService->laterPush('notify_queue', $notify, 60);
|
||||
}
|
||||
$SybOil->saveLogStore('企油通消费回调处理成功:' . json_encode($c, 320));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
|
||||
@@ -87,12 +87,31 @@ class NotifyQueue extends Command
|
||||
return false;
|
||||
}
|
||||
$partnersOil = new PartnersOil();
|
||||
$optType = (string)($data['optType'] ?? '');
|
||||
|
||||
// 加油:一单可多次加油,每笔单独通知。消息里带 consume_id 时推这一笔的占用金额和消费单号;
|
||||
// 老消息没有 consume_id(上线前入队的)则沿用旧口径推订单金额
|
||||
$consume = null;
|
||||
if ($optType == 'add_oil' && !empty($data['consume_id'])) {
|
||||
$consume = Db::name('qyt_consume')->where('id', (int)$data['consume_id'])->find();
|
||||
}
|
||||
if ($optType == 'refund') {
|
||||
$amount = $order['refund_amount'];
|
||||
} elseif ($consume) {
|
||||
$amount = number_format((int)$consume['consume_amount'] / 100, 2, '.', '');
|
||||
} else {
|
||||
$amount = $order['total_price'];
|
||||
}
|
||||
|
||||
$data = [
|
||||
'oilCardNo' => $order['oilCardNo'],
|
||||
'amount' => $order['total_price'],
|
||||
'shareNo' => $order['shareNo'],
|
||||
'type' => $data['optType'] == 'share_oil' ? 'add' : 'sub',
|
||||
'optType' => $data['optType'],
|
||||
// 分油:订单金额;加油:本次加油占用金额;退款:退款金额
|
||||
'amount' => $amount,
|
||||
// 订单实付金额(即分油金额),所有场景都带,方便对接方拿 amount 对照参考
|
||||
'orderAmount' => number_format((float)$order['total_price'], 2, '.', ''),
|
||||
'shareNo' => $consume && $consume['order_code'] !== '' ? $consume['order_code'] : $order['shareNo'],
|
||||
'type' => $optType == 'share_oil' ? 'add' : 'sub',
|
||||
'optType' => $optType,
|
||||
];
|
||||
$header['method'] = 'syb.notify.oilCard.change';
|
||||
$res = $partnersOil->sendRequest($partners, $partners['callback'], $data, $header);
|
||||
@@ -103,11 +122,16 @@ class NotifyQueue extends Command
|
||||
'oil_callback' => 1,
|
||||
'oil_callback_time' => time(),
|
||||
]);
|
||||
}else{
|
||||
}elseif($data['optType'] == 'add_oil'){
|
||||
// 退款通知成功不记 use_callback,避免把「加油已回调」标错
|
||||
$order->save([
|
||||
'use_callback' => 1,
|
||||
'use_callback_time' => time(),
|
||||
]);
|
||||
// 逐笔记录这一笔加油已通知
|
||||
if ($consume) {
|
||||
Db::name('qyt_consume')->where('id', $consume['id'])->update(['notify_status' => 1, 'notify_time' => time()]);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ declare (strict_types=1);
|
||||
|
||||
namespace app\store\controller;
|
||||
|
||||
use app\common\model\qyt\Enterprise as EnterpriseModel;
|
||||
use app\store\controller\Controller;
|
||||
use app\store\model\Partners as PartnersModel;
|
||||
use app\store\model\partners\Order as OrderModel;
|
||||
@@ -40,15 +41,47 @@ class Partners extends Controller
|
||||
$newList = [];
|
||||
$OrderModel = new OrderModel();
|
||||
$date = date("Y-m-d");
|
||||
// 企油通企业:口径与下单时 Enterprise::resolve 一致(绑定且启用 -> 用绑定的;否则 -> 默认企业)
|
||||
$bindIds = array_filter(array_map('intval', array_column($list['data'], 'qyt_enterprise_id')));
|
||||
$enterprises = $bindIds
|
||||
? array_column(EnterpriseModel::whereIn('enterprise_id', $bindIds)
|
||||
->field('enterprise_id,company_name,passageway,is_enable')->select()->toArray(), null, 'enterprise_id')
|
||||
: [];
|
||||
$default = EnterpriseModel::getDefault();
|
||||
foreach ($list['data'] as $val){
|
||||
$val['day_money'] =$OrderModel->getOrderTotalPrice($date, $date, $val);
|
||||
$val['total_money'] =$OrderModel->getOrderTotalPrice(null, null, $val);
|
||||
$val = array_merge($val, $this->enterpriseInfo((int)$val['qyt_enterprise_id'], $enterprises, $default));
|
||||
$newList[] = $val;
|
||||
}
|
||||
$list['data'] = $newList;
|
||||
return $this->renderSuccess(['list'=>$list,'user'=>$this->store['user']]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 合作商实际使用的企油通企业
|
||||
* qyt_enterprise_source:bind 绑定 / default 未绑定走默认 / invalid 绑定的企业已停用或删除(实际走默认)/ none 无可用企业
|
||||
*/
|
||||
private function enterpriseInfo(int $bindId, array $enterprises, $default): array
|
||||
{
|
||||
$bound = $bindId > 0 ? ($enterprises[$bindId] ?? null) : null;
|
||||
if ($bound && (int)$bound['is_enable'] === 1) {
|
||||
$source = 'bind';
|
||||
$use = $bound;
|
||||
} else {
|
||||
$source = $bindId > 0 ? 'invalid' : 'default';
|
||||
$use = $default ? $default->toArray() : null;
|
||||
!$use && $source = $bindId > 0 ? 'invalid' : 'none';
|
||||
}
|
||||
return [
|
||||
'qyt_enterprise_source' => $source,
|
||||
// 绑定失效时也把原绑定企业名带出来,便于排查
|
||||
'qyt_bind_name' => $bound['company_name'] ?? '',
|
||||
'qyt_enterprise_name' => $use['company_name'] ?? '',
|
||||
'qyt_enterprise_channel' => isset($use['passageway']) ? ($use['passageway'] === 'pingan' ? '平安银行' : '支付宝企业码') : '',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有商户
|
||||
* @return mixed
|
||||
|
||||
@@ -16,7 +16,9 @@ namespace app\store\controller\partners;
|
||||
use app\common\service\PartnersBalance;
|
||||
use app\common\service\QueueService;
|
||||
use app\common\model\qyt\Enterprise as EnterpriseModel;
|
||||
use app\common\service\QytInvoice;
|
||||
use app\common\service\QytOil;
|
||||
use app\common\service\QytRefund;
|
||||
use app\store\controller\Controller;
|
||||
use app\store\model\order\QrCode as QrCodeModel;
|
||||
use app\store\model\partners\Order as OrderModel;
|
||||
@@ -66,8 +68,12 @@ class Order extends Controller
|
||||
if (!$order) {
|
||||
return $this->renderError('订单不存在');
|
||||
}
|
||||
if ($order['use_status'] != 0) {
|
||||
return $this->renderError('该单已加油核销');
|
||||
// 一单可以分多次加油:没加完的单继续给加油地址
|
||||
if ((int) $order['refund_status'] === 2) {
|
||||
return $this->renderError('该单已退款,不能再加油');
|
||||
}
|
||||
if ((float) $order['used_amount'] + 0.005 >= (float) $order['total_price']) {
|
||||
return $this->renderError('该单金额已全部加油');
|
||||
}
|
||||
if ($order['oil_status'] != 1) {
|
||||
return $this->renderError('该单尚未发放成功,无法获取加油地址');
|
||||
@@ -193,12 +199,120 @@ class Order extends Controller
|
||||
if ($order['use_status'] != 1) {
|
||||
return $this->renderError('该单未加油核销,无可回调内容');
|
||||
}
|
||||
(new QueueService())->push('notify_queue', json_encode([
|
||||
'order_id' => $orderId,
|
||||
'type' => 'sub',
|
||||
'optType' => 'add_oil',
|
||||
'retry_count' => 0,
|
||||
], 320));
|
||||
return $this->renderSuccess('已重新推送加油结果');
|
||||
// 每笔加油单独通知:优先补推还没通知成功的;都通知过了就全部重推一遍(下游按 shareNo 去重)
|
||||
$ids = Db::name('qyt_consume')->where('order_id', $orderId)->where('notify_status', 0)->order('id')->column('id');
|
||||
$all = !$ids;
|
||||
$all && $ids = Db::name('qyt_consume')->where('order_id', $orderId)->order('id')->column('id');
|
||||
$queue = new QueueService();
|
||||
if (!$ids) {
|
||||
// 没有消费流水(改造前的历史单),按旧口径推一次
|
||||
$queue->push('notify_queue', json_encode(['order_id' => $orderId, 'optType' => 'add_oil', 'retry_count' => 0], 320));
|
||||
return $this->renderSuccess('已重新推送加油结果');
|
||||
}
|
||||
foreach ($ids as $consumeId) {
|
||||
$queue->push('notify_queue', json_encode([
|
||||
'order_id' => $orderId,
|
||||
'optType' => 'add_oil',
|
||||
'consume_id' => (int) $consumeId,
|
||||
'retry_count' => 0,
|
||||
], 320));
|
||||
}
|
||||
return $this->renderSuccess(($all ? '已重新推送全部 ' : '已补推未通知的 ') . count($ids) . ' 笔加油结果');
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 退款(整单全额:7.17 查凭证 + 7.16 调低额度)
|
||||
// ================================================================
|
||||
|
||||
/**
|
||||
* 发起退款
|
||||
*/
|
||||
public function refund(): Json
|
||||
{
|
||||
$orderId = (int) $this->request->param('orderId');
|
||||
$reason = trim((string) $this->request->param('reason', ''));
|
||||
if (!Db::name('partners_order')->where('order_id', $orderId)->count()) {
|
||||
return $this->renderError('订单不存在');
|
||||
}
|
||||
$res = QytRefund::apply($orderId, 'admin', $this->operator(), $reason);
|
||||
return $res['ok'] ? $this->renderSuccess($res['data'], $res['message']) : $this->renderError($res['message']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认「待人工确认」的退款:向企油通取证(7.13 流水 + 7.17 凭证)
|
||||
*/
|
||||
public function refundConfirm(): Json
|
||||
{
|
||||
$res = QytRefund::confirm((int) $this->request->param('orderId'));
|
||||
return $this->decided($res) ? $this->renderSuccess($res['data'], $res['message']) : $this->renderError($res['message']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 人工判定「待人工确认」的退款
|
||||
* result = success / failed,remark 必填(判定依据)
|
||||
*/
|
||||
public function refundResolve(): Json
|
||||
{
|
||||
$result = (string) $this->request->param('result');
|
||||
if (!in_array($result, ['success', 'failed'], true)) {
|
||||
return $this->renderError('判定结果不合法');
|
||||
}
|
||||
$res = QytRefund::resolve(
|
||||
(int) $this->request->param('orderId'),
|
||||
$result === 'success',
|
||||
$this->operator(),
|
||||
(string) $this->request->param('remark', '')
|
||||
);
|
||||
return $this->decided($res) ? $this->renderSuccess($res['data'], $res['message']) : $this->renderError($res['message']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单的退款流水
|
||||
*/
|
||||
public function refundLog(): Json
|
||||
{
|
||||
$list = Db::name('qyt_refund')
|
||||
->where('order_id', (int) $this->request->param('orderId'))
|
||||
->field('refund_id,refund_no,source,operator,reason,amount,quota_id,total_before,available_before,total_after,available_after,status,message,create_time,update_time')
|
||||
->order('refund_id', 'desc')
|
||||
->select()->toArray();
|
||||
foreach ($list as &$row) {
|
||||
$row['status_text'] = QytRefund::STATUS_TEXT[(int) $row['status']] ?? '';
|
||||
$row['create_time_text'] = date('Y-m-d H:i:s', (int) $row['create_time']);
|
||||
}
|
||||
return $this->renderSuccess(compact('list'));
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 发票(7.14,按「企业 + 消费月份 + 司机」关联)
|
||||
// ================================================================
|
||||
|
||||
public function invoice(): Json
|
||||
{
|
||||
$order = Db::name('partners_order')->where('order_id', (int) $this->request->param('orderId'))->find();
|
||||
if (!$order) {
|
||||
return $this->renderError('订单不存在');
|
||||
}
|
||||
if ((int) $order['use_status'] !== 1) {
|
||||
return $this->renderError('该订单还没有加油记录,暂无发票');
|
||||
}
|
||||
$res = QytInvoice::forOrder($order, (bool) $this->request->param('refresh', 0));
|
||||
return $res['ok'] ? $this->renderSuccess($res, $res['message']) : $this->renderError($res['message']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取证 / 人工判定是否得出了结论:退款成功,或明确判定为「未生效」都算请求成功,
|
||||
* 由返回的 refundStatus 区分;状态不符、取证失败、仍无法判定才算失败
|
||||
*/
|
||||
private function decided(array $res): bool
|
||||
{
|
||||
return $res['ok']
|
||||
|| ($res['status'] === QytRefund::FAILED && $res['reason'] === QytRefund::REASON_UPSTREAM);
|
||||
}
|
||||
|
||||
/** 当前后台操作人 */
|
||||
private function operator(): string
|
||||
{
|
||||
return (string) ($this->store['user']['user_name'] ?? 'admin');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | 后台 · 企油通司机管理
|
||||
// +----------------------------------------------------------------------
|
||||
// | list() 司机档案 + 从订单实时汇总的累计发放 / 已退款 / 订单数
|
||||
// | detail() 档案、涉及企业、最近订单(发放失败原因里能看到二要素核验结果)
|
||||
// | upstream() 实时查企油通:balance 7.12 钱包余额 / wallet 7.13 钱包流水 / voucher 7.17 支付凭证
|
||||
// |
|
||||
// | 身份证号全程只在后端使用,返回前端一律打码。
|
||||
// +----------------------------------------------------------------------
|
||||
declare (strict_types=1);
|
||||
|
||||
namespace app\store\controller\qyt;
|
||||
|
||||
use app\common\model\qyt\Enterprise as EnterpriseModel;
|
||||
use app\common\service\QytOil;
|
||||
use app\common\service\QytRefund;
|
||||
use app\store\controller\Controller;
|
||||
use think\facade\Db;
|
||||
use think\response\Json;
|
||||
|
||||
class Driver extends Controller
|
||||
{
|
||||
public function list(): Json
|
||||
{
|
||||
$param = $this->request->param();
|
||||
$keyword = trim((string) ($param['keyword'] ?? ''));
|
||||
|
||||
$query = Db::name('qyt_driver')->where('store_id', $this->storeId);
|
||||
if ($keyword !== '') {
|
||||
if (preg_match('/^\d{17}[\dXx]$/', $keyword)) {
|
||||
$query->where('id_card', strtoupper($keyword));
|
||||
} elseif (preg_match('/^1\d{10}$/', $keyword)) {
|
||||
$query->where('mobile', $keyword);
|
||||
} else {
|
||||
$query->whereLike('name', '%' . $keyword . '%');
|
||||
}
|
||||
}
|
||||
if (isset($param['conflict']) && $param['conflict'] !== '' && (int) $param['conflict'] >= 0) {
|
||||
$query->where('conflict', (int) $param['conflict']);
|
||||
}
|
||||
|
||||
$list = $query->order('last_order_time', 'desc')->paginate(15)->toArray();
|
||||
$stats = $this->stats(array_column($list['data'], 'id_card'));
|
||||
foreach ($list['data'] as &$row) {
|
||||
$row = $this->formatDriver($row, $stats[$row['id_card']] ?? []);
|
||||
}
|
||||
return $this->renderSuccess(compact('list'));
|
||||
}
|
||||
|
||||
public function detail(int $driverId): Json
|
||||
{
|
||||
$driver = $this->findDriver($driverId);
|
||||
if (!$driver) {
|
||||
return $this->renderError('司机不存在');
|
||||
}
|
||||
$stats = $this->stats([$driver['id_card']]);
|
||||
|
||||
// 涉及过的企业(上游查询要按企业查)
|
||||
$entIds = Db::name('partners_order')->where('store_id', $this->storeId)
|
||||
->where('id_card', $driver['id_card'])->where('qyt_enterprise_id', '>', 0)
|
||||
->group('qyt_enterprise_id')->column('qyt_enterprise_id');
|
||||
$enterprises = $entIds
|
||||
? EnterpriseModel::whereIn('enterprise_id', $entIds)
|
||||
->field('enterprise_id,company_name,passageway,status')->select()->toArray()
|
||||
: [];
|
||||
|
||||
$orders = Db::name('partners_order')->where('store_id', $this->storeId)->where('id_card', $driver['id_card'])
|
||||
->field('order_id,oilCardNo,partners_order_no,total_price,driver_name,mobile,oil_status_zsy,oil_text,use_status,refund_status,refund_text,qyt_enterprise_id,create_time')
|
||||
->order('order_id', 'desc')->limit(20)->select()->toArray();
|
||||
foreach ($orders as &$o) {
|
||||
$o['mobile'] = QytOil::maskMobile((string) $o['mobile']);
|
||||
$o['refund_status_text'] = QytRefund::STATUS_TEXT[(int) $o['refund_status']] ?? '';
|
||||
$o['create_time_text'] = date('Y-m-d H:i:s', (int) $o['create_time']);
|
||||
}
|
||||
|
||||
return $this->renderSuccess([
|
||||
'detail' => $this->formatDriver($driver, $stats[$driver['id_card']] ?? []),
|
||||
'enterprises' => $enterprises,
|
||||
'orders' => $orders,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 实时查企油通
|
||||
* type: balance 7.12 / wallet 7.13 / voucher 7.17
|
||||
*/
|
||||
public function upstream(int $driverId, int $enterpriseId, string $type = 'balance', int $page = 1): Json
|
||||
{
|
||||
$driver = $this->findDriver($driverId);
|
||||
if (!$driver) {
|
||||
return $this->renderError('司机不存在');
|
||||
}
|
||||
$enterprise = EnterpriseModel::detail($enterpriseId);
|
||||
if (!$enterprise) {
|
||||
return $this->renderError('企业不存在');
|
||||
}
|
||||
$qyt = QytOil::forEnterprise($enterprise);
|
||||
$mobile = (string) $driver['mobile'];
|
||||
$idCard = (string) $driver['id_card'];
|
||||
$page = max(1, $page);
|
||||
|
||||
switch ($type) {
|
||||
case 'balance':
|
||||
$res = $qyt->driverBalanceQuery($mobile, $idCard, $page, 20);
|
||||
break;
|
||||
case 'wallet':
|
||||
$res = $qyt->driverWalletQuery($mobile, $idCard, $page, 20);
|
||||
break;
|
||||
case 'voucher':
|
||||
$res = $qyt->getVoucherList($mobile, $idCard, $page, 20);
|
||||
break;
|
||||
default:
|
||||
return $this->renderError('查询类型不合法');
|
||||
}
|
||||
if ($res['code'] !== '00') {
|
||||
return $this->renderError('企油通返回:' . ($res['message'] ?: '查询失败'));
|
||||
}
|
||||
|
||||
$data = (array) $res['data'];
|
||||
// 7.12 / 7.13 金额是「分」,统一转成元再给前端;7.17 本来就是元
|
||||
if ($type === 'balance' || $type === 'wallet') {
|
||||
foreach (['totalGrantAmount', 'availableBalance'] as $k) {
|
||||
isset($data[$k]) && $data[$k . 'Yuan'] = QytOil::fenToYuan($data[$k]);
|
||||
}
|
||||
foreach (['list', 'transactionDetails'] as $listKey) {
|
||||
foreach ((array) ($data[$listKey] ?? []) as $i => $item) {
|
||||
foreach (['total_amount', 'available_amount', 'amount'] as $k) {
|
||||
if (isset($item[$k]) && is_numeric($item[$k])) {
|
||||
$data[$listKey][$i][$k . '_yuan'] = QytOil::fenToYuan((int) $item[$k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $this->renderSuccess(['type' => $type, 'data' => QytOil::maskDeep($data)]);
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
|
||||
private function findDriver(int $driverId): ?array
|
||||
{
|
||||
$row = Db::name('qyt_driver')->where('store_id', $this->storeId)->where('driver_id', $driverId)->find();
|
||||
return $row ?: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从订单实时汇总,按身份证号分组
|
||||
*/
|
||||
private function stats(array $idCards): array
|
||||
{
|
||||
if (!$idCards) {
|
||||
return [];
|
||||
}
|
||||
$rows = Db::name('partners_order')
|
||||
->where('store_id', $this->storeId)
|
||||
->whereIn('id_card', $idCards)
|
||||
->fieldRaw('id_card,'
|
||||
. ' COUNT(*) AS order_num,'
|
||||
. ' SUM(IF(oil_status_zsy = 2, total_price, 0)) AS grant_amount,'
|
||||
. ' SUM(IF(refund_status = 2, refund_amount, 0)) AS refund_amount,'
|
||||
. ' SUM(IF(use_status = 1, 1, 0)) AS used_num,'
|
||||
. ' SUM(used_amount) AS used_amount,'
|
||||
. ' SUM(IF(oil_status_zsy = 3, 1, 0)) AS fail_num,'
|
||||
. ' SUM(IF(oil_status_zsy = 1, 1, 0)) AS pending_num')
|
||||
->group('id_card')
|
||||
->select()->toArray();
|
||||
return array_column($rows, null, 'id_card');
|
||||
}
|
||||
|
||||
private function formatDriver(array $row, array $stat): array
|
||||
{
|
||||
$grant = round((float) ($stat['grant_amount'] ?? 0), 2);
|
||||
$refund = round((float) ($stat['refund_amount'] ?? 0), 2);
|
||||
$out = [
|
||||
'driver_id' => (int) $row['driver_id'],
|
||||
'name' => $row['name'],
|
||||
'mobile' => $row['mobile'],
|
||||
'id_card_mask' => QytOil::maskIdCard((string) $row['id_card']),
|
||||
'conflict' => (int) $row['conflict'],
|
||||
'conflict_text' => $row['conflict_text'],
|
||||
'last_order_time_text' => $row['last_order_time'] ? date('Y-m-d H:i:s', (int) $row['last_order_time']) : '',
|
||||
'order_num' => (int) ($stat['order_num'] ?? 0),
|
||||
'used_num' => (int) ($stat['used_num'] ?? 0),
|
||||
'fail_num' => (int) ($stat['fail_num'] ?? 0),
|
||||
'pending_num' => (int) ($stat['pending_num'] ?? 0),
|
||||
'grant_amount' => number_format($grant, 2, '.', ''),
|
||||
'refund_amount' => number_format($refund, 2, '.', ''),
|
||||
'used_amount' => number_format((float) ($stat['used_amount'] ?? 0), 2, '.', ''),
|
||||
// 本地口径的净发放额,仅供参考;真实可用额度以企油通 7.12 为准
|
||||
'net_amount' => number_format($grant - $refund, 2, '.', ''),
|
||||
];
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
@@ -234,6 +234,25 @@ class Enterprise extends Controller
|
||||
return $this->renderSuccess("同步完成:新增 {$added} 家,更新 {$updated} 家");
|
||||
}
|
||||
|
||||
/**
|
||||
* 7.11 企业账户余额(仅平安银行通道;银行侧实时查询,勿高频调用)
|
||||
*/
|
||||
public function balance(int $enterpriseId): Json
|
||||
{
|
||||
$model = EnterpriseModel::detail($enterpriseId);
|
||||
if (!$model) {
|
||||
return $this->renderError('企业不存在');
|
||||
}
|
||||
$res = QytOil::forEnterprise($model)->distributable();
|
||||
if ($res['code'] !== '00') {
|
||||
return $this->renderError('企油通返回:' . ($res['message'] ?: '查询失败'));
|
||||
}
|
||||
return $this->renderSuccess([
|
||||
'balance' => QytOil::fenToYuan($res['data']['balance'] ?? 0),
|
||||
'available_balance' => QytOil::fenToYuan($res['data']['available_balance'] ?? 0),
|
||||
], '查询成功');
|
||||
}
|
||||
|
||||
/** 记录同步结果 */
|
||||
private function saveSync($model, string $text): void
|
||||
{
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | 后台 · 企油通发票管理(7.14)
|
||||
// +----------------------------------------------------------------------
|
||||
// | list() 发票记录(按 企业 + 通道 + 月份 + 司机手机号 维度)
|
||||
// | sync() 同步:指定企业则同步该企业(可带司机手机号);不指定则批量同步所有启用企业的当月全量
|
||||
// +----------------------------------------------------------------------
|
||||
declare (strict_types=1);
|
||||
|
||||
namespace app\store\controller\qyt;
|
||||
|
||||
use app\common\model\qyt\Enterprise as EnterpriseModel;
|
||||
use app\common\service\QytInvoice;
|
||||
use app\store\controller\Controller;
|
||||
use think\facade\Db;
|
||||
use think\response\Json;
|
||||
|
||||
class Invoice extends Controller
|
||||
{
|
||||
public function list(): Json
|
||||
{
|
||||
$param = $this->request->param();
|
||||
$query = Db::name('qyt_invoice');
|
||||
!empty($param['qyt_enterprise_id']) && $query->where('qyt_enterprise_id', (int) $param['qyt_enterprise_id']);
|
||||
!empty($param['month']) && $query->where('month', (string) $param['month']);
|
||||
!empty($param['mobile']) && $query->where('mobile', trim((string) $param['mobile']));
|
||||
if (isset($param['has_invoice']) && $param['has_invoice'] !== '' && (int) $param['has_invoice'] >= 0) {
|
||||
(int) $param['has_invoice'] === 1 ? $query->where('invoice_count', '>', 0) : $query->where('invoice_count', 0);
|
||||
}
|
||||
|
||||
$list = $query->order(['month' => 'desc', 'invoice_id' => 'desc'])->paginate(15)->toArray();
|
||||
|
||||
$names = EnterpriseModel::column('company_name', 'enterprise_id');
|
||||
foreach ($list['data'] as &$row) {
|
||||
$row = QytInvoice::format($row);
|
||||
$row['company_name'] = $names[$row['qyt_enterprise_id']] ?? '(企业已删除)';
|
||||
}
|
||||
return $this->renderSuccess(compact('list'));
|
||||
}
|
||||
|
||||
public function sync(): Json
|
||||
{
|
||||
$month = trim((string) $this->request->param('month'));
|
||||
$mobile = trim((string) $this->request->param('mobile', ''));
|
||||
$enterpriseId = (int) $this->request->param('enterpriseId', 0);
|
||||
|
||||
if ($enterpriseId > 0) {
|
||||
$enterprise = EnterpriseModel::detail($enterpriseId);
|
||||
if (!$enterprise) {
|
||||
return $this->renderError('企业不存在');
|
||||
}
|
||||
$res = QytInvoice::sync($enterprise, $month, $mobile);
|
||||
return $res['ok'] ? $this->renderSuccess($res['row'], $res['message']) : $this->renderError($res['message']);
|
||||
}
|
||||
|
||||
// 批量:所有启用企业的该月全量
|
||||
$ok = $fail = 0;
|
||||
$errors = [];
|
||||
foreach (EnterpriseModel::where('is_enable', 1)->select() as $enterprise) {
|
||||
$res = QytInvoice::sync($enterprise, $month, '');
|
||||
if ($res['ok']) {
|
||||
$ok++;
|
||||
} else {
|
||||
$fail++;
|
||||
$errors[] = $enterprise['company_name'] . ':' . $res['message'];
|
||||
}
|
||||
}
|
||||
$message = "批量同步完成:成功 {$ok} 家,失败 {$fail} 家" . ($errors ? '(' . implode(';', array_slice($errors, 0, 3)) . ')' : '');
|
||||
return $fail && !$ok ? $this->renderError($message) : $this->renderSuccess([], $message);
|
||||
}
|
||||
}
|
||||
@@ -79,6 +79,8 @@ class Order extends OrderModel
|
||||
'oil_status' => -1, // 分油(发放)状态
|
||||
'oil_status_zsy' => -1, // 企油通发放态 0未提交 1发放中 2成功 3失败
|
||||
'use_status' => -1, // 加油核销状态
|
||||
'refund_status' => -1, // 退款状态 0未退款 1退款中 2已退款 3退款失败 4待人工确认
|
||||
'id_card' => '', // 司机身份证号(从司机管理跳转过来时使用)
|
||||
'betweenTime' => [] // 起止时间
|
||||
]);
|
||||
// 检索查询条件
|
||||
@@ -100,6 +102,10 @@ class Order extends OrderModel
|
||||
$params['oil_status_zsy'] > -1 && $filter[] = ['oil_status_zsy', '=', (int)$params['oil_status_zsy']];
|
||||
// 加油核销状态
|
||||
$params['use_status'] > -1 && $filter[] = ['use_status', '=', (int)$params['use_status']];
|
||||
// 退款状态
|
||||
$params['refund_status'] > -1 && $filter[] = ['order.refund_status', '=', (int)$params['refund_status']];
|
||||
// 司机身份证号(精确匹配)
|
||||
!empty($params['id_card']) && $filter[] = ['order.id_card', '=', strtoupper(trim((string)$params['id_card']))];
|
||||
// 起止时间
|
||||
if (!empty($params['betweenTime'])) {
|
||||
$times = between_time($params['betweenTime']);
|
||||
|
||||
Reference in New Issue
Block a user