Browse Source

Merge branch 'refs/heads/eta_2.0.8_ly_0804@guomengyuan' into debug

gmy 7 tháng trước cách đây
mục cha
commit
b26a6b2a08

+ 26 - 0
models/base_from_ly_classify.go

@@ -0,0 +1,26 @@
+// @Author gmy 2024/8/7 9:26:00
+package models
+
+import "github.com/beego/beego/v2/client/orm"
+
+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)"`            // 英文分类名称
+}
+
+func init() {
+	orm.RegisterModel(new(BaseFromLyClassify))
+}
+
+// GetLyClassifyByName 根据分类名称查询
+func GetLyClassifyByName(classifyName string) (item *BaseFromLyClassify, err error) {
+	o := orm.NewOrmUsingDB("data")
+	sql := `SELECT * FROM base_from_ly_classify WHERE classify_name=?`
+	err = o.Raw(sql, classifyName).QueryRow(&item)
+	return
+}

+ 49 - 0
models/base_from_ly_data.go

@@ -0,0 +1,49 @@
+// @Author gmy 2024/8/7 9:50:00
+package models
+
+import "github.com/beego/beego/v2/client/orm"
+
+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)"`                   // 数据值
+}
+
+func init() {
+	orm.RegisterModel(new(BaseFromLyData))
+}
+
+// AddLyDataList 批量插入数据记录列表
+func AddLyDataList(items []BaseFromLyData) (err error) {
+	o := orm.NewOrmUsingDB("data")
+	_, err = o.InsertMulti(len(items), items)
+	return
+}
+
+// GetLyDataByIndexIdAndDataTime 根据指标id和数据日期查询数据
+func GetLyDataByIndexIdAndDataTime(indexId int, dataTime string) (items []BaseFromLyData, err error) {
+	o := orm.NewOrmUsingDB("data")
+	sql := `SELECT * FROM base_from_ly_data WHERE base_from_ly_index_id=? AND data_time=?`
+	_, err = o.Raw(sql, indexId, dataTime).QueryRows(&items)
+	return
+}
+
+// GetLyDataByIndexIdAndDataTimeYM 根据指标id和数据日期的年月查询数据
+func GetLyDataByIndexIdAndDataTimeYM(indexId int, dataTime string) (items []BaseFromLyData, err error) {
+	o := orm.NewOrmUsingDB("data")
+	sql := `SELECT * FROM base_from_ly_data WHERE base_from_ly_index_id=? AND data_time like ?`
+	_, err = o.Raw(sql, indexId, dataTime+"%").QueryRows(&items)
+	return
+}
+
+// UpdateLyDataById 根据主键id更新数据
+func UpdateLyDataById(dataId int, value float64) (err error) {
+	o := orm.NewOrmUsingDB("data")
+	sql := `UPDATE base_from_ly_data SET value=? WHERE base_from_ly_data_id=?`
+	_, err = o.Raw(sql, value, dataId).Exec()
+	return
+}

+ 50 - 0
models/base_from_ly_index.go

@@ -0,0 +1,50 @@
+// @Author gmy 2024/8/7 9:38:00
+package models
+
+import (
+	"github.com/beego/beego/v2/client/orm"
+	"time"
+)
+
+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-是
+}
+
+// 在 init 函数中注册模型
+func init() {
+	orm.RegisterModel(new(BaseFromLyIndex))
+}
+
+// AddLyIndexList 批量插入指标记录列表
+func AddLyIndexList(items []*BaseFromLyIndex) (err error) {
+	o := orm.NewOrmUsingDB("data")
+	_, err = o.InsertMulti(len(items), items)
+	return
+}
+
+// AddLyIndex 添加指标
+func AddLyIndex(item *BaseFromLyIndex) (int64, error) {
+	item.CreateTime = time.Now().Format("2006-01-02 15:04:05")
+	o := orm.NewOrmUsingDB("data")
+	id, err := o.Insert(item)
+	if err != nil {
+		return 0, err
+	}
+	return id, nil
+}
+
+// GetLyIndexByCode 查询指标编码是否存在
+func GetLyIndexByCode(indexCode string) (item *BaseFromLyIndex, err error) {
+	o := orm.NewOrmUsingDB("data")
+	sql := `SELECT * FROM base_from_ly_index WHERE index_code=?`
+	err = o.Raw(sql, indexCode).QueryRow(&item)
+	return
+}

+ 38 - 0
models/ebd_data_ly.go

@@ -0,0 +1,38 @@
+// Package models
+// @Author gmy 2024/8/14 19:40:00
+package models
+
+import "github.com/beego/beego/v2/client/orm"
+
+type EdbDataLy struct {
+	edbDataId     int     `orm:"column(edb_data_id);pk"` // 数据ID
+	CreateTime    string  `orm:"column(create_time)"`    // 创建时间
+	ModifyTime    string  `orm:"column(modify_time)"`    // 修改时间
+	EdbInfoId     int     `orm:"column(edb_info_id)"`    // 指标id
+	EdbCode       string  `orm:"column(edb_code)"`       // 指标编码
+	DataTime      string  `orm:"column(data_time)"`      // 数据日期
+	Value         float64 `orm:"column(value)"`          // 数据值
+	DataTimestamp uint64  `orm:"column(data_timestamp)"` // 数据日期时间戳
+}
+
+func GetLyEdbDataByIndexCodeAndDataTime(indexCode string, dataTime string) (items []EdbDataLy, err error) {
+	o := orm.NewOrmUsingDB("data")
+	sql := `SELECT * FROM edb_data_ly WHERE index_code=? AND data_time like ?`
+	_, err = o.Raw(sql, indexCode, dataTime+"%").QueryRows(&items)
+	return
+}
+
+func GetLyEdbDataByIndexCodeAndExactDataTime(indexCode string, dataTime string) (items []EdbDataLy, err error) {
+	o := orm.NewOrmUsingDB("data")
+	sql := `SELECT * FROM edb_data_ly WHERE edb_code=? AND data_time=?`
+	_, err = o.Raw(sql, indexCode, dataTime).QueryRows(&items)
+	return
+}
+
+// UpdateLyEdbDataById 更新指标库数据 须根据指标编码和日期更新 仅适合月度数据
+func UpdateLyEdbDataById(id int, value float64) (err error) {
+	o := orm.NewOrmUsingDB("data")
+	sql := `UPDATE edb_data_ly SET value=? WHERE edb_data_id=?`
+	_, err = o.Raw(sql, value, id).Exec()
+	return
+}

+ 10 - 0
services/eia_steo.go

