package business import ( "bytes" "context" "testing" "time" "github.com/xuri/excelize/v2" "kra/internal/biz/business" "kra/internal/service/dto" ) type performanceServiceFake struct { data *business.PerformancePage calls int filter business.PerformanceFilter } func (f *performanceServiceFake) Query(_ context.Context, filter business.PerformanceFilter) (*business.PerformancePage, error) { f.calls++ f.filter = filter return f.data, nil } func (f *performanceServiceFake) Details(context.Context, string, uint64, time.Time, time.Time, string, int, int, ...uint) ([]*business.PerformanceChannel, int64, error) { f.calls++ return nil, 0, nil } type goalServiceFake struct { goal *business.PerformanceGoal calls int } type memberProfileFake struct { TeamUsecase profile *business.MemberProfile } func (f *memberProfileFake) UpdateProfile(_ context.Context, value business.MemberProfile) error { f.profile = &value return nil } func TestMemberProfileValidatesPhoneBeforeDelegating(t *testing.T) { f := &memberProfileFake{} s := &BusinessPerformanceService{teams: f} req := &dto.MemberProfileRequest{UserID: 9, RealName: "成员", Phone: "abc", HireDate: "2026-11-01", Employment: "employed"} if err := s.UpdateProfile(context.Background(), req); err != business.ErrMemberInput || f.profile != nil { t.Fatal("invalid phone reached usecase") } for _, phone := range []string{"", "138 0013 8000", "010-12345678"} { req.Phone = phone if err := s.UpdateProfile(context.Background(), req); err != nil || f.profile.Phone != phone { t.Fatalf("valid phone rejected: %q %v", phone, err) } } } func (f *goalServiceFake) Save(_ context.Context, g *business.PerformanceGoal) error { f.calls++ f.goal = g return nil } func (f *goalServiceFake) History(context.Context, string, uint64) (*business.GoalPage, error) { return &business.GoalPage{}, nil } func (f *goalServiceFake) Mine(context.Context) (*business.GoalMonth, error) { return nil, nil } func TestPerformanceServiceValidatesDatesAndPreservesNullableGoals(t *testing.T) { f := &performanceServiceFake{data: &business.PerformancePage{Available: true}} g := &goalServiceFake{} s := &BusinessPerformanceService{performance: f, goals: g} one, bad := 1, 13 for _, req := range []*dto.PerformanceQuery{nil, {BeginMonth: &one}, {BeginMonth: &one, EndMonth: &bad}, {Level: &bad}} { if _, err := s.Query(context.Background(), req, false); err != business.ErrPerformanceInput { t.Fatalf("无效查询应在进入用例前失败: %v", err) } } if f.calls != 0 { t.Fatal("无效查询访问了用例") } zero := int64(0) negative := int64(-1) if err := s.SaveGoal(context.Background(), &dto.GoalRequest{Kind: "person", TargetID: 9, Month: "2026-11", Open: &negative}); err != business.ErrGoalInput || g.calls != 0 { t.Fatal("负数目标不应进入用例") } if err := s.SaveGoal(context.Background(), &dto.GoalRequest{Kind: "person", TargetID: 9, Month: "2026-11", Open: &zero}); err != nil || g.goal.Open == nil || *g.goal.Open != 0 || g.goal.Amount != nil { t.Fatal("保存时应保留零和留空的差异") } } func TestPerformanceExportsMatchViewsAndKeepPreciseAmounts(t *testing.T) { open, amount := int64(10), int64(3000) periods := []business.PerformancePeriod{{Key: "2026-10", Label: "10月"}, {Key: "2026-11", Label: "11月"}} values := map[string]business.PerformanceMetrics{"2026-10": {Open: 1, Effective: 0, Pay: "10.00"}, "2026-11": {Open: 12, Effective: 2, Pay: "9007199254740993.01"}} member := &business.PerformanceRow{Member: &business.BusinessMember{Account: business.Account{ID: 9, Username: "=1+1"}, GroupID: 1, GroupName: "甲组"}, Total: values["2026-11"], Periods: values} group := &business.PerformanceGroupRow{Group: &business.BusinessGroup{ID: 1, Name: "甲组", MemberIDs: []uint{9}}, Total: values["2026-11"], UniqueTotal: values["2026-11"], Periods: values, UniquePeriods: values, Members: []*business.PerformanceRow{member}, Goals: map[string]*business.PerformanceGoal{"2026-11": {Open: &open, Amount: &amount}}} data := &business.PerformancePage{Available: true, Periods: periods, Rows: []*business.PerformanceRow{member}, Groups: []*business.PerformanceGroupRow{group}, Trend: values, Total: values["2026-11"], MatrixTotal: values["2026-11"], MatrixTrend: values, Granularity: "month", Prior: business.PerformanceMetrics{Pay: "0.00"}} s := &BusinessPerformanceService{performance: &performanceServiceFake{data: data}} for _, sample := range []struct { view, sheet string rows int }{{"groups", "小组绩效", 2}, {"trend", "趋势图", 3}, {"business", "商务绩效统计", 4}} { blob, err := s.Export(context.Background(), &dto.PerformanceQuery{View: sample.view}) if err != nil { t.Fatal(err) } file, err := excelize.OpenReader(bytes.NewReader(blob)) if err != nil { t.Fatal(err) } rows, err := file.GetRows(sample.sheet) if err != nil || len(rows) != sample.rows { t.Fatalf("视图导出行数不符或包含了展开成员: %s rows=%d %v", sample.view, len(rows), err) } if sample.view == "groups" { value, _ := file.GetCellValue(sample.sheet, "N2", excelize.Options{RawCellValue: true}) if value != "9007199254740993.01" { t.Fatalf("金额不能经过浮点转换: %s", value) } if rows[1][14] != "—" || rows[1][15] != "—" { t.Fatal("未设目标月份必须目标/实际分格占位") } } if sample.view == "business" { formula, _ := file.GetCellFormula(sample.sheet, "B2") if formula != "" || rows[1][1] != "=1+1" { t.Fatal("用户名称必须写成文本") } } if sample.view == "trend" && rows[1][0] != "2026-11" { t.Fatal("趋势导出应与页面倒序一致") } file.Close() } }