Browse Source

粮油代码迁移

gmy 7 months ago
parent
commit
d7df990523

+ 8 - 1
models/base.go

@@ -26,8 +26,15 @@ type BaseResponseResult struct {
 	Data    string `description:"返回数据,json格式字符串"`
 }
 
+type RequestResponse[T any] struct {
+	Ret     int    `json:"Ret"`
+	Success bool   `json:"Success"`
+	Data    T      `json:"Data"` // 这里是你要获取的关键字段
+	Msg     string `json:"Msg"`
+}
+
 func (r *BaseResponse) Init() *BaseResponse {
-	return &BaseResponse{Ret: 403,IsSendEmail: true}
+	return &BaseResponse{Ret: 403, IsSendEmail: true}
 }
 
 type BaseRequest struct {

+ 12 - 0
models/base_from_ly_classify.go

@@ -0,0 +1,12 @@
+// @Author gmy 2024/8/7 9:26:00
+package models
+
+type BaseFromLyClassify struct {
+	BaseFromLyClassifyId int    `orm:"column(base_from_ly_classify_id);pk"` // 分类ID
+	CreateTime           string `orm:"column(create_time)"`                 // 创建时间
+	ModifyTime           string `orm:"column(modify_time)"`                 // 修改时间
+	ClassifyName         string `orm:"column(classify_name)"`               // 分类名称
+	ParentId             int    `orm:"column(parent_id)"`                   // 上级id
+	Sort                 int    `orm:"column(sort)"`                        // 排序字段,越小越靠前
+	ClassifyNameEn       string `orm:"column(classify_name_en)"`            // 英文分类名称
+}

+ 12 - 0
models/base_from_ly_data.go

@@ -0,0 +1,12 @@
+// @Author gmy 2024/8/7 9:50:00
+package models
+
+type BaseFromLyData struct {
+	BaseFromLyDataId  int     `orm:"column(base_from_ly_data_id);pk"` // 数据ID
+	CreateTime        string  `orm:"column(create_time)"`             // 创建时间
+	ModifyTime        string  `orm:"column(modify_time)"`             // 修改时间
+	BaseFromLyIndexId int     `orm:"column(base_from_ly_index_id)"`   // 指标id
+	IndexCode         string  `orm:"column(index_code)"`              // 指标编码
+	DataTime          string  `orm:"column(data_time)"`               // 数据日期
+	Value             float64 `orm:"column(value)"`                   // 数据值
+}

+ 15 - 0
models/base_from_ly_index.go

@@ -0,0 +1,15 @@
+// Package models
+// @Author gmy 2024/8/7 9:38:00
+package models
+
+type BaseFromLyIndex struct {
+	BaseFromLyIndexId    int    `orm:"column(base_from_ly_index_id);pk"` // 指标ID
+	CreateTime           string `orm:"column(create_time)"`              // 创建时间
+	ModifyTime           string `orm:"column(modify_time)"`              // 修改时间
+	BaseFromLyClassifyId int    `orm:"column(base_from_ly_classify_id)"` // 原始数据指标分类id
+	IndexCode            string `orm:"column(index_code)"`               // 指标编码
+	IndexName            string `orm:"column(index_name)"`               // 指标名称
+	Frequency            string `orm:"column(frequency)"`                // 频度
+	Unit                 string `orm:"column(unit)"`                     // 单位
+	EdbExist             int    `orm:"column(edb_exist)"`                // 指标库是否已添加:0-否;1-是
+}

+ 13 - 0
models/base_from_ly_index_record.go

@@ -0,0 +1,13 @@
+// Package models
+// @Author gmy 2024/8/7 9:38:00
+package models
+
+type BaseFromLyIndexRecord struct {
+	BaseFromLyIndexRecordId int    `orm:"column(base_from_ly_index_record_id);pk"` // 指标记录ID
+	CreateTime              string `orm:"column(create_time)"`                     // 创建时间
+	ModifyTime              string `orm:"column(modify_time)"`                     // 修改时间
+	Product                 string `orm:"column(product)"`                         // 产品
+	Category                string `orm:"column(category)"`                        // 分类
+	Url                     string `orm:"column(url)"`                             // 指标页面地址
+	DataTime                string `orm:"column(data_time)"`                       // 数据日期
+}

+ 492 - 0
services/liangyou/commodity_liangyou.go

@@ -0,0 +1,492 @@
+package main
+
+import (
+	"context"
+	"encoding/json"
+	"eta/eta_data_analysis/models"
+	"eta/eta_data_analysis/utils"
+	"fmt"
+	"github.com/beego/beego/v2/core/logs"
+	"github.com/chromedp/cdproto/cdp"
+	"log"
+	"os"
+	"regexp"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/chromedp/chromedp"
+)
+
+var (
+	lyLoginPath = "https://www.fao.com.cn/"
+)
+
+// UserInfo 登录用户信息
+type UserInfo struct {
+	Username string
+	Password string
+}
+
+func main() {
+
+	// 读取 JSON 文件
+	configFile, err := os.ReadFile("D:\\go\\workspace1\\eta_crawler\\static\\liangyou.json")
+	if err != nil {
+		fmt.Printf("读取配置文件错误: %v\n", err)
+		return
+	}
+
+	// 定义通用的 map 结构体来解析 JSON
+	var data map[string]map[string]map[string][]string
+
+	// 解析 JSON 文件内容
+	err = json.Unmarshal(configFile, &data)
+	if err != nil {
+		fmt.Printf("解析配置文件错误: %v\n", err)
+		return
+	}
+
+	// 打印解析后的数据以验证
+	fmt.Printf("%+v\n", data)
+
+	// 创建 chromedp 执行上下文
+	options := []chromedp.ExecAllocatorOption{
+		chromedp.Flag("headless", false),
+		chromedp.Flag("disable-blink-features", "AutomationControlled"),
+		chromedp.UserAgent(`Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.80 Safari/537.36`),
+	}
+
+	allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), options...)
+	defer cancel()
+	ctx, cancel := chromedp.NewContext(allocCtx)
+	defer cancel()
+
+	// 登录操作
+	err = login(ctx)
+	if err != nil {
+		fmt.Printf("登录错误: %v\n", err)
+		return
+	}
+
+	// 遍历配置并爬取数据
+	for product, productData := range data {
+		for category, categoryData := range productData {
+			for report, keywords := range categoryData {
+				fmt.Printf("正在获取数据: %s -> %s -> %s\n", product, category, report)
+				err = fetchReportData(ctx, product, category, report, keywords)
+				if err != nil {
+					fmt.Printf("获取数据错误: %s -> %s -> %s: %v\n", product, category, report, err)
+					// 您看文章的速度太快了,歇一会再看吧
+					if strings.Contains(err.Error(), "您看文章的速度太快了,歇一会再看吧") {
+						return
+					}
+				}
+			}
+		}
+	}
+}
+
+func login(ctx context.Context) error {
+	userInfo := UserInfo{
+		Username: "13633849418",
+		Password: "828384Abc@",
+	}
+	return chromedp.Run(ctx,
+		chromedp.Navigate(lyLoginPath),
+		chromedp.Sleep(5*time.Second),
+		chromedp.Click(`a[id="btnLogin"]`, chromedp.ByQuery),
+		chromedp.Sleep(2*time.Second),
+		chromedp.SetValue(`input[id="userName"]`, userInfo.Username, chromedp.ByQuery),
+		chromedp.SetValue(`input[id="pwd"]`, userInfo.Password, chromedp.ByQuery),
+		chromedp.Sleep(2*time.Second),
+		chromedp.Click(`input[id="btn_Login"]`, chromedp.ByQuery),
+		chromedp.Sleep(5*time.Second),
+	)
+}
+
+func fetchReportData(ctx context.Context, product, category, report string, keywords []string) error {
+	// Navigate to the main page
+	err := chromedp.Run(ctx,
+		chromedp.Navigate(lyLoginPath),
+		chromedp.Sleep(5*time.Second),
+	)
+	if err != nil {
+		return err
+	}
+
+	// Navigate to the product page
+	productPageURL, err := fillProductPageURL(ctx, product, category)
+	if err != nil {
+		return err
+	}
+
+	// Navigate to the category page
+	var categoryPageURL string
+	err = chromedp.Run(ctx,
+		chromedp.Navigate(productPageURL),
+		chromedp.Sleep(5*time.Second),
+		chromedp.Click(fmt.Sprintf(`//div[contains(@class, "newBox")]//a[contains(text(), '%s')]`, category), chromedp.BySearch),
+		chromedp.Sleep(5*time.Second),
+		chromedp.Location(&categoryPageURL),
+	)
+	if err != nil {
+		return err
+	}
+	logs.Info("categoryPageURL: %s: %s: %s", product, category, categoryPageURL)
+
+	//var allReportURLs []string
+	var allReportURLMap = make(map[string]string)
+	for {
+		var htmlContent string
+		err = chromedp.Run(ctx,
+			chromedp.Navigate(categoryPageURL),
+			chromedp.Sleep(5*time.Second),
+			chromedp.OuterHTML("html", &htmlContent),
+		)
+		if err != nil {
+			return err
+		}
+		fmt.Printf("页面内容: %s\n", htmlContent)
+
+		// Extract report URLs containing the partial keyword
+		reportURLMap := extractReportURLs(htmlContent, report)
+		//allReportURLs = append(allReportURLs, reportURLs...)
+		for key, value := range reportURLMap {
+			allReportURLMap[key] = value
+		}
+
+		//  测试环境跑部分数据,上线放开
+		//break
+		// Check if next page button is disabled
+		//  测试环境跑部分数据,上线放开
+		var nextPageDisabled bool
+		err = chromedp.Run(ctx,
+			chromedp.Evaluate(`document.querySelector('div.my-page-next').classList.contains('my-page-forbid')`, &nextPageDisabled),
+		)
+		if err != nil {
+			return err
+		}
+
+		if nextPageDisabled {
+			break
+		}
+
+		// Click the next page button
+		err = chromedp.Run(ctx,
+			chromedp.Click(`div.my-page-next`, chromedp.ByQuery),
+			chromedp.Sleep(5*time.Second),
+			chromedp.Location(&categoryPageURL),
+		)
+		if err != nil {
+			return err
+		}
+
+	}
+
+	logs.Info("所有报告 URLs: %s: %s: %v", product, category, allReportURLMap)
+
+	if len(allReportURLMap) == 0 {
+		return fmt.Errorf("未找到报告 URL")
+	}
+
+	// 处理报告数据
+	for key, value := range allReportURLMap {
+		// 查询报告是否已经处理  这里只对近7天的数据进行处理
+		paramsLib := make(map[string]interface{})
+		paramsLib["Url"] = key
+		postEdbLib, err := utils.PostEdbLibRequest(paramsLib, utils.GET_LY_INDEX_RECORD_BY_URL)
+		if err != nil {
+			// 有错误就不继续执行
+			log.Printf("postEdbLib err: %v", err)
+			continue
+		}
+		var requestResponse models.RequestResponse[models.BaseFromLyIndexRecord]
+		err = json.Unmarshal(postEdbLib, &requestResponse)
+		lyIndexRecord := requestResponse.Data
+		if lyIndexRecord.DataTime != "" {
+			toTime, err := utils.StringToTime(lyIndexRecord.DataTime + " 00:00:00")
+			if err != nil {
+				logs.Error("时间格式转换错误: %s: %s: %s: %s: %v", product, category, report, key, err)
+				continue
+			}
+
+			if time.Now().Sub(toTime) > 7*24*time.Hour {
+				logs.Info("报告已处理: %s: %s: %s: %s", product, category, report, key)
+				continue
+			}
+		}
+
+		// 随机睡眠
+		rand := utils.RangeRand(20, 100)
+		fmt.Println(report+";sleep:", strconv.Itoa(int(rand)))
+		time.Sleep(time.Duration(rand) * time.Second)
+
+		err = processReport(ctx, product, category, key, keywords)
+		if err != nil {
+			logs.Error("处理报告错误: %s: %s: %s: %s: %v", product, category, report, key, err)
+			if strings.Contains(err.Error(), "您看文章的速度太快了,歇一会再看吧") {
+				// 如果报告内容包含 “您看文章的速度太快了,歇一会再看吧” 则停止处理,发短信通知
+				// 发送短信通知
+				alarm_msg.SendAlarmMsg(fmt.Sprintf("粮油商务网-爬取指标数据被限制,请稍后重试, ErrMsg: %s", err.Error()), 1)
+				return nil
+			}
+			continue
+		}
+
+		format, err := utils.ConvertTimeFormat(value)
+		if err != nil {
+			logs.Error("时间格式转换错误: %s, %s, %v: %v", product, category, value, err)
+			continue
+		}
+
+		// 处理报告成功,将维护指标数据读取进度到数据库,避免后面重复读取
+		recordId, err := models.AddLyIndexRecord(&models.BaseFromLyIndexRecord{
+			CreateTime: utils.GetCurrentTime(),
+			ModifyTime: utils.GetCurrentTime(),
+			Product:    product,
+			Category:   category,
+			Url:        key,
+			DataTime:   format,
+		})
+		if err != nil {
+			logs.Error("维护指标数据读取进度错误: %s, %s, %v: %v", product, category, recordId, err)
+			continue
+		}
+		logs.Info("维护指标数据读取进度成功: %s, %s, %v", product, category, recordId)
+	}
+
+	return nil
+}
+
+func fillProductPageURL(ctx context.Context, product string, category string) (string, error) {
+	// 选择 dl 标签下所有 a 标签的 XPath
+	selector := `//dl[contains(@class, 'dl_hot')]//a`
+	logs.Info("选择器表达式: %s", selector)
+
+	var nodes []*cdp.Node
+	var productPageURL string
+
+	// 获取 dl 标签下的所有 a 标签节点
+	err := chromedp.Run(ctx,
+		chromedp.WaitReady(selector, chromedp.BySearch),
+		chromedp.Nodes(selector, &nodes, chromedp.BySearch),
+	)
+	if err != nil {
+		return "", err
+	}
+
+	// 提取并打印所有 a 标签的 OuterHTML
+	var targetURL string
+	for _, node := range nodes {
+		var outerHTML string
+
+		// 获取 a 标签的 OuterHTML
+		err = chromedp.Run(ctx,
+			chromedp.OuterHTML(node.FullXPath(), &outerHTML, chromedp.BySearch),
+		)
+		if err != nil {
+			return "", err
+		}
+
+		// 打印获取的 OuterHTML 内容
+		logs.Info("Link OuterHTML: %s", outerHTML)
+
+		// 从 OuterHTML 中提取 href 和文本内容
+		// 使用正则或字符串处理提取 href 和文本内容
+		href, linkText := extractHrefAndText(outerHTML)
+
+		// 打印提取的 href 和文本内容
+		logs.Info("Link Text: %s, Href: %s", linkText, href)
+
+		// 如果文本内容匹配目标产品
+		if linkText == product {
+			// 拼接完整的 URL
+			/*if !strings.HasPrefix(href, "http") {
+				href = lyLoginPath + href
+			}*/
+			targetURL = href
+			break
+		}
+	}
+
+	if targetURL == "" {
+		return "", fmt.Errorf("未找到匹配的产品链接")
+	}
+
+	// 显示更多内容
+	err = chromedp.Run(ctx,
+		chromedp.Evaluate(`document.getElementById("moreSpeList").style.display = "block";`, nil),
+	)
+	if err != nil {
+		return "", err
+	}
+
+	// 点击目标产品的链接
+	clickSelector := fmt.Sprintf(`//a[@href='%s']`, targetURL)
+	err = chromedp.Run(ctx,
+		chromedp.WaitReady(clickSelector, chromedp.BySearch),
+		chromedp.Click(clickSelector, chromedp.BySearch),
+		chromedp.Sleep(5*time.Second),
+		chromedp.Location(&productPageURL),
+	)
+	if err != nil {
+		return "", err
+	}
+
+	// 返回点击后的页面URL
+	logs.Info("productPageURL: %s", productPageURL)
+	return productPageURL, nil
+}
+
+// extractHrefAndText 从 OuterHTML 提取 href 和文本内容的辅助函数
+func extractHrefAndText(outerHTML string) (string, string) {
+	// 使用正则表达式或其他字符串处理方法提取 href 和文本内容
+	// 这里只是一个简单的例子,具体实现需要根据 HTML 结构来调整
+	hrefRegex := `href="([^"]+)"`
+	textRegex := `>([^<]+)<`
+
+	hrefMatches := regexp.MustCompile(hrefRegex).FindStringSubmatch(outerHTML)
+	textMatches := regexp.MustCompile(textRegex).FindStringSubmatch(outerHTML)
+
+	href := ""
+	linkText := ""
+	if len(hrefMatches) > 1 {
+		href = hrefMatches[1]
+	}
+	if len(textMatches) > 1 {
+		linkText = textMatches[1]
+	}
+
+	return href, linkText
+}
+
+// Extract report URLs from the HTML content
+func extractReportURLs(htmlContent, keyword string) map[string]string {
+	//var reportURLs []string
+	var reportURLMap = make(map[string]string)
+	var reportURL string
+
+	// Find all occurrences of the keyword and extract report URLs
+	content := htmlContent
+	for {
+		startIdx := strings.Index(content, keyword)
+		if startIdx == -1 {
+			break
+		}
+		startIdx += len(keyword)
+
+		// Extract the URL from the HTML content
+		urlStartIdx := strings.LastIndex(content[:startIdx], `href="`) + len(`href="`)
+		urlEndIdx := strings.Index(content[urlStartIdx:], `"`) + urlStartIdx
+		if urlStartIdx > 0 && urlEndIdx > urlStartIdx {
+			reportURL = content[urlStartIdx:urlEndIdx]
+			//reportURLs = append(reportURLs, content[urlStartIdx:urlEndIdx])
+		}
+
+		content = content[startIdx:]
+
+		// Now extract the content inside the first <div class="short_right">
+		divStartIdx := strings.Index(content, `<div class="short_right">`)
+		if divStartIdx != -1 {
+			divStartIdx += len(`<div class="short_right">`)
+			divEndIdx := strings.Index(content[divStartIdx:], `</div>`) + divStartIdx
+			if divEndIdx > divStartIdx {
+				shortRightContent := content[divStartIdx:divEndIdx]
+
+				// Extract the first <div> content inside <div class="short_right">
+				innerDivStartIdx := strings.Index(shortRightContent, `<div>`)
+				if innerDivStartIdx != -1 {
+					innerDivStartIdx += len(`<div>`)
+					//innerDivEndIdx := strings.Index(shortRightContent[innerDivStartIdx:], `</div>`) + innerDivStartIdx
+					innerDivContent := shortRightContent[innerDivStartIdx:]
+					fmt.Println("Inner Div Content:", innerDivContent)
+					reportURLMap[reportURL] = innerDivContent
+				}
+			}
+		}
+	}
+
+	return reportURLMap
+}
+
+// Process the report data
+func processReport(ctx context.Context, product string, category string, reportURL string, keywords []string) error {
+	// Navigate to the report page
+	var reportContent string
+	err := chromedp.Run(ctx,
+		chromedp.Navigate(lyLoginPath+reportURL),
+		chromedp.WaitVisible("body", chromedp.ByQuery), // 等待 body 元素可见,确保页面已加载
+		chromedp.Sleep(5*time.Second),                  // 等待额外时间,以确保动态内容加载
+		chromedp.OuterHTML("html", &reportContent),     // 获取页面 HTML 内容
+	)
+	if err != nil {
+		return err
+	}
+
+	// 如果文章内容包含 “您看文章的速度太快了,歇一会再看吧” 则返回指定错误
+	if strings.Contains(reportContent, "您看文章的速度太快了,歇一会再看吧") {
+		return fmt.Errorf("您看文章的速度太快了,歇一会再看吧")
+	}
+
+	var lyIndexDataList []models.BaseFromLyData
+	// Process the data based on keywords
+	for _, keyword := range keywords {
+		partialKeyword := strings.Split(keyword, ":")
+		// Select appropriate processor based on product and category
+		processor, err := GetProcessor(product, category)
+		if err != nil {
+			return err
+		}
+
+		// 查询报告所属分类
+		classify, err := models.GetLyClassifyByName(product)
+		if err != nil {
+			return err
+		}
+
+		// Process the report content using the selected processor
+		baseFromLyDataList, err := processor.Process(ctx, product, reportContent, partialKeyword, classify.BaseFromLyClassifyId)
+		if err != nil {
+			return err
+		}
+		if len(baseFromLyDataList) > 0 {
+			for _, baseFromLyData := range baseFromLyDataList {
+				if baseFromLyData.DataTime != "" && baseFromLyData.IndexCode != "" && baseFromLyData.IndexCode != "lysww" {
+					baseFromLyData.CreateTime = utils.GetCurrentTime()
+					baseFromLyData.ModifyTime = utils.GetCurrentTime()
+					lyIndexDataList = append(lyIndexDataList, baseFromLyData)
+				}
+			}
+		}
+
+	}
+	// 新增指标数据
+	if len(lyIndexDataList) > 0 {
+		err = models.AddLyDataList(lyIndexDataList)
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func addLyIndex(classifyId int, indexCode string, indexName string, unit string, frequency string) (int, error) {
+	// 添加指标
+	index := &models.BaseFromLyIndex{
+		CreateTime:           utils.GetCurrentTime(),
+		ModifyTime:           utils.GetCurrentTime(),
+		BaseFromLyClassifyId: classifyId,
+		IndexCode:            indexCode,
+		IndexName:            indexName,
+		Frequency:            frequency,
+		Unit:                 unit,
+		EdbExist:             0,
+	}
+	indexId, err := models.AddLyIndex(index)
+	if err != nil {
+		return 0, err
+	}
+	return int(indexId), nil
+}

+ 2278 - 0
services/liangyou/processor_business_logic.go

@@ -0,0 +1,2278 @@
+// @Author gmy 2024/8/6 10:50:00
+package main
+
+import (
+	"context"
+	"eta/eta_data_analysis/models"
+	"eta/eta_data_analysis/utils"
+	"fmt"
+	"github.com/PuerkitoBio/goquery"
+	"github.com/beego/beego/v2/core/logs"
+	"github.com/chromedp/chromedp"
+	"log"
+	"regexp"
+	"strconv"
+	"strings"
+	"unicode"
+)
+
+var (
+	sourceName = "lysww" // 粮油商务网
+)
+
+// TableData 用于存储表格的数据
+type TableData struct {
+	Headers []string   `json:"headers"`
+	Rows    [][]string `json:"rows"`
+}
+
+// ImportCostProcessor
+// @Description: 进口成本处理器
+type ImportCostProcessor struct{}
+
+func (p *ImportCostProcessor) Process(ctx context.Context, product string, reportContent string, keywords []string, classifyId int) ([]models.BaseFromLyData, error) {
+	logs.Info("Processing import cost...")
+
+	// 解析关键字
+	if len(keywords) < 5 {
+		return []models.BaseFromLyData{}, fmt.Errorf("ProcessingImportCostProcessor Process() : keywords must contain at least 5 elements")
+	}
+
+	// 拿到 行关键字和列关键字
+	columnName := keywords[len(keywords)-4]
+	rowVariety := keywords[0]
+	rowPort := keywords[len(keywords)-3]
+	indexNamePrefix := keywords[:1]
+	indexNameSuffix := keywords[1:]
+
+	// 提取所有表格数据
+	tableData := getNoHeadTableData(reportContent)
+
+	// 提取日期信息
+	dateText, err := getDateInfo(ctx)
+	if err != nil {
+		return []models.BaseFromLyData{}, err
+	}
+
+	// 时间格式转换
+	format, err := utils.ConvertTimeFormat(dateText)
+	if err != nil {
+		return []models.BaseFromLyData{}, err
+	}
+
+	// 解析日期并计算当前月份
+	var targetMonths []string
+	if product == "油菜籽" {
+		targetMonths, err = utils.ParseDateAndMonthColzaOil(format)
+	} else {
+		targetMonths, err = utils.ParseDateAndMonth(dateText)
+	}
+	if err != nil {
+		return []models.BaseFromLyData{}, fmt.Errorf("ProcessingImportCostProcessor Process() : Failed to parse date: %v", err)
+	}
+	fmt.Printf("Target Month: %s\n", targetMonths)
+
+	// 处理提取的表格数据
+	var result []models.BaseFromLyData
+
+	for _, data := range tableData {
+		tableHeaders := data.Headers
+		tableRows := data.Rows
+
+		// 查找目标列
+		columnIdx := -1
+		for i, header := range tableHeaders {
+			if strings.Contains(header, columnName) {
+				columnIdx = i
+				break
+			}
+		}
+		if columnIdx == -1 {
+			log.Printf("ProcessingImportCostProcessor Process() : Column '%s' not found in table", columnName)
+			continue
+		}
+
+		// 处理表格中的每一行
+		//var flag bool = true
+		var previousRowVariety string
+		var previousRowPort string
+		for rowIndex, row := range tableRows {
+			if len(row) == len(tableHeaders) {
+				previousRowVariety = row[0]
+				previousRowPort = row[1]
+			} else if len(row) == len(tableHeaders)-1 {
+				previousRowPort = row[0]
+				row = append([]string{previousRowVariety}, row...)
+				tableRows[rowIndex] = row
+			} else if len(row) == len(tableHeaders)-2 {
+				row = append([]string{previousRowVariety, previousRowPort}, row...)
+				tableRows[rowIndex] = row
+			}
+			for _, targetMonth := range targetMonths {
+				if len(row) >= len(tableHeaders) && strings.Contains(rowVariety, row[0]) && row[1] == targetMonth && row[len(row)-1] == rowPort {
+					if columnIdx < len(row) {
+						// 指标名称
+						indexNameList := append(indexNamePrefix, append([]string{targetMonth}, indexNameSuffix...)...)
+						indexName := strings.Join(indexNameList[:len(keywords)-2], ":")
+						// 指标编码
+						indexCode := utils.GenerateIndexCode(sourceName, indexName)
+						// 指标id获取
+						indexId, err := getIndexId(indexCode, indexName, classifyId, sourceName, keywords[len(keywords)-2], keywords[len(keywords)-1])
+						if err != nil {
+							logs.Error("ProcessingImportCostProcessor Process() : Failed to get index id: %v", err)
+							continue
+						}
+
+						indexData, err := models.GetLyDataByIndexIdAndDataTime(indexId, format)
+						if err != nil {
+							logs.Error("ProcessingImportCostProcessor Process() : Failed to get data by index id and date: %v", err)
+							continue
+						}
+						if len(indexData) > 0 {
+							logs.Info("ProcessingImportCostProcessor Process() : Data already exists for index %d and date %s", indexId, dateText)
+							continue
+						}
+
+						valueStr := row[columnIdx]
+						value, err := strconv.ParseFloat(valueStr, 64)
+						if err != nil {
+							return []models.BaseFromLyData{}, fmt.Errorf("failed to parse value '%s': %v", valueStr, err)
+						}
+						// 创建并添加到结果列表
+						baseFromLyData := models.BaseFromLyData{
+							DataTime:          format,
+							Value:             value,
+							BaseFromLyIndexId: indexId,
+							IndexCode:         indexCode,
+						}
+						result = append(result, baseFromLyData)
+					} else {
+						log.Printf("ProcessingImportCostProcessor Process() : Column index out of range for row '%s', '%s'", rowVariety, rowPort)
+					}
+					break
+				}
+			}
+
+		}
+
+	}
+
+	return result, nil
+}
+
+// ProcessingProfitProcessor
+// @Description: 加工利润处理器
+type ProcessingProfitProcessor struct{}
+
+func (p *ProcessingProfitProcessor) Process(ctx context.Context, product string, reportContent string, keywords []string, classifyId int) ([]models.BaseFromLyData, error) {
+	fmt.Println("Processing processing profit...")
+	// 解析关键字
+	if len(keywords) < 4 {
+		return []models.BaseFromLyData{}, fmt.Errorf("ProcessingProfitProcessor Process() : keywords must contain at least 4 elements")
+	}
+
+	// 拿到 行关键字和列关键字
+	columnName := keywords[1]
+	rowVariety := keywords[0]
+	indexNamePrefix := keywords[:1]
+	indexNameSuffix := keywords[1:]
+
+	// 提取所有表格数据
+	tableData := getNoHeadTableData(reportContent)
+
+	// 提取日期信息
+	dateText, err := getDateInfo(ctx)
+	if err != nil {
+		return []models.BaseFromLyData{}, err
+	}
+
+	// 时间格式转换
+	format, err := utils.ConvertTimeFormat(dateText)
+	if err != nil {
+		return []models.BaseFromLyData{}, err
+	}
+
+	// 解析日期并计算当前月份 和 后两月
+	yearMonths, err := utils.ConvertTimeFormatToYearMonth(format)
+	if err != nil {
+		return nil, err
+	}
+	fmt.Printf("Target yearMonth: %s\n", yearMonths)
+
+	// 处理提取的表格数据
+	var result []models.BaseFromLyData
+
+	for _, data := range tableData {
+		tableHeaders := data.Headers
+		tableRows := data.Rows
+
+		// 查找目标列
+		columnIdx := -1
+		for i, header := range tableHeaders {
+			if strings.Contains(columnName, header) {
+				columnIdx = i
+				break
+			}
+		}
+		if columnIdx == -1 {
+			log.Printf("ProcessingProfitProcessor Process() : Column '%s' not found in table", columnName)
+			continue
+		}
+
+		// 处理表格中的每一行
+		var previousRowVariety string
+		for rowIndex, row := range tableRows {
+			if len(row) == len(tableHeaders) {
+				previousRowVariety = row[0]
+			} else if len(row) == len(tableHeaders)-1 {
+				row = append([]string{previousRowVariety}, row...)
+				tableRows[rowIndex] = row
+			}
+
+			for _, targetMonth := range yearMonths {
+				if len(row) >= len(tableHeaders) && row[0] == rowVariety && row[1] == targetMonth {
+					if columnIdx < len(row) {
+						// 指标名称
+						indexNameList := append(indexNamePrefix, append([]string{targetMonth}, indexNameSuffix...)...)
+						indexName := strings.Join(indexNameList[:len(keywords)-1], ":")
+						// 指标编码
+						indexCode := utils.GenerateIndexCode(sourceName, indexName)
+						// 指标id获取
+						indexId, err := getIndexId(indexCode, indexName, classifyId, sourceName, keywords[len(keywords)-2], keywords[len(keywords)-1])
+						if err != nil {
+							logs.Error("ProcessingProfitProcessor Process() : Failed to get index id: %v", err)
+							continue
+						}
+
+						indexData, err := models.GetLyDataByIndexIdAndDataTime(indexId, format)
+						if err != nil {
+							logs.Error("ProcessingProfitProcessor Process() : Failed to get data by index id and date: %v", err)
+							continue
+						}
+						if len(indexData) > 0 {
+							logs.Info("ProcessingProfitProcessor Process() : Data already exists for index %d and date %s", indexId, dateText)
+							continue
+						}
+
+						valueStr := row[columnIdx]
+						value, err := strconv.ParseFloat(valueStr, 64)
+						if err != nil {
+							return []models.BaseFromLyData{}, fmt.Errorf("failed to parse value '%s': %v", valueStr, err)
+						}
+						// 创建并添加到结果列表
+						baseFromLyData := models.BaseFromLyData{
+							DataTime:          format,
+							Value:             value,
+							BaseFromLyIndexId: indexId,
+							IndexCode:         indexCode,
+						}
+						result = append(result, baseFromLyData)
+					} else {
+						log.Printf("ProcessingProfitProcessor Process() : Column index out of range for row '%s', '%s'", rowVariety, columnName)
+					}
+					break
+				}
+			}
+
+		}
+	}
+
+	return result, nil
+}
+
+// ShippingCostProcessor
+// @Description: 船运费用处理器
+type ShippingCostProcessor struct{}
+
+func (p *ShippingCostProcessor) Process(ctx context.Context, product string, reportContent string, keywords []string, classifyId int) ([]models.BaseFromLyData, error) {
+	fmt.Println("Processing processing profit...")
+	// 解析关键字
+	if len(keywords) < 4 {
+		return []models.BaseFromLyData{}, fmt.Errorf("ShippingCostProcessor Process() : keywords must contain at least 5 elements")
+	}
+
+	// 拿到 行关键字和列关键字
+	columnName := keywords[len(keywords)-3]
+	rowVariety := keywords[0]
+	rowDestination := keywords[1]
+	rowShipType := keywords[2]
+
+	// 提取所有表格数据
+	tableData := getNoHeadTableData(reportContent)
+
+	// 提取日期信息
+	dateText, err := getDateInfo(ctx)
+	if err != nil {
+		return []models.BaseFromLyData{}, err
+	}
+
+	// 时间格式转换
+	format, err := utils.ConvertTimeFormat(dateText)
+	if err != nil {
+		return []models.BaseFromLyData{}, err
+	}
+
+	// 处理提取的表格数据
+	var result []models.BaseFromLyData
+
+	for _, data := range tableData {
+		tableHeaders := data.Headers
+		tableRows := data.Rows
+
+		// 查找目标列
+		columnIdx := -1
+		for i, header := range tableHeaders {
+			if strings.Contains(header, columnName) {
+				columnIdx = i
+				break
+			}
+		}
+		if columnIdx == -1 {
+			log.Printf("ShippingCostProcessor Process() : Column '%s' not found in table", columnName)
+			continue
+		}
+
+		// 处理表格中的每一行
+		for rowIndex, row := range tableRows {
+			if len(row) == len(tableHeaders)-1 {
+				row = append([]string{rowVariety}, row...)
+				tableRows[rowIndex] = row
+				rowShipType, err = extractValueInParentheses(rowVariety)
+				if err != nil {
+					logs.Error("ShippingCostProcessor Process() : Failed to extract value in parentheses: %v", err)
+					continue
+				}
+
+			}
+			if len(row) >= len(tableHeaders) && row[0] == rowVariety && (row[1] == rowDestination || strings.Contains(row[0], row[1])) && row[2] == rowShipType {
+				if columnIdx < len(row) {
+					// 指标名称
+					indexName := strings.Join(keywords[:len(keywords)-3], `:`)
+					// 指标编码
+					indexCode := utils.GenerateIndexCode(sourceName, indexName)
+					// 指标id获取
+					indexId, err := getIndexId(indexCode, indexName, classifyId, sourceName, keywords[len(keywords)-2], keywords[len(keywords)-1])
+					if err != nil {
+						logs.Error("ShippingCostProcessor Process() : Failed to get index id: %v", err)
+						continue
+					}
+
+					indexData, err := models.GetLyDataByIndexIdAndDataTime(indexId, format)
+					if err != nil {
+						logs.Error("ShippingCostProcessor Process() : Failed to get data by index id and date: %v", err)
+						continue
+					}
+					if len(indexData) > 0 {
+						logs.Info("ShippingCostProcessor Process() : Data already exists for index %d and date %s", indexId, dateText)
+						continue
+					}
+
+					valueStr := row[columnIdx]
+					value, err := strconv.ParseFloat(valueStr, 64)
+					if err != nil {
+						return []models.BaseFromLyData{}, fmt.Errorf("failed to parse value '%s': %v", valueStr, err)
+					}
+					// 创建并添加到结果列表
+					baseFromLyData := models.BaseFromLyData{
+						DataTime:          format,
+						Value:             value,
+						BaseFromLyIndexId: indexId,
+						IndexCode:         indexCode,
+					}
+					result = append(result, baseFromLyData)
+				} else {
+					log.Printf("ShippingCostProcessor Process() : Column index out of range for row '%s', '%s'", rowVariety, columnName)
+				}
+				break
+			}
+		}
+	}
+
+	return result, nil
+}
+
+// SupplyDemandBalanceProcessor
+// @Description: 供需平衡处理器
+type SupplyDemandBalanceProcessor struct{}
+
+func (p *SupplyDemandBalanceProcessor) Process(ctx context.Context, product string, reportContent string, keywords []string, classifyId int) ([]models.BaseFromLyData, error) {
+	// https://www.fao.com.cn/art/gG7gKTCNDHLJNsq9QRYjoQ==.htm
+	logs.Info("Processing processing report...")
+	// 解析关键字
+	if len(keywords) < 4 {
+		return []models.BaseFromLyData{}, fmt.Errorf("SupplyDemandBalanceProcessor Process() : keywords must contain at least 4 elements")
+	}
+
+	// 拿到 行关键字和列关键字
+	var columnName string
+	rowVariety := keywords[1]
+
+	// 提取所有表格数据
+	tableData := getTableData(reportContent, true)
+	logs.Info("SupplyDemandBalanceProcessor Process() : Table data: %v", tableData)
+
+	// 提取日期信息
+	dateText, err := getDateInfo(ctx)
+	if err != nil {
+		return []models.BaseFromLyData{}, err
+	}
+
+	// 时间格式转换
+	format, err := utils.ConvertTimeFormat(dateText)
+	if err != nil {
+		return []models.BaseFromLyData{}, err
+	}
+
+	currentYearAndNextYear, err := utils.GetCurrentYearAndNextYear(format)
+	if err != nil {
+		return nil, err
+	}
+
+	month, err := utils.GetCurrentMonth(format)
+	if err != nil {
+		return nil, err
+	}
+	monthSuffix := "预估"
+	logs.Info("SupplyDemandBalanceProcessor Process() : Target Year: %s:%s\n", currentYearAndNextYear, month+monthSuffix)
+
+	// 处理提取的表格数据
+	var result []models.BaseFromLyData
+
+	headers := tableData.Headers
+	rows := tableData.Rows
+
+	for _, year := range currentYearAndNextYear {
+		columnName = year + month + monthSuffix
+		isCurrentYear, err := utils.IsCurrentYear(year)
+		if err != nil {
+			logs.Error("SupplyDemandBalanceProcessor Process() : Failed to determine if year is current year: %v", err)
+			continue
+		}
+		if !isCurrentYear {
+			format, err = utils.GetNextYearLastDay(format)
+			if err != nil {
+				logs.Error("SupplyDemandBalanceProcessor Process() : Failed to get next year last day: %v", err)
+				continue
+			}
+		}
+
+		// 查找目标列
+		columnIdx := -1
+		for i, header := range headers {
+			if strings.Contains(columnName, header) {
+				columnIdx = i
+				break
+			}
+		}
+		if columnIdx == -1 {
+			logs.Error("SupplyDemandBalanceProcessor Process() : Column '%s' not found in table", columnName)
+			continue
+		}
+		// 处理表格中的每一行
+		for _, row := range rows {
+			if len(row) >= len(headers) && row[0] == rowVariety {
+				if columnIdx < len(row) {
+					// 指标名称
+					indexName := strings.Join(keywords[:len(keywords)-2], ":")
+					// 指标编码
+					indexCode := utils.GenerateIndexCode(sourceName, indexName)
+					// 指标id获取
+					indexId, err := getIndexId(indexCode, indexName, classifyId, sourceName, keywords[len(keywords)-2], keywords[len(keywords)-1])
+					if err != nil {
+						logs.Error("SupplyDemandBalanceProcessor Process() : Failed to get index id: %v", err)
+						continue
+					}
+					valueStr := row[columnIdx]
+					value, err := strconv.ParseFloat(valueStr, 64)
+					if err != nil {
+						return []models.BaseFromLyData{}, fmt.Errorf("SupplyDemandBalanceProcessor Process() : failed to parse value '%s': %v", valueStr, err)
+					}
+					yearMonth, err := utils.GetYearMonth(format)
+					if err != nil {
+						logs.Error("SupplyDemandBalanceProcessor Process() : Failed to get year month: %v", err)
+						continue
+					}
+					indexData, err := models.GetLyDataByIndexIdAndDataTimeYM(indexId, yearMonth)
+					if err != nil {
+						logs.Error("SupplyDemandBalanceProcessor Process() : Failed to get data by index id and date: %v", err)
+						continue
+					}
+					if len(indexData) > 0 {
+						logs.Info("SupplyDemandBalanceProcessor Process() : Data already exists for index %d and date %s", indexId, dateText)
+						// 存在走更新逻辑 主要更新今年在去年的预估值
+						indexData := indexData[0]
+						if indexData.Value != value {
+							time, err := utils.StringToTime(indexData.ModifyTime)
+							if err != nil {
+								return nil, err
+							}
+
+							timeZero, err := utils.StringToTimeZero(format)
+							if err != nil {
+								return nil, err
+							}
+
+							if time.Before(timeZero) {
+								// 更新指标数据
+								err := models.UpdateLyDataById(indexData.BaseFromLyDataId, value)
+								if err != nil {
+									logs.Error("SupplyDemandBalanceProcessor Process() : Failed to update data: %v", err)
+									continue
+								}
+								// 更新指标库数据
+								edbIndexData, err := models.GetLyEdbDataByIndexCodeAndDataTime(indexData.IndexCode, yearMonth)
+								if err != nil {
+									return nil, err
+								}
+								if len(edbIndexData) > 0 {
+									err := models.UpdateLyEdbDataById(edbIndexData[0].EdbInfoId, value)
+									if err != nil {
+										return nil, err
+									}
+								}
+							}
+						}
+						continue
+					}
+
+					// 创建并添加到结果列表
+					baseFromLyData := models.BaseFromLyData{
+						DataTime:          format,
+						Value:             value,
+						BaseFromLyIndexId: indexId,
+						IndexCode:         indexCode,
+					}
+					result = append(result, baseFromLyData)
+				} else {
+					log.Printf("SupplyDemandBalanceProcessor Process() : Column index out of range for row '%s', '%s'", rowVariety, columnName)
+				}
+				break
+			}
+		}
+	}
+	return result, nil
+}
+
+// PurchaseShippingProcessor
+// @Description: 采购装船处理器
+type PurchaseShippingProcessor struct{}
+
+func (p *PurchaseShippingProcessor) Process(ctx context.Context, product string, reportContent string, keywords []string, classifyId int) ([]models.BaseFromLyData, error) {
+	logs.Info("Processing purchase shipping...")
+	// 解析关键字
+	if len(keywords) < 3 {
+		return []models.BaseFromLyData{}, fmt.Errorf("SupplyDemandBalanceProcessor Process() : keywords must contain at least 3 elements")
+	}
+
+	// 拿到 行关键字和列关键字
+	columnName := keywords[len(keywords)-3]
+
+	// 提取所有表格数据
+	tableData := getPurchaseShippingTableData(reportContent)
+	logs.Info("SupplyDemandBalanceProcessor Process() : Table data: %v", tableData)
+
+	// 提取日期信息
+	dateText, err := getDateInfo(ctx)
+	if err != nil {
+		return []models.BaseFromLyData{}, err
+	}
+
+	// 时间格式转换
+	format, err := utils.ConvertTimeFormat(dateText)
+	if err != nil {
+		return []models.BaseFromLyData{}, err
+	}
+
+	// 处理提取的表格数据
+	var result []models.BaseFromLyData
+
+	headers := tableData.Headers
+	rows := tableData.Rows
+
+	// 查找目标列
+	columnIdx := -1
+	for i, header := range headers {
+		if strings.Contains(columnName, header) {
+			columnIdx = i
+			break
+		}
+	}
+	if columnIdx == -1 {
+		log.Printf("SupplyDemandBalanceProcessor Process() : Column '%s' not found in table", columnName)
+	} else {
+		// 处理表格中的每一行
+		for _, row := range rows {
+			if len(row) >= len(headers) {
+				if columnIdx < len(row) {
+					if !isNumber(row[columnIdx]) {
+						continue
+					}
+					// 指标名称
+					indexName := strings.Join(keywords[:len(keywords)-3], ":")
+					// 指标编码
+					indexCode := utils.GenerateIndexCode(sourceName, indexName)
+					// 指标id获取
+					indexId, err := getIndexId(indexCode, indexName, classifyId, sourceName, keywords[len(keywords)-2], keywords[len(keywords)-1])
+					if err != nil {
+						logs.Error("SupplyDemandBalanceProcessor Process() : Failed to get index id: %v", err)
+						continue
+					}
+					var yearMonth string
+					number, err := utils.ConvertMonthToNumber(row[1])
+					if err != nil {
+						return nil, err
+					}
+					yearMonth = row[0] + "-" + number
+					isSameMonth, err := utils.IsSameMonth(format, yearMonth)
+					if err != nil {
+						return nil, err
+					}
+					if isSameMonth {
+						yearMonth = format
+					} else {
+						lastDayOfMonth, err := utils.GetLastDayOfMonth(yearMonth)
+						if err != nil {
+							return nil, err
+						}
+						yearMonth = lastDayOfMonth
+					}
+
+					valueStr := row[columnIdx]
+					value, err := strconv.ParseFloat(valueStr, 64)
+					if err != nil {
+						return []models.BaseFromLyData{}, fmt.Errorf("SupplyDemandBalanceProcessor Process() : failed to parse value '%s': %v", valueStr, err)
+					}
+
+					month, err := utils.GetYearMonth(yearMonth)
+					if err != nil {
+						return nil, err
+					}
+					indexData, err := models.GetLyDataByIndexIdAndDataTimeYM(indexId, month)
+					if err != nil {
+						logs.Error("SupplyDemandBalanceProcessor Process() : Failed to get data by index id and date: %v", err)
+						continue
+					}
+					if len(indexData) > 0 {
+						if indexData[0].Value != value {
+							logs.Info("SupplyDemandBalanceProcessor Process() : Data already exists for index %d and date %s", indexId, dateText)
+
+							lyData := indexData[0]
+							time, err := utils.StringToTime(lyData.ModifyTime)
+							if err != nil {
+								return nil, err
+							}
+
+							timeZero, err := utils.StringToTimeZero(format)
+							if err != nil {
+								return nil, err
+							}
+
+							if time.Before(timeZero) {
+								// 更新指标数据
+								err := models.UpdateLyDataById(lyData.BaseFromLyDataId, value)
+								if err != nil {
+									return nil, err
+								}
+
+								// 同步更新指标库数据 须根据指标编码和日期更新
+								edbIndexData, err := models.GetLyEdbDataByIndexCodeAndDataTime(lyData.IndexCode, month)
+								if err != nil {
+									return nil, err
+								}
+								if len(edbIndexData) > 0 {
+									err := models.UpdateLyEdbDataById(edbIndexData[0].EdbInfoId, value)
+									if err != nil {
+										return nil, err
+									}
+								}
+							}
+						}
+						continue
+					}
+
+					// 创建并添加到结果列表
+					baseFromLyData := models.BaseFromLyData{
+						DataTime:          yearMonth,
+						Value:             value,
+						BaseFromLyIndexId: indexId,
+						IndexCode:         indexCode,
+					}
+					result = append(result, baseFromLyData)
+					continue
+				} else {
+					log.Printf("SupplyDemandBalanceProcessor Process() : Column index out of range for row '%s'", columnName)
+				}
+				break
+			}
+		}
+	}
+
+	return result, nil
+}
+
+// ProcessingReportProcessor
+// @Description: 加工报告处理器
+type ProcessingReportProcessor struct {
+}
+
+func (p *ProcessingReportProcessor) Process(ctx context.Context, product string, reportContent string, keywords []string, classifyId int) ([]models.BaseFromLyData, error) {
+	logs.Info("Processing processing report...")
+	// 解析关键字
+	if len(keywords) < 3 {
+		return []models.BaseFromLyData{}, fmt.Errorf("ProcessingReportProcessor Process() : keywords must contain at least 3 elements")
+	}
+
+	// 拿到 行关键字和列关键字
+	columnName := keywords[0]
+	rowName := keywords[1]
+
+	// 提取所有表格数据
+	tableData := getAllTableData(reportContent)
+
+	// 提取日期信息
+	dateText, err := getDateInfo(ctx)
+	if err != nil {
+		return []models.BaseFromLyData{}, err
+	}
+	indexName := strings.Join(keywords[:len(keywords)-2], ":")
+	// 指标编码
+	indexCode := utils.GenerateIndexCode(sourceName, indexName)
+	// 指标id获取
+	indexId, err := getIndexId(indexCode, indexName, classifyId, sourceName, keywords[len(keywords)-2], keywords[len(keywords)-1])
+	if err != nil {
+		return nil, err
+	}
+
+	// 校验指标数据是否存在 根据指标id和日期 存在则跳过,不存在正常往下走
+	format, err := utils.ConvertTimeFormat(dateText)
+	if err != nil {
+		return []models.BaseFromLyData{}, err
+	}
+	indexData, err := models.GetLyDataByIndexIdAndDataTime(indexId, format)
+	if err != nil {
+		return []models.BaseFromLyData{}, fmt.Errorf("ProcessingReportProcessor Process() : Failed to get data by index id and date: %v", err)
+	}
+	if len(indexData) > 0 {
+		logs.Info("ProcessingReportProcessor Process() : Data already exists for index %d and date %s", indexId, dateText)
+		// 不必做更新处理,报告每周刷新,即使本周和上周数据一致,也需要每周记录
+		return []models.BaseFromLyData{}, nil
+	}
+
+	// 解析日期并计算当前周数
+	targetWeek, err := utils.ParseDateAndWeek(dateText)
+	if err != nil {
+		return []models.BaseFromLyData{}, fmt.Errorf("ProcessingReportProcessor Process() : Failed to parse date: %v", err)
+	}
+
+	fmt.Printf("Target Week: %s\n", targetWeek)
+
+	var result []models.BaseFromLyData
+	// 处理提取的表格数据
+	for _, data := range tableData {
+		tableHeaders := data.Headers
+		tableRows := data.Rows
+
+		// 查找目标列
+		columnIdx := -1
+		for i, header := range tableHeaders {
+			headerString := extractChinese(header)
+			if strings.Contains(columnName, headerString) {
+				// 这个表格不是很好处理,这里写的有些僵硬,后续需要优化
+				if columnName == "国内大豆开机率" {
+					i = i + 2
+				}
+				columnIdx = i
+				break
+			}
+		}
+
+		if columnIdx == -1 {
+			logs.Error("ProcessingReportProcessor Process() : Column '%s' not found in table", columnName)
+			continue
+		}
+
+		// 查找本周的列位置
+		weekIdx := -1
+		for i, header := range tableHeaders {
+			if strings.Contains(header, targetWeek) && i > columnIdx {
+				weekIdx = i
+				break
+			}
+		}
+
+		if weekIdx == -1 {
+			fmt.Printf("Week column '%s' not found in table\n", targetWeek)
+			continue
+		}
+
+		// 查找目标行
+		for _, row := range tableRows {
+			if strings.Contains(row[0], rowName) {
+				if columnIdx < len(row) {
+					// 指标名称
+					indexName := strings.Join(keywords[:len(keywords)-2], ":")
+					// 指标编码
+					indexCode := utils.GenerateIndexCode(sourceName, indexName)
+					// 指标id获取
+					indexId, err := getIndexId(indexCode, indexName, classifyId, sourceName, keywords[len(keywords)-2], keywords[len(keywords)-1])
+					if err != nil {
+						logs.Error("ProcessingReportProcessor Process() : Failed to get index id: %v", err)
+						continue
+					}
+
+					indexData, err := models.GetLyDataByIndexIdAndDataTime(indexId, format)
+					if err != nil {
+						logs.Error("ProcessingReportProcessor Process() : Failed to get data by index id and date: %v", err)
+						continue
+					}
+					if len(indexData) > 0 {
+						logs.Info("ProcessingReportProcessor Process() : Data already exists for index %d and date %s", indexId, dateText)
+						// 无需走更新逻辑,报告每日更新,即使今天和昨天数据一致,也需要每天记录,如果到这里也只是说,今天这个报告被读取了两次
+						continue
+					}
+
+					valueStr := row[columnIdx]
+					value, err := strconv.ParseFloat(valueStr, 64)
+					if err != nil {
+						return []models.BaseFromLyData{}, fmt.Errorf("ProcessingReportProcessor Process() : failed to parse value '%s': %v", valueStr, err)
+					}
+					// 创建并添加到结果列表
+					baseFromLyData := models.BaseFromLyData{
+						DataTime:          format,
+						Value:             value,
+						BaseFromLyIndexId: indexId,
+						IndexCode:         indexCode,
+					}
+					result = append(result, baseFromLyData)
+					continue
+				} else {
+					log.Printf("ProcessingReportProcessor Process() : Column index out of range for row '%s', '%s'", rowName, columnName)
+				}
+				break
+			}
+
+			/*if len(row) > 0 && strings.Contains(row[0], rowName) {
+				if weekIdx < len(row) {
+					logs.Info("Value in column '%s' - '%s': %s", columnName, rowName, row[columnIdx])
+					numFlag := isNumeric(row[columnIdx])
+					if numFlag {
+						value, err := strconv.ParseFloat(row[columnIdx], 64)
+						if err != nil {
+							logs.Error("ProcessingReportProcessor Process() : Error converting value to float64: %v", err)
+							return []models.BaseFromLyData{}, err
+						}
+						// 返回BaseFromLyData对象的数据
+						baseFromLyData := models.BaseFromLyData{
+							DataTime: dateText,
+							Value:    value,
+						}
+						result = append(result, baseFromLyData)
+					}
+				} else {
+					logs.Error("ProcessingReportProcessor Process() : Column index out of range")
+				}
+			}*/
+		}
+	}
+
+	return result, nil
+}
+
+// InventoryAnalysisProcessor
+// @Description: 库存分析处理器
+type InventoryAnalysisProcessor struct{}
+
+func (p *InventoryAnalysisProcessor) Process(ctx context.Context, product string, reportContent string, keywords []string, classifyId int) ([]models.BaseFromLyData, error) {
+	// https://www.fao.com.cn/art/yg1IKj9FpPEIDv2LefnPhQ==.htm
+	logs.Info("Processing inventory analysis...")
+
+	// 解析关键字
+	if len(keywords) < 4 {
+		return []models.BaseFromLyData{}, fmt.Errorf("SupplyDemandBalanceProcessor Process() : keywords must contain at least 4 elements")
+	}
+
+	// 拿到 行关键字和列关键字
+	columnName := keywords[0]
+	rowVariety := keywords[1]
+
+	columnSuffix := "本周"
+	columnName = columnName + columnSuffix
+
+	// 提取所有表格数据
+	tableData := getTableData(reportContent, true)
+	logs.Info("SupplyDemandBalanceProcessor Process() : Table data: %v", tableData)
+
+	// 提取日期信息
+	dateText, err := getDateInfo(ctx)
+	if err != nil {
+		return []models.BaseFromLyData{}, err
+	}
+
+	// 时间格式转换
+	format, err := utils.ConvertTimeFormat(dateText)
+	if err != nil {
+		return []models.BaseFromLyData{}, err
+	}
+
+	// 处理提取的表格数据
+	var result []models.BaseFromLyData
+
+	headers := tableData.Headers
+	rows := tableData.Rows
+
+	// 查找目标列
+	columnIdx := -1
+	for i, header := range headers {
+		header := removeParentheses(header)
+		if strings.Contains(columnName, header) {
+			columnIdx = i
+			break
+		}
+	}
+	if columnIdx == -1 {
+		logs.Error("SupplyDemandBalanceProcessor Process() : Column '%s' not found in table", columnName)
+	} else {
+		// 处理表格中的每一行
+		for _, row := range rows {
+			if len(row) >= len(headers) && strings.Contains(row[0], rowVariety) {
+				if columnIdx < len(row) {
+					// 指标名称
+					indexName := strings.Join(keywords[:len(keywords)-2], ":")
+					indexName = removeParentheses(indexName)
+					// 指标编码
+					indexCode := utils.GenerateIndexCode(sourceName, indexName)
+					// 指标id获取
+					indexId, err := getIndexId(indexCode, indexName, classifyId, sourceName, keywords[len(keywords)-2], keywords[len(keywords)-1])
+					if err != nil {
+						logs.Error("SupplyDemandBalanceProcessor Process() : Failed to get index id: %v", err)
+						continue
+					}
+
+					indexData, err := models.GetLyDataByIndexIdAndDataTime(indexId, format)
+					if err != nil {
+						logs.Error("SupplyDemandBalanceProcessor Process() : Failed to get data by index id and date: %v", err)
+						continue
+					}
+					if len(indexData) > 0 {
+						logs.Info("SupplyDemandBalanceProcessor Process() : Data already exists for index %d and date %s", indexId, dateText)
+						// 无需走更新逻辑,报告每周更新,即使本周和上周数据一致,也需要每周记录,如果到这里也只是说,今天这个报告被读取了两次
+						continue
+					}
+
+					valueStr := row[columnIdx]
+					value, err := strconv.ParseFloat(valueStr, 64)
+					if err != nil {
+						return []models.BaseFromLyData{}, fmt.Errorf("SupplyDemandBalanceProcessor Process() : failed to parse value '%s': %v", valueStr, err)
+					}
+					// 创建并添加到结果列表
+					baseFromLyData := models.BaseFromLyData{
+						DataTime:          format,
+						Value:             value,
+						BaseFromLyIndexId: indexId,
+						IndexCode:         indexCode,
+					}
+					result = append(result, baseFromLyData)
+					continue
+				} else {
+					log.Printf("SupplyDemandBalanceProcessor Process() : Column index out of range for row '%s', '%s'", rowVariety, columnName)
+				}
+				break
+			}
+		}
+	}
+
+	return result, nil
+}
+
+// PriceSpreadArbitrageProcessor
+// @Description: 价差套利处理器
+type PriceSpreadArbitrageProcessor struct{}
+
+func (p *PriceSpreadArbitrageProcessor) Process(ctx context.Context, product string, reportContent string, keywords []string, classifyId int) ([]models.BaseFromLyData, error) {
+	fmt.Println("Processing processing profit...")
+	// 解析关键字
+	if len(keywords) < 4 {
+		return []models.BaseFromLyData{}, fmt.Errorf("ProcessingProfitProcessor Process() : keywords must contain at least 4 elements")
+	}
+
+	// 拿到 行关键字和列关键字
+	var columnDate string
+	rowVariety := keywords[0]
+	rowBourse := keywords[1]
+	// 提取所有表格数据
+	tableData := getNoHeadTableData(reportContent)
+
+	// 提取日期信息
+	dateText, err := getDateInfo(ctx)
+	if err != nil {
+		return []models.BaseFromLyData{}, err
+	}
+
+	// 时间格式转换
+	format, err := utils.ConvertTimeFormat(dateText)
+	if err != nil {
+		return []models.BaseFromLyData{}, err
+	}
+	day, err := utils.ConvertTimeFormatToYearMonthDay(format)
+	if err != nil {
+		return nil, err
+	}
+	columnDate = day
+
+	// 处理提取的表格数据
+	var result []models.BaseFromLyData
+
+	for _, data := range tableData {
+		tableHeaders := data.Headers
+		tableRows := data.Rows
+
+		// 查找目标列
+		columnIdx := -1
+		for i, header := range tableHeaders {
+			if strings.Contains(header, columnDate) {
+				columnIdx = i
+				break
+			}
+		}
+		if columnIdx == -1 {
+			log.Printf("ProcessingProfitProcessor Process() : Column '%s' not found in table", columnDate)
+			continue
+		}
+
+		// 处理表格中的每一行
+		for _, row := range tableRows {
+			if len(row) >= len(tableHeaders) && row[0] == rowVariety && row[1] == rowBourse {
+				if columnIdx < len(row) {
+					// 指标名称
+					indexName := strings.Join(keywords[:len(keywords)-2], ":")
+					// 指标编码
+					indexCode := utils.GenerateIndexCode(sourceName, indexName)
+					// 指标id获取
+					indexId, err := getIndexId(indexCode, indexName, classifyId, sourceName, keywords[len(keywords)-2], keywords[len(keywords)-1])
+					if err != nil {
+						logs.Error("ProcessingProfitProcessor Process() : Failed to get index id: %v", err)
+						continue
+					}
+
+					indexData, err := models.GetLyDataByIndexIdAndDataTime(indexId, format)
+					if err != nil {
+						logs.Error("ProcessingProfitProcessor Process() : Failed to get data by index id and date: %v", err)
+						continue
+					}
+					if len(indexData) > 0 {
+						logs.Info("ProcessingProfitProcessor Process() : Data already exists for index %d and date %s", indexId, dateText)
+						// 无需走更新逻辑,报告每天更新,即使今天和每天数据一致,也需要每天记录,如果到这里也只是说,今天这个报告被读取了两次
+						continue
+					}
+
+					valueStr := row[columnIdx]
+					value, err := strconv.ParseFloat(valueStr, 64)
+					if err != nil {
+						return []models.BaseFromLyData{}, fmt.Errorf("failed to parse value '%s': %v", valueStr, err)
+					}
+					// 创建并添加到结果列表
+					baseFromLyData := models.BaseFromLyData{
+						DataTime:          format,
+						Value:             value,
+						BaseFromLyIndexId: indexId,
+						IndexCode:         indexCode,
+					}
+					result = append(result, baseFromLyData)
+				} else {
+					log.Printf("ProcessingProfitProcessor Process() : Column index out of range for row '%s', '%s'", rowVariety, columnDate)
+				}
+				break
+			}
+
+		}
+	}
+
+	return result, nil
+}
+
+// DailyTransactionProcessor
+// @Description: 每日成交处理器
+type DailyTransactionProcessor struct{}
+
+func (p *DailyTransactionProcessor) Process(ctx context.Context, product string, reportContent string, keywords []string, classifyId int) ([]models.BaseFromLyData, error) {
+	fmt.Println("Processing processing profit...")
+	// 解析关键字
+	if len(keywords) < 4 {
+		return []models.BaseFromLyData{}, fmt.Errorf("DailyTransactionProcessor Process() : keywords must contain at least 4 elements")
+	}
+
+	// 获取第一个表格
+	areaTableDataList := getNoHeadTableData(reportContent)
+	if len(areaTableDataList) == 0 {
+		return []models.BaseFromLyData{}, fmt.Errorf("DailyTransactionProcessor Process() : No table data found")
+	}
+	areaTableData := areaTableDataList[0]
+	// 获取第二个表格
+	blocTableData := getTableData(reportContent, false)
+	if blocTableData.Headers == nil {
+		return []models.BaseFromLyData{}, fmt.Errorf("DailyTransactionProcessor Process() : No table data found")
+
+	}
+	logs.Info("SupplyDemandBalanceProcessor Process() : areaTableData data: %v, blocTableData data: %v", areaTableData, blocTableData)
+	// 提取日期信息
+	dateText, err := getDateInfo(ctx)
+	if err != nil {
+		return []models.BaseFromLyData{}, err
+	}
+
+	// 时间格式转换
+	format, err := utils.ConvertTimeFormat(dateText)
+	if err != nil {
+		return []models.BaseFromLyData{}, err
+	}
+
+	// 处理提取的表格数据
+	var result []models.BaseFromLyData
+
+	areaHeaders := areaTableData.Headers
+	areaRows := areaTableData.Rows
+
+	// 第一个表格
+	// 拿到 行关键字和列关键字
+	columnArea := keywords[1]
+	var rowAreaMonthDays []string
+	rowWeek := "地区总计"
+
+	monthDay, err := utils.GetWeekdaysInSameWeek(format)
+	if err != nil {
+		return nil, err
+	}
+	rowAreaMonthDays = monthDay
+
+	// 查找目标列
+	areaColumnIdx := -1
+	for i, header := range areaHeaders {
+		if strings.Contains(header, columnArea) {
+			areaColumnIdx = i
+			break
+		}
+	}
+	if areaColumnIdx == -1 {
+		log.Printf("DailyTransactionProcessor Process() : One Column '%s' not found in table", columnArea)
+	} else if !strings.Contains(strings.Join(keywords[:len(keywords)-3], ":"), "主要集团") {
+		for _, row := range areaRows {
+			if len(row) >= len(areaHeaders) && row[0] == rowWeek {
+				if areaColumnIdx < len(row) {
+					// 指标名称
+					indexName := strings.Join(keywords[:len(keywords)-3], ":")
+					// 指标编码
+					indexCode := utils.GenerateIndexCode(sourceName, indexName)
+					// 指标id获取
+					indexId, err := getIndexId(indexCode, indexName, classifyId, sourceName, keywords[len(keywords)-2], keywords[len(keywords)-1])
+					if err != nil {
+						logs.Error("DailyTransactionProcessor Process() : Failed to get index id: %v", err)
+						continue
+					}
+
+					indexData, err := models.GetLyDataByIndexIdAndDataTime(indexId, format)
+					if err != nil {
+						logs.Error("DailyTransactionProcessor Process() : Failed to get data by index id and date: %v", err)
+						continue
+					}
+					if len(indexData) > 0 {
+						logs.Info("DailyTransactionProcessor Process() : Data already exists for index %d and date %s", indexId, dateText)
+						// 无需走更新逻辑,报告每周更新,一周出来一周中每天得数据,即使本周和上周数据一致,也需要每天记录,如果到这里也只是说,今天这个报告被读取了两次
+						continue
+					}
+
+					valueStr := row[areaColumnIdx]
+					isChinese := IsChinese(valueStr)
+					if isChinese {
+						continue
+					}
+					value, err := strconv.ParseFloat(valueStr, 64)
+					if err != nil {
+						return []models.BaseFromLyData{}, fmt.Errorf("failed to parse value '%s': %v", valueStr, err)
+					}
+					// 创建并添加到结果列表
+					var dealDate string
+					if row[0] == rowWeek {
+						dealDate = format
+					} else {
+						date, err := utils.ConvertToDate(row[0])
+						if err != nil {
+							return nil, err
+						}
+						dealDate = date
+					}
+					baseFromLyData := models.BaseFromLyData{
+						DataTime:          dealDate,
+						Value:             value,
+						BaseFromLyIndexId: indexId,
+						IndexCode:         indexCode,
+					}
+
+					result = append(result, baseFromLyData)
+				} else {
+					log.Printf("DailyTransactionProcessor Process() : Column index out of range for row '%s', '%s'", monthDay, columnArea)
+				}
+				break
+			} else {
+				for _, monthDay := range rowAreaMonthDays {
+					if len(row) >= len(areaHeaders) && (row[0] == monthDay && !strings.Contains(strings.Join(keywords[:len(keywords)-3], ":"), "周度")) {
+						if areaColumnIdx < len(row) {
+							// 指标名称
+							indexName := strings.Join(keywords[:len(keywords)-3], ":")
+							// 指标编码
+							indexCode := utils.GenerateIndexCode(sourceName, indexName)
+							// 指标id获取
+							indexId, err := getIndexId(indexCode, indexName, classifyId, sourceName, keywords[len(keywords)-2], keywords[len(keywords)-1])
+							if err != nil {
+								logs.Error("DailyTransactionProcessor Process() : Failed to get index id: %v", err)
+								continue
+							}
+
+							indexData, err := models.GetLyDataByIndexIdAndDataTime(indexId, format)
+							if err != nil {
+								logs.Error("DailyTransactionProcessor Process() : Failed to get data by index id and date: %v", err)
+								continue
+							}
+							if len(indexData) > 0 {
+								logs.Info("DailyTransactionProcessor Process() : Data already exists for index %d and date %s", indexId, dateText)
+								// 无需走更新逻辑,报告每周更新,即使本周和上周数据一致,也需要每周记录,如果到这里也只是说,今天这个报告被读取了两次
+								continue
+							}
+
+							valueStr := row[areaColumnIdx]
+							isChinese := IsChinese(valueStr)
+							if isChinese {
+								continue
+							}
+							value, err := strconv.ParseFloat(valueStr, 64)
+							if err != nil {
+								return []models.BaseFromLyData{}, fmt.Errorf("failed to parse value '%s': %v", valueStr, err)
+							}
+							// 创建并添加到结果列表
+							var dealDate string
+							if row[0] == rowWeek {
+								dealDate = format
+							} else {
+								date, err := utils.ConvertToDate(row[0])
+								if err != nil {
+									return nil, err
+								}
+								dealDate = date
+							}
+							baseFromLyData := models.BaseFromLyData{
+								DataTime:          dealDate,
+								Value:             value,
+								BaseFromLyIndexId: indexId,
+								IndexCode:         indexCode,
+							}
+
+							result = append(result, baseFromLyData)
+						} else {
+							log.Printf("DailyTransactionProcessor Process() : Column index out of range for row '%s', '%s'", monthDay, columnArea)
+						}
+						break
+					}
+				}
+			}
+		}
+	}
+
+	// 第二个表格
+	// 拿到 行关键字和列关键字
+	columnBloc := keywords[len(keywords)-3]
+	rowBloc := keywords[1]
+
+	blocHeaders := blocTableData.Headers
+	blocRows := blocTableData.Rows
+
+	// 查找目标列
+	blocColumnIdx := -1
+	for i, header := range blocHeaders {
+		if strings.Contains(header, columnBloc) {
+			blocColumnIdx = i
+			break
+		}
+	}
+
+	if blocColumnIdx == -1 {
+		log.Printf("DailyTransactionProcessor Process() : Two Column '%s' not found in table", columnBloc)
+	} else {
+		// 处理表格中的每一行
+		for _, row := range blocRows {
+			if len(row) >= len(blocHeaders) && strings.Contains(row[0], rowBloc) {
+				if blocColumnIdx < len(row) {
+					// 指标名称
+					indexName := strings.Join(keywords[:len(keywords)-3], ":")
+					indexName = removeParentheses(indexName)
+					// 指标编码
+					indexCode := utils.GenerateIndexCode(sourceName, indexName)
+					// 指标id获取
+					indexId, err := getIndexId(indexCode, indexName, classifyId, sourceName, keywords[len(keywords)-2], keywords[len(keywords)-1])
+					if err != nil {
+						logs.Error("SupplyDemandBalanceProcessor Process() : Failed to get index id: %v", err)
+						continue
+					}
+
+					indexData, err := models.GetLyDataByIndexIdAndDataTime(indexId, format)
+					if err != nil {
+						logs.Error("SupplyDemandBalanceProcessor Process() : Failed to get data by index id and date: %v", err)
+						continue
+					}
+					if len(indexData) > 0 {
+						logs.Info("SupplyDemandBalanceProcessor Process() : Data already exists for index %d and date %s", indexId, dateText)
+						continue
+					}
+
+					valueStr := row[blocColumnIdx]
+					value, err := strconv.ParseFloat(valueStr, 64)
+					if err != nil {
+						return []models.BaseFromLyData{}, fmt.Errorf("SupplyDemandBalanceProcessor Process() : failed to parse value '%s': %v", valueStr, err)
+					}
+					// 创建并添加到结果列表
+					baseFromLyData := models.BaseFromLyData{
+						DataTime:          format,
+						Value:             value,
+						BaseFromLyIndexId: indexId,
+						IndexCode:         indexCode,
+					}
+					result = append(result, baseFromLyData)
+				} else {
+					log.Printf("SupplyDemandBalanceProcessor Process() : Column index out of range for row '%s', '%s'", rowBloc, columnBloc)
+				}
+				break
+			}
+		}
+	}
+
+	return result, nil
+}
+
+// PalmOilImportCostProcessor 棕榈油进口成本
+type PalmOilImportCostProcessor struct{}
+
+func (p *PalmOilImportCostProcessor) Process(ctx context.Context, product string, reportContent string, keywords []string, classifyId int) ([]models.BaseFromLyData, error) {
+	logs.Info("Processing palm oil import cost...")
+	// 解析关键字
+	if len(keywords) < 5 {
+		return []models.BaseFromLyData{}, fmt.Errorf("PalmOilImportCostProcessor Process() : keywords must contain at least 5 elements")
+	}
+
+	// 拿到 行关键字和列关键字
+	columnName := keywords[len(keywords)-4]
+	rowVariety := keywords[0]
+	rowPort := keywords[len(keywords)-3]
+	indexNamePrefix := keywords[:1]
+	indexNameSuffix := keywords[1:]
+
+	// 提取所有表格数据
+	tableData := getNoHeadTableData(reportContent)
+
+	// 提取日期信息
+	dateText, err := getDateInfo(ctx)
+	if err != nil {
+		return []models.BaseFromLyData{}, err
+	}
+
+	// 时间格式转换
+	format, err := utils.ConvertTimeFormat(dateText)
+	if err != nil {
+		return []models.BaseFromLyData{}, err
+	}
+
+	// 解析日期并计算当前月份
+	targetMonths, err := utils.GetYearMonthNoYear(format)
+	if err != nil {
+		return []models.BaseFromLyData{}, fmt.Errorf("PalmOilImportCostProcessor Process() : Failed to parse date: %v", err)
+	}
+	fmt.Printf("Target Month: %s\n", targetMonths)
+
+	// 处理提取的表格数据
+	var result []models.BaseFromLyData
+
+	for _, data := range tableData {
+		tableHeaders := data.Headers
+		tableRows := data.Rows
+
+		// 查找目标列
+		columnIdx := -1
+		for i, header := range tableHeaders {
+			if strings.Contains(header, columnName) {
+				columnIdx = i
+				break
+			}
+		}
+		if columnIdx == -1 {
+			log.Printf("PalmOilImportCostProcessor Process() : Column '%s' not found in table", columnName)
+			continue
+		}
+
+		// 处理表格中的每一行
+		//var flag bool = true
+		var previousRowVariety string
+		var previousRowPort string
+		var previousRowFob string
+		for rowIndex, row := range tableRows {
+			if len(row) == len(tableHeaders) {
+				previousRowVariety = row[0]
+				previousRowPort = row[1]
+				previousRowFob = row[2]
+			} else if len(row) == len(tableHeaders)-1 {
+				previousRowPort = row[0]
+				previousRowFob = row[1]
+				row = append([]string{previousRowVariety}, row...)
+				tableRows[rowIndex] = row
+			} else if len(row) == len(tableHeaders)-2 {
+				// 这段这里不需要。。。先保留吧
+				previousRowFob = row[0]
+				row = append([]string{previousRowVariety, previousRowPort}, row...)
+				tableRows[rowIndex] = row
+			} else if len(row) == len(tableHeaders)-3 {
+				row = append([]string{previousRowVariety, previousRowPort, previousRowFob}, row...)
+				tableRows[rowIndex] = row
+			}
+			for _, targetMonth := range targetMonths {
+				if len(row) >= len(tableHeaders) && strings.Contains(rowVariety, row[0]) && row[1] == targetMonth && row[len(row)-1] == rowPort {
+					if columnIdx < len(row) {
+						// 指标名称
+						indexNameList := append(indexNamePrefix, append([]string{targetMonth}, indexNameSuffix...)...)
+						indexName := strings.Join(indexNameList[:len(keywords)-2], ":")
+						// 指标编码
+						indexCode := utils.GenerateIndexCode(sourceName, indexName)
+						// 指标id获取
+						indexId, err := getIndexId(indexCode, indexName, classifyId, sourceName, keywords[len(keywords)-2], keywords[len(keywords)-1])
+						if err != nil {
+							logs.Error("PalmOilImportCostProcessor Process() : Failed to get index id: %v", err)
+							continue
+						}
+
+						indexData, err := models.GetLyDataByIndexIdAndDataTime(indexId, format)
+						if err != nil {
+							logs.Error("PalmOilImportCostProcessor Process() : Failed to get data by index id and date: %v", err)
+							continue
+						}
+						if len(indexData) > 0 {
+							logs.Info("PalmOilImportCostProcessor Process() : Data already exists for index %d and date %s", indexId, dateText)
+							continue
+						}
+
+						valueStr := row[columnIdx]
+						value, err := strconv.ParseFloat(valueStr, 64)
+						if err != nil {
+							return []models.BaseFromLyData{}, fmt.Errorf("failed to parse value '%s': %v", valueStr, err)
+						}
+						// 创建并添加到结果列表
+						baseFromLyData := models.BaseFromLyData{
+							DataTime:          format,
+							Value:             value,
+							BaseFromLyIndexId: indexId,
+							IndexCode:         indexCode,
+						}
+						result = append(result, baseFromLyData)
+					} else {
+						log.Printf("PalmOilImportCostProcessor Process() : Column index out of range for row '%s', '%s'", rowVariety, rowPort)
+					}
+					break
+				}
+			}
+
+		}
+
+	}
+
+	return result, nil
+}
+
+// ImportEstimateProcessor
+// @Description: 进口预估处理器
+type ImportEstimateProcessor struct{}
+
+func (p *ImportEstimateProcessor) Process(ctx context.Context, product string, reportContent string, keywords []string, classifyId int) ([]models.BaseFromLyData, error) {
+	logs.Info("Processing import estimate...")
+	// 解析关键字
+	if len(keywords) < 4 {
+		return []models.BaseFromLyData{}, fmt.Errorf("ImportEstimateProcessor Process() : keywords must contain at least 4 elements")
+	}
+
+	// 拿到 行关键字和列关键字
+	var columnDates []string
+	rowVariety := keywords[1]
+
+	// 提取所有表格数据
+	tableData := getNoHeadTableData(reportContent)
+
+	// 提取日期信息
+	dateText, err := getDateInfo(ctx)
+	if err != nil {
+		return []models.BaseFromLyData{}, err
+	}
+
+	// 时间格式转换
+	format, err := utils.ConvertTimeFormat(dateText)
+	if err != nil {
+		return []models.BaseFromLyData{}, err
+	}
+
+	columnDates, err = utils.GetNextThreeMonthsNoYear(format)
+	if err != nil {
+		return nil, err
+	}
+
+	monthsLastDay, err := utils.GetNextThreeMonthsLastDay(format)
+	if err != nil {
+		return nil, err
+	}
+
+	// 处理提取的表格数据
+	var result []models.BaseFromLyData
+
+	for _, data := range tableData {
+		tableHeaders := data.Headers
+		tableRows := data.Rows
+
+		// 查找目标列
+		for _, columnDate := range columnDates {
+			columnIdx := -1
+			for i, tableHeader := range tableHeaders {
+				if strings.Contains(tableHeader, columnDate) {
+					columnIdx = i
+					break
+				}
+			}
+
+			if columnIdx == -1 {
+				log.Printf("ImportEstimateProcessor Process() : Column '%s' not found in table", columnDate)
+				continue
+			} else {
+				// 处理表格中的每一行
+				for _, row := range tableRows {
+					if len(row) >= len(tableHeaders) && strings.Contains(row[0], rowVariety) && isNumber(row[columnIdx]) {
+						if columnIdx < len(row) {
+							// 指标名称
+							indexName := strings.Join(keywords[:len(keywords)-2], `:`)
+							// 指标编码
+							indexCode := utils.GenerateIndexCode(sourceName, indexName)
+							// 指标id获取
+							indexId, err := getIndexId(indexCode, indexName, classifyId, sourceName, keywords[len(keywords)-2], keywords[len(keywords)-1])
+							if err != nil {
+								logs.Error("ImportEstimateProcessor Process() : Failed to get index id: %v", err)
+								continue
+							}
+							toNumber, err := utils.ConvertMonthToNumber(columnDate)
+							if err != nil {
+								logs.Error("ImportEstimateProcessor Process() : Failed to convert month to number: %v", err)
+								continue
+							}
+							slice, err := utils.GetElementInSlice(monthsLastDay, toNumber)
+							if err != nil {
+								logs.Error("ImportEstimateProcessor Process() : Failed to get element in slice: %v", err)
+								continue
+							}
+
+							indexData, err := models.GetLyDataByIndexIdAndDataTime(indexId, slice)
+							if err != nil {
+								logs.Error("ImportEstimateProcessor Process() : Failed to get data by index id and date: %v", err)
+								continue
+							}
+
+							valueStr := row[columnIdx]
+							value, err := strconv.ParseFloat(valueStr, 64)
+							if err != nil {
+								return []models.BaseFromLyData{}, fmt.Errorf("failed to parse value '%s': %v", valueStr, err)
+							}
+
+							if len(indexData) > 0 {
+								if indexData[0].Value != value {
+									logs.Info("ImportEstimateProcessor Process() : Data already exists for index %d and date %s", indexId, dateText)
+
+									lyData := indexData[0]
+									time, err := utils.StringToTime(lyData.ModifyTime)
+									if err != nil {
+										return nil, err
+									}
+
+									timeZero, err := utils.StringToTimeZero(format)
+									if err != nil {
+										return nil, err
+									}
+
+									if lyData.Value != value && time.Before(timeZero) {
+										// 更新指标数据
+										err := models.UpdateLyDataById(lyData.BaseFromLyDataId, value)
+										if err != nil {
+											return nil, err
+										}
+
+										// 同步更新指标库数据
+										lyEdbIndexData, err := models.GetLyEdbDataByIndexCodeAndExactDataTime(lyData.IndexCode, lyData.DataTime)
+										if err != nil {
+											return nil, err
+										}
+										if len(lyEdbIndexData) > 0 {
+											err := models.UpdateLyEdbDataById(lyEdbIndexData[0].EdbInfoId, value)
+											if err != nil {
+												return nil, err
+											}
+										}
+									}
+								}
+
+								continue
+							}
+
+							// 创建并添加到结果列表
+							baseFromLyData := models.BaseFromLyData{
+								DataTime:          slice,
+								Value:             value,
+								BaseFromLyIndexId: indexId,
+								IndexCode:         indexCode,
+							}
+							result = append(result, baseFromLyData)
+						} else {
+							log.Printf("ImportEstimateProcessor Process() : Column index out of range for row '%s', '%s'", rowVariety, columnDate)
+						}
+						break
+					}
+				}
+			}
+		}
+	}
+
+	return result, nil
+}
+
+// InternationalPriceProcessor
+// @Description: 国际价格处理器
+type InternationalPriceProcessor struct{}
+
+func (p *InternationalPriceProcessor) Process(ctx context.Context, product string, reportContent string, keywords []string, classifyId int) ([]models.BaseFromLyData, error) {
+	logs.Info("Processing international price...")
+	// 解析关键字
+	if len(keywords) < 4 {
+		return []models.BaseFromLyData{}, fmt.Errorf("InternationalPriceProcessor Process() : keywords must contain at least 4 elements")
+	}
+
+	// 拿到 行关键字和列关键字
+	columnName := keywords[1]
+	rowVariety := keywords[0]
+	indexNamePrefix := keywords[:1]
+	indexNameSuffix := keywords[1:]
+
+	// 提取所有表格数据
+	tableData := getNoHeadTableData(reportContent)
+
+	// 提取日期信息
+	dateText, err := getDateInfo(ctx)
+	if err != nil {
+		return []models.BaseFromLyData{}, err
+	}
+
+	// 时间格式转换
+	format, err := utils.ConvertTimeFormat(dateText)
+	if err != nil {
+		return []models.BaseFromLyData{}, err
+	}
+
+	// 解析日期并计算当前月份 和 后两月
+	yearMonths, err := utils.ConvertTimeFormatToYearMonth(format)
+	if err != nil {
+		return nil, err
+	}
+	fmt.Printf("Target yearMonth: %s\n", yearMonths)
+
+	// 处理提取的表格数据
+	var result []models.BaseFromLyData
+
+	for _, data := range tableData {
+		tableHeaders := data.Headers
+		tableRows := data.Rows
+
+		// 查找目标列
+		columnIdx := -1
+		for i, header := range tableHeaders {
+			if strings.Contains(header, columnName) {
+				columnIdx = i
+				break
+			}
+		}
+		if columnIdx == -1 {
+			log.Printf("InternationalPriceProcessor Process() : Column '%s' not found in table", columnName)
+			continue
+		}
+
+		// 处理表格中的每一行
+		var previousRowVariety string
+		for rowIndex, row := range tableRows {
+			if len(row) == len(tableHeaders) {
+				previousRowVariety = row[0]
+			} else if len(row) == len(tableHeaders)-1 {
+				row = append([]string{previousRowVariety}, row...)
+				tableRows[rowIndex] = row
+			}
+
+			for _, targetMonth := range yearMonths {
+				if len(row) >= len(tableHeaders) && row[0] == rowVariety && row[1] == targetMonth {
+					if columnIdx < len(row) {
+						// 指标名称
+						indexNameList := append(indexNamePrefix, append([]string{targetMonth}, indexNameSuffix...)...)
+						indexName := strings.Join(indexNameList[:len(keywords)-2], ":")
+						// 指标编码
+						indexCode := utils.GenerateIndexCode(sourceName, indexName)
+						// 指标id获取
+						indexId, err := getIndexId(indexCode, indexName, classifyId, sourceName, keywords[len(keywords)-2], keywords[len(keywords)-1])
+						if err != nil {
+							logs.Error("InternationalPriceProcessor Process() : Failed to get index id: %v", err)
+							continue
+						}
+
+						indexData, err := models.GetLyDataByIndexIdAndDataTime(indexId, format)
+						if err != nil {
+							logs.Error("InternationalPriceProcessor Process() : Failed to get data by index id and date: %v", err)
+							continue
+						}
+						if len(indexData) > 0 {
+							logs.Info("InternationalPriceProcessor Process() : Data already exists for index %d and date %s", indexId, dateText)
+							// 无需更新 指标展示本月和后两月的数据,报告每天更新,每天的值可能会改变,即使今天和每天数据一致,也需要每天记录,如果到这里也只是说,今天这个报告被读取了两次
+							continue
+						}
+
+						valueStr := row[columnIdx]
+						value, err := strconv.ParseFloat(valueStr, 64)
+						if err != nil {
+							return []models.BaseFromLyData{}, fmt.Errorf("failed to parse value '%s': %v", valueStr, err)
+						}
+						// 创建并添加到结果列表
+						baseFromLyData := models.BaseFromLyData{
+							DataTime:          format,
+							Value:             value,
+							BaseFromLyIndexId: indexId,
+							IndexCode:         indexCode,
+						}
+						result = append(result, baseFromLyData)
+					} else {
+						log.Printf("InternationalPriceProcessor Process() : Column index out of range for row '%s', '%s'", rowVariety, columnName)
+					}
+					break
+				}
+			}
+		}
+	}
+
+	return result, nil
+}
+
+// CanadaStatisticsBureauProcessor
+// @Description: 加拿大统计局处理器
+type CanadaStatisticsBureauProcessor struct{}
+
+func (p *CanadaStatisticsBureauProcessor) Process(ctx context.Context, product string, reportContent string, keywords []string, classifyId int) ([]models.BaseFromLyData, error) {
+	logs.Info("Processing Canada statistics bureau...")
+	// 解析关键字
+	if len(keywords) < 4 {
+		return []models.BaseFromLyData{}, fmt.Errorf("CanadaStatisticsBureauProcessor Process() : keywords must contain at least 4 elements")
+	}
+
+	// 拿到 行关键字和列关键字
+	columnDate := "本周"
+	rowVariety := keywords[1]
+	// 提取所有表格数据
+	tableData := getNoHeadTableData(reportContent)
+
+	// 提取日期信息
+	dateText, err := getDateInfo(ctx)
+	if err != nil {
+		return []models.BaseFromLyData{}, err
+	}
+
+	// 时间格式转换
+	format, err := utils.ConvertTimeFormat(dateText)
+	if err != nil {
+		return []models.BaseFromLyData{}, err
+	}
+
+	// 处理提取的表格数据
+	var result []models.BaseFromLyData
+
+	for _, data := range tableData {
+		tableHeaders := data.Headers
+		tableRows := data.Rows
+
+		// 查找目标列
+		columnIdx := -1
+		for i, header := range tableHeaders {
+			if strings.Contains(header, columnDate) {
+				columnIdx = i
+				break
+			}
+		}
+		if columnIdx == -1 {
+			log.Printf("CanadaStatisticsBureauProcessor Process() : Column '%s' not found in table", columnDate)
+			continue
+		}
+
+		// 处理表格中的每一行
+		for _, row := range tableRows {
+			if len(row) >= len(tableHeaders) {
+				if columnIdx < len(row) {
+					if row[0] != rowVariety {
+						continue
+					}
+					// 指标名称
+					indexName := strings.Join(keywords[:len(keywords)-2], ":")
+					// 指标编码
+					indexCode := utils.GenerateIndexCode(sourceName, indexName)
+					// 指标id获取
+					indexId, err := getIndexId(indexCode, indexName, classifyId, sourceName, keywords[len(keywords)-2], keywords[len(keywords)-1])
+					if err != nil {
+						logs.Error("CanadaStatisticsBureauProcessor Process() : Failed to get index id: %v", err)
+						continue
+					}
+
+					indexData, err := models.GetLyDataByIndexIdAndDataTime(indexId, format)
+					if err != nil {
+						logs.Error("CanadaStatisticsBureauProcessor Process() : Failed to get data by index id and date: %v", err)
+						continue
+					}
+					if len(indexData) > 0 {
+						logs.Info("CanadaStatisticsBureauProcessor Process() : Data already exists for index %d and date %s", indexId, dateText)
+						// 无需更新 指标展示本周的数据,报告每周更新,即使本周和上周数据一致,也需要每周记录,如果到这里也只是说,今天这个报告被读取了两次
+						continue
+					}
+
+					valueStr := row[columnIdx]
+					value, err := strconv.ParseFloat(valueStr, 64)
+					if err != nil {
+						return []models.BaseFromLyData{}, fmt.Errorf("failed to parse value '%s': %v", valueStr, err)
+					}
+					// 创建并添加到结果列表
+					baseFromLyData := models.BaseFromLyData{
+						DataTime:          format,
+						Value:             value,
+						BaseFromLyIndexId: indexId,
+						IndexCode:         indexCode,
+					}
+					result = append(result, baseFromLyData)
+				} else {
+					log.Printf("CanadaStatisticsBureauProcessor Process() : Column index out of range for row '%s'", columnDate)
+				}
+				break
+			}
+		}
+	}
+
+	return result, nil
+}
+
+// ImportExportAnalysisProcessor
+// @Description: 进出口分析处理器
+type ImportExportAnalysisProcessor struct{}
+
+func (p *ImportExportAnalysisProcessor) Process(ctx context.Context, product string, reportContent string, keywords []string, classifyId int) ([]models.BaseFromLyData, error) {
+	fmt.Println("Processing processing profit...")
+	// 解析关键字
+	if len(keywords) < 3 {
+		return []models.BaseFromLyData{}, fmt.Errorf("ProcessingProfitProcessor Process() : keywords must contain at least 3 elements")
+	}
+
+	// 拿到 行关键字和列关键字
+	var columnDates []string
+	// 提取所有表格数据
+	tableData := getNoHeadTableData(reportContent)
+
+	// 提取日期信息
+	dateText, err := getDateInfo(ctx)
+	if err != nil {
+		return []models.BaseFromLyData{}, err
+	}
+
+	// 时间格式转换
+	format, err := utils.ConvertTimeFormat(dateText)
+	if err != nil {
+		return []models.BaseFromLyData{}, err
+	}
+
+	// 2025年1月可能才出2024年12月的数据,所以往前取一年
+	columnDates, err = utils.GetCurrentYearAndLastYear(format)
+	if err != nil {
+		return nil, err
+	}
+
+	// 处理提取的表格数据
+	var result []models.BaseFromLyData
+
+	for _, data := range tableData {
+		tableHeaders := data.Headers
+		tableRows := data.Rows
+
+		for _, columnDate := range columnDates {
+			// 查找目标列
+			columnIdx := -1
+			for i, header := range tableHeaders {
+				if strings.Contains(header, columnDate) {
+					columnIdx = i
+					break
+				}
+			}
+
+			if columnIdx == -1 {
+				log.Printf("ProcessingProfitProcessor Process() : Column '%s' not found in table", columnDate)
+				continue
+			}
+
+			// 处理表格中的每一行
+			for _, row := range tableRows {
+				if len(row) >= len(tableHeaders) {
+					if columnIdx < len(row) && isNumber(row[columnIdx]) && isNumber(row[0]) {
+						// 指标名称
+						indexName := strings.Join(keywords[:len(keywords)-2], ":")
+						// 指标编码
+						indexCode := utils.GenerateIndexCode(sourceName, indexName)
+						// 指标id获取
+						indexId, err := getIndexId(indexCode, indexName, classifyId, sourceName, keywords[len(keywords)-2], keywords[len(keywords)-1])
+						if err != nil {
+							logs.Error("ProcessingProfitProcessor Process() : Failed to get index id: %v", err)
+							continue
+						}
+						atoi, err := strconv.Atoi(row[0])
+						if err != nil {
+							return nil, err
+						}
+						date := columnDate[:4] + "-" + fmt.Sprintf("%02d", atoi)
+						lastDayOfMonth, err := utils.GetLastDayOfMonth(date)
+						if err != nil {
+							return nil, err
+						}
+
+						indexData, err := models.GetLyDataByIndexIdAndDataTime(indexId, lastDayOfMonth)
+						if err != nil {
+							logs.Error("ProcessingProfitProcessor Process() : Failed to get data by index id and date: %v", err)
+							continue
+						}
+						if len(indexData) > 0 {
+							logs.Info("ProcessingProfitProcessor Process() : Data already exists for index %d and date %s", indexId, dateText)
+							continue
+						}
+
+						valueStr := row[columnIdx]
+						value, err := strconv.ParseFloat(valueStr, 64)
+						if err != nil {
+							return []models.BaseFromLyData{}, fmt.Errorf("failed to parse value '%s': %v", valueStr, err)
+						}
+						// 创建并添加到结果列表
+						baseFromLyData := models.BaseFromLyData{
+							DataTime:          lastDayOfMonth,
+							Value:             value,
+							BaseFromLyIndexId: indexId,
+							IndexCode:         indexCode,
+						}
+						result = append(result, baseFromLyData)
+						continue
+					} else {
+						log.Printf("ProcessingProfitProcessor Process() : Column index out of range for row '%s'", columnDate)
+					}
+					break
+				}
+			}
+		}
+	}
+
+	return result, nil
+}
+
+// ExtractValueInParentheses 从字符串中提取括号中的值
+func extractValueInParentheses(input string) (string, error) {
+	re := regexp.MustCompile(`(([^)]+))`)
+	matches := re.FindStringSubmatch(input)
+
+	if len(matches) > 1 {
+		return matches[1], nil
+	}
+
+	return "", fmt.Errorf("no value found in parentheses")
+}
+
+// 获取指标id,根据指标名称判断,没有插入指标生成返回
+func getIndexId(indexCode string, indexName string, classifyId int, sourceName string, frequency string, unit string) (int, error) {
+	if indexCode == "lysww" {
+		return 0, fmt.Errorf("indexCode is error")
+	}
+
+	// 判断指标是否存在
+	var indexId int
+	indexInfo, err := models.GetLyIndexByCode(indexCode)
+	if err != nil {
+		return indexId, err
+	}
+	if indexInfo == nil {
+		// 新增指标
+		index, err := addLyIndex(classifyId, indexCode, indexName, frequency, unit)
+		if err != nil {
+			return 0, err
+		}
+		indexId = index
+	} else {
+		indexId = indexInfo.BaseFromLyIndexId
+	}
+	return indexId, nil
+}
+
+// 获取页面时间信息
+func getDateInfo(ctx context.Context) (string, error) {
+	var dateText string
+	err := chromedp.Run(ctx,
+		chromedp.Evaluate(`document.querySelector('div.a_date span').innerText`, &dateText),
+	)
+	if err != nil {
+		return "", fmt.Errorf("processing Process() : Failed to extract report date: %v", err)
+	}
+
+	logs.Info("Processing Process() : Report Extracted Date: %s", dateText)
+	return dateText, nil
+}
+
+// 获取所有表格数据 获取表格中有thead标签的数据
+func getAllTableData(reportContent string) []TableData {
+	var tableData []TableData
+
+	doc, err := goquery.NewDocumentFromReader(strings.NewReader(reportContent))
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	// 选择 id 为 "a_content" 的 div
+	doc.Find("#a_content").Each(func(index int, item *goquery.Selection) {
+		item.Find("table").Each(func(index int, table *goquery.Selection) {
+			var headers []string
+			var rows [][]string
+
+			// 提取表头
+			table.Find("thead th").Each(func(index int, th *goquery.Selection) {
+				headers = append(headers, th.Text())
+			})
+
+			// 提取表格行数据
+			table.Find("tbody tr").Each(func(index int, row *goquery.Selection) {
+				var rowData []string
+				row.Find("td").Each(func(index int, td *goquery.Selection) {
+					rowData = append(rowData, td.Text())
+				})
+				rows = append(rows, rowData)
+			})
+
+			// 仅在表头存在时添加到结果中
+			if len(headers) > 0 {
+				tableData = append(tableData, TableData{
+					Headers: headers,
+					Rows:    rows,
+				})
+			}
+		})
+	})
+	return tableData
+}
+
+// 获取无头表格数据
+func getNoHeadTableData(reportContent string) []TableData {
+	var tableData []TableData
+
+	doc, err := goquery.NewDocumentFromReader(strings.NewReader(reportContent))
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	// Find the div with id "a_content"
+	doc.Find("#a_content").Each(func(index int, div *goquery.Selection) {
+		// Find all tables within the div
+		div.Find("table").Each(func(index int, table *goquery.Selection) {
+			var headers []string
+			var rows [][]string
+
+			// Extract table headers if any
+			table.Find("tr").Each(func(index int, tr *goquery.Selection) {
+				var rowData []string
+				tr.Find("td, th").Each(func(index int, cell *goquery.Selection) {
+					cellText := cell.Text()
+					rowData = append(rowData, cellText)
+				})
+
+				if index == 0 && len(rowData) > 0 {
+					// The first row is treated as the header row
+					headers = rowData
+				} else if len(rowData) > 0 {
+					// Add the row data to the rows slice
+					rows = append(rows, rowData)
+				}
+			})
+
+			// Only add table data if headers are present
+			if len(headers) > 0 {
+				tableData = append(tableData, TableData{
+					Headers: headers,
+					Rows:    rows,
+				})
+			}
+		})
+	})
+
+	return tableData
+}
+
+// 获取表格数据 获取id 为 a_content 的 div 中的第一个表格 左上角那个单元格会拼在第一个,会拼上列上的合并单元格
+func getTableData(reportContent string, isFirst bool) TableData {
+	doc, err := goquery.NewDocumentFromReader(strings.NewReader(reportContent))
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	tableData := &TableData{}
+
+	// 只提取 id 为 a_content 的 div 中的第一个表格
+	var firstTable *goquery.Selection
+	if isFirst {
+		firstTable = doc.Find("div#a_content table").First()
+	} else {
+		firstTable = doc.Find("div#a_content table").Last()
+	}
+
+	var combinedHeaders []string
+
+	// 提取表头
+	firstTable.Find("tr").Each(func(i int, row *goquery.Selection) {
+		if i == 0 {
+			// 第一行处理合并单元格,保存到 combinedHeaders
+			row.Find("td,th").Each(func(j int, cell *goquery.Selection) {
+				if j == 0 {
+					// 把左上角的“年度(10/9月)”放入 Headers 第一个元素
+					tableData.Headers = append(tableData.Headers, strings.TrimSpace(cell.Text()))
+				} else {
+					// 处理其他单元格
+					colspan, exists := cell.Attr("colspan")
+					if exists {
+						spanCount := 0
+						fmt.Sscanf(colspan, "%d", &spanCount)
+						for k := 0; k < spanCount; k++ {
+							combinedHeaders = append(combinedHeaders, strings.TrimSpace(cell.Text()))
+						}
+					} else {
+						combinedHeaders = append(combinedHeaders, strings.TrimSpace(cell.Text()))
+					}
+				}
+			})
+		} else if i == 1 {
+			// 第二行处理具体标题,组合后保存到 Headers
+			row.Find("td,th").Each(func(j int, cell *goquery.Selection) {
+				if j < len(combinedHeaders) {
+					fullHeader := combinedHeaders[j] + strings.TrimSpace(cell.Text())
+					tableData.Headers = append(tableData.Headers, fullHeader)
+				}
+			})
+		} else {
+			// 处理数据行
+			var rowData []string
+			row.Find("td").Each(func(j int, cell *goquery.Selection) {
+				rowData = append(rowData, strings.TrimSpace(cell.Text()))
+			})
+			if len(rowData) > 0 {
+				tableData.Rows = append(tableData.Rows, rowData)
+			}
+		}
+	})
+
+	return *tableData
+}
+
+// 获取采购装船表格数据
+func getPurchaseShippingTableData(reportContent string) TableData {
+	doc, err := goquery.NewDocumentFromReader(strings.NewReader(reportContent))
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	var tableData TableData
+
+	// 只提取 id 为 a_content 的 div 中的第一个表格
+	firstTable := doc.Find("div#a_content table").First()
+
+	// 提取表头
+	var headers []string
+	var subHeaders []string
+
+	firstTable.Find("thead tr").Each(func(i int, row *goquery.Selection) {
+		row.Find("th").Each(func(j int, cell *goquery.Selection) {
+			headerText := strings.TrimSpace(cell.Text())
+
+			if i == 0 {
+				// 处理第一行表头
+				colspan, exists := cell.Attr("colspan")
+				if exists {
+					spanCount := 0
+					fmt.Sscanf(colspan, "%d", &spanCount)
+					for k := 0; k < spanCount; k++ {
+						headers = append(headers, headerText)
+					}
+				} else {
+					headers = append(headers, headerText)
+				}
+			} else if i == 1 {
+				// 处理第二行表头
+				subHeaders = append(subHeaders, headerText)
+			}
+		})
+	})
+
+	// 合并第一行和第二行表头信息
+	if len(subHeaders) > 0 {
+		for i := 0; i < len(subHeaders); i++ {
+			// 从第四个单元格开始拼接
+			headers[3+i] = headers[3+i] + subHeaders[i]
+		}
+	}
+
+	tableData.Headers = headers
+
+	// 处理数据行
+	firstTable.Find("tbody tr").Each(func(i int, row *goquery.Selection) {
+		var rowData []string
+		row.Find("td").Each(func(j int, cell *goquery.Selection) {
+			rowData = append(rowData, strings.TrimSpace(cell.Text()))
+		})
+		if len(rowData) > 0 {
+			tableData.Rows = append(tableData.Rows, rowData)
+		}
+	})
+
+	return tableData
+}
+
+// 判断字符串是否是数字
+func isNumeric(value string) bool {
+	// 正则表达式匹配整数和浮点数
+	re := regexp.MustCompile(`^[+-]?(\d+(\.\d*)?|\.\d+)$`)
+	return re.MatchString(value)
+}
+
+// 只保留汉字
+func extractChinese(text string) string {
+	re := regexp.MustCompile(`[^\p{Han}]`) // 匹配非汉字字符
+	return re.ReplaceAllString(text, "")
+}
+
+// 去除括号中的内容 包含括号 ()
+func removeParentheses(text string) string {
+	re := regexp.MustCompile(`\([^)]*\)`)
+	return re.ReplaceAllString(text, "")
+}
+
+// IsChinese 判断传入的是否是汉字
+func IsChinese(str string) bool {
+	for _, r := range str {
+		if unicode.Is(unicode.Han, r) {
+			return true
+		}
+	}
+	return false
+}
+
+// 判断是否是数字
+func isNumber(str string) bool {
+	_, err := strconv.ParseFloat(str, 64)
+	return err == nil
+}

+ 110 - 0
services/liangyou/processor_factory.go

@@ -0,0 +1,110 @@
+// @Author gmy 2024/8/6 10:48:00
+package main
+
+import (
+	"context"
+	"eta/eta_crawler/models"
+	"fmt"
+)
+
+type ReportProcessor interface {
+	Process(ctx context.Context, product string, reportContent string, keywords []string, classifyId int) ([]models.BaseFromLyData, error)
+}
+
+func GetProcessor(product string, category string) (ReportProcessor, error) {
+	if product == "大豆" {
+		switch category {
+		case "进口成本":
+			return &ImportCostProcessor{}, nil
+		case "加工利润":
+			return &ProcessingProfitProcessor{}, nil
+		case "船运费用":
+			return &ShippingCostProcessor{}, nil
+		case "供需平衡":
+			return &SupplyDemandBalanceProcessor{}, nil
+		case "采购装船":
+			return &PurchaseShippingProcessor{}, nil
+		case "库存分析":
+			return &InventoryAnalysisProcessor{}, nil
+		case "加工报告":
+			return &ProcessingReportProcessor{}, nil
+		default:
+			return nil, fmt.Errorf("unknown category: %s", category)
+		}
+	} else if product == "豆粕" {
+		switch category {
+		case "库存分析":
+			return &InventoryAnalysisProcessor{}, nil
+		default:
+			return nil, fmt.Errorf("unknown category: %s", category)
+		}
+	} else if product == "大豆油" {
+		switch category {
+		case "库存分析":
+			return &InventoryAnalysisProcessor{}, nil
+		case "价差套利":
+			return &PriceSpreadArbitrageProcessor{}, nil
+		case "每日成交":
+			return &DailyTransactionProcessor{}, nil
+		default:
+			return nil, fmt.Errorf("unknown category: %s", category)
+		}
+	} else if product == "棕榈油" {
+		switch category {
+		case "国际价格":
+			return &InternationalPriceProcessor{}, nil
+		case "进口成本":
+			return &PalmOilImportCostProcessor{}, nil
+		case "库存分析":
+			return &InventoryAnalysisProcessor{}, nil
+		case "每日成交":
+			return &DailyTransactionProcessor{}, nil
+		default:
+			return nil, fmt.Errorf("unknown category: %s", category)
+		}
+	} else if product == "油菜籽" {
+		switch category {
+		case "进口成本":
+			return &ImportCostProcessor{}, nil
+		case "库存分析":
+			return &InventoryAnalysisProcessor{}, nil
+		case "进口预估":
+			return &ImportEstimateProcessor{}, nil
+		case "加拿大统计局":
+			return &CanadaStatisticsBureauProcessor{}, nil
+		case "进出口分析":
+			return &ImportExportAnalysisProcessor{}, nil
+		default:
+			return nil, fmt.Errorf("unknown category: %s", category)
+		}
+	} else if product == "菜粕" {
+		switch category {
+		case "库存分析":
+			return &InventoryAnalysisProcessor{}, nil
+		case "进出口分析":
+			return &ImportExportAnalysisProcessor{}, nil
+		default:
+			return nil, fmt.Errorf("unknown category: %s", category)
+		}
+	} else if product == "菜籽油" {
+		switch category {
+		case "库存分析":
+			return &InventoryAnalysisProcessor{}, nil
+		case "进口预估":
+			return &ImportEstimateProcessor{}, nil
+		case "每日成交":
+			return &DailyTransactionProcessor{}, nil
+		default:
+			return nil, fmt.Errorf("unknown category: %s", category)
+		}
+	} else if product == "葵花粕" {
+		switch category {
+		case "进口预估":
+			return &ImportEstimateProcessor{}, nil
+		default:
+			return nil, fmt.Errorf("unknown category: %s", category)
+		}
+	}
+	// 可以添加更多的逻辑来处理其他产品和类别
+	return nil, fmt.Errorf("no processor found for product %s and category %s", product, category)
+}

+ 20 - 1
utils/common.go

@@ -3,6 +3,7 @@ package utils
 import (
 	"bufio"
 	"crypto/md5"
+	cryRand "crypto/rand"
 	"crypto/sha1"
 	"encoding/base64"
 	"encoding/hex"
@@ -15,6 +16,7 @@ import (
 	"image/png"
 	"io"
 	"math"
+	"math/big"
 	"math/rand"
 	"net"
 	"net/http"
@@ -1310,4 +1312,21 @@ func ContainsEnglishLetter(s string) bool {
 		}
 	}
 	return false
-}
+}
+
+// RangeRand 取区间随机数
+func RangeRand(min, max int64) int64 {
+	if min > max {
+		return max
+	}
+	if min < 0 {
+		f64Min := math.Abs(float64(min))
+		i64Min := int64(f64Min)
+		result, _ := cryRand.Int(cryRand.Reader, big.NewInt(max+1+i64Min))
+
+		return result.Int64() - i64Min
+	} else {
+		result, _ := cryRand.Int(cryRand.Reader, big.NewInt(max-min+1))
+		return min + result.Int64()
+	}
+}

+ 25 - 16
utils/constants.go

@@ -233,20 +233,29 @@ var FrequencyDaysMap = map[string]int{
 
 // edb_index_lib 的接口名称
 const (
-	LIB_ROUTE_YONGYI_HANDLE             = "yongyi/handle/excel_data"   //涌益咨询处理excel数据并入库 数据地址
-	LIB_ROUTE_COAL_MINE_JSM_HISTORY     = "/coal_mine/jsm/history"     //jsm三省煤炭网历史数据处理excel数据并入库 数据地址
-	LIB_ROUTE_COAL_MINE_COASTAL_HISTORY = "/coal_mine/coastal/history" //沿海煤炭网历史数据处理excel数据并入库 数据地址
-	LIB_ROUTE_COAL_MINE_INLAND_HISTORY  = "/coal_mine/inland/history"  //内陆三省煤炭网历史数据处理excel数据并入库 数据地址
-	LIB_ROUTE_COAL_MINE_JSM             = "/coal_mine/jsm"             //jsm三省煤炭网历史数据处理excel数据并入库 数据地址
-	LIB_ROUTE_COAL_MINE_COASTAL         = "/coal_mine/coastal"         //沿海煤炭网历史数据处理excel数据并入库 数据地址
-	LIB_ROUTE_COAL_MINE_INLAND          = "/coal_mine/inland"          //内陆三省煤炭网历史数据处理excel数据并入库 数据地址
-	LIB_ROUTE_COAL_MINE_FIRM            = "/coal_mine/firm"            //分公司旬度煤炭网数据处理excel数据并入库 数据地址
-	LIB_ROUTE_FENWEI_HANDLE             = "fenwei/handle/excel_data"   // 汾渭煤炭excel数据入库接口地址
-	LIB_ROUTE_FENWEI_CLASSIFY           = "fenwei/classify_tree"       // 汾渭煤炭分类接口地址
-	LIB_ROUTE_FENWEI_INDEX_LIST         = "fenwei/base_index_list"     // 汾渭煤炭指标列表接口地址
-	LIB_ROUTE_COAL_MINE_MTJH            = "/mtjh/data"                 //煤炭江湖数据处理excel数据并入库 数据地址
-	LIB_ROUTE_CCF_EDB_HANDLE            = "ccf/handle/edb_data"        // CCF化纤信息指标入库接口地址
-	LIB_ROUTE_CCF_TABLE_HANDLE          = "ccf/handle/table_data"      // CCF化纤信息装置表格入库接口地址
-	LIB_ROUTE_OILCHEM_TABLE_HANDLE      = "oilchem/handle/edb_data"  // 隆众资讯入库接口地址
-	LIB_ROUTE_FENWEI_NET_DATA_HANDLE    = "/fenwei/net/data/handle"    // 汾渭网页数据处理
+	LIB_ROUTE_YONGYI_HANDLE                  = "yongyi/handle/excel_data"                     //涌益咨询处理excel数据并入库 数据地址
+	LIB_ROUTE_COAL_MINE_JSM_HISTORY          = "/coal_mine/jsm/history"                       //jsm三省煤炭网历史数据处理excel数据并入库 数据地址
+	LIB_ROUTE_COAL_MINE_COASTAL_HISTORY      = "/coal_mine/coastal/history"                   //沿海煤炭网历史数据处理excel数据并入库 数据地址
+	LIB_ROUTE_COAL_MINE_INLAND_HISTORY       = "/coal_mine/inland/history"                    //内陆三省煤炭网历史数据处理excel数据并入库 数据地址
+	LIB_ROUTE_COAL_MINE_JSM                  = "/coal_mine/jsm"                               //jsm三省煤炭网历史数据处理excel数据并入库 数据地址
+	LIB_ROUTE_COAL_MINE_COASTAL              = "/coal_mine/coastal"                           //沿海煤炭网历史数据处理excel数据并入库 数据地址
+	LIB_ROUTE_COAL_MINE_INLAND               = "/coal_mine/inland"                            //内陆三省煤炭网历史数据处理excel数据并入库 数据地址
+	LIB_ROUTE_COAL_MINE_FIRM                 = "/coal_mine/firm"                              //分公司旬度煤炭网数据处理excel数据并入库 数据地址
+	LIB_ROUTE_FENWEI_HANDLE                  = "fenwei/handle/excel_data"                     // 汾渭煤炭excel数据入库接口地址
+	LIB_ROUTE_FENWEI_CLASSIFY                = "fenwei/classify_tree"                         // 汾渭煤炭分类接口地址
+	LIB_ROUTE_FENWEI_INDEX_LIST              = "fenwei/base_index_list"                       // 汾渭煤炭指标列表接口地址
+	LIB_ROUTE_COAL_MINE_MTJH                 = "/mtjh/data"                                   //煤炭江湖数据处理excel数据并入库 数据地址
+	LIB_ROUTE_CCF_EDB_HANDLE                 = "ccf/handle/edb_data"                          // CCF化纤信息指标入库接口地址
+	LIB_ROUTE_CCF_TABLE_HANDLE               = "ccf/handle/table_data"                        // CCF化纤信息装置表格入库接口地址
+	LIB_ROUTE_OILCHEM_TABLE_HANDLE           = "oilchem/handle/edb_data"                      // 隆众资讯入库接口地址
+	LIB_ROUTE_FENWEI_NET_DATA_HANDLE         = "/fenwei/net/data/handle"                      // 汾渭网页数据处理
+	GET_LY_CLASSIFY_BY_NAME                  = "/ly/get/ly/classify/by/name"                  // 获取分类
+	GET_LY_INDEX_RECORD_BY_URL               = "/ly/get/ly/index/record/by/url"               // 根据url获取指标已读取记录
+	ADD_LY_INDEX_RECORD                      = "/ly/add/ly/index/record"                      // 维护指标数据读取进度到数据库
+	ADD_LY_DATA_LIST                         = "/ly/add/ly/data/list"                         // 新增指标数据列表
+	ADD_LY_INDEX                             = "/ly/add/ly/index"                             // 新增指标
+	GET_LY_DATA_BY_INDEX_ID_AND_DATA_TIME    = "/ly/get/ly/data/by/index/id/and/data/time"    // 根据指标id和时间获取指标数据
+	GET_LY_DATA_BY_INDEX_ID_AND_DATA_TIME_YM = "/ly/get/ly/data/by/index/id/and/data/time/ym" // 根据指标id和年月时间获取指标数据
+	UPDATE_LY_DATA_BY_ID                     = "/ly/update/ly/data/by/id"                     // 更新数据源指标数据
+	UPDATE_LY_EDB_DATA_BY_ID                 = "/ly/update/ly/edb/data/by/id"                 // 更新指标库指标数据
 )

+ 488 - 0
utils/date_util.go

@@ -0,0 +1,488 @@
+// Package utils @Author gmy 2024/8/6 16:06:00
+package utils
+
+import (
+	"fmt"
+	"strconv"
+	"strings"
+	"time"
+)
+
+// ParseDateAndWeek parseDateAndWeek 解析日期并计算当前周数 ==> 24年31周
+func ParseDateAndWeek(dateText string) (string, error) {
+	// 解析日期
+	reportDate, err := time.Parse("2006-01-02", strings.TrimSpace(strings.Split(dateText, " ")[0]))
+	if err != nil {
+		return "", fmt.Errorf("failed to parse report date: %v", err)
+	}
+
+	// 计算年份和周数
+	year, week := reportDate.ISOWeek()
+	// 获取年份的后两位
+	shortYear := year % 100
+	targetWeek := fmt.Sprintf("%02d年第%d周", shortYear, week)
+
+	return targetWeek, nil
+}
+
+// ParseDateAndMonth 解析时间并计算当前月份 和 后两月 1月就是1月F,二月是二月G 规则:F=1月,G=2月,H=3月,J=4月,K=5月,M=6月,N=7月,Q=8月,U=9月,V=10月,X=11月,Z=12月
+func ParseDateAndMonth(dateText string) ([]string, error) {
+	reportDate, err := time.Parse("2006-01-02", strings.TrimSpace(strings.Split(dateText, " ")[0]))
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse report date: %v", err)
+	}
+
+	months := make([]string, 3)
+	monthMap := map[string]string{
+		"01": "1月F",
+		"02": "2月G",
+		"03": "3月H",
+		"04": "4月J",
+		"05": "5月K",
+		"06": "6月M",
+		"07": "7月N",
+		"08": "8月Q",
+		"09": "9月X",
+		"10": "10月X",
+		"11": "11月X",
+		"12": "12月Z",
+	}
+
+	for i := 0; i < 3; i++ {
+		month := reportDate.AddDate(0, i, 0).Format("01")
+		months[i] = monthMap[month]
+	}
+
+	return months, nil
+}
+
+// ParseDateAndMonthColzaOil 油菜籽 进口成本 时间映射
+func ParseDateAndMonthColzaOil(dateText string) ([]string, error) {
+	reportDate, err := time.Parse("2006-01-02", strings.TrimSpace(strings.Split(dateText, " ")[0]))
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse report date: %v", err)
+	}
+
+	months := make([]string, 3)
+	monthMap := map[string]string{
+		"01": "1月F",
+		"02": "2月H",
+		"03": "3月H",
+		"04": "4月K",
+		"05": "5月K",
+		"06": "6月N",
+		"07": "7月N",
+		"08": "8月X",
+		"09": "9月X",
+		"10": "10月X",
+		"11": "11月X",
+		"12": "12月F",
+	}
+
+	for i := 0; i < 3; i++ {
+		month := reportDate.AddDate(0, i, 0).Format("01")
+		months[i] = monthMap[month]
+	}
+
+	return months, nil
+}
+
+// GetCurrentTime 获取当前时间 格式为 2024-08-07 15:29:58
+func GetCurrentTime() string {
+	return time.Now().Format("2006-01-02 15:04:05")
+}
+
+// ConvertTimeFormat 转换时间格式 dateText 格式为 2024-08-03 07:53 --> 2024-08-03
+func ConvertTimeFormat(dateText string) (string, error) {
+	// 解析日期
+	reportDate, err := time.Parse("2006-01-02 15:04", strings.TrimSpace(dateText))
+	if err != nil {
+		return "", fmt.Errorf("failed to parse report date: %v", err)
+	}
+
+	return reportDate.Format("2006-01-02"), nil
+}
+
+// GetNextThreeMonthsNoYear 获取当前月和后两月 不带年份,转换时间格式 dateText 格式为 2024-08-03 --> 8月,9月,10月
+func GetNextThreeMonthsNoYear(dateText string) ([]string, error) {
+	// 解析日期字符串为时间类型
+	date, err := time.Parse("2006-01-02", dateText)
+	if err != nil {
+		return nil, fmt.Errorf("日期解析错误: %v", err)
+	}
+
+	// 存储结果的切片
+	var result []string
+
+	// 获取本月及后两个月的月份
+	for i := 0; i < 3; i++ {
+		month := int(date.Month())
+
+		// 构建并添加当前年月到结果中
+		result = append(result, fmt.Sprintf("%d月", month))
+
+		// 将日期加一个月
+		date = date.AddDate(0, 1, 0)
+	}
+
+	return result, nil
+}
+
+// GetNextThreeMonthsLastDay 取当前月的最后一天和后两个月的最后一天 时间格式为 2024-08-03 --> 2024-08-31, 2024-09-30, 2024-10-31
+func GetNextThreeMonthsLastDay(dateText string) ([]string, error) {
+	// 解析日期字符串为时间类型
+	date, err := time.Parse("2006-01-02", dateText)
+	if err != nil {
+		return nil, fmt.Errorf("日期解析错误: %v", err)
+	}
+
+	// 存储结果的切片
+	var result []string
+
+	// 获取本月及后两个月的最后一天
+	for i := 0; i < 3; i++ {
+		// 获取下个月的第一天
+		nextMonth := date.AddDate(0, 1, 0)
+
+		// 获取当前月的最后一天
+		lastDay := nextMonth.AddDate(0, 0, -nextMonth.Day())
+
+		// 添加到结果中
+		result = append(result, lastDay.Format("2006-01-02"))
+
+		// 将日期加一个月
+		date = date.AddDate(0, 1, 0)
+	}
+
+	return result, nil
+}
+
+// GetElementInSlice 获取切片中特定的元素,判断传入月份是否在当前切片月份匹配,如果匹配则返回切片中对应的元素 参数格式为 dateTexts month, [2024-08-31, 2024-09-30, 2024-10-31] 08 --> 2024-08-31, 07 --> nil
+func GetElementInSlice(dateTexts []string, month string) (string, error) {
+	for _, dateText := range dateTexts {
+		reportDate, err := time.Parse("2006-01-02", strings.TrimSpace(dateText))
+		if err != nil {
+			return "", fmt.Errorf("failed to parse report date: %v", err)
+		}
+
+		if strings.HasSuffix(reportDate.Format("2006-01"), month) {
+			return dateText, nil
+		}
+	}
+
+	return "", fmt.Errorf("未找到匹配的月份")
+}
+
+// StringToTime string 类型时间转换为 time 类型时间 dateText 格式为 2024-08-03 00:00:00 --> 2024-08-03 00:00:00
+func StringToTime(dateText string) (time.Time, error) {
+	// 解析日期
+	reportDate, err := time.Parse("2006-01-02 15:04:05", strings.TrimSpace(dateText))
+	if err != nil {
+		return time.Time{}, fmt.Errorf("failed to parse report date: %v", err)
+	}
+
+	return reportDate, nil
+}
+
+// StringToTimeZero string 类型时间转换为 time dateText 格式为 2024-08-03 --> 2024-08-03 00:00:00
+func StringToTimeZero(dateText string) (time.Time, error) {
+	// 解析日期
+	reportDate, err := time.Parse("2006-01-02", strings.TrimSpace(dateText))
+	if err != nil {
+		return time.Time{}, fmt.Errorf("failed to parse report date: %v", err)
+	}
+
+	return reportDate, nil
+}
+
+// GetWeekdaysInSameWeek 拿到传入时间本周当前工作日的时间列表,时间格式 dataText 格式为 2024-08-03 --> 8月3日
+func GetWeekdaysInSameWeek(dateStr string) ([]string, error) {
+	// 解析输入日期字符串
+	t, err := time.Parse("2006-01-02", dateStr)
+	if err != nil {
+		return nil, err
+	}
+
+	// 获取星期几
+	weekday := t.Weekday()
+
+	// 计算星期一的日期
+	monday := t.AddDate(0, 0, -int(weekday)+1)
+
+	// 生成这周的工作日列表(周一至周五)
+	var weekdays []string
+	for i := 0; i < 5; i++ {
+		day := monday.AddDate(0, 0, i)
+		weekdays = append(weekdays, fmt.Sprintf("%d月%d日", day.Month(), day.Day()))
+	}
+
+	return weekdays, nil
+}
+
+// ConvertToDate 转换后获取当前传入的时间 时间格式为 7月22日 --> 2024-07-22
+func ConvertToDate(dateText string) (string, error) {
+	// 假设当前年份为 2024
+	currentYear := time.Now().Year()
+
+	// 分割日期字符串
+	parts := strings.Split(dateText, "月")
+	if len(parts) != 2 {
+		return "", fmt.Errorf("日期格式错误")
+	}
+
+	// 获取月和日的部分
+	month, err := strconv.Atoi(parts[0])
+	if err != nil {
+		return "", fmt.Errorf("月份解析错误: %v", err)
+	}
+	day, err := strconv.Atoi(strings.TrimSuffix(parts[1], "日"))
+	if err != nil {
+		return "", fmt.Errorf("日期解析错误: %v", err)
+	}
+
+	// 构建日期
+	date := time.Date(currentYear, time.Month(month), day, 0, 0, 0, 0, time.Local)
+
+	// 格式化为 "2024-07-22"
+	return date.Format("2006-01-02"), nil
+}
+
+// ConvertTimeFormatToYearMonthDay 转换时间格式 dateText 格式为 2024-08-03 --> 24年8月3日
+func ConvertTimeFormatToYearMonthDay(dateText string) (string, error) {
+	// 解析日期
+	reportDate, err := time.Parse("2006-01-02", strings.TrimSpace(dateText))
+	if err != nil {
+		return "", fmt.Errorf("failed to parse report date: %v", err)
+	}
+
+	// 获取年份的后两位
+	shortYear := reportDate.Year() % 100
+
+	return fmt.Sprintf("%02d年%d月%d日", shortYear, reportDate.Month(), reportDate.Day()), nil
+}
+
+// GetCurrentYearAndLastYear 获取当前年份和前一年的年份 转换时间格式 dateText 格式为 2024-08-03 --> 2024年,2023年
+func GetCurrentYearAndLastYear(dateText string) ([]string, error) {
+	// 解析日期
+	reportDate, err := time.Parse("2006-01-02", strings.TrimSpace(dateText))
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse report date: %v", err)
+	}
+
+	years := make([]string, 2)
+	year := reportDate.Year()
+
+	// 当前年份
+	years[0] = fmt.Sprintf("%d年", year)
+	// 前一年
+	years[1] = fmt.Sprintf("%d年", year-1)
+
+	return years, nil
+}
+
+// ConvertTimeFormatToYearMonth 转换时间格式 dateText 返回本月 和 后两月 格式为 2024-08-03 --> 2024年8月,2024-10-03 --> 2024年10月
+func ConvertTimeFormatToYearMonth(dateText string) ([]string, error) {
+
+	// 解析日期
+	reportDate, err := time.Parse("2006-01-02", strings.TrimSpace(dateText))
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse report date: %v", err)
+	}
+
+	months := make([]string, 3)
+	for i := 0; i < 3; i++ {
+		month := reportDate.AddDate(0, i, 0).Format("2006年1月")
+		months[i] = month
+	}
+
+	return months, nil
+}
+
+// GetYearMonthNoYear 获取本月和后两月的年月 2024-08-03 --> 24年8月,24年9月,24年10月
+func GetYearMonthNoYear(dateText string) ([]string, error) {
+	// 解析日期
+	reportDate, err := time.Parse("2006-01-02", strings.TrimSpace(dateText))
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse report date: %v", err)
+	}
+
+	months := make([]string, 3)
+	for i := 0; i < 3; i++ {
+		month := reportDate.AddDate(0, i, 0).Format("06年1月")
+		months[i] = month
+	}
+
+	return months, nil
+}
+
+// GetCurrentYearAndNextYear 获取当时所在得年度和明年得年度列表 2024-08-03 --> 2023/24年度, 2024/25年度
+func GetCurrentYearAndNextYear(dateText string) ([]string, error) {
+	// 解析日期
+	reportDate, err := time.Parse("2006-01-02", strings.TrimSpace(dateText))
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse report date: %v", err)
+	}
+
+	years := make([]string, 2)
+	year := reportDate.Year()
+
+	// 当前年度
+	years[0] = fmt.Sprintf("%d/%02d年度", year-1, year%100)
+	// 下一年度
+	years[1] = fmt.Sprintf("%d/%02d年度", year, (year+1)%100)
+
+	return years, nil
+}
+
+// IsSameMonth 判断当前传入年月是否是同一月 2024-08-03, 2024-08 --> true, 2024-08-03, 2024-07 --> false
+func IsSameMonth(dateText1, dateText2 string) (bool, error) {
+	// 解析日期
+	date1, err := time.Parse("2006-01-02", strings.TrimSpace(dateText1))
+	if err != nil {
+		return false, fmt.Errorf("failed to parse date1: %v", err)
+	}
+
+	date2, err := time.Parse("2006-01", strings.TrimSpace(dateText2))
+	if err != nil {
+		return false, fmt.Errorf("failed to parse date2: %v", err)
+	}
+
+	return date1.Year() == date2.Year() && date1.Month() == date2.Month(), nil
+}
+
+// GetLastDayOfMonth 获取传入年月的最后一天 dateText 格式为 2024-08 --> 2024-08-31
+func GetLastDayOfMonth(dateText string) (string, error) {
+	// 解析日期
+	date, err := time.Parse("2006-01", strings.TrimSpace(dateText))
+	if err != nil {
+		return "", fmt.Errorf("failed to parse date: %v", err)
+	}
+
+	// 获取下个月的第一天
+	nextMonth := date.AddDate(0, 1, 0)
+
+	// 获取本月的最后一天
+	lastDay := nextMonth.AddDate(0, 0, -1)
+
+	return lastDay.Format("2006-01-02"), nil
+}
+
+// ConvertMonthToNumber 时间转换 格式 8月 --> 08
+func ConvertMonthToNumber(dateText string) (string, error) {
+	// 去掉字符串中的 "月"
+	trimmed := strings.TrimSuffix(strings.TrimSpace(dateText), "月")
+
+	// 将月份转换为整数
+	month, err := strconv.Atoi(trimmed)
+	if err != nil {
+		return "", fmt.Errorf("failed to parse month: %v", err)
+	}
+
+	return fmt.Sprintf("%02d", month), nil
+}
+
+// 时间转换 格式 1 --> 01
+func ConvertMonthToNumber1(dateText string) (string, error) {
+	// 将月份转换为整数
+	month, err := strconv.Atoi(dateText)
+	if err != nil {
+		return "", fmt.Errorf("failed to parse month: %v", err)
+	}
+
+	return fmt.Sprintf("%02d", month), nil
+}
+
+// GetNextThreeMonths 获取传入时间的本月及后两月的年月 2024-08-03 --> 24年8月
+func GetNextThreeMonths(dateText string) ([]string, error) {
+	// 解析日期字符串为时间类型
+	date, err := time.Parse("2006-01-02", dateText)
+	if err != nil {
+		return nil, fmt.Errorf("日期解析错误: %v", err)
+	}
+
+	// 存储结果的切片
+	var result []string
+
+	// 获取本月及后两个月的年份和月份
+	for i := 0; i < 3; i++ {
+		year := date.Year() % 100
+		month := int(date.Month())
+
+		// 构建并添加当前年月到结果中
+		result = append(result, fmt.Sprintf("%d年%d月", year, month))
+
+		// 将日期加一个月
+		date = date.AddDate(0, 1, 0)
+	}
+
+	return result, nil
+}
+
+// IsCurrentYear 判断是否是当前年度 传入日期格式为 2023/24年度  --> true, 2024/25年度 --> false
+func IsCurrentYear(dateText string) (bool, error) {
+	// 去掉字符串中的 "年度"
+	trimmed := strings.TrimSuffix(strings.TrimSpace(dateText), "年度")
+
+	// 分割年份,例如 "2023/24" -> ["2023", "24"]
+	parts := strings.Split(trimmed, "/")
+	if len(parts) != 2 {
+		return false, fmt.Errorf("invalid date format: %s", dateText)
+	}
+
+	// 将前一年的年份转换为整数
+	startYear, err := strconv.Atoi(parts[0])
+	if err != nil {
+		return false, fmt.Errorf("failed to parse start year: %v", err)
+	}
+
+	// 获取当前年份
+	currentYear := time.Now().Year()
+
+	// 如果当前年份等于 dateText 中的后一年的年份,返回 true
+	if currentYear == startYear+1 {
+		return true, nil
+	}
+
+	return false, nil
+}
+
+// GetNextYearLastDay 获取明年本月份的最后一天 2024-08-03 --> 2025-08-31
+func GetNextYearLastDay(dateText string) (string, error) {
+	// 解析日期
+	reportDate, err := time.Parse("2006-01-02", strings.TrimSpace(dateText))
+	if err != nil {
+		return "", fmt.Errorf("failed to parse report date: %v", err)
+	}
+
+	// 获取下一年的年份
+	nextYear := reportDate.Year() + 1
+	// 获取本月份的最后一天
+	lastDay := time.Date(nextYear, reportDate.Month()+1, 0, 0, 0, 0, 0, reportDate.Location())
+
+	return lastDay.Format("2006-01-02"), nil
+}
+
+// GetYearMonth 获取年月日 2024-08-03 --> 2024-08
+func GetYearMonth(dateText string) (string, error) {
+	// 解析日期
+	reportDate, err := time.Parse("2006-01-02", strings.TrimSpace(dateText))
+	if err != nil {
+		return "", fmt.Errorf("failed to parse report date: %v", err)
+	}
+
+	return reportDate.Format("2006-01"), nil
+}
+
+// GetCurrentMonth 获取当前月份 2024-08-03 --> 8月
+func GetCurrentMonth(dateText string) (string, error) {
+	// 解析日期
+	reportDate, err := time.Parse("2006-01-02", strings.TrimSpace(dateText))
+	if err != nil {
+		return "", fmt.Errorf("failed to parse report date: %v", err)
+	}
+
+	// 计算月份
+	month := reportDate.Month()
+
+	return fmt.Sprintf("%d月", month), nil
+}

