فهرست منبع

refactor: 统一 dev/sit/uat 三环境配置,共用一份代码

把三个分支原本散落的差异收敛到 .env.{dev,sit,uat} 三份配置文件,
代码只有一份,通过 APP_ENV 切换。私有部署,三份 .env 直接入库。

核心改造:
- config.py 引入 APP_ENV,按 .env.{APP_ENV} 加载;
  新增端口、下游 URL、Redis 单机/哨兵双模配置项。
- 新增 alien_util/redis_client.py,兼容单机与 Sentinel 两种 Redis。
- celery_app.py broker/backend 自动适配单机/哨兵。
- gateway 抽出通用 _proxy,新增 /health/redis 与 /api/contract/* 代理。
- 各服务监听端口改为从 settings 读取;Dockerfile 端口由 ARG/ENV 控制。

部署体系:
- 三份 .env.{dev,sit,uat} 入库;新增 .env.example 模板。
- 旧的根 .env 删除(被 .env.{APP_ENV} 体系替代)。
- 新增 .dockerignore,避免本地敏感文件误打入镜像。
- 重写 Jenkinsfile:choice(APP_ENV) + string(BRANCH) 参数化,
  一份脚本服务 dev/sit/uat;容器名/网络/日志/镜像 TAG 全部带 env 后缀。
天空之城 2 هفته پیش
والد
کامیت
7aac7eacb3
19فایلهای تغییر یافته به همراه779 افزوده شده و 185 حذف شده
  1. 31 0
      .dockerignore
  2. 0 22
      .env
  3. 34 18
      .env.dev
  4. 59 0
      .env.example
  5. 38 0
      .env.sit
  6. 41 0
      .env.uat
  7. 7 1
      .gitignore
  8. 197 0
      Jenkinsfile
  9. 8 2
      alien_contract/Dockerfile
  10. 1 1
      alien_contract/main.py
  11. 9 2
      alien_gateway/Dockerfile
  12. 126 25
      alien_gateway/config.py
  13. 40 23
      alien_gateway/main.py
  14. 1 1
      alien_lawyer/main.py
  15. 8 2
      alien_store/Dockerfile
  16. 1 1
      alien_store/main.py
  17. 11 13
      alien_util/celery_app.py
  18. 167 0
      alien_util/redis_client.py
  19. 0 74
      test.py

+ 31 - 0
.dockerignore

@@ -0,0 +1,31 @@
+# 避免误打入容器的本地/敏感文件
+.env
+.env.local
+.env.*.local
+
+# Python 缓存
+__pycache__/
+*.pyc
+*.pyo
+*.pyd
+.pytest_cache/
+
+# 虚拟环境
+.venv/
+venv/
+
+# IDE / Git
+.git/
+.gitignore
+.idea/
+.vscode/
+
+# 日志和临时文件
+*.log
+common/logs/
+*.tmp
+
+# 镜像构建无需的文件
+README.md
+docs/
+tests/

+ 0 - 22
.env

@@ -1,22 +0,0 @@
-SECRET_KEY="your-super-secret-key-change-me"
-ALGORITHM="HS256"
-ACCESS_TOKEN_EXPIRE_MINUTES=10080
- # 数据库配置
-DB_USER="root"
-DB_PASSWORD="Alien123456"
-DB_HOST="120.26.186.130"
-DB_PORT=30001
-DB_NAME="alien_sit"
-
-# redis配置
-# REDIS_URL= "redis://:Alien123456@172.31.154.180:30002/0"
-REDIS_URL="redis://:Alien123456@120.26.186.130:30002/0"
-
-# 下游服务地址
-STORE_BASE_URL="http://127.0.0.1:8001"# alien_store 服务地址
-
-# 阿里云短信配置
-ALIYUN_SMS_SIGN_NAME_CONTRACT="爱丽恩严大连商务科技"
-ALIYUN_SMS_TEMPLATE_CODE_CONTRACT="SMS_501820309"
-ALIYUN_ACCESS_KEY_ID="LTAI5t77CS9gD7JMkMAjD2vF"
-ALIYUN_ACCESS_KEY_SECRET="jLYGPpaJuc7NqmRdLvu1ObAS9CJFB8"

+ 34 - 18
.env.dev

@@ -1,22 +1,38 @@
-SECRET_KEY="your-super-secret-key-change-me"
-ALGORITHM="HS256"
+################################################################################
+# DEV 环境(本地开发)
+# 此文件不入库,由开发者本机维护。模板见 .env.example
+################################################################################
+
+SECRET_KEY=your-super-secret-key-change-me
+ALGORITHM=HS256
 ACCESS_TOKEN_EXPIRE_MINUTES=10080
- # 数据库配置
-DB_USER="root"
-DB_PASSWORD="Alien123456"
-DB_HOST="192.168.2.253"
-DB_PORT=40001
-DB_NAME="alien_sit"
 
-# redis配置
-# REDIS_URL= "redis://:Alien123456@172.31.154.180:30002/0"
-REDIS_URL="redis://:Alien123456@192.168.2.253:40002/0"
+# 数据库
+DB_USER=root
+DB_PASSWORD=Alien123456
+DB_HOST=120.26.186.130
+DB_PORT=30001
+DB_NAME=alien_sit
+
+# Redis(单机模式)
+REDIS_URL=redis://:Alien123456@120.26.186.130:30002/0
+REDIS_SENTINELS=
+
+# 服务端口
+GATEWAY_PORT=33333
+STORE_PORT=8001
+CONTRACT_PORT=8002
+LAWYER_PORT=8004
+
+# 下游服务地址(本地开发各服务在 127.0.0.1 上分端口启动)
+STORE_BASE_URL=http://127.0.0.1:8001
+CONTRACT_BASE_URL=http://127.0.0.1:8002
 
-# 下游服务地址
-STORE_BASE_URL="http://127.0.0.1:8001"# alien_store 服务地址
+# e签宝
+ESIGN_CALLBACK_URL=http://120.26.186.130:33333/api/contract/esign/callback
 
-# 阿里云短信配置
-ALIYUN_SMS_SIGN_NAME_CONTRACT="爱丽恩严大连商务科技"
-ALIYUN_SMS_TEMPLATE_CODE_CONTRACT="SMS_501820309"
-ALIYUN_ACCESS_KEY_ID="LTAI5t77CS9gD7JMkMAjD2vF"
-ALIYUN_ACCESS_KEY_SECRET="jLYGPpaJuc7NqmRdLvu1ObAS9CJFB8"
+# 阿里云短信
+ALIYUN_SMS_SIGN_NAME_CONTRACT=爱丽恩严大连商务科技
+ALIYUN_SMS_TEMPLATE_CODE_CONTRACT=SMS_501820309
+ALIYUN_ACCESS_KEY_ID=LTAI5t77CS9gD7JMkMAjD2vF
+ALIYUN_ACCESS_KEY_SECRET=jLYGPpaJuc7NqmRdLvu1ObAS9CJFB8

+ 59 - 0
.env.example

@@ -0,0 +1,59 @@
+################################################################################
+# Alien Cloud Python - 环境变量模板(入库,不含敏感信息)
+#
+# 使用方法:
+#   1) 复制为 .env.dev / .env.sit / .env.uat(按环境填充)
+#   2) 启动服务前设置 APP_ENV,例如:
+#        export APP_ENV=dev        # 本地开发
+#        docker run -e APP_ENV=uat ...
+#   3) config.py 会自动加载 .env.${APP_ENV}
+################################################################################
+
+# 当前环境标识(dev/sit/uat),通常通过 docker run -e APP_ENV=xxx 注入而非写在文件里
+# APP_ENV=dev
+
+# -------------------- 鉴权 --------------------
+SECRET_KEY=please-change-me
+ALGORITHM=HS256
+ACCESS_TOKEN_EXPIRE_MINUTES=10080
+
+# -------------------- 数据库 --------------------
+DB_USER=root
+DB_PASSWORD=
+DB_HOST=
+DB_PORT=3306
+DB_NAME=
+
+# -------------------- Redis(单机与哨兵二选一) --------------------
+# 单机模式:填 REDIS_URL,留空 REDIS_SENTINELS
+REDIS_URL=
+# 哨兵模式:填 REDIS_SENTINELS,留空 REDIS_URL
+REDIS_SENTINELS=
+REDIS_MASTER_NAME=mymaster
+REDIS_PASSWORD=
+REDIS_DB=0
+REDIS_SENTINEL_USERNAME=
+REDIS_SENTINEL_PASSWORD=
+REDIS_SOCKET_TIMEOUT=0.5
+REDIS_CONNECT_TIMEOUT=1.0
+
+# -------------------- 服务监听端口(容器内) --------------------
+GATEWAY_PORT=33333
+STORE_PORT=8001
+CONTRACT_PORT=8002
+LAWYER_PORT=8004
+
+# -------------------- 下游服务地址(网关代理用) --------------------
+STORE_BASE_URL=http://127.0.0.1:8001
+CONTRACT_BASE_URL=http://127.0.0.1:8002
+
+# -------------------- e签宝回调地址 --------------------
+ESIGN_CALLBACK_URL=
+ESIGN_DEVELOPER_CALLBACK_URL=
+ESIGN_REDIRECT_URL=https://www.esign.cn/
+
+# -------------------- 阿里云短信 --------------------
+ALIYUN_SMS_SIGN_NAME_CONTRACT=
+ALIYUN_SMS_TEMPLATE_CODE_CONTRACT=
+ALIYUN_ACCESS_KEY_ID=
+ALIYUN_ACCESS_KEY_SECRET=

+ 38 - 0
.env.sit

@@ -0,0 +1,38 @@
+################################################################################
+# SIT 环境(集成测试)
+# 此文件不入库,由 CI/CD 或运维注入。模板见 .env.example
+################################################################################
+
+SECRET_KEY=your-super-secret-key-change-me
+ALGORITHM=HS256
+ACCESS_TOKEN_EXPIRE_MINUTES=10080
+
+# 数据库
+DB_USER=root
+DB_PASSWORD=Alien123456
+DB_HOST=120.26.186.130
+DB_PORT=30001
+DB_NAME=alien_sit
+
+# Redis(单机模式)
+REDIS_URL=redis://:Alien123456@172.31.154.180:30002/0
+REDIS_SENTINELS=
+
+# 服务端口
+GATEWAY_PORT=33333
+STORE_PORT=8001
+CONTRACT_PORT=8002
+LAWYER_PORT=8004
+
+# 下游服务地址(容器化部署时由 Jenkinsfile 用 -e 覆盖为容器名)
+STORE_BASE_URL=http://127.0.0.1:8001
+CONTRACT_BASE_URL=http://127.0.0.1:8002
+
+# e签宝
+ESIGN_CALLBACK_URL=http://120.26.186.130:33333/api/contract/esign/callback
+
+# 阿里云短信
+ALIYUN_SMS_SIGN_NAME_CONTRACT=爱丽恩严大连商务科技
+ALIYUN_SMS_TEMPLATE_CODE_CONTRACT=SMS_501820309
+ALIYUN_ACCESS_KEY_ID=LTAI5t77CS9gD7JMkMAjD2vF
+ALIYUN_ACCESS_KEY_SECRET=jLYGPpaJuc7NqmRdLvu1ObAS9CJFB8

+ 41 - 0
.env.uat

@@ -0,0 +1,41 @@
+################################################################################
+# UAT 环境
+# 此文件不入库,由 CI/CD 或运维注入。模板见 .env.example
+################################################################################
+
+SECRET_KEY=your-super-secret-key-change-me
+ALGORITHM=HS256
+ACCESS_TOKEN_EXPIRE_MINUTES=10080
+
+# 数据库
+DB_USER=root
+DB_PASSWORD=Alien123456
+DB_HOST=ailiendb.mysql.rds.aliyuncs.com
+DB_PORT=53306
+DB_NAME=alien_uat
+
+# Redis(哨兵高可用模式)
+REDIS_URL=
+REDIS_SENTINELS=192.168.2.251:36379,192.168.2.252:36379,192.168.2.253:36379
+REDIS_MASTER_NAME=mymaster
+REDIS_PASSWORD=my_password_123
+REDIS_SENTINEL_PASSWORD=
+
+# 服务端口(UAT 使用独立端口避开占用)
+GATEWAY_PORT=43333
+STORE_PORT=8001
+CONTRACT_PORT=8002
+LAWYER_PORT=8004
+
+# 下游服务地址(容器化部署时由 Jenkinsfile 用 -e 覆盖为容器名)
+STORE_BASE_URL=http://127.0.0.1:8001
+CONTRACT_BASE_URL=http://127.0.0.1:8002
+
+# e签宝
+ESIGN_CALLBACK_URL=https://uat.ailien.shop/api/contract/esign/callback
+
+# 阿里云短信
+ALIYUN_SMS_SIGN_NAME_CONTRACT=爱丽恩严大连商务科技
+ALIYUN_SMS_TEMPLATE_CODE_CONTRACT=SMS_501820309
+ALIYUN_ACCESS_KEY_ID=LTAI5t77CS9gD7JMkMAjD2vF
+ALIYUN_ACCESS_KEY_SECRET=jLYGPpaJuc7NqmRdLvu1ObAS9CJFB8

+ 7 - 1
.gitignore

@@ -101,7 +101,11 @@ celerybeat.pid
 *.sage.py
 
 # Environments
+# 私有部署:三个环境的真实配置 .env.dev / .env.sit / .env.uat 入库;
+# 本地个人覆盖文件不入库。
 .env
+.env.local
+.env.*.local
 .venv
 env/
 venv/
@@ -143,4 +147,6 @@ cython_debug/
 .history/
 
 # macOS
-.DS_Store
+.DS_Store
+
+commit_msg.txt

+ 197 - 0
Jenkinsfile

@@ -0,0 +1,197 @@
+pipeline {
+  agent any
+
+  options {
+    buildDiscarder(logRotator(numToKeepStr: '10'))
+    timestamps()
+    ansiColor('xterm')
+  }
+
+  // 构建时手选环境与分支:一份 Jenkinsfile 服务 dev / sit / uat 三个环境
+  parameters {
+    choice(
+      name: 'APP_ENV',
+      choices: ['dev', 'sit', 'uat'],
+      description: '部署到哪个环境'
+    )
+    string(
+      name: 'BRANCH',
+      defaultValue: 'dev',
+      description: '要部署的 Git 分支(一般 dev 环境用 dev 分支,sit 用 sit,uat 用 uat;只有一条主干时统一填 main)'
+    )
+  }
+
+  environment {
+    // ---------- Dockerfile 路径 ----------
+    DOCKERFILE_STORE    = "alien_store/Dockerfile"
+    DOCKERFILE_GATEWAY  = "alien_gateway/Dockerfile"
+    DOCKERFILE_CONTRACT = "alien_contract/Dockerfile"
+
+    // ---------- 镜像仓库(如不推远端 Registry 就留空) ----------
+    REGISTRY      = ""
+    REGISTRY_CRED = "registry-cred-id"
+
+    // ---------- 三环境共用的命名规则:全部以 ${APP_ENV} 区分 ----------
+    IMAGE_TAG               = "${params.APP_ENV}-${env.BUILD_NUMBER}"
+    IMAGE_STORE             = "${REGISTRY ? REGISTRY + '/' : ''}alien_store:${IMAGE_TAG}"
+    IMAGE_GATEWAY           = "${REGISTRY ? REGISTRY + '/' : ''}alien_gateway:${IMAGE_TAG}"
+    IMAGE_CONTRACT          = "${REGISTRY ? REGISTRY + '/' : ''}alien_contract:${IMAGE_TAG}"
+
+    CONTAINER_NAME_STORE    = "alien_store_py-${params.APP_ENV}"
+    CONTAINER_NAME_GATEWAY  = "alien_gateway_py-${params.APP_ENV}"
+    CONTAINER_NAME_CONTRACT = "alien_contract_py-${params.APP_ENV}"
+
+    DOCKER_NET              = "alien_net_${params.APP_ENV}"
+    LOG_ROOT                = "/docker/python-${params.APP_ENV}/logs"
+  }
+
+  stages {
+
+    stage('Show Build Info') {
+      steps {
+        echo "============================================================"
+        echo " 部署环境  : ${params.APP_ENV}"
+        echo " 部署分支  : ${params.BRANCH}"
+        echo " 镜像 TAG  : ${IMAGE_TAG}"
+        echo " 网络名    : ${DOCKER_NET}"
+        echo " 日志根目录: ${LOG_ROOT}"
+        echo "============================================================"
+      }
+    }
+
+    // 注意:使用 "Pipeline script from SCM" 模式时 Jenkins 已自动 checkout
+    // 但因为我们参数化了 BRANCH,这里显式 checkout 指定分支
+    stage('Checkout') {
+      steps {
+        checkout([
+          $class: 'GitSCM',
+          branches: [[name: "*/${params.BRANCH}"]],
+          userRemoteConfigs: scm.userRemoteConfigs
+        ])
+      }
+    }
+
+    stage('Load Env Port Mapping') {
+      // 从 .env.${APP_ENV} 解析 GATEWAY_PORT,作为 docker -p 宿主端口映射用
+      // (容器内监听端口同样靠 Dockerfile 默认 ARG/ENV 与 .env 配合)
+      steps {
+        script {
+          def envFile = ".env.${params.APP_ENV}"
+          if (!fileExists(envFile)) {
+            error "缺少环境配置文件: ${envFile}(仓库里应该有这份;请检查 .gitignore 是否误屏蔽)"
+          }
+          def gatewayPort = sh(
+            script: "grep -E '^GATEWAY_PORT=' ${envFile} | head -n1 | cut -d= -f2 | tr -d '\"' | tr -d \"'\"",
+            returnStdout: true
+          ).trim()
+          if (!gatewayPort) {
+            gatewayPort = "33333"
+          }
+          env.GATEWAY_PORT = gatewayPort
+          echo "从 ${envFile} 解析到 GATEWAY_PORT=${env.GATEWAY_PORT}"
+        }
+      }
+    }
+
+    stage('Build Images') {
+      steps {
+        script {
+          def buildArgs = "--build-arg APP_ENV=${params.APP_ENV}"
+          if (env.REGISTRY?.trim()) {
+            withDockerRegistry(credentialsId: env.REGISTRY_CRED, url: "") {
+              sh "docker build ${buildArgs} -f ${DOCKERFILE_STORE}    -t ${IMAGE_STORE}    ."
+              sh "docker build ${buildArgs} -f ${DOCKERFILE_GATEWAY}  -t ${IMAGE_GATEWAY}  ."
+              sh "docker build ${buildArgs} -f ${DOCKERFILE_CONTRACT} -t ${IMAGE_CONTRACT} ."
+            }
+          } else {
+            sh "docker build ${buildArgs} -f ${DOCKERFILE_STORE}    -t ${IMAGE_STORE}    ."
+            sh "docker build ${buildArgs} -f ${DOCKERFILE_GATEWAY}  -t ${IMAGE_GATEWAY}  ."
+            sh "docker build ${buildArgs} -f ${DOCKERFILE_CONTRACT} -t ${IMAGE_CONTRACT} ."
+          }
+        }
+      }
+    }
+
+    stage('Push Images') {
+      when { expression { return env.REGISTRY?.trim() as boolean } }
+      steps {
+        withDockerRegistry(credentialsId: env.REGISTRY_CRED, url: "") {
+          sh "docker push ${IMAGE_STORE}"
+          sh "docker push ${IMAGE_GATEWAY}"
+          sh "docker push ${IMAGE_CONTRACT}"
+        }
+      }
+    }
+
+    stage('Deploy') {
+      steps {
+        script {
+          echo ">>> [${params.APP_ENV}] 部署镜像: ${IMAGE_STORE} / ${IMAGE_GATEWAY} / ${IMAGE_CONTRACT}"
+          sh """
+            set -e
+
+            docker network create ${DOCKER_NET} 2>/dev/null || true
+            mkdir -p ${LOG_ROOT}/store ${LOG_ROOT}/gateway ${LOG_ROOT}/contract
+
+            docker rm -f ${CONTAINER_NAME_STORE} ${CONTAINER_NAME_GATEWAY} ${CONTAINER_NAME_CONTRACT} 2>/dev/null || true
+
+            # 1) 下游:store
+            #    APP_ENV=${params.APP_ENV} 让 config.py 加载镜像内的 .env.${params.APP_ENV}
+            docker run -d --name ${CONTAINER_NAME_STORE} \\
+              --network ${DOCKER_NET} \\
+              -e APP_ENV=${params.APP_ENV} \\
+              -v ${LOG_ROOT}/store:/app/common/logs/alien_store \\
+              --restart unless-stopped \\
+              ${IMAGE_STORE}
+
+            # 2) 下游:contract
+            docker run -d --name ${CONTAINER_NAME_CONTRACT} \\
+              --network ${DOCKER_NET} \\
+              -e APP_ENV=${params.APP_ENV} \\
+              -v ${LOG_ROOT}/contract:/app/common/logs/alien_contract \\
+              --restart unless-stopped \\
+              ${IMAGE_CONTRACT}
+
+            # 3) 网关:gateway(唯一对外端口,并用 -e 把下游地址覆盖为容器名)
+            docker run -d --name ${CONTAINER_NAME_GATEWAY} \\
+              --network ${DOCKER_NET} \\
+              -p ${env.GATEWAY_PORT}:${env.GATEWAY_PORT} \\
+              -e APP_ENV=${params.APP_ENV} \\
+              -e STORE_BASE_URL=http://${CONTAINER_NAME_STORE}:8001 \\
+              -e CONTRACT_BASE_URL=http://${CONTAINER_NAME_CONTRACT}:8002 \\
+              -v ${LOG_ROOT}/gateway:/app/common/logs/alien_gateway \\
+              --restart unless-stopped \\
+              ${IMAGE_GATEWAY}
+          """
+        }
+      }
+    }
+
+    stage('Smoke Test') {
+      steps {
+        script {
+          sh """
+            sleep 3
+            echo '--- /health ---'
+            curl -sf http://127.0.0.1:${env.GATEWAY_PORT}/health || (echo 'gateway /health 失败' && exit 1)
+            echo
+            echo '--- /health/redis ---'
+            curl -sf http://127.0.0.1:${env.GATEWAY_PORT}/health/redis || echo 'redis 健康检查未通过(非阻塞,部署继续)'
+          """
+        }
+      }
+    }
+  }
+
+  post {
+    success {
+      echo "[${params.APP_ENV}] 环境部署成功!gateway: http://<host>:${env.GATEWAY_PORT}"
+    }
+    failure {
+      echo "[${params.APP_ENV}] 环境部署失败,请检查上面日志。"
+    }
+    always {
+      cleanWs()
+    }
+  }
+}

