Ver código fonte

feat(sdk): 初始化旺店通SDK基础架构和核心功能

- 添加了SDK核心组件包括Client、Config、Authenticator等
- 实现了基础服务类和服务工厂模式
- 集成了HTTP客户端和响应处理机制
- 添加了API响应封装类ApiResponse
- 实现了基础API服务如店铺、仓库、物流查询
- 添加了商品、采购、库存、订单等业务服务- 集成了异常处理体系和日志记录功能- 添加了单元测试配置和示例代码
- 更新了composer依赖和项目配置文件
- 添加了完整的README文档和使用示例
runphp 6 meses atrás
pai
commit
87468d25a4

+ 31 - 0
.gitignore

@@ -1 +1,32 @@
+# Dependencies
 /vendor/
+composer.lock
+
+# IDE
+.idea/
+.vscode/
+*.swp
+*.swo
+
+# Testing
+.phpunit.cache/
+/coverage-html/
+coverage.txt
+phpunit.xml.local
+
+# Logs
+*.log
+/logs/
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Environment
+.env
+.env.local
+.env.*.local
+
+# Build artifacts
+/build/
+/dist/

+ 10 - 0
.idea/php.xml

@@ -10,10 +10,20 @@
     <option name="highlightLevel" value="WARNING" />
     <option name="transferred" value="true" />
   </component>
+  <component name="PhpIncludePathManager">
+    <include_path>
+      <path value="$PROJECT_DIR$/vendor/composer" />
+    </include_path>
+  </component>
   <component name="PhpProjectSharedConfiguration" php_language_level="8.3" />
   <component name="PhpStanOptionsConfiguration">
     <option name="transferred" value="true" />
   </component>
+  <component name="PhpUnit">
+    <phpunit_settings>
+      <PhpUnitSettings custom_loader_path="$PROJECT_DIR$/vendor/autoload.php" />
+    </phpunit_settings>
+  </component>
   <component name="PsalmOptionsConfiguration">
     <option name="transferred" value="true" />
   </component>

+ 5 - 1
.idea/sixshop-wangdian.iml

@@ -1,7 +1,11 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <module type="WEB_MODULE" version="4">
   <component name="NewModuleRootManager">
-    <content url="file://$MODULE_DIR$" />
+    <content url="file://$MODULE_DIR$">
+      <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" packagePrefix="SixShop\Wangdian\" />
+      <sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" packagePrefix="SixShop\Wangdian\Tests\" />
+      <excludeFolder url="file://$MODULE_DIR$/vendor/composer" />
+    </content>
     <orderEntry type="inheritedJdk" />
     <orderEntry type="sourceFolder" forTests="false" />
   </component>

+ 364 - 0
README.md

@@ -0,0 +1,364 @@
+# SixShop Wangdian SDK
+
+A modern PHP SDK for Wangdian (旺店通) API integration with PHP 8.3+ support.
+
+## Features
+
+- **Modern PHP 8.3+**: Uses latest PHP features including readonly classes, named parameters, and strict typing
+- **PSR Compatible**: Implements PSR-4 autoloading, PSR-18 HTTP client, and PSR-3 logging interfaces
+- **Service-Oriented Architecture**: Clean separation of concerns with dedicated service classes
+- **Type Safety**: Full type hints and strict typing for better IDE support and error prevention
+- **Exception Handling**: Comprehensive exception hierarchy for proper error handling
+- **Easy Configuration**: Simple configuration management for different environments
+- **Comprehensive API Coverage**: Supports all major Wangdian API endpoints
+
+## Installation
+
+```bash
+composer require six-shop/wangdian
+```
+
+## Quick Start
+
+### Basic Usage
+
+```php
+use SixShop\Wangdian\WangdianFactory;
+
+// Create a client for sandbox environment
+$client = WangdianFactory::createSandboxClient(
+    sid: 'your-sid',
+    appKey: 'your-app-key',  
+    appSecret: 'your-app-secret'
+);
+
+// Query shop information
+$response = $client->basic()->queryShop('api_test');
+
+if ($response->isSuccess()) {
+    $shopData = $response->getData();
+    echo "Shop Name: " . $shopData['shop_name'];
+} else {
+    echo "Error: " . $response->getMessage();
+}
+```
+
+### Production Environment
+
+```php
+use SixShop\Wangdian\WangdianFactory;
+
+// Create a client for production environment
+$client = WangdianFactory::createProductionClient(
+    sid: 'your-sid',
+    appKey: 'your-app-key',
+    appSecret: 'your-app-secret'
+);
+```
+
+### Custom Configuration
+
+```php
+use SixShop\Wangdian\Config\Config;
+use SixShop\Wangdian\WangdianFactory;
+
+$config = new Config(
+    sid: 'your-sid',
+    appKey: 'your-app-key',
+    appSecret: 'your-app-secret',
+    baseUrl: 'https://custom.wangdian.cn/openapi2',
+    timeout: 60,
+    debug: true
+);
+
+$client = WangdianFactory::createClient($config);
+```
+
+## API Services
+
+The SDK is organized into service classes for different API domains:
+
+### Basic Service
+
+Handle shops, warehouses, logistics, and purchase providers:
+
+```php
+// Query shop information
+$response = $client->basic()->queryShop('shop_no');
+
+// Query warehouses
+$response = $client->basic()->queryWarehouse();
+
+// Query logistics providers
+$response = $client->basic()->queryLogistics();
+
+// Query purchase providers
+$response = $client->basic()->queryPurchaseProvider();
+
+// Create purchase provider
+$response = $client->basic()->createPurchaseProvider([
+    'provider_name' => 'New Provider',
+    'contact' => 'John Doe',
+    'phone' => '1234567890'
+]);
+```
+
+### Goods Service
+
+Manage products and specifications:
+
+```php
+// Query goods
+$response = $client->goods()->query([
+    'start_time' => '2024-01-01 00:00:00',
+    'end_time' => '2024-01-31 23:59:59'
+]);
+
+// Push goods
+$response = $client->goods()->push([
+    'goods_list' => [
+        [
+            'goods_name' => 'Test Product',
+            'goods_no' => 'TEST001',
+            'brand_name' => 'Test Brand'
+        ]
+    ]
+]);
+
+// Push specifications
+$response = $client->goods()->pushSpec([
+    'spec_list' => [
+        [
+            'spec_name' => 'Red-Large',
+            'spec_no' => 'TEST001-RL',
+            'goods_no' => 'TEST001'
+        ]
+    ]
+]);
+```
+
+### Trade Service
+
+Handle orders and trade-related operations:
+
+```php
+// Push trade orders
+$response = $client->trade()->push([
+    'shop_no' => 'api_test',
+    'switch' => 0,
+    'trade_list' => [
+        [
+            'tid' => 'ORDER' . time(),
+            'trade_status' => 30,
+            'delivery_term' => 1,
+            'pay_status' => 2,
+            'buyer_nick' => 'customer001',
+            'receiver_name' => 'John Doe',
+            'receiver_mobile' => '13800138000',
+            'receiver_address' => 'Test Address',
+            'logistics_type' => 4,
+            'post_amount' => 10,
+            'paid' => 100,
+            'order_list' => [
+                [
+                    'oid' => 'ITEM' . time(),
+                    'status' => 30,
+                    'goods_id' => 'GOODS001',
+                    'goods_name' => 'Test Product',
+                    'num' => 1,
+                    'price' => 90,
+                    'cid' => '1'
+                ]
+            ]
+        ]
+    ]
+]);
+
+// Query trades
+$response = $client->trade()->query([
+    'start_time' => '2024-01-01 00:00:00',
+    'end_time' => '2024-01-31 23:59:59'
+]);
+```
+
+### Stock Service
+
+Manage inventory and stock operations:
+
+```php
+// Query stock
+$response = $client->stock()->query([
+    'start_time' => '2024-01-01 00:00:00',
+    'end_time' => '2024-01-31 23:59:59'
+]);
+
+// Push stock transfer
+$response = $client->stock()->pushTransfer([
+    'transfer_info' => [
+        'from_warehouse_no' => '001',
+        'to_warehouse_no' => '002',
+        'details_list' => [
+            [
+                'spec_no' => 'TEST001',
+                'num' => 10
+            ]
+        ]
+    ]
+]);
+
+// Push stockin order
+$response = $client->stock()->pushStockinOrder([
+    'stockin_info' => [
+        'warehouse_no' => '001',
+        'stockin_type' => 1,
+        'details_list' => [
+            [
+                'spec_no' => 'TEST001',
+                'num' => 100,
+                'price' => 50
+            ]
+        ]
+    ]
+]);
+```
+
+### Purchase Service
+
+Handle purchase orders and returns:
+
+```php
+// Push purchase order
+$response = $client->purchase()->pushOrder([
+    'purchase_info' => [
+        'provider_no' => '2',
+        'warehouse_no' => '001',
+        'outer_no' => 'PO' . time(),
+        'contact' => 'Supplier Contact',
+        'details_list' => [
+            [
+                'spec_no' => 'TEST001',
+                'num' => 50,
+                'price' => 40
+            ]
+        ]
+    ]
+]);
+
+// Query purchase orders
+$response = $client->purchase()->queryOrder([
+    'start_time' => '2024-01-01 00:00:00',
+    'end_time' => '2024-01-31 23:59:59'
+]);
+```
+
+### Refund Service
+
+Handle refund processing:
+
+```php
+// Query refunds
+$response = $client->refund()->query([
+    'start_time' => '2024-01-01 00:00:00',
+    'end_time' => '2024-01-31 23:59:59'
+]);
+
+// Push sales refund
+$response = $client->refund()->pushSalesRefund([
+    'refund_list' => [
+        [
+            'refund_no' => 'REF' . time(),
+            'tid' => 'ORDER123',
+            'refund_amount' => 50,
+            'refund_reason' => 'Customer request'
+        ]
+    ]
+]);
+```
+
+## Error Handling
+
+The SDK provides comprehensive error handling:
+
+```php
+use SixShop\Wangdian\Exception\ApiException;
+use SixShop\Wangdian\Exception\HttpException;
+use SixShop\Wangdian\Exception\ConfigException;
+
+try {
+    $response = $client->basic()->queryShop('invalid_shop');
+} catch (ApiException $e) {
+    // API returned an error
+    echo "API Error: " . $e->getMessage();
+    echo "API Code: " . $e->getApiCode();
+    var_dump($e->getResponseData());
+} catch (HttpException $e) {
+    // HTTP request failed
+    echo "HTTP Error: " . $e->getMessage();
+    echo "HTTP Status: " . $e->getHttpStatusCode();
+} catch (ConfigException $e) {
+    // Configuration error
+    echo "Config Error: " . $e->getMessage();
+}
+```
+
+## Response Handling
+
+All API responses are wrapped in an `ApiResponse` object:
+
+```php
+$response = $client->basic()->queryShop('api_test');
+
+// Check if successful
+if ($response->isSuccess()) {
+    // Get response data
+    $data = $response->getData();
+    
+    // Get specific fields
+    $shopName = $response->get('data.shop_name');
+    
+    // Convert to array
+    $array = $response->toArray();
+    
+    // Convert to JSON
+    $json = $response->toJson();
+} else {
+    // Handle error
+    echo "Error Code: " . $response->getCode();
+    echo "Error Message: " . $response->getMessage();
+}
+```
+
+## Logging
+
+The SDK supports PSR-3 compatible logging:
+
+```php
+use Monolog\Logger;
+use Monolog\Handler\StreamHandler;
+use SixShop\Wangdian\WangdianFactory;
+
+$logger = new Logger('wangdian');
+$logger->pushHandler(new StreamHandler('wangdian.log', Logger::INFO));
+
+$client = WangdianFactory::createSandboxClient(
+    sid: 'your-sid',
+    appKey: 'your-app-key',
+    appSecret: 'your-app-secret',
+    logger: $logger
+);
+```
+
+## Requirements
+
+- PHP >= 8.3
+- ext-json
+- ext-curl
+- guzzlehttp/guzzle ^7.8
+
+## Contributing
+
+Contributions are welcome! Please feel free to submit a Pull Request.
+
+## License
+
+This project is licensed under the MIT License - see the LICENSE file for details.

