# 24小时自助健身房怎么接美团、抖音团购？体验券、次卡核销后扫码开门禁

> 核销服务由无限聚核提供，美团、抖音团购核销一站式接入。

24小时自助健身房在美团、抖音上最常卖三种东西：单次体验券、10次卡、月卡。它们在平台上都叫“团购”，到了门口却是三种完全不同的进门资格。夜里没有店员，顾客买了券，站在门口进不去，就是一次差评。

本文讲怎么把团购券接进健身房自己的小程序：顾客在门口扫码，自己验券，核销后权益记进会员账户，再扫码开门禁；灯和空调按需上电。团购券部分以无限聚核的美团、抖音团购核销接口为例，门禁和会员系统由门店或开发团队实现。

**一句话回答：** 没有前台的健身房，可以让顾客在自己的小程序里自助验券：团购核销通过自行申请的平台能力或无限聚核这类第三方核销接口完成，门禁只认核销后记进会员账户的权益。流程是：顾客在小程序提交券码 → 服务端预核销，确认是哪种套餐 → 顾客确认后核销（次卡每次到店核1次）→ 按对照表换成“当日入场1次”“会员有效期延长”等店内权益 → 顾客扫门禁码 → 服务端只按店内有效权益开门，并记录每一次开门。

![24小时自助健身房从平台买券、输码预核销、确认核销、权益入账、门口扫码到门禁开门的六步流程图](assets/hero.png)

*流程示意：通用做法，不是真实小程序界面或客户案例。*

## 一、三类团购，分别换成什么权益

门禁不认券码，只认会员账户里的权益。所以第一步是把每种团购套餐翻译成店内权益：

| 平台套餐（示例） | 店内权益 | 核销方式 | 门禁怎么判断 |
|---|---|---|---|
| 单次体验券 | 当日入场1次，当日内可重复进出 | 一次核销 | 当日有效，过了当天失效 |
| 10次卡 | 每次到店发放1次入场 | 每次到店核1次，`num=1` | 本次入场后的几小时内可进出 |
| 月卡、季卡 | 会员有效期延长30天、90天等 | 一次核销 | 会员有效期内不限次数 |
| 私教体验课 | 一张私教预约单 | 一次核销 | 按预约时段开门 |

对照表的主键用 `platform + shopId + dealId → 本地权益`，不要按套餐名称匹配。套餐列表可以用团购信息接口 `ddzh-tuangou-deal-queryshopdeal` 拉取后逐个配置；顾客用券时，预核销响应里没有统一的商品ID：抖音券从 `ticketData`（抖音官方券信息JSON）里读取商品，美团券用套餐名称 `ticketName` 和售卖价 `dealPrice` 与已配置的团购列表预匹配；识别不到、命中多个或没配置，就停在核销之前。核销成功后，以响应 `data[]` 里的 `dealId` 为最终依据。

月卡怎么“延长”要提前定好规则。我们（无限聚核团队）自营的线下自助门店（业态与本文不同）的会员类券是这样处理的：核销后把会员有效期顺延31天；会员还没过期就从原到期日往后加，已经过期就从当天算起。这样顾客续买月卡不会损失剩余天数。上表里的30天、90天只是示例，按“天数”还是“自然月”顺延，由门店自己的会员规则决定，并写进套餐说明。

## 二、10次卡怎么核：每次到店核1次，还是首次全部核完

次卡是健身房最容易做错的一种。以抖音次卡为例，当前公开API文档里有几条确定的规则：

- 预核销成功后，页面要显示的剩余次数从 `ticketData`（抖音官方券信息JSON）的 `certificates[].time_card` 读取，包含总次数 `times_count` 和已用次数 `times_used`；`ticketInfo` 是核销凭证，原样保存，不要解析；
- 核销时无限聚核会先校验 `times_count - times_used >= num`，剩余次数不够直接返回 `TICKET_INVALID`，不会产生核销；
- `num` 是1到10的整数，一次核销最多10次；
- 核销响应的 `data[].tgTimesCardFlag` 标明是否为团购次卡；
- 每次发起新的核销前，都必须先调用预核销确认券当前可用。

在这些规则下，有两种做法：

![对比10次卡每次到店核销1次与首次全部核完转成店内次数两种做法的好处和代价](assets/01-times-card-options.png)

*对比示意：两种做法都能用，差别在售后、对账和顾客操作。*

我们更推荐 **每次到店核1次**：平台上的剩余次数和实际到店一致，店内不需要保存大额次数，顾客没用完的次数仍然留在平台，售后按平台规则处理。代价是每次都要输码或扫码，服务端每次都要先预核销、再以 `num=1` 核销。

