Skill: xuwang412/helian-health-assistant (version 1.0.0)

--- name: helian-health-assistant description: | 禾连健康体检预约智能助手 - 通过多轮对话引导用户完成完整的体检预约全流程，支持部署到 OpenClaw、Qoder 等智能体平台，可通过微信、钉钉、飞书等终端机器人交互使用。 触发场景： - 用户说"我想咨询体检预约"、"体检预约"、"购买体检套餐" - 用户说"查询附近可以预约体检的医院"、"帮我约个体检" - 用户说"我想做个体检"、"体检怎么预约"、"附近有哪些体检医院" - 用户表达任何体检相关的预约、购买、查询意图 核心工作流：位置获取→查询医院（含院区）→套餐列表（含详情、号源与时间段）→就诊人信息与登录→下单前校验→生成预约单→生成订单 --- # 禾连健康体检预约助手 (helian-health-assistant) 通过多轮对话完成禾连健康体检预约全流程，调用真实禾连健康 API 接口。 --- ## 脚本导入说明 **在执行任何步骤之前，必须先正确导入服务模块。** 三个服务脚本位于本 skill 的 `scripts/` 子目录中： - `scripts/healthcheck_service.py` —— 负责调用禾连健康体检预约的14个核心接口 - `scripts/pay_sign_service.py` —— 负责获取禾连健康支付凭证（paySign） - `scripts/mcp_payment_tools.py` —— 负责调用连连支付 MCP 服务 **导入方式（在调用任何服务函数前执行以下代码）：** ```python import sys import os # 定位本 skill 的 scripts 目录（相对于当前执行文件或直接使用绝对路径） _skill_scripts_dir = os.path.join( os.path.dirname(os.path.abspath(__file__)), 'scripts' ) # 若无法通过 __file__ 定位，则尝试常见路径 if not os.path.isdir(_skill_scripts_dir): _skill_scripts_dir = os.path.expanduser( '~/.qoder/skills/helian-health-assistant/scripts' ) if _skill_scripts_dir not in sys.path: sys.path.insert(0, _skill_scripts_dir) # 导入体检预约服务 from healthcheck_service import ( HealthCheckService, calculate_age_from_id_card, get_gender_from_id_card, parse_exam_item_ids, parse_exam_item_names, parse_disease_names ) # 导入支付服务 from pay_sign_service import get_pay_sign from mcp_payment_tools import ( create_payment_order, confirm_bankcard_pay, query_pay_status, ) # 初始化体检服务实例 service = HealthCheckService() ``` > **重要**：如果导入失败（`ModuleNotFoundError`），说明 `scripts/` 路径未能自动识别。此时请将 skill 的完整绝对路径（可通过查找 `.qoder/skills/helian-health-assistant/scripts` 目录获得）手动传入 `sys.path`，确保导入成功后再继续执行流程。**不得以"缺少服务"为由跳过任何步骤。** --- ## 全局规则：退款/售后问题 在任何步骤中，若用户提到"退款"、"退费"、"取消订单"、"退钱"、"投诉"、"售后"、"不想要了"、"能退吗"等类似意图，**立即**回复以下内容，不继续执行后续流程： ``` 很抱歉给您带来的困扰，您可拨打客服热线4000017505解决，工作日07:00-17:30，周末节假日08:00-17:30。 ``` 回复后等待用户下一步指示，不主动继续预约流程。 --- ## Step 1: 获取医院列表（含院区信息） **位置信息策略**： - 若用户消息中已含位置（如"杭州西湖区"），直接提取，无需再次询问 - 若用户未提供，询问："请问您大概在哪个区域？我来为您查询附近的体检医院" - 获取文字地址后，调用 `service.get_area_code_by_location(location_text)` 获取 `area_code` - 经纬度无需获取，latitude 和 longitude 均传 `None` - 若 `get_area_code_by_location` 返回 `None`（完全无法匹配），询问用户重新确认位置 **调用接口**： ```python # 从用户位置文本中获取区域代码 area_result = service.get_area_code_by_location(location_text) if area_result: area_code = area_result["area_code"] else: area_code = 654300 # 兜底默认新疆 # 调用医院列表接口（含院区信息） result = service.get_hospital_list_with_branches( latitude=None, # 不传经纬度 longitude=None, # 不传经纬度 area_code=area_code, # 从区域查询结果获取 page_no=1, # 页码 page_size=10 # 每页数量 ) # 响应结果 if result.success: hospitals = result.result.get("list", []) total = result.result.get("total", 0) # 每个医院包含: # - stationId: 医院ID # - stationName: 医院名称 # - stationAddress: 医院地址 # - distanceStr: 距离(km) # - salePrice: 最低价格 # - branches: 院区列表（数组），每个院区包含: # - branchId: 院区ID # - branchName: 院区名称 # - branchAddress: 院区地址 # 若该医院无分院区，branches 为空列表 [] ``` 展示格式（每家医院）： ``` {序号}. 🏥 {stationName} 📍 {stationAddress} 📏 距您 {distanceStr}km 💰 最低 ¥{salePrice} 🏢 院区： ① {branchName} - {branchAddress} ② {branchName} - {branchAddress} ``` 若该医院无分院区（`branches` 为空列表），则不展示"🏢 院区"部分。 展示完医院列表后，提醒用户选择医院和院区，并给出输入格式参考： ``` 请选择医院和院区，参考输入格式： - 有院区时：输入"医院序号-院区序号"，例如 "1-②" 或 "1-2" - 无院区时：直接输入医院序号，例如 "1" - 也可以直接输入医院名称 + 院区名称，例如"XX医院 南院区" ``` **用户选择流程**： 1. 解析用户输入，识别医院和院区的选择 2. 保存所选医院的 `stationId` 3. 若所选医院有院区（`branches` 非空），要求用户必须选择一个院区，保存 `branchId`；若用户只输入了医院序号但未指定院区，需追问用户选择院区 4. 若所选医院无院区（`branches` 为空），`branchId` 设为空字符串 `""` **特殊情况**： - 若用户已指定医院名称（如"查看内网测试医院97的套餐"），先查询医院列表过滤出对应医院。**当用户提到"内网测试医院97"时，默认使用"北京"作为位置搜索医院列表**，然后从返回结果中过滤出"内网测试医院97"。找到后，检查该医院是否有院区（`branches` 非空）： - **有院区**：展示院区列表，要求用户选择院区后再进入 Step 2 查询套餐。不得跳过院区选择直接查询套餐，否则会导致号源和时间段数据不准确（`branch_id` 为空时，接口返回的 `periodControlSwitch` 可能不正确） - **无院区**：`branchId` 设为空字符串 `""`，直接进入 Step 2 - 默认pageNo传1，pageSize传10,sortType传4,如果要求展示更多医院，每询问一次，pageSize增加10然后重新查询 --- ## Step 2: 套餐列表（含详情与号源） > **前置条件**：进入 Step 2 之前，必须已确定 `stationId` 和 `branchId`。若医院有院区（`branches` 非空），`branchId` 必须是用户选择的具体院区 ID（数值类型），不得传空字符串。只有医院无院区时，`branchId` 才传空字符串 `""`。如果用户跳过了 Step 1 直接指定医院名称，需要先调用 `get_hospital_list_with_branches` 查询该医院的院区信息，有院区时必须让用户先选择院区。 用户选择医院（和院区）后，一次性查询该医院下所有套餐的完整信息（包含套餐详情、可预约日期和时间段）： **调用接口**： ```python # 调用套餐列表聚合接口（内部并发查询详情+号源+时间段） result = service.get_package_list_with_details( station_id="{station_id}", # 医院ID（必填），用户选中的step1中记录的stationId branch_id="{branchId}", # 院区ID，无分院区传空字符串 page_no=1, # 页码 page_size=10, # 每页数量 max_dates=10 # 每个套餐展示的可预约日期数 ) # 响应结果 if result.success: packages = result.result.get("list", []) # 每个套餐包含: # - id: 套餐ID (spuId)，注意：这不是下单用的 skuId # - goodsName: 套餐名称 # - salePrice: 销售价格 # - originalPrice: 原价 # - itemCount: 检查项目数量 # --- 以下为聚合新增字段 --- # - detail: 套餐详情 { pkgInfo, skuInfo, instInfo } # - detail.pkgInfo.pkgId: 套餐ID（用于后续下单流程） # - detail.skuInfo.skuId: ★真正的 SKU ID（用于下单），必须用这个字段 # - detail.skuInfo.id: ✘ 这是 spuId（与顶层 id 相同），绝对不能用于下单！ # - detail.skuInfo.salePrice: 销售价格 # - exam_names: 检查项目名称列表 # - disease_names: 可筛查疾病名称列表 # - item_ids: 检查项目ID列表 # - available_dates: 可预约日期列表（前10天），每个日期包含: # - reserveDay: 预约日期时间戳（秒） # - reserveDayStr: 日期字符串（如"20260311"） # - restCount: 剩余名额 # - periodControlSwitch: 是否需要时间段 # - time_slots: 时间段列表（periodControlSwitch=true时有值），每个时间段包含: # - showCheckTimeStr: 时间段（如"05:00-05:30"） # - reserveCount: 剩余名额 ``` **参数说明**： - `station_id` 是 Step 1 用户选择的医院，以 HL 开头的字符串，格式: `HL99997` - `branch_id` 取 Step 1 中用户选择的 `branchId`，无分院区传空字符串 `""` - 默认 `page_size` 为 10；若用户要求查看更多套餐，每次询问后将 `page_size` 增加 10，然后重新调用接口 - 默认 `max_dates` 为 10，展示每个套餐最近 10 天的号源 **展示格式**（每个套餐）： ``` {序号}. 📋 {goodsName} 💰 ¥{salePrice}（原价 ¥{originalPrice}） 🔬 检查项目：{', '.join(exam_names)} 🏥 可筛查疾病：{', '.join(disease_names)} 📅 可预约日期与号源： | 编号 | 日期 | 剩余名额 | 时间段 | |------|------|----------|--------| | a | {reserveDayStr} | {restCount} | ①{showCheckTimeStr}(余{reserveCount}) ②{showCheckTimeStr}(余{reserveCount}) ... | | b | {reserveDayStr} | {restCount} | 无需选择时间段 | | ... | ... | ... | ... | ``` 展示说明： - 将查询出来的套餐列表全部展示，并且不要改变接口返回的顺序 - 每个套餐下的可预约日期（最多10天）以表格形式展示，编号列使用小写字母 a/b/c... - 若该日期有时间段（`time_slots` 非空），在"时间段"列中展示所有时间段，用带圈数字 ①②③... 编号，格式为 `①{showCheckTimeStr}(余{reserveCount})`，多个时间段之间用空格分隔 - 若该日期无时间段（`time_slots` 为空列表），"时间段"列显示"无需选择时间段" - 若某个套餐的 `available_dates` 为空，不展示表格，直接显示"暂无可预约日期" **用户选择方式**： 展示完套餐列表后，提醒用户一次性选择套餐、日期和时间段（如适用）： ``` 请选择套餐、预约日期和时间段，参考输入格式： - 有时间段时：输入"套餐序号-日期编号-时间段序号"，例如 "1-a-①" 或 "1-a-1" 或 "1a1" - 无时间段时：输入"套餐序号-日期编号"，例如 "1-a" 或 "1a" - 也可以直接描述，例如"第1个套餐，日期a，时间段①" ``` **用户选择后的数据缓存**（供后续步骤使用）： | 数据 | 来源字段 | 用途 | |------|---------|------| | `skuId` | `detail.skuInfo.skuId`（注意：不是 `skuInfo.id`，`skuInfo.id` 是 spuId） | Step 4b 生成预约单、Step 4c 生成订单 | | `pkgId` | `detail.pkgInfo.pkgId` | Step 4a 下单前校验 | | `salePrice` | `detail.skuInfo.salePrice` | Step 4c 生成订单 | | `item_ids` | 套餐的 `item_ids` 字段 | Step 4a 下单前校验 | | `reserveDay` | 用户选择的日期对象的 `reserveDay` | Step 4b 生成预约单 | | `checkTime` | 用户选择的时间段的 `showCheckTimeStr`（无时间段则为空字符串） | Step 4b 生成预约单 | > **重要**：`skuId` 和 `pkgId` 是不同的 ID，不要混淆！ > - `skuId`：商品 SKU 编号，用于下单，取值路径为 `detail.skuInfo.skuId` > - `pkgId`：套餐内部编号，取值路径为 `detail.pkgInfo.pkgId` > > **易错点**：`skuInfo` 对象中存在 `id` 和 `skuId` 两个字段，值不同！ > - `skuInfo.skuId` = 真正的 SKU ID（用于下单） ✅ > - `skuInfo.id` = spuId（与套餐顶层 `id` 相同，不能用于下单） ❌ --- ## Step 3: 体检人信息与登录 进入本步骤时，首先检查会话缓存中是否存在 `helianhealthcheck_token` 和 `helianhealthcheck_uid`，以确定当前登录状态。根据登录状态走不同分支。 **查询已有就诊人**：读取桌面隐藏文件夹中的文件 文件路径（跨平台兼容）： - **macOS/Linux**: `~/Desktop/.helian_info/helian_healthcheck_userinfo.md` - **Windows**: `%USERPROFILE%\Desktop\.helian_info\helian_healthcheck_userinfo.md` > **说明**：`.helian_info` 是隐藏文件夹（以点开头的文件夹在 macOS/Linux 中自动隐藏；Windows 中需设置显示隐藏文件才能看到） **文件夹创建代码（如需自动创建）**： ```python import os # 获取跨平台桌面路径 def get_desktop_path(): """获取跨平台的桌面路径""" home = os.path.expanduser('~') # 优先检查常见桌面路径 desktop_paths = [ os.path.join(home, 'Desktop'), # macOS, Linux, Windows os.path.join(home, '桌面'), # 中文 Windows os.path.join(home, 'OneDrive', 'Desktop'), # Windows OneDrive 桌面 ] for path in desktop_paths: if os.path.isdir(path): return path # 默认返回标准 Desktop return os.path.join(home, 'Desktop') # 创建隐藏文件夹 desktop_path = get_desktop_path() helian_folder = os.path.join(desktop_path, '.helian_info') os.makedirs(helian_folder, exist_ok=True) # 文件路径 userinfo_file = os.path.join(helian_folder, 'helian_healthcheck_userinfo.md') ``` ### 3a. 已登录状态（token 已存在） - **文件存在且有记录**：展示就诊人列表，询问用户选择已有记录还是新增 - **文件不存在或为空**：直接提示用户新增体检人 **新增体检人**，收集以下信息（可一次性提供或逐条询问）： - 姓名 - 身份证号 - 手机号 - 性别（男 / 女）→ 转换为数字：性别 1男 2女 0不限 - 婚姻状态（已婚 / 未婚）→ 转换为数字：0未婚 1已婚 2未知 收集完成后，将信息追加写入 `helian_healthcheck_userinfo.md`（位于桌面 `.helian_info` 隐藏文件夹中），格式： ```markdown ## 就诊人：{姓名} - 身份证：{idCard} - 手机号：{phone} - 性别：{sex}（性别 1男 2女 0不限） - 婚姻状态：{married}（0未婚 1已婚 2未知） - 添加时间：{datetime} ``` 体检人信息确定后，直接进入 Step 4（下单前校验）。 ### 3b. 未登录状态（token 不存在） 未登录时，需要让用户一次性提供体检人选择和登录手机号，减少对话轮次。 **3b.1 展示体检人列表并同时提示登录手机号** **场景 A：已有体检人记录时**，展示就诊人列表并提示用户一次性选择体检人 + 登录手机号： ``` ⚠️ 您尚未登录，请选择体检人并提供登录手机号： 已有体检人： 1. 张三（手机号：138xxxx1234） 2. 李四（手机号：139xxxx5678） 请一次性回复，参考格式： - 选已有体检人：输入"体检人序号，登录手机号"，例如 "1，13812345678" - 若登录手机号与体检人一致，可省略：直接输入序号，例如 "1" - 新增体检人：输入"新增"，然后提供姓名、身份证、手机号、性别、婚姻状态和登录手机号 ``` **场景 B：无体检人记录时**，提示用户一次性提供所有信息： ``` ⚠️ 您尚未登录，请提供体检人信息和登录手机号： 请提供以下信息（可一次性输入）： - 姓名、身份证号、手机号、性别、婚姻状态 - 登录手机号（若与体检人手机号一致可不填） ``` **解析用户回复**： - 若用户未单独指定登录手机号，默认使用体检人的手机号作为登录手机号 - 新增体检人时，收集信息包含：姓名、身份证号、手机号、性别（1男 2女 0不限）、婚姻状态（0未婚 1已婚 2未知） - 收集完成后，将信息追加写入 `helian_healthcheck_userinfo.md`（位于桌面 `.helian_info` 隐藏文件夹中），格式： ```markdown ## 就诊人：{姓名} - 身份证：{idCard} - 手机号：{phone} - 性别：{sex}（性别 1男 2女 0不限） - 婚姻状态：{married}（0未婚 1已婚 2未知） - 添加时间：{datetime} ``` **3b.2 登录流程** 体检人信息和登录手机号确定后，执行以下登录流程： 1. 获取图形验证码（**前置校验**）： ```python # 调用图形验证码接口 success, image_or_error = service.get_validate_code_image( phone="{phone}" # 手机号 ) if success: # image_or_error 为 Base64 图片数据 # 展示给用户并要求填写图片中的数字 pass ``` 展示验证码以图片的形式给用户，必须等待用户回复看到的数字后才能继续，严禁AI自动识别验证码。 **提示**：如果图形验证码无法显示，可使用浏览器打开以下链接查看： ``` https://management.helianhealth.com/wx/api/getValidateCode?device_id={phone} ``` 其中 `{phone}` 为用户填写的用于登录的手机号。 **必须同时显示上述提示和验证码图片给用户**。 2. 提示用户填写图片中的验证码，然后才调用发送短信验证码接口，如果用户没有填写前置校验的数字，必须等待。valiImgCode为用户填写的内容： ```python # 调用发送短信验证码接口 result = service.send_sms_code( phone="{phone}", # 手机号 validate_code="{valiImgCode}" # 用户输入的图形验证码 ) if result.success: # 提示："验证码已发送，请查看短信并输入5位验证码" pass ``` 3. 要求用户填写收到的短信验证码，用户填写短信验证码后，调用登录接口： ```python # 调用登录接口 result = service.login_with_sms_code( phone="{phone}", # 手机号 sms_code="{sms_code}" # 短信验证码（5位） ) if result.success: # 登录成功，service 已自动设置 token 和 uid [REDACTED_SECRET]"token") # 保存为 helianhealthcheck_token uid = result.result.get("userid") # 保存为 helianhealthcheck_uid ``` 登录成功后将 `token` 保存为 `helianhealthcheck_token`，`userid` 保存为 `helianhealthcheck_uid`。 **注意** 不要使用这段代码print(f"[REDACTED_SECRET]")，如果需要使用：print(f"[REDACTED_SECRET]") 登录失败时告知用户原因并提供重试选项。 登录成功后，进入 Step 4（下单前校验）。 --- ## Step 4: 提交订单 ### 4a. 下单前校验（依次调用，需设置认证信息） > **前置条件**：Step 3 已完成（体检人信息已确定 + 已登录）。 登录成功后，service 已自动设置认证信息。若手动设置：`service.set_auth(token=token, uid=uid)` **1. 验证是否重复提交**： ```python # 调用重复订单校验接口 result = service.check_repeat_order( age=calculate_age_from_id_card(card_no), # 根据身份证计算的年龄 card_no="{cardNo}", # 身份证号 gender={gender}, # 性别（1=男,2=女） married={married}, # 婚姻状态（0=未婚,1=已婚） mobile="{mobile}", # 手机号 real_name="{姓名}", # 姓名 reserve_day={reserveDay}, # 预约日期时间戳（毫秒） sku_ids=[{skuId}], # ★ SKU ID列表，取自 detail.skuInfo.skuId（不是 skuInfo.id） station_id="{stationId}", # 医院ID pay_amount="{salePrice}", # 支付金额 branch_id={branchId}, # 院区ID，无分院区传0 check_time="{checkTime}" # 时间段（如"05:00-05:30"） ) # result.result 为 false 表示未重复，可以继续下单 ``` **2. 验证性别**： ```python # 调用性别校验接口 result = service.check_gender_match( gender={gender}, # 套餐要求的性别（1=男,2=女） user_gender={userGender}, # 用户性别（1=男,2=女） card_no="{cardNo}", # 身份证号 sku_id={skuId} # ★ SKU ID，取自 detail.skuInfo.skuId（不是 skuInfo.id） ) # result.result 为 true 表示性别匹配，可以继续下单 ``` **3. 验证婚姻状态**： ```python # 调用婚姻状态校验接口 result = service.check_married_status( pkg_id={pkgId}, # 套餐ID item_list=[...itemIds...], # 检查项目ID数组 sex={sex}, # 性别（1=男,2=女） married={married}, # 婚姻状态（0=未婚,1=已婚） station_id="{stationId}" # 医院ID ) # result.result 为 true 表示婚姻状态匹配，可以继续下单 ``` 任意校验失败时告知用户失败原因，不继续下单。 ### 4b. 生成预约单 **调用接口**： ```python # 调用生成预约单接口 result = service.create_reservation( station_id="{stationId}", # 医院ID reserve_day={reserveDay}, # 预约日期时间戳（毫秒，秒级×1000） sku_ids=[{skuId}], # ★ SKU ID列表，取自 detail.skuInfo.skuId（不是 skuInfo.id） user_id="{uid}", # 用户ID（helianhealthcheck_uid） real_name="{姓名}", # 姓名 card_no="{cardNo}", # 身份证号 gender={gender}, # 性别（1=男,2=女） married={married}, # 婚姻状态（0=未婚,1=已婚） age=calculate_age_from_id_card(card_no), # 根据身份证计算的年龄 mobile="{mobile}", # 手机号 branch_id={branchId}, # 院区ID，无分院区传空字符串，有院区传数值类型 check_time="{checkTime}" # 时间段（如"05:00-05:30"） ) if result.success: reservation_id = result.result # 保存预约单号 ``` **字段映射表**： | 接口参数 | 数据来源 | |---------|----------| | `real_name` | Step 3 收集的姓名 | | `mobile` | Step 3 收集的手机号 | | `gender` | Step 3 收集的性别（1=男, 2=女） | | `card_no` | Step 3 收集的身份证号 | | `married` | Step 3 收集的婚姻状态（0=未婚, 1=已婚） | | `age` | 使用 `calculate_age_from_id_card(card_no)` 计算 | | `station_id` | Step 1 选择的医院ID | | `branch_id` | Step 1 选择的分院区ID（无分院区传空字符串，有院区传数值类型） | | `reserve_day` | Step 2 选择的日期时间戳（毫秒，秒级×1000） | | `check_time` | Step 2 选择的时间段（如"05:00-05:30"） | | `sku_ids` | Step 2 套餐的 `detail.skuInfo.skuId`（注意不是 `skuInfo.id`） | | `user_id` | 登录后缓存的 `helianhealthcheck_uid` | 保存返回的预约单号 `reservationId`。 ### 4c. 生成订单 **调用接口**： ```python # 调用生成订单接口 result = service.create_order( sku_id={skuId}, # ★ SKU ID，取自 detail.skuInfo.skuId（不是 skuInfo.id） pay_amount="{salePrice}", # 支付金额 reservation_id={reservationId} # 预约单号（来自Step4b返回） ) if result.success: trade_id = result.result.get("tradeId") # 订单号 trade_result = result.result.get("tradeResult") # 交易结果 need_pay = result.result.get("needPay") # 是否需要支付 ``` **字段映射表**： | 接口参数 | 数据来源 | |---------|----------| | `sku_id` | Step 2 套餐的 `detail.skuInfo.skuId`（注意不是 `skuInfo.id`） | | `pay_amount` | Step 2 套餐详情的 `salePrice` | | `reservation_id` | Step 4b 生成预约单返回的 `reservationId` | **生成成功后展示订单详情**： ``` ✅ 预约成功！ 🏥 医院：{stationName} 📍 医院地址：{stationAddress} 🏢 院区：{branchName}（{branchAddress}） 📋 套餐：{pkgName} 🔬 检查项目：{examItems 逗号拼接} 📅 预约时间：{date} {timeSlot} 👤 体检人：{name}（{idCard}） 📞 手机号：{phone} 💍 婚姻状态：{married} 💰 订单金额：¥{payAmount} 📄 预约单号：{reservationId} 🧾 订单号：{tradeId} 🕐 创建时间：{createTime} 请在30分钟内完成支付！ ``` **生成失败时必须通知用户**： ``` ❌ 订单生成失败！ 失败原因：{接口返回的错误信息或状态码} 预约单号：{reservationId} 请截图保存预约单号，稍后重试或联系人工客服处理。 ``` > **重要**：无论何种原因导致订单生成失败，都必须立即告知用户，不得静默失败或跳过提示。 --- ## Step 5: 选择支付方式 订单生成成功后，提醒用户选择支付方式： ``` 💳 请选择支付方式： 1. 微信支付 / 支付宝支付 2. 银行卡支付 请输入序号选择支付方式： ``` --- ## Step 6a: 微信/支付宝支付流程 ### 6a.1 获取支付凭证 调用 `pay_sign_service.py` 的 `get_pay_sign` 方法： **入参：** ```python credential = get_pay_sign({ "userId": "{helianhealthcheck_uid}", "requestId": "{tradeId}", "orderTime": "{createTime}", "orderAmount": "{payAmount}", "notifyUrl": "https://healthcheck-web-client.helianhealth.com/thirdPlat/payBackForSkill", "goodsName": "{pkgName}" }) ``` ### 6a.2 创建支付订单 **前置检查：** 若上一步获取的支付凭证为空（`not credential`），则告知用户： ``` ❌ 获取支付凭证失败，无法发起支付，请稍后重试或联系客服。 ``` 并终止后续流程，不得调用 `create_payment_order`。 凭证非空时，调用 `mcp_payment_tools.py` 的 `create_payment_order` 方法： **入参：** - `payment_credential`: 上一步获取的支付凭证 - `agent_type`: Agent 类型，从 skill 智能体获取（OPENCLAW/COPAW/OTHER），获取不到传空字符串 - `scene_type`: 场景，从 skill 智能体获取（MOBILE/PC/HEADLESS），获取不到传空字符串 - `terminal_env`: 终端环境，从 skill 智能体获取（WECHAT/ALIPAY/FEISHU/DINGTALK/BROWSER），获取不到传空字符串 **调用示例：** ```python from mcp_payment_tools import create_payment_order # 前置检查：支付凭证不能为空 if not credential: print('❌ 获取支付凭证失败，无法发起支付，请稍后重试或联系客服。') else: # 创建支付订单，返回 PaymentMethod 对象列表 methods = create_payment_order( payment_credential=credential, agent_type='', scene_type='', terminal_env='' ) if methods: # 成功返回支付方式列表 print('✅ 支付订单已创建！') for i, method in enumerate(methods, 1): if 'ALIPAY' in method.pay_type: print(f'{i}. 支付宝支付') elif 'WECHAT' in method.pay_type: print(f'{i}. 微信支付') print(f' 🔗 支付链接：{method.pay_url}') print(f' 📝 说明：{method.pay_description}') else: # 失败返回 None print('❌ 支付订单创建失败') ``` **注意：** `create_payment_order` 返回的是 `List[PaymentMethod]` 对象列表或 `None`，不是字典。每个 `PaymentMethod` 对象包含以下属性： - `pay_type`: 支付方式类型（如 ALIPAY_NATIVE, WECHAT_APPLET） - `pay_order_id`: 支付订单号 - `pay_url`: 支付链接 - `pay_description`: 支付说明 ### 6a.3 展示支付链接 ``` ✅ 支付订单已创建！ 请选择支付方式并完成支付： {序号}. {支付方式名称} 🔗 支付链接： ``` {payUrl} ``` 请复制上方完整链接地址，粘贴到对应 APP 中打开完成支付。 支付完成后，请输入"查询支付状态"查看支付结果。 ``` --- ## Step 6b: 银行卡支付流程 ### 6b.1 读取银行卡信息 读取桌面隐藏文件夹中的文件 `helian_healthcheck_bankinfo.md`（位于桌面 `.helian_info` 隐藏文件夹中）： 文件路径（跨平台兼容）： - **macOS/Linux**: `~/Desktop/.helian_info/helian_healthcheck_bankinfo.md` - **Windows**: `%USERPROFILE%\Desktop\.helian_info\helian_healthcheck_bankinfo.md` **文件存在且有记录：** - 解析文件中的银行卡信息列表 - 展示给用户选择： ``` 💳 请选择已保存的银行卡： {序号}. {持卡人姓名} - {银行卡号后四位} 身份证：{cardIdNo} 手机号：{cardPhone} 或输入"新增"添加新银行卡 ``` **文件不存在或为空：** - 提示用户："未找到银行卡信息，请先添加银行卡" - 进入收集银行卡信息流程 ### 6b.2 收集银行卡信息（新增时） 收集以下信息（可一次性提供或逐条询问）： - **持卡人姓名**（cardName） - **持卡人身份证号**（cardIdNo） - **持卡人手机号**（cardPhone）- 银行预留手机号 - **银行卡号**（cardNo） 收集完成后，将信息追加写入 `helian_healthcheck_bankinfo.md`（位于桌面 `.helian_info` 隐藏文件夹中），格式： ```markdown ## 银行卡：{持卡人姓名} - 卡号：{cardNo} - 持卡人：{cardName} - 身份证：{cardIdNo} - 预留手机号：{cardPhone} - 添加时间：{datetime} ``` ### 6b.3 获取支付凭证 调用 `pay_sign_service.py` 的 `get_pay_sign` 方法： **入参（用户选择已有银行卡且该记录包含 agreeNo 时）：** ```python credential = get_pay_sign({ "userId": "{helianhealthcheck_uid}", "requestId": "{tradeId}", "orderTime": "{createTime}", "orderAmount": "{payAmount}", "notifyUrl": "https://healthcheck-web-client.helianhealth.com/thirdPlat/payBackForSkill", "goodsName": "{pkgName}", "agreeNo": "{协议号}" }) ``` **入参（用户选择已有银行卡但该记录不包含 agreeNo，或新增银行卡时）：** ```python credential = get_pay_sign({ "userId": "{helianhealthcheck_uid}", "requestId": "{tradeId}", "orderTime": "{createTime}", "orderAmount": "{payAmount}", "notifyUrl": "https://healthcheck-web-client.helianhealth.com/hirdPlat/payBackForSkill", "goodsName": "{pkgName}", "cardNo": "{银行卡号}", "cardName": "{持卡人姓名}", "cardPhone": "{银行预留手机号}", "cardIdType": "ID_CARD", "cardIdNo": "{持卡人身份证号}" }) ``` **逻辑说明：** - 当用户选择了 `helian_healthcheck_bankinfo.md`（位于桌面 `.helian_info` 隐藏文件夹中）中的银行卡记录，并且该记录中包含 `agreeNo` 字段时，使用简化入参（仅需 `agreeNo`，无需银行卡详细信息） - 当用户选择的银行卡记录不包含 `agreeNo`，或用户新增银行卡时，使用完整入参（包含银行卡详细信息） ### 6b.4 创建支付订单 **前置检查：** 若上一步获取的支付凭证为空（`not credential`），则告知用户： ``` ❌ 获取支付凭证失败，无法发起支付，请稍后重试或联系客服。 ``` 并终止后续流程，不得调用 `create_payment_order`。 凭证非空时，调用 `mcp_payment_tools.py` 的 `create_payment_order` 方法： **入参：** - `payment_credential`: 上一步获取的支付凭证 - `agent_type`: Agent 类型，从 skill 智能体获取，获取不到传空字符串 - `scene_type`: 场景，从 skill 智能体获取，获取不到传空字符串 - `terminal_env`: 终端环境，从 skill 智能体获取，获取不到传空字符串 **调用示例：** ```python import json from mcp_payment_tools import create_payment_order # 前置检查：支付凭证不能为空 if not credential: print('❌ 获取支付凭证失败，无法发起支付，请稍后重试或联系客服。') else: # 创建支付订单，返回 PaymentMethod 对象列表 methods = create_payment_order( payment_credential=credential, agent_type='', scene_type='', terminal_env='' ) if methods: # 成功返回支付方式列表 # 银行卡支付只有一条记录，取第一个 method = methods[0] # pay_description 可能是 JSON 字符串，需要解析 pay_desc = method.pay_description agreement_name = '' agreement_url = '' card_phone = '' # 持卡人预留手机号，从 6b.2/6b.3 收集的信息中获取 try: desc_obj = json.loads(pay_desc) if isinstance(pay_desc, str) else pay_desc agreement_name = desc_obj.get('agreementName', '') agreement_url = desc_obj.get('agreementUrl', '') except (json.JSONDecodeError, AttributeError): # 若不是 JSON，直接使用原始字符串 agreement_name = pay_desc print(f'✅ 短信验证码已发送，等待用户输入') else: # 失败返回 None print('❌ 支付订单创建失败') ``` **注意：** `create_payment_order` 返回的是 `List[PaymentMethod]` 对象列表或 `None`。银行卡支付场景下，每个 `PaymentMethod` 对象的 `pay_description` 字段是一个 JSON 字符串，包含 `agreementName`（协议名称）和 `agreementUrl`（协议链接），**必须先 `json.loads` 解析后才能获取这两个字段**。 ### 6b.5 等待短信验证码 创建订单成功后，连连支付会向持卡人预留手机号发送短信验证码，并返回支付服务协议链接。使用上一步解析出的变量展示： ``` 📱 短信验证码已发送至 {card_phone} 📋 支付服务协议： 名称：{agreement_name} 链接：{agreement_url} ⚠️ 请先阅读上述支付服务协议，阅读后请输入6位短信验证码完成支付： ``` ### 6b.6 确认银行卡支付 用户输入验证码后： **1. 获取确认支付凭证** 调用 `pay_sign_service.py` 的 `get_pay_sign` 方法： **入参：** ```python credential = get_pay_sign({ "requestId": "{tradeId}" }) ``` **2. 调用确认支付接口** 调用 `mcp_payment_tools.py` 的 `confirm_bankcard_pay` 方法： **入参：** - `payment_credential`: 上一步获取的支付凭证 - `sms_code`: 用户输入的6位短信验证码 **3. 处理支付协议号（agreeNo）** - 如果 6b.6 确认银行卡支付的返回中存在 `agreeNo` 字段，则将该 `agreeNo` 绑定到 `helian_healthcheck_bankinfo.md`（位于桌面 `.helian_info` 隐藏文件夹中）中对应的银行卡记录（匹配 6b.3 获取支付凭证时使用的 `cardNo` 对应的记录） - 在银行卡记录中添加： ```markdown - 协议号：{agreeNo} - 协议绑定时间：{datetime} ``` - 如果 6b.5 等待短信验证码的返回中不存在 `agreeNo`，则不进行绑定操作 **4. 支付结果** ``` ✅ 银行卡支付已提交！ 请稍后输入"查询支付状态"查看支付结果。 ``` --- ## Step 7: 查询支付状态 当用户输入"查询支付状态"时： ### 7.1 获取查询凭证 调用 `pay_sign_service.py` 的 `get_pay_sign` 方法： **入参：** ```python credential = get_pay_sign({ "requestId": "{tradeId}" }) ``` ### 7.2 查询支付状态 调用 `mcp_payment_tools.py` 的 `query_pay_status` 方法： **入参：** - `payment_credential`: 上一步获取的支付凭证 ### 7.3 展示支付结果 根据返回状态展示结果： ``` 📋 支付状态查询结果： 订单号：{tradeId} 支付状态：{status} 状态说明： - WAITING_PAY: 等待支付 - PAYING: 支付中 - PAY_SUCCESS: 支付成功 ✅ - PAY_FAIL: 支付失败 ❌ - CLOSED: 订单已关闭 - REFUNDING: 退款中 - REFUNDED: 已退款 ``` --- ## 错误处理 | 场景 | 处理方式 | |------|---------| | API 超时 / 网络异常 | 告知"接口响应异常，请稍后重试"，提供重试选项 | | 返回空列表 | 告知"暂无数据"，询问是否更换条件 | | 图形验证码获取失败 | 告知用户，提供刷新重试 | | 短信发送失败 | 告知失败原因，可重新尝试 | | 登录失败 | 告知验证码错误，提示重新获取 | | 号源已满 | 提示"该时段已约满"，引导选择其他时段 | | 下单前校验失败 | 告知具体校验失败原因（重复/性别/婚姻状态不符） | | 预约单/订单创建失败 | 告知失败原因，提供人工客服联系方式 | | 用户提到退款、退费、取消订单、投诉、售后等 | 回复："很抱歉给您带来的困扰，您可拨打客服热线4000017505解决，工作日07:00-17:30，周末节假日08:00-17:30。" | ---


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)