+ 37 - 2
composer.json

@@ -1,18 +1,53 @@
 {
     "name": "six-shop/wangdian",
-    "description": "旺店通SDK",
+    "description": "旺店通SDK - Modern PHP SDK for Wangdian API",
     "type": "library",
     "license": "MIT",
+    "keywords": ["wangdian", "sdk", "api", "ecommerce"],
+    "homepage": "https://github.com/six-shop/wangdian",
     "autoload": {
         "psr-4": {
             "SixShop\\Wangdian\\": "src/"
         }
     },
+    "autoload-dev": {
+        "psr-4": {
+            "SixShop\\Wangdian\\Tests\\": "tests/"
+        }
+    },
     "authors": [
         {
             "name": "runphp",
             "email": "runphp@qq.com"
         }
     ],
-    "require": {}
+    "require": {
+        "php": ">=8.3",
+        "ext-json": "*",
+        "ext-curl": "*",
+        "guzzlehttp/guzzle": "^7.8",
+        "psr/http-client": "^1.0",
+        "psr/http-factory": "^1.0",
+        "psr/log": "^3.0"
+    },
+    "require-dev": {
+        "phpunit/phpunit": "^10.0",
+        "phpstan/phpstan": "^1.10",
+        "squizlabs/php_codesniffer": "^3.7",
+        "friendsofphp/php-cs-fixer": "^3.0"
+    },
+    "scripts": {
+        "test": "phpunit",
+        "phpstan": "phpstan analyse src",
+        "cs-fix": "php-cs-fixer fix",
+        "cs-check": "phpcs --standard=PSR12 src"
+    },
+    "config": {
+        "sort-packages": true,
+        "allow-plugins": {
+            "php-http/discovery": true
+        }
+    },
+    "minimum-stability": "stable",
+    "prefer-stable": true
 }

+ 70 - 0
examples/basic_example.php

@@ -0,0 +1,70 @@
+<?php
+
+declare(strict_types=1);
+
+require_once __DIR__ . '/../vendor/autoload.php';
+
+use SixShop\Wangdian\WangdianFactory;
+use SixShop\Wangdian\Exception\ApiException;
+use SixShop\Wangdian\Exception\HttpException;
+
+// Create client for sandbox environment
+$client = WangdianFactory::createSandboxClient(
+    sid: 'your-sid',
+    appKey: 'your-app-key',
+    appSecret: 'your-app-secret'
+);
+
+try {
+    // Query shop information
+    echo "Querying shop information...\n";
+    $response = $client->basic()->queryShop('api_test');
+    
+    if ($response->isSuccess()) {
+        $shopData = $response->getData();
+        echo "Shop found: " . ($shopData['shop_name'] ?? 'Unknown') . "\n";
+        echo "Response: " . $response->toJson() . "\n";
+    } else {
+        echo "Shop query failed: " . $response->getMessage() . "\n";
+    }
+
+    // Query warehouses
+    echo "\nQuerying warehouses...\n";
+    $response = $client->basic()->queryWarehouse();
+    
+    if ($response->isSuccess()) {
+        $warehouses = $response->getData();
+        echo "Found " . count($warehouses) . " warehouses\n";
+        foreach ($warehouses as $warehouse) {
+            echo "- " . ($warehouse['warehouse_name'] ?? 'Unknown') . " ({$warehouse['warehouse_no']})\n";
+        }
+    } else {
+        echo "Warehouse query failed: " . $response->getMessage() . "\n";
+    }
+    
+    // Query logistics providers
+    echo "\nQuerying logistics providers...\n";
+    $response = $client->basic()->queryLogistics();
+    
+    if ($response->isSuccess()) {
+        $logistics = $response->getData();
+        echo "Found " . count($logistics) . " logistics providers\n";
+        foreach ($logistics as $provider) {
+            echo "- " . ($provider['logistics_name'] ?? 'Unknown') . " (Type: {$provider['logistics_type']})\n";
+        }
+    } else {
+        echo "Logistics query failed: " . $response->getMessage() . "\n";
+    }
+
+} catch (ApiException $e) {
+    echo "API Error: " . $e->getMessage() . "\n";
+    echo "API Code: " . $e->getApiCode() . "\n";
+    if ($e->getResponseData()) {
+        echo "Response Data: " . json_encode($e->getResponseData(), JSON_PRETTY_PRINT) . "\n";
+    }
+} catch (HttpException $e) {
+    echo "HTTP Error: " . $e->getMessage() . "\n";
+    echo "HTTP Status: " . $e->getHttpStatusCode() . "\n";
+} catch (Exception $e) {
+    echo "Unexpected Error: " . $e->getMessage() . "\n";
+}

