# `qianapp://` 协议规范

> **跨端唯一权威规范**。本文件描述的协议契约对 Android / iOS / 鸿蒙 SDK 行为一致性具有约束力,
> 任何修改必须三端 SDK + H5 团队四方同意。

## 1. URL 形态

```
qianapp://<custom-path>?<query-string>
```

- **scheme**:固定 `qianapp`(小写;SDK 解析时大小写不敏感)
- **custom-path**:`host` + `path` 拼接,SDK 解析时去除前后斜杠后赋值给 `customPath`
- **query-string**:标准 URL query,UTF-8 percent-encoded

### 示例

```
qianapp://callback/sign?flowId=abc-123&action=sign&result=success&from=tencent_ess
qianapp://done?flowId=f1&action=fill&result=success&from=tencent_ess&customer_id=u-42
qianapp://result?flowId=f2&action=reject_sign&result=fail&from=tencent_ess
```

## 2. 字段集合

### 2.1 Recognized Fields(SDK 抽到 `SignResult` 顶层字段)

| 字段名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `flowId` | String | 否 | 流程 ID;协议未给则为空字符串 |
| `action` | String | 否 | 动作类型,见 §3.1 已知值表 |
| `result` | String | 否 | 结果状态,见 §3.2 已知值表 |
| `from` | String | 否 | 来源标识,通常为 `tencent_ess` |

> **协议在 wire 层仅 String**:`action` / `result` 不是枚举,SDK 不做映射翻译,空字符串与未知值原样透出。

### 2.2 Extras(剩余 query 透传)

除上述 4 个 recognized field 外,所有客户自定义 query 透传到 `SignResult.extras: Map<String, String>`。

**典型用途**:
- 客户在 H5 发起签署时塞入业务上下文(如 `customer_id`、`channel`、`session_token`)
- 历史字段 `errorCode` / `errorMsg`(原方案草稿包含,已**不再放进顶层字段**,会落入 `extras`)

## 3. 已知值表

### 3.1 `action` 已知值

| wire 值 | 含义 | Android 常量 | iOS 常量 | 鸿蒙常量 |
|---|---|---|---|---|
| `fill` | 填写流程完成 | `SignAction.FILL` | `SignAction.fill` | `SignAction.FILL` |
| `sign` | 签署流程完成 | `SignAction.SIGN` | `SignAction.sign` | `SignAction.SIGN` |
| `reject_fill` | 主动拒绝填写 | `SignAction.REJECT_FILL` | `SignAction.rejectFill` | `SignAction.REJECT_FILL` |
| `reject_sign` | 主动拒绝签署 | `SignAction.REJECT_SIGN` | `SignAction.rejectSign` | `SignAction.REJECT_SIGN` |
| `view` | 仅查看 | `SignAction.VIEW` | `SignAction.view` | `SignAction.VIEW` |
| `""`(空字符串) | 协议未指定动作 | —— | —— | —— |

### 3.2 `result` 已知值

| wire 值 | 含义 | Android 常量 | iOS 常量 | 鸿蒙常量 |
|---|---|---|---|---|
| `success` | 操作成功 | `SignResultStatus.SUCCESS` | `SignResultStatus.success` | `SignResultStatus.SUCCESS` |
| `fail` | 操作失败 | `SignResultStatus.FAIL` | `SignResultStatus.fail` | `SignResultStatus.FAIL` |
| `""`(空字符串) | 协议未指定结果 | —— | —— | —— |

### 3.3 `from` 固定值

固定为 `tencent_ess`,客户用于校验回调来源。

## 4. `extras` 透传规则

- **Key 字符集**:由 H5 端控制,SDK 不做白名单校验
- **Value 编码**:UTF-8 percent-encoded;SDK 解析时 percent-decoded(`+` 等价空格)
- **重复 key**:取**首个**,后续覆盖丢弃(与 H5 团队约定一致)
- **空 value**:`?key=` 保留为空字符串而非 `null`

## 5. URL 解码

