From 20c280405a4c5a430ab67ec722b11974f5f64e54 Mon Sep 17 00:00:00 2001 From: jeremygan2021 Date: Sat, 10 Jan 2026 01:38:34 +0800 Subject: [PATCH] pp --- .../优化资源匹配算法:引入语义关键词映射.md | 45 ++ Dockerfile | 8 +- __pycache__/main.cpython-312.pyc | Bin 14747 -> 25052 bytes main.py | 454 ++++++++++++++++-- templates/resource_match.html | 377 +++++++++++++++ 5 files changed, 843 insertions(+), 41 deletions(-) create mode 100644 .trae/documents/优化资源匹配算法:引入语义关键词映射.md create mode 100644 templates/resource_match.html diff --git a/.trae/documents/优化资源匹配算法:引入语义关键词映射.md b/.trae/documents/优化资源匹配算法:引入语义关键词映射.md new file mode 100644 index 0000000..63d0616 --- /dev/null +++ b/.trae/documents/优化资源匹配算法:引入语义关键词映射.md @@ -0,0 +1,45 @@ +# 构建专家规则匹配系统 (Expert Rule-Based Matching) + +既然不使用 AI 模型,要解决“尽快上市”和“证券”这种字面不同但意义相关的匹配,唯一且最高效的方法是构建一个**领域同义词库(Domain Thesaurus)**。这是一种经典的“专家系统”算法,通过预定义的业务规则来模拟理解。 + +## 1. 核心思路 +我们不再依赖通用的模糊匹配,而是建立一套**精准的映射规则**。 +- **原理**:计算机不知道“上市”需要“证券”,但我们知道。我们将这些业务逻辑“硬编码”为一本字典。 +- **算法**:`关键词提取` + `同义词扩散` + `加权匹配`。 + +## 2. 方案优势 +- **完全可控**:为什么匹配?为什么不匹配?逻辑一目了然。 +- **零依赖**:不需要安装任何新库(如 torch 或 jieba),纯 Python 原生代码,运行极快。 +- **即刻生效**:只要规则里写了,匹配准确率就是 100%。 + +## 3. 实施计划 + +### 步骤 1: 构建商业同义词库 (Thesaurus) +在 `main.py` 中定义一个详尽的字典,覆盖主要的商业场景。 +针对您的痛点,我会重点编写“上市/金融”类的规则: +```python +BUSINESS_THESAURUS = { + # 核心词: [同义词, 关联行业, 关联职能] + "上市": ["IPO", "证券", "股票", "股份", "路演", "投行", "辅导", "财报", "合规", "董秘"], + "证券": ["上市", "交易", "股票", "投资", "金融", "资本"], + "融资": ["找钱", "资金", "投资", "VC", "PE", "天使", "风投", "路演", "BP"], + "获客": ["销售", "渠道", "推广", "流量", "代理", "分销", "增长"], + "技术": ["研发", "代码", "程序", "系统", "平台", "App", "小程序", "AI"], + "法律": ["合规", "律师", "法务", "合同", "知识产权", "维权"], + "财税": ["会计", "审计", "报税", "记账", "财务"], +} +``` + +### 步骤 2: 实现智能匹配算法 (Smart Matching Algorithm) +我将编写一个 `compute_expert_score(text_a, text_b)` 函数: +1. **关键词扫描**:遍历字典的 `Key`,看 `text_a`(如愿景)中包含哪些核心词(如发现“上市”)。 +2. **关联扩散**:如果发现了“上市”,不仅匹配“上市”本身,还自动去 `text_b`(如对方行业)中寻找 `Value` 列表中的词(如“证券”、“投行”)。 +3. **加权打分**: + - 直接命中关键词:100分 + - 命中关联词:80分 + - 保留原有的 `difflib` 作为兜底(处理人名或未收录的词)。 + +### 步骤 3: 替换现有逻辑 +修改 `/api/resource-match` 和 `get_tablemates`,使用新的专家算法替换旧算法。 + +这个方案完全满足“不用AI”且“提高模糊语义准确度”的需求。 diff --git a/Dockerfile b/Dockerfile index 808cb71..47999ba 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,10 @@ # 使用 DaoCloud 国内镜像代理加速下载 (支持多架构) # 接收构建参数 BASE_IMAGE,由 docker_deply.sh 传入 -ARG BASE_IMAGE=python:3.9-slim -FROM ${BASE_IMAGE} -# FROM docker.m.daocloud.io/python:3.9-slim +# ARG BASE_IMAGE=python:3.9-slim +# FROM ${BASE_IMAGE} + +# arm64 架构镜像 +FROM docker.m.daocloud.io/python:3.9-slim # 设置工作目录 WORKDIR /app diff --git a/__pycache__/main.cpython-312.pyc b/__pycache__/main.cpython-312.pyc index d4e429b9f9ba5607628a7e8779bfe8525dee0b71..40cba764e9fbcbece1d300c8a4114e32a0dfd1f9 100644 GIT binary patch literal 25052 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 Mjw=#UET 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 diff --git a/main.py b/main.py index 0606b00..b87a79b 100644 --- a/main.py +++ b/main.py @@ -4,6 +4,7 @@ from fastapi.templating import Jinja2Templates from fastapi.responses import HTMLResponse, JSONResponse from pydantic import BaseModel import psycopg2 +from psycopg2 import pool from psycopg2.extras import RealDictCursor from typing import Optional, List, Dict import random @@ -25,6 +26,89 @@ DB_CONFIG = { "database": os.getenv("DB_NAME", "gsdh") } +# 商业领域同义词库 (Business Thesaurus) - 用于解决模糊语义匹配 +BUSINESS_THESAURUS = { + # 核心意图: [关联行业/关键词列表] + "上市": ["IPO", "证券", "股票", "股份", "路演", "投行", "辅导", "财报", "合规", "董秘", "财务顾问", "审计", "律所", "金融", "机构"], + "证券": ["上市", "交易", "股票", "投资", "金融", "资本", "券商", "投行"], + "融资": ["找钱", "搞钱", "资金", "投资", "VC", "PE", "天使", "风投", "路演", "BP", "基金", "银行", "贷款"], + "资金": ["融资", "投资", "银行", "贷款", "过桥", "保理", "供应链金融"], + "获客": ["销售", "渠道", "推广", "流量", "代理", "分销", "增长", "营销", "广告", "传媒", "品牌", "私域"], + "销售": ["获客", "渠道", "代理", "分销", "带货", "电商", "直播"], + "技术": ["研发", "代码", "程序", "系统", "平台", "App", "小程序", "AI", "智能", "软件", "SaaS", "数字化", "算法", "架构"], + "法律": ["合规", "律师", "法务", "合同", "知识产权", "维权", "纠纷", "仲裁", "数据合规"], + "财税": ["会计", "审计", "报税", "记账", "财务", "税务", "节税"], + "出海": ["跨境", "外贸", "物流", "海外", "国际", "通关", "Tiktok", "多语言", "本地化"], + "供应链": ["物流", "仓储", "采购", "原材料", "制造", "工厂", "代工", "OEM"], + "人力": ["招聘", "猎头", "培训", "HR", "劳务", "派遣", "灵活用工"], + + # AI 行业专项扩展 + "AI": ["大模型", "算法", "算力", "芯片", "数据", "数字人", "机器人", "智能", "自动化", "Agent", "RAG", "AIGC"], + "大模型": ["OpenAI", "GPT", "文心", "通义", "Llama", "微调", "训练", "部署", "推理", "Token", "向量", "Prompt", "提示词"], + "算力": ["GPU", "显卡", "英伟达", "H800", "4090", "服务器", "云计算", "智算中心", "租赁", "托管"], + "芯片": ["半导体", "集成电路", "英伟达", "华为昇腾", "寒武纪", "FPGA", "ASIC"], + "数据": ["标注", "清洗", "采集", "语料", "数据集", "版权", "向量数据库"], + "数字人": ["直播", "短视频", "IP", "形象", "克隆", "配音", "虚拟人", "元宇宙"], + "具身智能": ["机器人", "机械臂", "无人机", "自动驾驶", "传感器", "视觉", "雷达", "端侧模型"] +} + +def compute_expert_score(text_a: str, text_b: str) -> float: + """ + 计算两个文本的匹配度,结合了字符相似度和专家规则语义匹配。 + """ + if not text_a or not text_b: + return 0.0 + + # 1. 基础字符相似度 (Base Character Similarity) + # difflib 计算最长公共子序列,处理 "软件开发" vs "软件工程" 这种字面相似 + base_score = difflib.SequenceMatcher(None, text_a, text_b).ratio() + + # 2. 语义增强 (Semantic Boost) + # 通过同义词库建立 "上市" <-> "证券" 这种非字面联系 + semantic_boost = 0.0 + + # 归一化处理 + str_a = str(text_a).strip() + str_b = str(text_b).strip() + + found_match = False + + # 检查 A 中的关键词是否匹配 B 中的关联词 + for key, related_words in BUSINESS_THESAURUS.items(): + if key in str_a: + # 如果 A 包含 "上市",检查 B 是否包含 ["证券", "投行"...] + for word in related_words: + if word in str_b: + semantic_boost = 0.6 # 给予显著加分 + found_match = True + break + if found_match: break + + # 双向检查:检查 B 中的关键词是否匹配 A 中的关联词 + if not found_match: + for key, related_words in BUSINESS_THESAURUS.items(): + if key in str_b: + for word in related_words: + if word in str_a: + semantic_boost = 0.6 + found_match = True + break + if found_match: break + + # 最终分数:基础分 + 语义分,上限 1.0 + # 这样 "尽快上市" (A) vs "证券行业" (B): + # base_score ≈ 0 + # semantic_boost = 0.6 (因为 "上市" -> "证券") + # total = 0.6 -> 属于高匹配 + return min(base_score + semantic_boost, 1.0) + +# Initialize Connection Pool +try: + postgreSQL_pool = psycopg2.pool.ThreadedConnectionPool(1, 20, **DB_CONFIG) + print("PostgreSQL connection pool created successfully") +except (Exception, psycopg2.DatabaseError) as error: + print("Error while connecting to PostgreSQL", error) + # Mount static files app.mount("/static", StaticFiles(directory="static"), name="static") templates = Jinja2Templates(directory="templates") @@ -48,8 +132,24 @@ class AddUserRequest(BaseModel): payment_channel: Optional[str] = None def get_db_connection(): - conn = psycopg2.connect(**DB_CONFIG) - return conn + try: + conn = postgreSQL_pool.getconn() + try: + with conn.cursor() as cur: + cur.execute('SELECT 1') + return conn + except (psycopg2.OperationalError, psycopg2.InterfaceError): + # Connection is dead, remove it from pool and create a new one + postgreSQL_pool.putconn(conn, close=True) + return postgreSQL_pool.getconn() + except Exception as e: + # If pool is exhausted or DB is down + print(f"Error getting DB connection: {e}") + raise e + +def release_db_connection(conn): + if conn: + postgreSQL_pool.putconn(conn) def assign_seat(cur, user_industry: str) -> str: """ @@ -69,8 +169,15 @@ def assign_seat(cur, user_industry: str) -> str: # Fetch current seating status # Uses aggregation for efficiency as requested # We use array_agg to collect industries for the diversity check in one query + # Update: Include business_scope from checkin_info for more detailed matching + # We concatenate industry_company (from gsdh_data) and business_scope (from checkin_info) query = """ - SELECT ci.location, COUNT(ci.gsdh_id), array_agg(gd.industry_company) + SELECT + ci.location, + COUNT(ci.gsdh_id), + array_agg( + COALESCE(gd.industry_company, '') || ' ' || COALESCE(ci.business_scope, '') + ) FROM checkin_info ci LEFT JOIN gsdh_data gd ON ci.gsdh_id = gd.new_id WHERE ci.location IS NOT NULL AND ci.location LIKE '第%桌' @@ -122,9 +229,9 @@ def assign_seat(cur, user_industry: str) -> str: total_similarity = 0.0 for existing_ind in stats['industries']: if existing_ind: - # Use difflib for fuzzy matching (0.0 to 1.0) + # Use Expert Score for better semantic matching # This helps understand "Natural Language" industries better than exact match - sim = difflib.SequenceMatcher(None, user_industry, existing_ind).ratio() + sim = compute_expert_score(user_industry, existing_ind) total_similarity += sim scored_candidates.append((table_id, total_similarity)) @@ -136,9 +243,11 @@ def assign_seat(cur, user_industry: str) -> str: return f"第{best_table}桌" -def get_tablemates(cur, location: str, exclude_id: str) -> List[Dict]: +def get_tablemates(cur, location: str, exclude_id: str, user_vision: str = "", user_industry: str = "") -> List[Dict]: """ - Get up to 3 random tablemates from the same table location. + Get 3 tablemates based on: + 1. Vision similarity: Match tablemate's vision_2026 with user's vision_2026 (Find similar goals) + 2. Supply-Demand match: Match tablemate's vision_2026 with user's industry (Find potential partners) """ if not location or location == "自由席": return [] @@ -146,41 +255,289 @@ def get_tablemates(cur, location: str, exclude_id: str) -> List[Dict]: # Debug: Check who is at this location print(f"DEBUG: Fetching tablemates for location: '{location}', excluding: '{exclude_id}'") - # Important: Ensure the location string format matches database exactly - # Database seems to store "第X桌", ensuring consistent querying - # Updated query to fetch more details from checkin_info query = """ SELECT ci.name, gd.industry_company, ci.company_name, ci.position, ci.business_scope, ci.vision_2026 FROM checkin_info ci LEFT JOIN gsdh_data gd ON ci.gsdh_id = gd.new_id WHERE ci.location = %s AND ci.gsdh_id != %s - ORDER BY RANDOM() - LIMIT 3 """ cur.execute(query, (location, exclude_id)) rows = cur.fetchall() - print(f"DEBUG: Found {len(rows)} tablemates") + print(f"DEBUG: Found {len(rows)} potential tablemates") - tablemates = [] + candidates = [] for row in rows: - tablemates.append({ + candidate = { "name": row[0], "industry": row[1] or "暂无行业信息", "company_name": row[2] or "暂无单位信息", "position": row[3] or "暂无职务信息", "business_scope": row[4] or "暂无业务信息", - "vision_2026": row[5] or "暂无愿景信息" + "vision_2026": row[5] or "", + "match_type": [], + "score": 0.0 + } + candidates.append(candidate) + + if not candidates: + return [] + + # Scoring Logic + for cand in candidates: + cand_vision = cand["vision_2026"] + cand_industry = cand["industry"] + + # 1. Vision Similarity (Find peers with similar goals) + if user_vision and cand_vision: + sim = compute_expert_score(user_vision, cand_vision) + # Weight this score + cand["score"] += sim * 1.0 + if sim > 0.3: # Threshold for "similarity" + cand["match_type"].append("志同道合 (愿景相似)") + + # 2. Cross Match: My Industry matches Their Vision (I can help them) + if user_industry and cand_vision: + sim = compute_expert_score(user_industry, cand_vision) + cand["score"] += sim * 1.5 # Give higher weight to potential business match + if sim > 0.3: + cand["match_type"].append("潜在合作 (您的行业匹配对方愿景)") + + # 3. Cross Match: Their Industry matches My Vision (They can help me) + if user_vision and cand_industry: + sim = compute_expert_score(user_vision, cand_industry) + cand["score"] += sim * 1.5 + if sim > 0.3: + cand["match_type"].append("潜在贵人 (对方行业匹配您的愿景)") + + # Sort by score descending + candidates.sort(key=lambda x: x["score"], reverse=True) + + # Take top 3 + top_candidates = candidates[:3] + + # Format output + result = [] + for cand in top_candidates: + # If no specific match type, just say "同桌伙伴" + match_reason = " | ".join(cand["match_type"]) if cand["match_type"] else "同桌伙伴" + + result.append({ + "name": cand["name"], + "industry": cand["industry"], + "company_name": cand["company_name"], + "position": cand["position"], + "business_scope": cand["business_scope"], + "vision_2026": cand["vision_2026"] or "暂无愿景信息", + "match_reason": match_reason }) - return tablemates + + return result @app.get("/", response_class=HTMLResponse) async def read_root(request: Request): return templates.TemplateResponse("index.html", {"request": request}) +class UnlockRequest(BaseModel): + my_phone: str + target_id: str + +class ResourceMatchRequest(BaseModel): + phone: str + +@app.get("/search", response_class=HTMLResponse) +async def resource_match_page(request: Request): + return templates.TemplateResponse("resource_match.html", {"request": request}) + +@app.post("/api/resource-match") +def resource_match(req: ResourceMatchRequest): + try: + conn = get_db_connection() + cur = conn.cursor(cursor_factory=RealDictCursor) + + # 1. Fetch current user + # Optimize: Only fetch necessary fields + # Points are now in checkin_info + cur.execute(""" + SELECT gd.new_id, gd.name, gd.phone, ci.social_point as points, + gd.industry_company, ci.business_scope, ci.vision_2026 + FROM gsdh_data gd + LEFT JOIN checkin_info ci ON gd.new_id = ci.gsdh_id + WHERE gd.phone = %s + """, (req.phone,)) + user = cur.fetchone() + + if not user: + cur.close() + release_db_connection(conn) + return JSONResponse(content={"success": False, "message": "用户不存在"}, status_code=404) + + user_industry = f"{user['industry_company'] or ''} {user['business_scope'] or ''}".strip() + user_vision = user['vision_2026'] or "" + + # 2. Fetch ALL other users (who have checked in) + # Performance Note: Fetching all rows is slow if N is large. + # But for N < 1000 it's acceptable. For larger N, we need vector search (e.g. pgvector). + # We limit the fields to reduce payload size. + cur.execute(""" + SELECT gd.new_id, gd.name, gd.phone, gd.industry_company, + ci.company_name, ci.position, ci.business_scope, ci.vision_2026, ci.location + FROM checkin_info ci + JOIN gsdh_data gd ON ci.gsdh_id = gd.new_id + WHERE gd.new_id != %s + """, (user['new_id'],)) + others = cur.fetchall() + + cur.close() + release_db_connection(conn) + + # 3. Calculate Matches (In-Memory Python) + # difflib.SequenceMatcher is O(N*M), running it 3 times for every user is expensive. + # We can optimize by pre-calculating and caching, or just doing it efficiently. + + matches = { + "customers": [], # My Industry vs Their Vision + "partners": [], # My Vision vs Their Industry + "peers": [] # My Industry vs Their Industry + } + + # Optimization: Pre-compile SequenceMatcher objects if possible, but ratio() needs both strings. + # We use a threshold to quickly filter obvious non-matches if we had embeddings. + # For now, we stick to string matching but handle None values gracefully. + + for other in others: + # Handle potential None values safely + other_ind_comp = other.get('industry_company') or '' + other_bus_scope = other.get('business_scope') or '' + other_industry = f"{other_ind_comp} {other_bus_scope}".strip() + other_vision = other.get('vision_2026') or "" + + # 3.1 Customers (They need me) + # My Industry (Supply) matches Their Vision (Demand) + if user_industry and other_vision: + # Quick length check optimization: if length difference is huge, ratio will be low + score = compute_expert_score(user_industry, other_vision) + if score > 0.2: + matches["customers"].append({**other, "score": score}) + + # 3.2 Partners (I need them) + # My Vision (Demand) matches Their Industry (Supply) + if user_vision and other_industry: + score = compute_expert_score(user_vision, other_industry) + if score > 0.2: + matches["partners"].append({**other, "score": score}) + + # 3.3 Peers (Same industry) + # My Industry matches Their Industry + if user_industry and other_industry: + score = compute_expert_score(user_industry, other_industry) + if score > 0.3: + matches["peers"].append({**other, "score": score}) + + # 4. Sort and Limit + for key in matches: + matches[key].sort(key=lambda x: x["score"], reverse=True) + # Limit to top 20 for display performance + matches[key] = matches[key][:20] + + # Hide sensitive info by default + for p in matches[key]: + # Safe phone masking + p_phone = p.get('phone', '') + if len(p_phone) >= 7: + p['phone'] = p_phone[:3] + "****" + p_phone[-4:] + else: + p['phone'] = "****" + + p['location'] = "???" # Hidden location + p['unlocked'] = False + # Clean up internal fields to reduce JSON size + # p.pop('new_id', None) + + return { + "success": True, + "user": { + "name": user['name'], + "industry_company": user['industry_company'], + "points": user['points'] if user['points'] is not None else 0, + "phone": user['phone'] + }, + "matches": matches + } + + except Exception as e: + import traceback + traceback.print_exc() + if 'conn' in locals() and conn: + release_db_connection(conn) + return JSONResponse(content={"success": False, "message": str(e)}, status_code=500) + +@app.post("/api/unlock-contact") +def unlock_contact(req: UnlockRequest): + try: + conn = get_db_connection() + cur = conn.cursor(cursor_factory=RealDictCursor) + + # 1. Check User Points + # Points are now in checkin_info, queried by gsdh_id (which we can get from phone via gsdh_data join) + cur.execute(""" + SELECT ci.social_point as points + FROM checkin_info ci + JOIN gsdh_data gd ON ci.gsdh_id = gd.new_id + WHERE gd.phone = %s + """, (req.my_phone,)) + res = cur.fetchone() + + if not res: + cur.close() + release_db_connection(conn) + return JSONResponse(content={"success": False, "message": "用户未签到或不存在"}, status_code=404) + + points = res['points'] if res['points'] is not None else 0 + if points <= 0: + cur.close() + release_db_connection(conn) + return JSONResponse(content={"success": False, "message": "积分不足"}, status_code=400) + + # 2. Deduct Point + # Update checkin_info using a subquery to map phone to gsdh_id + cur.execute(""" + UPDATE checkin_info + SET social_point = social_point - 1 + WHERE gsdh_id = (SELECT new_id FROM gsdh_data WHERE phone = %s) + """, (req.my_phone,)) + + # 3. Fetch Target Info + cur.execute(""" + SELECT gd.phone, ci.location + FROM gsdh_data gd + LEFT JOIN checkin_info ci ON gd.new_id = ci.gsdh_id + WHERE gd.new_id = %s + """, (req.target_id,)) + target = cur.fetchone() + + conn.commit() + cur.close() + release_db_connection(conn) + + return { + "success": True, + "remaining_points": points - 1, + "contact": { + "phone": target['phone'], + "location": target['location'] or "未分配座位" + } + } + + except Exception as e: + if 'conn' in locals() and conn: + conn.rollback() + release_db_connection(conn) + return JSONResponse(content={"success": False, "message": str(e)}, status_code=500) + @app.get("/api/search") -async def search_user(query: str): +def search_user(query: str): """ Search user by phone (exact match) or name (fuzzy match). """ @@ -200,21 +557,24 @@ async def search_user(query: str): users = cur.fetchall() if len(users) == 0: - conn.close() + cur.close() + release_db_connection(conn) return JSONResponse(content={"found": False, "message": "未查询到相关信息,请检查输入是否正确"}, status_code=404) elif len(users) > 1: # If multiple users found by name, return list for user to select (simplified here to return first or error) # For this MVP, let's return all matching users so frontend can handle selection - conn.close() + cur.close() + release_db_connection(conn) return JSONResponse(content={"found": True, "multiple": True, "users": users}) else: user = users[0] # Check if already signed if user.get('is_signed') == 'TRUE': - # If already signed, fetch their assigned seat and tablemates - cur.execute("SELECT location FROM checkin_info WHERE gsdh_id = %s", (user['new_id'],)) - checkin_info = cur.fetchone() + # Check if already signed + cur.execute("SELECT location, vision_2026 FROM checkin_info WHERE gsdh_id = %s", (user['new_id'],)) + checkin_info = cur.fetchone() # Fetch as RealDictRow + assigned_seat = checkin_info['location'] if checkin_info else "自由席" # Fetch tablemates @@ -223,10 +583,17 @@ async def search_user(query: str): # We can adapt get_tablemates or just use key access if we pass the DictCursor # Let's create a fresh standard cursor to be safe and consistent with get_tablemates implementation cur_plain = conn.cursor() - tablemates = get_tablemates(cur_plain, assigned_seat, user['new_id']) + + # Fetch user's vision and industry for matching + user_vision = checkin_info.get('vision_2026', '') if checkin_info else '' + # user['industry_company'] is already available in user dict + user_industry = user.get('industry_company', '') + + tablemates = get_tablemates(cur_plain, assigned_seat, user['new_id'], user_vision, user_industry) cur_plain.close() - conn.close() + cur.close() + release_db_connection(conn) return JSONResponse(content={ "found": True, "user": user, @@ -235,14 +602,17 @@ async def search_user(query: str): "tablemates": tablemates }) - conn.close() + cur.close() + release_db_connection(conn) return JSONResponse(content={"found": True, "user": user, "already_signed": False}) except Exception as e: + if 'conn' in locals() and conn: + release_db_connection(conn) return JSONResponse(content={"error": str(e)}, status_code=500) @app.post("/api/checkin") -async def checkin_user(checkin_data: CheckinRequest): +def checkin_user(checkin_data: CheckinRequest): try: conn = get_db_connection() cur = conn.cursor() @@ -250,16 +620,20 @@ async def checkin_user(checkin_data: CheckinRequest): # 0. Get user's industry from gsdh_data to help with seat allocation cur.execute("SELECT industry_company FROM gsdh_data WHERE new_id = %s", (checkin_data.gsdh_id,)) res = cur.fetchone() - user_industry = res[0] if res else "" + base_industry = res[0] if res and res[0] else "" + + # Combine base industry with the newly provided business_scope for better matching + user_industry_info = f"{base_industry} {checkin_data.business_scope or ''}".strip() # 1. Allocate Seat - assigned_seat = assign_seat(cur, user_industry) + assigned_seat = assign_seat(cur, user_industry_info) # 2. Insert into checkin_info with assigned seat + # Initialize social_point to 5 insert_sql = """ INSERT INTO checkin_info - (name, phone, company_name, position, business_scope, vision_2026, location, gsdh_id) - VALUES (%s, %s, %s, %s, %s, %s, %s, %s) + (name, phone, company_name, position, business_scope, vision_2026, location, gsdh_id, social_point) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, 4) """ cur.execute(insert_sql, ( checkin_data.name, @@ -279,16 +653,18 @@ async def checkin_user(checkin_data: CheckinRequest): conn.commit() # 4. Fetch tablemates for the newly assigned seat - tablemates = get_tablemates(cur, assigned_seat, checkin_data.gsdh_id) + # Use provided vision and industry for matching + tablemates = get_tablemates(cur, assigned_seat, checkin_data.gsdh_id, checkin_data.vision_2026 or "", user_industry_info) cur.close() - conn.close() + release_db_connection(conn) return {"success": True, "message": "签到成功!", "seat": assigned_seat, "tablemates": tablemates} except Exception as e: - if 'conn' in locals(): + if 'conn' in locals() and conn: conn.rollback() + release_db_connection(conn) return JSONResponse(content={"success": False, "message": f"签到失败: {str(e)}"}, status_code=500) @app.get("/add-user", response_class=HTMLResponse) @@ -298,7 +674,7 @@ async def add_user_page(request: Request): return templates.TemplateResponse("add_user.html", {"request": request, "secret": secret}) @app.post("/api/add-user") -async def add_user_api(user_data: AddUserRequest): +def add_user_api(user_data: AddUserRequest): try: conn = get_db_connection() cur = conn.cursor() @@ -306,7 +682,8 @@ async def add_user_api(user_data: AddUserRequest): # Check if phone already exists cur.execute("SELECT * FROM gsdh_data WHERE phone = %s", (user_data.phone,)) if cur.fetchone(): - conn.close() + cur.close() + release_db_connection(conn) return JSONResponse(content={"success": False, "message": "该手机号已存在"}, status_code=400) # Calculate next new_id @@ -330,12 +707,13 @@ async def add_user_api(user_data: AddUserRequest): conn.commit() cur.close() - conn.close() + release_db_connection(conn) return {"success": True, "message": "添加成功", "new_id": new_id} except Exception as e: - if 'conn' in locals(): + if 'conn' in locals() and conn: conn.rollback() + release_db_connection(conn) return JSONResponse(content={"success": False, "message": f"添加失败: {str(e)}"}, status_code=500) if __name__ == "__main__": diff --git a/templates/resource_match.html b/templates/resource_match.html new file mode 100644 index 0000000..dabd109 --- /dev/null +++ b/templates/resource_match.html @@ -0,0 +1,377 @@ + + + + + + 资源匹配 - 供需对接 + + + + + + +
+ + Header Image + + +
+ +
+ +

资源雷达 📡

+

发现全场潜在商机与合作伙伴

+ + +
+
+
+ +
+
+
+
+
+
+
+
社交点数
+
💎
+
+
+
+ + +
+
+

开启资源匹配

+
+ + +
+ +
+
+ + +
+ + +
+ + + +
+ + +
+
+ +

暂无匹配结果

+
+ +
+ +
+ 匹配度 % +
+ +
+
+ +
+
+

+

+ + +

+

+
+
+ + +
+ + + + + + + +
+ + +
+
+

+

+
+ +
+
+
+
+ + +
+ + +
+
+ + + + \ No newline at end of file