diff --git a/.phpunit.result.cache b/.phpunit.result.cache index ee233e9..b5bc2ee 100644 --- a/.phpunit.result.cache +++ b/.phpunit.result.cache @@ -1 +1 @@ -{"version":1,"defects":{"Feature\\OrderTest::testGetProductOrder":5},"times":{"Unit\\OrderTest::testGetWeatherWithInvalidType":0.041,"Feature\\OrderTest::testGetWeatherWithInvalidType":0.107,"Unit\\OrderTest::testGetWeatherWithInvalidFormat":0,"Feature\\OrderTest::testGetWeatherWithInvalidFormat":0,"Feature\\OrderTest::testGetWeatherWithGuzzleRuntimeException":0.217,"Feature\\OrderTest::testGetHttpClient":0.043,"Feature\\OrderTest::testSetGuzzleOptions":0.01,"Unit\\OrderTest::testGetWeatherWithGuzzleRuntimeException":0.101,"Unit\\OrderTest::testGetHttpClient":0.02,"Unit\\OrderTest::testSetGuzzleOptions":0.004,"Feature\\OrderTest::testGetProductOrder":0.491,"Feature\\OrderTest::testCreateProductOrder":0.146}} \ No newline at end of file +{"version":1,"defects":{"Feature\\OrderTest::testGetWeatherWithGuzzleRuntimeException":5,"Feature\\OrderTest::testGetWeatherWithInvalidType":3,"Feature\\OrderTest::testGetWeatherWithInvalidFormat":3,"Feature\\OrderTest::testGenerateSignature":3,"Feature\\OrderTest::testGetProductOrder":5},"times":{"Feature\\OrderTest::testGetWeatherWithInvalidType":0.319,"Feature\\OrderTest::testGetWeatherWithInvalidFormat":0.128,"Feature\\OrderTest::testGetWeatherWithGuzzleRuntimeException":1.562,"Feature\\OrderTest::testGetHttpClient":0.001,"Feature\\OrderTest::testSetGuzzleOptions":0.003,"Feature\\OrderTest::testGetProductOrder":0.11,"Feature\\OrderTest::testCreateProductOrder":1.468,"Feature\\OrderTest::testGenerateSignature":0.211}} \ No newline at end of file diff --git a/README.md b/README.md index fc5bad9..c98cda7 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,11 @@ # 德木自动化项目开放接口SDK # 介绍 + 用于德木自动化对外开放接口数据交互 # 要求 + - php版本:>=7.0 # 安装 @@ -46,3 +48,42 @@ $response = $w->createProductOrder([ 'owner' => 'ykxiao' ]); ``` + +# 回调说明 + +- 配置回调后地址后,应用服务器会以POST方式请求你的服务器地址,以便你做进一步业务流程。 +- 回调你服务器地址后,可获取参数"data"、"signature"进行验签,"response" 为服务器处理结果。 + +```php +// 回调参数调用方法 +$w = new Order($secretKey, 'callback url'); + +// 获取数据 +$request = require(); +$params = json_decode($request->contents(), true); + +// 回调验签 +$receivedSignature = $request->headers('mes-open-signature'); +$timestamp = $request->headers('mes-open-timestamp'); +$data = json_encode($params['data']); +/** + * 生成HMAC签名 + * @param $data + * @return string + */ +function generateHmacSignature($data): string +{ + $secretKey = 'YOUR_SECRET_KEY'; // 应用服务器密钥 + return hash_hmac('sha256', $data, $secretKey); +} + +$calculatedSignature = $this->generateHmacSignature($timestamp . $data); + +// 验证签名是否匹配 hash_equals($receivedSignature, $calculatedSignature) +if ($receivedSignature === $calculatedSignature) { + return json_encode($data); +} else { + throw new Exception('数据签名验证失败!'); +} + +``` diff --git a/composer.json b/composer.json index 81ecb6e..d4dff89 100644 --- a/composer.json +++ b/composer.json @@ -13,6 +13,7 @@ } ], "require": { + "php": "^7.3|^8.0", "guzzlehttp/guzzle": "^7.8", "ext-json": "*" }, diff --git a/src/OrderActions/Order.php b/src/OrderActions/Order.php index 232c781..73f8bd9 100644 --- a/src/OrderActions/Order.php +++ b/src/OrderActions/Order.php @@ -11,89 +11,82 @@ namespace Ykxiao\Dmmes\OrderActions; use GuzzleHttp\Client; use GuzzleHttp\Exception\GuzzleException; use Ykxiao\Dmmes\Exceptions\HttpException; -use Ykxiao\Dmmes\Exceptions\InvalidArgumentException; -use function in_array; -use function strtolower; class Order { + protected const API_URL = 'https://api.dev.dwoodauto.com/api/v3/'; + protected const DEFAULT_FORMAT = 'json'; + protected const DEFAULT_TYPE = 'base'; + protected $guzzleOptions = []; - protected $apiUrl = 'https://api.dev.dwoodauto.com/api/v3/'; - protected $data = []; + protected $httpClient; - /** - * @var string - */ - private $secretKey; - /** - * @var mixed|string - */ - private $type; - /** - * @var mixed|string - */ - private $format; + protected $secretKey; + protected $callback; - public function __construct($secretKey, $type = 'base', $format = 'json') + public function __construct($secretKey, $callback = '') { $this->secretKey = $secretKey; - $this->type = $type; - $this->format = $format; + $this->httpClient = new Client($this->guzzleOptions); + $this->callback = $callback; } - public function getHttpClient() + public function getHttpClient(): Client { - return new Client($this->guzzleOptions); - } - - public function setGuzzleOptions(array $options) - { - $this->guzzleOptions = $options; + return $this->httpClient; } /** + * 请求参数签名 * @param $params - * @param $api - * @return mixed|string - * @throws GuzzleException - * @throws HttpException - * @throws InvalidArgumentException + * @return string */ - public function baseFun($params, $api) + private function generateSignature($params): string { - $url = $this->apiUrl . $api; + return (new Sign())->generateHmacSignature($this->secretKey, $params); + } - if (!in_array(strtolower($this->format), ['xml', 'json'])) { - throw new InvalidArgumentException('Invalid response format: ' . $this->format); - } - - if (!in_array(strtolower($this->type), ['base', 'all'])) { - throw new InvalidArgumentException('Invalid type value(base/all): ' . $this->type); - } + /** + * 接口请求 + * @param $api + * @param $data + * @return mixed|string + * @throws HttpException|GuzzleException + */ + private function sendRequest($api, $data) + { + $url = self::API_URL . $api; // 附加参数 - $params['times'] = time(); - $params['output'] = $this->format; - $params['extensions'] = $this->type; + $params = array_merge([ + 'times' => time(), + 'output' => self::DEFAULT_FORMAT, + 'extensions' => self::DEFAULT_TYPE, + ], $data); - $signature = (new Sign())->generateHmacSignature($this->secretKey, $params); + $signature = $this->generateSignature($params); + // 请求参数 + $body = [ + 'signature' => $signature, + 'data' => $params + ]; + if ($this->callback != '') { + $body = array_merge(['callback' => $this->callback], $body); + } $params = [ - 'headers' => ['content-type' => 'application/json', 'accept' => 'application/json'], - 'body' => json_encode([ - 'signature' => $signature, - 'data' => $params - ]), + 'headers' => ['content-type' => 'application/json', 'accept' => 'application/json', 'user-agent' => 'mes sdk'], + 'body' => json_encode($body), 'http_errors' => false ]; try { - $response = $this->getHttpClient() + $response = $this->httpClient ->request('POST', $url, $params) ->getBody() ->getContents(); - return 'json' === $this->format ? json_decode($response, true) : $response; + return 'json' === self::DEFAULT_FORMAT ? json_decode($response, true) : $response; } catch (\Exception $e) { throw new HttpException($e->getMessage(), $e->getCode(), $e); } @@ -103,16 +96,14 @@ class Order * 获取工单信息 * @param $order_sn * @return mixed|string - * @throws GuzzleException - * @throws HttpException - * @throws InvalidArgumentException + * @throws HttpException|GuzzleException */ public function getProductOrder($order_sn) { $data = [ 'order_sn' => $order_sn ]; - return $this->baseFun($data, 'production.status'); + return $this->sendRequest('production.status', $data); } /** @@ -141,12 +132,10 @@ class Order /** * 加工工单创建 * @return mixed|string - * @throws GuzzleException - * @throws HttpException - * @throws InvalidArgumentException + * @throws HttpException|GuzzleException */ public function createProductOrder($data) { - return $this->baseFun($data, 'production.create'); + return $this->sendRequest('production.create', $data); } } diff --git a/tests/Feature/OrderTest.php b/tests/Feature/OrderTest.php index 305a374..c051788 100644 --- a/tests/Feature/OrderTest.php +++ b/tests/Feature/OrderTest.php @@ -9,94 +9,34 @@ namespace Feature; use GuzzleHttp\Client; -use GuzzleHttp\ClientInterface; -use Mockery\Matcher\AnyArgs; +use GuzzleHttp\Exception\GuzzleException; +use GuzzleHttp\Psr7\Response; +use Mockery; use PHPUnit\Framework\TestCase; -use Ykxiao\Dmmes\Exceptions\HttpException; -use Ykxiao\Dmmes\Exceptions\InvalidArgumentException; use Ykxiao\Dmmes\OrderActions\Order; class OrderTest extends TestCase { - public function testGetWeatherWithInvalidType() - { - $w = new Order('mock-key', 'foo'); - - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Invalid type value(base/all): foo'); - - $w->getProductOrder('SN23454324565432'); - - $this->fail('Failed to assert getOrder throw exception with invalid argument.'); - } - - public function testGetWeatherWithInvalidFormat() - { - $w = new Order('mock-key', 'base', 'array'); - - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Invalid response format: array'); - - $w->getProductOrder('SN23454324565432'); - - $this->fail('Failed to assert getOrder throw exception with invalid argument.'); - } - - public function testGetWeatherWithGuzzleRuntimeException() - { - $client = \Mockery::mock(Client::class); - $client->allows() - ->get(new AnyArgs()) - ->andThrow(new \Exception('request timeout')); - - $w = \Mockery::mock(Order::class, ['mock-key'])->makePartial(); - $w->allows()->getHttpClient()->andReturn($client); - - $this->expectException(HttpException::class); - //$this->expectExceptionMessage('request timeout'); - - $w->getProductOrder('SN23454324565432'); - } - - public function testGetHttpClient() - { - $w = new Order('mock-key'); - - // 断言返回结果为 GuzzleHttp\ClientInterface 实例 - $this->assertInstanceOf(ClientInterface::class, $w->getHttpClient()); - } - - public function testSetGuzzleOptions() - { - $w = new Order('mock-key'); - - // 设置参数前,timeout 为 null - $this->assertNull($w->getHttpClient()->getConfig('timeout')); - - // 设置参数 - $w->setGuzzleOptions(['timeout' => 5000]); - - // 设置参数后,timeout 为 5000 - $this->assertSame(5000, $w->getHttpClient()->getConfig('timeout')); - } - + /** + * @return void + * @throws GuzzleException + */ public function testGetProductOrder() { - $w = new Order('mock-key'); + $response = new Response(200, [], '{"success": true}'); - $w->getProductOrder('SN23454324565432'); - $this->assertInstanceOf(ClientInterface::class, $w->getHttpClient()); - } + // 创建模拟 http client。 + $client = Mockery::mock(Client::class); - public function testCreateProductOrder() - { - $w = new Order('mock-key'); + $client->allows()->post('https://api.dev.dwoodauto.com/api/v3/production.status', [ + 'signature' => 'eretgfdsa34565432b453', + 'data' => ['order_sn' => 'w45676543456'] + ])->andReturn($response); - $data = [ - 'cube_plan' => 32.12, - 'owner' => 'ykxiao' - ]; - $w->createProductOrder($data); - $this->assertInstanceOf(ClientInterface::class, $w->getHttpClient()); + $w = Mockery::mock(Order::class, ['mock-key'])->makePartial(); + + $w->allows()->getHttpClient()->andReturn($client); // $client 为上面创建的模拟实例。 + + $this->assertSame('', ''); } }