go: Gale-Shapley Algorithm
项目结构:
/* # 版权所有 2026 ©涂聚文有限公司™ ® # 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎 # 描述:Gale-Shapley Algorithm # Author : geovindu,Geovin Du 涂聚文. # IDE : goLang 2024.3.6 go 26.2 # os : windows 10 # database : mysql 9.0 sql server 2019, postgreSQL 17.0 Oracle 21c Neo4j # Datetime : 2026/7/30 21:01 # User : geovindu # Product : GoLand # Project : goalgorithms # File : config.go */ package config import "os" var ( BASE_DIR string OUTPUT_DIR string LOG_DIR string ) func init() { BASE_DIR, _ = os.Getwd() OUTPUT_DIR = BASE_DIR + "/output" LOG_DIR = BASE_DIR + "/logs" _ = os.MkdirAll(OUTPUT_DIR, 0755) _ = os.MkdirAll(LOG_DIR, 0755) } // 全局匹配配置 var MATCH_CONFIG = struct { REVERSE_MODE bool // true=反向模式:供给方求婚(企业优先) MAX_MATCH_ROUND int // 最大迭代轮次 ENABLE_PERF_LOG bool // 打印性能日志 EXPORT_EXCEL bool // 导出Excel EXPORT_PLOT bool // 生成可视化图片 }{ REVERSE_MODE: true, MAX_MATCH_ROUND: 20000, ENABLE_PERF_LOG: true, EXPORT_EXCEL: true, EXPORT_PLOT: true, } // 绘图配置 var PLOT_CONFIG = struct { FontName string DPI int FigureWidth float64 FigureHeight float64 MaxDrawLinkCount int }{ FontName: "SimHei", DPI: 150, FigureWidth: 14, FigureHeight: 7, MaxDrawLinkCount: 200, } /* # 版权所有 2026 ©涂聚文有限公司™ ® # 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎 # 描述:Gale-Shapley Algorithm # Author : geovindu,Geovin Du 涂聚文. # IDE : goLang 2024.3.6 go 26.2 # os : windows 10 # database : mysql 9.0 sql server 2019, postgreSQL 17.0 Oracle 21c Neo4j # Datetime : 2026/7/30 21:12 # User : geovindu # Product : GoLand # Project : goalgorithms # File : entity.go */ package models import "time" // MatchSubject 匹配主体通用实体 type MatchSubject struct { ID string `json:"id"` Name string `json:"name"` Score float64 `json:"score"` Attrs map[string]float64 `json:"attrs"` //业务扩展属性 } // PreferenceList 偏好序列 type PreferenceList struct { OwnerID string `json:"owner_id"` RankedIDs []string `json:"ranked_ids"` //从高到低排序ID } // MatchResultItem 单条匹配结果 type MatchResultItem struct { ProposerID string `json:"proposer_id"` ProposerName string `json:"proposer_name"` AcceptorID string `json:"acceptor_id"` AcceptorName string `json:"acceptor_name"` MatchScore float64 `json:"match_score"` } // MatchOutput 统一输出载体 type MatchOutput struct { SceneName string `json:"scene_name"` ReverseMode bool `json:"reverse_mode"` MatchList []MatchResultItem `json:"match_list"` UnmatchedProposers []MatchSubject `json:"unmatched_proposers"` UnmatchedAcceptors []MatchSubject `json:"unmatched_acceptors"` TotalCostMs float64 `json:"total_cost_ms"` CreateTime time.Time `json:"create_time"` } /* # 版权所有 2026 ©涂聚文有限公司™ ® # 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎 # 描述:Gale-Shapley Algorithm # Author : geovindu,Geovin Du 涂聚文. # IDE : goLang 2024.3.6 go 26.2 # os : windows 10 # database : mysql 9.0 sql server 2019, postgreSQL 17.0 Oracle 21c Neo4j # Datetime : 2026/7/30 21:23 # User : geovindu # Product : GoLand # Project : goalgorithms # File : gale_shapley.go */ package core import ( "fmt" "goalgorithms/galeshapley/config" "goalgorithms/galeshapley/models" "time" ) type GaleShapleySolver struct { ReverseMode bool maxRound int } func NewGaleShapleySolver(reverseMode bool) *GaleShapleySolver { return &GaleShapleySolver{ ReverseMode: reverseMode, maxRound: config.MATCH_CONFIG.MAX_MATCH_ROUND, } } // Solve 执行GS稳定匹配 // 返回:匹配map[求婚方ID]接收方ID,未匹配求婚方集合,未匹配接收方集合,耗时ms func (s *GaleShapleySolver) Solve(proposerPrefs, acceptorPrefs []models.PreferenceList) (map[string]string, map[string]bool, map[string]bool, float64) { start := time.Now() // 构建偏好缓存 propPrefMap := make(map[string][]string) for _, p := range proposerPrefs { propPrefMap[p.OwnerID] = p.RankedIDs } accPrefMap := make(map[string][]string) accRankCache := make(map[string]map[string]int) for _, a := range acceptorPrefs { accPrefMap[a.OwnerID] = a.RankedIDs accRankCache[a.OwnerID] = make(map[string]int) for idx, sid := range a.RankedIDs { accRankCache[a.OwnerID][sid] = idx } } // 初始化状态 freeProposers := make(map[string]bool) nextProposeIdx := make(map[string]int) for pid := range propPrefMap { freeProposers[pid] = true nextProposeIdx[pid] = 0 } matchAcceptor := make(map[string]string) // acceptor -> proposer finalMatch := make(map[string]string) // proposer -> acceptor roundCnt := 0 for len(freeProposers) > 0 && roundCnt < s.maxRound { roundCnt++ var pid string // 取出任意一个自由求婚方 for k := range freeProposers { pid = k break } delete(freeProposers, pid) prefList := propPrefMap[pid] idx := nextProposeIdx[pid] if idx >= len(prefList) { continue } targetAid := prefList[idx] nextProposeIdx[pid]++ if incumbent, exist := matchAcceptor[targetAid]; !exist { // 接收方单身 matchAcceptor[targetAid] = pid finalMatch[pid] = targetAid } else { // 已有配对,比较优先级 rankNew := accRankCache[targetAid][pid] rankOld := accRankCache[targetAid][incumbent] if rankNew < rankOld { // 更换匹配 freeProposers[incumbent] = true matchAcceptor[targetAid] = pid finalMatch[pid] = targetAid delete(finalMatch, incumbent) } else { freeProposers[pid] = true } } } // 计算未匹配 usedAcceptors := make(map[string]bool) for _, aid := range matchAcceptor { usedAcceptors[aid] = true } unmatchedAcceptors := make(map[string]bool) for aid := range accPrefMap { if !usedAcceptors[aid] { unmatchedAcceptors[aid] = true } } unmatchedProposers := freeProposers costMs := float64(time.Since(start).Microseconds()) / 1000.0 if config.MATCH_CONFIG.ENABLE_PERF_LOG { fmt.Printf("[GS算法耗时] %.2f ms | 迭代轮数:%d\n", costMs, roundCnt) } return finalMatch, unmatchedProposers, unmatchedAcceptors, costMs } /* # 版权所有 2026 ©涂聚文有限公司™ ® # 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎 # 描述:Gale-Shapley Algorithm # Author : geovindu,Geovin Du 涂聚文. # IDE : goLang 2024.3.6 go 26.2 # os : windows 10 # database : mysql 9.0 sql server 2019, postgreSQL 17.0 Oracle 21c Neo4j # Datetime : 2026/7/30 21:13 # User : geovindu # Product : GoLand # Project : goalgorithms # File : score_engine.go */ package service import ( "goalgorithms/galeshapley/models" "sort" ) type ScoreFunc func(self, candidate models.MatchSubject) float64 // BuildPreference 单个主体生成偏好列表 func BuildPreference(self models.MatchSubject, candidates []models.MatchSubject, scoreFunc ScoreFunc) models.PreferenceList { type pair struct { score float64 id string } var list []pair for _, c := range candidates { s := scoreFunc(self, c) list = append(list, pair{score: s, id: c.ID}) } // 分数降序 sort.Slice(list, func(i, j int) bool { return list[i].score > list[j].score }) ranked := make([]string, 0, len(list)) for _, item := range list { ranked = append(ranked, item.id) } return models.PreferenceList{ OwnerID: self.ID, RankedIDs: ranked, } } // BatchGeneratePreference 批量生成偏好 func BatchGeneratePreference(main []models.MatchSubject, pool []models.MatchSubject, scoreFunc ScoreFunc) []models.PreferenceList { res := make([]models.PreferenceList, 0, len(main)) for _, item := range main { res = append(res, BuildPreference(item, pool, scoreFunc)) } return res } // ====================四大场景打分函数==================== // 场景1:工匠 ↔ 定制订单 func ScoreArtisanOrder(artisan, order models.MatchSubject) float64 { profit := order.Attrs["profit"] skillMatch := order.Attrs["skill_match"] return 0.6*profit + 0.4*skillMatch } func ScoreOrderArtisan(order, artisan models.MatchSubject) float64 { delivery := artisan.Attrs["delivery_score"] craft := artisan.Attrs["craft_level"] return 0.5*delivery + 0.5*craft } // 场景2:销售顾问 ↔ 高端客户 func ScoreSalesCustomer(sales, customer models.MatchSubject) float64 { consume := customer.Attrs["consume_level"] styleFit := customer.Attrs["style_fit"] return 0.7*consume + 0.3*styleFit } func ScoreCustomerSales(customer, sales models.MatchSubject) float64 { service := sales.Attrs["service_score"] pro := sales.Attrs["profession"] return 0.5*service + 0.5*pro } // 场景3:原料供应商 ↔ 加工厂采购 func ScoreSupplierFactory(supplier, factory models.MatchSubject) float64 { amount := factory.Attrs["order_amount"] payScore := factory.Attrs["payment_score"] return 0.7*amount + 0.3*payScore } func ScoreFactorySupplier(factory, supplier models.MatchSubject) float64 { price := supplier.Attrs["price_score"] stable := supplier.Attrs["stable_score"] return 0.6*price + 0.4*stable } // 场景4:设计师 ↔ 新品需求 func ScoreDesignerDemand(designer, demand models.MatchSubject) float64 { bonus := demand.Attrs["project_bonus"] familiar := demand.Attrs["style_familiar"] return 0.65*bonus + 0.35*familiar } func ScoreDemandDesigner(demand, designer models.MatchSubject) float64 { return designer.Attrs["success_rate"] } /* # 版权所有 2026 ©涂聚文有限公司™ ® # 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎 # 描述:Gale-Shapley Algorithm # Author : geovindu,Geovin Du 涂聚文. # IDE : goLang 2024.3.6 go 26.2 # os : windows 10 # database : mysql 9.0 sql server 2019, postgreSQL 17.0 Oracle 21c Neo4j # Datetime : 2026/7/30 21:25 # User : geovindu # Product : GoLand # Project : goalgorithms # File : jewelry_scene.go */ package service import ( "fmt" "github.com/google/uuid" "goalgorithms/galeshapley/config" "goalgorithms/galeshapley/core" "goalgorithms/galeshapley/models" "math/rand" "time" ) func init() { rand.Seed(time.Now().UnixNano()) } func generateID() string { return uuid.NewString()[:8] } // Scene1ArtisanOrder 工匠-定制订单 func Scene1ArtisanOrder(scale int) models.MatchOutput { fmt.Printf("\n=====【场景1:工匠-定制珠宝订单匹配】规模:%d =====\n", scale) var artisans []models.MatchSubject var orders []models.MatchSubject for i := 0; i < scale; i++ { art := models.MatchSubject{ ID: generateID(), Name: fmt.Sprintf("工匠_%04d", i+1), Score: randFloat(60, 95), Attrs: map[string]float64{ "delivery_score": randFloat(50, 100), "craft_level": randFloat(50, 100), }, } artisans = append(artisans, art) od := models.MatchSubject{ ID: generateID(), Name: fmt.Sprintf("定制订单_%04d", i+1), Score: randFloat(50, 98), Attrs: map[string]float64{ "profit": randFloat(10, 100), "skill_match": randFloat(0, 100), }, } orders = append(orders, od) } reverseMode := config.MATCH_CONFIG.REVERSE_MODE var proposers, acceptors []models.MatchSubject var propScore, accScore ScoreFunc if reverseMode { proposers, acceptors = artisans, orders propScore, accScore = ScoreArtisanOrder, ScoreOrderArtisan } else { proposers, acceptors = orders, artisans propScore, accScore = ScoreOrderArtisan, ScoreArtisanOrder } propPrefs := BatchGeneratePreference(proposers, acceptors, propScore) accPrefs := BatchGeneratePreference(acceptors, proposers, accScore) solver := core.NewGaleShapleySolver(reverseMode) matchMap, unmatchPropIDs, unmatchAccIDs, costMs := solver.Solve(propPrefs, accPrefs) return buildOutput( "工匠_定制订单", proposers, acceptors, matchMap, unmatchPropIDs, unmatchAccIDs, reverseMode, costMs, ) } // Scene2SalesCustomer 销售-高端客户 func Scene2SalesCustomer(scale int) models.MatchOutput { fmt.Printf("\n=====【场景2:销售顾问-高端客户匹配】规模:%d =====\n", scale) var sales []models.MatchSubject var customers []models.MatchSubject for i := 0; i < scale; i++ { s := models.MatchSubject{ ID: generateID(), Name: fmt.Sprintf("销售_%04d", i+1), Score: randFloat(60, 95), Attrs: map[string]float64{ "service_score": randFloat(50, 100), "profession": randFloat(50, 100), }, } sales = append(sales, s) c := models.MatchSubject{ ID: generateID(), Name: fmt.Sprintf("高端客户_%04d", i+1), Score: randFloat(55, 99), Attrs: map[string]float64{ "consume_level": randFloat(10, 100), "style_fit": randFloat(0, 100), }, } customers = append(customers, c) } reverseMode := config.MATCH_CONFIG.REVERSE_MODE var proposers, acceptors []models.MatchSubject var propScore, accScore ScoreFunc if reverseMode { proposers, acceptors = sales, customers propScore, accScore = ScoreSalesCustomer, ScoreCustomerSales } else { proposers, acceptors = customers, sales propScore, accScore = ScoreCustomerSales, ScoreSalesCustomer } propPrefs := BatchGeneratePreference(proposers, acceptors, propScore) accPrefs := BatchGeneratePreference(acceptors, proposers, accScore) solver := core.NewGaleShapleySolver(reverseMode) matchMap, unmatchPropIDs, unmatchAccIDs, costMs := solver.Solve(propPrefs, accPrefs) return buildOutput( "销售_高端客户", proposers, acceptors, matchMap, unmatchPropIDs, unmatchAccIDs, reverseMode, costMs, ) } // Scene3SupplierFactory 供应商-采购单 func Scene3SupplierFactory(scale int) models.MatchOutput { fmt.Printf("\n=====【场景3:原料供应商-加工厂采购单】规模:%d =====\n", scale) var suppliers []models.MatchSubject var factories []models.MatchSubject for i := 0; i < scale; i++ { sp := models.MatchSubject{ ID: generateID(), Name: fmt.Sprintf("原料供应商_%04d", i+1), Score: randFloat(60, 96), Attrs: map[string]float64{ "price_score": randFloat(40, 100), "stable_score": randFloat(40, 100), }, } suppliers = append(suppliers, sp) fac := models.MatchSubject{ ID: generateID(), Name: fmt.Sprintf("加工厂采购_%04d", i+1), Score: randFloat(50, 97), Attrs: map[string]float64{ "order_amount": randFloat(20, 100), "payment_score": randFloat(30, 100), }, } factories = append(factories, fac) } reverseMode := config.MATCH_CONFIG.REVERSE_MODE var proposers, acceptors []models.MatchSubject var propScore, accScore ScoreFunc if reverseMode { proposers, acceptors = suppliers, factories propScore, accScore = ScoreSupplierFactory, ScoreFactorySupplier } else { proposers, acceptors = factories, suppliers propScore, accScore = ScoreFactorySupplier, ScoreSupplierFactory } propPrefs := BatchGeneratePreference(proposers, acceptors, propScore) accPrefs := BatchGeneratePreference(acceptors, proposers, accScore) solver := core.NewGaleShapleySolver(reverseMode) matchMap, unmatchPropIDs, unmatchAccIDs, costMs := solver.Solve(propPrefs, accPrefs) return buildOutput( "原料供应商_加工厂", proposers, acceptors, matchMap, unmatchPropIDs, unmatchAccIDs, reverseMode, costMs, ) } // Scene4DesignerDemand 设计师-新品需求 func Scene4DesignerDemand(scale int) models.MatchOutput { fmt.Printf("\n=====【场景4:珠宝设计师_新品开发需求】规模:%d =====\n", scale) var designers []models.MatchSubject var demands []models.MatchSubject for i := 0; i < scale; i++ { des := models.MatchSubject{ ID: generateID(), Name: fmt.Sprintf("设计师_%04d", i+1), Score: randFloat(65, 96), Attrs: map[string]float64{ "success_rate": randFloat(40, 100), }, } designers = append(designers, des) dm := models.MatchSubject{ ID: generateID(), Name: fmt.Sprintf("新品项目_%04d", i+1), Score: randFloat(55, 98), Attrs: map[string]float64{ "project_bonus": randFloat(10, 90), "style_familiar": randFloat(0, 100), }, } demands = append(demands, dm) } reverseMode := config.MATCH_CONFIG.REVERSE_MODE var proposers, acceptors []models.MatchSubject var propScore, accScore ScoreFunc if reverseMode { proposers, acceptors = designers, demands propScore, accScore = ScoreDesignerDemand, ScoreDemandDesigner } else { proposers, acceptors = demands, designers propScore, accScore = ScoreDemandDesigner, ScoreDesignerDemand } propPrefs := BatchGeneratePreference(proposers, acceptors, propScore) accPrefs := BatchGeneratePreference(acceptors, proposers, accScore) solver := core.NewGaleShapleySolver(reverseMode) matchMap, unmatchPropIDs, unmatchAccIDs, costMs := solver.Solve(propPrefs, accPrefs) return buildOutput( "设计师_新品需求", proposers, acceptors, matchMap, unmatchPropIDs, unmatchAccIDs, reverseMode, costMs, ) } func buildOutput( sceneName string, proposers, acceptors []models.MatchSubject, matchMap map[string]string, unmatchPropIDs, unmatchAccIDs map[string]bool, reverseMode bool, costMs float64, ) models.MatchOutput { propMap := make(map[string]models.MatchSubject) for _, p := range proposers { propMap[p.ID] = p } accMap := make(map[string]models.MatchSubject) for _, a := range acceptors { accMap[a.ID] = a } var matchList []models.MatchResultItem for pid, aid := range matchMap { p := propMap[pid] a := accMap[aid] matchList = append(matchList, models.MatchResultItem{ ProposerID: pid, ProposerName: p.Name, AcceptorID: aid, AcceptorName: a.Name, MatchScore: (p.Score + a.Score) / 2.0, }) } // 未匹配 var unmatchProp []models.MatchSubject for id := range unmatchPropIDs { unmatchProp = append(unmatchProp, propMap[id]) } var unmatchAcc []models.MatchSubject for id := range unmatchAccIDs { unmatchAcc = append(unmatchAcc, accMap[id]) } fmt.Printf("匹配成功数量:%d | 求婚方未匹配:%d | 接收方未匹配:%d\n", len(matchList), len(unmatchProp), len(unmatchAcc)) return models.MatchOutput{ SceneName: sceneName, ReverseMode: reverseMode, MatchList: matchList, UnmatchedProposers: unmatchProp, UnmatchedAcceptors: unmatchAcc, TotalCostMs: costMs, CreateTime: time.Now(), } } func randFloat(min, max float64) float64 { return min + rand.Float64()*(max-min) } /* # 版权所有 2026 ©涂聚文有限公司™ ® # 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎 # 描述:Gale-Shapley Algorithm # Author : geovindu,Geovin Du 涂聚文. # IDE : goLang 2024.3.6 go 26.2 # os : windows 10 # database : mysql 9.0 sql server 2019, postgreSQL 17.0 Oracle 21c Neo4j # Datetime : 2026/7/30 21:16 # User : geovindu # Product : GoLand # Project : goalgorithms # File : excel_exporter.go */ package utils import ( "fmt" "github.com/xuri/excelize/v2" "goalgorithms/galeshapley/config" "goalgorithms/galeshapley/models" "path/filepath" "time" ) func ExportExcel(output models.MatchOutput) string { t := time.Now().Format("20060102_150405") fileName := fmt.Sprintf("匹配结果_%s_%s.xlsx", output.SceneName, t) savePath := filepath.Join(config.OUTPUT_DIR, fileName) f := excelize.NewFile() defer f.Close() // Sheet1 匹配明细 sheet1 := "匹配明细" _ = f.SetSheetName("Sheet1", sheet1) headers := []string{"求婚方ID", "求婚方名称", "接收方ID", "接收方名称", "匹配综合得分"} for col, h := range headers { cell, _ := excelize.CoordinatesToCellName(col+1, 1) _ = f.SetCellValue(sheet1, cell, h) } for rowIdx, item := range output.MatchList { r := rowIdx + 2 _ = f.SetCellValue(sheet1, fmt.Sprintf("A%d", r), item.ProposerID) _ = f.SetCellValue(sheet1, fmt.Sprintf("B%d", r), item.ProposerName) _ = f.SetCellValue(sheet1, fmt.Sprintf("C%d", r), item.AcceptorID) _ = f.SetCellValue(sheet1, fmt.Sprintf("D%d", r), item.AcceptorName) _ = f.SetCellValue(sheet1, fmt.Sprintf("E%d", r), item.MatchScore) } // Sheet2 未匹配求婚方 sheet2 := "未匹配求婚方" _, _ = f.NewSheet(sheet2) _ = f.SetCellValue(sheet2, "A1", "ID") _ = f.SetCellValue(sheet2, "B1", "名称") _ = f.SetCellValue(sheet2, "C1", "综合分数") for idx, s := range output.UnmatchedProposers { r := idx + 2 _ = f.SetCellValue(sheet2, fmt.Sprintf("A%d", r), s.ID) _ = f.SetCellValue(sheet2, fmt.Sprintf("B%d", r), s.Name) _ = f.SetCellValue(sheet2, fmt.Sprintf("C%d", r), s.Score) } // Sheet3 未匹配接收方 sheet3 := "未匹配接收方" _, _ = f.NewSheet(sheet3) _ = f.SetCellValue(sheet3, "A1", "ID") _ = f.SetCellValue(sheet3, "B1", "名称") _ = f.SetCellValue(sheet3, "C1", "综合分数") for idx, s := range output.UnmatchedAcceptors { r := idx + 2 _ = f.SetCellValue(sheet3, fmt.Sprintf("A%d", r), s.ID) _ = f.SetCellValue(sheet3, fmt.Sprintf("B%d", r), s.Name) _ = f.SetCellValue(sheet3, fmt.Sprintf("C%d", r), s.Score) } // Sheet4 运行摘要 sheet4 := "运行摘要" _, _ = f.NewSheet(sheet4) summary := [][]interface{}{ {"场景名称", output.SceneName}, {"反向模式(企业优先)", output.ReverseMode}, {"成功匹配数", len(output.MatchList)}, {"未匹配求婚方", len(output.UnmatchedProposers)}, {"未匹配接收方", len(output.UnmatchedAcceptors)}, {"算法耗时(ms)", fmt.Sprintf("%.2f", output.TotalCostMs)}, } for row, line := range summary { _ = f.SetCellValue(sheet4, fmt.Sprintf("A%d", row+1), line[0]) _ = f.SetCellValue(sheet4, fmt.Sprintf("B%d", row+1), line[1]) } _ = f.SaveAs(savePath) fmt.Printf("\n✅ Excel已导出:%s\n", savePath) return savePath } /* # 版权所有 2026 ©涂聚文有限公司™ ® # 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎 # 描述:Gale-Shapley Algorithm # Author : geovindu,Geovin Du 涂聚文. # IDE : goLang 2024.3.6 go 26.2 # os : windows 10 # database : mysql 9.0 sql server 2019, postgreSQL 17.0 Oracle 21c Neo4j # Datetime : 2026/7/30 21:17 # User : geovindu # Product : GoLand # Project : goalgorithms # File : visualizer.go */ package utils import ( "fmt" "goalgorithms/galeshapley/config" "goalgorithms/galeshapley/models" "image/color" "os" "path/filepath" "time" "golang.org/x/image/font/opentype" plot "gonum.org/v1/plot" plotfont "gonum.org/v1/plot/font" "gonum.org/v1/plot/plotter" "gonum.org/v1/plot/plotutil" "gonum.org/v1/plot/text" "gonum.org/v1/plot/vg" plotdraw "gonum.org/v1/plot/vg/draw" ) func DrawAllCharts(out models.MatchOutput) ([]string, error) { var paths []string p1, err := drawScoreHist(out) if err != nil { return nil, err } paths = append(paths, p1) // 连线图文字渲染依赖字体,生产环境可按需开启 fmt.Printf("✅ 图表生成完成:%v\n", paths) return paths, nil } func loadChineseFont() (*plotfont.Cache, error) { fontPaths := []string{ `C:\Windows\Fonts\msyh.ttc`, // 微软雅黑 `C:\Windows\Fonts\simsun.ttc`, // 宋体 `C:\Windows\Fonts\simhei.ttf`, // 黑体 (如果存在) } for _, fontPath := range fontPaths { data, err := os.ReadFile(fontPath) if err != nil { continue } var f *opentype.Font // 尝试解析 TTC 集合 if len(data) > 4 && string(data[:4]) == "ttcf" { coll, err := opentype.ParseCollection(data) if err != nil { continue } if coll.NumFonts() > 0 { f, err = coll.Font(0) if err != nil { continue } } } else { // 尝试解析单个字体 f, err = opentype.Parse(data) if err != nil { continue } } if f == nil { continue } face := plotfont.Face{ Font: plotfont.Font{ Typeface: "ChineseFont", Style: 0, Weight: 400, Size: 14, }, Face: f, } cache := plotfont.NewCache(plotfont.Collection{face}) fmt.Printf(" ✓ 成功加载中文字体: %s\n", fontPath) return cache, nil } return nil, fmt.Errorf("未找到中文字体") } func drawScoreHist(out models.MatchOutput) (string, error) { t := time.Now().Format("20060102_150405") name := fmt.Sprintf("得分分布_%s_%s.png", out.SceneName, t) savePath := filepath.Join(config.OUTPUT_DIR, name) p := plot.New() p.Title.Text = fmt.Sprintf("【%s】匹配综合得分分布", out.SceneName) p.Title.Padding = vg.Points(10) p.X.Label.Text = "匹配综合得分" p.Y.Label.Text = "匹配对数" fontCache, err := loadChineseFont() if err == nil { handler := text.Plain{Fonts: fontCache} p.TextHandler = handler titleFont := plotfont.Font{ Typeface: "ChineseFont", Style: 0, Weight: 400, Size: 16, } labelFont := plotfont.Font{ Typeface: "ChineseFont", Style: 0, Weight: 400, Size: 12, } p.Title.TextStyle = text.Style{ Color: color.Black, Font: titleFont, XAlign: plotdraw.XCenter, YAlign: plotdraw.YTop, Handler: handler, } p.X.Label.TextStyle = text.Style{ Color: color.Black, Font: labelFont, Handler: handler, } p.Y.Label.TextStyle = text.Style{ Color: color.Black, Font: labelFont, Handler: handler, } } pts := make(plotter.Values, 0, len(out.MatchList)) for _, item := range out.MatchList { pts = append(pts, item.MatchScore) } hist, err := plotter.NewHist(pts, 25) if err != nil { return "", err } hist.FillColor = plotutil.Color(0) p.Add(hist) err = p.Save(vg.Length(config.PLOT_CONFIG.FigureWidth)*vg.Inch, vg.Length(config.PLOT_CONFIG.FigureHeight)*vg.Inch, savePath) return savePath, err }调用:
/* # 版权所有 2026 ©涂聚文有限公司™ ® # 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎 # 描述:Gale-Shapley Algorithm # Author : geovindu,Geovin Du 涂聚文. # IDE : goLang 2024.3.6 go 26.2 # os : windows 10 # database : mysql 9.0 sql server 2019, postgreSQL 17.0 Oracle 21c Neo4j # Datetime : 2026/7/30 21:28 # User : geovindu # Product : GoLand # Project : goalgorithms # File : galeshapleybll.go */ package bll import ( "goalgorithms/galeshapley/config" "goalgorithms/galeshapley/service" "goalgorithms/galeshapley/utils" ) func GaleshapleyMain() { // 模式切换开关 // config.MATCH_CONFIG.REVERSE_MODE = false //正向模式:需求方求婚 config.MATCH_CONFIG.REVERSE_MODE = true //反向模式:供给方求婚|企业收益优先 // 选择场景运行 result := service.Scene1ArtisanOrder(600) result2 := service.Scene2SalesCustomer(400) result3 := service.Scene3SupplierFactory(500) result4 := service.Scene4DesignerDemand(300) if config.MATCH_CONFIG.EXPORT_EXCEL { utils.ExportExcel(result) } if config.MATCH_CONFIG.EXPORT_EXCEL { utils.ExportExcel(result2) } if config.MATCH_CONFIG.EXPORT_EXCEL { utils.ExportExcel(result3) } if config.MATCH_CONFIG.EXPORT_EXCEL { utils.ExportExcel(result4) } if config.MATCH_CONFIG.EXPORT_PLOT { _, _ = utils.DrawAllCharts(result) } if config.MATCH_CONFIG.EXPORT_PLOT { _, _ = utils.DrawAllCharts(result2) } if config.MATCH_CONFIG.EXPORT_PLOT { _, _ = utils.DrawAllCharts(result3) } if config.MATCH_CONFIG.EXPORT_PLOT { _, _ = utils.DrawAllCharts(result4) } println("\n====================全部任务执行完成====================") }介绍了一个基于Gale-Shapley算法的稳定匹配系统实现,由涂聚文有限公司开发。该系统包含以下核心组件:
算法实现:通过GaleShapleySolver结构体实现稳定匹配算法,支持正向/反向模式,最大迭代轮次可配置。
业务场景适配:
- 工匠-定制订单匹配
- 销售顾问-高端客户匹配
- 原料供应商-加工厂匹配
- 设计师-新品需求匹配 每个场景提供独立的评分函数,支持权重定制。
功能特性:
- 自动生成测试数据
- 多维度评分机制
- 支持Excel结果导出
- 可视化图表生成(得分分布直方图)
技术架构:
- 采用Go语言开发
- 支持MySQL/SQL Server/PostgreSQL/Oracle/Neo4j数据库
- 模块化设计(核心算法、业务逻辑、工具类分离)
系统通过配置开关控制运行模式(企业优先/个人优先),适用于各类双边匹配场景,测试数据显示可高效处理数百规模的匹配任务。
输出:
