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>
This commit is contained in:
@@ -0,0 +1,105 @@
|
|||||||
|
<?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', '%' . addcslashes($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('查询失败,请稍后重试');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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', '%' . addcslashes($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('查询失败,请稍后重试');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?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 ($validKey === false || $apiKey === '' || !hash_equals($validKey, $apiKey)) {
|
||||||
|
$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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
-1
@@ -637,4 +637,10 @@ Route::group('/crm', function () {
|
|||||||
Route::any('/update', [app\common\controller\VehModificationController::class, 'update']);
|
Route::any('/update', [app\common\controller\VehModificationController::class, 'update']);
|
||||||
Route::any('/destroy', [app\common\controller\VehModificationController::class, 'destroy']);
|
Route::any('/destroy', [app\common\controller\VehModificationController::class, 'destroy']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 公共 API(API Key 鉴权;通过 noNeedLogin/noNeedHeaderData 跳过 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]);
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
# 备货单公共 API 对接文档
|
||||||
|
|
||||||
|
## 基础信息
|
||||||
|
|
||||||
|
| 项 | 值 |
|
||||||
|
|---|---|
|
||||||
|
| Base URL | `https://your-domain.com` |
|
||||||
|
| 鉴权方式 | 请求头 `Fdesk-Api-Key` |
|
||||||
|
| 响应格式 | JSON |
|
||||||
|
|
||||||
|
## 鉴权
|
||||||
|
|
||||||
|
所有请求必须携带 API Key 请求头:
|
||||||
|
|
||||||
|
```
|
||||||
|
Fdesk-Api-Key: <your-api-key>
|
||||||
|
```
|
||||||
|
|
||||||
|
缺少或无效时返回:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "fail",
|
||||||
|
"code": 401,
|
||||||
|
"message": "无效的 API Key",
|
||||||
|
"data": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 通用响应结构
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success | fail",
|
||||||
|
"code": 200,
|
||||||
|
"message": "描述信息",
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| code | 含义 |
|
||||||
|
|------|------|
|
||||||
|
| 200 | 成功 |
|
||||||
|
| 400 | 查询失败 |
|
||||||
|
| 401 | API Key 无效或缺失 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 接口列表
|
||||||
|
|
||||||
|
### 1. PI 号列表
|
||||||
|
|
||||||
|
查询已审批通过的备货单 PI 号,含关联的车架号和车型。
|
||||||
|
|
||||||
|
**请求**
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /common/public/pi/list
|
||||||
|
```
|
||||||
|
|
||||||
|
**参数**
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| page | int | 否 | 页码,默认 1 |
|
||||||
|
| page_size | int | 否 | 每页条数,默认 20,最大 200 |
|
||||||
|
| pi_no | string | 否 | PI 号筛选,模糊匹配 |
|
||||||
|
|
||||||
|
**响应示例**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"code": 200,
|
||||||
|
"message": "请求成功",
|
||||||
|
"data": {
|
||||||
|
"total": 150,
|
||||||
|
"per_page": 20,
|
||||||
|
"current_page": 1,
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"pi_no": "PI20250601001",
|
||||||
|
"vins": ["LSVAU2A39N2123456", "LSVAU2A39N2123457"],
|
||||||
|
"model_names": ["BYD Song Plus DM-i", "BYD Seal"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**字段说明**
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| pi_no | string | 备货单 PI 号 |
|
||||||
|
| vins | string[] | 该 PI 下所有车架号 |
|
||||||
|
| model_names | string[] | 该 PI 下所有车型名称(与 vins 一一对应) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. VIN 号列表
|
||||||
|
|
||||||
|
查询已审批通过的备货单车架号及对应车型。
|
||||||
|
|
||||||
|
**请求**
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /common/public/vin/list
|
||||||
|
```
|
||||||
|
|
||||||
|
**参数**
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| page | int | 否 | 页码,默认 1 |
|
||||||
|
| page_size | int | 否 | 每页条数,默认 20,最大 200 |
|
||||||
|
| vin | string | 否 | 车架号筛选,模糊匹配 |
|
||||||
|
|
||||||
|
**响应示例**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"code": 200,
|
||||||
|
"message": "请求成功",
|
||||||
|
"data": {
|
||||||
|
"total": 500,
|
||||||
|
"per_page": 20,
|
||||||
|
"current_page": 1,
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"vin": "LSVAU2A39N2123456",
|
||||||
|
"model_name": "BYD Song Plus DM-i"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**字段说明**
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| vin | string | 车架号 |
|
||||||
|
| model_name | string | 车型名称 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 调用示例
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查询 PI 列表(第一页,每页10条)
|
||||||
|
curl -H "Fdesk-Api-Key: your-api-key" \
|
||||||
|
"https://your-domain.com/common/public/pi/list?page=1&page_size=10"
|
||||||
|
|
||||||
|
# 按 PI 号搜索
|
||||||
|
curl -H "Fdesk-Api-Key: your-api-key" \
|
||||||
|
"https://your-domain.com/common/public/pi/list?pi_no=PI202506"
|
||||||
|
|
||||||
|
# 查询 VIN 列表(第一页,每页50条)
|
||||||
|
curl -H "Fdesk-Api-Key: your-api-key" \
|
||||||
|
"https://your-domain.com/common/public/vin/list?page=1&page_size=50"
|
||||||
|
|
||||||
|
# 按 VIN 号搜索
|
||||||
|
curl -H "Fdesk-Api-Key: your-api-key" \
|
||||||
|
"https://your-domain.com/common/public/vin/list?vin=LSVAU2A3"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
- 两个接口仅返回审批状态为"已通过"(approval_status=2)的备货单数据
|
||||||
|
- 已删除的备货单不会出现在查询结果中
|
||||||
|
- 接口为只读,不支持新增、修改、删除操作
|
||||||
|
- `pi_no` 和 `vin` 筛选参数支持模糊匹配(包含即可)
|
||||||
@@ -0,0 +1,290 @@
|
|||||||
|
# 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"
|
||||||
|
```
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
# Public Stock Order API Design
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Provide two read-only public APIs for internal systems to query approved stock order PI numbers and VIN numbers, with API Key authentication.
|
||||||
|
|
||||||
|
## Data Model
|
||||||
|
|
||||||
|
- `stock_orders.approval_status = 2` means approved
|
||||||
|
- `stock_orders.pi_no` — PI number (string)
|
||||||
|
- `stock_order_items.vin` — VIN/chassis number (string)
|
||||||
|
- `stock_order_items.model_snapshot` — car model name (plain string)
|
||||||
|
- `stock_order_items.order_id` → `stock_orders.id`
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
New middleware `ApiKeyAuth`:
|
||||||
|
|
||||||
|
- Read `Fdesk-Api-Key` header
|
||||||
|
- Compare against key stored in `.env` (`API_KEY`) or `config/app.php`
|
||||||
|
- Reject with 401 on mismatch or missing key
|
||||||
|
- Bypass both `AuthCheck` (JWT) and `BasicData` middleware
|
||||||
|
|
||||||
|
## Endpoints
|
||||||
|
|
||||||
|
### A. PI Number List
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /common/public/pi/list
|
||||||
|
Headers: Fdesk-Api-Key: <key>
|
||||||
|
Query: page, page_size, pi_no (optional filter)
|
||||||
|
```
|
||||||
|
|
||||||
|
Response:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"code": 200,
|
||||||
|
"data": {
|
||||||
|
"total": 150,
|
||||||
|
"per_page": 20,
|
||||||
|
"current_page": 1,
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"pi_no": "PI20250601001",
|
||||||
|
"vins": ["LSVAU2A39N2123456", "LSVAU2A39N2123457"],
|
||||||
|
"model_names": ["Model Y", "Model 3"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Logic:
|
||||||
|
1. Query `stock_orders` where `approval_status = 2` and `pi_no IS NOT NULL` and `pi_no != ''`
|
||||||
|
2. Optional filter by `pi_no` (LIKE match)
|
||||||
|
3. Paginate
|
||||||
|
4. For each order, eager-load `items` → collect `vin` and `model_snapshot`
|
||||||
|
5. Return aggregated result
|
||||||
|
|
||||||
|
### B. VIN Number List
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /common/public/vin/list
|
||||||
|
Headers: Fdesk-Api-Key: <key>
|
||||||
|
Query: page, page_size, vin (optional filter)
|
||||||
|
```
|
||||||
|
|
||||||
|
Response:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "success",
|
||||||
|
"code": 200,
|
||||||
|
"data": {
|
||||||
|
"total": 500,
|
||||||
|
"per_page": 20,
|
||||||
|
"current_page": 1,
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"vin": "LSVAU2A39N2123456",
|
||||||
|
"model_name": "Model Y"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Logic:
|
||||||
|
1. Query `stock_order_items` via `stock_orders` where `approval_status = 2`
|
||||||
|
2. Optional filter by `vin` (LIKE match)
|
||||||
|
3. Paginate
|
||||||
|
4. Return `vin` + `model_snapshot` per item
|
||||||
|
|
||||||
|
## File Changes
|
||||||
|
|
||||||
|
| Action | File | Description |
|
||||||
|
|--------|------|-------------|
|
||||||
|
| New | `app/middleware/ApiKeyAuth.php` | API Key auth middleware |
|
||||||
|
| New | `app/common/controller/PublicApiController.php` | Two endpoints |
|
||||||
|
| Modify | `config/route.php` | Add `/common/public/*` route group with ApiKeyAuth |
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
|
||||||
|
- Read-only, no write operations
|
||||||
|
- Only `approval_status = 2` records
|
||||||
|
- Pagination required (default page_size = 20)
|
||||||
|
- No JWT / BasicData middleware for these routes
|
||||||
Reference in New Issue
Block a user