# 浏览器基础 API

> 内置文档中心为 `/help`；对外登录字段与响应见 [登录与授权契约](/help/integration-api)。本页是基础接口记录，不是全量接口契约。

> 当前新增接口及应用接入见 [集成协议](/help/integration)，本页保留基础认证接口记录。

路径前缀 `/api/v1`。成功返回 `{code:0,message:"success",data:...}`，失败用 HTTP 状态及 `{code,message}`。所有接口 `Cache-Control: no-store`。JSON 默认最大 64KiB；门户配置请求允许 256KiB，解码后的门户内容不得超过 200KiB。此文只列实际实现的端点。

后台及工作区分页列表接受 `page`（从 1 开始）与 `limit`（1–100，默认 30）。响应正文继续以数组放在 `data` 中；`X-Total-Count` 是应用相同身份权限与筛选条件后的准确总条数，`X-Page-Size` 是实际页大小。客户端应据此计算总页数；筛选或删除使当前页越界时，应回退到最后一个有效页，不能根据本页数组长度推算总页数。

浏览器访问 `GET /auth/csrf` 获取 CSRF Cookie 与响应中的 `csrf_token`；所有非GET请求要求 `Origin` 与 `APP_ORIGIN` 完全一致、`X-CSRF-Token` 与 Cookie 一致。不能跨域直接调用。身份通过 HttpOnly Cookie，不在响应正文返回JWT。

| 方法 | 路径 | 请求/结果 |
| --- | --- | --- |
| GET | /auth/csrf | 设置CSRF Cookie，返回 csrf_token |
| POST | /auth/code | email、purpose=register或reset；发送6位验证码，10分钟有效 |
| POST | /auth/register | email、nickname、password、code |
| POST | /auth/login | email、password、remember（布尔） |
| POST | /auth/refresh | 使用刷新Cookie，轮换并重发访问Cookie |
| POST | /auth/logout | 撤销当前会话，清除身份Cookie |
| POST | /auth/reset-password | email、code、password；撤销该用户所有会话 |
| GET | /user/profile | user、permissions |
| PUT | /user/profile | nickname，1–20字符 |
| POST | /profile/email/code | email、password；向当前未验证邮箱或新邮箱发送验证码 |
| PUT | /profile/email | email、password、code；验证当前邮箱，或更换并验证新邮箱 |
| PUT | /user/password | current_password、password；成功后重新登录 |
| GET | /user/sessions | sessions、current_id，包含 IP、User-Agent 和最近活动时间，最多100条有效会话 |
| DELETE | /user/sessions/:id | 只能撤销本人的会话 |
| GET | /user/login-risk/preferences | 风险登录通知开关、站内信/邮件渠道与通知阈值 |
| PUT | /user/login-risk/preferences | 保存 enabled、in_app_enabled、email_enabled、threshold（1—100） |
| GET | /user/login-risk/events | 最近30次登录风险判定；IP 仅返回脱敏网段 |
| GET | /portal | 门户 version/content；仅返回可见板块 |
| GET | /admin/portal | 完整门户（含隐藏板块），要求 portal:write |
| PUT | /admin/portal | version/content；原子保存发布并递增版本，过期version返回409 |
| GET | /admin/portal/configs | 命名方案摘要、当前发布方案与待发布状态，要求 portal:write |
| POST | /admin/portal/configs | name、content；新建方案，最多 30 个 |
| GET | /admin/portal/configs/:id | 读取一个方案的 version/content 与发布状态 |
| PUT | /admin/portal/configs/:id | name、version、content；保存但不发布，过期 version 返回 409 |
| POST | /admin/portal/configs/:id/copy | name、version；复制最近保存的方案版本 |
| POST | /admin/portal/configs/:id/publish | version；原子发布指定已保存版本并更新公开门户 |

内容结构：`{brand,headline,description,theme,hero_layout,hero_image,hero_image_alt,sections:[{id,title,body,visible,layout,image,image_alt,image_position,cards}]}`。旧结构字段保持兼容；缺少新字段时使用默认紫色、品牌图形和文字版式。`layout` 可为 `text`、`split`、`banner` 或 `cards`；板块和卡片都可引用已有图片存储中的门户图片或管理员允许的 HTTP(S) 图片。卡片支持标签、标题、简介、图片、预设图标、配色与 HTTP(S)/站内跳转链接，每个板块最多 8 张。板块按数组顺序显示；文本不解析 HTML。所有方案写入与发布均要求 `portal:write`，保存和发布分别校验并发版本及图片引用。

