海外交友 App 后端。整个系统围绕一个核心命题展开:
真人(BH, BioHuman)与数字人(DH, DigitalHuman)共存。 真人供给不足时,由 AI 数字人承接冷启动阶段的陪伴;两条链路共用同一套消息、社交关系与付费体系。
因此这个仓库里同时存在两件通常不会放在一起的东西:一套 7 服务的 Java 微服务后端,和一个把 LLM 当"演员"用的对话引擎(Python 原型 → Java 重写两代)。
534 个 Java 源文件 · 7 个微服务 + 1 个公共库 · 7 个 gRPC 契约模块 · 8 份 .proto · 2 代数字人引擎
┌────────────────────────┐
App / H5 ──REST──► mobile-gateway (BFF) │ 鉴权 / 限流 / 聚合 / 协议转换
└───────┬────────────────┘
│ gRPC(全系统唯一的东西向协议)
┌────────────┬───────┼────────────┬─────────────┐
▼ ▼ ▼ ▼ ▼
user-service match-service post-service payment-service
▲ ▲ ▲ ▲
└────────────┴─────┬──────┴─────────────┘
│
im-service ──► ai-chat-v2 (Java) 数字人引擎
│ └──► ai-chat (Python) 原型
│
OpenIM / LiveKit(IM 与音视频 provider,只有 im-service 直连)
| 服务 | 职责 | 关键机制 |
|---|---|---|
mobile-gateway |
对外唯一入口(REST BFF),聚合下游 gRPC | JWT 签发/校验 + refresh + 黑名单、IP 白名单、限流、TraceId 透传 |
user-service |
身份(手机/三方/设备)与资料、兴趣标签、附近的人、DH 候选池 | 多路 ResolveOrCreate、头像 presigned 直传、地理字典(US Cities 种子数据) |
match-service |
每日卡片队列、滑卡、匹配、访客/喜欢记录、配额 | 队列按在线/离线分别生成、Ranker + PreferenceBuilder、Outbox 最终一致 |
post-service |
动态发布、评论、点赞、Feed | 写扩散到 MQ、Redis 计数与 Feed 池、ShedLock 定时落库/打分 |
payment-service |
订单、订阅、金币账本、提现 | PayPal 执行器、REST + gRPC 双协议、幂等账本 |
im-service |
消息路由、在线状态、出站通知、DH 打字机 | provider 适配层(OpenIM / TencentIM)、事件分发、金币扣费、反导流检测 |
dating-common |
横切公共库 | 业务主键生成器(含 DB 序列)、S3 对象存储与 presigned、告警通道(企业微信 + 令牌桶限流 + logback appender) |
ai-chat-v2 |
Java 版数字人引擎(6 个 Maven 模块) | Agent = LLM + Loop + Tools,ArchUnit 守护模块边界 |
ai-chat |
Python 版数字人引擎(原型) | LangChain / LangGraph、SmartRouter 多模态路由与熔断 |
proto/ |
gRPC 契约,按服务拆模块 | 每个服务一个 jar,可独立发版;Java + Python 双份 stub |
这是整个项目里设计密度最高的部分。产品要求 DH 的回复必须像真人,于是引擎的写法是反直觉的:
Agent = LLM + Loop + Tools,循环里零业务逻辑。
im-service ──gRPC ChatAgent/Chat──► ChatAgentGrpcService
│
LaneManager 同会话串行 + 连发合并 + 运行中可打断
│
AgentRuntime pre-hooks → loop → post-hooks
│
AgentLoop 纯 LLM + function-calling
│
send_message 唯一产出用户可见消息的出口
三条不变量决定了代码为什么长这样:
- LLM 的文本输出 ≠ 用户消息。 只有
send_message工具(COMMIT 阶段)能产出可见消息;模型不调它,DH 就沉默。这条把"说什么"和"要不要说"彻底解耦。 - provider 原始报文逐字回传。 剥离或重排
rawPart会打断跨轮次的 function-calling —— 所以模型报文不做任何规范化。 - 循环内零业务逻辑。 要加行为只能加 Hook 或 Tool,
AgentLoop本身不生长。
配套的工程约束:
- 模块边界由 ArchUnit 编译期守护(6 条规则),
aichat-app是唯一 composition root;依赖方向写错,mvn test直接红。 - 同会话串行、跨会话并行:
LaneManager把并发模型收在一处,连发消息在阅读窗内合并成一次 LLM 调用,被合并的那次 gRPC 返回空内容,由最后一次返回完整回复。 - 安全流水线分层:正则闸(自曝 AI 身份 / 外泄联系方式)→ 分类器 → 分段投递 → 出口守卫;分类器不可用时按 fail-closed 处理。
- 可观测:每一轮 run 都能按 traceId 追出 hook 顺序、工具调用、provider 选择。
Python 版(ai-chat/)是同一命题的前一代实现:LangGraph 编排 + checkpointer 持久化,VisionAgent 走 SmartRouter 在多家多模态 provider 之间轮询并熔断回退。两代并存是刻意的 —— v2 的很多设计决定就是照着 v1 在真实流量里暴露的问题改的。
proto/ 是一个多模块工程,每个服务一个独立 jar,而不是打一个"全宇宙"包 —— 避免 gateway 拉进它根本用不到的契约。Java 侧由 protobuf-maven-plugin 生成,Python 侧由 gen-python.sh 生成,两侧 schema 同源。
服务之间没有任何共享 DTO:跨进程只认 proto 生成的类型,REST 层的 VO/DTO 只存在于 gateway。
服务间不出现 HTTP/Feign/RestTemplate 互调,也不允许连别人家的 DB / Redis key / 对象桶。要数据就调对方 gRPC。
这条约束落到代码上有几个具体后果:
ObjectStorage接口刻意不暴露 bucket 入参 —— 从签名层面堵死"直读别人家桶";跨服务的对象一律由 owner service 自己读、自己签。- 对象存储只回
*_key不回完整 URL,URL 在读取时现签。 - 跨表不做 JOIN:持久层单表,跨表在 service 层多次单表查 + 内存拼装。
- post:
CreatePost落库后把扩散动作丢进 RocketMQ,消费端做 Feed 写扩散;Redis 里维护计数增量与 Feed 池,再由 ShedLock 保证多实例只有一个节点跑定时落库/打分任务。Producer 自己带有限次重试,客户端内置重试关掉,避免重试叠加;失败转 DLQ。 - match:匹配结果的 IM 副作用不放进同一个事务,走 Outbox 表 + 重试任务,代价是几秒延迟,换来的是不跨服务开事务。
业务服务不直连 OpenIM / LiveKit,全部经 im-service。im-service 内部再用 ImProviderAdaptor 抽象把 provider 隔离(OpenIM / TencentIM 两套实现),上层 handler 只处理事件语义:回调分发 → 反导流检测 → 金币扣费 → AI 回复派发 → 在线状态维护。
这样换 IM 供应商的影响面被限制在一个 adaptor 里。
- 时间一律 UTC:DB 列
TIMESTAMPTZ、JVMTZ=UTC、连接 session 强制 UTC,三层对齐。 - 业务主键用带语义的 BIGINT 而非自增 id:
环境位 + 日期 + 当天序号,由dating-common统一生成,数据库序列兜底。自增 id 只做内部物理主键,不出现在任何 API 出参里。
| 层 | 选型 |
|---|---|
| 语言 / 运行时 | Java 21(虚拟线程)、Python 3.13 |
| 框架 | Spring Boot 3.3.5、MyBatis-Plus 3.5、Spring Security(gateway) |
| 关系库 | PostgreSQL + Flyway(唯一关系库,时间列一律 TIMESTAMPTZ) |
| 缓存 / 分布式锁 | Redis + Redisson 3.36(所有 key 带前缀 + TTL) |
| 消息 | RocketMQ(rocketmq-spring 2.3;异步事件 / 写扩散 / 解耦) |
| 服务间 | gRPC 1.68 + Protobuf 4.28 |
| 配置与服务发现 | Nacos(Config + Discovery 同 namespace) |
| 对象存储 | S3 兼容;dating-common 提供 ObjectStorage 抽象与 presigned 能力 |
| 分布式调度 | ShedLock 5.16 |
| LLM / 多模态 | DeepSeek(对话)、Groq / Gemini / Z.AI(视觉,多 provider 路由 + 熔断) |
| 编排 | LangChain / LangGraph(Python 版) |
| 边界守护 | ArchUnit |
.
├── dating-server/ # 7 个 Java 微服务 + 公共库(各自独立 Maven 构建)
│ ├── dating-common/ # 业务主键 / 对象存储 / 告警
│ ├── mobile-gateway/ # REST BFF(对外唯一入口)
│ ├── user-service/ # 身份 / 资料 / 推荐
│ ├── match-service/ # 卡片队列 / 匹配
│ ├── post-service/ # 动态 / Feed
│ ├── payment-service/ # 订单 / 订阅 / 金币
│ └── im-service/ # 消息 / 在线状态 / 通知
├── ai-chat-v2/ # Java 数字人引擎(6 个模块,ArchUnit 守护边界)
│ ├── aichat-models/ # 领域模型,无框架依赖
│ ├── aichat-core/ # AgentLoop / Runtime / LaneManager + 端口接口
│ ├── aichat-llm/ # LLM client + 可 hedge 的路由
│ ├── aichat-infra/ # Redis 历史与 trace、下游 gRPC 适配
│ ├── aichat-features/ # Hook / Tool 实现
│ └── aichat-app/ # Spring Boot 装配(唯一 composition root)
├── ai-chat/ # Python 数字人引擎(LangChain/LangGraph 原型)
├── proto/ # gRPC 契约,按服务拆模块,Java + Python 双份 stub
├── nacos/ # 各服务的配置模板(真值不入库)
└── deploy/ # 本仓库全部服务的 dev 编排
| 决策 | 换来什么 | 代价 |
|---|---|---|
| 服务间只走 gRPC,禁 HTTP 互调 | 契约强类型、可独立演进、性能可预期 | 调试链路比 REST 长,需要 grpcurl / reflection |
| 契约按服务拆 jar | 调用方只拉需要的依赖 | 契约发版次数变多 |
| 持久层禁多表 JOIN | 表可独立分库、写入路径可预测 | 复杂查询要在内存里拼 |
| 时间全链路 UTC | 跨机房 / 跨时区不会串天 | 展示层必须显式做时区转换 |
| LLM 循环内零业务逻辑 | 行为可插拔、循环稳定 | Hook 顺序本身变成一种需要被守护的契约 |
| 数字人引擎从 Python 重写到 Java | 与 Java 侧共用一套部署 / 观测 / 依赖治理 | 一次完整重写的成本 |
依赖 JDK 21、Maven 3.9+、Docker(Compose v2)。中间件(PostgreSQL / Redis / Nacos / MinIO / RocketMQ)与各服务的连接信息全部走配置模板,仓库里不含任何真实凭据。
# 1. 契约装到本地仓库(各服务的 gRPC 依赖来自这里)
cd proto && mvn -DskipTests install
# 2. 公共库装到本地仓库(user-service 依赖它)
cd ../dating-server/dating-common && mvn -DskipTests install
# 3. 其余服务各自独立构建(dating-server 下没有 parent pom,每个服务自成一个 Maven 工程)
cd ../user-service && mvn -DskipTests package
# 4. 把 nacos/ 下的配置模板贴到 Nacos 对应 namespace,替换其中的占位符
# (模板里出现 infra.example.com / your_* 的地方都是待填值)
# 5. 复制环境变量模板并填值
cp deploy/.env.deploy.example deploy/.env.deploy
# 6. 起服务
docker network create dating-app
docker compose -f deploy/docker-compose.dev.yml up -d --build各服务的默认端口见 deploy/docker-compose.dev.yml;健康检查统一走 /actuator/health。
ai-chat-v2/README.md—— 数字人引擎 v2 的模块划分、一次请求的完整路径、三条不变量ai-chat/README.md—— Python 版引擎的能力清单与核心机制dating-server/dating-common/README.md—— 公共库dating-server/dating-common/ALERT.md—— 告警通道接入nacos/README.md—— 配置模板约定