@@ -52,6 +52,11 @@ func syncEiaSteoData() (err error) {
 
 	var nowClassify *models.BaseFromEiaSteoClassify
 	for _, v := range eiaSteoData.VIEWSDATA.ROWS {
+		// 如果没有数据,那么就返回
+		if v.HASDATA != 1 {
+			continue
+		}
+
 		if v.LEVEL == 1 {
 			tmpNowClassify, ok := classifyMap[v.CHARTNAME]
 			if !ok {
@@ -71,8 +76,13 @@ func syncEiaSteoData() (err error) {
 			} else {
 				nowClassify = tmpNowClassify
 			}
+			//continue
+		}
+		// 如果系列名称为空的话,那么也返回
+		if v.SERIESID == `` {
 			continue
 		}
+
 		if v.LEVEL > 1 && v.SERIESID == `` {
 			continue
 		}

+ 402 - 0
services/liangyou/commodity_liangyou.go

@@ -0,0 +1,402 @@
+package liangyou
+
+import (
+	"context"
+	"encoding/json"
+	models "eta/eta_crawler/models"
+	"eta/eta_crawler/utils"
+	"fmt"
+	"github.com/beego/beego/v2/core/logs"
+	"github.com/chromedp/cdproto/cdp"
+	"os"
+	"regexp"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/chromedp/chromedp"
+)
+
+const (
+	lyLoginPath = "https://www.fao.com.cn/"
+)
+
+func LyDataDeal(cont context.Context) (err error) {
+	// 读取 JSON 文件
+	configFile, err := os.ReadFile(utils.LY_JSON_PATH)
+	if err != nil {
+		fmt.Printf("读取配置文件错误: %v\n", err)
+		return nil
+	}
+
+	// 定义通用的 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 nil
+	}
+
+	// 打印解析后的数据以验证
+	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 nil
+	}
+
+	// 遍历配置并爬取数据
+	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)
+				}
+			}
+		}
+	}
+	return nil
+}
+
+func login(ctx context.Context) error {
+
+	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"]`, utils.LY_USERNAME, chromedp.ByQuery),
+		chromedp.SetValue(`input[id="pwd"]`, utils.LY_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
+	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
+		reportURLs := extractReportURLs(htmlContent, report)
+		allReportURLs = append(allReportURLs, reportURLs...)
+
+		// 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, allReportURLs)
+
+	if len(allReportURLs) == 0 {
+		return fmt.Errorf("未找到报告 URL")
+	}
+
+	// 处理报告数据
+	for _, reportURL := range allReportURLs {
+		// 随机睡眠 todo 跑数据时放开
+		rand := utils.RangeRand(10, 100)
+		fmt.Println(report+";sleep:", strconv.Itoa(int(rand)))
+		time.Sleep(time.Duration(rand) * time.Second)
+
+		err = processReport(ctx, product, category, reportURL, keywords)
+		if err != nil {
+			logs.Error("处理报告错误: %s: %s: %s: %s: %v", product, category, report, reportURL, err)
+		}
+	}
+
+	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) []string {
+	var reportURLs []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 {
+			reportURLs = append(reportURLs, content[urlStartIdx:urlEndIdx])
+		}
+
+		content = content[startIdx:]
+	}
+
+	return reportURLs
+}
+
+// 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
+	/*// 使用 XPath 构造选择器
+	clickSelector := fmt.Sprintf(`//a[@href='%s']`, reportURL)
+
+	err := chromedp.Run(ctx,
+		chromedp.WaitReady(clickSelector, chromedp.BySearch), // 等待选择器准备好
+		chromedp.Click(clickSelector, chromedp.BySearch),     // 点击目标链接
+		chromedp.Sleep(5*time.Second),                        // 等待页面加载
+		chromedp.OuterHTML("html", &reportContent),           // 获取页面 HTML 内容
+	)
+	if err != nil {
+		return err
+	}*/
+	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
+	}
+
+	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.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
+}

+ 2264 - 0
services/liangyou/processor_business_logic.go

