2026-09-13 00:57:32 +08:00
// 功能描述:用户地址列表及本人客服工单创建、查询、确认和取消。
// 版本: 1.1.0。
2026-07-30 18:04:34 +08:00
package user
import (
2026-09-13 00:57:32 +08:00
"errors"
2026-07-30 18:04:34 +08:00
"strings"
"time"
"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-31 09:54:17 +08:00
common "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
2026-07-30 18:04:34 +08:00
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
2026-09-13 00:57:32 +08:00
"gorm.io/gorm/clause"
2026-07-30 18:04:34 +08:00
)
// ListAddresses 返回当前用户未归档地址。
func ListAddresses ( ctx * gin . Context ) {
2026-07-31 09:54:17 +08:00
account , ok := common . UserAccount ( ctx )
2026-07-30 18:04:34 +08:00
if ! ok {
return
}
var list [ ] models . UserAddress
2026-07-31 09:54:17 +08:00
if err := impl . DBService . Where ( "user_account_id = ? AND status <> ?" , account . ID , common . StatusArchived ) . Order ( "is_default desc, created_at desc" ) . Find ( & list ) . Error ; err != nil {
2026-07-30 18:04:34 +08:00
infra . Response . Error ( ctx , err )
return
}
2026-07-31 09:54:17 +08:00
infra . Response . Success ( ctx , common . ResourceResponse ( list ) )
2026-07-30 18:04:34 +08:00
}
var userTicketCategories = map [ string ] bool {
"installation" : true , "repair" : true , "inspection" : true , "reinspection" : true , "customer_service" : true ,
}
// CreateTicket 创建工单,服务人员只能由后台分派。
func CreateTicket ( ctx * gin . Context ) {
2026-07-31 09:54:17 +08:00
account , ok := common . UserAccount ( ctx )
2026-07-30 18:04:34 +08:00
if ! ok {
return
}
var request struct {
2026-09-13 00:57:32 +08:00
RequestNo string ` json:"request_no" binding:"required,max=128" `
Category string ` json:"category" binding:"required" `
Description string ` json:"description" binding:"required,max=2000" `
AddressIdentity string ` json:"address_identity" `
AppointmentAt * time . Time ` json:"appointment_at" `
FaultType string ` json:"fault_type" binding:"omitempty,oneof=leak valve alarm other" `
Photos [ ] ticketPhotoRequest ` json:"photos" binding:"max=3,dive" `
}
if ctx . ShouldBindJSON ( & request ) != nil || ! userTicketCategories [ request . Category ] || strings . TrimSpace ( request . Description ) == "" || strings . TrimSpace ( request . RequestNo ) == "" {
2026-07-30 18:04:34 +08:00
infra . Response . Error ( ctx , errcode . ErrInvalidArgument )
return
}
2026-09-13 00:57:32 +08:00
var ticket models . CsTicket
err := impl . DBService . Transaction ( func ( tx * gorm . DB ) error {
// 同一账户串行创建;重试先返回首次事实,不受地址后续修改或删除影响。
if err := lockAddressOwner ( tx , account . ID ) ; err != nil {
return err
2026-07-30 18:04:34 +08:00
}
2026-09-13 00:57:32 +08:00
err := tx . Where ( "request_no = ? AND user_account_id = ?" , request . RequestNo , account . ID ) . First ( & ticket ) . Error
if err == nil {
return nil
}
if ! errors . Is ( err , gorm . ErrRecordNotFound ) {
return err
}
if request . AppointmentAt != nil && request . AppointmentAt . Before ( time . Now ( ) ) {
return errcode . ErrInvalidArgument
}
var relation models . UserServiceRelation
if err := tx . Where ( "user_account_id = ? AND status = ?" , account . ID , common . StatusEnable ) . First ( & relation ) . Error ; err != nil && ! errors . Is ( err , gorm . ErrRecordNotFound ) {
return err
}
addressText , contactName , contactPhone := "" , account . Name , account . Phone
if request . AddressIdentity != "" {
var address models . UserAddress
if err := tx . Where ( "identity = ? AND user_account_id = ? AND status <> ?" , request . AddressIdentity , account . ID , common . StatusArchived ) . First ( & address ) . Error ; err != nil {
return errcode . ErrRecordNotFound
}
addressText = address . Address
if address . ContactName != "" {
contactName = address . ContactName
}
if address . ContactPhone != "" {
contactPhone = address . ContactPhone
}
}
ticket = models . CsTicket {
Entity : common . NewEntity ( common . StatusEnable ) , TicketStatus : 32 , TicketNo : common . RecordNo ( "TK" ) , RequestNo : request . RequestNo ,
UserAccountID : account . ID , GasBasicID : relation . GasBasicID , DeliveryBasicID : relation . DeliveryBasicID ,
Category : request . Category , Priority : "normal" , Description : strings . TrimSpace ( request . Description ) , FaultType : request . FaultType ,
Address : addressText , ContactName : contactName , ContactPhone : contactPhone , AppointmentAt : request . AppointmentAt , OperatorIdentity : account . Identity ,
}
if err := tx . Create ( & ticket ) . Error ; err != nil {
return err
}
return saveTicketPhotos ( tx , ticket , account . Identity , request . Photos )
} )
if err != nil {
2026-07-30 18:04:34 +08:00
infra . Response . Error ( ctx , err )
return
}
infra . Response . Success ( ctx , gin . H { "identity" : ticket . Identity , "ticket_no" : ticket . TicketNo , "ticket_status" : ticket . TicketStatus } )
}
// ListTickets 仅返回当前用户自己的工单。
func ListTickets ( ctx * gin . Context ) {
2026-07-31 09:54:17 +08:00
account , ok := common . UserAccount ( ctx )
2026-07-30 18:04:34 +08:00
if ! ok {
return
}
var list [ ] models . CsTicket
2026-07-31 09:54:17 +08:00
if err := impl . DBService . Where ( "user_account_id = ? AND status <> ?" , account . ID , common . StatusArchived ) . Order ( "created_at desc" ) . Find ( & list ) . Error ; err != nil {
2026-07-30 18:04:34 +08:00
infra . Response . Error ( ctx , err )
return
}
2026-09-13 00:57:32 +08:00
items := make ( [ ] any , 0 , len ( list ) )
photos , err := ticketPhotosForList ( list )
if err != nil {
infra . Response . Error ( ctx , err )
2026-07-30 18:04:34 +08:00
return
}
2026-09-13 00:57:32 +08:00
for _ , ticket := range list {
item := common . ResourceResponse ( ticket ) . ( map [ string ] any )
item [ "status_name" ] , item [ "allowed_actions" ] = ticketState ( ticket . TicketStatus )
item [ "photos" ] = photos [ ticket . ID ]
items = append ( items , item )
2026-07-30 18:04:34 +08:00
}
2026-09-13 00:57:32 +08:00
infra . Response . Success ( ctx , items )
}
// ticketState 发布已有状态机允许的动作;未知状态不授予任何写权限。
func ticketState ( status int ) ( string , [ ] string ) {
names := map [ int ] string { 32 : "待受理" , 18 : "已派单" , 11 : "处理中" , 21 : "处理异常" , 34 : "待确认" , 23 : "已完成" , 22 : "已取消" }
name , exists := names [ status ]
if ! exists {
name = "状态待核实"
}
actions := [ ] string { }
switch status {
case 32 , 18 , 11 , 21 , 34 :
actions = append ( actions , "cancel" )
}
if status == 34 {
actions = append ( actions , "confirm" )
}
return name , actions
}
// ConfirmTicket 用户确认工作人员提交的处理结果,重复确认不覆盖完成时间。
func ConfirmTicket ( ctx * gin . Context ) {
changeTicketState ( ctx , "confirm" , 23 )
2026-07-30 18:04:34 +08:00
}
// CancelTicket 取消尚未完成的本人工单。
func CancelTicket ( ctx * gin . Context ) {
2026-09-13 00:57:32 +08:00
changeTicketState ( ctx , "cancel" , 22 )
}
// changeTicketState 锁定本人工单并校验状态,归档和其他用户的记录不可操作。
func changeTicketState ( ctx * gin . Context , action string , target int ) {
2026-07-31 09:54:17 +08:00
account , ok := common . UserAccount ( ctx )
2026-07-30 18:04:34 +08:00
if ! ok {
return
}
2026-09-13 00:57:32 +08:00
err := impl . DBService . Transaction ( func ( tx * gorm . DB ) error {
var ticket models . CsTicket
if err := tx . Clauses ( clause . Locking { Strength : "UPDATE" } ) . Where ( "identity = ? AND user_account_id = ? AND status <> ?" , ctx . Param ( "identity" ) , account . ID , common . StatusArchived ) . First ( & ticket ) . Error ; err != nil {
return errcode . ErrRecordNotFound
}
if ticket . TicketStatus == target {
return nil
}
_ , actions := ticketState ( ticket . TicketStatus )
allowed := false
for _ , candidate := range actions {
allowed = allowed || candidate == action
}
if ! allowed {
return errcode . ErrInvalidArgument
}
values := map [ string ] any { "ticket_status" : target , "operator_identity" : account . Identity }
if action == "confirm" {
values [ "completed_at" ] = time . Now ( )
}
return tx . Model ( & ticket ) . Updates ( values ) . Error
} )
if err != nil {
infra . Response . Error ( ctx , err )
2026-07-30 18:04:34 +08:00
return
}
2026-09-13 00:57:32 +08:00
if action == "confirm" {
infra . Response . Success ( ctx , gin . H { "confirmed" : true } )
} else {
infra . Response . Success ( ctx , gin . H { "cancelled" : true } )
2026-07-30 18:04:34 +08:00
}
}