Files
junhong_cmp_fiber/docs/proposals/单卡授权企业功能设计.md
huang fdcff33058
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m9s
feat: 实现企业卡授权和授权记录管理功能
主要功能:
- 添加企业卡授权/回收接口 (POST /enterprises/:id/allocate-cards, recall-cards)
- 添加授权记录管理接口 (GET/PUT /authorizations)
- 实现代理用户数据权限过滤(只能查看自己店铺下企业的授权记录)
- 添加 GORM callback 支持授权记录表的数据权限过滤

技术改进:
- 原生 SQL 查询手动添加数据权限过滤(ListWithJoin, GetByIDWithJoin)
- 移除卡授权预检接口(allocate-cards/preview),保留内部方法
- 完善单元测试和集成测试覆盖
2026-01-26 15:07:03 +08:00

428 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 单卡授权企业功能设计
## 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, -- 授权人IDshop_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. **测试**时注意验证各种边界情况