Jelajahi Sumber

Merge branch 'master' into feature/usda_psd_edb_data

# Conflicts:
#	utils/config.go
xyxie 3 bulan lalu
induk
melakukan
1ad4f0a08e

+ 41 - 4
models/base_from_eia_steo.go

@@ -2,9 +2,10 @@ package models
 
 import (
 	"eta/eta_crawler/utils"
+	"time"
+
 	"github.com/beego/beego/v2/client/orm"
 	"github.com/shopspring/decimal"
-	"time"
 )
 
 // BaseFromEiaSteoIndex EiaSteo指标
@@ -51,6 +52,8 @@ type BaseFromEiaSteoClassify struct {
 	BaseFromEiaSteoClassifyId int       `orm:"column(base_from_eia_steo_classify_id);pk"`
 	ClassifyName              string    `description:"分类名称(中文名称)"`
 	ClassifyNameOriginal      string    `description:"分类名称(原始名称)"`
+	ParentId                  int       `description:"父级id"`
+	Level                     int       `description:"层级"`
 	ModifyTime                time.Time `description:"最新更新时间"`
 	CreateTime                time.Time `description:"创建时间"`
 }
@@ -85,6 +88,20 @@ func GetBaseFromEiaSteoClassifyAll() (list []*BaseFromEiaSteoClassify, err error
 	return
 }
 
