package staff import ( "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" ) // Attendance 上下班打卡;存在进行中任务时禁止下班。 func Attendance(ctx *gin.Context) { account, ok := common.StaffAccount(ctx) if !ok { return } var request struct { Action string `json:"action" binding:"required,oneof=clock_in clock_out"` OccurredAt time.Time `json:"occurred_at" binding:"required"` Longitude string `json:"longitude" binding:"required"` Latitude string `json:"latitude" binding:"required"` DeviceIdentity string `json:"device_identity" binding:"required"` RequestNo string `json:"request_no" binding:"required"` } if ctx.ShouldBindJSON(&request) != nil { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } if request.Action == "clock_out" { var count int64 impl.DBService.Model(&models.GasorderBasic{}).Where("staff_account_id = ? AND order_status IN ?", account.ID, []int{18, 11, 21, 34}).Count(&count) if count == 0 { impl.DBService.Model(&models.CsTicket{}).Where("staff_account_id = ? AND ticket_status IN ?", account.ID, []int{18, 11, 21, 34}).Count(&count) } if count > 0 { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } } record := models.StaffAttendance{ Entity: common.NewEntity(common.StatusEnable), StaffAccountID: account.ID, RoleCode: account.RoleCode, Action: request.Action, OccurredAt: request.OccurredAt, Longitude: request.Longitude, Latitude: request.Latitude, DeviceIdentity: request.DeviceIdentity, RequestNo: request.RequestNo, } workStatus := "on_duty" if request.Action == "clock_out" { workStatus = "off_duty" } err := impl.DBService.Transaction(func(tx *gorm.DB) error { if err := tx.Create(&record).Error; err != nil { return err } return tx.Model(&account).Update("work_status", workStatus).Error }) if err != nil { var existing models.StaffAttendance if impl.DBService.Where("request_no = ? AND staff_account_id = ?", request.RequestNo, account.ID).First(&existing).Error != nil { infra.Response.Error(ctx, err) return } record = existing } infra.Response.Success(ctx, gin.H{"identity": record.Identity, "work_status": workStatus}) } // ListTickets 仅返回分派给当前人员且与岗位匹配的工单。 func ListTickets(ctx *gin.Context) { account, ok := common.StaffAccount(ctx) if !ok { return } categories := []string{"installation", "repair"} if account.RoleCode == "operations" { categories = []string{"inspection", "reinspection"} } if account.RoleCode == "delivery" { infra.Response.Success(ctx, []models.CsTicket{}) return } var list []models.CsTicket if err := impl.DBService.Where("staff_account_id = ? AND category IN ? AND status <> ?", account.ID, categories, common.StatusArchived). Order("created_at desc").Find(&list).Error; err != nil { infra.Response.Error(ctx, err) return } infra.Response.Success(ctx, common.ResourceResponse(list)) } // GetTicket 按公开 identity 返回当前工作人员被分派的单一工单。 func GetTicket(ctx *gin.Context) { account, ok := common.StaffAccount(ctx) if !ok { return } var ticket models.CsTicket if impl.DBService.Where("identity = ? AND staff_account_id = ? AND status <> ?", ctx.Param("identity"), account.ID, common.StatusArchived). First(&ticket).Error != nil { infra.Response.Error(ctx, errcode.ErrRecordNotFound) return } infra.Response.Success(ctx, common.ResourceResponse(ticket)) } // StartTicket 将本人已分派工单置为处理中。 func StartTicket(ctx *gin.Context) { updateTicketStatus(ctx, 18, 11, nil) } // ExceptionTicket 标记本人处理中工单异常。 func ExceptionTicket(ctx *gin.Context) { updateTicketStatus(ctx, 11, 21, nil) } // RecoverTicket 恢复本人异常工单。 func RecoverTicket(ctx *gin.Context) { updateTicketStatus(ctx, 21, 11, nil) } // SubmitTicketResult 追加现场证据并提交用户确认;不合格或高风险结果必须进入异常。 func SubmitTicketResult(ctx *gin.Context) { account, ok := common.StaffAccount(ctx) if !ok { return } var request struct { Result string `json:"result" binding:"required,max=2000"` Conclusion string `json:"conclusion" binding:"required,oneof=qualified noncompliant high_risk"` Evidences []struct { EvidenceType string `json:"evidence_type" binding:"required"` MediaType string `json:"media_type" binding:"required,oneof=image video signature"` FileURI string `json:"file_uri" binding:"required"` CapturedAt time.Time `json:"captured_at" binding:"required"` Longitude string `json:"longitude" binding:"required"` Latitude string `json:"latitude" binding:"required"` RequestNo string `json:"request_no" binding:"required"` } `json:"evidences" binding:"required,min=2"` } if ctx.ShouldBindJSON(&request) != nil { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } var ticket models.CsTicket if impl.DBService.Where("identity = ? AND staff_account_id = ? AND ticket_status = ?", ctx.Param("identity"), account.ID, 11).First(&ticket).Error != nil { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } images, videos, signatures := 0, 0, 0 stages := map[string]bool{} for _, evidence := range request.Evidences { if !strings.HasPrefix(evidence.FileURI, "/uploads/") { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } switch evidence.MediaType { case "image": images++ case "video": videos++ case "signature": signatures++ } stages[evidence.EvidenceType] = true } valid := images <= 6 && videos <= 3 && signatures >= 1 if ticket.Category == "installation" || ticket.Category == "repair" { valid = valid && stages["before"] && stages["during"] && stages["after"] } else { valid = valid && images >= 1 } if !valid { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } nextStatus := 34 if request.Conclusion != "qualified" { nextStatus = 21 } err := impl.DBService.Transaction(func(tx *gorm.DB) error { now := time.Now() for _, item := range request.Evidences { record := models.CsTicketEvidence{ Entity: common.NewEntity(common.StatusEnable), CsTicketID: ticket.ID, EvidenceType: item.EvidenceType, MediaType: item.MediaType, FileURI: item.FileURI, CapturedAt: item.CapturedAt, ReceivedAt: now, Longitude: item.Longitude, Latitude: item.Latitude, Source: "app", IntegrityStatus: "unverified", OperatorIdentity: account.Identity, RequestNo: item.RequestNo, } if err := tx.Create(&record).Error; err != nil { return err } } result := tx.Model(&models.CsTicket{}).Where("id = ? AND ticket_status = ?", ticket.ID, 11). Updates(map[string]any{"ticket_status": nextStatus, "result": request.Result, "operator_identity": account.Identity}) if result.Error != nil { return result.Error } if result.RowsAffected != 1 { return gorm.ErrInvalidData } return nil }) if err != nil { infra.Response.Error(ctx, err) return } infra.Response.Success(ctx, gin.H{"ticket_status": nextStatus}) } func updateTicketStatus(ctx *gin.Context, from, to int, extra map[string]any) { account, ok := common.StaffAccount(ctx) if !ok { return } values := map[string]any{"ticket_status": to, "operator_identity": account.Identity} if extra != nil { for key, value := range extra { values[key] = value } } if to == 11 { now := time.Now() values["started_at"] = &now } result := impl.DBService.Model(&models.CsTicket{}). Where("identity = ? AND staff_account_id = ? AND ticket_status = ?", ctx.Param("identity"), account.ID, from). Updates(values) if result.Error != nil { infra.Response.Error(ctx, result.Error) return } if result.RowsAffected != 1 { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } infra.Response.Success(ctx, gin.H{"ticket_status": to}) }