完成举报管理

dev
truthhun 1 year ago
parent a6ac0080ec
commit 9994670465

@ -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',
};
}
}

@ -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
}

@ -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))

@ -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
}

@ -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
}

@ -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,
})
}

@ -0,0 +1,137 @@
<template>
<div class="com-form-report">
<el-form
ref="report"
label-position="top"
label-width="80px"
:model="report"
>
<el-form-item label="文档">
<el-input v-model="report.document_title" :disabled="true"></el-input>
</el-form-item>
<el-form-item label="举报原因">
<el-radio-group v-model="report.reason" class="report-reason">
<el-row>
<el-col
:span="8"
v-for="item in reportOptions"
:key="'rs' + item.value"
>
<el-radio :label="item.value">{{ item.label }}</el-radio>
</el-col>
</el-row>
</el-radio-group>
</el-form-item>
<template v-if="isAdmin">
<el-form-item label="处理状态">
<el-switch
v-model="report.status"
active-text="已处理"
inactive-text="未处理"
>
</el-switch>
</el-form-item>
<el-form-item label="处理备注">
<el-input
v-model="report.remark"
placeholder="请输入文档处理相关备注"
type="textarea"
rows="3"
></el-input>
</el-form-item>
</template>
<el-form-item>
<el-button
type="primary"
icon="el-icon-check"
class="btn-block"
@click="setReport"
>提交</el-button
>
</el-form-item>
</el-form>
</div>
</template>
<script>
import { createReport, updateReport } from '~/api/report'
import { reportOptions } from '~/utils/enum'
export default {
name: 'FormReport',
props: {
isAdmin: {
type: Boolean,
default: false,
},
initReport: {
type: Object,
default: () => {
return {
id: 0,
report_id: 0,
document_id: 0,
}
},
},
},
data() {
return {
report: { id: 0, status: 0 },
reportOptions,
statusOptions: [
{ label: '未处理', value: 0 },
{ label: '已处理', value: 1 },
],
}
},
watch: {
initReport: {
handler(val) {
this.report = { status: 0, ...val }
},
immediate: true,
},
},
created() {
this.report = this.initReport
},
methods: {
async setReport() {
if (this.report.id > 0) {
const res = await updateReport(this.report)
if (res.status === 200) {
this.$message.success('更新成功')
this.$emit('success')
} else {
this.$message.error(res.data.message)
}
} else {
const res = await createReport(this.report)
if (res.status === 200) {
this.$message.success('提交成功')
this.$emit('success')
} else {
this.$message.error(res.data.message)
}
}
},
reset() {
this.report = { id: 0 }
this.$refs.report.resetFields()
this.$refs.report.clearValidate()
},
},
}
</script>
<style lang="scss">
.com-form-report {
.el-select {
width: 100%;
}
.report-reason {
width: 100%;
.el-radio {
margin-bottom: 10px;
}
}
}
</style>

@ -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: '附件管理',

