Scope knowledge and image libraries by brand
Frontend CI / Frontend (push) Successful in 2m59s
Backend CI / Backend (push) Successful in 14m58s

This commit is contained in:
2026-06-30 12:30:10 +08:00
parent 95ddb3057f
commit ced0c4ec0f
35 changed files with 1136 additions and 326 deletions
@@ -32,15 +32,15 @@ type mediaSupplyArticleImageUpload struct {
Content []byte
}
func (s *MediaSupplyService) prepareMeijiequanArticleContent(ctx context.Context, tenantID int64, content string) (string, error) {
func (s *MediaSupplyService) prepareMeijiequanArticleContent(ctx context.Context, tenantID int64, brandID int64, content string) (string, error) {
html := meijiequanArticleHTML(content)
if strings.TrimSpace(html) == "" {
return "", nil
}
return s.uploadMeijiequanArticleImages(ctx, tenantID, html)
return s.uploadMeijiequanArticleImages(ctx, tenantID, brandID, html)
}
func (s *MediaSupplyService) uploadMeijiequanArticleImages(ctx context.Context, tenantID int64, fragment string) (string, error) {
func (s *MediaSupplyService) uploadMeijiequanArticleImages(ctx context.Context, tenantID int64, brandID int64, fragment string) (string, error) {
if !strings.Contains(strings.ToLower(fragment), "<img") {
return fragment, nil
}
@@ -56,7 +56,7 @@ func (s *MediaSupplyService) uploadMeijiequanArticleImages(ctx context.Context,
cache := make(map[string]string)
for _, node := range nodes {
if err := s.uploadMeijiequanArticleImageNode(ctx, tenantID, node, cache); err != nil {
if err := s.uploadMeijiequanArticleImageNode(ctx, tenantID, brandID, node, cache); err != nil {
return "", err
}
}
@@ -70,31 +70,31 @@ func (s *MediaSupplyService) uploadMeijiequanArticleImages(ctx context.Context,
return builder.String(), nil
}
func (s *MediaSupplyService) uploadMeijiequanArticleImageNode(ctx context.Context, tenantID int64, node *nethtml.Node, cache map[string]string) error {
func (s *MediaSupplyService) uploadMeijiequanArticleImageNode(ctx context.Context, tenantID int64, brandID int64, node *nethtml.Node, cache map[string]string) error {
if node == nil {
return nil
}
if node.Type == nethtml.ElementNode && strings.EqualFold(node.Data, "img") {
if err := s.uploadMeijiequanArticleImageAttrs(ctx, tenantID, node, cache); err != nil {
if err := s.uploadMeijiequanArticleImageAttrs(ctx, tenantID, brandID, node, cache); err != nil {
return err
}
}
for child := node.FirstChild; child != nil; child = child.NextSibling {
if err := s.uploadMeijiequanArticleImageNode(ctx, tenantID, child, cache); err != nil {
if err := s.uploadMeijiequanArticleImageNode(ctx, tenantID, brandID, child, cache); err != nil {
return err
}
}
return nil
}
func (s *MediaSupplyService) uploadMeijiequanArticleImageAttrs(ctx context.Context, tenantID int64, node *nethtml.Node, cache map[string]string) error {
func (s *MediaSupplyService) uploadMeijiequanArticleImageAttrs(ctx context.Context, tenantID int64, brandID int64, node *nethtml.Node, cache map[string]string) error {
src := mediaSupplyHTMLAttrValue(node.Attr, "src")
originalSrc := mediaSupplyHTMLAttrValue(node.Attr, "_src")
dataSrc := mediaSupplyHTMLAttrValue(node.Attr, "data-src")
assetID := mediaSupplyHTMLInt64AttrValue(node.Attr, "data-asset-id")
currentURL := firstNonEmptyText(src, originalSrc, dataSrc)
upload, resolved, err := s.resolveMeijiequanArticleImageUpload(ctx, tenantID, currentURL, assetID)
upload, resolved, err := s.resolveMeijiequanArticleImageUpload(ctx, tenantID, brandID, currentURL, assetID)
if err != nil {
return err
}
@@ -158,8 +158,8 @@ func (s *MediaSupplyService) uploadMeijiequanArticleImageAttrs(ctx context.Conte
return nil
}
func (s *MediaSupplyService) resolveMeijiequanArticleImageUpload(ctx context.Context, tenantID int64, imageURL string, assetID int64) (mediaSupplyArticleImageUpload, bool, error) {
objectKey, internalAssetURL, ok := s.mediaSupplyObjectKeyFromPublicAssetURL(imageURL, tenantID)
func (s *MediaSupplyService) resolveMeijiequanArticleImageUpload(ctx context.Context, tenantID int64, brandID int64, imageURL string, assetID int64) (mediaSupplyArticleImageUpload, bool, error) {
objectKey, internalAssetURL, ok := s.mediaSupplyObjectKeyFromPublicAssetURL(ctx, imageURL, tenantID, brandID)
if internalAssetURL {
if !ok {
if assetID <= 0 {
@@ -171,7 +171,7 @@ func (s *MediaSupplyService) resolveMeijiequanArticleImageUpload(ctx context.Con
}
}
if assetID > 0 {
objectKey, err := s.loadMediaSupplyImageAssetObjectKey(ctx, tenantID, assetID)
objectKey, err := s.loadMediaSupplyImageAssetObjectKey(ctx, tenantID, brandID, assetID)
if err != nil {
return mediaSupplyArticleImageUpload{}, false, err
}
@@ -259,7 +259,7 @@ func mediaSupplyUploadFilename(objectKey, sourceURL, ext string) string {
return name
}
func (s *MediaSupplyService) mediaSupplyObjectKeyFromPublicAssetURL(raw string, tenantID int64) (string, bool, bool) {
func (s *MediaSupplyService) mediaSupplyObjectKeyFromPublicAssetURL(ctx context.Context, raw string, tenantID int64, brandID int64) (string, bool, bool) {
token, ok := mediaSupplyPublicAssetTokenFromURL(raw)
if !ok {
return "", false, false
@@ -268,6 +268,9 @@ func (s *MediaSupplyService) mediaSupplyObjectKeyFromPublicAssetURL(raw string,
if !valid || !mediaSupplyObjectKeyAllowedForTenant(objectKey, tenantID) {
return "", true, false
}
if !s.mediaSupplyImageObjectKeyBelongsToBrand(ctx, tenantID, brandID, objectKey) {
return "", true, false
}
return objectKey, true, true
}
@@ -317,16 +320,48 @@ func mediaSupplyObjectKeyAllowedForTenant(objectKey string, tenantID int64) bool
strings.HasPrefix(objectKey, tenantPrefix+"articles/")
}
func (s *MediaSupplyService) loadMediaSupplyImageAssetObjectKey(ctx context.Context, tenantID, assetID int64) (string, error) {
func mediaSupplyObjectKeyIsImageLibraryAsset(objectKey string, tenantID int64) bool {
if tenantID <= 0 {
return false
}
tenantPrefix := "tenants/" + strconv.FormatInt(tenantID, 10) + "/"
return strings.HasPrefix(strings.TrimSpace(objectKey), tenantPrefix+"images/")
}
func (s *MediaSupplyService) mediaSupplyImageObjectKeyBelongsToBrand(ctx context.Context, tenantID, brandID int64, objectKey string) bool {
if s == nil || s.pool == nil || !mediaSupplyObjectKeyIsImageLibraryAsset(objectKey, tenantID) {
return true
}
if tenantID <= 0 || brandID <= 0 || strings.TrimSpace(objectKey) == "" {
return false
}
var exists bool
if err := s.pool.QueryRow(ctx, `
SELECT EXISTS (
SELECT 1
FROM image_assets
WHERE tenant_id = $1
AND brand_id = $2
AND object_key = $3
AND status = 'active'
AND deleted_at IS NULL
)
`, tenantID, brandID, objectKey).Scan(&exists); err != nil {
return false
}
return exists
}
func (s *MediaSupplyService) loadMediaSupplyImageAssetObjectKey(ctx context.Context, tenantID, brandID, assetID int64) (string, error) {
if s == nil || s.pool == nil {
return "", errMediaSupplyArticleImageUpload
}
var objectKey string
if err := s.pool.QueryRow(ctx, `
SELECT object_key
FROM image_assets
WHERE id = $1 AND tenant_id = $2 AND status = 'active' AND deleted_at IS NULL
`, assetID, tenantID).Scan(&objectKey); err != nil {
SELECT object_key
FROM image_assets
WHERE id = $1 AND tenant_id = $2 AND brand_id = $3 AND status = 'active' AND deleted_at IS NULL
`, assetID, tenantID, brandID).Scan(&objectKey); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return "", errMediaSupplyArticleImageUpload
}