第二次更新

This commit is contained in:
2026-09-10 20:56:46 +08:00
parent 4d6a7e7aac
commit caeb4d6760
310 changed files with 2427 additions and 382 deletions
+1 -1
View File
@@ -26,7 +26,7 @@ class Qyt
public function __construct(array $conf = null)
{
$this->conf = $conf ?: (array) config('qyt');
$this->conf = $conf ?: \app\common\service\QytConfig::get();
$this->privateKey = $this->loadPrivateKey($this->conf['private_key'] ?? '');
$this->platformPublicKey = $this->loadPublicKey($this->conf['platform_public_key'] ?? '');
}
+7
View File
@@ -55,6 +55,9 @@ class Setting extends EnumBasics
// 分类页模板
const PAGE_CATEGORY_TEMPLATE = 'page_category_template';
// 企油通对接设置(非敏感项;RSA 密钥仍在 .env,不入库)
const QYT = 'qyt';
/**
* 获取订单类型值
* @return array
@@ -110,6 +113,10 @@ class Setting extends EnumBasics
'value' => self::PAGE_CATEGORY_TEMPLATE,
'describe' => '分类页模板',
],
self::QYT => [
'value' => self::QYT,
'describe' => '企油通对接设置',
],
];
}
}
@@ -0,0 +1,109 @@
<?php
// +----------------------------------------------------------------------
// | 企油通企业模型
// +----------------------------------------------------------------------
// | 一个接入方 appId 下可挂多个企业(见《企油通接口对接文档.md》7.22)。
// | 企业级参数:business_license / passageway / institution_id 等存本表;
// | 接入方级参数:appId、RSA 私钥、接口域名仍在 .env,私钥不入库。
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace app\common\model\qyt;
use cores\BaseModel;
class Enterprise extends BaseModel
{
protected $name = 'qyt_enterprise';
protected $pk = 'enterprise_id';
/** 通道:配置键 => 企油通 passageway 编码 */
const PASSAGEWAY_CODE = [
'alipay' => 1,
'pingan' => 2,
];
/** 通道名称 */
const PASSAGEWAY_NAME = [
'alipay' => '支付宝企业码',
'pingan' => '平安银行',
];
/** 企业状态(文档 2.6) */
const STATUS_TEXT = [
'pending' => '待完善',
'submitted' => '待审核',
'approved' => '审核通过',
'rejected' => '审核拒绝',
'frozen' => '已冻结',
];
/**
* 企业详情
* @param int|array $where
* @return static|null
*/
public static function detail($where)
{
!is_array($where) && $where = ['enterprise_id' => (int) $where];
return static::get($where);
}
/**
* 取默认企业(合作商未绑定时的兜底)
* @return static|null
*/
public static function getDefault()
{
return static::where('is_enable', 1)->where('is_default', 1)
->order('enterprise_id', 'asc')->find();
}
/**
* 解析某个合作商本次下单应使用的企业
* 优先级:合作商绑定的企业 -> 默认企业;都取不到返回 null,由调用方拒单
* @param int $enterpriseId 合作商绑定的企业ID,0 表示未绑定
* @return static|null
*/
public static function resolve(int $enterpriseId = 0)
{
if ($enterpriseId > 0) {
$one = static::where('enterprise_id', $enterpriseId)->where('is_enable', 1)->find();
if ($one) {
return $one;
}
}
return static::getDefault();
}
/** 当前通道的企油通 passageway 编码 */
public function passagewayCode(): int
{
return self::PASSAGEWAY_CODE[$this->getData('passageway')] ?? self::PASSAGEWAY_CODE['alipay'];
}
/** 当前通道中文名 */
public function passagewayName(): string
{
return self::PASSAGEWAY_NAME[$this->getData('passageway')] ?? '';
}
/** 当前通道是否要求司机身份证号(平安银行要求实名) */
public function requireIdCard(): bool
{
return $this->getData('passageway') === 'pingan';
}
/** 状态中文(访问器,后台列表直接可用) */
public function getStatusTextAttr($value, $data): string
{
return self::STATUS_TEXT[$data['status'] ?? ''] ?? ($data['status'] ?? '');
}
/** 通道中文(访问器) */
public function getPassagewayTextAttr($value, $data): string
{
return self::PASSAGEWAY_NAME[$data['passageway'] ?? ''] ?? '';
}
}
@@ -312,6 +312,25 @@ class Setting extends BaseModel
"d) 买家在完成该笔交易(订单状态为“已签收”)后才能得到此笔交易的相应积分,如购买商品参加店铺其他优惠,则优惠的金额部分不享受积分获取;",
],
],
// 企油通对接设置
// ⚠️ 只放非敏感项:RSA 私钥与平台公钥始终从 .env 读取,绝不入库,
// 避免数据库备份文件等同于资金级密钥副本。见 app\common\service\QytConfig
SettingEnum::QYT => [
'key' => SettingEnum::QYT,
'describe' => '企油通对接设置',
// 均为「接入方级」参数。企业级参数(执照号 / 通道 / 机构号)
// 在「后台 → 企业管理」里按企业维护,不在这里配。
'values' => [
'base_url' => '', // 接口域名,不含结尾斜杠
'app_id' => '', // 接入方 appId
'no_prefix' => 'HM', // 编号前缀默认值(企业可各自覆盖)
'voucher_name' => '运单卡', // 7.7 凭证名称默认值(企业可各自覆盖)
'h5_base_url' => '', // 7.15 落地页
'h5_app_id' => '', // 7.15 免登 appId
'consume_notify_url' => '', // 7.10 回调地址(仅记录,实际在企油通后台配)
'log' => 1, // 是否记录详细请求/响应日志
],
],
// 分类页模板
SettingEnum::PAGE_CATEGORY_TEMPLATE => [
'key' => SettingEnum::PAGE_CATEGORY_TEMPLATE,
+147
View File
@@ -0,0 +1,147 @@
<?php
// +----------------------------------------------------------------------
// | 企油通配置读取
// +----------------------------------------------------------------------
// | 三层来源,从低到高:
// | 1. config/qyt.php —— 读 .env,含【密钥】与接口域名(接入方级)
// | 2. 后台「企油通对接设置」—— yoshop_store_setting.key='qyt'(接入方级默认值)
// | 3. 企业信息 —— yoshop_qyt_enterprise(企业级:执照号 / 通道 / 机构号 …)
// |
// | 分级依据见《企油通接口对接文档.md》:appId 与 RSA 密钥属于「接入方(慧明)」,
// | 一个 appId 下可挂多个企业;business_license、passageway、institution_id 是企业级。
// |
// | 敏感项(private_key、platform_public_key)只从 .env 读,任何一层都不能覆盖,
// | 数据库备份 / SQL 注入 / 后台账号失窃都拿不到签名私钥。
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace app\common\service;
use app\common\enum\Setting as SettingEnum;
use app\common\model\qyt\Enterprise as EnterpriseModel;
use think\facade\Db;
class QytConfig
{
/** 常驻进程内缓存有效期(秒):后台改完最迟这么久生效 */
const TTL = 10;
/** 后台设置可覆盖的接入方级标量项 */
const OVERRIDABLE = [
'base_url', 'app_id', 'no_prefix', 'voucher_name',
'h5_base_url', 'h5_app_id', 'consume_notify_url',
];
/** @var array|null */
protected static $cache = null;
/** @var int */
protected static $cacheAt = 0;
/**
* 接入方级配置(不含任何企业信息)
*/
public static function get(): array
{
if (self::$cache !== null && (time() - self::$cacheAt) < self::TTL) {
return self::$cache;
}
$conf = (array) config('qyt');
$db = static::dbValues();
foreach (self::OVERRIDABLE as $k) {
if (isset($db[$k]) && trim((string) $db[$k]) !== '') {
$conf[$k] = trim((string) $db[$k]);
}
}
// 日志开关是布尔,0 是合法值,不能用非空判断
if (isset($db['log'])) {
$conf['log'] = (bool) $db['log'];
}
self::$cache = $conf;
self::$cacheAt = time();
return $conf;
}
/**
* 叠加企业级参数,得到某个企业实际使用的完整配置
*
* @param EnterpriseModel|array $enterprise
* @return array 额外带 enterprise_id / enterprise_name / passageway_code / require_id_card
*/
public static function forEnterprise($enterprise): array
{
$conf = self::get();
$e = $enterprise instanceof EnterpriseModel ? $enterprise->toArray() : (array) $enterprise;
$passageway = (string) ($e['passageway'] ?? 'alipay');
!isset(EnterpriseModel::PASSAGEWAY_CODE[$passageway]) && $passageway = 'alipay';
$conf['enterprise_id'] = (int) ($e['enterprise_id'] ?? 0);
$conf['enterprise_name'] = (string) ($e['company_name'] ?? '');
$conf['business_license'] = (string) ($e['business_license'] ?? '');
$conf['passageway'] = $passageway;
$conf['passageway_code'] = EnterpriseModel::PASSAGEWAY_CODE[$passageway];
$conf['passageway_name'] = EnterpriseModel::PASSAGEWAY_NAME[$passageway];
$conf['institution_id'] = (string) ($e['institution_id'] ?? '');
// 平安银行通道要求实名
$conf['require_id_card'] = $passageway === 'pingan';
// 以下留空则回落接入方级默认值
foreach (['app_id', 'no_prefix', 'voucher_name'] as $k) {
$v = trim((string) ($e[$k] ?? ''));
$v !== '' && $conf[$k] = $v;
}
return $conf;
}
/**
* 取单项,支持点号:QytConfig::item('h5_base_url')
* @param mixed $default
* @return mixed
*/
public static function item(string $path, $default = null)
{
$node = self::get();
foreach (explode('.', $path) as $seg) {
if (!is_array($node) || !array_key_exists($seg, $node)) {
return $default;
}
$node = $node[$seg];
}
return $node;
}
/** 清掉进程内缓存(单元测试 / 后台保存后使用) */
public static function flush(): void
{
self::$cache = null;
self::$cacheAt = 0;
}
/**
* 读后台设置。直接查表而不用 Setting 模型,是因为常驻命令行进程里
* BaseModel::$storeId 不会被初始化(只有 store/api 模块才赋值),
* 走模型会查不到数据。本项目为单商城部署,取 store_id 最小的一条即可。
*/
protected static function dbValues(): array
{
try {
$json = Db::name('store_setting')
->where('key', SettingEnum::QYT)
->order('store_id', 'asc')
->value('values');
if (empty($json)) {
return [];
}
$arr = json_decode((string) $json, true);
return is_array($arr) ? $arr : [];
} catch (\Throwable $e) {
// 表不存在 / 库连不上时静默回落到 .env,不能因为读设置失败就中断发放
return [];
}
}
}
+289 -47
View File
@@ -1,95 +1,310 @@
<?php
// +----------------------------------------------------------------------
// | 企油通 · 上游接口客户端(支付宝企业码通道)
// | 企油通 · 上游接口客户端
// +----------------------------------------------------------------------
// | 本次实际对接仅 3 个接口:
// | 7.7 运单油费下发 createAndSendQuota —— 本类 createAndSendQuota()
// | 7.15 H5 跳转地址 (本地签名生成 URL) —— 本类 h5Url()
// | 7.10 运单加油明细推送(异步回调) —— app\api\controller\QytNotify
// | 通道固定为支付宝企业码(passageway=1),司机手机号 + 姓名必填。
// | 多企业:一个接入方 appId 下可挂多个企业,业务接口靠 business_license 区分。
// | 用法:QytOil::forEnterprise($enterprise)->createAndSendQuota([...])
// | 不带企业的接口(7.22 企业列表)用 new QytOil() 即可。
// |
// | 已实现:
// | 7.2 企业申请创建 openAnAccount
// | 7.5 企业账户查询 accountQuery
// | 7.7 运单油费下发 createAndSendQuota
// | 7.15 H5免登地址 h5Url(本地签名,不发请求)
// | 7.22 企业列表查询 enterpriseList
// | 7.10 消费回调由 app\api\controller\QytNotify 接收
// |
// | 通道:alipay=支付宝企业码(passageway=1) / pingan=平安银行(passageway=2),
// | 通道开关存在企业信息里。平安通道要求实名,7.7 必传 id_card。
// | 金额:对外「元」入参,7.7 内部换算为「分」。
// |
// | ⚠️ 7.7 是异步接口,返回成功只代表【受理】,真实发放结果需用 7.8
// | waybillBatch 按 batch_no 查询(见文档 §5 第 7 条)。当前状态机仍按
// | 「受理即完成」处理,7.8 轮询尚未接入。
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace app\common\service;
use app\common\crypto\Qyt as QytCrypto;
use app\common\model\qyt\Enterprise as EnterpriseModel;
class QytOil extends BaseService
{
// 支付宝企业码通道
const PASSAGEWAY_ALIPAY = 1;
// 通道标识
const CHANNEL_ALIPAY = 'alipay';
const CHANNEL_PINGAN = 'pingan';
/** @var array config('qyt') */
/** @var array 合并后的配置(接入方级 + 企业级) */
protected $conf;
/** @var QytCrypto */
protected $crypto;
/** @var EnterpriseModel|array|null 当前企业 */
protected $enterprise = null;
protected function initialize()
{
$this->conf = (array) config('qyt');
$this->conf = QytConfig::get();
$this->crypto = new QytCrypto($this->conf);
}
// ================================================================
// 企业上下文
// ================================================================
/**
* 绑定企业后返回实例(业务接口的常规入口)
* @param EnterpriseModel|array $enterprise
*/
public static function forEnterprise($enterprise): self
{
return (new static())->useEnterprise($enterprise);
}
/**
* 切换当前企业:执照号 / 通道 / 机构号 / 编号前缀等随之变化
* @param EnterpriseModel|array $enterprise
*/
public function useEnterprise($enterprise): self
{
$this->enterprise = $enterprise;
$this->conf = QytConfig::forEnterprise($enterprise);
$this->crypto = new QytCrypto($this->conf);
return $this;
}
public function enterpriseId(): int
{
return (int) ($this->conf['enterprise_id'] ?? 0);
}
public function enterpriseName(): string
{
return (string) ($this->conf['enterprise_name'] ?? '');
}
public function businessLicense(): string
{
return (string) ($this->conf['business_license'] ?? '');
}
/** 当前通道标识 alipay / pingan */
public function channelKey(): string
{
return (string) ($this->conf['passageway'] ?? self::CHANNEL_ALIPAY);
}
/** 当前通道中文名 */
public function channelName(): string
{
return (string) ($this->conf['passageway_name'] ?? '');
}
/** 当前通道的企油通 passageway 编码:1 支付宝企业码 / 2 平安银行 */
public function channelCode(): int
{
return (int) ($this->conf['passageway_code'] ?? 1);
}
/** 当前通道是否必须上送司机身份证号(平安银行=是) */
public function requireIdCard(): bool
{
return !empty($this->conf['require_id_card']);
}
/**
* 生成企油通全平台唯一编号(加企业前缀),确定性派生 -> 重试保持一致
* batch_no / waybill_no / waybill_no_code 都是【全平台唯一】而非接入方内唯一,
* 所以各企业建议用不同前缀,避免与其他接入方或兄弟企业撞号。
* @param string $type B=batch / W=waybill / C=waybill_no_code
* @param string $seed 订单号 / order_id 等业务种子
*/
public function no(string $type, string $seed): string
{
return ((string) ($this->conf['no_prefix'] ?: 'HM')) . $type . $seed;
}
// ================================================================
// 7.7 运单油费下发(一张订单 = 1 batch / 1 waybill / 1 employee)
// ================================================================
/**
* @param array $data ['batch_no','waybill_no','waybill_no_code','mobile','name','amount'(元)]
* @param array $data ['batch_no','waybill_no','waybill_no_code','mobile','name','amount'(元),'id_card'(平安通道必填)]
* @return array 归一化 ['code'=>'00'|其它, 'data'=>[], 'message'=>'', 'dup_batch'=>bool]
*/
public function createAndSendQuota(array $data): array
{
if (($err = $this->requireEnterprise()) !== null) {
return $err;
}
// 支付宝企业码通道下 institution_id 必填;缺了就地失败,不把空值发给企油通
if ($this->channelCode() === 1 && $this->conf['institution_id'] === '') {
return $this->fail("企业[{$this->enterpriseName()}]未配置机构号(institution_id),支付宝企业码通道无法下发");
}
$idCard = trim((string) ($data['id_card'] ?? ''));
if ($this->requireIdCard() && $idCard === '') {
return $this->fail("通道[{$this->channelName()}]要求实名,缺少司机身份证号(idCard)");
}
$employee = [
'mobile' => $data['mobile'],
'name' => $data['name'],
'quota_amount' => self::yuanToFen($data['amount']), // 分
'voucher_name' => $this->conf['voucher_name'],
'effective_start' => '', // 空=当日生效
'effective_end' => '', // 空=长期有效
'waybill_no_code' => $data['waybill_no_code'],
];
// 身份证号仅在要求实名的通道上送,避免向不需要的通道多传敏感信息
if ($this->requireIdCard()) {
$employee['id_card'] = $idCard;
}
$params = [
'batch_no' => $data['batch_no'],
'business_license' => $this->conf['business_license'],
'passageway' => (string) self::PASSAGEWAY_ALIPAY,
'institution_id' => $this->conf['institution_id'],
'business_license' => $this->businessLicense(),
'passageway' => (string) $this->channelCode(),
'institution_id' => (string) $this->conf['institution_id'],
'waybill_list' => [[
'waybill_no' => $data['waybill_no'],
'employee_list' => [[
'mobile' => $data['mobile'],
'name' => $data['name'],
'quota_amount' => self::yuanToFen($data['amount']), // 分
'voucher_name' => $this->conf['voucher_name'],
'effective_start' => '', // 空=当日生效
'effective_end' => '', // 空=长期有效
'waybill_no_code' => $data['waybill_no_code'],
]],
'employee_list' => [$employee],
]],
];
return $this->post('/enterprise/api/zhyk/createAndSendQuota', $params);
}
// ================================================================
// 7.8 运单发放结果查询(7.7 是异步的,真实结果只能从这里拿)
// ================================================================
/**
* @param string $batchNo 7.7 提交时用的批次号
* @return array data: batch_no / status(1待处理 2处理中 3全部成功 4部分成功 5处理失败) / list[]
* list[] 里每条含 waybill_no_code / handle_status(1待处理 2成功 3失败) / error_msg
*/
public function waybillBatch(string $batchNo): array
{
$params = ['batch_no' => $batchNo];
// business_license 可选;传了会校验归属,能更早发现串企业的问题
$this->businessLicense() !== '' && $params['business_license'] = $this->businessLicense();
return $this->post('/enterprise/api/zhyk/waybillBatch', $params);
}
/**
* 从 7.8 的返回里挑出某条运单流水的处理结果
* @return array|null ['handle_status'=>int, 'error_msg'=>string, 'remark'=>string]
*/
public static function pickWaybill(array $data, string $waybillNoCode): ?array
{
foreach ((array) ($data['list'] ?? []) as $row) {
if ((string) ($row['waybill_no_code'] ?? '') === $waybillNoCode) {
return [
'handle_status' => (int) ($row['handle_status'] ?? 0),
'error_msg' => (string) ($row['error_msg'] ?? ''),
'remark' => (string) ($row['remark'] ?? ''),
];
}
}
return null;
}
// ================================================================
// 7.2 企业申请创建
// ================================================================
/**
* @param array $data ['business_license','business_license_url','passageway'(如 "1,2"),'company_name','contact_mobile','contact_name']
*/
public function openAnAccount(array $data): array
{
return $this->post('/enterprise/api/zhyk/openAnAccount', [
'business_license' => $data['business_license'],
'business_license_url' => $data['business_license_url'],
'passageway' => (string) $data['passageway'],
'company_name' => $data['company_name'],
'contact_mobile' => $data['contact_mobile'],
'contact_name' => $data['contact_name'],
]);
}
// ================================================================
// 7.5 企业账户信息查询
// ================================================================
public function accountQuery(): array
{
if (($err = $this->requireEnterprise()) !== null) {
return $err;
}
return $this->post('/enterprise/api/zhyk/accountQuery', [
'business_license' => $this->businessLicense(),
'passageway' => (string) $this->channelCode(),
]);
}
// ================================================================
// 7.22 企业列表查询(枚举本 appId 名下企业,需先开通权限,未开通返回 403)
// ================================================================
public function enterpriseList(array $params = []): array
{
return $this->post('/enterprise/api/enterprise/list', [
'page' => $params['page'] ?? 1,
'page_size' => $params['page_size'] ?? 100,
]);
}
// ================================================================
// 7.15 H5 免登加油地址(本地签名生成 URL,不发起接口请求)
// 文档:本地址与支付通道无关,落地页由系统按企业实际通道自动判定
// ================================================================
public function h5Url(string $mobile): string
{
$creditcode = !empty($this->conf['h5_with_creditcode']) ? (string) $this->conf['business_license'] : '';
// creditcode 传了则本次登录固定到该企业,且必须一并入签
$creditcode = !empty($this->conf['h5_with_creditcode']) ? $this->businessLicense() : '';
return $this->crypto->h5Url($mobile, $creditcode);
}
// ================================================================
// 内部:统一请求
// 内部
// ================================================================
/** 未绑定企业 / 企业没有执照号时的统一拦截 */
protected function requireEnterprise(): ?array
{
if ($this->businessLicense() === '') {
return $this->fail('未指定企业或企业缺少统一社会信用代码(business_license)');
}
return null;
}
/** 构造一个失败返回,并记日志 */
protected function fail(string $message): array
{
$this->saveLog('[配置错误] ' . $message);
return ['code' => 'ERR', 'data' => [], 'message' => $message, 'dup_batch' => false, 'raw' => []];
}
/**
* @return array ['code' => '00'成功 | 其它, 'data' => array, 'message' => string, 'dup_batch' => bool, 'raw' => array]
*/
protected function post(string $path, array $bizParams): array
{
$url = rtrim((string) $this->conf['base_url'], '/') . $path;
$url = rtrim((string) $this->conf['base_url'], '/') . $path;
$body = $this->crypto->envelope($bizParams);
$this->saveLog("[请求] {$path} " . json_encode(['url' => $url, 'params' => $bizParams], JSON_UNESCAPED_UNICODE));
// 日志脱敏:手机号 / 身份证号不落明文
$this->saveLog("[请求] {$path} " . json_encode(
['企业' => $this->enterpriseName(), 'url' => $url, 'params' => self::maskDeep($bizParams)],
JSON_UNESCAPED_UNICODE));
$resp = $this->curlPost($url, json_encode($body, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
$this->saveLog("[响应] {$path} " . $resp);
$this->saveLog("[响应] {$path} " . self::maskText($resp));
$json = json_decode($resp, true);
if (!is_array($json)) {
@@ -97,15 +312,13 @@ class QytOil extends BaseService
}
// 通用响应结构:{ status: ok|error, data, message, serialNo, sign }
$ok = ($json['status'] ?? '') === 'ok';
// 【TODO 待对接人确认】是否所有响应都带 sign、验签串如何构造
// if ($ok && !empty($json['sign'])) { $ok = $this->crypto->verify($json['data'] ?? [], $json['sign']); }
$ok = ($json['status'] ?? '') === 'ok';
$message = (string) ($json['message'] ?? '');
// 幂等:batch_no 已存在 —— 视为「已受理」,供 7.7 重试判定
$dupBatch = false;
if (!$ok && $message !== '') {
foreach ((array) (config('qyt.dup_batch_hints') ?: ['已存在', '重复', 'duplicate', 'exist']) as $hint) {
foreach ((array) ($this->conf['dup_batch_hints'] ?: ['已存在', '重复', 'duplicate', 'exist']) as $hint) {
if (mb_stripos($message, $hint) !== false) {
$dupBatch = true;
break;
@@ -145,9 +358,49 @@ class QytOil extends BaseService
return (string) $resp;
}
// ================================================================
// 工具
// ================================================================
// ---------------- 敏感信息脱敏(日志 / 后台展示复用) ----------------
/** 手机号:138****8000 */
public static function maskMobile(string $s): string
{
return strlen($s) === 11 ? substr($s, 0, 3) . '****' . substr($s, -4) : $s;
}
/** 身份证号:4501**********1234 */
public static function maskIdCard(string $s): string
{
$len = strlen($s);
return $len >= 8 ? substr($s, 0, 4) . str_repeat('*', $len - 8) . substr($s, -4) : $s;
}
/** 递归脱敏数组(按键名识别) */
public static function maskDeep($data)
{
if (!is_array($data)) {
return $data;
}
foreach ($data as $k => $v) {
if (is_array($v)) {
$data[$k] = self::maskDeep($v);
} elseif ($k === 'mobile') {
$data[$k] = self::maskMobile((string) $v);
} elseif ($k === 'id_card' || $k === 'idCard') {
$data[$k] = self::maskIdCard((string) $v);
}
}
return $data;
}
/** 脱敏 JSON 文本里的手机号 / 身份证号(用于原始响应报文落日志) */
public static function maskText(string $s): string
{
$s = preg_replace_callback('/"(id_card|idCard)"\s*:\s*"([^"]+)"/i',
function ($m) { return '"' . $m[1] . '":"' . self::maskIdCard($m[2]) . '"'; }, $s);
return (string) preg_replace_callback('/"mobile"\s*:\s*"(\d{11})"/',
function ($m) { return '"mobile":"' . self::maskMobile($m[1]) . '"'; }, $s);
}
// ---------------- 工具 ----------------
/** 元 → 分 */
public static function yuanToFen($yuan): int
@@ -161,17 +414,6 @@ class QytOil extends BaseService
return bcdiv((string) (int) $fen, '100', 2);
}
/**
* 生成企油通全平台唯一编号(加企业前缀),确定性派生 -> 重试保持一致
* @param string $type B=batch / W=waybill / C=waybill_no_code
* @param string $seed 订单号 / order_id 等业务种子
*/
public static function genNo(string $type, string $seed): string
{
$prefix = (string) (config('qyt.no_prefix') ?: 'HM');
return $prefix . $type . $seed;
}
/** 记录日志到 runtime/storelog */
public function saveLog($data): void
{