Files
fdesk/docs/superpowers/plans/2026-06-02-public-stock-api.md
T
mzl92 35d98c16e9 feat: 新增备货单公共 API 接口(PI 号/VIN 号列表)
- 新增 ApiKeyAuth 中间件(hash_equals 防时序攻击)
- 新增 PublicApiController(piList/vinList 两个只读接口)
- 仅返回已审批通过(approval_status=2)的数据,支持分页和模糊搜索
- LIKE 通配符已转义,异常信息不暴露内部细节
- 附对接文档 docs/public-stock-api.md

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 17:44:53 +08:00

291 lines
9.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Public Stock Order API Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add two read-only public APIs (PI list, VIN list) for internal systems to query approved stock orders, authenticated via API Key.
**Architecture:** New `PublicApiController` in `app/common/controller/` with a dedicated `ApiKeyAuth` middleware. Routes registered in a standalone route group that bypasses JWT and BasicData middleware. Pagination via Eloquent's `paginate()`.
**Tech Stack:** PHP 8.1+, Webman framework, Eloquent ORM, illuminate/pagination
---
## File Structure
| Action | File | Responsibility |
|--------|------|----------------|
| Create | `app/middleware/ApiKeyAuth.php` | Validate `Fdesk-Api-Key` header against env config |
| Create | `app/common/controller/PublicApiController.php` | Two endpoints: PI list, VIN list |
| Modify | `config/route.php` | Add `/common/public/*` route group with ApiKeyAuth middleware |
---
### Task 1: Create ApiKeyAuth Middleware
**Files:**
- Create: `app/middleware/ApiKeyAuth.php`
- [ ] **Step 1: Create the middleware file**
```php
<?php
namespace app\middleware;
use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;
class ApiKeyAuth implements MiddlewareInterface
{
public function process(Request $request, callable $handler): Response
{
$apiKey = $request->header('Fdesk-Api-Key');
$validKey = getenv('PUBLIC_API_KEY');
if (!$apiKey || !$validKey || $apiKey !== $validKey) {
$body = [
'status' => 'fail',
'code' => 401,
'message' => '无效的 API Key',
'data' => [],
];
return response(json_encode($body, JSON_UNESCAPED_UNICODE), 401, ['Content-Type' => 'application/json']);
}
return $handler($request);
}
}
```
- [ ] **Step 2: Commit**
```bash
git add app/middleware/ApiKeyAuth.php
git commit -m "feat: add ApiKeyAuth middleware for public API endpoints"
```
---
### Task 2: Create PublicApiController
**Files:**
- Create: `app/common/controller/PublicApiController.php`
- [ ] **Step 1: Create the controller file**
```php
<?php
namespace app\common\controller;
use app\common\model\StockOrders;
use app\common\model\StockOrderItems;
use support\Request;
class PublicApiController extends BaseController
{
protected $noNeedLogin = ['piList', 'vinList'];
protected $noNeedHeaderData = ['piList', 'vinList'];
/**
* PI 号列表(含关联的 VIN 和车型名称)
* GET /common/public/pi/list
*/
public function piList(Request $request)
{
try {
$page = (int)($request->input('page', 1));
$pageSize = (int)($request->input('page_size', 20));
$piNo = $request->input('pi_no', '');
if ($page < 1) $page = 1;
if ($pageSize < 1 || $pageSize > 200) $pageSize = 20;
$query = StockOrders::where('approval_status', 2)
->whereNotNull('pi_no')
->where('pi_no', '<>', '');
if ($piNo !== '') {
$query->where('pi_no', 'like', "%{$piNo}%");
}
$query->with(['items' => function ($q) {
$q->select('id', 'order_id', 'vin', 'model_snapshot');
}]);
$paginator = $query->orderBy('id', 'desc')
->paginate($pageSize, ['id', 'pi_no'], 'page', $page);
$data = $paginator->getCollection()->map(function ($order) {
return [
'pi_no' => $order->pi_no,
'vins' => $order->items->pluck('vin')->filter()->values()->toArray(),
'model_names' => $order->items->pluck('model_snapshot')->filter()->values()->toArray(),
];
})->toArray();
return $this->success([
'total' => $paginator->total(),
'per_page' => $paginator->perPage(),
'current_page' => $paginator->currentPage(),
'data' => $data,
]);
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}
/**
* VIN 号列表(含车型名称)
* GET /common/public/vin/list
*/
public function vinList(Request $request)
{
try {
$page = (int)($request->input('page', 1));
$pageSize = (int)($request->input('page_size', 20));
$vin = $request->input('vin', '');
if ($page < 1) $page = 1;
if ($pageSize < 1 || $pageSize > 200) $pageSize = 20;
$query = StockOrderItems::whereHas('order', function ($q) {
$q->where('approval_status', 2);
})->whereNotNull('vin')
->where('vin', '<>', '');
if ($vin !== '') {
$query->where('vin', 'like', "%{$vin}%");
}
$paginator = $query->orderBy('id', 'desc')
->paginate($pageSize, ['id', 'vin', 'model_snapshot'], 'page', $page);
$data = $paginator->getCollection()->map(function ($item) {
return [
'vin' => $item->vin,
'model_name' => $item->model_snapshot,
];
})->toArray();
return $this->success([
'total' => $paginator->total(),
'per_page' => $paginator->perPage(),
'current_page' => $paginator->currentPage(),
'data' => $data,
]);
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}
}
```
- [ ] **Step 2: Commit**
```bash
git add app/common/controller/PublicApiController.php
git commit -m "feat: add PublicApiController with PI and VIN list endpoints"
```
---
### Task 3: Register Routes
**Files:**
- Modify: `config/route.php`
- [ ] **Step 1: Add the route group**
在 `config/route.php` 文件末尾(最后一个 `});` 之后)追加:
```php
// 公共 API(API Key 鉴权,不走 JWT / BasicData)
Route::group('/common/public', function () {
Route::any('/pi/list', [app\common\controller\PublicApiController::class, 'piList']);
Route::any('/vin/list', [app\common\controller\PublicApiController::class, 'vinList']);
})->middleware([app\middleware\AccessControl::class, app\middleware\ApiKeyAuth::class]);
```
This route group only uses `AccessControl` (CORS) + `ApiKeyAuth`. It explicitly bypasses `BasicData` and `AuthCheck`.
- [ ] **Step 2: Commit**
```bash
git add config/route.php
git commit -m "feat: register public API routes with ApiKeyAuth middleware"
```
---
### Task 4: Configure API Key in Environment
**Files:**
- Modify: `.env` (or equivalent environment config)
- [ ] **Step 1: Add the API key to the environment config**
Add to the `.env` file (or equivalent environment variable source):
```
PUBLIC_API_KEY=your-secret-api-key-here
```
Generate a real key, e.g.:
```bash
php -r "echo bin2hex(random_bytes(32)) . PHP_EOL;"
```
Use the generated value as the key.
- [ ] **Step 2: Verify the middleware reads it correctly**
The middleware uses `getenv('PUBLIC_API_KEY')`. Confirm this is consistent with how other env vars are read in the project (e.g., `getenv('DB_HOST')` in `config/database.php`).
---
### Task 5: Manual Verification
- [ ] **Step 1: Test PI list endpoint (no auth)**
```bash
curl -s http://localhost:8787/common/public/pi/list | php -r 'echo json_encode(json_decode(file_get_contents("php://stdin")), JSON_PRETTY_PRINT);'
```
Expected: `{"status":"fail","code":401,"message":"无效的 API Key"}`
- [ ] **Step 2: Test PI list endpoint (with auth)**
```bash
curl -s -H "Fdesk-Api-Key: your-secret-api-key-here" "http://localhost:8787/common/public/pi/list?page=1&page_size=5" | php -r 'echo json_encode(json_decode(file_get_contents("php://stdin")), JSON_PRETTY_PRINT);'
```
Expected: JSON with `status: success`, `data` containing `total`, `per_page`, `current_page`, and array of `{pi_no, vins, model_names}`.
- [ ] **Step 3: Test VIN list endpoint (with auth)**
```bash
curl -s -H "Fdesk-Api-Key: your-secret-api-key-here" "http://localhost:8787/common/public/vin/list?page=1&page_size=5" | php -r 'echo json_encode(json_decode(file_get_contents("php://stdin")), JSON_PRETTY_PRINT);'
```
Expected: JSON with `status: success`, `data` containing array of `{vin, model_name}`.
- [ ] **Step 4: Test with pi_no filter**
```bash
curl -s -H "Fdesk-Api-Key: your-secret-api-key-here" "http://localhost:8787/common/public/pi/list?pi_no=PI2025" | php -r 'echo json_encode(json_decode(file_get_contents("php://stdin")), JSON_PRETTY_PRINT);'
```
Expected: filtered results or empty data array.
- [ ] **Step 5: Commit final verification**
No code changes expected. If fixes were needed during testing, commit them:
```bash
git add -A
git commit -m "fix: adjustments from manual verification of public stock API"
```