diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 7e8ebfd..0438265 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -1,159 +1,30 @@ -name: Docker 构建与部署 +name: 部署到服务器 on: push: branches: [main, master] workflow_dispatch: - inputs: - arch: - description: '构建架构' - required: true - default: 'amd64' - type: choice - options: - - amd64 - - arm64 - - both - upload_only: - description: '仅上传已存在的tar文件' - required: false - default: 'false' - type: boolean env: - IMAGE_NAME: checkin_sys - IMAGE_TAG: latest - CONTAINER_NAME: checkin_sys-container - LOCAL_PORT: 8800 - CONTAINER_PORT: 8800 + TARGET_DIR: /home/quant/data/dev/checkin jobs: - build: - runs-on: ubuntu - if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && !inputs.upload_only) - - steps: - - name: 设置国内镜像源 - run: | - git config --global url."https://ghproxy.com/".insteadOf "https://github.com/" - - - name: 配置 Docker 镜像源 - run: | - sudo mkdir -p /etc/docker - | sudo tee /etc/docker/daemon.json << 'EOF' -{ - "registry-mirrors": [ - "https://docker.mirrors.ustc.edu.cn", - "https://hub-mirror.c.163.com", - "https://mirror.baidubce.com" - ] -} -EOF - sudo service docker restart || true - - - name: 检出代码 - uses: actions/checkout@v4 - - - name: 设置 Buildx - uses: docker/setup-buildx-action@v3 - with: - driver-opts: | - image=moby/buildkit:buildx-stable-1 - - - name: 提取架构信息 - id: arch - run: | - if [ "${{ github.event.inputs.arch }}" == "arm64" ]; then - echo "platform=linux/arm64" >> $GITHUB_OUTPUT - echo "arch_suffix=-arm64" >> $GITHUB_OUTPUT - elif [ "${{ github.event.inputs.arch }}" == "both" ]; then - echo "platforms=linux/amd64,linux/arm64" >> $GITHUB_OUTPUT - else - echo "platform=linux/amd64" >> $GITHUB_OUTPUT - echo "arch_suffix=-amd64" >> $GITHUB_OUTPUT - fi - - - name: 构建 Docker 镜像 - uses: docker/build-push-action@v5 - with: - context: . - platforms: ${{ steps.arch.outputs.platforms || 'linux/amd64' }} - push: false - load: true - tags: ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} - outputs: type=docker,dest=${{ env.IMAGE_NAME }}-${{ env.IMAGE_TAG }}${{ steps.arch.outputs.arch_suffix }}.tar - - - name: 上传镜像 artifact - uses: actions/upload-artifact@v4 - with: - name: docker-image-${{ steps.arch.outputs.arch_suffix || '-amd64' }} - path: ${{ env.IMAGE_NAME }}-${{ env.IMAGE_TAG }}${{ steps.arch.outputs.arch_suffix }}.tar - retention-days: 1 - deploy: runs-on: ubuntu-latest - needs: build - if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' steps: - - name: 设置国内镜像源 - run: | - git config --global url."https://ghproxy.com/".insteadOf "https://github.com/" - - - name: 配置 Docker 镜像源 - run: | - sudo mkdir -p /etc/docker - | sudo tee /etc/docker/daemon.json << 'EOF' -{ - "registry-mirrors": [ - "https://docker.mirrors.ustc.edu.cn", - "https://hub-mirror.c.163.com", - "https://mirror.baidubce.com" - ] -} -EOF - sudo service docker restart || true - - - name: 检出代码 - uses: actions/checkout@v4 - - - name: 下载镜像 artifact (AMD64) - if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && (inputs.arch == 'amd64' || inputs.arch == 'both')) - uses: actions/download-artifact@v4 - with: - name: docker-image--amd64 - path: . - - - name: 下载镜像 artifact (ARM64) - if: github.event_name == 'workflow_dispatch' && inputs.arch == 'both' - uses: actions/download-artifact@v4 - with: - name: docker-image--arm64 - path: . - - name: 安装 sshpass run: | sudo apt-get update sudo apt-get install -y sshpass - - name: 上传到服务器 - env: - SERVER_HOST: ${{ secrets.SERVER_HOST }} - SERVER_USER: ${{ secrets.SERVER_USER }} - SERVER_PASSWORD: ${{ secrets.SERVER_PASSWORD }} - SERVER_PORT: ${{ secrets.SERVER_PORT }} - TAR_FILE: ${{ env.IMAGE_NAME }}-${{ env.IMAGE_TAG }}-amd64.tar - run: | - sshpass -p "$SERVER_PASSWORD" scp -P "$SERVER_PORT" -o StrictHostKeyChecking=no "$TAR_FILE" "${SERVER_USER}@${SERVER_HOST}:/tmp/" - sshpass -p "$SERVER_PASSWORD" scp -P "$SERVER_PORT" -o StrictHostKeyChecking=no ".env" "${SERVER_USER}@${SERVER_HOST}:/tmp/" - - name: 在服务器上部署 env: SERVER_HOST: ${{ secrets.SERVER_HOST }} SERVER_USER: ${{ secrets.SERVER_USER }} SERVER_PASSWORD: ${{ secrets.SERVER_PASSWORD }} SERVER_PORT: ${{ secrets.SERVER_PORT }} - TAR_FILE: ${{ env.IMAGE_NAME }}-${{ env.IMAGE_TAG }}-amd64.tar + TARGET_DIR: ${{ env.TARGET_DIR }} run: | sshpass -p "$SERVER_PASSWORD" ssh -p "$SERVER_PORT" -o StrictHostKeyChecking=no "${SERVER_USER}@${SERVER_HOST}" << 'EOF' set -e @@ -162,62 +33,37 @@ EOF echo "$SERVER_PASSWORD" | sudo -S -p '' "$@" } - echo "[INFO] 开始服务器端部署..." + echo "===== 切换到目标目录: $TARGET_DIR =====" + cd $TARGET_DIR || { + echo "错误:目录 $TARGET_DIR 不存在!" + exit 1 + } - # 检查并停止现有容器 - if run_sudo docker ps -a --format 'table {{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then - echo "[INFO] 发现现有容器 ${CONTAINER_NAME},正在停止并删除..." - run_sudo docker stop ${CONTAINER_NAME} || true - run_sudo docker rm ${CONTAINER_NAME} || true + echo -e "\n===== 停止并清理 Docker =====" + run_sudo docker compose down || true + + echo -e "\n===== 拉取 Git 代码 =====" + if ! git pull; then + echo "警告:Git pull 失败,尝试强制同步远程代码..." + git fetch --all + CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) + git reset --hard origin/$CURRENT_BRANCH + git pull fi - # 检查并删除现有镜像 - if run_sudo docker images --format 'table {{.Repository}}:{{.Tag}}' | grep -q "^${IMAGE_NAME}:${IMAGE_TAG}$"; then - echo "[INFO] 发现现有镜像 ${IMAGE_NAME}:${IMAGE_TAG},正在删除..." - run_sudo docker rmi ${IMAGE_NAME}:${IMAGE_TAG} || true - fi + echo -e "\n===== 配置环境变量 =====" + cat > backend/.env <<'ENVEOF' +ALIYUN_ACCESS_KEY_ID=LTAI5tE62GW8MKyoEaotzxXk +ALIYUN_ACCESS_KEY_SECRET=Zdzqo1fgj57DxxioXOotNKhJdSfVQW +ALIYUN_OSS_ENDPOINT=https://oss-cn-shanghai.aliyuncs.com +ALIYUN_OSS_BUCKET_NAME=tangledup-ai-staging +ALIYUN_OSS_INTERNAL_ENDPOINT=https://oss-cn-shanghai-internal.aliyuncs.com +ALIYUN_TINGWU_APP_KEY=6eOX7N3tKE0fDwb +DASHSCOPE_API_KEY=sk-84e9eef24a274f568d4fa15c97556c9f +ENVEOF - # 加载新镜像 - echo "[INFO] 加载新镜像..." - run_sudo docker load -i /tmp/${TAR_FILE} + echo -e "\n===== 启动 Docker 容器 =====" + run_sudo docker compose up -d --build - # 验证镜像是否加载成功 - if run_sudo docker images | grep -q "${IMAGE_NAME}"; then - echo "[SUCCESS] 镜像加载成功" - else - echo "[ERROR] 镜像加载失败" - exit 1 - fi - - # 启动新容器 - echo "[INFO] 启动新容器..." - run_sudo docker run -d --network host --name ${CONTAINER_NAME} \ - --env-file /tmp/.env \ - -e DB_HOST=localhost \ - -e DB_PORT=5432 \ - ${IMAGE_NAME}:${IMAGE_TAG} - - # 验证容器是否启动成功 - if run_sudo docker ps | grep -q "${CONTAINER_NAME}"; then - echo "[SUCCESS] 容器启动成功" - echo "[INFO] 容器状态:" - run_sudo docker ps | grep "${CONTAINER_NAME}" - else - echo "[ERROR] 容器启动失败" - echo "[INFO] 查看容器日志:" - run_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}" + echo -e "\n===== 操作完成!=====" EOF - - - name: 清理本地临时文件 - run: | - rm -f ${{ env.IMAGE_NAME }}-${{ env.IMAGE_TAG }}-*.tar diff --git a/__pycache__/main.cpython-312.pyc b/__pycache__/main.cpython-312.pyc index c66f396..40dc01a 100644 Binary files a/__pycache__/main.cpython-312.pyc and b/__pycache__/main.cpython-312.pyc differ diff --git a/main.py b/main.py index dd9f633..41accc8 100644 --- a/main.py +++ b/main.py @@ -1661,6 +1661,241 @@ async def success_page(request: Request): CONFIG = load_config() return templates.TemplateResponse("success.html", {"request": request, "config": CONFIG}) +@app.get("/data-base", response_class=HTMLResponse) +async def data_base_page(request: Request): + """ + 渲染数据库管理页面。 + """ + return templates.TemplateResponse("data_base.html", {"request": request}) + +@app.get("/api/db/tables") +def get_db_tables(): + """ + 获取数据库中所有的表。 + """ + try: + conn = get_db_connection() + cur = conn.cursor(cursor_factory=RealDictCursor) + + cur.execute(""" + SELECT table_name + FROM information_schema.tables + WHERE table_schema = 'public' + AND table_type = 'BASE TABLE' + ORDER BY table_name + """) + rows = cur.fetchall() + + tables = [row['table_name'] for row in rows] + + cur.close() + release_db_connection(conn) + + return {"success": True, "tables": tables} + except Exception as e: + return JSONResponse(content={"success": False, "message": str(e)}, status_code=500) + +@app.get("/api/db/table/{table_name}") +def get_table_data(table_name: str, page: int = 1, page_size: int = 50): + """ + 获取指定表的数据。 + """ + try: + conn = get_db_connection() + cur = conn.cursor(cursor_factory=RealDictCursor) + + # 获取主键 + cur.execute(""" + SELECT a.attname AS column_name + FROM pg_index i + JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey) + WHERE i.indrelid = %s::regclass + AND i.indisprimary + """, (table_name,)) + pk_result = cur.fetchone() + primary_key = pk_result['column_name'] if pk_result else '' + + # 获取总行数 + cur.execute(f'SELECT COUNT(*) as count FROM "{table_name}"') + total = cur.fetchone()['count'] + + # 分页查询 + offset = (page - 1) * page_size + query = f'SELECT * FROM "{table_name}" ORDER BY "{primary_key}" LIMIT %s OFFSET %s' + cur.execute(query, (page_size, offset)) + rows = cur.fetchall() + + # 转换为普通字典 + result_rows = [] + for row in rows: + result_rows.append(dict(row)) + + # 获取列名 + columns = list(result_rows[0].keys()) if result_rows else [] + + cur.close() + release_db_connection(conn) + + return { + "success": True, + "rows": result_rows, + "columns": columns, + "total": total, + "primary_key": primary_key, + "page": page, + "page_size": page_size + } + except Exception as e: + return JSONResponse(content={"success": False, "message": str(e)}, status_code=500) + +@app.get("/api/db/table/{table_name}/columns") +def get_table_columns(table_name: str): + """ + 获取指定表的列信息。 + """ + try: + conn = get_db_connection() + cur = conn.cursor(cursor_factory=RealDictCursor) + + cur.execute(""" + SELECT + column_name, + data_type, + is_nullable, + column_default + FROM information_schema.columns + WHERE table_name = %s + AND table_schema = 'public' + ORDER BY ordinal_position + """, (table_name,)) + rows = cur.fetchall() + + cur.close() + release_db_connection(conn) + + return {"success": True, "columns": rows} + except Exception as e: + return JSONResponse(content={"success": False, "message": str(e)}, status_code=500) + +@app.post("/api/db/table/{table_name}/insert") +async def insert_table_data(table_name: str, request: Request): + """ + 向指定表插入数据。 + """ + try: + body = await request.json() + data = body.get('data', {}) + + if not data: + return JSONResponse(content={"success": False, "message": "没有数据"}, status_code=400) + + conn = get_db_connection() + cur = conn.cursor() + + columns = list(data.keys()) + values = list(data.values()) + placeholders = ','.join(['%s'] * len(columns)) + column_names = ','.join([f'"{col}"' for col in columns]) + + query = f'INSERT INTO "{table_name}" ({column_names}) VALUES ({placeholders})' + cur.execute(query, values) + + conn.commit() + cur.close() + release_db_connection(conn) + + return {"success": True, "message": "添加成功"} + except Exception as e: + return JSONResponse(content={"success": False, "message": str(e)}, status_code=500) + +@app.put("/api/db/table/{table_name}/update") +async def update_table_data(table_name: str, request: Request): + """ + 更新指定表的数据。 + """ + try: + body = await request.json() + data = body.get('data', {}) + record_id = body.get('id') + + if not data or not record_id: + return JSONResponse(content={"success": False, "message": "缺少参数"}, status_code=400) + + conn = get_db_connection() + cur = conn.cursor(cursor_factory=RealDictCursor) + + # 获取主键 + cur.execute(""" + SELECT a.attname AS column_name + FROM pg_index i + JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey) + WHERE i.indrelid = %s::regclass + AND i.indisprimary + """, (table_name,)) + pk_result = cur.fetchone() + primary_key = pk_result['column_name'] if pk_result else None + + if not primary_key: + return JSONResponse(content={"success": False, "message": "表没有主键,无法更新"}, status_code=400) + + # 构建更新语句 + set_clause = ','.join([f'"{col}" = %s' for col in data.keys()]) + values = list(data.values()) + values.append(record_id) + + query = f'UPDATE "{table_name}" SET {set_clause} WHERE "{primary_key}" = %s' + cur.execute(query, values) + + conn.commit() + cur.close() + release_db_connection(conn) + + return {"success": True, "message": "更新成功"} + except Exception as e: + return JSONResponse(content={"success": False, "message": str(e)}, status_code=500) + +@app.delete("/api/db/table/{table_name}/delete") +async def delete_table_data(table_name: str, request: Request): + """ + 删除指定表的数据。 + """ + try: + body = await request.json() + ids = body.get('ids', []) + + if not ids: + return JSONResponse(content={"success": False, "message": "没有要删除的记录"}, status_code=400) + + conn = get_db_connection() + cur = conn.cursor(cursor_factory=RealDictCursor) + + # 获取主键 + cur.execute(""" + SELECT a.attname AS column_name + FROM pg_index i + JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey) + WHERE i.indrelid = %s::regclass + AND i.indisprimary + """, (table_name,)) + pk_result = cur.fetchone() + primary_key = pk_result['column_name'] if pk_result else None + + if not primary_key: + return JSONResponse(content={"success": False, "message": "表没有主键,无法删除"}, status_code=400) + + # 删除 ids 中的记录 + placeholders = ','.join(['%s'] * len(ids)) + query = f'DELETE FROM "{table_name}" WHERE "{primary_key}" IN ({placeholders})' + cur.execute(query, ids) + + conn.commit() + cur.close() + release_db_connection(conn) + + return {"success": True, "message": f"成功删除 {cur.rowcount} 条记录"} + except Exception as e: + return JSONResponse(content={"success": False, "message": str(e)}, status_code=500) + class TicketPaymentRequest(BaseModel): name: str phone: str diff --git a/templates/data_base.html b/templates/data_base.html new file mode 100644 index 0000000..5fde983 --- /dev/null +++ b/templates/data_base.html @@ -0,0 +1,796 @@ + + +
+ + +