2026-08-11 20:49:28 +08:00
// 功能描述:实现平台总后台的用户地址与服务关系管理。
2026-08-18 22:49:14 +08:00
// 版本: v1.2
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-08-13 22:50:37 +08:00
"strings"
2026-07-29 14:25:38 +08:00
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 { } )
2026-08-13 22:50:37 +08:00
query = filterUserAddressByContract ( query , ctx . Query ( "gasorder_contract_identity" ) )
2026-08-11 20:49:28 +08:00
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 ) } )
}
2026-08-13 22:50:37 +08:00
// 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 )
}
2026-08-11 20:49:28 +08:00
// 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 ( & current ) . 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 ( & current ) . 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-08-18 22:49:14 +08:00
var errUserServiceTransferBlocked = errors . New ( "用户存在生效合同、未完成订单或未关闭工单,请处理完成后再变更所属气站" )
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
2026-08-18 22:49:14 +08:00
if err := common . ActiveRecords ( impl . DBService ) . Select ( "user_account_id" , "gas_basic_id" ) .
2026-07-29 15:54:20 +08:00
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
}
2026-08-18 22:49:14 +08:00
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
}
}
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" } )
}
2026-08-18 22:49:14 +08:00
// 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
}
2026-07-28 13:55:40 +08:00
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
}