加强了多个组件的预览百分比处理。

在 API 中新增了章节专属的预览百分比,并更新了相关模型和处理器。
改进了阅读场景下确定有效预览百分比的逻辑。
更新了小程序配置,加入了新的阅读页面入口。
This commit is contained in:
Alex-larget
2026-03-25 17:21:55 +08:00
parent d8362bc7a9
commit ae7402fafa
9 changed files with 233 additions and 82 deletions

View File

@@ -39,12 +39,17 @@ function getContentParseConfig() {
}
/** 补全 mentionDisplay避免旧数据无字段昵称去空白防「@ 名」 */
/** 付费墙「已阅读 x%」:与章节接口 previewPercent 一致(未全文解锁时),缺省 20 */
/** 试读比例:优先 data.previewPercent章节私有否则顶层 previewPercent全局 unpaid_preview_percent),缺省 20 */
function normalizePreviewPercent(res) {
const p = res && res.previewPercent
const n = typeof p === 'number' ? Math.round(p) : parseInt(String(p), 10)
if (!isNaN(n) && n >= 1 && n <= 100) return n
return 20
if (!res) return 20
const tryNum = (v) => {
const n = typeof v === 'number' ? Math.round(v) : parseInt(String(v), 10)
if (!isNaN(n) && n >= 1 && n <= 100) return n
return undefined
}
const inner = res.data && res.data.previewPercent
const outer = res.previewPercent
return tryNum(inner) ?? tryNum(outer) ?? 20
}
function normalizeMentionSegments(segments) {
@@ -93,7 +98,7 @@ Page({
// 阅读进度
readingProgress: 0,
/** 未解锁付费墙展示:与 /api/miniprogram/book/chapter/* 的 previewPercent 对齐 */
/** 未解锁付费墙:合并 data.previewPercent章节与顶层 previewPercent(全局) */
previewPercent: 20,
showPaywall: false,

View File

@@ -23,6 +23,13 @@
"condition": {
"miniprogram": {
"list": [
{
"name": "88888888",
"pathName": "pages/read/read",
"query": "mid=219",
"scene": null,
"launchMode": "default"
},
{
"name": "开发登录",
"pathName": "pages/dev-login/dev-login",

View File

@@ -20,6 +20,8 @@ export interface SectionItem {
payCount?: number
hotScore?: number
hotRank?: number
/** 章节试读%覆盖(与列表接口 previewPercent 一致) */
previewPercent?: number | null
}
export interface ChapterItem {

View File

@@ -74,6 +74,8 @@ interface SectionListItem {
payCount?: number
hotScore?: number
hotRank?: number
/** 章节级试读比例覆盖,未设则走全局「未付费预览比例」 */
previewPercent?: number | null
}
interface Section {
@@ -88,6 +90,7 @@ interface Section {
payCount?: number
hotScore?: number
hotRank?: number
previewPercent?: number | null
}
interface Chapter {
@@ -114,6 +117,17 @@ interface SectionOrder {
payTime?: string
}
/** 解析接口/列表中的试读比例(兼容数字、字符串、部分代理下划线字段) */
function parsePreviewPercentInput(raw: unknown): number | undefined {
if (raw === null || raw === undefined) return undefined
if (typeof raw === 'number' && Number.isFinite(raw)) return Math.round(raw)
if (typeof raw === 'string' && raw.trim() !== '') {
const n = Number(raw.trim().replace(/%/g, ''))
if (Number.isFinite(n)) return Math.round(n)
}
return undefined
}
interface EditingSection {
id: string
originalId?: string
@@ -125,7 +139,8 @@ interface EditingSection {
isNew?: boolean
isPinned?: boolean
hotScore?: number
previewPercent?: number
/** undefined=未改不写库null=清空用全局;数字=章节覆盖 */
previewPercent?: number | null
editionStandard?: boolean
editionPremium?: boolean
}
@@ -159,6 +174,7 @@ function buildTree(sections: SectionListItem[], hotRankMap: Map<string, number>,
payCount: s.payCount ?? 0,
hotScore: s.hotScore ?? 0,
hotRank: hotRankMap.get(s.id) ?? 0,
previewPercent: parsePreviewPercentInput(s.previewPercent),
})
}
const parts = Array.from(partMap.values()).map((p) => ({
@@ -823,12 +839,24 @@ export function ContentPage() {
section.mid != null && section.mid > 0
? `/api/db/book?action=read&mid=${section.mid}`
: `/api/db/book?action=read&id=${encodeURIComponent(section.id)}`
const data = await get<{ success?: boolean; section?: { title?: string; price?: number; content?: string; editionStandard?: boolean; editionPremium?: boolean }; error?: string }>(
url,
)
const data = await get<{
success?: boolean
section?: {
title?: string
price?: number
content?: string
editionStandard?: boolean
editionPremium?: boolean
previewPercent?: number | null
}
error?: string
}>(url)
if (data?.success && data.section) {
const sec = data.section as { isNew?: boolean; editionStandard?: boolean; editionPremium?: boolean }
const sec = data.section as Record<string, unknown>
const isPremium = sec.editionPremium === true
const fromRead =
parsePreviewPercentInput(sec.previewPercent) ?? parsePreviewPercentInput(sec.preview_percent)
const fromTree = parsePreviewPercentInput(section.previewPercent)
setEditingSection({
id: section.id,
originalId: section.id,
@@ -837,10 +865,11 @@ export function ContentPage() {
content: data.section.content ?? '',
filePath: section.filePath,
isFree: section.isFree || section.price === 0,
isNew: sec.isNew ?? section.isNew,
isNew: (sec.isNew as boolean | undefined) ?? section.isNew,
isPinned: pinnedSectionIds.includes(section.id),
hotScore: section.hotScore ?? 0,
editionStandard: isPremium ? false : (sec.editionStandard ?? true),
previewPercent: fromRead ?? fromTree ?? undefined,
editionStandard: isPremium ? false : ((sec.editionStandard as boolean | undefined) ?? true),
editionPremium: isPremium,
})
} else {
@@ -855,6 +884,7 @@ export function ContentPage() {
isNew: section.isNew,
isPinned: pinnedSectionIds.includes(section.id),
hotScore: section.hotScore ?? 0,
previewPercent: parsePreviewPercentInput(section.previewPercent),
editionStandard: true,
editionPremium: false,
})
@@ -871,6 +901,7 @@ export function ContentPage() {
content: '',
filePath: section.filePath,
isFree: section.isFree,
previewPercent: parsePreviewPercentInput(section.previewPercent),
})
} finally {
setIsLoadingContent(false)
@@ -892,22 +923,27 @@ export function ContentPage() {
const originalId = editingSection.originalId || editingSection.id
const idChanged = editingSection.id !== originalId
const saveBody: Record<string, unknown> = {
id: originalId,
...(idChanged ? { newId: editingSection.id } : {}),
title: editingSection.title,
price: editingSection.isFree ? 0 : editingSection.price,
content,
isFree: editingSection.isFree || editingSection.price === 0,
isNew: editingSection.isNew,
hotScore: editingSection.hotScore,
editionStandard: editingSection.editionPremium ? false : (editingSection.editionStandard ?? true),
editionPremium: editingSection.editionPremium ?? false,
saveToFile: true,
}
if (editingSection.previewPercent === null) {
saveBody.previewPercent = null
} else if (typeof editingSection.previewPercent === 'number' && Number.isFinite(editingSection.previewPercent)) {
saveBody.previewPercent = editingSection.previewPercent
}
const res = await put<{ success?: boolean; error?: string }>(
'/api/db/book',
{
id: originalId,
...(idChanged ? { newId: editingSection.id } : {}),
title: editingSection.title,
price: editingSection.isFree ? 0 : editingSection.price,
content,
isFree: editingSection.isFree || editingSection.price === 0,
isNew: editingSection.isNew,
hotScore: editingSection.hotScore,
previewPercent: editingSection.previewPercent ?? null,
editionStandard: editingSection.editionPremium ? false : (editingSection.editionStandard ?? true),
editionPremium: editingSection.editionPremium ?? false,
saveToFile: true,
},
saveBody,
{ timeout: SAVE_REQUEST_TIMEOUT },
)
const effectiveId = idChanged ? editingSection.id : originalId
@@ -2107,10 +2143,19 @@ export function ContentPage() {
max={100}
className="bg-[#0a1628] border-gray-700 text-white"
placeholder={`全局 ${previewPercent}%`}
value={editingSection.previewPercent ?? ''}
value={editingSection.previewPercent != null ? String(editingSection.previewPercent) : ''}
onChange={(e) => {
const v = e.target.value === '' ? undefined : Math.min(100, Math.max(0, Number(e.target.value)))
setEditingSection({ ...editingSection, previewPercent: v })
const raw = e.target.value.trim()
if (raw === '') {
setEditingSection({ ...editingSection, previewPercent: null })
return
}
const n = Number(raw)
if (!Number.isFinite(n)) return
setEditingSection({
...editingSection,
previewPercent: Math.min(100, Math.max(1, Math.round(n))),
})
}}
/>
</div>
@@ -2404,15 +2449,17 @@ export function ContentPage() {
<div
key={result.id}
className="p-3 rounded-lg bg-[#162840] hover:bg-[#1a3050] cursor-pointer transition-colors"
onClick={() =>
onClick={() => {
const row = sectionsList.find((x) => x.id === result.id)
handleReadSection({
id: result.id,
mid: result.mid,
title: result.title,
price: result.price ?? 1,
filePath: '',
previewPercent: row?.previewPercent,
})
}
}}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
@@ -2564,7 +2611,16 @@ export function ContentPage() {
variant="ghost"
size="sm"
className="text-gray-500 hover:text-[#38bdac] h-6 px-1"
onClick={() => handleReadSection({ id: s.id, mid: s.mid, title: s.title, price: s.price, filePath: '' })}
onClick={() =>
handleReadSection({
id: s.id,
mid: s.mid,
title: s.title,
price: s.price,
filePath: '',
previewPercent: s.previewPercent,
})
}
title="编辑文章"
>
<Edit3 className="w-3 h-3" />

View File

@@ -58,14 +58,14 @@ func sortChaptersByNaturalID(list []model.Chapter) {
var allChaptersSelectCols = []string{
"mid", "id", "part_id", "part_title", "chapter_id", "chapter_title",
"section_title", "word_count", "is_free", "price", "sort_order", "status",
"is_new", "edition_standard", "edition_premium", "hot_score", "created_at", "updated_at",
"is_new", "edition_standard", "edition_premium", "hot_score", "preview_percent", "created_at", "updated_at",
}
// chapterMetaCols 章节详情元数据(不含 content用于 content 缓存命中时的轻量查询
var chapterMetaCols = []string{
"mid", "id", "part_id", "part_title", "chapter_id", "chapter_title",
"section_title", "word_count", "is_free", "price", "sort_order", "status",
"is_new", "edition_standard", "edition_premium", "hot_score", "created_at", "updated_at",
"is_new", "edition_standard", "edition_premium", "hot_score", "preview_percent", "created_at", "updated_at",
}
// allChaptersCache 内存缓存,减轻 DB 压力30 秒 TTL
@@ -643,6 +643,17 @@ func getUnpaidPreviewPercent(db *gorm.DB) int {
return 20
}
// effectivePreviewPercent 章节 preview_percent 优先1~100否则用全局 unpaid_preview_percent
func effectivePreviewPercent(db *gorm.DB, ch *model.Chapter) int {
if ch != nil && ch.PreviewPercent != nil {
p := *ch.PreviewPercent
if p >= 1 && p <= 100 {
return p
}
}
return getUnpaidPreviewPercent(db)
}
// previewContent 取内容的前 percent%(不少于 100 字,上限 500 字),并追加省略提示
func previewContent(content string, percent int) string {
total := utf8.RuneCountInString(content)
@@ -712,12 +723,13 @@ func findChapterAndRespond(c *gin.Context, whereFn func(*gorm.DB) *gorm.DB) {
isPremium := ch.EditionPremium != nil && *ch.EditionPremium
hasFullAccess := isFree || checkUserChapterAccess(db, userID, ch.ID, isPremium)
var returnContent string
var unpaidPreviewPercent int // 未解锁时试读比例system_config.unpaid_preview_percent);已解锁时不写入响应
// 未解锁:正文截取用「章节覆盖 全局」;响应里顶层 previewPercent 仅表示全局默认data.previewPercent 表示章节私有model omitempty
var effectiveUnpaidPreviewPercent int
if hasFullAccess {
returnContent = ch.Content
} else {
unpaidPreviewPercent = getUnpaidPreviewPercent(db)
returnContent = previewContent(ch.Content, unpaidPreviewPercent)
effectiveUnpaidPreviewPercent = effectivePreviewPercent(db, &ch)
returnContent = previewContent(ch.Content, effectiveUnpaidPreviewPercent)
}
// data 中的 content 必须与外层 content 一致,避免泄露完整内容给未授权用户
@@ -740,7 +752,7 @@ func findChapterAndRespond(c *gin.Context, whereFn func(*gorm.DB) *gorm.DB) {
"hasFullAccess": hasFullAccess,
}
if !hasFullAccess {
out["previewPercent"] = unpaidPreviewPercent
out["previewPercent"] = getUnpaidPreviewPercent(db)
}
// 文章详情内直接输出上一篇/下一篇,省去单独请求
if list := getOrderedChapterList(); len(list) > 0 {

View File

@@ -41,7 +41,7 @@ func naturalLessSectionID(a, b string) bool {
var listSelectCols = []string{
"id", "mid", "section_title", "price", "is_free", "is_new",
"part_id", "part_title", "chapter_id", "chapter_title", "sort_order",
"hot_score", "updated_at",
"hot_score", "preview_percent", "updated_at",
}
// sectionListItem 与前端 SectionListItem 一致(小写驼峰)
@@ -60,7 +60,8 @@ type sectionListItem struct {
ClickCount int64 `json:"clickCount"` // 阅读次数reading_progress
PayCount int64 `json:"payCount"` // 付款笔数orders.product_type=section
HotScore float64 `json:"hotScore"` // 热度积分(加权计算)
IsPinned bool `json:"isPinned,omitempty"` // 是否置顶(仅 ranking 返回)
IsPinned bool `json:"isPinned,omitempty"` // 是否置顶(仅 ranking 返回)
PreviewPercent *int `json:"previewPercent,omitempty"`
}
// computeSectionListWithHotScore 计算章节列表(含 hotScore保持 sort_order 顺序,供 章节管理 树使用
@@ -264,19 +265,20 @@ func computeSectionsWithHotScore(db *gorm.DB, setPinned bool) ([]sectionListItem
hot = float64(r.HotScore)
}
item := sectionListItem{
ID: r.ID,
MID: r.MID,
Title: r.SectionTitle,
Price: price,
IsFree: r.IsFree,
IsNew: r.IsNew,
PartID: r.PartID,
PartTitle: r.PartTitle,
ChapterID: r.ChapterID,
ChapterTitle: r.ChapterTitle,
ClickCount: readCnt,
PayCount: payCnt,
HotScore: hot,
ID: r.ID,
MID: r.MID,
Title: r.SectionTitle,
Price: price,
IsFree: r.IsFree,
IsNew: r.IsNew,
PartID: r.PartID,
PartTitle: r.PartTitle,
ChapterID: r.ChapterID,
ChapterTitle: r.ChapterTitle,
ClickCount: readCnt,
PayCount: payCnt,
HotScore: hot,
PreviewPercent: r.PreviewPercent,
}
if setPinned {
item.IsPinned = pinnedSet[r.ID]
@@ -286,6 +288,42 @@ func computeSectionsWithHotScore(db *gorm.DB, setPinned bool) ([]sectionListItem
return sections, nil
}
// dbBookReadSectionOut 管理端 read 详情previewPercent 必须始终出现在 JSONnull=走全局),避免 gin.H+nil 被序列化省略
type dbBookReadSectionOut struct {
ID string `json:"id"`
Title string `json:"title"`
Price float64 `json:"price"`
Content string `json:"content"`
IsNew *bool `json:"isNew,omitempty"`
PartID string `json:"partId"`
PartTitle string `json:"partTitle"`
ChapterID string `json:"chapterId"`
ChapterTitle string `json:"chapterTitle"`
EditionStandard *bool `json:"editionStandard,omitempty"`
EditionPremium *bool `json:"editionPremium,omitempty"`
PreviewPercent *int `json:"previewPercent"` // 禁止 omitempty与 chapters 表 preview_percent 对齐
}
func chapterToReadSectionOut(ch *model.Chapter, price float64) dbBookReadSectionOut {
if ch == nil {
return dbBookReadSectionOut{Price: price}
}
return dbBookReadSectionOut{
ID: ch.ID,
Title: ch.SectionTitle,
Price: price,
Content: ch.Content,
IsNew: ch.IsNew,
PartID: ch.PartID,
PartTitle: ch.PartTitle,
ChapterID: ch.ChapterID,
ChapterTitle: ch.ChapterTitle,
EditionStandard: ch.EditionStandard,
EditionPremium: ch.EditionPremium,
PreviewPercent: ch.PreviewPercent,
}
}
// DBBookAction GET/POST/PUT /api/db/book
func DBBookAction(c *gin.Context) {
db := database.DB()
@@ -336,19 +374,7 @@ func DBBookAction(c *gin.Context) {
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"section": gin.H{
"id": ch.ID,
"title": ch.SectionTitle,
"price": price,
"content": ch.Content,
"isNew": ch.IsNew,
"partId": ch.PartID,
"partTitle": ch.PartTitle,
"chapterId": ch.ChapterID,
"chapterTitle": ch.ChapterTitle,
"editionStandard": ch.EditionStandard,
"editionPremium": ch.EditionPremium,
},
"section": chapterToReadSectionOut(&ch, price),
})
return
}
@@ -371,19 +397,7 @@ func DBBookAction(c *gin.Context) {
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"section": gin.H{
"id": ch.ID,
"title": ch.SectionTitle,
"price": price,
"content": ch.Content,
"isNew": ch.IsNew,
"partId": ch.PartID,
"partTitle": ch.PartTitle,
"chapterId": ch.ChapterID,
"chapterTitle": ch.ChapterTitle,
"editionStandard": ch.EditionStandard,
"editionPremium": ch.EditionPremium,
},
"section": chapterToReadSectionOut(&ch, price),
})
return
case "section-orders":
@@ -536,6 +550,7 @@ func DBBookAction(c *gin.Context) {
ChapterID string `json:"chapterId"`
ChapterTitle string `json:"chapterTitle"`
HotScore *float64 `json:"hotScore"`
PreviewPercent nullablePreviewPercentJSON `json:"previewPercent"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "请求体无效"})
@@ -703,6 +718,20 @@ func DBBookAction(c *gin.Context) {
if body.ChapterTitle != "" {
updates["chapter_title"] = body.ChapterTitle
}
if body.PreviewPercent.Set {
if body.PreviewPercent.Val == nil {
updates["preview_percent"] = nil
} else {
p := *body.PreviewPercent.Val
if p < 1 {
p = 1
}
if p > 100 {
p = 100
}
updates["preview_percent"] = p
}
}
var existing model.Chapter
err = db.Where("id = ?", body.ID).First(&existing).Error
if err == gorm.ErrRecordNotFound {
@@ -748,6 +777,16 @@ func DBBookAction(c *gin.Context) {
if body.IsNew != nil {
ch.IsNew = body.IsNew
}
if body.PreviewPercent.Set && body.PreviewPercent.Val != nil {
p := *body.PreviewPercent.Val
if p < 1 {
p = 1
}
if p > 100 {
p = 100
}
ch.PreviewPercent = &p
}
if err := db.Create(&ch).Error; err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
return
@@ -812,6 +851,26 @@ func DBBookAction(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "不支持的请求方法"})
}
// nullablePreviewPercentJSON 区分JSON 未传 key不改 preview_percent、null清空用全局、数字章节覆盖
type nullablePreviewPercentJSON struct {
Set bool
Val *int
}
func (n *nullablePreviewPercentJSON) UnmarshalJSON(data []byte) error {
n.Set = true
if string(data) == "null" {
n.Val = nil
return nil
}
var v int
if err := json.Unmarshal(data, &v); err != nil {
return err
}
n.Val = &v
return nil
}
type reorderItem struct {
ID string `json:"id"`
PartID string `json:"partId"`

View File

@@ -37,7 +37,7 @@ func H5ReadPage(c *gin.Context) {
return
}
percent := getUnpaidPreviewPercent(db)
percent := effectivePreviewPercent(db, &ch)
preview := h5PreviewContent(ch.Content, percent)
title := ch.SectionTitle

View File

@@ -22,7 +22,7 @@ type Chapter struct {
EditionStandard *bool `gorm:"column:edition_standard" json:"editionStandard,omitempty"` // 是否属于普通版
EditionPremium *bool `gorm:"column:edition_premium" json:"editionPremium,omitempty"` // 是否属于增值版
HotScore float64 `gorm:"column:hot_score;type:decimal(10,2);default:0" json:"hotScore"` // 热度分(加权计算),用于排名算法
PreviewPercent *int `gorm:"column:preview_percent" json:"previewPercent,omitempty"` // 章节级预览比例(%)nil 表示使用全局设置
PreviewPercent *int `gorm:"column:preview_percent" json:"previewPercent,omitempty"` // 章节私有试读%;小程序章节接口写在 data 内nil=未覆盖,用响应顶层 previewPercent全局
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
}

View File

@@ -136,3 +136,13 @@
{"level":"debug","timestamp":"2026-03-22T07:50:38+08:00","caller":"kernel/baseClient.go:459","content":"------------------response content:HTTP/1.1 200 OK\r\nContent-Length: 252\r\nCache-Control: no-cache, must-revalidate\r\nConnection: keep-alive\r\nContent-Language: zh-CN\r\nContent-Type: application/json; charset=utf-8\r\nDate: Sat, 21 Mar 2026 23:50:38 GMT\r\nKeep-Alive: timeout=8\r\nRequest-Id: 08CEDDFCCD0610AD0118D681ECAE0120CEAF33288A71-0\r\nServer: nginx\r\nWechatpay-Nonce: e895de036317f70f8a5ef1490b5884ee\r\nWechatpay-Serial: 5F2543BF58239A4EB68FA4433DF1438A88B34B16\r\nWechatpay-Signature: X1OSk9wn/q6XdK6FjA5v1YeLnyzenGPiSs4pMMRgFrNWJInZBG4vmUkq6O06sb7BYJSsTW92d2IE3BLxEC4pvUKKcwmtwB/2viZT6tD7Pb17rsJSCvnDFjp9Y/TGo96wSHO6DOWtDB0xrMJdDTlV0eTWj/HbdFS7SK6aPNn6XEVJ0C7vMfVrVHKyPzPzDpIqr/VSLeDAcVwHm4vF902+5eadg1aHRxUhfgb3SFWhO/isdo52xvEhdzBAdQErz+6ys0D1YKGEOZSG+qQLG4dfG8bCJHeN3SOWbzioWVSVVDyZZdKE4yP7myFefRZbuxlNtQLPkOlBWpL2d0nmXpunxg==\r\nWechatpay-Signature-Type: WECHATPAY2-SHA256-RSA2048\r\nWechatpay-Timestamp: 1774137038\r\nX-Content-Type-Options: nosniff\r\n\r\n{\"amount\":{\"payer_currency\":\"CNY\",\"total\":198000},\"appid\":\"wxb8bbb2b10dec74aa\",\"mchid\":\"1318592501\",\"out_trade_no\":\"MP20260322071744757265\",\"promotion_detail\":[],\"scene_info\":{\"device_id\":\"\"},\"trade_state\":\"NOTPAY\",\"trade_state_desc\":\"订单未支付\"}"}
{"level":"debug","timestamp":"2026-03-22T07:50:38+08:00","caller":"kernel/baseClient.go:457","content":"GET https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/MP20260322071754905280?mchid=1318592501 request header: { Content-Type:application/jsonAuthorization:WECHATPAY2-SHA256-RSA2048 mchid=\"1318592501\",nonce_str=\"JbRwjKfwQUYuQnb5ETPS70hKiLxrv7X8\",timestamp=\"1774137038\",serial_no=\"4A1DB62CD5C9BE0B6FC51C30621D6F99686E75C5\",signature=\"TGLUEBAA4Wz5DEkvYeSFtVhAP1Mq0UiJF/g2wHNU985N++AEoASPmeSr2Q9k9XohoqUBVywm1TgL2hWWP4Nl6Okwi2Y8wm8sJTKeXZdf19MkFqiGk6IuCROV4pv0FBxEAJsqFE46tEwJVYh5mFTId6f4K9ois1IMDE0LtLdJGnZkHSLtILmkQ/8OvyWI1JMOCyJUWwAAoS7kpw9rkOYAZBUHOHZdfT3Za0upxGOGWIXwhHpciFg4biboKTaa0Ez7imOvRzkMopsPWvrjp+HwABMyNv654TvVfivrWT9T8prSUp26Ts8yycyzaR6SxxP8FeuqAJzWMUBm4FVe/i2FhA==\"Accept:*/*} request body:"}
{"level":"debug","timestamp":"2026-03-22T07:50:38+08:00","caller":"kernel/baseClient.go:459","content":"------------------response content:HTTP/1.1 200 OK\r\nContent-Length: 252\r\nCache-Control: no-cache, must-revalidate\r\nConnection: keep-alive\r\nContent-Language: zh-CN\r\nContent-Type: application/json; charset=utf-8\r\nDate: Sat, 21 Mar 2026 23:50:38 GMT\r\nKeep-Alive: timeout=8\r\nRequest-Id: 08CEDDFCCD0610F40418C681ECAE0120F4D60E28C1AD03-0\r\nServer: nginx\r\nWechatpay-Nonce: 1b75baf8b8fc4289a9f71eec8fb6657a\r\nWechatpay-Serial: 5F2543BF58239A4EB68FA4433DF1438A88B34B16\r\nWechatpay-Signature: bmr7I8PgmS6OrdYsik56QOvqPnLMD8RMFQb7YpN2ocUsdQhwpYhtWsZWmU1SRoKuQy50vLIgZz5LNWqxOoP9wRChiOKOrTHWpUNtG8ClDLb8EyfM9JH9iP7vF0pcIOu9iWOzdHCo7rYUSTcO0Q75uqaRLdnlmgniaanOqULPvpVQFvgVIfemiGO9ZPljrlxbebwkRo1MBBPxG0ktTqgRpaw6K1/iw/EceuGfXVp2X75t7KC+3SY4HVMMUyTn5LsVZvBPmsCF1z+l72vd8PtSnwIB9GjXsaMnDjaZ3tjx/FrKv1AdKYM6TRYImGtbqdsgBqGOc83tkBOmweh86rfHFw==\r\nWechatpay-Signature-Type: WECHATPAY2-SHA256-RSA2048\r\nWechatpay-Timestamp: 1774137038\r\nX-Content-Type-Options: nosniff\r\n\r\n{\"amount\":{\"payer_currency\":\"CNY\",\"total\":198000},\"appid\":\"wxb8bbb2b10dec74aa\",\"mchid\":\"1318592501\",\"out_trade_no\":\"MP20260322071754905280\",\"promotion_detail\":[],\"scene_info\":{\"device_id\":\"\"},\"trade_state\":\"NOTPAY\",\"trade_state_desc\":\"订单未支付\"}"}
{"level":"debug","timestamp":"2026-03-25T16:30:01+08:00","caller":"kernel/accessToken.go:381","content":"GET https://api.weixin.qq.com/cgi-bin/token?appid=wxb8bbb2b10dec74aa&grant_type=client_credential&neededText=&secret=3c1fb1f63e6e052222bbcead9d07fe0c request header: { Accept:*/*} "}
{"level":"debug","timestamp":"2026-03-25T16:30:01+08:00","caller":"kernel/accessToken.go:383","content":"------------------response content:HTTP/1.1 200 OK\r\nContent-Length: 174\r\nConnection: keep-alive\r\nContent-Type: application/json; encoding=utf-8\r\nDate: Wed, 25 Mar 2026 08:30:02 GMT\r\n\r\n{\"access_token\":\"102_1nVe9nouwUQVRneYKgpBAthfO7lK8j0pU5MWSww9YXeSYQZJhdAuT9rFG0RVjeUFOW4WM3NkgKn7IgR0MutKQQGkTayM1LKAa4RSY7eHCSF2FmaSNPZRgNd1Lz0LWMaAGAALB\",\"expires_in\":7200}"}
{"level":"debug","timestamp":"2026-03-25T16:30:01+08:00","caller":"kernel/baseClient.go:457","content":"GET https://api.weixin.qq.com/sns/jscode2session?access_token=102_1nVe9nouwUQVRneYKgpBAthfO7lK8j0pU5MWSww9YXeSYQZJhdAuT9rFG0RVjeUFOW4WM3NkgKn7IgR0MutKQQGkTayM1LKAa4RSY7eHCSF2FmaSNPZRgNd1Lz0LWMaAGAALB&appid=wxb8bbb2b10dec74aa&grant_type=authorization_code&js_code=0f3cMtFa1bTPpL0w9wHa1IoZul2cMtF6&secret=3c1fb1f63e6e052222bbcead9d07fe0c request header: { Accept:*/*} "}
{"level":"debug","timestamp":"2026-03-25T16:30:01+08:00","caller":"kernel/baseClient.go:459","content":"------------------response content:HTTP/1.1 200 OK\r\nContent-Length: 82\r\nConnection: keep-alive\r\nContent-Type: text/plain\r\nDate: Wed, 25 Mar 2026 08:30:02 GMT\r\n\r\n{\"session_key\":\"FdEpG3ungTOgwwMCuHAHlw==\",\"openid\":\"ogpTW5fmXRGNpoUbXB3UEqnVe5Tg\"}"}
{"level":"debug","timestamp":"2026-03-25T16:30:02+08:00","caller":"kernel/baseClient.go:457","content":"POST https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi request header: { Content-Type:application/jsonAuthorization:WECHATPAY2-SHA256-RSA2048 mchid=\"1318592501\",nonce_str=\"PQPUqlvtqZBJa7f4OEm2rJigUWQBsMFH\",timestamp=\"1774427402\",serial_no=\"4A1DB62CD5C9BE0B6FC51C30621D6F99686E75C5\",signature=\"AqC8kRxYySQmpJBMFtsVH8bwRdpCoajalW7v4vBbmhkFoZT7w+Bk+aXv6AEN9rNMM+qVmxezzZiC8EnIGMDpIij5aweAvBW5uVAhMiRon7YzC/WiWeTUq3LAiJTuVx33GGsNmuKHaQeNe2nlA0q0Wra6ICXPQnyBPBW0ZDxFzSYhFZTukqAupeU7YHjtFaWtytlbogCyQ8E6FHgML/F0qKgQnpqe3T0TALYpeVJig2MlVnN8Y+l+OpTZ3wlyVHGGZSDNyaifdHoiQtd0k2aanjqV2uC3yRxAdd2o3PF+fUKU48wgnHwpKYyu7rEJzFaARkYGoJ2+niGV6dl2h2ZNHQ==\"Accept:*/*} request body:"}
{"level":"debug","timestamp":"2026-03-25T16:30:02+08:00","caller":"kernel/baseClient.go:459","content":"------------------response content:HTTP/1.1 200 OK\r\nContent-Length: 52\r\nCache-Control: no-cache, must-revalidate\r\nConnection: keep-alive\r\nContent-Language: zh-CN\r\nContent-Type: application/json; charset=utf-8\r\nDate: Wed, 25 Mar 2026 08:30:03 GMT\r\nKeep-Alive: timeout=8\r\nRequest-Id: 088BBA8ECE0610EC0218DAD28C5820AEE20728A3D802-0\r\nServer: nginx\r\nWechatpay-Nonce: ce934611f13cac0d6d90aa0d3bb0781d\r\nWechatpay-Serial: 5F2543BF58239A4EB68FA4433DF1438A88B34B16\r\nWechatpay-Signature: w/tqQQPfl4fd1mOKZh1NOuYa5Qg2e1Pn5mMxttHdfRHebaBGdZNkIK/qOoVGEuJj7rMbZtESR6V+HuuBWY0pVFkMjzC+y5s55SEKGGWjC2y31fCMCYUdOT3gATuLaWKiiUDNnko9mSOy9T65wOIWwlI6WsXQTrJ/yDElAquzzp3Ba5QnlA1wveC5346omiorCViWd9vW4SKQylWpqk3jb7JA5CHzlX0U+i2Ug3gY/t+ZJgtyQbumHoFgtPo4dZo6nSzjvgfxc8gRtdvcTktPlCcTe/TQuEvEAn2H6gSXL8U/3rggkiT7vm9dcBL80V7gBKZZl1OUUoe8LdWoI3qAGw==\r\nWechatpay-Signature-Type: WECHATPAY2-SHA256-RSA2048\r\nWechatpay-Timestamp: 1774427403\r\nX-Content-Type-Options: nosniff\r\n\r\n{\"prepay_id\":\"wx25163003573437b216d90c6158f23e0001\"}"}
{"level":"debug","timestamp":"2026-03-25T16:33:45+08:00","caller":"kernel/baseClient.go:457","content":"GET https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/MP20260325163002440000?mchid=1318592501 request header: { Content-Type:application/jsonAuthorization:WECHATPAY2-SHA256-RSA2048 mchid=\"1318592501\",nonce_str=\"ajWu7GhGuFo0buylmPOJSLtfgnCe9XGe\",timestamp=\"1774427625\",serial_no=\"4A1DB62CD5C9BE0B6FC51C30621D6F99686E75C5\",signature=\"Ql6/s2BujcCIWPE910c8Hzxp+KHSKvMFFVUxW5IAHeAGS9XJDfouSAH0Gwbvi5fQaQ+vZiLxD6Z2Rd6TQpqzT3iaugmu81tpCyhqIFX2wajXCOw6RhDXRa07slXLS9uO1ZtGsnwEeC+ok4QQeP8rzPlCZi8E3ehcB/agu070M86PuZ2GpL2Rl8qtLCYW2dlxEJEQSgKWVQsE/MdcwaAdsxqND1Ul83vpr9H1ixvm9WyzqJN33xeAegl85ydOh6WF5RBfcBFcNReaIzO6+pSXP1/+fc3IhNLKmwliGWFI5Dr+KgL5aN7OpblJU/27XvDVry598sCqkYkhN2iROV3EUA==\"Accept:*/*} request body:"}
{"level":"debug","timestamp":"2026-03-25T16:33:45+08:00","caller":"kernel/baseClient.go:459","content":"------------------response content:HTTP/1.1 200 OK\r\nContent-Length: 250\r\nCache-Control: no-cache, must-revalidate\r\nConnection: keep-alive\r\nContent-Language: zh-CN\r\nContent-Type: application/json; charset=utf-8\r\nDate: Wed, 25 Mar 2026 08:33:46 GMT\r\nKeep-Alive: timeout=8\r\nRequest-Id: 08EABB8ECE06109F04189082F8AF0120D8D90328CF49-0\r\nServer: nginx\r\nWechatpay-Nonce: b8048d342513c2da6196bba1b71a40ee\r\nWechatpay-Serial: 5F2543BF58239A4EB68FA4433DF1438A88B34B16\r\nWechatpay-Signature: QJ5LpLXHxdHU7s9+ui0+yNZwe3hiYYlia0Phc8t4wHjbA5fxu5HuC6EB7KsfswfL67B+yORhspup6wI23H3IuUK6BP9JZ24zUIs2eoo5ONNmrQIO9vppAA3Q8je/zPKjIlmIkVibiXo4k/OL3Ub1aSRaR4gglKKjpMXZH943y374Z1fY4zyOPwGrg3fMHVtHLHvsJf/7QOfRE1VGlqqEkgvAdE/+hYA0xGFPjauXFTUqmTUnwL9vLMiv9V1HETbhsTfkphZ7dtJTIe087frV8lP/C2mQU4UALc3J2EqNpOE4scMByijkT9dayev5SsaC+DVxkg4NtSHUA2sb9nkMwQ==\r\nWechatpay-Signature-Type: WECHATPAY2-SHA256-RSA2048\r\nWechatpay-Timestamp: 1774427626\r\nX-Content-Type-Options: nosniff\r\n\r\n{\"amount\":{\"payer_currency\":\"CNY\",\"total\":3000},\"appid\":\"wxb8bbb2b10dec74aa\",\"mchid\":\"1318592501\",\"out_trade_no\":\"MP20260325163002440000\",\"promotion_detail\":[],\"scene_info\":{\"device_id\":\"\"},\"trade_state\":\"NOTPAY\",\"trade_state_desc\":\"订单未支付\"}"}
{"level":"debug","timestamp":"2026-03-25T16:38:46+08:00","caller":"kernel/baseClient.go:457","content":"GET https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/MP20260325163002440000?mchid=1318592501 request header: { Content-Type:application/jsonAuthorization:WECHATPAY2-SHA256-RSA2048 mchid=\"1318592501\",nonce_str=\"hkXSXB4Qzr7zDbF1hxMm726B7x8CxPR2\",timestamp=\"1774427925\",serial_no=\"4A1DB62CD5C9BE0B6FC51C30621D6F99686E75C5\",signature=\"LxjtaHJLKqRGnoNqcpkjeGBHaCUs9RUXRp/EQcP4nhDep65UoYVOpFDEWEKMiQtUKL0hPB33NSU9hQeH6WjMvquCbsszExcnIX8INNuXcA3HfKWKqZaZug3GCICCjF8Qz7brY/raeaTT4W5rkgA9Js7G3llTgGVLmHntOdegVIXtDIEjq5a134MmwhjImi5acXp6gfET88Zes+RPIhiuk13Vj+FQH1k4LlqfVh5O68RVB7zEwx1CPbhrwCNiOVNVVYhLIE+kvMIgUmLmod30qfjjW3BW0KKoD53GpdYIvuh1E/iXqeDzB9i0JN1NgGblmxTgtRr4nQnVEZUmyFoKWA==\"Accept:*/*} request body:"}
{"level":"debug","timestamp":"2026-03-25T16:38:46+08:00","caller":"kernel/baseClient.go:459","content":"------------------response content:HTTP/1.1 200 OK\r\nContent-Length: 250\r\nCache-Control: no-cache, must-revalidate\r\nConnection: keep-alive\r\nContent-Language: zh-CN\r\nContent-Type: application/json; charset=utf-8\r\nDate: Wed, 25 Mar 2026 08:38:46 GMT\r\nKeep-Alive: timeout=8\r\nRequest-Id: 0896BE8ECE0610B90418CBC0C05520E0CF1928F0F302-0\r\nServer: nginx\r\nWechatpay-Nonce: b8dc2cedcb145dc63bb4fd1d7944e28a\r\nWechatpay-Serial: 5F2543BF58239A4EB68FA4433DF1438A88B34B16\r\nWechatpay-Signature: IHfBE6FO4cZZk+X1oulmGu+PauZ5BK4Ai7x9TviyzKrZpGGgcV6RE5z1BdI4AGJGiBv0RiecWCdJ1jUSwXAi9QPVgag8qepWaQCD+SYvHyRbU8bQJhRXYfhWOdwpI2qgUx2/mIcTX/kpE+DJVyz/2CpMKnIPy1J1W//37zv0vuUwZJ+OdVZRcOIL6UiuUB34KKWcUtF6g9GahSDsAKziSSu6Qe9Os9Ja/3dc9Xpxc7VbH/Qj1ovvjciiq/KWuh/+eJjoRBaiXC9mIm9AxckxxYSumYmR2jAWXomolrOiTaAAJ7Jc/wYiNa0AXwyonN/RyhggsPwBeqeOW6ShQlCahQ==\r\nWechatpay-Signature-Type: WECHATPAY2-SHA256-RSA2048\r\nWechatpay-Timestamp: 1774427926\r\nX-Content-Type-Options: nosniff\r\n\r\n{\"amount\":{\"payer_currency\":\"CNY\",\"total\":3000},\"appid\":\"wxb8bbb2b10dec74aa\",\"mchid\":\"1318592501\",\"out_trade_no\":\"MP20260325163002440000\",\"promotion_detail\":[],\"scene_info\":{\"device_id\":\"\"},\"trade_state\":\"NOTPAY\",\"trade_state_desc\":\"订单未支付\"}"}