完成管理后台评论审核

dev
truthhun 1 year ago
parent 99ca85c0dc
commit 1e87845534

@ -0,0 +1,113 @@
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";
import "api/v1/user.proto";
package api.v1;
option go_package = "moredoc/api/v1;v1";
option java_multiple_files = true;
option java_package = "api.v1";
message Comment{
google.protobuf.Timestamp created_at = 1 [ (gogoproto.stdtime) = true ];
google.protobuf.Timestamp updated_at = 2 [ (gogoproto.stdtime) = true ];
int64 id = 3;
int64 parent_id = 4;
string content = 5;
int64 document_id = 6;
int32 status = 7;
int32 comment_count = 8;
int64 user_id = 9;
User user = 10;
string document_title=11;
}
message CheckCommentRequest {
repeated int64 id = 1;
int32 status = 2;
}
message DeleteCommentRequest {
repeated int64 id = 1;
}
message GetCommentRequest {
int64 id = 1;
}
message ListCommentRequest {
int64 page = 1;
int64 size = 2;
string wd = 3;
repeated string field = 4;
string order = 5;
repeated int32 status = 6;
int64 document_id = 7;
int64 user_id = 8;
repeated int64 parent_id = 9;
bool with_document_title = 10;
}
message ListCommentReply {
int64 total = 1;
repeated Comment comment = 2;
}
message CreateCommentRequest{
int64 document_id = 1;
int64 parent_id = 2;
string content = 3;
string captcha_id = 4;
string captcha = 5;
}
service CommentAPI{
rpc CreateComment (CreateCommentRequest) returns (google.protobuf.Empty) {
option (google.api.http) = {
post: '/api/v1/comment',
body: '*',
};
}
//
rpc UpdateComment (Comment) returns (google.protobuf.Empty){
option (google.api.http) = {
put: '/api/v1/comment',
body: '*',
};
}
//
rpc DeleteComment (DeleteCommentRequest) returns (google.protobuf.Empty){
option (google.api.http) = {
delete: '/api/v1/comment',
};
}
//
rpc GetComment (GetCommentRequest) returns (Comment){
option (google.api.http) = {
get: '/api/v1/comment',
};
}
//
rpc ListComment (ListCommentRequest) returns (ListCommentReply){
option (google.api.http) = {
get: '/api/v1/comment/list',
};
}
//
rpc CheckComment (CheckCommentRequest) returns (google.protobuf.Empty){
option (google.api.http) = {
post: '/api/v1/comment/check',
body: '*',
};
}
}

@ -22,6 +22,7 @@ message Group {
int32 user_count = 7;
int32 sort = 8;
bool enable_upload = 11;
bool enable_comment_approval = 12;
google.protobuf.Timestamp created_at = 9 [ (gogoproto.stdtime) = true ];
google.protobuf.Timestamp updated_at = 10 [ (gogoproto.stdtime) = true ];
}