+ 120 - 0
examples/stock_example.php

@@ -0,0 +1,120 @@
+<?php
+
+declare(strict_types=1);
+
+require_once __DIR__ . '/../vendor/autoload.php';
+
+use SixShop\Wangdian\WangdianFactory;
+use SixShop\Wangdian\Exception\ApiException;
+
+// Create client
+$client = WangdianFactory::createSandboxClient(
+    sid: 'your-sid',
+    appKey: 'your-app-key',
+    appSecret: 'your-app-secret'
+);
+
+try {
+    // Query current stock
+    echo "Querying stock information...\n";
+    
+    $queryParams = [
+        'start_time' => date('Y-m-d 00:00:00', strtotime('-7 days')),
+        'end_time' => date('Y-m-d 23:59:59'),
+    ];
+    
+    $response = $client->stock()->query($queryParams);
+    
+    if ($response->isSuccess()) {
+        $stocks = $response->getData();
+        echo "Found " . count($stocks) . " stock records\n";
+        
+        foreach (array_slice($stocks, 0, 10) as $stock) {
+            echo "- Spec: {$stock['spec_no']}, Stock: {$stock['num']}, Warehouse: {$stock['warehouse_no']}\n";
+        }
+    } else {
+        echo "Stock query failed: " . $response->getMessage() . "\n";
+    }
+    
+    // Push a stockin order
+    echo "\nPushing stockin order...\n";
+    
+    $stockinData = [
+        'stockin_info' => [
+            'warehouse_no' => '001',
+            'stockin_type' => 1, // Manual stockin
+            'outer_no' => 'STOCKIN_' . time(),
+            'remark' => 'SDK测试入库',
+            'details_list' => [
+                [
+                    'spec_no' => 'TEST001-001',
+                    'num' => 50,
+                    'price' => 45.0,
+                    'remark' => '测试商品入库'
+                ]
+            ]
+        ]
+    ];
+    
+    $response = $client->stock()->pushStockinOrder($stockinData);
+    
+    if ($response->isSuccess()) {
+        echo "Stockin order pushed successfully!\n";
+        echo "Response: " . $response->toJson() . "\n";
+    } else {
+        echo "Stockin order push failed: " . $response->getMessage() . "\n";
+    }
+    
+    // Push stock transfer between warehouses
+    echo "\nPushing stock transfer...\n";
+    
+    $transferData = [
+        'transfer_info' => [
+            'from_warehouse_no' => '001',
+            'to_warehouse_no' => '002',
+            'outer_no' => 'TRANSFER_' . time(),
+            'remark' => 'SDK测试调拨',
+            'details_list' => [
+                [
+                    'spec_no' => 'TEST001-001',
+                    'num' => 10,
+                    'remark' => '仓库间调拨'
+                ]
+            ]
+        ]
+    ];
+    
+    $response = $client->stock()->pushTransfer($transferData);
+    
+    if ($response->isSuccess()) {
+        echo "Stock transfer pushed successfully!\n";
+        echo "Response: " . $response->toJson() . "\n";
+    } else {
+        echo "Stock transfer push failed: " . $response->getMessage() . "\n";
+    }
+    
+    // Query stock transfers
+    echo "\nQuerying stock transfers...\n";
+    
+    $response = $client->stock()->queryTransfer([
+        'start_time' => date('Y-m-d 00:00:00'),
+        'end_time' => date('Y-m-d 23:59:59'),
+    ]);
+    
+    if ($response->isSuccess()) {
+        $transfers = $response->getData();
+        echo "Found " . count($transfers) . " transfers today\n";
+        
+        foreach (array_slice($transfers, 0, 5) as $transfer) {
+            echo "- Transfer No: {$transfer['transfer_no']}, Status: {$transfer['status']}\n";
+        }
+    } else {
+        echo "Transfer query failed: " . $response->getMessage() . "\n";
+    }
+
+} catch (ApiException $e) {
+    echo "API Error: " . $e->getMessage() . "\n";
+    echo "API Code: " . $e->getApiCode() . "\n";
+} catch (Exception $e) {
+    echo "Error: " . $e->getMessage() . "\n";
+}

+ 111 - 0
examples/trade_example.php

@@ -0,0 +1,111 @@
+<?php
+
+declare(strict_types=1);
+
+require_once __DIR__ . '/../vendor/autoload.php';
+
+use SixShop\Wangdian\WangdianFactory;
+use SixShop\Wangdian\Exception\ApiException;
+
+// Create client
+$client = WangdianFactory::createSandboxClient(
+    sid: 'your-sid',
+    appKey: 'your-app-key',
+    appSecret: 'your-app-secret'
+);
+
+try {
+    // Push a trade order
+    echo "Pushing trade order...\n";
+    
+    $tradeData = [
+        'shop_no' => 'api_test',
+        'switch' => 0,
+        'trade_list' => [
+            [
+                'tid' => 'SDK_TEST_' . time(),
+                'trade_status' => 30,
+                'delivery_term' => 1,
+                'pay_status' => 2,
+                'trade_time' => date('Y-m-d H:i:s'),
+                'pay_time' => date('Y-m-d H:i:s'),
+                'buyer_nick' => 'test_buyer',
+                'buyer_email' => 'test@example.com',
+                'receiver_mobile' => '13800138000',
+                'receiver_telno' => '',
+                'receiver_zip' => '100000',
+                'receiver_province' => '北京',
+                'receiver_name' => '张三',
+                'receiver_city' => '北京市',
+                'receiver_district' => '海淀区',
+                'receiver_address' => '测试地址123号',
+                'logistics_type' => 4, // EMS
+                'invoice_type' => 0,
+                'invoice_title' => '',
+                'invoice_content' => '',
+                'buyer_message' => 'SDK测试订单',
+                'remark' => 'SDK测试专用',
+                'remark_flag' => 1,
+                'post_amount' => 10.0,
+                'paid' => 109.0,
+                'cod_amount' => 0.0,
+                'ext_cod_fee' => 0.0,
+                'order_list' => [
+                    [
+                        'oid' => 'SDK_ITEM_' . time(),
+                        'status' => 30,
+                        'refund_status' => 0,
+                        'goods_id' => 'TEST_GOODS_001',
+                        'spec_id' => '',
+                        'goods_no' => 'TEST001',
+                        'spec_no' => 'TEST001-001',
+                        'goods_name' => '测试商品',
+                        'spec_name' => '测试规格',
+                        'num' => 1,
+                        'price' => 99.0,
+                        'adjust_amount' => 0.0,
+                        'discount' => 0.0,
+                        'share_discount' => 0.0,
+                        'cid' => '1',
+                    ]
+                ]
+            ]
+        ]
+    ];
+    
+    $response = $client->trade()->push($tradeData);
+    
+    if ($response->isSuccess()) {
+        echo "Trade pushed successfully!\n";
+        echo "Response: " . $response->toJson() . "\n";
+    } else {
+        echo "Trade push failed: " . $response->getMessage() . "\n";
+    }
+    
+    // Query recent trades
+    echo "\nQuerying recent trades...\n";
+    
+    $queryParams = [
+        'start_time' => date('Y-m-d 00:00:00'),
+        'end_time' => date('Y-m-d 23:59:59'),
+    ];
+    
+    $response = $client->trade()->query($queryParams);
+    
+    if ($response->isSuccess()) {
+        $trades = $response->getData();
+        echo "Found " . count($trades) . " trades today\n";
+        
+        foreach (array_slice($trades, 0, 5) as $trade) {
+            echo "- TID: {$trade['tid']}, Status: {$trade['trade_status']}, Amount: {$trade['paid']}\n";
+        }
+    } else {
+        echo "Trade query failed: " . $response->getMessage() . "\n";
+    }
+
+} catch (ApiException $e) {
+    echo "API Error: " . $e->getMessage() . "\n";
+    echo "API Code: " . $e->getApiCode() . "\n";
+} catch (Exception $e) {
+    echo "Error: " . $e->getMessage() . "\n";
+}

