diff --git a/backend/docs/prompt.md b/backend/docs/prompt.md index c44f2f1..e6094fe 100644 --- a/backend/docs/prompt.md +++ b/backend/docs/prompt.md @@ -22,6 +22,8 @@ |:------|:----| | 日志库 | 使用 logrus,不要使用其他日志库 | | 注释 | 接口的请求和响应结构体需要加上**中文注释** | +| 接口参数 | 可选参数(非必输)必须使用指针类型(如 `*string`),以便正确区分“未传”和“明确传空”的区别 | +| 参数校验 | 返回无效请求错误(如 `900001`)时,必须在提示信息后追加具体的原因,例如:`Message: msg + ": " + err.Error()` | | 设计原则 | 避免过度设计,保持代码的简洁和可读性 | | 错误处理 | 不可为了处理错误而处理错误,让错误在源头尖叫,而不是让它在下游腐烂。| @@ -68,6 +70,8 @@ - ✅ 为所有接口字段添加中文注释 - ✅ 遇到不确定的问题时主动询问 - ✅ 让错误在源头尖叫,而不是让它在下游腐烂 +- ✅ 可选参数使用指针类型 +- ✅ 参数校验报错追加具体原因 (`err.Error()`) ### 不应该做的 diff --git a/backend/internal/api/handlers/cu_invoice_handler.go b/backend/internal/api/handlers/cu_invoice_handler.go index 69fe6b4..52d29bb 100644 --- a/backend/internal/api/handlers/cu_invoice_handler.go +++ b/backend/internal/api/handlers/cu_invoice_handler.go @@ -42,7 +42,7 @@ func (h *CuInvoiceHandler) CreateInvoice(c *gin.Context) { if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, models.ErrorResponse{ Code: "900001", - Message: i18n.GetI18nErrorMessage("900001", lang), + Message: i18n.GetI18nErrorMessage("900001", lang) + ": " + err.Error(), Timestamp: getCurrentTimestamp(), }) return @@ -93,7 +93,7 @@ func (h *CuInvoiceHandler) GetUserInvoices(c *gin.Context) { if err := c.ShouldBindQuery(&req); err != nil { c.JSON(http.StatusBadRequest, models.ErrorResponse{ Code: "900001", - Message: i18n.GetI18nErrorMessage("900001", lang), + Message: i18n.GetI18nErrorMessage("900001", lang) + ": " + err.Error(), Timestamp: getCurrentTimestamp(), }) return @@ -225,7 +225,7 @@ func (h *CuInvoiceHandler) UpdateInvoice(c *gin.Context) { if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, models.ErrorResponse{ Code: "900001", - Message: i18n.GetI18nErrorMessage("900001", lang), + Message: i18n.GetI18nErrorMessage("900001", lang) + ": " + err.Error(), Timestamp: getCurrentTimestamp(), }) return diff --git a/backend/internal/api/handlers/invoice_handler.go b/backend/internal/api/handlers/invoice_handler.go index 9cef583..d6ce5a1 100644 --- a/backend/internal/api/handlers/invoice_handler.go +++ b/backend/internal/api/handlers/invoice_handler.go @@ -156,7 +156,7 @@ func (h *AdminInvoiceHandler) RejectInvoice(c *gin.Context) { if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, models.ErrorResponse{ Code: "900001", - Message: i18n.GetI18nErrorMessage("900001", lang), + Message: i18n.GetI18nErrorMessage("900001", lang) + ": " + err.Error(), Timestamp: getCurrentTimestamp(), }) return @@ -201,7 +201,7 @@ func (h *AdminInvoiceHandler) IssueInvoice(c *gin.Context) { if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, models.ErrorResponse{ Code: "900001", - Message: i18n.GetI18nErrorMessage("900001", lang), + Message: i18n.GetI18nErrorMessage("900001", lang) + ": " + err.Error(), Timestamp: getCurrentTimestamp(), }) return @@ -244,7 +244,7 @@ func (h *AdminInvoiceHandler) UploadInvoiceFile(c *gin.Context) { if invoiceNo == "" { c.JSON(http.StatusBadRequest, models.ErrorResponse{ Code: "900001", - Message: i18n.GetI18nErrorMessage("900001", lang), + Message: i18n.GetI18nErrorMessage("900001", lang) + ": invoice_no is required", Timestamp: getCurrentTimestamp(), }) return @@ -255,7 +255,7 @@ func (h *AdminInvoiceHandler) UploadInvoiceFile(c *gin.Context) { if err != nil { c.JSON(http.StatusBadRequest, models.ErrorResponse{ Code: "900001", - Message: i18n.GetI18nErrorMessage("900001", lang), + Message: i18n.GetI18nErrorMessage("900001", lang) + ": " + err.Error(), Timestamp: getCurrentTimestamp(), }) return @@ -266,7 +266,7 @@ func (h *AdminInvoiceHandler) UploadInvoiceFile(c *gin.Context) { if filepath.Ext(header.Filename) != ".pdf" { c.JSON(http.StatusBadRequest, models.ErrorResponse{ Code: "900001", - Message: i18n.GetI18nErrorMessage("900001", lang), + Message: i18n.GetI18nErrorMessage("900001", lang) + ": file type must be pdf", Timestamp: getCurrentTimestamp(), }) return @@ -281,7 +281,7 @@ func (h *AdminInvoiceHandler) UploadInvoiceFile(c *gin.Context) { if err := c.SaveUploadedFile(header, filePath); err != nil { c.JSON(http.StatusInternalServerError, models.ErrorResponse{ Code: "900004", - Message: i18n.GetI18nErrorMessage("900004", lang), + Message: i18n.GetI18nErrorMessage("900004", lang) + ": " + err.Error(), Timestamp: getCurrentTimestamp(), }) return @@ -320,7 +320,7 @@ func (h *AdminInvoiceHandler) DownloadByToken(c *gin.Context) { if token == "" { c.JSON(http.StatusBadRequest, models.ErrorResponse{ Code: "900001", - Message: i18n.GetI18nErrorMessage("900001", lang), + Message: i18n.GetI18nErrorMessage("900001", lang) + ": token is required", Timestamp: getCurrentTimestamp(), }) return diff --git a/backend/internal/models/admin_invoice_models.go b/backend/internal/models/admin_invoice_models.go index 331cde4..cbe18e2 100644 --- a/backend/internal/models/admin_invoice_models.go +++ b/backend/internal/models/admin_invoice_models.go @@ -4,8 +4,8 @@ package models // InvoiceRejectRequest 发票驳回请求结构(管理端) type InvoiceRejectRequest struct { - RejectReason string `json:"reject_reason" binding:"required,max=500"` // 驳回原因,必填,最大500字符 - Suggestion string `json:"suggestion" binding:"required,max=500"` // 修改建议,必填,最大500字符 + RejectReason string `json:"reject_reason" binding:"required,max=500"` // 驳回原因,必填,最大500字符 + Suggestion *string `json:"suggestion" binding:"omitempty,max=500"` // 修改建议,非必填,最大500字符 } // InvoiceIssueRequest 发票开票请求结构(管理端) diff --git a/backend/internal/models/cu_invoice_models.go b/backend/internal/models/cu_invoice_models.go index 0247b70..441e742 100644 --- a/backend/internal/models/cu_invoice_models.go +++ b/backend/internal/models/cu_invoice_models.go @@ -16,12 +16,12 @@ type InvoiceCreateRequest struct { // InvoiceUpdateRequest 客户端修改发票请求结构 type InvoiceUpdateRequest struct { - InvoiceType string `json:"invoice_type" binding:"required,oneof=personal enterprise vat_special"` // 发票类型,必填,personal-个人/enterprise-企业普票/vat_special-增值税专用发票 - ReceiverEmail string `json:"receiver_email" binding:"required,email"` // 收票邮箱,必填,格式为邮箱地址 - Title string `json:"title" binding:"required,max=200"` // 发票抬头,必填,最多200字符 - TaxpayerID *string `json:"taxpayer_id,omitempty"` // 纳税人识别号,企业发票必填,个人发票可为空 - Content string `json:"content" binding:"omitempty,max=200"` // 开票内容,非必填,最多200字符 - Remark *string `json:"remark,omitempty"` // 备注,可选 + InvoiceType *string `json:"invoice_type" binding:"omitempty,oneof=personal enterprise vat_special"` // 发票类型,非必填,personal-个人/enterprise-企业普票/vat_special-增值税专用发票 + ReceiverEmail *string `json:"receiver_email" binding:"omitempty,email"` // 收票邮箱,非必填,格式为邮箱地址 + Title *string `json:"title" binding:"omitempty,max=200"` // 发票抬头,非必填,最多200字符 + TaxpayerID *string `json:"taxpayer_id,omitempty"` // 纳税人识别号,企业发票必填,个人发票可为空 + Content *string `json:"content" binding:"omitempty,max=200"` // 开票内容,非必填,最多200字符 + Remark *string `json:"remark,omitempty"` // 备注,可选 } // InvoiceListRequest 发票列表查询请求结构 diff --git a/backend/internal/service/admin_invoice_service.go b/backend/internal/service/admin_invoice_service.go index ff08017..639fce5 100644 --- a/backend/internal/service/admin_invoice_service.go +++ b/backend/internal/service/admin_invoice_service.go @@ -65,7 +65,7 @@ func (s *adminInvoiceService) RejectInvoice(ctx context.Context, invoiceID strin now := time.Now() invoice.Status = models.InvoiceStatusRejected invoice.RejectReason = &req.RejectReason - invoice.Suggestion = &req.Suggestion + invoice.Suggestion = req.Suggestion invoice.RejectedAt = &now invoice.RejectedBy = &adminID invoice.UpdatedAt = now diff --git a/backend/internal/service/cu_invoice_service.go b/backend/internal/service/cu_invoice_service.go index 0d99929..898656c 100644 --- a/backend/internal/service/cu_invoice_service.go +++ b/backend/internal/service/cu_invoice_service.go @@ -196,17 +196,40 @@ func (s *cuInvoiceService) UpdateInvoice(ctx context.Context, invoiceID, cuUserI } // 业务逻辑校验:发票类型和纳税人识别号 - if req.InvoiceType != models.InvoiceTypePersonal && (req.TaxpayerID == nil || *req.TaxpayerID == "") { + // 如果传入了新的 InvoiceType 或者 TaxpayerID,我们要校验最终的结果 + finalInvoiceType := invoice.InvoiceType + if req.InvoiceType != nil { + finalInvoiceType = *req.InvoiceType + } + + finalTaxpayerID := invoice.TaxpayerID + if req.TaxpayerID != nil { + finalTaxpayerID = req.TaxpayerID // This handles explicitly setting it to nil or "" as well if needed? Wait, if they set *req.TaxpayerID == "" it will just replace it. + } + + if finalInvoiceType != models.InvoiceTypePersonal && (finalTaxpayerID == nil || *finalTaxpayerID == "") { return nil, i18n.NewI18nError("700004", lang) } - // 更新字段 - invoice.InvoiceType = req.InvoiceType - invoice.Title = req.Title - invoice.TaxpayerID = req.TaxpayerID - invoice.Content = req.Content - invoice.ReceiverEmail = req.ReceiverEmail - invoice.Remark = req.Remark + // 更新字段(仅更新非空字段) + if req.InvoiceType != nil { + invoice.InvoiceType = *req.InvoiceType + } + if req.Title != nil { + invoice.Title = *req.Title + } + if req.TaxpayerID != nil { + invoice.TaxpayerID = req.TaxpayerID + } + if req.Content != nil { + invoice.Content = *req.Content + } + if req.ReceiverEmail != nil { + invoice.ReceiverEmail = *req.ReceiverEmail + } + if req.Remark != nil { + invoice.Remark = req.Remark + } // 重置状态和驳回信息 invoice.Status = models.InvoiceStatusPending