@ -0,0 +1,264 @@
package biz
import (
"context"
pb "moredoc/api/v1"
"moredoc/middleware/auth"
"moredoc/model"
"moredoc/util"
"moredoc/util/captcha"
"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 CommentAPIService struct {
pb.UnimplementedCommentAPIServer
dbModel *model.DBModel
logger *zap.Logger
}
func NewCommentAPIService(dbModel *model.DBModel, logger *zap.Logger) (service *CommentAPIService) {
return &CommentAPIService{dbModel: dbModel, logger: logger.Named("CommentAPIService")}
}
func (s *CommentAPIService) checkLogin(ctx context.Context) (*auth.UserClaims, error) {
return checkGRPCLogin(s.dbModel, ctx)
}
func (s *CommentAPIService) checkPermission(ctx context.Context) (*auth.UserClaims, error) {
return checkGRPCPermission(s.dbModel, ctx)
}
// 发表评论
func (s *CommentAPIService) CreateComment(ctx context.Context, req *pb.CreateCommentRequest) (*emptypb.Empty, error) {
userClaims, err := checkGRPCLogin(s.dbModel, ctx)
if err != nil {
return nil, err
}
// 评论验证码错误
cfg := s.dbModel.GetConfigOfSecurity(model.ConfigSecurityEnableCaptchaComment)
if cfg.EnableCaptchaComment && !captcha.VerifyCaptcha(req.CaptchaId, req.Captcha) {
return nil, status.Errorf(codes.InvalidArgument, "验证码错误")
}
comment := &model.Comment{}
err = util.CopyStruct(req, comment)
if err != nil {
s.logger.Error("CreateDocument", zap.Error(err))
return nil, status.Errorf(codes.Internal, "发布评论失败:"+err.Error())
}
if comment.DocumentId <= 0 {
return nil, status.Errorf(codes.InvalidArgument, "文档id不能为空")
}
if comment.Content == "" {
return nil, status.Errorf(codes.InvalidArgument, "评论内容不能为空")
}
defaultStatus, err := s.dbModel.CanIPublishComment(userClaims.UserId)
if err != nil {
return nil, status.Error(codes.PermissionDenied, err.Error())
}
comment.Status = defaultStatus
comment.UserId = userClaims.UserId
err = s.dbModel.CreateComment(comment)
if err != nil {
s.logger.Error("CreateComment", zap.Error(err))
return nil, status.Errorf(codes.Internal, "发布评论失败:"+err.Error())
}
return &emptypb.Empty{}, nil
}
// 更新评论,仅限管理员
func (s *CommentAPIService) UpdateComment(ctx context.Context, req *pb.Comment) (*emptypb.Empty, error) {
userClaims, err := s.checkPermission(ctx)
if err != nil {
return nil, err
}
s.logger.Debug("UpdateComment", zap.Any("user", userClaims), zap.Any("req", req))
// 只允许更新评论状态和内容
updateFields := []string{"content", "status"}
comment := &model.Comment{}
util.CopyStruct(req, comment)
err = s.dbModel.UpdateComment(comment, updateFields...)
if err != nil {
return nil, status.Errorf(codes.Internal, "更新评论失败")
}
return &emptypb.Empty{}, nil
}
// 登录的用户,可删除自己的评论
// 管理员,可删除任意评论
func (s *CommentAPIService) DeleteComment(ctx context.Context, req *pb.DeleteCommentRequest) (*emptypb.Empty, error) {
var userIds []int64
userClaims, err := s.checkPermission(ctx)
if err != nil && userClaims == nil {
// 未登录用户
return nil, err
}
// 已登录用户,判断有管理员权限
isAdmin := userClaims.UserId > 0 && err == nil
if !isAdmin {
// 非管理员,限定只能删除自己的评论
userIds = append(userIds, userClaims.UserId)
}
err = s.dbModel.DeleteComment(req.Id, userIds...)
if err != nil {
return nil, status.Errorf(codes.Internal, "删除评论失败")
}
return &emptypb.Empty{}, nil
}
func (s *CommentAPIService) GetComment(ctx context.Context, req *pb.GetCommentRequest) (*pb.Comment, error) {
_, err := s.checkPermission(ctx)
if err != nil {
return nil, err
}
s.logger.Debug("GetComment", zap.Any("req", req))
comment, err := s.dbModel.GetComment(req.Id)
if err != nil {
return nil, status.Errorf(codes.Internal, "获取评论失败:"+err.Error())
}
user, _ := s.dbModel.GetUser(comment.UserId, model.UserPublicFields...)
pbComment := &pb.Comment{}
util.CopyStruct(comment, pbComment)
util.CopyStruct(user, pbComment.User)
return pbComment, nil
}
func (s *CommentAPIService) ListComment(ctx context.Context, req *pb.ListCommentRequest) (*pb.ListCommentReply, error) {
var (
isLogin bool
isAdmin bool
)
userClaims, err := s.checkPermission(ctx)
if err != nil && userClaims == nil {
// 未登录用户
isLogin = false
} else {
// 已登录用户,判断有管理员权限
isLogin = true
isAdmin = userClaims.UserId > 0 && err == nil
}
opt := &model.OptionGetCommentList{
Page: int(req.Page),
Size: int(req.Size_),
WithCount: true,
QueryIn: make(map[string][]interface{}),
QueryLike: make(map[string][]interface{}),
Sort: []string{req.Order},
}
// default status
opt.QueryIn["status"] = []interface{}{model.CommentStatusApproved}
if req.DocumentId > 0 {
opt.QueryIn["document_id"] = []interface{}{req.DocumentId}
opt.Page = 1
opt.Size = 1000000
}
if len(req.ParentId) > 0 {
opt.QueryIn["parent_id"] = util.Slice2Interface(req.ParentId)
}
if isAdmin && req.Wd != "" {
opt.QueryLike["content"] = []interface{}{req.Wd}
}
if (isLogin && req.UserId == userClaims.UserId) || isAdmin {
delete(opt.QueryIn, "status")
if len(req.Status) > 0 {
opt.QueryIn["status"] = util.Slice2Interface(req.Status)
}
}
if req.UserId > 0 {
opt.QueryIn["user_id"] = []interface{}{req.UserId}
}
comments, total, err := s.dbModel.GetCommentList(opt)
if err != nil && err != gorm.ErrRecordNotFound {
return nil, status.Errorf(codes.Internal, "获取评论列表失败")
}
resp := &pb.ListCommentReply{
Total: total,
}
util.CopyStruct(comments, &resp.Comment)
var (
userIds []int64
documentIds []int64
userIdMapCommentIdx = make(map[int64][]int)
documentIdMapCommentIdx = make(map[int64][]int)
)
for idx, comment := range comments {
userIds = append(userIds, comment.UserId)
documentIds = append(documentIds, comment.DocumentId)
userIdMapCommentIdx[comment.UserId] = append(userIdMapCommentIdx[comment.UserId], idx)
documentIdMapCommentIdx[comment.DocumentId] = append(documentIdMapCommentIdx[comment.DocumentId], idx)
}
if len(userIds) > 0 {
users, _, _ := s.dbModel.GetUserList(&model.OptionGetUserList{
SelectFields: model.UserPublicFields,
WithCount: false,
QueryIn: map[string][]interface{}{"id": util.Slice2Interface(userIds)},
})
for _, user := range users {
indexes := userIdMapCommentIdx[user.Id]
for _, idx := range indexes {
util.CopyStruct(user, &resp.Comment[idx].User)
}
}
}
if len(documentIds) > 0 {
documents, _, _ := s.dbModel.GetDocumentList(&model.OptionGetDocumentList{
SelectFields: []string{"id", "title"},
WithCount: false,
QueryIn: map[string][]interface{}{"id": util.Slice2Interface(documentIds)},
})
for _, document := range documents {
indexes := documentIdMapCommentIdx[document.Id]
for _, idx := range indexes {
resp.Comment[idx].DocumentTitle = document.Title
}
}
}
return resp, nil
}
func (s *CommentAPIService) CheckComment(ctx context.Context, req *pb.CheckCommentRequest) (*emptypb.Empty, error) {
_, err := s.checkPermission(ctx)
if err != nil {
return nil, err
}
err = s.dbModel.UpdateCommentStatus(req.Id, req.Status)
if err != nil {
return nil, status.Errorf(codes.Internal, err.Error())
}
return &emptypb.Empty{}, nil
}