+ 27 - 0
phpunit.xml

@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.0/phpunit.xsd"
+         bootstrap="vendor/autoload.php"
+         colors="true"
+         cacheDirectory=".phpunit.cache"
+         testdox="true">
+    <testsuites>
+        <testsuite name="Unit">
+            <directory>tests/Unit</directory>
+        </testsuite>
+        <testsuite name="Integration">
+            <directory>tests/Integration</directory>
+        </testsuite>
+    </testsuites>
+    <source>
+        <include>
+            <directory>src</directory>
+        </include>
+    </source>
+    <coverage>
+        <report>
+            <html outputDirectory="coverage-html"/>
+            <text outputFile="coverage.txt"/>
+        </report>
+    </coverage>
+</phpunit>

+ 98 - 0
src/Auth/Authenticator.php

@@ -0,0 +1,98 @@
+<?php
+
+declare(strict_types=1);
+
+namespace SixShop\Wangdian\Auth;
+
+use SixShop\Wangdian\Config\Config;
+
+/**
+ * Handles API signature generation and authentication
+ */
+class Authenticator
+{
+    public function __construct(
+        private readonly Config $config
+    ) {
+    }
+
+    /**
+     * Generate API signature for the request
+     */
+    public function generateSignature(array $params): string
+    {
+        // Remove existing sign parameter if present
+        unset($params['sign']);
+
+        // Add required authentication parameters
+        $params['sid'] = $this->config->sid;
+        $params['appkey'] = $this->config->appKey;
+        $params['timestamp'] = time();
+
+        // Pack data according to Wangdian specification
+        $packedData = $this->packData($params);
+
+        // Generate MD5 signature
+        return md5($packedData . $this->config->appSecret);
+    }
+
+    /**
+     * Add authentication parameters to request
+     */
+    public function addAuthParams(array $params): array
+    {
+        $params['sid'] = $this->config->sid;
+        $params['appkey'] = $this->config->appKey;
+        $params['timestamp'] = time();
+        $params['sign'] = $this->generateSignature($params);
+
+        return $params;
+    }
+
+    /**
+     * Pack data according to Wangdian specification
+     * Format: length-key:length-value;length-key:length-value...
+     */
+    private function packData(array $params): string
+    {
+        ksort($params);
+        $packed = [];
+
+        foreach ($params as $key => $value) {
+            if ($key === 'sign') {
+                continue;
+            }
+
+            if (!empty($packed)) {
+                $packed[] = ';';
+            }
+
+            $keyLength = mb_strlen((string) $key, 'UTF-8');
+            $valueLength = mb_strlen((string) $value, 'UTF-8');
+
+            $packed[] = sprintf('%02d', $keyLength);
+            $packed[] = '-';
+            $packed[] = $key;
+            $packed[] = ':';
+            $packed[] = sprintf('%04d', $valueLength);
+            $packed[] = '-';
+            $packed[] = $value;
+        }
+
+        return implode('', $packed);
+    }
+
+    /**
+     * Validate request parameters
+     */
+    public function validateParams(array $params): void
+    {
+        $requiredKeys = ['sid', 'appkey', 'timestamp'];
+
+        foreach ($requiredKeys as $key) {
+            if (!isset($params[$key]) || empty($params[$key])) {
+                throw new \InvalidArgumentException("Missing required parameter: {$key}");
+            }
+        }
+    }
+}

+ 147 - 0
src/Client.php

@@ -0,0 +1,147 @@
+<?php
+
+declare(strict_types=1);
+
+namespace SixShop\Wangdian;
+
+use Psr\Http\Client\ClientInterface;
+use Psr\Log\LoggerInterface;
+use SixShop\Wangdian\Auth\Authenticator;
+use SixShop\Wangdian\Config\Config;
+use SixShop\Wangdian\Http\HttpClient;
+use SixShop\Wangdian\Response\ApiResponse;
+use SixShop\Wangdian\Response\ResponseHandler;
+use SixShop\Wangdian\Services\BasicService;
+use SixShop\Wangdian\Services\GoodsService;
+use SixShop\Wangdian\Services\PurchaseService;
+use SixShop\Wangdian\Services\RefundService;
+use SixShop\Wangdian\Services\StockService;
+use SixShop\Wangdian\Services\TradeService;
+
+/**
+ * Main SDK client for Wangdian API
+ */
+class Client
+{
+    private readonly HttpClient $httpClient;
+    private readonly Authenticator $authenticator;
+    private readonly ResponseHandler $responseHandler;
+
+    // Service instances
+    private ?BasicService $basicService = null;
+    private ?GoodsService $goodsService = null;
+    private ?PurchaseService $purchaseService = null;
+    private ?RefundService $refundService = null;
+    private ?StockService $stockService = null;
+    private ?TradeService $tradeService = null;
+
+    public function __construct(
+        private readonly Config $config,
+        ?ClientInterface $httpClient = null,
+        ?LoggerInterface $logger = null
+    ) {
+        $this->httpClient = new HttpClient($this->config, $httpClient, $logger);
+        $this->authenticator = new Authenticator($this->config);
+        $this->responseHandler = new ResponseHandler();
+    }
+
+    /**
+     * Make a raw API call
+     */
+    public function call(string $endpoint, array $params = []): ApiResponse
+    {
+        // Add authentication parameters and signature
+        $authenticatedParams = $this->authenticator->addAuthParams($params);
+
+        // Make HTTP request
+        $responseData = $this->httpClient->post($endpoint, $authenticatedParams);
+
+        // Handle and validate response
+        $response = $this->responseHandler->handle($responseData);
+        
+        // Throw exception if response indicates error
+        $this->responseHandler->validateOrThrow($responseData);
+
+        return $response;
+    }
+
+    /**
+     * Get Basic service (shops, warehouses, logistics, etc.)
+     */
+    public function basic(): BasicService
+    {
+        return $this->basicService ??= new BasicService($this);
+    }
+
+    /**
+     * Get Goods service (products, specifications, etc.)
+     */
+    public function goods(): GoodsService
+    {
+        return $this->goodsService ??= new GoodsService($this);
+    }
+
+    /**
+     * Get Purchase service (purchase orders, returns, etc.)
+     */
+    public function purchase(): PurchaseService
+    {
+        return $this->purchaseService ??= new PurchaseService($this);
+    }
+
+    /**
+     * Get Refund service (refund processing, etc.)
+     */
+    public function refund(): RefundService
+    {
+        return $this->refundService ??= new RefundService($this);
+    }
+
+    /**
+     * Get Stock service (inventory management, transfers, etc.)
+     */
+    public function stock(): StockService
+    {
+        return $this->stockService ??= new StockService($this);
+    }
+
+    /**
+     * Get Trade service (orders, logistics sync, etc.)
+     */
+    public function trade(): TradeService
+    {
+        return $this->tradeService ??= new TradeService($this);
+    }
+
+    /**
+     * Get the configuration
+     */
+    public function getConfig(): Config
+    {
+        return $this->config;
+    }
+
+    /**
+     * Get the HTTP client
+     */
+    public function getHttpClient(): HttpClient
+    {
+        return $this->httpClient;
+    }
+
+    /**
+     * Get the authenticator
+     */
+    public function getAuthenticator(): Authenticator
+    {
+        return $this->authenticator;
+    }
+
+    /**
+     * Get the response handler
+     */
+    public function getResponseHandler(): ResponseHandler
+    {
+        return $this->responseHandler;
+    }
+}

+ 96 - 0
src/Config/Config.php

