Files
douba_shop/app/services/other/PrivacyPhoneServices.php
Karlcarlo 42d98407de feat: 集成隐私小号功能保护用户手机号
新增隐私小号服务类,在订单创建时自动绑定隐私手机号,收货时自动解绑
在快递发货和收货流程中使用隐私小号替代真实手机号,保护用户隐私
添加数据库字段存储绑定关系,提供系统配置开关和测试脚本
2026-04-01 00:43:51 +08:00

330 lines
9.1 KiB
PHP
Raw 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
{
/**
* API地址
*/
private string $apiUrl = 'https://api.mixcom.cn';
/**
* 获取AppKey
* @return string
*/
public function getAppKey(): string
{
return sys_config('privacy_phone_appkey', '');
}
/**
* 获取AppSecret
* @return string
*/
public function getAppSecret(): string
{
return sys_config('privacy_phone_appsecret', '');
}
/**
* 检查功能是否开启
* @return bool
*/
public function isEnabled(): bool
{
return (bool)sys_config('privacy_phone_status', 0);
}
/**
* 生成签名
* @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
]);
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;
}
$bindResult = $this->axBind($number, $realPhone);
if (!empty($bindResult['success']) && $bindResult['code'] == 200) {
$result = [
'user_phone' => $number,
'bind_id' => $realPhone,
'status' => 1
];
Log::info('隐私小号绑定成功', [
'privacy_phone' => $number,
'real_phone' => $realPhone
]);
}
} catch (\Exception $e) {
Log::error('隐私小号绑定异常', [
'real_phone' => $realPhone,
'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;
}
try {
$result = $this->unAxBind($privacyPhone, $realPhone);
if (!empty($result['success']) && $result['code'] == 200) {
Log::info('隐私小号解绑成功', [
'privacy_phone' => $privacyPhone,
'real_phone' => $realPhone
]);
return true;
}
} catch (\Exception $e) {
Log::error('隐私小号解绑异常', [
'privacy_phone' => $privacyPhone,
'real_phone' => $realPhone,
'error' => $e->getMessage()
]);
}
return false;
}
}