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),保留内部方法 - 完善单元测试和集成测试覆盖
13 KiB
13 KiB
单卡授权企业功能设计
1. 需求背景与目标
1.1 背景
当前系统中的企业卡授权功能是假实现,需要改造成真实的单卡授权功能。该功能与现有的单卡分配功能类似,但核心区别在于:
- 分配:转移卡的所有权,卡的
shop_id会变更 - 授权:仅授予使用权,卡的所有权(
shop_id)保持不变
1.2 目标
- 实现真实的单卡授权功能,支持代理和平台向企业授权单卡使用权
- 保证授权后的权限控制,企业只能查看和操作被授权的卡片
- 建立完整的授权记录和追踪机制
2. 功能设计
2.1 授权流程
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
请求参数:
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"`
}
响应参数:
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
请求参数:
type RevokeEnterpriseAuthorizationRequest struct {
CardIDs []int64 `json:"card_ids" validate:"required,min=1,max=1000"`
}
响应参数:
type RevokeEnterpriseAuthorizationResponse struct {
SuccessCount int `json:"success_count"`
FailCount int `json:"fail_count"`
Details []RevokeResultDetail `json:"details"`
}
3.3 企业卡列表接口调整
现有接口:GET /api/v1/enterprise/cards
调整内容:
- 查询逻辑需要关联
enterprise_card_authorization表 - 返回授权相关信息(授权人、授权时间)
- 屏蔽敏感信息(成本价、分销价、上游供应商)
响应参数调整:
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 表结构
-- 表已存在,确认字段是否满足需求
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 需要修改的文件列表
-
Service 层:
internal/service/iot_card/service.go- 新增授权相关方法internal/service/enterprise/service.go- 修改卡列表查询逻辑
-
Store 层:
internal/store/postgres/enterprise_card_authorization_store.go- 授权记录存储internal/store/postgres/iot_card_store.go- 增加授权校验查询
-
Handler 层:
internal/handler/admin/iot_card_handler.go- 新增授权接口internal/handler/enterprise/card_handler.go- 修改列表接口
-
Model 层:
internal/model/enterprise_card_authorization.go- 确认模型定义internal/model/dto/iot_card_dto.go- 新增请求/响应 DTO
-
路由注册:
internal/bootstrap/routes/admin_routes.go- 注册授权接口cmd/api/docs.go和cmd/gendocs/main.go- 更新文档生成器
5.2 核心逻辑伪代码
5.2.1 授权服务实现
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 卡片查询权限
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 操作权限控制
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 信息展示控制
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 注意事项
- 不要修改现有的分配功能逻辑
- 确保授权和分配功能相互独立
- 严格区分授权(使用权)和分配(所有权)
- 保持
shop_id不变是授权的核心特征 - 测试时注意验证各种边界情况