From 1ebec18869d889a5f0cb76671630c0e2703e3b89 Mon Sep 17 00:00:00 2001 From: Frudrax Cheng Date: Tue, 2 Jun 2026 10:38:29 +0800 Subject: [PATCH] Add responsible signature to aftersales confirmation --- models/models.go | 48 ++++++++++++++------------- services/aftersales_service.go | 29 ++++++++++------- services/aftersales_service_test.go | 50 +++++++++++++++++++++++------ 3 files changed, 83 insertions(+), 44 deletions(-) diff --git a/models/models.go b/models/models.go index c2d5422..dc7a519 100644 --- a/models/models.go +++ b/models/models.go @@ -266,12 +266,13 @@ type AftersalesOrder struct { WorkOrderStatus string `gorm:"size:32;default:'created'" json:"workOrderStatus"` AuthorizationStatus string `gorm:"size:32;default:'pending'" json:"authorizationStatus"` - TechnicianID *uint `json:"technicianId"` - CreatedBy *uint `json:"createdBy"` - ScannedAt *time.Time `json:"scannedAt"` - ConfirmedAt *time.Time `json:"confirmedAt"` - RejectCount int `gorm:"default:0" json:"rejectCount"` - Signature string `gorm:"type:text" json:"signature,omitempty"` + TechnicianID *uint `json:"technicianId"` + CreatedBy *uint `json:"createdBy"` + ScannedAt *time.Time `json:"scannedAt"` + ConfirmedAt *time.Time `json:"confirmedAt"` + RejectCount int `gorm:"default:0" json:"rejectCount"` + Signature string `gorm:"type:text" json:"signature,omitempty"` + ResponsibleSignature string `gorm:"type:text" json:"responsibleSignature,omitempty"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` @@ -310,11 +311,13 @@ type SubmitForConfirmationDTO struct { // CustomerConfirmDTO 客户确认请求 // Signature 为客户在网页上手写签名的 base64 PNG dataURL,仅 authorize 时必填 +// ResponsibleSignature 为负责人在网页上手写签名的 base64 PNG dataURL,仅 authorize 时必填 // RejectReason 为客户拒绝的原因,仅 reject 时必填 type CustomerConfirmDTO struct { - Action string `json:"action" validate:"required,oneof=authorize reject"` - Signature string `json:"signature,omitempty" validate:"required_if=Action authorize"` - RejectReason string `json:"rejectReason,omitempty" validate:"required_if=Action reject"` + Action string `json:"action" validate:"required,oneof=authorize reject"` + Signature string `json:"signature,omitempty" validate:"required_if=Action authorize"` + ResponsibleSignature string `json:"responsibleSignature,omitempty" validate:"required_if=Action authorize"` + RejectReason string `json:"rejectReason,omitempty" validate:"required_if=Action reject"` } // ReassignAftersalesDTO 重新分配技术员请求 @@ -324,17 +327,18 @@ type ReassignAftersalesDTO struct { // AftersalesPublicView 公开查询返回视图(脱敏) type AftersalesPublicView struct { - SerialNumber string `json:"serialNumber"` - CompanyName string `json:"companyName"` - CompanyAddress string `json:"companyAddress"` - ContactName string `json:"contactName"` - ServiceType string `json:"serviceType"` - IssueDescription string `json:"issueDescription"` - ResolutionNote string `json:"resolutionNote"` - WorkOrderStatus string `json:"workOrderStatus"` - AuthorizationStatus string `json:"authorizationStatus"` - TechnicianName string `json:"technicianName"` - CreatedAt time.Time `json:"createdAt"` - ConfirmedAt *time.Time `json:"confirmedAt"` - Signature string `json:"signature,omitempty"` + SerialNumber string `json:"serialNumber"` + CompanyName string `json:"companyName"` + CompanyAddress string `json:"companyAddress"` + ContactName string `json:"contactName"` + ServiceType string `json:"serviceType"` + IssueDescription string `json:"issueDescription"` + ResolutionNote string `json:"resolutionNote"` + WorkOrderStatus string `json:"workOrderStatus"` + AuthorizationStatus string `json:"authorizationStatus"` + TechnicianName string `json:"technicianName"` + CreatedAt time.Time `json:"createdAt"` + ConfirmedAt *time.Time `json:"confirmedAt"` + Signature string `json:"signature,omitempty"` + ResponsibleSignature string `json:"responsibleSignature,omitempty"` } diff --git a/services/aftersales_service.go b/services/aftersales_service.go index a960648..a7b87f8 100644 --- a/services/aftersales_service.go +++ b/services/aftersales_service.go @@ -329,18 +329,19 @@ func (s *AftersalesService) PublicQuery(serialNumber string) (*models.Aftersales } view := &models.AftersalesPublicView{ - SerialNumber: order.SerialNumber, - CompanyName: order.CompanyName, - CompanyAddress: order.CompanyAddress, - ContactName: order.ContactName, - ServiceType: order.ServiceType, - IssueDescription: order.IssueDescription, - ResolutionNote: order.ResolutionNote, - WorkOrderStatus: order.WorkOrderStatus, - AuthorizationStatus: order.AuthorizationStatus, - CreatedAt: order.CreatedAt, - ConfirmedAt: order.ConfirmedAt, - Signature: order.Signature, + SerialNumber: order.SerialNumber, + CompanyName: order.CompanyName, + CompanyAddress: order.CompanyAddress, + ContactName: order.ContactName, + ServiceType: order.ServiceType, + IssueDescription: order.IssueDescription, + ResolutionNote: order.ResolutionNote, + WorkOrderStatus: order.WorkOrderStatus, + AuthorizationStatus: order.AuthorizationStatus, + CreatedAt: order.CreatedAt, + ConfirmedAt: order.ConfirmedAt, + Signature: order.Signature, + ResponsibleSignature: order.ResponsibleSignature, } if order.Technician != nil { view.TechnicianName = order.Technician.Name @@ -372,10 +373,14 @@ func (s *AftersalesService) CustomerConfirm(serialNumber string, dto models.Cust if err := validateSignature(dto.Signature); err != nil { return nil, err } + if err := validateSignature(dto.ResponsibleSignature); err != nil { + return nil, err + } order.WorkOrderStatus = WorkOrderStatusClosed order.AuthorizationStatus = AuthorizationStatusAuthorized order.ConfirmedAt = &now order.Signature = strings.TrimSpace(dto.Signature) + order.ResponsibleSignature = strings.TrimSpace(dto.ResponsibleSignature) case "reject": reason := strings.TrimSpace(dto.RejectReason) if reason == "" { diff --git a/services/aftersales_service_test.go b/services/aftersales_service_test.go index 7cc49ee..4ca59db 100644 --- a/services/aftersales_service_test.go +++ b/services/aftersales_service_test.go @@ -214,9 +214,11 @@ func TestAftersalesService_CustomerConfirm_Authorize(t *testing.T) { }, owner) sig := validSignatureFixture() + responsibleSig := validSignatureFixture() view, err := svc.CustomerConfirm(order.SerialNumber, models.CustomerConfirmDTO{ - Action: "authorize", - Signature: sig, + Action: "authorize", + Signature: sig, + ResponsibleSignature: responsibleSig, }) assert.NoError(t, err) assert.NotNil(t, view) @@ -224,10 +226,12 @@ func TestAftersalesService_CustomerConfirm_Authorize(t *testing.T) { assert.Equal(t, AuthorizationStatusAuthorized, view.AuthorizationStatus) assert.NotNil(t, view.ConfirmedAt) assert.Equal(t, sig, view.Signature) + assert.Equal(t, responsibleSig, view.ResponsibleSignature) var refreshed models.AftersalesOrder database.DB.Where("serial_number = ?", order.SerialNumber).First(&refreshed) assert.Equal(t, sig, refreshed.Signature) + assert.Equal(t, responsibleSig, refreshed.ResponsibleSignature) } func TestAftersalesService_CustomerConfirm_AuthorizeRejectsEmptySignature(t *testing.T) { @@ -244,8 +248,31 @@ func TestAftersalesService_CustomerConfirm_AuthorizeRejectsEmptySignature(t *tes }, owner) _, err := svc.CustomerConfirm(order.SerialNumber, models.CustomerConfirmDTO{ - Action: "authorize", - Signature: "", + Action: "authorize", + Signature: "", + ResponsibleSignature: validSignatureFixture(), + }) + assert.Error(t, err) + assert.Contains(t, err.Error(), "签名") +} + +func TestAftersalesService_CustomerConfirm_AuthorizeRejectsEmptyResponsibleSignature(t *testing.T) { + confirmTest(t) + owner := seedTechnician(t, "aftersales_empty_responsible_sig_owner") + defer database.DB.Unscoped().Delete(&owner) + + order := createOrderFor(t, owner, "13800007778") + defer database.DB.Unscoped().Delete(order) + + svc := AftersalesService{} + _, _ = svc.SubmitForConfirmation(order.SerialNumber, models.SubmitForConfirmationDTO{ + ResolutionNote: "done", + }, owner) + + _, err := svc.CustomerConfirm(order.SerialNumber, models.CustomerConfirmDTO{ + Action: "authorize", + Signature: validSignatureFixture(), + ResponsibleSignature: "", }) assert.Error(t, err) assert.Contains(t, err.Error(), "签名") @@ -266,8 +293,9 @@ func TestAftersalesService_CustomerConfirm_AuthorizeRejectsInvalidSignature(t *t // 非 dataURL 格式 _, err := svc.CustomerConfirm(order.SerialNumber, models.CustomerConfirmDTO{ - Action: "authorize", - Signature: "not-a-data-url", + Action: "authorize", + Signature: "not-a-data-url", + ResponsibleSignature: validSignatureFixture(), }) assert.Error(t, err) assert.Contains(t, err.Error(), "签名格式") @@ -275,8 +303,9 @@ func TestAftersalesService_CustomerConfirm_AuthorizeRejectsInvalidSignature(t *t // 太短 tiny := "data:image/png;base64," + base64.StdEncoding.EncodeToString([]byte("xx")) _, err = svc.CustomerConfirm(order.SerialNumber, models.CustomerConfirmDTO{ - Action: "authorize", - Signature: tiny, + Action: "authorize", + Signature: tiny, + ResponsibleSignature: validSignatureFixture(), }) assert.Error(t, err) assert.Contains(t, err.Error(), "过短") @@ -348,8 +377,9 @@ func TestAftersalesService_CustomerConfirm_RejectsWrongStatus(t *testing.T) { svc := AftersalesService{} // 未提交客户确认,工单仍是 created,应该拒绝 _, err := svc.CustomerConfirm(order.SerialNumber, models.CustomerConfirmDTO{ - Action: "authorize", - Signature: validSignatureFixture(), + Action: "authorize", + Signature: validSignatureFixture(), + ResponsibleSignature: validSignatureFixture(), }) assert.Error(t, err) }