# IGP.UnitySDK.Compliance

IGP Unity SDK 的实名认证和防沉迷功能合规模块。

当前模块暴露实名认证和防沉迷功能事件与读取入口。游戏可以通过 `IGPCompliance.SubmitRealNameAsync()` 把用户输入交给 Desktop 上传，不需要引入 WebView；已有 WebView 的游戏仍可使用 `IGPRuntimeManager.CreateAntiAddictionRealNameWebSessionAsync()`。

## UPM 使用说明

UPM 包 `compliance-igp-0.3.4.tgz` 依赖同版本 Core，完整运行时代码由 `Plugins/IGP.UnitySDK.Compliance.dll` 提供。安装 Core 和 Compliance 后，按本页示例和数据结构表直接调用；本 README 和 `Samples~/IGPSDKIntegration/` 随包提供，供开发者查看和显式导入。

## 安装

在 `Window > Package Manager` 中通过 `Add package from tarball` 先安装
`core-igp-0.3.4.tgz`，再安装 `compliance-igp-0.3.4.tgz`。包 ID 仍为
`cn.indiegp.sdk.unity.compliance`；`.unitypackage` 是同版本源码载体。两种载体不要同时安装。

## 最小接入

```csharp
using IGP.UnitySDK;
using IGP.UnitySDK.Compliance;

public sealed class ComplianceDriver
{
    private IGPRuntimeManager runtimeManager;

    public async void Refresh()
    {
        var antiAddictionEvent =
            await IGPCompliance.RefreshAntiAddictionEventAsync(runtimeManager);
        var ageRange =
            await IGPCompliance.RefreshAntiAddictionAgeRangeAsync(runtimeManager);
        var remainingTime =
            await IGPCompliance.RefreshAntiAddictionRemainingTimeAsync(runtimeManager);

        if (antiAddictionEvent.action == IGPAntiAddictionComplianceActions.Block)
        {
            // 游戏自行处理限制进入、退出、切账号或提示。
        }
    }

    public async void OpenVerification()
    {
        var webSession = await runtimeManager.CreateAntiAddictionRealNameWebSessionAsync();
        OpenGameWebView(webSession.url);
    }

    public async void OnWebViewNavigation(string url)
    {
        if (!url.StartsWith("igp://anti-addiction/real-name-complete"))
        {
            return;
        }

        CloseGameWebView();
        await RefreshAfterVerification();
    }

    private async System.Threading.Tasks.Task RefreshAfterVerification()
    {
        var antiAddictionEvent =
            await IGPCompliance.RefreshAntiAddictionEventAsync(runtimeManager);
        // 根据最新状态放行、提示或返回主菜单。
    }

    private void OpenGameWebView(string url) {}
    private void CloseGameWebView() {}

    public async void SubmitVerification(string realName, string idCardNumber)
    {
        var profile = await IGPCompliance.SubmitRealNameAsync(
            runtimeManager,
            realName,
            idCardNumber);

        // 上传成功后 SDK 会主动刷新状态。
        // 状态实际变化时，AntiAddictionChanged 和上方订阅会被触发。
        UnityEngine.Debug.Log(profile.verificationStatus);
    }
}
```

监听运行过程中的变化：

```csharp
var subscription = IGPCompliance.SubscribeAntiAddictionEvents(
    runtimeManager,
    antiAddictionEvent =>
    {
        // 游戏自行处理事件。
    });
```

不用时调用：

```csharp
subscription.Dispose();
```

## 调用参数

`runtimeManager` 均为必填的 `IGPRuntimeManager`。各刷新方法和创建 WebView
入口的 `appIdOverride` 为可选 `int?`，默认 `null` 并使用 `IGPConfig.appId`。
`SubmitRealNameAsync` 的 `realName` 与 `idCardNumber` 是必填非空 `string`；
`identityDocumentType` 是可选 `string`，默认
`IGPAntiAddictionIdentityDocumentTypes.CnIdCard`；`appIdOverride` 同样默认
`null`。

