package service
import (
"fmt"
"log"
"os"
"regexp"
"sort"
"strconv"
"strings"
"unicode/utf16"
"unicode/utf8"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)
// ExtractedImage 包含图片的详细信息
type ExtractedImage struct {
Field string // 图片所属字段(如 "spec_detail", "white_background_pic")
FullPath string // 在JSON中的完整路径(用于后续操作)
ImageURL string // 图片URL
Index int // 在数组中的索引(如果是数组字段)
IsInArray bool // 是否在数组中
}
// JSONProcessor 处理 JSON 数据的工具类
type JSONProcessor struct {
jsonStr string
rootPath string // 动态确定的根路径(如 "schema" 或 "data")
}
// NewJSONProcessor 创建新的 JSON 处理器
func NewJSONProcessor(jsonStr string) *JSONProcessor {
// 动态确定根路径
rootPath := determineRootPath(jsonStr)
return &JSONProcessor{
jsonStr: jsonStr,
rootPath: rootPath,
}
}
// determineRootPath 动态确定根路径
func determineRootPath(jsonStr string) string {
// 可能的根路径候选
candidates := []string{"schema", "data", "model"}
for _, candidate := range candidates {
// 检查是否存在 candidate.model
if gjson.Get(jsonStr, candidate+".model").Exists() {
return candidate + ".model"
}
// 检查是否存在 candidate
if gjson.Get(jsonStr, candidate).Exists() {
return candidate
}
}
// 默认使用 "schema.model"
log.Println("无法确定根路径,使用默认值 schema.model")
return "schema.model"
}
// getFullPath 构建完整路径
func (jp *JSONProcessor) getFullPath(subPath string) string {
if subPath == "" {
return jp.rootPath
}
return jp.rootPath + "." + subPath
}
// RemoveInvalidImage 删除不合规图片及其关联数据
func (jp *JSONProcessor) RemoveInvalidImage(invalidURL string) error {
// 1. 检查并删除 spec_detail 中的规格值
if err := jp.removeSpecValuesByImage(invalidURL); err != nil {
return err
}
// 2. 删除其他图片字段中的无效图片
fields := []string{"white_background_pic", "main_image_three_to_four", "pic"}
for _, field := range fields {
// 特殊处理:白底图只替换不删除
if field == "white_background_pic" {
if err := jp.replaceImageInField(field, invalidURL); err != nil {
return fmt.Errorf("failed to replace in %s: %v", field, err)
}
} else {
if err := jp.removeImageFromField(field, invalidURL); err != nil {
return fmt.Errorf("failed to remove from %s: %v", field, err)
}
}
}
// 3. 从描述中删除图片
if err := jp.removeImageFromDescription(invalidURL); err != nil {
return fmt.Errorf("failed to remove from description: %v", err)
}
return nil
}
// replaceImageInField 替换指定字段中的图片(特殊处理)
func (jp *JSONProcessor) replaceImageInField(fieldPath, invalidURL string) error {
fullPath := jp.getFullPath(fieldPath + ".value")
result := gjson.Get(jp.jsonStr, fullPath)
if !result.Exists() {
return nil
}
var newJSON string
var err error
result.ForEach(func(index gjson.Result, item gjson.Result) bool {
if url := item.Get("url").String(); url == invalidURL {
// 特殊处理:替换为默认图片
newImage := map[string]interface{}{
"url": "https://default-image.com/replacement.jpg",
"width": 800,
"height": 600,
}
itemPath := fmt.Sprintf("%s.%d", fullPath, index.Int())
newJSON, err = sjson.Set(jp.jsonStr, itemPath, newImage)
if err != nil {
return false
}
jp.jsonStr = newJSON
}
return true
})
return err
}
// removeSpecValuesByImage 删除包含指定图片URL的规格值
func (jp *JSONProcessor) removeSpecValuesByImage(invalidURL string) error {
specDetailPath := jp.getFullPath("spec_detail.value")
result := gjson.Get(jp.jsonStr, specDetailPath)
if !result.Exists() {
return nil // 没有规格数据,直接返回
}
// 收集需要删除的规格值ID
var specValueIDs []string
result.ForEach(func(_, group gjson.Result) bool {
group.Get("spec_values").ForEach(func(_, spec gjson.Result) bool {
if imgURL := spec.Get("img_url").String(); imgURL == invalidURL {
if id := spec.Get("id").String(); id != "" {
specValueIDs = append(specValueIDs, id)
}
}
return true
})
return true
})
// 删除所有关联的规格值
for _, id := range specValueIDs {
if err := jp.RemoveSpecValueByID(id); err != nil {
return fmt.Errorf("failed to remove spec value %s: %v", id, err)
}
}
return nil
}
// removeImageFromField 从指定图片字段中删除无效图片
func (jp *JSONProcessor) removeImageFromField(fieldPath, invalidURL string) error {
fullPath := jp.getFullPath(fieldPath + ".value")
result := gjson.Get(jp.jsonStr, fullPath)
if !result.Exists() {
return nil // 字段不存在,直接返回
}
// 收集需要删除的索引
var indicesToDelete []int
result.ForEach(func(index gjson.Result, item gjson.Result) bool {
if url := item.Get("url").String(); url == invalidURL {
indicesToDelete = append(indicesToDelete, int(index.Int()))
}
return true
})
// 从后往前删除(避免索引变化)
sort.Sort(sort.Reverse(sort.IntSlice(indicesToDelete)))
for _, idx := range indicesToDelete {
path := fmt.Sprintf("%s.%d", fullPath, idx)
var err error
jp.jsonStr, err = sjson.Delete(jp.jsonStr, path)
if err != nil {
return err
}
}
return nil
}
// unescapeUnicode 解码 Unicode 转义序列(如 \u003c -> <)
func unescapeUnicode(s string) string {
var buf strings.Builder
reader := strings.NewReader(s) // 使用不同的变量名避免冲突
for {
r, _, err := reader.ReadRune() // 读取原始字符
if err != nil {
break
}
if r == '\\' {
next, _, _ := reader.ReadRune() // 读取下一个字符
if next == 'u' {
// 读取 4 位十六进制
hex := make([]rune, 4)
for i := 0; i < 4; i++ {
ch, _, _ := reader.ReadRune()
hex[i] = ch
}
// 转换为 rune
if code, err := strconv.ParseInt(string(hex), 16, 32); err == nil {
buf.WriteRune(rune(code))
continue
}
buf.WriteRune('\\')
buf.WriteRune('u')
buf.WriteString(string(hex))
continue
}
buf.WriteRune('\\')
buf.WriteRune(next)
continue
}
buf.WriteRune(r)
}
return buf.String()
}
// escapeUnicode 将特殊字符编码为 Unicode 转义序列
func escapeUnicode(s string) string {
var buf strings.Builder
for _, r := range s {
if r <= 0x7F { // ASCII 字符
switch r {
case '"', '\\', '<', '>', '&':
buf.WriteString(fmt.Sprintf("\\u%04X", r))
default:
buf.WriteRune(r)
}
} else { // 非 ASCII 字符
if utf8.ValidRune(r) {
buf.WriteRune(r)
} else {
// 处理代理对(surrogate pairs)
for _, surr := range utf16.Encode([]rune{r}) {
buf.WriteString(fmt.Sprintf("\\u%04X", surr))
}
}
}
}
return buf.String()
}
// isEscapedFormat 检测字符串是否为Unicode转义格式
func isEscapedFormat(s string) bool {
// 检查是否存在未转义的HTML标签字符
if strings.Contains(s, "<") || strings.Contains(s, ">") {
return false
}
// 检查是否存在常见的Unicode转义序列
return strings.Contains(s, `\u`)
}
// removeImageFromDescription 从描述中删除无效图片(保持原始格式)
func (jp *JSONProcessor) removeImageFromDescription(invalidURL string) error {
descPath := jp.getFullPath("description.value")
result := gjson.Get(jp.jsonStr, descPath)
if !result.Exists() {
return nil
}
// 获取原始字符串
originalHTML := result.String()
// 检测原始格式
isEscaped := isEscapedFormat(originalHTML)
var decodedHTML string
if isEscaped {
// Unicode转义格式:先解码
decodedHTML = unescapeUnicode(originalHTML)
} else {
// 直接HTML格式:直接使用
decodedHTML = originalHTML
}
// 处理图片删除
escapedURL := regexp.QuoteMeta(invalidURL)
re := regexp.MustCompile(
`<img\s[^>]*?src\s*=\s*["']` + escapedURL + `["'][^>]*>`,
)
newHTML := re.ReplaceAllString(decodedHTML, "")
// 根据原始格式重新编码
var finalContent string
if isEscaped {
// 重新编码为Unicode转义序列
finalContent = escapeUnicode(newHTML)
} else {
// 保持直接HTML格式
finalContent = newHTML
}
// 使用SetRaw设置,避免二次编码
var err error
jp.jsonStr, err = sjson.SetRaw(jp.jsonStr, descPath, strconv.Quote(finalContent))
return err
}
// ExtractAllPics 提取所有图片及其详细信息
func (jp *JSONProcessor) ExtractAllPics() []ExtractedImage {
var images []ExtractedImage
// 1. 处理指定字段的图片
fields := []string{"spec_detail", "white_background_pic", "main_image_three_to_four", "pic"}
for _, field := range fields {
path := jp.getFullPath(field + ".value")
result := gjson.Get(jp.jsonStr, path)
if result.Exists() {
// 处理数组字段
if result.IsArray() {
array := result.Array()
for idx, item := range array {
// 处理 spec_detail 的特殊结构
if field == "spec_detail" {
specValues := item.Get("spec_values")
if specValues.Exists() && specValues.IsArray() {
specArray := specValues.Array()
for specIdx, spec := range specArray {
if img := spec.Get("img_url"); img.Exists() {
specPath := fmt.Sprintf("%s.%d.spec_values.%d.img_url", path, idx, specIdx)
images = append(images, ExtractedImage{
Field: field,
FullPath: specPath,
ImageURL: img.String(),
Index: specIdx,
IsInArray: true,
})
}
}
}
} else if url := item.Get("url"); url.Exists() {
// 处理其他标准字段
itemPath := fmt.Sprintf("%s.%d.url", path, idx)
images = append(images, ExtractedImage{
Field: field,
FullPath: itemPath,
ImageURL: url.String(),
Index: idx,
IsInArray: true,
})
}
}
} else {
// 处理非数组字段(如果存在)
if url := result.Get("url"); url.Exists() {
images = append(images, ExtractedImage{
Field: field,
FullPath: path + ".url",
ImageURL: url.String(),
IsInArray: false,
})
}
}
}
}
// 2. 处理 description 中的图片
descPath := jp.getFullPath("description.value")
descResult := gjson.Get(jp.jsonStr, descPath)
if descResult.Exists() {
html := descResult.String()
re := regexp.MustCompile(`<img[^>]+src="([^">]+)"`)
matches := re.FindAllStringSubmatch(html, -1)
for _, match := range matches {
if len(match) > 1 {
images = append(images, ExtractedImage{
Field: "description",
FullPath: descPath,
ImageURL: match[1],
})
}
}
}
return images
}
// RemoveSpecValueByID 根据 spec_value ID 删除规格值及其关联的 SKU
func (jp *JSONProcessor) RemoveSpecValueByID(specValueID string) error {
// 1. 删除 spec_detail 中的 spec_value
specDetailPath := jp.getFullPath("spec_detail.value")
specDetailResult := gjson.Get(jp.jsonStr, specDetailPath)
if !specDetailResult.Exists() {
return fmt.Errorf("spec_detail.value does not exist")
}
specGroups := specDetailResult.Array()
// 收集需要删除的索引(组索引和值索引)
type deletionPoint struct {
groupIndex int
valueIndex int
}
var deletions []deletionPoint
// 遍历所有规格组
for groupIdx, group := range specGroups {
specValues := group.Get("spec_values").Array()
// 遍历组内的规格值
for valueIdx, value := range specValues {
if id := value.Get("id").String(); id == specValueID {
deletions = append(deletions, deletionPoint{groupIdx, valueIdx})
}
}
}
// 从后往前删除(避免索引变化)
sort.Slice(deletions, func(i, j int) bool {
if deletions[i].groupIndex == deletions[j].groupIndex {
return deletions[i].valueIndex > deletions[j].valueIndex
}
return deletions[i].groupIndex > deletions[j].groupIndex
})
for _, del := range deletions {
path := fmt.Sprintf("%s.%d.spec_values.%d", specDetailPath, del.groupIndex, del.valueIndex)
var err error
jp.jsonStr, err = sjson.Delete(jp.jsonStr, path)
if err != nil {
return err
}
}
// 2. 删除关联的 SKU
skuDetailPath := jp.getFullPath("sku_detail.value")
skuDetailResult := gjson.Get(jp.jsonStr, skuDetailPath)
if !skuDetailResult.Exists() {
// 没有 SKU 数据,直接返回
return nil
}
skus := skuDetailResult.Array()
var skuIndicesToDelete []int
// 收集需要删除的 SKU 索引
for idx, sku := range skus {
specDetailIDs := sku.Get("spec_detail_ids").Array()
for _, id := range specDetailIDs {
if id.String() == specValueID {
skuIndicesToDelete = append(skuIndicesToDelete, idx)
break // 找到匹配即跳出
}
}
}
// 从后往前删除 SKU(避免索引变化)
sort.Sort(sort.Reverse(sort.IntSlice(skuIndicesToDelete)))
for _, idx := range skuIndicesToDelete {
path := fmt.Sprintf("%s.%d", skuDetailPath, idx)
var err error
jp.jsonStr, err = sjson.Delete(jp.jsonStr, path)
if err != nil {
return err
}
}
return nil
}
// UpdateSkuPrice 更新 SKU 的价格
func (jp *JSONProcessor) UpdateSkuPrice(skuID, newPrice string) error {
path := jp.getFullPath("sku_detail.value")
result := gjson.Get(jp.jsonStr, path)
if !result.Exists() {
return fmt.Errorf("sku_detail.value does not exist")
}
skuArray := result.Array()
for idx, sku := range skuArray {
if id := sku.Get("id").String(); id == skuID {
fullPath := fmt.Sprintf("%s.%d.price", path, idx)
var err error
jp.jsonStr, err = sjson.Set(jp.jsonStr, fullPath, newPrice)
return err
}
}
return fmt.Errorf("SKU with ID %s not found", skuID)
}
// UpdateSkuStock 更新 SKU 的库存
func (jp *JSONProcessor) UpdateSkuStock(skuID string, newStock int) error {
path := jp.getFullPath("sku_detail.value")
result := gjson.Get(jp.jsonStr, path)
if !result.Exists() {
return fmt.Errorf("sku_detail.value does not exist")
}
skuArray := result.Array()
for idx, sku := range skuArray {
if id := sku.Get("id").String(); id == skuID {
// 更新 stock_info.stock_num
stockPath := fmt.Sprintf("%s.%d.stock_info.stock_num", path, idx)
var err error
jp.jsonStr, err = sjson.Set(jp.jsonStr, stockPath, newStock)
if err != nil {
return err
}
// 更新 self_sell_stock
selfStockPath := fmt.Sprintf("%s.%d.self_sell_stock", path, idx)
jp.jsonStr, err = sjson.Set(jp.jsonStr, selfStockPath, newStock)
return err
}
}
return fmt.Errorf("SKU with ID %s not found", skuID)
}
// GetField 获取指定字段的值
func (jp *JSONProcessor) GetField(fieldPath string) (gjson.Result, bool) {
fullPath := jp.getFullPath(fieldPath)
result := gjson.Get(jp.jsonStr, fullPath)
return result, result.Exists()
}
// UpdateField 更新指定字段的值
func (jp *JSONProcessor) UpdateField(fieldPath string, newValue interface{}) error {
fullPath := jp.getFullPath(fieldPath)
newJSON, err := sjson.Set(jp.jsonStr, fullPath, newValue)
if err != nil {
return err
}
jp.jsonStr = newJSON
return nil
}
// DeleteField 删除指定字段
func (jp *JSONProcessor) DeleteField(fieldPath string) error {
fullPath := jp.getFullPath(fieldPath)
newJSON, err := sjson.Delete(jp.jsonStr, fullPath)
if err != nil {
return err
}
jp.jsonStr = newJSON
return nil
}
// AddArrayItem 向数组字段添加新元素
func (jp *JSONProcessor) AddArrayItem(fieldPath string, newItem interface{}) error {
// 检查数组字段是否存在
basePath := jp.getFullPath(fieldPath + ".value")
result := gjson.Get(jp.jsonStr, basePath)
// 如果数组不存在,先创建空数组
if !result.Exists() {
var err error
jp.jsonStr, err = sjson.Set(jp.jsonStr, basePath, []interface{}{})
if err != nil {
return fmt.Errorf("failed to create array field: %v", err)
}
}
// 添加新元素到数组末尾
fullPath := fmt.Sprintf("%s.-1", basePath)
newJSON, err := sjson.Set(jp.jsonStr, fullPath, newItem)
if err != nil {
return fmt.Errorf("failed to add array item: %v", err)
}
jp.jsonStr = newJSON
return nil
}
// RemoveArrayItem 从数组中删除指定索引的元素
func (jp *JSONProcessor) RemoveArrayItem(fieldPath string, index int) error {
basePath := jp.getFullPath(fieldPath + ".value")
result := gjson.Get(jp.jsonStr, basePath)
if !result.Exists() {
return fmt.Errorf("array field does not exist")
}
// 检查索引是否有效
array := result.Array()
if index < 0 || index >= len(array) {
return fmt.Errorf("index %d out of range [0, %d]", index, len(array)-1)
}
fullPath := fmt.Sprintf("%s.%d", basePath, index)
newJSON, err := sjson.Delete(jp.jsonStr, fullPath)
if err != nil {
return err
}
jp.jsonStr = newJSON
return nil
}
// GetJSON 获取当前处理后的 JSON
func (jp *JSONProcessor) GetJSON() string {
return jp.jsonStr
}
// TestFunc 测试函数
func TestFunc() {
content, err := os.ReadFile("asyncCheckPost.json")
if err != nil {
log.Printf("读取过滤文件失败: %v", err)
return
}
processor := NewJSONProcessor(string(content))
// 提取所有图片
allImages := processor.ExtractAllPics()
fmt.Printf("提取到 %d 张图片\n", len(allImages))
// 打印图片信息
for _, img := range allImages {
fmt.Printf("字段: %-25s | URL: %s\n", img.Field, img.ImageURL)
fmt.Printf("路径: %s\n", img.FullPath)
if img.IsInArray {
fmt.Printf("索引: %d\n", img.Index)
}
fmt.Println("----------------------------------")
}
// 假设我们检测到第二张图片不合规
if len(allImages) > 1 {
invalidImage := allImages[1]
fmt.Printf("检测到不合规图片: %s (位于 %s)\n", invalidImage.ImageURL, invalidImage.Field)
// 特殊处理白底图
if invalidImage.Field == "white_background_pic" {
fmt.Println("特殊处理:替换白底图")
if err := processor.replaceImageInField(invalidImage.Field, invalidImage.ImageURL); err != nil {
fmt.Println("替换图片失败:", err)
} else {
fmt.Println("成功替换白底图")
}
} else {
// 删除不合规图片及其关联数据
if err := processor.RemoveInvalidImage(invalidImage.ImageURL); err != nil {
fmt.Println("删除不合规图片失败:", err)
} else {
fmt.Println("成功删除不合规图片及其关联数据")
}
}
}
// 1. 删除规格值及其关联的 SKU
specValueID := "1839581026529404" // 小号钢架尺30米的 ID
if err := processor.RemoveSpecValueByID(specValueID); err != nil {
fmt.Println("删除规格值失败:", err)
} else {
fmt.Println("成功删除规格值及其关联SKU")
}
// 2. 更新 SKU 价格和库存
skuID := "3527608283404802" // 第一个 SKU 的 ID
newPrice := "99.99"
newStock := 3000
if err := processor.UpdateSkuPrice(skuID, newPrice); err != nil {
fmt.Println("更新价格失败:", err)
} else {
fmt.Println("成功更新SKU价格")
}
if err := processor.UpdateSkuStock(skuID, newStock); err != nil {
fmt.Println("更新库存失败:", err)
} else {
fmt.Println("成功更新SKU库存")
}
// 3. 测试数组操作
// 向不存在的数组添加元素
if err := processor.AddArrayItem("new_field", map[string]string{"test": "value"}); err != nil {
fmt.Println("添加数组项失败:", err)
} else {
fmt.Println("成功添加数组项到新字段")
}
// 尝试删除不存在的数组项
if err := processor.RemoveArrayItem("non_existent_field", 0); err != nil {
fmt.Println("删除数组项失败(预期):", err)
} else {
fmt.Println("意外成功删除不存在的数组项")
}
// 尝试越界删除
if err := processor.RemoveArrayItem("pic", 100); err != nil {
fmt.Println("删除数组项失败(预期):", err)
} else {
fmt.Println("意外成功删除越界数组项")
}
// 获取最终 JSON
// finalJSON := processor.GetJSON()
// fmt.Println(finalJSON)
}
这段是修改后的代码,在格式为:<p><img src=\"https://p3-aio.ecombdimg.com 的格式中转换正确,而在\u003cp\u003e\u003cimg src=\"https://p3-aio.ecombdimg.com 这样的格式中,也转换成立 <p><img src=\"https://p3-aio.ecombdimg.com,还需要你帮我修改这个BUG