Files
datahub/backend/app/Controller/Api/V1/SkuMappingController.php
T
2026-04-17 11:06:34 +08:00

627 lines
26 KiB
PHP
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.
<?php
declare(strict_types=1);
namespace App\Controller\Api\V1;
use App\Controller\AbstractController;
use App\Middleware\AuthMiddleware;
use App\Middleware\PermissionMiddleware;
use App\Model\SkuMapping;
use App\Model\SkuOrigin;
use App\Model\Store;
use App\Service\OperationLogService;
use App\Service\SkuService;
use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\Middleware;
use Hyperf\HttpServer\Annotation\RequestMapping;
use OpenApi\Attributes as OA;
use Psr\Http\Message\ResponseInterface;
/**
* SKU 平台映射管理接口
*/
#[OA\Tag(name: 'SkuMappings', description: 'SKU 平台映射管理')]
#[Controller(prefix: "/api/v1/sku-mappings")]
#[Middleware(AuthMiddleware::class)]
#[Middleware(PermissionMiddleware::class)]
class SkuMappingController extends AbstractController
{
public function __construct(
protected readonly SkuService $skuService,
) {}
/**
* 映射列表
*/
#[OA\Get(
path: '/sku-mappings',
summary: 'SKU 映射列表',
description: '获取 SKU 平台映射列表,支持分页和多维度筛选',
security: [['bearerAuth' => []]],
tags: ['SkuMappings'],
parameters: [
new OA\Parameter(name: 'page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 1)),
new OA\Parameter(name: 'per_page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 15, maximum: 100)),
new OA\Parameter(name: 'company_id', in: 'query', required: false, description: '公司 ID 精确筛选', schema: new OA\Schema(type: 'integer')),
new OA\Parameter(name: 'platform_id', in: 'query', required: false, description: '平台 ID 精确筛选', schema: new OA\Schema(type: 'integer')),
new OA\Parameter(name: 'store_id', in: 'query', required: false, description: '店铺 ID 精确筛选', schema: new OA\Schema(type: 'integer')),
new OA\Parameter(name: 'origin_sku', in: 'query', required: false, description: '原始 SKU 模糊搜索', schema: new OA\Schema(type: 'string')),
new OA\Parameter(name: 'platform_outer_sku', in: 'query', required: false, description: '平台侧 SKU 模糊搜索', schema: new OA\Schema(type: 'string')),
new OA\Parameter(name: 'origin_sku_id', in: 'query', required: false, description: '内部 SKU ID 精确筛选', schema: new OA\Schema(type: 'integer')),
new OA\Parameter(name: 'enabled', in: 'query', required: false, description: '启用状态', schema: new OA\Schema(type: 'boolean')),
new OA\Parameter(name: 'bundled', in: 'query', required: false, description: '组合商品映射筛选', schema: new OA\Schema(type: 'boolean')),
],
responses: [
new OA\Response(
response: 200,
description: '获取成功',
content: new OA\JsonContent(properties: [
new OA\Property(property: 'code', type: 'integer', example: 0),
new OA\Property(property: 'message', type: 'string', example: '获取成功'),
new OA\Property(property: 'data', properties: [
new OA\Property(property: 'items', type: 'array', items: new OA\Items(ref: '#/components/schemas/SkuMapping')),
new OA\Property(property: 'total', type: 'integer', example: 100),
new OA\Property(property: 'page', type: 'integer', example: 1),
new OA\Property(property: 'per_page', type: 'integer', example: 15),
], type: 'object'),
])
),
new OA\Response(response: 401, description: '未认证', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
new OA\Response(response: 403, description: '无权限', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
]
)]
#[RequestMapping(path: "", methods: "GET")]
public function index(): array
{
$query = SkuMapping::query()->select([
'id', 'company_id', 'platform_id', 'store_id', 'origin_sku',
'origin_sku_id', 'platform_outer_sku', 'platform_product_id',
'enabled', 'bundled', 'note', 'created_at', 'updated_at',
]);
$this->applyDataScope($query);
// 筛选条件
$filters = [
'company_id' => 'exact',
'platform_id' => 'exact',
'store_id' => 'exact',
'origin_sku_id' => 'exact',
];
foreach ($filters as $field => $type) {
$value = $this->request->input($field);
if ($value !== null && $value !== '') {
$query->where($field, (int) $value);
}
}
$origin_sku = $this->request->input('origin_sku');
if ($origin_sku !== null && $origin_sku !== '') {
$query->where('origin_sku', 'ilike', "%{$origin_sku}%");
}
$platform_outer_sku = $this->request->input('platform_outer_sku');
if ($platform_outer_sku !== null && $platform_outer_sku !== '') {
$query->where('platform_outer_sku', 'ilike', "%{$platform_outer_sku}%");
}
$enabled = $this->request->input('enabled');
if ($enabled !== null && $enabled !== '') {
$query->where('enabled', filter_var($enabled, FILTER_VALIDATE_BOOLEAN));
}
$bundled = $this->request->input('bundled');
if ($bundled !== null && $bundled !== '') {
$query->where('bundled', filter_var($bundled, FILTER_VALIDATE_BOOLEAN));
}
$query->orderBy('created_at', 'desc');
$per_page = min(max((int) $this->request->input('per_page', 15), 1), 100);
$page = max((int) $this->request->input('page', 1), 1);
$total = $query->count();
$items = $query->offset(($page - 1) * $per_page)->limit($per_page)->get();
return [
'code' => 0,
'message' => '获取成功',
'data' => [
'items' => $items,
'total' => $total,
'page' => $page,
'per_page' => $per_page,
],
];
}
/**
* 检查重复映射
*/
#[OA\Get(
path: '/sku-mappings/check-duplicate',
summary: '检查同平台重复映射',
description: '检查指定 origin_sku 在某平台下是否已存在映射记录,帮助用户决定是否复用已有编码',
security: [['bearerAuth' => []]],
tags: ['SkuMappings'],
parameters: [
new OA\Parameter(name: 'origin_sku_id', in: 'query', required: true, description: '内部 SKU ID', schema: new OA\Schema(type: 'integer')),
new OA\Parameter(name: 'platform_id', in: 'query', required: true, description: '平台 ID', schema: new OA\Schema(type: 'integer')),
],
responses: [
new OA\Response(
response: 200,
description: '检查完成',
content: new OA\JsonContent(properties: [
new OA\Property(property: 'code', type: 'integer', example: 0),
new OA\Property(property: 'data', properties: [
new OA\Property(property: 'has_duplicate', type: 'boolean', example: true),
new OA\Property(property: 'existing_mappings', type: 'array', items: new OA\Items(type: 'object')),
new OA\Property(property: 'message', type: 'string'),
], type: 'object'),
])
),
new OA\Response(response: 422, description: '参数校验失败', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
]
)]
#[RequestMapping(path: "check-duplicate", methods: "GET")]
public function checkDuplicate(): ResponseInterface|array
{
$origin_sku_id = $this->request->input('origin_sku_id');
$platform_id = $this->request->input('platform_id');
if (!$origin_sku_id || !$platform_id) {
return $this->response->json([
'code' => 422,
'message' => '缺少必填参数: origin_sku_id, platform_id',
])->withStatus(422);
}
$result = $this->skuService->checkDuplicate((int) $origin_sku_id, (int) $platform_id);
return [
'code' => 0,
'message' => 'success',
'data' => $result,
];
}
/**
* 自动生成 platform_outer_sku
*/
#[OA\Post(
path: '/sku-mappings/generate-sku',
summary: '自动生成平台侧 SKU',
description: '根据指定策略自动生成 platform_outer_sku,同时返回重复检测结果',
security: [['bearerAuth' => []]],
tags: ['SkuMappings'],
requestBody: new OA\RequestBody(
required: true,
content: new OA\JsonContent(
required: ['origin_sku_id', 'platform_id', 'strategy'],
properties: [
new OA\Property(property: 'origin_sku_id', type: 'integer', example: 1),
new OA\Property(property: 'platform_id', type: 'integer', example: 1),
new OA\Property(property: 'strategy', type: 'string', enum: ['prefix', 'prefix_random', 'manual'], example: 'prefix'),
new OA\Property(property: 'prefix', type: 'string', example: 'C03'),
new OA\Property(property: 'random_length', type: 'integer', example: 4),
new OA\Property(property: 'manual_value', type: 'string', nullable: true),
]
)
),
responses: [
new OA\Response(
response: 200,
description: '生成成功',
content: new OA\JsonContent(properties: [
new OA\Property(property: 'code', type: 'integer', example: 0),
new OA\Property(property: 'data', properties: [
new OA\Property(property: 'generated_sku', type: 'string', example: 'C03_0032'),
new OA\Property(property: 'strategy_record', type: 'string', example: 'prefix:C03'),
new OA\Property(property: 'has_duplicate', type: 'boolean'),
new OA\Property(property: 'existing_mappings', type: 'array', items: new OA\Items(type: 'object')),
new OA\Property(property: 'message', type: 'string'),
], type: 'object'),
])
),
new OA\Response(response: 422, description: '参数校验失败', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
]
)]
#[RequestMapping(path: "generate-sku", methods: "POST")]
public function generateSku(): ResponseInterface|array
{
$data = $this->request->all();
$required = ['origin_sku_id', 'platform_id', 'strategy'];
foreach ($required as $field) {
if (!isset($data[$field]) || $data[$field] === '') {
return $this->response->json([
'code' => 422,
'message' => "缺少必填参数: {$field}",
])->withStatus(422);
}
}
$origin_sku_id = (int) $data['origin_sku_id'];
$platform_id = (int) $data['platform_id'];
$strategy = $data['strategy'];
$prefix = $data['prefix'] ?? '';
$random_length = (int) ($data['random_length'] ?? 4);
$manual_value = $data['manual_value'] ?? null;
// 查找 origin_sku 记录
$sku_origin = SkuOrigin::query()->find($origin_sku_id);
if (!$sku_origin) {
return $this->response->json([
'code' => 422,
'message' => '指定的内部 SKU 不存在',
])->withStatus(422);
}
// 生成 platform_outer_sku
$generated_sku = $this->skuService->generatePlatformOuterSku(
strategy: $strategy,
origin_sku: $sku_origin->sku,
prefix: $prefix,
random_length: $random_length,
manual_value: $manual_value,
);
$strategy_record = $this->skuService->buildStrategyRecord($strategy, $prefix, $random_length);
// 检查重复
$duplicate_check = $this->skuService->checkDuplicate($origin_sku_id, $platform_id);
return [
'code' => 0,
'message' => 'success',
'data' => [
'generated_sku' => $generated_sku,
'strategy_record' => $strategy_record,
'has_duplicate' => $duplicate_check['has_duplicate'],
'existing_mappings' => $duplicate_check['existing_mappings'],
'message' => $duplicate_check['message'],
],
];
}
/**
* 映射详情
*/
#[OA\Get(
path: '/sku-mappings/{id}',
summary: 'SKU 映射详情',
security: [['bearerAuth' => []]],
tags: ['SkuMappings'],
parameters: [
new OA\Parameter(name: 'id', in: 'path', required: true, schema: new OA\Schema(type: 'integer')),
],
responses: [
new OA\Response(
response: 200,
description: '获取成功',
content: new OA\JsonContent(properties: [
new OA\Property(property: 'code', type: 'integer', example: 0),
new OA\Property(property: 'message', type: 'string', example: '获取成功'),
new OA\Property(property: 'data', ref: '#/components/schemas/SkuMapping'),
])
),
new OA\Response(response: 404, description: '数据不存在', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
]
)]
#[RequestMapping(path: "{id}", methods: "GET")]
public function show(int $id): ResponseInterface|array
{
$query = SkuMapping::query();
$this->applyDataScope($query);
$record = $query->where('id', $id)->first();
if (!$record) {
return $this->response->json([
'code' => 404,
'message' => '数据不存在',
])->withStatus(404);
}
return [
'code' => 0,
'message' => '获取成功',
'data' => $record,
];
}
/**
* 创建映射
*/
#[OA\Post(
path: '/sku-mappings',
summary: '创建 SKU 映射',
security: [['bearerAuth' => []]],
tags: ['SkuMappings'],
requestBody: new OA\RequestBody(
required: true,
content: new OA\JsonContent(
required: ['company_id', 'platform_id', 'origin_sku_id', 'platform_outer_sku'],
properties: [
new OA\Property(property: 'company_id', type: 'integer', example: 3),
new OA\Property(property: 'platform_id', type: 'integer', example: 1),
new OA\Property(property: 'store_id', type: 'integer', nullable: true, example: 101),
new OA\Property(property: 'origin_sku_id', type: 'integer', example: 1),
new OA\Property(property: 'platform_outer_sku', type: 'string', example: 'C03_0032'),
new OA\Property(property: 'platform_product_id', type: 'string', example: 'ITEM-001'),
new OA\Property(property: 'generation_strategy', type: 'string', nullable: true, example: 'prefix:C03'),
new OA\Property(property: 'warehouse_id', type: 'integer', nullable: true),
new OA\Property(property: 'enabled', type: 'boolean', example: true),
new OA\Property(property: 'bundled', type: 'boolean', example: false, description: '是否为组合商品映射'),
new OA\Property(property: 'note', type: 'string', nullable: true),
]
)
),
responses: [
new OA\Response(
response: 201,
description: '创建成功',
content: new OA\JsonContent(properties: [
new OA\Property(property: 'code', type: 'integer', example: 0),
new OA\Property(property: 'message', type: 'string', example: '创建成功'),
new OA\Property(property: 'data', ref: '#/components/schemas/SkuMapping'),
])
),
new OA\Response(response: 422, description: '参数校验失败', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
]
)]
#[RequestMapping(path: "", methods: "POST")]
public function store(): ResponseInterface|array
{
$data = $this->request->all();
$required_fields = ['company_id', 'platform_id', 'origin_sku_id', 'platform_outer_sku'];
foreach ($required_fields as $field) {
if (!isset($data[$field]) || $data[$field] === '') {
return $this->response->json([
'code' => 422,
'message' => "缺少必填字段: {$field}",
])->withStatus(422);
}
}
$origin_sku_id = (int) $data['origin_sku_id'];
$platform_id = (int) $data['platform_id'];
$platform_outer_sku = (string) $data['platform_outer_sku'];
$origin_sku = $this->skuService->autoFillOriginSku($origin_sku_id);
if ($origin_sku === null) {
return $this->response->json([
'code' => 422,
'message' => '指定的 origin_sku_id 不存在',
])->withStatus(422);
}
if (!$this->skuService->isUniqueMapping($origin_sku_id, $platform_id, $platform_outer_sku)) {
return $this->response->json([
'code' => 422,
'message' => '该映射组合已存在(origin_sku_id + platform_id + platform_outer_sku',
])->withStatus(422);
}
$record = SkuMapping::query()->create([
'company_id' => (int) $data['company_id'],
'platform_id' => $platform_id,
'store_id' => isset($data['store_id']) ? (int) $data['store_id'] : null,
'origin_sku' => $origin_sku,
'origin_sku_id' => $origin_sku_id,
'platform_product_id' => $data['platform_product_id'] ?? null,
'platform_outer_sku' => $platform_outer_sku,
'generation_strategy' => $data['generation_strategy'] ?? null,
'warehouse_id' => isset($data['warehouse_id']) ? (int) $data['warehouse_id'] : null,
'enabled' => $data['enabled'] ?? true,
'bundled' => (bool) ($data['bundled'] ?? false),
'note' => $data['note'] ?? null,
]);
$user_id = OperationLogService::getCurrentUserId();
if ($user_id) {
OperationLogService::log(
$user_id,
'create',
'sku_mapping',
$record->id,
'创建 SKU 映射: ' . $origin_sku . ' → ' . $platform_outer_sku,
ip: $this->request->getHeaderLine('x-real-ip') ?: null,
);
}
return $this->response->json([
'code' => 0,
'message' => '创建成功',
'data' => $record,
])->withStatus(201);
}
/**
* 更新映射
*/
#[OA\Put(
path: '/sku-mappings/{id}',
summary: '更新 SKU 映射',
security: [['bearerAuth' => []]],
tags: ['SkuMappings'],
parameters: [
new OA\Parameter(name: 'id', in: 'path', required: true, schema: new OA\Schema(type: 'integer')),
],
requestBody: new OA\RequestBody(
required: true,
content: new OA\JsonContent(properties: [
new OA\Property(property: 'origin_sku_id', type: 'integer'),
new OA\Property(property: 'platform_id', type: 'integer'),
new OA\Property(property: 'platform_outer_sku', type: 'string'),
new OA\Property(property: 'platform_product_id', type: 'string'),
new OA\Property(property: 'generation_strategy', type: 'string', nullable: true),
new OA\Property(property: 'store_id', type: 'integer', nullable: true),
new OA\Property(property: 'warehouse_id', type: 'integer', nullable: true),
new OA\Property(property: 'enabled', type: 'boolean'),
new OA\Property(property: 'bundled', type: 'boolean', description: '是否为组合商品映射'),
new OA\Property(property: 'note', type: 'string', nullable: true),
])
),
responses: [
new OA\Response(
response: 200,
description: '更新成功',
content: new OA\JsonContent(properties: [
new OA\Property(property: 'code', type: 'integer', example: 0),
new OA\Property(property: 'message', type: 'string', example: '更新成功'),
new OA\Property(property: 'data', ref: '#/components/schemas/SkuMapping'),
])
),
new OA\Response(response: 404, description: '数据不存在', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
]
)]
#[RequestMapping(path: "{id}", methods: "PUT")]
public function update(int $id): ResponseInterface|array
{
$query = SkuMapping::query();
$this->applyDataScope($query);
$record = $query->where('id', $id)->first();
if (!$record) {
return $this->response->json([
'code' => 404,
'message' => '数据不存在',
])->withStatus(404);
}
$data = $this->request->all();
$allowed_fields = [
'origin_sku_id', 'platform_id', 'platform_product_id', 'platform_outer_sku',
'generation_strategy', 'store_id', 'warehouse_id', 'enabled', 'bundled', 'note',
];
$update_data = array_intersect_key($data, array_flip($allowed_fields));
$uniqueness_keys = ['origin_sku_id', 'platform_id', 'platform_outer_sku'];
if (array_intersect_key($update_data, array_flip($uniqueness_keys)) !== []) {
$next_origin_sku_id = (int) ($update_data['origin_sku_id'] ?? $record->origin_sku_id);
$next_platform_id = (int) ($update_data['platform_id'] ?? $record->platform_id);
$next_platform_outer_sku = (string) ($update_data['platform_outer_sku'] ?? $record->platform_outer_sku);
if (!$this->skuService->isUniqueMapping(
$next_origin_sku_id,
$next_platform_id,
$next_platform_outer_sku,
exclude_id: $record->id,
)) {
return $this->response->json([
'code' => 422,
'message' => '该映射组合已存在(origin_sku_id + platform_id + platform_outer_sku',
])->withStatus(422);
}
}
if (isset($update_data['origin_sku_id'])) {
$new_origin_sku = $this->skuService->autoFillOriginSku((int) $update_data['origin_sku_id']);
if ($new_origin_sku === null) {
return $this->response->json([
'code' => 422,
'message' => '指定的 origin_sku_id 不存在',
])->withStatus(422);
}
$update_data['origin_sku'] = $new_origin_sku;
}
$record->update($update_data);
$user_id = OperationLogService::getCurrentUserId();
if ($user_id) {
OperationLogService::log(
$user_id,
'update',
'sku_mapping',
$record->id,
"更新 SKU 映射 #{$record->id}",
detail: ['updated_fields' => array_keys($update_data)],
ip: $this->request->getHeaderLine('x-real-ip') ?: null,
);
}
return [
'code' => 0,
'message' => '更新成功',
'data' => $record->fresh(),
];
}
/**
* 删除映射
*/
#[OA\Delete(
path: '/sku-mappings/{id}',
summary: '删除 SKU 映射',
security: [['bearerAuth' => []]],
tags: ['SkuMappings'],
parameters: [
new OA\Parameter(name: 'id', in: 'path', required: true, schema: new OA\Schema(type: 'integer')),
],
responses: [
new OA\Response(
response: 200,
description: '删除成功',
content: new OA\JsonContent(properties: [
new OA\Property(property: 'code', type: 'integer', example: 0),
new OA\Property(property: 'message', type: 'string', example: '删除成功'),
])
),
new OA\Response(response: 404, description: '数据不存在', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')),
]
)]
#[RequestMapping(path: "{id}", methods: "DELETE")]
public function destroy(int $id): ResponseInterface|array
{
$query = SkuMapping::query();
$this->applyDataScope($query);
$record = $query->where('id', $id)->first();
if (!$record) {
return $this->response->json([
'code' => 404,
'message' => '数据不存在',
])->withStatus(404);
}
$record->delete();
$user_id = OperationLogService::getCurrentUserId();
if ($user_id) {
OperationLogService::log(
$user_id,
'delete',
'sku_mapping',
$id,
"删除 SKU 映射 #{$id}",
ip: $this->request->getHeaderLine('x-real-ip') ?: null,
);
}
return [
'code' => 0,
'message' => '删除成功',
];
}
/**
* DataScope 过滤
*/
private function applyDataScope(\Hyperf\Database\Model\Builder $query): void
{
$scope_type = $this->request->getAttribute('scope_type');
$scope_ids = $this->request->getAttribute('scope_ids', []);
if ($scope_type === 'store') {
$query->whereIn('store_id', $scope_ids);
} elseif ($scope_type === 'platform') {
$query->whereIn('platform_id', $scope_ids);
}
}
}