已完成用户APP首期功能开发
交付用户端首期页面、配套接口、后台资源及测试文档。用户APP构建、静态分析和三个管理后台构建通过;完整测试仍有2项失败,后端模型注释检查未通过,详见交付记录。
This commit is contained in:
218
backend/api/internal/logic/client/user/device_groups.go
Normal file
218
backend/api/internal/logic/client/user/device_groups.go
Normal file
@@ -0,0 +1,218 @@
|
||||
// 功能:用户设备分组资料与本人设备归组,严格隔离账户并保持新增幂等;版本: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})
|
||||
}
|
||||
Reference in New Issue
Block a user