From 226224162f607ffc8b42942824256e0a0285d851 Mon Sep 17 00:00:00 2001 From: lily Date: Thu, 17 Sep 2026 18:49:22 +0800 Subject: [PATCH] =?UTF-8?q?add:=E8=84=9A=E6=9C=AC=E5=88=9D=E5=A7=8B?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 17 + cmd/check_sheet_permission/main.go | 49 ++ common_util/feishu_util/client.go | 56 ++ common_util/feishu_util/defaults.go | 7 + common_util/feishu_util/sheet.go | 236 +++++++ common_util/feishu_util/url.go | 51 ++ common_util/feishu_util/url_test.go | 25 + common_util/feishu_util/wiki.go | 83 +++ config/config.example.yaml | 8 + config/load.go | 91 +++ go.mod | 8 + go.sum | 6 + .../check_agentic_project_name/README.md | 61 ++ .../check_agentic_project_name/main.go | 585 ++++++++++++++++++ ...–问题汇总管理表 - 9月各产业优化问题汇总表.xlsx | Bin 0 -> 16769 bytes 15 files changed, 1283 insertions(+) create mode 100644 .gitignore create mode 100644 cmd/check_sheet_permission/main.go create mode 100644 common_util/feishu_util/client.go create mode 100644 common_util/feishu_util/defaults.go create mode 100644 common_util/feishu_util/sheet.go create mode 100644 common_util/feishu_util/url.go create mode 100644 common_util/feishu_util/url_test.go create mode 100644 common_util/feishu_util/wiki.go create mode 100644 config/config.example.yaml create mode 100644 config/load.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 haoxian_hisense/agentic_project/check_agentic_project_name/README.md create mode 100644 haoxian_hisense/agentic_project/check_agentic_project_name/main.go create mode 100644 haoxian_hisense/local/月度优化问题汇总管理表 - 9月各产业优化问题汇总表.xlsx diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7e77241 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +# local secrets / env config +config/config.dev.yaml + +# generated DML dumps +haoxian_hisense/agentic_project/check_agentic_project_name/dml_*.sql + +# Go +*.exe +*.test +*.out +vendor/ + +# OS / IDE +.DS_Store +.idea/ +.vscode/ +*.swp diff --git a/cmd/check_sheet_permission/main.go b/cmd/check_sheet_permission/main.go new file mode 100644 index 0000000..82070a0 --- /dev/null +++ b/cmd/check_sheet_permission/main.go @@ -0,0 +1,49 @@ +package main + +import ( + "context" + "fmt" + "os" + + "ai_script/common_util/feishu_util" +) + +func main() { + client, err := feishu_util.DefaultClient() + if err != nil { + fail("加载配置失败", err) + } + + url := feishu_util.DefaultOptimizationIssueSheetURL + title := feishu_util.DefaultOptimizationIssueSheetTitle + if len(os.Args) > 1 { + url = os.Args[1] + } + if len(os.Args) > 2 { + title = os.Args[2] + } + + ctx := context.Background() + meta, token, err := client.CheckReadPermission(ctx, url, title) + if err != nil { + fail("读取权限校验失败", err) + } + fmt.Printf("OK: 具备读取权限\n") + fmt.Printf(" spreadsheet_token: %s\n", token) + fmt.Printf(" sheet_id: %s\n", meta.SheetID) + fmt.Printf(" sheet_title: %s\n", meta.Title) + + values, err := client.ReadSpreadsheetURL(ctx, url, title) + if err != nil { + fail("读取表格失败", err) + } + fmt.Printf(" rows: %d\n", len(values.Values)) + if len(values.Values) > 0 { + fmt.Printf(" first_row_cols: %d\n", len(values.Values[0])) + } +} + +func fail(msg string, err error) { + fmt.Fprintf(os.Stderr, "%s: %v\n", msg, err) + os.Exit(1) +} diff --git a/common_util/feishu_util/client.go b/common_util/feishu_util/client.go new file mode 100644 index 0000000..1489e91 --- /dev/null +++ b/common_util/feishu_util/client.go @@ -0,0 +1,56 @@ +package feishu_util + +import ( + "fmt" + "sync" + + "ai_script/config" + + lark "github.com/larksuite/oapi-sdk-go/v3" +) + +var ( + clientOnce sync.Once + clientInst *Client + clientErr error +) + +// Client 封装飞书 OpenAPI 客户端与应用凭证。 +type Client struct { + AppID string + AppSecret string + Lark *lark.Client +} + +// NewClient 使用显式凭证创建客户端。 +func NewClient(appID, appSecret string) *Client { + return &Client{ + AppID: appID, + AppSecret: appSecret, + Lark: lark.NewClient(appID, appSecret), + } +} + +// NewClientFromConfig 从项目配置创建客户端。 +func NewClientFromConfig(cfg *config.Config) (*Client, error) { + if cfg == nil { + return nil, fmt.Errorf("config 为空") + } + if cfg.Feishu.AppID == "" || cfg.Feishu.AppSecret == "" { + return nil, fmt.Errorf("配置缺少 feishu.app_id / feishu.app_secret") + } + return NewClient(cfg.Feishu.AppID, cfg.Feishu.AppSecret), nil +} + +// DefaultClient 懒加载:读取本地 config/config.{APP_ENV}.yaml 并创建单例客户端。 +func DefaultClient() (*Client, error) { + clientOnce.Do(func() { + cfg, err := config.Load() + if err != nil { + clientErr = err + return + } + clientInst, clientErr = NewClientFromConfig(cfg) + }) + return clientInst, clientErr +} diff --git a/common_util/feishu_util/defaults.go b/common_util/feishu_util/defaults.go new file mode 100644 index 0000000..3809f8a --- /dev/null +++ b/common_util/feishu_util/defaults.go @@ -0,0 +1,7 @@ +package feishu_util + +// 月度优化问题汇总管理表(知识库挂载的电子表格) +const ( + DefaultOptimizationIssueSheetURL = "https://mi8elatzm1x.feishu.cn/wiki/IbCUwR4bvil4E1k9k2LcCiwSn8t?sheet=QyDfOp" + DefaultOptimizationIssueSheetTitle = "9月各产业优化问题汇总表" +) diff --git a/common_util/feishu_util/sheet.go b/common_util/feishu_util/sheet.go new file mode 100644 index 0000000..f7c396e --- /dev/null +++ b/common_util/feishu_util/sheet.go @@ -0,0 +1,236 @@ +package feishu_util + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "strings" + + larkcore "github.com/larksuite/oapi-sdk-go/v3/core" + larksheets "github.com/larksuite/oapi-sdk-go/v3/service/sheets/v3" +) + +// SheetMeta 工作表元信息。 +type SheetMeta struct { + SheetID string + Title string + Index int + RowCount int + ColumnCount int +} + +// SheetValues 读取到的单元格数据。 +type SheetValues struct { + SpreadsheetToken string + SheetID string + Title string + Range string + Values [][]interface{} +} + +// ListSheets 列出电子表格下全部工作表。 +func (c *Client) ListSheets(ctx context.Context, spreadsheetToken string) ([]*SheetMeta, error) { + req := larksheets.NewQuerySpreadsheetSheetReqBuilder(). + SpreadsheetToken(spreadsheetToken). + Build() + resp, err := c.Lark.Sheets.V3.SpreadsheetSheet.Query(ctx, req) + if err != nil { + return nil, fmt.Errorf("查询工作表列表失败: %w", err) + } + if !resp.Success() { + return nil, fmt.Errorf("查询工作表列表失败: code=%d msg=%s log_id=%s detail=%s", + resp.Code, resp.Msg, resp.RequestId(), larkcore.Prettify(resp.CodeError)) + } + if resp.Data == nil { + return nil, nil + } + out := make([]*SheetMeta, 0, len(resp.Data.Sheets)) + for i, s := range resp.Data.Sheets { + meta := &SheetMeta{Index: i} + if s.SheetId != nil { + meta.SheetID = *s.SheetId + } + if s.Title != nil { + meta.Title = *s.Title + } + if s.GridProperties != nil { + if s.GridProperties.RowCount != nil { + meta.RowCount = *s.GridProperties.RowCount + } + if s.GridProperties.ColumnCount != nil { + meta.ColumnCount = *s.GridProperties.ColumnCount + } + } + out = append(out, meta) + } + return out, nil +} + +// FindSheet 按 sheetID 或标题查找工作表;优先 sheetID,其次精确匹配标题。 +func (c *Client) FindSheet(ctx context.Context, spreadsheetToken, sheetID, title string) (*SheetMeta, error) { + sheets, err := c.ListSheets(ctx, spreadsheetToken) + if err != nil { + return nil, err + } + if sheetID != "" { + for _, s := range sheets { + if s.SheetID == sheetID { + return s, nil + } + } + return nil, fmt.Errorf("未找到 sheet_id=%s 的工作表", sheetID) + } + title = strings.TrimSpace(title) + if title != "" { + for _, s := range sheets { + if s.Title == title { + return s, nil + } + } + return nil, fmt.Errorf("未找到标题为 %q 的工作表", title) + } + return nil, fmt.Errorf("请提供 sheetID 或 sheet 标题") +} + +// ReadSheetValues 读取指定工作表范围。 +// cellRange 为空时按工作表实际行列读取整表(飞书 open range 列数上限约 100)。 +func (c *Client) ReadSheetValues(ctx context.Context, spreadsheetToken, sheetID, cellRange string) (*SheetValues, error) { + if spreadsheetToken == "" || sheetID == "" { + return nil, fmt.Errorf("spreadsheetToken / sheetID 不能为空") + } + if cellRange == "" { + var err error + cellRange, err = c.defaultSheetRange(ctx, spreadsheetToken, sheetID) + if err != nil { + return nil, err + } + } + rangeExpr := cellRange + if !strings.Contains(cellRange, "!") { + rangeExpr = sheetID + "!" + cellRange + } + + apiPath := fmt.Sprintf("/open-apis/sheets/v2/spreadsheets/%s/values/%s", spreadsheetToken, rangeExpr) + apiResp, err := c.Lark.Do(ctx, &larkcore.ApiReq{ + HttpMethod: http.MethodGet, + ApiPath: apiPath, + QueryParams: larkcore.QueryParams{"valueRenderOption": []string{"ToString"}}, + SupportedAccessTokenTypes: []larkcore.AccessTokenType{larkcore.AccessTokenTypeTenant}, + }) + if err != nil { + return nil, fmt.Errorf("读取表格失败: %w", err) + } + + var raw struct { + Code int `json:"code"` + Msg string `json:"msg"` + Data struct { + Revision int `json:"revision"` + SpreadsheetToken string `json:"spreadsheetToken"` + ValueRange struct { + Range string `json:"range"` + Values [][]interface{} `json:"values"` + } `json:"valueRange"` + } `json:"data"` + } + if err := json.Unmarshal(apiResp.RawBody, &raw); err != nil { + return nil, fmt.Errorf("解析表格响应失败: %w", err) + } + if raw.Code != 0 { + return nil, fmt.Errorf("读取表格失败: code=%d msg=%s body=%s", raw.Code, raw.Msg, string(apiResp.RawBody)) + } + + return &SheetValues{ + SpreadsheetToken: spreadsheetToken, + SheetID: sheetID, + Range: raw.Data.ValueRange.Range, + Values: raw.Data.ValueRange.Values, + }, nil +} + +// ReadSpreadsheetURL 解析飞书链接并读取指定 sheet(按 URL 中 sheet id 或标题)。 +func (c *Client) ReadSpreadsheetURL(ctx context.Context, rawURL, sheetTitle string) (*SheetValues, error) { + ref, err := ParseSpreadsheetURL(rawURL) + if err != nil { + return nil, err + } + token, err := c.ResolveSpreadsheetToken(ctx, ref) + if err != nil { + return nil, err + } + meta, err := c.FindSheet(ctx, token, ref.SheetID, sheetTitle) + if err != nil { + return nil, err + } + values, err := c.ReadSheetValues(ctx, token, meta.SheetID, "") + if err != nil { + return nil, err + } + values.Title = meta.Title + return values, nil +} + +func (c *Client) defaultSheetRange(ctx context.Context, spreadsheetToken, sheetID string) (string, error) { + sheets, err := c.ListSheets(ctx, spreadsheetToken) + if err != nil { + return "", err + } + rows, cols := 200, 26 + for _, s := range sheets { + if s.SheetID != sheetID { + continue + } + if s.RowCount > 0 { + rows = s.RowCount + } + if s.ColumnCount > 0 { + cols = s.ColumnCount + } + break + } + // 飞书 values 接口对形如 A:Z 的开放范围最多约 100 列 + if cols > 100 { + cols = 100 + } + if rows < 1 { + rows = 1 + } + return fmt.Sprintf("A1:%s%d", colIndexToName(cols), rows), nil +} + +// colIndexToName 将 1-based 列序号转为 Excel 列名(1->A, 26->Z, 27->AA)。 +func colIndexToName(index int) string { + if index < 1 { + index = 1 + } + name := make([]byte, 0, 4) + for index > 0 { + index-- + name = append([]byte{byte('A' + index%26)}, name...) + index /= 26 + } + return string(name) +} + +// CheckReadPermission 验证应用是否具备读取目标飞书表格的权限。 +// 成功时返回可访问的工作表元信息;失败时返回飞书错误详情,便于排查权限/授权。 +func (c *Client) CheckReadPermission(ctx context.Context, rawURL, sheetTitle string) (*SheetMeta, string, error) { + ref, err := ParseSpreadsheetURL(rawURL) + if err != nil { + return nil, "", err + } + token, err := c.ResolveSpreadsheetToken(ctx, ref) + if err != nil { + return nil, "", err + } + meta, err := c.FindSheet(ctx, token, ref.SheetID, sheetTitle) + if err != nil { + return nil, token, err + } + // 再读一小段确认内容读权限(不仅是元信息) + if _, err := c.ReadSheetValues(ctx, token, meta.SheetID, "A1:A1"); err != nil { + return meta, token, fmt.Errorf("可列举工作表,但读取内容失败(请检查 sheets 读权限及文档授权): %w", err) + } + return meta, token, nil +} diff --git a/common_util/feishu_util/url.go b/common_util/feishu_util/url.go new file mode 100644 index 0000000..d82ecfd --- /dev/null +++ b/common_util/feishu_util/url.go @@ -0,0 +1,51 @@ +package feishu_util + +import ( + "fmt" + "net/url" + "strings" +) + +// SpreadsheetRef 解析后的飞书表格定位信息。 +type SpreadsheetRef struct { + // RawURL 原始链接 + RawURL string + // WikiToken 知识库节点 token(URL 中 /wiki/ 后一段);非 wiki 链接为空 + WikiToken string + // SpreadsheetToken 电子表格 token;wiki 链接需再调接口换取 obj_token + SpreadsheetToken string + // SheetID URL 查询参数 sheet= 的值(可选) + SheetID string +} + +// ParseSpreadsheetURL 解析飞书表格/知识库链接。 +// 支持: +// - https://xxx.feishu.cn/wiki/{wikiToken}?sheet={sheetId} +// - https://xxx.feishu.cn/sheets/{spreadsheetToken}?sheet={sheetId} +func ParseSpreadsheetURL(raw string) (*SpreadsheetRef, error) { + raw = strings.TrimSpace(raw) + if raw == "" { + return nil, fmt.Errorf("表格 URL 为空") + } + u, err := url.Parse(raw) + if err != nil { + return nil, fmt.Errorf("解析 URL 失败: %w", err) + } + + ref := &SpreadsheetRef{RawURL: raw, SheetID: u.Query().Get("sheet")} + path := strings.Trim(u.Path, "/") + parts := strings.Split(path, "/") + if len(parts) < 2 { + return nil, fmt.Errorf("无法从 URL 解析 token: %s", raw) + } + + switch parts[0] { + case "wiki": + ref.WikiToken = parts[1] + case "sheets": + ref.SpreadsheetToken = parts[1] + default: + return nil, fmt.Errorf("不支持的飞书链接类型 %q,仅支持 /wiki/ 或 /sheets/", parts[0]) + } + return ref, nil +} diff --git a/common_util/feishu_util/url_test.go b/common_util/feishu_util/url_test.go new file mode 100644 index 0000000..23c0c52 --- /dev/null +++ b/common_util/feishu_util/url_test.go @@ -0,0 +1,25 @@ +package feishu_util + +import "testing" + +func TestParseSpreadsheetURL_Wiki(t *testing.T) { + ref, err := ParseSpreadsheetURL(DefaultOptimizationIssueSheetURL) + if err != nil { + t.Fatal(err) + } + if ref.WikiToken != "IbCUwR4bvil4E1k9k2LcCiwSn8t" { + t.Fatalf("wiki token = %s", ref.WikiToken) + } + if ref.SheetID != "QyDfOp" { + t.Fatalf("sheet id = %s", ref.SheetID) + } +} + +func TestColIndexToName(t *testing.T) { + cases := map[int]string{1: "A", 26: "Z", 27: "AA", 52: "AZ", 53: "BA"} + for n, want := range cases { + if got := colIndexToName(n); got != want { + t.Fatalf("colIndexToName(%d)=%s want %s", n, got, want) + } + } +} diff --git a/common_util/feishu_util/wiki.go b/common_util/feishu_util/wiki.go new file mode 100644 index 0000000..689cd61 --- /dev/null +++ b/common_util/feishu_util/wiki.go @@ -0,0 +1,83 @@ +package feishu_util + +import ( + "context" + "fmt" + + larkcore "github.com/larksuite/oapi-sdk-go/v3/core" + larkwiki "github.com/larksuite/oapi-sdk-go/v3/service/wiki/v2" +) + +// WikiNode 知识库节点解析结果。 +type WikiNode struct { + SpaceID string + NodeToken string + ObjToken string + ObjType string + Title string +} + +// ResolveWikiNode 通过知识库 node_token 获取实际云文档 obj_token。 +// 知识库 URL 中的 token 不是 spreadsheet_token,读写表格内容前必须先转换。 +func (c *Client) ResolveWikiNode(ctx context.Context, wikiToken string) (*WikiNode, error) { + if wikiToken == "" { + return nil, fmt.Errorf("wikiToken 为空") + } + req := larkwiki.NewGetNodeSpaceReqBuilder(). + Token(wikiToken). + Build() + resp, err := c.Lark.Wiki.V2.Space.GetNode(ctx, req) + if err != nil { + return nil, fmt.Errorf("获取知识库节点失败: %w", err) + } + if !resp.Success() { + return nil, fmt.Errorf("获取知识库节点失败: code=%d msg=%s log_id=%s detail=%s", + resp.Code, resp.Msg, resp.RequestId(), larkcore.Prettify(resp.CodeError)) + } + if resp.Data == nil || resp.Data.Node == nil { + return nil, fmt.Errorf("获取知识库节点返回空数据") + } + n := resp.Data.Node + node := &WikiNode{} + if n.SpaceId != nil { + node.SpaceID = *n.SpaceId + } + if n.NodeToken != nil { + node.NodeToken = *n.NodeToken + } + if n.ObjToken != nil { + node.ObjToken = *n.ObjToken + } + if n.ObjType != nil { + node.ObjType = *n.ObjType + } + if n.Title != nil { + node.Title = *n.Title + } + if node.ObjToken == "" { + return nil, fmt.Errorf("知识库节点未返回 obj_token") + } + return node, nil +} + +// ResolveSpreadsheetToken 将 SpreadsheetRef 解析为可调用 Sheets API 的 spreadsheet_token。 +func (c *Client) ResolveSpreadsheetToken(ctx context.Context, ref *SpreadsheetRef) (string, error) { + if ref == nil { + return "", fmt.Errorf("SpreadsheetRef 为空") + } + if ref.SpreadsheetToken != "" { + return ref.SpreadsheetToken, nil + } + if ref.WikiToken == "" { + return "", fmt.Errorf("既无 SpreadsheetToken 也无 WikiToken") + } + node, err := c.ResolveWikiNode(ctx, ref.WikiToken) + if err != nil { + return "", err + } + if node.ObjType != "" && node.ObjType != "sheet" { + return "", fmt.Errorf("知识库节点类型为 %s,不是电子表格(sheet)", node.ObjType) + } + ref.SpreadsheetToken = node.ObjToken + return node.ObjToken, nil +} diff --git a/config/config.example.yaml b/config/config.example.yaml new file mode 100644 index 0000000..3d9fa02 --- /dev/null +++ b/config/config.example.yaml @@ -0,0 +1,8 @@ +# 配置模板(可入库)。复制为 config/config.dev.yaml 后填入真实值。 +# 本地:config/config.dev.yaml(已 gitignore) +feishu: + app_id: "" # 飞书应用 App ID,如 cli_xxx + app_secret: "" # 飞书应用 App Secret,勿提交仓库 +hisense_geo: + base_url: "https://geo-api.hisense.com" + token: "" # 生产环境 x-token,勿提交仓库 diff --git a/config/load.go b/config/load.go new file mode 100644 index 0000000..89bef1c --- /dev/null +++ b/config/load.go @@ -0,0 +1,91 @@ +package config + +import ( + "fmt" + "os" + "path/filepath" + + "gopkg.in/yaml.v3" +) + +// Config 项目根目录 config/config.{env}.yaml(敏感项已 gitignore)。 +type Config struct { + Feishu FeishuConfig `yaml:"feishu"` + HisenseGeo HisenseGeoConfig `yaml:"hisense_geo"` +} + +// FeishuConfig 飞书应用凭证。 +type FeishuConfig struct { + AppID string `yaml:"app_id"` + AppSecret string `yaml:"app_secret"` +} + +// HisenseGeoConfig 海信 GEO 生产环境接口配置。 +type HisenseGeoConfig struct { + BaseURL string `yaml:"base_url"` // 默认 https://geo-api.hisense.com + Token string `yaml:"token"` // x-token,勿提交仓库 +} + +// Load 按 APP_ENV 加载 config 目录下配置,默认 dev。 +// 查找顺序: +// 1. CONFIG_PATH / FEISHU_CONFIG_PATH +// 2. 当前工作目录 ./config/config.{env}.yaml +// 3. 向上查找含 config/ 目录的项目根下的 config/config.{env}.yaml +func Load() (*Config, error) { + env := os.Getenv("APP_ENV") + if env == "" { + env = "dev" + } + name := "config." + env + ".yaml" + rel := filepath.Join("config", name) + + candidates := []string{} + if p := os.Getenv("CONFIG_PATH"); p != "" { + candidates = append(candidates, p) + } + if p := os.Getenv("FEISHU_CONFIG_PATH"); p != "" { + candidates = append(candidates, p) + } + candidates = append(candidates, rel) + if root := FindProjectRoot(); root != "" { + candidates = append(candidates, filepath.Join(root, rel)) + } + + var lastErr error + for _, path := range candidates { + data, err := os.ReadFile(path) + if err != nil { + lastErr = err + continue + } + var cfg Config + if err := yaml.Unmarshal(data, &cfg); err != nil { + return nil, fmt.Errorf("解析配置失败 (%s): %w", path, err) + } + if cfg.HisenseGeo.BaseURL == "" { + cfg.HisenseGeo.BaseURL = "https://geo-api.hisense.com" + } + return &cfg, nil + } + return nil, fmt.Errorf("未找到配置文件 %s(放在 config/ 目录,或设置 CONFIG_PATH): %w", rel, lastErr) +} + +// FindProjectRoot 向上查找包含 config/ 目录的项目根。 +func FindProjectRoot() string { + wd, err := os.Getwd() + if err != nil { + return "" + } + dir := wd + for { + cfgDir := filepath.Join(dir, "config") + if st, err := os.Stat(cfgDir); err == nil && st.IsDir() { + return dir + } + parent := filepath.Dir(dir) + if parent == dir { + return "" + } + dir = parent + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2711790 --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module ai_script + +go 1.24.8 + +require ( + github.com/larksuite/oapi-sdk-go/v3 v3.12.0 + gopkg.in/yaml.v3 v3.0.1 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..5ef658d --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +github.com/larksuite/oapi-sdk-go/v3 v3.12.0 h1:H8NP6YIgfEX0RBhKse25npdeZoiaDv8mrw9nCnCVFRc= +github.com/larksuite/oapi-sdk-go/v3 v3.12.0/go.mod h1:F1nwLfYBSKvD8mS/OFw90Gr+V4RIugPBR44m/ThA5jg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/haoxian_hisense/agentic_project/check_agentic_project_name/README.md b/haoxian_hisense/agentic_project/check_agentic_project_name/README.md new file mode 100644 index 0000000..5b755a4 --- /dev/null +++ b/haoxian_hisense/agentic_project/check_agentic_project_name/README.md @@ -0,0 +1,61 @@ +# check_agentic_project_name + +刷数脚本:检查项目名称、产业/品类、产品名称、漏出词等数据一致性。 + +## 数据源 + +1. 飞书表格「9月各产业优化问题汇总表」 + `https://mi8elatzm1x.feishu.cn/wiki/IbCUwR4bvil4E1k9k2LcCiwSn8t?sheet=QyDfOp` +2. 生产接口 + `GET https://geo-api.hisense.com/api/agentic-project/project?page=1&page_size=999&listed=all` + Header: `x-token` + +## 匹配主键(跟接口保持一致) + +| 飞书 | 接口 | +|------|------| +| 优化问题 | `project_name` | +| 产业(= 品牌 + 品类) | `brand` + `category`(直接拼接) | + +接口侧额外过滤(必须同时满足): + +- `listed = 1` +- `status = 'running'` +- `end_month = '2026-09'` + +匹配规则: + +1. 先按上述过滤筛接口项目 +2. 常规:`产业 == brand+category`(如 `海信中央空调`) +3. 简写兼容:`产业 == brand` 且 `品类 == category`(如 ASKO / 古洛尼) +4. 同名+同产业仍多条时:优先飞书【产品名称】/【特征露出词】与接口一致 + +`品类` ↔ `category` 仅作校验罗列,不阻断主键匹配。 + +## 比对项 + +1. 飞书【优化问题】在接口中找不到 +2. 飞书【产业】无法按规则匹配接口【brand+category】(匹配失败,打印完整对照) +3. 飞书【产业】与接口【brand+category】字面不一致(主键已匹配,如简写 `ASKO` vs `ASKO全品类`) +4. 飞书【品类】与接口【category】不一致(主键已匹配) +5. 飞书【产品名称】与接口 `product_name` 不一致 → 输出更新 SQL +6. 飞书【特征露出词】与接口 `exposure_word` 不一致 → 输出更新 SQL + +## 运行 + +```bash +go run ./haoxian_hisense/agentic_project/check_agentic_project_name +``` + +## 配置 + +`config/config.dev.yaml`(已 gitignore): + +```yaml +feishu: + app_id: "" + app_secret: "" +hisense_geo: + base_url: "https://geo-api.hisense.com" + token: "" # x-token,勿提交仓库 +``` diff --git a/haoxian_hisense/agentic_project/check_agentic_project_name/main.go b/haoxian_hisense/agentic_project/check_agentic_project_name/main.go new file mode 100644 index 0000000..655c79a --- /dev/null +++ b/haoxian_hisense/agentic_project/check_agentic_project_name/main.go @@ -0,0 +1,585 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "sort" + "strings" + "time" + "unicode" + + "ai_script/common_util/feishu_util" + "ai_script/config" +) + +func main() { + // 加载项目根目录 config/config.{env}.yaml(含飞书凭证与 GEO Token) + cfg, err := config.Load() + if err != nil { + log.Fatalf("加载配置失败: %v", err) + } + + ctx := context.Background() + + // --------------------------------------------------------------------------- + // 1、拉取飞书表格中「9月各产业优化问题汇总表」sheet 数据 + // https://mi8elatzm1x.feishu.cn/wiki/IbCUwR4bvil4E1k9k2LcCiwSn8t?sheet=QyDfOp + // --------------------------------------------------------------------------- + feishuRows, err := fetchFeishuOptimizationRows(ctx, cfg) + if err != nil { + log.Fatalf("拉取飞书表格失败: %v", err) + } + log.Printf("[1] 飞书表格有效行数: %d", len(feishuRows)) + + // --------------------------------------------------------------------------- + // 2、拉取生产环境项目表数据 + // GET {base_url}/api/agentic-project/project?page=1&page_size=999&listed=all + // Header: x-token + // --------------------------------------------------------------------------- + projects, err := fetchProdProjects(ctx, cfg) + if err != nil { + log.Fatalf("拉取生产环境项目失败: %v", err) + } + log.Printf("[2] 生产环境项目数: %d", len(projects)) + + // 按 project_name 分组,匹配时用 产业 ↔ brand+category(与接口保持一致) + projectsByName := indexProjectsByName(projects) + + // --------------------------------------------------------------------------- + // 3、比对差异点 + // 匹配主键:飞书【优化问题+产业】 ↔ 接口【project_name+(brand+category)】 + // 品类 ↔ category 仅作校验罗列,不阻断主键匹配 + // --------------------------------------------------------------------------- + compareAndLogDiffs(feishuRows, projectsByName) +} + +// feishuRow 飞书「9月各产业优化问题汇总表」解析后的一行。 +type feishuRow struct { + RowNum int // 表格中的物理行号(1-based,含表头) + Industry string // 产业 = 品牌+品类 → 对齐接口 brand+category + Category string // 品类 → 对齐接口 category + ProductName string // 产品名称 + ExposureWord string // 特征露出词 + OptimQuestion string // 优化问题 → 对齐接口 project_name +} + +// prodProject 生产环境 agentic_project 列表项。 +type prodProject struct { + ID string + CompanyID string // company_id + ProjectName string // project_name + Brand string // brand + Category string // category + ProductName string // product_name(数组拼接) + ExposureWord string // exposure_word + Status string // status: running / completed / ... + Listed int // listed + EndMonth string // end_month,如 2026-09 +} + +// 匹配时额外过滤条件(跟业务月度表对齐) +const ( + requiredListed = 1 + requiredStatus = "running" + requiredEndMonth = "2026-09" +) + +// eligibleProject 是否满足 listed/status/end_month 过滤。 +func eligibleProject(p prodProject) bool { + return p.Listed == requiredListed && + strings.EqualFold(p.Status, requiredStatus) && + p.EndMonth == requiredEndMonth +} + +// apiIndustry 接口侧「产业」等价字段:brand + category(无连接符)。 +func (p prodProject) apiIndustry() string { + return p.Brand + p.Category +} + +// industryMatched 飞书产业与接口是否对齐(跟接口保持一致): +// 1) 产业 == brand+category(常规:海信中央空调) +// 2) 产业 == brand 且 品类 == category(简写:ASKO / 古洛尼) +func industryMatched(row feishuRow, api prodProject) bool { + if foldText(row.Industry) == foldText(api.apiIndustry()) { + return true + } + if foldText(row.Industry) == foldText(api.Brand) && foldText(row.Category) == foldText(api.Category) { + return true + } + return false +} + +// findMatchedProject 按优化问题找同名项目,再按产业规则对齐接口。 +// 仅考虑 listed=1 AND status=running AND end_month=2026-09; +// 同名+同产业仍多条时,优先产品名称/露出词一致。 +func findMatchedProject(row feishuRow, projectsByName map[string][]prodProject) (prodProject, bool) { + cands := projectsByName[foldText(row.OptimQuestion)] + matched := make([]prodProject, 0, 2) + for _, api := range cands { + if !eligibleProject(api) { + continue + } + if industryMatched(row, api) { + matched = append(matched, api) + } + } + if len(matched) == 0 { + return prodProject{}, false + } + if len(matched) == 1 { + return matched[0], true + } + + best := matched[0] + bestScore := matchScore(row, best) + for _, api := range matched[1:] { + if s := matchScore(row, api); s > bestScore { + best = api + bestScore = s + } + } + ids := make([]string, 0, len(matched)) + for _, m := range matched { + ids = append(ids, m.ID) + } + log.Printf("[匹配消歧] 优化问题=%q 产业=%q 候选id=%v,选用 id=%s score=%d(飞书产品=%q 露出词=%q)", + row.OptimQuestion, row.Industry, ids, best.ID, bestScore, row.ProductName, row.ExposureWord) + return best, true +} + +// matchScore 同名同产业多候选时的打分(越高越优先)。 +func matchScore(row feishuRow, api prodProject) int { + score := 0 + if sameDelimitedText(row.ProductName, api.ProductName) { + score += 100 + } + if sameDelimitedText(row.ExposureWord, api.ExposureWord) { + score += 50 + } + return score +} + +// fetchFeishuOptimizationRows 步骤1:读取飞书 sheet 并解析为结构化行。 +func fetchFeishuOptimizationRows(ctx context.Context, cfg *config.Config) ([]feishuRow, error) { + client, err := feishu_util.NewClientFromConfig(cfg) + if err != nil { + return nil, err + } + values, err := client.ReadSpreadsheetURL(ctx, + feishu_util.DefaultOptimizationIssueSheetURL, + feishu_util.DefaultOptimizationIssueSheetTitle, + ) + if err != nil { + return nil, err + } + return parseFeishuSheet(values.Values) +} + +// parseFeishuSheet 根据表头定位列并解析数据行。 +func parseFeishuSheet(values [][]interface{}) ([]feishuRow, error) { + headerIdx := -1 + colIndustry, colCategory := -1, -1 + colProduct, colExposure, colQuestion := -1, -1, -1 + + for i, row := range values { + cells := rowToStrings(row) + for j, cell := range cells { + switch strings.TrimSpace(cell) { + case "产业": + colIndustry = j + case "品类": + colCategory = j + case "产品名称": + colProduct = j + case "特征露出词": + colExposure = j + case "优化问题": + colQuestion = j + } + } + if colIndustry >= 0 && colCategory >= 0 && colProduct >= 0 && colExposure >= 0 && colQuestion >= 0 { + headerIdx = i + break + } + colIndustry, colCategory, colProduct, colExposure, colQuestion = -1, -1, -1, -1, -1 + } + if headerIdx < 0 { + return nil, fmt.Errorf("未在飞书表格中找到表头列:产业 / 品类 / 产品名称 / 特征露出词 / 优化问题") + } + + out := make([]feishuRow, 0) + for i := headerIdx + 1; i < len(values); i++ { + cells := rowToStrings(values[i]) + question := cellAt(cells, colQuestion) + if question == "" { + // 空行 / 产业分隔空行跳过 + continue + } + out = append(out, feishuRow{ + RowNum: i + 1, + Industry: cellAt(cells, colIndustry), + Category: cellAt(cells, colCategory), + ProductName: cellAt(cells, colProduct), + ExposureWord: cellAt(cells, colExposure), + OptimQuestion: question, + }) + } + return out, nil +} + +// fetchProdProjects 步骤2:拉取 agentic-project 列表(含 brand/category/exposure_word)。 +func fetchProdProjects(ctx context.Context, cfg *config.Config) ([]prodProject, error) { + if cfg.HisenseGeo.Token == "" { + return nil, fmt.Errorf("配置缺少 hisense_geo.token") + } + base := strings.TrimRight(cfg.HisenseGeo.BaseURL, "/") + url := fmt.Sprintf("%s/api/agentic-project/project?page=1&page_size=999&listed=all", base) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, err + } + req.Header.Set("Accept", "application/json") + req.Header.Set("Content-Type", "application/json; charset=utf-8") + req.Header.Set("Origin", "https://geo.hisense.com") + req.Header.Set("Referer", "https://geo.hisense.com/") + req.Header.Set("x-token", cfg.HisenseGeo.Token) + + httpClient := &http.Client{Timeout: 60 * time.Second} + resp, err := httpClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("agentic-project 接口 HTTP %d: %s", resp.StatusCode, truncate(string(body), 300)) + } + + var raw struct { + Code int `json:"code"` + Message string `json:"message"` + Data struct { + Total int `json:"total"` + List []struct { + ID json.Number `json:"id"` + CompanyID json.Number `json:"company_id"` + ProjectName string `json:"project_name"` + Brand string `json:"brand"` + Category string `json:"category"` + ProductName []string `json:"product_name"` + ExposureWord string `json:"exposure_word"` + Status string `json:"status"` + Listed int `json:"listed"` + EndMonth string `json:"end_month"` + } `json:"list"` + } `json:"data"` + } + if err := json.Unmarshal(body, &raw); err != nil { + return nil, fmt.Errorf("解析 agentic-project 响应失败: %w", err) + } + if raw.Code != 0 { + return nil, fmt.Errorf("agentic-project 接口业务错误 code=%d message=%s", raw.Code, raw.Message) + } + + out := make([]prodProject, 0, len(raw.Data.List)) + nonCompany13 := 0 + eligible := 0 + for _, w := range raw.Data.List { + companyID := w.CompanyID.String() + if companyID == "" { + companyID = "13" + } + if companyID != "13" { + nonCompany13++ + } + p := prodProject{ + ID: w.ID.String(), + CompanyID: companyID, + ProjectName: strings.TrimSpace(w.ProjectName), + Brand: strings.TrimSpace(w.Brand), + Category: strings.TrimSpace(w.Category), + ProductName: joinProductNames(w.ProductName), + ExposureWord: strings.TrimSpace(w.ExposureWord), + Status: strings.TrimSpace(w.Status), + Listed: w.Listed, + EndMonth: strings.TrimSpace(w.EndMonth), + } + if eligibleProject(p) { + eligible++ + } + out = append(out, p) + } + log.Printf("[2] 满足 listed=%d AND status=%s AND end_month=%s 的项目: %d / %d", + requiredListed, requiredStatus, requiredEndMonth, eligible, len(out)) + if nonCompany13 > 0 { + log.Printf("[2] 警告: 存在 company_id != 13 的项目 %d 条", nonCompany13) + } else { + log.Printf("[2] 全部项目 company_id = 13") + } + if raw.Data.Total > len(out) { + log.Printf("[2] 警告: 接口 total=%d,本页仅返回 %d 条,可能未拉全", raw.Data.Total, len(out)) + } + return out, nil +} + +func indexProjectsByName(projects []prodProject) map[string][]prodProject { + m := make(map[string][]prodProject) + for _, p := range projects { + key := foldText(p.ProjectName) + if key == "" { + continue + } + m[key] = append(m[key], p) + } + return m +} + +type industryCategoryMismatch struct { + row feishuRow + apiList []prodProject // 同名 project_name 下的候选项目 +} + +type matchedPair struct { + row feishuRow + api prodProject +} + +// compareAndLogDiffs 按接口语义匹配,并输出差异与更新 SQL。 +func compareAndLogDiffs(rows []feishuRow, projectsByName map[string][]prodProject) { + var ( + missing []feishuRow + fieldMismatch []industryCategoryMismatch + matched []matchedPair + categoryOnly []matchedPair // 主键已匹配,但品类 ≠ category + ) + + var industryLiteralDiff []matchedPair // 主键已匹配,但飞书产业 ≠ brand+category(多为简写兼容) + + for _, row := range rows { + if api, ok := findMatchedProject(row, projectsByName); ok { + matched = append(matched, matchedPair{row: row, api: api}) + if foldText(row.Industry) != foldText(api.apiIndustry()) { + industryLiteralDiff = append(industryLiteralDiff, matchedPair{row: row, api: api}) + } + if foldText(row.Category) != foldText(api.Category) { + categoryOnly = append(categoryOnly, matchedPair{row: row, api: api}) + } + continue + } + cands := projectsByName[foldText(row.OptimQuestion)] + if len(cands) == 0 { + missing = append(missing, row) + continue + } + fieldMismatch = append(fieldMismatch, industryCategoryMismatch{row: row, apiList: cands}) + } + + // ---------- 3.0 主键匹配概况 ---------- + log.Printf("========== 主键匹配概况 ==========") + log.Printf("匹配主键: 飞书【优化问题+产业】 ↔ 接口【project_name+(brand+category)】") + log.Printf("接口过滤: listed=%d AND status=%s AND end_month=%s", requiredListed, requiredStatus, requiredEndMonth) + log.Printf("说明: 飞书【产业】应对齐 brand+category;简写兼容(产业=brand)时会列入「产业字面不一致」") + log.Printf("飞书有效行=%d, 主键匹配=%d, 产业匹配失败=%d, 产业字面不一致(已匹配)=%d, 优化问题缺失=%d, 品类不一致(已匹配)=%d", + len(rows), len(matched), len(fieldMismatch), len(industryLiteralDiff), len(missing), len(categoryOnly)) + if len(missing) == 0 && len(fieldMismatch) == 0 { + log.Printf("结论: 主键(优化问题+产业)全部对应上") + } + + // ---------- 3.1 优化问题在接口中找不到 ---------- + log.Printf("========== 3.1 飞书【优化问题】在接口中找不到(共 %d 条)==========", len(missing)) + for _, row := range missing { + log.Printf("[缺失] 行=%d 优化问题=%q 产业=%q 品类=%q", + row.RowNum, row.OptimQuestion, row.Industry, row.Category) + } + + // ---------- 3.1b 产业匹配失败(同名存在但无法按规则对齐) ---------- + log.Printf("========== 3.1b 飞书【产业】无法匹配接口【brand+category】(共 %d 条)==========", len(fieldMismatch)) + for _, d := range fieldMismatch { + log.Printf("[产业匹配失败] 行=%d 优化问题=%q", d.row.RowNum, d.row.OptimQuestion) + log.Printf(" 飞书: 产业=%q 品类=%q", d.row.Industry, d.row.Category) + for _, api := range d.apiList { + log.Printf(" 接口: id=%s project_name=%q brand=%q category=%q → brand+category=%q product_name=%q exposure_word=%q", + api.ID, api.ProjectName, api.Brand, api.Category, api.apiIndustry(), api.ProductName, api.ExposureWord) + } + } + + // ---------- 3.1b2 主键已匹配,但飞书产业字面 ≠ brand+category ---------- + log.Printf("========== 3.1b2 飞书【产业】与接口【brand+category】字面不一致(主键已匹配,共 %d 条)==========", len(industryLiteralDiff)) + for _, d := range industryLiteralDiff { + log.Printf("[产业字面不一致] 行=%d id=%s 优化问题=%q", d.row.RowNum, d.api.ID, d.row.OptimQuestion) + log.Printf(" 飞书: 产业=%q 品类=%q", d.row.Industry, d.row.Category) + log.Printf(" 接口: brand=%q category=%q → brand+category=%q", + d.api.Brand, d.api.Category, d.api.apiIndustry()) + } + + // ---------- 3.1c 品类不一致(主键已匹配) ---------- + log.Printf("========== 3.1c 飞书【品类】与接口【category】不一致(主键已匹配,共 %d 条)==========", len(categoryOnly)) + for _, d := range categoryOnly { + log.Printf("[品类不一致] 行=%d 优化问题=%q id=%s", d.row.RowNum, d.row.OptimQuestion, d.api.ID) + log.Printf(" 飞书: 产业=%q 品类=%q", d.row.Industry, d.row.Category) + log.Printf(" 接口: brand=%q category=%q → brand+category=%q", + d.api.Brand, d.api.Category, d.api.apiIndustry()) + } + + // ---------- 3.2 产品名称不一致 + 更新 SQL ---------- + var productDiff []matchedPair + for _, m := range matched { + if !sameDelimitedText(m.row.ProductName, m.api.ProductName) { + productDiff = append(productDiff, m) + } + } + log.Printf("========== 3.2 飞书【产品名称】与接口 product_name 不一致(共 %d 条)==========", len(productDiff)) + for _, d := range productDiff { + log.Printf("[产品名称不一致] 行=%d id=%s company_id=%s 优化问题=%q 产业=%q 品类=%q\n 飞书=%q\n 接口=%q", + d.row.RowNum, d.api.ID, d.api.CompanyID, d.row.OptimQuestion, d.row.Industry, d.row.Category, + d.row.ProductName, d.api.ProductName) + } + log.Printf("---------- 3.2 产品名称更新 SQL(共 %d 条)----------", len(productDiff)) + for _, d := range productDiff { + fmt.Println(buildUpdateSQL("product_name", d.row.ProductName, d.api.ID, d.api.CompanyID)) + } + + // ---------- 3.3 特征露出词不一致 + 更新 SQL ---------- + var exposureDiff []matchedPair + for _, m := range matched { + if !sameDelimitedText(m.row.ExposureWord, m.api.ExposureWord) { + exposureDiff = append(exposureDiff, m) + } + } + log.Printf("========== 3.3 飞书【特征露出词】与接口 exposure_word 不一致(共 %d 条)==========", len(exposureDiff)) + for _, d := range exposureDiff { + log.Printf("[特征露出词不一致] 行=%d id=%s company_id=%s 优化问题=%q 产业=%q 品类=%q\n 飞书=%q\n 接口=%q", + d.row.RowNum, d.api.ID, d.api.CompanyID, d.row.OptimQuestion, d.row.Industry, d.row.Category, + d.row.ExposureWord, d.api.ExposureWord) + } + log.Printf("---------- 3.3 特征露出词更新 SQL(共 %d 条)----------", len(exposureDiff)) + for _, d := range exposureDiff { + fmt.Println(buildUpdateSQL("exposure_word", d.row.ExposureWord, d.api.ID, d.api.CompanyID)) + } + + log.Printf("比对完成: 飞书行=%d, 主键匹配=%d, 产业匹配失败=%d, 产业字面不一致=%d, 品类不一致=%d, 优化问题缺失=%d, 产品名称不一致=%d, 特征露出词不一致=%d", + len(rows), len(matched), len(fieldMismatch), len(industryLiteralDiff), len(categoryOnly), len(missing), len(productDiff), len(exposureDiff)) +} + +// buildUpdateSQL 生成以飞书值为准的回写 SQL(带 company_id 条件)。 +func buildUpdateSQL(column, value, id, companyID string) string { + if companyID == "" { + companyID = "13" + } + return fmt.Sprintf("update `agentic`.agentic_project set %s = '%s' where id = %s and company_id = %s", + column, escapeSQLString(value), id, companyID) +} + +func escapeSQLString(s string) string { + return strings.ReplaceAll(s, "'", "''") +} + +func joinProductNames(names []string) string { + parts := make([]string, 0, len(names)) + for _, n := range names { + n = strings.TrimSpace(n) + if n != "" { + parts = append(parts, n) + } + } + return strings.Join(parts, "|") +} + +// sameDelimitedText 比较以 |/| 分隔的多值文本(忽略顺序与首尾空白)。 +func sameDelimitedText(a, b string) bool { + return delimitedSetKey(a) == delimitedSetKey(b) +} + +func delimitedSetKey(s string) string { + s = strings.ReplaceAll(s, "|", "|") + parts := strings.Split(s, "|") + norm := make([]string, 0, len(parts)) + seen := make(map[string]struct{}) + for _, p := range parts { + p = normalizeText(p) + if p == "" { + continue + } + if _, ok := seen[p]; ok { + continue + } + seen[p] = struct{}{} + norm = append(norm, p) + } + sort.Strings(norm) + return strings.Join(norm, "|") +} + +func normalizeText(s string) string { + s = strings.TrimSpace(s) + if s == "" { + return "" + } + var b strings.Builder + b.Grow(len(s)) + prevSpace := false + for _, r := range s { + if unicode.IsSpace(r) { + if !prevSpace { + b.WriteRune(' ') + prevSpace = true + } + continue + } + prevSpace = false + b.WriteRune(r) + } + return strings.TrimSpace(b.String()) +} + +// foldText 归一化后转小写,避免 Vidda/VIdda 等大小写差异影响匹配。 +func foldText(s string) string { + return strings.ToLower(normalizeText(s)) +} + +func rowToStrings(row []interface{}) []string { + out := make([]string, len(row)) + for i, v := range row { + out[i] = stringifyCell(v) + } + return out +} + +func cellAt(cells []string, idx int) string { + if idx < 0 || idx >= len(cells) { + return "" + } + return strings.TrimSpace(cells[idx]) +} + +func stringifyCell(v interface{}) string { + if v == nil { + return "" + } + switch t := v.(type) { + case string: + return t + case float64: + if t == float64(int64(t)) { + return fmt.Sprintf("%d", int64(t)) + } + return fmt.Sprintf("%v", t) + default: + return strings.TrimSpace(fmt.Sprint(t)) + } +} + +func truncate(s string, n int) string { + if len(s) <= n { + return s + } + return s[:n] + "..." +} diff --git a/haoxian_hisense/local/月度优化问题汇总管理表 - 9月各产业优化问题汇总表.xlsx b/haoxian_hisense/local/月度优化问题汇总管理表 - 9月各产业优化问题汇总表.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..779c573d356140d572706eafc52a7f1487e95fd2 GIT binary patch literal 16769 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 literal 0 HcmV?d00001