删除hurl

This commit is contained in:
2026-05-11 12:00:02 +08:00
parent b6d21b81e3
commit 02d522564f
5 changed files with 0 additions and 981 deletions

View File

@@ -1,66 +0,0 @@
# 项目画像Hurl 测试自动生成用)
<!-- 由 hurl-test skill 自动生成,请勿手动修改 -->
<!-- 如需刷新,删除此文件后重新运行 skill -->
## 技术栈
- 语言: 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/v1JWT 认证)
## Schema 定义位置
- DTO 目录: internal/model/dto/
- 命名规则: {module}_dto.go
- 字段标签: json / validate / description
- 通用 DTO: common.goIDReq 等)
## 统一响应格式
```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

View File

@@ -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

View File

@@ -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

View File

@@ -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_amountDTO 定义且列表接口有)
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

View File

@@ -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
- **失败**1C-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 | 未修复,断言保留 |