Skip to content

Commit 36d9fb1

Browse files
authored
feat: 完成新增提案功能 (#53)
* feat: 完成新增提案功能 * fix: 复用已有的 Assembler 函数
1 parent 3c58af3 commit 36d9fb1

File tree

9 files changed

+265
-27
lines changed

9 files changed

+265
-27
lines changed

api/handler/proposal.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,18 @@ import (
2525
// CreateProposal 新建一个提案
2626
// @router /api/proposal/add [POST]
2727
func CreateProposal(c *gin.Context) {
28-
// TODO: not implemented
28+
var req dto.CreateProposalReq
29+
var resp *dto.CreateProposalResp
30+
var err error
31+
32+
if err = c.ShouldBindJSON(&req); err != nil {
33+
PostProcess(c, &req, nil, err)
34+
return
35+
}
36+
c.Set(consts.CtxUserID, token.GetUserID(c))
37+
38+
resp, err = provider.Get().ProposalService.CreateProposal(c, &req)
39+
PostProcess(c, req, resp, err)
2940
}
3041

3142
// ListProposals godoc
@@ -52,7 +63,6 @@ func ListProposals(c *gin.Context) {
5263

5364
resp, err = provider.Get().ProposalService.ListProposals(c, &req)
5465
PostProcess(c, &req, resp, err)
55-
5666
}
5767

5868
// GetProposal 获取提案详情

application/dto/proposal.go

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,28 @@ package dto
1616

1717
import "time"
1818

19+
// CreateProposalReq 新增投票请求参数
20+
type CreateProposalReq struct {
21+
Title string `json:"title" binding:"required"`
22+
Content string `json:"content" binding:"required"`
23+
Status string `json:"status" binding:"required"`
24+
Course *CourseVO `json:"course" binding:"required"`
25+
}
26+
27+
// CreateProposalResp 新增投票响应
28+
type CreateProposalResp struct {
29+
*Resp
30+
ProposalID string `json:"proposalId"` // 提案ID
31+
}
32+
1933
type ProposalVO struct {
20-
ID string `json:"id"`
21-
UserID string `json:"userId"`
22-
Title string `json:"title"`
23-
Content string `json:"content"`
24-
Course *CourseVO `json:"course"`
34+
ID string `json:"id"`
35+
UserID string `json:"userId"`
36+
Title string `json:"title"`
37+
Content string `json:"content"`
38+
Status string `json:"status"` // pending / approved / rejected
39+
Deleted bool `json:"deleted"`
40+
Course *CourseVO `json:"course"`
2541
CreatedAt time.Time `json:"createdAt"`
2642
UpdatedAt time.Time `json:"updatedAt"`
2743
}

application/service/proposal.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,27 +16,34 @@ package service
1616

1717
import (
1818
"context"
19+
"time"
1920

2021
"github.com/Boyuan-IT-Club/Meowpick-Backend/application/assembler"
2122
"github.com/Boyuan-IT-Club/Meowpick-Backend/application/dto"
2223
"github.com/Boyuan-IT-Club/Meowpick-Backend/infra/cache"
24+
"github.com/Boyuan-IT-Club/Meowpick-Backend/infra/model"
2325
"github.com/Boyuan-IT-Club/Meowpick-Backend/infra/repo"
26+
"github.com/Boyuan-IT-Club/Meowpick-Backend/infra/util/mapping"
2427
"github.com/Boyuan-IT-Club/Meowpick-Backend/types/consts"
2528
"github.com/Boyuan-IT-Club/Meowpick-Backend/types/errno"
2629

2730
"github.com/Boyuan-IT-Club/go-kit/errorx"
2831
"github.com/Boyuan-IT-Club/go-kit/logs"
2932
"github.com/google/wire"
33+
"go.mongodb.org/mongo-driver/bson/primitive"
3034
)
3135

3236
var _ IProposalService = (*ProposalService)(nil)
3337

3438
type IProposalService interface {
39+
CreateProposal(ctx context.Context, req *dto.CreateProposalReq) (*dto.CreateProposalResp, error)
3540
ToggleProposal(ctx context.Context, req *dto.ToggleProposalReq) (resp *dto.ToggleProposalResp, err error)
3641
ListProposals(ctx context.Context, req *dto.ListProposalReq) (*dto.ListProposalResp, error)
3742
}
3843

3944
type ProposalService struct {
45+
CourseRepo *repo.CourseRepo
46+
CourseAssembler *assembler.CourseAssembler
4047
ProposalRepo *repo.ProposalRepo
4148
ProposalCache *cache.ProposalCache
4249
ProposalAssembler *assembler.ProposalAssembler
@@ -47,6 +54,96 @@ var ProposalServiceSet = wire.NewSet(
4754
wire.Bind(new(IProposalService), new(*ProposalService)),
4855
)
4956

57+
// CreateProposal 添加一个新的课程提案
58+
func (s *ProposalService) CreateProposal(ctx context.Context, req *dto.CreateProposalReq) (*dto.CreateProposalResp, error) {
59+
// 鉴权
60+
userId, ok := ctx.Value(consts.CtxUserID).(string)
61+
if !ok || userId == "" {
62+
return nil, errorx.New(errno.ErrUserNotLogin)
63+
}
64+
65+
// 转换为 courseModel
66+
courseDB, err := s.CourseAssembler.ToCourseDB(ctx, req.Course)
67+
if err != nil {
68+
return nil, errorx.WrapByCode(err, errno.ErrCourseCvtFailed,
69+
errorx.KV("src", "database course"), errorx.KV("dst", "course vo"),
70+
)
71+
}
72+
73+
// 检查是否已经存在相同的提案(前端传回的和我已有的ProposalCourse)
74+
existingProposal, err := s.ProposalRepo.IsCourseInExistingProposals(ctx, courseDB)
75+
if err != nil {
76+
return nil, errorx.WrapByCode(err, errno.ErrProposalCourseFindInProposalsFailed,
77+
errorx.KV("key", consts.ReqCourse),
78+
errorx.KV("value", req.Course.Name),
79+
)
80+
}
81+
82+
// 检查是否已经存在相同的课程(前端传回的的和我已有的course)
83+
existingCourse, err := s.CourseRepo.IsCourseInExistingCourses(ctx, courseDB)
84+
if err != nil {
85+
return nil, errorx.WrapByCode(err, errno.ErrProposalCourseFindInCoursesFailed,
86+
errorx.KV("key", consts.ReqCourse),
87+
errorx.KV("value", req.Course.Name),
88+
)
89+
}
90+
91+
if existingProposal == true {
92+
return nil, errorx.New(errno.ErrProposalCourseFoundInProposals,
93+
errorx.KV("key", consts.ReqCourse),
94+
errorx.KV("value", req.Course.Name),
95+
)
96+
}
97+
if existingCourse == true {
98+
return nil, errorx.New(errno.ErrProposalCourseFoundInCourses,
99+
errorx.KV("key", consts.ReqCourse),
100+
errorx.KV("value", req.Course.Name),
101+
)
102+
}
103+
campuses := []int32{}
104+
for _, campus := range req.Course.Campuses {
105+
campuses = append(campuses, mapping.Data.GetCampusIDByName(campus))
106+
}
107+
108+
// 构造课程信息
109+
course := model.Course{
110+
Name: req.Course.Name,
111+
Code: req.Course.Code,
112+
TeacherIDs: make([]string, 0),
113+
Department: mapping.Data.GetDepartmentIDByName(req.Course.Department),
114+
Category: mapping.Data.GetCategoryIDByName(req.Course.Category),
115+
Campuses: campuses,
116+
CreatedAt: time.Now(),
117+
UpdatedAt: time.Now(),
118+
}
119+
120+
// 创建提案对象
121+
status := mapping.Data.GetProposalStatusIDByName(req.Status)
122+
proposal := &model.Proposal{
123+
ID: primitive.NewObjectID().Hex(),
124+
UserID: userId,
125+
Title: req.Title,
126+
Content: req.Content,
127+
Deleted: false,
128+
Course: &course,
129+
Status: status,
130+
CreatedAt: time.Now(),
131+
UpdatedAt: time.Now(),
132+
}
133+
134+
// 保存提案到数据库
135+
if err = s.ProposalRepo.Insert(ctx, proposal); err != nil {
136+
return nil, errorx.WrapByCode(err, errno.ErrProposalCreateFailed,
137+
errorx.KV(consts.ReqTitle, req.Title),
138+
errorx.KV(consts.CtxUserID, userId))
139+
}
140+
141+
return &dto.CreateProposalResp{
142+
Resp: dto.Success(),
143+
ProposalID: proposal.ID,
144+
}, nil
145+
}
146+
50147
// ToggleProposal 切换投票状态
51148
func (s *ProposalService) ToggleProposal(ctx context.Context, req *dto.ToggleProposalReq) (resp *dto.ToggleProposalResp, err error) {
52149

infra/repo/course.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ type ICourseRepo interface {
4646
GetCategoriesByName(ctx context.Context, name string) ([]int32, error)
4747
GetCampusesByName(ctx context.Context, name string) ([]int32, error)
4848
GetSuggestionsByName(ctx context.Context, name string, param *dto.PageParam) ([]*model.Course, error)
49+
50+
IsCourseInExistingCourses(ctx context.Context, vo *model.Course) (bool, error)
4951
}
5052

5153
type CourseRepo struct {
@@ -211,3 +213,24 @@ func (r *CourseRepo) GetSuggestionsByName(ctx context.Context, name string, para
211213
}
212214
return courses, nil
213215
}
216+
217+
// IsCourseInExistingCourses 检查课程是否已经存在于现有课程中
218+
// 比较的字段包括: Name, Code, Department, Category, Campuses, TeacherIDs
219+
func (r *CourseRepo) IsCourseInExistingCourses(ctx context.Context, vo *model.Course) (bool, error) {
220+
filter := bson.M{
221+
consts.Name: vo.Name,
222+
consts.Code: vo.Code,
223+
consts.Department: vo.Department,
224+
consts.Categories: vo.Category,
225+
consts.Campuses: vo.Campuses,
226+
consts.TeacherIDs: vo.TeacherIDs,
227+
consts.Deleted: false, // 只检查未删除的提案
228+
}
229+
230+
count, err := r.conn.CountDocuments(ctx, filter)
231+
if err != nil {
232+
return false, err
233+
}
234+
235+
return count > 0, nil
236+
}

infra/repo/proposal.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ const (
3838
)
3939

4040
type IProposalRepo interface {
41+
Insert(ctx context.Context, proposal *model.Proposal) error
42+
IsCourseInExistingProposals(ctx context.Context, courseVO *model.Course) (bool, error)
4143
FindMany(ctx context.Context, param *dto.PageParam) ([]*model.Proposal, int64, error)
4244
Toggle(ctx context.Context, userId, targetId string, targetType int32) (bool, error)
4345
IsProposal(ctx context.Context, userId, targetId string, targetType int32) (bool, error)
@@ -54,6 +56,34 @@ func NewProposalRepo(cfg *config.Config) *ProposalRepo {
5456
return &ProposalRepo{conn: conn}
5557
}
5658

59+
// Insert 插入一个新的提案
60+
func (r *ProposalRepo) Insert(ctx context.Context, proposal *model.Proposal) error {
61+
_, err := r.conn.InsertOneNoCache(ctx, proposal)
62+
return err
63+
}
64+
65+
// IsCourseInExistingProposals 检查课程是否已经存在于现有提案中
66+
// 比较的字段包括: Name, Code, Department, Category, Campuses, TeacherIDs
67+
func (r *ProposalRepo) IsCourseInExistingProposals(ctx context.Context, courseDB *model.Course) (bool, error) {
68+
filter := bson.M{
69+
consts.Name: courseDB.Name,
70+
consts.Code: courseDB.Code,
71+
consts.Department: courseDB.Department,
72+
consts.Categories: courseDB.Category,
73+
consts.Campuses: courseDB.Campuses,
74+
consts.TeacherIDs: courseDB.TeacherIDs,
75+
consts.Deleted: false, // 只检查未删除的提案
76+
}
77+
78+
// 查询提案中是否已存在该课程
79+
count, err := r.conn.CountDocuments(ctx, filter)
80+
if err != nil {
81+
return false, err
82+
}
83+
84+
return count > 0, nil
85+
}
86+
5787
// FindMany 分页查询所有未删除的提案
5888
func (r *ProposalRepo) FindMany(ctx context.Context, param *dto.PageParam) ([]*model.Proposal, int64, error) {
5989
proposals := []*model.Proposal{}

infra/util/mapping/mapping.go

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,21 +24,25 @@ import (
2424

2525
// StaticData 存放所有静态映射数据
2626
type StaticData struct {
27-
CampusNameByID map[int32]string
28-
DepartmentNameByID map[int32]string
29-
CategoryNameByID map[int32]string
30-
CampusIDByName map[string]int32
31-
DepartmentIDByName map[string]int32
32-
CategoryIDByName map[string]int32
27+
CampusNameByID map[int32]string
28+
DepartmentNameByID map[int32]string
29+
CategoryNameByID map[int32]string
30+
ProposalStatusNameByID map[int32]string
31+
CampusIDByName map[string]int32
32+
DepartmentIDByName map[string]int32
33+
CategoryIDByName map[string]int32
34+
ProposalStatusIDByName map[string]int32
3335
}
3436

3537
var Data = &StaticData{
36-
CampusNameByID: make(map[int32]string),
37-
DepartmentNameByID: make(map[int32]string),
38-
CategoryNameByID: make(map[int32]string),
39-
CampusIDByName: make(map[string]int32),
40-
DepartmentIDByName: make(map[string]int32),
41-
CategoryIDByName: make(map[string]int32),
38+
CampusNameByID: make(map[int32]string),
39+
DepartmentNameByID: make(map[int32]string),
40+
CategoryNameByID: make(map[int32]string),
41+
ProposalStatusNameByID: make(map[int32]string),
42+
CampusIDByName: make(map[string]int32),
43+
DepartmentIDByName: make(map[string]int32),
44+
CategoryIDByName: make(map[string]int32),
45+
ProposalStatusIDByName: make(map[string]int32),
4246
}
4347

4448
func init() {
@@ -51,6 +55,9 @@ func init() {
5155
for k, v := range mapping.CategoriesMap {
5256
Data.CategoryNameByID[k] = v
5357
}
58+
for k, v := range mapping.ProposalStatusMap {
59+
Data.ProposalStatusNameByID[k] = v
60+
}
5461

5562
for id, name := range Data.CampusNameByID {
5663
Data.CampusIDByName[name] = id
@@ -61,6 +68,9 @@ func init() {
6168
for id, name := range Data.CategoryNameByID {
6269
Data.CategoryIDByName[name] = id
6370
}
71+
for id, name := range Data.ProposalStatusNameByID {
72+
Data.ProposalStatusIDByName[name] = id
73+
}
6474
}
6575

6676
func (d *StaticData) GetCampusNameByID(id int32) string {
@@ -84,6 +94,13 @@ func (d *StaticData) GetCategoryNameByID(id int32) string {
8494
return "未知分类"
8595
}
8696

97+
func (d *StaticData) GetProposalStatusNameByID(id int32) string {
98+
if name, ok := d.ProposalStatusNameByID[id]; ok {
99+
return name
100+
}
101+
return "未知状态"
102+
}
103+
87104
func (d *StaticData) GetCampusIDByName(name string) int32 {
88105
if id, ok := d.CampusIDByName[name]; ok {
89106
return id
@@ -105,6 +122,13 @@ func (d *StaticData) GetCategoryIDByName(name string) int32 {
105122
return 0
106123
}
107124

125+
func (d *StaticData) GetProposalStatusIDByName(name string) int32 {
126+
if id, ok := d.ProposalStatusIDByName[name]; ok {
127+
return id
128+
}
129+
return 0
130+
}
131+
108132
// --- 搜索方法(正则+包含匹配)---
109133

110134
// GetBestCategoryIDByKeyword 根据关键词获取最匹配的单个分类ID

types/consts/consts.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ const (
8080
ReqType = "type"
8181
ReqCourseID = "courseId"
8282
ReqTargetID = "targetId"
83+
ReqTitle = "title"
8384
)
8485

8586
// 限制相关

0 commit comments

Comments
 (0)