Files
platforms/backend/api/internal/logic/platform/user/relation.go

358 lines
14 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.2
package user
import (
"errors"
"strings"
"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{})
query = filterUserAddressByContract(query, ctx.Query("gasorder_contract_identity"))
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)})
}
// filterUserAddressByContract 仅返回指定配送合同签约用户的地址,避免后台下单时跨用户选择。
func filterUserAddressByContract(query *gorm.DB, contractIdentity string) *gorm.DB {
contractIdentity = strings.TrimSpace(contractIdentity)
if contractIdentity == "" {
return query
}
contractUser := common.ActiveRecords(impl.DBService.Model(&models.GasorderContract{})).
Select("user_account_id").Where("identity = ?", contractIdentity)
return query.Where("user_account_id = (?)", contractUser)
}
// 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"`
}
var errUserServiceTransferBlocked = errors.New("用户存在生效合同、未完成订单或未关闭工单,请处理完成后再变更所属气站")
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", "gas_basic_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
}
if current.GasBasicID != gasBasicID {
blocked, err := userServiceTransferBlocked(impl.DBService, current)
if err != nil {
infra.Response.Error(ctx, err)
return
}
if blocked {
infra.Response.Error(ctx, errUserServiceTransferBlocked)
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"})
}
// UpdateUserServiceRelationStatus 更新服务关系状态;归档必须执行履约阻断检查。
func UpdateUserServiceRelationStatus(ctx *gin.Context) {
var request struct {
Status int `json:"status" binding:"required"`
}
if err := ctx.ShouldBindJSON(&request); err != nil || !common.IsGenericRecordStatus(request.Status) {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
if request.Status == common.StatusArchived && !allowUserServiceRelationRemoval(ctx) {
return
}
common.UpdateAllowedByIdentity(ctx, &models.UserServiceRelation{}, gin.H{"status": request.Status}, []string{"status"})
}
// ArchiveUserServiceRelation 归档服务关系;存在未完成履约业务时拒绝解除归属。
func ArchiveUserServiceRelation(ctx *gin.Context) {
if !allowUserServiceRelationRemoval(ctx) {
return
}
common.UpdateAllowedByIdentity(ctx, &models.UserServiceRelation{}, gin.H{"status": common.StatusArchived}, []string{"status"})
}
// allowUserServiceRelationRemoval 校验当前服务关系是否允许解除。
func allowUserServiceRelationRemoval(ctx *gin.Context) bool {
var current models.UserServiceRelation
if err := common.ActiveRecords(impl.DBService).Where("identity = ?", ctx.Param("identity")).First(&current).Error; err != nil {
common.RespondRecordError(ctx, err)
return false
}
blocked, err := userServiceTransferBlocked(impl.DBService, current)
if err != nil {
infra.Response.Error(ctx, err)
return false
}
if blocked {
infra.Response.Error(ctx, errUserServiceTransferBlocked)
return false
}
return true
}
// userServiceTransferBlocked 检查原气站仍在履约中的合同、订单和工单。
func userServiceTransferBlocked(databaseService *gorm.DB, relation models.UserServiceRelation) (bool, error) {
if relation.GasBasicID == 0 {
return false, nil
}
checks := []struct {
model any
where string
args []any
}{
{&models.GasorderContract{}, "user_account_id = ? AND gas_basic_id = ? AND status <> ? AND contract_status = ?", []any{relation.UserAccountID, relation.GasBasicID, common.StatusArchived, common.StatusActive}},
{&models.GasorderBasic{}, "user_account_id = ? AND gas_basic_id = ? AND status <> ? AND order_status NOT IN ?", []any{relation.UserAccountID, relation.GasBasicID, common.StatusArchived, []int{common.StatusCompleted, common.StatusCancelled}}},
{&models.CsTicket{}, "user_account_id = ? AND gas_basic_id = ? AND status <> ? AND ticket_status = ?", []any{relation.UserAccountID, relation.GasBasicID, common.StatusArchived, common.StatusOpen}},
}
for _, check := range checks {
var count int64
if err := databaseService.Model(check.model).Where(check.where, check.args...).Count(&count).Error; err != nil {
return false, err
}
if count > 0 {
return true, nil
}
}
return false, nil
}
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
}