定制 banguncode/php-frista 二次开发

按需修改功能、优化性能、对接业务系统,提供一站式技术支持

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

banguncode/php-frista

Composer 安装命令:

composer require banguncode/php-frista

包简介

A simple PHP library for BPJS Kesehatan facial recognition integration.

README 文档

README

PHPFrista is a PHP library for integrating with BPJS Kesehatan FRISTA (Facial Recognition Integrated System).
It covers the full biometric flow: authentication, face verification, and first-time enrollment.

Features

  • Token-based authentication with server-side session validation — stale tokens are detected and refreshed automatically.
  • Face identification from encoding alone, no ID required (/face/recognition2).
  • Participant lookup by NIK (/face/nik2).
  • Face verification by ID + encoding (/face/match2) with all documented server responses mapped to StatusCode constants.
  • Biometric enrollment from a JPEG file, base64 string, or encoding array (/face/upload, /face/upload2).
  • Input validation for identity number, encoding format, and image type.
  • Single shared cURL wrapper — no duplicated request boilerplate.

Important

navigator.mediaDevices.getUserMedia() only works on localhost or over HTTPS.

For capturing face descriptors in the browser, face-api.js works well with the FRISTA encoding format. The detection.descriptor array from withFaceDescriptor() is a 128-element Float32Array that maps directly to the $encoding parameter.

