{"openapi":"3.1.0","info":{"title":"nirvana-service","description":"Rust 重写的 nirvana 后端服务. 跟 nirvana-core (Python FastAPI) 字节级 API 兼容, 灰度切流量无 App 端感知。","license":{"name":"Proprietary"},"version":"0.1.0"},"servers":[{"url":"http://localhost:18889","description":"本机 dev"},{"url":"https://api.example.com","description":"生产 (示例)"}],"paths":{"/faith/auth/login":{"post":{"tags":["auth"],"summary":"用户登录 — 返回 JWT + 用户信息. **客户端密码须先 SHA-256**.","operationId":"auth_login","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserLoginReq"}}},"required":true},"responses":{"200":{"description":"用户不存在 (code=200204) / 密码错 (code=200205) / 已禁用 (code=200206) — 走 envelope code, HTTP 状态仍 200"}}}},"/faith/incense/temple/stats/{temple_id}":{"post":{"tags":["incense"],"summary":"`POST /incense/temple/stats/{temple_id}` — 寺庙上香聚合.\n返回 `{temple_id, total_offerings, recent_prayers[]}`, 公开端点.","operationId":"incense_temple_stats","parameters":[{"name":"temple_id","in":"path","description":"寺庙主键","required":true,"schema":{"type":"integer","format":"int32"}}],"responses":{"200":{"description":"成功 (envelope.data 是 TempleOfferingStatsData)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TempleOfferingStatsData"}}}}}}},"/faith/practice/pure-land/{id}":{"post":{"tags":["practice"],"summary":"净土详情 — path 取 id, body 可空. 404 if not found.","operationId":"practice_pure_land_detail","parameters":[{"name":"id","in":"path","description":"净土主键","required":true,"schema":{"type":"integer","format":"int32"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PureLandDetailReq"}}},"required":true},"responses":{"200":{"description":"成功 (envelope.data 是 PureLandOut)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PureLandOut"}}}},"404":{"description":"净土不存在"}}}},"/faith/practice/pure-lands":{"post":{"tags":["practice"],"summary":"净土列表 — 从 `pure_land` 表读, 默认 3 处 (西方/东方/兜率).\n公开端点, 不需要 JWT.","operationId":"practice_pure_lands","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PureLandsReq"}}},"required":true},"responses":{"200":{"description":"成功 (envelope.data 是 PureLandListData)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PureLandListData"}}}}}}},"/faith/practice/qa/ask_ai":{"post":{"tags":["practice"],"summary":"对经文段落向 AI 提问 — 走 LiteLLM 代理 (默认 aws_cs4 模型), 答复落库\n`practice_user_sect_sutra_qa`, `answer_user_type='ai'`. **要求 JWT**.","operationId":"practice_qa_ask_ai","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AskAiReq"}}},"required":true},"responses":{"200":{"description":"AI 调用失败 / 参数非法 — 走 envelope code"}},"security":[{"bearer_auth":[]}]}},"/faith/practice/sect/list":{"post":{"tags":["practice"],"summary":"宗派列表 — cursor 分页, 5min Redis 缓存, App 首页用.","operationId":"practice_sect_list","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SectListReq"}}},"required":true},"responses":{"200":{"description":"成功 (envelope.data 是 SectListData)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SectListData"}}}}}}},"/faith/wish/categories":{"post":{"tags":["wish"],"summary":"许愿分类字典 — App 端许愿表单的 category 下拉.\n公开端点, body 可空.","operationId":"wish_categories","responses":{"200":{"description":"成功 (envelope.data 是 WishCategoriesData)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WishCategoriesData"}}}}}}},"/faith/wish/incense-types":{"post":{"tags":["wish"],"summary":"上香炷数字典 — App 端选择 single/triple/nine 用.\n公开端点, body 可空.","operationId":"wish_incense_types","responses":{"200":{"description":"成功 (envelope.data 是 IncenseTypesData)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IncenseTypesData"}}}}}}},"/health/live":{"get":{"tags":["health"],"summary":"进程探针 — 立即返回 200, 不查任何外部依赖.","operationId":"health_live","responses":{"200":{"description":"进程在","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResp"}}}}}}},"/health/ready":{"get":{"tags":["health"],"summary":"就绪探针 — DB ping + Redis ping 都通才 200, 否则 503.","operationId":"health_ready","responses":{"200":{"description":"全部依赖健康","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResp"}}}},"503":{"description":"某个依赖挂了","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResp"}}}}}}}},"components":{"schemas":{"ApiEnvelopeMeta":{"type":"object","description":"标准 envelope 元信息 — 实际响应是 `{code, message, data}`. data 字段类型\n跟具体 endpoint 不同, 这里只描述外层结构, data 用 `Object` 表示。\n\n单 endpoint 标注 response 时直接用具体的 `XxxData` struct 而不是这个 —\n这个 schema 仅作\"业务码体系\"的总参考。","required":["code","message","data"],"properties":{"code":{"type":"integer","format":"int32","description":"业务码: 200=成功, 100000=AUTH_INVALID, 100001=AUTH_EXPIRED,\n200201=USERNAME_ALREADY_EXISTS, 200205=INVALID_PASSWORD ..."},"data":{"type":"object","description":"实际业务数据. 不同 endpoint shape 不同。"},"message":{"type":"string","description":"用户可见的消息. 成功时为空字符串."}}},"AskAiData":{"type":"object","description":"`qa/ask_ai` 响应 data. 跟 Python `practice_service.ask_ai_about_paragraph`\n返回的 dict 字段一一对齐: `qa_id` / `question` / `answer` / `paragraph_content`.","required":["qa_id","question","answer"],"properties":{"answer":{"type":"string"},"paragraph_content":{"type":["string","null"],"description":"命中 paragraph_id 时给段落原文, 没命中或没传都给 null。"},"qa_id":{"type":"integer","format":"int32"},"question":{"type":"string"}}},"AskAiReq":{"type":"object","description":"`POST /practice/qa/ask_ai` 请求体.\n\n对应 Python `class AskAiReq`. `user_id` 字段保留兼容 App 老 payload, 但实际\n鉴权走 JWT — handler 用 `AuthUser.id` 而不是 `req.user_id`.","required":["question"],"properties":{"paragraph_content":{"type":["string","null"],"description":"段落原文 — 离线客户端直接带上, 服务端不必再查段落表。优先于 `paragraph_id`。"},"paragraph_id":{"type":["integer","null"],"format":"int32","description":"段落 ID — 可选 (老 App 传)。离线客户端改直接传 `paragraph_content`。"},"question":{"type":"string","description":"用户提问 (1-500 字). 超长 / 空都返回 BadRequest。"},"sutra_code":{"type":["string","null"],"description":"稳定字符串键 (manifest id). 离线客户端用它定位经书, 优先于 `sutra_id`。"},"sutra_id":{"type":["integer","null"],"format":"int32","description":"服务端经书数值 id. 离线客户端拿不到, 改传 `sutra_code`。两者至少一个。"},"user_id":{"type":["integer","null"],"format":"int32","description":"兼容老 App payload — 服务端忽略, user_id 取自 JWT。"}}},"HealthResp":{"type":"object","description":"`/health/live` `/health/ready` 响应体.","required":["status"],"properties":{"checks":{"description":"仅 ready 探针有 — 各依赖项 ping 结果。"},"status":{"type":"string","description":"`live` / `ready` / `not_ready`"}}},"IncenseTypeItem":{"type":"object","description":"`wish_incense_type` 行.","required":["code","name","count","description","sort_order"],"properties":{"code":{"type":"string"},"count":{"type":"integer","format":"int32"},"description":{"type":"string"},"name":{"type":"string"},"sort_order":{"type":"integer","format":"int32"}}},"IncenseTypesData":{"type":"object","required":["incense_types"],"properties":{"incense_types":{"type":"array","items":{"$ref":"#/components/schemas/IncenseTypeItem"}}}},"PureLandDetailReq":{"type":"object","description":"`POST /practice/pure-land/{id}` 请求 (body 可空, 主键走 path)."},"PureLandListData":{"type":"object","required":["pure_lands"],"properties":{"pure_lands":{"type":"array","items":{"$ref":"#/components/schemas/PureLandOut"}}}},"PureLandOut":{"type":"object","description":"输出: 单条净土详情.\n\nJSON 字段（conditions/practices/sutras）从 DB 的 JSON 列拍成 `Vec<String>`,\nservice 层负责解码 — 万一 DB 里数据格式错乱（如手工写成 JSON 对象），\nfallback 成空数组而不是 500.","required":["id","code","name","buddha","description","characteristics","conditions","practices","sutras"],"properties":{"buddha":{"type":"string"},"characteristics":{"type":"string"},"code":{"type":"string"},"conditions":{"type":"array","items":{"type":"string"}},"description":{"type":"string"},"id":{"type":"integer","format":"int32"},"image_url":{"type":["string","null"]},"name":{"type":"string"},"name_en":{"type":["string","null"]},"practices":{"type":"array","items":{"type":"string"}},"sutras":{"type":"array","items":{"type":"string"}}}},"PureLandsReq":{"type":"object"},"SectListData":{"type":"object","description":"`/practice/sect/list` 响应 data 字段.","required":["sects","has_more","last_id"],"properties":{"has_more":{"type":"boolean","description":"是否还有下一页 (返回数 >= page_size 视为可能有)."},"last_id":{"type":"integer","format":"int32","description":"当前页最后一条 id, 客户端下次传它做 cursor."},"sects":{"type":"array","items":{"$ref":"#/components/schemas/SectListItem"}}}},"SectListItem":{"type":"object","description":"宗派列表项 (响应 data.sects 中的元素).\n\n字段顺序 / 命名跟 Python 的 service 返回 dict 完全一致 — App 端无感切换。","required":["id","code","name"],"properties":{"banner_image":{"type":["string","null"]},"category":{"type":["string","null"]},"characteristics":{"type":["string","null"]},"code":{"type":"string"},"core_teachings":{"type":["string","null"]},"description":{"type":["string","null"]},"description_en":{"type":["string","null"]},"display_order":{"type":["integer","null"],"format":"int32"},"icon_url":{"type":["string","null"]},"id":{"type":"integer","format":"int32"},"name":{"type":"string"},"name_en":{"type":["string","null"]}}},"SectListReq":{"type":"object","description":"`POST /practice/sect/list` 请求体\n\n对应 Python `class SectListReq`.","properties":{"category":{"type":["string","null"],"description":"可选分类过滤. None = 全部宗派。"},"last_id":{"type":"integer","format":"int32","description":"cursor 分页 — 上一页最后一条 sect.id; 首页传 0。"},"page_size":{"type":"integer","format":"int64","description":"每页条数, 默认 20, 最大 100。"}}},"TempleOfferingStatsData":{"type":"object","description":"`POST /incense/temple/stats/{temple_id}` 响应.","required":["temple_id","total_offerings","recent_prayers"],"properties":{"recent_prayers":{"type":"array","items":{"type":"string"},"description":"最近 N 条祈愿文 (默认 10), 用于在寺庙详情页滚动展示."},"temple_id":{"type":"integer","format":"int32"},"total_offerings":{"type":"integer","format":"int64"}}},"UserAuthRespData":{"type":"object","description":"登录 / 注册响应 data — Python `UserAuthRespData`. token 加新建 / 已存在用户的全量信息.","required":["token","user"],"properties":{"token":{"type":"string","description":"HS256 JWT — 跟 nirvana-core 共享 secret, App 的同一个 token 在两个 service 都能用。"},"user":{"$ref":"#/components/schemas/UserInfoOut"}}},"UserInfoOut":{"type":"object","description":"用户信息 (登录 / 注册 / userinfo 共用响应字段). 跟 Python `UserInfoData` 完全一致.","required":["id","username","status","created_at"],"properties":{"avatar":{"type":["string","null"]},"created_at":{"type":"string","description":"格式化为 `YYYY-MM-DD HH:MM:SS` (跟 Python `strftime` 输出一致)."},"email":{"type":["string","null"]},"id":{"type":"integer","format":"int32"},"nickname":{"type":["string","null"]},"phone":{"type":["string","null"]},"status":{"type":"integer","format":"int32"},"username":{"type":"string"}}},"UserLoginReq":{"type":"object","required":["username","password"],"properties":{"password":{"type":"string","description":"客户端**已 SHA-256** 哈希过的密码 — 服务端不再 hash, 直接比对。"},"username":{"type":"string"}}},"WishCategoriesData":{"type":"object","required":["categories"],"properties":{"categories":{"type":"array","items":{"$ref":"#/components/schemas/WishCategoryItem"}}}},"WishCategoryItem":{"type":"object","description":"`wish_category` 行 — App 端读 `WishCategory`. 字段对齐 App audit:\ncode / name / description / icon / color / sort_order.","required":["code","name","description","icon","color","sort_order"],"properties":{"code":{"type":"string"},"color":{"type":"string"},"description":{"type":"string"},"icon":{"type":"string"},"name":{"type":"string"},"sort_order":{"type":"integer","format":"int32"}}}},"securitySchemes":{"bearer_auth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"跟 nirvana-core 共用同一 secret 签发的 HS256 token."}}},"tags":[{"name":"health","description":"健康探针 (k8s liveness/readiness probe)"},{"name":"auth","description":"用户认证 (注册 / 登录 / 资料 / 密码 / 注销)"},{"name":"practice","description":"修行模块 (宗派 / 净土 / 问答 / 苦集灭道 / 活动)"},{"name":"incense","description":"上香 / 订单 / 寺庙聚合"},{"name":"wish","description":"许愿 / 功德 / 字典"}]}