fix: 修复分页字段/角色DTO/套餐详情/批量分配Bug,新增C端测试登录接口和Hurl价格验证测试
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:
2026-03-31 09:54:08 +08:00
parent 33e7f99fdc
commit 2ef8b8a705
54 changed files with 1697 additions and 260 deletions

View File

@@ -101,17 +101,11 @@ func (s *Service) List(ctx context.Context, req *dto.ListAssetAllocationRecordRe
list = append(list, item)
}
totalPages := int(total) / pageSize
if int(total)%pageSize > 0 {
totalPages++
}
return &dto.ListAssetAllocationRecordResponse{
List: list,
Total: total,
Page: page,
PageSize: pageSize,
TotalPages: totalPages,
List: list,
Total: total,
Page: page,
PageSize: pageSize,
}, nil
}

View File

@@ -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{
List: list,
Total: total,
Page: page,
PageSize: pageSize,
TotalPages: totalPages,
List: list,
Total: total,
Page: page,
PageSize: pageSize,
}, nil
}

View File

@@ -717,6 +717,54 @@ func (s *Service) issueLoginToken(ctx context.Context, customerID uint) (string,
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
)
// 在事务中查找或创建测试客户并绑定资产
// 使用固定的开发测试 OpenIDdev_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 {
phoneCooldownKey := constants.RedisClientSendCodePhoneLimitKey(phone)
exists, err := s.redis.Exists(ctx, phoneCooldownKey).Result()

View File

@@ -127,17 +127,11 @@ func (s *Service) List(ctx context.Context, req *dto.ListDeviceRequest) (*dto.Li
list = append(list, item)
}
totalPages := int(total) / pageSize
if int(total)%pageSize > 0 {
totalPages++
}
return &dto.ListDeviceResponse{
List: list,
Total: total,
Page: page,
PageSize: pageSize,
TotalPages: totalPages,
List: list,
Total: total,
Page: page,
PageSize: pageSize,
}, nil
}

View File

@@ -110,17 +110,11 @@ func (s *Service) List(ctx context.Context, req *dto.ListDeviceImportTaskRequest
list = append(list, s.toTaskResponse(task))
}
totalPages := int(total) / pageSize
if int(total)%pageSize > 0 {
totalPages++
}
return &dto.ListDeviceImportTaskResponse{
List: list,
Total: total,
Page: page,
PageSize: pageSize,
TotalPages: totalPages,
List: list,
Total: total,
Page: page,
PageSize: pageSize,
}, nil
}

View File

@@ -157,17 +157,11 @@ func (s *Service) ListStandalone(ctx context.Context, req *dto.ListStandaloneIot
list = append(list, item)
}
totalPages := int(total) / pageSize
if int(total)%pageSize > 0 {
totalPages++
}
return &dto.ListStandaloneIotCardResponse{
List: list,
Total: total,
Page: page,
PageSize: pageSize,
TotalPages: totalPages,
List: list,
Total: total,
Page: page,
PageSize: pageSize,
}, nil
}

View File

@@ -143,17 +143,11 @@ func (s *Service) List(ctx context.Context, req *dto.ListImportTaskRequest) (*dt
list = append(list, s.toTaskResponse(task))
}
totalPages := int(total) / pageSize
if int(total)%pageSize > 0 {
totalPages++
}
return &dto.ListImportTaskResponse{
List: list,
Total: total,
Page: page,
PageSize: pageSize,
TotalPages: totalPages,
List: list,
Total: total,
Page: page,
PageSize: pageSize,
}, nil
}

View File

@@ -1234,17 +1234,11 @@ func (s *Service) List(ctx context.Context, req *dto.OrderListRequest, buyerType
list = append(list, s.buildOrderResponse(o, itemsMap[o.ID]))
}
totalPages := int(total) / pageSize
if int(total)%pageSize > 0 {
totalPages++
}
return &dto.OrderListResponse{
List: list,
Total: total,
Page: page,
PageSize: pageSize,
TotalPages: totalPages,
List: list,
Total: total,
Page: page,
PageSize: pageSize,
}, nil
}

View File