+ 8 - 2
alien_contract/Dockerfile

@@ -17,6 +17,12 @@ RUN poetry source add --priority=primary tsinghua https://pypi.tuna.tsinghua.edu
 
 COPY . .
 
-EXPOSE 8005
+ARG APP_ENV=dev
+ENV APP_ENV=${APP_ENV}
 
-CMD ["uvicorn", "alien_contract.main:app", "--host", "0.0.0.0", "--port", "8002"]
+ARG CONTRACT_PORT=8002
+ENV CONTRACT_PORT=${CONTRACT_PORT}
+
+EXPOSE ${CONTRACT_PORT}
+
+CMD ["sh", "-c", "uvicorn alien_contract.main:app --host 0.0.0.0 --port ${CONTRACT_PORT}"]

+ 1 - 1
alien_contract/main.py

@@ -19,4 +19,4 @@ async def health():
 if __name__ == "__main__":
     import uvicorn
 
-    uvicorn.run(app, host="0.0.0.0", port=8005)
+    uvicorn.run(app, host="0.0.0.0", port=settings.CONTRACT_PORT)

+ 9 - 2
alien_gateway/Dockerfile

@@ -17,6 +17,13 @@ RUN poetry source add --priority=primary tsinghua https://pypi.tuna.tsinghua.edu
 
 COPY . .
 
