diff --git a/backend/app/Controller/Api/V1/DashboardController.php b/backend/app/Controller/Api/V1/DashboardController.php new file mode 100644 index 0000000..e3c915d --- /dev/null +++ b/backend/app/Controller/Api/V1/DashboardController.php @@ -0,0 +1,251 @@ + []]], + tags: ['Dashboard'], + 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/DashboardOverview'), + ]) + ), + 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: "overview", methods: "GET")] + public function overview(): ResponseInterface|array + { + $scope_type = $this->request->getAttribute('scope_type', 'all'); + $scope_ids = $this->request->getAttribute('scope_ids', []); + + $data = $this->statsService->getOverview($scope_type, $scope_ids); + + return [ + 'code' => 0, + 'message' => '获取成功', + 'data' => $data, + ]; + } + + /** + * 趋势数据 + */ + #[OA\Get( + path: '/api/v1/dashboard/trend', + summary: '获取趋势数据', + description: '返回指定时间范围内按日/周/月聚合的成功和失败同步趋势。', + security: [['bearerAuth' => []]], + tags: ['Dashboard'], + parameters: [ + new OA\Parameter(name: 'from', in: 'query', required: false, description: '起始日期(默认30天前)', schema: new OA\Schema(type: 'string', format: 'date', example: '2026-02-15')), + new OA\Parameter(name: 'to', in: 'query', required: false, description: '结束日期(默认今天)', schema: new OA\Schema(type: 'string', format: 'date', example: '2026-03-17')), + new OA\Parameter(name: 'group_by', in: 'query', required: false, description: '聚合粒度', schema: new OA\Schema(type: 'string', enum: ['day', 'week', 'month'], default: 'day')), + new OA\Parameter(name: 'data_type', in: 'query', required: false, description: '数据类型筛选', schema: new OA\Schema(type: 'string', enum: ['order', 'product', 'refund', 'inventory'])), + ], + 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', type: 'array', items: new OA\Items(ref: '#/components/schemas/DashboardTrendItem')), + ]) + ), + new OA\Response(response: 400, description: '参数错误', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')), + 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: "trend", methods: "GET")] + public function trend(): ResponseInterface|array + { + $from = $this->request->input('from', date('Y-m-d', strtotime('-30 days'))); + $to = $this->request->input('to', date('Y-m-d')); + $group_by = $this->request->input('group_by', 'day'); + $data_type = $this->request->input('data_type'); + + // 校验 group_by + if (!in_array($group_by, DashboardStatsService::VALID_GROUP_BY, true)) { + return $this->response->json([ + 'code' => 400, + 'message' => "无效的 group_by 参数: {$group_by},可选值: " . implode(', ', DashboardStatsService::VALID_GROUP_BY), + 'data' => null, + ])->withStatus(400); + } + + // 校验 data_type + if ($data_type !== null && !in_array($data_type, DashboardStatsService::VALID_DATA_TYPES, true)) { + return $this->response->json([ + 'code' => 400, + 'message' => "无效的 data_type 参数: {$data_type},可选值: " . implode(', ', DashboardStatsService::VALID_DATA_TYPES), + 'data' => null, + ])->withStatus(400); + } + + // 校验日期格式 + if (!$this->isValidDate($from) || !$this->isValidDate($to)) { + return $this->response->json([ + 'code' => 400, + 'message' => '无效的日期格式,请使用 YYYY-MM-DD', + 'data' => null, + ])->withStatus(400); + } + + // 校验 from <= to + if ($from > $to) { + return $this->response->json([ + 'code' => 400, + 'message' => 'from 日期不能晚于 to 日期', + 'data' => null, + ])->withStatus(400); + } + + $scope_type = $this->request->getAttribute('scope_type', 'all'); + $scope_ids = $this->request->getAttribute('scope_ids', []); + + $data = $this->statsService->getTrend($from, $to, $group_by, $data_type, $scope_type, $scope_ids); + + return [ + 'code' => 0, + 'message' => '获取成功', + 'data' => $data, + ]; + } + + /** + * 分组统计 + */ + #[OA\Get( + path: '/api/v1/dashboard/breakdown', + summary: '获取分组统计', + description: '按公司/平台/店铺维度统计成功和失败同步数。', + security: [['bearerAuth' => []]], + tags: ['Dashboard'], + parameters: [ + new OA\Parameter(name: 'dimension', in: 'query', required: true, description: '分组维度', schema: new OA\Schema(type: 'string', enum: ['company', 'platform', 'store'])), + new OA\Parameter(name: 'from', in: 'query', required: false, description: '起始日期(默认今天)', schema: new OA\Schema(type: 'string', format: 'date', example: '2026-03-17')), + new OA\Parameter(name: 'to', in: 'query', required: false, description: '结束日期(默认今天)', schema: new OA\Schema(type: 'string', format: 'date', example: '2026-03-17')), + new OA\Parameter(name: 'data_type', in: 'query', required: false, description: '数据类型筛选', schema: new OA\Schema(type: 'string', enum: ['order', 'product', 'refund', 'inventory'])), + ], + 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', type: 'array', items: new OA\Items(ref: '#/components/schemas/DashboardBreakdownItem')), + ]) + ), + new OA\Response(response: 400, description: '参数错误', content: new OA\JsonContent(ref: '#/components/schemas/ErrorResponse')), + 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: "breakdown", methods: "GET")] + public function breakdown(): ResponseInterface|array + { + $dimension = $this->request->input('dimension'); + + // 校验 dimension 必填 + if ($dimension === null || $dimension === '') { + return $this->response->json([ + 'code' => 400, + 'message' => '缺少必填参数: dimension', + 'data' => null, + ])->withStatus(400); + } + + // 校验 dimension 枚举 + if (!in_array($dimension, DashboardStatsService::VALID_DIMENSIONS, true)) { + return $this->response->json([ + 'code' => 400, + 'message' => "无效的 dimension 参数: {$dimension},可选值: " . implode(', ', DashboardStatsService::VALID_DIMENSIONS), + 'data' => null, + ])->withStatus(400); + } + + $from = $this->request->input('from', date('Y-m-d')); + $to = $this->request->input('to', date('Y-m-d')); + $data_type = $this->request->input('data_type'); + + // 校验 data_type + if ($data_type !== null && !in_array($data_type, DashboardStatsService::VALID_DATA_TYPES, true)) { + return $this->response->json([ + 'code' => 400, + 'message' => "无效的 data_type 参数: {$data_type},可选值: " . implode(', ', DashboardStatsService::VALID_DATA_TYPES), + 'data' => null, + ])->withStatus(400); + } + + // 校验日期格式 + if (!$this->isValidDate($from) || !$this->isValidDate($to)) { + return $this->response->json([ + 'code' => 400, + 'message' => '无效的日期格式,请使用 YYYY-MM-DD', + 'data' => null, + ])->withStatus(400); + } + + $scope_type = $this->request->getAttribute('scope_type', 'all'); + $scope_ids = $this->request->getAttribute('scope_ids', []); + + $data = $this->statsService->getBreakdown($dimension, $from, $to, $data_type, $scope_type, $scope_ids); + + return [ + 'code' => 0, + 'message' => '获取成功', + 'data' => $data, + ]; + } + + /** + * 校验日期格式 YYYY-MM-DD + */ + private function isValidDate(string $date): bool + { + $d = \DateTime::createFromFormat('Y-m-d', $date); + return $d !== false && $d->format('Y-m-d') === $date; + } +}