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})
|
||
}
|