diff --git a/internal/label/internal/integration/handler_test.go b/internal/label/internal/integration/handler_test.go index 8cee1736..45a7cc68 100644 --- a/internal/label/internal/integration/handler_test.go +++ b/internal/label/internal/integration/handler_test.go @@ -80,12 +80,15 @@ func (s *HandlerTestSuite) TestSystemLabels() { before: func(t *testing.T) { err := s.db.Create([]dao.Label{ {Id: 1, Name: "test", Uid: -1}, - {Id: 2, Name: "non-system", Uid: 123}}).Error + {Id: 2, Name: "non-system", Uid: 123}, + {Id: 3, Name: "test-1", Uid: -1}, + }).Error require.NoError(t, err) }, wantCode: 200, wantResp: test.Result[[]web.Label]{ Data: []web.Label{ + {Id: 3, Name: "test-1"}, {Id: 1, Name: "test"}, }, }, diff --git a/internal/label/internal/repository/dao/dao.go b/internal/label/internal/repository/dao/dao.go index 15eb4448..a6fcd2f0 100644 --- a/internal/label/internal/repository/dao/dao.go +++ b/internal/label/internal/repository/dao/dao.go @@ -48,7 +48,7 @@ func (dao *LabelGORMDAO) CreateLabel(ctx context.Context, label Label) (int64, e func (dao *LabelGORMDAO) UidLabels(ctx context.Context, uid int64) ([]Label, error) { var res []Label err := dao.db.WithContext(ctx). - Where("uid = ?", uid).Find(&res).Error + Where("uid = ?", uid).Order("id DESC").Find(&res).Error return res, err } @@ -57,9 +57,9 @@ func NewLabelGORMDAO(db *egorm.Component) LabelDAO { } type Label struct { - Id int64 `gorm:"primaryKey,autoIncrement"` - Name string - Uid int64 `gorm:"index"` + Id int64 `gorm:"primaryKey,autoIncrement"` + Name string `gorm:"type:varchar(256);unique"` + Uid int64 `gorm:"index"` Ctime int64 Utime int64 } diff --git a/internal/project/internal/domain/project.go b/internal/project/internal/domain/project.go index 2b3332a2..744926a3 100644 --- a/internal/project/internal/domain/project.go +++ b/internal/project/internal/domain/project.go @@ -41,6 +41,7 @@ type Project struct { Questions []Question Resumes []Resume Introductions []Introduction + Combos []Combo // 目前来说,我们只需要两个 SN,而不是需要维持整个 SPU // 后续如果需要 SPU 的其他字段,就重构为结构体 @@ -161,3 +162,24 @@ const ( func (r Role) ToUint8() uint8 { return uint8(r) } + +// Combo 面试套路,连招 +type Combo struct { + Id int64 + Title string + Content string + Utime int64 + Status ComboStatus +} + +type ComboStatus uint8 + +func (s ComboStatus) ToUint8() uint8 { + return uint8(s) +} + +const ( + ComboStatusUnknown ComboStatus = iota + ComboStatusUnpublished + ComboStatusPublished +) diff --git a/internal/project/internal/integration/admin_combo_test.go b/internal/project/internal/integration/admin_combo_test.go new file mode 100644 index 00000000..13d4a61a --- /dev/null +++ b/internal/project/internal/integration/admin_combo_test.go @@ -0,0 +1,410 @@ +// Copyright 2023 ecodeclub +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//go:build e2e + +package integration + +import ( + "context" + "fmt" + "net/http" + "testing" + "time" + + "github.com/ecodeclub/ekit/iox" + "github.com/ecodeclub/webook/internal/project/internal/domain" + "github.com/ecodeclub/webook/internal/project/internal/repository/dao" + "github.com/ecodeclub/webook/internal/project/internal/web" + "github.com/ecodeclub/webook/internal/test" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func (s *AdminProjectTestSuite) TestComboSave() { + const pid = 123 + testCases := []struct { + name string + before func(t *testing.T) + after func(t *testing.T) + + req web.ComboSaveReq + wantCode int + wantResp test.Result[int64] + }{ + { + name: "新建", + before: func(t *testing.T) {}, + after: func(t *testing.T) { + // 验证数据库的数据 + ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) + defer cancel() + c, err := s.adminPrjDAO.ComboById(ctx, 1) + require.NoError(t, err) + assert.True(t, c.Utime > 0) + c.Utime = 0 + assert.True(t, c.Ctime > 0) + c.Ctime = 0 + assert.Equal(t, dao.ProjectCombo{ + Id: 1, + Pid: pid, + Title: "标题1", + Content: "内容1", + Status: domain.ComboStatusUnpublished.ToUint8(), + }, c) + }, + req: web.ComboSaveReq{ + Pid: pid, + Combo: web.Combo{ + Title: "标题1", + Content: "内容1", + }, + }, + wantCode: 200, + wantResp: test.Result[int64]{Data: 1}, + }, + { + name: "更新", + before: func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) + defer cancel() + err := s.db.WithContext(ctx).Create(&dao.ProjectCombo{ + Id: 2, + Pid: pid, + Title: "老的标题1", + Content: "老的内容1", + Status: domain.ComboStatusPublished.ToUint8(), + Utime: 123, + Ctime: 123, + }).Error + require.NoError(t, err) + }, + after: func(t *testing.T) { + // 验证数据库的数据 + ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) + defer cancel() + c, err := s.adminPrjDAO.ComboById(ctx, 2) + require.NoError(t, err) + // 更新时间变了 + assert.True(t, c.Utime > 123) + c.Utime = 0 + assert.Equal(t, dao.ProjectCombo{ + Id: 2, + // pid 不会发生变化 + Pid: 123, + Title: "标题1", + Content: "内容1", + Status: domain.ComboStatusUnpublished.ToUint8(), + // Ctime 也不会发生变化 + Ctime: 123, + }, c) + }, + req: web.ComboSaveReq{ + Pid: 1234, + Combo: web.Combo{ + Id: 2, + Title: "标题1", + Content: "内容1", + }, + }, + wantCode: 200, + wantResp: test.Result[int64]{Data: 2}, + }, + } + + for _, tc := range testCases { + s.T().Run(tc.name, func(t *testing.T) { + tc.before(t) + req, err := http.NewRequest(http.MethodPost, + "/project/combo/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) + val := recorder.MustScan() + assert.Equal(t, tc.wantResp, val) + tc.after(t) + }) + } +} + +func (s *AdminProjectTestSuite) TestComboDetail() { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) + defer cancel() + const pid = 123 + err := s.db.WithContext(ctx).Create(s.mockCombo(pid, 1)).Error + require.NoError(s.T(), err) + testCases := []struct { + name string + req web.IdReq + wantCode int + wantResp test.Result[web.Combo] + }{ + { + name: "成功", + req: web.IdReq{ + Id: 1, + }, + + wantCode: 200, + wantResp: test.Result[web.Combo]{ + Data: web.Combo{ + Id: 1, + Title: "标题1", + Content: "内容1", + Status: domain.ComboStatusUnpublished.ToUint8(), + Utime: 1, + }, + }, + }, + } + + for _, tc := range testCases { + s.T().Run(tc.name, func(t *testing.T) { + req, err := http.NewRequest(http.MethodPost, + "/project/combo/detail", iox.NewJSONReader(tc.req)) + req.Header.Set("content-type", "application/json") + require.NoError(t, err) + recorder := test.NewJSONResponseRecorder[web.Combo]() + s.server.ServeHTTP(recorder, req) + require.Equal(t, tc.wantCode, recorder.Code) + val := recorder.MustScan() + assert.Equal(t, tc.wantResp, val) + }) + } +} + +func (s *AdminProjectTestSuite) TestComboPublish() { + const pid = 123 + testCases := []struct { + name string + before func(t *testing.T) + after func(t *testing.T) + + req web.ComboSaveReq + wantCode int + wantResp test.Result[int64] + }{ + { + name: "全新建", + before: func(t *testing.T) { + + }, + after: func(t *testing.T) { + // 验证两个库都有数据 + ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) + defer cancel() + c, err := s.adminPrjDAO.ComboById(ctx, 1) + require.NoError(t, err) + assert.True(t, c.Utime > 0) + c.Utime = 0 + assert.True(t, c.Ctime > 0) + c.Ctime = 0 + assert.Equal(t, dao.ProjectCombo{ + Id: 1, + Pid: pid, + Title: "标题1", + Content: "内容1", + Status: domain.ComboStatusPublished.ToUint8(), + }, c) + + var pub dao.PubProjectCombo + err = s.db.WithContext(ctx).Where("id = ?", 1).First(&pub).Error + require.NoError(t, err) + assert.True(t, pub.Utime > 0) + pub.Utime = 0 + assert.True(t, pub.Ctime > 0) + pub.Ctime = 0 + assert.Equal(t, dao.PubProjectCombo{ + Id: 1, + Pid: pid, + Title: "标题1", + Content: "内容1", + Status: domain.ComboStatusPublished.ToUint8(), + }, pub) + }, + req: web.ComboSaveReq{ + Pid: pid, + Combo: web.Combo{ + Title: "标题1", + Content: "内容1", + }, + }, + wantCode: 200, + wantResp: test.Result[int64]{ + Data: 1, + }, + }, + + { + name: "制作库存在,线上库新建", + before: func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) + defer cancel() + err := s.db.WithContext(ctx).Create(dao.ProjectCombo{ + Id: 2, + Pid: pid, + Title: "老的标题1", + Content: "老的内容1", + Status: domain.ComboStatusPublished.ToUint8(), + Utime: 123, + Ctime: 123, + }).Error + require.NoError(t, err) + }, + after: func(t *testing.T) { + // 验证两个库都有数据 + ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) + defer cancel() + c, err := s.adminPrjDAO.ComboById(ctx, 2) + require.NoError(t, err) + assert.True(t, c.Utime > 123) + c.Utime = 0 + assert.Equal(t, dao.ProjectCombo{ + Id: 2, + Pid: pid, + Title: "标题1", + Content: "内容1", + Status: domain.ComboStatusPublished.ToUint8(), + Ctime: 123, + }, c) + + var pub dao.PubProjectCombo + err = s.db.WithContext(ctx).Where("id = ?", 2).First(&pub).Error + require.NoError(t, err) + assert.True(t, pub.Utime > 0) + pub.Utime = 0 + assert.True(t, pub.Ctime > 0) + pub.Ctime = 0 + assert.Equal(t, dao.PubProjectCombo{ + Id: 2, + Pid: pid, + Title: "标题1", + Content: "内容1", + Status: domain.ComboStatusPublished.ToUint8(), + }, pub) + }, + req: web.ComboSaveReq{ + Pid: pid, + Combo: web.Combo{ + Id: 2, + Title: "标题1", + Content: "内容1", + }, + }, + wantCode: 200, + wantResp: test.Result[int64]{ + Data: 2, + }, + }, + + { + name: "制作库线上库更新", + before: func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) + defer cancel() + err := s.db.WithContext(ctx).Create(dao.ProjectCombo{ + Id: 3, + Pid: pid, + Title: "老的标题1", + Content: "老的内容1", + Status: domain.ComboStatusPublished.ToUint8(), + Utime: 123, + Ctime: 123, + }).Error + require.NoError(t, err) + err = s.db.WithContext(ctx).Create(dao.PubProjectCombo{ + Id: 3, + Pid: pid, + Title: "老的标题1", + Content: "老的内容1", + Status: domain.ComboStatusPublished.ToUint8(), + Utime: 123, + Ctime: 123, + }).Error + require.NoError(t, err) + }, + after: func(t *testing.T) { + // 验证两个库都有数据 + ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) + defer cancel() + c, err := s.adminPrjDAO.ComboById(ctx, 3) + require.NoError(t, err) + assert.True(t, c.Utime > 123) + c.Utime = 0 + assert.Equal(t, dao.ProjectCombo{ + Id: 3, + Pid: pid, + Title: "标题1", + Content: "内容1", + Status: domain.ComboStatusPublished.ToUint8(), + Ctime: 123, + }, c) + + var pub dao.PubProjectCombo + err = s.db.WithContext(ctx).Where("id = ?", 3).First(&pub).Error + require.NoError(t, err) + assert.True(t, pub.Utime > 123) + pub.Utime = 0 + assert.Equal(t, dao.PubProjectCombo{ + Id: 3, + Pid: pid, + Title: "标题1", + Content: "内容1", + Ctime: 123, + Status: domain.ComboStatusPublished.ToUint8(), + }, pub) + }, + req: web.ComboSaveReq{ + Pid: pid, + Combo: web.Combo{ + Id: 3, + Title: "标题1", + Content: "内容1", + }, + }, + wantCode: 200, + wantResp: test.Result[int64]{ + Data: 3, + }, + }, + } + for _, tc := range testCases { + s.T().Run(tc.name, func(t *testing.T) { + tc.before(t) + req, err := http.NewRequest(http.MethodPost, + "/project/combo/publish", 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) + val := recorder.MustScan() + assert.Equal(t, tc.wantResp, val) + tc.after(t) + }) + } +} + +func (s *AdminProjectTestSuite) mockCombo(pid, id int64) dao.ProjectCombo { + return dao.ProjectCombo{ + Id: id, + Pid: pid, + Title: fmt.Sprintf("标题%d", id), + Content: fmt.Sprintf("内容%d", id), + Status: domain.ComboStatusUnpublished.ToUint8(), + Ctime: id, + Utime: id, + } +} diff --git a/internal/project/internal/integration/admin_project_test.go b/internal/project/internal/integration/admin_project_test.go index e95ef3bf..428d8680 100644 --- a/internal/project/internal/integration/admin_project_test.go +++ b/internal/project/internal/integration/admin_project_test.go @@ -97,6 +97,10 @@ func (s *AdminProjectTestSuite) TearDownTest() { require.NoError(s.T(), err) err = s.db.Exec("TRUNCATE TABLE pub_project_introductions;").Error require.NoError(s.T(), err) + err = s.db.Exec("TRUNCATE TABLE project_combos;").Error + require.NoError(s.T(), err) + err = s.db.Exec("TRUNCATE TABLE pub_project_combos;").Error + require.NoError(s.T(), err) } // TestProjectSave 测试 Project 本身数据的保存 @@ -663,6 +667,10 @@ func (s *AdminProjectTestSuite) TestProjectDetail() { err = s.db.Create(&que).Error require.NoError(s.T(), err) + combo := s.mockCombo(1, 1) + err = s.db.Create(&combo).Error + require.NoError(s.T(), err) + testCases := []struct { name string req web.IdReq @@ -714,7 +722,7 @@ func (s *AdminProjectTestSuite) TestProjectDetail() { Role: domain.RoleManager.ToUint8(), Content: "内容1", Analysis: "分析1", - Status: domain.ResumeStatusPublished.ToUint8(), + Status: domain.IntroductionStatusPublished.ToUint8(), Utime: 1, }, }, @@ -724,10 +732,19 @@ func (s *AdminProjectTestSuite) TestProjectDetail() { Analysis: "分析1", Answer: "回答1", Title: "标题1", - Status: domain.ResumeStatusPublished.ToUint8(), + Status: domain.QuestionStatusPublished.ToUint8(), Utime: 1, }, }, + Combos: []web.Combo{ + { + Id: 1, + Content: "内容1", + Title: "标题1", + Status: domain.ComboStatusUnpublished.ToUint8(), + Utime: 1, + }, + }, }, }, }, diff --git a/internal/project/internal/integration/project_test.go b/internal/project/internal/integration/project_test.go index 25dd20c5..112a8f76 100644 --- a/internal/project/internal/integration/project_test.go +++ b/internal/project/internal/integration/project_test.go @@ -322,6 +322,15 @@ func (s *ProjectTestSuite) TestProjectDetail() { Utime: 11, }, }, + Combos: []web.Combo{ + { + Id: 11, + Content: "内容11", + Title: "标题11", + Status: domain.ResumeStatusPublished.ToUint8(), + Utime: 11, + }, + }, }, }, }, @@ -389,6 +398,22 @@ func (s *ProjectTestSuite) insertWholeProject(id int64) { que := s.mockQue(id, id*10+1) err = s.db.Create(&que).Error require.NoError(s.T(), err) + + combo := s.mockCombo(id, id*10+1) + err = s.db.Create(&combo).Error + require.NoError(s.T(), err) +} + +func (s *ProjectTestSuite) mockCombo(pid, id int64) dao.PubProjectCombo { + return dao.PubProjectCombo{ + Id: id, + Pid: pid, + Title: fmt.Sprintf("标题%d", id), + Content: fmt.Sprintf("内容%d", id), + Status: domain.ComboStatusPublished.ToUint8(), + Ctime: id, + Utime: id, + } } func (s *ProjectTestSuite) mockProject(id int64) dao.PubProject { diff --git a/internal/project/internal/repository/admin.go b/internal/project/internal/repository/admin.go index eb9b82a0..7f9cff79 100644 --- a/internal/project/internal/repository/admin.go +++ b/internal/project/internal/repository/admin.go @@ -48,6 +48,9 @@ type ProjectAdminRepository interface { IntroductionSave(ctx context.Context, pid int64, intr domain.Introduction) (int64, error) IntroductionDetail(ctx context.Context, id int64) (domain.Introduction, error) IntroductionSync(ctx context.Context, pid int64, intr domain.Introduction) (int64, error) + ComboSave(ctx context.Context, pid int64, c domain.Combo) (int64, error) + ComboDetail(ctx context.Context, cid int64) (domain.Combo, error) + ComboSync(ctx context.Context, pid int64, c domain.Combo) (int64, error) } var _ ProjectAdminRepository = (*projectAdminRepository)(nil) @@ -56,6 +59,23 @@ type projectAdminRepository struct { dao dao.ProjectAdminDAO } +func (repo *projectAdminRepository) ComboSync(ctx context.Context, pid int64, c domain.Combo) (int64, error) { + entity := repo.comboToEntity(c) + entity.Pid = pid + return repo.dao.ComboSync(ctx, entity) +} + +func (repo *projectAdminRepository) ComboDetail(ctx context.Context, cid int64) (domain.Combo, error) { + c, err := repo.dao.ComboById(ctx, cid) + return repo.comboToDomain(c), err +} + +func (repo *projectAdminRepository) ComboSave(ctx context.Context, pid int64, c domain.Combo) (int64, error) { + entity := repo.comboToEntity(c) + entity.Pid = pid + return repo.dao.ComboSave(ctx, entity) +} + func (repo *projectAdminRepository) ResumePublish(ctx context.Context, pid int64, resume domain.Resume) (int64, error) { entity := repo.rsmToEntity(resume) entity.Pid = pid @@ -137,6 +157,7 @@ func (repo *projectAdminRepository) Detail(ctx context.Context, id int64) (domai diffs []dao.ProjectDifficulty ques []dao.ProjectQuestion intrs []dao.ProjectIntroduction + combos []dao.ProjectCombo ) eg.Go(func() error { var err error @@ -167,8 +188,14 @@ func (repo *projectAdminRepository) Detail(ctx context.Context, id int64) (domai intrs, err = repo.dao.Introductions(ctx, id) return err }) + + eg.Go(func() error { + var err error + combos, err = repo.dao.Combos(ctx, id) + return err + }) err := eg.Wait() - return repo.prjToDomain(prj, resumes, diffs, ques, intrs), err + return repo.prjToDomain(prj, resumes, diffs, ques, intrs, combos), err } func (repo *projectAdminRepository) Count(ctx context.Context) (int64, error) { @@ -178,7 +205,7 @@ func (repo *projectAdminRepository) Count(ctx context.Context) (int64, error) { func (repo *projectAdminRepository) List(ctx context.Context, offset int, limit int) ([]domain.Project, error) { res, err := repo.dao.List(ctx, offset, limit) return slice.Map(res, func(idx int, src dao.Project) domain.Project { - return repo.prjToDomain(src, nil, nil, nil, nil) + return repo.prjToDomain(src, nil, nil, nil, nil, nil) }), err } @@ -256,6 +283,7 @@ func (repo *projectAdminRepository) prjToDomain(prj dao.Project, diff []dao.ProjectDifficulty, ques []dao.ProjectQuestion, intrs []dao.ProjectIntroduction, + combos []dao.ProjectCombo, ) domain.Project { return domain.Project{ Id: prj.Id, @@ -282,6 +310,9 @@ func (repo *projectAdminRepository) prjToDomain(prj dao.Project, Introductions: slice.Map(intrs, func(idx int, src dao.ProjectIntroduction) domain.Introduction { return repo.intrToDomain(src) }), + Combos: slice.Map(combos, func(idx int, src dao.ProjectCombo) domain.Combo { + return repo.comboToDomain(src) + }), } } @@ -328,3 +359,23 @@ func (repo *projectAdminRepository) queToDomain(d dao.ProjectQuestion) domain.Qu Utime: time.UnixMilli(d.Utime), } } + +func (repo *projectAdminRepository) comboToDomain(c dao.ProjectCombo) domain.Combo { + return domain.Combo{ + Id: c.Id, + Title: c.Title, + Content: c.Content, + Utime: c.Utime, + Status: domain.ComboStatus(c.Status), + } +} + +func (repo *projectAdminRepository) comboToEntity(c domain.Combo) dao.ProjectCombo { + return dao.ProjectCombo{ + Id: c.Id, + Title: c.Title, + Content: c.Content, + Utime: c.Utime, + Status: c.Status.ToUint8(), + } +} diff --git a/internal/project/internal/repository/dao/admin.go b/internal/project/internal/repository/dao/admin.go index 2edaf1cf..c0260a83 100644 --- a/internal/project/internal/repository/dao/admin.go +++ b/internal/project/internal/repository/dao/admin.go @@ -50,6 +50,10 @@ type ProjectAdminDAO interface { IntroductionById(ctx context.Context, id int64) (ProjectIntroduction, error) IntroductionSync(ctx context.Context, intr ProjectIntroduction) (int64, error) Introductions(ctx context.Context, pid int64) ([]ProjectIntroduction, error) + ComboSave(ctx context.Context, c ProjectCombo) (int64, error) + ComboById(ctx context.Context, cid int64) (ProjectCombo, error) + ComboSync(ctx context.Context, c ProjectCombo) (int64, error) + Combos(ctx context.Context, pid int64) ([]ProjectCombo, error) } var _ ProjectAdminDAO = &GORMProjectAdminDAO{} @@ -59,6 +63,50 @@ type GORMProjectAdminDAO struct { prjUpdateColumns []string } +func (dao *GORMProjectAdminDAO) Combos(ctx context.Context, pid int64) ([]ProjectCombo, error) { + var res []ProjectCombo + err := dao.db.WithContext(ctx).Where("pid = ?", pid).Find(&res).Error + return res, err +} + +func (dao *GORMProjectAdminDAO) ComboSync(ctx context.Context, c ProjectCombo) (int64, error) { + err := dao.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + _, err := dao.comboSave(tx, &c) + if err != nil { + return err + } + pubCb := PubProjectCombo(c) + return tx.Clauses(clause.OnConflict{ + DoUpdates: clause.AssignmentColumns([]string{ + "content", "title", "status", "utime", + }), + }).Create(&pubCb).Error + }) + return c.Id, err +} + +func (dao *GORMProjectAdminDAO) ComboById(ctx context.Context, cid int64) (ProjectCombo, error) { + var c ProjectCombo + err := dao.db.WithContext(ctx).Where("id = ?", cid).First(&c).Error + return c, err +} + +func (dao *GORMProjectAdminDAO) ComboSave(ctx context.Context, c ProjectCombo) (int64, error) { + return dao.comboSave(dao.db.WithContext(ctx), &c) +} + +func (dao *GORMProjectAdminDAO) comboSave(tx *gorm.DB, c *ProjectCombo) (int64, error) { + now := time.Now().UnixMilli() + c.Utime = now + c.Ctime = now + err := tx.Clauses(clause.OnConflict{ + DoUpdates: clause.AssignmentColumns([]string{ + "content", "title", "status", "utime", + }), + }).Create(c).Error + return c.Id, err +} + func (dao *GORMProjectAdminDAO) ResumeSync(ctx context.Context, rsm ProjectResume) (int64, error) { err := dao.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { _, err := dao.rsmSave(tx, &rsm) diff --git a/internal/project/internal/repository/dao/dao.go b/internal/project/internal/repository/dao/dao.go index db4bb009..7ed2a108 100644 --- a/internal/project/internal/repository/dao/dao.go +++ b/internal/project/internal/repository/dao/dao.go @@ -29,6 +29,7 @@ type ProjectDAO interface { Difficulties(ctx context.Context, pid int64) ([]PubProjectDifficulty, error) Questions(ctx context.Context, pid int64) ([]PubProjectQuestion, error) Introductions(ctx context.Context, pid int64) ([]PubProjectIntroduction, error) + Combos(ctx context.Context, pid int64) ([]PubProjectCombo, error) } var _ ProjectDAO = &GORMProjectDAO{} @@ -38,6 +39,12 @@ type GORMProjectDAO struct { briefColumns []string } +func (dao *GORMProjectDAO) Combos(ctx context.Context, pid int64) ([]PubProjectCombo, error) { + var res []PubProjectCombo + err := dao.db.WithContext(ctx).Where("pid = ?", pid).Find(&res).Error + return res, err +} + func (dao *GORMProjectDAO) Introductions(ctx context.Context, pid int64) ([]PubProjectIntroduction, error) { var res []PubProjectIntroduction err := dao.db.WithContext(ctx).Where("pid = ?", pid).Find(&res).Error diff --git a/internal/project/internal/repository/dao/init.go b/internal/project/internal/repository/dao/init.go index b781ea33..951060d2 100644 --- a/internal/project/internal/repository/dao/init.go +++ b/internal/project/internal/repository/dao/init.go @@ -27,5 +27,8 @@ func InitTables(db *egorm.Component) error { &ProjectResume{}, &PubProjectResume{}, &ProjectIntroduction{}, - &PubProjectIntroduction{}) + &PubProjectIntroduction{}, + &ProjectCombo{}, + &PubProjectCombo{}, + ) } diff --git a/internal/project/internal/repository/dao/types.go b/internal/project/internal/repository/dao/types.go index 7370628b..255306e7 100644 --- a/internal/project/internal/repository/dao/types.go +++ b/internal/project/internal/repository/dao/types.go @@ -93,3 +93,15 @@ type ProjectQuestion struct { Utime int64 Ctime int64 } + +type ProjectCombo struct { + Id int64 `gorm:"primaryKey,autoIncrement"` + Pid int64 `gorm:"index"` + Title string `gorm:"type:varchar(256)"` + Content string + Status uint8 + Utime int64 + Ctime int64 +} + +type PubProjectCombo ProjectCombo diff --git a/internal/project/internal/repository/repository.go b/internal/project/internal/repository/repository.go index 54777351..a0c50ec8 100644 --- a/internal/project/internal/repository/repository.go +++ b/internal/project/internal/repository/repository.go @@ -39,7 +39,7 @@ type CachedRepository struct { func (repo *CachedRepository) Brief(ctx context.Context, id int64) (domain.Project, error) { //TODO implement me prj, err := repo.dao.BriefById(ctx, id) - return repo.prjToDomain(prj, nil, nil, nil, nil), err + return repo.prjToDomain(prj, nil, nil, nil, nil, nil), err } func (repo *CachedRepository) Detail(ctx context.Context, id int64) (domain.Project, error) { //TODO implement me @@ -50,6 +50,7 @@ func (repo *CachedRepository) Detail(ctx context.Context, id int64) (domain.Proj diffs []dao.PubProjectDifficulty ques []dao.PubProjectQuestion intrs []dao.PubProjectIntroduction + combos []dao.PubProjectCombo ) eg.Go(func() error { var err error @@ -80,14 +81,21 @@ func (repo *CachedRepository) Detail(ctx context.Context, id int64) (domain.Proj intrs, err = repo.dao.Introductions(ctx, id) return err }) + + eg.Go(func() error { + var err error + combos, err = repo.dao.Combos(ctx, id) + return err + }) + err := eg.Wait() - return repo.prjToDomain(prj, resumes, diffs, ques, intrs), err + return repo.prjToDomain(prj, resumes, diffs, ques, intrs, combos), err } func (repo *CachedRepository) List(ctx context.Context, offset int, limit int) ([]domain.Project, error) { res, err := repo.dao.List(ctx, offset, limit) return slice.Map(res, func(idx int, src dao.PubProject) domain.Project { - return repo.prjToDomain(src, nil, nil, nil, nil) + return repo.prjToDomain(src, nil, nil, nil, nil, nil) }), err } @@ -96,6 +104,7 @@ func (repo *CachedRepository) prjToDomain(prj dao.PubProject, diff []dao.PubProjectDifficulty, ques []dao.PubProjectQuestion, intrs []dao.PubProjectIntroduction, + combos []dao.PubProjectCombo, ) domain.Project { return domain.Project{ Id: prj.Id, @@ -124,6 +133,19 @@ func (repo *CachedRepository) prjToDomain(prj dao.PubProject, Introductions: slice.Map(intrs, func(idx int, src dao.PubProjectIntroduction) domain.Introduction { return repo.intrToDomain(src) }), + Combos: slice.Map(combos, func(idx int, src dao.PubProjectCombo) domain.Combo { + return repo.comboToDomain(src) + }), + } +} + +func (repo *CachedRepository) comboToDomain(c dao.PubProjectCombo) domain.Combo { + return domain.Combo{ + Id: c.Id, + Title: c.Title, + Content: c.Content, + Utime: c.Utime, + Status: domain.ComboStatus(c.Status), } } diff --git a/internal/project/internal/service/admin.go b/internal/project/internal/service/admin.go index 05d1e726..affed27e 100644 --- a/internal/project/internal/service/admin.go +++ b/internal/project/internal/service/admin.go @@ -50,8 +50,14 @@ type ProjectAdminService interface { IntroductionSave(ctx context.Context, pid int64, intr domain.Introduction) (int64, error) IntroductionDetail(ctx context.Context, id int64) (domain.Introduction, error) IntroductionPublish(ctx context.Context, pid int64, intr domain.Introduction) (int64, error) + + ComboSave(ctx context.Context, pid int64, c domain.Combo) (int64, error) + ComboDetail(ctx context.Context, cid int64) (domain.Combo, error) + ComboPublish(ctx context.Context, pid int64, c domain.Combo) (int64, error) } +var _ ProjectAdminService = (*projectAdminService)(nil) + type projectAdminService struct { adminRepo repository.ProjectAdminRepository repo repository.Repository @@ -59,6 +65,25 @@ type projectAdminService struct { logger *elog.Component } +func (svc *projectAdminService) ComboPublish(ctx context.Context, pid int64, c domain.Combo) (int64, error) { + c.Status = domain.ComboStatusPublished + id, err := svc.adminRepo.ComboSync(ctx, pid, c) + if err == nil { + // 同步数据 + svc.syncToSearch(pid) + } + return id, err +} + +func (svc *projectAdminService) ComboDetail(ctx context.Context, cid int64) (domain.Combo, error) { + return svc.adminRepo.ComboDetail(ctx, cid) +} + +func (svc *projectAdminService) ComboSave(ctx context.Context, pid int64, c domain.Combo) (int64, error) { + c.Status = domain.ComboStatusUnpublished + return svc.adminRepo.ComboSave(ctx, pid, c) +} + func (svc *projectAdminService) ResumePublish(ctx context.Context, pid int64, resume domain.Resume) (int64, error) { resume.Status = domain.ResumeStatusPublished id, err := svc.adminRepo.ResumePublish(ctx, pid, resume) diff --git a/internal/project/internal/web/admin.go b/internal/project/internal/web/admin.go index 4532eacd..761e9044 100644 --- a/internal/project/internal/web/admin.go +++ b/internal/project/internal/web/admin.go @@ -50,6 +50,11 @@ func (h *AdminHandler) PrivateRoutes(server *gin.Engine) { g.POST("/introduction/save", ginx.B(h.IntroductionSave)) g.POST("/introduction/detail", ginx.B(h.IntroductionDetail)) g.POST("/introduction/publish", ginx.B(h.IntroductionPublish)) + + // 面试小套路,连招 + g.POST("/combo/save", ginx.B(h.ComboSave)) + g.POST("/combo/detail", ginx.B(h.ComboDetail)) + g.POST("/combo/publish", ginx.B(h.ComboPublish)) } func (h *AdminHandler) List(ctx *ginx.Context, req Page) (ginx.Result, error) { @@ -240,6 +245,36 @@ func (h *AdminHandler) IntroductionPublish(ctx *ginx.Context, }, nil } +func (h *AdminHandler) ComboSave(ctx *ginx.Context, req ComboSaveReq) (ginx.Result, error) { + id, err := h.svc.ComboSave(ctx, req.Pid, req.Combo.toDomain()) + if err != nil { + return systemErrorResult, err + } + return ginx.Result{ + Data: id, + }, nil +} + +func (h *AdminHandler) ComboDetail(ctx *ginx.Context, req IdReq) (ginx.Result, error) { + res, err := h.svc.ComboDetail(ctx, req.Id) + if err != nil { + return systemErrorResult, err + } + return ginx.Result{ + Data: newCombo(res), + }, nil +} + +func (h *AdminHandler) ComboPublish(ctx *ginx.Context, req ComboSaveReq) (ginx.Result, error) { + id, err := h.svc.ComboPublish(ctx, req.Pid, req.Combo.toDomain()) + if err != nil { + return systemErrorResult, err + } + return ginx.Result{ + Data: id, + }, nil +} + func NewAdminHandler(svc service.ProjectAdminService) *AdminHandler { return &AdminHandler{ svc: svc, diff --git a/internal/project/internal/web/vo.go b/internal/project/internal/web/vo.go index 328d5ccb..c25d1c24 100644 --- a/internal/project/internal/web/vo.go +++ b/internal/project/internal/web/vo.go @@ -46,6 +46,7 @@ type Project struct { Resumes []Resume `json:"resumes,omitempty"` Questions []Question `json:"questions,omitempty"` Introductions []Introduction `json:"introductions,omitempty"` + Combos []Combo `json:"combos,omitempty"` Interactive Interactive `json:"interactive,omitempty"` Permitted bool `json:"permitted"` CodeSPU string `json:"codeSPU"` @@ -80,6 +81,9 @@ func newProject(p domain.Project, intr interactive.Interactive) Project { Introductions: slice.Map(p.Introductions, func(idx int, src domain.Introduction) Introduction { return newIntroduction(src) }), + Combos: slice.Map(p.Combos, func(idx int, src domain.Combo) Combo { + return newCombo(src) + }), Interactive: newInteractive(intr), } } @@ -280,3 +284,37 @@ func newInteractive(intr interactive.Interactive) Interactive { Collected: intr.Collected, } } + +// Combo 面试套路(连招) +type Combo struct { + Id int64 `json:"id,omitempty"` + Title string `json:"title,omitempty"` + Content string `json:"content,omitempty"` + Utime int64 `json:"utime,omitempty"` + Status uint8 `json:"status,omitempty"` +} + +func (c Combo) toDomain() domain.Combo { + return domain.Combo{ + Id: c.Id, + Title: c.Title, + Content: c.Content, + Utime: c.Utime, + Status: domain.ComboStatus(c.Status), + } +} + +func newCombo(c domain.Combo) Combo { + return Combo{ + Id: c.Id, + Title: c.Title, + Content: c.Content, + Utime: c.Utime, + Status: c.Status.ToUint8(), + } +} + +type ComboSaveReq struct { + Pid int64 `json:"pid,omitempty"` + Combo Combo `json:"combo,omitempty"` +} diff --git a/internal/search/internal/integration/handler_test.go b/internal/search/internal/integration/handler_test.go index 5ced24ee..0a0c8750 100644 --- a/internal/search/internal/integration/handler_test.go +++ b/internal/search/internal/integration/handler_test.go @@ -207,7 +207,7 @@ func (s *HandlerTestSuite) TestBizSearch() { }, }, req: web.SearchReq{ - KeyWords: "biz:case:test_content test_keywords test_shorthands test_guidance test_title test_label", + Keywords: "biz:case:test_content test_keywords test_shorthands test_guidance test_title test_label", }, }, { @@ -543,7 +543,7 @@ func (s *HandlerTestSuite) TestBizSearch() { }, }, req: web.SearchReq{ - KeyWords: "biz:question:test_content test_title test_label test_analysis_keywords test_analysis_shorthand test_analysis_highlight test_analysis_guidance test_basic_keywords test_basic_shorthand test_basic_highlight test_basic_guidance test_intermediate_keywords test_intermediate_shorthand test_intermediate_highlight test_intermediate_guidance test_advanced_keywords test_advanced_shorthand test_advanced_highlight test_advanced_guidance", + Keywords: "biz:question:test_content test_title test_label test_analysis_keywords test_analysis_shorthand test_analysis_highlight test_analysis_guidance test_basic_keywords test_basic_shorthand test_basic_highlight test_basic_guidance test_intermediate_keywords test_intermediate_shorthand test_intermediate_highlight test_intermediate_guidance test_advanced_keywords test_advanced_shorthand test_advanced_highlight test_advanced_guidance", }, }, { @@ -625,7 +625,7 @@ func (s *HandlerTestSuite) TestBizSearch() { }, }, req: web.SearchReq{ - KeyWords: "biz:skill:test_name test_label test_desc test_advanced test_basic test_intermediate", + Keywords: "biz:skill:test_name test_label test_desc test_advanced test_basic test_intermediate", }, }, { @@ -656,7 +656,7 @@ func (s *HandlerTestSuite) TestBizSearch() { }, }, req: web.SearchReq{ - KeyWords: "biz:questionSet:test_title test_desc", + Keywords: "biz:questionSet:test_title test_desc", }, }, } @@ -684,7 +684,7 @@ func (s *HandlerTestSuite) TestSearch() { time.Sleep(1 * time.Second) req, err := http.NewRequest(http.MethodPost, "/search/list", iox.NewJSONReader(web.SearchReq{ - KeyWords: "biz:all:test_title", + Keywords: "biz:all:test_title", })) req.Header.Set("content-type", "application/json") require.NoError(t, err) diff --git a/internal/search/internal/service/search.go b/internal/search/internal/service/search.go index cc4e9215..7a93469d 100644 --- a/internal/search/internal/service/search.go +++ b/internal/search/internal/service/search.go @@ -25,7 +25,7 @@ import ( ) type SearchService interface { - // 出于长远考虑,这里你用 expr 来代表搜索的表达式,后期我们会考虑支持类似 github 那种复杂的搜索表达式 + // Search 出于长远考虑,这里你用 expr 来代表搜索的表达式,后期我们会考虑支持类似 github 那种复杂的搜索表达式 // 目前你可以认为,传递过来的就是 biz:all:xxxx // 业务专属就是 biz:question:xxx 这种形态 // xxx 就是搜索的内容 @@ -67,7 +67,7 @@ func (s *searchSvc) Search(ctx context.Context, expr string) (*domain.SearchResu } func (s *searchSvc) parseExpr(expr string) (string, string, error) { - searchParams := strings.Split(expr, ":") + searchParams := strings.SplitN(expr, ":", 3) if len(searchParams) == 3 { typ := searchParams[0] if typ != "biz" { @@ -78,7 +78,6 @@ func (s *searchSvc) parseExpr(expr string) (string, string, error) { return biz, keywords, nil } return "", "", errors.New("参数错误") - } func NewSearchSvc( diff --git a/internal/search/internal/web/handler.go b/internal/search/internal/web/handler.go index c7a6512e..57ddd315 100644 --- a/internal/search/internal/web/handler.go +++ b/internal/search/internal/web/handler.go @@ -38,7 +38,7 @@ func (h *Handler) PublicRoutes(server *gin.Engine) { } func (h *Handler) List(ctx *ginx.Context, req SearchReq) (ginx.Result, error) { - data, err := h.svc.Search(ctx, req.KeyWords) + data, err := h.svc.Search(ctx, req.Keywords) if err != nil { return systemErrorResult, err } diff --git a/internal/search/internal/web/vo.go b/internal/search/internal/web/vo.go index 80ac4f0f..bbd76433 100644 --- a/internal/search/internal/web/vo.go +++ b/internal/search/internal/web/vo.go @@ -21,7 +21,7 @@ import ( ) type SearchReq struct { - KeyWords string `json:"keyWords,omitempty"` + Keywords string `json:"keywords,omitempty"` } type Case struct {