1、合作商余额查询

2、分油账户司机可用额度
This commit is contained in:
2026-10-20 23:21:10 +08:00
parent 45452c1799
commit fafd3e2c16
134 changed files with 2344 additions and 352 deletions
+79 -6
View File
@@ -24,6 +24,10 @@ use app\store\model\Company as CompanyModel;
use app\store\model\Express as ExpressModel;
use app\common\enum\order\PayType as OrderPayTypeEnum;
use app\store\model\order\QrCode as QrCodeModel;
use app\common\model\qyt\Enterprise as EnterpriseModel;
use app\common\service\QytOil;
use app\api\service\User as UserService;
use think\facade\Db;
use cores\exception\BaseException;
use think\facade\Request;
use think\response\Json;
@@ -309,9 +313,77 @@ class Order extends Controller
*/
public function qrCode(): Json
{
// 企油通改造:小程序油品发放/核销链路已停用(上游定聚已下线)。
// 保留接口避免小程序端报错,统一返回提示;如需恢复请改走 QytOil。
return $this->renderError('该功能已停用');
$orderGoodsId = (int)$this->request->param('orderGoodsId');
[$order, $error] = $this->findOilOrder($orderGoodsId);
if ($error !== '') {
return $this->renderError($error);
}
// 免登链接要带本单企业的 creditcode(营业执照号),把登录固定到该企业
$enterprise = EnterpriseModel::detail((int)$order['qyt_enterprise_id']);
if (!$enterprise) {
return $this->renderError('本单绑定的企油通企业不存在,无法获取加油地址');
}
if ($enterprise['status'] !== 'approved') {
return $this->renderError('企业尚未通过企油通审核,暂时无法加油');
}
$h5Url = QytOil::forEnterprise($enterprise)->h5Url((string)$order['mobile']);
if (!$h5Url) {
return $this->renderError('获取加油地址失败,请稍后重试');
}
// 留痕
(new QrCodeModel())->add([
'oilCardNo' => $order['oilCardNo'],
'qrCode' => $h5Url,
'amount' => $order['goods_price'],
'order_id' => (int)$order['mall_order_id'],
'user_id' => (int)$order['user_id'],
'store_id' => (int)$order['store_id'],
]);
return $this->renderSuccess(['qrCode' => $h5Url], '提交成功');
}
/**
* 按订单商品行找到对应的企油通发放单,并校验是否还能加油
*
* 小程序单在支付成功后由 MallOil::create 建了一条 yoshop_partners_order
* (source='mall',mall_order_goods_id 指回这里的订单商品行)。
*
* @param int $orderGoodsId
* @return array [发放单, 错误信息]
* @throws BaseException
*/
private function findOilOrder(int $orderGoodsId): array
{
if ($orderGoodsId <= 0) {
return [null, '缺少参数 orderGoodsId'];
}
$userId = (int)UserService::getCurrentLoginUserId();
$orderGoods = Db::name('order_goods')->where('order_goods_id', '=', $orderGoodsId)->find();
if (!$orderGoods) {
return [null, '未找到订单商品'];
}
// 越权校验:只能取自己订单的加油地址
if ((int)$orderGoods['user_id'] !== $userId) {
return [null, '未找到订单商品'];
}
$order = Db::name('partners_order')
->where('source', '=', 'mall')
->where('mall_order_goods_id', '=', $orderGoodsId)
->find();
if (!$order) {
return [null, '该订单还没有发放记录'];
}
if ((int)$order['oil_status'] !== 1) {
return [null, '油费还在发放中,请稍后再试'];
}
if ((int)$order['refund_status'] === 2) {
return [null, '该订单已退款,不能再加油'];
}
// 一单可以分多次加油:没加完的单继续给加油地址
if ((float)$order['used_amount'] + 0.005 >= (float)$order['total_price']) {
return [null, '该订单金额已全部加油'];
}
return [$order, ''];
}
/**
@@ -321,9 +393,10 @@ class Order extends Controller
*/
public function qrCodeImg(): Json
{
// 企油通改造:小程序油品发放/核销链路已停用(上游定聚已下线)。
// 保留接口避免小程序端报错,统一返回提示;如需恢复请改走 QytOil。
return $this->renderError('该功能已停用');
// 企油通给的是 H5 免登链接,没有二维码图片接口。
// 本商城跑在 H5(微信内置浏览器)下,走的是 qrCode + webview;
// 本方法只在微信小程序环境下被调用,暂不支持。
return $this->renderError('请在微信或浏览器中打开加油链接');
}
public function getBase64($url){
+81 -2
View File
@@ -2,8 +2,13 @@
// +----------------------------------------------------------------------
// | 企油通 · 回调接收
// +----------------------------------------------------------------------
// | 入口:public/qytconsume.php -> /qytNotify/consume
// | 对应企油通 7.10「运单加油明细推送(异步)」,我方须返回 {"code":"100","msg":"Success"}
// | 入口:
// | public/qytconsume.php -> /qytNotify/consume 企油通 7.10「运单加油明细推送」
// | public/qytwaybill.php -> /qytNotify/waybill 企油通 3.10「发放结果通知」
// |
// | 两个回调都是 multipart/form-data,业务参数在表单字段里(JSON 字符串):
// | 7.10 -> params 我方须返回 {"code":"100","msg":"Success"}
// | 3.10 -> data + sign 我方只要返回 HTTP 2xx 即算成功,响应体不做要求
// +----------------------------------------------------------------------
declare (strict_types=1);
@@ -70,4 +75,78 @@ class QytNotify extends Controller
return json(['code' => '100', 'msg' => 'Success']);
}
/**
* 3.10 发放结果通知(运单油费 / 企业福利)
*
* 企油通把 7.7 提交的批次处理完后(全部成功 / 部分成功 / 处理失败)主动推过来,
* 一个批次一条,内容与 7.8 查询结果一致。接上之后就不必再靠轮询等结果。
*
* 约定(见文档 3.10):
* · form-data,字段 data(JSON 字符串)+ sign
* · scene 区分场景:oil=运单油费下发(7.7),welfare=员工福利下发(9.3)
* · 接收端返回 HTTP 2xx 即视为成功;非 2xx 或 5 秒内无响应会重试,最多 5 次
* · 同一批次可能收到多次(重试 / 批次重新提交),必须幂等
* · 明细最多约 1 万条,报文可能很大
*
* 所以这里只做「尽快收下、丢进队列、立刻回 200」,真正的状态跃迁交给
* callback:queue 异步做,避免大报文处理超过上游 5 秒超时导致无谓重试。
*
* @return \think\response\Json
*/
public function waybill()
{
$request = Request::instance();
// ⚠️ 必须直接读 $_POST:cores\Request 给所有入参挂了全局过滤器
// ['my_trim','my_htmlspecialchars','filter_emoji'],走 $request->post() 取到的
// JSON 引号会被转义成 &quot;,json_decode 必然失败(通知会被静默丢掉)
$formData = isset($_POST['data']) ? trim((string) $_POST['data']) : '';
$sign = isset($_POST['sign']) ? trim((string) $_POST['sign']) : '';
$raw = $request->getContent();
$payload = $formData !== '' ? $formData : $raw;
$qyt = new QytOil();
// 明细可能上万条,日志只留头部,完整报文靠 batch_no 去企油通后台对
$qyt->saveLog('3.10 发放结果通知原文[' . $request->contentType() . '](截断):'
. mb_substr($payload, 0, 4000) . (mb_strlen($payload) > 4000 ? ' …(已截断)' : ''));
$data = json_decode($payload, true);
// 兜底:若上游或中间层做过 HTML 转义,反转义后再解析一次
if (!is_array($data) && strpos($payload, '&quot;') !== false) {
$data = json_decode(htmlspecialchars_decode($payload, ENT_QUOTES), true);
}
// 兼容:直接推 JSON body 且外层包了一层 {data: ..., sign: ...}
if (is_array($data) && isset($data['data'])) {
$sign === '' && $sign = (string) ($data['sign'] ?? '');
$data = is_string($data['data']) ? json_decode($data['data'], true) : $data['data'];
}
if (!is_array($data) || empty($data['batch_no'])) {
$qyt->saveLog('3.10 发放结果通知解析失败或缺少 batch_no,已忽略');
// 仍然回 2xx:报文我方解析不了,让上游重试 5 次也没有意义,
// 这类单由 7.8 轮询兜底(轮询并没有下线)
return json(['code' => '100', 'msg' => 'Success']);
}
// TODO 验签(对 data 原串签名,规则见开发指南 2.5,待对接人给到平台公钥与算法确认)
// if ($sign !== '' && !(new QytCrypto())->verifyNotify($payload, $sign)) {
// $qyt->saveLog('3.10 发放结果通知验签失败 batch_no=' . $data['batch_no']);
// return json(['code' => '99', 'msg' => 'sign error']);
// }
// 只处理运单油费(7.7)。员工福利(9.3)我方没有对接,收下回 2xx 即可,
// 否则上游会按失败重试 5 次。
$scene = (string) ($data['scene'] ?? 'oil');
if ($scene !== 'oil') {
$qyt->saveLog('3.10 发放结果通知 scene=' . $scene . '(非运单油费),已忽略 batch_no=' . $data['batch_no']);
return json(['code' => '100', 'msg' => 'Success']);
}
(new QueueService())->push('callback_queue', json_encode([
'header' => ['method' => 'qyt_waybill_result'],
'res' => $data,
], 320));
return json(['code' => '100', 'msg' => 'Success']);
}
}
+41
View File
@@ -17,6 +17,7 @@ use app\api\model\User as UserModel;
use app\api\model\UserCoupon as UserCouponModel;
use app\api\service\User as UserService;
use cores\exception\BaseException;
use think\facade\Db;
/**
* 用户管理
@@ -76,6 +77,46 @@ class User extends Controller
return $this->renderSuccess('恭喜您,手机号绑定成功');
}
/**
* 实名信息(姓名 / 身份证号 / 车牌号)
*
* 分油实名制:上游分油前做「姓名 + 身份证号」二要素认证,所以下单前必须补全。
* 车牌号只在本地留存与后台展示,不上送上游。
* @return Json
* @throws BaseException
*/
public function realname(): Json
{
$userInfo = UserService::getCurrentLoginUser(true);
return $this->renderSuccess([
'realname' => [
'realName' => (string)$userInfo['real_name'],
'idCard' => (string)$userInfo['id_card'],
'plateNo' => (string)$userInfo['plate_no'],
// 身份证号已用于分油后不允许再改(上游按身份证号累计司机额度)
'idCardLocked' => (string)$userInfo['id_card'] !== '' && Db::name('partners_order')
->where('user_id', '=', (int)$userInfo['user_id'])
->where('source', '=', 'mall')
->count() > 0,
'isReady' => UserModel::isRealnameReady($userInfo),
]
]);
}
/**
* 保存实名信息
* @return Json
* @throws BaseException
*/
public function saveRealname(): Json
{
$model = new UserModel;
if (!$model->realname($this->postForm())) {
return $this->renderError($model->getError() ?: '保存失败');
}
return $this->renderSuccess('保存成功');
}
/**
* 修改个人信息(头像昵称)
* @return Json
+70
View File
@@ -16,6 +16,7 @@ use app\common\enum\user\balanceLog\Scene as SceneEnum;
use app\common\library\helper;
use app\store\model\user\BalanceLog as BalanceLogModel;
use think\facade\Cache;
use think\facade\Db;
use app\api\service\User as UserService;
use app\api\model\UserOauth as UserOauthModel;
use app\common\model\User as UserModel;
@@ -112,6 +113,75 @@ class User extends UserModel
return $userInfo->save($data);
}
/**
* 保存实名信息(姓名 / 身份证号 / 车牌号)
*
* 分油是实名制的:上游在分油前会做「姓名 + 身份证号」二要素认证,
* 所以这三项必须在下单前补全。车牌号只在本地留存与后台展示,不上送上游。
*
* @param array $form
* @return bool
* @throws BaseException
*/
public function realname(array $form): bool
{
$realName = trim((string)($form['realName'] ?? ''));
$idCard = strtoupper(trim((string)($form['idCard'] ?? '')));
$plateNo = strtoupper(trim((string)($form['plateNo'] ?? '')));
if ($realName === '') {
$this->error = '请填写真实姓名';
return false;
}
// 中文姓名 2~20 字,允许少数民族姓名里的间隔号
if (!preg_match('/^[\x{4e00}-\x{9fa5}][\x{4e00}-\x{9fa5}·]{1,19}$/u', $realName)) {
$this->error = '姓名格式不正确,请填写身份证上的中文姓名';
return false;
}
if (!preg_match('/^\d{17}[\dX]$/', $idCard)) {
$this->error = '身份证号格式不正确(18位)';
return false;
}
// 省份简称 + 城市字母(不含 I/O,避免与 1/0 混淆) + 5~7 位序号(含新能源 8 位车牌)
if (!preg_match('/^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-HJ-NP-Z][A-HJ-NP-Z0-9]{4,6}$/u', $plateNo)) {
$this->error = '车牌号格式不正确';
return false;
}
// 当前登录的用户信息
$userInfo = UserService::getCurrentLoginUser(true);
// 身份证号一旦用于分油就不允许再改:上游按身份证号累计司机额度,
// 改了会导致历史订单的额度与司机对不上。姓名/车牌可以改。
if ((string)$userInfo['id_card'] !== '' && (string)$userInfo['id_card'] !== $idCard) {
$used = Db::name('partners_order')
->where('user_id', '=', (int)$userInfo['user_id'])
->where('source', '=', 'mall')
->count();
if ($used > 0) {
$this->error = '身份证号已用于分油,不能修改,请联系客服';
return false;
}
}
return $userInfo->save([
'real_name' => $realName,
'id_card' => $idCard,
'plate_no' => $plateNo,
]) !== false;
}
/**
* 实名信息是否已补全(下单前的必要条件)
* @param array|static $user
* @return bool
*/
public static function isRealnameReady($user): bool
{
return trim((string)($user['real_name'] ?? '')) !== ''
&& trim((string)($user['id_card'] ?? '')) !== ''
&& trim((string)($user['plate_no'] ?? '')) !== '';
}
/**
* 验证绑定的手机号
* @param array $data
@@ -370,9 +370,30 @@ class Checkout extends BaseService
'balance' => $this->user['balance'],
'points' => $this->user['points'],
'address_id' => $this->user['address_id'],
// 实名信息(分油实名制,下单前必须补全):结算页据此拦截并跳去补全
'real_name' => (string)$this->user['real_name'],
'id_card' => (string)$this->user['id_card'],
'plate_no' => (string)$this->user['plate_no'],
'isRealnameReady' => UserModel::isRealnameReady($this->user),
'hasOilGoods' => $this->hasOilGoods(),
'needRealname' => $this->hasOilGoods() && !UserModel::isRealnameReady($this->user),
];
}
/**
* 本次结算是否包含油券商品(oilCardType = zsy_ticket)
* 油券要走企油通分油,必须有姓名/身份证号/车牌号
*/
private function hasOilGoods(): bool
{
foreach ($this->goodsList as $goods) {
if (($goods['oilCardType'] ?? '') === 'zsy_ticket') {
return true;
}
}
return false;
}
/**
* 当前用户可用的优惠券列表
* @param float $orderTotalPrice 总金额
@@ -757,6 +778,12 @@ class Checkout extends BaseService
return false;
}
}
// 油券商品要走企油通分油(实名制:上游做姓名+身份证号二要素认证),
// 下单前必须补全实名信息。结算页已拦截,这里是服务端兜底。
if ($this->hasOilGoods() && !UserModel::isRealnameReady($this->user)) {
$this->error = '请先完善姓名、身份证号和车牌号后再下单';
return false;
}
return true;
}
+30 -60
View File
@@ -18,6 +18,7 @@ use app\common\lock\RedisLock;
use app\common\model\Coupon;
use app\common\service\SybOil;
use app\common\service\IdingjuOil;
use app\common\service\MallOil;
use app\store\model\Company as CompanyModel;
use app\store\model\Coupon as CouponModel;
use app\store\model\GoodsCoupon as GoodsCouponModel;
@@ -108,15 +109,6 @@ class PaySuccess extends BaseService
}
// 获取订单详情
$detail = OrderModel::getUserOrderDetail((int)$this->model['order_id']);
// 触发分油接口
$shareStatus = $this->shareOil($detail);
if (!$shareStatus) {
// 记录日志
log_record(['name' => '分油失败', [
'order_id' => $this->model['order_id'],
]]);
// return false;
}
// 事务处理
$this->model->transaction(function () use ($payType, $payData, $detail) {
// 更新订单状态
@@ -126,6 +118,16 @@ class PaySuccess extends BaseService
// 记录订单支付信息
$this->updatePayInfo($payType);
});
// 触发分油/发券(放在事务之后:企油通发放是真金白银,必须等订单确实置为已支付再发,
// 否则事务回滚会出现「订单未支付但油费已下发」)
$shareStatus = $this->shareOil($detail);
if (!$shareStatus) {
// 记录日志;不影响支付成功的结果,发放失败由 7.8 判定后自动退回用户余额
log_record(['name' => '分油失败', [
'order_id' => $this->model['order_id'],
'error' => $this->error,
]]);
}
return true;
}
@@ -250,62 +252,30 @@ class PaySuccess extends BaseService
}
}
/**
* 支付成功后的商品发放
* · 油券(zsy_ticket) -> 建企油通发放单,由 partners:queue 调 7.7 下发
* · 优惠券(coupon) -> 直接发券(原逻辑不变)
*
* 返回 false 只用于写一条「分油失败」日志,不阻断支付成功:用户已经付过款,
* 订单必须正常置为已支付。发放失败由 7.8 判定后自动退回用户余额,或后台「重新发放」。
*/
private function shareOil($order): bool
{
$IdingjuOil = new IdingjuOil();
$ok = true;
// 下单用户(含实名信息:姓名/身份证号/车牌号,结算时已校验必填)
$user = Db::name('user')->where('user_id', (int)$order['user_id'])->find() ?: [];
foreach ($order->goods as $goods) {
/* @var GoodsModel $goods */
// 企油通改造:小程序油品发放链路已停用,支付成功后不再触发上游分油(保留优惠券发放)。
// 如需恢复小程序侧发放,改走 QytOil 并补齐司机三要素来源。
if ($goods['oilCardType'] == 'zsy_ticket') {
continue;
}
if($goods['oilCardType'] == 'zsy_ticket') {
$data['amount'] = $goods['total_price']; // 金额
$company = CompanyModel::detail(['orgNo' => $this->getCompany($goods['goods_no'])]);
if (!$company) {
$this->error = '没有可用参数';
return false;
// 企油通:一个油券商品行建一条发放单,金额取该行实付金额
$res = MallOil::create($order->toArray(), $goods->toArray(), $user);
if (!$res['ok']) {
$this->error = $res['message'];
$goods->save(['oil_text' => mb_substr($res['message'], 0, 190)]);
$ok = false;
}
try {
// 查找一个未被调用的用户
$account = $this->processUserOrders($company['company_id']);
} catch (\Throwable $e) {
$this->error = $e->getMessage();
return false;
}
// 查询公司余额
/*if ($data['amount'] > $company['balance']) {
$this->error = '公司余额不足';
return false;
}*/
// 对接平台接口
$res = $IdingjuOil->oilOilCardShare([
'goodsNo'=>$goods['goods_no'],
'amount'=>$data['amount'],
'userNo'=>$account['userNo'],
'mobile'=>$account['mobile'],
'saleOrderNo'=>$goods['oilCardNo'],
'oilCardType'=>$goods['oilCardType']
], $company);
//$res = json_decode('{"code":"00","message":"请求成功","errCode":null,"errMessage":null,"data":{"oilCardNo":null,"amount":100,"balance":null,"shareNo":"CDV2408021928160001"}}', true);
if ($res['code'] != 1) {
$this->error = ($res['errMessage'] ?? $res['message']);
return false;
}
$item['account_id'] = $account['account_id'];
$item['userNo'] = $account['userNo'];
$item['mobile'] = $account['mobile'];
$item['company_id'] = $company['company_id'];
$item['shareNo'] = $res['data']['trade_no'] ?? '';
$item['qrCode'] = $res['data']['useUrl'] ?? '';
$item['oil_status_zsy'] = $res['data']['state'] ?? 0;
$item['oil_time'] = time();
$item['oil_status'] = $item['oil_status_zsy'] == 2 ? 1 : 0; // 已发券 为1
$goods->save($item);
} elseif($goods['oilCardType'] == 'coupon'){
} elseif ($goods['oilCardType'] == 'coupon') {
// 发放优惠券
$coupons = GoodsCouponModel::getCouponList($goods['goods_id'])->toArray();
if($coupons && is_array($coupons)) {
@@ -319,7 +289,7 @@ class PaySuccess extends BaseService
}
}
}
return true;
return $ok;
}
/**