# ============================================================ # 测试:套餐资源完整流程(创建→分配→佣金→调价→强充) # 生成时间:2026-03-30 # 涉及模块:auth, role, package_series, package, shop, # shop_series_grant, batch_allocation, batch_pricing # 涉及接口:25+ 个 # 前置条件:服务运行 + 数据库已迁移 + Redis 可用 # ============================================================ # 流程: # 1. 平台管理员登录 # 2. 创建客户角色(创建店铺的前置依赖) # 3. 创建套餐系列(含固定一次性佣金 + 强充配置) # 4. 创建套餐 → 启用 → 上架 # 5. 创建一级代理店铺(自动创建初始账号) # 6. 平台→一级代理:创建系列授权 + 管理授权套餐 # 7. 创建二级代理店铺(挂在一级代理下) # 8. 一级代理登录 → 给二级代理分配系列授权 # 9. 各级代理修改零售价 → 验证价格隔离 # 10. 平台:批量分配 + 批量调价 # 11. 代理佣金概览验证 # 12. 强充配置验证 # 13. 异常测试(未认证、参数校验、越权) # ============================================================ # ══════════════════════════════════════════════════════════════ # 第 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 "$.msg" == "success" jsonpath "$.timestamp" isString jsonpath "$.data.access_token" isString jsonpath "$.data.refresh_token" isString jsonpath "$.data.expires_in" isInteger jsonpath "$.data.user.id" isInteger jsonpath "$.data.user.username" isString jsonpath "$.data.user.user_type" == 1 # ══════════════════════════════════════════════════════════════ # 第 2 步:创建客户角色(创建店铺需要 default_role_id) # ══════════════════════════════════════════════════════════════ POST {{base_url}}/api/admin/roles Authorization: Bearer {{admin_token}} Content-Type: application/json { "role_name": "hurl测试角色-{{newUuid}}", "role_desc": "hurl 自动化测试用客户角色", "role_type": 2 } HTTP 200 [Captures] test_role_id: jsonpath "$.data.id" [Asserts] jsonpath "$.code" == 0 jsonpath "$.data.id" isInteger jsonpath "$.data.role_type" == 2 jsonpath "$.data.status" == 1 # ══════════════════════════════════════════════════════════════ # 第 3 步:创建套餐系列(固定一次性佣金 + 强充配置) # ══════════════════════════════════════════════════════════════ POST {{base_url}}/api/admin/package-series Authorization: Bearer {{admin_token}} Content-Type: application/json { "series_code": "HURL-SERIES-{{newUuid}}", "series_name": "Hurl测试系列", "description": "自动化测试用套餐系列", "enable_one_time_commission": true, "one_time_commission_config": { "enable": true, "trigger_type": "first_recharge", "threshold": 0, "commission_type": "fixed", "commission_amount": 5000, "tiers": [], "validity_type": "permanent", "validity_value": "", "enable_force_recharge": true, "force_calc_type": "fixed", "force_amount": 5000 } } HTTP 200 [Captures] series_id: jsonpath "$.data.id" [Asserts] jsonpath "$.code" == 0 jsonpath "$.data.id" isInteger jsonpath "$.data.series_code" isString jsonpath "$.data.series_name" == "Hurl测试系列" jsonpath "$.data.description" == "自动化测试用套餐系列" jsonpath "$.data.enable_one_time_commission" == true jsonpath "$.data.one_time_commission_config" isObject jsonpath "$.data.one_time_commission_config.enable" == true jsonpath "$.data.one_time_commission_config.commission_type" == "fixed" jsonpath "$.data.one_time_commission_config.commission_amount" == 5000 jsonpath "$.data.one_time_commission_config.enable_force_recharge" == true jsonpath "$.data.one_time_commission_config.force_calc_type" == "fixed" jsonpath "$.data.one_time_commission_config.force_amount" == 5000 jsonpath "$.data.status" == 1 jsonpath "$.data.created_at" isString jsonpath "$.data.updated_at" isString # ══════════════════════════════════════════════════════════════ # 第 4a 步:创建套餐(关联到系列) # ══════════════════════════════════════════════════════════════ POST {{base_url}}/api/admin/packages Authorization: Bearer {{admin_token}} Content-Type: application/json { "package_code": "HURL-PKG-{{newUuid}}", "package_name": "Hurl测试套餐-月包30G", "series_id": {{series_id}}, "package_type": "formal", "duration_months": 12, "real_data_mb": 30720, "virtual_data_mb": 0, "enable_virtual_data": false, "suggested_retail_price": 15000, "cost_price": 8000, "calendar_type": "natural_month", "data_reset_cycle": "monthly", "expiry_base": "from_activation" } HTTP 200 [Captures] package_id: jsonpath "$.data.id" package_code: jsonpath "$.data.package_code" [Asserts] jsonpath "$.code" == 0 jsonpath "$.data.id" isInteger jsonpath "$.data.package_code" isString jsonpath "$.data.package_name" == "Hurl测试套餐-月包30G" jsonpath "$.data.series_id" == {{series_id}} jsonpath "$.data.package_type" == "formal" jsonpath "$.data.duration_months" == 12 jsonpath "$.data.real_data_mb" == 30720 jsonpath "$.data.virtual_data_mb" == 0 jsonpath "$.data.enable_virtual_data" == false jsonpath "$.data.suggested_retail_price" == 15000 jsonpath "$.data.cost_price" == 8000 jsonpath "$.data.calendar_type" == "natural_month" jsonpath "$.data.data_reset_cycle" == "monthly" jsonpath "$.data.expiry_base" == "from_activation" jsonpath "$.data.status" isInteger jsonpath "$.data.shelf_status" isInteger jsonpath "$.data.created_at" isString jsonpath "$.data.updated_at" isString # ── 第 4b 步:启用套餐 ── PATCH {{base_url}}/api/admin/packages/{{package_id}}/status Authorization: Bearer {{admin_token}} Content-Type: application/json { "status": 1 } HTTP 200 [Asserts] jsonpath "$.code" == 0 # ── 第 4c 步:上架套餐 ── PATCH {{base_url}}/api/admin/packages/{{package_id}}/shelf Authorization: Bearer {{admin_token}} Content-Type: application/json { "shelf_status": 1 } HTTP 200 [Asserts] jsonpath "$.code" == 0 # ── 第 4d 步:验证套餐详情(启用+上架后) ── GET {{base_url}}/api/admin/packages/{{package_id}} Authorization: Bearer {{admin_token}} HTTP 200 [Asserts] jsonpath "$.code" == 0 jsonpath "$.data.id" == {{package_id}} jsonpath "$.data.status" == 1 jsonpath "$.data.shelf_status" == 1 # ══════════════════════════════════════════════════════════════ # 第 5 步:创建一级代理店铺(自动创建初始账号) # 幂等处理:如果已存在则跳过创建,通过账号查询获取 shop_id # ══════════════════════════════════════════════════════════════ POST {{base_url}}/api/admin/shops Authorization: Bearer {{admin_token}} Content-Type: application/json { "shop_name": "Hurl一级代理店铺", "shop_code": "HURL-SHOP1-{{newUuid}}", "contact_name": "测试联系人1", "contact_phone": "13800000001", "province": "湖南省", "city": "长沙市", "district": "岳麓区", "address": "测试地址一级", "default_role_id": {{test_role_id}}, "init_password": "{{agent1_password}}", "init_username": "{{agent1_username}}", "init_phone": "{{agent1_phone}}" } HTTP * # 通过账号查询获取 shop_id(无论创建成功或已存在) 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 # ══════════════════════════════════════════════════════════════ # 第 6a 步:平台→一级代理 创建系列授权(设置固定佣金 + 强充) # 幂等处理:如已存在则跳过,通过列表查询获取 grant_id # ══════════════════════════════════════════════════════════════ # [FINDING C-3] 创建授权接口:请求传了 packages 但创建响应不含 packages 数组 # 需通过 GET 详情才能看到 packages,这里用 HTTP * 兼容幂等(重跑时可能 409) POST {{base_url}}/api/admin/shop-series-grants Authorization: Bearer {{admin_token}} Content-Type: application/json { "shop_id": {{shop1_id}}, "series_id": {{series_id}}, "one_time_commission_amount": 3000, "enable_force_recharge": true, "force_recharge_amount": 5000, "packages": [ { "package_id": {{package_id}}, "cost_price": 10000 } ] } HTTP * # 查询获取 grant_id(无论创建成功或已存在) GET {{base_url}}/api/admin/shop-series-grants?shop_id={{shop1_id}}&series_id={{series_id}}&page=1&page_size=1 Authorization: Bearer {{admin_token}} HTTP 200 [Captures] grant1_id: jsonpath "$.data.items[0].id" [Asserts] jsonpath "$.code" == 0 jsonpath "$.data.items" count >= 1 jsonpath "$.data.items[0].shop_id" == {{shop1_id}} # ── 第 6b 步:验证一级代理授权详情 ── GET {{base_url}}/api/admin/shop-series-grants/{{grant1_id}} Authorization: Bearer {{admin_token}} HTTP 200 [Asserts] jsonpath "$.code" == 0 jsonpath "$.data.id" == {{grant1_id}} jsonpath "$.data.shop_id" == {{shop1_id}} jsonpath "$.data.shop_name" == "Hurl一级代理店铺" jsonpath "$.data.series_id" == {{series_id}} jsonpath "$.data.series_name" == "Hurl测试系列" jsonpath "$.data.series_code" isString jsonpath "$.data.commission_type" == "fixed" jsonpath "$.data.one_time_commission_amount" == 3000 jsonpath "$.data.commission_tiers" isList jsonpath "$.data.force_recharge_locked" isBoolean jsonpath "$.data.force_recharge_enabled" == true jsonpath "$.data.force_recharge_amount" == 5000 jsonpath "$.data.allocator_shop_id" == 0 jsonpath "$.data.allocator_shop_name" isString jsonpath "$.data.status" == 1 jsonpath "$.data.packages" isList jsonpath "$.data.packages" count >= 1 jsonpath "$.data.packages[0].package_id" == {{package_id}} jsonpath "$.data.packages[0].package_name" == "Hurl测试套餐-月包30G" jsonpath "$.data.packages[0].package_code" isString jsonpath "$.data.packages[0].cost_price" == 10000 jsonpath "$.data.packages[0].shelf_status" isInteger jsonpath "$.data.packages[0].status" isInteger # ══════════════════════════════════════════════════════════════ # 第 7 步:创建二级代理店铺(挂在一级代理下) # 幂等处理:如果已存在则跳过创建,通过账号查询获取 shop_id # ══════════════════════════════════════════════════════════════ POST {{base_url}}/api/admin/shops Authorization: Bearer {{admin_token}} Content-Type: application/json { "shop_name": "Hurl二级代理店铺", "shop_code": "HURL-SHOP2-{{newUuid}}", "parent_id": {{shop1_id}}, "contact_name": "测试联系人2", "contact_phone": "13800000002", "province": "湖南省", "city": "长沙市", "district": "天心区", "address": "测试地址二级", "default_role_id": {{test_role_id}}, "init_password": "{{agent2_password}}", "init_username": "{{agent2_username}}", "init_phone": "{{agent2_phone}}" } HTTP * # 通过账号查询获取 shop_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 # ══════════════════════════════════════════════════════════════ # 第 8a 步:一级代理登录 # ══════════════════════════════════════════════════════════════ POST {{base_url}}/api/auth/login Content-Type: application/json { "username": "{{agent1_username}}", "password": "{{agent1_password}}", "device": "web" } HTTP 200 [Captures] agent1_token: jsonpath "$.data.access_token" agent1_shop_id: jsonpath "$.data.user.shop_id" [Asserts] jsonpath "$.code" == 0 jsonpath "$.data.access_token" isString jsonpath "$.data.user.user_type" == 3 jsonpath "$.data.user.shop_id" == {{shop1_id}} # ══════════════════════════════════════════════════════════════ # 第 8b 步:一级代理→二级代理 创建系列授权(佣金更低) # 幂等处理:如已存在则跳过,通过列表查询获取 grant_id # ══════════════════════════════════════════════════════════════ POST {{base_url}}/api/admin/shop-series-grants Authorization: Bearer {{agent1_token}} Content-Type: application/json { "shop_id": {{shop2_id}}, "series_id": {{series_id}}, "one_time_commission_amount": 2000, "enable_force_recharge": true, "force_recharge_amount": 5000, "packages": [ { "package_id": {{package_id}}, "cost_price": 12000 } ] } HTTP * # 查询获取 grant_id(无论创建成功或已存在) GET {{base_url}}/api/admin/shop-series-grants?shop_id={{shop2_id}}&series_id={{series_id}}&page=1&page_size=1 Authorization: Bearer {{agent1_token}} HTTP 200 [Captures] grant2_id: jsonpath "$.data.items[0].id" [Asserts] jsonpath "$.code" == 0 jsonpath "$.data.items" count >= 1 # ══════════════════════════════════════════════════════════════ # 第 9a 步:一级代理修改零售价 → 15000 分(150 元) # ══════════════════════════════════════════════════════════════ PATCH {{base_url}}/api/admin/packages/{{package_id}}/retail-price Authorization: Bearer {{agent1_token}} Content-Type: application/json { "retail_price": 15000 } HTTP 200 [Asserts] jsonpath "$.code" == 0 # ── 第 9b 步:一级代理查看套餐 → 验证零售价=15000 ── # [FINDING C-1] 套餐详情接口应包含 one_time_commission_amount(DTO 定义且列表接口有) GET {{base_url}}/api/admin/packages/{{package_id}} Authorization: Bearer {{agent1_token}} HTTP 200 [Asserts] jsonpath "$.code" == 0 jsonpath "$.data.id" == {{package_id}} jsonpath "$.data.package_name" == "Hurl测试套餐-月包30G" jsonpath "$.data.retail_price" == 15000 jsonpath "$.data.cost_price" == 10000 jsonpath "$.data.one_time_commission_amount" == 3000 jsonpath "$.data.profit_margin" exists # ══════════════════════════════════════════════════════════════ # 第 10a 步:二级代理登录 # ══════════════════════════════════════════════════════════════ POST {{base_url}}/api/auth/login Content-Type: application/json { "username": "{{agent2_username}}", "password": "{{agent2_password}}", "device": "web" } HTTP 200 [Captures] agent2_token: jsonpath "$.data.access_token" [Asserts] jsonpath "$.code" == 0 jsonpath "$.data.access_token" isString jsonpath "$.data.user.user_type" == 3 jsonpath "$.data.user.shop_id" == {{shop2_id}} # ══════════════════════════════════════════════════════════════ # 第 10b 步:二级代理修改零售价 → 18000 分(180 元) # ══════════════════════════════════════════════════════════════ PATCH {{base_url}}/api/admin/packages/{{package_id}}/retail-price Authorization: Bearer {{agent2_token}} Content-Type: application/json { "retail_price": 18000 } HTTP 200 [Asserts] jsonpath "$.code" == 0 # ── 第 10c 步:二级代理查看套餐 → 验证零售价=18000(与一级不同!) ── # [FINDING C-1] 同上,详情接口应包含 one_time_commission_amount GET {{base_url}}/api/admin/packages/{{package_id}} Authorization: Bearer {{agent2_token}} HTTP 200 [Asserts] jsonpath "$.code" == 0 jsonpath "$.data.id" == {{package_id}} # 关键断言:二级代理看到的零售价 != 一级代理(18000 vs 15000) jsonpath "$.data.retail_price" == 18000 jsonpath "$.data.cost_price" == 12000 jsonpath "$.data.one_time_commission_amount" == 2000 jsonpath "$.data.profit_margin" exists # ── 第 10d 步:回到一级代理视角,确认零售价未被二级覆盖 ── GET {{base_url}}/api/admin/packages/{{package_id}} Authorization: Bearer {{agent1_token}} HTTP 200 [Asserts] jsonpath "$.code" == 0 # 一级代理的零售价仍然是 15000,与二级代理互不影响 jsonpath "$.data.retail_price" == 15000 jsonpath "$.data.cost_price" == 10000 # ══════════════════════════════════════════════════════════════ # 第 11 步:平台身份 - 批量调价(给一级代理加价 500 分) # ══════════════════════════════════════════════════════════════ POST {{base_url}}/api/admin/shop-package-batch-pricing Authorization: Bearer {{admin_token}} Content-Type: application/json { "shop_id": {{shop1_id}}, "series_id": {{series_id}}, "price_adjustment": { "type": "fixed", "value": 500 }, "change_reason": "Hurl测试批量调价" } HTTP 200 [Asserts] jsonpath "$.code" == 0 jsonpath "$.data.updated_count" isInteger jsonpath "$.data.updated_count" >= 1 jsonpath "$.data.affected_ids" isList # ── 验证调价后代理仍可正常查看套餐 ── GET {{base_url}}/api/admin/packages/{{package_id}} Authorization: Bearer {{agent1_token}} HTTP 200 [Asserts] jsonpath "$.code" == 0 jsonpath "$.data.id" == {{package_id}} jsonpath "$.data.cost_price" isInteger # ══════════════════════════════════════════════════════════════ # 第 12 步:平台身份 - 批量分配套餐 # ══════════════════════════════════════════════════════════════ # [FINDING C-2] 批量分配对已有授权的店铺返回 500,应返回业务错误码 POST {{base_url}}/api/admin/shop-package-batch-allocations Authorization: Bearer {{admin_token}} Content-Type: application/json { "shop_id": {{shop2_id}}, "series_id": {{series_id}}, "price_adjustment": { "type": "percent", "value": 100 }, "one_time_commission_amount": 1500 } HTTP 200 [Asserts] jsonpath "$.code" == 0 # ══════════════════════════════════════════════════════════════ # 第 13 步:强充配置验证 # ══════════════════════════════════════════════════════════════ # 更新一级代理授权:调整佣金金额 # 注意:force_recharge_locked=true 时强充金额由系列配置控制,grant 级别无法修改 PUT {{base_url}}/api/admin/shop-series-grants/{{grant1_id}} Authorization: Bearer {{admin_token}} Content-Type: application/json { "one_time_commission_amount": 3500, "enable_force_recharge": true, "force_recharge_amount": 6000 } HTTP 200 [Asserts] jsonpath "$.code" == 0 jsonpath "$.data.one_time_commission_amount" == 3500 jsonpath "$.data.force_recharge_enabled" == true # 强充金额因 locked=true 保持系列配置值 5000,不会变为 6000 jsonpath "$.data.force_recharge_amount" == 5000 jsonpath "$.data.force_recharge_locked" == true # 验证更新持久化成功 GET {{base_url}}/api/admin/shop-series-grants/{{grant1_id}} Authorization: Bearer {{admin_token}} HTTP 200 [Asserts] jsonpath "$.code" == 0 jsonpath "$.data.one_time_commission_amount" == 3500 jsonpath "$.data.force_recharge_enabled" == true jsonpath "$.data.force_recharge_amount" == 5000 jsonpath "$.data.force_recharge_locked" == true # ══════════════════════════════════════════════════════════════ # 第 14 步:代理佣金概览验证(一级代理视角) # ══════════════════════════════════════════════════════════════ GET {{base_url}}/api/admin/my/commission-summary Authorization: Bearer {{agent1_token}} HTTP 200 [Asserts] jsonpath "$.code" == 0 jsonpath "$.data" isObject # ══════════════════════════════════════════════════════════════ # 第 15 步:验证授权套餐详情(有下级分配时不可修改成本价) # ══════════════════════════════════════════════════════════════ GET {{base_url}}/api/admin/shop-series-grants/{{grant1_id}} Authorization: Bearer {{admin_token}} HTTP 200 [Asserts] jsonpath "$.code" == 0 jsonpath "$.data.packages" isList jsonpath "$.data.packages" count >= 1 jsonpath "$.data.packages[0].package_id" isInteger jsonpath "$.data.packages[0].package_name" isString jsonpath "$.data.packages[0].cost_price" isInteger # ══════════════════════════════════════════════════════════════ # 第 16 步:系列授权列表查询 # ══════════════════════════════════════════════════════════════ GET {{base_url}}/api/admin/shop-series-grants?shop_id={{shop1_id}} Authorization: Bearer {{admin_token}} HTTP 200 [Asserts] jsonpath "$.code" == 0 jsonpath "$.data.items" isList jsonpath "$.data.total" isInteger jsonpath "$.data.total" >= 1 jsonpath "$.data.page" isInteger jsonpath "$.data.size" isInteger jsonpath "$.data.items[0].id" isInteger jsonpath "$.data.items[0].shop_id" == {{shop1_id}} jsonpath "$.data.items[0].shop_name" isString jsonpath "$.data.items[0].series_id" isInteger jsonpath "$.data.items[0].series_name" isString jsonpath "$.data.items[0].commission_type" isString jsonpath "$.data.items[0].one_time_commission_amount" isInteger jsonpath "$.data.items[0].force_recharge_enabled" isBoolean jsonpath "$.data.items[0].force_recharge_locked" isBoolean jsonpath "$.data.items[0].force_recharge_amount" isInteger jsonpath "$.data.items[0].allocator_shop_id" isInteger jsonpath "$.data.items[0].allocator_shop_name" isString jsonpath "$.data.items[0].package_count" isInteger jsonpath "$.data.items[0].status" isInteger jsonpath "$.data.items[0].created_at" isString # ══════════════════════════════════════════════════════════════ # 异 常 测 试 # ══════════════════════════════════════════════════════════════ # ── 异常 1:未认证访问(无 token) ── GET {{base_url}}/api/admin/packages HTTP 401 [Asserts] jsonpath "$.code" == 1002 # ── 异常 2:参数校验失败(缺少必填字段) ── POST {{base_url}}/api/admin/packages Authorization: Bearer {{admin_token}} Content-Type: application/json { "package_name": "" } HTTP 400 [Asserts] jsonpath "$.code" == 1001 # ── 异常 3:参数校验 - 创建套餐系列编码为空触发冲突 ── POST {{base_url}}/api/admin/package-series Authorization: Bearer {{admin_token}} Content-Type: application/json { "series_name": "" } HTTP 409 [Asserts] jsonpath "$.code" isInteger jsonpath "$.code" != 0 # [FINDING C-4] 创建授权不校验目标店铺是否存在,shop_id=9999999 也能创建成功 # 预期应返回 404,实际返回 200 + 创建了无效授权记录 POST {{base_url}}/api/admin/shop-series-grants Authorization: Bearer {{admin_token}} Content-Type: application/json { "shop_id": 9999999, "series_id": {{series_id}}, "one_time_commission_amount": 1000 } HTTP 404 [Asserts] jsonpath "$.code" != 0 # ── 异常 5:二级代理越权 - 尝试给一级代理的店铺分配 ── # 一级代理已有该系列授权,所以返回冲突(409)而非越权(403) # 但实际上二级代理本就无权操作上级店铺,应优先返回 403 POST {{base_url}}/api/admin/shop-series-grants Authorization: Bearer {{agent2_token}} Content-Type: application/json { "shop_id": {{shop1_id}}, "series_id": {{series_id}}, "one_time_commission_amount": 1000 } HTTP 409 [Asserts] jsonpath "$.code" isInteger jsonpath "$.code" != 0 # ── 异常 6:过期/无效 Token ── GET {{base_url}}/api/admin/packages Authorization: Bearer invalid_token_12345 HTTP 401 [Asserts] jsonpath "$.code" == 1003