|
|
package model
|
|
|
|
|
|
import (
|
|
|
"fmt"
|
|
|
"moredoc/util"
|
|
|
"moredoc/util/converter"
|
|
|
"os"
|
|
|
"path/filepath"
|
|
|
"strings"
|
|
|
"time"
|
|
|
|
|
|
"go.uber.org/zap"
|
|
|
"golang.org/x/net/html"
|
|
|
"gorm.io/gorm"
|
|
|
)
|
|
|
|
|
|
const (
|
|
|
// 封面,按照A4纸的尺寸比例
|
|
|
DocumentCoverWidth = 210
|
|
|
DocumentCoverHeight = 297
|
|
|
)
|
|
|
|
|
|
const (
|
|
|
DocumentStatusPending = iota // 待转换
|
|
|
DocumentStatusConverting // 转换中
|
|
|
DocumentStatusConverted // 已转换
|
|
|
DocumentStatusFailed // 转换失败
|
|
|
DocumentStatusDisabled // 已禁用
|
|
|
DocumentStatusRePending // 重新等待转换
|
|
|
)
|
|
|
|
|
|
var DocumentStatusMap = map[int]struct{}{
|
|
|
DocumentStatusPending: {},
|
|
|
DocumentStatusConverting: {},
|
|
|
DocumentStatusConverted: {},
|
|
|
DocumentStatusFailed: {},
|
|
|
DocumentStatusDisabled: {},
|
|
|
}
|
|
|
|
|
|
type Document struct {
|
|
|
Id int64 `form:"id" json:"id,omitempty" gorm:"primaryKey;autoIncrement;column:id;comment:;"`
|
|
|
Title string `form:"title" json:"title,omitempty" gorm:"column:title;type:varchar(255);size:255;comment:文档名称;"`
|
|
|
Keywords string `form:"keywords" json:"keywords,omitempty" gorm:"column:keywords;type:varchar(255);size:255;comment:文档关键字;"`
|
|
|
Description string `form:"description" json:"description,omitempty" gorm:"column:description;type:varchar(255);size:255;comment:文档描述;"`
|
|
|
UserId int64 `form:"user_id" json:"user_id,omitempty" gorm:"column:user_id;type:bigint(20);size:20;default:0;index:user_id;comment:文档所属用户ID;"`
|
|
|
Width int `form:"width" json:"width,omitempty" gorm:"column:width;type:int(11);size:11;default:0;comment:宽;"`
|
|
|
Height int `form:"height" json:"height,omitempty" gorm:"column:height;type:int(11);size:11;default:0;comment:高;"`
|
|
|
Preview int `form:"preview" json:"preview,omitempty" gorm:"column:preview;type:int(11);size:11;default:0;comment:允许预览页数;"`
|
|
|
Pages int `form:"pages" json:"pages,omitempty" gorm:"column:pages;type:int(11);size:11;default:0;comment:文档页数;index:idx_pages;"`
|
|
|
DownloadCount int `form:"download_count" json:"download_count,omitempty" gorm:"column:download_count;type:int(11);size:11;default:0;comment:下载人次;index:idx_download_count;"`
|
|
|
ViewCount int `form:"view_count" json:"view_count,omitempty" gorm:"column:view_count;type:int(11);size:11;default:0;comment:浏览人次;index:idx_view_count;"`
|
|
|
FavoriteCount int `form:"favorite_count" json:"favorite_count,omitempty" gorm:"column:favorite_count;type:int(11);size:11;default:0;comment:收藏人次;index:idx_favorite_count;"`
|
|
|
CommentCount int `form:"comment_count" json:"comment_count,omitempty" gorm:"column:comment_count;type:int(11);size:11;default:0;comment:评论人次;"`
|
|
|
Score int `form:"score" json:"score,omitempty" gorm:"column:score;type:int(11);size:11;default:300;comment:评分,3位整数表示,500表示5分;"`
|
|
|
ScoreCount int `form:"score_count" json:"score_count,omitempty" gorm:"column:score_count;type:int(11);size:11;default:0;comment:评分数量;"`
|
|
|
Price int `form:"price" json:"price,omitempty" gorm:"column:price;type:int(11);size:11;default:0;comment:价格,0表示免费;index:idx_price;"`
|
|
|
Size int64 `form:"size" json:"size,omitempty" gorm:"column:size;type:bigint(20);size:20;default:0;comment:文件大小;"`
|
|
|
Ext string `form:"ext" json:"ext,omitempty" gorm:"column:ext;type:varchar(16);size:16;index:idx_ext;comment:文件扩展名"`
|
|
|
Status int `form:"status" json:"status,omitempty" gorm:"column:status;type:smallint(6);size:6;default:0;index:status;comment:文档状态:0 待转换,1 转换中,2 转换完成,3 转换失败,4 禁用;"`
|
|
|
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:更新时间;"`
|
|
|
DeletedAt *gorm.DeletedAt `form:"deleted_at" json:"deleted_at,omitempty" gorm:"column:deleted_at;type:datetime;index:idx_deleted_at;comment:删除时间;"`
|
|
|
DeletedUserId int64 `form:"deleted_user_id" json:"deleted_user_id,omitempty" gorm:"column:deleted_user_id;type:bigint(20);size:20;default:0;comment:删除用户ID;"`
|
|
|
EnableGZIP bool `form:"enable_gzip" json:"enable_gzip,omitempty" gorm:"column:enable_gzip;type:tinyint(1);size:1;default:0;comment:是否启用GZIP压缩;"`
|
|
|
RecommendAt *time.Time `form:"recommend_at" json:"recommend_at,omitempty" gorm:"column:recommend_at;type:datetime;comment:推荐时间;index:idx_recommend_at;"`
|
|
|
PreviewExt string `form:"preview_ext" json:"preview_ext,omitempty" gorm:"column:preview_ext;type:varchar(16);size:16;default:.svg;comment:预览图扩展名;"`
|
|
|
}
|
|
|
|
|
|
func (Document) TableName() string {
|
|
|
return tablePrefix + "document"
|
|
|
}
|
|
|
|
|
|
// UpdateDocument 更新Document,如果需要更新指定字段,则请指定updateFields参数
|
|
|
func (m *DBModel) UpdateDocument(document *Document, categoryId []int64, updateFields ...string) (err error) {
|
|
|
sess := m.db.Begin()
|
|
|
defer func() {
|
|
|
if err != nil {
|
|
|
sess.Rollback()
|
|
|
} else {
|
|
|
sess.Commit()
|
|
|
}
|
|
|
}()
|
|
|
|
|
|
var (
|
|
|
oldDocCategories []DocumentCategory
|
|
|
oldDocCategoryIds []int64
|
|
|
newDocCategories []DocumentCategory
|
|
|
modelDocumentCategory = &DocumentCategory{}
|
|
|
modelCategory = &Category{}
|
|
|
)
|
|
|
|
|
|
sess.Table(modelDocumentCategory.TableName()).Select("category_id").Where("document_id = ?", document.Id).Find(&oldDocCategories)
|
|
|
for _, cate := range oldDocCategories {
|
|
|
oldDocCategoryIds = append(oldDocCategoryIds, cate.CategoryId)
|
|
|
}
|
|
|
|
|
|
if len(oldDocCategoryIds) > 0 {
|
|
|
err = sess.Where("document_id = ?", document.Id).Delete(modelDocumentCategory).Error
|
|
|
if err != nil {
|
|
|
m.logger.Error("Delete DocumentCategory", zap.Error(err))
|
|
|
return
|
|
|
}
|
|
|
|
|
|
// 更新分类统计
|
|
|
err = sess.Model(modelCategory).Where("id in (?)", oldDocCategoryIds).Update("doc_count", gorm.Expr("doc_count - ?", 1)).Error
|
|
|
if err != nil {
|
|
|
m.logger.Error("Update doc_count--", zap.Error(err))
|
|
|
return
|
|
|
}
|
|
|
}
|
|
|
|
|
|
for _, cateId := range categoryId {
|
|
|
newDocCategories = append(newDocCategories, DocumentCategory{
|
|
|
DocumentId: document.Id,
|
|
|
CategoryId: cateId,
|
|
|
})
|
|
|
}
|
|
|
if len(newDocCategories) > 0 {
|
|
|
m.logger.Debug("newDocCategories", zap.Any("newDocCategories", newDocCategories))
|
|
|
err = sess.Create(&newDocCategories).Error
|
|
|
if err != nil {
|
|
|
m.logger.Error("Create New Category", zap.Error(err))
|
|
|
return
|
|
|
}
|
|
|
|
|
|
err = sess.Model(modelCategory).Where("id in (?)", categoryId).Update("doc_count", gorm.Expr("doc_count + ?", 1)).Error
|
|
|
if err != nil {
|
|
|
m.logger.Error("Update doc_count++", zap.Error(err))
|
|
|
return
|
|
|
}
|
|
|
}
|
|
|
|
|
|
updateFields = m.FilterValidFields(Document{}.TableName(), updateFields...)
|
|
|
if len(updateFields) > 0 { // 更新指定字段
|
|
|
sess = sess.Select(updateFields)
|
|
|
} else {
|
|
|
sess = sess.Select(m.GetTableFields(document.TableName())).Omit("deleted_at", "deleted_user_id")
|
|
|
}
|
|
|
|
|
|
err = sess.Where("id = ?", document.Id).Updates(document).Error
|
|
|
if err != nil {
|
|
|
m.logger.Error("UpdateDocument", zap.Error(err))
|
|
|
return
|
|
|
}
|
|
|
|
|
|
return
|
|
|
}
|
|
|
|
|
|
func (m *DBModel) SetDocumentReconvert() (err error) {
|
|
|
err = m.db.Model(&Document{}).
|
|
|
Where("status = ?", DocumentStatusFailed).
|
|
|
Update("status", DocumentStatusPending).Error
|
|
|
if err != nil {
|
|
|
m.logger.Error("SetDocumentReconvert", zap.Error(err))
|
|
|
}
|
|
|
return
|
|
|
}
|
|
|
|
|
|
func (m *DBModel) UpdateDocumentField(id int64, fieldValue map[string]interface{}) (err error) {
|
|
|
err = m.db.Model(&Document{}).Where("id = ?", id).Updates(fieldValue).Error
|
|
|
if err != nil {
|
|
|
m.logger.Error("UpdateDocumentField", zap.Error(err))
|
|
|
return
|
|
|
}
|
|
|
return
|
|
|
}
|
|
|
|
|
|
// GetDocument 根据id获取Document
|
|
|
func (m *DBModel) GetDocument(id interface{}, fields ...string) (document Document, err error) {
|
|
|
db := m.db
|
|
|
|
|
|
fields = m.FilterValidFields(Document{}.TableName(), fields...)
|
|
|
if len(fields) > 0 {
|
|
|
db = db.Select(fields)
|
|
|
}
|
|
|
|
|
|
err = db.Where("id = ?", id).First(&document).Error
|
|
|
return
|
|
|
}
|
|
|
|
|
|
type OptionGetDocumentList 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
|
|
|
IsRecycle bool // 是否是回收站模式查询
|
|
|
IsRecommend []bool
|
|
|
FeeType string // 费用类型:free免费,charge收费
|
|
|
}
|
|
|
|
|
|
// GetDocumentList 获取Document列表
|
|
|
func (m *DBModel) GetDocumentList(opt *OptionGetDocumentList) (documentList []Document, total int64, err error) {
|
|
|
tableDocument := Document{}.TableName() + " d"
|
|
|
db := m.db.Unscoped().Table(tableDocument)
|
|
|
if opt.IsRecycle {
|
|
|
// 回收站模式,只根据删除的倒序排序
|
|
|
opt.Sort = []string{"d.deleted_at desc"}
|
|
|
db = db.Where("d.deleted_at IS NOT NULL")
|
|
|
} else {
|
|
|
db = db.Where("d.deleted_at IS NULL")
|
|
|
}
|
|
|
|
|
|
m.logger.Debug("GetDocumentList", zap.Any("opt", opt))
|
|
|
|
|
|
db = m.generateQueryIn(db, tableDocument, opt.QueryIn)
|
|
|
db = m.generateQueryLike(db, tableDocument, opt.QueryLike)
|
|
|
db = m.generateQueryRange(db, tableDocument, opt.QueryRange)
|
|
|
if len(opt.Ids) > 0 {
|
|
|
db = db.Where("id in (?)", opt.Ids)
|
|
|
}
|
|
|
|
|
|
if categoryIds, ok := opt.QueryIn["category_id"]; ok && len(categoryIds) > 0 {
|
|
|
tableCategory := DocumentCategory{}.TableName()
|
|
|
db = db.Joins("left join "+tableCategory+" dc on dc.document_id = d.id").Where("dc.category_id in (?)", categoryIds)
|
|
|
}
|
|
|
|
|
|
if l := len(opt.IsRecommend); l == 1 {
|
|
|
if opt.IsRecommend[0] {
|
|
|
db = db.Where("d.`recommend_at` IS NOT NULL")
|
|
|
} else {
|
|
|
db = db.Where("d.`recommend_at` IS NULL")
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if opt.FeeType != "" {
|
|
|
switch opt.FeeType {
|
|
|
case "free":
|
|
|
db = db.Where("d.`price` = ?", 0)
|
|
|
case "charge":
|
|
|
db = db.Where("d.`price` > ?", 0)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if opt.WithCount {
|
|
|
err = db.Group("d.id").Count(&total).Error
|
|
|
if err != nil {
|
|
|
m.logger.Error("GetDocumentList", zap.Error(err))
|
|
|
return
|
|
|
}
|
|
|
}
|
|
|
|
|
|
opt.SelectFields = m.FilterValidFields(tableDocument, opt.SelectFields...)
|
|
|
if len(opt.SelectFields) > 0 {
|
|
|
db = db.Select(opt.SelectFields)
|
|
|
} else {
|
|
|
db = db.Select(m.GetTableFields(tableDocument))
|
|
|
}
|
|
|
|
|
|
if len(opt.Sort) > 0 {
|
|
|
db = m.generateQuerySort(db, tableDocument, opt.Sort)
|
|
|
} else {
|
|
|
db = db.Order("d.id desc")
|
|
|
}
|
|
|
|
|
|
db = db.Offset((opt.Page - 1) * opt.Size).Limit(opt.Size)
|
|
|
err = db.Group("d.id").Find(&documentList).Error
|
|
|
if err != nil && err != gorm.ErrRecordNotFound {
|
|
|
m.logger.Error("GetDocumentList", zap.Error(err))
|
|
|
}
|
|
|
return
|
|
|
}
|
|
|
|
|
|
// DeleteDocument 删除数据
|
|
|
func (m *DBModel) DeleteDocument(ids []int64, deletedUserId int64, deepDelete ...bool) (err error) {
|
|
|
var (
|
|
|
docs []Document
|
|
|
docCates []DocumentCategory
|
|
|
docFields = []string{"id", "status", "user_id", "deleted_at", "deleted_user_id", "title"}
|
|
|
docCateFields = []string{"id", "document_id", "category_id"}
|
|
|
modelUser = &User{}
|
|
|
modelDocument = &Document{}
|
|
|
modelDocumentCategory = &DocumentCategory{}
|
|
|
modelCategory = &Category{}
|
|
|
docCateMap = make(map[int64][]int64)
|
|
|
)
|
|
|
|
|
|
// 1. 查询文档信息
|
|
|
m.db.Model(modelDocument).Select(docFields).Unscoped().Where("id in (?)", ids).Find(&docs)
|
|
|
m.db.Model(modelDocumentCategory).Select(docCateFields).Where("document_id in (?)", ids).Find(&docCates)
|
|
|
|
|
|
for _, docCate := range docCates {
|
|
|
docCateMap[docCate.DocumentId] = append(docCateMap[docCate.DocumentId], docCate.CategoryId)
|
|
|
}
|
|
|
|
|
|
cfgScore := m.GetConfigOfScore(ConfigScoreDeleteDocument, ConfigScoreCreditName)
|
|
|
|
|
|
sess := m.db.Begin()
|
|
|
defer func() {
|
|
|
if err != nil {
|
|
|
sess.Rollback()
|
|
|
} else {
|
|
|
sess.Commit()
|
|
|
}
|
|
|
}()
|
|
|
|
|
|
if len(deepDelete) > 0 && deepDelete[0] { // 标记附件为删除状态
|
|
|
err = sess.Where("type = ? and type_id in (?)", AttachmentTypeDocument, ids).Delete(&Attachment{}).Error
|
|
|
if err != nil {
|
|
|
m.logger.Error("DeleteDocument", zap.Error(err))
|
|
|
return
|
|
|
}
|
|
|
}
|
|
|
|
|
|
for _, doc := range docs {
|
|
|
if doc.DeletedAt == nil {
|
|
|
err = sess.Model(modelUser).Where("id = ?", doc.UserId).Update("doc_count", gorm.Expr("doc_count - ?", 1)).Error
|
|
|
if err != nil {
|
|
|
m.logger.Error("DeleteDocument", zap.Error(err))
|
|
|
return
|
|
|
}
|
|
|
|
|
|
if cateIds, ok := docCateMap[doc.Id]; ok && len(cateIds) > 0 {
|
|
|
err = sess.Model(modelCategory).Where("id in (?)", cateIds).Update("doc_count", gorm.Expr("doc_count - ?", 1)).Error
|
|
|
if err != nil {
|
|
|
m.logger.Error("DeleteDocument", zap.Error(err))
|
|
|
return
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if len(deepDelete) > 0 && deepDelete[0] { // 彻底删除
|
|
|
err = sess.Unscoped().Delete(&doc).Error
|
|
|
if err != nil {
|
|
|
m.logger.Error("DeleteDocument", zap.Error(err))
|
|
|
return
|
|
|
}
|
|
|
|
|
|
// 关联的分类也需要删除
|
|
|
err = sess.Unscoped().Where("document_id = ?", doc.Id).Delete(modelDocumentCategory).Error
|
|
|
if err != nil {
|
|
|
m.logger.Error("DeleteDocument", zap.Error(err))
|
|
|
return
|
|
|
}
|
|
|
continue
|
|
|
}
|
|
|
|
|
|
// 逻辑删除
|
|
|
err = sess.Model(&doc).Where("id = ?", doc.Id).Updates(map[string]interface{}{
|
|
|
"deleted_at": time.Now(),
|
|
|
"deleted_user_id": deletedUserId,
|
|
|
}).Error
|
|
|
if err != nil {
|
|
|
m.logger.Error("DeleteDocument", zap.Error(err))
|
|
|
return
|
|
|
}
|
|
|
|
|
|
// 扣除积分和添加动态
|
|
|
dynamic := &Dynamic{
|
|
|
UserId: doc.UserId,
|
|
|
Type: DynamicTypeDeleteDocument,
|
|
|
Content: fmt.Sprintf("删除了文档《%s》", doc.Title),
|
|
|
}
|
|
|
|
|
|
if cfgScore.DeleteDocument != 0 { // 小于0表示扣除积分
|
|
|
score := cfgScore.DeleteDocument
|
|
|
if score < 0 {
|
|
|
score = -score
|
|
|
}
|
|
|
dynamic.Content += fmt.Sprintf(",扣除了 %d %s", score, cfgScore.CreditName)
|
|
|
err = sess.Model(modelUser).Where("id = ?", doc.UserId).Update("credit_count", gorm.Expr("credit_count - ?", score)).Error
|
|
|
if err != nil {
|
|
|
m.logger.Error("DeleteDocument", zap.Error(err))
|
|
|
return
|
|
|
}
|
|
|
}
|
|
|
|
|
|
err = sess.Create(dynamic).Error
|
|
|
if err != nil {
|
|
|
m.logger.Error("DeleteDocument", zap.Error(err))
|
|
|
return
|
|
|
}
|
|
|
|
|
|
if err != nil {
|
|
|
m.logger.Error("DeleteDocument", zap.Error(err))
|
|
|
return
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return
|
|
|
}
|
|
|
|
|
|
// RecoverRecycleDocument 恢复回收站中的文档
|
|
|
func (m *DBModel) RecoverRecycleDocument(documentId []int64) (err error) {
|
|
|
var (
|
|
|
modelDocument = &Document{}
|
|
|
modelCategory = &Category{}
|
|
|
modelUser = &User{}
|
|
|
documentCategories []DocumentCategory
|
|
|
docs []Document
|
|
|
)
|
|
|
|
|
|
m.db.Select([]string{"category_id"}).Where("document_id in (?)", documentId).Find(&documentCategories)
|
|
|
m.db.Select("user_id").Unscoped().Where("id in (?)", documentId).Find(&docs)
|
|
|
|
|
|
sess := m.db.Begin()
|
|
|
defer func() {
|
|
|
if err != nil {
|
|
|
sess.Rollback()
|
|
|
} else {
|
|
|
sess.Commit()
|
|
|
}
|
|
|
}()
|
|
|
|
|
|
err = sess.Model(modelDocument).Unscoped().Where("id in ?", documentId).Updates(map[string]interface{}{
|
|
|
"deleted_at": nil,
|
|
|
"deleted_user_id": 0,
|
|
|
}).Error
|
|
|
|
|
|
if err != nil {
|
|
|
m.logger.Error("RecoverRecycleDocument", zap.Error(err))
|
|
|
return
|
|
|
}
|
|
|
|
|
|
for _, docCate := range documentCategories {
|
|
|
err = sess.Model(modelCategory).Where("id = ?", docCate.CategoryId).Update("doc_count", gorm.Expr("doc_count + ?", 1)).Error
|
|
|
if err != nil {
|
|
|
m.logger.Error("RecoverRecycleDocument", zap.Error(err))
|
|
|
return
|
|
|
}
|
|
|
}
|
|
|
|
|
|
for _, doc := range docs {
|
|
|
err = sess.Model(modelUser).Where("id = ?", doc.UserId).Update("doc_count", gorm.Expr("doc_count + ?", 1)).Error
|
|
|
if err != nil {
|
|
|
m.logger.Error("RecoverRecycleDocument", zap.Error(err))
|
|
|
return
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return
|
|
|
}
|
|
|
|
|
|
func (m *DBModel) ClearRecycleDocument() (err error) {
|
|
|
var docs []Document
|
|
|
err = m.db.Unscoped().Select("id").Where("deleted_at is not null").Find(&docs).Error
|
|
|
if err != nil && err != gorm.ErrRecordNotFound {
|
|
|
m.logger.Error("ClearRecycleDocument", zap.Error(err))
|
|
|
return
|
|
|
}
|
|
|
|
|
|
if len(docs) == 0 {
|
|
|
err = nil
|
|
|
return
|
|
|
}
|
|
|
|
|
|
var ids []int64
|
|
|
for _, doc := range docs {
|
|
|
ids = append(ids, doc.Id)
|
|
|
}
|
|
|
|
|
|
err = m.DeleteDocument(ids, 0, true)
|
|
|
if err != nil {
|
|
|
m.logger.Error("DeleteDocument", zap.Error(err))
|
|
|
}
|
|
|
|
|
|
return
|
|
|
}
|
|
|
|
|
|
// 批量创建文档
|
|
|
func (m *DBModel) CreateDocuments(documents []Document, categoryIds []int64) (docs []Document, err error) {
|
|
|
sess := m.db.Begin()
|
|
|
defer func() {
|
|
|
if err != nil {
|
|
|
sess.Rollback()
|
|
|
} else {
|
|
|
sess.Commit()
|
|
|
}
|
|
|
}()
|
|
|
|
|
|
docCount := len(documents)
|
|
|
|
|
|
// 1. 分类下的文档数增加
|
|
|
err = sess.Model(&Category{}).
|
|
|
Where("id in ?", categoryIds).
|
|
|
Update("doc_count", gorm.Expr("doc_count + ?", docCount)).Error
|
|
|
if err != nil {
|
|
|
m.logger.Error("CreateDocuments", zap.Error(err))
|
|
|
return
|
|
|
}
|
|
|
|
|
|
// 2. 批量创建文档
|
|
|
err = sess.Create(documents).Error
|
|
|
if err != nil {
|
|
|
m.logger.Error("CreateDocuments", zap.Error(err))
|
|
|
return
|
|
|
}
|
|
|
docs = documents
|
|
|
|
|
|
// 3. 文档与分类关联
|
|
|
var docCates []DocumentCategory
|
|
|
for _, doc := range documents {
|
|
|
for _, cateId := range categoryIds {
|
|
|
docCates = append(docCates, DocumentCategory{
|
|
|
DocumentId: doc.Id,
|
|
|
CategoryId: cateId,
|
|
|
})
|
|
|
}
|
|
|
}
|
|
|
err = sess.Create(docCates).Error
|
|
|
if err != nil {
|
|
|
m.logger.Error("CreateDocuments", zap.Error(err))
|
|
|
return
|
|
|
}
|
|
|
|
|
|
// 用户文档数增加
|
|
|
err = sess.Model(&User{}).Where("id = ?", documents[0].UserId).Update("doc_count", gorm.Expr("doc_count + ?", docCount)).Error
|
|
|
if err != nil {
|
|
|
m.logger.Error("CreateDocuments", zap.Error(err))
|
|
|
return
|
|
|
}
|
|
|
|
|
|
// 奖励的数量
|
|
|
awardCount := docCount
|
|
|
cfg := m.GetConfigOfScore()
|
|
|
m.logger.Debug("CreateDocuments", zap.Any("GetConfigOfScore", cfg))
|
|
|
if cfg.UploadDocumentLimit > 0 {
|
|
|
var todayUploadCount int64
|
|
|
sess.Model(&Document{}).Where("user_id = ? and created_at >= ?", documents[0].UserId, time.Now().Format("2006-01-02")).Count(&todayUploadCount)
|
|
|
|
|
|
// 默认获得的积分奖励
|
|
|
creditCount := cfg.UploadDocument * int32(docCount)
|
|
|
if todayUploadCount > int64(cfg.UploadDocumentLimit) {
|
|
|
awardCount = int(cfg.UploadDocumentLimit + int32(docCount) - int32(todayUploadCount))
|
|
|
creditCount = cfg.UploadDocument * int32(awardCount)
|
|
|
}
|
|
|
m.logger.Debug("CreateDocuments", zap.Int32("creditCount", creditCount))
|
|
|
if creditCount > 0 {
|
|
|
err = sess.Model(&User{}).Where("id = ?", documents[0].UserId).Update("credit_count", gorm.Expr("credit_count + ?", creditCount)).Error
|
|
|
if err != nil {
|
|
|
m.logger.Error("CreateDocuments", zap.Error(err))
|
|
|
return
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 添加动态
|
|
|
var dynamics []Dynamic
|
|
|
for idx, doc := range documents {
|
|
|
var award int32
|
|
|
if idx < awardCount {
|
|
|
award = cfg.UploadDocument
|
|
|
}
|
|
|
content := fmt.Sprintf(`上传了文档《<a href="/document/%d">%s</a>》`, doc.Id, html.EscapeString(doc.Title))
|
|
|
if award > 0 {
|
|
|
content += fmt.Sprintf(`,获得了 %d %s奖励`, award, m.GetCreditName())
|
|
|
}
|
|
|
dynamics = append(dynamics, Dynamic{
|
|
|
UserId: doc.UserId,
|
|
|
Type: DynamicTypeUpload,
|
|
|
Content: content,
|
|
|
})
|
|
|
}
|
|
|
err = sess.Create(dynamics).Error
|
|
|
if err != nil {
|
|
|
m.logger.Error("CreateDocuments", zap.Error(err))
|
|
|
return
|
|
|
}
|
|
|
return
|
|
|
}
|
|
|
|
|
|
// GetDocumentStatusConvertedByHash 根据文档hash,查询已转换了的文档状态
|
|
|
func (m *DBModel) GetDocumentStatusConvertedByHash(hash []string) (hashMapDocuments map[string]Document) {
|
|
|
var (
|
|
|
tableDocument = Document{}.TableName()
|
|
|
tableAttachment = Attachment{}.TableName()
|
|
|
attachMapIndex = make(map[int64]int)
|
|
|
documentIds []int64
|
|
|
docs []Document
|
|
|
)
|
|
|
|
|
|
hashMapDocuments = make(map[string]Document)
|
|
|
sql := fmt.Sprintf(
|
|
|
"select a.hash,a.type_id from %s a left join %s d on a.type_id = d.id where a.hash in ? and d.status = ? group by a.hash",
|
|
|
tableAttachment, tableDocument,
|
|
|
)
|
|
|
|
|
|
var attachemnts []Attachment
|
|
|
err := m.db.Raw(sql, hash, DocumentStatusConverted).Find(&attachemnts).Error
|
|
|
if err != nil {
|
|
|
m.logger.Error("GetDocumentStatusConvertedByHash", zap.Error(err))
|
|
|
return
|
|
|
}
|
|
|
|
|
|
for idx, attachment := range attachemnts {
|
|
|
attachMapIndex[attachment.TypeId] = idx
|
|
|
documentIds = append(documentIds, attachment.TypeId)
|
|
|
}
|
|
|
|
|
|
if len(documentIds) == 0 {
|
|
|
return
|
|
|
}
|
|
|
|
|
|
m.db.Where("id in ?", documentIds).Find(&docs)
|
|
|
for _, doc := range docs {
|
|
|
hashMapDocuments[attachemnts[attachMapIndex[doc.Id]].Hash] = doc
|
|
|
}
|
|
|
return
|
|
|
}
|
|
|
|
|
|
// ConvertDocument 文档转换。如果err返回gorm.ErrRecordNotFound,表示已没有文档需要转换
|
|
|
// 1. 查询待转换的文档
|
|
|
// 2. 文档对应的md5 hash中,是否有已转换的文档,如果有,则直接关联和调整状态为已转换
|
|
|
// 3. 文档转PDF
|
|
|
// 4. PDF截取第一章图片作为封面
|
|
|
// 5. 根据允许最大的预览页面,将PDF转为svg,同时转gzip压缩,如果有需要的话
|
|
|
// 6. 提取PDF文本以及获取文档信息
|
|
|
// 7. 更新文档状态
|
|
|
func (m *DBModel) ConvertDocument() (err error) {
|
|
|
var document Document
|
|
|
err = m.db.Where("status in ?", []int{DocumentStatusPending, DocumentStatusRePending}).First(&document).Error
|
|
|
if err != nil {
|
|
|
if err != gorm.ErrRecordNotFound {
|
|
|
m.logger.Error("ConvertDocument", zap.Error(err))
|
|
|
}
|
|
|
return
|
|
|
}
|
|
|
defer func() {
|
|
|
m.SetDocumentConvertError(document.Id, err)
|
|
|
}()
|
|
|
// 文档转为PDF
|
|
|
cfg := m.GetConfigOfConverter()
|
|
|
m.SetDocumentStatus(document.Id, DocumentStatusConverting)
|
|
|
|
|
|
attachment := m.GetAttachmentByTypeAndTypeId(AttachmentTypeDocument, document.Id)
|
|
|
if attachment.Id == 0 { // 附件不存在
|
|
|
m.SetDocumentStatus(document.Id, DocumentStatusFailed)
|
|
|
if err != nil {
|
|
|
m.logger.Error("ConvertDocument", zap.Error(err))
|
|
|
}
|
|
|
return
|
|
|
}
|
|
|
|
|
|
localFile := strings.TrimLeft(attachment.Path, "./")
|
|
|
baseDir := strings.TrimSuffix(localFile, filepath.Ext(localFile))
|
|
|
cover := baseDir + "/cover.png"
|
|
|
|
|
|
if !cfg.EnableConvertRepeatedDocument && document.Status != DocumentStatusRePending {
|
|
|
_, errCover := os.Stat(cover)
|
|
|
hashMapDocs := m.GetDocumentStatusConvertedByHash([]string{attachment.Hash}) // 文档hash
|
|
|
if len(hashMapDocs) > 0 && errCover == nil { // 双重确认文档是否已转换成功:1. 存在相同hash的已转换的文档,2. 存在封面图片
|
|
|
m.logger.Info("ConvertDocument", zap.Bool("EnableConvertRepeatedDocument", cfg.EnableConvertRepeatedDocument), zap.String("hash", attachment.Hash), zap.Any("hashMapDocs", hashMapDocs))
|
|
|
// 已有文档转换成功,将hash相同的文档相关数据迁移到当前文档
|
|
|
sql := " UPDATE `%s` SET `description`= ? , `enable_gzip` = ?, `width` = ?, `height`= ?, `preview`= ?, `pages` = ?, `status` = ? WHERE status in ? and id in (select type_id from `%s` where `hash` = ? and `type` = ?)"
|
|
|
sql = fmt.Sprintf(sql, Document{}.TableName(), Attachment{}.TableName())
|
|
|
for hash, doc := range hashMapDocs {
|
|
|
err = m.db.Exec(sql,
|
|
|
doc.Description, doc.EnableGZIP, doc.Width, doc.Height, doc.Preview, doc.Pages, DocumentStatusConverted, []int{DocumentStatusPending, DocumentStatusConverting, DocumentStatusFailed}, hash, AttachmentTypeDocument,
|
|
|
).Error
|
|
|
if err != nil {
|
|
|
m.logger.Error("ConvertDocument", zap.Error(err))
|
|
|
return
|
|
|
}
|
|
|
}
|
|
|
return
|
|
|
}
|
|
|
}
|
|
|
|
|
|
timeout := 30 * time.Minute
|
|
|
if cfg.Timeout > 0 {
|
|
|
timeout = time.Duration(cfg.Timeout) * time.Minute
|
|
|
}
|
|
|
|
|
|
cvt := converter.NewConverter(m.logger, timeout)
|
|
|
defer cvt.Clean() // 清除缓存目录
|
|
|
dstPDF, err := cvt.ConvertToPDF(localFile)
|
|
|
if err != nil {
|
|
|
m.SetDocumentStatus(document.Id, DocumentStatusFailed)
|
|
|
m.logger.Error("ConvertDocument", zap.Error(err))
|
|
|
return
|
|
|
}
|
|
|
document.Pages, _ = cvt.CountPDFPages(dstPDF)
|
|
|
document.Preview = cfg.MaxPreview
|
|
|
if document.Pages < cfg.MaxPreview {
|
|
|
document.Preview = document.Pages
|
|
|
}
|
|
|
|
|
|
// PDF截取第一章图片作为封面(封面不是最重要的,期间出现错误,不影响文档转换)
|
|
|
pages, err := cvt.ConvertPDFToPNG(dstPDF, 1, 1)
|
|
|
if err != nil {
|
|
|
m.logger.Error("get pdf cover", zap.Error(err))
|
|
|
}
|
|
|
|
|
|
if len(pages) > 0 {
|
|
|
coverBig := baseDir + "/cover.big.png"
|
|
|
util.CopyFile(pages[0].PagePath, coverBig)
|
|
|
util.CopyFile(pages[0].PagePath, cover)
|
|
|
util.CropImage(cover, DocumentCoverWidth, DocumentCoverHeight)
|
|
|
document.Width, document.Height, _ = util.GetImageSize(coverBig) // 页面宽高
|
|
|
}
|
|
|
|
|
|
// PDF转为SVG
|
|
|
toPage := document.Pages
|
|
|
if cfg.MaxPreview > 0 {
|
|
|
toPage = cfg.MaxPreview
|
|
|
}
|
|
|
if toPage > document.Pages {
|
|
|
toPage = document.Pages
|
|
|
}
|
|
|
|
|
|
pages, err = cvt.ConvertPDFToPages(dstPDF, 1, toPage, &converter.OptionConvertPages{
|
|
|
EnableSVGO: cfg.EnableSVGO,
|
|
|
EnableGZIP: cfg.EnableGZIP,
|
|
|
Extension: cfg.Extension,
|
|
|
})
|
|
|
if err != nil {
|
|
|
m.SetDocumentStatus(document.Id, DocumentStatusFailed)
|
|
|
m.logger.Error("ConvertDocument", zap.Error(err))
|
|
|
return
|
|
|
}
|
|
|
|
|
|
ext := "." + cfg.Extension
|
|
|
if ext == ".svg" && cfg.EnableGZIP {
|
|
|
ext = ".gzip.svg"
|
|
|
}
|
|
|
|
|
|
for _, page := range pages {
|
|
|
dst := fmt.Sprintf(baseDir+"/%d%s", page.PageNum, ext)
|
|
|
m.logger.Debug("ConvertDocument CopyFile", zap.String("src", page.PagePath), zap.String("dst", dst))
|
|
|
errCopy := util.CopyFile(page.PagePath, dst)
|
|
|
if errCopy != nil {
|
|
|
m.logger.Error("ConvertDocument CopyFile", zap.Error(errCopy))
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 提取PDF文本以及获取文档信息
|
|
|
textFile, _ := cvt.ConvertPDFToTxt(dstPDF)
|
|
|
util.CopyFile(textFile, baseDir+"/content.txt")
|
|
|
|
|
|
// 读取文本内容,以提取关键字和摘要
|
|
|
if content, errRead := os.ReadFile(textFile); errRead == nil {
|
|
|
contentStr := string(content)
|
|
|
replacer := strings.NewReplacer("\r", " ", "\n", " ", "\t", " ")
|
|
|
document.Description = strings.TrimSpace(replacer.Replace(util.Substr(contentStr, 255)))
|
|
|
}
|
|
|
|
|
|
document.Status = DocumentStatusConverted
|
|
|
document.EnableGZIP = cfg.EnableGZIP
|
|
|
document.PreviewExt = ext
|
|
|
err = m.db.Select("description", "cover", "width", "height", "preview", "pages", "status", "enable_gzip", "preview_ext").Where("id = ?", document.Id).Updates(document).Error
|
|
|
if err != nil {
|
|
|
m.SetDocumentStatus(document.Id, DocumentStatusFailed)
|
|
|
m.logger.Error("ConvertDocument", zap.Error(err))
|
|
|
}
|
|
|
return
|
|
|
}
|
|
|
|
|
|
func (m *DBModel) SetDocumentStatus(documentId int64, status int) (err error) {
|
|
|
err = m.db.Model(&Document{}).Where("id = ?", documentId).Update("status", status).Error
|
|
|
if err != nil {
|
|
|
m.logger.Error("SetDocumentStatus", zap.Error(err))
|
|
|
}
|
|
|
return
|
|
|
}
|
|
|
|
|
|
// 设置文章推荐状态
|
|
|
func (m *DBModel) SetDocumentRecommend(documentIds []int64, typ int32) (err error) {
|
|
|
db := m.db.Model(&Document{}).Where("id in (?)", documentIds)
|
|
|
switch typ {
|
|
|
case 0: // 取消推荐
|
|
|
err = db.Update("recommend_at", nil).Error
|
|
|
case 1: // 推荐
|
|
|
err = db.Where("recommend_at IS NULL").Update("recommend_at", time.Now()).Error
|
|
|
case 2: // 置顶
|
|
|
err = db.Update("recommend_at", time.Now()).Error
|
|
|
}
|
|
|
if err != nil {
|
|
|
m.logger.Error("SetDocumentRecommend", zap.Error(err))
|
|
|
}
|
|
|
return
|
|
|
}
|
|
|
|
|
|
func (m *DBModel) CountDocument(status ...int) (count int64, err error) {
|
|
|
db := m.db.Model(&Document{})
|
|
|
if len(status) > 0 {
|
|
|
db = db.Where("status in (?)", status)
|
|
|
}
|
|
|
err = db.Count(&count).Error
|
|
|
if err != nil {
|
|
|
m.logger.Error("CountDocument", zap.Error(err))
|
|
|
}
|
|
|
return
|
|
|
}
|
|
|
|
|
|
func (m *DBModel) SetDocumentsCategory(documentId, categoryId []int64) (err error) {
|
|
|
tx := m.db.Begin()
|
|
|
defer func() {
|
|
|
if err != nil {
|
|
|
tx.Rollback()
|
|
|
} else {
|
|
|
tx.Commit()
|
|
|
}
|
|
|
}()
|
|
|
|
|
|
for _, id := range documentId {
|
|
|
// 1. 旧的文档分类,减少计数
|
|
|
var docCates []DocumentCategory
|
|
|
m.db.Model(&DocumentCategory{}).Where("document_id = ?", id).Find(&docCates)
|
|
|
for _, cate := range docCates {
|
|
|
err = tx.Model(&Category{}).Where("id = ?", cate.CategoryId).Update("doc_count", gorm.Expr("doc_count - ?", 1)).Error
|
|
|
if err != nil {
|
|
|
m.logger.Error("SetDocumentsCategory", zap.Error(err))
|
|
|
return
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 2. 删除旧的分类
|
|
|
err = tx.Model(&DocumentCategory{}).Where("document_id = ?", id).Delete(&DocumentCategory{}).Error
|
|
|
if err != nil {
|
|
|
m.logger.Error("SetDocumentsCategory", zap.Error(err))
|
|
|
return
|
|
|
}
|
|
|
|
|
|
// 3. 添加新的分类
|
|
|
docCates = []DocumentCategory{}
|
|
|
for _, cid := range categoryId {
|
|
|
docCates = append(docCates, DocumentCategory{
|
|
|
DocumentId: id,
|
|
|
CategoryId: cid,
|
|
|
})
|
|
|
}
|
|
|
err = tx.Create(&docCates).Error
|
|
|
if err != nil {
|
|
|
m.logger.Error("SetDocumentsCategory", zap.Error(err))
|
|
|
return
|
|
|
}
|
|
|
|
|
|
// 4. 更新文档分类统计
|
|
|
err = tx.Model(&Category{}).Where("id in (?)", categoryId).Update("doc_count", gorm.Expr("doc_count + ?", 1)).Error
|
|
|
if err != nil {
|
|
|
m.logger.Error("SetDocumentsCategory", zap.Error(err))
|
|
|
return
|
|
|
}
|
|
|
}
|
|
|
return
|
|
|
}
|