健康检查不在前缀内：`GET /health/live`、`GET /health/ready`。后者检测PostgreSQL及Redis。

密码至少12个Unicode字符且UTF-8不超过72字节。验证码单用途、单次使用，5次错误后作废，60秒重发间隔。API限流采用Redis，Redis不可用则认证失败关闭。登录失败不区分邮箱不存在或密码错误。

## 风险登录检测

风险检测在会话和登录令牌成功签发后运行，默认阈值为 50，任何判定或通知失败都不会撤销或阻断已完成的登录。刷新访问令牌不会创建新的登录风险事件；需要二次验证的流程只能在二次验证完成并正式签发会话后调用登录完成钩子。

当前只使用服务端实际拥有的信号：规范化登录 IP、会话客户端类型，以及从 User-Agent 归类出的设备、操作系统和浏览器类别。新 IP 与新客户端各计 50 分；首次登录只建立基线并计 0 分。系统不会根据 IP 猜测国家、城市或精确位置，也不依赖外部风控或付费地理位置服务。

`enabled` 只控制是否通知，关闭后仍保留检测记录。站内信和邮件可独立开启；达到阈值的同一 IP/客户端指纹在 24 小时内最多安排一次通知。邮件由 outbox 异步重试，SMTP 未配置或投递失败不会影响登录及已生成的站内信。API 和通知正文只显示 IPv4 `/24` 或 IPv6 `/64` 网段，不返回事件所关联的完整 IP 或原始 User-Agent。
# 管理员首次运行自检

