first commit

This commit is contained in:
jeremygan2021
2026-01-06 16:37:28 +08:00
commit bad11e8fa5
8 changed files with 1907 additions and 0 deletions

Binary file not shown.

Binary file not shown.

307
docker_deply.sh Executable file
View File

@@ -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

182
gsdh.csv Normal file
View File

@@ -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,微信,
1 new_id name industry_company contact_info phone fee payment_channel is_signed
2 1 孙成耕 高等教育,农业 13708714964 26 一见星球 FALSE
3 2 和德光 冶炼自动化 18088930670 26 一见星球 FALSE
4 3 熊志华 大数据 13922200602 26 一见星球 FALSE
5 4 和兆云 工业自动化 13987660503 26 一见星球 FALSE
6 5 程云龙 航空 18669209165 26 一见星球 FALSE
7 6 唐佑初 家电、百货、农产品 18988138899 26 一见星球 FALSE
8 7 杨婵 教育 18687715696 26 一见星球 FALSE
9 8 姜安峰 文化传媒 13211747473 26 一见星球 FALSE
10 9 刘夏 教育 18487147072 26 一见星球 FALSE
11 10 王为家 酒水经销商 15198989711 26 一见星球 FALSE
12 11 胡泰姜 互联网 18608719363 26 一见星球 FALSE
13 12 杨丽萍 医药 15969182571 26 一见星球 FALSE
14 13 林七 保险 15024589748 26 一见星球 FALSE
15 14 Zoe 教育+农业 15969500332 26 一见星球 FALSE
16 15 李怡清 软件 13888947017 26 一见星球 FALSE
17 16 朱英杰 文化传播 15087063865 26 一见星球 FALSE
18 17 Johnny 跨境电商 18687605779 26 微信 FALSE
19 18 张妩 服务(酒店) 1388829509 26 微信 FALSE
20 19 马建军 大溪谷 瓶装水、 13987672775 26 微信 FALSE
21 20 周海洋 制造业 17608785235 26 微信 FALSE
22 21 云天明 财务管理 18866433121 26 微信 FALSE
23 22 文昱桦 数字化转型 13777018667 26 微信 FALSE
24 23 文昱桦朋友 数字化转型 13777018667 26 微信 FALSE
25 24 龙金贵 凯缘春酒业 13698779505 26 微信 FALSE
26 25 唐儒 大健康轻创业 13099992576 26 微信 FALSE
27 26 窦海涛 人工智能 13845432196 26 微信 FALSE
28 27 刘磊 跨境电子商务 13529005525 26 微信 FALSE
29 28 李若菡 道医 有事,已退费 14787451514 26 微信 FALSE
30 29 戴军 个体 13529383555 26 微信 FALSE
31 30 李延辉 生物医药 18908789027 26 微信 FALSE
32 31 张鹏程 医疗机器人 13308789694 26 微信 FALSE
33 32 倪钜 珠宝 13150587227 26 微信 FALSE
34 33 陈昭宇 文旅 免费 17600099614 FALSE
35 34 张馨艺 自媒体 免费 13888651626 FALSE
36 35 黄升 社群运营 免费 1 FALSE
37 36 马艺 AI跨境 免费 FALSE
38 37 岳岳 自媒体 18687329243 志愿者 FALSE
39 38 岳岳朋友刘致豪 自媒体 15188137009 志愿者 FALSE
40 39 岳岳王天胜 自媒体 13320543169 志愿者 FALSE
41 40 岳岳宗彗莲 自媒体 19988427837 志愿者 FALSE
42 41 陆起 飞书 免费 13347705547 FALSE
43 42 陆起朋友王维 飞书 免费 13521170687 FALSE
44 43 陆起朋友 飞书 免费 FALSE
45 44 刘翔 财务公司 18725032602 FALSE
46 45 宋宇 后端工程师 18388411803 FALSE
47 46 高立仁 新能源汽车公司 19187151953 FALSE
48 47 赵若曦 云纺集团 免费 13987673203 FALSE
49 48 李李 智行滇峰说演讲 免费 FALSE
50 49 海浪 安信集团 免费 17713249555 FALSE
51 50 姚超 思享界 13033031987 志愿者 FALSE
52 51 姚超摄影师 思享界 摄影师 FALSE
53 52 小唯 志愿者 FALSE
54 53 朱晓丽 律师 18206868114 志愿者 FALSE
55 54 涞涞 康养中医药 13089899853 志愿者 FALSE
56 55 赖应海 创业者 18157390046 志愿者 FALSE
57 56 冯立伟 量迹 13220881316 总负责 FALSE
58 57 赵广明 鬼才明创意工作室 18611218523 总策划 FALSE
59 58 甘昭泉 量迹 18585164448 分享嘉宾 FALSE
60 59 丁靖洋 派动科技 18221820102 分享嘉宾 FALSE
61 60 刘伯煜 大筑科技 18312876673 分享嘉宾 FALSE
62 61 曹平飞 阿里云 嘉宾 FALSE
63 62 李欢 阿里云 18187811772 分享嘉宾 FALSE
64 63 暂定 百度 分享嘉宾 FALSE
65 64 郑文浩 百度 嘉宾 15820798660
66 65 三三 彩创、飞书 分享嘉宾
67 66 钱丹 彩创、飞书 18788400115 联合主办
68 67 沙梅兰 掌上春城 15912524519 媒体
69 68 李凌竹 广播电视台 13888585678 媒体
70 69 晏丽 云南广电传媒 15911520520 分享嘉宾
71 70 拍摄 云南广电传媒 15911520520 摄影师
72 71 采访 云南广电传媒 15911520520 采访
73 72 许应烨 AI 15331657816
74 73 汤小姐 打杂 已进群 13922791778
75 74 张玮楠 茶企 已有微信 18289790037
76 75 李逸飞 图书电商 嘉宾 18810586550
77 76 Robyn 教育留学 w 13211788556
78 77 刘胜 教育 w 14787812507
79 78 闵莹 软件开发 w 18687510353
80 79 金玉辉 昆明飞威报刊图书有限公司 / 18388109039
81 80 陈佑 图书批发 / 18288927098
82 81 蒋晓伟 法务 w 13085392813 26 微信
83 82 xiaoMa IT 1.32387E+11
84 83 王瑞琦 科技 w 15925183977
85 84 秦贵 图书行业 / 18788443543
86 85 杨慧 图书电商 / 18869769859
87 86 赵榕 图书类目 / 18608899476
88 87 邓勇 教育 w 18208813207
89 88 张出高 教育 w 18206871081
90 89 王海倩 教育 15559634666
91 90 陈锐嶂 商贸流通 w 13008671498
92 91 雷乔玲 财务 w 15198838759
93 92 lucky 跨境电商 w 18214611725
94 93 李忠岘 教育 w 15808886488
95 94 陈奕恒 跨境电商 w 15887158441
96 95 经纬 科技 13987382399
97 96 段晓方 开发 w 17687023127
98 97 高立仁 新能源汽车 w 17504311180
99 98 吴松泽 教师 w 18762304432
100 99 周正宜 跨境电商 w 17689845870
101 100 赵云翔 跨境电商 w 17751891232
102 101 李皓月 医疗 w 17368456286
103 102 徐衡 设计 w 13518713214 26 微信
104 103 葛一瑶 电力 w 15912137626 26 微信
105 104 陆易航 智能开发 w 15087148039 26 微信
106 105 邓智铭 程序员 w 13211685339
107 106 姚来弟 计算机 w 18288440214
108 107 萧清 数字文创 w 17606909560
109 108 发发 新媒体 13278714977
110 109 塔米 教育 13250714571
111 110 张华英 律师 w 13658853415
112 111 王天胜 直播带货 15687698763
113 112 张橙 传媒 w 18082712849 26 微信
114 113 周总 商业服务,为企业申办各类型补贴 w 13987141588
115 114 吴潇雪 法律 w 15969591218
116 115 蒋乃浩 娱乐 w 13888862549 26 微信
117 116 李烨 医疗 w 13619605970
118 117 白羽 文化产业 w 13187856156
119 118 李易为 文旅 13085358008
120 119 高嘉煜 软件开发 w 15126615549 26 微信
121 120 毕先生 花卉 w 13888466977
122 121 汤镇宇 互联网 w 13629615669
123 122 刘先生 电商 w 15908809814
124 123 晏瑞婕 金融 w 13013313443
125 124 杨势挥 KTV w 18787440187
126 125 滕乾忠 教育 18187128470
127 126 芾邴 教育,科技 13161994127
128 127 Alice 云计算 15087060387
129 128 李丹 法律 w 15125990928
130 129 吴疆 能源 w 13718215039
131 130 高丹妮 房地产、新媒体 免费 15287335911 青商会员
132 131 聂怡然 智能餐饮 w 13808725796
133 132 一雄 OPC w 18812269173 26 微信
134 133 杜丽娜 自由职业 w 13888115882
135 134 ayao 音乐ai 18987063951
136 135 周其建 人工智能 18887103888
137 136 杨万峰 制造业 1528821438
138 137 Mia 旅居 19911866777
139 138 张雪娟 教育 18988153273
140 139 禹姣 银行业 15887205279
141 140 郑华 零售 15140295598
142 141 毕玉 AI 19146959276
143 142 象叔 IP内容营销 13888834251
144 143 王贺 旅游规划与设计 13987188550
145 144 瑾瑾瑾 AI创作 13658897411
146 145 阿布 传统行业 15087097719
147 146 杨文虎 新闻 13577034568
148 147 知识产权 13211662238
149 148 安娜 自由职业 18208706046
150 149 暴雨 医药 15969182571
151 150 小源 教育 18314522519
152 151 杨泽文 电力 17803866644
153 152 張競丹 旅遊 18987589587
154 153 赵锐涵 金融 13529216958
155 154 范洁 大健康食品 13888182707
156 155 陈莹 服务 19224081189
157 156 杨立平 漫剧 13988620996 26 微信
158 157 杨立蕊 漫剧 13888707563 26 微信
159 158 郭艳 茶行业 18787145995 26 微信
160 159 孟妹 广告 13759060777 26 微信
161 160 丁艳玲 餐饮行业 18187315565 26 微信
162 161 钟源 教育/餐饮 18314522519 26 微信
163 162 付吟吟 昆明精悦广告有限公司总经理 免费 13619622871 青商会员
164 163 李慧 云南智慧光经贸有限公司 法人 免费 15825186018 青商会员
165 164 陈锦玥 自由职业 18987471556 26 微信
166 165 鄢木西 星梧科技(云南)有限责任公司 19912828161 26 微信
167 166 孔浩 IT信息行业 13668780930 26 微信
168 167 李拉 互联网 13759408584 26 微信
169 168 吴芬 食品电商 15331725758 26 微信
170 169 黄政葵 26 微信
171 170 温立新 村播 免费 13330499193
172 171 孔德风 百度 嘉宾 18087960002
173 172 李程旻 火山引擎 免费 19187149926
174 173 王若谷 咖啡 免费 18468038180
175 174 张童 电力行业 18187527804 26 微信
176 175 李虹婷 投资 18608738555 青商会员
177 176 周鑫 银行 青商会员
178 177 张志刚 青创会 嘉宾 15508805566
179 178 段晓芳 全栈开发工程师 免费 17687023127
180 179 张培恒 富滇银行-oa 免费 18288711024
181 180 王娇 软件运维 免费 15808742554
182 181 宁杰 医疗行业 13698786789 26 微信