@@ -155,6 +155,28 @@ func (s *Service) Get(ctx context.Context, id uint) (*dto.PackageResponse, error
series, err := s.packageSeriesStore.GetByID(ctx, pkg.SeriesID)
if err == nil {
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

View File

@@ -258,17 +258,11 @@ func (s *Service) List(ctx context.Context, req *dto.RechargeListRequest, userID
list = append(list, s.buildRechargeResponse(r))
}
totalPages := int(total) / pageSize
if int(total)%pageSize > 0 {
totalPages++
}
return &dto.RechargeListResponse{
List: list,
Total: total,
Page: page,
PageSize: pageSize,
TotalPages: totalPages,
List: list,
Total: total,
Page: page,
PageSize: pageSize,
}, nil
}

View File

@@ -6,6 +6,7 @@ import (
"context"
"fmt"
"strings"
"time"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
@@ -34,7 +35,7 @@ func New(roleStore *postgres.RoleStore, permissionStore *postgres.PermissionStor
}
// 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
currentUserID := middleware.GetUserIDFromContext(ctx)
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 role, nil
return toResponse(role), nil
}
// 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)
if err != nil {
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 role, nil
return toResponse(role), nil
}
// 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
currentUserID := middleware.GetUserIDFromContext(ctx)
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 role, nil
return toResponse(role), nil
}
// Delete 软删除角色
@@ -293,6 +294,21 @@ func (s *Service) UpdateStatus(ctx context.Context, id uint, status int) error {
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 {
types := strings.Split(availableForRoleTypes, ",")
for _, t := range types {

View File

@@ -82,8 +82,15 @@ func (s *Service) BatchAllocate(ctx context.Context, req *dto.BatchAllocatePacka
}
return s.db.Transaction(func(tx *gorm.DB) error {
packageAllocations := make([]*model.ShopPackageAllocation, 0, len(packages))
txPkgAllocStore := postgres.NewShopPackageAllocationStore(tx)
for _, pkg := range packages {
// 已存在该套餐的分配记录则跳过,避免唯一约束冲突
_, existErr := txPkgAllocStore.GetByShopAndPackageForSystem(ctx, req.ShopID, pkg.ID)
if existErr == nil {
continue
}
costPrice := pkg.CostPrice
if req.PriceAdjustment != nil {
costPrice = s.calculateAdjustedPrice(pkg.CostPrice, req.PriceAdjustment)
@@ -99,11 +106,9 @@ func (s *Service) BatchAllocate(ctx context.Context, req *dto.BatchAllocatePacka
SeriesAllocationID: &seriesAllocation.ID,
Status: constants.StatusEnabled,
}
packageAllocations = append(packageAllocations, allocation)
}
if err := tx.CreateInBatches(packageAllocations, 100).Error; err != nil {
return errors.Wrap(errors.CodeInternalError, err, "批量创建套餐分配失败")
if err := tx.Create(allocation).Error; err != nil {
return errors.Wrap(errors.CodeInternalError, err, "创建套餐分配失败")
}
}
return nil

View File

@@ -212,6 +212,12 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateShopSeriesGrantRequ
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. 检查重复授权
exists, err := s.shopSeriesAllocationStore.ExistsByShopAndSeries(ctx, req.ShopID, req.SeriesID)
if err != nil {
@@ -300,7 +306,6 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateShopSeriesGrantRequ
}
// 6. 事务中创建 ShopSeriesAllocation + N 条 ShopPackageAllocation
var result *dto.ShopSeriesGrantResponse
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
txSeriesStore := postgres.NewShopSeriesAllocationStore(tx)
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
result, buildErr = s.buildGrantResponse(ctx, allocation, series, config)
return buildErr
return nil
})
if err != nil {
return nil, err
}
return result, nil
// 事务提交后构建完整响应(此时 packages 已可查询到)
return s.buildGrantResponse(ctx, allocation, series, config)
}
// Get 查询单条系列授权详情
@@ -494,17 +498,11 @@ func (s *Service) List(ctx context.Context, req *dto.ShopSeriesGrantListRequest)
items = append(items, item)
}
totalPages := int(total) / pageSize
if int(total)%pageSize != 0 {
totalPages++
}
return &dto.ShopSeriesGrantPageResult{
List: items,
Total: total,
Page: page,
PageSize: pageSize,
TotalPages: totalPages,
List: items,
Total: total,
Page: page,
PageSize: pageSize,
}, nil
}