-EXPOSE 33333
+# 默认 dev 环境,部署时通过 -e APP_ENV=sit/uat 覆盖
+ARG APP_ENV=dev
+ENV APP_ENV=${APP_ENV}
 
-CMD ["uvicorn", "alien_gateway.main:app", "--host", "0.0.0.0", "--port", "33333"]
+ARG GATEWAY_PORT=33333
+ENV GATEWAY_PORT=${GATEWAY_PORT}
+
+EXPOSE ${GATEWAY_PORT}
+
+CMD ["sh", "-c", "uvicorn alien_gateway.main:app --host 0.0.0.0 --port ${GATEWAY_PORT}"]

+ 126 - 25
alien_gateway/config.py

@@ -1,48 +1,149 @@
 from pydantic_settings import BaseSettings, SettingsConfigDict
-from typing import List
-from dotenv import load_dotenv
+from typing import Any, Dict, List, Optional, Tuple
+from urllib.parse import quote
 import os
-load_dotenv()
+
+# 通过环境变量 APP_ENV 选择 .env 文件,默认 dev:
+# - 本地开发:APP_ENV=dev  -> .env.dev
+# - 测试环境:APP_ENV=sit  -> .env.sit
+# - UAT环境:APP_ENV=uat   -> .env.uat
+APP_ENV = os.getenv("APP_ENV", "dev")
+_ENV_FILE = f".env.{APP_ENV}"
+
 
 class Settings(BaseSettings):
     # 基础配置
     PROJECT_NAME: str = "Alien Cloud Python"
     API_V1_STR: str = "/api/v1"
