dujian hai 2 semanas
pai
achega
2bb8cdad01

+ 164 - 0
docs/jenkins/produ/README-ACK-GRAY-RELEASE.md

@@ -0,0 +1,164 @@
+# alien-ack-cluster 使用说明与灰度发布
+
+集群:**alien-ack-cluster**(ACK 托管版,Kubernetes **v1.26.2**)。节点池 **default-nodepool** 当前 **3 节点**(截图中状态为 **Unknown** 时须先修复,否则 Pod 无法调度)。
+
+Harbor:**39.106.135.88**(与 Jenkins 构建机同区域/VPC 为佳,避免跨公网拉镜像超时)。
+
+---
+
+## 一、上线前检查清单
+
+### 1. 节点状态为 Ready
+
+控制台节点为 **Unknown** 时,在任意能访问 API 的机器执行:
+
+```bash
+kubectl get nodes -o wide
+kubectl describe node <节点名>
+```
+
+常见原因:
+
+| 现象 | 处理 |
+|------|------|
+| NotReady + 超时 | 节点安全组未放行 **6443**、**10250**;节点与 API Server 网络不通 |
+| Kubelet 未启动 | SSH 到 ECS,`systemctl status kubelet` |
+| 磁盘/内存压力 | `df -h`、`free -h`,清理镜像或扩容 |
+| 容器运行时 | 节点为 **containerd 1.6.20**,勿混用 docker 与 containerd 配置 |
+
+修复后应显示 **Ready**,且 `kubectl get pods -n kube-system` 中 CoreDNS、kube-proxy 正常(控制台「集群巡检」曾提示 CoreDNS 异常时优先处理)。
+
+### 2. 本地 kubectl 接入 ACK
+
+1. 控制台 → **alien-ack-cluster** → **连接信息** → 复制 **公网/内网 kubeconfig**(内网 Jenkins 用内网地址)。  
+2. 保存为文件,例如 `~/.kube/config-ack-alien`。  
+3. Jenkins 凭据类型 **Secret file**,ID 如 `ack-kubeconfig-alien`。  
+4. 流水线中:`withCredentials([file(credentialsId: 'ack-kubeconfig-alien', variable: 'KUBECONFIG')]) { sh 'kubectl get ns' }`。
+
+### 3. Harbor 与 ACK 拉镜像
+
+1. Harbor 项目 `alien` 设为 **私有**,创建 **机器人账号**(push + pull)。  
+2. ACK 命名空间创建 **imagePullSecret**(见 `k8s/examples/secret-harbor.example.yaml`)。  
+3. 各 Deployment `spec.template.spec.imagePullSecrets` 引用该 Secret。  
+4. 在 **每台节点** 或仅依赖 K8S Secret:推荐只用 `imagePullSecrets`,无需节点 docker login。  
+5. 将 Jenkins 机构建用的基础镜像 `my-openjdk8-ffmpeg:v1` 推送到 Harbor,例如:  
+   `docker tag my-openjdk8-ffmpeg:v1 39.106.135.88/alien/base/openjdk8-ffmpeg:v1`  
+   `docker push 39.106.135.88/alien/base/openjdk8-ffmpeg:v1`
+
+---
+
+## 二、推荐 K8S 资源模型
+
+```text
+Namespace: alien-produ
+├── Deployment/gateway          (stable, 副本数 N)
+├── Deployment/gateway-canary   (灰度, 副本数 1~2,仅 canary 策略时更新)
+├── Service/gateway             → stable Pod
+├── Service/gateway-canary      → canary Pod
+├── Ingress/alien-gateway       → 主路由 + Nginx 灰度注解
+└── ConfigMap/Secret              → bootstrap、Jasypt(勿把明文密码提交 Git)
+```
+
+Java 微服务端口与现网 compose 一致(gateway **8000**,store **50014**,…),见各 `deployment-*.yaml`。
+
+**配置中心**:ACK 上 Nacos 若仍用 39.106.135.88 上的实例,须在 Pod 环境变量或 `bootstrap-prod.yml` 中写 **可达的 K8S Service 地址或 SLB**,不能写 Docker 容器名 `nacos`(除非 Nacos 也迁进同一集群)。
+
+---
+
+## 三、灰度发布(推荐:Nginx Ingress 金丝雀)
+
+ACK 控制台 → **应用** → **Ingress** 需已安装 **Nginx Ingress Controller**(应用市场「Nginx Ingress Controller」或「ALB Ingress」;下文以 **Nginx Ingress 注解** 为例,与仓库示例一致)。
+
+### 原理
+
+```mermaid
+flowchart LR
+  User[客户端] --> Ing[Ingress alien-gateway]
+  Ing -->|权重 90%| SvcStable[Service gateway]
+  Ing -->|权重 10%| SvcCanary[Service gateway-canary]
+  SvcStable --> DepStable[Deployment gateway]
+  SvcCanary --> DepCanary[Deployment gateway-canary]
+```
+
+- **stable**:当前线上版本,副本数较多。  
+- **canary**:新版本,副本数少。  
+- **Ingress** 通过注解 `canary` + `canary-weight` 按百分比分流。  
+
+### 操作步骤(与 Jenkins 参数联动)
+
+1. **首次**:`kubectl apply -f k8s/examples/namespace.yaml`  
+2. **首次**:`kubectl apply -f k8s/examples/deployment-gateway.yaml`(及 stable Service)  
+3. **首次**:`kubectl apply -f k8s/examples/deployment-gateway-canary.yaml` + `service-gateway-canary.yaml`  
+4. **首次**:`kubectl apply -f k8s/examples/ingress-gateway-canary.example.yaml`(改 host、TLS)  
+5. Jenkins 构建新镜像 `39.106.135.88/alien/gateway:build-42`  
+6. Job 选 **DEPLOY_STRATEGY=canary**,**CANARY_WEIGHT=10**:  
+   - 仅更新 `gateway-canary` Deployment 镜像  
+   - 将 Ingress 注解 `canary-weight` 设为 `10`  
+7. 观察监控与错误率后,逐步提高 `20` → `50` → `100`  
+8. **全量**:将 **stable** Deployment 镜像改为新版本,**canary-weight 置 0** 或删除 canary Deployment,避免长期双轨  
+
+示例注解(完整见 `ingress-gateway-canary.example.yaml`):
+
+```yaml
+metadata:
+  annotations:
+    nginx.ingress.kubernetes.io/canary: "true"
+    nginx.ingress.kubernetes.io/canary-weight: "10"
+```
+
+### 与 RollingUpdate 的区别
+
+| 方式 | K8S 内置 RollingUpdate | Ingress 灰度(本方案) |
+|------|------------------------|-------------------------|
+| 流量 | 逐 Pod 替换,旧版 Pod 逐渐减少 | 新旧版本 **同时** 接流量,可调比例 |
+| 回滚 | `kubectl rollout undo` | 将 `canary-weight` 设为 `0` 或回滚 canary Deployment |
+| Jenkins | `DEPLOY_STRATEGY=rolling` | `DEPLOY_STRATEGY=canary` |
+
+---
+
+## 四、三节点节点池容量建议
+
+当前约 **25 核 / 45Gi** 集群总量(控制台资源监控)。7 个 Java 服务若各 **2 副本** + 灰度 **1 副本**,峰值 Pod 数约 **7×3=21**,需控制每 Pod **request**(示例 manifest 中 `512Mi`/`250m`),避免 Pending。
+
+调度建议:
+
+- 无状态服务:**podAntiAffinity** 尽量 spread 到 3 台(示例 deployment 已含软反亲和)  
+- **gateway** 可 2 副本 + HPA(需 metrics-server;控制台若提示 APIService `metrics.k8s.io` 不可用,先装 metrics-server 再开 HPA)  
+- **job**、定时任务类可 1 副本  
+
+---
+
+## 五、从 Docker Compose 迁到 ACK 的映射
+
+| Compose(39.105.153.68 produ) | ACK |
+|------------------------------|-----|
+| `gateway-produ` 容器 | Deployment `gateway` |
+| 卷挂载 jar | 镜像内打包 jar(本流水线 Dockerfile) |
+| `common-network-produ` | ClusterIP Service + Ingress |
+| 环境变量 Jasypt | Secret `alien-jasypt` 或 ACK 配置项 |
+| 支付证书目录 | Secret volume `alien-pay-cert-store` 等 |
+
+迁移动作顺序建议:**gateway → store → 其余**;每迁一个服务,Ingress 切一条 path 或子域名,保留 compose 回滚路径直至稳定。
+
+---
+
+## 六、阿里云 ACK 控制台常用入口
+
+| 目标 | 路径 |
+|------|------|
+| 工作负载 / Deployment | 集群 → 工作负载 → 无状态 |
+| 灰度 / Ingress | 集群 → 网络 → Ingress |
+| 镜像拉取失败事件 | 工作负载 → Pod → 事件 |
+| 日志 | 工作负载 → Pod → 日志;或接入 SLS |
+| 节点池扩容 | 节点管理 → default-nodepool → 扩容 |
+
+---
+
+## 七、与 Jenkins produ 流水线联调
+
+1. 手工 `kubectl apply` 示例 manifest 一次。  
+2. 跑通 `gateway-k8s` Job(rolling,权重 100% 等价全量)。  
+3. 再跑 `canary` + `CANARY_WEIGHT=5` 验证流量分裂。  
+4. 其余 6 个服务复制 gateway 的 Ingress/Deployment 命名模式。  
+
+问题排查:`kubectl describe pod -n alien-produ`、`kubectl logs -n alien-produ deploy/gateway-canary`。

