package staff import ( "math" "strconv" "strings" "time" "git.apinb.com/bsm-sdk/core/errcode" "git.apinb.com/bsm-sdk/core/infra" "git.apinb.com/heqiapp/platforms/backend/api/internal/config" "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" ) // ListDeliveryOrders 返回仅分派给当前配送人员的订单。 func ListDeliveryOrders(ctx *gin.Context) { account, ok := requireDeliveryAccount(ctx) if !ok { return } var orders []models.GasorderBasic if err := impl.DBService.Where("staff_account_id = ? AND status <> ?", account.ID, common.StatusArchived). Order("created_at desc").Find(&orders).Error; err != nil { infra.Response.Error(ctx, err) return } infra.Response.Success(ctx, deliveryOrderResponses(orders)) } // GetDeliveryOrder 返回当前配送人员订单详情及气瓶项目。 func GetDeliveryOrder(ctx *gin.Context) { account, ok := requireDeliveryAccount(ctx) if !ok { return } order, ok := requireDeliveryOrder(ctx, account, false) if !ok { return } var items []models.GasorderItem if err := impl.DBService.Where("gasorder_basic_id = ?", order.ID).Order("created_at asc").Find(&items).Error; err != nil { infra.Response.Error(ctx, err) return } infra.Response.Success(ctx, gin.H{"order": deliveryOrderResponse(order), "items": common.ResourceResponse(items)}) } // StartDeliveryOrder 将已就绪订单置为配送中并创建本次轨迹。 func StartDeliveryOrder(ctx *gin.Context) { transitionDeliveryOrder(ctx, common.StatusReady, common.StatusDelivering, true) } // AppendDeliveryTracks 批量补传配送中轨迹点;request_no 保证重复补传不重复落库。 func AppendDeliveryTracks(ctx *gin.Context) { account, ok := requireDeliveryAccount(ctx) if !ok { return } var request struct { Points []deliveryTrackPointRequest `json:"points" binding:"required,min=1,max=100"` } if ctx.ShouldBindJSON(&request) != nil { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } order, ok := requireDeliveryOrder(ctx, account, true) if !ok || order.OrderStatus != common.StatusDelivering { if ok { infra.Response.Error(ctx, errcode.ErrInvalidArgument) } return } var track models.GasorderTrack if impl.DBService.Where("gasorder_basic_id = ? AND staff_account_id = ? AND completed_at IS NULL", order.ID, account.ID). First(&track).Error != nil { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } receivedAt := time.Now() points := make([]models.GasorderTrackPoint, 0, len(request.Points)) for _, item := range request.Points { if !validTrackPoint(item) { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } points = append(points, models.GasorderTrackPoint{ Entity: common.NewEntity(common.StatusEnable), GasorderTrackID: track.ID, RequestNo: item.RequestNo, Longitude: item.Longitude, Latitude: item.Latitude, OccurredAt: item.OccurredAt, ReceivedAt: receivedAt, Source: item.Source, Accuracy: item.Accuracy, Speed: item.Speed, Direction: item.Direction, }) } if err := impl.DBService.Clauses(clause.OnConflict{DoNothing: true}).Create(&points).Error; err != nil { infra.Response.Error(ctx, err) return } infra.Response.Success(ctx, gin.H{"accepted": len(points)}) } // ArriveDeliveryOrder 校验地理围栏并把配送单推进到待签收。 func ArriveDeliveryOrder(ctx *gin.Context) { account, ok := requireDeliveryAccount(ctx) if !ok { return } var request deliveryTrackPointRequest if ctx.ShouldBindJSON(&request) != nil || !validTrackPoint(request) { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } order, ok := requireDeliveryOrder(ctx, account, true) if !ok { return } distance, valid := coordinateDistanceMeters(order.Longitude, order.Latitude, request.Longitude, request.Latitude) if order.OrderStatus != common.StatusDelivering || !valid || distance > config.Spec.Global.DeliveryArrivalRadiusMeters { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } err := impl.DBService.Transaction(func(tx *gorm.DB) error { var track models.GasorderTrack if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). Where("gasorder_basic_id = ? AND staff_account_id = ? AND completed_at IS NULL", order.ID, account.ID). First(&track).Error; err != nil { return err } now := time.Now() point := models.GasorderTrackPoint{ Entity: common.NewEntity(common.StatusEnable), GasorderTrackID: track.ID, RequestNo: request.RequestNo, Longitude: request.Longitude, Latitude: request.Latitude, OccurredAt: request.OccurredAt, ReceivedAt: now, Source: request.Source, Accuracy: request.Accuracy, Speed: request.Speed, Direction: request.Direction, } if err := tx.Create(&point).Error; err != nil { return err } if err := tx.Model(&track).Update("completed_at", &now).Error; err != nil { return err } result := tx.Model(&models.GasorderBasic{}). Where("id = ? AND staff_account_id = ? AND order_status = ?", order.ID, account.ID, common.StatusDelivering). Update("order_status", common.StatusAwaitingConfirmation) if result.Error != nil || result.RowsAffected != 1 { return gorm.ErrInvalidData } return tx.Create(deliveryStatusRecord(order, account, common.StatusDelivering, common.StatusAwaitingConfirmation, "配送到达")).Error }) if err != nil { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } infra.Response.Success(ctx, gin.H{"order_status": common.StatusAwaitingConfirmation, "distance_meters": math.Round(distance)}) } // ExceptionDeliveryOrder 将配送中或待签收订单置为异常。 func ExceptionDeliveryOrder(ctx *gin.Context) { account, ok := requireDeliveryAccount(ctx) if !ok { return } var request struct { Reason string `json:"reason" binding:"required,max=1000"` } if ctx.ShouldBindJSON(&request) != nil { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } order, ok := requireDeliveryOrder(ctx, account, true) if !ok || (order.OrderStatus != common.StatusDelivering && order.OrderStatus != common.StatusAwaitingConfirmation) { if ok { infra.Response.Error(ctx, errcode.ErrInvalidArgument) } return } updateDeliveryStatus(ctx, order, account, common.StatusException, request.Reason, gin.H{"previous_order_status": order.OrderStatus}) } // RecoverDeliveryOrder 将本人异常订单恢复到异常前状态。 func RecoverDeliveryOrder(ctx *gin.Context) { account, ok := requireDeliveryAccount(ctx) if !ok { return } var request struct { Reason string `json:"reason" binding:"required,max=1000"` } if ctx.ShouldBindJSON(&request) != nil { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } order, ok := requireDeliveryOrder(ctx, account, true) if !ok || order.OrderStatus != common.StatusException || (order.PreviousOrderStatus != common.StatusDelivering && order.PreviousOrderStatus != common.StatusAwaitingConfirmation) { if ok { infra.Response.Error(ctx, errcode.ErrInvalidArgument) } return } target := order.PreviousOrderStatus updateDeliveryStatus(ctx, order, account, target, request.Reason, gin.H{"previous_order_status": common.StatusDraft}) } // SubmitDeliveryReceipt 保存签收凭证并完成订单,重复 request_no 返回既有结果。 func SubmitDeliveryReceipt(ctx *gin.Context) { account, ok := requireDeliveryAccount(ctx) if !ok { return } var request struct { RequestNo string `json:"request_no" binding:"required"` ConfirmType string `json:"confirm_type" binding:"required,oneof=signature receipt_code"` RecipientName string `json:"recipient_name" binding:"required,max=64"` RecipientPhone string `json:"recipient_phone" binding:"max=32"` ProofURI string `json:"proof_uri" binding:"required,max=512"` Remark string `json:"remark" binding:"max=1000"` } if ctx.ShouldBindJSON(&request) != nil || !strings.HasPrefix(request.ProofURI, "/uploads/") { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } var existing models.GasorderConfirm if impl.DBService.Where("request_no = ?", request.RequestNo).First(&existing).Error == nil { infra.Response.Success(ctx, gin.H{"confirmed": true, "identity": existing.Identity, "order_status": common.StatusCompleted}) return } order, ok := requireDeliveryOrder(ctx, account, true) if !ok || order.OrderStatus != common.StatusAwaitingConfirmation { if ok { infra.Response.Error(ctx, errcode.ErrInvalidArgument) } return } confirm := models.GasorderConfirm{ Entity: common.NewEntity(common.StatusEnable), GasorderBasicID: order.ID, RequestNo: request.RequestNo, ConfirmType: request.ConfirmType, RecipientName: request.RecipientName, RecipientPhone: request.RecipientPhone, ProofURI: request.ProofURI, ConfirmedAt: time.Now(), Remark: request.Remark, } err := impl.DBService.Transaction(func(tx *gorm.DB) error { if err := tx.Create(&confirm).Error; err != nil { return err } result := tx.Model(&models.GasorderBasic{}). Where("id = ? AND staff_account_id = ? AND order_status = ?", order.ID, account.ID, common.StatusAwaitingConfirmation). Update("order_status", common.StatusCompleted) if result.Error != nil || result.RowsAffected != 1 { return gorm.ErrInvalidData } if err := tx.Model(&models.GasorderItem{}).Where("gasorder_basic_id = ?", order.ID).Update("active", false).Error; err != nil { return err } return tx.Create(deliveryStatusRecord(order, account, common.StatusAwaitingConfirmation, common.StatusCompleted, "用户签收")).Error }) if err != nil { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } infra.Response.Success(ctx, gin.H{"confirmed": true, "identity": confirm.Identity, "order_status": common.StatusCompleted}) } type deliveryTrackPointRequest struct { RequestNo string `json:"request_no" binding:"required"` Longitude string `json:"longitude" binding:"required"` Latitude string `json:"latitude" binding:"required"` OccurredAt time.Time `json:"occurred_at" binding:"required"` Source string `json:"source" binding:"required,oneof=gps network manual"` Accuracy string `json:"accuracy"` Speed string `json:"speed"` Direction string `json:"direction"` } func requireDeliveryAccount(ctx *gin.Context) (models.StaffAccount, bool) { account, ok := common.StaffAccount(ctx) if !ok { return account, false } if account.RoleCode != "delivery" { infra.Response.Error(ctx, errcode.ErrPermissionDenied) return account, false } return account, true } func requireDeliveryOrder(ctx *gin.Context, account models.StaffAccount, lock bool) (models.GasorderBasic, bool) { var order models.GasorderBasic query := impl.DBService if lock { query = query.Clauses(clause.Locking{Strength: "UPDATE"}) } if query.Where("identity = ? AND staff_account_id = ? AND status <> ?", ctx.Param("identity"), account.ID, common.StatusArchived). First(&order).Error != nil { infra.Response.Error(ctx, errcode.ErrRecordNotFound) return order, false } return order, true } func transitionDeliveryOrder(ctx *gin.Context, from, to int, createTrack bool) { account, ok := requireDeliveryAccount(ctx) if !ok { return } var request struct { Reason string `json:"reason" binding:"required,max=1000"` } if ctx.ShouldBindJSON(&request) != nil { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } order, ok := requireDeliveryOrder(ctx, account, true) if !ok || order.OrderStatus != from { if ok { infra.Response.Error(ctx, errcode.ErrInvalidArgument) } return } err := impl.DBService.Transaction(func(tx *gorm.DB) error { result := tx.Model(&models.GasorderBasic{}). Where("id = ? AND staff_account_id = ? AND order_status = ?", order.ID, account.ID, from). Update("order_status", to) if result.Error != nil || result.RowsAffected != 1 { return gorm.ErrInvalidData } if createTrack { var attempt int if err := tx.Model(&models.GasorderTrack{}).Where("gasorder_basic_id = ?", order.ID). Select("COALESCE(MAX(attempt_no), 0)").Scan(&attempt).Error; err != nil { return err } if err := tx.Create(&models.GasorderTrack{ Entity: common.NewEntity(common.StatusEnable), GasorderBasicID: order.ID, StaffAccountID: account.ID, AttemptNo: attempt + 1, StartedAt: time.Now(), }).Error; err != nil { return err } } return tx.Create(deliveryStatusRecord(order, account, from, to, request.Reason)).Error }) if err != nil { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } infra.Response.Success(ctx, gin.H{"order_status": to}) } func updateDeliveryStatus(ctx *gin.Context, order models.GasorderBasic, account models.StaffAccount, target int, reason string, extra gin.H) { updates := gin.H{"order_status": target} for key, value := range extra { updates[key] = value } err := impl.DBService.Transaction(func(tx *gorm.DB) error { result := tx.Model(&models.GasorderBasic{}). Where("id = ? AND staff_account_id = ? AND order_status = ?", order.ID, account.ID, order.OrderStatus). Updates(updates) if result.Error != nil || result.RowsAffected != 1 { return gorm.ErrInvalidData } return tx.Create(deliveryStatusRecord(order, account, order.OrderStatus, target, reason)).Error }) if err != nil { infra.Response.Error(ctx, errcode.ErrInvalidArgument) return } infra.Response.Success(ctx, gin.H{"order_status": target}) } func deliveryStatusRecord(order models.GasorderBasic, account models.StaffAccount, from, to int, reason string) models.GasorderStatus { return models.GasorderStatus{ Entity: common.NewEntity(common.StatusEnable), GasorderBasicID: order.ID, FromStatus: from, ToStatus: to, OperatorIdentity: account.Identity, OperatorName: account.Name, OccurredAt: time.Now(), Reason: strings.TrimSpace(reason), } } func deliveryOrderResponses(orders []models.GasorderBasic) []gin.H { responses := make([]gin.H, 0, len(orders)) for _, order := range orders { responses = append(responses, deliveryOrderResponse(order)) } return responses } func deliveryOrderResponse(order models.GasorderBasic) gin.H { return gin.H{ "identity": order.Identity, "order_no": order.OrderNo, "order_status": order.OrderStatus, "address": order.Address, "longitude": order.Longitude, "latitude": order.Latitude, "contact_name": order.ContactName, "contact_phone": order.ContactPhone, "payable_amount": order.PayableAmount, "remark": order.Remark, "created_at": order.CreatedAt, "updated_at": order.UpdatedAt, "allowed_actions": deliveryAllowedActions(order.OrderStatus), } } func deliveryAllowedActions(status int) []string { switch status { case common.StatusReady: return []string{"start"} case common.StatusDelivering: return []string{"append_tracks", "arrive", "exception"} case common.StatusAwaitingConfirmation: return []string{"submit_receipt", "exception"} case common.StatusException: return []string{"recover"} default: return []string{} } } func validTrackPoint(point deliveryTrackPointRequest) bool { _, longitudeOK := parseCoordinate(point.Longitude, -180, 180) _, latitudeOK := parseCoordinate(point.Latitude, -90, 90) return point.RequestNo != "" && longitudeOK && latitudeOK && !point.OccurredAt.IsZero() } func coordinateDistanceMeters(longitudeA, latitudeA, longitudeB, latitudeB string) (float64, bool) { lonA, okA := parseCoordinate(longitudeA, -180, 180) latA, okB := parseCoordinate(latitudeA, -90, 90) lonB, okC := parseCoordinate(longitudeB, -180, 180) latB, okD := parseCoordinate(latitudeB, -90, 90) if !(okA && okB && okC && okD) { return 0, false } const earthRadiusMeters = 6371000 latitudeDelta := (latB - latA) * math.Pi / 180 longitudeDelta := (lonB - lonA) * math.Pi / 180 a := math.Sin(latitudeDelta/2)*math.Sin(latitudeDelta/2) + math.Cos(latA*math.Pi/180)*math.Cos(latB*math.Pi/180)* math.Sin(longitudeDelta/2)*math.Sin(longitudeDelta/2) return earthRadiusMeters * 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a)), true } func parseCoordinate(value string, minimum, maximum float64) (float64, bool) { parsed, err := strconv.ParseFloat(strings.TrimSpace(value), 64) return parsed, err == nil && parsed >= minimum && parsed <= maximum && !math.IsNaN(parsed) && !math.IsInf(parsed, 0) }