-    
-    # 鉴权配置 (原 alien-gateway 职责)
-    SECRET_KEY: str = os.getenv("SECRET_KEY")
-    ALGORITHM: str = os.getenv("ALGORITHM")
-    ACCESS_TOKEN_EXPIRE_MINUTES: int = os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES")  # 7天
-    
+    APP_ENV: str = APP_ENV
+
+    # 鉴权配置(原 alien-gateway 职责)
+    SECRET_KEY: str = os.getenv("SECRET_KEY", "")
+    ALGORITHM: str = os.getenv("ALGORITHM", "HS256")
+    ACCESS_TOKEN_EXPIRE_MINUTES: int = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "10080"))
+
     # 数据库配置
-    DB_USER: str = os.getenv("DB_USER")
-    DB_PASSWORD: str = os.getenv("DB_PASSWORD")
-    DB_HOST: str = os.getenv("DB_HOST")
-    DB_PORT: int = os.getenv("DB_PORT")
-    DB_NAME: str = os.getenv("DB_NAME")
+    DB_USER: str = os.getenv("DB_USER", "")
+    DB_PASSWORD: str = os.getenv("DB_PASSWORD", "")
+    DB_HOST: str = os.getenv("DB_HOST", "")
+    DB_PORT: int = int(os.getenv("DB_PORT", "3306"))
+    DB_NAME: str = os.getenv("DB_NAME", "")
 
