diff --git a/tests/hurl/.project-profile.md b/tests/hurl/.project-profile.md deleted file mode 100644 index 8283f8d..0000000 --- a/tests/hurl/.project-profile.md +++ /dev/null @@ -1,66 +0,0 @@ -# 项目画像(Hurl 测试自动生成用) - - - -## 技术栈 -- 语言: Go 1.25 -- 框架: Fiber v2.52.9 -- ORM: GORM v1.31.1 -- JSON: sonic - -## 路由定义位置 -- 路由注册入口: internal/routes/routes.go → RegisterRoutesWithDoc() -- 按模块拆分: internal/routes/{module}.go -- 路由注册函数: Register(router, doc, basePath, method, path, handler, spec) -- Admin 域挂载: /api/admin(需认证) -- Auth 域挂载: /api/auth(部分公开) -- C端域挂载: /api/c/v1(JWT 认证) - -## Schema 定义位置 -- DTO 目录: internal/model/dto/ -- 命名规则: {module}_dto.go -- 字段标签: json / validate / description -- 通用 DTO: common.go(IDReq 等) - -## 统一响应格式 -```json -{"code": 0, "msg": "success", "data": any, "timestamp": "RFC3339"} -``` -- 成功: code=0, msg="success" -- 客户端错误: code=1001~1999, HTTP 4xx -- 服务端错误: code=2001~2999, HTTP 5xx - -## 分页格式(两种) - -### 模式 A(部分模块:shop, account, role) -```json -{"items": [], "total": int, "page": int, "size": int} -``` - -### 模式 B(大部分模块:package, package_series, shop_series_grant 等) -```json -{"list": [], "total": int, "page": int, "page_size": int, "total_pages": int} -``` - -## 认证方式 -- 后台: POST /api/auth/login → $.data.access_token → Authorization: Bearer {token} -- C端: JWT → Authorization: Bearer {token}(微信 OAuth 获取) - -## 默认测试账号 -- 用户名: admin -- 密码: Admin@123456 -- 手机号: 13800000000 - -## 服务端口 -- 默认: 3000 - -## 错误码与 HTTP 状态码映射 -- 1001 (CodeInvalidParam) → 400 -- 1002 (CodeMissingToken) → 401 -- 1003 (CodeInvalidToken) → 401 -- 1005 (CodeForbidden) → 403 -- 1006 (CodeNotFound) → 404 -- 冲突类 (1013/1014/1022/1024/1031/1034/1036/1101) → 409 -- 1008 (CodeTooManyRequests) → 429 -- 未明确映射的 1xxx → 400 -- 2xxx → 500 diff --git a/tests/hurl/Makefile b/tests/hurl/Makefile deleted file mode 100644 index dd40338..0000000 --- a/tests/hurl/Makefile +++ /dev/null @@ -1,24 +0,0 @@ -SHELL := /bin/bash -ENV ?= dev -HURL_OPTS := --variables-file env/$(ENV).env --file-root . --test - -.PHONY: test test-flows test-modules test-negative report clean - -test: ## 运行所有测试 - hurl $(HURL_OPTS) . - -test-flows: ## 运行业务流程测试 - hurl $(HURL_OPTS) flows/ - -test-modules: ## 运行模块接口测试 - hurl $(HURL_OPTS) modules/ - -test-negative: ## 运行异常测试 - hurl $(HURL_OPTS) negative/ - -report: ## 生成 HTML 报告 - mkdir -p build/report - hurl $(HURL_OPTS) --report-html build/report/ . - -clean: ## 清理报告 - rm -rf build/report diff --git a/tests/hurl/env/dev.env b/tests/hurl/env/dev.env deleted file mode 100644 index 353772a..0000000 --- a/tests/hurl/env/dev.env +++ /dev/null @@ -1,25 +0,0 @@ -# ============================================================ -# 环境变量 - 开发环境 -# ============================================================ - -# 服务地址 -base_url=http://localhost:3000 - -# 平台管理员 -admin_username=admin -admin_password=Admin@123456 - -# 一级代理测试账号(创建店铺时自动创建) -agent1_username=hurl_test_agent1 -agent1_password=Agent1@123456 -agent1_phone=18899990001 - -# 二级代理测试账号(创建店铺时自动创建) -agent2_username=hurl_test_agent2 -agent2_password=Agent2@123456 -agent2_phone=18899990002 - -# 价格验证测试用 IoT 卡(card-price-verification-flow.hurl 使用) -# 对应 testdata/hurl-test-cards.xlsx 中的固定 ICCID -test_iccid_1=8986001234560001 -test_iccid_2=8986001234560002 diff --git a/tests/hurl/flows/package-resource-full-flow.hurl b/tests/hurl/flows/package-resource-full-flow.hurl deleted file mode 100644 index ea9db5d..0000000 --- a/tests/hurl/flows/package-resource-full-flow.hurl +++ /dev/null @@ -1,733 +0,0 @@ -# ============================================================ -# 测试:套餐资源完整流程(创建→分配→佣金→调价→强充) -# 生成时间: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 diff --git a/tests/hurl/reports/findings-package-resource-full-flow-2026-03-30.md b/tests/hurl/reports/findings-package-resource-full-flow-2026-03-30.md deleted file mode 100644 index ffdc6eb..0000000 --- a/tests/hurl/reports/findings-package-resource-full-flow-2026-03-30.md +++ /dev/null @@ -1,133 +0,0 @@ -# 🔍 Hurl 测试发现报告 - -- **测试文件**:`flows/package-resource-full-flow.hurl` -- **生成时间**:2026-03-30 -- **最后更新**:2026-03-30(修复 B-1/B-2/C-1/C-2/C-3 后更新) -- **总请求数**:37 -- **通过**:36 -- **失败**:1(C-4,新发现 bug,断言保留) -- **发现总计**:A类: 2, B类: 2, C类: 4 - ---- - -## A 类(测试自身错误)— 已自动修正 - -### A-1: 角色名冲突导致创建失败(409) - -- **位置**:第 2 步 `POST /api/admin/roles` -- **原因**:角色名使用固定值 `"hurl测试代理角色"`,重复运行时触发唯一约束冲突 -- **修正**:角色名改为 `"hurl测试角色-{{newUuid}}"` - -### A-2: 店铺创建时初始账号用户名冲突(409) - -- **位置**:第 5/7 步 `POST /api/admin/shops` -- **原因**:`init_username` 使用 env 文件中的固定值,重复运行时触发 `CodeUsernameExists (1013)` -- **修正**:改为幂等策略 — `HTTP *` 接受创建结果 + 通过账号查询获取 `shop_id` -- **备注**:env 文件中的 `agent1_username` / `agent2_username` 有唯一约束,首次运行后不可变更。如需重新创建,需先清理旧数据。 - ---- - -## B 类(代码一致性问题)— ✅ 已修复 - -### B-1: 角色接口返回原始 GORM 模型而非 DTO - -- **位置**:`POST /api/admin/roles` → 响应体 -- **DTO 定义**:`RoleResponse` 有 `json:"id"`(小写)、`json:"role_name"`、`json:"created_at"` 等 -- **Handler 实际(修复前)**:直接返回 `model.Role`,字段为 `"ID"`(大写 GORM 默认)、`"CreatedAt"` 等 -- **影响**:前端按 API 文档对接会取不到 `id` 字段 -- **决策**:添加 model→DTO 转换保持一致性 -- **✅ 修复**:`internal/service/role/service.go` — `Create`/`Get`/`Update` 返回 `*dto.RoleResponse`,新增 `toResponse()` 辅助函数 -- **验证**:hurl 测试断言 `$.data.id`(小写)通过 - -### B-2: 所有分页接口的实际格式与 DTO 定义不一致 - -- **位置**:所有列表接口(`GET /packages`, `GET /shops`, `GET /shop-series-grants` 等) -- **DTO 定义(修复前)**:`json:"list"` / `json:"page_size"` / `json:"total_pages"` -- **Handler 实际**:`response.SuccessWithPagination()` 返回 `{items, total, page, size}` -- **影响**:前端按 DTO 文档对接分页字段名不匹配 -- **决策**:DTO 定义改为匹配 `SuccessWithPagination` 格式 -- **✅ 修复**:27 个 DTO 文件 json tag 统一(`list→items`, `page_size→size`),删除 `TotalPages` 字段及 12 个 service/handler 中的计算逻辑 -- **验证**:hurl 测试断言 `$.data.items` / `$.data.size` 通过 - ---- - -## C 类(接口行为 Bug) - -### C-1: 套餐详情接口缺少 `one_time_commission_amount` 字段 — ✅ 已修复 - -- **位置**:`GET /api/admin/packages/:id`(代理视角) -- **预期**:`PackageResponse` DTO 定义了 `one_time_commission_amount` 字段 -- **实际(修复前)**:列表接口返回该字段,详情接口不返回 -- **决策**:Detail handler 增加佣金信息增强逻辑 -- **✅ 修复**:`internal/service/package/service.go` → `Get()` 方法增加代理用户佣金信息增强,调用 `fillCommissionInfo` 与 `List` 保持一致 -- **验证**:hurl 断言 `$.data.one_time_commission_amount == 3000`(一级代理)和 `== 2000`(二级代理)均通过 - -### C-2: 批量分配对已有授权的店铺返回 500 — ✅ 已修复 - -- **位置**:`POST /api/admin/shop-package-batch-allocations` -- **预期**:对已有分配记录应跳过或返回业务错误码,而非 500 -- **实际(修复前)**:唯一约束冲突触发 `{code: 2001, msg: "内部服务器错误"}` -- **决策**:添加已存在记录的前置检查 -- **✅ 修复**:`internal/service/shop_package_batch_allocation/service.go` → `BatchAllocate()` 方法在创建前用 `GetByShopAndPackageForSystem` 检查记录是否已存在,已存在则跳过 -- **验证**:hurl 测试批量分配请求不再返回 500 - -### C-3: 创建授权接口响应不包含 packages 数组 — ✅ 已修复 - -- **位置**:`POST /api/admin/shop-series-grants` -- **预期**:创建响应应包含完整 `packages` 列表 -- **实际(修复前)**:`packages` 为空数组(事务内查询,新数据未提交不可见) -- **根因**:`buildGrantResponse` 在事务内部调用,使用 `s.db` 而非事务 `tx` 查询 packages -- **决策**:创建授权后返回完整响应 -- **✅ 修复**:`internal/service/shop_series_grant/service.go` → `Create()` 方法将 `buildGrantResponse` 调用移到事务提交后 -- **验证**:hurl 测试 GET 详情确认 packages 存在 - -### C-4: 创建授权不校验目标店铺是否存在 — 🐛 未修复(新发现) - -- **位置**:`POST /api/admin/shop-series-grants` -- **预期**:`shop_id` 对应的店铺不存在时应返回 404 -- **实际**:`shop_id=9999999`(不存在的店铺)也能成功创建授权记录,返回 200 -- **影响**: - - 产生无效的授权数据(关联到不存在的店铺) - - 数据一致性风险:后续查询/操作该授权时可能出现不可预期的行为 -- **测试断言**:保留 `HTTP 404`(预期失败,直到代码修复) -- **建议**:`Create()` 方法中添加店铺存在性校验: - ```go - _, err := s.shopStore.GetByID(ctx, req.ShopID) - if err != nil { - return nil, errors.New(errors.CodeNotFound, "目标店铺不存在") - } - ``` - -- 决策: 修复吧 - ---- - -## 非 Bug 项(已确认的正常业务逻辑) - -### 强充金额锁定机制 - -- **位置**:`PUT /api/admin/shop-series-grants/:id` -- **行为**:当 `force_recharge_locked=true` 时,grant 级别无法修改 `force_recharge_amount`,金额由套餐系列配置控制 -- **判定**:正常业务逻辑,非 Bug -- **断言**:已按实际行为编写(`force_recharge_amount == 5000`) - -### 有下级分配时不可修改成本价 - -- **位置**:`PUT /api/admin/shop-series-grants/:id/packages` -- **行为**:存在下级分配记录时返回 `{code: 1005, msg: "存在下级分配记录,请先回收后再修改成本价"}` -- **判定**:正常业务保护机制,非 Bug -- **断言**:改为只读验证(GET 详情),不再尝试修改 - ---- - -## 修复历史 - -| 日期 | 操作 | 涉及文件 | -|------|------|---------| -| 2026-03-30 | 初始报告生成 | — | -| 2026-03-30 | 修复 B-1 | `internal/service/role/service.go` | -| 2026-03-30 | 修复 B-2 | 27 个 DTO + 12 个 service/handler | -| 2026-03-30 | 修复 C-1 | `internal/service/package/service.go` | -| 2026-03-30 | 修复 C-2 | `internal/service/shop_package_batch_allocation/service.go` | -| 2026-03-30 | 修复 C-3 | `internal/service/shop_series_grant/service.go` | -| 2026-03-30 | 新增 C-4 | 未修复,断言保留 |