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