Example capture page:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>Face Capture</title>
    <style>
        body { font-family: Arial; margin: 20px; }
        #videoWrap { position: relative; width: 640px; }
        video { width: 100%; background: #000; transform: scaleX(-1); }
        canvas { position: absolute; left: 0; top: 0; transform: scaleX(-1); }
        #capturedImage { max-width: 640px; margin-top: 20px; display: none; }
        button { padding: 8px 12px; margin: 10px 5px 0 0; }
        #log { margin-top: 10px; padding: 10px; background: #f5f5f5; font-family: monospace; font-size: 12px; max-height: 300px; overflow: auto; }
    </style>
</head>
<body>
    <h2>Face Capture</h2>
    <div id="videoWrap">
        <video id="video" autoplay muted playsinline></video>
        <canvas id="overlay"></canvas>
    </div>
    <button id="btnCapture" disabled>Capture</button>
    <img id="capturedImage" />
    <pre id="log"></pre>

    <script src="https://cdn.jsdelivr.net/npm/face-api.js@0.22.2/dist/face-api.min.js"></script>
    <script>
        const video        = document.getElementById('video');
        const logEl        = document.getElementById('log');
        const btnCapture   = document.getElementById('btnCapture');
        const capturedImage = document.getElementById('capturedImage');

        function log(msg) { logEl.innerHTML += msg + '\n'; }

        async function init() {
            try {
                log('Loading models...');
                await faceapi.nets.ssdMobilenetv1.loadFromUri('./models/ssd_mobilenetv1');
                await faceapi.nets.faceLandmark68Net.loadFromUri('./models/face_landmark_68');
                await faceapi.nets.faceRecognitionNet.loadFromUri('./models/face_recognition');
                log('Models loaded');

                const stream = await navigator.mediaDevices.getUserMedia({ video: true });
                video.srcObject = stream;
                video.addEventListener('loadedmetadata', () => {
                    log('Camera ready');
                    btnCapture.disabled = false;
                });
            } catch (e) {
                log('Error: ' + e.message);
            }
        }

        async function captureAndDetect() {
            const canvas = document.createElement('canvas');
            canvas.width  = video.videoWidth;
            canvas.height = video.videoHeight;
            const ctx = canvas.getContext('2d');

            // Mirror the frame to match the mirrored video display
            ctx.translate(canvas.width, 0);
            ctx.scale(-1, 1);
            ctx.drawImage(video, 0, 0);

            capturedImage.src = canvas.toDataURL();
            capturedImage.style.display = 'block';

            log('Detecting face...');
            const detection = await faceapi
                .detectSingleFace(canvas)
                .withFaceLandmarks()
                .withFaceDescriptor();

            if (!detection) { log('No face detected'); return; }

            log('Score: ' + detection.detection.score.toFixed(2));
            log('Encoding: ' + JSON.stringify(Array.from(detection.descriptor)));
        }

        btnCapture.onclick = captureAndDetect;
        window.addEventListener('load', init);
    </script>
</body>
</html>

Installation

composer require banguncode/php-frista

Requirements

  • PHP >= 5.5
  • Extensions: curl, fileinfo
  • Outbound HTTPS access to frista.bpjs-kesehatan.go.id

Directory Structure

php-frista/
├── src/
│   ├── FacialRecognition.php
│   └── StatusCode.php
├── tests/
│   └── FacialRecognitionTest.php
├── composer.json
└── README.md

Usage

1. Authenticate

<?php

require __DIR__ . '/vendor/autoload.php';

use PHPFrista\FacialRecognition;
use PHPFrista\StatusCode;

$frista = (new FacialRecognition())->init('vclaim_username', 'vclaim_password');

The token is cached in the PHP session. On each call, it is validated against the server via checkSession() before being reused. If the server reports the token as expired or invalid, a fresh login is performed automatically — no manual token management needed.

Default values:

  • Base URL: https://frista.bpjs-kesehatan.go.id
  • API version: 3.0.2

To override either:

$frista = (new FacialRecognition())
    ->setBaseUrl('https://staging.example.com')
    ->setVersion('3.0.3')
    ->init('username', 'password');

2. Validate Token (optional)

checkSession() pings the server to confirm the current token is still active. It is called automatically inside auth(), but you can also call it directly.

$valid = $frista->checkSession(); // true, false, or null (no token)

3. Identify by Face (no ID needed)

recognition() searches the entire FRISTA database using a face encoding alone. Useful when the participant's ID is unknown — e.g. a walk-in patient.

$result = $frista->recognition($encoding); // raw decoded response or null

4. Look Up by NIK

nik() retrieves a participant's biometric record by their 16-digit NIK.

$result = $frista->nik('3212************'); // raw decoded response or null

5. Verify a Face

$id       = '3212************';      // 16-digit NIK or 13-digit BPJS card number
$encoding = $request->getJSON()->encoding; // 128 floats forwarded from face-api.js

$result = $frista->verify($id, $encoding);

switch ($result['status']) {
    case StatusCode::OK:
        // $result['data'] contains: match_id, nama, nik, nokartu, score
        echo 'Verified: ' . $result['data']['nama'];
        break;

    case StatusCode::ALREADY_REGISTERED:
        echo 'Already checked in today.';
        break;

    case StatusCode::UNREGISTERED:
        // Participant is in BPJS but has no biometric yet.
        // Register via image file/base64: $frista->register($id, $file)
        // Register via encoding directly: $frista->uploadEncoding($id, $encoding)
        break;

    case StatusCode::FACE_MISMATCH:
        // $result['data']['distance'] is available for debugging
        echo 'Face did not match: ' . $result['message'];
        break;

    case StatusCode::FACE_UNPROCESSABLE:
        echo 'Photo quality too low, ask participant to retake.';
        break;

    case StatusCode::DUPLICATE_PHOTO:
        echo 'Identical photo was used before — participant must take a new photo.';
        break;

    default:
        echo 'Error: ' . $result['message'];
        break;
}

6. Register a Face (first-time only)

Only call register() when verify() returns StatusCode::UNREGISTERED.

From a JPEG file:

$upload = $frista->register($id, '/path/to/photo.jpg', true);

From a base64 string (data URI prefix is handled automatically):

$b64 = base64_encode(file_get_contents('/path/to/photo.jpg'));
// or a data URI: 'data:image/jpeg;base64,...'

$upload = $frista->register($id, $b64, false);

Both forms return:

['status' => StatusCode::OK, 'message' => 'Pendaftaran biometrik berhasil']
// or
['status' => StatusCode::INTEGRATION_ERROR, 'message' => '...']

Register via encoding (when a JPEG is not available but the face descriptor is already known):

// kanal refers to the type of healthcare facility:
//   'fktl' — Fasilitas Kesehatan Tingkat Lanjutan (hospital/specialist)
//   'fktp' — Fasilitas Kesehatan Tingkat Pertama (clinic/puskesmas)
$result = $frista->uploadEncoding($id, $encoding);           // defaults to 'fktl'
$result = $frista->uploadEncoding($id, $encoding, 'fktp');   // primary care facility
// returns raw decoded response or null

API Response Reference — /face/match2

These are the raw server responses from the match2 endpoint and their corresponding StatusCode mappings.

HTTP Server status StatusCode constant Notes
400 INTEGRATION_ERROR Malformed payload
200 false INVALID_ID "ID harus numeric" — caught locally before the request
200 false FACE_UNPROCESSABLE "Wajah tidak dapat diproses" — encoding sent but face extraction failed
200 false FACE_MISMATCH "Verifikasi wajah gagal. Foto tidak sesuai" — data.distance available
200 false UNREGISTERED code: "0" — participant exists but has no biometric on record
200 false ALREADY_REGISTERED "Peserta telah terdaftar hari ini" — treated as a pass
200 false DUPLICATE_PHOTO "Anda pernah melakukan upload dengan foto yang identik"
200 true OK data contains match_id, nama, nik, nokartu, score

Example success response from the server:

{
    "data": {
        "match_id": "69fc********************",
        "nama": "RUM HAIDAR FAUZAN",
        "nik": "3212************",
        "nokartu": "000*******245",
        "score": 0.604043983216737
    },
    "message": "Data ditemukan",
    "status": true
}

Example mismatch response (StatusCode::FACE_MISMATCH):

{
    "data": { "distance": 99.30039597991038 },
    "message": "Verifikasi wajah gagal. Foto tidak sesuai",
    "status": false
}

Example unregistered response (StatusCode::UNREGISTERED):

{
    "code": "0",
    "message": "Peserta belum melakukan perekaman wajah. Harap Hubungi Admin.",
    "status": false
}

Status Code Reference

Constant Description
OK Operation successful
ALREADY_REGISTERED Participant already verified today
UNREGISTERED No biometric on record — enrol with register()
FACE_MISMATCH Face recognised but score below threshold
FACE_UNPROCESSABLE Server could not extract a face from the encoding
DUPLICATE_PHOTO Identical photo was previously uploaded
AUTH_FAILED Login to BPJS server failed
INVALID_ID Identity number is not 13 or 16 digits
INVALID_ENCODING Encoding is not 128 numeric values
INVALID_IMAGE Image is not a valid JPEG or path does not exist
INTEGRATION_ERROR Unexpected error from the BPJS server
INTERNAL_SERVER_ERROR Server response was empty or unreadable
SERVER_UNREACHABLE Connection timed out or server is down

banguncode/php-frista 适用场景与选型建议

banguncode/php-frista 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 36 次下载、GitHub Stars 达 7, 最近一次更新时间为 2025 年 08 月 12 日, 在 PHP 生态内属于活跃度较高的组件。

它主要适用于以下技术方向: 「Facial Recognition」 「bpjs」 「frista」 「bridging」 等业务场景。在实际项目中,围绕这些方向常见需要落地的问题包括:接口对接、性能调优、并发安全、与既有框架(Laravel / ThinkPHP / Yii / Webman 等)的兼容适配,以及生产环境的日志埋点与稳定性保障。

我们在过去多个企业项目中使用过 banguncode/php-frista 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。

围绕 banguncode/php-frista 我们能提供哪些服务?
定制开发 / 二次开发

基于 banguncode/php-frista 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。

BUG 修复 & 性能优化

线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。

项目外包 & 长期维护

承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。

yvsm@zunyunkeji.com QQ:316430983 微信:yvsm316 西安尊云信息科技 · 专注 PHP / Go / 分布式系统研发

统计信息

  • 总下载量: 36
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 7
  • 点击次数: 24
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 7
  • Watchers: 0
  • Forks: 7
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2025-08-12