# 服务器集成

> 用于协会自有系统的机密客户端、client_credentials、会员卡状态、secret 轮换。

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

对于**协会自己的**系统——管理软件、闸机、CMS 插件——使用带有 `client_id` + `client_secret` 的**机密**客户端。secret 只显示一次，绝不能出现在浏览器中。

## 认证

```js title="Node"
import { VimineServer } from '@vimine/sdk/server';

const vimineServer = new VimineServer({
    clientId: 'vmc_…',
    clientSecret: process.env.VIMINE_SECRET,
    scopes: ['memberships:read']
});
const token = await vimineServer.token();      // client_credentials — 1 小时有效的令牌，自动续期
```

不使用 SDK 时：`POST /v1/oauth/token`，带上 `grant_type=client_credentials` 和 HTTP Basic `client_id:client_secret`（或在请求体中传 `client_id`/`client_secret`）：

```sh
curl -u "vmc_…:vms_…" -d "grant_type=client_credentials&scope=memberships:read" \
     https://api.vimine.network/v1/oauth/token
```

在读取端点（`/v1/memberships/{id}/status`、`/v1/memberships`）上，也可以**直接**使用 HTTP Basic，无需先获取令牌，方便硬件和低代码平台——但绝不能在浏览器中这样做。

## 会员卡状态

```js
const s = await vimineServer.memberships.status('membership-uuid');
// { id, status: 'active', verdict: 'valid', valid_until: '2027-…', membership_type: { cod: 'ORD' } }
```

不含任何个人数据：只有状态和有效期。`id` 是会员在自己的会员卡上看到的 id（通过“使用 Vimine 登录”获得的 `membership.id`），或您的系统在登记该会员卡时收到的 id。

