package shop import ( "context" "gorm.io/gorm" "gorm.io/gorm/clause" accessauditapp "github.com/break/junhong_cmp_fiber/internal/application/accessaudit" "github.com/break/junhong_cmp_fiber/internal/model" "github.com/break/junhong_cmp_fiber/internal/model/dto" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" "github.com/break/junhong_cmp_fiber/pkg/middleware" ) // UpdateService 收口店铺资料与业务员归属的简单写事务脚本。 type UpdateService struct { db *gorm.DB audit accessauditapp.Writer } // NewUpdateService 创建店铺更新事务脚本。 func NewUpdateService(db *gorm.DB, audit accessauditapp.Writer) *UpdateService { return &UpdateService{db: db, audit: audit} } // Update 更新单个店铺;业务员归属变化不会传播到其他店铺。 func (s *UpdateService) Update(ctx context.Context, shopID uint, request *dto.UpdateShopRequest) (*dto.ShopResponse, error) { if shopID == 0 { return nil, errors.New(errors.CodeInvalidParam) } userType := middleware.GetUserTypeFromContext(ctx) if userType == constants.UserTypeEnterprise { return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在") } if err := middleware.CanManageShop(ctx, shopID); err != nil { return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在") } if userType == constants.UserTypeAgent && request.BusinessOwnerAccountIDSet { return nil, errors.New(errors.CodeForbidden, "无权限设置店铺业务员") } if userType == constants.UserTypeAgent && request.ClientLoginDisabled != nil && middleware.GetShopIDFromContext(ctx) != shopID { return nil, errors.New(errors.CodeForbidden, "无权限修改其他店铺的C端登录限制") } if userType != constants.UserTypeSuperAdmin && userType != constants.UserTypePlatform && userType != constants.UserTypeAgent { return nil, errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在") } operatorID := middleware.GetUserIDFromContext(ctx) if operatorID == 0 { return nil, errors.New(errors.CodeUnauthorized) } var response *dto.ShopResponse var beforeShop *model.Shop var parentShop *model.Shop err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { var shop model.Shop if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&shop, shopID).Error; err != nil { return errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在") } before := shop beforeShop = &before parentShop = loadAuditParentShop(tx, shop.ParentID) if request.BusinessOwnerAccountIDSet { ownerID, err := validateUpdatedBusinessOwner(tx, request.BusinessOwnerAccountID) if err != nil { return err } shop.BusinessOwnerAccountID = ownerID } if request.ClientLoginDisabled != nil { shop.ClientLoginDisabled = *request.ClientLoginDisabled } shop.ShopName = request.ShopName shop.ContactName = request.ContactName shop.ContactPhone = request.ContactPhone shop.Province = request.Province shop.City = request.City shop.District = request.District shop.Address = request.Address shop.Status = request.Status shop.Updater = operatorID if err := tx.Save(&shop).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "更新店铺失败") } parentName := "" if shop.ParentID != nil { var parent model.Shop if err := tx.Select("shop_name").First(&parent, *shop.ParentID).Error; err == nil { parentName = parent.ShopName } } response = newShopResponse(&shop, parentName) if err := fillBusinessOwnerResponse(tx, &shop, response); err != nil { return err } if shopProfileChanged(&before, &shop) { if s.audit == nil { return errors.New(errors.CodeInvalidStatus, "店铺更新审计接缝未配置") } if err := s.audit.WriteAccessChange(ctx, tx, accessauditapp.ChangeAudit{ ActionCode: constants.AuditActionShopUpdated, Summary: "更新店铺基础资料", OperatorID: operatorID, Shop: &shop, ParentShop: parentShop, BeforeData: shopProfileData(&before), AfterData: shopProfileData(&shop), }); err != nil { return errors.Wrap(errors.CodeInternalError, err, "写入店铺更新审计失败") } } if err := s.writeStateAudits(ctx, tx, &before, &shop, parentShop, operatorID); err != nil { return err } return nil }) if err != nil { if beforeShop != nil && requestedShopProfileChanged(beforeShop, request) { accessauditapp.RecordFailure(ctx, s.db, s.audit, accessauditapp.ChangeAudit{ ActionCode: constants.AuditActionShopUpdated, Summary: "更新店铺基础资料失败", Result: shopAuditFailureResult(err), OperatorID: operatorID, Shop: beforeShop, ParentShop: parentShop, }, err) } s.recordStateFailures(ctx, beforeShop, parentShop, request, operatorID, err) return nil, err } return response, nil } func (s *UpdateService) writeStateAudits(ctx context.Context, tx *gorm.DB, before, after, parent *model.Shop, operatorID uint) error { if s.audit == nil && (before.Status != after.Status || !sameOptionalUint(before.BusinessOwnerAccountID, after.BusinessOwnerAccountID) || before.ClientLoginDisabled != after.ClientLoginDisabled) { return errors.New(errors.CodeInvalidStatus, "店铺状态审计接缝未配置") } if before.Status != after.Status { action, summary, subject := constants.AuditActionShopDisabled, "禁用店铺", "店铺已禁用" if after.Status == constants.StatusEnabled { action, summary, subject = constants.AuditActionShopEnabled, "启用店铺", "店铺已启用" } if err := s.audit.WriteAccessChange(ctx, tx, accessauditapp.ChangeAudit{ ActionCode: action, Summary: summary, OperatorID: operatorID, Shop: after, ParentShop: parent, BeforeData: map[string]any{"status": before.Status}, AfterData: map[string]any{"status": after.Status}, SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: subject, }); err != nil { return errors.Wrap(errors.CodeInternalError, err, "写入店铺状态审计失败") } } if !sameOptionalUint(before.BusinessOwnerAccountID, after.BusinessOwnerAccountID) { accounts := businessOwnerAuditAccounts(tx, before.BusinessOwnerAccountID, after.BusinessOwnerAccountID) if err := s.audit.WriteAccessChange(ctx, tx, accessauditapp.ChangeAudit{ ActionCode: constants.AuditActionShopBusinessOwnerUpdated, Summary: "更新店铺业务员归属", OperatorID: operatorID, Shop: after, ParentShop: parent, Accounts: accounts, BeforeData: map[string]any{"business_owner_account_id": before.BusinessOwnerAccountID}, AfterData: map[string]any{"business_owner_account_id": after.BusinessOwnerAccountID}, SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: "店铺业务员归属已更新", }); err != nil { return errors.Wrap(errors.CodeInternalError, err, "写入店铺业务员审计失败") } } if before.ClientLoginDisabled != after.ClientLoginDisabled { subject := "店铺 C 端登录限制已解除" if after.ClientLoginDisabled { subject = "店铺 C 端登录已限制" } if err := s.audit.WriteAccessChange(ctx, tx, accessauditapp.ChangeAudit{ ActionCode: constants.AuditActionShopClientLoginLimitUpdated, Summary: "更新店铺 C 端登录限制", OperatorID: operatorID, Shop: after, ParentShop: parent, BeforeData: map[string]any{"client_login_disabled": before.ClientLoginDisabled}, AfterData: map[string]any{"client_login_disabled": after.ClientLoginDisabled}, SubjectVisibility: constants.AuditSubjectResult, SubjectSummary: subject, }); err != nil { return errors.Wrap(errors.CodeInternalError, err, "写入店铺登录限制审计失败") } } return nil } func (s *UpdateService) recordStateFailures(ctx context.Context, shop, parent *model.Shop, request *dto.UpdateShopRequest, operatorID uint, originalErr error) { if shop == nil { return } record := func(action, summary string) { accessauditapp.RecordFailure(ctx, s.db, s.audit, accessauditapp.ChangeAudit{ ActionCode: action, Summary: summary, Result: shopAuditFailureResult(originalErr), OperatorID: operatorID, Shop: shop, ParentShop: parent, SubjectVisibility: constants.AuditSubjectInternalOnly, }, originalErr) } if shop.Status != request.Status { action := constants.AuditActionShopDisabled if request.Status == constants.StatusEnabled { action = constants.AuditActionShopEnabled } record(action, "更新店铺状态失败") } if request.BusinessOwnerAccountIDSet && !sameOptionalUint(shop.BusinessOwnerAccountID, request.BusinessOwnerAccountID) { record(constants.AuditActionShopBusinessOwnerUpdated, "更新店铺业务员归属失败") } if request.ClientLoginDisabled != nil && shop.ClientLoginDisabled != *request.ClientLoginDisabled { record(constants.AuditActionShopClientLoginLimitUpdated, "更新店铺 C 端登录限制失败") } } func businessOwnerAuditAccounts(tx *gorm.DB, beforeID, afterID *uint) []accessauditapp.AccountChange { changes := make([]accessauditapp.AccountChange, 0, 2) if account := loadAuditAccount(tx, beforeID); account != nil { changes = append(changes, accessauditapp.AccountChange{ Account: account, Relation: constants.AuditResourceRelationReference, Role: constants.AuditResourceRoleShopPreviousBusinessOwner, BeforeData: map[string]any{"assigned": true}, AfterData: map[string]any{"assigned": false}, }) } if account := loadAuditAccount(tx, afterID); account != nil { changes = append(changes, accessauditapp.AccountChange{ Account: account, Relation: constants.AuditResourceRelationReference, Role: constants.AuditResourceRoleShopBusinessOwner, BeforeData: map[string]any{"assigned": false}, AfterData: map[string]any{"assigned": true}, }) } return changes } func loadAuditAccount(tx *gorm.DB, accountID *uint) *model.Account { if accountID == nil { return nil } var account model.Account if err := tx.Unscoped().First(&account, *accountID).Error; err != nil { return nil } return &account } func sameOptionalUint(left, right *uint) bool { if left == nil || right == nil { return left == nil && right == nil } return *left == *right } func requestedShopProfileChanged(shop *model.Shop, request *dto.UpdateShopRequest) bool { return shop.ShopName != request.ShopName || shop.ContactName != request.ContactName || shop.ContactPhone != request.ContactPhone || shop.Province != request.Province || shop.City != request.City || shop.District != request.District || shop.Address != request.Address } func loadAuditParentShop(tx *gorm.DB, parentID *uint) *model.Shop { if parentID == nil { return nil } var parent model.Shop if err := tx.Unscoped().First(&parent, *parentID).Error; err != nil { return nil } return &parent } func validateUpdatedBusinessOwner(tx *gorm.DB, requestedID *uint) (*uint, error) { if requestedID == nil { return nil, nil } if *requestedID == 0 { return nil, errors.New(errors.CodeInvalidParam, "业务员账号无效") } var account model.Account if err := tx.Clauses(clause.Locking{Strength: "SHARE"}). Where("id = ? AND user_type = ? AND status = ?", *requestedID, constants.UserTypePlatform, constants.StatusEnabled). First(&account).Error; err != nil { return nil, errors.New(errors.CodeInvalidParam, "业务员账号无效或不可用") } ownerID := account.ID return &ownerID, nil }