@ -0,0 +1,236 @@
<template>
<div>
<el-card shadow="never" class="search-card">
<FormSearch
:fields="searchFormFields"
:loading="loading"
:show-create="false"
:show-delete="true"
:disabled-delete="selectedRow.length === 0"
@onSearch="onSearch"
@onCreate="onCreate"
@onDelete="batchDelete"
/>
</el-card>
<el-card shadow="never" class="mgt-20px">
<TableList
:loading="loading"
:table-data="reports"
:fields="tableListFields"
:show-actions="true"
:show-view="false"
:show-edit="true"
:show-delete="true"
:show-select="true"
@selectRow="selectRow"
@editRow="editRow"
@deleteRow="deleteRow"
/>
</el-card>
<el-card shadow="never" class="mgt-20px">
<div class="text-right">
<el-pagination
background
:current-page="search.page"
:page-sizes="[10, 20, 50, 100]"
:page-size="search.size"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handlePageChange"
>
</el-pagination>
</div>
</el-card>
<el-dialog
:title="report.id ? '编辑举报' : '新增举报'"
:visible.sync="formReportVisible"
width="640px"
>
<FormReport
ref="reportForm"
:init-report="report"
:is-admin="true"
@success="formReportSuccess"
/>
</el-dialog>
</div>
</template>
<script>
import { listReport, deleteReport } from '~/api/report'
import { reportOptions } from '~/utils/enum'
import TableList from '~/components/TableList.vue'
import FormSearch from '~/components/FormSearch.vue'
import FormReport from '~/components/FormReport.vue'
export default {
components: { TableList, FormSearch, FormReport },
layout: 'admin',
data() {
return {
loading: false,
formReportVisible: false,
search: {
wd: '',
page: 1,
status: [],
size: 10,
},
reports: [],
reportOptions,
total: 0,
searchFormFields: [],
tableListFields: [],
selectedRow: [],
report: { id: 0 },
}
},
async created() {
this.initSearchForm()
this.initTableListFields()
await this.listReport()
},
methods: {
async listReport() {
this.loading = true
const res = await listReport(this.search)
if (res.status === 200) {
this.reports = res.data.report
this.total = res.data.total
} else {
this.$message.error(res.data.message)
}
this.loading = false
},
handleSizeChange(val) {
this.search.size = val
this.listReport()
},
handlePageChange(val) {
this.search.page = val
this.listReport()
},
onSearch(search) {
this.search = { ...this.search, page: 1, ...search }
this.listReport()
},
onCreate() {
this.report = { id: 0 }
this.formReportVisible = true
this.$nextTick(() => {
this.$refs.reportForm.reset()
})
},
async editRow(row) {
this.report = row
this.formReportVisible = true
},
formReportSuccess() {
this.formReportVisible = false
this.listReport()
},
batchDelete() {
this.$confirm(
`您确定要删除选中的【${this.selectedRow.length}条】举报吗?删除之后不可恢复!`,
'温馨提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
)
.then(async () => {
const ids = this.selectedRow.map((item) => item.id)
const res = await deleteReport({ id: ids })
if (res.status === 200) {
this.$message.success('删除成功')
this.listReport()
} else {
this.$message.error(res.data.message)
}
})
.catch(() => {})
},
deleteRow(row) {
this.$confirm(
`您确定要删除对文档【${row.document_title}】的举报吗?删除之后不可恢复!`,
'温馨提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
)
.then(async () => {
const res = await deleteReport({ id: row.id })
if (res.status === 200) {
this.$message.success('删除成功')
this.listReport()
} else {
this.$message.error(res.data.message)
}
})
.catch(() => {})
},
selectRow(rows) {
this.selectedRow = rows
},
initSearchForm() {
this.searchFormFields = [
{
type: 'text',
label: '关键字',
name: 'wd',
placeholder: '请输入关键字',
},
{
type: 'select',
label: '状态',
name: 'enable',
placeholder: '请选择状态',
multiple: true,
options: [
{ label: '启用', value: 1 },
{ label: '禁用', value: 0 },
],
},
]
},
initTableListFields() {
const reasonEnum = {}
this.reportOptions.forEach((item) => {
reasonEnum[item.value] = item
})
this.tableListFields = [
{ prop: 'id', label: 'ID', width: 80, type: 'number', fixed: 'left' },
{
prop: 'status',
label: '是否已处理',
width: 100,
type: 'bool',
fixed: 'left',
},
{ prop: 'document_title', label: '名称', minWidth: 150, fixed: 'left' },
{
prop: 'reason',
label: '举报原因',
width: 80,
type: 'enum',
enum: reasonEnum,
},
{
prop: 'username',
label: '举报人',
width: 100,
},
{ prop: 'remark', label: '处理描述', minWidth: 150 },
{ prop: 'created_at', label: '举报时间', width: 160, type: 'datetime' },
{ prop: 'updated_at', label: '更新时间', width: 160, type: 'datetime' },
]
},
},
}
</script>
<style></style>

@ -104,7 +104,11 @@
上传分享
</div>
<div class="btn-actions">
<el-button type="primary" plain icon="el-icon-warning-outline"
<el-button
type="primary"
@click="showReport"
plain
icon="el-icon-warning-outline"
>举报</el-button
>
<el-button
@ -238,6 +242,14 @@
</el-row>
</el-card>
</div>
<el-dialog title="举报文档" :visible.sync="reportVisible" width="520px">
<FormReport
ref="reportForm"
:init-report="report"
:is-admin="false"
@success="formReportSuccess"
/>
</el-dialog>
</div>
</template>
@ -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

@ -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',

@ -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',

Loading…
Cancel
Save