diff --git a/internal/domain/audit/audit.go b/internal/domain/audit/audit.go index a52136f..62db0b5 100644 --- a/internal/domain/audit/audit.go +++ b/internal/domain/audit/audit.go @@ -3,17 +3,17 @@ package audit import "time" type Event struct { - ID string `json:"id"` - SessionID string `json:"session_id,omitempty"` - TicketID string `json:"ticket_id,omitempty"` - Type string `json:"type"` - Action string `json:"action,omitempty"` - Channel string `json:"channel,omitempty"` - OpenID string `json:"open_id,omitempty"` - ActorID string `json:"actor_id,omitempty"` - SourceIP string `json:"source_ip,omitempty"` - Payload map[string]any `json:"payload,omitempty"` - BeforeState map[string]any `json:"before_state,omitempty"` - AfterState map[string]any `json:"after_state,omitempty"` - CreatedAt time.Time `json:"created_at"` + ID string `json:"id"` + SessionID string `json:"session_id,omitempty"` + TicketID string `json:"ticket_id,omitempty"` + Type string `json:"type"` + Action string `json:"action,omitempty"` + Channel string `json:"channel,omitempty"` + OpenID string `json:"open_id,omitempty"` + ActorID string `json:"actor_id,omitempty"` + SourceIP string `json:"source_ip,omitempty"` + Payload map[string]any `json:"payload,omitempty"` + BeforeState map[string]any `json:"before_state,omitempty"` + AfterState map[string]any `json:"after_state,omitempty"` + CreatedAt time.Time `json:"created_at"` } diff --git a/internal/domain/audit/audit_test.go b/internal/domain/audit/audit_test.go index bbd20c9..e7565ce 100644 --- a/internal/domain/audit/audit_test.go +++ b/internal/domain/audit/audit_test.go @@ -164,10 +164,10 @@ func TestEvent_TicketAndSessionFields(t *testing.T) { // Session-scoped event sessionEvent := Event{ - ID: "e2", + ID: "e2", SessionID: "s-1", - Type: "session", - Action: "message", + Type: "session", + Action: "message", } if sessionEvent.SessionID != "s-1" { diff --git a/internal/domain/error/cserrors/codes_test.go b/internal/domain/error/cserrors/codes_test.go index c50daca..e02f445 100644 --- a/internal/domain/error/cserrors/codes_test.go +++ b/internal/domain/error/cserrors/codes_test.go @@ -79,8 +79,8 @@ func TestErrorMsg_UnknownCode(t *testing.T) { func TestErrorMsg_SpecificCodes(t *testing.T) { tests := []struct { - code string - expectedMsg string + code string + expectedMsg string }{ {CS_SES_4001, "session not found"}, {CS_SES_4002, "message rate limit exceeded"}, @@ -102,38 +102,38 @@ func TestErrorMsg_SpecificCodes(t *testing.T) { func TestErrorMsg_AllKnownCodesReturnNonEmpty(t *testing.T) { // Verify all codes defined in the switch have non-empty messages knownCodes := map[string]string{ - CS_SES_4001: "session not found", - CS_SES_4002: "message rate limit exceeded", - CS_SES_4003: "identity verification locked", - CS_IDT_4001: "identity information mismatch", - CS_IDT_4002: "verification code incorrect", + CS_SES_4001: "session not found", + CS_SES_4002: "message rate limit exceeded", + CS_SES_4003: "identity verification locked", + CS_IDT_4001: "identity information mismatch", + CS_IDT_4002: "verification code incorrect", CS_TICKET_4001: "ticket not found", CS_TICKET_4002: "ticket already assigned", CS_TICKET_4092: "ticket resolve conflict", CS_TICKET_4093: "ticket close conflict", - CS_KB_4001: "knowledge-base entry not found", - CS_KB_4002: "entry name already exists", - CS_LLM_5001: "LLM service unavailable", - CS_LLM_5002: "LLM request timeout", - CS_AUTH_4001: "access denied", - CS_AUTH_4031: "missing webhook signature", - CS_AUTH_4032: "invalid webhook timestamp", - CS_AUTH_4033: "stale webhook request", - CS_AUTH_4034: "invalid webhook signature", - CS_HTTP_405: "method not allowed", - CS_REQ_4001: "invalid JSON", - CS_REQ_4131: "request body too large", - CS_REQ_4002: "channel, open_id and content are required", - CS_REQ_4003: "content exceeds maximum length", - CS_REQ_4004: "unable to read request body", - CS_REQ_4008: "channel is required", - CS_REQ_4005: "ticket_id and agent_id are required", - CS_REQ_4006: "ticket_id and resolution are required", - CS_REQ_4007: "ticket_id and resolution are required", - CS_REQ_4009: "feedback score must be between 1 and 5", - CS_REQ_4010: "handoff reason is required", - CS_SYS_5001: "internal server error", - CS_SYS_5002: "list tickets failed", + CS_KB_4001: "knowledge-base entry not found", + CS_KB_4002: "entry name already exists", + CS_LLM_5001: "LLM service unavailable", + CS_LLM_5002: "LLM request timeout", + CS_AUTH_4001: "access denied", + CS_AUTH_4031: "missing webhook signature", + CS_AUTH_4032: "invalid webhook timestamp", + CS_AUTH_4033: "stale webhook request", + CS_AUTH_4034: "invalid webhook signature", + CS_HTTP_405: "method not allowed", + CS_REQ_4001: "invalid JSON", + CS_REQ_4131: "request body too large", + CS_REQ_4002: "channel, open_id and content are required", + CS_REQ_4003: "content exceeds maximum length", + CS_REQ_4004: "unable to read request body", + CS_REQ_4008: "channel is required", + CS_REQ_4005: "ticket_id and agent_id are required", + CS_REQ_4006: "ticket_id and resolution are required", + CS_REQ_4007: "ticket_id and resolution are required", + CS_REQ_4009: "feedback score must be between 1 and 5", + CS_REQ_4010: "handoff reason is required", + CS_SYS_5001: "internal server error", + CS_SYS_5002: "list tickets failed", } for code, expectedMsg := range knownCodes { @@ -142,4 +142,4 @@ func TestErrorMsg_AllKnownCodesReturnNonEmpty(t *testing.T) { t.Errorf("ErrorMsg(%q): expected %q, got %q", code, expectedMsg, msg) } } -} \ No newline at end of file +} diff --git a/internal/domain/intent/intent.go b/internal/domain/intent/intent.go index 473a0de..0f45e1e 100644 --- a/internal/domain/intent/intent.go +++ b/internal/domain/intent/intent.go @@ -9,11 +9,11 @@ type Result struct { } const ( - IntentQuota = "quota" - IntentToken = "token" - IntentError = "error" - IntentHandoff = "handoff" - IntentGeneral = "general" - IntentRefund = "refund" + IntentQuota = "quota" + IntentToken = "token" + IntentError = "error" + IntentHandoff = "handoff" + IntentGeneral = "general" + IntentRefund = "refund" IntentSecurity = "security" ) diff --git a/internal/domain/session/session.go b/internal/domain/session/session.go index b450ea0..a2a6d7f 100644 --- a/internal/domain/session/session.go +++ b/internal/domain/session/session.go @@ -5,10 +5,10 @@ import "time" type Status string const ( - StatusIdle Status = "idle" - StatusProcessing Status = "processing" - StatusHandoff Status = "handoff" - StatusClosed Status = "closed" + StatusIdle Status = "idle" + StatusProcessing Status = "processing" + StatusHandoff Status = "handoff" + StatusClosed Status = "closed" ) type MessageContext struct { diff --git a/internal/domain/session/session_test.go b/internal/domain/session/session_test.go index 2e1cb77..1ee952d 100644 --- a/internal/domain/session/session_test.go +++ b/internal/domain/session/session_test.go @@ -187,4 +187,4 @@ func TestSession_FullLifecycle(t *testing.T) { if sess.Status != StatusClosed { t.Error("failed to transition to Closed") } -} \ No newline at end of file +} diff --git a/internal/domain/ticket/ticket.go b/internal/domain/ticket/ticket.go index 54d2393..abf3456 100644 --- a/internal/domain/ticket/ticket.go +++ b/internal/domain/ticket/ticket.go @@ -22,16 +22,16 @@ const ( ) type Ticket struct { - ID string `json:"id"` - SessionID string `json:"session_id"` - UserID string `json:"user_id,omitempty"` - Priority Priority `json:"priority"` - Status Status `json:"status"` - HandoffReason string `json:"handoff_reason"` - AssignedTo string `json:"assigned_to,omitempty"` - ContextSnapshot map[string]any `json:"context_snapshot"` - Resolution string `json:"resolution,omitempty"` - CreatedAt time.Time `json:"created_at"` - ResolvedAt *time.Time `json:"resolved_at,omitempty"` - UpdatedAt time.Time `json:"updated_at"` + ID string `json:"id"` + SessionID string `json:"session_id"` + UserID string `json:"user_id,omitempty"` + Priority Priority `json:"priority"` + Status Status `json:"status"` + HandoffReason string `json:"handoff_reason"` + AssignedTo string `json:"assigned_to,omitempty"` + ContextSnapshot map[string]any `json:"context_snapshot"` + Resolution string `json:"resolution,omitempty"` + CreatedAt time.Time `json:"created_at"` + ResolvedAt *time.Time `json:"resolved_at,omitempty"` + UpdatedAt time.Time `json:"updated_at"` } diff --git a/internal/domain/ticket/ticket_test.go b/internal/domain/ticket/ticket_test.go index 4f1b773..4d292c8 100644 --- a/internal/domain/ticket/ticket_test.go +++ b/internal/domain/ticket/ticket_test.go @@ -132,8 +132,8 @@ func TestTicket_Fields(t *testing.T) { func TestTicket_ResolvedAtOptional(t *testing.T) { // Test that ResolvedAt can be nil (open ticket) tk := Ticket{ - ID: "open-ticket", - Status: StatusOpen, + ID: "open-ticket", + Status: StatusOpen, ResolvedAt: nil, } if tk.ResolvedAt != nil { @@ -170,4 +170,4 @@ func TestTicket_StatusTransitions(t *testing.T) { if tk.Status != StatusClosed { t.Error("failed to transition to Closed") } -} \ No newline at end of file +} diff --git a/internal/domain/ticketstats/stats.go b/internal/domain/ticketstats/stats.go index 974e0c7..212a26a 100644 --- a/internal/domain/ticketstats/stats.go +++ b/internal/domain/ticketstats/stats.go @@ -2,12 +2,12 @@ package ticketstats // Stats represents aggregated ticket statistics for monitoring dashboards. type Stats struct { - Total int `json:"total_tickets"` - Open int `json:"open"` - Resolved int `json:"resolved"` - Closed int `json:"closed"` - ByChannel map[string]int `json:"by_channel"` - ByPriority map[string]int `json:"by_priority"` - HandoffCount int `json:"handoff_count"` - AvgResolutionTimeMinutes float64 `json:"avg_resolution_time_minutes"` + Total int `json:"total_tickets"` + Open int `json:"open"` + Resolved int `json:"resolved"` + Closed int `json:"closed"` + ByChannel map[string]int `json:"by_channel"` + ByPriority map[string]int `json:"by_priority"` + HandoffCount int `json:"handoff_count"` + AvgResolutionTimeMinutes float64 `json:"avg_resolution_time_minutes"` } diff --git a/internal/http/handlers/health_handler_test.go b/internal/http/handlers/health_handler_test.go index 3acca16..07cb159 100644 --- a/internal/http/handlers/health_handler_test.go +++ b/internal/http/handlers/health_handler_test.go @@ -106,13 +106,13 @@ func TestHealthHandler_Health_ReturnsOK(t *testing.T) { func TestTicketStatsHandler_Get_Success(t *testing.T) { mock := &mockTicketStatsServiceForStats{ stats: ticketstats.Stats{ - Total: 100, - Open: 30, - Resolved: 50, - Closed: 20, - ByChannel: map[string]int{"api": 40, "web": 60}, - ByPriority: map[string]int{"P1": 10, "P2": 60, "P3": 30}, - HandoffCount: 15, + Total: 100, + Open: 30, + Resolved: 50, + Closed: 20, + ByChannel: map[string]int{"api": 40, "web": 60}, + ByPriority: map[string]int{"P1": 10, "P2": 60, "P3": 30}, + HandoffCount: 15, AvgResolutionTimeMinutes: 45.5, }, err: nil, diff --git a/internal/http/handlers/webhook_handler_test.go b/internal/http/handlers/webhook_handler_test.go index 3b76a48..5dcf6be 100644 --- a/internal/http/handlers/webhook_handler_test.go +++ b/internal/http/handlers/webhook_handler_test.go @@ -173,4 +173,4 @@ func TestClientIP_NoPort(t *testing.T) { if ip != "192.168.1.100" { t.Errorf("clientIP() = %s, want 192.168.1.100", ip) } -} \ No newline at end of file +} diff --git a/internal/http/handlers/webhook_security_test.go b/internal/http/handlers/webhook_security_test.go index a9ad57f..56a656b 100644 --- a/internal/http/handlers/webhook_security_test.go +++ b/internal/http/handlers/webhook_security_test.go @@ -7,7 +7,6 @@ import ( "strconv" "testing" "time" - ) // TestWebhookSecurity_InvalidTimestampFormat covers CS_AUTH_4032: diff --git a/internal/platform/httpx/limits_test.go b/internal/platform/httpx/limits_test.go index e936e1c..91b6b33 100644 --- a/internal/platform/httpx/limits_test.go +++ b/internal/platform/httpx/limits_test.go @@ -143,4 +143,4 @@ func TestRateLimiter_WithRateLimit_XForwardedFor(t *testing.T) { if rec.Code != http.StatusOK { t.Errorf("different IP: expected 200, got %d", rec.Code) } -} \ No newline at end of file +} diff --git a/internal/service/dialog/service_test.go b/internal/service/dialog/service_test.go index 4799f5e..f9739f1 100644 --- a/internal/service/dialog/service_test.go +++ b/internal/service/dialog/service_test.go @@ -7,10 +7,10 @@ import ( "time" "github.com/bridge/ai-customer-service/internal/domain/audit" + intentdomain "github.com/bridge/ai-customer-service/internal/domain/intent" "github.com/bridge/ai-customer-service/internal/domain/message" "github.com/bridge/ai-customer-service/internal/domain/session" "github.com/bridge/ai-customer-service/internal/domain/ticket" - intentdomain "github.com/bridge/ai-customer-service/internal/domain/intent" "github.com/bridge/ai-customer-service/internal/service/handoff" intentservice "github.com/bridge/ai-customer-service/internal/service/intent" "github.com/bridge/ai-customer-service/internal/service/reply" diff --git a/internal/service/reply/service_test.go b/internal/service/reply/service_test.go index 9a5c167..026bf38 100644 --- a/internal/service/reply/service_test.go +++ b/internal/service/reply/service_test.go @@ -160,4 +160,4 @@ func TestGenerate_ContextCancellation(t *testing.T) { if result == "" { t.Error("Generate with cancelled context should still return answer") } -} \ No newline at end of file +} diff --git a/test/integration/dialog_service_test.go b/test/integration/dialog_service_test.go index 674e1b2..d4eb2e4 100644 --- a/test/integration/dialog_service_test.go +++ b/test/integration/dialog_service_test.go @@ -26,12 +26,12 @@ func TestDialogService_AC02_IntentMatrix(t *testing.T) { svc := dialog.NewService(sessions, audits, tickets, dedup, intentservice.NewService(), reply.NewService(knowledge), handoff.NewService()) tests := []struct { - name string - content string - wantIntent string - wantHandoff bool - wantPriority string // empty if no handoff expected - wantReply bool // whether to check reply is non-empty + name string + content string + wantIntent string + wantHandoff bool + wantPriority string // empty if no handoff expected + wantReply bool // whether to check reply is non-empty }{ { name: "AC-02: 退款意图 → P1 handoff", @@ -58,11 +58,11 @@ func TestDialogService_AC02_IntentMatrix(t *testing.T) { wantReply: true, }, { - name: "AC-02: 正常查询 → bot 回复无 handoff", - content: "查询额度", - wantIntent: "quota", - wantHandoff: false, - wantReply: true, + name: "AC-02: 正常查询 → bot 回复无 handoff", + content: "查询额度", + wantIntent: "quota", + wantHandoff: false, + wantReply: true, }, } @@ -70,9 +70,9 @@ func TestDialogService_AC02_IntentMatrix(t *testing.T) { t.Run(tc.name, func(t *testing.T) { result, err := svc.Process(context.Background(), &message.UnifiedMessage{ MessageID: "m_" + tc.name, - Channel: "widget", - OpenID: "u_" + tc.name, - Content: tc.content, + Channel: "widget", + OpenID: "u_" + tc.name, + Content: tc.content, }) if err != nil { t.Fatalf("Process() error = %v", err) diff --git a/test/integration/ticket_stats_handler_test.go b/test/integration/ticket_stats_handler_test.go index 285f039..6f40a77 100644 --- a/test/integration/ticket_stats_handler_test.go +++ b/test/integration/ticket_stats_handler_test.go @@ -41,27 +41,27 @@ func setupTicketStatsHandler(stats ticketstats.Stats) (*httptest.ResponseRecorde // ticketStatsResponse mirrors the JSON shape of ticketstats.Stats. type ticketStatsResponse struct { - Total int `json:"total_tickets"` - Open int `json:"open"` - Resolved int `json:"resolved"` - Closed int `json:"closed"` - ByChannel map[string]int `json:"by_channel"` - ByPriority map[string]int `json:"by_priority"` - HandoffCount int `json:"handoff_count"` - AvgResolutionTimeMinutes float64 `json:"avg_resolution_time_minutes"` + Total int `json:"total_tickets"` + Open int `json:"open"` + Resolved int `json:"resolved"` + Closed int `json:"closed"` + ByChannel map[string]int `json:"by_channel"` + ByPriority map[string]int `json:"by_priority"` + HandoffCount int `json:"handoff_count"` + AvgResolutionTimeMinutes float64 `json:"avg_resolution_time_minutes"` } // TestTicketStats_Success verifies the stats endpoint returns correct // counts when the store has tickets. func TestTicketStats_Success(t *testing.T) { stats := ticketstats.Stats{ - Total: 100, - Open: 30, - Resolved: 50, - Closed: 20, - ByChannel: map[string]int{"api": 40, "web": 60}, - ByPriority: map[string]int{"P1": 10, "P2": 60, "P3": 30}, - HandoffCount: 15, + Total: 100, + Open: 30, + Resolved: 50, + Closed: 20, + ByChannel: map[string]int{"api": 40, "web": 60}, + ByPriority: map[string]int{"P1": 10, "P2": 60, "P3": 30}, + HandoffCount: 15, AvgResolutionTimeMinutes: 45.5, } @@ -114,13 +114,13 @@ func TestTicketStats_Success(t *testing.T) { // TestTicketStats_Empty verifies that an empty store returns all-zero stats. func TestTicketStats_Empty(t *testing.T) { stats := ticketstats.Stats{ - Total: 0, - Open: 0, - Resolved: 0, - Closed: 0, - ByChannel: map[string]int{}, - ByPriority: map[string]int{}, - HandoffCount: 0, + Total: 0, + Open: 0, + Resolved: 0, + Closed: 0, + ByChannel: map[string]int{}, + ByPriority: map[string]int{}, + HandoffCount: 0, AvgResolutionTimeMinutes: 0, } @@ -163,14 +163,14 @@ func TestTicketStats_GroupedCounts(t *testing.T) { Resolved: 10, Closed: 5, ByChannel: map[string]int{ - "api": 8, - "web": 12, + "api": 8, + "web": 12, "wechat": 5, }, ByPriority: map[string]int{ - "P1": 3, + "P1": 3, "P2": 15, - "P3": 7, + "P3": 7, }, HandoffCount: 6, AvgResolutionTimeMinutes: 120.0,