Files
lunar-mini/server/internal/service/baike_term_sync_test.go
T
gouki fec383f257
Publish Mini Program Dev Version / publish (push) Successful in 1m12s
Build and Publish Server / build (push) Successful in 8m24s
feat(wiki): 二十四节气详情抓取百度百科并图文展示
数据源:百度百科开放接口 BaikeLemmaCardApi(无反爬,返回结构化 JSON)
与「历史上的今天」(维基)完全解耦,维基代码原样保留可随时恢复:

服务端:
- WikiEntry 增加 image(配图)和 extra(结构化字段 JSON)字段
- 新建 BaikeTermSyncLog 独立日志表(不污染维基 WikiSyncLog)
- 新建 BaikeTermSyncService 独立同步服务(24 节气,间隔 1.2s)
- 启动时缺详情自动补齐,之后按配置间隔定时增量刷新
- 管理 API:GET/POST /admin/api/wiki/term-sync*

管理后台:
- 百科条目 Tab 顶部加节气同步卡片(X/24 + 状态 + 同步按钮)
- 条目编辑器加配图 URL(含预览)和节气扩展字段编辑

小程序:
- 百科页展开详情图文展示:配图 + 简介 + 物候/习俗/养生等分块

本地验证:24/24 节气抓取成功,image/extra 全部填充,维基数据不受影响
2026-08-13 00:31:43 +00:00

131 lines
4.6 KiB
Go

package service
import (
"encoding/json"
"strings"
"testing"
)
// 取自百度百科 BaikeLemmaCardApi 「立春」的真实返回片段
const sampleLemmaJSON = `{
"id":25702,"key":"立春","desc":"二十四节气之一","title":"立春",
"image":"https://bkimg.cdn.bcebos.com/pic/d62a6059252dd42a2834edd463674cb5c9ea15ce1205",
"abstract":"立春(Beginning of Spring),又名正月节、岁节、改岁、岁旦、打春等,为二十四节气之首。当太阳到达黄经315°时为立春,于每年公历2月3-5日交节。",
"card":[
{"key":"m25_nameC","name":"中文名","value":["立春"]},
{"key":"m25_nameE","name":"外文名","value":["Beginning of Spring<sup>3</sup><a name=\"ref_3\"></a>","Lichun"]},
{"key":"m25_alternateName","name":"别名","value":["岁首、岁节、改岁、立春节、正月节"]},
{"key":"m25_meaning","name":"涵义","value":["阳气上升,万物更生,新岁开启"]},
{"key":"m25_solarDate","name":"公历时间","value":["公历2月3日-2月5日交节"]},
{"key":"m25_ecliptic","name":"黄道位置","value":["太阳到达黄经315度"]},
{"key":"m25_phenology","name":"物候现象","value":["东风解冻、蛰虫始振、鱼陟负冰"]},
{"key":"m25_farming","name":"农事活动","value":["备耕春播"]},
{"key":"m25_customs","name":"传统习俗","value":["<a target=_blank href=\"/item/x\">演春</a>、迎春、打春"]},
{"key":"m25_health","name":"起居养生","value":["防躁动,避免过度劳累,保证睡眠情绪豁达。"]},
{"key":"m25_flower","name":"花信","value":["迎春、樱桃、望春<sup>35</sup><a name=\"ref_35\"></a>"]}
]
}`
func TestParseLemmaCard(t *testing.T) {
var lemma baikeLemmaCard
if err := json.Unmarshal([]byte(sampleLemmaJSON), &lemma); err != nil {
t.Fatalf("JSON 解析失败: %v", err)
}
if lemma.Key != "立春" {
t.Errorf("key = %q, want 立春", lemma.Key)
}
if lemma.Image == "" {
t.Error("image 不应为空")
}
if !strings.Contains(lemma.Abstract, "二十四节气之首") {
t.Errorf("abstract 应含「二十四节气之首」, got %q", lemma.Abstract)
}
if len(lemma.Card) == 0 {
t.Fatal("card 不应为空")
}
}
func TestLemmaCardMapping(t *testing.T) {
var lemma baikeLemmaCard
json.Unmarshal([]byte(sampleLemmaJSON), &lemma)
extra := map[string]string{}
for _, c := range lemma.Card {
text := cleanTermHTML(firstValue(c.Value))
if text == "" {
continue
}
switch c.Name {
case "别名":
extra["alias"] = text
case "外文名":
extra["english"] = text
case "涵义":
extra["meaning"] = text
case "物候现象":
extra["phenology"] = text
case "传统习俗":
extra["customs"] = text
case "起居养生":
extra["health"] = text
}
}
if extra["alias"] != "岁首、岁节、改岁、立春节、正月节" {
t.Errorf("alias = %q", extra["alias"])
}
if extra["meaning"] != "阳气上升,万物更生,新岁开启" {
t.Errorf("meaning = %q", extra["meaning"])
}
if extra["phenology"] != "东风解冻、蛰虫始振、鱼陟负冰" {
t.Errorf("phenology = %q", extra["phenology"])
}
// 传统习俗含 HTML 链接,应被清洗
if strings.Contains(extra["customs"], "<a") || strings.Contains(extra["customs"], "href") {
t.Errorf("customs 应清洗 HTML, got %q", extra["customs"])
}
if !strings.Contains(extra["customs"], "演春") {
t.Errorf("customs 应保留文字「演春」, got %q", extra["customs"])
}
// 外文名含 <sup> 应清洗
if strings.Contains(extra["english"], "<sup>") {
t.Errorf("english 应清洗 sup 标签, got %q", extra["english"])
}
}
func TestCleanTermHTML(t *testing.T) {
cases := map[string]string{
`<a target=_blank href="/item/x">演春</a>、迎春`: "演春、迎春",
`Beginning of Spring<sup>3</sup><a name="ref_3"></a>`: "Beginning of Spring",
` 多余&nbsp; 空白 `: "多余 空白",
}
for in, want := range cases {
if got := cleanTermHTML(in); got != want {
t.Errorf("cleanTermHTML(%q) = %q, want %q", in, got, want)
}
}
}
func TestFirstSentence(t *testing.T) {
s := "立春为二十四节气之首。当太阳到达黄经315°时为立春。"
if got := firstSentence(s); got != "立春为二十四节气之首。" {
t.Errorf("firstSentence = %q", got)
}
// 无句号时返回原文
if got := firstSentence("无句号文本"); got != "无句号文本" {
t.Errorf("firstSentence 无句号 = %q", got)
}
}
func TestTermSort(t *testing.T) {
if termSort("立春") != 1 {
t.Errorf("termSort(立春) = %d, want 1", termSort("立春"))
}
if termSort("大寒") != 24 {
t.Errorf("termSort(大寒) = %d, want 24", termSort("大寒"))
}
if termSort("不存在") != 0 {
t.Errorf("termSort(不存在) = %d, want 0", termSort("不存在"))
}
}