feat: 新增代理分配套餐上架状态(shelf_status)功能
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m56s

- 新增数据库迁移:为 shop_package_allocation 表添加 shelf_status 字段
- 更新模型/DTO:ShopPackageAllocation 增加 ShelfStatus 字段及相关枚举
- 更新套餐分配 Service:支持上架/下架状态管理逻辑
- 更新套餐 Store/Service:根据 shelf_status 过滤可售套餐
- 更新购买验证 Service:引入上架状态校验逻辑
- 归档 OpenSpec 变更:2026-03-02-agent-allocation-shelf-status
- 同步更新主规范文档:allocation-shelf-status、package-management、purchase-validation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-02 15:38:54 +08:00
parent 8efe79526a
commit 61155952a7
22 changed files with 677 additions and 44 deletions

View File

@@ -252,7 +252,9 @@ func (s *Service) UpdateStatus(ctx context.Context, id uint, status int) error {
return errors.New(errors.CodeUnauthorized, "未授权访问")
}
_, err := s.packageAllocationStore.GetByID(ctx, id)
// 任务 3.2:所有者校验 —— 代理只能修改自己创建的分配记录的 status
// 使用系统级查询(不带数据权限过滤),再做业务层权限判断以返回正确的 403
allocation, err := s.packageAllocationStore.GetByIDForSystem(ctx, id)
if err != nil {
if err == gorm.ErrRecordNotFound {
return errors.New(errors.CodeNotFound, "分配记录不存在")
@@ -260,6 +262,15 @@ func (s *Service) UpdateStatus(ctx context.Context, id uint, status int) error {
return errors.Wrap(errors.CodeInternalError, err, "获取分配记录失败")
}
userType := middleware.GetUserTypeFromContext(ctx)
if userType == constants.UserTypeAgent {
callerShopID := middleware.GetShopIDFromContext(ctx)
// 代理用户只能修改自己作为 allocator 的记录
if allocation.AllocatorShopID != callerShopID {
return errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在")
}
}
if err := s.packageAllocationStore.UpdateStatus(ctx, id, status, currentUserID); err != nil {
return errors.Wrap(errors.CodeInternalError, err, "更新状态失败")
}
@@ -267,6 +278,43 @@ func (s *Service) UpdateStatus(ctx context.Context, id uint, status int) error {
return nil
}
// UpdateShelfStatus 更新分配记录的上下架状态
// 代理独立控制自己客户侧套餐可见性,不影响平台全局状态
func (s *Service) UpdateShelfStatus(ctx context.Context, allocationID uint, shelfStatus int) error {
currentUserID := middleware.GetUserIDFromContext(ctx)
if currentUserID == 0 {
return errors.New(errors.CodeUnauthorized, "未授权访问")
}
allocation, err := s.packageAllocationStore.GetByID(ctx, allocationID)
if err != nil {
if err == gorm.ErrRecordNotFound {
return errors.New(errors.CodeNotFound, "分配记录不存在")
}
return errors.Wrap(errors.CodeInternalError, err, "获取分配记录失败")
}
// 上架时检查套餐全局状态:禁用的套餐不允许上架
if shelfStatus == constants.ShelfStatusOn {
pkg, err := s.packageStore.GetByID(ctx, allocation.PackageID)
if err != nil {
if err == gorm.ErrRecordNotFound {
return errors.New(errors.CodeNotFound, "套餐不存在")
}
return errors.Wrap(errors.CodeInternalError, err, "获取套餐失败")
}
if pkg.Status == constants.StatusDisabled {
return errors.New(errors.CodeInvalidStatus, "套餐已禁用,无法上架")
}
}
if err := s.packageAllocationStore.UpdateShelfStatus(ctx, allocationID, shelfStatus, currentUserID); err != nil {
return errors.Wrap(errors.CodeInternalError, err, "更新上下架状态失败")
}
return nil
}
func (s *Service) buildResponse(ctx context.Context, a *model.ShopPackageAllocation, shopName, packageName, packageCode string) (*dto.ShopPackageAllocationResponse, error) {
var seriesID uint
seriesName := ""
@@ -304,6 +352,7 @@ func (s *Service) buildResponse(ctx context.Context, a *model.ShopPackageAllocat
AllocatorShopName: allocatorShopName,
CostPrice: a.CostPrice,
Status: a.Status,
ShelfStatus: a.ShelfStatus,
CreatedAt: a.CreatedAt.Format(time.RFC3339),
UpdatedAt: a.UpdatedAt.Format(time.RFC3339),
}, nil