From 0a37181db5791ac20548be8723a1f5d31654c102 Mon Sep 17 00:00:00 2001 From: wenliang zhu <73632785+juniaoshaonian@users.noreply.github.com> Date: Wed, 7 Aug 2024 13:02:04 +0800 Subject: [PATCH] =?UTF-8?q?Cases=20=E6=A1=88=E4=BE=8B=E9=9B=86=E5=92=8C?= =?UTF-8?q?=E8=BF=9B=E5=BA=A6=E5=8A=9F=E8=83=BD=E6=B7=BB=E5=8A=A0=20(#249)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 添加案例集 * 添加候选案例 * 为案例集添加进度 * maka check * 添加githubrepo --- internal/ai/handlers.go | 12 +- internal/ai/internal/domain/llm.go | 1 + .../internal/integration/llm_service_test.go | 89 ++- .../ai/internal/integration/startup/wire.go | 2 + .../internal/integration/startup/wire_gen.go | 4 +- .../service/llm/handler/biz/case_examine.go | 33 + internal/cases/internal/domain/case_set.go | 22 + internal/cases/internal/domain/cases.go | 5 +- internal/cases/internal/domain/const.go | 5 + internal/cases/internal/domain/examine.go | 31 + internal/cases/internal/errs/code.go | 2 + internal/cases/internal/event/event.go | 6 +- .../admin_case_set_handler_test.go | 750 ++++++++++++++++++ .../integration/case_set_handler_test.go | 691 ++++++++++++++++ .../integration/examine_handler_test.go | 255 ++++++ .../internal/integration/handler_test.go | 265 ++++--- .../internal/integration/startup/wire.go | 33 + .../internal/integration/startup/wire_gen.go | 45 +- .../cases/internal/repository/case_set.go | 181 +++++ internal/cases/internal/repository/cases.go | 38 +- .../cases/internal/repository/dao/cases.go | 24 +- .../internal/repository/dao/cases_set.go | 138 ++++ .../internal/repository/dao/exam_type.go | 32 + .../cases/internal/repository/dao/examine.go | 64 ++ .../cases/internal/repository/dao/init.go | 6 + .../cases/internal/repository/dao/types.go | 32 +- internal/cases/internal/repository/examine.go | 59 ++ internal/cases/internal/service/case_set.go | 102 +++ internal/cases/internal/service/examine.go | 125 +++ .../internal/web/admin_case_set_handler.go | 130 +++ .../cases/internal/web/case_set_handler.go | 148 ++++ internal/cases/internal/web/exam_handler.go | 44 + internal/cases/internal/web/handler.go | 5 +- internal/cases/internal/web/vo.go | 99 ++- internal/cases/module.go | 7 +- internal/cases/wire.go | 16 + internal/cases/wire_gen.go | 28 +- internal/search/internal/domain/type.go | 11 +- .../internal/integration/handler_test.go | 477 +++++------ internal/search/internal/repository/case.go | 27 +- .../search/internal/repository/dao/case.go | 27 +- .../internal/repository/dao/case_index.json | 5 +- internal/search/internal/web/vo.go | 54 +- ioc/wire_gen.go | 4 +- 44 files changed, 3708 insertions(+), 426 deletions(-) create mode 100644 internal/ai/internal/service/llm/handler/biz/case_examine.go create mode 100644 internal/cases/internal/domain/case_set.go create mode 100644 internal/cases/internal/domain/const.go create mode 100644 internal/cases/internal/domain/examine.go create mode 100644 internal/cases/internal/integration/admin_case_set_handler_test.go create mode 100644 internal/cases/internal/integration/case_set_handler_test.go create mode 100644 internal/cases/internal/integration/examine_handler_test.go create mode 100644 internal/cases/internal/repository/case_set.go create mode 100644 internal/cases/internal/repository/dao/cases_set.go create mode 100644 internal/cases/internal/repository/dao/exam_type.go create mode 100644 internal/cases/internal/repository/dao/examine.go create mode 100644 internal/cases/internal/repository/examine.go create mode 100644 internal/cases/internal/service/case_set.go create mode 100644 internal/cases/internal/service/examine.go create mode 100644 internal/cases/internal/web/admin_case_set_handler.go create mode 100644 internal/cases/internal/web/case_set_handler.go create mode 100644 internal/cases/internal/web/exam_handler.go diff --git a/internal/ai/handlers.go b/internal/ai/handlers.go index 4fec44bc..4ae2a14e 100644 --- a/internal/ai/handlers.go +++ b/internal/ai/handlers.go @@ -56,8 +56,16 @@ func InitQuestionExamineHandler( // log -> cfg -> credit -> record -> question_examine -> platform builder := biz.NewQuestionExamineBizHandlerBuilder() common = append(common, builder) - res := biz.NewCombinedBizHandler("question_examine", common, platform) - return res + return biz.NewCombinedBizHandler("question_examine", common, platform) + +} +func InitCaseExamineHandler( + common []handler.Builder, + // platform 就是真正的出口 + platform handler.Handler) *biz.CompositionHandler { + builder := biz.NewCaseExamineBizHandlerBuilder() + common = append(common, builder) + return biz.NewCombinedBizHandler("case_examine", common, platform) } func InitCommonHandlers(log *log.HandlerBuilder, diff --git a/internal/ai/internal/domain/llm.go b/internal/ai/internal/domain/llm.go index 422298e1..a5742d1f 100644 --- a/internal/ai/internal/domain/llm.go +++ b/internal/ai/internal/domain/llm.go @@ -1,6 +1,7 @@ package domain const BizQuestionExamine = "question_examine" +const BizCaseExamine = "case_examine" type LLMRequest struct { Biz string diff --git a/internal/ai/internal/integration/llm_service_test.go b/internal/ai/internal/integration/llm_service_test.go index d33cee66..37bfe4c9 100644 --- a/internal/ai/internal/integration/llm_service_test.go +++ b/internal/ai/internal/integration/llm_service_test.go @@ -43,7 +43,7 @@ func (s *LLMServiceSuite) SetupSuite() { db := testioc.InitDB() s.db = db err := dao.InitTables(db) - require.NoError(s.T(), err) + s.NoError(err) s.logDao = dao.NewGORMLLMLogDAO(db) // 先插入 BizConfig @@ -56,7 +56,16 @@ func (s *LLMServiceSuite) SetupSuite() { Ctime: now, Utime: now, }).Error - assert.NoError(s.T(), err) + s.NoError(err) + err = s.db.Create(&dao.BizConfig{ + Biz: domain.BizCaseExamine, + MaxInput: 100, + PromptTemplate: "这是案例 %s,这是用户输入 %s", + KnowledgeId: knowledgeId, + Ctime: now, + Utime: now, + }).Error + s.NoError(err) } func (s *LLMServiceSuite) TearDownSuite() { @@ -154,6 +163,82 @@ func (s *LLMServiceSuite) TestService() { }, creditLogModel) }, }, + { + name: "案例测试-成功", + req: domain.LLMRequest{ + Biz: domain.BizCaseExamine, + Uid: 123, + Tid: "13", + Input: []string{ + "案例1", + "用户输入1", + }, + }, + assertFunc: assert.NoError, + before: func(t *testing.T, + ctrl *gomock.Controller) (llmHandler.Handler, credit.Service) { + llmHdl := hdlmocks.NewMockHandler(ctrl) + llmHdl.EXPECT().Handle(gomock.Any(), gomock.Any()). + Return(domain.LLMResponse{ + Tokens: 100, + Amount: 100, + Answer: "aians", + }, nil) + creditSvc := creditmocks.NewMockService(ctrl) + creditSvc.EXPECT().GetCreditsByUID(gomock.Any(), gomock.Any()).Return(credit.Credit{ + TotalAmount: 1000, + }, nil) + creditSvc.EXPECT().TryDeductCredits(gomock.Any(), gomock.Any()).Return(11, nil) + creditSvc.EXPECT().ConfirmDeductCredits(gomock.Any(), int64(123), int64(11)).Return(nil) + return llmHdl, creditSvc + }, + after: func(t *testing.T, resp domain.LLMResponse) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) + defer cancel() + // 校验response写入的内容是否正确 + assert.Equal(t, domain.LLMResponse{ + Tokens: 100, + Amount: 100, + Answer: "aians", + }, resp) + var logModel dao.LLMRecord + err := s.db.WithContext(ctx).Where("tid = ?", "13").First(&logModel).Error + require.NoError(t, err) + logModel.Id = 0 + s.assertLog(dao.LLMRecord{ + Id: 0, + Tid: "13", + Uid: 123, + Biz: domain.BizCaseExamine, + Tokens: 100, + Amount: 100, + KnowledgeId: knowledgeId, + Input: sqlx.JsonColumn[[]string]{ + Valid: true, + Val: []string{ + "案例1", + "用户输入1", + }, + }, + Status: 1, + PromptTemplate: sqlx.NewNullString("这是案例 %s,这是用户输入 %s"), + Answer: sqlx.NewNullString("aians"), + }, logModel) + // 校验credit写入的内容是否正确 + var creditLogModel dao.LLMCredit + err = s.db.WithContext(ctx).Where("tid = ?", "13").First(&creditLogModel).Error + require.NoError(t, err) + assert.True(t, creditLogModel.Id != 0) + creditLogModel.Id = 0 + s.assertCreditLog(dao.LLMCredit{ + Tid: "13", + Uid: 123, + Biz: domain.BizCaseExamine, + Amount: 100, + Status: 1, + }, creditLogModel) + }, + }, { name: "积分不足", req: domain.LLMRequest{ diff --git a/internal/ai/internal/integration/startup/wire.go b/internal/ai/internal/integration/startup/wire.go index 2a1b69e8..b542f3ba 100644 --- a/internal/ai/internal/integration/startup/wire.go +++ b/internal/ai/internal/integration/startup/wire.go @@ -51,7 +51,9 @@ func InitModule(db *egorm.Component, func InitHandlerFacade(common []handler.Builder, llm handler.Handler) *biz.FacadeHandler { que := ai.InitQuestionExamineHandler(common, llm) + ca := ai.InitCaseExamineHandler(common, llm) return biz.NewHandler(map[string]handler.Handler{ + ca.Biz(): ca, que.Biz(): que, }) } diff --git a/internal/ai/internal/integration/startup/wire_gen.go b/internal/ai/internal/integration/startup/wire_gen.go index 51ed9436..65015e0b 100644 --- a/internal/ai/internal/integration/startup/wire_gen.go +++ b/internal/ai/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 @@ -51,7 +51,9 @@ func InitModule(db *gorm.DB, hdl handler.Handler, creditSvc *credit.Module) (*ai func InitHandlerFacade(common []handler.Builder, llm2 handler.Handler) *biz.FacadeHandler { que := ai.InitQuestionExamineHandler(common, llm2) + ca := ai.InitCaseExamineHandler(common, llm2) return biz.NewHandler(map[string]handler.Handler{ + ca.Biz(): ca, que.Biz(): que, }) } diff --git a/internal/ai/internal/service/llm/handler/biz/case_examine.go b/internal/ai/internal/service/llm/handler/biz/case_examine.go new file mode 100644 index 00000000..c3789310 --- /dev/null +++ b/internal/ai/internal/service/llm/handler/biz/case_examine.go @@ -0,0 +1,33 @@ +package biz + +import ( + "context" + "fmt" + "unicode/utf8" + + "github.com/ecodeclub/webook/internal/ai/internal/domain" + "github.com/ecodeclub/webook/internal/ai/internal/service/llm/handler" +) + +type CaseExamineBizHandlerBuilder struct { +} + +func NewCaseExamineBizHandlerBuilder() *CaseExamineBizHandlerBuilder { + return &CaseExamineBizHandlerBuilder{} +} + +func (h *CaseExamineBizHandlerBuilder) Next(next handler.Handler) handler.Handler { + return handler.HandleFunc(func(ctx context.Context, req domain.LLMRequest) (domain.LLMResponse, error) { + title := req.Input[0] + userInput := req.Input[1] + userInputLen := utf8.RuneCount([]byte(userInput)) + + if userInputLen > req.Config.MaxInput { + return domain.LLMResponse{}, fmt.Errorf("输入太长,最常不超过 %d,现有长度 %d", req.Config.MaxInput, userInputLen) + } + // 把 input 和 prompt 结合起来 + prompt := fmt.Sprintf(req.Config.PromptTemplate, title, userInput) + req.Prompt = prompt + return next.Handle(ctx, req) + }) +} diff --git a/internal/cases/internal/domain/case_set.go b/internal/cases/internal/domain/case_set.go new file mode 100644 index 00000000..4b85ce1f --- /dev/null +++ b/internal/cases/internal/domain/case_set.go @@ -0,0 +1,22 @@ +package domain + +import "github.com/ecodeclub/ekit/slice" + +type CaseSet struct { + ID int64 + Uid int64 + // 标题 + Title string + // 描述 + Description string + Biz string + BizId int64 + Cases []Case + Utime int64 +} + +func (set CaseSet) Cids() []int64 { + return slice.Map(set.Cases, func(idx int, src Case) int64 { + return src.Id + }) +} diff --git a/internal/cases/internal/domain/cases.go b/internal/cases/internal/domain/cases.go index 27850f2b..0ae41d72 100644 --- a/internal/cases/internal/domain/cases.go +++ b/internal/cases/internal/domain/cases.go @@ -14,7 +14,8 @@ type Case struct { Introduction string Title string Content string - CodeRepo string + GithubRepo string + GiteeRepo string // 关键字,辅助记忆,提取重点 Keywords string // 速记,口诀 @@ -24,6 +25,8 @@ type Case struct { // 引导点 Guidance string Status CaseStatus + Biz string + BizId int64 Ctime time.Time Utime time.Time } diff --git a/internal/cases/internal/domain/const.go b/internal/cases/internal/domain/const.go new file mode 100644 index 00000000..cd22553c --- /dev/null +++ b/internal/cases/internal/domain/const.go @@ -0,0 +1,5 @@ +package domain + +var ( + DefaultBiz = "baguwen" +) diff --git a/internal/cases/internal/domain/examine.go b/internal/cases/internal/domain/examine.go new file mode 100644 index 00000000..bff203d4 --- /dev/null +++ b/internal/cases/internal/domain/examine.go @@ -0,0 +1,31 @@ +package domain + +type ExamineCaseResult struct { + Cid int64 + Result CaseResult + // 原始回答,源自 AI + RawResult string + + // 使用的 token 数量 + Tokens int64 + // 花费的金额 + Amount int64 + Tid string +} + +type CaseResult uint8 + +func (r CaseResult) ToUint8() uint8 { + return uint8(r) +} + +const ( + // ResultFailed 完全没通过,或者完全没有考过,我们不需要区别这两种状态 + ResultFailed CaseResult = iota + // ResultBasic 只回答出来了 15K 的部分 + ResultBasic + // ResultIntermediate 回答了 25K 部分 + ResultIntermediate + // ResultAdvanced 回答出来了 35K 部分 + ResultAdvanced +) diff --git a/internal/cases/internal/errs/code.go b/internal/cases/internal/errs/code.go index 82c26162..80cc0027 100644 --- a/internal/cases/internal/errs/code.go +++ b/internal/cases/internal/errs/code.go @@ -2,6 +2,8 @@ package errs var ( SystemError = ErrorCode{Code: 505001, Msg: "系统错误"} + // InsufficientCredits 这个不管说是客户端错误还是服务端错误,都有点勉强,所以随便用一个 5 + InsufficientCredits = ErrorCode{Code: 505002, Msg: "积分不足"} ) type ErrorCode struct { diff --git a/internal/cases/internal/event/event.go b/internal/cases/internal/event/event.go index c9440fae..b72648a0 100644 --- a/internal/cases/internal/event/event.go +++ b/internal/cases/internal/event/event.go @@ -32,7 +32,8 @@ type Case struct { Labels []string `json:"labels"` Title string `json:"title"` Content string `json:"content"` - CodeRepo string `json:"code_repo"` + GithubRepo string `json:"github_repo"` + GiteeRepo string `json:"gitee_repo"` Keywords string `json:"keywords"` Shorthand string `json:"shorthand"` Highlight string `json:"highlight"` @@ -59,7 +60,8 @@ func newCase(ca domain.Case) Case { Labels: ca.Labels, Title: ca.Title, Content: ca.Content, - CodeRepo: ca.CodeRepo, + GithubRepo: ca.GithubRepo, + GiteeRepo: ca.GiteeRepo, Keywords: ca.Keywords, Shorthand: ca.Shorthand, Highlight: ca.Highlight, diff --git a/internal/cases/internal/integration/admin_case_set_handler_test.go b/internal/cases/internal/integration/admin_case_set_handler_test.go new file mode 100644 index 00000000..752f1fd1 --- /dev/null +++ b/internal/cases/internal/integration/admin_case_set_handler_test.go @@ -0,0 +1,750 @@ +//go:build e2e + +package integration + +import ( + "context" + "fmt" + "net/http" + "strconv" + "testing" + "time" + + "github.com/ecodeclub/ekit/iox" + "github.com/ecodeclub/ekit/sqlx" + "github.com/ecodeclub/ginx/session" + eveMocks "github.com/ecodeclub/webook/internal/cases/internal/event/mocks" + "github.com/ecodeclub/webook/internal/cases/internal/integration/startup" + "github.com/ecodeclub/webook/internal/cases/internal/repository/dao" + "github.com/ecodeclub/webook/internal/cases/internal/web" + "github.com/ecodeclub/webook/internal/interactive" + intrmocks "github.com/ecodeclub/webook/internal/interactive/mocks" + "github.com/ecodeclub/webook/internal/pkg/middleware" + "github.com/ecodeclub/webook/internal/test" + testioc "github.com/ecodeclub/webook/internal/test/ioc" + "github.com/ego-component/egorm" + "github.com/gin-gonic/gin" + "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 AdminCaseSetTestSuite struct { + suite.Suite + server *egin.Component + db *egorm.Component + dao dao.CaseSetDAO + caseDao dao.CaseDAO + ctrl *gomock.Controller + producer *eveMocks.MockSyncEventProducer +} + +func (s *AdminCaseSetTestSuite) SetupSuite() { + s.ctrl = gomock.NewController(s.T()) + s.producer = eveMocks.NewMockSyncEventProducer(s.ctrl) + intrSvc := intrmocks.NewMockService(s.ctrl) + intrModule := &interactive.Module{ + Svc: intrSvc, + } + module, err := startup.InitModule(s.producer, intrModule) + require.NoError(s.T(), err) + adminHandler := module.AdminSetHandler + econf.Set("server", map[string]any{"contextTimeout": "1s"}) + server := egin.Load("server").Build() + + server.Use(func(ctx *gin.Context) { + ctx.Set("_session", session.NewMemorySession(session.Claims{ + Uid: uid, + Data: map[string]string{ + "creator": "true", + "memberDDL": strconv.FormatInt(time.Now().Add(time.Hour).UnixMilli(), 10), + }, + })) + }) + adminHandler.PrivateRoutes(server.Engine) + s.server = server + server.Use(middleware.NewCheckMembershipMiddlewareBuilder(nil).Build()) + s.db = testioc.InitDB() + s.dao = dao.NewCaseSetDAO(s.db) + s.caseDao = dao.NewCaseDao(s.db) +} + +func (s *AdminCaseSetTestSuite) TearDownTest() { + err := s.db.Exec("TRUNCATE TABLE `case_sets`").Error + require.NoError(s.T(), err) + err = s.db.Exec("TRUNCATE TABLE `case_set_cases`").Error + require.NoError(s.T(), err) + err = s.db.Exec("TRUNCATE TABLE `cases`").Error + require.NoError(s.T(), err) +} + +func (s *AdminCaseSetTestSuite) TestSave() { + testcases := []struct { + name string + before func(t *testing.T) + after func(t *testing.T, id int64) + req web.CaseSet + wantCode int + wantResp test.Result[int64] + }{ + { + name: "保存", + before: func(t *testing.T) { + + }, + after: func(t *testing.T, id int64) { + set, err := s.dao.GetByID(context.Background(), id) + require.NoError(s.T(), err) + assert.True(t, set.Ctime != 0) + assert.True(t, set.Utime != 0) + set.Ctime = 0 + set.Utime = 0 + assert.Equal(t, dao.CaseSet{ + Id: 1, + Uid: uid, + Title: "test title", + Description: "test description", + Biz: "baguwen", + BizId: 22, + }, set) + + }, + req: web.CaseSet{ + + Title: "test title", + Description: "test description", + Biz: "baguwen", + BizId: 22, + }, + wantCode: 200, + }, + { + name: "编辑", + before: func(t *testing.T) { + _, err := s.dao.Create(context.Background(), dao.CaseSet{ + Id: 1, + Uid: uid, + Title: "test title", + Description: "test description", + Biz: "baguwen", + BizId: 23, + Ctime: 123, + Utime: 234, + }) + require.NoError(s.T(), err) + + }, + after: func(t *testing.T, id int64) { + set, err := s.dao.GetByID(context.Background(), id) + require.NoError(s.T(), err) + assert.True(t, set.Ctime != 0) + assert.True(t, set.Utime != 0) + set.Ctime = 0 + set.Utime = 0 + assert.Equal(t, dao.CaseSet{ + Id: 1, + Uid: uid, + Title: "new title", + Description: "new description", + Biz: "jijibo", + BizId: 66, + }, set) + + }, + req: web.CaseSet{ + Id: 1, + Title: "new title", + Description: "new description", + Biz: "jijibo", + BizId: 66, + }, + wantCode: 200, + }, + } + for _, tc := range testcases { + s.T().Run(tc.name, func(t *testing.T) { + tc.before(t) + req, err := http.NewRequest(http.MethodPost, + "/case-sets/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) + tc.after(t, recorder.MustScan().Data) + // 清理掉 123 的数据 + err = s.db.Exec("TRUNCATE table `case_sets`").Error + require.NoError(t, err) + + }) + } +} + +func (s *AdminCaseSetTestSuite) Test_UpdateCases() { + testcases := []struct { + name string + before func(t *testing.T) int64 + after func(t *testing.T, id int64) + req web.UpdateCases + wantCode int + wantResp test.Result[int64] + }{ + { + name: "空案例集_添加多个案例", + before: func(t *testing.T) int64 { + id, err := s.dao.Create(context.Background(), dao.CaseSet{ + Uid: uid, + Title: "test title", + Description: "test description", + Biz: "baguwen", + BizId: 23, + Ctime: 123, + Utime: 234, + }) + require.NoError(t, err) + _, err = s.caseDao.Save(context.Background(), getTestCase(1)) + require.NoError(t, err) + _, err = s.caseDao.Save(context.Background(), getTestCase(2)) + require.NoError(t, err) + return id + }, + req: web.UpdateCases{ + CIDs: []int64{1, 2}, + }, + after: func(t *testing.T, id int64) { + cases, err := s.dao.GetCasesByID(context.Background(), id) + require.NoError(t, err) + for idx, ca := range cases { + require.True(t, ca.Ctime != 0) + require.True(t, ca.Utime != 0) + ca.Ctime = 0 + ca.Utime = 0 + cases[idx] = ca + } + assert.Equal(t, []dao.Case{ + getTestCase(1), + getTestCase(2), + }, cases) + }, + wantCode: 200, + }, + { + name: "非空案例集_添加多个案例", + before: func(t *testing.T) int64 { + id, err := s.dao.Create(context.Background(), dao.CaseSet{ + Uid: uid, + Title: "test title", + Description: "test description", + Biz: "baguwen", + BizId: 23, + Ctime: 123, + Utime: 234, + }) + require.NoError(t, err) + _, err = s.caseDao.Save(context.Background(), getTestCase(1)) + require.NoError(t, err) + _, err = s.caseDao.Save(context.Background(), getTestCase(2)) + require.NoError(t, err) + _, err = s.caseDao.Save(context.Background(), getTestCase(3)) + require.NoError(t, err) + _, err = s.caseDao.Save(context.Background(), getTestCase(4)) + require.NoError(t, err) + _, err = s.caseDao.Save(context.Background(), getTestCase(5)) + require.NoError(t, err) + err = s.dao.UpdateCasesByID(context.Background(), id, []int64{1, 2}) + require.NoError(t, err) + return id + }, + req: web.UpdateCases{ + CIDs: []int64{1, 2, 3, 4, 5}, + }, + wantCode: 200, + after: func(t *testing.T, id int64) { + cases, err := s.dao.GetCasesByID(context.Background(), id) + require.NoError(t, err) + for idx, ca := range cases { + require.True(t, ca.Ctime != 0) + require.True(t, ca.Utime != 0) + ca.Ctime = 0 + ca.Utime = 0 + cases[idx] = ca + } + assert.Equal(t, []dao.Case{ + getTestCase(1), + getTestCase(2), + getTestCase(3), + getTestCase(4), + getTestCase(5), + }, cases) + }, + }, + { + name: "删除部分案例", + before: func(t *testing.T) int64 { + id, err := s.dao.Create(context.Background(), dao.CaseSet{ + Uid: uid, + Title: "test title", + Description: "test description", + Biz: "baguwen", + BizId: 23, + Ctime: 123, + Utime: 234, + }) + require.NoError(t, err) + _, err = s.caseDao.Save(context.Background(), getTestCase(1)) + require.NoError(t, err) + _, err = s.caseDao.Save(context.Background(), getTestCase(2)) + require.NoError(t, err) + _, err = s.caseDao.Save(context.Background(), getTestCase(3)) + require.NoError(t, err) + _, err = s.caseDao.Save(context.Background(), getTestCase(4)) + require.NoError(t, err) + _, err = s.caseDao.Save(context.Background(), getTestCase(5)) + require.NoError(t, err) + err = s.dao.UpdateCasesByID(context.Background(), id, []int64{1, 2, 3, 4, 5}) + require.NoError(t, err) + return id + }, + req: web.UpdateCases{ + CIDs: []int64{1, 2, 3}, + }, + wantCode: 200, + after: func(t *testing.T, id int64) { + cases, err := s.dao.GetCasesByID(context.Background(), id) + require.NoError(t, err) + for idx, ca := range cases { + require.True(t, ca.Ctime != 0) + require.True(t, ca.Utime != 0) + ca.Ctime = 0 + ca.Utime = 0 + cases[idx] = ca + } + assert.Equal(t, []dao.Case{ + getTestCase(1), + getTestCase(2), + getTestCase(3), + }, cases) + }, + }, + { + name: "删除全部案例", + before: func(t *testing.T) int64 { + id, err := s.dao.Create(context.Background(), dao.CaseSet{ + Uid: uid, + Title: "test title", + Description: "test description", + Biz: "baguwen", + BizId: 23, + Ctime: 123, + Utime: 234, + }) + require.NoError(t, err) + _, err = s.caseDao.Save(context.Background(), getTestCase(1)) + require.NoError(t, err) + _, err = s.caseDao.Save(context.Background(), getTestCase(2)) + require.NoError(t, err) + _, err = s.caseDao.Save(context.Background(), getTestCase(3)) + require.NoError(t, err) + _, err = s.caseDao.Save(context.Background(), getTestCase(4)) + require.NoError(t, err) + _, err = s.caseDao.Save(context.Background(), getTestCase(5)) + require.NoError(t, err) + err = s.dao.UpdateCasesByID(context.Background(), id, []int64{1, 2, 3, 4, 5}) + require.NoError(t, err) + return id + }, + req: web.UpdateCases{ + CIDs: []int64{}, + }, + wantCode: 200, + after: func(t *testing.T, id int64) { + cases, err := s.dao.GetCasesByID(context.Background(), id) + require.NoError(t, err) + for idx, ca := range cases { + require.True(t, ca.Ctime != 0) + require.True(t, ca.Utime != 0) + ca.Ctime = 0 + ca.Utime = 0 + cases[idx] = ca + } + assert.Equal(t, 0, len(cases)) + }, + }, + { + name: "同时添加/删除部分案例", + before: func(t *testing.T) int64 { + id, err := s.dao.Create(context.Background(), dao.CaseSet{ + Uid: uid, + Title: "test title", + Description: "test description", + Biz: "baguwen", + BizId: 23, + Ctime: 123, + Utime: 234, + }) + require.NoError(t, err) + _, err = s.caseDao.Save(context.Background(), getTestCase(1)) + require.NoError(t, err) + _, err = s.caseDao.Save(context.Background(), getTestCase(2)) + require.NoError(t, err) + _, err = s.caseDao.Save(context.Background(), getTestCase(3)) + require.NoError(t, err) + _, err = s.caseDao.Save(context.Background(), getTestCase(4)) + require.NoError(t, err) + _, err = s.caseDao.Save(context.Background(), getTestCase(5)) + require.NoError(t, err) + err = s.dao.UpdateCasesByID(context.Background(), id, []int64{1, 2, 3}) + require.NoError(t, err) + return id + }, + req: web.UpdateCases{ + CIDs: []int64{1, 2, 4}, + }, + wantCode: 200, + after: func(t *testing.T, id int64) { + cases, err := s.dao.GetCasesByID(context.Background(), id) + require.NoError(t, err) + for idx, ca := range cases { + require.True(t, ca.Ctime != 0) + require.True(t, ca.Utime != 0) + ca.Ctime = 0 + ca.Utime = 0 + cases[idx] = ca + } + assert.Equal(t, []dao.Case{ + getTestCase(1), + getTestCase(2), + getTestCase(4), + }, cases) + }, + }, + { + name: "案例集不存在", + before: func(t *testing.T) int64 { + return 3 + }, + after: func(t *testing.T, id int64) { + }, + req: web.UpdateCases{ + CIDs: []int64{1, 2, 3}, + }, + wantCode: 500, + wantResp: test.Result[int64]{Code: 502001, Msg: "系统错误"}, + }, + } + for _, tc := range testcases { + s.T().Run(tc.name, func(t *testing.T) { + id := tc.before(t) + tc.req.CSID = id + req, err := http.NewRequest(http.MethodPost, + "/case-sets/cases/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) + tc.after(t, id) + // 清理掉 123 的数据 + err = s.db.Exec("TRUNCATE table `case_sets`").Error + err = s.db.Exec("TRUNCATE table `case_set_cases`").Error + require.NoError(t, err) + + }) + } +} + +func (s *AdminCaseSetTestSuite) Test_List() { + for i := 1; i < 20; i++ { + _, err := s.dao.Create(context.Background(), getTestCaseSet(int64(i))) + require.NoError(s.T(), err) + } + testcases := []struct { + name string + after func(t *testing.T, id int64) + req web.Page + wantCode int + wantResp web.CaseSetList + }{ + { + name: "列表", + req: web.Page{ + Offset: 0, + Limit: 2, + }, + wantCode: 200, + wantResp: web.CaseSetList{ + Total: 19, + CaseSets: []web.CaseSet{ + { + Id: 19, + Title: "title19", + Description: "description19", + Biz: "baguwen", + BizId: 49, + }, + { + Id: 18, + Title: "title18", + Description: "description18", + Biz: "baguwen", + BizId: 48, + }, + }, + }, + }, + { + name: "列表--分页", + req: web.Page{ + Offset: 2, + Limit: 2, + }, + wantCode: 200, + wantResp: web.CaseSetList{ + Total: 19, + CaseSets: []web.CaseSet{ + { + Id: 17, + Title: "title17", + Description: "description17", + Biz: "baguwen", + BizId: 47, + }, + { + Id: 16, + Title: "title16", + Description: "description16", + Biz: "baguwen", + BizId: 46, + }, + }, + }, + }, + } + for _, tc := range testcases { + s.T().Run(tc.name, func(t *testing.T) { + req, err := http.NewRequest(http.MethodPost, + "/case-sets/list", iox.NewJSONReader(tc.req)) + req.Header.Set("content-type", "application/json") + require.NoError(t, err) + recorder := test.NewJSONResponseRecorder[web.CaseSetList]() + s.server.ServeHTTP(recorder, req) + require.Equal(t, tc.wantCode, recorder.Code) + res := recorder.MustScan().Data + for idx, ca := range res.CaseSets { + require.True(t, ca.Utime != 0) + ca.Utime = 0 + res.CaseSets[idx] = ca + } + assert.Equal(t, tc.wantResp, res) + }) + } +} + +func (s *AdminCaseSetTestSuite) Test_Detail() { + testcases := []struct { + name string + before func(t *testing.T) int64 + wantCode int + wantResp web.CaseSet + }{ + { + name: "题集详情", + before: func(t *testing.T) int64 { + id, err := s.dao.Create(context.Background(), dao.CaseSet{ + Uid: uid, + Title: "test title", + Description: "test description", + Biz: "baguwen", + BizId: 23, + Ctime: 123, + Utime: 234, + }) + require.NoError(t, err) + _, err = s.caseDao.Save(context.Background(), getTestCase(1)) + require.NoError(t, err) + _, err = s.caseDao.Save(context.Background(), getTestCase(2)) + require.NoError(t, err) + _, err = s.caseDao.Save(context.Background(), getTestCase(3)) + require.NoError(t, err) + _, err = s.caseDao.Save(context.Background(), getTestCase(4)) + require.NoError(t, err) + _, err = s.caseDao.Save(context.Background(), getTestCase(5)) + require.NoError(t, err) + err = s.dao.UpdateCasesByID(context.Background(), id, []int64{1, 2, 3, 4, 5}) + require.NoError(t, err) + return id + }, + wantCode: 200, + wantResp: web.CaseSet{ + Title: "test title", + Description: "test description", + Biz: "baguwen", + Cases: []web.Case{ + getCase(1), + getCase(2), + getCase(3), + getCase(4), + getCase(5), + }, + BizId: 23, + }, + }, + } + for _, tc := range testcases { + s.T().Run(tc.name, func(t *testing.T) { + id := tc.before(t) + req, err := http.NewRequest(http.MethodPost, + "/case-sets/detail", iox.NewJSONReader(web.CaseSetID{ + ID: id, + })) + req.Header.Set("content-type", "application/json") + require.NoError(t, err) + recorder := test.NewJSONResponseRecorder[web.CaseSet]() + s.server.ServeHTTP(recorder, req) + require.Equal(t, tc.wantCode, recorder.Code) + res := recorder.MustScan().Data + for idx, ca := range res.Cases { + require.True(t, ca.Utime != 0) + ca.Utime = 0 + res.Cases[idx] = ca + } + tc.wantResp.Id = id + require.True(t, res.Utime != 0) + res.Utime = 0 + assert.Equal(t, tc.wantResp, res) + }) + } +} + +func (s *AdminCaseSetTestSuite) TestQuestionSet_Candidates() { + testCases := []struct { + name string + + before func(t *testing.T) + req web.CandidateReq + + wantCode int + wantResp test.Result[web.CasesList] + }{ + { + name: "获取成功", + before: func(t *testing.T) { + // 准备数据 + // 创建一个空案例集 + id, err := s.dao.Create(context.Background(), dao.CaseSet{ + Id: 1, + Uid: uid, + Title: "Go", + Description: "Go题集", + Biz: "roadmap", + BizId: 2, + Utime: 123, + }) + require.NoError(t, err) + // 添加案例 + cases := []dao.Case{ + getTestCase(1), + getTestCase(2), + getTestCase(3), + getTestCase(4), + getTestCase(5), + getTestCase(6), + } + err = s.db.WithContext(context.Background()).Create(&cases).Error + require.NoError(t, err) + cids := []int64{1, 2, 3} + require.NoError(t, s.dao.UpdateCasesByID(context.Background(), id, cids)) + }, + req: web.CandidateReq{ + CSID: 1, + Offset: 1, + Limit: 2, + }, + wantCode: 200, + wantResp: test.Result[web.CasesList]{ + Data: web.CasesList{ + Total: 3, + Cases: []web.Case{ + getCase(5), + getCase(4), + }, + }, + }, + }, + } + + for _, tc := range testCases { + s.T().Run(tc.name, func(t *testing.T) { + tc.before(t) + req, err := http.NewRequest(http.MethodPost, + "/case-sets/candidate", iox.NewJSONReader(tc.req)) + req.Header.Set("content-type", "application/json") + require.NoError(t, err) + recorder := test.NewJSONResponseRecorder[web.CasesList]() + s.server.ServeHTTP(recorder, req) + require.Equal(t, tc.wantCode, recorder.Code) + assert.Equal(t, tc.wantResp, recorder.MustScan()) + }) + } +} + +func TestCaseSetAdminHandler(t *testing.T) { + suite.Run(t, new(AdminCaseSetTestSuite)) +} + +func getTestCase(id int64) dao.Case { + return dao.Case{ + Id: id, + Uid: uid, + Introduction: fmt.Sprintf("intr%d", id), + Title: fmt.Sprintf("title%d", id), + Content: fmt.Sprintf("content%d", id), + Labels: sqlx.JsonColumn[[]string]{ + Valid: true, + Val: []string{"case", "mysql"}, + }, + GithubRepo: fmt.Sprintf("githubrepo%d", id), + GiteeRepo: fmt.Sprintf("giteerepo%d", id), + Keywords: fmt.Sprintf("keywords%d", id), + Shorthand: fmt.Sprintf("shorthand%d", id), + Highlight: fmt.Sprintf("highlight%d", id), + Guidance: fmt.Sprintf("guidance%d", id), + Biz: "question", + BizId: 11, + Status: 2, + } +} + +func getTestCaseSet(id int64) dao.CaseSet { + return dao.CaseSet{ + Id: id, + Uid: uid, + Title: fmt.Sprintf("title%d", id), + Description: fmt.Sprintf("description%d", id), + Biz: "baguwen", + BizId: id + 30, + } +} + +func getCase(id int64) web.Case { + ca := web.Case{ + Id: id, + Introduction: fmt.Sprintf("intr%d", id), + Title: fmt.Sprintf("title%d", id), + Content: fmt.Sprintf("content%d", id), + Labels: []string{"case", "mysql"}, + GithubRepo: fmt.Sprintf("githubrepo%d", id), + GiteeRepo: fmt.Sprintf("giteerepo%d", id), + Keywords: fmt.Sprintf("keywords%d", id), + Shorthand: fmt.Sprintf("shorthand%d", id), + Highlight: fmt.Sprintf("highlight%d", id), + Guidance: fmt.Sprintf("guidance%d", id), + Biz: "question", + BizId: 11, + Status: 2, + } + return ca +} diff --git a/internal/cases/internal/integration/case_set_handler_test.go b/internal/cases/internal/integration/case_set_handler_test.go new file mode 100644 index 00000000..b2a4a6f6 --- /dev/null +++ b/internal/cases/internal/integration/case_set_handler_test.go @@ -0,0 +1,691 @@ +//go:build e2e + +package integration + +import ( + "context" + "fmt" + "net/http" + "strconv" + "testing" + "time" + + "github.com/ecodeclub/ekit/iox" + "github.com/ecodeclub/ginx/session" + "github.com/ecodeclub/webook/internal/ai" + "github.com/ecodeclub/webook/internal/cases/internal/domain" + eveMocks "github.com/ecodeclub/webook/internal/cases/internal/event/mocks" + "github.com/ecodeclub/webook/internal/cases/internal/integration/startup" + "github.com/ecodeclub/webook/internal/cases/internal/repository/dao" + "github.com/ecodeclub/webook/internal/cases/internal/web" + "github.com/ecodeclub/webook/internal/interactive" + intrmocks "github.com/ecodeclub/webook/internal/interactive/mocks" + "github.com/ecodeclub/webook/internal/pkg/middleware" + "github.com/ecodeclub/webook/internal/test" + testioc "github.com/ecodeclub/webook/internal/test/ioc" + "github.com/ego-component/egorm" + "github.com/gin-gonic/gin" + "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 CaseSetTestSuite struct { + suite.Suite + server *egin.Component + db *egorm.Component + dao dao.CaseSetDAO + caseDao dao.CaseDAO + ctrl *gomock.Controller + producer *eveMocks.MockSyncEventProducer +} + +func (c *CaseSetTestSuite) SetupSuite() { + ctrl := gomock.NewController(c.T()) + c.producer = eveMocks.NewMockSyncEventProducer(ctrl) + + intrSvc := intrmocks.NewMockService(ctrl) + intrModule := &interactive.Module{ + Svc: intrSvc, + } + + // 模拟返回的数据 + // 使用如下规律: + // 1. liked == id % 2 == 1 (奇数为 true) + // 2. collected = id %2 == 0 (偶数为 true) + // 3. viewCnt = id + 1 + // 4. likeCnt = id + 2 + // 5. collectCnt = id + 3 + intrSvc.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + AnyTimes().DoAndReturn(func(ctx context.Context, + biz string, id int64, uid int64) (interactive.Interactive, error) { + intr := c.mockInteractive(biz, id) + return intr, nil + }) + intrSvc.EXPECT().GetByIds(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, + biz string, ids []int64) (map[int64]interactive.Interactive, error) { + res := make(map[int64]interactive.Interactive, len(ids)) + for _, id := range ids { + intr := c.mockInteractive(biz, id) + res[id] = intr + } + return res, nil + }).AnyTimes() + + module, err := startup.InitExamModule(c.producer, intrModule, &ai.Module{}) + require.NoError(c.T(), err) + econf.Set("server", map[string]any{"contextTimeout": "1s"}) + server := egin.Load("server").Build() + + module.CsHdl.PublicRoutes(server.Engine) + server.Use(func(ctx *gin.Context) { + ctx.Set("_session", session.NewMemorySession(session.Claims{ + Uid: uid, + Data: map[string]string{ + "creator": "true", + "memberDDL": strconv.FormatInt(time.Now().Add(time.Hour).UnixMilli(), 10), + }, + })) + }) + module.CsHdl.PrivateRoutes(server.Engine) + server.Use(middleware.NewCheckMembershipMiddlewareBuilder(nil).Build()) + + c.server = server + c.db = testioc.InitDB() + err = dao.InitTables(c.db) + require.NoError(c.T(), err) + c.dao = dao.NewCaseSetDAO(c.db) + c.caseDao = dao.NewCaseDao(c.db) +} + +func (c *CaseSetTestSuite) TearDownTest() { + err := c.db.Exec("TRUNCATE TABLE `case_sets`").Error + require.NoError(c.T(), err) + err = c.db.Exec("TRUNCATE TABLE `case_set_cases`").Error + require.NoError(c.T(), err) + err = c.db.Exec("TRUNCATE TABLE `cases`").Error + require.NoError(c.T(), err) + err = c.db.Exec("TRUNCATE TABLE `case_examine_records`").Error + require.NoError(c.T(), err) + err = c.db.Exec("TRUNCATE TABLE `case_results`").Error + require.NoError(c.T(), err) +} + +func (s *CaseSetTestSuite) TestCaseSetDetailByBiz() { + var now int64 = 123 + testCases := []struct { + name string + before func(t *testing.T) + after func(t *testing.T) + req web.BizReq + + wantCode int + wantResp test.Result[web.CaseSet] + }{ + { + name: "空案例集", + before: func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + // 创建一个空题集 + id, err := s.dao.Create(ctx, dao.CaseSet{ + Id: 321, + Uid: uid, + Title: "Go", + Biz: "roadmap", + BizId: 2, + Description: "Go的desc", + }) + require.NoError(t, err) + require.Equal(t, int64(321), id) + }, + after: func(t *testing.T) { + }, + req: web.BizReq{ + Biz: "roadmap", + BizId: 2, + }, + wantCode: 200, + wantResp: test.Result[web.CaseSet]{ + Data: web.CaseSet{ + Id: 321, + Title: "Go", + Description: "Go的desc", + Biz: "roadmap", + BizId: 2, + Interactive: web.Interactive{ + ViewCnt: 322, + LikeCnt: 323, + CollectCnt: 324, + Liked: true, + Collected: false, + }, + }, + }, + }, + { + name: "非空案例集", + before: func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + id, err := s.dao.Create(ctx, dao.CaseSet{ + Id: 322, + Uid: uid, + Title: "Go", + Description: "Go案例集", + Biz: "roadmap", + BizId: 3, + }) + require.NoError(t, err) + require.Equal(t, int64(322), id) + + // 添加案例 + cases := []dao.Case{ + { + Id: 614, + Uid: uid + 1, + Biz: "project", + BizId: 1, + Title: "Go案例1", + Content: "Go案例1", + Ctime: now, + Utime: now, + }, + { + Id: 615, + Uid: uid + 2, + Biz: "project", + BizId: 1, + Title: "Go案例2", + Content: "Go案例2", + Ctime: now, + Utime: now, + }, + { + Id: 616, + Uid: uid + 3, + Biz: "project", + BizId: 1, + Title: "Go案例3", + Content: "Go案例3", + Ctime: now, + Utime: now, + }, + } + err = s.db.WithContext(ctx).Create(&cases).Error + require.NoError(t, err) + cids := []int64{614, 615, 616} + require.NoError(t, s.dao.UpdateCasesByID(ctx, id, cids)) + + // 添加用户答题记录,只需要添加一个就可以 + err = s.db.WithContext(ctx).Create(&dao.CaseResult{ + Uid: uid, + Cid: 614, + Result: domain.ResultAdvanced.ToUint8(), + Ctime: now, + Utime: now, + }).Error + require.NoError(t, err) + + // 题集中题目为1 + cs, err := s.dao.GetCasesByID(ctx, id) + require.NoError(t, err) + require.Equal(t, len(cids), len(cs)) + }, + after: func(t *testing.T) { + }, + req: web.BizReq{ + Biz: "roadmap", + BizId: 3, + }, + wantCode: 200, + wantResp: test.Result[web.CaseSet]{ + Data: web.CaseSet{ + Id: 322, + Biz: "roadmap", + BizId: 3, + Title: "Go", + Description: "Go案例集", + Interactive: web.Interactive{ + ViewCnt: 323, + LikeCnt: 324, + CollectCnt: 325, + Liked: false, + Collected: true, + }, + Cases: []web.Case{ + { + Id: 614, + Biz: "project", + BizId: 1, + Title: "Go案例1", + Content: "Go案例1", + ExamineResult: domain.ResultAdvanced.ToUint8(), + Utime: now, + }, + { + Id: 615, + Biz: "project", + BizId: 1, + Title: "Go案例2", + Content: "Go案例2", + Utime: now, + }, + { + Id: 616, + Biz: "project", + BizId: 1, + Title: "Go案例3", + Content: "Go案例3", + Utime: now, + }, + }, + }, + }, + }, + } + + for _, tc := range testCases { + tc := tc + s.T().Run(tc.name, func(t *testing.T) { + tc.before(t) + req, err := http.NewRequest(http.MethodPost, + "/case-sets/detail/biz", iox.NewJSONReader(tc.req)) + req.Header.Set("content-type", "application/json") + require.NoError(t, err) + recorder := test.NewJSONResponseRecorder[web.CaseSet]() + s.server.ServeHTTP(recorder, req) + require.Equal(t, tc.wantCode, recorder.Code) + data := recorder.MustScan() + assert.True(t, data.Data.Utime != 0) + data.Data.Utime = 0 + assert.Equal(t, tc.wantResp, data) + tc.after(t) + }) + } +} + +func (s *CaseSetTestSuite) TestCaseSet_Detail() { + var now int64 = 123 + testCases := []struct { + name string + before func(t *testing.T) + after func(t *testing.T) + req web.CaseSetID + + wantCode int + wantResp test.Result[web.CaseSet] + }{ + { + name: "空案例集", + before: func(t *testing.T) { + t.Helper() + + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + // 创建一个空案例集 + id, err := s.dao.Create(ctx, dao.CaseSet{ + Id: 321, + Uid: uid, + Title: "Go", + Biz: "roadmap", + BizId: 2, + Description: "Go案例集", + Utime: now, + }) + require.NoError(t, err) + require.Equal(t, int64(321), id) + }, + after: func(t *testing.T) { + }, + req: web.CaseSetID{ + ID: 321, + }, + wantCode: 200, + wantResp: test.Result[web.CaseSet]{ + Data: web.CaseSet{ + Id: 321, + Title: "Go", + Description: "Go案例集", + Biz: "roadmap", + BizId: 2, + Interactive: web.Interactive{ + ViewCnt: 322, + LikeCnt: 323, + CollectCnt: 324, + Liked: true, + Collected: false, + }, + }, + }, + }, + { + name: "非空案例集", + before: func(t *testing.T) { + t.Helper() + + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + // 创建一个空题集 + id, err := s.dao.Create(ctx, dao.CaseSet{ + Id: 322, + Uid: uid, + Title: "Go", + Description: "Go案例集", + Biz: "roadmap", + BizId: 2, + }) + require.NoError(t, err) + require.Equal(t, int64(322), id) + + // 添加问题 + questions := []dao.Case{ + { + Id: 614, + Uid: uid + 1, + Biz: "project", + BizId: 1, + Title: "Go案例1", + Content: "Go案例1", + Ctime: now, + Utime: now, + }, + { + Id: 615, + Uid: uid + 2, + Biz: "project", + BizId: 1, + Title: "Go案例2", + Content: "Go案例2", + Ctime: now, + Utime: now, + }, + { + Id: 616, + Uid: uid + 3, + Biz: "project", + BizId: 1, + Title: "Go案例3", + Content: "Go案例3", + Ctime: now, + Utime: now, + }, + } + err = s.db.WithContext(ctx).Create(&questions).Error + require.NoError(t, err) + cids := []int64{614, 615, 616} + require.NoError(t, s.dao.UpdateCasesByID(ctx, id, cids)) + + // 添加用户答题记录,只需要添加一个就可以 + err = s.db.WithContext(ctx).Create(&dao.CaseResult{ + Uid: uid, + Cid: 614, + Result: domain.ResultAdvanced.ToUint8(), + Ctime: now, + Utime: now, + }).Error + require.NoError(t, err) + + // 题集中题目为1 + qs, err := s.dao.GetCasesByID(ctx, id) + require.NoError(t, err) + require.Equal(t, len(cids), len(qs)) + }, + after: func(t *testing.T) { + }, + req: web.CaseSetID{ + ID: 322, + }, + wantCode: 200, + wantResp: test.Result[web.CaseSet]{ + Data: web.CaseSet{ + Id: 322, + Biz: "roadmap", + BizId: 2, + Title: "Go", + Description: "Go案例集", + Interactive: web.Interactive{ + ViewCnt: 323, + LikeCnt: 324, + CollectCnt: 325, + Liked: false, + Collected: true, + }, + Cases: []web.Case{ + { + Id: 614, + Biz: "project", + BizId: 1, + Title: "Go案例1", + Content: "Go案例1", + ExamineResult: domain.ResultAdvanced.ToUint8(), + Utime: now, + }, + { + Id: 615, + Biz: "project", + BizId: 1, + Title: "Go案例2", + Content: "Go案例2", + Utime: now, + }, + { + Id: 616, + Biz: "project", + BizId: 1, + Title: "Go案例3", + Content: "Go案例3", + Utime: now, + }, + }, + }, + }, + }, + } + + for _, tc := range testCases { + tc := tc + s.T().Run(tc.name, func(t *testing.T) { + tc.before(t) + req, err := http.NewRequest(http.MethodPost, + "/case-sets/detail", iox.NewJSONReader(tc.req)) + req.Header.Set("content-type", "application/json") + require.NoError(t, err) + recorder := test.NewJSONResponseRecorder[web.CaseSet]() + s.server.ServeHTTP(recorder, req) + require.Equal(t, tc.wantCode, recorder.Code) + data := recorder.MustScan() + assert.True(t, data.Data.Utime != 0) + data.Data.Utime = 0 + assert.Equal(t, tc.wantResp, data) + tc.after(t) + }) + } +} + +func (s *CaseSetTestSuite) TestCaseSet_ListAllQuestionSets() { + // 插入一百条 + total := 100 + data := make([]dao.CaseSet, 0, total) + + for idx := 0; idx < total; idx++ { + // 空题集 + data = append(data, dao.CaseSet{ + Uid: int64(uid + idx), + Title: fmt.Sprintf("案例集标题 %d", idx), + Description: fmt.Sprintf("案例集简介 %d", idx), + Utime: 123, + }) + } + // 这个接口是不会查询到这些数据的 + data = append(data, dao.CaseSet{ + Uid: 200, + Title: fmt.Sprintf("案例集标题 %d", 200), + Description: fmt.Sprintf("案例集简介 %d", 200), + Biz: "project", + BizId: 200, + Utime: 123, + }) + err := s.db.Create(&data).Error + require.NoError(s.T(), err) + + testCases := []struct { + name string + req web.Page + + wantCode int + wantResp test.Result[web.CaseSetList] + }{ + { + name: "获取成功", + req: web.Page{ + Limit: 2, + Offset: 0, + }, + wantCode: 200, + wantResp: test.Result[web.CaseSetList]{ + Data: web.CaseSetList{ + CaseSets: []web.CaseSet{ + { + Id: 100, + Title: "案例集标题 99", + Description: "案例集简介 99", + Biz: "baguwen", + Utime: 123, + Interactive: web.Interactive{ + ViewCnt: 101, + LikeCnt: 102, + CollectCnt: 103, + Liked: false, + Collected: true, + }, + }, + { + Id: 99, + Title: "案例集标题 98", + Description: "案例集简介 98", + Biz: "baguwen", + Utime: 123, + Interactive: web.Interactive{ + ViewCnt: 100, + LikeCnt: 101, + CollectCnt: 102, + Liked: true, + Collected: false, + }, + }, + }, + }, + }, + }, + { + name: "获取部分", + req: web.Page{ + Limit: 2, + Offset: 99, + }, + wantCode: 200, + wantResp: test.Result[web.CaseSetList]{ + Data: web.CaseSetList{ + CaseSets: []web.CaseSet{ + { + Id: 1, + Title: "案例集标题 0", + Description: "案例集简介 0", + Biz: "baguwen", + Utime: 123, + Interactive: web.Interactive{ + ViewCnt: 2, + LikeCnt: 3, + CollectCnt: 4, + Liked: true, + Collected: false, + }, + }, + }, + }, + }, + }, + } + + for _, tc := range testCases { + tc := tc + s.T().Run(tc.name, func(t *testing.T) { + req, err := http.NewRequest(http.MethodPost, + "/case-sets/list", iox.NewJSONReader(tc.req)) + req.Header.Set("content-type", "application/json") + require.NoError(t, err) + recorder := test.NewJSONResponseRecorder[web.CaseSetList]() + s.server.ServeHTTP(recorder, req) + require.Equal(t, tc.wantCode, recorder.Code) + assert.Equal(t, tc.wantResp, recorder.MustScan()) + }) + } +} + +func (s *CaseSetTestSuite) TestQuestionSet_RetrieveQuestionSetDetail_Failed() { + testCases := []struct { + name string + before func(t *testing.T) + after func(t *testing.T) + req web.CaseSetID + + wantCode int + wantResp test.Result[int64] + }{ + { + name: "题集ID非法_题集ID不存在", + before: func(t *testing.T) { + t.Helper() + }, + after: func(t *testing.T) { + t.Helper() + }, + req: web.CaseSetID{ + ID: 10000, + }, + wantCode: 500, + wantResp: test.Result[int64]{Code: 505001, Msg: "系统错误"}, + }, + } + for _, tc := range testCases { + tc := tc + s.T().Run(tc.name, func(t *testing.T) { + tc.before(t) + req, err := http.NewRequest(http.MethodPost, + "/case-sets/detail", 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 *CaseSetTestSuite) mockInteractive(biz string, id int64) interactive.Interactive { + liked := id%2 == 1 + collected := id%2 == 0 + return interactive.Interactive{ + Biz: biz, + BizId: id, + ViewCnt: int(id + 1), + LikeCnt: int(id + 2), + CollectCnt: int(id + 3), + Liked: liked, + Collected: collected, + } +} + +func TestCaseSetHandler(t *testing.T) { + suite.Run(t, new(CaseSetTestSuite)) +} diff --git a/internal/cases/internal/integration/examine_handler_test.go b/internal/cases/internal/integration/examine_handler_test.go new file mode 100644 index 00000000..767471fa --- /dev/null +++ b/internal/cases/internal/integration/examine_handler_test.go @@ -0,0 +1,255 @@ +//go:build e2e + +package integration + +import ( + "context" + "net/http" + "testing" + "time" + + "github.com/ecodeclub/ekit/iox" + "github.com/ecodeclub/ginx/session" + "github.com/ecodeclub/webook/internal/ai" + aimocks "github.com/ecodeclub/webook/internal/ai/mocks" + "github.com/ecodeclub/webook/internal/cases/internal/domain" + eveMocks "github.com/ecodeclub/webook/internal/cases/internal/event/mocks" + "github.com/ecodeclub/webook/internal/cases/internal/integration/startup" + "github.com/ecodeclub/webook/internal/cases/internal/repository/dao" + "github.com/ecodeclub/webook/internal/cases/internal/web" + "github.com/ecodeclub/webook/internal/interactive" + intrmocks "github.com/ecodeclub/webook/internal/interactive/mocks" + "github.com/ecodeclub/webook/internal/test" + testioc "github.com/ecodeclub/webook/internal/test/ioc" + "github.com/ego-component/egorm" + "github.com/gin-gonic/gin" + "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 ExamineHandlerTest struct { + suite.Suite + server *egin.Component + db *egorm.Component + dao dao.ExamineDAO + ctrl *gomock.Controller +} + +func (s *ExamineHandlerTest) SetupSuite() { + s.ctrl = gomock.NewController(s.T()) + producer := eveMocks.NewMockSyncEventProducer(s.ctrl) + ctrl := gomock.NewController(s.T()) + aiSvc := aimocks.NewMockService(ctrl) + aiSvc.EXPECT().Invoke(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, req ai.LLMRequest) (ai.LLMResponse, error) { + return ai.LLMResponse{ + Tokens: req.Uid, + Amount: req.Uid, + Answer: "评分:15K", + }, nil + }).AnyTimes() + intrSvc := intrmocks.NewMockService(s.ctrl) + intrModule := &interactive.Module{ + Svc: intrSvc, + } + module, err := startup.InitExamModule(producer, intrModule, &ai.Module{Svc: aiSvc}) + require.NoError(s.T(), err) + hdl := module.ExamineHdl + s.db = testioc.InitDB() + econf.Set("server", map[string]any{"contextTimeout": "1s"}) + server := egin.Load("server").Build() + server.Use(func(ctx *gin.Context) { + ctx.Set(session.CtxSessionKey, + session.NewMemorySession(session.Claims{ + Uid: uid, + })) + }) + hdl.MemberRoutes(server.Engine) + s.server = server + s.dao = dao.NewGORMExamineDAO(s.db) + // 提前准备 Question,这是所有测试都可以使用的 + err = s.db.Create(&dao.PublishCase{ + Id: 1, + Title: "测试案例1", + }).Error + assert.NoError(s.T(), err) + err = s.db.Create(&dao.PublishCase{ + Id: 2, + Title: "测试案例2", + }).Error + assert.NoError(s.T(), err) +} + +func (s *ExamineHandlerTest) TearDownTest() { + err := s.db.Exec("TRUNCATE TABLE `case_examine_records`").Error + require.NoError(s.T(), err) + err = s.db.Exec("TRUNCATE TABLE `case_results`").Error + require.NoError(s.T(), err) +} + +func (s *ExamineHandlerTest) TearDownSuite() { + err := s.db.Exec("TRUNCATE TABLE `publish_cases`").Error + require.NoError(s.T(), err) +} + +func (s *ExamineHandlerTest) TestExamine() { + testCases := []struct { + name string + before func(t *testing.T) + after func(t *testing.T) + + req web.ExamineReq + + wantCode int + wantResp test.Result[web.ExamineResult] + }{ + { + name: "新用户", + before: func(t *testing.T) { + + }, + after: func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) + defer cancel() + var record dao.CaseExamineRecord + err := s.db.Where("uid = ? ", uid).Order("id DESC").First(&record).Error + require.NoError(t, err) + assert.True(t, record.Utime > 0) + record.Utime = 0 + assert.True(t, record.Ctime > 0) + record.Ctime = 0 + assert.True(t, record.Id > 0) + record.Id = 0 + assert.True(t, len(record.Tid) > 0) + record.Tid = "" + assert.Equal(t, dao.CaseExamineRecord{ + Uid: uid, + Cid: 1, + Result: domain.ResultBasic.ToUint8(), + RawResult: "评分:15K", + Tokens: uid, + Amount: uid, + }, record) + + var caseRes dao.CaseResult + err = s.db.WithContext(ctx). + Where("cid = ? AND uid = ?", 1, uid). + First(&caseRes).Error + require.NoError(t, err) + assert.True(t, caseRes.Ctime > 0) + caseRes.Ctime = 0 + assert.True(t, caseRes.Utime > 0) + caseRes.Utime = 0 + assert.True(t, caseRes.Id > 0) + caseRes.Id = 0 + assert.Equal(t, dao.CaseResult{ + Result: domain.ResultBasic.ToUint8(), + Cid: 1, + Uid: uid, + }, caseRes) + }, + req: web.ExamineReq{ + Cid: 1, + Input: "测试一下", + }, + wantCode: 200, + wantResp: test.Result[web.ExamineResult]{ + Data: web.ExamineResult{ + Result: domain.ResultBasic.ToUint8(), + RawResult: "评分:15K", + Amount: uid, + }, + }, + }, + { + // 这个测试依赖于前面的测试产生的 eid = 1 + name: "老用户重复测试", + before: func(t *testing.T) { + err := s.db.Create(&dao.CaseResult{ + Id: 2, + Uid: uid, + Cid: 2, + Result: domain.ResultIntermediate.ToUint8(), + Ctime: 123, + Utime: 123, + }).Error + require.NoError(t, err) + }, + after: func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) + defer cancel() + const csid = 2 + var record dao.CaseExamineRecord + err := s.db.Where("uid = ? ", uid).Order("id DESC").First(&record).Error + require.NoError(t, err) + assert.True(t, record.Utime > 0) + record.Utime = 0 + assert.True(t, record.Ctime > 0) + record.Ctime = 0 + assert.True(t, record.Id > 0) + record.Id = 0 + assert.True(t, len(record.Tid) > 0) + record.Tid = "" + assert.Equal(t, dao.CaseExamineRecord{ + Uid: uid, + Cid: 2, + Result: domain.ResultBasic.ToUint8(), + RawResult: "评分:15K", + Tokens: uid, + Amount: uid, + }, record) + + var caseRes dao.CaseResult + err = s.db.WithContext(ctx). + Where("cid = ? AND uid = ?", 2, uid). + First(&caseRes).Error + require.NoError(t, err) + assert.True(t, caseRes.Ctime > 0) + caseRes.Ctime = 0 + assert.True(t, caseRes.Utime > 0) + caseRes.Utime = 0 + assert.True(t, caseRes.Id > 0) + caseRes.Id = 0 + assert.Equal(t, dao.CaseResult{ + Result: domain.ResultBasic.ToUint8(), + Cid: csid, + Uid: uid, + }, caseRes) + }, + wantCode: 200, + req: web.ExamineReq{ + Cid: 2, + Input: "测试一下", + }, + wantResp: test.Result[web.ExamineResult]{ + Data: web.ExamineResult{ + Result: domain.ResultBasic.ToUint8(), + RawResult: "评分:15K", + Amount: uid, + }, + }, + }, + } + + for _, tc := range testCases { + s.T().Run(tc.name, func(t *testing.T) { + tc.before(t) + req, err := http.NewRequest(http.MethodPost, + "/cases/examine", iox.NewJSONReader(tc.req)) + req.Header.Set("content-type", "application/json") + require.NoError(t, err) + recorder := test.NewJSONResponseRecorder[web.ExamineResult]() + s.server.ServeHTTP(recorder, req) + require.Equal(t, tc.wantCode, recorder.Code) + assert.Equal(t, tc.wantResp, recorder.MustScan()) + tc.after(t) + }) + } +} + +func TestExamineHandler(t *testing.T) { + suite.Run(t, new(ExamineHandlerTest)) +} diff --git a/internal/cases/internal/integration/handler_test.go b/internal/cases/internal/integration/handler_test.go index 4b7629d0..d92b06a3 100644 --- a/internal/cases/internal/integration/handler_test.go +++ b/internal/cases/internal/integration/handler_test.go @@ -161,12 +161,15 @@ func (s *HandlerTestSuite) TestSave() { Valid: true, Val: []string{"MySQL"}, }, - Status: domain.UnPublishedStatus.ToUint8(), - CodeRepo: "www.github.com", - Keywords: "mysql_keywords", - Shorthand: "mysql_shorthand", - Highlight: "mysql_highlight", - Guidance: "mysql_guidance", + BizId: 11, + Biz: "question", + Status: domain.UnPublishedStatus.ToUint8(), + GithubRepo: "www.github.com", + GiteeRepo: "www.gitee.com", + Keywords: "mysql_keywords", + Shorthand: "mysql_shorthand", + Highlight: "mysql_highlight", + Guidance: "mysql_guidance", }, ca) }, req: web.SaveReq{ @@ -175,10 +178,13 @@ func (s *HandlerTestSuite) TestSave() { Content: "案例1内容", Introduction: "案例1介绍", Labels: []string{"MySQL"}, - CodeRepo: "www.github.com", + GithubRepo: "www.github.com", + GiteeRepo: "www.gitee.com", Keywords: "mysql_keywords", Shorthand: "mysql_shorthand", Highlight: "mysql_highlight", + BizId: 11, + Biz: "question", Guidance: "mysql_guidance", }, }, @@ -202,14 +208,16 @@ func (s *HandlerTestSuite) TestSave() { Valid: true, Val: []string{"old-MySQL"}, }, - - CodeRepo: "old-github.com", - Keywords: "old_mysql_keywords", - Shorthand: "old_mysql_shorthand", - Highlight: "old_mysql_highlight", - Guidance: "old_mysql_guidance", - Ctime: 123, - Utime: 234, + BizId: 12, + Biz: "xxx", + GithubRepo: "old-github.com", + GiteeRepo: "old-gitee.com", + Keywords: "old_mysql_keywords", + Shorthand: "old_mysql_shorthand", + Highlight: "old_mysql_highlight", + Guidance: "old_mysql_guidance", + Ctime: 123, + Utime: 234, }).Error require.NoError(t, err) }, @@ -229,12 +237,15 @@ func (s *HandlerTestSuite) TestSave() { Valid: true, Val: []string{"MySQL"}, }, - Status: domain.UnPublishedStatus.ToUint8(), - CodeRepo: "www.github.com", - Keywords: "mysql_keywords", - Shorthand: "mysql_shorthand", - Highlight: "mysql_highlight", - Guidance: "mysql_guidance", + Status: domain.UnPublishedStatus.ToUint8(), + GithubRepo: "www.github.com", + GiteeRepo: "www.gitee.com", + Keywords: "mysql_keywords", + Shorthand: "mysql_shorthand", + Highlight: "mysql_highlight", + Guidance: "mysql_guidance", + BizId: 11, + Biz: "question", }, ca) }, req: web.SaveReq{ @@ -244,11 +255,14 @@ func (s *HandlerTestSuite) TestSave() { Introduction: "案例2介绍", Content: "案例2内容", Labels: []string{"MySQL"}, - CodeRepo: "www.github.com", + GithubRepo: "www.github.com", + GiteeRepo: "www.gitee.com", Keywords: "mysql_keywords", Shorthand: "mysql_shorthand", Highlight: "mysql_highlight", Guidance: "mysql_guidance", + BizId: 11, + Biz: "question", }, }, wantCode: 200, @@ -379,16 +393,19 @@ func (s *HandlerTestSuite) TestDetail() { Valid: true, Val: []string{"Redis"}, }, - Status: domain.PublishedStatus.ToUint8(), - Title: "redis案例标题", - Content: "redis案例内容", - CodeRepo: "redis仓库", - Keywords: "redis_keywords", - Shorthand: "redis_shorthand", - Highlight: "redis_highlight", - Guidance: "redis_guidance", - Ctime: 12, - Utime: 12, + Status: domain.PublishedStatus.ToUint8(), + Title: "redis案例标题", + Content: "redis案例内容", + GithubRepo: "redis github仓库", + GiteeRepo: "redis gitee仓库", + Keywords: "redis_keywords", + Shorthand: "redis_shorthand", + Highlight: "redis_highlight", + Guidance: "redis_guidance", + Biz: "case", + BizId: 11, + Ctime: 12, + Utime: 12, }).Error require.NoError(s.T(), err) testCases := []struct { @@ -406,17 +423,20 @@ func (s *HandlerTestSuite) TestDetail() { wantCode: 200, wantResp: test.Result[web.Case]{ Data: web.Case{ - Id: 3, - Labels: []string{"Redis"}, - Title: "redis案例标题", - Content: "redis案例内容", - CodeRepo: "redis仓库", - Status: domain.PublishedStatus.ToUint8(), - Keywords: "redis_keywords", - Shorthand: "redis_shorthand", - Highlight: "redis_highlight", - Guidance: "redis_guidance", - Utime: 12, + Id: 3, + Labels: []string{"Redis"}, + Title: "redis案例标题", + Content: "redis案例内容", + GithubRepo: "redis github仓库", + GiteeRepo: "redis gitee仓库", + Status: domain.PublishedStatus.ToUint8(), + Keywords: "redis_keywords", + Shorthand: "redis_shorthand", + Highlight: "redis_highlight", + Guidance: "redis_guidance", + Biz: "case", + BizId: 11, + Utime: 12, }, }, }, @@ -467,12 +487,16 @@ func (s *HandlerTestSuite) TestPublish() { Valid: true, Val: []string{"MySQL"}, }, - Status: domain.PublishedStatus.ToUint8(), - CodeRepo: "www.github.com", - Keywords: "mysql_keywords", - Shorthand: "mysql_shorthand", - Highlight: "mysql_highlight", - Guidance: "mysql_guidance", + Status: domain.PublishedStatus.ToUint8(), + + GithubRepo: "www.github.com", + GiteeRepo: "www.gitee.com", + Keywords: "mysql_keywords", + Shorthand: "mysql_shorthand", + Highlight: "mysql_highlight", + Guidance: "mysql_guidance", + Biz: "case", + BizId: 11, } s.assertCase(t, wantCase, ca) publishCase, err := s.dao.GetPublishCase(ctx, 1) @@ -485,11 +509,14 @@ func (s *HandlerTestSuite) TestPublish() { Content: "案例1内容", Introduction: "案例1介绍", Labels: []string{"MySQL"}, - CodeRepo: "www.github.com", + GithubRepo: "www.github.com", + GiteeRepo: "www.gitee.com", Keywords: "mysql_keywords", Shorthand: "mysql_shorthand", Highlight: "mysql_highlight", Guidance: "mysql_guidance", + Biz: "case", + BizId: 11, }, }, wantCode: 200, @@ -516,13 +543,16 @@ func (s *HandlerTestSuite) TestPublish() { Valid: true, Val: []string{"old-MySQL"}, }, - CodeRepo: "old-github.com", - Keywords: "old_mysql_keywords", - Shorthand: "old_mysql_shorthand", - Highlight: "old_mysql_highlight", - Guidance: "old_mysql_guidance", - Ctime: 123, - Utime: 234, + GithubRepo: "old-github.com", + GiteeRepo: "old-gitee.com", + Keywords: "old_mysql_keywords", + Shorthand: "old_mysql_shorthand", + Highlight: "old_mysql_highlight", + Guidance: "old_mysql_guidance", + Biz: "case", + BizId: 11, + Ctime: 123, + Utime: 234, }).Error require.NoError(t, err) }, @@ -540,12 +570,15 @@ func (s *HandlerTestSuite) TestPublish() { Valid: true, Val: []string{"MySQL"}, }, - Status: domain.PublishedStatus.ToUint8(), - CodeRepo: "www.github.com", - Keywords: "mysql_keywords", - Shorthand: "mysql_shorthand", - Highlight: "mysql_highlight", - Guidance: "mysql_guidance", + Status: domain.PublishedStatus.ToUint8(), + GithubRepo: "www.github.com", + GiteeRepo: "www.gitee.com", + Keywords: "mysql_keywords", + Shorthand: "mysql_shorthand", + Highlight: "mysql_highlight", + Guidance: "mysql_guidance", + Biz: "question", + BizId: 12, } s.assertCase(t, wantCase, ca) publishCase, err := s.dao.GetPublishCase(ctx, 2) @@ -560,11 +593,14 @@ func (s *HandlerTestSuite) TestPublish() { Content: "案例2内容", Introduction: "案例2介绍", Labels: []string{"MySQL"}, - CodeRepo: "www.github.com", + GithubRepo: "www.github.com", + GiteeRepo: "www.gitee.com", Keywords: "mysql_keywords", Shorthand: "mysql_shorthand", Highlight: "mysql_highlight", Guidance: "mysql_guidance", + Biz: "question", + BizId: 12, }, }, wantCode: 200, @@ -591,13 +627,16 @@ func (s *HandlerTestSuite) TestPublish() { Valid: true, Val: []string{"old-MySQL"}, }, - CodeRepo: "old-github.com", - Keywords: "old_mysql_keywords", - Shorthand: "old_mysql_shorthand", - Highlight: "old_mysql_highlight", - Guidance: "old_mysql_guidance", - Ctime: 123, - Utime: 234, + GithubRepo: "old-github.com", + GiteeRepo: "old-gitee.com", + Keywords: "old_mysql_keywords", + Shorthand: "old_mysql_shorthand", + Highlight: "old_mysql_highlight", + Guidance: "old_mysql_guidance", + Biz: "question", + BizId: 12, + Ctime: 123, + Utime: 234, } err := s.db.WithContext(ctx).Create(&oldCase).Error require.NoError(t, err) @@ -619,12 +658,15 @@ func (s *HandlerTestSuite) TestPublish() { Valid: true, Val: []string{"MySQL"}, }, - Status: domain.PublishedStatus.ToUint8(), - CodeRepo: "www.github.com", - Keywords: "mysql_keywords", - Shorthand: "mysql_shorthand", - Highlight: "mysql_highlight", - Guidance: "mysql_guidance", + Status: domain.PublishedStatus.ToUint8(), + GithubRepo: "www.github.com", + GiteeRepo: "www.gitee.com", + Keywords: "mysql_keywords", + Shorthand: "mysql_shorthand", + Highlight: "mysql_highlight", + Guidance: "mysql_guidance", + Biz: "ai", + BizId: 13, } s.assertCase(t, wantCase, ca) publishCase, err := s.dao.GetPublishCase(ctx, 3) @@ -639,11 +681,14 @@ func (s *HandlerTestSuite) TestPublish() { Content: "案例2内容", Introduction: "案例2介绍", Labels: []string{"MySQL"}, - CodeRepo: "www.github.com", + GithubRepo: "www.github.com", + GiteeRepo: "www.gitee.com", Keywords: "mysql_keywords", Shorthand: "mysql_shorthand", Highlight: "mysql_highlight", Guidance: "mysql_guidance", + Biz: "ai", + BizId: 13, }, }, wantCode: 200, @@ -785,15 +830,18 @@ func (s *HandlerTestSuite) TestPubDetail() { Valid: true, Val: []string{"Redis"}, }, - Status: domain.PublishedStatus.ToUint8(), - Title: "redis案例标题", - Content: "redis案例内容", - CodeRepo: "redis仓库", - Keywords: "redis_keywords", - Shorthand: "redis_shorthand", - Highlight: "redis_highlight", - Guidance: "redis_guidance", - Utime: 13, + Status: domain.PublishedStatus.ToUint8(), + Title: "redis案例标题", + Content: "redis案例内容", + GithubRepo: "redis github仓库", + GiteeRepo: "redis gitee仓库", + Keywords: "redis_keywords", + Shorthand: "redis_shorthand", + Highlight: "redis_highlight", + Guidance: "redis_guidance", + Biz: "ai", + BizId: 13, + Utime: 13, }).Error require.NoError(s.T(), err) testCases := []struct { @@ -816,12 +864,15 @@ func (s *HandlerTestSuite) TestPubDetail() { Title: "redis案例标题", Introduction: "redis案例介绍", Content: "redis案例内容", - CodeRepo: "redis仓库", + GithubRepo: "redis github仓库", + GiteeRepo: "redis gitee仓库", Status: domain.PublishedStatus.ToUint8(), Keywords: "redis_keywords", Shorthand: "redis_shorthand", Highlight: "redis_highlight", Guidance: "redis_guidance", + Biz: "ai", + BizId: 13, Utime: 13, Interactive: web.Interactive{ Liked: true, @@ -867,14 +918,15 @@ func (s *HandlerTestSuite) TestEvent() { // 发布 publishReq := web.SaveReq{ Case: web.Case{ - Title: "案例2", - Content: "案例2内容", - Labels: []string{"MySQL"}, - CodeRepo: "www.github.com", - Keywords: "mysql_keywords", - Shorthand: "mysql_shorthand", - Highlight: "mysql_highlight", - Guidance: "mysql_guidance", + Title: "案例2", + Content: "案例2内容", + Labels: []string{"MySQL"}, + GithubRepo: "www.github.com", + GiteeRepo: "www.gitee.com", + Keywords: "mysql_keywords", + Shorthand: "mysql_shorthand", + Highlight: "mysql_highlight", + Guidance: "mysql_guidance", }, } req2, err := http.NewRequest(http.MethodPost, @@ -892,16 +944,17 @@ func (s *HandlerTestSuite) TestEvent() { assert.True(t, evt.Id > 0) evt.Id = 0 assert.Equal(t, event.Case{ - Title: "案例2", - Uid: uid, - Content: "案例2内容", - Labels: []string{"MySQL"}, - CodeRepo: "www.github.com", - Keywords: "mysql_keywords", - Shorthand: "mysql_shorthand", - Highlight: "mysql_highlight", - Guidance: "mysql_guidance", - Status: 2, + Title: "案例2", + Uid: uid, + Content: "案例2内容", + Labels: []string{"MySQL"}, + GithubRepo: "www.github.com", + GiteeRepo: "www.gitee.com", + Keywords: "mysql_keywords", + Shorthand: "mysql_shorthand", + Highlight: "mysql_highlight", + Guidance: "mysql_guidance", + Status: 2, }, evt) } diff --git a/internal/cases/internal/integration/startup/wire.go b/internal/cases/internal/integration/startup/wire.go index 2384ab46..759eb45a 100644 --- a/internal/cases/internal/integration/startup/wire.go +++ b/internal/cases/internal/integration/startup/wire.go @@ -17,9 +17,11 @@ package startup import ( + "github.com/ecodeclub/webook/internal/ai" "github.com/ecodeclub/webook/internal/cases" "github.com/ecodeclub/webook/internal/cases/internal/event" "github.com/ecodeclub/webook/internal/cases/internal/repository" + "github.com/ecodeclub/webook/internal/cases/internal/repository/dao" "github.com/ecodeclub/webook/internal/cases/internal/service" "github.com/ecodeclub/webook/internal/cases/internal/web" "github.com/ecodeclub/webook/internal/interactive" @@ -32,11 +34,42 @@ func InitModule( intrModule *interactive.Module) (*cases.Module, error) { wire.Build(cases.InitCaseDAO, testioc.BaseSet, + dao.NewCaseSetDAO, repository.NewCaseRepo, + repository.NewCaseSetRepo, event.NewInteractiveEventProducer, service.NewService, + service.NewCaseSetService, web.NewHandler, + web.NewAdminCaseSetHandler, wire.FieldsOf(new(*interactive.Module), "Svc"), + wire.Struct(new(cases.Module), "Svc", "Hdl", "AdminSetHandler"), + ) + return new(cases.Module), nil +} + +func InitExamModule( + syncProducer event.SyncEventProducer, + intrModule *interactive.Module, + aiModule *ai.Module) (*cases.Module, error) { + wire.Build( + testioc.BaseSet, + cases.InitCaseDAO, + dao.NewCaseSetDAO, + dao.NewGORMExamineDAO, + repository.NewCaseRepo, + repository.NewCaseSetRepo, + repository.NewCachedExamineRepository, + event.NewInteractiveEventProducer, + service.NewCaseSetService, + service.NewService, + service.NewLLMExamineService, + web.NewHandler, + web.NewAdminCaseSetHandler, + web.NewExamineHandler, + web.NewCaseSetHandler, + wire.FieldsOf(new(*interactive.Module), "Svc"), + wire.FieldsOf(new(*ai.Module), "Svc"), wire.Struct(new(cases.Module), "*"), ) return new(cases.Module), nil diff --git a/internal/cases/internal/integration/startup/wire_gen.go b/internal/cases/internal/integration/startup/wire_gen.go index e924b93a..f1b8aa18 100644 --- a/internal/cases/internal/integration/startup/wire_gen.go +++ b/internal/cases/internal/integration/startup/wire_gen.go @@ -1,15 +1,17 @@ // 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 package startup import ( + "github.com/ecodeclub/webook/internal/ai" "github.com/ecodeclub/webook/internal/cases" "github.com/ecodeclub/webook/internal/cases/internal/event" "github.com/ecodeclub/webook/internal/cases/internal/repository" + "github.com/ecodeclub/webook/internal/cases/internal/repository/dao" "github.com/ecodeclub/webook/internal/cases/internal/service" "github.com/ecodeclub/webook/internal/cases/internal/web" "github.com/ecodeclub/webook/internal/interactive" @@ -30,9 +32,46 @@ func InitModule(syncProducer event.SyncEventProducer, intrModule *interactive.Mo serviceService := service.NewService(caseRepo, interactiveEventProducer, syncProducer) service2 := intrModule.Svc handler := web.NewHandler(serviceService, service2) + caseSetDAO := dao.NewCaseSetDAO(db) + caseSetRepository := repository.NewCaseSetRepo(caseSetDAO) + caseSetService := service.NewCaseSetService(caseSetRepository, caseRepo) + adminCaseSetHandler := web.NewAdminCaseSetHandler(caseSetService) module := &cases.Module{ - Svc: serviceService, - Hdl: handler, + Svc: serviceService, + Hdl: handler, + AdminSetHandler: adminCaseSetHandler, + } + return module, nil +} + +func InitExamModule(syncProducer event.SyncEventProducer, intrModule *interactive.Module, aiModule *ai.Module) (*cases.Module, error) { + db := testioc.InitDB() + caseDAO := cases.InitCaseDAO(db) + caseRepo := repository.NewCaseRepo(caseDAO) + mq := testioc.InitMQ() + interactiveEventProducer, err := event.NewInteractiveEventProducer(mq) + if err != nil { + return nil, err + } + serviceService := service.NewService(caseRepo, interactiveEventProducer, syncProducer) + service2 := intrModule.Svc + handler := web.NewHandler(serviceService, service2) + caseSetDAO := dao.NewCaseSetDAO(db) + caseSetRepository := repository.NewCaseSetRepo(caseSetDAO) + caseSetService := service.NewCaseSetService(caseSetRepository, caseRepo) + adminCaseSetHandler := web.NewAdminCaseSetHandler(caseSetService) + examineDAO := dao.NewGORMExamineDAO(db) + examineRepository := repository.NewCachedExamineRepository(examineDAO) + llmService := aiModule.Svc + examineService := service.NewLLMExamineService(caseRepo, examineRepository, llmService) + examineHandler := web.NewExamineHandler(examineService) + caseSetHandler := web.NewCaseSetHandler(caseSetService, examineService, service2) + module := &cases.Module{ + Svc: serviceService, + Hdl: handler, + AdminSetHandler: adminCaseSetHandler, + ExamineHdl: examineHandler, + CsHdl: caseSetHandler, } return module, nil } diff --git a/internal/cases/internal/repository/case_set.go b/internal/cases/internal/repository/case_set.go new file mode 100644 index 00000000..fb119ddd --- /dev/null +++ b/internal/cases/internal/repository/case_set.go @@ -0,0 +1,181 @@ +package repository + +import ( + "context" + "time" + + "github.com/ecodeclub/ekit/slice" + "github.com/ecodeclub/webook/internal/cases/internal/domain" + "github.com/ecodeclub/webook/internal/cases/internal/repository/dao" +) + +type CaseSetRepository interface { + CreateCaseSet(ctx context.Context, set domain.CaseSet) (int64, error) + UpdateCases(ctx context.Context, set domain.CaseSet) error + GetByID(ctx context.Context, id int64) (domain.CaseSet, error) + Total(ctx context.Context) (int64, error) + List(ctx context.Context, offset int, limit int) ([]domain.CaseSet, error) + UpdateNonZero(ctx context.Context, set domain.CaseSet) error + GetByIDs(ctx context.Context, ids []int64) ([]domain.CaseSet, error) + + ListByBiz(ctx context.Context, offset, limit int, biz string) ([]domain.CaseSet, error) + GetByBiz(ctx context.Context, biz string, bizId int64) (domain.CaseSet, error) +} + +type caseSetRepo struct { + dao dao.CaseSetDAO +} + +func NewCaseSetRepo(caseSetDao dao.CaseSetDAO) CaseSetRepository { + return &caseSetRepo{ + dao: caseSetDao, + } +} + +func (c *caseSetRepo) ListByBiz(ctx context.Context, offset, limit int, biz string) ([]domain.CaseSet, error) { + qs, err := c.dao.ListByBiz(ctx, offset, limit, biz) + if err != nil { + return nil, err + } + return slice.Map(qs, func(idx int, src dao.CaseSet) domain.CaseSet { + return c.toDomainCaseSet(src) + }), err +} + +func (c *caseSetRepo) GetByBiz(ctx context.Context, biz string, bizId int64) (domain.CaseSet, error) { + set, err := c.dao.GetByBiz(ctx, biz, bizId) + if err != nil { + return domain.CaseSet{}, err + } + cases, err := c.getDomainCases(ctx, set.Id) + if err != nil { + return domain.CaseSet{}, err + } + return domain.CaseSet{ + ID: set.Id, + Uid: set.Uid, + Title: set.Title, + Biz: set.Biz, + BizId: set.BizId, + Description: set.Description, + Cases: cases, + Utime: set.Utime, + }, nil +} + +func (c *caseSetRepo) CreateCaseSet(ctx context.Context, set domain.CaseSet) (int64, error) { + return c.dao.Create(ctx, c.toEntityQuestionSet(set)) +} + +func (c *caseSetRepo) UpdateCases(ctx context.Context, set domain.CaseSet) error { + cids := make([]int64, 0, len(set.Cases)) + for i := range set.Cases { + cids = append(cids, set.Cases[i].Id) + } + return c.dao.UpdateCasesByID(ctx, set.ID, cids) +} + +func (c *caseSetRepo) GetByID(ctx context.Context, id int64) (domain.CaseSet, error) { + set, err := c.dao.GetByID(ctx, id) + if err != nil { + return domain.CaseSet{}, err + } + cases, err := c.getDomainCases(ctx, id) + if err != nil { + return domain.CaseSet{}, err + } + + return domain.CaseSet{ + ID: set.Id, + Uid: set.Uid, + Title: set.Title, + Description: set.Description, + BizId: set.BizId, + Biz: set.Biz, + Cases: cases, + Utime: set.Utime, + }, nil +} + +func (c *caseSetRepo) getDomainCases(ctx context.Context, id int64) ([]domain.Case, error) { + cases, err := c.dao.GetCasesByID(ctx, id) + if err != nil { + return nil, err + } + return slice.Map(cases, func(idx int, src dao.Case) domain.Case { + return c.toDomainCase(src) + }), err +} + +func (c *caseSetRepo) Total(ctx context.Context) (int64, error) { + return c.dao.Count(ctx) +} + +func (c *caseSetRepo) List(ctx context.Context, offset int, limit int) ([]domain.CaseSet, error) { + qs, err := c.dao.List(ctx, offset, limit) + if err != nil { + return nil, err + } + return slice.Map(qs, func(idx int, src dao.CaseSet) domain.CaseSet { + return c.toDomainCaseSet(src) + }), err +} + +func (c *caseSetRepo) UpdateNonZero(ctx context.Context, set domain.CaseSet) error { + return c.dao.UpdateNonZero(ctx, c.toEntityQuestionSet(set)) +} + +func (c *caseSetRepo) GetByIDs(ctx context.Context, ids []int64) ([]domain.CaseSet, error) { + qs, err := c.dao.GetByIDs(ctx, ids) + if err != nil { + return nil, err + } + return slice.Map(qs, func(idx int, src dao.CaseSet) domain.CaseSet { + return c.toDomainCaseSet(src) + }), err +} + +func (c *caseSetRepo) toEntityQuestionSet(d domain.CaseSet) dao.CaseSet { + return dao.CaseSet{ + Id: d.ID, + Uid: d.Uid, + Title: d.Title, + Description: d.Description, + BizId: d.BizId, + Biz: d.Biz, + } +} + +func (c *caseSetRepo) toDomainCase(caseDao dao.Case) domain.Case { + return domain.Case{ + Id: caseDao.Id, + Uid: caseDao.Uid, + Introduction: caseDao.Introduction, + Labels: caseDao.Labels.Val, + Title: caseDao.Title, + Content: caseDao.Content, + GiteeRepo: caseDao.GiteeRepo, + GithubRepo: caseDao.GithubRepo, + Keywords: caseDao.Keywords, + Shorthand: caseDao.Shorthand, + Highlight: caseDao.Highlight, + Guidance: caseDao.Guidance, + Status: domain.CaseStatus(caseDao.Status), + Biz: caseDao.Biz, + BizId: caseDao.BizId, + Utime: time.UnixMilli(caseDao.Utime), + Ctime: time.UnixMilli(caseDao.Ctime), + } +} + +func (c *caseSetRepo) toDomainCaseSet(caseSetDao dao.CaseSet) domain.CaseSet { + return domain.CaseSet{ + ID: caseSetDao.Id, + Uid: caseSetDao.Uid, + Title: caseSetDao.Title, + Description: caseSetDao.Description, + BizId: caseSetDao.BizId, + Biz: caseSetDao.Biz, + Utime: caseSetDao.Utime, + } +} diff --git a/internal/cases/internal/repository/cases.go b/internal/cases/internal/repository/cases.go index e82dc38b..30f9c237 100644 --- a/internal/cases/internal/repository/cases.go +++ b/internal/cases/internal/repository/cases.go @@ -4,6 +4,8 @@ import ( "context" "time" + "golang.org/x/sync/errgroup" + "github.com/ecodeclub/ekit/slice" "github.com/ecodeclub/ekit/sqlx" @@ -23,12 +25,38 @@ type CaseRepo interface { Total(ctx context.Context) (int64, error) Save(ctx context.Context, ca domain.Case) (int64, error) GetById(ctx context.Context, caseId int64) (domain.Case, error) + + // Exclude 分页接口,不含这些 id 的问题 + Exclude(ctx context.Context, ids []int64, offset int, limit int) ([]domain.Case, int64, error) } type caseRepo struct { caseDao dao.CaseDAO } +func (c *caseRepo) Exclude(ctx context.Context, ids []int64, offset int, limit int) ([]domain.Case, int64, error) { + var ( + eg errgroup.Group + cnt int64 + data []dao.Case + ) + eg.Go(func() error { + var err error + cnt, err = c.caseDao.NotInTotal(ctx, ids) + return err + }) + + eg.Go(func() error { + var err error + data, err = c.caseDao.NotIn(ctx, ids, offset, limit) + return err + }) + err := eg.Wait() + return slice.Map(data, func(idx int, src dao.Case) domain.Case { + return c.toDomain(src) + }), cnt, err +} + func (c *caseRepo) PubList(ctx context.Context, offset int, limit int) ([]domain.Case, error) { caseList, err := c.caseDao.PublishCaseList(ctx, offset, limit) if err != nil { @@ -101,11 +129,14 @@ func (c *caseRepo) toEntity(caseDomain domain.Case) dao.Case { Introduction: caseDomain.Introduction, Title: caseDomain.Title, Content: caseDomain.Content, - CodeRepo: caseDomain.CodeRepo, + GithubRepo: caseDomain.GithubRepo, + GiteeRepo: caseDomain.GiteeRepo, Keywords: caseDomain.Keywords, Shorthand: caseDomain.Shorthand, Highlight: caseDomain.Highlight, Guidance: caseDomain.Guidance, + Biz: caseDomain.Biz, + BizId: caseDomain.BizId, Status: caseDomain.Status.ToUint8(), } } @@ -118,11 +149,14 @@ func (c *caseRepo) toDomain(caseDao dao.Case) domain.Case { Labels: caseDao.Labels.Val, Title: caseDao.Title, Content: caseDao.Content, - CodeRepo: caseDao.CodeRepo, + GithubRepo: caseDao.GithubRepo, + GiteeRepo: caseDao.GiteeRepo, Keywords: caseDao.Keywords, Shorthand: caseDao.Shorthand, Highlight: caseDao.Highlight, Guidance: caseDao.Guidance, + Biz: caseDao.Biz, + BizId: caseDao.BizId, Utime: time.UnixMilli(caseDao.Utime), Ctime: time.UnixMilli(caseDao.Ctime), Status: domain.CaseStatus(caseDao.Status), diff --git a/internal/cases/internal/repository/dao/cases.go b/internal/cases/internal/repository/dao/cases.go index 33d7c4d5..15003be2 100644 --- a/internal/cases/internal/repository/dao/cases.go +++ b/internal/cases/internal/repository/dao/cases.go @@ -24,6 +24,9 @@ type CaseDAO interface { PublishCaseCount(ctx context.Context) (int64, error) GetPublishCase(ctx context.Context, caseId int64) (PublishCase, error) GetPubByIDs(ctx context.Context, ids []int64) ([]PublishCase, error) + + NotInTotal(ctx context.Context, ids []int64) (int64, error) + NotIn(ctx context.Context, ids []int64, offset int, limit int) ([]Case, error) } type caseDAO struct { @@ -32,6 +35,23 @@ type caseDAO struct { updateColumns []string } +func (ca *caseDAO) NotInTotal(ctx context.Context, ids []int64) (int64, error) { + var res int64 + err := ca.db.WithContext(ctx). + Model(&Case{}). + Where("id NOT IN ?", ids).Count(&res).Error + return res, err +} + +func (ca *caseDAO) NotIn(ctx context.Context, ids []int64, offset int, limit int) ([]Case, error) { + var res []Case + err := ca.db.WithContext(ctx). + Model(&Case{}). + Where("id NOT IN ?", ids).Order("utime DESC"). + Offset(offset).Limit(limit).Find(&res).Error + return res, err +} + func (ca *caseDAO) Count(ctx context.Context) (int64, error) { var res int64 err := ca.db.WithContext(ctx).Model(&Case{}).Select("COUNT(id)").Count(&res).Error @@ -122,7 +142,7 @@ func NewCaseDao(db *egorm.Component) CaseDAO { listColumns: []string{"id", "labels", "status", "introduction", "title", "utime"}, updateColumns: []string{ "introduction", "labels", "title", "content", - "code_repo", "keywords", "shorthand", "highlight", - "guidance", "status", "utime"}, + "github_repo", "gitee_repo", "keywords", "shorthand", "highlight", + "guidance", "status", "utime", "biz", "biz_id"}, } } diff --git a/internal/cases/internal/repository/dao/cases_set.go b/internal/cases/internal/repository/dao/cases_set.go new file mode 100644 index 00000000..b095e8bb --- /dev/null +++ b/internal/cases/internal/repository/dao/cases_set.go @@ -0,0 +1,138 @@ +package dao + +import ( + "context" + "time" + + "github.com/ego-component/egorm" + "gorm.io/gorm" +) + +type CaseSetDAO interface { + Create(ctx context.Context, cs CaseSet) (int64, error) + GetByID(ctx context.Context, id int64) (CaseSet, error) + + GetCasesByID(ctx context.Context, id int64) ([]Case, error) + UpdateCasesByID(ctx context.Context, id int64, cids []int64) error + + Count(ctx context.Context) (int64, error) + List(ctx context.Context, offset, limit int) ([]CaseSet, error) + UpdateNonZero(ctx context.Context, set CaseSet) error + GetByIDs(ctx context.Context, ids []int64) ([]CaseSet, error) + + ListByBiz(ctx context.Context, offset int, limit int, biz string) ([]CaseSet, error) + GetByBiz(ctx context.Context, biz string, bizId int64) (CaseSet, error) +} + +type caseSetDAO struct { + db *egorm.Component +} + +func NewCaseSetDAO(db *egorm.Component) CaseSetDAO { + return &caseSetDAO{db: db} +} + +func (c *caseSetDAO) ListByBiz(ctx context.Context, offset int, limit int, biz string) ([]CaseSet, error) { + var res []CaseSet + db := c.db.WithContext(ctx) + err := db.Where("biz = ?", biz). + Offset(offset).Limit(limit).Order("id DESC").Find(&res).Error + return res, err +} + +func (c *caseSetDAO) GetByBiz(ctx context.Context, biz string, bizId int64) (CaseSet, error) { + var res CaseSet + db := c.db.WithContext(ctx) + err := db.Where("biz = ? AND biz_id = ?", biz, bizId). + Order("utime DESC"). + First(&res).Error + return res, err +} + +func (c *caseSetDAO) Create(ctx context.Context, cs CaseSet) (int64, error) { + now := time.Now().UnixMilli() + cs.Ctime = now + cs.Utime = now + err := c.db.WithContext(ctx).Create(&cs).Error + return cs.Id, err +} + +func (c *caseSetDAO) GetByID(ctx context.Context, id int64) (CaseSet, error) { + var cs CaseSet + err := c.db.WithContext(ctx).Where("id = ?", id).First(&cs).Error + return cs, err +} + +func (c *caseSetDAO) GetCasesByID(ctx context.Context, id int64) ([]Case, error) { + var cids []int64 + err := c.db.WithContext(ctx). + Select("cid"). + Model(&CaseSetCase{}). + Where("cs_id = ?", id). + Scan(&cids).Error + if err != nil { + return nil, err + } + var cs []Case + err = c.db.WithContext(ctx). + Model(&Case{}). + Where("id in ?", cids). + Scan(&cs).Error + return cs, err + +} + +func (c *caseSetDAO) UpdateCasesByID(ctx context.Context, id int64, cids []int64) error { + return c.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + var cs CaseSet + if err := tx.WithContext(ctx).First(&cs, "id = ? ", id).Error; err != nil { + return err + } + // 全部删除 + if err := tx.WithContext(ctx).Where("cs_id = ?", id).Delete(&CaseSetCase{}).Error; err != nil { + return err + } + + if len(cids) == 0 { + return nil + } + + // 重新创建 + now := time.Now().UnixMilli() + var newQuestions []CaseSetCase + for i := range cids { + newQuestions = append(newQuestions, CaseSetCase{ + CSID: id, + CID: cids[i], + Ctime: now, + Utime: now, + }) + } + return tx.WithContext(ctx).Create(&newQuestions).Error + }) +} + +func (c *caseSetDAO) Count(ctx context.Context) (int64, error) { + var res int64 + db := c.db.WithContext(ctx).Model(&CaseSet{}) + err := db.Select("COUNT(id)").Count(&res).Error + return res, err +} + +func (c *caseSetDAO) List(ctx context.Context, offset, limit int) ([]CaseSet, error) { + var res []CaseSet + db := c.db.WithContext(ctx) + err := db.Offset(offset).Limit(limit).Order("id DESC").Find(&res).Error + return res, err +} + +func (c *caseSetDAO) UpdateNonZero(ctx context.Context, set CaseSet) error { + set.Utime = time.Now().UnixMilli() + return c.db.WithContext(ctx).Where("id = ?", set.Id).Updates(set).Error +} + +func (c *caseSetDAO) GetByIDs(ctx context.Context, ids []int64) ([]CaseSet, error) { + var res []CaseSet + err := c.db.Model(&CaseSet{}).WithContext(ctx).Where("id in (?)", ids).Find(&res).Error + return res, err +} diff --git a/internal/cases/internal/repository/dao/exam_type.go b/internal/cases/internal/repository/dao/exam_type.go new file mode 100644 index 00000000..68a8ab99 --- /dev/null +++ b/internal/cases/internal/repository/dao/exam_type.go @@ -0,0 +1,32 @@ +package dao + +// ExamineRecord 业务层面上记录 +type CaseExamineRecord struct { + Id int64 + Uid int64 + Cid int64 + // 代表这一次测试的 ID + // 这个主要是为了和 AI 打交道,有一个唯一凭证 + Tid string + Result uint8 + // 原始的 AI 回答 + RawResult string + // 冗余字段,使用的 tokens 数量 + Tokens int64 + // 冗余字段,花费的金额 + Amount int64 + + Ctime int64 + Utime int64 +} + +// CaseResult 某人是否已经回答出来了 +type CaseResult struct { + Id int64 + // 目前来看,查询至少会有一个 uid,所以我们把 uid 放在唯一索引最前面 + Uid int64 `gorm:"uniqueIndex:uid_cid"` + Cid int64 `gorm:"uniqueIndex:uid_cid"` + Result uint8 + Ctime int64 + Utime int64 +} diff --git a/internal/cases/internal/repository/dao/examine.go b/internal/cases/internal/repository/dao/examine.go new file mode 100644 index 00000000..057e64cf --- /dev/null +++ b/internal/cases/internal/repository/dao/examine.go @@ -0,0 +1,64 @@ +package dao + +import ( + "context" + "time" + + "github.com/ego-component/egorm" + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +var ErrRecordNotFound = gorm.ErrRecordNotFound + +type ExamineDAO interface { + SaveResult(ctx context.Context, record CaseExamineRecord) error + GetResultByUidAndCid(ctx context.Context, uid int64, cid int64) (CaseResult, error) + GetResultByUidAndCids(ctx context.Context, uid int64, cids []int64) ([]CaseResult, error) +} + +type GORMExamineDAO struct { + db *egorm.Component +} + +func NewGORMExamineDAO(db *egorm.Component) ExamineDAO { + return &GORMExamineDAO{ + db: db, + } +} + +func (dao *GORMExamineDAO) SaveResult(ctx context.Context, record CaseExamineRecord) error { + now := time.Now().UnixMilli() + return dao.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + record.Ctime = now + record.Utime = now + err := tx.Create(&record).Error + if err != nil { + return err + } + return tx.Clauses(clause.OnConflict{ + // 如果有记录了,就更新结果和更新时间 + DoUpdates: clause.AssignmentColumns([]string{ + "result", "utime", + }), + }).Create(&CaseResult{ + Uid: record.Uid, + Cid: record.Cid, + Result: record.Result, + Ctime: now, + Utime: now, + }).Error + }) +} + +func (dao *GORMExamineDAO) GetResultByUidAndCid(ctx context.Context, uid int64, cid int64) (CaseResult, error) { + var res CaseResult + err := dao.db.WithContext(ctx).Where("uid = ? AND cid = ?", uid, cid).First(&res).Error + return res, err +} + +func (dao *GORMExamineDAO) GetResultByUidAndCids(ctx context.Context, uid int64, cids []int64) ([]CaseResult, error) { + var res []CaseResult + err := dao.db.WithContext(ctx).Where("uid = ? AND cid IN ?", uid, cids).Find(&res).Error + return res, err +} diff --git a/internal/cases/internal/repository/dao/init.go b/internal/cases/internal/repository/dao/init.go index 4b43e3cc..3af96b01 100644 --- a/internal/cases/internal/repository/dao/init.go +++ b/internal/cases/internal/repository/dao/init.go @@ -6,5 +6,11 @@ func InitTables(db *egorm.Component) error { return db.AutoMigrate( &Case{}, &PublishCase{}, + &CaseSet{}, + &CaseSetCase{}, + &CaseResult{}, + &CaseExamineRecord{}, + &CaseExamineRecord{}, + &CaseResult{}, ) } diff --git a/internal/cases/internal/repository/dao/types.go b/internal/cases/internal/repository/dao/types.go index 271ffc21..ad81e97c 100644 --- a/internal/cases/internal/repository/dao/types.go +++ b/internal/cases/internal/repository/dao/types.go @@ -13,7 +13,8 @@ type Case struct { // Case 内容 Content string // 代码仓库地址 - CodeRepo string + GithubRepo string + GiteeRepo string // 关键字,辅助记忆,提取重点 Keywords string // 速记,口诀 @@ -22,7 +23,9 @@ type Case struct { Highlight string // 引导点 Guidance string - Status uint8 `gorm:"type:tinyint(3);comment:0-未知 1-未发表 2-已发表"` + Status uint8 `gorm:"type:tinyint(3);comment:0-未知 1-未发表 2-已发表"` + Biz string `gorm:"type=varchar(256);index:biz;not null;default:'baguwen';"` + BizId int64 `gorm:"index:biz;not null;default:0;"` Ctime int64 Utime int64 `gorm:"index"` } @@ -36,3 +39,28 @@ type PublishCase Case func (PublishCase) TableName() string { return "publish_cases" } + +type CaseSet struct { + Id int64 `gorm:"primaryKey,autoIncrement"` + // 所有者 + Uid int64 `gorm:"index"` + // 题集标题 + Title string + // 题集描述 + Description string + + Biz string `gorm:"type=varchar(256);index:biz;not null;default:'baguwen';"` + BizId int64 `gorm:"index:biz;not null;default:0;"` + + Ctime int64 + Utime int64 `gorm:"index"` +} + +// QuestionSetQuestion 题集问题 —— 题集与题目的关联关系 +type CaseSetCase struct { + Id int64 `gorm:"primaryKey,autoIncrement"` + CSID int64 `gorm:"column:cs_id;uniqueIndex:csid_cid"` + CID int64 `gorm:"column:cid;uniqueIndex:csid_cid"` + Ctime int64 + Utime int64 `gorm:"index"` +} diff --git a/internal/cases/internal/repository/examine.go b/internal/cases/internal/repository/examine.go new file mode 100644 index 00000000..a22ed8b1 --- /dev/null +++ b/internal/cases/internal/repository/examine.go @@ -0,0 +1,59 @@ +package repository + +import ( + "context" + "errors" + + "github.com/ecodeclub/webook/internal/cases/internal/domain" + "github.com/ecodeclub/webook/internal/cases/internal/repository/dao" + + "github.com/ecodeclub/ekit/slice" +) + +type ExamineRepository interface { + SaveResult(ctx context.Context, uid, cid int64, result domain.ExamineCaseResult) error + GetResultByUidAndQid(ctx context.Context, uid int64, cid int64) (domain.CaseResult, error) + GetResultsByIds(ctx context.Context, uid int64, ids []int64) ([]domain.ExamineCaseResult, error) +} + +var _ ExamineRepository = &CachedExamineRepository{} + +type CachedExamineRepository struct { + dao dao.ExamineDAO +} + +func (repo *CachedExamineRepository) GetResultsByIds(ctx context.Context, uid int64, ids []int64) ([]domain.ExamineCaseResult, error) { + res, err := repo.dao.GetResultByUidAndCids(ctx, uid, ids) + return slice.Map(res, func(idx int, src dao.CaseResult) domain.ExamineCaseResult { + return domain.ExamineCaseResult{ + Cid: src.Cid, + Result: domain.CaseResult(src.Result), + } + }), err +} + +func (repo *CachedExamineRepository) GetResultByUidAndQid(ctx context.Context, uid int64, cid int64) (domain.CaseResult, error) { + res, err := repo.dao.GetResultByUidAndCid(ctx, uid, cid) + if errors.Is(err, dao.ErrRecordNotFound) { + return domain.ResultFailed, nil + } + return domain.CaseResult(res.Result), err +} + +func (repo *CachedExamineRepository) SaveResult(ctx context.Context, uid, cid int64, result domain.ExamineCaseResult) error { + // 开始记录 + err := repo.dao.SaveResult(ctx, dao.CaseExamineRecord{ + Uid: uid, + Cid: cid, + Tid: result.Tid, + Result: result.Result.ToUint8(), + RawResult: result.RawResult, + Tokens: result.Tokens, + Amount: result.Amount, + }) + return err +} + +func NewCachedExamineRepository(dao dao.ExamineDAO) ExamineRepository { + return &CachedExamineRepository{dao: dao} +} diff --git a/internal/cases/internal/service/case_set.go b/internal/cases/internal/service/case_set.go new file mode 100644 index 00000000..b5d739dc --- /dev/null +++ b/internal/cases/internal/service/case_set.go @@ -0,0 +1,102 @@ +package service + +import ( + "context" + + "github.com/ecodeclub/ekit/slice" + "github.com/ecodeclub/webook/internal/cases/internal/domain" + "github.com/ecodeclub/webook/internal/cases/internal/repository" + "golang.org/x/sync/errgroup" +) + +type CaseSetService interface { + Save(ctx context.Context, set domain.CaseSet) (int64, error) + UpdateCases(ctx context.Context, set domain.CaseSet) error + List(ctx context.Context, offset, limit int) ([]domain.CaseSet, int64, error) + Detail(ctx context.Context, id int64) (domain.CaseSet, error) + GetByIds(ctx context.Context, ids []int64) ([]domain.CaseSet, error) + + ListByBiz(ctx context.Context, offset, limit int, biz string) ([]domain.CaseSet, error) + ListDefault(ctx context.Context, offset, limit int) ([]domain.CaseSet, error) + GetByBiz(ctx context.Context, biz string, bizId int64) (domain.CaseSet, error) + GetCandidates(ctx context.Context, id int64, offset int, limit int) ([]domain.Case, int64, error) +} + +type casSetSvc struct { + repo repository.CaseSetRepository + caRepo repository.CaseRepo +} + +func NewCaseSetService(repo repository.CaseSetRepository, caRepo repository.CaseRepo) CaseSetService { + return &casSetSvc{ + repo: repo, + caRepo: caRepo, + } +} + +func (c *casSetSvc) GetCandidates(ctx context.Context, id int64, offset int, limit int) ([]domain.Case, int64, error) { + cs, err := c.repo.GetByID(ctx, id) + if err != nil { + return nil, 0, err + } + cids := slice.Map(cs.Cases, func(idx int, src domain.Case) int64 { + return src.Id + }) + return c.caRepo.Exclude(ctx, cids, offset, limit) +} + +func (c *casSetSvc) ListDefault(ctx context.Context, offset, limit int) ([]domain.CaseSet, error) { + return c.repo.ListByBiz(ctx, offset, limit, domain.DefaultBiz) +} + +func (c *casSetSvc) ListByBiz(ctx context.Context, offset, limit int, biz string) ([]domain.CaseSet, error) { + return c.repo.ListByBiz(ctx, offset, limit, biz) +} + +func (c *casSetSvc) GetByBiz(ctx context.Context, biz string, bizId int64) (domain.CaseSet, error) { + return c.repo.GetByBiz(ctx, biz, bizId) +} + +func (c *casSetSvc) Save(ctx context.Context, set domain.CaseSet) (int64, error) { + var id = set.ID + var err error + if set.ID > 0 { + err = c.repo.UpdateNonZero(ctx, set) + } else { + id, err = c.repo.CreateCaseSet(ctx, set) + } + return id, err +} + +func (c *casSetSvc) UpdateCases(ctx context.Context, set domain.CaseSet) error { + return c.repo.UpdateCases(ctx, set) +} + +func (c *casSetSvc) List(ctx context.Context, offset, limit int) ([]domain.CaseSet, int64, error) { + var eg errgroup.Group + var sets []domain.CaseSet + var total int64 + eg.Go(func() error { + var eerr error + sets, eerr = c.repo.List(ctx, offset, limit) + return eerr + }) + eg.Go(func() error { + var eerr error + total, eerr = c.repo.Total(ctx) + return eerr + }) + + if err := eg.Wait(); err != nil { + return nil, 0, err + } + return sets, total, nil +} + +func (c *casSetSvc) Detail(ctx context.Context, id int64) (domain.CaseSet, error) { + return c.repo.GetByID(ctx, id) +} + +func (c *casSetSvc) GetByIds(ctx context.Context, ids []int64) ([]domain.CaseSet, error) { + return c.repo.GetByIDs(ctx, ids) +} diff --git a/internal/cases/internal/service/examine.go b/internal/cases/internal/service/examine.go new file mode 100644 index 00000000..f504e5a8 --- /dev/null +++ b/internal/cases/internal/service/examine.go @@ -0,0 +1,125 @@ +// 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" + "strings" + + "github.com/ecodeclub/ekit/slice" + "github.com/ecodeclub/webook/internal/ai" + "github.com/ecodeclub/webook/internal/cases/internal/domain" + "github.com/ecodeclub/webook/internal/cases/internal/repository" + "github.com/lithammer/shortuuid/v4" +) + +var ErrInsufficientCredit = ai.ErrInsufficientCredit + +// ExamineService 测试服务 +// +//go:generate mockgen -source=./examine.go -destination=../../mocks/examine.mock.go -package=quemocks -typed=true ExamineService +type ExamineService interface { + // Examine 测试服务 + // input 是用户输入的内容 + Examine(ctx context.Context, uid, cid int64, input string) (domain.ExamineCaseResult, error) + QuestionResult(ctx context.Context, uid, cid int64) (domain.CaseResult, error) + GetResults(ctx context.Context, uid int64, ids []int64) (map[int64]domain.ExamineCaseResult, error) +} + +var _ ExamineService = &LLMExamineService{} + +// LLMExamineService 使用 LLM 进行评价的测试服务 +type LLMExamineService struct { + caseRepo repository.CaseRepo + repo repository.ExamineRepository + aiSvc ai.LLMService +} + +func (svc *LLMExamineService) GetResults(ctx context.Context, uid int64, ids []int64) (map[int64]domain.ExamineCaseResult, error) { + results, err := svc.repo.GetResultsByIds(ctx, uid, ids) + return slice.ToMap[domain.ExamineCaseResult, int64](results, func(ele domain.ExamineCaseResult) int64 { + return ele.Cid + }), err +} + +func (svc *LLMExamineService) QuestionResult(ctx context.Context, uid, qid int64) (domain.CaseResult, error) { + return svc.repo.GetResultByUidAndQid(ctx, uid, qid) +} + +func (svc *LLMExamineService) Examine(ctx context.Context, + uid int64, + cid int64, input string) (domain.ExamineCaseResult, error) { + const biz = "case_examine" + // 实际上我们只需要 title,但是懒得写一个新的接口了 + ca, err := svc.caseRepo.GetPubByID(ctx, cid) + if err != nil { + return domain.ExamineCaseResult{}, err + } + tid := shortuuid.New() + aiReq := ai.LLMRequest{ + Uid: uid, + Tid: tid, + Biz: biz, + Input: []string{ca.Title, input}, + } + aiResp, err := svc.aiSvc.Invoke(ctx, aiReq) + if err != nil { + return domain.ExamineCaseResult{}, err + } + // 解析结果 + parsedRes := svc.parseExamineResult(aiResp.Answer) + result := domain.ExamineCaseResult{ + Result: parsedRes, + RawResult: aiResp.Answer, + Tokens: aiResp.Tokens, + Amount: aiResp.Amount, + Tid: tid, + } + // 开始记录结果 + err = svc.repo.SaveResult(ctx, uid, cid, result) + return result, err +} + +func (svc *LLMExamineService) parseExamineResult(answer string) domain.CaseResult { + answer = strings.TrimSpace(answer) + // 获取第一行 + segs := strings.SplitN(answer, "\n", 2) + if len(segs) < 1 { + return domain.ResultFailed + } + result := segs[0] + switch { + case strings.Contains(result, "15K"): + return domain.ResultBasic + case strings.Contains(result, "25K"): + return domain.ResultIntermediate + case strings.Contains(result, "35K"): + return domain.ResultAdvanced + default: + return domain.ResultFailed + } +} + +func NewLLMExamineService( + caseRepo repository.CaseRepo, + repo repository.ExamineRepository, + aiSvc ai.LLMService, +) ExamineService { + return &LLMExamineService{ + caseRepo: caseRepo, + repo: repo, + aiSvc: aiSvc, + } +} diff --git a/internal/cases/internal/web/admin_case_set_handler.go b/internal/cases/internal/web/admin_case_set_handler.go new file mode 100644 index 00000000..51613d78 --- /dev/null +++ b/internal/cases/internal/web/admin_case_set_handler.go @@ -0,0 +1,130 @@ +package web + +import ( + "github.com/ecodeclub/ekit/slice" + "github.com/ecodeclub/ginx" + "github.com/ecodeclub/ginx/session" + "github.com/ecodeclub/webook/internal/cases/internal/domain" + "github.com/ecodeclub/webook/internal/cases/internal/service" + "github.com/gin-gonic/gin" +) + +type AdminCaseSetHandler struct { + svc service.CaseSetService +} + +func NewAdminCaseSetHandler(svc service.CaseSetService) *AdminCaseSetHandler { + return &AdminCaseSetHandler{svc: svc} +} + +func (a *AdminCaseSetHandler) PublicRoutes(server *gin.Engine) { +} + +func (a *AdminCaseSetHandler) PrivateRoutes(server *gin.Engine) { + g := server.Group("/case-sets") + g.POST("/save", ginx.BS[CaseSet](a.SaveCaseSet)) + g.POST("/cases/save", ginx.B[UpdateCases](a.UpdateCases)) + g.POST("/list", ginx.B[Page](a.ListCaseSets)) + g.POST("/detail", ginx.B[CaseSetID](a.RetrieveCaseSetDetail)) + g.POST("/candidate", ginx.B[CandidateReq](a.Candidate)) + +} + +func (a *AdminCaseSetHandler) Candidate(ctx *ginx.Context, req CandidateReq) (ginx.Result, error) { + data, cnt, err := a.svc.GetCandidates(ctx, req.CSID, req.Offset, req.Limit) + if err != nil { + return systemErrorResult, err + } + castList := toCaseList(data, cnt) + return ginx.Result{ + Data: castList, + }, nil +} + +func (a *AdminCaseSetHandler) SaveCaseSet(ctx *ginx.Context, + req CaseSet, + sess session.Session) (ginx.Result, error) { + uid := sess.Claims().Uid + id, err := a.svc.Save(ctx, domain.CaseSet{ + ID: req.Id, + Uid: uid, + Title: req.Title, + Description: req.Description, + Biz: req.Biz, + BizId: req.BizId, + }) + if err != nil { + return systemErrorResult, err + } + return ginx.Result{ + Data: id, + }, nil +} + +func (a *AdminCaseSetHandler) UpdateCases(ctx *ginx.Context, req UpdateCases) (ginx.Result, error) { + cs := slice.Map(req.CIDs, func(idx int, src int64) domain.Case { + return domain.Case{ + Id: src, + } + }) + err := a.svc.UpdateCases(ctx.Request.Context(), domain.CaseSet{ + ID: req.CSID, + Cases: cs, + }) + if err != nil { + return systemErrorResult, err + } + return ginx.Result{}, nil + +} + +func (a *AdminCaseSetHandler) ListCaseSets(ctx *ginx.Context, req Page) (ginx.Result, error) { + list, count, err := a.svc.List(ctx, req.Offset, req.Limit) + if err != nil { + return systemErrorResult, err + } + return ginx.Result{ + Data: CaseSetList{ + Total: count, + CaseSets: slice.Map(list, func(idx int, src domain.CaseSet) CaseSet { + return newCaseSet(src) + }), + }, + }, nil +} + +func (a *AdminCaseSetHandler) RetrieveCaseSetDetail(ctx *ginx.Context, req CaseSetID) (ginx.Result, error) { + detail, err := a.svc.Detail(ctx, req.ID) + if err != nil { + return systemErrorResult, err + } + return ginx.Result{ + Data: newCaseSet(detail), + }, nil + +} + +func toCaseList(data []domain.Case, cnt int64) CasesList { + return CasesList{ + Total: cnt, + Cases: slice.Map(data, func(idx int, src domain.Case) Case { + return Case{ + Id: src.Id, + Title: src.Title, + Content: src.Content, + Labels: src.Labels, + GiteeRepo: src.GiteeRepo, + GithubRepo: src.GithubRepo, + Keywords: src.Keywords, + Shorthand: src.Shorthand, + Introduction: src.Introduction, + Highlight: src.Highlight, + Guidance: src.Guidance, + Biz: src.Biz, + BizId: src.BizId, + Status: src.Status.ToUint8(), + Utime: src.Utime.UnixMilli(), + } + }), + } +} diff --git a/internal/cases/internal/web/case_set_handler.go b/internal/cases/internal/web/case_set_handler.go new file mode 100644 index 00000000..aae0acd0 --- /dev/null +++ b/internal/cases/internal/web/case_set_handler.go @@ -0,0 +1,148 @@ +package web + +import ( + "context" + + "github.com/ecodeclub/ekit/slice" + "github.com/ecodeclub/ginx" + "github.com/ecodeclub/ginx/session" + "github.com/ecodeclub/webook/internal/cases/internal/domain" + "github.com/ecodeclub/webook/internal/cases/internal/service" + "github.com/ecodeclub/webook/internal/interactive" + "github.com/gin-gonic/gin" + "github.com/gotomicro/ego/core/elog" + "golang.org/x/sync/errgroup" +) + +type CaseSetHandler struct { + svc service.CaseSetService + examineSvc service.ExamineService + logger *elog.Component + intrSvc interactive.Service +} + +func NewCaseSetHandler( + svc service.CaseSetService, + examineSvc service.ExamineService, + intrSvc interactive.Service) *CaseSetHandler { + return &CaseSetHandler{ + svc: svc, + intrSvc: intrSvc, + examineSvc: examineSvc, + logger: elog.DefaultLogger, + } +} + +func (h *CaseSetHandler) PublicRoutes(server *gin.Engine) {} + +func (h *CaseSetHandler) PrivateRoutes(server *gin.Engine) { + g := server.Group("/case-sets") + g.POST("/list", ginx.B[Page](h.ListCaseSets)) + g.POST("/detail", ginx.BS(h.RetrieveCaseSetDetail)) + g.POST("/detail/biz", ginx.BS(h.GetDetailByBiz)) +} + +// ListCaseSets 展示个人案例集 +func (h *CaseSetHandler) ListCaseSets(ctx *ginx.Context, req Page) (ginx.Result, error) { + data, err := h.svc.ListDefault(ctx, req.Offset, req.Limit) + if err != nil { + return systemErrorResult, err + } + // 查询点赞收藏记录 + intrs := map[int64]interactive.Interactive{} + if len(data) > 0 { + ids := slice.Map(data, func(idx int, src domain.CaseSet) int64 { + return src.ID + }) + var err1 error + intrs, err1 = h.intrSvc.GetByIds(ctx, "caseSet", ids) + // 这个数据查询不到也不需要担心 + if err1 != nil { + h.logger.Error("查询案例集的点赞数据失败", + elog.Any("ids", ids), + elog.FieldErr(err)) + } + } + return ginx.Result{ + Data: CaseSetList{ + CaseSets: slice.Map(data, func(idx int, src domain.CaseSet) CaseSet { + qs := newCaseSet(src) + qs.Interactive = newInteractive(intrs[src.ID]) + return qs + }), + }, + }, nil +} + +// RetrieveCaseSetDetail 案例集详情 +func (h *CaseSetHandler) RetrieveCaseSetDetail( + ctx *ginx.Context, + req CaseSetID, sess session.Session) (ginx.Result, error) { + + data, err := h.svc.Detail(ctx.Request.Context(), req.ID) + if err != nil { + return systemErrorResult, err + } + return h.getDetail(ctx, sess.Claims().Uid, data) +} + +func (h *CaseSetHandler) GetDetailByBiz( + ctx *ginx.Context, + req BizReq, sess session.Session) (ginx.Result, error) { + data, err := h.svc.GetByBiz(ctx, req.Biz, req.BizId) + if err != nil { + return systemErrorResult, err + } + return h.getDetail(ctx, sess.Claims().Uid, data) +} + +func (h *CaseSetHandler) getDetail( + ctx context.Context, + uid int64, + cs domain.CaseSet) (ginx.Result, error) { + var ( + eg errgroup.Group + intr interactive.Interactive + resultMap map[int64]domain.ExamineCaseResult + ) + + eg.Go(func() error { + var err error + intr, err = h.intrSvc.Get(ctx, "caseSet", cs.ID, uid) + return err + }) + + eg.Go(func() error { + var err error + resultMap, err = h.examineSvc.GetResults(ctx, uid, cs.Cids()) + return err + }) + + err := eg.Wait() + if err != nil { + return systemErrorResult, err + } + + return ginx.Result{ + Data: h.toCaseSetVO(cs, intr, resultMap), + }, nil +} + +func (h *CaseSetHandler) toCaseSetVO( + set domain.CaseSet, + intr interactive.Interactive, + results map[int64]domain.ExamineCaseResult) CaseSet { + cs := newCaseSet(set) + cs.Cases = h.toCaseVO(set.Cases, results) + cs.Interactive = newInteractive(intr) + return cs +} + +func (h *CaseSetHandler) toCaseVO(cases []domain.Case, results map[int64]domain.ExamineCaseResult) []Case { + return slice.Map(cases, func(idx int, src domain.Case) Case { + ca := newCase(src) + res := results[ca.Id] + ca.ExamineResult = res.Result.ToUint8() + return ca + }) +} diff --git a/internal/cases/internal/web/exam_handler.go b/internal/cases/internal/web/exam_handler.go new file mode 100644 index 00000000..5b73dade --- /dev/null +++ b/internal/cases/internal/web/exam_handler.go @@ -0,0 +1,44 @@ +package web + +import ( + "errors" + + "github.com/ecodeclub/ginx" + "github.com/ecodeclub/ginx/session" + "github.com/ecodeclub/webook/internal/cases/internal/errs" + "github.com/ecodeclub/webook/internal/cases/internal/service" + "github.com/gin-gonic/gin" +) + +type ExamineHandler struct { + svc service.ExamineService +} + +func NewExamineHandler(svc service.ExamineService) *ExamineHandler { + return &ExamineHandler{ + svc: svc, + } +} + +func (h *ExamineHandler) MemberRoutes(server *gin.Engine) { + g := server.Group("/cases/examine") + g.POST("", ginx.BS(h.Examine)) +} + +func (h *ExamineHandler) Examine(ctx *ginx.Context, req ExamineReq, sess session.Session) (ginx.Result, error) { + res, err := h.svc.Examine(ctx, sess.Claims().Uid, req.Cid, req.Input) + switch { + case errors.Is(err, service.ErrInsufficientCredit): + return ginx.Result{ + Code: errs.InsufficientCredits.Code, + Msg: errs.InsufficientCredits.Msg, + }, nil + + case err == nil: + return ginx.Result{ + Data: newExamineResult(res), + }, nil + default: + return systemErrorResult, err + } +} diff --git a/internal/cases/internal/web/handler.go b/internal/cases/internal/web/handler.go index 9f678620..58b0266b 100644 --- a/internal/cases/internal/web/handler.go +++ b/internal/cases/internal/web/handler.go @@ -187,11 +187,14 @@ func newCase(ca domain.Case) Case { Introduction: ca.Introduction, Content: ca.Content, Labels: ca.Labels, - CodeRepo: ca.CodeRepo, + GiteeRepo: ca.GiteeRepo, + GithubRepo: ca.GithubRepo, Keywords: ca.Keywords, Shorthand: ca.Shorthand, Highlight: ca.Highlight, Guidance: ca.Guidance, + Biz: ca.Biz, + BizId: ca.BizId, Status: ca.Status.ToUint8(), Utime: ca.Utime.UnixMilli(), } diff --git a/internal/cases/internal/web/vo.go b/internal/cases/internal/web/vo.go index 5a2b0584..82511c92 100644 --- a/internal/cases/internal/web/vo.go +++ b/internal/cases/internal/web/vo.go @@ -1,6 +1,7 @@ package web import ( + "github.com/ecodeclub/ekit/slice" "github.com/ecodeclub/webook/internal/cases/internal/domain" "github.com/ecodeclub/webook/internal/interactive" ) @@ -23,8 +24,9 @@ type Case struct { Labels []string `json:"labels,omitempty"` // 面试案例内容 - Content string `json:"content,omitempty"` - CodeRepo string `json:"codeRepo,omitempty"` + Content string `json:"content,omitempty"` + GithubRepo string `json:"githubRepo,omitempty"` + GiteeRepo string `json:"giteeRepo,omitempty"` // 关键字,辅助记忆,提取重点 Keywords string `json:"keywords,omitempty"` // 速记,口诀 @@ -32,11 +34,14 @@ type Case struct { // 亮点 Highlight string `json:"highlight,omitempty"` // 引导点 - Guidance string `json:"guidance,omitempty"` - Status uint8 `json:"status,omitempty"` - Utime int64 `json:"utime,omitempty"` - + Guidance string `json:"guidance,omitempty"` + Status uint8 `json:"status,omitempty"` + Utime int64 `json:"utime,omitempty"` + Biz string `json:"biz,omitempty"` + BizId int64 `json:"biz_id,omitempty"` Interactive Interactive `json:"interactive,omitempty"` + + ExamineResult uint8 `json:"examineResult"` } type CaseId struct { @@ -52,11 +57,14 @@ func (c Case) toDomain() domain.Case { Title: c.Title, Labels: c.Labels, Content: c.Content, - CodeRepo: c.CodeRepo, + GithubRepo: c.GithubRepo, + GiteeRepo: c.GiteeRepo, Keywords: c.Keywords, Shorthand: c.Shorthand, Introduction: c.Introduction, Highlight: c.Highlight, + Biz: c.Biz, + BizId: c.BizId, Guidance: c.Guidance, } } @@ -78,3 +86,80 @@ func newInteractive(intr interactive.Interactive) Interactive { Collected: intr.Collected, } } + +type CaseSet struct { + Id int64 `json:"id,omitempty"` + Title string `json:"title,omitempty"` + Description string `json:"description,omitempty"` + Cases []Case `json:"cases,omitempty"` + Biz string `json:"biz"` + BizId int64 `json:"bizId"` + Utime int64 `json:"utime,omitempty"` + Interactive Interactive `json:"interactive,omitempty"` +} + +type UpdateCases struct { + CSID int64 `json:"csid"` + CIDs []int64 `json:"cids,omitempty"` +} + +type CaseSetList struct { + Total int64 `json:"total,omitempty"` + CaseSets []CaseSet `json:"caseSets,omitempty"` +} + +type CaseSetID struct { + ID int64 `json:"id"` +} + +type CandidateReq struct { + CSID int64 `json:"csid"` + Offset int `json:"offset,omitempty"` + Limit int `json:"limit,omitempty"` +} + +func newCaseSet(src domain.CaseSet) CaseSet { + return CaseSet{ + Id: src.ID, + Title: src.Title, + + Description: src.Description, + Cases: slice.Map(src.Cases, func(idx int, src domain.Case) Case { + return newCase(src) + }), + Biz: src.Biz, + BizId: src.BizId, + Utime: src.Utime, + } +} + +type ExamineResult struct { + Cid int64 + Result uint8 `json:"result"` + // 原始回答,源自 AI + RawResult string `json:"rawResult"` + + // 使用的 token 数量 + Tokens int64 `json:"tokens"` + // 花费的金额 + Amount int64 `json:"amount"` +} + +type ExamineReq struct { + Cid int64 `json:"cid"` + Input string `json:"input"` +} + +func newExamineResult(r domain.ExamineCaseResult) ExamineResult { + return ExamineResult{ + Cid: r.Cid, + Result: r.Result.ToUint8(), + RawResult: r.RawResult, + Amount: r.Amount, + } +} + +type BizReq struct { + Biz string `json:"biz"` + BizId int64 `json:"bizId"` +} diff --git a/internal/cases/module.go b/internal/cases/module.go index f9ca590c..dd20ec38 100644 --- a/internal/cases/module.go +++ b/internal/cases/module.go @@ -15,6 +15,9 @@ package cases type Module struct { - Svc Service - Hdl *Handler + Svc Service + Hdl *Handler + AdminSetHandler *AdminCaseSetHandler + ExamineHdl *ExamineHandler + CsHdl *CaseSetHandler } diff --git a/internal/cases/wire.go b/internal/cases/wire.go index 02cb37c6..ecd76cc1 100644 --- a/internal/cases/wire.go +++ b/internal/cases/wire.go @@ -5,6 +5,8 @@ package cases import ( "sync" + "github.com/ecodeclub/webook/internal/ai" + "github.com/ecodeclub/webook/internal/interactive" "github.com/ecodeclub/mq-api" @@ -23,14 +25,25 @@ import ( func InitModule(db *egorm.Component, intrModule *interactive.Module, + aiModule *ai.Module, q mq.MQ) (*Module, error) { wire.Build(InitCaseDAO, + dao.NewCaseSetDAO, + dao.NewGORMExamineDAO, repository.NewCaseRepo, + repository.NewCaseSetRepo, + repository.NewCachedExamineRepository, event.NewSyncEventProducer, event.NewInteractiveEventProducer, + service.NewCaseSetService, service.NewService, + service.NewLLMExamineService, web.NewHandler, + web.NewAdminCaseSetHandler, + web.NewExamineHandler, + web.NewCaseSetHandler, wire.FieldsOf(new(*interactive.Module), "Svc"), + wire.FieldsOf(new(*ai.Module), "Svc"), wire.Struct(new(Module), "*"), ) return new(Module), nil @@ -55,3 +68,6 @@ func InitCaseDAO(db *egorm.Component) dao.CaseDAO { type Handler = web.Handler type Service = service.Service type Case = domain.Case +type AdminCaseSetHandler = web.AdminCaseSetHandler +type ExamineHandler = web.ExamineHandler +type CaseSetHandler = web.CaseSetHandler diff --git a/internal/cases/wire_gen.go b/internal/cases/wire_gen.go index dc96f5d0..b2751e77 100644 --- a/internal/cases/wire_gen.go +++ b/internal/cases/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,6 +10,7 @@ import ( "sync" "github.com/ecodeclub/mq-api" + "github.com/ecodeclub/webook/internal/ai" "github.com/ecodeclub/webook/internal/cases/internal/domain" "github.com/ecodeclub/webook/internal/cases/internal/event" "github.com/ecodeclub/webook/internal/cases/internal/repository" @@ -23,7 +24,7 @@ import ( // Injectors from wire.go: -func InitModule(db *gorm.DB, intrModule *interactive.Module, q mq.MQ) (*Module, error) { +func InitModule(db *gorm.DB, intrModule *interactive.Module, aiModule *ai.Module, q mq.MQ) (*Module, error) { caseDAO := InitCaseDAO(db) caseRepo := repository.NewCaseRepo(caseDAO) interactiveEventProducer, err := event.NewInteractiveEventProducer(q) @@ -37,9 +38,22 @@ func InitModule(db *gorm.DB, intrModule *interactive.Module, q mq.MQ) (*Module, serviceService := service.NewService(caseRepo, interactiveEventProducer, syncEventProducer) service2 := intrModule.Svc handler := web.NewHandler(serviceService, service2) + caseSetDAO := dao.NewCaseSetDAO(db) + caseSetRepository := repository.NewCaseSetRepo(caseSetDAO) + caseSetService := service.NewCaseSetService(caseSetRepository, caseRepo) + adminCaseSetHandler := web.NewAdminCaseSetHandler(caseSetService) + examineDAO := dao.NewGORMExamineDAO(db) + examineRepository := repository.NewCachedExamineRepository(examineDAO) + llmService := aiModule.Svc + examineService := service.NewLLMExamineService(caseRepo, examineRepository, llmService) + examineHandler := web.NewExamineHandler(examineService) + caseSetHandler := web.NewCaseSetHandler(caseSetService, examineService, service2) module := &Module{ - Svc: serviceService, - Hdl: handler, + Svc: serviceService, + Hdl: handler, + AdminSetHandler: adminCaseSetHandler, + ExamineHdl: examineHandler, + CsHdl: caseSetHandler, } return module, nil } @@ -67,3 +81,9 @@ type Handler = web.Handler type Service = service.Service type Case = domain.Case + +type AdminCaseSetHandler = web.AdminCaseSetHandler + +type ExamineHandler = web.ExamineHandler + +type CaseSetHandler = web.CaseSetHandler diff --git a/internal/search/internal/domain/type.go b/internal/search/internal/domain/type.go index 04f6af62..1aec6040 100644 --- a/internal/search/internal/domain/type.go +++ b/internal/search/internal/domain/type.go @@ -8,11 +8,12 @@ import ( type Case struct { Id int64 // 作者 - Uid int64 - Labels []string - Title string - Content string - CodeRepo string + Uid int64 + Labels []string + Title string + Content string + GithubRepo string + GiteeRepo string // 关键字,辅助记忆,提取重点 Keywords string // 速记,口诀 diff --git a/internal/search/internal/integration/handler_test.go b/internal/search/internal/integration/handler_test.go index bb1447c5..f1f69ace 100644 --- a/internal/search/internal/integration/handler_test.go +++ b/internal/search/internal/integration/handler_test.go @@ -127,82 +127,88 @@ func (s *HandlerTestSuite) TestBizSearch() { wantAns: web.SearchResult{ Cases: []web.Case{ { - Id: 6, - Uid: 1, - Labels: []string{"label1"}, - Title: "test_title", - Content: "Elasticsearch内容", - CodeRepo: "Elasticsearch代码库", - Keywords: "Elasticsearch关键词", - Shorthand: "Elasticsearch速记", - Highlight: "Elasticsearch亮点", - Guidance: "Elasticsearch引导", - Status: 2, + Id: 6, + Uid: 1, + Labels: []string{"label1"}, + Title: "test_title", + Content: "Elasticsearch内容", + GithubRepo: "Elasticsearch github代码库", + GiteeRepo: "Elasticsearch gitee代码库", + Keywords: "Elasticsearch关键词", + Shorthand: "Elasticsearch速记", + Highlight: "Elasticsearch亮点", + Guidance: "Elasticsearch引导", + Status: 2, }, { - Id: 5, - Uid: 1, - Labels: []string{"test_label"}, - Title: "Elasticsearch标题", - Content: "Elasticsearch内容", - CodeRepo: "Elasticsearch代码库", - Keywords: "Elasticsearch关键词", - Shorthand: "Elasticsearch速记", - Highlight: "Elasticsearch亮点", - Guidance: "Elasticsearch引导", - Status: 2, + Id: 5, + Uid: 1, + Labels: []string{"test_label"}, + Title: "Elasticsearch标题", + Content: "Elasticsearch内容", + GithubRepo: "Elasticsearch github代码库", + GiteeRepo: "Elasticsearch gitee代码库", + Keywords: "Elasticsearch关键词", + Shorthand: "Elasticsearch速记", + Highlight: "Elasticsearch亮点", + Guidance: "Elasticsearch引导", + Status: 2, }, { - Id: 2, - Uid: 1, - Labels: []string{"label1"}, - Title: "Elasticsearch标题", - Content: "Elasticsearch内容", - CodeRepo: "Elasticsearch代码库", - Keywords: "test_keywords", - Shorthand: "Elasticsearch速记", - Highlight: "Elasticsearch亮点", - Guidance: "Elasticsearch引导", - Status: 2, + Id: 2, + Uid: 1, + Labels: []string{"label1"}, + Title: "Elasticsearch标题", + Content: "Elasticsearch内容", + GithubRepo: "Elasticsearch github代码库", + GiteeRepo: "Elasticsearch gitee代码库", + Keywords: "test_keywords", + Shorthand: "Elasticsearch速记", + Highlight: "Elasticsearch亮点", + Guidance: "Elasticsearch引导", + Status: 2, }, { - Id: 3, - Uid: 1, - Labels: []string{"label1", "label2"}, - Title: "Elasticsearch标题", - Content: "Elasticsearch内容", - CodeRepo: "Elasticsearch代码库", - Keywords: "Elasticsearch关键词", - Shorthand: "test_shorthands", - Highlight: "Elasticsearch亮点", - Guidance: "Elasticsearch引导", - Status: 2, + Id: 3, + Uid: 1, + Labels: []string{"label1", "label2"}, + Title: "Elasticsearch标题", + Content: "Elasticsearch内容", + GithubRepo: "Elasticsearch github代码库", + GiteeRepo: "Elasticsearch gitee代码库", + Keywords: "Elasticsearch关键词", + Shorthand: "test_shorthands", + Highlight: "Elasticsearch亮点", + Guidance: "Elasticsearch引导", + Status: 2, }, { - Id: 1, - Uid: 1, - Labels: []string{"label1", "label2"}, - Title: "Elasticsearch标题", - Content: "test_content", - CodeRepo: "Elasticsearch代码库", - Keywords: "Elasticsearch关键词", - Shorthand: "Elasticsearch速记", - Highlight: "Elasticsearch亮点", - Guidance: "Elasticsearch引导", - Status: 2, + Id: 1, + Uid: 1, + Labels: []string{"label1", "label2"}, + Title: "Elasticsearch标题", + Content: "test_content", + GithubRepo: "Elasticsearch github代码库", + GiteeRepo: "Elasticsearch gitee代码库", + Keywords: "Elasticsearch关键词", + Shorthand: "Elasticsearch速记", + Highlight: "Elasticsearch亮点", + Guidance: "Elasticsearch引导", + Status: 2, }, { - Id: 4, - Uid: 1, - Labels: []string{"label1", "label2"}, - Title: "Elasticsearch标题", - Content: "Elasticsearch内容", - CodeRepo: "Elasticsearch代码库", - Keywords: "Elasticsearch关键词", - Shorthand: "Elasticsearch速记", - Highlight: "Elasticsearch亮点", - Guidance: "test_guidance", - Status: 2, + Id: 4, + Uid: 1, + Labels: []string{"label1", "label2"}, + Title: "Elasticsearch标题", + Content: "Elasticsearch内容", + GithubRepo: "Elasticsearch github代码库", + GiteeRepo: "Elasticsearch gitee代码库", + Keywords: "Elasticsearch关键词", + Shorthand: "Elasticsearch速记", + Highlight: "Elasticsearch亮点", + Guidance: "test_guidance", + Status: 2, }, }, }, @@ -704,17 +710,18 @@ func (s *HandlerTestSuite) TestSearch() { want := web.SearchResult{ Cases: []web.Case{ { - Id: 2, - Uid: 1, - Labels: []string{"label1"}, - Title: "test_title", - Content: "Elasticsearch内容", - CodeRepo: "Elasticsearch代码库", - Keywords: "test_keywords", - Shorthand: "Elasticsearch速记", - Highlight: "Elasticsearch亮点", - Guidance: "Elasticsearch引导", - Status: 2, + Id: 2, + Uid: 1, + Labels: []string{"label1"}, + Title: "test_title", + Content: "Elasticsearch内容", + GithubRepo: "Elasticsearch github代码库", + GiteeRepo: "Elasticsearch gitee代码库", + Keywords: "test_keywords", + Shorthand: "Elasticsearch速记", + Highlight: "Elasticsearch亮点", + Guidance: "Elasticsearch引导", + Status: 2, }, }, Questions: []web.Question{ @@ -784,19 +791,20 @@ func (s *HandlerTestSuite) TestSync() { err := json.Unmarshal(res.Source, &ans) require.NoError(t, err) assert.Equal(t, dao.Case{ - Id: 1, - Uid: 1001, - Labels: []string{"label1", "label2"}, - Title: "Test Case", - Content: "Test Content", - CodeRepo: "github.com/test", - Keywords: "test keywords", - Shorthand: "test shorthand", - Highlight: "test highlight", - Guidance: "test guidance", - Status: 1, - Ctime: 1619430000, - Utime: 1619430000, + Id: 1, + Uid: 1001, + Labels: []string{"label1", "label2"}, + Title: "Test Case", + Content: "Test Content", + GithubRepo: "github.com/test", + GiteeRepo: "gitee.com/test", + Keywords: "test keywords", + Shorthand: "test shorthand", + Highlight: "test highlight", + Guidance: "test guidance", + Status: 1, + Ctime: 1619430000, + Utime: 1619430000, }, ans) }, }, @@ -953,30 +961,32 @@ func (s *HandlerTestSuite) TestSearchLimit() { wantAns: web.SearchResult{ Cases: []web.Case{ { - Id: 6, - Uid: 1, - Labels: []string{"label1"}, - Title: "test_title", - Content: "Elasticsearch内容", - CodeRepo: "Elasticsearch代码库", - Keywords: "Elasticsearch关键词", - Shorthand: "Elasticsearch速记", - Highlight: "Elasticsearch亮点", - Guidance: "Elasticsearch引导", - Status: 2, + Id: 6, + Uid: 1, + Labels: []string{"label1"}, + Title: "test_title", + Content: "Elasticsearch内容", + GithubRepo: "Elasticsearch github代码库", + GiteeRepo: "Elasticsearch gitee代码库", + Keywords: "Elasticsearch关键词", + Shorthand: "Elasticsearch速记", + Highlight: "Elasticsearch亮点", + Guidance: "Elasticsearch引导", + Status: 2, }, { - Id: 5, - Uid: 1, - Labels: []string{"test_label"}, - Title: "Elasticsearch标题", - Content: "Elasticsearch内容", - CodeRepo: "Elasticsearch代码库", - Keywords: "Elasticsearch关键词", - Shorthand: "Elasticsearch速记", - Highlight: "Elasticsearch亮点", - Guidance: "Elasticsearch引导", - Status: 2, + Id: 5, + Uid: 1, + Labels: []string{"test_label"}, + Title: "Elasticsearch标题", + Content: "Elasticsearch内容", + GithubRepo: "Elasticsearch github代码库", + GiteeRepo: "Elasticsearch gitee代码库", + Keywords: "Elasticsearch关键词", + Shorthand: "Elasticsearch速记", + Highlight: "Elasticsearch亮点", + Guidance: "Elasticsearch引导", + Status: 2, }, }, }, @@ -1148,19 +1158,20 @@ func getCase(t *testing.T) event.SyncEvent { BizID: 1, } val := dao.Case{ - Id: 1, - Uid: 1001, - Labels: []string{"label1", "label2"}, - Title: "Test Case", - Content: "Test Content", - CodeRepo: "github.com/test", - Keywords: "test keywords", - Shorthand: "test shorthand", - Highlight: "test highlight", - Guidance: "test guidance", - Status: 1, - Ctime: 1619430000, - Utime: 1619430000, + Id: 1, + Uid: 1001, + Labels: []string{"label1", "label2"}, + Title: "Test Case", + Content: "Test Content", + GithubRepo: "github.com/test", + GiteeRepo: "gitee.com/test", + Keywords: "test keywords", + Shorthand: "test shorthand", + Highlight: "test highlight", + Guidance: "test guidance", + Status: 1, + Ctime: 1619430000, + Utime: 1619430000, } caseByte, err := json.Marshal(val) require.NoError(t, err) @@ -1268,109 +1279,116 @@ func getSkill(t *testing.T) event.SyncEvent { func (s *HandlerTestSuite) initCases() { testcases := []dao.Case{ { - Id: 1, - Uid: 1, - Labels: []string{"label1", "label2"}, - Title: "Elasticsearch标题", - Content: "test_content", - CodeRepo: "Elasticsearch代码库", - Keywords: "Elasticsearch关键词", - Shorthand: "Elasticsearch速记", - Highlight: "Elasticsearch亮点", - Guidance: "Elasticsearch引导", - Status: 2, - Ctime: 1619708855, - Utime: 1619708855, + Id: 1, + Uid: 1, + Labels: []string{"label1", "label2"}, + Title: "Elasticsearch标题", + Content: "test_content", + GithubRepo: "Elasticsearch github代码库", + GiteeRepo: "Elasticsearch gitee代码库", + Keywords: "Elasticsearch关键词", + Shorthand: "Elasticsearch速记", + Highlight: "Elasticsearch亮点", + Guidance: "Elasticsearch引导", + Status: 2, + Ctime: 1619708855, + Utime: 1619708855, }, { - Id: 2, - Uid: 1, - Labels: []string{"label1"}, - Title: "Elasticsearch标题", - Content: "Elasticsearch内容", - CodeRepo: "Elasticsearch代码库", - Keywords: "test_keywords", - Shorthand: "Elasticsearch速记", - Highlight: "Elasticsearch亮点", - Guidance: "Elasticsearch引导", - Status: 2, - Ctime: 1619708855, - Utime: 1619708855, + Id: 2, + Uid: 1, + Labels: []string{"label1"}, + Title: "Elasticsearch标题", + Content: "Elasticsearch内容", + GithubRepo: "Elasticsearch github代码库", + GiteeRepo: "Elasticsearch gitee代码库", + Keywords: "test_keywords", + Shorthand: "Elasticsearch速记", + Highlight: "Elasticsearch亮点", + Guidance: "Elasticsearch引导", + Status: 2, + Ctime: 1619708855, + Utime: 1619708855, }, { - Id: 3, - Uid: 1, - Labels: []string{"label1", "label2"}, - Title: "Elasticsearch标题", - Content: "Elasticsearch内容", - CodeRepo: "Elasticsearch代码库", - Keywords: "Elasticsearch关键词", - Shorthand: "test_shorthands", - Highlight: "Elasticsearch亮点", - Guidance: "Elasticsearch引导", - Status: 2, - Ctime: 1619708855, - Utime: 1619708855, + Id: 3, + Uid: 1, + Labels: []string{"label1", "label2"}, + Title: "Elasticsearch标题", + Content: "Elasticsearch内容", + GithubRepo: "Elasticsearch github代码库", + GiteeRepo: "Elasticsearch gitee代码库", + Keywords: "Elasticsearch关键词", + Shorthand: "test_shorthands", + Highlight: "Elasticsearch亮点", + Guidance: "Elasticsearch引导", + Status: 2, + Ctime: 1619708855, + Utime: 1619708855, }, { - Id: 4, - Uid: 1, - Labels: []string{"label1", "label2"}, - Title: "Elasticsearch标题", - Content: "Elasticsearch内容", - CodeRepo: "Elasticsearch代码库", - Keywords: "Elasticsearch关键词", - Shorthand: "Elasticsearch速记", - Highlight: "Elasticsearch亮点", - Guidance: "test_guidance", - Status: 2, - Ctime: 1619708855, - Utime: 1619708855, + Id: 4, + Uid: 1, + Labels: []string{"label1", "label2"}, + Title: "Elasticsearch标题", + Content: "Elasticsearch内容", + GithubRepo: "Elasticsearch github代码库", + GiteeRepo: "Elasticsearch gitee代码库", + Keywords: "Elasticsearch关键词", + Shorthand: "Elasticsearch速记", + Highlight: "Elasticsearch亮点", + Guidance: "test_guidance", + Status: 2, + Ctime: 1619708855, + Utime: 1619708855, }, { - Id: 5, - Uid: 1, - Labels: []string{"test_label"}, - Title: "Elasticsearch标题", - Content: "Elasticsearch内容", - CodeRepo: "Elasticsearch代码库", - Keywords: "Elasticsearch关键词", - Shorthand: "Elasticsearch速记", - Highlight: "Elasticsearch亮点", - Guidance: "Elasticsearch引导", - Status: 2, - Ctime: 1619708855, - Utime: 1619708855, + Id: 5, + Uid: 1, + Labels: []string{"test_label"}, + Title: "Elasticsearch标题", + Content: "Elasticsearch内容", + GithubRepo: "Elasticsearch github代码库", + GiteeRepo: "Elasticsearch gitee代码库", + Keywords: "Elasticsearch关键词", + Shorthand: "Elasticsearch速记", + Highlight: "Elasticsearch亮点", + Guidance: "Elasticsearch引导", + Status: 2, + Ctime: 1619708855, + Utime: 1619708855, }, { - Id: 6, - Uid: 1, - Labels: []string{"label1"}, - Title: "test_title", - Content: "Elasticsearch内容", - CodeRepo: "Elasticsearch代码库", - Keywords: "Elasticsearch关键词", - Shorthand: "Elasticsearch速记", - Highlight: "Elasticsearch亮点", - Guidance: "Elasticsearch引导", - Status: 2, - Ctime: 1619708855, - Utime: 1619708855, + Id: 6, + Uid: 1, + Labels: []string{"label1"}, + Title: "test_title", + Content: "Elasticsearch内容", + GithubRepo: "Elasticsearch github代码库", + GiteeRepo: "Elasticsearch gitee代码库", + Keywords: "Elasticsearch关键词", + Shorthand: "Elasticsearch速记", + Highlight: "Elasticsearch亮点", + Guidance: "Elasticsearch引导", + Status: 2, + Ctime: 1619708855, + Utime: 1619708855, }, { - Id: 7, - Uid: 1, - Labels: []string{"label1", "test_label"}, - Title: "test_title未发布", - Content: "Elasticsearch内容", - CodeRepo: "Elasticsearch代码库", - Keywords: "Elasticsearch关键词", - Shorthand: "Elasticsearch速记", - Highlight: "Elasticsearch亮点", - Guidance: "Elasticsearch引导", - Status: 1, - Ctime: 1619708855, - Utime: 1619708855, + Id: 7, + Uid: 1, + Labels: []string{"label1", "test_label"}, + Title: "test_title未发布", + Content: "Elasticsearch内容", + GithubRepo: "Elasticsearch github代码库", + GiteeRepo: "Elasticsearch gitee代码库", + Keywords: "Elasticsearch关键词", + Shorthand: "Elasticsearch速记", + Highlight: "Elasticsearch亮点", + Guidance: "Elasticsearch引导", + Status: 1, + Ctime: 1619708855, + Utime: 1619708855, }, } s.insertCase(testcases) @@ -1855,19 +1873,20 @@ func (s *HandlerTestSuite) insertSkills(sks []dao.Skill) { func (s *HandlerTestSuite) initSearchData() { cas := []dao.Case{ { - Id: 2, - Uid: 1, - Labels: []string{"label1"}, - Title: "test_title", - Content: "Elasticsearch内容", - CodeRepo: "Elasticsearch代码库", - Keywords: "test_keywords", - Shorthand: "Elasticsearch速记", - Highlight: "Elasticsearch亮点", - Guidance: "Elasticsearch引导", - Status: 2, - Ctime: 1619708855, - Utime: 1619708855, + Id: 2, + Uid: 1, + Labels: []string{"label1"}, + Title: "test_title", + Content: "Elasticsearch内容", + GithubRepo: "Elasticsearch github代码库", + GiteeRepo: "Elasticsearch gitee代码库", + Keywords: "test_keywords", + Shorthand: "Elasticsearch速记", + Highlight: "Elasticsearch亮点", + Guidance: "Elasticsearch引导", + Status: 2, + Ctime: 1619708855, + Utime: 1619708855, }, } s.insertCase(cas) diff --git a/internal/search/internal/repository/case.go b/internal/search/internal/repository/case.go index c676fbb8..14b1ab0b 100644 --- a/internal/search/internal/repository/case.go +++ b/internal/search/internal/repository/case.go @@ -46,18 +46,19 @@ func (c *caseRepository) SearchCase(ctx context.Context, offset, limit int, keyw func (*caseRepository) toDomain(p dao.Case) domain.Case { return domain.Case{ - Id: p.Id, - Uid: p.Uid, - Labels: p.Labels, - Title: p.Title, - Content: p.Content, - Keywords: p.Keywords, - CodeRepo: p.CodeRepo, - Shorthand: p.Shorthand, - Highlight: p.Highlight, - Guidance: p.Guidance, - Status: domain.CaseStatus(p.Status), - Ctime: time.UnixMilli(p.Ctime), - Utime: time.UnixMilli(p.Utime), + Id: p.Id, + Uid: p.Uid, + Labels: p.Labels, + Title: p.Title, + Content: p.Content, + Keywords: p.Keywords, + GithubRepo: p.GithubRepo, + GiteeRepo: p.GiteeRepo, + Shorthand: p.Shorthand, + Highlight: p.Highlight, + Guidance: p.Guidance, + Status: domain.CaseStatus(p.Status), + Ctime: time.UnixMilli(p.Ctime), + Utime: time.UnixMilli(p.Utime), } } diff --git a/internal/search/internal/repository/dao/case.go b/internal/search/internal/repository/dao/case.go index 942f3004..7dd9d797 100644 --- a/internal/search/internal/repository/dao/case.go +++ b/internal/search/internal/repository/dao/case.go @@ -26,19 +26,20 @@ const CaseIndexName = "case_index" // todo 添加分词器 type Case struct { - Id int64 `json:"id"` - Uid int64 `json:"uid"` - Labels []string `json:"labels"` - Title string `json:"title"` - Content string `json:"content"` - CodeRepo string `json:"code_repo"` - Keywords string `json:"keywords"` - Shorthand string `json:"shorthand"` - Highlight string `json:"highlight"` - Guidance string `json:"guidance"` - Status uint8 `json:"status"` - Ctime int64 `json:"ctime"` - Utime int64 `json:"utime"` + Id int64 `json:"id"` + Uid int64 `json:"uid"` + Labels []string `json:"labels"` + Title string `json:"title"` + Content string `json:"content"` + GithubRepo string `json:"github_repo"` + GiteeRepo string `json:"gitee_repo"` + Keywords string `json:"keywords"` + Shorthand string `json:"shorthand"` + Highlight string `json:"highlight"` + Guidance string `json:"guidance"` + Status uint8 `json:"status"` + Ctime int64 `json:"ctime"` + Utime int64 `json:"utime"` } type CaseElasticDAO struct { client *elastic.Client diff --git a/internal/search/internal/repository/dao/case_index.json b/internal/search/internal/repository/dao/case_index.json index ee9311e0..a803f80f 100644 --- a/internal/search/internal/repository/dao/case_index.json +++ b/internal/search/internal/repository/dao/case_index.json @@ -16,7 +16,10 @@ "content": { "type": "text" }, - "code_repo": { + "github_repo": { + "type": "keyword" + }, + "gitee_repo": { "type": "keyword" }, "keywords": { diff --git a/internal/search/internal/web/vo.go b/internal/search/internal/web/vo.go index aa152894..fb05e476 100644 --- a/internal/search/internal/web/vo.go +++ b/internal/search/internal/web/vo.go @@ -27,19 +27,20 @@ type SearchReq struct { } type Case struct { - Id int64 `json:"id,omitempty"` - Uid int64 `json:"uid,omitempty"` - Labels []string `json:"labels,omitempty"` - Title string `json:"title,omitempty"` - Content string `json:"content,omitempty"` - CodeRepo string `json:"code_repo,omitempty"` - Keywords string `json:"keywords,omitempty"` - Shorthand string `json:"shorthand,omitempty"` - Highlight string `json:"highlight,omitempty"` - Guidance string `json:"guidance,omitempty"` - Status uint8 `json:"status,omitempty"` - Ctime string `json:"ctime,omitempty"` - Utime string `json:"utime,omitempty"` + Id int64 `json:"id,omitempty"` + Uid int64 `json:"uid,omitempty"` + Labels []string `json:"labels,omitempty"` + Title string `json:"title,omitempty"` + Content string `json:"content,omitempty"` + GithubRepo string `json:"githubRepo,omitempty"` + GiteeRepo string `json:"giteeRepo,omitempty"` + Keywords string `json:"keywords,omitempty"` + Shorthand string `json:"shorthand,omitempty"` + Highlight string `json:"highlight,omitempty"` + Guidance string `json:"guidance,omitempty"` + Status uint8 `json:"status,omitempty"` + Ctime string `json:"ctime,omitempty"` + Utime string `json:"utime,omitempty"` } type Question struct { @@ -110,19 +111,20 @@ func NewSearchResult(res *domain.SearchResult) SearchResult { var newResult SearchResult for _, oldCase := range res.Cases { newCase := Case{ - Id: oldCase.Id, - Uid: oldCase.Uid, - Labels: oldCase.Labels, - Title: oldCase.Title, - Content: oldCase.Content, - CodeRepo: oldCase.CodeRepo, - Keywords: oldCase.Keywords, - Shorthand: oldCase.Shorthand, - Highlight: oldCase.Highlight, - Guidance: oldCase.Guidance, - Status: oldCase.Status.ToUint8(), - Ctime: oldCase.Ctime.Format(time.DateTime), - Utime: oldCase.Utime.Format(time.DateTime), + Id: oldCase.Id, + Uid: oldCase.Uid, + Labels: oldCase.Labels, + Title: oldCase.Title, + Content: oldCase.Content, + GithubRepo: oldCase.GithubRepo, + GiteeRepo: oldCase.GiteeRepo, + Keywords: oldCase.Keywords, + Shorthand: oldCase.Shorthand, + Highlight: oldCase.Highlight, + Guidance: oldCase.Guidance, + Status: oldCase.Status.ToUint8(), + Ctime: oldCase.Ctime.Format(time.DateTime), + Utime: oldCase.Utime.Format(time.DateTime), } newResult.Cases = append(newResult.Cases, newCase) } diff --git a/ioc/wire_gen.go b/ioc/wire_gen.go index e177b9a5..3393d2ba 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 github.com/google/wire/cmd/wire +//go:generate go run -mod=mod github.com/google/wire/cmd/wire //go:build !wireinject // +build !wireinject @@ -74,7 +74,7 @@ func InitApp() (*App, error) { handler2 := InitUserHandler(db, cache, mq, module, permissionModule) config := InitCosConfig() handler3 := cos.InitHandler(config) - casesModule, err := cases.InitModule(db, interactiveModule, mq) + casesModule, err := cases.InitModule(db, interactiveModule, aiModule, mq) if err != nil { return nil, err }