# 活动礼物测试（送礼得道具）- 接口与封装说明

本 skill 的**送礼**与**道具**（称号 + 背包）相关接口在 **scripts/** 内实现，不依赖项目外层 api/Client。称号与背包查询均在 **get_bag_number.py**，只是校验方法不同。

## 1. 送礼接口

- **实现位置**：`scripts/gift_client.py` 的 `send_gift(uid, recv_uid, gift_id, number=1)`（直接 requests 请求活动 API，并解析 RewardNameMap 为可读奖励列表）
- **参数**：
  - `uid`：送礼用户 ID
  - `recv_uid`：接收礼物用户 ID
  - `gift_id`：礼物 ID
  - `number`：赠送数量，默认 1
- **返回**：成功时 `{"code": 0, "msg": "ok", "data": ["奖励名x数量", ...]}`；异常或业务失败时 `{"code": <非0>, "msg": "...", "data": None}`

**配置**：API 地址在 `gift_client.py` 顶部 `SEND_GIFT_URL`，支持环境变量 `SEND_GIFT_URL` 覆盖。若出现 502 或与「正常送礼」接口不一致，请确认 URL 是否与业务使用的接口一致；其它接口（称号、背包、清空）均使用 `wespy-admin-api-dev.wepieoa.com`，送礼接口默认使用 `wespylogin2-dev.afunapp.com`，可按需统一。

**调用示例**（任意目录执行，只要能找到 scripts 或已把 skill 根加入 path）：

```bash
python <当前skill目录>/scripts/gift_client.py <uid> <recv_uid> <gift_id> [number]
```

```python
import sys, os
skill_root = os.environ.get("SEND_GIFT_GET_PROP_SKILL_DIR", "<当前skill目录>")
sys.path.insert(0, skill_root)
from scripts.gift_client import send_gift
r = send_gift(162819971, 162819972, 122384, 1)
assert r.get("code") == 0
print(r.get("data"))  # 如 ["暖暖相依礼物卡x1"]
```

## 2. 配置说明

- **送礼**：`scripts/gift_client.py` 顶部 `SEND_GIFT_URL`，可按需改为配置或环境变量。
- **依赖**：`pip install -r scripts/requirements.txt`（requests）。

## 3. 道具到账校验（称号 + 背包，同一文件）

**实现**：`scripts/get_bag_number.py`。称号与背包道具都在此文件，只是方法不同。

### 3.1 称号类（prop 的一种）

- **方法**：`get_title_number(uid, title_name)` — 查用户某称号的剩余时长
  - 返回：`{"code": 0, "title_name": str, "remain_time_string": str}` 或失败时 `{"code": -1, "msg": str, ...}`
- **方法**：`get_title_remain_days(uid, title_name)` — 从 remain_time_string 解析出整数天数（如 "10天" → 10），无法解析返回 -1

**配置**：API 地址与请求头在文件顶部 `TITLE_API_URL`、`TITLE_HEADERS`。

### 3.2 背包道具类（prop 的一种）

- **方法**：`get_prop_number(uid, prop_name, prop_id, select_type=1)` — 查用户背包中某道具的数量（或按剩余时长折算的天数）
  - 返回：数量（int），未查到或异常返回 0

**配置**：背包 API 在文件内 `url = "https://wespy-admin-api-dev.wepieoa.com/bag_api/select"`。

**命令行**：当前 `get_bag_number.py` 的 `__main__` 为称号查询；道具查询请在代码中调用 `get_prop_number(uid, prop_name, prop_id)`。

## 4. 场景：送 x 个礼物后获得 xxx 称号 N 天（含到账校验）

- **实现**：`scripts/verify_gift_title.py`
- **流程**：先送 x 次礼，再按「称号归属」查对应用户的称号是否到账及剩余天数是否满足预期。
- **称号归属**：`title_recipient` 为 **sender/送礼人** 时校验送礼人(uid)的称号，为 **receiver/收礼人** 时校验收礼人(recv_uid)的称号（默认）。
- **参数**：`uid, recv_uid, gift_id, send_count, expected_title_substr, expected_days`；可选 `title_recipient="sender"|"receiver"`。
- **返回**：`{"ok": bool, "msg": str, "send_ok": bool, "matched": str|None, "title_check": {...}, "title_recipient": str, "last_data": list}`

**命令行示例**：

```bash
# 收礼人获得称号（默认）
python <当前skill目录>/scripts/verify_gift_title.py 162819971 162819972 122384 10 宝石达人 10

# 送礼人获得称号
python <当前skill目录>/scripts/verify_gift_title.py 162819972 162819971 122456 10 我爱听歌 1 sender
```

**代码示例**：

```python
from scripts.verify_gift_title import verify_gift_title

# 收礼人得称号
r = verify_gift_title(162819971, 162819972, "122384", 10, "宝石达人", 10)
# 送礼人得称号
r = verify_gift_title(162819972, 162819971, "122456", 10, "我爱听歌", 1, title_recipient="sender")
assert r["ok"], r["msg"]
```

## 5. 玩法用例（换礼物、换规则即可测）

**含义**：测试「**某礼物**」的玩法——**收到 x 个**或**送 x 个** → **谁**获得 **xxx 称号 x 天**。用例只描述玩法，不写死账号；账号用默认或你当次提供。

**对 AI 怎么说**（你只需说玩法和要测的礼物）：

- 「帮我测试一下 **122456** 礼物，玩法是**送 10 个**可以获得**我爱听歌**称号 **1** 天（**送礼人**得）。」
- 「测 **122384**：**收满 10 个**得**宝石达人**称号 **10** 天（**收礼人**得）。」

AI 会按玩法跑 `verify_gift_title`（称号归属：送满→多为 sender，收满→多为 receiver），uid/recv_uid 用你提供的或 skill 里默认的。

**用例文件**：`scripts/test_cases_title.json`。每条用例只写**玩法**，不写 uid/recv_uid 则用文件顶部的 `default_uid`、`default_recv_uid`。

**单条玩法用例格式**（可只写玩法，不写 uid/recv_uid）：

```json
{
  "name": "122456-送满10个得我爱听歌1天",
  "gift_id": "122456",
  "trigger": "送10个",
  "count_trigger": 10,
  "title_recipient": "sender",
  "expected_title": "我爱听歌",
  "expected_days": 1
}
```

**运行**：`python scripts/run_title_cases.py [用例文件]`；可选 `--uid`、`--recv` 指定当次账号，`-q` 仅打摘要。未指定时优先从 **test_user.json** 取第一对（男=送礼人，女=收礼人）。

### 5.1 两种验证模式（按玩法选择）

- **只验证阈值**：玩法为「每收/送 x 个得 **某个称号 x 天**」（单一固定奖励）时，只跑阈值场景（送 n-1 无、送 n 有）。用 **run_rule_suite.py** 或 **verify_gift_title.py**，或用 **verify_gift_prob_rewards.py --threshold-only**（配置通过 `--config -` 或文件传入）。
- **阈值 + 释出**：玩法为「每收/送 x 个得 **n 个礼物**」（多选一/多种奖励）时，先验证阈值，再验证**每种奖励都至少释出一次**。用 **verify_gift_prob_rewards.py --full**，默认最多送 **2000** 个礼物（`--max-gifts 2000`）；若超过 2000 个仍未全部释出则**失败**。
- **大批量跑测结束后验证**：跑完 `--full` 后，**不要重跑**，直接用 **--query-only** 查收礼人背包/称号即可。同配置 + `--pair N` 指定收礼人组。
- **兜底策略**：当 `--full --max-gifts 4000` 等长耗时测试跑完但**未捕获到输出**时，**直接执行 --query-only** 验证，不要重跑测试。
- 配置通过 stdin 或文件传入，无需改代码：`echo '{"gift_id":"122480","trigger_count":10,"reward_options":[...]}' | python verify_gift_prob_rewards.py --config - --threshold-only` 或 `--full` 或 `--query-only`。

## 6. 测试账号 test_user.json

- **路径**：`scripts/test_user.json`
- **格式**：`{"男性uid": "女性uid", ...}`，key 为男性 uid，value 为女性 uid。
- **约定**：测试时默认 **男性 = 送礼人(uid)**，**女性 = 收礼人(recv_uid)**。
- **使用**：`run_rule_suite.py`、`run_title_cases.py` 在不传 `--uid`/`--recv` 时，会从该文件读取**第一对**作为默认账号；`run_rule_suite` 可用 `--pair N` 指定第 N 对（0 为第一对），场景 5 的「另一用户」未指定时用第二对女性。

**规则：同一组账号不能复用在不同玩法上。** 若同一礼物有多条玩法（如「收礼人收满 10 得称号 A」「送礼人送满 20 得称号 B」），**每条玩法必须用不同组账号**：先测收礼人规则用的那一组，在测送礼人规则时不能再使用，应换 **--pair 1**（第二组）或下一组。否则送礼人账号会因已送过很多礼而被「污染」（例如已触发送满 20 得称号 B），导致后续测送礼人规则时场景 1（送 9 次预期无称号）失败。

## 7. 测试前添加好友（add_friend）

- **脚本**：`scripts/add_friend.py`，方法 `add_friend(uid1, uid2)`，用于添加指定双向好友（调用后台「添加指定双向好友」）。
- **使用时机**：若 test_user 中某对不是好友，送礼会报「还不是好友，无法送礼物」；测试前先加好友可避免该问题。
- **run_rule_suite**：默认会先对「送礼人–收礼人」「送礼人–场景5另一用户」调用 `add_friend`，再清空称号、再给送礼人加金币、再跑 5 场景；加 `--no-add-friend` 则不加好友。
- **verify_gift_title**：参数 `ensure_friends=True` 时会在送礼前对 uid 与 recv_uid 调用 `add_friend`；`run_title_cases.py` 加 `--add-friend` 时会对每条用例传 `ensure_friends=True`。

## 8. 测试前清空称号（clear_user_prop）

- **脚本**：`scripts/clear_user_prop.py`，方法 `clear_user_title(uid)`，用于清空对应用户的称号与金币（调用后台「清空用户道具」）。
- **使用时机**：测试前清空送礼人、收礼人的称号，保证从干净状态验证到账。
- **run_rule_suite 默认**：跑测时**先清空**送礼人、收礼人、场景5另一用户的称号，**再加金币**给送礼人，**最后跑 5 场景**，一条命令完成，无需先单独执行清空/加金币。加 `--no-clear` 或 `--no-add-gold` 可跳过对应步骤。
- **单独执行**（仅当套件内清空/加金币未生效时备用）：
  ```bash
  python <当前skill目录>/scripts/clear_user_prop.py <送礼人uid> <收礼人uid>
  python <当前skill目录>/scripts/add_gold_coin.py <送礼人uid>
  ```
  再跑 `run_rule_suite.py ... --no-clear --no-add-gold`。

## 8.1 测试前给送礼人加金币（add_gold_coin）

- **脚本**：`scripts/add_gold_coin.py`，方法 `add_gold_coin(uid, amount=100000000)`，给指定用户增加金币（调用后台「修改金币」）。
- **使用时机**：确保送礼人金币足够，避免测试中因金币不足送礼失败。
- **单独执行**（仅当套件内未生效时备用）：`python .../add_gold_coin.py <送礼人uid> [金额]`，默认 100000000。
- **run_rule_suite**：默认在清空**之后**、跑场景**之前**对**送礼人**加金币 100000000；加 `--no-add-gold` 则不加，`--add-gold-amount N` 可改金额。

## 8.2 备用：套件内清空/加金币未生效时

默认应**直接跑 run_rule_suite**（会先清空、再加金币、再跑 5 场景）。若你环境里套件内清空或加金币不生效，再改为先单独执行清空与加金币，再用 `--no-clear --no-add-gold` 跑 5 场景（示例见第 8 节单独执行命令）。

## 9. 完整规则测试（5 场景）

当你说「测一下 xxx 礼物，送 10 个就可以获得 xx 称号 1 天」时，应跑**完整 5 场景**，而不是只跑一次送 10 个。

**默认流程**：加好友 → 清空送礼人+收礼人+场景5另一用户称号 → 给送礼人加 100000000 金币 → 跑 5 场景。

**脚本**：`scripts/run_rule_suite.py`

**5 个场景**（由 `--trigger`、`--days` 决定，支持满 10、满 20 等）：

| 场景 | 操作 | 预期 |
|------|------|------|
| 1 | 每次送 1 个，送 (trigger-1) 次 | **不**获得称号 |
| 2 | 再送 1 次（共 trigger 次） | 获得称号至少 days 天 |
| 3 | 一次性送 trigger 个 | 获得称号 |
| 4 | 一次性送 66 个 | 至少 66÷trigger 天（取整） |
| 5 | 给**另一用户**送 n1+n2=trigger 个（1 个 1 个送） | 称号归属方获得称号 |

**命令行**（不传 `--uid`/`--recv` 时自动用 **test_user.json** 第一对：男=送礼人，女=收礼人）：

```bash
python scripts/run_rule_suite.py <礼物ID> <称号名> [--trigger 10] [--days 1] [--uid 送礼人] [--recv 收礼人] [--pair 0] [--other 场景5另一用户] [--who sender|receiver]
# 使用 test_user.json 第一对（默认会先加好友、再清空称号，再跑 5 场景）
python scripts/run_rule_suite.py 122456 我爱听歌
# 不加好友 / 不清空 / 不加金币，直接测
python scripts/run_rule_suite.py 122456 我爱听歌 --no-add-friend --no-clear --no-add-gold
# 指定加金币金额（默认 100000000）
python scripts/run_rule_suite.py 122456 我爱听歌 --add-gold-amount 200000000
# 指定第 2 对
python scripts/run_rule_suite.py 122456 我爱听歌 --pair 1
```

`--who receiver` 表示「收满 N 个收礼人得称号」。

## 10. 场景：送/收 x 礼得 x 个道具（背包类 prop）

与「送 x 礼得称号」流程相同，只是到账校验用 **get_prop_number**：送满 x 次后，对获得方（送礼人或收礼人）调用 `get_prop_number(uid, prop_name, prop_id)`，核对背包中该道具数量 ≥ 预期即可。

## 11. 道具规则 4 场景（按玩法阈值动态填入）

适用于「送 N 个该礼物获得 M 天/个某道具」的标准化测试，**根据 trigger、per_trigger 动态计算预期值**。

**脚本**：`scripts/run_prop_rule_suite.py`

**默认**：跑测前会先清空送礼人、收礼人、另一用户的**道具**（`clear_user_props`），再跑 4 场景，否则场景1（送 N-1 个双方无该道具）易因历史道具失败；加 `--no-clear` 则不清空。

**4 个场景**（由 `trigger`、`per_trigger` 决定）：

| 场景 | 操作 | 预期 |
|------|------|------|
| 1 | 送 **(trigger-1)** 个该礼物 | **送礼人、收礼人都不会**获得该道具 |
| 2 | 在 1 的基础上再送 1 个（共 **trigger** 个） | 归属方获得 **per_trigger** 天/个道具 |
| 3 | 给**不同的人**送满 **trigger** 个 | 该人获得 **per_trigger** 天/个道具 |
| 4 | 给**同一个人**送 **10** 个该礼物 | 获得 **10÷trigger×per_trigger** 天/个道具 |

**命令行**（参数即玩法阈值，自动填入场景数值）：

```bash
python scripts/run_prop_rule_suite.py <礼物ID> <trigger> <per_trigger> <prop_id> <prop_name> [--recipient receiver|sender] [--uid] [--recv] [--other] [--pair 0]
```

**示例**：送 2 个得 2 天头像框「烟花告白」ID 1206245（收礼人得）

```bash
python scripts/run_prop_rule_suite.py 121008 2 2 1206245 烟花告白 --recipient receiver
```

**示例**：送 10 个得 1 天某道具（送礼人得）

```bash
python scripts/run_prop_rule_suite.py <礼物ID> 10 1 <prop_id> <prop_name> --recipient sender
```

不传 `--uid`/`--recv` 时从 **test_user.json** 取第 `--pair` 对；场景 3 的「不同的人」不传 `--other` 时用第二对女性。

---

AI 执行礼物测试时，会使用上述 `send_gift`、`verify_gift_title`、`get_title_number` / `get_title_remain_days`、`get_prop_number`、**run_prop_rule_suite** 进行送礼与道具（称号/背包）到账校验。
