# 单卡授权企业功能设计 ## 1. 需求背景与目标 ### 1.1 背景 当前系统中的企业卡授权功能是假实现,需要改造成真实的单卡授权功能。该功能与现有的单卡分配功能类似,但核心区别在于: - **分配**:转移卡的所有权,卡的 `shop_id` 会变更 - **授权**:仅授予使用权,卡的所有权(`shop_id`)保持不变 ### 1.2 目标 - 实现真实的单卡授权功能,支持代理和平台向企业授权单卡使用权 - 保证授权后的权限控制,企业只能查看和操作被授权的卡片 - 建立完整的授权记录和追踪机制 ## 2. 功能设计 ### 2.1 授权流程 ```mermaid graph TD A[发起授权请求] --> B{校验授权主体} B -->|代理| C[校验是否为自己的企业] B -->|平台| D[校验数据权限] C --> E{校验卡片状态} D --> E E -->|已绑定设备| F[提示走设备授权] E -->|已被授权| G[提示需先回收] E -->|可授权| H{校验卡片归属} H -->|平台卡| I[直接授权] H -->|代理卡| J{校验企业归属} J -->|属于该代理| K[创建授权记录] J -->|不属于| L[拒绝授权] I --> K K --> M[返回成功] ``` ### 2.2 校验规则 #### 2.2.1 授权主体校验 - **代理**: - 只能授权自己拥有的卡(`shop_id` 等于代理ID) - 只能授权给自己名下的企业 - **平台**: - 可以授权平台拥有的卡给任意企业 - 可以授权代理拥有的卡,但只能授权给该代理名下的企业 #### 2.2.2 卡片状态校验 - 已绑定设备的卡不能授权(`device_id` 不为空) - 已被授权的卡必须先回收才能重新授权 - 卡片状态必须为"已分销" #### 2.2.3 企业归属校验 - 授权代理卡时,目标企业必须属于该代理 ### 2.3 数据变更 授权成功后: - `iot_card` 表:**不做任何变更**(`shop_id`、状态等保持不变) - `enterprise_card_authorization` 表:创建授权记录 - **不记录**到 `asset_allocation_record` 表 回收授权后: - 软删除 `enterprise_card_authorization` 表中的授权记录 - 企业立即失去该卡的查看和操作权限 ## 3. 接口定义 ### 3.1 批量授权接口 **路径**:`POST /api/v1/iot-card/authorize-to-enterprise` **请求参数**: ```go type AuthorizeCardsToEnterpriseRequest struct { CardIDs []int64 `json:"card_ids" validate:"required,min=1,max=1000"` EnterpriseID int64 `json:"enterprise_id" validate:"required"` Remark string `json:"remark" validate:"max=200"` } ``` **响应参数**: ```go type AuthorizeCardsToEnterpriseResponse struct { SuccessCount int `json:"success_count"` FailCount int `json:"fail_count"` Details []AuthorizeResultDetail `json:"details"` } type AuthorizeResultDetail struct { CardID int64 `json:"card_id"` Success bool `json:"success"` Message string `json:"message,omitempty"` } ``` ### 3.2 回收授权接口 **路径**:`POST /api/v1/iot-card/revoke-enterprise-authorization` **请求参数**: ```go type RevokeEnterpriseAuthorizationRequest struct { CardIDs []int64 `json:"card_ids" validate:"required,min=1,max=1000"` } ``` **响应参数**: ```go type RevokeEnterpriseAuthorizationResponse struct { SuccessCount int `json:"success_count"` FailCount int `json:"fail_count"` Details []RevokeResultDetail `json:"details"` } ``` ### 3.3 企业卡列表接口调整 **现有接口**:`GET /api/v1/enterprise/cards` **调整内容**: 1. 查询逻辑需要关联 `enterprise_card_authorization` 表 2. 返回授权相关信息(授权人、授权时间) 3. 屏蔽敏感信息(成本价、分销价、上游供应商) **响应参数调整**: ```go type EnterpriseCardDetail struct { // 基础信息 ID int64 `json:"id"` ICCID string `json:"iccid"` IMSI string `json:"imsi"` Status string `json:"status"` // ... 其他基础字段 // 授权信息 IsAuthorized bool `json:"is_authorized"` AuthorizedBy string `json:"authorized_by,omitempty"` AuthorizedByID int64 `json:"authorized_by_id,omitempty"` AuthorizedAt time.Time `json:"authorized_at,omitempty"` // 屏蔽字段(不返回) // CostPrice float64 // DistributionPrice float64 // UpstreamSupplier string } ``` ## 4. 数据库设计 ### 4.1 EnterpriseCardAuthorization 表结构 ```sql -- 表已存在,确认字段是否满足需求 CREATE TABLE IF NOT EXISTS enterprise_card_authorization ( id BIGINT PRIMARY KEY, enterprise_id BIGINT NOT NULL, -- 被授权的企业ID card_id BIGINT NOT NULL, -- 物联网卡ID authorized_by BIGINT NOT NULL, -- 授权人ID(shop_id) authorized_by_type VARCHAR(20) NOT NULL,-- 授权人类型:platform/agent remark VARCHAR(200), -- 备注 created_at TIMESTAMP NOT NULL, updated_at TIMESTAMP NOT NULL, deleted_at TIMESTAMP -- 软删除 ); -- 索引设计 CREATE UNIQUE INDEX idx_card_enterprise_active ON enterprise_card_authorization(card_id, enterprise_id) WHERE deleted_at IS NULL; CREATE INDEX idx_enterprise_cards ON enterprise_card_authorization(enterprise_id, deleted_at); CREATE INDEX idx_card_authorization ON enterprise_card_authorization(card_id, deleted_at); ``` ## 5. 实现计划 ### 5.1 需要修改的文件列表 1. **Service 层**: - `internal/service/iot_card/service.go` - 新增授权相关方法 - `internal/service/enterprise/service.go` - 修改卡列表查询逻辑 2. **Store 层**: - `internal/store/postgres/enterprise_card_authorization_store.go` - 授权记录存储 - `internal/store/postgres/iot_card_store.go` - 增加授权校验查询 3. **Handler 层**: - `internal/handler/admin/iot_card_handler.go` - 新增授权接口 - `internal/handler/enterprise/card_handler.go` - 修改列表接口 4. **Model 层**: - `internal/model/enterprise_card_authorization.go` - 确认模型定义 - `internal/model/dto/iot_card_dto.go` - 新增请求/响应 DTO 5. **路由注册**: - `internal/bootstrap/routes/admin_routes.go` - 注册授权接口 - `cmd/api/docs.go` 和 `cmd/gendocs/main.go` - 更新文档生成器 ### 5.2 核心逻辑伪代码 #### 5.2.1 授权服务实现 ```go func (s *IotCardService) AuthorizeCardsToEnterprise(ctx context.Context, req *dto.AuthorizeCardsToEnterpriseRequest) (*dto.AuthorizeCardsToEnterpriseResponse, error) { // 1. 获取当前用户信息 userInfo := GetUserFromContext(ctx) // 2. 校验企业归属 if userInfo.ShopType == "agent" { enterprise, err := s.enterpriseStore.GetByID(ctx, req.EnterpriseID) if err != nil || enterprise.ShopID != userInfo.ShopID { return nil, errors.ErrNoPermission } } // 3. 批量处理卡片 var results []dto.AuthorizeResultDetail successCount := 0 for _, cardID := range req.CardIDs { result := s.authorizeSingleCard(ctx, cardID, req.EnterpriseID, userInfo) results = append(results, result) if result.Success { successCount++ } } return &dto.AuthorizeCardsToEnterpriseResponse{ SuccessCount: successCount, FailCount: len(req.CardIDs) - successCount, Details: results, }, nil } func (s *IotCardService) authorizeSingleCard(ctx context.Context, cardID, enterpriseID int64, userInfo *UserInfo) dto.AuthorizeResultDetail { // 1. 获取卡信息 card, err := s.cardStore.GetByID(ctx, cardID) if err != nil { return dto.AuthorizeResultDetail{ CardID: cardID, Success: false, Message: "卡片不存在", } } // 2. 校验卡片状态 if card.DeviceID != nil { return dto.AuthorizeResultDetail{ CardID: cardID, Success: false, Message: "已绑定设备的卡片请走设备授权流程", } } // 3. 检查是否已被授权 existing, _ := s.authStore.GetActiveAuthorization(ctx, cardID) if existing != nil { return dto.AuthorizeResultDetail{ CardID: cardID, Success: false, Message: "卡片已被授权,请先回收", } } // 4. 校验权限 if userInfo.ShopType == "agent" && card.ShopID != userInfo.ShopID { return dto.AuthorizeResultDetail{ CardID: cardID, Success: false, Message: "无权授权该卡片", } } if userInfo.ShopType == "platform" && card.ShopID != constants.PlatformShopID { // 平台授权代理卡,需要校验企业归属 enterprise, _ := s.enterpriseStore.GetByID(ctx, enterpriseID) if enterprise.ShopID != card.ShopID { return dto.AuthorizeResultDetail{ CardID: cardID, Success: false, Message: "只能将代理的卡授权给该代理的企业", } } } // 5. 创建授权记录 auth := &model.EnterpriseCardAuthorization{ EnterpriseID: enterpriseID, CardID: cardID, AuthorizedBy: userInfo.ShopID, AuthorizedByType: userInfo.ShopType, Remark: req.Remark, } err = s.authStore.Create(ctx, auth) if err != nil { return dto.AuthorizeResultDetail{ CardID: cardID, Success: false, Message: "创建授权记录失败", } } return dto.AuthorizeResultDetail{ CardID: cardID, Success: true, } } ``` ### 5.3 企业侧权限控制 #### 5.3.1 卡片查询权限 ```go func (s *EnterpriseCardStore) GetCardsByEnterpriseID(ctx context.Context, enterpriseID int64, filter *CardFilter) ([]*model.IotCard, error) { query := ` SELECT DISTINCT c.* FROM iot_card c INNER JOIN enterprise_card_authorization eca ON c.id = eca.card_id AND eca.enterprise_id = $1 AND eca.deleted_at IS NULL WHERE 1=1 ` // ... 其他筛选条件 } ``` #### 5.3.2 操作权限控制 ```go func (s *EnterpriseService) CanOperateCard(ctx context.Context, enterpriseID, cardID int64) bool { // 检查是否有授权记录 auth, err := s.authStore.GetActiveAuthorization(ctx, cardID) if err != nil || auth == nil || auth.EnterpriseID != enterpriseID { return false } return true } ``` #### 5.3.3 信息展示控制 ```go func (h *EnterpriseCardHandler) transformCardForEnterprise(card *model.IotCard, auth *model.EnterpriseCardAuthorization) *dto.EnterpriseCardDetail { return &dto.EnterpriseCardDetail{ // 基础信息 ID: card.ID, ICCID: card.ICCID, IMSI: card.IMSI, Status: card.Status, // 授权信息 IsAuthorized: true, AuthorizedBy: auth.AuthorizedByName, AuthorizedByID: auth.AuthorizedBy, AuthorizedAt: auth.CreatedAt, // 以下字段不返回 // CostPrice: nil, // DistributionPrice: nil, // UpstreamSupplier: nil, } } ``` ## 6. 测试要点 ### 6.1 授权功能测试 - **权限测试**: - 代理只能授权自己的卡给自己的企业 - 平台可以授权任意卡,但代理卡只能授权给对应代理的企业 - 普通用户无授权权限 - **状态校验测试**: - 已绑定设备的卡不能授权 - 已授权的卡不能重复授权 - 非"已分销"状态的卡不能授权 - **批量操作测试**: - 部分成功场景处理 - 超过1000张卡的限制 ### 6.2 回收功能测试 - 回收后企业立即失去权限 - 回收记录正确(软删除) - 批量回收的事务处理 ### 6.3 企业权限测试 - 企业只能看到被授权的卡 - 企业看不到成本价、分销价等敏感信息 - 企业可以执行停机/复机操作 - 企业不能修改卡信息 ### 6.4 性能测试 - 批量授权1000张卡的性能 - 企业卡列表查询性能(关联授权表) ## 7. 风险评估与注意事项 ### 7.1 数据一致性风险 - **风险**:授权记录与实际权限不一致 - **措施**: - 使用唯一索引防止重复授权 - 所有查询基于授权表,不依赖缓存 ### 7.2 权限泄露风险 - **风险**:企业看到不该看的信息 - **措施**: - DTO 层严格控制字段返回 - Service 层增加权限校验中间件 ### 7.3 性能风险 - **风险**:授权表数据量大导致查询慢 - **措施**: - 合理设计索引 - 考虑分页查询 - 监控慢查询 ### 7.4 业务风险 - **风险**:误操作导致大量卡片授权错误 - **措施**: - 增加操作日志 - 关键操作二次确认 - 提供批量回收功能 ### 7.5 注意事项 1. **不要修改**现有的分配功能逻辑 2. **确保**授权和分配功能相互独立 3. **严格区分**授权(使用权)和分配(所有权) 4. **保持** `shop_id` 不变是授权的核心特征 5. **测试**时注意验证各种边界情况