Files
platforms/backend/api/internal/logic/client/user/device_groups.go
czl231 3ef33b531d 已完成用户APP首期功能开发
交付用户端首期页面、配套接口、后台资源及测试文档。用户APP构建、静态分析和三个管理后台构建通过;完整测试仍有2项失败,后端模型注释检查未通过,详见交付记录。
2026-09-13 00:57:32 +08:00

219 lines
7.5 KiB
Go
Raw 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.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})
}