Files
store_ai_front/BACKEND_API_DOCUMENTATION.md
2026-05-11 10:05:33 +08:00

2071 lines
96 KiB
Markdown
Raw Permalink 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.
# Backend API 文档
生成日期2026-05-08
生成依据:`backend/routes/api.php``php artisan route:list --path=api``app/Http/Controllers/Api/**``app/Services/**`
本文档是当前 backend API 的唯一维护入口。`backend/docs` 下的旧 API 文档已移除,后续接口变更统一更新本文件。
本文档只覆盖当前 `backend` 暴露的 API 路由,共 43 个路由入口。Laravel 自动支持的 `HEAD` 不单独列出。
## 通用约定
### Base URL
生产或本地部署时以实际后端域名为准,下文统一写作:
```text
{BACKEND_API_BASE_URL}/api
```
本地默认通常是:
```text
http://127.0.0.1:8000/api
```
### 请求头
| 请求头 | 适用接口 | 说明 |
|---------------------------------------------------|--------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `Content-Type: application/json` | 有 JSON body 的接口 | 请求体使用 JSON。 |
| `Authorization: Bearer <sanctum_token>` | Dashboard / 当前用户接口 | Laravel Sanctum Web 用户 token。邮箱已验证账号可由 `/api/auth/login` 返回;邮箱未验证账号必须先通过 `/api/auth/email/verify-code`,验证码校验成功后才返回 token重置密码仅在账号邮箱已验证时返回 token。Dashboard 路由会拒绝扩展 token。 |
| `Authorization: Bearer <extension_sanctum_token>` | Chrome 扩展接口 | 扩展专用 Sanctum token`/api/auth/extension-pair``/api/auth/refresh-extension-token` 返回;后端会校验 `storeai_extension` token name 和 `extension:*` ability。扩展 token 只能访问扩展接口和 `/api/auth/logout`。 |
| `X-Ext-Version: <version>` | 扩展扫描相关接口 | 用于扩展版本门禁,低于 `MIN_EXT_VERSION` 会返回 `ext_outdated`。 |
| `Authorization: Bearer <CRON_SECRET>` | Cron 接口 | 配置 `CRON_SECRET` 后必填;生产环境未配置会返回 `misconfigured`。 |
| `Stripe-Signature: <signature>` | Stripe webhook | Stripe webhook 原始 body 签名。 |
| `X-Telegram-Bot-Api-Secret-Token: <secret>` | Telegram webhook | Telegram webhook secret token。 |
### 响应格式
所有 backend API 统一返回三元素 envelope。前端已确定重写不再保留旧 `{ ok: ... }` 顶层兼容格式。
```json
{
"code": 1,
"message": "ok",
"data": {}
}
```
失败响应的顶层 `code` 放整型 status旧接口字符串错误码统一放入 `data.apiCode`;异常响应不改变实际 HTTP status codeHTTP
层保持 200
```json
{
"code": 400,
"message": "Validation failed.",
"data": {
"apiCode": "invalid_payload"
}
}
```
部分错误会附带额外字段:
```json
{
"code": 400,
"message": "schema validation failed",
"data": {
"apiCode": "invalid_payload",
"details": {
"fieldErrors": {
"payload.today.orders": ["must be an integer"]
}
}
}
}
```
空业务数据返回空数组或空对象,取决于控制器业务返回值;客户端只应依赖外层 `code/message/data`
```json
{
"code": 1,
"message": "ok",
"data": {}
}
```
### 通用错误
下表的 `响应 code` 指 JSON 响应体里的 `code` 字段,不是 HTTP status code异常响应的 HTTP status 保持 200。
| 响应 code | data.apiCode | 说明 |
|--------:|-------------------------------------------------------------------------|-----------------------------|
| 400 | `invalid_payload` / `invalid_email` / `invalid_code` | 请求参数格式错误。 |
| 401 | `unauthorized` | 缺少 token、token 无效、签名错误或未登录。 |
| 402 | `subscription_required` | 当前订阅状态不允许扫描。 |
| 404 | `not_found` | 资源不存在、无权访问或 debug 门禁关闭。 |
| 409 | `platform_account_mismatch` / `subscription_active` / `agreement_stale` | 业务状态冲突。 |
| 426 | `ext_outdated` | Chrome 扩展版本低于后端要求。 |
| 429 | `too_many_requests` / `rate_limited` | 请求过于频繁或手动扫描达到日限额。 |
| 500 | `server_error` / `config_error` / `stripe_error` / `handler_error` | 服务端错误、配置缺失或第三方调用失败。 |
| 503 | `misconfigured` | 生产环境缺少必需配置。 |
## 公共对象
### `User`
| 字段 | 类型 | 说明 |
|-------------------|--------------|------------------|
| `id` | string | Laravel 用户 UUID。 |
| `email` | string | 邮箱。 |
| `name` | string\|null | 用户名,可为空。 |
| `emailVerifiedAt` | string\|null | 邮箱验证时间ISO 字符串。 |
### `ApiUser`
| 字段 | 类型 | 说明 |
|---------|--------------|--------------|
| `id` | string | 当前登录用户 UUID。 |
| `email` | string\|null | 当前登录用户邮箱。 |
### `BrandCore`
`BrandCore` 是 Dashboard 读接口返回的品牌基础对象。不同接口会按页面需要裁剪或追加字段。
| 字段 | 类型 | 说明 |
|--------------------------|---------------|-------------------------------------------------|
| `id` | string | 品牌 UUID。 |
| `name` | string | 品牌名称。 |
| `timezone` | string\|null | 品牌时区,例如 `Asia/Kuala_Lumpur`。 |
| `morning_brief_hour` | integer\|null | 早报本地小时0-23。 |
| `evening_recap_hour` | integer\|null | 晚报本地小时0-23。 |
| `last_morning_brief_at` | string\|null | 最近早报发送时间。 |
| `last_evening_recap_at` | string\|null | 最近晚报发送时间。 |
| `subscription_status` | string\|null | 订阅状态,例如 `trial``active``past_due``canceled`。 |
| `trial_ends_at` | string\|null | 试用结束时间。 |
| `subscription_cancel_at` | string\|null | 订阅预约取消时间。 |
| `is_comp` | boolean | 是否内部赠送账号。 |
| `telegram_chat_id` | string\|null | Telegram chat id。 |
| `platform_account_id` | string\|null | 已绑定 Shopee seller id。 |
| `created_at` | string\|null | 创建时间。 |
| `last_extension_version` | string\|null | 最近扩展心跳版本。 |
| `stores` | array | 店铺列表,元素为 `{ id: string }`。 |
#### `subscription_status` 枚举
`subscription_status``brands` 表保存的原始账单状态;前端判断“是否可用 / 是否到期”时还要结合 `is_comp``trial_ends_at`
`subscription_cancel_at`
| 值 | 含义 | 到期时间口径 |
|--------------|------------------------------------------------|---------------------------------------------------------------------------------------|
| `trial` | 试用中。创建首个品牌时后端写入 `trial_ends_at = now + 1 day`。 | 使用 `trial_ends_at` 判断试用到期;为空时按试用可用但无倒计时时间处理。 |
| `active` | Stripe 订阅有效。 | `subscription_cancel_at` 为空表示当前无已知到期时间;若为未来时间,表示已预约取消,付费权益到 `subscription_cancel_at`。 |
| `past_due` | Stripe 付款异常或 `unpaid`。 | 当前 API 不给确定到期时间Stripe 后续重试失败并发送 deleted webhook 后会变为 `canceled`。 |
| `canceled` | 订阅已取消或 Stripe `incomplete_expired`。 | 已到期,不再使用未来到期时间;后端会清空 `subscription_cancel_at`。 |
| `null` / 其它值 | 旧数据或异常数据。 | 按无有效订阅处理。 |
有效订阅状态的派生规则:
| 条件 | 派生状态 | 扫描权限 |
|-----------------------------------------------------------------------|--------------------|------------------|
| `is_comp = true` | `comp` | 允许,内部赠送账号优先级最高。 |
| `subscription_status = trial``trial_ends_at` 为空或晚于当前 UTC 时间 | `trial_active` | 允许。 |
| `subscription_status = trial``trial_ends_at` 已过期或无法解析 | `trial_expired` | 不允许。 |
| `subscription_status = active``subscription_cancel_at` 为空 | `active` | 允许。 |
| `subscription_status = active``subscription_cancel_at` 晚于当前 UTC 时间 | `active_canceling` | 允许,展示“已预约取消”。 |
| `subscription_status = active``subscription_cancel_at` 已过期或无法解析 | `canceled` | 不允许。 |
| `subscription_status = past_due` | `past_due` | 允许,但应提示用户更新付款方式。 |
| `subscription_status = canceled` | `canceled` | 不允许。 |
如果前端要展示“订阅到期时间”,优先按以下顺序取值:`is_comp=true` 时无到期时间;`trial``trial_ends_at``active` 且已预约取消时取
`subscription_cancel_at`;普通 `active` 当前没有到期时间字段,因为后端暂未保存 Stripe `current_period_end`
### `ScanRow`
| 字段 | 类型 | 说明 |
|-----------------------|---------------|----------------------------|
| `id` | string | scan UUID。 |
| `scanned_at` | string | 扫描时间。 |
| `status` | string | `ok``partial``failed`。 |
| `error` | string\|null | 扩展提取错误文本。 |
| `payload` | object\|null | 扫描原始 payload。 |
| `telegram_message_id` | integer\|null | 手动扫描 Telegram 回声消息 id。 |
### `AlertRow`
| 字段 | 类型 | 说明 |
|----------------|---------|---------------------|
| `id` | string | alert UUID。 |
| `rule_id` | string | 规则 ID。 |
| `priority` | string | 告警优先级,例如 `P1``P2`。 |
| `confidence` | string | 置信度。 |
| `title` | string | 标题。 |
| `diagnosis` | string | 诊断文案。 |
| `action` | string | 建议动作。 |
| `data` | object | 告警附加 JSON。 |
| `acknowledged` | boolean | 是否已确认。 |
| `created_at` | string | 创建时间。 |
### `StoreBrief`
| 字段 | 类型 | 说明 |
|-------------------------|--------------|-----------------|
| `verdict_tier` | string\|null | AI 今日结论等级。 |
| `verdict_headline` | string\|null | AI 今日结论标题。 |
| `verdict_factors` | array | AI 结论因素。 |
| `verdict_action` | string\|null | AI 建议行动。 |
| `bottleneck_stage` | string\|null | 增长瓶颈阶段。 |
| `growth_diagnosis_json` | object\|null | AI 增长诊断 JSON。 |
| `top_actions_json` | array\|null | AI Top Actions。 |
| `confidence` | object | AI 置信度 JSON。 |
## 系统接口
### GET `/api/health`
鉴权:无。
入参:无。
请求示例:
```bash
curl -X GET "{BACKEND_API_BASE_URL}/api/health"
```
响应参数:
| 字段 | 类型 | 说明 |
|--------------------|--------------|--------------------------|
| `code` | integer | 成功为 `1`。 |
| `message` | string | 成功为 `ok`。 |
| `data.service` | string | 固定为 `storeai-api`。 |
| `data.version` | string\|null | `config('app.version')`。 |
| `data.environment` | string | 当前 Laravel 环境。 |
## 认证接口
### POST `/api/auth/register`
鉴权:无。
入参:
| 位置 | 字段 | 类型 | 必填 | 说明 |
|------|------------|--------------|----|-----------------------|
| body | `email` | string | 是 | 合法邮箱,服务端会转小写并 trim。 |
| body | `password` | string | 是 | 8-128 字符。 |
| body | `name` | string\|null | 否 | 最多 120 字符,空字符串按 null。 |
请求示例:
```bash
curl -X POST "{BACKEND_API_BASE_URL}/api/auth/register" \
-H "Content-Type: application/json" \
-d '{
"email": "owner@example.test",
"password": "secure-password",
"name": "Store Owner"
}'
```
响应参数:
| 字段 | 类型 | 说明 |
|----------------------------------|---------|----------------------------|
| `code` | integer | 成功为 `1`。 |
| `message` | string | 成功为 `ok`。 |
| `data.requiresEmailVerification` | boolean | 固定为 `true`,表示必须先完成邮箱验证码校验。 |
| `data.email` | string | 标准化后的邮箱。 |
| `data.user` | `User` | 新建用户。 |
| `data.sent` | boolean | 是否已发送邮箱验证码。 |
| `data.expiresInSeconds` | integer | 验证码剩余有效秒数。 |
错误:
| 响应 code | data.apiCode | 说明 |
|--------:|-------------------|------------|
| 400 | `invalid_payload` | 邮箱或密码格式错误。 |
| 400 | `duplicate_email` | 邮箱已注册。 |
备注:注册成功后会发送 6 位邮箱验证码,但不会签发登录 token只有 `/api/auth/email/verify-code` 校验成功后才会签发 Web
Sanctum token。`users.id` 同时作为业务 `account_id` 使用。
### POST `/api/auth/login`
鉴权:无。
入参:
| 位置 | 字段 | 类型 | 必填 | 说明 |
|------|------------|--------|----|-----------|
| body | `email` | string | 是 | 合法邮箱。 |
| body | `password` | string | 是 | 8-128 字符。 |
请求示例:
```bash
curl -X POST "{BACKEND_API_BASE_URL}/api/auth/login" \
-H "Content-Type: application/json" \
-d '{
"email": "owner@example.test",
"password": "secure-password"
}'
```
响应参数:
| 字段 | 类型 | 说明 |
|----------------------------------|---------|---------------------------------------------|
| `code` | integer | 成功为 `1`。 |
| `message` | string | 成功为 `ok`。 |
| `data.user` | `User` | 登录用户。邮箱未验证时也会返回,`emailVerifiedAt``null`。 |
| `data.token` | string | 邮箱已验证时返回的新签发 Sanctum Bearer token。 |
| `data.requiresEmailVerification` | boolean | 邮箱未验证时返回 `true`。 |
| `data.email` | string | 邮箱未验证时返回的标准化邮箱。 |
| `data.sent` | boolean | 邮箱未验证时表示是否已发送新的验证码。 |
| `data.expiresInSeconds` | integer | 新验证码剩余有效秒数。 |
| `data.retryAfterSeconds` | integer | 验证码重发限流时返回。 |
错误:
| 响应 code | data.apiCode | 说明 |
|--------:|-----------------------|----------|
| 400 | `invalid_payload` | 入参格式错误。 |
| 401 | `invalid_credentials` | 邮箱或密码错误。 |
备注:邮箱已验证时,登录成功会先删除当前账号所有旧 Sanctum token包括旧 Web token 和扩展 token再签发新的 Web
token扩展需要重新配对。邮箱未验证时不会返回 token会清理该账号旧 token 并返回 `requiresEmailVerification=true`
,前端应进入邮箱验证码流程。
### GET `/api/auth/me`
鉴权Sanctum。仅 Web/Dashboard token 可调用,扩展 token 会被 `dashboard.token` 门禁拒绝。
入参:无。
请求示例:
```bash
curl -X GET "{BACKEND_API_BASE_URL}/api/auth/me" \
-H "Authorization: Bearer <sanctum_token>"
```
响应参数:
| 字段 | 类型 | 说明 |
|-------------|--------------|----------------|
| `code` | integer | 成功为 `1`。 |
| `message` | string | 成功为 `ok`。 |
| `data.user` | `User`\|null | 当前 token 对应用户。 |
### POST `/api/auth/logout`
鉴权Sanctum。
入参:无。
请求示例:
```bash
curl -X POST "{BACKEND_API_BASE_URL}/api/auth/logout" \
-H "Authorization: Bearer <sanctum_token>"
```
响应参数:
| 字段 | 类型 | 说明 |
|-----------|---------|-----------|
| `code` | integer | 成功为 `1`。 |
| `message` | string | 成功为 `ok`。 |
备注:退出登录会删除当前账号所有 Sanctum token包括 Web token 和扩展 token让 Dashboard 和 Chrome 扩展同时下线。
### POST `/api/auth/rotate-session`
鉴权Sanctum。
入参:无。
请求示例:
```bash
curl -X POST "{BACKEND_API_BASE_URL}/api/auth/rotate-session" \
-H "Authorization: Bearer <sanctum_token>"
```
响应参数:
| 字段 | 类型 | 说明 |
|----------------|---------|--------------------------------------------------------|
| `code` | integer | 成功为 `1`。 |
| `message` | string | 成功为 `ok`。 |
| `data.rotated` | boolean | 固定为 `false`。 |
| `data.reason` | string | 固定为 `sanctum_bearer_tokens_are_already_device_scoped`。 |
备注:兼容 Origin 的 Supabase session rotate 调用Sanctum 下是 no-op。
### PATCH `/api/auth/password`
鉴权Sanctum。
入参:
| 位置 | 字段 | 类型 | 必填 | 说明 |
|------|------------|--------|----|---------------|
| body | `password` | string | 是 | 新密码8-128 字符。 |
请求示例:
```bash
curl -X PATCH "{BACKEND_API_BASE_URL}/api/auth/password" \
-H "Authorization: Bearer <sanctum_token>" \
-H "Content-Type: application/json" \
-d '{
"password": "new-secure-password"
}'
```
响应参数:
| 字段 | 类型 | 说明 |
|-----------|---------|-----------|
| `code` | integer | 成功为 `1`。 |
| `message` | string | 成功为 `ok`。 |
错误:
| 响应 code | data.apiCode | 说明 |
|--------:|-------------------|----------|
| 400 | `invalid_payload` | 密码长度不合法。 |
### GET `/api/auth/check-verified`
鉴权:无。
入参:
| 位置 | 字段 | 类型 | 必填 | 说明 |
|-------|---------|--------|----|-------------|
| query | `email` | string | 是 | 要查询验证状态的邮箱。 |
请求示例:
```bash
curl -X GET "{BACKEND_API_BASE_URL}/api/auth/check-verified?email=owner%40example.test"
```
响应参数:
| 字段 | 类型 | 说明 |
|-----------------|---------|---------------------------------|
| `code` | integer | 成功为 `1`。 |
| `message` | string | 成功为 `ok`。 |
| `data.verified` | boolean | `users.email_verified_at` 是否非空。 |
错误:
| 响应 code | data.apiCode | 说明 |
|--------:|-----------------|---------|
| 400 | `invalid_email` | 邮箱格式错误。 |
备注:响应头包含 `cache-control: no-store`
### POST `/api/auth/email/verification-code`
鉴权:无。
入参:
| 位置 | 字段 | 类型 | 必填 | 说明 |
|------|---------|--------|----|------------|
| body | `email` | string | 是 | 要发送验证码的邮箱。 |
请求示例:
```bash
curl -X POST "{BACKEND_API_BASE_URL}/api/auth/email/verification-code" \
-H "Content-Type: application/json" \
-d '{
"email": "owner@example.test"
}'
```
响应参数:
| 字段 | 类型 | 说明 |
|-------------------------|---------|---------------------------------|
| `code` | integer | 成功为 `1`。 |
| `message` | string | 成功为 `ok`。 |
| `data.sent` | boolean | 是否触发发送。未知邮箱也返回 `true` 以防账号枚举。 |
| `data.alreadyVerified` | boolean | 已验证邮箱返回 `true`,此时 `sent=false`。 |
| `data.expiresInSeconds` | integer | 验证码剩余有效秒数,实际发送时返回。 |
错误:
| 响应 code | data.apiCode | 说明 |
|--------:|---------------------|---------------------------------|
| 400 | `invalid_email` | 邮箱格式错误。 |
| 429 | `too_many_requests` | 发送过于频繁,响应含 `retryAfterSeconds`。 |
### POST `/api/auth/email/verify-code`
鉴权:无。
入参:
| 位置 | 字段 | 类型 | 必填 | 说明 |
|------|---------|--------|----|-----------|
| body | `email` | string | 是 | 邮箱。 |
| body | `code` | string | 是 | 6 位数字验证码。 |
请求示例:
```bash
curl -X POST "{BACKEND_API_BASE_URL}/api/auth/email/verify-code" \
-H "Content-Type: application/json" \
-d '{
"email": "owner@example.test",
"code": "123456"
}'
```
响应参数:
| 字段 | 类型 | 说明 |
|-----------------|---------|---------------------------|
| `code` | integer | 成功为 `1`。 |
| `message` | string | 成功为 `ok`。 |
| `data.verified` | boolean | 成功为 `true`。 |
| `data.user` | `User` | 已验证用户。 |
| `data.token` | string | 新签发 Sanctum Bearer token。 |
错误:
| 响应 code | data.apiCode | 说明 |
|--------:|-----------------|-----------------------------|
| 400 | `invalid_email` | 邮箱格式错误。 |
| 400 | `invalid_code` | 验证码格式错误、邮箱不存在、验证码不存在或验证码错误。 |
| 400 | `expired_code` | 验证码已过期。 |
备注:校验成功等价于一次 Web 登录,会删除当前账号所有旧 Web/扩展 token并签发新 Web token。
### POST `/api/auth/password/forgot`
鉴权:无。
入参:
| 位置 | 字段 | 类型 | 必填 | 说明 |
|------|---------|--------|----|-----------|
| body | `email` | string | 是 | 要重置密码的邮箱。 |
请求示例:
```bash
curl -X POST "{BACKEND_API_BASE_URL}/api/auth/password/forgot" \
-H "Content-Type: application/json" \
-d '{
"email": "owner@example.test"
}'
```
响应参数:
| 字段 | 类型 | 说明 |
|-------------------------|---------|-----------------------------|
| `code` | integer | 成功为 `1`。 |
| `message` | string | 成功为 `ok`。 |
| `data.sent` | boolean | 固定按成功处理;未知邮箱不发信但仍返回 `true`。 |
| `data.expiresInSeconds` | integer | 验证码有效秒数,实际发信时返回。 |
错误:
| 响应 code | data.apiCode | 说明 |
|--------:|---------------------|---------------------------------|
| 400 | `invalid_email` | 邮箱格式错误。 |
| 429 | `too_many_requests` | 发送过于频繁,响应含 `retryAfterSeconds`。 |
### POST `/api/auth/password/reset`
鉴权:无。
入参:
| 位置 | 字段 | 类型 | 必填 | 说明 |
|------|------------|--------|----|---------------|
| body | `email` | string | 是 | 邮箱。 |
| body | `code` | string | 是 | 6 位数字验证码。 |
| body | `password` | string | 是 | 新密码8-128 字符。 |
请求示例:
```bash
curl -X POST "{BACKEND_API_BASE_URL}/api/auth/password/reset" \
-H "Content-Type: application/json" \
-d '{
"email": "owner@example.test",
"code": "123456",
"password": "new-secure-password"
}'
```
响应参数:
| 字段 | 类型 | 说明 |
|----------------------------------|---------|------------------------------------|
| `code` | integer | 成功为 `1`。 |
| `message` | string | 成功为 `ok`。 |
| `data.reset` | boolean | 成功为 `true`。 |
| `data.user` | `User` | 重置密码后的用户。 |
| `data.token` | string | 邮箱已验证时返回的新签发 Sanctum Bearer token。 |
| `data.requiresEmailVerification` | boolean | 账号邮箱未验证时返回 `true`,此时不返回 `token`。 |
| `data.email` | string | 账号邮箱未验证时返回的标准化邮箱。 |
| `data.sent` | boolean | 账号邮箱未验证时表示是否已发送邮箱验证验证码。 |
| `data.expiresInSeconds` | integer | 邮箱验证验证码剩余有效秒数。 |
| `data.retryAfterSeconds` | integer | 邮箱验证验证码重发限流时返回。 |
错误:
| 响应 code | data.apiCode | 说明 |
|--------:|-------------------|-----------------------------|
| 400 | `invalid_email` | 邮箱格式错误。 |
| 400 | `invalid_code` | 验证码格式错误、邮箱不存在、验证码不存在或验证码错误。 |
| 400 | `expired_code` | 验证码已过期。 |
| 400 | `invalid_payload` | 新密码不符合长度要求。 |
备注:成功后会消费该邮箱所有未使用重置码。若账号邮箱已验证,会删除当前账号所有旧 Web/扩展 token并签发新 Web
token若账号邮箱未验证只更新密码、清理旧 token、发送邮箱验证验证码并返回 `requiresEmailVerification=true`
### POST `/api/auth/extension-pair`
鉴权Sanctum。
入参:
| 位置 | 字段 | 类型 | 必填 | 说明 |
|------|---------|--------|----|---------------------|
| body | `label` | string | 否 | 扩展设备标签,最多保留 120 字符。 |
请求示例:
```bash
curl -X POST "{BACKEND_API_BASE_URL}/api/auth/extension-pair" \
-H "Authorization: Bearer <sanctum_token>" \
-H "Content-Type: application/json" \
-d '{
"label": "Chrome - Store Owner Laptop"
}'
```
响应参数:
| 字段 | 类型 | 说明 |
|-----------------------------------|--------------|---------------------------------------------------------------|
| `code` | integer | 成功为 `1`。 |
| `message` | string | 成功为 `ok`。 |
| `data.token` | string | 扩展专用 Sanctum plaintext token格式为 Sanctum 标准 `<id>\|<secret>`。 |
| `data.expiresAt` | string | 扩展 token 过期时间。 |
| `data.userEmail` | string\|null | 当前用户邮箱。 |
| `data.apiBaseUrl` | string | 后续扩展请求使用的 API base URL。 |
| `data.priorTokensRevoked` | integer | 本次撤销的旧扩展 token 数量。 |
| `data.priorTokensFromOtherDevice` | boolean | 是否踢掉了其他设备 token。 |
备注:扩展 token 明文只返回一次,数据库写入 `personal_access_tokens``name``storeai_extension`
`storeai_extension:<label>`abilities 只包含扩展权限;新配对会删除该账号所有旧扩展 token保持扩展侧单设备策略但不会删除当前
Web token。普通 Dashboard Sanctum token 不能调用扩展接口。
### POST `/api/auth/refresh-extension-token`
鉴权:扩展机器 token。
必需 header`Authorization: Bearer <extension_sanctum_token>`
入参:无。
请求示例:
```bash
curl -X POST "{BACKEND_API_BASE_URL}/api/auth/refresh-extension-token" \
-H "Authorization: Bearer <extension_sanctum_token>"
```
响应参数:
| 字段 | 类型 | 说明 |
|------------------|---------|----------------------------------------------------------------|
| `code` | integer | 成功为 `1`。 |
| `message` | string | 成功为 `ok`。 |
| `data.token` | string | 新扩展专用 Sanctum plaintext token格式为 Sanctum 标准 `<id>\|<secret>`。 |
| `data.expiresAt` | string | 新 token 过期时间。 |
错误:
| 响应 code | data.apiCode | 说明 |
|--------:|----------------|-----------------------------------------|
| 401 | `unauthorized` | 缺少、格式错误、已删除、已过期或缺少对应扩展 ability 的 token。 |
备注:刷新成功会先创建新扩展 token再删除同账号其它扩展 token删除范围只限扩展 token不删除 Web token。
## Dashboard 与 Onboarding
### GET `/api/dashboard/shell`
鉴权Sanctum。
入参:无。
请求示例:
```bash
curl -X GET "{BACKEND_API_BASE_URL}/api/dashboard/shell" \
-H "Authorization: Bearer <sanctum_token>"
```
响应参数:
| 字段 | 类型 | 说明 |
|-------------------------------------|--------------|-------------------------------------------|
| `code` | integer | 成功为 `1`。 |
| `message` | string | 成功为 `ok`。 |
| `data.user` | `ApiUser` | 当前用户。 |
| `data.brand` | object\|null | 当前账号第一品牌的 shell 信息。 |
| `data.brand.id` | string | 品牌 UUID。 |
| `data.brand.name` | string | 品牌名。 |
| `data.brand.subscription_status` | string | 订阅状态,枚举见 `BrandCore.subscription_status`。 |
| `data.brand.trial_ends_at` | string\|null | 试用结束时间。 |
| `data.brand.subscription_cancel_at` | string\|null | 预约取消时间。 |
| `data.brand.is_comp` | boolean | 是否内部赠送账号。 |
### GET `/api/dashboard`
鉴权Sanctum。
入参:无。
请求示例:
```bash
curl -X GET "{BACKEND_API_BASE_URL}/api/dashboard" \
-H "Authorization: Bearer <sanctum_token>"
```
响应参数:
| 字段 | 类型 | 说明 |
|----------------------------|--------------------|------------------------------------------------|
| `code` | integer | 成功为 `1`。 |
| `message` | string | 成功为 `ok`。 |
| `data.user` | `ApiUser` | 当前用户。 |
| `data.brand` | `BrandCore`\|null | 当前账号第一品牌。无品牌时只返回 `user``brand:null`。 |
| `data.latestScan` | `ScanRow`\|null | 最近一次扫描。 |
| `data.previousScan` | `ScanRow`\|null | 最近一次可对比的历史成功扫描。 |
| `data.scanDots` | array | 最近 12 次扫描轻量列表,元素 `{ id, scanned_at, status }`。 |
| `data.alerts` | `AlertRow[]` | 最近扫描下的告警列表。 |
| `data.competitorMeta` | array | 竞品元数据,元素 `{ product_url, label }`。 |
| `data.storeBrief` | `StoreBrief`\|null | 最近扫描 AI brief。 |
| `data.dismissedKeys` | string[] | 当前品牌本地日已 Mark done 的 action key。 |
| `data.skuMatrixEntries` | array\|null | 当日 SKU Matrix 条目,来自 `sku_matrix.matrix_json`。 |
| `data.skuMatrixConfidence` | string\|null | SKU Matrix 置信度。 |
| `data.gmvRows` | array | 最近 40 条成功 scan 的 `{ scanned_at, payload }`。 |
### GET `/api/dashboard/settings`
鉴权Sanctum。
入参:无。
请求示例:
```bash
curl -X GET "{BACKEND_API_BASE_URL}/api/dashboard/settings" \
-H "Authorization: Bearer <sanctum_token>"
```
响应参数:
| 字段 | 类型 | 说明 |
|------------------------|-------------------|------------------------------------------------------------------|
| `code` | integer | 成功为 `1`。 |
| `message` | string | 成功为 `ok`。 |
| `data.user` | `ApiUser` | 当前用户。 |
| `data.brand` | `BrandCore`\|null | 设置页品牌信息;附加 `thresholds``platform_account_id_bound_at``stores`。 |
| `data.manualUsedToday` | integer | 品牌本地日内已使用手动扫描次数。 |
| `data.lastAutoScanAt` | string\|null | 最近一次 scheduled scan 时间。 |
| `data.competitors` | array | 竞品列表,元素 `{ id, product_url, label, created_at }`。 |
### GET `/api/dashboard/billing`
鉴权Sanctum。
入参:无。
请求示例:
```bash
curl -X GET "{BACKEND_API_BASE_URL}/api/dashboard/billing" \
-H "Authorization: Bearer <sanctum_token>"
```
响应参数:
| 字段 | 类型 | 说明 |
|----------------|-------------------|-----------------------------------------------|
| `code` | integer | 成功为 `1`。 |
| `message` | string | 成功为 `ok`。 |
| `data.user` | `ApiUser` | 当前用户。 |
| `data.brand` | `BrandCore`\|null | 账单页品牌信息,含 Stripe customer/subscription id 字段。 |
| `data.billing` | null | 当前固定为 null保留给前端兼容。 |
### GET `/api/onboarding/status`
鉴权Sanctum。
入参:无。
请求示例:
```bash
curl -X GET "{BACKEND_API_BASE_URL}/api/onboarding/status" \
-H "Authorization: Bearer <sanctum_token>"
```
响应参数:
| 字段 | 类型 | 说明 |
|-------------------|-------------------|------------------------------------|
| `code` | integer | 成功为 `1`。 |
| `message` | string | 成功为 `ok`。 |
| `data.user` | `ApiUser` | 当前用户。 |
| `data.brand` | `BrandCore`\|null | Onboarding 所需品牌信息。 |
| `data.lastScanAt` | string\|null | 当前店铺最近一次 `ok``partial` scan 时间。 |
## 当前用户与扩展接口
### POST `/api/me/brand`
鉴权Sanctum。
入参:
| 位置 | 字段 | 类型 | 必填 | 说明 |
|------|------------|--------|----|----------------------|
| body | `name` | string | 是 | 品牌名称trim 后 1-80 字符。 |
| body | `timezone` | string | 是 | PHP 支持的时区名,最长 64 字符。 |
请求示例:
```bash
curl -X POST "{BACKEND_API_BASE_URL}/api/me/brand" \
-H "Authorization: Bearer <sanctum_token>" \
-H "Content-Type: application/json" \
-d '{
"name": "StoreAI Demo Brand",
"timezone": "Asia/Kuala_Lumpur"
}'
```
响应参数:
| 字段 | 类型 | 说明 |
|-----------------------|---------|---------------|
| `code` | integer | 成功为 `1`。 |
| `message` | string | 成功为 `ok`。 |
| `data.brand.id` | string | 新品牌 UUID。 |
| `data.brand.name` | string | 品牌名称。 |
| `data.brand.timezone` | string | 品牌时区。 |
| `data.store.id` | string | 同步创建的店铺 UUID。 |
错误:
| 响应 code | data.apiCode | 说明 |
|--------:|-------------------|-------------|
| 400 | `invalid_payload` | 品牌名或时区格式错误。 |
| 400 | `brand_exists` | 当前账号已有品牌。 |
### PATCH `/api/me/brand`
鉴权Sanctum。
入参:
| 位置 | 字段 | 类型 | 必填 | 说明 |
|------|------------|--------|----|----------------------|
| body | `name` | string | 否 | 品牌名称trim 后 1-80 字符。 |
| body | `timezone` | string | 否 | PHP 支持的时区名,最长 64 字符。 |
请求示例:
```bash
curl -X PATCH "{BACKEND_API_BASE_URL}/api/me/brand" \
-H "Authorization: Bearer <sanctum_token>" \
-H "Content-Type: application/json" \
-d '{
"name": "StoreAI Demo Brand",
"timezone": "Asia/Kuala_Lumpur"
}'
```
响应参数:
| 字段 | 类型 | 说明 |
|--------------|---------|----------------------------------|
| `code` | integer | 成功为 `1`。 |
| `message` | string | 成功为 `ok`。 |
| `data.brand` | object | 有变更时返回 `{ id, name, timezone }`。 |
| `data.noop` | boolean | 没有可更新字段时返回 `true`。 |
错误:
| 响应 code | data.apiCode | 说明 |
|--------:|-------------------|----------------------|
| 400 | `invalid_payload` | 品牌名或时区格式错误。 |
| 500 | `server_error` | 当前账号没有可更新品牌或更新后读取失败。 |
### DELETE `/api/me/account`
鉴权Sanctum。
入参:无。
请求示例:
```bash
curl -X DELETE "{BACKEND_API_BASE_URL}/api/me/account" \
-H "Authorization: Bearer <sanctum_token>"
```
响应参数:
| 字段 | 类型 | 说明 |
|-----------|---------|-----------|
| `code` | integer | 成功为 `1`。 |
| `message` | string | 成功为 `ok`。 |
错误:
| 响应 code | data.apiCode | 说明 |
|--------:|-----------------------|-----------------------------------------|
| 409 | `subscription_active` | 品牌仍有 active 或 past_due 订阅,必须先取消或等待周期结束。 |
### POST `/api/me/action/dismiss`
鉴权Sanctum。
入参:
| 位置 | 字段 | 类型 | 必填 | 说明 |
|------|--------------|--------|----|--------------------------|
| body | `action_key` | string | 是 | Top Action key1-200 字符。 |
请求示例:
```bash
curl -X POST "{BACKEND_API_BASE_URL}/api/me/action/dismiss" \
-H "Authorization: Bearer <sanctum_token>" \
-H "Content-Type: application/json" \
-d '{
"action_key": "sku:SKU-001:fix_conversion"
}'
```
响应参数:
| 字段 | 类型 | 说明 |
|-----------|---------|-----------|
| `code` | integer | 成功为 `1`。 |
| `message` | string | 成功为 `ok`。 |
错误:
| 响应 code | data.apiCode | 说明 |
|--------:|-------------------|-----------------------|
| 400 | `no_brand` | 当前账号没有品牌。 |
| 400 | `invalid_payload` | `action_key` 缺失或长度非法。 |
备注:`dismiss_date` 使用品牌本地日开始时间转 UTC 后的日期键,与 Dashboard 和定时报表过滤逻辑一致。
### GET `/api/me/competitors`
鉴权:扩展机器 token。
必需 header`Authorization: Bearer <extension_sanctum_token>``X-Ext-Version`
入参:无。
请求示例:
```bash
curl -X GET "{BACKEND_API_BASE_URL}/api/me/competitors" \
-H "Authorization: Bearer <extension_sanctum_token>" \
-H "X-Ext-Version: 0.1.0"
```
响应参数:
| 字段 | 类型 | 说明 |
|----------------------------------|--------------|----------------|
| `code` | integer | 成功为 `1`。 |
| `message` | string | 成功为 `ok`。 |
| `data.competitors` | array | 当前账号所有品牌已登记竞品。 |
| `data.competitors[].id` | string | 竞品 UUID。 |
| `data.competitors[].brand_id` | string | 品牌 UUID。 |
| `data.competitors[].product_url` | string | Shopee 商品 URL。 |
| `data.competitors[].label` | string\|null | 竞品备注。 |
错误:
| 响应 code | data.apiCode | 说明 |
|--------:|----------------|-----------------|
| 401 | `unauthorized` | 扩展 token 缺失或无效。 |
| 426 | `ext_outdated` | 扩展版本过旧。 |
### GET `/api/me/scan-quota`
鉴权:扩展机器 token。
必需 header`Authorization: Bearer <extension_sanctum_token>``X-Ext-Version`
入参:无。
请求示例:
```bash
curl -X GET "{BACKEND_API_BASE_URL}/api/me/scan-quota" \
-H "Authorization: Bearer <extension_sanctum_token>" \
-H "X-Ext-Version: 0.1.0"
```
响应参数:
| 字段 | 类型 | 说明 |
|-------------------------------|--------------|---------------------------------------------------------|
| `code` | integer | 成功为 `1`。 |
| `message` | string | 成功为 `ok`。 |
| `data.allowed` | boolean | 是否允许手动扫描。 |
| `data.reason` | string | 不允许时返回,例如 `subscription_required``manual_cap_reached`。 |
| `message` | string | 不允许时的用户提示。 |
| `data.manualUsed` | integer | 品牌本地日已使用手动扫描次数。 |
| `data.manualCap` | integer | 每日手动扫描上限,默认 3。 |
| `data.remaining` | integer | 剩余手动扫描次数。 |
| `data.boundPlatformAccountId` | string\|null | 品牌已绑定 Shopee seller id。 |
| `data.subState` | string | 计算后的订阅状态。 |
| `data.trialEndsAt` | string\|null | 试用结束时间。 |
| `data.latestExtVersion` | string | 后端配置的最新扩展版本。 |
| `data.extDownloadUrl` | string | 扩展下载地址。 |
错误:
| 响应 code | data.apiCode | 说明 |
|--------:|----------------|------------------|
| 401 | `unauthorized` | 扩展 token 缺失或无效。 |
| 404 | `not_found` | 当前账号没有品牌或品牌没有店铺。 |
| 426 | `ext_outdated` | 扩展版本过旧。 |
### POST `/api/extension/heartbeat`
鉴权:扩展机器 token。
必需 header`Authorization: Bearer <extension_sanctum_token>`
入参:
| 位置 | 字段 | 类型 | 必填 | 说明 |
|------|---------------|--------------|----|-------------------------|
| body | `skip_reason` | string\|null | 是 | 扩展本轮跳过扫描原因,可为 null。 |
| body | `ext_version` | string | 是 | 扩展版本1-40 字符。 |
| body | `tick_at` | string | 是 | 客户端心跳时间,可被解析为 datetime。 |
请求示例:
```bash
curl -X POST "{BACKEND_API_BASE_URL}/api/extension/heartbeat" \
-H "Authorization: Bearer <extension_sanctum_token>" \
-H "Content-Type: application/json" \
-d '{
"skip_reason": "Shopee tab not open",
"ext_version": "0.1.0",
"tick_at": "2026-05-08T10:15:00Z"
}'
```
响应参数:
| 字段 | 类型 | 说明 |
|-----------|---------|----------------------------|
| `code` | integer | 成功为 `1`。 |
| `message` | string | 成功为 `ok`。 |
| `code` | string | 更新失败时可能返回 `update_failed`。 |
错误:
| 响应 code | data.apiCode | 说明 |
|--------:|-------------------|--------------------|
| 400 | `invalid_payload` | 请求体格式错误。 |
| 401 | `unauthorized` | 扩展 token 缺失或无效。 |
| 404 | `not_found` | 当前 token 所属账号没有品牌。 |
## 品牌设置接口
### GET `/api/brands/{id}/competitors`
鉴权Sanctum。
入参:
| 位置 | 字段 | 类型 | 必填 | 说明 |
|------|------|--------|----|----------|
| path | `id` | string | 是 | 品牌 UUID。 |
请求示例:
```bash
curl -X GET "{BACKEND_API_BASE_URL}/api/brands/00000000-0000-4000-8000-000000000101/competitors" \
-H "Authorization: Bearer <sanctum_token>"
```
响应参数:
| 字段 | 类型 | 说明 |
|----------------------------------|--------------|-----------------------------|
| `code` | integer | 成功为 `1`。 |
| `message` | string | 成功为 `ok`。 |
| `data.competitors` | array | 竞品列表。无权访问品牌时返回空数组,不泄露品牌存在性。 |
| `data.competitors[].id` | string | 竞品 UUID。 |
| `data.competitors[].product_url` | string | Shopee 商品 URL。 |
| `data.competitors[].label` | string\|null | 竞品备注。 |
| `data.competitors[].created_at` | string\|null | 创建时间。 |
错误:
| 响应 code | data.apiCode | 说明 |
|--------:|-------------------|----------------|
| 400 | `invalid_payload` | 品牌 id 不是 UUID。 |
### POST `/api/brands/{id}/competitors`
鉴权Sanctum。
入参:
| 位置 | 字段 | 类型 | 必填 | 说明 |
|------|--------------|--------------|----|---------------------------------------------|
| path | `id` | string | 是 | 品牌 UUID。 |
| body | `productUrl` | string | 是 | Shopee 商品页 URL必须匹配 `-i.<shopId>.<itemId>`。 |
| body | `label` | string\|null | 否 | 竞品备注,最多 80 字符。 |
请求示例:
```bash
curl -X POST "{BACKEND_API_BASE_URL}/api/brands/00000000-0000-4000-8000-000000000101/competitors" \
-H "Authorization: Bearer <sanctum_token>" \
-H "Content-Type: application/json" \
-d '{
"productUrl": "https://shopee.com.my/example-product-i.123456.987654321",
"label": "Main competitor"
}'
```
响应参数:
| 字段 | 类型 | 说明 |
|-------------------------------|--------------|----------------|
| `code` | integer | 成功为 `1`。 |
| `message` | string | 成功为 `ok`。 |
| `data.competitor.id` | string | 新竞品 UUID。 |
| `data.competitor.product_url` | string | Shopee 商品 URL。 |
| `data.competitor.label` | string\|null | 竞品备注。 |
| `data.competitor.created_at` | string\|null | 创建时间。 |
错误:
| 响应 code | data.apiCode | 说明 |
|--------:|-------------------|-----------------|
| 400 | `invalid_payload` | URL 或备注格式错误。 |
| 400 | `duplicate` | 已登记相同 URL。 |
| 400 | `over_limit` | 超过每品牌 10 个竞品上限。 |
| 404 | `not_found` | 品牌不存在或不属于当前用户。 |
### DELETE `/api/brands/{id}/competitors/{competitorId}`
鉴权Sanctum。
入参:
| 位置 | 字段 | 类型 | 必填 | 说明 |
|------|----------------|--------|----|----------|
| path | `id` | string | 是 | 品牌 UUID。 |
| path | `competitorId` | string | 是 | 竞品 UUID。 |
请求示例:
```bash
curl -X DELETE "{BACKEND_API_BASE_URL}/api/brands/00000000-0000-4000-8000-000000000101/competitors/00000000-0000-4000-8000-000000000201" \
-H "Authorization: Bearer <sanctum_token>"
```
响应参数:
| 字段 | 类型 | 说明 |
|-----------|---------|-----------|
| `code` | integer | 成功为 `1`。 |
| `message` | string | 成功为 `ok`。 |
错误:
| 响应 code | data.apiCode | 说明 |
|--------:|-------------------|-----------------|
| 400 | `invalid_payload` | path UUID 格式错误。 |
备注:删除是幂等的,行不存在或无权删除也返回成功。
### PATCH `/api/brands/{id}/notifications`
鉴权Sanctum。
入参:
| 位置 | 字段 | 类型 | 必填 | 说明 |
|------|--------------------|---------|----|-------------------------------------------------|
| path | `id` | string | 是 | 品牌 UUID。 |
| body | `morningBriefHour` | integer | 否 | 早报本地小时0-23。 |
| body | `eveningRecapHour` | integer | 否 | 晚报本地小时0-23。 |
| body | `pushMode` | string | 否 | 兼容旧字段,只接受 `daily_digest``instant_alert`,不写库。 |
| body | `dailyReportHour` | integer | 否 | 兼容旧字段,仅校验,不写库。 |
| body | `silentStartHour` | integer | 否 | 兼容旧字段,仅校验,不写库。 |
| body | `silentEndHour` | integer | 否 | 兼容旧字段,仅校验,不写库。 |
请求示例:
```bash
curl -X PATCH "{BACKEND_API_BASE_URL}/api/brands/00000000-0000-4000-8000-000000000101/notifications" \
-H "Authorization: Bearer <sanctum_token>" \
-H "Content-Type: application/json" \
-d '{
"morningBriefHour": 9,
"eveningRecapHour": 18,
"pushMode": "daily_digest",
"dailyReportHour": 9,
"silentStartHour": 22,
"silentEndHour": 8
}'
```
响应参数:
| 字段 | 类型 | 说明 |
|---------------------------|---------|--------------------|
| `code` | integer | 成功为 `1`。 |
| `message` | string | 成功为 `ok`。 |
| `data.morning_brief_hour` | integer | 更新后的早报小时,有实际更新时返回。 |
| `data.evening_recap_hour` | integer | 更新后的晚报小时,有实际更新时返回。 |
| `data.noop` | boolean | 没有可写字段时返回 `true`。 |
错误:
| 响应 code | data.apiCode | 说明 |
|--------:|-------------------|----------------------------------|
| 400 | `invalid_payload` | 小时值不是整数、范围不在 0-23或同一请求内早晚报小时相同。 |
| 404 | `not_found` | 品牌不存在或不属于当前用户。 |
### PATCH `/api/brands/{id}/telegram`
鉴权Sanctum。
入参:
| 位置 | 字段 | 类型 | 必填 | 说明 |
|------|----------|--------------|----|---------------------------------------------------|
| path | `id` | string | 是 | 品牌 UUID。 |
| body | `chatId` | string\|null | 是 | Telegram chat id必须匹配 `^-?\d{1,20}$`;传 null 表示解绑。 |
请求示例:
```bash
curl -X PATCH "{BACKEND_API_BASE_URL}/api/brands/00000000-0000-4000-8000-000000000101/telegram" \
-H "Authorization: Bearer <sanctum_token>" \
-H "Content-Type: application/json" \
-d '{
"chatId": "-1001234567890"
}'
```
响应参数:
| 字段 | 类型 | 说明 |
|-----------------------|--------------|------------------------|
| `code` | integer | 成功为 `1`。 |
| `message` | string | 成功为 `ok`。 |
| `data.telegramChatId` | string\|null | 更新后的 Telegram chat id。 |
错误:
| 响应 code | data.apiCode | 说明 |
|--------:|-------------------|-------------------|
| 400 | `invalid_payload` | `chatId` 缺失或格式错误。 |
| 404 | `not_found` | 品牌不存在或不属于当前用户。 |
### POST `/api/brands/{id}/telegram/pair`
鉴权Sanctum。
入参:
| 位置 | 字段 | 类型 | 必填 | 说明 |
|------|------|--------|----|----------|
| path | `id` | string | 是 | 品牌 UUID。 |
请求示例:
```bash
curl -X POST "{BACKEND_API_BASE_URL}/api/brands/00000000-0000-4000-8000-000000000101/telegram/pair" \
-H "Authorization: Bearer <sanctum_token>"
```
响应参数:
| 字段 | 类型 | 说明 |
|-------------------|---------|-------------------------------------------------|
| `code` | integer | 成功为 `1`。 |
| `message` | string | 成功为 `ok`。 |
| `data.token` | string | 一次性配对 token格式 `pair_<36hex>`。 |
| `data.expiresAt` | string | 过期时间。 |
| `data.deeplink` | string | Telegram `https://t.me/<bot>?start=<token>` 链接。 |
| `data.ttlSeconds` | integer | 固定 900 秒。 |
错误:
| 响应 code | data.apiCode | 说明 |
|--------:|-----------------|---------------------------|
| 404 | `not_found` | 品牌不存在或不属于当前用户。 |
| 500 | `misconfigured` | 缺少 `TELEGRAM_BOT_HANDLE`。 |
### PATCH `/api/brands/{id}/thresholds`
鉴权Sanctum。
入参:
| 位置 | 字段 | 类型 | 必填 | 说明 |
|------|--------------|--------|----|-----------------------------------|
| path | `id` | string | 是 | 品牌 UUID。 |
| body | `thresholds` | object | 是 | 阈值覆盖对象;未知 key 会忽略;值为 null 表示移除覆盖。 |
请求示例:
```bash
curl -X PATCH "{BACKEND_API_BASE_URL}/api/brands/00000000-0000-4000-8000-000000000101/thresholds" \
-H "Authorization: Bearer <sanctum_token>" \
-H "Content-Type: application/json" \
-d '{
"thresholds": {
"SALES_01_GMV_DECLINE": 0.25,
"SALES_01_MIN_ORDERS": 5,
"SALES_02_ZERO_HOURS": 3,
"SALES_04_DAYS_LEFT": 4,
"SALES_05_RATING_MAX": 3,
"AD_01_MIN_SPEND": 50,
"AD_02_KEYWORD_SHARE": 0.5,
"AD_03_ROAS_MIN": 2.5,
"COMP_01_GAP": 0.12,
"COMP_02_DROP": null
}
}'
```
允许的 `thresholds` key
| key | 类型 | 范围 | 说明 |
|------------------------|---------|----------|-----------|
| `SALES_01_GMV_DECLINE` | number | 0.1-0.5 | GMV 下降阈值。 |
| `SALES_01_MIN_ORDERS` | integer | 3-20 | 最小订单数。 |
| `SALES_02_ZERO_HOURS` | integer | 1-6 | 连续零单小时数。 |
| `SALES_04_DAYS_LEFT` | integer | 1-7 | 库存剩余天数。 |
| `SALES_05_RATING_MAX` | integer | 1-4 | 低评分上限。 |
| `AD_01_MIN_SPEND` | integer | 10-200 | 广告最低花费。 |
| `AD_02_KEYWORD_SHARE` | number | 0.3-0.8 | 关键词花费占比。 |
| `AD_03_ROAS_MIN` | number | 1.5-5.0 | 最低 ROAS。 |
| `COMP_01_GAP` | number | 0.05-0.3 | 竞品价差阈值。 |
| `COMP_02_DROP` | number | 0.05-0.3 | 竞品降价阈值。 |
响应参数:
| 字段 | 类型 | 说明 |
|-------------------|---------|-----------------|
| `code` | integer | 成功为 `1`。 |
| `message` | string | 成功为 `ok`。 |
| `data.thresholds` | object | 合并保存后的品牌阈值覆盖对象。 |
错误:
| 响应 code | data.apiCode | 说明 |
|--------:|-------------------|-----------------------------------------------|
| 400 | `invalid_payload` | `thresholds` 不是对象、值不是数字/null、整数 key 传小数或超出范围。 |
| 404 | `not_found` | 品牌不存在或不属于当前用户。 |
## 告警接口
### POST `/api/alerts/{id}/ack`
鉴权Sanctum。
入参:
| 位置 | 字段 | 类型 | 必填 | 说明 |
|------|-------|---------|----|------------------|
| path | `id` | string | 是 | Alert UUID。 |
| body | `ack` | boolean | 否 | 是否确认;缺省为 `true`。 |
请求示例:
```bash
curl -X POST "{BACKEND_API_BASE_URL}/api/alerts/00000000-0000-4000-8000-000000000301/ack" \
-H "Authorization: Bearer <sanctum_token>" \
-H "Content-Type: application/json" \
-d '{
"ack": true
}'
```
响应参数:
| 字段 | 类型 | 说明 |
|---------------------|---------|-------------|
| `code` | integer | 成功为 `1`。 |
| `message` | string | 成功为 `ok`。 |
| `data.id` | string | Alert UUID。 |
| `data.acknowledged` | boolean | 更新后的确认状态。 |
错误:
| 响应 code | data.apiCode | 说明 |
|--------:|-------------------|----------------------|
| 400 | `invalid_payload` | Alert id 不是 UUID。 |
| 404 | `not_found` | Alert 不存在或不属于当前用户品牌。 |
## 扫描上传接口
### POST `/api/ingest/scan`
鉴权:扩展机器 token。
必需 header`Authorization: Bearer <extension_sanctum_token>``X-Ext-Version`
顶层入参:
| 位置 | 字段 | 类型 | 必填 | 说明 |
|------|---------------------|--------------|----|---------------------------------------------|
| body | `brandId` | string | 是 | 品牌 UUID。 |
| body | `storeId` | string | 是 | 店铺 UUID。 |
| body | `scannedAt` | string | 是 | 扫描时间,可解析为 datetime。 |
| body | `payload` | object | 是 | 扫描业务数据,见下方 schema。 |
| body | `extractorStatus` | string | 是 | `ok``partial``failed`。 |
| body | `extractorErrors` | string[] | 否 | 提取错误列表,最多 50 项,缺省 `[]`。 |
| body | `trigger` | string | 否 | `manual``scheduled`,缺省 `manual`。 |
| body | `platformAccountId` | string\|null | 否 | Shopee seller id1-64 字符4 位以上纯数字才会参与绑定/比对。 |
| body | `fieldMeta` | object | 否 | 字段采集来源元数据,缺省 `{}`。 |
`payload.today`
| 字段 | 类型 | 必填 | 说明 |
|---------------------------------------|---------------|----|---------------|
| `orders` | integer | 是 | 今日订单数,>=0。 |
| `gmv_cents` | integer | 是 | 今日 GMV 分,>=0。 |
| `cancel_rate` | number | 是 | 取消率0-1。 |
| `return_rate` | number | 是 | 退货率0-1。 |
| `gmv_delta_yesterday_pct` | number\|null | 否 | GMV 较昨日变化比例。 |
| `gmv_net_cents` | integer\|null | 否 | 净 GMV 分,>=0。 |
| `conversion_rate` | number\|null | 否 | 转化率。 |
| `visitors` | integer\|null | 否 | 访客数,>=0。 |
| `product_clicks` | integer\|null | 否 | 商品点击数,>=0。 |
| `aov_cents` | integer\|null | 否 | 客单价分,>=0。 |
| `orders_delta_yesterday_pct` | number\|null | 否 | 订单较昨日变化比例。 |
| `conversion_rate_delta_yesterday_pct` | number\|null | 否 | 转化率较昨日变化比例。 |
| `visitors_delta_yesterday_pct` | number\|null | 否 | 访客较昨日变化比例。 |
| `product_clicks_delta_yesterday_pct` | number\|null | 否 | 商品点击较昨日变化比例。 |
| `aov_delta_yesterday_pct` | number\|null | 否 | 客单价较昨日变化比例。 |
`payload.skus[]`
| 字段 | 类型 | 必填 | 说明 |
|--------------------|---------------|----|----------------------------------------|
| `sku_id` | string | 是 | SKU id。 |
| `name` | string | 是 | SKU 名称。 |
| `price_cents` | integer\|null | 否 | 价格分,>=0。 |
| `l30d_sales` | integer\|null | 否 | 近 30 天销量,>=0。 |
| `l30d_impressions` | integer\|null | 否 | 近 30 天曝光,>=0。 |
| `impressions` | integer | 是 | 曝光,>=0。 |
| `clicks` | integer | 是 | 点击,>=0。 |
| `add_to_cart` | integer | 是 | 加购,>=0。 |
| `orders` | integer | 是 | 订单,>=0。 |
| `gmv_cents` | integer | 是 | GMV 分,>=0。 |
| `stock` | integer | 是 | 库存,>=0。 |
| `status` | string | 是 | `on_sale``unlisted``out_of_stock`。 |
`payload.ads[]`
| 字段 | 类型 | 必填 | 说明 |
|--------------------------|---------------|----|----------------------------------------------|
| `campaign_id` | string | 是 | 广告活动 id。 |
| `campaign_name` | string | 是 | 广告活动名称。 |
| `type` | string | 否 | 广告类型,缺省 `unknown`。 |
| `state` | string | 否 | `ongoing``paused``unknown`,缺省 `unknown`。 |
| `spend_cents` | integer | 是 | 花费分,>=0。 |
| `clicks` | integer | 是 | 点击,>=0。 |
| `orders` | integer | 是 | 订单,>=0。 |
| `revenue_cents` | integer | 是 | 收入分,>=0。 |
| `impressions` | integer | 否 | 曝光,>=0缺省 0。 |
| `roas` | number | 否 | ROAS>=0缺省 0。 |
| `target_roas` | number\|null | 否 | 目标 ROAS>=0。 |
| `daily_budget_cents` | integer\|null | 否 | 日预算分,>=0。 |
| `keywords` | array | 是 | 关键词列表。 |
| `keywords[].keyword` | string | 是 | 关键词。 |
| `keywords[].spend_cents` | integer | 是 | 关键词花费分,>=0。 |
| `keywords[].clicks` | integer | 是 | 关键词点击,>=0。 |
| `keywords[].orders` | integer | 是 | 关键词订单,>=0。 |
其他 `payload` 字段:
| 字段 | 类型 | 必填 | 说明 |
|----------------------|--------------|----|-----------------------------------------------------------------------------------------------------|
| `store_id` | string | 是 | 店铺 UUID。 |
| `scanned_at` | string | 是 | 扫描时间。 |
| `recent_3h[]` | array | 是 | 元素 `{ hour:string, orders:integer>=0, gmv_cents:integer>=0 }`。 |
| `reviews[]` | array | 是 | 元素 `{ review_id, sku_id:null\|string, rating:1-5, text, replied:boolean, created_at }`。 |
| `competitors[]` | array | 是 | 元素 `{ product_url:url, price_cents:integer>=0, rating:null\|number 0-5, sold_label:null\|string }`。 |
| `review_summary` | object\|null | 否 | 包含 `shop_rating``total_reviews_l30d``unanswered_negative``rating_histogram`。 |
| `product_aggregates` | object\|null | 否 | 数值字典,值可为 number 或 null。 |
| `traffic_sources` | array\|null | 否 | 流量来源数组,含 source、sales、clicks、conversion 等字段。 |
| `shop_health` | object\|null | 否 | 店铺健康对象;传 null 表示本次未采集,传 object 时内部字段见下表。 |
`payload.shop_health`
| 字段 | 类型 | 必填 | 说明 |
|---------------------------------|---------------|----|----------------------------------------------------|
| `rating_overall` | string\|null | 是 | 店铺健康评级,允许 `excellent``good``fair``poor` 或 null。 |
| `penalty_points` | integer\|null | 是 | 当前罚分,>=0未知传 null。 |
| `penalty_threshold` | integer\|null | 是 | 触发风险提示的罚分阈值,>=1未知传 null。 |
| `metrics` | object | 是 | 健康指标字典key 为指标名value 见下方 `metrics.*`。 |
| `metrics.*.value` | number\|null | 是 | 指标当前值。 |
| `metrics.*.target` | number\|null | 是 | 指标目标值。 |
| `metrics.*.target_op` | string\|null | 是 | 达标比较符,允许 `lt``lte``gt``gte` 或 null。 |
| `metrics.*.counts_toward_score` | boolean | 是 | 是否参与店铺健康评分。 |
`fieldMeta` 元素:
| 字段 | 类型 | 必填 | 说明 |
|-------------------|--------|----|-----------------------------------------------------------------|
| `source` | string | 是 | `fetch_hook``dom``json_ld``og_meta``aggregate``missing`。 |
| `selectorMatch` | string | 否 | `primary``fallback``text_anchor`。 |
| `shopeeDataStamp` | string | 否 | Shopee 数据戳。 |
| `ageMinutes` | number | 否 | 数据年龄分钟数。 |
请求示例:
```bash
curl -X POST "{BACKEND_API_BASE_URL}/api/ingest/scan" \
-H "Authorization: Bearer <extension_sanctum_token>" \
-H "X-Ext-Version: 0.1.0" \
-H "Content-Type: application/json" \
-d '{
"brandId": "00000000-0000-4000-8000-000000000101",
"storeId": "00000000-0000-4000-8000-000000000102",
"scannedAt": "2026-05-08T10:15:00Z",
"extractorStatus": "ok",
"extractorErrors": [],
"trigger": "manual",
"platformAccountId": "1234567890",
"fieldMeta": {
"today.orders": {
"source": "fetch_hook",
"selectorMatch": "primary",
"shopeeDataStamp": "seller-center-v1",
"ageMinutes": 2
},
"ads[0].spend_cents": {
"source": "dom",
"selectorMatch": "fallback",
"ageMinutes": 5
}
},
"payload": {
"store_id": "00000000-0000-4000-8000-000000000102",
"scanned_at": "2026-05-08T10:15:00Z",
"today": {
"orders": 18,
"gmv_cents": 258000,
"cancel_rate": 0.02,
"return_rate": 0.01,
"gmv_delta_yesterday_pct": -0.18,
"gmv_net_cents": 252000,
"conversion_rate": 0.042,
"visitors": 1200,
"product_clicks": 260,
"aov_cents": 14333,
"orders_delta_yesterday_pct": -0.12,
"conversion_rate_delta_yesterday_pct": -0.08,
"visitors_delta_yesterday_pct": 0.06,
"product_clicks_delta_yesterday_pct": -0.03,
"aov_delta_yesterday_pct": -0.04
},
"recent_3h": [
{ "hour": "2026-05-08T08:00:00Z", "orders": 3, "gmv_cents": 42000 },
{ "hour": "2026-05-08T09:00:00Z", "orders": 0, "gmv_cents": 0 },
{ "hour": "2026-05-08T10:00:00Z", "orders": 1, "gmv_cents": 12000 }
],
"skus": [
{
"sku_id": "SKU-001",
"name": "Hydrating Serum 30ml",
"price_cents": 6900,
"l30d_sales": 180,
"l30d_impressions": 12000,
"impressions": 2800,
"clicks": 210,
"add_to_cart": 36,
"orders": 12,
"gmv_cents": 82800,
"stock": 24,
"status": "on_sale"
}
],
"ads": [
{
"campaign_id": "AD-001",
"campaign_name": "Hero SKU Search Ads",
"type": "search",
"state": "ongoing",
"spend_cents": 4200,
"clicks": 96,
"orders": 5,
"revenue_cents": 34500,
"impressions": 3600,
"roas": 8.21,
"target_roas": 5,
"daily_budget_cents": 10000,
"keywords": [
{ "keyword": "serum", "spend_cents": 1600, "clicks": 36, "orders": 1 },
{ "keyword": "hydrating serum", "spend_cents": 2600, "clicks": 60, "orders": 4 }
]
}
],
"reviews": [
{
"review_id": "REV-001",
"sku_id": "SKU-001",
"rating": 2,
"text": "Bottle leaked during shipping",
"replied": false,
"created_at": "2026-05-08T04:00:00Z"
}
],
"competitors": [
{
"product_url": "https://shopee.com.my/example-product-i.123456.987654321",
"price_cents": 6200,
"rating": 4.8,
"sold_label": "1.2k sold"
}
],
"review_summary": {
"shop_rating": 4.7,
"total_reviews_l30d": 42,
"unanswered_negative": 3,
"rating_histogram": { "1": 1, "2": 2, "3": 4, "4": 12, "5": 23 }
},
"product_aggregates": {
"avg_price_cents": 7200,
"active_sku_count": 18,
"out_of_stock_count": 2
},
"traffic_sources": [
{
"source": "search",
"sales": 12,
"clicks": 180,
"conversion_rate": 0.066,
"gmv_cents": 168000
}
],
"shop_health": {
"rating_overall": "good",
"penalty_points": 0,
"penalty_threshold": 3,
"metrics": {
"late_shipment_rate": {
"value": 0.01,
"target": 0.03,
"target_op": "lte",
"counts_toward_score": true
},
"chat_response_rate": {
"value": 0.92,
"target": 0.85,
"target_op": "gte",
"counts_toward_score": true
}
}
}
}
}'
```
响应参数:
| 字段 | 类型 | 说明 |
|----------------------|---------|---------------------|
| `code` | integer | 成功为 `1`。 |
| `message` | string | 成功为 `ok`。 |
| `data.scanId` | string | 新 scan UUID。 |
| `data.alertsEmitted` | integer | 本次规则引擎产生的 alert 数量。 |
错误:
| 响应 code | data.apiCode | 说明 |
|--------:|-----------------------------|-------------------------------------------|
| 400 | `invalid_payload` | JSON 非法或 schema 校验失败。 |
| 401 | `unauthorized` | 扩展 token 缺失或无效。 |
| 402 | `subscription_required` | 当前订阅状态不允许扫描。 |
| 403 | `brand_mismatch` | 品牌不属于 token 账号,或店铺不属于品牌。 |
| 409 | `platform_account_mismatch` | 当前 Shopee seller id 与品牌已绑定 seller id 不一致。 |
| 426 | `ext_outdated` | 扩展版本过旧。 |
| 429 | `rate_limited` | 手动扫描达到每日上限。 |
备注:成功后会写入 scans、SKU/ad/competitor snapshots执行规则告警和 AI 派生数据生成;手动扫描会尝试发送 Telegram
回声scheduled 扫描会尝试补发 catch-up brief。
## Cron 定时报表接口
### GET|POST `/api/cron/scheduled-report`
鉴权Cron secret。生产环境要求 `CRON_SECRET` 已配置并通过 `Authorization: Bearer <CRON_SECRET>` 传入。非生产环境未配置
secret 时开放。
入参:无。
请求示例:
```bash
curl -X POST "{BACKEND_API_BASE_URL}/api/cron/scheduled-report" \
-H "Authorization: Bearer <CRON_SECRET>"
```
响应参数:
| 字段 | 类型 | 说明 |
|--------------------------------------|---------|---------------------------|
| `code` | integer | 成功为 `1`。 |
| `message` | string | 成功为 `ok`。 |
| `data.runAt` | string | 本次 cron UTC 执行时间。 |
| `data.report.scannedBrands` | integer | 扫描的品牌数量。 |
| `data.report.fired` | integer | 实际发送报表数量。 |
| `data.report.skipped.wrong_hour` | integer | 未到品牌本地发送窗口数量。 |
| `data.report.skipped.cooldown` | integer | 仍在 22 小时冷却期或并发抢占失败数量。 |
| `data.report.skipped.no_channel` | integer | 无 Telegram/WhatsApp 渠道数量。 |
| `data.report.skipped.no_store` | integer | 无店铺数量。 |
| `data.report.skipped.invalid_config` | integer | 报表小时配置非法数量。 |
| `data.report.errors` | integer | 单品牌处理异常数量。 |
| `data.trialReport.scanned` | integer | 扫描试用到期提醒的品牌数量。 |
| `data.trialReport.sent4h` | integer | 发送 4 小时试用提醒数量。 |
| `data.trialReport.sent1h` | integer | 发送 1 小时试用提醒数量。 |
| `data.trialReport.errors` | integer | 试用提醒异常数量。 |
错误:
| 响应 code | data.apiCode | 说明 |
|--------:|-----------------|------------------------|
| 401 | `unauthorized` | Cron secret 不匹配。 |
| 503 | `misconfigured` | 生产环境未配置 `CRON_SECRET`。 |
备注:服务端按品牌本地 `morning_brief_hour` / `evening_recap_hour` 判断发送窗口,并使用 `last_morning_brief_at` /
`last_evening_recap_at` 做跨入口防重复。
### GET `/api/cron/daily-digest`
鉴权:同 `/api/cron/scheduled-report`
入参:无。
请求示例:
```bash
curl -X GET "{BACKEND_API_BASE_URL}/api/cron/daily-digest" \
-H "Authorization: Bearer <CRON_SECRET>"
```
响应参数:完全复用 `/api/cron/scheduled-report`
备注:这是旧 daily digest URL 的兼容入口,当前直接复用 twice-daily scheduled-report 业务服务。
## Stripe 接口
### POST `/api/stripe/checkout`
鉴权Sanctum。
入参:
| 位置 | 字段 | 类型 | 必填 | 说明 |
|--------|---------------------|--------|------|---------------------------------------------------------|
| body | `agreement_version` | string | 条件必填 | 当前品牌首次订阅且尚未接受 SaaS 协议时必填,必须等于 `SAAS_AGREEMENT_VERSION`。 |
| header | `x-forwarded-for` | string | 否 | 审计 IP优先取第一段。 |
| header | `User-Agent` | string | 否 | 协议接受审计 UA最多保存 500 字符。 |
请求示例:
```bash
curl -X POST "{BACKEND_API_BASE_URL}/api/stripe/checkout" \
-H "Authorization: Bearer <sanctum_token>" \
-H "Content-Type: application/json" \
-H "x-forwarded-for: 203.0.113.10, 10.0.0.1" \
-H "User-Agent: StoreAI Web/1.0" \
-d '{
"agreement_version": "2"
}'
```
响应参数:
| 字段 | 类型 | 说明 |
|------------|---------|----------------------|
| `code` | integer | 成功为 `1`。 |
| `message` | string | 成功为 `ok`。 |
| `data.url` | string | Stripe Checkout URL。 |
错误:
| 响应 code | data.apiCode | 说明 |
|--------:|----------------------|------------------------|
| 400 | `no_brand` | 当前账号没有品牌。 |
| 400 | `agreement_required` | 首次订阅缺少协议版本。 |
| 409 | `agreement_stale` | 协议版本不是当前版本。 |
| 500 | `config_error` | 缺少 Stripe price id 配置。 |
| 500 | `stripe_error` | Stripe API 调用失败。 |
| 500 | `server_error` | Stripe 未返回 URL。 |
### POST `/api/stripe/portal`
鉴权Sanctum。
入参:无。
请求示例:
```bash
curl -X POST "{BACKEND_API_BASE_URL}/api/stripe/portal" \
-H "Authorization: Bearer <sanctum_token>"
```
响应参数:
| 字段 | 类型 | 说明 |
|------------|---------|----------------------------|
| `code` | integer | 成功为 `1`。 |
| `message` | string | 成功为 `ok`。 |
| `data.url` | string | Stripe Billing Portal URL。 |
错误:
| 响应 code | data.apiCode | 说明 |
|--------:|----------------|----------------------------|
| 400 | `no_customer` | 当前品牌没有 Stripe customer id。 |
| 500 | `stripe_error` | Stripe API 调用失败。 |
| 500 | `server_error` | Stripe 未返回 URL。 |
### POST `/api/stripe/webhook`
鉴权Stripe 签名。请求必须保留 Stripe 原始 body。
入参:
| 位置 | 字段 | 类型 | 必填 | 说明 |
|----------|--------------------|--------|----|--------------------|
| header | `Stripe-Signature` | string | 是 | Stripe webhook 签名。 |
| raw body | event | object | 是 | Stripe event JSON。 |
请求示例:
```bash
curl -X POST "{BACKEND_API_BASE_URL}/api/stripe/webhook" \
-H "Stripe-Signature: t=1715150000,v1=<stripe_signature>" \
-H "Content-Type: application/json" \
-d '{
"id": "evt_123",
"type": "checkout.session.completed",
"data": {
"object": {
"id": "cs_test_123",
"customer": "cus_123",
"metadata": {
"brand_id": "00000000-0000-4000-8000-000000000101"
},
"subscription": "sub_123"
}
}
}'
```
支持事件:
| 事件 | 处理 |
|----------------------------------------------------|--------------------------------------------|
| `checkout.session.completed` | 写回品牌 `stripe_customer_id`。 |
| `customer.subscription.created` | 更新订阅状态、subscription id、customer id、预约取消时间。 |
| `customer.subscription.updated` | 同上;首次进入 `past_due` 会尝试 Telegram 提醒。 |
| `customer.subscription.deleted` | 标记 canceled清空 subscription id 和预约取消时间。 |
| `charge.dispute.created` | 写入 dispute 审计,尝试通知管理员 Telegram。 |
| `charge.dispute.updated` / `charge.dispute.closed` | 更新 dispute 状态。 |
响应参数:
| 字段 | 类型 | 说明 |
|-----------------|---------|-----------------------|
| `code` | integer | 成功为 `1`。 |
| `message` | string | 成功为 `ok`。 |
| `data.received` | string | 已处理事件类型。 |
| `data.deduped` | boolean | 重复已完成事件返回 `true`。 |
| `data.eventId` | string | 被去重的 Stripe event id。 |
错误:
| 响应 code | data.apiCode | 说明 |
|--------:|---------------------|--------------------------------------------------|
| 400 | `missing_signature` | 缺少 `Stripe-Signature`。 |
| 400 | `bad_signature` | 签名校验失败、webhook secret 缺失或 body 非法。 |
| 500 | `handler_error` | handler 异常HTTP 仍返回 200后端记录 failed 审计行,响应体表达失败。 |
## Telegram 接口
### POST `/api/telegram/webhook`
鉴权Telegram webhook secret。
入参:
| 位置 | 字段 | 类型 | 必填 | 说明 |
|--------|-----------------------------------|---------|------|-----------------------------------|
| header | `X-Telegram-Bot-Api-Secret-Token` | string | 是 | 必须等于后端 `TELEGRAM_WEBHOOK_SECRET`。 |
| body | `message.chat.id` | integer | 条件必填 | Telegram chat id无 chat id 会忽略。 |
| body | `message.text` | string | 条件必填 | 配对命令,格式 `/start pair_<hex>`。 |
| body | `message.from.first_name` | string | 否 | 成功提示中使用。 |
请求示例:
```bash
curl -X POST "{BACKEND_API_BASE_URL}/api/telegram/webhook" \
-H "X-Telegram-Bot-Api-Secret-Token: <TELEGRAM_WEBHOOK_SECRET>" \
-H "Content-Type: application/json" \
-d '{
"update_id": 100001,
"message": {
"message_id": 10,
"text": "/start pair_1234567890abcdef1234567890abcdef1234",
"chat": {
"id": -1001234567890,
"type": "group",
"title": "StoreAI Alerts"
},
"from": {
"id": 123456,
"first_name": "Owner"
}
}
}'
```
响应参数:
| 字段 | 类型 | 说明 |
|----------------|---------|---------------------------------------------------------------------------------------------------------|
| `code` | integer | 成功为 `1`。 |
| `message` | string | 成功为 `ok`。 |
| `data.brandId` | string | 成功消费配对 token 后返回品牌 UUID。 |
| `data.ignored` | string | 非配对或无法消费时返回原因:`no_chat_id``not_pair_command``unknown_token``already_consumed``expired``race_lost`。 |
错误:
| 响应 code | data.apiCode | 说明 |
|--------:|-------------------|--------------------------------|
| 400 | `invalid_payload` | 请求体不是合法 JSON。 |
| 401 | `unauthorized` | Secret header 不匹配。 |
| 503 | `misconfigured` | 后端未配置 Telegram webhook secret。 |
## Debug 接口
Debug 路由默认关闭。必须配置 `ENABLE_DEBUG_ROUTES=true` 后才可访问;关闭时返回 404
`{ "code":0, "message":"Not found.", "data": { "error":"not_found" } }`。开启后仍要求 Sanctum 鉴权。
### GET `/api/debug/last-scan`
鉴权Debug 门禁 + Sanctum。
入参:无。
请求示例:
```bash
curl -X GET "{BACKEND_API_BASE_URL}/api/debug/last-scan" \
-H "Authorization: Bearer <sanctum_token>"
```
响应参数:
| 字段 | 类型 | 说明 |
|-------------------------------|--------------|-----------------------------------------------------------------------------------------|
| `code` | integer | 成功为 `1`。 |
| `message` | string | 成功为 `ok`。 |
| `data.diagnosis` | string | 无品牌、无店铺、无扫描等状态下返回,例如 `no_brand``no_store``no_scans`。 |
| `data.message` | string | 诊断说明。 |
| `data.brand` | object | 品牌摘要含时区、报表小时、Telegram 是否存在。 |
| `data.latest_scan` | object | 最新 scan 摘要 `{ id, scanned_at, status, error }`。 |
| `data.field_presence` | object\|null | payload 关键字段是否存在的统计。 |
| `data.audit_dump` | object\|null | 最近 scan 的 today、商品、广告、竞品等排障摘录。 |
| `data.latest_store_brief` | object\|null | 最近 scan 的 AI brief 原始摘要。 |
| `data.sku_matrix` | object\|null | 最近 SKU Matrix`computed_for``generated_at``confidence``quadrant_counts``entries`。 |
| `data.history_depth` | object | 历史 scan 深度统计。 |
| `data.competitor_diag` | object | 竞品配置和最近 10 次 scan 竞品字段统计。 |
| `data.rules_dry_run` | object\|null | 规则引擎只读 dry-run 结果。 |
| `data.ai` | object | AI provider 与 key 存在性诊断,不返回真实 key。 |
| `data.live_verdict_reattempt` | object | 只读 AI verdict 重试结果。 |
| `data.live_growth_reattempt` | object | 只读 AI growth 重试结果。 |
| `data.diagnostics` | string[] | 额外排障建议。 |
错误:
| 响应 code | data.apiCode | 说明 |
|--------:|----------------|--------------|
| 401 | `unauthorized` | 未登录。 |
| 404 | `not_found` | Debug 门禁未开启。 |
备注:该接口只读,不写 `alerts``store_briefs``sku_matrix` 或业务表。
### GET `/api/debug/test-verdict`
鉴权Debug 门禁 + Sanctum。
入参:无。
请求示例:
```bash
curl -X GET "{BACKEND_API_BASE_URL}/api/debug/test-verdict" \
-H "Authorization: Bearer <sanctum_token>"
```
响应参数:
| 字段 | 类型 | 说明 |
|-----------------------------------|--------------|------------------------------------------------------------------------------------|
| `code` | integer | 成功为 `1`。 |
| `message` | string | 成功为 `ok`。 |
| `data.diagnosis` | string | 诊断失败原因:`no_store``no_scan_payload``no_api_key``unsupported_provider_for_debug`。 |
| `data.provider` | string | 当前 `DIAGNOSIS_PROVIDER`。 |
| `data.model` | string | 实际请求 DeepSeek 的模型;`deepseek-v4-flash``deepseek-reasoner` 会改写为 `deepseek-chat`。 |
| `data.api_key_length` | integer | API key 长度,不返回真实 key。 |
| `data.request.endpoint` | string | DeepSeek endpoint。 |
| `data.request.tool_name` | string | 固定 `emit_verdict`。 |
| `data.request.user_prompt_chars` | integer | prompt 字符数。 |
| `data.response.http_status` | integer | DeepSeek HTTP 状态码。 |
| `data.response.duration_ms` | integer | 请求耗时毫秒。 |
| `data.response.network_error` | string\|null | 网络异常摘要。 |
| `data.response.body_preview` | string | 响应前 2000 字符。 |
| `data.response.body_json_present` | boolean | 响应是否可解析为 JSON。 |
| `data.tool_call_summary` | object\|null | tool call 摘要,含 finish reason、tool name、raw args、usage 等。 |
错误:
| 响应 code | data.apiCode | 说明 |
|--------:|----------------|--------------|
| 401 | `unauthorized` | 未登录。 |
| 404 | `not_found` | Debug 门禁未开启。 |
备注:该接口只支持 DeepSeek wire format 诊断Anthropic provider 请使用 `/api/debug/last-scan` 查看 provider 包装层诊断。