feat: Implement comprehensive client and administrator invoice management features.

This commit is contained in:
wood911010 committed 2026-02-24 22:26:09 +08:00
1 parent b65b4ac801
commit 560adafe27
7 files changed
+49 -22

No files matched your search

+4
View File
@@ -22,6 +22,8 @@
|:------|:----|
| 日志库 | 使用 logrus,不要使用其他日志库 |
| 注释 | 接口的请求和响应结构体需要加上**中文注释** |
| 接口参数 | 可选参数(非必输)必须使用指针类型(如 `*string`),以便正确区分“未传”和“明确传空”的区别 |
| 参数校验 | 返回无效请求错误(如 `900001`)时,必须在提示信息后追加具体的原因,例如:`Message: msg + ": " + err.Error()` |
| 设计原则 | 避免过度设计,保持代码的简洁和可读性 |
| 错误处理 | 不可为了处理错误而处理错误,让错误在源头尖叫,而不是让它在下游腐烂。|
@@ -68,6 +70,8 @@
- ✅ 为所有接口字段添加中文注释
- ✅ 遇到不确定的问题时主动询问
- ✅ 让错误在源头尖叫,而不是让它在下游腐烂
- ✅ 可选参数使用指针类型
- ✅ 参数校验报错追加具体原因 (`err.Error()`)
### 不应该做的
@@ -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
@@ -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
@@ -5,7 +5,7 @@ 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字符
Suggestion *string `json:"suggestion" binding:"omitempty,max=500"` // 修改建议,非必填,最大500字符
}
// InvoiceIssueRequest 发票开票请求结构(管理端)
+4 -4
View File
@@ -16,11 +16,11 @@ 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字符
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字符
Content *string `json:"content" binding:"omitempty,max=200"` // 开票内容,非必填,最多200字符
Remark *string `json:"remark,omitempty"` // 备注,可选
}
@@ -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
+29 -6
View File
@@ -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
// 更新字段(仅更新非空字段)
if req.InvoiceType != nil {
invoice.InvoiceType = *req.InvoiceType
}
if req.Title != nil {
invoice.Title = *req.Title
}
if req.TaxpayerID != nil {
invoice.TaxpayerID = req.TaxpayerID
invoice.Content = req.Content
invoice.ReceiverEmail = req.ReceiverEmail
}
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