@ -0,0 +1,227 @@
package model
import (
// "fmt"
// "strings"
"strings"
"time"
"go.uber.org/zap"
"gorm.io/gorm"
)
const (
CommentStatusPending = iota // 待审核
CommentStatusApproved // 已审核
CommentStatusRejected // 已拒绝
)
type Comment struct {
Id int64 `form:"id" json:"id,omitempty" gorm:"primaryKey;autoIncrement;column:id;comment:;"`
UserId int64 `form:"user_id" json:"user_id,omitempty" gorm:"column:user_id;type:bigint(20);size:20;index:idx_user_id;comment:发布评论的用户;"`
ParentId int64 `form:"parent_id" json:"parent_id,omitempty" gorm:"column:parent_id;type:bigint(20);size:20;default:0;comment:上级ID;index:idx_parent_id;"`
Content string `form:"content" json:"content,omitempty" gorm:"column:content;type:text;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;"`
Status int8 `form:"status" json:"status,omitempty" gorm:"column:status;type:tinyint(4);size:4;default:0;comment:0 待审1过审2拒绝;"`
CommentCount int `form:"comment_count" json:"comment_count,omitempty" gorm:"column:comment_count;type:int(11);size:11;default:0;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 (Comment) TableName() string {
return tablePrefix + "comment"
}
// CreateComment 创建Comment
func (m *DBModel) CreateComment(comment *Comment) (err error) {
tx := m.db.Begin()
defer func() {
if err != nil {
tx.Rollback()
} else {
tx.Commit()
}
}()
err = tx.Create(comment).Error
if err != nil {
m.logger.Error("CreateComment", zap.Error(err))
return
}
// 文档评论数+1
err = tx.Model(&Document{}).Where("id = ?", comment.DocumentId).Update("comment_count", gorm.Expr("comment_count + ?", 1)).Error
if err != nil {
m.logger.Error("CreateComment", zap.Error(err))
return
}
// 用户评论数+1
err = tx.Model(&User{}).Where("id = ?", comment.UserId).Update("comment_count", gorm.Expr("comment_count + ?", 1)).Error
if err != nil {
m.logger.Error("CreateComment", zap.Error(err))
return
}
// 更新上级评论的评论数
if comment.ParentId > 0 {
err = tx.Model(&Comment{}).Where("id = ?", comment.ParentId).Update("comment_count", gorm.Expr("comment_count + ?", 1)).Error
if err != nil {
m.logger.Error("CreateComment", zap.Error(err))
return
}
}
return
}
// UpdateComment 更新Comment如果需要更新指定字段则请指定updateFields参数
func (m *DBModel) UpdateComment(comment *Comment, updateFields ...string) (err error) {
db := m.db.Model(comment)
tableName := Comment{}.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 = ?", comment.Id).Updates(comment).Error
if err != nil {
m.logger.Error("UpdateComment", zap.Error(err))
}
return
}
// GetComment 根据id获取Comment
func (m *DBModel) GetComment(id interface{}, fields ...string) (comment Comment, err error) {
db := m.db
fields = m.FilterValidFields(Comment{}.TableName(), fields...)
if len(fields) > 0 {
db = db.Select(fields)
}
err = db.Where("id = ?", id).First(&comment).Error
return
}
type OptionGetCommentList 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
}
// GetCommentList 获取Comment列表
func (m *DBModel) GetCommentList(opt *OptionGetCommentList) (commentList []Comment, total int64, err error) {
tableName := Comment{}.TableName()
db := m.db.Model(&Comment{})
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("GetCommentList", 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(&commentList).Error
if err != nil && err != gorm.ErrRecordNotFound {
m.logger.Error("GetCommentList", zap.Error(err))
}
return
}
// DeleteComment 删除数据
// 删除评论之后,对应文档的评论数量也要减少,对应的父级文档评论数量也要减少,用户评论数量也要减少
func (m *DBModel) DeleteComment(ids []int64, limitUserId ...int64) (err error) {
tx := m.db.Begin()
defer func() {
if err != nil {
tx.Rollback()
} else {
tx.Commit()
}
}()
var (
comments []Comment
user = &User{}
document = &Document{}
)
cond := []string{"id in (?)"}
args := []interface{}{ids}
if len(limitUserId) > 0 {
cond = append(cond, "user_id in (?)")
args = append(args, limitUserId)
}
condStr := strings.Join(cond, " and ")
tx.Where(condStr, args...).Select("id", "parent_id", "document_id", "user_id").Find(&comments)
if len(comments) == 0 {
return
}
err = tx.Where(condStr, args...).Delete(&Comment{}).Error
if err != nil {
m.logger.Error("DeleteComment", zap.Error(err))
return
}
for _, comment := range comments {
// 更新文档评论数
err = tx.Model(document).Where("id = ?", comment.DocumentId).UpdateColumn("comment_count", gorm.Expr("comment_count - ?", 1)).Error
if err != nil {
m.logger.Error("DeleteComment", zap.Error(err))
return
}
// 更新父级评论数
if comment.ParentId > 0 {
err = tx.Model(&comment).Where("id = ?", comment.ParentId).UpdateColumn("comment_count", gorm.Expr("comment_count - ?", 1)).Error
if err != nil {
m.logger.Error("DeleteComment", zap.Error(err))
return
}
}
// 更新用户评论数
err = tx.Model(user).Where("id = ?", comment.UserId).UpdateColumn("comment_count", gorm.Expr("comment_count - ?", 1)).Error
if err != nil {
m.logger.Error("DeleteComment", zap.Error(err))
return
}
}
return
}
func (m *DBModel) UpdateCommentStatus(ids []int64, status int32) (err error) {
err = m.db.Model(&Comment{}).Where("id in (?) and status != ?", ids, status).Update("status", status).Error
if err != nil {
m.logger.Error("UpdateCommentStatus", zap.Error(err))
}
return
}

@ -203,6 +203,7 @@ type ConfigSystem struct {
const (
ConfigSecurityMaxDocumentSize = "max_document_size" // 是否关闭注册
ConfigSecurityCommentInterval = "comment_interval" // 评论时间间隔
ConfigSecurityIsClose = "is_close" // 是否关闭注册
ConfigSecurityCloseStatement = "close_statement" // 闭站说明
ConfigSecurityEnableRegister = "enable_register" // 是否允许注册
@ -214,6 +215,7 @@ const (
type ConfigSecurity struct {
MaxDocumentSize int32 `json:"max_document_size"` // 允许上传的最大文档大小
CommentInterval int32 `json:"comment_interval"` // 评论时间间隔, 单位秒
IsClose bool `json:"is_close"` // 是否闭站
CloseStatement string `json:"close_statement"` // 闭站说明
EnableRegister bool `json:"enable_register"` // 是否启用注册
@ -346,7 +348,7 @@ func (m *DBModel) GetConfigOfSecurity(name ...string) (config ConfigSecurity) {
case "is_close", "enable_register", "enable_captcha_login", "enable_captcha_register", "enable_captcha_comment", "enable_captcha_find_password", "enable_captcha_upload":
value, _ := strconv.ParseBool(cfg.Value)
data[cfg.Name] = value
case "max_document_size":
case "max_document_size", "comment_interval":
data[cfg.Name], _ = strconv.Atoi(cfg.Value)
default:
data[cfg.Name] = cfg.Value
@ -409,6 +411,7 @@ func (m *DBModel) initConfig() (err error) {
// 安全配置项
{Category: ConfigCategorySecurity, Name: ConfigSecurityMaxDocumentSize, Label: "最大文档大小(MB)", Value: "50", Placeholder: "允许用户上传的最大文档大小默认为50即50MB", InputType: "number", Sort: 15, Options: ""},
{Category: ConfigCategorySecurity, Name: ConfigSecurityCommentInterval, Label: "评论时间间隔", Value: "10", Placeholder: "用户评论时间间隔单位为秒。0表示不限制。", InputType: "number", Sort: 15, Options: ""},
{Category: ConfigCategorySecurity, Name: ConfigSecurityIsClose, Label: "是否关闭网站", Value: "false", Placeholder: "请选择是否关闭网站", InputType: "switch", Sort: 16, Options: ""},
{Category: ConfigCategorySecurity, Name: ConfigSecurityCloseStatement, Label: "闭站说明", Value: "false", Placeholder: "关闭网站后,页面提示的内容", InputType: "textarea", Sort: 17, Options: ""},
{Category: ConfigCategorySecurity, Name: ConfigSecurityEnableRegister, Label: "是否允许注册", Value: "true", Placeholder: "请选择是否允许用户注册", InputType: "switch", Sort: 18, Options: ""},

@ -1,3 +1,4 @@
package model
func getPermissions() (permissions []Permission) {
@ -54,6 +55,11 @@ func getPermissions() (permissions []Permission) {
{Title: "上传文章图片和音视频", Description: "", Method: "POST", Path: "/api/v1/upload/article"},
{Title: "上传文档分类封面", Description: "", Method: "POST", Path: "/api/v1/upload/category"},
{Title: "上传配置图片文件", Description: "", Method: "POST", Path: "/api/v1/upload/config"},
{Title: "获取评论列表", Description: "", Method: "GRPC", Path: "/api.v1.CommentAPI/ListComment"},
{Title: "获取单个评论", Description: "", Method: "GRPC", Path: "/api.v1.CommentAPI/GetComment"},
{Title: "批量审核评论", Description: "", Method: "GRPC", Path: "/api.v1.CommentAPI/CheckComment"},
{Title: "删除评论", Description: "", Method: "GRPC", Path: "/api.v1.CommentAPI/DeleteComment"},
}
return
}

@ -9,17 +9,18 @@ import (
)
type Group struct {
Id int64 `form:"id" json:"id,omitempty" gorm:"primaryKey;autoIncrement;column:id;comment:用户组 id;"`
Title string `form:"title" json:"title,omitempty" gorm:"column:title;type:varchar(64);size:64;index:title,unique;comment:用户组名称;"`
Color string `form:"color" json:"color,omitempty" gorm:"column:color;type:varchar(20);size:20;comment:颜色;"`
IsDefault bool `form:"is_default" json:"is_default,omitempty" gorm:"column:is_default;type:tinyint(3);default:0;index:is_default;comment:是否默认;"`
IsDisplay bool `form:"is_display" json:"is_display,omitempty" gorm:"column:is_display;type:tinyint(3);default:0;comment:是否显示在用户名后;"`
Description string `form:"description" json:"description,omitempty" gorm:"column:description;type:varchar(255);size:255;comment:用户组描述;"`
UserCount int `form:"user_count" json:"user_count,omitempty" gorm:"column:user_count;type:int(11);size:11;default:0;comment:用户数量;"`
Sort int `form:"sort" json:"sort,omitempty" gorm:"column:sort;type:int(11);size:11;default:0;comment:排序,值越大越靠前;"`
EnableUpload bool `form:"enable_upload" json:"enable_upload,omitempty" gorm:"column:enable_upload;type:tinyint(3);default:0;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:更新时间;"`
Id int64 `form:"id" json:"id,omitempty" gorm:"primaryKey;autoIncrement;column:id;comment:用户组 id;"`
Title string `form:"title" json:"title,omitempty" gorm:"column:title;type:varchar(64);size:64;index:title,unique;comment:用户组名称;"`
Color string `form:"color" json:"color,omitempty" gorm:"column:color;type:varchar(20);size:20;comment:颜色;"`
IsDefault bool `form:"is_default" json:"is_default,omitempty" gorm:"column:is_default;type:tinyint(3);default:0;index:is_default;comment:是否默认;"`
IsDisplay bool `form:"is_display" json:"is_display,omitempty" gorm:"column:is_display;type:tinyint(3);default:0;comment:是否显示在用户名后;"`
Description string `form:"description" json:"description,omitempty" gorm:"column:description;type:varchar(255);size:255;comment:用户组描述;"`
UserCount int `form:"user_count" json:"user_count,omitempty" gorm:"column:user_count;type:int(11);size:11;default:0;comment:用户数量;"`
Sort int `form:"sort" json:"sort,omitempty" gorm:"column:sort;type:int(11);size:11;default:0;comment:排序,值越大越靠前;"`
EnableUpload bool `form:"enable_upload" json:"enable_upload,omitempty" gorm:"column:enable_upload;type:tinyint(3);default:0;comment:是否允许上传文档;"`
EnableCommentApproval bool `form:"enable_comment_approval" json:"enable_comment_approval,omitempty" gorm:"column:enable_comment_approval;type:tinyint(3);default:0;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 (Group) TableName() string {
@ -27,7 +28,6 @@ func (Group) TableName() string {
}
// CreateGroup 创建Group
// TODO: 创建成功之后,注意相关表统计字段数值的增减
func (m *DBModel) CreateGroup(group *Group) (err error) {
sess := m.db.Begin()
defer func() {
@ -94,7 +94,7 @@ func (m *DBModel) UpdateGroup(group *Group, updateFields ...string) (err error)
}
// GetGroup 根据id获取Group
func (m *DBModel) GetGroup(id interface{}, fields ...string) (group Group, err error) {
func (m *DBModel) GetGroup(id int64, fields ...string) (group Group, err error) {
db := m.db
fields = m.FilterValidFields(Group{}.TableName(), fields...)

@ -171,6 +171,7 @@ func (m *DBModel) SyncDB() (err error) {
&Logout{},
&Article{},
&Favorite{},
&Comment{},
}
if err = m.db.AutoMigrate(tableModels...); err != nil {
m.logger.Fatal("SyncDB", zap.Error(err))
@ -277,9 +278,6 @@ func (m *DBModel) initGroupAndPermission() (err error) {
// 如果没有任何用户组,则初始化
var existGroup Group
m.db.First(&existGroup)
if existGroup.Id > 0 {
return
}
sess := m.db.Begin()
defer func() {
@ -290,10 +288,13 @@ func (m *DBModel) initGroupAndPermission() (err error) {
}
}()
err = sess.Create(&groups).Error
if err != nil {
m.logger.Error("initGroup", zap.Error(err))
return
if existGroup.Id == 0 {
// 用户组还不存在,则创建初始用户组
err = sess.Create(&groups).Error
if err != nil {
m.logger.Error("initGroup", zap.Error(err))
return
}
}
// 初始化权限

@ -110,7 +110,7 @@ func (m *DBModel) DeletePermission(ids []interface{}) (err error) {
return
}
// CheckPermissionByUserId 根据用户ID检查用户是否有权限
// CheckPermissionByUserId 根据用户ID检查用户是否有权限
func (m *DBModel) CheckPermissionByUserId(userId int64, path string, httpMethod ...string) (permission Permission, yes bool) {
var (
userGroups []UserGroup
@ -118,12 +118,6 @@ func (m *DBModel) CheckPermissionByUserId(userId int64, path string, httpMethod
method string
)
// NOTE: ID为1的用户拥有所有权限可以理解为类似linux的root用户
if userId == 1 {
yes = true
return
}
if len(httpMethod) > 0 {
method = httpMethod[0]
}
@ -136,10 +130,16 @@ func (m *DBModel) CheckPermissionByUserId(userId int64, path string, httpMethod
}
permission, yes = m.CheckPermissionByGroupId(groupId, method, path)
// NOTE: ID为1的用户拥有所有权限可以理解为类似linux的root用户
if !yes && userId == 1 {
yes = true
return
}
return permission, yes
}
// CheckPermissionByGroupId 根据用户所属用户组ID检查用户是否有权限
// CheckPermissionByGroupId 根据用户所属用户组ID检查用户是否有权限
func (m *DBModel) CheckPermissionByGroupId(groupId []int64, method, path string) (permission Permission, yes bool) {
var err error
fields := []string{"id", "method", "path", "title"}

@ -430,3 +430,48 @@ func (m *DBModel) CanIUploadDocument(userId int64) (yes bool) {
}
return group.Id > 0
}
// 用户是否发表评论
func (m *DBModel) CanIPublishComment(userId int64) (defaultCommentStatus int8, err error) {
if userId <= 0 {
return
}
var (
group Group
userGroup UserGroup
comment Comment
)
m.db.Select("g.id").Table(group.TableName()+" g").Joins(
"left join "+userGroup.TableName()+" ug on g.id=ug.group_id",
).Where("ug.user_id = ? and g.enable_comment_approval = ?", userId, false).Find(&group)
// 评论不需要审核
if group.Id > 0 {
defaultCommentStatus = CommentStatusApproved
} else {
defaultCommentStatus = CommentStatusPending
}
// 评论时间间隔
commentInterval := m.GetConfigOfSecurity(ConfigSecurityCommentInterval).CommentInterval
if commentInterval <= 0 {
return
}
// 获取用户最新的一条评论信息
m.db.Select("id", "created_at").Where("user_id = ?", userId).Order("id desc").Find(&comment)
if comment.Id <= 0 {
return
}
seconds := int32(time.Since(*comment.CreatedAt).Seconds())
left := commentInterval - seconds
if left > 0 {
err = fmt.Errorf("您的评论太快了,请等待 %d 秒后再试", left)
return
}
return
}

@ -121,5 +121,14 @@ func RegisterGRPCService(dbModel *model.DBModel, logger *zap.Logger, endpoint st
return
}
// 评论服务
commentAPIService := biz.NewCommentAPIService(dbModel, logger)
v1.RegisterCommentAPIServer(grpcServer, commentAPIService)
err = v1.RegisterCommentAPIHandlerFromEndpoint(context.Background(), gwmux, endpoint, dialOpts)
if err != nil {
logger.Error("RegisterCommentAPIHandlerFromEndpoint", zap.Error(err))
return
}
return
}

@ -0,0 +1,49 @@
import service from '~/utils/request'
export const createComment = (data) => {
return service({
url: '/api/v1/comment',
method: 'post',
data,
})
}
export const updateComment = (data) => {
return service({
url: '/api/v1/comment',
method: 'put',
data,
})
}
export const deleteComment = (params) => {
return service({
url: '/api/v1/comment',
method: 'delete',
params,
})
}
export const getComment = (params) => {
return service({
url: '/api/v1/comment',
method: 'get',
params,
})
}
export const listComment = (params) => {
return service({
url: '/api/v1/comment/list',
method: 'get',
params,
})
}
export const checkComment = (data) => {
return service({
url: '/api/v1/comment/check',
method: 'post',
data,
})
}

@ -0,0 +1,137 @@
<template>
<el-row :class="`com-comment-item com-comment-item-${size}`">
<el-col :span="2">
<nuxt-link :to="{ name: 'user-id', params: { id: user.id } }"
><user-avatar :size="size == 'small' ? 40 : 48" :user="comment.user"
/></nuxt-link>
</el-col>
<el-col :span="22">
<div class="username">
<nuxt-link
class="el-link el-link--default"
:to="{ name: 'user-id', params: { id: comment.user_id } }"
>{{ comment.user.username }}</nuxt-link
>
</div>
<div class="comment-content">
<!-- eslint-disable-next-line vue/no-v-html -->
<span v-html="comment.reply_user" />
{{ comment.content }}
</div>
<div class="comment-action">
<el-row class="help-block">
<el-col :span="12">
<el-tooltip
:content="formatDatetime(comment.created_at)"
placement="right"
>
<span class="text-muted">
<i class="el-icon-time"></i>
{{ formatRelativeTime(comment.created_at) }}
</span>
</el-tooltip>
</el-col>
<el-col :span="12" class="text-right">
<el-button
type="text"
size="small"
icon="el-icon-chat-dot-square"
@click="reply"
>回复</el-button
>
</el-col>
</el-row>
</div>
<form-comment
v-if="replyComment"
:article-id="comment.article_id"
:parent-id="comment.id"
:placeholder="`回复 ${comment.user.username}`"
@success="commentSuccess"
/>
<slot></slot>
</el-col>
</el-row>
</template>
<script>
import { mapGetters } from 'vuex'
import UserAvatar from '~/components/UserAvatar.vue'
import { formatRelativeTime, formatDatetime } from '~/utils/utils'
export default {
name: 'CommentItem',
components: { UserAvatar },
props: {
size: {
type: String,
default: 'default', // defaultsmall
},
comment: {
type: Object,
default: () => ({
id: 0,
parent_id: 0,
user_id: 0,
username: '匿名',
avatar: '',
group_id: 0,
verify_status: 0,
content: '内容加载中...',
created_at: '0000-00-00',
}),
},
},
data() {
return {
replyComment: false,
}
},
computed: {
...mapGetters('user', ['user']),
},
methods: {
formatRelativeTime,
formatDatetime,
reply() {
this.replyComment = !this.replyComment
},
commentSuccess() {
this.$emit('success')
},
},
}
</script>
<style lang="scss" scoped>
.com-comment-item {
font-size: 14px;
.comment-content {
margin-top: 10px;
margin-bottom: 10px;
background-color: #f5f7f8;
border-radius: 4px;
padding: 20px;
box-sizing: border-box;
color: #565656;
span {
position: relative;
top: -2px;
}
}
.username a {
font-weight: 400;
color: #222;
}
}
.com-comment-item-small {
font-size: 13px;
.el-col-2 {
width: 7%;
}
.el-col-22 {
width: 93%;
}
.comment-content {
padding: 15px;
}
}
</style>

@ -0,0 +1,122 @@
<template>
<div class="com-comment-list">
<comment-item
v-for="comment in comments"
:key="'comment-' + comment.id"
:comment="comment"
@success="commentSuccess"
>
<comment-item
v-for="child in comment.children"
:key="'comment-' + child.id"
:comment="child"
:size="'small'"
@success="commentSuccess"
></comment-item>
</comment-item>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import CommentItem from '~/components/CommentItem.vue'
import { listComment } from '~/api/comment'
export default {
name: 'CommentList',
components: { CommentItem },
props: {
articleId: {
type: Number,
default: 0,
},
parentId: {
type: Number,
default: 0,
},
},
data() {
return {
comments: [],
req: {
article_id: this.articleId,
parent_id: this.parentId,
},
}
},
computed: {
...mapGetters('user', ['user']),
},
watch: {
articleId: {
handler(val) {
this.req.article_id = val
},
immediate: true,
},
parentId: {
handler(val) {
this.req.parent_id = val
},
immediate: true,
},
},
created() {
this.getComments()
},
methods: {
//
async getComments() {
if (!this.req.article_id) return
const res = await listComment({
article_id: this.articleId,
order: 'id asc',
})
if (res.status === 200) {
this.comments = this.comments2tree(res.data.comment || [])
}
},
commentSuccess() {
this.getComments()
},
comments2tree(comments) {
const tree = []
const map = {}
comments.forEach((comment) => {
map[comment.id] = comment
})
comments.forEach((comment) => {
//
let parent = map[comment.parent_id]
let replyUser = ''
if (comment.parent_id && parent) {
try {
replyUser = `<a href="/user/${parent.user.id}" class="el-link el-link--primary" target="blank">@${parent.user.username}</a>`
} catch (error) {}
}
while (parent && parent.parent_id) {
parent = map[parent.parent_id]
}
comment.reply_user = replyUser
if (parent) {
;(parent.children || (parent.children = [])).push(comment)
} else {
tree.push(comment)
}
})
return tree
},
},
}
</script>
<style lang="scss">
.com-comment-list {
& > .el-row {
margin-top: 20px;
border-bottom: 1px solid #efefef;
padding-bottom: 10px;
}
& > .el-row:first-of-type {
margin-top: 0;
}
}
</style>

@ -0,0 +1,190 @@
<template>
<div class="com-form-comment">
<el-form
ref="form"
:inline="true"
:model="comment"
:rules="rules"
class="form-comment"
>
<el-form-item prop="content" class="comment-content">
<el-input
v-model="comment.content"
type="textarea"
:placeholder="placeholder"
:autosize="{ minRows: 4, maxRows: 6 }"
/>
</el-form-item>
<el-form-item class="comment-btns">
<el-row>
<el-col :span="7"> 请文明评论理性发言. </el-col>
<el-col :span="17" class="text-right">
<template v-if="captcha.enable">
<el-form-item>
<div class="captcha">
<div v-if="captcha.type == 'audio'">
<el-row :gutter="15">
<el-col :span="20">
<audio
controls="controls"
:src="captcha.captcha"
></audio>
</el-col>
<el-col :span="4">
<el-tooltip placement="top" content="刷新语音验证码">
<el-button
icon="el-icon-refresh"
class="btn-audio-refresh"
@click="loadCaptcha"
></el-button>
</el-tooltip>
</el-col>
</el-row>
</div>
<div v-else>
<el-tooltip placement="top" content="点击可刷新验证码">
<img
:src="captcha.captcha"
class="pointer"
@click="loadCaptcha"
/>
</el-tooltip>
</div>
</div>
</el-form-item>
<el-form-item
prop="captcha"
:rules="[
{ required: true, trigger: 'blur', message: '请输入验证码' },
]"
>
<el-input
v-model="comment.captcha"
placeholder="请输入验证码"
></el-input>
</el-form-item>
</template>
<el-form-item>
<el-button
type="primary"
icon="el-icon-position"
@click="submitForm('form')"
>发布评论</el-button
>
</el-form-item>
</el-col>
</el-row>
</el-form-item>
</el-form>
</div>
</template>
<script>
import { getUserCaptcha } from '~/api/user'
import { createComment } from '~/api/comment'
export default {
name: 'FormComment',
props: {
articleId: {
type: Number,
default: 0,
},
parentId: {
type: Number,
default: 0,
},
placeholder: {
type: String,
default: '请输入评论内容',
},
},
data() {
return {
comment: {
article_id: this.articleId,
parent_id: this.parentId,
content: '',
captcha: '',
captcha_id: '',
},
captcha: {
enable: false,
},
rules: {
content: [
{ required: true, message: '请输入评论内容', trigger: 'blur' },
],
},
}
},
watch: {
articleId: {
handler(val) {
this.comment.article_id = val
},
immediate: true,
},
parentId: {
handler(val) {
this.comment.parent_id = val
},
immediate: true,
},
},
created() {
this.loadCaptcha()
},
methods: {
submitForm(formName) {
this.$refs[formName].validate(async (valid) => {
if (valid) {
const res = await createComment(this.comment)
if (res.status === 200) {
this.$message.success('评论成功')
this.comment.content = ''
this.comment.captcha = ''
this.loadCaptcha()
this.$emit('success')
} else {
this.$message.error(res.data.message)
}
} else {
return false
}
})
},
async loadCaptcha() {
const res = await getUserCaptcha({ type: 'comment', t: Date.now() })
if (res.data.enable) {
//
this.comment = {
...this.comment,
captcha_id: res.data.id,
}
this.captcha = res.data
}
},
},
}
</script>
<style lang="scss">
.com-form-comment {
.comment-content {
width: 100%;
.el-form-item__content {
display: block;
}
}
.comment-btns {
width: 100%;
img {
height: 40px;
}
.el-form-item__content {
display: block;
}
.captcha {
float: left;
}
}
}
</style>

@ -0,0 +1,77 @@
<template>
<div class="com-form-comment-check">
<el-form
ref="form"
:model="icomment"
class="form-comment-check"
label-position="top"
>
<el-form-item prop="content" label="评论内容">
<el-input
v-model="icomment.content"
type="textarea"
:placeholder="placeholder"
:autosize="{ minRows: 4, maxRows: 6 }"
disabled
/>
</el-form-item>
<el-form-item label="审核状态">
<el-radio-group v-model="icomment.status">
<el-radio :label="0">待审核</el-radio>
<el-radio :label="1">审核通过</el-radio>
<el-radio :label="2">审核拒绝</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item>
<el-button
type="primary"
class="btn-block"
icon="el-icon-check"
@click="onSubmit"
>提交</el-button
>
</el-form-item>
</el-form>
</div>
</template>
<script>
import { updateComment } from '~/api/comment'
export default {
name: 'FormCommentCheck',
props: {
comment: {
type: Object,
default: () => {},
},
},
data() {
return {
icomment: {
id: 0,
content: '',
status: 0,
},
}
},
watch: {
comment: {
handler(val) {
this.icomment = { status: 0, ...val }
},
immediate: true,
},
},
methods: {
async onSubmit() {
const res = await updateComment(this.icomment)
if (res.status === 200) {
this.$message.success('更新成功')
this.$emit('success')
} else {
this.$message.error(res.data.message)
}
},
},
}
</script>
<style lang="scss"></style>

@ -50,6 +50,17 @@
>
</el-switch>
</el-form-item>
<el-form-item label="评论需审核">
<el-switch
v-model="group.enable_comment_approval"
style="display: block"
active-color="#13ce66"
inactive-color="#ff4949"
active-text="是"
inactive-text="否"
>
</el-switch>
</el-form-item>
<el-form-item label="是否在用户名后展示">
<el-switch
v-model="group.is_display"

@ -0,0 +1,56 @@
<template>
<div class="com-user-avatar">
<el-avatar
:size="size"
:alt="user.username"
:src="user.avatar"
@error="errorAvatar"
>
<img src="/static/images/avatar.png" />
</el-avatar>
</div>
</template>
<script>
export default {
name: 'UserAvatar',
props: {
size: {
type: Number,
default: 80,
},
src: {
type: String,
default: '',
},
user: {
type: Object,
default: () => {},
},
},
methods: {
errorAvatar() {
return true
},
},
}
</script>
<style lang="scss" scoped>
.com-user-avatar {
display: inline-block;
position: relative;
.el-avatar {
border: 2px solid #ddd;
padding: 3px;
box-sizing: border-box;
background-color: #fff;
&:hover {
border: 2px solid #409eff;
}
img {
height: 100%;
width: 100%;
border-radius: 50%;
}
}
}
</style>

@ -3,10 +3,7 @@
<div class="user-card-avatar">
<!-- 如果是用户自己则点击头像可以更换头像 -->
<nuxt-link :to="'/user/' + user.id">
<el-avatar :src="user.avatar">
<!-- 默认 -->
<img src="/static/images/avatar.png" alt="" />
</el-avatar>
<UserAvatar :user="user" />
</nuxt-link>
</div>
<div class="user-card-username">
@ -46,7 +43,7 @@
</el-row>
</div>
<div v-if="!hideSignature" class="user-card-signature">
<div>个性签名</div>
<div><small>个性签名</small></div>
<div class="help-block">
{{ user.signature || '暂无个性签名' }}
</div>
@ -93,7 +90,7 @@ export default {
methods: {},
}
</script>
<style lang="scss">
<style lang="scss" scoped>
.com-user-card {
text-align: center;
.el-avatar {

@ -1,16 +1,271 @@
<template>
<div>
<div>{{ $route.name }}</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"
>
<template slot="buttons">
<el-dropdown
:disabled="selectedRow.length === 0"
@command="checkComment"
>
<el-button type="primary">
批量审批 <i class="el-icon-arrow-down el-icon--right"></i>
</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item :command="1">审核通过</el-dropdown-item>
<el-dropdown-item :command="2">审核拒绝</el-dropdown-item>
<el-dropdown-item :command="0">变为待审</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
</FormSearch>
</el-card>
<el-card shadow="never" class="mgt-20px">
<TableList
:loading="loading"
:table-data="comments"
:fields="tableListFields"
:show-actions="true"
:show-view="false"
:show-edit="true"
:show-delete="true"
:show-select="true"
:actions-min-width="160"
@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
v-if="comment.id > 0"
title="评论编审"
:visible.sync="formCommentVisible"
width="640px"
>
<FormCommentCheck
ref="formComment"
:comment="comment"
@success="formCommentSuccess"
/>
</el-dialog>
</div>
</template>
<script>
import {
listComment,
deleteComment,
getComment,
checkComment,
} from '~/api/comment'
import TableList from '~/components/TableList.vue'
import FormSearch from '~/components/FormSearch.vue'
export default {
components: { TableList, FormSearch },
layout: 'admin',
data() {
return {
loading: false,
formCommentVisible: false,
search: {
wd: '',
page: 1,
status: [],
size: 10,
order: 'id desc',
},
comments: [],
total: 0,
searchFormFields: [],
tableListFields: [],
selectedRow: [],
comment: { id: 0 },
}
},
head() {
return {
title: `面板 - MOREDOC · 魔刀文库`,
title: `评论管理 - ${this.settings.system.sitename}`,
}
},
computed: {
settings() {
return this.$store.state.setting.settings
},
},
async created() {
this.initSearchForm()
this.initTableListFields()
await this.listComment()
},
methods: {
async listComment() {
this.loading = true
const res = await listComment(this.search)
if (res.status === 200) {
this.comments = (res.data.comment || []).map((item) => {
item.username = item.user.username
return item
})
this.total = res.data.total
} else {
this.$message.error(res.data.message)
}
this.loading = false
},
handleSizeChange(val) {
this.search.size = val
this.listComment()
},
handlePageChange(val) {
this.search.page = val
this.listComment()
},
onSearch(search) {
this.search = { ...this.search, page: 1, ...search }
this.listComment()
},
onCreate() {
this.comment = { id: 0 }
this.formCommentVisible = true
this.$nextTick(() => {
this.$refs.commentForm.reset()
})
},
async editRow(row) {
const res = await getComment({ id: row.id })
if (res.status === 200) {
this.comment = res.data
this.formCommentVisible = true
} else {
this.$message.error(res.data.message)
}
},
formCommentSuccess() {
this.formCommentVisible = false
this.listComment()
},
async checkComment(cmd) {
const res = await checkComment({
id: this.selectedRow.map((item) => item.id),
status: cmd,
})
if (res.status === 200) {
this.$message.success('审批成功')
this.listComment()
return
}
this.$message.error(res.data.message || '审批失败')
},
batchDelete() {
this.$confirm(
`您确定要删除选中的【${this.selectedRow.length}条】评论吗?删除之后不可恢复!`,
'温馨提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
)
.then(async () => {
const ids = this.selectedRow.map((item) => item.id)
const res = await deleteComment({ id: ids })
if (res.status === 200) {
this.$message.success('删除成功')
this.listComment()
} else {
this.$message.error(res.data.message)
}
})
.catch(() => {})
},
deleteRow(row) {
this.$confirm(`您确定要删除该评论吗?删除之后不可恢复!`, '温馨提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(async () => {
const res = await deleteComment({ id: row.id })
if (res.status === 200) {
this.$message.success('删除成功')
this.listComment()
} 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: 'status',
placeholder: '请选择状态',
multiple: true,
options: [
{ label: '审核拒绝', value: 2 },
{ label: '审核通过', value: 1 },
{ label: '待审核', value: 0 },
],
},
]
},
initTableListFields() {
this.tableListFields = [
{ prop: 'id', label: 'ID', width: 80, type: 'number', fixed: 'left' },
{
prop: 'status',
label: '状态',
width: 80,
type: 'enum',
fixed: 'left',
enum: {
2: { label: '审核拒绝', value: 2, type: 'danger' },
1: { label: '审核通过', value: 1, type: 'success' },
0: { label: '待审核', value: 0 },
},
},
{ prop: 'document_title', label: '文档', minWidth: 150 },
{ prop: 'content', label: '评论内容', minWidth: 150 },
{ prop: 'username', label: '评论人', minWidth: 150 },
{ prop: 'created_at', label: '创建时间', width: 160, type: 'datetime' },
{ prop: 'updated_at', label: '更新时间', width: 160, type: 'datetime' },
]
},
},
}
</script>
<style></style>

@ -227,6 +227,7 @@ export default {
title: '',
is_display: true,
enable_upload: false,
enable_comment_approval: false,
is_default: false,
}
},
@ -254,6 +255,12 @@ export default {
width: 120,
type: 'bool',
},
{
prop: 'enable_comment_approval',
label: '评论需审核',
width: 120,
type: 'bool',
},
{ prop: 'is_display', label: '是否展示', width: 80, type: 'bool' },
{ prop: 'description', label: '描述', width: 250 },
{ prop: 'created_at', label: '创建时间', width: 160, type: 'datetime' },

@ -67,6 +67,12 @@ const cumstomPermissionMap = {
children: [],
pages: ['/admin/article'],
},
'api.v1.CommentAPI': {
label: '评论管理',
path: 'ListComment',
children: [],
pages: ['/admin/comment'],
},
upload: {
id: 0,
label: '上传管理',

Loading…
Cancel
Save