Files
olons_server/docs/移动端任务与账户平台前后端对接文档.md
T

1222 lines
44 KiB
Markdown
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.
# 移动端任务与账户平台前后端对接文档
> 版本:V1.0
> 更新日期:2026-10-09
> 适用项目:`olons_server`
> 数据源:MySQL `olons_admin` 中的 `hg_*` 业务表
## 1. 文档说明
本文档用于移动端与 Olons Server 后端联调。内容依据《移动端任务与账户平台需求文档》、当前代码实现和现有数据库表结构整理。
接口分为两类:
- **已实现**:当前代码已经开放,可以直接联调。
- **待实现**:依据需求和现有数据模型定义的目标契约,不表示服务当前已经开放。
当前认证、个人中心、首页内容、任务订单和手工商城接口已实现。其余接口应按本文档契约逐步开发,实施时如果字段或状态发生调整,必须同步更新本文档后再联调。
## 2. 能力范围
| 领域 | 状态 | 说明 |
| --- | --- | --- |
| 注册、登录 | 已实现 | 创建会员、资料、CNY 钱包并签发登录会话 |
| 退出 | 已实现 | 撤销当前 Redis 会话 |
| 个人资料、密码 | 已实现 | 会员概要、头像、登录密码和交易密码 |
| 首页、内容、Banner、等级 | 已实现 | 只返回已发布且已生效的配置和内容 |
| 任务、订单、任务提交 | 已实现 | 大厅、资格预检、领单、查询和凭证提交;审核结算由后台负责 |
| 钱包、账户明细 | 已实现 | 钱包余额随会员资料返回;账户明细读取不可变流水表并支持分页、业务类型和收支方向筛选 |
| 收款账户、取出 | 已实现(沙箱) | 加密绑定、试算、申请、资金冻结和记录查询;上线前仍需支付与合规评审 |
| 储入 | 待实现 | 需在支付与合规评审后开放 |
| 手工商城 | 已实现 | 已发布商品列表、分类筛选和详情;当前只提供客服咨询动作 |
| 客服、通知、上传 | 待实现 | 部分数据表已存在 |
## 3. 公共协议
### 3.1 基础路径
```text
{BASE_URL}/v1
```
接口不再包含应用名路径段,旧前缀不再提供兼容路由。
生产和测试环境均应使用 HTTPS。
### 3.2 公共请求头
| Header | 必填 | 说明 |
| --- | --- | --- |
| `Content-Type: application/json` | JSON 请求必填 | 请求编码 |
| `Accept: application/json` | 建议 | 响应编码 |
| `X-Request-ID` | 建议 | 客户端生成 UUID,用于链路追踪 |
| `X-Device-ID` | 建议 | 安装级随机标识,不使用 IMEI 等硬件标识 |
| `Authorization: Bearer <token>` | 受保护接口必填 | 登录成功后取得的用户会话 |
| `Idempotency-Key` | 取出提交必填 | 8–80 位请求唯一键;相同业务重试必须复用,不同请求禁止复用 |
服务采用单租户模式,客户端不得在 Header、Query 或 Body 中传入租户身份。登录仅按手机号查找账号;数据库中的 `tenant_id` 仅作为兼容字段保留。
### 3.3 登录会话
当前 Token 是 32 字节随机数的 Base64URL 编码,不是 JWT。前端不得解析 Token 或从中推断用户信息。
- App 使用系统安全存储;Web 不在普通 LocalStorage 中长期保存敏感会话。
- Token 不得进入 URL、埋点、普通日志或错误上报。
- HTTP `401`:清除本地会话,保存当前页面地址并跳转登录。
- HTTP `403`:展示无权限或账户访问受限,不直接按登录过期处理。
- “记住密码”只属于客户端安全存储策略,不作为接口字段上传,严禁明文保存密码。
### 3.4 时间、金额和比例
- 时间统一为 RFC 3339 UTC,例如 `2026-10-09T10:00:00.123Z`。
- 金额统一使用最小货币单位整数。CNY 的 `amount=29800` 表示 `298.00` 元。
- 比例统一使用万分比。`rate=500` 表示 `5%`。
- 前端不得使用浮点数参与资金计算,页面展示值不参与后端结算。
金额对象示例:
```json
{
"amount": 29800,
"currency": "CNY"
}
```
### 3.5 成功响应
```json
{
"code": 0,
"data": {},
"request_id": "29ece743-7474-4f32-8141-31f994da68e9",
"server_time": "2026-10-09T10:00:00.123Z"
}
```
`data` 可为对象、列表或 `null`。前端同时判断 HTTP 状态码和业务 `code`。
### 3.6 失败响应
```json
{
"code": 30000,
"reason": "PHONE_INVALID_FORMAT",
"message": "validation failed",
"details": {
"field": "phone"
},
"request_id": "29ece743-7474-4f32-8141-31f994da68e9",
"server_time": "2026-10-09T10:00:00.123Z"
}
```
| 字段 | 用途 |
| --- | --- |
| `code` | 稳定业务码,用于程序分支 |
| `reason` | 稳定英文原因,用于本地化文案映射 |
| `message` | 后端降级文案 |
| `details` | 错误字段、等待时间等结构化详情 |
| `request_id` | 客服和日志排查依据 |
### 3.7 分页
```http
GET /v1/task-orders?page=1&page_size=20&status=pending
```
- `page` 从 1 开始,默认 1。
- `page_size` 默认 20,最大 100。
- 默认按创建时间倒序。
- 排序字段必须使用后端白名单。
```json
{
"code": 0,
"data": {
"items": [],
"page": 1,
"page_size": 20,
"total": 0,
"has_next": false
},
"request_id": "uuid",
"server_time": "2026-10-09T10:00:00Z"
}
```
### 3.8 重复提交
- 注册接口不使用 `Idempotency-Key`,由账户名、手机号等数据库唯一约束最终防重。
- 任务领取、任务提交、储入和取出等高风险写操作必须由后端使用业务单号、状态机和账务约束防止重复执行。
- 前端在写请求执行中禁用按钮;发生超时后先查询业务单状态,不盲目再次提交。
- 钱包入账以业务单号及 `hg_wallet_ledger.idempotency_key` 等后端约束为准。
## 4. 认证接口
### 4.1 注册(已实现)
```http
POST /v1/auth/register
Content-Type: application/json
```
未登录可访问。
```json
{
"username": "alice01",
"phone": "13800138000",
"password": "Secret123",
"referral_code": "123456",
"privacy_accepted": true
}
```
| 字段 | 类型 | 必填 | 规则 |
| --- | --- | --- | --- |
| `username` | string | 是 | 1–10 位小写字母或数字;后端去空格并转小写 |
| `phone` | string | 是 | 中国大陆 11 位手机号,不带 `+86` |
| `password` | string | 是 | 6–72 字节,不做 MD5 |
| `referral_code` | string | 是 | 6 位数字,必须匹配正常会员邀请码 |
| `privacy_accepted` | boolean | 是 | 必须为 `true` |
注册事务同时写入:
- `hg_app_member`:会员主档。
- `hg_app_member_profile`:会员资料。
- `hg_wallet_account`:CNY 零余额钱包。
成功响应:
```json
{
"code": 0,
"data": {
"member_id": 9,
"member_no": "MABCDEFGHIJKLMNOPQRST",
"username": "alice01",
"phone": "13800138000",
"invite_code": "654321",
"token": "random-base64url-token",
"token_type": "Bearer",
"expires_at": "2026-10-23T10:00:00Z"
},
"request_id": "uuid",
"server_time": "2026-10-09T10:00:00Z"
}
```
注册成功即自动登录。前端保存 Token 后直接进入首页,不再调用登录接口。
### 4.2 登录(已实现)
```http
POST /v1/auth/login
Content-Type: application/json
```
```json
{
"phone": "13800138000",
"password": "Secret123"
}
```
成功响应与注册相同。连续登录失败达到 10 次时,该账号在 15 分钟窗口内被限流。
### 4.3 退出(已实现)
```http
POST /v1/auth/logout
Authorization: Bearer <token>
```
后端删除当前会话,响应 `data=null`。缺少、格式错误或已失效的 Token 返回 HTTP 401 和 `AUTH_REQUIRED`。前端无论请求是否成功都清除本地凭证。
### 4.4 认证错误
| HTTP | `reason` | 说明 |
| --- | --- | --- |
| 400 | `MALFORMED_REQUEST` | JSON 不合法、字段类型错误或存在未知字段 |
| 400 | `USERNAME_INVALID_FORMAT` | 账户名格式错误 |
| 400 | `PHONE_INVALID_FORMAT` | 手机号格式错误 |
| 400 | `PASSWORD_INVALID_FORMAT` | 密码格式错误 |
| 400 | `REFERRAL_CODE_INVALID` | 邀请码为空、格式错误或不存在 |
| 400 | `PRIVACY_POLICY_NOT_ACCEPTED` | 未同意隐私政策 |
| 401 | `INVALID_CREDENTIALS` | 手机号或密码错误 |
| 409 | `ACCOUNT_ALREADY_REGISTERED` | 账户名、手机号或其他唯一字段冲突 |
| 429 | `RATE_LIMITED` | 登录失败次数过多 |
| 503 | `SERVICE_UNAVAILABLE` | Redis 或敏感数据密钥未正确配置 |
## 5. 个人中心(已实现)
### 5.1 当前会员概要(已实现)
```http
GET /v1/me
```
```json
{
"code": 0,
"data": {
"member_id": 9,
"member_no": "MABCDEFGHIJKLMNOPQRST",
"username": "alice01",
"phone": "13800138000",
"avatar_url": "",
"invite_code": "654321",
"level": {
"id": 1,
"code": "normal",
"name": "普通会员",
"icon_url": "",
"commission_rate": 180
},
"member_status": "active",
"member_category": "real",
"credit_score": 100,
"wallet": {
"available_amount": 0,
"frozen_amount": 0,
"total_income": 0,
"total_expense": 0,
"currency": "CNY",
"status": "active",
"version": 0
}
},
"request_id": "uuid",
"server_time": "2026-10-09T10:00:00Z"
}
```
### 5.2 更换头像(已实现)
```http
PATCH /v1/me/avatar
```
```json
{
"attachment_id": 101
}
```
附件必须是 `app_id=olons_server`、归属当前会员、状态有效且 MIME 类型为图片的 `hg_sys_attachment` 记录。上传接口未实现前,前端不能自行生成可用附件 ID。
成功响应 `data`:
```json
{
"attachment_id": 101,
"avatar_url": "https://cdn.example.com/avatar.png"
}
```
### 5.3 修改登录密码(已实现)
```http
PUT /v1/me/password
```
```json
{
"old_password": "OldSecret123",
"new_password": "NewSecret456"
}
```
新密码为 6–72 字节且不能与当前密码相同。修改成功后撤销该会员在密码修改前签发的所有会话,包括当前会话;前端收到成功响应后清理 Token 并返回登录页。
### 5.4 设置或修改交易密码(已实现)
```http
PUT /v1/me/trade-password
```
```json
{
"login_password": "Secret123",
"old_trade_password": "123456",
"new_trade_password": "654321"
}
```
交易密码固定为 6 位数字。首次设置时不传 `old_trade_password`;已设置时必须传入正确的旧交易密码。每次操作都需要 `login_password` 二次确认。
### 5.5 个人中心错误
| HTTP | `reason` | 说明 |
| --- | --- | --- |
| 400 | `AVATAR_ATTACHMENT_INVALID` | 附件无效、非图片或不属于当前会员 |
| 400 | `CURRENT_PASSWORD_INVALID` | 当前登录密码错误 |
| 400 | `LOGIN_PASSWORD_INVALID` | 设置交易密码时二次确认的登录密码错误 |
| 400 | `NEW_PASSWORD_INVALID` | 新登录密码格式错误或与原密码相同 |
| 400 | `OLD_TRADE_PASSWORD_REQUIRED` | 已设置交易密码但未传旧密码 |
| 400 | `OLD_TRADE_PASSWORD_INVALID` | 旧交易密码错误 |
| 400 | `TRADE_PASSWORD_INVALID` | 新交易密码不是 6 位数字或与原密码相同 |
| 404 | `ACCOUNT_NOT_FOUND` | 会员不存在或已停用 |
| 409 | `ACCOUNT_CHANGED` | 会员安全信息已被并发修改,需重新操作 |
## 6. 首页和内容(已实现)
### 6.1 首页聚合(已实现)
```http
GET /v1/home
```
```json
{
"code": 0,
"data": {
"profile": {
"username": "alice01",
"avatar_url": "",
"level_name": "普通会员"
},
"wallet": {
"available_amount": 100000,
"frozen_amount": 0,
"currency": "CNY"
},
"features": {
"task_enabled": true,
"recharge_enabled": false,
"withdrawal_enabled": false,
"manual_mall_enabled": true
},
"banners": [],
"notices": [],
"task_summary": {
"today_count": 0,
"today_commission_amount": 0,
"yesterday_commission_amount": 0
}
},
"request_id": "uuid",
"server_time": "2026-10-09T10:00:00Z"
}
```
`features` 由当前已发布的 `hg_site_config_version.config_json` 生成,前端不得写死。关闭的功能入口应显示后端提供的原因。
功能开关支持位于 `features` 对象内或配置根节点。找不到已发布且已到生效时间的配置时,所有开关默认为 `false`。
`task_summary.today_count` 按当日创建的任务订单统计;今日和昨日佣金仅统计 `hg_task_submission.submission_status=4` 的已结算数据。
### 6.2 Banner(已实现)
```http
GET /v1/banners?position=home_top
```
`position` 默认为 `home_top`,只允许小写字母、数字、下划线和连字符。接口只返回已发布、处于投放时间内且未删除的 Banner。
Banner 跳转类型:
| 数据库值 | API `target_type` | 处理 |
| --- | --- | --- |
| 1 | `none` | 不跳转,不返回 `target_value` |
| 2 | `internal_route` | 只接受单斜线开头的站内路由 |
| 3 | `external_url` | 只接受带有有效域名的 HTTPS URL |
不安全或不识别的跳转值降级为 `none`。
### 6.3 内容详情(已实现)
```http
GET /v1/contents/{content_key}
```
`privacy_policy` 和 `user_agreement` 允许未登录访问,其他内容需要 Bearer Token。目前数据库中尚无这两个公开内容键,在后台发布前请求会返回 `404 RESOURCE_NOT_FOUND`。
内容详情建议字段:
```json
{
"content_key": "privacy_policy",
"content_type": "policy",
"title": "用户隐私政策",
"summary": "",
"body_html": "<p>...</p>",
"body_text": "...",
"version": 3,
"published_at": "2026-10-09T08:00:00Z"
}
```
`asset_ids` 始终返回数组。`body_html` 来自后台已清洗的发布版本;前端仍需禁止脚本、事件属性和危险 URL scheme。
### 6.4 会员等级(已实现)
```http
GET /v1/member-levels
```
只返回已启用等级及其当前已发布、已生效版本,按后台 `sort` 排序。
列表项:
```json
{
"id": 1,
"code": "VIP1",
"name": "普通会员",
"icon_url": "",
"upgrade_amount": 50000,
"gift_amount": 2000,
"daily_order_limit": 10,
"commission_rate": 180,
"daily_withdraw_limit": 4,
"rules": {},
"version": 1
}
```
### 6.5 荣誉榜(已实现)
```http
GET /v1/leaderboard
```
接口要求登录,返回当前租户内已完成领取订单总金额排名前 50 的有效会员。页面将订单总金额展示为领取积分,接口字段 `claimed_points` 的单位为分。排名只统计
`hg_trade_order.order_type=2 AND order_status=4` 的真实任务订单;昵称由服务端脱敏,前端不得还原或展示手机号。
```json
{
"code": 0,
"data": [
{
"rank": 1,
"username": "风***淡",
"avatar_url": "https://cdn.example.com/avatar.png",
"claimed_points": 125800,
"status": "领取成功"
}
]
}
```
没有已完成任务时返回空数组,不使用模拟排行数据填充页面。
### 6.6 首页和内容错误
| HTTP | `reason` | 说明 |
| --- | --- | --- |
| 400 | `BANNER_POSITION_INVALID` | Banner 位置参数格式错误 |
| 400 | `CONTENT_KEY_INVALID` | 内容键格式错误 |
| 404 | `RESOURCE_NOT_FOUND` | 内容不存在、未发布或未生效 |
## 7. 任务和订单(已实现)
### 7.1 接口清单
| 方法 | 接口 | 用途 |
| --- | --- | --- |
| GET | `/v1/task-halls` | 当前等级可访问的任务大厅 |
| GET | `/v1/tasks/eligibility?hall_id=1` | 领取资格预检 |
| GET | `/v1/tasks/stats` | 总资产、今日/昨日佣金、领取数量 |
| POST | `/v1/task-orders/claim` | 领取或匹配任务 |
| GET | `/v1/task-orders` | 订单分页 |
| GET | `/v1/task-orders/{order_id}` | 订单详情 |
| POST | `/v1/task-orders/{order_id}/submissions` | 提交任务证据 |
本章接口全部要求登录。数据来源为 `hg_task_hall`、`hg_task`、`hg_trade_order`、`hg_task_submission`、`hg_wallet_account` 和 `hg_wallet_ledger`,仅处理 `order_type=2` 的任务订单,不会混入手工商城订单。
任务开关和领单规则读取当前已发布且已生效的 `hg_site_config_version.config_json`。支持以下配置键(可放在根节点或嵌套对象中):
| 配置键 | 含义 | 未配置时 |
| --- | --- | --- |
| `task_enabled` | 任务功能开关 | `false`,关闭领单 |
| `task_minimum_amount` | 最低可用余额,单位分 | `0` |
| `task_business_start_at` | 营业开始时间,格式 `HH:mm` 或 `HH:mm:ss` | `00:00:00` |
| `task_business_end_at` | 营业结束时间,格式 `HH:mm` 或 `HH:mm:ss` | `23:59:59` |
| `task_deadline_minutes` | 订单完成时限,分钟 | `15` |
开关采用失败关闭策略:找不到有效配置或没有 `task_enabled=true` 时,可以读取大厅、统计及历史订单,但资格预检返回 `TASK_DISABLED`,领单被拒绝。
### 7.2 领取资格预检
```http
GET /v1/tasks/eligibility?hall_id=1
```
```json
{
"code": 0,
"data": {
"eligible": false,
"reason": "INSUFFICIENT_BALANCE",
"message": "可用余额不足",
"available_amount": 10000,
"minimum_amount": 50000,
"today_count": 2,
"daily_limit": 5,
"pending_order_id": 0,
"business_start_at": "09:00:00+08:00",
"business_end_at": "22:00:00+08:00"
},
"request_id": "uuid",
"server_time": "2026-10-09T10:00:00Z"
}
```
预检包括营业时间、会员状态、钱包状态、最低余额、当日数量、未完成订单及等级范围。预检只供展示,领取时后端必须在事务内再次校验。
判断顺序为:任务开关、营业时间、钱包状态、最低余额、等级每日上限、未完成订单。`risk_status` 是会员类别(`1=真人`、`2=假人`),不是风控状态,不能据此拒绝领单。
### 7.3 领取任务
```http
POST /v1/task-orders/claim
```
```json
{
"hall_id": 1
}
```
成功返回订单摘要,包括 `id`、`order_no`、`status`、`product`、`total_amount`、`commission_amount`、`currency` 和 `deadline_at`。
领单事务会锁定当前会员和钱包记录,重新校验大厅等级、账户状态、最低余额、会员等级每日领取次数和未完成订单。校验通过后先创建 `status=matching`、`order_type=2` 的派单中订单,此时商品、数量、金额和佣金均为空或 `0`,与后台订单列表的“派单中/匹配订单”原型一致。会员的 `auto_dispatch=1` 时,达到网站设置的 `autoOrderDelaySeconds` 延时后由系统自动匹配:从当前已发布、在有效期内、币种为 CNY,且价格同时满足会员可用余额与 `autoOrderMinAmount`/`autoOrderMaxAmount` 区间的 `hg_product` 中随机选取商品,按会员当前等级版本计算佣金,并将订单原子更新为 `status=pending`。匹配成功时在同一事务内从可用余额扣除订单本金并转入冻结余额,同时生成可用余额支出和冻结余额收入流水。订单完成时释放冻结本金、返回可用余额并另行入账佣金;订单超时时也必须释放并退回本金。`auto_dispatch=2` 或自动匹配暂时找不到商品时,订单保持“派单中”,管理员可在后台通过“匹配订单”人工指定商品,人工匹配也执行相同的本金扣除和冻结流程。自动和人工匹配共用同一状态条件,并发时只有一方能更新成功。新注册会员默认开启自动派单,管理员可在会员编辑弹窗修改派单模式。
| HTTP | `reason` | 说明 |
| --- | --- | --- |
| 409 | `PENDING_ORDER_EXISTS` | 存在未完成订单 |
| 422 | `OUTSIDE_BUSINESS_HOURS` | 不在营业时间 |
| 422 | `INSUFFICIENT_BALANCE` | 可用余额不足 |
| 422 | `DAILY_LIMIT_REACHED` | 已达当日上限 |
| 422 | `LEVEL_NOT_ELIGIBLE` | 等级不符合 |
| 422 | `ACCOUNT_RESTRICTED` | 账户状态受限 |
| 422 | `TASK_DISABLED` | 任务开关未开启或没有已生效配置 |
| 503 | `NO_TASK_AVAILABLE` | 当前没有可匹配任务 |
### 7.4 订单列表
```http
GET /v1/task-orders?page=1&page_size=20&status=pending
```
`status` 可选:`all`、`matching`、`pending`、`processing`、`completed`、`expired`、`frozen`、`cancelled`、`failed`。
```json
{
"items": [
{
"id": 101,
"order_no": "TO202610090001A1B2C3D4",
"task_id": 1,
"product": {
"id": 20,
"name": "示例商品",
"cover_url": "https://cdn.example.com/product.png"
},
"quantity": 1,
"unit_price_amount": 29800,
"total_amount": 29800,
"commission_amount": 1490,
"currency": "CNY",
"status": "pending",
"status_text": "待处理",
"message": "请在截止时间前完成",
"created_at": "2026-10-09T10:00:00Z",
"updated_at": "2026-10-09T10:01:00Z",
"deadline_at": "2026-10-09T10:15:00Z"
}
],
"page": 1,
"page_size": 20,
"total": 1
}
```
### 7.5 订单详情与提交
订单详情额外返回任务说明、商品和规则快照、状态时间线、失败原因、关联账变及 `allowed_actions`。倒计时使用响应 `server_time` 与 `deadline_at` 的差值。
```http
POST /v1/task-orders/{order_id}/submissions
```
```json
{}
```
成功返回 `submission_no`、`status=settled`、`submitted_at` 和 `order_status=completed`。重复提交不得生成多笔提交记录或佣金流水。
正常情况下仅 `pending` 订单允许提交。前端按原型提供“立即提交”确认操作,不要求用户填写凭证。接口仍兼容可选的 `evidence_text` 和 `evidence_attachment_ids`:文字最长 2000 字符、附件最多 9 个;附件必须属于当前租户和当前会员、`app_id=olons_server`、状态有效,且类型为图片或 PDF。提交事务对订单、钱包加锁,超时订单自动置为 `expired`;有效提交创建唯一的已结算记录、佣金流水并增加可用余额,最后把订单置为 `completed`。后台审核页保留已结算记录供查询,冻结、强制处理和人工补偿只作为异常兜底。
订单详情返回:
- `task`:任务标题、说明和详情 HTML;
- `rule_snapshot`:领单时固化的佣金、等级和截止时间;
- `submission`:凭证、审核状态与驳回原因;
- `timeline`:创建、匹配、提交、审核、结算、完成时间点;
- `ledgers`:以订单号或提交号关联的钱包流水;
- `allowed_actions`:待处理为 `submit`、`contact_support`,超时/冻结/失败仅为 `contact_support`;提交成功后不再提供重复提交操作。
### 7.6 任务统计
```http
GET /v1/tasks/stats
```
```json
{
"available_amount": 100000,
"frozen_amount": 29800,
"total_asset_amount": 129800,
"today_commission_amount": 1490,
"yesterday_commission_amount": 1000,
"today_claim_count": 2,
"today_completed_count": 1
}
```
佣金只统计 `hg_task_submission.submission_status=4` 的已结算记录,今日领取和完成数量只统计任务订单。日期边界按数据库 UTC 日期计算。
### 7.7 通用校验错误
| HTTP | `reason` | 说明 |
| --- | --- | --- |
| 400 | `HALL_ID_INVALID` | 大厅 ID 缺失或格式错误 |
| 400 | `ORDER_ID_INVALID` | 订单 ID 格式错误 |
| 400 | `PAGE_INVALID` / `PAGE_SIZE_INVALID` | 分页参数错误,`page_size` 最大 100 |
| 400 | `ORDER_STATUS_INVALID` | 状态筛选值不在允许范围内 |
| 400 | `EVIDENCE_INVALID` | 凭证内容或附件不符合要求 |
| 404 | `ORDER_NOT_FOUND` | 订单不存在或不属于当前会员 |
| 409 | `ORDER_STATE_INVALID` | 当前状态不允许提交或已经提交过 |
| 409 | `ORDER_EXPIRED` | 提交时订单已超时 |
## 8. 手工商城(已实现)
| 方法 | 接口 | 说明 |
| --- | --- | --- |
| GET | `/v1/manual-products?page=1&page_size=20&category_id=0` | `product_type=2` 的已发布商品 |
| GET | `/v1/manual-products/{product_id}` | 商品详情和可执行动作 |
两个接口均要求登录,只返回同时满足以下条件的商品:
- `hg_product.product_type=2`;
- 商品 `publish_status=3`,未被软删除且在展示有效期内;
- 所属 `hg_product_category.status=1`,并且分类未被软删除。
### 8.1 商品列表
```http
GET /v1/manual-products?page=1&page_size=20&category_id=0
```
`category_id=0` 或不传表示全部启用分类,指定正整数时按分类筛选。推荐商品优先,再按后台 `sort` 和商品 ID 排序。响应示例:
```json
{
"items": [
{
"id": 4,
"product_no": "DEMO-P-03",
"category": {
"id": 2,
"code": "DEMO_DAILY",
"name": "演示日用百货"
},
"name": "已发布演示商品",
"shop_name": "Olons 演示店",
"price_amount": 12900,
"currency": "CNY",
"cover_url": "https://cdn.example.com/product.png",
"summary": "商品说明摘要",
"claimed_count": 1,
"is_recommended": true,
"is_limited": true,
"start_at": "2026-10-01T00:00:00Z",
"end_at": "2026-10-31T23:59:59Z",
"available_actions": ["contact_support"]
}
],
"page": 1,
"page_size": 20,
"total": 1
}
```
金额单位为分。现有 `hg_product` 没有独立摘要字段,`summary` 由商品详情转换为纯文本后截取前 120 个字符。`claimed_count` 直接读取商品表中的固定展示数,不在页面或接口中重新计算。`is_limited` 在商品配置了 `end_at` 时为 `true`,客户端使用该字段区分限时商品,不根据分类名称推断;`start_at`、`end_at` 为商品展示有效期。
### 8.2 商品详情
```http
GET /v1/manual-products/4
```
详情在列表字段基础上增加:
```json
{
"payment_type": "balance",
"detail_text": "已发布演示商品,用于展示。",
"created_at": "2026-10-05T14:32:29Z",
"updated_at": "2026-10-08T14:32:29Z"
}
```
当前版本只支持用户主动咨询,不创建商城订单,也不扣款或冻结余额,因此 `available_actions` 固定返回 `["contact_support"]`。后续开放购买时必须另行增加下单接口、库存校验、幂等与资金事务,不能复用咨询动作直接扣款。
运营详情不会以原始 HTML 返回。后端移除标签、脚本和样式内容后输出 `detail_text`,避免持久型 XSS;`cover_url` 仅接受不包含用户凭据的 HTTPS 地址,不符合要求时返回空字符串。
### 8.3 错误码
| HTTP | `reason` | 说明 |
| --- | --- | --- |
| 400 | `CATEGORY_ID_INVALID` | 分类 ID 不是非负整数 |
| 400 | `PRODUCT_ID_INVALID` | 商品 ID 缺失或格式错误 |
| 400 | `PAGE_INVALID` / `PAGE_SIZE_INVALID` | 分页参数错误,`page_size` 最大 100 |
| 404 | `PRODUCT_NOT_FOUND` | 商品不存在、未发布、已过展示期、已删除或分类已停用 |
## 9. 钱包和明细(待实现)
### 9.1 钱包概要
```http
GET /v1/wallet
```
```json
{
"code": 0,
"data": {
"available_amount": 100000,
"frozen_amount": 29800,
"total_income": 50000,
"total_expense": 10000,
"currency": "CNY",
"status": "active",
"version": 12
},
"request_id": "uuid",
"server_time": "2026-10-09T10:00:00Z"
}
```
### 9.2 账户明细
```http
GET /v1/wallet/ledgers?page=1&page_size=20&biz_type=task&direction=income
```
```json
{
"id": 1001,
"ledger_no": "WL202610090001",
"biz_type": "task",
"biz_no": "TO202610090001",
"entry_type": "commission",
"direction": "income",
"amount": 1490,
"before_amount": 100000,
"after_amount": 101490,
"currency": "CNY",
"remark": "任务佣金",
"occurred_at": "2026-10-09T10:30:00Z",
"resource": {
"type": "task_order",
"id": 101
}
}
```
钱包流水只能由后端业务事务生成,移动端没有新增、修改或删除流水的接口。
## 10. 储入(待实现)
本组接口在支付通道、收款主体和合规评审完成前只允许沙箱联调。
| 方法 | 接口 | 用途 |
| --- | --- | --- |
| GET | `/v1/recharge/channels` | 可用通道、金额范围和手续费规则 |
| POST | `/v1/recharge-orders` | 创建储入订单 |
| GET | `/v1/recharge-orders` | 储入记录分页 |
| GET | `/v1/recharge-orders/{order_id}` | 储入详情 |
创建请求:
```json
{
"channel_id": 1,
"amount": 50000,
"currency": "CNY",
"voucher_attachment_ids": [],
"remark": ""
}
```
响应包含 `recharge_no`、`status`、`request_amount`、`payable_amount`、`currency`、`expire_at` 和经过校验的支付指引。不通过客服聊天发送临时个人收款账户。
公开状态:`pending_payment`、`reviewing`、`processing`、`succeeded`、`failed`、`expired`、`cancelled`。当前人工审核流程创建后直接进入 `reviewing`;`pending_payment` 仅为后续接入真实支付通道和支付回调时保留。失败、驳回或过期必须返回 `reason_code` 和 `reason_text`。
## 11. 收款账户和取出(已实现,沙箱)
本组接口已完成代码和现有数据库的读接口联调。在支付通道、付款主体、实名要求、反洗钱策略和法务评审完成前,只允许沙箱使用,不得连接真实付款通道。
取出功能读取当前已发布且已生效的 `hg_site_config_version.config_json`。仅当根节点或嵌套对象中的 `withdrawal_enabled=true` 时允许试算和提交;没有有效配置时按失败关闭策略返回 `WITHDRAWAL_DISABLED`。收款账户查询与绑定不受该开关影响。
### 11.1 收款账户
```http
GET /v1/payment-accounts
POST /v1/payment-accounts
PUT /v1/payment-accounts
GET /v1/payment-accounts/details
POST /v1/attachments/images
```
先分别上传微信和支付宝二维码。图片接口使用 `multipart/form-data`,字段名为 `file`,仅接受不超过 5MB 的 JPG、PNG、WebP,返回附件 `id` 和可访问的 `url`。随后提交新增请求:
```json
{
"bank_name": "中国工商银行",
"holder_name": "张三",
"identity_no": "11010119900101123X",
"account_no": "6222000000001234",
"wechat_qr_attachment_id": 101,
"alipay_qr_attachment_id": 102
}
```
在“取出信息”页面,`GET /v1/payment-accounts/details` 返回当前会员自己的原始绑定字段及二维码附件 ID;页面将首条默认账户回填到同一表单。修改时调用 `PUT /v1/payment-accounts`,请求体沿用新增字段并增加已有账户的 `id`。更新按租户、会员和账户 ID 校验归属,原记录 ID 保持不变;未更换二维码时可沿用已绑定的有效附件 ID。历史资料无法解密时不回填脱敏字符串,用户须重新填写敏感字段。
绑卡页面按原型固定为银行卡账户,字段规则如下:
| 字段 | 是否必填 | 说明 |
| --- | --- | --- |
| `holder_name` | 是 | 2–64 个字符 |
| `identity_no` | 是 | 15 位或 18 位身份证号,18 位末位允许 X |
| `account_no` | 是 | 8–32 位数字,空格和连字符会被移除 |
| `bank_name` | 是 | 2–100 个字符 |
| `wechat_qr_attachment_id` | 是 | 当前会员上传的有效图片附件 |
| `alipay_qr_attachment_id` | 是 | 当前会员上传的有效图片附件,不能与微信二维码共用同一附件 |
姓名、身份证号和银行卡号使用 AES-256-GCM 独立随机 nonce 加密,身份证号和银行卡号另存 HMAC-SHA256 检索哈希。列表只返回脱敏信息,绝不返回明文或密文。绑定不要求支付密码;支付密码只在取出提交时校验。当前产品不设置银行卡后台审核流程,绑定成功后直接写入 `verify_status=verified`,可立即用于取出。
列表项示例:
```json
{
"id": 1,
"account_type": "bank_card",
"bank_code": "BANK",
"bank_name": "中国工商银行",
"holder_mask": "张*",
"identity_no_mask": "1101**********123X",
"account_no_mask": "6222********1234",
"wechat_qr_attachment_id": 101,
"alipay_qr_attachment_id": 102,
"verify_status": "verified",
"is_default": true,
"created_at": "2026-10-09T10:00:00Z",
"updated_at": "2026-10-09T10:00:00Z"
}
```
### 11.2 取出试算与提交
```http
POST /v1/withdrawal-orders/quote
POST /v1/withdrawal-orders
GET /v1/withdrawal-orders?page=1&page_size=20&status=all
GET /v1/withdrawal-orders/{order_id}
```
试算请求:
```json
{
"payment_account_id": 1,
"amount": 50000,
"currency": "CNY"
}
```
试算根据已核验收款账户选择启用且支持取出的 `hg_payment_channel`。优先匹配账户类型对应通道,不存在时使用人工通道。手续费支持固定金额、万分比和两者相加三种模式;比例手续费向上取整到分。
试算返回金额、渠道、可用余额、当日次数、额度范围和 5 分钟后的 `quote_expires_at`,但不冻结余额:
```json
{
"payment_account_id": 1,
"account_mask": "6222********1234",
"channel_id": 3,
"channel_code": "bank",
"channel_name": "银行卡",
"request_amount": 50000,
"fee_amount": 100,
"actual_amount": 49900,
"currency": "CNY",
"minimum_amount": 1,
"maximum_amount": 100000,
"available_amount": 100000,
"today_count": 0,
"daily_limit": 4,
"quote_expires_at": "2026-10-09T10:05:00Z"
}
```
提交请求:
```http
POST /v1/withdrawal-orders
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json
```
```json
{
"payment_account_id": 1,
"amount": 50000,
"currency": "CNY",
"trade_password": "654321"
}
```
后端不信任试算结果,提交时重新读取通道并计算手续费和到账金额。`Idempotency-Key` 会结合租户和会员派生稳定业务单号;相同请求重试返回原订单并标记 `idempotent_replay=true`,同一个键用于不同账户、金额或币种时返回 `IDEMPOTENCY_CONFLICT`,不依赖额外幂等记录表。
提交事务执行以下操作:
1. 锁定会员,确认账户有效、交易密码未变化;
2. 校验收款账户属于当前会员且已核验;
3. 锁定并重新校验支付通道规则;
4. 锁定 CNY 钱包,检查钱包状态、可用余额及会员等级每日取出次数;
5. 创建 `pending_review` 取出订单和加密收款账户快照;
6. 可用余额减少申请金额,冻结余额增加申请金额;
7. 分别写入可用余额支出和冻结余额收入两条不可变流水。
订单、钱包和两条流水在同一数据库事务内提交,任一步失败全部回滚。提交阶段不增加 `total_expense`;后台付款成功时才把冻结金额转为最终支出,失败、驳回或取消时必须解冻退回。后台状态处理不属于本章客户端接口。
公开状态为 `pending_review`、`reviewing`、`processing`、`succeeded`、`failed`、`rejected`、`cancelled`。列表支持上述状态过滤和标准分页;详情、列表均返回脱敏收款快照、金额、手续费、状态、失败原因和各状态时间,不返回 `account_snapshot_cipher`。
### 11.3 错误码
| HTTP | `reason` | 说明 |
| --- | --- | --- |
| 400 | `ACCOUNT_TYPE_INVALID` | 收款账户类型不支持 |
| 400 | `PAYMENT_ACCOUNT_FIELDS_INVALID` | 动态必填字段或格式错误 |
| 400 | `TRADE_PASSWORD_INVALID` | 交易密码格式或内容错误 |
| 400 | `CURRENCY_INVALID` | 当前仅支持 CNY |
| 400 | `IDEMPOTENCY_KEY_INVALID` | 取出提交未提供合规的幂等键 |
| 400 | `WITHDRAWAL_STATUS_INVALID` | 列表状态筛选值错误 |
| 409 | `PAYMENT_ACCOUNT_EXISTS` | 相同类型和账号已经绑定 |
| 409 | `IDEMPOTENCY_CONFLICT` | 同一幂等键被用于不同请求 |
| 422 | `TRADE_PASSWORD_NOT_SET` | 尚未设置交易密码 |
| 422 | `PAYMENT_ACCOUNT_INVALID` | 收款账户不存在、已删除或未核验 |
| 422 | `WITHDRAWAL_DISABLED` | 取出功能未开启 |
| 422 | `WITHDRAWAL_AMOUNT_INVALID` | 金额不在通道范围、超过余额或手续费不合理 |
| 422 | `WITHDRAWAL_DAILY_LIMIT_REACHED` | 已达到会员等级每日次数上限 |
| 422 | `INSUFFICIENT_BALANCE` | 可用余额不足 |
| 422 | `ACCOUNT_RESTRICTED` | 会员或钱包状态受限 |
| 429 | `RATE_LIMITED` | 交易密码连续错误次数过多,15 分钟后重试 |
| 503 | `WITHDRAWAL_CHANNEL_UNAVAILABLE` | 没有可用的取出通道 |
## 12. 客服、通知和上传
### 12.1 客服(已实现)
```http
GET /v1/customer-services
```
响应字段包括 `id`、`service_code`、`name`、`avatar_url`、`work_time`、`online_status`、`service_url`、`backup_contact` 和 `privacy_notice`。
只返回 `hg_customer_service.status=1` 且未删除的客服,按 `sort`、`id` 排序。`online_status` 映射为 `online`、`offline` 或 `busy`。头像和客服地址仅返回不含用户凭据的 HTTPS URL,不符合要求时返回空字符串。
客服 URL 必须经服务端域名白名单校验。前端 WebView/iframe 只开启必要权限,并显示第三方服务来源和隐私提示。
### 12.2 通知
```http
GET /v1/notices?page=1&page_size=20
GET /v1/notices/{notice_id}
POST /v1/notices/{notice_id}/read
```
当前已实现通知列表接口。列表包含当前会员通知和 `member_id=0` 的全体通知,只返回有效期内且未撤回记录,按创建时间倒序排列。
```json
{
"items": [
{
"id": 1,
"title": "系统维护通知",
"content": "通知内容",
"source_type": "real",
"start_at": "2026-10-13T00:00:00Z",
"end_at": "2026-10-20T00:00:00Z",
"created_at": "2026-10-13T00:00:00Z"
}
],
"page": 1,
"page_size": 20,
"total": 1
}
```
`source_type` 为 `real`(系统通知)或 `operational_simulation`(活动通知)。详情和已读接口尚未实现;当前数据库没有会员通知已读关系表,如需精确已读状态,实现前应补充数据模型。
### 12.3 上传
```http
POST /v1/uploads
```
```json
{
"scene": "task_evidence",
"file_name": "evidence.jpg",
"mime_type": "image/jpeg",
"size": 102400
}
```
推荐流程为“申请凭证 → 上传隔离区 → 安全扫描 → 绑定业务”。业务接口只接受扫描通过且属于当前会员的附件 ID。
| 场景 | 类型 | 建议限制 |
| --- | --- | --- |
| `avatar` | JPEG、PNG、WebP | 5 MiB,校验像素尺寸 |
| `task_evidence` | JPEG、PNG、WebP、PDF | 单文件 10 MiB,最多 9 个 |
| `payment_qr` | JPEG、PNG、WebP | 5 MiB,内容校验 |
| `recharge_voucher` | JPEG、PNG、WebP、PDF | 单文件 10 MiB |
## 13. 状态枚举
数据库数字状态是内部实现。API 对前端返回稳定字符串和 `status_text`,前端不得依赖数据库数值。
`hg_app_member.risk_status` 字段名沿用现有数据库,实际业务含义是会员类别:`1` 真人、`2` 假人。API 统一使用 `member_category=real|fake`,不将该字段用于判断账户是否可登录。
| 领域 | API 状态 |
| --- | --- |
| 会员 | `active`、`disabled`、`cancelled` |
| 会员类别 | `real`、`fake` |
| 任务订单 | `matching`、`pending`、`processing`、`completed`、`expired`、`frozen`、`cancelled`、`failed` |
| 任务提交 | `reviewing`、`approved`、`rejected`、`settled`、`reversed` |
| 储入 | `pending_payment`、`reviewing`、`processing`、`succeeded`、`failed`、`expired`、`cancelled` |
| 取出 | `pending_review`、`reviewing`、`processing`、`succeeded`、`failed`、`rejected`、`cancelled` |
| 收款账户核验 | `pending`、`verified`、`rejected` |
| 客服 | `online`、`offline`、`busy` |
已实现领域按本表映射;待实现领域在开发前仍需与 HotGo 字典和正式状态流转确认。
## 14. 通用错误与重试
| HTTP | `reason` | 前端处理 |
| --- | --- | --- |
| 400 | `MALFORMED_REQUEST` | 根据 `details.field` 定位表单字段 |
| 401 | `AUTH_REQUIRED` | 清除会话并跳转登录 |
| 403 | `FORBIDDEN` | 显示无权限或账户访问受限 |
| 404 | `RESOURCE_NOT_FOUND` | 显示内容下线或记录不存在 |
| 405 | `METHOD_NOT_ALLOWED` | 作为开发错误记录,不重试 |
| 409 | `CONFLICT` | 刷新业务资源后让用户决定下一步 |
| 422 | 具体业务原因码 | 显示原因和恢复入口 |
| 429 | `RATE_LIMITED` | 按 `retry_after_seconds` 倒计时 |
| 500 | `INTERNAL` | 展示通用错误和 `request_id` |
| 503 | `SERVICE_UNAVAILABLE` | 安全读请求可限次重试 |
- GET 在网络错误或 503 时可指数退避,最多自动重试 2 次。
- POST、PUT、PATCH 默认不自动重试;超时后先查询业务资源。
- 400、401、403、404、409、422 不做网络层自动重试。
## 15. 页面与接口映射
| 页面 | 首屏接口 | 操作接口 |
| --- | --- | --- |
| `/login` | — | `POST /auth/login` |
| `/register` | `GET /contents/privacy_policy` | `POST /auth/register` |
| `/home` | `GET /home` | Banner、快捷入口、内容详情 |
| `/receive` | `GET /tasks/eligibility`、`GET /tasks/stats` | `POST /task-orders/claim` |
| 记录页 | `GET /task-orders` | 跳转订单详情 |
| `/orderDetail` | `GET /task-orders/{id}` | `POST /task-orders/{id}/submissions` |
| `/mall` | `GET /manual-products` | 查看详情或客服 |
| `/customerService` | `GET /customer-services` | 打开安全 WebView/iframe |
| `/mine` | `GET /me` | 钱包、收款账户、密码、通知、退出 |
| `/recharge` | `GET /recharge/channels`、`GET /member-levels` | `POST /recharge-orders` |
| `/withdrawal` | `GET /wallet`、`GET /payment-accounts` | 试算、提交取出 |
| `/depositRecord` | `GET /recharge-orders` | 查看详情 |
| `/withdrawalRecord` | `GET /withdrawal-orders` | 查看详情 |
| `/bill` | `GET /wallet/ledgers` | 跳转关联业务详情 |
表中省略公共前缀 `/v1`。
## 16. API 与数据表映射
| 领域 | 主表 | 关联表 |
| --- | --- | --- |
| 认证、会员资料 | `hg_app_member` | `hg_app_member_profile`、`hg_app_member_level` |
| 收款账户 | `hg_app_member_payment_account` | `hg_sys_attachment` |
| 钱包 | `hg_wallet_account` | `hg_wallet_ledger` |
| 首页配置 | `hg_site_config_version` | `hg_banner`、`hg_content`、`hg_content_version` |
| 会员等级 | `hg_app_member_level` | `hg_app_member_level_version` |
| 任务大厅 | `hg_task_hall` | `hg_task_hall_level` |
| 任务订单 | `hg_task`、`hg_trade_order` | `hg_task_level`、`hg_product`、`hg_task_submission`、`hg_wallet_ledger` |
| 手工商城 | `hg_product` | `hg_product_category`、`hg_sys_attachment` |
| 储入 | `hg_recharge_order` | `hg_payment_channel`、`hg_wallet_account`、`hg_wallet_ledger` |
| 取出 | `hg_withdrawal_order` | `hg_app_member_payment_account`、`hg_wallet_account`、`hg_wallet_ledger` |
| 客服 | `hg_customer_service` | 第三方会话映射 |
| 通知 | `hg_app_member_notice` | 已读关系待补充 |
## 17. 前端实现要求
### 17.1 请求层
- 集中注入 `BASE_URL`、`X-Request-ID`、`X-Device-ID` 和 `Authorization`。
- 集中处理统一响应、401、429、超时和断网。
- 搜索、分页和页签切换时取消过期请求,避免旧响应覆盖新状态。
- 日志不记录密码、交易密码、Token、完整手机号、银行卡和收款码。
### 17.2 页面状态
每个页面至少覆盖:
- 首次加载、下拉刷新、加载更多、空状态、可重试失败、不可重试业务失败。
- 写请求期间按钮 loading 和防重复点击。
- 服务端返回的状态文案、失败原因与恢复入口。
- 320–430 像素宽度可用;键盘弹出时表单按钮可达;触控区域至少 44×44 像素。
### 17.3 语言和文案
- `reason`、`status` 等枚举固定使用英文。
- 前端语言包映射用户文案,不把后端 `message` 作为唯一文案源。
- 本文档统一使用“储入”“取出”“领取”。如产品最终改为“充值”“提现”“接单”,前后端文档与语言包必须同时调整。
## 18. 联调顺序
1. **P0 认证**:注册、登录、退出、会话过期和错误映射。
2. **P0 账户首页**:`/me`、`/home`、Banner、内容、等级和钱包概要。
3. **P0 任务闭环**:预检、领取、订单列表、详情、提交、审核状态和账变。
4. **P1 内容服务**:手工商城、客服、通知和上传。
5. **P1 资金闭环**:收款账户、储入、取出、回调、审核和对账;开始前必须完成支付与法务评审。
## 19. 联调验收清单
- [ ] 未登录用户只能访问登录、注册和公开协议。
- [ ] 注册成功后自动进入登录态,不重复调用登录。
- [ ] 401 清理会话并返回登录;403 不误判为会话过期。
- [ ] 页签快速切换时旧请求不会覆盖新数据。
- [ ] 重复点击任务领取或提交不生成重复订单、提交或账变。
- [ ] 订单、钱包、账变和统计口径一致。
- [ ] 储入和取出失败、驳回、超时及取消均有原因和恢复路径。
- [ ] 手机号、密码、交易密码、银行卡和 Token 不进入日志或埋点。
- [ ] 富文本、商品名、公告和客服链接无可执行恶意内容。
- [ ] 金额均按最小货币单位传输,无浮点精度差异。
- [ ] 断网、超时、429 和 5xx 均有清晰恢复方式。
## 20. 待确认事项
1. 登录注册是否加入图片验证码、短信验证或设备风控挑战。
2. 是否新增刷新 Token、设备会话列表和“退出所有设备”。
3. 任务、储入和取出数据库状态值的最终字典与允许转换。
4. 任务领取是随机匹配、规则匹配还是用户选择,以及截止时间的正式存储位置。
5. 会员等级版本中储入、取出、任务数和佣金规则的正式 JSON Schema。
6. 支付通道、收款主体、实名要求、取出审核和适用地区的合规结论。
7. 客服使用自建系统还是第三方 WebView/iframe,以及允许传递的用户信息。
8. 通知是否要求逐会员已读记录;如需要,数据库应补充相应关系表。