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

9.0 KiB
Raw Blame History

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

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
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

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
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 文件末尾(最后一个 }); 之后)追加:

// 公共 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
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.:

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)
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)
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)
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
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:

git add -A
git commit -m "fix: adjustments from manual verification of public stock API"