@@ -0,0 +1,2264 @@
+// Package liangyou
+// @Author gmy 2024/8/6 10:50:00
+package liangyou
+
+import (
+	"context"
+	"eta/eta_crawler/models"
+	"eta/eta_crawler/utils"
+	"fmt"
+	"github.com/PuerkitoBio/goquery"
+	"github.com/beego/beego/v2/core/logs"
+	"github.com/chromedp/chromedp"
+	"log"
+	"regexp"
+	"strconv"
+	"strings"
+	"unicode"
+)
+
+const (
+	lySourceName = "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(lySourceName, indexName)
+						// 指标id获取
+						indexId, err := getIndexId(indexCode, indexName, classifyId, lySourceName, 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(lySourceName, indexName)
+						// 指标id获取
+						indexId, err := getIndexId(indexCode, indexName, classifyId, lySourceName, 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(lySourceName, indexName)
+					// 指标id获取
+					indexId, err := getIndexId(indexCode, indexName, classifyId, lySourceName, 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(lySourceName, indexName)
+					// 指标id获取
+					indexId, err := getIndexId(indexCode, indexName, classifyId, lySourceName, 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(lySourceName, indexName)
+					// 指标id获取
+					indexId, err := getIndexId(indexCode, indexName, classifyId, lySourceName, 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(lySourceName, indexName)
+	// 指标id获取
+	indexId, err := getIndexId(indexCode, indexName, classifyId, lySourceName, 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(lySourceName, indexName)
+					// 指标id获取
+					indexId, err := getIndexId(indexCode, indexName, classifyId, lySourceName, 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(lySourceName, indexName)
+					// 指标id获取
+					indexId, err := getIndexId(indexCode, indexName, classifyId, lySourceName, 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(lySourceName, indexName)
+					// 指标id获取
+					indexId, err := getIndexId(indexCode, indexName, classifyId, lySourceName, 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")
+	}
+
+	// 获取第一个表格
+	areaTableData := getNoHeadTableData(reportContent)[0]
+	// 获取第二个表格
+	blocTableData := getTableData(reportContent, false)
+	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(lySourceName, indexName)
+					// 指标id获取
+					indexId, err := getIndexId(indexCode, indexName, classifyId, lySourceName, 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(lySourceName, indexName)
+							// 指标id获取
+							indexId, err := getIndexId(indexCode, indexName, classifyId, lySourceName, 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(lySourceName, indexName)
+					// 指标id获取
+					indexId, err := getIndexId(indexCode, indexName, classifyId, lySourceName, 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(lySourceName, indexName)
+						// 指标id获取
+						indexId, err := getIndexId(indexCode, indexName, classifyId, lySourceName, 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(lySourceName, indexName)
+							// 指标id获取
+							indexId, err := getIndexId(indexCode, indexName, classifyId, lySourceName, 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(lySourceName, indexName)
+						// 指标id获取
+						indexId, err := getIndexId(indexCode, indexName, classifyId, lySourceName, 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(lySourceName, indexName)
+					// 指标id获取
+					indexId, err := getIndexId(indexCode, indexName, classifyId, lySourceName, 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(lySourceName, indexName)
+						// 指标id获取
+						indexId, err := getIndexId(indexCode, indexName, classifyId, lySourceName, 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, lySourceName string, frequency string, unit string) (int, error) {
+	// 判断指标是否存在
+	var indexId int
+	indexInfo, err := models.GetLyIndexByCode(indexCode)
+	if err != 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
+}

+ 111 - 0
services/liangyou/processor_factory.go

@@ -0,0 +1,111 @@
+// Package liangyou
+// @Author gmy 2024/8/6 10:48:00
+package liangyou
+
+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)
+}

+ 8 - 4
services/sci99/sci99.go

@@ -6,12 +6,13 @@ import (
 	"encoding/json"
 	"eta/eta_crawler/utils"
 	"fmt"
-	"github.com/mozillazg/go-pinyin"
 	"io/ioutil"
 	"net/http"
 	"strconv"
 	"strings"
 	"time"
+
+	"github.com/mozillazg/go-pinyin"
 )
 
 var IndexCodeMap = make(map[string]string)
@@ -164,7 +165,7 @@ func Scii99(cont context.Context) (err error) {
 
 				// 初始化是不加日期获取全部,初始化后只拿前三天的
 				if utils.IS_INIT_SCI99 != "true" {
-					deailReq.Start = time.Now().AddDate(0,0,-5).Format(utils.FormatDate2)
+					deailReq.Start = time.Now().AddDate(0, 0, -5).Format(utils.FormatDate2)
 					deailReq.End = time.Now().Format(utils.FormatDate2)
 				}
 
@@ -216,8 +217,8 @@ func Scii99(cont context.Context) (err error) {
 					utils.FileLog.Info("Error unmarshalling JSON:", err)
 					return
 				}
-				fmt.Println("classifyName:",classifyName)
-				fmt.Println("indexName:",indexName)
+				fmt.Println("classifyName:", classifyName)
+				fmt.Println("indexName:", indexName)
 				fmt.Println("数据长度:", len(detailResponse.Data.List))
 				param := make(map[string]interface{})
 				param["ListData"] = v
@@ -361,6 +362,9 @@ func HttpPost(url, postData string, params ...string) ([]byte, error) {
 	req.Header.Set("Content-Type", contentType)
 	req.Header.Set("authorization", utils.MD5(utils.APP_EDB_LIB_NAME_EN+utils.EDB_LIB_Md5_KEY))
 	resp, err := client.Do(req)
+	if err != nil {
+		return nil, err
+	}
 	defer resp.Body.Close()
 	b, err := ioutil.ReadAll(resp.Body)
 	fmt.Println("HttpPost:" + string(b))

+ 113 - 85
services/smm_shanghai.go

@@ -3,6 +3,7 @@ package services
 import (
 	"context"
 	"encoding/json"
+	"errors"
 	"eta/eta_crawler/models"
 	"eta/eta_crawler/services/sci99"
 	"eta/eta_crawler/utils"
@@ -13,6 +14,7 @@ import (
 	"strings"
 	"time"
 
+	"github.com/beego/beego/v2/task"
 	"github.com/xuri/excelize/v2"
 )
 
@@ -26,13 +28,13 @@ type SmmRequest struct {
 }
 
 var nickelReqs = []SmmRequest{
-	{
-		Name:      "SMM 1#电解镍",
-		IndexCode: "",
-		Url:       "https://hq.smm.cn/ajax/spot/history/201102250239",
-		Frequency: "日度",
-		Unit:      "元/吨",
-	},
+	// {
+	// 	Name:      "SMM 1#电解镍",
+	// 	IndexCode: "",
+	// 	Url:       "https://hq.smm.cn/ajax/spot/history/201102250239",
+	// 	Frequency: "日度",
+	// 	Unit:      "元/吨",
+	// },
 	{
 		Name:      "1#金川镍",
 		Url:       "https://hq.smm.cn/ajax/spot/history/201102250174",
@@ -51,43 +53,43 @@ var nickelReqs = []SmmRequest{
 		Frequency: "日度",
 		Unit:      "元/吨",
 	},
-	{
-		Name:      `印尼内贸红土镍矿1.2%(到厂价)`,
-		Url:       "https://hq.smm.cn/ajax/spot/history/202311230010",
-		Frequency: "周度",
-		Unit:      "美元/湿吨",
-	},
-	{
-		Name:      `印尼内贸红土镍矿1.6%(到厂价)`,
-		Url:       "https://hq.smm.cn/ajax/spot/history/202311230011",
-		Frequency: "周度",
-		Unit:      "美元/湿吨",
-	},
+	// {
+	// 	Name:      `印尼内贸红土镍矿1.2%(到厂价)`,
+	// 	Url:       "https://hq.smm.cn/ajax/spot/history/202311230010",
+	// 	Frequency: "周度",
+	// 	Unit:      "美元/湿吨",
+	// },
+	// {
+	// 	Name:      `印尼内贸红土镍矿1.6%(到厂价)`,
+	// 	Url:       "https://hq.smm.cn/ajax/spot/history/202311230011",
+	// 	Frequency: "周度",
+	// 	Unit:      "美元/湿吨",
+	// },
 	{
 		Name:      `菲律宾红土镍矿0.9%,Al≥7%(CIF)`,
 		Url:       "https://hq.smm.cn/ajax/spot/history/201608170001",
 		Frequency: "日度",
 		Unit:      "美元/湿吨",
 	},
-	{
-		Name:      `菲律宾红土镍矿0.9%,Al<7%(CIF) `, // 重名
-		Url:       "https://hq.smm.cn/ajax/spot/history/202109140006",
-		Frequency: "日度",
-		Unit:      "美元/湿吨",
-	},
+	// {
+	// 	Name:      `菲律宾红土镍矿0.9%,Al<7%(CIF) `, // 重名
+	// 	Url:       "https://hq.smm.cn/ajax/spot/history/202109140006",
+	// 	Frequency: "日度",
+	// 	Unit:      "美元/湿吨",
+	// },
 
-	{
-		Name:      `菲律宾红土镍矿1.3%(CIF)`,
-		Url:       "https://hq.smm.cn/ajax/spot/history/202109140001",
-		Frequency: "日度",
-		Unit:      "美元/湿吨",
-	},
-	{
-		Name:      `菲律宾红土镍矿1.4%(CIF)`,
-		Url:       "https://hq.smm.cn/ajax/spot/history/202109140003",
-		Frequency: "日度",
-		Unit:      "美元/湿吨",
-	},
+	// {
+	// 	Name:      `菲律宾红土镍矿1.3%(CIF)`,
+	// 	Url:       "https://hq.smm.cn/ajax/spot/history/202109140001",
+	// 	Frequency: "日度",
+	// 	Unit:      "美元/湿吨",
+	// },
+	// {
+	// 	Name:      `菲律宾红土镍矿1.4%(CIF)`,
+	// 	Url:       "https://hq.smm.cn/ajax/spot/history/202109140003",
+	// 	Frequency: "日度",
+	// 	Unit:      "美元/湿吨",
+	// },
 	{
 		Name:      `菲律宾红土镍矿1.5%(CIF)`,
 		Url:       "https://hq.smm.cn/ajax/spot/history/201608170002",
@@ -107,24 +109,24 @@ var nickelReqs = []SmmRequest{
 		Frequency: "日度",
 		Unit:      "美元/湿吨",
 	},
-	{
-		Name:      `菲律宾红土镍矿0.9%,Al<7%(FOB)`, //重名
-		Url:       "https://hq.smm.cn/ajax/spot/history/202109140005",
-		Frequency: "日度",
-		Unit:      "美元/湿吨",
-	},
-	{
-		Name:      `菲律宾红土镍矿1.3%(FOB)`,
-		Url:       "https://hq.smm.cn/ajax/spot/history/202109140002",
-		Frequency: "日度",
-		Unit:      "美元/湿吨",
-	},
-	{
-		Name:      `菲律宾红土镍矿1.4%(FOB)`,
-		Url:       "https://hq.smm.cn/ajax/spot/history/202109140004",
-		Frequency: "日度",
-		Unit:      "美元/湿吨",
-	},
+	// {
+	// 	Name:      `菲律宾红土镍矿0.9%,Al<7%(FOB)`, //重名
+	// 	Url:       "https://hq.smm.cn/ajax/spot/history/202109140005",
+	// 	Frequency: "日度",
+	// 	Unit:      "美元/湿吨",
+	// },
+	// {
+	// 	Name:      `菲律宾红土镍矿1.3%(FOB)`,
+	// 	Url:       "https://hq.smm.cn/ajax/spot/history/202109140002",
+	// 	Frequency: "日度",
+	// 	Unit:      "美元/湿吨",
+	// },
+	// {
+	// 	Name:      `菲律宾红土镍矿1.4%(FOB)`,
+	// 	Url:       "https://hq.smm.cn/ajax/spot/history/202109140004",
+	// 	Frequency: "日度",
+	// 	Unit:      "美元/湿吨",
+	// },
 	{
 		Name:      `菲律宾红土镍矿1.5%(FOB)`,
 		Url:       "https://hq.smm.cn/ajax/spot/history/201509220002",
@@ -170,12 +172,12 @@ var copperReqs = []SmmRequest{
 		Value:     "low",
 		Frequency: "日度",
 	},
-	{
-		Name:      "SMM 广东1#电解铜",
-		Url:       "https://hq.smm.cn/ajax/spot/history/201912300001",
-		Unit:      "元/吨",
-		Frequency: "日度",
-	},
+	// {
+	// 	Name:      "SMM 广东1#电解铜",
+	// 	Url:       "https://hq.smm.cn/ajax/spot/history/201912300001",
+	// 	Unit:      "元/吨",
+	// 	Frequency: "日度",
+	// },
 	{
 		Name:      "SMM 广东1#电解铜升贴水最大值",
 		Url:       "https://hq.smm.cn/ajax/premium/history/201912300002",
@@ -275,18 +277,18 @@ var zincReqs = []SmmRequest{
 	// 	Unit:      "元/吨",
 	// 	Frequency: "日度",
 	// },
-	{
-		Name:      "Zn50国产TC(月)",
-		Url:       "https://hq.smm.cn/ajax/spot/history/201312030008",
-		Unit:      "元/金属吨",
-		Frequency: "月度",
-	},
-	{
-		Name:      "Zn50进口TC(月)",
-		Url:       "https://hq.smm.cn/ajax/spot/history/201312030009",
-		Unit:      "美元/千吨",
-		Frequency: "月度",
-	},
+	// {
+	// 	Name:      "Zn50国产TC(月)",
+	// 	Url:       "https://hq.smm.cn/ajax/spot/history/201312030008",
+	// 	Unit:      "元/金属吨",
+	// 	Frequency: "月度",
+	// },
+	// {
+	// 	Name:      "Zn50进口TC(月)",
+	// 	Url:       "https://hq.smm.cn/ajax/spot/history/201312030009",
+	// 	Unit:      "美元/千吨",
+	// 	Frequency: "月度",
+	// },
 	{
 		Name:      "Zn50国产TC(周)",
 		Url:       "https://hq.smm.cn/ajax/spot/history/202004070002",
@@ -465,10 +467,17 @@ func ExcelToSmm(f *excelize.File, s string) (edbInfoList []*EdbInfoData) {
 		}
 		var parseDate time.Time
 		var err error
+		// 表格的日期存在两种格式
 		if strings.Contains(s, "锌") {
 			parseDate, err = time.Parse(utils.FormatDate, v)
+			if err != nil {
+				parseDate, err = time.Parse("01-02-06", v)
+			}
 		} else {
 			parseDate, err = time.Parse("01-02-06", v)
+			if err != nil {
+				parseDate, err = time.Parse(utils.FormatDate, v)
+			}
 		}
 		if err != nil {
 			fmt.Println(err)
@@ -505,8 +514,25 @@ func ExcelToSmm(f *excelize.File, s string) (edbInfoList []*EdbInfoData) {
 	return
 }
 
-func OldExcel() {
-	excelPath := ""
+func SyncOldExcel(ctx context.Context) error {
+	fmt.Println("excel数据录入中")
+	err := OldExcel()
+	if err != nil {
+		fmt.Println(err)
+		utils.FileLog.Info(err.Error())
+		return err
+	}
+	fmt.Println("excel数据录入成功")
+	task.DeleteTask("shanghaiOldExcel")
+	return nil
+}
+
+func OldExcel() (err error) {
+	excelPath := utils.OLD_EXCEL_PATH_JR
+	if excelPath == "" {
+		fmt.Println("excel文件路径为空")
+		return
+	}
 	f, err := excelize.OpenFile(excelPath)
 	if err != nil {
 		fmt.Print(err)
@@ -515,7 +541,7 @@ func OldExcel() {
 	var edbClassify = []string{`镍`, `铜`, `锌`, `锌(日度)`}
 	for _, v := range edbClassify {
 		edbInfoList := ExcelToSmm(f, v)
-		_, err := json.Marshal(edbInfoList)
+		_, err = json.Marshal(edbInfoList)
 		if err != nil {
 			fmt.Println(err)
 			return
@@ -523,28 +549,30 @@ func OldExcel() {
 		fmt.Println("成功:", v)
 		urlStr := `shanghai_smm/refresh/excel`
 		postUrl := utils.EDB_LIB_URL + urlStr
-		postData, err := json.Marshal(edbInfoList)
-		if err != nil {
-			utils.FileLog.Info("Marshal Err:" + err.Error())
+		postData, er := json.Marshal(edbInfoList)
+		if er != nil {
+			err = er
+			utils.FileLog.Info("Marshal Err:" + er.Error())
 			return
 		}
-		result, err := sci99.HttpPost(postUrl, string(postData), "application/json")
-		if err != nil {
-			utils.FileLog.Info("HttpPost Err:" + err.Error())
+		result, er := sci99.HttpPost(postUrl, string(postData), "application/json")
+		if er != nil {
+			err = er
+			utils.FileLog.Info("HttpPost Err:" + er.Error())
 			return
 		}
 		resp := new(models.BaseResponse)
 		err = json.Unmarshal(result, &resp)
 		if err != nil {
-			fmt.Println(err)
 			utils.FileLog.Info("Unmarshal resp Err:" + err.Error())
 			return
 		}
 		if resp.Ret != 200 {
-			fmt.Println("上海有色网excel历史有色数据更新失败")
+			err = errors.New(resp.ErrMsg)
 			utils.FileLog.Info("上海有色网excel历史有色数据更新失败")
 			return
 		}
-		time.Sleep(10 * time.Second)
+		time.Sleep(time.Second)
 	}
+	return
 }

+ 10 - 2
services/task.go

@@ -34,6 +34,8 @@ func Task() {
 
 		crawlerIcpi := task.NewTask("refreshData", "0 0,30 16-23 * * *", CrawlerIcpi) //居民消费价格指数
 
+		//lyData := task.NewTask("refreshData", "0 0,30 16-23 * * *", liangyou.LyDataDeal) //todo 粮油商务网 时间上线定
+
 		// 统计局-分月季年爬
 		//refreshNationalMonthA := task.NewTask("RefreshNationalMonthDbA", "0 15 2 10 * *", national_data.RefreshNationalMonthDbA)
 		//refreshNationalMonthB := task.NewTask("RefreshNationalMonthDbB", "0 15 2 16 * *", national_data.RefreshNationalMonthDbB)
@@ -51,6 +53,7 @@ func Task() {
 		task.AddTask("UN月度数据", syncYearMonthComTrade) //每月1号的3点同步
 
 		task.AddTask("居民消费价格指数", crawlerIcpi) //每月1号的3点同步
+		//task.AddTask("粮油商务网", lyData)
 	}
 
 	if utils.BusinessCode == utils.BusinessCodeFuBang {
@@ -60,9 +63,14 @@ func Task() {
 		task.AddTask("卓创资讯", sci99)
 	}
 
-	crawlerSmm := task.NewTask("refreshShangHaiSmmData", "0 0 10-18 * * *", SyncShangHaiSmm) // 上海有色网爬虫
+	if utils.BusinessCode == utils.BusinessCodeJinRui {
+		crawlerSmm := task.NewTask("refreshShangHaiSmmData", "0 0 11-20 * * *", SyncShangHaiSmm)       // 上海有色网爬虫
+		task.AddTask("上海有色网指标爬取", crawlerSmm)                                                          //每天10-20点, 每小时爬一次
+		crawlerSmmExcel := task.NewTask("refreshShangHaiSmmDataExcel", "0/30 * * * * *", SyncOldExcel) // 上海有色网爬虫
+		task.AddTask("shanghaiOldExcel", crawlerSmmExcel)                                              // 爬取成功后删除该任务
+
+	}
 
-	task.AddTask("上海有色网指标爬取", crawlerSmm) //每天10-18点, 每小时爬一次
 	//task.AddTask("统计局数据爬取-月度A", refreshNationalMonthA) // 每月10号2:15执行
 	//task.AddTask("统计局数据爬取-月度B", refreshNationalMonthB) // 每月16号2:15执行
 	//task.AddTask("统计局数据爬取-季度", refreshNationalQuarter) // 每月15号1:25执行

+ 497 - 0
static/liangyou.json

@@ -0,0 +1,497 @@
+{
+  "大豆": {
+    "进口成本": {
+      "国际大豆进口成本参考价": [
+        "美湾:国际大豆进口成本价:期货收盘:张家港:美分/蒲式耳:日度",
+        "美湾:国际大豆进口成本价:升贴水:张家港:美分/蒲式耳:日度",
+        "美湾:国际大豆进口成本价:FOB价:张家港:美元/吨:日度",
+        "美湾:国际大豆进口成本价:运费:张家港:美元/吨:日度",
+        "美湾:国际大豆进口成本价:CNF升贴水:张家港:美分/蒲式耳:日度",
+        "美湾:国际大豆进口成本价:CNF:张家港:美元/吨:日度",
+        "美湾:国际大豆进口成本价:进口成本:张家港:元/吨:日度",
+        "巴西:国际大豆进口成本价:期货收盘:张家港:美分/蒲式耳:日度",
+        "巴西:国际大豆进口成本价:升贴水:张家港:美分/蒲式耳:日度",
+        "巴西:国际大豆进口成本价:FOB价:张家港:美元/吨:日度",
+        "巴西:国际大豆进口成本价:运费:张家港:美元/吨:日度",
+        "巴西:国际大豆进口成本价:CNF升贴水:张家港:美分/蒲式耳:日度",
+        "巴西:国际大豆进口成本价:CNF:张家港:美元/吨:日度",
+        "巴西:国际大豆进口成本价:进口成本:张家港:元/吨:日度"
+      ]
+    },
+    "加工利润": {
+      "进口大豆盘面榨利及现货榨利表": [
+        "美湾:进口大豆盘面榨利:元/吨:日度",
+        "巴西:进口大豆盘面榨利:元/吨:日度",
+        "美湾:进口大豆现货榨利:元/吨:日度",
+        "巴西:进口大豆现货榨利:元/吨:日度"
+      ]
+    },
+    "船运费用": {
+      "国际谷物船运费报价及走势图": [
+        "巴西桑托斯:中国北方港口:超灵便型船:国际谷物船运费:当日价格:美元:日度",
+        "阿根廷:中国北方港口:巴拿马型船:国际谷物船运费:当日价格:美元:日度",
+        "美湾密西西比河:中国北方港口:巴拿马型船:国际谷物船运费:当日价格:美元:日度",
+        "美西塔科马:中国北方港口:巴拿马型船:国际谷物船运费:当日价格:美元:日度",
+        "美国北太平洋沿岸:中国:巴拿马型船:国际谷物船运费:当日价格:美元:日度",
+        "美国墨西哥湾:中国:巴拿马型船:国际谷物船运费:当日价格:美元:日度",
+        "巴西巴拉那瓜:中国:巴拿马型船:国际谷物船运费:当日价格:美元:日度",
+        "伊特科提亚拉港:中国:巴拿马型船:国际谷物船运费:当日价格:美元:日度",
+        "波罗的海巴拿马型指数(BPI):国际谷物船运费:当日价格:%:日度",
+        "波罗的海超灵便型指数(BSI):国际谷物船运费:当日价格:%:日度",
+        "波罗的海海岬型指数(BCI):国际谷物船运费:当日价格:%:日度",
+        "波罗的海干散货指数(BDI):国际谷物船运费:当日价格:%:日度",
+        "波罗的海灵便型指数(BHSI):国际谷物船运费:当日价格:%:日度"
+      ]
+    },
+    "供需平衡": {
+      "年度中国大豆市场供需报告": [
+        "中国大豆市场供需:期初库存:万吨:月度",
+        "中国大豆市场供需:种植面积:万吨:月度",
+        "中国大豆市场供需:国内产量:万吨:月度",
+        "中国大豆市场供需:进口量:万吨:月度",
+        "中国大豆市场供需:总供应量:万吨:月度",
+        "中国大豆市场供需:压榨用量:万吨:月度",
+        "中国大豆市场供需:其中:国产大豆:万吨:月度",
+        "中国大豆市场供需:进口大豆:万吨:月度",
+        "中国大豆市场供需:出口量:万吨:月度",
+        "中国大豆市场供需:食用量:万吨:月度",
+        "中国大豆市场供需:种用及其他:万吨:月度",
+        "中国大豆市场供需:总需求量:万吨:月度",
+        "中国大豆市场供需:期末库存:万吨:月度"
+      ]
+    },
+    "采购装船": {
+      "中国大豆采购进度周统计": [
+        "中国大豆计划采购量:万吨:周度",
+        "中国大豆已采购量:美国:万吨:周度",
+        "中国大豆已采购量:巴西:万吨:周度",
+        "中国大豆已采购量:阿根廷/乌拉圭:万吨:周度",
+        "中国大豆已采购量:小计:万吨:周度",
+        "中国大豆未采购量:万吨:周度",
+        "中国大豆采购进度:%:周度"
+      ]
+    },
+    "加工报告": {
+      "国内大豆周度加工量调查": [
+        "国内大豆加工量:河南省:万吨:周度",
+        "国内大豆加工量:湖北省:万吨:周度",
+        "国内大豆加工量:湖南省:万吨:周度",
+        "国内大豆加工量:黑龙江:万吨:周度",
+        "国内大豆加工量:吉林省:万吨:周度",
+        "国内大豆加工量:辽宁省:万吨:周度",
+        "国内大豆加工量:内蒙古:万吨:周度",
+        "国内大豆加工量:河北省:万吨:周度",
+        "国内大豆加工量:天津市:万吨:周度",
+        "国内大豆加工量:江西省:万吨:周度",
+        "国内大豆加工量:山东省:万吨:周度",
+        "国内大豆加工量:安徽省:万吨:周度",
+        "国内大豆加工量:江苏省:万吨:周度",
+        "国内大豆加工量:上海市:万吨:周度",
+        "国内大豆加工量:浙江省:万吨:周度",
+        "国内大豆加工量:福建省:万吨:周度",
+        "国内大豆加工量:广东省:万吨:周度",
+        "国内大豆加工量:广西省:万吨:周度",
+        "国内大豆加工量:海南省:万吨:周度",
+        "国内大豆加工量:陕西省:万吨:周度",
+        "国内大豆加工量:四川省:万吨:周度",
+        "国内大豆加工量:重庆市:万吨:周度",
+        "国内大豆加工量:云南省:万吨:周度",
+        "国内大豆加工量:合计:万吨:周度",
+        "国内大豆加工量:其中:国产:万吨:周度",
+        "国内大豆加工量:进口:万吨:周度",
+        "国内大豆开机率:河南省:%:周度",
+        "国内大豆开机率:湖北省:%:周度",
+        "国内大豆开机率:湖南省:%:周度",
+        "国内大豆开机率:黑龙江:%:周度",
+        "国内大豆开机率:吉林省:%:周度",
+        "国内大豆开机率:辽宁省:%:周度",
+        "国内大豆开机率:内蒙古:%:周度",
+        "国内大豆开机率:河北省:%:周度",
+        "国内大豆开机率:天津市:%:周度",
+        "国内大豆开机率:山西省:%:周度",
+        "国内大豆开机率:山东省:%:周度",
+        "国内大豆开机率:安徽省:%:周度",
+        "国内大豆开机率:江苏省:%:周度",
+        "国内大豆开机率:上海市:%:周度",
+        "国内大豆开机率:浙江省:%:周度",
+        "国内大豆开机率:福建省:%:周度",
+        "国内大豆开机率:广东省:%:周度",
+        "国内大豆开机率:广西省:%:周度",
+        "国内大豆开机率:海南省:%:周度",
+        "国内大豆开机率:陕西省:%:周度",
+        "国内大豆开机率:四川省:%:周度",
+        "国内大豆开机率:重庆市:%:周度",
+        "国内大豆开机率:云南省:%:周度",
+        "国内大豆开机率:合计:%:周度",
+        "国内大豆开机率:其中:国产:%:周度",
+        "国内大豆开机率:进口:%:周度"
+      ]
+    },
+    "库存分析": {
+      "全国油厂进口大豆库存量统计周报": [
+        "全国油厂进口大豆库存量(万吨):东北地区:万吨:周度",
+        "全国油厂进口大豆库存量(万吨):华北地区:万吨:周度",
+        "全国油厂进口大豆库存量(万吨):华东地区:万吨:周度",
+        "全国油厂进口大豆库存量(万吨):华南地区:万吨:周度",
+        "全国油厂进口大豆库存量(万吨):西南地区:万吨:周度",
+        "全国油厂进口大豆库存量(万吨):其他地区:万吨:周度",
+        "全国油厂进口大豆库存量(万吨):全国统计:万吨:周度",
+        "全国油厂进口大豆库存量(万吨):其中:沿海库存:万吨:周度"
+      ]
+    }
+  },
+  "豆粕": {
+    "库存分析": {
+      "全国油厂豆粕库存与合同统计周报": [
+        "全国油厂豆粕库存量(万吨):东北地区:万吨:周度",
+        "全国油厂豆粕库存量(万吨):华北地区:万吨:周度",
+        "全国油厂豆粕库存量(万吨):华东地区:万吨:周度",
+        "全国油厂豆粕库存量(万吨):华中地区:万吨:周度",
+        "全国油厂豆粕库存量(万吨):华南地区:万吨:周度",
+        "全国油厂豆粕库存量(万吨):西南地区:万吨:周度",
+        "全国油厂豆粕库存量(万吨):西北地区:万吨:周度",
+        "全国油厂豆粕库存量(万吨):全国合计:万吨:周度",
+        "全国油厂豆粕库存量(万吨):其中:沿海库存:万吨:周度",
+        "全国油厂豆粕合同量(万吨):东北地区:万吨:周度",
+        "全国油厂豆粕合同量(万吨):华北地区:万吨:周度",
+        "全国油厂豆粕合同量(万吨):华东地区:万吨:周度",
+        "全国油厂豆粕合同量(万吨):华中地区:万吨:周度",
+        "全国油厂豆粕合同量(万吨):华南地区:万吨:周度",
+        "全国油厂豆粕合同量(万吨):西南地区:万吨:周度",
+        "全国油厂豆粕合同量(万吨):西北地区:万吨:周度",
+        "全国油厂豆粕合同量(万吨):全国合计:万吨:周度",
+        "全国油厂豆粕合同量(万吨):其中:沿海库存:万吨:周度"
+      ]
+    }
+  },
+  "大豆油": {
+    "库存分析": {
+      "全国油厂豆油库存与合同统计周报": [
+        "全国油厂豆油库存量(万吨):东北地区:万吨:周度",
+        "全国油厂豆油库存量(万吨):华北地区:万吨:周度",
+        "全国油厂豆油库存量(万吨):华东地区:万吨:周度",
+        "全国油厂豆油库存量(万吨):华中地区:万吨:周度",
+        "全国油厂豆油库存量(万吨):华南地区:万吨:周度",
+        "全国油厂豆油库存量(万吨):西南地区:万吨:周度",
+        "全国油厂豆油库存量(万吨):西北地区:万吨:周度",
+        "全国油厂豆油库存量(万吨):全国合计:万吨:周度",
+        "全国油厂豆油库存量(万吨):其中:沿海库存:万吨:周度",
+        "全国油厂豆油合同量(万吨):东北地区:万吨:周度",
+        "全国油厂豆油合同量(万吨):华北地区:万吨:周度",
+        "全国油厂豆油合同量(万吨):华东地区:万吨:周度",
+        "全国油厂豆油合同量(万吨):华中地区:万吨:周度",
+        "全国油厂豆油合同量(万吨):华南地区:万吨:周度",
+        "全国油厂豆油合同量(万吨):西南地区:万吨:周度",
+        "全国油厂豆油合同量(万吨):西北地区:万吨:周度",
+        "全国油厂豆油合同量(万吨):全国合计:万吨:周度",
+        "全国油厂豆油合同量(万吨):其中:沿海库存:万吨:周度"
+      ]
+    },
+    "价差套利": {
+      "豆棕油期现货价差统计": [
+        "棕榈油主力:大商所:期现货价差:元/吨:日度",
+        "豆油主力:大商所:期现货价差:元/吨:日度",
+        "24度棕榈油:进口液:期现货价差:元/吨:日度",
+        "一级豆油:进口压榨:期现货价差:元/吨:日度"
+      ]
+    },
+    "每日成交": {
+      "豆油成交量及价格统计": [
+        "豆油成交量:东北:吨:日度",
+        "豆油成交量:华北:吨:日度",
+        "豆油成交量:江苏:吨:日度",
+        "豆油成交量:浙江:吨:日度",
+        "豆油成交量:山东:吨:日度",
+        "豆油成交量:广东:吨:日度",
+        "豆油成交量:广西:吨:日度",
+        "豆油成交量:福建:吨:日度",
+        "豆油成交量:其他:吨:日度",
+        "豆油成交量:合计:吨:日度",
+        "豆油周度成交量:东北:吨:周度",
+        "豆油周度成交量:华北:吨:周度",
+        "豆油周度成交量:江苏:吨:周度",
+        "豆油周度成交量:浙江:吨:周度",
+        "豆油周度成交量:山东:吨:周度",
+        "豆油周度成交量:广东:吨:周度",
+        "豆油周度成交量:广西:吨:周度",
+        "豆油周度成交量:福建:吨:周度",
+        "豆油周度成交量:其他:吨:周度",
+        "豆油周度成交量:合计:吨:周度",
+        "主要集团:九三:豆油现货成交量:吨:周度",
+        "主要集团:中粮:豆油现货成交量:吨:周度",
+        "主要集团:中储粮:豆油现货成交量:吨:周度",
+        "豆油成交量:东北:1:吨:日度",
+        "豆油成交量:华北:1:吨:日度",
+        "豆油成交量:江苏:1:吨:日度",
+        "豆油成交量:浙江:1:吨:日度",
+        "豆油成交量:山东:1:吨:日度",
+        "豆油成交量:广东:1:吨:日度",
+        "豆油成交量:广西:1:吨:日度",
+        "豆油成交量:福建:1:吨:日度",
+        "豆油成交量:其他:1:吨:日度",
+        "豆油成交量:合计:1:吨:日度",
+        "豆油周度成交量:东北:1:吨:周度",
+        "豆油周度成交量:华北:1:吨:周度",
+        "豆油周度成交量:江苏:1:吨:周度",
+        "豆油周度成交量:浙江:1:吨:周度",
+        "豆油周度成交量:山东:1:吨:周度",
+        "豆油周度成交量:广东:1:吨:周度",
+        "豆油周度成交量:广西:1:吨:周度",
+        "豆油周度成交量:福建:1:吨:周度",
+        "豆油周度成交量:其他:1:吨:周度",
+        "豆油周度成交量:合计:1:吨:周度",
+        "主要集团:九三:豆油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:中粮:豆油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:中储粮:豆油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:达孚:豆油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:嘉吉:豆油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:金光:豆油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:邦基:豆油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:益海:豆油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:汇福:豆油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:渤海:豆油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:香驰:豆油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:中海:豆油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:其他:豆油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:总计:豆油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:九三:豆油基差成交量:本周(吨)基差成交:吨:周度",
+        "主要集团:中粮:豆油基差成交量:本周(吨)基差成交:吨:周度",
+        "主要集团:中储粮:豆油基差成交量:本周(吨)基差成交:吨:周度",
+        "主要集团:达孚:豆油基差成交量:本周(吨)基差成交:吨:周度",
+        "主要集团:嘉吉:豆油基差成交量:本周(吨)基差成交:吨:周度",
+        "主要集团:金光:豆油基差成交量:本周(吨)基差成交:吨:周度",
+        "主要集团:邦基:豆油基差成交量:本周(吨)基差成交:吨:周度",
+        "主要集团:益海:豆油基差成交量:本周(吨)基差成交:吨:周度",
+        "主要集团:汇福:豆油基差成交量:本周(吨)基差成交:吨:周度",
+        "主要集团:渤海:豆油基差成交量:本周(吨)基差成交:吨:周度",
+        "主要集团:香驰:豆油基差成交量:本周(吨)基差成交:吨:周度",
+        "主要集团:中海:豆油基差成交量:本周(吨)基差成交:吨:周度",
+        "主要集团:其他:豆油基差成交量:本周(吨)基差成交:吨:周度",
+        "主要集团:总计:豆油基差成交量:本周(吨)基差成交:吨:周度"
+      ]
+    }
+  },
+  "棕榈油": {
+    "国际价格": {
+      "国际棕榈油FOB报价及走势": [
+        "马来棕榈液油:FOB价格:美元/吨:日度",
+        "马来棕榈油:FOB价格:美元/吨:日度",
+        "马来棕榈硬脂:FOB价格:美元/吨:日度",
+        "印尼毛棕油:FOB价格:美元/吨:日度"
+      ]
+    },
+    "进口成本": {
+      "马来西亚棕榈油进口成本参考价": [
+        "棕榈液油(24度):运费:张家港:美元/吨:日度",
+        "棕榈液油(24度):CNF:张家港:美元/吨:日度",
+        "棕榈液油(24度):完税价:张家港:美元/吨:日度",
+        "棕榈液油(24度):进口成本:张家港:元/吨:日度"
+      ]
+    },
+    "库存分析": {
+      "全国棕榈油库存与合同统计周报": [
+        "棕榈油24度及以下库存:华北地区:万吨:周度",
+        "棕榈油24度及以下库存:华东地区:万吨:周度",
+        "棕榈油24度及以下库存:华南地区:万吨:周度",
+        "棕榈油24度及以下库存:其他地区:万吨:周度",
+        "棕榈油24度及以下库存:全国合计:万吨:周度",
+        "棕油总库存:华北地区:万吨:周度",
+        "棕油总库存:华东地区:万吨:周度",
+        "棕油总库存:华南地区:万吨:周度",
+        "棕油总库存:其他地区:万吨:周度",
+        "棕油总库存:全国合计:万吨:周度",
+        "棕榈油合同量:华北地区:万吨:周度",
+        "棕榈油合同量:华东地区:万吨:周度",
+        "棕榈油合同量:华南地区:万吨:周度",
+        "棕榈油合同量:其他地区:万吨:周度",
+        "棕榈油合同量:全国合计:万吨:周度"
+      ]
+    },
+    "每日成交": {
+      "棕榈油成交量及价格统计": [
+        "棕榈油成交量:华北:1:吨:日度",
+        "棕榈油成交量:山东:1:吨:日度",
+        "棕榈油成交量:江苏:1:吨:日度",
+        "棕榈油成交量:浙江:1:吨:日度",
+        "棕榈油成交量:福建:1:吨:日度",
+        "棕榈油成交量:广东:1:吨:日度",
+        "棕榈油成交量:广西:1:吨:日度",
+        "棕榈油成交量:其他:1:吨:日度",
+        "棕榈油成交量:合计:1:吨:日度",
+        "棕榈油周度成交量:华北:1:吨:日度",
+        "棕榈油周度成交量:山东:1:吨:日度",
+        "棕榈油周度成交量:江苏:1:吨:日度",
+        "棕榈油周度成交量:浙江:1:吨:日度",
+        "棕榈油周度成交量:福建:1:吨:日度",
+        "棕榈油周度成交量:广东:1:吨:日度",
+        "棕榈油周度成交量:广西:1:吨:日度",
+        "棕榈油周度成交量:其他:1:吨:日度",
+        "棕榈油周度成交量:合计:1:吨:日度",
+        "主要集团:中粮:豆油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:金光:豆油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:益海:豆油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:来宝:豆油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:合益荣:豆油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:其他:豆油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:总计:豆油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:中粮:豆油基差成交量:本周(吨)基差成交:吨:周度",
+        "主要集团:金光:豆油基差成交量:本周(吨)基差成交:吨:周度",
+        "主要集团:益海:豆油基差成交量:本周(吨)基差成交:吨:周度",
+        "主要集团:来宝:豆油基差成交量:本周(吨)基差成交:吨:周度",
+        "主要集团:合益荣:豆油基差成交量:本周(吨)基差成交:吨:周度",
+        "主要集团:其他:豆油基差成交量:本周(吨)基差成交:吨:周度",
+        "主要集团:总计:豆油基差成交量:本周(吨)基差成交:吨:周度"
+      ]
+    }
+  },
+  "油菜籽": {
+    "进口成本": {
+      "加拿大油菜籽理论进口成本": [
+        "加拿大油菜籽:期货收盘:广州港:美分/蒲式耳:日度",
+        "加拿大油菜籽:升贴水:广州港:美元/吨:日度",
+        "加拿大油菜籽:FOB价:广州港:美元/吨:日度",
+        "加拿大油菜籽:运费:广州港:美元/吨:日度",
+        "加拿大油菜籽:CNF升贴水:广州港:美元/吨:日度",
+        "加拿大油菜籽:CNF:广州港:美元/吨:日度",
+        "加拿大油菜籽:进口成本:广州港:元/吨:日度"
+      ]
+    },
+    "库存分析": {
+      "全国油厂进口油菜籽库存量统计周报": [
+        "全国油厂进口油菜籽库存量:广西地区:万吨:周度",
+        "全国油厂进口油菜籽库存量:广东地区:万吨:周度",
+        "全国油厂进口油菜籽库存量:福建地区:万吨:周度",
+        "全国油厂进口油菜籽库存量:江苏地区:万吨:周度",
+        "全国油厂进口油菜籽库存量:辽宁地区:万吨:周度",
+        "全国油厂进口油菜籽库存量:其它地区:万吨:周度",
+        "全国油厂进口油菜籽库存量:全国统计:万吨:周度"
+      ]
+    },
+    "进口预估": {
+      "进口油菜籽月度进口量预估": [
+        "进口油菜籽月度进口量预估:本年进口量:万吨:周度",
+        "进口油菜籽月度进口量预估:本年海关进口量:万吨:周度"
+      ]
+    },
+    "加拿大统计局": {
+      "加拿大双低油菜籽周度商业库存": [
+        "加拿大双低油菜籽:期初库存:万吨:周度",
+        "加拿大双低油菜籽:上市量:万吨:周度",
+        "加拿大双低油菜籽:出口量:万吨:周度",
+        "加拿大双低油菜籽:消费量:万吨:周度",
+        "加拿大双低油菜籽:期末库存:万吨:周度"
+      ]
+    },
+    "进出口分析": {
+      "油菜籽进口数量分析": [
+        "油菜籽进口量:万吨:月度"
+      ],
+      "油菜籽出口数量分析": [
+        "油菜籽出口量:吨:月度"
+      ]
+    }
+  },
+  "菜粕": {
+    "库存分析": {
+      "全国油厂进口压榨菜粕库存与合同统计周报": [
+        "全国油厂进口压榨菜粕库存量:广西地区:万吨:周度",
+        "全国油厂进口压榨菜粕库存量:广东地区:万吨:周度",
+        "全国油厂进口压榨菜粕库存量:福建地区:万吨:周度",
+        "全国油厂进口压榨菜粕库存量:江苏地区:万吨:周度",
+        "全国油厂进口压榨菜粕库存量:辽宁地区:万吨:周度",
+        "全国油厂进口压榨菜粕库存量:其他地区:万吨:周度",
+        "全国油厂进口压榨菜粕库存量:全国合计:万吨:周度",
+        "全国油厂进口压榨菜粕合同量:广西地区:万吨:周度",
+        "全国油厂进口压榨菜粕合同量:广东地区:万吨:周度",
+        "全国油厂进口压榨菜粕合同量:福建地区:万吨:周度",
+        "全国油厂进口压榨菜粕合同量:江苏地区:万吨:周度",
+        "全国油厂进口压榨菜粕合同量:辽宁地区:万吨:周度",
+        "全国油厂进口压榨菜粕合同量:其他地区:万吨:周度",
+        "全国油厂进口压榨菜粕合同量:全国合计:万吨:周度"
+
+      ]
+    },
+    "进出口分析": {
+      "菜粕出口数量分析": [
+        "菜粕出口数量:吨:月度"
+      ],
+      "菜粕进口数量分析": [
+        "菜粕进口数量:万吨:月度"
+      ]
+    }
+  },
+  "菜籽油": {
+    "库存分析": {
+      "全国油厂进口压榨菜油库存与合同统计周报": [
+        "全国油厂进口压榨菜油库存量:广西地区:万吨:周度",
+        "全国油厂进口压榨菜油库存量:广东地区:万吨:周度",
+        "全国油厂进口压榨菜油库存量:福建地区:万吨:周度",
+        "全国油厂进口压榨菜油库存量:江苏地区:万吨:周度",
+        "全国油厂进口压榨菜油库存量:辽宁地区:万吨:周度",
+        "全国油厂进口压榨菜油库存量:其他地区:万吨:周度",
+        "全国油厂进口压榨菜油库存量:全国合计:万吨:周度",
+        "全国油厂进口压榨菜油库存量:其中:非油企库存:万吨:周度",
+        "全国油厂进口压榨菜油库存量:油企库存:万吨:周度",
+        "全国油厂进口压榨菜油合同量:广西地区:万吨:周度",
+        "全国油厂进口压榨菜油合同量:广东地区:万吨:周度",
+        "全国油厂进口压榨菜油合同量:福建地区:万吨:周度",
+        "全国油厂进口压榨菜油合同量:江苏地区:万吨:周度",
+        "全国油厂进口压榨菜油合同量:辽宁地区:万吨:周度",
+        "全国油厂进口压榨菜油合同量:其他地区:万吨:周度",
+        "全国油厂进口压榨菜油合同量:全国合计:万吨:周度",
+        "全国油厂进口压榨菜油合同量:其中:非油企库存:万吨:周度",
+        "全国油厂进口压榨菜油合同量:油企库存:万吨:周度"
+      ]
+    },
+    "进口预估": {
+      "菜籽油月度进口量预估": [
+        "菜籽油月度进口量预估:本年进口量:万吨:周度",
+        "菜籽油月度进口量预估:本年海关进口量:万吨:周度"
+      ]
+    },
+    "每日成交": {
+      "菜籽油成交量及价格统计": [
+        "菜籽油成交量:东北:1:吨:日度",
+        "菜籽油成交量:华东:1:吨:日度",
+        "菜籽油成交量:福建:1:吨:日度",
+        "菜籽油成交量:广东:1:吨:日度",
+        "菜籽油成交量:广西:1:吨:日度",
+        "菜籽油成交量:其它:1:吨:日度",
+        "菜籽油成交量:合计:1:吨:日度",
+        "菜籽油成交量:华南合计:1:吨:日度",
+        "菜籽油成交量:沿海合计:1:吨:日度",
+        "菜籽油周度成交量:东北:1:吨:周度",
+        "菜籽油周度成交量:华东:1:吨:周度",
+        "菜籽油周度成交量:福建:1:吨:周度",
+        "菜籽油周度成交量:广东:1:吨:周度",
+        "菜籽油周度成交量:广西:1:吨:周度",
+        "菜籽油周度成交量:其它:1:吨:周度",
+        "菜籽油周度成交量:合计:1:吨:周度",
+        "菜籽油周度成交量:华南合计:1:吨:周度",
+        "菜籽油周度成交量:沿海合计:1:吨:周度",
+        "主要集团:营口嘉里:菜籽油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:富之源:菜籽油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:防城大海:菜籽油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:防城澳加:菜籽油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:成都中粮:菜籽油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:其它:菜籽油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:总计:菜籽油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:营口嘉里:菜籽油基差成交量:本周(吨)基差成交:吨:周度",
+        "主要集团:富之源:菜籽油基差成交量:本周(吨)基差成交:吨:周度",
+        "主要集团:防城大海:菜籽油基差成交量:本周(吨)基差成交:吨:周度",
+        "主要集团:防城澳加:菜籽油基差成交量:本周(吨)基差成交:吨:周度",
+        "主要集团:成都中粮:菜籽油基差成交量:本周(吨)基差成交:吨:周度",
+        "主要集团:其它:菜籽油基差成交量:本周(吨)基差成交:吨:周度",
+        "主要集团:总计:菜籽油基差成交量:本周(吨)基差成交:吨:周度"
+      ]
+    }
+  },
+  "葵花粕": {
+    "进口预估": {
+      "进口葵花粕月度进口量预估": [
+        "进口葵花粕月度进口量预估:本年进口量:万吨:周度",
+        "进口葵花粕月度进口量预估:本年海关进口量:万吨:周度"
+      ]
+    }
+  }
+}

+ 23 - 2
utils/config.go

@@ -2,9 +2,10 @@ package utils
 
 import (
 	"fmt"
+	"strconv"
+
 	beeLogger "github.com/beego/bee/v2/logger"
 	"github.com/beego/beego/v2/server/web"
-	"strconv"
 )
 
 var (
@@ -32,12 +33,23 @@ var (
 	IS_INIT_SCI99     string
 )
 
+// 粮油商务网
+var (
+	LY_USERNAME  string
+	LY_PASSWORD  string
+	LY_JSON_PATH string
+)
+
 var (
 	EDB_LIB_URL         string
 	APP_EDB_LIB_NAME_EN string
 	EDB_LIB_Md5_KEY     string
 )
 
+var (
+	OLD_EXCEL_PATH_JR string
+)
+
 func init() {
 	tmpRunMode, err := web.AppConfig.String("run_mode")
 	if err != nil {
@@ -80,11 +92,20 @@ func init() {
 		APP_EDB_LIB_NAME_EN = config["app_edb_lib_name_en"]
 		EDB_LIB_Md5_KEY = config["edb_lib_md5_key"]
 	}
+	if BusinessCode == BusinessCodeJinRui {
+		APP_EDB_LIB_NAME_EN = config["app_edb_lib_name_en"]
+		EDB_LIB_Md5_KEY = config["edb_lib_md5_key"]
+		EDB_LIB_URL = config["edb_lib_url"]
+		OLD_EXCEL_PATH_JR = config["old_excel_path_jr"]
+	}
 
 	if RunMode == "release" {
 
 	} else {
-
+		// todo 粮油商务网 上线再根据环境做配置
+		LY_USERNAME = config["ly_username"]
+		LY_PASSWORD = config["ly_password"]
+		LY_JSON_PATH = config["ly_json_path"]
 	}
 	//日志配置
 	{

+ 1 - 0
utils/constants.go

@@ -29,4 +29,5 @@ const (
 const (
 	BusinessCodeRelease = "E2023080900" // 生产环境
 	BusinessCodeFuBang  = "E2024020200"
+	BusinessCodeJinRui  = "E2023122901"
 )

+ 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月G",
+		"03": "3月H",
+		"04": "4月J",
+		"05": "5月K",
+		"06": "6月M",
+		"07": "7月N",
+		"08": "8月X",
+		"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
+}
+
+// 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
+}

+ 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, "")
+}*/