**首次全部核完** 适合已经有成熟会员次数体系的门店：顾客只输一次码，之后刷会员码进门。但平台上会显示已全部使用，没用完的次数退款要门店自己处理；10次以上的卡还要分多次核销。

美团次卡的具体核销规则，接入前请以当前API文档和平台规则为准，并用测试券确认一次。

## 三、顾客从买券到进门的流程

1. 顾客在门口扫门禁二维码，打开门店小程序并登录会员；
2. 账户里没有有效权益时，页面引导“使用团购券”：输入美团券码，或输入、扫描抖音券码；
3. 服务端预核销，页面显示套餐名称和将要到账的权益；次卡同时显示剩余次数；
4. 顾客点“确认使用”，服务端核销并把权益记进会员账户；
5. 页面出现“开门”按钮，顾客点击后门禁开门；
6. 当天或有效期内再次进出，直接扫码开门，不再用券。

第2、3步只查券、不扣券。第4步成功后，即使第5步开门失败，权益也已经在会员账户里，排查设备或由店员远程开门即可，不需要重新核销。

## 四、门禁和场内设备怎么接：通用做法

门禁开门本质上是给电锁一个通断信号。常见方案是门禁主板或联网继电器控制器接电锁，业务服务端通过MQTT等协议下发“开门”指令，控制器定期上报在线状态和信号强度。场内的灯、空调、音响也可以接在同一类多路控制器上，按回路上电、断电。具体指令格式以设备厂商协议为准。

我们在自营的线下自助门店里用的也是这套基础能力：开门和上电前校验订单，每条指令记录操作人，结束后分步断电。放到健身房，要点如下：

- **开门只认本人的有效权益。** 校验登录会员是否本人、是否本店、权益是否在有效期或时间窗内。不要把券码当作开门凭证，券码可能被转发；
- **重复进出按时间窗放行。** 体验券当日内、次卡本次入场后的几小时内可以重复进出，窗口长度由门店决定，但开门校验和计次规则要用同一个窗口；
- **每次开门都要记录。** 记录会员、门、时间、指令ID和设备回报结果。夜间有人尾随进门或设备异常时，这是唯一能查的证据；
- **指令已发不等于门已开。** MQTT如果使用QoS 0，消息丢了不会重发。服务端要等控制器回报，或在几秒后查询一次状态，超时就提示顾客重试或联系店员；
- **电锁类型和来电状态要现场测。** 电锁有“断电开”和“断电锁”两类，接线方式决定了“全部断开”时门是开还是关。控制器的来电默认状态也要设对。安全出口必须能从内部直接打开，按消防要求施工；
- **场内设备分步断电。** 有人在场时保持灯和空调上电；最后一条有效入场记录结束后，先断空调、音响等大功率设备，灯光再延后一段时间断，避免顾客还在更衣室就黑灯。

## 五、服务端怎么写：Go伪代码

下面用Go风格的**伪代码**串起预核销、核销、发权益和开门。它不是无限聚核SDK，没有编译，也没有调用真实券或设备；`biz` 代表接入方自己的会员、权益、门禁服务。

