# 活动与报名

> 在您的网站上发布协会的活动，让会员报名（公开活动也允许嘉宾报名），并用核验会员卡的同一个扫码器办理入场登记。

URL: https://developers.vimine.network/zh-hans/guides/events/

协会在管理后台中发布的活动，以与会员卡相同的原则通过 API 提供：**headless 文案**（标题、摘要、markdown 描述、图片、地点、日期、链接）以及每种报名类型的**实时余量**——但绝不包括*谁*报了名。用语沿用意大利第三部门的习惯：称为**报名**（iscrizione），有金额时称为**参与费**（contributo）。

:::note[开始之前]
您的客户端需要 `events:read`、`registrations:read` 和 `registrations:write` 权限（参见[权限、错误与限制](https://developers.vimine.network/zh-hans/guides/scopes-and-errors/)）。如需在不触及真实数据的情况下试用，请使用[沙盒](https://developers.vimine.network/zh-hans/guides/sandbox/)：孪生协会可以像真实协会一样发布活动。
:::

## 两种可见性

| `visibility` | 谁能看到 | 谁能报名 |
|---|---|---|
| `members_only`（默认） | 只有持会员令牌或协会自己客户端的令牌时可见 | 持有效会员卡的会员 |
| `public` | 任何人，无需登录：`GET /v1/aps/{slug}/events` | 会员，以及——当报名类型不要求会员卡时——**嘉宾** |

公开活动会向您网站的访客展示活动文案和各报名类型的剩余名额；仅限会员的活动只在登录后出现。

## 显示活动

```js title="公开页面，无需登录"
import { publicApi } from '@vimine/sdk';
const api = publicApi();
const { items } = await api.events.list('your-slug', { filter: 'upcoming' });
const event = await api.events.get('your-slug', 'jazz-night');
```

```js title="使用 Vimine 登录之后（包括仅限会员的活动）"
const { items } = await vimine.events.list();           // events:read
const event = await vimine.events.get('jazz-night');
```

每个活动都带有 `registration_types[]`：`amount`（0 = 免费）、`currency`、`remaining`（剩余名额，`null` = 不限）、`open`，以及在未开放时的 `reason`（`sold_out`、`registration_not_open`、`registration_closed`）。`status` 为 `published`、`completed`（日期已过）或 `cancelled`。

无需编写代码：[`<vimine-events>`](https://developers.vimine.network/zh-hans/sdk/elements/) 元素会渲染活动列表和报名按钮。

## 尚未入会的人

公开活动也能触达非会员：这正是一个人决定入会的时刻。协会在 Vimine 上的入会页面会**带着该活动**打开，当事人一步即可入会并预约名额：

```
https://app.vimine.network/enroll/your-slug?event=jazz-night
```

```js title="同一地址，在您的代码中生成"
import { eventEnrollUrl } from '@vimine/sdk/elements';
const url = eventEnrollUrl('https://app.vimine.network', 'your-slug', 'jazz-night', 'en');
```

名额的结果取决于会员资格所处的阶段，页面会告诉当事人：

| 会员资格 | 名额 |
|---|---|
| 立即生效（免费会员卡，无需审批） | 若活动报名免费，则**已确认** |
| 等待审批或会费 | 对于仅限会员的报名：名额**保留**至会员卡生效，届时若报名免费则自动确认。对于向所有人开放的报名：立即确认，与非会员相同 |
| 活动报名需要支付参与费 | 名额**保留**至协会设定的期限：协会记录参与费后即确认 |
| 电子邮箱地址尚未确认 | 不保留任何名额：当事人通过邮件中的链接确认地址时才申请名额，并会收到一封邮件告知结果 |

已经是会员的人也使用同一个页面：登录后点一个按钮即可预约，无需填写任何内容（`&signin=1` 会立即打开登录）。这只适用于已发布且尚未举行的 `public` 活动：如果是其他任何 slug，该页面就是普通的入会页面。

Vimine 上的活动公开页面（`/agenda/your-slug/jazz-night`）和 `<vimine-events>` 已经向访客提供了这一入口：“加入协会并预约”。

## 为会员报名

在显示按钮之前，先询问 API 该会员能否报名——响应会说明不能报名的原因：

```js
const { can_register, types } = await vimine.events.eligibility('jazz-night');
// types[i].reasons: membership_required, membership_not_active, membership_not_yet_valid, sold_out, max_per_user, registration_closed, …
```

然后为其报名（`registrations:write`）：

```js
const reg = await vimine.registrations.create('jazz-night', { registration_type: types[0].uid, note: '我和姐姐一起来' });
if (reg.status === 'confirmed') drawQr(reg.token);           // 免费类型：已确认，二维码已就绪
if (reg.status === 'reserved') show(reg.payment_instructions); // 需付参与费：名额保留至付款为止
```

- **免费**类型会立即确认并返回 `token`：即会员在入场时出示的二维码内容（请您自行绘制，使用您喜欢的库）。
- **需付参与费**的类型会保留名额（`reserved_until`），并返回协会的付款说明；秘书处记录参与费后予以确认——会员会收到一封电子邮件，此后 `vimine.registrations.get(id)` 就会带有 `token`。银行卡支付将通过 Stripe 提供。
- 如果会员的会员卡仍在等待生效，报名会保持 `reserved`，并在会员卡生效时自动确认。
- `422 not_eligible` 会在 `errors[]` 中带有与报名资格查询相同的原因。

会员的报名：`vimine.registrations.list({ filter: 'upcoming' })`；在活动的取消期限内（开始前 `registration.cancellation_hours` 小时）取消：`vimine.registrations.cancel(id)`——超过期限则返回 `409 window_closed`。

## 嘉宾（公开活动）

没有 Vimine 账户的访客可以从您的服务器（机密客户端，`registrations:write`）报名参加**公开**活动，前提是报名类型不要求会员卡：

```js title="server"
await srv.registrations.guest('jazz-night', { email, first_name, last_name, registration_type });
// → 始终为 { status: 'accepted' }
```

不会创建任何账户。嘉宾会收到一封带确认链接（有效期 24 小时）的电子邮件，链接指向 Vimine 上的活动页面：免费类型会在那里确认名额并立即显示二维码；需付参与费的类型则保留名额，并显示付款说明。如果您希望自己托管确认页面，请用 `api.confirmRegistration(token)` 使用链接中的令牌。

名额一经确认——免费类型立即确认，其他情况在记录参与费后确认——嘉宾会收到第二封电子邮件，内含他专属的链接：它会打开显示入场二维码的活动页面，在活动结束之前一直有效（活动改期后仍然有效）。嘉宾没有个人中心：链接就是他的名额，而不是二维码本身（转发的邮件并不等于名额）。如果您自己托管该页面，请用 `api.viewRegistration(token)` 读取名额（SDK 自 0.3.2 起）。

响应始终是 `202 { status: 'accepted' }`：API 从不透露某个地址是否已经报名。报名资格问题（`sold_out`，或仅限会员的类型返回 `membership_required`）返回 `422`。

:::caution[税务]
根据意大利《第三部门法典》，**非会员**缴纳的参与费属于经营性收入：当活动被设为公开时，管理后台会提醒这一点；每笔参与费都会根据付款时该报名所关联的会员资格自动归类。
:::

## 入场时

报名二维码和会员卡一样，是一个签名令牌：同一个扫码器、同一个调用都能接受它。

```js title="server（闸机、扫码器、服务台）"
const r = await srv.checkins.create(scannedToken);
if (r.kind === 'registration') {
    // r.verdict: valid | already_checked_in | not_confirmed | cancelled | event_cancelled | membership_invalid
    // r.first：首次通行时为 true；类型要求会员卡时带有 r.membership_verdict
}
```

公开核验接口也能回答报名令牌：`GET /v1/verify/{token}` → `{ status, kind: 'registration', event: { slug, starts_at } }`，不含个人数据。

## 从您的管理系统

某个活动的报名——id、状态、类型、会员资格、入场登记，**不含个人数据**——通过 `srv.registrations.listForEvent(slug, { status, limit, cursor })` 获取（`registrations:read`，适用于支持 API 列表的套餐）。姓名只保留在协会的管理后台中。

## 参考

- [浏览器 SDK](https://developers.vimine.network/zh-hans/sdk/browser/#events) · [服务器 SDK](https://developers.vimine.network/zh-hans/sdk/server/#events) · [`<vimine-events>`](https://developers.vimine.network/zh-hans/sdk/elements/) · [类型](https://developers.vimine.network/zh-hans/sdk/types/)
- HTTP API：[参考文档](https://developers.vimine.network/zh-hans/api/console/)中标签为 `events` 和 `registrations` 的操作。