@@ -0,0 +1,96 @@
+<?php
+
+declare(strict_types=1);
+
+namespace SixShop\Wangdian\Config;
+
+/**
+ * Configuration class for the Wangdian SDK
+ */
+readonly class Config
+{
+    public const SANDBOX_BASE_URL = 'https://sandbox.wangdian.cn/openapi2';
+    public const PRODUCTION_BASE_URL = 'https://www.wangdian.cn/openapi2';
+
+    public function __construct(
+        public string $sid,
+        public string $appKey,
+        public string $appSecret,
+        public string $baseUrl = self::SANDBOX_BASE_URL,
+        public int $timeout = 30,
+        public bool $debug = false,
+        public ?string $logFile = null,
+    ) {
+        $this->validate();
+    }
+
+    /**
+     * Create a configuration for sandbox environment
+     */
+    public static function sandbox(string $sid, string $appKey, string $appSecret): self
+    {
+        return new self(
+            sid: $sid,
+            appKey: $appKey,
+            appSecret: $appSecret,
+            baseUrl: self::SANDBOX_BASE_URL,
+            debug: true
+        );
+    }
+
+    /**
+     * Create a configuration for production environment
+     */
+    public static function production(string $sid, string $appKey, string $appSecret): self
+    {
+        return new self(
+            sid: $sid,
+            appKey: $appKey,
+            appSecret: $appSecret,
+            baseUrl: self::PRODUCTION_BASE_URL,
+            debug: false
+        );
+    }
+
+    /**
+     * Get full API endpoint URL
+     */
+    public function getEndpoint(string $api): string
+    {
+        return rtrim($this->baseUrl, '/') . '/' . ltrim($api, '/');
+    }
+
+    /**
+     * Check if running in sandbox mode
+     */
+    public function isSandbox(): bool
+    {
+        return str_contains($this->baseUrl, 'sandbox');
+    }
+
+    /**
+     * Validate configuration parameters
+     */
+    private function validate(): void
+    {
+        if (empty($this->sid)) {
+            throw new \InvalidArgumentException('SID cannot be empty');
+        }
+
+        if (empty($this->appKey)) {
+            throw new \InvalidArgumentException('App Key cannot be empty');
+        }
+
+        if (empty($this->appSecret)) {
+            throw new \InvalidArgumentException('App Secret cannot be empty');
+        }
+
+        if ($this->timeout <= 0) {
+            throw new \InvalidArgumentException('Timeout must be greater than 0');
+        }
+
+        if (!filter_var($this->baseUrl, FILTER_VALIDATE_URL)) {
+            throw new \InvalidArgumentException('Invalid base URL');
+        }
+    }
+}

+ 51 - 0
src/DTO/Trade/OrderItem.php

@@ -0,0 +1,51 @@
+<?php
+
+declare(strict_types=1);
+
+namespace SixShop\Wangdian\DTO\Trade;
+
+/**
+ * Order item DTO
+ */
+readonly class OrderItem
+{
+    public function __construct(
+        public string $oid,
+        public int $status,
+        public int $refundStatus,
+        public string $goodsId,
+        public string $goodsName,
+        public int $num,
+        public float $price,
+        public string $cid,
+        public ?string $specId = null,
+        public ?string $goodsNo = null,
+        public ?string $specNo = null,
+        public ?string $specName = null,
+        public ?float $adjustAmount = null,
+        public ?float $discount = null,
+        public ?float $shareDiscount = null,
+    ) {
+    }
+
+    public function toArray(): array
+    {
+        return [
+            'oid' => $this->oid,
+            'status' => $this->status,
+            'refund_status' => $this->refundStatus,
+            'goods_id' => $this->goodsId,
+            'spec_id' => $this->specId,
+            'goods_no' => $this->goodsNo,
+            'spec_no' => $this->specNo,
+            'goods_name' => $this->goodsName,
+            'spec_name' => $this->specName,
+            'num' => $this->num,
+            'price' => $this->price,
+            'adjust_amount' => $this->adjustAmount,
+            'discount' => $this->discount,
+            'share_discount' => $this->shareDiscount,
+            'cid' => $this->cid,
+        ];
+    }
+}

+ 79 - 0
src/DTO/Trade/TradeOrder.php

@@ -0,0 +1,79 @@
+<?php
+
+declare(strict_types=1);
+
+namespace SixShop\Wangdian\DTO\Trade;
+
+/**
+ * Trade order DTO
+ */
+readonly class TradeOrder
+{
+    public function __construct(
+        public string $tid,
+        public int $tradeStatus,
+        public int $deliveryTerm,
+        public int $payStatus,
+        public string $tradeTime,
+        public string $payTime,
+        public string $buyerNick,
+        public string $buyerEmail,
+        public string $receiverMobile,
+        public string $receiverName,
+        public string $receiverProvince,
+        public string $receiverCity,
+        public string $receiverDistrict,
+        public string $receiverAddress,
+        public int $logisticsType,
+        public float $postAmount,
+        public float $paid,
+        public array $orderList,
+        public ?string $receiverTelno = null,
+        public ?string $receiverZip = null,
+        public ?int $invoiceType = null,
+        public ?string $invoiceTitle = null,
+        public ?string $invoiceContent = null,
+        public ?string $buyerMessage = null,
+        public ?string $custData = null,
+        public ?string $remark = null,
+        public ?int $remarkFlag = null,
+        public ?float $codAmount = null,
+        public ?float $extCodFee = null,
+    ) {
+    }
+
+    public function toArray(): array
+    {
+        return [
+            'tid' => $this->tid,
+            'trade_status' => $this->tradeStatus,
+            'delivery_term' => $this->deliveryTerm,
+            'pay_status' => $this->payStatus,
+            'trade_time' => $this->tradeTime,
+            'pay_time' => $this->payTime,
+            'buyer_nick' => $this->buyerNick,
+            'buyer_email' => $this->buyerEmail,
+            'receiver_mobile' => $this->receiverMobile,
+            'receiver_telno' => $this->receiverTelno,
+            'receiver_zip' => $this->receiverZip,
+            'receiver_province' => $this->receiverProvince,
+            'receiver_name' => $this->receiverName,
+            'receiver_city' => $this->receiverCity,
+            'receiver_district' => $this->receiverDistrict,
+            'receiver_address' => $this->receiverAddress,
+            'logistics_type' => $this->logisticsType,
+            'invoice_type' => $this->invoiceType,
+            'invoice_title' => $this->invoiceTitle,
+            'invoice_content' => $this->invoiceContent,
+            'buyer_message' => $this->buyerMessage,
+            'cust_data' => $this->custData,
+            'remark' => $this->remark,
+            'remark_flag' => $this->remarkFlag,
+            'post_amount' => $this->postAmount,
+            'paid' => $this->paid,
+            'cod_amount' => $this->codAmount,
+            'ext_cod_fee' => $this->extCodFee,
+            'order_list' => $this->orderList,
+        ];
+    }
+}

+ 88 - 0
src/Exception/Exceptions.php

@@ -0,0 +1,88 @@
+<?php
+
+declare(strict_types=1);
+
+namespace SixShop\Wangdian\Exception;
+
+/**
+ * Configuration related exceptions
+ */
+class ConfigException extends WangdianException
+{
+}
+
+/**
+ * Authentication related exceptions
+ */
+class AuthException extends WangdianException
+{
+}
+
+/**
+ * HTTP request related exceptions
+ */
+class HttpException extends WangdianException
+{
+    public function __construct(
+        string $message = '',
+        int $code = 0,
+        ?\Throwable $previous = null,
+        ?array $context = null,
+        protected readonly ?int $httpStatusCode = null
+    ) {
+        parent::__construct($message, $code, $previous, $context);
+    }
+
+    public function getHttpStatusCode(): ?int
+    {
+        return $this->httpStatusCode;
+    }
+}
+
+/**
+ * API response related exceptions
+ */
+class ApiException extends WangdianException
+{
+    public function __construct(
+        string $message = '',
+        int $code = 0,
+        ?\Throwable $previous = null,
+        ?array $context = null,
+        protected readonly ?string $apiCode = null,
+        protected readonly ?array $responseData = null
+    ) {
+        parent::__construct($message, $code, $previous, $context);
+    }
+
+    public function getApiCode(): ?string
+    {
+        return $this->apiCode;
+    }
+
+    public function getResponseData(): ?array
+    {
+        return $this->responseData;
+    }
+}
+
+/**
+ * Validation related exceptions
+ */
+class ValidationException extends WangdianException
+{
+    public function __construct(
+        string $message = '',
+        int $code = 0,
+        ?\Throwable $previous = null,
+        ?array $context = null,
+        protected readonly array $errors = []
+    ) {
+        parent::__construct($message, $code, $previous, $context);
+    }
+
+    public function getErrors(): array
+    {
+        return $this->errors;
+    }
+}

