package enterprise_card import ( "context" "fmt" "time" 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/store/postgres" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" "github.com/break/junhong_cmp_fiber/pkg/middleware" "go.uber.org/zap" "gorm.io/gorm" "gorm.io/gorm/clause" ) type AuthorizationService struct { db *gorm.DB enterpriseStore *postgres.EnterpriseStore iotCardStore *postgres.IotCardStore authorizationStore *postgres.EnterpriseCardAuthorizationStore logger *zap.Logger accessAudit accessauditapp.Writer } func NewAuthorizationService( db *gorm.DB, enterpriseStore *postgres.EnterpriseStore, iotCardStore *postgres.IotCardStore, authorizationStore *postgres.EnterpriseCardAuthorizationStore, logger *zap.Logger, accessAudit accessauditapp.Writer, ) *AuthorizationService { return &AuthorizationService{ db: db, enterpriseStore: enterpriseStore, iotCardStore: iotCardStore, authorizationStore: authorizationStore, logger: logger, accessAudit: accessAudit, } } type BatchAuthorizeRequest struct { EnterpriseID uint CardIDs []uint AuthorizerID uint AuthorizerType int Remark string } func (s *AuthorizationService) BatchAuthorize(ctx context.Context, req BatchAuthorizeRequest) error { if len(req.CardIDs) == 0 { return errors.New(errors.CodeInvalidParam, "卡ID列表不能为空") } userID := middleware.GetUserIDFromContext(ctx) userType := middleware.GetUserTypeFromContext(ctx) shopID := middleware.GetShopIDFromContext(ctx) if userID == 0 { return errors.New(errors.CodeUnauthorized, "用户信息无效") } enterprise, err := s.enterpriseStore.GetByID(ctx, req.EnterpriseID) if err != nil { if err == gorm.ErrRecordNotFound { return errors.New(errors.CodeEnterpriseNotFound, "企业不存在") } return err } if userType == constants.UserTypeAgent { if enterprise.OwnerShopID == nil || *enterprise.OwnerShopID != shopID { return errors.New(errors.CodeCannotAuthorizeToOthersEnterprise, "只能授权给自己的企业") } } cards, err := s.iotCardStore.GetByIDs(ctx, req.CardIDs) if err != nil { return err } if len(cards) != len(req.CardIDs) { return errors.New(errors.CodeIotCardNotFound, "部分卡不存在") } cardMap := make(map[uint]*model.IotCard) for _, card := range cards { cardMap[card.ID] = card } for _, cardID := range req.CardIDs { card := cardMap[cardID] if card.ShopID == nil { return errors.New(errors.CodeIotCardStatusNotAllowed, fmt.Sprintf("卡 %s 未分销,不能授权", card.ICCID)) } if userType == constants.UserTypeAgent && *card.ShopID != shopID { return errors.New(errors.CodeCannotAuthorizeOthersCard, fmt.Sprintf("卡 %s 不属于您的店铺", card.ICCID)) } } boundCardIDs, err := s.iotCardStore.GetBoundCardIDs(ctx, req.CardIDs) if err != nil { return err } if len(boundCardIDs) > 0 { return errors.New(errors.CodeCannotAuthorizeBoundCard, "部分卡已绑定设备,不能授权") } existingAuths, err := s.authorizationStore.ListByCards(ctx, req.CardIDs, false) if err != nil { return err } existingMap := make(map[uint]bool) for _, auth := range existingAuths { if auth.EnterpriseID == req.EnterpriseID { existingMap[auth.CardID] = true } } var newAuths []*model.EnterpriseCardAuthorization for _, cardID := range req.CardIDs { if existingMap[cardID] { continue } newAuths = append(newAuths, &model.EnterpriseCardAuthorization{ EnterpriseID: req.EnterpriseID, CardID: cardID, AuthorizedBy: req.AuthorizerID, AuthorizerType: req.AuthorizerType, Remark: req.Remark, }) } if len(newAuths) == 0 { return errors.New(errors.CodeCardAlreadyAuthorized, "所有卡已授权给该企业") } return s.authorizationStore.BatchCreate(ctx, newAuths) } type RevokeAuthorizationsRequest struct { EnterpriseID uint CardIDs []uint RevokedBy uint } func (s *AuthorizationService) RevokeAuthorizations(ctx context.Context, req RevokeAuthorizationsRequest) error { if len(req.CardIDs) == 0 { return errors.New(errors.CodeInvalidParam, "卡ID列表不能为空") } userID := middleware.GetUserIDFromContext(ctx) userType := middleware.GetUserTypeFromContext(ctx) if userID == 0 { return errors.New(errors.CodeUnauthorized, "用户信息无效") } existingAuths, err := s.authorizationStore.ListByCards(ctx, req.CardIDs, false) if err != nil { return err } authMap := make(map[uint]*model.EnterpriseCardAuthorization) for _, auth := range existingAuths { if auth.EnterpriseID == req.EnterpriseID { authMap[auth.CardID] = auth } } if len(authMap) == 0 { return errors.New(errors.CodeCardNotAuthorized, "卡未授权给该企业") } if userType == constants.UserTypeAgent { for _, auth := range authMap { if auth.AuthorizedBy != userID { return errors.New(errors.CodeCannotRevokeOthersAuthorization, "只能回收自己创建的授权") } } } var cardIDsToRevoke []uint for cardID := range authMap { cardIDsToRevoke = append(cardIDsToRevoke, cardID) } return s.authorizationStore.RevokeAuthorizations(ctx, req.EnterpriseID, cardIDsToRevoke, req.RevokedBy) } type ListAuthorizationsRequest struct { EnterpriseID *uint AuthorizedBy *uint IncludeRevoked bool Page int PageSize int } type ListAuthorizationsResponse struct { Authorizations []*model.EnterpriseCardAuthorization Total int64 } func (s *AuthorizationService) ListAuthorizations(ctx context.Context, req ListAuthorizationsRequest) (*ListAuthorizationsResponse, error) { if req.Page <= 0 { req.Page = 1 } if req.PageSize <= 0 { req.PageSize = constants.DefaultPageSize } if req.PageSize > constants.MaxPageSize { req.PageSize = constants.MaxPageSize } opts := postgres.AuthorizationListOptions{ EnterpriseID: req.EnterpriseID, AuthorizedBy: req.AuthorizedBy, IncludeRevoked: req.IncludeRevoked, Offset: (req.Page - 1) * req.PageSize, Limit: req.PageSize, } auths, total, err := s.authorizationStore.ListWithOptions(ctx, opts) if err != nil { return nil, err } return &ListAuthorizationsResponse{ Authorizations: auths, Total: total, }, nil } func (s *AuthorizationService) GetAuthorizedCardIDs(ctx context.Context, enterpriseID uint) ([]uint, error) { return s.authorizationStore.GetActiveAuthorizedCardIDs(ctx, enterpriseID) } type ListRecordsRequest struct { EnterpriseID *uint ICCID string AuthorizerType *int Status *int StartTime string EndTime string Page int PageSize int } type AuthorizationRecord struct { ID uint EnterpriseID uint EnterpriseName string CardID uint ICCID string MSISDN string AuthorizedBy uint AuthorizerName string AuthorizerType int AuthorizedAt string RevokedBy *uint RevokerName string RevokedAt *string Status int Remark string } type ListRecordsResponse struct { Items []AuthorizationRecord Total int64 Page int Size int } func (s *AuthorizationService) ListRecords(ctx context.Context, req ListRecordsRequest) (*ListRecordsResponse, error) { if req.Page <= 0 { req.Page = 1 } if req.PageSize <= 0 { req.PageSize = constants.DefaultPageSize } if req.PageSize > constants.MaxPageSize { req.PageSize = constants.MaxPageSize } opts := postgres.AuthorizationWithJoinListOptions{ EnterpriseID: req.EnterpriseID, ICCID: req.ICCID, AuthorizerType: req.AuthorizerType, Status: req.Status, Offset: (req.Page - 1) * req.PageSize, Limit: req.PageSize, } if req.StartTime != "" { t, err := parseDate(req.StartTime) if err == nil { opts.StartTime = &t } } if req.EndTime != "" { t, err := parseDate(req.EndTime) if err == nil { endTime := t.AddDate(0, 0, 1) opts.EndTime = &endTime } } results, total, err := s.authorizationStore.ListWithJoin(ctx, opts) if err != nil { return nil, err } items := make([]AuthorizationRecord, len(results)) for i, r := range results { status := 1 if r.RevokedAt != nil { status = 0 } var revokedAt *string if r.RevokedAt != nil { t := r.RevokedAt.Format("2006-01-02 15:04:05") revokedAt = &t } revokerName := "" if r.RevokerName != nil { revokerName = *r.RevokerName } items[i] = AuthorizationRecord{ ID: r.ID, EnterpriseID: r.EnterpriseID, EnterpriseName: r.EnterpriseName, CardID: r.CardID, ICCID: r.ICCID, MSISDN: r.MSISDN, AuthorizedBy: r.AuthorizedBy, AuthorizerName: r.AuthorizerName, AuthorizerType: r.AuthorizerType, AuthorizedAt: r.AuthorizedAt.Format("2006-01-02 15:04:05"), RevokedBy: r.RevokedBy, RevokerName: revokerName, RevokedAt: revokedAt, Status: status, Remark: r.Remark, } } return &ListRecordsResponse{ Items: items, Total: total, Page: req.Page, Size: req.PageSize, }, nil } func (s *AuthorizationService) GetRecordDetail(ctx context.Context, id uint) (*AuthorizationRecord, error) { r, err := s.authorizationStore.GetByIDWithJoin(ctx, id) if err != nil { if err == gorm.ErrRecordNotFound { return nil, errors.New(errors.CodeNotFound, "授权记录不存在") } return nil, err } status := 1 if r.RevokedAt != nil { status = 0 } var revokedAt *string if r.RevokedAt != nil { t := r.RevokedAt.Format("2006-01-02 15:04:05") revokedAt = &t } revokerName := "" if r.RevokerName != nil { revokerName = *r.RevokerName } return &AuthorizationRecord{ ID: r.ID, EnterpriseID: r.EnterpriseID, EnterpriseName: r.EnterpriseName, CardID: r.CardID, ICCID: r.ICCID, MSISDN: r.MSISDN, AuthorizedBy: r.AuthorizedBy, AuthorizerName: r.AuthorizerName, AuthorizerType: r.AuthorizerType, AuthorizedAt: r.AuthorizedAt.Format("2006-01-02 15:04:05"), RevokedBy: r.RevokedBy, RevokerName: revokerName, RevokedAt: revokedAt, Status: status, Remark: r.Remark, }, nil } func (s *AuthorizationService) UpdateRecordRemark(ctx context.Context, id uint, remark string) (*AuthorizationRecord, error) { userID := middleware.GetUserIDFromContext(ctx) userType := middleware.GetUserTypeFromContext(ctx) if userID == 0 { return nil, errors.New(errors.CodeUnauthorized, "用户信息无效") } if s.db == nil || s.accessAudit == nil { return nil, errors.New(errors.CodeInvalidStatus, "企业卡授权审计接缝未配置") } record, err := s.authorizationStore.GetByIDWithJoin(ctx, id) if err != nil { if err == gorm.ErrRecordNotFound { return nil, errors.New(errors.CodeNotFound, "授权记录不存在") } return nil, err } switch userType { case constants.UserTypeSuperAdmin, constants.UserTypePlatform: // 超级管理员和平台用户: 允许修改任意授权记录备注 case constants.UserTypeAgent: // 代理用户: 只能修改自己创建的授权记录 if record.AuthorizedBy != userID { err := errors.New(errors.CodeForbidden, "只能修改自己创建的授权记录备注") s.recordRemarkFailure(ctx, record, err) return nil, err } case constants.UserTypeEnterprise: // 企业用户: 禁止修改授权记录备注 err := errors.New(errors.CodeForbidden, "企业用户不允许修改授权记录备注") s.recordRemarkFailure(ctx, record, err) return nil, err default: err := errors.New(errors.CodeForbidden, "无权限修改授权记录备注") s.recordRemarkFailure(ctx, record, err) return nil, err } var enterprise model.Enterprise var card model.IotCard var auth model.EnterpriseCardAuthorization var ownerShop *model.Shop err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&auth, id).Error; err != nil { if err == gorm.ErrRecordNotFound { return errors.New(errors.CodeNotFound, "授权记录不存在") } return errors.Wrap(errors.CodeDatabaseError, err, "查询授权记录失败") } if userType == constants.UserTypeAgent && auth.AuthorizedBy != userID { return errors.New(errors.CodeForbidden, "只能修改自己创建的授权记录备注") } if err := tx.First(&enterprise, auth.EnterpriseID).Error; err != nil { return errors.New(errors.CodeForbidden, "无权限操作该资源或资源不存在") } if err := tx.First(&card, auth.CardID).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "查询授权卡失败") } if enterprise.OwnerShopID != nil { var shop model.Shop if err := tx.Unscoped().First(&shop, *enterprise.OwnerShopID).Error; err == nil { ownerShop = &shop } } beforeRemark := auth.Remark if beforeRemark == remark { return nil } if err := tx.Model(&model.EnterpriseCardAuthorization{}).Where("id = ?", auth.ID).Update("remark", remark).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "更新授权备注失败") } auth.Remark = remark return s.accessAudit.WriteAccessChange(ctx, tx, accessauditapp.ChangeAudit{ ActionCode: constants.AuditActionEnterpriseCardRemarkUpdated, Summary: "更新企业卡授权备注", OperatorID: userID, Enterprise: &enterprise, Shop: ownerShop, Cards: []accessauditapp.IotCardChange{{ Card: &card, Relation: constants.AuditResourceRelationReference, SubjectVisibility: constants.AuditSubjectInternalOnly, }}, CardAuthorizations: []accessauditapp.EnterpriseCardAuthorizationChange{{ Authorization: &auth, BeforeData: map[string]any{"remark": beforeRemark}, AfterData: map[string]any{"remark": remark}, }}, }) }) if err != nil { s.recordRemarkFailure(ctx, record, err) return nil, err } result, err := s.GetRecordDetail(ctx, id) if err != nil { return nil, err } return result, nil } func (s *AuthorizationService) recordRemarkFailure(ctx context.Context, record *postgres.AuthorizationWithJoin, originalErr error) { if record == nil { return } enterprise, _ := s.enterpriseStore.GetByID(ctx, record.EnterpriseID) card, _ := s.iotCardStore.GetByID(ctx, record.CardID) auth := &model.EnterpriseCardAuthorization{ ID: record.ID, EnterpriseID: record.EnterpriseID, CardID: record.CardID, AuthorizedBy: record.AuthorizedBy, AuthorizerType: record.AuthorizerType, AuthorizedAt: record.AuthorizedAt, RevokedBy: record.RevokedBy, RevokedAt: record.RevokedAt, Remark: record.Remark, } accessauditapp.RecordFailure(ctx, s.db, s.accessAudit, accessauditapp.ChangeAudit{ ActionCode: constants.AuditActionEnterpriseCardRemarkUpdated, Summary: "更新企业卡授权备注失败", Result: enterpriseCardFailureResult(originalErr), OperatorID: middleware.GetUserIDFromContext(ctx), Enterprise: enterprise, Cards: []accessauditapp.IotCardChange{{Card: card}}, CardAuthorizations: []accessauditapp.EnterpriseCardAuthorizationChange{{Authorization: auth}}, }, originalErr) } func parseDate(dateStr string) (time.Time, error) { return time.ParseInLocation("2006-01-02", dateStr, time.Local) }