# ============================================================ # 测试:卡导入 → 分配 → C 端价格验证 # 生成时间:2026-03-30 # 涉及模块:auth, iot_card(import), shop, package_series, package, # shop_series_grant, shop_package_allocation, client_auth, client_asset # ============================================================ # 流程: # 1. 平台管理员登录 # 2. 查询运营商列表,获取 carrier_id # 3. 获取对象存储上传 URL(purpose=iot_import) # 4. 上传 Excel 文件(IoT 卡测试数据)到预签名 URL # 5. 提交卡导入任务 # 6. 轮询导入任务直到完成(status=3) # 7. 获取一级代理店铺 ID(shop1_id) # 8. 获取二级代理店铺 ID(shop2_id) # 9. 获取套餐系列 ID(series_id)和套餐 ID(package_id) # 10. 分配 ICCID_1 给一级代理(shop1) # 11. 分配 ICCID_2 给二级代理(shop2) # 12. 绑定两张卡到套餐系列 # 13. C 端测试登录(ICCID_1) # 14. 验证 ICCID_1 的可购套餐 → retail_price = 一级代理零售价(15000) # 15. C 端测试登录(ICCID_2) # 16. 验证 ICCID_2 的可购套餐 → retail_price = 二级代理零售价(18000) # ============================================================ # 前置条件: # 1. 已运行 package-resource-full-flow.hurl(创建了 shop1/shop2/series/package) # 2. 开启 logging.development=true(服务配置) # 3. 已配置对象存储(S3/MinIO),步骤 3-4 依赖对象存储上传 # 4. env 文件中已配置 agent1_username/agent2_username # ============================================================ # ══════════════════════════════════════════════════════════════ # 第 1 步:平台管理员登录 # ══════════════════════════════════════════════════════════════ POST {{base_url}}/api/auth/login Content-Type: application/json { "username": "{{admin_username}}", "password": "{{admin_password}}", "device": "web" } HTTP 200 [Captures] admin_token: jsonpath "$.data.access_token" [Asserts] jsonpath "$.code" == 0 jsonpath "$.data.access_token" isString jsonpath "$.data.user.user_type" == 1 # ══════════════════════════════════════════════════════════════ # 第 2 步:查询运营商列表,获取第一个可用 carrier_id # ══════════════════════════════════════════════════════════════ GET {{base_url}}/api/admin/carriers?page=1&page_size=1&status=1 Authorization: Bearer {{admin_token}} HTTP 200 [Captures] carrier_id: jsonpath "$.data.items[0].id" [Asserts] jsonpath "$.code" == 0 jsonpath "$.data.items" count >= 1 jsonpath "$.data.items[0].id" isInteger # ══════════════════════════════════════════════════════════════ # 第 3 步:获取对象存储上传 URL(IoT 卡导入 Excel) # ══════════════════════════════════════════════════════════════ POST {{base_url}}/api/admin/storage/upload-url Authorization: Bearer {{admin_token}} Content-Type: application/json { "file_name": "hurl-test-cards.xlsx", "content_type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "purpose": "iot_import" } HTTP 200 [Captures] upload_url: jsonpath "$.data.upload_url" file_key: jsonpath "$.data.file_key" [Asserts] jsonpath "$.code" == 0 jsonpath "$.data.upload_url" isString jsonpath "$.data.file_key" isString jsonpath "$.data.expires_in" isInteger # ══════════════════════════════════════════════════════════════ # 第 4 步:上传 Excel 文件到预签名 URL # 文件路径相对于 Hurl 运行目录(tests/hurl/) # ══════════════════════════════════════════════════════════════ PUT {{upload_url}} Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet file,testdata/hurl-test-cards.xlsx; HTTP 200 # ══════════════════════════════════════════════════════════════ # 第 5 步:提交 IoT 卡导入任务 # 导入 Excel 包含 ICCID_1(HURL-TEST-CARD-001)和 ICCID_2(HURL-TEST-CARD-002) # ══════════════════════════════════════════════════════════════ POST {{base_url}}/api/admin/iot-cards/import Authorization: Bearer {{admin_token}} Content-Type: application/json { "carrier_id": {{carrier_id}}, "batch_no": "HURL-PRICE-TEST", "file_key": "{{file_key}}" } HTTP 200 [Captures] import_task_id: jsonpath "$.data.task_id" [Asserts] jsonpath "$.code" == 0 jsonpath "$.data.task_id" isInteger jsonpath "$.data.task_no" isString # ══════════════════════════════════════════════════════════════ # 第 6 步:轮询导入任务,等待完成(status=3 表示已完成) # 注:Hurl 不支持循环轮询,这里做 3 次间隔查询覆盖大多数场景 # 如需更长等待,可手动重跑此步骤 # ══════════════════════════════════════════════════════════════ # 第 1 次查询(立即) GET {{base_url}}/api/admin/iot-cards/import-tasks/{{import_task_id}} Authorization: Bearer {{admin_token}} HTTP 200 [Asserts] jsonpath "$.code" == 0 jsonpath "$.data.id" == {{import_task_id}} jsonpath "$.data.status" isInteger # 等待 2 秒后第 2 次查询(Hurl 通过 delay 选项实现) GET {{base_url}}/api/admin/iot-cards/import-tasks/{{import_task_id}} Authorization: Bearer {{admin_token}} [Options] delay: 2000 HTTP 200 [Asserts] jsonpath "$.code" == 0 # 等待 2 秒后第 3 次查询,断言任务成功完成 GET {{base_url}}/api/admin/iot-cards/import-tasks/{{import_task_id}} Authorization: Bearer {{admin_token}} [Options] delay: 2000 HTTP 200 [Asserts] jsonpath "$.code" == 0 jsonpath "$.data.status" == 3 jsonpath "$.data.success_count" >= 1 # ══════════════════════════════════════════════════════════════ # 第 7 步:获取一级代理店铺 ID(shop1_id) # 依赖 package-resource-full-flow.hurl 已创建 agent1_username 账号 # ══════════════════════════════════════════════════════════════ GET {{base_url}}/api/admin/accounts?username={{agent1_username}}&page=1&page_size=1 Authorization: Bearer {{admin_token}} HTTP 200 [Captures] shop1_id: jsonpath "$.data.items[0].shop_id" [Asserts] jsonpath "$.code" == 0 jsonpath "$.data.items" count >= 1 jsonpath "$.data.items[0].username" == "{{agent1_username}}" jsonpath "$.data.items[0].user_type" == 3 jsonpath "$.data.items[0].shop_id" isInteger # ══════════════════════════════════════════════════════════════ # 第 8 步:获取二级代理店铺 ID(shop2_id) # ══════════════════════════════════════════════════════════════ GET {{base_url}}/api/admin/accounts?username={{agent2_username}}&page=1&page_size=1 Authorization: Bearer {{admin_token}} HTTP 200 [Captures] shop2_id: jsonpath "$.data.items[0].shop_id" [Asserts] jsonpath "$.code" == 0 jsonpath "$.data.items" count >= 1 jsonpath "$.data.items[0].username" == "{{agent2_username}}" jsonpath "$.data.items[0].user_type" == 3 jsonpath "$.data.items[0].shop_id" isInteger # ══════════════════════════════════════════════════════════════ # 第 9 步:获取套餐系列 ID 和套餐 ID # 依赖 package-resource-full-flow.hurl 创建的 "Hurl测试系列" # ══════════════════════════════════════════════════════════════ # 获取套餐系列(按名称模糊匹配,取最新创建的) GET {{base_url}}/api/admin/package-series?series_name=Hurl%E6%B5%8B%E8%AF%95%E7%B3%BB%E5%88%97&page=1&page_size=1 Authorization: Bearer {{admin_token}} HTTP 200 [Captures] series_id: jsonpath "$.data.items[0].id" [Asserts] jsonpath "$.code" == 0 jsonpath "$.data.items" count >= 1 jsonpath "$.data.items[0].series_name" == "Hurl测试系列" # 获取该系列下的套餐(取最新的) GET {{base_url}}/api/admin/packages?series_id={{series_id}}&page=1&page_size=1 Authorization: Bearer {{admin_token}} HTTP 200 [Captures] package_id: jsonpath "$.data.items[0].id" [Asserts] jsonpath "$.code" == 0 jsonpath "$.data.items" count >= 1 jsonpath "$.data.items[0].series_id" == {{series_id}} # ══════════════════════════════════════════════════════════════ # 第 10 步:分配 ICCID_1(8986001234560001)给一级代理(shop1) # 幂等处理:HTTP * 兼容已分配场景 # ══════════════════════════════════════════════════════════════ POST {{base_url}}/api/admin/iot-cards/standalone/allocate Authorization: Bearer {{admin_token}} Content-Type: application/json { "to_shop_id": {{shop1_id}}, "selection_type": "list", "iccids": ["{{test_iccid_1}}"], "remark": "Hurl 价格验证测试 - 一级代理" } HTTP * [Asserts] jsonpath "$.code" isInteger # ══════════════════════════════════════════════════════════════ # 第 11 步:分配 ICCID_2(8986001234560002)给二级代理(shop2) # ══════════════════════════════════════════════════════════════ POST {{base_url}}/api/admin/iot-cards/standalone/allocate Authorization: Bearer {{admin_token}} Content-Type: application/json { "to_shop_id": {{shop2_id}}, "selection_type": "list", "iccids": ["{{test_iccid_2}}"], "remark": "Hurl 价格验证测试 - 二级代理" } HTTP * [Asserts] jsonpath "$.code" isInteger # ══════════════════════════════════════════════════════════════ # 第 12 步:绑定两张卡到套餐系列(series_binding) # 卡需要绑定套餐系列才能在 C 端看到可购套餐 # ══════════════════════════════════════════════════════════════ PATCH {{base_url}}/api/admin/iot-cards/series-binding Authorization: Bearer {{admin_token}} Content-Type: application/json { "iccids": ["{{test_iccid_1}}", "{{test_iccid_2}}"], "series_id": {{series_id}} } HTTP 200 [Asserts] jsonpath "$.code" == 0 jsonpath "$.data.success_count" >= 1 # ══════════════════════════════════════════════════════════════ # 第 13 步:C 端测试登录(ICCID_1,归属一级代理) # 调用开发环境专用接口,传入 ICCID 自动绑定测试客户并签发 JWT # ══════════════════════════════════════════════════════════════ POST {{base_url}}/api/c/v1/auth/dev-login Content-Type: application/json { "identifier": "{{test_iccid_1}}" } HTTP 200 [Captures] customer1_token: jsonpath "$.data.token" customer1_id: jsonpath "$.data.customer_id" [Asserts] jsonpath "$.code" == 0 jsonpath "$.data.token" isString jsonpath "$.data.customer_id" isInteger # ══════════════════════════════════════════════════════════════ # 第 14 步:验证 ICCID_1 的可购套餐价格 = 一级代理零售价(15000 分) # retail_price 来自一级代理在 shop_package_allocation 中设置的零售价 # 前置条件:package-resource-full-flow.hurl 中一级代理已设置零售价 15000 # ══════════════════════════════════════════════════════════════ GET {{base_url}}/api/c/v1/asset/packages?identifier={{test_iccid_1}} Authorization: Bearer {{customer1_token}} HTTP 200 [Asserts] jsonpath "$.code" == 0 jsonpath "$.data.packages" isList jsonpath "$.data.packages" count >= 1 # 关键断言:一级代理归属的卡,C 端看到的是一级代理设置的零售价 15000 jsonpath "$.data.packages[0].retail_price" == 15000 jsonpath "$.data.packages[0].package_id" == {{package_id}} # ══════════════════════════════════════════════════════════════ # 第 15 步:C 端测试登录(ICCID_2,归属二级代理) # ══════════════════════════════════════════════════════════════ POST {{base_url}}/api/c/v1/auth/dev-login Content-Type: application/json { "identifier": "{{test_iccid_2}}" } HTTP 200 [Captures] customer2_token: jsonpath "$.data.token" customer2_id: jsonpath "$.data.customer_id" [Asserts] jsonpath "$.code" == 0 jsonpath "$.data.token" isString jsonpath "$.data.customer_id" isInteger # ══════════════════════════════════════════════════════════════ # 第 16 步:验证 ICCID_2 的可购套餐价格 = 二级代理零售价(18000 分) # ══════════════════════════════════════════════════════════════ GET {{base_url}}/api/c/v1/asset/packages?identifier={{test_iccid_2}} Authorization: Bearer {{customer2_token}} HTTP 200 [Asserts] jsonpath "$.code" == 0 jsonpath "$.data.packages" isList jsonpath "$.data.packages" count >= 1 # 关键断言:二级代理归属的卡,C 端看到的是二级代理设置的零售价 18000(与一级代理不同!) jsonpath "$.data.packages[0].retail_price" == 18000 jsonpath "$.data.packages[0].package_id" == {{package_id}} # ══════════════════════════════════════════════════════════════ # 第 17 步:用两个 token 访问对方的卡(越权验证) # customer1 不应能查询 ICCID_2 的套餐(未绑定该资产) # ══════════════════════════════════════════════════════════════ GET {{base_url}}/api/c/v1/asset/packages?identifier={{test_iccid_2}} Authorization: Bearer {{customer1_token}} HTTP 403 [Asserts] jsonpath "$.code" == 1005 # ══════════════════════════════════════════════════════════════ # 第 18 步:验证同一套餐两个代理视角价格确实不同(一致性检查) # ══════════════════════════════════════════════════════════════ GET {{base_url}}/api/c/v1/asset/info?identifier={{test_iccid_1}} Authorization: Bearer {{customer1_token}} HTTP 200 [Asserts] jsonpath "$.code" == 0 jsonpath "$.data.asset_type" == "card" jsonpath "$.data.iccid" == "{{test_iccid_1}}" GET {{base_url}}/api/c/v1/asset/info?identifier={{test_iccid_2}} Authorization: Bearer {{customer2_token}} HTTP 200 [Asserts] jsonpath "$.code" == 0 jsonpath "$.data.asset_type" == "card" jsonpath "$.data.iccid" == "{{test_iccid_2}}"