wcc 3 meses atrás
pai
commit
2b48da4cc4
9 arquivos alterados com 1543 adições e 20 exclusões
  1. 884 0
      graph/batch.go
  2. 27 3
      graph/go.mod
  3. 68 0
      graph/go.sum
  4. BIN
      graph/graph
  5. BIN
      graph/graph-http
  6. 22 0
      graph/graph_test.go
  7. 7 7
      graph/init.go
  8. 59 10
      graph/main.go
  9. 476 0
      graph/utils.go

+ 884 - 0
graph/batch.go

@@ -0,0 +1,884 @@
+package main
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"github.com/olivere/elastic/v7"
+	nebula "github.com/vesoft-inc/nebula-go/v3"
+	"io"
+	util "jygit.jydev.jianyu360.cn/data_processing/common_utils"
+	"log"
+	"strings"
+	"sync"
+	"time"
+	"unicode/utf8"
+)
+
+func dealCompanyBase22() {
+	log.Println("dealCompanyBase", "开始处理数据")
+	session, pool, err := ConnectToNebula(HostList, UserName, PassWord)
+	if err != nil {
+		log.Fatalf("Failed to connect to Nebula Graph: %v", err)
+	}
+	defer pool.Close()
+	defer session.Release()
+	defer util.Catch()
+	sess := Mgo181.GetMgoConn()
+	defer Mgo181.DestoryMongoConn(sess)
+	it := sess.DB("mixdata").C("company_base").Find(nil).Sort("_id").Select(nil).Iter()
+
+	//jobChan := make(chan InsertJob, WorkerCount*2)
+	var wg sync.WaitGroup
+	count := 0
+
+	// 新增一个 job 构建的协程池
+	buildChan := make(chan map[string]interface{}, WorkerCount*5) // 用于传递原始 Mongo 数据
+	var wgBuild sync.WaitGroup
+	for i := 0; i < 2; i++ {
+		wgBuild.Add(1)
+		go func() {
+			defer wgBuild.Done()
+			count2 := 0
+			for tmp := range buildChan {
+				count2++
+				if count2%10000 == 0 {
+					log.Printf("已处理 %d 条,休息 1 秒...\n", count)
+					time.Sleep(time.Second)
+				}
+				c1 := Legal{
+					Name: util.ObjToString(tmp["company_name"]),
+					Code: util.ObjToString(tmp["credit_no"]),
+					Type: "企业",
+				}
+
+				r1, err := InsertCompany(session, c1)
+				if err != nil {
+					log.Println("InsertCompany", r1, err)
+				}
+
+				// 耗时查询移到这里
+				rea, resb := GetInvByLevel(c1.Name, 1, 0, false)
+				for _, v := range rea {
+					d := Legal{
+						Name: v.company_name,
+						Code: v.credit_no,
+						Type: "企业",
+					}
+					r, err := InsertCompany(session, d)
+					if err != nil {
+						log.Println("InsertCompany", r, err)
+					}
+				}
+
+				for _, v := range resb {
+					d := Invest{
+						FromCode: v.stock_name,
+						ToCode:   v.company_name,
+						Amount:   v.stock_amount,
+						Ratio:    v.stock_rate,
+					}
+					err := InsertInvestRel(session, d)
+					if err != nil {
+						log.Println("InsertInvestRel", err, d)
+					}
+				}
+
+			}
+		}()
+	}
+
+	//realNum := 0
+	for tmp := make(map[string]interface{}); it.Next(&tmp); count++ {
+		if count%10000 == 0 {
+			log.Println("current:", count, tmp["company_name"], tmp["_id"])
+		}
+		if util.IntAll(tmp["use_flag"]) > 0 {
+			continue
+		}
+		if util.ObjToString(tmp["company_type"]) == "个体工商户" || util.ObjToString(tmp["company_type"]) == "个人独资企业" {
+			continue
+		}
+		if util.ObjToString(tmp["company_name"]) == "" || util.ObjToString(tmp["credit_no"]) == "" {
+			continue
+		}
+		// 注销;关闭
+		if strings.Contains(util.ObjToString(tmp["company_status"]), "吊销") || strings.Contains(util.ObjToString(tmp["company_status"]), "注销") || strings.Contains(util.ObjToString(tmp["company_status"]), "关闭") {
+			continue
+		}
+		if utf8.RuneCountInString(util.ObjToString(tmp["company_name"])) < 5 {
+			continue
+		}
+		buildChan <- tmp // 推送到异步处理构建
+	}
+
+	close(buildChan)
+	wgBuild.Wait()
+	wg.Wait()
+	log.Println("完成!")
+}
+
+func dealCompanyBase() {
+	log.Println("dealCompanyBase", "开始处理数据")
+	session, pool, err := ConnectToNebula(HostList, UserName, PassWord)
+	if err != nil {
+		log.Fatalf("Failed to connect to Nebula Graph: %v", err)
+	}
+	defer pool.Close()
+	defer session.Release()
+	defer util.Catch()
+	sess := Mgo181.GetMgoConn()
+	defer Mgo181.DestoryMongoConn(sess)
+	it := sess.DB("mixdata").C("company_base").Find(nil).Select(nil).Iter()
+
+	//jobChan := make(chan InsertJob, WorkerCount*2)
+	var wg sync.WaitGroup
+	count := 0
+
+	// 新增一个 job 构建的协程池
+	buildChan := make(chan map[string]interface{}, WorkerCount*10) // 用于传递原始 Mongo 数据
+	jobChan := make(chan InsertJob, WorkerCount*5)
+
+	// 启动工作协程;存储数据
+	//for i := 0; i < WorkerCount; i++ {
+	//	wg.Add(1)
+	//	go insertWorker(session, &wg, jobChan)
+	//}
+
+	//写入图形数据库
+	for i := 0; i < WorkerCount; i++ {
+		wg.Add(1)
+		go func() {
+			defer wg.Done()
+			// 每个 worker 拿自己的 session
+			localSession, err := pool.GetSession(UserName, PassWord)
+			if err != nil {
+				log.Println("获取 session 失败:", err)
+				return
+			}
+			defer localSession.Release()
+			insertWorker2(localSession, jobChan)
+		}()
+	}
+
+	var wgBuild sync.WaitGroup
+	for i := 0; i < WorkerCount; i++ {
+		wgBuild.Add(1)
+		go func() {
+			defer wgBuild.Done()
+			for tmp := range buildChan {
+				c1 := Legal{
+					Id:   util.ObjToString(tmp["company_id"]),
+					Name: util.ObjToString(tmp["company_name"]),
+					Code: util.ObjToString(tmp["credit_no"]),
+					Type: "企业",
+				}
+				if utf8.RuneCountInString(c1.Name) < 5 {
+					continue
+				}
+				job := InsertJob{}
+				job.Companies = append(job.Companies, c1)
+
+				// 耗时查询移到这里
+				rea, resb := GetInvByLevel(c1.Name, 2, 0, false)
+				for _, v := range rea {
+					d := Legal{
+						Id:   v.company_id,
+						Name: v.company_name,
+						Code: v.credit_no,
+						Type: "企业",
+					}
+					job.Companies = append(job.Companies, d)
+				}
+
+				for _, v := range resb {
+					d := Invest{
+						FromCode: v.stock_id,
+						ToCode:   v.company_id,
+						Amount:   v.stock_amount,
+						Ratio:    v.stock_rate,
+					}
+					job.Relations = append(job.Relations, d)
+				}
+
+				jobChan <- job
+			}
+		}()
+	}
+
+	//realNum := 0
+	for tmp := make(map[string]interface{}); it.Next(&tmp); count++ {
+		if count%10000 == 0 {
+			log.Println("current:", count, tmp["company_name"])
+		}
+		if util.IntAll(tmp["use_flag"]) > 0 {
+			continue
+		}
+		if util.ObjToString(tmp["company_type"]) == "个体工商户" || util.ObjToString(tmp["company_type"]) == "个人独资企业" {
+			continue
+		}
+		if util.ObjToString(tmp["company_name"]) == "" || util.ObjToString(tmp["credit_no"]) == "" {
+			continue
+		}
+		// 注销;关闭
+		if strings.Contains(util.ObjToString(tmp["company_status"]), "吊销") || strings.Contains(util.ObjToString(tmp["company_status"]), "注销") || strings.Contains(util.ObjToString(tmp["company_status"]), "关闭") {
+			continue
+		}
+
+		buildChan <- tmp // 推送到异步处理构建
+
+		//1、处理点
+		//job := InsertJob{}
+		//c1 := Legal{
+		//	Name: util.ObjToString(tmp["company_name"]),
+		//	Code: util.ObjToString(tmp["credit_no"]),
+		//	Type: "企业",
+		//}
+		//if utf8.RuneCountInString(c1.Name) < 5 {
+		//	continue
+		//}
+		//job.Companies = append(job.Companies, c1)
+		////2、处理变
+		//rea, resb := GetInvByLevel(c1.Name, 1, 0, false)
+		//for _, v := range rea {
+		//	d := Legal{
+		//		Name: v.company_name,
+		//		Code: v.credit_no,
+		//		Type: "企业",
+		//	}
+		//	job.Companies = append(job.Companies, d)
+		//}
+		//
+		//for _, v := range resb {
+		//	d := Invest{
+		//		FromCode: v.stock_name,
+		//		ToCode:   v.company_name,
+		//		Amount:   v.stock_amount,
+		//		Ratio:    v.stock_rate,
+		//	}
+		//	job.Relations = append(job.Relations, d)
+		//}
+		//
+		//jobChan <- job
+	}
+
+	close(buildChan)
+	wgBuild.Wait()
+	close(jobChan)
+	wg.Wait()
+	log.Println("完成!")
+}
+
+func batchDealGraph() {
+	session, pool, err := ConnectToNebula(HostList, UserName, PassWord)
+	if err != nil {
+		log.Fatalf("Failed to connect to Nebula Graph: %v", err)
+	}
+	defer pool.Close()
+	defer session.Release()
+
+	client, err := elastic.NewClient(
+		elastic.SetURL("http://172.17.4.184:19908"),
+		//elastic.SetURL("http://127.0.0.1:19908"),
+		elastic.SetBasicAuth("jybid", "Top2023_JEB01i@31"),
+		elastic.SetSniff(false),
+	)
+	if err != nil {
+		log.Fatalf("创建 Elasticsearch 客户端失败:%s", err)
+	}
+
+	query := elastic.NewBoolQuery().
+		//北京,天津,河北,上海,江苏,浙江,安徽
+		//Must(elastic.NewTermQuery("area", "北京市")).
+		//Must(elastic.NewTermsQuery("subtype", "中标", "单一", "成交", "合同")).
+		MustNot(
+			elastic.NewTermQuery("company_type", "个体工商户"),
+			elastic.NewTermsQuery("company_status", "吊销", "注销"),
+		)
+	//Must(elastic.NewTermQuery("company_name", "北京剑鱼信息技术有限公司"))
+	//Must(elastic.NewTermsQuery("company_area", "河南"))
+
+	ctx := context.Background()
+	searchSource := elastic.NewSearchSource().
+		Query(query).
+		Size(10000).
+		Sort("_doc", true)
+
+	searchService := client.Scroll("qyxy").
+		Size(10000).
+		Scroll("5m").
+		SearchSource(searchSource)
+
+	jobChan := make(chan InsertJob, WorkerCount*2)
+	var wg sync.WaitGroup
+
+	// 启动工作协程
+	for i := 0; i < WorkerCount; i++ {
+		wg.Add(1)
+		go insertWorker(session, &wg, jobChan)
+	}
+
+	total := 0
+	for {
+		res, err := searchService.Do(ctx)
+		if err == io.EOF {
+			break
+		}
+		if err != nil {
+			log.Println("scroll error:", err)
+			break
+		}
+		fmt.Println("总数是:", res.TotalHits())
+		if len(res.Hits.Hits) == 0 {
+			break
+		}
+
+		job := InsertJob{}
+		for _, hit := range res.Hits.Hits {
+			var doc map[string]interface{}
+			if err := json.Unmarshal(hit.Source, &doc); err != nil {
+				log.Println("解析失败", err)
+				continue
+			}
+			c1 := Legal{
+				Id:   util.ObjToString(doc["id"]),
+				Name: util.ObjToString(doc["company_name"]),
+				Code: util.ObjToString(doc["credit_no"]),
+				Type: "企业",
+			}
+			////存续、在营、开业、在册
+			//if strings.Contains(util.ObjToString(doc["company_status"]), "存续") || strings.Contains(util.ObjToString(doc["company_status"]), "在营") || strings.Contains(util.ObjToString(doc["company_status"]), "在册") || strings.Contains(util.ObjToString(doc["company_status"]), "开业") {
+			//	c1.State = "有效"
+			//} else {
+			//	c1.State = "无效"
+			//}
+			if !strings.Contains(c1.Name, "公司") {
+				continue
+			}
+			if c1.Name == "" || c1.Code == "" || strings.Contains(c1.Name, "已除名") {
+				continue
+			}
+			if strings.Contains(util.ObjToString(doc["company_status"]), "吊销") || strings.Contains(util.ObjToString(doc["company_status"]), "注销") {
+				continue
+			}
+
+			if utf8.RuneCountInString(c1.Name) < 5 {
+				continue
+			}
+			job.Companies = append(job.Companies, c1)
+
+			if partners, ok := doc["partners"].([]interface{}); ok {
+				for _, partner := range partners {
+					if da, ok := partner.(map[string]interface{}); ok {
+						if !strings.Contains(util.ObjToString(da["stock_type"]), "自然人") && !strings.Contains(util.ObjToString(da["stock_type"]), "个人") {
+							if util.ObjToString(da["stock_name"]) == "" || util.ObjToString(da["identify_no"]) == "" {
+								continue
+							}
+
+							//1
+							where1 := map[string]interface{}{
+								"company_name": util.ObjToString(da["stock_name"]),
+							}
+							tmpBase, _ := Mgo181.FindOne("company_base", where1)
+							if len(*tmpBase) > 0 {
+								c2 := Legal{
+									Id:   util.ObjToString((*tmpBase)["company_id"]),
+									Name: util.ObjToString(da["stock_name"]),
+									Code: util.ObjToString(da["identify_no"]),
+									Type: "企业",
+								}
+								//if strings.Contains(util.ObjToString((*tmpBase)["company_status"]), "存续") || strings.Contains(util.ObjToString((*tmpBase)["company_status"]), "在营") || strings.Contains(util.ObjToString((*tmpBase)["company_status"]), "在册") || strings.Contains(util.ObjToString((*tmpBase)["company_status"]), "开业") {
+								//	c2.State = "有效"
+								//} else {
+								//	c2.State = "无效"
+								//}
+								job.Companies = append(job.Companies, c2)
+								//2
+								where := map[string]interface{}{
+									"company_name": util.ObjToString(doc["company_name"]),
+									"stock_name":   util.ObjToString(da["stock_name"]),
+								}
+								ddd, _ := Mgo181.FindOne("company_partner", where)
+								if len(*ddd) > 0 {
+									par := *ddd
+									invest := Invest{
+										FromCode: c2.Id,
+										ToCode:   c1.Id,
+										Ratio:    util.Float64All(par["stock_proportion"]),
+										Amount:   ParseStockCapital(util.ObjToString(par["stock_capital"])),
+									}
+									job.Relations = append(job.Relations, invest)
+								}
+							}
+						}
+					}
+				}
+			}
+		}
+
+		jobChan <- job
+		total += len(res.Hits.Hits)
+		log.Println("处理总量:", total)
+	}
+
+	close(jobChan)
+	wg.Wait()
+	log.Println("完成!")
+}
+
+func insertWorker2(session *nebula.Session, jobs <-chan InsertJob) {
+	for job := range jobs {
+		// 分批插入公司
+		for i := 0; i < len(job.Companies); i += BatchSize {
+			end := i + BatchSize
+			if end > len(job.Companies) {
+				end = len(job.Companies)
+			}
+			BatchInsertCompanies(session, job.Companies[i:end])
+			//time.Sleep(time.Second * 1)
+		}
+
+		// 分批插入投资关系
+		for i := 0; i < len(job.Relations); i += BatchSize {
+			end := i + BatchSize
+			if end > len(job.Relations) {
+				end = len(job.Relations)
+			}
+			BatchInsertInvestRels(session, job.Relations[i:end])
+			//time.Sleep(time.Second * 1)
+		}
+	}
+}
+
+func insertWorker(session *nebula.Session, wg *sync.WaitGroup, jobs <-chan InsertJob) {
+	defer wg.Done()
+	for job := range jobs {
+		// 分批插入公司
+		for i := 0; i < len(job.Companies); i += BatchSize {
+			end := i + BatchSize
+			if end > len(job.Companies) {
+				end = len(job.Companies)
+			}
+			BatchInsertCompanies(session, job.Companies[i:end])
+		}
+
+		// 分批插入投资关系
+		for i := 0; i < len(job.Relations); i += BatchSize {
+			end := i + BatchSize
+			if end > len(job.Relations) {
+				end = len(job.Relations)
+			}
+			BatchInsertInvestRels(session, job.Relations[i:end])
+		}
+	}
+}
+
+func BatchInsertCompanies(session *nebula.Session, companies []Legal) {
+	if len(companies) == 0 {
+		return
+	}
+	var sb strings.Builder
+	sb.WriteString("USE " + Table_Space + "; ")
+	for _, c := range companies {
+		sb.WriteString(fmt.Sprintf(`INSERT VERTEX Legal(name, code, type, state) VALUES "%s":("%s", "%s", "%s", "%s");`, c.Id, c.Name, c.Code, c.Type, c.State))
+	}
+	_, err := session.Execute(sb.String())
+	if err != nil {
+		log.Println("批量插入公司失败:", err)
+	}
+}
+
+func BatchInsertInvestRels(session *nebula.Session, rels []Invest) {
+	if len(rels) == 0 {
+		return
+	}
+	var sb strings.Builder
+	sb.WriteString("USE " + Table_Space + "; ")
+	for _, r := range rels {
+		sb.WriteString(fmt.Sprintf(`INSERT EDGE Invest(amount, ratio) VALUES "%s"->"%s":(%f, %f);`, r.FromCode, r.ToCode, r.Amount, r.Ratio))
+	}
+	_, err := session.Execute(sb.String())
+	if err != nil {
+		log.Println("批量插入投资关系失败:", err)
+	}
+}
+
+type InvestRelationResult struct {
+	Related     bool
+	Paths       []map[string]string
+	CommonNodes []CommonNodeInfo
+}
+
+type CommonNodeInfo struct {
+	VID  string
+	Name string
+}
+
+func CheckInvestRelationWithIntersection(session *nebula.Session, names []string, depth int) (bool, []map[string]string, []string, error) {
+	if len(names) == 0 || depth <= 0 {
+		return false, nil, nil, fmt.Errorf("invalid input")
+	}
+
+	// Step 1: 获取所有企业的 VID
+	vids := make([]string, 0)
+	vidToName := make(map[string]string)
+	inputVIDSet := make(map[string]bool)
+
+	for _, name := range names {
+		query := fmt.Sprintf(`LOOKUP ON Legal WHERE Legal.name == "%s" YIELD id(vertex)`, name)
+		resp, err := session.Execute(query)
+		if err != nil || !resp.IsSucceed() {
+			log.Printf("lookup failed for name %s: %v", name, err)
+			continue
+		}
+		for _, row := range resp.GetRows() {
+			vid := string(row.Values[0].GetSVal())
+			vids = append(vids, fmt.Sprintf(`"%s"`, vid))
+			vidToName[vid] = name
+			inputVIDSet[vid] = true
+		}
+	}
+
+	if len(vids) < 2 {
+		return false, nil, nil, nil // 不足两个公司参与判断
+	}
+
+	// Step 2: 查找路径(双向 Invest)
+	fromClause := strings.Join(vids, ",")
+	query := fmt.Sprintf(`
+		GO FROM %s OVER Invest BIDIRECT UPTO %d STEPS 
+		YIELD src(edge) AS from, dst(edge) AS to
+	`, fromClause, depth)
+
+	resp, err := session.Execute(query)
+	if err != nil || !resp.IsSucceed() {
+		return false, nil, nil, fmt.Errorf("GO query failed: %v", err)
+	}
+
+	// Step 3: 统计路径和交集节点
+	relationPaths := make([]map[string]string, 0)
+	nodeToSources := make(map[string]map[string]bool) // key: node, value: set of inputVIDs
+
+	for _, row := range resp.GetRows() {
+		from := string(row.Values[0].GetSVal())
+		to := string(row.Values[1].GetSVal())
+
+		// 记录路径
+		relationPaths = append(relationPaths, map[string]string{
+			"from": from,
+			"to":   to,
+		})
+
+		// 记录 from 节点来源
+		if !inputVIDSet[from] {
+			if nodeToSources[from] == nil {
+				nodeToSources[from] = make(map[string]bool)
+			}
+			for _, vid := range vids {
+				if from == strings.Trim(vid, `"`) {
+					continue
+				}
+				if strings.Contains(query, vid) {
+					nodeToSources[from][strings.Trim(vid, `"`)] = true
+				}
+			}
+		}
+
+		// 记录 to 节点来源
+		if !inputVIDSet[to] {
+			if nodeToSources[to] == nil {
+				nodeToSources[to] = make(map[string]bool)
+			}
+			for _, vid := range vids {
+				if to == strings.Trim(vid, `"`) {
+					continue
+				}
+				if strings.Contains(query, vid) {
+					nodeToSources[to][strings.Trim(vid, `"`)] = true
+				}
+			}
+		}
+	}
+
+	// Step 4: 找出出现在多个输入公司路径中的中间节点(交集)
+	commonNodeVIDs := make([]string, 0)
+	for node, sourceSet := range nodeToSources {
+		if len(sourceSet) >= 2 {
+			commonNodeVIDs = append(commonNodeVIDs, node)
+		}
+	}
+
+	// Step 5: 查名称
+	intersectionNames := make([]string, 0)
+	if len(commonNodeVIDs) > 0 {
+		query := fmt.Sprintf(`FETCH PROP ON Legal %s YIELD Legal.name`, strings.Join(wrapInQuotes(commonNodeVIDs), ","))
+		resp, err := session.Execute(query)
+		if err == nil && resp.IsSucceed() {
+			for _, row := range resp.GetRows() {
+				name := string(row.Values[1].GetSVal())
+				intersectionNames = append(intersectionNames, name)
+			}
+		}
+	}
+
+	found := len(intersectionNames) > 0
+	return found, relationPaths, intersectionNames, nil
+}
+
+func wrapInQuotes(ids []string) []string {
+	result := make([]string, len(ids))
+	for i, id := range ids {
+		result[i] = fmt.Sprintf(`"%s"`, id)
+	}
+	return result
+}
+
+func CheckInvestRelation1(session *nebula.Session, names []string, depth int) (bool, []map[string]string, error) {
+	if len(names) == 0 || depth <= 0 {
+		return false, nil, fmt.Errorf("invalid input")
+	}
+
+	// Step 1: 获取所有企业的 VID
+	vids := make([]string, 0)
+	nameSet := make(map[string]bool)
+	for _, name := range names {
+		query := fmt.Sprintf(`LOOKUP ON Legal WHERE Legal.name == "%s" YIELD id(vertex)`, name)
+		resp, err := session.Execute(query)
+		if err != nil || !resp.IsSucceed() {
+			log.Printf("lookup failed for name %s: %v", name, err)
+			continue
+		}
+		for _, row := range resp.GetRows() {
+			//vid := row.Values[0].GetSVal()
+			vid := string(row.Values[0].GetSVal())
+			vids = append(vids, fmt.Sprintf(`"%s"`, vid))
+			nameSet[vid] = true
+		}
+	}
+
+	if len(vids) == 0 {
+		return false, nil, nil // 没有查出任何节点
+	}
+
+	// Step 2: 构造 GO 查询路径
+	fromClause := strings.Join(vids, ",")
+	query := fmt.Sprintf(`
+		GO FROM %s OVER Invest UPTO %d STEPS
+		YIELD src(edge) AS from, dst(edge) AS to
+	`, fromClause, depth)
+
+	resp, err := session.Execute(query)
+	if err != nil {
+		return false, nil, fmt.Errorf("GO query failed: %v", err)
+	}
+	if !resp.IsSucceed() {
+		return false, nil, fmt.Errorf("Nebula error: %s", resp.GetErrorMsg())
+	}
+
+	// Step 3: 分析路径结果
+	resultPaths := make([]map[string]string, 0)
+	found := false
+	for _, row := range resp.GetRows() {
+		from := string(row.Values[0].GetSVal())
+		to := string(row.Values[1].GetSVal())
+
+		if nameSet[from] && nameSet[to] && from != to {
+			found = true
+		}
+
+		resultPaths = append(resultPaths, map[string]string{
+			"from": from,
+			"to":   to,
+		})
+	}
+
+	return found, resultPaths, nil
+}
+
+func FindInvestmentRelations() {
+
+}
+
+//type PathRelation struct {
+//	Companies []string
+//	Paths     []string
+//}
+//
+//func CheckLegalRelationsGraph(session *nebula.Session, names []string, deep int) (*PathRelation, error) {
+//	// 查询 name -> vid 映射
+//	nameToVid := make(map[string]string)
+//	vidToName := make(map[string]string)
+//	for _, name := range names {
+//		vid, err := getVidByName(session, name)
+//		if err != nil {
+//			log.Printf("获取 %s 的 VID 失败: %v", name, err)
+//			continue
+//		}
+//		nameToVid[name] = vid
+//		vidToName[vid] = name
+//	}
+//
+//	allPaths := [][]string{}
+//	checked := make(map[string]bool)
+//
+//	// 遍历所有组合
+//	for i := 0; i < len(names); i++ {
+//		for j := i + 1; j < len(names); j++ {
+//			a, b := names[i], names[j]
+//			vidA, okA := nameToVid[a]
+//			vidB, okB := nameToVid[b]
+//			if !okA || !okB {
+//				continue
+//			}
+//			key := vidA + "|" + vidB
+//			if checked[key] {
+//				continue
+//			}
+//			checked[key] = true
+//
+//			if pathAB, _ := findPath(session, vidA, vidB, deep); len(pathAB) > 0 {
+//				allPaths = append(allPaths, pathAB)
+//			}
+//			if pathBA, _ := findPath(session, vidB, vidA, deep); len(pathBA) > 0 {
+//				allPaths = append(allPaths, pathBA)
+//			}
+//
+//			// 共同上级路径
+//			common, commonPaths := checkCommonAncestor(session, vidA, vidB, deep)
+//			if common {
+//				allPaths = append(allPaths, commonPaths)
+//			}
+//		}
+//	}
+//
+//	// 1. 收集所有涉及的 VID
+//	vidSet := make(map[string]struct{})
+//	for _, path := range allPaths {
+//		for _, vid := range path {
+//			vidSet[vid] = struct{}{}
+//		}
+//	}
+//
+//	// 2. 获取所有 VID 的公司名
+//	for vid := range vidSet {
+//		if _, ok := vidToName[vid]; ok {
+//			continue
+//		}
+//		query := fmt.Sprintf(`FETCH PROP ON Legal "%s" YIELD Legal.name`, vid)
+//		resp, err := session.Execute(query)
+//		if err != nil || resp.IsEmpty() {
+//			continue
+//		}
+//		rows := resp.GetRows()
+//		if len(rows) > 0 && len(rows[0].Values) > 0 && rows[0].Values[0].SVal != nil {
+//			vidToName[vid] = string(rows[0].Values[0].SVal)
+//		}
+//	}
+//
+//	// 3. 清洗路径并格式化输出
+//	companySet := make(map[string]struct{})
+//	result := &PathRelation{
+//		Companies: []string{},
+//		Paths:     []string{},
+//	}
+//
+//	for _, path := range allPaths {
+//		namesPath := []string{}
+//		last := ""
+//		for _, vid := range path {
+//			name, ok := vidToName[vid]
+//			if !ok {
+//				continue
+//			}
+//			if name == last {
+//				continue // 去除重复节点
+//			}
+//			namesPath = append(namesPath, name)
+//			last = name
+//			companySet[name] = struct{}{}
+//		}
+//		if len(namesPath) >= 2 {
+//			result.Paths = append(result.Paths, strings.Join(namesPath, "->"))
+//		}
+//	}
+//
+//	for name := range companySet {
+//		result.Companies = append(result.Companies, name)
+//	}
+//	sort.Strings(result.Companies)
+//	return result, nil
+//}
+//
+//func checkCommonAncestor(session *nebula.Session, aVid, bVid string, deep int) (bool, []string) {
+//	query := fmt.Sprintf(`
+//	(
+//		GO 1 TO %d STEPS FROM "%s" OVER Invest REVERSELY YIELD dst(edge) AS ancestor
+//	)
+//	INTERSECT
+//	(
+//		GO 1 TO %d STEPS FROM "%s" OVER Invest REVERSELY YIELD dst(edge) AS ancestor
+//	);
+//	`, deep, aVid, deep, bVid)
+//
+//	resp, err := session.Execute(query)
+//	if err != nil {
+//		return false, nil
+//	}
+//	ancestors, err := getFirstColumnStrings(resp)
+//	if err != nil || len(ancestors) == 0 {
+//		return false, nil
+//	}
+//
+//	// 只返回第一个共同祖先的简单路径:a->ancestor->b
+//	return true, []string{aVid, ancestors[0], bVid}
+//}
+//
+//func findPath(session *nebula.Session, fromVid, toVid string, maxStep int) ([]string, error) {
+//	query := fmt.Sprintf(`FIND ALL PATH FROM "%s" TO "%s" OVER Invest UPTO %d STEPS YIELD path as p`, fromVid, toVid, maxStep)
+//	resp, err := session.Execute(query)
+//	if err != nil {
+//		return nil, err
+//	}
+//	return getFirstColumnStrings(resp)
+//}
+//
+//func getVidByName(session *nebula.Session, name string) (string, error) {
+//	query := fmt.Sprintf(`
+//USE `+Table_Space+`;
+//LOOKUP ON Legal WHERE Legal.name == "%s" YIELD id(vertex)`, name)
+//	resp, err := session.Execute(query)
+//	if err != nil {
+//		return "", err
+//	}
+//
+//	values, err := getFirstColumnStrings(resp)
+//	if err != nil || len(values) == 0 {
+//		return "", fmt.Errorf("未找到公司: %s", name)
+//	}
+//	return values[0], nil
+//}
+//
+//func getFirstColumnStrings(resp *nebula.ResultSet) ([]string, error) {
+//	if resp == nil {
+//		return nil, fmt.Errorf("result set is nil")
+//	}
+//
+//	var values []string
+//	for _, row := range resp.GetRows() {
+//		if len(row.Values) == 0 {
+//			continue
+//		}
+//		val := row.Values[0]
+//		switch {
+//		case val.SVal != nil:
+//			values = append(values, string(val.SVal))
+//		case val.IVal != nil:
+//			values = append(values, fmt.Sprintf("%d", *val.IVal))
+//		case val.BVal != nil:
+//			values = append(values, fmt.Sprintf("%v", *val.BVal))
+//		default:
+//			log.Printf("未知类型值: %+v", val)
+//		}
+//	}
+//	return values, nil
+//}