## 数据结构

合规模型位于 `IGP.UnitySDK.Models` 和 `IGP.UnitySDK.Compliance` 命名空间。下表中的返回字段都由 SDK 填充；游戏只读取快照，不修改它来影响平台状态。

### `IGPAntiAddictionStatus`

| 字段 | 类型 | 必填/默认值 | 生命周期与含义 |
| --- | --- | --- | --- |
| `enabled` | `bool` | 必填；默认 `false` | 当前 app 是否启用防沉迷评估。每次刷新或 Host 状态变更时替换整个状态快照。 |
| `canPlayNow` | `bool` | 必填；默认 `false` | 当前时刻是否允许游戏继续；业务应以最新快照为准。 |
| `state` | `string` | 必填；默认 `Disabled` | 稳定状态字符串；可通过只读 `ParsedState` 解析为 `IGPAntiAddictionState`。 |
| `reasonCode` | `string` | 可选；默认空字符串 | 当前决策的稳定原因码；空值表示 Host 未提供。 |
| `reasonMessage` | `string` | 可选；默认空字符串 | 面向诊断或展示的原因文本，不应用作业务分支键。 |
| `isMinor` | `bool?` | 可选；默认 `null` | `true`/`false` 表示已知未成年状态，`null` 表示未知。 |
| `ageBand` | `string` | 必填；默认 `Unknown` | 年龄段稳定字符串；可通过只读 `ParsedAgeBand` 解析为 `IGPAgeBand`。 |
| `evaluatedAt` | `string` | 可选；默认空字符串 | 本次评估时间；非空时为 Host 返回的 ISO 时间。 |
| `playableUntil` | `string` | 可选；默认空字符串 | 当前允许时段的结束时间；空值表示无法计算剩余时长。 |
| `nextPlayableAt` | `string` | 可选；默认空字符串 | 当前受限时预计下次可玩时间。 |
| `nextStatusChangeAt` | `string` | 可选；默认空字符串 | 已知的下次状态切换时间。 |

### `IGPAntiAddictionComplianceEvent`

| 字段 | 类型 | 必填/默认值 | 生命周期与含义 |
| --- | --- | --- | --- |
| `module` | `string` | 必填；固定 `anti-addiction` | 标识事件所属模块。 |
| `code` | `int` | 必填；默认 `0` | 稳定事件码，使用 `IGPAntiAddictionComplianceEventCodes` 比较。 |
| `key` | `string` | 必填；默认 `DISABLED` | 稳定事件键，使用 `IGPAntiAddictionComplianceEventKeys` 比较。 |
| `action` | `string` | 必填；默认 `ALLOW` | 当前建议动作，只为 `ALLOW` 或 `BLOCK`。 |
| `canPlayNow` | `bool` | 必填；默认 `true` | 与 `action` 对应的布尔判断。 |
| `reasonCode` | `string` | 可选；默认空字符串 | 从当前状态复制的稳定原因码。 |
| `reasonMessage` | `string` | 可选；默认空字符串 | 从当前状态复制的说明文本。 |
| `status` | `IGPAntiAddictionStatus` | 必填；默认空状态对象 | 生成该事件时的完整状态快照；后续刷新不会原地修改此对象。 |

### 查询结果

| 类型/字段 | 类型 | 必填/默认值 | 生命周期与含义 |
| --- | --- | --- | --- |
| `IGPAntiAddictionAgeRangeResult.ageRange` | `int` | 必填；默认 `-1` | 年龄段：未知 `-1`、未满 8 岁 `0`、8-15 岁 `8`、16-17 岁 `16`、成人 `18`。 |
| `IGPAntiAddictionAgeRangeResult.ageBand` | `string` | 必填；默认 `Unknown` | 与本次查询快照对应的年龄段字符串。 |
| `IGPAntiAddictionAgeRangeResult.status` | `IGPAntiAddictionStatus` | 必填；默认空状态对象 | 计算年龄段时使用的状态快照。 |
| `IGPAntiAddictionRemainingTimeResult.remainingTimeSeconds` | `int` | 必填；默认 `-1` | 剩余秒数；未知为 `-1`，当前不可玩为 `0`。 |
| `IGPAntiAddictionRemainingTimeResult.remainingTimeMinutes` | `int` | 必填；默认 `-1` | 剩余分钟数，按秒数向上取整；未知为 `-1`。 |
| `IGPAntiAddictionRemainingTimeResult.status` | `IGPAntiAddictionStatus` | 必填；默认空状态对象 | 计算剩余时长时使用的状态快照。 |