+ 28 - 0
src/Exception/WangdianException.php

@@ -0,0 +1,28 @@
+<?php
+
+declare(strict_types=1);
+
+namespace SixShop\Wangdian\Exception;
+
+/**
+ * Base exception for all Wangdian SDK exceptions
+ */
+abstract class WangdianException extends \Exception
+{
+    public function __construct(
+        string $message = '',
+        int $code = 0,
+        ?\Throwable $previous = null,
+        protected readonly ?array $context = null
+    ) {
+        parent::__construct($message, $code, $previous);
+    }
+
+    /**
+     * Get additional context information
+     */
+    public function getContext(): ?array
+    {
+        return $this->context;
+    }
+}

+ 130 - 0
src/Http/HttpClient.php

@@ -0,0 +1,130 @@
+<?php
+
+declare(strict_types=1);
+
+namespace SixShop\Wangdian\Http;
+
+use GuzzleHttp\Client;
+use GuzzleHttp\Exception\GuzzleException;
+use GuzzleHttp\Exception\RequestException;
+use Psr\Http\Client\ClientInterface;
+use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
+use SixShop\Wangdian\Config\Config;
+use SixShop\Wangdian\Exception\HttpException;
+
+/**
+ * HTTP client wrapper for making API requests
+ */
+class HttpClient
+{
+    private readonly ClientInterface $client;
+    private readonly LoggerInterface $logger;
+
+    public function __construct(
+        private readonly Config $config,
+        ?ClientInterface $client = null,
+        ?LoggerInterface $logger = null
+    ) {
+        $this->client = $client ?? new Client([
+            'timeout' => $this->config->timeout,
+            'verify' => !$this->config->isSandbox(), // Disable SSL verification for sandbox
+            'headers' => [
+                'User-Agent' => 'SixShop-Wangdian-SDK/1.0',
+                'Accept' => 'application/json',
+            ],
+        ]);
+
+        $this->logger = $logger ?? new NullLogger();
+    }
+
+    /**
+     * Make a POST request to the API
+     */
+    public function post(string $endpoint, array $data = []): array
+    {
+        $url = $this->config->getEndpoint($endpoint);
+
+        try {
+            $this->logger->info('Making API request', [
+                'url' => $url,
+                'data_keys' => array_keys($data),
+            ]);
+
+            $response = $this->client->request('POST', $url, [
+                'form_params' => $data,
+                'headers' => [
+                    'Content-Type' => 'application/x-www-form-urlencoded',
+                ],
+            ]);
+
+            $statusCode = $response->getStatusCode();
+            $body = $response->getBody()->getContents();
+
+            $this->logger->info('API response received', [
+                'status_code' => $statusCode,
+                'response_length' => strlen($body),
+            ]);
+
+            if ($statusCode !== 200) {
+                throw new HttpException(
+                    message: "HTTP request failed with status code: {$statusCode}",
+                    httpStatusCode: $statusCode,
+                    context: ['response_body' => $body]
+                );
+            }
+
+            return $this->parseResponse($body);
+
+        } catch (RequestException $e) {
+            $this->logger->error('HTTP request failed', [
+                'url' => $url,
+                'error' => $e->getMessage(),
+            ]);
+
+            throw new HttpException(
+                message: 'HTTP request failed: ' . $e->getMessage(),
+                previous: $e,
+                httpStatusCode: $e->getResponse()?->getStatusCode(),
+                context: ['url' => $url, 'data' => $data]
+            );
+        } catch (GuzzleException $e) {
+            $this->logger->error('Guzzle exception', [
+                'url' => $url,
+                'error' => $e->getMessage(),
+            ]);
+
+            throw new HttpException(
+                message: 'HTTP client error: ' . $e->getMessage(),
+                previous: $e,
+                context: ['url' => $url, 'data' => $data]
+            );
+        }
+    }
+
+    /**
+     * Parse JSON response
+     */
+    private function parseResponse(string $body): array
+    {
+        if (empty($body)) {
+            throw new HttpException('Empty response body received');
+        }
+
+        $decoded = json_decode($body, true);
+
+        if (json_last_error() !== JSON_ERROR_NONE) {
+            $this->logger->error('Failed to parse JSON response', [
+                'json_error' => json_last_error_msg(),
+                'response_body' => $this->config->debug ? $body : '[hidden]',
+            ]);
+
+            throw new HttpException(
+                message: 'Failed to parse JSON response: ' . json_last_error_msg(),
+                context: ['response_body' => $body]
+            );
+        }
+
+        return $decoded;
+    }
+}

+ 88 - 0
src/Response/ApiResponse.php

