From 50e83a1e704d806a5be73d33ded60435c16c5823 Mon Sep 17 00:00:00 2001 From: Deng Ming Date: Mon, 8 Jul 2024 10:28:43 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90=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 --- README.md | 1 + .../internal/repository/dao/question_set.go | 12 +- .../internal/repository/question_set.go | 8 + .../question/internal/service/question_set.go | 6 + internal/question/mocks/quetion_set.mock.go | 235 ++++++++ internal/question/module.go | 7 +- internal/question/types.go | 2 + internal/question/wire_gen.go | 13 +- internal/roadmap/internal/domain/types.go | 61 +++ internal/roadmap/internal/errs/code.go | 10 + .../internal/integration/admin_test.go | 505 ++++++++++++++++++ .../internal/integration/handler_test.go | 189 +++++++ .../internal/integration/startup/wire.go | 32 ++ .../internal/integration/startup/wire_gen.go | 21 + internal/roadmap/internal/repository/admin.go | 108 ++++ .../roadmap/internal/repository/converter.go | 55 ++ .../roadmap/internal/repository/dao/admin.go | 84 +++ .../roadmap/internal/repository/dao/dao.go | 53 ++ .../roadmap/internal/repository/dao/init.go | 21 + .../roadmap/internal/repository/dao/types.go | 59 ++ .../roadmap/internal/repository/repository.go | 53 ++ internal/roadmap/internal/service/admin.go | 62 +++ internal/roadmap/internal/service/biz.go | 126 +++++ internal/roadmap/internal/service/service.go | 42 ++ internal/roadmap/internal/web/admin.go | 120 +++++ internal/roadmap/internal/web/handler.go | 60 +++ internal/roadmap/internal/web/result.go | 27 + internal/roadmap/internal/web/vo.go | 129 +++++ internal/roadmap/types.go | 27 + internal/roadmap/wire.go | 64 +++ internal/roadmap/wire_gen.go | 58 ++ internal/search/internal/event/consumer.go | 5 +- ioc/admin.go | 4 + ioc/gin.go | 4 + ioc/wire.go | 3 + ioc/wire_gen.go | 12 +- 36 files changed, 2257 insertions(+), 21 deletions(-) create mode 100644 internal/question/mocks/quetion_set.mock.go create mode 100644 internal/roadmap/internal/domain/types.go create mode 100644 internal/roadmap/internal/errs/code.go create mode 100644 internal/roadmap/internal/integration/admin_test.go create mode 100644 internal/roadmap/internal/integration/handler_test.go create mode 100644 internal/roadmap/internal/integration/startup/wire.go create mode 100644 internal/roadmap/internal/integration/startup/wire_gen.go create mode 100644 internal/roadmap/internal/repository/admin.go create mode 100644 internal/roadmap/internal/repository/converter.go create mode 100644 internal/roadmap/internal/repository/dao/admin.go create mode 100644 internal/roadmap/internal/repository/dao/dao.go create mode 100644 internal/roadmap/internal/repository/dao/init.go create mode 100644 internal/roadmap/internal/repository/dao/types.go create mode 100644 internal/roadmap/internal/repository/repository.go create mode 100644 internal/roadmap/internal/service/admin.go create mode 100644 internal/roadmap/internal/service/biz.go create mode 100644 internal/roadmap/internal/service/service.go create mode 100644 internal/roadmap/internal/web/admin.go create mode 100644 internal/roadmap/internal/web/handler.go create mode 100644 internal/roadmap/internal/web/result.go create mode 100644 internal/roadmap/internal/web/vo.go create mode 100644 internal/roadmap/types.go create mode 100644 internal/roadmap/wire.go create mode 100644 internal/roadmap/wire_gen.go diff --git a/README.md b/README.md index 6c6440e1..463612f6 100644 --- a/README.md +++ b/README.md @@ -49,3 +49,4 @@ - credit - 10 - project - 11 - marketing - 12 +- roadmap - 13 # 路线图 \ No newline at end of file diff --git a/internal/question/internal/repository/dao/question_set.go b/internal/question/internal/repository/dao/question_set.go index 22f8aee4..161dd777 100644 --- a/internal/question/internal/repository/dao/question_set.go +++ b/internal/question/internal/repository/dao/question_set.go @@ -16,17 +16,12 @@ package dao import ( "context" - "errors" "time" "github.com/ego-component/egorm" "gorm.io/gorm" ) -var ( - ErrInvalidQuestionID = errors.New("问题ID非法") -) - type QuestionSetDAO interface { Create(ctx context.Context, qs QuestionSet) (int64, error) GetByID(ctx context.Context, id int64) (QuestionSet, error) @@ -37,12 +32,19 @@ type QuestionSetDAO interface { Count(ctx context.Context) (int64, error) List(ctx context.Context, offset, limit int) ([]QuestionSet, error) UpdateNonZero(ctx context.Context, set QuestionSet) error + GetByIDs(ctx context.Context, ids []int64) ([]QuestionSet, error) } type GORMQuestionSetDAO struct { db *egorm.Component } +func (g *GORMQuestionSetDAO) GetByIDs(ctx context.Context, ids []int64) ([]QuestionSet, error) { + var res []QuestionSet + err := g.db.WithContext(ctx).Where("id IN ?", ids).Find(&res).Error + return res, err +} + func (g *GORMQuestionSetDAO) UpdateNonZero(ctx context.Context, set QuestionSet) error { set.Utime = time.Now().UnixMilli() return g.db.WithContext(ctx).Where("id = ?", set.Id).Updates(set).Error diff --git a/internal/question/internal/repository/question_set.go b/internal/question/internal/repository/question_set.go index f8d0e14a..7b11963f 100644 --- a/internal/question/internal/repository/question_set.go +++ b/internal/question/internal/repository/question_set.go @@ -31,6 +31,7 @@ type QuestionSetRepository interface { Total(ctx context.Context) (int64, error) List(ctx context.Context, offset int, limit int) ([]domain.QuestionSet, error) UpdateNonZero(ctx context.Context, set domain.QuestionSet) error + GetByIDs(ctx context.Context, ids []int64) ([]domain.QuestionSet, error) } type questionSetRepository struct { @@ -38,6 +39,13 @@ type questionSetRepository struct { logger *elog.Component } +func (q *questionSetRepository) GetByIDs(ctx context.Context, ids []int64) ([]domain.QuestionSet, error) { + qs, err := q.dao.GetByIDs(ctx, ids) + return slice.Map(qs, func(idx int, src dao.QuestionSet) domain.QuestionSet { + return q.toDomainQuestionSet(src) + }), err +} + func (q *questionSetRepository) UpdateNonZero(ctx context.Context, set domain.QuestionSet) error { return q.dao.UpdateNonZero(ctx, q.toEntityQuestionSet(set)) } diff --git a/internal/question/internal/service/question_set.go b/internal/question/internal/service/question_set.go index dea75511..b7f6469a 100644 --- a/internal/question/internal/service/question_set.go +++ b/internal/question/internal/service/question_set.go @@ -26,11 +26,13 @@ import ( "golang.org/x/sync/errgroup" ) +//go:generate mockgen -source=./question_set.go -destination=../../mocks/quetion_set.mock.go -package=quemocks -typed=true QuestionSetService type QuestionSetService interface { Save(ctx context.Context, set domain.QuestionSet) (int64, error) UpdateQuestions(ctx context.Context, set domain.QuestionSet) error List(ctx context.Context, offset, limit int) ([]domain.QuestionSet, int64, error) Detail(ctx context.Context, id int64) (domain.QuestionSet, error) + GetByIds(ctx context.Context, ids []int64) ([]domain.QuestionSet, error) } type questionSetService struct { @@ -41,6 +43,10 @@ type questionSetService struct { syncTimeout time.Duration } +func (q *questionSetService) GetByIds(ctx context.Context, ids []int64) ([]domain.QuestionSet, error) { + return q.repo.GetByIDs(ctx, ids) +} + func NewQuestionSetService(repo repository.QuestionSetRepository, intrProducer event.InteractiveEventProducer, producer event.SyncDataToSearchEventProducer) QuestionSetService { diff --git a/internal/question/mocks/quetion_set.mock.go b/internal/question/mocks/quetion_set.mock.go new file mode 100644 index 00000000..f377106f --- /dev/null +++ b/internal/question/mocks/quetion_set.mock.go @@ -0,0 +1,235 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./question_set.go +// +// Generated by this command: +// +// mockgen -source=./question_set.go -destination=../../mocks/quetion_set.mock.go -package=quemocks -typed=true QuestionSetService +// +// Package quemocks is a generated GoMock package. +package quemocks + +import ( + context "context" + reflect "reflect" + + domain "github.com/ecodeclub/webook/internal/question/internal/domain" + gomock "go.uber.org/mock/gomock" +) + +// MockQuestionSetService is a mock of QuestionSetService interface. +type MockQuestionSetService struct { + ctrl *gomock.Controller + recorder *MockQuestionSetServiceMockRecorder +} + +// MockQuestionSetServiceMockRecorder is the mock recorder for MockQuestionSetService. +type MockQuestionSetServiceMockRecorder struct { + mock *MockQuestionSetService +} + +// NewMockQuestionSetService creates a new mock instance. +func NewMockQuestionSetService(ctrl *gomock.Controller) *MockQuestionSetService { + mock := &MockQuestionSetService{ctrl: ctrl} + mock.recorder = &MockQuestionSetServiceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockQuestionSetService) EXPECT() *MockQuestionSetServiceMockRecorder { + return m.recorder +} + +// Detail mocks base method. +func (m *MockQuestionSetService) Detail(ctx context.Context, id int64) (domain.QuestionSet, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Detail", ctx, id) + ret0, _ := ret[0].(domain.QuestionSet) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Detail indicates an expected call of Detail. +func (mr *MockQuestionSetServiceMockRecorder) Detail(ctx, id any) *QuestionSetServiceDetailCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Detail", reflect.TypeOf((*MockQuestionSetService)(nil).Detail), ctx, id) + return &QuestionSetServiceDetailCall{Call: call} +} + +// QuestionSetServiceDetailCall wrap *gomock.Call +type QuestionSetServiceDetailCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *QuestionSetServiceDetailCall) Return(arg0 domain.QuestionSet, arg1 error) *QuestionSetServiceDetailCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *QuestionSetServiceDetailCall) Do(f func(context.Context, int64) (domain.QuestionSet, error)) *QuestionSetServiceDetailCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *QuestionSetServiceDetailCall) DoAndReturn(f func(context.Context, int64) (domain.QuestionSet, error)) *QuestionSetServiceDetailCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// GetByIds mocks base method. +func (m *MockQuestionSetService) GetByIds(ctx context.Context, ids []int64) ([]domain.QuestionSet, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetByIds", ctx, ids) + ret0, _ := ret[0].([]domain.QuestionSet) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetByIds indicates an expected call of GetByIds. +func (mr *MockQuestionSetServiceMockRecorder) GetByIds(ctx, ids any) *QuestionSetServiceGetByIdsCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByIds", reflect.TypeOf((*MockQuestionSetService)(nil).GetByIds), ctx, ids) + return &QuestionSetServiceGetByIdsCall{Call: call} +} + +// QuestionSetServiceGetByIdsCall wrap *gomock.Call +type QuestionSetServiceGetByIdsCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *QuestionSetServiceGetByIdsCall) Return(arg0 []domain.QuestionSet, arg1 error) *QuestionSetServiceGetByIdsCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *QuestionSetServiceGetByIdsCall) Do(f func(context.Context, []int64) ([]domain.QuestionSet, error)) *QuestionSetServiceGetByIdsCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *QuestionSetServiceGetByIdsCall) DoAndReturn(f func(context.Context, []int64) ([]domain.QuestionSet, error)) *QuestionSetServiceGetByIdsCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// List mocks base method. +func (m *MockQuestionSetService) List(ctx context.Context, offset, limit int) ([]domain.QuestionSet, int64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "List", ctx, offset, limit) + ret0, _ := ret[0].([]domain.QuestionSet) + ret1, _ := ret[1].(int64) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// List indicates an expected call of List. +func (mr *MockQuestionSetServiceMockRecorder) List(ctx, offset, limit any) *QuestionSetServiceListCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockQuestionSetService)(nil).List), ctx, offset, limit) + return &QuestionSetServiceListCall{Call: call} +} + +// QuestionSetServiceListCall wrap *gomock.Call +type QuestionSetServiceListCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *QuestionSetServiceListCall) Return(arg0 []domain.QuestionSet, arg1 int64, arg2 error) *QuestionSetServiceListCall { + c.Call = c.Call.Return(arg0, arg1, arg2) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *QuestionSetServiceListCall) Do(f func(context.Context, int, int) ([]domain.QuestionSet, int64, error)) *QuestionSetServiceListCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *QuestionSetServiceListCall) DoAndReturn(f func(context.Context, int, int) ([]domain.QuestionSet, int64, error)) *QuestionSetServiceListCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// Save mocks base method. +func (m *MockQuestionSetService) Save(ctx context.Context, set domain.QuestionSet) (int64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Save", ctx, set) + ret0, _ := ret[0].(int64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Save indicates an expected call of Save. +func (mr *MockQuestionSetServiceMockRecorder) Save(ctx, set any) *QuestionSetServiceSaveCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Save", reflect.TypeOf((*MockQuestionSetService)(nil).Save), ctx, set) + return &QuestionSetServiceSaveCall{Call: call} +} + +// QuestionSetServiceSaveCall wrap *gomock.Call +type QuestionSetServiceSaveCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *QuestionSetServiceSaveCall) Return(arg0 int64, arg1 error) *QuestionSetServiceSaveCall { + c.Call = c.Call.Return(arg0, arg1) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *QuestionSetServiceSaveCall) Do(f func(context.Context, domain.QuestionSet) (int64, error)) *QuestionSetServiceSaveCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *QuestionSetServiceSaveCall) DoAndReturn(f func(context.Context, domain.QuestionSet) (int64, error)) *QuestionSetServiceSaveCall { + c.Call = c.Call.DoAndReturn(f) + return c +} + +// UpdateQuestions mocks base method. +func (m *MockQuestionSetService) UpdateQuestions(ctx context.Context, set domain.QuestionSet) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateQuestions", ctx, set) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateQuestions indicates an expected call of UpdateQuestions. +func (mr *MockQuestionSetServiceMockRecorder) UpdateQuestions(ctx, set any) *QuestionSetServiceUpdateQuestionsCall { + mr.mock.ctrl.T.Helper() + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateQuestions", reflect.TypeOf((*MockQuestionSetService)(nil).UpdateQuestions), ctx, set) + return &QuestionSetServiceUpdateQuestionsCall{Call: call} +} + +// QuestionSetServiceUpdateQuestionsCall wrap *gomock.Call +type QuestionSetServiceUpdateQuestionsCall struct { + *gomock.Call +} + +// Return rewrite *gomock.Call.Return +func (c *QuestionSetServiceUpdateQuestionsCall) Return(arg0 error) *QuestionSetServiceUpdateQuestionsCall { + c.Call = c.Call.Return(arg0) + return c +} + +// Do rewrite *gomock.Call.Do +func (c *QuestionSetServiceUpdateQuestionsCall) Do(f func(context.Context, domain.QuestionSet) error) *QuestionSetServiceUpdateQuestionsCall { + c.Call = c.Call.Do(f) + return c +} + +// DoAndReturn rewrite *gomock.Call.DoAndReturn +func (c *QuestionSetServiceUpdateQuestionsCall) DoAndReturn(f func(context.Context, domain.QuestionSet) error) *QuestionSetServiceUpdateQuestionsCall { + c.Call = c.Call.DoAndReturn(f) + return c +} diff --git a/internal/question/module.go b/internal/question/module.go index 1f5e8100..895843f0 100644 --- a/internal/question/module.go +++ b/internal/question/module.go @@ -15,7 +15,8 @@ package baguwen type Module struct { - Svc Service - Hdl *Handler - QsHdl *QuestionSetHandler + Svc Service + SetSvc QuestionSetService + Hdl *Handler + QsHdl *QuestionSetHandler } diff --git a/internal/question/types.go b/internal/question/types.go index 0544dbf6..3ca0040f 100644 --- a/internal/question/types.go +++ b/internal/question/types.go @@ -24,4 +24,6 @@ type Handler = web.Handler type QuestionSetHandler = web.QuestionSetHandler type Service = service.Service +type QuestionSetService = service.QuestionSetService type Question = domain.Question +type QuestionSet = domain.QuestionSet diff --git a/internal/question/wire_gen.go b/internal/question/wire_gen.go index 9790d2de..282dddb5 100644 --- a/internal/question/wire_gen.go +++ b/internal/question/wire_gen.go @@ -37,16 +37,17 @@ func InitModule(db *gorm.DB, intrModule *interactive.Module, ec ecache.Cache, q return nil, err } serviceService := service.NewService(repositoryRepository, syncDataToSearchEventProducer, interactiveEventProducer) - interactiveService := intrModule.Svc - handler := web.NewHandler(serviceService, interactiveService) questionSetDAO := InitQuestionSetDAO(db) questionSetRepository := repository.NewQuestionSetRepository(questionSetDAO) questionSetService := service.NewQuestionSetService(questionSetRepository, interactiveEventProducer, syncDataToSearchEventProducer) - questionSetHandler := web.NewQuestionSetHandler(questionSetService, interactiveService) + service2 := intrModule.Svc + handler := web.NewHandler(serviceService, service2) + questionSetHandler := web.NewQuestionSetHandler(questionSetService, service2) module := &Module{ - Svc: serviceService, - Hdl: handler, - QsHdl: questionSetHandler, + Svc: serviceService, + SetSvc: questionSetService, + Hdl: handler, + QsHdl: questionSetHandler, } return module, nil } diff --git a/internal/roadmap/internal/domain/types.go b/internal/roadmap/internal/domain/types.go new file mode 100644 index 00000000..4f21686b --- /dev/null +++ b/internal/roadmap/internal/domain/types.go @@ -0,0 +1,61 @@ +// 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 domain + +type Roadmap struct { + Id int64 + Title string + + Biz string + BizId int64 + + Edges []Edge + Utime int64 +} + +func (r Roadmap) Bizs() ([]string, []int64) { + // SRC + DST,所以乘以 2,而后加上本体的 biz + bizs := make([]string, 0, len(r.Edges)*2+1) + bizIds := make([]int64, 0, len(r.Edges)*2+1) + for _, edge := range r.Edges { + bizs = append(bizs, edge.Src.Biz.Biz, edge.Dst.Biz.Biz) + bizIds = append(bizIds, edge.Src.BizId, edge.Dst.BizId) + } + // 加上本身的 + bizs = append(bizs, r.Biz) + bizIds = append(bizIds, r.BizId) + return bizs, bizIds +} + +type Node struct { + Biz +} + +type Edge struct { + Id int64 + Src Node + Dst Node +} + +type Biz struct { + Biz string + BizId int64 + Title string +} + +const ( + BizQuestion = "question" + BizQuestionSet = "questionSet" +) diff --git a/internal/roadmap/internal/errs/code.go b/internal/roadmap/internal/errs/code.go new file mode 100644 index 00000000..3fc918c8 --- /dev/null +++ b/internal/roadmap/internal/errs/code.go @@ -0,0 +1,10 @@ +package errs + +var ( + SystemError = ErrorCode{Code: 513001, Msg: "系统错误"} +) + +type ErrorCode struct { + Code int + Msg string +} diff --git a/internal/roadmap/internal/integration/admin_test.go b/internal/roadmap/internal/integration/admin_test.go new file mode 100644 index 00000000..a5421995 --- /dev/null +++ b/internal/roadmap/internal/integration/admin_test.go @@ -0,0 +1,505 @@ +// 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. + +//go:build e2e + +package integration + +import ( + "context" + "fmt" + "net/http" + "testing" + "time" + + "github.com/ecodeclub/ekit/sqlx" + + "github.com/ecodeclub/ekit/iox" + "github.com/ecodeclub/ekit/slice" + baguwen "github.com/ecodeclub/webook/internal/question" + quemocks "github.com/ecodeclub/webook/internal/question/mocks" + "github.com/ecodeclub/webook/internal/roadmap/internal/domain" + "github.com/ecodeclub/webook/internal/roadmap/internal/integration/startup" + "github.com/ecodeclub/webook/internal/roadmap/internal/repository/dao" + "github.com/ecodeclub/webook/internal/roadmap/internal/web" + "github.com/ecodeclub/webook/internal/test" + testioc "github.com/ecodeclub/webook/internal/test/ioc" + "github.com/ego-component/egorm" + "github.com/gotomicro/ego/core/econf" + "github.com/gotomicro/ego/server/egin" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + "go.uber.org/mock/gomock" + "gorm.io/gorm" +) + +type AdminHandlerTestSuite struct { + suite.Suite + db *egorm.Component + server *egin.Component + hdl *web.AdminHandler + dao dao.AdminDAO + mockQueSetSvc *quemocks.MockQuestionSetService + mockQueSvc *quemocks.MockService +} + +func (s *AdminHandlerTestSuite) SetupSuite() { + ctrl := gomock.NewController(s.T()) + mockQueSvc := quemocks.NewMockService(ctrl) + // mockQueSvc 固定返回 + + mockQueSvc.EXPECT().GetPubByIDs(gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, ids []int64) ([]baguwen.Question, error) { + return slice.Map(ids, func(idx int, src int64) baguwen.Question { + return baguwen.Question{ + Id: src, + Title: fmt.Sprintf("题目%d", src), + } + }), nil + }).AnyTimes() + + mockQueSetSvc := quemocks.NewMockQuestionSetService(ctrl) + mockQueSetSvc.EXPECT().GetByIds(gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, ids []int64) ([]baguwen.QuestionSet, error) { + return slice.Map(ids, func(idx int, src int64) baguwen.QuestionSet { + return baguwen.QuestionSet{ + Id: src, + Title: fmt.Sprintf("题集%d", src), + } + }), nil + }).AnyTimes() + + m := startup.InitModule(&baguwen.Module{ + Svc: mockQueSvc, + SetSvc: mockQueSetSvc, + }) + s.hdl = m.AdminHdl + + econf.Set("server", map[string]any{"contextTimeout": "10s"}) + server := egin.Load("server").Build() + s.hdl.PrivateRoutes(server.Engine) + s.server = server + s.db = testioc.InitDB() + s.dao = dao.NewGORMAdminDAO(s.db) +} + +func (s *AdminHandlerTestSuite) TearDownTest() { + err := s.db.Exec("TRUNCATE TABLE roadmaps").Error + require.NoError(s.T(), err) + err = s.db.Exec("TRUNCATE TABLE roadmap_edges").Error + require.NoError(s.T(), err) +} + +func (s *AdminHandlerTestSuite) TestSave() { + testCases := []struct { + name string + before func(t *testing.T) + after func(t *testing.T) + + req web.Roadmap + wantCode int + wantResp test.Result[int64] + }{ + { + name: "新建", + before: func(t *testing.T) { + + }, + after: func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + r, err := s.dao.GetById(ctx, 1) + require.NoError(t, err) + assert.True(t, r.Utime > 0) + r.Utime = 0 + assert.True(t, r.Ctime > 0) + r.Ctime = 0 + assert.Equal(t, dao.Roadmap{ + Id: 1, + Title: "标题1", + Biz: sqlx.NewNullString("test"), + BizId: sqlx.NewNullInt64(123), + }, r) + }, + req: web.Roadmap{ + Title: "标题1", + Biz: "test", + BizId: 123, + }, + wantCode: 200, + wantResp: test.Result[int64]{ + Data: 1, + }, + }, + { + name: "更新", + before: func(t *testing.T) { + s.db.Create(&dao.Roadmap{ + Id: 2, + Title: "老的标题2", + Biz: sqlx.NewNullString("test-old"), + BizId: sqlx.NewNullInt64(124), + Ctime: 123, + Utime: 123, + }) + }, + after: func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + r, err := s.dao.GetById(ctx, 2) + require.NoError(t, err) + assert.True(t, r.Utime > 0) + r.Utime = 0 + assert.Equal(t, dao.Roadmap{ + Id: 2, + Title: "标题2", + Biz: sqlx.NewNullString("test"), + BizId: sqlx.NewNullInt64(125), + Ctime: 123, + }, r) + }, + req: web.Roadmap{ + Id: 2, + Title: "标题2", + Biz: "test", + BizId: 125, + }, + 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/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) TestList() { + testCases := []struct { + name string + before func(t *testing.T) + after func(t *testing.T) + + req web.Page + wantCode int + wantResp test.Result[web.RoadmapListResp] + }{ + { + name: "获取成功", + before: func(t *testing.T) { + // 在数据库中插入数据 + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + err := s.db.WithContext(ctx).Create(dao.Roadmap{ + Id: 1, + Title: "标题1", + Biz: sqlx.NewNullString(domain.BizQuestionSet), + BizId: sqlx.NewNullInt64(1), + Utime: 123, + }).Error + require.NoError(t, err) + err = s.db.WithContext(ctx).Create(dao.Roadmap{ + Id: 2, + Title: "标题2", + Biz: sqlx.NewNullString(domain.BizQuestionSet), + BizId: sqlx.NewNullInt64(2), + Utime: 123, + }).Error + require.NoError(t, err) + err = s.db.WithContext(ctx).Create(dao.Roadmap{ + Id: 3, + Title: "标题3", + Biz: sqlx.NewNullString(domain.BizQuestion), + BizId: sqlx.NewNullInt64(3), + Utime: 123, + }).Error + require.NoError(t, err) + }, + after: func(t *testing.T) { + + }, + req: web.Page{ + Offset: 0, + Limit: 3, + }, + wantCode: 200, + wantResp: test.Result[web.RoadmapListResp]{ + Data: web.RoadmapListResp{ + Total: 3, + Maps: []web.Roadmap{ + { + Id: 3, + Title: "标题3", + Biz: domain.BizQuestion, + BizId: 3, + BizTitle: "题目3", + Utime: 123, + }, + { + Id: 2, + Title: "标题2", + Biz: domain.BizQuestionSet, + BizId: 2, + BizTitle: "题集2", + Utime: 123, + }, + { + Id: 1, + Title: "标题1", + Biz: domain.BizQuestionSet, + BizId: 1, + BizTitle: "题集1", + Utime: 123, + }, + }, + }, + }, + }, + } + + for _, tc := range testCases { + s.T().Run(tc.name, func(t *testing.T) { + tc.before(t) + req, err := http.NewRequest(http.MethodPost, + "/roadmap/list", iox.NewJSONReader(tc.req)) + req.Header.Set("content-type", "application/json") + require.NoError(t, err) + recorder := test.NewJSONResponseRecorder[web.RoadmapListResp]() + 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) 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}, + {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}, + } + err = db.Create(&edges).Error + require.NoError(s.T(), err) + + testCases := []struct { + name string + + req web.IdReq + wantCode int + wantResp test.Result[web.Roadmap] + }{ + { + name: "获取成功", + req: web.IdReq{Id: 1}, + wantCode: 200, + wantResp: test.Result[web.Roadmap]{ + Data: web.Roadmap{ + Id: 1, + Title: "标题1", + Biz: domain.BizQuestion, + BizId: 123, + BizTitle: "题目123", + Utime: 222, + Edges: []web.Edge{ + { + Id: 1, + Src: web.Node{ + Biz: domain.BizQuestionSet, + BizId: 1, + Title: "题集1", + }, + Dst: web.Node{ + Biz: domain.BizQuestion, + BizId: 2, + Title: "题目2", + }, + }, + { + Id: 2, + Src: web.Node{ + BizId: 2, + Biz: domain.BizQuestion, + Title: "题目2", + }, + Dst: web.Node{ + BizId: 3, + Biz: domain.BizQuestionSet, + Title: "题集3", + }, + }, + }, + }, + }, + }, + } + + for _, tc := range testCases { + s.T().Run(tc.name, func(t *testing.T) { + req, err := http.NewRequest(http.MethodPost, + "/roadmap/detail", iox.NewJSONReader(tc.req)) + req.Header.Set("content-type", "application/json") + require.NoError(t, err) + recorder := test.NewJSONResponseRecorder[web.Roadmap]() + s.server.ServeHTTP(recorder, req) + require.Equal(t, tc.wantCode, recorder.Code) + assert.Equal(t, tc.wantResp, recorder.MustScan()) + }) + } +} + +func (s *AdminHandlerTestSuite) TestAddEdge() { + testCases := []struct { + name string + before func(t *testing.T) + after func(t *testing.T) + + req web.AddEdgeReq + wantCode int + wantResp test.Result[any] + }{ + { + name: "添加成功", + before: func(t *testing.T) { + + }, + after: func(t *testing.T) { + var edge dao.Edge + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + err := s.db.WithContext(ctx).Where("rid = ?", 1).First(&edge).Error + require.NoError(t, err) + assert.True(t, edge.Ctime > 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, + }, 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, + }, + }, + }, + wantCode: 200, + }, + } + + for _, tc := range testCases { + s.T().Run(tc.name, func(t *testing.T) { + tc.before(t) + req, err := http.NewRequest(http.MethodPost, + "/roadmap/edge/save", 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) TestDelete() { + 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) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + err := s.db.WithContext(ctx).Create(&dao.Edge{ + Id: 1, + }).Error + require.NoError(t, err) + }, + after: func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + var edge dao.Edge + err := s.db.WithContext(ctx).Where("id = ?", 1).First(&edge).Error + assert.Equal(t, gorm.ErrRecordNotFound, err) + }, + wantCode: 200, + req: web.IdReq{Id: 1}, + }, + } + for _, tc := range testCases { + s.T().Run(tc.name, func(t *testing.T) { + tc.before(t) + req, err := http.NewRequest(http.MethodPost, + "/roadmap/edge/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 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 new file mode 100644 index 00000000..02994a25 --- /dev/null +++ b/internal/roadmap/internal/integration/handler_test.go @@ -0,0 +1,189 @@ +// 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 integration + +import ( + "context" + "fmt" + "net/http" + "testing" + "time" + + "github.com/ecodeclub/ekit/iox" + "github.com/ecodeclub/ekit/slice" + "github.com/ecodeclub/ekit/sqlx" + baguwen "github.com/ecodeclub/webook/internal/question" + quemocks "github.com/ecodeclub/webook/internal/question/mocks" + "github.com/ecodeclub/webook/internal/roadmap/internal/domain" + "github.com/ecodeclub/webook/internal/roadmap/internal/integration/startup" + "github.com/ecodeclub/webook/internal/roadmap/internal/repository/dao" + "github.com/ecodeclub/webook/internal/roadmap/internal/web" + "github.com/ecodeclub/webook/internal/test" + testioc "github.com/ecodeclub/webook/internal/test/ioc" + "github.com/ego-component/egorm" + "github.com/gotomicro/ego/core/econf" + "github.com/gotomicro/ego/server/egin" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + "go.uber.org/mock/gomock" +) + +type HandlerTestSuite struct { + suite.Suite + db *egorm.Component + server *egin.Component + hdl *web.Handler + dao dao.AdminDAO +} + +func (s *HandlerTestSuite) SetupSuite() { + ctrl := gomock.NewController(s.T()) + mockQueSvc := quemocks.NewMockService(ctrl) + // mockQueSvc 固定返回 + + mockQueSvc.EXPECT().GetPubByIDs(gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, ids []int64) ([]baguwen.Question, error) { + return slice.Map(ids, func(idx int, src int64) baguwen.Question { + return baguwen.Question{ + Id: src, + Title: fmt.Sprintf("题目%d", src), + } + }), nil + }).AnyTimes() + + mockQueSetSvc := quemocks.NewMockQuestionSetService(ctrl) + mockQueSetSvc.EXPECT().GetByIds(gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, ids []int64) ([]baguwen.QuestionSet, error) { + return slice.Map(ids, func(idx int, src int64) baguwen.QuestionSet { + return baguwen.QuestionSet{ + Id: src, + Title: fmt.Sprintf("题集%d", src), + } + }), nil + }).AnyTimes() + + m := startup.InitModule(&baguwen.Module{ + Svc: mockQueSvc, + SetSvc: mockQueSetSvc, + }) + s.hdl = m.Hdl + + econf.Set("server", map[string]any{"contextTimeout": "10s"}) + server := egin.Load("server").Build() + s.hdl.PrivateRoutes(server.Engine) + s.server = server + s.db = testioc.InitDB() + s.dao = dao.NewGORMAdminDAO(s.db) +} + +func (s *HandlerTestSuite) TearDownTest() { + err := s.db.Exec("TRUNCATE TABLE roadmaps").Error + require.NoError(s.T(), err) + err = s.db.Exec("TRUNCATE TABLE roadmap_edges").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{ + 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}, + {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}, + } + err = db.Create(&edges).Error + require.NoError(s.T(), err) + + testCases := []struct { + name string + + req web.Biz + wantCode int + wantResp test.Result[web.Roadmap] + }{ + { + name: "获取成功", + req: web.Biz{BizId: 123, Biz: domain.BizQuestion}, + wantCode: 200, + wantResp: test.Result[web.Roadmap]{ + Data: web.Roadmap{ + Id: 1, + Title: "标题1", + Biz: domain.BizQuestion, + BizId: 123, + BizTitle: "题目123", + Utime: 222, + Edges: []web.Edge{ + { + Id: 1, + Src: web.Node{ + Biz: domain.BizQuestionSet, + BizId: 1, + Title: "题集1", + }, + Dst: web.Node{ + Biz: domain.BizQuestion, + BizId: 2, + Title: "题目2", + }, + }, + { + Id: 2, + Src: web.Node{ + BizId: 2, + Biz: domain.BizQuestion, + Title: "题目2", + }, + Dst: web.Node{ + BizId: 3, + Biz: domain.BizQuestionSet, + Title: "题集3", + }, + }, + }, + }, + }, + }, + } + + for _, tc := range testCases { + s.T().Run(tc.name, func(t *testing.T) { + req, err := http.NewRequest(http.MethodPost, + "/roadmap/detail", iox.NewJSONReader(tc.req)) + req.Header.Set("content-type", "application/json") + require.NoError(t, err) + recorder := test.NewJSONResponseRecorder[web.Roadmap]() + s.server.ServeHTTP(recorder, req) + require.Equal(t, tc.wantCode, recorder.Code) + assert.Equal(t, tc.wantResp, recorder.MustScan()) + }) + } +} + +func TestHandler(t *testing.T) { + suite.Run(t, new(HandlerTestSuite)) +} diff --git a/internal/roadmap/internal/integration/startup/wire.go b/internal/roadmap/internal/integration/startup/wire.go new file mode 100644 index 00000000..a16a8db0 --- /dev/null +++ b/internal/roadmap/internal/integration/startup/wire.go @@ -0,0 +1,32 @@ +// 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. + +//go:build wireinject + +package startup + +import ( + baguwen "github.com/ecodeclub/webook/internal/question" + "github.com/ecodeclub/webook/internal/roadmap" + testioc "github.com/ecodeclub/webook/internal/test/ioc" + "github.com/google/wire" +) + +func InitModule(queModule *baguwen.Module) *roadmap.Module { + wire.Build( + testioc.BaseSet, + roadmap.InitModule, + ) + return new(roadmap.Module) +} diff --git a/internal/roadmap/internal/integration/startup/wire_gen.go b/internal/roadmap/internal/integration/startup/wire_gen.go new file mode 100644 index 00000000..b71a0239 --- /dev/null +++ b/internal/roadmap/internal/integration/startup/wire_gen.go @@ -0,0 +1,21 @@ +// Code generated by Wire. DO NOT EDIT. + +//go:generate go run github.com/google/wire/cmd/wire +//go:build !wireinject +// +build !wireinject + +package startup + +import ( + baguwen "github.com/ecodeclub/webook/internal/question" + "github.com/ecodeclub/webook/internal/roadmap" + testioc "github.com/ecodeclub/webook/internal/test/ioc" +) + +// Injectors from wire.go: + +func InitModule(queModule *baguwen.Module) *roadmap.Module { + db := testioc.InitDB() + module := roadmap.InitModule(db, queModule) + return module +} diff --git a/internal/roadmap/internal/repository/admin.go b/internal/roadmap/internal/repository/admin.go new file mode 100644 index 00000000..2e7fe3f7 --- /dev/null +++ b/internal/roadmap/internal/repository/admin.go @@ -0,0 +1,108 @@ +// 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 repository + +import ( + "context" + + "github.com/ecodeclub/ekit/sqlx" + + "github.com/ecodeclub/ekit/slice" + "github.com/ecodeclub/webook/internal/roadmap/internal/domain" + "github.com/ecodeclub/webook/internal/roadmap/internal/repository/dao" + "golang.org/x/sync/errgroup" +) + +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 +} + +var _ AdminRepository = &CachedAdminRepository{} + +// CachedAdminRepository 虽然还没缓存,但是将来肯定要有缓存的 +type CachedAdminRepository struct { + converter + dao dao.AdminDAO +} + +func (repo *CachedAdminRepository) DeleteEdge(ctx context.Context, id int64) error { + return repo.dao.DeleteEdge(ctx, id) +} + +func (repo *CachedAdminRepository) AddEdge(ctx context.Context, rid int64, edge domain.Edge) error { + return repo.dao.AddEdge(ctx, dao.Edge{ + Rid: rid, + SrcId: edge.Src.BizId, + SrcBiz: edge.Src.Biz.Biz, + DstId: edge.Dst.BizId, + DstBiz: edge.Dst.Biz.Biz, + }) +} + +func (repo *CachedAdminRepository) GetById(ctx context.Context, id int64) (domain.Roadmap, error) { + var ( + eg errgroup.Group + r dao.Roadmap + edges []dao.Edge + ) + eg.Go(func() error { + var err error + r, err = repo.dao.GetById(ctx, id) + return err + }) + + eg.Go(func() error { + var err error + edges, err = repo.dao.GetEdgesByRid(ctx, id) + return err + }) + err := eg.Wait() + if err != nil { + return domain.Roadmap{}, err + } + res := repo.toDomain(r) + res.Edges = repo.edgesToDomain(edges) + return res, nil +} + +func (repo *CachedAdminRepository) List(ctx context.Context, offset int, limit int) ([]domain.Roadmap, error) { + rs, err := repo.dao.List(ctx, offset, limit) + return slice.Map(rs, func(idx int, src dao.Roadmap) domain.Roadmap { + return repo.toDomain(src) + }), err +} + +func (repo *CachedAdminRepository) Save(ctx context.Context, r domain.Roadmap) (int64, error) { + return repo.dao.Save(ctx, repo.toEntity(r)) +} + +func (repo *CachedAdminRepository) toEntity(r domain.Roadmap) dao.Roadmap { + return dao.Roadmap{ + Id: r.Id, + Title: r.Title, + Biz: sqlx.NewNullString(r.Biz), + BizId: sqlx.NewNullInt64(r.BizId), + } +} + +func NewCachedAdminRepository(dao dao.AdminDAO) AdminRepository { + return &CachedAdminRepository{ + dao: dao, + } +} diff --git a/internal/roadmap/internal/repository/converter.go b/internal/roadmap/internal/repository/converter.go new file mode 100644 index 00000000..702fb331 --- /dev/null +++ b/internal/roadmap/internal/repository/converter.go @@ -0,0 +1,55 @@ +// 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 repository + +import ( + "github.com/ecodeclub/ekit/slice" + "github.com/ecodeclub/webook/internal/roadmap/internal/domain" + "github.com/ecodeclub/webook/internal/roadmap/internal/repository/dao" +) + +// 公共的转换放过来这里 +type converter struct { +} + +func (converter) toDomain(r dao.Roadmap) domain.Roadmap { + return domain.Roadmap{ + Id: r.Id, + Title: r.Title, + Biz: r.Biz.String, + BizId: r.BizId.Int64, + Utime: r.Utime, + } +} + +func (converter) edgesToDomain(edges []dao.Edge) []domain.Edge { + return slice.Map(edges, func(idx int, edge dao.Edge) domain.Edge { + 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, + }, + }, + } + }) +} diff --git a/internal/roadmap/internal/repository/dao/admin.go b/internal/roadmap/internal/repository/dao/admin.go new file mode 100644 index 00000000..7e48f937 --- /dev/null +++ b/internal/roadmap/internal/repository/dao/admin.go @@ -0,0 +1,84 @@ +// 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 dao + +import ( + "context" + "time" + + "github.com/ego-component/egorm" + "gorm.io/gorm/clause" +) + +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) + GetEdgesByRid(ctx context.Context, rid int64) ([]Edge, error) + AddEdge(ctx context.Context, edge Edge) error + DeleteEdge(ctx context.Context, id int64) error +} + +var _ AdminDAO = &GORMAdminDAO{} + +type GORMAdminDAO struct { + db *egorm.Component +} + +func (dao *GORMAdminDAO) DeleteEdge(ctx context.Context, id int64) error { + return dao.db.WithContext(ctx).Where("id = ?", id).Delete(&Edge{}).Error +} + +func (dao *GORMAdminDAO) AddEdge(ctx context.Context, edge Edge) error { + now := time.Now().UnixMilli() + edge.Utime = now + edge.Ctime = now + return dao.db.WithContext(ctx).Create(&edge).Error +} + +func (dao *GORMAdminDAO) 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 *GORMAdminDAO) List(ctx context.Context, offset int, limit int) ([]Roadmap, error) { + var res []Roadmap + err := dao.db.WithContext(ctx).Order("id DESC").Offset(offset).Limit(limit).Find(&res).Error + return res, err +} + +func (dao *GORMAdminDAO) GetById(ctx context.Context, id int64) (Roadmap, error) { + var r Roadmap + err := dao.db.WithContext(ctx).Where("id = ?", id).First(&r).Error + return r, err +} + +func (dao *GORMAdminDAO) Save(ctx context.Context, r Roadmap) (int64, error) { + now := time.Now().UnixMilli() + r.Ctime = now + r.Utime = now + err := dao.db.WithContext(ctx). + Clauses(clause.OnConflict{ + DoUpdates: clause.AssignmentColumns([]string{"title", "biz", "biz_id", "utime"}), + }).Create(&r).Error + return r.Id, err +} + +func NewGORMAdminDAO(db *egorm.Component) AdminDAO { + return &GORMAdminDAO{ + db: db, + } +} diff --git a/internal/roadmap/internal/repository/dao/dao.go b/internal/roadmap/internal/repository/dao/dao.go new file mode 100644 index 00000000..70ab98e5 --- /dev/null +++ b/internal/roadmap/internal/repository/dao/dao.go @@ -0,0 +1,53 @@ +// 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 dao + +import ( + "context" + + "github.com/ego-component/egorm" + "gorm.io/gorm" +) + +var ErrRecordNotFound = gorm.ErrRecordNotFound + +type RoadmapDAO interface { + GetEdgesByRid(ctx context.Context, rid int64) ([]Edge, error) + GetByBiz(ctx context.Context, biz string, bizId int64) (Roadmap, error) +} + +var _ RoadmapDAO = &GORMRoadmapDAO{} + +type GORMRoadmapDAO struct { + db *egorm.Component +} + +func (dao *GORMRoadmapDAO) GetByBiz(ctx context.Context, biz string, bizId int64) (Roadmap, error) { + var r Roadmap + err := dao.db.WithContext(ctx). + Where("biz = ? AND biz_id = ?", biz, bizId). + First(&r).Error + 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 NewGORMRoadmapDAO(db *egorm.Component) RoadmapDAO { + return &GORMRoadmapDAO{db: db} +} diff --git a/internal/roadmap/internal/repository/dao/init.go b/internal/roadmap/internal/repository/dao/init.go new file mode 100644 index 00000000..9ce978bf --- /dev/null +++ b/internal/roadmap/internal/repository/dao/init.go @@ -0,0 +1,21 @@ +// 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 dao + +import "github.com/ego-component/egorm" + +func InitTables(db *egorm.Component) error { + return db.AutoMigrate(&Roadmap{}, &Edge{}) +} diff --git a/internal/roadmap/internal/repository/dao/types.go b/internal/roadmap/internal/repository/dao/types.go new file mode 100644 index 00000000..851df9ad --- /dev/null +++ b/internal/roadmap/internal/repository/dao/types.go @@ -0,0 +1,59 @@ +// 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 dao + +import "database/sql" + +// Roadmap 后续要考虑引入制作库,线上库 +type Roadmap struct { + Id int64 `gorm:"primaryKey,autoIncrement"` + Title string + // 关联的 ID,不一定都有 + // 例如说这个是专门给题集用的,这里就是代表题集的 ID + // 唯一索引确保一个业务不会有两个业务图 + BizId sql.NullInt64 `gorm:"uniqueIndex:biz"` + Biz sql.NullString `gorm:"type:varchar(128);uniqueIndex:biz"` + + Ctime int64 + Utime int64 +} + +func (r Roadmap) TableName() string { + return "roadmaps" +} + +type Edge struct { + Id int64 `gorm:"primaryKey,autoIncrement"` + + // 理论上来说 Edge 中的 Rid, Src, Dst 构成一个唯一索引。 + // 但是因为都是内部在操作,所以没太大必要真的建立这个唯一索引 + // Roadmap 的 ID + Rid int64 `gorm:"index"` + + // 源头 + SrcId int64 `gorm:"index:src"` + SrcBiz string `gorm:"type:varchar(128);index:src"` + + // 目标 + DstId int64 `gorm:"index:dst"` + DstBiz string `gorm:"type:varchar(128);index:dst"` + + Utime int64 + Ctime int64 +} + +func (e Edge) TableName() string { + return "roadmap_edges" +} diff --git a/internal/roadmap/internal/repository/repository.go b/internal/roadmap/internal/repository/repository.go new file mode 100644 index 00000000..53816eee --- /dev/null +++ b/internal/roadmap/internal/repository/repository.go @@ -0,0 +1,53 @@ +// 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 repository + +import ( + "context" + + "github.com/ecodeclub/webook/internal/roadmap/internal/domain" + "github.com/ecodeclub/webook/internal/roadmap/internal/repository/dao" +) + +var ErrRoadmapNotFound = dao.ErrRecordNotFound + +type Repository interface { + GetByBiz(ctx context.Context, biz string, bizId int64) (domain.Roadmap, error) +} + +var _ Repository = &CachedRepository{} + +type CachedRepository struct { + converter + dao dao.RoadmapDAO +} + +func (repo *CachedRepository) GetByBiz(ctx context.Context, biz string, bizId int64) (domain.Roadmap, error) { + r, err := repo.dao.GetByBiz(ctx, biz, bizId) + if err != nil { + return domain.Roadmap{}, err + } + edges, err := repo.dao.GetEdgesByRid(ctx, r.Id) + if err != nil { + return domain.Roadmap{}, err + } + res := repo.toDomain(r) + res.Edges = repo.edgesToDomain(edges) + return res, nil +} + +func NewCachedRepository(dao dao.RoadmapDAO) Repository { + return &CachedRepository{dao: dao} +} diff --git a/internal/roadmap/internal/service/admin.go b/internal/roadmap/internal/service/admin.go new file mode 100644 index 00000000..ac296223 --- /dev/null +++ b/internal/roadmap/internal/service/admin.go @@ -0,0 +1,62 @@ +// 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" + + "github.com/ecodeclub/webook/internal/roadmap/internal/domain" + "github.com/ecodeclub/webook/internal/roadmap/internal/repository" +) + +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 + DeleteEdge(ctx context.Context, id int64) error +} + +var _ AdminService = &adminService{} + +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) AddEdge(ctx context.Context, rid int64, edge domain.Edge) error { + return svc.repo.AddEdge(ctx, rid, edge) +} + +func (svc *adminService) Detail(ctx context.Context, id int64) (domain.Roadmap, error) { + return svc.repo.GetById(ctx, id) +} + +func (svc *adminService) List(ctx context.Context, offset int, limit int) ([]domain.Roadmap, error) { + return svc.repo.List(ctx, offset, limit) +} + +func (svc *adminService) Save(ctx context.Context, r domain.Roadmap) (int64, error) { + return svc.repo.Save(ctx, r) +} + +func NewAdminService(repo repository.AdminRepository) AdminService { + return &adminService{ + repo: repo, + } +} diff --git a/internal/roadmap/internal/service/biz.go b/internal/roadmap/internal/service/biz.go new file mode 100644 index 00000000..ada12343 --- /dev/null +++ b/internal/roadmap/internal/service/biz.go @@ -0,0 +1,126 @@ +// 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/service.go b/internal/roadmap/internal/service/service.go new file mode 100644 index 00000000..0146f00f --- /dev/null +++ b/internal/roadmap/internal/service/service.go @@ -0,0 +1,42 @@ +// 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" + + "github.com/ecodeclub/webook/internal/roadmap/internal/domain" + "github.com/ecodeclub/webook/internal/roadmap/internal/repository" +) + +var ErrRoadmapNotFound = repository.ErrRoadmapNotFound + +type Service interface { + Detail(ctx context.Context, biz string, bizId int64) (domain.Roadmap, error) +} + +var _ Service = &service{} + +type service struct { + repo repository.Repository +} + +func (svc *service) Detail(ctx context.Context, biz string, bizId int64) (domain.Roadmap, error) { + return svc.repo.GetByBiz(ctx, biz, bizId) +} + +func NewService(repo repository.Repository) Service { + return &service{repo: repo} +} diff --git a/internal/roadmap/internal/web/admin.go b/internal/roadmap/internal/web/admin.go new file mode 100644 index 00000000..18af3476 --- /dev/null +++ b/internal/roadmap/internal/web/admin.go @@ -0,0 +1,120 @@ +// 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 web + +import ( + "github.com/ecodeclub/ekit/slice" + "github.com/ecodeclub/ginx" + "github.com/ecodeclub/webook/internal/roadmap/internal/domain" + "github.com/ecodeclub/webook/internal/roadmap/internal/service" + "github.com/gin-gonic/gin" +) + +type AdminHandler struct { + svc service.AdminService + bizSvc service.BizService +} + +func (h *AdminHandler) PrivateRoutes(server *gin.Engine) { + g := server.Group("/roadmap") + g.POST("/save", ginx.B(h.Save)) + g.POST("/list", ginx.B(h.List)) + g.POST("/detail", ginx.B(h.Detail)) + + edge := g.Group("/edge") + edge.POST("/save", ginx.B(h.AddEdge)) + edge.POST("/delete", ginx.B(h.DeleteEdge)) +} + +func (h *AdminHandler) Save(ctx *ginx.Context, req Roadmap) (ginx.Result, error) { + id, err := h.svc.Save(ctx, req.toDomain()) + if err != nil { + return systemErrorResult, err + } + return ginx.Result{ + Data: id, + }, nil +} + +func (h *AdminHandler) List(ctx *ginx.Context, req Page) (ginx.Result, error) { + rs, err := h.svc.List(ctx, req.Offset, req.Limit) + if err != nil { + return systemErrorResult, err + } + bizs := make([]string, 0, len(rs)) + bizIds := make([]int64, 0, len(rs)) + for _, r := range rs { + bizs = append(bizs, r.Biz) + bizIds = append(bizIds, r.BizId) + } + // 获取 biz 对应的信息 + bizsMap, err := h.bizSvc.GetBizs(ctx, bizs, bizIds) + if err != nil { + return systemErrorResult, err + } + return ginx.Result{ + Data: RoadmapListResp{ + Total: len(rs), + Maps: slice.Map(rs, func(idx int, src domain.Roadmap) Roadmap { + res := newRoadmap(src) + res.BizTitle = bizsMap[src.Biz][src.BizId].Title + return res + }), + }, + }, 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()) + if err != nil { + return systemErrorResult, err + } + return ginx.Result{}, nil +} + +func (h *AdminHandler) DeleteEdge(ctx *ginx.Context, req IdReq) (ginx.Result, error) { + err := h.svc.DeleteEdge(ctx, req.Id) + if err != nil { + return systemErrorResult, err + } + return ginx.Result{}, nil +} + +func (h *AdminHandler) Detail(ctx *ginx.Context, req IdReq) (ginx.Result, error) { + r, err := h.svc.Detail(ctx, req.Id) + if err != nil { + return systemErrorResult, err + } + bizs, bizIds := r.Bizs() + bizMap, err := h.bizSvc.GetBizs(ctx, bizs, bizIds) + if err != nil { + return systemErrorResult, err + } + + rm := newRoadmapWithBiz(r, bizMap) + return ginx.Result{ + Data: rm, + }, nil +} + +func NewAdminHandler( + svc service.AdminService, + bizSvc service.BizService) *AdminHandler { + return &AdminHandler{ + svc: svc, + bizSvc: bizSvc, + } +} diff --git a/internal/roadmap/internal/web/handler.go b/internal/roadmap/internal/web/handler.go new file mode 100644 index 00000000..7048a31c --- /dev/null +++ b/internal/roadmap/internal/web/handler.go @@ -0,0 +1,60 @@ +// 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 web + +import ( + "github.com/ecodeclub/ginx" + "github.com/ecodeclub/webook/internal/roadmap/internal/service" + "github.com/gin-gonic/gin" +) + +type Handler struct { + svc service.Service + bizSvc service.BizService +} + +func (h *Handler) PrivateRoutes(server *gin.Engine) { + g := server.Group("/roadmap") + g.POST("/detail", ginx.B(h.Detail)) +} + +func (h *Handler) Detail(ctx *ginx.Context, req Biz) (ginx.Result, error) { + r, err := h.svc.Detail(ctx, req.Biz, req.BizId) + switch err { + case service.ErrRoadmapNotFound: + // 没有 + return ginx.Result{}, nil + case nil: + bizs, bizIds := r.Bizs() + bizMap, err := h.bizSvc.GetBizs(ctx, bizs, bizIds) + if err != nil { + return systemErrorResult, err + } + + rm := newRoadmapWithBiz(r, bizMap) + return ginx.Result{ + Data: rm, + }, nil + default: + return systemErrorResult, err + } +} + +func NewHandler(svc service.Service, bizSvc service.BizService) *Handler { + return &Handler{ + svc: svc, + bizSvc: bizSvc, + } +} diff --git a/internal/roadmap/internal/web/result.go b/internal/roadmap/internal/web/result.go new file mode 100644 index 00000000..c9d0c1ca --- /dev/null +++ b/internal/roadmap/internal/web/result.go @@ -0,0 +1,27 @@ +// 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 web + +import ( + "github.com/ecodeclub/ginx" + "github.com/ecodeclub/webook/internal/roadmap/internal/errs" +) + +var ( + systemErrorResult = ginx.Result{ + Code: errs.SystemError.Code, + Msg: errs.SystemError.Msg, + } +) diff --git a/internal/roadmap/internal/web/vo.go b/internal/roadmap/internal/web/vo.go new file mode 100644 index 00000000..e0639201 --- /dev/null +++ b/internal/roadmap/internal/web/vo.go @@ -0,0 +1,129 @@ +// 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 web + +import ( + "github.com/ecodeclub/ekit/slice" + "github.com/ecodeclub/webook/internal/roadmap/internal/domain" +) + +type AddEdgeReq struct { + // roadmap 的 ID + Rid int64 + Edge Edge +} + +type Page struct { + Offset int `json:"offset,omitempty"` + Limit int `json:"limit,omitempty"` +} + +type RoadmapListResp struct { + Total int `json:"total,omitempty"` + Maps []Roadmap `json:"maps,omitempty"` +} + +type Roadmap struct { + Id int64 `json:"id"` + Title string `json:"title"` + Biz string `json:"biz"` + BizId int64 `json:"bizId"` + BizTitle string `json:"bizTitle"` + Utime int64 `json:"utime"` + Edges []Edge `json:"edges"` +} + +func newRoadmapWithBiz(r domain.Roadmap, + bizMap map[string]map[int64]domain.Biz) Roadmap { + rm := newRoadmap(r) + rm.BizTitle = bizMap[r.Biz][r.BizId].Title + rm.Edges = slice.Map(r.Edges, func(idx int, edge domain.Edge) Edge { + src := newNode(edge.Src) + src.Title = bizMap[src.Biz][src.BizId].Title + dst := newNode(edge.Dst) + dst.Title = bizMap[dst.Biz][dst.BizId].Title + return Edge{ + Id: edge.Id, + Src: src, + Dst: dst, + } + }) + return rm +} + +func newRoadmap(r domain.Roadmap) Roadmap { + return Roadmap{ + Id: r.Id, + Title: r.Title, + Biz: r.Biz, + BizId: r.BizId, + Utime: r.Utime, + } +} + +func (r Roadmap) toDomain() domain.Roadmap { + return domain.Roadmap{ + Id: r.Id, + Title: r.Title, + Biz: r.Biz, + BizId: r.BizId, + Utime: r.Utime, + } +} + +type IdReq struct { + Id int64 `json:"id,omitempty"` +} + +type Node struct { + BizId int64 `json:"bizId"` + Biz string `json:"biz"` + Title string `json:"title"` +} + +func (n Node) toDomain() domain.Node { + return domain.Node{ + Biz: domain.Biz{ + BizId: n.BizId, + Biz: n.Biz, + }, + } +} + +func newNode(node domain.Node) Node { + return Node{ + 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"` +} + +func (e Edge) toDomain() domain.Edge { + return domain.Edge{ + Src: e.Src.toDomain(), + Dst: e.Dst.toDomain(), + } +} + +type Biz struct { + Biz string `json:"biz"` + BizId int64 `json:"bizId"` +} diff --git a/internal/roadmap/types.go b/internal/roadmap/types.go new file mode 100644 index 00000000..13084005 --- /dev/null +++ b/internal/roadmap/types.go @@ -0,0 +1,27 @@ +// 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 roadmap + +import ( + "github.com/ecodeclub/webook/internal/roadmap/internal/web" +) + +type Module struct { + AdminHdl *AdminHandler + Hdl *Handler +} + +type AdminHandler = web.AdminHandler +type Handler = web.Handler diff --git a/internal/roadmap/wire.go b/internal/roadmap/wire.go new file mode 100644 index 00000000..25208ddb --- /dev/null +++ b/internal/roadmap/wire.go @@ -0,0 +1,64 @@ +// 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. + +//go:build wireinject + +package roadmap + +import ( + "sync" + + baguwen "github.com/ecodeclub/webook/internal/question" + "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/web" + "github.com/ego-component/egorm" + "github.com/google/wire" +) + +func InitModule(db *egorm.Component, queModule *baguwen.Module) *Module { + wire.Build( + web.NewAdminHandler, + service.NewAdminService, + service.NewConcurrentBizService, + repository.NewCachedAdminRepository, + initAdminDAO, + + web.NewHandler, + service.NewService, + repository.NewCachedRepository, + dao.NewGORMRoadmapDAO, + + wire.Struct(new(Module), "*"), + wire.FieldsOf(new(*baguwen.Module), "Svc", "SetSvc"), + ) + return new(Module) +} + +var ( + adminDAO dao.AdminDAO + daoInitOnce sync.Once +) + +func initAdminDAO(db *egorm.Component) dao.AdminDAO { + daoInitOnce.Do(func() { + err := dao.InitTables(db) + if err != nil { + panic(err) + } + adminDAO = dao.NewGORMAdminDAO(db) + }) + return adminDAO +} diff --git a/internal/roadmap/wire_gen.go b/internal/roadmap/wire_gen.go new file mode 100644 index 00000000..1a5a9cdb --- /dev/null +++ b/internal/roadmap/wire_gen.go @@ -0,0 +1,58 @@ +// Code generated by Wire. DO NOT EDIT. + +//go:generate go run github.com/google/wire/cmd/wire +//go:build !wireinject +// +build !wireinject + +package roadmap + +import ( + "sync" + + baguwen "github.com/ecodeclub/webook/internal/question" + "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/web" + "github.com/ego-component/egorm" + "gorm.io/gorm" +) + +// Injectors from wire.go: + +func InitModule(db *gorm.DB, queModule *baguwen.Module) *Module { + daoAdminDAO := initAdminDAO(db) + adminRepository := repository.NewCachedAdminRepository(daoAdminDAO) + adminService := service.NewAdminService(adminRepository) + serviceService := queModule.Svc + questionSetService := queModule.SetSvc + bizService := service.NewConcurrentBizService(serviceService, questionSetService) + adminHandler := web.NewAdminHandler(adminService, bizService) + roadmapDAO := dao.NewGORMRoadmapDAO(db) + repositoryRepository := repository.NewCachedRepository(roadmapDAO) + service2 := service.NewService(repositoryRepository) + handler := web.NewHandler(service2, bizService) + module := &Module{ + AdminHdl: adminHandler, + Hdl: handler, + } + return module +} + +// wire.go: + +var ( + adminDAO dao.AdminDAO + daoInitOnce sync.Once +) + +func initAdminDAO(db *egorm.Component) dao.AdminDAO { + daoInitOnce.Do(func() { + err := dao.InitTables(db) + if err != nil { + panic(err) + } + adminDAO = dao.NewGORMAdminDAO(db) + }) + return adminDAO +} diff --git a/internal/search/internal/event/consumer.go b/internal/search/internal/event/consumer.go index 9b20646b..4f9334c7 100644 --- a/internal/search/internal/event/consumer.go +++ b/internal/search/internal/event/consumer.go @@ -18,8 +18,8 @@ import ( "context" "encoding/json" "fmt" - "log" "strconv" + "strings" "github.com/ecodeclub/mq-api" "github.com/ecodeclub/webook/internal/search/internal/service" @@ -56,7 +56,6 @@ func (s *SyncConsumer) Consume(ctx context.Context) error { if err != nil { return fmt.Errorf("解析消息失败: %w", err) } - log.Println("xxxxxxxx", evt) indexName := getIndexName(evt.Biz) docId := strconv.Itoa(evt.BizID) err = s.svc.Input(ctx, indexName, docId, evt.Data) @@ -81,5 +80,5 @@ func (s *SyncConsumer) Stop(_ context.Context) error { } func getIndexName(biz string) string { - return fmt.Sprintf("%s_index", biz) + return fmt.Sprintf("%s_index", strings.ToLower(biz)) } diff --git a/ioc/admin.go b/ioc/admin.go index a339a74b..7238f8f9 100644 --- a/ioc/admin.go +++ b/ioc/admin.go @@ -18,6 +18,8 @@ import ( "net/http" "strings" + "github.com/ecodeclub/webook/internal/roadmap" + "github.com/ecodeclub/ginx" "github.com/ecodeclub/ginx/session" "github.com/ecodeclub/webook-private/nonsense" @@ -32,6 +34,7 @@ import ( type AdminServer *egin.Component func InitAdminServer(prj *project.AdminHandler, + rm *roadmap.AdminHandler, mark *marketing.AdminHandler) AdminServer { res := egin.Load("admin").Build() res.Use(cors.New(cors.Config{ @@ -57,6 +60,7 @@ func InitAdminServer(prj *project.AdminHandler, res.Use(AdminPermission()) prj.PrivateRoutes(res.Engine) mark.PrivateRoutes(res.Engine) + rm.PrivateRoutes(res.Engine) return res } diff --git a/ioc/gin.go b/ioc/gin.go index 8522983e..4339f8cd 100644 --- a/ioc/gin.go +++ b/ioc/gin.go @@ -18,6 +18,8 @@ import ( "net/http" "strings" + "github.com/ecodeclub/webook/internal/roadmap" + "github.com/ecodeclub/webook/internal/search" "github.com/ecodeclub/ginx/middlewares/activelimit/locallimit" @@ -74,6 +76,7 @@ func initGinxServer(sp session.Provider, marketingHdl *marketing.Handler, intrHdl *interactive.Handler, searchHdl *search.Handler, + roadmapHdl *roadmap.Handler, ) *egin.Component { session.SetDefaultProvider(sp) res := egin.Load("web").Build() @@ -121,6 +124,7 @@ func initGinxServer(sp session.Provider, pHdl.PrivateRoutes(res.Engine) orderHdl.PrivateRoutes(res.Engine) searchHdl.PrivateRoutes(res.Engine) + roadmapHdl.PrivateRoutes(res.Engine) creditHdl.PrivateRoutes(res.Engine) marketingHdl.PrivateRoutes(res.Engine) diff --git a/ioc/wire.go b/ioc/wire.go index c68df3ce..a00462da 100644 --- a/ioc/wire.go +++ b/ioc/wire.go @@ -33,6 +33,7 @@ import ( "github.com/ecodeclub/webook/internal/project" baguwen "github.com/ecodeclub/webook/internal/question" "github.com/ecodeclub/webook/internal/recon" + "github.com/ecodeclub/webook/internal/roadmap" "github.com/ecodeclub/webook/internal/search" "github.com/ecodeclub/webook/internal/skill" "github.com/google/wire" @@ -77,6 +78,8 @@ func InitApp() (*App, error) { middleware.NewCheckPermissionMiddlewareBuilder, search.InitModule, wire.FieldsOf(new(*search.Module), "Hdl"), + roadmap.InitModule, + wire.FieldsOf(new(*roadmap.Module), "Hdl", "AdminHdl"), initLocalActiveLimiterBuilder, initCronJobs, // 这两个顺序不要换 diff --git a/ioc/wire_gen.go b/ioc/wire_gen.go index 34f2b63e..fd52f563 100644 --- a/ioc/wire_gen.go +++ b/ioc/wire_gen.go @@ -1,6 +1,6 @@ // Code generated by Wire. DO NOT EDIT. -//go:generate go run -mod=mod github.com/google/wire/cmd/wire +//go:generate go run github.com/google/wire/cmd/wire //go:build !wireinject // +build !wireinject @@ -23,6 +23,7 @@ import ( "github.com/ecodeclub/webook/internal/project" baguwen "github.com/ecodeclub/webook/internal/question" "github.com/ecodeclub/webook/internal/recon" + "github.com/ecodeclub/webook/internal/roadmap" "github.com/ecodeclub/webook/internal/search" "github.com/ecodeclub/webook/internal/skill" "github.com/google/wire" @@ -113,10 +114,13 @@ func InitApp() (*App, error) { return nil, err } handler14 := searchModule.Hdl - component := initGinxServer(provider, checkMembershipMiddlewareBuilder, localActiveLimit, checkPermissionMiddlewareBuilder, handler, questionSetHandler, webHandler, handler2, handler3, handler4, handler5, handler6, handler7, handler8, handler9, handler10, handler11, handler12, handler13, handler14) + roadmapModule := roadmap.InitModule(db, baguwenModule) + handler15 := roadmapModule.Hdl + component := initGinxServer(provider, checkMembershipMiddlewareBuilder, localActiveLimit, checkPermissionMiddlewareBuilder, handler, questionSetHandler, webHandler, handler2, handler3, handler4, handler5, handler6, handler7, handler8, handler9, handler10, handler11, handler12, handler13, handler14, handler15) adminHandler := projectModule.AdminHdl - webAdminHandler := marketingModule.AdminHdl - adminServer := InitAdminServer(adminHandler, webAdminHandler) + webAdminHandler := roadmapModule.AdminHdl + adminHandler2 := marketingModule.AdminHdl + adminServer := InitAdminServer(adminHandler, webAdminHandler, adminHandler2) closeTimeoutOrdersJob := orderModule.CloseTimeoutOrdersJob closeTimeoutLockedCreditsJob := creditModule.CloseTimeoutLockedCreditsJob syncWechatOrderJob := paymentModule.SyncWechatOrderJob