219 lines
7.5 KiB
Go
219 lines
7.5 KiB
Go
|
|
// 功能:用户设备分组资料与本人设备归组,严格隔离账户并保持新增幂等;版本:1.0.0。
|
|||
|
|
package user
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"errors"
|
|||
|
|
"strings"
|
|||
|
|
"unicode/utf8"
|
|||
|
|
|
|||
|
|
"git.apinb.com/bsm-sdk/core/errcode"
|
|||
|
|
"git.apinb.com/bsm-sdk/core/infra"
|
|||
|
|
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
|
|||
|
|
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
|
|||
|
|
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
|||
|
|
"github.com/gin-gonic/gin"
|
|||
|
|
"github.com/google/uuid"
|
|||
|
|
"gorm.io/gorm"
|
|||
|
|
"gorm.io/gorm/clause"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
const maxDeviceGroups = 20
|
|||
|
|
|
|||
|
|
var (
|
|||
|
|
errDeviceGroupLimit = errcode.NewError(2511, "最多可创建20个设备分组")
|
|||
|
|
errDeviceGroupDuplicate = errcode.NewError(2512, "该分组名称已存在")
|
|||
|
|
errDeviceGroupRequestChanged = errcode.NewError(2513, "该新增请求已处理,请关闭表单并刷新分组列表")
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
type deviceGroupInput struct {
|
|||
|
|
Name string `json:"name"`
|
|||
|
|
RequestNo string `json:"request_no"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// validDeviceGroupInput 清理名称并校验新增幂等标识。
|
|||
|
|
func validDeviceGroupInput(input *deviceGroupInput, creating bool) bool {
|
|||
|
|
input.Name = strings.TrimSpace(input.Name)
|
|||
|
|
if input.Name == "" || utf8.RuneCountInString(input.Name) > 32 {
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
if !creating {
|
|||
|
|
return true
|
|||
|
|
}
|
|||
|
|
parsed, err := uuid.Parse(input.RequestNo)
|
|||
|
|
return err == nil && parsed != uuid.Nil && parsed.String() == input.RequestNo
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ListDeviceGroups 返回本人分组及每组当前仍归属本人的设备公开标识。
|
|||
|
|
func ListDeviceGroups(ctx *gin.Context) {
|
|||
|
|
account, ok := common.UserAccount(ctx)
|
|||
|
|
if !ok {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
var groups []models.UserDeviceGroup
|
|||
|
|
if err := impl.DBService.Where("user_account_id = ? AND status = ?", account.ID, common.StatusEnable).
|
|||
|
|
Order("sort_no, created_at, identity").Limit(maxDeviceGroups).Find(&groups).Error; err != nil {
|
|||
|
|
infra.Response.Error(ctx, err)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
groupIDs := make([]uint64, 0, len(groups))
|
|||
|
|
for _, group := range groups {
|
|||
|
|
groupIDs = append(groupIDs, group.ID)
|
|||
|
|
}
|
|||
|
|
devicesByGroup := map[uint64][]string{}
|
|||
|
|
if len(groupIDs) > 0 {
|
|||
|
|
var devices []models.ProductInfo
|
|||
|
|
if err := ownedProductQuery(account.ID).Where("device_group_id IN ? AND device_kind IN ? AND product_status <> ?", groupIDs, []string{"valve", "alarm"}, common.StatusScrapped).
|
|||
|
|
Order("created_at, identity").Find(&devices).Error; err != nil {
|
|||
|
|
infra.Response.Error(ctx, err)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
for _, device := range devices {
|
|||
|
|
devicesByGroup[device.DeviceGroupID] = append(devicesByGroup[device.DeviceGroupID], device.Identity)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
items := make([]gin.H, 0, len(groups))
|
|||
|
|
for _, group := range groups {
|
|||
|
|
identities := devicesByGroup[group.ID]
|
|||
|
|
if identities == nil {
|
|||
|
|
identities = []string{}
|
|||
|
|
}
|
|||
|
|
items = append(items, gin.H{"identity": group.Identity, "name": group.Name, "sort_no": group.SortNo, "device_identities": identities})
|
|||
|
|
}
|
|||
|
|
infra.Response.Success(ctx, items)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// SaveDeviceGroup 新建或改名;账户行锁保证上限、幂等和同名约束串行执行。
|
|||
|
|
func SaveDeviceGroup(ctx *gin.Context) {
|
|||
|
|
account, ok := common.UserAccount(ctx)
|
|||
|
|
if !ok {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
creating := ctx.Param("identity") == ""
|
|||
|
|
var input deviceGroupInput
|
|||
|
|
if ctx.ShouldBindJSON(&input) != nil || !validDeviceGroupInput(&input, creating) {
|
|||
|
|
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
var group models.UserDeviceGroup
|
|||
|
|
err := impl.DBService.Transaction(func(tx *gorm.DB) error {
|
|||
|
|
if err := lockAddressOwner(tx, account.ID); err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
if creating {
|
|||
|
|
err := tx.Where("user_account_id = ? AND request_no = ?", account.ID, input.RequestNo).First(&group).Error
|
|||
|
|
if err == nil {
|
|||
|
|
if group.Status != common.StatusEnable || group.Name != input.Name {
|
|||
|
|
return errDeviceGroupRequestChanged
|
|||
|
|
}
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
var count int64
|
|||
|
|
if err := tx.Model(&models.UserDeviceGroup{}).Where("user_account_id = ? AND status = ?", account.ID, common.StatusEnable).Count(&count).Error; err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
if count >= maxDeviceGroups {
|
|||
|
|
return errDeviceGroupLimit
|
|||
|
|
}
|
|||
|
|
group = models.UserDeviceGroup{Entity: common.NewEntity(common.StatusEnable), UserAccountID: account.ID, Name: input.Name, RequestNo: input.RequestNo, SortNo: int(count)}
|
|||
|
|
} else if err := tx.Where("identity = ? AND user_account_id = ? AND status = ?", ctx.Param("identity"), account.ID, common.StatusEnable).First(&group).Error; err != nil {
|
|||
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|||
|
|
return errcode.ErrRecordNotFound
|
|||
|
|
}
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
var duplicates int64
|
|||
|
|
if err := tx.Model(&models.UserDeviceGroup{}).Where("user_account_id = ? AND status = ? AND name = ? AND identity <> ?", account.ID, common.StatusEnable, input.Name, group.Identity).Count(&duplicates).Error; err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
if duplicates > 0 {
|
|||
|
|
return errDeviceGroupDuplicate
|
|||
|
|
}
|
|||
|
|
group.Name = input.Name
|
|||
|
|
return tx.Save(&group).Error
|
|||
|
|
})
|
|||
|
|
if err != nil {
|
|||
|
|
infra.Response.Error(ctx, err)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
infra.Response.Success(ctx, gin.H{"identity": group.Identity, "name": group.Name, "sort_no": group.SortNo})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// DeleteDeviceGroup 先解除本人设备归组再归档分组;重复删除保持成功。
|
|||
|
|
func DeleteDeviceGroup(ctx *gin.Context) {
|
|||
|
|
account, ok := common.UserAccount(ctx)
|
|||
|
|
if !ok {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
err := impl.DBService.Transaction(func(tx *gorm.DB) error {
|
|||
|
|
if err := lockAddressOwner(tx, account.ID); err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
var group models.UserDeviceGroup
|
|||
|
|
if err := tx.Where("identity = ? AND user_account_id = ?", ctx.Param("identity"), account.ID).First(&group).Error; err != nil {
|
|||
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|||
|
|
return errcode.ErrRecordNotFound
|
|||
|
|
}
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
if group.Status == common.StatusArchived {
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
if err := tx.Model(&models.ProductInfo{}).Where("user_account_id = ? AND device_group_id = ?", account.ID, group.ID).Update("device_group_id", 0).Error; err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
return tx.Model(&group).Update("status", common.StatusArchived).Error
|
|||
|
|
})
|
|||
|
|
if err != nil {
|
|||
|
|
infra.Response.Error(ctx, err)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
infra.Response.Success(ctx, gin.H{"deleted": true})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// AssignDeviceGroup 将本人启用智能设备移入本人分组;空标识表示移出分组。
|
|||
|
|
func AssignDeviceGroup(ctx *gin.Context) {
|
|||
|
|
account, ok := common.UserAccount(ctx)
|
|||
|
|
if !ok {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
var request struct {
|
|||
|
|
GroupIdentity string `json:"group_identity"`
|
|||
|
|
}
|
|||
|
|
if ctx.ShouldBindJSON(&request) != nil {
|
|||
|
|
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
err := impl.DBService.Transaction(func(tx *gorm.DB) error {
|
|||
|
|
var device models.ProductInfo
|
|||
|
|
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where(
|
|||
|
|
"identity = ? AND user_account_id = ? AND status = ? AND device_kind IN ? AND product_status <> ?",
|
|||
|
|
ctx.Param("identity"), account.ID, common.StatusEnable, []string{"valve", "alarm"}, common.StatusScrapped,
|
|||
|
|
).First(&device).Error; err != nil {
|
|||
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|||
|
|
return errcode.ErrRecordNotFound
|
|||
|
|
}
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
groupID := uint64(0)
|
|||
|
|
if strings.TrimSpace(request.GroupIdentity) != "" {
|
|||
|
|
var group models.UserDeviceGroup
|
|||
|
|
if err := tx.Where("identity = ? AND user_account_id = ? AND status = ?", strings.TrimSpace(request.GroupIdentity), account.ID, common.StatusEnable).First(&group).Error; err != nil {
|
|||
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|||
|
|
return errcode.ErrRecordNotFound
|
|||
|
|
}
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
groupID = group.ID
|
|||
|
|
}
|
|||
|
|
return tx.Model(&device).Update("device_group_id", groupID).Error
|
|||
|
|
})
|
|||
|
|
if err != nil {
|
|||
|
|
infra.Response.Error(ctx, err)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
infra.Response.Success(ctx, gin.H{"updated": true})
|
|||
|
|
}
|