-    # redis配置
-    # REDIS_URL: str = "redis://:Alien123456@172.31.154.180:30002/0"
-    REDIS_URL: str = os.getenv("REDIS_URL")
+    # Redis 单机配置(与 Sentinel 二选一;REDIS_URL 非空时走单机模式)
+    REDIS_URL: str = os.getenv("REDIS_URL", "")
 
-    # 下游服务地址
-    STORE_BASE_URL: str = os.getenv("STORE_BASE_URL")  # alien_store 服务地址
+    # Redis Sentinel 高可用配置(REDIS_SENTINELS 非空时走哨兵模式)
+    # 例:REDIS_SENTINELS=192.168.2.251:36379,192.168.2.252:36379,192.168.2.253:36379
+    REDIS_SENTINELS: str = os.getenv("REDIS_SENTINELS", "")
+    REDIS_MASTER_NAME: str = os.getenv("REDIS_MASTER_NAME", "mymaster")
+    REDIS_PASSWORD: str = os.getenv("REDIS_PASSWORD", "")
+    REDIS_DB: int = int(os.getenv("REDIS_DB", "0"))
+    REDIS_SENTINEL_USERNAME: str = os.getenv("REDIS_SENTINEL_USERNAME", "")
+    REDIS_SENTINEL_PASSWORD: str = os.getenv("REDIS_SENTINEL_PASSWORD", "")
+    REDIS_SOCKET_TIMEOUT: float = float(os.getenv("REDIS_SOCKET_TIMEOUT", "0.5"))
+    REDIS_CONNECT_TIMEOUT: float = float(os.getenv("REDIS_CONNECT_TIMEOUT", "1.0"))
 
-    # 阿里云短信配置
-    ALIYUN_SMS_SIGN_NAME_CONTRACT: str = os.getenv("ALIYUN_SMS_SIGN_NAME_CONTRACT")
-    ALIYUN_SMS_TEMPLATE_CODE_CONTRACT: str = os.getenv("ALIYUN_SMS_TEMPLATE_CODE_CONTRACT")
-    ALIYUN_ACCESS_KEY_ID: str = os.getenv("ALIYUN_ACCESS_KEY_ID")
-    ALIYUN_ACCESS_KEY_SECRET: str = os.getenv("ALIYUN_ACCESS_KEY_SECRET")
+    # 各服务监听端口(容器内端口;外部映射由 Jenkinsfile / docker-compose 控制)
+    GATEWAY_PORT: int = int(os.getenv("GATEWAY_PORT", "33333"))
+    STORE_PORT: int = int(os.getenv("STORE_PORT", "8001"))
+    CONTRACT_PORT: int = int(os.getenv("CONTRACT_PORT", "8002"))
+    LAWYER_PORT: int = int(os.getenv("LAWYER_PORT", "8004"))
 
+    # 下游服务地址(网关反向代理用)
+    STORE_BASE_URL: str = os.getenv("STORE_BASE_URL", "http://127.0.0.1:8001")
+    CONTRACT_BASE_URL: str = os.getenv("CONTRACT_BASE_URL", "http://127.0.0.1:8002")
 
+    # 阿里云短信配置
+    ALIYUN_SMS_SIGN_NAME_CONTRACT: str = os.getenv("ALIYUN_SMS_SIGN_NAME_CONTRACT", "")
+    ALIYUN_SMS_TEMPLATE_CODE_CONTRACT: str = os.getenv("ALIYUN_SMS_TEMPLATE_CODE_CONTRACT", "")
+    ALIYUN_ACCESS_KEY_ID: str = os.getenv("ALIYUN_ACCESS_KEY_ID", "")
+    ALIYUN_ACCESS_KEY_SECRET: str = os.getenv("ALIYUN_ACCESS_KEY_SECRET", "")
 
     @property
     def SQLALCHEMY_DATABASE_URI(self) -> str:
         return f"mysql+pymysql://{self.DB_USER}:{self.DB_PASSWORD}@{self.DB_HOST}:{self.DB_PORT}/{self.DB_NAME}"
 
