强化配送订单合同调度约束
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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("数据库错误不应透传给客户端")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user