feat: 实现企业卡授权和授权记录管理功能
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m9s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m9s
主要功能: - 添加企业卡授权/回收接口 (POST /enterprises/:id/allocate-cards, recall-cards) - 添加授权记录管理接口 (GET/PUT /authorizations) - 实现代理用户数据权限过滤(只能查看自己店铺下企业的授权记录) - 添加 GORM callback 支持授权记录表的数据权限过滤 技术改进: - 原生 SQL 查询手动添加数据权限过滤(ListWithJoin, GetByIDWithJoin) - 移除卡授权预检接口(allocate-cards/preview),保留内部方法 - 完善单元测试和集成测试覆盖
This commit is contained in:
414
internal/service/enterprise_card/authorization_service.go
Normal file
414
internal/service/enterprise_card/authorization_service.go
Normal file
@@ -0,0 +1,414 @@
|
||||
package enterprise_card
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
type AuthorizationService struct {
|
||||
enterpriseStore *postgres.EnterpriseStore
|
||||
iotCardStore *postgres.IotCardStore
|
||||
authorizationStore *postgres.EnterpriseCardAuthorizationStore
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func NewAuthorizationService(
|
||||
enterpriseStore *postgres.EnterpriseStore,
|
||||
iotCardStore *postgres.IotCardStore,
|
||||
authorizationStore *postgres.EnterpriseCardAuthorizationStore,
|
||||
logger *zap.Logger,
|
||||
) *AuthorizationService {
|
||||
return &AuthorizationService{
|
||||
enterpriseStore: enterpriseStore,
|
||||
iotCardStore: iotCardStore,
|
||||
authorizationStore: authorizationStore,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
if err := s.authorizationStore.UpdateRemark(ctx, id, remark); err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, errors.New(errors.CodeNotFound, "授权记录不存在")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.GetRecordDetail(ctx, id)
|
||||
}
|
||||
|
||||
func parseDate(dateStr string) (time.Time, error) {
|
||||
return time.ParseInLocation("2006-01-02", dateStr, time.Local)
|
||||
}
|
||||
@@ -32,7 +32,7 @@ func New(
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) AllocateCardsPreview(ctx context.Context, enterpriseID uint, req *dto.AllocateCardsPreviewReq) (*dto.AllocateCardsPreviewResp, error) {
|
||||
func (s *Service) allocateCardsPreview(ctx context.Context, enterpriseID uint, req *dto.AllocateCardsPreviewReq) (*dto.AllocateCardsPreviewResp, error) {
|
||||
currentUserID := middleware.GetUserIDFromContext(ctx)
|
||||
if currentUserID == 0 {
|
||||
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
|
||||
@@ -172,7 +172,6 @@ func (s *Service) AllocateCardsPreview(ctx context.Context, enterpriseID uint, r
|
||||
|
||||
func (s *Service) AllocateCards(ctx context.Context, enterpriseID uint, req *dto.AllocateCardsReq) (*dto.AllocateCardsResp, error) {
|
||||
currentUserID := middleware.GetUserIDFromContext(ctx)
|
||||
currentShopID := middleware.GetShopIDFromContext(ctx)
|
||||
if currentUserID == 0 {
|
||||
return nil, errors.New(errors.CodeUnauthorized, "未授权访问")
|
||||
}
|
||||
@@ -182,7 +181,7 @@ func (s *Service) AllocateCards(ctx context.Context, enterpriseID uint, req *dto
|
||||
return nil, errors.New(errors.CodeEnterpriseNotFound, "企业不存在")
|
||||
}
|
||||
|
||||
preview, err := s.AllocateCardsPreview(ctx, enterpriseID, &dto.AllocateCardsPreviewReq{ICCIDs: req.ICCIDs})
|
||||
preview, err := s.allocateCardsPreview(ctx, enterpriseID, &dto.AllocateCardsPreviewReq{ICCIDs: req.ICCIDs})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -224,18 +223,18 @@ func (s *Service) AllocateCards(ctx context.Context, enterpriseID uint, req *dto
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
userType := middleware.GetUserTypeFromContext(ctx)
|
||||
auths := make([]*model.EnterpriseCardAuthorization, 0)
|
||||
for _, cardID := range cardIDsToAllocate {
|
||||
if existingAuths[cardID] {
|
||||
continue
|
||||
}
|
||||
auths = append(auths, &model.EnterpriseCardAuthorization{
|
||||
EnterpriseID: enterpriseID,
|
||||
IotCardID: cardID,
|
||||
ShopID: currentShopID,
|
||||
AuthorizedBy: currentUserID,
|
||||
AuthorizedAt: &now,
|
||||
Status: 1,
|
||||
EnterpriseID: enterpriseID,
|
||||
CardID: cardID,
|
||||
AuthorizedBy: currentUserID,
|
||||
AuthorizedAt: now,
|
||||
AuthorizerType: userType,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -409,7 +408,7 @@ func (s *Service) updateCardNetworkStatus(ctx context.Context, enterpriseID, car
|
||||
}
|
||||
|
||||
auth, err := s.enterpriseCardAuthStore.GetByEnterpriseAndCard(ctx, enterpriseID, cardID)
|
||||
if err != nil || auth.Status != 1 {
|
||||
if err != nil || auth.RevokedAt != nil {
|
||||
return errors.New(errors.CodeForbidden, "无权限操作此卡")
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user