+ 27 - 3
graph/go.mod

@@ -11,22 +11,46 @@ require (
 require (
 	github.com/PuerkitoBio/goquery v1.8.0 // indirect
 	github.com/andybalholm/cascadia v1.3.1 // indirect
+	github.com/bytedance/sonic v1.11.6 // indirect
+	github.com/bytedance/sonic/loader v0.1.1 // indirect
+	github.com/cloudwego/base64x v0.1.4 // indirect
+	github.com/cloudwego/iasm v0.2.0 // indirect
 	github.com/dchest/captcha v1.0.0 // indirect
+	github.com/gabriel-vasile/mimetype v1.4.3 // indirect
+	github.com/gin-contrib/sse v0.1.0 // indirect
+	github.com/gin-gonic/gin v1.10.0 // indirect
+	github.com/go-playground/locales v0.14.1 // indirect
+	github.com/go-playground/universal-translator v0.18.1 // indirect
+	github.com/go-playground/validator/v10 v10.20.0 // indirect
+	github.com/goccy/go-json v0.10.2 // indirect
 	github.com/golang/snappy v0.0.1 // indirect
 	github.com/josharian/intern v1.0.0 // indirect
+	github.com/json-iterator/go v1.1.12 // indirect
 	github.com/klauspost/compress v1.13.6 // indirect
+	github.com/klauspost/cpuid/v2 v2.2.7 // indirect
+	github.com/leodido/go-urn v1.4.0 // indirect
 	github.com/mailru/easyjson v0.7.7 // indirect
+	github.com/mattn/go-isatty v0.0.20 // indirect
+	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+	github.com/modern-go/reflect2 v1.0.2 // indirect
 	github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect
+	github.com/pelletier/go-toml/v2 v2.2.2 // indirect
 	github.com/pkg/errors v0.9.1 // indirect
+	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
+	github.com/ugorji/go/codec v1.2.12 // indirect
 	github.com/vesoft-inc/fbthrift v0.0.0-20230214024353-fa2f34755b28 // indirect
 	github.com/xdg-go/pbkdf2 v1.0.0 // indirect
 	github.com/xdg-go/scram v1.1.1 // indirect
 	github.com/xdg-go/stringprep v1.0.3 // indirect
 	github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
 	go.mongodb.org/mongo-driver v1.10.1 // indirect
-	golang.org/x/crypto v0.14.0 // indirect
-	golang.org/x/net v0.17.0 // indirect
+	golang.org/x/arch v0.8.0 // indirect
+	golang.org/x/crypto v0.23.0 // indirect
+	golang.org/x/net v0.25.0 // indirect
 	golang.org/x/sync v0.1.0 // indirect
-	golang.org/x/text v0.13.0 // indirect
+	golang.org/x/sys v0.20.0 // indirect
+	golang.org/x/text v0.15.0 // indirect
+	google.golang.org/protobuf v1.34.1 // indirect
 	gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
 )

+ 68 - 0
graph/go.sum

@@ -7,8 +7,16 @@ github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x0
 github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
 github.com/aws/aws-sdk-go v1.43.21/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
 github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
+github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
+github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
+github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
+github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
+github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
+github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
+github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
 github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -22,9 +30,23 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
 github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
 github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
+github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
+github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
+github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
+github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
+github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
+github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
+github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
+github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
+github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
+github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
+github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
 github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
+github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
+github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
 github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@@ -50,26 +72,44 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
 github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=
 github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
 github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
 github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
 github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
 github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc=
 github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
+github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
+github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
+github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
+github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
 github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
+github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
 github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
 github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
 github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0=
 github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
 github.com/nsqio/go-nsq v1.1.0/go.mod h1:vKq36oyeVXgsS5Q8YEO7WghqidAVXQlcFxzQbQTuDEY=
 github.com/olivere/elastic/v7 v7.0.32 h1:R7CXvbu8Eq+WlsLgxmKVKPox0oOwAE/2T9Si5BnvK6E=
 github.com/olivere/elastic/v7 v7.0.32/go.mod h1:c7PVmLe3Fxq77PIfY/bZmxY/TAamBhCzZ8xDOE09a9k=
 github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
+github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
+github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -80,12 +120,24 @@ github.com/smartystreets/assertions v1.1.1/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYl
 github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM=
 github.com/smartystreets/gunit v1.4.2/go.mod h1:ZjM1ozSIMJlAz/ay4SG8PeKF00ckUp+zMHZXV9/bvak=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
 github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
+github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
+github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
+github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
+github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
 github.com/vesoft-inc/fbthrift v0.0.0-20230214024353-fa2f34755b28 h1:gpoPCGeOEuk/TnoY9nLVK1FoBM5ie7zY3BPVG8q43ME=
 github.com/vesoft-inc/fbthrift v0.0.0-20230214024353-fa2f34755b28/go.mod h1:xu7e9za8StcJhBZmCDwK1Hyv4/Y0xFsjS+uqp10ECJg=
 github.com/vesoft-inc/nebula-go/v3 v3.8.0 h1:ecB87KMnMUcuKbgFESKIscdxA7Y1TcX7XEVqZQ1UqlA=
@@ -109,6 +161,9 @@ go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
 go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
 go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
 go.uber.org/zap v1.22.0/go.mod h1:H4siCOZOrAolnUPJEkfaSjDqyP+BDS0DdDWzwcgt3+U=
+golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
+golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
+golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@@ -116,6 +171,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
 golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
 golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
+golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
+golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
@@ -141,6 +198,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
 golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
 golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
+golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
+golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -162,8 +221,11 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
+golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -177,6 +239,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
 golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
+golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -210,6 +274,8 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
+google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
+google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -226,3 +292,5 @@ honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWh
 honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 jygit.jydev.jianyu360.cn/data_processing/common_utils v0.0.0-20240412074219-927f3f682cb3 h1:mTokQIoOu/oZ2oCSAPayIFfnglIHP0qbOw1Ez6biKDo=
 jygit.jydev.jianyu360.cn/data_processing/common_utils v0.0.0-20240412074219-927f3f682cb3/go.mod h1:1Rp0ioZBhikjXHYYXmnzL6RNfvTDM/2XvRB+vuPLurI=
+nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
+rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

BIN
graph/graph


BIN
graph/graph-http


+ 22 - 0
graph/graph_test.go

@@ -0,0 +1,22 @@
+package main
+
+import (
+	"log"
+	"testing"
+)
+
+func TestCheckInvestRelation(t *testing.T) {
+	session, pool, err := ConnectToNebula(HostList, UserName, PassWord)
+	if err != nil {
+		log.Fatalf("Failed to connect to Nebula Graph: %v", err)
+	}
+	defer pool.Close()
+	defer session.Release()
+	names := []string{"北京剑鱼信息技术有限公司", "河南剑鱼数字科技有限公司", "新疆拓普丰联网络信息技术有限公司"}
+	//res, err := CheckLegalRelationsGraph(session, names, 3)
+	res, err := CheckLegalRelations(session, names, 3)
+	if err != nil {
+		log.Println(res, err)
+	}
+	log.Println(res)
+}

+ 7 - 7
graph/init.go

@@ -5,13 +5,13 @@ import "jygit.jydev.jianyu360.cn/data_processing/common_utils/mongodb"
 func InitMgo() {
 	//181 凭安库
 	Mgo181 = &mongodb.MongodbSim{
-		MongodbAddr: "172.17.4.181:27001",
-		//MongodbAddr: "127.0.0.1:27001",
-		DbName:   "mixdata",
-		Size:     10,
-		UserName: "",
-		Password: "",
-		//Direct:      true,
+		//MongodbAddr: "172.17.4.181:27001",
+		MongodbAddr: "127.0.0.1:27001",
+		DbName:      "mixdata",
+		Size:        10,
+		UserName:    "",
+		Password:    "",
+		Direct:      true,
 	}
 	Mgo181.InitPool()
 }

+ 59 - 10
graph/main.go

@@ -4,12 +4,14 @@ import (
 	"context"
 	"encoding/json"
 	"fmt"
+	"github.com/gin-gonic/gin"
 	"github.com/olivere/elastic/v7"
 	nebula "github.com/vesoft-inc/nebula-go/v3"
 	"io"
 	util "jygit.jydev.jianyu360.cn/data_processing/common_utils"
 	"jygit.jydev.jianyu360.cn/data_processing/common_utils/mongodb"
 	"log"
+	"net/http"
 	"regexp"
 	"strconv"
 	"strings"
@@ -23,13 +25,17 @@ var (
 	PassWord    = "jianyu@123"
 	Mgo181      *mongodb.MongodbSim
 	Table_Space = "legal_profile"
+	WorkerCount = 5
+	BatchSize   = 100
 )
 
 // Legal 代表公司节点的结构体
 type Legal struct {
-	Name string
-	Code string
-	Type string
+	Id    string
+	Name  string
+	Code  string
+	Type  string //类型,企业,事业单哪位,政府部门
+	State string //状态,有效/无效;不是存续、在营、开业、在册
 }
 
 // Invest 代表公司之间的投资关系边的结构体
@@ -40,6 +46,11 @@ type Invest struct {
 	Ratio    float64
 }
 
+type InsertJob struct {
+	Companies []Legal
+	Relations []Invest
+}
+
 type InvestVertex struct { //顶点
 	id           string
 	company_id   string
@@ -80,12 +91,48 @@ func ConnectToNebula(hosts []nebula.HostAddress, username, password string) (*ne
 	return session, pool, nil
 }
 
+type CheckRequest struct {
+	Names []string `json:"names"`
+	Deep  int      `json:"deep"`
+}
+
 func main() {
-	InitMgo()
-	getQyxytData()
+	//InitMgo()
+	//dda()
+	//dealCompanyBase22()
+	//dealCompanyBase() //迭代company_base 处理企业数据
+	//batchDealGraph() // 迭代es 处理企业数据;
 	//
 
 	log.Println("数据处理完毕!!!!!!!")
+	//封装对外提供的HTTP
+	session, pool, err := ConnectToNebula(HostList, UserName, PassWord)
+	if err != nil {
+		log.Fatalf("Failed to connect to Nebula Graph: %v", err)
+	}
+	defer pool.Close()
+	defer session.Release()
+	// 初始化 Gin 路由
+	r := gin.Default()
+	// 注册 POST 接口
+	r.POST("/check-relations", func(c *gin.Context) {
+		var req CheckRequest
+		if err := c.ShouldBindJSON(&req); err != nil {
+			c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数无效"})
+			return
+		}
+
+		results, err := CheckLegalRelations(session, req.Names, req.Deep)
+		if err != nil {
+			c.JSON(http.StatusInternalServerError, gin.H{"error": "查询失败", "details": err.Error()})
+			return
+		}
+
+		c.JSON(http.StatusOK, results)
+	})
+
+	// 启动服务
+	r.Run(":8080")
 }
 
 func dda() {
@@ -101,8 +148,10 @@ func dda() {
 	defer session.Release()
 	for _, v := range rea {
 		d := Legal{
+			Id:   v.company_id,
 			Name: v.company_name,
 			Code: v.credit_no,
+			Type: "企业",
 		}
 		res, err := InsertCompany(session, d)
 		if err != nil {
@@ -112,8 +161,8 @@ func dda() {
 
 	for _, v := range resb {
 		d := Invest{
-			FromCode: v.stock_name,
-			ToCode:   v.company_name,
+			FromCode: v.stock_id,
+			ToCode:   v.company_id,
 			Amount:   v.stock_amount,
 			Ratio:    v.stock_rate,
 		}
@@ -374,8 +423,8 @@ func InsertCompany(session *nebula.Session, company Legal) (string, error) {
 	//insertCompanyStmt = fmt.Sprintf(insertCompanyStmt, inv.id, inv.company_id, inv.company_name)
 	query := fmt.Sprintf(`
 		USE `+Table_Space+`;
-		INSERT VERTEX Legal(name, code, type) VALUES "%s":("%s", "%s", "%s")
-	`, company.Name, company.Name, company.Code, company.Type)
+		INSERT VERTEX Legal(name, code, type, state ) VALUES "%s":("%s", "%s", "%s", "%s")
+	`, company.Id, company.Name, company.Code, company.Type, company.State)
 
 	// 执行查询
 	result, err := session.Execute(query)
@@ -564,7 +613,7 @@ func getInvByLevel(company_name string, maxLevel int, direction int, person bool
 				}
 			}
 		}
-		log.Printf("已处理层级%d,当前队列深度%d", current.level, len(queue))
+		//log.Printf("已处理层级%d,当前队列深度%d", current.level, len(queue))
 	}
 	return verter, edges
 }

+ 476 - 0
graph/utils.go

@@ -0,0 +1,476 @@
+package main
+
+import (
+	"fmt"
+	nebula "github.com/vesoft-inc/nebula-go/v3"
+	"log"
+)
+
+// 表示企业关系结果
+//type AllRelationResult struct {
+//	RelatedCompanies []string // 有关系的企业列表
+//	Paths            []string // 对应的路径
+//}
+//
+//// 批量获取企业的 VID
+//func getVidsByName(session *nebula.Session, names []string) (map[string]string, error) {
+//	if len(names) == 0 {
+//		return nil, nil
+//	}
+//	conditions := ""
+//	for i, name := range names {
+//		if i > 0 {
+//			conditions += " OR "
+//		}
+//		conditions += fmt.Sprintf("Legal.name == \"%s\"", name)
+//	}
+//	query := fmt.Sprintf(`
+//USE %s;
+//LOOKUP ON Legal WHERE %s YIELD id(vertex) AS vid, properties(vertex).name AS name
+//`, Table_Space, conditions)
+//	resp, err := session.Execute(query)
+//	if err != nil {
+//		return nil, err
+//	}
+//	nameToVid := make(map[string]string)
+//	for _, row := range resp.GetRows() {
+//		if len(row.Values) >= 2 {
+//			if row.Values[0].SVal != nil && row.Values[1].SVal != nil {
+//				nameToVid[string(row.Values[1].SVal)] = string(row.Values[0].SVal)
+//			}
+//		}
+//	}
+//	return nameToVid, nil
+//}
+//
+//// 获取 VID 对应的名称
+//func getVidName(session *nebula.Session, vid string) (string, error) {
+//	query := fmt.Sprintf(`
+//USE %s;
+//FETCH PROP ON Legal "%s" YIELD properties(vertex).name AS name
+//`, Table_Space, vid)
+//	resp, err := session.Execute(query)
+//	if err != nil {
+//		return "", err
+//	}
+//	names, err := getFirstColumnStrings(resp)
+//	if err != nil || len(names) == 0 {
+//		return "", fmt.Errorf("未找到 VID %s 的名称", vid)
+//	}
+//	return names[0], nil
+//}
+//
+//// 查找路径
+//func findPath(session *nebula.Session, fromVid, toVid string, maxStep int, pathCache map[string][]string) ([]string, error) {
+//	key := fmt.Sprintf("%s->%s:%d", fromVid, toVid, maxStep)
+//	if cachedPath, ok := pathCache[key]; ok {
+//		return cachedPath, nil
+//	}
+//	query := fmt.Sprintf(`FIND  ALL PATH FROM "%s" TO "%s" OVER Invest UPTO %d STEPS YIELD path as p`, fromVid, toVid, maxStep)
+//	resp, err := session.Execute(query)
+//	if err != nil {
+//		return nil, err
+//	}
+//	path, err := getFirstColumnStrings(resp)
+//	if err != nil {
+//		return nil, err
+//	}
+//	pathCache[key] = path
+//	return path, nil
+//}
+//
+//// 检查共同祖先
+//func checkCommonAncestor(session *nebula.Session, aVid, bVid string, deep int, pathCache map[string][]string) (bool, []string, string) {
+//	key := fmt.Sprintf("%s&%s:%d", aVid, bVid, deep)
+//	if cachedPath, ok := pathCache[key]; ok {
+//		if len(cachedPath) > 0 {
+//			return true, cachedPath, cachedPath[1]
+//		}
+//		return false, nil, ""
+//	}
+//	query := fmt.Sprintf(`
+//    (
+//        GO 1 TO %d STEPS FROM "%s" OVER Invest REVERSELY YIELD dst(edge) AS ancestor
+//    )
+//    INTERSECT
+//    (
+//        GO 1 TO %d STEPS FROM "%s" OVER Invest REVERSELY YIELD dst(edge) AS ancestor
+//    );
+//    `, deep, aVid, deep, bVid)
+//	resp, err := session.Execute(query)
+//	if err != nil {
+//		return false, nil, ""
+//	}
+//	ancestors, err := getFirstColumnStrings(resp)
+//	if err != nil || len(ancestors) == 0 {
+//		pathCache[key] = nil
+//		return false, nil, ""
+//	}
+//	pathA, _ := findPath(session, aVid, ancestors[0], deep, pathCache)
+//	pathB, _ := findPath(session, bVid, ancestors[0], deep, pathCache)
+//	var path []string
+//	if len(pathB) > 1 {
+//		path = append(pathA, pathB[1:]...)
+//	} else {
+//		path = append(pathA, pathB...)
+//	}
+//	pathCache[key] = path
+//	return true, path, ancestors[0]
+//}
+//
+//// 将 VID 路径转换为名称路径
+//func convertVidPathToNamePath(session *nebula.Session, vidPath []string) (string, error) {
+//	namePath := ""
+//	for i, vid := range vidPath {
+//		name, err := getVidName(session, vid)
+//		if err != nil {
+//			return "", err
+//		}
+//		if i > 0 {
+//			namePath += "->"
+//		}
+//		namePath += name
+//	}
+//	return namePath, nil
+//}
+//
+//// 检查企业关系
+//func CheckLegalRelations(session *nebula.Session, names []string, deep int) (AllRelationResult, error) {
+//	result := AllRelationResult{}
+//	checked := make(map[string]bool)
+//	nameToVid, err := getVidsByName(session, names)
+//	if err != nil {
+//		return result, err
+//	}
+//	pathCache := make(map[string][]string)
+//	relatedCompaniesSet := make(map[string]bool)
+//	var paths []string
+//
+//	for i := 0; i < len(names); i++ {
+//		for j := i + 1; j < len(names); j++ {
+//			a, b := names[i], names[j]
+//			vidA, okA := nameToVid[a]
+//			vidB, okB := nameToVid[b]
+//			if !okA || !okB {
+//				continue
+//			}
+//
+//			key := vidA + "|" + vidB
+//			if checked[key] {
+//				continue
+//			}
+//			checked[key] = true
+//
+//			// 1. a -> b
+//			pathAB, err := findPath(session, vidA, vidB, deep, pathCache)
+//			if err != nil {
+//				log.Printf("查找 %s 到 %s 的路径失败: %v", a, b, err)
+//				continue
+//			}
+//			if len(pathAB) > 0 {
+//				pathStr, err := convertVidPathToNamePath(session, pathAB)
+//				if err != nil {
+//					log.Printf("转换 %s 到 %s 的路径失败: %v", a, b, err)
+//					continue
+//				}
+//				paths = append(paths, pathStr)
+//				for _, vid := range pathAB {
+//					name, err := getVidName(session, vid)
+//					if err != nil {
+//						log.Printf("获取 VID %s 对应的名称失败: %v", vid, err)
+//						continue
+//					}
+//					relatedCompaniesSet[name] = true
+//				}
+//				continue
+//			}
+//
+//			// 2. b -> a
+//			pathBA, err := findPath(session, vidB, vidA, deep, pathCache)
+//			if err != nil {
+//				log.Printf("查找 %s 到 %s 的路径失败: %v", b, a, err)
+//				continue
+//			}
+//			if len(pathBA) > 0 {
+//				pathStr, err := convertVidPathToNamePath(session, pathBA)
+//				if err != nil {
+//					log.Printf("转换 %s 到 %s 的路径失败: %v", b, a, err)
+//					continue
+//				}
+//				paths = append(paths, pathStr)
+//				for _, vid := range pathBA {
+//					name, err := getVidName(session, vid)
+//					if err != nil {
+//						log.Printf("获取 VID %s 对应的名称失败: %v", vid, err)
+//						continue
+//					}
+//					relatedCompaniesSet[name] = true
+//				}
+//				continue
+//			}
+//
+//			// 3. common ancestor
+//			common, path, _ := checkCommonAncestor(session, vidA, vidB, deep, pathCache)
+//			if common {
+//				pathStr, err := convertVidPathToNamePath(session, path)
+//				if err != nil {
+//					log.Printf("转换 %s 和 %s 到共同祖先的路径失败: %v", a, b, err)
+//					continue
+//				}
+//				paths = append(paths, pathStr)
+//				for _, vid := range path {
+//					name, err := getVidName(session, vid)
+//					if err != nil {
+//						log.Printf("获取 VID %s 对应的名称失败: %v", vid, err)
+//						continue
+//					}
+//					relatedCompaniesSet[name] = true
+//				}
+//			}
+//		}
+//	}
+//
+//	for company := range relatedCompaniesSet {
+//		result.RelatedCompanies = append(result.RelatedCompanies, company)
+//	}
+//	result.Paths = paths
+//
+//	return result, nil
+//}
+//
+//// getFirstColumnStrings 适配 nebula-go v3 取出字符串类型列
+//func getFirstColumnStrings(resp *nebula.ResultSet) ([]string, error) {
+//	if resp == nil {
+//		return nil, fmt.Errorf("result set is nil")
+//	}
+//
+//	var values []string
+//	for _, row := range resp.GetRows() {
+//		if len(row.Values) == 0 {
+//			continue
+//		}
+//		val := row.Values[0]
+//		switch {
+//		case val.SVal != nil:
+//			values = append(values, string(val.SVal))
+//		case val.IVal != nil:
+//			values = append(values, fmt.Sprintf("%d", *val.IVal))
+//		case val.BVal != nil:
+//			values = append(values, fmt.Sprintf("%v", *val.BVal))
+//		default:
+//			log.Printf("未知类型值: %+v", val)
+//		}
+//	}
+//	return values, nil
+//}
+
+func CheckLegalRelations(session *nebula.Session, names []string, deep int) ([]RelationResult, error) {
+	results := []RelationResult{}
+	checked := make(map[string]bool)
+	nameToVid, err := getAllVids(session, names)
+	if err != nil {
+		return nil, err
+	}
+	vidToName := reverseMap(nameToVid)
+
+	for i := 0; i < len(names); i++ {
+		for j := i + 1; j < len(names); j++ {
+			a, b := names[i], names[j]
+			vidA, okA := nameToVid[a]
+			vidB, okB := nameToVid[b]
+			if !okA || !okB {
+				continue
+			}
+
+			key := vidA + "|" + vidB
+			if checked[key] {
+				continue
+			}
+			checked[key] = true
+
+			// 1. a -> b
+			pathAB, err := findPath(session, vidA, vidB, deep)
+			if err != nil {
+				return nil, err
+			}
+			if len(pathAB) > 0 {
+				readablePath := convertPathToNames(pathAB, vidToName)
+				results = append(results, RelationResult{A: a, B: b, RelationType: "direct_or_indirect", Path: readablePath})
+				continue
+			}
+
+			// 2. b -> a
+			pathBA, err := findPath(session, vidB, vidA, deep)
+			if err != nil {
+				return nil, err
+			}
+			if len(pathBA) > 0 {
+				readablePath := convertPathToNames(pathBA, vidToName)
+				results = append(results, RelationResult{A: b, B: a, RelationType: "direct_or_indirect", Path: readablePath})
+				continue
+			}
+
+			// 3. common ancestor
+			common, ancestorVid, err := checkCommonAncestor(session, vidA, vidB, deep)
+			if err != nil {
+				return nil, err
+			}
+			if common {
+				ancestorName := getAncestorName(session, ancestorVid, vidToName)
+				aName := vidToName[vidA]
+				bName := vidToName[vidB]
+				if ancestorName != "" && aName != "" && bName != "" {
+					readablePath := []string{
+						fmt.Sprintf("%s -> %s", aName, ancestorName),
+						fmt.Sprintf("%s -> %s", bName, ancestorName),
+					}
+					results = append(results, RelationResult{A: a, B: b, RelationType: "common_ancestor", Path: readablePath})
+				}
+			}
+		}
+	}
+
+	return results, nil
+}
+
+func getAllVids(session *nebula.Session, names []string) (map[string]string, error) {
+	nameToVid := make(map[string]string)
+	for _, name := range names {
+		vid, err := getVidByName(session, name)
+		if err != nil {
+			log.Printf("获取 %s 的 VID 失败: %v", name, err)
+			continue
+		}
+		nameToVid[name] = vid
+	}
+	return nameToVid, nil
+}
+
+func checkCommonAncestor(session *nebula.Session, aVid, bVid string, deep int) (bool, string, error) {
+	query := fmt.Sprintf(`
+    (
+        GO 1 TO %d STEPS FROM "%s" OVER Invest REVERSELY YIELD dst(edge) AS ancestor
+    )
+    INTERSECT
+    (
+        GO 1 TO %d STEPS FROM "%s" OVER Invest REVERSELY YIELD dst(edge) AS ancestor
+    );
+    `, deep, aVid, deep, bVid)
+
+	resp, err := session.Execute(query)
+	if err != nil {
+		return false, "", err
+	}
+	ancestors, err := getFirstColumnStrings(resp)
+	if err != nil || len(ancestors) == 0 {
+		return false, "", nil
+	}
+	return true, ancestors[0], nil
+}
+
+func findPath(session *nebula.Session, fromVid, toVid string, maxStep int) ([]string, error) {
+	query := fmt.Sprintf(`FIND  ALL PATH FROM "%s" TO "%s" OVER Invest UPTO %d STEPS YIELD path as p`, fromVid, toVid, maxStep)
+	resp, err := session.Execute(query)
+	if err != nil {
+		return nil, err
+	}
+	return getFirstColumnStrings(resp)
+}
+
+func getVidByName(session *nebula.Session, name string) (string, error) {
+	query := fmt.Sprintf(`
+USE `+Table_Space+`;
+LOOKUP ON Legal WHERE Legal.name == "%s" YIELD id(vertex)`, name)
+	resp, err := session.Execute(query)
+	if err != nil {
+		return "", err
+	}
+
+	values, err := getFirstColumnStrings(resp)
+	if err != nil || len(values) == 0 {
+		return "", fmt.Errorf("未找到公司: %s", name)
+	}
+	return values[0], nil
+}
+
+type RelationResult struct {
+	A, B         string   // 公司名
+	RelationType string   // 关系类型:"direct_or_indirect", "common_ancestor"
+	Path         []string // 路径中的公司名称,以更直观的形式展示
+}
+
+// getFirstColumnStrings 适配 nebula-go v3 取出字符串类型列
+func getFirstColumnStrings(resp *nebula.ResultSet) ([]string, error) {
+	if resp == nil {
+		return nil, fmt.Errorf("result set is nil")
+	}
+
+	var values []string
+	for _, row := range resp.GetRows() {
+		if len(row.Values) == 0 {
+			continue
+		}
+		val := row.Values[0]
+		switch {
+		case val.SVal != nil:
+			values = append(values, string(val.SVal))
+		case val.IVal != nil:
+			values = append(values, fmt.Sprintf("%d", *val.IVal))
+		case val.BVal != nil:
+			values = append(values, fmt.Sprintf("%v", *val.BVal))
+		case val.PVal != nil:
+			// 处理点类型
+			//src := val.PVal.GetSrc()
+			//if src.GetId != nil {
+			//	values = append(values, string(*src.SVal))
+			//} else if src.IVal != nil {
+			//	values = append(values, fmt.Sprintf("%d", *src.IVal))
+			//} else {
+			//	log.Printf("未知的点源 ID 类型: %+v", src)
+			//}
+		default:
+			log.Printf("未知类型值: %+v", val)
+		}
+	}
+	return values, nil
+}
+
+func reverseMap(m map[string]string) map[string]string {
+	result := make(map[string]string)
+	for k, v := range m {
+		result[v] = k
+	}
+	return result
+}
+
+func convertPathToNames(path []string, vidToName map[string]string) []string {
+	readablePath := make([]string, 0, len(path))
+	for i := 0; i < len(path)-1; i++ {
+		fromName, okFrom := vidToName[path[i]]
+		toName, okTo := vidToName[path[i+1]]
+		if okFrom && okTo && fromName != toName {
+			readablePath = append(readablePath, fmt.Sprintf("%s -> %s", fromName, toName))
+		}
+	}
+	return readablePath
+}
+
+func getAncestorName(session *nebula.Session, ancestorVid string, vidToName map[string]string) string {
+	if name, ok := vidToName[ancestorVid]; ok {
+		return name
+	}
+	query := fmt.Sprintf(`
+USE `+Table_Space+`;
+FETCH PROP ON Legal "%s" YIELD Legal.name;
+`, ancestorVid)
+	resp, err := session.Execute(query)
+	if err != nil {
+		log.Printf("获取祖先公司名称失败: %v", err)
+		return ""
+	}
+	names, err := getFirstColumnStrings(resp)
+	if err != nil || len(names) == 0 {
+		return ""
+	}
+	return names[0]
+}