feat(media-supply): add image upload functionality and enhance HTML handling in article processing
Frontend CI / Frontend (push) Successful in 2m51s
Backend CI / Backend (push) Successful in 19m11s

This commit is contained in:
2026-06-03 15:49:01 +08:00
parent 316e2a34aa
commit d098633a67
3 changed files with 98 additions and 27 deletions
@@ -292,8 +292,51 @@ func TestMeijiequanCreateOrderFormMatchesUpstreamShape(t *testing.T) {
func TestMeijiequanArticleHTMLKeepsExistingHTML(t *testing.T) {
input := `<p><img src="http://example.test/a.png" title="x" _src="http://example.test/a.png" alt="a"></p><p>正文</p>`
if got := meijiequanArticleHTML(input); got != input {
t.Fatalf("expected html passthrough, got %s", got)
got := meijiequanArticleHTML(input)
if !strings.Contains(got, `<p><img src="http://example.test/a.png"`) ||
!strings.Contains(got, `title="x"`) ||
!strings.Contains(got, `_src="http://example.test/a.png"`) ||
!strings.Contains(got, `alt="a"`) ||
!strings.Contains(got, `<p>正文</p>`) {
t.Fatalf("expected html image and paragraphs to be preserved, got %s", got)
}
}
func TestMeijiequanArticleHTMLConvertsMilkdownMixedMarkdown(t *testing.T) {
input := strings.Join([]string{
"# 标题",
"",
`<p class="article-editor-image article-editor-image--center" align="center"><img src="http://example.test/a.webp" alt="图注" title="图注" data-asset-id="88" /></p>`,
"",
"## 小标题",
"",
"- 第一条",
"- **第二条**",
"",
"| 维度 | 说明 |",
"| --- | --- |",
"| 环保 | ENF |",
}, "\n")
got := meijiequanArticleHTML(input)
if !strings.Contains(got, "<h1>标题</h1>") ||
!strings.Contains(got, `<img src="http://example.test/a.webp"`) ||
!strings.Contains(got, `_src="http://example.test/a.webp"`) ||
strings.Contains(got, "data-asset-id") ||
!strings.Contains(got, "<h2>小标题</h2>") ||
!strings.Contains(got, "<li>第一条</li>") ||
!strings.Contains(got, "<strong>第二条</strong>") ||
!strings.Contains(got, "<table>") ||
!strings.Contains(got, "<td>ENF</td>") {
t.Fatalf("unexpected mixed markdown html: %s", got)
}
}
func TestMeijiequanArticleHTMLBackfillsImageSrcFromOriginalSrc(t *testing.T) {
got := meijiequanArticleHTML(`<p><img _src="http://example.test/original.png" alt="a"></p>`)
if !strings.Contains(got, `src="http://example.test/original.png"`) ||
!strings.Contains(got, `_src="http://example.test/original.png"`) {
t.Fatalf("expected src and _src to be normalized, got %s", got)
}
}
+46 -23
View File
@@ -19,6 +19,8 @@ import (
goredis "github.com/redis/go-redis/v9"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/extension"
goldhtml "github.com/yuin/goldmark/renderer/html"
nethtml "golang.org/x/net/html"
"golang.org/x/net/html/atom"
@@ -1153,28 +1155,16 @@ func meijiequanArticleHTML(content string) string {
if source == "" {
return ""
}
if looksLikeHTML(source) {
return source
}
return markdownToMeijiequanHTML(source)
}
func looksLikeHTML(source string) bool {
lower := strings.ToLower(strings.TrimSpace(source))
return strings.Contains(lower, "<p") ||
strings.Contains(lower, "<h1") ||
strings.Contains(lower, "<h2") ||
strings.Contains(lower, "<h3") ||
strings.Contains(lower, "<img") ||
strings.Contains(lower, "<figure") ||
strings.Contains(lower, "<table") ||
strings.Contains(lower, "<ul") ||
strings.Contains(lower, "<ol")
}
func markdownToMeijiequanHTML(markdown string) string {
var output bytes.Buffer
if err := goldmark.Convert([]byte(markdown), &output); err != nil {
renderer := goldmark.New(
goldmark.WithExtensions(extension.GFM),
goldmark.WithRendererOptions(goldhtml.WithUnsafe()),
)
if err := renderer.Convert([]byte(markdown), &output); err != nil {
return ""
}
return addMeijiequanImageSrcAttributes(strings.TrimSpace(output.String()))
@@ -1211,24 +1201,57 @@ func addMeijiequanImageSrcAttribute(node *nethtml.Node) {
}
if node.Type == nethtml.ElementNode && strings.EqualFold(node.Data, "img") {
src := ""
hasOriginalSrc := false
originalSrc := ""
dataSrc := ""
attrs := make([]nethtml.Attribute, 0, len(node.Attr)+1)
for _, attr := range node.Attr {
switch strings.ToLower(attr.Key) {
switch strings.ToLower(strings.TrimSpace(attr.Key)) {
case "src":
src = strings.TrimSpace(attr.Val)
case "_src":
hasOriginalSrc = true
originalSrc = strings.TrimSpace(attr.Val)
case "data-src":
dataSrc = strings.TrimSpace(attr.Val)
case "data-asset-id":
continue
}
attrs = append(attrs, attr)
}
if src == "" {
src = firstNonEmptyText(originalSrc, dataSrc)
}
if src != "" {
for idx := range attrs {
switch strings.ToLower(strings.TrimSpace(attrs[idx].Key)) {
case "src":
attrs[idx].Val = src
case "_src":
attrs[idx].Val = src
}
}
if !meijiequanHTMLAttrsHaveKey(attrs, "src") {
attrs = append(attrs, nethtml.Attribute{Key: "src", Val: src})
}
if !meijiequanHTMLAttrsHaveKey(attrs, "_src") {
attrs = append(attrs, nethtml.Attribute{Key: "_src", Val: src})
}
}
if src != "" && !hasOriginalSrc {
node.Attr = append(node.Attr, nethtml.Attribute{Key: "_src", Val: src})
}
node.Attr = attrs
}
for child := node.FirstChild; child != nil; child = child.NextSibling {
addMeijiequanImageSrcAttribute(child)
}
}
func meijiequanHTMLAttrsHaveKey(attrs []nethtml.Attribute, key string) bool {
for _, attr := range attrs {
if strings.EqualFold(strings.TrimSpace(attr.Key), key) {
return true
}
}
return false
}
func (c *MeijiequanClient) httpClient(ctx context.Context) (*http.Client, string, error) {
session, err := c.loadSession(ctx)
if err != nil {