2026-07-30 21:47:41 +08:00
|
|
|
|
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"
|
2026-07-31 09:54:17 +08:00
|
|
|
|
common "git.apinb.com/heqiapp/platforms/backend/api/internal/logic/common"
|
2026-07-30 21:47:41 +08:00
|
|
|
|
"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
|
2026-07-31 09:54:17 +08:00
|
|
|
|
if err := impl.DBService.Where("staff_account_id = ? AND status <> ?", account.ID, common.StatusArchived).
|
2026-07-30 21:47:41 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
2026-07-31 09:54:17 +08:00
|
|
|
|
infra.Response.Success(ctx, gin.H{"order": deliveryOrderResponse(order), "items": common.ResourceResponse(items)})
|
2026-07-30 21:47:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// StartDeliveryOrder 将已就绪订单置为配送中并创建本次轨迹。
|
|
|
|
|
|
func StartDeliveryOrder(ctx *gin.Context) {
|
2026-07-31 09:54:17 +08:00
|
|
|
|
transitionDeliveryOrder(ctx, common.StatusReady, common.StatusDelivering, true)
|
2026-07-30 21:47:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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)
|
2026-07-31 09:54:17 +08:00
|
|
|
|
if !ok || order.OrderStatus != common.StatusDelivering {
|
2026-07-30 21:47:41 +08:00
|
|
|
|
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{
|
2026-07-31 09:54:17 +08:00
|
|
|
|
Entity: common.NewEntity(common.StatusEnable), GasorderTrackID: track.ID, RequestNo: item.RequestNo,
|
2026-07-30 21:47:41 +08:00
|
|
|
|
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)
|
2026-07-31 09:54:17 +08:00
|
|
|
|
if order.OrderStatus != common.StatusDelivering || !valid || distance > config.Spec.Global.DeliveryArrivalRadiusMeters {
|
2026-07-30 21:47:41 +08:00
|
|
|
|
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{
|
2026-07-31 09:54:17 +08:00
|
|
|
|
Entity: common.NewEntity(common.StatusEnable), GasorderTrackID: track.ID, RequestNo: request.RequestNo,
|
2026-07-30 21:47:41 +08:00
|
|
|
|
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{}).
|
2026-07-31 09:54:17 +08:00
|
|
|
|
Where("id = ? AND staff_account_id = ? AND order_status = ?", order.ID, account.ID, common.StatusDelivering).
|
|
|
|
|
|
Update("order_status", common.StatusAwaitingConfirmation)
|
2026-07-30 21:47:41 +08:00
|
|
|
|
if result.Error != nil || result.RowsAffected != 1 {
|
|
|
|
|
|
return gorm.ErrInvalidData
|
|
|
|
|
|
}
|
2026-07-31 09:54:17 +08:00
|
|
|
|
return tx.Create(deliveryStatusRecord(order, account, common.StatusDelivering, common.StatusAwaitingConfirmation, "配送到达")).Error
|
2026-07-30 21:47:41 +08:00
|
|
|
|
})
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-07-31 09:54:17 +08:00
|
|
|
|
infra.Response.Success(ctx, gin.H{"order_status": common.StatusAwaitingConfirmation, "distance_meters": math.Round(distance)})
|
2026-07-30 21:47:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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)
|
2026-07-31 09:54:17 +08:00
|
|
|
|
if !ok || (order.OrderStatus != common.StatusDelivering && order.OrderStatus != common.StatusAwaitingConfirmation) {
|
2026-07-30 21:47:41 +08:00
|
|
|
|
if ok {
|
|
|
|
|
|
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
|
|
|
|
|
}
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-07-31 09:54:17 +08:00
|
|
|
|
updateDeliveryStatus(ctx, order, account, common.StatusException, request.Reason, gin.H{"previous_order_status": order.OrderStatus})
|
2026-07-30 21:47:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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)
|
2026-07-31 09:54:17 +08:00
|
|
|
|
if !ok || order.OrderStatus != common.StatusException ||
|
|
|
|
|
|
(order.PreviousOrderStatus != common.StatusDelivering && order.PreviousOrderStatus != common.StatusAwaitingConfirmation) {
|
2026-07-30 21:47:41 +08:00
|
|
|
|
if ok {
|
|
|
|
|
|
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
|
|
|
|
|
}
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
target := order.PreviousOrderStatus
|
2026-07-31 09:54:17 +08:00
|
|
|
|
updateDeliveryStatus(ctx, order, account, target, request.Reason, gin.H{"previous_order_status": common.StatusDraft})
|
2026-07-30 21:47:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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 {
|
2026-07-31 09:54:17 +08:00
|
|
|
|
infra.Response.Success(ctx, gin.H{"confirmed": true, "identity": existing.Identity, "order_status": common.StatusCompleted})
|
2026-07-30 21:47:41 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
order, ok := requireDeliveryOrder(ctx, account, true)
|
2026-07-31 09:54:17 +08:00
|
|
|
|
if !ok || order.OrderStatus != common.StatusAwaitingConfirmation {
|
2026-07-30 21:47:41 +08:00
|
|
|
|
if ok {
|
|
|
|
|
|
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
|
|
|
|
|
}
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
confirm := models.GasorderConfirm{
|
2026-07-31 09:54:17 +08:00
|
|
|
|
Entity: common.NewEntity(common.StatusEnable), GasorderBasicID: order.ID, RequestNo: request.RequestNo,
|
2026-07-30 21:47:41 +08:00
|
|
|
|
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{}).
|
2026-07-31 09:54:17 +08:00
|
|
|
|
Where("id = ? AND staff_account_id = ? AND order_status = ?", order.ID, account.ID, common.StatusAwaitingConfirmation).
|
|
|
|
|
|
Update("order_status", common.StatusCompleted)
|
2026-07-30 21:47:41 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
2026-07-31 09:54:17 +08:00
|
|
|
|
return tx.Create(deliveryStatusRecord(order, account, common.StatusAwaitingConfirmation, common.StatusCompleted, "用户签收")).Error
|
2026-07-30 21:47:41 +08:00
|
|
|
|
})
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-07-31 09:54:17 +08:00
|
|
|
|
infra.Response.Success(ctx, gin.H{"confirmed": true, "identity": confirm.Identity, "order_status": common.StatusCompleted})
|
2026-07-30 21:47:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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) {
|
2026-07-31 09:54:17 +08:00
|
|
|
|
account, ok := common.StaffAccount(ctx)
|
2026-07-30 21:47:41 +08:00
|
|
|
|
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"})
|
|
|
|
|
|
}
|
2026-07-31 09:54:17 +08:00
|
|
|
|
if query.Where("identity = ? AND staff_account_id = ? AND status <> ?", ctx.Param("identity"), account.ID, common.StatusArchived).
|
2026-07-30 21:47:41 +08:00
|
|
|
|
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{
|
2026-07-31 09:54:17 +08:00
|
|
|
|
Entity: common.NewEntity(common.StatusEnable), GasorderBasicID: order.ID,
|
2026-07-30 21:47:41 +08:00
|
|
|
|
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{
|
2026-07-31 09:54:17 +08:00
|
|
|
|
Entity: common.NewEntity(common.StatusEnable), GasorderBasicID: order.ID,
|
2026-07-30 21:47:41 +08:00
|
|
|
|
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 {
|
2026-07-31 09:54:17 +08:00
|
|
|
|
case common.StatusReady:
|
2026-07-30 21:47:41 +08:00
|
|
|
|
return []string{"start"}
|
2026-07-31 09:54:17 +08:00
|
|
|
|
case common.StatusDelivering:
|
2026-07-30 21:47:41 +08:00
|
|
|
|
return []string{"append_tracks", "arrive", "exception"}
|
2026-07-31 09:54:17 +08:00
|
|
|
|
case common.StatusAwaitingConfirmation:
|
2026-07-30 21:47:41 +08:00
|
|
|
|
return []string{"submit_receipt", "exception"}
|
2026-07-31 09:54:17 +08:00
|
|
|
|
case common.StatusException:
|
2026-07-30 21:47:41 +08:00
|
|
|
|
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)
|
|
|
|
|
|
}
|