From d19cb53f24fe6b2d7ba3d44e4af45a3c55a6a311 Mon Sep 17 00:00:00 2001 From: zetaloop Date: Sun, 3 May 2026 18:59:11 +0800 Subject: [PATCH 1/6] =?UTF-8?q?docs:=20=E6=B8=85=E7=90=86=E8=BF=87?= =?UTF-8?q?=E6=97=B6=E7=9A=84=E9=83=A8=E7=BD=B2=E6=96=87=E6=A1=A3=E4=B8=8E?= =?UTF-8?q?=E6=A8=A1=E6=9D=BF=E6=AE=8B=E7=95=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/docker/.env.example | 4 - deploy/docker/README.md | 48 - deploy/docker/docker-compose.yml | 9 - deploy/example/OPERATOR-INSTALL-EXAMPLE.md | 184 -- deploy/example/kafka-strimzi-example.yaml | 80 - deploy/example/mongodb-community-example.yaml | 36 - .../mongodb-ha-replicaset-example.yaml | 46 - .../mongodb-sharded-cluster-example.yaml | 218 --- deploy/example/pg-dx-configmap.yaml | 11 - docs/ENVOY_EXT_AUTHZ_ADAPTER.md | 230 --- docs/ENVOY_GATEWAY_GUIDE.md | 817 --------- docs/INTEGRATION.md | 601 ------- docs/PROJECT_GUIDE.md | 1032 ------------ docs/README.md | 108 -- docs/deployment-troubleshooting.md | 385 ----- docs/email-kafka-consumer-test-guide.md | 266 --- docs/email-task-deployment-troubleshooting.md | 147 -- docs/gozero-redis-configuration.md | 1497 ----------------- docs/kubernetes-service-explanation.md | 743 -------- docs/loki-log-troubleshooting.md | 216 --- docs/loki-usage-guide.md | 174 -- docs/redis-sentinel-troubleshooting.md | 779 --------- docs/redis-services-guide.md | 1179 ------------- docs/redis-username-discovery.md | 1068 ------------ 24 files changed, 9878 deletions(-) delete mode 100644 deploy/docker/.env.example delete mode 100644 deploy/docker/README.md delete mode 100644 deploy/docker/docker-compose.yml delete mode 100644 deploy/example/OPERATOR-INSTALL-EXAMPLE.md delete mode 100644 deploy/example/kafka-strimzi-example.yaml delete mode 100644 deploy/example/mongodb-community-example.yaml delete mode 100644 deploy/example/mongodb-ha-replicaset-example.yaml delete mode 100644 deploy/example/mongodb-sharded-cluster-example.yaml delete mode 100644 deploy/example/pg-dx-configmap.yaml delete mode 100644 docs/ENVOY_EXT_AUTHZ_ADAPTER.md delete mode 100644 docs/ENVOY_GATEWAY_GUIDE.md delete mode 100644 docs/INTEGRATION.md delete mode 100644 docs/PROJECT_GUIDE.md delete mode 100644 docs/README.md delete mode 100644 docs/deployment-troubleshooting.md delete mode 100644 docs/email-kafka-consumer-test-guide.md delete mode 100644 docs/email-task-deployment-troubleshooting.md delete mode 100644 docs/gozero-redis-configuration.md delete mode 100644 docs/kubernetes-service-explanation.md delete mode 100644 docs/loki-log-troubleshooting.md delete mode 100644 docs/loki-usage-guide.md delete mode 100644 docs/redis-sentinel-troubleshooting.md delete mode 100644 docs/redis-services-guide.md delete mode 100644 docs/redis-username-discovery.md diff --git a/deploy/docker/.env.example b/deploy/docker/.env.example deleted file mode 100644 index 152e774..0000000 --- a/deploy/docker/.env.example +++ /dev/null @@ -1,4 +0,0 @@ -HARBOR_REGISTRY=harbor.example.com -HARBOR_PROJECT=juwan -IMAGE_NAME=st-1-example -IMAGE_TAG=latest diff --git a/deploy/docker/README.md b/deploy/docker/README.md deleted file mode 100644 index 7d8bbc2..0000000 --- a/deploy/docker/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# Docker 服务器部署方案(Gitea Actions) - -本方案替代 Jenkins: - -1. `push` 代码后由 Gitea Actions 构建镜像并推送 Harbor。 -2. 同一工作流通过 SSH 连接服务器,执行 `docker compose pull && docker compose up -d` 完成更新。 - -## 1) 服务器准备 - -在目标服务器安装: - -- Docker Engine -- Docker Compose 插件(`docker compose version` 可用) - -并确保部署用户有 docker 权限: - -```bash -sudo usermod -aG docker -``` - -## 2) Gitea 仓库 Secrets - -在仓库中配置以下 Secrets: - -- `HARBOR_REGISTRY`:例如 `harbor.example.com` -- `HARBOR_PROJECT`:例如 `juwan` -- `HARBOR_USERNAME` -- `HARBOR_PASSWORD` -- `DEPLOY_HOST`:服务器地址 -- `DEPLOY_PORT`:可选,默认 `22` -- `DEPLOY_USER`:服务器 SSH 用户 -- `DEPLOY_SSH_KEY`:私钥内容(PEM) -- `DEPLOY_PATH`:可选,默认 `/opt/st-1-example` - -## 3) 触发规则 - -- 构建推送:`main/master/dev/feature/**` -- 自动部署:仅 `main/master` - -如需改分支规则,编辑: - -- `.gitea/workflows/build-push-harbor.yml` - -## 4) 端口与服务 - -Compose 文件:`deploy/docker/docker-compose.yml` - -默认映射:`8888:8888`,服务名:`st-example`。 diff --git a/deploy/docker/docker-compose.yml b/deploy/docker/docker-compose.yml deleted file mode 100644 index 17888ef..0000000 --- a/deploy/docker/docker-compose.yml +++ /dev/null @@ -1,9 +0,0 @@ -services: - st-example: - image: ${HARBOR_REGISTRY}/${HARBOR_PROJECT}/${IMAGE_NAME}:${IMAGE_TAG:-latest} - container_name: st-example - restart: always - ports: - - "8888:8888" - environment: - TZ: Asia/Shanghai diff --git a/deploy/example/OPERATOR-INSTALL-EXAMPLE.md b/deploy/example/OPERATOR-INSTALL-EXAMPLE.md deleted file mode 100644 index e6d4ec7..0000000 --- a/deploy/example/OPERATOR-INSTALL-EXAMPLE.md +++ /dev/null @@ -1,184 +0,0 @@ -# Operator 安装与示例使用 - -本文档提供 Strimzi Operator 与 MongoDB Community Operator 的两种安装方式: - -- Helm 安装 -- kubectl 安装 - -> 示例资源文件位于 `deploy/example`,默认使用 `juwan` 命名空间。 -> 请先确保你的 Operator 能 watch 到 `juwan`,否则请改 namespace 或调整 Operator watch 范围。 - -## 1) Strimzi Operator(Kafka) - -### 1.1 使用 Helm 安装 - -```bash -kubectl create namespace kafka -helm repo add strimzi https://strimzi.io/charts/ -helm repo update -helm install strimzi-kafka-operator strimzi/strimzi-kafka-operator -n kafka -``` - -### 1.2 使用 kubectl 安装 - -```bash -kubectl create namespace kafka -kubectl apply -f https://strimzi.io/install/latest?namespace=kafka -n kafka -``` - -### 1.3 安装验证 - -```bash -kubectl get pods -n kafka -kubectl get crd | grep kafka.strimzi.io -``` - -### 1.4 应用 Kafka 示例 - -```bash -kubectl create namespace juwan -kubectl apply -f deploy/example/kafka-strimzi-example.yaml -kubectl get kafka,kafkatopic,kafkauser -n juwan -``` - -## 2) MongoDB Community Operator - -### 2.1 使用 Helm 安装 - -```bash -kubectl create namespace mongodb -helm repo add mongodb https://mongodb.github.io/helm-charts -helm repo update -helm install mongodb-kubernetes-operator mongodb/community-operator -n mongodb -``` - -### 2.2 使用 kubectl 安装 - -```bash -kubectl create namespace mongodb -kubectl apply -f https://raw.githubusercontent.com/mongodb/mongodb-kubernetes-operator/master/config/crd/bases/mongodbcommunity.mongodb.com_mongodbcommunity.yaml -kubectl apply -k https://github.com/mongodb/mongodb-kubernetes-operator/config/rbac/ -kubectl apply -k https://github.com/mongodb/mongodb-kubernetes-operator/config/manager/ -``` - -### 2.3 安装验证 - -```bash -kubectl get pods -n mongodb -kubectl get crd | grep mongodbcommunity.mongodb.com -``` - -### 2.4 应用 MongoDB 示例 - -```bash -kubectl create namespace juwan -kubectl apply -f deploy/example/mongodb-community-example.yaml -kubectl get mongodbcommunity -n juwan -``` - -## 3) MongoDB:哨兵集群与分片集群搭建 - -### 3.1 关于“哨兵集群”的说明 - -MongoDB 没有 Redis Sentinel 的独立哨兵组件。 -MongoDB 的高可用由 **Replica Set(副本集)** 原生完成(自动主从切换、故障恢复)。 - -因此在 MongoDB 场景里,“哨兵集群”通常对应为“副本集高可用集群”。 - -### 3.2 MongoDB“哨兵等价”方案:副本集高可用 - -本仓库提供了高可用副本集 YAML:`deploy/example/mongodb-ha-replicaset-example.yaml`。 - -```bash -kubectl create namespace juwan -kubectl apply -f deploy/example/mongodb-ha-replicaset-example.yaml -kubectl get mongodbcommunity -n juwan -``` - -查看副本集状态(任选一个 Pod 进入): - -```bash -kubectl get pods -n juwan -kubectl exec -it -n juwan -- mongosh --eval "rs.status()" -``` - -生产建议: - -- 成员数保持奇数(3/5/7) -- 使用持久化卷(PVC),不要用临时盘 -- 跨可用区调度(反亲和) -- 开启备份与监控 - -### 3.3 MongoDB 分片集群架构(Sharded Cluster) - -分片集群由三层组成: - -- Config Server ReplicaSet(保存分片元数据,建议 3 节点) -- Shard ReplicaSet(每个分片都是副本集,建议每分片 3 节点) -- Mongos(路由层,对业务暴露统一入口) - -### 3.4 分片集群搭建步骤(kubectl 方式) - -> 说明:MongoDB Community Operator 主要用于副本集管理。分片集群在社区实践中通常采用“手动编排(StatefulSet/Service)+ mongosh 初始化”。 - -本仓库提供了分片集群基础编排 YAML:`deploy/example/mongodb-sharded-cluster-example.yaml`。 - -```bash -kubectl create namespace juwan -kubectl apply -f deploy/example/mongodb-sharded-cluster-example.yaml -kubectl get pods,svc -n juwan -``` - -1) 部署 Config Server 副本集(3 节点) - -- 使用 StatefulSet + Headless Service 部署 `mongod --configsvr --replSet cfg-rs` - -1) 部署 Shard 副本集(例如 `shard1-rs`、`shard2-rs`,每个 3 节点) - -- 使用 StatefulSet + Headless Service 部署 `mongod --shardsvr --replSet ` - -1) 部署 Mongos 路由层 - -- Deployment 部署 `mongos --configdb cfg-rs/:27019,:27019,:27019` - -1) 初始化各副本集 - -```bash -# 初始化 Config Server RS -kubectl exec -it -n juwan -- mongosh --port 27019 --eval 'rs.initiate({_id:"cfg-rs",configsvr:true,members:[{_id:0,host:"cfg-0.cfg-svc.juwan.svc.cluster.local:27019"},{_id:1,host:"cfg-1.cfg-svc.juwan.svc.cluster.local:27019"},{_id:2,host:"cfg-2.cfg-svc.juwan.svc.cluster.local:27019"}]})' - -# 初始化 shard1 RS -kubectl exec -it -n juwan -- mongosh --port 27018 --eval 'rs.initiate({_id:"shard1-rs",members:[{_id:0,host:"shard1-0.shard1-svc.juwan.svc.cluster.local:27018"},{_id:1,host:"shard1-1.shard1-svc.juwan.svc.cluster.local:27018"},{_id:2,host:"shard1-2.shard1-svc.juwan.svc.cluster.local:27018"}]})' - -# 初始化 shard2 RS -kubectl exec -it -n juwan -- mongosh --port 27018 --eval 'rs.initiate({_id:"shard2-rs",members:[{_id:0,host:"shard2-0.shard2-svc.juwan.svc.cluster.local:27018"},{_id:1,host:"shard2-1.shard2-svc.juwan.svc.cluster.local:27018"},{_id:2,host:"shard2-2.shard2-svc.juwan.svc.cluster.local:27018"}]})' -``` - -1) 通过 Mongos 注册分片并启用分片 - -```bash -kubectl exec -it -n juwan -- mongosh --port 27017 --eval 'sh.addShard("shard1-rs/shard1-0.shard1-svc.juwan.svc.cluster.local:27018,shard1-1.shard1-svc.juwan.svc.cluster.local:27018,shard1-2.shard1-svc.juwan.svc.cluster.local:27018")' -kubectl exec -it -n juwan -- mongosh --port 27017 --eval 'sh.addShard("shard2-rs/shard2-0.shard2-svc.juwan.svc.cluster.local:27018,shard2-1.shard2-svc.juwan.svc.cluster.local:27018,shard2-2.shard2-svc.juwan.svc.cluster.local:27018")' -kubectl exec -it -n juwan -- mongosh --port 27017 --eval 'sh.enableSharding("appdb")' -kubectl exec -it -n juwan -- mongosh --port 27017 --eval 'sh.shardCollection("appdb.user_events", {"userId": "hashed"})' -``` - -1) 验证分片状态 - -```bash -kubectl exec -it -n juwan -- mongosh --port 27017 --eval 'sh.status()' -``` - -## 4) 卸载(可选) - -### Strimzi(Helm 安装场景) - -```bash -helm uninstall strimzi-kafka-operator -n kafka -``` - -### MongoDB Operator(Helm 安装场景) - -```bash -helm uninstall mongodb-kubernetes-operator -n mongodb -``` diff --git a/deploy/example/kafka-strimzi-example.yaml b/deploy/example/kafka-strimzi-example.yaml deleted file mode 100644 index ba35505..0000000 --- a/deploy/example/kafka-strimzi-example.yaml +++ /dev/null @@ -1,80 +0,0 @@ -# Strimzi Kafka 集群示例 -# 前提:已安装 Strimzi Operator,且 Operator 具备对本命名空间的 watch 权限。 -apiVersion: kafka.strimzi.io/v1beta2 -kind: Kafka -metadata: - name: juwan-kafka - namespace: juwan # 示例业务命名空间 -spec: - kafka: - version: 3.9.0 # Kafka Broker 版本 - replicas: 1 # 开发环境可用;生产环境建议 >= 3 - listeners: - - name: plain - port: 9092 - type: internal # 仅集群内部访问 - tls: false # 明文 listener,内网调试方便 - - name: tls - port: 9093 - type: internal - tls: true # TLS listener,推荐业务接入使用 - config: - # 单副本容错参数(仅适合开发环境) - offsets.topic.replication.factor: 1 - transaction.state.log.replication.factor: 1 - transaction.state.log.min.isr: 1 - default.replication.factor: 1 - min.insync.replicas: 1 - storage: - type: ephemeral # 临时存储,Pod 重建会丢数据;生产建议 persistent-claim - zookeeper: - replicas: 1 # 开发环境可用;生产环境建议 >= 3 - storage: - type: ephemeral - # 开启 Topic/User Operator,便于声明式管理 Topic 和账号 - entityOperator: - topicOperator: {} - userOperator: {} ---- -# 业务 Topic 示例 -apiVersion: kafka.strimzi.io/v1beta2 -kind: KafkaTopic -metadata: - name: user-events # 用户事件主题 - namespace: juwan - labels: - strimzi.io/cluster: juwan-kafka # 关联 Kafka 集群名 -spec: - partitions: 3 # 分区数,决定并行消费能力 - replicas: 1 # 副本数,开发环境示例 - config: - retention.ms: 604800000 # 7 天 - segment.bytes: 1073741824 # 1GiB ---- -# Kafka 用户与 ACL 示例 -apiVersion: kafka.strimzi.io/v1beta2 -kind: KafkaUser -metadata: - name: app-producer # 应用侧生产者账号 - namespace: juwan - labels: - strimzi.io/cluster: juwan-kafka -spec: - authentication: - type: tls # 生成 TLS 证书凭据 Secret - authorization: - type: simple - acls: - - resource: - type: topic - name: user-events - patternType: literal - operations: - - Read - - Write - - resource: - type: group - name: app-consumer-group - patternType: literal - operations: - - Read diff --git a/deploy/example/mongodb-community-example.yaml b/deploy/example/mongodb-community-example.yaml deleted file mode 100644 index eb7cab6..0000000 --- a/deploy/example/mongodb-community-example.yaml +++ /dev/null @@ -1,36 +0,0 @@ -# MongoDB 应用用户密码示例(请改为更安全的值,或对接外部 Secret 管理) -apiVersion: v1 -kind: Secret -metadata: - name: mongodb-app-user-password - namespace: juwan # 示例业务命名空间 -type: Opaque -stringData: - password: ChangeMe123456 # 示例明文,仅用于演示 ---- -# MongoDB Community Operator 自定义资源示例 -apiVersion: mongodbcommunity.mongodb.com/v1 -kind: MongoDBCommunity -metadata: - name: juwan-mongodb - namespace: juwan -spec: - members: 3 # 副本集成员数,生产建议保持奇数 - type: ReplicaSet - version: "7.0.12" # MongoDB 版本 - security: - authentication: - modes: - - SCRAM # 启用用户名密码认证 - users: - - name: app-user # 业务账号 - db: admin - passwordSecretRef: - name: mongodb-app-user-password # 引用上方 Secret - roles: - - name: readWrite - db: appdb # 对 appdb 库授予读写 - scramCredentialsSecretName: app-user-scram # Operator 生成的凭据 Secret - additionalMongodConfig: - # 示例:开启 WiredTiger 日志压缩 - storage.wiredTiger.engineConfig.journalCompressor: zlib diff --git a/deploy/example/mongodb-ha-replicaset-example.yaml b/deploy/example/mongodb-ha-replicaset-example.yaml deleted file mode 100644 index 444ccc5..0000000 --- a/deploy/example/mongodb-ha-replicaset-example.yaml +++ /dev/null @@ -1,46 +0,0 @@ -# MongoDB 高可用(副本集)示例 -# 说明:MongoDB 没有 Redis Sentinel 组件;副本集即其高可用机制。 -apiVersion: v1 -kind: Secret -metadata: - name: mongodb-ha-app-user-password - namespace: juwan -type: Opaque -stringData: - password: ChangeMe_ReallyStrongPassword ---- -apiVersion: mongodbcommunity.mongodb.com/v1 -kind: MongoDBCommunity -metadata: - name: juwan-mongodb-ha - namespace: juwan -spec: - members: 3 - type: ReplicaSet - version: "7.0.12" - # 生产建议开启持久化(具体 storageClassName 按集群调整) - statefulSet: - spec: - volumeClaimTemplates: - - metadata: - name: data-volume - spec: - accessModes: ["ReadWriteOnce"] - resources: - requests: - storage: 20Gi - security: - authentication: - modes: - - SCRAM - users: - - name: app-user - db: admin - passwordSecretRef: - name: mongodb-ha-app-user-password - roles: - - name: readWrite - db: appdb - scramCredentialsSecretName: app-user-scram - additionalMongodConfig: - storage.wiredTiger.engineConfig.journalCompressor: zlib diff --git a/deploy/example/mongodb-sharded-cluster-example.yaml b/deploy/example/mongodb-sharded-cluster-example.yaml deleted file mode 100644 index 85d586a..0000000 --- a/deploy/example/mongodb-sharded-cluster-example.yaml +++ /dev/null @@ -1,218 +0,0 @@ -# MongoDB 分片集群最小示例(ConfigRS + 2 个 ShardRS + Mongos) -# 使用方式: -# 1) 先 apply 本文件 -# 2) 按文档执行 rs.initiate / sh.addShard / sh.enableSharding -# 注意:本示例侧重结构演示,生产环境请补齐资源限制、反亲和、PDB、备份与监控。 ---- -apiVersion: v1 -kind: Service -metadata: - name: cfg-svc - namespace: juwan -spec: - clusterIP: None - selector: - app: mongo-cfg - ports: - - name: mongo - port: 27019 - targetPort: 27019 ---- -apiVersion: apps/v1 -kind: StatefulSet -metadata: - name: cfg - namespace: juwan -spec: - serviceName: cfg-svc - replicas: 3 - selector: - matchLabels: - app: mongo-cfg - template: - metadata: - labels: - app: mongo-cfg - spec: - containers: - - name: mongod - image: mongo:7.0 - args: - [ - "--configsvr", - "--replSet", - "cfg-rs", - "--port", - "27019", - "--bind_ip_all", - ] - ports: - - containerPort: 27019 - name: mongo - volumeMounts: - - name: data - mountPath: /data/db - volumeClaimTemplates: - - metadata: - name: data - spec: - accessModes: ["ReadWriteOnce"] - resources: - requests: - storage: 20Gi ---- -apiVersion: v1 -kind: Service -metadata: - name: shard1-svc - namespace: juwan -spec: - clusterIP: None - selector: - app: mongo-shard1 - ports: - - name: mongo - port: 27018 - targetPort: 27018 ---- -apiVersion: apps/v1 -kind: StatefulSet -metadata: - name: shard1 - namespace: juwan -spec: - serviceName: shard1-svc - replicas: 3 - selector: - matchLabels: - app: mongo-shard1 - template: - metadata: - labels: - app: mongo-shard1 - spec: - containers: - - name: mongod - image: mongo:7.0 - args: - [ - "--shardsvr", - "--replSet", - "shard1-rs", - "--port", - "27018", - "--bind_ip_all", - ] - ports: - - containerPort: 27018 - name: mongo - volumeMounts: - - name: data - mountPath: /data/db - volumeClaimTemplates: - - metadata: - name: data - spec: - accessModes: ["ReadWriteOnce"] - resources: - requests: - storage: 20Gi ---- -apiVersion: v1 -kind: Service -metadata: - name: shard2-svc - namespace: juwan -spec: - clusterIP: None - selector: - app: mongo-shard2 - ports: - - name: mongo - port: 27018 - targetPort: 27018 ---- -apiVersion: apps/v1 -kind: StatefulSet -metadata: - name: shard2 - namespace: juwan -spec: - serviceName: shard2-svc - replicas: 3 - selector: - matchLabels: - app: mongo-shard2 - template: - metadata: - labels: - app: mongo-shard2 - spec: - containers: - - name: mongod - image: mongo:7.0 - args: - [ - "--shardsvr", - "--replSet", - "shard2-rs", - "--port", - "27018", - "--bind_ip_all", - ] - ports: - - containerPort: 27018 - name: mongo - volumeMounts: - - name: data - mountPath: /data/db - volumeClaimTemplates: - - metadata: - name: data - spec: - accessModes: ["ReadWriteOnce"] - resources: - requests: - storage: 20Gi ---- -apiVersion: v1 -kind: Service -metadata: - name: mongos - namespace: juwan -spec: - selector: - app: mongos - ports: - - name: mongo - port: 27017 - targetPort: 27017 ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: mongos - namespace: juwan -spec: - replicas: 2 - selector: - matchLabels: - app: mongos - template: - metadata: - labels: - app: mongos - spec: - containers: - - name: mongos - image: mongo:7.0 - args: - - "mongos" - - "--configdb" - - "cfg-rs/cfg-0.cfg-svc.juwan.svc.cluster.local:27019,cfg-1.cfg-svc.juwan.svc.cluster.local:27019,cfg-2.cfg-svc.juwan.svc.cluster.local:27019" - - "--bind_ip_all" - - "--port" - - "27017" - ports: - - containerPort: 27017 - name: mongo diff --git a/deploy/example/pg-dx-configmap.yaml b/deploy/example/pg-dx-configmap.yaml deleted file mode 100644 index f692663..0000000 --- a/deploy/example/pg-dx-configmap.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: db-dx-init-script - namespace: juwan - labels: - app: db-dx-init-script -data: - init-extensions-sql: | - create extension if not exists "uuid-ossp"; - create extension if not exists "pg_trgm"; diff --git a/docs/ENVOY_EXT_AUTHZ_ADAPTER.md b/docs/ENVOY_EXT_AUTHZ_ADAPTER.md deleted file mode 100644 index 6612287..0000000 --- a/docs/ENVOY_EXT_AUTHZ_ADAPTER.md +++ /dev/null @@ -1,230 +0,0 @@ -# Envoy ext_authz 适配 user-rpc.ValidateToken(最小实现) - -## 目标 - -- Envoy 不直接调用业务 proto 方法。 -- 新增一个内部服务 `authz-adapter`,实现 Envoy 标准 gRPC 鉴权接口。 -- `authz-adapter` 再调用现有 `user-rpc.ValidateToken` 完成会话态二次校验。 - ---- - -## 1) 最小接口定义(Envoy 标准) - -`authz-adapter` 需要实现的是 Envoy 官方服务: - -- gRPC Service: `envoy.service.auth.v3.Authorization` -- RPC Method: `Check(CheckRequest) returns (CheckResponse)` - -Go 里通常使用包: - -- `github.com/envoyproxy/go-control-plane/envoy/service/auth/v3` -- `github.com/envoyproxy/go-control-plane/envoy/type/v3` - ---- - -## 2) 最小 Go 适配器骨架 - -```go -package main - -import ( - "context" - "net" - "strconv" - "strings" - - core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" - authv3 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" - typev3 "github.com/envoyproxy/go-control-plane/envoy/type/v3" - "google.golang.org/grpc" - - userpb "juwan-backend/app/users/rpc/pb" -) - -type server struct { - authv3.UnimplementedAuthorizationServer - userRpc userpb.UsercenterClient -} - -func (s *server) Check(ctx context.Context, req *authv3.CheckRequest) (*authv3.CheckResponse, error) { - attrs := req.GetAttributes() - httpReq := attrs.GetRequest().GetHttp() - if httpReq == nil { - return deny(403, "missing http attributes"), nil - } - - path := httpReq.GetPath() - - // 放行公共接口(探针、登录/注册、发送验证码) - if path == "/healthz" || - path == "/api/v1/auth/login" || - path == "/api/v1/auth/register" || - path == "/api/v1/auth/forgot-password" || - path == "/api/v1/auth/reset-password" || - path == "/api/v1/auth/forgot-password/send" || - path == "/api/v1/email/verification-code/send" { - return allow(nil), nil - } - - token := extractCookie(httpReq.GetHeaders(), "JToken") - if token == "" { - return deny(401, "missing token cookie"), nil - } - - userIDHeader := getHeader(httpReq.GetHeaders(), "x-auth-user-id") - if userIDHeader == "" { - return deny(401, "missing x-auth-user-id header"), nil - } - - userID, err := strconv.ParseInt(userIDHeader, 10, 64) - if err != nil { - return deny(401, "invalid x-auth-user-id"), nil - } - - // 调用现有业务 RPC 做会话态二次校验 - vt, err := s.userRpc.ValidateToken(ctx, &userpb.ValidateTokenReq{ - Token: token, - UserId: userID, - }) - if err != nil || vt == nil || !vt.Valid { - return deny(401, "invalid token"), nil - } - - // 透传给后端 API - headers := []*core.HeaderValueOption{ - { - Header: &core.HeaderValue{Key: "x-auth-user-id", Value: strconv.FormatInt(vt.UserId, 10)}, - }, - { - Header: &core.HeaderValue{Key: "x-auth-role-type", Value: vt.RoleType}, - }, - } - - return allow(headers), nil -} - -func allow(headers []*core.HeaderValueOption) *authv3.CheckResponse { - return &authv3.CheckResponse{ - Status: &typev3.Status{Code: int32(typev3.Code_OK)}, - HttpResponse: &authv3.CheckResponse_OkResponse{ - OkResponse: &authv3.OkHttpResponse{Headers: headers}, - }, - } -} - -func deny(code int32, msg string) *authv3.CheckResponse { - return &authv3.CheckResponse{ - Status: &typev3.Status{Code: int32(typev3.Code_PERMISSION_DENIED)}, - HttpResponse: &authv3.CheckResponse_DeniedResponse{ - DeniedResponse: &authv3.DeniedHttpResponse{ - Status: &typev3.HttpStatus{Code: typev3.StatusCode(code)}, - Body: "{\"code\":" + strconv.Itoa(int(code)) + ",\"message\":\"" + msg + "\"}", - Headers: []*core.HeaderValueOption{ - {Header: &core.HeaderValue{Key: "content-type", Value: "application/json"}}, - }, - }, - }, - } -} - -func extractCookie(headers map[string]string, name string) string { - c := headers["cookie"] - parts := strings.Split(c, ";") - for _, p := range parts { - kv := strings.SplitN(strings.TrimSpace(p), "=", 2) - if len(kv) == 2 && kv[0] == name { - return kv[1] - } - } - return "" -} - -func getHeader(headers map[string]string, name string) string { - for k, v := range headers { - if strings.EqualFold(k, name) { - return v - } - } - return "" -} - -func main() { - lis, _ := net.Listen("tcp", ":9002") - grpcServer := grpc.NewServer() - - // TODO: 创建 user-rpc client 并注入 - // userRpcClient := ... - authv3.RegisterAuthorizationServer(grpcServer, &server{userRpc: nil}) - - _ = grpcServer.Serve(lis) -} -``` - ---- - -## 3) Envoy 最小配置片段(插入现有 `http_filters`) - -在 `envoy.filters.http.router` 之前加入: - -```yaml -- name: envoy.filters.http.ext_authz - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz - transport_api_version: V3 - failure_mode_allow: false - with_request_body: - max_request_bytes: 8192 - allow_partial_message: true - grpc_service: - envoy_grpc: - cluster_name: authz_adapter_cluster - timeout: 0.5s -``` - -并在 `clusters` 中加入: - -```yaml -- name: authz_adapter_cluster - connect_timeout: 0.5s - type: STRICT_DNS - lb_policy: ROUND_ROBIN - load_assignment: - cluster_name: authz_adapter_cluster - endpoints: - - lb_endpoints: - - endpoint: - address: - socket_address: - address: authz-adapter-svc.juwan.svc.cluster.local - port_value: 9002 - http2_protocol_options: {} -``` - -> gRPC 上游必须启用 `http2_protocol_options: {}`。 - ---- - -## 4) 落地清单 - -1. 新建 `authz-adapter` 服务(Deployment + Service)。 -2. Adapter 内部连 `user-rpc-svc:9001`,调用 `ValidateToken`。 -3. Envoy 加 `ext_authz` filter + `authz_adapter_cluster`。 -4. 明确失败语义: - - 无 token -> 401 - - token 无效/过期 -> 401 - - 权限不足 -> 403 -5. `jwt_authn` 先注入 `x-auth-user-id`、`x-auth-is-admin`,再由 adapter 透传 `x-auth-user-id`、`x-auth-role-type`。 -6. 灰度建议:先仅对 `/api/v1/auth` 和 `/api/v1/email` 验证链路,再逐步扩展到其它业务路径。 - -> 实践建议:若保留 K8s `readiness/liveness` 探针使用 `/healthz`,请确保该路径在 `ext_authz` 上也放行,否则会出现探针 403 导致 Pod 重启。 - ---- - -## 5) 与当前 `jwt_authn` 的关系 - -- 可以并存: - - 先 `jwt_authn` 快速验签并注入 claim header - - 再 `ext_authz` 做 Redis 会话态、黑名单、细粒度权限 -- 也可以只保留 `ext_authz`(由 adapter 内完成全部逻辑)。 - -推荐:**先并存**,稳定后再决定是否简化。 diff --git a/docs/ENVOY_GATEWAY_GUIDE.md b/docs/ENVOY_GATEWAY_GUIDE.md deleted file mode 100644 index 91aab5c..0000000 --- a/docs/ENVOY_GATEWAY_GUIDE.md +++ /dev/null @@ -1,817 +0,0 @@ -# Envoy Gateway 配置指南(带 JWT 认证) - -## 📋 目录 - -1. [快速开始](#快速开始) -2. [添加新服务](#添加新服务) -3. [JWT 认证配置](#jwt-认证配置) -4. [分级访问控制](#分级访问控制) -5. [故障排查](#故障排查) -6. [当前实现说明(与仓库配置对齐)](#当前实现说明与仓库配置对齐) -7. [ext_authz 适配方案](#ext_authz-适配方案) -8. [前端接入示例(邮箱验证码)](#前端接入示例邮箱验证码) - ---- - -## 当前实现说明(与仓库配置对齐) - -> 本节对应当前实际配置文件:`deploy/k8s/envoy/envoy.yaml`。 - -### 0) 当前公共路由(不需要登录) - -当前网关对以下路径做了“公共放行”: - -- `/healthz`(直返 200,用于探针) -- `POST /api/v1/auth/login` -- `POST /api/v1/auth/register` -- `POST /api/v1/auth/forgot-password` -- `POST /api/v1/auth/reset-password` -- `POST /api/v1/auth/forgot-password/send` -- `POST /api/v1/email/verification-code/send` - -此外,当前配置还对一批只读接口做了 JWT 白名单豁免,例如 `GET /api/v1/games*`、`GET /api/v1/players*`、`GET /api/v1/services*`、`GET /api/v1/posts*`、`GET /api/v1/shops*` 以及部分 `GET /api/v1/users/*` 子路径。精确范围以 `deploy/k8s/envoy/envoy.yaml` 中的 `jwt_authn.rules` 为准。 - -实现方式: - -- 在 `jwt_authn.rules` 中将上述路径加入白名单(不要求 JWT) -- 在路由层对上述路径关闭 `ext_authz`(避免公共接口和探针被二次鉴权拦截) - -### 1) 用户认证后,`UserId` 放在哪里? - -当前 Envoy 使用 `envoy.filters.http.jwt_authn` 做 JWT 校验,校验通过后通过 `claim_to_headers` 将 claim 注入到转发请求头: - -- `UserId` -> `x-auth-user-id` -- `IsAdmin` -> `x-auth-is-admin` - -也就是说,后端 API(如 user-api/email-api)拿到的是 HTTP Header,不是 Envoy 动态元数据。 - -### 2) 当前配置是否实现了“换票”(token renew)? - -没有。 - -当前 Envoy 配置仅负责: - -- 从 Cookie `JToken` 提取 JWT -- 用 HS256 + `issuer: juwan-user-rpc` 验签与过期检查 - -当 token 过期时,`jwt_authn` 会直接拒绝请求,不会调用 user-rpc 的 `JwtManager.Renew`。 - -### 3) Envoy 能否直接调用 `user-rpc.ValidateToken`? - -结论:不能“直接”用现有 `ValidateToken` protobuf 接口接入 Envoy 认证链。 - -原因: - -- Envoy 内置认证过滤器(如 `jwt_authn`、`ext_authz`)要求固定协议。 -- `ext_authz` 的 gRPC 需要实现 Envoy 标准服务 `envoy.service.auth.v3.Authorization`,不是业务自定义的 `pb.usercenter/ValidateToken`。 - -当前仓库已经采用 `authz-adapter` 方案:Envoy 先做 `jwt_authn`,再通过 `ext_authz` 调用 `authz-adapter`,由它内部调用 `user-rpc.ValidateToken` 做会话态二次校验。 - -当前链路包含: - -- `authz-adapter` 服务(实现 Envoy `CheckRequest/CheckResponse`) -- Envoy `ext_authz` filter 与 `authz_adapter_cluster` -- `jwt_authn` 注入 `x-auth-user-id`、`x-auth-is-admin` -- `authz-adapter` 透传 `x-auth-user-id`、`x-auth-role-type` -- 失败码与错误体规范(401/403) - -### 4) 与你现有 `ValidateTokenLogic` 的一致性提醒 - -当前 `app/users/rpc/internal/logic/validateTokenLogic.go` 中: - -- `JwtManager.Valid()` 负责验证 JWT 字符串本身 -- 当前逻辑还会校验 JWT payload 中的 `UserId` 与请求传入的 `userId` 一致,再查询数据库回填 `RoleType` - -这意味着当前 `ext_authz -> user-rpc.ValidateToken` 链已经具备 token 有效性和 userId 一致性校验。 - ---- - -## ext_authz 适配方案 - -如果你希望 Envoy 在鉴权阶段调用 `user-rpc.ValidateToken`,请看完整落地文档: - -- [ENVOY_EXT_AUTHZ_ADAPTER.md](ENVOY_EXT_AUTHZ_ADAPTER.md) - -该文档包含: - -- Envoy 标准 `Authorization.Check` 最小实现骨架 -- 调用 `user-rpc.ValidateToken` 的适配逻辑示例 -- 可直接嵌入现有网关的 `ext_authz` filter + cluster 配置片段 - ---- - -## 前端接入示例(邮箱验证码) - -以下示例基于当前 K8s 网关配置: - -- 登录:`POST /api/v1/auth/login`(公共放行) -- 发送验证码:`POST /api/v1/email/verification-code/send`(公共放行,无需登录) -- CSRF 头:`xsrf-token`(请求头) -- CSRF Cookie:`__Host-XSRF-TOKEN`(可读) -- CSRF Guard Cookie:`__Host-XSRF-GUARD`(`HttpOnly`) -- JWT Cookie:`JToken`(`HttpOnly`,前端不可读,但会随请求自动携带) - -> 注意:当前 Envoy 给 CSRF Cookie 设置了 `Secure` + `SameSite=Strict`。前端必须走 **HTTPS 且同站点** 才能稳定工作。 - -### 接入流程 - -1. 先发一个安全方法请求(GET),让 Envoy 下发 XSRF 双 Cookie。 -2. 注册场景可直接调用发送验证码接口,仅需携带 `xsrf-token`。 -3. 登录场景可先调用登录接口拿 `JToken`,后续访问受保护接口时自动携带。 - -### 前端示例(TypeScript + fetch) - -```ts -const API_BASE = "https://your-gateway-domain"; - -function getCookie(name: string): string { - const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - const match = document.cookie.match(new RegExp(`(?:^|; )${escaped}=([^;]*)`)); - return match ? decodeURIComponent(match[1]) : ""; -} - -async function primeXsrfCookies() { - await fetch(`${API_BASE}/healthz`, { - method: "GET", - credentials: "include", - }); -} - -async function login(username: string, password: string) { - const xsrfToken = getCookie("__Host-XSRF-TOKEN"); - const res = await fetch(`${API_BASE}/api/v1/auth/login`, { - method: "POST", - credentials: "include", - headers: { - "Content-Type": "application/json", - "xsrf-token": xsrfToken, - }, - body: JSON.stringify({ username, password }), - }); - - if (!res.ok) { - const text = await res.text(); - throw new Error(`login failed: ${res.status} ${text}`); - } - - return res.json(); -} - -type SendCodeReq = { - email: string; - scene: "register" | "login" | "reset_password" | "bind_email"; -}; - -async function sendVerificationCode(req: SendCodeReq) { - const xsrfToken = getCookie("__Host-XSRF-TOKEN"); - const res = await fetch(`${API_BASE}/api/v1/email/verification-code/send`, { - method: "POST", - credentials: "include", - headers: { - "Content-Type": "application/json", - "xsrf-token": xsrfToken, - }, - body: JSON.stringify(req), - }); - - if (!res.ok) { - const text = await res.text(); - throw new Error(`send code failed: ${res.status} ${text}`); - } - - return res.json() as Promise<{ - requestId: string; - expireInSec: number; - message: string; - }>; -} - -// 页面初始化时建议执行一次 -await primeXsrfCookies(); - -// 注册场景:无需登录即可发送验证码 -const data = await sendVerificationCode({ - email: "alice@example.com", - scene: "register", -}); - -console.log("code request:", data); - -// 如需调用受保护接口,再执行登录 -await login("alice", "P@ssw0rd"); -``` - -### 常见前端坑位 - -- 必须加 `credentials: "include"`,否则 Cookie 不会带上。 -- `JToken` 是 `HttpOnly`,前端读不到,这是正常设计。 -- 如果你前后端跨站点,`SameSite=Strict` 会导致 Cookie 不发送;需要改网关 Cookie 策略。 -- 本地 `http://localhost` 下,`Secure` Cookie 不会生效;建议本地也走 HTTPS(例如反向代理或证书)。 - ---- - -## 快速开始 - -### 前置条件 - -- K8s 集群正在运行(已验证 ✅) -- Envoy Gateway Pod 处于 Running 状态 -- 所有后端服务已部署 - -### 当前网关状态 - -```bash -# 查看 Envoy Pod -kubectl get pods -n juwan -l app=envoy-gateway - -# 查看网关 Service -kubectl get svc -n juwan envoy-gateway - -# 查看 ConfigMap -kubectl get cm -n juwan envoy-config -``` - -### 访问网关 - -```bash -# 通过 kubectl 端口转发(本地测试) -kubectl port-forward -n juwan svc/envoy-gateway 8080:80 & - -# 测试 -curl http://localhost:8080/healthz -``` - ---- - -## 添加新服务 - -### 场景:添加 Product 服务 - -#### 1. 创建服务的 K8s 部署清单 - -编辑或创建 `deploy/k8s/service/product/product-api.yaml`: - -```yaml -apiVersion: v1 -kind: Namespace -metadata: - name: juwan ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: product-api-config - namespace: juwan -data: - product-api.yaml: | - Name: product-api - Host: 0.0.0.0 - Port: 8890 - Database: - DataSource: postgres://user:pass@pg-dx:5432/juwan ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: product-api - namespace: juwan - labels: - app: product-api -spec: - replicas: 2 - selector: - matchLabels: - app: product-api - template: - metadata: - labels: - app: product-api - spec: - containers: - - name: api - image: your-registry/product-api:latest - imagePullPolicy: IfNotPresent - ports: - - containerPort: 8890 - name: http - volumeMounts: - - name: config - mountPath: /etc/product-api - env: - - name: TZ - value: "Asia/Shanghai" - resources: - requests: - cpu: 100m - memory: 128Mi - limits: - cpu: 500m - memory: 512Mi - livenessProbe: - httpGet: - path: /health - port: 8890 - initialDelaySeconds: 10 - periodSeconds: 10 - volumes: - - name: config - configMap: - name: product-api-config ---- -apiVersion: v1 -kind: Service -metadata: - name: product-api-svc - namespace: juwan -spec: - selector: - app: product-api - ports: - - port: 8890 - targetPort: 8890 - name: http - type: ClusterIP -``` - -#### 2. 在 Envoy 网关中添加路由 - -编辑 `deploy/k8s/envoy-gateway.yaml`,在 `route_config` 的 `routes` 部分添加: - -```yaml -# ... 在现有路由下方添加: -- match: - prefix: /api/products - route: - cluster: product_api_cluster - timeout: 30s -``` - -#### 3. 在 Envoy 网关中添加上游集群 - -编辑 `deploy/k8s/envoy-gateway.yaml`,在 `clusters` 部分添加: - -```yaml -- name: product_api_cluster - connect_timeout: 5s - type: STRICT_DNS - dns_lookup_family: V4_ONLY - lb_policy: ROUND_ROBIN - load_assignment: - cluster_name: product_api_cluster - endpoints: - - lb_endpoints: - - endpoint: - address: - socket_address: - address: product-api-svc.juwan.svc.cluster.local - port_value: 8890 - health_checks: - - timeout: 3s - interval: 10s - unhealthy_threshold: 2 - healthy_threshold: 2 - http_health_check: - path: /health -``` - -#### 4. 部署到集群 - -```bash -# 部署 Product API -kubectl apply -f deploy/k8s/service/product/product-api.yaml - -# 更新 Envoy 配置 -kubectl apply -f deploy/k8s/envoy-gateway.yaml - -# 重启 Envoy Pod 以加载新配置 -kubectl delete pods -n juwan -l app=envoy-gateway - -# 验证 -kubectl get pods -n juwan - -# 测试新接口 -curl http://localhost:8080/api/products -``` - ---- - -## JWT 认证配置 - -### 1. 生成 JWT 密钥并存储 - -```bash -# 执行设置脚本 -bash deploy/envoy/setup-jwt-auth.sh - -# 或手动执行 -JWT_SECRET=$(openssl rand -hex 32) -echo "保存这个密钥: $JWT_SECRET" - -# 创建 K8s Secret -kubectl create secret generic jwt-secret \ - --from-literal=key=$JWT_SECRET \ - -n juwan -``` - -### 2. 配置 Envoy JWT 认证 - -编辑 `deploy/k8s/envoy-gateway.yaml`,更新 `http_filters` 部分: - -```yaml -http_filters: - # JWT 认证过滤器(必须在 router 之前) - - name: envoy.filters.http.jwt_authn - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication - - providers: - default: - issuer: "juwan" - audiences: "api" - # 使用 ConfigMap 中的 JWKS(已通过 volumeMount 挂载) - local_jwks: - filename: /etc/envoy/jwks.json - - rules: - # 规则1: 登录端点不需要认证 - - match: - prefix: /api/users/login - allow_missing_or_failed: true - - # 规则2: 注册端点不需要认证 - - match: - prefix: /api/users/register - allow_missing_or_failed: true - - # 规则3: 获取公开商品列表不需要认证 - - match: - prefix: /api/products - case_sensitive: false - methods: ["GET"] # 仅 GET 不需要认证 - allow_missing_or_failed: true - - # 规则4: 其他所有路由需要认证 - - match: - prefix: "/" - requires: - provider_name: "default" - - # 路由过滤器(在 JWT 认证之后) - - name: envoy.filters.http.router - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router -``` - -### 3. 在 Envoy Deployment 中挂载 JWKS - -编辑 `deploy/k8s/envoy-gateway.yaml` 的 Deployment 部分: - -```yaml -spec: - # ... 其他配置 ... - template: - spec: - containers: - - name: envoy - # ... 其他配置 ... - volumeMounts: - - name: envoy-config - mountPath: /etc/envoy - - name: jwks-config # ← 新增 - mountPath: /etc/envoy - - volumes: - - name: envoy-config - configMap: - name: envoy-config - - name: jwks-config # ← 新增 - configMap: - name: jwks-config - items: - - key: jwks.json - path: jwks.json -``` - -### 4. 在 API 服务中生成 JWT Token - -在 User API 的 login 端点(`app/users/api/internal/logic/user/loginlogic.go`): - -```go -package user - -import ( - "context" - "time" - "github.com/golang-jwt/jwt/v4" - "app/users/api/internal/svc" - "app/users/api/internal/types" -) - -type LoginLogic struct { - ctx context.Context - svcCtx *svc.ServiceContext -} - -func (l *LoginLogic) Login(req *types.LoginReq) (*types.LoginResp, error) { - // TODO: 验证用户名和密码 - - // 从配置中获取 JWT 密钥 - jwtSecret := l.svcCtx.Config.JwtSecret - if jwtSecret == "" { - jwtSecret = "default-secret" // 开发环境默认值 - } - - // 生成 JWT Token - claims := jwt.MapClaims{ - "userId": 1, // 实际应从数据库获取 - "username": req.Username, - "exp": time.Now().Add(24 * time.Hour).Unix(), - "iat": time.Now().Unix(), - } - - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - tokenString, err := token.SignedString([]byte(jwtSecret)) - if err != nil { - return nil, err - } - - return &types.LoginResp{ - Token: tokenString, - Expires: time.Now().Add(24 * time.Hour).Unix(), - UserId: 1, - Username: req.Username, - Email: "user@example.com", - }, nil -} -``` - -### 5. 在 API 配置中设置 JWT 密钥 - -编辑 `app/users/api/etc/user-api.yaml`: - -```yaml -Name: user-api -Host: 0.0.0.0 -Port: 8888 - -JwtSecret: "${JWT_SECRET}" # 环境变量 - -Database: - DataSource: postgres://... - -UserRpc: - Endpoints: - - user-rpc-svc.juwan.svc.cluster.local:50051 -``` - -编辑 `app/users/api/internal/config/config.go`: - -```go -package config - -import "github.com/zeromicro/go-zero/rest" - -type Config struct { - rest.RestConf - JwtSecret string `json:"jwtSecret"` -} -``` - -在 K8s Deployment 中设置环境变量: - -```yaml -spec: - template: - spec: - containers: - - name: api - env: - - name: JWT_SECRET - valueFrom: - secretKeyRef: - name: jwt-secret - key: key -``` - ---- - -## 分级访问控制 - -### 场景1: 获取用户信息(有权限区分) - -如果用户查看自己的信息 → 返回完整数据 -如果用户查看他人信息 → 返回部分数据 - -#### 在 RPC 服务中实现 - -编辑 `app/users/rpc/internal/logic/getUsersByIdLogic.go`: - -```go -func (l *GetUsersByIdLogic) GetUsersById(ctx context.Context, in *pb.GetUsersByIdReq) (*pb.GetUsersByIdResp, error) { - // 获取请求者的 userId(由 API 层通过 context 传递) - requesterID, ok := ctx.Value("userId").(int64) - if !ok { - requesterID = 0 // 未认证用户 - } - - targetID := in.Id - - // 查询数据库 - user := l.svcCtx.UserModel.FindOne(ctx, targetID) - if user == nil { - return nil, status.Error(codes.NotFound, "user not found") - } - - resp := &pb.GetUsersByIdResp{ - Users: &pb.Users{ - UserId: user.UserId, - Username: user.Username, - CreatedAt: user.CreatedAt, - }, - } - - // 权限检查:自己可以看全部,别人只能看部分 - if requesterID == targetID { - resp.Users.Email = user.Email // ✅ 自己可见 - resp.Users.Phone = user.Phone // ✅ 自己可见 - resp.Users.Passwd = "" // ❌ 密码永远不返回 - } - // else: 只返回基本信息(username, userId) - - return resp, nil -} -``` - -#### 在 API 层调用时传递 userId - -编辑 `app/users/api/internal/logic/user/getUserInfoLogic.go`: - -```go -func (l *GetUserInfoLogic) GetUserInfo(req *types.GetUserInfoReq) (*types.UserInfo, error) { - // 从 context 获取当前认证用户 - currentUserID, ok := l.ctx.Value("userId").(int64) - if !ok { - // 未认证 → 只能查看公开信息 - currentUserID = 0 - } - - // 调用 RPC,传递 userId - ctx := context.WithValue(l.ctx, "userId", currentUserID) - rpcResp, err := l.svcCtx.UserRpc.GetUsersById(ctx, &pb.GetUsersByIdReq{ - Id: req.UserId, - }) - if err != nil { - return nil, err - } - - return &types.UserInfo{ - UserId: rpcResp.Users.UserId, - Username: rpcResp.Users.Username, - Email: rpcResp.Users.Email, - Phone: rpcResp.Users.Phone, - CreateAt: rpcResp.Users.CreatedAt, - }, nil -} -``` - -### 场景2: 修改用户信息(只能修改自己) - -```go -func (l *UpdateUserInfoLogic) UpdateUserInfo(req *types.UpdateUserInfoReq) (*types.UpdateUserInfoResp, error) { - // 获取当前认证用户 - currentUserID, ok := l.ctx.Value("userId").(int64) - if !ok { - return nil, errors.New("unauthorized") - } - - // 权限检查:只能修改自己的信息 - if currentUserID != req.UserId { - return nil, errors.New("forbidden: can only update your own info") - } - - // 更新用户信息 - // ... - - return &types.UpdateUserInfoResp{ - Message: "更新成功", - }, nil -} -``` - ---- - -## 故障排查 - -### 问题1: Envoy Pod 启动失败 - -```bash -# 查看日志 -kubectl logs -n juwan -l app=envoy-gateway --tail=100 - -# 常见错误及解决 -# Error: "no such field" -# → YAML 字段名拼写错误或与 Envoy 版本不兼容 -# → 检查 Envoy 版本并查看官方文档 - -# Error: "unknown cluster" -# → envoy-gateway.yaml 中缺少 cluster 定义 -# → 确保添加了所有需要的 cluster 部分 - -# Error: "unknown extension type" -# → 使用了 Envoy 不支持的扩展类型 -# → 检查 "@type" 字段是否正确 -``` - -### 问题2: JWT 认证失败 - -```bash -# 验证 JWKS ConfigMap 是否存在 -kubectl get cm -n juwan jwks-config - -# 查看 JWKS 内容 -kubectl get cm jwks-config -n juwan -o jsonpath='{.data.jwks\.json}' - -# 验证 Envoy 能否读取 JWKS -kubectl exec -it {envoy-pod-name} -n juwan -- ls -la /etc/envoy/ - -# 测试没有 Token 的请求(应返回 401) -curl -v http://localhost/api/users/1 - -# 测试有效 Token 的请求 -TOKEN="your-jwt-token" -curl -H "Authorization: Bearer $TOKEN" http://localhost/api/users/1 -``` - -### 问题3: 后端服务无法访问 - -```bash -# 查看 Service 是否存在 -kubectl get svc -n juwan - -# 测试 DNS 解析 -kubectl exec -it {pod-name} -n juwan -- \ - nslookup product-api-svc.juwan.svc.cluster.local - -# 查看 Pod 是否正确运行 -kubectl get pods -n juwan -l app=product-api - -# 查看后端服务日志 -kubectl logs -n juwan -l app=product-api --tail=50 - -# Envoy 检查上游集群状态 -kubectl exec -it {envoy-pod-name} -n juwan -- \ - curl localhost:9901/clusters | grep -A5 product_api_cluster -``` - -### 问题4: 跨域请求失败 - -如果前端遇到 CORS 问题: - -```yaml -# 在 Envoy 配置中添加 CORS 过滤器 -http_filters: - - name: envoy.filters.http.cors - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors - - # JWT 认证过滤器(在 CORS 之后) - - name: envoy.filters.http.jwt_authn - # ... -``` - ---- - -## 配置更新流程 - -每次修改 `envoy-gateway.yaml` 后的完整更新步骤: - -```bash -# 1. 验证 YAML 语法 -kubectl apply -f deploy/k8s/envoy-gateway.yaml --dry-run=client - -# 2. 应用配置 -kubectl apply -f deploy/k8s/envoy-gateway.yaml - -# 3. 监控 Pod 重启(应该自动重新加载) -kubectl get pods -n juwan -l app=envoy-gateway -w - -# 4. 查看最新日志确认无错误 -kubectl logs -n juwan -l app=envoy-gateway --tail=50 - -# 5. 测试新配置 -curl http://localhost/api/your-new-endpoint -``` - ---- - -## 总结 - -| 任务 | 文件 | 说明 | -|-----|------|------| -| 添加新 API | `desc/api/`, `app/*/api/` | 定义接口并实现业务逻辑 | -| 添加新 RPC | `desc/rpc/`, `app/*/rpc/` | 内部服务通信(不通过网关) | -| 更新网关路由 | `deploy/k8s/envoy-gateway.yaml` | 添加路由、集群、认证规则 | -| 配置认证 | `deploy/envoy/setup-jwt-auth.sh` | 生成和管理 JWT 密钥 | -| 部署到 K8s | `deploy/k8s/service/` | 创建服务的 Deployment 和 Service | - -需要更多帮助?查看 `PROJECT_GUIDE.md` 了解完整的项目架构和工作流! diff --git a/docs/INTEGRATION.md b/docs/INTEGRATION.md deleted file mode 100644 index f002f4b..0000000 --- a/docs/INTEGRATION.md +++ /dev/null @@ -1,601 +0,0 @@ -# JWT 集成指南 - -指导如何将 JWT Manager 集成到 RPC Handlers 和业务逻辑中。 - -## 1. gRPC Unary Interceptor 实现 - -在 RPC 服务中添加 JWT 验证拦截器。 - -### 创建拦截器 - -创建文件 [app/users/rpc/internal/interceptor/jwt_interceptor.go](../../../app/users/rpc/internal/interceptor/jwt_interceptor.go): - -```go -package interceptor - -import ( - "context" - "log" - - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/metadata" - "google.golang.org/grpc/status" - - "yourmodule/app/users/rpc/internal/svc" -) - -// JwtUnaryInterceptor 验证 gRPC 请求中的 JWT 令牌 -func JwtUnaryInterceptor(svcCtx *svc.ServiceContext) grpc.UnaryServerInterceptor { - return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { - // 获取请求元数据 - md, ok := metadata.FromIncomingContext(ctx) - if !ok { - return nil, status.Error(codes.Unauthenticated, "missing metadata") - } - - // 从 Authorization 头提取令牌 - tokens := md.Get("authorization") - if len(tokens) == 0 { - return nil, status.Error(codes.Unauthenticated, "missing authorization header") - } - - token := tokens[0] - - // 验证令牌 - claims, err := svcCtx.JwtManager.Valid(ctx, token) - if err != nil { - log.Printf("Token validation failed: %v", err) - - // 尝试刷新令牌(如果过期但仍在 Redis 中) - newToken, refreshErr := svcCtx.JwtManager.Renew(ctx, token) - if refreshErr == nil && newToken != "" { - // 在响应头中返回新令牌 - grpc.SetHeader(ctx, metadata.Pairs("authorization", newToken)) - // 继续处理请求,使用原令牌的声明 - // 注意:实际应用中需要重新验证新令牌 - newClaims, err := svcCtx.JwtManager.Valid(ctx, newToken) - if err != nil { - return nil, status.Error(codes.Unauthenticated, "token refresh failed") - } - claims = newClaims - } else { - return nil, status.Error(codes.Unauthenticated, "invalid or expired token") - } - } - - // 将声明附加到上下文,供处理器使用 - newCtx := context.WithValue(ctx, "claims", claims) - - return handler(newCtx, req) - } -} - -// JwtStreamInterceptor 验证流式 gRPC 请求中的 JWT 令牌 -func JwtStreamInterceptor(svcCtx *svc.ServiceContext) grpc.StreamServerInterceptor { - return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { - md, ok := metadata.FromIncomingContext(ss.Context()) - if !ok { - return status.Error(codes.Unauthenticated, "missing metadata") - } - - tokens := md.Get("authorization") - if len(tokens) == 0 { - return status.Error(codes.Unauthenticated, "missing authorization header") - } - - token := tokens[0] - claims, err := svcCtx.JwtManager.Valid(ss.Context(), token) - if err != nil { - return status.Error(codes.Unauthenticated, "invalid token") - } - - // 创建包装流以注入上下文 - wrappedStream := &WrappedStream{ - ServerStream: ss, - ctx: context.WithValue(ss.Context(), "claims", claims), - } - - return handler(srv, wrappedStream) - } -} - -// WrappedStream 包装 grpc.ServerStream 以注入新的上下文 -type WrappedStream struct { - grpc.ServerStream - ctx context.Context -} - -func (w *WrappedStream) Context() context.Context { - return w.ctx -} -``` - -### 在 Server 中注册拦截器 - -修改 [app/users/rpc/usercenter/usercenter.go](../app/users/rpc/usercenter/usercenter.go): - -```go -package main - -import ( - "flag" - "fmt" - "log" - - "yourmodule/app/users/rpc/internal/config" - "yourmodule/app/users/rpc/internal/interceptor" - "yourmodule/app/users/rpc/internal/server" - "yourmodule/app/users/rpc/internal/svc" - "yourmodule/app/users/rpc/pb" - - "github.com/zeromicro/go-zero/core/conf" - "github.com/zeromicro/go-zero/core/logx" - "google.golang.org/grpc" -) - -var configFile = flag.String("f", "etc/pb.yaml", "the config file") - -func main() { - flag.Parse() - - var c config.Config - conf.MustLoad(*configFile, &c) - ctx := svc.NewServiceContext(c) - - logx.DisableStat() - - s := grpc.NewServer( - grpc.UnaryInterceptor(interceptor.JwtUnaryInterceptor(ctx)), - grpc.StreamInterceptor(interceptor.JwtStreamInterceptor(ctx)), - ) - - pb.RegisterUsercenterServer(s, server.NewUsercenterServer(ctx)) - - logx.Infof("Starting gRPC server on %s:%d", c.Host, c.Port) - if err := s.Serve(net.Listen("tcp", "0.0.0.0:"+fmt.Sprintf("%d", c.Port))); err != nil { - logx.Error(err) - } -} -``` - -## 2. 登录 Handler 实现 - -实现 [app/users/api/internal/handler/user/loginHandler.go](../../../app/users/api/internal/handler/user/loginHandler.go): - -```go -package user - -import ( - "context" - "log" - "net/http" - - "yourmodule/app/users/api/internal/logic/user" - "yourmodule/app/users/api/internal/svc" - "yourmodule/app/users/api/internal/types" -) - -// LoginHandler 处理用户登录 -func LoginHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - var req types.LoginRequest - - // 解析请求体... - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid request", http.StatusBadRequest) - return - } - - // 调用业务逻辑 - resp, err := user.NewLoginLogic(r.Context(), svcCtx).Login(&req) - if err != nil { - log.Printf("Login failed: %v", err) - http.Error(w, "Login failed", http.StatusUnauthorized) - return - } - - // 返回令牌 - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(resp) - } -} -``` - -实现 [app/users/api/internal/logic/user/loginLogic.go](../../../app/users/api/internal/logic/user/loginLogic.go): - -```go -package user - -import ( - "context" - "errors" - - "yourmodule/app/users/api/internal/svc" - "yourmodule/app/users/api/internal/types" -) - -type LoginLogic struct { - ctx context.Context - svcCtx *svc.ServiceContext -} - -func NewLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LoginLogic { - return &LoginLogic{ - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *LoginLogic) Login(req *types.LoginRequest) (*types.LoginResponse, error) { - // 1. 验证用户凭证(密码等) - user, err := l.svcCtx.UserModel.FindByEmail(l.ctx, req.Email) - if err != nil { - return nil, errors.New("user not found") - } - - // 2. 验证密码 - if !user.VerifyPassword(req.Password) { - return nil, errors.New("invalid password") - } - - // 3. 生成 JWT 令牌 - token, err := l.svcCtx.JwtManager.New( - l.ctx, - user.ID, - user.Email, - user.Name, - ) - if err != nil { - return nil, errors.New("failed to generate token") - } - - // 4. 返回令牌 - return &types.LoginResponse{ - Token: token, - User: types.User{ - ID: user.ID, - Email: user.Email, - Name: user.Name, - }, - }, nil -} -``` - -## 3. 在 Handlers 中使用声明 - -在 Protected Handlers 中提取并使用声明: - -```go -package user - -import ( - "context" - "log" - "net/http" - - "yourmodule/app/users/api/internal/svc" - "yourmodule/app/users/api/internal/types" - "github.com/golang-jwt/jwt/v4" -) - -// GetUserInfoHandler 获取当前用户信息 -func GetUserInfoHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - // 从上下文提取声明(由拦截器设置) - claims, ok := r.Context().Value("claims").(*jwt.RegisteredClaims) - if !ok { - http.Error(w, "Unauthorized", http.StatusUnauthorized) - return - } - - // 使用声明中的用户信息 - userID := claims.Subject // 用户 ID 存储在 Subject 中 - log.Printf("User %s requested their info", userID) - - // 查询用户信息 - user, err := svcCtx.UserModel.FindByID(r.Context(), userID) - if err != nil { - http.Error(w, "User not found", http.StatusNotFound) - return - } - - // 返回用户信息 - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(user) - } -} -``` - -## 4. 令牌刷新端点 - -实现令牌刷新端点: - -```go -package user - -import ( - "net/http" - - "yourmodule/app/users/api/internal/svc" - "yourmodule/app/users/api/internal/types" -) - -// RefreshTokenHandler 刷新过期的 JWT 令牌 -func RefreshTokenHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - var req types.RefreshTokenRequest - - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid request", http.StatusBadRequest) - return - } - - // 提取旧令牌 - oldToken := req.Token - - // 尝试刷新令牌 - newToken, err := svcCtx.JwtManager.Renew(r.Context(), oldToken) - if err != nil { - http.Error(w, "Token refresh failed", http.StatusUnauthorized) - return - } - - // 返回新令牌 - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(types.RefreshTokenResponse{ - Token: newToken, - }) - } -} -``` - -## 5. 登出处理 - -实现登出端点以撤销令牌: - -```go -package user - -import ( - "net/http" - - "yourmodule/app/users/api/internal/svc" -) - -// LogoutHandler 登出用户(撤销令牌) -func LogoutHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - // 从上下文提取声明 - claims, ok := r.Context().Value("claims").(*jwt.RegisteredClaims) - if !ok { - http.Error(w, "Unauthorized", http.StatusUnauthorized) - return - } - - userID := claims.Subject - - // 获取用户当前令牌 - currentToken := r.Header.Get("Authorization") - - // 撤销令牌 - err := svcCtx.JwtManager.Revoke(r.Context(), userID, currentToken) - if err != nil { - http.Error(w, "Logout failed", http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]string{"message": "logged out successfully"}) - } -} -``` - -## 6. 特定端点的 JWT 验证 - -对于 REST API,在需要的 handlers 中手动验证令牌: - -### 在 Routes 中配置 - -修改 [app/users/api/internal/handler/routes.go](../app/users/api/internal/handler/routes.go): - -```go -package handler - -import ( - "net/http" - - "yourmodule/app/users/api/internal/middleware" - "yourmodule/app/users/api/internal/svc" - "yourmodule/app/users/api/internal/handler/user" -) - -// RegisterRoutes 注册所有路由 -func RegisterRoutes(router *http.ServeMux, svcCtx *svc.ServiceContext) { - // 公开路由 - router.HandleFunc("POST /api/v1/auth/login", user.LoginHandler(svcCtx)) - router.HandleFunc("POST /api/v1/auth/refresh", user.RefreshTokenHandler(svcCtx)) - - // 受保护的路由(需要 JWT 验证) - protected := middleware.JwtMiddleware(svcCtx) - router.HandleFunc("GET /api/v1/users/me", protected(user.GetUserInfoHandler(svcCtx))) - router.HandleFunc("POST /api/v1/users/logout", protected(user.LogoutHandler(svcCtx))) - router.HandleFunc("PUT /api/v1/users/me", protected(user.UpdateUserInfoHandler(svcCtx))) -} -``` - -### 创建 JWT 中间件 - -创建 [app/users/api/internal/middleware/jwt.go](../../../app/users/api/internal/middleware/jwt.go): - -```go -package middleware - -import ( - "context" - "net/http" - "strings" - - "yourmodule/app/users/api/internal/svc" -) - -// JwtMiddleware 为 HTTP 处理器添加 JWT 验证 -func JwtMiddleware(svcCtx *svc.ServiceContext) func(http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // 从 Authorization 头提取令牌 - authHeader := r.Header.Get("Authorization") - if authHeader == "" { - http.Error(w, "Missing authorization header", http.StatusUnauthorized) - return - } - - // 期望格式: "Bearer " - parts := strings.SplitN(authHeader, " ", 2) - if len(parts) != 2 || parts[0] != "Bearer" { - http.Error(w, "Invalid authorization header", http.StatusUnauthorized) - return - } - - token := parts[1] - - // 验证令牌 - claims, err := svcCtx.JwtManager.Valid(r.Context(), token) - if err != nil { - // 尝试刷新 - newToken, refreshErr := svcCtx.JwtManager.Renew(r.Context(), token) - if refreshErr != nil { - http.Error(w, "Invalid or expired token", http.StatusUnauthorized) - return - } - - // 在响应头返回新令牌 - w.Header().Set("X-New-Token", newToken) - - // 重新验证新令牌 - claims, err = svcCtx.JwtManager.Valid(r.Context(), newToken) - if err != nil { - http.Error(w, "Token refresh failed", http.StatusUnauthorized) - return - } - } - - // 将声明附加到上下文 - newCtx := context.WithValue(r.Context(), "claims", claims) - next.ServeHTTP(w, r.WithContext(newCtx)) - }) - } -} -``` - -## 7. 错误处理最佳实践 - -```go -package logic - -import ( - "errors" - "log" - - "yourmodule/app/users/rpc/internal/utils" -) - -// HandleJwtError 处理 JWT 相关错误 -func HandleJwtError(err error) error { - if errors.Is(err, utils.ErrTokenExpired) { - log.Println("Token has expired, user needs to refresh") - return errors.New("token expired - use refresh endpoint") - } - - if errors.Is(err, utils.ErrTokenInvalid) { - log.Println("Token is invalid or malformed") - return errors.New("invalid token") - } - - if errors.Is(err, utils.ErrTokenNotFound) { - log.Println("Token not found in Redis (revoked or expired)") - return errors.New("token revoked or expired") - } - - return err -} -``` - -## 8. 测试 JWT 集成 - -### 单元测试示例 - -```go -package interceptor - -import ( - "context" - "testing" - - "google.golang.org/grpc/codes" - "google.golang.org/grpc/metadata" - "google.golang.org/grpc/status" -) - -func TestJwtUnaryInterceptor_ValidToken(t *testing.T) { - // 1. 创建有效的令牌 - token, err := svcCtx.JwtManager.New(context.Background(), "user123", "user@example.com", "John") - if err != nil { - t.Fatalf("Failed to create token: %v", err) - } - - // 2. 创建包含令牌的上下文 - md := metadata.Pairs("authorization", token) - ctx := metadata.NewIncomingContext(context.Background(), md) - - // 3. 调用拦截器 - _, err = JwtUnaryInterceptor(svcCtx)(ctx, nil, nil, func(ctx context.Context, req interface{}) (interface{}, error) { - return "success", nil - }) - - if err != nil { - t.Errorf("Unexpected error: %v", err) - } -} - -func TestJwtUnaryInterceptor_ExpiredToken(t *testing.T) { - // 1. 创建过期的令牌或使用无效令牌 - token := "invalid.token.here" - - // 2. 创建包含令牌的上下文 - md := metadata.Pairs("authorization", token) - ctx := metadata.NewIncomingContext(context.Background(), md) - - // 3. 调用拦截器 - _, err := JwtUnaryInterceptor(svcCtx)(ctx, nil, nil, func(ctx context.Context, req interface{}) (interface{}, error) { - return "success", nil - }) - - // 4. 验证错误 - st, ok := status.FromError(err) - if !ok || st.Code() != codes.Unauthenticated { - t.Errorf("Expected Unauthenticated error, got: %v", err) - } -} -``` - -## 9. 生产部署清单 - -在将 JWT 集成部署到生产环境前: - -- [ ] 所有令牌端点都进行了压力测试 -- [ ] 令牌刷新逻辑已验证 -- [ ] 错误处理覆盖了所有 JWT 失败情况 -- [ ] 审计日志记录了所有认证尝试 -- [ ] 密钥轮换计划已确定 -- [ ] 监控和告警已配置 -- [ ] 灾难恢复流程已文档化 -- [ ] 所有依赖于 JWT 的服务都已更新 - -## 相关文件 - -- [app/users/rpc/internal/utils/jwt.go](../app/users/rpc/internal/utils/jwt.go) - JWT Manager 实现 -- [app/users/rpc/internal/config/config.go](../app/users/rpc/internal/config/config.go) - JWT 配置 -- [app/users/rpc/internal/svc/serviceContext.go](../app/users/rpc/internal/svc/serviceContext.go) - 依赖注入 -- [deploy/k8s/secrets/jwt-secret.yaml](./jwt-secret.yaml) - Secret 和 RBAC -- [deploy/k8s/secrets/DEPLOYMENT.md](./DEPLOYMENT.md) - 部署指南 diff --git a/docs/PROJECT_GUIDE.md b/docs/PROJECT_GUIDE.md deleted file mode 100644 index 689f6cc..0000000 --- a/docs/PROJECT_GUIDE.md +++ /dev/null @@ -1,1032 +0,0 @@ -# Juwan 后端项目完整使用指南 - -## 项目概述 - -``` -Juwan 是一个基于 Go-Zero 微服务框架的分布式后端系统,采用以下架构: - -┌─────────────────────────────────────────────────────────────────────┐ -│ Envoy Gateway (负载均衡、认证) │ -│ 端口: 80 (HTTP) │ -└──────────────┬──────────────────────────────────────────────────────┘ - │ - ┌───────┴──────────┐ - │ │ - ┌───▼────────┐ ┌───▼────────┐ - │ User API │ │ Order API │ - │ (8888) │ │ (8888) │ - └───┬────────┘ └────────────┘ - │ - ┌───▼────────────────────┐ - │ User RPC (内部使用) │ - │ gRPC (不暴露) │ - └────────────────────────┘ - │ - ┌───▼────────────────────┐ - │ PostgreSQL Database │ - └────────────────────────┘ -``` - -**关键特性:** -- ✅ API 层通过 Envoy Gateway 暴露给外部 -- ✅ RPC 层仅限集群内部通信(通过 K8s Service Discovery) -- ✅ JWT 认证(可选路由) -- ✅ 密码加密存储 -- ✅ 用户会话管理 - ---- - -## 1️⃣ 添加新服务(完整步骤) - -### 示例:添加一个 Product API 服务 - -#### Step 1: 定义 API 接口 - -创建 `desc/api/product.api`: - -```goctl -syntax = "v1" - -type ( - Product { - ProductId int64 `json:"productId"` - Name string `json:"name"` - Description string `json:"description"` - Price float64 `json:"price"` - Stock int32 `json:"stock"` - CreateAt int64 `json:"createAt"` - } - - CreateProductReq { - Name string `json:"name" binding:"required,min=2"` - Description string `json:"description"` - Price float64 `json:"price" binding:"required,gt=0"` - Stock int32 `json:"stock" binding:"required,gte=0"` - } - - CreateProductResp { - ProductId int64 `json:"productId"` - Message string `json:"message"` - } - - GetProductReq { - ProductId int64 `path:"productId" binding:"required,gt=0"` - } - - ListProductsReq { - Page int64 `form:"page" binding:"required,gt=0"` - Limit int64 `form:"limit" binding:"required,gt=0,lte=100"` - } - - ListProductsResp { - Total int64 `json:"total"` - Products []Product `json:"products"` - } -) - -@server ( - group: product - prefix: /api/products - middleware: Logger -) -service product-api { - @doc (summary: "创建商品") - @handler CreateProduct - post / (CreateProductReq) returns (CreateProductResp) - - @doc (summary: "获取商品详情") - @handler GetProduct - get /:productId (GetProductReq) returns (Product) - - @doc (summary: "列出所有商品") - @handler ListProducts - get / (ListProductsReq) returns (ListProductsResp) -} -``` - -#### Step 2: 创建 RPC 定义(内部服务) - -创建 `desc/rpc/product.proto`: - -```proto -syntax = "proto3"; - -option go_package = "./pb"; - -package pb; - -message Product { - int64 productId = 1; - string name = 2; - string description = 3; - double price = 4; - int32 stock = 5; - int64 createAt = 6; -} - -message GetProductReq { - int64 productId = 1; -} - -message GetProductResp { - Product product = 1; -} - -message UpdateStockReq { - int64 productId = 1; - int32 delta = 2; // 正数增加库存,负数减少 -} - -message UpdateStockResp { - int32 newStock = 1; -} - -service ProductCenter { - rpc GetProduct(GetProductReq) returns (GetProductResp); - rpc UpdateStock(UpdateStockReq) returns (UpdateStockResp); -} -``` - -#### Step 3: 生成代码 - -```bash -# 生成 API 服务代码 -goctl api go -api desc/api/product.api -dir app/product/api --style=goZero - -# 生成 RPC 服务代码 -goctl rpc protoc desc/rpc/product.proto \ - --go_out=./app/product/rpc \ - --go-grpc_out=./app/product/rpc \ - --zrpc_out=./app/product/rpc \ - --style=goZero -``` - -#### Step 4: 实现业务逻辑 - -编辑 `app/product/api/internal/logic/product/createproduct.go`: - -```go -package product - -import ( - "context" - "product-api/app/product/api/internal/svc" - "product-api/app/product/api/internal/types" -) - -type CreateProductLogic struct { - ctx context.Context - svcCtx *svc.ServiceContext -} - -func NewCreateProductLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateProductLogic { - return &CreateProductLogic{ - ctx: ctx, - svcCtx: svcCtx, - } -} - -func (l *CreateProductLogic) CreateProduct(req *types.CreateProductReq) (*types.CreateProductResp, error) { - // TODO: 调用 RPC 或数据库创建商品 - return &types.CreateProductResp{ - ProductId: 1, - Message: "创建成功", - }, nil -} -``` - -#### Step 5: 配置 K8s 部署 - -创建 `deploy/k8s/service/product/product-api.yaml`: - -```yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: product-api-config - namespace: juwan -data: - product-api.yaml: | - Name: product-api - Host: 0.0.0.0 - Port: 8890 - Database: - DataSource: postgres://user:pass@pg-dx:5432/juwan - ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: product-api - namespace: juwan -spec: - replicas: 2 - selector: - matchLabels: - app: product-api - template: - metadata: - labels: - app: product-api - spec: - containers: - - name: product-api - image: your-registry/product-api:latest - ports: - - containerPort: 8890 - volumeMounts: - - name: config - mountPath: /etc/product-api - env: - - name: TZ - value: "Asia/Shanghai" - volumes: - - name: config - configMap: - name: product-api-config - ---- -apiVersion: v1 -kind: Service -metadata: - name: product-api-svc - namespace: juwan -spec: - selector: - app: product-api - ports: - - port: 8890 - targetPort: 8890 - type: ClusterIP -``` - -#### Step 6: 更新 Envoy 网关配置 - -在 `deploy/k8s/envoy-gateway.yaml` 中添加: - -```yaml -# 在 http_filters->route_config->virtual_hosts->routes 添加: -- match: - prefix: /api/products - route: - cluster: product_api_cluster - timeout: 30s - -# 在 clusters 添加: -- name: product_api_cluster - connect_timeout: 5s - type: STRICT_DNS - dns_lookup_family: V4_ONLY - lb_policy: ROUND_ROBIN - load_assignment: - cluster_name: product_api_cluster - endpoints: - - lb_endpoints: - - endpoint: - address: - socket_address: - address: product-api-svc.juwan.svc.cluster.local - port_value: 8890 - health_checks: - - timeout: 3s - interval: 10s - unhealthy_threshold: 2 - healthy_threshold: 2 - http_health_check: - path: /health -``` - -#### Step 7: 部署到集群 - -```bash -# 应用 K8s 配置 -kubectl apply -f deploy/k8s/service/product/product-api.yaml - -# 更新 Envoy Gateway 配置 -kubectl apply -f deploy/k8s/envoy-gateway.yaml - -# 重启 Envoy Pod 使配置生效 -kubectl delete pods -n juwan -l app=envoy-gateway - -# 验证 -kubectl get pods -n juwan -curl http://localhost/api/products -``` - ---- - -## 2️⃣ RPC 服务内部隔离(不暴露给外部) - -### 当前架构(推荐) - -``` -┌────────────────────────────────────────────┐ -│ 外部客户端 (互联网) │ -└────────────┬─────────────────────────────┘ - │ - ┌────▼──────────┐ - │ Envoy Gateway│ - │ (仅 HTTP) │ - └────┬──────────┘ - │ - ┌────────┴─────────────┐ - │ │ -┌───▼───────────┐ ┌──────▼─────────┐ -│ User API │ │ Product API │ -│ (8888) │ │ (8890) │ -└───┬───────────┘ └────────────────┘ - │ -┌───▼──────────────────────────────────┐ -│ User RPC (完全隐藏) │ -│ - 不暴露端口 │ -│ - gRPC 通信 │ -│ - K8s Service DNS 发现 │ -│ - NetworkPolicy 限制通信 │ -└──────────────────────────────────────┘ -``` - -### 实现步骤 - -#### 1. 创建仅内部的 Service(无 port 暴露) - -在 `deploy/k8s/service/user/user-rpc.yaml`: - -```yaml -apiVersion: v1 -kind: Service -metadata: - name: user-rpc-svc - namespace: juwan -spec: - selector: - app: user-rpc - ports: - - name: grpc - port: 50051 - targetPort: 50051 - type: ClusterIP # 📌 仅限集群内部访问,不暴露 NodePort 或 LoadBalancer - sessionAffinity: None -``` - -#### 2. 配置 NetworkPolicy(进一步限制) - -```yaml -apiVersion: networking.k8s.io/v1 -kind: NetworkPolicy -metadata: - name: user-rpc-access - namespace: juwan -spec: - podSelector: - matchLabels: - app: user-rpc - policyTypes: - - Ingress - ingress: - # 仅允许 API 和其他服务(如 Order API)访问 - - from: - - podSelector: - matchLabels: - app: user-api - - podSelector: - matchLabels: - app: order-api - - podSelector: - matchLabels: - app: product-api - ports: - - protocol: TCP - port: 50051 -``` - -#### 3. API 服务中调用 RPC - -在 `app/users/api/internal/svc/servicecontext.go`: - -```go -package svc - -import ( - "github.com/zeromicro/go-zero/zrpc" - "app/users/api/internal/config" - "app/users/rpc/pb" -) - -type ServiceContext struct { - Config config.Config - UserRpc pb.UsercenterClient // RPC 客户端 -} - -func NewServiceContext(c config.Config) *ServiceContext { - userRpcClient := zrpc.MustNewClient(c.UserRpc) - return &ServiceContext{ - Config: c, - UserRpc: pb.NewUsercenterClient(userRpcClient.Conn()), - } -} -``` - -配置文件 `app/users/api/etc/user-api.yaml`: - -```yaml -Name: user-api -Host: 0.0.0.0 -Port: 8888 - -# RPC 配置(使用 K8s DNS) -UserRpc: - Endpoints: - - user-rpc-svc.juwan.svc.cluster.local:50051 - -Database: - DataSource: postgres://user:pass@pg-dx:5432/juwan -``` - -#### 4. RPC 不在 Envoy 中配置路由 - -❌ **不要**在 `envoy-gateway.yaml` 中添加 RPC 集群: - -```yaml -# ❌❌❌ 不要这样做 ❌❌❌ -# routes: -# - match: -# prefix: /juwan.pb.Usercenter/ -# route: -# cluster: user_rpc_cluster # 这样会暴露 RPC -``` - ---- - -## 3️⃣ JWT 认证与分级访问控制 - -### 实现逻辑 - -``` -请求到达 Envoy → JWT 验证 - -┌──────────────────────────────────────┐ -│ 无效或缺省 Token │ -└──────────┬───────────────────────────┘ - │ - 公开路由 (允许) ──→ /api/users/login - /api/users/register - /api/products (列表、详情) - - 受保护路由 (拒绝) ──→ 返回 401 Unauthorized - │ - ┌──────▼───────────────────────────┐ - │ 有效 Token │ - │ (已登录) │ - └──────┬───────────────────────────┘ - │ - ┌──────▼──────────────────────────────────────────────┐ - │ 完整数据访问 (取决于后端 RPC) │ - │ - 用户信息 (包括隐私信息) │ - │ - 订单历史 │ - │ - 收藏列表 │ - └──────────────────────────────────────────────────────┘ -``` - -### 配置步骤 - -#### 1. 生成 JWT 密钥并存储为 K8s Secret - -```bash -# 生成 HMAC 密钥(用于签名) -openssl rand -hex 32 - -# 输出示例: a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 - -# 创建 Secret -kubectl create secret generic jwt-secret \ - --from-literal=key=a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 \ - -n juwan - -# 验证 -kubectl get secret jwt-secret -n juwan -o yaml -``` - -#### 2. 更新 Envoy 配置(添加 JWT 验证) - -编辑 `deploy/k8s/envoy-gateway.yaml`: - -```yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: envoy-config - namespace: juwan -data: - envoy.yaml: | - static_resources: - listeners: - - name: listener_http - address: - socket_address: - address: 0.0.0.0 - port_value: 8080 - filter_chains: - - filters: - - name: envoy.filters.network.http_connection_manager - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager - stat_prefix: ingress_http - http_filters: - # JWT 认证过滤器(在 router 之前) - - name: envoy.filters.http.jwt_authn - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication - - # 定义 JWT 提供者 - providers: - my-provider: - issuer: "juwan" - audiences: "api" - # 使用文件系统上的密钥 - local_jwks: - filename: /etc/envoy/jwks.json - - # 定义受保护的路由 - rules: - # 规则1: 登录和注册不需要认证 - - match: - prefix: /api/users/login - allow_missing_or_failed: true # 允许缺省/失败的 token - - match: - prefix: /api/users/register - allow_missing_or_failed: true - # 规则2: 重定向认证失败请求 - - match: - prefix: "/" - requires: - provider_name: "my-provider" - # 如果认证失败,Envoy 直接拒绝,返回 401 - - # 路由过滤器 - - name: envoy.filters.http.router - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router - - route_config: - name: local_route - virtual_hosts: - - name: backend - domains: ["*"] - routes: - - match: - prefix: /api/users - route: - cluster: user_api_cluster - - match: - prefix: /api/products - route: - cluster: product_api_cluster - # ... 其他路由 ... - - # ... clusters 定义保持不变 ... -``` - -#### 3. 在 API 服务中添加认证中间件 - -创建 `app/users/api/internal/middleware/authmiddleware.go`: - -```go -package middleware - -import ( - "fmt" - "net/http" - "strings" - - "github.com/golang-jwt/jwt/v4" -) - -type AuthMiddleware struct { - JwtSecret string -} - -func (m *AuthMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - // 获取 Authorization header - authHeader := r.Header.Get("Authorization") - if authHeader == "" { - // 如果认证失败,Envoy 会返回 401, - // 但我们可以在 API 层添加自定义逻辑 - next(w, r) - return - } - - parts := strings.Split(authHeader, " ") - if len(parts) != 2 || parts[0] != "Bearer" { - http.Error(w, "Invalid authorization header", http.StatusUnauthorized) - return - } - - token := parts[1] - - // 验证 token - _, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) { - if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { - return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) - } - return []byte(m.JwtSecret), nil - }) - - if err != nil { - http.Error(w, "Invalid token", http.StatusUnauthorized) - return - } - - // Token 有效,继续处理 - next(w, r) - } -} -``` - -#### 4. 登录端点返回 JWT Token - -编辑 `app/users/api/internal/logic/user/loginlogic.go`: - -```go -package user - -import ( - "context" - "time" - "github.com/golang-jwt/jwt/v4" - "app/users/api/internal/svc" - "app/users/api/internal/types" -) - -type LoginLogic struct { - ctx context.Context - svcCtx *svc.ServiceContext -} - -func (l *LoginLogic) Login(req *types.LoginReq) (*types.LoginResp, error) { - // TODO: 验证用户名和密码 - - // 生成 JWT Token - claims := jwt.MapClaims{ - "userId": 1, // 实际应从数据库获取 - "username": req.Username, - "exp": time.Now().Add(24 * time.Hour).Unix(), - "iat": time.Now().Unix(), - } - - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - tokenString, err := token.SignedString([]byte(l.svcCtx.Config.JwtSecret)) - if err != nil { - return nil, err - } - - return &types.LoginResp{ - Token: tokenString, - Expires: time.Now().Add(24 * time.Hour).Unix(), - }, nil -} -``` - -### JWT 认证时的分级访问 - -**后端 RPC 可处理分级访问:** - -```go -// 在 User RPC 中实现 -func (s *UsercenterServer) GetUserInfo(ctx context.Context, req *pb.GetUsersByIdReq) (*pb.GetUsersByIdResp, error) { - // 获取请求者的 userId(从 context 中取,由 API 层传递) - requesterID := ctx.Value("userId").(int64) - targetID := req.Id - - // 查询用户信息 - user := s.getUserFromDB(targetID) - - if requesterID == targetID { - // 自己查看自己 → 返回完整信息(包含隐私信息) - return &pb.GetUsersByIdResp{ - Users: &pb.Users{ - UserId: user.UserId, - Username: user.Username, - Email: user.Email, // ✅ 包含 - Phone: user.Phone, // ✅ 包含 - // ... 所有字段 - }, - }, nil - } else { - // 查看别人 → 返回部分信息 - return &pb.GetUsersByIdResp{ - Users: &pb.Users{ - UserId: user.UserId, - Username: user.Username, - // ❌ 不返回 Email、Phone 等隐私信息 - }, - }, nil - } -} -``` - -**或在 API 层处理:** - -```go -func (l *GetUserInfoLogic) GetUserInfo(req *types.GetUserInfoReq) (*types.UserInfo, error) { - // 从 context 获取当前认证用户 - currentUser := l.ctx.Value("userId").(int64) - - // 调用 RPC - rpcResp, err := l.svcCtx.UserRpc.GetUsersById(l.ctx, &pb.GetUsersByIdReq{ - Id: req.UserId, - }) - - if currentUser == req.UserId { - // 自己查看自己 → 返回所有信息 - return &types.UserInfo{ - UserId: rpcResp.Users.UserId, - Username: rpcResp.Users.Username, - Email: rpcResp.Users.Email, - Phone: rpcResp.Users.Phone, - }, nil - } else { - // 查看别人 → 仅返回公开信息 - return &types.UserInfo{ - UserId: rpcResp.Users.UserId, - Username: rpcResp.Users.Username, - }, nil - } -} -``` - ---- - -## 4️⃣ 认证失败处理策略 - -### 当前配置 - -| 路由 | 认证要求 | 认证失败 | 示例 | -|-----|---------|--------|------| -| `/api/users/login` | ❌ 不需要 | 放行 | `POST /api/users/login` (允许) | -| `/api/users/register` | ❌ 不需要 | 放行 | `POST /api/users/register` (允许) | -| `/api/users/:userId` | ✅ 需要 | 拒绝 401 | `GET /api/users/123` (无 token → 401) | -| `/api/users/:userId/password` | ✅ 需要 | 拒绝 401 | `PUT /api/users/123/password` (无 token → 401) | -| `/api/products` | ❌ 不需要 | 放行 | `GET /api/products` (允许) | -| `/api/products/:id` | ❌ 不需要 | 放行 | `GET /api/products/1` (允许) | - -### 自定义错误响应 - -创建 `deploy/k8s/envoy-gateway.yaml` 的自定义拒绝响应: - -```yaml -# 在 jwt_authn 过滤器中配置 -http_filters: - - name: envoy.filters.http.jwt_authn - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication - providers: - my-provider: - issuer: "juwan" - audiences: "api" - local_jwks: - filename: /etc/envoy/jwks.json - rules: - - match: - prefix: /api/users/login - allow_missing_or_failed: true - - match: - prefix: /api/users/register - allow_missing_or_failed: true - - match: - prefix: / - requires: - provider_name: "my-provider" - # 认证失败后的行为 - bypass_cors_preflight: false # 允许 OPTIONS 跨域请求 -``` - -**认证失败时 Envoy 返回 401:** - -```json -HTTP/1.1 401 Unauthorized -content-type: text/html - -Jwt verification fails. -``` - -### 在 API 层添加自定义错误处理 - -如果希望自定义错误响应,可以在每个受保护的 handler 中添加检查: - -```go -func (l *UpdateUserInfoLogic) UpdateUserInfo(req *types.UpdateUserInfoReq) (*types.UpdateUserInfoResp, error) { - // 获取当前请求者的身份 - ctx := context.WithValue(l.ctx, "currentUserId", req.UserId) - - // 如果没有匹配的 token,API 层可以添加自定义错误 - if req.UserId <= 0 { - return nil, errors.New("invalid user id") - } - - // 继续处理 - return &types.UpdateUserInfoResp{Message: "更新成功"}, nil -} -``` - ---- - -## 5️⃣ 完整工作流示例 - -### 场景:用户注册 → 登录 → 获取用户信息 - -#### 步骤 1: 注册用户(无需认证) - -```bash -curl -X POST http://localhost/api/users/register \ - -H "Content-Type: application/json" \ - -d '{ - "username": "john_doe", - "password": "securePass123", - "email": "john@example.com", - "phone": "13800138001" - }' - -# 响应 -{ - "userId": 1, - "username": "john_doe", - "email": "john@example.com", - "message": "用户注册成功" -} -``` - -#### 步骤 2: 登录获取 Token(无需认证) - -```bash -curl -X POST http://localhost/api/users/login \ - -H "Content-Type: application/json" \ - -d '{ - "username": "john_doe", - "password": "securePass123" - }' - -# 响应 -{ - "userId": 1, - "username": "john_doe", - "email": "john@example.com", - "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", - "expires": 1708694400 -} -``` - -#### 步骤 3: 获取用户信息(需要 Token) - -```bash -# ❌ 无 Token → 401 Unauthorized -curl http://localhost/api/users/1 -# Jwt verification fails. - -# ✅ 有 Token → 成功 -curl http://localhost/api/users/1 \ - -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." - -# 响应 -{ - "userId": 1, - "username": "john_doe", - "email": "john@example.com", - "phone": "13800138001", - "avatar": "https://...", - "status": 1, - "createAt": 1708608000, - "updateAt": 1708608000 -} -``` - -#### 步骤 4: 查看他人信息(部分数据) - -```bash -# 用户1 查看用户2 的信息(已登录) -curl http://localhost/api/users/2 \ - -H "Authorization: Bearer eyJhbGc..." - -# 响应(仅公开信息) -{ - "userId": 2, - "username": "jane_doe", - # ❌ 不包含 email, phone 等隐私信息 -} -``` - ---- - -## 6️⃣ 部署检查清单 - -在部署新服务或更新配置前,使用此清单: - -- [ ] **API 定义** - `desc/api/*.api` 文件已创建 -- [ ] **RPC 定义** - `desc/rpc/*.proto` 文件已创建(如需内部通信) -- [ ] **代码生成** - 运行 `goctl api/rpc` 命令生成代码 -- [ ] **业务逻辑** - 编辑 `app/*/api/internal/logic/` 实现功能 -- [ ] **K8s 部署清单** - 创建 `deploy/k8s/service/*/` 文件 - - [ ] ConfigMap(配置) - - [ ] Deployment(部署) - - [ ] Service(K8s 服务发现) - - [ ] NetworkPolicy(网络隔离,可选) -- [ ] **Envoy 更新** - 修改 `deploy/k8s/envoy-gateway.yaml` - - [ ] 添加路由规则 - - [ ] 添加上游集群 - - [ ] 验证健康检查地址 -- [ ] **测试验证** - ```bash - kubectl apply -f deploy/k8s/service/{name}/ - kubectl apply -f deploy/k8s/envoy-gateway.yaml - kubectl delete pods -n juwan -l app=envoy-gateway - curl http://localhost/api/{path} # 端口转发后测试 - ``` - ---- - -## 7️⃣ 故障排查 - -### 问题 1: 新服务无法访问 - -```bash -# 检查 Pod 状态 -kubectl get pods -n juwan - -# 查看 Pod 日志 -kubectl logs -n juwan -l app=your-service --tail=100 - -# 检查 Service -kubectl get svc -n juwan - -# 测试 DNS 解析 -kubectl exec -it {pod-name} -n juwan -- nslookup your-service-svc.juwan.svc.cluster.local -``` - -### 问题 2: Envoy 配置错误 - -```bash -# 查看 Envoy Pod 日志 -kubectl logs -n juwan -l app=envoy-gateway --tail=50 - -# 常见错误 -# - "no such field" → YAML 字段名与 Envoy 版本不兼容 -# - "Unknown cluster" → Envoy 配置中缺少 cluster 定义 -# - "Connection refused" → 后端服务未启动或 DNS 无法解析 -``` - -### 问题 3: JWT 认证失败 - -```bash -# 检查 JWT 配置是否正确 -kubectl get configmap envoy-config -n juwan -o yaml - -# 查看 jwks.json 是否存在 -kubectl exec -it {envoy-pod} -n juwan -- cat /etc/envoy/jwks.json - -# 验证 Token 格式 -curl -H "Authorization: Bearer {token}" http://localhost/api/users/1 -``` - ---- - -## 文件组织总结 - -``` -project-root/ -├── desc/ # 接口定义 -│ ├── api/ -│ │ ├── users.api # User API 定义 -│ │ └── product.api # ← 新增:Product API 定义 -│ ├── rpc/ -│ │ ├── users.proto # User RPC 定义 -│ │ └── product.proto # ← 新增:Product RPC 定义 -│ └── sql/ -│ -├── app/ # 应用代码 -│ ├── users/ -│ │ ├── api/ # User API 实现 -│ │ └── rpc/ # User RPC 实现(内部) -│ └── product/ # ← 新增:Product 服务 -│ ├── api/ -│ └── rpc/ -│ -├── deploy/ # K8s 部署 -│ ├── k8s/ -│ │ ├── envoy-gateway.yaml # ← 更新:添加新路由 -│ │ ├── base/ -│ │ └── service/ -│ │ ├── user/ -│ │ │ ├── user-api.yaml -│ │ │ └── user-rpc.yaml -│ │ └── product/ # ← 新增:Product 部署文件 -│ │ ├── product-api.yaml -│ │ └── product-rpc.yaml -│ └── envoy/ # 参考配置 -│ -└── docs/ # 文档 - └── PROJECT_GUIDE.md # ← 本文件 -``` - ---- - -希望这份指南能帮助你快速上手项目!有任何问题欢迎提出 📝 diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 9cec6a9..0000000 --- a/docs/README.md +++ /dev/null @@ -1,108 +0,0 @@ -# Envoy Gateway Configuration - -This document explains how the Envoy unified ingress gateway is configured and how to modify it. - -## Files - -- deploy/k8s/envoy/envoy.yaml: ConfigMap + Deployment + Service for Envoy - -## Current Behavior - -- Envoy listens on port 8080 in the Pod and exposes port 80 via a ClusterIP Service. -- Route `/api/users` to `user-api-svc:8888`. -- Route `/api/email` to `email-api-svc:8888`. -- Route `/healthz` returns `200 ok` directly from gateway. -- Unknown routes return `404` from gateway. - -## Routing - -In envoy.yaml, routes are defined under: - -static_resources -> listeners -> http_connection_manager -> route_config -> virtual_hosts - -The current routing rules are: - -- `prefix: /api/users` -> `cluster: user_api_cluster` -- `prefix: /api/email` -> `cluster: email_api_cluster` -- `path: /healthz` -> direct response `200` -- `prefix: /` -> direct response `404` - -To add a new HTTP service, add a new route above the default route and define a new cluster. - -Example: route `/api/order` to `order-api-svc:8899` - -1) Add a route match: - -- match: - prefix: "/api/order" - route: - cluster: order_api_cluster - -1) Add a cluster: - -- name: order_api_cluster - connect_timeout: 2s - type: STRICT_DNS - lb_policy: ROUND_ROBIN - load_assignment: - cluster_name: order_api_cluster - endpoints: - - lb_endpoints: - - endpoint: - address: - socket_address: - address: order-api-svc.juwan.svc.cluster.local - port_value: 8899 - -## CSRF Protection (Double Cookie) - -Envoy uses a Lua filter for double-cookie CSRF validation: - -- Safe methods (GET/HEAD/OPTIONS): - - If missing, Envoy auto-issues two cookies: - - `csrf_token` - - `csrf_guard` -- Unsafe methods (POST/PUT/PATCH/DELETE, etc): - - Requires BOTH headers: - - `X-CSRF-Token` - - `X-CSRF-Guard` - - Requires BOTH cookies: - - `csrf_token` - - `csrf_guard` - - Header values must exactly match cookie values, otherwise Envoy returns `403`. - -If you want different cookie or header names, update these constants in Lua: - -- `TOKEN_COOKIE` -- `GUARD_COOKIE` -- `TOKEN_HEADER` -- `GUARD_HEADER` - -To relax or tighten rules, edit the functions: - -- is_safe(method) -- envoy_on_request(request_handle) - -## Cookie Attributes - -Current Set-Cookie: - -- `csrf_token=; Path=/; SameSite=Strict` -- `csrf_guard=; Path=/; SameSite=Strict` - -## Deployment - -Apply or update: - -kubectl apply -f deploy/k8s/envoy/envoy.yaml - -## Common Changes - -- Change listening port: - - Update listener port_value and Service targetPort/port. -- Change service namespace: - - Update cluster DNS addresses (e.g. `service.ns.svc.cluster.local`). -- Add more services: - - Add route + add cluster, as shown above. -- Update CSRF policy: - - Edit Lua validation logic in `envoy.filters.http.lua`. diff --git a/docs/deployment-troubleshooting.md b/docs/deployment-troubleshooting.md deleted file mode 100644 index eec8a7e..0000000 --- a/docs/deployment-troubleshooting.md +++ /dev/null @@ -1,385 +0,0 @@ -# Kubernetes 部署问题排查与解决记录 - -**日期**: 2026年2月23日 -**问题**: user-rpc 和 Redis 部署失败 -**状态**: 已诊断,解决中 - ---- - -## 📋 问题描述 - -执行 `kubectl apply -f test.yaml` 后,资源虽然创建成功,但实际的应用 pods 并未正常运行: - -``` -kubectl apply -f ..\test.yaml -✓ deployment.apps/user-rpc created -✓ service/user-rpc-svc created -✓ horizontalpodautoscaler.autoscaling/user-rpc-hpa-c created -✓ horizontalpodautoscaler.autoscaling/user-rpc-hpa-m created -✓ redisreplication.redis.redis.opstreelabs.in/user-redis created -✓ redissentinel.redis.redis.opstreelabs.in/user-redis-sentinel created -✓ cluster.postgresql.cnpg.io/user-db created -``` - -但执行 `kubectl get all` 后,发现: -- ❌ **user-rpc pods 未创建**(Deployment 0/3 replicas ready) -- ❌ **Redis pods 未创建**(RedisReplication 资源存在但无 pods) -- ✅ user-db pods 正常运行(3/3) - ---- - -## 🔍 排查过程 - -### 第一步:检查 Deployment 状态 - -```bash -kubectl describe deployment user-rpc -``` - -**发现**: -``` -Conditions: - Type Status Reason - ---- ------ ------ - Progressing True NewReplicaSetCreated - Available False MinimumReplicasUnavailable - ReplicaFailure True FailedCreate -``` - -### 第二步:检查 ReplicaSet 详情 - -```bash -kubectl describe replicaset user-rpc-6bf77fbcd9 -``` - -**发现关键错误**: -``` -Events: - Type Reason Age From Message - ---- ------ ---- ---- ------- - Warning FailedCreate 3m53s replicaset-controller Error creating: - pods "user-rpc-6bf77fbcd9-" is forbidden: error looking up service - account default/find-endpoints: serviceaccount "find-endpoints" not found -``` - -**问题 #1 诊断完成**:❌ **缺失 ServiceAccount "find-endpoints"** - -### 第三步:检查现有 ServiceAccounts - -```bash -kubectl get serviceaccount -``` - -**结果**: -``` -NAME AGE -cluster-example 4d10h -default 13d -redis-operator 9h -user-db 4m9s -``` - -确认 `find-endpoints` 不存在。 - -### 第四步:检查 Secrets - -```bash -kubectl get secrets -``` - -**结果**:默认 secrets 都存在,包括: -- ✅ user-db-app -- ✅ user-redis -- ✅ user-db-ca, user-db-replication, user-db-server - -### 第五步:检查 Redis 部署 - -```bash -kubectl get redisreplication -kubectl get pods | grep redis -``` - -**发现**: -- ✅ RedisReplication 资源存在 -- ❌ Redis pods **完全没有被创建** - -**问题 #2 诊断**:❌ **Redis Operator 未响应 RedisReplication 资源** - ---- - -## 🔧 第一次修复尝试 - -### 创建缺失的 ServiceAccount - -```bash -kubectl create serviceaccount find-endpoints -``` - -**结果**:✅ ServiceAccount 创建成功 - -### 重启 Deployment - -```bash -kubectl rollout restart deployment user-rpc -``` - -**等待 5-10 秒后重新检查**: - -```bash -kubectl get pods -o wide -``` - -**新的发现**: - -``` -NAME READY STATUS RESTARTS AGE -user-rpc-66f97fbdcc-ws7rc 0/1 ErrImagePull 0 26s -user-rpc-6bf77fbcd9-njm2z 0/1 ErrImagePull 0 29s -user-rpc-6bf77fbcd9-nwjtw 0/1 ImagePullBackOff 0 29s -user-rpc-6bf77fbcd9-wjrf8 0/1 ErrImagePull 0 29s -``` - -✅ **好消息**:Pods 现在被创建了!(说明 ServiceAccount 问题已解决) -❌ **新问题**:镜像拉取失败 - ---- - -## 🐛 根因分析 - -### 问题 #1:缺失 ServiceAccount ✅ 已解决 - -**根本原因**:test.yaml 的 Deployment manifest 指定了: -```yaml -spec: - template: - spec: - serviceAccountName: find-endpoints # 这个 ServiceAccount 不存在 -``` - -但没有在 test.yaml 中创建 ServiceAccount 资源。 - -**解决方案**: -```bash -kubectl create serviceaccount find-endpoints -``` - -或在 test.yaml 中添加: -```yaml ---- -apiVersion: v1 -kind: ServiceAccount -metadata: - name: find-endpoints - namespace: default -``` - ---- - -### 问题 #2:镜像拉取失败 ❌ 需要修复 - -```bash -kubectl describe pod user-rpc-6bf77fbcd9-njm2z -``` - -**详细错误日志**: - -``` -Events: - Warning Failed 38s kubelet Failed to pull image - "103.236.53.208:4418/library/user-rpc@sha256:76b27d3eb4d5d44e...": - Error response from daemon: Get "https://103.236.53.208:4418/v2/": - context deadline exceeded (Client.Timeout exceeded while awaiting headers) - - Warning Failed 23s kubelet Failed to pull image - "103.236.53.208:4418/library/user-rpc@sha256:76b27d3eb4d5d44e...": - http: server gave HTTP response to HTTPS client -``` - -**根本原因分析**: - -1. **网络连接失败**:`context deadline exceeded` - 无法连接到镜像仓库 -2. **协议不匹配**:`http: server gave HTTP response to HTTPS client` - - - 地址 `103.236.53.208:4418` 应该是 HTTP 而不是 HTTPS - - Docker daemon 尝试用 HTTPS 连接,但服务器使用 HTTP - -**可能原因**: -- 镜像仓库地址错误或不可访问 -- 镜像仓库需要特定的网络配置 -- 仓库服务器离线或配置不当 - ---- - -### 问题 #3:Redis 部署失败 ❌ 需要诊断 - -**现象**: -- RedisReplication 和 RedisSentinel CRD 资源创建成功 -- 但没有对应的 Redis pods 被创建 -- `kubectl get pods | grep redis` 返回空 - -**可能原因**: - -1. **Redis Operator 未正常工作** - - Operator pod 可能存在问题 - - Operator 未能监听到新的 RedisReplication 资源 - -2. **CRD 或 API 版本问题** - - manifest 中使用的 API 版本 `v1beta2` 可能不匹配 Operator 版本 - -3. **资源限制或权限问题** - - Operator 无权限创建 pods - - 集群资源限制阻止了 pod 创建 - ---- - -## ✅ 已执行的修复 - -| # | 问题 | 修复方法 | 状态 | -|---|------|---------|------| -| 1 | 缺失 ServiceAccount | `kubectl create serviceaccount find-endpoints` | ✅ 完成 | -| 2 | 镜像拉取失败 | 需要更新镜像地址或修复网络 | ⏳ 待处理 | -| 3 | Redis pods 未创建 | 需要诊断 Operator 日志 | ⏳ 待诊断 | - ---- - -## 🚀 下一步解决方案 - -### 优先级 1:修复 user-rpc 镜像拉取 - -**选项 A:使用本地/内部镜像** -```yaml -# 修改 test.yaml 中的镜像地址 -image: localhost:5000/user-rpc:latest # 本地私有仓库 -# 或 -image: user-rpc:latest # 本地镜像(如果已通过 docker load 导入) -``` - -**选项 B:修复仓库地址** -```yaml -# 如果 103.236.53.208:4418 确实是正确仓库 -image: http://103.236.53.208:4418/library/user-rpc:latest # 显式使用 HTTP -``` - -**验证步骤**: -```bash -# 检查镜像仓库连接性 -curl -v http://103.236.53.208:4418/v2/ -``` - -### 优先级 2:诊断 Redis Operator - -```bash -# 查看 Operator 日志 -kubectl logs -l app.kubernetes.io/name=redis-operator -f - -# 查看 Operator pod -kubectl get pods -A | grep redis-operator - -# 查看 RedisReplication 详细信息 -kubectl describe redisreplication user-redis - -# 检查 Operator 权限(RBAC) -kubectl get role,rolebinding,clusterrole,clusterrolebinding | grep redis -``` - -### 优先级 3:增强 test.yaml - -建议在 test.yaml 中添加缺失的资源定义: - -```yaml ---- -apiVersion: v1 -kind: ServiceAccount -metadata: - name: find-endpoints - namespace: default - ---- -apiVersion: v1 -kind: Secret -metadata: - name: registry-credentials - namespace: default -type: kubernetes.io/dockercfg -data: - .dockercfg: # 如果需要私有仓库认证 -``` - ---- - -## 📊 当前集群状态 - -### Pods 状态总结 - -| 应用 | 期望副本 | 实际运行 | 状态 | -|------|---------|---------|------| -| user-db | 3 | 3 | ✅ 正常 | -| user-rpc | 3 | 0 | ❌ 镜像拉取失败 | -| Redis | 3 | 0 | ❌ Operator 未创建 | -| Sentinel | 3 | 0 | ❌ Operator 未创建 | - -### Services 状态 - -``` -✅ kubernetes (内置) -✅ user-rpc-svc:9001 -✅ user-db-r:5432 (只读副本) -✅ user-db-ro:5432 (只读副本) -✅ user-db-rw:5432 (读写主副本) -``` - -### HPA 配置 - -``` -✅ user-rpc-hpa-c (CPU 目标: 80%) - 无法工作(pods 未运行) -✅ user-rpc-hpa-m (Memory 目标: 80%) - 无法工作(pods 未运行) -``` - ---- - -## 📝 关键命令速查表 - -```bash -# 查看 Deployment 状态 -kubectl describe deployment user-rpc - -# 查看 ReplicaSet 错误事件 -kubectl describe replicaset user-rpc-6bf77fbcd9 - -# 查看 Pod 详细错误 -kubectl describe pod user-rpc-6bf77fbcd9-njm2z - -# 查看 Pod 日志 -kubectl logs user-rpc-6bf77fbcd9-njm2z - -# 查看所有事件(按时间排序) -kubectl get events --sort-by='.lastTimestamp' - -# 查看特定命名空间的所有资源 -kubectl get all -n default - -# 重新启动 deployment(强制重新创建 pods) -kubectl rollout restart deployment user-rpc - -# 查看 Operator 日志 -kubectl logs -l app.kubernetes.io/name=redis-operator - -# 检查 CRD 注册状态 -kubectl api-resources | grep redis -``` - ---- - -## 🎯 总结 - -| 问题 | 原因 | 解决状态 | -|------|------|---------| -| **ServiceAccount 缺失** | manifest 中声明但未创建 | ✅ **已解决** | -| **镜像拉取失败** | 仓库地址不可达或协议不匹配 | ⏳ **待处理** | -| **Redis 未部署** | Operator 未响应 CRD | ⏳ **待诊断** | - -**建议行动**: -1. 确认/修复 user-rpc 镜像地址 -2. 诊断 Redis Operator 状态 -3. 验证所有依赖的 ServiceAccounts 和 Secrets 是否存在 -4. 考虑在 test.yaml 中添加完整的资源定义,避免手工创建 - diff --git a/docs/email-kafka-consumer-test-guide.md b/docs/email-kafka-consumer-test-guide.md deleted file mode 100644 index 2f04615..0000000 --- a/docs/email-kafka-consumer-test-guide.md +++ /dev/null @@ -1,266 +0,0 @@ -# Email Consumer Kafka 投递与日志验证实验手册 - -## 1. 实验目标 - -验证 `email-task` consumer 是否能正常消费 Kafka 消息,并在日志中打印消费内容。 - -本实验同时给出两种验证方式: - -1. `kubectl logs` 直接查看 Pod 日志 -2. Grafana + Loki 查看聚合日志 - ---- - -## 2. 实验前提 - -### 2.1 需要满足的运行状态 - -```bash -kubectl -n juwan get pods -l app=email-task -kubectl -n kafka get pods -kubectl -n monitoring get pods -``` - -预期: - -- `email-task` 至少 1 个 Pod 为 `Running` -- Kafka 集群有可用 broker(如 `my-cluster-kafka-pool-0`) -- `loki/promtail/grafana` 为 `Running`(若需要 Loki 验证) - -### 2.2 本次实验使用的关键配置 - -来自 `app/email/mq/etc/email.yaml`: - -- Broker: `my-cluster-kafka-bootstrap.kafka.svc.cluster.local:9092` -- Topic: `email-task` -- Group: `email-consumer-group` - ---- - -## 3. 实验步骤(详细) - -## 步骤 1:确认 Topic 存在 - -### 目的 - -避免消息投递到不存在的 Topic,导致消费端无数据。 - -### 指令 - -```bash -kubectl -n kafka exec my-cluster-kafka-pool-0 -- \ - /opt/kafka/bin/kafka-topics.sh \ - --bootstrap-server my-cluster-kafka-bootstrap:9092 \ - --list -``` - -### 预期结果 - -输出中包含: - -- `email-task` - ---- - -## 步骤 2:投递一条最小测试消息(纯文本) - -### 目的 - -先验证链路通路(producer -> kafka -> consumer)是否正常,不引入 JSON 转义复杂度。 - -### 指令 - -```bash -kubectl -n kafka exec my-cluster-kafka-pool-0 -- /bin/bash -lc \ -"printf 'test-email-message\\n' | \ -/opt/kafka/bin/kafka-console-producer.sh \ ---bootstrap-server my-cluster-kafka-bootstrap:9092 \ ---topic email-task" -``` - -### 预期结果 - -命令正常返回(通常无额外输出)。 - ---- - -## 步骤 3:查看 consumer 日志(kubectl 直查) - -### 目的 - -确认 consumer 实际收到消息并执行日志打印。 - -### 指令(回看最近日志) - -```bash -kubectl -n juwan logs -l app=email-task --tail=120 -``` - -### 指令(实时追踪) - -```bash -kubectl -n juwan logs -l app=email-task -f --since=10m -``` - -### 预期日志示例 - -```text -Consume get message key: , value: test-email-message -``` - -说明: - -- key 为空是正常的(本次 producer 未设置 key) -- value 为投递内容,说明消费链路正常 - ---- - -## 步骤 4:投递业务消息(验证码 JSON) - -### 目的 - -模拟真实业务 payload,验证 consumer 对业务消息格式的处理。 - -### 指令 - -```bash -kubectl -n kafka exec my-cluster-kafka-pool-0 -- /bin/bash -lc "cat <<'EOF' | \ -/opt/kafka/bin/kafka-console-producer.sh \ ---bootstrap-server my-cluster-kafka-bootstrap:9092 \ ---topic email-task -{\"type\":\"verification_code\",\"email\":\"test@example.com\",\"code\":\"123456\",\"scene\":\"login\",\"expired_minutes\":5} -EOF" -``` - -### 预期结果 - -- producer 正常返回 -- `email-task` 日志可看到包含 JSON 的消费日志 - ---- - -## 步骤 5:投递业务消息(活动通知 JSON) - -### 目的 - -验证另一类业务消息(活动通知)通路。 - -### 指令 - -```bash -kubectl -n kafka exec my-cluster-kafka-pool-0 -- /bin/bash -lc "cat <<'EOF' | \ -/opt/kafka/bin/kafka-console-producer.sh \ ---bootstrap-server my-cluster-kafka-bootstrap:9092 \ ---topic email-task -{\"type\":\"activity_notice\",\"email\":\"test@example.com\",\"title\":\"春季活动\",\"content\":\"满100减20\",\"activity_id\":\"A20260225\"} -EOF" -``` - -### 预期结果 - -- producer 正常返回 -- consumer 日志出现活动消息内容 - ---- - -## 步骤 6:使用 Loki/Grafana 验证(可选) - -### 目的 - -确认日志采集链路(Promtail -> Loki -> Grafana)正常,便于后续线上排查。 - -### 6.1 打开 Grafana - -```bash -kubectl port-forward -n monitoring svc/grafana 3000:3000 -``` - -浏览器:`http://localhost:3000` - -### 6.2 在 Explore 中查询 - -使用 Loki 数据源,输入: - -```logql -{job="kubernetes-pods", namespace="juwan", app="email-task"} |= "Consume get message" -``` - -若没有结果: - -1. 把时间范围调大到 `Last 6 hours`/`Last 24 hours` -2. 放宽查询条件: - -```logql -{job="kubernetes-pods", namespace="juwan", pod=~"email-task-.*"} -``` - ---- - -## 4. 一键复现实验命令(顺序执行) - -```bash -# 1) 查看 topic -kubectl -n kafka exec my-cluster-kafka-pool-0 -- /opt/kafka/bin/kafka-topics.sh --bootstrap-server my-cluster-kafka-bootstrap:9092 --list - -# 2) 发测试消息 -kubectl -n kafka exec my-cluster-kafka-pool-0 -- /bin/bash -lc "printf 'test-email-message\\n' | /opt/kafka/bin/kafka-console-producer.sh --bootstrap-server my-cluster-kafka-bootstrap:9092 --topic email-task" - -# 3) 看 consumer 日志 -kubectl -n juwan logs -l app=email-task --tail=120 -``` - ---- - -## 5. 常见问题与处理 - -### 问题 1:发消息命令报引号/EOF错误 - -现象:`unexpected EOF while looking for matching`。 - -原因:Shell 引号转义不正确。 - -处理: - -- 先用纯文本消息验证链路 -- JSON 使用 here-doc(`cat <<'EOF'`)方式,避免转义混乱 - -### 问题 2:发了消息但 consumer 无日志 - -排查顺序: - -1. `email-task` 是否 Running -2. Topic 是否正确(`email-task`) -3. consumer group 是否一致(`email-consumer-group`) -4. 查看 Pod 实时日志(`-f`) -5. 若只看 Loki,请放大时间窗口并放宽标签条件 - -### 问题 3:Loki 查不到但 kubectl logs 能看到 - -说明业务正常,问题在日志采集查询链路: - -- 检查 Promtail target 是否 ready -- 检查 Loki 查询标签/时间范围 -- 参考 `docs/loki-log-troubleshooting.md` - ---- - -## 6. 实验结论判定标准 - -满足以下任一即可判定消费链路可用: - -1. `kubectl logs` 出现:`Consume get message ...` -2. Grafana Loki 查询出现对应消费日志 - -若两者都出现,说明: - -- Kafka 投递正常 -- Consumer 消费正常 -- 日志采集与检索链路正常 - ---- - -## 7. 关联文档 - -- Loki 使用:`docs/loki-usage-guide.md` -- Loki 排错:`docs/loki-log-troubleshooting.md` -- Email 部署排错:`docs/email-task-deployment-troubleshooting.md` diff --git a/docs/email-task-deployment-troubleshooting.md b/docs/email-task-deployment-troubleshooting.md deleted file mode 100644 index 2b061b2..0000000 --- a/docs/email-task-deployment-troubleshooting.md +++ /dev/null @@ -1,147 +0,0 @@ -# Email Task 部署故障排查与修复记录 - -## 1. 问题现象 - -部署 `email-task` 时出现调度失败: - -```text -Warning FailedScheduling 0/1 nodes are available: 1 Insufficient memory. -no new claims to deallocate, preemption: 0/1 nodes are available: -1 No preemption victims found for incoming pod. -``` - -表现为: - -- `Deployment` 期望副本无法全部就绪 -- `Pod` 长时间 `Pending` - ---- - -## 2. 排查思路 - -按以下顺序排查: - -1. **看部署配置是否过高请求**(`requests/limits` + `replicas`) -2. **看节点可分配资源和已分配资源**(确认是否真的是内存不足) -3. **看滚动策略是否会额外拉起新 Pod**(`maxSurge` 可能放大内存压力) -4. **看容器健康检查是否匹配服务类型**(任务型服务不一定监听端口) - ---- - -## 3. 关键排查命令 - -### 3.1 查看节点可分配资源 - -```powershell -kubectl get nodes -o custom-columns=NAME:.metadata.name,ALLOCATABLE_CPU:.status.allocatable.cpu,ALLOCATABLE_MEM:.status.allocatable.memory -``` - -### 3.2 查看部署与 Pod 状态 - -```powershell -kubectl -n juwan get deploy email-task -o wide -kubectl -n juwan get pods -l app=email-task -o wide -kubectl -n juwan describe pod -l app=email-task -``` - -### 3.3 查看节点资源分配占比 - -```powershell -kubectl describe node minikube -``` - -关注输出中的 `Allocated resources`: - -- `memory requests` 已接近节点上限(本次约 97%) - -### 3.4 查看部署策略与探针配置 - -```powershell -kubectl -n juwan get deploy email-task -o yaml -kubectl -n juwan logs deploy/email-task --tail=120 -``` - ---- - -## 4. 根因分析 - -本次是**组合问题**: - -1. **内存请求过高 + 副本过多** - - 原始配置:`replicas=3` - - 每个 Pod 请求 `memory=512Mi` - - 单节点场景下,叠加现有业务后无法继续调度 - -2. **滚动更新默认 `maxSurge=25%`** - - 更新时可能额外起新 Pod,进一步触发内存不足 - -3. **探针不匹配服务行为** - - 原配置为 `tcpSocket:8080` 探针 - - 实际 `email-task` 是任务型服务,日志显示启动后并未提供该端口服务 - - 导致 `Readiness/Liveness` 持续失败 - ---- - -## 5. 修复方案 - -仅修改文件: - -- `deploy/k8s/service/email/email.yaml` - -### 5.1 降低资源请求与副本基线 - -- `replicas: 3 -> 1` -- `requests.cpu: 500m -> 100m` -- `requests.memory: 512Mi -> 128Mi` -- `limits.cpu: 1000m -> 500m` -- `limits.memory: 1024Mi -> 512Mi` - -### 5.2 调整 HPA 基线与上限 - -- 两个 HPA(CPU / Memory)统一: - - `minReplicas: 3 -> 1` - - `maxReplicas: 10 -> 3` - -### 5.3 调整滚动发布策略 - -- `strategy.rollingUpdate.maxSurge: 0` -- `strategy.rollingUpdate.maxUnavailable: 1` - -目的:避免滚动期间额外拉起 Pod 造成瞬时内存不足。 - -### 5.4 移除不适配的 8080 TCP 探针 - -移除: - -- `readinessProbe.tcpSocket:8080` -- `livenessProbe.tcpSocket:8080` - ---- - -## 6. 修复执行命令 - -```powershell -kubectl apply -f deploy/k8s/service/email/email.yaml -kubectl -n juwan rollout restart deploy/email-task -kubectl -n juwan rollout status deploy/email-task --timeout=180s -kubectl -n juwan get pods -l app=email-task -o wide -kubectl -n juwan describe pods -l app=email-task | Select-String -Pattern 'FailedScheduling|Unhealthy|Warning|Events|Node:' -``` - ---- - -## 7. 修复结果 - -- `Deployment` 滚动成功 -- 新 Pod 成功调度并 `Running` -- 无新的 `FailedScheduling` 与 `Unhealthy` 事件 - ---- - -## 8. 后续建议 - -1. 若要恢复多副本,先按节点容量逐步上调(建议先 2 副本并观测)。 -2. 为任务型服务设计更合适的健康检查方式: - - 可考虑 `exec` 探针或业务自检端点。 -3. 在单节点开发环境中统一降低默认 `requests`,防止多个服务叠加后调度失败。 -4. 如需高可用,建议扩容节点而不是仅依赖压缩资源。 diff --git a/docs/gozero-redis-configuration.md b/docs/gozero-redis-configuration.md deleted file mode 100644 index b8f0e0f..0000000 --- a/docs/gozero-redis-configuration.md +++ /dev/null @@ -1,1497 +0,0 @@ -# Go-Zero 框架 Redis 配置完全指南 - -**框架版本:** go-zero v1.5+ -**Redis 版本:** 7.0.12 -**部署环境:** Kubernetes (juwan namespace) -**文档日期:** 2026年2月22日 - ---- - -## 📋 目录 - -1. [配置概览](#配置概览) -2. [单节点模式](#单节点模式) -3. [Sentinel 哨兵模式](#sentinel-哨兵模式) -4. [集群模式](#集群模式) -5. [配置项详解](#配置项详解) -6. [代码实现](#代码实现) -7. [常用操作示例](#常用操作示例) -8. [高级特性](#高级特性) -9. [性能优化](#性能优化) -10. [故障排查](#故障排查) -11. [最佳实践](#最佳实践) - ---- - -## 🎯 配置概览 - -### Go-Zero Redis 支持的模式 - -| 模式 | Type 值 | 用途 | 高可用 | 推荐度 | -|-----|---------|------|--------|--------| -| **单节点** | `node` | 开发/测试 | ❌ | ⭐⭐ | -| **Sentinel** | `sentinel` | 生产环境 | ✅ | ⭐⭐⭐⭐⭐ | -| **集群** | `cluster` | 大规模分片 | ✅ | ⭐⭐⭐⭐ | - -### 配置文件位置 - -``` -app/users/rpc/ -├── etc/ -│ └── pb.yaml ← Redis 配置写在这里 -├── internal/ -│ ├── config/ -│ │ └── config.go ← 定义配置结构 -│ └── svc/ -│ └── serviceContext.go ← 初始化 Redis 客户端 -└── pb.go -``` - ---- - -## 🔵 单节点模式 - -### 适用场景 - -- ✅ 开发环境 -- ✅ 测试环境 -- ✅ POC 演示 -- ❌ 生产环境(无高可用) - -### 配置文件 - -**`app/users/rpc/etc/pb.yaml`** -```yaml -Name: user.rpc -ListenOn: 0.0.0.0:9001 - -# Redis 单节点配置 -Redis: - Host: user-redis-master.juwan.svc.cluster.local:6379 - Type: node - Pass: ${REDIS_PASSWORD} # 从环境变量读取 - # Db: 0 # 可选,默认 0 - # MaxIdle: 8 # 可选,连接池最大闲置连接数 - # MaxActive: 0 # 可选,连接池最大活跃连接数,0 表示无限制 - -Etcd: - Hosts: - - etcd-service.juwan.svc.cluster.local:2379 - Key: user.rpc -``` - -### Config 结构 - -**`app/users/rpc/internal/config/config.go`** -```go -package config - -import ( - "github.com/zeromicro/go-zero/core/stores/redis" - "github.com/zeromicro/go-zero/zrpc" -) - -type Config struct { - zrpc.RpcServerConf - Redis redis.RedisConf // Redis 配置 -} -``` - -### ServiceContext 初始化 - -**`app/users/rpc/internal/svc/serviceContext.go`** -```go -package svc - -import ( - "github.com/zeromicro/go-zero/core/stores/redis" - "juwan-backend/app/users/rpc/internal/config" -) - -type ServiceContext struct { - Config config.Config - Redis *redis.Redis -} - -func NewServiceContext(c config.Config) *ServiceContext { - return &ServiceContext{ - Config: c, - Redis: redis.MustNewRedis(c.Redis), // 初始化 Redis - } -} -``` - -### 使用示例 - -**`app/users/rpc/internal/logic/getUsersByIdLogic.go`** -```go -package logic - -import ( - "context" - "encoding/json" - "fmt" - "time" - - "juwan-backend/app/users/rpc/internal/svc" - "juwan-backend/app/users/rpc/pb" - - "github.com/zeromicro/go-zero/core/logx" -) - -type GetUsersByIdLogic struct { - ctx context.Context - svcCtx *svc.ServiceContext - logx.Logger -} - -func NewGetUsersByIdLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUsersByIdLogic { - return &GetUsersByIdLogic{ - ctx: ctx, - svcCtx: svcCtx, - Logger: logx.WithContext(ctx), - } -} - -func (l *GetUsersByIdLogic) GetUsersById(in *pb.GetUsersByIdReq) (*pb.GetUsersByIdResp, error) { - // 缓存 key - cacheKey := fmt.Sprintf("user:%d", in.Id) - - // 1. 尝试从缓存获取 - cached, err := l.svcCtx.Redis.Get(cacheKey) - if err == nil && cached != "" { - // 缓存命中 - var user pb.User - if err := json.Unmarshal([]byte(cached), &user); err == nil { - l.Logger.Infof("Cache hit for user:%d", in.Id) - return &pb.GetUsersByIdResp{User: &user}, nil - } - } - - // 2. 缓存未命中,从数据库查询 - l.Logger.Infof("Cache miss for user:%d, querying DB", in.Id) - user := l.fetchUserFromDB(in.Id) - if user == nil { - return nil, fmt.Errorf("user not found") - } - - // 3. 写入缓存(1小时过期) - userJSON, _ := json.Marshal(user) - err = l.svcCtx.Redis.Setex(cacheKey, string(userJSON), 3600) - if err != nil { - l.Logger.Errorf("Failed to set cache: %v", err) - } - - return &pb.GetUsersByIdResp{User: user}, nil -} - -func (l *GetUsersByIdLogic) fetchUserFromDB(id int64) *pb.User { - // 实际从数据库查询 - // ... - return &pb.User{Id: id, Name: "John Doe"} -} -``` - ---- - -## 🟡 Sentinel 哨兵模式 - -### 适用场景 - -- ✅✅✅ **生产环境强烈推荐** -- ✅ 自动故障转移 -- ✅ 高可用架构 -- ✅ 主从自动切换 - -### 配置文件 - -**`app/users/rpc/etc/pb.yaml`** -```yaml -Name: user.rpc -ListenOn: 0.0.0.0:9001 - -# Redis Sentinel 配置(推荐生产环境使用) -Redis: - - Host: user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379 - Type: sentinel - Pass: ${REDIS_PASSWORD} - -# 或者使用完整配置 -# Redis: -# Host: user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379 -# Type: sentinel -# Pass: ${REDIS_PASSWORD} -# # Sentinel 特有配置 -# MasterName: mymaster # Sentinel 主节点名称,默认 mymaster - -Etcd: - Hosts: - - etcd-service.juwan.svc.cluster.local:2379 - Key: user.rpc -``` - -### Config 结构(同单节点) - -**`app/users/rpc/internal/config/config.go`** -```go -package config - -import ( - "github.com/zeromicro/go-zero/core/stores/redis" - "github.com/zeromicro/go-zero/zrpc" -) - -type Config struct { - zrpc.RpcServerConf - Redis redis.RedisConf // 支持所有模式 -} -``` - -### ServiceContext 初始化(同单节点) - -**`app/users/rpc/internal/svc/serviceContext.go`** -```go -package svc - -import ( - "github.com/zeromicro/go-zero/core/stores/redis" - "juwan-backend/app/users/rpc/internal/config" -) - -type ServiceContext struct { - Config config.Config - Redis *redis.Redis -} - -func NewServiceContext(c config.Config) *ServiceContext { - // go-zero 会根据 Type 自动选择连接模式 - return &ServiceContext{ - Config: c, - Redis: redis.MustNewRedis(c.Redis), - } -} -``` - -### Sentinel 配置详解 - -**完整配置选项:** -```yaml -Redis: - Host: user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379 - Type: sentinel - Pass: ${REDIS_PASSWORD} - - # Sentinel 特有配置 - MasterName: mymaster # Sentinel 监控的主节点名称 - - # 连接池配置(可选) - MaxIdle: 8 # 最大闲置连接数 - MaxActive: 0 # 最大活跃连接数,0 表示无限制 - IdleTimeout: 300 # 闲置连接超时时间(秒) - - # 超时配置(可选) - ConnectTimeout: 5000 # 连接超时(毫秒) - ReadTimeout: 3000 # 读超时(毫秒) - WriteTimeout: 3000 # 写超时(毫秒) -``` - -### 优势说明 - -```go -// Sentinel 模式的自动故障处理流程: - -// 1. 应用连接到 Sentinel -app → Sentinel Service (26379) - -// 2. Sentinel 返回当前主节点地址 -Sentinel → app: "主节点在 10.244.1.10:6379" - -// 3. 应用连接到主节点进行读写 -app → Redis Master (10.244.1.10:6379) - -// 4. 主节点故障,Sentinel 检测到 -Redis Master (✗ 宕机) -Sentinel → 检测 → 投票 → 提升新主节点 - -// 5. 应用下次请求时自动连接到新主节点 -app → Sentinel → "新主节点在 10.244.2.20:6379" -app → New Redis Master (10.244.2.20:6379) - -// 整个过程应用无需重启,自动完成切换! -``` - ---- - -## 🔴 集群模式 - -### 适用场景 - -- ✅ 大规模数据(需要分片) -- ✅ 超高并发 -- ✅ 数据量超过单机内存 -- ⚠️ 配置和运维复杂度高 - -### 配置文件 - -**`app/users/rpc/etc/pb.yaml`** -```yaml -Name: user.rpc -ListenOn: 0.0.0.0:9001 - -# Redis Cluster 配置 -Redis: - - Host: redis-cluster-0.redis-cluster.juwan.svc.cluster.local:6379 - - Host: redis-cluster-1.redis-cluster.juwan.svc.cluster.local:6379 - - Host: redis-cluster-2.redis-cluster.juwan.svc.cluster.local:6379 - Type: cluster - Pass: ${REDIS_PASSWORD} - -Etcd: - Hosts: - - etcd-service.juwan.svc.cluster.local:2379 - Key: user.rpc -``` - -### Config 结构 - -**`app/users/rpc/internal/config/config.go`** -```go -package config - -import ( - "github.com/zeromicro/go-zero/core/stores/redis" - "github.com/zeromicro/go-zero/zrpc" -) - -type Config struct { - zrpc.RpcServerConf - Redis redis.RedisConf -} -``` - -### 集群模式特点 - -**数据分片:** -``` -应用请求 - ↓ -根据 key 计算 hash slot (0-16383) - ↓ -路由到对应的分片节点 - ↓ -┌─────────┬─────────┬─────────┐ -│ Shard 1 │ Shard 2 │ Shard 3 │ -│ 0-5460 │5461-10922│10923-16383│ -└─────────┴─────────┴─────────┘ -``` - -**注意事项:** -- ❌ 不支持多 key 操作(如 MGET, MSET)跨分片 -- ❌ 不支持事务(MULTI/EXEC)跨分片 -- ✅ 单 key 操作完全正常 -- ✅ 支持 hash tag 控制 key 分布 - ---- - -## 📚 配置项详解 - -### redis.RedisConf 完整配置 - -**结构定义:** -```go -type RedisConf struct { - Host string // Redis 地址 - Type string // 类型: node, sentinel, cluster - Pass string // 密码 - Db int // 数据库编号 (0-15),cluster 模式不支持 - - // Sentinel 模式专用 - MasterName string // Sentinel 主节点名称 - - // 连接池配置 - MaxIdle int // 最大闲置连接数 - MaxActive int // 最大活跃连接数,0 表示无限制 - IdleTimeout time.Duration // 闲置连接超时 - - // 超时配置 - ConnectTimeout time.Duration // 连接超时 - ReadTimeout time.Duration // 读超时 - WriteTimeout time.Duration // 写超时 - - // TLS 配置(可选) - Tls bool // 是否启用 TLS -} -``` - -### 各配置项说明 - -#### 1. Host(必填) - -**单节点模式:** -```yaml -Redis: - Host: user-redis-master.juwan.svc.cluster.local:6379 -``` - -**Sentinel 模式:** -```yaml -Redis: - Host: user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379 - # 注意:这里填 Sentinel 地址(端口 26379),不是 Redis 地址 -``` - -**集群模式:** -```yaml -Redis: - # 可以填任意一个节点,客户端会自动发现其他节点 - - Host: redis-cluster-0:6379 - - Host: redis-cluster-1:6379 - - Host: redis-cluster-2:6379 -``` - -#### 2. Type(必填) - -| 值 | 说明 | -|----|------| -| `node` | 单节点模式 | -| `sentinel` | Sentinel 哨兵模式 | -| `cluster` | 集群模式 | - -#### 3. Pass(强烈推荐) - -**从环境变量读取(推荐):** -```yaml -Redis: - Pass: ${REDIS_PASSWORD} -``` - -**硬编码(不推荐):** -```yaml -Redis: - Pass: "your-password" # ❌ 不安全 -``` - -#### 4. Db(可选,默认 0) - -**适用模式:** 仅 `node` 和 `sentinel` 模式 - -```yaml -Redis: - Db: 0 # 数据库编号 0-15 -``` - -**注意:** -- ❌ Cluster 模式不支持多数据库 -- ✅ 单节点和 Sentinel 支持 0-15 - -#### 5. MaxIdle(可选,默认 8) - -```yaml -Redis: - MaxIdle: 8 # 连接池中最大闲置连接数 -``` - -**建议值:** -- 低并发:`8` -- 中并发:`16` -- 高并发:`32` 或 `CPU 核心数 * 2` - -#### 6. MaxActive(可选,默认 0) - -```yaml -Redis: - MaxActive: 0 # 0 表示无限制 - # MaxActive: 100 # 或设置一个上限 -``` - -**建议值:** -- 开发环境:`0`(无限制) -- 生产环境:`100-500`(根据实际负载) - -#### 7. IdleTimeout(可选,默认 300 秒) - -```yaml -Redis: - IdleTimeout: 300 # 秒 -``` - -**说明:** 闲置连接超过此时间会被关闭 - -#### 8. 超时配置(可选) - -```yaml -Redis: - ConnectTimeout: 5000 # 连接超时 5 秒 - ReadTimeout: 3000 # 读超时 3 秒 - WriteTimeout: 3000 # 写超时 3 秒 -``` - ---- - -## 💻 代码实现 - -### 完整项目结构 - -``` -app/users/rpc/ -├── etc/ -│ ├── pb.yaml # 开发环境配置 -│ └── pb-prod.yaml # 生产环境配置 -├── internal/ -│ ├── config/ -│ │ └── config.go # 配置结构定义 -│ ├── svc/ -│ │ └── serviceContext.go # 服务上下文(初始化 Redis) -│ ├── logic/ -│ │ ├── addUsersLogic.go -│ │ ├── getUsersByIdLogic.go -│ │ └── ... -│ └── server/ -│ └── usercenterServer.go -├── pb/ -│ ├── users.pb.go -│ └── users_grpc.pb.go -└── usercenter.go # 主程序入口 -``` - -### 1. 配置文件示例 - -**开发环境 `etc/pb.yaml`:** -```yaml -Name: user.rpc -ListenOn: 0.0.0.0:9001 - -# 开发环境使用单节点 -Redis: - Host: localhost:6379 - Type: node - Pass: dev_password - Db: 0 - -Etcd: - Hosts: - - localhost:2379 - Key: user.rpc - -Log: - Level: info - Mode: console -``` - -**生产环境 `etc/pb-prod.yaml`:** -```yaml -Name: user.rpc -ListenOn: 0.0.0.0:9001 - -# 生产环境使用 Sentinel -Redis: - Host: user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379 - Type: sentinel - Pass: ${REDIS_PASSWORD} - MasterName: mymaster - MaxIdle: 16 - MaxActive: 100 - IdleTimeout: 300 - ConnectTimeout: 5000 - ReadTimeout: 3000 - WriteTimeout: 3000 - -Etcd: - Hosts: - - etcd-0.etcd.juwan.svc.cluster.local:2379 - - etcd-1.etcd.juwan.svc.cluster.local:2379 - - etcd-2.etcd.juwan.svc.cluster.local:2379 - Key: user.rpc - -Log: - Level: error - Mode: file - Path: /var/log/user-rpc - KeepDays: 7 -``` - -### 2. Config 定义 - -**`internal/config/config.go`** -```go -package config - -import ( - "github.com/zeromicro/go-zero/core/stores/redis" - "github.com/zeromicro/go-zero/zrpc" -) - -type Config struct { - zrpc.RpcServerConf - - // Redis 配置 - Redis redis.RedisConf - - // 其他配置... - // DB postgres.Config - // Kafka kafka.Config -} -``` - -### 3. ServiceContext 初始化 - -**`internal/svc/serviceContext.go`** -```go -package svc - -import ( - "github.com/zeromicro/go-zero/core/stores/redis" - "juwan-backend/app/users/rpc/internal/config" -) - -type ServiceContext struct { - Config config.Config - Redis *redis.Redis - // 其他依赖... - // DB *gorm.DB -} - -func NewServiceContext(c config.Config) *ServiceContext { - // 初始化 Redis(支持所有模式:node, sentinel, cluster) - rdb := redis.MustNewRedis(c.Redis) - - return &ServiceContext{ - Config: c, - Redis: rdb, - } -} -``` - -### 4. 主程序入口 - -**`usercenter.go`** -```go -package main - -import ( - "flag" - "fmt" - - "juwan-backend/app/users/rpc/internal/config" - "juwan-backend/app/users/rpc/internal/server" - "juwan-backend/app/users/rpc/internal/svc" - "juwan-backend/app/users/rpc/pb" - - "github.com/zeromicro/go-zero/core/conf" - "github.com/zeromicro/go-zero/core/service" - "github.com/zeromicro/go-zero/zrpc" - "google.golang.org/grpc" - "google.golang.org/grpc/reflection" -) - -var configFile = flag.String("f", "etc/pb.yaml", "the config file") - -func main() { - flag.Parse() - - // 加载配置 - var c config.Config - conf.MustLoad(*configFile, &c) - - // 初始化服务上下文 - ctx := svc.NewServiceContext(c) - - // 创建 gRPC 服务 - s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) { - pb.RegisterUsercenterServer(grpcServer, server.NewUsercenterServer(ctx)) - - if c.Mode == service.DevMode || c.Mode == service.TestMode { - reflection.Register(grpcServer) - } - }) - defer s.Stop() - - fmt.Printf("Starting rpc server at %s...\n", c.ListenOn) - s.Start() -} -``` - ---- - -## 🔧 常用操作示例 - -### 1. 基本读写操作 - -```go -package logic - -import ( - "context" - "fmt" - "time" -) - -type UserLogic struct { - ctx context.Context - svcCtx *svc.ServiceContext - logx.Logger -} - -// Set 操作 -func (l *UserLogic) SetUser(userId int64, data string) error { - key := fmt.Sprintf("user:%d", userId) - return l.svcCtx.Redis.Set(key, data) -} - -// Setex 操作(带过期时间) -func (l *UserLogic) SetUserWithExpiry(userId int64, data string) error { - key := fmt.Sprintf("user:%d", userId) - // 缓存 1 小时 - return l.svcCtx.Redis.Setex(key, data, 3600) -} - -// Get 操作 -func (l *UserLogic) GetUser(userId int64) (string, error) { - key := fmt.Sprintf("user:%d", userId) - return l.svcCtx.Redis.Get(key) -} - -// Del 操作 -func (l *UserLogic) DeleteUser(userId int64) error { - key := fmt.Sprintf("user:%d", userId) - _, err := l.svcCtx.Redis.Del(key) - return err -} - -// Exists 检查 -func (l *UserLogic) UserExists(userId int64) (bool, error) { - key := fmt.Sprintf("user:%d", userId) - return l.svcCtx.Redis.Exists(key) -} -``` - -### 2. Hash 操作 - -```go -// HSet 操作 -func (l *UserLogic) SetUserField(userId int64, field, value string) error { - key := fmt.Sprintf("user:%d", userId) - return l.svcCtx.Redis.Hset(key, field, value) -} - -// HGet 操作 -func (l *UserLogic) GetUserField(userId int64, field string) (string, error) { - key := fmt.Sprintf("user:%d", userId) - return l.svcCtx.Redis.Hget(key, field) -} - -// HGetAll 操作 -func (l *UserLogic) GetAllUserFields(userId int64) (map[string]string, error) { - key := fmt.Sprintf("user:%d", userId) - return l.svcCtx.Redis.Hgetall(key) -} - -// HMSet 批量设置 -func (l *UserLogic) SetUserFields(userId int64, fields map[string]string) error { - key := fmt.Sprintf("user:%d", userId) - return l.svcCtx.Redis.Hmset(key, fields) -} -``` - -### 3. List 操作 - -```go -// LPush 操作 -func (l *UserLogic) AddMessage(userId int64, message string) error { - key := fmt.Sprintf("messages:%d", userId) - _, err := l.svcCtx.Redis.Lpush(key, message) - return err -} - -// LRange 操作 -func (l *UserLogic) GetMessages(userId int64, start, stop int) ([]string, error) { - key := fmt.Sprintf("messages:%d", userId) - return l.svcCtx.Redis.Lrange(key, start, stop) -} - -// LLen 操作 -func (l *UserLogic) GetMessageCount(userId int64) (int, error) { - key := fmt.Sprintf("messages:%d", userId) - return l.svcCtx.Redis.Llen(key) -} -``` - -### 4. Set 操作 - -```go -// SAdd 添加成员 -func (l *UserLogic) AddUserTag(userId int64, tag string) error { - key := fmt.Sprintf("user:tags:%d", userId) - _, err := l.svcCtx.Redis.Sadd(key, tag) - return err -} - -// SMembers 获取所有成员 -func (l *UserLogic) GetUserTags(userId int64) ([]string, error) { - key := fmt.Sprintf("user:tags:%d", userId) - return l.svcCtx.Redis.Smembers(key) -} - -// SIsMember 检查成员 -func (l *UserLogic) HasUserTag(userId int64, tag string) (bool, error) { - key := fmt.Sprintf("user:tags:%d", userId) - return l.svcCtx.Redis.Sismember(key, tag) -} -``` - -### 5. Sorted Set 操作 - -```go -// ZAdd 添加成员 -func (l *UserLogic) AddToLeaderboard(userId int64, score int64) error { - key := "leaderboard" - _, err := l.svcCtx.Redis.Zadd(key, score, fmt.Sprintf("%d", userId)) - return err -} - -// ZRevRange 获取排行榜(从高到低) -func (l *UserLogic) GetTopUsers(count int) ([]string, error) { - key := "leaderboard" - return l.svcCtx.Redis.Zrevrange(key, 0, int64(count-1)) -} - -// ZRank 获取排名 -func (l *UserLogic) GetUserRank(userId int64) (int64, error) { - key := "leaderboard" - return l.svcCtx.Redis.Zrank(key, fmt.Sprintf("%d", userId)) -} -``` - -### 6. 缓存模式实现 - -**Cache-Aside Pattern(推荐):** -```go -func (l *UserLogic) GetUserById(userId int64) (*User, error) { - cacheKey := fmt.Sprintf("user:%d", userId) - - // 1. 查缓存 - cached, err := l.svcCtx.Redis.Get(cacheKey) - if err == nil && cached != "" { - var user User - if err := json.Unmarshal([]byte(cached), &user); err == nil { - return &user, nil - } - } - - // 2. 查数据库 - user, err := l.getUserFromDB(userId) - if err != nil { - return nil, err - } - - // 3. 写缓存 - userJSON, _ := json.Marshal(user) - l.svcCtx.Redis.Setex(cacheKey, string(userJSON), 3600) - - return user, nil -} - -func (l *UserLogic) UpdateUser(user *User) error { - // 1. 更新数据库 - if err := l.updateUserInDB(user); err != nil { - return err - } - - // 2. 删除缓存(下次读取时会重新加载) - cacheKey := fmt.Sprintf("user:%d", user.Id) - l.svcCtx.Redis.Del(cacheKey) - - return nil -} -``` - -### 7. 分布式锁 - -```go -// 获取分布式锁 -func (l *UserLogic) AcquireLock(key string, expiry int) (bool, error) { - lockKey := fmt.Sprintf("lock:%s", key) - return l.svcCtx.Redis.Setnx(lockKey, "1") -} - -// 释放锁 -func (l *UserLogic) ReleaseLock(key string) error { - lockKey := fmt.Sprintf("lock:%s", key) - _, err := l.svcCtx.Redis.Del(lockKey) - return err -} - -// 使用示例 -func (l *UserLogic) ProcessWithLock(userId int64) error { - lockKey := fmt.Sprintf("user:%d", userId) - - // 获取锁 - acquired, err := l.AcquireLock(lockKey, 10) - if err != nil { - return err - } - if !acquired { - return fmt.Errorf("failed to acquire lock") - } - defer l.ReleaseLock(lockKey) - - // 执行业务逻辑 - // ... - - return nil -} -``` - -### 8. Pipeline 批量操作 - -```go -// 使用 go-redis 原生客户端进行 Pipeline -func (l *UserLogic) BatchSetUsers(users []*User) error { - // go-zero 的 Redis 包装了 go-redis,可以获取原生客户端 - rdb := l.svcCtx.Redis - - pipe := rdb.Pipelined(func(pip redis.Pipeliner) error { - for _, user := range users { - key := fmt.Sprintf("user:%d", user.Id) - userJSON, _ := json.Marshal(user) - pip.Set(context.Background(), key, userJSON, time.Hour) - } - return nil - }) - - return pipe -} -``` - ---- - -## 🚀 高级特性 - -### 1. 缓存穿透防护(布隆过滤器) - -```go -import "github.com/zeromicro/go-zero/core/bloom" - -type UserLogic struct { - ctx context.Context - svcCtx *svc.ServiceContext - filter *bloom.Filter -} - -func (l *UserLogic) GetUserWithBloom(userId int64) (*User, error) { - // 1. 布隆过滤器检查 - if !l.filter.Exists([]byte(fmt.Sprintf("%d", userId))) { - return nil, fmt.Errorf("user not found") - } - - // 2. 查缓存 - cacheKey := fmt.Sprintf("user:%d", userId) - cached, err := l.svcCtx.Redis.Get(cacheKey) - if err == nil && cached != "" { - var user User - json.Unmarshal([]byte(cached), &user) - return &user, nil - } - - // 3. 查数据库 - user, err := l.getUserFromDB(userId) - if err != nil { - return nil, err - } - - // 4. 写缓存 - userJSON, _ := json.Marshal(user) - l.svcCtx.Redis.Setex(cacheKey, string(userJSON), 3600) - - return user, nil -} -``` - -### 2. 缓存击穿防护(Singleflight) - -```go -import "golang.org/x/sync/singleflight" - -type UserLogic struct { - ctx context.Context - svcCtx *svc.ServiceContext - sg singleflight.Group -} - -func (l *UserLogic) GetUserWithSingleflight(userId int64) (*User, error) { - cacheKey := fmt.Sprintf("user:%d", userId) - - // 使用 Singleflight 确保同一时刻只有一个请求查询 - v, err, _ := l.sg.Do(cacheKey, func() (interface{}, error) { - // 1. 查缓存 - cached, err := l.svcCtx.Redis.Get(cacheKey) - if err == nil && cached != "" { - var user User - json.Unmarshal([]byte(cached), &user) - return &user, nil - } - - // 2. 查数据库 - user, err := l.getUserFromDB(userId) - if err != nil { - return nil, err - } - - // 3. 写缓存 - userJSON, _ := json.Marshal(user) - l.svcCtx.Redis.Setex(cacheKey, string(userJSON), 3600) - - return user, nil - }) - - if err != nil { - return nil, err - } - - return v.(*User), nil -} -``` - -### 3. 缓存雪崩防护(随机过期时间) - -```go -import ( - "math/rand" - "time" -) - -func (l *UserLogic) SetCacheWithRandomExpiry(key string, value string, baseExpiry int) error { - // 在基础过期时间上增加随机值(±20%) - randomOffset := rand.Intn(baseExpiry / 5) - expiry := baseExpiry + randomOffset - (baseExpiry / 10) - - return l.svcCtx.Redis.Setex(key, value, expiry) -} - -// 使用示例 -func (l *UserLogic) CacheUser(user *User) error { - key := fmt.Sprintf("user:%d", user.Id) - userJSON, _ := json.Marshal(user) - - // 基础过期时间 1 小时,实际会在 48-72 分钟之间随机 - return l.SetCacheWithRandomExpiry(key, string(userJSON), 3600) -} -``` - ---- - -## ⚡ 性能优化 - -### 1. 连接池配置优化 - -**根据并发量调整:** -```yaml -# 低并发(< 100 QPS) -Redis: - MaxIdle: 8 - MaxActive: 100 - -# 中并发(100-1000 QPS) -Redis: - MaxIdle: 16 - MaxActive: 500 - -# 高并发(> 1000 QPS) -Redis: - MaxIdle: 32 - MaxActive: 1000 -``` - -### 2. 超时配置优化 - -```yaml -Redis: - # 连接超时:通常设置较大值 - ConnectTimeout: 5000 # 5 秒 - - # 读写超时:设置较小值,快速失败 - ReadTimeout: 1000 # 1 秒 - WriteTimeout: 1000 # 1 秒 -``` - -### 3. Pipeline 批量操作 - -**避免循环调用:** -```go -// ❌ 不好:循环调用 -for _, user := range users { - key := fmt.Sprintf("user:%d", user.Id) - l.svcCtx.Redis.Set(key, user.Name) -} - -// ✅ 推荐:使用 Pipeline -pipe := l.svcCtx.Redis.Pipelined(func(pip redis.Pipeliner) error { - for _, user := range users { - key := fmt.Sprintf("user:%d", user.Id) - pip.Set(context.Background(), key, user.Name, 0) - } - return nil -}) -``` - -### 4. 合理的缓存过期时间 - -```go -const ( - CacheExpiryShort = 300 // 5 分钟 - 热点数据 - CacheExpiryMedium = 3600 // 1 小时 - 常规数据 - CacheExpiryLong = 86400 // 1 天 - 冷数据 -) - -func (l *UserLogic) SetUserCache(user *User, expiry int) error { - key := fmt.Sprintf("user:%d", user.Id) - userJSON, _ := json.Marshal(user) - return l.svcCtx.Redis.Setex(key, string(userJSON), expiry) -} -``` - -### 5. Key 命名规范 - -```go -// 推荐的 Key 命名规范 -const ( - KeyPrefixUser = "user:" // user:123 - KeyPrefixSession = "session:" // session:abc123 - KeyPrefixCache = "cache:" // cache:user:list - KeyPrefixLock = "lock:" // lock:order:456 - KeyPrefixCounter = "counter:" // counter:page:views -) - -// 使用函数生成 Key -func UserCacheKey(userId int64) string { - return fmt.Sprintf("%s%d", KeyPrefixUser, userId) -} - -func SessionKey(sessionId string) string { - return fmt.Sprintf("%s%s", KeyPrefixSession, sessionId) -} -``` - ---- - -## 🔍 故障排查 - -### 1. 连接失败 - -**问题:** `dial tcp xxx:6379: i/o timeout` - -**排查步骤:** -```bash -# 1. 检查 Redis 服务 -kubectl get pods -n juwan | grep redis - -# 2. 检查 Service -kubectl get svc -n juwan | grep redis - -# 3. 测试网络连通性 -kubectl run -it --rm nettest --image=busybox --restart=Never -n juwan -- \ - nc -zv user-redis-master 6379 - -# 4. 查看应用日志 -kubectl logs -f user-rpc-xxx -n juwan -``` - -**解决方案:** -- 确认 Host 配置正确 -- 确认网络策略允许访问 -- 检查 Redis Pod 状态 - -### 2. 认证失败 - -**问题:** `NOAUTH Authentication required` - -**排查:** -```go -// 打印配置(调试用) -func main() { - var c config.Config - conf.MustLoad(*configFile, &c) - - // 检查密码是否正确加载 - fmt.Printf("Redis Config: Host=%s, Pass=%s\n", c.Redis.Host, c.Redis.Pass) -} -``` - -**解决方案:** -- 确认环境变量 `REDIS_PASSWORD` 已设置 -- 确认 Secret 正确挂载 -- 检查密码是否正确 - -### 3. 性能问题 - -**慢查询检测:** -```go -import "time" - -func (l *UserLogic) GetUserWithMetrics(userId int64) (*User, error) { - start := time.Now() - defer func() { - duration := time.Since(start) - if duration > 100*time.Millisecond { - l.Logger.Warnf("Slow Redis query: %v", duration) - } - }() - - // 执行查询 - key := fmt.Sprintf("user:%d", userId) - cached, err := l.svcCtx.Redis.Get(key) - // ... -} -``` - -**常见原因:** -- 连接池耗尽 → 增大 MaxActive -- 大 Value 传输 → 拆分或压缩数据 -- 网络延迟 → 检查网络质量 - -### 4. 内存泄漏 - -**检查连接是否正确关闭:** -```go -// go-zero 的 Redis 客户端会自动管理连接 -// 但如果使用原生 go-redis 客户端,需要手动关闭 - -// ❌ 错误示例 -func bad() { - rdb := redis.NewClient(&redis.Options{...}) - // 使用完后没有关闭 -} - -// ✅ 正确示例 -func good() { - rdb := redis.NewClient(&redis.Options{...}) - defer rdb.Close() - // ... -} -``` - ---- - -## 📖 最佳实践 - -### 1. 环境变量管理 - -**Kubernetes Deployment:** -```yaml -apiVersion: apps/v1 -kind: Deployment -metadata: - name: user-rpc - namespace: juwan -spec: - template: - spec: - containers: - - name: user-rpc - image: user-rpc:v1 - env: - # 从 Secret 读取 Redis 密码 - - name: REDIS_PASSWORD - valueFrom: - secretKeyRef: - name: user-redis - key: password - - # 从 ConfigMap 读取其他配置 - - name: REDIS_HOST - valueFrom: - configMapKeyRef: - name: user-rpc-config - key: redis.host -``` - -### 2. 配置分离 - -**开发、测试、生产环境分离:** -```bash -# 开发环境 -go run usercenter.go -f etc/pb-dev.yaml - -# 测试环境 -go run usercenter.go -f etc/pb-test.yaml - -# 生产环境 -./usercenter -f etc/pb-prod.yaml -``` - -### 3. 监控指标 - -```go -import ( - "github.com/zeromicro/go-zero/core/metric" - "github.com/zeromicro/go-zero/core/prometheus" -) - -var ( - redisCacheHit = metric.NewCounterVec(&metric.CounterVecOpts{ - Namespace: "user_rpc", - Subsystem: "redis", - Name: "cache_hit_total", - Help: "redis cache hit count", - Labels: []string{"key"}, - }) - - redisCacheMiss = metric.NewCounterVec(&metric.CounterVecOpts{ - Namespace: "user_rpc", - Subsystem: "redis", - Name: "cache_miss_total", - Help: "redis cache miss count", - Labels: []string{"key"}, - }) -) - -func (l *UserLogic) GetUserWithMetrics(userId int64) (*User, error) { - cacheKey := fmt.Sprintf("user:%d", userId) - - cached, err := l.svcCtx.Redis.Get(cacheKey) - if err == nil && cached != "" { - redisCacheHit.Inc("user") - var user User - json.Unmarshal([]byte(cached), &user) - return &user, nil - } - - redisCacheMiss.Inc("user") - // 查询数据库... -} -``` - -### 4. 错误处理 - -```go -func (l *UserLogic) GetUser(userId int64) (*User, error) { - cacheKey := fmt.Sprintf("user:%d", userId) - - // 缓存查询失败不应该中断流程 - cached, err := l.svcCtx.Redis.Get(cacheKey) - if err == nil && cached != "" { - var user User - if json.Unmarshal([]byte(cached), &user) == nil { - return &user, nil - } - } - - // 缓存失败,降级查询数据库 - user, err := l.getUserFromDB(userId) - if err != nil { - return nil, err - } - - // 尝试回写缓存,失败不影响返回结果 - go func() { - userJSON, _ := json.Marshal(user) - if err := l.svcCtx.Redis.Setex(cacheKey, string(userJSON), 3600); err != nil { - l.Logger.Errorf("Failed to set cache: %v", err) - } - }() - - return user, nil -} -``` - -### 5. 缓存更新策略 - -**Write-Through(同步更新):** -```go -func (l *UserLogic) UpdateUser(user *User) error { - // 1. 更新数据库 - if err := l.updateUserInDB(user); err != nil { - return err - } - - // 2. 同步更新缓存 - cacheKey := fmt.Sprintf("user:%d", user.Id) - userJSON, _ := json.Marshal(user) - if err := l.svcCtx.Redis.Setex(cacheKey, string(userJSON), 3600); err != nil { - l.Logger.Errorf("Failed to update cache: %v", err) - } - - return nil -} -``` - -**Write-Behind(异步更新):** -```go -func (l *UserLogic) UpdateUserAsync(user *User) error { - // 1. 立即更新缓存 - cacheKey := fmt.Sprintf("user:%d", user.Id) - userJSON, _ := json.Marshal(user) - l.svcCtx.Redis.Setex(cacheKey, string(userJSON), 3600) - - // 2. 异步更新数据库 - go func() { - if err := l.updateUserInDB(user); err != nil { - l.Logger.Errorf("Failed to update DB: %v", err) - // 回滚缓存 - l.svcCtx.Redis.Del(cacheKey) - } - }() - - return nil -} -``` - ---- - -## 📚 参考资源 - -### 官方文档 -- [go-zero 官方文档](https://go-zero.dev/) -- [go-zero Redis 文档](https://go-zero.dev/docs/tutorials/redis) -- [go-redis 文档](https://redis.uptrace.dev/) - -### 示例代码 -- [go-zero Examples](https://github.com/zeromicro/go-zero/tree/master/example) -- [go-zero Book Store](https://github.com/zeromicro/go-zero-book-store) - -### 相关工具 -- [RedisInsight](https://redis.com/redis-enterprise/redis-insight/) - Redis 管理工具 -- [redis-cli](https://redis.io/docs/manual/cli/) - Redis 命令行工具 - ---- - -## 📝 总结 - -### 快速开始检查清单 - -- [ ] 1. 在 `config.go` 中定义 `Redis redis.RedisConf` -- [ ] 2. 在配置文件中添加 Redis 配置 -- [ ] 3. 在 `ServiceContext` 中初始化 `redis.MustNewRedis(c.Redis)` -- [ ] 4. 在 Kubernetes中配置环境变量 `REDIS_PASSWORD` -- [ ] 5. 在 logic 中使用 `l.svcCtx.Redis` -- [ ] 6. 测试连接是否正常 - -### 生产环境推荐配置 - -```yaml -Redis: - Host: user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379 - Type: sentinel - Pass: ${REDIS_PASSWORD} - MasterName: mymaster - MaxIdle: 16 - MaxActive: 100 - ConnectTimeout: 5000 - ReadTimeout: 3000 - WriteTimeout: 3000 -``` - -### 关键点提醒 - -1. ✅ **生产环境必须使用 Sentinel 模式** -2. ✅ **密码通过环境变量传递,不要硬编码** -3. ✅ **合理设置过期时间,防止缓存雪崩** -4. ✅ **使用 Pipeline 优化批量操作** -5. ✅ **实现缓存降级策略,Redis 故障不影响主流程** - ---- - -**文档版本:** 1.0 -**创建日期:** 2026年2月22日 -**维护者:** Backend Team -**下次审查:** 2026年3月22日 diff --git a/docs/kubernetes-service-explanation.md b/docs/kubernetes-service-explanation.md deleted file mode 100644 index 65c2df3..0000000 --- a/docs/kubernetes-service-explanation.md +++ /dev/null @@ -1,743 +0,0 @@ -# Redis Kubernetes Service 详细解析 - -**问题:** 为什么 Redis 有 8 个 Service,但应用配置中只使用 `user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379`? - -**日期:** 2026年2月22日 - ---- - -## 📋 目录 - -1. [Service 概览](#service-概览) -2. [Kubernetes Service 基础](#kubernetes-service-基础) -3. [8 个 Service 的详细说明](#8-个-service-的详细说明) -4. [为什么使用哪个 Service](#为什么使用哪个-service) -5. [Service 创建原理](#service-创建原理) -6. [网络流量路由](#网络流量路由) -7. [故障排查](#故障排查) - ---- - -## 📊 Service 概览 - -### 当前 Redis 的 8 个 Service - -```bash -$ kubectl get svc -n juwan | grep redis - -NAME TYPE CLUSTER-IP PORTS -user-redis ClusterIP 10.103.91.84 6379/TCP,9121/TCP 33m -user-redis-additional ClusterIP 10.107.228.48 6379/TCP 33m -user-redis-headless ClusterIP None 6379/TCP 33m -user-redis-master ClusterIP 10.97.120.76 6379/TCP 33m -user-redis-replica ClusterIP 10.100.213.103 6379/TCP 33m -user-redis-sentinel-sentinel ClusterIP 10.105.28.231 26379/TCP 32m -user-redis-sentinel-sentinel-additional ClusterIP 10.97.111.42 26379/TCP 32m -user-redis-sentinel-sentinel-headless ClusterIP None 26379/TCP 32m -``` - -### 按功能分类 - -| 分类 | Service 名称 | 作用 | -|-----|-------------|------| -| **Redis 数据层** | user-redis | 通用入口 | -| | user-redis-additional | 备用入口 | -| | user-redis-master | 主节点专用 | -| | user-redis-replica | 从节点专用 | -| | user-redis-headless | Pod 间通信 | -| **Sentinel 监控层** | user-redis-sentinel-sentinel | Sentinel 入口 ⭐ | -| | user-redis-sentinel-sentinel-additional | 备用入口 | -| | user-redis-sentinel-sentinel-headless | Sentinel 间通信 | - ---- - -## 🔷 Kubernetes Service 基础 - -### Service 的作用 - -**Kubernetes 中的 Service 是什么?** - -``` -┌─────────────────────────────────────────────────┐ -│ Kubernetes Cluster │ -│ │ -│ Service (虚拟 IP + DNS) │ -│ ↓ │ -│ Endpoints (实际 Pod IP 列表) │ -│ ├─ 10.244.0.10:6379 (Pod 1) │ -│ ├─ 10.244.1.20:6379 (Pod 2) │ -│ └─ 10.244.2.30:6379 (Pod 3) │ -│ │ -│ 客户端 ──→ Service IP (稳定) ──→ Pod IP (变化) │ -└─────────────────────────────────────────────────┘ -``` - -### Service 的三种类型 - -| 类型 | CLUSTER-IP | 用途 | 示例 | -|-----|-----------|------|------| -| **ClusterIP** | ✅ 有 | 集群内访问 | 10.103.91.84 | -| **ClusterIP
(Headless)** | ❌ None | Pod 间直接通信 | None | -| **NodePort** | ✅ 有 | 集群外访问 | 10.103.91.84 | - ---- - -## 🔍 8 个 Service 的详细说明 - -### 第一组:Redis 数据层 Service(端口 6379) - -#### 1️⃣ user-redis(ClusterIP) - -**基本信息:** -```yaml -名称: user-redis -类型: ClusterIP (有负载均衡) -Cluster IP: 10.103.91.84 -端口: 6379/TCP, 9121/TCP -DNS: user-redis.juwan.svc.cluster.local -``` - -**Endpoints 信息:** -```bash -$ kubectl get endpoints user-redis -n juwan - -NAME ENDPOINTS -user-redis 10.244.0.10:6379,10.244.1.20:6379,10.244.2.30:6379 -``` - -**负载均衡机制:** -``` -客户端请求 ──→ Service IP (10.103.91.84) - ↓ - kube-proxy (iptables/ipvs) - ↓ - 随机选择一个 Pod - ├─ 10.244.0.10 (redis-0) - ├─ 10.244.1.20 (redis-1) ← 可能 - └─ 10.244.2.30 (redis-2) -``` - -**特点:** -- ✅ 对所有 Pod 轮询负载均衡 -- ✅ 包含 Redis 数据服务(6379)和 Exporter(9121) -- ⚠️ 可能把写请求轮询到从节点导致失败 - -**适用场景:** -- 监控抓取(Prometheus 从 9121 端口抓指标) -- 不关心读写分离的简单查询 - -**为什么有 2 个端口?** -``` -6379: Redis 数据服务 -9121: Prometheus Exporter 监控端口 - └─ 暴露 Redis 性能指标给 Prometheus - (redis_up, redis_memory_used, etc.) -``` - -**不用这个的原因:** -``` -❌ 如果直接使用 user-redis 进行读写: - ├─ 写请求可能被路由到从节点 (error) - ├─ 无法进行故障自动转移 - └─ 依赖于手动更新配置 -``` - ---- - -#### 2️⃣ user-redis-additional(ClusterIP) - -**基本信息:** -```yaml -名称: user-redis-additional -类型: ClusterIP (有负载均衡) -Cluster IP: 10.107.228.48 -端口: 6379/TCP -Endpoints: 同 user-redis -``` - -**作用:** -- 功能完全同 `user-redis` -- 提供额外的访问入口 -- 用于多租户/网络隔离场景 - -**为什么有这个?** -``` -场景:某些网络策略可能只允许访问特定 Service -└─ 额外的 Service 提供备用入口 -``` - -**不常用的原因:** -- 大多数场景用 `user-redis` 就足够 -- `user-redis-additional` 是备用 - ---- - -#### 3️⃣ user-redis-headless(ClusterIP: None) - -**基本信息:** -```yaml -名称: user-redis-headless -类型: ClusterIP (Headless Service) -Cluster IP: None ← 关键:无虚拟 IP -端口: 6379/TCP -DNS: user-redis-headless.juwan.svc.cluster.local -``` - -**特殊之处:无虚拟 IP** - -```bash -# 正常 Service 查询返回虚拟 IP -$ nslookup user-redis.juwan.svc.cluster.local -Name: user-redis.juwan.svc.cluster.local -Address: 10.103.91.84 ← 虚拟 IP - -# Headless Service 查询返回所有 Pod IP -$ nslookup user-redis-headless.juwan.svc.cluster.local -Name: user-redis-headless.juwan.svc.cluster.local -Address: 10.244.0.10 ← Pod 1 实际 IP -Address: 10.244.1.20 ← Pod 2 实际 IP -Address: 10.244.2.30 ← Pod 3 实际 IP -``` - -**使用场景:** - -``` -┌─────────────────────────────────────────────────┐ -│ StatefulSet (Redis Cluster/Replication) │ -│ │ -│ redis-0 (主) redis-1 (从) redis-2 (从) │ -│ ↓ ↓ ↓ │ -│ 10.244.0.10 10.244.1.20 10.244.2.30 │ -│ ↑ │ -│ 需要直接连接到特定 Pod: │ -│ redis-0.user-redis-headless (连接主节点) │ -│ redis-1.user-redis-headless (连接从节点) │ -└─────────────────────────────────────────────────┘ -``` - -**谁在使用?** -- Redis 主从复制:从节点需要连接到已知的主节点 -- Sentinel 监控:需要直接访问特定 Redis 实例 -- Redis Operator 内部使用 - -**为什么应用不用这个?** -``` -❌ Pod DNS 只能在 Pod 内使用 - └─ 外部应用不知道 Pod 的具体 DNS 名称 - -✅ 用虚拟 Service IP 的优势 - └─ 无需关心底层 Pod 变化 -``` - ---- - -#### 4️⃣ user-redis-master(ClusterIP) - -**基本信息:** -```yaml -名称: user-redis-master -类型: ClusterIP -Cluster IP: 10.97.120.76 -端口: 6379/TCP -Endpoints: 10.244.0.10:6379 (只有 1 个 Pod) -DNS: user-redis-master.juwan.svc.cluster.local -``` - -**特点:只指向主节点** - -```bash -$ kubectl get endpoints user-redis-master -n juwan - -NAME ENDPOINTS -user-redis-master 10.244.0.10:6379 ← 仅主节点 -``` - -**对比所有 Endpoints:** -``` -user-redis-master: 10.244.0.10 (主) -user-redis-replica: 10.244.1.20, 10.244.2.30 (从) -user-redis: 所有 Pod -``` - -**为什么分开?** -``` -┌─────────────────────────────────────────┐ -│ Redis 主从架构 │ -│ │ -│ Redis Master (10.244.0.10) │ -│ ├─ 处理所有写操作 │ -│ └─ 赋值数据给 Slave │ -│ │ -│ Redis Slave 1 (10.244.1.20) │ -│ └─ 处理只读操作 │ -│ │ -│ Redis Slave 2 (10.244.2.30) │ -│ └─ 处理只读操作 │ -└─────────────────────────────────────────┘ - -请求分类: -┌───────────────────────┐ -│ SET key value │ ──→ user-redis-master (10.97.120.76) -│ HSET user:1 name john │ -└───────────────────────┘ - -┌───────────────────────┐ -│ GET key │ ──→ user-redis-replica (10.100.213.103) -│ HGET user:1 name │ -└───────────────────────┘ -``` - -**适用场景:** -- ✅ 读写分离架构 -- ✅ 优化读性能(从节点处理读) -- ✅ 减轻主节点负担 - -**为什么应用通常不直接用?** -``` -❌ 需要在应用层面区分读写操作 - ├─ 写操作 → user-redis-master - ├─ 只读操作 → user-redis-replica - └─ 代码复杂度高 - -✅ Sentinel 模式自动处理 - └─ 应用无需关心主从区别 -``` - ---- - -#### 5️⃣ user-redis-replica(ClusterIP) - -**基本信息:** -```yaml -名称: user-redis-replica -类型: ClusterIP -Cluster IP: 10.100.213.103 -端口: 6379/TCP -Endpoints: 10.244.1.20:6379, 10.244.2.30:6379 (两个从节点) -DNS: user-redis-replica.juwan.svc.cluster.local -``` - -**特点:只指向从节点,支持负载均衡** - -```bash -$ kubectl get endpoints user-redis-replica -n juwan - -NAME ENDPOINTS -user-redis-replica 10.244.1.20:6379, 10.244.2.30:6379 -``` - -**读流量分散:** -``` -应用发送 GET 请求 - ↓ - user-redis-replica (10.100.213.103) - ↓ - 随机选择一个从节点 - ├─ 10.244.1.20 (redis-1) ← 可能 - └─ 10.244.2.30 (redis-2) ← 可能 -``` - -**适用场景:** -- 除了 Sentinel 模式外的读优化 -- 需要手动管理读写分离 - ---- - -### 第二组:Sentinel 监控层 Service(端口 26379) - -#### 6️⃣ user-redis-sentinel-sentinel(ClusterIP)⭐⭐⭐ - -**基本信息:** -```yaml -名称: user-redis-sentinel-sentinel -类型: ClusterIP -Cluster IP: 10.105.28.231 -端口: 26379/TCP -Endpoints: 10.244.0.50:26379, 10.244.1.70:26379, 10.244.2.90:26379 - (3 个 Sentinel 实例) -DNS: user-redis-sentinel-sentinel.juwan.svc.cluster.local -``` - -**为什么应用使用这个?** - -``` -应用程序配置: -┌──────────────────────────────────────────────┐ -│ Redis: │ -│ Host: user-redis-sentinel-sentinel │ -│ Port: 26379 │ -│ Type: sentinel │ -│ MasterName: mymaster │ -└──────────────────────────────────────────────┘ - -连接流程: -┌─────────────────────────────────────────────┐ -│ 应用程序 │ -└────────────────────┬────────────────────────┘ - │ - ↓ -┌─────────────────────────────────────────────┐ -│ user-redis-sentinel-sentinel (26379) │ -│ ├─ Sentinel 1: 10.244.0.50:26379 │ -│ ├─ Sentinel 2: 10.244.1.70:26379 │ -│ └─ Sentinel 3: 10.244.2.90:26379 │ -└────────────────────┬────────────────────────┘ - │ - 应用询问: "mymaster 在哪?" - ↓ - Sentinel 回答: "在 10.244.0.10:6379" - ↓ -┌─────────────────────────────────────────────┐ -│ Redis Master: 10.244.0.10:6379 │ -│ (应用直接连接进行读写) │ -└─────────────────────────────────────────────┘ - -故障转移过程: -Master 故障 → Sentinel 检测 → 提升新主节点 - → 应用下次查询时 → 获得新主节点 IP - → 自动连接新主节点 -``` - -**为什么这是最佳选择?** - -1. **自动故障转移** - ``` - 主节点宕机 (✗) → Sentinel 自动选举新主 → 应用自动连接 - ``` - -2. **高可用** - ``` - Sentinel 集群(3 个) → 任意 1-2 个故障仍可用 - ``` - -3. **应用无感知** - ``` - 应用只需配置 MasterName: mymaster - 无需关心主从地址变化 - ``` - -4. **标准做法** - ``` - ✅ 业界公认的 Redis 高可用方案 - ✅ 最小化应用改动 - ✅ 自动化程度最高 - ``` - -**为什么不用其他 Service?** - -``` -❌ user-redis-master/user-redis-replica - └─ 需要应用层区分读写,主从切换需要重启应用 - -❌ user-redis/user-redis-additional - └─ 没有故障转移能力,故障时应用会报错 - -✅ user-redis-sentinel-sentinel - └─ 自动发现新主节点,无需重启应用 -``` - ---- - -#### 7️⃣ user-redis-sentinel-sentinel-additional(ClusterIP) - -**说明:** 功能同 `user-redis-sentinel-sentinel`,备用入口 - ---- - -#### 8️⃣ user-redis-sentinel-sentinel-headless(ClusterIP: None) - -**说明:** 供 Sentinel 内部通信和选举使用 - ---- - -## 🎯 为什么使用哪个 Service - -### 应用配置选择 - -#### ⭐⭐⭐ Sentinel 模式(生产推荐) - -```yaml -# 应用配置 -Redis: - Host: user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379 - Type: sentinel - MasterName: mymaster - Pass: ${REDIS_PASSWORD} -``` - -**优势:** -- ✅ 自动故障转移(RTO < 30 秒) -- ✅ 应用无需重启 -- ✅ 自动发现新主节点 -- ✅ 生产标准做法 - ---- - -#### ⭐⭐ 主从分离模式(可选) - -```yaml -# 应用配置(需要两个 host) -Redis: - Master: - Host: user-redis-master.juwan.svc.cluster.local:6379 - Slave: - Host: user-redis-replica.juwan.svc.cluster.local:6379 -``` - -**适用场景:** -- 读写分离显著 -- 对读性能有极高要求 - -**缺点:** -- 主从故障需手动切换 -- 应用层复杂度高 - ---- - -#### ❌ 不推荐的做法 - -```yaml -# ❌ 直接连接单个节点 -Redis: - Host: user-redis-0.user-redis-headless.juwan.svc.cluster.local:6379 - # 问题:Pod 重启 IP 变化,需要更新配置 - -# ❌ 连接通用 Service(无故障转移) -Redis: - Host: user-redis.juwan.svc.cluster.local:6379 - # 问题:无法自动转移,故障时应用报错 - -# ❌ 硬编码 Pod IP -Redis: - Host: 10.244.0.10:6379 - # 问题:Pod 重启 IP 变化,应用立即不可用 -``` - ---- - -## 🔌 Service 创建原理 - -### 为什么会自动创建这么多 Service? - -**由 Redis Operator 自动创建:** - -```go -// Redis Operator 逻辑(伪代码) -func CreateServicesForRedis(redis *RedisReplication) { - // 数据层 Service - CreateService("user-redis", AllRedisNodes) - CreateService("user-redis-additional", AllRedisNodes) - CreateService("user-redis-master", [MasterNode]) - CreateService("user-redis-replica", [SlaveNodes]) - CreateHeadlessService("user-redis-headless", AllRedisNodes) - - // 监控层 Service - CreateService("user-redis-sentinel-sentinel", AllSentinelNodes) - CreateService("user-redis-sentinel-sentinel-additional", AllSentinelNodes) - CreateHeadlessService("user-redis-sentinel-sentinel-headless", AllSentinelNodes) -} -``` - -**为什么这样设计?** - -| Service | 原因 | -|---------|------| -| 多个 ClusterIP | 不同场景需要不同的 Endpoints 配置 | -| 包含 additional | 网络隔离/多租户支持 | -| 包含 headless | StatefulSet 需要 Pod 间直接通信 | - -**类比:** -``` -Redis Operator 就像一个完整的产品 -└─ 提供多种方式使用 Redis - ├─ 简单: user-redis - ├─ 高级: user-redis-master/replica - ├─ HA: user-redis-sentinel-sentinel - └─ 内部: headless services -``` - ---- - -## 🌐 网络流量路由 - -### 查询 Service 背后的 Pod - -**查看 Service Endpoints:** - -```bash -# 查看 user-redis 关联的 Pod -$ kubectl get endpoints user-redis -n juwan -NAME ENDPOINTS -user-redis 10.244.0.10:6379,10.244.1.20:6379,10.244.2.30:6379 - -# 查看 user-redis-master 关联的 Pod -$ kubectl get endpoints user-redis-master -n juwan -NAME ENDPOINTS -user-redis-master 10.244.0.10:6379 - -# 查看 user-redis-replica 关联的 Pod -$ kubectl get endpoints user-redis-replica -n juwan -NAME ENDPOINTS -user-redis-replica 10.244.1.20:6379,10.244.2.30:6379 -``` - -**Pod 和 Service 的映射关系:** - -``` -Pods (实际运行的实例) Services (虚拟 IP) -└─ redis-0 (主) └─ user-redis (所有) - ├─ 10.244.0.10 ├─ 10.103.91.84 - └─ :6379 - └─ user-redis-master (仅主) -└─ redis-1 (从) ├─ 10.97.120.76 - ├─ 10.244.1.20 - └─ :6379 - └─ user-redis-replica (仅从) -└─ redis-2 (从) ├─ 10.100.213.103 - ├─ 10.244.2.30 - └─ :6379 -``` - -**DNS 解析过程:** - -``` -应用 DNS 查询 - └─ user-redis-master.juwan.svc.cluster.local - ↓ -CoreDNS (Kubernetes DNS) - └─ 查询并返回 Service IP: - ├─ 10.97.120.76 (user-redis-master) - ├─ 或 10.100.213.103 (user-redis-replica) - ├─ 或 10.103.91.84 (user-redis) - └─ 或 Sentinel 的 IP -``` - -**Sentinel 模式的特殊之处:** - -``` -应用查询 Sentinel - └─ user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379 - ↓ -Sentinel Service (负载均衡到 3 个 Sentinel 节点) - ↓ -Sentinel 节点 (任选一个) - ↓ -应用询问: "mymaster 主节点 IP 是什么?" - ↓ -Sentinel 回答: "10.244.0.10:6379" - ↓ -应用直接连接 Redis Master: 10.244.0.10:6379 -``` - ---- - -## 🔧 故障排查 - -### 问题 1:为什么应用连接失败? - -**检查步骤:** - -```bash -# 1. 验证 Service 存在 -kubectl get svc user-redis-sentinel-sentinel -n juwan - -# 2. 验证 Endpoints 不为空 -kubectl get endpoints user-redis-sentinel-sentinel -n juwan - -# 3. 测试 DNS 解析 -kubectl run -it --rm nettest --image=busybox --restart=Never -n juwan -- \ - nslookup user-redis-sentinel-sentinel.juwan.svc.cluster.local - -# 4. 测试连接性 -kubectl run -it --rm nettest --image=busybox --restart=Never -n juwan -- \ - nc -zv user-redis-sentinel-sentinel.juwan.svc.cluster.local 26379 - -# 5. 查看应用日志 -kubectl logs -f user-rpc-xxx -n juwan -``` - -### 问题 2:为什么看不到某个 Service? - -```bash -# 确保在正确的命名空间 -kubectl get svc -n juwan | grep redis - -# 如果 Redis Operator 有问题,Service 可能不会创建 -# 查看 Operator 日志 -kubectl logs -n default deployment/redis-operator -``` - -### 问题 3:Service IP 经常变化? - -```bash -# Service IP 是稳定的(除非被删除和重建) -# 如果频繁变化,说明 Service 被频繁重建 - -# 检查 Service 创建事件 -kubectl describe svc user-redis-sentinel-sentinel -n juwan - -# 检查 Operator 是否有异常 -kubectl describe redissentinel user-redis-sentinel -n juwan -``` - ---- - -## 📚 总结 - -### 快速理解 - -| Service | 用途 | 应用是否使用 | -|---------|------|-----------| -| **user-redis-sentinel-sentinel** | ⭐ Sentinel 高可用 | ✅ **生产推荐** | -| user-redis-master | 直连主节点 | ⚠️ 需要读写分离 | -| user-redis-replica | 直连从节点 | ⚠️ 需要读写分离 | -| user-redis | 通用入口 | ❌ 不推荐(无 HA) | -| headless services | 内部通信 | ❌ 应用不用 | - -### 为什么有这么多 Service? - -**答案:** 为了提供灵活的使用方式 - -``` -Redis Operator 的设计理念: -┌─────────────────────────────────────────┐ -│ 提供完整的 Redis 高可用解决方案 │ -│ │ -│ ├─ 简单使用场景 │ -│ │ └─ user-redis (所有节点) │ -│ │ │ -│ ├─ 高级使用场景 │ -│ │ ├─ user-redis-master (写) │ -│ │ └─ user-redis-replica (读) │ -│ │ │ -│ ├─ 生产场景 (推荐) │ -│ │ └─ user-redis-sentinel-sentinel │ -│ │ │ -│ └─ 内部通信 │ -│ └─ headless services │ -└─────────────────────────────────────────┘ -``` - -### 应用该用哪个? - -**一句话:使用 `user-redis-sentinel-sentinel:26379` + Sentinel 模式** - -```yaml -# 这是最佳实践 -Redis: - Host: user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379 - Type: sentinel - MasterName: mymaster -``` - -**为什么?** -- ✅ 自动故障转移 -- ✅ 应用无需重启 -- ✅ 无需手工干预 -- ✅ 行业标准 - ---- - -**文档版本:** 1.0 -**创建日期:** 2026年2月22日 -**维护者:** DevOps Team diff --git a/docs/loki-log-troubleshooting.md b/docs/loki-log-troubleshooting.md deleted file mode 100644 index 3709a05..0000000 --- a/docs/loki-log-troubleshooting.md +++ /dev/null @@ -1,216 +0,0 @@ -# Loki 无日志排查与修复手册 - -## 背景 - -现象:Grafana Explore 使用 Loki 数据源查询 `{job="kubernetes-pods"}` 时无结果,页面提示 `No logs found`。 - -影响:日志链路不可用,无法按服务排查线上问题。 - -链路目标: - -- Promtail 采集 Kubernetes Pod 日志 -- Loki 存储与检索日志 -- Grafana 查询展示日志 - ---- - -## 一、排查思路 - -本次按“组件健康 -> 采集发现 -> 文件可读 -> 入库验证 -> 查询验证”的顺序排查。 - -### 1) 确认监控组件健康 - -先确认 Promtail/Loki/Grafana 是否都在 Running,避免在异常状态下排查配置。 - -### 2) 判断是“没采到”还是“没查到” - -通过 Promtail `/targets` 和 `/service-discovery` 页面确认是否存在 active target。 - -- 若 `0/0` 或 `0/1 unready`,说明采集端有问题 -- 若 target 正常但 Loki 无数据,再看推送或查询标签 - -### 3) 检查 Promtail 能否访问节点日志文件 - -重点确认: - -- `/var/log/pods` 是否可见 -- 采集路径是否匹配 -- 是否存在大量 `stat ... no such file or directory` - -### 4) 直连 Loki API 验证是否入库 - -绕过 Grafana,直接访问 Loki API。 - -- 若 API 无数据,问题在 Promtail 采集/推送链路 -- 若 API 有数据,问题在 Grafana 查询条件、时间范围或标签 - ---- - -## 二、根因分析 - -本次属于“Promtail 侧采集链路不完整”,主要问题如下: - -1. Kubernetes SD 目标未生效(Promtail targets 显示 `kubernetes-pods (0/0)`)。 -2. 即使加入静态采集,目标一度 `0/1 unready`。 -3. `/var/log/pods` 下日志多为符号链接,真实目标在 `/var/lib/docker/containers`。 -4. Promtail 容器未挂载 `/var/lib/docker/containers`,导致大量 `stat ... no such file or directory`。 -5. 标签维度不足,不便于按业务服务名筛选日志。 - ---- - -## 三、修复思路 - -### 1) 强化 Promtail 权限与发现能力 - -在 `deploy/k8s/monitoring/promtail.yaml` 中: - -- 补充 RBAC 资源权限: - - `nodes` - - `pods` - - `pods/log` - - `services` - - `endpoints` - - `namespaces` - -### 2) 增加静态采集兜底 - -在 `scrape_configs` 中新增 `kubernetes-pods-static`,路径: - -- `/var/log/pods/*/*/*.log` - -用于在 Kubernetes SD 临时失效时仍能采集日志。 - -### 3) 修复宿主机日志访问链路 - -Promtail DaemonSet 增加: - -- `securityContext.runAsUser: 0` -- `securityContext.runAsGroup: 0` -- 挂载 `hostPath: /var/lib/docker/containers` - -并挂载到容器内同路径只读。 - -### 4) 完善标签体系,支持按服务筛选 - -新增/保留标签: - -- `namespace` -- `pod` -- `container` -- `app` - -静态采集通过 `pipeline_stages` 从 `filename` 解析标签,并从 `pod` 生成 `app`(去除滚动后缀)。 - ---- - -## 四、关键变更文件 - -- `deploy/k8s/monitoring/promtail.yaml` - -本次 Loki 主配置与 Grafana 数据源无需改动,核心修复集中在 Promtail 采集侧。 - ---- - -## 五、排查与修复命令清单 - -> 以下命令均在项目根目录执行。 - -### 1) 组件状态检查 - -```powershell -kubectl get pods -n monitoring -o wide -kubectl get svc -n monitoring -kubectl logs -n monitoring -l app=promtail --tail=120 -kubectl logs -n monitoring -l app=loki --tail=80 -``` - -### 2) Promtail 文件系统与配置检查 - -```powershell -$pod=(kubectl get pod -n monitoring -l app=promtail -o jsonpath='{.items[0].metadata.name}') -kubectl exec -n monitoring $pod -- sh -c "ls -ld /var/log /var/log/pods /var/log/containers" -kubectl exec -n monitoring $pod -- sh -c "find /var/log/pods -name '*.log' | head -n 20" -kubectl exec -n monitoring $pod -- sh -c "cat /etc/promtail/promtail.yaml" -kubectl exec -n monitoring $pod -- sh -c "cat /run/promtail/positions.yaml | head -n 120" -``` - -### 3) Promtail Web 诊断页(targets / service-discovery) - -```powershell -$pod=(kubectl get pod -n monitoring -l app=promtail -o jsonpath='{.items[0].metadata.name}') -$job=Start-Job -ScriptBlock { param($p) kubectl port-forward -n monitoring pod/$p 19080:9080 } -ArgumentList $pod -Start-Sleep -Seconds 3 -Invoke-WebRequest -UseBasicParsing http://127.0.0.1:19080/targets | Select-Object -ExpandProperty Content -Invoke-WebRequest -UseBasicParsing http://127.0.0.1:19080/service-discovery | Select-Object -ExpandProperty Content -Stop-Job $job -ErrorAction SilentlyContinue -Remove-Job $job -Force -ErrorAction SilentlyContinue -``` - -### 4) RBAC 实测 - -```powershell -kubectl auth can-i list pods --as=system:serviceaccount:monitoring:promtail --all-namespaces -kubectl auth can-i watch pods --as=system:serviceaccount:monitoring:promtail --all-namespaces -kubectl auth can-i list namespaces --as=system:serviceaccount:monitoring:promtail -kubectl auth can-i get nodes --as=system:serviceaccount:monitoring:promtail -``` - -### 5) 应用修复并滚动重启 Promtail - -```powershell -kubectl apply -f deploy/k8s/monitoring/promtail.yaml -kubectl rollout restart ds/promtail -n monitoring -kubectl rollout status ds/promtail -n monitoring --timeout=120s -kubectl logs -n monitoring -l app=promtail --tail=120 -``` - -### 6) Loki API 直连验证 - -```powershell -$job=Start-Job -ScriptBlock { kubectl port-forward -n monitoring svc/loki 13100:3100 } -Start-Sleep -Seconds 3 -Invoke-WebRequest -UseBasicParsing "http://127.0.0.1:13100/loki/api/v1/query_range?query=%7Bjob%3D%22kubernetes-pods%22%7D&limit=10" | Select-Object -ExpandProperty Content -Stop-Job $job -ErrorAction SilentlyContinue -Remove-Job $job -Force -ErrorAction SilentlyContinue -``` - -### 7) 按 app 标签验证 - -```powershell -$job=Start-Job -ScriptBlock { kubectl port-forward -n monitoring svc/loki 13100:3100 } -Start-Sleep -Seconds 3 -Invoke-WebRequest -UseBasicParsing "http://127.0.0.1:13100/loki/api/v1/query_range?query=%7Bjob%3D%22kubernetes-pods%22%2Capp%3D~%22.+%22%7D&limit=5" | Select-Object -ExpandProperty Content -Stop-Job $job -ErrorAction SilentlyContinue -Remove-Job $job -Force -ErrorAction SilentlyContinue -``` - ---- - -## 六、Grafana 查询建议 - -建议先放大时间范围(Last 6 hours / Last 24 hours),再逐步收敛: - -```logql -{job="kubernetes-pods"} -{job="kubernetes-pods", namespace="juwan"} -{job="kubernetes-pods", app="user-rpc"} -{job="kubernetes-pods", app=~"user-rpc|snowflake|email-mq"} |= "error" -``` - ---- - -## 七、后续优化建议 - -1. 当前 Loki 使用 `emptyDir`,重建后数据会丢失;生产建议改 PVC 持久化。 -2. 可以补充 Promtail 的 `drop` 规则,减少噪音日志(如健康检查日志)。 -3. 建议在 Grafana 中预置业务 Dashboard 与告警规则(按 app + error rate)。 - ---- - -## 八、结论 - -本次无日志的核心问题不在 Loki 或 Grafana,而在 Promtail 采集链路: - -- 发现目标不稳定 + 日志文件符号链接目标未挂载 - -完成上述修复后,已可通过 Loki API 查到日志,并支持按 `app` 维度查询。 diff --git a/docs/loki-usage-guide.md b/docs/loki-usage-guide.md deleted file mode 100644 index 94ee656..0000000 --- a/docs/loki-usage-guide.md +++ /dev/null @@ -1,174 +0,0 @@ -# Loki 使用指南(日志查看) - -本文说明在当前项目中如何使用 Loki 查看 Kubernetes 日志,包括 Grafana 查询、LogQL 常用语句、命令行验证与常见排错。 - ---- - -## 1. 日志链路说明 - -当前日志链路: - -- Promtail 采集节点日志文件 -- Loki 存储与检索日志 -- Grafana 作为查询与展示入口 - -相关配置文件: - -- `deploy/k8s/monitoring/promtail.yaml` -- `deploy/k8s/monitoring/loki.yaml` -- `deploy/k8s/monitoring/grafana.yaml` - ---- - -## 2. 快速开始(Grafana 查看日志) - -### 步骤 1:确认监控组件运行 - -```bash -kubectl get pods -n monitoring -``` - -至少应看到 `promtail`、`loki`、`grafana` 为 `Running`。 - -### 步骤 2:打开 Grafana - -```bash -kubectl port-forward -n monitoring svc/grafana 3000:3000 -``` - -浏览器打开:`http://localhost:3000` - -默认账号密码(按现有配置): - -- 用户名:`admin` -- 密码:`change-me` - -### 步骤 3:进入 Explore 查询 - -- 左侧菜单进入 **Explore** -- 数据源选择 **Loki** -- 时间范围建议先设为 **Last 6 hours** 或 **Last 24 hours** -- 输入 LogQL 查询并点击 **Run query** - ---- - -## 3. 常用 LogQL 查询语句 - -### 3.1 全量日志 - -```logql -{job="kubernetes-pods"} -``` - -### 3.2 按命名空间过滤 - -```logql -{job="kubernetes-pods", namespace="juwan"} -``` - -### 3.3 按服务(app 标签)过滤 - -```logql -{job="kubernetes-pods", app="user-rpc"} -``` - -### 3.4 多服务联合过滤 - -```logql -{job="kubernetes-pods", app=~"user-rpc|snowflake|email-mq"} -``` - -### 3.5 按容器名过滤 - -```logql -{job="kubernetes-pods", container="user-rpc"} -``` - -### 3.6 关键字过滤(错误日志) - -```logql -{job="kubernetes-pods", namespace="juwan"} |= "error" -``` - -### 3.7 多关键字正则过滤 - -```logql -{job="kubernetes-pods", namespace="juwan"} |~ "(error|panic|fatal|timeout)" -``` - -### 3.8 统计最近 5 分钟错误量(按 app) - -```logql -sum by (app) (count_over_time({job="kubernetes-pods"} |~ "(?i)error|panic|fatal" [5m])) -``` - ---- - -## 4. 不经过 Grafana 的直连验证(Loki API) - -用于区分“Grafana 查询问题”与“日志未入库问题”。 - -### 4.1 端口转发 Loki - -```bash -kubectl port-forward -n monitoring svc/loki 3100:3100 -``` - -### 4.2 查询是否有流数据 - -```bash -curl "http://127.0.0.1:3100/loki/api/v1/query_range?query={job=\"kubernetes-pods\"}&limit=10" -``` - -### 4.3 查询 app 标签流 - -```bash -curl "http://127.0.0.1:3100/loki/api/v1/query_range?query={job=\"kubernetes-pods\",app=~\".+\"}&limit=10" -``` - -如果 API 返回 `result` 非空,说明 Loki 已正常入库。 - ---- - -## 5. 常见问题与处理 - -### 问题 1:Grafana 显示 No logs found - -建议按顺序检查: - -1. 时间范围是否太短(先调大到 6h/24h) -2. 查询标签是否过窄(先用 `{job="kubernetes-pods"}`) -3. Promtail 是否正常运行并有 target -4. Loki API 是否能直接查到数据 - -### 问题 2:Promtail 有 Running 但仍无日志 - -重点检查: - -- `promtail` targets 是否 `ready` -- 是否存在 `stat ... no such file or directory` -- 是否挂载日志目录(`/var/log` 与 `/var/lib/docker/containers`) -- 是否有足够 RBAC 权限(pods/nodes/namespaces 等) - -### 问题 3:查不到某个服务日志 - -建议检查: - -- 该服务 pod 是否在运行并产生日志 -- `namespace` 与 `app` 过滤条件是否正确 -- 先用 `namespace` 过滤,再逐步加 `app`、`container` 条件 - ---- - -## 6. 推荐查询习惯 - -1. 先粗后细:全量 -> namespace -> app -> container -> 关键字 -2. 先看时间范围:避免默认 1h 漏查 -3. 遇到空结果先用 Loki API 验证入库 -4. 保存常用查询到 Grafana Dashboard,便于团队复用 - ---- - -## 7. 参考 - -- Loki 故障排查文档:`docs/loki-log-troubleshooting.md` diff --git a/docs/redis-sentinel-troubleshooting.md b/docs/redis-sentinel-troubleshooting.md deleted file mode 100644 index c642b63..0000000 --- a/docs/redis-sentinel-troubleshooting.md +++ /dev/null @@ -1,779 +0,0 @@ -# Redis Sentinel 部署问题诊断与修复报告 - -**问题日期:** 2026年2月22日 -**命名空间:** juwan -**涉及资源:** user-rpc deployment, RedisSentinel - ---- - -## 📋 目录 - -1. [问题背景](#问题背景) -2. [问题现象](#问题现象) -3. [诊断过程](#诊断过程) -4. [根因分析](#根因分析) -5. [解决方案](#解决方案) -6. [修复步骤](#修复步骤) -7. [验证结果](#验证结果) -8. [后续建议](#后续建议) - ---- - -## 🎯 问题背景 - -### 部署目标 -部署一个简单的三节点 Redis Sentinel 哨兵集群作为缓存服务,供 user-rpc 服务使用。后续如有需要再扩展为分片集群。 - -### 初始配置 -在 `deploy/k8s/service/user/user-rpc.yaml` 中配置了: -- user-rpc Deployment(3副本) -- user-rpc Service -- HPA(CPU和内存) -- **RedisSentinel 资源** -- PostgreSQL Cluster - ---- - -## 🔴 问题现象 - -### 执行的操作 -```bash -kubectl apply -f .\deploy\k8s\service\user\user-rpc.yaml -``` - -### 输出结果 -``` -deployment.apps/user-rpc configured -service/user-rpc-svc unchanged -horizontalpodautoscaler.autoscaling/user-rpc-hpa-c unchanged -horizontalpodautoscaler.autoscaling/user-rpc-hpa-m unchanged -redissentinel.redis.redis.opstreelabs.in/user-redis unchanged -cluster.postgresql.cnpg.io/user-db unchanged -``` - -### 观察到的异常 -查看命名空间资源: -```bash -kubectl get all -n juwan -``` - -**发现:** -- ✅ user-api pods 正常运行 -- ✅ user-rpc pods 正常运行 -- ✅ PostgreSQL clusters 正常运行 -- ❌ **没有任何 Redis 相关的 Pod** -- ❌ **没有 Redis Service** - ---- - -## 🔍 诊断过程 - -### 步骤 1:检查 RedisSentinel 资源状态 - -**目的:** 确认 RedisSentinel 资源是否被成功创建 - -**命令:** -```bash -kubectl get redissentinel user-redis -n juwan -``` - -**输出:** -``` -NAME AGE -user-redis 9m56s -``` - -**分析:** -- ✅ RedisSentinel 资源已创建 -- ❌ 但没有创建任何 Pod -- **结论:** Operator 没有按照 RedisSentinel 规格创建实际资源 - ---- - -### 步骤 2:查看 RedisSentinel 详细信息 - -**目的:** 检查资源的详细配置和事件 - -**命令:** -```bash -kubectl describe redissentinel user-redis -n juwan -``` - -**关键输出:** -```yaml -API Version: redis.redis.opstreelabs.in/v1beta2 -Kind: RedisSentinel -Metadata: - Creation Timestamp: 2026-02-22T12:41:47Z - Finalizers: - redisSentinelFinalizer - Generation: 2 -Spec: - Cluster Size: 3 - Redis Sentinel Config: - Redis Replication Name: user-redis # ⚠️ 关键配置 - Master Group Name: myMaster - Redis Port: 6379 - Quorum: 2 -Events: # ⚠️ 没有任何事件 -``` - -**分析:** -- 配置中引用了 `redisReplicationName: user-redis` -- 没有任何 Events,说明 Operator 可能在等待某些前置条件 -- **需要进一步检查依赖资源** - ---- - -### 步骤 3:检查 Secret 资源 - -**目的:** 确认 RedisSentinel 所需的密码 Secret 是否存在 - -**命令:** -```bash -kubectl get secret user-redis -n juwan -``` - -**输出:** -``` -NAME TYPE DATA AGE -user-redis Opaque 1 5h48m -``` - -**分析:** -- ✅ Secret 存在,排除认证配置问题 - ---- - -### 步骤 4:查找 Redis Pods - -**目的:** 确认是否有 Redis Pod 使用不同的命名规则 - -**命令:** -```bash -kubectl get pods -n juwan | Select-String redis -``` - -**输出:** -``` -(空输出 - 没有找到任何 Redis Pod) -``` - -**分析:** -- ❌ 确认没有任何 Redis Pod 被创建 - ---- - -### 步骤 5:检查 StatefulSet - -**目的:** Redis 通常使用 StatefulSet 部署,检查是否有相关资源 - -**命令:** -```bash -kubectl get statefulset -n juwan -``` - -**输出:** -``` -No resources found in juwan namespace. -``` - -**分析:** -- ❌ 没有 StatefulSet 被创建 -- **结论:** Operator 完全没有执行创建动作 - ---- - -### 步骤 6:检查 Redis Operator 状态 - -**目的:** 确认 Redis Operator 本身是否正常运行 - -**命令:** -```bash -kubectl get pods -A | Select-String redis-operator -``` - -**输出:** -``` -default redis-operator-7dcf4468c9-gfbcm 1/1 Running 0 4h50m -ot-operators redis-operator-69bb645f5b-6kxkv 0/1 ErrImageNeverPull 0 4h48m -``` - -**分析:** -- ✅ default namespace 中的 Operator 正常运行 -- ⚠️ ot-operators namespace 中的 Operator 镜像拉取失败(但不影响我们的场景) - ---- - -### 步骤 7:查看 Operator 日志 - -**目的:** 从 Operator 日志中寻找线索 - -**命令:** -```bash -kubectl logs redis-operator-7dcf4468c9-gfbcm -n default --tail=50 -``` - -**关键输出:** -```json -{"level":"info","ts":"2026-02-22T08:01:56Z","msg":"Starting Controller","controller":"redissentinel"} -{"level":"info","ts":"2026-02-22T08:01:56Z","msg":"Starting workers","controller":"redissentinel","worker count":1} -``` - -**分析:** -- ✅ RedisSentinel Controller 已启动 -- ✅ 没有错误日志 -- ❌ 但也没有处理 user-redis 资源的日志 -- **推测:** Operator 在等待某个依赖资源 - ---- - -### 步骤 8:检查 RedisReplication 资源(关键发现) - -**目的:** 根据 RedisSentinel 配置中的 `redisReplicationName: user-redis`,检查对应的 RedisReplication 是否存在 - -**命令:** -```bash -kubectl get redisreplication -n juwan -``` - -**输出:** -``` -No resources found in juwan namespace. -``` - -**分析:** -- ❌ **RedisReplication 资源不存在!** -- 🔎 **这就是问题的根本原因** - ---- - -## 💡 根因分析 - -### 问题根源 - -**RedisSentinel 依赖 RedisReplication,但配置中只创建了 RedisSentinel,没有创建 RedisReplication。** - -### Redis Operator 架构理解 - -在 OpsTree Redis Operator 中,资源之间的关系如下: - -``` -┌─────────────────────────────────────────┐ -│ RedisSentinel (哨兵层) │ -│ - 3个 Sentinel 节点 │ -│ - 负责监控和自动故障转移 │ -│ - 引用: redisReplicationName │ -└──────────────┬──────────────────────────┘ - │ 监控 - ↓ -┌─────────────────────────────────────────┐ -│ RedisReplication (数据层) │ -│ - 1个 Master + N个 Replica │ -│ - 提供实际的缓存服务 │ -│ - 主从复制 │ -└─────────────────────────────────────────┘ -``` - -### 错误配置的问题 - -原始配置直接创建了 RedisSentinel,但: - -1. **缺少被监控对象:** Sentinel 需要监控一个 RedisReplication 集群 -2. **引用不存在的资源:** `redisReplicationName: user-redis` 指向一个不存在的 RedisReplication -3. **Operator 行为:** Operator 发现依赖的 RedisReplication 不存在,因此不会创建 Sentinel Pod - -### 为什么没有错误提示? - -- CRD 验证只检查语法和字段类型 -- 资源引用关系由 Operator 运行时检查 -- Operator 采用了"等待依赖"策略,而不是报错 - ---- - -## ✅ 解决方案 - -### 正确的部署顺序 - -1. **先创建 RedisReplication**(建立 Redis 主从复制集群) -2. **再创建 RedisSentinel**(监控上述复制集群) - -### 配置结构 - -```yaml -# 第一步:创建 Redis 主从复制(数据层) -apiVersion: redis.redis.opstreelabs.in/v1beta2 -kind: RedisReplication -metadata: - name: user-redis # Sentinel 将引用这个名称 - namespace: juwan -spec: - clusterSize: 3 # 1 Master + 2 Replicas - kubernetesConfig: - image: quay.io/opstree/redis:v7.0.12 - resources: - requests: - cpu: 100m - memory: 128Mi - limits: - cpu: 500m - memory: 512Mi - redisSecret: - name: user-redis - key: password - storage: - volumeClaimTemplate: - spec: - accessModes: ["ReadWriteOnce"] - resources: - requests: - storage: 1Gi # 每个 Redis 节点 1GB 存储 - ---- -# 第二步:创建 Sentinel 监控(监控层) -apiVersion: redis.redis.opstreelabs.in/v1beta2 -kind: RedisSentinel -metadata: - name: user-redis-sentinel # 使用不同的名称避免混淆 - namespace: juwan -spec: - clusterSize: 3 # 3个 Sentinel 节点(推荐奇数) - kubernetesConfig: - image: quay.io/opstree/redis-sentinel:v7.0.12 # 使用 Sentinel 专用镜像 - redisSentinelConfig: - redisReplicationName: user-redis # 引用上面的 RedisReplication - masterGroupName: mymaster - quorum: "2" # 需要 2 个 Sentinel 同意才能进行故障转移 -``` - ---- - -## 🔧 修复步骤 - -### 步骤 1:删除错误的 RedisSentinel 资源 - -**命令:** -```bash -kubectl delete redissentinel user-redis -n juwan -``` - -**输出:** -``` -redissentinel.redis.redis.opstreelabs.in "user-redis" deleted -``` - -**说明:** 删除仅创建了 CRD 实例但未创建实际 Pod 的资源 - ---- - -### 步骤 2:更新配置文件 - -修改 `deploy/k8s/service/user/user-rpc.yaml`,将单独的 RedisSentinel 替换为: -1. RedisReplication(数据层) -2. RedisSentinel(监控层) - -**变更内容:** -- 添加 `RedisReplication` 资源定义 -- 添加 `storage.volumeClaimTemplate` 配置 -- 修改 RedisSentinel 的 `metadata.name` 为 `user-redis-sentinel` -- 使用正确的 Sentinel 镜像:`quay.io/opstree/redis-sentinel:v7.0.12` -- 完善 Sentinel 配置参数 - ---- - -### 步骤 3:应用更新后的配置 - -**命令:** -```bash -kubectl apply -f .\deploy\k8s\service\user\user-rpc.yaml -``` - -**输出:** -``` -deployment.apps/user-rpc configured -service/user-rpc-svc unchanged -horizontalpodautoscaler.autoscaling/user-rpc-hpa-c unchanged -horizontalpodautoscaler.autoscaling/user-rpc-hpa-m unchanged -redisreplication.redis.redis.opstreelabs.in/user-redis created ✅ -redissentinel.redis.redis.opstreelabs.in/user-redis-sentinel created ✅ -cluster.postgresql.cnpg.io/user-db unchanged -``` - -**分析:** -- ✅ RedisReplication 成功创建 -- ✅ RedisSentinel 成功创建 -- 🎯 两个资源都是新创建(created),符合预期 - ---- - -## ✅ 验证结果 - -### 验证 1:检查 Pod 创建情况(等待 30 秒) - -**命令:** -```bash -kubectl get statefulset,pods -n juwan | Select-String -Pattern "user-redis|NAME" -``` - -**输出:** -``` -NAME READY AGE -statefulset.apps/user-redis 3/3 81s ✅ -statefulset.apps/user-redis-sentinel-sentinel 3/3 24s ✅ - -NAME READY STATUS RESTARTS AGE -pod/user-redis-0 2/2 Running 0 80s ✅ -pod/user-redis-1 2/2 Running 0 52s ✅ -pod/user-redis-2 2/2 Running 0 47s ✅ -pod/user-redis-sentinel-sentinel-0 1/1 Running 0 24s ✅ -pod/user-redis-sentinel-sentinel-1 1/1 Running 0 8s ✅ -pod/user-redis-sentinel-sentinel-2 1/1 Running 0 5s ✅ -``` - -**分析:** -- ✅ **RedisReplication** 创建了 3 个 Pod(user-redis-0/1/2) - - 每个 Pod 有 2 个容器(2/2):Redis + Exporter - - 所有 Pod 处于 Running 状态 -- ✅ **RedisSentinel** 创建了 3 个 Pod(user-redis-sentinel-sentinel-0/1/2) - - 每个 Pod 有 1 个容器(1/1):Sentinel - - 所有 Pod 处于 Running 状态 -- ✅ 创建了 2 个 StatefulSet,READY 状态为 3/3 - ---- - -### 验证 2:检查 Service 资源 - -**命令:** -```bash -kubectl get svc -n juwan | Select-String -Pattern "redis|NAME" -``` - -**输出:** -``` -NAME TYPE CLUSTER-IP PORT(S) AGE -user-redis ClusterIP 10.103.91.84 6379/TCP,9121/TCP 95s ✅ -user-redis-additional ClusterIP 10.107.228.48 6379/TCP 95s -user-redis-headless ClusterIP None 6379/TCP 95s ✅ -user-redis-master ClusterIP 10.97.120.76 6379/TCP 95s ✅ -user-redis-replica ClusterIP 10.100.213.103 6379/TCP 95s ✅ -user-redis-sentinel-sentinel ClusterIP 10.105.28.231 26379/TCP 40s ✅ -user-redis-sentinel-sentinel-additional ClusterIP 10.97.111.42 26379/TCP 39s -user-redis-sentinel-sentinel-headless ClusterIP None 26379/TCP 41s -``` - -**Service 功能说明:** - -#### Redis 数据层 Service(端口 6379) -- **user-redis-master**: 主节点服务,用于写操作 -- **user-redis-replica**: 从节点服务,用于读操作 -- **user-redis**: 通用访问入口(负载均衡到所有节点) -- **user-redis-headless**: 无头服务,用于 StatefulSet Pod 间通信 -- **user-redis-additional**: 额外的访问入口 - -#### Sentinel 监控层 Service(端口 26379) -- **user-redis-sentinel-sentinel**: Sentinel 访问入口 -- **user-redis-sentinel-sentinel-headless**: Sentinel 节点间通信 -- **user-redis-sentinel-sentinel-additional**: 额外的 Sentinel 访问入口 - ---- - -### 验证 3:检查完整的集群状态 - -**命令:** -```bash -kubectl get all -n juwan -``` - -**最终状态统计:** - -| 资源类型 | 名称 | 数量 | 状态 | -|---------|------|------|------| -| **Deployment** | user-api | 3/3 | ✅ Running | -| **Deployment** | user-rpc | 3/3 | ✅ Running | -| **StatefulSet** | cluster-example (PostgreSQL) | 3/3 | ✅ Running | -| **StatefulSet** | user-db (PostgreSQL) | 3/3 | ✅ Running | -| **StatefulSet** | user-redis (Redis 数据) | 3/3 | ✅ Running | -| **StatefulSet** | user-redis-sentinel-sentinel | 3/3 | ✅ Running | - -**Pod 总计:** 18 个(全部 Running) -**Service 总计:** 13 个 -**HPA 总计:** 6 个 - ---- - -## 📊 架构图 - -### 部署后的 Redis 架构 - -``` -┌────────────────────────────────────────────────────────────┐ -│ 应用层 (user-rpc) │ -│ │ -│ [需要添加 Redis 连接配置] │ -└──────────┬─────────────────────────────┬───────────────────┘ - │ │ - │ 写操作 │ 读操作 - ↓ ↓ - ┌─────────────┐ ┌─────────────┐ - │ user-redis- │ │ user-redis- │ - │ master │ │ replica │ - │ Service │ │ Service │ - └─────────────┘ └─────────────┘ - │ │ - └──────────┬──────────────────┘ - ↓ - ┌──────────────────────────────────────────┐ - │ RedisReplication (数据层) │ - │ │ - │ ┌──────────┐ ┌──────────┐ ┌───────┐ │ - │ │ Master │→ │ Replica │→ │Replica│ │ - │ │ redis-0 │ │ redis-1 │ │redis-2│ │ - │ └──────────┘ └──────────┘ └───────┘ │ - └──────────────────────────────────────────┘ - ↑ - │ 监控 & 故障转移 - │ - ┌──────────────────────────────────────────┐ - │ RedisSentinel (监控层) │ - │ │ - │ ┌──────────┐ ┌──────────┐ ┌───────┐ │ - │ │Sentinel-0│ │Sentinel-1│ │Sentinel-2│ - │ └──────────┘ └──────────┘ └───────┘ │ - │ │ - │ Quorum: 2/3 (多数派决策) │ - └──────────────────────────────────────────┘ -``` - ---- - -## 📝 后续建议 - -### 1. 应用集成 Redis - -user-rpc 服务目前还没有配置 Redis 连接,需要: - -#### 修改配置文件 `app/users/rpc/etc/pb.yaml` -```yaml -Name: pb.rpc -ListenOn: 0.0.0.0:8080 - -# 添加 Redis 配置(使用 Sentinel 模式) -Redis: - - Host: user-redis-sentinel-sentinel:26379 - Type: sentinel - MasterName: mymaster - Pass: ${REDIS_PASSWORD} - -# 或使用主从模式 -# Redis: -# - Host: user-redis-master:6379 # 写 -# Type: node -# Pass: ${REDIS_PASSWORD} -# - Host: user-redis-replica:6379 # 读 -# Type: node -# Pass: ${REDIS_PASSWORD} - -Etcd: - Hosts: - - etcd-service:2379 # 需要配置实际的 Etcd 地址 - Key: pb.rpc -``` - -#### 修改 Config 结构 `app/users/rpc/internal/config/config.go` -```go -package config - -import ( - "github.com/zeromicro/go-zero/core/stores/redis" - "github.com/zeromicro/go-zero/zrpc" -) - -type Config struct { - zrpc.RpcServerConf - Redis redis.RedisConf // 添加 Redis 配置 -} -``` - -#### 初始化 Redis 客户端 `app/users/rpc/internal/svc/serviceContext.go` -```go -package svc - -import ( - "github.com/zeromicro/go-zero/core/stores/redis" - "juwan-backend/app/users/rpc/internal/config" -) - -type ServiceContext struct { - Config config.Config - Redis *redis.Redis // 添加 Redis 客户端 -} - -func NewServiceContext(c config.Config) *ServiceContext { - return &ServiceContext{ - Config: c, - Redis: redis.MustNewRedis(c.Redis), // 初始化 Redis - } -} -``` - -#### 更新 Deployment 环境变量 -```yaml -# deploy/k8s/service/user/user-rpc.yaml -env: - - name: DB_URI - valueFrom: - secretKeyRef: - name: user-db-app - key: uri - - name: REDIS_PASSWORD # 添加 Redis 密码 - valueFrom: - secretKeyRef: - name: user-redis - key: password -``` - ---- - -### 2. Redis 性能监控 - -已启用 Redis Exporter(端口 9121),可以配置 Prometheus 监控: - -```yaml -apiVersion: v1 -kind: ServiceMonitor -metadata: - name: user-redis-metrics - namespace: juwan -spec: - selector: - matchLabels: - app: user-redis - endpoints: - - port: redis-exporter - interval: 30s -``` - -**监控指标:** -- redis_up: 实例状态 -- redis_connected_clients: 连接数 -- redis_memory_used_bytes: 内存使用 -- redis_commands_processed_total: 命令处理数 -- redis_master_repl_offset: 复制偏移量 - ---- - -### 3. 高可用性测试 - -#### 测试主节点故障转移 -```bash -# 1. 查找当前主节点 -kubectl exec -it user-redis-sentinel-sentinel-0 -n juwan -- redis-cli -p 26379 SENTINEL get-master-addr-by-name mymaster - -# 2. 模拟主节点故障 -kubectl delete pod user-redis-0 -n juwan - -# 3. 观察 Sentinel 的故障转移过程 -kubectl logs -f user-redis-sentinel-sentinel-0 -n juwan - -# 4. 确认新主节点 -kubectl exec -it user-redis-sentinel-sentinel-0 -n juwan -- redis-cli -p 26379 SENTINEL get-master-addr-by-name mymaster -``` - -#### 预期结果 -- Sentinel 检测到主节点下线(5 秒) -- 2/3 Sentinel 节点达成共识(quorum=2) -- 自动提升一个从节点为主节点 -- 客户端自动重连到新主节点 - ---- - -### 4. 扩展为分片集群(未来) - -当缓存数据量增长需要横向扩展时,可以迁移到 RedisCluster: - -```yaml -apiVersion: redis.redis.opstreelabs.in/v1beta2 -kind: RedisCluster -metadata: - name: user-redis-cluster - namespace: juwan -spec: - clusterSize: 6 # 3 主 + 3 从 - kubernetesConfig: - image: quay.io/opstree/redis:v7.0.12 - redisLeader: - replicas: 3 - redisFollower: - replicas: 3 - storage: - volumeClaimTemplate: - spec: - accessModes: ["ReadWriteOnce"] - resources: - requests: - storage: 5Gi -``` - -**迁移步骤:** -1. 部署新的 RedisCluster -2. 使用 redis-cli --cluster import 迁移数据 -3. 更新应用配置指向新集群 -4. 下线旧的 Sentinel 集群 - ---- - -### 5. 备份策略 - -Redis Operator 不提供自动备份,建议配置定时任务: - -```bash -# 创建 CronJob 定期执行 BGSAVE -apiVersion: batch/v1 -kind: CronJob -metadata: - name: redis-backup - namespace: juwan -spec: - schedule: "0 2 * * *" # 每天凌晨 2 点 - jobTemplate: - spec: - template: - spec: - containers: - - name: backup - image: redis:7.0.12 - command: - - /bin/sh - - -c - - | - redis-cli -h user-redis-master -a $REDIS_PASSWORD BGSAVE - # 将 /data/dump.rdb 上传到对象存储 - restartPolicy: OnFailure -``` - ---- - -## 📚 总结 - -### 关键经验 - -1. **理解资源依赖关系:** RedisSentinel 依赖 RedisReplication,部署顺序很重要 -2. **资源命名规范:** 使用清晰的名称区分不同层次的资源(如 user-redis 和 user-redis-sentinel) -3. **诊断思路:** - - 从现象(Pod 缺失)→ 资源状态(CRD 存在)→ Operator 日志 → 依赖检查 - - 逐层排查,最终定位到 RedisReplication 缺失 -4. **验证完整性:** 不仅要检查 Pod,还要验证 Service、StatefulSet 等所有相关资源 - -### 文档价值 - -本文档可用于: -- ✅ 团队知识传承 -- ✅ 类似问题的快速排查手册 -- ✅ 新成员的 Redis Operator 学习资料 -- ✅ 事后复盘和经验总结 - ---- - -**最后更新时间:** 2026年2月22日 -**文档状态:** ✅ 问题已解决,Redis 集群运行正常 -**下一步行动:** 配置应用连接 Redis diff --git a/docs/redis-services-guide.md b/docs/redis-services-guide.md deleted file mode 100644 index 7dbe018..0000000 --- a/docs/redis-services-guide.md +++ /dev/null @@ -1,1179 +0,0 @@ -# Redis Services 连接指南 - -**环境:** juwan namespace -**Redis 版本:** 7.0.12 -**部署模式:** RedisReplication + RedisSentinel -**文档日期:** 2026年2月22日 - ---- - -## 📋 目录 - -1. [Service 列表总览](#service-列表总览) -2. [Redis 数据层 Service 详解](#redis-数据层-service-详解) -3. [Sentinel 监控层 Service 详解](#sentinel-监控层-service-详解) -4. [管理工具连接方式](#管理工具连接方式) -5. [应用服务连接方式](#应用服务连接方式) -6. [连接方式对比](#连接方式对比) -7. [最佳实践建议](#最佳实践建议) -8. [故障排查](#故障排查) - ---- - -## 📊 Service 列表总览 - -当前集群中部署的 Redis 相关 Service: - -| Service 名称 | 类型 | Cluster IP | 端口 | 用途 | -|-------------|------|-----------|------|------| -| **user-redis** | ClusterIP | 10.103.91.84 | 6379, 9121 | 通用访问 + 监控 | -| **user-redis-additional** | ClusterIP | 10.107.228.48 | 6379 | 额外访问入口 | -| **user-redis-headless** | ClusterIP(None) | None | 6379 | Pod 间直接访问 | -| **user-redis-master** ⭐ | ClusterIP | 10.97.120.76 | 6379 | 主节点访问 | -| **user-redis-replica** ⭐ | ClusterIP | 10.100.213.103 | 6379 | 从节点访问 | -| **user-redis-sentinel-sentinel** ⭐⭐⭐ | ClusterIP | 10.105.28.231 | 26379 | Sentinel 访问 | -| **user-redis-sentinel-sentinel-additional** | ClusterIP | 10.97.111.42 | 26379 | 额外 Sentinel 入口 | -| **user-redis-sentinel-sentinel-headless** | ClusterIP(None) | None | 26379 | Sentinel 间通信 | - -**图例:** -- ⭐⭐⭐ 生产环境强烈推荐 -- ⭐ 生产环境可用 -- 无标记:特殊场景或内部使用 - ---- - -## 🔴 Redis 数据层 Service 详解 - -### 1. user-redis-master ⭐ - -**基本信息** -```yaml -名称: user-redis-master -类型: ClusterIP -IP: 10.97.120.76 -端口: 6379/TCP -DNS: user-redis-master.juwan.svc.cluster.local -``` - -**功能特点** -- 🎯 自动追踪当前 Redis 主节点 -- ✅ 确保所有写操作到达主节点 -- 🔄 故障转移后自动指向新主节点 -- 💪 提供最强一致性保证 - -**适用场景** -- ✅ 所有写操作(SET, HSET, ZADD 等) -- ✅ 需要强一致性的读操作 -- ✅ 事务操作(MULTI/EXEC) -- ❌ 不适合高并发读请求 - -**连接示例** -```go -// Go - go-redis -rdb := redis.NewClient(&redis.Options{ - Addr: "user-redis-master.juwan.svc.cluster.local:6379", - Password: os.Getenv("REDIS_PASSWORD"), - DB: 0, -}) - -// 写操作 -err := rdb.Set(ctx, "user:1001", "John Doe", 0).Err() -``` - -```bash -# CLI 测试 -kubectl run -it --rm redis-cli --image=redis:7.0.12 --restart=Never -n juwan -- \ - redis-cli -h user-redis-master -a SET test "hello" -``` - ---- - -### 2. user-redis-replica ⭐ - -**基本信息** -```yaml -名称: user-redis-replica -类型: ClusterIP -IP: 10.100.213.103 -端口: 6379/TCP -DNS: user-redis-replica.juwan.svc.cluster.local -``` - -**功能特点** -- 📖 负载均衡到所有从节点(当前 2 个) -- ⚡ 分散读请求,提升吞吐量 -- 🕐 可能存在轻微的复制延迟(通常 < 100ms) -- 🚫 只读模式,写操作会失败 - -**适用场景** -- ✅ 高并发读请求 -- ✅ 查询操作(GET, HGET, ZRANGE 等) -- ✅ 统计分析类查询 -- ⚠️ 对数据实时性要求不高的场景 -- ❌ 不能用于写操作 - -**连接示例** -```go -// Go - 读写分离配置 -masterClient := redis.NewClient(&redis.Options{ - Addr: "user-redis-master.juwan.svc.cluster.local:6379", - Password: os.Getenv("REDIS_PASSWORD"), -}) - -replicaClient := redis.NewClient(&redis.Options{ - Addr: "user-redis-replica.juwan.svc.cluster.local:6379", - Password: os.Getenv("REDIS_PASSWORD"), - ReadOnly: true, -}) - -// 写操作用 master -masterClient.Set(ctx, "counter", 100, 0) - -// 读操作用 replica -val, err := replicaClient.Get(ctx, "counter").Result() -``` - ---- - -### 3. user-redis - -**基本信息** -```yaml -名称: user-redis -类型: ClusterIP -IP: 10.103.91.84 -端口: 6379/TCP (Redis), 9121/TCP (Exporter) -DNS: user-redis.juwan.svc.cluster.local -``` - -**功能特点** -- 🔀 负载均衡到所有 Redis 节点(主 + 从) -- 📊 端口 9121 暴露 Prometheus 指标 -- ⚠️ 写操作可能路由到从节点导致失败 - -**适用场景** -- ✅ Prometheus 监控抓取(端口 9121) -- ⚠️ 测试环境的简单访问 -- ❌ 不推荐生产环境读写操作 - -**监控配置** -```yaml -# Prometheus ServiceMonitor -apiVersion: monitoring.coreos.com/v1 -kind: ServiceMonitor -metadata: - name: redis-metrics - namespace: juwan -spec: - selector: - matchLabels: - app: redis - endpoints: - - port: redis-exporter - interval: 30s - path: /metrics -``` - ---- - -### 4. user-redis-additional - -**基本信息** -```yaml -名称: user-redis-additional -类型: ClusterIP -IP: 10.107.228.48 -端口: 6379/TCP -``` - -**功能特点** -- 功能类似 user-redis -- 提供额外的访问入口 -- 用于多租户或网络隔离场景 - -**适用场景** -- 特殊网络策略场景 -- 多应用隔离访问 -- 备用访问点 - ---- - -### 5. user-redis-headless - -**基本信息** -```yaml -名称: user-redis-headless -类型: ClusterIP (Headless - None) -端口: 6379/TCP -DNS: - - user-redis-0.user-redis-headless.juwan.svc.cluster.local - - user-redis-1.user-redis-headless.juwan.svc.cluster.local - - user-redis-2.user-redis-headless.juwan.svc.cluster.local -``` - -**功能特点** -- 🎯 直接返回所有 Pod IP,不做负载均衡 -- 🔗 用于 StatefulSet Pod 间通信 -- 📡 Redis 主从复制使用此服务发现 - -**适用场景** -- ✅ 内部复制通信 -- ✅ 集群管理操作 -- ✅ 需要直接访问特定 Pod -- ❌ 不适合应用层使用 - -**直接访问示例** -```bash -# 直接连接 user-redis-0 -redis-cli -h user-redis-0.user-redis-headless.juwan.svc.cluster.local -a - -# DNS 解析会返回具体 Pod IP -nslookup user-redis-headless.juwan.svc.cluster.local -``` - ---- - -## 🟡 Sentinel 监控层 Service 详解 - -### 1. user-redis-sentinel-sentinel ⭐⭐⭐ - -**基本信息** -```yaml -名称: user-redis-sentinel-sentinel -类型: ClusterIP -IP: 10.105.28.231 -端口: 26379/TCP -DNS: user-redis-sentinel-sentinel.juwan.svc.cluster.local -``` - -**功能特点** -- 🛡️ 提供高可用 Redis 访问 -- 🔄 自动发现主节点 -- ⚡ 主节点故障时自动切换 -- 📍 客户端自动跟踪主节点变化 - -**Sentinel 架构** -``` -应用 → Sentinel Service → Sentinel 节点 (3个) - ↓ - 监控 Redis 集群 - ↓ - 自动发现当前主节点位置 -``` - -**适用场景** -- ✅✅✅ **生产环境强烈推荐** -- ✅ 需要自动故障转移 -- ✅ 高可用架构 -- ✅ 无需手动处理主从切换 - -**连接示例** -```go -// Go - go-redis Sentinel 模式 -rdb := redis.NewFailoverClient(&redis.FailoverOptions{ - MasterName: "mymaster", - SentinelAddrs: []string{ - "user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379", - }, - Password: os.Getenv("REDIS_PASSWORD"), - DB: 0, - - // 连接池配置 - PoolSize: 10, - MinIdleConns: 5, - - // 超时配置 - DialTimeout: 5 * time.Second, - ReadTimeout: 3 * time.Second, - WriteTimeout: 3 * time.Second, -}) - -// 使用方式与普通客户端完全一致 -err := rdb.Set(ctx, "key", "value", 0).Err() -val, err := rdb.Get(ctx, "key").Result() -``` - -```python -# Python - redis-py Sentinel 模式 -from redis.sentinel import Sentinel - -sentinel = Sentinel([ - ('user-redis-sentinel-sentinel.juwan.svc.cluster.local', 26379) -], socket_timeout=0.5) - -# 获取主节点(写) -master = sentinel.master_for('mymaster', - password=os.getenv('REDIS_PASSWORD'), - socket_timeout=0.5) -master.set('key', 'value') - -# 获取从节点(读) -slave = sentinel.slave_for('mymaster', - password=os.getenv('REDIS_PASSWORD'), - socket_timeout=0.5) -value = slave.get('key') -``` - -```yaml -# Spring Boot application.yml -spring: - redis: - sentinel: - master: mymaster - nodes: - - user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379 - password: ${REDIS_PASSWORD} -``` - -**Sentinel 命令查询** -```bash -# 查看主节点信息 -kubectl exec -it user-redis-sentinel-sentinel-0 -n juwan -- \ - redis-cli -p 26379 SENTINEL get-master-addr-by-name mymaster - -# 查看所有监控的主节点 -kubectl exec -it user-redis-sentinel-sentinel-0 -n juwan -- \ - redis-cli -p 26379 SENTINEL masters - -# 查看从节点列表 -kubectl exec -it user-redis-sentinel-sentinel-0 -n juwan -- \ - redis-cli -p 26379 SENTINEL slaves mymaster - -# 查看 Sentinel 节点 -kubectl exec -it user-redis-sentinel-sentinel-0 -n juwan -- \ - redis-cli -p 26379 SENTINEL sentinels mymaster -``` - ---- - -### 2. user-redis-sentinel-sentinel-additional - -**基本信息** -```yaml -名称: user-redis-sentinel-sentinel-additional -类型: ClusterIP -IP: 10.97.111.42 -端口: 26379/TCP -``` - -**功能特点** -- 功能同 user-redis-sentinel-sentinel -- 提供额外访问入口 -- 用于多客户端分离 - -**适用场景** -- 多应用共享 Redis 时的访问隔离 -- 网络策略要求 -- 流量分离 - ---- - -### 3. user-redis-sentinel-sentinel-headless - -**基本信息** -```yaml -名称: user-redis-sentinel-sentinel-headless -类型: ClusterIP (Headless - None) -端口: 26379/TCP -``` - -**功能特点** -- Sentinel 节点间通信 -- 选举和投票 -- 状态同步 - -**适用场景** -- 内部使用,应用层无需关注 - ---- - -## 🔧 管理工具连接方式 - -### 使用 kubectl port-forward(推荐) - -#### 方式一:连接主节点 -```bash -# 转发主节点服务到本地 -kubectl port-forward -n juwan svc/user-redis-master 6379:6379 - -# 或直接转发到 Pod -kubectl port-forward -n juwan pod/user-redis-0 6379:6379 -``` - -然后在管理工具中配置: -- **Host**: localhost -- **Port**: 6379 -- **Password**: (见下方获取方法) - -#### 方式二:使用 LoadBalancer(生产不推荐) -```yaml -# 临时暴露服务(仅用于调试) -apiVersion: v1 -kind: Service -metadata: - name: redis-external - namespace: juwan -spec: - type: LoadBalancer - selector: - app: user-redis - ports: - - port: 6379 - targetPort: 6379 -``` - ---- - -### 获取 Redis 密码 - -```bash -# 方式一:直接输出 -kubectl get secret user-redis -n juwan -o jsonpath='{.data.password}' | base64 -d - -# 方式二:设置为环境变量 -export REDIS_PASSWORD=$(kubectl get secret user-redis -n juwan -o jsonpath='{.data.password}' | base64 -d) -echo $REDIS_PASSWORD - -# 方式三:保存到文件 -kubectl get secret user-redis -n juwan -o jsonpath='{.data.password}' | base64 -d > redis-password.txt -``` - ---- - -### 常用管理工具配置 - -#### Redis Desktop Manager (RedisInsight) -``` -Connection Name: juwan-user-redis -Host: localhost (使用 port-forward) -Port: 6379 -Username: (留空) -Password: (从 Secret 获取) -``` - -#### redis-cli -```bash -# 在集群内访问 -kubectl exec -it user-redis-0 -n juwan -- redis-cli -a - -# 从本地访问(需要 port-forward) -redis-cli -h localhost -p 6379 -a - -# 常用命令 -INFO replication # 查看复制状态 -INFO stats # 查看统计信息 -CLUSTER INFO # 查看集群信息 -KEYS * # 查看所有 key(生产谨慎使用) -``` - ---- - -## 💻 应用服务连接方式 - -### 方案一:Sentinel 模式(生产强烈推荐)⭐⭐⭐ - -**优点** -- ✅ 自动故障转移 -- ✅ 高可用 -- ✅ 无需手动切换 -- ✅ 客户端自动重连 -- ✅ 支持读写分离 - -#### Go-Zero 配置 - -**配置文件** `app/users/rpc/etc/pb.yaml` -```yaml -Name: pb.rpc -ListenOn: 0.0.0.0:9001 - -# Redis Sentinel 配置 -Redis: - Type: sentinel - MasterName: mymaster - SentinelAddrs: - - user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379 - Pass: ${REDIS_PASSWORD} # 从环境变量读取 - -Etcd: - Hosts: - - etcd-service.juwan.svc.cluster.local:2379 - Key: pb.rpc -``` - -**Config 结构** `app/users/rpc/internal/config/config.go` -```go -package config - -import ( - "github.com/zeromicro/go-zero/core/stores/redis" - "github.com/zeromicro/go-zero/zrpc" -) - -type Config struct { - zrpc.RpcServerConf - Redis redis.RedisConf -} -``` - -**初始化 Redis** `app/users/rpc/internal/svc/serviceContext.go` -```go -package svc - -import ( - "github.com/zeromicro/go-zero/core/stores/redis" - "juwan-backend/app/users/rpc/internal/config" -) - -type ServiceContext struct { - Config config.Config - Redis *redis.Redis -} - -func NewServiceContext(c config.Config) *ServiceContext { - return &ServiceContext{ - Config: c, - Redis: redis.MustNewRedis(c.Redis), - } -} -``` - -**使用示例** `app/users/rpc/internal/logic/getUsersByIdLogic.go` -```go -func (l *GetUsersByIdLogic) GetUsersById(in *pb.GetUsersByIdReq) (*pb.GetUsersByIdResp, error) { - // 尝试从缓存获取 - cacheKey := fmt.Sprintf("user:%d", in.Id) - cached, err := l.svcCtx.Redis.Get(cacheKey) - if err == nil && cached != "" { - // 缓存命中 - var user pb.User - json.Unmarshal([]byte(cached), &user) - return &pb.GetUsersByIdResp{User: &user}, nil - } - - // 从数据库查询 - user := l.fetchUserFromDB(in.Id) - - // 写入缓存 - userJSON, _ := json.Marshal(user) - l.svcCtx.Redis.Setex(cacheKey, string(userJSON), 3600) // 1小时过期 - - return &pb.GetUsersByIdResp{User: user}, nil -} -``` - -#### Go (原生 go-redis) - -```go -package main - -import ( - "context" - "github.com/redis/go-redis/v9" - "time" -) - -func NewRedisClient() *redis.Client { - rdb := redis.NewFailoverClient(&redis.FailoverOptions{ - MasterName: "mymaster", - SentinelAddrs: []string{ - "user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379", - }, - Password: os.Getenv("REDIS_PASSWORD"), - DB: 0, - - // 连接池配置 - PoolSize: 10, - MinIdleConns: 5, - - // 超时配置 - DialTimeout: 5 * time.Second, - ReadTimeout: 3 * time.Second, - WriteTimeout: 3 * time.Second, - - // 重试配置 - MaxRetries: 3, - MinRetryBackoff: 8 * time.Millisecond, - MaxRetryBackoff: 512 * time.Millisecond, - }) - - // 测试连接 - ctx := context.Background() - if err := rdb.Ping(ctx).Err(); err != nil { - panic(err) - } - - return rdb -} -``` - -#### Python (redis-py) - -```python -from redis.sentinel import Sentinel -import os - -# 初始化 Sentinel -sentinel = Sentinel([ - ('user-redis-sentinel-sentinel.juwan.svc.cluster.local', 26379) -], socket_timeout=5.0) - -# 获取主节点连接(用于写操作) -master = sentinel.master_for( - 'mymaster', - password=os.getenv('REDIS_PASSWORD'), - socket_timeout=3.0, - socket_connect_timeout=5.0, - socket_keepalive=True, - socket_keepalive_options={}, - connection_pool_kwargs={ - 'max_connections': 50 - } -) - -# 获取从节点连接(用于读操作) -slave = sentinel.slave_for( - 'mymaster', - password=os.getenv('REDIS_PASSWORD'), - socket_timeout=3.0 -) - -# 使用 -master.set('key', 'value') -value = slave.get('key') -``` - -#### Java (Spring Data Redis) - -```yaml -# application.yml -spring: - redis: - timeout: 3000ms - password: ${REDIS_PASSWORD} - sentinel: - master: mymaster - nodes: - - user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379 - lettuce: - pool: - max-active: 10 - max-idle: 5 - min-idle: 2 - max-wait: 3000ms -``` - -```java -@Configuration -public class RedisConfig { - - @Bean - public RedisTemplate redisTemplate( - RedisConnectionFactory connectionFactory) { - RedisTemplate template = new RedisTemplate<>(); - template.setConnectionFactory(connectionFactory); - template.setKeySerializer(new StringRedisSerializer()); - template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); - return template; - } -} -``` - ---- - -### 方案二:主从分离模式 ⭐ - -**优点** -- ✅ 读写性能优化 -- ✅ 配置清晰 -- ⚠️ 需要手动处理故障 - -**Go-Zero 配置** -```yaml -# app/users/rpc/etc/pb.yaml -Redis: - - Host: user-redis-master.juwan.svc.cluster.local:6379 - Type: node - Pass: ${REDIS_PASSWORD} - - Host: user-redis-replica.juwan.svc.cluster.local:6379 - Type: node - Pass: ${REDIS_PASSWORD} -``` - -**代码示例** -```go -type ServiceContext struct { - Config config.Config - RedisMaster *redis.Redis // 写操作 - RedisReplica *redis.Redis // 读操作 -} - -func NewServiceContext(c config.Config) *ServiceContext { - return &ServiceContext{ - Config: c, - RedisMaster: redis.MustNewRedis(c.Redis[0]), // master - RedisReplica: redis.MustNewRedis(c.Redis[1]), // replica - } -} - -// 写操作 -l.svcCtx.RedisMaster.Set("key", "value") - -// 读操作 -val, _ := l.svcCtx.RedisReplica.Get("key") -``` - ---- - -### 方案三:简单模式(仅测试环境) - -```yaml -Redis: - Host: user-redis-master.juwan.svc.cluster.local:6379 - Type: node - Pass: ${REDIS_PASSWORD} -``` - ---- - -### Kubernetes Deployment 配置 - -```yaml -# deploy/k8s/service/user/user-rpc.yaml -apiVersion: apps/v1 -kind: Deployment -metadata: - name: user-rpc - namespace: juwan -spec: - template: - spec: - containers: - - name: user-rpc - image: user-rpc:v1 - env: - # 数据库连接 - - name: DB_URI - valueFrom: - secretKeyRef: - name: user-db-app - key: uri - - # Redis 密码 - - name: REDIS_PASSWORD - valueFrom: - secretKeyRef: - name: user-redis - key: password - - # 健康检查 - readinessProbe: - tcpSocket: - port: 9001 - initialDelaySeconds: 10 - periodSeconds: 10 - - livenessProbe: - tcpSocket: - port: 9001 - initialDelaySeconds: 15 - periodSeconds: 20 -``` - ---- - -## 📊 连接方式对比 - -| 连接方式 | 优点 | 缺点 | 故障转移 | 读写分离 | 复杂度 | 推荐度 | -|---------|------|------|---------|---------|--------|--------| -| **Sentinel 模式** | 自动高可用,客户端自动切换 | 配置稍复杂 | ✅ 自动 | ✅ 支持 | 中 | ⭐⭐⭐⭐⭐ | -| **主从分离** | 性能优化,逻辑清晰 | 需手动处理故障 | ❌ 手动 | ✅ 支持 | 中 | ⭐⭐⭐ | -| **仅连 Master** | 配置简单,强一致性 | 单点故障,读性能差 | ❌ 手动 | ❌ 不支持 | 低 | ⭐⭐ | -| **仅连 Replica** | 读性能好 | 只读,不能写入 | ❌ 手动 | ✅ 仅读 | 低 | ⭐ | -| **连 user-redis** | 极简单 | 性能差,不可靠 | ❌ 无 | ❌ 不支持 | 低 | ❌ | - ---- - -## 🎯 最佳实践建议 - -### 生产环境(强烈推荐) - -```yaml -# 使用 Sentinel 模式 -Redis: - Type: sentinel - MasterName: mymaster - SentinelAddrs: - - user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379 - Pass: ${REDIS_PASSWORD} - - # 连接池配置 - PoolSize: 10 - MinIdleConns: 5 - - # 超时配置 - DialTimeout: 5s - ReadTimeout: 3s - WriteTimeout: 3s - - # 重试配置 - MaxRetries: 3 -``` - -**理由** -- ✅ 自动故障转移,RTO < 30秒 -- ✅ 客户端无感知切换 -- ✅ 无需人工介入 -- ✅ 久经考验的成熟方案 - ---- - -### 开发/测试环境 - -```yaml -# 简化配置,直连主节点 -Redis: - Host: user-redis-master.juwan.svc.cluster.local:6379 - Type: node - Pass: ${REDIS_PASSWORD} -``` - -或使用 port-forward: -```bash -kubectl port-forward -n juwan svc/user-redis-master 6379:6379 -``` - ---- - -### 性能优化建议 - -#### 1. 连接池配置 -```go -PoolSize: runtime.NumCPU() * 2, // CPU 数量的 2 倍 -MinIdleConns: runtime.NumCPU(), // CPU 数量 -MaxConnAge: 30 * time.Minute, // 连接最大存活时间 -``` - -#### 2. 超时配置 -```go -DialTimeout: 5 * time.Second, // 连接超时 -ReadTimeout: 3 * time.Second, // 读超时 -WriteTimeout: 3 * time.Second, // 写超时 -PoolTimeout: 4 * time.Second, // 获取连接超时 -``` - -#### 3. 命令优化 -```go -// ❌ 避免:循环中多次调用 -for i := 0; i < 1000; i++ { - rdb.Set(ctx, fmt.Sprintf("key:%d", i), i) -} - -// ✅ 推荐:使用 Pipeline -pipe := rdb.Pipeline() -for i := 0; i < 1000; i++ { - pipe.Set(ctx, fmt.Sprintf("key:%d", i), i, 0) -} -pipe.Exec(ctx) -``` - -#### 4. 缓存策略 -```go -// Cache-Aside Pattern -func GetUser(id int64) (*User, error) { - // 1. 先查缓存 - cacheKey := fmt.Sprintf("user:%d", id) - cached, err := rdb.Get(ctx, cacheKey).Result() - if err == nil { - var user User - json.Unmarshal([]byte(cached), &user) - return &user, nil - } - - // 2. 缓存未命中,查数据库 - user := queryFromDB(id) - - // 3. 写入缓存 - userJSON, _ := json.Marshal(user) - rdb.Set(ctx, cacheKey, userJSON, 1*time.Hour) - - return user, nil -} -``` - ---- - -### 监控配置 - -#### Prometheus 指标采集 -```yaml -apiVersion: monitoring.coreos.com/v1 -kind: ServiceMonitor -metadata: - name: redis-exporter - namespace: juwan -spec: - selector: - matchLabels: - app: user-redis - endpoints: - - port: redis-exporter # 端口 9121 - interval: 30s - path: /metrics -``` - -#### 关键指标监控 -```yaml -# 告警规则示例 -groups: - - name: redis - rules: - # Redis 实例宕机 - - alert: RedisDown - expr: redis_up == 0 - for: 1m - annotations: - summary: "Redis instance down" - - # 内存使用率过高 - - alert: RedisMemoryHigh - expr: redis_memory_used_bytes / redis_memory_max_bytes > 0.9 - for: 5m - annotations: - summary: "Redis memory usage > 90%" - - # 连接数过高 - - alert: RedisConnectionsHigh - expr: redis_connected_clients > 1000 - for: 5m - annotations: - summary: "Redis connections > 1000" -``` - ---- - -### 安全建议 - -#### 1. 密码管理 -```bash -# 定期轮换密码 -kubectl create secret generic user-redis \ - --from-literal=password=$(openssl rand -base64 32) \ - --dry-run=client -o yaml | kubectl apply -f - - -# 重启 Redis Pods 使新密码生效 -kubectl rollout restart statefulset/user-redis -n juwan -``` - -#### 2. 网络策略 -```yaml -apiVersion: networking.k8s.io/v1 -kind: NetworkPolicy -metadata: - name: redis-access - namespace: juwan -spec: - podSelector: - matchLabels: - app: user-redis - policyTypes: - - Ingress - ingress: - # 只允许同命名空间的 user-rpc 访问 - - from: - - podSelector: - matchLabels: - app: user-rpc - ports: - - protocol: TCP - port: 6379 -``` - -#### 3. TLS 加密(可选) -```yaml -# Redis TLS 配置 -apiVersion: redis.redis.opstreelabs.in/v1beta2 -kind: RedisReplication -metadata: - name: user-redis -spec: - TLS: - enabled: true - secret: - secretName: redis-tls-cert -``` - ---- - -## 🔍 故障排查 - -### 1. 连接失败 - -**症状** -``` -Error: dial tcp 10.97.120.76:6379: i/o timeout -``` - -**排查步骤** -```bash -# 1. 检查 Service 是否存在 -kubectl get svc user-redis-master -n juwan - -# 2. 检查 Endpoints -kubectl get endpoints user-redis-master -n juwan - -# 3. 检查 Pod 状态 -kubectl get pods -l app=user-redis -n juwan - -# 4. 测试网络连通性 -kubectl run -it --rm netshoot --image=nicolaka/netshoot --restart=Never -n juwan -- \ - nc -zv user-redis-master 6379 - -# 5. 查看 Pod 日志 -kubectl logs user-redis-0 -n juwan -c redis -``` - ---- - -### 2. 认证失败 - -**症状** -``` -Error: NOAUTH Authentication required -``` - -**解决方法** -```bash -# 1. 确认 Secret 存在 -kubectl get secret user-redis -n juwan - -# 2. 验证密码 -PASSWORD=$(kubectl get secret user-redis -n juwan -o jsonpath='{.data.password}' | base64 -d) -echo $PASSWORD - -# 3. 测试连接 -kubectl exec -it user-redis-0 -n juwan -- redis-cli -a $PASSWORD PING -``` - ---- - -### 3. 主从复制异常 - -**症状** -``` -Warning: Redis replica lag is high -``` - -**排查** -```bash -# 在主节点执行 -kubectl exec -it user-redis-0 -n juwan -- redis-cli -a INFO replication - -# 查看输出 -# role:master -# connected_slaves:2 -# slave0:ip=10.244.1.10,port=6379,state=online,offset=1234,lag=0 -# slave1:ip=10.244.2.15,port=6379,state=online,offset=1234,lag=0 -``` - -**如果 lag 过大** -```bash -# 检查网络延迟 -kubectl exec -it user-redis-0 -n juwan -- ping user-redis-1 - -# 检查 Redis 性能 -kubectl exec -it user-redis-0 -n juwan -- redis-cli -a INFO stats -``` - ---- - -### 4. Sentinel 无法发现主节点 - -**症状** -``` -Error: sentinel: no master found -``` - -**排查** -```bash -# 1. 检查 Sentinel 状态 -kubectl exec -it user-redis-sentinel-sentinel-0 -n juwan -- \ - redis-cli -p 26379 SENTINEL masters - -# 2. 检查主节点地址 -kubectl exec -it user-redis-sentinel-sentinel-0 -n juwan -- \ - redis-cli -p 26379 SENTINEL get-master-addr-by-name mymaster - -# 3. 查看 Sentinel 日志 -kubectl logs user-redis-sentinel-sentinel-0 -n juwan - -# 4. 手动触发故障转移(慎用) -kubectl exec -it user-redis-sentinel-sentinel-0 -n juwan -- \ - redis-cli -p 26379 SENTINEL failover mymaster -``` - ---- - -### 5. 性能问题 - -**慢查询分析** -```bash -# 查看慢查询 -kubectl exec -it user-redis-0 -n juwan -- \ - redis-cli -a SLOWLOG GET 10 - -# 设置慢查询阈值(10ms) -kubectl exec -it user-redis-0 -n juwan -- \ - redis-cli -a CONFIG SET slowlog-log-slower-than 10000 -``` - -**命令统计** -```bash -# 查看命令统计 -kubectl exec -it user-redis-0 -n juwan -- \ - redis-cli -a INFO commandstats -``` - -**内存分析** -```bash -# 查看内存使用 -kubectl exec -it user-redis-0 -n juwan -- \ - redis-cli -a INFO memory - -# 查看大 key -kubectl exec -it user-redis-0 -n juwan -- \ - redis-cli -a --bigkeys -``` - ---- - -## 📚 参考资源 - -### 官方文档 -- [Redis Sentinel Documentation](https://redis.io/docs/management/sentinel/) -- [go-redis Documentation](https://redis.uptrace.dev/) -- [OpsTree Redis Operator](https://ot-redis-operator.netlify.app/) - -### 客户端库 -- Go: [github.com/redis/go-redis/v9](https://github.com/redis/go-redis) -- Python: [redis-py](https://github.com/redis/redis-py) -- Java: [Spring Data Redis](https://spring.io/projects/spring-data-redis) -- Node.js: [ioredis](https://github.com/luin/ioredis) - -### 监控工具 -- [RedisInsight](https://redis.com/redis-enterprise/redis-insight/) -- [Redis Exporter](https://github.com/oliver006/redis_exporter) -- Grafana Dashboard: [Redis Dashboard 11835](https://grafana.com/grafana/dashboards/11835) - ---- - -## 📝 更新日志 - -| 日期 | 版本 | 变更内容 | -|------|------|---------| -| 2026-02-22 | 1.0 | 初始版本,包含完整的 Service 介绍和连接指南 | - ---- - -**文档维护者**: DevOps Team -**最后更新**: 2026年2月22日 -**下一次审查**: 2026年3月22日 diff --git a/docs/redis-username-discovery.md b/docs/redis-username-discovery.md deleted file mode 100644 index 312026a..0000000 --- a/docs/redis-username-discovery.md +++ /dev/null @@ -1,1068 +0,0 @@ -# Redis 用户名查找和认证配置指南 - -**问题日期:** 2026年2月22日 -**环境:** juwan namespace -**Redis 版本:** 7.0.12 -**部署模式:** RedisReplication (OpsTree Operator) - ---- - -## 📋 目录 - -1. [问题背景](#问题背景) -2. [Redis 认证机制演进](#redis-认证机制演进) -3. [诊断过程](#诊断过程) -4. [用户名发现结果](#用户名发现结果) -5. [各语言连接配置](#各语言连接配置) -6. [常见问题解答](#常见问题解答) -7. [安全建议](#安全建议) - ---- - -## 🎯 问题背景 - -### 问题描述 -在配置应用连接 Redis 时,发现只有密码信息(存储在 Kubernetes Secret `user-redis` 中),但不清楚: -- Redis 是否需要用户名? -- 如果需要,用户名是什么? -- 如何查找 Redis 的用户名配置? - -### 初始已知信息 -```yaml -Secret: user-redis -Namespace: juwan -Key: password -Value: (base64 编码的密码) -``` - -### 疑问 -- 使用 RedisInsight 等管理工具时,Username 字段应该填什么? -- 应用代码中是否需要配置 Username? -- 不同 Redis 版本的认证方式是否有差异? - ---- - -## 📚 Redis 认证机制演进 - -### Redis 6.0 之前:传统密码认证 - -**特点:** -- ❌ 不支持多用户 -- ✅ 只需配置一个全局密码 -- 🔧 配置项:`requirepass` -- 👤 隐式用户名:`default`(客户端不需要指定) - -**配置示例:** -```conf -# redis.conf -requirepass mypassword -``` - -**连接方式:** -```bash -# 只需要密码 -redis-cli -h host -p 6379 -a mypassword - -# 或使用 AUTH 命令 -redis-cli -h host -p 6379 -> AUTH mypassword -``` - -**优点:** -- ✅ 配置简单 -- ✅ 向后兼容 - -**缺点:** -- ❌ 所有客户端共享同一密码 -- ❌ 无法区分不同应用的访问权限 -- ❌ 无法限制特定命令 -- ❌ 审计困难 - ---- - -### Redis 6.0+:ACL(访问控制列表) - -**引入时间:** Redis 6.0 (2020年5月) - -**新特性:** -- ✅ 支持多用户 -- ✅ 每个用户有独立的密码 -- ✅ 细粒度权限控制(命令、key、channel) -- ✅ 默认用户 `default` 兼容旧版本 -- ✅ 支持运行时动态修改 - -**架构对比:** -``` -传统模式: -┌─────────────┐ -│ 所有客户端 │ -└──────┬──────┘ - │ (一个密码) - ↓ -┌─────────────┐ -│ Redis │ -│ (全权限) │ -└─────────────┘ - -ACL 模式: -┌────────┐ ┌────────┐ ┌────────┐ -│ App A │ │ App B │ │ Admin │ -└───┬────┘ └───┬────┘ └───┬────┘ - │ user1 │ user2 │ admin - │ (读写) │ (只读) │ (全部) - ↓ ↓ ↓ -┌───────────────────────────────┐ -│ Redis ACL │ -│ user1: +@write +@read │ -│ user2: +@read ~cache:* │ -│ admin: +@all ~* │ -└───────────────────────────────┘ -``` - -**ACL 规则语法:** -``` -user on ~ + - -示例: -user alice on >secret123 ~* +@all # 全部权限 -user bob on >pass456 ~cache:* +get +set # 只能操作 cache:* 的 key -user readonly on >readonly ~* +@read -@write # 只读权限 -``` - ---- - -## 🔍 诊断过程 - -### 步骤 1:确认 Pod 容器名称 - -**目的:** 找到 Redis 容器的正确名称,以便执行命令 - -**命令:** -```bash -kubectl get pod user-redis-0 -n juwan -o jsonpath='{.spec.containers[*].name}' -``` - -**输出:** -``` -user-redis redis-exporter -``` - -**分析:** -- ✅ Pod 中有 2 个容器 -- 📦 `user-redis`:Redis 主容器 -- 📊 `redis-exporter`:Prometheus 监控容器 -- 🎯 **需要连接到 `user-redis` 容器** - -**关键信息:** -```yaml -Pod: user-redis-0 -Containers: - - name: user-redis # ← Redis 服务容器 - image: quay.io/opstree/redis:v7.0.12 - port: 6379 - - name: redis-exporter # ← 监控容器 - image: quay.io/opstree/redis-exporter - port: 9121 -``` - ---- - -### 步骤 2:获取 Redis 密码 - -**目的:** 从 Kubernetes Secret 中提取密码,用于认证 - -**命令:** -```bash -kubectl get secret user-redis -n juwan -o jsonpath='{.data.password}' | base64 -d -``` - -**输出:** -``` -<密码内容> # 实际密码已隐藏 -``` - -**说明:** -- Secret 中的数据以 base64 编码存储 -- 需要解码才能得到明文密码 -- `base64 -d` 在 Linux/Mac 上;Windows PowerShell 需要用其他方法 - -**Windows PowerShell 解码方法:** -```powershell -$encoded = kubectl get secret user-redis -n juwan -o jsonpath='{.data.password}' -[System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($encoded)) -``` - ---- - -### 步骤 3:检查 ACL 配置(关键步骤) - -**目的:** 查询 Redis 的用户配置,确认是否启用 ACL 以及有哪些用户 - -**命令:** -```bash -kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ - redis-cli -a $(kubectl get secret user-redis -n juwan -o jsonpath='{.data.password}' | base64 -d) \ - ACL LIST -``` - -**命令解析:** -```bash -kubectl exec -it user-redis-0 \ # 在 user-redis-0 Pod 中执行命令 - -n juwan \ # 命名空间 - -c user-redis \ # 指定容器 - -- \ # 分隔符 - redis-cli \ # Redis 命令行工具 - -a $(kubectl get secret ...) \ # 使用密码认证 - ACL LIST # 列出所有 ACL 用户 -``` - -**输出:** -``` -Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe. -1) "user default on #e3e7d4b9413497efc274c747b2ee88023e00e6416080db92c7bdd49a73f32d3d ~* &* +@all" -``` - -**输出分析:** - -#### 警告信息 -``` -Warning: Using a password with '-a' option on the command line interface may not be safe. -``` -- ⚠️ 安全提醒:命令行中的密码可能被历史记录或进程列表泄露 -- 💡 生产环境建议:使用配置文件或环境变量传递密码 - -#### ACL 规则详解 -``` -user default on #e3e7d4b...73f32d3d ~* &* +@all -| | | | | | | -| | | | | | └─ 权限:所有命令 -| | | | | └──── 所有 Pub/Sub channel -| | | | └─────── 所有 key pattern -| | | └──────────────────────────── 密码哈希(SHA256) -| | └─────────────────────────────── 状态:已启用 -| └─────────────────────────────────────── 用户名 -└──────────────────────────────────────────── 类型:用户 -``` - -**权限标识含义:** - -| 标识 | 含义 | 说明 | -|-----|------|------| -| `on` | 用户已启用 | 可以登录 | -| `off` | 用户已禁用 | 无法登录 | -| `~*` | Key Pattern | `*` = 所有 key;`~cache:*` = 只能访问 cache 开头的 key | -| `&*` | Pub/Sub Pattern | `*` = 所有 channel | -| `+@all` | 命令权限 | `@all` = 所有命令;`+get +set` = 只能用 GET/SET | -| `-@dangerous` | 禁止命令组 | 禁止危险命令(FLUSHDB, KEYS 等)| -| `#hash` | 密码哈希 | SHA256 哈希值,不存储明文 | - -**命令组示例:** -- `@read`:只读命令(GET, HGET, LRANGE 等) -- `@write`:写入命令(SET, HSET, LPUSH 等) -- `@admin`:管理命令(CONFIG, SHUTDOWN 等) -- `@dangerous`:危险命令(FLUSHALL, KEYS 等) -- `@all`:所有命令 - -**结论:** -- ✅ Redis 启用了 ACL 模式 -- 👤 **用户名是:`default`** -- 🔑 密码已配置(存储为 SHA256 哈希) -- 🔓 权限:完全访问(所有命令、所有 key、所有 channel) - ---- - -### 步骤 4:验证用户名配置 - -**目的:** 确认 `default` 用户可以正常工作 - -**方法 1:使用密码(不指定用户名)** -```bash -kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ - redis-cli -a PING -``` - -**预期输出:** -``` -Warning: Using a password with '-a' option on the command line interface may not be safe. -PONG -``` - -✅ **说明:只提供密码可以正常连接(默认使用 default 用户)** - ---- - -**方法 2:显式指定用户名** -```bash -kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ - redis-cli --user default --pass PING -``` - -**预期输出:** -``` -PONG -``` - -✅ **说明:显式指定 default 用户也可以正常连接** - ---- - -**方法 3:测试错误的用户名** -```bash -kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ - redis-cli --user wronguser --pass PING -``` - -**预期输出:** -``` -(error) WRONGPASS invalid username-password pair or user is disabled. -``` - -❌ **说明:不存在的用户名会认证失败** - ---- - -### 步骤 5:查看完整的 ACL 信息 - -**目的:** 了解 `default` 用户的详细权限配置 - -**命令:** -```bash -kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ - redis-cli -a ACL GETUSER default -``` - -**输出:** -``` - 1) "flags" - 2) 1) "on" - 2) "allkeys" - 3) "allchannels" - 4) "allcommands" - 3) "passwords" - 4) 1) "e3e7d4b9413497efc274c747b2ee88023e00e6416080db92c7bdd49a73f32d3d" - 5) "commands" - 6) "+@all" - 7) "keys" - 8) 1) "*" - 9) "channels" -10) 1) "*" -11) "selectors" -12) (empty array) -``` - -**详细分析:** - -| 字段 | 值 | 含义 | -|-----|---|------| -| `flags` | `on, allkeys, allchannels, allcommands` | 用户已启用,可访问所有资源 | -| `passwords` | `e3e7d4b...` (SHA256) | 密码哈希 | -| `commands` | `+@all` | 允许所有命令组 | -| `keys` | `*` | 可访问所有 key | -| `channels` | `*` | 可访问所有 Pub/Sub channel | - -**权限总结:** -- ✅ 用户状态:启用(on) -- ✅ 命令权限:所有命令(+@all) -- ✅ Key 权限:所有 key(~*) -- ✅ Channel 权限:所有 channel(&*) -- 🔒 密码:已设置且加密存储 - ---- - -## 🎯 用户名发现结果 - -### 最终结论 - -#### ✅ 当前 Redis 配置 -```yaml -认证模式: ACL (Redis 6.0+) -用户名: default -密码: (存储在 Secret user-redis 中) -权限: 完全访问(超级用户) -状态: 已启用 -``` - -#### 📝 连接参数汇总 - -| 参数 | 值 | 备注 | -|-----|---|------| -| **用户名** | `default` | 可省略,客户端默认使用 | -| **密码** | `kubectl get secret user-redis -n juwan -o jsonpath='{.data.password}' \| base64 -d` | 必需 | -| **Host (集群内)** | `user-redis-master.juwan.svc.cluster.local` | 写操作 | -| **Host (集群内)** | `user-redis-replica.juwan.svc.cluster.local` | 读操作 | -| **Host (Sentinel)** | `user-redis-sentinel-sentinel.juwan.svc.cluster.local` | 推荐 | -| **Port** | `6379` | Redis 数据端口 | -| **Sentinel Port** | `26379` | Sentinel 端口 | - ---- - -### 为什么大多数情况不需要指定用户名? - -#### Redis 客户端的默认行为 - -1. **向后兼容** - ``` - Redis 6.0+ 保持了与旧版本的兼容性 - 当客户端只提供密码时,自动使用 'default' 用户 - ``` - -2. **客户端实现** - ```go - // go-redis 内部逻辑(伪代码) - if password != "" && username == "" { - username = "default" // 自动补全 - } - ``` - -3. **AUTH 命令演进** - ```bash - # Redis 5.x 及之前 - AUTH password # 只有密码 - - # Redis 6.0+(向后兼容) - AUTH password # 等价于 AUTH default password - AUTH username password # 新格式 - ``` - -#### 何时需要显式指定用户名? - -| 场景 | 是否需要 | 原因 | -|-----|---------|------| -| 使用 `default` 用户 | ❌ 不需要 | 客户端自动使用 | -| 创建了自定义用户 | ✅ 需要 | 必须明确指定 | -| 多应用共享 Redis | ✅ 推荐 | 权限隔离 | -| 审计需求 | ✅ 推荐 | 区分访问来源 | -| 管理工具连接 | ⚠️ 可选 | 有些工具要求填写 | - ---- - -## 💻 各语言连接配置 - -### Go (go-redis) - -#### 方式 1:只用密码(推荐) -```go -import "github.com/redis/go-redis/v9" - -// 不指定 Username,自动使用 default -rdb := redis.NewClient(&redis.Options{ - Addr: "user-redis-master.juwan.svc.cluster.local:6379", - Password: os.Getenv("REDIS_PASSWORD"), // 只需密码 - DB: 0, -}) -``` - -#### 方式 2:显式指定用户名 -```go -// 显式指定 Username(效果相同) -rdb := redis.NewClient(&redis.Options{ - Addr: "user-redis-master.juwan.svc.cluster.local:6379", - Username: "default", // 明确指定 - Password: os.Getenv("REDIS_PASSWORD"), - DB: 0, -}) -``` - -#### Sentinel 模式(生产推荐) -```go -// Sentinel 模式也支持用户名 -rdb := redis.NewFailoverClient(&redis.FailoverOptions{ - MasterName: "mymaster", - SentinelAddrs: []string{ - "user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379", - }, - // Username 可省略(默认 default) - Password: os.Getenv("REDIS_PASSWORD"), - DB: 0, -}) -``` - ---- - -### Go-Zero 框架 - -#### 配置文件 -```yaml -# app/users/rpc/etc/pb.yaml -Redis: - Type: sentinel - MasterName: mymaster - SentinelAddrs: - - user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379 - Pass: ${REDIS_PASSWORD} # 只需密码,不需要用户名 -``` - -#### 如果需要自定义用户 -```yaml -Redis: - Type: sentinel - MasterName: mymaster - SentinelAddrs: - - user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379 - Username: default # 可选:显式指定 - Pass: ${REDIS_PASSWORD} -``` - ---- - -### Python (redis-py) - -#### 只用密码 -```python -import redis - -# 不指定 username -r = redis.Redis( - host='user-redis-master.juwan.svc.cluster.local', - port=6379, - password=os.getenv('REDIS_PASSWORD'), # 只需密码 - db=0 -) -``` - -#### 显式指定用户名(redis-py 4.3+) -```python -# 显式指定 username -r = redis.Redis( - host='user-redis-master.juwan.svc.cluster.local', - port=6379, - username='default', # 明确指定 - password=os.getenv('REDIS_PASSWORD'), - db=0 -) -``` - -#### Sentinel 模式 -```python -from redis.sentinel import Sentinel - -sentinel = Sentinel([ - ('user-redis-sentinel-sentinel.juwan.svc.cluster.local', 26379) -]) - -# 获取主节点(不指定 username) -master = sentinel.master_for( - 'mymaster', - password=os.getenv('REDIS_PASSWORD') -) - -# 或显式指定 -master = sentinel.master_for( - 'mymaster', - username='default', - password=os.getenv('REDIS_PASSWORD') -) -``` - ---- - -### Java (Spring Data Redis) - -#### application.yml(只用密码) -```yaml -spring: - redis: - password: ${REDIS_PASSWORD} # 只需密码 - sentinel: - master: mymaster - nodes: - - user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379 -``` - -#### 显式指定用户名(Spring Boot 2.6+) -```yaml -spring: - redis: - username: default # 可选 - password: ${REDIS_PASSWORD} - sentinel: - master: mymaster - nodes: - - user-redis-sentinel-sentinel.juwan.svc.cluster.local:26379 -``` - -#### Java 代码(Jedis) -```java -// 只用密码 -JedisPoolConfig poolConfig = new JedisPoolConfig(); -JedisPool pool = new JedisPool( - poolConfig, - "user-redis-master.juwan.svc.cluster.local", - 6379, - 2000, - "password" // 只需密码 -); - -// 显式指定用户名(Jedis 4.0+) -JedisPool pool = new JedisPool( - poolConfig, - "user-redis-master.juwan.svc.cluster.local", - 6379, - 2000, - "default", // 用户名 - "password" // 密码 -); -``` - ---- - -### Node.js (ioredis) - -#### 只用密码 -```javascript -const Redis = require('ioredis'); - -// 不指定 username -const redis = new Redis({ - host: 'user-redis-master.juwan.svc.cluster.local', - port: 6379, - password: process.env.REDIS_PASSWORD, // 只需密码 -}); -``` - -#### 显式指定用户名(ioredis 4.0+) -```javascript -// 显式指定 username -const redis = new Redis({ - host: 'user-redis-master.juwan.svc.cluster.local', - port: 6379, - username: 'default', // 明确指定 - password: process.env.REDIS_PASSWORD, -}); -``` - -#### Sentinel 模式 -```javascript -const redis = new Redis({ - sentinels: [ - { - host: 'user-redis-sentinel-sentinel.juwan.svc.cluster.local', - port: 26379 - } - ], - name: 'mymaster', - password: process.env.REDIS_PASSWORD, // username 可省略 -}); -``` - ---- - -### redis-cli 命令行 - -#### 只用密码 -```bash -# 方式 1:命令行参数 -redis-cli -h user-redis-master.juwan.svc.cluster.local -p 6379 -a - -# 方式 2:交互式登录 -redis-cli -h user-redis-master.juwan.svc.cluster.local -p 6379 -> AUTH -OK -``` - -#### 显式指定用户名 -```bash -# 方式 1:命令行参数 -redis-cli -h host -p 6379 --user default --pass - -# 方式 2:交互式登录 -redis-cli -h host -p 6379 -> AUTH default -OK -``` - ---- - -## 🛠️ 管理工具配置 - -### RedisInsight - -**配置示例:** -``` -Name: juwan-user-redis -Host: localhost (使用 kubectl port-forward) -Port: 6379 -Username: default ← 填这个(或留空) -Password: <从 Secret 获取> -``` - -**说明:** -- ✅ Username 可以填 `default` -- ✅ Username 也可以留空(某些版本支持) -- 🔧 建议先 port-forward:`kubectl port-forward -n juwan svc/user-redis-master 6379:6379` - ---- - -### Redis Commander - -**Docker 运行:** -```bash -docker run -d \ - -e REDIS_HOSTS=juwan:user-redis-master.juwan.svc.cluster.local:6379:0:password \ - -p 8081:8081 \ - rediscommander/redis-commander -``` - -**URL 格式:** -``` -# 不带用户名 -redis://:@host:6379 - -# 带用户名 -redis://default:@host:6379 -``` - ---- - -### Another Redis Desktop Manager - -**连接配置:** -```json -{ - "name": "juwan-redis", - "host": "localhost", - "port": 6379, - "auth": "password", // 只需密码 - "username": "" // 留空(或填 "default") -} -``` - ---- - -## ❓ 常见问题解答 - -### Q1: 为什么有些地方说 Redis 不需要用户名? - -**A:** 这取决于 Redis 版本: - -``` -Redis 5.x 及之前: -└─ ❌ 不支持用户名,只有全局密码 - └─ 配置:requirepass password - -Redis 6.0+: -└─ ✅ 支持 ACL 多用户 - └─ 但保持向后兼容 - └─ 如果只提供密码,自动使用 'default' 用户 -``` - -### Q2: 我的 Redis 是哪个版本? - -**查看方法:** -```bash -kubectl exec -it user-redis-0 -n juwan -c user-redis -- redis-cli -v -# 或 -kubectl exec -it user-redis-0 -n juwan -c user-redis -- redis-server --version -``` - -**你的环境:** -``` -Redis 7.0.12 (quay.io/opstree/redis:v7.0.12) -✅ 支持 ACL -✅ 用户名: default -``` - -### Q3: 如何创建自定义用户? - -**场景:** 为不同应用创建独立的用户账号 - -#### 创建只读用户 -```bash -kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ - redis-cli -a ACL SETUSER readonly on \ - '>readonly_password' \ - ~* \ - +@read -@write -@dangerous -``` - -**验证:** -```bash -# 使用新用户登录 -kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ - redis-cli --user readonly --pass readonly_password GET somekey - -# 尝试写操作(应该失败) -kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ - redis-cli --user readonly --pass readonly_password SET somekey value -# (error) NOPERM User has no permissions to run the 'set' command -``` - -#### 创建应用专用用户 -```bash -# 只能访问 app:* 开头的 key -kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ - redis-cli -a ACL SETUSER app_user on \ - '>app_password' \ - ~app:* \ - +@all -``` - -#### 保存 ACL 配置 -```bash -# 持久化到配置文件 -kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ - redis-cli -a ACL SAVE -``` - -⚠️ **注意:** 在 Kubernetes 环境中,Pod 重启可能丢失 ACL 配置。建议: -1. 使用 ConfigMap 存储 ACL 配置 -2. 在 StatefulSet 启动脚本中加载配置 -3. 或使用 Redis Operator 的 ACL 管理功能 - -### Q4: 如何查看所有用户? - -```bash -kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ - redis-cli -a ACL LIST -``` - -### Q5: 如何重置用户密码? - -#### 重置 default 用户密码 -```bash -# 1. 生成新密码 -NEW_PASSWORD=$(openssl rand -base64 32) - -# 2. 在 Redis 中更新 -kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ - redis-cli -a ACL SETUSER default ">$NEW_PASSWORD" - -# 3. 更新 Kubernetes Secret -kubectl create secret generic user-redis \ - --from-literal=password=$NEW_PASSWORD \ - --dry-run=client -o yaml | kubectl apply -f - - -# 4. 重启应用 Pods(使新密码生效) -kubectl rollout restart deployment/user-rpc -n juwan -``` - -### Q6: 客户端报错 "WRONGPASS invalid username-password pair" - -**可能原因:** - -1. **密码错误** - ```bash - # 验证密码 - kubectl get secret user-redis -n juwan -o jsonpath='{.data.password}' | base64 -d - ``` - -2. **用户名错误** - ```bash - # 检查用户是否存在 - kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ - redis-cli -a ACL LIST - ``` - -3. **用户被禁用** - ```bash - # 启用用户 - kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ - redis-cli -a ACL SETUSER default on - ``` - -4. **网络连接到了错误的 Redis 实例** - ```bash - # 确认连接的主机 - kubectl get svc -n juwan | grep redis - ``` - -### Q7: 在 Kubernetes 中如何安全地传递密码? - -**推荐方案:环境变量 + Secret** - -```yaml -# Deployment -apiVersion: apps/v1 -kind: Deployment -metadata: - name: user-rpc -spec: - template: - spec: - containers: - - name: user-rpc - env: - - name: REDIS_PASSWORD - valueFrom: - secretKeyRef: - name: user-redis - key: password -``` - -**应用代码:** -```go -// 从环境变量读取 -password := os.Getenv("REDIS_PASSWORD") - -rdb := redis.NewClient(&redis.Options{ - Addr: "user-redis-master.juwan.svc.cluster.local:6379", - Password: password, -}) -``` - -**❌ 不推荐:** -```yaml -# 不要在配置文件中硬编码密码 -Redis: - Password: "hardcoded_password" # ❌ 不安全 -``` - ---- - -## 🔒 安全建议 - -### 1. 密码强度 - -**检查当前密码强度:** -```bash -PASSWORD=$(kubectl get secret user-redis -n juwan -o jsonpath='{.data.password}' | base64 -d) -echo "密码长度: ${#PASSWORD}" -``` - -**推荐:** -- ✅ 至少 32 字符 -- ✅ 包含大小写字母、数字、特殊字符 -- ✅ 使用密码生成器:`openssl rand -base64 32` - -### 2. 权限最小化 - -**不要所有应用都用 default 超级用户!** - -```bash -# 为不同应用创建独立用户 -# 用户 A:只能读写自己的 key -kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ - redis-cli -a ACL SETUSER app_a on \ - '>password_a' ~app_a:* +@all - -# 用户 B:只读 -kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ - redis-cli -a ACL SETUSER app_b on \ - '>password_b' ~* +@read -``` - -### 3. 禁止危险命令 - -**即使是 default 用户,也应该限制危险命令:** - -```bash -# 禁止 FLUSHALL, FLUSHDB, KEYS 等命令 -kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ - redis-cli -a ACL SETUSER default -flushall -flushdb -keys - -# 查看限制后的权限 -kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ - redis-cli -a ACL GETUSER default -``` - -### 4. 审计日志 - -**启用 ACL 日志记录:** -```bash -# 查看最近的 ACL 事件 -kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ - redis-cli -a ACL LOG 10 - -# 输出示例: -# 1) reason: auth -# username: default -# timestamp: 1234567890 -``` - -### 5. 定期轮换密码 - -**建议:每 90 天轮换一次** - -```bash -#!/bin/bash -# rotate-redis-password.sh - -# 1. 生成新密码 -NEW_PWD=$(openssl rand -base64 32) - -# 2. 更新 Redis -kubectl exec -it user-redis-0 -n juwan -c user-redis -- \ - redis-cli -a "$OLD_PWD" ACL SETUSER default ">$NEW_PWD" - -# 3. 更新 Secret -kubectl create secret generic user-redis \ - --from-literal=password="$NEW_PWD" \ - -n juwan --dry-run=client -o yaml | kubectl apply -f - - -# 4. 滚动重启应用 -kubectl rollout restart deployment/user-rpc -n juwan - -echo "密码已轮换,新密码: $NEW_PWD" -``` - -### 6. 网络隔离 - -**NetworkPolicy 限制访问:** -```yaml -apiVersion: networking.k8s.io/v1 -kind: NetworkPolicy -metadata: - name: redis-access-policy - namespace: juwan -spec: - podSelector: - matchLabels: - app: user-redis - policyTypes: - - Ingress - ingress: - # 只允许 user-rpc 访问 - - from: - - podSelector: - matchLabels: - app: user-rpc - ports: - - protocol: TCP - port: 6379 -``` - ---- - -## 📝 总结 - -### 核心发现 - -| 项目 | 值 | -|-----|---| -| 认证模式 | ✅ ACL (Redis 6.0+) | -| 用户名 | `default` | -| 密码位置 | Secret `user-redis` in namespace `juwan` | -| 权限级别 | 超级用户(+@all ~* &*) | -| 是否必须指定用户名 | ❌ 不必须(客户端默认使用 default) | - -### 最佳实践 - -1. **开发/测试环境** - ```yaml - # 简单配置即可 - Password: - # Username 省略 - ``` - -2. **生产环境(推荐)** - ```yaml - # 为每个应用创建独立用户 - Username: app_user - Password: - # 限制权限和 key 范围 - ``` - -3. **连接方式** - ``` - ⭐⭐⭐ Sentinel 模式(推荐) - - 自动故障转移 - - 高可用 - - 只需密码(username 可省略) - ``` - ---- - -**文档版本:** 1.0 -**创建日期:** 2026年2月22日 -**维护者:** DevOps Team -**下次审查:** 2026年3月22日 From 60a353060954aa45c1e3d7699fbdc7578dff3ac7 Mon Sep 17 00:00:00 2001 From: zetaloop Date: Sun, 3 May 2026 18:59:44 +0800 Subject: [PATCH 2/6] =?UTF-8?q?docs:=20=E5=BD=92=E6=A1=A3=E5=9F=BA?= =?UTF-8?q?=E4=BA=8E=E6=97=A7=E7=89=88=20k8s=20=E7=9A=84=20jwt=20=E5=8A=A0?= =?UTF-8?q?=E5=AF=86=E9=83=A8=E7=BD=B2=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/{ => _archive}/secrets/DEPLOYMENT.md | 0 docs/{ => _archive}/secrets/ENCRYPTION.md | 0 docs/{ => _archive}/secrets/FLOWCHART.md | 0 docs/{ => _archive}/secrets/INDEX.md | 0 docs/{ => _archive}/secrets/QUICK_REFERENCE.md | 0 docs/{ => _archive}/secrets/README.md | 0 docs/{ => _archive}/secrets/SUMMARY.md | 0 docs/{ => _archive}/secrets/VERIFICATION.md | 0 docs/{ => _archive}/secrets/jwt-secret.yaml | 0 9 files changed, 0 insertions(+), 0 deletions(-) rename docs/{ => _archive}/secrets/DEPLOYMENT.md (100%) rename docs/{ => _archive}/secrets/ENCRYPTION.md (100%) rename docs/{ => _archive}/secrets/FLOWCHART.md (100%) rename docs/{ => _archive}/secrets/INDEX.md (100%) rename docs/{ => _archive}/secrets/QUICK_REFERENCE.md (100%) rename docs/{ => _archive}/secrets/README.md (100%) rename docs/{ => _archive}/secrets/SUMMARY.md (100%) rename docs/{ => _archive}/secrets/VERIFICATION.md (100%) rename docs/{ => _archive}/secrets/jwt-secret.yaml (100%) diff --git a/docs/secrets/DEPLOYMENT.md b/docs/_archive/secrets/DEPLOYMENT.md similarity index 100% rename from docs/secrets/DEPLOYMENT.md rename to docs/_archive/secrets/DEPLOYMENT.md diff --git a/docs/secrets/ENCRYPTION.md b/docs/_archive/secrets/ENCRYPTION.md similarity index 100% rename from docs/secrets/ENCRYPTION.md rename to docs/_archive/secrets/ENCRYPTION.md diff --git a/docs/secrets/FLOWCHART.md b/docs/_archive/secrets/FLOWCHART.md similarity index 100% rename from docs/secrets/FLOWCHART.md rename to docs/_archive/secrets/FLOWCHART.md diff --git a/docs/secrets/INDEX.md b/docs/_archive/secrets/INDEX.md similarity index 100% rename from docs/secrets/INDEX.md rename to docs/_archive/secrets/INDEX.md diff --git a/docs/secrets/QUICK_REFERENCE.md b/docs/_archive/secrets/QUICK_REFERENCE.md similarity index 100% rename from docs/secrets/QUICK_REFERENCE.md rename to docs/_archive/secrets/QUICK_REFERENCE.md diff --git a/docs/secrets/README.md b/docs/_archive/secrets/README.md similarity index 100% rename from docs/secrets/README.md rename to docs/_archive/secrets/README.md diff --git a/docs/secrets/SUMMARY.md b/docs/_archive/secrets/SUMMARY.md similarity index 100% rename from docs/secrets/SUMMARY.md rename to docs/_archive/secrets/SUMMARY.md diff --git a/docs/secrets/VERIFICATION.md b/docs/_archive/secrets/VERIFICATION.md similarity index 100% rename from docs/secrets/VERIFICATION.md rename to docs/_archive/secrets/VERIFICATION.md diff --git a/docs/secrets/jwt-secret.yaml b/docs/_archive/secrets/jwt-secret.yaml similarity index 100% rename from docs/secrets/jwt-secret.yaml rename to docs/_archive/secrets/jwt-secret.yaml From 164f98cf33ecd2d4cf9b364c0ca3261380ea5d1c Mon Sep 17 00:00:00 2001 From: zetaloop Date: Sun, 3 May 2026 19:00:58 +0800 Subject: [PATCH 3/6] =?UTF-8?q?docs:=20=E9=87=8D=E5=86=99=20dev=20README?= =?UTF-8?q?=20=E5=AF=B9=E9=BD=90=20per-domain=20=E6=8B=93=E6=89=91?= =?UTF-8?q?=E4=B8=8E=E5=89=8D=E7=AB=AF=E9=9B=86=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/dev/README.md | 77 ++++++++++++++++++++------------------------ 1 file changed, 35 insertions(+), 42 deletions(-) diff --git a/deploy/dev/README.md b/deploy/dev/README.md index 8cccb5d..1c3aa98 100644 --- a/deploy/dev/README.md +++ b/deploy/dev/README.md @@ -4,63 +4,54 @@ - Docker(需要 buildx) - Python 3(构建脚本) +- Git(含 submodule:首次需 `git submodule update --init --recursive`) ## 使用 ```bash cd deploy/dev -# 1. 构建所有微服务镜像 -# 默认 8 并行,可通过环境变量 BAKE_BATCH_SIZE 调整 +# 1. 构建所有镜像(默认 8 并行,可通过 BAKE_BATCH_SIZE 调整) python3 build.py # 2. 启动 docker compose up -d -# 3. 查看状态 -docker compose ps - -# 4. 通过网关访问 -curl http://127.0.0.1:18080/healthz - -# 5. 停止 -docker compose down +# 3. 通过网关访问 +open http://127.0.0.1:18080 ``` -构建脚本会扫描 `app/` 下所有 `api`、`rpc`、`mq`、`adapter` 入口,通过 `docker buildx bake` 并行构建所有服务镜像,生成 `juwan/-:dev`。 +构建脚本扫描 `app/` 下所有 `api`、`rpc`、`mq`、`adapter` 入口和 `frontend/`,通过 `docker buildx bake` 并行构建所有镜像,生成 `juwan/-:dev` 与 `juwan/frontend:dev`。 -端到端接口测试走网关 `http://127.0.0.1:18080`,`18801-18814` 是各服务的直连端口,不经过认证链路。 +前端作为 submodule 接入 compose,通过 envoy 的同源 fallback 路由向浏览器提供,无需独立端口。 -Chat WebSocket 通过网关 `ws://127.0.0.1:18080/ws/chat` 访问。WebTransport 使用 `18443/udp` 的 `/wt/chat` 入口。 - -如需只启动部分服务: - -```bash -docker compose up -d postgres redis snowflake player-rpc player-api -``` +Chat WebSocket 通过 `ws://127.0.0.1:18080/ws/chat` 访问。WebTransport 使用 `18443/udp` 的 `/wt/chat`。 ## 端口映射 -| 服务 | 宿主机端口 | -| ---------------- | ---------------- | -| PostgreSQL | 15432 | -| Redis | 16379 | -| Kafka | 19092 | -| Envoy Gateway | 18080 | -| users-api | 18801 | -| player-api | 18802 | -| game-api | 18803 | -| shop-api | 18804 | -| order-api | 18805 | -| wallet-api | 18806 | -| community-api | 18807 | -| objectstory-api | 18808 | -| email-api | 18809 | -| chat-api | 18810, 18443/udp | -| review-api | 18811 | -| dispute-api | 18812 | -| notification-api | 18813 | -| search-api | 18814 | +| 服务 | 宿主机端口 | 说明 | +| ---------------- | ---------------- | ---------------------------------- | +| Envoy Gateway | 18080 | 浏览器入口,`/api/*`、`/ws/*`、前端静态都从这里出 | +| Redis | 16379 | 共享会话与验证码 | +| MongoDB | 27017 | chat 消息持久化 | +| Kafka | 19092 | email-mq 任务队列 | +| ratelimit | 18081, 16070 | 限流服务 | +| users-api | 18801 | 直连调试入口,不经认证链路 | +| player-api | 18802 | | +| game-api | 18803 | | +| shop-api | 18804 | | +| order-api | 18805 | | +| wallet-api | 18806 | | +| community-api | 18807 | | +| objectstory-api | 18808 | | +| email-api | 18809 | | +| chat-api | 18810, 18889, 18443/udp | | +| review-api | 18811 | | +| dispute-api | 18812 | | +| notification-api | 18813 | | +| search-api | 18814 | | + +11 个 per-domain PostgreSQL(`users-db`、`player-db`、`game-db`、`shop-db`、`order-db`、`wallet-db`、`community-db`、`review-db`、`dispute-db`、`notification-db`、`search-db`)和 `frontend` 容器不暴露宿主端口,仅在 compose 内网通过 DNS 互访。 ## 环境变量 @@ -74,17 +65,19 @@ docker compose up -d postgres redis snowflake player-rpc player-api | ADMIN_PASSWORD | 管理员密码 | admin123 | | ADMIN_EMAIL | 管理员邮箱 | admin@juwan.dev | +默认 admin 固定 ID `100000`,拥有消费者、打手、店主三种身份全部开通,并预置了店铺、服务、钱包、帖子等演示数据,可直接用于完整链路联调。 + ## 认证 登录和注册通过 `users-api` 下发 `JToken` Cookie。`envoy-gateway` 负责 JWT 校验并注入认证头,`authz-adapter` 做会话态二次校验,后端服务只消费 `x-auth-user-id` 等头。 -写接口需要先 `GET /healthz` 领取 `XSRF-TOKEN` 和 `XSRF-GUARD`,再在请求头带上 `xsrf-token`。 +写接口需要先 `GET /healthz` 领取 `__Host-XSRF-TOKEN` 和 `__Host-XSRF-GUARD` cookie,再在请求头带上 `xsrf-token`。 注册和密码重置都需要先调验证码接口拿到 `requestId`,再把它放到 `X-Request-Id` 请求头里发后续请求。 -## 数据库初始化 +## 数据库与初始化 -首次启动时 PostgreSQL 会自动执行 `desc/sql/` 下的建表语句。如需重新初始化,删除 volume 后重启: +每个 per-domain PostgreSQL 首次启动时,通过挂载 `desc/sql//` 和 `deploy/dev/fixture/.sql` 自动完成建表与演示数据导入。如需完全重置: ```bash docker compose down -v From 44c73e787f1e7fb45497b53ecc81a280a26f604e Mon Sep 17 00:00:00 2001 From: zetaloop Date: Sun, 3 May 2026 19:03:09 +0800 Subject: [PATCH 4/6] =?UTF-8?q?docs:=20=E9=87=8D=E6=96=B0=E7=94=9F?= =?UTF-8?q?=E6=88=90=20swagger=20=E6=8E=A5=E5=8F=A3=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- desc/api/docs/community-api.json | 1658 ++++++++++------------- desc/api/docs/dispute-api.json | 485 +++++++ desc/api/docs/email-api.json | 185 +-- desc/api/docs/game-api.json | 414 +++--- desc/api/docs/notification-api.json | 305 +++++ desc/api/docs/objectstory-api.json | 171 ++- desc/api/docs/order-api.json | 1336 ++++++++----------- desc/api/docs/player-api.json | 1686 ++++++++++-------------- desc/api/docs/review-api.json | 401 ++++++ desc/api/docs/search-api.json | 549 ++++++++ desc/api/docs/shop-api.json | 1674 ++++++++++-------------- desc/api/docs/users-api.json | 1877 ++++++++++++--------------- desc/api/docs/wallet-api.json | 459 ++++--- 13 files changed, 5963 insertions(+), 5237 deletions(-) create mode 100644 desc/api/docs/dispute-api.json create mode 100644 desc/api/docs/notification-api.json create mode 100644 desc/api/docs/review-api.json create mode 100644 desc/api/docs/search-api.json diff --git a/desc/api/docs/community-api.json b/desc/api/docs/community-api.json index 1101cb1..8ace6fa 100644 --- a/desc/api/docs/community-api.json +++ b/desc/api/docs/community-api.json @@ -1,1067 +1,797 @@ { + "swagger": "2.0", + "info": { + "title": "", + "version": "" + }, + "schemes": [ + "http", + "https" + ], "consumes": [ "application/json" ], "produces": [ "application/json" ], - "schemes": [ - "https" - ], - "swagger": "2.0", - "info": { - "version": "1.0" - }, - "basePath": "/", "paths": { "/api/v1/comments/{id}/like": { - "post": { - "consumes": [ - "application/x-www-form-urlencoded" - ], - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], - "summary": "点赞评论", - "operationId": "communityLikeComment", - "parameters": [ - { - "type": "integer", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "", - "schema": { - "type": "object" - } - } - } - }, "delete": { - "consumes": [ - "application/x-www-form-urlencoded" - ], - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], "summary": "取消点赞评论", - "operationId": "communityUnlikeComment", - "parameters": [ - { - "type": "integer", - "name": "id", - "in": "path", - "required": true - } - ], + "operationId": "UnlikeComment", "responses": { "200": { - "description": "", + "description": "A successful response.", "schema": { - "type": "object" + "$ref": "#/definitions/EmptyResp" } } - } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/PathId" + } + } + ], + "tags": [ + "community" + ] + }, + "post": { + "summary": "点赞评论", + "operationId": "LikeComment", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/EmptyResp" + } + } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/PathId" + } + } + ], + "tags": [ + "community" + ] } }, "/api/v1/posts": { "get": { - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], "summary": "获取帖子列表", - "operationId": "communityListPosts", - "parameters": [ - { - "type": "integer", - "default": 0, - "example": 0, - "name": "offset", - "in": "query", - "required": true - }, - { - "type": "integer", - "default": 20, - "example": 20, - "name": "limit", - "in": "query", - "required": true - }, - { - "type": "string", - "name": "tags", - "in": "query", - "allowEmptyValue": true - }, - { - "type": "string", - "name": "sortBy", - "in": "query", - "allowEmptyValue": true - } - ], + "operationId": "ListPosts", "responses": { "200": { - "description": "", + "description": "A successful response.", "schema": { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "type": "object", - "required": [ - "id", - "title", - "content", - "images", - "tags", - "likeCount", - "commentCount", - "liked", - "author", - "createdAt" - ], - "properties": { - "author": { - "type": "object", - "required": [ - "id", - "username", - "nickname", - "avatar", - "role", - "verifiedRoles", - "verificationStatus", - "createdAt" - ], - "properties": { - "avatar": { - "type": "string" - }, - "bio": { - "type": "string" - }, - "createdAt": { - "type": "string" - }, - "id": { - "type": "string" - }, - "nickname": { - "type": "string" - }, - "phone": { - "type": "string" - }, - "role": { - "description": "consumer, player, owner, admin", - "type": "string" - }, - "username": { - "type": "string" - }, - "verificationStatus": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "verifiedRoles": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "commentCount": { - "type": "integer" - }, - "content": { - "type": "string" - }, - "createdAt": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "images": { - "type": "array", - "items": { - "type": "string" - } - }, - "likeCount": { - "type": "integer" - }, - "liked": { - "type": "boolean" - }, - "tags": { - "type": "array", - "items": { - "type": "string" - } - }, - "title": { - "type": "string" - } - } - } - }, - "meta": { - "type": "object", - "required": [ - "total", - "offset", - "limit" - ], - "properties": { - "limit": { - "type": "integer" - }, - "offset": { - "type": "integer" - }, - "total": { - "type": "integer" - } - } - } - } + "$ref": "#/definitions/PostListResp" } } - } + }, + "parameters": [ + { + "name": "offset", + "in": "query", + "required": true, + "type": "integer", + "format": "int64", + "default": "0" + }, + { + "name": "limit", + "in": "query", + "required": true, + "type": "integer", + "format": "int64", + "default": "20" + }, + { + "name": "tags", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "sortBy", + "in": "query", + "required": false, + "type": "string" + } + ], + "tags": [ + "community" + ], + "consumes": [ + "multipart/form-data" + ] }, "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], "summary": "发布帖子", - "operationId": "communityCreatePost", + "operationId": "CreatePost", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/Post" + } + } + }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { - "type": "object", - "required": [ - "title", - "content", - "images", - "tags" - ], - "properties": { - "content": { - "type": "string" - }, - "images": { - "type": "array", - "items": { - "type": "string" - } - }, - "linkedOrderId": { - "type": "string" - }, - "tags": { - "type": "array", - "items": { - "type": "string" - } - }, - "title": { - "type": "string" - } - } + "$ref": "#/definitions/CreatePostReq" } } ], - "responses": { - "200": { - "description": "", - "schema": { - "type": "object", - "properties": { - "author": { - "type": "object", - "required": [ - "id", - "username", - "nickname", - "avatar", - "role", - "verifiedRoles", - "verificationStatus", - "createdAt" - ], - "properties": { - "avatar": { - "type": "string" - }, - "bio": { - "type": "string" - }, - "createdAt": { - "type": "string" - }, - "id": { - "type": "string" - }, - "nickname": { - "type": "string" - }, - "phone": { - "type": "string" - }, - "role": { - "description": "consumer, player, owner, admin", - "type": "string" - }, - "username": { - "type": "string" - }, - "verificationStatus": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "verifiedRoles": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "commentCount": { - "type": "integer" - }, - "content": { - "type": "string" - }, - "createdAt": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "images": { - "type": "array", - "items": { - "type": "string" - } - }, - "likeCount": { - "type": "integer" - }, - "liked": { - "type": "boolean" - }, - "tags": { - "type": "array", - "items": { - "type": "string" - } - }, - "title": { - "type": "string" - } - } - } - } - } + "tags": [ + "community" + ] } }, "/api/v1/posts/{id}": { "get": { - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], "summary": "获取帖子详情", - "operationId": "communityGetPost", - "parameters": [ - { - "type": "integer", - "name": "id", - "in": "path", - "required": true - } - ], + "operationId": "GetPost", "responses": { "200": { - "description": "", + "description": "A successful response.", "schema": { - "type": "object", - "properties": { - "author": { - "type": "object", - "required": [ - "id", - "username", - "nickname", - "avatar", - "role", - "verifiedRoles", - "verificationStatus", - "createdAt" - ], - "properties": { - "avatar": { - "type": "string" - }, - "bio": { - "type": "string" - }, - "createdAt": { - "type": "string" - }, - "id": { - "type": "string" - }, - "nickname": { - "type": "string" - }, - "phone": { - "type": "string" - }, - "role": { - "description": "consumer, player, owner, admin", - "type": "string" - }, - "username": { - "type": "string" - }, - "verificationStatus": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "verifiedRoles": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "commentCount": { - "type": "integer" - }, - "content": { - "type": "string" - }, - "createdAt": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "images": { - "type": "array", - "items": { - "type": "string" - } - }, - "likeCount": { - "type": "integer" - }, - "liked": { - "type": "boolean" - }, - "tags": { - "type": "array", - "items": { - "type": "string" - } - }, - "title": { - "type": "string" - } - } + "$ref": "#/definitions/Post" } } - } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string" + } + ], + "tags": [ + "community" + ] } }, "/api/v1/posts/{id}/comments": { "get": { - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], "summary": "获取帖子评论", - "operationId": "communityListComments", - "parameters": [ - { - "type": "integer", - "name": "id", - "in": "path", - "required": true - }, - { - "type": "integer", - "default": 0, - "example": 0, - "name": "offset", - "in": "query", - "required": true - }, - { - "type": "integer", - "default": 20, - "example": 20, - "name": "limit", - "in": "query", - "required": true - } - ], + "operationId": "ListComments", "responses": { "200": { - "description": "", + "description": "A successful response.", "schema": { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "type": "object", - "required": [ - "id", - "content", - "author", - "likeCount", - "liked", - "createdAt" - ], - "properties": { - "author": { - "type": "object", - "required": [ - "id", - "username", - "nickname", - "avatar", - "role", - "verifiedRoles", - "verificationStatus", - "createdAt" - ], - "properties": { - "avatar": { - "type": "string" - }, - "bio": { - "type": "string" - }, - "createdAt": { - "type": "string" - }, - "id": { - "type": "string" - }, - "nickname": { - "type": "string" - }, - "phone": { - "type": "string" - }, - "role": { - "description": "consumer, player, owner, admin", - "type": "string" - }, - "username": { - "type": "string" - }, - "verificationStatus": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "verifiedRoles": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "content": { - "type": "string" - }, - "createdAt": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "likeCount": { - "type": "integer" - }, - "liked": { - "type": "boolean" - } - } - } - }, - "meta": { - "type": "object", - "required": [ - "total", - "offset", - "limit" - ], - "properties": { - "limit": { - "type": "integer" - }, - "offset": { - "type": "integer" - }, - "total": { - "type": "integer" - } - } - } - } + "$ref": "#/definitions/CommentListResp" } } - } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "id", + "in": "query", + "required": true, + "type": "integer", + "format": "int64" + }, + { + "name": "offset", + "in": "query", + "required": true, + "type": "integer", + "format": "int64", + "default": "0" + }, + { + "name": "limit", + "in": "query", + "required": true, + "type": "integer", + "format": "int64", + "default": "20" + } + ], + "tags": [ + "community" + ] }, "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], "summary": "发表评论", - "operationId": "communityCreateComment", + "operationId": "CreateComment", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/Comment" + } + } + }, "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string" + }, { "name": "body", "in": "body", "required": true, "schema": { - "type": "object", - "required": [ - "content" - ], - "properties": { - "content": { - "type": "string" - } - } + "$ref": "#/definitions/CreateCommentReq" } } ], - "responses": { - "200": { - "description": "", - "schema": { - "type": "object", - "properties": { - "author": { - "type": "object", - "required": [ - "id", - "username", - "nickname", - "avatar", - "role", - "verifiedRoles", - "verificationStatus", - "createdAt" - ], - "properties": { - "avatar": { - "type": "string" - }, - "bio": { - "type": "string" - }, - "createdAt": { - "type": "string" - }, - "id": { - "type": "string" - }, - "nickname": { - "type": "string" - }, - "phone": { - "type": "string" - }, - "role": { - "description": "consumer, player, owner, admin", - "type": "string" - }, - "username": { - "type": "string" - }, - "verificationStatus": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "verifiedRoles": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "content": { - "type": "string" - }, - "createdAt": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "likeCount": { - "type": "integer" - }, - "liked": { - "type": "boolean" - } - } - } - } - } + "tags": [ + "community" + ] } }, "/api/v1/posts/{id}/like": { - "post": { - "consumes": [ - "application/x-www-form-urlencoded" - ], - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], - "summary": "点赞帖子", - "operationId": "communityLikePost", - "parameters": [ - { - "type": "integer", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "", - "schema": { - "type": "object" - } - } - } - }, "delete": { - "consumes": [ - "application/x-www-form-urlencoded" - ], - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], "summary": "取消点赞帖子", - "operationId": "communityUnlikePost", - "parameters": [ - { - "type": "integer", - "name": "id", - "in": "path", - "required": true - } - ], + "operationId": "UnlikePost", "responses": { "200": { - "description": "", + "description": "A successful response.", "schema": { - "type": "object" + "$ref": "#/definitions/EmptyResp" } } - } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/PathId" + } + } + ], + "tags": [ + "community" + ] + }, + "post": { + "summary": "点赞帖子", + "operationId": "LikePost", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/EmptyResp" + } + } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/PathId" + } + } + ], + "tags": [ + "community" + ] } }, "/api/v1/posts/{id}/pin": { - "post": { - "consumes": [ - "application/x-www-form-urlencoded" - ], - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], - "summary": "置顶帖子", - "operationId": "communityPinPost", - "parameters": [ - { - "type": "integer", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "", - "schema": { - "type": "object" - } - } - } - }, "delete": { - "consumes": [ - "application/x-www-form-urlencoded" - ], - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], "summary": "取消置顶", - "operationId": "communityUnpinPost", - "parameters": [ - { - "type": "integer", - "name": "id", - "in": "path", - "required": true - } - ], + "operationId": "UnpinPost", "responses": { "200": { - "description": "", + "description": "A successful response.", "schema": { - "type": "object" + "$ref": "#/definitions/EmptyResp" } } - } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/PathId" + } + } + ], + "tags": [ + "community" + ] + }, + "post": { + "summary": "置顶帖子", + "operationId": "PinPost", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/EmptyResp" + } + } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/PathId" + } + } + ], + "tags": [ + "community" + ] } }, "/api/v1/users/{id}/posts": { "get": { - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], "summary": "获取用户帖子", - "operationId": "communityListUserPosts", - "parameters": [ - { - "type": "integer", - "name": "id", - "in": "path", - "required": true - }, - { - "type": "integer", - "default": 0, - "example": 0, - "name": "offset", - "in": "query", - "required": true - }, - { - "type": "integer", - "default": 20, - "example": 20, - "name": "limit", - "in": "query", - "required": true - } - ], + "operationId": "ListUserPosts", "responses": { "200": { - "description": "", + "description": "A successful response.", "schema": { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "type": "object", - "required": [ - "id", - "title", - "content", - "images", - "tags", - "likeCount", - "commentCount", - "liked", - "author", - "createdAt" - ], - "properties": { - "author": { - "type": "object", - "required": [ - "id", - "username", - "nickname", - "avatar", - "role", - "verifiedRoles", - "verificationStatus", - "createdAt" - ], - "properties": { - "avatar": { - "type": "string" - }, - "bio": { - "type": "string" - }, - "createdAt": { - "type": "string" - }, - "id": { - "type": "string" - }, - "nickname": { - "type": "string" - }, - "phone": { - "type": "string" - }, - "role": { - "description": "consumer, player, owner, admin", - "type": "string" - }, - "username": { - "type": "string" - }, - "verificationStatus": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "verifiedRoles": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "commentCount": { - "type": "integer" - }, - "content": { - "type": "string" - }, - "createdAt": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "images": { - "type": "array", - "items": { - "type": "string" - } - }, - "likeCount": { - "type": "integer" - }, - "liked": { - "type": "boolean" - }, - "tags": { - "type": "array", - "items": { - "type": "string" - } - }, - "title": { - "type": "string" - } - } - } - }, - "meta": { - "type": "object", - "required": [ - "total", - "offset", - "limit" - ], - "properties": { - "limit": { - "type": "integer" - }, - "offset": { - "type": "integer" - }, - "total": { - "type": "integer" - } - } - } - } + "$ref": "#/definitions/PostListResp" } } - } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "id", + "in": "query", + "required": true, + "type": "integer", + "format": "int64" + }, + { + "name": "offset", + "in": "query", + "required": true, + "type": "integer", + "format": "int64", + "default": "0" + }, + { + "name": "limit", + "in": "query", + "required": true, + "type": "integer", + "format": "int64", + "default": "20" + } + ], + "tags": [ + "community" + ] } } }, - "x-date": "2026-04-22 22:30:25", - "x-description": "This is a goctl generated swagger file.", - "x-github": "https://github.com/zeromicro/go-zero", - "x-go-zero-doc": "https://go-zero.dev/", - "x-goctl-version": "1.10.1" -} \ No newline at end of file + "definitions": { + "Comment": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "content": { + "type": "string" + }, + "author": { + "$ref": "#/definitions/UserProfile" + }, + "likeCount": { + "type": "integer", + "format": "int64" + }, + "liked": { + "type": "boolean", + "format": "boolean" + }, + "createdAt": { + "type": "string" + } + }, + "title": "Comment", + "required": [ + "id", + "content", + "author", + "likeCount", + "liked", + "createdAt" + ] + }, + "CommentListResp": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/Comment" + } + }, + "meta": { + "$ref": "#/definitions/PageMeta" + } + }, + "title": "CommentListResp", + "required": [ + "items", + "meta" + ] + }, + "CreateCommentReq": { + "type": "object", + "properties": { + "postId": { + "type": "integer", + "format": "int64" + }, + "content": { + "type": "string" + } + }, + "title": "CreateCommentReq", + "required": [ + "-", + "content" + ] + }, + "CreatePostReq": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "content": { + "type": "string" + }, + "images": { + "type": "array", + "items": { + "type": "string" + } + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "linkedOrderId": { + "type": "string" + } + }, + "title": "CreatePostReq", + "required": [ + "title", + "content", + "images", + "tags" + ] + }, + "EmptyResp": { + "type": "object", + "title": "EmptyResp" + }, + "ListCommentsReq": { + "type": "object", + "properties": { + "offset": { + "type": "integer", + "format": "int64", + "default": "0" + }, + "limit": { + "type": "integer", + "format": "int64", + "default": "20" + } + }, + "title": "ListCommentsReq" + }, + "PageMeta": { + "type": "object", + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "offset": { + "type": "integer", + "format": "int64" + }, + "limit": { + "type": "integer", + "format": "int64" + } + }, + "title": "PageMeta", + "required": [ + "total", + "offset", + "limit" + ] + }, + "PageReq": { + "type": "object", + "properties": { + "offset": { + "type": "integer", + "format": "int64", + "default": "0" + }, + "limit": { + "type": "integer", + "format": "int64", + "default": "20" + } + }, + "title": "PageReq", + "required": [ + "offset", + "limit" + ] + }, + "PathId": { + "type": "object", + "title": "PathId" + }, + "Post": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "title": { + "type": "string" + }, + "content": { + "type": "string" + }, + "images": { + "type": "array", + "items": { + "type": "string" + } + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "linkedOrderId": { + "type": "integer", + "format": "int64" + }, + "pinned": { + "type": "boolean", + "format": "boolean" + }, + "likeCount": { + "type": "integer", + "format": "int64" + }, + "commentCount": { + "type": "integer", + "format": "int64" + }, + "liked": { + "type": "boolean", + "format": "boolean" + }, + "author": { + "$ref": "#/definitions/UserProfile" + }, + "createdAt": { + "type": "string" + } + }, + "title": "Post", + "required": [ + "id", + "title", + "content", + "images", + "tags", + "pinned", + "likeCount", + "commentCount", + "liked", + "author", + "createdAt" + ] + }, + "PostListReq": { + "type": "object", + "properties": { + "offset": { + "type": "integer", + "format": "int64", + "default": "0" + }, + "limit": { + "type": "integer", + "format": "int64", + "default": "20" + }, + "tags": { + "type": "string" + }, + "sortBy": { + "type": "string" + } + }, + "title": "PostListReq" + }, + "PostListResp": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/Post" + } + }, + "meta": { + "$ref": "#/definitions/PageMeta" + } + }, + "title": "PostListResp", + "required": [ + "items", + "meta" + ] + }, + "SimpleUser": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "nickname": { + "type": "string" + }, + "avatar": { + "type": "string" + } + }, + "title": "SimpleUser", + "required": [ + "id", + "nickname", + "avatar" + ] + }, + "UserProfile": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "username": { + "type": "string" + }, + "nickname": { + "type": "string" + }, + "avatar": { + "type": "string" + }, + "role": { + "type": "string", + "description": " consumer, player, owner, admin" + }, + "verifiedRoles": { + "type": "array", + "items": { + "type": "string" + } + }, + "verificationStatus": { + "type": "object" + }, + "phone": { + "type": "string" + }, + "bio": { + "type": "string" + }, + "createdAt": { + "type": "string" + } + }, + "title": "UserProfile", + "required": [ + "id", + "username", + "nickname", + "avatar", + "role", + "verifiedRoles", + "verificationStatus", + "createdAt" + ] + } + }, + "securityDefinitions": { + "apiKey": { + "type": "apiKey", + "description": "Enter JWT Bearer token **_only_**", + "name": "Authorization", + "in": "header" + } + } +} diff --git a/desc/api/docs/dispute-api.json b/desc/api/docs/dispute-api.json new file mode 100644 index 0000000..0f9caf6 --- /dev/null +++ b/desc/api/docs/dispute-api.json @@ -0,0 +1,485 @@ +{ + "swagger": "2.0", + "info": { + "title": "", + "version": "" + }, + "schemes": [ + "http", + "https" + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "paths": { + "/api/v1/disputes": { + "get": { + "summary": "获取争议列表", + "operationId": "ListDisputes", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/DisputeListResp" + } + } + }, + "parameters": [ + { + "name": "offset", + "in": "query", + "required": true, + "type": "integer", + "format": "int64", + "default": "0" + }, + { + "name": "limit", + "in": "query", + "required": true, + "type": "integer", + "format": "int64", + "default": "20" + }, + { + "name": "status", + "in": "query", + "required": false, + "type": "string" + } + ], + "tags": [ + "dispute" + ], + "consumes": [ + "multipart/form-data" + ] + } + }, + "/api/v1/disputes/{id}/appeal": { + "post": { + "summary": "申诉", + "operationId": "AppealDispute", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/EmptyResp" + } + } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/AppealReq" + } + } + ], + "tags": [ + "dispute" + ] + } + }, + "/api/v1/disputes/{id}/response": { + "post": { + "summary": "回应争议", + "operationId": "RespondDispute", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/EmptyResp" + } + } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/DisputeResponseReq" + } + } + ], + "tags": [ + "dispute" + ] + } + }, + "/api/v1/orders/{id}/dispute": { + "get": { + "summary": "获取订单争议", + "operationId": "GetOrderDispute", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/Dispute" + } + } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string" + } + ], + "tags": [ + "dispute" + ] + }, + "post": { + "summary": "发起争议", + "operationId": "CreateDispute", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/EmptyResp" + } + } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/CreateDisputeReq" + } + } + ], + "tags": [ + "dispute" + ] + } + } + }, + "definitions": { + "AppealReq": { + "type": "object", + "properties": { + "reason": { + "type": "string" + } + }, + "title": "AppealReq", + "required": [ + "reason" + ] + }, + "CreateDisputeReq": { + "type": "object", + "properties": { + "reason": { + "type": "string" + }, + "evidence": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "title": "CreateDisputeReq", + "required": [ + "reason", + "evidence" + ] + }, + "Dispute": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "orderId": { + "type": "integer", + "format": "int64" + }, + "initiatorId": { + "type": "integer", + "format": "int64" + }, + "initiatorName": { + "type": "string" + }, + "respondentId": { + "type": "integer", + "format": "int64" + }, + "reason": { + "type": "string" + }, + "evidence": { + "type": "array", + "items": { + "type": "string" + } + }, + "status": { + "type": "string" + }, + "result": { + "type": "string" + }, + "respondentReason": { + "type": "string" + }, + "respondentEvidence": { + "type": "array", + "items": { + "type": "string" + } + }, + "appealReason": { + "type": "string" + }, + "appealedAt": { + "type": "string" + }, + "resolvedBy": { + "type": "integer", + "format": "int64" + }, + "resolvedAt": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "updatedAt": { + "type": "string" + } + }, + "title": "Dispute", + "required": [ + "id", + "orderId", + "initiatorId", + "initiatorName", + "respondentId", + "reason", + "evidence", + "status", + "respondentEvidence", + "createdAt", + "updatedAt" + ] + }, + "DisputeListReq": { + "type": "object", + "properties": { + "offset": { + "type": "integer", + "format": "int64", + "default": "0" + }, + "limit": { + "type": "integer", + "format": "int64", + "default": "20" + }, + "status": { + "type": "string" + } + }, + "title": "DisputeListReq" + }, + "DisputeListResp": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/Dispute" + } + }, + "meta": { + "$ref": "#/definitions/PageMeta" + } + }, + "title": "DisputeListResp", + "required": [ + "items", + "meta" + ] + }, + "DisputePathId": { + "type": "object", + "title": "DisputePathId" + }, + "DisputeResponseReq": { + "type": "object", + "properties": { + "reason": { + "type": "string" + }, + "evidence": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "title": "DisputeResponseReq", + "required": [ + "reason", + "evidence" + ] + }, + "EmptyResp": { + "type": "object", + "title": "EmptyResp" + }, + "PageMeta": { + "type": "object", + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "offset": { + "type": "integer", + "format": "int64" + }, + "limit": { + "type": "integer", + "format": "int64" + } + }, + "title": "PageMeta", + "required": [ + "total", + "offset", + "limit" + ] + }, + "PageReq": { + "type": "object", + "properties": { + "offset": { + "type": "integer", + "format": "int64", + "default": "0" + }, + "limit": { + "type": "integer", + "format": "int64", + "default": "20" + } + }, + "title": "PageReq", + "required": [ + "offset", + "limit" + ] + }, + "SimpleUser": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "nickname": { + "type": "string" + }, + "avatar": { + "type": "string" + } + }, + "title": "SimpleUser", + "required": [ + "id", + "nickname", + "avatar" + ] + }, + "UserProfile": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "username": { + "type": "string" + }, + "nickname": { + "type": "string" + }, + "avatar": { + "type": "string" + }, + "role": { + "type": "string", + "description": " consumer, player, owner, admin" + }, + "verifiedRoles": { + "type": "array", + "items": { + "type": "string" + } + }, + "verificationStatus": { + "type": "object" + }, + "phone": { + "type": "string" + }, + "bio": { + "type": "string" + }, + "createdAt": { + "type": "string" + } + }, + "title": "UserProfile", + "required": [ + "id", + "username", + "nickname", + "avatar", + "role", + "verifiedRoles", + "verificationStatus", + "createdAt" + ] + } + }, + "securityDefinitions": { + "apiKey": { + "type": "apiKey", + "description": "Enter JWT Bearer token **_only_**", + "name": "Authorization", + "in": "header" + } + } +} diff --git a/desc/api/docs/email-api.json b/desc/api/docs/email-api.json index 5a66e5b..e6f8d05 100644 --- a/desc/api/docs/email-api.json +++ b/desc/api/docs/email-api.json @@ -1,121 +1,138 @@ { + "swagger": "2.0", + "info": { + "title": "", + "version": "1.0" + }, + "schemes": [ + "http", + "https" + ], "consumes": [ "application/json" ], "produces": [ "application/json" ], - "schemes": [ - "https" - ], - "swagger": "2.0", - "info": { - "version": "1.0" - }, - "basePath": "/", "paths": { "/api/v1/auth/forgot-password/send": { "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], "summary": "忘记密码-发送验证码", - "operationId": "emailForgotPassword", + "operationId": "ForgotPassword", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/EmptyResp" + } + } + }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { - "type": "object", - "required": [ - "email" - ], - "properties": { - "email": { - "type": "string" - } - } + "$ref": "#/definitions/ForgotPasswordReq" } } ], - "responses": { - "200": { - "description": "", - "schema": { - "type": "object" - } - } - } + "tags": [ + "email" + ] } }, "/api/v1/email/verification-code/send": { "post": { - "description": "向用户邮箱发送验证码,支持注册、登录、重置密码、绑定邮箱等场景", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], "summary": "发送邮箱验证码", - "operationId": "emailSendVerificationCode", + "description": "向用户邮箱发送验证码,支持注册、登录、重置密码、绑定邮箱等场景", + "operationId": "SendVerificationCode", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/SendVerificationCodeResp" + } + } + }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { - "type": "object", - "required": [ - "email", - "scene" - ], - "properties": { - "email": { - "type": "string" - }, - "scene": { - "type": "string" - } - } + "$ref": "#/definitions/SendVerificationCodeReq" } } ], - "responses": { - "200": { - "description": "", - "schema": { - "type": "object", - "properties": { - "expireInSec": { - "type": "integer" - }, - "message": { - "type": "string" - }, - "requestId": { - "type": "string" - } - } - } - } - } + "tags": [ + "email" + ] } } }, - "x-date": "2026-04-22 22:30:26", - "x-description": "This is a goctl generated swagger file.", - "x-github": "https://github.com/zeromicro/go-zero", - "x-go-zero-doc": "https://go-zero.dev/", - "x-goctl-version": "1.10.1" -} \ No newline at end of file + "definitions": { + "EmptyResp": { + "type": "object", + "title": "EmptyResp" + }, + "ForgotPasswordReq": { + "type": "object", + "properties": { + "email": { + "type": "string" + } + }, + "title": "ForgotPasswordReq", + "required": [ + "email" + ] + }, + "SendVerificationCodeReq": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "scene": { + "type": "string" + } + }, + "title": "SendVerificationCodeReq", + "required": [ + "email", + "required", + "scene" + ] + }, + "SendVerificationCodeResp": { + "type": "object", + "properties": { + "requestId": { + "type": "string" + }, + "expireInSec": { + "type": "integer", + "format": "int64" + }, + "message": { + "type": "string" + } + }, + "title": "SendVerificationCodeResp", + "required": [ + "requestId", + "expireInSec", + "message" + ] + } + }, + "securityDefinitions": { + "apiKey": { + "type": "apiKey", + "description": "Enter JWT Bearer token **_only_**", + "name": "Authorization", + "in": "header" + } + } +} diff --git a/desc/api/docs/game-api.json b/desc/api/docs/game-api.json index 6c1e01d..b1f7168 100644 --- a/desc/api/docs/game-api.json +++ b/desc/api/docs/game-api.json @@ -1,213 +1,281 @@ { + "swagger": "2.0", + "info": { + "title": "", + "version": "" + }, + "schemes": [ + "http", + "https" + ], "consumes": [ "application/json" ], "produces": [ "application/json" ], - "schemes": [ - "https" - ], - "swagger": "2.0", - "info": { - "version": "1.0" - }, - "basePath": "/", "paths": { - "/api/v1/games": { + "/api/v1/games/": { "get": { - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], "summary": "获取游戏列表", - "operationId": "gameListGames", - "parameters": [ - { - "type": "integer", - "default": 0, - "example": 0, - "name": "offset", - "in": "query", - "required": true - }, - { - "type": "integer", - "default": 20, - "example": 20, - "name": "limit", - "in": "query", - "required": true - } - ], + "operationId": "ListGames", "responses": { "200": { - "description": "", + "description": "A successful response.", "schema": { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "type": "object", - "required": [ - "name", - "icon", - "category" - ], - "properties": { - "category": { - "type": "string" - }, - "icon": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "name": { - "type": "string" - } - } - } - }, - "meta": { - "type": "object", - "required": [ - "total", - "offset", - "limit" - ], - "properties": { - "limit": { - "type": "integer" - }, - "offset": { - "type": "integer" - }, - "total": { - "type": "integer" - } - } - } - } + "$ref": "#/definitions/GameListResp" } } - } + }, + "parameters": [ + { + "name": "offset", + "in": "query", + "required": true, + "type": "integer", + "format": "int64", + "default": "0" + }, + { + "name": "limit", + "in": "query", + "required": true, + "type": "integer", + "format": "int64", + "default": "20" + } + ], + "tags": [ + "game" + ], + "consumes": [ + "multipart/form-data" + ] }, "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], "summary": "创建游戏", - "operationId": "gameCreateGame", + "operationId": "CreateGame", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/Game" + } + } + }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { - "type": "object", - "required": [ - "name", - "icon", - "category" - ], - "properties": { - "category": { - "type": "string" - }, - "icon": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "name": { - "type": "string" - } - } + "$ref": "#/definitions/Game" } } ], - "responses": { - "200": { - "description": "", - "schema": { - "type": "object", - "properties": { - "category": { - "type": "string" - }, - "icon": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "name": { - "type": "string" - } - } - } - } - } + "tags": [ + "game" + ] } }, "/api/v1/games/{id}": { "get": { - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], "summary": "获取游戏详情", - "operationId": "gameGetGame", - "parameters": [ - { - "type": "integer", - "name": "id", - "in": "path", - "required": true - } - ], + "operationId": "GetGame", "responses": { "200": { - "description": "", + "description": "A successful response.", "schema": { - "type": "object", - "properties": { - "category": { - "type": "string" - }, - "icon": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "name": { - "type": "string" - } - } + "$ref": "#/definitions/Game" } } - } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string" + } + ], + "tags": [ + "game" + ] } } }, - "x-date": "2026-04-22 22:30:23", - "x-description": "This is a goctl generated swagger file.", - "x-github": "https://github.com/zeromicro/go-zero", - "x-go-zero-doc": "https://go-zero.dev/", - "x-goctl-version": "1.10.1" -} \ No newline at end of file + "definitions": { + "EmptyResp": { + "type": "object", + "title": "EmptyResp" + }, + "Game": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "category": { + "type": "string" + } + }, + "title": "Game", + "required": [ + "name", + "icon", + "category" + ] + }, + "GameListResp": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/Game" + } + }, + "meta": { + "$ref": "#/definitions/PageMeta" + } + }, + "title": "GameListResp", + "required": [ + "items", + "meta" + ] + }, + "GetGameReq": { + "type": "object", + "title": "GetGameReq" + }, + "PageMeta": { + "type": "object", + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "offset": { + "type": "integer", + "format": "int64" + }, + "limit": { + "type": "integer", + "format": "int64" + } + }, + "title": "PageMeta", + "required": [ + "total", + "offset", + "limit" + ] + }, + "PageReq": { + "type": "object", + "properties": { + "offset": { + "type": "integer", + "format": "int64", + "default": "0" + }, + "limit": { + "type": "integer", + "format": "int64", + "default": "20" + } + }, + "title": "PageReq", + "required": [ + "offset", + "limit" + ] + }, + "SimpleUser": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "nickname": { + "type": "string" + }, + "avatar": { + "type": "string" + } + }, + "title": "SimpleUser", + "required": [ + "id", + "nickname", + "avatar" + ] + }, + "UserProfile": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "username": { + "type": "string" + }, + "nickname": { + "type": "string" + }, + "avatar": { + "type": "string" + }, + "role": { + "type": "string", + "description": " consumer, player, owner, admin" + }, + "verifiedRoles": { + "type": "array", + "items": { + "type": "string" + } + }, + "verificationStatus": { + "type": "object" + }, + "phone": { + "type": "string" + }, + "bio": { + "type": "string" + }, + "createdAt": { + "type": "string" + } + }, + "title": "UserProfile", + "required": [ + "id", + "username", + "nickname", + "avatar", + "role", + "verifiedRoles", + "verificationStatus", + "createdAt" + ] + } + }, + "securityDefinitions": { + "apiKey": { + "type": "apiKey", + "description": "Enter JWT Bearer token **_only_**", + "name": "Authorization", + "in": "header" + } + } +} diff --git a/desc/api/docs/notification-api.json b/desc/api/docs/notification-api.json new file mode 100644 index 0000000..80b3312 --- /dev/null +++ b/desc/api/docs/notification-api.json @@ -0,0 +1,305 @@ +{ + "swagger": "2.0", + "info": { + "title": "", + "version": "" + }, + "schemes": [ + "http", + "https" + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "paths": { + "/api/v1/notifications": { + "get": { + "summary": "获取通知列表", + "operationId": "ListNotifications", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/NotificationListResp" + } + } + }, + "parameters": [ + { + "name": "offset", + "in": "query", + "required": true, + "type": "integer", + "format": "int64", + "default": "0" + }, + { + "name": "limit", + "in": "query", + "required": true, + "type": "integer", + "format": "int64", + "default": "20" + } + ], + "tags": [ + "notification" + ], + "consumes": [ + "multipart/form-data" + ] + } + }, + "/api/v1/notifications/read-all": { + "put": { + "summary": "全部已读", + "operationId": "ReadAllNotifications", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/EmptyResp" + } + } + }, + "parameters": [ + { + "name": "body", + "description": " 空响应", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/EmptyResp" + } + } + ], + "tags": [ + "notification" + ] + } + }, + "/api/v1/notifications/{id}/read": { + "put": { + "summary": "标记已读", + "operationId": "ReadNotification", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/EmptyResp" + } + } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/PathId" + } + } + ], + "tags": [ + "notification" + ] + } + } + }, + "definitions": { + "EmptyResp": { + "type": "object", + "title": "EmptyResp" + }, + "Notification": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "type": { + "type": "string" + }, + "title": { + "type": "string" + }, + "content": { + "type": "string" + }, + "read": { + "type": "boolean", + "format": "boolean" + }, + "link": { + "type": "string" + }, + "createdAt": { + "type": "string" + } + }, + "title": "Notification", + "required": [ + "id", + "type", + "title", + "content", + "read", + "createdAt" + ] + }, + "NotificationListResp": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/Notification" + } + }, + "meta": { + "$ref": "#/definitions/PageMeta" + } + }, + "title": "NotificationListResp", + "required": [ + "items", + "meta" + ] + }, + "PageMeta": { + "type": "object", + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "offset": { + "type": "integer", + "format": "int64" + }, + "limit": { + "type": "integer", + "format": "int64" + } + }, + "title": "PageMeta", + "required": [ + "total", + "offset", + "limit" + ] + }, + "PageReq": { + "type": "object", + "properties": { + "offset": { + "type": "integer", + "format": "int64", + "default": "0" + }, + "limit": { + "type": "integer", + "format": "int64", + "default": "20" + } + }, + "title": "PageReq", + "required": [ + "offset", + "limit" + ] + }, + "PathId": { + "type": "object", + "title": "PathId" + }, + "SimpleUser": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "nickname": { + "type": "string" + }, + "avatar": { + "type": "string" + } + }, + "title": "SimpleUser", + "required": [ + "id", + "nickname", + "avatar" + ] + }, + "UserProfile": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "username": { + "type": "string" + }, + "nickname": { + "type": "string" + }, + "avatar": { + "type": "string" + }, + "role": { + "type": "string", + "description": " consumer, player, owner, admin" + }, + "verifiedRoles": { + "type": "array", + "items": { + "type": "string" + } + }, + "verificationStatus": { + "type": "object" + }, + "phone": { + "type": "string" + }, + "bio": { + "type": "string" + }, + "createdAt": { + "type": "string" + } + }, + "title": "UserProfile", + "required": [ + "id", + "username", + "nickname", + "avatar", + "role", + "verifiedRoles", + "verificationStatus", + "createdAt" + ] + } + }, + "securityDefinitions": { + "apiKey": { + "type": "apiKey", + "description": "Enter JWT Bearer token **_only_**", + "name": "Authorization", + "in": "header" + } + } +} diff --git a/desc/api/docs/objectstory-api.json b/desc/api/docs/objectstory-api.json index 0145d04..9bc6c68 100644 --- a/desc/api/docs/objectstory-api.json +++ b/desc/api/docs/objectstory-api.json @@ -1,95 +1,132 @@ { + "swagger": "2.0", + "info": { + "title": "文件服务", + "description": "处理文件上传与获取", + "version": "v1" + }, + "schemes": [ + "http", + "https" + ], "consumes": [ "application/json" ], "produces": [ "application/json" ], - "schemes": [ - "https" - ], - "swagger": "2.0", - "info": { - "title": "文件服务", - "version": "v1" - }, - "basePath": "/", "paths": { "/api/v1/files": { "get": { - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], "summary": "文件获取接口 (如果是私有文件,通过此接口获取或重定向)", - "operationId": "fileGetFile", - "parameters": [ - { - "type": "string", - "name": "key", - "in": "query", - "required": true - } - ], + "operationId": "GetFile", "responses": { "200": { - "description": "", + "description": "A successful response.", "schema": {} } - } + }, + "parameters": [ + { + "name": "key", + "in": "query", + "required": true, + "type": "string" + } + ], + "tags": [ + "file" + ], + "consumes": [ + "multipart/form-data" + ] } }, "/api/v1/upload": { "post": { - "consumes": [ - "application/x-www-form-urlencoded" - ], - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], "summary": "文件上传接口", - "operationId": "fileUpload", - "parameters": [ - { - "enum": [ - "avatar", - "chat", - "post", - "verification", - "dispute" - ], - "type": "string", - "description": "文件类型限制", - "name": "type", - "in": "formData", - "required": true - } - ], + "operationId": "Upload", "responses": { "200": { - "description": "", + "description": "A successful response.", "schema": { - "type": "object", - "properties": { - "url": { - "description": "返回 CDN 地址或访问地址", - "type": "string" - } - } + "$ref": "#/definitions/UploadResp" } } - } + }, + "parameters": [ + { + "name": "body", + "description": " 上传请求参数(File文件流在Handler中通过 r.FormFile 获取)", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/UploadReq" + } + } + ], + "tags": [ + "file" + ], + "consumes": [ + "multipart/form-data" + ] } } }, - "x-date": "2026-04-22 22:30:27", - "x-description": "This is a goctl generated swagger file.", - "x-github": "https://github.com/zeromicro/go-zero", - "x-go-zero-doc": "https://go-zero.dev/", - "x-goctl-version": "1.10.1" -} \ No newline at end of file + "definitions": { + "GetFileReq": { + "type": "object", + "properties": { + "key": { + "type": "string" + } + }, + "title": "GetFileReq", + "required": [ + "key" + ] + }, + "UploadReq": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "avatar", + "chat", + "post", + "verification", + "dispute" + ], + "description": " 文件类型限制" + } + }, + "title": "UploadReq", + "required": [ + "type" + ] + }, + "UploadResp": { + "type": "object", + "properties": { + "url": { + "type": "string", + "description": " 返回 CDN 地址或访问地址" + } + }, + "title": "UploadResp", + "required": [ + "url" + ] + } + }, + "securityDefinitions": { + "apiKey": { + "type": "apiKey", + "description": "Enter JWT Bearer token **_only_**", + "name": "Authorization", + "in": "header" + } + } +} diff --git a/desc/api/docs/order-api.json b/desc/api/docs/order-api.json index 13dbc5b..afe447f 100644 --- a/desc/api/docs/order-api.json +++ b/desc/api/docs/order-api.json @@ -1,889 +1,675 @@ { + "swagger": "2.0", + "info": { + "title": "聚玩订单服务", + "description": "处理订单业务", + "version": "1.0" + }, + "schemes": [ + "http", + "https" + ], "consumes": [ "application/json" ], "produces": [ "application/json" ], - "schemes": [ - "https" - ], - "swagger": "2.0", - "info": { - "title": "聚玩订单服务", - "version": "1.0" - }, - "basePath": "/", "paths": { - "/api/v1/orders": { + "/api/v1/orders/": { "get": { - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], "summary": "获取订单列表", - "operationId": "orderListOrders", - "parameters": [ - { - "type": "integer", - "default": 0, - "example": 0, - "name": "offset", - "in": "query", - "required": true - }, - { - "type": "integer", - "default": 20, - "example": 20, - "name": "limit", - "in": "query", - "required": true - }, - { - "type": "string", - "description": "consumer, player, owner", - "name": "role", - "in": "query", - "required": true - }, - { - "type": "string", - "name": "status", - "in": "query", - "allowEmptyValue": true - } - ], + "operationId": "ListOrders", "responses": { "200": { - "description": "", + "description": "A successful response.", "schema": { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "type": "object", - "required": [ - "id", - "consumerId", - "playerId", - "service", - "status", - "totalPrice", - "createdAt" - ], - "properties": { - "acceptedAt": { - "type": "string" - }, - "completedAt": { - "type": "string" - }, - "consumerId": { - "type": "integer" - }, - "createdAt": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "note": { - "type": "string" - }, - "playerId": { - "type": "string" - }, - "service": { - "type": "object", - "required": [ - "id", - "playerId", - "gameId", - "gameName", - "title", - "description", - "price", - "unit", - "availability" - ], - "properties": { - "availability": { - "type": "array", - "items": { - "type": "string" - } - }, - "description": { - "type": "string" - }, - "gameId": { - "type": "integer" - }, - "gameName": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "playerId": { - "type": "integer" - }, - "price": { - "type": "number" - }, - "rankRange": { - "type": "string" - }, - "title": { - "type": "string" - }, - "unit": { - "type": "string" - } - } - }, - "shopId": { - "type": "integer" - }, - "status": { - "type": "string" - }, - "totalPrice": { - "type": "number" - } - } - } - }, - "meta": { - "type": "object", - "required": [ - "total", - "offset", - "limit" - ], - "properties": { - "limit": { - "type": "integer" - }, - "offset": { - "type": "integer" - }, - "total": { - "type": "integer" - } - } - } - } + "$ref": "#/definitions/OrderListResp" } } - } + }, + "parameters": [ + { + "name": "offset", + "in": "query", + "required": true, + "type": "integer", + "format": "int64", + "default": "0" + }, + { + "name": "limit", + "in": "query", + "required": true, + "type": "integer", + "format": "int64", + "default": "20" + }, + { + "name": "role", + "description": " consumer, player, owner", + "in": "query", + "required": true, + "type": "string" + }, + { + "name": "status", + "in": "query", + "required": false, + "type": "string" + } + ], + "tags": [ + "order" + ], + "consumes": [ + "multipart/form-data" + ] }, "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], "summary": "创建订单", - "operationId": "orderCreateOrder", + "operationId": "CreateOrder", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/CreateOrderResp" + } + } + }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { - "type": "object", - "required": [ - "playerId", - "serviceId", - "quantity" - ], - "properties": { - "note": { - "type": "string" - }, - "playerId": { - "type": "integer" - }, - "quantity": { - "type": "integer" - }, - "serviceId": { - "type": "integer" - }, - "shopId": { - "type": "integer" - } - } + "$ref": "#/definitions/CreateOrderReq" } } ], - "responses": { - "200": { - "description": "", - "schema": { - "type": "object", - "properties": { - "ok": { - "type": "boolean" - }, - "order": { - "type": "object", - "required": [ - "id", - "consumerId", - "playerId", - "service", - "status", - "totalPrice", - "createdAt" - ], - "properties": { - "acceptedAt": { - "type": "string" - }, - "completedAt": { - "type": "string" - }, - "consumerId": { - "type": "integer" - }, - "createdAt": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "note": { - "type": "string" - }, - "playerId": { - "type": "string" - }, - "service": { - "type": "object", - "required": [ - "id", - "playerId", - "gameId", - "gameName", - "title", - "description", - "price", - "unit", - "availability" - ], - "properties": { - "availability": { - "type": "array", - "items": { - "type": "string" - } - }, - "description": { - "type": "string" - }, - "gameId": { - "type": "integer" - }, - "gameName": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "playerId": { - "type": "integer" - }, - "price": { - "type": "number" - }, - "rankRange": { - "type": "string" - }, - "title": { - "type": "string" - }, - "unit": { - "type": "string" - } - } - }, - "shopId": { - "type": "integer" - }, - "status": { - "type": "string" - }, - "totalPrice": { - "type": "number" - } - } - } - } - } - } - } + "tags": [ + "order" + ] } }, "/api/v1/orders/paid": { "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], "summary": "创建并支付订单", - "operationId": "orderCreateAndPayOrder", + "operationId": "CreateAndPayOrder", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/CreateOrderResp" + } + } + }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { - "type": "object", - "required": [ - "playerId", - "serviceId", - "quantity" - ], - "properties": { - "note": { - "type": "string" - }, - "playerId": { - "type": "integer" - }, - "quantity": { - "type": "integer" - }, - "serviceId": { - "type": "integer" - }, - "shopId": { - "type": "integer" - } - } + "$ref": "#/definitions/CreateOrderReq" } } ], - "responses": { - "200": { - "description": "", - "schema": { - "type": "object", - "properties": { - "ok": { - "type": "boolean" - }, - "order": { - "type": "object", - "required": [ - "id", - "consumerId", - "playerId", - "service", - "status", - "totalPrice", - "createdAt" - ], - "properties": { - "acceptedAt": { - "type": "string" - }, - "completedAt": { - "type": "string" - }, - "consumerId": { - "type": "integer" - }, - "createdAt": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "note": { - "type": "string" - }, - "playerId": { - "type": "string" - }, - "service": { - "type": "object", - "required": [ - "id", - "playerId", - "gameId", - "gameName", - "title", - "description", - "price", - "unit", - "availability" - ], - "properties": { - "availability": { - "type": "array", - "items": { - "type": "string" - } - }, - "description": { - "type": "string" - }, - "gameId": { - "type": "integer" - }, - "gameName": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "playerId": { - "type": "integer" - }, - "price": { - "type": "number" - }, - "rankRange": { - "type": "string" - }, - "title": { - "type": "string" - }, - "unit": { - "type": "string" - } - } - }, - "shopId": { - "type": "integer" - }, - "status": { - "type": "string" - }, - "totalPrice": { - "type": "number" - } - } - } - } - } - } - } + "tags": [ + "order" + ] } }, "/api/v1/orders/{id}": { "get": { - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], "summary": "获取订单详情", - "operationId": "orderGetOrder", - "parameters": [ - { - "type": "integer", - "name": "id", - "in": "path", - "required": true - } - ], + "operationId": "GetOrder", "responses": { "200": { - "description": "", + "description": "A successful response.", "schema": { - "type": "object", - "properties": { - "acceptedAt": { - "type": "string" - }, - "completedAt": { - "type": "string" - }, - "consumerId": { - "type": "integer" - }, - "createdAt": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "note": { - "type": "string" - }, - "playerId": { - "type": "string" - }, - "service": { - "type": "object", - "required": [ - "id", - "playerId", - "gameId", - "gameName", - "title", - "description", - "price", - "unit", - "availability" - ], - "properties": { - "availability": { - "type": "array", - "items": { - "type": "string" - } - }, - "description": { - "type": "string" - }, - "gameId": { - "type": "integer" - }, - "gameName": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "playerId": { - "type": "integer" - }, - "price": { - "type": "number" - }, - "rankRange": { - "type": "string" - }, - "title": { - "type": "string" - }, - "unit": { - "type": "string" - } - } - }, - "shopId": { - "type": "integer" - }, - "status": { - "type": "string" - }, - "totalPrice": { - "type": "number" - } - } + "$ref": "#/definitions/Order" } } - } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string" + } + ], + "tags": [ + "order" + ] } }, "/api/v1/orders/{id}/accept": { "post": { - "consumes": [ - "application/x-www-form-urlencoded" - ], - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], "summary": "接单", - "operationId": "orderAcceptOrder", - "parameters": [ - { - "type": "integer", - "name": "id", - "in": "path", - "required": true - } - ], + "operationId": "AcceptOrder", "responses": { "200": { - "description": "", + "description": "A successful response.", "schema": { - "type": "object" + "$ref": "#/definitions/EmptyResp" } } - } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/PathId" + } + } + ], + "tags": [ + "order" + ] } }, "/api/v1/orders/{id}/cancel": { "post": { - "consumes": [ - "application/x-www-form-urlencoded" - ], - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], "summary": "取消订单", - "operationId": "orderCancelOrder", - "parameters": [ - { - "type": "integer", - "name": "id", - "in": "path", - "required": true - } - ], + "operationId": "CancelOrder", "responses": { "200": { - "description": "", + "description": "A successful response.", "schema": { - "type": "object" + "$ref": "#/definitions/EmptyResp" } } - } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/PathId" + } + } + ], + "tags": [ + "order" + ] } }, "/api/v1/orders/{id}/confirm-close": { "post": { - "consumes": [ - "application/x-www-form-urlencoded" - ], - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], "summary": "确认结算", - "operationId": "orderConfirmCloseOrder", - "parameters": [ - { - "type": "integer", - "name": "id", - "in": "path", - "required": true - } - ], + "operationId": "ConfirmCloseOrder", "responses": { "200": { - "description": "", + "description": "A successful response.", "schema": { - "type": "object" + "$ref": "#/definitions/EmptyResp" } } - } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/PathId" + } + } + ], + "tags": [ + "order" + ] } }, "/api/v1/orders/{id}/pay": { "post": { - "consumes": [ - "application/x-www-form-urlencoded" - ], - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], "summary": "支付订单", - "operationId": "orderPayOrder", - "parameters": [ - { - "type": "integer", - "name": "id", - "in": "path", - "required": true - } - ], + "operationId": "PayOrder", "responses": { "200": { - "description": "", + "description": "A successful response.", "schema": { - "type": "object" + "$ref": "#/definitions/EmptyResp" } } - } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/PathId" + } + } + ], + "tags": [ + "order" + ] } }, "/api/v1/orders/{id}/reorder": { "post": { - "consumes": [ - "application/x-www-form-urlencoded" - ], - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], "summary": "再来一单", - "operationId": "orderReorder", - "parameters": [ - { - "type": "integer", - "name": "id", - "in": "path", - "required": true - } - ], + "operationId": "Reorder", "responses": { "200": { - "description": "", + "description": "A successful response.", "schema": { - "type": "object", - "properties": { - "ok": { - "type": "boolean" - }, - "order": { - "type": "object", - "required": [ - "id", - "consumerId", - "playerId", - "service", - "status", - "totalPrice", - "createdAt" - ], - "properties": { - "acceptedAt": { - "type": "string" - }, - "completedAt": { - "type": "string" - }, - "consumerId": { - "type": "integer" - }, - "createdAt": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "note": { - "type": "string" - }, - "playerId": { - "type": "string" - }, - "service": { - "type": "object", - "required": [ - "id", - "playerId", - "gameId", - "gameName", - "title", - "description", - "price", - "unit", - "availability" - ], - "properties": { - "availability": { - "type": "array", - "items": { - "type": "string" - } - }, - "description": { - "type": "string" - }, - "gameId": { - "type": "integer" - }, - "gameName": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "playerId": { - "type": "integer" - }, - "price": { - "type": "number" - }, - "rankRange": { - "type": "string" - }, - "title": { - "type": "string" - }, - "unit": { - "type": "string" - } - } - }, - "shopId": { - "type": "integer" - }, - "status": { - "type": "string" - }, - "totalPrice": { - "type": "number" - } - } - } - } + "$ref": "#/definitions/CreateOrderResp" } } - } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/PathId" + } + } + ], + "tags": [ + "order" + ] } }, "/api/v1/orders/{id}/request-close": { "post": { - "consumes": [ - "application/x-www-form-urlencoded" - ], - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], "summary": "申请结算", - "operationId": "orderRequestCloseOrder", - "parameters": [ - { - "type": "integer", - "name": "id", - "in": "path", - "required": true - } - ], + "operationId": "RequestCloseOrder", "responses": { "200": { - "description": "", + "description": "A successful response.", "schema": { - "type": "object" + "$ref": "#/definitions/EmptyResp" } } - } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/PathId" + } + } + ], + "tags": [ + "order" + ] } } }, - "x-date": "2026-04-22 22:30:23", - "x-description": "This is a goctl generated swagger file.", - "x-github": "https://github.com/zeromicro/go-zero", - "x-go-zero-doc": "https://go-zero.dev/", - "x-goctl-version": "1.10.1" -} \ No newline at end of file + "definitions": { + "CreateOrderReq": { + "type": "object", + "properties": { + "playerId": { + "type": "integer", + "format": "int64" + }, + "shopId": { + "type": "integer", + "format": "int64" + }, + "serviceId": { + "type": "integer", + "format": "int64" + }, + "quantity": { + "type": "integer", + "format": "int32" + }, + "note": { + "type": "string" + } + }, + "title": "CreateOrderReq", + "required": [ + "playerId", + "serviceId", + "quantity" + ] + }, + "CreateOrderResp": { + "type": "object", + "properties": { + "ok": { + "type": "boolean", + "format": "boolean" + }, + "order": { + "$ref": "#/definitions/Order" + } + }, + "title": "CreateOrderResp", + "required": [ + "ok", + "order" + ] + }, + "EmptyResp": { + "type": "object", + "title": "EmptyResp" + }, + "Order": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "consumerId": { + "type": "integer", + "format": "int64" + }, + "playerId": { + "type": "string" + }, + "shopId": { + "type": "integer", + "format": "int64" + }, + "service": { + "$ref": "#/definitions/PlayerService" + }, + "status": { + "type": "string" + }, + "totalPrice": { + "type": "number", + "format": "double" + }, + "note": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "acceptedAt": { + "type": "string" + }, + "completedAt": { + "type": "string" + } + }, + "title": "Order", + "required": [ + "id", + "consumerId", + "playerId", + "service", + "status", + "totalPrice", + "createdAt" + ] + }, + "OrderListReq": { + "type": "object", + "properties": { + "offset": { + "type": "integer", + "format": "int64", + "default": "0" + }, + "limit": { + "type": "integer", + "format": "int64", + "default": "20" + }, + "role": { + "type": "string", + "description": " consumer, player, owner" + }, + "status": { + "type": "string" + } + }, + "title": "OrderListReq", + "required": [ + "role" + ] + }, + "OrderListResp": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/Order" + } + }, + "meta": { + "$ref": "#/definitions/PageMeta" + } + }, + "title": "OrderListResp", + "required": [ + "items", + "meta" + ] + }, + "PageMeta": { + "type": "object", + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "offset": { + "type": "integer", + "format": "int64" + }, + "limit": { + "type": "integer", + "format": "int64" + } + }, + "title": "PageMeta", + "required": [ + "total", + "offset", + "limit" + ] + }, + "PageReq": { + "type": "object", + "properties": { + "offset": { + "type": "integer", + "format": "int64", + "default": "0" + }, + "limit": { + "type": "integer", + "format": "int64", + "default": "20" + } + }, + "title": "PageReq", + "required": [ + "offset", + "limit" + ] + }, + "PathId": { + "type": "object", + "title": "PathId" + }, + "PlayerService": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "playerId": { + "type": "integer", + "format": "int64" + }, + "gameId": { + "type": "integer", + "format": "int64" + }, + "gameName": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "price": { + "type": "number", + "format": "double" + }, + "unit": { + "type": "string" + }, + "rankRange": { + "type": "string" + }, + "availability": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "title": "PlayerService", + "required": [ + "id", + "playerId", + "gameId", + "gameName", + "title", + "description", + "price", + "unit", + "availability" + ] + }, + "SimpleUser": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "nickname": { + "type": "string" + }, + "avatar": { + "type": "string" + } + }, + "title": "SimpleUser", + "required": [ + "id", + "nickname", + "avatar" + ] + }, + "UserProfile": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "username": { + "type": "string" + }, + "nickname": { + "type": "string" + }, + "avatar": { + "type": "string" + }, + "role": { + "type": "string", + "description": " consumer, player, owner, admin" + }, + "verifiedRoles": { + "type": "array", + "items": { + "type": "string" + } + }, + "verificationStatus": { + "type": "object" + }, + "phone": { + "type": "string" + }, + "bio": { + "type": "string" + }, + "createdAt": { + "type": "string" + } + }, + "title": "UserProfile", + "required": [ + "id", + "username", + "nickname", + "avatar", + "role", + "verifiedRoles", + "verificationStatus", + "createdAt" + ] + } + }, + "securityDefinitions": { + "apiKey": { + "type": "apiKey", + "description": "Enter JWT Bearer token **_only_**", + "name": "Authorization", + "in": "header" + } + } +} diff --git a/desc/api/docs/player-api.json b/desc/api/docs/player-api.json index a611e74..0818af4 100644 --- a/desc/api/docs/player-api.json +++ b/desc/api/docs/player-api.json @@ -1,1143 +1,779 @@ { + "swagger": "2.0", + "info": { + "title": "聚玩打手服务", + "description": "聚玩用户服务处理打手信息管理、服务发布及订单相关接口", + "version": "1.0" + }, + "schemes": [ + "http", + "https" + ], "consumes": [ "application/json" ], "produces": [ "application/json" ], - "schemes": [ - "https" - ], - "swagger": "2.0", - "info": { - "title": "聚玩打手服务", - "version": "1.0" - }, - "basePath": "/", "paths": { "/api/v1/players": { "get": { - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], "summary": "获取打手列表", - "operationId": "playerListPlayers", - "parameters": [ - { - "type": "integer", - "default": 0, - "example": 0, - "name": "offset", - "in": "query", - "required": true - }, - { - "type": "integer", - "default": 20, - "example": 20, - "name": "limit", - "in": "query", - "required": true - }, - { - "type": "integer", - "name": "gameId", - "in": "query", - "allowEmptyValue": true - }, - { - "type": "integer", - "name": "gender", - "in": "query", - "allowEmptyValue": true - } - ], + "operationId": "ListPlayers", "responses": { "200": { - "description": "", + "description": "A successful response.", "schema": { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "type": "object", - "required": [ - "id", - "user", - "rating", - "totalOrders", - "completionRate", - "status", - "games", - "services", - "gender", - "tags" - ], - "properties": { - "completionRate": { - "type": "number" - }, - "games": { - "type": "array", - "items": { - "type": "string" - } - }, - "gender": { - "type": "boolean" - }, - "id": { - "type": "integer" - }, - "rating": { - "type": "number" - }, - "services": { - "type": "array", - "items": { - "type": "object", - "required": [ - "playerId", - "gameId", - "gameName", - "title", - "description", - "price", - "unit", - "availability" - ], - "properties": { - "availability": { - "type": "array", - "items": { - "type": "string" - } - }, - "description": { - "type": "string" - }, - "gameId": { - "type": "integer" - }, - "gameName": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "playerId": { - "type": "integer" - }, - "price": { - "type": "number" - }, - "rankRange": { - "type": "string" - }, - "title": { - "type": "string" - }, - "unit": { - "type": "string" - } - } - } - }, - "shopId": { - "type": "string" - }, - "shopName": { - "type": "string" - }, - "status": { - "type": "string" - }, - "tags": { - "type": "array", - "items": { - "type": "string" - } - }, - "totalOrders": { - "type": "integer" - }, - "user": { - "type": "object", - "required": [ - "id", - "username", - "nickname", - "avatar", - "role", - "verifiedRoles", - "verificationStatus", - "createdAt" - ], - "properties": { - "avatar": { - "type": "string" - }, - "bio": { - "type": "string" - }, - "createdAt": { - "type": "string" - }, - "id": { - "type": "string" - }, - "nickname": { - "type": "string" - }, - "phone": { - "type": "string" - }, - "role": { - "description": "consumer, player, owner, admin", - "type": "string" - }, - "username": { - "type": "string" - }, - "verificationStatus": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "verifiedRoles": { - "type": "array", - "items": { - "type": "string" - } - } - } - } - } - } - }, - "meta": { - "type": "object", - "required": [ - "total", - "offset", - "limit" - ], - "properties": { - "limit": { - "type": "integer" - }, - "offset": { - "type": "integer" - }, - "total": { - "type": "integer" - } - } - } - } + "$ref": "#/definitions/PlayerListResp" } } - } + }, + "parameters": [ + { + "name": "offset", + "in": "query", + "required": true, + "type": "integer", + "format": "int64", + "default": "0" + }, + { + "name": "limit", + "in": "query", + "required": true, + "type": "integer", + "format": "int64", + "default": "20" + }, + { + "name": "gameId", + "in": "query", + "required": false, + "type": "integer", + "format": "int64" + }, + { + "name": "gender", + "in": "query", + "required": false, + "type": "integer", + "format": "int64" + } + ], + "tags": [ + "player" + ], + "consumes": [ + "multipart/form-data" + ] } }, "/api/v1/players/me": { - "post": { - "consumes": [ - "application/x-www-form-urlencoded" - ], - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], - "summary": "初始化当前用户的打手资料", - "operationId": "playerInitPlayer", + "get": { + "summary": "获取当前用户的打手资料", + "operationId": "GetMyPlayer", "responses": { "200": { - "description": "", + "description": "A successful response.", "schema": { - "type": "object", - "properties": { - "completionRate": { - "type": "number" - }, - "games": { - "type": "array", - "items": { - "type": "string" - } - }, - "gender": { - "type": "boolean" - }, - "id": { - "type": "integer" - }, - "rating": { - "type": "number" - }, - "services": { - "type": "array", - "items": { - "type": "object", - "required": [ - "playerId", - "gameId", - "gameName", - "title", - "description", - "price", - "unit", - "availability" - ], - "properties": { - "availability": { - "type": "array", - "items": { - "type": "string" - } - }, - "description": { - "type": "string" - }, - "gameId": { - "type": "integer" - }, - "gameName": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "playerId": { - "type": "integer" - }, - "price": { - "type": "number" - }, - "rankRange": { - "type": "string" - }, - "title": { - "type": "string" - }, - "unit": { - "type": "string" - } - } - } - }, - "shopId": { - "type": "string" - }, - "shopName": { - "type": "string" - }, - "status": { - "type": "string" - }, - "tags": { - "type": "array", - "items": { - "type": "string" - } - }, - "totalOrders": { - "type": "integer" - }, - "user": { - "type": "object", - "required": [ - "id", - "username", - "nickname", - "avatar", - "role", - "verifiedRoles", - "verificationStatus", - "createdAt" - ], - "properties": { - "avatar": { - "type": "string" - }, - "bio": { - "type": "string" - }, - "createdAt": { - "type": "string" - }, - "id": { - "type": "string" - }, - "nickname": { - "type": "string" - }, - "phone": { - "type": "string" - }, - "role": { - "description": "consumer, player, owner, admin", - "type": "string" - }, - "username": { - "type": "string" - }, - "verificationStatus": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "verifiedRoles": { - "type": "array", - "items": { - "type": "string" - } - } - } - } - } + "$ref": "#/definitions/PlayerProfile" } } - } + }, + "tags": [ + "player" + ] + }, + "post": { + "summary": "初始化当前用户的打手资料", + "operationId": "InitPlayer", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/PlayerProfile" + } + } + }, + "parameters": [ + { + "name": "body", + "description": " 空响应", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/EmptyResp" + } + } + ], + "tags": [ + "player" + ] } }, "/api/v1/players/me/status": { "put": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], "summary": "更新接单状态", - "operationId": "playerUpdatePlayerStatus", + "operationId": "UpdatePlayerStatus", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/EmptyResp" + } + } + }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { - "type": "object", - "required": [ - "status" - ], - "properties": { - "status": { - "type": "string" - } - } + "$ref": "#/definitions/UpdatePlayerStatusReq" } } ], - "responses": { - "200": { - "description": "", - "schema": { - "type": "object" - } - } - } + "tags": [ + "player" + ] } }, "/api/v1/players/{id}": { "get": { - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], "summary": "获取打手详情", - "operationId": "playerGetPlayer", - "parameters": [ - { - "type": "integer", - "name": "id", - "in": "path", - "required": true - } - ], + "operationId": "GetPlayer", "responses": { "200": { - "description": "", + "description": "A successful response.", "schema": { - "type": "object", - "properties": { - "completionRate": { - "type": "number" - }, - "games": { - "type": "array", - "items": { - "type": "string" - } - }, - "gender": { - "type": "boolean" - }, - "id": { - "type": "integer" - }, - "rating": { - "type": "number" - }, - "services": { - "type": "array", - "items": { - "type": "object", - "required": [ - "playerId", - "gameId", - "gameName", - "title", - "description", - "price", - "unit", - "availability" - ], - "properties": { - "availability": { - "type": "array", - "items": { - "type": "string" - } - }, - "description": { - "type": "string" - }, - "gameId": { - "type": "integer" - }, - "gameName": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "playerId": { - "type": "integer" - }, - "price": { - "type": "number" - }, - "rankRange": { - "type": "string" - }, - "title": { - "type": "string" - }, - "unit": { - "type": "string" - } - } - } - }, - "shopId": { - "type": "string" - }, - "shopName": { - "type": "string" - }, - "status": { - "type": "string" - }, - "tags": { - "type": "array", - "items": { - "type": "string" - } - }, - "totalOrders": { - "type": "integer" - }, - "user": { - "type": "object", - "required": [ - "id", - "username", - "nickname", - "avatar", - "role", - "verifiedRoles", - "verificationStatus", - "createdAt" - ], - "properties": { - "avatar": { - "type": "string" - }, - "bio": { - "type": "string" - }, - "createdAt": { - "type": "string" - }, - "id": { - "type": "string" - }, - "nickname": { - "type": "string" - }, - "phone": { - "type": "string" - }, - "role": { - "description": "consumer, player, owner, admin", - "type": "string" - }, - "username": { - "type": "string" - }, - "verificationStatus": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "verifiedRoles": { - "type": "array", - "items": { - "type": "string" - } - } - } - } - } + "$ref": "#/definitions/PlayerProfile" } } - } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string" + } + ], + "tags": [ + "player" + ] } }, "/api/v1/players/{id}/services": { "get": { - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], "summary": "获取指定打手的服务列表", - "operationId": "playerListPlayerServices", - "parameters": [ - { - "type": "integer", - "default": 0, - "example": 0, - "name": "offset", - "in": "query", - "required": true - }, - { - "type": "integer", - "default": 20, - "example": 20, - "name": "limit", - "in": "query", - "required": true - }, - { - "type": "integer", - "name": "id", - "in": "path", - "required": true - } - ], + "operationId": "ListPlayerServices", "responses": { "200": { - "description": "", + "description": "A successful response.", "schema": { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "type": "object", - "required": [ - "playerId", - "gameId", - "gameName", - "title", - "description", - "price", - "unit", - "availability" - ], - "properties": { - "availability": { - "type": "array", - "items": { - "type": "string" - } - }, - "description": { - "type": "string" - }, - "gameId": { - "type": "integer" - }, - "gameName": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "playerId": { - "type": "integer" - }, - "price": { - "type": "number" - }, - "rankRange": { - "type": "string" - }, - "title": { - "type": "string" - }, - "unit": { - "type": "string" - } - } - } - }, - "meta": { - "type": "object", - "required": [ - "total", - "offset", - "limit" - ], - "properties": { - "limit": { - "type": "integer" - }, - "offset": { - "type": "integer" - }, - "total": { - "type": "integer" - } - } - } - } + "$ref": "#/definitions/PlayerServiceListResp" } } - } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "offset", + "in": "query", + "required": true, + "type": "integer", + "format": "int64", + "default": "0" + }, + { + "name": "limit", + "in": "query", + "required": true, + "type": "integer", + "format": "int64", + "default": "20" + } + ], + "tags": [ + "player" + ] } }, "/api/v1/services": { "get": { - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], "summary": "获取所有服务列表", - "operationId": "playerListServices", - "parameters": [ - { - "type": "integer", - "default": 0, - "example": 0, - "name": "offset", - "in": "query", - "required": true - }, - { - "type": "integer", - "default": 20, - "example": 20, - "name": "limit", - "in": "query", - "required": true - } - ], + "operationId": "ListServices", "responses": { "200": { - "description": "", + "description": "A successful response.", "schema": { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "type": "object", - "required": [ - "playerId", - "gameId", - "gameName", - "title", - "description", - "price", - "unit", - "availability" - ], - "properties": { - "availability": { - "type": "array", - "items": { - "type": "string" - } - }, - "description": { - "type": "string" - }, - "gameId": { - "type": "integer" - }, - "gameName": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "playerId": { - "type": "integer" - }, - "price": { - "type": "number" - }, - "rankRange": { - "type": "string" - }, - "title": { - "type": "string" - }, - "unit": { - "type": "string" - } - } - } - }, - "meta": { - "type": "object", - "required": [ - "total", - "offset", - "limit" - ], - "properties": { - "limit": { - "type": "integer" - }, - "offset": { - "type": "integer" - }, - "total": { - "type": "integer" - } - } - } - } + "$ref": "#/definitions/PlayerServiceListResp" } } - } + }, + "parameters": [ + { + "name": "offset", + "in": "query", + "required": true, + "type": "integer", + "format": "int64", + "default": "0" + }, + { + "name": "limit", + "in": "query", + "required": true, + "type": "integer", + "format": "int64", + "default": "20" + } + ], + "tags": [ + "player" + ], + "consumes": [ + "multipart/form-data" + ] }, "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], "summary": "创建服务", - "operationId": "playerCreateService", + "operationId": "CreateService", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/PlayerService" + } + } + }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { - "type": "object", - "required": [ - "price", - "unit" - ], - "properties": { - "availability": { - "type": "array", - "items": { - "type": "string" - } - }, - "description": { - "type": "string" - }, - "gameId": { - "type": "integer" - }, - "price": { - "type": "number" - }, - "rankRange": { - "type": "string" - }, - "title": { - "type": "string" - }, - "unit": { - "type": "string" - } - } + "$ref": "#/definitions/CreateServiceReq" } } ], - "responses": { - "200": { - "description": "", - "schema": { - "type": "object", - "properties": { - "availability": { - "type": "array", - "items": { - "type": "string" - } - }, - "description": { - "type": "string" - }, - "gameId": { - "type": "integer" - }, - "gameName": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "playerId": { - "type": "integer" - }, - "price": { - "type": "number" - }, - "rankRange": { - "type": "string" - }, - "title": { - "type": "string" - }, - "unit": { - "type": "string" - } - } - } - } - } + "tags": [ + "player" + ] } }, "/api/v1/services/{id}": { "get": { - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], "summary": "获取服务详情", - "operationId": "playerGetService", - "parameters": [ - { - "type": "integer", - "name": "id", - "in": "path", - "required": true - } - ], + "operationId": "GetService", "responses": { "200": { - "description": "", + "description": "A successful response.", "schema": { - "type": "object", - "properties": { - "availability": { - "type": "array", - "items": { - "type": "string" - } - }, - "description": { - "type": "string" - }, - "gameId": { - "type": "integer" - }, - "gameName": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "playerId": { - "type": "integer" - }, - "price": { - "type": "number" - }, - "rankRange": { - "type": "string" - }, - "title": { - "type": "string" - }, - "unit": { - "type": "string" - } - } + "$ref": "#/definitions/PlayerService" } } - } - }, - "put": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], - "summary": "更新服务", - "operationId": "playerUpdateService", + }, "parameters": [ { - "type": "integer", "name": "id", "in": "path", - "required": true + "required": true, + "type": "string" + } + ], + "tags": [ + "player" + ] + }, + "delete": { + "summary": "删除服务", + "operationId": "DeleteService", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/EmptyResp" + } + } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string" }, { "name": "body", "in": "body", "required": true, "schema": { - "type": "object", - "required": [ - "availability" - ], - "properties": { - "availability": { - "type": "array", - "items": { - "type": "string" - } - }, - "description": { - "type": "string" - }, - "gameId": { - "type": "integer" - }, - "price": { - "type": "number" - }, - "rankRange": { - "type": "string" - }, - "title": { - "type": "string" - }, - "unit": { - "type": "string" - } - } + "$ref": "#/definitions/DeleteServiceReq" } } ], + "tags": [ + "player" + ] + }, + "put": { + "summary": "更新服务", + "operationId": "UpdateService", "responses": { "200": { - "description": "", + "description": "A successful response.", "schema": { - "type": "object", - "properties": { - "availability": { - "type": "array", - "items": { - "type": "string" - } - }, - "description": { - "type": "string" - }, - "gameId": { - "type": "integer" - }, - "gameName": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "playerId": { - "type": "integer" - }, - "price": { - "type": "number" - }, - "rankRange": { - "type": "string" - }, - "title": { - "type": "string" - }, - "unit": { - "type": "string" - } - } + "$ref": "#/definitions/PlayerService" } } - } - }, - "delete": { - "consumes": [ - "application/x-www-form-urlencoded" - ], - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], - "summary": "删除服务", - "operationId": "playerDeleteService", + }, "parameters": [ { - "type": "integer", "name": "id", "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, "schema": { - "type": "object" + "$ref": "#/definitions/UpdateServiceReq" } } - } + ], + "tags": [ + "player" + ] } } }, - "x-date": "2026-04-22 22:30:24", - "x-description": "This is a goctl generated swagger file.", - "x-github": "https://github.com/zeromicro/go-zero", - "x-go-zero-doc": "https://go-zero.dev/", - "x-goctl-version": "1.10.1" -} \ No newline at end of file + "definitions": { + "CreateServiceReq": { + "type": "object", + "properties": { + "gameId": { + "type": "integer", + "format": "int64" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "price": { + "type": "number", + "format": "double" + }, + "unit": { + "type": "string" + }, + "rankRange": { + "type": "string" + }, + "availability": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "title": "CreateServiceReq", + "required": [ + "price", + "unit" + ] + }, + "DeleteServiceReq": { + "type": "object", + "title": "DeleteServiceReq" + }, + "EmptyResp": { + "type": "object", + "title": "EmptyResp" + }, + "GetPlayerReq": { + "type": "object", + "title": "GetPlayerReq" + }, + "GetServiceReq": { + "type": "object", + "title": "GetServiceReq" + }, + "ListPlayerServicesReq": { + "type": "object", + "properties": { + "offset": { + "type": "integer", + "format": "int64", + "default": "0" + }, + "limit": { + "type": "integer", + "format": "int64", + "default": "20" + } + }, + "title": "ListPlayerServicesReq" + }, + "PageMeta": { + "type": "object", + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "offset": { + "type": "integer", + "format": "int64" + }, + "limit": { + "type": "integer", + "format": "int64" + } + }, + "title": "PageMeta", + "required": [ + "total", + "offset", + "limit" + ] + }, + "PageReq": { + "type": "object", + "properties": { + "offset": { + "type": "integer", + "format": "int64", + "default": "0" + }, + "limit": { + "type": "integer", + "format": "int64", + "default": "20" + } + }, + "title": "PageReq", + "required": [ + "offset", + "limit" + ] + }, + "PlayerListReq": { + "type": "object", + "properties": { + "offset": { + "type": "integer", + "format": "int64", + "default": "0" + }, + "limit": { + "type": "integer", + "format": "int64", + "default": "20" + }, + "gameId": { + "type": "integer", + "format": "int64" + }, + "gender": { + "type": "integer", + "format": "int64" + } + }, + "title": "PlayerListReq" + }, + "PlayerListResp": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/PlayerProfile" + } + }, + "meta": { + "$ref": "#/definitions/PageMeta" + } + }, + "title": "PlayerListResp", + "required": [ + "items", + "meta" + ] + }, + "PlayerProfile": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "user": { + "$ref": "#/definitions/UserProfile" + }, + "rating": { + "type": "number", + "format": "double" + }, + "totalOrders": { + "type": "integer", + "format": "int64" + }, + "completionRate": { + "type": "number", + "format": "double" + }, + "status": { + "type": "string" + }, + "games": { + "type": "array", + "items": { + "type": "string" + } + }, + "services": { + "type": "array", + "items": { + "$ref": "#/definitions/PlayerService" + } + }, + "shopId": { + "type": "string" + }, + "shopName": { + "type": "string" + }, + "gender": { + "type": "boolean", + "format": "boolean" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "title": "PlayerProfile", + "required": [ + "id", + "user", + "rating", + "totalOrders", + "completionRate", + "status", + "games", + "services", + "gender", + "tags" + ] + }, + "PlayerService": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "playerId": { + "type": "integer", + "format": "int64" + }, + "gameId": { + "type": "integer", + "format": "int64" + }, + "gameName": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "price": { + "type": "number", + "format": "double" + }, + "unit": { + "type": "string" + }, + "rankRange": { + "type": "string" + }, + "availability": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "title": "PlayerService", + "required": [ + "playerId", + "gameId", + "gameName", + "title", + "description", + "price", + "unit", + "availability" + ] + }, + "PlayerServiceListResp": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/PlayerService" + } + }, + "meta": { + "$ref": "#/definitions/PageMeta" + } + }, + "title": "PlayerServiceListResp", + "required": [ + "items", + "meta" + ] + }, + "SimpleUser": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "nickname": { + "type": "string" + }, + "avatar": { + "type": "string" + } + }, + "title": "SimpleUser", + "required": [ + "id", + "nickname", + "avatar" + ] + }, + "UpdatePlayerStatusReq": { + "type": "object", + "properties": { + "status": { + "type": "string" + } + }, + "title": "UpdatePlayerStatusReq", + "required": [ + "status" + ] + }, + "UpdateServiceReq": { + "type": "object", + "properties": { + "gameId": { + "type": "integer", + "format": "int64" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "price": { + "type": "number", + "format": "double" + }, + "unit": { + "type": "string" + }, + "rankRange": { + "type": "string" + }, + "availability": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "title": "UpdateServiceReq", + "required": [ + "availability" + ] + }, + "UserProfile": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "username": { + "type": "string" + }, + "nickname": { + "type": "string" + }, + "avatar": { + "type": "string" + }, + "role": { + "type": "string", + "description": " consumer, player, owner, admin" + }, + "verifiedRoles": { + "type": "array", + "items": { + "type": "string" + } + }, + "verificationStatus": { + "type": "object" + }, + "phone": { + "type": "string" + }, + "bio": { + "type": "string" + }, + "createdAt": { + "type": "string" + } + }, + "title": "UserProfile", + "required": [ + "id", + "username", + "nickname", + "avatar", + "role", + "verifiedRoles", + "verificationStatus", + "createdAt" + ] + } + }, + "securityDefinitions": { + "apiKey": { + "type": "apiKey", + "description": "Enter JWT Bearer token **_only_**", + "name": "Authorization", + "in": "header" + } + } +} diff --git a/desc/api/docs/review-api.json b/desc/api/docs/review-api.json new file mode 100644 index 0000000..c22cf25 --- /dev/null +++ b/desc/api/docs/review-api.json @@ -0,0 +1,401 @@ +{ + "swagger": "2.0", + "info": { + "title": "", + "version": "" + }, + "schemes": [ + "http", + "https" + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "paths": { + "/api/v1/orders/{id}/review": { + "post": { + "summary": "提交评价", + "operationId": "SubmitReview", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/EmptyResp" + } + } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/SubmitReviewReq" + } + } + ], + "tags": [ + "review" + ] + } + }, + "/api/v1/orders/{id}/reviews": { + "get": { + "summary": "获取订单评价", + "operationId": "GetOrderReviews", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/ReviewListResp" + } + } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "id", + "in": "query", + "required": true, + "type": "integer", + "format": "int64" + } + ], + "tags": [ + "review" + ] + } + }, + "/api/v1/reviews": { + "get": { + "summary": "获取公开评价列表", + "operationId": "ListReviews", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/ReviewListResp" + } + } + }, + "parameters": [ + { + "name": "offset", + "in": "query", + "required": true, + "type": "integer", + "format": "int64", + "default": "0" + }, + { + "name": "limit", + "in": "query", + "required": true, + "type": "integer", + "format": "int64", + "default": "20" + } + ], + "tags": [ + "review" + ], + "consumes": [ + "multipart/form-data" + ] + } + }, + "/api/v1/users/{id}/reviews": { + "get": { + "summary": "获取用户收到的评价", + "operationId": "ListUserReviews", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/ReviewListResp" + } + } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "id", + "in": "query", + "required": true, + "type": "integer", + "format": "int64" + }, + { + "name": "offset", + "in": "query", + "required": true, + "type": "integer", + "format": "int64", + "default": "0" + }, + { + "name": "limit", + "in": "query", + "required": true, + "type": "integer", + "format": "int64", + "default": "20" + } + ], + "tags": [ + "review" + ] + } + } + }, + "definitions": { + "EmptyResp": { + "type": "object", + "title": "EmptyResp" + }, + "GetOrderReviewsReq": { + "type": "object", + "title": "GetOrderReviewsReq" + }, + "ListUserReviewsReq": { + "type": "object", + "properties": { + "offset": { + "type": "integer", + "format": "int64", + "default": "0" + }, + "limit": { + "type": "integer", + "format": "int64", + "default": "20" + } + }, + "title": "ListUserReviewsReq" + }, + "PageMeta": { + "type": "object", + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "offset": { + "type": "integer", + "format": "int64" + }, + "limit": { + "type": "integer", + "format": "int64" + } + }, + "title": "PageMeta", + "required": [ + "total", + "offset", + "limit" + ] + }, + "PageReq": { + "type": "object", + "properties": { + "offset": { + "type": "integer", + "format": "int64", + "default": "0" + }, + "limit": { + "type": "integer", + "format": "int64", + "default": "20" + } + }, + "title": "PageReq", + "required": [ + "offset", + "limit" + ] + }, + "Review": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "orderId": { + "type": "integer", + "format": "int64" + }, + "fromUserId": { + "type": "integer", + "format": "int64" + }, + "fromUserName": { + "type": "string" + }, + "rating": { + "type": "integer", + "format": "int32" + }, + "content": { + "type": "string" + }, + "sealed": { + "type": "boolean", + "format": "boolean" + }, + "createdAt": { + "type": "string" + } + }, + "title": "Review", + "required": [ + "id", + "orderId", + "fromUserId", + "fromUserName", + "rating", + "content", + "sealed", + "createdAt" + ] + }, + "ReviewListResp": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/Review" + } + }, + "meta": { + "$ref": "#/definitions/PageMeta" + } + }, + "title": "ReviewListResp", + "required": [ + "items", + "meta" + ] + }, + "ReviewPathId": { + "type": "object", + "title": "ReviewPathId" + }, + "SimpleUser": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "nickname": { + "type": "string" + }, + "avatar": { + "type": "string" + } + }, + "title": "SimpleUser", + "required": [ + "id", + "nickname", + "avatar" + ] + }, + "SubmitReviewReq": { + "type": "object", + "properties": { + "rating": { + "type": "integer", + "format": "int32" + }, + "content": { + "type": "string" + } + }, + "title": "SubmitReviewReq", + "required": [ + "rating" + ] + }, + "UserProfile": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "username": { + "type": "string" + }, + "nickname": { + "type": "string" + }, + "avatar": { + "type": "string" + }, + "role": { + "type": "string", + "description": " consumer, player, owner, admin" + }, + "verifiedRoles": { + "type": "array", + "items": { + "type": "string" + } + }, + "verificationStatus": { + "type": "object" + }, + "phone": { + "type": "string" + }, + "bio": { + "type": "string" + }, + "createdAt": { + "type": "string" + } + }, + "title": "UserProfile", + "required": [ + "id", + "username", + "nickname", + "avatar", + "role", + "verifiedRoles", + "verificationStatus", + "createdAt" + ] + } + }, + "securityDefinitions": { + "apiKey": { + "type": "apiKey", + "description": "Enter JWT Bearer token **_only_**", + "name": "Authorization", + "in": "header" + } + } +} diff --git a/desc/api/docs/search-api.json b/desc/api/docs/search-api.json new file mode 100644 index 0000000..0c37aa7 --- /dev/null +++ b/desc/api/docs/search-api.json @@ -0,0 +1,549 @@ +{ + "swagger": "2.0", + "info": { + "title": "", + "version": "" + }, + "schemes": [ + "http", + "https" + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "paths": { + "/api/v1/favorites": { + "get": { + "summary": "获取收藏列表", + "operationId": "ListFavorites", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/FavoriteListResp" + } + } + }, + "parameters": [ + { + "name": "offset", + "in": "query", + "required": true, + "type": "integer", + "format": "int64", + "default": "0" + }, + { + "name": "limit", + "in": "query", + "required": true, + "type": "integer", + "format": "int64", + "default": "20" + } + ], + "tags": [ + "favorites" + ], + "consumes": [ + "multipart/form-data" + ] + }, + "post": { + "summary": "添加收藏", + "operationId": "AddFavorite", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/EmptyResp" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/FavoriteReq" + } + } + ], + "tags": [ + "favorites" + ] + } + }, + "/api/v1/favorites/{id}": { + "delete": { + "summary": "取消收藏", + "operationId": "RemoveFavorite", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/EmptyResp" + } + } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/PathIDReq" + } + } + ], + "tags": [ + "favorites" + ] + } + }, + "/api/v1/recommendations/home": { + "get": { + "summary": "首页推荐", + "operationId": "Recommendations", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/SearchResp" + } + } + }, + "parameters": [ + { + "name": "offset", + "in": "query", + "required": true, + "type": "integer", + "format": "int64", + "default": "0" + }, + { + "name": "limit", + "in": "query", + "required": true, + "type": "integer", + "format": "int64", + "default": "20" + } + ], + "tags": [ + "search" + ], + "consumes": [ + "multipart/form-data" + ] + } + }, + "/api/v1/search": { + "get": { + "summary": "统一搜索", + "operationId": "Search", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/SearchResp" + } + } + }, + "parameters": [ + { + "name": "offset", + "in": "query", + "required": true, + "type": "integer", + "format": "int64", + "default": "0" + }, + { + "name": "limit", + "in": "query", + "required": true, + "type": "integer", + "format": "int64", + "default": "20" + }, + { + "name": "q", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "min", + "in": "query", + "required": false, + "type": "number", + "format": "double" + }, + { + "name": "max", + "in": "query", + "required": false, + "type": "number", + "format": "double" + }, + { + "name": "onlyOnline", + "in": "query", + "required": false, + "type": "boolean", + "format": "boolean" + }, + { + "name": "sort", + "in": "query", + "required": false, + "type": "string" + } + ], + "tags": [ + "search" + ], + "consumes": [ + "multipart/form-data" + ] + } + }, + "/api/v1/users/{id}/favorites/check": { + "get": { + "summary": "检查收藏状态", + "operationId": "CheckFavorite", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/FavoriteCheckResp" + } + } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "id", + "in": "query", + "required": true, + "type": "integer", + "format": "int64" + }, + { + "name": "targetType", + "in": "query", + "required": true, + "type": "string" + }, + { + "name": "targetId", + "in": "query", + "required": true, + "type": "string" + } + ], + "tags": [ + "favorites" + ], + "consumes": [ + "multipart/form-data" + ] + } + } + }, + "definitions": { + "EmptyResp": { + "type": "object", + "title": "EmptyResp" + }, + "Favorite": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "userId": { + "type": "string" + }, + "targetType": { + "type": "string" + }, + "targetId": { + "type": "string" + }, + "createdAt": { + "type": "string" + } + }, + "title": "Favorite", + "required": [ + "id", + "userId", + "targetType", + "targetId", + "createdAt" + ] + }, + "FavoriteCheckReq": { + "type": "object", + "properties": { + "targetType": { + "type": "string" + }, + "targetId": { + "type": "string" + } + }, + "title": "FavoriteCheckReq", + "required": [ + "targetType", + "targetId" + ] + }, + "FavoriteCheckResp": { + "type": "object", + "properties": { + "favorited": { + "type": "boolean", + "format": "boolean" + } + }, + "title": "FavoriteCheckResp", + "required": [ + "favorited" + ] + }, + "FavoriteListResp": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/Favorite" + } + }, + "meta": { + "$ref": "#/definitions/PageMeta" + } + }, + "title": "FavoriteListResp", + "required": [ + "items", + "meta" + ] + }, + "FavoriteReq": { + "type": "object", + "properties": { + "targetType": { + "type": "string", + "description": " player, shop" + }, + "targetId": { + "type": "string" + } + }, + "title": "FavoriteReq", + "required": [ + "targetType", + "targetId" + ] + }, + "PageMeta": { + "type": "object", + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "offset": { + "type": "integer", + "format": "int64" + }, + "limit": { + "type": "integer", + "format": "int64" + } + }, + "title": "PageMeta", + "required": [ + "total", + "offset", + "limit" + ] + }, + "PageReq": { + "type": "object", + "properties": { + "offset": { + "type": "integer", + "format": "int64", + "default": "0" + }, + "limit": { + "type": "integer", + "format": "int64", + "default": "20" + } + }, + "title": "PageReq", + "required": [ + "offset", + "limit" + ] + }, + "PathIDReq": { + "type": "object", + "title": "PathIDReq" + }, + "SearchReq": { + "type": "object", + "properties": { + "offset": { + "type": "integer", + "format": "int64", + "default": "0" + }, + "limit": { + "type": "integer", + "format": "int64", + "default": "20" + }, + "q": { + "type": "string" + }, + "min": { + "type": "number", + "format": "double" + }, + "max": { + "type": "number", + "format": "double" + }, + "onlyOnline": { + "type": "boolean", + "format": "boolean" + }, + "sort": { + "type": "string" + } + }, + "title": "SearchReq" + }, + "SearchResp": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "type": "object" + }, + "description": " Mixed items" + }, + "meta": { + "$ref": "#/definitions/PageMeta" + } + }, + "title": "SearchResp", + "required": [ + "items", + "meta" + ] + }, + "SimpleUser": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "nickname": { + "type": "string" + }, + "avatar": { + "type": "string" + } + }, + "title": "SimpleUser", + "required": [ + "id", + "nickname", + "avatar" + ] + }, + "UserProfile": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "username": { + "type": "string" + }, + "nickname": { + "type": "string" + }, + "avatar": { + "type": "string" + }, + "role": { + "type": "string", + "description": " consumer, player, owner, admin" + }, + "verifiedRoles": { + "type": "array", + "items": { + "type": "string" + } + }, + "verificationStatus": { + "type": "object" + }, + "phone": { + "type": "string" + }, + "bio": { + "type": "string" + }, + "createdAt": { + "type": "string" + } + }, + "title": "UserProfile", + "required": [ + "id", + "username", + "nickname", + "avatar", + "role", + "verifiedRoles", + "verificationStatus", + "createdAt" + ] + } + }, + "securityDefinitions": { + "apiKey": { + "type": "apiKey", + "description": "Enter JWT Bearer token **_only_**", + "name": "Authorization", + "in": "header" + } + } +} diff --git a/desc/api/docs/shop-api.json b/desc/api/docs/shop-api.json index 5520ba5..8d3e078 100644 --- a/desc/api/docs/shop-api.json +++ b/desc/api/docs/shop-api.json @@ -1,1148 +1,904 @@ { + "swagger": "2.0", + "info": { + "title": "", + "version": "" + }, + "schemes": [ + "http", + "https" + ], "consumes": [ "application/json" ], "produces": [ "application/json" ], - "schemes": [ - "https" - ], - "swagger": "2.0", - "info": { - "version": "1.0" - }, - "basePath": "/", "paths": { "/api/v1/shops": { "get": { - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], "summary": "获取店铺列表", - "operationId": "shopListShops", - "parameters": [ - { - "type": "integer", - "default": 0, - "example": 0, - "name": "offset", - "in": "query", - "required": true - }, - { - "type": "integer", - "default": 20, - "example": 20, - "name": "limit", - "in": "query", - "required": true - } - ], + "operationId": "ListShops", "responses": { "200": { - "description": "", + "description": "A successful response.", "schema": { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "type": "object", - "required": [ - "id", - "owner", - "name", - "description", - "rating", - "totalOrders", - "playerCount", - "commissionType", - "commissionValue", - "announcements", - "templateConfig" - ], - "properties": { - "announcements": { - "type": "array", - "items": { - "type": "string" - } - }, - "banner": { - "type": "string" - }, - "commissionType": { - "type": "string" - }, - "commissionValue": { - "type": "string" - }, - "description": { - "type": "string" - }, - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "owner": { - "type": "object", - "required": [ - "id", - "username", - "nickname", - "avatar", - "role", - "verifiedRoles", - "verificationStatus", - "createdAt" - ], - "properties": { - "avatar": { - "type": "string" - }, - "bio": { - "type": "string" - }, - "createdAt": { - "type": "string" - }, - "id": { - "type": "string" - }, - "nickname": { - "type": "string" - }, - "phone": { - "type": "string" - }, - "role": { - "description": "consumer, player, owner, admin", - "type": "string" - }, - "username": { - "type": "string" - }, - "verificationStatus": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "verifiedRoles": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "playerCount": { - "type": "integer" - }, - "rating": { - "type": "string" - }, - "templateConfig": {}, - "totalOrders": { - "type": "integer" - } - } - } - }, - "meta": { - "type": "object", - "required": [ - "total", - "offset", - "limit" - ], - "properties": { - "limit": { - "type": "integer" - }, - "offset": { - "type": "integer" - }, - "total": { - "type": "integer" - } - } - } - } + "$ref": "#/definitions/ShopListResp" } } - } + }, + "parameters": [ + { + "name": "offset", + "in": "query", + "required": true, + "type": "integer", + "format": "int64", + "default": "0" + }, + { + "name": "limit", + "in": "query", + "required": true, + "type": "integer", + "format": "int64", + "default": "20" + } + ], + "tags": [ + "shop" + ], + "consumes": [ + "multipart/form-data" + ] }, "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], "summary": "创建店铺", - "operationId": "shopCreateShop", + "operationId": "CreateShop", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/ShopProfile" + } + } + }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { - "type": "object", - "required": [ - "name", - "description", - "commissionType", - "commissionValue" - ], - "properties": { - "commissionType": { - "type": "string" - }, - "commissionValue": { - "type": "string" - }, - "description": { - "type": "string" - }, - "name": { - "type": "string" - } - } + "$ref": "#/definitions/CreateShopReq" } } ], + "tags": [ + "shop" + ] + } + }, + "/api/v1/shops/invitations/mine": { + "get": { + "summary": "获取我收到的邀请", + "operationId": "MyInvitations", "responses": { "200": { - "description": "", + "description": "A successful response.", "schema": { - "type": "object", - "properties": { - "announcements": { - "type": "array", - "items": { - "type": "string" - } - }, - "banner": { - "type": "string" - }, - "commissionType": { - "type": "string" - }, - "commissionValue": { - "type": "string" - }, - "description": { - "type": "string" - }, - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "owner": { - "type": "object", - "required": [ - "id", - "username", - "nickname", - "avatar", - "role", - "verifiedRoles", - "verificationStatus", - "createdAt" - ], - "properties": { - "avatar": { - "type": "string" - }, - "bio": { - "type": "string" - }, - "createdAt": { - "type": "string" - }, - "id": { - "type": "string" - }, - "nickname": { - "type": "string" - }, - "phone": { - "type": "string" - }, - "role": { - "description": "consumer, player, owner, admin", - "type": "string" - }, - "username": { - "type": "string" - }, - "verificationStatus": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "verifiedRoles": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "playerCount": { - "type": "integer" - }, - "rating": { - "type": "string" - }, - "templateConfig": {}, - "totalOrders": { - "type": "integer" - } - } + "$ref": "#/definitions/ShopInvitationListResp" } } - } + }, + "tags": [ + "shop" + ] } }, "/api/v1/shops/invitations/{id}": { "delete": { - "consumes": [ - "application/x-www-form-urlencoded" - ], - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], "summary": "拒绝邀请", - "operationId": "shopRejectInvitation", - "parameters": [ - { - "type": "integer", - "name": "id", - "in": "path", - "required": true - } - ], + "operationId": "RejectInvitation", "responses": { "200": { - "description": "", + "description": "A successful response.", "schema": { - "type": "object" + "$ref": "#/definitions/EmptyResp" } } - } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/AcceptInvitationReq" + } + } + ], + "tags": [ + "shop" + ] } }, "/api/v1/shops/invitations/{id}/accept": { "post": { - "consumes": [ - "application/x-www-form-urlencoded" - ], - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], "summary": "接受邀请", - "operationId": "shopAcceptInvitation", - "parameters": [ - { - "type": "integer", - "name": "id", - "in": "path", - "required": true - } - ], + "operationId": "AcceptInvitation", "responses": { "200": { - "description": "", + "description": "A successful response.", "schema": { - "type": "object" + "$ref": "#/definitions/EmptyResp" } } - } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/AcceptInvitationReq" + } + } + ], + "tags": [ + "shop" + ] } }, "/api/v1/shops/mine": { "get": { - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], "summary": "获取当前用户的店铺", - "operationId": "shopGetMyShop", + "operationId": "GetMyShop", "responses": { "200": { - "description": "", + "description": "A successful response.", "schema": { - "type": "object", - "properties": { - "announcements": { - "type": "array", - "items": { - "type": "string" - } - }, - "banner": { - "type": "string" - }, - "commissionType": { - "type": "string" - }, - "commissionValue": { - "type": "string" - }, - "description": { - "type": "string" - }, - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "owner": { - "type": "object", - "required": [ - "id", - "username", - "nickname", - "avatar", - "role", - "verifiedRoles", - "verificationStatus", - "createdAt" - ], - "properties": { - "avatar": { - "type": "string" - }, - "bio": { - "type": "string" - }, - "createdAt": { - "type": "string" - }, - "id": { - "type": "string" - }, - "nickname": { - "type": "string" - }, - "phone": { - "type": "string" - }, - "role": { - "description": "consumer, player, owner, admin", - "type": "string" - }, - "username": { - "type": "string" - }, - "verificationStatus": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "verifiedRoles": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "playerCount": { - "type": "integer" - }, - "rating": { - "type": "string" - }, - "templateConfig": {}, - "totalOrders": { - "type": "integer" - } - } + "$ref": "#/definitions/ShopProfile" } } - } + }, + "tags": [ + "shop" + ] } }, "/api/v1/shops/{id}": { "get": { - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], "summary": "获取店铺详情", - "operationId": "shopGetShop", - "parameters": [ - { - "type": "integer", - "name": "id", - "in": "path", - "required": true - } - ], + "operationId": "GetShop", "responses": { "200": { - "description": "", + "description": "A successful response.", "schema": { - "type": "object", - "properties": { - "announcements": { - "type": "array", - "items": { - "type": "string" - } - }, - "banner": { - "type": "string" - }, - "commissionType": { - "type": "string" - }, - "commissionValue": { - "type": "string" - }, - "description": { - "type": "string" - }, - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "owner": { - "type": "object", - "required": [ - "id", - "username", - "nickname", - "avatar", - "role", - "verifiedRoles", - "verificationStatus", - "createdAt" - ], - "properties": { - "avatar": { - "type": "string" - }, - "bio": { - "type": "string" - }, - "createdAt": { - "type": "string" - }, - "id": { - "type": "string" - }, - "nickname": { - "type": "string" - }, - "phone": { - "type": "string" - }, - "role": { - "description": "consumer, player, owner, admin", - "type": "string" - }, - "username": { - "type": "string" - }, - "verificationStatus": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "verifiedRoles": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "playerCount": { - "type": "integer" - }, - "rating": { - "type": "string" - }, - "templateConfig": {}, - "totalOrders": { - "type": "integer" - } - } + "$ref": "#/definitions/ShopProfile" } } - } - }, - "put": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], - "summary": "更新店铺信息", - "operationId": "shopUpdateShop", + }, "parameters": [ { - "type": "integer", "name": "id", "in": "path", - "required": true + "required": true, + "type": "string" + } + ], + "tags": [ + "shop" + ] + }, + "put": { + "summary": "更新店铺信息", + "operationId": "UpdateShop", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/ShopProfile" + } + } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string" }, { "name": "body", "in": "body", "required": true, "schema": { - "type": "object", - "properties": { - "allowIndependentOrders": { - "type": "boolean" - }, - "allowMultiShop": { - "type": "boolean" - }, - "commissionType": { - "type": "string" - }, - "commissionValue": { - "type": "string" - }, - "description": { - "type": "string" - }, - "dispatchMode": { - "type": "string" - }, - "name": { - "type": "string" - } - } + "$ref": "#/definitions/UpdateShopReq" } } ], - "responses": { - "200": { - "description": "", - "schema": { - "type": "object", - "properties": { - "announcements": { - "type": "array", - "items": { - "type": "string" - } - }, - "banner": { - "type": "string" - }, - "commissionType": { - "type": "string" - }, - "commissionValue": { - "type": "string" - }, - "description": { - "type": "string" - }, - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "owner": { - "type": "object", - "required": [ - "id", - "username", - "nickname", - "avatar", - "role", - "verifiedRoles", - "verificationStatus", - "createdAt" - ], - "properties": { - "avatar": { - "type": "string" - }, - "bio": { - "type": "string" - }, - "createdAt": { - "type": "string" - }, - "id": { - "type": "string" - }, - "nickname": { - "type": "string" - }, - "phone": { - "type": "string" - }, - "role": { - "description": "consumer, player, owner, admin", - "type": "string" - }, - "username": { - "type": "string" - }, - "verificationStatus": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "verifiedRoles": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "playerCount": { - "type": "integer" - }, - "rating": { - "type": "string" - }, - "templateConfig": {}, - "totalOrders": { - "type": "integer" - } - } - } - } - } + "tags": [ + "shop" + ] } }, "/api/v1/shops/{id}/announcements": { "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], "summary": "新增店铺公告", - "operationId": "shopAddAnnouncement", + "operationId": "AddAnnouncement", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/EmptyResp" + } + } + }, "parameters": [ { - "type": "integer", "name": "id", "in": "path", - "required": true + "required": true, + "type": "string" }, { "name": "body", "in": "body", "required": true, "schema": { - "type": "object", - "required": [ - "content" - ], - "properties": { - "content": { - "type": "string" - } - } + "$ref": "#/definitions/AnnouncementReq" } } ], - "responses": { - "200": { - "description": "", - "schema": { - "type": "object" - } - } - } + "tags": [ + "shop" + ] } }, "/api/v1/shops/{id}/announcements/{index}": { "delete": { - "consumes": [ - "application/x-www-form-urlencoded" - ], - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], "summary": "删除店铺公告", - "operationId": "shopDeleteAnnouncement", - "parameters": [ - { - "type": "integer", - "name": "id", - "in": "path", - "required": true - }, - { - "type": "integer", - "name": "index", - "in": "path", - "required": true - } - ], + "operationId": "DeleteAnnouncement", "responses": { "200": { - "description": "", + "description": "A successful response.", "schema": { - "type": "object" + "$ref": "#/definitions/EmptyResp" } } - } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "index", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/DeleteAnnouncementReq" + } + } + ], + "tags": [ + "shop" + ] } }, "/api/v1/shops/{id}/income-stats": { "get": { - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], "summary": "获取收入统计", - "operationId": "shopGetShopIncomeStats", - "parameters": [ - { - "type": "integer", - "name": "id", - "in": "path", - "required": true - } - ], + "operationId": "GetShopIncomeStats", "responses": { "200": { - "description": "", + "description": "A successful response.", "schema": { - "type": "object", - "properties": { - "completedOrders": { - "type": "integer" - }, - "monthlyIncome": { - "type": "string" - }, - "pendingSettlement": { - "type": "string" - }, - "totalOrders": { - "type": "integer" - }, - "totalWithdrawn": { - "type": "string" - } - } + "$ref": "#/definitions/IncomeStatsResp" } } - } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string" + } + ], + "tags": [ + "shop" + ] } }, "/api/v1/shops/{id}/invitations": { - "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], - "summary": "邀请打手", - "operationId": "shopInvitePlayer", + "get": { + "summary": "获取店铺邀请列表", + "operationId": "ListShopInvitations", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/ShopInvitationListResp" + } + } + }, "parameters": [ { - "type": "integer", "name": "id", "in": "path", - "required": true + "required": true, + "type": "string" + } + ], + "tags": [ + "shop" + ] + }, + "post": { + "summary": "邀请打手", + "operationId": "InvitePlayer", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/EmptyResp" + } + } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string" }, { "name": "body", "in": "body", "required": true, "schema": { - "type": "object", - "required": [ - "playerId" - ], - "properties": { - "playerId": { - "type": "integer" - } - } + "$ref": "#/definitions/InvitationReq" } } ], - "responses": { - "200": { - "description": "", - "schema": { - "type": "object" - } - } - } + "tags": [ + "shop" + ] } }, "/api/v1/shops/{id}/players/{playerId}": { "delete": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], "summary": "移除打手", - "operationId": "shopRemovePlayer", + "operationId": "RemovePlayer", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/EmptyResp" + } + } + }, "parameters": [ { - "type": "integer", "name": "id", "in": "path", - "required": true + "required": true, + "type": "string" + }, + { + "name": "playerId", + "in": "path", + "required": true, + "type": "string" }, { "name": "body", "in": "body", "required": true, "schema": { - "type": "object", - "required": [ - "playerId" - ], - "properties": { - "playerId": { - "type": "integer" - } - } + "$ref": "#/definitions/RemovePlayerReq" } } ], - "responses": { - "200": { - "description": "", - "schema": { - "type": "object" - } - } - } + "tags": [ + "shop" + ] } }, "/api/v1/shops/{id}/template": { "put": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], "summary": "更新店铺模板", - "operationId": "shopUpdateShopTemplate", + "operationId": "UpdateShopTemplate", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/EmptyResp" + } + } + }, "parameters": [ { - "type": "integer", "name": "id", "in": "path", - "required": true + "required": true, + "type": "string" }, { "name": "body", "in": "body", "required": true, "schema": { - "type": "object", - "required": [ - "sections" - ], - "properties": { - "sections": { - "type": "string" - } - } + "$ref": "#/definitions/UpdateTemplateReq" } } ], - "responses": { - "200": { - "description": "", - "schema": { - "type": "object" - } - } - } + "tags": [ + "shop" + ] } }, "/api/v1/users/{id}/shop": { "get": { - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], "summary": "获取店长的店铺", - "operationId": "shopGetUserShop", - "parameters": [ - { - "type": "integer", - "name": "id", - "in": "path", - "required": true - } - ], + "operationId": "GetUserShop", "responses": { "200": { - "description": "", + "description": "A successful response.", "schema": { - "type": "object", - "properties": { - "announcements": { - "type": "array", - "items": { - "type": "string" - } - }, - "banner": { - "type": "string" - }, - "commissionType": { - "type": "string" - }, - "commissionValue": { - "type": "string" - }, - "description": { - "type": "string" - }, - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "owner": { - "type": "object", - "required": [ - "id", - "username", - "nickname", - "avatar", - "role", - "verifiedRoles", - "verificationStatus", - "createdAt" - ], - "properties": { - "avatar": { - "type": "string" - }, - "bio": { - "type": "string" - }, - "createdAt": { - "type": "string" - }, - "id": { - "type": "string" - }, - "nickname": { - "type": "string" - }, - "phone": { - "type": "string" - }, - "role": { - "description": "consumer, player, owner, admin", - "type": "string" - }, - "username": { - "type": "string" - }, - "verificationStatus": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "verifiedRoles": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "playerCount": { - "type": "integer" - }, - "rating": { - "type": "string" - }, - "templateConfig": {}, - "totalOrders": { - "type": "integer" - } - } + "$ref": "#/definitions/ShopProfile" } } - } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string" + } + ], + "tags": [ + "shop" + ] } } }, - "x-date": "2026-04-22 22:30:24", - "x-description": "This is a goctl generated swagger file.", - "x-github": "https://github.com/zeromicro/go-zero", - "x-go-zero-doc": "https://go-zero.dev/", - "x-goctl-version": "1.10.1" -} \ No newline at end of file + "definitions": { + "AcceptInvitationReq": { + "type": "object", + "title": "AcceptInvitationReq" + }, + "AnnouncementReq": { + "type": "object", + "properties": { + "content": { + "type": "string" + } + }, + "title": "AnnouncementReq", + "required": [ + "content" + ] + }, + "CreateShopReq": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "commissionType": { + "type": "string" + }, + "commissionValue": { + "type": "string" + } + }, + "title": "CreateShopReq", + "required": [ + "name", + "description", + "commissionType", + "commissionValue" + ] + }, + "DeleteAnnouncementReq": { + "type": "object", + "title": "DeleteAnnouncementReq" + }, + "EmptyResp": { + "type": "object", + "title": "EmptyResp" + }, + "IncomeStatsResp": { + "type": "object", + "properties": { + "monthlyIncome": { + "type": "string" + }, + "pendingSettlement": { + "type": "string" + }, + "totalWithdrawn": { + "type": "string" + }, + "totalOrders": { + "type": "integer", + "format": "int64" + }, + "completedOrders": { + "type": "integer", + "format": "int64" + } + }, + "title": "IncomeStatsResp", + "required": [ + "monthlyIncome", + "pendingSettlement", + "totalWithdrawn", + "totalOrders", + "completedOrders" + ] + }, + "InvitationReq": { + "type": "object", + "properties": { + "playerId": { + "type": "integer", + "format": "int64" + } + }, + "title": "InvitationReq", + "required": [ + "playerId" + ] + }, + "PageMeta": { + "type": "object", + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "offset": { + "type": "integer", + "format": "int64" + }, + "limit": { + "type": "integer", + "format": "int64" + } + }, + "title": "PageMeta", + "required": [ + "total", + "offset", + "limit" + ] + }, + "PageReq": { + "type": "object", + "properties": { + "offset": { + "type": "integer", + "format": "int64", + "default": "0" + }, + "limit": { + "type": "integer", + "format": "int64", + "default": "20" + } + }, + "title": "PageReq", + "required": [ + "offset", + "limit" + ] + }, + "RemovePlayerReq": { + "type": "object", + "title": "RemovePlayerReq" + }, + "ShopIdReq": { + "type": "object", + "title": "ShopIdReq" + }, + "ShopInvitation": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "shopId": { + "type": "integer", + "format": "int64" + }, + "playerId": { + "type": "integer", + "format": "int64" + }, + "status": { + "type": "string" + }, + "invitedBy": { + "type": "integer", + "format": "int64" + }, + "createdAt": { + "type": "integer", + "format": "int64" + }, + "respondedAt": { + "type": "integer", + "format": "int64" + } + }, + "title": "ShopInvitation", + "required": [ + "id", + "shopId", + "playerId", + "status", + "invitedBy", + "createdAt" + ] + }, + "ShopInvitationListResp": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/ShopInvitation" + } + } + }, + "title": "ShopInvitationListResp", + "required": [ + "items" + ] + }, + "ShopListResp": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/ShopProfile" + } + }, + "meta": { + "$ref": "#/definitions/PageMeta" + } + }, + "title": "ShopListResp", + "required": [ + "items", + "meta" + ] + }, + "ShopProfile": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "owner": { + "$ref": "#/definitions/UserProfile" + }, + "name": { + "type": "string" + }, + "banner": { + "type": "string" + }, + "description": { + "type": "string" + }, + "rating": { + "type": "string" + }, + "totalOrders": { + "type": "integer", + "format": "int64" + }, + "playerCount": { + "type": "integer", + "format": "int64" + }, + "commissionType": { + "type": "string" + }, + "commissionValue": { + "type": "string" + }, + "allowMultiShop": { + "type": "boolean", + "format": "boolean" + }, + "allowIndependentOrders": { + "type": "boolean", + "format": "boolean" + }, + "dispatchMode": { + "type": "string" + }, + "announcements": { + "type": "array", + "items": { + "type": "string" + } + }, + "templateConfig": { + "type": "object" + } + }, + "title": "ShopProfile", + "required": [ + "id", + "owner", + "name", + "description", + "rating", + "totalOrders", + "playerCount", + "commissionType", + "commissionValue", + "allowMultiShop", + "allowIndependentOrders", + "dispatchMode", + "announcements", + "templateConfig" + ] + }, + "SimpleUser": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "nickname": { + "type": "string" + }, + "avatar": { + "type": "string" + } + }, + "title": "SimpleUser", + "required": [ + "id", + "nickname", + "avatar" + ] + }, + "UpdateShopReq": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "commissionType": { + "type": "string" + }, + "commissionValue": { + "type": "string" + }, + "allowMultiShop": { + "type": "boolean", + "format": "boolean" + }, + "allowIndependentOrders": { + "type": "boolean", + "format": "boolean" + }, + "dispatchMode": { + "type": "string" + } + }, + "title": "UpdateShopReq" + }, + "UpdateTemplateReq": { + "type": "object", + "properties": { + "sections": { + "type": "string" + } + }, + "title": "UpdateTemplateReq", + "required": [ + "sections" + ] + }, + "UserIdReq": { + "type": "object", + "title": "UserIdReq" + }, + "UserProfile": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "username": { + "type": "string" + }, + "nickname": { + "type": "string" + }, + "avatar": { + "type": "string" + }, + "role": { + "type": "string", + "description": " consumer, player, owner, admin" + }, + "verifiedRoles": { + "type": "array", + "items": { + "type": "string" + } + }, + "verificationStatus": { + "type": "object" + }, + "phone": { + "type": "string" + }, + "bio": { + "type": "string" + }, + "createdAt": { + "type": "string" + } + }, + "title": "UserProfile", + "required": [ + "id", + "username", + "nickname", + "avatar", + "role", + "verifiedRoles", + "verificationStatus", + "createdAt" + ] + } + }, + "securityDefinitions": { + "apiKey": { + "type": "apiKey", + "description": "Enter JWT Bearer token **_only_**", + "name": "Authorization", + "in": "header" + } + } +} diff --git a/desc/api/docs/users-api.json b/desc/api/docs/users-api.json index cb39b0f..90a9b65 100644 --- a/desc/api/docs/users-api.json +++ b/desc/api/docs/users-api.json @@ -1,1076 +1,927 @@ { + "swagger": "2.0", + "info": { + "title": "聚玩用户服务", + "description": "处理用户注册、登录、个人信息管理及关注系统", + "version": "1.0" + }, + "schemes": [ + "http", + "https" + ], "consumes": [ "application/json" ], "produces": [ "application/json" ], - "schemes": [ - "https" - ], - "swagger": "2.0", - "info": { - "title": "聚玩用户服务", - "version": "1.0" - }, - "basePath": "/", "paths": { "/api/v1/admin/verifications": { "get": { - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], "summary": "管理员获取认证申请列表 (分页)", - "operationId": "verificationAdminGetVerifications", - "parameters": [ - { - "type": "integer", - "default": 0, - "example": 0, - "name": "offset", - "in": "query", - "required": true - }, - { - "type": "integer", - "default": 20, - "example": 20, - "name": "limit", - "in": "query", - "required": true - }, - { - "type": "string", - "description": "筛选角色", - "name": "role", - "in": "query", - "allowEmptyValue": true - }, - { - "type": "string", - "description": "筛选状态,默认 pending", - "name": "status", - "in": "query", - "allowEmptyValue": true - } - ], + "operationId": "GetVerifications", "responses": { "200": { - "description": "", + "description": "A successful response.", "schema": { - "type": "object", - "properties": { - "list": { - "type": "array", - "items": { - "type": "object", - "required": [ - "id", - "userId", - "userNickname", - "role", - "status", - "materials", - "rejectReason", - "createdAt", - "reviewedAt" - ], - "properties": { - "createdAt": { - "description": "申请时间", - "type": "string" - }, - "id": { - "description": "认证记录ID (主键,用于管理员操作)", - "type": "integer" - }, - "materials": { - "description": "核心字段:对应 DB 的 JSONB", - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "rejectReason": { - "description": "驳回原因", - "type": "string" - }, - "reviewedAt": { - "description": "审核时间", - "type": "string" - }, - "role": { - "description": "申请角色: player, owner", - "type": "string" - }, - "status": { - "description": "pending, approved, rejected", - "type": "string" - }, - "userId": { - "description": "申请人ID (外键)", - "type": "integer" - }, - "userNickname": { - "description": "冗余显示,方便前端展示", - "type": "string" - } - } - } - }, - "total": { - "type": "integer" - } - } + "$ref": "#/definitions/GetPendingListResp" } } - } + }, + "parameters": [ + { + "name": "offset", + "in": "query", + "required": true, + "type": "integer", + "format": "int64", + "default": "0" + }, + { + "name": "limit", + "in": "query", + "required": true, + "type": "integer", + "format": "int64", + "default": "20" + }, + { + "name": "role", + "description": " 筛选角色", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "status", + "description": " 筛选状态,默认 pending", + "in": "query", + "required": false, + "type": "string" + } + ], + "tags": [ + "verification_admin" + ], + "consumes": [ + "multipart/form-data" + ] } }, "/api/v1/admin/verifications/{id}/approve": { "post": { - "consumes": [ - "application/x-www-form-urlencoded" - ], - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], "summary": "管理员通过申请", - "operationId": "verificationAdminApproveVerification", - "parameters": [ - { - "type": "integer", - "description": "注意:这是 user_verifications.id", - "name": "id", - "in": "path", - "required": true - } - ], + "operationId": "ApproveVerification", "responses": { "200": { - "description": "", + "description": "A successful response.", "schema": { - "type": "object" + "$ref": "#/definitions/VerificationEmptyResp" } } - } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "body", + "description": " 管理员:审核操作请求 (只包含 ID 路径参数)", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/VerificationIdReq" + } + } + ], + "tags": [ + "verification_admin" + ] } }, "/api/v1/admin/verifications/{id}/reject": { "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], "summary": "管理员驳回申请", - "operationId": "verificationAdminRejectVerification", + "operationId": "RejectVerification", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/VerificationEmptyResp" + } + } + }, "parameters": [ { - "type": "integer", "name": "id", "in": "path", - "required": true + "required": true, + "type": "string" + }, + { + "name": "body", + "description": " 管理员:驳回请求", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/RejectVerificationReq" + } + } + ], + "tags": [ + "verification_admin" + ] + } + }, + "/api/v1/auth/login": { + "post": { + "summary": "用户登录", + "operationId": "Login", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/LoginResp" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/LoginReq" + } + } + ], + "tags": [ + "auth" + ] + } + }, + "/api/v1/auth/logout": { + "post": { + "summary": "退出登录", + "operationId": "Logout", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/EmptyResp" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/LogoutReq" + } + } + ], + "tags": [ + "auth" + ] + } + }, + "/api/v1/auth/register": { + "post": { + "summary": "用户注册", + "operationId": "Register", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/RegisterResp" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/RegisterReq" + } + } + ], + "tags": [ + "auth" + ] + } + }, + "/api/v1/auth/reset-password": { + "post": { + "summary": "重置密码", + "operationId": "ResetPassword", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/EmptyResp" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ResetPasswordReq" + } + } + ], + "tags": [ + "auth" + ] + } + }, + "/api/v1/users/me": { + "get": { + "summary": "获取当前登录用户信息", + "operationId": "GetMe", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/User" + } + } + }, + "tags": [ + "user" + ] + }, + "put": { + "summary": "更新个人资料", + "operationId": "UpdateMe", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/User" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/UpdateUserProfileReq" + } + } + ], + "tags": [ + "user" + ] + } + }, + "/api/v1/users/me/preferences/notifications": { + "put": { + "summary": "更新通知偏好", + "operationId": "UpdateNotificationSettings", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/EmptyResp" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/UpdateNotifySettingsReq" + } + } + ], + "tags": [ + "user" + ] + } + }, + "/api/v1/users/me/preferences/theme": { + "put": { + "summary": "更新主题偏好", + "operationId": "UpdateThemeSettings", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/EmptyResp" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/UpdateThemeSettingsReq" + } + } + ], + "tags": [ + "user" + ] + } + }, + "/api/v1/users/me/switch-role": { + "post": { + "summary": "切换当前激活角色", + "operationId": "SwitchRole", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/EmptyResp" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/SwitchRoleReq" + } + } + ], + "tags": [ + "user" + ] + } + }, + "/api/v1/users/me/verification": { + "get": { + "summary": "获取我的所有认证状态", + "operationId": "GetMyVerifications", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/GetMyVerificationsResp" + } + } + }, + "tags": [ + "verification_user" + ] + }, + "post": { + "summary": "提交或修改角色认证申请 (支持幂等更新)", + "operationId": "ApplyVerification", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/VerificationEmptyResp" + } + } + }, + "parameters": [ + { + "name": "body", + "description": " 提交申请请求", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/ApplyVerificationReq" + } + } + ], + "tags": [ + "verification_user" + ] + } + }, + "/api/v1/users/{id}": { + "get": { + "summary": "获取指定用户信息", + "operationId": "GetUserInfo", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/User" + } + } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string" + } + ], + "tags": [ + "user" + ] + } + }, + "/api/v1/users/{id}/follow": { + "delete": { + "summary": "取消关注用户", + "operationId": "UnfollowUser", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/EmptyResp" + } + } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string" }, { "name": "body", "in": "body", "required": true, "schema": { - "type": "object", - "required": [ - "reason" - ], - "properties": { - "reason": { - "description": "必填:驳回原因", - "type": "string" - } - } + "$ref": "#/definitions/UnfollowUserReq" } } ], - "responses": { - "200": { - "description": "", - "schema": { - "type": "object" - } - } - } - } - }, - "/api/v1/auth/login": { - "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], - "summary": "用户登录", - "operationId": "authLogin", - "parameters": [ - { - "name": "body", - "in": "body", - "required": true, - "schema": { - "type": "object", - "required": [ - "username", - "password" - ], - "properties": { - "password": { - "type": "string" - }, - "phone": { - "description": "手机号登录", - "type": "string" - }, - "remember": { - "type": "boolean" - }, - "username": { - "description": "或用户名登录", - "type": "string" - } - } - } - } - ], - "responses": { - "200": { - "description": "", - "schema": { - "type": "object", - "properties": { - "user": { - "type": "object", - "required": [ - "id", - "username", - "nickname", - "avatar", - "role", - "verifiedRoles", - "verificationStatus", - "phone", - "bio", - "createdAt" - ], - "properties": { - "avatar": { - "type": "string" - }, - "bio": { - "type": "string" - }, - "createdAt": { - "description": "ISO 8601", - "type": "string" - }, - "id": { - "type": "string" - }, - "nickname": { - "type": "string" - }, - "phone": { - "type": "string" - }, - "role": { - "description": "consumer, player, owner, admin", - "type": "string" - }, - "username": { - "type": "string" - }, - "verificationStatus": { - "description": "e.g. {\"player\": \"approved\"}", - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "verifiedRoles": { - "description": "e.g. [\"consumer\", \"player\"]", - "type": "array", - "items": { - "type": "string" - } - } - } - } - } - } - } - } - } - }, - "/api/v1/auth/logout": { - "post": { - "consumes": [ - "application/x-www-form-urlencoded" - ], - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], - "summary": "退出登录", - "operationId": "authLogout", - "responses": { - "200": { - "description": "", - "schema": { - "type": "object" - } - } - } - } - }, - "/api/v1/auth/register": { - "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], - "summary": "用户注册", - "operationId": "authRegister", - "parameters": [ - { - "name": "body", - "in": "body", - "required": true, - "schema": { - "type": "object", - "required": [ - "email", - "username", - "password", - "vcode" - ], - "properties": { - "email": { - "type": "string" - }, - "password": { - "type": "string" - }, - "phone": { - "type": "string" - }, - "username": { - "type": "string" - }, - "vcode": { - "description": "验证码", - "type": "string" - } - } - } - } - ], - "responses": { - "200": { - "description": "", - "schema": { - "type": "object", - "properties": { - "user": { - "type": "object", - "required": [ - "id", - "username", - "nickname", - "avatar", - "role", - "verifiedRoles", - "verificationStatus", - "phone", - "bio", - "createdAt" - ], - "properties": { - "avatar": { - "type": "string" - }, - "bio": { - "type": "string" - }, - "createdAt": { - "description": "ISO 8601", - "type": "string" - }, - "id": { - "type": "string" - }, - "nickname": { - "type": "string" - }, - "phone": { - "type": "string" - }, - "role": { - "description": "consumer, player, owner, admin", - "type": "string" - }, - "username": { - "type": "string" - }, - "verificationStatus": { - "description": "e.g. {\"player\": \"approved\"}", - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "verifiedRoles": { - "description": "e.g. [\"consumer\", \"player\"]", - "type": "array", - "items": { - "type": "string" - } - } - } - } - } - } - } - } - } - }, - "/api/v1/auth/reset-password": { - "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], - "summary": "重置密码", - "operationId": "authResetPassword", - "parameters": [ - { - "name": "body", - "in": "body", - "required": true, - "schema": { - "type": "object", - "required": [ - "email", - "vcode", - "newPassword" - ], - "properties": { - "email": { - "type": "string" - }, - "newPassword": { - "type": "string" - }, - "phone": { - "type": "string" - }, - "vcode": { - "type": "string" - } - } - } - } - ], - "responses": { - "200": { - "description": "", - "schema": { - "type": "object" - } - } - } - } - }, - "/api/v1/users/me": { - "get": { - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], - "summary": "获取当前登录用户信息", - "operationId": "userGetMe", - "responses": { - "200": { - "description": "", - "schema": { - "type": "object", - "properties": { - "avatar": { - "type": "string" - }, - "bio": { - "type": "string" - }, - "createdAt": { - "description": "ISO 8601", - "type": "string" - }, - "id": { - "type": "string" - }, - "nickname": { - "type": "string" - }, - "phone": { - "type": "string" - }, - "role": { - "description": "consumer, player, owner, admin", - "type": "string" - }, - "username": { - "type": "string" - }, - "verificationStatus": { - "description": "e.g. {\"player\": \"approved\"}", - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "verifiedRoles": { - "description": "e.g. [\"consumer\", \"player\"]", - "type": "array", - "items": { - "type": "string" - } - } - } - } - } - } - }, - "put": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], - "summary": "更新个人资料", - "operationId": "userUpdateMe", - "parameters": [ - { - "name": "body", - "in": "body", - "required": true, - "schema": { - "type": "object", - "properties": { - "avatar": { - "type": "string" - }, - "bio": { - "type": "string" - }, - "nickname": { - "type": "string" - } - } - } - } - ], - "responses": { - "200": { - "description": "", - "schema": { - "type": "object", - "properties": { - "avatar": { - "type": "string" - }, - "bio": { - "type": "string" - }, - "createdAt": { - "description": "ISO 8601", - "type": "string" - }, - "id": { - "type": "string" - }, - "nickname": { - "type": "string" - }, - "phone": { - "type": "string" - }, - "role": { - "description": "consumer, player, owner, admin", - "type": "string" - }, - "username": { - "type": "string" - }, - "verificationStatus": { - "description": "e.g. {\"player\": \"approved\"}", - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "verifiedRoles": { - "description": "e.g. [\"consumer\", \"player\"]", - "type": "array", - "items": { - "type": "string" - } - } - } - } - } - } - } - }, - "/api/v1/users/me/preferences/notifications": { - "put": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], - "summary": "更新通知偏好", - "operationId": "userUpdateNotificationSettings", - "parameters": [ - { - "name": "body", - "in": "body", - "required": true, - "schema": { - "type": "object", - "properties": { - "community": { - "type": "boolean" - }, - "order": { - "type": "boolean" - }, - "system": { - "type": "boolean" - } - } - } - } - ], - "responses": { - "200": { - "description": "", - "schema": { - "type": "object" - } - } - } - } - }, - "/api/v1/users/me/preferences/theme": { - "put": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], - "summary": "更新主题偏好", - "operationId": "userUpdateThemeSettings", - "parameters": [ - { - "name": "body", - "in": "body", - "required": true, - "schema": { - "type": "object", - "required": [ - "theme" - ], - "properties": { - "theme": { - "description": "dark, light", - "type": "string" - } - } - } - } - ], - "responses": { - "200": { - "description": "", - "schema": { - "type": "object" - } - } - } - } - }, - "/api/v1/users/me/switch-role": { - "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], - "summary": "切换当前激活角色", - "operationId": "userSwitchRole", - "parameters": [ - { - "name": "body", - "in": "body", - "required": true, - "schema": { - "type": "object", - "required": [ - "role" - ], - "properties": { - "role": { - "description": "目标角色", - "type": "string" - } - } - } - } - ], - "responses": { - "200": { - "description": "", - "schema": { - "type": "object" - } - } - } - } - }, - "/api/v1/users/me/verification": { - "get": { - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], - "summary": "获取我的所有认证状态", - "operationId": "verificationUserGetMyVerifications", - "responses": { - "200": { - "description": "", - "schema": { - "type": "object", - "properties": { - "list": { - "type": "array", - "items": { - "type": "object", - "required": [ - "id", - "userId", - "userNickname", - "role", - "status", - "materials", - "rejectReason", - "createdAt", - "reviewedAt" - ], - "properties": { - "createdAt": { - "description": "申请时间", - "type": "string" - }, - "id": { - "description": "认证记录ID (主键,用于管理员操作)", - "type": "integer" - }, - "materials": { - "description": "核心字段:对应 DB 的 JSONB", - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "rejectReason": { - "description": "驳回原因", - "type": "string" - }, - "reviewedAt": { - "description": "审核时间", - "type": "string" - }, - "role": { - "description": "申请角色: player, owner", - "type": "string" - }, - "status": { - "description": "pending, approved, rejected", - "type": "string" - }, - "userId": { - "description": "申请人ID (外键)", - "type": "integer" - }, - "userNickname": { - "description": "冗余显示,方便前端展示", - "type": "string" - } - } - } - } - } - } - } - } + "tags": [ + "user" + ] }, "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], - "summary": "提交或修改角色认证申请 (支持幂等更新)", - "operationId": "verificationUserApplyVerification", - "parameters": [ - { - "name": "body", - "in": "body", - "required": true, - "schema": { - "type": "object", - "required": [ - "role", - "materials" - ], - "properties": { - "materials": { - "description": "证明材料键值对 {\"idCardFront\": \"http...\", \"license\": \"http...\"}", - "type": "object", - "required": [ - "idCardFront", - "idCardBack" - ], - "properties": { - "gameScreenshots": { - "description": "游戏截图URL列表", - "type": "array", - "items": { - "type": "string" - } - }, - "idCardBack": { - "description": "身份证反面照片URL", - "type": "string" - }, - "idCardFront": { - "description": "身份证正面照片URL", - "type": "string" - }, - "voiceDemo": { - "description": "语音认证示例URL", - "type": "string" - } - } - }, - "role": { - "description": "申请什么角色", - "type": "string" - } - } - } - } - ], - "responses": { - "200": { - "description": "", - "schema": { - "type": "object" - } - } - } - } - }, - "/api/v1/users/{id}": { - "get": { - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], - "summary": "获取指定用户信息", - "operationId": "userGetUserInfo", - "parameters": [ - { - "type": "string", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "", - "schema": { - "type": "object", - "properties": { - "avatar": { - "type": "string" - }, - "bio": { - "type": "string" - }, - "createdAt": { - "description": "ISO 8601", - "type": "string" - }, - "id": { - "type": "string" - }, - "nickname": { - "type": "string" - }, - "phone": { - "type": "string" - }, - "role": { - "description": "consumer, player, owner, admin", - "type": "string" - }, - "username": { - "type": "string" - }, - "verificationStatus": { - "description": "e.g. {\"player\": \"approved\"}", - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "verifiedRoles": { - "description": "e.g. [\"consumer\", \"player\"]", - "type": "array", - "items": { - "type": "string" - } - } - } - } - } - } - } - }, - "/api/v1/users/{id}/follow": { - "post": { - "consumes": [ - "application/x-www-form-urlencoded" - ], - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], "summary": "关注用户", - "operationId": "userFollowUser", - "parameters": [ - { - "type": "string", - "name": "id", - "in": "path", - "required": true - } - ], + "operationId": "FollowUser", "responses": { "200": { - "description": "", + "description": "A successful response.", "schema": { - "type": "object" + "$ref": "#/definitions/EmptyResp" } } - } - }, - "delete": { - "consumes": [ - "application/x-www-form-urlencoded" - ], - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], - "summary": "取消关注用户", - "operationId": "userUnfollowUser", + }, "parameters": [ { - "type": "string", "name": "id", "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, "schema": { - "type": "object" + "$ref": "#/definitions/FollowUserReq" } } - } + ], + "tags": [ + "user" + ] } } }, - "x-date": "2026-04-22 22:30:22", - "x-description": "This is a goctl generated swagger file.", - "x-github": "https://github.com/zeromicro/go-zero", - "x-go-zero-doc": "https://go-zero.dev/", - "x-goctl-version": "1.10.1" + "definitions": { + "ApplyVerificationReq": { + "type": "object", + "properties": { + "role": { + "type": "string", + "description": " 申请什么角色" + }, + "materials": { + "$ref": "#/definitions/MaterialJson", + "description": " 证明材料键值对 {\"idCardFront\": \"http...\", \"license\": \"http...\"}" + } + }, + "title": "ApplyVerificationReq", + "required": [ + "role", + "materials" + ] + }, + "EmptyResp": { + "type": "object", + "title": "EmptyResp" + }, + "FollowUserReq": { + "type": "object", + "title": "FollowUserReq" + }, + "GetMyVerificationsResp": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/VerificationItem" + } + } + }, + "title": "GetMyVerificationsResp", + "required": [ + "list" + ] + }, + "GetPendingListReq": { + "type": "object", + "properties": { + "offset": { + "type": "integer", + "format": "int64", + "default": "0" + }, + "limit": { + "type": "integer", + "format": "int64", + "default": "20" + }, + "role": { + "type": "string", + "description": " 筛选角色" + }, + "status": { + "type": "string", + "description": " 筛选状态,默认 pending" + } + }, + "title": "GetPendingListReq", + "required": [ + "offset", + "limit" + ] + }, + "GetPendingListResp": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/VerificationItem" + } + }, + "total": { + "type": "integer", + "format": "int64" + } + }, + "title": "GetPendingListResp", + "required": [ + "list", + "total" + ] + }, + "GetUserReq": { + "type": "object", + "title": "GetUserReq" + }, + "LoginReq": { + "type": "object", + "properties": { + "phone": { + "type": "string", + "description": " 手机号登录" + }, + "username": { + "type": "string", + "description": " 或用户名登录" + }, + "password": { + "type": "string" + }, + "remember": { + "type": "boolean", + "format": "boolean" + } + }, + "title": "LoginReq", + "required": [ + "password" + ] + }, + "LoginResp": { + "type": "object", + "properties": { + "user": { + "$ref": "#/definitions/User" + } + }, + "title": "LoginResp", + "required": [ + "user" + ] + }, + "LogoutReq": { + "type": "object", + "title": "LogoutReq" + }, + "MaterialJson": { + "type": "object", + "properties": { + "idCardFront": { + "type": "string", + "description": " 身份证正面照片URL" + }, + "idCardBack": { + "type": "string", + "description": " 身份证反面照片URL" + }, + "gameScreenshots": { + "type": "array", + "items": { + "$ref": "#/definitions/string" + }, + "description": " 游戏截图URL列表" + }, + "voiceDemo": { + "type": "string", + "description": " 语音认证示例URL" + } + }, + "title": "MaterialJson", + "required": [ + "idCardFront", + "idCardBack" + ] + }, + "RegisterReq": { + "type": "object", + "properties": { + "phone": { + "type": "string" + }, + "email": { + "type": "string" + }, + "username": { + "type": "string" + }, + "password": { + "type": "string" + }, + "vcode": { + "type": "string", + "description": " 验证码" + } + }, + "title": "RegisterReq", + "required": [ + "username", + "password" + ] + }, + "RegisterResp": { + "type": "object", + "properties": { + "user": { + "$ref": "#/definitions/User" + } + }, + "title": "RegisterResp", + "required": [ + "user" + ] + }, + "RejectVerificationReq": { + "type": "object", + "properties": { + "reason": { + "type": "string", + "description": " 必填:驳回原因" + } + }, + "title": "RejectVerificationReq", + "required": [ + "reason" + ] + }, + "ResetPasswordReq": { + "type": "object", + "properties": { + "phone": { + "type": "string" + }, + "email": { + "type": "string" + }, + "vcode": { + "type": "string" + }, + "newPassword": { + "type": "string" + } + }, + "title": "ResetPasswordReq", + "required": [ + "vcode", + "newPassword" + ] + }, + "SwitchRoleReq": { + "type": "object", + "properties": { + "role": { + "type": "string", + "description": " 目标角色" + } + }, + "title": "SwitchRoleReq", + "required": [ + "role" + ] + }, + "UnfollowUserReq": { + "type": "object", + "title": "UnfollowUserReq" + }, + "UpdateNotifySettingsReq": { + "type": "object", + "properties": { + "order": { + "type": "boolean", + "format": "boolean" + }, + "community": { + "type": "boolean", + "format": "boolean" + }, + "system": { + "type": "boolean", + "format": "boolean" + } + }, + "title": "UpdateNotifySettingsReq" + }, + "UpdateThemeSettingsReq": { + "type": "object", + "properties": { + "theme": { + "type": "string", + "description": " dark, light" + } + }, + "title": "UpdateThemeSettingsReq", + "required": [ + "theme" + ] + }, + "UpdateUserProfileReq": { + "type": "object", + "properties": { + "nickname": { + "type": "string" + }, + "avatar": { + "type": "string" + }, + "bio": { + "type": "string" + } + }, + "title": "UpdateUserProfileReq" + }, + "User": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "username": { + "type": "string" + }, + "nickname": { + "type": "string" + }, + "avatar": { + "type": "string" + }, + "role": { + "type": "string", + "description": " consumer, player, owner, admin" + }, + "verifiedRoles": { + "type": "array", + "items": { + "type": "string" + }, + "description": " e.g. [\"consumer\", \"player\"]" + }, + "verificationStatus": { + "type": "object", + "description": " e.g. {\"player\": \"approved\"}" + }, + "phone": { + "type": "string" + }, + "bio": { + "type": "string" + }, + "createdAt": { + "type": "string", + "description": " ISO 8601" + } + }, + "title": "User", + "required": [ + "id", + "username", + "nickname", + "avatar", + "role", + "verifiedRoles", + "verificationStatus", + "createdAt" + ] + }, + "VerificationEmptyResp": { + "type": "object", + "title": "VerificationEmptyResp" + }, + "VerificationIdReq": { + "type": "object", + "title": "VerificationIdReq" + }, + "VerificationItem": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "description": " 认证记录ID (主键,用于管理员操作)" + }, + "userId": { + "type": "integer", + "format": "int64", + "description": " 申请人ID (外键)" + }, + "userNickname": { + "type": "string", + "description": " 冗余显示,方便前端展示" + }, + "role": { + "type": "string", + "description": " 申请角色: player, owner" + }, + "status": { + "type": "string", + "description": " pending, approved, rejected" + }, + "materials": { + "type": "object", + "description": " 核心字段:对应 DB 的 JSONB" + }, + "rejectReason": { + "type": "string", + "description": " 驳回原因" + }, + "createdAt": { + "type": "string", + "description": " 申请时间" + }, + "reviewedAt": { + "type": "string", + "description": " 审核时间" + } + }, + "title": "VerificationItem", + "required": [ + "id", + "userId", + "userNickname", + "role", + "status", + "materials", + "rejectReason", + "createdAt", + "reviewedAt" + ] + } + }, + "securityDefinitions": { + "apiKey": { + "type": "apiKey", + "description": "Enter JWT Bearer token **_only_**", + "name": "Authorization", + "in": "header" + } + } } diff --git a/desc/api/docs/wallet-api.json b/desc/api/docs/wallet-api.json index aba34f0..9372c1f 100644 --- a/desc/api/docs/wallet-api.json +++ b/desc/api/docs/wallet-api.json @@ -1,234 +1,339 @@ { + "swagger": "2.0", + "info": { + "title": "钱包服务", + "description": "处理钱包充值相关", + "version": "1.0" + }, + "schemes": [ + "http", + "https" + ], "consumes": [ "application/json" ], "produces": [ "application/json" ], - "schemes": [ - "https" - ], - "swagger": "2.0", - "info": { - "title": "钱包服务", - "version": "1.0" - }, - "basePath": "/", "paths": { "/api/v1/wallet/balance": { "get": { - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], "summary": "获取余额", - "operationId": "walletGetBalance", + "operationId": "GetBalance", "responses": { "200": { - "description": "", + "description": "A successful response.", "schema": { - "type": "object", - "properties": { - "balance": { - "type": "string" - }, - "frozenBalance": { - "type": "string" - } - } + "$ref": "#/definitions/WalletBalance" } } - } + }, + "tags": [ + "wallet" + ] } }, "/api/v1/wallet/topup": { "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], "summary": "充值", - "operationId": "walletTopup", + "operationId": "Topup", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/EmptyResp" + } + } + }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { - "type": "object", - "required": [ - "amount", - "method" - ], - "properties": { - "amount": { - "type": "string" - }, - "method": { - "type": "string" - } - } + "$ref": "#/definitions/TopupReq" } } ], - "responses": { - "200": { - "description": "", - "schema": { - "type": "object" - } - } - } + "tags": [ + "wallet" + ] } }, "/api/v1/wallet/transactions": { "get": { - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], "summary": "获取流水", - "operationId": "walletListTransactions", - "parameters": [ - { - "type": "integer", - "default": 0, - "example": 0, - "name": "offset", - "in": "query", - "required": true - }, - { - "type": "integer", - "default": 20, - "example": 20, - "name": "limit", - "in": "query", - "required": true - } - ], + "operationId": "ListTransactions", "responses": { "200": { - "description": "", + "description": "A successful response.", "schema": { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "type": "object", - "required": [ - "id", - "type", - "amount", - "description", - "createdAt" - ], - "properties": { - "amount": { - "type": "string" - }, - "createdAt": { - "type": "string" - }, - "description": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "orderId": { - "type": "string" - }, - "type": { - "type": "string" - } - } - } - }, - "meta": { - "type": "object", - "required": [ - "total", - "offset", - "limit" - ], - "properties": { - "limit": { - "type": "integer" - }, - "offset": { - "type": "integer" - }, - "total": { - "type": "integer" - } - } - } - } + "$ref": "#/definitions/TransactionListResp" } } - } + }, + "parameters": [ + { + "name": "offset", + "in": "query", + "required": true, + "type": "integer", + "format": "int64", + "default": "0" + }, + { + "name": "limit", + "in": "query", + "required": true, + "type": "integer", + "format": "int64", + "default": "20" + } + ], + "tags": [ + "wallet" + ], + "consumes": [ + "multipart/form-data" + ] } }, "/api/v1/wallet/withdraw": { "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "schemes": [ - "https" - ], "summary": "提现", - "operationId": "walletWithdraw", + "operationId": "Withdraw", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/EmptyResp" + } + } + }, "parameters": [ { "name": "body", "in": "body", "required": true, "schema": { - "type": "object", - "required": [ - "amount", - "method" - ], - "properties": { - "amount": { - "type": "string" - }, - "method": { - "type": "string" - } - } + "$ref": "#/definitions/TopupReq" } } ], - "responses": { - "200": { - "description": "", - "schema": { - "type": "object" - } - } - } + "tags": [ + "wallet" + ] } } }, - "x-date": "2026-04-22 22:30:25", - "x-description": "This is a goctl generated swagger file.", - "x-github": "https://github.com/zeromicro/go-zero", - "x-go-zero-doc": "https://go-zero.dev/", - "x-goctl-version": "1.10.1" -} \ No newline at end of file + "definitions": { + "EmptyResp": { + "type": "object", + "title": "EmptyResp" + }, + "PageMeta": { + "type": "object", + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "offset": { + "type": "integer", + "format": "int64" + }, + "limit": { + "type": "integer", + "format": "int64" + } + }, + "title": "PageMeta", + "required": [ + "total", + "offset", + "limit" + ] + }, + "PageReq": { + "type": "object", + "properties": { + "offset": { + "type": "integer", + "format": "int64", + "default": "0" + }, + "limit": { + "type": "integer", + "format": "int64", + "default": "20" + } + }, + "title": "PageReq", + "required": [ + "offset", + "limit" + ] + }, + "SimpleUser": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "nickname": { + "type": "string" + }, + "avatar": { + "type": "string" + } + }, + "title": "SimpleUser", + "required": [ + "id", + "nickname", + "avatar" + ] + }, + "TopupReq": { + "type": "object", + "properties": { + "amount": { + "type": "string" + }, + "method": { + "type": "string" + } + }, + "title": "TopupReq", + "required": [ + "amount", + "method" + ] + }, + "Transaction": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "type": { + "type": "string" + }, + "amount": { + "type": "string" + }, + "description": { + "type": "string" + }, + "orderId": { + "type": "string" + }, + "createdAt": { + "type": "string" + } + }, + "title": "Transaction", + "required": [ + "id", + "type", + "amount", + "description", + "createdAt" + ] + }, + "TransactionListResp": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/Transaction" + } + }, + "meta": { + "$ref": "#/definitions/PageMeta" + } + }, + "title": "TransactionListResp", + "required": [ + "items", + "meta" + ] + }, + "UserProfile": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "username": { + "type": "string" + }, + "nickname": { + "type": "string" + }, + "avatar": { + "type": "string" + }, + "role": { + "type": "string", + "description": " consumer, player, owner, admin" + }, + "verifiedRoles": { + "type": "array", + "items": { + "type": "string" + } + }, + "verificationStatus": { + "type": "object" + }, + "phone": { + "type": "string" + }, + "bio": { + "type": "string" + }, + "createdAt": { + "type": "string" + } + }, + "title": "UserProfile", + "required": [ + "id", + "username", + "nickname", + "avatar", + "role", + "verifiedRoles", + "verificationStatus", + "createdAt" + ] + }, + "WalletBalance": { + "type": "object", + "properties": { + "balance": { + "type": "string" + }, + "frozenBalance": { + "type": "string" + } + }, + "title": "WalletBalance", + "required": [ + "balance", + "frozenBalance" + ] + } + }, + "securityDefinitions": { + "apiKey": { + "type": "apiKey", + "description": "Enter JWT Bearer token **_only_**", + "name": "Authorization", + "in": "header" + } + } +} From 96541470547dea69f67c4d9f45aa0afa039d6e93 Mon Sep 17 00:00:00 2001 From: zetaloop Date: Sun, 3 May 2026 19:03:56 +0800 Subject: [PATCH 5/6] =?UTF-8?q?chore:=20=E6=9B=B4=E6=96=B0=E5=89=8D?= =?UTF-8?q?=E7=AB=AF=E4=BB=93=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend b/frontend index 4f87834..7b191c5 160000 --- a/frontend +++ b/frontend @@ -1 +1 @@ -Subproject commit 4f878340e60e5cabeed135abce77845934180df2 +Subproject commit 7b191c5d6e66a649d3f69b3dc3a12a2886985aa9 From a3518d20f1d3b08d3ff01add949417652c985c1d Mon Sep 17 00:00:00 2001 From: zetaloop Date: Sun, 3 May 2026 20:58:28 +0800 Subject: [PATCH 6/6] =?UTF-8?q?docs:=20=E5=9C=A8=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E6=96=87=E6=A1=A3=E8=A1=A5=E5=85=A8=20title=20=E4=B8=8E=20desc?= =?UTF-8?q?=20=E5=B9=B6=E6=A0=87=E6=B3=A8=20ID=20=E5=BA=8F=E5=88=97?= =?UTF-8?q?=E5=8C=96=E5=BD=A2=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- desc/api/community.api | 7 +++++++ desc/api/dispute.api | 7 +++++++ desc/api/docs/community-api.json | 5 +++-- desc/api/docs/dispute-api.json | 5 +++-- desc/api/docs/email-api.json | 3 ++- desc/api/docs/game-api.json | 5 +++-- desc/api/docs/notification-api.json | 5 +++-- desc/api/docs/order-api.json | 2 +- desc/api/docs/player-api.json | 2 +- desc/api/docs/review-api.json | 5 +++-- desc/api/docs/search-api.json | 5 +++-- desc/api/docs/shop-api.json | 5 +++-- desc/api/docs/users-api.json | 2 +- desc/api/docs/wallet-api.json | 2 +- desc/api/email.api | 2 ++ desc/api/game.api | 7 +++++++ desc/api/notification.api | 7 +++++++ desc/api/order.api | 2 +- desc/api/player.api | 2 +- desc/api/review.api | 7 +++++++ desc/api/search.api | 7 +++++++ desc/api/shop.api | 7 +++++++ desc/api/users.api | 2 +- desc/api/wallet.api | 2 +- 24 files changed, 82 insertions(+), 23 deletions(-) diff --git a/desc/api/community.api b/desc/api/community.api index b8018db..31ffbe1 100644 --- a/desc/api/community.api +++ b/desc/api/community.api @@ -2,6 +2,13 @@ syntax = "v1" import "common.api" +info ( + title: "聚玩社区服务" + desc: "处理帖子、评论、点赞等社区互动。ID 字段(int64)以 string 传输" + author: "Asadz" + version: "1.0" +) + type ( PathId { Id int64 `path:"id"` diff --git a/desc/api/dispute.api b/desc/api/dispute.api index d17420e..2f931e5 100644 --- a/desc/api/dispute.api +++ b/desc/api/dispute.api @@ -2,6 +2,13 @@ syntax = "v1" import "common.api" +info ( + title: "聚玩争议服务" + desc: "处理订单争议申诉与仲裁。ID 字段(int64)以 string 传输" + author: "Asadz" + version: "1.0" +) + type ( DisputePathId { Id int64 `path:"id"` diff --git a/desc/api/docs/community-api.json b/desc/api/docs/community-api.json index 8ace6fa..21eba89 100644 --- a/desc/api/docs/community-api.json +++ b/desc/api/docs/community-api.json @@ -1,8 +1,9 @@ { "swagger": "2.0", "info": { - "title": "", - "version": "" + "title": "聚玩社区服务", + "description": "处理帖子、评论、点赞等社区互动。ID 字段(int64)以 string 传输", + "version": "1.0" }, "schemes": [ "http", diff --git a/desc/api/docs/dispute-api.json b/desc/api/docs/dispute-api.json index 0f9caf6..c96d8be 100644 --- a/desc/api/docs/dispute-api.json +++ b/desc/api/docs/dispute-api.json @@ -1,8 +1,9 @@ { "swagger": "2.0", "info": { - "title": "", - "version": "" + "title": "聚玩争议服务", + "description": "处理订单争议申诉与仲裁。ID 字段(int64)以 string 传输", + "version": "1.0" }, "schemes": [ "http", diff --git a/desc/api/docs/email-api.json b/desc/api/docs/email-api.json index e6f8d05..ce8eb8e 100644 --- a/desc/api/docs/email-api.json +++ b/desc/api/docs/email-api.json @@ -1,7 +1,8 @@ { "swagger": "2.0", "info": { - "title": "", + "title": "聚玩邮件服务", + "description": "处理邮件验证码发送与密码找回", "version": "1.0" }, "schemes": [ diff --git a/desc/api/docs/game-api.json b/desc/api/docs/game-api.json index b1f7168..efc8e3f 100644 --- a/desc/api/docs/game-api.json +++ b/desc/api/docs/game-api.json @@ -1,8 +1,9 @@ { "swagger": "2.0", "info": { - "title": "", - "version": "" + "title": "聚玩游戏服务", + "description": "管理游戏目录与分类。ID 字段(int64)以 string 传输", + "version": "1.0" }, "schemes": [ "http", diff --git a/desc/api/docs/notification-api.json b/desc/api/docs/notification-api.json index 80b3312..3ba986c 100644 --- a/desc/api/docs/notification-api.json +++ b/desc/api/docs/notification-api.json @@ -1,8 +1,9 @@ { "swagger": "2.0", "info": { - "title": "", - "version": "" + "title": "聚玩通知服务", + "description": "管理站内消息通知。ID 字段(int64)以 string 传输", + "version": "1.0" }, "schemes": [ "http", diff --git a/desc/api/docs/order-api.json b/desc/api/docs/order-api.json index afe447f..a8e8831 100644 --- a/desc/api/docs/order-api.json +++ b/desc/api/docs/order-api.json @@ -2,7 +2,7 @@ "swagger": "2.0", "info": { "title": "聚玩订单服务", - "description": "处理订单业务", + "description": "处理订单业务。ID 字段(int64)以 string 传输", "version": "1.0" }, "schemes": [ diff --git a/desc/api/docs/player-api.json b/desc/api/docs/player-api.json index 0818af4..029db74 100644 --- a/desc/api/docs/player-api.json +++ b/desc/api/docs/player-api.json @@ -2,7 +2,7 @@ "swagger": "2.0", "info": { "title": "聚玩打手服务", - "description": "聚玩用户服务处理打手信息管理、服务发布及订单相关接口", + "description": "聚玩用户服务处理打手信息管理、服务发布及订单相关接口。ID 字段(int64)以 string 传输", "version": "1.0" }, "schemes": [ diff --git a/desc/api/docs/review-api.json b/desc/api/docs/review-api.json index c22cf25..27214b7 100644 --- a/desc/api/docs/review-api.json +++ b/desc/api/docs/review-api.json @@ -1,8 +1,9 @@ { "swagger": "2.0", "info": { - "title": "", - "version": "" + "title": "聚玩评价服务", + "description": "处理订单评价与评分。ID 字段(int64)以 string 传输", + "version": "1.0" }, "schemes": [ "http", diff --git a/desc/api/docs/search-api.json b/desc/api/docs/search-api.json index 0c37aa7..8a78822 100644 --- a/desc/api/docs/search-api.json +++ b/desc/api/docs/search-api.json @@ -1,8 +1,9 @@ { "swagger": "2.0", "info": { - "title": "", - "version": "" + "title": "聚玩搜索服务", + "description": "内容搜索与首页推荐", + "version": "1.0" }, "schemes": [ "http", diff --git a/desc/api/docs/shop-api.json b/desc/api/docs/shop-api.json index 8d3e078..d2c3a28 100644 --- a/desc/api/docs/shop-api.json +++ b/desc/api/docs/shop-api.json @@ -1,8 +1,9 @@ { "swagger": "2.0", "info": { - "title": "", - "version": "" + "title": "聚玩店铺服务", + "description": "管理店铺信息、员工邀请、模板配置。ID 字段(int64)以 string 传输", + "version": "1.0" }, "schemes": [ "http", diff --git a/desc/api/docs/users-api.json b/desc/api/docs/users-api.json index 90a9b65..1748560 100644 --- a/desc/api/docs/users-api.json +++ b/desc/api/docs/users-api.json @@ -2,7 +2,7 @@ "swagger": "2.0", "info": { "title": "聚玩用户服务", - "description": "处理用户注册、登录、个人信息管理及关注系统", + "description": "处理用户注册、登录、个人信息管理及关注系统。ID 字段(int64)以 string 传输", "version": "1.0" }, "schemes": [ diff --git a/desc/api/docs/wallet-api.json b/desc/api/docs/wallet-api.json index 9372c1f..8716c66 100644 --- a/desc/api/docs/wallet-api.json +++ b/desc/api/docs/wallet-api.json @@ -2,7 +2,7 @@ "swagger": "2.0", "info": { "title": "钱包服务", - "description": "处理钱包充值相关", + "description": "处理钱包充值相关。ID 字段(int64)以 string 传输", "version": "1.0" }, "schemes": [ diff --git a/desc/api/email.api b/desc/api/email.api index e4f171d..1cca76b 100644 --- a/desc/api/email.api +++ b/desc/api/email.api @@ -1,6 +1,8 @@ syntax = "v1" info ( + title: "聚玩邮件服务" + desc: "处理邮件验证码发送与密码找回" author: "Asadz" date: "2024-06-19" version: "1.0" diff --git a/desc/api/game.api b/desc/api/game.api index 423b8d3..c243066 100644 --- a/desc/api/game.api +++ b/desc/api/game.api @@ -2,6 +2,13 @@ syntax = "v1" import "common.api" +info ( + title: "聚玩游戏服务" + desc: "管理游戏目录与分类。ID 字段(int64)以 string 传输" + author: "Asadz" + version: "1.0" +) + type ( Game { Id int64 `json:"id,string,optional"` diff --git a/desc/api/notification.api b/desc/api/notification.api index 608e4e0..1b3a18e 100644 --- a/desc/api/notification.api +++ b/desc/api/notification.api @@ -2,6 +2,13 @@ syntax = "v1" import "common.api" +info ( + title: "聚玩通知服务" + desc: "管理站内消息通知。ID 字段(int64)以 string 传输" + author: "Asadz" + version: "1.0" +) + type ( PathId { Id int64 `path:"id"` diff --git a/desc/api/order.api b/desc/api/order.api index 34b47f7..1c430c1 100644 --- a/desc/api/order.api +++ b/desc/api/order.api @@ -4,7 +4,7 @@ import "common.api" info ( title: "聚玩订单服务" - desc: "处理订单业务" + desc: "处理订单业务。ID 字段(int64)以 string 传输" author: "Asadz" version: "1.0" ) diff --git a/desc/api/player.api b/desc/api/player.api index 7309813..1f33e68 100644 --- a/desc/api/player.api +++ b/desc/api/player.api @@ -2,7 +2,7 @@ syntax = "v1" info ( title: "聚玩打手服务" - desc: "聚玩用户服务处理打手信息管理、服务发布及订单相关接口" + desc: "聚玩用户服务处理打手信息管理、服务发布及订单相关接口。ID 字段(int64)以 string 传输" author: "Asadz" version: "1.0" ) diff --git a/desc/api/review.api b/desc/api/review.api index 7a67046..2209140 100644 --- a/desc/api/review.api +++ b/desc/api/review.api @@ -2,6 +2,13 @@ syntax = "v1" import "common.api" +info ( + title: "聚玩评价服务" + desc: "处理订单评价与评分。ID 字段(int64)以 string 传输" + author: "Asadz" + version: "1.0" +) + type ( ReviewPathId { Id int64 `path:"id"` diff --git a/desc/api/search.api b/desc/api/search.api index 17636b6..8ad0b27 100644 --- a/desc/api/search.api +++ b/desc/api/search.api @@ -2,6 +2,13 @@ syntax = "v1" import "common.api" +info ( + title: "聚玩搜索服务" + desc: "内容搜索与首页推荐" + author: "Asadz" + version: "1.0" +) + type ( PathIDReq { Id int64 `path:"id"` diff --git a/desc/api/shop.api b/desc/api/shop.api index 96278e1..f5d5079 100644 --- a/desc/api/shop.api +++ b/desc/api/shop.api @@ -2,6 +2,13 @@ syntax = "v1" import "common.api" +info ( + title: "聚玩店铺服务" + desc: "管理店铺信息、员工邀请、模板配置。ID 字段(int64)以 string 传输" + author: "Asadz" + version: "1.0" +) + type ( ShopProfile { Id string `json:"id"` diff --git a/desc/api/users.api b/desc/api/users.api index cda3ab5..315b5e5 100644 --- a/desc/api/users.api +++ b/desc/api/users.api @@ -2,7 +2,7 @@ syntax = "v1" info ( title: "聚玩用户服务" - desc: "处理用户注册、登录、个人信息管理及关注系统" + desc: "处理用户注册、登录、个人信息管理及关注系统。ID 字段(int64)以 string 传输" author: "Asadz" version: "1.0" ) diff --git a/desc/api/wallet.api b/desc/api/wallet.api index 812f1f8..46d04d3 100644 --- a/desc/api/wallet.api +++ b/desc/api/wallet.api @@ -2,7 +2,7 @@ syntax = "v1" info ( title: "钱包服务" - desc: "处理钱包充值相关" + desc: "处理钱包充值相关。ID 字段(int64)以 string 传输" author: "Asadz" version: "1.0" )