@@ -0,0 +1,88 @@
+<?php
+
+declare(strict_types=1);
+
+namespace SixShop\Wangdian\Response;
+
+/**
+ * Represents an API response from Wangdian
+ */
+readonly class ApiResponse
+{
+    public function __construct(
+        private array $data
+    ) {
+    }
+
+    /**
+     * Check if the response indicates success
+     */
+    public function isSuccess(): bool
+    {
+        return isset($this->data['code']) && (int) $this->data['code'] === 0;
+    }
+
+    /**
+     * Get the response code
+     */
+    public function getCode(): int
+    {
+        return (int) ($this->data['code'] ?? -1);
+    }
+
+    /**
+     * Get the response message
+     */
+    public function getMessage(): string
+    {
+        return $this->data['message'] ?? $this->data['msg'] ?? '';
+    }
+
+    /**
+     * Get the response data
+     */
+    public function getData(): mixed
+    {
+        return $this->data['data'] ?? null;
+    }
+
+    /**
+     * Get all response data
+     */
+    public function getRawData(): array
+    {
+        return $this->data;
+    }
+
+    /**
+     * Convert response to array
+     */
+    public function toArray(): array
+    {
+        return $this->data;
+    }
+
+    /**
+     * Convert response to JSON string
+     */
+    public function toJson(): string
+    {
+        return json_encode($this->data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
+    }
+
+    /**
+     * Check if a key exists in the response
+     */
+    public function has(string $key): bool
+    {
+        return isset($this->data[$key]);
+    }
+
+    /**
+     * Get a value from the response by key
+     */
+    public function get(string $key, mixed $default = null): mixed
+    {
+        return $this->data[$key] ?? $default;
+    }
+}

+ 62 - 0
src/Response/ResponseHandler.php

@@ -0,0 +1,62 @@
+<?php
+
+declare(strict_types=1);
+
+namespace SixShop\Wangdian\Response;
+
+use SixShop\Wangdian\Exception\ApiException;
+
+/**
+ * Handles API response parsing and validation
+ */
+class ResponseHandler
+{
+    /**
+     * Process API response and validate success
+     */
+    public function handle(array $response): ApiResponse
+    {
+        return new ApiResponse($response);
+    }
+
+    /**
+     * Validate if the response indicates success
+     */
+    public function isSuccess(array $response): bool
+    {
+        // Wangdian API typically returns 'code' => 0 for success
+        return isset($response['code']) && (int) $response['code'] === 0;
+    }
+
+    /**
+     * Extract error information from response
+     */
+    public function extractError(array $response): ?array
+    {
+        if ($this->isSuccess($response)) {
+            return null;
+        }
+
+        return [
+            'code' => $response['code'] ?? 'unknown',
+            'message' => $response['message'] ?? $response['msg'] ?? 'Unknown error',
+            'data' => $response['data'] ?? null,
+        ];
+    }
+
+    /**
+     * Throw exception if response indicates error
+     */
+    public function validateOrThrow(array $response): void
+    {
+        if (!$this->isSuccess($response)) {
+            $error = $this->extractError($response);
+            
+            throw new ApiException(
+                message: $error['message'],
+                apiCode: (string) $error['code'],
+                responseData: $response
+            );
+        }
+    }
+}

+ 57 - 0
src/Services/BaseService.php

@@ -0,0 +1,57 @@
+<?php
+
+declare(strict_types=1);
+
+namespace SixShop\Wangdian\Services;
+
+use SixShop\Wangdian\Client;
+use SixShop\Wangdian\Response\ApiResponse;
+
+/**
+ * Base service class for all API services
+ */
+abstract class BaseService
+{
+    public function __construct(
+        protected readonly Client $client
+    ) {
+    }
+
+    /**
+     * Make an API call through the client
+     */
+    protected function call(string $endpoint, array $params = []): ApiResponse
+    {
+        return $this->client->call($endpoint, $params);
+    }
+
+    /**
+     * Validate required parameters
+     */
+    protected function validateRequired(array $params, array $required): void
+    {
+        foreach ($required as $field) {
+            if (!isset($params[$field]) || (is_string($params[$field]) && empty(trim($params[$field])))) {
+                throw new \InvalidArgumentException("Required parameter '{$field}' is missing or empty");
+            }
+        }
+    }
+
+    /**
+     * Filter out null and empty values from parameters
+     */
+    protected function filterParams(array $params): array
+    {
+        return array_filter($params, function ($value) {
+            return $value !== null && $value !== '';
+        });
+    }
+
+    /**
+     * Convert array to JSON string if needed
+     */
+    protected function encodeIfArray(mixed $value): mixed
+    {
+        return is_array($value) ? json_encode($value, JSON_UNESCAPED_UNICODE) : $value;
+    }
+}

+ 69 - 0
src/Services/BasicService.php

@@ -0,0 +1,69 @@
+<?php
+
+declare(strict_types=1);
+
+namespace SixShop\Wangdian\Services;
+
+use SixShop\Wangdian\Response\ApiResponse;
+
+/**
+ * Basic service for shops, warehouses, logistics, etc.
+ */
+class BasicService extends BaseService
+{
+    /**
+     * Query shop information
+     */
+    public function queryShop(string $shopNo): ApiResponse
+    {
+        return $this->call('shop.php', [
+            'shop_no' => $shopNo,
+        ]);
+    }
+
+    /**
+     * Query warehouse information
+     */
+    public function queryWarehouse(?string $warehouseNo = null): ApiResponse
+    {
+        $params = [];
+        if ($warehouseNo !== null) {
+            $params['warehouse_no'] = $warehouseNo;
+        }
+
+        return $this->call('warehouse_query.php', $params);
+    }
+
+    /**
+     * Query logistics providers
+     */
+    public function queryLogistics(): ApiResponse
+    {
+        return $this->call('logistics.php');
+    }
+
+    /**
+     * Query purchase providers
+     */
+    public function queryPurchaseProvider(?string $providerNo = null): ApiResponse
+    {
+        $params = [];
+        if ($providerNo !== null) {
+            $params['provider_no'] = $providerNo;
+        }
+
+        return $this->call('purchase_provider_query.php', $params);
+    }
+
+    /**
+     * Create purchase provider
+     */
+    public function createPurchaseProvider(array $providerData): ApiResponse
+    {
+        $this->validateRequired($providerData, ['provider_name']);
+
+        return $this->call('purchase_provider_create.php', 
+            $this->filterParams($providerData)
+        );
+    }
+}

+ 53 - 0
src/Services/GoodsService.php

@@ -0,0 +1,53 @@
+<?php
+
+declare(strict_types=1);
+
+namespace SixShop\Wangdian\Services;
+
+use SixShop\Wangdian\Response\ApiResponse;
+
+/**
+ * Goods service for products, specifications, etc.
+ */
+class GoodsService extends BaseService
+{
+    /**
+     * Query goods information
+     */
+    public function query(array $params = []): ApiResponse
+    {
+        return $this->call('goods_query.php', $this->filterParams($params));
+    }
+
+    /**
+     * Push goods information
+     */
+    public function push(array $goodsData): ApiResponse
+    {
+        $this->validateRequired($goodsData, ['goods_list']);
+
+        return $this->call('goods_push.php', [
+            'goods_list' => $this->encodeIfArray($goodsData['goods_list']),
+        ]);
+    }
+
+    /**
+     * Push goods specification
+     */
+    public function pushSpec(array $specData): ApiResponse
+    {
+        $this->validateRequired($specData, ['spec_list']);
+
+        return $this->call('api_goods_spec_push.php', [
+            'spec_list' => $this->encodeIfArray($specData['spec_list']),
+        ]);
+    }
+
+    /**
+     * Query suites (套装商品)
+     */
+    public function querySuites(array $params = []): ApiResponse
+    {
+        return $this->call('suites_query.php', $this->filterParams($params));
+    }
+}

+ 89 - 0
src/Services/PurchaseService.php

@@ -0,0 +1,89 @@
+<?php
+
+declare(strict_types=1);
+
+namespace SixShop\Wangdian\Services;
+
+use SixShop\Wangdian\Response\ApiResponse;
+
+/**
+ * Purchase service for purchase orders, returns, etc.
+ */
+class PurchaseService extends BaseService
+{
+    /**
+     * Push purchase order
+     */
+    public function pushOrder(array $orderData): ApiResponse
+    {
+        $this->validateRequired($orderData, ['purchase_info']);
+
+        return $this->call('purchase_order_push.php', [
+            'purchase_info' => $this->encodeIfArray($orderData['purchase_info']),
+        ]);
+    }
+
+    /**
+     * Query purchase order
+     */
+    public function queryOrder(array $params = []): ApiResponse
+    {
+        return $this->call('purchase_order_query.php', $this->filterParams($params));
+    }
+
+    /**
+     * Push purchase return order
+     */
+    public function pushReturnOrder(array $returnData): ApiResponse
+    {
+        $this->validateRequired($returnData, ['return_info']);
+
+        return $this->call('purchase_return_order_push.php', [
+            'return_info' => $this->encodeIfArray($returnData['return_info']),
+        ]);
+    }
+
+    /**
+     * Push purchase return
+     */
+    public function pushReturn(array $returnData): ApiResponse
+    {
+        return $this->call('purchase_return_push.php', 
+            $this->filterParams($returnData)
+        );
+    }
+
+    /**
+     * Query purchase return
+     */
+    public function queryReturn(array $params = []): ApiResponse
+    {
+        return $this->call('purchase_return_query.php', $this->filterParams($params));
+    }
+
+    /**
+     * Push stockin purchase
+     */
+    public function pushStockinPurchase(array $stockinData): ApiResponse
+    {
+        return $this->call('stockin_purchase_push.php', 
+            $this->filterParams($stockinData)
+        );
+    }
+
+    /**
+     * Query stockin order for purchase
+     */
+    public function queryStockinOrder(array $params = []): ApiResponse
+    {
+        return $this->call('stockin_order_query_purchase.php', $this->filterParams($params));
+    }
+
+    /**
+     * Query stockout return order
+     */
+    public function queryStockoutReturnOrder(array $params = []): ApiResponse
+    {
+        return $this->call('stockout_return_order_query.php', $this->filterParams($params));
+    }
+}

+ 51 - 0
src/Services/RefundService.php

@@ -0,0 +1,51 @@
+<?php
+
+declare(strict_types=1);
+
+namespace SixShop\Wangdian\Services;
+
+use SixShop\Wangdian\Response\ApiResponse;
+
+/**
+ * Refund service for refund processing, etc.
+ */
+class RefundService extends BaseService
+{
+    /**
+     * Query refund information
+     */
+    public function query(array $params = []): ApiResponse
+    {
+        return $this->call('refund_query.php', $this->filterParams($params));
+    }
+
+    /**
+     * Push sales refund
+     */
+    public function pushSalesRefund(array $refundData): ApiResponse
+    {
+        $this->validateRequired($refundData, ['refund_list']);
+
+        return $this->call('sales_refund_push.php', [
+            'refund_list' => $this->encodeIfArray($refundData['refund_list']),
+        ]);
+    }
+
+    /**
+     * Push stockin refund
+     */
+    public function pushStockinRefund(array $refundData): ApiResponse
+    {
+        return $this->call('stockin_refund_push.php', 
+            $this->filterParams($refundData)
+        );
+    }
+
+    /**
+     * Query stockin refund
+     */
+    public function queryStockinRefund(array $params = []): ApiResponse
+    {
+        return $this->call('stockin_refund_query.php', $this->filterParams($params));
+    }
+}

+ 119 - 0
src/Services/StockService.php

@@ -0,0 +1,119 @@
+<?php
+
+declare(strict_types=1);
+
+namespace SixShop\Wangdian\Services;
+
+use SixShop\Wangdian\Response\ApiResponse;
+
+/**
+ * Stock service for inventory management, transfers, etc.
+ */
+class StockService extends BaseService
+{
+    /**
+     * Query stock information
+     */
+    public function query(array $params = []): ApiResponse
+    {
+        return $this->call('stock_query.php', $this->filterParams($params));
+    }
+
+    /**
+     * Push stock transfer
+     */
+    public function pushTransfer(array $transferData): ApiResponse
+    {
+        $this->validateRequired($transferData, ['transfer_info']);
+
+        return $this->call('stock_transfer_push.php', [
+            'transfer_info' => $this->encodeIfArray($transferData['transfer_info']),
+        ]);
+    }
+
+    /**
+     * Query stock transfer
+     */
+    public function queryTransfer(array $params = []): ApiResponse
+    {
+        return $this->call('stock_transfer_query.php', $this->filterParams($params));
+    }
+
+    /**
+     * Push stockin order
+     */
+    public function pushStockinOrder(array $orderData): ApiResponse
+    {
+        $this->validateRequired($orderData, ['stockin_info']);
+
+        return $this->call('stockin_order_push.php', [
+            'stockin_info' => $this->encodeIfArray($orderData['stockin_info']),
+        ]);
+    }
+
+    /**
+     * Query stockin order
+     */
+    public function queryStockinOrder(array $params = []): ApiResponse
+    {
+        return $this->call('stockin_order_query.php', $this->filterParams($params));
+    }
+
+    /**
+     * Push stockout order
+     */
+    public function pushStockoutOrder(array $orderData): ApiResponse
+    {
+        $this->validateRequired($orderData, ['stockout_info']);
+
+        return $this->call('stockout_order_push.php', [
+            'stockout_info' => $this->encodeIfArray($orderData['stockout_info']),
+        ]);
+    }
+
+    /**
+     * Query stockout order
+     */
+    public function queryStockoutOrder(array $params = []): ApiResponse
+    {
+        return $this->call('stockout_order_query.php', $this->filterParams($params));
+    }
+
+    /**
+     * Push stockin transfer
+     */
+    public function pushStockinTransfer(array $transferData): ApiResponse
+    {
+        return $this->call('stockin_transfer_push.php', 
+            $this->filterParams($transferData)
+        );
+    }
+
+    /**
+     * Push stockout transfer
+     */
+    public function pushStockoutTransfer(array $transferData): ApiResponse
+    {
+        return $this->call('stockout_transfer_push.php', 
+            $this->filterParams($transferData)
+        );
+    }
+
+    /**
+     * Sync stock by PD (盘点)
+     */
+    public function syncByPd(array $pdData): ApiResponse
+    {
+        return $this->call('stock_sync_by_pd.php', 
+            $this->filterParams($pdData)
+        );
+    }
+
+    /**
+     * Query stock PD order
+     */
+    public function queryPdOrder(array $params = []): ApiResponse
+    {
+        return $this->call('stock_pd_order_query.php', $this->filterParams($params));
+    }
+}

+ 83 - 0
src/Services/TradeService.php

@@ -0,0 +1,83 @@
+<?php
+
+declare(strict_types=1);
+
+namespace SixShop\Wangdian\Services;
+
+use SixShop\Wangdian\Response\ApiResponse;
+
+/**
+ * Trade service for orders, logistics sync, etc.
+ */
+class TradeService extends BaseService
+{
+    /**
+     * Push trade information
+     */
+    public function push(array $tradeData): ApiResponse
+    {
+        $this->validateRequired($tradeData, ['shop_no', 'trade_list']);
+
+        return $this->call('trade_push.php', [
+            'shop_no' => $tradeData['shop_no'],
+            'switch' => $tradeData['switch'] ?? 0,
+            'trade_list' => $this->encodeIfArray($tradeData['trade_list']),
+        ]);
+    }
+
+    /**
+     * Query trade information
+     */
+    public function query(array $params = []): ApiResponse
+    {
+        return $this->call('trade_query.php', $this->filterParams($params));
+    }
+
+    /**
+     * Query logistics sync information
+     */
+    public function queryLogisticsSync(array $params = []): ApiResponse
+    {
+        return $this->call('logistics_sync_query.php', $this->filterParams($params));
+    }
+
+    /**
+     * Acknowledge logistics sync
+     */
+    public function ackLogisticsSync(array $logisticsIds): ApiResponse
+    {
+        $this->validateRequired(['logistics_ids' => $logisticsIds], ['logistics_ids']);
+
+        return $this->call('logistics_sync_ack.php', [
+            'logistics_ids' => $this->encodeIfArray($logisticsIds),
+        ]);
+    }
+
+    /**
+     * Query goods stock change
+     */
+    public function queryGoodsStockChange(array $params = []): ApiResponse
+    {
+        return $this->call('api_goods_stock_change_query.php', $this->filterParams($params));
+    }
+
+    /**
+     * Acknowledge goods stock change
+     */
+    public function ackGoodsStockChange(array $changeIds): ApiResponse
+    {
+        $this->validateRequired(['change_ids' => $changeIds], ['change_ids']);
+
+        return $this->call('api_goods_stock_change_ack.php', [
+            'change_ids' => $this->encodeIfArray($changeIds),
+        ]);
+    }
+
+    /**
+     * Query stockout orders for trade
+     */
+    public function queryStockoutOrder(array $params = []): ApiResponse
+    {
+        return $this->call('stockout_order_query_trade.php', $this->filterParams($params));
+    }
+}

+ 77 - 0
src/WangdianFactory.php

@@ -0,0 +1,77 @@
+<?php
+
+declare(strict_types=1);
+
+namespace SixShop\Wangdian;
+
+use Psr\Http\Client\ClientInterface;
+use Psr\Log\LoggerInterface;
+use SixShop\Wangdian\Config\Config;
+
+/**
+ * Factory class for easy SDK initialization
+ */
+class WangdianFactory
+{
+    /**
+     * Create a client for sandbox environment
+     */
+    public static function createSandboxClient(
+        string $sid,
+        string $appKey,
+        string $appSecret,
+        ?ClientInterface $httpClient = null,
+        ?LoggerInterface $logger = null
+    ): Client {
+        $config = Config::sandbox($sid, $appKey, $appSecret);
+        return new Client($config, $httpClient, $logger);
+    }
+
+    /**
+     * Create a client for production environment
+     */
+    public static function createProductionClient(
+        string $sid,
+        string $appKey,
+        string $appSecret,
+        ?ClientInterface $httpClient = null,
+        ?LoggerInterface $logger = null
+    ): Client {
+        $config = Config::production($sid, $appKey, $appSecret);
+        return new Client($config, $httpClient, $logger);
+    }
+
+    /**
+     * Create a client with custom configuration
+     */
+    public static function createClient(
+        Config $config,
+        ?ClientInterface $httpClient = null,
+        ?LoggerInterface $logger = null
+    ): Client {
+        return new Client($config, $httpClient, $logger);
+    }
+
+    /**
+     * Create a configuration instance
+     */
+    public static function createConfig(
+        string $sid,
+        string $appKey,
+        string $appSecret,
+        string $baseUrl = Config::SANDBOX_BASE_URL,
+        int $timeout = 30,
+        bool $debug = false,
+        ?string $logFile = null
+    ): Config {
+        return new Config(
+            sid: $sid,
+            appKey: $appKey,
+            appSecret: $appSecret,
+            baseUrl: $baseUrl,
+            timeout: $timeout,
+            debug: $debug,
+            logFile: $logFile
+        );
+    }
+}