From 9994670465ccd6f7ab53824f8b36b2f1d2fad870 Mon Sep 17 00:00:00 2001 From: truthhun <1272881215@qq.com> Date: Tue, 3 Jan 2023 17:20:44 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90=E4=B8=BE=E6=8A=A5=E7=AE=A1?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/v1/report.proto | 71 ++++++++ biz/report.go | 123 ++++++++++++++ model/init.go | 1 + model/report.go | 134 +++++++++++++++ service/serve/registerGRPCService.go | 9 + web/api/report.js | 35 ++++ web/components/FormReport.vue | 137 ++++++++++++++++ web/layouts/admin.vue | 5 + web/pages/admin/report.vue | 236 +++++++++++++++++++++++++++ web/pages/document/_id.vue | 28 +++- web/utils/enum.js | 9 + web/utils/permission.js | 6 + 12 files changed, 793 insertions(+), 1 deletion(-) create mode 100644 api/v1/report.proto create mode 100644 biz/report.go create mode 100644 model/report.go create mode 100644 web/api/report.js create mode 100644 web/components/FormReport.vue create mode 100644 web/pages/admin/report.vue diff --git a/api/v1/report.proto b/api/v1/report.proto new file mode 100644 index 0000000..0dff240 --- /dev/null +++ b/api/v1/report.proto @@ -0,0 +1,71 @@ +syntax = "proto3"; + +import "google/protobuf/timestamp.proto"; +import "gogoproto/gogo.proto"; +// import "validate/validate.proto"; +import "google/api/annotations.proto"; +import "google/protobuf/empty.proto"; + +package api.v1; + +option go_package = "moredoc/api/v1;v1"; +option java_multiple_files = true; +option java_package = "api.v1"; + +// 这里是proto文件中的结构体,可以根据需要删除或者调整 +message Report { + int64 id = 1; + int64 document_id = 2; + int64 user_id = 3; + int32 reason = 4; + bool status = 5; + google.protobuf.Timestamp created_at = 6 [ (gogoproto.stdtime) = true ]; + google.protobuf.Timestamp updated_at = 7 [ (gogoproto.stdtime) = true ]; + string document_title = 8; + string remark = 9; + string username = 10; +} + +message DeleteReportRequest { repeated int64 id = 1; } + +message ListReportRequest { + int64 page = 1; + int64 size = 2; + string wd = 3; + repeated string field = 4; + string order = 5; + repeated bool status = 6; +} + +message ListReportReply { + int64 total = 1; + repeated Report report = 2; +} + +service ReportAPI { + rpc CreateReport(Report) returns (google.protobuf.Empty) { + option (google.api.http) = { + post : '/api/v1/report', + body : '*', + }; + } + + rpc UpdateReport(Report) returns (google.protobuf.Empty) { + option (google.api.http) = { + put : '/api/v1/report', + body : '*', + }; + } + + rpc DeleteReport(DeleteReportRequest) returns (google.protobuf.Empty) { + option (google.api.http) = { + delete : '/api/v1/report', + }; + } + + rpc ListReport(ListReportRequest) returns (ListReportReply) { + option (google.api.http) = { + get : '/api/v1/report/list', + }; + } +} \ No newline at end of file diff --git a/biz/report.go b/biz/report.go new file mode 100644 index 0000000..c5fb3ce --- /dev/null +++ b/biz/report.go @@ -0,0 +1,123 @@ +package biz + +import ( + "context" + + pb "moredoc/api/v1" + "moredoc/middleware/auth" + "moredoc/model" + "moredoc/util" + + "go.uber.org/zap" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/emptypb" + "gorm.io/gorm" +) + +type ReportAPIService struct { + pb.UnimplementedReportAPIServer + dbModel *model.DBModel + logger *zap.Logger +} + +func NewReportAPIService(dbModel *model.DBModel, logger *zap.Logger) (service *ReportAPIService) { + return &ReportAPIService{dbModel: dbModel, logger: logger.Named("ReportAPIService")} +} + +func (s *ReportAPIService) checkLogin(ctx context.Context) (*auth.UserClaims, error) { + return checkGRPCLogin(s.dbModel, ctx) +} + +func (s *ReportAPIService) checkPermission(ctx context.Context) (*auth.UserClaims, error) { + return checkGRPCPermission(s.dbModel, ctx) +} + +func (s *ReportAPIService) CreateReport(ctx context.Context, req *pb.Report) (*emptypb.Empty, error) { + UserClaims, err := s.checkLogin(ctx) + if err != nil { + return nil, err + } + + if req.DocumentId == 0 { + return nil, status.Error(codes.InvalidArgument, "文档参数不正确") + } + + report, _ := s.dbModel.GetReportByDocUser(req.DocumentId, UserClaims.UserId) + if report.Id > 0 { + return nil, status.Error(codes.AlreadyExists, "您已举报过当前文档") + } + + util.CopyStruct(req, &report) + report.UserId = UserClaims.UserId + err = s.dbModel.CreateReport(&report) + if err != nil { + return nil, status.Error(codes.Internal, "创建举报失败") + } + + return &emptypb.Empty{}, nil +} + +func (s *ReportAPIService) UpdateReport(ctx context.Context, req *pb.Report) (*emptypb.Empty, error) { + _, err := s.checkPermission(ctx) + if err != nil { + return nil, err + } + + report := &model.Report{} + util.CopyStruct(req, report) + err = s.dbModel.UpdateReport(report, "status", "remark") + if err != nil { + return nil, status.Error(codes.Internal, "更新举报失败") + } + + return &emptypb.Empty{}, nil +} + +func (s *ReportAPIService) DeleteReport(ctx context.Context, req *pb.DeleteReportRequest) (*emptypb.Empty, error) { + _, err := s.checkPermission(ctx) + if err != nil { + return nil, err + } + + err = s.dbModel.DeleteReport(req.Id) + if err != nil { + return nil, status.Error(codes.Internal, "删除举报失败") + } + + return &emptypb.Empty{}, nil +} + +func (s *ReportAPIService) ListReport(ctx context.Context, req *pb.ListReportRequest) (*pb.ListReportReply, error) { + _, err := s.checkPermission(ctx) + if err != nil { + return nil, err + } + + opt := &model.OptionGetReportList{ + WithCount: true, + Page: int(req.Page), + Size: int(req.Size_), + QueryLike: make(map[string][]interface{}), + QueryIn: make(map[string][]interface{}), + } + + if req.Wd != "" { + opt.QueryLike["document_title"] = []interface{}{req.Wd} + } + + if len(req.Status) > 0 { + opt.QueryIn["status"] = util.Slice2Interface(req.Status) + } + + reports, total, err := s.dbModel.GetReportList(opt) + if err != nil && err != gorm.ErrRecordNotFound { + return nil, status.Error(codes.Internal, "获取举报列表失败") + } + + pbReport := &pb.ListReportReply{ + Total: total, + Report: reports, + } + return pbReport, nil +} diff --git a/model/init.go b/model/init.go index 7735560..6b0e377 100644 --- a/model/init.go +++ b/model/init.go @@ -174,6 +174,7 @@ func (m *DBModel) SyncDB() (err error) { &Comment{}, &Dynamic{}, &Sign{}, + &Report{}, } if err = m.db.AutoMigrate(tableModels...); err != nil { m.logger.Fatal("SyncDB", zap.Error(err)) diff --git a/model/report.go b/model/report.go new file mode 100644 index 0000000..1d773c4 --- /dev/null +++ b/model/report.go @@ -0,0 +1,134 @@ +package model + +import ( + v1 "moredoc/api/v1" + "time" + + "go.uber.org/zap" + "gorm.io/gorm" +) + +type Report struct { + Id int64 `form:"id" json:"id,omitempty" gorm:"primaryKey;autoIncrement;column:id;comment:;"` + DocumentId int64 `form:"document_id" json:"document_id,omitempty" gorm:"column:document_id;type:bigint(20);size:20;default:0;comment:文档ID;index:idx_document_id;"` + DocumentTitle string `form:"document_title" json:"document_title,omitempty" gorm:"column:document_title;type:varchar(255);size:255;default:'';comment:文档标题;"` + UserId int64 `form:"user_id" json:"user_id,omitempty" gorm:"column:user_id;type:bigint(20);size:20;default:0;comment:用户ID;index:idx_user_id;"` + Username string `form:"username" json:"username,omitempty" gorm:"column:username;type:varchar(64);size:64;default:'';comment:用户名;"` + Reason int `form:"reason" json:"reason,omitempty" gorm:"column:reason;type:int(11);size:11;default:0;comment:举报原因;"` + Status bool `form:"status" json:"status,omitempty" gorm:"column:status;type:tinyint(4);size:4;default:0;comment:是否已处理;index:idx_status;"` + Remark string `form:"remark" json:"remark,omitempty" gorm:"column:remark;type:varchar(255);size:255;default:'';comment:备注;"` + CreatedAt *time.Time `form:"created_at" json:"created_at,omitempty" gorm:"column:created_at;type:datetime;comment:创建时间;"` + UpdatedAt *time.Time `form:"updated_at" json:"updated_at,omitempty" gorm:"column:updated_at;type:datetime;comment:更新时间;"` +} + +func (Report) TableName() string { + return tablePrefix + "report" +} + +// CreateReport 创建Report +func (m *DBModel) CreateReport(report *Report) (err error) { + doc, _ := m.GetDocument(report.DocumentId, "id", "title") + report.DocumentTitle = doc.Title + user, _ := m.GetUser(report.UserId, "id", "username") + report.Username = user.Username + err = m.db.Create(report).Error + if err != nil { + m.logger.Error("CreateReport", zap.Error(err)) + return + } + return +} + +// UpdateReport 更新Report,如果需要更新指定字段,则请指定updateFields参数 +func (m *DBModel) UpdateReport(report *Report, updateFields ...string) (err error) { + db := m.db.Model(report) + tableName := Report{}.TableName() + + updateFields = m.FilterValidFields(tableName, updateFields...) + if len(updateFields) > 0 { // 更新指定字段 + db = db.Select(updateFields) + } else { // 更新全部字段,包括零值字段 + db = db.Select(m.GetTableFields(tableName)) + } + + err = db.Where("id = ?", report.Id).Updates(report).Error + if err != nil { + m.logger.Error("UpdateReport", zap.Error(err)) + } + return +} + +// GetReport 根据id获取Report +func (m *DBModel) GetReport(id interface{}, fields ...string) (report Report, err error) { + db := m.db + + fields = m.FilterValidFields(Report{}.TableName(), fields...) + if len(fields) > 0 { + db = db.Select(fields) + } + + err = db.Where("id = ?", id).First(&report).Error + return +} + +type OptionGetReportList struct { + Page int + Size int + WithCount bool // 是否返回总数 + Ids []interface{} // id列表 + SelectFields []string // 查询字段 + QueryRange map[string][2]interface{} // map[field][]{min,max} + QueryIn map[string][]interface{} // map[field][]{value1,value2,...} + QueryLike map[string][]interface{} // map[field][]{value1,value2,...} + Sort []string +} + +// GetReportList 获取Report列表 +func (m *DBModel) GetReportList(opt *OptionGetReportList) (reportList []*v1.Report, total int64, err error) { + tableName := Report{}.TableName() + db := m.db.Model(&Report{}) + db = m.generateQueryRange(db, tableName, opt.QueryRange) + db = m.generateQueryIn(db, tableName, opt.QueryIn) + db = m.generateQueryLike(db, tableName, opt.QueryLike) + + if len(opt.Ids) > 0 { + db = db.Where("id in (?)", opt.Ids) + } + + if opt.WithCount { + err = db.Count(&total).Error + if err != nil { + m.logger.Error("GetReportList", zap.Error(err)) + return + } + } + + opt.SelectFields = m.FilterValidFields(tableName, opt.SelectFields...) + if len(opt.SelectFields) > 0 { + db = db.Select(opt.SelectFields) + } + + db = m.generateQuerySort(db, tableName, opt.Sort) + + db = db.Offset((opt.Page - 1) * opt.Size).Limit(opt.Size) + + err = db.Find(&reportList).Error + if err != nil && err != gorm.ErrRecordNotFound { + m.logger.Error("GetReportList", zap.Error(err)) + } + return +} + +// DeleteReport 删除数据 +func (m *DBModel) DeleteReport(ids []int64) (err error) { + err = m.db.Where("id in (?)", ids).Delete(&Report{}).Error + if err != nil { + m.logger.Error("DeleteReport", zap.Error(err)) + } + return +} + +func (m *DBModel) GetReportByDocUser(docId, userId int64) (report Report, err error) { + err = m.db.Where("doc_id = ? and user_id = ?", docId, userId).First(&report).Error + return +} diff --git a/service/serve/registerGRPCService.go b/service/serve/registerGRPCService.go index 7909764..0b1d374 100644 --- a/service/serve/registerGRPCService.go +++ b/service/serve/registerGRPCService.go @@ -130,5 +130,14 @@ func RegisterGRPCService(dbModel *model.DBModel, logger *zap.Logger, endpoint st return } + // 举报服务 + reportAPIService := biz.NewReportAPIService(dbModel, logger) + v1.RegisterReportAPIServer(grpcServer, reportAPIService) + err = v1.RegisterReportAPIHandlerFromEndpoint(context.Background(), gwmux, endpoint, dialOpts) + if err != nil { + logger.Error("RegisterReportAPIHandlerFromEndpoint", zap.Error(err)) + return + } + return } diff --git a/web/api/report.js b/web/api/report.js new file mode 100644 index 0000000..650f125 --- /dev/null +++ b/web/api/report.js @@ -0,0 +1,35 @@ +import service from '~/utils/request' + +export const createReport = (data) => { + return service({ + url: '/api/v1/report', + method: 'post', + data, + }) +} + +export const updateReport = (data) => { + return service({ + url: '/api/v1/report', + method: 'put', + data, + }) +} + +export const deleteReport = (params) => { + return service({ + url: '/api/v1/report', + method: 'delete', + params, + }) +} + +export const listReport = (params) => { + return service({ + url: '/api/v1/report/list', + method: 'get', + params, + }) +} + + diff --git a/web/components/FormReport.vue b/web/components/FormReport.vue new file mode 100644 index 0000000..e2961da --- /dev/null +++ b/web/components/FormReport.vue @@ -0,0 +1,137 @@ + + + diff --git a/web/layouts/admin.vue b/web/layouts/admin.vue index 5ee87e4..8cf3754 100644 --- a/web/layouts/admin.vue +++ b/web/layouts/admin.vue @@ -177,6 +177,11 @@ export default { title: '评论管理', icon: 'el-icon-chat-dot-square', }, + { + page: '/admin/report', + title: '举报管理', + icon: 'el-icon-warning-outline', + }, { page: '/admin/attachment', title: '附件管理', diff --git a/web/pages/admin/report.vue b/web/pages/admin/report.vue new file mode 100644 index 0000000..883aa22 --- /dev/null +++ b/web/pages/admin/report.vue @@ -0,0 +1,236 @@ + + + + diff --git a/web/pages/document/_id.vue b/web/pages/document/_id.vue index 0e8e17c..e03d6e5 100644 --- a/web/pages/document/_id.vue +++ b/web/pages/document/_id.vue @@ -104,7 +104,11 @@ 上传分享
- 举报
+ + + @@ -280,6 +292,12 @@ export default { }, scaleSpan: 18, loadingImage: '/static/images/loading.svg', + reportVisible: false, + report: { + document_id: 0, + document_title: '', + reason: 1, + }, } }, head() { @@ -366,6 +384,14 @@ export default { this.$router.replace('/404') } }, + showReport() { + this.report.document_id = this.document.id + this.report.document_title = this.document.title + this.reportVisible = true + }, + formReportSuccess() { + this.reportVisible = false + }, handleScroll() { const scrollTop = document.documentElement.scrollTop || document.body.scrollTop diff --git a/web/utils/enum.js b/web/utils/enum.js index dd4eb72..a29b587 100644 --- a/web/utils/enum.js +++ b/web/utils/enum.js @@ -37,6 +37,15 @@ export const boolOptions = [ { label: '否', value: false, type: 'danger' }, ] +export const reportOptions = [ + { label: '垃圾广告', value: 1 }, + { label: '淫秽色情', value: 2 }, + { label: '虚假中奖', value: 3 }, + { label: '敏感信息', value: 4 }, + { label: '人身攻击', value: 5 }, + { label: '骚扰他人', value: 6 }, +] + export const methodOptions = [ { label: 'GET', diff --git a/web/utils/permission.js b/web/utils/permission.js index 77f0a83..9f3e240 100644 --- a/web/utils/permission.js +++ b/web/utils/permission.js @@ -37,6 +37,12 @@ const cumstomPermissionMap = { children: [], pages: ['/admin/attachment'], }, + 'api.v1.ReportAPI': { + label: '举报管理', + path: 'ListReport', + children: [], + pages: ['/admin/report'], + }, 'api.v1.BannerAPI': { label: '横幅管理', path: 'ListBanner',