diff --git a/miniprogram/pages/read/read.js b/miniprogram/pages/read/read.js index 8a8359b1..c83f0527 100644 --- a/miniprogram/pages/read/read.js +++ b/miniprogram/pages/read/read.js @@ -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, diff --git a/miniprogram/project.private.config.json b/miniprogram/project.private.config.json index fb71e442..23ef0531 100644 --- a/miniprogram/project.private.config.json +++ b/miniprogram/project.private.config.json @@ -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", diff --git a/soul-admin/src/pages/content/ChapterTree.tsx b/soul-admin/src/pages/content/ChapterTree.tsx index 76d3ebac..8ecf220b 100644 --- a/soul-admin/src/pages/content/ChapterTree.tsx +++ b/soul-admin/src/pages/content/ChapterTree.tsx @@ -20,6 +20,8 @@ export interface SectionItem { payCount?: number hotScore?: number hotRank?: number + /** 章节试读%覆盖(与列表接口 previewPercent 一致) */ + previewPercent?: number | null } export interface ChapterItem { diff --git a/soul-admin/src/pages/content/ContentPage.tsx b/soul-admin/src/pages/content/ContentPage.tsx index 059143de..fc6b8b7f 100644 --- a/soul-admin/src/pages/content/ContentPage.tsx +++ b/soul-admin/src/pages/content/ContentPage.tsx @@ -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, 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 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 = { + 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))), + }) }} /> @@ -2404,15 +2449,17 @@ export function ContentPage() {
+ 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, }) - } + }} >
@@ -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="编辑文章" > diff --git a/soul-api/internal/handler/book.go b/soul-api/internal/handler/book.go index 11bd0d40..a383e5d6 100644 --- a/soul-api/internal/handler/book.go +++ b/soul-api/internal/handler/book.go @@ -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 { diff --git a/soul-api/internal/handler/db_book.go b/soul-api/internal/handler/db_book.go index 590711d2..1c02a69a 100644 --- a/soul-api/internal/handler/db_book.go +++ b/soul-api/internal/handler/db_book.go @@ -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 必须始终出现在 JSON(null=走全局),避免 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"` diff --git a/soul-api/internal/handler/h5_read.go b/soul-api/internal/handler/h5_read.go index ccfef8b3..a998f070 100644 --- a/soul-api/internal/handler/h5_read.go +++ b/soul-api/internal/handler/h5_read.go @@ -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 diff --git a/soul-api/internal/model/chapter.go b/soul-api/internal/model/chapter.go index 0d189b44..eca63b54 100644 --- a/soul-api/internal/model/chapter.go +++ b/soul-api/internal/model/chapter.go @@ -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"` } diff --git a/soul-api/wechat/info.log b/soul-api/wechat/info.log index 5abf98c8..6476ef97 100644 --- a/soul-api/wechat/info.log +++ b/soul-api/wechat/info.log @@ -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\":\"订单未支付\"}"}