+    @property
+    def REDIS_MODE(self) -> str:
+        # standalone(单机) / sentinel(哨兵)
+        return "sentinel" if self.REDIS_SENTINELS.strip() else "standalone"
+
+    @property
+    def REDIS_SENTINEL_NODES(self) -> List[Tuple[str, int]]:
+        nodes: List[Tuple[str, int]] = []
+        for item in self.REDIS_SENTINELS.split(","):
+            entry = item.strip()
+            if not entry:
+                continue
+            if ":" not in entry:
+                raise ValueError(f"Invalid REDIS_SENTINELS entry: {entry}")
+            host, port = entry.split(":", 1)
+            nodes.append((host.strip(), int(port.strip())))
+        return nodes
+
+    @property
+    def REDIS_SENTINEL_KWARGS(self) -> Dict[str, Any]:
+        kwargs: Dict[str, Any] = {}
+        if self.REDIS_SENTINEL_USERNAME:
+            kwargs["username"] = self.REDIS_SENTINEL_USERNAME
+        if self.REDIS_SENTINEL_PASSWORD:
+            kwargs["password"] = self.REDIS_SENTINEL_PASSWORD
+        return kwargs
+
+    @property
+    def REDIS_SENTINEL_URL(self) -> str:
+        # 标准 Sentinel URL:
+        # redis+sentinel://[username[:password]@]host1:port1,host2:port2/db?sentinel_master=mymaster
+        if self.REDIS_SENTINEL_USERNAME:
+            user = quote(self.REDIS_SENTINEL_USERNAME, safe="")
+            pwd = quote(self.REDIS_PASSWORD, safe="")
+            auth = f"{user}:{pwd}@"
+        elif self.REDIS_PASSWORD:
+            pwd = quote(self.REDIS_PASSWORD, safe="")
+            auth = f":{pwd}@"
+        else:
+            auth = ""
+        hosts = ",".join(f"{host}:{port}" for host, port in self.REDIS_SENTINEL_NODES)
+        master = quote(self.REDIS_MASTER_NAME, safe="")
+        return f"redis+sentinel://{auth}{hosts}/{self.REDIS_DB}?sentinel_master={master}"
+
+    @property
+    def REDIS_CELERY_SENTINEL_URL(self) -> str:
+        # Celery/Kombu Sentinel URL 格式:
+        # sentinel://:redis_password@sentinel_host:port/db;...
+        auth = f":{quote(self.REDIS_PASSWORD, safe='')}@" if self.REDIS_PASSWORD else ""
+        return ";".join(
+            f"sentinel://{auth}{host}:{port}/{self.REDIS_DB}"
+            for host, port in self.REDIS_SENTINEL_NODES
+        )
+
+    @property
+    def REDIS_SENTINEL_TRANSPORT_OPTIONS(self) -> Dict[str, Any]:
+        return {
+            "master_name": self.REDIS_MASTER_NAME,
+            "sentinel_kwargs": self.REDIS_SENTINEL_KWARGS,
+            "password": self.REDIS_PASSWORD,
+            "db": self.REDIS_DB,
+        }
+
+    @property
+    def CELERY_BROKER_URL(self) -> str:
+        # 单机优先用 REDIS_URL;哨兵则用 Celery Sentinel URL
+        return self.REDIS_CELERY_SENTINEL_URL if self.REDIS_MODE == "sentinel" else self.REDIS_URL
+
+    @property
+    def CELERY_BACKEND_URL(self) -> str:
+        return self.CELERY_BROKER_URL
+
+    @property
+    def CELERY_TRANSPORT_OPTIONS(self) -> Optional[Dict[str, Any]]:
+        return self.REDIS_SENTINEL_TRANSPORT_OPTIONS if self.REDIS_MODE == "sentinel" else None
+
     model_config = SettingsConfigDict(
         case_sensitive=True,
-        env_file=".env.dev",
+        env_file=_ENV_FILE,
+        extra="ignore",
     )
 
+
 settings = Settings()

+ 40 - 23
alien_gateway/main.py

@@ -6,10 +6,11 @@ from fastapi import FastAPI, Request, Response, HTTPException
 from fastapi.middleware.cors import CORSMiddleware
 from starlette.status import HTTP_502_BAD_GATEWAY
 from alien_gateway.config import settings
