package services import ( "encoding/base64" "strings" "testing" "github.com/stretchr/testify/assert" "git.beifan.cn/trace-system/backend-go/database" "git.beifan.cn/trace-system/backend-go/models" ) // validSignatureFixture 构造一个合法的签名 dataURL(>200 字节) func validSignatureFixture() string { payload := strings.Repeat("a", 400) return "data:image/png;base64," + base64.StdEncoding.EncodeToString([]byte(payload)) } // confirmTest 为客户确认相关测试做准备:清空限流计数(多个测试可能共享同一序列号) func confirmTest(t *testing.T) { t.Helper() ResetConfirmRateLimit() } func seedTechnician(t *testing.T, username string) models.User { t.Helper() user := models.User{ Username: username, Password: "hashed", Name: "技术员-" + username, Email: username + "@example.com", Role: "technician", } if err := database.DB.Create(&user).Error; err != nil { t.Fatalf("seed technician failed: %v", err) } return user } func createOrderFor(t *testing.T, user models.User, phone string) *models.AftersalesOrder { t.Helper() svc := AftersalesService{} order, err := svc.Create(models.CreateAftersalesOrderDTO{ CompanyName: "TestCo_" + user.Username, CompanyAddress: "测试地址", ContactName: "张三", ContactPhone: phone, ServiceType: "software", IssueDescription: "系统无法启动", }, user.ID) if err != nil { t.Fatalf("create order failed: %v", err) } return order } func TestAftersalesService_Create_GeneratesYYMMDDNNSerial(t *testing.T) { user := seedTechnician(t, "aftersales_create_tech") defer database.DB.Unscoped().Delete(&user) svc := AftersalesService{} order, err := svc.Create(models.CreateAftersalesOrderDTO{ CompanyName: "AftersalesSerialCo", CompanyAddress: "杭州市西湖区", ContactName: "李四", ContactPhone: "13800001234", ServiceType: "hardware", IssueDescription: "硬盘故障", }, user.ID) assert.NoError(t, err) assert.NotNil(t, order) assert.True(t, strings.HasPrefix(order.SerialNumber, "zjbf-sh-")) assert.Len(t, order.SerialNumber, len("zjbf-sh-")+8, "default serial should be 8 digits (YYMMDDNN)") assert.Equal(t, WorkOrderStatusCreated, order.WorkOrderStatus) assert.Equal(t, AuthorizationStatusPending, order.AuthorizationStatus) assert.NotNil(t, order.TechnicianID) assert.Equal(t, user.ID, *order.TechnicianID) database.DB.Unscoped().Delete(order) database.DB.Unscoped().Where("company_name = ?", "AftersalesSerialCo").Delete(&models.Company{}) } func TestAftersalesService_Create_SerialIncrementsWithinDay(t *testing.T) { user := seedTechnician(t, "aftersales_seq_tech") defer database.DB.Unscoped().Delete(&user) svc := AftersalesService{} first, err := svc.Create(models.CreateAftersalesOrderDTO{ CompanyName: "SeqCo", CompanyAddress: "addr", ContactName: "A", ContactPhone: "13800002000", ServiceType: "maintenance", IssueDescription: "issue 1", }, user.ID) assert.NoError(t, err) second, err := svc.Create(models.CreateAftersalesOrderDTO{ CompanyName: "SeqCo", CompanyAddress: "addr", ContactName: "A", ContactPhone: "13800002000", ServiceType: "maintenance", IssueDescription: "issue 2", }, user.ID) assert.NoError(t, err) assert.NotEqual(t, first.SerialNumber, second.SerialNumber) // 第二单的序号应大于第一单(按天递增) assert.True(t, second.SerialNumber > first.SerialNumber) database.DB.Unscoped().Delete(first) database.DB.Unscoped().Delete(second) database.DB.Unscoped().Where("company_name = ?", "SeqCo").Delete(&models.Company{}) } func TestAftersalesService_SubmitForConfirmation_OwnerOnly(t *testing.T) { owner := seedTechnician(t, "aftersales_owner") intruder := seedTechnician(t, "aftersales_intruder") defer database.DB.Unscoped().Delete(&owner) defer database.DB.Unscoped().Delete(&intruder) order := createOrderFor(t, owner, "13800003000") defer database.DB.Unscoped().Delete(order) defer database.DB.Unscoped().Where("company_name = ?", order.CompanyName).Delete(&models.Company{}) svc := AftersalesService{} // 非负责人技术员不能提交 _, err := svc.SubmitForConfirmation(order.SerialNumber, models.SubmitForConfirmationDTO{ ResolutionNote: "已重装系统", }, intruder) assert.Error(t, err) // 负责人可以提交 updated, err := svc.SubmitForConfirmation(order.SerialNumber, models.SubmitForConfirmationDTO{ ResolutionNote: "已重装系统", }, owner) assert.NoError(t, err) assert.Equal(t, WorkOrderStatusPendingConfirmation, updated.WorkOrderStatus) assert.Equal(t, "已重装系统", updated.ResolutionNote) } func TestAftersalesService_SubmitForConfirmation_RejectsClosed(t *testing.T) { owner := seedTechnician(t, "aftersales_closed_owner") defer database.DB.Unscoped().Delete(&owner) order := createOrderFor(t, owner, "13800004000") defer database.DB.Unscoped().Delete(order) defer database.DB.Unscoped().Where("company_name = ?", order.CompanyName).Delete(&models.Company{}) svc := AftersalesService{} _, err := svc.SubmitForConfirmation(order.SerialNumber, models.SubmitForConfirmationDTO{ ResolutionNote: "first submit", }, owner) assert.NoError(t, err) // 已经 pending_confirmation 状态,不能再提交 _, err = svc.SubmitForConfirmation(order.SerialNumber, models.SubmitForConfirmationDTO{ ResolutionNote: "second submit", }, owner) assert.Error(t, err) } func TestAftersalesService_PublicQuery_MasksPhoneAndSetsScannedAt(t *testing.T) { owner := seedTechnician(t, "aftersales_public_owner") defer database.DB.Unscoped().Delete(&owner) order := createOrderFor(t, owner, "13800005000") defer database.DB.Unscoped().Delete(order) defer database.DB.Unscoped().Where("company_name = ?", order.CompanyName).Delete(&models.Company{}) svc := AftersalesService{} view, err := svc.PublicQuery(order.SerialNumber) assert.NoError(t, err) assert.NotNil(t, view) assert.Equal(t, order.CompanyName, view.CompanyName) assert.Equal(t, order.ContactName, view.ContactName) // PublicView 不应该带电话字段(只有以下字段,通过类型保证) // 验证 ScannedAt 已设置 var refreshed models.AftersalesOrder database.DB.Where("serial_number = ?", order.SerialNumber).First(&refreshed) assert.NotNil(t, refreshed.ScannedAt) } func TestAftersalesService_CustomerConfirm_Authorize(t *testing.T) { confirmTest(t) owner := seedTechnician(t, "aftersales_auth_owner") defer database.DB.Unscoped().Delete(&owner) order := createOrderFor(t, owner, "13800006789") defer database.DB.Unscoped().Delete(order) defer database.DB.Unscoped().Where("company_name = ?", order.CompanyName).Delete(&models.Company{}) svc := AftersalesService{} _, _ = svc.SubmitForConfirmation(order.SerialNumber, models.SubmitForConfirmationDTO{ ResolutionNote: "done", }, owner) sig := validSignatureFixture() view, err := svc.CustomerConfirm(order.SerialNumber, models.CustomerConfirmDTO{ Action: "authorize", Signature: sig, }) assert.NoError(t, err) assert.NotNil(t, view) assert.Equal(t, WorkOrderStatusClosed, view.WorkOrderStatus) assert.Equal(t, AuthorizationStatusAuthorized, view.AuthorizationStatus) assert.NotNil(t, view.ConfirmedAt) assert.Equal(t, sig, view.Signature) var refreshed models.AftersalesOrder database.DB.Where("serial_number = ?", order.SerialNumber).First(&refreshed) assert.Equal(t, sig, refreshed.Signature) } func TestAftersalesService_CustomerConfirm_AuthorizeRejectsEmptySignature(t *testing.T) { confirmTest(t) owner := seedTechnician(t, "aftersales_emptysig_owner") defer database.DB.Unscoped().Delete(&owner) order := createOrderFor(t, owner, "13800007777") defer database.DB.Unscoped().Delete(order) defer database.DB.Unscoped().Where("company_name = ?", order.CompanyName).Delete(&models.Company{}) svc := AftersalesService{} _, _ = svc.SubmitForConfirmation(order.SerialNumber, models.SubmitForConfirmationDTO{ ResolutionNote: "done", }, owner) _, err := svc.CustomerConfirm(order.SerialNumber, models.CustomerConfirmDTO{ Action: "authorize", Signature: "", }) assert.Error(t, err) assert.Contains(t, err.Error(), "签名") } func TestAftersalesService_CustomerConfirm_AuthorizeRejectsInvalidSignature(t *testing.T) { confirmTest(t) owner := seedTechnician(t, "aftersales_badsig_owner") defer database.DB.Unscoped().Delete(&owner) order := createOrderFor(t, owner, "13800007788") defer database.DB.Unscoped().Delete(order) defer database.DB.Unscoped().Where("company_name = ?", order.CompanyName).Delete(&models.Company{}) svc := AftersalesService{} _, _ = svc.SubmitForConfirmation(order.SerialNumber, models.SubmitForConfirmationDTO{ ResolutionNote: "done", }, owner) // 非 dataURL 格式 _, err := svc.CustomerConfirm(order.SerialNumber, models.CustomerConfirmDTO{ Action: "authorize", Signature: "not-a-data-url", }) assert.Error(t, err) assert.Contains(t, err.Error(), "签名格式") // 太短 tiny := "data:image/png;base64," + base64.StdEncoding.EncodeToString([]byte("xx")) _, err = svc.CustomerConfirm(order.SerialNumber, models.CustomerConfirmDTO{ Action: "authorize", Signature: tiny, }) assert.Error(t, err) assert.Contains(t, err.Error(), "过短") } func TestAftersalesService_CustomerConfirm_RejectRequiresReason(t *testing.T) { confirmTest(t) owner := seedTechnician(t, "aftersales_rejnoreason_owner") defer database.DB.Unscoped().Delete(&owner) order := createOrderFor(t, owner, "13800008877") defer database.DB.Unscoped().Delete(order) defer database.DB.Unscoped().Where("company_name = ?", order.CompanyName).Delete(&models.Company{}) svc := AftersalesService{} _, _ = svc.SubmitForConfirmation(order.SerialNumber, models.SubmitForConfirmationDTO{ ResolutionNote: "done", }, owner) _, err := svc.CustomerConfirm(order.SerialNumber, models.CustomerConfirmDTO{ Action: "reject", RejectReason: "", }) assert.Error(t, err) assert.Contains(t, err.Error(), "退回原因") } func TestAftersalesService_CustomerConfirm_RejectIncrementsCount(t *testing.T) { confirmTest(t) owner := seedTechnician(t, "aftersales_reject_owner") defer database.DB.Unscoped().Delete(&owner) order := createOrderFor(t, owner, "13800008888") defer database.DB.Unscoped().Delete(order) defer database.DB.Unscoped().Where("company_name = ?", order.CompanyName).Delete(&models.Company{}) svc := AftersalesService{} _, _ = svc.SubmitForConfirmation(order.SerialNumber, models.SubmitForConfirmationDTO{ ResolutionNote: "done", }, owner) view, err := svc.CustomerConfirm(order.SerialNumber, models.CustomerConfirmDTO{ Action: "reject", RejectReason: "没修好", }) assert.NoError(t, err) assert.Equal(t, WorkOrderStatusRejected, view.WorkOrderStatus) assert.Equal(t, AuthorizationStatusUnauthorized, view.AuthorizationStatus) assert.Empty(t, view.Signature, "reject 不应该写入签名") var refreshed models.AftersalesOrder database.DB.Where("serial_number = ?", order.SerialNumber).First(&refreshed) assert.Equal(t, 1, refreshed.RejectCount) assert.Contains(t, refreshed.ResolutionNote, "没修好") // 退回后技术员可以再次提交 _, err = svc.SubmitForConfirmation(order.SerialNumber, models.SubmitForConfirmationDTO{ ResolutionNote: "re-do", }, owner) assert.NoError(t, err) } func TestAftersalesService_CustomerConfirm_RejectsWrongStatus(t *testing.T) { confirmTest(t) owner := seedTechnician(t, "aftersales_wrongstatus_owner") defer database.DB.Unscoped().Delete(&owner) order := createOrderFor(t, owner, "13800009999") defer database.DB.Unscoped().Delete(order) defer database.DB.Unscoped().Where("company_name = ?", order.CompanyName).Delete(&models.Company{}) svc := AftersalesService{} // 未提交客户确认,工单仍是 created,应该拒绝 _, err := svc.CustomerConfirm(order.SerialNumber, models.CustomerConfirmDTO{ Action: "authorize", Signature: validSignatureFixture(), }) assert.Error(t, err) } func TestAftersalesService_ForceClose_AdminOverride(t *testing.T) { owner := seedTechnician(t, "aftersales_force_owner") defer database.DB.Unscoped().Delete(&owner) order := createOrderFor(t, owner, "13800001111") defer database.DB.Unscoped().Delete(order) defer database.DB.Unscoped().Where("company_name = ?", order.CompanyName).Delete(&models.Company{}) svc := AftersalesService{} updated, err := svc.ForceClose(order.SerialNumber) assert.NoError(t, err) assert.Equal(t, WorkOrderStatusClosed, updated.WorkOrderStatus) assert.Equal(t, AuthorizationStatusAuthorized, updated.AuthorizationStatus) }