欢迎光临
我们一直在努力

视频号 POI 团购系统接入

视频号 POI 团购系统接入实战:高德 ID 映射、核销逆向与门店分账落地

说明:本文内容基于棱镜智汇作为微信官方认证视频号团购服务商的实际落地经验整理,所有业务场景均经过线上生产环境验证。以下代码为通用演示示例,仅用于展示业务实现思路,不代表企业线上生产源码,实际接入时请结合自身技术栈和微信支付最新文档进行调整。
做本地生活 SaaS 的团队接入视频号团购时,最容易踩坑的不是接口本身,而是 POI 地址映射偏差、核销退款逆向链路、多门店维度分账这三块视频号特有的业务场景。本文从实际落地视角拆解完整对接方案,并附上核心代码实现。

一、POI 门店认领与高德 ID 映射机制

视频号 POI 底层地址库完全复用高德地图数据,门店认领的本质是把商家自有门店 ID 和高德 POI ID 建立绑定关系。这一步是后续所有团购挂载、核销、对账的基础,也是线上故障最高发的环节。

常见三类映射错误:

  • 同名门店匹配到错误地址(连锁品牌尤其严重);
  • 门店搬迁后高德地址未更新,导致 POI 定位偏移;
  • 一门店多 POI 重复认领,核销数据分散统计。
  • 推荐映射方案:三级匹配+人工兜底

    package poi

    import (
    "context"
    "fmt"
    "time"
    )

    // PoiMatcher POI匹配器
    type PoiMatcher struct {
    amapClient *amap.Client
    similarity *textSimilarity
    manualQueue chan<- *StoreInfo
    }

    // MatchResult 匹配结果
    type MatchResult struct {
    PoiID string `json:"poi_id"`
    Source string `json:"source"` // auto / manual
    Confidence float64 `json:"confidence"` // 0-1
    MatchedAt time.Time `json:"matched_at"`
    Error string `json:"error,omitempty"`
    }

    // Match 三级匹配主入口
    func (m *PoiMatcher) Match(ctx context.Context, store *StoreInfo) *MatchResult {
    // 第一级:精确经纬度 + 名称全字匹配
    pois, err := m.amapClient.SearchNearby(ctx, store.Name, store.Lng, store.Lat, 500)
    if err != nil {
    return &MatchResult{Error: fmt.Sprintf("高德接口异常: %v", err)}
    }
    if len(pois) == 1 {
    return &MatchResult{PoiID: pois[0].ID, Source: "auto", Confidence: 1.0, MatchedAt: time.Now()}
    }

    // 第二级:地址文本相似度(>0.85 自动绑定)
    for _, poi := range pois {
    sim := m.similarity.Calc(store.Address, poi.Address)
    if sim > 0.85 {
    return &MatchResult{PoiID: poi.ID, Source: "auto", Confidence: sim, MatchedAt: time.Now()}
    }
    }

    // 第三级:转人工审核(异步非阻塞)
    select {
    case m.manualQueue <- store:
    default:
    return &MatchResult{Error: "人工审核队列已满,请稍后重试"}
    }
    return &MatchResult{Source: "manual", Confidence: 0, MatchedAt: time.Now()}
    }

    核心原则:宁可不匹配走人工审核,也不要自动绑定到错误 POI——挂错定位带来的核销投诉和客诉成本远高于人工审核。

    二、团购券核销与售后退款双链路设计

    视频号本地生活核销订单不支持直接调用支付退款接口,需根据核销时长区分两套逆向流程,核心分为「冻结期撤销核销退款」「已结算售后维权退款」两套独立链路。

    完整券状态流转:

    已创建 → 已支付 → 可核销 → 已核销(7 天冻结结算期)

    7 天内:撤销核销退回可核销

    核销满 7 天:进入 15 天售后维权期(券不可恢复)

    1. 冻结期撤销核销流程(核销 7 日内)

    用户与商家协商全额退款时,优先调用撤销核销接口,将券恢复未核销状态后再执行退款。

    package revoke

    import (
    "context"
    "fmt"
    "time"
    "github.com/go-resty/resty/v2"
    )

    const (
    RevokeAPI = "https://p.wecard.tencent.com/cloudpay/v1/pay/ec/voucher/revoke"
    )

    type RevokeRequest struct {
    RevokeRequestNo string `json:"revoke_request_no"`
    RevokeVouchers []RevokeVoucherItem `json:"revoke_vouchers"`
    }

    type RevokeVoucherItem struct {
    VerifyID string `json:"verify_id"`
    }

    type RevokeResponse struct {
    Code int `json:"code"`
    Message string `json:"message"`
    Data struct {
    RevokeID string `json:"revoke_id"`
    } `json:"data"`
    }

    // RevokeVerify 撤销核销(幂等 + 超时)
    func RevokeVerify(ctx context.Context, verifyID string) (*RevokeResponse, error) {
    // 幂等键:业务单号 + 操作类型
    idempotentKey := fmt.Sprintf("revoke:%s", verifyID)
    if ok, _ := redis.SetNX(ctx, idempotentKey, "processing", 30*time.Second); !ok {
    return nil, fmt.Errorf("重复请求,请勿重试")
    }
    defer redis.Del(ctx, idempotentKey)

    req := &RevokeRequest{
    RevokeRequestNo: fmt.Sprintf("rev_%s_%d", verifyID, time.Now().UnixNano()),
    RevokeVouchers: []RevokeVoucherItem{{VerifyID: verifyID}},
    }

    client := resty.New().SetTimeout(5 * time.Second)
    var resp RevokeResponse
    httpResp, err := client.R().
    SetContext(ctx).
    SetHeader("Content-Type", "application/json").
    SetBody(req).
    SetResult(&resp).
    Post(RevokeAPI)

    if err != nil {
    return nil, fmt.Errorf("调用撤销接口失败: %w", err)
    }
    if httpResp.StatusCode() != 200 {
    return nil, fmt.Errorf("撤销接口返回非200: %d", httpResp.StatusCode())
    }
    if resp.Code != 0 {
    return nil, fmt.Errorf("撤销失败: %s", resp.Message)
    }
    return &resp, nil
    }

    撤销成功后,券重置为待使用,复用普通未核销订单退款逻辑,支持全额、部分退款。

    2、已结算售后退款流程(核销超 7 天)

    资金解冻至商家可提现余额后,无法撤回核销记录,统一通过售后单体系流转处理退款:

    from dataclasses import dataclass
    from typing import Optional, Dict
    from decimal import Decimal
    import logging

    logger = logging.getLogger(__name__)

    @dataclass
    class AftersaleResult:
    success: bool
    aftersale_id: str
    refund_amount: Decimal
    error: Optional[str] = None

    def handle_aftersale_audit(aftersale_data: Dict) > AftersaleResult:
    """
    售后审核通过处理退款
    前提:调用本函数前,商家已在后台点击"同意售后"
    """

    aftersale_id = aftersale_data["aftersale_id"]
    refund_fee = Decimal(aftersale_data["refund_fee"])

    # 1. 幂等检查(本地已处理过直接返回)
    exist = db.query_aftersale_record(aftersale_id)
    if exist:
    return AftersaleResult(
    success=True,
    aftersale_id=aftersale_id,
    refund_amount=exist["refund_amount"],
    error=None
    )

    # 2. 前置校验:售后单当前状态是否允许退款
    aftersale_status = wechat_api.query_aftersale_status(aftersale_id)
    if aftersale_status not in ("WAIT_AGREE", "AGREED"):
    return AftersaleResult(
    success=False,
    aftersale_id=aftersale_id,
    refund_amount=refund_fee,
    error=f"售后单状态异常,当前状态: {aftersale_status}"
    )

    # 3. 按原分账比例冲减各门店台账
    verify_record = db.get_verify_by_order(aftersale_data["order_id"])
    if not verify_record:
    logger.error(f"核销记录不存在,order_id={aftersale_data['order_id']}")
    return AftersaleResult(
    success=False,
    aftersale_id=aftersale_id,
    refund_amount=refund_fee,
    error="未找到对应核销记录"
    )

    ratio_map = get_split_ratio(verify_record.split_details)
    for account, ratio in ratio_map.items():
    rollback = refund_fee * Decimal(str(ratio))
    db.update_ledger(account, rollback)

    # 4. 调用微信售后同意接口(平台自动扣款并原路退回)
    try:
    wechat_api.agree_aftersale(aftersale_id)
    except Exception as e:
    # 失败回滚台账
    db.rollback_ledger(account, rollback)
    return AftersaleResult(
    success=False,
    aftersale_id=aftersale_id,
    refund_amount=refund_fee,
    error=f"调用售后同意接口失败: {e}"
    )

    # 5. 记录售后对账流水
    db.insert_aftersale_record(aftersale_id, refund_fee, verify_record.id)

    return AftersaleResult(
    success=True,
    aftersale_id=aftersale_id,
    refund_amount=refund_fee,
    error=None
    )

    该流程不会改变团购券已核销状态,仅做财务冲销对账,无券复用能力。

    3. 对账核心注意点

  • 撤销核销退款:核销记录反向冲销,核销流水、退款流水成对勾兑;
  • 售后维权退款:单独生成售后对账记录,与原核销订单关联对账;
  • T+1 拉取微信支付资金账单时,需区分「团购商品退款」、「售后扣款退款」两类流水,建议本地分别打标,按订单号/售后单号分类勾稽(勾兑),避免账实不符。
  • 三、门店维度分账与总部对账设计

    连锁品牌做视频号团购,最核心的诉求是按门店维度分账、总部统一对账。视频号官方只提供商户号维度的资金结算,门店级分账需要服务商系统自行实现。

    推荐分账架构:虚拟账户 + 日终清算

  • 每笔核销订单实时入账至对应门店虚拟账户;
  • 日终批量生成门店清算单,从总部商户号提现后划转至各门店;
  • 资金边界:所有资金始终在微信支付体系内流转,服务商仅做记账,不触碰实际资金。
  • 对账三层校验:

    层级对账对象对账频率差异处理
    第一层 核销订单 vs 微信支付流水 准实时 自动补单
    第二层 门店分账台账 vs 总部结算金额 日终 差异工单
    第三层 总部提现金额 vs 银行到账金额 T+1 财务人工复核

    四、类目报白的技术对接要点

    高风险类目(美业、医美、教培、旅游)必须通过服务商通道报白,技术侧有几个容易忽略的细节:

  • 主体资质与门店资质分离提交:总部营业执照、品牌授权是主体资质,每家门店的卫生许可证、办学许可证是门店资质,不能混在一起上传;
  • 审核状态回调幂等:同一门店可能多次提交、多次回调,必须以最新一次审核结果为准;
  • 报白通过后商品类目同步有延迟:通常审核通过后2-4小时才能正常上架对应类目商品,立即上架会报“类目无权限”错误。
  • 五、全链路监控与告警

    视频号团购的故障影响面比普通支付更广——不仅影响收款,还影响用户到店核销体验,容易引发客诉。建议重点监控三个指标:

  • POI 认领成功率(低于 95% 告警,通常是高德接口限流或地址库更新);
  • 核销回调成功率(低于 99% 告警,可能是接口证书过期或网络问题);
  • 退款对账差异率(高于 0.1% 告警,可能是逆向链路漏处理)。
  • 总结

    视频号 POI 团购系统对接的核心难点,集中在 POI 地址映射、核销退款逆向链路、门店分账这三个视频号独有的业务场景上,而非通用的支付接口调用。团队接入时建议优先把这三块的容错和对账机制做扎实,再逐步扩展商品管理、营销玩法等上层功能,避免前期只关注“能跑通流程”、后期线上故障频发的被动局面。

    赞(0)
    未经允许不得转载:171主机测评 » 视频号 POI 团购系统接入
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址