+ 60 - 0
utils/http_request.go

@@ -0,0 +1,60 @@
+package utils
+
+import (
+	"encoding/json"
+
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"strings"
+)
+
+// PostEdbLibRequest 调用指标接口
+func PostEdbLibRequest(param map[string]interface{}, method string) (result []byte, err error) {
+	postUrl := EDB_LIB_URL + method
+	postData, err := json.Marshal(param)
+	if err != nil {
+		return
+	}
+	result, err = HttpPostRequest(postUrl, string(postData), "application/json")
+	if err != nil {
+		return
+	}
+	return
+}
+
+func HttpPostRequest(url, postData string, params ...string) ([]byte, error) {
+	fmt.Println("HttpPost Url:" + url)
+	body := ioutil.NopCloser(strings.NewReader(postData))
+	client := &http.Client{}
+	req, err := http.NewRequest("POST", url, body)
+	if err != nil {
+		return nil, err
+	}
+	contentType := "application/x-www-form-urlencoded;charset=utf-8"
+	if len(params) > 0 && params[0] != "" {
+		contentType = params[0]
+	}
+	req.Header.Set("Content-Type", contentType)
+	req.Header.Set("authorization", MD5(APP_EDB_LIB_NAME_EN+EDB_LIB_Md5_KEY))
+	resp, err := client.Do(req)
+	if err != nil {
+		fmt.Println("client.Do err:" + err.Error())
+		return nil, err
+	}
+	defer resp.Body.Close()
+	b, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		fmt.Println("HttpPost:" + string(b))
+	}
+	return b, err
+}
+
+func HttpGetRequest(url string) ([]byte, error) {
+	res, err := http.Get(url)
+	if err != nil {
+		return nil, err
+	}
+	defer res.Body.Close()
+	return ioutil.ReadAll(res.Body)
+}