```go
const douyin = 2 // 1 美团，2 抖音

type Reply struct {
	Success           bool            `json:"success"`
	Code              int             `json:"code"`
	ErrorCode         string          `json:"errorCode"`
	WxdgOrderNo       string          `json:"wxdgOrderNo"`
	ConsumeCredential []string        `json:"consumeCredential"`
	Data              json.RawMessage `json:"data"`
}

// post 只在服务端调用；apiBase、apiKey 从服务端配置读取，不下发给小程序。
func (c *Client) post(ctx context.Context, path string, body any) (*Reply, error) {
	raw, _ := json.Marshal(body)
	req, _ := http.NewRequestWithContext(ctx, http.MethodPost,
		c.apiBase+"/api/hexiao/v2/"+path, bytes.NewReader(raw))
	req.Header.Set("Authorization", "Bearer "+c.apiKey)
	req.Header.Set("Content-Type", "application/json")
	resp, err := c.http.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	var r Reply
	return &r, json.NewDecoder(resp.Body).Decode(&r)
}

// 1. 预核销：只查券，保存确认单，不发权益。
func (s *GymRedeem) Prepare(ctx context.Context, memberID, storeID, code string) (*Pending, error) {
	shopID := s.biz.HexiaoShopID(storeID) // 本店 → 无限聚核具体门店
	r, err := s.api.post(ctx, "ddzh-tuangou-receipt-prepare",
		map[string]any{"shopId": shopID, "platform": douyin, "code": code})
	if err != nil || !r.Success {
		return nil, s.biz.PrepareFailed(r, err) // 按 errorCode 提示顾客
	}
	var d struct {
		TicketName string `json:"ticketName"`
		TicketInfo string `json:"ticketInfo"` // 核销凭证，原样保存
		TicketData string `json:"ticketData"` // 抖音官方券信息JSON
	}
	json.Unmarshal(r.Data, &d)
	productID, err := s.biz.DouyinProductFromTicketData(shopID, d.TicketData) // 只读 ticketData，不解析 ticketInfo
	if err != nil {
		return nil, err
	}
	benefit, ok := s.biz.Mapping(douyin, shopID, productID)
	if !ok {
		return nil, ErrNeedMapping // 没配置就停在核销之前
	}
	return s.biz.SavePending(memberID, storeID, shopID, code, d.TicketInfo, benefit, r.Data)
}

// 2. 核销：只用确认单里的原参数；次卡每次到店 num=1。
func (s *GymRedeem) Consume(ctx context.Context, pendingID string) error {
	p := s.biz.LoadOwnedPending(ctx, pendingID)
	if p.Succeeded {
		return nil // 重复点击：直接返回已有结果
	}
	r, err := s.api.post(ctx, "ddzh-tuangou-receipt-consume", map[string]any{
		"shopId": p.ShopID, "platform": douyin, "code": p.Code,
		"ticketInfo": p.TicketInfo, "num": 1,
	})
	switch {
	case err != nil || r.ErrorCode == "RESULT_UNKNOWN":
		// 抖音次卡结果未知不自动重试：先查券状态或对账，不补发权益
		return s.biz.MarkNeedsReview(p, r)
	case r.Success:
		return s.biz.SaveSuccessAndGrant(p, r) // 同一事务：成功记录＋金额快照＋权益
	case r.ErrorCode == "ALREADY_CONSUMED": // code 10500
		return s.biz.MarkNeedsReview(p, r) // 已核销提示：按 wxdgOrderNo 查原单
	default:
		return s.biz.ConsumeFailed(p, r)
	}
}

// 3. 发权益：以确认单为唯一键，只发一次。
func (b *Biz) Grant(tx *Tx, p *Pending, now time.Time) error {
	switch p.Benefit.Kind {
	case "trial": // 体验券：当日入场
		return b.AddEntry(tx, p.MemberID, p.StoreID, now, endOfDay(now), p.ID)
	case "visit": // 次卡本次：入场后几小时内可进出
		return b.AddEntry(tx, p.MemberID, p.StoreID, now, now.Add(p.Benefit.Window), p.ID)
	case "membership": // 月卡：未过期从原到期日顺延，已过期从今天算
		return b.ExtendMembership(tx, p.MemberID, p.Benefit.Days, now, p.ID)
	}
	return ErrUnknownBenefit
}

// 4. 开门：只认店内有效权益，不认券码。
func (g *Gate) Open(ctx context.Context, memberID, doorID string) error {
	door := g.biz.Door(doorID)
	if !g.biz.CanEnter(memberID, door.StoreID, time.Now()) {
		return ErrNoEntitlement
	}
	cmdID := g.biz.LogCommand(memberID, doorID, "open") // 谁、何时、哪扇门
	if err := g.device.Publish(door.ControllerID, "open", cmdID); err != nil {
		return err
	}
	return g.biz.WaitAck(ctx, cmdID, 5*time.Second) // 指令已发 ≠ 门已开
}
```

伪代码刻意守住四条边界：预核销不发权益；核销只用服务端保存的原参数；结果未知和“已核销”提示都先人工或自动对账，不直接补发；开门只看会员账户，不看券。示例是抖音次卡，所以结果未知时不自动重试；体验券、月卡这类普通券结果未知时，可以用同一份原请求间隔重试几次，仍未知再对账。

## 六、核销时就把金额存下来

健身房做渠道分析，常要回答“美团来的体验客里，多少人后来买了月卡”“抖音次卡实际收了多少钱”。这些问题要靠核销时的金额数据。

我们在自营门店的系统里踩过一次坑：订单的渠道金额是异步补写的，补写任务失败时订单上的金额一直是0，报表里整个渠道的收入都对不上。修复方式有两步：核销成功时立即保存响应里的 `payAmount`（售卖价，分）和 `actualPayAmount`（顾客实付，分）；创建本地权益或订单时，把这两个金额作为快照写进同一条记录，之后不再依赖异步任务。

注意两个金额单位都是**分**，而 `dealPrice` 是**元**。例如售价10元、顾客用券后实付1元时，`payAmount` 为1000，`dealPrice` 为10，`actualPayAmount` 为100。