`POST /api/v1/admin/setup-check`：需要登录、CSRF 和 `settings:write` 权限。检查数据库/Redis 连通与 SMTP、S3、支付、第三方登录配置，返回脱敏结果及回调地址并记录审计。状态和验收边界见 [Issue #3 自检说明](/help/setup-check)。

## 订单取消与售后退款

个人工作台的订单详情提供待付款取消和退货退款申请。管理工作台的订单详情提供取消、取消并退款、售后全额及部分退款；退款处理页显示类型、现金、积分和渠道错误，失败任务可重试，银行退款须填写实际流水号。

| 操作 | 接口 | 约束 |
| --- | --- | --- |
| 用户取消 | `POST /orders/:id/cancel`，`{}` | 仅本人 `pending` 订单；重复取消成功，冻结积分、库存及优惠券仅释放一次 |
| 管理员取消 | `POST /admin/orders/:id/cancel`，`{}` | `orders:write`；同样仅待付款订单 |
| 管理员退款 | `POST /admin/orders/:id/refund` | `orders:write`；请求见下文，成功返回 `data.id` 退款 ID |
| 用户退货退款 | `POST /orders/:id/refund`，`{reason}` | 本人已付款或已完成订单，无其他进行中退款；申请退还剩余额度，审核通过后回收授权及续期 |
| 审核申请 | `POST /admin/refunds/:id/review`，`{approve,note}` | `orders:write`，仅 `requested` 可审核；驳回释放额度并恢复原订单状态 |
| 银行退款确认 | `POST /admin/refunds/:id/confirm-transfer`，`{reference}` | `orders:write`，必须实际退款后录入；同一退款同一流水可重放，流水不得重复用于其他退款 |
| 重试失败任务 | `POST /admin/refunds/:id/retry`，`{}` | `orders:write`，复用原退款 ID 与渠道幂等键，不创建第二笔退款 |

管理员退款请求：

```json
{
  "kind": "after_sales",
  "reason": "服务补偿，保留履约",
  "idempotency_key": "refund-operation-unique-001",
  "cash_minor": 250,
  "points_amount": 20
}
```

`kind=after_sales` 支持 `paid`、`completed` 订单，退款过程中和完成后均保留订单状态、服务履约、软件授权及续期。省略 **两个** 金额字段表示退还全部剩余额度；指定金额表示部分退款，两个字段必须同时提供。现金使用原币种最小单位整数（当前支持的 CNY/USD/EUR/AUD/HKD 两位小数币种中，250 表示 2.50），积分使用原始积分整数，且须为原订单 `points_per_minor` 的整数倍；不允许将积分转为现金，也不退还优惠折扣金额。两种金额各自累计不得超过原支付值，零金额售后退款被拒绝。

`kind=cancel_order` 仅允许已付款、尚未履约且无进行中退款的纯服务订单；不得提供金额字段，系统退还全部剩余额度。含软件授权或续期的混合订单在支付时已部分履行，不能整单取消，应选择售后退款。取消退款登记后订单为 `refunding`，阻止服务交付；退款成功后为 `cancelled`，恢复库存和未过期优惠券。免费订单或已全额售后退款的未履约服务订单仍可取消，不发生重复资金退回。

订单详情 `GET /orders/:id` 或管理接口额外返回 `refundable: {cash_minor,points_amount,pending}` 和 `can_cancel_refund`。可退额度扣除所有未驳回的退款，包括申请中、渠道处理中、成功和失败待核对的退款；晚到/重复付款的退款独立绑定实际支付尝试，不占原订单售后额度。详情用于展示，提交时由服务端在订单锁内重新核算。无权限返回 403，非本人订单返回 404，状态或幂等冲突返回 409，非法金额返回 400。

退款幂等键为 8–128 字符，在同一订单内唯一。相同键及相同请求返回同一个退款 ID；修改类型、原因或金额须使用新键。所有退款修改遵循订单锁→退款锁顺序，积分成功入账和库存恢复均在事务内完成。渠道退款走原有适配器和 outbox：只有渠道确认成功才入账积分，异常或未确认继续保留额度；重试沿用原退款 ID。现金为零的积分退款在本地事务完成。

取消订单后收到验证通过的晚到付款，或非待付款订单收到另一笔付款，会对该笔支付尝试自动全额原路退款，保留订单状态，不重复返还积分。重复回调在订单锁后重新检查支付状态；后台查询继续使用原有付款对账机制。银行晚到账退款同样需要人工确认实际退款流水。

原有 `full_return` 退款继续执行授权回收；`after_sales` 全额和部分退款均不会进入该路径。发生正常订单退款时，已有发票进入 `void_pending` 等待财务处理；不自动开具调整后的发票。取消、退款登记、审核、银行确认和重试均保留原有审计记录和专用通知反馈。


## 开发者套餐配置与购买一致性

历史订阅版本续费策略：`PUT /admin/developers/plans/:id/history/:history/renewal` 请求 `{enabled:false,expected_revision:1}`，需 `developers:write`。`history` 是该套餐历史列表中的条目 ID；接口校验归属，归并条目写入原来源的统一策略。仅历史订阅配置可操作，当前配置须使用套餐上下架。历史列表额外返回 `renewal_enabled`、`renewal_revision`、`renewal_accounts`、`can_manage_renewal`。策略并发冲突 409，越权 403，归属不符 404。

开发者总览 `account` 增加 `auto_renew_available`、`auto_renew_reason`、`auto_renew_replacement`；旧配置停续费后仍返回已确认条款，普通预算保存不改变条款。选择当前替代套餐必须经过用户确认，提交其 ID、当前 `auto_renew_expected_revision` 和 `auto_renew_accept_latest:true`。停止/恢复写审计并通知受影响的自动续费账户；扣款事务共享锁定原始策略，停用提交后新续费不可绕过。已有订单/权益不回收，当前套餐不自动下架。

套餐采用稳定商品 ID，`GET /developer/plans` 和商品列表包含 `developer_revision`。普通开发者套餐下单时在 `items[]` 中回传该值，例如 `{product_id,quantity:1,developer_revision:2}`；配置已变化时返回 409，客户端应刷新价格与权益后让用户重新确认。兼容未修改过的首版套餐省略此字段；修改后的套餐必须带上当前版本。已签发升级报价、已创建订单和已发放权益使用各自快照，不按当前商品重算。

运营管理支持 `PUT /admin/developers/plans/:id` 直接编辑配置，请求附带 `expected_revision`；保存后原商品 ID 不变，历史配置自动留存。`GET /admin/developers/plans/:id/history` 分页读取历史；`POST /admin/developers/plans/:id/merge` 接收 `{expected_revision,sources:[{id,revision}]}`，明确将同类型旧套餐归并至路径中的套餐。旧商品保留引用、停止新购并退出主列表，不根据名称自动归并。全部管理接口仍需 `developers:write`。

`GET /developer/overview` 的 `account.auto_renew_revision` 和 `account.auto_renew_terms` 表示已确认续费配置。`PUT /developer/billing` 保存同一个续费商品时，默认保留该配置；普通预算修改不会改变续费价格与权益。更换续费商品，或显式提交 `auto_renew_accept_latest:true` 及当前 `auto_renew_expected_revision` 后，才采用新配置。最新配置已变化时返回 409，金额上限仍会核验。归并旧套餐后继续按原确认快照续费，其可用状态由最终归属套餐控制。
