已完成用户APP首期功能开发

交付用户端首期页面、配套接口、后台资源及测试文档。用户APP构建、静态分析和三个管理后台构建通过;完整测试仍有2项失败,后端模型注释检查未通过,详见交付记录。
This commit is contained in:
czl231
2026-09-13 00:57:32 +08:00
parent a0a4bb1218
commit 3ef33b531d
793 changed files with 40959 additions and 1204 deletions

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