SDK 解析时:
1. URL 整体不做 `URLDecoder.decode`(避免 query 中 `?` `&` 的歧义);只对 query 拆分后的 key / value 单独 decode
2. percent-decoding 用 **UTF-8**(与 application/x-www-form-urlencoded 一致)
3. `+` 在 value 中等价空格

## 6. 解析失败 fallback

下列情形返回**全空** `SignResult`(各 String 字段为空字符串、`extras` 为空 Map):

- 输入 URL 为 `null` / 空字符串
- 输入 URL scheme 非 `qianapp`
- query 解码异常

> **一致性保证**:Android 端通过 `SignResult.EMPTY` 单例,iOS 端通过 `SignResult.empty` 单例,鸿蒙端通过 `SignResult.EMPTY` 静态 getter 返回相同语义。

## 7. 跨端一致性要求

| 维度 | 约束 |
|---|---|
| 字段命名 | Android `data class` / iOS `struct` / 鸿蒙 `class` 字段名 1:1 一致 |
| 字段类型 | `action` / `result` 在三端均为 String,**绝不**为 enum |
| 解析行为 | 重复 key、空 value、未知值、非法 URL 等边界情形三端解析结果完全一致 |
| 单测 | Android `SignResultParserTest`、iOS `SignResultParserTests`、鸿蒙 `SignResultParser.test.ets` 共享 13 类用例,断言完全对齐 |

## 8. 示例 URL 与期望解析

### 8.1 正常签署成功

输入:`qianapp://callback/sign?flowId=abc-123&action=sign&result=success&from=tencent_ess`

| 字段 | 解析值 |
|---|---|
| `customPath` | `callback/sign` |
| `flowId` | `abc-123` |
| `action` | `sign` |
| `result` | `success` |
| `from` | `tencent_ess` |
| `extras` | `{}` |

### 8.2 异常拒签

输入:`qianapp://result?flowId=f2&action=reject_sign&result=fail&from=tencent_ess&errorCode=10086&errorMsg=user_canceled`

| 字段 | 解析值 |
|---|---|
| `customPath` | `result` |
| `flowId` | `f2` |
| `action` | `reject_sign` |
| `result` | `fail` |
| `from` | `tencent_ess` |
| `extras` | `{"errorCode": "10086", "errorMsg": "user_canceled"}` |

### 8.3 未知 action 边界

输入:`qianapp://x?action=futureBrandNewAction&result=mystery`

| 字段 | 解析值 |
|---|---|
| `customPath` | `x` |
| `action` | `futureBrandNewAction` |
| `result` | `mystery` |
| 其他 | 空 |

> 客户在 `when` / `switch` 的 `else` / `default` 分支可以拿到原值,自行处理。

### 8.4 中文 percent-encoded

输入:`qianapp://x?action=sign&note=%E7%AD%BE%E7%BD%B2%E6%88%90%E5%8A%9F`

| 字段 | 解析值 |
|---|---|
| `action` | `sign` |
| `extras["note"]` | `签署成功` |

### 8.5 extras 含 errorCode

输入:`qianapp://x?action=sign&result=fail&errorCode=500&errorMsg=server_error`

| 字段 | 解析值 |
|---|---|
| `action` | `sign` |
| `result` | `fail` |
| `extras["errorCode"]` | `500` |
| `extras["errorMsg"]` | `server_error` |

## 9. 客户接入提示

- **`action` / `result` 是 String**:在 `when` / `switch` 上推荐用 SDK 提供的 `SignAction.SIGN` 等常量比对,**而非裸字符串**,降低拼写错误概率
- **务必加 `else` 分支**:协议未来可能新增 action(SDK 不强制校验),`else` 拿到原始字符串后建议提示用户「请升级 App 版本」
- **`extras` 透传业务字段**:不要尝试把所有 query 都映射到 SDK 字段;客户自定义业务字段直接用 `extras["xxx"]` 读取
- **解析失败兜底**:`SignResult` 各字段全为空 → 客户可对照 `rawUrl` 判断是否解析失败
