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:
@@ -61,40 +61,6 @@ components:
|
||||
description: 用户名
|
||||
type: string
|
||||
type: object
|
||||
DtoAllocateCardsPreviewReq:
|
||||
properties:
|
||||
iccids:
|
||||
description: 需要授权的 ICCID 列表(最多1000个)
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
required:
|
||||
- iccids
|
||||
type: object
|
||||
DtoAllocateCardsPreviewResp:
|
||||
properties:
|
||||
device_bundles:
|
||||
description: 需要整体授权的设备包
|
||||
items:
|
||||
$ref: '#/components/schemas/DtoDeviceBundle'
|
||||
nullable: true
|
||||
type: array
|
||||
failed_items:
|
||||
description: 失败的卡
|
||||
items:
|
||||
$ref: '#/components/schemas/DtoFailedItem'
|
||||
nullable: true
|
||||
type: array
|
||||
standalone_cards:
|
||||
description: 可直接授权的卡(未绑定设备)
|
||||
items:
|
||||
$ref: '#/components/schemas/DtoStandaloneCard'
|
||||
nullable: true
|
||||
type: array
|
||||
summary:
|
||||
$ref: '#/components/schemas/DtoAllocatePreviewSummary'
|
||||
type: object
|
||||
DtoAllocateCardsReq:
|
||||
properties:
|
||||
confirm_device_bundles:
|
||||
@@ -130,24 +96,6 @@ components:
|
||||
description: 成功数量
|
||||
type: integer
|
||||
type: object
|
||||
DtoAllocatePreviewSummary:
|
||||
properties:
|
||||
device_card_count:
|
||||
description: 设备卡数量
|
||||
type: integer
|
||||
device_count:
|
||||
description: 设备数量
|
||||
type: integer
|
||||
failed_count:
|
||||
description: 失败数量
|
||||
type: integer
|
||||
standalone_card_count:
|
||||
description: 独立卡数量
|
||||
type: integer
|
||||
total_card_count:
|
||||
description: 总卡数量
|
||||
type: integer
|
||||
type: object
|
||||
DtoAllocateStandaloneCardsRequest:
|
||||
properties:
|
||||
batch_no:
|
||||
@@ -451,6 +399,81 @@ components:
|
||||
nullable: true
|
||||
type: array
|
||||
type: object
|
||||
DtoAuthorizationItem:
|
||||
properties:
|
||||
authorized_at:
|
||||
description: 授权时间
|
||||
format: date-time
|
||||
type: string
|
||||
authorized_by:
|
||||
description: 授权人ID
|
||||
minimum: 0
|
||||
type: integer
|
||||
authorizer_name:
|
||||
description: 授权人名称
|
||||
type: string
|
||||
authorizer_type:
|
||||
description: 授权人类型:2=平台,3=代理
|
||||
type: integer
|
||||
card_id:
|
||||
description: 卡ID
|
||||
minimum: 0
|
||||
type: integer
|
||||
enterprise_id:
|
||||
description: 企业ID
|
||||
minimum: 0
|
||||
type: integer
|
||||
enterprise_name:
|
||||
description: 企业名称
|
||||
type: string
|
||||
iccid:
|
||||
description: ICCID
|
||||
type: string
|
||||
id:
|
||||
description: 授权记录ID
|
||||
minimum: 0
|
||||
type: integer
|
||||
msisdn:
|
||||
description: 手机号
|
||||
type: string
|
||||
remark:
|
||||
description: 备注
|
||||
type: string
|
||||
revoked_at:
|
||||
description: 回收时间
|
||||
format: date-time
|
||||
nullable: true
|
||||
type: string
|
||||
revoked_by:
|
||||
description: 回收人ID
|
||||
minimum: 0
|
||||
nullable: true
|
||||
type: integer
|
||||
revoker_name:
|
||||
description: 回收人名称
|
||||
type: string
|
||||
status:
|
||||
description: 状态:1=有效,0=已回收
|
||||
type: integer
|
||||
type: object
|
||||
DtoAuthorizationListResp:
|
||||
properties:
|
||||
items:
|
||||
description: 授权记录列表
|
||||
items:
|
||||
$ref: '#/components/schemas/DtoAuthorizationItem'
|
||||
nullable: true
|
||||
type: array
|
||||
page:
|
||||
description: 当前页码
|
||||
type: integer
|
||||
size:
|
||||
description: 每页数量
|
||||
type: integer
|
||||
total:
|
||||
description: 总记录数
|
||||
type: integer
|
||||
type: object
|
||||
DtoChangePasswordRequest:
|
||||
properties:
|
||||
new_password:
|
||||
@@ -881,37 +904,6 @@ components:
|
||||
description: 总记录数
|
||||
type: integer
|
||||
type: object
|
||||
DtoDeviceBundle:
|
||||
properties:
|
||||
bundle_cards:
|
||||
description: 连带卡(同设备的其他卡)
|
||||
items:
|
||||
$ref: '#/components/schemas/DtoDeviceBundleCard'
|
||||
nullable: true
|
||||
type: array
|
||||
device_id:
|
||||
description: 设备ID
|
||||
minimum: 0
|
||||
type: integer
|
||||
device_no:
|
||||
description: 设备号
|
||||
type: string
|
||||
trigger_card:
|
||||
$ref: '#/components/schemas/DtoDeviceBundleCard'
|
||||
type: object
|
||||
DtoDeviceBundleCard:
|
||||
properties:
|
||||
iccid:
|
||||
description: ICCID
|
||||
type: string
|
||||
iot_card_id:
|
||||
description: 卡ID
|
||||
minimum: 0
|
||||
type: integer
|
||||
msisdn:
|
||||
description: 手机号
|
||||
type: string
|
||||
type: object
|
||||
DtoEnterpriseCardItem:
|
||||
properties:
|
||||
carrier_id:
|
||||
@@ -2054,26 +2046,6 @@ components:
|
||||
description: 总记录数
|
||||
type: integer
|
||||
type: object
|
||||
DtoStandaloneCard:
|
||||
properties:
|
||||
carrier_id:
|
||||
description: 运营商ID
|
||||
minimum: 0
|
||||
type: integer
|
||||
iccid:
|
||||
description: ICCID
|
||||
type: string
|
||||
iot_card_id:
|
||||
description: 卡ID
|
||||
minimum: 0
|
||||
type: integer
|
||||
msisdn:
|
||||
description: 手机号
|
||||
type: string
|
||||
status_name:
|
||||
description: 状态名称
|
||||
type: string
|
||||
type: object
|
||||
DtoStandaloneIotCardResponse:
|
||||
properties:
|
||||
activated_at:
|
||||
@@ -2178,6 +2150,12 @@ components:
|
||||
nullable: true
|
||||
type: string
|
||||
type: object
|
||||
DtoUpdateAuthorizationRemarkReq:
|
||||
properties:
|
||||
remark:
|
||||
description: 备注(最多500字)
|
||||
type: string
|
||||
type: object
|
||||
DtoUpdateCustomerAccountPasswordReq:
|
||||
properties:
|
||||
password:
|
||||
@@ -3307,6 +3285,199 @@ paths:
|
||||
summary: 分配记录详情
|
||||
tags:
|
||||
- 资产分配记录
|
||||
/api/admin/authorizations:
|
||||
get:
|
||||
parameters:
|
||||
- description: 页码
|
||||
in: query
|
||||
name: page
|
||||
schema:
|
||||
description: 页码
|
||||
minimum: 1
|
||||
type: integer
|
||||
- description: 每页数量
|
||||
in: query
|
||||
name: page_size
|
||||
schema:
|
||||
description: 每页数量
|
||||
maximum: 100
|
||||
minimum: 1
|
||||
type: integer
|
||||
- description: 按企业ID筛选
|
||||
in: query
|
||||
name: enterprise_id
|
||||
schema:
|
||||
description: 按企业ID筛选
|
||||
minimum: 0
|
||||
nullable: true
|
||||
type: integer
|
||||
- description: 按ICCID模糊查询
|
||||
in: query
|
||||
name: iccid
|
||||
schema:
|
||||
description: 按ICCID模糊查询
|
||||
type: string
|
||||
- description: 授权人类型:2=平台,3=代理
|
||||
in: query
|
||||
name: authorizer_type
|
||||
schema:
|
||||
description: 授权人类型:2=平台,3=代理
|
||||
nullable: true
|
||||
type: integer
|
||||
- description: 状态:0=已回收,1=有效
|
||||
in: query
|
||||
name: status
|
||||
schema:
|
||||
description: 状态:0=已回收,1=有效
|
||||
nullable: true
|
||||
type: integer
|
||||
- description: 授权时间起(格式:2006-01-02)
|
||||
in: query
|
||||
name: start_time
|
||||
schema:
|
||||
description: 授权时间起(格式:2006-01-02)
|
||||
type: string
|
||||
- description: 授权时间止(格式:2006-01-02)
|
||||
in: query
|
||||
name: end_time
|
||||
schema:
|
||||
description: 授权时间止(格式:2006-01-02)
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DtoAuthorizationListResp'
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 请求参数错误
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 未认证或认证已过期
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 无权访问
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 服务器内部错误
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: 授权记录列表
|
||||
tags:
|
||||
- 授权记录管理
|
||||
/api/admin/authorizations/{id}:
|
||||
get:
|
||||
parameters:
|
||||
- description: 授权记录ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
description: 授权记录ID
|
||||
minimum: 0
|
||||
type: integer
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DtoAuthorizationItem'
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 请求参数错误
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 未认证或认证已过期
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 无权访问
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 服务器内部错误
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: 授权记录详情
|
||||
tags:
|
||||
- 授权记录管理
|
||||
/api/admin/authorizations/{id}/remark:
|
||||
put:
|
||||
parameters:
|
||||
- description: 授权记录ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
description: 授权记录ID
|
||||
minimum: 0
|
||||
type: integer
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DtoUpdateAuthorizationRemarkReq'
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DtoAuthorizationItem'
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 请求参数错误
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 未认证或认证已过期
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 无权访问
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 服务器内部错误
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: 修改授权备注
|
||||
tags:
|
||||
- 授权记录管理
|
||||
/api/admin/commission/withdrawal-requests:
|
||||
get:
|
||||
parameters:
|
||||
@@ -4149,58 +4320,6 @@ paths:
|
||||
summary: 授权卡给企业
|
||||
tags:
|
||||
- 企业卡授权
|
||||
/api/admin/enterprises/{id}/allocate-cards/preview:
|
||||
post:
|
||||
parameters:
|
||||
- description: 企业ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
description: 企业ID
|
||||
minimum: 0
|
||||
type: integer
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DtoAllocateCardsPreviewReq'
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DtoAllocateCardsPreviewResp'
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 请求参数错误
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 未认证或认证已过期
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 无权访问
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
description: 服务器内部错误
|
||||
security:
|
||||
- BearerAuth: []
|
||||
summary: 卡授权预检
|
||||
tags:
|
||||
- 企业卡授权
|
||||
/api/admin/enterprises/{id}/cards:
|
||||
get:
|
||||
parameters:
|
||||
|
||||
428
docs/proposals/单卡授权企业功能设计.md
Normal file
428
docs/proposals/单卡授权企业功能设计.md
Normal file
@@ -0,0 +1,428 @@
|
||||
# 单卡授权企业功能设计
|
||||
|
||||
## 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. **测试**时注意验证各种边界情况
|
||||
Reference in New Issue
Block a user