### 实名结果

| 类型/字段 | 类型 | 必填/默认值 | 生命周期与含义 |
| --- | --- | --- | --- |
| `IGPAntiAddictionRealNameWebSession.ticket` | `string` | 必填；默认空字符串 | 本次 WebView 流程的短期票据，只在内存中传给该流程，不记录日志或自行拼接 API。 |
| `IGPAntiAddictionRealNameWebSession.url` | `string` | 必填；默认空字符串 | 本次登录/实名流程入口；仅在 `expiresAt` 前使用。 |
| `IGPAntiAddictionRealNameWebSession.expiresAt` | `string` | 必填；默认空字符串 | WebSession 失效时间；过期后重新创建，不复用旧 ticket。 |
| `IGPAntiAddictionAccountSummary.verificationStatus` | `string` | 必填；默认空字符串 | 本次提交后的实名状态。 |
| `IGPAntiAddictionAccountSummary.isVerified` | `bool` | 必填；默认 `false` | 本次提交结果是否已实名。 |
| `IGPAntiAddictionAccountSummary.isMinor` | `bool?` | 可选；默认 `null` | 已知时表示是否未成年，`null` 表示服务未给出。 |
| `IGPAntiAddictionAccountSummary.identityDocumentType` | `string?` | 可选；默认 `null` | 服务确认的证件类型。 |
| `IGPAntiAddictionAccountSummary.realNameMasked` | `string?` | 可选；默认 `null` | 脱敏姓名；不会返回完整姓名。 |
| `IGPAntiAddictionAccountSummary.idCardMasked` | `string?` | 可选；默认 `null` | 脱敏证件号；不会返回完整证件号。 |
| `IGPAntiAddictionAccountSummary.failureReason` | `string?` | 可选；默认 `null` | 未通过时的服务原因。 |
| `IGPAntiAddictionAccountSummary.verifiedAt` | `string?` | 可选；默认 `null` | 已通过时的服务时间戳。 |

`Current*` 方法返回当前内存快照，`Refresh*Async` 从 Host 刷新后返回新快照。
订阅只在公开状态实际变化时回调；不再需要监听时必须 `Dispose()` 订阅。
参数无效会抛出 `ArgumentException`，Core/Host 调用失败会抛出带稳定错误码的
`IGPSDKException`；不要依赖异常消息文本做业务分支。

事件码和状态键请使用 `IGPAntiAddictionComplianceEventKeys`、`IGPAntiAddictionComplianceEventCodes` 与 `IGPAntiAddictionComplianceActions` 中的常量，不要依赖本地化消息文本。

## 当前范围

- 提供稳定事件码。
- 提供当前事件读取入口。
- 提供状态变化监听入口。
- 提供年龄段读取入口，返回未知、0、8、16、18。
- 提供剩余可玩时长读取入口，返回秒和分钟。
- 网络或服务异常默认转换为不允许进入，提示为「网络或服务异常」。
- 支持获取游戏内 IGP 登录与实名认证入口；用户未登录时会先在 WebView 内登录，未实名时继续填写实名信息。
- 支持把姓名、证件类型和证件号交给 Desktop 上传；Desktop 复用登录态和实名 API，SDK 不直接发送 HTTP。
- 原生提交成功后 SDK 主动刷新防沉迷状态；只有公开状态变化时才触发 `AntiAddictionChanged`，相同状态不重复触发。
- 不在 SDK 内实现弹窗或流程决策。