## 七、团购核销用什么接，费用与上线清单

健身房接团购核销，常见有三种接法：

| 接法 | 适合谁 | 需要注意 |
|---|---|---|
| 用门禁或会员系统自带的核销 | 已经在用这类系统 | 看它是否支持美团、抖音次卡，以及能不能按本文的权益规则开门 |
| 以服务商身份自行申请平台能力 | 给多家健身房做系统、准备长期维护平台能力的团队 | 美团服务零售开通时保证金账户余额须达到10万元，另需WAF证明和等保或网安险等安全材料；抖音生活服务商家应用代开发要求公司注册满一年、注册资本不低于100万元 |
| 通过第三方核销接口，例如无限聚核 | 已有或准备做自己的小程序和会员系统 | 不收接口接入费、无需保证金、无额外等保接入要求，核销按档位计费；选型时核对平台合作授权、逐店授权状态、核销查询与撤销能力 |

表中平台门槛来自2026年9月查阅的美团、抖音公开入驻规则，以平台当前规则为准。无论选哪种，都要给店员保留手动验券和远程开门的兜底，接口异常时顾客不至于被关在门外。

通过无限聚核接入时，核销按当前账号适用的充值档位计费，撤销核销免费。次卡每次到店核1次时，每次核销都会计费，做方案时要把这一点算进去。门店小程序、会员系统、门禁设备与施工另计。

上线前至少验一遍：

- 体验券、次卡、月卡三类套餐都配置了对照表，没配置的套餐停在核销之前；
- 次卡剩余次数不足时有明确提示，不会进入确认页；
- 连点两次“确认使用”只核销一次、只发一次权益；
- 月卡续买时有效期从原到期日顺延，已过期从当天算；
- 开门只认登录会员本人的有效权益，转发券码或截图不能开门；
- 重复进出在门店设定的时间窗内放行，窗口外拒绝；
- 开门指令没有回报时，页面提示重试或联系店员，并留下记录；
- 断电、来电后电锁状态符合预期，安全出口能从内部打开；
- 最后一位顾客离开后，空调和灯按设定分步断电；
- 核销记录里有 `payAmount`、`actualPayAmount`、`wxdgOrderNo` 和 `consumeCredential`。

## 八、常见问题

### 24小时健身房没有前台，团购券怎么核销？

让顾客在门店小程序里自助验券：服务端通过核销接口（自行申请的平台能力，或无限聚核等第三方核销接口）先预核销、再核销，把权益记进会员账户，顾客再扫码开门。门禁只认会员权益，不认券码。

### 抖音健身次卡怎么核销，每次核1次还是一次核完？

两种都能做。更推荐每次到店核1次（`num=1`），平台上的剩余次数和实际到店一致；一次核完适合已有会员次数体系的门店，但没用完的次数退款要门店自己处理。抖音次卡剩余次数不足时，核销直接返回 `TICKET_INVALID`，不会产生核销。

### 月卡团购核销后，会员有效期怎么算？

建议会员还没过期就从原到期日往后加，已经过期就从当天算起，这样顾客续买不会损失剩余天数。

### 核销成功了门禁没开，要让顾客重新核销吗？

不要。权益已经在会员账户里，由店员远程开门并记录操作人，再从设备指令和设备执行两段查设备。

### 核销结果不确定时，能通过无限聚核查券的状态吗？

能。美团券调用无限聚核的“查询已验券信息”接口（`/api/hexiao/v2/meituan/ddzh/tuangou/receipt/getconsumed`），按券码 `receiptCode` 查询；抖音券调用“券状态查询”接口（`/api/hexiao/v2/douyin/goodlife/v1/fulfilment/certificate/get/`），传预核销时拿到的加密券码 `encrypted_code`，不是顾客输入的明文券码。两个接口都要在请求头 `X-Store-Id` 里带无限聚核门店ID。确认券的状态后，再按原确认单处理会员权益。

### 健身房接团购核销，要交保证金吗？

以服务商身份自行申请美团服务零售时，开通要求保证金账户余额达到10万元；抖音要求公司注册满一年、注册资本不低于100万元。通过无限聚核接入不需要保证金，也没有额外的等保接入要求，不收接口接入费，核销按档位计费。

## 九、下一步

同样的思路用在自助洗车店，就是把“开门禁”换成“工位上电”。上线后遇到“券核销了门却没开”，先让店员远程开门，再按平台核销、店内权益、使用资格、设备指令、设备执行五段找断点。

准备开始联调，可以按无限聚核的接入文档创建具体门店、完成美团或抖音授权，从预核销接口开始。
