完善合同气瓶绑定约束与候选过滤

- 明确合同状态、气瓶归属、停用、报废和重复绑定失败原因
- 根据当前合同过滤可绑定气瓶,排除无效及重复候选项
- 从合同列表进入绑定页时锁定预填合同,防止切换上下文
- 切换通用新建页合同后清空旧气瓶并重新加载候选项
- 增加前后端回归检查并同步中文操作日志
This commit is contained in:
czl231
2026-08-13 00:10:42 +08:00
parent 96d37d63cf
commit 9937e3a508
11 changed files with 120 additions and 10 deletions

View File

@@ -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{

View File

@@ -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

View File

@@ -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"]) {