fix: 修复分页字段/角色DTO/套餐详情/批量分配Bug,新增C端测试登录接口和Hurl价格验证测试
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
- 修复B-2:27个分页DTO字段名统一(list→items, page_size→size,删除total_pages) - 修复B-1:角色接口统一返回DTO(id小写,消除GORM大写字段名) - 修复C-1:套餐详情接口(GET /packages/:id)补充代理佣金字段 - 修复C-2:批量分配已存在记录由500改为静默跳过 - 修复C-3:创建系列授权响应在事务提交后构建,packages数组不再为空 - 新增C端开发测试登录接口(POST /api/c/v1/auth/dev-login,仅logging.development=true时生效) - 新增Hurl测试:card-price-verification-flow.hurl(导入卡→分配→C端价格验证) - 新增测试Excel数据文件和dev.env测试变量
This commit is contained in:
@@ -19,6 +19,7 @@
|
|||||||
| 维护规范文档 | `doc-management` | 规范文档流程和维护规则 |
|
| 维护规范文档 | `doc-management` | 规范文档流程和维护规则 |
|
||||||
| 调试 bug / 排查异常 | `systematic-debugging` | 四阶段根因分析流程、逐层诊断、场景速查表 |
|
| 调试 bug / 排查异常 | `systematic-debugging` | 四阶段根因分析流程、逐层诊断、场景速查表 |
|
||||||
| 涉及业务逻辑的开发/修改 | `kb-sync` | 开发前查阅知识库、开发后更新知识库、业务决策记录 |
|
| 涉及业务逻辑的开发/修改 | `kb-sync` | 开发前查阅知识库、开发后更新知识库、业务决策记录 |
|
||||||
|
| 编写接口测试 / Hurl 测试 | `hurl-test` | 四阶段交互式生成 .hurl 测试文件(探索→确认→生成→验证),DTO 驱动的字段完整性断言 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -832,7 +832,7 @@ components:
|
|||||||
$ref: '#/components/schemas/DtoBoundCardInfo'
|
$ref: '#/components/schemas/DtoBoundCardInfo'
|
||||||
type: array
|
type: array
|
||||||
current_month_usage_mb:
|
current_month_usage_mb:
|
||||||
description: 本月已用流量MB(asset_type=card时有效)
|
description: 系统累计的自然月流量MB(asset_type=card时有效)
|
||||||
type: number
|
type: number
|
||||||
device_protect_status:
|
device_protect_status:
|
||||||
description: 保护期状态(asset_type=device时有效):none/stop/start
|
description: 保护期状态(asset_type=device时有效):none/stop/start
|
||||||
@@ -1533,6 +1533,9 @@ components:
|
|||||||
created_at:
|
created_at:
|
||||||
description: 创建时间
|
description: 创建时间
|
||||||
type: string
|
type: string
|
||||||
|
data_reset_day:
|
||||||
|
description: 上游流量重置日(1-28)
|
||||||
|
type: integer
|
||||||
description:
|
description:
|
||||||
description: 运营商描述
|
description: 运营商描述
|
||||||
type: string
|
type: string
|
||||||
@@ -2150,6 +2153,12 @@ components:
|
|||||||
carrier_type:
|
carrier_type:
|
||||||
description: 运营商类型 (CMCC:中国移动, CUCC:中国联通, CTCC:中国电信, CBN:中国广电)
|
description: 运营商类型 (CMCC:中国移动, CUCC:中国联通, CTCC:中国电信, CBN:中国广电)
|
||||||
type: string
|
type: string
|
||||||
|
data_reset_day:
|
||||||
|
description: 上游流量重置日(1-28),运营商每月清零网关计数器的日期,默认1
|
||||||
|
maximum: 28
|
||||||
|
minimum: 1
|
||||||
|
nullable: true
|
||||||
|
type: integer
|
||||||
description:
|
description:
|
||||||
description: 运营商描述
|
description: 运营商描述
|
||||||
maxLength: 500
|
maxLength: 500
|
||||||
@@ -3101,6 +3110,10 @@ components:
|
|||||||
description: 电池电量百分比(无电池时为null)
|
description: 电池电量百分比(无电池时为null)
|
||||||
nullable: true
|
nullable: true
|
||||||
type: integer
|
type: integer
|
||||||
|
client_number:
|
||||||
|
description: 当前已连接客户端数
|
||||||
|
nullable: true
|
||||||
|
type: integer
|
||||||
connect_time:
|
connect_time:
|
||||||
description: 本次联网时间(秒)
|
description: 本次联网时间(秒)
|
||||||
nullable: true
|
nullable: true
|
||||||
@@ -3383,6 +3396,10 @@ components:
|
|||||||
description: 电池电量百分比
|
description: 电池电量百分比
|
||||||
nullable: true
|
nullable: true
|
||||||
type: integer
|
type: integer
|
||||||
|
client_number:
|
||||||
|
description: 当前已连接客户端数
|
||||||
|
nullable: true
|
||||||
|
type: integer
|
||||||
connect_time:
|
connect_time:
|
||||||
description: 设备本次联网时间(秒)
|
description: 设备本次联网时间(秒)
|
||||||
nullable: true
|
nullable: true
|
||||||
@@ -6733,6 +6750,12 @@ components:
|
|||||||
minLength: 1
|
minLength: 1
|
||||||
nullable: true
|
nullable: true
|
||||||
type: string
|
type: string
|
||||||
|
data_reset_day:
|
||||||
|
description: 上游流量重置日(1-28),运营商每月清零网关计数器的日期
|
||||||
|
maximum: 28
|
||||||
|
minimum: 1
|
||||||
|
nullable: true
|
||||||
|
type: integer
|
||||||
description:
|
description:
|
||||||
description: 运营商描述
|
description: 运营商描述
|
||||||
maxLength: 500
|
maxLength: 500
|
||||||
@@ -18520,7 +18543,7 @@ paths:
|
|||||||
summary: 获取轮询任务统计
|
summary: 获取轮询任务统计
|
||||||
tags:
|
tags:
|
||||||
- 轮询管理-监控
|
- 轮询管理-监控
|
||||||
/api/admin/refunds/:
|
/api/admin/refunds:
|
||||||
get:
|
get:
|
||||||
parameters:
|
parameters:
|
||||||
- description: 页码(默认1)
|
- description: 页码(默认1)
|
||||||
|
|||||||
@@ -229,17 +229,11 @@ func (h *PollingAlertHandler) ListHistory(c *fiber.Ctx) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
totalPages := int(total) / req.PageSize
|
|
||||||
if int(total)%req.PageSize > 0 {
|
|
||||||
totalPages++
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.Success(c, &dto.PollingAlertHistoryListResp{
|
return response.Success(c, &dto.PollingAlertHistoryListResp{
|
||||||
Items: items,
|
Items: items,
|
||||||
Total: total,
|
Total: total,
|
||||||
Page: req.Page,
|
Page: req.Page,
|
||||||
PageSize: req.PageSize,
|
PageSize: req.PageSize,
|
||||||
TotalPages: totalPages,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -214,17 +214,11 @@ func (h *PollingCleanupHandler) ListLogs(c *fiber.Ctx) error {
|
|||||||
items = append(items, h.toLogResp(log))
|
items = append(items, h.toLogResp(log))
|
||||||
}
|
}
|
||||||
|
|
||||||
totalPages := int(total) / req.PageSize
|
|
||||||
if int(total)%req.PageSize > 0 {
|
|
||||||
totalPages++
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.Success(c, &dto.DataCleanupLogListResp{
|
return response.Success(c, &dto.DataCleanupLogListResp{
|
||||||
Items: items,
|
Items: items,
|
||||||
Total: total,
|
Total: total,
|
||||||
Page: req.Page,
|
Page: req.Page,
|
||||||
PageSize: req.PageSize,
|
PageSize: req.PageSize,
|
||||||
TotalPages: totalPages,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -206,17 +206,11 @@ func (h *PollingManualTriggerHandler) ListHistory(c *fiber.Ctx) error {
|
|||||||
items = append(items, h.toLogResp(log))
|
items = append(items, h.toLogResp(log))
|
||||||
}
|
}
|
||||||
|
|
||||||
totalPages := int(total) / req.PageSize
|
|
||||||
if int(total)%req.PageSize > 0 {
|
|
||||||
totalPages++
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.Success(c, &dto.ManualTriggerLogListResp{
|
return response.Success(c, &dto.ManualTriggerLogListResp{
|
||||||
Items: items,
|
Items: items,
|
||||||
Total: total,
|
Total: total,
|
||||||
Page: req.Page,
|
Page: req.Page,
|
||||||
PageSize: req.PageSize,
|
PageSize: req.PageSize,
|
||||||
TotalPages: totalPages,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -149,6 +149,29 @@ func (h *ClientAuthHandler) ChangePhone(c *fiber.Ctx) error {
|
|||||||
return response.Success(c, resp)
|
return response.Success(c, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DevLogin 开发环境测试登录(仅开发模式可用)
|
||||||
|
// POST /api/c/v1/auth/dev-login
|
||||||
|
func (h *ClientAuthHandler) DevLogin(c *fiber.Ctx) error {
|
||||||
|
var req dto.DevLoginRequest
|
||||||
|
if err := c.BodyParser(&req); err != nil {
|
||||||
|
return errors.New(errors.CodeInvalidParam)
|
||||||
|
}
|
||||||
|
if err := clientAuthValidator.Struct(&req); err != nil {
|
||||||
|
h.logger.Warn("测试登录参数校验失败", zap.Error(err))
|
||||||
|
return errors.New(errors.CodeInvalidParam)
|
||||||
|
}
|
||||||
|
|
||||||
|
token, customerID, isNewUser, err := h.service.DevLogin(c.UserContext(), req.Identifier)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return response.Success(c, dto.DevLoginResponse{
|
||||||
|
Token: token,
|
||||||
|
CustomerID: customerID,
|
||||||
|
IsNewUser: isNewUser,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Logout A7 退出登录
|
// Logout A7 退出登录
|
||||||
// POST /api/c/v1/auth/logout
|
// POST /api/c/v1/auth/logout
|
||||||
func (h *ClientAuthHandler) Logout(c *fiber.Ctx) error {
|
func (h *ClientAuthHandler) Logout(c *fiber.Ctx) error {
|
||||||
|
|||||||
@@ -51,6 +51,6 @@ type AgentRechargeListRequest struct {
|
|||||||
type AgentRechargeListResponse struct {
|
type AgentRechargeListResponse struct {
|
||||||
Total int64 `json:"total" description:"总记录数"`
|
Total int64 `json:"total" description:"总记录数"`
|
||||||
Page int `json:"page" description:"当前页码"`
|
Page int `json:"page" description:"当前页码"`
|
||||||
PageSize int `json:"page_size" description:"每页条数"`
|
PageSize int `json:"size" description:"每页条数"`
|
||||||
List []*AgentRechargeResponse `json:"list" description:"充值记录列表"`
|
List []*AgentRechargeResponse `json:"items" description:"充值记录列表"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,5 +14,5 @@ type AllocationConfigResponse struct {
|
|||||||
|
|
||||||
// AllocationConfigListResponse 配置版本列表响应
|
// AllocationConfigListResponse 配置版本列表响应
|
||||||
type AllocationConfigListResponse struct {
|
type AllocationConfigListResponse struct {
|
||||||
List []*AllocationConfigResponse `json:"list" description:"配置版本列表"`
|
List []*AllocationConfigResponse `json:"items" description:"配置版本列表"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,9 +22,8 @@ type PriceHistoryListRequest struct {
|
|||||||
|
|
||||||
// PriceHistoryPageResult 成本价历史分页结果
|
// PriceHistoryPageResult 成本价历史分页结果
|
||||||
type PriceHistoryPageResult struct {
|
type PriceHistoryPageResult struct {
|
||||||
List []*PriceHistoryResponse `json:"list" description:"历史记录列表"`
|
List []*PriceHistoryResponse `json:"items" description:"历史记录列表"`
|
||||||
Total int64 `json:"total" description:"总数"`
|
Total int64 `json:"total" description:"总数"`
|
||||||
Page int `json:"page" description:"当前页"`
|
Page int `json:"page" description:"当前页"`
|
||||||
PageSize int `json:"page_size" description:"每页数量"`
|
PageSize int `json:"size" description:"每页数量"`
|
||||||
TotalPages int `json:"total_pages" description:"总页数"`
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,11 +43,10 @@ type AssetAllocationRecordResponse struct {
|
|||||||
|
|
||||||
// ListAssetAllocationRecordResponse 分配记录列表响应
|
// ListAssetAllocationRecordResponse 分配记录列表响应
|
||||||
type ListAssetAllocationRecordResponse struct {
|
type ListAssetAllocationRecordResponse struct {
|
||||||
List []*AssetAllocationRecordResponse `json:"list" description:"分配记录列表"`
|
List []*AssetAllocationRecordResponse `json:"items" description:"分配记录列表"`
|
||||||
Total int64 `json:"total" description:"总数"`
|
Total int64 `json:"total" description:"总数"`
|
||||||
Page int `json:"page" description:"当前页码"`
|
Page int `json:"page" description:"当前页码"`
|
||||||
PageSize int `json:"page_size" description:"每页数量"`
|
PageSize int `json:"size" description:"每页数量"`
|
||||||
TotalPages int `json:"total_pages" description:"总页数"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAssetAllocationRecordRequest 获取分配记录详情请求
|
// GetAssetAllocationRecordRequest 获取分配记录详情请求
|
||||||
|
|||||||
@@ -44,9 +44,8 @@ type AssetWalletTransactionItem struct {
|
|||||||
|
|
||||||
// AssetWalletTransactionListResponse 资产钱包流水列表响应
|
// AssetWalletTransactionListResponse 资产钱包流水列表响应
|
||||||
type AssetWalletTransactionListResponse struct {
|
type AssetWalletTransactionListResponse struct {
|
||||||
List []*AssetWalletTransactionItem `json:"list" description:"流水列表"`
|
List []*AssetWalletTransactionItem `json:"items" description:"流水列表"`
|
||||||
Total int64 `json:"total" description:"总记录数"`
|
Total int64 `json:"total" description:"总记录数"`
|
||||||
Page int `json:"page" description:"当前页码"`
|
Page int `json:"page" description:"当前页码"`
|
||||||
PageSize int `json:"page_size" description:"每页数量"`
|
PageSize int `json:"size" description:"每页数量"`
|
||||||
TotalPages int `json:"total_pages" description:"总页数"`
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,9 +55,8 @@ type UpdateCarrierStatusParams struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CarrierPageResult struct {
|
type CarrierPageResult struct {
|
||||||
List []*CarrierResponse `json:"list" description:"运营商列表"`
|
List []*CarrierResponse `json:"items" description:"运营商列表"`
|
||||||
Total int64 `json:"total" description:"总数"`
|
Total int64 `json:"total" description:"总数"`
|
||||||
Page int `json:"page" description:"当前页"`
|
Page int `json:"page" description:"当前页"`
|
||||||
PageSize int `json:"page_size" description:"每页数量"`
|
PageSize int `json:"size" description:"每页数量"`
|
||||||
TotalPages int `json:"total_pages" description:"总页数"`
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -161,10 +161,10 @@ type AssetPackageHistoryRequest struct {
|
|||||||
|
|
||||||
// AssetPackageHistoryResponse B3 资产套餐历史响应
|
// AssetPackageHistoryResponse B3 资产套餐历史响应
|
||||||
type AssetPackageHistoryResponse struct {
|
type AssetPackageHistoryResponse struct {
|
||||||
List []AssetPackageResponse `json:"list" description:"套餐历史列表"`
|
List []AssetPackageResponse `json:"items" description:"套餐历史列表"`
|
||||||
Total int64 `json:"total" description:"总数"`
|
Total int64 `json:"total" description:"总数"`
|
||||||
Page int `json:"page" description:"页码"`
|
Page int `json:"page" description:"页码"`
|
||||||
PageSize int `json:"page_size" description:"每页数量"`
|
PageSize int `json:"size" description:"每页数量"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
|
|||||||
@@ -101,3 +101,20 @@ type ChangePhoneResponse struct {
|
|||||||
type LogoutResponse struct {
|
type LogoutResponse struct {
|
||||||
Success bool `json:"success" description:"是否成功"`
|
Success bool `json:"success" description:"是否成功"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// DevLogin 开发环境测试登录(仅开发模式可用)
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
// DevLoginRequest 开发环境测试登录请求
|
||||||
|
// ⚠️ 仅限 logging.development=true 时生效,严禁在生产环境暴露
|
||||||
|
type DevLoginRequest struct {
|
||||||
|
Identifier string `json:"identifier" validate:"required,min=1,max=50" required:"true" minLength:"1" maxLength:"50" description:"资产标识符(ICCID/虚拟号等)"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DevLoginResponse 开发环境测试登录响应
|
||||||
|
type DevLoginResponse struct {
|
||||||
|
Token string `json:"token" description:"JWT 令牌"`
|
||||||
|
CustomerID uint `json:"customer_id" description:"客户ID"`
|
||||||
|
IsNewUser bool `json:"is_new_user" description:"是否新创建的测试客户"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -80,10 +80,10 @@ type ClientOrderListItem struct {
|
|||||||
|
|
||||||
// ClientOrderListResponse D2 客户端订单列表响应
|
// ClientOrderListResponse D2 客户端订单列表响应
|
||||||
type ClientOrderListResponse struct {
|
type ClientOrderListResponse struct {
|
||||||
List []ClientOrderListItem `json:"list" description:"订单列表"`
|
List []ClientOrderListItem `json:"items" description:"订单列表"`
|
||||||
Total int64 `json:"total" description:"总数"`
|
Total int64 `json:"total" description:"总数"`
|
||||||
Page int `json:"page" description:"页码"`
|
Page int `json:"page" description:"页码"`
|
||||||
PageSize int `json:"page_size" description:"每页数量"`
|
PageSize int `json:"size" description:"每页数量"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
|
|||||||
@@ -45,10 +45,10 @@ type WalletTransactionItem struct {
|
|||||||
|
|
||||||
// WalletTransactionListResponse C2 钱包流水列表响应
|
// WalletTransactionListResponse C2 钱包流水列表响应
|
||||||
type WalletTransactionListResponse struct {
|
type WalletTransactionListResponse struct {
|
||||||
List []WalletTransactionItem `json:"list" description:"流水列表"`
|
List []WalletTransactionItem `json:"items" description:"流水列表"`
|
||||||
Total int64 `json:"total" description:"总数"`
|
Total int64 `json:"total" description:"总数"`
|
||||||
Page int `json:"page" description:"页码"`
|
Page int `json:"page" description:"页码"`
|
||||||
PageSize int `json:"page_size" description:"每页数量"`
|
PageSize int `json:"size" description:"每页数量"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
@@ -131,8 +131,8 @@ type ClientRechargeListItem struct {
|
|||||||
|
|
||||||
// ClientRechargeListResponse C5 充值记录列表响应
|
// ClientRechargeListResponse C5 充值记录列表响应
|
||||||
type ClientRechargeListResponse struct {
|
type ClientRechargeListResponse struct {
|
||||||
List []ClientRechargeListItem `json:"list" description:"充值记录列表"`
|
List []ClientRechargeListItem `json:"items" description:"充值记录列表"`
|
||||||
Total int64 `json:"total" description:"总数"`
|
Total int64 `json:"total" description:"总数"`
|
||||||
Page int `json:"page" description:"页码"`
|
Page int `json:"page" description:"页码"`
|
||||||
PageSize int `json:"page_size" description:"每页数量"`
|
PageSize int `json:"size" description:"每页数量"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,11 +33,10 @@ type CommissionRecordListRequest struct {
|
|||||||
|
|
||||||
// CommissionRecordPageResult 佣金记录分页结果
|
// CommissionRecordPageResult 佣金记录分页结果
|
||||||
type CommissionRecordPageResult struct {
|
type CommissionRecordPageResult struct {
|
||||||
List []*CommissionRecordResponse `json:"list" description:"佣金记录列表"`
|
List []*CommissionRecordResponse `json:"items" description:"佣金记录列表"`
|
||||||
Total int64 `json:"total" description:"总数"`
|
Total int64 `json:"total" description:"总数"`
|
||||||
Page int `json:"page" description:"当前页"`
|
Page int `json:"page" description:"当前页"`
|
||||||
PageSize int `json:"page_size" description:"每页数量"`
|
PageSize int `json:"size" description:"每页数量"`
|
||||||
TotalPages int `json:"total_pages" description:"总页数"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CommissionStatsResponse 佣金统计响应
|
// CommissionStatsResponse 佣金统计响应
|
||||||
|
|||||||
@@ -49,11 +49,10 @@ type DeviceResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ListDeviceResponse struct {
|
type ListDeviceResponse struct {
|
||||||
List []*DeviceResponse `json:"list" description:"设备列表"`
|
List []*DeviceResponse `json:"items" description:"设备列表"`
|
||||||
Total int64 `json:"total" description:"总数"`
|
Total int64 `json:"total" description:"总数"`
|
||||||
Page int `json:"page" description:"当前页码"`
|
Page int `json:"page" description:"当前页码"`
|
||||||
PageSize int `json:"page_size" description:"每页数量"`
|
PageSize int `json:"size" description:"每页数量"`
|
||||||
TotalPages int `json:"total_pages" description:"总页数"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetDeviceRequest struct {
|
type GetDeviceRequest struct {
|
||||||
|
|||||||
@@ -41,11 +41,10 @@ type DeviceImportTaskResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ListDeviceImportTaskResponse struct {
|
type ListDeviceImportTaskResponse struct {
|
||||||
List []*DeviceImportTaskResponse `json:"list" description:"任务列表"`
|
List []*DeviceImportTaskResponse `json:"items" description:"任务列表"`
|
||||||
Total int64 `json:"total" description:"总数"`
|
Total int64 `json:"total" description:"总数"`
|
||||||
Page int `json:"page" description:"当前页码"`
|
Page int `json:"page" description:"当前页码"`
|
||||||
PageSize int `json:"page_size" description:"每页数量"`
|
PageSize int `json:"size" description:"每页数量"`
|
||||||
TotalPages int `json:"total_pages" description:"总页数"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type DeviceImportResultItemDTO struct {
|
type DeviceImportResultItemDTO struct {
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ type H5EnterpriseDeviceListReq struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type EnterpriseDeviceListResp struct {
|
type EnterpriseDeviceListResp struct {
|
||||||
List []EnterpriseDeviceItem `json:"list" description:"设备列表"`
|
List []EnterpriseDeviceItem `json:"items" description:"设备列表"`
|
||||||
Total int64 `json:"total" description:"总数"`
|
Total int64 `json:"total" description:"总数"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -88,10 +88,10 @@ type ExchangeOrderResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ExchangeListResponse struct {
|
type ExchangeListResponse struct {
|
||||||
List []*ExchangeOrderResponse `json:"list" description:"换货单列表"`
|
List []*ExchangeOrderResponse `json:"items" description:"换货单列表"`
|
||||||
Total int64 `json:"total" description:"总数"`
|
Total int64 `json:"total" description:"总数"`
|
||||||
Page int `json:"page" description:"当前页码"`
|
Page int `json:"page" description:"当前页码"`
|
||||||
PageSize int `json:"page_size" description:"每页数量"`
|
PageSize int `json:"size" description:"每页数量"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ClientExchangePendingResponse struct {
|
type ClientExchangePendingResponse struct {
|
||||||
|
|||||||
@@ -55,11 +55,10 @@ type StandaloneIotCardResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ListStandaloneIotCardResponse struct {
|
type ListStandaloneIotCardResponse struct {
|
||||||
List []*StandaloneIotCardResponse `json:"list" description:"单卡列表"`
|
List []*StandaloneIotCardResponse `json:"items" description:"单卡列表"`
|
||||||
Total int64 `json:"total" description:"总数"`
|
Total int64 `json:"total" description:"总数"`
|
||||||
Page int `json:"page" description:"当前页码"`
|
Page int `json:"page" description:"当前页码"`
|
||||||
PageSize int `json:"page_size" description:"每页数量"`
|
PageSize int `json:"size" description:"每页数量"`
|
||||||
TotalPages int `json:"total_pages" description:"总页数"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ImportIotCardRequest struct {
|
type ImportIotCardRequest struct {
|
||||||
@@ -105,11 +104,10 @@ type ImportTaskResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ListImportTaskResponse struct {
|
type ListImportTaskResponse struct {
|
||||||
List []*ImportTaskResponse `json:"list" description:"任务列表"`
|
List []*ImportTaskResponse `json:"items" description:"任务列表"`
|
||||||
Total int64 `json:"total" description:"总数"`
|
Total int64 `json:"total" description:"总数"`
|
||||||
Page int `json:"page" description:"当前页码"`
|
Page int `json:"page" description:"当前页码"`
|
||||||
PageSize int `json:"page_size" description:"每页数量"`
|
PageSize int `json:"size" description:"每页数量"`
|
||||||
TotalPages int `json:"total_pages" description:"总页数"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ImportResultItemDTO struct {
|
type ImportResultItemDTO struct {
|
||||||
|
|||||||
@@ -26,11 +26,10 @@ type MyPackageResponse struct {
|
|||||||
|
|
||||||
// MyPackagePageResult 我的可售套餐分页结果
|
// MyPackagePageResult 我的可售套餐分页结果
|
||||||
type MyPackagePageResult struct {
|
type MyPackagePageResult struct {
|
||||||
List []*MyPackageResponse `json:"list" description:"套餐列表"`
|
List []*MyPackageResponse `json:"items" description:"套餐列表"`
|
||||||
Total int64 `json:"total" description:"总数"`
|
Total int64 `json:"total" description:"总数"`
|
||||||
Page int `json:"page" description:"当前页"`
|
Page int `json:"page" description:"当前页"`
|
||||||
PageSize int `json:"page_size" description:"每页数量"`
|
PageSize int `json:"size" description:"每页数量"`
|
||||||
TotalPages int `json:"total_pages" description:"总页数"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MyPackageDetailResponse 我的可售套餐详情响应
|
// MyPackageDetailResponse 我的可售套餐详情响应
|
||||||
@@ -71,11 +70,10 @@ type MySeriesAllocationResponse struct {
|
|||||||
|
|
||||||
// MySeriesAllocationPageResult 我的套餐系列分配分页结果
|
// MySeriesAllocationPageResult 我的套餐系列分配分页结果
|
||||||
type MySeriesAllocationPageResult struct {
|
type MySeriesAllocationPageResult struct {
|
||||||
List []*MySeriesAllocationResponse `json:"list" description:"分配列表"`
|
List []*MySeriesAllocationResponse `json:"items" description:"分配列表"`
|
||||||
Total int64 `json:"total" description:"总数"`
|
Total int64 `json:"total" description:"总数"`
|
||||||
Page int `json:"page" description:"当前页"`
|
Page int `json:"page" description:"当前页"`
|
||||||
PageSize int `json:"page_size" description:"每页数量"`
|
PageSize int `json:"size" description:"每页数量"`
|
||||||
TotalPages int `json:"total_pages" description:"总页数"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// PriceSource 价格来源常量
|
// PriceSource 价格来源常量
|
||||||
|
|||||||
@@ -82,11 +82,10 @@ type OrderResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type OrderListResponse struct {
|
type OrderListResponse struct {
|
||||||
List []*OrderResponse `json:"list" description:"订单列表"`
|
List []*OrderResponse `json:"items" description:"订单列表"`
|
||||||
Total int64 `json:"total" description:"总数"`
|
Total int64 `json:"total" description:"总数"`
|
||||||
Page int `json:"page" description:"当前页码"`
|
Page int `json:"page" description:"当前页码"`
|
||||||
PageSize int `json:"page_size" description:"每页数量"`
|
PageSize int `json:"size" description:"每页数量"`
|
||||||
TotalPages int `json:"total_pages" description:"总页数"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetOrderRequest struct {
|
type GetOrderRequest struct {
|
||||||
|
|||||||
@@ -124,11 +124,10 @@ type UpdateRetailPriceParams struct {
|
|||||||
|
|
||||||
// PackagePageResult 套餐分页结果
|
// PackagePageResult 套餐分页结果
|
||||||
type PackagePageResult struct {
|
type PackagePageResult struct {
|
||||||
List []*PackageResponse `json:"list" description:"套餐列表"`
|
List []*PackageResponse `json:"items" description:"套餐列表"`
|
||||||
Total int64 `json:"total" description:"总数"`
|
Total int64 `json:"total" description:"总数"`
|
||||||
Page int `json:"page" description:"当前页"`
|
Page int `json:"page" description:"当前页"`
|
||||||
PageSize int `json:"page_size" description:"每页数量"`
|
PageSize int `json:"size" description:"每页数量"`
|
||||||
TotalPages int `json:"total_pages" description:"总页数"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// PackageUsageItemResponse 套餐使用项响应(客户视图)
|
// PackageUsageItemResponse 套餐使用项响应(客户视图)
|
||||||
|
|||||||
@@ -82,9 +82,8 @@ type UpdatePackageSeriesStatusParams struct {
|
|||||||
|
|
||||||
// PackageSeriesPageResult 套餐系列分页结果
|
// PackageSeriesPageResult 套餐系列分页结果
|
||||||
type PackageSeriesPageResult struct {
|
type PackageSeriesPageResult struct {
|
||||||
List []*PackageSeriesResponse `json:"list" description:"套餐系列列表"`
|
List []*PackageSeriesResponse `json:"items" description:"套餐系列列表"`
|
||||||
Total int64 `json:"total" description:"总数"`
|
Total int64 `json:"total" description:"总数"`
|
||||||
Page int `json:"page" description:"当前页"`
|
Page int `json:"page" description:"当前页"`
|
||||||
PageSize int `json:"page_size" description:"每页数量"`
|
PageSize int `json:"size" description:"每页数量"`
|
||||||
TotalPages int `json:"total_pages" description:"总页数"`
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,11 +69,10 @@ type PollingAlertHistoryResp struct {
|
|||||||
|
|
||||||
// PollingAlertHistoryListResp 告警历史列表响应
|
// PollingAlertHistoryListResp 告警历史列表响应
|
||||||
type PollingAlertHistoryListResp struct {
|
type PollingAlertHistoryListResp struct {
|
||||||
Items []*PollingAlertHistoryResp `json:"items" description:"告警历史列表"`
|
Items []*PollingAlertHistoryResp `json:"items" description:"告警历史列表"`
|
||||||
Total int64 `json:"total" description:"总数"`
|
Total int64 `json:"total" description:"总数"`
|
||||||
Page int `json:"page" description:"当前页"`
|
Page int `json:"page" description:"当前页"`
|
||||||
PageSize int `json:"page_size" description:"每页数量"`
|
PageSize int `json:"size" description:"每页数量"`
|
||||||
TotalPages int `json:"total_pages" description:"总页数"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListPollingAlertHistoryReq 查询告警历史请求
|
// ListPollingAlertHistoryReq 查询告警历史请求
|
||||||
|
|||||||
@@ -59,11 +59,10 @@ type DataCleanupLogResp struct {
|
|||||||
|
|
||||||
// DataCleanupLogListResp 数据清理日志列表响应
|
// DataCleanupLogListResp 数据清理日志列表响应
|
||||||
type DataCleanupLogListResp struct {
|
type DataCleanupLogListResp struct {
|
||||||
Items []*DataCleanupLogResp `json:"items" description:"日志列表"`
|
Items []*DataCleanupLogResp `json:"items" description:"日志列表"`
|
||||||
Total int64 `json:"total" description:"总数"`
|
Total int64 `json:"total" description:"总数"`
|
||||||
Page int `json:"page" description:"当前页"`
|
Page int `json:"page" description:"当前页"`
|
||||||
PageSize int `json:"page_size" description:"每页数量"`
|
PageSize int `json:"size" description:"每页数量"`
|
||||||
TotalPages int `json:"total_pages" description:"总页数"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListDataCleanupLogReq 查询数据清理日志请求
|
// ListDataCleanupLogReq 查询数据清理日志请求
|
||||||
|
|||||||
@@ -73,9 +73,8 @@ type UpdatePollingConfigStatusParams struct {
|
|||||||
|
|
||||||
// PollingConfigPageResult 轮询配置分页结果
|
// PollingConfigPageResult 轮询配置分页结果
|
||||||
type PollingConfigPageResult struct {
|
type PollingConfigPageResult struct {
|
||||||
List []*PollingConfigResponse `json:"list" description:"配置列表"`
|
List []*PollingConfigResponse `json:"items" description:"配置列表"`
|
||||||
Total int64 `json:"total" description:"总数"`
|
Total int64 `json:"total" description:"总数"`
|
||||||
Page int `json:"page" description:"当前页"`
|
Page int `json:"page" description:"当前页"`
|
||||||
PageSize int `json:"page_size" description:"每页数量"`
|
PageSize int `json:"size" description:"每页数量"`
|
||||||
TotalPages int `json:"total_pages" description:"总页数"`
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,11 +51,10 @@ type ManualTriggerLogResp struct {
|
|||||||
|
|
||||||
// ManualTriggerLogListResp 手动触发日志列表响应
|
// ManualTriggerLogListResp 手动触发日志列表响应
|
||||||
type ManualTriggerLogListResp struct {
|
type ManualTriggerLogListResp struct {
|
||||||
Items []*ManualTriggerLogResp `json:"items" description:"日志列表"`
|
Items []*ManualTriggerLogResp `json:"items" description:"日志列表"`
|
||||||
Total int64 `json:"total" description:"总数"`
|
Total int64 `json:"total" description:"总数"`
|
||||||
Page int `json:"page" description:"当前页"`
|
Page int `json:"page" description:"当前页"`
|
||||||
PageSize int `json:"page_size" description:"每页数量"`
|
PageSize int `json:"size" description:"每页数量"`
|
||||||
TotalPages int `json:"total_pages" description:"总页数"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListManualTriggerLogReq 查询手动触发日志请求
|
// ListManualTriggerLogReq 查询手动触发日志请求
|
||||||
|
|||||||
@@ -64,16 +64,10 @@ type RechargeListRequest struct {
|
|||||||
|
|
||||||
// RechargeListResponse 充值订单列表响应
|
// RechargeListResponse 充值订单列表响应
|
||||||
type RechargeListResponse struct {
|
type RechargeListResponse struct {
|
||||||
// 列表数据
|
List []*RechargeResponse `json:"items" description:"列表数据"`
|
||||||
List []*RechargeResponse `json:"list" description:"列表数据"`
|
Total int64 `json:"total" description:"总记录数"`
|
||||||
// 总记录数
|
Page int `json:"page" description:"当前页码"`
|
||||||
Total int64 `json:"total" description:"总记录数"`
|
PageSize int `json:"size" description:"每页数量"`
|
||||||
// 当前页码
|
|
||||||
Page int `json:"page" description:"当前页码"`
|
|
||||||
// 每页数量
|
|
||||||
PageSize int `json:"page_size" description:"每页数量"`
|
|
||||||
// 总页数
|
|
||||||
TotalPages int `json:"total_pages" description:"总页数"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RechargeCheckRequest 充值预检请求
|
// RechargeCheckRequest 充值预检请求
|
||||||
|
|||||||
@@ -103,11 +103,10 @@ type ShopSeriesGrantListItem struct {
|
|||||||
|
|
||||||
// ShopSeriesGrantPageResult 系列授权分页结果
|
// ShopSeriesGrantPageResult 系列授权分页结果
|
||||||
type ShopSeriesGrantPageResult struct {
|
type ShopSeriesGrantPageResult struct {
|
||||||
List []*ShopSeriesGrantListItem `json:"list" description:"授权列表"`
|
List []*ShopSeriesGrantListItem `json:"items" description:"授权列表"`
|
||||||
Total int64 `json:"total" description:"总数"`
|
Total int64 `json:"total" description:"总数"`
|
||||||
Page int `json:"page" description:"当前页"`
|
Page int `json:"page" description:"当前页"`
|
||||||
PageSize int `json:"page_size" description:"每页数量"`
|
PageSize int `json:"size" description:"每页数量"`
|
||||||
TotalPages int `json:"total_pages" description:"总页数"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateShopSeriesGrantParams 更新系列授权聚合参数(文档生成用)
|
// UpdateShopSeriesGrantParams 更新系列授权聚合参数(文档生成用)
|
||||||
|
|||||||
@@ -123,10 +123,10 @@ type WechatConfigResponse struct {
|
|||||||
|
|
||||||
// WechatConfigListResponse 微信参数配置列表响应
|
// WechatConfigListResponse 微信参数配置列表响应
|
||||||
type WechatConfigListResponse struct {
|
type WechatConfigListResponse struct {
|
||||||
List []*WechatConfigResponse `json:"list" description:"配置列表"`
|
List []*WechatConfigResponse `json:"items" description:"配置列表"`
|
||||||
Total int64 `json:"total" description:"总数"`
|
Total int64 `json:"total" description:"总数"`
|
||||||
Page int `json:"page" description:"当前页"`
|
Page int `json:"page" description:"当前页"`
|
||||||
PageSize int `json:"page_size" description:"每页数量"`
|
PageSize int `json:"size" description:"每页数量"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// MaskShortSecret 对短密钥进行脱敏处理
|
// MaskShortSecret 对短密钥进行脱敏处理
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
apphandler "github.com/break/junhong_cmp_fiber/internal/handler/app"
|
apphandler "github.com/break/junhong_cmp_fiber/internal/handler/app"
|
||||||
"github.com/break/junhong_cmp_fiber/internal/middleware"
|
"github.com/break/junhong_cmp_fiber/internal/middleware"
|
||||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||||
|
"github.com/break/junhong_cmp_fiber/pkg/config"
|
||||||
"github.com/break/junhong_cmp_fiber/pkg/openapi"
|
"github.com/break/junhong_cmp_fiber/pkg/openapi"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -53,6 +54,18 @@ func RegisterPersonalCustomerRoutes(router fiber.Router, doc *openapi.Generator,
|
|||||||
Output: &dto.ClientSendCodeResponse{},
|
Output: &dto.ClientSendCodeResponse{},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 开发模式测试登录接口(仅 logging.development=true 时注册)
|
||||||
|
if config.Get().Logging.Development {
|
||||||
|
Register(router, doc, basePath, "POST", authBasePath+"/dev-login", handlers.ClientAuth.DevLogin, RouteSpec{
|
||||||
|
Summary: "开发环境测试登录",
|
||||||
|
Description: "⚠️ 仅开发模式可用。传入资产标识符(ICCID等),自动绑定测试客户并签发 JWT,用于接口测试。",
|
||||||
|
Tags: []string{"个人客户 - 认证"},
|
||||||
|
Auth: false,
|
||||||
|
Input: &dto.DevLoginRequest{},
|
||||||
|
Output: &dto.DevLoginResponse{},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// === 需要认证的 auth 路由 ===
|
// === 需要认证的 auth 路由 ===
|
||||||
authProtectedGroup := router.Group(authBasePath)
|
authProtectedGroup := router.Group(authBasePath)
|
||||||
authProtectedGroup.Use(personalAuthMiddleware.Authenticate())
|
authProtectedGroup.Use(personalAuthMiddleware.Authenticate())
|
||||||
|
|||||||
@@ -101,17 +101,11 @@ func (s *Service) List(ctx context.Context, req *dto.ListAssetAllocationRecordRe
|
|||||||
list = append(list, item)
|
list = append(list, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
totalPages := int(total) / pageSize
|
|
||||||
if int(total)%pageSize > 0 {
|
|
||||||
totalPages++
|
|
||||||
}
|
|
||||||
|
|
||||||
return &dto.ListAssetAllocationRecordResponse{
|
return &dto.ListAssetAllocationRecordResponse{
|
||||||
List: list,
|
List: list,
|
||||||
Total: total,
|
Total: total,
|
||||||
Page: page,
|
Page: page,
|
||||||
PageSize: pageSize,
|
PageSize: pageSize,
|
||||||
TotalPages: totalPages,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -108,17 +108,11 @@ func (s *Service) ListTransactions(ctx context.Context, assetType string, assetI
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
totalPages := int(total) / pageSize
|
|
||||||
if int(total)%pageSize > 0 {
|
|
||||||
totalPages++
|
|
||||||
}
|
|
||||||
|
|
||||||
return &dto.AssetWalletTransactionListResponse{
|
return &dto.AssetWalletTransactionListResponse{
|
||||||
List: list,
|
List: list,
|
||||||
Total: total,
|
Total: total,
|
||||||
Page: page,
|
Page: page,
|
||||||
PageSize: pageSize,
|
PageSize: pageSize,
|
||||||
TotalPages: totalPages,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -717,6 +717,54 @@ func (s *Service) issueLoginToken(ctx context.Context, customerID uint) (string,
|
|||||||
return token, needBindPhone, nil
|
return token, needBindPhone, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DevLogin 开发环境测试登录
|
||||||
|
// 根据资产标识符查找或创建测试客户并直接签发 JWT,无需微信 OAuth
|
||||||
|
// ⚠️ 仅限 logging.development=true 时由路由层暴露,严禁生产环境调用
|
||||||
|
func (s *Service) DevLogin(ctx context.Context, identifier string) (string, uint, bool, error) {
|
||||||
|
assetType, assetID, err := s.resolveAsset(ctx, identifier)
|
||||||
|
if err != nil {
|
||||||
|
return "", 0, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
customerID uint
|
||||||
|
isNewUser bool
|
||||||
|
)
|
||||||
|
|
||||||
|
// 在事务中查找或创建测试客户并绑定资产
|
||||||
|
// 使用固定的开发测试 OpenID(dev_test_+identifier),避免重复创建
|
||||||
|
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
devOpenID := "dev_test_" + identifier
|
||||||
|
devAppID := "dev_test_app"
|
||||||
|
|
||||||
|
cid, created, findErr := s.findOrCreateCustomer(ctx, tx, devAppID, devOpenID, "", "测试用户", "", "dev")
|
||||||
|
if findErr != nil {
|
||||||
|
return findErr
|
||||||
|
}
|
||||||
|
if bindErr := s.bindAsset(ctx, tx, cid, assetType, assetID); bindErr != nil {
|
||||||
|
return bindErr
|
||||||
|
}
|
||||||
|
customerID = cid
|
||||||
|
isNewUser = created
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", 0, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
token, _, err := s.issueLoginToken(ctx, customerID)
|
||||||
|
if err != nil {
|
||||||
|
return "", 0, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("开发环境测试登录成功",
|
||||||
|
zap.Uint("customer_id", customerID),
|
||||||
|
zap.String("identifier", identifier),
|
||||||
|
)
|
||||||
|
|
||||||
|
return token, customerID, isNewUser, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) checkSendCodeRateLimit(ctx context.Context, phone, clientIP string) error {
|
func (s *Service) checkSendCodeRateLimit(ctx context.Context, phone, clientIP string) error {
|
||||||
phoneCooldownKey := constants.RedisClientSendCodePhoneLimitKey(phone)
|
phoneCooldownKey := constants.RedisClientSendCodePhoneLimitKey(phone)
|
||||||
exists, err := s.redis.Exists(ctx, phoneCooldownKey).Result()
|
exists, err := s.redis.Exists(ctx, phoneCooldownKey).Result()
|
||||||
|
|||||||
@@ -127,17 +127,11 @@ func (s *Service) List(ctx context.Context, req *dto.ListDeviceRequest) (*dto.Li
|
|||||||
list = append(list, item)
|
list = append(list, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
totalPages := int(total) / pageSize
|
|
||||||
if int(total)%pageSize > 0 {
|
|
||||||
totalPages++
|
|
||||||
}
|
|
||||||
|
|
||||||
return &dto.ListDeviceResponse{
|
return &dto.ListDeviceResponse{
|
||||||
List: list,
|
List: list,
|
||||||
Total: total,
|
Total: total,
|
||||||
Page: page,
|
Page: page,
|
||||||
PageSize: pageSize,
|
PageSize: pageSize,
|
||||||
TotalPages: totalPages,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -110,17 +110,11 @@ func (s *Service) List(ctx context.Context, req *dto.ListDeviceImportTaskRequest
|
|||||||
list = append(list, s.toTaskResponse(task))
|
list = append(list, s.toTaskResponse(task))
|
||||||
}
|
}
|
||||||
|
|
||||||
totalPages := int(total) / pageSize
|
|
||||||
if int(total)%pageSize > 0 {
|
|
||||||
totalPages++
|
|
||||||
}
|
|
||||||
|
|
||||||
return &dto.ListDeviceImportTaskResponse{
|
return &dto.ListDeviceImportTaskResponse{
|
||||||
List: list,
|
List: list,
|
||||||
Total: total,
|
Total: total,
|
||||||
Page: page,
|
Page: page,
|
||||||
PageSize: pageSize,
|
PageSize: pageSize,
|
||||||
TotalPages: totalPages,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -157,17 +157,11 @@ func (s *Service) ListStandalone(ctx context.Context, req *dto.ListStandaloneIot
|
|||||||
list = append(list, item)
|
list = append(list, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
totalPages := int(total) / pageSize
|
|
||||||
if int(total)%pageSize > 0 {
|
|
||||||
totalPages++
|
|
||||||
}
|
|
||||||
|
|
||||||
return &dto.ListStandaloneIotCardResponse{
|
return &dto.ListStandaloneIotCardResponse{
|
||||||
List: list,
|
List: list,
|
||||||
Total: total,
|
Total: total,
|
||||||
Page: page,
|
Page: page,
|
||||||
PageSize: pageSize,
|
PageSize: pageSize,
|
||||||
TotalPages: totalPages,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -143,17 +143,11 @@ func (s *Service) List(ctx context.Context, req *dto.ListImportTaskRequest) (*dt
|
|||||||
list = append(list, s.toTaskResponse(task))
|
list = append(list, s.toTaskResponse(task))
|
||||||
}
|
}
|
||||||
|
|
||||||
totalPages := int(total) / pageSize
|
|
||||||
if int(total)%pageSize > 0 {
|
|
||||||
totalPages++
|
|
||||||
}
|
|
||||||
|
|
||||||
return &dto.ListImportTaskResponse{
|
return &dto.ListImportTaskResponse{
|
||||||
List: list,
|
List: list,
|
||||||
Total: total,
|
Total: total,
|
||||||
Page: page,
|
Page: page,
|
||||||
PageSize: pageSize,
|
PageSize: pageSize,
|
||||||
TotalPages: totalPages,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1234,17 +1234,11 @@ func (s *Service) List(ctx context.Context, req *dto.OrderListRequest, buyerType
|
|||||||
list = append(list, s.buildOrderResponse(o, itemsMap[o.ID]))
|
list = append(list, s.buildOrderResponse(o, itemsMap[o.ID]))
|
||||||
}
|
}
|
||||||
|
|
||||||
totalPages := int(total) / pageSize
|
|
||||||
if int(total)%pageSize > 0 {
|
|
||||||
totalPages++
|
|
||||||
}
|
|
||||||
|
|
||||||
return &dto.OrderListResponse{
|
return &dto.OrderListResponse{
|
||||||
List: list,
|
List: list,
|
||||||
Total: total,
|
Total: total,
|
||||||
Page: page,
|
Page: page,
|
||||||
PageSize: pageSize,
|
PageSize: pageSize,
|
||||||
TotalPages: totalPages,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -155,6 +155,28 @@ func (s *Service) Get(ctx context.Context, id uint) (*dto.PackageResponse, error
|
|||||||
series, err := s.packageSeriesStore.GetByID(ctx, pkg.SeriesID)
|
series, err := s.packageSeriesStore.GetByID(ctx, pkg.SeriesID)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
resp.SeriesName = &series.SeriesName
|
resp.SeriesName = &series.SeriesName
|
||||||
|
|
||||||
|
// 代理用户:增强佣金信息(与 List 接口保持一致)
|
||||||
|
userType := middleware.GetUserTypeFromContext(ctx)
|
||||||
|
shopID := middleware.GetShopIDFromContext(ctx)
|
||||||
|
if userType == constants.UserTypeAgent && shopID > 0 && series.EnableOneTimeCommission {
|
||||||
|
config, _ := series.GetOneTimeCommissionConfig()
|
||||||
|
if config != nil && config.Enable {
|
||||||
|
allocations, saErr := s.shopSeriesAllocationStore.GetByShopID(ctx, shopID)
|
||||||
|
if saErr == nil {
|
||||||
|
seriesAllocationMap := make(map[uint]*model.ShopSeriesAllocation)
|
||||||
|
seriesConfigMap := make(map[uint]*model.OneTimeCommissionConfig)
|
||||||
|
for _, alloc := range allocations {
|
||||||
|
if alloc.SeriesID == pkg.SeriesID {
|
||||||
|
seriesAllocationMap[alloc.SeriesID] = alloc
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
seriesConfigMap[pkg.SeriesID] = config
|
||||||
|
s.fillCommissionInfo(resp, pkg.SeriesID, seriesAllocationMap, seriesConfigMap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return resp, nil
|
return resp, nil
|
||||||
|
|||||||
@@ -258,17 +258,11 @@ func (s *Service) List(ctx context.Context, req *dto.RechargeListRequest, userID
|
|||||||
list = append(list, s.buildRechargeResponse(r))
|
list = append(list, s.buildRechargeResponse(r))
|
||||||
}
|
}
|
||||||
|
|
||||||
totalPages := int(total) / pageSize
|
|
||||||
if int(total)%pageSize > 0 {
|
|
||||||
totalPages++
|
|
||||||
}
|
|
||||||
|
|
||||||
return &dto.RechargeListResponse{
|
return &dto.RechargeListResponse{
|
||||||
List: list,
|
List: list,
|
||||||
Total: total,
|
Total: total,
|
||||||
Page: page,
|
Page: page,
|
||||||
PageSize: pageSize,
|
PageSize: pageSize,
|
||||||
TotalPages: totalPages,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||||
@@ -34,7 +35,7 @@ func New(roleStore *postgres.RoleStore, permissionStore *postgres.PermissionStor
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create 创建角色
|
// Create 创建角色
|
||||||
func (s *Service) Create(ctx context.Context, req *dto.CreateRoleRequest) (*model.Role, error) {
|
func (s *Service) Create(ctx context.Context, req *dto.CreateRoleRequest) (*dto.RoleResponse, error) {
|
||||||
// 获取当前用户 ID
|
// 获取当前用户 ID
|
||||||
currentUserID := middleware.GetUserIDFromContext(ctx)
|
currentUserID := middleware.GetUserIDFromContext(ctx)
|
||||||
if currentUserID == 0 {
|
if currentUserID == 0 {
|
||||||
@@ -62,11 +63,11 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateRoleRequest) (*mode
|
|||||||
return nil, errors.Wrap(errors.CodeInternalError, err, "创建角色失败")
|
return nil, errors.Wrap(errors.CodeInternalError, err, "创建角色失败")
|
||||||
}
|
}
|
||||||
|
|
||||||
return role, nil
|
return toResponse(role), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get 获取角色
|
// Get 获取角色
|
||||||
func (s *Service) Get(ctx context.Context, id uint) (*model.Role, error) {
|
func (s *Service) Get(ctx context.Context, id uint) (*dto.RoleResponse, error) {
|
||||||
role, err := s.roleStore.GetByID(ctx, id)
|
role, err := s.roleStore.GetByID(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == gorm.ErrRecordNotFound {
|
if err == gorm.ErrRecordNotFound {
|
||||||
@@ -74,11 +75,11 @@ func (s *Service) Get(ctx context.Context, id uint) (*model.Role, error) {
|
|||||||
}
|
}
|
||||||
return nil, errors.Wrap(errors.CodeInternalError, err, "获取角色失败")
|
return nil, errors.Wrap(errors.CodeInternalError, err, "获取角色失败")
|
||||||
}
|
}
|
||||||
return role, nil
|
return toResponse(role), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update 更新角色
|
// Update 更新角色
|
||||||
func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdateRoleRequest) (*model.Role, error) {
|
func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdateRoleRequest) (*dto.RoleResponse, error) {
|
||||||
// 获取当前用户 ID
|
// 获取当前用户 ID
|
||||||
currentUserID := middleware.GetUserIDFromContext(ctx)
|
currentUserID := middleware.GetUserIDFromContext(ctx)
|
||||||
if currentUserID == 0 {
|
if currentUserID == 0 {
|
||||||
@@ -120,7 +121,7 @@ func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdateRoleReques
|
|||||||
return nil, errors.Wrap(errors.CodeInternalError, err, "更新角色失败")
|
return nil, errors.Wrap(errors.CodeInternalError, err, "更新角色失败")
|
||||||
}
|
}
|
||||||
|
|
||||||
return role, nil
|
return toResponse(role), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete 软删除角色
|
// Delete 软删除角色
|
||||||
@@ -293,6 +294,21 @@ func (s *Service) UpdateStatus(ctx context.Context, id uint, status int) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// toResponse 将 model.Role 转换为 dto.RoleResponse
|
||||||
|
func toResponse(role *model.Role) *dto.RoleResponse {
|
||||||
|
return &dto.RoleResponse{
|
||||||
|
ID: role.ID,
|
||||||
|
RoleName: role.RoleName,
|
||||||
|
RoleDesc: role.RoleDesc,
|
||||||
|
RoleType: role.RoleType,
|
||||||
|
Status: role.Status,
|
||||||
|
Creator: role.Creator,
|
||||||
|
Updater: role.Updater,
|
||||||
|
CreatedAt: role.CreatedAt.Format(time.RFC3339),
|
||||||
|
UpdatedAt: role.UpdatedAt.Format(time.RFC3339),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func contains(availableForRoleTypes, roleTypeStr string) bool {
|
func contains(availableForRoleTypes, roleTypeStr string) bool {
|
||||||
types := strings.Split(availableForRoleTypes, ",")
|
types := strings.Split(availableForRoleTypes, ",")
|
||||||
for _, t := range types {
|
for _, t := range types {
|
||||||
|
|||||||
@@ -82,8 +82,15 @@ func (s *Service) BatchAllocate(ctx context.Context, req *dto.BatchAllocatePacka
|
|||||||
}
|
}
|
||||||
|
|
||||||
return s.db.Transaction(func(tx *gorm.DB) error {
|
return s.db.Transaction(func(tx *gorm.DB) error {
|
||||||
packageAllocations := make([]*model.ShopPackageAllocation, 0, len(packages))
|
txPkgAllocStore := postgres.NewShopPackageAllocationStore(tx)
|
||||||
|
|
||||||
for _, pkg := range packages {
|
for _, pkg := range packages {
|
||||||
|
// 已存在该套餐的分配记录则跳过,避免唯一约束冲突
|
||||||
|
_, existErr := txPkgAllocStore.GetByShopAndPackageForSystem(ctx, req.ShopID, pkg.ID)
|
||||||
|
if existErr == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
costPrice := pkg.CostPrice
|
costPrice := pkg.CostPrice
|
||||||
if req.PriceAdjustment != nil {
|
if req.PriceAdjustment != nil {
|
||||||
costPrice = s.calculateAdjustedPrice(pkg.CostPrice, req.PriceAdjustment)
|
costPrice = s.calculateAdjustedPrice(pkg.CostPrice, req.PriceAdjustment)
|
||||||
@@ -99,11 +106,9 @@ func (s *Service) BatchAllocate(ctx context.Context, req *dto.BatchAllocatePacka
|
|||||||
SeriesAllocationID: &seriesAllocation.ID,
|
SeriesAllocationID: &seriesAllocation.ID,
|
||||||
Status: constants.StatusEnabled,
|
Status: constants.StatusEnabled,
|
||||||
}
|
}
|
||||||
packageAllocations = append(packageAllocations, allocation)
|
if err := tx.Create(allocation).Error; err != nil {
|
||||||
}
|
return errors.Wrap(errors.CodeInternalError, err, "创建套餐分配失败")
|
||||||
|
}
|
||||||
if err := tx.CreateInBatches(packageAllocations, 100).Error; err != nil {
|
|
||||||
return errors.Wrap(errors.CodeInternalError, err, "批量创建套餐分配失败")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -212,6 +212,12 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateShopSeriesGrantRequ
|
|||||||
return nil, errors.New(errors.CodeInvalidParam, "该系列未启用一次性佣金,无法创建授权")
|
return nil, errors.New(errors.CodeInvalidParam, "该系列未启用一次性佣金,无法创建授权")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 1.5 校验目标店铺是否存在
|
||||||
|
_, err = s.shopStore.GetByID(ctx, req.ShopID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New(errors.CodeNotFound, "目标店铺不存在")
|
||||||
|
}
|
||||||
|
|
||||||
// 2. 检查重复授权
|
// 2. 检查重复授权
|
||||||
exists, err := s.shopSeriesAllocationStore.ExistsByShopAndSeries(ctx, req.ShopID, req.SeriesID)
|
exists, err := s.shopSeriesAllocationStore.ExistsByShopAndSeries(ctx, req.ShopID, req.SeriesID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -300,7 +306,6 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateShopSeriesGrantRequ
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 6. 事务中创建 ShopSeriesAllocation + N 条 ShopPackageAllocation
|
// 6. 事务中创建 ShopSeriesAllocation + N 条 ShopPackageAllocation
|
||||||
var result *dto.ShopSeriesGrantResponse
|
|
||||||
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
txSeriesStore := postgres.NewShopSeriesAllocationStore(tx)
|
txSeriesStore := postgres.NewShopSeriesAllocationStore(tx)
|
||||||
if createErr := txSeriesStore.Create(ctx, allocation); createErr != nil {
|
if createErr := txSeriesStore.Create(ctx, allocation); createErr != nil {
|
||||||
@@ -353,15 +358,14 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateShopSeriesGrantRequ
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var buildErr error
|
return nil
|
||||||
result, buildErr = s.buildGrantResponse(ctx, allocation, series, config)
|
|
||||||
return buildErr
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, nil
|
// 事务提交后构建完整响应(此时 packages 已可查询到)
|
||||||
|
return s.buildGrantResponse(ctx, allocation, series, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get 查询单条系列授权详情
|
// Get 查询单条系列授权详情
|
||||||
@@ -494,17 +498,11 @@ func (s *Service) List(ctx context.Context, req *dto.ShopSeriesGrantListRequest)
|
|||||||
items = append(items, item)
|
items = append(items, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
totalPages := int(total) / pageSize
|
|
||||||
if int(total)%pageSize != 0 {
|
|
||||||
totalPages++
|
|
||||||
}
|
|
||||||
|
|
||||||
return &dto.ShopSeriesGrantPageResult{
|
return &dto.ShopSeriesGrantPageResult{
|
||||||
List: items,
|
List: items,
|
||||||
Total: total,
|
Total: total,
|
||||||
Page: page,
|
Page: page,
|
||||||
PageSize: pageSize,
|
PageSize: pageSize,
|
||||||
TotalPages: totalPages,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
66
tests/hurl/.project-profile.md
Normal file
66
tests/hurl/.project-profile.md
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
# 项目画像(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/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
|
||||||
24
tests/hurl/Makefile
Normal file
24
tests/hurl/Makefile
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
SHELL := /bin/bash
|
||||||
|
ENV ?= dev
|
||||||
|
HURL_OPTS := --variables-file env/$(ENV).env --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
|
||||||
25
tests/hurl/env/dev.env
vendored
Normal file
25
tests/hurl/env/dev.env
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# ============================================================
|
||||||
|
# 环境变量 - 开发环境
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
# 服务地址
|
||||||
|
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
|
||||||
381
tests/hurl/flows/card-price-verification-flow.hurl
Normal file
381
tests/hurl/flows/card-price-verification-flow.hurl
Normal file
@@ -0,0 +1,381 @@
|
|||||||
|
# ============================================================
|
||||||
|
# 测试:卡导入 → 分配 → 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}}"
|
||||||
733
tests/hurl/flows/package-resource-full-flow.hurl
Normal file
733
tests/hurl/flows/package-resource-full-flow.hurl
Normal file
@@ -0,0 +1,733 @@
|
|||||||
|
# ============================================================
|
||||||
|
# 测试:套餐资源完整流程(创建→分配→佣金→调价→强充)
|
||||||
|
# 生成时间: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
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
# 🔍 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 | 未修复,断言保留 |
|
||||||
BIN
tests/hurl/testdata/hurl-test-cards.xlsx
vendored
Normal file
BIN
tests/hurl/testdata/hurl-test-cards.xlsx
vendored
Normal file
Binary file not shown.
Reference in New Issue
Block a user