2026-08-11 20:49:28 +08:00
|
|
|
|
// 功能描述:实现平台总后台的用户地址与服务关系管理。
|
|
|
|
|
|
// 版本:v1.1
|
2026-07-28 13:55:40 +08:00
|
|
|
|
package user
|
2026-07-27 02:03:26 +08:00
|
|
|
|
|
|
|
|
|
|
import (
|
2026-07-29 14:25:38 +08:00
|
|
|
|
"errors"
|
|
|
|
|
|
|
2026-07-27 02:03:26 +08:00
|
|
|
|
"git.apinb.com/bsm-sdk/core/errcode"
|
|
|
|
|
|
"git.apinb.com/bsm-sdk/core/infra"
|
|
|
|
|
|
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
|
2026-07-28 10:42:26 +08:00
|
|
|
|
"git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
|
2026-07-27 02:03:26 +08:00
|
|
|
|
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
2026-07-29 14:03:10 +08:00
|
|
|
|
"gorm.io/gorm"
|
2026-07-27 02:03:26 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
2026-07-28 13:55:40 +08:00
|
|
|
|
type addressRequest struct {
|
2026-07-27 10:11:07 +08:00
|
|
|
|
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"`
|
2026-07-27 02:03:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-11 20:49:28 +08:00
|
|
|
|
// 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
|
|
|
|
|
|
}
|
2026-07-27 02:03:26 +08:00
|
|
|
|
func CreateUserAddress(ctx *gin.Context) {
|
2026-07-28 13:55:40 +08:00
|
|
|
|
var request addressRequest
|
2026-07-27 02:03:26 +08:00
|
|
|
|
if err := ctx.ShouldBindJSON(&request); err != nil {
|
|
|
|
|
|
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-07-28 10:42:26 +08:00
|
|
|
|
userAccountID, err := common.ResolveIdentityID(&models.UserAccount{}, request.UserAccountIdentity, true)
|
2026-07-27 10:11:07 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-07-29 13:18:52 +08:00
|
|
|
|
address := models.UserAddress{Entity: common.NewEntity(common.StatusEnable), UserAccountID: userAccountID, Address: request.Address, Longitude: request.Longitude, Latitude: request.Latitude, IsDefault: request.IsDefault}
|
2026-07-29 14:03:10 +08:00
|
|
|
|
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 {
|
2026-07-27 02:03:26 +08:00
|
|
|
|
infra.Response.Error(ctx, err)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-07-28 10:42:26 +08:00
|
|
|
|
common.RespondCreatedResource(ctx, address)
|
2026-07-27 02:03:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
func UpdateUserAddress(ctx *gin.Context) {
|
2026-07-28 13:55:40 +08:00
|
|
|
|
var request addressRequest
|
2026-07-27 02:03:26 +08:00
|
|
|
|
if err := ctx.ShouldBindJSON(&request); err != nil {
|
|
|
|
|
|
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-07-28 10:42:26 +08:00
|
|
|
|
userAccountID, err := common.ResolveIdentityID(&models.UserAccount{}, request.UserAccountIdentity, true)
|
2026-07-27 10:11:07 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-07-29 14:03:10 +08:00
|
|
|
|
err = impl.DBService.Transaction(func(tx *gorm.DB) error {
|
|
|
|
|
|
var current models.UserAddress
|
|
|
|
|
|
if err := tx.Where("identity = ?", ctx.Param("identity")).First(¤t).Error; err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
2026-07-29 14:25:38 +08:00
|
|
|
|
if current.UserAccountID != userAccountID {
|
|
|
|
|
|
return errors.New("address owner cannot be changed")
|
|
|
|
|
|
}
|
2026-07-29 14:03:10 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-08-11 20:18:18 +08:00
|
|
|
|
return tx.Model(¤t).Updates(userAddressUpdateValues(request, userAccountID)).Error
|
2026-07-29 14:03:10 +08:00
|
|
|
|
})
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
common.RespondRecordError(ctx, err)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
infra.Response.Success(ctx, gin.H{"updated": true})
|
2026-07-27 02:03:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-11 20:18:18 +08:00
|
|
|
|
// 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,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-28 13:55:40 +08:00
|
|
|
|
type serviceRelationRequest struct {
|
2026-07-27 10:11:07 +08:00
|
|
|
|
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"`
|
2026-07-27 02:03:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-28 10:42:26 +08:00
|
|
|
|
func ListUserServiceRelation(ctx *gin.Context) { common.ListPage[models.UserServiceRelation](ctx) }
|
|
|
|
|
|
func GetUserServiceRelation(ctx *gin.Context) { common.GetByIdentity[models.UserServiceRelation](ctx) }
|
2026-07-27 02:03:26 +08:00
|
|
|
|
func CreateUserServiceRelation(ctx *gin.Context) {
|
2026-07-28 13:55:40 +08:00
|
|
|
|
var request serviceRelationRequest
|
2026-07-27 02:03:26 +08:00
|
|
|
|
if err := ctx.ShouldBindJSON(&request); err != nil {
|
|
|
|
|
|
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-07-28 13:55:40 +08:00
|
|
|
|
userAccountID, gasBasicID, deliveryBasicID, staffAccountID, ok := resolveServiceRelation(ctx, request)
|
|
|
|
|
|
if !ok {
|
2026-07-27 10:11:07 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-07-29 13:18:52 +08:00
|
|
|
|
relation := models.UserServiceRelation{Entity: common.NewEntity(common.StatusEnable), UserAccountID: userAccountID, GasBasicID: gasBasicID, DeliveryBasicID: deliveryBasicID, StaffAccountID: staffAccountID}
|
2026-07-27 02:03:26 +08:00
|
|
|
|
if err := impl.DBService.Create(&relation).Error; err != nil {
|
|
|
|
|
|
infra.Response.Error(ctx, err)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-07-28 10:42:26 +08:00
|
|
|
|
common.RespondCreatedResource(ctx, relation)
|
2026-07-27 02:03:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
func UpdateUserServiceRelation(ctx *gin.Context) {
|
2026-07-28 13:55:40 +08:00
|
|
|
|
var request serviceRelationRequest
|
2026-07-27 02:03:26 +08:00
|
|
|
|
if err := ctx.ShouldBindJSON(&request); err != nil {
|
|
|
|
|
|
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-07-28 13:55:40 +08:00
|
|
|
|
userAccountID, gasBasicID, deliveryBasicID, staffAccountID, ok := resolveServiceRelation(ctx, request)
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-07-29 15:54:20 +08:00
|
|
|
|
var current models.UserServiceRelation
|
|
|
|
|
|
if err := common.ActiveRecords(impl.DBService).Select("user_account_id").
|
|
|
|
|
|
Where("identity = ?", ctx.Param("identity")).First(¤t).Error; err != nil {
|
|
|
|
|
|
common.RespondRecordError(ctx, err)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if current.UserAccountID != userAccountID {
|
|
|
|
|
|
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-07-28 13:55:40 +08:00
|
|
|
|
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) {
|
2026-07-28 10:42:26 +08:00
|
|
|
|
userAccountID, err := common.ResolveIdentityID(&models.UserAccount{}, request.UserAccountIdentity, true)
|
2026-07-27 10:11:07 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
2026-07-28 13:55:40 +08:00
|
|
|
|
return 0, 0, 0, 0, false
|
2026-07-27 10:11:07 +08:00
|
|
|
|
}
|
2026-07-28 10:42:26 +08:00
|
|
|
|
gasBasicID, err := common.ResolveIdentityID(&models.GasBasic{}, request.GasBasicIdentity, false)
|
2026-07-27 10:11:07 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
2026-07-28 13:55:40 +08:00
|
|
|
|
return 0, 0, 0, 0, false
|
2026-07-27 10:11:07 +08:00
|
|
|
|
}
|
2026-07-28 10:42:26 +08:00
|
|
|
|
deliveryBasicID, err := common.ResolveIdentityID(&models.DeliveryBasic{}, request.DeliveryBasicIdentity, false)
|
2026-07-27 10:11:07 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
2026-07-28 13:55:40 +08:00
|
|
|
|
return 0, 0, 0, 0, false
|
2026-07-27 10:11:07 +08:00
|
|
|
|
}
|
2026-07-28 10:42:26 +08:00
|
|
|
|
staffAccountID, err := common.ResolveIdentityID(&models.StaffAccount{}, request.StaffAccountIdentity, false)
|
2026-07-27 10:11:07 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
2026-07-28 13:55:40 +08:00
|
|
|
|
return 0, 0, 0, 0, false
|
2026-07-27 10:11:07 +08:00
|
|
|
|
}
|
2026-08-12 00:37:58 +08:00
|
|
|
|
if !validateServiceRelationOrganization(gasBasicID, deliveryBasicID, staffAccountID) {
|
2026-07-29 14:25:38 +08:00
|
|
|
|
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
|
|
|
|
|
return 0, 0, 0, 0, false
|
|
|
|
|
|
}
|
2026-07-28 13:55:40 +08:00
|
|
|
|
return userAccountID, gasBasicID, deliveryBasicID, staffAccountID, true
|
2026-07-27 02:03:26 +08:00
|
|
|
|
}
|
2026-08-12 00:37:58 +08:00
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
|
}
|