Files
platforms/backend/api/internal/logic/platform/user/relation.go
czl231 6bcde94388 功能:完善用户服务关系展示与组织联动
统一平台、气站和配送点后台的服务关系业务名称展示。
新增气站、配送点、服务人员三级联动及暂未分配语义。
后端补充组织归属、人员角色和启用状态的严格校验。
补充前后端测试、项目文档、操作日志及本机日志忽略规则。
2026-08-12 00:37:58 +08:00

264 lines
10 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.
// 功能描述:实现平台总后台的用户地址与服务关系管理。
// 版本v1.1
package user
import (
"errors"
"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"
"gorm.io/gorm"
)
type addressRequest struct {
UserAccountIdentity string `json:"user_account_identity" binding:"required"`
Address string `json:"address" binding:"required,max=255"`
Longitude string `json:"longitude" binding:"max=32"`
Latitude string `json:"latitude" binding:"max=32"`
IsDefault bool `json:"is_default"`
}
// ListUserAddress 查询用户地址分页列表,并为已授权的平台页面恢复地址与坐标。
func ListUserAddress(ctx *gin.Context) {
page, size := common.PageSize(ctx)
var list []models.UserAddress
var total int64
query := common.ApplyKeywordFilter(ctx, common.ActiveRecords(impl.DBService.Model(&models.UserAddress{})), &models.UserAddress{})
if err := query.Count(&total).Error; err != nil {
infra.Response.Error(ctx, err)
return
}
if err := query.Order("created_at desc").Offset((page - 1) * size).Limit(size).Find(&list).Error; err != nil {
infra.Response.Error(ctx, err)
return
}
response, err := common.PublicResourceResponse(list)
if err != nil {
infra.Response.Error(ctx, err)
return
}
protected := common.ProtectPreciseLocation(ctx, &models.UserAddress{}, response)
infra.Response.Success(ctx, gin.H{"total": total, "list": restoreUserAddressLocations(protected, list)})
}
// GetUserAddress 查询用户地址详情,并返回详情与编辑页需要的地址和坐标。
func GetUserAddress(ctx *gin.Context) {
var address models.UserAddress
if err := common.ActiveRecords(impl.DBService).Where("identity = ?", ctx.Param("identity")).First(&address).Error; err != nil {
common.RespondRecordError(ctx, err)
return
}
response, err := common.PublicResourceResponse(address)
if err != nil {
infra.Response.Error(ctx, err)
return
}
protected := common.ProtectPreciseLocation(ctx, &models.UserAddress{}, response)
infra.Response.Success(ctx, restoreUserAddressLocations(protected, []models.UserAddress{address}))
}
// restoreUserAddressLocations 仅在用户地址受控接口中恢复完整地址和精确坐标。
func restoreUserAddressLocations(response any, addresses []models.UserAddress) any {
restore := func(record map[string]any, address models.UserAddress) {
record["address"] = address.Address
record["longitude"] = address.Longitude
record["latitude"] = address.Latitude
}
switch data := response.(type) {
case []any:
for index, item := range data {
if index >= len(addresses) {
break
}
if record, ok := item.(map[string]any); ok {
restore(record, addresses[index])
}
}
case map[string]any:
if len(addresses) > 0 {
restore(data, addresses[0])
}
}
return response
}
func CreateUserAddress(ctx *gin.Context) {
var request addressRequest
if err := ctx.ShouldBindJSON(&request); err != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
userAccountID, err := common.ResolveIdentityID(&models.UserAccount{}, request.UserAccountIdentity, true)
if err != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
address := models.UserAddress{Entity: common.NewEntity(common.StatusEnable), UserAccountID: userAccountID, Address: request.Address, Longitude: request.Longitude, Latitude: request.Latitude, IsDefault: request.IsDefault}
if err := impl.DBService.Transaction(func(tx *gorm.DB) error {
if request.IsDefault {
if err := tx.Model(&models.UserAddress{}).Where("user_account_id = ?", userAccountID).Update("is_default", false).Error; err != nil {
return err
}
}
return tx.Create(&address).Error
}); err != nil {
infra.Response.Error(ctx, err)
return
}
common.RespondCreatedResource(ctx, address)
}
func UpdateUserAddress(ctx *gin.Context) {
var request addressRequest
if err := ctx.ShouldBindJSON(&request); err != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
userAccountID, err := common.ResolveIdentityID(&models.UserAccount{}, request.UserAccountIdentity, true)
if err != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
err = impl.DBService.Transaction(func(tx *gorm.DB) error {
var current models.UserAddress
if err := tx.Where("identity = ?", ctx.Param("identity")).First(&current).Error; err != nil {
return err
}
if current.UserAccountID != userAccountID {
return errors.New("address owner cannot be changed")
}
if request.IsDefault {
if err := tx.Model(&models.UserAddress{}).
Where("user_account_id = ? AND id <> ?", userAccountID, current.ID).
Update("is_default", false).Error; err != nil {
return err
}
}
return tx.Model(&current).Updates(userAddressUpdateValues(request, userAccountID)).Error
})
if err != nil {
common.RespondRecordError(ctx, err)
return
}
infra.Response.Success(ctx, gin.H{"updated": true})
}
// userAddressUpdateValues 构造 GORM 支持的标准更新映射,并保留 false 等零值字段。
func userAddressUpdateValues(request addressRequest, userAccountID uint64) map[string]any {
return map[string]any{
"user_account_id": userAccountID,
"address": request.Address,
"longitude": request.Longitude,
"latitude": request.Latitude,
"is_default": request.IsDefault,
}
}
type serviceRelationRequest struct {
UserAccountIdentity string `json:"user_account_identity" binding:"required"`
GasBasicIdentity string `json:"gas_basic_identity"`
DeliveryBasicIdentity string `json:"delivery_basic_identity"`
StaffAccountIdentity string `json:"staff_account_identity"`
}
func ListUserServiceRelation(ctx *gin.Context) { common.ListPage[models.UserServiceRelation](ctx) }
func GetUserServiceRelation(ctx *gin.Context) { common.GetByIdentity[models.UserServiceRelation](ctx) }
func CreateUserServiceRelation(ctx *gin.Context) {
var request serviceRelationRequest
if err := ctx.ShouldBindJSON(&request); err != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
userAccountID, gasBasicID, deliveryBasicID, staffAccountID, ok := resolveServiceRelation(ctx, request)
if !ok {
return
}
relation := models.UserServiceRelation{Entity: common.NewEntity(common.StatusEnable), UserAccountID: userAccountID, GasBasicID: gasBasicID, DeliveryBasicID: deliveryBasicID, StaffAccountID: staffAccountID}
if err := impl.DBService.Create(&relation).Error; err != nil {
infra.Response.Error(ctx, err)
return
}
common.RespondCreatedResource(ctx, relation)
}
func UpdateUserServiceRelation(ctx *gin.Context) {
var request serviceRelationRequest
if err := ctx.ShouldBindJSON(&request); err != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
userAccountID, gasBasicID, deliveryBasicID, staffAccountID, ok := resolveServiceRelation(ctx, request)
if !ok {
return
}
var current models.UserServiceRelation
if err := common.ActiveRecords(impl.DBService).Select("user_account_id").
Where("identity = ?", ctx.Param("identity")).First(&current).Error; err != nil {
common.RespondRecordError(ctx, err)
return
}
if current.UserAccountID != userAccountID {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
common.UpdateAllowedByIdentity(ctx, &models.UserServiceRelation{}, gin.H{"user_account_id": userAccountID, "gas_basic_id": gasBasicID, "delivery_basic_id": deliveryBasicID, "staff_account_id": staffAccountID}, []string{"user_account_id", "gas_basic_id", "delivery_basic_id", "staff_account_id"})
}
func resolveServiceRelation(ctx *gin.Context, request serviceRelationRequest) (uint64, uint64, uint64, uint64, bool) {
userAccountID, err := common.ResolveIdentityID(&models.UserAccount{}, request.UserAccountIdentity, true)
if err != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return 0, 0, 0, 0, false
}
gasBasicID, err := common.ResolveIdentityID(&models.GasBasic{}, request.GasBasicIdentity, false)
if err != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return 0, 0, 0, 0, false
}
deliveryBasicID, err := common.ResolveIdentityID(&models.DeliveryBasic{}, request.DeliveryBasicIdentity, false)
if err != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return 0, 0, 0, 0, false
}
staffAccountID, err := common.ResolveIdentityID(&models.StaffAccount{}, request.StaffAccountIdentity, false)
if err != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return 0, 0, 0, 0, false
}
if !validateServiceRelationOrganization(gasBasicID, deliveryBasicID, staffAccountID) {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return 0, 0, 0, 0, false
}
return userAccountID, gasBasicID, deliveryBasicID, staffAccountID, true
}
// validateServiceRelationOrganization 读取组织记录并执行服务关系专用的严格归属校验。
func validateServiceRelationOrganization(gasBasicID, deliveryBasicID, staffAccountID uint64) bool {
var delivery models.DeliveryBasic
if deliveryBasicID != 0 {
if err := common.ActiveRecords(impl.DBService).First(&delivery, deliveryBasicID).Error; err != nil {
return false
}
}
var staff models.StaffAccount
if staffAccountID != 0 {
if err := common.ActiveRecords(impl.DBService).First(&staff, staffAccountID).Error; err != nil {
return false
}
}
return serviceRelationOrganizationMatches(gasBasicID, deliveryBasicID, staffAccountID, delivery, staff)
}
// serviceRelationOrganizationMatches 要求配送点、服务人员与当前选择完全一致;全空表示暂未分配。
func serviceRelationOrganizationMatches(gasBasicID, deliveryBasicID, staffAccountID uint64, delivery models.DeliveryBasic, staff models.StaffAccount) bool {
if deliveryBasicID != 0 && delivery.GasBasicID != gasBasicID {
return false
}
if staffAccountID == 0 {
return true
}
validRole := staff.RoleCode == "installer" || staff.RoleCode == "delivery" || staff.RoleCode == "operations"
return validRole && staff.Status == common.StatusEnable && staff.GasBasicID == gasBasicID && staff.DeliveryBasicID == deliveryBasicID
}