|
|
package biz
|
|
|
|
|
|
import (
|
|
|
"context"
|
|
|
"fmt"
|
|
|
"net/url"
|
|
|
"os"
|
|
|
"path/filepath"
|
|
|
"strings"
|
|
|
"time"
|
|
|
|
|
|
pb "moredoc/api/v1"
|
|
|
"moredoc/middleware/auth"
|
|
|
"moredoc/model"
|
|
|
"moredoc/util"
|
|
|
"moredoc/util/filetil"
|
|
|
"moredoc/util/segword/jieba"
|
|
|
|
|
|
"github.com/golang-jwt/jwt"
|
|
|
"go.uber.org/zap"
|
|
|
"google.golang.org/grpc/codes"
|
|
|
"google.golang.org/grpc/status"
|
|
|
"google.golang.org/protobuf/types/known/emptypb"
|
|
|
)
|
|
|
|
|
|
type DocumentAPIService struct {
|
|
|
pb.UnimplementedDocumentAPIServer
|
|
|
dbModel *model.DBModel
|
|
|
logger *zap.Logger
|
|
|
}
|
|
|
|
|
|
func NewDocumentAPIService(dbModel *model.DBModel, logger *zap.Logger) (service *DocumentAPIService) {
|
|
|
return &DocumentAPIService{dbModel: dbModel, logger: logger.Named("DocumentAPIService")}
|
|
|
}
|
|
|
|
|
|
func (s *DocumentAPIService) checkPermission(ctx context.Context) (userClaims *auth.UserClaims, err error) {
|
|
|
return checkGRPCPermission(s.dbModel, ctx)
|
|
|
}
|
|
|
|
|
|
func (s *DocumentAPIService) checkLogin(ctx context.Context) (userClaims *auth.UserClaims, err error) {
|
|
|
return checkGRPCLogin(s.dbModel, ctx)
|
|
|
}
|
|
|
|
|
|
// CreateDocument 创建文档
|
|
|
// 0. 判断是否有权限
|
|
|
// 1. 同名覆盖:找到该作者上传的相同title和ext的文档,然后用新文件覆盖,同时文档状态改为待转换
|
|
|
// 2. 相同hash的文档如果已经被转换了,则该文档的状态直接改为已转换
|
|
|
// 3. 判断附件ID是否与用户ID匹配,不匹配则跳过该文档
|
|
|
func (s *DocumentAPIService) CreateDocument(ctx context.Context, req *pb.CreateDocumentRequest) (*emptypb.Empty, error) {
|
|
|
userCliams, err := s.checkLogin(ctx)
|
|
|
if err != nil {
|
|
|
return nil, err
|
|
|
}
|
|
|
|
|
|
if !s.dbModel.CanIUploadDocument(userCliams.UserId) {
|
|
|
return nil, status.Error(codes.PermissionDenied, "没有权限上传文档")
|
|
|
}
|
|
|
|
|
|
var (
|
|
|
attachmentIds []interface{}
|
|
|
attachmentMap = make(map[int64]model.Attachment)
|
|
|
)
|
|
|
|
|
|
for _, item := range req.Document {
|
|
|
attachmentIds = append(attachmentIds, item.AttachmentId)
|
|
|
}
|
|
|
|
|
|
attachments, _, _ := s.dbModel.GetAttachmentList(&model.OptionGetAttachmentList{
|
|
|
Ids: attachmentIds,
|
|
|
QueryIn: map[string][]interface{}{"user_id": {userCliams.UserId}},
|
|
|
})
|
|
|
if len(attachments) == 0 {
|
|
|
return nil, status.Error(codes.InvalidArgument, "文档文件参数attachment_id不正确")
|
|
|
}
|
|
|
|
|
|
for _, attachment := range attachments {
|
|
|
attachmentMap[attachment.Id] = attachment
|
|
|
}
|
|
|
|
|
|
var (
|
|
|
documents []model.Document
|
|
|
docMapAttachment = make(map[int]int64)
|
|
|
)
|
|
|
for idx, doc := range req.Document {
|
|
|
attachment, ok := attachmentMap[doc.AttachmentId]
|
|
|
if !ok {
|
|
|
continue
|
|
|
}
|
|
|
|
|
|
doc := model.Document{
|
|
|
Title: doc.Title,
|
|
|
Keywords: strings.Join(jieba.SegWords(doc.Title), ","),
|
|
|
UserId: userCliams.UserId,
|
|
|
// UUID: uuid.Must(uuid.NewV4()).String(),
|
|
|
Score: 300,
|
|
|
Price: int(doc.Price),
|
|
|
Size: attachment.Size,
|
|
|
Ext: attachment.Ext,
|
|
|
Status: model.DocumentStatusPending,
|
|
|
}
|
|
|
docMapAttachment[idx] = attachment.Id
|
|
|
documents = append(documents, doc)
|
|
|
}
|
|
|
|
|
|
docs, err := s.dbModel.CreateDocuments(documents, req.CategoryId)
|
|
|
if err != nil {
|
|
|
return nil, status.Error(codes.Internal, err.Error())
|
|
|
}
|
|
|
|
|
|
attachIdTypeIdMap := make(map[int64]int64)
|
|
|
for idx, doc := range docs {
|
|
|
if attachmentId, ok := docMapAttachment[idx]; ok {
|
|
|
attachIdTypeIdMap[attachmentId] = doc.Id
|
|
|
}
|
|
|
}
|
|
|
|
|
|
s.dbModel.SetAttachmentTypeId(attachIdTypeIdMap)
|
|
|
|
|
|
return &emptypb.Empty{}, nil
|
|
|
}
|
|
|
|
|
|
// UpdateDocument 更新文档
|
|
|
// 1. 对于普通用户,可以更新自己创建的文档
|
|
|
// 2. 对于管理员,可以更新所有文档
|
|
|
func (s *DocumentAPIService) UpdateDocument(ctx context.Context, req *pb.Document) (*emptypb.Empty, error) {
|
|
|
s.logger.Debug("UpdateDocument", zap.Any("req", req))
|
|
|
userClaims, err := s.checkPermission(ctx)
|
|
|
if userClaims == nil { // 未登录
|
|
|
return nil, err
|
|
|
}
|
|
|
|
|
|
fields := []string{"id", "title", "keywords", "description", "price"}
|
|
|
doc := &model.Document{}
|
|
|
util.CopyStruct(req, doc)
|
|
|
|
|
|
if err != nil { // 普通用户,只能更新自己的文档
|
|
|
existDoc, _ := s.dbModel.GetDocument(req.Id, "id", "user_id")
|
|
|
if existDoc.UserId != userClaims.UserId {
|
|
|
return nil, status.Error(codes.PermissionDenied, "文档不存在或没有权限")
|
|
|
}
|
|
|
} else { // 管理员,可以更新所有文档
|
|
|
fields = append(fields, "status")
|
|
|
}
|
|
|
|
|
|
err = s.dbModel.UpdateDocument(doc, req.CategoryId, fields...)
|
|
|
if err != nil {
|
|
|
return nil, status.Error(codes.Internal, err.Error())
|
|
|
}
|
|
|
|
|
|
return &emptypb.Empty{}, nil
|
|
|
}
|
|
|
|
|
|
// DeleteDocument 删除文档
|
|
|
// 1. 对于普通用户,可以删除自己创建的文档
|
|
|
// 2. 对于管理员,可以删除所有文档
|
|
|
func (s *DocumentAPIService) DeleteDocument(ctx context.Context, req *pb.DeleteDocumentRequest) (*emptypb.Empty, error) {
|
|
|
userClaims, err := s.checkPermission(ctx)
|
|
|
s.logger.Info("DeleteDocument", zap.Any("userClaims", userClaims), zap.Error(err))
|
|
|
if err != nil && userClaims == nil { // 未登录
|
|
|
return nil, err
|
|
|
}
|
|
|
|
|
|
ids := req.Id
|
|
|
if err != nil { // 普通用户,只能删除自己创建的文档
|
|
|
userDocs, _, _ := s.dbModel.GetDocumentList(&model.OptionGetDocumentList{
|
|
|
WithCount: false,
|
|
|
SelectFields: []string{"id"},
|
|
|
Ids: util.Slice2Interface(req.Id),
|
|
|
})
|
|
|
|
|
|
if len(userDocs) == 0 {
|
|
|
return &emptypb.Empty{}, nil
|
|
|
}
|
|
|
|
|
|
for _, doc := range userDocs {
|
|
|
ids = append(ids, doc.Id)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
err = s.dbModel.DeleteDocument(ids, userClaims.UserId)
|
|
|
if err != nil {
|
|
|
return nil, status.Errorf(codes.Internal, "删除文档失败:%v", err)
|
|
|
}
|
|
|
|
|
|
return &emptypb.Empty{}, nil
|
|
|
}
|
|
|
|
|
|
// GetDocument 获取文档(任何人都可以调用)。当文档禁用之后,只有管理员可以查看
|
|
|
func (s *DocumentAPIService) GetDocument(ctx context.Context, req *pb.GetDocumentRequest) (*pb.Document, error) {
|
|
|
doc, _ := s.dbModel.GetDocument(req.Id)
|
|
|
if doc.Id == 0 {
|
|
|
return nil, status.Error(codes.NotFound, "文档不存在")
|
|
|
}
|
|
|
doc.ViewCount += 1
|
|
|
|
|
|
_, err := s.checkPermission(ctx)
|
|
|
if err != nil && doc.Status == model.DocumentStatusDisabled {
|
|
|
return nil, status.Error(codes.NotFound, "文档不存在或没有权限")
|
|
|
}
|
|
|
|
|
|
pbDoc := &pb.Document{}
|
|
|
util.CopyStruct(doc, pbDoc)
|
|
|
docCates, _, _ := s.dbModel.GetDocumentCategoryList(
|
|
|
&model.OptionGetDocumentCategoryList{
|
|
|
WithCount: false,
|
|
|
SelectFields: []string{"category_id"},
|
|
|
QueryIn: map[string][]interface{}{"document_id": {doc.Id}},
|
|
|
},
|
|
|
)
|
|
|
|
|
|
for _, dc := range docCates {
|
|
|
pbDoc.CategoryId = append(pbDoc.CategoryId, dc.CategoryId)
|
|
|
}
|
|
|
|
|
|
if req.WithAuthor {
|
|
|
user, _ := s.dbModel.GetUser(doc.UserId, model.UserPublicFields...)
|
|
|
pbDoc.User = &pb.User{}
|
|
|
util.CopyStruct(&user, pbDoc.User)
|
|
|
s.dbModel.UpdateDocumentField(doc.Id, map[string]interface{}{"view_count": doc.ViewCount})
|
|
|
}
|
|
|
|
|
|
// 查找文档相关联的附件。对于列表,只返回hash和id,不返回其他字段
|
|
|
attchment := s.dbModel.GetAttachmentByTypeAndTypeId(model.AttachmentTypeDocument, doc.Id, "hash", "path")
|
|
|
fixedData := make(map[string]interface{})
|
|
|
if pbDoc.Width == 0 || pbDoc.Height == 0 {
|
|
|
bigCover := strings.TrimLeft(strings.TrimSuffix(attchment.Path, filepath.Ext(attchment.Path)), "./") + "/cover.big.png"
|
|
|
doc.Width, doc.Height, _ = util.GetImageSize(bigCover)
|
|
|
if doc.Width*doc.Height > 0 {
|
|
|
pbDoc.Width = int32(doc.Width)
|
|
|
pbDoc.Height = int32(doc.Height)
|
|
|
fixedData = map[string]interface{}{"width": doc.Width, "height": doc.Height}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if desc := strings.TrimSpace(pbDoc.Description); desc == "" {
|
|
|
desc = doc.Title // 默认
|
|
|
textFile := strings.TrimLeft(strings.TrimSuffix(attchment.Path, filepath.Ext(attchment.Path)), "./") + "/content.txt"
|
|
|
if content, errRead := os.ReadFile(textFile); errRead == nil { // 读取文本内容,以提取关键字和摘要
|
|
|
contentStr := string(content)
|
|
|
replacer := strings.NewReplacer("\r", " ", "\n", " ", "\t", " ")
|
|
|
contentStr = strings.TrimSpace(replacer.Replace(util.Substr(contentStr, 255)))
|
|
|
if contentStr != "" {
|
|
|
desc = contentStr
|
|
|
}
|
|
|
}
|
|
|
doc.Description = desc
|
|
|
fixedData["description"] = desc
|
|
|
}
|
|
|
|
|
|
if len(fixedData) > 0 {
|
|
|
s.dbModel.UpdateDocumentField(doc.Id, fixedData)
|
|
|
}
|
|
|
|
|
|
pbDoc.Attachment = &pb.Attachment{Hash: attchment.Hash}
|
|
|
return pbDoc, nil
|
|
|
}
|
|
|
|
|
|
// ListDocument 查询文档列表
|
|
|
// 1. 对于普通用户,只能查询未禁用的文档,且最多只能查询100页
|
|
|
// 2. 对于管理员,可以查询所有文档,可以根据关键字进行查询
|
|
|
func (s *DocumentAPIService) ListDocument(ctx context.Context, req *pb.ListDocumentRequest) (*pb.ListDocumentReply, error) {
|
|
|
opt := &model.OptionGetDocumentList{
|
|
|
WithCount: req.Limit <= 0,
|
|
|
Page: int(req.Page),
|
|
|
Size: int(req.Size_),
|
|
|
SelectFields: req.Field,
|
|
|
QueryIn: make(map[string][]interface{}),
|
|
|
QueryLike: make(map[string][]interface{}),
|
|
|
IsRecommend: req.IsRecommend,
|
|
|
}
|
|
|
|
|
|
if len(req.Order) > 0 {
|
|
|
opt.Sort = []string{req.Order}
|
|
|
}
|
|
|
|
|
|
if len(req.CategoryId) > 0 {
|
|
|
opt.QueryIn["category_id"] = util.Slice2Interface(req.CategoryId)
|
|
|
}
|
|
|
|
|
|
if len(req.UserId) > 0 {
|
|
|
opt.QueryIn["user_id"] = []interface{}{req.UserId[0]}
|
|
|
}
|
|
|
|
|
|
_, err := s.checkPermission(ctx)
|
|
|
if err == nil { // 有权限,则不限页数
|
|
|
if req.Wd != "" {
|
|
|
opt.QueryLike["title"] = []interface{}{req.Wd}
|
|
|
opt.QueryLike["keywords"] = []interface{}{req.Wd}
|
|
|
opt.QueryLike["description"] = []interface{}{req.Wd}
|
|
|
}
|
|
|
|
|
|
if len(req.Status) > 0 {
|
|
|
opt.QueryIn["status"] = util.Slice2Interface(req.Status)
|
|
|
}
|
|
|
} else {
|
|
|
opt.Size = util.LimitRange(opt.Size, 1, 24)
|
|
|
opt.Page = util.LimitRange(opt.Page, 1, 100)
|
|
|
if len(req.Status) == 1 && req.Status[0] == model.DocumentStatusConverted {
|
|
|
opt.QueryIn["status"] = []interface{}{
|
|
|
model.DocumentStatusConverted,
|
|
|
}
|
|
|
} else {
|
|
|
opt.QueryIn["status"] = []interface{}{
|
|
|
model.DocumentStatusPending, model.DocumentStatusConverting,
|
|
|
model.DocumentStatusConverted, model.DocumentStatusFailed,
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if req.Limit > 0 {
|
|
|
opt.Size = int(req.Limit)
|
|
|
opt.Page = 1
|
|
|
}
|
|
|
|
|
|
s.logger.Debug("ListDocument", zap.Any("opt", opt))
|
|
|
return s.listDocument(opt)
|
|
|
}
|
|
|
|
|
|
// ListRecycleDocument 回收站文档
|
|
|
func (s *DocumentAPIService) ListRecycleDocument(ctx context.Context, req *pb.ListDocumentRequest) (*pb.ListDocumentReply, error) {
|
|
|
_, err := s.checkPermission(ctx)
|
|
|
if err != nil {
|
|
|
return nil, status.Error(codes.PermissionDenied, err.Error())
|
|
|
}
|
|
|
|
|
|
opt := &model.OptionGetDocumentList{
|
|
|
WithCount: true,
|
|
|
Page: int(req.Page),
|
|
|
Size: int(req.Size_),
|
|
|
SelectFields: req.Field,
|
|
|
QueryIn: make(map[string][]interface{}),
|
|
|
QueryLike: make(map[string][]interface{}),
|
|
|
IsRecycle: true,
|
|
|
}
|
|
|
|
|
|
if len(req.CategoryId) > 0 {
|
|
|
opt.QueryIn["category_id"] = util.Slice2Interface(req.CategoryId)
|
|
|
}
|
|
|
|
|
|
if len(req.UserId) > 0 {
|
|
|
opt.QueryIn["user_id"] = []interface{}{req.UserId[0]}
|
|
|
}
|
|
|
|
|
|
if req.Wd != "" {
|
|
|
opt.QueryLike["title"] = []interface{}{req.Wd}
|
|
|
opt.QueryLike["keywords"] = []interface{}{req.Wd}
|
|
|
opt.QueryLike["description"] = []interface{}{req.Wd}
|
|
|
}
|
|
|
|
|
|
if len(req.Status) > 0 {
|
|
|
opt.QueryIn["status"] = util.Slice2Interface(req.Status)
|
|
|
}
|
|
|
|
|
|
return s.listDocument(opt)
|
|
|
}
|
|
|
|
|
|
// RecoverRecycleDocument 恢复回收站文档
|
|
|
func (s *DocumentAPIService) RecoverRecycleDocument(ctx context.Context, req *pb.RecoverRecycleDocumentRequest) (*emptypb.Empty, error) {
|
|
|
_, err := s.checkPermission(ctx)
|
|
|
if err != nil {
|
|
|
return nil, status.Error(codes.PermissionDenied, err.Error())
|
|
|
}
|
|
|
|
|
|
s.logger.Debug("RecoverRecycleDocument", zap.Any("req", req))
|
|
|
|
|
|
if len(req.Id) == 0 {
|
|
|
return &emptypb.Empty{}, nil
|
|
|
}
|
|
|
|
|
|
err = s.dbModel.RecoverRecycleDocument(req.Id)
|
|
|
if err != nil {
|
|
|
return nil, status.Error(codes.Internal, err.Error())
|
|
|
}
|
|
|
|
|
|
return &emptypb.Empty{}, nil
|
|
|
}
|
|
|
|
|
|
// DeleteRecycleDocument 删除回收站文档
|
|
|
func (s *DocumentAPIService) DeleteRecycleDocument(ctx context.Context, req *pb.DeleteDocumentRequest) (*emptypb.Empty, error) {
|
|
|
userClaims, err := s.checkPermission(ctx)
|
|
|
if err != nil {
|
|
|
return nil, status.Error(codes.PermissionDenied, err.Error())
|
|
|
}
|
|
|
|
|
|
err = s.dbModel.DeleteDocument(req.Id, userClaims.UserId, true)
|
|
|
if err != nil {
|
|
|
return nil, status.Error(codes.Internal, err.Error())
|
|
|
}
|
|
|
|
|
|
return &emptypb.Empty{}, nil
|
|
|
}
|
|
|
|
|
|
// ClearRecycleDocument 清空回收站文档
|
|
|
func (s *DocumentAPIService) ClearRecycleDocument(ctx context.Context, req *emptypb.Empty) (*emptypb.Empty, error) {
|
|
|
_, err := s.checkPermission(ctx)
|
|
|
if err != nil {
|
|
|
return nil, status.Error(codes.PermissionDenied, err.Error())
|
|
|
}
|
|
|
|
|
|
err = s.dbModel.ClearRecycleDocument()
|
|
|
if err != nil {
|
|
|
s.logger.Error("ClearRecycleDocument", zap.Error(err))
|
|
|
return nil, status.Error(codes.Internal, err.Error())
|
|
|
}
|
|
|
|
|
|
return &emptypb.Empty{}, nil
|
|
|
}
|
|
|
|
|
|
func (s *DocumentAPIService) listDocument(opt *model.OptionGetDocumentList) (*pb.ListDocumentReply, error) {
|
|
|
docs, total, err := s.dbModel.GetDocumentList(opt)
|
|
|
if err != nil {
|
|
|
return nil, status.Error(codes.Internal, err.Error())
|
|
|
}
|
|
|
|
|
|
var pbDocs []*pb.Document
|
|
|
err = util.CopyStruct(&docs, &pbDocs)
|
|
|
if err != nil {
|
|
|
s.logger.Error("CopyStruct failed", zap.Error(err))
|
|
|
}
|
|
|
|
|
|
var (
|
|
|
docCates []model.DocumentCategory
|
|
|
docUsers []model.User
|
|
|
docIndexMap = make(map[int64]int)
|
|
|
userIndexesMap = make(map[int64][]int)
|
|
|
deletedUserIndexMap = make(map[int64][]int)
|
|
|
docIds []int64
|
|
|
userIds []int64
|
|
|
)
|
|
|
|
|
|
for i, doc := range pbDocs {
|
|
|
docIndexMap[doc.Id] = i
|
|
|
userIndexesMap[doc.UserId] = append(userIndexesMap[doc.UserId], i)
|
|
|
docIds = append(docIds, doc.Id)
|
|
|
if doc.UserId > 0 {
|
|
|
userIds = append(userIds, doc.UserId)
|
|
|
}
|
|
|
if doc.DeletedUserId > 0 {
|
|
|
userIds = append(userIds, doc.DeletedUserId)
|
|
|
deletedUserIndexMap[doc.DeletedUserId] = append(deletedUserIndexMap[doc.DeletedUserId], i)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if len(pbDocs) > 0 {
|
|
|
docCates, _, _ = s.dbModel.GetDocumentCategoryList(&model.OptionGetDocumentCategoryList{
|
|
|
WithCount: false,
|
|
|
SelectFields: []string{"document_id", "category_id"},
|
|
|
QueryIn: map[string][]interface{}{"document_id": util.Slice2Interface(docIds)},
|
|
|
})
|
|
|
for _, docCate := range docCates {
|
|
|
pbDocs[docIndexMap[docCate.DocumentId]].CategoryId = append(pbDocs[docIndexMap[docCate.DocumentId]].CategoryId, docCate.CategoryId)
|
|
|
}
|
|
|
|
|
|
docUsers, _, _ = s.dbModel.GetUserList(&model.OptionGetUserList{
|
|
|
WithCount: false,
|
|
|
SelectFields: []string{"id", "username"},
|
|
|
QueryIn: map[string][]interface{}{"id": util.Slice2Interface(userIds)},
|
|
|
})
|
|
|
|
|
|
// 查找文档相关联的附件。对于列表,只返回hash和id,不返回其他字段
|
|
|
attachments, _, _ := s.dbModel.GetAttachmentList(&model.OptionGetAttachmentList{
|
|
|
WithCount: false,
|
|
|
SelectFields: []string{"hash", "id", "type_id"},
|
|
|
QueryIn: map[string][]interface{}{
|
|
|
"type_id": util.Slice2Interface(docIds),
|
|
|
"type": {model.AttachmentTypeDocument},
|
|
|
},
|
|
|
})
|
|
|
|
|
|
for _, attachment := range attachments {
|
|
|
index := docIndexMap[attachment.TypeId]
|
|
|
pbDocs[index].Attachment = &pb.Attachment{
|
|
|
Hash: attachment.Hash,
|
|
|
}
|
|
|
}
|
|
|
|
|
|
for _, docUser := range docUsers {
|
|
|
indexes := userIndexesMap[docUser.Id]
|
|
|
for _, index := range indexes {
|
|
|
pbDocs[index].Username = docUser.Username
|
|
|
}
|
|
|
|
|
|
indexes = deletedUserIndexMap[docUser.Id]
|
|
|
for _, index := range indexes {
|
|
|
pbDocs[index].DeletedUsername = docUser.Username
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return &pb.ListDocumentReply{
|
|
|
Total: total,
|
|
|
Document: pbDocs,
|
|
|
}, nil
|
|
|
}
|
|
|
|
|
|
// SetDocumentRecommend 推荐文档
|
|
|
func (s *DocumentAPIService) SetDocumentRecommend(ctx context.Context, req *pb.SetDocumentRecommendRequest) (*emptypb.Empty, error) {
|
|
|
_, err := s.checkPermission(ctx)
|
|
|
if err != nil {
|
|
|
return nil, status.Error(codes.PermissionDenied, err.Error())
|
|
|
}
|
|
|
|
|
|
err = s.dbModel.SetDocumentRecommend(req.Id, req.Type)
|
|
|
if err != nil {
|
|
|
return nil, status.Error(codes.Internal, err.Error())
|
|
|
}
|
|
|
|
|
|
return &emptypb.Empty{}, nil
|
|
|
}
|
|
|
|
|
|
func (s *DocumentAPIService) ListDocumentForHome(ctx context.Context, req *pb.ListDocumentForHomeRequest) (*pb.ListDocumentForHomeResponse, error) {
|
|
|
// 1. 查询启用了的分类
|
|
|
categories, _, _ := s.dbModel.GetCategoryList(&model.OptionGetCategoryList{
|
|
|
WithCount: false,
|
|
|
QueryIn: map[string][]interface{}{
|
|
|
"enable": {true},
|
|
|
"parent_id": {0},
|
|
|
},
|
|
|
})
|
|
|
|
|
|
if len(categories) == 0 {
|
|
|
return &pb.ListDocumentForHomeResponse{}, nil
|
|
|
}
|
|
|
|
|
|
limit := 5
|
|
|
if req.Limit > 0 && req.Limit <= 100 {
|
|
|
limit = int(req.Limit)
|
|
|
}
|
|
|
|
|
|
resp := &pb.ListDocumentForHomeResponse{}
|
|
|
for _, category := range categories {
|
|
|
docs, _, _ := s.dbModel.GetDocumentList(&model.OptionGetDocumentList{
|
|
|
WithCount: false,
|
|
|
QueryIn: map[string][]interface{}{
|
|
|
"category_id": {category.Id},
|
|
|
},
|
|
|
Page: 1,
|
|
|
Size: limit,
|
|
|
Sort: []string{"id desc"},
|
|
|
SelectFields: []string{"id", "title", "ext"},
|
|
|
})
|
|
|
|
|
|
var pbDocs []*pb.Document
|
|
|
util.CopyStruct(&docs, &pbDocs)
|
|
|
resp.Document = append(resp.Document, &pb.ListDocumentForHomeItem{
|
|
|
CategoryId: category.Id,
|
|
|
CategoryName: category.Title,
|
|
|
CategoryCover: category.Cover,
|
|
|
Document: pbDocs,
|
|
|
})
|
|
|
}
|
|
|
|
|
|
return resp, nil
|
|
|
}
|
|
|
|
|
|
// 搜索文档
|
|
|
func (s *DocumentAPIService) SearchDocument(ctx context.Context, req *pb.SearchDocumentRequest) (res *pb.SearchDocumentReply, err error) {
|
|
|
res = &pb.SearchDocumentReply{}
|
|
|
now := time.Now()
|
|
|
opt := &model.OptionGetDocumentList{
|
|
|
WithCount: true,
|
|
|
Page: int(req.Page),
|
|
|
Size: int(req.Size_),
|
|
|
QueryIn: make(map[string][]interface{}),
|
|
|
}
|
|
|
opt.Size = util.LimitRange(opt.Size, 10, 10)
|
|
|
opt.Page = util.LimitRange(opt.Page, 1, 100)
|
|
|
if req.Wd == "" {
|
|
|
return res, nil
|
|
|
}
|
|
|
opt.QueryLike = map[string][]interface{}{
|
|
|
"title": util.Slice2Interface(strings.Split(req.Wd, " ")),
|
|
|
"keywords": util.Slice2Interface(strings.Split(req.Wd, " ")),
|
|
|
"description": util.Slice2Interface(strings.Split(req.Wd, " ")),
|
|
|
}
|
|
|
if len(req.CategoryId) > 0 {
|
|
|
opt.QueryIn = map[string][]interface{}{
|
|
|
"category_id": util.Slice2Interface(req.CategoryId),
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if req.Ext != "" {
|
|
|
exts := filetil.GetExts(req.Ext)
|
|
|
if len(exts) > 0 {
|
|
|
opt.QueryIn["ext"] = util.Slice2Interface(exts)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if req.Sort != "" {
|
|
|
opt.Sort = []string{req.Sort}
|
|
|
}
|
|
|
|
|
|
docs, total, err := s.dbModel.GetDocumentList(opt)
|
|
|
if err != nil {
|
|
|
return res, status.Errorf(codes.Internal, "搜索文档失败:%s", err)
|
|
|
}
|
|
|
util.CopyStruct(&docs, &res.Document)
|
|
|
res.Total = total
|
|
|
res.Spend = fmt.Sprintf("%.3f", time.Since(now).Seconds())
|
|
|
return res, nil
|
|
|
}
|
|
|
|
|
|
// 下载文档
|
|
|
// 0. 查询用户是否登录
|
|
|
// 1. 查询文档是否存在
|
|
|
// 2. 查询用户是否购买和下载过
|
|
|
// 3. 查询文档是否免费
|
|
|
func (s *DocumentAPIService) DownloadDocument(ctx context.Context, req *pb.Document) (res *pb.DownloadDocumentReply, err error) {
|
|
|
cfg := s.dbModel.GetConfigOfDownload()
|
|
|
userClaims, err := s.checkLogin(ctx)
|
|
|
if err != nil && !cfg.EnableGuestDownload {
|
|
|
// 未登录且不允许游客下载
|
|
|
return res, status.Errorf(codes.Unauthenticated, err.Error())
|
|
|
}
|
|
|
|
|
|
var userId int64
|
|
|
if userClaims != nil {
|
|
|
userId = userClaims.UserId
|
|
|
}
|
|
|
|
|
|
ip := ""
|
|
|
ips, _ := util.GetGRPCRemoteIP(ctx)
|
|
|
if len(ips) > 0 {
|
|
|
ip = ips[0]
|
|
|
}
|
|
|
|
|
|
downloadIP := s.dbModel.CountDownloadTodayForIP(ip)
|
|
|
if downloadIP >= int64(cfg.TimesEveryIP) {
|
|
|
return res, status.Errorf(codes.PermissionDenied, "您所在IP今日下载次数已达上限(%d)", cfg.TimesEveryIP)
|
|
|
}
|
|
|
|
|
|
downloadUser := s.dbModel.CountDownloadTodayForUser(userId)
|
|
|
if downloadUser >= int64(cfg.TimesEveryDay) {
|
|
|
return res, status.Errorf(codes.PermissionDenied, "您的账户今日下载次数已达上限(%d)", cfg.TimesEveryDay)
|
|
|
}
|
|
|
|
|
|
// 查询文档存不存在
|
|
|
doc, err := s.dbModel.GetDocument(req.Id, "id", "price", "status", "title", "ext", "user_id")
|
|
|
if err != nil || doc.Status == model.DocumentStatusDisabled {
|
|
|
return res, status.Errorf(codes.NotFound, "文档不存在")
|
|
|
}
|
|
|
|
|
|
// 文档不免费且未登录
|
|
|
if doc.Price > 0 && userId == 0 {
|
|
|
return res, status.Errorf(codes.PermissionDenied, "付费文档,请先登录再下载")
|
|
|
}
|
|
|
|
|
|
// 查询附件存不存在
|
|
|
attachment := s.dbModel.GetAttachmentByTypeAndTypeId(model.AttachmentTypeDocument, doc.Id, "id", "hash")
|
|
|
if attachment.Id == 0 {
|
|
|
return res, status.Errorf(codes.NotFound, "附件不存在")
|
|
|
}
|
|
|
|
|
|
user, _ := s.dbModel.GetUser(userId)
|
|
|
if doc.UserId != userId && user.CreditCount < doc.Price {
|
|
|
return res, status.Errorf(codes.PermissionDenied, "魔豆不足,无法下载")
|
|
|
}
|
|
|
|
|
|
// 用户可以免费下载自己的文档
|
|
|
free := doc.UserId == userId || s.dbModel.CanIFreeDownload(userId, doc.Id)
|
|
|
down := &model.Download{
|
|
|
UserId: userId,
|
|
|
DocumentId: doc.Id,
|
|
|
Ip: ip,
|
|
|
IsPay: !free,
|
|
|
}
|
|
|
|
|
|
s.logger.Debug("下载文档", zap.Any("down", down), zap.Bool("canFreeDownload", free))
|
|
|
|
|
|
// 直接返回下载地址
|
|
|
err = s.dbModel.CreateDownload(down)
|
|
|
if err != nil {
|
|
|
return res, status.Errorf(codes.Internal, "创建下载失败:%s", err.Error())
|
|
|
}
|
|
|
|
|
|
link, err := s.generateDownloadURL(doc, cfg, attachment.Hash)
|
|
|
if err != nil {
|
|
|
return res, status.Errorf(codes.Internal, "生成下载地址失败:%s", err.Error())
|
|
|
}
|
|
|
res = &pb.DownloadDocumentReply{
|
|
|
Url: link,
|
|
|
}
|
|
|
return res, nil
|
|
|
}
|
|
|
|
|
|
// 通过JWT生成下载文档的URL
|
|
|
func (s *DocumentAPIService) generateDownloadURL(document model.Document, cfg model.ConfigDownload, hash string) (link string, err error) {
|
|
|
expiredAt := time.Now().Add(time.Second * time.Duration(cfg.UrlDuration)).Unix()
|
|
|
claims := jwt.StandardClaims{
|
|
|
ExpiresAt: expiredAt,
|
|
|
Id: hash,
|
|
|
}
|
|
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
|
|
tokenString, err := token.SignedString([]byte(cfg.SecretKey))
|
|
|
if err != nil {
|
|
|
return "", err
|
|
|
}
|
|
|
return fmt.Sprintf("/download/%s?filename=%s", tokenString, url.QueryEscape(document.Title+document.Ext)), nil
|
|
|
}
|
|
|
|
|
|
func (s *DocumentAPIService) GetRelatedDocuments(ctx context.Context, req *pb.Document) (res *pb.ListDocumentReply, err error) {
|
|
|
docs, _ := s.dbModel.GetRelatedDocuments(req.Id)
|
|
|
res = &pb.ListDocumentReply{}
|
|
|
util.CopyStruct(&docs, &res.Document)
|
|
|
return res, nil
|
|
|
}
|
|
|
|
|
|
// 获取文档评分
|
|
|
func (s *DocumentAPIService) GetDocumentScore(ctx context.Context, req *pb.DocumentScore) (res *pb.DocumentScore, err error) {
|
|
|
userClaims, err := s.checkLogin(ctx)
|
|
|
if err != nil {
|
|
|
return nil, err
|
|
|
}
|
|
|
|
|
|
score, _ := s.dbModel.GetDocumentScore(userClaims.UserId, req.DocumentId)
|
|
|
res = &pb.DocumentScore{}
|
|
|
util.CopyStruct(&score, res)
|
|
|
return res, nil
|
|
|
}
|
|
|
|
|
|
// 设置文档评分
|
|
|
func (s *DocumentAPIService) SetDocumentScore(ctx context.Context, req *pb.DocumentScore) (res *emptypb.Empty, err error) {
|
|
|
userClaims, err := s.checkLogin(ctx)
|
|
|
if err != nil {
|
|
|
return nil, err
|
|
|
}
|
|
|
|
|
|
score, _ := s.dbModel.GetDocumentScore(userClaims.UserId, req.DocumentId)
|
|
|
if score.Id > 0 {
|
|
|
return nil, status.Errorf(codes.PermissionDenied, "您已经评分过了")
|
|
|
}
|
|
|
|
|
|
score = model.DocumentScore{
|
|
|
UserId: userClaims.UserId,
|
|
|
DocumentId: req.DocumentId,
|
|
|
Score: int(req.Score),
|
|
|
}
|
|
|
err = s.dbModel.CreateDocumentScore(&score)
|
|
|
if err != nil {
|
|
|
return nil, status.Errorf(codes.Internal, "评分失败:%s", err.Error())
|
|
|
}
|
|
|
|
|
|
return &emptypb.Empty{}, nil
|
|
|
}
|