From 448dc9e337d113e86941d831a712b623a2000a75 Mon Sep 17 00:00:00 2001 From: zwl <1633720889@qq.com> Date: Sun, 12 Jan 2025 15:42:19 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E8=B7=AF=E7=BA=BF=E5=9B=BE?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/roadmap/internal/domain/types.go | 11 +- .../internal/integration/admin_test.go | 516 +++++++++++++++--- .../internal/integration/handler_test.go | 93 ++-- .../internal/integration/startup/wire_gen.go | 2 +- internal/roadmap/internal/repository/admin.go | 163 +++++- .../roadmap/internal/repository/converter.go | 43 +- .../roadmap/internal/repository/dao/admin.go | 130 +++++ .../roadmap/internal/repository/dao/dao.go | 33 +- .../roadmap/internal/repository/dao/init.go | 2 +- .../roadmap/internal/repository/dao/types.go | 40 ++ .../roadmap/internal/repository/repository.go | 4 +- internal/roadmap/internal/service/admin.go | 31 +- internal/roadmap/internal/service/biz.go | 126 ----- internal/roadmap/internal/service/biz/biz.go | 68 +++ .../roadmap/internal/service/biz/question.go | 34 ++ .../internal/service/biz/question_set.go | 33 ++ internal/roadmap/internal/service/biz/type.go | 18 + internal/roadmap/internal/web/admin.go | 58 +- internal/roadmap/internal/web/handler.go | 5 +- internal/roadmap/internal/web/vo.go | 33 +- internal/roadmap/wire.go | 12 +- internal/roadmap/wire_gen.go | 10 +- 22 files changed, 1181 insertions(+), 284 deletions(-) delete mode 100644 internal/roadmap/internal/service/biz.go create mode 100644 internal/roadmap/internal/service/biz/biz.go create mode 100644 internal/roadmap/internal/service/biz/question.go create mode 100644 internal/roadmap/internal/service/biz/question_set.go create mode 100644 internal/roadmap/internal/service/biz/type.go diff --git a/internal/roadmap/internal/domain/types.go b/internal/roadmap/internal/domain/types.go index 4f21686b..398b6bb5 100644 --- a/internal/roadmap/internal/domain/types.go +++ b/internal/roadmap/internal/domain/types.go @@ -40,13 +40,18 @@ func (r Roadmap) Bizs() ([]string, []int64) { } type Node struct { + ID int64 Biz + Rid int64 + Attrs string } type Edge struct { - Id int64 - Src Node - Dst Node + Id int64 + Type string + Attrs string + Src Node + Dst Node } type Biz struct { diff --git a/internal/roadmap/internal/integration/admin_test.go b/internal/roadmap/internal/integration/admin_test.go index c193901c..a291155a 100644 --- a/internal/roadmap/internal/integration/admin_test.go +++ b/internal/roadmap/internal/integration/admin_test.go @@ -20,6 +20,7 @@ import ( "context" "fmt" "net/http" + "net/http/httptest" "testing" "time" @@ -100,6 +101,10 @@ func (s *AdminHandlerTestSuite) TearDownTest() { require.NoError(s.T(), err) err = s.db.Exec("TRUNCATE TABLE roadmap_edges").Error require.NoError(s.T(), err) + err = s.db.Exec("TRUNCATE TABLE roadmap_nodes").Error + require.NoError(s.T(), err) + err = s.db.Exec("TRUNCATE TABLE roadmap_edges_v1").Error + require.NoError(s.T(), err) } func (s *AdminHandlerTestSuite) TestSave() { @@ -299,72 +304,98 @@ func (s *AdminHandlerTestSuite) TestList() { } func (s *AdminHandlerTestSuite) TestDetail() { - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - defer cancel() - db := s.db.WithContext(ctx) - // 插入数据的 - err := db.Create(&dao.Roadmap{ - Id: 1, - Title: "标题1", - Biz: sqlx.NewNullString(domain.BizQuestion), - BizId: sqlx.NewNullInt64(123), - Ctime: 222, - Utime: 222, - }).Error - require.NoError(s.T(), err) - edges := []dao.Edge{ - {Id: 1, Rid: 1, SrcBiz: domain.BizQuestionSet, SrcId: 1, DstBiz: domain.BizQuestion, DstId: 2, Utime: 123}, - {Id: 2, Rid: 1, SrcBiz: domain.BizQuestion, SrcId: 2, DstBiz: domain.BizQuestionSet, DstId: 3, Utime: 124}, - {Id: 3, Rid: 2, SrcBiz: domain.BizQuestion, SrcId: 2, DstBiz: domain.BizQuestionSet, DstId: 3, Utime: 125}, - } - err = db.Create(&edges).Error - require.NoError(s.T(), err) - testCases := []struct { - name string - + name string + before func(t *testing.T) + after func(t *testing.T) req web.IdReq wantCode int wantResp test.Result[web.Roadmap] }{ { - name: "获取成功", + name: "获取成功", + before: func(t *testing.T) { + // 创建roadmap + err := s.db.Create(&dao.Roadmap{ + Id: 1, + Title: "Roadmap 1", + Biz: sqlx.NewNullString("question"), + BizId: sqlx.NewNullInt64(123), + }).Error + require.NoError(t, err) + + // 创建三个节点 + nodes := []dao.Node{ + {Id: 1, Biz: "question", Rid: 1, RefId: 123, Attrs: "attributes1"}, + {Id: 2, Biz: "questionSet", Rid: 1, RefId: 456, Attrs: "attributes2"}, + {Id: 3, Biz: "questionSet", Rid: 1, RefId: 789, Attrs: "attributes3"}, + } + err = s.db.Create(&nodes).Error + require.NoError(t, err) + + // 创建三条边 + edges := []dao.EdgeV1{ + {Id: 1, Rid: 1, SrcNode: 1, DstNode: 3, Type: "default", Attrs: "edge attributes 1"}, + {Id: 2, Rid: 1, SrcNode: 3, DstNode: 2, Type: "default", Attrs: "edge attributes 2"}, + {Id: 3, Rid: 2, SrcNode: 3, DstNode: 2, Type: "default", Attrs: "edge attributes 3"}, + } + err = s.db.Create(&edges).Error + require.NoError(t, err) + }, + after: func(t *testing.T) { + // 清理数据库或其他后置操作 + }, req: web.IdReq{Id: 1}, wantCode: 200, wantResp: test.Result[web.Roadmap]{ Data: web.Roadmap{ Id: 1, - Title: "标题1", - Biz: domain.BizQuestion, + Title: "Roadmap 1", + Biz: "question", BizId: 123, BizTitle: "题目123", - Utime: 222, Edges: []web.Edge{ { Id: 2, Src: web.Node{ - BizId: 2, - Biz: domain.BizQuestion, - Title: "题目2", + ID: 3, + Biz: "questionSet", + BizId: 789, + Rid: 1, + Attrs: "attributes3", + Title: "题集789", }, Dst: web.Node{ - BizId: 3, - Biz: domain.BizQuestionSet, - Title: "题集3", + ID: 2, + Biz: "questionSet", + BizId: 456, + Rid: 1, + Attrs: "attributes2", + Title: "题集456", }, + Type: "default", + Attrs: "edge attributes 2", }, { Id: 1, Src: web.Node{ - Biz: domain.BizQuestionSet, - BizId: 1, - Title: "题集1", + ID: 1, + Biz: "question", + BizId: 123, + Rid: 1, + Attrs: "attributes1", + Title: "题目123", }, Dst: web.Node{ - Biz: domain.BizQuestion, - BizId: 2, - Title: "题目2", + ID: 3, + Biz: "questionSet", + BizId: 789, + Rid: 1, + Attrs: "attributes3", + Title: "题集789", }, + Type: "default", + Attrs: "edge attributes 1", }, }, }, @@ -374,6 +405,7 @@ func (s *AdminHandlerTestSuite) TestDetail() { for _, tc := range testCases { s.T().Run(tc.name, func(t *testing.T) { + tc.before(t) req, err := http.NewRequest(http.MethodPost, "/roadmap/detail", iox.NewJSONReader(tc.req)) req.Header.Set("content-type", "application/json") @@ -382,58 +414,120 @@ func (s *AdminHandlerTestSuite) TestDetail() { s.server.ServeHTTP(recorder, req) require.Equal(t, tc.wantCode, recorder.Code) assert.Equal(t, tc.wantResp, recorder.MustScan()) + tc.after(t) }) } } -func (s *AdminHandlerTestSuite) TestAddEdge() { +func (s *AdminHandlerTestSuite) TestSaveEdge() { testCases := []struct { - name string - before func(t *testing.T) - after func(t *testing.T) - + name string + before func(t *testing.T) + after func(t *testing.T) req web.AddEdgeReq wantCode int wantResp test.Result[any] }{ { - name: "添加成功", + name: "新增边成功", before: func(t *testing.T) { - + // 创建三个节点 + nodes := []dao.Node{ + {Id: 1, Biz: "question", Rid: 1, RefId: 123, Attrs: "attributes1"}, + {Id: 2, Biz: "case", Rid: 1, RefId: 456, Attrs: "attributes2"}, + {Id: 3, Biz: "common", Rid: 0, RefId: 789, Attrs: "attributes3"}, + } + err := s.db.Create(&nodes).Error + require.NoError(t, err) }, after: func(t *testing.T) { - var edge dao.Edge + // 验证边已被添加 + var edge dao.EdgeV1 ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() - err := s.db.WithContext(ctx).Where("rid = ?", 1).First(&edge).Error + err := s.db.WithContext(ctx).Where("src_node = ? AND dst_node = ?", 1, 2).First(&edge).Error require.NoError(t, err) - assert.True(t, edge.Ctime > 0) + require.True(t, edge.Ctime != 0) + require.True(t, edge.Utime != 0) edge.Ctime = 0 - assert.True(t, edge.Utime > 0) edge.Utime = 0 - assert.Equal(t, dao.Edge{ - Id: 1, - Rid: 1, - SrcBiz: domain.BizQuestion, - SrcId: 123, - DstBiz: domain.BizQuestionSet, - DstId: 234, + assert.Equal(t, dao.EdgeV1{ + Id: 1, + SrcNode: 1, + DstNode: 2, + Rid: 1, + Type: "default", + Attrs: "attrs", }, edge) }, req: web.AddEdgeReq{ Rid: 1, Edge: web.Edge{ - Src: web.Node{ - Biz: domain.BizQuestion, - BizId: 123, - }, - Dst: web.Node{ - Biz: domain.BizQuestionSet, - BizId: 234, - }, + Id: 1, + Src: web.Node{ID: 1}, + Dst: web.Node{ID: 2}, + Type: "default", + Attrs: "attrs", + }, + }, + wantCode: 200, + wantResp: test.Result[any]{}, + }, + { + name: "编辑边成功", + before: func(t *testing.T) { + // 创建一个边 + nodes := []dao.Node{ + {Id: 1, Biz: "question", Rid: 1, RefId: 123, Attrs: "attributes1"}, + {Id: 2, Biz: "case", Rid: 1, RefId: 456, Attrs: "attributes2"}, + {Id: 3, Biz: "common", Rid: 0, RefId: 789, Attrs: "attributes3"}, + } + err := s.db.Create(&nodes).Error + require.NoError(t, err) + err = s.db.Create(&dao.EdgeV1{ + Id: 1, + SrcNode: 1, + DstNode: 2, + Rid: 1, + Type: "default", + Attrs: "attrs", + Ctime: 123, + Utime: 321, + }).Error + require.NoError(t, err) + }, + after: func(t *testing.T) { + // 验证边已被编辑 + var edge dao.EdgeV1 + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + err := s.db.WithContext(ctx).Where("id = ?", 1).First(&edge).Error + require.NoError(t, err) + require.True(t, edge.Ctime != 0) + require.True(t, edge.Utime != 0) + edge.Ctime = 0 + edge.Utime = 0 + assert.Equal(t, dao.EdgeV1{ + Id: 1, + SrcNode: 1, + DstNode: 3, // 更新后的目标节点 + Rid: 1, + Type: "updated", + Attrs: "attrsv1", + }, edge) + }, + req: web.AddEdgeReq{ + Rid: 1, + Edge: web.Edge{ + Id: 1, // 指定边的ID以进行编辑 + Src: web.Node{ID: 1}, + Dst: web.Node{ID: 3}, // 更新目标节点 + Type: "updated", + Attrs: "attrsv1", }, }, wantCode: 200, + wantResp: test.Result[any]{}, }, } @@ -449,11 +543,15 @@ func (s *AdminHandlerTestSuite) TestAddEdge() { require.Equal(t, tc.wantCode, recorder.Code) assert.Equal(t, tc.wantResp, recorder.MustScan()) tc.after(t) + err = s.db.Exec("TRUNCATE TABLE roadmap_nodes").Error + require.NoError(s.T(), err) + err = s.db.Exec("TRUNCATE TABLE roadmap_edges_v1").Error + require.NoError(s.T(), err) }) } } -func (s *AdminHandlerTestSuite) TestDelete() { +func (s *AdminHandlerTestSuite) TestDeleteEdge() { testCases := []struct { name string before func(t *testing.T) @@ -468,7 +566,7 @@ func (s *AdminHandlerTestSuite) TestDelete() { before: func(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() - err := s.db.WithContext(ctx).Create(&dao.Edge{ + err := s.db.WithContext(ctx).Create(&dao.EdgeV1{ Id: 1, }).Error require.NoError(t, err) @@ -500,6 +598,290 @@ func (s *AdminHandlerTestSuite) TestDelete() { } } +func (s *AdminHandlerTestSuite) TestSaveNode() { + testCases := []struct { + name string + before func(t *testing.T) + after func(t *testing.T) + req web.Node + wantCode int + wantResp test.Result[int64] + }{ + { + name: "新建节点成功", + before: func(t *testing.T) { + // 可以在这里设置测试前的数据库状态或其他依赖 + }, + after: func(t *testing.T) { + var node dao.Node + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + err := s.db.WithContext(ctx).Where("id = ?", 1).First(&node).Error + require.NoError(t, err) + assert.True(t, node.Ctime > 0) + node.Ctime = 0 + assert.True(t, node.Utime > 0) + node.Utime = 0 + assert.Equal(t, dao.Node{ + Id: 1, + Biz: "question", + Rid: 1, + RefId: 123, + Attrs: "some attributes", + }, node) + }, + req: web.Node{ + Biz: "question", + Rid: 1, + BizId: 123, + Attrs: "some attributes", + }, + wantCode: 200, + wantResp: test.Result[int64]{Data: 1}, + }, + { + name: "更新节点成功", + before: func(t *testing.T) { + err := s.db.Create(&dao.Node{ + Id: 2, + Biz: "question", + Rid: 2, + RefId: 456, + Attrs: "old attributes", + Ctime: 123, + Utime: 123, + }).Error + require.NoError(t, err) + }, + after: func(t *testing.T) { + var node dao.Node + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + err := s.db.WithContext(ctx).Where("id = ?", 2).First(&node).Error + require.NoError(t, err) + assert.True(t, node.Utime > 123) + node.Utime = 0 + assert.Equal(t, dao.Node{ + Id: 2, + Biz: "case", + Rid: 2, + RefId: 789, + Attrs: "new attributes", + Ctime: 123, + }, node) + }, + req: web.Node{ + ID: 2, + Biz: "case", + Rid: 2, + BizId: 789, + Attrs: "new attributes", + }, + wantCode: 200, + wantResp: test.Result[int64]{Data: 2}, + }, + } + + for _, tc := range testCases { + s.T().Run(tc.name, func(t *testing.T) { + tc.before(t) + req, err := http.NewRequest(http.MethodPost, + "/roadmap/node/save", iox.NewJSONReader(tc.req)) + req.Header.Set("content-type", "application/json") + require.NoError(t, err) + recorder := test.NewJSONResponseRecorder[int64]() + s.server.ServeHTTP(recorder, req) + require.Equal(t, tc.wantCode, recorder.Code) + assert.Equal(t, tc.wantResp, recorder.MustScan()) + tc.after(t) + }) + } +} + +func (s *AdminHandlerTestSuite) TestDeleteNode() { + testCases := []struct { + name string + before func(t *testing.T) + after func(t *testing.T) + req web.IdReq + wantCode int + wantResp test.Result[any] + }{ + { + name: "删除节点成功", + before: func(t *testing.T) { + // 预先插入一个节点 + err := s.db.Create(&dao.Node{ + Id: 1, + Biz: "question", + Rid: 1, + RefId: 123, + Attrs: "some attributes", + }).Error + require.NoError(t, err) + }, + after: func(t *testing.T) { + // 验证节点已被删除 + var node dao.Node + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + err := s.db.WithContext(ctx).Where("id = ?", 1).First(&node).Error + assert.Equal(t, gorm.ErrRecordNotFound, err) + }, + req: web.IdReq{Id: 1}, + wantCode: 200, + wantResp: test.Result[any]{}, + }, + } + + for _, tc := range testCases { + s.T().Run(tc.name, func(t *testing.T) { + tc.before(t) + req, err := http.NewRequest(http.MethodPost, + "/roadmap/node/delete", iox.NewJSONReader(tc.req)) + req.Header.Set("content-type", "application/json") + require.NoError(t, err) + recorder := test.NewJSONResponseRecorder[any]() + s.server.ServeHTTP(recorder, req) + require.Equal(t, tc.wantCode, recorder.Code) + assert.Equal(t, tc.wantResp, recorder.MustScan()) + tc.after(t) + }) + } +} + +func (s *AdminHandlerTestSuite) TestNodeList() { + testCases := []struct { + name string + before func(t *testing.T) + after func(t *testing.T) + req web.IdReq + wantCode int + wantResp test.Result[[]web.Node] + }{ + { + name: "获取节点列表成功,包括rid为0和rid为3的节点", + before: func(t *testing.T) { + // 预先插入一些节点,包括rid为0和rid为3的节点 + nodes := []dao.Node{ + {Id: 1, Biz: "question", Rid: 1, RefId: 123, Attrs: "attributes1"}, + {Id: 2, Biz: "case", Rid: 1, RefId: 456, Attrs: "attributes2"}, + {Id: 3, Biz: "common", Rid: 0, RefId: 789, Attrs: "attributes3"}, // rid为0的节点 + {Id: 4, Biz: "special", Rid: 3, RefId: 101, Attrs: "attributes4"}, // rid为3的节点 + } + err := s.db.Create(&nodes).Error + require.NoError(t, err) + }, + after: func(t *testing.T) { + // 清理数据库或其他后置操作 + }, + req: web.IdReq{Id: 1}, + wantCode: 200, + wantResp: test.Result[[]web.Node]{ + Data: []web.Node{ + {ID: 3, Biz: "common", Rid: 0, BizId: 789, Attrs: "attributes3"}, + {ID: 2, Biz: "case", Rid: 1, BizId: 456, Attrs: "attributes2"}, + {ID: 1, Biz: "question", Rid: 1, BizId: 123, Attrs: "attributes1"}, + }, + }, + }, + } + + for _, tc := range testCases { + s.T().Run(tc.name, func(t *testing.T) { + tc.before(t) + req, err := http.NewRequest(http.MethodPost, + "/roadmap/node/list", iox.NewJSONReader(tc.req)) + req.Header.Set("content-type", "application/json") + require.NoError(t, err) + recorder := test.NewJSONResponseRecorder[[]web.Node]() + s.server.ServeHTTP(recorder, req) + require.Equal(t, tc.wantCode, recorder.Code) + assert.Equal(t, tc.wantResp, recorder.MustScan()) + tc.after(t) + }) + } +} + +func (s *AdminHandlerTestSuite) TestSanitize() { + roadmaps := []dao.Roadmap{ + {Id: 1, Title: "Roadmap 1", Biz: sqlx.NewNullString("biz1"), BizId: sqlx.NewNullInt64(101)}, + {Id: 2, Title: "Roadmap 2", Biz: sqlx.NewNullString("biz2"), BizId: sqlx.NewNullInt64(102)}, + {Id: 3, Title: "Roadmap 3", Biz: sqlx.NewNullString("biz3"), BizId: sqlx.NewNullInt64(103)}, + } + for _, roadmap := range roadmaps { + err := s.db.Create(&roadmap).Error + require.NoError(s.T(), err) + } + edges := []dao.Edge{ + {Rid: 1, SrcBiz: "biz1", SrcId: 1, DstBiz: "biz1", DstId: 2}, + {Rid: 1, SrcBiz: "biz1", SrcId: 2, DstBiz: "biz1", DstId: 3}, + {Rid: 1, SrcBiz: "biz1", SrcId: 3, DstBiz: "biz1", DstId: 4}, + {Rid: 1, SrcBiz: "biz1", SrcId: 4, DstBiz: "biz1", DstId: 5}, + {Rid: 2, SrcBiz: "biz2", SrcId: 1, DstBiz: "biz2", DstId: 2}, + {Rid: 2, SrcBiz: "biz2", SrcId: 2, DstBiz: "biz2", DstId: 3}, + {Rid: 2, SrcBiz: "biz2", SrcId: 3, DstBiz: "biz2", DstId: 4}, + {Rid: 2, SrcBiz: "biz2", SrcId: 4, DstBiz: "biz2", DstId: 5}, + {Rid: 3, SrcBiz: "biz3", SrcId: 1, DstBiz: "biz3", DstId: 2}, + {Rid: 3, SrcBiz: "biz3", SrcId: 2, DstBiz: "biz3", DstId: 3}, + {Rid: 3, SrcBiz: "biz3", SrcId: 3, DstBiz: "biz3", DstId: 4}, + {Rid: 3, SrcBiz: "biz3", SrcId: 4, DstBiz: "biz3", DstId: 5}, + } + for _, edge := range edges { + err := s.db.Create(&edge).Error + require.NoError(s.T(), err) + } + req, err := http.NewRequest(http.MethodPost, "/roadmap/sanitize", nil) + require.NoError(s.T(), err) + recorder := httptest.NewRecorder() + s.server.ServeHTTP(recorder, req) + require.Equal(s.T(), http.StatusOK, recorder.Code) + + time.Sleep(10 * time.Second) + s.checkSanitizeData(edges) +} + +func (s *AdminHandlerTestSuite) checkSanitizeData(edge1s []dao.Edge) { + var nodes []dao.Node + err := s.db.WithContext(context.Background()).Model(&dao.Node{}).Find(&nodes).Error + require.NoError(s.T(), err) + nodeMap := s.getNodeMap(nodes) + wantEdgev1s := slice.Map(edge1s, func(idx int, src dao.Edge) dao.EdgeV1 { + return s.getEdgev1(src, nodeMap) + }) + var edgev1s []dao.EdgeV1 + err = s.db.WithContext(context.Background()).Model(&dao.EdgeV1{}).Find(&edgev1s).Error + require.NoError(s.T(), err) + actualEdgev1s := slice.Map(edgev1s, func(idx int, src dao.EdgeV1) dao.EdgeV1 { + require.True(s.T(), src.Ctime != 0) + require.True(s.T(), src.Utime != 0) + require.True(s.T(), src.Id != 0) + src.Ctime = 0 + src.Utime = 0 + src.Id = 0 + return src + }) + assert.ElementsMatch(s.T(), wantEdgev1s, actualEdgev1s) +} + +func (s *AdminHandlerTestSuite) getEdgev1(edge dao.Edge, nodeMap map[string]dao.Node) dao.EdgeV1 { + dstNode := nodeMap[fmt.Sprintf("%d_%s_%d", edge.Rid, edge.DstBiz, edge.DstId)] + srcNode := nodeMap[fmt.Sprintf("%d_%s_%d", edge.Rid, edge.SrcBiz, edge.SrcId)] + return dao.EdgeV1{ + Rid: edge.Rid, + SrcNode: srcNode.Id, + DstNode: dstNode.Id, + } +} + +func (s *AdminHandlerTestSuite) getNodeMap(nodes []dao.Node) map[string]dao.Node { + nodeMap := make(map[string]dao.Node, len(nodes)) + for _, node := range nodes { + nodeMap[fmt.Sprintf("%d_%s_%d", node.Rid, node.Biz, node.RefId)] = node + } + return nodeMap +} + func TestAdminHandler(t *testing.T) { suite.Run(t, new(AdminHandlerTestSuite)) } diff --git a/internal/roadmap/internal/integration/handler_test.go b/internal/roadmap/internal/integration/handler_test.go index 02994a25..908f72b7 100644 --- a/internal/roadmap/internal/integration/handler_test.go +++ b/internal/roadmap/internal/integration/handler_test.go @@ -19,7 +19,6 @@ import ( "fmt" "net/http" "testing" - "time" "github.com/ecodeclub/ekit/iox" "github.com/ecodeclub/ekit/slice" @@ -94,29 +93,42 @@ func (s *HandlerTestSuite) TearDownTest() { require.NoError(s.T(), err) err = s.db.Exec("TRUNCATE TABLE roadmap_edges").Error require.NoError(s.T(), err) + err = s.db.Exec("TRUNCATE TABLE roadmap_nodes").Error + require.NoError(s.T(), err) + err = s.db.Exec("TRUNCATE TABLE roadmap_edges_v1").Error + require.NoError(s.T(), err) } func (s *HandlerTestSuite) TestDetail() { - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - defer cancel() - db := s.db.WithContext(ctx) - // 插入数据的 - err := db.Create(&dao.Roadmap{ + t := s.T() + // 创建roadmap + err := s.db.Create(&dao.Roadmap{ Id: 1, - Title: "标题1", - Biz: sqlx.NewNullString(domain.BizQuestion), + Title: "Roadmap 1", + Biz: sqlx.NewNullString("question"), BizId: sqlx.NewNullInt64(123), Ctime: 222, Utime: 222, }).Error - require.NoError(s.T(), err) - edges := []dao.Edge{ - {Id: 1, Rid: 1, SrcBiz: domain.BizQuestionSet, SrcId: 1, DstBiz: domain.BizQuestion, DstId: 2}, - {Id: 2, Rid: 1, SrcBiz: domain.BizQuestion, SrcId: 2, DstBiz: domain.BizQuestionSet, DstId: 3}, - {Id: 3, Rid: 2, SrcBiz: domain.BizQuestion, SrcId: 2, DstBiz: domain.BizQuestionSet, DstId: 3}, + require.NoError(t, err) + + // 创建三个节点 + nodes := []dao.Node{ + {Id: 1, Biz: "question", Rid: 1, RefId: 123, Attrs: "attributes1"}, + {Id: 2, Biz: "questionSet", Rid: 1, RefId: 456, Attrs: "attributes2"}, + {Id: 3, Biz: "questionSet", Rid: 1, RefId: 789, Attrs: "attributes3"}, } - err = db.Create(&edges).Error - require.NoError(s.T(), err) + err = s.db.Create(&nodes).Error + require.NoError(t, err) + + // 创建三条边 + edges := []dao.EdgeV1{ + {Id: 1, Rid: 1, SrcNode: 1, DstNode: 3, Type: "default", Attrs: "edge attributes 1"}, + {Id: 2, Rid: 1, SrcNode: 3, DstNode: 2, Type: "default", Attrs: "edge attributes 2"}, + {Id: 3, Rid: 2, SrcNode: 3, DstNode: 2, Type: "default", Attrs: "edge attributes 3"}, + } + err = s.db.Create(&edges).Error + require.NoError(t, err) testCases := []struct { name string @@ -132,37 +144,53 @@ func (s *HandlerTestSuite) TestDetail() { wantResp: test.Result[web.Roadmap]{ Data: web.Roadmap{ Id: 1, - Title: "标题1", - Biz: domain.BizQuestion, + Title: "Roadmap 1", + Biz: "question", BizId: 123, BizTitle: "题目123", Utime: 222, Edges: []web.Edge{ { - Id: 1, + Id: 2, Src: web.Node{ - Biz: domain.BizQuestionSet, - BizId: 1, - Title: "题集1", + ID: 3, + Biz: "questionSet", + BizId: 789, + Rid: 1, + Attrs: "attributes3", + Title: "题集789", }, Dst: web.Node{ - Biz: domain.BizQuestion, - BizId: 2, - Title: "题目2", + ID: 2, + Biz: "questionSet", + BizId: 456, + Rid: 1, + Attrs: "attributes2", + Title: "题集456", }, + Type: "default", + Attrs: "edge attributes 2", }, { - Id: 2, + Id: 1, Src: web.Node{ - BizId: 2, - Biz: domain.BizQuestion, - Title: "题目2", + ID: 1, + Biz: "question", + BizId: 123, + Rid: 1, + Attrs: "attributes1", + Title: "题目123", }, Dst: web.Node{ - BizId: 3, - Biz: domain.BizQuestionSet, - Title: "题集3", + ID: 3, + Biz: "questionSet", + BizId: 789, + Rid: 1, + Attrs: "attributes3", + Title: "题集789", }, + Type: "default", + Attrs: "edge attributes 1", }, }, }, @@ -179,7 +207,8 @@ func (s *HandlerTestSuite) TestDetail() { recorder := test.NewJSONResponseRecorder[web.Roadmap]() s.server.ServeHTTP(recorder, req) require.Equal(t, tc.wantCode, recorder.Code) - assert.Equal(t, tc.wantResp, recorder.MustScan()) + data := recorder.MustScan() + assert.Equal(t, tc.wantResp, data) }) } } diff --git a/internal/roadmap/internal/integration/startup/wire_gen.go b/internal/roadmap/internal/integration/startup/wire_gen.go index b71a0239..7ec295c6 100644 --- a/internal/roadmap/internal/integration/startup/wire_gen.go +++ b/internal/roadmap/internal/integration/startup/wire_gen.go @@ -1,6 +1,6 @@ // Code generated by Wire. DO NOT EDIT. -//go:generate go run github.com/google/wire/cmd/wire +//go:generate go run -mod=mod github.com/google/wire/cmd/wire //go:build !wireinject // +build !wireinject diff --git a/internal/roadmap/internal/repository/admin.go b/internal/roadmap/internal/repository/admin.go index 2e7fe3f7..e1b13143 100644 --- a/internal/roadmap/internal/repository/admin.go +++ b/internal/roadmap/internal/repository/admin.go @@ -16,6 +16,10 @@ package repository import ( "context" + "fmt" + "time" + + "github.com/gotomicro/ego/core/elog" "github.com/ecodeclub/ekit/sqlx" @@ -29,8 +33,16 @@ type AdminRepository interface { Save(ctx context.Context, r domain.Roadmap) (int64, error) List(ctx context.Context, offset int, limit int) ([]domain.Roadmap, error) GetById(ctx context.Context, id int64) (domain.Roadmap, error) + AddEdge(ctx context.Context, rid int64, edge domain.Edge) error DeleteEdge(ctx context.Context, id int64) error + SanitizeData() + + SaveNode(ctx context.Context, node domain.Node) (int64, error) + DeleteNode(ctx context.Context, id int64) error + NodeList(ctx context.Context, rid int64) ([]domain.Node, error) + SaveEdgeV1(ctx context.Context, rid int64, edge domain.Edge) error + DeleteEdgeV1(ctx context.Context, id int64) error } var _ AdminRepository = &CachedAdminRepository{} @@ -38,7 +50,132 @@ var _ AdminRepository = &CachedAdminRepository{} // CachedAdminRepository 虽然还没缓存,但是将来肯定要有缓存的 type CachedAdminRepository struct { converter - dao dao.AdminDAO + dao dao.AdminDAO + logger *elog.Component +} + +func (repo *CachedAdminRepository) SanitizeData() { + go repo.sanitizeData() +} + +func (repo *CachedAdminRepository) sanitizeData() { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + roadMaps, err := repo.dao.AllRoadmap(ctx) + cancel() + if err != nil { + repo.logger.Error("获取路线图失败", elog.FieldErr(err)) + } + for _, roadMap := range roadMaps { + // 开始清洗每个路线图 + rctx, rcancel := context.WithTimeout(context.Background(), 100000*time.Second) + err = repo.sanitizeRoadmap(rctx, roadMap.Id) + rcancel() + if err == nil { + repo.logger.Info(fmt.Sprintf("清洗路线图 %d成功", roadMap.Id)) + } else { + repo.logger.Error(fmt.Sprintf("清洗路线图 %d失败", roadMap.Id), elog.FieldErr(err)) + } + } +} + +func (repo *CachedAdminRepository) sanitizeRoadmap(ctx context.Context, rid int64) error { + edges, err := repo.dao.GetEdgesByRid(ctx, rid) + if err != nil { + return err + } + // 获取node + nodeMap := make(map[string]dao.Node, len(edges)*2) + for _, edge := range edges { + dstkey := repo.getkey(edge.DstBiz, edge.DstId) + if _, ok := nodeMap[dstkey]; !ok { + nodeMap[dstkey] = dao.Node{ + Biz: edge.DstBiz, + Rid: rid, + RefId: edge.DstId, + } + } + srckey := repo.getkey(edge.SrcBiz, edge.SrcId) + if _, ok := nodeMap[srckey]; !ok { + nodeMap[srckey] = dao.Node{ + Biz: edge.SrcBiz, + Rid: rid, + RefId: edge.SrcId, + } + } + } + nodes, err := repo.dao.CreateNodes(ctx, repo.getValues(nodeMap)) + if err != nil { + return err + } + nodeMap = make(map[string]dao.Node, len(edges)*2) + for _, node := range nodes { + key := repo.getkey(node.Biz, node.RefId) + nodeMap[key] = node + } + + // 获取edgev1 + edgev1List := make([]dao.EdgeV1, 0, len(edges)) + for _, edge := range edges { + srckey := repo.getkey(edge.SrcBiz, edge.SrcId) + dstkey := repo.getkey(edge.DstBiz, edge.DstId) + srcNode := nodeMap[srckey] + dstNode := nodeMap[dstkey] + edgev1List = append(edgev1List, dao.EdgeV1{ + Rid: rid, + SrcNode: srcNode.Id, + DstNode: dstNode.Id, + }) + } + return repo.dao.CreateEdgeV1s(ctx, edgev1List) +} +func (repo *CachedAdminRepository) getkey(biz string, id int64) string { + return fmt.Sprintf("%s_%d", biz, id) +} + +func (repo *CachedAdminRepository) getValues(nodeMap map[string]dao.Node) []dao.Node { + nodes := make([]dao.Node, 0, len(nodeMap)) + for _, v := range nodeMap { + nodes = append(nodes, v) + } + return nodes +} + +func (repo *CachedAdminRepository) SaveNode(ctx context.Context, node domain.Node) (int64, error) { + return repo.dao.SaveNode(ctx, repo.toEntityNode(node)) +} + +func (repo *CachedAdminRepository) DeleteNode(ctx context.Context, id int64) error { + return repo.dao.DeleteNode(ctx, id) +} + +func (repo *CachedAdminRepository) NodeList(ctx context.Context, rid int64) ([]domain.Node, error) { + nodes, err := repo.dao.NodeList(ctx, rid) + if err != nil { + return nil, err + } + return slice.Map(nodes, func(idx int, src dao.Node) domain.Node { + return domain.Node{ + ID: src.Id, + Biz: domain.Biz{Biz: src.Biz, BizId: src.RefId}, + Rid: src.Rid, + Attrs: src.Attrs, + } + }), nil +} + +func (repo *CachedAdminRepository) SaveEdgeV1(ctx context.Context, rid int64, edge domain.Edge) error { + return repo.dao.SaveEdgeV1(ctx, dao.EdgeV1{ + Id: edge.Id, + Rid: rid, + SrcNode: edge.Src.ID, + DstNode: edge.Dst.ID, + Type: edge.Type, + Attrs: edge.Attrs, + }) +} + +func (repo *CachedAdminRepository) DeleteEdgeV1(ctx context.Context, id int64) error { + return repo.dao.DeleteEdge(ctx, id) } func (repo *CachedAdminRepository) DeleteEdge(ctx context.Context, id int64) error { @@ -57,9 +194,10 @@ func (repo *CachedAdminRepository) AddEdge(ctx context.Context, rid int64, edge func (repo *CachedAdminRepository) GetById(ctx context.Context, id int64) (domain.Roadmap, error) { var ( - eg errgroup.Group - r dao.Roadmap - edges []dao.Edge + eg errgroup.Group + r dao.Roadmap + edges []dao.EdgeV1 + nodeMap map[int64]dao.Node ) eg.Go(func() error { var err error @@ -69,7 +207,7 @@ func (repo *CachedAdminRepository) GetById(ctx context.Context, id int64) (domai eg.Go(func() error { var err error - edges, err = repo.dao.GetEdgesByRid(ctx, id) + nodeMap, edges, err = repo.dao.GetEdgesByRidV1(ctx, id) return err }) err := eg.Wait() @@ -77,7 +215,7 @@ func (repo *CachedAdminRepository) GetById(ctx context.Context, id int64) (domai return domain.Roadmap{}, err } res := repo.toDomain(r) - res.Edges = repo.edgesToDomain(edges) + res.Edges = repo.edgesToDomain(edges, nodeMap) return res, nil } @@ -101,8 +239,19 @@ func (repo *CachedAdminRepository) toEntity(r domain.Roadmap) dao.Roadmap { } } +func (repo *CachedAdminRepository) toEntityNode(node domain.Node) dao.Node { + return dao.Node{ + Id: node.ID, + Biz: node.Biz.Biz, + Rid: node.Rid, + RefId: node.Biz.BizId, + Attrs: node.Attrs, + } +} + func NewCachedAdminRepository(dao dao.AdminDAO) AdminRepository { return &CachedAdminRepository{ - dao: dao, + dao: dao, + logger: elog.DefaultLogger, } } diff --git a/internal/roadmap/internal/repository/converter.go b/internal/roadmap/internal/repository/converter.go index 702fb331..e118fe5e 100644 --- a/internal/roadmap/internal/repository/converter.go +++ b/internal/roadmap/internal/repository/converter.go @@ -34,22 +34,35 @@ func (converter) toDomain(r dao.Roadmap) domain.Roadmap { } } -func (converter) edgesToDomain(edges []dao.Edge) []domain.Edge { - return slice.Map(edges, func(idx int, edge dao.Edge) domain.Edge { +func (c converter) edgesToDomain(edges []dao.EdgeV1, nodeMap map[int64]dao.Node) []domain.Edge { + return slice.Map(edges, func(idx int, edge dao.EdgeV1) domain.Edge { + var srcNode, dstNode domain.Node + daoSrcNode, ok := nodeMap[edge.SrcNode] + if ok { + srcNode = c.nodeToDomain(daoSrcNode) + } + daoDstNode, ok := nodeMap[edge.DstNode] + if ok { + dstNode = c.nodeToDomain(daoDstNode) + } return domain.Edge{ - Id: edge.Id, - Src: domain.Node{ - Biz: domain.Biz{ - BizId: edge.SrcId, - Biz: edge.SrcBiz, - }, - }, - Dst: domain.Node{ - Biz: domain.Biz{ - BizId: edge.DstId, - Biz: edge.DstBiz, - }, - }, + Id: edge.Id, + Type: edge.Type, + Attrs: edge.Attrs, + Src: srcNode, + Dst: dstNode, } }) } + +func (converter) nodeToDomain(daoNode dao.Node) domain.Node { + return domain.Node{ + ID: daoNode.Id, + Rid: daoNode.Rid, + Attrs: daoNode.Attrs, + Biz: domain.Biz{ + Biz: daoNode.Biz, + BizId: daoNode.RefId, + }, + } +} diff --git a/internal/roadmap/internal/repository/dao/admin.go b/internal/roadmap/internal/repository/dao/admin.go index aa5347e0..0021849b 100644 --- a/internal/roadmap/internal/repository/dao/admin.go +++ b/internal/roadmap/internal/repository/dao/admin.go @@ -23,12 +23,28 @@ import ( ) type AdminDAO interface { + // 路线图的相关方法 Save(ctx context.Context, r Roadmap) (int64, error) GetById(ctx context.Context, id int64) (Roadmap, error) List(ctx context.Context, offset int, limit int) ([]Roadmap, error) + AllRoadmap(ctx context.Context) ([]Roadmap, error) + + // 旧版本边的操作 GetEdgesByRid(ctx context.Context, rid int64) ([]Edge, error) AddEdge(ctx context.Context, edge Edge) error DeleteEdge(ctx context.Context, id int64) error + + // 新版本节点的操作 + SaveNode(ctx context.Context, node Node) (int64, error) + DeleteNode(ctx context.Context, id int64) error + NodeList(ctx context.Context, rid int64) ([]Node, error) + CreateNodes(ctx context.Context, nodes []Node) ([]Node, error) + + // 新版本边的操作 + GetEdgesByRidV1(ctx context.Context, rid int64) (map[int64]Node, []EdgeV1, error) + CreateEdgeV1s(ctx context.Context, edgev1List []EdgeV1) error + SaveEdgeV1(ctx context.Context, edge EdgeV1) error + DeleteEdgeV1(ctx context.Context, id int64) error } var _ AdminDAO = &GORMAdminDAO{} @@ -37,6 +53,120 @@ type GORMAdminDAO struct { db *egorm.Component } +func (dao *GORMAdminDAO) AllRoadmap(ctx context.Context) ([]Roadmap, error) { + var res []Roadmap + err := dao.db.WithContext(ctx).Order("id DESC").Find(&res).Error + return res, err +} + +func (dao *GORMAdminDAO) CreateNodes(ctx context.Context, nodes []Node) ([]Node, error) { + now := time.Now().UnixMilli() + for idx := range nodes { + nodes[idx].Ctime = now + nodes[idx].Utime = now + } + err := dao.db.WithContext(ctx).Create(&nodes).Error + return nodes, err +} + +func (dao *GORMAdminDAO) CreateEdgeV1s(ctx context.Context, edgev1List []EdgeV1) error { + now := time.Now().UnixMilli() + for idx := range edgev1List { + edgev1List[idx].Ctime = now + edgev1List[idx].Utime = now + } + return dao.db.WithContext(ctx).Create(&edgev1List).Error +} + +func (dao *GORMAdminDAO) GetEdgesByRidV1(ctx context.Context, rid int64) (map[int64]Node, []EdgeV1, error) { + var edges []EdgeV1 + err := dao.db.WithContext(ctx).Where("rid = ?", rid). + Order("id desc"). + Find(&edges).Error + if err != nil { + return nil, nil, err + } + + nodeIds := make(map[int64]struct{}) + for _, edge := range edges { + nodeIds[edge.SrcNode] = struct{}{} + nodeIds[edge.DstNode] = struct{}{} + } + + var nodes []Node + err = dao.db.WithContext(ctx).Where("id IN ?", keys(nodeIds)).Find(&nodes).Error + if err != nil { + return nil, nil, err + } + nodeMap := make(map[int64]Node, len(nodes)) + for _, node := range nodes { + nodeMap[node.Id] = node + } + return nodeMap, edges, nil +} + +func keys(m map[int64]struct{}) []int64 { + ks := make([]int64, 0, len(m)) + for k := range m { + ks = append(ks, k) + } + return ks +} + +func (dao *GORMAdminDAO) SaveNode(ctx context.Context, node Node) (int64, error) { + now := time.Now().UnixMilli() + node.Utime = now + node.Ctime = now + err := dao.db.WithContext(ctx). + Clauses( + clause.OnConflict{ + DoUpdates: clause.AssignmentColumns([]string{"biz", "rid", "ref_id", "attrs", "utime"}), + }, + ). + Create(&node).Error + return node.Id, err +} + +func (dao *GORMAdminDAO) DeleteNode(ctx context.Context, id int64) error { + return dao.db. + WithContext(ctx). + Where("id = ?", id).Delete(&Node{}).Error +} + +// NodeList 获取本路线图的节点,和公共的节点 +func (dao *GORMAdminDAO) NodeList(ctx context.Context, rid int64) ([]Node, error) { + var nodes []Node + err := dao.db.WithContext(ctx). + // 获取当前路线图的节点,或者通用节点 + Where("rid = ? or rid = 0", rid). + Order("id desc"). + Find(&nodes).Error + return nodes, err +} + +func (dao *GORMAdminDAO) SaveEdgeV1(ctx context.Context, edge EdgeV1) error { + now := time.Now().UnixMilli() + edge.Utime = now + edge.Ctime = now + return dao.db.WithContext(ctx). + Clauses( + clause.OnConflict{ + DoUpdates: clause.AssignmentColumns([]string{ + "src_node", + "dst_node", + "type", + "attrs", + "utime", + }), + }, + ). + Create(&edge).Error +} + +func (dao *GORMAdminDAO) DeleteEdgeV1(ctx context.Context, id int64) error { + return dao.db.WithContext(ctx).Where("id = ?", id).Delete(&EdgeV1{}).Error +} + func (dao *GORMAdminDAO) DeleteEdge(ctx context.Context, id int64) error { return dao.db.WithContext(ctx).Where("id = ?", id).Delete(&Edge{}).Error } diff --git a/internal/roadmap/internal/repository/dao/dao.go b/internal/roadmap/internal/repository/dao/dao.go index 70ab98e5..3c0b9304 100644 --- a/internal/roadmap/internal/repository/dao/dao.go +++ b/internal/roadmap/internal/repository/dao/dao.go @@ -24,7 +24,7 @@ import ( var ErrRecordNotFound = gorm.ErrRecordNotFound type RoadmapDAO interface { - GetEdgesByRid(ctx context.Context, rid int64) ([]Edge, error) + GetEdgesByRid(ctx context.Context, rid int64) (map[int64]Node, []EdgeV1, error) GetByBiz(ctx context.Context, biz string, bizId int64) (Roadmap, error) } @@ -42,10 +42,33 @@ func (dao *GORMRoadmapDAO) GetByBiz(ctx context.Context, biz string, bizId int64 return r, err } -func (dao *GORMRoadmapDAO) GetEdgesByRid(ctx context.Context, rid int64) ([]Edge, error) { - var res []Edge - err := dao.db.WithContext(ctx).Where("rid = ?", rid).Find(&res).Error - return res, err +func (dao *GORMRoadmapDAO) GetEdgesByRid(ctx context.Context, rid int64) (map[int64]Node, []EdgeV1, error) { + var edges []EdgeV1 + err := dao.db.WithContext(ctx).Where("rid = ?", rid). + Order("id desc"). + Find(&edges).Error + if err != nil { + return nil, nil, err + } + + nodeIds := make(map[int64]struct{}) + for _, edge := range edges { + nodeIds[edge.SrcNode] = struct{}{} + nodeIds[edge.DstNode] = struct{}{} + } + + var nodes []Node + err = dao.db.WithContext(ctx).Where("id IN ?", keys(nodeIds)). + Order("id desc"). + Find(&nodes).Error + if err != nil { + return nil, nil, err + } + nodeMap := make(map[int64]Node, len(nodes)) + for _, node := range nodes { + nodeMap[node.Id] = node + } + return nodeMap, edges, nil } func NewGORMRoadmapDAO(db *egorm.Component) RoadmapDAO { diff --git a/internal/roadmap/internal/repository/dao/init.go b/internal/roadmap/internal/repository/dao/init.go index 9ce978bf..93da48d5 100644 --- a/internal/roadmap/internal/repository/dao/init.go +++ b/internal/roadmap/internal/repository/dao/init.go @@ -17,5 +17,5 @@ package dao import "github.com/ego-component/egorm" func InitTables(db *egorm.Component) error { - return db.AutoMigrate(&Roadmap{}, &Edge{}) + return db.AutoMigrate(&Roadmap{}, &Edge{}, &EdgeV1{}, &Node{}) } diff --git a/internal/roadmap/internal/repository/dao/types.go b/internal/roadmap/internal/repository/dao/types.go index 851df9ad..105b9e90 100644 --- a/internal/roadmap/internal/repository/dao/types.go +++ b/internal/roadmap/internal/repository/dao/types.go @@ -57,3 +57,43 @@ type Edge struct { func (e Edge) TableName() string { return "roadmap_edges" } + +type EdgeV1 struct { + Id int64 `gorm:"primaryKey,autoIncrement"` + + // 理论上来说 Edge 中的 Rid, Src, Dst 构成一个唯一索引。 + // 但是因为都是内部在操作,所以没太大必要真的建立这个唯一索引 + // Roadmap 的 ID + Rid int64 `gorm:"index"` + + // 源头 + SrcNode int64 `gorm:"index:src_node"` + + // 目标 + DstNode int64 `gorm:"index:dst_node"` + + Type string + Attrs string + + Utime int64 + Ctime int64 +} +type Node struct { + Id int64 `gorm:"primaryKey,autoIncrement"` + // plainText, link + Biz string + + // 关联id + RefId int64 + Attrs string + Rid int64 `gorm:"index"` + Utime int64 + Ctime int64 +} + +func (e Node) TableName() string { + return "roadmap_nodes" +} +func (e EdgeV1) TableName() string { + return "roadmap_edges_v1" +} diff --git a/internal/roadmap/internal/repository/repository.go b/internal/roadmap/internal/repository/repository.go index 53816eee..fa8095ec 100644 --- a/internal/roadmap/internal/repository/repository.go +++ b/internal/roadmap/internal/repository/repository.go @@ -39,12 +39,12 @@ func (repo *CachedRepository) GetByBiz(ctx context.Context, biz string, bizId in if err != nil { return domain.Roadmap{}, err } - edges, err := repo.dao.GetEdgesByRid(ctx, r.Id) + nodeMap, edges, err := repo.dao.GetEdgesByRid(ctx, r.Id) if err != nil { return domain.Roadmap{}, err } res := repo.toDomain(r) - res.Edges = repo.edgesToDomain(edges) + res.Edges = repo.edgesToDomain(edges, nodeMap) return res, nil } diff --git a/internal/roadmap/internal/service/admin.go b/internal/roadmap/internal/service/admin.go index ac296223..edbce5e2 100644 --- a/internal/roadmap/internal/service/admin.go +++ b/internal/roadmap/internal/service/admin.go @@ -25,7 +25,12 @@ type AdminService interface { Detail(ctx context.Context, id int64) (domain.Roadmap, error) Save(ctx context.Context, r domain.Roadmap) (int64, error) List(ctx context.Context, offset int, limit int) ([]domain.Roadmap, error) - AddEdge(ctx context.Context, rid int64, edge domain.Edge) error + SanitizeData() + + SaveNode(ctx context.Context, node domain.Node) (int64, error) + DeleteNode(ctx context.Context, id int64) error + NodeList(ctx context.Context, rid int64) ([]domain.Node, error) + SaveEdge(ctx context.Context, rid int64, edge domain.Edge) error DeleteEdge(ctx context.Context, id int64) error } @@ -35,12 +40,28 @@ type adminService struct { repo repository.AdminRepository } -func (svc *adminService) DeleteEdge(ctx context.Context, id int64) error { - return svc.repo.DeleteEdge(ctx, id) +func (svc *adminService) SanitizeData() { + svc.repo.SanitizeData() +} + +func (svc *adminService) SaveNode(ctx context.Context, node domain.Node) (int64, error) { + return svc.repo.SaveNode(ctx, node) +} + +func (svc *adminService) DeleteNode(ctx context.Context, id int64) error { + return svc.repo.DeleteNode(ctx, id) } -func (svc *adminService) AddEdge(ctx context.Context, rid int64, edge domain.Edge) error { - return svc.repo.AddEdge(ctx, rid, edge) +func (svc *adminService) NodeList(ctx context.Context, rid int64) ([]domain.Node, error) { + return svc.repo.NodeList(ctx, rid) +} + +func (svc *adminService) SaveEdge(ctx context.Context, rid int64, edge domain.Edge) error { + return svc.repo.SaveEdgeV1(ctx, rid, edge) +} + +func (svc *adminService) DeleteEdge(ctx context.Context, id int64) error { + return svc.repo.DeleteEdgeV1(ctx, id) } func (svc *adminService) Detail(ctx context.Context, id int64) (domain.Roadmap, error) { diff --git a/internal/roadmap/internal/service/biz.go b/internal/roadmap/internal/service/biz.go deleted file mode 100644 index ada12343..00000000 --- a/internal/roadmap/internal/service/biz.go +++ /dev/null @@ -1,126 +0,0 @@ -// Copyright 2023 ecodeclub -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package service - -import ( - "context" - "fmt" - "sync" - - "github.com/ecodeclub/ekit/mapx" - baguwen "github.com/ecodeclub/webook/internal/question" - "github.com/ecodeclub/webook/internal/roadmap/internal/domain" - "golang.org/x/sync/errgroup" -) - -// BizService 作为一个聚合服务,下沉到这里以减轻 web 的逻辑负担 -type BizService interface { - // GetBizs bizs 和 ids 的长度必须一样 - // 返回值是 biz-id-Biz 的结构 - GetBizs(ctx context.Context, bizs []string, ids []int64) (map[string]map[int64]domain.Biz, error) -} - -var _ BizService = &ConcurrentBizService{} - -// ConcurrentBizService 强调并发 -type ConcurrentBizService struct { - queSvc baguwen.Service - queSetSvc baguwen.QuestionSetService -} - -func (svc *ConcurrentBizService) GetBizs(ctx context.Context, bizs []string, ids []int64) (map[string]map[int64]domain.Biz, error) { - // 先按照 biz 分组 - // biz 不会有很多 - bizIdMap := mapx.NewMultiBuiltinMap[string, int64](4) - for i := 0; i < len(bizs); i++ { - // 这里不对长度做检测,调用者负责确保长度一致 - _ = bizIdMap.Put(bizs[i], ids[i]) - } - - var eg errgroup.Group - keys := bizIdMap.Keys() - var lock sync.Mutex - res := make(map[string]map[int64]domain.Biz, len(keys)) - for _, key := range keys { - bizIds, ok := bizIdMap.Get(key) - if !ok { - continue - } - - // 1.22 之后可以去掉 - key := key - eg.Go(func() error { - bizMap, err := svc.GetBizsByIds(ctx, key, bizIds) - if err == nil { - lock.Lock() - res[key] = bizMap - lock.Unlock() - } - return err - }) - } - err := eg.Wait() - return res, err -} - -// GetBizsByIds 将来可能需要暴露出去,暂时保留定义为公共接口 -func (svc *ConcurrentBizService) GetBizsByIds(ctx context.Context, biz string, ids []int64) (map[int64]domain.Biz, error) { - // biz 不是很多,所以可以用 switch - // 后续可以重构为策略模式 - switch biz { - case domain.BizQuestion: - return svc.getQuestions(ctx, ids) - case domain.BizQuestionSet: - return svc.getQuestionSet(ctx, ids) - default: - return nil, fmt.Errorf("不支持的 Biz: %s", biz) - } -} - -func (svc *ConcurrentBizService) getQuestions(ctx context.Context, ids []int64) (map[int64]domain.Biz, error) { - ques, err := svc.queSvc.GetPubByIDs(ctx, ids) - if err != nil { - return nil, err - } - res := make(map[int64]domain.Biz, len(ques)) - for _, que := range ques { - res[que.Id] = domain.Biz{ - Biz: domain.BizQuestion, - BizId: que.Id, - Title: que.Title, - } - } - return res, nil -} - -func (svc *ConcurrentBizService) getQuestionSet(ctx context.Context, ids []int64) (map[int64]domain.Biz, error) { - qs, err := svc.queSetSvc.GetByIds(ctx, ids) - if err != nil { - return nil, err - } - res := make(map[int64]domain.Biz, len(qs)) - for _, q := range qs { - res[q.Id] = domain.Biz{ - Biz: domain.BizQuestionSet, - BizId: q.Id, - Title: q.Title, - } - } - return res, nil -} - -func NewConcurrentBizService(queSvc baguwen.Service, queSetSvc baguwen.QuestionSetService) BizService { - return &ConcurrentBizService{queSvc: queSvc, queSetSvc: queSetSvc} -} diff --git a/internal/roadmap/internal/service/biz/biz.go b/internal/roadmap/internal/service/biz/biz.go new file mode 100644 index 00000000..49987a3e --- /dev/null +++ b/internal/roadmap/internal/service/biz/biz.go @@ -0,0 +1,68 @@ +package biz + +import ( + "context" + "fmt" + "sync" + + "github.com/ecodeclub/ekit/mapx" + "github.com/ecodeclub/webook/internal/roadmap/internal/domain" + "golang.org/x/sync/errgroup" +) + +// ConcurrentBizService 强调并发 +type ConcurrentBizService struct { + BizStrategyMap map[string]Strategy +} + +func NewConcurrentBizService(bizStrategtMap map[string]Strategy) Service { + return &ConcurrentBizService{ + BizStrategyMap: bizStrategtMap, + } +} + +func (svc *ConcurrentBizService) GetBizs(ctx context.Context, bizs []string, ids []int64) (map[string]map[int64]domain.Biz, error) { + // 先按照 biz 分组 + // biz 不会有很多 + bizIdMap := mapx.NewMultiBuiltinMap[string, int64](4) + for i := 0; i < len(bizs); i++ { + // 这里不对长度做检测,调用者负责确保长度一致 + _ = bizIdMap.Put(bizs[i], ids[i]) + } + + var eg errgroup.Group + keys := bizIdMap.Keys() + var lock sync.Mutex + res := make(map[string]map[int64]domain.Biz, len(keys)) + for _, key := range keys { + bizIds, ok := bizIdMap.Get(key) + if !ok { + continue + } + + // 1.22 之后可以去掉 + key := key + eg.Go(func() error { + bizMap, err := svc.GetBizsByIds(ctx, key, bizIds) + if err == nil { + lock.Lock() + res[key] = bizMap + lock.Unlock() + } + return err + }) + } + err := eg.Wait() + return res, err +} + +// GetBizsByIds 将来可能需要暴露出去,暂时保留定义为公共接口 +func (svc *ConcurrentBizService) GetBizsByIds(ctx context.Context, biz string, ids []int64) (map[int64]domain.Biz, error) { + // biz 不是很多,所以可以用 switch + // 后续可以重构为策略模式 + strategy, ok := svc.BizStrategyMap[biz] + if !ok { + return nil, fmt.Errorf("不支持的 Biz: %s", biz) + } + return strategy.GetBizsByIds(ctx, ids) +} diff --git a/internal/roadmap/internal/service/biz/question.go b/internal/roadmap/internal/service/biz/question.go new file mode 100644 index 00000000..ba217442 --- /dev/null +++ b/internal/roadmap/internal/service/biz/question.go @@ -0,0 +1,34 @@ +package biz + +import ( + "context" + + baguwen "github.com/ecodeclub/webook/internal/question" + "github.com/ecodeclub/webook/internal/roadmap/internal/domain" +) + +type QuestionStrategy struct { + queSvc baguwen.Service +} + +func NewQuestionStrategy(queSvc baguwen.Service) Strategy { + return &QuestionStrategy{ + queSvc: queSvc, + } +} + +func (q *QuestionStrategy) GetBizsByIds(ctx context.Context, ids []int64) (map[int64]domain.Biz, error) { + ques, err := q.queSvc.GetPubByIDs(ctx, ids) + if err != nil { + return nil, err + } + res := make(map[int64]domain.Biz, len(ques)) + for _, que := range ques { + res[que.Id] = domain.Biz{ + Biz: domain.BizQuestion, + BizId: que.Id, + Title: que.Title, + } + } + return res, nil +} diff --git a/internal/roadmap/internal/service/biz/question_set.go b/internal/roadmap/internal/service/biz/question_set.go new file mode 100644 index 00000000..9eeef321 --- /dev/null +++ b/internal/roadmap/internal/service/biz/question_set.go @@ -0,0 +1,33 @@ +package biz + +import ( + "context" + + baguwen "github.com/ecodeclub/webook/internal/question" + "github.com/ecodeclub/webook/internal/roadmap/internal/domain" +) + +type QuestionSetStrategy struct { + queSetSvc baguwen.QuestionSetService +} + +func NewQuestionSetStrategy(queSvc baguwen.QuestionSetService) Strategy { + return &QuestionSetStrategy{ + queSetSvc: queSvc, + } +} +func (q *QuestionSetStrategy) GetBizsByIds(ctx context.Context, ids []int64) (map[int64]domain.Biz, error) { + qs, err := q.queSetSvc.GetByIds(ctx, ids) + if err != nil { + return nil, err + } + res := make(map[int64]domain.Biz, len(qs)) + for _, q := range qs { + res[q.Id] = domain.Biz{ + Biz: domain.BizQuestionSet, + BizId: q.Id, + Title: q.Title, + } + } + return res, nil +} diff --git a/internal/roadmap/internal/service/biz/type.go b/internal/roadmap/internal/service/biz/type.go new file mode 100644 index 00000000..b4aa3769 --- /dev/null +++ b/internal/roadmap/internal/service/biz/type.go @@ -0,0 +1,18 @@ +package biz + +import ( + "context" + + "github.com/ecodeclub/webook/internal/roadmap/internal/domain" +) + +// Service 作为一个聚合服务,下沉到这里以减轻 web 的逻辑负担 +type Service interface { + // GetBizs bizs 和 ids 的长度必须一样 + // 返回值是 biz-id-Biz 的结构 + GetBizs(ctx context.Context, bizs []string, ids []int64) (map[string]map[int64]domain.Biz, error) +} + +type Strategy interface { + GetBizsByIds(ctx context.Context, ids []int64) (map[int64]domain.Biz, error) +} diff --git a/internal/roadmap/internal/web/admin.go b/internal/roadmap/internal/web/admin.go index 18af3476..709cbf27 100644 --- a/internal/roadmap/internal/web/admin.go +++ b/internal/roadmap/internal/web/admin.go @@ -19,12 +19,13 @@ import ( "github.com/ecodeclub/ginx" "github.com/ecodeclub/webook/internal/roadmap/internal/domain" "github.com/ecodeclub/webook/internal/roadmap/internal/service" + "github.com/ecodeclub/webook/internal/roadmap/internal/service/biz" "github.com/gin-gonic/gin" ) type AdminHandler struct { svc service.AdminService - bizSvc service.BizService + bizSvc biz.Service } func (h *AdminHandler) PrivateRoutes(server *gin.Engine) { @@ -32,10 +33,54 @@ func (h *AdminHandler) PrivateRoutes(server *gin.Engine) { g.POST("/save", ginx.B(h.Save)) g.POST("/list", ginx.B(h.List)) g.POST("/detail", ginx.B(h.Detail)) + g.POST("/sanitize", ginx.W(h.Sanitize)) edge := g.Group("/edge") - edge.POST("/save", ginx.B(h.AddEdge)) + edge.POST("/save", ginx.B(h.SaveEdge)) edge.POST("/delete", ginx.B(h.DeleteEdge)) + + node := g.Group("/node") + node.POST("/save", ginx.B(h.SaveNode)) + node.POST("/delete", ginx.B[IdReq](h.DeleteNode)) + node.POST("/list", ginx.B[IdReq](h.NodeList)) + +} + +func (h *AdminHandler) Sanitize(ctx *ginx.Context) (ginx.Result, error) { + h.svc.SanitizeData() + return ginx.Result{}, nil +} + +func (h *AdminHandler) NodeList(ctx *ginx.Context, req IdReq) (ginx.Result, error) { + nodeList, err := h.svc.NodeList(ctx, req.Id) + if err != nil { + return systemErrorResult, err + } + list := slice.Map(nodeList, func(idx int, src domain.Node) Node { + return newNode(src) + }) + return ginx.Result{ + Data: list, + }, nil +} + +func (h *AdminHandler) SaveNode(ctx *ginx.Context, node Node) (ginx.Result, error) { + n := node.toDomain() + id, err := h.svc.SaveNode(ctx, n) + if err != nil { + return systemErrorResult, err + } + return ginx.Result{ + Data: id, + }, nil +} + +func (h *AdminHandler) DeleteNode(ctx *ginx.Context, req IdReq) (ginx.Result, error) { + err := h.svc.DeleteNode(ctx, req.Id) + if err != nil { + return systemErrorResult, err + } + return ginx.Result{}, nil } func (h *AdminHandler) Save(ctx *ginx.Context, req Roadmap) (ginx.Result, error) { @@ -76,9 +121,9 @@ func (h *AdminHandler) List(ctx *ginx.Context, req Page) (ginx.Result, error) { }, nil } -// AddEdge 后面可以考虑重构为 Save 语义 -func (h *AdminHandler) AddEdge(ctx *ginx.Context, req AddEdgeReq) (ginx.Result, error) { - err := h.svc.AddEdge(ctx, req.Rid, req.Edge.toDomain()) +// SaveEdge 后面可以考虑重构为 Save 语义 +func (h *AdminHandler) SaveEdge(ctx *ginx.Context, req AddEdgeReq) (ginx.Result, error) { + err := h.svc.SaveEdge(ctx, req.Rid, req.Edge.toDomain()) if err != nil { return systemErrorResult, err } @@ -103,7 +148,6 @@ func (h *AdminHandler) Detail(ctx *ginx.Context, req IdReq) (ginx.Result, error) if err != nil { return systemErrorResult, err } - rm := newRoadmapWithBiz(r, bizMap) return ginx.Result{ Data: rm, @@ -112,7 +156,7 @@ func (h *AdminHandler) Detail(ctx *ginx.Context, req IdReq) (ginx.Result, error) func NewAdminHandler( svc service.AdminService, - bizSvc service.BizService) *AdminHandler { + bizSvc biz.Service) *AdminHandler { return &AdminHandler{ svc: svc, bizSvc: bizSvc, diff --git a/internal/roadmap/internal/web/handler.go b/internal/roadmap/internal/web/handler.go index 7048a31c..a954dd1e 100644 --- a/internal/roadmap/internal/web/handler.go +++ b/internal/roadmap/internal/web/handler.go @@ -17,12 +17,13 @@ package web import ( "github.com/ecodeclub/ginx" "github.com/ecodeclub/webook/internal/roadmap/internal/service" + "github.com/ecodeclub/webook/internal/roadmap/internal/service/biz" "github.com/gin-gonic/gin" ) type Handler struct { svc service.Service - bizSvc service.BizService + bizSvc biz.Service } func (h *Handler) PrivateRoutes(server *gin.Engine) { @@ -52,7 +53,7 @@ func (h *Handler) Detail(ctx *ginx.Context, req Biz) (ginx.Result, error) { } } -func NewHandler(svc service.Service, bizSvc service.BizService) *Handler { +func NewHandler(svc service.Service, bizSvc biz.Service) *Handler { return &Handler{ svc: svc, bizSvc: bizSvc, diff --git a/internal/roadmap/internal/web/vo.go b/internal/roadmap/internal/web/vo.go index e0639201..284353ff 100644 --- a/internal/roadmap/internal/web/vo.go +++ b/internal/roadmap/internal/web/vo.go @@ -55,9 +55,11 @@ func newRoadmapWithBiz(r domain.Roadmap, dst := newNode(edge.Dst) dst.Title = bizMap[dst.Biz][dst.BizId].Title return Edge{ - Id: edge.Id, - Src: src, - Dst: dst, + Id: edge.Id, + Type: edge.Type, + Attrs: edge.Attrs, + Src: src, + Dst: dst, } }) return rm @@ -88,6 +90,9 @@ type IdReq struct { } type Node struct { + ID int64 `json:"id"` + Rid int64 `json:"rid"` + Attrs string `json:"attrs"` BizId int64 `json:"bizId"` Biz string `json:"biz"` Title string `json:"title"` @@ -95,6 +100,9 @@ type Node struct { func (n Node) toDomain() domain.Node { return domain.Node{ + ID: n.ID, + Rid: n.Rid, + Attrs: n.Attrs, Biz: domain.Biz{ BizId: n.BizId, Biz: n.Biz, @@ -104,22 +112,31 @@ func (n Node) toDomain() domain.Node { func newNode(node domain.Node) Node { return Node{ + ID: node.ID, + Rid: node.Rid, + Attrs: node.Attrs, BizId: node.BizId, + Biz: node.Biz.Biz, Title: node.Title, } } type Edge struct { - Id int64 `json:"id"` - Src Node `json:"src"` - Dst Node `json:"dst"` + Id int64 `json:"id"` + Type string `json:"type"` + Attrs string `json:"attrs"` + Src Node `json:"src"` + Dst Node `json:"dst"` } func (e Edge) toDomain() domain.Edge { return domain.Edge{ - Src: e.Src.toDomain(), - Dst: e.Dst.toDomain(), + Id: e.Id, + Type: e.Type, + Attrs: e.Attrs, + Src: e.Src.toDomain(), + Dst: e.Dst.toDomain(), } } diff --git a/internal/roadmap/wire.go b/internal/roadmap/wire.go index 25208ddb..cec0832c 100644 --- a/internal/roadmap/wire.go +++ b/internal/roadmap/wire.go @@ -19,6 +19,9 @@ package roadmap import ( "sync" + "github.com/ecodeclub/webook/internal/roadmap/internal/domain" + "github.com/ecodeclub/webook/internal/roadmap/internal/service/biz" + baguwen "github.com/ecodeclub/webook/internal/question" "github.com/ecodeclub/webook/internal/roadmap/internal/repository" "github.com/ecodeclub/webook/internal/roadmap/internal/repository/dao" @@ -32,7 +35,7 @@ func InitModule(db *egorm.Component, queModule *baguwen.Module) *Module { wire.Build( web.NewAdminHandler, service.NewAdminService, - service.NewConcurrentBizService, + NewConcurrentBizService, repository.NewCachedAdminRepository, initAdminDAO, @@ -62,3 +65,10 @@ func initAdminDAO(db *egorm.Component) dao.AdminDAO { }) return adminDAO } + +func NewConcurrentBizService(questionSvc baguwen.Service, questionSetSvc baguwen.QuestionSetService) biz.Service { + return biz.NewConcurrentBizService(map[string]biz.Strategy{ + domain.BizQuestion: biz.NewQuestionStrategy(questionSvc), + domain.BizQuestionSet: biz.NewQuestionSetStrategy(questionSetSvc), + }) +} diff --git a/internal/roadmap/wire_gen.go b/internal/roadmap/wire_gen.go index 1a5a9cdb..464c4a0f 100644 --- a/internal/roadmap/wire_gen.go +++ b/internal/roadmap/wire_gen.go @@ -1,6 +1,6 @@ // Code generated by Wire. DO NOT EDIT. -//go:generate go run github.com/google/wire/cmd/wire +//go:generate go run -mod=mod github.com/google/wire/cmd/wire //go:build !wireinject // +build !wireinject @@ -10,9 +10,11 @@ import ( "sync" baguwen "github.com/ecodeclub/webook/internal/question" + "github.com/ecodeclub/webook/internal/roadmap/internal/domain" "github.com/ecodeclub/webook/internal/roadmap/internal/repository" "github.com/ecodeclub/webook/internal/roadmap/internal/repository/dao" "github.com/ecodeclub/webook/internal/roadmap/internal/service" + "github.com/ecodeclub/webook/internal/roadmap/internal/service/biz" "github.com/ecodeclub/webook/internal/roadmap/internal/web" "github.com/ego-component/egorm" "gorm.io/gorm" @@ -26,7 +28,7 @@ func InitModule(db *gorm.DB, queModule *baguwen.Module) *Module { adminService := service.NewAdminService(adminRepository) serviceService := queModule.Svc questionSetService := queModule.SetSvc - bizService := service.NewConcurrentBizService(serviceService, questionSetService) + bizService := NewConcurrentBizService(serviceService, questionSetService) adminHandler := web.NewAdminHandler(adminService, bizService) roadmapDAO := dao.NewGORMRoadmapDAO(db) repositoryRepository := repository.NewCachedRepository(roadmapDAO) @@ -56,3 +58,7 @@ func initAdminDAO(db *egorm.Component) dao.AdminDAO { }) return adminDAO } + +func NewConcurrentBizService(questionSvc baguwen.Service, questionSetSvc baguwen.QuestionSetService) biz.Service { + return biz.NewConcurrentBizService(map[string]biz.Strategy{domain.BizQuestion: biz.NewQuestionStrategy(questionSvc), domain.BizQuestionSet: biz.NewQuestionSetStrategy(questionSetSvc)}) +}