feat: SSR 内容门控与积分钱包/签到抽奖
闭合积分解锁经济闭环:锁定壳与 unlock,以及 profile 钱包流水与每日签到/抽奖 PRG。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -12,7 +12,7 @@ var (
|
||||
)
|
||||
|
||||
// ComposeBodyToHTML 将发帖表单正文转为可消毒 HTML。
|
||||
// 若已含块级 HTML 标签则原样交 Sanitize;否则按纯文本/轻量 Markdown 转段落。
|
||||
// 若已含块级 HTML / 门控标签则原样交 Sanitize;否则按纯文本/轻量 Markdown 转段落。
|
||||
func ComposeBodyToHTML(raw string) string {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
@@ -22,7 +22,9 @@ func ComposeBodyToHTML(raw string) string {
|
||||
if strings.Contains(lower, "<p") || strings.Contains(lower, "<div") ||
|
||||
strings.Contains(lower, "<h1") || strings.Contains(lower, "<ul") ||
|
||||
strings.Contains(lower, "<ol") || strings.Contains(lower, "<pre") ||
|
||||
strings.Contains(lower, "<blockquote") {
|
||||
strings.Contains(lower, "<blockquote") ||
|
||||
strings.Contains(lower, "<members-only") || strings.Contains(lower, "<reply-only") ||
|
||||
strings.Contains(lower, "<points-only") {
|
||||
return raw
|
||||
}
|
||||
|
||||
@@ -86,6 +88,12 @@ func HTMLToComposePlain(htmlBody string) string {
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
lower := strings.ToLower(s)
|
||||
// 含门控块时保留 HTML,避免编辑丢失
|
||||
if strings.Contains(lower, "<members-only") || strings.Contains(lower, "<reply-only") ||
|
||||
strings.Contains(lower, "<points-only") {
|
||||
return s
|
||||
}
|
||||
// img → markdown
|
||||
imgRe := regexp.MustCompile(`(?i)<img[^>]+src="([^"]+)"[^>]*>`)
|
||||
s = imgRe.ReplaceAllString(s, "")
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
@@ -54,10 +54,17 @@ func redactGatedBlocks(html string, re *regexp.Regexp, tag string) string {
|
||||
}
|
||||
length := gatedContentLength(inner)
|
||||
gate := "login"
|
||||
title := "登录后可见"
|
||||
hint := "请登录后查看隐藏内容"
|
||||
if tag == "reply-only" {
|
||||
gate = "reply"
|
||||
title = "回复后可见"
|
||||
hint = "参与评论后即可查看"
|
||||
}
|
||||
return `<` + tag + ` data-gate="` + gate + `" data-locked="true" data-length="` + strconv.Itoa(length) + `"></` + tag + `>`
|
||||
return fmt.Sprintf(
|
||||
`<%s data-gate="%s" data-locked="true" data-length="%d"><div class="j13-gate" data-gate-shell="%s"><p class="j13-gate__title">%s</p><p class="j13-gate__meta">约 %d 字</p><p class="j13-gate__hint">%s</p></div></%s>`,
|
||||
tag, gate, length, gate, title, length, hint, tag,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ func postContentHTMLPolicy() *bluemonday.Policy {
|
||||
p.AllowAttrs("colspan", "rowspan").OnElements("th", "td")
|
||||
p.AllowAttrs(
|
||||
"data-locked", "data-length", "data-gate", "data-cost", "data-block-key",
|
||||
"data-gate-shell",
|
||||
"data-code-copy", "data-code-fold", "data-lang", "data-full",
|
||||
"data-code-style", "data-line-numbers", "data-collapsed",
|
||||
"data-line-count", "data-lineno-digits",
|
||||
|
||||
@@ -100,7 +100,10 @@ func RedactPointsOnlyHTML(html string, unlocked map[string]bool) string {
|
||||
cost = 1
|
||||
}
|
||||
length := gatedContentLength(inner)
|
||||
return fmt.Sprintf(`<points-only data-gate="points" data-cost="%d" data-block-key="%s" data-locked="true" data-length="%d"></points-only>`, cost, key, length)
|
||||
return fmt.Sprintf(
|
||||
`<points-only data-gate="points" data-cost="%d" data-block-key="%s" data-locked="true" data-length="%d"><div class="j13-gate j13-gate--points" data-gate-shell="points"><p class="j13-gate__title">积分可见</p><p class="j13-gate__meta">需 %d 积分 · 约 %d 字</p><p class="j13-gate__hint">登录后可解锁</p></div></points-only>`,
|
||||
cost, key, length, cost, length,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user