java_client.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. import httpx
  2. from typing import Optional
  3. from app.config import settings
  4. import logging
  5. logger = logging.getLogger(__name__)
  6. class JavaClient:
  7. """Java 后端 HTTP 客户端 (单例, 复用连接池)"""
  8. _instance: Optional["JavaClient"] = None
  9. def __new__(cls):
  10. if cls._instance is None:
  11. cls._instance = super().__new__(cls)
  12. cls._instance._client = None
  13. cls._instance._base_url = settings.java_base_url
  14. return cls._instance
  15. async def _get_client(self) -> httpx.AsyncClient:
  16. if self._client is None:
  17. limits = httpx.Limits(
  18. max_connections=10,
  19. max_keepalive_connections=5,
  20. keepalive_expiry=30,
  21. )
  22. self._client = httpx.AsyncClient(
  23. base_url=self._base_url,
  24. timeout=httpx.Timeout(120.0, connect=3.0),
  25. limits=limits,
  26. )
  27. return self._client
  28. async def close(self):
  29. if self._client:
  30. await self._client.aclose()
  31. self._client = None
  32. JavaClient._instance = None
  33. async def get_published_articles(self) -> list[dict]:
  34. """拉取已发布知识库文章(分页 + records 解析,避免单次响应过大 / 返回 dict 结构)"""
  35. client = await self._get_client()
  36. page = 1
  37. size = 50
  38. records = []
  39. while True:
  40. resp = await client.post("/api/article/list", json={"status": "published", "page": page, "size": size})
  41. data = resp.json()
  42. if data.get("code") != 200:
  43. break
  44. body = data.get("data", {}) or {}
  45. items = body.get("records", []) or []
  46. records.extend(items)
  47. total = body.get("total", 0)
  48. if page * size >= int(total or 0) or not items:
  49. break
  50. page += 1
  51. return records
  52. async def search_products(self, keyword: str, limit: int = 5) -> list[dict]:
  53. client = await self._get_client()
  54. resp = await client.post("/api/product/search", json={
  55. "keyword": keyword, "status": "上架", "limit": limit
  56. })
  57. data = resp.json()
  58. if data.get("code") == 200:
  59. return data.get("data", [])
  60. return []
  61. async def search_activities(self, keyword: str, limit: int = 5) -> list[dict]:
  62. client = await self._get_client()
  63. resp = await client.post("/api/activity/search", json={
  64. "keyword": keyword, "status": "published", "limit": limit
  65. })
  66. data = resp.json()
  67. if data.get("code") == 200:
  68. return data.get("data", [])
  69. return []
  70. async def search_articles(self, keyword: str, limit: int = 5) -> list[dict]:
  71. client = await self._get_client()
  72. resp = await client.post("/api/article/search", json={
  73. "keyword": keyword, "status": "published", "limit": limit
  74. })
  75. data = resp.json()
  76. if data.get("code") == 200:
  77. return data.get("data", [])
  78. return []
  79. async def get_user_context(self, user_id: int, params: Optional[dict] = None) -> dict:
  80. client = await self._get_client()
  81. resp = await client.post(settings.effective_java_context_url, json={
  82. "user_id": str(user_id),
  83. "params": params or {},
  84. })
  85. data = resp.json()
  86. if data.get("code") == 200:
  87. return data.get("data", {})
  88. return {}
  89. @staticmethod
  90. def _join_metadata(value) -> str:
  91. """把后端返回的标签/维度数组或字符串统一为 Chroma metadata 字符串。"""
  92. if value is None:
  93. return ""
  94. if isinstance(value, str):
  95. return value
  96. if isinstance(value, (list, tuple, set)):
  97. return ",".join(str(item) for item in value if item)
  98. return str(value)
  99. @classmethod
  100. def _map_dan_knowledge(cls, record: dict) -> dict:
  101. """dan_knowledge_base 字段(camelCase) → loader/_build_chunks 所需结构。"""
  102. return {
  103. "id": record.get("id"),
  104. "title": record.get("title") or "",
  105. "content": record.get("content") or "",
  106. "category": "",
  107. "tags": cls._join_metadata(record.get("tagNames") or record.get("tags")),
  108. "dimensions": cls._join_metadata(
  109. record.get("dimensionCodes") or record.get("dimensions")),
  110. "source_url": record.get("sourceUrl") or "",
  111. "pub_date": str(record.get("createdAt") or record.get("updatedAt") or ""),
  112. }
  113. async def get_microbiome_articles(self) -> list[dict]:
  114. """拉取菌群知识库文章(统一数据源 dan_knowledge_base,source_type='article')
  115. 2026-10-20 迁移:microbiome_article 表废弃,809 篇菌群知识统一到
  116. dan_knowledge_base 管理。分页拉 /api/admin/knowledge-base/list,
  117. ORDER BY id 稳定分页,避免跨页重复。标签/维度由后端批量回填。
  118. """
  119. client = await self._get_client()
  120. page = 1
  121. size = 50
  122. records = []
  123. while True:
  124. resp = await client.post("/api/admin/knowledge-base/list",
  125. json={"page": page, "size": size, "status": 1, "sourceType": "article"})
  126. data = resp.json()
  127. if data.get("code") != 200:
  128. break
  129. body = data.get("data", {}) or {}
  130. items = body.get("records", []) or []
  131. records.extend(items)
  132. total = body.get("total", 0)
  133. if page * size >= int(total or 0) or not items:
  134. break
  135. page += 1
  136. return [
  137. self._map_dan_knowledge(r) for r in records if r.get("id") is not None
  138. ]
  139. async def get_dan_knowledge_base(self) -> list[dict]:
  140. """拉取案例报告源(dan_knowledge_base 表,status=1,排除 article 菌群文章)
  141. 若 article 为 0 条(INSERT 前确实为 0),则该源拉 0 条,正常。
  142. 返回结构包含 content/tags/dimensions,供 Chroma metadata 使用。
  143. """
  144. client = await self._get_client()
  145. page = 1
  146. size = 1000
  147. records = []
  148. while True:
  149. resp = await client.post("/api/admin/knowledge-base/list", json={"page": page, "size": size, "status": 1, "excludeSourceType": "article"})
  150. data = resp.json()
  151. if data.get("code") != 200:
  152. break
  153. body = data.get("data", {}) or {}
  154. items = body.get("records", []) or []
  155. records.extend(items)
  156. total = body.get("total", 0)
  157. if page * size >= int(total or 0) or not items:
  158. break
  159. page += 1
  160. return [
  161. self._map_dan_knowledge(r) for r in records if r.get("id") is not None
  162. ]
  163. async def get_member_reports(self, member_id: int) -> list[dict]:
  164. """获取某个家庭成员的健康报告列表"""
  165. client = await self._get_client()
  166. resp = await client.post("/api/health/report/list", json={"memberId": member_id})
  167. data = resp.json()
  168. if data.get("code") == 200:
  169. return data.get("data", [])
  170. return []
  171. async def get_report_indicators(self, report_id: int) -> list[dict]:
  172. """获取健康报告的所有指标明细"""
  173. client = await self._get_client()
  174. resp = await client.post("/api/health/report/indicator/list", json={"reportId": report_id})
  175. data = resp.json()
  176. if data.get("code") == 200:
  177. return data.get("data", [])
  178. return []
  179. async def query_health_knowledge(self, item_type: str, item_name: str) -> dict | None:
  180. """查询健康知识库中某个指标/菌属/营养素的定义"""
  181. client = await self._get_client()
  182. resp = await client.post("/api/health/knowledge/query", json={
  183. "itemType": item_type, "itemName": item_name,
  184. })
  185. data = resp.json()
  186. if data.get("code") == 200 and data.get("data"):
  187. return data["data"]
  188. return None
  189. async def batch_query_health_knowledge(self, queries: list[dict]) -> list[dict]:
  190. """批量查询健康知识库"""
  191. client = await self._get_client()
  192. resp = await client.post("/api/health/knowledge/batch-query", json={"queries": queries})
  193. data = resp.json()
  194. if data.get("code") == 200:
  195. return data.get("data", [])
  196. return []
  197. async def get_family_context(self, user_id: int, intent_type: str = "child_info", params: dict | None = None) -> dict:
  198. """获取家庭上下文数据(通过公开的 /api/ai/context 端点)"""
  199. client = await self._get_client()
  200. body = {"user_id": str(user_id), "intent_type": intent_type}
  201. if params:
  202. body["params"] = params
  203. resp = await client.post("/api/ai/context", json=body)
  204. data = resp.json()
  205. if data.get("code") == 200:
  206. return data.get("data", {})
  207. return {}
  208. async def get_member_profile(self, member_id: int) -> dict:
  209. """获取家庭成员画像快照"""
  210. client = await self._get_client()
  211. resp = await client.post("/api/profile/my", json={"memberId": member_id})
  212. data = resp.json()
  213. if data.get("code") == 200 and data.get("data"):
  214. return data["data"]
  215. return {}
  216. async def get_member_health_status(self, member_id: int) -> dict:
  217. """获取家庭成员健康自检问卷(health_status,内部端点无用户归属校验)"""
  218. client = await self._get_client()
  219. resp = await client.post("/api/health-status/internal/get", json={"memberId": member_id})
  220. data = resp.json()
  221. if data.get("code") == 200 and data.get("data"):
  222. return data["data"]
  223. return {}
  224. async def get_member_diet_preferences(self, member_id: int) -> dict:
  225. """获取家庭成员饮食偏好(diet_preferences,含饮食结构现状)"""
  226. client = await self._get_client()
  227. resp = await client.post("/api/diet/preferences/current-member", json={"memberId": member_id})
  228. data = resp.json()
  229. if data.get("code") == 200 and data.get("data"):
  230. return data["data"]
  231. return {}
  232. async def get_member_indicator_current(self, member_id: int) -> list[dict]:
  233. """获取成员指标当前值+相对上次变化(member_indicator_current,含 delta/trend)"""
  234. client = await self._get_client()
  235. resp = await client.post("/api/health/indicators/current", json={"memberId": member_id})
  236. data = resp.json()
  237. if data.get("code") == 200:
  238. return data.get("data", [])
  239. return []