+ 157 - 0
docs/jenkins/produ/README.md

@@ -0,0 +1,157 @@
+# 预生产分支 → Harbor → ACK 生产 K8S 流水线
+
+Harbor 宿主机:**39.106.135.88**(与 Jenkins 同机或同 VPC)。ACK 集群:**alien-ack-cluster**(华北2 北京)。
+
+本目录支持 **单服务发版** 与 **整体/多选发版**(Harbor + ACK);与宿主机 `docker compose` 晋升流水线(`Jenkinsfile-prod-promote-from-uat.groovy`)并行存在,互不替代,直到 ACK 全量切流。
+
+---
+
+## 常见问题(Harbor / 目录结构 / 发版粒度)
+
+### Harbor 要不要在 Web 界面手工操作?
+
+**日常发版不需要。** 流水线在 `k8s-produ-lib.groovy` 里自动完成:
+
+1. `docker login 39.106.135.88`(凭据 `harbor-robot-alien`)
+2. `docker build` → 打 tag `39.106.135.88/alien/<服务名>:<tag>`
+3. `docker push` 到 Harbor
+4. `kubectl set image` 让 ACK 拉同一 tag
+
+运维**仅需一次性**在 Harbor Web(或 API)完成:`alien` 项目、机器人账号、(可选)镜像保留策略。之后查镜像/删旧 tag/排障时仍可打开 Harbor UI,**不是发版必经步骤**。
+
+### `_shared/k8s-produ-lib.groovy` 是什么?能单独部署吗?
+
+| 文件 | 角色 | Jenkins Script Path 是否指向它 |
+|------|------|-------------------------------|
+| `produ/<服务>/Jenkinsfile` | **入口**(Jenkins 只认这个名字) | **是**,每个 Job 指向各自路径 |
+| `_shared/k8s-produ-lib.groovy` | **共享函数库**(checkout、mvn、Harbor、kubectl) | **否**,由入口 `load '.../k8s-produ-lib.groovy'` 引用 |
+| `_shared/service-registry.groovy` | 七个服务列表 + whole/multi 筛选 | 否,供 `whole/Jenkinsfile` 使用 |
+
+每个微服务 Job 仍是 **单独一条流水线**(例如只构建 gateway);共享库是为避免 7 份重复脚本,**不代替** `Jenkinsfile`。
+
+### 既要单独发布,又要整体发布?
+
+| Jenkins Job(建议名) | Script Path | 粒度 |
+|----------------------|-------------|------|
+| `gateway-k8s` … `dining-k8s` | `docs/jenkins/produ/<prodDir>/Jenkinsfile` | **仅该服务**(Maven `-pl alien-xxx`) |
+| `alien-cloud-k8s-whole` | `docs/jenkins/produ/whole/Jenkinsfile` | **whole / single / multi**(参数 `DEPLOY_MODE`,语义同 prod-promote) |
+
+整体 Job 一次 `mvn package` 打全仓库,再按选中服务依次 build/push/deploy;单服务 Job 只打对应模块,适合热修复单个服务。
+
+---
+
+## 一、Jenkins 任务如何组织(对应你截图中的「新建任务」)
+
+### 1. 是否选「文件夹」?
+
+**是,推荐选「文件夹」。**
+
+| 类型 | 用途 |
+|------|------|
+| **文件夹** | 独立命名空间,其下可建多个 **流水线** 任务,例如 `gateway-k8s`、`store-k8s`,名称可与其他视图重复 |
+| **流水线** | 真正执行构建的 Job,每个微服务一条 |
+| **多分支流水线** | 按 Git 分支自动建子 Job;若预生产固定一条分支(如 `uat-20260202`),用普通流水线 + 参数 `GIT_BRANCH` 更简单 |
+
+建议结构:
+
+```text
+alien-cloud-produ-K8S-from-uat/     ← 文件夹(你截图中的名称)
+├── alien-cloud-k8s-whole/          ← 流水线,Script Path: docs/jenkins/produ/whole/Jenkinsfile(整体/多选)
+├── gateway-k8s/                    ← 流水线,Script Path: docs/jenkins/produ/gateway/Jenkinsfile
+├── store-k8s/
+├── second-k8s/
+├── store-platform-k8s/
+├── lawyer-k8s/
+├── job-k8s/
+└── dining-k8s/
+```
+
+**不要**在文件夹里再选「文件夹」类型去套微服务——微服务一级用 **流水线** 即可。
+
+### 2. 是否用 Pipeline script from SCM?
+
+**可以,且推荐。**
+
+每个 `*-k8s` 流水线 Job 配置:
+
+1. **流水线** → **定义**:Pipeline script from SCM  
+2. **SCM**:Git,仓库 `http://8.152.195.41:3000/alien/alien_cloud`(与现网一致)  
+3. **分支**:`*/uat-20260202` 或 `*/你的预生产分支`  
+4. **Script Path**(按服务不同):
+   - gateway:`docs/jenkins/produ/gateway/Jenkinsfile`
+   - store:`docs/jenkins/produ/store/Jenkinsfile`
+   - … 见下表  
+
+5. **凭据**:`zhanghaomimapingzheng`(与 UAT 流水线一致)  
+6. **构建触发**:可选手动;稳定后加「上游 UAT 构建成功」触发  
+
+与 `Jenkinsfile-prod-promote-from-uat.groovy` 的关系:
+
+| 维度 | prod-promote-from-uat | produ K8S 流水线 |
+|------|----------------------|------------------|
+| 制品 | UAT 目录 jar → 生产机 compose | Maven jar → 镜像 → Harbor → ACK |
+| 发版粒度 | 单 Job 内 whole/single/multi | **whole Job** + **每服务一个 Job** |
+| 灰度 | 无 | 支持 Ingress 权重灰度(见 README-ACK-GRAY-RELEASE.md) |
+
+---
+
+## 二、微服务与 Script Path 对照
+
+| prodDir | 模块 | Jenkins Script Path | ACK Deployment(默认) |
+|---------|------|---------------------|-------------------------|
+| gateway | alien-gateway | `docs/jenkins/produ/gateway/Jenkinsfile` | `gateway` |
+| store | alien-store | `docs/jenkins/produ/store/Jenkinsfile` | `store` |
+| second | alien-second | `docs/jenkins/produ/second/Jenkinsfile` | `second` |
+| store-platform | alien-store-platform | `docs/jenkins/produ/store-platform/Jenkinsfile` | `store-platform` |
+| lawyer | alien-lawyer | `docs/jenkins/produ/lawyer/Jenkinsfile` | `lawyer` |
+| job | alien-job | `docs/jenkins/produ/job/Jenkinsfile` | `job` |
+| dining | alien-dining | `docs/jenkins/produ/dining/Jenkinsfile` | `dining` |
+| **(整体)** | 全部七个 | `docs/jenkins/produ/whole/Jenkinsfile` | 按 `DEPLOY_MODE` |
+
+**未纳入本目录**(属其他仓库或镜像名不同,需另建 Job):
+
+- Python:`py_gateway_produ`、`py_contract_produ`、`py_esign_produ`(镜像 `alien_gateway:local` 等)  
+- `xxl-job-admin-produ`、Harbor 自身组件  
+
+---
+
+## 三、Jenkins 侧必配项(首次)
+
+在 Jenkins **全局**或文件夹 **凭据** 中准备:
+
+| 凭据 ID(示例,按实际改 Jenkinsfile 内常量) | 类型 | 用途 |
+|---------------------------------------------|------|------|
+| `zhanghaomimapingzheng` | Username/password | Git 拉取 |
+| `harbor-robot-alien` | Username/password | `docker login 39.106.135.88` |
+| `ack-kubeconfig-alien` | Secret file | `KUBECONFIG`,内容来自 ACK「连接信息」下载 |
+
+Jenkins 节点需具备:
+
+- `mvn`、`docker`(能访问 Harbor 443/80)、`kubectl`  
+- 若 Jenkins 在容器内:挂载 `docker.sock`,并将 `kubectl` 与 kubeconfig 挂入或装在镜像内  
+
+Harbor 上建议建项目 **`alien`**,镜像名:`39.106.135.88/alien/<prodDir>:<tag>`。
+
+基础镜像 `my-openjdk8-ffmpeg:v1` 需 **先 push 到 Harbor**(如 `39.106.135.88/alien/base/openjdk8-ffmpeg:v1`),并在 Job 参数 `BASE_IMAGE` 中填写,否则 ACK 节点拉不到。
+
+---
+
+## 四、ACK 与灰度
+
+详述见同目录 **[README-ACK-GRAY-RELEASE.md](./README-ACK-GRAY-RELEASE.md)**(集群接入、命名空间、Ingress 灰度、节点 Unknown 排查)。
+
+K8S 清单示例:`k8s/examples/`。
+
+---
+
+## 五、流水线参数速览
+
+各服务 Jenkinsfile 共用库 `docs/jenkins/produ/_shared/k8s-produ-lib.groovy`,主要参数:
+
+- `GIT_BRANCH`:预生产分支  
+- `IMAGE_TAG`:默认 `build-${BUILD_NUMBER}`,可填 `uat-20260202-123`  
+- `DEPLOY_STRATEGY`:`rolling`(直接替换)或 `canary`(Ingress 权重)  
+- `CANARY_WEIGHT`:灰度流量百分比(仅 canary)  
+- `K8S_NAMESPACE`:默认 `alien-produ`  
+
+首次部署前在 ACK 应用 `k8s/examples/namespace.yaml` 与各服务 `deployment-*.yaml`(或让流水线 `kubectl apply` 首次全量 apply)。

