This commit is contained in:
xing 2022-10-06 21:33:04 +08:00
parent 7bdc47559a
commit be5accaa10
13 changed files with 361 additions and 15 deletions

View File

@ -26,6 +26,7 @@ var postListIdsCache *cache.MapCache[string, PostIds]
var searchPostIdsCache *cache.MapCache[string, PostIds] var searchPostIdsCache *cache.MapCache[string, PostIds]
var maxPostIdCache *cache.SliceCache[uint64] var maxPostIdCache *cache.SliceCache[uint64]
var TotalRaw int var TotalRaw int
var usersCache *cache.MapCache[uint64, models.WpUsers]
func InitActionsCommonCache() { func InitActionsCommonCache() {
archivesCaches = &Arch{ archivesCaches = &Arch{
@ -53,6 +54,9 @@ func InitActionsCommonCache() {
postCommentCaches = cache.NewMapCacheByFn[uint64, []models.WpComments](postComments, vars.Conf.CommentsCacheTime) postCommentCaches = cache.NewMapCacheByFn[uint64, []models.WpComments](postComments, vars.Conf.CommentsCacheTime)
maxPostIdCache = cache.NewSliceCache[uint64](getMaxPostId, vars.Conf.MaxPostIdCacheTime) maxPostIdCache = cache.NewSliceCache[uint64](getMaxPostId, vars.Conf.MaxPostIdCacheTime)
usersCache = cache.NewMapCacheByBatchFn[uint64, models.WpUsers](getUsers, time.Hour)
usersCache.SetCacheFunc(getUser)
} }
func ClearCache() { func ClearCache() {
@ -147,8 +151,11 @@ func postComments(args ...any) ([]models.WpComments, error) {
}, nil, 0) }, nil, 0)
} }
func RecentComments(ctx context.Context) (r []models.WpComments) { func RecentComments(ctx context.Context, n int) (r []models.WpComments) {
r, err := recentCommentsCaches.GetCache(ctx, time.Second) r, err := recentCommentsCaches.GetCache(ctx, time.Second)
if len(r) > n {
r = r[0:n]
}
logs.ErrPrintln(err, "get recent comment") logs.ErrPrintln(err, "get recent comment")
return return
} }
@ -158,7 +165,7 @@ func recentComments(...any) (r []models.WpComments, err error) {
{"post_status", "publish"}, {"post_status", "publish"},
}, "comment_ID,comment_author,comment_post_ID,post_title", "", models.SqlBuilder{{"comment_date_gmt", "desc"}}, models.SqlBuilder{ }, "comment_ID,comment_author,comment_post_ID,post_title", "", models.SqlBuilder{{"comment_date_gmt", "desc"}}, models.SqlBuilder{
{"a", "left join", "wp_posts b", "a.comment_post_ID=b.ID"}, {"a", "left join", "wp_posts b", "a.comment_post_ID=b.ID"},
}, 5) }, 10)
} }
func GetContextPost(ctx context.Context, id uint64, date time.Time) (prev, next models.WpPosts, err error) { func GetContextPost(ctx context.Context, id uint64, date time.Time) (prev, next models.WpPosts, err error) {
@ -177,7 +184,7 @@ func getPostContext(arg ...any) (r PostContext, err error) {
{"post_date", ">", t.Format("2006-01-02 15:04:05")}, {"post_date", ">", t.Format("2006-01-02 15:04:05")},
{"post_status", "in", ""}, {"post_status", "in", ""},
{"post_type", "post"}, {"post_type", "post"},
}, "ID,post_title,post_password", nil, []any{"publish", "private"}) }, "ID,post_title,post_password", nil, []any{"publish"})
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
err = nil err = nil
} }
@ -188,7 +195,7 @@ func getPostContext(arg ...any) (r PostContext, err error) {
{"post_date", "<", t.Format("2006-01-02 15:04:05")}, {"post_date", "<", t.Format("2006-01-02 15:04:05")},
{"post_status", "in", ""}, {"post_status", "in", ""},
{"post_type", "post"}, {"post_type", "post"},
}, "ID,post_title", models.SqlBuilder{{"post_date", "desc"}}, []any{"publish", "private"}) }, "ID,post_title", models.SqlBuilder{{"post_date", "desc"}}, []any{"publish"})
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
err = nil err = nil
} }
@ -239,15 +246,18 @@ func categories(...any) (terms []models.WpTermsMy, err error) {
return return
} }
func RecentPosts(ctx context.Context) (r []models.WpPosts) { func RecentPosts(ctx context.Context, n int) (r []models.WpPosts) {
r, err := recentPostsCaches.GetCache(ctx, time.Second) r, err := recentPostsCaches.GetCache(ctx, time.Second)
if n < len(r) {
r = r[:n]
}
logs.ErrPrintln(err, "get recent post") logs.ErrPrintln(err, "get recent post")
return return
} }
func recentPosts(...any) (r []models.WpPosts, err error) { func recentPosts(...any) (r []models.WpPosts, err error) {
r, err = models.Find[models.WpPosts](models.SqlBuilder{{ r, err = models.Find[models.WpPosts](models.SqlBuilder{{
"post_type", "post", "post_type", "post",
}, {"post_status", "publish"}}, "ID,post_title,post_password", "", models.SqlBuilder{{"post_date", "desc"}}, nil, 5) }, {"post_status", "publish"}}, "ID,post_title,post_password", "", models.SqlBuilder{{"post_date", "desc"}}, nil, 10)
for i, post := range r { for i, post := range r {
if post.PostPassword != "" { if post.PostPassword != "" {
PasswordProjectTitle(&r[i]) PasswordProjectTitle(&r[i])

28
actions/common/users.go Normal file
View File

@ -0,0 +1,28 @@
package common
import (
"github.com/gin-gonic/gin"
"github/fthvgb1/wp-go/logs"
"github/fthvgb1/wp-go/models"
"time"
)
func getUsers(...any) (m map[uint64]models.WpUsers, err error) {
m = make(map[uint64]models.WpUsers)
r, err := models.Find[models.WpUsers](nil, "*", "", nil, nil, 0)
for _, user := range r {
m[user.Id] = user
}
return
}
func getUser(a ...any) (r models.WpUsers, err error) {
id := a[0].(uint64)
return models.FindOneById[models.WpUsers](id)
}
func GetUser(ctx *gin.Context, uid uint64) models.WpUsers {
r, err := usersCache.GetCache(ctx, uid, time.Second, uid)
logs.ErrPrintln(err, "get user", uid)
return r
}

View File

@ -27,10 +27,10 @@ func Detail(c *gin.Context) {
hh := detailHandler{ hh := detailHandler{
c, c,
} }
recent := common.RecentPosts(c) recent := common.RecentPosts(c, 5)
archive := common.Archives() archive := common.Archives()
categoryItems := common.Categories(c) categoryItems := common.Categories(c)
recentComments := common.RecentComments(c) recentComments := common.RecentComments(c, 5)
var h = gin.H{ var h = gin.H{
"title": models.Options["blogname"], "title": models.Options["blogname"],
"options": models.Options, "options": models.Options,

90
actions/feed.go Normal file
View File

@ -0,0 +1,90 @@
package actions
import (
"bytes"
"fmt"
"github.com/gin-gonic/gin"
"github/fthvgb1/wp-go/actions/common"
"github/fthvgb1/wp-go/helper"
"github/fthvgb1/wp-go/logs"
"github/fthvgb1/wp-go/models"
"github/fthvgb1/wp-go/plugins"
"github/fthvgb1/wp-go/templates"
"html"
"html/template"
"net/http"
"strings"
"time"
"unicode/utf8"
)
func Feed() func(ctx *gin.Context) {
fs, err := template.ParseFS(templates.TemplateFs, "feed/feed.gohtml")
if err != nil {
panic(err)
}
return func(c *gin.Context) {
c.Header("Content-Type", "application/rss+xml; charset=UTF-8")
c.Header("Cache-Control", "no-cache, must-revalidate, max-age=0")
c.Header("Expires", "Wed, 11 Jan 1984 05:00:00 GMT")
//c.Header("Last-Modified", "false")
c.Header("ETag", helper.StringMd5("gmt"))
r := common.RecentPosts(c, 10)
ids := helper.SliceMap(r, func(t models.WpPosts) uint64 {
return t.Id
})
posts, err := common.GetPostsByIds(c, ids)
if err != nil {
c.Status(http.StatusInternalServerError)
c.Abort()
return
}
type p struct {
models.WpPosts
Cates string
CommentLink string
Username string
Category string
Link string
Description string
Date string
}
rr := helper.SliceMap(posts, func(t models.WpPosts) p {
common.PasswordProjectTitle(&t)
if t.PostPassword != "" {
common.PasswdProjectContent(&t)
}
l := ""
if t.CommentStatus == "open" {
l = fmt.Sprintf("%s/p/%d#comments", models.Options["siteurl"], t.Id)
}
user := common.GetUser(c, t.PostAuthor)
content := plugins.DigestRaw(t.PostContent, utf8.RuneCountInString(t.PostContent), t.Id)
t.PostContent = content
return p{
WpPosts: t,
Cates: strings.Join(t.Categories, "、"),
CommentLink: l,
Username: user.DisplayName,
Link: fmt.Sprintf("%s/p/%d", models.Options["siteurl"], t.Id),
Description: plugins.DigestRaw(content, 55, t.Id),
Date: t.PostDateGmt.Format(time.RFC1123Z),
}
})
h := gin.H{
"posts": rr,
"options": models.Options,
"now": time.Now().Format(time.RFC1123Z),
}
var buf bytes.Buffer
err = fs.Execute(&buf, h)
if err != nil {
logs.ErrPrintln(err, "parse template")
return
}
c.String(http.StatusOK, html.UnescapeString(buf.String()))
}
}

View File

@ -133,8 +133,6 @@ func (h *indexHandle) parseParams() {
h.setTitleLR(helper.StrJoin(`"`, s, `"`, "的搜索结果"), models.Options["blogname"]) h.setTitleLR(helper.StrJoin(`"`, s, `"`, "的搜索结果"), models.Options["blogname"])
h.search = s h.search = s
h.scene = plugins.Search h.scene = plugins.Search
} else {
h.status = append(h.status, "private")
} }
p := h.c.Query("paged") p := h.c.Query("paged")
if p == "" { if p == "" {
@ -162,9 +160,9 @@ func Index(c *gin.Context) {
h := newIndexHandle(c) h := newIndexHandle(c)
h.parseParams() h.parseParams()
archive := common.Archives() archive := common.Archives()
recent := common.RecentPosts(c) recent := common.RecentPosts(c, 5)
categoryItems := common.Categories(c) categoryItems := common.Categories(c)
recentComments := common.RecentComments(c) recentComments := common.RecentComments(c, 5)
ginH := gin.H{ ginH := gin.H{
"options": models.Options, "options": models.Options,
"recentPosts": recent, "recentPosts": recent,

60
helper/html.go Normal file
View File

@ -0,0 +1,60 @@
package helper
import (
"strings"
)
var entitlesMap = map[int][]string{
EntCompat: {"&amp;", "&quot;", "&lt;", "&gt;"},
EntQuotes: {"&amp;", "&quot;", "&#039;", "&lt;", "&gt;"},
EntNoQuotes: {"&amp;", "&lt;", "&gt;"},
EntSpace: {"&nbsp;"},
}
var unEntitlesMap = map[int][]string{
EntCompat: {"&", "\"", "<", ">"},
EntQuotes: {"&", "\"", "'", "<", ">"},
EntNoQuotes: {"&", "<", ">"},
EntSpace: {" "},
}
const (
EntCompat = 1
EntQuotes = 2
EntNoQuotes = 4
EntSpace = 8
)
func htmlSpecialChars(text string, flags int) string {
r, ok := unEntitlesMap[flags]
e := entitlesMap[flags]
if !ok {
r = unEntitlesMap[EntCompat]
e = entitlesMap[EntCompat]
}
if flags&EntSpace == EntSpace {
r = append(r, unEntitlesMap[EntSpace]...)
e = append(e, entitlesMap[EntSpace]...)
}
for i, entitle := range r {
text = strings.Replace(text, entitle, e[i], -1)
}
return text
}
func htmlSpecialCharsDecode(text string, flags int) string {
r, ok := entitlesMap[flags]
u := unEntitlesMap[flags]
if !ok {
r = entitlesMap[EntCompat]
u = unEntitlesMap[EntCompat]
}
if flags&EntSpace == EntSpace {
r = append(r, entitlesMap[EntSpace]...)
u = append(u, unEntitlesMap[EntSpace]...)
}
for i, entitle := range r {
text = strings.Replace(text, entitle, u[i], -1)
}
return text
}

92
helper/html_test.go Normal file
View File

@ -0,0 +1,92 @@
package helper
import "testing"
func Test_htmlSpecialChars(t *testing.T) {
type args struct {
text string
flags int
}
tests := []struct {
name string
args args
want string
}{
{
name: "t1",
args: args{text: "<a href='test'>Test</a>", flags: EntQuotes},
want: "&lt;a href=&#039;test&#039;&gt;Test&lt;/a&gt;",
}, {
name: "t2",
args: args{text: "<a href='test'>Test</a>", flags: EntCompat},
want: "&lt;a href='test'&gt;Test&lt;/a&gt;",
}, {
name: "t3",
args: args{text: "<a href='test'>T est</a>", flags: EntCompat | EntSpace},
want: "&lt;a&nbsp;href='test'&gt;T&nbsp;est&lt;/a&gt;",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := htmlSpecialChars(tt.args.text, tt.args.flags); got != tt.want {
t.Errorf("htmlSpecialChars() = %v, want %v", got, tt.want)
}
})
}
}
func Test_htmlSpecialCharsDecode(t *testing.T) {
type args struct {
text string
flags int
}
tests := []struct {
name string
args args
want string
}{
{
name: "t1",
args: args{
text: "&lt;a href='test'&gt;Test&lt;/a&gt;",
flags: EntCompat,
},
want: "<a href='test'>Test</a>",
}, {
name: "t2",
args: args{
text: "&lt;a href=&#039;test&#039;&gt;Test&lt;/a&gt;",
flags: EntQuotes,
},
want: "<a href='test'>Test</a>",
}, {
name: "t3",
args: args{
text: "<p>this -&gt; &quot;</p>\n",
flags: EntNoQuotes,
},
want: "<p>this -> &quot;</p>\n",
}, {
name: "t4",
args: args{
text: "<p>this -&gt; &quot;</p>\n",
flags: EntCompat,
},
want: "<p>this -> \"</p>\n",
}, {
name: "t5",
args: args{
text: "<p>this -&gt;&nbsp;&quot;</p>\n",
flags: EntCompat | EntSpace,
},
want: "<p>this -> \"</p>\n",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := htmlSpecialCharsDecode(tt.args.text, tt.args.flags); got != tt.want {
t.Errorf("htmlSpecialCharsDecode() = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -213,7 +213,7 @@ func SimplePagination[T Model](where ParseWhere, fields, group string, page, pag
return return
} }
func FindOneById[T Model](id int) (T, error) { func FindOneById[T Model, I ~int | ~uint64 | ~int64 | ~int32](id I) (T, error) {
var r T var r T
sql := fmt.Sprintf("select * from `%s` where `%s`=?", r.Table(), r.PrimaryKey()) sql := fmt.Sprintf("select * from `%s` where `%s`=?", r.Table(), r.PrimaryKey())
err := db.Db.Get(&r, sql, id) err := db.Db.Get(&r, sql, id)

24
models/wp_users.go Normal file
View File

@ -0,0 +1,24 @@
package models
import "time"
type WpUsers struct {
Id uint64 `gorm:"column:ID" db:"ID" json:"ID"`
UserLogin string `gorm:"column:user_login" db:"user_login" json:"user_login"`
UserPass string `gorm:"column:user_pass" db:"user_pass" json:"user_pass"`
UserNicename string `gorm:"column:user_nicename" db:"user_nicename" json:"user_nicename"`
UserEmail string `gorm:"column:user_email" db:"user_email" json:"user_email"`
UserUrl string `gorm:"column:user_url" db:"user_url" json:"user_url"`
UserRegistered time.Time `gorm:"column:user_registered" db:"user_registered" json:"user_registered"`
UserActivationKey string `gorm:"column:user_activation_key" db:"user_activation_key" json:"user_activation_key"`
UserStatus int `gorm:"column:user_status" db:"user_status" json:"user_status"`
DisplayName string `gorm:"column:display_name" db:"display_name" json:"display_name"`
}
func (u WpUsers) Table() string {
return "wp_users"
}
func (u WpUsers) PrimaryKey() string {
return "ID"
}

View File

@ -52,6 +52,7 @@ func SetupRouter() *gin.Engine {
r.GET("/p/date/:year/:month/page/:page", actions.Index) r.GET("/p/date/:year/:month/page/:page", actions.Index)
r.POST("/login", actions.Login) r.POST("/login", actions.Login)
r.GET("/p/:id", actions.Detail) r.GET("/p/:id", actions.Detail)
r.GET("/feed", actions.Feed())
if helper.IsContainInArr(gin.Mode(), []string{gin.DebugMode, gin.TestMode}) { if helper.IsContainInArr(gin.Mode(), []string{gin.DebugMode, gin.TestMode}) {
pprof.Register(r, "dev/pprof") pprof.Register(r, "dev/pprof")
} }

View File

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
xmlns:content="http://purl.org/rss/1.0/modules/content/"
xmlns:wfw="http://wellformedweb.org/CommentAPI/"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:atom="http://www.w3.org/2005/Atom"
xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
>
<channel>
<title>{{ .options.blogname }}</title>
<atom:link href="{{.options.home}}/feed" rel="self" type="application/rss+xml"/>
<link>{{.options.siteurl}}/</link>
<description>{{.options.blogdescription}}</description>
<lastBuildDate>{{.now}}</lastBuildDate>
<language>zh-CN</language>
<sy:updatePeriod>
hourly
</sy:updatePeriod>
<sy:updateFrequency>
1
</sy:updateFrequency>
<generator>https://wordpress.org/?v=6.0.2</generator>
{{range $k,$v := .posts}}
<item>
<title>{{$v.PostTitle}}</title>
<link>{{$v.Link}}</link>
<comments>{{ $v.CommentLink}}</comments>
<dc:creator><![CDATA[{{$v.Username}}]]></dc:creator>
<pubDate>{{$v.Date}}</pubDate>
<category><![CDATA[{{$v.Cates}}]]></category>
<guid isPermaLink="false">{{$v.Guid}}</guid>
<description><![CDATA[{{$v.Description}}]]></description>
<content:encoded><![CDATA[{{$v.PostContent}}]]></content:encoded>
{{if gt $v.CommentCount 0 }}
<wfw:commentRss>{{$v.CommentLink}}</wfw:commentRss>
<slash:comments>{{$v.CommentCount}}</slash:comments>
{{end}}
</item>
{{end}}
</channel>
</rss>

View File

@ -58,7 +58,6 @@
<aside id="meta-2" class="widget widget_meta"><h2 class="widget-title">其他操作</h2> <aside id="meta-2" class="widget widget_meta"><h2 class="widget-title">其他操作</h2>
<nav aria-label="其他操作"> <nav aria-label="其他操作">
<ul> <ul>
<li><a href="/wp-login.php">登录</a></li>
<li><a href="/feed">条目feed</a></li> <li><a href="/feed">条目feed</a></li>
<li><a href="/comments/feed">评论feed</a></li> <li><a href="/comments/feed">评论feed</a></li>
</ul> </ul>

View File

@ -8,7 +8,7 @@ import (
"path/filepath" "path/filepath"
) )
//go:embed posts layout //go:embed posts layout feed
var TemplateFs embed.FS var TemplateFs embed.FS
type FsTemplate struct { type FsTemplate struct {