feat: 集成隐私小号功能保护用户手机号
新增隐私小号服务类,在订单创建时自动绑定隐私手机号,收货时自动解绑 在快递发货和收货流程中使用隐私小号替代真实手机号,保护用户隐私 添加数据库字段存储绑定关系,提供系统配置开关和测试脚本
This commit is contained in:
384
AGENTS.md
Normal file
384
AGENTS.md
Normal file
@@ -0,0 +1,384 @@
|
||||
# AGENTS.md - CRMEB PRO Development Guide
|
||||
|
||||
## Project Overview
|
||||
CRMEB PRO is an e-commerce system based on ThinkPHP 8.0 + Swoole 4. PHP 8.0+ required. This document provides guidelines for agentic coding assistants working in this codebase.
|
||||
|
||||
---
|
||||
|
||||
## 1. Build/Lint/Test Commands
|
||||
|
||||
### Application Commands
|
||||
```bash
|
||||
# Start Swoole server (main application command)
|
||||
php think swoole
|
||||
|
||||
# Clear runtime files
|
||||
php think clear
|
||||
|
||||
# Build route cache
|
||||
php think optimize:route
|
||||
|
||||
# Build database schema cache
|
||||
php think optimize:schema
|
||||
```
|
||||
|
||||
### Code Generation
|
||||
```bash
|
||||
# Generate controller
|
||||
php think make:controller api@user/User
|
||||
|
||||
# Generate model
|
||||
php think make:model api@user/User
|
||||
|
||||
# Generate service (app/services/)
|
||||
php think make:service api@user/User
|
||||
|
||||
# Generate dao (app/dao/)
|
||||
php think make:dao api@user/User
|
||||
|
||||
# Generate validate class
|
||||
php think make:validate api@user/User
|
||||
|
||||
# Generate event
|
||||
php think make:event UserLogin
|
||||
|
||||
# Generate listener
|
||||
php think make:listener UserLoginListener
|
||||
|
||||
# Generate middleware
|
||||
php think make:middleware Auth
|
||||
```
|
||||
|
||||
### Queue Commands
|
||||
```bash
|
||||
# Process queue jobs
|
||||
php think queue:work
|
||||
|
||||
# Listen to queue
|
||||
php think queue:listen
|
||||
|
||||
# Retry failed jobs
|
||||
php think queue:retry
|
||||
|
||||
# List failed jobs
|
||||
php think queue:failed
|
||||
```
|
||||
|
||||
### Dependency Management
|
||||
```bash
|
||||
# Install dependencies
|
||||
composer install
|
||||
|
||||
# Update dependencies
|
||||
composer update
|
||||
|
||||
# Dump autoload
|
||||
composer dump-autoload
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Code Style Guidelines
|
||||
|
||||
### PSR Standards
|
||||
- Follows **PSR-2** (Coding Style) and **PSR-4** (Autoloading)
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
| Element | Convention | Example |
|
||||
|---------|------------|---------|
|
||||
| Directory | lowercase + underscore | `app/controller/admin` |
|
||||
| File (.php) | CamelCase or lowercase_underscore | `UserController.php`, `common.php` |
|
||||
| Class/Interface/Trait | PascalCase | `BaseServices`, `UserDao` |
|
||||
| Method | camelCase | `getUserInfo()`, `createToken()` |
|
||||
| Property | camelCase | `$adminInfo`, `$page` |
|
||||
| Variable | camelCase | `$userId`, `$orderData` |
|
||||
| Constant | UPPER_SNAKE_CASE | `PASSWORD_BCRYPT`, `MAX_RETRY` |
|
||||
| Config param | lowercase + underscore | `url_route_on`, `cache.is_gzde` |
|
||||
| Table/Column | lowercase + underscore | `eb_user`, `user_name` |
|
||||
|
||||
### File Header Template
|
||||
```php
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: CRMEB Team <admin@crmeb.com>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
namespace app\controller\admin;
|
||||
```
|
||||
|
||||
### PHP Syntax Rules
|
||||
1. **Namespace/use blocks** - Insert blank line after use statements
|
||||
2. **Braces** - Class/method braces on separate lines
|
||||
3. **Access modifiers** - Required for all methods and properties (`private`, `protected`, `public`)
|
||||
4. **Parameters** - Comma after each param, not before
|
||||
5. **Control structures** - Space after keyword (`if (` not `if(`)
|
||||
6. **Assignment** - Spaces around `=` (`$arr = []`)
|
||||
7. **Closing tag** - Omit `?>` for pure PHP files
|
||||
8. **PHP 8.0+ features** - Use nullsafe operator (`$obj?->method()`), named arguments, union types
|
||||
|
||||
---
|
||||
|
||||
## 3. Architecture Patterns
|
||||
|
||||
### Directory Structure
|
||||
```
|
||||
app/
|
||||
├── controller/ # Controllers (extend BaseController)
|
||||
│ ├── admin/ # Admin panel controllers
|
||||
│ ├── api/ # API controllers
|
||||
│ └── supplier/ # Supplier controllers
|
||||
├── services/ # Business logic layer
|
||||
├── dao/ # Data access layer
|
||||
├── model/ # Database models
|
||||
├── validate/ # Form validation classes
|
||||
├── http/
|
||||
│ └── middleware/ # Middleware classes
|
||||
└── jobs/ # Queue jobs
|
||||
|
||||
crmeb/
|
||||
├── basic/ # Base classes (BaseController, BaseModel, BaseServices)
|
||||
├── traits/ # Reusable traits
|
||||
├── services/ # Core services
|
||||
├── utils/ # Utility classes
|
||||
└── exceptions/ # Custom exception classes
|
||||
```
|
||||
|
||||
### Layer Pattern
|
||||
```
|
||||
Controller -> Service -> Dao -> Model
|
||||
```
|
||||
- **Controller**: Request handling, response formatting, auth checks
|
||||
- **Service**: Business logic, transaction management
|
||||
- **Dao**: Data query operations, search conditions
|
||||
- **Model**: Database operations via ORM
|
||||
|
||||
### Class Naming
|
||||
- Controllers: `{Name}Controller` (e.g., `StoreProductController`)
|
||||
- Services: `{Name}Services` (e.g., `StoreProductServices`)
|
||||
- DAOs: `{Name}Dao` (e.g., `StoreProductDao`)
|
||||
- Models: `{Name}` (e.g., `StoreProduct`)
|
||||
- Validates: `{Name}Validate` (e.g., `StoreProductValidate`)
|
||||
|
||||
---
|
||||
|
||||
## 4. Controller Guidelines
|
||||
|
||||
### Base Controller
|
||||
```php
|
||||
use app\controller\admin\AuthController; // extends BaseController
|
||||
|
||||
class ProductController extends AuthController
|
||||
{
|
||||
// Access $this->request via dependency injection
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
// Use $this->success() / $this->fail() for JSON responses
|
||||
return $this->success(['list' => []]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Request Data Retrieval
|
||||
```php
|
||||
// Single parameter
|
||||
$id = $this->request->param('id');
|
||||
|
||||
// Multiple parameters with defaults
|
||||
[$page, $limit] = $this->request->more(['page/d', 'limit/d']);
|
||||
|
||||
// With default values
|
||||
[$name, $status] = $this->request->more([
|
||||
['name', ''],
|
||||
['status', 0]
|
||||
], true);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Service Guidelines
|
||||
|
||||
### BaseServices Pattern
|
||||
```php
|
||||
namespace app\services\product;
|
||||
|
||||
use app\services\BaseServices;
|
||||
|
||||
class StoreProductServices extends BaseServices
|
||||
{
|
||||
// Inject dao via constructor or magic method __call
|
||||
public function getInfo(int $id)
|
||||
{
|
||||
return $this->dao->get($id);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Dao Injection
|
||||
Services access Dao via `$this->dao` property (injected via `ServicesTrait`).
|
||||
|
||||
---
|
||||
|
||||
## 6. Dao Guidelines
|
||||
|
||||
### BaseDao Pattern
|
||||
```php
|
||||
namespace app\dao\product;
|
||||
|
||||
use app\dao\BaseDao;
|
||||
|
||||
class StoreProductDao extends BaseDao
|
||||
{
|
||||
protected string $alias = 'product'; // Table alias
|
||||
protected string $filterateField = []; // Fields to filter from search
|
||||
|
||||
protected function setModel(): string
|
||||
{
|
||||
return StoreProduct::class;
|
||||
}
|
||||
|
||||
// Custom search conditions
|
||||
public function search(array $where = [])
|
||||
{
|
||||
return parent::search($where);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Model Guidelines
|
||||
|
||||
### BaseModel Pattern
|
||||
```php
|
||||
namespace app\model\product;
|
||||
|
||||
use crmeb\basic\BaseModel;
|
||||
use crmeb\traits\ModelTrait;
|
||||
|
||||
class StoreProduct extends BaseModel
|
||||
{
|
||||
use ModelTrait;
|
||||
|
||||
// Define searchers for field-based queries
|
||||
protected string $name = 'store_product';
|
||||
protected string $pk = 'id';
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Validation Guidelines
|
||||
|
||||
### Validate Class Pattern
|
||||
```php
|
||||
namespace app\validate\admin\product;
|
||||
|
||||
use think\Validate;
|
||||
|
||||
class StoreProductValidate extends Validate
|
||||
{
|
||||
protected $rule = [
|
||||
'name' => 'require|max:200',
|
||||
'price' => 'require|float|egt:0',
|
||||
];
|
||||
|
||||
protected $message = [
|
||||
'name.require' => '请填写商品名称',
|
||||
'price.require' => '请填写价格',
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
### Validation in Controller
|
||||
```php
|
||||
use app\validate\admin\product\StoreProductValidate;
|
||||
|
||||
public function save(StoreProductValidate $validate)
|
||||
{
|
||||
// Validates automatically, throws ValidateException on failure
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Error Handling
|
||||
|
||||
### Custom Exceptions
|
||||
```php
|
||||
// In crmeb/exceptions/
|
||||
throw new AuthException('无权限访问', 400);
|
||||
throw new ApiException('参数错误', 400);
|
||||
throw new AdminException('操作失败', 400);
|
||||
```
|
||||
|
||||
### ExceptionHandle
|
||||
The app extends ThinkPHP's exception handling to return JSON responses:
|
||||
```php
|
||||
// Returns: {"status": 400, "msg": "错误信息", "data": {...}}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Middleware Guidelines
|
||||
|
||||
### Middleware Interface Pattern
|
||||
```php
|
||||
namespace app\http\middleware;
|
||||
|
||||
use app\http\middleware\BaseMiddleware;
|
||||
use app\interfaces\MiddlewareInterface;
|
||||
|
||||
class AuthMiddleware implements MiddlewareInterface
|
||||
{
|
||||
public function handle(Request $request, \Closure $next)
|
||||
{
|
||||
// Before logic
|
||||
$response = $next($request);
|
||||
// After logic
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. Important Patterns
|
||||
|
||||
### Transactions
|
||||
```php
|
||||
// In services
|
||||
$this->transaction(function () use ($data) {
|
||||
$this->save($data);
|
||||
$this->dao->incUpdate($id, 'views', 1);
|
||||
});
|
||||
```
|
||||
|
||||
### JSON Response Helper
|
||||
```php
|
||||
// Available in controllers via BaseController
|
||||
return $this->success($data);
|
||||
return $this->fail('Error message');
|
||||
return app('json')->code(500)->make(400, 'msg', $data);
|
||||
```
|
||||
|
||||
### Magic Methods in Request
|
||||
```php
|
||||
// These are available via __call in Request class
|
||||
$uid = $this->request->uid();
|
||||
$adminInfo = $this->request->adminInfo();
|
||||
$tokenData = $this->request->tokenData();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. Type Hints
|
||||
- Use strict type hints: `public function get(int $id): ?array`
|
||||
- Return types required for public methods
|
||||
- Nullable types using `?` prefix
|
||||
@@ -211,6 +211,7 @@ class Login
|
||||
*/
|
||||
public function verify(Request $request)
|
||||
{
|
||||
// 1. 获取请求参数:手机号、验证码类型、防刷校验key、图形验证码类型及校验值
|
||||
[$phone, $type, $key, $captchaType, $captchaVerification] = $request->postMore([
|
||||
['phone', 0],
|
||||
['type', ''],
|
||||
@@ -219,13 +220,16 @@ class Login
|
||||
['captchaVerification', ''],
|
||||
], true);
|
||||
|
||||
// 2. 构造缓存Key:防刷校验Key与当前分钟级别的频率限制Key
|
||||
$keyName = 'sms.key.' . $key;
|
||||
$nowKey = 'sms.' . date('YmdHi');
|
||||
|
||||
// 3. 校验请求合法性:检查前端传递的防刷校验Key是否存在于缓存中,防止非法/机器请求
|
||||
if (!CacheService::has($keyName))
|
||||
return app('json')->make(401, '发送验证码失败,请刷新页面重新获取');
|
||||
|
||||
|
||||
// 4. 发送频率限制(分钟级防刷):检查当前一分钟内该接口的调用总次数是否超过配置上限(默认20次)
|
||||
$total = 1;
|
||||
if (CacheService::has($nowKey)) {
|
||||
$total = CacheService::get($nowKey);
|
||||
@@ -233,23 +237,33 @@ class Login
|
||||
return app('json')->success('已发送');
|
||||
}
|
||||
|
||||
//二次验证
|
||||
// 二次验证(图形验证码校验,当前代码中被注释)
|
||||
// try {
|
||||
// aj_captcha_check_two($captchaType, $captchaVerification);
|
||||
// } catch (\Throwable $e) {
|
||||
// return app('json')->fail($e->getError());
|
||||
// }
|
||||
|
||||
// 5. 数据验证:使用 RegisterValidates 验证器专门校验手机号格式是否合法
|
||||
try {
|
||||
validate(RegisterValidates::class)->scene('code')->check(['phone' => $phone]);
|
||||
} catch (ValidateException $e) {
|
||||
return app('json')->fail($e->getError());
|
||||
}
|
||||
|
||||
// 6. 获取配置:从系统配置中获取验证码的有效时长(单位:分钟),默认1分钟
|
||||
$time = sys_config('verify_expire_time', 1);
|
||||
|
||||
// 7. 核心业务处理:调用 LoginServices 的 verify 方法,执行如检查是否注册、生成验证码、异步派发短信发送任务等逻辑
|
||||
$smsCode = $this->services->verify($phone, $type, $time, app()->request->ip());
|
||||
|
||||
// 8. 结果处理:如果短信验证码生成及派发成功
|
||||
if ($smsCode) {
|
||||
// 将验证码写入缓存,设置过期时间(转换为秒)
|
||||
CacheService::set('code_' . $phone, $smsCode, $time * 60);
|
||||
// 记录/更新当前分钟的接口请求频次(有效期61秒)
|
||||
CacheService::set($nowKey, $total, 61);
|
||||
// 触发短信发送后的事件(可用于后续扩展业务逻辑)
|
||||
event('sms.sendAfter', [$smsCode, $phone]);
|
||||
return app('json')->success('发送成功');
|
||||
} else {
|
||||
|
||||
@@ -21,6 +21,7 @@ use app\services\agent\AgentLevelServices;
|
||||
use app\services\activity\coupon\StoreCouponUserServices;
|
||||
use app\services\agent\DivisionServices;
|
||||
use app\services\other\CityAreaServices;
|
||||
use app\services\other\PrivacyPhoneServices;
|
||||
use app\services\pay\PayServices;
|
||||
use app\services\product\brand\StoreBrandServices;
|
||||
use app\services\product\category\StoreProductCategoryServices;
|
||||
@@ -196,12 +197,33 @@ class StoreOrderCreateServices extends BaseServices
|
||||
$userAddress = $addressInfo['province'] . ' ' . $addressInfo['city'] . ' ' . $addressInfo['district'] . ' ' . $addressInfo['street'] . ' ' . $addressInfo['detail'];
|
||||
$userLocation = $addressInfo['longitude'] . ' ' . $addressInfo['latitude'];
|
||||
$type = $is_channel == 1 ? 20 : $type;
|
||||
|
||||
$realPhone = $addressInfo['phone'];
|
||||
$orderUserPhone = $realPhone;
|
||||
$bindId = '';
|
||||
$privatePhoneStatus = 0;
|
||||
|
||||
/** @var PrivacyPhoneServices $privacyPhoneServices */
|
||||
$privacyPhoneServices = app()->make(PrivacyPhoneServices::class);
|
||||
if ($privacyPhoneServices->isEnabled() && $realPhone) {
|
||||
try {
|
||||
$privateData = $privacyPhoneServices->bindForOrder($realPhone);
|
||||
if ($privateData['user_phone']) {
|
||||
$orderUserPhone = $privateData['user_phone'];
|
||||
$bindId = $privateData['bind_id'];
|
||||
$privatePhoneStatus = $privateData['status'];
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error('隐私小号绑定失败', ['error' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
$orderInfo = [
|
||||
'uid' => $uid,
|
||||
'type' => $type,
|
||||
'order_id' => $this->getNewOrderId(),
|
||||
'real_name' => $addressInfo['real_name'],
|
||||
'user_phone' => $addressInfo['phone'],
|
||||
'user_phone' => $orderUserPhone,
|
||||
'user_address' => $userAddress,
|
||||
'user_location' => $userLocation,
|
||||
'cart_id' => $cartIds,
|
||||
@@ -247,6 +269,8 @@ class StoreOrderCreateServices extends BaseServices
|
||||
'send_gift_price' => $priceData['sendGiftPrice'],
|
||||
'send_gift_mark' => $sendGiftMark,
|
||||
'channel' => $is_channel,
|
||||
'private_bind_id' => $bindId,
|
||||
'private_phone_status' => $privatePhoneStatus,
|
||||
];
|
||||
if ($userInfo['user_type'] == 'wechat' || $userInfo['user_type'] == 'routine') {
|
||||
/** @var WechatUserServices $wechatServices */
|
||||
@@ -264,7 +288,7 @@ class StoreOrderCreateServices extends BaseServices
|
||||
}
|
||||
|
||||
$priceData['coupon_id'] = $couponId;
|
||||
$order = $this->transaction(function () use ($cartIds, $couponId, $orderInfo, $cartInfo, $key, $userInfo, $useIntegral, $priceData, $type, $activity_id, $uid, $addressId, $promotions_give) {
|
||||
$order = $this->transaction(function () use ($cartIds, $couponId, $orderInfo, $cartInfo, $key, $userInfo, $useIntegral, $priceData, $type, $activity_id, $uid, $addressId, $promotions_give, $realPhone) {
|
||||
//创建订单
|
||||
$order = $this->dao->save($orderInfo);
|
||||
if ($couponId) {
|
||||
|
||||
@@ -294,7 +294,7 @@ class StoreOrderDeliveryServices extends BaseServices
|
||||
}
|
||||
$expData['com'] = $data['delivery_code'];
|
||||
$expData['to_name'] = $orderInfo['real_name'];
|
||||
$expData['to_tel'] = $orderInfo['user_phone'];
|
||||
$expData['to_tel'] = $orderInfo['private_phone'] ?: $orderInfo['user_phone'];
|
||||
$expData['to_addr'] = $orderInfo['user_address'];
|
||||
$expData['from_name'] = $data['to_name'];
|
||||
$expData['from_tel'] = $data['to_tel'];
|
||||
@@ -346,7 +346,7 @@ class StoreOrderDeliveryServices extends BaseServices
|
||||
$expressService = app()->make(ServeServices::class);
|
||||
$expData['kuaidicom'] = $data['delivery_code'];
|
||||
$expData['man_name'] = $orderInfo->real_name;
|
||||
$expData['phone'] = $orderInfo->user_phone;
|
||||
$expData['phone'] = $orderInfo->private_phone ?: $orderInfo->user_phone;
|
||||
$expData['address'] = $orderInfo->user_address;
|
||||
$expData['send_real_name'] = $data['to_name'];
|
||||
$expData['send_phone'] = $data['to_tel'];
|
||||
@@ -626,7 +626,7 @@ class StoreOrderDeliveryServices extends BaseServices
|
||||
$expressService = app()->make(ServeServices::class);
|
||||
$expData['com'] = $dumpInfo['com'];
|
||||
$expData['to_name'] = $orderInfo->real_name;
|
||||
$expData['to_tel'] = $orderInfo->user_phone;
|
||||
$expData['to_tel'] = $orderInfo->private_phone ?: $orderInfo->user_phone;
|
||||
$expData['to_addr'] = $orderInfo->user_address;
|
||||
$expData['from_name'] = $dumpInfo['from_name'];
|
||||
$expData['from_tel'] = $dumpInfo['from_tel'];
|
||||
|
||||
@@ -25,6 +25,7 @@ use app\services\user\member\MemberCardServices;
|
||||
use app\services\user\UserBillServices;
|
||||
use app\services\user\UserBrokerageServices;
|
||||
use app\services\user\UserServices;
|
||||
use app\services\other\PrivacyPhoneServices;
|
||||
use think\annotation\Inject;
|
||||
use think\exception\ValidateException;
|
||||
use think\facade\Log;
|
||||
@@ -148,6 +149,17 @@ class StoreOrderTakeServices extends BaseServices
|
||||
if (!($res1 && $res2 && $res3)) {
|
||||
throw new ValidateException('收货失败!');
|
||||
}
|
||||
// 解绑隐私手机号 (AX模式)
|
||||
// user_phone存储隐私号, private_bind_id存储真实手机号
|
||||
if ($order['private_bind_id'] && $order['private_phone_status'] == 1) {
|
||||
/** @var PrivacyPhoneServices $privacyPhoneServices */
|
||||
$privacyPhoneServices = app()->make(PrivacyPhoneServices::class);
|
||||
$unbindResult = $privacyPhoneServices->unbindForOrder($order['user_phone'], $order['private_bind_id']);
|
||||
if ($unbindResult) {
|
||||
$order->private_phone_status = 0;
|
||||
$order->save();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}, $isTran);
|
||||
}
|
||||
|
||||
329
app/services/other/PrivacyPhoneServices.php
Normal file
329
app/services/other/PrivacyPhoneServices.php
Normal file
@@ -0,0 +1,329 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
14
docs/mixcom_php_sdk_v2.0/index.php
Normal file
14
docs/mixcom_php_sdk_v2.0/index.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
date_default_timezone_set('Asia/Shanghai');
|
||||
|
||||
define('ROOT_PATH', dirname(__FILE__) . DIRECTORY_SEPARATOR);
|
||||
|
||||
require './lib/PrivateNumber.php';
|
||||
|
||||
$pn = new PrivateNumber();
|
||||
// 这里设置appkey
|
||||
$pn->setAppkey('');
|
||||
// 这里设置秘钥
|
||||
$pn->setSecret('');
|
||||
$result = $pn->countNumber();
|
||||
print_r($result);
|
||||
457
docs/mixcom_php_sdk_v2.0/lib/PrivateNumber.php
Normal file
457
docs/mixcom_php_sdk_v2.0/lib/PrivateNumber.php
Normal file
@@ -0,0 +1,457 @@
|
||||
<?php
|
||||
// 号码隐私保护类
|
||||
class PrivateNumber
|
||||
{
|
||||
// 网站的appkey
|
||||
private $appkey = "";
|
||||
// appkey 对应的秘钥
|
||||
private $secret = "";
|
||||
public $apiurl = "https://api.mixcom.cn";
|
||||
|
||||
/**
|
||||
* 设置appkey
|
||||
* @param appkey
|
||||
*/
|
||||
public function setAppkey($appkey)
|
||||
{
|
||||
$this->appkey = $appkey;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置秘钥
|
||||
* @param secret
|
||||
*/
|
||||
public function setSecret($secret)
|
||||
{
|
||||
$this->secret = $secret;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取服务器的时间
|
||||
* @return
|
||||
*/
|
||||
public function getServerTime()
|
||||
{
|
||||
$url = $this->apiurl . "/virtual/getServerTime";
|
||||
$json = $this->get($url);
|
||||
return $json;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可用的axb号码
|
||||
* region_code 区号,如深圳区号为 0755, 通过这个参数查询指定地区的axb号码, 可选
|
||||
* 获取号码的优先级: 绑定数量少优先 > 绑定总数少优先
|
||||
* @return
|
||||
*/
|
||||
public function getAxbNumber($arr = array())
|
||||
{
|
||||
$url = $this->apiurl . "/virtual/getaxbnumber";
|
||||
$params = array(
|
||||
'region_code' => '',
|
||||
);
|
||||
$params = array_merge($params, $arr);
|
||||
$json = $this->post($url, $params);
|
||||
return $json;
|
||||
}
|
||||
|
||||
/**
|
||||
* axb号码绑定
|
||||
* number 虚拟小号,必选
|
||||
* aparty 呼叫方的号码,必选
|
||||
* bparty 被叫方的号码,必选
|
||||
* recording 是否开启录音,1开启, 0 不开启。使用前请先确定使用了通话录音功能
|
||||
* recordhinttone 录音的提示音,可选
|
||||
* maxduration 最大通话时长,单位为分钟,到了时间会自动挂断,可选
|
||||
* lastminvoice 通话最后一分钟的提示音, 当最大通话时长不为0时有效,可选
|
||||
* callerhinttone A拨打X通话前等待音,可选
|
||||
* calleeHintTone B拨打X通话前等待音,可选
|
||||
* direction 呼叫方向, 可选值0, 1, 2. 0代表可双向呼叫,可选
|
||||
* enddate 解绑时间,时间戳格式,可选,0代表不设置解绑时间
|
||||
* @return
|
||||
*/
|
||||
public function axbBind($arr = array())
|
||||
{
|
||||
$url = $this->apiurl . "/virtual/axbBind";
|
||||
$params = array(
|
||||
'number' => '',
|
||||
'aparty' => '',
|
||||
'bparty' => '',
|
||||
'recording' => 0,
|
||||
'recordhinttone' => '',
|
||||
'maxduration' => 0,
|
||||
'lastminvoice' => '',
|
||||
'callerhinttone' => '',
|
||||
'calleeHintTone' => '',
|
||||
'direction' => 0,
|
||||
'enddate' => 0,
|
||||
);
|
||||
$params = array_merge($params, $arr);
|
||||
$json = $this->post($url, $params);
|
||||
return $json;
|
||||
}
|
||||
|
||||
/**
|
||||
* axb号码解绑
|
||||
* number 虚拟小号,必选
|
||||
* aparty 呼叫方的号码,必选
|
||||
* bparty 被叫方的号码,必选
|
||||
* @return
|
||||
*/
|
||||
public function unAxbBind($arr = array())
|
||||
{
|
||||
$url = $this->apiurl . "/virtual/unAxbBind";
|
||||
$params = array(
|
||||
'number' => '',
|
||||
'aparty' => '',
|
||||
'bparty' => '',
|
||||
);
|
||||
$params = array_merge($params, $arr);
|
||||
$json = $this->post($url, $params);
|
||||
return $json;
|
||||
}
|
||||
|
||||
/**
|
||||
* axb号码自动绑定
|
||||
* aparty 呼叫方的号码,必选
|
||||
* bparty 被叫方的号码,必选
|
||||
* recording 是否开启录音,1开启, 0 不开启。使用前请先确定使用了通话录音功能
|
||||
* recordhinttone 录音的提示音,可选
|
||||
* maxduration 最大通话时长,单位为分钟,到了时间会自动挂断,可选
|
||||
* lastminvoice 通话最后一分钟的提示音, 当最大通话时长不为0时有效,可选
|
||||
* callerhinttone A拨打X通话前等待音,可选
|
||||
* calleeHintTone B拨打X通话前等待音,可选
|
||||
* direction 呼叫方向, 可选值0, 1, 2. 0代表可双向呼叫,可选
|
||||
* enddate 解绑时间,时间戳格式,可选,0代表不设置解绑时间
|
||||
* @return
|
||||
*/
|
||||
public function axbAutoBind($arr = array())
|
||||
{
|
||||
$url = $this->apiurl . "/virtual/axbAutoBind";
|
||||
$params = array(
|
||||
'aparty' => '',
|
||||
'bparty' => '',
|
||||
'region_code' => '',
|
||||
'defregioncode' => '',
|
||||
'recording' => 0,
|
||||
'recordhinttone' => '',
|
||||
'maxduration' => 0,
|
||||
'lastminvoice' => '',
|
||||
'callerhinttone' => '',
|
||||
'calleeHintTone' => '',
|
||||
'direction' => 0,
|
||||
'enddate' => 0,
|
||||
);
|
||||
$params = array_merge($params, $arr);
|
||||
$json = $this->post($url, $params);
|
||||
return $json;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可用的ax号码
|
||||
* region_code 区号,如深圳区号为 0755, 通过这个参数查询指定地区的ax号码, 可选
|
||||
* @return
|
||||
*/
|
||||
public function getAxNumber($arr = array())
|
||||
{
|
||||
$url = $this->apiurl . "/virtual/getAxNumber";
|
||||
$params = array(
|
||||
'region_code' => '',
|
||||
);
|
||||
$params = array_merge($params, $arr);
|
||||
$json = $this->post($url, $params);
|
||||
return $json;
|
||||
}
|
||||
|
||||
/**
|
||||
* ax号码绑定
|
||||
* number 虚拟小号,必选
|
||||
* aparty 呼叫方的号码,必选
|
||||
* recording 是否开启录音,1开启, 0 不开启。使用前请先确定使用了通话录音功能,可选
|
||||
* recordhinttone 录音前播放的提示音,可选
|
||||
* display_call 是否开启号码透传。默认为0,不开启。1为开启,开启后可能会导致部分通话无法接通,可选
|
||||
* callerhinttone A拨打X通话前等待音,可选
|
||||
* calleeHintTone B拨打X通话前等待音,可选
|
||||
* enddate 解绑时间,时间戳格式,可选,0代表不设置解绑时间
|
||||
* @return
|
||||
*/
|
||||
public function axBind($arr = array())
|
||||
{
|
||||
$url = $this->apiurl . "/virtual/axBind";
|
||||
$params = array(
|
||||
'number' => '',
|
||||
'aparty' => '',
|
||||
'recording' => 0,
|
||||
'recordhinttone' => '',
|
||||
'display_call' => 0,
|
||||
'callerhinttone' => '',
|
||||
'calleehinttone' => '',
|
||||
'enddate' => 0,
|
||||
);
|
||||
$params = array_merge($params, $arr);
|
||||
$json = $this->post($url, $params);
|
||||
return $json;
|
||||
}
|
||||
|
||||
/**
|
||||
* ax号码解绑
|
||||
* number 虚拟小号,必选
|
||||
* aparty 呼叫方的号码,必选
|
||||
* @return
|
||||
*/
|
||||
public function unAxBind($arr = array())
|
||||
{
|
||||
$url = $this->apiurl . "/virtual/unAxBind";
|
||||
$params = array(
|
||||
'number' => '',
|
||||
'aparty' => '',
|
||||
);
|
||||
$params = array_merge($params, $arr);
|
||||
$json = $this->post($url, $params);
|
||||
return $json;
|
||||
}
|
||||
|
||||
/**
|
||||
* AX绑定呼出
|
||||
* number 虚拟小号,必选
|
||||
* aparty 呼叫方的号码,必选
|
||||
* bparty 被叫方的号码,必选
|
||||
* @return
|
||||
*/
|
||||
public function axDialNumber($arr = array())
|
||||
{
|
||||
$url = $this->apiurl . "/virtual/axdialnumber";
|
||||
$params = array(
|
||||
'number' => '',
|
||||
'aparty' => '',
|
||||
'bparty' => '',
|
||||
);
|
||||
$params = array_merge($params, $arr);
|
||||
$json = $this->post($url, $params);
|
||||
return $json;
|
||||
}
|
||||
|
||||
/**
|
||||
* ax号码自动绑定
|
||||
* aparty 呼叫方的号码,必选
|
||||
* region_code 需要的虚拟小号的区号, 可选
|
||||
* display_call 是否开启号码透传。默认为0,不开启。1为开启,开启后可能会导致部分通话无法接通,可选
|
||||
* recording 是否开启录音,1开启, 0 不开启。使用前请先确定使用了通话录音功能,可选
|
||||
* recordhinttone 录音前播放的提示音,可选
|
||||
* callerhinttone A拨打X通话前等待音,可选
|
||||
* calleeHintTone B拨打X通话前等待音,可选
|
||||
* enddate 解绑时间,时间戳格式,可选,0代表不设置解绑时间
|
||||
* @return
|
||||
*/
|
||||
public function axAutoBind($arr = array())
|
||||
{
|
||||
$url = $this->apiurl . "/virtual/axAutoBind";
|
||||
$params = array(
|
||||
'aparty' => '',
|
||||
'region_code' => '',
|
||||
'defregioncode' => '',
|
||||
'display_call' => 0,
|
||||
'recording' => 0,
|
||||
'recordhinttone' => '',
|
||||
'callerhinttone' => '',
|
||||
'calleehinttone' => '',
|
||||
'enddate' => 0,
|
||||
);
|
||||
$params = array_merge($params, $arr);
|
||||
$json = $this->post($url, $params);
|
||||
return $json;
|
||||
}
|
||||
|
||||
/**
|
||||
* 挂断指定通话
|
||||
* subscriptionid 该通话的绑定关系id
|
||||
* @return
|
||||
*/
|
||||
public function callStop($arr)
|
||||
{
|
||||
$url = $this->apiurl . "/virtual/callStop";
|
||||
$params = [
|
||||
'subscriptionid' => '',
|
||||
];
|
||||
$params = array_merge($params, $arr);
|
||||
$json = $this->post($url, $params);
|
||||
return $json;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取小号的个数
|
||||
* @return
|
||||
*/
|
||||
public function countNumber()
|
||||
{
|
||||
$url = $this->apiurl . "/virtual/countNumber";
|
||||
$params = array();
|
||||
$json = $this->post($url, $params);
|
||||
return $json;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询所有的号码
|
||||
* page 需要查询的页数,每页100条数据
|
||||
* @return
|
||||
*/
|
||||
public function queryAllNumber($arr = array())
|
||||
{
|
||||
$url = $this->apiurl . "/virtual/queryAllNumber";
|
||||
$params = array(
|
||||
'page' => 1,
|
||||
);
|
||||
$params = array_merge($params, $arr);
|
||||
$json = $this->post($url, $params);
|
||||
return $json;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询号码的绑定关系
|
||||
* number 虚拟小号,可选
|
||||
* aparty 呼叫方的号码,可选
|
||||
* bparty 被叫方的号码,可选
|
||||
* @return
|
||||
*/
|
||||
public function queryBindRelation($arr = array())
|
||||
{
|
||||
$url = $this->apiurl . "/virtual/querybindrelation";
|
||||
$params = array(
|
||||
'number' => '',
|
||||
'aparty' => '',
|
||||
'bparty' => '',
|
||||
);
|
||||
$params = array_merge($params, $arr);
|
||||
$json = $this->post($url, $params);
|
||||
return $json;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询号码的绑定关系日志
|
||||
* number 虚拟小号,可选
|
||||
* aparty 呼叫方的号码,可选
|
||||
* bparty 被叫方的号码,可选
|
||||
* timestart 开始时间,可选
|
||||
* timeend 结束时间,可选
|
||||
* page 页数,可选
|
||||
* @return
|
||||
*/
|
||||
public function queryRelationLog($arr = array())
|
||||
{
|
||||
$url = $this->apiurl . "/virtual/queryrelationlog";
|
||||
$params = array(
|
||||
'number' => '',
|
||||
'aparty' => '',
|
||||
'bparty' => '',
|
||||
'timestart' => 0,
|
||||
'timeend' => 0,
|
||||
'page' => 1,
|
||||
);
|
||||
$params = array_merge($params, $arr);
|
||||
$json = $this->post($url, $params);
|
||||
return $json;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询通话记录
|
||||
* number 虚拟小号,可选
|
||||
* aparty 呼叫方的号码,可选
|
||||
* bparty 被叫方的号码,可选
|
||||
* timestart 开始时间,可选
|
||||
* timeend 结束时间,可选
|
||||
* page 页数,可选
|
||||
* @return
|
||||
*/
|
||||
public function queryCallRecord($arr = array())
|
||||
{
|
||||
$url = $this->apiurl . "/virtual/querycallrecord";
|
||||
$params = array(
|
||||
'number' => '',
|
||||
'aparty' => '',
|
||||
'bparty' => '',
|
||||
'timestart' => 0,
|
||||
'timeend' => 0,
|
||||
'page' => 1,
|
||||
);
|
||||
$params = array_merge($params, $arr);
|
||||
$json = $this->post($url, $params);
|
||||
return $json;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回录音的下载链接(在录音未删除之前有效)
|
||||
* number 虚拟小号,必选
|
||||
* callIdentifier 通话录音id,必选
|
||||
*/
|
||||
public function getRecordUrl($arr = array())
|
||||
{
|
||||
$url = $this->apiurl . "/virtual/getrecordurl";
|
||||
$params = array(
|
||||
'number' => '',
|
||||
'callIdentifier' => '',
|
||||
);
|
||||
$params = array_merge($params, $arr);
|
||||
$json = $this->post($url, $params);
|
||||
return $json;
|
||||
}
|
||||
|
||||
private function get($url)
|
||||
{
|
||||
$ch = curl_init();
|
||||
//设置选项,包括URL
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($ch, CURLOPT_HEADER, 0);
|
||||
|
||||
//执行并获取HTML文档内容
|
||||
$data = curl_exec($ch);
|
||||
//释放curl句柄
|
||||
curl_close($ch);
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function post($url, $params = array())
|
||||
{
|
||||
$params['time'] = time();
|
||||
$params['appkey'] = $this->appkey;
|
||||
$params['sign'] = $this->getSign($params);
|
||||
|
||||
$ch = curl_init(); //初始化curl
|
||||
curl_setopt($ch, CURLOPT_URL, $url); //抓取指定网页
|
||||
curl_setopt($ch, CURLOPT_HEADER, 0); //设置header
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); //要求结果为字符串且输出到屏幕上
|
||||
curl_setopt($ch, CURLOPT_POST, 1); //post提交方式
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $params);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
||||
$data = curl_exec($ch); //运行curl
|
||||
curl_close($ch);
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function getSign($params)
|
||||
{
|
||||
// key转换为小写
|
||||
$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]);
|
||||
}
|
||||
}
|
||||
// 对key进行排序, 按名称升序
|
||||
ksort($params);
|
||||
// 将数据连接,进行加密
|
||||
$data = '';
|
||||
foreach ($params as $key => $val) {
|
||||
$data .= $val;
|
||||
}
|
||||
$data .= $this->secret;
|
||||
$sign = strtolower(sha1($data . sha1($this->secret)));
|
||||
return $sign;
|
||||
}
|
||||
}
|
||||
1
docs/mixcom_php_sdk_v2.0/readme.txt
Normal file
1
docs/mixcom_php_sdk_v2.0/readme.txt
Normal file
@@ -0,0 +1 @@
|
||||
隐私保护小号php sdk,不使用composer自动加载的方式执行
|
||||
20
docs/privacy_phone_update.sql
Normal file
20
docs/privacy_phone_update.sql
Normal file
@@ -0,0 +1,20 @@
|
||||
-- 隐私手机号功能 - 数据库改动 (AX模式)
|
||||
-- 执行时间: 2026-04-01
|
||||
-- 说明: AX模式下 user_phone 直接存储隐私手机号
|
||||
-- private_bind_id 存储真实手机号(用于解绑)
|
||||
-- private_phone_status 存储绑定状态
|
||||
|
||||
-- 1. 订单表添加字段
|
||||
ALTER TABLE `eb_store_order`
|
||||
ADD COLUMN `private_bind_id` VARCHAR(18) NOT NULL DEFAULT '' COMMENT '隐私绑定真实手机号(用于解绑)' AFTER `user_address`,
|
||||
ADD COLUMN `private_phone_status` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '隐私手机绑定状态:0未绑定,1已绑定' AFTER `private_bind_id`;
|
||||
|
||||
-- 2. 系统配置表添加隐私手机号配置
|
||||
INSERT INTO `eb_system_config` (`id`, `is_store`, `menu_name`, `type`, `input_type`, `config_tab_id`, `parameter`, `upload_type`, `required`, `width`, `high`, `value`, `info`, `desc`, `sort`, `status`) VALUES
|
||||
(NULL, 0, 'privacy_phone_status', 'radio', 'input', 1, '0=>关闭\n1=>开启', 0, '', 0, 0, '0', '隐私手机号功能', '开启后订单发货使用隐私手机号', 0, 1),
|
||||
(NULL, 0, 'privacy_phone_appkey', 'text', 'input', 1, '', 0, 'required:true', 100, 0, '', '米糠云AppKey', '米糠云小号平台AppKey', 0, 1),
|
||||
(NULL, 0, 'privacy_phone_appsecret', 'text', 'input', 1, '', 0, 'required:true', 100, 0, '', '米糠云AppSecret', '米糠云小号平台AppSecret', 0, 1);
|
||||
|
||||
-- 查询验证
|
||||
-- SELECT id, order_id, user_phone, private_bind_id, private_phone_status FROM eb_store_order LIMIT 10;
|
||||
-- SELECT * FROM eb_system_config WHERE menu_name LIKE 'privacy_phone%';
|
||||
BIN
docs/米糠云接口文档V2.0.docx
Normal file
BIN
docs/米糠云接口文档V2.0.docx
Normal file
Binary file not shown.
228
tests/Other/PrivacyPhone/MikangyunApiTest.php
Normal file
228
tests/Other/PrivacyPhone/MikangyunApiTest.php
Normal file
@@ -0,0 +1,228 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: CRMEB Team <admin@crmeb.com>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
namespace Tests\Other\PrivacyPhone;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* 米糠云隐私小号接口测试
|
||||
*/
|
||||
class MikangyunApiTest extends TestCase
|
||||
{
|
||||
private string $appkey = '0f2494a0e258c85575d1e2aa68e07687';
|
||||
private string $appsecret = ''; // TODO: 请填写完整的AppSecret
|
||||
private string $serverRoot = 'http://api.mixcom.cn';
|
||||
|
||||
/**
|
||||
* 生成签名
|
||||
* @param array $params
|
||||
* @return string
|
||||
*/
|
||||
private function generateSign(array $params): string
|
||||
{
|
||||
$params = array_filter($params, function ($v, $k) {
|
||||
return $v !== '' && $k !== 'sign';
|
||||
}, ARRAY_FILTER_USE_BOTH);
|
||||
|
||||
ksort($params);
|
||||
|
||||
$string = '';
|
||||
foreach ($params as $v) {
|
||||
$string .= $v;
|
||||
}
|
||||
|
||||
return strtolower(sha1($string . sha1($this->appsecret)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送POST请求
|
||||
* @param string $url
|
||||
* @param array $params
|
||||
* @return array
|
||||
*/
|
||||
private function post(string $url, array $params): array
|
||||
{
|
||||
$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, http_build_query($params));
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/x-www-form-urlencoded']);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
return [
|
||||
'http_code' => $httpCode,
|
||||
'response' => $response,
|
||||
'error' => $error,
|
||||
'decoded' => json_decode($response, true) ?? []
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试获取AXB可用小号
|
||||
*/
|
||||
public function testGetAxbNumber()
|
||||
{
|
||||
echo "\n========== 测试获取AXB可用小号 ==========\n";
|
||||
|
||||
if (empty($this->appsecret)) {
|
||||
$this->markTestSkipped('请先填写完整的AppSecret');
|
||||
}
|
||||
|
||||
$time = time();
|
||||
$params = [
|
||||
'appkey' => $this->appkey,
|
||||
'time' => $time,
|
||||
];
|
||||
$params['sign'] = $this->generateSign($params);
|
||||
|
||||
$url = $this->serverRoot . '/virtual/getAxbNumber';
|
||||
|
||||
echo "请求URL: $url\n";
|
||||
echo "请求参数: " . json_encode($params, JSON_UNESCAPED_UNICODE) . "\n";
|
||||
|
||||
$result = $this->post($url, $params);
|
||||
|
||||
echo "HTTP状态码: {$result['http_code']}\n";
|
||||
echo "响应内容: {$result['response']}\n";
|
||||
|
||||
if ($result['error']) {
|
||||
echo "curl错误: {$result['error']}\n";
|
||||
}
|
||||
|
||||
$this->assertEquals(200, $result['http_code'], 'HTTP状态码应为200');
|
||||
|
||||
$data = $result['decoded'];
|
||||
$this->assertArrayHasKey('success', $data, '响应应包含success字段');
|
||||
$this->assertArrayHasKey('code', $data, '响应应包含code字段');
|
||||
$this->assertArrayHasKey('data', $data, '响应应包含data字段');
|
||||
|
||||
if ($data['success'] && !empty($data['data'])) {
|
||||
echo "\n获取成功!可用小号列表:\n";
|
||||
foreach ($data['data'] as $item) {
|
||||
echo " - 号码: {$item['number']}, 区号: {$item['region_code']}, 绑定数: {$item['bindcount']}\n";
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试AXB绑定
|
||||
* @depends testGetAxbNumber
|
||||
*/
|
||||
public function testAxbBind($axbData)
|
||||
{
|
||||
echo "\n========== 测试AXB绑定 ==========\n";
|
||||
|
||||
if (empty($this->appsecret)) {
|
||||
$this->markTestSkipped('请先填写完整的AppSecret');
|
||||
}
|
||||
|
||||
if (empty($axbData['data']) || empty($axbData['data'][0]['number'])) {
|
||||
$this->markTestSkipped('无可用小号,无法测试绑定');
|
||||
}
|
||||
|
||||
// TODO: 请填写测试用的A号码(用户手机号)和B号码(商家/快递手机号)
|
||||
$aParty = ''; // TODO: 填写A号码
|
||||
$bParty = ''; // TODO: 填写B号码
|
||||
|
||||
if (empty($aParty) || empty($bParty)) {
|
||||
$this->markTestSkipped('请先填写A号码和B号码进行测试');
|
||||
}
|
||||
|
||||
$number = $axbData['data'][0]['number'];
|
||||
$time = time();
|
||||
|
||||
$params = [
|
||||
'appkey' => $this->appkey,
|
||||
'time' => $time,
|
||||
'number' => $number,
|
||||
'aparty' => $aParty,
|
||||
'bparty' => $bParty,
|
||||
];
|
||||
$params['sign'] = $this->generateSign($params);
|
||||
|
||||
$url = $this->serverRoot . '/virtual/axbBind';
|
||||
|
||||
echo "请求URL: $url\n";
|
||||
echo "请求参数: " . json_encode($params, JSON_UNESCAPED_UNICODE) . "\n";
|
||||
|
||||
$result = $this->post($url, $params);
|
||||
|
||||
echo "HTTP状态码: {$result['http_code']}\n";
|
||||
echo "响应内容: {$result['response']}\n";
|
||||
|
||||
$this->assertEquals(200, $result['http_code'], 'HTTP状态码应为200');
|
||||
|
||||
$data = $result['decoded'];
|
||||
$this->assertTrue($data['success'] ?? false, '绑定应成功');
|
||||
$this->assertEquals(200, $data['code'] ?? 0, '响应码应为200');
|
||||
|
||||
echo "\n绑定成功!\n";
|
||||
echo "隐私小号: $number\n";
|
||||
echo "A号码: $aParty\n";
|
||||
echo "B号码: $bParty\n";
|
||||
|
||||
return [
|
||||
'number' => $number,
|
||||
'aparty' => $aParty,
|
||||
'bparty' => $bParty,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试AXB解绑
|
||||
* @depends testAxbBind
|
||||
*/
|
||||
public function testUnAxbBind($bindData)
|
||||
{
|
||||
echo "\n========== 测试AXB解绑 ==========\n";
|
||||
|
||||
if (empty($this->appsecret)) {
|
||||
$this->markTestSkipped('请先填写完整的AppSecret');
|
||||
}
|
||||
|
||||
$time = time();
|
||||
|
||||
$params = [
|
||||
'appkey' => $this->appkey,
|
||||
'time' => $time,
|
||||
'number' => $bindData['number'],
|
||||
'aparty' => $bindData['aparty'],
|
||||
'bparty' => $bindData['bparty'],
|
||||
];
|
||||
$params['sign'] = $this->generateSign($params);
|
||||
|
||||
$url = $this->serverRoot . '/virtual/unAxbBind';
|
||||
|
||||
echo "请求URL: $url\n";
|
||||
echo "请求参数: " . json_encode($params, JSON_UNESCAPED_UNICODE) . "\n";
|
||||
|
||||
$result = $this->post($url, $params);
|
||||
|
||||
echo "HTTP状态码: {$result['http_code']}\n";
|
||||
echo "响应内容: {$result['response']}\n";
|
||||
|
||||
$this->assertEquals(200, $result['http_code'], 'HTTP状态码应为200');
|
||||
|
||||
$data = $result['decoded'];
|
||||
$this->assertTrue($data['success'] ?? false, '解绑应成功');
|
||||
|
||||
echo "\n解绑成功!\n";
|
||||
}
|
||||
}
|
||||
230
tests/Other/PrivacyPhone/test_api.php
Normal file
230
tests/Other/PrivacyPhone/test_api.php
Normal file
@@ -0,0 +1,230 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: CRMEB Team <admin@crmeb.com>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* 米糠云隐私小号接口测试脚本
|
||||
*
|
||||
* 使用方法:
|
||||
* 1. 填写下方配置信息
|
||||
* 2. 命令行执行: php tests/Other/PrivacyPhone/test_api.php
|
||||
*/
|
||||
|
||||
// 配置信息
|
||||
$appkey = '0f2494a0e258c85575d1e2aa68e07687';
|
||||
$appsecret = 'SZWgOa'; // 完整AppSecret
|
||||
$aParty = '13693687880'; // A号码 - 用户手机号
|
||||
|
||||
$serverRoot = 'https://api.mixcom.cn'; // 注意:SDK中使用的是https
|
||||
|
||||
/**
|
||||
* 生成签名 - 参照SDK实现
|
||||
*/
|
||||
function generateSign(array $params, string $secret): string
|
||||
{
|
||||
// key转换为小写
|
||||
$params = array_change_key_case($params, CASE_LOWER);
|
||||
|
||||
// 排除不需要加密的参数 (key为c、a或sign)
|
||||
foreach ($params as $key => $val) {
|
||||
if ($key == 'c' || $key == 'a' || $key == 'sign') {
|
||||
unset($params[$key]);
|
||||
}
|
||||
if ($val === '') {
|
||||
unset($params[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
// 对key进行排序, 按名称升序
|
||||
ksort($params);
|
||||
|
||||
// 将数据连接,进行加密
|
||||
$data = '';
|
||||
foreach ($params as $key => $val) {
|
||||
$data .= $val;
|
||||
}
|
||||
$data .= $secret;
|
||||
|
||||
$sign = strtolower(sha1($data . sha1($secret)));
|
||||
|
||||
echo "\n[签名调试]\n";
|
||||
echo "key小写转换后: " . json_encode(array_keys($params)) . "\n";
|
||||
echo "排除后参数: " . json_encode($params) . "\n";
|
||||
echo "参与签名数据拼接: $data\n";
|
||||
echo "sha1(secret): " . sha1($secret) . "\n";
|
||||
echo "最终签名: $sign\n";
|
||||
|
||||
return $sign;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送POST请求
|
||||
*/
|
||||
function post(string $url, array $params): array
|
||||
{
|
||||
$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);
|
||||
|
||||
return [
|
||||
'http_code' => $httpCode,
|
||||
'response' => $response,
|
||||
'error' => $error,
|
||||
'decoded' => json_decode($response, true) ?? []
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 打印结果
|
||||
*/
|
||||
function printResult(string $title, array $result): void
|
||||
{
|
||||
echo "\n========== $title ==========\n";
|
||||
echo "HTTP状态码: {$result['http_code']}\n";
|
||||
echo "响应内容: {$result['response']}\n";
|
||||
if ($result['error']) {
|
||||
echo "curl错误: {$result['error']}\n";
|
||||
}
|
||||
echo "\n";
|
||||
}
|
||||
|
||||
// ========== 开始测试 ==========
|
||||
|
||||
echo "米糠云隐私小号接口测试\n";
|
||||
echo "========================\n";
|
||||
echo "时间: " . date('Y-m-d H:i:s') . "\n";
|
||||
echo "AppKey: $appkey\n";
|
||||
echo "AppSecret: " . substr($appsecret, 0, 6) . "***\n";
|
||||
echo "A号码: $aParty\n";
|
||||
echo "API地址: $serverRoot\n";
|
||||
|
||||
// 检查配置
|
||||
if (empty($appsecret) || empty($aParty)) {
|
||||
echo "\n❌ 错误: 请填写完整的AppSecret和A号码\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// 测试1: 获取AXB可用小号
|
||||
echo "\n[测试1] 获取AXB可用小号\n";
|
||||
|
||||
$params = [
|
||||
'appkey' => $appkey,
|
||||
'time' => time(),
|
||||
];
|
||||
$params['sign'] = generateSign($params, $appsecret);
|
||||
|
||||
$url = $serverRoot . '/virtual/getaxbnumber';
|
||||
|
||||
echo "请求URL: $url\n";
|
||||
echo "请求参数: " . json_encode($params, JSON_UNESCAPED_UNICODE) . "\n";
|
||||
|
||||
$result = post($url, $params);
|
||||
printResult('获取AXB可用小号响应', $result);
|
||||
|
||||
if ($result['http_code'] !== 200) {
|
||||
echo "❌ HTTP请求失败\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$data = $result['decoded'];
|
||||
|
||||
if (empty($data['success'])) {
|
||||
echo "❌ 接口返回失败\n";
|
||||
echo "错误码: " . ($data['code'] ?? 'N/A') . "\n";
|
||||
echo "错误信息: " . ($data['msg'] ?? 'N/A') . "\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (empty($data['data'])) {
|
||||
echo "❌ 没有可用的隐私小号\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
echo "✅ 获取成功!可用小号列表:\n";
|
||||
foreach ($data['data'] as $index => $item) {
|
||||
echo " [" . ($index + 1) . "] 号码: {$item['number']}, 区号: {$item['region_code']}, 绑定数: {$item['bindcount']}\n";
|
||||
}
|
||||
|
||||
$availableNumber = $data['data'][0]['number'];
|
||||
$bParty = $availableNumber; // 使用获取到的第一个可用号作为B号码进行测试
|
||||
|
||||
// 测试2: AXB绑定
|
||||
echo "\n[测试2] AXB绑定\n";
|
||||
echo "隐私小号(X): $availableNumber\n";
|
||||
echo "A号码(用户): $aParty\n";
|
||||
echo "B号码(商家): $bParty\n";
|
||||
|
||||
$params = [
|
||||
'appkey' => $appkey,
|
||||
'time' => time(),
|
||||
'number' => $availableNumber,
|
||||
'aparty' => $aParty,
|
||||
'bparty' => $bParty,
|
||||
];
|
||||
$params['sign'] = generateSign($params, $appsecret);
|
||||
|
||||
$url = $serverRoot . '/virtual/axbBind';
|
||||
|
||||
echo "请求URL: $url\n";
|
||||
echo "请求参数: " . json_encode($params, JSON_UNESCAPED_UNICODE) . "\n";
|
||||
|
||||
$result = post($url, $params);
|
||||
printResult('AXB绑定响应', $result);
|
||||
|
||||
$data = $result['decoded'];
|
||||
|
||||
if (!empty($data['success']) && $data['code'] == 200) {
|
||||
echo "✅ 绑定成功!\n";
|
||||
|
||||
// 测试3: AXB解绑
|
||||
echo "\n[测试3] AXB解绑\n";
|
||||
|
||||
$params = [
|
||||
'appkey' => $appkey,
|
||||
'time' => time(),
|
||||
'number' => $availableNumber,
|
||||
'aparty' => $aParty,
|
||||
'bparty' => $bParty,
|
||||
];
|
||||
$params['sign'] = generateSign($params, $appsecret);
|
||||
|
||||
$url = $serverRoot . '/virtual/unAxbBind';
|
||||
|
||||
echo "请求URL: $url\n";
|
||||
echo "请求参数: " . json_encode($params, JSON_UNESCAPED_UNICODE) . "\n";
|
||||
|
||||
$result = post($url, $params);
|
||||
printResult('AXB解绑响应', $result);
|
||||
|
||||
$data = $result['decoded'];
|
||||
|
||||
if (!empty($data['success']) && $data['code'] == 200) {
|
||||
echo "✅ 解绑成功!\n";
|
||||
} else {
|
||||
echo "❌ 解绑失败\n";
|
||||
echo "错误信息: " . ($data['msg'] ?? 'N/A') . "\n";
|
||||
}
|
||||
} else {
|
||||
echo "❌ 绑定失败\n";
|
||||
echo "错误码: " . ($data['code'] ?? 'N/A') . "\n";
|
||||
echo "错误信息: " . ($data['msg'] ?? 'N/A') . "\n";
|
||||
}
|
||||
|
||||
echo "\n========== 测试完成 ==========\n";
|
||||
Reference in New Issue
Block a user