- 引入 alibabacloud/dysmsapi-20170525 依赖,集成阿里云短信服务 - 将短信验证码从6位改为4位,优化用户体验 - 添加聚水潭商品按时间范围同步功能,支持分页拉取 - 增强Cron任务异常处理,添加错误日志记录 - 修复系统附件服务中图片路径处理逻辑 - 更新阿里云SDK相关组件至最新版本 - 添加think-trace调试扩展,便于问题排查 - 优化跨域中间件,使用str_contains替代strpos
378 lines
11 KiB
PHP
378 lines
11 KiB
PHP
<?php
|
||
// +----------------------------------------------------------------------
|
||
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
|
||
// +----------------------------------------------------------------------
|
||
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
|
||
// +----------------------------------------------------------------------
|
||
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
|
||
// +----------------------------------------------------------------------
|
||
// | Author: CRMEB Team <admin@crmeb.com>
|
||
// +----------------------------------------------------------------------
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace app\services\other;
|
||
|
||
use app\services\BaseServices;
|
||
use think\facade\Log;
|
||
|
||
/**
|
||
* 隐私小号服务类 (AX模式)
|
||
* Class PrivacyPhoneServices
|
||
* @package app\services\other
|
||
*/
|
||
class PrivacyPhoneServices extends BaseServices
|
||
{
|
||
/**
|
||
* 标准化AX小号:从后往前截取11位手机号
|
||
* @param string $number
|
||
* @return string
|
||
*/
|
||
private function normalizeAxNumber(string $number): string
|
||
{
|
||
if ($number === '') {
|
||
return '';
|
||
}
|
||
|
||
return strlen($number) > 11 ? substr($number, -11) : $number;
|
||
}
|
||
|
||
/**
|
||
* 日志上下文转字符串(ThinkPHP日志上下文仅支持占位符替换)
|
||
* @param mixed $value
|
||
* @return string
|
||
*/
|
||
private function toLogString($value): string
|
||
{
|
||
if (is_string($value)) {
|
||
return $value;
|
||
}
|
||
|
||
$json = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||
return $json === false ? var_export($value, true) : $json;
|
||
}
|
||
|
||
/**
|
||
* API地址
|
||
*/
|
||
private string $apiUrl = 'https://api.mixcom.cn';
|
||
|
||
/**
|
||
* 获取AppKey
|
||
* @return string
|
||
*/
|
||
public function getAppKey(): string
|
||
{
|
||
return env('PRIVACY_PHONE.APPKEY', '');
|
||
}
|
||
|
||
/**
|
||
* 获取AppSecret
|
||
* @return string
|
||
*/
|
||
public function getAppSecret(): string
|
||
{
|
||
return env('PRIVACY_PHONE.APPSECRET', '');
|
||
}
|
||
|
||
/**
|
||
* 检查功能是否开启
|
||
* @return bool
|
||
*/
|
||
public function isEnabled(): bool
|
||
{
|
||
return (bool)env('PRIVACY_PHONE.ENABLE', false);
|
||
}
|
||
|
||
/**
|
||
* 生成签名
|
||
* @param array $params
|
||
* @return string
|
||
*/
|
||
private function generateSign(array $params): string
|
||
{
|
||
$secret = $this->getAppSecret();
|
||
if (empty($secret)) {
|
||
throw new \Exception('隐私小号AppSecret未配置');
|
||
}
|
||
|
||
$params = array_change_key_case($params, CASE_LOWER);
|
||
|
||
foreach ($params as $key => $val) {
|
||
if ($key == 'c' || $key == 'a' || $key == 'sign') {
|
||
unset($params[$key]);
|
||
}
|
||
if ($val === '') {
|
||
unset($params[$key]);
|
||
}
|
||
}
|
||
|
||
ksort($params);
|
||
|
||
$data = '';
|
||
foreach ($params as $key => $val) {
|
||
$data .= $val;
|
||
}
|
||
$data .= $secret;
|
||
|
||
return strtolower(sha1($data . sha1($secret)));
|
||
}
|
||
|
||
/**
|
||
* 发送POST请求
|
||
* @param string $url
|
||
* @param array $params
|
||
* @return array
|
||
*/
|
||
private function post(string $url, array $params): array
|
||
{
|
||
$params['time'] = time();
|
||
$params['appkey'] = $this->getAppKey();
|
||
$params['sign'] = $this->generateSign($params);
|
||
|
||
$ch = curl_init();
|
||
curl_setopt($ch, CURLOPT_URL, $url);
|
||
curl_setopt($ch, CURLOPT_POST, true);
|
||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||
curl_setopt($ch, CURLOPT_POSTFIELDS, $params);
|
||
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
|
||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
||
|
||
$response = curl_exec($ch);
|
||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||
$error = curl_error($ch);
|
||
curl_close($ch);
|
||
|
||
$result = [
|
||
'http_code' => $httpCode,
|
||
'response' => $response,
|
||
'error' => $error,
|
||
'decoded' => json_decode($response, true) ?? []
|
||
];
|
||
|
||
Log::info('隐私小号API请求 url={url} params={params} result={result}', [
|
||
'url' => $url,
|
||
'params' => $this->toLogString($params),
|
||
'result' => $this->toLogString($result),
|
||
]);
|
||
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* 获取可用AX小号
|
||
* @param string $regionCode 区号(可选)
|
||
* @return array
|
||
*/
|
||
public function getAxNumber(string $regionCode = ''): array
|
||
{
|
||
if (!$this->isEnabled()) {
|
||
return [];
|
||
}
|
||
|
||
$params = [];
|
||
if ($regionCode) {
|
||
$params['region_code'] = $regionCode;
|
||
}
|
||
|
||
$url = $this->apiUrl . '/virtual/getaxnumber';
|
||
$result = $this->post($url, $params);
|
||
|
||
if ($result['http_code'] !== 200) {
|
||
throw new \Exception('获取隐私小号失败:HTTP ' . $result['http_code']);
|
||
}
|
||
|
||
$data = $result['decoded'];
|
||
if (empty($data['success']) || $data['code'] != 200 || empty($data['data'])) {
|
||
throw new \Exception('获取隐私小号失败:' . ($data['msg'] ?? '未知错误'));
|
||
}
|
||
|
||
return $data['data'];
|
||
}
|
||
|
||
/**
|
||
* 获取第一个可用小号
|
||
* @return string|null
|
||
*/
|
||
public function getFirstAxNumber(): ?string
|
||
{
|
||
$numbers = $this->getAxNumber();
|
||
return $numbers[0]['number'] ?? null;
|
||
}
|
||
|
||
/**
|
||
* AX绑定
|
||
* @param string $number 隐私小号
|
||
* @param string $aParty A号码(用户手机号)
|
||
* @param array $options 可选参数
|
||
* @return array
|
||
*/
|
||
public function axBind(string $number, string $aParty, array $options = []): array
|
||
{
|
||
if (!$this->isEnabled()) {
|
||
throw new \Exception('隐私小号功能未开启');
|
||
}
|
||
|
||
$params = array_merge([
|
||
'number' => $number,
|
||
'aparty' => $aParty,
|
||
'recording' => 0,
|
||
'recordhinttone' => '',
|
||
'display_call' => 0,
|
||
'callerhinttone' => '',
|
||
'calleehinttone' => '',
|
||
'enddate' => 0,
|
||
], $options);
|
||
|
||
$url = $this->apiUrl . '/virtual/axBind';
|
||
$result = $this->post($url, $params);
|
||
|
||
if ($result['http_code'] !== 200) {
|
||
throw new \Exception('AX绑定失败:HTTP ' . $result['http_code']);
|
||
}
|
||
|
||
$data = $result['decoded'];
|
||
if (empty($data['success']) || $data['code'] != 200) {
|
||
throw new \Exception('AX绑定失败:' . ($data['msg'] ?? '未知错误'));
|
||
}
|
||
|
||
return $data;
|
||
}
|
||
|
||
/**
|
||
* AX解绑
|
||
* @param string $number 隐私小号
|
||
* @param string $aParty A号码(用户手机号)
|
||
* @return array
|
||
*/
|
||
public function unAxBind(string $number, string $aParty): array
|
||
{
|
||
if (!$this->isEnabled()) {
|
||
throw new \Exception('隐私小号功能未开启');
|
||
}
|
||
|
||
$params = [
|
||
'number' => $number,
|
||
'aparty' => $aParty,
|
||
];
|
||
|
||
$url = $this->apiUrl . '/virtual/unAxBind';
|
||
$result = $this->post($url, $params);
|
||
|
||
if ($result['http_code'] !== 200) {
|
||
throw new \Exception('AX解绑失败:HTTP ' . $result['http_code']);
|
||
}
|
||
|
||
$data = $result['decoded'];
|
||
if (empty($data['success']) || $data['code'] != 200) {
|
||
throw new \Exception('AX解绑失败:' . ($data['msg'] ?? '未知错误'));
|
||
}
|
||
|
||
return $data;
|
||
}
|
||
|
||
/**
|
||
* 订单创建时绑定隐私手机号
|
||
* @param string $realPhone 用户真实手机号
|
||
* @return array ['user_phone' => 隐私号, 'bind_id' => 真实手机号, 'status' => 0]
|
||
*/
|
||
public function bindForOrder(string $realPhone): array
|
||
{
|
||
if (!$this->isEnabled()) {
|
||
return [];
|
||
}
|
||
|
||
$result = [
|
||
'user_phone' => '',
|
||
'bind_id' => '',
|
||
'status' => 0
|
||
];
|
||
|
||
try {
|
||
$number = $this->getFirstAxNumber();
|
||
if (!$number) {
|
||
Log::error('获取隐私小号失败:无可用号码');
|
||
return $result;
|
||
}
|
||
|
||
$bindNumber = $this->normalizeAxNumber($number);
|
||
if (strlen($bindNumber) !== 11) {
|
||
Log::error('隐私小号格式异常 raw_number={raw_number} bind_number={bind_number}', [
|
||
'raw_number' => $number,
|
||
'bind_number' => $bindNumber,
|
||
]);
|
||
return $result;
|
||
}
|
||
|
||
$bindResult = $this->axBind($bindNumber, $realPhone);
|
||
|
||
if (!empty($bindResult['success']) && $bindResult['code'] == 200) {
|
||
$result = [
|
||
'user_phone' => $bindNumber,
|
||
'bind_id' => $realPhone,
|
||
'status' => 1
|
||
];
|
||
Log::info('隐私小号绑定成功 privacy_phone={privacy_phone} real_phone={real_phone}', [
|
||
'privacy_phone' => $bindNumber,
|
||
'real_phone' => $realPhone,
|
||
]);
|
||
}
|
||
} catch (\Exception $e) {
|
||
Log::error('隐私小号绑定异常 real_phone={real_phone} ax_number={ax_number} error={error}', [
|
||
'real_phone' => $realPhone,
|
||
'ax_number' => $bindNumber ?? ($number ?? ''),
|
||
'error' => $e->getMessage(),
|
||
]);
|
||
}
|
||
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* 订单完成时解绑隐私手机号
|
||
* @param string $privacyPhone 隐私手机号(user_phone)
|
||
* @param string $realPhone 真实手机号(private_bind_id)
|
||
* @return bool
|
||
*/
|
||
public function unbindForOrder(string $privacyPhone, string $realPhone): bool
|
||
{
|
||
if (!$this->isEnabled()) {
|
||
return false;
|
||
}
|
||
|
||
if (empty($privacyPhone)) {
|
||
return false;
|
||
}
|
||
|
||
$unbindPhone = $this->normalizeAxNumber($privacyPhone);
|
||
if (strlen($unbindPhone) !== 11) {
|
||
Log::error('隐私小号格式异常 privacy_phone={privacy_phone} unbind_phone={unbind_phone}', [
|
||
'privacy_phone' => $privacyPhone,
|
||
'unbind_phone' => $unbindPhone,
|
||
]);
|
||
return false;
|
||
}
|
||
|
||
try {
|
||
$result = $this->unAxBind($unbindPhone, $realPhone);
|
||
|
||
if (!empty($result['success']) && $result['code'] == 200) {
|
||
Log::info('隐私小号解绑成功 privacy_phone={privacy_phone} real_phone={real_phone}', [
|
||
'privacy_phone' => $unbindPhone,
|
||
'real_phone' => $realPhone,
|
||
]);
|
||
return true;
|
||
}
|
||
} catch (\Exception $e) {
|
||
Log::error('隐私小号解绑异常 privacy_phone={privacy_phone} real_phone={real_phone} error={error}', [
|
||
'privacy_phone' => $unbindPhone,
|
||
'real_phone' => $realPhone,
|
||
'error' => $e->getMessage(),
|
||
]);
|
||
}
|
||
|
||
return false;
|
||
}
|
||
}
|