+ 88 - 0
utils/index_code_util.go

@@ -0,0 +1,88 @@
+// @Author gmy 2024/8/7 10:41:00
+package utils
+
+import (
+	"fmt"
+	"github.com/mozillazg/go-pinyin"
+	"strings"
+	"unicode"
+)
+
+// GenerateIndexCode 指标编码规则:粮油商务网拼音首字母+指标名称拼音首字母,数字、字母保留,特殊字符拿掉
+// 例:美湾:9月U:国际大豆进口成本价:期货收盘:张家港 -----> lyswwmw9yUgjddjkcbjqhspzjg
+func GenerateIndexCode(sourceName string, indexName string) string {
+
+	// 获取汉字的拼音首字母,保留数字和大写字母
+	firstLetters := getFirstLetters(indexName)
+
+	// 组合 sourceName 和处理后的拼音首字母
+	indexCode := fmt.Sprintf("%s%s", sourceName, firstLetters)
+
+	return indexCode
+}
+
+// getFirstLetters 获取汉字的拼音首字母,并保留数字和大写字母
+func getFirstLetters(input string) string {
+	// 设置拼音转换选项,只获取首字母
+	args := pinyin.NewArgs()
+	args.Style = pinyin.FirstLetter
+
+	// 定义用于存储结果的字符串
+	var result strings.Builder
+
+	// 遍历输入字符串中的每个字符
+	for _, r := range input {
+		if unicode.IsDigit(r) || unicode.IsUpper(r) {
+			// 保留数字和大写字母
+			result.WriteRune(r)
+		} else if unicode.Is(unicode.Han, r) {
+			// 如果是汉字,则获取其拼音首字母
+			py := pinyin.Pinyin(string(r), args)
+			if len(py) > 0 && len(py[0]) > 0 {
+				result.WriteString(py[0][0])
+			}
+		}
+		// 对于其他字符,忽略处理
+	}
+
+	return result.String()
+}
+
+/*func GenerateIndexCode(sourceName string, indexName string) string {
+	// 获取拼音首字母
+	indexInitials := getFirstLetter(indexName)
+
+	// 保留源名称中的字母和数字
+	sourceNameFiltered := filterAlphanumeric(indexName)
+
+	// 拼接源名称和首字母
+	indexCode := sourceName + sourceNameFiltered + indexInitials
+
+	// 保留字母和数字,去掉其他特殊字符
+	re := regexp.MustCompile(`[^a-zA-Z0-9]`)
+	indexCode = re.ReplaceAllString(indexCode, "")
+
+	// 转换为小写
+	indexCode = strings.ToLower(indexCode)
+
+	return indexCode
+}
+
+// 获取字符串中的拼音首字母
+func getFirstLetter(s string) string {
+	a := pinyin.NewArgs()
+	p := pinyin.Pinyin(s, a)
+	firstLetters := ""
+	for _, syllables := range p {
+		if len(syllables) > 0 {
+			firstLetters += strings.ToUpper(syllables[0][:1])
+		}
+	}
+	return firstLetters
+}
+
+// 过滤只保留字母和数字
+func filterAlphanumeric(s string) string {
+	re := regexp.MustCompile(`[^a-zA-Z0-9]`)
+	return re.ReplaceAllString(s, "")
+}*/