Files
junhong_cmp_fiber/internal/service/enterprise_card/authorization_service.go
break b3499adfca 固化七月迭代审计治理进展以隔离线上热修
Constraint: 切换 main 前必须保存当前七月分支全部项目进展,套餐生效提案仅属于 Iteration/7-11。

Rejected: 将七月套餐修复直接移植到 main | 两个分支的可靠投递架构不同。

Confidence: medium

Scope-risk: broad

Directive: 不得将本提交整体 cherry-pick 到 main;main 套餐热修必须基于其纯 Asynq 代码独立实施。

Tested: git diff --check;openspec validate fix-package-activation-starvation --strict。

Not-tested: 按用户要求未运行自动化测试;go build ./... 因当前审计改造中的 Enterprise 模型字面量和 role.recordFailure 参数类型错误未通过。
2026-08-03 09:47:22 +08:00

530 lines
15 KiB
Go

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)
}