+func GetChildBaseFromEiaSteoClassifyById(classifyId int) (list []*BaseFromEiaSteoClassify, err error) {
+	o := orm.NewOrmUsingDB("data")
+	sql := `SELECT * FROM base_from_eia_steo_classify WHERE parent_id = ?`
+	_, err = o.Raw(sql, classifyId).QueryRows(&list)
+	return
+}
+
+func GetBaseFromEiaSteoClassifyByName(classifyName string) (item *BaseFromEiaSteoClassify, err error) {
+	o := orm.NewOrmUsingDB("data")
+	sql := `SELECT * FROM base_from_eia_steo_classify WHERE classify_name = ?`
+	err = o.Raw(sql, classifyName).QueryRow(&item)
+	return
+}
+
 // GetBaseFromEiaSteoIndexAll 获取所有的指标
 func GetBaseFromEiaSteoIndexAll() (list []*BaseFromEiaSteoIndex, err error) {
 	o := orm.NewOrmUsingDB("data")
@@ -93,6 +110,12 @@ func GetBaseFromEiaSteoIndexAll() (list []*BaseFromEiaSteoIndex, err error) {
 	return
 }
 
+func lastDayOfMonth(year int, month time.Month) string {
+	firstDay := time.Date(year, month, 1, 0, 0, 0, 0, time.Local)
+	nextMonth := firstDay.AddDate(0, 1, 0)
+	return nextMonth.Add(-time.Second * 1).Format(utils.FormatDate)
+}
+
 // HandleEiaSteoData 数据处理
 func HandleEiaSteoData(dataList map[string]interface{}, indexInfo *BaseFromEiaSteoIndex) (err error) {
 	o := orm.NewOrmUsingDB("data")
@@ -116,7 +139,9 @@ func HandleEiaSteoData(dataList map[string]interface{}, indexInfo *BaseFromEiaSt
 
 	existMap := make(map[string]*BaseFromEiaSteoData)
 	for _, v := range existList {
-		existMap[v.DataTime.Format(utils.FormatDate)] = v
+		// 产品要求每个月的最后一天,为了兼容之前每个月第一天的数据
+		dateTime := lastDayOfMonth(v.DataTime.Year(), v.DataTime.Month())
+		existMap[dateTime] = v
 	}
 
 	// 待添加的数据列表
@@ -135,13 +160,20 @@ func HandleEiaSteoData(dataList map[string]interface{}, indexInfo *BaseFromEiaSt
 			err = tmpErr
 			return
 		}
+		initDate := time.Date(2010, time.January, 1, 0, 0, 0, 0, time.Local)
+		if currDate.Before(initDate) {
+			continue
+		}
 		if currDate.After(endDate) {
 			endDate = currDate
 		}
 		if currDate.Before(startDate) {
 			startDate = currDate
 		}
-		date = currDate.Format(utils.FormatDate)
+		// 产品要求每个月的最后一天
+		date = lastDayOfMonth(currDate.Year(), currDate.Month())
+		currDate, _ = time.ParseInLocation(utils.FormatDate, date, time.Local)
+		// date = currDate.Format(utils.FormatDate)
 		//fmt.Println("date:", date)
 
 		tmpBaseFromEiaSteoData, ok := existMap[date]
@@ -158,10 +190,15 @@ func HandleEiaSteoData(dataList map[string]interface{}, indexInfo *BaseFromEiaSt
 		} else {
 			nowVal := decimal.NewFromFloat(tmpBaseFromEiaSteoData.Value)
 			//fmt.Println(date, "=========", nowVal.Equal(valDecimal))
+			var updateCols []string
 			if !nowVal.Equal(valDecimal) {
 				tmpBaseFromEiaSteoData.Value = val
 				tmpBaseFromEiaSteoData.ModifyTime = time.Now()
-				_, err = to.Update(tmpBaseFromEiaSteoData, "Value", "ModifyTime")
+				updateCols = append(updateCols, "value")
+				updateCols = append(updateCols, "modify_time")
+			}
+			if len(updateCols) > 0 {
+				_, err = to.Update(tmpBaseFromEiaSteoData, updateCols...)
 				if err != nil {
 					return
 				}

+ 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
+}

+ 46 - 0
models/base_from_ly_index_record.go

@@ -0,0 +1,46 @@
+// Package models
+// @Author gmy 2024/8/7 9:38:00
+package models
+
+import (
+	"errors"
+	"github.com/beego/beego/v2/client/orm"
+)
+
+type BaseFromLyIndexRecord struct {
+	BaseFromLyIndexRecordId int    `orm:"column(base_from_ly_index_record_id);pk"` // 指标记录ID
+	CreateTime              string `orm:"column(create_time)"`                     // 创建时间
+	ModifyTime              string `orm:"column(modify_time)"`                     // 修改时间
+	Product                 string `orm:"column(product)"`                         // 产品
+	Category                string `orm:"column(category)"`                        // 分类
+	Url                     string `orm:"column(url)"`                             // 指标页面地址
+	DataTime                string `orm:"column(data_time)"`                       // 数据日期
+}
+
+// 在 init 函数中注册模型
+func init() {
+	orm.RegisterModel(new(BaseFromLyIndexRecord))
+}
+
+// AddLyIndexRecord 添加指标记录
+func AddLyIndexRecord(item *BaseFromLyIndexRecord) (int64, error) {
+	o := orm.NewOrmUsingDB("data")
+	id, err := o.Insert(item)
+	if err != nil {
+		return 0, err
+	}
+	return id, nil
+}
+
+// GetLyIndexRecordByUrl 查询指标记录是否存在
+func GetLyIndexRecordByUrl(url string) (item *BaseFromLyIndexRecord, err error) {
+	o := orm.NewOrmUsingDB("data")
+	sql := `SELECT * FROM base_from_ly_index_record WHERE url=?`
+	err = o.Raw(sql, url).QueryRow(&item)
+
+	if errors.Is(err, orm.ErrNoRows) {
+		return nil, nil
+	}
+
+	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
+}

+ 238 - 12
services/eia_steo.go

@@ -4,24 +4,58 @@ import (
 	"context"
 	"encoding/json"
 	"eta/eta_crawler/models"
+	"eta/eta_crawler/utils"
 	"fmt"
-	"github.com/rdlucklib/rdluck_tools/http"
 	"reflect"
 	"strconv"
+	"strings"
 	"time"
+
+	"github.com/rdlucklib/rdluck_tools/http"
 )
 
+var noCrawlerName = map[string]struct{}{"Real Gross Domestic Product": {}, "Nominal U.S. Dollar Exchange Rate": {}}
+
 func SyncEiaSteoData(cont context.Context) (err error) {
-	err = syncEiaSteoData()
+	// err = syncEiaSteoData()
+	eiaSteoUrls := map[string]string{
+		"International Petroleum and Other Liquids Production、Consumption、Inventories": `https://www.eia.gov/outlooks/steo/data/browser/data/index.php?v=6&f=M&s=0&id=&linechart=PAPR_OECD~PAPR_NONOPEC&maptype=0&ctype=linechart&map=&method=getData`,
+		"Non-OPEC Petroleum and Other Liquids Production":                              `https://www.eia.gov/outlooks/steo/data/browser/data/index.php?v=29&f=M&s=0&start=201901&end=202512&id=&ctype=linechart&maptype=0&method=getData`,
+		"Total Liquid Fuels Production":                                                `https://www.eia.gov/outlooks/steo/data/browser/data/index.php?v=7&f=M&s=0&start=201901&end=202512&maptype=0&ctype=linechart&id=&method=getData`,
+		"Total Crude Oil Production":                                                   `https://www.eia.gov/outlooks/steo/data/browser/data/index.php?v=30&f=M&s=0&start=201901&end=202512&id=&ctype=linechart&maptype=0&method=getData`,
+		"World Petroleum and Other Liquid Fuels Consumption":                           `https://www.eia.gov/outlooks/steo/data/browser/data/index.php?v=31&f=M&s=0&start=201901&end=202512&maptype=0&ctype=linechart&id=&method=getData`,
+		"U.S. Petroleum and Other Liquids Supply、Consumption、Inventories":              `https://www.eia.gov/outlooks/steo/data/browser/data/index.php?v=9&f=M&s=0&start=201901&end=202512&maptype=0&ctype=linechart&method=getData`,
+		"Drilling Productivity Metrics":                                                `https://www.eia.gov/outlooks/steo/data/browser/data/index.php?v=32&f=M&s=0&start=201901&end=202512&ctype=linechart&maptype=0&method=getData`,
+		"Crude Oil and Natural Gas Production from Shale and Tight Formations":         `https://www.eia.gov/outlooks/steo/data/browser/data/index.php?v=33&f=M&s=0&start=201901&end=202512&maptype=0&ctype=linechart&method=getData`,
+	}
+	var eiaIndexName = []string{
+		"International Petroleum and Other Liquids Production、Consumption、Inventories",
+		"Non-OPEC Petroleum and Other Liquids Production",
+		"Total Liquid Fuels Production",
+		"Total Crude Oil Production",
+		"World Petroleum and Other Liquid Fuels Consumption",
+		"U.S. Petroleum and Other Liquids Supply、Consumption、Inventories",
+		"Drilling Productivity Metrics",
+		"Crude Oil and Natural Gas Production from Shale and Tight Formations",
+	}
+
+	for _, name := range eiaIndexName {
+		url := eiaSteoUrls[name]
+		err = syncEiaSteoDataV2(name, url)
+		if err != nil {
+			fmt.Println("同步失败", err)
+			return
+		}
+	}
 	return
 }
 
-func syncEiaSteoData() (err error) {
+func syncEiaSteoData(eiaSteoUrl string) (err error) {
 	// 获取数据
 
 	//官网地址:https://www.eia.gov/outlooks/steo/data/browser/#/?v=6&f=M&s=0&start=201701&end=202312&linechart=~T3_STCHANGE_WORLD&maptype=0&ctype=linechart&map=
 	// 这是获取数据的链接(月度的)
-	eiaSteoUrl := "https://www.eia.gov/outlooks/steo/data/browser/data/index.php?v=6&f=M&s=0&id=&linechart=PAPR_OECD~PAPR_NONOPEC&maptype=0&ctype=linechart&map=&method=getData"
+	// eiaSteoUrl := "https://www.eia.gov/outlooks/steo/data/browser/data/index.php?v=6&f=M&s=0&id=&linechart=PAPR_OECD~PAPR_NONOPEC&maptype=0&ctype=linechart&map=&method=getData"
 	eiaSteoData, err := queryData(eiaSteoUrl)
 	if err != nil {
 		fmt.Println("读取失败", err)
@@ -91,17 +125,12 @@ func syncEiaSteoData() (err error) {
 
 		eiaSteoIndex, ok := indexMap[v.SERIESID]
 
-		// 指标名称(中文)
-		indexName := EiaSteoNameMap[v.SERIESID]
-		if indexName == `` {
-			indexName = v.CHARTNAME
-		}
 		if !ok {
 			eiaSteoIndex = &models.BaseFromEiaSteoIndex{
 				//BaseFromEiaSteoIndexId:    0,
 				BaseFromEiaSteoClassifyId: nowClassify.BaseFromEiaSteoClassifyId,
 				IndexCode:                 v.SERIESID,
-				IndexName:                 indexName,
+				IndexName:                 v.CHARTNAME, // 不做中文名称的转换
 				IndexNameOriginal:         v.CHARTNAME,
 				Frequency:                 "月度",
 				Level:                     v.LEVEL,
@@ -128,8 +157,8 @@ func syncEiaSteoData() (err error) {
 				eiaSteoIndex.BaseFromEiaSteoClassifyId = nowClassify.BaseFromEiaSteoClassifyId
 				updateCol = append(updateCol, "BaseFromEiaSteoClassifyId")
 			}
-			if eiaSteoIndex.IndexName != indexName {
-				eiaSteoIndex.IndexName = indexName
+			if eiaSteoIndex.IndexName != v.CHARTNAME {
+				eiaSteoIndex.IndexName = v.CHARTNAME
 				updateCol = append(updateCol, "IndexName")
 			}
 			if eiaSteoIndex.IndexNameOriginal != v.CHARTNAME {
@@ -173,6 +202,203 @@ func syncEiaSteoData() (err error) {
 	return
 }
 
+func syncEiaSteoDataV2(eiaSteoName, eiaSteoUrl string) (err error) {
+	defer func() {
+		if r := recover(); r != nil {
+			fmt.Println("异常:", r)
+			fmt.Println("异常的eiaSteo名字", eiaSteoName)
+		}
+	}()
+	// 获取数据
+
+	//官网地址:https://www.eia.gov/outlooks/steo/data/browser/#/?v=6&f=M&s=0&start=201701&end=202312&linechart=~T3_STCHANGE_WORLD&maptype=0&ctype=linechart&map=
+	// 这是获取数据的链接(月度的)
+	// eiaSteoUrl := "https://www.eia.gov/outlooks/steo/data/browser/data/index.php?v=6&f=M&s=0&id=&linechart=PAPR_OECD~PAPR_NONOPEC&maptype=0&ctype=linechart&map=&method=getData"
+	eiaSteoData, err := queryData(eiaSteoUrl)
+	if err != nil {
+		fmt.Println("读取失败", err)
+		return
+	}
+
+	parentClassify, err := models.GetBaseFromEiaSteoClassifyByName(eiaSteoName)
+	if err != nil && err.Error() != utils.ErrNoRow() {
+		fmt.Println("添加eiaSteo指标异常, err: ", err.Error())
+		return
+	}
+	if parentClassify == nil {
+		tmpClassify := &models.BaseFromEiaSteoClassify{
+			ClassifyName:         eiaSteoName,
+			ClassifyNameOriginal: eiaSteoName,
+			Level:                1,
+			ModifyTime:           time.Now(),
+			CreateTime:           time.Now(),
+		}
+		// 新增指标分类
+		err = tmpClassify.AddBaseFromEiaSteoClassify()
+		if err != nil {
+			return
+		}
+		parentClassify = tmpClassify
+	}
+
+	// 获取分类列表
+	classifyList, err := models.GetChildBaseFromEiaSteoClassifyById(parentClassify.BaseFromEiaSteoClassifyId)
+	if err != nil {
+		fmt.Println("获取分类失败:", err)
+		return
+	}
+	classifyMap := make(map[string]*models.BaseFromEiaSteoClassify)
+	for _, v := range classifyList {
+		classifyMap[v.ClassifyNameOriginal] = v
+	}
+	// 获取指标列表
+	indexList, err := models.GetBaseFromEiaSteoIndexAll()
+	if err != nil {
+		fmt.Println("获取分类失败:", err)
+		return
+	}
+	indexMap := make(map[string]*models.BaseFromEiaSteoIndex)
+	for _, v := range indexList {
+		indexMap[v.IndexCode] = v
+	}
+
+	var hasClassify *models.BaseFromEiaSteoClassify
+	var noDataClassify *models.BaseFromEiaSteoClassify
+	for i, v := range eiaSteoData.VIEWSDATA.ROWS {
+		// 如果没有数据,那么就返回
+		if v.HASDATA != 1 && strings.TrimSpace(v.CHARTNAME) == "" {
+			curRow := eiaSteoData.VIEWSDATA.ROWS
+			length := len(eiaSteoData.VIEWSDATA.ROWS)
+			if length > i+1 {
+				if noDataClassify != nil {
+					if curRow[i+1].LEVEL == 1 && strings.TrimSpace(curRow[i+1].CHARTNAME) != "" && curRow[i+1].HASDATA != 1 {
+						noDataClassify = nil
+					}
+				}
+				if hasClassify != nil {
+					if curRow[i+1].LEVEL == 1 && strings.TrimSpace(curRow[i+1].CHARTNAME) != "" && curRow[i+1].HASDATA == 1 {
+						hasClassify = nil
+					}
+				}
+			}
+			continue
+		}
+		if noDataClassify == nil && hasClassify == nil && strings.TrimSpace(v.CHARTNAME) != "" && v.LEVEL == 1 {
+			classify, ok := classifyMap[v.CHARTNAME]
+			if !ok {
+				tmpClassify := &models.BaseFromEiaSteoClassify{
+					BaseFromEiaSteoClassifyId: 0,
+					ClassifyName:              v.CHARTNAME,
+					ClassifyNameOriginal:      v.CHARTNAME,
+					ParentId:                  parentClassify.BaseFromEiaSteoClassifyId,
+					Level:                     2,
+					ModifyTime:                time.Now(),
+					CreateTime:                time.Now(),
+				}
+				classifyMap[v.CHARTNAME] = tmpClassify
+				if v.HASDATA == 1 {
+					hasClassify = tmpClassify
+				} else {
+					noDataClassify = tmpClassify
+				}
+				if _, ok := noCrawlerName[tmpClassify.ClassifyName]; ok {
+					continue
+				}
+				// 新增指标分类
+				err = tmpClassify.AddBaseFromEiaSteoClassify()
+				if err != nil {
+					return
+				}
+			} else {
+				if v.HASDATA == 1 {
+					hasClassify = classify
+				} else {
+					noDataClassify = classify
+				}
+			}
+		}
+		// 如果系列名称为空的话,那么也返回
+		if v.SERIESID == `` {
+			continue
+		}
+		if v.HASDATA != 1 {
+			continue
+		}
+
+		var curClassify *models.BaseFromEiaSteoClassify
+		if noDataClassify != nil {
+			curClassify = noDataClassify
+		} else {
+			curClassify = hasClassify
+		}
+		if _, ok := noCrawlerName[curClassify.ClassifyName]; ok {
+			continue
+		}
+
+		eiaSteoIndex, ok := indexMap[v.SERIESID]
+		if !ok {
+			eiaSteoIndex = &models.BaseFromEiaSteoIndex{
+				//BaseFromEiaSteoIndexId:    0,
+				BaseFromEiaSteoClassifyId: curClassify.BaseFromEiaSteoClassifyId,
+				IndexCode:                 v.SERIESID,
+				IndexName:                 v.CHARTNAME, // 不做中文名称的转换
+				IndexNameOriginal:         v.CHARTNAME,
+				Frequency:                 "月度",
+				Level:                     v.LEVEL,
+				Unit:                      v.UNITS,
+				Super:                     v.SUPER,
+				Precision:                 v.PRECISION,
+				LastHistorical:            strconv.Itoa(v.LASTHISTORICAL),
+				Description:               v.DESCRIPTION,
+				IsMappable:                v.ISMAPPABLE,
+				StartDate:                 time.Now(),
+				EndDate:                   time.Now(),
+				ModifyTime:                time.Now(),
+				CreateTime:                time.Now(),
+			}
+			// 新增指标
+			err = eiaSteoIndex.Add()
+			if err != nil {
+				return
+			}
+			indexMap[v.SERIESID] = eiaSteoIndex
+		} else {
+			updateCol := make([]string, 0)
+			if eiaSteoIndex.BaseFromEiaSteoClassifyId != curClassify.BaseFromEiaSteoClassifyId {
+				eiaSteoIndex.BaseFromEiaSteoClassifyId = curClassify.BaseFromEiaSteoClassifyId
+				updateCol = append(updateCol, "BaseFromEiaSteoClassifyId")
+			}
+			if eiaSteoIndex.IndexName != v.CHARTNAME {
+				eiaSteoIndex.IndexName = v.CHARTNAME
+				updateCol = append(updateCol, "IndexName")
+			}
+			if eiaSteoIndex.IndexNameOriginal != v.CHARTNAME {
+				eiaSteoIndex.IndexNameOriginal = v.CHARTNAME
+				updateCol = append(updateCol, "IndexNameOriginal")
+			}
+			if len(updateCol) > 0 {
+				eiaSteoIndex.Update(updateCol)
+			}
+
+		}
+
+		//校验数据类型对不对
+		valType := reflect.TypeOf(v.DATA)
+		switch valType.String() {
+		case "[]interface {}": // 没有数据
+		case "map[string]interface {}": // 有数据
+			data := v.DATA.(map[string]interface{})
+			err = models.HandleEiaSteoData(data, eiaSteoIndex)
+			if err != nil {
+				return
+			}
+		}
+
+		fmt.Println(v.CHARTNAME, v.SERIESID, "==", v.PRECISION, "===========")
+	}
+	return
+}
+
 // queryData 接口请求网站数据
 func queryData(eiaSteoUrl string) (eiaSteoData EiaSteoData, err error) {
 	body, err := http.Get(eiaSteoUrl)

+ 476 - 0
services/liangyou/commodity_liangyou.go

@@ -0,0 +1,476 @@
+package liangyou
+
+import (
+	"context"
+	"encoding/json"
+	models "eta/eta_crawler/models"
+	"eta/eta_crawler/services/alarm_msg"
+	"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"
+)
+
+var (
+	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)
+					// 您看文章的速度太快了,歇一会再看吧
+					if strings.Contains(err.Error(), "您看文章的速度太快了,歇一会再看吧") {
+						return
+					}
+				}
+			}
+		}
+	}
+	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
+	var allReportURLMap = make(map[string]string)
+	for {
+		var htmlContent string
+		err = chromedp.Run(ctx,
+			chromedp.Navigate(categoryPageURL),
+			chromedp.Sleep(5*time.Second),
+			chromedp.OuterHTML("html", &htmlContent),
+		)
+		if err != nil {
+			return err
+		}
+		fmt.Printf("页面内容: %s\n", htmlContent)
+
+		// Extract report URLs containing the partial keyword
+		reportURLMap := extractReportURLs(htmlContent, report)
+		//allReportURLs = append(allReportURLs, reportURLs...)
+		for key, value := range reportURLMap {
+			allReportURLMap[key] = value
+		}
+
+		//  测试环境跑部分数据,上线放开
+		//break
+		// Check if next page button is disabled
+		//  测试环境跑部分数据,上线放开
+		var nextPageDisabled bool
+		err = chromedp.Run(ctx,
+			chromedp.Evaluate(`document.querySelector('div.my-page-next').classList.contains('my-page-forbid')`, &nextPageDisabled),
+		)
+		if err != nil {
+			return err
+		}
+
+		if nextPageDisabled {
+			break
+		}
+
+		// Click the next page button
+		err = chromedp.Run(ctx,
+			chromedp.Click(`div.my-page-next`, chromedp.ByQuery),
+			chromedp.Sleep(5*time.Second),
+			chromedp.Location(&categoryPageURL),
+		)
+		if err != nil {
+			return err
+		}
+
+	}
+
+	logs.Info("所有报告 URLs: %s: %s: %v", product, category, allReportURLMap)
+
+	if len(allReportURLMap) == 0 {
+		return fmt.Errorf("未找到报告 URL")
+	}
+
+	// 处理报告数据
+	for key, value := range allReportURLMap {
+		// 查询报告是否已经处理  这里只对近7天的数据进行处理
+		lyIndexRecord, err := models.GetLyIndexRecordByUrl(key)
+		if err != nil {
+			continue
+		}
+		if lyIndexRecord != nil {
+			toTime, err := utils.StringToTime(lyIndexRecord.DataTime + " 00:00:00")
+			if err != nil {
+				logs.Error("时间格式转换错误: %s: %s: %s: %s: %v", product, category, report, key, err)
+				continue
+			}
+
+			if time.Now().Sub(toTime) > 7*24*time.Hour {
+				logs.Info("报告已处理: %s: %s: %s: %s", product, category, report, key)
+				continue
+			}
+		}
+
+		// 随机睡眠
+		rand := utils.RangeRand(20, 100)
+		fmt.Println(report+";sleep:", strconv.Itoa(int(rand)))
+		time.Sleep(time.Duration(rand) * time.Second)
+
+		err = processReport(ctx, product, category, key, keywords)
+		if err != nil {
+			logs.Error("处理报告错误: %s: %s: %s: %s: %v", product, category, report, key, err)
+			if strings.Contains(err.Error(), "您看文章的速度太快了,歇一会再看吧") {
+				// 如果报告内容包含 “您看文章的速度太快了,歇一会再看吧” 则停止处理,发短信通知
+				// 发送短信通知
+				alarm_msg.SendAlarmMsg(fmt.Sprintf("粮油商务网-爬取指标数据被限制,请稍后重试, ErrMsg: %s", err.Error()), 1)
+				return nil
+			}
+			continue
+		}
+
+		format, err := utils.ConvertTimeFormat(value)
+		if err != nil {
+			logs.Error("时间格式转换错误: %s, %s, %v: %v", product, category, value, err)
+			continue
+		}
+
+		// 处理报告成功,将维护指标数据读取进度到数据库,避免后面重复读取
+		recordId, err := models.AddLyIndexRecord(&models.BaseFromLyIndexRecord{
+			CreateTime: utils.GetCurrentTime(),
+			ModifyTime: utils.GetCurrentTime(),
+			Product:    product,
+			Category:   category,
+			Url:        key,
+			DataTime:   format,
+		})
+		if err != nil {
+			logs.Error("维护指标数据读取进度错误: %s, %s, %v: %v", product, category, recordId, err)
+			continue
+		}
+		logs.Info("维护指标数据读取进度成功: %s, %s, %v", product, category, recordId)
+	}
+
+	return nil
+}
+
+func fillProductPageURL(ctx context.Context, product string, category string) (string, error) {
+	// 选择 dl 标签下所有 a 标签的 XPath
+	selector := `//dl[contains(@class, 'dl_hot')]//a`
+	logs.Info("选择器表达式: %s", selector)
+
+	var nodes []*cdp.Node
+	var productPageURL string
+
+	// 获取 dl 标签下的所有 a 标签节点
+	err := chromedp.Run(ctx,
+		chromedp.WaitReady(selector, chromedp.BySearch),
+		chromedp.Nodes(selector, &nodes, chromedp.BySearch),
+	)
+	if err != nil {
+		return "", err
+	}
+
+	// 提取并打印所有 a 标签的 OuterHTML
+	var targetURL string
+	for _, node := range nodes {
+		var outerHTML string
+
+		// 获取 a 标签的 OuterHTML
+		err = chromedp.Run(ctx,
+			chromedp.OuterHTML(node.FullXPath(), &outerHTML, chromedp.BySearch),
+		)
+		if err != nil {
+			return "", err
+		}
+
+		// 打印获取的 OuterHTML 内容
+		logs.Info("Link OuterHTML: %s", outerHTML)
+
+		// 从 OuterHTML 中提取 href 和文本内容
+		// 使用正则或字符串处理提取 href 和文本内容
+		href, linkText := extractHrefAndText(outerHTML)
+
+		// 打印提取的 href 和文本内容
+		logs.Info("Link Text: %s, Href: %s", linkText, href)
+
+		// 如果文本内容匹配目标产品
+		if linkText == product {
+			// 拼接完整的 URL
+			/*if !strings.HasPrefix(href, "http") {
+				href = lyLoginPath + href
+			}*/
+			targetURL = href
+			break
+		}
+	}
+
+	if targetURL == "" {
+		return "", fmt.Errorf("未找到匹配的产品链接")
+	}
+
+	// 显示更多内容
+	err = chromedp.Run(ctx,
+		chromedp.Evaluate(`document.getElementById("moreSpeList").style.display = "block";`, nil),
+	)
+	if err != nil {
+		return "", err
+	}
+
+	// 点击目标产品的链接
+	clickSelector := fmt.Sprintf(`//a[@href='%s']`, targetURL)
+	err = chromedp.Run(ctx,
+		chromedp.WaitReady(clickSelector, chromedp.BySearch),
+		chromedp.Click(clickSelector, chromedp.BySearch),
+		chromedp.Sleep(5*time.Second),
+		chromedp.Location(&productPageURL),
+	)
+	if err != nil {
+		return "", err
+	}
+
+	// 返回点击后的页面URL
+	logs.Info("productPageURL: %s", productPageURL)
+	return productPageURL, nil
+}
+
+// extractHrefAndText 从 OuterHTML 提取 href 和文本内容的辅助函数
+func extractHrefAndText(outerHTML string) (string, string) {
+	// 使用正则表达式或其他字符串处理方法提取 href 和文本内容
+	// 这里只是一个简单的例子,具体实现需要根据 HTML 结构来调整
+	hrefRegex := `href="([^"]+)"`
+	textRegex := `>([^<]+)<`
+
+	hrefMatches := regexp.MustCompile(hrefRegex).FindStringSubmatch(outerHTML)
+	textMatches := regexp.MustCompile(textRegex).FindStringSubmatch(outerHTML)
+
+	href := ""
+	linkText := ""
+	if len(hrefMatches) > 1 {
+		href = hrefMatches[1]
+	}
+	if len(textMatches) > 1 {
+		linkText = textMatches[1]
+	}
+
+	return href, linkText
+}
+
+// Extract report URLs from the HTML content
+func extractReportURLs(htmlContent, keyword string) map[string]string {
+	//var reportURLs []string
+	var reportURLMap = make(map[string]string)
+	var reportURL string
+
+	// Find all occurrences of the keyword and extract report URLs
+	content := htmlContent
+	for {
+		startIdx := strings.Index(content, keyword)
+		if startIdx == -1 {
+			break
+		}
+		startIdx += len(keyword)
+
+		// Extract the URL from the HTML content
+		urlStartIdx := strings.LastIndex(content[:startIdx], `href="`) + len(`href="`)
+		urlEndIdx := strings.Index(content[urlStartIdx:], `"`) + urlStartIdx
+		if urlStartIdx > 0 && urlEndIdx > urlStartIdx {
+			reportURL = content[urlStartIdx:urlEndIdx]
+			//reportURLs = append(reportURLs, content[urlStartIdx:urlEndIdx])
+		}
+
+		content = content[startIdx:]
+
+		// Now extract the content inside the first <div class="short_right">
+		divStartIdx := strings.Index(content, `<div class="short_right">`)
+		if divStartIdx != -1 {
+			divStartIdx += len(`<div class="short_right">`)
+			divEndIdx := strings.Index(content[divStartIdx:], `</div>`) + divStartIdx
+			if divEndIdx > divStartIdx {
+				shortRightContent := content[divStartIdx:divEndIdx]
+
+				// Extract the first <div> content inside <div class="short_right">
+				innerDivStartIdx := strings.Index(shortRightContent, `<div>`)
+				if innerDivStartIdx != -1 {
+					innerDivStartIdx += len(`<div>`)
+					//innerDivEndIdx := strings.Index(shortRightContent[innerDivStartIdx:], `</div>`) + innerDivStartIdx
+					innerDivContent := shortRightContent[innerDivStartIdx:]
+					fmt.Println("Inner Div Content:", innerDivContent)
+					reportURLMap[reportURL] = innerDivContent
+				}
+			}
+		}
+	}
+
+	return reportURLMap
+}
+
+// Process the report data
+func processReport(ctx context.Context, product string, category string, reportURL string, keywords []string) error {
+	// Navigate to the report page
+	var reportContent string
+	err := chromedp.Run(ctx,
+		chromedp.Navigate(lyLoginPath+reportURL),
+		chromedp.WaitVisible("body", chromedp.ByQuery), // 等待 body 元素可见,确保页面已加载
+		chromedp.Sleep(5*time.Second),                  // 等待额外时间,以确保动态内容加载
+		chromedp.OuterHTML("html", &reportContent),     // 获取页面 HTML 内容
+	)
+	if err != nil {
+		return err
+	}
+
+	// 如果文章内容包含 “您看文章的速度太快了,歇一会再看吧” 则返回指定错误
+	if strings.Contains(reportContent, "您看文章的速度太快了,歇一会再看吧") {
+		return fmt.Errorf("您看文章的速度太快了,歇一会再看吧")
+	}
+
+	var lyIndexDataList []models.BaseFromLyData
+	// Process the data based on keywords
+	for _, keyword := range keywords {
+		partialKeyword := strings.Split(keyword, ":")
+		// Select appropriate processor based on product and category
+		processor, err := GetProcessor(product, category)
+		if err != nil {
+			return err
+		}
+
+		// 查询报告所属分类
+		classify, err := models.GetLyClassifyByName(product)
+		if err != nil {
+			return err
+		}
+
+		// Process the report content using the selected processor
+		baseFromLyDataList, err := processor.Process(ctx, product, reportContent, partialKeyword, classify.BaseFromLyClassifyId)
+		if err != nil {
+			return err
+		}
+		if len(baseFromLyDataList) > 0 {
+			for _, baseFromLyData := range baseFromLyDataList {
+				if baseFromLyData.DataTime != "" && baseFromLyData.IndexCode != "" && baseFromLyData.IndexCode != "lysww" {
+					baseFromLyData.CreateTime = utils.GetCurrentTime()
+					baseFromLyData.ModifyTime = utils.GetCurrentTime()
+					lyIndexDataList = append(lyIndexDataList, baseFromLyData)
+				}
+			}
+		}
+
+	}
+	// 新增指标数据
+	if len(lyIndexDataList) > 0 {
+		err = models.AddLyDataList(lyIndexDataList)
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func addLyIndex(classifyId int, indexCode string, indexName string, unit string, frequency string) (int, error) {
+	// 添加指标
+	index := &models.BaseFromLyIndex{
+		CreateTime:           utils.GetCurrentTime(),
+		ModifyTime:           utils.GetCurrentTime(),
+		BaseFromLyClassifyId: classifyId,
+		IndexCode:            indexCode,
+		IndexName:            indexName,
+		Frequency:            frequency,
+		Unit:                 unit,
+		EdbExist:             0,
+	}
+	indexId, err := models.AddLyIndex(index)
+	if err != nil {
+		return 0, err
+	}
+	return int(indexId), nil
+}

+ 2257 - 0
services/liangyou/processor_business_logic.go

@@ -0,0 +1,2257 @@
+// 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"
+)
+
+var (
+	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("ImportCostProcessor 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("ImportCostProcessor 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("ImportCostProcessor 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("ImportCostProcessor Process() : Failed to get index id: %v", err)
+							continue
+						}
+
+						indexData, err := models.GetLyDataByIndexIdAndDataTime(indexId, format)
+						if err != nil {
+							logs.Error("ImportCostProcessor Process() : Failed to get data by index id and date: %v", err)
+							continue
+						}
+						if len(indexData) > 0 {
+							logs.Info("ImportCostProcessor 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("ImportCostProcessor 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("PurchaseShippingProcessor Process() : keywords must contain at least 3 elements")
+	}
+
+	// 拿到 行关键字和列关键字
+	columnName := keywords[len(keywords)-3]
+
+	// 提取所有表格数据
+	tableData := getPurchaseShippingTableData(reportContent)
+	logs.Info("PurchaseShippingProcessor 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("PurchaseShippingProcessor 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("PurchaseShippingProcessor 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("PurchaseShippingProcessor 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("PurchaseShippingProcessor Process() : Failed to get data by index id and date: %v", err)
+						continue
+					}
+					if len(indexData) > 0 {
+						if indexData[0].Value != value {
+							logs.Info("PurchaseShippingProcessor 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("PurchaseShippingProcessor 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
+			}
+		}
+	}
+
+	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("InventoryAnalysisProcessor Process() : keywords must contain at least 4 elements")
+	}
+
+	// 拿到 行关键字和列关键字
+	columnName := keywords[0]
+	rowVariety := keywords[1]
+
+	columnSuffix := "本周"
+	columnName = columnName + columnSuffix
+
+	// 提取所有表格数据
+	tableData := getTableData(reportContent, true)
+	logs.Info("InventoryAnalysisProcessor 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("InventoryAnalysisProcessor 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("InventoryAnalysisProcessor Process() : Failed to get index id: %v", err)
+						continue
+					}
+
+					indexData, err := models.GetLyDataByIndexIdAndDataTime(indexId, format)
+					if err != nil {
+						logs.Error("InventoryAnalysisProcessor Process() : Failed to get data by index id and date: %v", err)
+						continue
+					}
+					if len(indexData) > 0 {
+						logs.Info("InventoryAnalysisProcessor 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("InventoryAnalysisProcessor 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("InventoryAnalysisProcessor 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("PriceSpreadArbitrageProcessor 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("PriceSpreadArbitrageProcessor 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("PriceSpreadArbitrageProcessor Process() : Failed to get index id: %v", err)
+						continue
+					}
+
+					indexData, err := models.GetLyDataByIndexIdAndDataTime(indexId, format)
+					if err != nil {
+						logs.Error("PriceSpreadArbitrageProcessor Process() : Failed to get data by index id and date: %v", err)
+						continue
+					}
+					if len(indexData) > 0 {
+						logs.Info("PriceSpreadArbitrageProcessor 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("PriceSpreadArbitrageProcessor Process() : Column index out of range for row '%s', '%s'", rowVariety, columnDate)
+				}
+				break
+			}
+
+		}
+	}
+
+	return result, nil
+}
+
+// DailyTransactionProcessor
+// @Description: 每日成交处理器
+type DailyTransactionProcessor struct{}
+
+func (p *DailyTransactionProcessor) Process(ctx context.Context, product string, reportContent string, keywords []string, classifyId int) ([]models.BaseFromLyData, error) {
+	fmt.Println("Processing processing profit...")
+	// 解析关键字
+	if len(keywords) < 4 {
+		return []models.BaseFromLyData{}, fmt.Errorf("DailyTransactionProcessor Process() : keywords must contain at least 4 elements")
+	}
+
+	// 获取第一个表格
+	areaTableDataList := getNoHeadTableData(reportContent)
+	if len(areaTableDataList) == 0 {
+		return []models.BaseFromLyData{}, fmt.Errorf("DailyTransactionProcessor Process() : No table data found")
+	}
+	areaTableData := areaTableDataList[0]
+	// 获取第二个表格
+	blocTableData := getTableData(reportContent, false)
+	if blocTableData.Headers == nil {
+		return []models.BaseFromLyData{}, fmt.Errorf("DailyTransactionProcessor Process() : No table data found")
+
+	}
+	logs.Info("SupplyDemandBalanceProcessor Process() : areaTableData data: %v, blocTableData data: %v", areaTableData, blocTableData)
+	// 提取日期信息
+	dateText, err := getDateInfo(ctx)
+	if err != nil {
+		return []models.BaseFromLyData{}, err
+	}
+
+	// 时间格式转换
+	format, err := utils.ConvertTimeFormat(dateText)
+	if err != nil {
+		return []models.BaseFromLyData{}, err
+	}
+
+	// 处理提取的表格数据
+	var result []models.BaseFromLyData
+
+	areaHeaders := areaTableData.Headers
+	areaRows := areaTableData.Rows
+
+	// 第一个表格
+	// 拿到 行关键字和列关键字
+	columnArea := keywords[1]
+	var rowAreaMonthDays []string
+	rowWeek := "地区总计"
+
+	monthDay, err := utils.GetWeekdaysInSameWeek(format)
+	if err != nil {
+		return nil, err
+	}
+	rowAreaMonthDays = monthDay
+
+	// 查找目标列
+	areaColumnIdx := -1
+	for i, header := range areaHeaders {
+		if strings.Contains(header, columnArea) {
+			areaColumnIdx = i
+			break
+		}
+	}
+	if areaColumnIdx == -1 {
+		log.Printf("DailyTransactionProcessor Process() : One Column '%s' not found in table", columnArea)
+	} else if !strings.Contains(strings.Join(keywords[:len(keywords)-3], ":"), "主要集团") {
+		for _, row := range areaRows {
+			if len(row) >= len(areaHeaders) && row[0] == rowWeek {
+				if areaColumnIdx < len(row) {
+					// 指标名称
+					indexName := strings.Join(keywords[:len(keywords)-3], ":")
+					// 指标编码
+					indexCode := utils.GenerateIndexCode(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("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[blocColumnIdx]
+					value, err := strconv.ParseFloat(valueStr, 64)
+					if err != nil {
+						return []models.BaseFromLyData{}, fmt.Errorf("DailyTransactionProcessor 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("DailyTransactionProcessor 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("ImportExportAnalysisProcessor 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("ImportExportAnalysisProcessor 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("ImportExportAnalysisProcessor 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("ImportExportAnalysisProcessor Process() : Failed to get data by index id and date: %v", err)
+							continue
+						}
+						if len(indexData) > 0 {
+							logs.Info("ImportExportAnalysisProcessor 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("ImportExportAnalysisProcessor Process() : Column index out of range for row '%s'", columnDate)
+					}
+					break
+				}
+			}
+		}
+	}
+
+	return result, nil
+}
+
+// ExtractValueInParentheses 从字符串中提取括号中的值
+func extractValueInParentheses(input string) (string, error) {
+	re := regexp.MustCompile(`(([^)]+))`)
+	matches := re.FindStringSubmatch(input)
+
+	if len(matches) > 1 {
+		return matches[1], nil
+	}
+
+	return "", fmt.Errorf("no value found in parentheses")
+}
+
+// 获取指标id,根据指标名称判断,没有插入指标生成返回
+func getIndexId(indexCode string, indexName string, classifyId int, sourceName string, frequency string, unit string) (int, error) {
+	if indexCode == "lysww" {
+		return 0, fmt.Errorf("indexCode is error")
+	}
+
+	// 判断指标是否存在
+	var indexId int
+	indexInfo, err := models.GetLyIndexByCode(indexCode)
+	if err != nil {
+		return indexId, err
+	}
+	if indexInfo == nil {
+		// 新增指标
+		index, err := addLyIndex(classifyId, indexCode, indexName, frequency, unit)
+		if err != nil {
+			return 0, err
+		}
+		indexId = index
+	} else {
+		indexId = indexInfo.BaseFromLyIndexId
+	}
+	return indexId, nil
+}
+
+// 获取页面时间信息
+func getDateInfo(ctx context.Context) (string, error) {
+	var dateText string
+	err := chromedp.Run(ctx,
+		chromedp.Evaluate(`document.querySelector('div.a_date span').innerText`, &dateText),
+	)
+	if err != nil {
+		return "", fmt.Errorf("processing Process() : Failed to extract report date: %v", err)
+	}
+
+	logs.Info("Processing Process() : Report Extracted Date: %s", dateText)
+	return dateText, nil
+}
+
+// 获取所有表格数据 获取表格中有thead标签的数据
+func getAllTableData(reportContent string) []TableData {
+	var tableData []TableData
+
+	doc, err := goquery.NewDocumentFromReader(strings.NewReader(reportContent))
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	// 选择 id 为 "a_content" 的 div
+	doc.Find("#a_content").Each(func(index int, item *goquery.Selection) {
+		item.Find("table").Each(func(index int, table *goquery.Selection) {
+			var headers []string
+			var rows [][]string
+
+			// 提取表头
+			table.Find("thead th").Each(func(index int, th *goquery.Selection) {
+				headers = append(headers, th.Text())
+			})
+
+			// 提取表格行数据
+			table.Find("tbody tr").Each(func(index int, row *goquery.Selection) {
+				var rowData []string
+				row.Find("td").Each(func(index int, td *goquery.Selection) {
+					rowData = append(rowData, td.Text())
+				})
+				rows = append(rows, rowData)
+			})
+
+			// 仅在表头存在时添加到结果中
+			if len(headers) > 0 {
+				tableData = append(tableData, TableData{
+					Headers: headers,
+					Rows:    rows,
+				})
+			}
+		})
+	})
+	return tableData
+}
+
+// 获取无头表格数据
+func getNoHeadTableData(reportContent string) []TableData {
+	var tableData []TableData
+
+	doc, err := goquery.NewDocumentFromReader(strings.NewReader(reportContent))
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	// Find the div with id "a_content"
+	doc.Find("#a_content").Each(func(index int, div *goquery.Selection) {
+		// Find all tables within the div
+		div.Find("table").Each(func(index int, table *goquery.Selection) {
+			var headers []string
+			var rows [][]string
+
+			// Extract table headers if any
+			table.Find("tr").Each(func(index int, tr *goquery.Selection) {
+				var rowData []string
+				tr.Find("td, th").Each(func(index int, cell *goquery.Selection) {
+					cellText := cell.Text()
+					rowData = append(rowData, cellText)
+				})
+
+				if index == 0 && len(rowData) > 0 {
+					// The first row is treated as the header row
+					headers = rowData
+				} else if len(rowData) > 0 {
+					// Add the row data to the rows slice
+					rows = append(rows, rowData)
+				}
+			})
+
+			// Only add table data if headers are present
+			if len(headers) > 0 {
+				tableData = append(tableData, TableData{
+					Headers: headers,
+					Rows:    rows,
+				})
+			}
+		})
+	})
+
+	return tableData
+}
+
+// 获取表格数据 获取id 为 a_content 的 div 中的第一个表格 左上角那个单元格会拼在第一个,会拼上列上的合并单元格
+func getTableData(reportContent string, isFirst bool) TableData {
+	doc, err := goquery.NewDocumentFromReader(strings.NewReader(reportContent))
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	tableData := &TableData{}
+
+	// 只提取 id 为 a_content 的 div 中的第一个表格
+	var firstTable *goquery.Selection
+	if isFirst {
+		firstTable = doc.Find("div#a_content table").First()
+	} else {
+		firstTable = doc.Find("div#a_content table").Last()
+	}
+
+	var combinedHeaders []string
+
+	// 提取表头
+	firstTable.Find("tr").Each(func(i int, row *goquery.Selection) {
+		if i == 0 {
+			// 第一行处理合并单元格,保存到 combinedHeaders
+			row.Find("td,th").Each(func(j int, cell *goquery.Selection) {
+				if j == 0 {
+					// 把左上角的“年度(10/9月)”放入 Headers 第一个元素
+					tableData.Headers = append(tableData.Headers, strings.TrimSpace(cell.Text()))
+				} else {
+					// 处理其他单元格
+					colspan, exists := cell.Attr("colspan")
+					if exists {
+						spanCount := 0
+						fmt.Sscanf(colspan, "%d", &spanCount)
+						for k := 0; k < spanCount; k++ {
+							combinedHeaders = append(combinedHeaders, strings.TrimSpace(cell.Text()))
+						}
+					} else {
+						combinedHeaders = append(combinedHeaders, strings.TrimSpace(cell.Text()))
+					}
+				}
+			})
+		} else if i == 1 {
+			// 第二行处理具体标题,组合后保存到 Headers
+			row.Find("td,th").Each(func(j int, cell *goquery.Selection) {
+				if j < len(combinedHeaders) {
+					fullHeader := combinedHeaders[j] + strings.TrimSpace(cell.Text())
+					tableData.Headers = append(tableData.Headers, fullHeader)
+				}
+			})
+		} else {
+			// 处理数据行
+			var rowData []string
+			row.Find("td").Each(func(j int, cell *goquery.Selection) {
+				rowData = append(rowData, strings.TrimSpace(cell.Text()))
+			})
+			if len(rowData) > 0 {
+				tableData.Rows = append(tableData.Rows, rowData)
+			}
+		}
+	})
+
+	return *tableData
+}
+
+// 获取采购装船表格数据
+func getPurchaseShippingTableData(reportContent string) TableData {
+	doc, err := goquery.NewDocumentFromReader(strings.NewReader(reportContent))
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	var tableData TableData
+
+	// 只提取 id 为 a_content 的 div 中的第一个表格
+	firstTable := doc.Find("div#a_content table").First()
+
+	// 提取表头
+	var headers []string
+	var subHeaders []string
+
+	firstTable.Find("thead tr").Each(func(i int, row *goquery.Selection) {
+		row.Find("th").Each(func(j int, cell *goquery.Selection) {
+			headerText := strings.TrimSpace(cell.Text())
+
+			if i == 0 {
+				// 处理第一行表头
+				colspan, exists := cell.Attr("colspan")
+				if exists {
+					spanCount := 0
+					fmt.Sscanf(colspan, "%d", &spanCount)
+					for k := 0; k < spanCount; k++ {
+						headers = append(headers, headerText)
+					}
+				} else {
+					headers = append(headers, headerText)
+				}
+			} else if i == 1 {
+				// 处理第二行表头
+				subHeaders = append(subHeaders, headerText)
+			}
+		})
+	})
+
+	// 合并第一行和第二行表头信息
+	if len(subHeaders) > 0 {
+		for i := 0; i < len(subHeaders); i++ {
+			// 从第四个单元格开始拼接
+			headers[3+i] = headers[3+i] + subHeaders[i]
+		}
+	}
+
+	tableData.Headers = headers
+
+	// 处理数据行
+	firstTable.Find("tbody tr").Each(func(i int, row *goquery.Selection) {
+		var rowData []string
+		row.Find("td").Each(func(j int, cell *goquery.Selection) {
+			rowData = append(rowData, strings.TrimSpace(cell.Text()))
+		})
+		if len(rowData) > 0 {
+			tableData.Rows = append(tableData.Rows, rowData)
+		}
+	})
+
+	return tableData
+}
+
+// 判断字符串是否是数字
+func isNumeric(value string) bool {
+	// 正则表达式匹配整数和浮点数
+	re := regexp.MustCompile(`^[+-]?(\d+(\.\d*)?|\.\d+)$`)
+	return re.MatchString(value)
+}
+
+// 只保留汉字
+func extractChinese(text string) string {
+	re := regexp.MustCompile(`[^\p{Han}]`) // 匹配非汉字字符
+	return re.ReplaceAllString(text, "")
+}
+
+// 去除括号中的内容 包含括号 ()
+func removeParentheses(text string) string {
+	re := regexp.MustCompile(`\([^)]*\)`)
+	return re.ReplaceAllString(text, "")
+}
+
+// IsChinese 判断传入的是否是汉字
+func IsChinese(str string) bool {
+	for _, r := range str {
+		if unicode.Is(unicode.Han, r) {
+			return true
+		}
+	}
+	return false
+}
+
+// 判断是否是数字
+func isNumber(str string) bool {
+	_, err := strconv.ParseFloat(str, 64)
+	return err == nil
+}

+ 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 - 0
services/task.go

@@ -2,6 +2,7 @@ package services
 
 import (
 	"context"
+	"eta/eta_crawler/services/liangyou"
 	"eta/eta_crawler/services/sci99"
 	"eta/eta_crawler/utils"
 	"fmt"
@@ -54,6 +55,13 @@ func Task() {
 		task.AddTask("UN月度数据", syncYearMonthComTrade) //每月1号的3点同步
 
 		task.AddTask("居民消费价格指数", crawlerIcpi) //每月1号的3点同步
+
+	}
+
+	if utils.LY_OPEN == "1" {
+		lyData := task.NewTask("refreshData", "0 0 12 * * *", liangyou.LyDataDeal) // 粮油商务网
+
+		task.AddTask("粮油商务网", lyData)
 	}
 
 	if utils.BusinessCode == utils.BusinessCodeFuBang {

+ 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:吨:周度",
+        "主要集团:营口嘉里:菜籽油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:富之源:菜籽油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:防城大海:菜籽油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:防城澳加:菜籽油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:成都中粮:菜籽油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:其它:菜籽油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:总计:菜籽油现货成交量:本周(吨)现货成交:吨:周度",
+        "主要集团:营口嘉里:菜籽油基差成交量:本周(吨)基差成交:吨:周度",
+        "主要集团:富之源:菜籽油基差成交量:本周(吨)基差成交:吨:周度",
+        "主要集团:防城大海:菜籽油基差成交量:本周(吨)基差成交:吨:周度",
+        "主要集团:防城澳加:菜籽油基差成交量:本周(吨)基差成交:吨:周度",
+        "主要集团:成都中粮:菜籽油基差成交量:本周(吨)基差成交:吨:周度",
+        "主要集团:其它:菜籽油基差成交量:本周(吨)基差成交:吨:周度",
+        "主要集团:总计:菜籽油基差成交量:本周(吨)基差成交:吨:周度"
+      ]
+    }
+  },
+  "葵花粕": {
+    "进口预估": {
+      "进口葵花粕月度进口量预估": [
+        "进口葵花粕月度进口量预估:本年进口量:万吨:周度",
+        "进口葵花粕月度进口量预估:本年海关进口量:万吨:周度"
+      ]
+    }
+  }
+}

+ 17 - 0
utils/config.go

@@ -34,6 +34,14 @@ var (
 	IS_INIT_SCI99     string
 )
 
+// 粮油商务网
+var (
+	LY_USERNAME  string
+	LY_PASSWORD  string
+	LY_JSON_PATH string
+	LY_OPEN      string
+)
+
 var (
 	EDB_LIB_URL         string
 	APP_EDB_LIB_NAME_EN string
@@ -109,8 +117,17 @@ func init() {
 		logMaxDaysStr := config["log_max_day"]
 		LogMaxDays, _ = strconv.Atoi(logMaxDaysStr)
 	}
+
 	PYTHON_PATH = config["python_path"]
 	if PYTHON_PATH == "" {
 		PYTHON_PATH = "python3"
 	}
+
+	{
+		LY_USERNAME = config["ly_username"]
+		LY_PASSWORD = config["ly_password"]
+		LY_JSON_PATH = config["ly_json_path"]
+		LY_OPEN = config["ly_open"]
+
+	}
 }

+ 488 - 0
utils/date_util.go

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

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