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

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,6 +1,9 @@
// 功能描述:提供平台配送点资源管理与受控候选筛选。
// 版本v1.1.0
package delivery
import (
"strconv"
"strings"
"git.apinb.com/bsm-sdk/core/errcode"
@@ -11,6 +14,19 @@ import (
"github.com/gin-gonic/gin"
)
// parseDeliveryStatusFilter 解析可选的配送点通用状态筛选,拒绝归档和未知状态。
func parseDeliveryStatusFilter(value string) (*int, bool) {
value = strings.TrimSpace(value)
if value == "" {
return nil, true
}
status, err := strconv.Atoi(value)
if err != nil || !common.IsGenericRecordStatus(status) || status == common.StatusArchived {
return nil, false
}
return &status, true
}
// ListDeliveryBasic 查询配送点分页列表。
func ListDeliveryBasic(ctx *gin.Context) {
page, size := common.PageSize(ctx)
@@ -22,6 +38,11 @@ func ListDeliveryBasic(ctx *gin.Context) {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
status, ok := parseDeliveryStatusFilter(ctx.Query("status"))
if !ok {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
var list []models.DeliveryBasic
var total int64
query := common.ApplyKeywordFilter(ctx, common.ActiveRecords(impl.DBService.Model(&models.DeliveryBasic{})), &models.DeliveryBasic{})
@@ -32,6 +53,9 @@ func ListDeliveryBasic(ctx *gin.Context) {
common.StatusArchived,
)
}
if status != nil {
query = query.Where("delivery_basic.status = ?", *status)
}
if err := query.Count(&total).Error; err != nil {
infra.Response.Error(ctx, err)
return

View File

@@ -1,3 +1,5 @@
// 功能描述:验证平台配送点展示与候选筛选规则。
// 版本v1.1.0
package delivery
import (
@@ -37,6 +39,21 @@ func TestRestoreDeliveryBasicAddress(t *testing.T) {
}
}
// TestParseDeliveryStatusFilter 验证分配候选只接受明确的非归档通用状态。
func TestParseDeliveryStatusFilter(t *testing.T) {
if status, ok := parseDeliveryStatusFilter("1"); !ok || status == nil || *status != 1 {
t.Fatalf("启用状态筛选解析失败status=%v ok=%v", status, ok)
}
if status, ok := parseDeliveryStatusFilter(""); !ok || status != nil {
t.Fatalf("空状态筛选应保持兼容status=%v ok=%v", status, ok)
}
for _, value := range []string{"3", "enabled", "99"} {
if _, ok := parseDeliveryStatusFilter(value); ok {
t.Fatalf("非法配送点状态筛选被接受:%q", value)
}
}
}
// TestNewDeliveryAccountUsesAdminRole 验证平台创建配送点账号时固定后台唯一支持的角色编码。
func TestNewDeliveryAccountUsesAdminRole(t *testing.T) {
account := newDeliveryAccount(9, accountRequest{Username: "delivery-admin", DisplayName: "配送点管理员"}, "password-hash")

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("数据库错误不应透传给客户端")
}
}

View File

@@ -1,8 +1,11 @@
// 功能描述:提供平台工作人员资源管理与受控候选筛选。
// 版本v1.1.0
package staff
import (
"strconv"
"strings"
"time"
"git.apinb.com/bsm-sdk/core/errcode"
"git.apinb.com/bsm-sdk/core/infra"
@@ -14,9 +17,10 @@ import (
)
type staffListFilters struct {
RoleCodes []string
Status *int
WorkStatus string
RoleCodes []string
Status *int
WorkStatus string
ValidCredentialOnly bool
}
type staffOrganizationFilters struct {
@@ -27,7 +31,7 @@ type staffOrganizationFilters struct {
}
// parseStaffListFilters 将工作人员列表查询参数收敛为闭集条件,拒绝含糊或冲突值。
func parseStaffListFilters(roleCode, roleCodes, status, workStatus string) (staffListFilters, bool) {
func parseStaffListFilters(roleCode, roleCodes, status, workStatus, validCredentialOnly string) (staffListFilters, bool) {
filters := staffListFilters{}
roleCode = strings.TrimSpace(roleCode)
roleCodes = strings.TrimSpace(roleCodes)
@@ -65,6 +69,11 @@ func parseStaffListFilters(roleCode, roleCodes, status, workStatus string) (staf
return staffListFilters{}, false
}
filters.WorkStatus = workStatus
validCredentialOnly = strings.TrimSpace(validCredentialOnly)
if validCredentialOnly != "" && validCredentialOnly != "1" {
return staffListFilters{}, false
}
filters.ValidCredentialOnly = validCredentialOnly == "1"
return filters, true
}
@@ -103,6 +112,7 @@ func ListStaff(ctx *gin.Context) {
ctx.Query("role_codes"),
ctx.Query("status"),
ctx.Query("work_status"),
ctx.Query("valid_credential_only"),
)
if !ok {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
@@ -131,6 +141,14 @@ func ListStaff(ctx *gin.Context) {
if filters.WorkStatus != "" {
query = query.Where("work_status = ?", filters.WorkStatus)
}
if filters.ValidCredentialOnly {
now := time.Now()
query = query.Where(
"EXISTS (SELECT 1 FROM staff_credential WHERE staff_credential.staff_account_id = staff_account.id AND staff_credential.status = ? AND (staff_credential.expired_at IS NULL OR staff_credential.expired_at > ?))",
common.StatusEnable,
now,
)
}
if organizationFilters.GasIdentity != "" {
query = query.Where(
"gas_basic_id IN (SELECT id FROM gas_basic WHERE identity = ? AND status <> ?)",

View File

@@ -1,3 +1,5 @@
// 功能描述:验证工作人员角色、状态和组织候选筛选规则。
// 版本v1.1.0
package staff
import "testing"
@@ -25,30 +27,34 @@ func TestStaffRoleIsClosedEnumeration(t *testing.T) {
}
func TestStaffListFiltersAcceptExplicitBusinessScopes(t *testing.T) {
filters, ok := parseStaffListFilters("delivery", "", "1", "on_duty")
filters, ok := parseStaffListFilters("delivery", "", "1", "on_duty", "1")
if !ok || len(filters.RoleCodes) != 1 || filters.RoleCodes[0] != "delivery" {
t.Fatal("delivery assignment filters were rejected")
}
if filters.Status == nil || *filters.Status != 1 || filters.WorkStatus != "on_duty" {
t.Fatal("staff status filters were not preserved")
}
filters, ok = parseStaffListFilters("", "installer,delivery,operations", "1", "")
if !filters.ValidCredentialOnly {
t.Fatal("有效资质过滤条件未保留")
}
filters, ok = parseStaffListFilters("", "installer,delivery,operations", "1", "", "")
if !ok || len(filters.RoleCodes) != 3 {
t.Fatal("multi-role service-relation filters were rejected")
}
}
func TestStaffListFiltersRejectAmbiguousOrUnknownValues(t *testing.T) {
tests := [][4]string{
{"delivery", "installer,delivery", "", ""},
{"", "delivery,delivery", "", ""},
{"admin", "", "", ""},
{"", "", "3", ""},
{"", "", "enabled", ""},
{"", "", "", "available"},
tests := [][5]string{
{"delivery", "installer,delivery", "", "", ""},
{"", "delivery,delivery", "", "", ""},
{"admin", "", "", "", ""},
{"", "", "3", "", ""},
{"", "", "enabled", "", ""},
{"", "", "", "available", ""},
{"", "", "", "", "true"},
}
for _, test := range tests {
if _, ok := parseStaffListFilters(test[0], test[1], test[2], test[3]); ok {
if _, ok := parseStaffListFilters(test[0], test[1], test[2], test[3], test[4]); ok {
t.Fatalf("invalid staff filters were accepted: %#v", test)
}
}