强化配送订单合同调度约束

This commit is contained in:
czl231
2026-08-15 19:19:15 +08:00
parent 963e497bf3
commit 42a4fc2f41
15 changed files with 501 additions and 52 deletions

View File

@@ -1,3 +1,5 @@
// 功能描述:实现平台配送合同、配送订单及履约状态动作。
// 版本v1.2.0
package gasorder
import (
@@ -39,6 +41,12 @@ var (
errOrderCreatorNoDelivery = errors.New("所选合同未指定配送点,不能以配送点作为创建方")
errOrderCreatorDelivery = errors.New("创建方必须是配送合同指定的配送点")
errOrderCreatorStaff = errors.New("所选工作人员不属于配送合同的履约组织或账号未启用")
errAssignmentContractPoint = errors.New("配送合同已指定履约配送点,不能分配到其他配送点")
errAssignmentCurrentPoint = errors.New("已分配订单只能在当前配送点内改派配送人员")
errAssignmentDelivery = errors.New("所选配送点未启用或不属于订单履约气站")
errAssignmentStaffScope = errors.New("所选配送人员不属于订单配送点或履约气站")
errAssignmentStaffStatus = errors.New("所选工作人员不是启用且在岗的配送人员")
errAssignmentCredential = errors.New("所选配送人员缺少启用且未过期的有效资质")
)
// ListGasorderContract 按明确用途收窄合同候选;合同管理列表不带用途参数时保持全量查询。
@@ -758,18 +766,6 @@ func AssignGasorderBasic(ctx *gin.Context) {
}
operatorIdentity, operatorName := common.PlatformOperator(ctx)
err := impl.DBService.Transaction(func(tx *gorm.DB) error {
var delivery models.DeliveryBasic
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Where("identity = ? AND status = ?", request.DeliveryIdentity, common.StatusEnable).
First(&delivery).Error; err != nil {
return err
}
var staff models.StaffAccount
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Where("identity = ? AND status = ? AND work_status = ?", request.StaffIdentity, common.StatusEnable, "on_duty").
First(&staff).Error; err != nil {
return err
}
var order models.GasorderBasic
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("identity = ?", ctx.Param("identity")).First(&order).Error; err != nil {
return err
@@ -777,8 +773,33 @@ func AssignGasorderBasic(ctx *gin.Context) {
if order.OrderStatus != common.StatusCreated && order.OrderStatus != common.StatusAssigned {
return errors.New("order cannot be assigned")
}
if !validGasorderAssignment(order, delivery, staff) {
return errors.New("delivery or staff does not belong to the order organization")
var contract models.GasorderContract
if err := tx.Where("id = ?", order.GasorderContractID).First(&contract).Error; err != nil {
return err
}
var delivery models.DeliveryBasic
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Where("identity = ?", request.DeliveryIdentity).
First(&delivery).Error; err != nil {
return err
}
var staff models.StaffAccount
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Where("identity = ?", request.StaffIdentity).
First(&staff).Error; err != nil {
return err
}
if businessErr := gasorderAssignmentValidationError(order, contract, delivery, staff, true); businessErr != nil {
return businessErr
}
var credentialCount int64
if err := tx.Model(&models.StaffCredential{}).
Where("staff_account_id = ? AND status = ? AND (expired_at IS NULL OR expired_at > ?)", staff.ID, common.StatusEnable, time.Now()).
Count(&credentialCount).Error; err != nil {
return err
}
if credentialCount == 0 {
return errAssignmentCredential
}
previous := order.OrderStatus
if err := tx.Model(&order).Updates(map[string]any{
@@ -801,7 +822,11 @@ func AssignGasorderBasic(ctx *gin.Context) {
return nil
})
if err != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
if isGasorderAssignmentBusinessError(err) {
infra.Response.Error(ctx, err)
} else {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
}
return
}
infra.Response.Success(ctx, gin.H{"updated": true, "order_status": common.StatusAssigned})
@@ -994,14 +1019,37 @@ func releaseGasorderProducts(tx *gorm.DB, orderID uint64) error {
return tx.Model(&models.GasorderItem{}).Where("gasorder_basic_id = ?", orderID).Update("active", false).Error
}
func validGasorderAssignment(order models.GasorderBasic, delivery models.DeliveryBasic, staff models.StaffAccount) bool {
if delivery.GasBasicID != 0 && delivery.GasBasicID != order.GasBasicID {
return false
// gasorderAssignmentValidationError 校验合同固定点、改派边界及配送人员准入条件。
func gasorderAssignmentValidationError(order models.GasorderBasic, contract models.GasorderContract, delivery models.DeliveryBasic, staff models.StaffAccount, hasValidCredential bool) error {
if contract.DeliveryBasicID != 0 && delivery.ID != contract.DeliveryBasicID {
return errAssignmentContractPoint
}
if staff.DeliveryBasicID != delivery.ID {
return false
if contract.DeliveryBasicID == 0 && order.OrderStatus == common.StatusAssigned && order.DeliveryBasicID != 0 && delivery.ID != order.DeliveryBasicID {
return errAssignmentCurrentPoint
}
return staff.GasBasicID == 0 || staff.GasBasicID == order.GasBasicID
if delivery.Status != common.StatusEnable || delivery.GasBasicID != order.GasBasicID {
return errAssignmentDelivery
}
if staff.RoleCode != "delivery" || staff.Status != common.StatusEnable || staff.WorkStatus != "on_duty" {
return errAssignmentStaffStatus
}
if staff.DeliveryBasicID != delivery.ID || staff.GasBasicID != order.GasBasicID {
return errAssignmentStaffScope
}
if !hasValidCredential {
return errAssignmentCredential
}
return nil
}
// isGasorderAssignmentBusinessError 仅允许预定义分配错误返回前端,避免泄露数据库细节。
func isGasorderAssignmentBusinessError(err error) bool {
return errors.Is(err, errAssignmentContractPoint) ||
errors.Is(err, errAssignmentCurrentPoint) ||
errors.Is(err, errAssignmentDelivery) ||
errors.Is(err, errAssignmentStaffScope) ||
errors.Is(err, errAssignmentStaffStatus) ||
errors.Is(err, errAssignmentCredential)
}
func deliveryBelongsToGas(deliveryID, gasID uint64) bool {

View File

@@ -1,6 +1,9 @@
// 功能描述:验证平台配送合同、订单创建和调度状态规则。
// 版本v1.2.0
package gasorder
import (
"errors"
"reflect"
"strings"
"testing"
@@ -198,19 +201,67 @@ func TestGasorderCompleteStateMachine(t *testing.T) {
func TestGasorderAssignmentRequiresSameOrganization(t *testing.T) {
order := models.GasorderBasic{Entity: models.Entity{ID: 1}, GasBasicID: 10}
delivery := models.DeliveryBasic{Entity: models.Entity{ID: 20}, GasBasicID: 10}
staff := models.StaffAccount{GasBasicID: 10, DeliveryBasicID: 20}
if !validGasorderAssignment(order, delivery, staff) {
t.Fatal("valid organization assignment was rejected")
contract := models.GasorderContract{GasBasicID: 10}
delivery := models.DeliveryBasic{Entity: models.Entity{ID: 20, Status: common.StatusEnable}, GasBasicID: 10}
staff := models.StaffAccount{Entity: models.Entity{Status: common.StatusEnable}, RoleCode: "delivery", WorkStatus: "on_duty", GasBasicID: 10, DeliveryBasicID: 20}
if err := gasorderAssignmentValidationError(order, contract, delivery, staff, true); err != nil {
t.Fatalf("合法组织分配被拒绝:%v", err)
}
staff.DeliveryBasicID = 21
if validGasorderAssignment(order, delivery, staff) {
t.Fatal("cross-delivery staff assignment was accepted")
if err := gasorderAssignmentValidationError(order, contract, delivery, staff, true); !errors.Is(err, errAssignmentStaffScope) {
t.Fatalf("跨配送点人员未被拒绝:%v", err)
}
staff.DeliveryBasicID = 20
delivery.GasBasicID = 11
if validGasorderAssignment(order, delivery, staff) {
t.Fatal("cross-gas delivery assignment was accepted")
if err := gasorderAssignmentValidationError(order, contract, delivery, staff, true); !errors.Is(err, errAssignmentDelivery) {
t.Fatalf("跨气站配送点未被拒绝:%v", err)
}
}
// TestGasorderAssignmentRespectsContractAndQualification 覆盖合同固定点、改派、岗位状态和有效资质规则。
func TestGasorderAssignmentRespectsContractAndQualification(t *testing.T) {
baseOrder := models.GasorderBasic{Entity: models.Entity{ID: 1}, OrderStatus: common.StatusCreated, GasBasicID: 10}
baseContract := models.GasorderContract{GasBasicID: 10, DeliveryBasicID: 20}
baseDelivery := models.DeliveryBasic{Entity: models.Entity{ID: 20, Status: common.StatusEnable}, GasBasicID: 10}
baseStaff := models.StaffAccount{Entity: models.Entity{Status: common.StatusEnable}, RoleCode: "delivery", WorkStatus: "on_duty", GasBasicID: 10, DeliveryBasicID: 20}
tests := []struct {
name string
order models.GasorderBasic
contract models.GasorderContract
delivery models.DeliveryBasic
staff models.StaffAccount
hasCredential bool
want error
}{
{"合同固定配送点", baseOrder, baseContract, baseDelivery, baseStaff, true, nil},
{"跨合同配送点", baseOrder, baseContract, models.DeliveryBasic{Entity: models.Entity{ID: 21, Status: common.StatusEnable}, GasBasicID: 10}, baseStaff, true, errAssignmentContractPoint},
{"无合同点首次选择", baseOrder, models.GasorderContract{GasBasicID: 10}, baseDelivery, baseStaff, true, nil},
{"已分配订单跨点改派", models.GasorderBasic{OrderStatus: common.StatusAssigned, GasBasicID: 10, DeliveryBasicID: 20}, models.GasorderContract{GasBasicID: 10}, models.DeliveryBasic{Entity: models.Entity{ID: 21, Status: common.StatusEnable}, GasBasicID: 10}, baseStaff, true, errAssignmentCurrentPoint},
{"配送人员离岗", baseOrder, baseContract, baseDelivery, models.StaffAccount{Entity: models.Entity{Status: common.StatusEnable}, RoleCode: "delivery", WorkStatus: "off_duty", GasBasicID: 10, DeliveryBasicID: 20}, true, errAssignmentStaffStatus},
{"配送人员无有效资质", baseOrder, baseContract, baseDelivery, baseStaff, false, errAssignmentCredential},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got := gasorderAssignmentValidationError(test.order, test.contract, test.delivery, test.staff, test.hasCredential)
if !errors.Is(got, test.want) {
t.Fatalf("分配校验结果 = %v期望 %v", got, test.want)
}
})
}
}
// TestGasorderAssignmentBusinessErrorsAreSafe 验证仅预定义中文分配错误允许返回客户端。
func TestGasorderAssignmentBusinessErrorsAreSafe(t *testing.T) {
for _, businessError := range []error{
errAssignmentContractPoint, errAssignmentCurrentPoint, errAssignmentDelivery,
errAssignmentStaffScope, errAssignmentStaffStatus, errAssignmentCredential,
} {
if !isGasorderAssignmentBusinessError(businessError) {
t.Fatalf("分配业务错误未被识别:%v", businessError)
}
}
if isGasorderAssignmentBusinessError(gorm.ErrInvalidDB) {
t.Fatal("数据库错误不应透传给客户端")
}
}