如需核验**入场时出示的**会员卡，请改用二维码中的令牌调用 [`/v1/verify/{card_token}`](https://developers.vimine.network/zh-hans/guides/embed-and-verify/)：它完全不需要认证。

## 从您的系统为会员办理入会

拥有 `identity:signup` 和 `enroll:write` 后，您的管理系统可以创建账户并提交入会申请，**无需当事人经过 Vimine**：当事人会通过电子邮件收到链接，用于确认邮箱地址并设置密码。

```js
// 仅创建账户（此人尚未入会）
await vimineServer.signup({ email: 'mario.rossi@example.org', first_name: 'Mario', last_name: 'Rossi' });

// 账户 + 待处理的入会申请，由秘书处在 Vimine 管理后台中批准
const { statuto, membership_types } = await vimineServer.public.association('your-slug');
await vimineServer.enroll({
    email: 'mario.rossi@example.org',
    first_name: 'Mario',
    last_name: 'Rossi',
    codicefiscale: 'RSSMRA80A01H501U',      // 可选：没有税号时，需要 dob + birthplace + birth_country
    dob: '1980-01-01', birthplace: 'Roma', birth_country: 'IT',
    gender: 'M',                              // 仅用于统计：不与税号比对
    membership_type: membership_types[0].uid, // 只有一种类型时可省略
    accept_statuto: { version: statuto.version, sha256: statuto.sha256 },
    privacy_notice: privacy_notice && { version: privacy_notice.version, sha256: privacy_notice.sha256 } // 协会发布了隐私声明时必填
});
```

无论该地址背后是什么情况，响应**始终**是 `202 { status: 'accepted' }`：您无法得知某个电子邮箱是否已有 Vimine 账户（只有邮箱的所有者会通过邮件得知）。具体来说：

- **新地址** → 创建账户；使用 `enroll` 时，还会创建一份*待处理*的入会申请；发送欢迎邮件，附带邮箱地址确认；
- **已注册的地址** → 使用 `enroll` 时不写入任何内容：邮箱所有者会收到一个“确认您向 *<协会>* 提交的申请”的链接，有效期 24 小时，只有在他使用该链接后才会创建申请。如果他已经是会员，则什么也不会发生；
- `422` 错误只与*您的*请求数据有关（带 `errors[].field` 的 `validation_failed`、`statuto_outdated`、`invalid_type`）；协会不接受申请时返回 `403 api_disabled`；在[内测](https://developers.vimine.network/zh-hans/guides/scopes-and-errors/#内测)期间账户只能凭邀请创建，此时返回 `403 not_available`。

识别一个人，要么依靠**税号**，要么依靠出生日期、出生地和出生国（`dob`、`birthplace`、`birth_country`）——两者不能都没有：没有税号时，每缺少一项出生信息，`errors[]` 中就有一条 `required`。对于在国外出生的人，请务必发送 `birth_country`：他们的税号中只包含国家。税号与出生信息不一致时，返回 `422`，其中一条 `codicefiscale_mismatch` 带有 `mismatches`、`hints` 和 `confirmable`：当 `confirmable` 为 `true` 时，当事人可以确认该税号，您再带上 `codicefiscale_confirmed: true` 重复调用。如果协会登记会员的地址（公开资料中的 `aps.member_address`），请加上 `address`。如果协会收集监护人（行使父母责任者）的信息（`aps.collects_guardian_data`），且根据 `dob` 此人为未成年人，则 `guardian` 为必填——`first_name`、`last_name`、`relation`，可选 `codicefiscale`、`email`、`phone`——否则返回 `422`，并带有 `guardian: required`。完整流程见[没有意大利税号的会员](https://developers.vimine.network/zh-hans/guides/members-without-cf/)。

`accept_statuto` 必须指明**现行**章程：请在您的网站上向当事人展示章程（可在 [`association(slug)`](https://developers.vimine.network/zh-hans/sdk/public/) 中找到；文本和文件在 `GET /v1/aps/{slug}/statuto`），并传入其接受的版本。未登记可读章程的协会会返回 `accepting_requests: false`，申请则返回 `403 not_available`。协会面向会员的**隐私声明**也是如此（`association(slug)` 中的 `privacy_notice`，文本在 `GET /v1/aps/{slug}/privacy-notice`）：协会发布了隐私声明时，申请必须指明当事人阅读的版本，否则返回 `422`（缺少 `privacy_notice` 或 `privacy_notice_outdated`）；阅读确认以当事人的名义记录，并注明您的客户端。规则相同的可选项：`accept_terms` 和 `acknowledge_privacy`（账户使用条款和平台的隐私声明，`GET /v1/legal/{kind}`），在当事人已于您的网站上接受时传入。这些调用是幂等的：SDK 会为每次调用生成一个 `Idempotency-Key`，您也可以传入自己的键（`{ idempotencyKey: 'order-42' }`），以便在超时后安全地重试。邮件有发送配额（每个地址每天 3 封），沙盒中从不发送邮件。

## 入场登记

拥有 `checkins:write` 后，入口处的设备通过发送**从会员卡二维码中读取的令牌**（而不是某个标识）来记录会员的通行：响应只有判定结果，不含任何个人数据。可以直接使用 HTTP Basic，方便硬件和低代码平台。

```js
const r = await vimineServer.checkins.create('vmt_…');   // { verdict: 'valid' | 'expired' | 'not_yet_valid' | 'statuto_required' | 'inactive', status, membership_id, valid_until, identity_check, checked_in_at }
```

```sh
curl -u "vmc_…:vms_…" -H "Content-Type: application/json" -d '{"token":"vmt_…"}' https://api.vimine.network/v1/checkins
```

请求体为 `{ "token": "vmt_…" }`——从二维码中读取的令牌，可以来自会员卡，也可以来自活动报名；仅对会员卡而言，`card` 也可作为别名使用。

格式错误或伪造的令牌返回 `422 invalid_card`；其他协会的会员卡则是“未找到”。

`identity_check: 'pending'` 是给入口值守人员的提示：该会员没有可核实的税号，协会中也还没有人查看过其证件。请把它显示出来；它不属于 `verdict`，也不限制任何操作（参见[身份核对](https://developers.vimine.network/zh-hans/guides/members-without-cf/#入场时身份核对)）。

## 会员卡列表（Pro 和 Network 套餐）

`vimineServer.memberships.list({ status?, updated_since?, limit?, cursor? })` 分页返回（`next_cursor`）id、状态和日期——**绝不含个人数据**。使用 `updated_since` 只同步发生变化的内容。在不包含此功能的套餐上，响应为 `403 plan_limit`。

## secret 轮换

在管理后台中（“重新生成 secret”）：新的 secret 只显示一次，**旧的 secret 继续有效 24 小时**，因此您可以在不中断服务的情况下更新系统。如果怀疑 secret 已泄露，请撤销该客户端：它的所有会话会立即结束。

## 安全

- secret 多次输错，客户端会被锁定 15 分钟。
- 每个令牌都绑定在客户端所属的协会上：其他协会的会员卡只会显示为“未找到”。
- 使用刚轮换的凭据收到 `401` 时，SDK 会用新令牌重试一次。
- 响应采用 `application/problem+json`（参见[权限、错误与限制](https://developers.vimine.network/zh-hans/guides/scopes-and-errors/)）。
