chore: add api

This commit is contained in:
deepzz0
2021-04-27 23:46:43 +08:00
parent a39e3aac3b
commit 4749b1e681
7 changed files with 469 additions and 74 deletions

208
pkg/cache/cache.go vendored
View File

@@ -7,6 +7,7 @@ import (
"fmt"
"sort"
"strings"
"sync"
"time"
"github.com/eiblog/eiblog/pkg/cache/render"
@@ -37,6 +38,7 @@ func init() {
}
// Ei init
Ei = &Cache{
lock: sync.Mutex{},
Store: store,
TagArticles: make(map[string]model.SortedArticles),
ArticlesMap: make(map[string]*model.Article),
@@ -52,6 +54,7 @@ func init() {
// Cache 整站缓存
type Cache struct {
lock sync.Mutex
store.Store
// load from db
@@ -68,44 +71,6 @@ type Cache struct {
ArticlesMap map[string]*model.Article // slug:article
}
// // LoadInsertAccount 读取或创建账户
// LoadInsertAccount(ctx context.Context, acct *model.Account) (*model.Account, error)
// // UpdateAccount 更新账户
// UpdateAccount(ctx context.Context, name string, fields map[string]interface{}) error
//
// // LoadInsertBlogger 读取或创建博客
// LoadInsertBlogger(ctx context.Context, blogger *model.Blogger) (*model.Blogger, error)
// // UpdateBlogger 更新博客
// UpdateBlogger(ctx context.Context, fields map[string]interface{}) error
//
// // InsertSeries 创建专题
// InsertSeries(ctx context.Context, series *model.Series) error
// // RemoveSeries 删除专题
// RemoveSeries(ctx context.Context, id int) error
// // UpdateSeries 更新专题
// UpdateSeries(ctx context.Context, id int, fields map[string]interface{}) error
// // LoadAllSeries 读取所有专题
// LoadAllSeries(ctx context.Context) (model.SortedSeries, error)
//
// // InsertArticle 创建文章
// InsertArticle(ctx context.Context, article *model.Article) error
// // RemoveArticle 硬删除文章
// RemoveArticle(ctx context.Context, id int) error
// // DeleteArticle 软删除文章,放入回收箱
// DeleteArticle(ctx context.Context, id int) error
// // CleanArticles 清理回收站文章
// CleanArticles(ctx context.Context) error
// // UpdateArticle 更新文章
// UpdateArticle(ctx context.Context, id int, fields map[string]interface{}) error
// // RecoverArticle 恢复文章到草稿
// RecoverArticle(ctx context.Context, id int) error
// // LoadAllArticle 读取所有文章
// LoadAllArticle(ctx context.Context) (model.SortedArticles, error)
// // LoadTrashArticles 读取回收箱
// LoadTrashArticles(ctx context.Context) (model.SortedArticles, error)
// // LoadDraftArticles 读取草稿箱
// LoadDraftArticles(ctx context.Context) (model.SortedArticles, error)
// PageArticles 文章翻页
func (c *Cache) PageArticles(page int, pageSize int) (prev,
next int, articles []*model.Article) {
@@ -146,6 +111,121 @@ func (c *Cache) PageArticles(page int, pageSize int) (prev,
//
// }
// DeleteArticles 删除文章
func (c *Cache) DeleteArticles(ids []int) error {
c.lock.Lock()
defer c.lock.Unlock()
for _, id := range ids {
article, idx := c.FindArticleByID(id)
// drop from linkedList
if article.Prev == nil && article.Next != nil {
article.Next.Prev = nil
} else if article.Prev != nil && article.Next == nil {
article.Prev.Next = nil
} else if article.Prev != nil && article.Next != nil {
article.Prev.Next = article.Next
article.Next.Prev = article.Prev
}
// drop from articles
c.Articles = append(c.Articles[:idx], c.Articles[idx+1:]...)
delete(c.ArticlesMap, article.Slug)
// set delete
err := c.UpdateArticle(context.Background(), id, map[string]interface{}{
"deletetime": time.Now(),
})
if err != nil {
return err
}
// drop from tags,series,archives
c.redelArticle(article)
}
return nil
}
// AddArticle 添加文章
func (c *Cache) AddArticle(article *model.Article) error {
err := c.InsertArticle(context.Background(), article)
if err != nil {
return err
}
// 正式发布文章
if !article.IsDraft {
defer render.GenerateExcerptMarkdown(article)
c.ArticlesMap[article.Slug] = article
c.Articles = append([]*model.Article{article}, c.Articles...)
sort.Sort(c.Articles)
article, idx := c.FindArticleByID(article.ID)
if idx == 0 && c.Articles[idx+1].ID >=
config.Conf.BlogApp.General.StartID {
article.Next = c.Articles[idx+1]
c.Articles[idx+1].Prev = article
} else if idx > 0 && c.Articles[idx-1].ID >=
config.Conf.BlogApp.General.StartID {
article.Prev = c.Articles[idx-1]
if c.Articles[idx-1].Next != nil {
article.Next = c.Articles[idx-1].Next
c.Articles[idx-1].Next.Prev = article
}
c.Articles[idx-1].Next = article
}
c.readdArticle(article, true)
}
return nil
}
// ReplaceArticle 替换文章
func (c *Cache) ReplaceArticle(oldArticle, newArticle *model.Article) {
c.ArticlesMap[newArticle.Slug] = newArticle
render.GenerateExcerptMarkdown(newArticle)
if newArticle.ID < config.Conf.BlogApp.General.StartID {
return
}
if oldArticle != nil {
article, idx := c.FindArticleByID(oldArticle.ID)
if article.Prev == nil && article.Next != nil {
article.Next.Prev = nil
} else if article.Prev != nil && article.Next == nil {
article.Prev.Next = nil
} else if article.Prev != nil && article.Next != nil {
article.Prev.Next = article.Next
article.Next.Prev = article.Prev
}
c.Articles = append(Ei.Articles[:idx], Ei.Articles[idx+1:]...)
c.redelArticle(article)
}
c.Articles = append(c.Articles, newArticle)
sort.Sort(c.Articles)
_, idx := c.FindArticleByID(newArticle.ID)
if idx == 0 && c.Articles[idx+1].ID >= config.Conf.BlogApp.General.StartID {
newArticle.Next = c.Articles[idx+1]
c.Articles[idx+1].Prev = newArticle
} else if idx > 0 && c.Articles[idx-1].ID >=
config.Conf.BlogApp.General.StartID {
newArticle.Prev = Ei.Articles[idx-1]
if c.Articles[idx-1].Next != nil {
newArticle.Next = Ei.Articles[idx-1].Next
c.Articles[idx-1].Next.Prev = newArticle
}
c.Articles[idx-1].Next = newArticle
}
c.readdArticle(newArticle, true)
}
// FindArticleByID 通过ID查找文章
func (c *Cache) FindArticleByID(id int) (*model.Article, int) {
for i, article := range c.Articles {
if article.ID == id {
return article, i
}
}
return nil, -1
}
// loadOrInit 读取数据或初始化
func (c *Cache) loadOrInit() error {
blogapp := config.Conf.BlogApp
@@ -219,7 +299,7 @@ func (c *Cache) loadOrInit() error {
if Ei.Articles[i+1].ID >= blogapp.General.StartID {
v.Next = Ei.Articles[i+1]
}
c.rebuildArticle(v, false)
c.readdArticle(v, false)
}
Ei.Articles = articles
// 重建专题与归档
@@ -228,8 +308,8 @@ func (c *Cache) loadOrInit() error {
return nil
}
// rebuildArticle 重建缓存tag、series、archive
func (c *Cache) rebuildArticle(article *model.Article, needSort bool) {
// readdArticle 添加文章到tag、series、archive
func (c *Cache) readdArticle(article *model.Article, needSort bool) {
// tag
tags := strings.Split(article.Tags, ",")
for _, tag := range tags {
@@ -270,6 +350,52 @@ func (c *Cache) rebuildArticle(article *model.Article, needSort bool) {
}
}
// redelArticle 从tag、series、archive删除文章
func (c *Cache) redelArticle(article *model.Article) {
// tag
tags := strings.Split(article.Tags, ",")
for _, tag := range tags {
for i, v := range c.TagArticles[tag] {
if v == article {
c.TagArticles[tag] = append(c.TagArticles[tag][0:i], c.TagArticles[tag][i+1:]...)
if len(c.TagArticles[tag]) == 0 {
delete(c.TagArticles, tag)
}
}
}
}
// serie
for i, serie := range c.Series {
if serie.ID == article.SerieID {
for j, v := range serie.Articles {
if v == article {
Ei.Series[i].Articles = append(Ei.Series[i].Articles[0:j],
Ei.Series[i].Articles[j+1:]...)
PagesCh <- PageSeries
break
}
}
}
}
// archive
for i, archive := range c.Archives {
ay, am, _ := archive.Time.Date()
if y, m, _ := article.CreatedAt.Date(); ay == y && am == m {
for j, v := range archive.Articles {
if v == article {
c.Archives[i].Articles = append(c.Archives[i].Articles[0:j],
c.Archives[i].Articles[j+1:]...)
if len(c.Archives[i].Articles) == 0 {
c.Archives = append(c.Archives[:i], c.Archives[i+1:]...)
}
PagesCh <- PageArchive
break
}
}
}
}
}
// regeneratePages 重新生成series,archive页面
func (c *Cache) regeneratePages() {
for {

View File

@@ -3,13 +3,17 @@ package admin
import (
"context"
"errors"
"fmt"
"net/http"
"strconv"
"time"
"github.com/eiblog/eiblog/pkg/cache"
"github.com/eiblog/eiblog/pkg/config"
"github.com/eiblog/eiblog/pkg/core/blog"
"github.com/eiblog/eiblog/pkg/internal"
"github.com/eiblog/eiblog/pkg/model"
"github.com/eiblog/eiblog/tools"
"github.com/gin-gonic/gin"
@@ -176,29 +180,229 @@ func handleAPIPassword(c *gin.Context) {
// handleAPIPostDelete 删除文章,移入回收箱
func handleAPIPostDelete(c *gin.Context) {
// var ids []int32
// for _, v := range c.PostFormArray("cid[]") {
// i, err := strconv.Atoi(v)
// if err != nil || int32(i) < config.Conf.BlogApp.General.StartID {
// responseNotice(c, NoticeNotice, "参数错误", "")
// return
// }
// ids = append(ids, int32(i))
// }
// err := DelArticles(ids...)
// if err != nil {
// logd.Error(err)
// responseNotice(c, NOTICE_NOTICE, err.Error(), "")
// return
// }
//
// // elasticsearch
// err = ElasticDelIndex(ids)
// if err != nil {
// logrus.Error("handleAPIPostDelete.")
// }
// // TODO disqus delete
// responseNotice(c, NoticeSuccess, "删除成功", "")
var ids []int
for _, v := range c.PostFormArray("cid[]") {
id, err := strconv.Atoi(v)
if err != nil || id < config.Conf.BlogApp.General.StartID {
responseNotice(c, NoticeNotice, "参数错误", "")
return
}
ids = append(ids, id)
}
err := cache.Ei.DeleteArticles(ids)
if err != nil {
logrus.Error("handleAPIPostDelete.DeleteArticles: ", err)
responseNotice(c, NoticeNotice, err.Error(), "")
return
}
// elasticsearch
err = internal.ElasticDelIndex(ids)
if err != nil {
logrus.Error("handleAPIPostDelete.ElasticDelIndex: ", err)
}
// TODO disqus delete
responseNotice(c, NoticeSuccess, "删除成功", "")
}
// handleAPIPostCreate 创建文章
func handleAPIPostCreate(c *gin.Context) {
var (
err error
do string
cid int
)
defer func() {
switch do {
case "auto": // 自动保存
if err != nil {
c.JSON(http.StatusOK, gin.H{"fail": 1, "time": time.Now().Format("15:04:05 PM"), "cid": cid})
return
}
c.JSON(http.StatusOK, gin.H{"success": 0, "time": time.Now().Format("15:04:05 PM"), "cid": cid})
case "save", "publish": // 草稿,发布
if err != nil {
responseNotice(c, NoticeNotice, err.Error(), "")
return
}
uri := "/admin/manage-draft"
if do == "publish" {
uri = "/admin/manage-posts"
}
c.Redirect(http.StatusFound, uri)
}
}()
do = c.PostForm("do") // auto or save or publish
slug := c.PostForm("slug")
title := c.PostForm("title")
text := c.PostForm("text")
date := parseLocationDate(c.PostForm("date"))
serie := c.PostForm("serie")
tag := c.PostForm("tags")
update := c.PostForm("update")
if slug == "" || title == "" || text == "" {
err = errors.New("参数错误")
return
}
serieid, _ := strconv.Atoi(serie)
article := &model.Article{
Title: title,
Content: text,
Slug: slug,
IsDraft: do != "publish",
Author: cache.Ei.Account.Username,
SerieID: serieid,
Tags: tag,
CreatedAt: date,
}
cid, err = strconv.Atoi(c.PostForm("cid"))
// 新文章
if err != nil || cid < 1 {
err = cache.Ei.AddArticle(article)
if err != nil {
logrus.Error("handleAPIPostCreate.InsertArticle: ", err)
return
}
if !article.IsDraft {
// 异步执行,快
go func() {
// elastic
internal.ElasticAddIndex(article)
// rss
internal.PingFunc(cache.Ei.Blogger.BTitle, slug)
// disqus
internal.ThreadCreate(article, cache.Ei.Blogger.BTitle)
}()
}
return
}
// 旧文章
article.ID = cid
artc, _ := cache.Ei.FindArticleByID(article.ID)
if artc != nil {
article.IsDraft = false
article.Count = artc.Count
article.UpdatedAt = artc.UpdatedAt
}
if update == "true" || update == "1" {
artc.UpdatedAt = time.Now()
}
// 数据库更新
err = cache.Ei.UpdateArticle(context.Background(), artc.ID, map[string]interface{}{
"title": article.Title,
"content": article.Content,
"serie_id": article.SerieID,
"tags": article.Tags,
"is_draft": article.IsDraft,
"updated_at": article.UpdatedAt,
"created_at": article.CreatedAt,
})
if err != nil {
logrus.Error("handleAPIPostCreate.UpdateArticle: ", err)
return
}
if !artc.IsDraft {
cache.Ei.ReplaceArticle(artc, article)
// 异步执行,快
go func() {
// elastic
internal.ElasticAddIndex(article)
// rss
internal.PingFunc(cache.Ei.Blogger.BTitle, slug)
// disqus
if artc == nil {
internal.ThreadCreate(article, cache.Ei.Blogger.BTitle)
}
}()
}
}
// handleAPISerieDelete 只能逐一删除,专题下不能有文章
func handleAPISerieDelete(c *gin.Context) {
for _, v := range c.PostFormArray("mid[]") {
id, err := strconv.Atoi(v)
if err != nil || id < 1 {
responseNotice(c, NoticeNotice, err.Error(), "")
return
}
for i, serie := range cache.Ei.Series {
if serie.ID == id {
if len(serie.Articles) > 0 {
logrus.Error("handleAPISerieDelete.failed: ")
responseNotice(c, NoticeNotice, "请删除该专题下的所有文章", "")
return
}
err = cache.Ei.RemoveSerie(context.Background(), id)
if err != nil {
logrus.Error("handleAPISerieDelete.RemoveSerie: ")
responseNotice(c, NoticeNotice, err.Error(), "")
return
}
cache.Ei.Series[i] = nil
cache.Ei.Series = append(cache.Ei.Series[:i], cache.Ei.Series[i+1:]...)
cache.PagesCh <- cache.PageSeries
break
}
}
}
responseNotice(c, NoticeSuccess, "删除成功", "")
}
// handleAPISerieCreate 添加专题,如果专题有提交 mid 即更新专题
func handleAPISerieCreate(c *gin.Context) {
name := c.PostForm("name")
slug := c.PostForm("slug")
desc := c.PostForm("description")
if name == "" || slug == "" || desc == "" {
responseNotice(c, NoticeNotice, "参数错误", "")
return
}
mid, err := strconv.Atoi(c.PostForm("mid"))
if err == nil && mid > 0 {
var serie *model.Serie
for _, v := range cache.Ei.Series {
if v.ID == mid {
serie = v
break
}
}
if serie == nil {
responseNotice(c, NoticeNotice, "专题不存在", "")
return
}
err = cache.Ei.UpdateSerie(context.Background(), mid, map[string]interface{}{
"slug": slug,
"name": name,
"desc": desc,
})
if err != nil {
logrus.Error("handleAPISerieCreate.UpdateSerie: ", err)
responseNotice(c, NoticeNotice, err.Error(), "")
return
}
} else {
err = cache.Ei.InsertSerie(context.Background(), &model.Serie{
Slug: slug,
Name: name,
Desc: desc,
})
if err != nil {
logrus.Error("handleAPISerieCreate.InsertSerie: ", err)
responseNotice(c, NoticeNotice, err.Error(), "")
return
}
}
responseNotice(c, NoticeSuccess, "操作成功", "")
}
// parseLocationDate 解析日期
func parseLocationDate(date string) time.Time {
t, err := time.ParseInLocation("2006-01-02 15:04", date, time.Local)
if err == nil {
return t
}
return time.Now()
}
func responseNotice(c *gin.Context, typ, content, hl string) {

View File

@@ -73,7 +73,7 @@ func handleAdminPost(c *gin.Context) {
params["Domain"] = config.Conf.BlogApp.Host
params["Series"] = cache.Ei.Series
var tags []T
for tag, _ := range cache.Ei.TagArticles {
for tag := range cache.Ei.TagArticles {
tags = append(tags, T{tag, tag})
}
str, _ := json.Marshal(tags)

View File

@@ -13,6 +13,8 @@ import (
"time"
"github.com/eiblog/eiblog/pkg/config"
"github.com/eiblog/eiblog/pkg/model"
"github.com/eiblog/eiblog/tools"
"github.com/sirupsen/logrus"
)
@@ -22,6 +24,9 @@ const (
SearchFilter = `"filter":{"bool":{"must":[%s]}}`
SearchTerm = `{"term":{"%s":"%s"}}`
SearchDate = `{"range":{"date":{"gte":"%s","lte": "%s","format": "yyyy-MM-dd||yyyy-MM||yyyy"}}}` // 2016-10||/M
ElasticIndex = "eiblog"
ElasticType = "article"
)
func init() {
@@ -30,7 +35,7 @@ func init() {
}
mappings := fmt.Sprintf(`{"mappings":{"%s":{"properties":{"content":{"analyzer":"ik_syno","search_analyzer":"ik_syno","term_vector":"with_positions_offsets","type":"string"},"date":{"index":"not_analyzed","type":"date"},"slug":{"type":"string"},"tag":{"index":"not_analyzed","type":"string"},"title":{"analyzer":"ik_syno","search_analyzer":"ik_syno","term_vector":"with_positions_offsets","type":"string"}}}}}`, "article")
err := createIndexAndMappings("eiblog", "article", []byte(mappings))
err := createIndexAndMappings(ElasticIndex, ElasticType, []byte(mappings))
if err != nil {
panic(err)
}
@@ -97,7 +102,40 @@ func ElasticSearch(query string, size, from int) (*searchIndexResult, error) {
if kw != "" {
dsl = strings.Replace(strings.Replace(`{"highlight":{"fields":{"content":{},"title":{}},"post_tags":["\u003c/b\u003e"],"pre_tags":["\u003cb\u003e"]},"query":{"dis_max":{"queries":[{"match":{"title":{"boost":4,"minimum_should_match":"50%","query":"$1"}}},{"match":{"content":{"boost":4,"minimum_should_match":"75%","query":"$1"}}},{"match":{"tag":{"boost":2,"minimum_should_match":"100%","query":"$1"}}},{"match":{"slug":{"boost":1,"minimum_should_match":"100%","query":"$1"}}}],"tie_breaker":0.3}},$2}`, "$1", kw, -1), "$2", fmt.Sprintf(SearchFilter, strings.Join(filter, ",")), -1)
}
return indexQueryDSL("article", "eiblog", size, from, []byte(dsl))
return indexQueryDSL(ElasticIndex, ElasticType, size, from, []byte(dsl))
}
// ElasticAddIndex 添加或更新索引
func ElasticAddIndex(article *model.Article) error {
if err := checkESConfig(); err != nil {
return err
}
tags := strings.Split(article.Tags, ",")
img := tools.PickFirstImage(article.Content)
mapping := map[string]interface{}{
"title": article.Title,
"content": tools.IgnoreHtmlTag(article.Content),
"slug": article.Slug,
"tag": tags,
"img": img,
"date": article.CreatedAt,
}
data, _ := json.Marshal(mapping)
return indexOrUpdateDocument(ElasticIndex, ElasticType, article.ID, data)
}
// ElasticDelIndex 删除索引
func ElasticDelIndex(ids []int) error {
if err := checkESConfig(); err != nil {
return err
}
var target []string
for _, id := range ids {
target = append(target, fmt.Sprint(id))
}
return deleteIndexDocument(ElasticIndex, ElasticType, target)
}
// indicesCreateResult 索引创建结果
@@ -139,7 +177,7 @@ func createIndexAndMappings(index, typ string, mappings []byte) error {
}
// indexOrUpdateDocument 创建或更新索引
func indexOrUpdateDocument(index, typ string, id int32, doc []byte) (err error) {
func indexOrUpdateDocument(index, typ string, id int, doc []byte) (err error) {
rawurl := fmt.Sprintf("%s/%s/%s/%d", config.Conf.ESHost, index, typ, id)
resp, err := httpPut(rawurl, doc)
if err != nil {

View File

@@ -17,5 +17,5 @@ type Account struct {
LoginIP string `gorm:"column:login_ip;default:null" bson:"login_ip"` // 最近登录IP
LoginUA string `gorm:"column:login_ua;default:null" bson:"login_ua"` // 最近登录IP
LoginAt time.Time `gorm:"column:login_at;default:now()" bson:"login_at"` // 最近登录时间
CreatedAt time.Time `gorm:"column:creatd_at;default:now()" bson:"created_at"` // 创建时间
CreatedAt time.Time `gorm:"column:created_at;default:now()" bson:"created_at"` // 创建时间
}

View File

@@ -7,6 +7,7 @@ import (
"io"
"io/ioutil"
"path"
"regexp"
"time"
)
@@ -102,3 +103,27 @@ var monthToDays = map[time.Month]int{
func isLeapYear(year int) bool {
return year%4 == 0 && (year%100 != 0 || year%400 == 0)
}
var regexpImg = regexp.MustCompile(`data-src="(.*?)"`)
// PickFirstImage 获取第一张图片
func PickFirstImage(html string) string {
sli := regexpImg.FindAllStringSubmatch(html, 1)
if len(sli) > 0 && len(sli[0]) > 1 {
return sli[0][1]
}
return ""
}
var (
regexpBrackets = regexp.MustCompile(`<[\S\s]+?>`)
regexpEnter = regexp.MustCompile(`\s+`)
)
// IgnoreHtmlTag 去掉 html tag
func IgnoreHtmlTag(src string) string {
// 去除所有尖括号内的HTML代码
src = regexpBrackets.ReplaceAllString(src, "")
// 去除换行符
return regexpEnter.ReplaceAllString(src, "")
}

View File

@@ -1,7 +1,9 @@
// Package tools provides ...
package tools
import "regexp"
import (
"regexp"
)
var regexpEmail = regexp.MustCompile(`^(\w)+([\.\-]\w+)*@(\w)+((\.\w+)+)$`)