Files
douba_shop/app/services/other/PrivacyPhoneServices.php
Karlcarlo c7f85ffbda feat: 添加阿里云短信服务支持并优化验证码逻辑
- 引入 alibabacloud/dysmsapi-20170525 依赖,集成阿里云短信服务
- 将短信验证码从6位改为4位,优化用户体验
- 添加聚水潭商品按时间范围同步功能,支持分页拉取
- 增强Cron任务异常处理,添加错误日志记录
- 修复系统附件服务中图片路径处理逻辑
- 更新阿里云SDK相关组件至最新版本
- 添加think-trace调试扩展,便于问题排查
- 优化跨域中间件,使用str_contains替代strpos
2026-04-30 15:19:40 +08:00

378 lines
11 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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;
}
}