+ 104 - 0
docs/jenkins/produ/_shared/k8s-produ-lib.groovy

@@ -0,0 +1,104 @@
+/**
+ * ACK + Harbor 发版共享函数库(不是 Jenkins 入口文件)。
+ *
+ * - 单服务:produ/<prodDir>/Jenkinsfile 在顶层 pipeline 内 load 本文件后调用各 stage 函数。
+ * - 整体/多选:produ/whole/Jenkinsfile load 本文件 + service-registry.groovy。
+ *
+ * Harbor:流水线内 docker login / build / push,日常发版无需打开 Harbor Web。
+ */
+
+def checkoutBranch(def script, String branch, String gitUrl, String credentialsId) {
+    script.git branch: branch, credentialsId: credentialsId, url: gitUrl
+    script.sh """
+        set -e
+        git fetch origin
+        git reset --hard origin/${branch}
+        git log -1 --oneline
+    """
+}
+
+def mavenPackageModule(def script, String module) {
+    script.sh """
+        set -e
+        unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY ALL_PROXY all_proxy || true
+        export MAVEN_OPTS="-Dmaven.wagon.http.ssl.insecure=true -Dmaven.wagon.http.ssl.allowall=true"
+        mvn -version
+        mvn clean package -pl ${module} -am -DskipTests -e \\
+            -Dmaven.repo.local=\${WORKSPACE}/.m2/repository
+    """
+}
+
+/** 整体发版:根 POM 一次打包全部模块(比 7 次单模块 mvn 更快) */
+def mavenPackageAll(def script) {
+    script.sh """
+        set -e
+        unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY ALL_PROXY all_proxy || true
+        export MAVEN_OPTS="-Dmaven.wagon.http.ssl.insecure=true -Dmaven.wagon.http.ssl.allowall=true"
+        mvn -version
+        mvn clean package -DskipTests -e \\
+            -Dmaven.repo.local=\${WORKSPACE}/.m2/repository
+    """
+}
+
+def dockerBuildAndPush(def script, Map cfg) {
+    def registry = cfg.harborRegistry
+    def project = cfg.harborProject
+    def prodDir = cfg.prodDir
+    def module = cfg.module
+    def serverPort = cfg.serverPort
+    def withLib = cfg.withLib ? 'true' : 'false'
+    def baseImage = cfg.baseImage
+    def imageRef = cfg.imageRef
+    def harborCreds = cfg.harborCredentialsId
+    def dockerfile = 'docs/jenkins/produ/docker/Dockerfile.java-service'
+    def moduleDir = "${script.env.WORKSPACE}/${module}"
+    def jarName = "${module}-1.0.0.jar"
+
+    script.withCredentials([script.usernamePassword(
+        credentialsId: harborCreds, usernameVariable: 'HARBOR_USER', passwordVariable: 'HARBOR_PASS')]) {
+        script.sh """
+            set -e
+            mkdir -p ${moduleDir}/lib
+            touch ${moduleDir}/lib/.keep
+            echo "\${HARBOR_PASS}" | docker login ${registry} -u "\${HARBOR_USER}" --password-stdin
+            docker build -f ${dockerfile} \\
+                --build-arg BASE_IMAGE=${baseImage} \\
+                --build-arg JAR_FILE=${jarName} \\
+                --build-arg SERVER_PORT=${serverPort} \\
+                --build-arg WITH_LIB=${withLib} \\
+                -t ${imageRef} \\
+                ${moduleDir}
+            docker push ${imageRef}
+        """
+    }
+}
+
+def deployToAck(def script, Map cfg) {
+    def ns = cfg.k8sNamespace
+    def imageRef = cfg.imageRef
+    def strategy = cfg.deployStrategy
+    def targetDeploy = strategy == 'canary' ? cfg.deploymentCanary : cfg.deploymentStable
+    def canaryWeight = cfg.canaryWeight
+    def ingressCanary = cfg.ingressCanary
+    def kubeCreds = cfg.kubeCredentialsId
+
+    script.withCredentials([script.file(credentialsId: kubeCreds, variable: 'KUBECONFIG')]) {
+        script.sh """
+            set -e
+            kubectl config current-context
+            kubectl -n ${ns} set image deployment/${targetDeploy} app=${imageRef} --record
+            kubectl -n ${ns} rollout status deployment/${targetDeploy} --timeout=300s
+        """
+        if (strategy == 'canary') {
+            script.sh """
+                set -e
+                kubectl -n ${ns} annotate ingress ${ingressCanary} \\
+                    nginx.ingress.kubernetes.io/canary=true \\
+                    nginx.ingress.kubernetes.io/canary-weight=${canaryWeight} \\
+                    --overwrite
+            """
+        }
+    }
+}
+
+return this

