优化合同气瓶绑定与启用失败提示
- 将绑定气瓶入口移至合同列表,仅草稿合同显示 - 自动预填所选合同并保留返回路径 - 细分附件、有效期、气瓶绑定和状态校验失败原因 - 增加前后端回归检查及中文操作日志
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
// 功能:验证合同生命周期业务错误可安全、明确地返回前端。
|
||||
// 版本:v1.0.0
|
||||
package gasorder
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestContractChangeBusinessErrors 验证所有允许透传的合同业务原因。
|
||||
func TestContractChangeBusinessErrors(t *testing.T) {
|
||||
businessErrors := []error{
|
||||
errContractCannotActivate,
|
||||
errContractCannotTerminate,
|
||||
errContractAttachment,
|
||||
errContractNotEffective,
|
||||
errContractExpired,
|
||||
errContractNoProduct,
|
||||
}
|
||||
for _, businessErr := range businessErrors {
|
||||
if !isContractChangeBusinessError(businessErr) {
|
||||
t.Fatalf("业务错误未被识别:%s", businessErr)
|
||||
}
|
||||
}
|
||||
if isContractChangeBusinessError(errors.New("database connection failed")) {
|
||||
t.Fatal("内部错误不应透传给前端")
|
||||
}
|
||||
}
|
||||
|
||||
// TestContractActivationErrorMessages 验证关键启用失败原因保持清晰中文。
|
||||
func TestContractActivationErrorMessages(t *testing.T) {
|
||||
want := map[error]string{
|
||||
errContractAttachment: "合同附件无效,请重新上传有效的 PDF 文件后再启用",
|
||||
errContractNotEffective: "合同尚未到生效时间,暂时不能启用",
|
||||
errContractExpired: "合同已到期,请调整到期时间或续签后再启用",
|
||||
errContractNoProduct: "合同尚未绑定有效气瓶,请先绑定气瓶后再启用",
|
||||
}
|
||||
for businessErr, message := range want {
|
||||
if businessErr.Error() != message {
|
||||
t.Fatalf("错误提示不清晰:得到 %q,期望 %q", businessErr.Error(), message)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,16 @@ var gasorderCreatorModels = map[string]any{
|
||||
"delivery": &models.DeliveryBasic{}, "gas": &models.GasBasic{},
|
||||
}
|
||||
|
||||
// 合同生命周期业务错误会直接返回前端,避免把可处理原因模糊成参数错误。
|
||||
var (
|
||||
errContractCannotActivate = errors.New("当前合同状态不允许启用,请刷新页面后重试")
|
||||
errContractCannotTerminate = errors.New("仅生效中的合同可以终止")
|
||||
errContractAttachment = errors.New("合同附件无效,请重新上传有效的 PDF 文件后再启用")
|
||||
errContractNotEffective = errors.New("合同尚未到生效时间,暂时不能启用")
|
||||
errContractExpired = errors.New("合同已到期,请调整到期时间或续签后再启用")
|
||||
errContractNoProduct = errors.New("合同尚未绑定有效气瓶,请先绑定气瓶后再启用")
|
||||
)
|
||||
|
||||
func ListGasorderContract(ctx *gin.Context) { common.ListResource(ctx, &models.GasorderContract{}) }
|
||||
func GetGasorderContract(ctx *gin.Context) { getGasorderContract(ctx) }
|
||||
func ListGasorderContractProduct(ctx *gin.Context) {
|
||||
@@ -337,24 +347,30 @@ func changeGasorderContract(ctx *gin.Context, action string, target int) {
|
||||
return err
|
||||
}
|
||||
if action == "activate" && contract.ContractStatus != common.StatusDraft && contract.ContractStatus != common.StatusTerminated {
|
||||
return errors.New("contract cannot be activated")
|
||||
return errContractCannotActivate
|
||||
}
|
||||
if action == "terminate" && contract.ContractStatus != common.StatusActive {
|
||||
return errors.New("contract cannot be terminated")
|
||||
return errContractCannotTerminate
|
||||
}
|
||||
if target == common.StatusActive {
|
||||
attachmentPath, attachmentErr := contractAttachmentPath(contract.FileURI, false)
|
||||
if attachmentErr != nil || !validStoredContractPDF(attachmentPath) {
|
||||
return errors.New("contract has no valid attachment")
|
||||
return errContractAttachment
|
||||
}
|
||||
now := time.Now()
|
||||
if contract.EffectiveAt.After(now) || (contract.ExpiredAt != nil && !contract.ExpiredAt.After(now)) {
|
||||
return errors.New("contract outside effective period")
|
||||
if contract.EffectiveAt.After(now) {
|
||||
return errContractNotEffective
|
||||
}
|
||||
if contract.ExpiredAt != nil && !contract.ExpiredAt.After(now) {
|
||||
return errContractExpired
|
||||
}
|
||||
var productCount int64
|
||||
if err := tx.Model(&models.GasorderContractProduct{}).
|
||||
Where("gasorder_contract_id = ? AND unbound_at IS NULL", contract.ID).Count(&productCount).Error; err != nil || productCount == 0 {
|
||||
return errors.New("contract has no active product")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return errContractNoProduct
|
||||
}
|
||||
}
|
||||
if err := tx.Model(&contract).Update("contract_status", target).Error; err != nil {
|
||||
@@ -364,12 +380,26 @@ func changeGasorderContract(ctx *gin.Context, action string, target int) {
|
||||
return tx.Create(contractRevision(contract, action, request.Reason, operatorIdentity, operatorName)).Error
|
||||
})
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
if isContractChangeBusinessError(err) {
|
||||
infra.Response.Error(ctx, err)
|
||||
} else {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
}
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"updated": true, "contract_status": target})
|
||||
}
|
||||
|
||||
// isContractChangeBusinessError 仅允许预定义业务原因透传,避免泄露数据库内部错误。
|
||||
func isContractChangeBusinessError(err error) bool {
|
||||
return errors.Is(err, errContractCannotActivate) ||
|
||||
errors.Is(err, errContractCannotTerminate) ||
|
||||
errors.Is(err, errContractAttachment) ||
|
||||
errors.Is(err, errContractNotEffective) ||
|
||||
errors.Is(err, errContractExpired) ||
|
||||
errors.Is(err, errContractNoProduct)
|
||||
}
|
||||
|
||||
func contractRevision(contract models.GasorderContract, action, reason, operatorIdentity, operatorName string) *models.GasorderContractRevision {
|
||||
return &models.GasorderContractRevision{
|
||||
Entity: models.Entity{Identity: models.NewIdentity(), Status: common.StatusEnable},
|
||||
|
||||
Reference in New Issue
Block a user