Files

366 lines
16 KiB
Go
Raw Permalink Normal View History

// 功能家庭成员邀请确认、按设备授权、撤销和审计版本1.0.0。
package user
import (
"errors"
"fmt"
"strings"
"time"
"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 (
familyInvitePending = 10
familyInviteAccepted = 20
familyInviteRejected = 30
familyInviteRevoked = 40
)
var (
errFamilySelf = errcode.NewError(2510, "不能邀请当前账号加入自己的家庭")
errFamilyDuplicate = errcode.NewError(2511, "该手机号已有待确认或已加入的家庭成员")
errFamilyExpired = errcode.NewError(2512, "邀请已失效,请联系房主重新邀请")
)
type familyDevicePermissionInput struct {
DeviceIdentity string `json:"device_identity"`
CanView bool `json:"can_view"`
CanAlert bool `json:"can_alert"`
CanControl bool `json:"can_control"`
}
type familyInviteInput struct {
Name string `json:"name"`
Phone string `json:"phone"`
Relationship string `json:"relationship"`
RequestNo string `json:"request_no"`
Permissions []familyDevicePermissionInput `json:"device_permissions"`
}
// validFamilyInvite 清理并校验邀请资料;设备授权必须至少包含查看权限。
func validFamilyInvite(input *familyInviteInput) bool {
input.Name, input.Phone, input.Relationship = strings.TrimSpace(input.Name), strings.TrimSpace(input.Phone), strings.TrimSpace(input.Relationship)
parsed, err := uuid.Parse(input.RequestNo)
if err != nil || parsed == uuid.Nil || parsed.String() != input.RequestNo || input.Name == "" || utf8.RuneCountInString(input.Name) > 64 || !common.ValidPhone(input.Phone) || utf8.RuneCountInString(input.Relationship) > 32 {
return false
}
for _, permission := range input.Permissions {
if strings.TrimSpace(permission.DeviceIdentity) == "" || (permission.CanAlert || permission.CanControl) && !permission.CanView {
return false
}
}
return true
}
func maskFamilyPhone(phone string) string {
if len(phone) == 11 {
return phone[:3] + "****" + phone[7:]
}
return "***"
}
// familyAudit 在业务事务内追加不可变审计记录。
func familyAudit(tx *gorm.DB, ownerID, memberID, actorID uint64, action, summary string) error {
return tx.Create(&models.UserFamilyAudit{Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusEnable}, OwnerUserAccountID: ownerID, FamilyMemberID: memberID, ActorUserAccountID: actorID, Action: action, Summary: summary}).Error
}
// familyMemberView 返回脱敏成员和当前设备授权,不暴露内部关联键。
func familyMemberView(record models.UserFamilyMember, shares []models.UserDeviceShare, products map[uint64]models.ProductInfo) gin.H {
permissions := make([]gin.H, 0, len(shares))
for _, share := range shares {
product, ok := products[share.ProductInfoID]
if !ok || share.Status != common.StatusEnable {
continue
}
permissions = append(permissions, gin.H{"device_identity": product.Identity, "device_name": product.Name, "device_kind": product.DeviceKind, "can_view": share.CanView, "can_alert": share.CanAlert, "can_control": share.CanControl})
}
return gin.H{"identity": record.Identity, "name": record.Name, "phone_masked": maskFamilyPhone(record.Phone), "relationship": record.Relationship, "invite_status": record.InviteStatus, "expires_at": record.ExpiresAt, "device_permissions": permissions}
}
// FamilyDashboard 返回本人作为房主的成员、设备和权限汇总。
func FamilyDashboard(ctx *gin.Context) {
account, ok := common.UserAccount(ctx)
if !ok {
return
}
var members []models.UserFamilyMember
if err := impl.DBService.Where("owner_user_account_id = ? AND status = ? AND invite_status IN ?", account.ID, common.StatusEnable, []int{familyInvitePending, familyInviteAccepted}).Order("created_at, identity").Find(&members).Error; err != nil {
infra.Response.Error(ctx, err)
return
}
var devices []models.ProductInfo
if err := ownedProductQuery(account.ID).Where("device_kind IN ? AND product_status <> ?", []string{"valve", "alarm"}, common.StatusScrapped).Order("created_at, identity").Find(&devices).Error; err != nil {
infra.Response.Error(ctx, err)
return
}
memberIDs := make([]uint64, 0, len(members))
for _, member := range members {
memberIDs = append(memberIDs, member.ID)
}
var shares []models.UserDeviceShare
if len(memberIDs) > 0 {
if err := impl.DBService.Where("family_member_id IN ? AND status = ?", memberIDs, common.StatusEnable).Find(&shares).Error; err != nil {
infra.Response.Error(ctx, err)
return
}
}
productMap := make(map[uint64]models.ProductInfo, len(devices))
for _, device := range devices {
productMap[device.ID] = device
}
sharesByMember := map[uint64][]models.UserDeviceShare{}
shareCounts := map[uint64]int{}
for _, share := range shares {
sharesByMember[share.FamilyMemberID] = append(sharesByMember[share.FamilyMemberID], share)
if share.CanView {
shareCounts[share.ProductInfoID]++
}
}
memberViews := make([]gin.H, 0, len(members)+1)
memberViews = append(memberViews, gin.H{"identity": account.Identity, "name": account.Name, "phone_masked": maskFamilyPhone(account.Phone), "relationship": "房主", "invite_status": familyInviteAccepted, "is_owner": true, "device_permissions": []gin.H{}})
for _, member := range members {
memberViews = append(memberViews, familyMemberView(member, sharesByMember[member.ID], productMap))
}
deviceViews := make([]gin.H, 0, len(devices))
for _, device := range devices {
deviceViews = append(deviceViews, gin.H{"identity": device.Identity, "name": device.Name, "kind": device.DeviceKind, "share_count": shareCounts[device.ID], "mapping_configured": device.VendorDeviceID != ""})
}
incoming, err := incomingFamilyInvitations(account)
if err != nil {
infra.Response.Error(ctx, err)
return
}
infra.Response.Success(ctx, gin.H{"household_name": account.Name + "的家", "owner_name": account.Name, "member_count": len(members), "device_count": len(deviceViews), "members": memberViews, "devices": deviceViews, "incoming_invitations": incoming})
}
// incomingFamilyInvitations 读取当前手机号收到的有效邀请及房主名称。
func incomingFamilyInvitations(account models.UserAccount) ([]gin.H, error) {
var rows []models.UserFamilyMember
if err := impl.DBService.Where("phone = ? AND status = ? AND invite_status = ? AND expires_at > ?", account.Phone, common.StatusEnable, familyInvitePending, time.Now()).Order("created_at desc").Find(&rows).Error; err != nil {
return nil, err
}
ownerIDs := make([]uint64, 0, len(rows))
for _, row := range rows {
ownerIDs = append(ownerIDs, row.OwnerUserAccountID)
}
owners := map[uint64]string{}
if len(ownerIDs) > 0 {
var users []models.UserAccount
if err := impl.DBService.Where("id IN ?", ownerIDs).Find(&users).Error; err != nil {
return nil, err
}
for _, user := range users {
owners[user.ID] = user.Name
}
}
items := make([]gin.H, 0, len(rows))
for _, row := range rows {
items = append(items, gin.H{"identity": row.Identity, "owner_name": owners[row.OwnerUserAccountID], "name": row.Name, "relationship": row.Relationship, "expires_at": row.ExpiresAt})
}
return items, nil
}
// InviteFamilyMember 创建七天有效邀请及预设权限;成员接受前不会获得访问权。
func InviteFamilyMember(ctx *gin.Context) {
account, ok := common.UserAccount(ctx)
if !ok {
return
}
var input familyInviteInput
if ctx.ShouldBindJSON(&input) != nil || !validFamilyInvite(&input) {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
if input.Phone == account.Phone {
infra.Response.Error(ctx, errFamilySelf)
return
}
var record models.UserFamilyMember
err := impl.DBService.Transaction(func(tx *gorm.DB) error {
if err := lockAddressOwner(tx, account.ID); err != nil {
return err
}
if err := tx.Where("owner_user_account_id = ? AND request_no = ?", account.ID, input.RequestNo).First(&record).Error; err == nil {
return nil
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
var duplicate int64
if err := tx.Model(&models.UserFamilyMember{}).Where("owner_user_account_id = ? AND phone = ? AND status = ? AND invite_status IN ?", account.ID, input.Phone, common.StatusEnable, []int{familyInvitePending, familyInviteAccepted}).Count(&duplicate).Error; err != nil {
return err
}
if duplicate > 0 {
return errFamilyDuplicate
}
record = models.UserFamilyMember{Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusEnable}, OwnerUserAccountID: account.ID, Name: input.Name, Phone: input.Phone, Relationship: input.Relationship, InviteStatus: familyInvitePending, RequestNo: input.RequestNo, ExpiresAt: time.Now().Add(7 * 24 * time.Hour)}
if err := tx.Create(&record).Error; err != nil {
return err
}
if err := replaceFamilyPermissions(tx, account.ID, record.ID, input.Permissions); err != nil {
return err
}
return familyAudit(tx, account.ID, record.ID, account.ID, "invite", fmt.Sprintf("邀请%s预设%d台设备权限", record.Name, len(input.Permissions)))
})
if err != nil {
infra.Response.Error(ctx, err)
return
}
infra.Response.Success(ctx, gin.H{"identity": record.Identity, "invite_status": record.InviteStatus, "expires_at": record.ExpiresAt})
}
// replaceFamilyPermissions 仅允许房主名下设备,空列表表示撤销全部设备授权。
func replaceFamilyPermissions(tx *gorm.DB, ownerID, memberID uint64, inputs []familyDevicePermissionInput) error {
if err := tx.Model(&models.UserDeviceShare{}).Where("family_member_id = ? AND status = ?", memberID, common.StatusEnable).Update("status", common.StatusArchived).Error; err != nil {
return err
}
seen := map[string]bool{}
for _, input := range inputs {
identity := strings.TrimSpace(input.DeviceIdentity)
if seen[identity] {
return errcode.ErrInvalidArgument
}
seen[identity] = true
var product models.ProductInfo
if err := tx.Where("identity = ? AND user_account_id = ? AND status = ? AND device_kind IN ? AND product_status <> ?", identity, ownerID, common.StatusEnable, []string{"valve", "alarm"}, common.StatusScrapped).First(&product).Error; err != nil {
return errcode.ErrPermissionDenied
}
share := models.UserDeviceShare{Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusEnable}, FamilyMemberID: memberID, ProductInfoID: product.ID, CanView: input.CanView, CanAlert: input.CanAlert, CanControl: input.CanControl}
if err := tx.Clauses(clause.OnConflict{Columns: []clause.Column{{Name: "family_member_id"}, {Name: "product_info_id"}}, DoUpdates: clause.Assignments(map[string]interface{}{"status": common.StatusEnable, "can_view": input.CanView, "can_alert": input.CanAlert, "can_control": input.CanControl, "updated_at": time.Now()})}).Create(&share).Error; err != nil {
return err
}
}
return nil
}
// UpdateFamilyPermissions 调整成员设备范围,权限立即生效并写入审计。
func UpdateFamilyPermissions(ctx *gin.Context) {
account, ok := common.UserAccount(ctx)
if !ok {
return
}
var input struct {
Permissions []familyDevicePermissionInput `json:"device_permissions"`
}
if ctx.ShouldBindJSON(&input) != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
for _, permission := range input.Permissions {
if strings.TrimSpace(permission.DeviceIdentity) == "" || (permission.CanAlert || permission.CanControl) && !permission.CanView {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
}
err := impl.DBService.Transaction(func(tx *gorm.DB) error {
var member models.UserFamilyMember
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("identity = ? AND owner_user_account_id = ? AND status = ? AND invite_status IN ?", ctx.Param("identity"), account.ID, common.StatusEnable, []int{familyInvitePending, familyInviteAccepted}).First(&member).Error; err != nil {
return errcode.ErrRecordNotFound
}
if err := replaceFamilyPermissions(tx, account.ID, member.ID, input.Permissions); err != nil {
return err
}
return familyAudit(tx, account.ID, member.ID, account.ID, "update_permissions", fmt.Sprintf("调整%s的设备权限共%d台", member.Name, len(input.Permissions)))
})
if err != nil {
infra.Response.Error(ctx, err)
return
}
infra.Response.Success(ctx, gin.H{"updated": true})
}
// RevokeFamilyMember 撤销邀请或移除成员,同时停用其全部设备授权。
func RevokeFamilyMember(ctx *gin.Context) {
account, ok := common.UserAccount(ctx)
if !ok {
return
}
err := impl.DBService.Transaction(func(tx *gorm.DB) error {
var member models.UserFamilyMember
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("identity = ? AND owner_user_account_id = ? AND status = ? AND invite_status IN ?", ctx.Param("identity"), account.ID, common.StatusEnable, []int{familyInvitePending, familyInviteAccepted}).First(&member).Error; err != nil {
return errcode.ErrRecordNotFound
}
now := time.Now()
if err := tx.Model(&member).Updates(map[string]interface{}{"invite_status": familyInviteRevoked, "revoked_at": now, "status": common.StatusArchived}).Error; err != nil {
return err
}
if err := tx.Model(&models.UserDeviceShare{}).Where("family_member_id = ? AND status = ?", member.ID, common.StatusEnable).Update("status", common.StatusArchived).Error; err != nil {
return err
}
return familyAudit(tx, account.ID, member.ID, account.ID, "revoke", "撤销成员"+member.Name)
})
if err != nil {
infra.Response.Error(ctx, err)
return
}
infra.Response.Success(ctx, gin.H{"revoked": true})
}
// ListFamilyInvitations 返回当前手机号尚未过期的邀请。
func ListFamilyInvitations(ctx *gin.Context) {
account, ok := common.UserAccount(ctx)
if !ok {
return
}
items, err := incomingFamilyInvitations(account)
if err != nil {
infra.Response.Error(ctx, err)
return
}
infra.Response.Success(ctx, items)
}
// RespondFamilyInvitation 仅受邀手机号本人可接受或拒绝邀请。
func RespondFamilyInvitation(ctx *gin.Context) {
account, ok := common.UserAccount(ctx)
if !ok {
return
}
var input struct {
Accept bool `json:"accept"`
}
if ctx.ShouldBindJSON(&input) != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
err := impl.DBService.Transaction(func(tx *gorm.DB) error {
var member models.UserFamilyMember
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("identity = ? AND phone = ? AND status = ? AND invite_status = ?", ctx.Param("identity"), account.Phone, common.StatusEnable, familyInvitePending).First(&member).Error; err != nil {
return errcode.ErrRecordNotFound
}
if time.Now().After(member.ExpiresAt) {
return errFamilyExpired
}
status, action := familyInviteRejected, "reject"
updates := map[string]interface{}{"invite_status": status, "status": common.StatusArchived}
if input.Accept {
status, action = familyInviteAccepted, "accept"
now := time.Now()
updates = map[string]interface{}{"invite_status": status, "member_user_account_id": account.ID, "accepted_at": now}
}
if err := tx.Model(&member).Updates(updates).Error; err != nil {
return err
}
if !input.Accept {
if err := tx.Model(&models.UserDeviceShare{}).Where("family_member_id = ?", member.ID).Update("status", common.StatusArchived).Error; err != nil {
return err
}
}
return familyAudit(tx, member.OwnerUserAccountID, member.ID, account.ID, action, "成员响应家庭邀请")
})
if err != nil {
infra.Response.Error(ctx, err)
return
}
infra.Response.Success(ctx, gin.H{"accepted": input.Accept})
}