+ 430 - 0
docs/nginx/conf.d/temp/ailien-uat.aliyun-ecs.conf

@@ -0,0 +1,430 @@
+# ============================================================
+# UAT 完整配置:uat.ailien.shop(443 + 80 跳转)
+# 仓库路径:alien_cloud/docs/nginx/conf.d/temp/ailien-uat.aliyun-ecs.conf
+#
+# 【部署说明】
+# - 本文件在 conf.d/temp/ 下,不会被主配置 include conf.d/*.conf 自动加载。
+# - 上线:复制或软链到 Nginx 宿主机,例如:
+#     cp docs/nginx/conf.d/temp/ailien-uat.aliyun-ecs.conf /alien_prod/nginx/conf.d/ailien-uat.conf
+# - 依赖同目录上级:conf.d/00-common.conf(limit_req_zone payment_prepay、map $cors_origin)
+# - 片段:conf.d/temp/ailien-uat.inc.temp-tus-upload-verify-locations.conf(/files、/upload、/verify)
+#
+# 【路由分工】(与 upaibm_system Python 网关、Java alien-gateway 一致)
+#   /ai/、/ai/ws     → uat_ai_service(H100 upaibm gateway,UAT 端口 9300,勿用 30019)
+#   /api/、/         → uat_gateway(alien-gateway,ECS 私网 IP + 宿主机映射端口)
+#   /alienStore/ 等  → 可直连 store/second/dining,降低网关单点故障影响
+#
+# 【upstream 核对】部署前在 Nginx 容器/宿主机执行 docker ps,对照宿主机 PORTS 修改下方 IP:端口。
+#   - 阿里云 ECS 私网示例:172.23.9.202(勿用公网 EIP 作 upstream,易 upstream timed out)
+#   - AI 网关(upaibm):192.168.2.250:9300(见 upaibm_system gateway_service/.env.uat)
+# ============================================================
+
+# 阿里云 ECS:Java 微服务与 alien-gateway(VPC 私网 IP)
+upstream uat_gateway {
+    server 172.23.9.202:8001;
+    # server 172.23.9.202:18000;  # 若 compose 映射为 18000→容器 8000,改为此行并注释上一行
+    keepalive 32;
+}
+
+upstream uat_store {
+    server 172.23.9.202:30014;
+    # server 172.23.9.202:13004;  # 部分 compose:13004→容器 30004
+    keepalive 8;
+}
+
+upstream uat_second {
+    server 172.23.9.202:30015;
+    # server 172.23.9.202:13005;  # 部分 compose:13005→容器 30005
+    keepalive 8;
+}
+
+upstream uat_dining {
+    server 172.23.9.202:30019;
+    # server 39.106.135.88:30019;  # dining 若在其它机器,改为实际 IP:端口
+    keepalive 8;
+}
+
+# 预生产 AI(upaibm_system gateway_service,Consul + /ai/{service}/...)
+upstream uat_ai_service {
+    server 39.106.135.88:9300;
+    # server 172.23.9.202:9300;    # 若 AI 网关与 Java 同机且映射 9300,可改为此行
+    keepalive 32;
+}
+
+# 本机 upload 栈 nginx-gateway(docker-compose: 0.0.0.0:40007->80)
+upstream uat_upload_stack {
+    server 127.0.0.1:40007;
+    keepalive 8;
+}
+
+# 商户端 Tus/简单上传:同源 /ai-upload/ → 回环本 server 443(对齐 VITE_PROXY /ai-upload)
+upstream upl_ai_upload {
+    server uat.ailien.shop:443;
+    keepalive 8;
+}
+
+
+# --------------- UAT:https://uat.ailien.shop (443) ---------------
+server {
+    listen 443 ssl;
+    http2 on;
+    server_name uat.ailien.shop;
+
+    ssl_certificate     /etc/nginx/ssl/ailien.shop.pem;
+    ssl_certificate_key /etc/nginx/ssl/ailien.shop.key;
+    ssl_session_timeout 1d;
+    ssl_protocols TLSv1.2 TLSv1.3;
+    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
+
+    access_log /var/log/nginx/uat/uat.ailien.shop.access.log main;
+    error_log  /var/log/nginx/uat/uat.ailien.shop.error.log warn;
+
+    # 微信公众号 JS 接口安全域名校验(须在 location / 及网关代理之前)
+    # 后台填写:uat.ailien.shop  磁盘:/cert/MP_verify_u6feCTxr5iTTBeIx.txt
+    location = /MP_verify_u6feCTxr5iTTBeIx.txt {
+        alias /cert/MP_verify_u6feCTxr5iTTBeIx.txt;
+        default_type text/plain;
+        add_header Content-Type "text/plain; charset=utf-8";
+    }
+
+    # 商户端前端:https://uat.ailien.shop/group_web_merchant/
+    location /group_web_merchant/ {
+        root /alien_uat/nginx/html;
+        index index.html;
+        try_files $uri $uri/ @uat_merchant_spa;
+    }
+    location @uat_merchant_spa {
+        root /alien_uat/nginx/html;
+        try_files /group_web_merchant/index.html =404;
+    }
+    location = /group_web_merchant {
+        return 301 $scheme://$host/group_web_merchant/;
+    }
+
+    # GroupWeb:https://uat.ailien.shop/group_web/ 中台
+    location /group_web/ {
+        root /alien_uat/nginx/html;
+        index index.html;
+        try_files $uri $uri/ @uat_group_spa;
+    }
+    location @uat_group_spa {
+        root /alien_uat/nginx/html;
+        try_files /group_web/index.html =404;
+    }
+    location = /group_web {
+        return 301 $scheme://$host/group_web/;
+    }
+
+    # GroupLawyerWeb:https://uat.ailien.shop/group_lawyer_web/
+    location /group_lawyer_web/ {
+        root /alien_uat/nginx/html;
+        index index.html;
+        try_files $uri $uri/ @uat_lawyer_spa;
+    }
+    location @uat_lawyer_spa {
+        root /alien_uat/nginx/html;
+        try_files /group_lawyer_web/index.html =404;
+    }
+    location = /group_lawyer_web {
+        return 301 $scheme://$host/group_lawyer_web/;
+    }
+
+    # HBuilder 分享页等:勿走网关,否则 Spring Whitelabel 404。须写在 location / 之前。
+    location ^~ /h5/HBuilderProjects/ {
+        root /alien_uat/nginx/html;
+        try_files $uri =404;
+        add_header Cache-Control "public, max-age=300";
+    }
+
+    # AI WebSocket(wss://uat.ailien.shop/ai/ws/... → upaibm gateway)
+    location /ai/ws {
+        proxy_pass http://uat_ai_service;
+        proxy_http_version 1.1;
+        proxy_set_header Upgrade $http_upgrade;
+        proxy_set_header Connection $connection_upgrade;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header X-Forwarded-Proto $scheme;
+        proxy_connect_timeout 60s;
+        proxy_send_timeout 3600s;
+        proxy_read_timeout 3600s;
+    }
+
+    # AI HTTP(/ai/life-manager/... 等,upaibm Python 微服务)
+    location /ai/ {
+        if ($request_method = 'OPTIONS') {
+            add_header 'Access-Control-Allow-Origin' $cors_origin;
+            add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS';
+            add_header 'Access-Control-Allow-Headers' '*';
+            add_header 'Access-Control-Allow-Credentials' 'true';
+            add_header 'Access-Control-Max-Age' 3600;
+            add_header 'Content-Length' 0;
+            return 204;
+        }
+        add_header 'Access-Control-Allow-Origin' $cors_origin always;
+        add_header 'Access-Control-Allow-Credentials' 'true' always;
+        proxy_pass http://uat_ai_service;
+        proxy_http_version 1.1;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header X-Forwarded-Proto $scheme;
+        proxy_connect_timeout 60s;
+        proxy_send_timeout 300s;
+        proxy_read_timeout 300s;
+    }
+
+    # WebSocket 直连 alien-store:/alienStore/socket/xxx → /socket/xxx
+    location /alienStore/socket/ {
+        rewrite ^/alienStore/socket/(.*)$ /socket/$1 break;
+        proxy_pass http://uat_store;
+        proxy_http_version 1.1;
+        proxy_set_header Upgrade $http_upgrade;
+        proxy_set_header Connection $connection_upgrade;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header X-Forwarded-Proto $scheme;
+        proxy_connect_timeout 60s;
+        proxy_send_timeout 3600s;
+        proxy_read_timeout 3600s;
+    }
+
+    # HTTP 直连 alien-store(网关故障时 App 仍可访问 store API)
+    location /alienStore/ {
+        if ($request_method = 'OPTIONS') {
+            add_header 'Access-Control-Allow-Origin' $cors_origin;
+            add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS';
+            add_header 'Access-Control-Allow-Headers' '*';
+            add_header 'Access-Control-Allow-Credentials' 'true';
+            add_header 'Access-Control-Max-Age' 3600;
+            add_header 'Content-Length' 0;
+            return 204;
+        }
+        proxy_hide_header Access-Control-Allow-Origin;
+        proxy_hide_header Access-Control-Allow-Credentials;
+        proxy_hide_header Access-Control-Allow-Methods;
+        proxy_hide_header Access-Control-Allow-Headers;
+        proxy_hide_header Access-Control-Expose-Headers;
+        proxy_hide_header Access-Control-Max-Age;
+        add_header 'Access-Control-Allow-Origin' $cors_origin always;
+        add_header 'Access-Control-Allow-Credentials' 'true' always;
+        add_header Vary Origin always;
+        proxy_pass http://uat_store;
+        proxy_http_version 1.1;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header X-Forwarded-Proto $scheme;
+        proxy_set_header Upgrade $http_upgrade;
+        proxy_set_header Connection $connection_upgrade;
+        proxy_connect_timeout 60s;
+        proxy_send_timeout 3600s;
+        proxy_read_timeout 3600s;
+    }
+
+    # HTTP 直连 alien-second
+    location /alienSecond/ {
+        if ($request_method = 'OPTIONS') {
+            add_header 'Access-Control-Allow-Origin' $cors_origin;
+            add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS';
+            add_header 'Access-Control-Allow-Headers' '*';
+            add_header 'Access-Control-Allow-Credentials' 'true';
+            add_header 'Access-Control-Max-Age' 3600;
+            add_header 'Content-Length' 0;
+            return 204;
+        }
+        proxy_hide_header Access-Control-Allow-Origin;
+        proxy_hide_header Access-Control-Allow-Credentials;
+        proxy_hide_header Access-Control-Allow-Methods;
+        proxy_hide_header Access-Control-Allow-Headers;
+        proxy_hide_header Access-Control-Expose-Headers;
+        proxy_hide_header Access-Control-Max-Age;
+        add_header 'Access-Control-Allow-Origin' $cors_origin always;
+        add_header 'Access-Control-Allow-Credentials' 'true' always;
+        add_header Vary Origin always;
+        proxy_pass http://uat_second;
+        proxy_http_version 1.1;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header X-Forwarded-Proto $scheme;
+        proxy_set_header Upgrade $http_upgrade;
+        proxy_set_header Connection $connection_upgrade;
+        proxy_connect_timeout 60s;
+        proxy_send_timeout 3600s;
+        proxy_read_timeout 3600s;
+    }
+
+    # 点餐 SSE 长连接:直连 alien-dining
+    location /alienDining/store/order/sse/ {
+        rewrite ^/alienDining/(.*)$ /$1 break;
+        proxy_pass http://uat_dining;
+        proxy_http_version 1.1;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header X-Forwarded-Proto $scheme;
+        proxy_connect_timeout 60s;
+        proxy_send_timeout 86400s;
+        proxy_read_timeout 86400s;
+        proxy_buffering off;
+    }
+
+    # 支付预下单限流(依赖 00-common.conf 中 limit_req_zone payment_prepay)
+    location ~* payment/prePay {
+        limit_req zone=payment_prepay burst=1 nodelay;
+        limit_req_status 429;
+        add_header X-Payment-Limit "applied" always;
+        rewrite ^/api/(.*)$ /$1 break;
+        proxy_pass http://uat_gateway;
+        proxy_http_version 1.1;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header X-Forwarded-Proto $scheme;
+        proxy_set_header Upgrade $http_upgrade;
+        proxy_set_header Connection $connection_upgrade;
+        proxy_connect_timeout 60s;
+        proxy_send_timeout 3600s;
+        proxy_read_timeout 3600s;
+        if ($request_method = 'OPTIONS') {
+            add_header 'Access-Control-Allow-Origin' $cors_origin;
+            add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS';
+            add_header 'Access-Control-Allow-Headers' '*';
+            add_header 'Access-Control-Allow-Credentials' 'true';
+            add_header 'Access-Control-Max-Age' 3600;
+            add_header 'Content-Length' 0;
+            return 204;
+        }
+        proxy_hide_header Access-Control-Allow-Origin;
+        proxy_hide_header Access-Control-Allow-Credentials;
+        proxy_hide_header Access-Control-Allow-Methods;
+        proxy_hide_header Access-Control-Allow-Headers;
+        proxy_hide_header Access-Control-Expose-Headers;
+        proxy_hide_header Access-Control-Max-Age;
+        add_header 'Access-Control-Allow-Origin' $cors_origin always;
+        add_header 'Access-Control-Allow-Credentials' 'true' always;
+        add_header Vary Origin always;
+    }
+
+    # /api/xxx → 去前缀后转发 Java alien-gateway
+    location /api/ {
+        rewrite ^/api/(.*)$ /$1 break;
+        proxy_pass http://uat_gateway;
+        proxy_http_version 1.1;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header X-Forwarded-Proto $scheme;
+        proxy_set_header Upgrade $http_upgrade;
+        proxy_set_header Connection $connection_upgrade;
+        proxy_connect_timeout 60s;
+        proxy_send_timeout 3600s;
+        proxy_read_timeout 3600s;
+        if ($request_method = 'OPTIONS') {
+            add_header 'Access-Control-Allow-Origin' $cors_origin;
+            add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS';
+            add_header 'Access-Control-Allow-Headers' '*';
+            add_header 'Access-Control-Allow-Credentials' 'true';
+            add_header 'Access-Control-Max-Age' 3600;
+            add_header 'Content-Length' 0;
+            return 204;
+        }
+        proxy_hide_header Access-Control-Allow-Origin;
+        proxy_hide_header Access-Control-Allow-Credentials;
+        proxy_hide_header Access-Control-Allow-Methods;
+        proxy_hide_header Access-Control-Allow-Headers;
+        proxy_hide_header Access-Control-Expose-Headers;
+        proxy_hide_header Access-Control-Max-Age;
+        add_header 'Access-Control-Allow-Origin' $cors_origin always;
+        add_header 'Access-Control-Allow-Credentials' 'true' always;
+        add_header Vary Origin always;
+    }
+
+    # 静态上传目录(宿主机 /uploads)
+    location ^~ /uploads/ {
+        alias /uploads/;
+        try_files $uri =404;
+        add_header Cache-Control "public, max-age=86400";
+    }
+
+    # 商户端同源上传代理:/ai-upload/ → https://uat.ailien.shop(去掉 /ai-upload 前缀)
+    location = /ai-upload {
+        return 301 $scheme://$host/ai-upload/;
+    }
+    location ^~ /ai-upload/ {
+        rewrite ^/ai-upload(.*)$ $1 break;
+        proxy_pass https://upl_ai_upload;
+        proxy_http_version 1.1;
+        proxy_set_header Connection "";
+        proxy_ssl_server_name on;
+        proxy_ssl_name uat.ailien.shop;
+        proxy_set_header Host uat.ailien.shop;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header X-Forwarded-Proto $scheme;
+        proxy_connect_timeout 60s;
+        proxy_send_timeout 3600s;
+        proxy_read_timeout 3600s;
+        client_max_body_size 0;
+        proxy_request_buffering off;
+    }
+
+    # Tus /upload /verify(片段与 conf.d/temp/ailien-uat.inc.temp-tus-upload-verify-locations.conf 同步)
+    include conf.d/temp/ailien-uat.inc.temp-tus-upload-verify-locations.conf;
+
+    # 其余请求 → Java alien-gateway
+    location / {
+        if ($request_method = 'OPTIONS') {
+            add_header 'Access-Control-Allow-Origin' $cors_origin;
+            add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS';
+            add_header 'Access-Control-Allow-Headers' '*';
+            add_header 'Access-Control-Allow-Credentials' 'true';
+            add_header 'Access-Control-Max-Age' 3600;
+            add_header 'Content-Length' 0;
+            return 204;
+        }
+        proxy_hide_header Access-Control-Allow-Origin;
+        proxy_hide_header Access-Control-Allow-Credentials;
+        proxy_hide_header Access-Control-Allow-Methods;
+        proxy_hide_header Access-Control-Allow-Headers;
+        proxy_hide_header Access-Control-Expose-Headers;
+        proxy_hide_header Access-Control-Max-Age;
+        add_header 'Access-Control-Allow-Origin' $cors_origin always;
+        add_header 'Access-Control-Allow-Credentials' 'true' always;
+        add_header Vary Origin always;
+        proxy_pass http://uat_gateway;
+        proxy_http_version 1.1;
+        proxy_set_header Upgrade $http_upgrade;
+        proxy_set_header Connection $connection_upgrade;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header X-Forwarded-Proto $scheme;
+        proxy_connect_timeout 60s;
+        proxy_send_timeout 3600s;
+        proxy_read_timeout 3600s;
+    }
+}
+
+
+# --------------- UAT:HTTP 80 → HTTPS 443(微信域名校验文件须 HTTP 可访问) ---------------
+server {
+    listen 80;
+    server_name uat.ailien.shop;
+
+    access_log /var/log/nginx/uat/uat.ailien.shop.80.access.log main;
+    error_log  /var/log/nginx/uat/uat.ailien.shop.80.error.log warn;
+
+    location = /MP_verify_u6feCTxr5iTTBeIx.txt {
+        alias /cert/MP_verify_u6feCTxr5iTTBeIx.txt;
+        default_type text/plain;
+        add_header Content-Type "text/plain; charset=utf-8";
+    }
+
+    location / {
+        return 301 https://$host$request_uri;
+    }
+}