完善合同气瓶绑定约束与候选过滤
- 明确合同状态、气瓶归属、停用、报废和重复绑定失败原因 - 根据当前合同过滤可绑定气瓶,排除无效及重复候选项 - 从合同列表进入绑定页时锁定预填合同,防止切换上下文 - 切换通用新建页合同后清空旧气瓶并重新加载候选项 - 增加前后端回归检查并同步中文操作日志
This commit is contained in:
@@ -27,6 +27,22 @@ func TestContractChangeBusinessErrors(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestContractBindingErrorMessages 验证绑定失败时给出可操作的中文原因。
|
||||
func TestContractBindingErrorMessages(t *testing.T) {
|
||||
want := map[error]string{
|
||||
errBindingContractStatus: "仅草稿合同可以绑定气瓶",
|
||||
errBindingProductDisabled: "所选气瓶已停用,不能绑定到合同",
|
||||
errBindingProductScrapped: "所选气瓶已报废,不能绑定到合同",
|
||||
errBindingProductOwner: "所选气瓶不属于该合同用户,请选择合同用户所属的气瓶",
|
||||
errBindingProductDuplicate: "所选气瓶已绑定到该合同,请勿重复绑定",
|
||||
}
|
||||
for businessErr, message := range want {
|
||||
if businessErr.Error() != message {
|
||||
t.Fatalf("绑定错误提示不清晰:得到 %q,期望 %q", businessErr.Error(), message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestContractActivationErrorMessages 验证关键启用失败原因保持清晰中文。
|
||||
func TestContractActivationErrorMessages(t *testing.T) {
|
||||
want := map[error]string{
|
||||
|
||||
@@ -29,6 +29,11 @@ var (
|
||||
errContractNotEffective = errors.New("合同尚未到生效时间,暂时不能启用")
|
||||
errContractExpired = errors.New("合同已到期,请调整到期时间或续签后再启用")
|
||||
errContractNoProduct = errors.New("合同尚未绑定有效气瓶,请先绑定气瓶后再启用")
|
||||
errBindingContractStatus = errors.New("仅草稿合同可以绑定气瓶")
|
||||
errBindingProductDisabled = errors.New("所选气瓶已停用,不能绑定到合同")
|
||||
errBindingProductScrapped = errors.New("所选气瓶已报废,不能绑定到合同")
|
||||
errBindingProductOwner = errors.New("所选气瓶不属于该合同用户,请选择合同用户所属的气瓶")
|
||||
errBindingProductDuplicate = errors.New("所选气瓶已绑定到该合同,请勿重复绑定")
|
||||
)
|
||||
|
||||
func ListGasorderContract(ctx *gin.Context) { common.ListResource(ctx, &models.GasorderContract{}) }
|
||||
@@ -424,14 +429,36 @@ func BindGasorderContractProduct(ctx *gin.Context) {
|
||||
common.RespondRecordError(ctx, err)
|
||||
return
|
||||
}
|
||||
if contract.ContractStatus != common.StatusDraft && contract.ContractStatus != common.StatusActive {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
if contract.ContractStatus != common.StatusDraft {
|
||||
infra.Response.Error(ctx, errBindingContractStatus)
|
||||
return
|
||||
}
|
||||
var product models.ProductInfo
|
||||
if err := impl.DBService.Where("identity = ?", request.ProductIdentity).First(&product).Error; err != nil ||
|
||||
product.Status != common.StatusEnable || product.ProductStatus == common.StatusScrapped || product.UserAccountID != contract.UserAccountID {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
if err := impl.DBService.Where("identity = ?", request.ProductIdentity).First(&product).Error; err != nil {
|
||||
common.RespondRecordError(ctx, err)
|
||||
return
|
||||
}
|
||||
if product.Status != common.StatusEnable {
|
||||
infra.Response.Error(ctx, errBindingProductDisabled)
|
||||
return
|
||||
}
|
||||
if product.ProductStatus == common.StatusScrapped {
|
||||
infra.Response.Error(ctx, errBindingProductScrapped)
|
||||
return
|
||||
}
|
||||
if product.UserAccountID != contract.UserAccountID {
|
||||
infra.Response.Error(ctx, errBindingProductOwner)
|
||||
return
|
||||
}
|
||||
var activeBindingCount int64
|
||||
if err := impl.DBService.Model(&models.GasorderContractProduct{}).
|
||||
Where("gasorder_contract_id = ? AND product_info_id = ? AND unbound_at IS NULL", contract.ID, product.ID).
|
||||
Count(&activeBindingCount).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
if activeBindingCount > 0 {
|
||||
infra.Response.Error(ctx, errBindingProductDuplicate)
|
||||
return
|
||||
}
|
||||
var productType models.ProductType
|
||||
|
||||
@@ -28,12 +28,31 @@ var productLifecycleStatuses = map[int]bool{
|
||||
|
||||
func ProductInfoHandlers(relations ...common.ResourceRelation) (gin.HandlerFunc, gin.HandlerFunc, gin.HandlerFunc, gin.HandlerFunc) {
|
||||
fields := []string{"code", "name", "params", "produced_at", "action", "reason", "remark"}
|
||||
return func(ctx *gin.Context) { common.ListResource(ctx, &models.ProductInfo{}) },
|
||||
return listProductInfo,
|
||||
func(ctx *gin.Context) { createProductInfo(ctx, fields, relations) },
|
||||
func(ctx *gin.Context) { common.GetResource(ctx, &models.ProductInfo{}) },
|
||||
func(ctx *gin.Context) { updateProductInfo(ctx, fields, relations) }
|
||||
}
|
||||
|
||||
// listProductInfo 按合同筛选可绑定气瓶;普通列表请求保持原有行为。
|
||||
func listProductInfo(ctx *gin.Context) {
|
||||
contractIdentity := strings.TrimSpace(ctx.Query("contract_identity"))
|
||||
if contractIdentity == "" {
|
||||
common.ListResource(ctx, &models.ProductInfo{})
|
||||
return
|
||||
}
|
||||
var contract models.GasorderContract
|
||||
if err := impl.DBService.Where("identity = ?", contractIdentity).First(&contract).Error; err != nil {
|
||||
common.RespondRecordError(ctx, err)
|
||||
return
|
||||
}
|
||||
common.ListPageFiltered[models.ProductInfo](ctx, func(query *gorm.DB) *gorm.DB {
|
||||
return query.
|
||||
Where("status = ? AND product_status <> ? AND user_account_id = ?", common.StatusEnable, common.StatusScrapped, contract.UserAccountID).
|
||||
Where("NOT EXISTS (SELECT 1 FROM gasorder_contract_product WHERE gasorder_contract_product.product_info_id = product_info.id AND gasorder_contract_product.gasorder_contract_id = ? AND gasorder_contract_product.unbound_at IS NULL)", contract.ID)
|
||||
})
|
||||
}
|
||||
|
||||
func createProductInfo(ctx *gin.Context, fields []string, relations []common.ResourceRelation) {
|
||||
values, err := common.PrepareResourceValues(ctx, &models.ProductInfo{}, fields, relations)
|
||||
if err != nil || !validParamsText(values["params"]) {
|
||||
|
||||
Reference in New Issue
Block a user