修复配送订单创建方合同约束

This commit is contained in:
czl231
2026-08-14 01:15:33 +08:00
parent 9c3abf76d5
commit 08f1a917b3
9 changed files with 407 additions and 26 deletions

View File

@@ -34,6 +34,11 @@ var (
errBindingProductScrapped = errors.New("所选气瓶已报废,不能绑定到合同")
errBindingProductOwner = errors.New("所选气瓶不属于该合同用户,请选择合同用户所属的气瓶")
errBindingProductDuplicate = errors.New("所选气瓶已绑定到该合同,请勿重复绑定")
errOrderCreatorUser = errors.New("创建方必须是配送合同的签约用户")
errOrderCreatorGas = errors.New("创建方必须是配送合同的签约气站")
errOrderCreatorNoDelivery = errors.New("所选合同未指定配送点,不能以配送点作为创建方")
errOrderCreatorDelivery = errors.New("创建方必须是配送合同指定的配送点")
errOrderCreatorStaff = errors.New("所选工作人员不属于配送合同的履约组织或账号未启用")
)
// ListGasorderContract 按明确用途收窄合同候选;合同管理列表不带用途参数时保持全量查询。
@@ -588,11 +593,16 @@ func CreateGasorderBasic(ctx *gin.Context) {
if !contractEligibleForOrder(contract, now) {
return errors.New("contract is not active")
}
if request.CreatorType == "user" && creatorID != contract.UserAccountID {
return errors.New("user cannot create an order for another user")
var creatorStaff *models.StaffAccount
if request.CreatorType == "staff" {
var staff models.StaffAccount
if err := tx.Where("id = ? AND status = ?", creatorID, common.StatusEnable).First(&staff).Error; err != nil {
return errOrderCreatorStaff
}
creatorStaff = &staff
}
if request.CreatorType == "gas" && creatorID != contract.GasBasicID {
return errors.New("gas station does not own the contract")
if err := gasorderCreatorValidationError(contract, request.CreatorType, creatorID, creatorStaff); err != nil {
return err
}
var address models.UserAddress
if err := tx.Where("identity = ? AND user_account_id = ?", request.UserAddressIdentity, contract.UserAccountID).First(&address).Error; err != nil {
@@ -659,12 +669,60 @@ func CreateGasorderBasic(ctx *gin.Context) {
common.RespondCreatedResource(ctx, order)
return
}
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
if isGasorderCreatorBusinessError(err) {
infra.Response.Error(ctx, err)
} else {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
}
return
}
common.RespondCreatedResource(ctx, order)
}
// gasorderCreatorValidationError 校验平台代建订单中的业务创建方必须属于当前配送合同。
// 参数contract 为当前生效合同creatorType/creatorID 为提交的创建方staff 仅在工作人员类型时传入。
// 返回值:组合合法返回 nil否则返回可安全展示的中文业务错误。
func gasorderCreatorValidationError(contract models.GasorderContract, creatorType string, creatorID uint64, staff *models.StaffAccount) error {
switch creatorType {
case "user":
if creatorID != contract.UserAccountID {
return errOrderCreatorUser
}
case "gas":
if creatorID != contract.GasBasicID {
return errOrderCreatorGas
}
case "delivery":
if contract.DeliveryBasicID == 0 {
return errOrderCreatorNoDelivery
}
if creatorID != contract.DeliveryBasicID {
return errOrderCreatorDelivery
}
case "staff":
if staff == nil || staff.ID != creatorID || staff.Status != common.StatusEnable || staff.GasBasicID != contract.GasBasicID {
return errOrderCreatorStaff
}
if contract.DeliveryBasicID > 0 {
if staff.DeliveryBasicID != contract.DeliveryBasicID {
return errOrderCreatorStaff
}
} else if staff.DeliveryBasicID != 0 {
return errOrderCreatorStaff
}
}
return nil
}
// isGasorderCreatorBusinessError 限制仅透传预定义创建方错误,避免数据库细节泄露给前端。
func isGasorderCreatorBusinessError(err error) bool {
return errors.Is(err, errOrderCreatorUser) ||
errors.Is(err, errOrderCreatorGas) ||
errors.Is(err, errOrderCreatorNoDelivery) ||
errors.Is(err, errOrderCreatorDelivery) ||
errors.Is(err, errOrderCreatorStaff)
}
// contractEligibleForOrder 校验合同当前确实处于可履约时间窗口。
func contractEligibleForOrder(contract models.GasorderContract, now time.Time) bool {
return contract.ContractStatus == common.StatusActive &&

View File

@@ -110,6 +110,58 @@ func TestGasorderCreatorTypesCoverEveryConfirmedOrigin(t *testing.T) {
}
}
// TestGasorderCreatorValidationError 验证四类创建方均被严格限制在当前配送合同履约范围内。
func TestGasorderCreatorValidationError(t *testing.T) {
contract := models.GasorderContract{UserAccountID: 10, GasBasicID: 20, DeliveryBasicID: 30}
validStaff := &models.StaffAccount{
Entity: models.Entity{ID: 40, Status: common.StatusEnable},
GasBasicID: 20, DeliveryBasicID: 30,
}
tests := []struct {
name string
contract models.GasorderContract
creatorType string
creatorID uint64
staff *models.StaffAccount
want error
}{
{"合同用户", contract, "user", 10, nil, nil},
{"跨合同用户", contract, "user", 11, nil, errOrderCreatorUser},
{"合同气站", contract, "gas", 20, nil, nil},
{"跨合同气站", contract, "gas", 21, nil, errOrderCreatorGas},
{"合同配送点", contract, "delivery", 30, nil, nil},
{"跨合同配送点", contract, "delivery", 31, nil, errOrderCreatorDelivery},
{"合同未指定配送点", models.GasorderContract{UserAccountID: 10, GasBasicID: 20}, "delivery", 30, nil, errOrderCreatorNoDelivery},
{"合同配送点工作人员", contract, "staff", 40, validStaff, nil},
{"跨配送点工作人员", contract, "staff", 40, &models.StaffAccount{Entity: models.Entity{ID: 40, Status: common.StatusEnable}, GasBasicID: 20, DeliveryBasicID: 31}, errOrderCreatorStaff},
{"停用工作人员", contract, "staff", 40, &models.StaffAccount{Entity: models.Entity{ID: 40, Status: common.StatusDisable}, GasBasicID: 20, DeliveryBasicID: 30}, errOrderCreatorStaff},
{"气站直属工作人员", models.GasorderContract{UserAccountID: 10, GasBasicID: 20}, "staff", 40, &models.StaffAccount{Entity: models.Entity{ID: 40, Status: common.StatusEnable}, GasBasicID: 20}, nil},
{"无配送点合同拒绝下属配送点人员", models.GasorderContract{UserAccountID: 10, GasBasicID: 20}, "staff", 40, validStaff, errOrderCreatorStaff},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if got := gasorderCreatorValidationError(test.contract, test.creatorType, test.creatorID, test.staff); got != test.want {
t.Fatalf("创建方校验结果 = %v期望 %v", got, test.want)
}
})
}
}
// TestGasorderCreatorBusinessErrorsAreSafe 验证仅预定义中文创建方错误允许返回客户端。
func TestGasorderCreatorBusinessErrorsAreSafe(t *testing.T) {
for _, businessError := range []error{
errOrderCreatorUser, errOrderCreatorGas, errOrderCreatorNoDelivery,
errOrderCreatorDelivery, errOrderCreatorStaff,
} {
if !isGasorderCreatorBusinessError(businessError) {
t.Fatalf("创建方业务错误未被识别:%v", businessError)
}
}
if isGasorderCreatorBusinessError(gorm.ErrInvalidDB) {
t.Fatal("数据库错误不应透传给客户端")
}
}
func TestUnbindWritesIntegerGenericStatusAndReason(t *testing.T) {
updates := gasorderUnbindUpdates(" contract ended ", time.Now())
status, ok := updates["status"].(int)