+from alien_util.redis_client import check_redis_connection
 
 app = FastAPI(
     title=f"{settings.PROJECT_NAME} - Gateway & Auth Service",
-    version="1.0.0"
+    version="1.0.0",
 )
 
 app.add_middleware(
@@ -20,40 +21,50 @@ app.add_middleware(
     allow_headers=["*"],
 )
 
-# ------------------- 日志配置 -------------------
 LOG_DIR = os.path.join("common", "logs", "alien_gateway")
 os.makedirs(LOG_DIR, exist_ok=True)
 
+
 def _init_logger():
     logger = logging.getLogger("alien_gateway")
     if logger.handlers:
         return logger
     logger.setLevel(logging.INFO)
     fmt = logging.Formatter("%(asctime)s [%(levelname)s] %(name)s %(message)s")
-    
-    # 文件日志
+
     info_handler = logging.FileHandler(os.path.join(LOG_DIR, "info.log"), encoding="utf-8")
     info_handler.setLevel(logging.INFO)
     info_handler.setFormatter(fmt)
-    
+
     error_handler = logging.FileHandler(os.path.join(LOG_DIR, "error.log"), encoding="utf-8")
     error_handler.setLevel(logging.ERROR)
     error_handler.setFormatter(fmt)
-    
-    # 控制台日志
+
     console_handler = logging.StreamHandler()
     console_handler.setFormatter(fmt)
-    
+
     logger.addHandler(info_handler)
     logger.addHandler(error_handler)
     logger.addHandler(console_handler)
     return logger
 
+
 logger = _init_logger()
 
+
 @app.get("/health")
 async def health():
-    return {"service": "alien_gateway", "status": "ok"}
+    return {"service": "alien_gateway", "status": "ok", "env": settings.APP_ENV}
+
+
+@app.get("/health/redis")
+async def redis_health():
+    try:
+        return check_redis_connection()
+    except Exception as exc:
+        logger.error("redis health check failed err=%s", exc)
+        raise HTTPException(status_code=HTTP_502_BAD_GATEWAY, detail="Redis unavailable")
+
 
 # 此模块未来将承担 JWT 签发、权限校验中间件、路由聚合等核心功能
 @app.post("/auth/login")
@@ -74,22 +85,13 @@ HOP_BY_HOP_HEADERS: List[str] = [
 
 
 def _clean_headers(headers):
-    """移除 hop-by-hop 头,避免转发问题。"""
     return {k: v for k, v in headers.items() if k.lower() not in HOP_BY_HOP_HEADERS}
 
 
-@app.api_route("/api/store/{full_path:path}", methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"])
-async def proxy_to_store(full_path: str, request: Request):
-    """
-    简易网关:监听 33333 端口,将 /api/store/* 转发到 alien_store 服务。
-    """
-    target_url = f"{settings.STORE_BASE_URL}/api/store/{full_path}"
+async def _proxy(request: Request, target_url: str, service_tag: str) -> Response:
+    """通用反向代理:转发请求体、头部、查询参数并回写下游响应。"""
     client_ip = request.client.host if request.client else "-"
-
-    # 读取请求体
     body = await request.body()
-
-    # 过滤头部
     headers = _clean_headers(dict(request.headers))
 
     try:
@@ -102,10 +104,12 @@ async def proxy_to_store(full_path: str, request: Request):
                 params=request.query_params,
             )
     except Exception as exc:
-        logger.error("proxy to store failed ip=%s url=%s err=%s", client_ip, target_url, exc)
+        logger.error(
+            "proxy to %s failed ip=%s url=%s err=%s",
+            service_tag, client_ip, target_url, exc,
+        )
         raise HTTPException(status_code=HTTP_502_BAD_GATEWAY, detail="Upstream unavailable")
 
-    # 返回下游响应
     return Response(
         content=resp.content,
         status_code=resp.status_code,
@@ -113,6 +117,19 @@ async def proxy_to_store(full_path: str, request: Request):
         media_type=resp.headers.get("content-type"),
     )
 
+
+@app.api_route("/api/store/{full_path:path}", methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"])
+async def proxy_to_store(full_path: str, request: Request):
+    target_url = f"{settings.STORE_BASE_URL}/api/store/{full_path}"
+    return await _proxy(request, target_url, "store")
+
+
+@app.api_route("/api/contract/{full_path:path}", methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"])
+async def proxy_to_contract(full_path: str, request: Request):
+    target_url = f"{settings.CONTRACT_BASE_URL}/api/contract/{full_path}"
+    return await _proxy(request, target_url, "contract")
+
+
 if __name__ == "__main__":
     import uvicorn
-    uvicorn.run(app, host="0.0.0.0", port=33333)
+    uvicorn.run(app, host="0.0.0.0", port=settings.GATEWAY_PORT)

+ 1 - 1
alien_lawyer/main.py

@@ -15,4 +15,4 @@ async def health():
 
 if __name__ == "__main__":
     import uvicorn
-    uvicorn.run(app, host="0.0.0.0", port=8004)
+    uvicorn.run(app, host="0.0.0.0", port=settings.LAWYER_PORT)

+ 8 - 2
alien_store/Dockerfile

@@ -17,6 +17,12 @@ RUN poetry source add --priority=primary tsinghua https://pypi.tuna.tsinghua.edu
 
 COPY . .
 
-EXPOSE 8001
+ARG APP_ENV=dev
+ENV APP_ENV=${APP_ENV}
 
-CMD ["uvicorn", "alien_store.main:app", "--host", "0.0.0.0", "--port", "8001"]
+ARG STORE_PORT=8001
+ENV STORE_PORT=${STORE_PORT}
+
+EXPOSE ${STORE_PORT}
+
+CMD ["sh", "-c", "uvicorn alien_store.main:app --host 0.0.0.0 --port ${STORE_PORT}"]

+ 1 - 1
alien_store/main.py

@@ -51,4 +51,4 @@ async def health():
 
 if __name__ == "__main__":
     import uvicorn
-    uvicorn.run(app, host="0.0.0.0", port=8001)
+    uvicorn.run(app, host="0.0.0.0", port=settings.STORE_PORT)

+ 11 - 13
alien_util/celery_app.py

@@ -2,33 +2,31 @@ from celery import Celery
 from celery.schedules import crontab
 from alien_gateway.config import settings
 
-# Redis 配置(可以从环境变量读取,这里使用默认配置)
-REDIS_URL = settings.REDIS_URL
-
-# 创建 Celery 应用
+# 创建 Celery 应用:broker / backend 由 settings 自动适配单机或 Sentinel
 celery_app = Celery(
     "alien_cloud",
-    broker=REDIS_URL,
-    backend=REDIS_URL,
-    include=["alien_util.tasks.contract_tasks"]
+    broker=settings.CELERY_BROKER_URL,
+    backend=settings.CELERY_BACKEND_URL,
+    include=["alien_util.tasks.contract_tasks"],
 )
 
-# Celery 配置
-celery_app.conf.update(
+_celery_conf = dict(
     task_serializer="json",
     accept_content=["json"],
     result_serializer="json",
     timezone="Asia/Shanghai",
     enable_utc=True,
-    # 定时任务配置
     beat_schedule={
         "check-contract-expiry": {
             "task": "alien_util.tasks.contract_tasks.check_contract_expiry",
-            "schedule": crontab(hour=0, minute=1),  # 每天凌晨0点1分执行
+            "schedule": crontab(hour=0, minute=1),  # 每天凌晨 0:01 执行
         },
     },
 )
 
+# 哨兵模式下必须传 transport_options 让 kombu 感知 master_name 等参数
+if settings.CELERY_TRANSPORT_OPTIONS is not None:
+    _celery_conf["broker_transport_options"] = settings.CELERY_TRANSPORT_OPTIONS
+    _celery_conf["result_backend_transport_options"] = settings.CELERY_TRANSPORT_OPTIONS
 
-
-
+celery_app.conf.update(**_celery_conf)

+ 167 - 0
alien_util/redis_client.py

@@ -0,0 +1,167 @@
+"""Redis 客户端工厂:根据 settings.REDIS_MODE 自动选择单机或 Sentinel。
+
+- 单机模式:REDIS_URL 非空时启用,直接 redis.from_url
+- 哨兵模式:REDIS_SENTINELS 非空时启用,通过 Sentinel 获取主从节点
+"""
+from functools import lru_cache
+
+import redis
+from redis import Redis
+from redis.asyncio import Redis as AsyncRedis
+from redis.asyncio import from_url as async_from_url
+from redis.asyncio.sentinel import Sentinel as AsyncSentinel
+from redis.sentinel import Sentinel
+
+from alien_gateway.config import settings
+
+
+def _sentinel_kwargs() -> dict:
+    return settings.REDIS_SENTINEL_KWARGS
+
+
+def _redis_connection_kwargs() -> dict:
+    return {
+        "password": settings.REDIS_PASSWORD or None,
+        "db": settings.REDIS_DB,
+        "socket_timeout": settings.REDIS_SOCKET_TIMEOUT,
+        "socket_connect_timeout": settings.REDIS_CONNECT_TIMEOUT,
+        "retry_on_timeout": True,
+        "decode_responses": True,
+    }
+
+
+def _ensure_sentinel_nodes() -> None:
+    if not settings.REDIS_SENTINEL_NODES:
+        raise RuntimeError("REDIS_SENTINELS 未配置,无法通过 Sentinel 连接 Redis。")
+
+
+def _ensure_redis_url() -> None:
+    if not settings.REDIS_URL:
+        raise RuntimeError("REDIS_URL 未配置,无法以单机模式连接 Redis。")
+
+
+# --------------------------------------------------------------- Sentinel ----
+
+@lru_cache(maxsize=1)
+def get_sentinel_client() -> Sentinel:
+    _ensure_sentinel_nodes()
+    return Sentinel(
+        settings.REDIS_SENTINEL_NODES,
+        socket_timeout=settings.REDIS_SOCKET_TIMEOUT,
+        socket_connect_timeout=settings.REDIS_CONNECT_TIMEOUT,
+        sentinel_kwargs=_sentinel_kwargs(),
+    )
+
+
+@lru_cache(maxsize=1)
+def get_async_sentinel_client() -> AsyncSentinel:
+    _ensure_sentinel_nodes()
+    return AsyncSentinel(
+        settings.REDIS_SENTINEL_NODES,
+        socket_timeout=settings.REDIS_SOCKET_TIMEOUT,
+        socket_connect_timeout=settings.REDIS_CONNECT_TIMEOUT,
+        sentinel_kwargs=_sentinel_kwargs(),
+    )
+
+
+def _sentinel_master() -> Redis:
+    return get_sentinel_client().master_for(
+        service_name=settings.REDIS_MASTER_NAME,
+        **_redis_connection_kwargs(),
+    )
+
+
+def _sentinel_slave() -> Redis:
+    return get_sentinel_client().slave_for(
+        service_name=settings.REDIS_MASTER_NAME,
+        **_redis_connection_kwargs(),
+    )
+
+
+def _async_sentinel_master() -> AsyncRedis:
+    return get_async_sentinel_client().master_for(
+        service_name=settings.REDIS_MASTER_NAME,
+        **_redis_connection_kwargs(),
+    )
+
+
+def _async_sentinel_slave() -> AsyncRedis:
+    return get_async_sentinel_client().slave_for(
+        service_name=settings.REDIS_MASTER_NAME,
+        **_redis_connection_kwargs(),
+    )
+
+
+# ---------------------------------------------------------------- Standalone -
+
+@lru_cache(maxsize=1)
+def _standalone_client() -> Redis:
+    _ensure_redis_url()
+    return redis.from_url(settings.REDIS_URL, decode_responses=True)
+
+
+@lru_cache(maxsize=1)
+def _async_standalone_client() -> AsyncRedis:
+    _ensure_redis_url()
+    return async_from_url(settings.REDIS_URL, decode_responses=True)
+
+
+# ---------------------------------------------------------------- Public API -
+
+def get_redis_master() -> Redis:
+    if settings.REDIS_MODE == "sentinel":
+        return _sentinel_master()
+    return _standalone_client()
+
+
+def get_redis_slave() -> Redis:
+    if settings.REDIS_MODE == "sentinel":
+        return _sentinel_slave()
+    # 单机无主从概念,复用同一连接
+    return _standalone_client()
+
+
+def get_async_redis_master() -> AsyncRedis:
+    if settings.REDIS_MODE == "sentinel":
+        return _async_sentinel_master()
+    return _async_standalone_client()
+
+
+def get_async_redis_slave() -> AsyncRedis:
+    if settings.REDIS_MODE == "sentinel":
+        return _async_sentinel_slave()
+    return _async_standalone_client()
+
+
+def get_redis() -> Redis:
+    """默认入口:单机直接返回,哨兵返回主节点。"""
+    return get_redis_master()
+
+
+def get_async_redis() -> AsyncRedis:
+    return get_async_redis_master()
+
+
+def check_redis_connection() -> dict:
+    """执行 ping 检测连接,并附带模式信息。"""
+    if settings.REDIS_MODE == "sentinel":
+        sentinel = get_sentinel_client()
+        redis_client = get_redis_master()
+        pong = redis_client.ping()
+        host, port = sentinel.discover_master(settings.REDIS_MASTER_NAME)
+        return {
+            "ok": bool(pong),
+            "mode": "sentinel",
+            "master_name": settings.REDIS_MASTER_NAME,
+            "master_host": host,
+            "master_port": port,
+            "db": settings.REDIS_DB,
+        }
+
+    redis_client = get_redis()
+    pong = redis_client.ping()
+    return {
+        "ok": bool(pong),
+        "mode": "standalone",
+        "url": settings.REDIS_URL,
+    }

+ 0 - 74
test.py

@@ -1,74 +0,0 @@
-import requests
-
-
-# url = "http://127.0.0.1:8001/api/store/get_esign_templates"
-# url = "http://120.26.186.130:33333/api/store/get_esign_templates"
-# """
-# 商家入驻AI审核通过后, AI携带真实有效的信息调用此接口,数据库 store_contract 生成对应的数据
-# """
-# body = {
-#     "store_id": 666,
-#     "store_name": "爱丽恩严(大连)商务科技有限公司深圳分公司",
-#     "business_segment": "生活服务",
-#     "merchant_name": "彭少荣",
-#     "contact_phone": "13923864580",
-#     "ord_id": "91440300MADDW7XC4C"
-# }
-# res = requests.post(url, json=body)
-# print(res.text)
-
-# ----------------------------------------------------------------------------------------------------------------------
-# url = "http://127.0.0.1:8001/api/store/contracts/381"
-# url = "http://120.26.186.130:33333/api/store/contracts/381?status=0"
-# """
-# 商家点击合同管理模块时,可以条件筛选查询 未签署合同和已签署合同
-# status:签署状态 0 未签署 1 已签署
-# page:分页查询 页码 默认1
-# page_size:分页查询 每页数量 默认10
-# """
-# resp = requests.get(url)
-# print(resp.text)
-
-# ----------------------------------------------------------------------------------------------------------------------
-url = "http://127.0.0.1:8001/api/store/contracts/detail/d43a18e1e9164a7d8dab4ca5c49d9960"
-# url = "http://120.26.186.130:33333/api/store/contracts/detail/aea4124ac4614936b8625fedb64bac47"
-"""
-商家点击到某个具体的合同 向这个接口发送请求
-由于e签宝提供的合同模板URL会过期,所以会再次携带flow_id 向e签宝发起请求刷新新的URL
-下载URL同理
-"""
-resp = requests.get(url)
-print(resp.text)
-
-# ----------------------------------------------------------------------------------------------------------------------
-# url = "http://127.0.0.1:8001/api/store/esign/signurl"
-# url = "http://120.26.186.130:33333/api/store/esign/signurl"
-# body = {
-#     "sign_flow_id": "8980608cb10c448cbdf96aa7df51179b",
-#     "contact_phone": "13923864580"
-# }
-# """
-# 当商家点击签署按钮时
-# 携带合同相关的签署id和联系方式向e签宝发起请求
-# 获取到签署的页面链接
-# 并将签署url存入该合同对应的sign_url中
-# """
-# resp = requests.post(url, json=body)
-# print(resp.text)
-
-#-----------------------------------------------------------------------------------------------------------------------
-# url = "http://127.0.0.1:8001/api/store/get_all_templates"
-# url = "http://120.26.186.130:33333/api/store/get_all_templates"
-# params = {
-#     "page": 1, # 页码 默认1
-#     "page_size": 10, # 每页条数 默认10
-#     "store": "", # 店铺名称(模糊查询)
-#     "merchant_name": "", # 商家名称(模糊查询)
-#     "signing_status": "", # 签署状态
-#     "business_segment": "", # 经营板块
-#     "store_status": "", # 店铺状态:正常/禁用
-#     "expiry_start": "", # 到期时间起
-#     "expiry_end": "" # 到期时间止
-# }
-# resp = requests.get(url)
-# print(resp.text)