commit bad11e8fa5114b1eba774703e7aa3416d8d9d709 Author: jeremygan2021 Date: Tue Jan 6 16:37:28 2026 +0800 first commit diff --git a/__pycache__/main.cpython-312.pyc b/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000..73574d0 Binary files /dev/null and b/__pycache__/main.cpython-312.pyc differ diff --git a/__pycache__/main.cpython-313.pyc b/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000..8eb6081 Binary files /dev/null and b/__pycache__/main.cpython-313.pyc differ diff --git a/docker_deply.sh b/docker_deply.sh new file mode 100755 index 0000000..abe2d14 --- /dev/null +++ b/docker_deply.sh @@ -0,0 +1,307 @@ +#!/bin/bash + +# ============================================================================= +# Docker 镜像构建和部署自动化脚本 +# chmod u+x docker_deply.sh +# 用法: +# ./docker_deply.sh # 完整构建和部署流程 (默认AMD64架构) +# ./docker_deply.sh -amd # 构建和部署AMD64架构 +# ./docker_deply.sh -arm # 构建和部署ARM64架构 +# ./docker_deply.sh -upload # 仅上传已存在的tar文件并部署 +# ./docker_deply.sh -upload -amd # 仅上传已存在的AMD64架构tar文件并部署 +# ./docker_deply.sh -upload -arm # 仅上传已存在的ARM64架构tar文件并部署 +# ============================================================================= + +# 配置非局域网变量 - 公网上传方法 +# SERVER_HOST="121.43.104.161" # 服务器IP地址 +# SERVER_USER="ubuntu" # 服务器用户名 +# SERVER_PASSWORD="qweasdzxc1" # 服务器密码 +# SERVER_PORT="6222" + +# 配置局域网变量 - 公司局域网上传方法 +SERVER_HOST="6.6.6.86" # 服务器IP地址 +SERVER_USER="ubuntu" # 服务器用户名 +SERVER_PASSWORD="qweasdzxc1" # 服务器密码 +SERVER_PORT="22" # SSH端口,默认22 + + +IMAGE_NAME="crystal_back" # Docker镜像名称 +IMAGE_TAG="latest" # Docker镜像标签 +CONTAINER_NAME="crystal-container" # 容器名称 +LOCAL_PORT="8011" # 本地端口 +CONTAINER_PORT="8011" # 容器端口 +TAR_FILE="${IMAGE_NAME}-${IMAGE_TAG}.tar" # 压缩包文件名 + +# 架构相关变量 +PLATFORM="linux/amd64" # 默认架构 +ARCH_SUFFIX="" # 架构后缀,用于区分不同架构的tar文件 + +# 颜色输出 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# 日志函数 +log_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +log_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +log_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# 检查依赖 +check_dependencies() { + log_info "检查依赖..." + + if ! command -v docker &> /dev/null; then + log_error "Docker 未安装,请先安装 Docker" + exit 1 + fi + + if ! command -v sshpass &> /dev/null; then + log_error "sshpass 未安装,请先安装 sshpass" + log_info "macOS: brew install sshpass" + log_info "Ubuntu: sudo apt-get install sshpass" + exit 1 + fi + + log_success "依赖检查完成" +} + +# 解析命令行参数 +parse_arguments() { + while [[ $# -gt 0 ]]; do + case $1 in + -amd) + PLATFORM="linux/amd64" + ARCH_SUFFIX="-amd64" + log_info "设置目标架构为 AMD64" + shift + ;; + -arm) + PLATFORM="linux/arm64" + ARCH_SUFFIX="-arm64" + log_info "设置目标架构为 ARM64" + shift + ;; + -upload) + UPLOAD_ONLY=true + log_info "设置为仅上传模式" + shift + ;; + *) + log_error "未知参数: $1" + log_info "支持的参数: -amd, -arm, -upload" + exit 1 + ;; + esac + done + + # 更新TAR_FILE名,包含架构后缀 + TAR_FILE="${IMAGE_NAME}-${IMAGE_TAG}${ARCH_SUFFIX}.tar" + log_info "镜像文件名: ${TAR_FILE}" +} + +# 构建Docker镜像 +build_image() { + log_info "开始构建 Docker 镜像..." + + # 检查是否存在旧的tar文件 + if [ -f "$TAR_FILE" ]; then + log_warning "发现旧的tar文件,正在删除..." + rm -f "$TAR_FILE" + fi + + # 构建镜像并导出为tar文件 + docker buildx build --platform $PLATFORM -t "${IMAGE_NAME}:${IMAGE_TAG}" --output type=docker,dest="./${TAR_FILE}" . + + if [ $? -eq 0 ]; then + log_success "Docker 镜像构建完成: ${TAR_FILE}" + else + log_error "Docker 镜像构建失败" + exit 1 + fi +} + +# 上传文件到服务器 +upload_to_server() { + log_info "上传文件到服务器..." + + # 上传镜像tar文件 + sshpass -p "$SERVER_PASSWORD" scp -P "$SERVER_PORT" -o StrictHostKeyChecking=no "$TAR_FILE" "${SERVER_USER}@${SERVER_HOST}:/tmp/" + + # 上传.env文件 + sshpass -p "$SERVER_PASSWORD" scp -P "$SERVER_PORT" -o StrictHostKeyChecking=no ".env" "${SERVER_USER}@${SERVER_HOST}:/tmp/" + + if [ $? -eq 0 ]; then + log_success "文件上传成功" + else + log_error "文件上传失败" + exit 1 + fi +} + +# 在服务器上部署 +deploy_on_server() { + log_info "在服务器上部署..." + + sshpass -p "$SERVER_PASSWORD" ssh -p "$SERVER_PORT" -o StrictHostKeyChecking=no "${SERVER_USER}@${SERVER_HOST}" << EOF + set -e + + echo "[INFO] 开始服务器端部署..." + + # 检查并停止现有容器 + if sudo docker ps -a --format 'table {{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then + echo "[INFO] 发现现有容器 ${CONTAINER_NAME},正在停止并删除..." + sudo docker stop ${CONTAINER_NAME} || true + sudo docker rm ${CONTAINER_NAME} || true + fi + + # 检查并删除现有镜像 + if sudo docker images --format 'table {{.Repository}}:{{.Tag}}' | grep -q "^${IMAGE_NAME}:${IMAGE_TAG}$"; then + echo "[INFO] 发现现有镜像 ${IMAGE_NAME}:${IMAGE_TAG},正在删除..." + sudo docker rmi ${IMAGE_NAME}:${IMAGE_TAG} || true + fi + + # 加载新镜像 + echo "[INFO] 加载新镜像..." + sudo docker load -i /tmp/${TAR_FILE} + + # 验证镜像是否加载成功 + if sudo docker images | grep -q "${IMAGE_NAME}"; then + echo "[SUCCESS] 镜像加载成功" + else + echo "[ERROR] 镜像加载失败" + exit 1 + fi + + # 创建并使用.env文件中的变量 + echo "[INFO] 启动新容器..." + sudo docker run -d -p ${LOCAL_PORT}:${CONTAINER_PORT} --name ${CONTAINER_NAME} \ + --env-file /tmp/.env \ + ${IMAGE_NAME}:${IMAGE_TAG} + + # 验证容器是否启动成功 + if sudo docker ps | grep -q "${CONTAINER_NAME}"; then + echo "[SUCCESS] 容器启动成功" + echo "[INFO] 容器状态:" + sudo docker ps | grep "${CONTAINER_NAME}" + else + echo "[ERROR] 容器启动失败" + echo "[INFO] 查看容器日志:" + sudo docker logs ${CONTAINER_NAME} + exit 1 + fi + + # 清理临时文件 + echo "[INFO] 清理临时文件..." + rm -f /tmp/${TAR_FILE} + rm -f /tmp/.env + + echo "[SUCCESS] 部署完成!" + echo "[INFO] 应用访问地址: http://${SERVER_HOST}:${LOCAL_PORT}" +EOF + + if [ $? -eq 0 ]; then + log_success "服务器部署完成" + else + log_error "服务器部署失败" + exit 1 + fi +} + +# 清理本地文件 +cleanup_local() { + log_info "清理本地临时文件..." + + if [ -f "$TAR_FILE" ]; then + rm -f "$TAR_FILE" + log_success "本地临时文件已清理" + fi +} + +# 显示部署信息 +show_deployment_info() { + log_success "部署完成!" + echo "" + echo "==========================================" + echo "部署信息:" + echo "==========================================" + echo "服务器地址: ${SERVER_HOST}" + echo "应用端口: ${LOCAL_PORT}" + echo "访问地址: http://${SERVER_HOST}:${LOCAL_PORT}" + echo "容器名称: ${CONTAINER_NAME}" + echo "镜像名称: ${IMAGE_NAME}:${IMAGE_TAG}" + echo "目标架构: ${PLATFORM}" + echo "镜像文件: ${TAR_FILE}" + echo "==========================================" + echo "" + log_info "如需查看容器日志,请在服务器上运行: sudo docker logs ${CONTAINER_NAME}" + log_info "如需停止容器,请在服务器上运行: sudo docker stop ${CONTAINER_NAME}" +} + +# 主函数 +main() { + echo "==========================================" + echo "Docker 镜像构建和部署自动化脚本" + echo "==========================================" + echo "" + + # 解析命令行参数 + UPLOAD_ONLY=false + parse_arguments "$@" + + # 检查配置 + if [ "$SERVER_HOST" = "your-server-ip" ] || [ "$SERVER_PASSWORD" = "your-password" ]; then + log_error "请先配置脚本顶部的服务器信息" + log_info "需要修改的变量:" + log_info " - SERVER_HOST: 服务器IP地址" + log_info " - SERVER_USER: 服务器用户名" + log_info " - SERVER_PASSWORD: 服务器密码" + exit 1 + fi + + # 检查是否是上传模式 + if [ "$UPLOAD_ONLY" = true ]; then + log_info "检测到 -upload 参数,跳过构建步骤" + + # 检查tar文件是否存在 + if [ ! -f "$TAR_FILE" ]; then + log_error "未找到tar文件: $TAR_FILE" + log_info "请先运行脚本构建镜像,或确保tar文件存在" + exit 1 + fi + + log_success "找到tar文件: $TAR_FILE" + + # 执行上传和部署流程 + upload_to_server + deploy_on_server + cleanup_local + show_deployment_info + else + # 执行完整的部署流程 + check_dependencies + build_image + upload_to_server + deploy_on_server + cleanup_local + show_deployment_info + fi +} + +# 脚本入口 +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + main "$@" +fi \ No newline at end of file diff --git a/gsdh.csv b/gsdh.csv new file mode 100644 index 0000000..911d585 --- /dev/null +++ b/gsdh.csv @@ -0,0 +1,182 @@ +new_id,name,industry_company,contact_info,phone,fee,payment_channel,is_signed +1,孙成耕,"高等教育,农业",,13708714964,26,一见星球,FALSE +2,和德光,冶炼自动化,,18088930670,26,一见星球,FALSE +3,熊志华,大数据,,13922200602,26,一见星球,FALSE +4,和兆云,工业自动化,,13987660503,26,一见星球,FALSE +5,程云龙,航空,,18669209165,26,一见星球,FALSE +6,唐佑初,家电、百货、农产品,,18988138899,26,一见星球,FALSE +7,杨婵,教育,,18687715696,26,一见星球,FALSE +8,姜安峰,文化传媒,,13211747473,26,一见星球,FALSE +9,刘夏,教育,,18487147072,26,一见星球,FALSE +10,王为家,酒水经销商,,15198989711,26,一见星球,FALSE +11,胡泰姜,互联网,,18608719363,26,一见星球,FALSE +12,杨丽萍,医药,,15969182571,26,一见星球,FALSE +13,林七,保险,,15024589748,26,一见星球,FALSE +14,Zoe,教育+农业,,15969500332,26,一见星球,FALSE +15,李怡清,软件,,13888947017,26,一见星球,FALSE +16,朱英杰,文化传播,,15087063865,26,一见星球,FALSE +17,Johnny,跨境电商,,18687605779,26,微信,FALSE +18,张妩,服务(酒店),,1388829509,26,微信,FALSE +19,马建军,大溪谷 瓶装水、,,13987672775,26,微信,FALSE +20,周海洋,制造业,,17608785235,26,微信,FALSE +21,云天明,财务管理,,18866433121,26,微信,FALSE +22,文昱桦,数字化转型,,13777018667,26,微信,FALSE +23,文昱桦朋友,数字化转型,,13777018667,26,微信,FALSE +24,龙金贵,凯缘春酒业,,13698779505,26,微信,FALSE +25,唐儒,大健康轻创业,,13099992576,26,微信,FALSE +26,窦海涛,人工智能,,13845432196,26,微信,FALSE +27,刘磊,跨境电子商务,,13529005525,26,微信,FALSE +28,李若菡,道医,有事,已退费,14787451514,26,微信,FALSE +29,戴军,个体,,13529383555,26,微信,FALSE +30,李延辉,生物医药,,18908789027,26,微信,FALSE +31,张鹏程,医疗机器人,,13308789694,26,微信,FALSE +32,倪钜,珠宝,,13150587227,26,微信,FALSE +33,陈昭宇,文旅,免费,17600099614,,,FALSE +34,张馨艺,自媒体,免费,13888651626,,,FALSE +35,黄升,社群运营,免费,1,,,FALSE +36,马艺,AI跨境,免费,,,,FALSE +37,岳岳,自媒体,,18687329243,志愿者,,FALSE +38,岳岳朋友刘致豪,自媒体,,15188137009,志愿者,,FALSE +39,岳岳王天胜,自媒体,,13320543169,志愿者,,FALSE +40,岳岳宗彗莲,自媒体,,19988427837,志愿者,,FALSE +41,陆起,飞书,免费,13347705547,,,FALSE +42,陆起朋友王维,飞书,免费,13521170687,,,FALSE +43,陆起朋友,飞书,免费,,,,FALSE +44,刘翔,财务公司,,18725032602,,,FALSE +45,宋宇,后端工程师,,18388411803,,,FALSE +46,高立仁,新能源汽车公司,,19187151953,,,FALSE +47,赵若曦,云纺集团,免费,13987673203,,,FALSE +48,李李,智行滇峰说演讲,免费,,,,FALSE +49,海浪,安信集团,免费,17713249555,,,FALSE +50,姚超,思享界,,13033031987,志愿者,,FALSE +51,姚超摄影师,思享界,,,摄影师,,FALSE +52,小唯,,,,志愿者,,FALSE +53,朱晓丽,律师,,18206868114,志愿者,,FALSE +54,涞涞,康养中医药,,13089899853,志愿者,,FALSE +55,赖应海,创业者,,18157390046,志愿者,,FALSE +56,冯立伟,量迹,,13220881316,总负责,,FALSE +57,赵广明,鬼才明创意工作室,,18611218523,总策划,,FALSE +58,甘昭泉,量迹,,18585164448,分享嘉宾,,FALSE +59,丁靖洋,派动科技,,18221820102,分享嘉宾,,FALSE +60,刘伯煜,大筑科技,,18312876673,分享嘉宾,,FALSE +61,曹平飞,阿里云,嘉宾,,,,FALSE +62,李欢,阿里云,,18187811772,分享嘉宾,,FALSE +63,暂定,百度,,,分享嘉宾,,FALSE +64,郑文浩,百度,嘉宾,15820798660,,, +65,三三,彩创、飞书,,,分享嘉宾,, +66,钱丹,彩创、飞书,,18788400115,联合主办,, +67,沙梅兰,掌上春城,,15912524519,媒体,, +68,李凌竹,广播电视台,,13888585678,媒体,, +69,晏丽,云南广电传媒,,15911520520,分享嘉宾,, +70,拍摄,云南广电传媒,,15911520520,摄影师,, +71,采访,云南广电传媒,,15911520520,采访,, +72,许应烨,AI,,15331657816,,, +73,汤小姐,打杂,已进群,13922791778,,, +74,张玮楠,茶企,已有微信,18289790037,,, +75,李逸飞,图书电商,嘉宾,18810586550,,, +76,Robyn,教育留学,w,13211788556,,, +77,刘胜,教育,w,14787812507,,, +78,闵莹,软件开发,w,18687510353,,, +79,金玉辉,昆明飞威报刊图书有限公司,/,18388109039,,, +80,陈佑,图书批发,/,18288927098,,, +81,蒋晓伟,法务,w,13085392813,26,微信, +82,xiaoMa,IT,,1.32387E+11,,, +83,王瑞琦,科技,w,15925183977,,, +84,秦贵,图书行业,/,18788443543,,, +85,杨慧,图书电商,/,18869769859,,, +86,赵榕,图书类目,/,18608899476,,, +87,邓勇,教育,w,18208813207,,, +88,张出高,教育,w,18206871081,,, +89,王海倩,教育,,15559634666,,, +90,陈锐嶂,商贸流通,w,13008671498,,, +91,雷乔玲,财务,w,15198838759,,, +92,lucky,跨境电商,w,18214611725,,, +93,李忠岘,教育,w,15808886488,,, +94,陈奕恒,跨境电商,w,15887158441,,, +95,经纬,科技,,13987382399,,, +96,段晓方,开发,w,17687023127,,, +97,高立仁,新能源汽车,w,17504311180,,, +98,吴松泽,教师,w,18762304432,,, +99,周正宜,跨境电商,w,17689845870,,, +100,赵云翔,跨境电商,w,17751891232,,, +101,李皓月,医疗,w,17368456286,,, +102,徐衡,设计,w,13518713214,26,微信, +103,葛一瑶,电力,w,15912137626,26,微信, +104,陆易航,智能开发,w,15087148039,26,微信, +105,邓智铭,程序员,w,13211685339,,, +106,姚来弟,计算机,w,18288440214,,, +107,萧清,数字文创,w,17606909560,,, +108,发发,新媒体,,13278714977,,, +109,塔米,教育,,13250714571,,, +110,张华英,律师,w,13658853415,,, +111,王天胜,直播带货,,15687698763,,, +112,张橙,传媒,w,18082712849,26,微信, +113,周总,"商业服务,为企业申办各类型补贴",w,13987141588,,, +114,吴潇雪,法律,w,15969591218,,, +115,蒋乃浩,娱乐,w,13888862549,26,微信, +116,李烨,医疗,w,13619605970,,, +117,白羽,文化产业,w,13187856156,,, +118,李易为,文旅,,13085358008,,, +119,高嘉煜,软件开发,w,15126615549,26,微信, +120,毕先生,花卉,w,13888466977,,, +121,汤镇宇,互联网,w,13629615669,,, +122,刘先生,电商,w,15908809814,,, +123,晏瑞婕,金融,w,13013313443,,, +124,杨势挥,KTV,w,18787440187,,, +125,滕乾忠,教育,,18187128470,,, +126,芾邴,"教育,科技",,13161994127,,, +127,Alice,云计算,,15087060387,,, +128,李丹,法律,w,15125990928,,, +129,吴疆,能源,w,13718215039,,, +130,高丹妮,房地产、新媒体,免费,15287335911,青商会员,, +131,聂怡然,智能餐饮,w,13808725796,,, +132,一雄,OPC,w,18812269173,26,微信, +133,杜丽娜,自由职业,w,13888115882,,, +134,ayao,音乐ai,,18987063951,,, +135,周其建,人工智能,,18887103888,,, +136,杨万峰,制造业,,1528821438,,, +137,Mia,旅居,,19911866777,,, +138,张雪娟,教育,,18988153273,,, +139,禹姣,银行业,,15887205279,,, +140,郑华,零售,,15140295598,,, +141,毕玉,AI,,19146959276,,, +142,象叔,IP内容营销,,13888834251,,, +143,王贺,旅游规划与设计,,13987188550,,, +144,瑾瑾瑾,AI创作,,13658897411,,, +145,阿布,传统行业,,15087097719,,, +146,杨文虎,新闻,,13577034568,,, +147,陈,知识产权,,13211662238,,, +148,安娜,自由职业,,18208706046,,, +149,暴雨,医药,,15969182571,,, +150,小源,教育,,18314522519,,, +151,杨泽文,电力,,17803866644,,, +152,張競丹,旅遊,,18987589587,,, +153,赵锐涵,金融,,13529216958,,, +154,范洁,大健康食品,,13888182707,,, +155,陈莹,服务,,19224081189,,, +156,杨立平,漫剧,,13988620996,26,微信, +157,杨立蕊,漫剧,,13888707563,26,微信, +158,郭艳,茶行业,,18787145995,26,微信, +159,孟妹,广告,,13759060777,26,微信, +160,丁艳玲,餐饮行业,,18187315565,26,微信, +161,钟源,教育/餐饮,,18314522519,26,微信, +162,付吟吟,昆明精悦广告有限公司总经理,免费,13619622871,青商会员,, +163,李慧,云南智慧光经贸有限公司 法人,免费,15825186018,青商会员,, +164,陈锦玥,自由职业,,18987471556,26,微信, +165,鄢木西,星梧科技(云南)有限责任公司,,19912828161,26,微信, +166,孔浩,IT信息行业,,13668780930,26,微信, +167,李拉,互联网,,13759408584,26,微信, +168,吴芬,食品电商,,15331725758,26,微信, +169,黄政葵,,,,26,微信, +170,温立新,村播,免费,13330499193,,, +171,孔德风,百度,嘉宾,18087960002,,, +172,李程旻,火山引擎,免费,19187149926,,, +173,王若谷,咖啡,免费,18468038180,,, +174,张童,电力行业,,18187527804,26,微信, +175,李虹婷,投资,,18608738555,青商会员,, +176,周鑫,银行,,,青商会员,, +177,张志刚,青创会,嘉宾,15508805566,,, +178,段晓芳,全栈开发工程师,免费,17687023127,,, +179,张培恒,富滇银行-oa,免费,18288711024,,, +180,王娇,软件运维,免费,15808742554,,, +181,宁杰,医疗行业,,13698786789,26,微信, diff --git a/main.py b/main.py new file mode 100644 index 0000000..e09b6b1 --- /dev/null +++ b/main.py @@ -0,0 +1,310 @@ +from fastapi import FastAPI, HTTPException, Request +from fastapi.staticfiles import StaticFiles +from fastapi.templating import Jinja2Templates +from fastapi.responses import HTMLResponse, JSONResponse +from pydantic import BaseModel +import psycopg2 +from psycopg2.extras import RealDictCursor +from typing import Optional, List, Dict +import random +import uuid + +app = FastAPI() + +# Database connection parameters +DB_CONFIG = { + "host": "121.43.104.161", + "port": "6432", + "user": "gsdh", + "password": "123gsdh", + "database": "gsdh" +} + +# Mount static files +app.mount("/static", StaticFiles(directory="static"), name="static") +templates = Jinja2Templates(directory="templates") + +# Models +class CheckinRequest(BaseModel): + gsdh_id: str + name: str + phone: str + company_name: Optional[str] = None + position: Optional[str] = None + business_scope: Optional[str] = None + vision_2026: Optional[str] = None + location: Optional[str] = None + +class AddUserRequest(BaseModel): + name: str + phone: str + industry_company: Optional[str] = None + +def get_db_connection(): + conn = psycopg2.connect(**DB_CONFIG) + return conn + +def assign_seat(cur, user_industry: str) -> str: + """ + Allocate a seat based on: + 1. Even distribution (13 tables, max 12 per table) + 2. Mix industries (try to put user in a table where their industry is least represented) + """ + TOTAL_TABLES = 13 + MAX_PER_TABLE = 12 + + # Initialize table stats + # tables = { 1: {'count': 0, 'industries': []}, ... } + tables = {i: {'count': 0, 'industries': []} for i in range(1, TOTAL_TABLES + 1)} + + # Fetch current seating status + # We join checkin_info with gsdh_data to get industries of people ALREADY SEATED + query = """ + SELECT ci.location, gd.industry_company + FROM checkin_info ci + JOIN gsdh_data gd ON ci.gsdh_id = gd.new_id + WHERE ci.location IS NOT NULL AND ci.location LIKE '第%桌' + """ + cur.execute(query) + rows = cur.fetchall() + + for row in rows: + loc = row[0] # e.g. "第1桌" + ind = row[1] + try: + # Extract table number + table_num = int(loc.replace("第", "").replace("桌", "")) + if 1 <= table_num <= TOTAL_TABLES: + tables[table_num]['count'] += 1 + if ind: + tables[table_num]['industries'].append(ind) + except ValueError: + continue + + # Filter tables that are not full + available_tables = [t for t in tables.items() if t[1]['count'] < MAX_PER_TABLE] + + if not available_tables: + return "自由席" # Fallback if all full + + # Strategy 1: Find tables with Minimum Count (Even Distribution) + min_count = min(t[1]['count'] for t in available_tables) + candidates_step1 = [t for t in available_tables if t[1]['count'] == min_count] + + # Strategy 2: Among candidates, find best for diversity + # We want a table where user_industry is NOT present, or present least often + best_table = None + + # If user has no industry info, just pick random from candidates + if not user_industry: + best_table = random.choice(candidates_step1)[0] + else: + # Score candidates: lower score is better (score = count of this industry in that table) + scored_candidates = [] + for table_id, stats in candidates_step1: + # Simple fuzzy check: count how many times user_industry appears in stats['industries'] + # We use simple string containment + industry_count = sum(1 for existing_ind in stats['industries'] if existing_ind and user_industry in existing_ind) + scored_candidates.append((table_id, industry_count)) + + # Sort by industry count (asc) + scored_candidates.sort(key=lambda x: x[1]) + + # Pick the one with least collision + min_collision = scored_candidates[0][1] + final_candidates = [x[0] for x in scored_candidates if x[1] == min_collision] + best_table = random.choice(final_candidates) + + return f"第{best_table}桌" + +def get_tablemates(cur, location: str, exclude_id: str) -> List[Dict]: + """ + Get up to 3 random tablemates from the same table location. + """ + if not location or location == "自由席": + return [] + + # 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") + + tablemates = [] + for row in rows: + tablemates.append({ + "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 "暂无愿景信息" + }) + return tablemates + +@app.get("/", response_class=HTMLResponse) +async def read_root(request: Request): + return templates.TemplateResponse("index.html", {"request": request}) + +@app.get("/api/search") +async def search_user(query: str): + """ + Search user by phone (exact match) or name (fuzzy match). + """ + try: + conn = get_db_connection() + cur = conn.cursor(cursor_factory=RealDictCursor) + + # Priority 1: Exact Phone Match + cur.execute("SELECT * FROM gsdh_data WHERE phone = %s", (query,)) + user = cur.fetchone() + + # Priority 2: Fuzzy Name Match if not found by phone + if not user: + # Using ILIKE for case-insensitive fuzzy search + cur.execute("SELECT * FROM gsdh_data WHERE name ILIKE %s", (f"%{query}%",)) + users = cur.fetchall() + + if len(users) == 0: + conn.close() + 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() + 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() + assigned_seat = checkin_info['location'] if checkin_info else "自由席" + + # Fetch tablemates + # Use a new cursor for the helper function to avoid cursor factory conflict or state issues + # Note: get_tablemates expects a standard cursor for tuple results, but here we have DictCursor + # 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']) + cur_plain.close() + + conn.close() + return JSONResponse(content={ + "found": True, + "user": user, + "already_signed": True, + "seat": assigned_seat, + "tablemates": tablemates + }) + + conn.close() + return JSONResponse(content={"found": True, "user": user, "already_signed": False}) + + except Exception as e: + return JSONResponse(content={"error": str(e)}, status_code=500) + +@app.post("/api/checkin") +async def checkin_user(checkin_data: CheckinRequest): + try: + conn = get_db_connection() + cur = conn.cursor() + + # 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 "" + + # 1. Allocate Seat + assigned_seat = assign_seat(cur, user_industry) + + # 2. Insert into checkin_info with assigned seat + 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) + """ + cur.execute(insert_sql, ( + checkin_data.name, + checkin_data.phone, + checkin_data.company_name, + checkin_data.position, + checkin_data.business_scope, + checkin_data.vision_2026, + assigned_seat, # Use the generated seat + checkin_data.gsdh_id + )) + + # 3. Update gsdh_data is_signed to TRUE + update_sql = "UPDATE gsdh_data SET is_signed = 'TRUE' WHERE new_id = %s" + cur.execute(update_sql, (checkin_data.gsdh_id,)) + + conn.commit() + + # 4. Fetch tablemates for the newly assigned seat + tablemates = get_tablemates(cur, assigned_seat, checkin_data.gsdh_id) + + cur.close() + conn.close() + + return {"success": True, "message": "签到成功!", "seat": assigned_seat, "tablemates": tablemates} + + except Exception as e: + if 'conn' in locals(): + conn.rollback() + return JSONResponse(content={"success": False, "message": f"签到失败: {str(e)}"}, status_code=500) + +@app.get("/add-user", response_class=HTMLResponse) +async def add_user_page(request: Request): + return templates.TemplateResponse("add_user.html", {"request": request}) + +@app.post("/api/add-user") +async def add_user_api(user_data: AddUserRequest): + try: + conn = get_db_connection() + cur = conn.cursor() + + # Check if phone already exists + cur.execute("SELECT * FROM gsdh_data WHERE phone = %s", (user_data.phone,)) + if cur.fetchone(): + conn.close() + return JSONResponse(content={"success": False, "message": "该手机号已存在"}, status_code=400) + + new_id = str(uuid.uuid4()) + + insert_sql = """ + INSERT INTO gsdh_data (new_id, name, phone, industry_company, is_signed) + VALUES (%s, %s, %s, %s, 'FALSE') + """ + cur.execute(insert_sql, (new_id, user_data.name, user_data.phone, user_data.industry_company)) + + conn.commit() + cur.close() + conn.close() + + return {"success": True, "message": "添加成功", "new_id": new_id} + except Exception as e: + if 'conn' in locals(): + conn.rollback() + return JSONResponse(content={"success": False, "message": f"添加失败: {str(e)}"}, status_code=500) + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/static/image.jpg b/static/image.jpg new file mode 100644 index 0000000..84e9192 Binary files /dev/null and b/static/image.jpg differ diff --git a/templates/add_user.html b/templates/add_user.html new file mode 100644 index 0000000..6dfd39b --- /dev/null +++ b/templates/add_user.html @@ -0,0 +1,298 @@ + + + + + + 云南AI共生大会 - 添加嘉宾 + + + +
+
+ 云南AI共生大会 +

添加新嘉宾

+
+ +
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+ + +
+ +
+ 返回签到首页 +
+
+ + + + diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..efd3312 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,810 @@ + + + + + + 云南AI共生大会 - 签到 + + + +
+
+ 云南AI共生大会 +
+ 2026 INTELLIGENT LEADERSHIP • AI SYMBIOSIS +
+

云南AI共生大会

+
+

📅 时间:1月10日 下午 2:00

+

📍 地点:金鼎科技园18号平台B座一楼报告厅

+

💡 内容:邀请重磅大咖分享AI在各行业的企业应用及案例,含深度交流环节。

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