// 功能描述:用户地址列表及本人客服工单创建、查询、确认和取消。 // 版本:1.1.0。 package user import ( "errors" "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" common "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" "gorm.io/gorm/clause" ) // ListAddresses 返回当前用户未归档地址。 func ListAddresses(ctx *gin.Context) { account, ok := common.UserAccount(ctx) if !ok { return } var list []models.UserAddress 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 { infra.Response.Error(ctx, err) return } infra.Response.Success(ctx, common.ResourceResponse(list)) } var userTicketCategories = map[string]bool{ "installation": true, "repair": true, "inspection": true, "reinspection": true, "customer_service": true, } // CreateTicket 创建工单,服务人员只能由后台分派。 func CreateTicket(ctx *gin.Context) { account, ok := common.UserAccount(ctx) if !ok { return } var request struct { 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) == "" { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } var ticket models.CsTicket err := impl.DBService.Transaction(func(tx *gorm.DB) error { // 同一账户串行创建;重试先返回首次事实,不受地址后续修改或删除影响。 if err := lockAddressOwner(tx, account.ID); err != nil { return err } 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 { 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) { account, ok := common.UserAccount(ctx) if !ok { return } var list []models.CsTicket if err := impl.DBService.Where("user_account_id = ? AND status <> ?", account.ID, common.StatusArchived).Order("created_at desc").Find(&list).Error; err != nil { infra.Response.Error(ctx, err) return } items := make([]any, 0, len(list)) photos, err := ticketPhotosForList(list) if err != nil { infra.Response.Error(ctx, err) return } 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) } 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) } // CancelTicket 取消尚未完成的本人工单。 func CancelTicket(ctx *gin.Context) { changeTicketState(ctx, "cancel", 22) } // changeTicketState 锁定本人工单并校验状态,归档和其他用户的记录不可操作。 func changeTicketState(ctx *gin.Context, action string, target int) { account, ok := common.UserAccount(ctx) if !ok { return } 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) return } if action == "confirm" { infra.Response.Success(ctx, gin.H{"confirmed": true}) } else { infra.Response.Success(ctx, gin.H{"cancelled": true}) } }