310
main.py Normal file
View File

@@ -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)

BIN
static/image.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

298
templates/add_user.html Normal file
View File

@@ -0,0 +1,298 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>云南AI共生大会 - 添加嘉宾</title>
<style>
:root {
--primary-color: #00f2ff; /* Cyan from the AI sphere/text */
--secondary-color: #0066ff; /* Rich blue */
--bg-color: #050814; /* Deep space blue/black */
--card-bg: rgba(12, 24, 50, 0.5); /* Glassy blue tint */
--text-color: #ffffff;
--text-muted: #b0c4de; /* Light blue-grey */
--accent-glow: rgba(0, 242, 255, 0.6);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
background-color: var(--bg-color);
color: var(--text-color);
background-image:
radial-gradient(circle at 50% 0%, #1a3a75 0%, #050814 60%),
radial-gradient(circle at 85% 30%, rgba(0, 242, 255, 0.1) 0%, transparent 40%),
radial-gradient(circle at 15% 70%, rgba(0, 102, 255, 0.15) 0%, transparent 40%);
background-attachment: fixed;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
overflow-x: hidden;
}
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background:
linear-gradient(rgba(255,255,255,0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,0.03) 1px, transparent 1px);
background-size: 50px 50px;
pointer-events: none;
z-index: -1;
}
.container {
width: 100%;
max-width: 600px;
padding: 30px 20px;
position: relative;
z-index: 1;
}
header {
text-align: center;
margin-bottom: 40px;
padding-bottom: 20px;
border-bottom: 1px solid rgba(0, 242, 255, 0.2);
}
.header-img {
width: 100%;
max-height: 220px;
object-fit: cover;
border-radius: 12px;
margin-bottom: 25px;
box-shadow: 0 0 20px rgba(0, 242, 255, 0.2);
border: 1px solid rgba(0, 242, 255, 0.3);
}
h1 {
font-size: 2.4rem;
margin-bottom: 15px;
background: linear-gradient(180deg, #ffffff 0%, #d0eaff 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-weight: 900;
letter-spacing: 1px;
text-shadow: 0 0 30px rgba(0, 242, 255, 0.4);
position: relative;
}
.card {
background: var(--card-bg);
border-radius: 16px;
padding: 30px;
margin-bottom: 25px;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(0, 242, 255, 0.2);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.5), inset 0 0 20px rgba(0, 242, 255, 0.05);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
h2 {
color: #fff;
text-align: center;
margin-bottom: 25px;
font-weight: 700;
letter-spacing: 1px;
}
.input-group {
margin-bottom: 25px;
}
label {
display: block;
margin-bottom: 10px;
font-size: 0.95rem;
color: var(--primary-color);
font-weight: 500;
}
input, textarea, select {
width: 100%;
padding: 14px 16px;
background: rgba(0, 0, 0, 0.4);
border: 1px solid rgba(0, 242, 255, 0.3);
border-radius: 8px;
color: white;
font-size: 1rem;
outline: none;
transition: all 0.3s;
box-shadow: inset 0 2px 5px rgba(0,0,0,0.3);
}
input:focus, textarea:focus {
border-color: var(--primary-color);
box-shadow: 0 0 15px rgba(0, 242, 255, 0.3), inset 0 2px 5px rgba(0,0,0,0.3);
background: rgba(0, 0, 0, 0.6);
}
button {
width: 100%;
padding: 16px;
border: none;
border-radius: 8px;
background: linear-gradient(90deg, #0051ff 0%, #00f2ff 100%);
color: #fff;
font-size: 1.1rem;
font-weight: 700;
cursor: pointer;
transition: all 0.3s;
text-transform: uppercase;
letter-spacing: 1px;
box-shadow: 0 4px 15px rgba(0, 242, 255, 0.3);
text-shadow: 0 1px 2px rgba(0,0,0,0.2);
}
button:hover {
opacity: 1;
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0, 242, 255, 0.5);
background: linear-gradient(90deg, #0040cc 0%, #00d0dd 100%);
}
button:disabled {
background: #2a3b55;
cursor: not-allowed;
opacity: 0.6;
box-shadow: none;
transform: none;
}
.hidden {
display: none;
}
.error-msg {
color: #ff6b6b;
font-size: 0.9rem;
margin-top: 15px;
text-align: center;
background: rgba(255, 107, 107, 0.1);
padding: 10px;
border-radius: 6px;
border: 1px solid rgba(255, 107, 107, 0.3);
}
.success-msg {
color: #00ff88;
font-size: 1.2rem;
text-align: center;
margin: 20px 0 10px;
text-shadow: 0 0 10px rgba(0, 255, 136, 0.5);
}
.loader {
border: 3px solid rgba(255,255,255,0.1);
border-radius: 50%;
border-top: 3px solid var(--primary-color);
width: 20px;
height: 20px;
animation: spin 0.8s linear infinite;
display: inline-block;
vertical-align: middle;
margin-right: 10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div class="container">
<header>
<img src="/static/image.jpg" alt="云南AI共生大会" class="header-img" onerror="this.style.display='none'">
<h1>添加新嘉宾</h1>
</header>
<div class="card">
<form id="add-user-form" onsubmit="submitAddUser(event)">
<div class="input-group">
<label>姓名</label>
<input type="text" id="name" required placeholder="请输入嘉宾姓名">
</div>
<div class="input-group">
<label>手机号码</label>
<input type="text" id="phone" required placeholder="请输入手机号码">
</div>
<div class="input-group">
<label>单位名称 / 行业</label>
<input type="text" id="company" placeholder="请输入单位名称或行业信息">
</div>
<button type="submit" id="submit-btn">确认添加</button>
</form>
<div id="submit-error" class="error-msg hidden"></div>
<div id="submit-success" class="success-msg hidden"></div>
</div>
<div style="text-align: center; margin-top: 20px;">
<a href="/" style="color: var(--text-muted); text-decoration: none; border-bottom: 1px dashed var(--text-muted);">返回签到首页</a>
</div>
</div>
<script>
async function submitAddUser(e) {
e.preventDefault();
const btn = document.getElementById('submit-btn');
const errorDiv = document.getElementById('submit-error');
const successDiv = document.getElementById('submit-success');
btn.disabled = true;
btn.innerHTML = '<span class="loader"></span> 提交中...';
errorDiv.classList.add('hidden');
successDiv.classList.add('hidden');
const payload = {
name: document.getElementById('name').value,
phone: document.getElementById('phone').value,
industry_company: document.getElementById('company').value
};
try {
const response = await fetch('/api/add-user', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
const data = await response.json();
if (response.ok) {
successDiv.textContent = "添加成功!该嘉宾现在可以进行签到了。";
successDiv.classList.remove('hidden');
document.getElementById('add-user-form').reset();
} else {
errorDiv.textContent = data.message || "提交失败";
errorDiv.classList.remove('hidden');
}
} catch (err) {
errorDiv.textContent = "网络错误,请稍后重试";
errorDiv.classList.remove('hidden');
} finally {
btn.disabled = false;
btn.textContent = '确认添加';
}
}
</script>
</body>
</html>

810
templates/index.html Normal file
View File

@@ -0,0 +1,810 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>云南AI共生大会 - 签到</title>
<style>
:root {
--primary-color: #00f2ff; /* Cyan from the AI sphere/text */
--secondary-color: #0066ff; /* Rich blue */
--bg-color: #050814; /* Deep space blue/black */
--card-bg: rgba(12, 24, 50, 0.5); /* Glassy blue tint */
--text-color: #ffffff;
--text-muted: #b0c4de; /* Light blue-grey */
--accent-glow: rgba(0, 242, 255, 0.6);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
background-color: var(--bg-color);
color: var(--text-color);
/* Deep radial gradient mimicking the poster's atmosphere */
background-image:
radial-gradient(circle at 50% 0%, #1a3a75 0%, #050814 60%),
radial-gradient(circle at 85% 30%, rgba(0, 242, 255, 0.1) 0%, transparent 40%),
radial-gradient(circle at 15% 70%, rgba(0, 102, 255, 0.15) 0%, transparent 40%);
background-attachment: fixed;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
overflow-x: hidden;
}
/* Add a subtle grid or tech-line overlay if desired, but keep it clean for now */
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background:
linear-gradient(rgba(255,255,255,0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,0.03) 1px, transparent 1px);
background-size: 50px 50px;
pointer-events: none;
z-index: -1;
}
.container {
width: 100%;
max-width: 600px;
padding: 30px 20px;
position: relative;
z-index: 1;
}
header {
text-align: center;
margin-bottom: 40px;
padding-bottom: 20px;
border-bottom: 1px solid rgba(0, 242, 255, 0.2);
}
.header-img {
width: 100%;
max-height: 220px;
object-fit: cover;
border-radius: 12px;
margin-bottom: 25px;
box-shadow: 0 0 20px rgba(0, 242, 255, 0.2);
border: 1px solid rgba(0, 242, 255, 0.3);
}
h1 {
font-size: 2.4rem;
margin-bottom: 15px;
/* Poster style: Bold white/cyan gradient text */
background: linear-gradient(180deg, #ffffff 0%, #d0eaff 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-weight: 900;
letter-spacing: 1px;
text-shadow: 0 0 30px rgba(0, 242, 255, 0.4);
position: relative;
}
.event-info {
font-size: 0.95rem;
color: #dbe4ff;
line-height: 1.7;
margin-top: 20px;
text-align: left;
background: rgba(0, 20, 60, 0.4);
padding: 20px;
border-radius: 8px;
border-left: 4px solid var(--primary-color);
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
backdrop-filter: blur(5px);
}
.card {
background: var(--card-bg);
border-radius: 16px;
padding: 30px;
margin-bottom: 25px;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(0, 242, 255, 0.2);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.5), inset 0 0 20px rgba(0, 242, 255, 0.05);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.card:hover {
box-shadow: 0 8px 40px 0 rgba(0, 0, 0, 0.6), inset 0 0 20px rgba(0, 242, 255, 0.1);
}
h2 {
color: #fff;
text-align: center;
margin-bottom: 25px;
font-weight: 700;
letter-spacing: 1px;
}
.input-group {
margin-bottom: 25px;
}
label {
display: block;
margin-bottom: 10px;
font-size: 0.95rem;
color: var(--primary-color);
font-weight: 500;
}
input, textarea, select {
width: 100%;
padding: 14px 16px;
background: rgba(0, 0, 0, 0.4);
border: 1px solid rgba(0, 242, 255, 0.3);
border-radius: 8px;
color: white;
font-size: 1rem;
outline: none;
transition: all 0.3s;
box-shadow: inset 0 2px 5px rgba(0,0,0,0.3);
}
input:focus, textarea:focus {
border-color: var(--primary-color);
box-shadow: 0 0 15px rgba(0, 242, 255, 0.3), inset 0 2px 5px rgba(0,0,0,0.3);
background: rgba(0, 0, 0, 0.6);
}
/* Placeholder styling */
::placeholder {
color: rgba(255, 255, 255, 0.3);
}
button {
width: 100%;
padding: 16px;
border: none;
border-radius: 8px;
/* Gradient button */
background: linear-gradient(90deg, #0051ff 0%, #00f2ff 100%);
color: #fff;
font-size: 1.1rem;
font-weight: 700;
cursor: pointer;
transition: all 0.3s;
text-transform: uppercase;
letter-spacing: 1px;
box-shadow: 0 4px 15px rgba(0, 242, 255, 0.3);
text-shadow: 0 1px 2px rgba(0,0,0,0.2);
}
button:hover {
opacity: 1;
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0, 242, 255, 0.5);
background: linear-gradient(90deg, #0040cc 0%, #00d0dd 100%);
}
button:active {
transform: translateY(0);
}
button:disabled {
background: #2a3b55;
cursor: not-allowed;
opacity: 0.6;
box-shadow: none;
transform: none;
}
.hidden {
display: none;
}
.error-msg {
color: #ff6b6b;
font-size: 0.9rem;
margin-top: 15px;
text-align: center;
background: rgba(255, 107, 107, 0.1);
padding: 10px;
border-radius: 6px;
border: 1px solid rgba(255, 107, 107, 0.3);
}
.success-msg {
color: #00ff88;
font-size: 1.8rem;
text-align: center;
margin: 20px 0 10px;
text-shadow: 0 0 10px rgba(0, 255, 136, 0.5);
}
.seat-info {
font-size: 3rem;
font-weight: 800;
color: #fff;
margin: 30px 0;
text-shadow: 0 0 30px var(--primary-color);
padding: 30px;
border: 2px solid var(--primary-color);
border-radius: 16px;
background: rgba(0, 242, 255, 0.05);
box-shadow: 0 0 40px rgba(0, 242, 255, 0.15), inset 0 0 20px rgba(0, 242, 255, 0.1);
position: relative;
overflow: hidden;
}
/* Shine effect on seat info */
.seat-info::after {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: linear-gradient(45deg, transparent, rgba(255,255,255,0.1), transparent);
transform: rotate(45deg);
animation: shine 3s infinite;
}
@keyframes shine {
0% { transform: translateX(-100%) rotate(45deg); }
100% { transform: translateX(100%) rotate(45deg); }
}
.user-preview {
background: rgba(0, 81, 255, 0.15);
padding: 20px;
border-radius: 10px;
margin-bottom: 25px;
border: 1px solid rgba(0, 81, 255, 0.4);
text-align: center;
}
.user-preview h3 {
margin-bottom: 8px;
font-size: 1.3rem;
color: #fff;
}
.user-preview p {
color: #b0c4de;
font-size: 0.95rem;
}
/* Loading spinner */
.loader {
border: 3px solid rgba(255,255,255,0.1);
border-radius: 50%;
border-top: 3px solid var(--primary-color);
width: 20px;
height: 20px;
animation: spin 0.8s linear infinite;
display: inline-block;
vertical-align: middle;
margin-right: 10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.user-select-item {
padding: 15px;
background: rgba(255,255,255,0.05);
margin-bottom: 8px;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
border: 1px solid transparent;
}
.user-select-item:hover {
background: rgba(0, 242, 255, 0.1);
border-color: var(--primary-color);
}
/* Tablemates styling */
.tablemates-section {
margin-top: 25px;
border-top: 1px solid rgba(255,255,255,0.1);
padding-top: 20px;
}
.tablemate-card {
background: rgba(255, 255, 255, 0.05);
border-radius: 10px;
padding: 12px;
margin-bottom: 10px;
display: flex;
align-items: center;
border: 1px solid rgba(255, 255, 255, 0.1);
transition: transform 0.2s;
cursor: pointer;
position: relative;
}
.tablemate-card:hover {
transform: translateX(5px);
background: rgba(255, 255, 255, 0.08);
border-color: var(--primary-color);
}
.tablemate-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: linear-gradient(135deg, var(--secondary-color), var(--primary-color));
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
margin-right: 15px;
font-size: 1.2rem;
flex-shrink: 0;
}
.tablemate-info {
text-align: left;
flex-grow: 1;
}
.tablemate-name {
font-weight: bold;
color: #fff;
font-size: 1rem;
}
.tablemate-industry {
font-size: 0.8rem;
color: var(--text-muted);
}
.click-hint {
font-size: 0.7rem;
color: var(--primary-color);
opacity: 0.7;
margin-left: 10px;
}
/* Modal Styling */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0,0,0,0.8);
backdrop-filter: blur(5px);
}
.modal-content {
background: var(--bg-color);
margin: 15% auto;
padding: 25px;
border: 1px solid var(--primary-color);
border-radius: 15px;
width: 90%;
max-width: 500px;
box-shadow: 0 0 30px rgba(0, 242, 255, 0.3);
position: relative;
animation: modalOpen 0.3s;
}
@keyframes modalOpen {
from {opacity: 0; transform: translateY(-50px);}
to {opacity: 1; transform: translateY(0);}
}
.close {
color: #aaa;
float: right;
font-size: 28px;
font-weight: bold;
cursor: pointer;
}
.close:hover,
.close:focus {
color: var(--primary-color);
text-decoration: none;
cursor: pointer;
}
.detail-item {
margin-bottom: 15px;
border-bottom: 1px solid rgba(255,255,255,0.1);
padding-bottom: 10px;
}
.detail-label {
color: var(--primary-color);
font-size: 0.85rem;
margin-bottom: 5px;
text-transform: uppercase;
letter-spacing: 1px;
}
.detail-value {
color: #fff;
font-size: 1rem;
line-height: 1.5;
}
</style>
</head>
<body>
<div class="container">
<header>
<img src="/static/image.jpg" alt="云南AI共生大会" class="header-img" onerror="this.style.display='none'">
<div style="text-align: center; margin-bottom: 10px; color: var(--primary-color); font-size: 0.8rem; letter-spacing: 2px; font-weight: 600; opacity: 0.9;">
2026 INTELLIGENT LEADERSHIP • AI SYMBIOSIS
</div>
<h1>云南AI共生大会</h1>
<div class="event-info">
<p>📅 <strong>时间:</strong>1月10日 下午 2:00</p>
<p>📍 <strong>地点:</strong>金鼎科技园18号平台B座一楼报告厅</p>
<p>💡 <strong>内容:</strong>邀请重磅大咖分享AI在各行业的企业应用及案例含深度交流环节。</p>
</div>
</header>
<!-- Step 1: Search -->
<div id="step-search" class="card">
<h2 style="margin-bottom: 20px;">嘉宾签到</h2>
<div class="input-group">
<label>请输入手机号码或姓名</label>
<input type="text" id="search-input" placeholder="例如13800000000 或 张三">
</div>
<button onclick="searchUser()" id="search-btn">查询</button>
<div id="search-error" class="error-msg hidden"></div>
<div id="user-list" class="hidden" style="margin-top: 15px;"></div>
</div>
<!-- Step 2: Fill Info -->
<div id="step-form" class="card hidden">
<div class="user-preview">
<h3 id="display-name"></h3>
<p id="display-phone"></p>
<p id="display-company" style="font-size: 0.8rem; margin-top: 5px;"></p>
</div>
<form id="checkin-form" onsubmit="submitCheckin(event)">
<input type="hidden" id="gsdh-id">
<div class="input-group">
<label>姓名</label>
<input type="text" id="form-name" required>
</div>
<div class="input-group">
<label>手机号码 (不公开)</label>
<input type="text" id="form-phone" required>
</div>
<div class="input-group">
<label>单位名称</label>
<input type="text" id="form-company" placeholder="点此输入">
</div>
<div class="input-group">
<label>职务 (不公开)</label>
<input type="text" id="form-position" placeholder="点此输入">
</div>
<div class="input-group">
<label>公司主要经营 / 业务</label>
<textarea id="form-business" rows="2" placeholder="点此输入"></textarea>
</div>
<div class="input-group">
<label>2026年业务愿景</label>
<textarea id="form-vision" rows="3" placeholder="点此输入 (为了业务更好对接,最好表述清晰)"></textarea>
</div>
<input type="hidden" id="form-location" value="">
<button type="submit" id="submit-btn">确认签到</button>
</form>
<div id="submit-error" class="error-msg hidden"></div>
</div>
<!-- Step 3: Success -->
<div id="step-success" class="card hidden" style="text-align: center; padding: 40px 20px;">
<div style="font-size: 4rem; margin-bottom: 20px;">🎉</div>
<h2 class="success-msg">签到成功!</h2>
<p style="color: var(--text-muted); margin-bottom: 20px;">您的座位已分配</p>
<div id="seat-display" class="seat-info">
<!-- Seat number will be inserted here -->
</div>
<div id="tablemates-container" class="tablemates-section hidden">
<p style="color: var(--primary-color); margin-bottom: 15px; font-size: 0.9rem; text-transform: uppercase; letter-spacing: 1px;">同桌嘉宾推荐 <span style="font-size: 0.7em; opacity: 0.7; text-transform: none;">(点击卡片查看详情)</span></p>
<div id="tablemates-list">
<!-- Tablemates inserted here -->
</div>
</div>
<p style="margin-top: 20px;">请入座等待会议开始</p>
<button onclick="resetFlow()" style="margin-top: 30px; background: transparent; border: 1px solid rgba(255,255,255,0.2);">返回首页</button>
</div>
<!-- Step 4: Already Signed -->
<div id="step-already-signed" class="card hidden" style="text-align: center; padding: 40px 20px;">
<div style="font-size: 4rem; margin-bottom: 20px;"></div>
<h2 class="success-msg" style="color: #00f2ff;">您已完成签到</h2>
<div class="user-preview" style="margin: 20px 0; background: rgba(255,255,255,0.05); border: none;">
<h3 id="signed-name" style="margin-bottom: 5px;"></h3>
<p id="signed-phone" style="opacity: 0.7;"></p>
</div>
<p style="color: var(--text-muted); margin-bottom: 20px;">您的座位是</p>
<div id="signed-seat-display" class="seat-info">
<!-- Seat number will be inserted here -->
</div>
<div id="signed-tablemates-container" class="tablemates-section hidden">
<p style="color: var(--primary-color); margin-bottom: 15px; font-size: 0.9rem; text-transform: uppercase; letter-spacing: 1px;">同桌嘉宾推荐 <span style="font-size: 0.7em; opacity: 0.7; text-transform: none;">(点击卡片查看详情)</span></p>
<div id="signed-tablemates-list">
<!-- Tablemates inserted here -->
</div>
</div>
<p style="margin-top: 20px;">请入座等待会议开始</p>
<button onclick="resetFlow()" style="margin-top: 30px; background: transparent; border: 1px solid rgba(255,255,255,0.2);">返回首页</button>
</div>
</div>
<!-- Modal for details -->
<div id="detail-modal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeModal()">&times;</span>
<div style="text-align: center; margin-bottom: 20px;">
<div id="modal-avatar" style="width: 60px; height: 60px; border-radius: 50%; background: linear-gradient(135deg, var(--secondary-color), var(--primary-color)); display: inline-flex; align-items: center; justify-content: center; font-weight: bold; font-size: 1.5rem; margin-bottom: 10px;"></div>
<h2 id="modal-name" style="color: #fff; margin-bottom: 5px;"></h2>
<p id="modal-industry" style="color: var(--primary-color); font-size: 0.9rem;"></p>
</div>
<div class="detail-item">
<div class="detail-label">单位名称</div>
<div id="modal-company" class="detail-value"></div>
</div>
<div class="detail-item">
<div class="detail-label">职位</div>
<div id="modal-position" class="detail-value"></div>
</div>
<div class="detail-item">
<div class="detail-label">公司主要经营 / 业务</div>
<div id="modal-business" class="detail-value"></div>
</div>
<div class="detail-item" style="border-bottom: none;">
<div class="detail-label">2026年业务愿景</div>
<div id="modal-vision" class="detail-value"></div>
</div>
</div>
</div>
<script>
// Store selected user data
let currentUser = null;
async function searchUser() {
const query = document.getElementById('search-input').value.trim();
const btn = document.getElementById('search-btn');
const errorDiv = document.getElementById('search-error');
const listDiv = document.getElementById('user-list');
if (!query) {
errorDiv.textContent = "请输入查询内容";
errorDiv.classList.remove('hidden');
return;
}
// Reset UI
errorDiv.classList.add('hidden');
listDiv.classList.add('hidden');
listDiv.innerHTML = '';
btn.disabled = true;
btn.innerHTML = '<span class="loader"></span> 查询中...';
try {
const response = await fetch(`/api/search?query=${encodeURIComponent(query)}`);
const data = await response.json();
if (response.ok) {
if (data.multiple) {
// Handle multiple results
listDiv.classList.remove('hidden');
listDiv.innerHTML = '<p style="margin-bottom:10px; color:var(--text-muted)">查询到多位嘉宾,请选择:</p>';
data.users.forEach(user => {
const item = document.createElement('div');
item.className = 'user-select-item';
item.innerHTML = `<strong>${user.name}</strong> <span style="font-size:0.8em; opacity:0.7">(${user.phone})</span>`;
item.onclick = () => selectUser(user);
listDiv.appendChild(item);
});
} else if (data.already_signed) {
// Show already signed screen
document.getElementById('step-search').classList.add('hidden');
document.getElementById('step-already-signed').classList.remove('hidden');
document.getElementById('signed-name').textContent = data.user.name;
document.getElementById('signed-phone').textContent = data.user.phone;
document.getElementById('signed-seat-display').textContent = data.seat || "自由席";
// Show tablemates if available
if (data.tablemates && data.tablemates.length > 0) {
renderTablemates(data.tablemates, 'signed-tablemates-list', 'signed-tablemates-container');
} else {
document.getElementById('signed-tablemates-container').classList.add('hidden');
}
} else {
// Single user found
selectUser(data.user);
}
} else {
errorDiv.textContent = data.message || "查询失败";
errorDiv.classList.remove('hidden');
errorDiv.style.color = '#ff4d4d';
}
} catch (err) {
errorDiv.textContent = "网络错误,请稍后重试";
errorDiv.classList.remove('hidden');
} finally {
btn.disabled = false;
btn.textContent = '查询';
}
}
function selectUser(user) {
currentUser = user;
// Populate form
document.getElementById('gsdh-id').value = user.new_id;
document.getElementById('form-name').value = user.name;
document.getElementById('form-phone').value = user.phone;
document.getElementById('form-company').value = user.industry_company || ''; // Pre-fill if we have something roughly mapping
// Display preview
document.getElementById('display-name').textContent = user.name;
document.getElementById('display-phone').textContent = user.phone;
document.getElementById('display-company').textContent = user.industry_company || '暂无单位信息';
// Switch steps
document.getElementById('step-search').classList.add('hidden');
document.getElementById('step-form').classList.remove('hidden');
}
async function submitCheckin(e) {
e.preventDefault();
const btn = document.getElementById('submit-btn');
const errorDiv = document.getElementById('submit-error');
btn.disabled = true;
btn.innerHTML = '<span class="loader"></span> 提交中...';
errorDiv.classList.add('hidden');
const payload = {
gsdh_id: document.getElementById('gsdh-id').value,
name: document.getElementById('form-name').value,
phone: document.getElementById('form-phone').value,
company_name: document.getElementById('form-company').value,
position: document.getElementById('form-position').value,
business_scope: document.getElementById('form-business').value,
vision_2026: document.getElementById('form-vision').value,
location: document.getElementById('form-location').value
};
try {
const response = await fetch('/api/checkin', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
const data = await response.json();
if (response.ok) {
// Show assigned seat
document.getElementById('seat-display').textContent = data.seat || "自由席";
// Show tablemates if available
if (data.tablemates && data.tablemates.length > 0) {
renderTablemates(data.tablemates, 'tablemates-list', 'tablemates-container');
} else {
document.getElementById('tablemates-container').classList.add('hidden');
}
document.getElementById('step-form').classList.add('hidden');
document.getElementById('step-success').classList.remove('hidden');
} else {
errorDiv.textContent = data.message || "提交失败";
errorDiv.classList.remove('hidden');
}
} catch (err) {
errorDiv.textContent = "网络错误,请稍后重试";
errorDiv.classList.remove('hidden');
} finally {
btn.disabled = false;
btn.textContent = '确认签到';
}
}
function renderTablemates(tablemates, listId, containerId) {
const list = document.getElementById(listId);
list.innerHTML = '';
tablemates.forEach(mate => {
const initial = mate.name ? mate.name.charAt(0) : '?';
const div = document.createElement('div');
div.className = 'tablemate-card';
div.innerHTML = `
<div class="tablemate-avatar">${initial}</div>
<div class="tablemate-info">
<div class="tablemate-name">${mate.name} <span class="click-hint">点击详情</span></div>
<div class="tablemate-industry">${mate.industry}</div>
</div>
`;
// Add click event listener for details
div.onclick = () => showModal(mate);
list.appendChild(div);
});
document.getElementById(containerId).classList.remove('hidden');
}
function showModal(mate) {
const modal = document.getElementById('detail-modal');
const initial = mate.name ? mate.name.charAt(0) : '?';
document.getElementById('modal-avatar').textContent = initial;
document.getElementById('modal-name').textContent = mate.name;
document.getElementById('modal-industry').textContent = mate.industry;
document.getElementById('modal-company').textContent = mate.company_name || '未填写';
document.getElementById('modal-position').textContent = mate.position || '未填写';
document.getElementById('modal-business').textContent = mate.business_scope || '未填写';
document.getElementById('modal-vision').textContent = mate.vision_2026 || '未填写';
modal.style.display = "block";
}
function closeModal() {
document.getElementById('detail-modal').style.display = "none";
}
// Close modal when clicking outside
window.onclick = function(event) {
const modal = document.getElementById('detail-modal');
if (event.target == modal) {
modal.style.display = "none";
}
}
function resetFlow() {
document.getElementById('step-success').classList.add('hidden');
document.getElementById('step-already-signed').classList.add('hidden');
document.getElementById('step-search').classList.remove('hidden');
document.getElementById('search-input').value = '';
document.getElementById('user-list').innerHTML = '';
document.getElementById('checkin-form').reset();
currentUser = null;
}
</script>
</body>
</html>