feat: add application API documentation and C++ SDK

- Add complete API documentation for application integration (docs/API_DOCUMENT.md)
- Fix check-update API to use version ID comparison instead of string comparison
- Add validation: client version must exist in server version list
- Add support for patch/incremental updates with base_version check
- Add C++ SDK with HTTP client, JSON parser, and crypto support
- Add simple_test.cpp for standalone testing on Windows

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-28 11:58:44 +08:00
parent ed73bb470c
commit ed4a561704
10 changed files with 3423 additions and 14 deletions
+113 -14
View File
@@ -48,6 +48,8 @@ func handleAppGetInfo(c *gin.Context) {
func handleAppCheckUpdate(c *gin.Context) {
appKey := c.Param("appKey")
clientVersion := c.Query("version")
var app model.Application
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
response.Error(c, 404, "应用不存在")
@@ -59,14 +61,21 @@ func handleAppCheckUpdate(c *gin.Context) {
return
}
clientVersion := c.Query("version")
// 查询该应用的所有版本,按ID降序(创建顺序)
var allVersions []model.Version
if err := database.DB.Where("application_id = ? AND status = ?", app.ID, "active").
Order("id DESC").
Preload("Files").
Find(&allVersions).Error; err != nil {
response.Error(c, 500, "获取版本列表失败")
return
}
var latestVersion model.Version
err := database.DB.Where("application_id = ? AND status = ?", app.ID, "active").Order("created_at DESC").Preload("Files").First(&latestVersion).Error
if err != nil {
// 没有任何版本
if len(allVersions) == 0 {
response.Success(c, gin.H{
"has_update": false,
"current_version": clientVersion,
"latest_version": "",
"download_url": "",
"update_notes": "",
@@ -77,14 +86,82 @@ func handleAppCheckUpdate(c *gin.Context) {
return
}
hasUpdate := clientVersion != latestVersion.Version
if latestVersion.UpdateStrategy == "forced" {
hasUpdate = true
// 客户端必须提供版本号
if clientVersion == "" {
response.Error(c, 400, "请提供客户端版本号")
return
}
files := make([]gin.H, len(latestVersion.Files))
// 找到客户端当前版本对应的版本记录
var clientVersionRecord *model.Version
for i := range allVersions {
if allVersions[i].Version == clientVersion {
clientVersionRecord = &allVersions[i]
break
}
}
// 客户端版本不存在于服务器版本列表中,返回错误
if clientVersionRecord == nil {
response.Error(c, 404, "客户端版本不存在")
return
}
// 判断客户端版本状态:检查是否有更高ID的强制更新版本
isSuperseded := false
for i := range allVersions {
v := &allVersions[i]
// 存在ID更高(创建时间更晚)的强制更新版本
if v.ID > clientVersionRecord.ID && v.UpdateStrategy == "forced" {
isSuperseded = true
break
}
}
// 如果被取代(有强制更新),必须更新
// 找到最新的强制更新版本
var latestVersion *model.Version
var updateStrategy string
if isSuperseded {
// 找到ID最高的强制更新版本
for i := range allVersions {
v := &allVersions[i]
if v.ID > clientVersionRecord.ID && v.UpdateStrategy == "forced" {
if latestVersion == nil || v.ID > latestVersion.ID {
latestVersion = v
}
}
}
updateStrategy = "forced"
} else {
// 没有强制更新,检查是否有更高ID的版本(普通更新)
// 最新版本就是ID最高的版本(已经是按ID降序排列,第一个就是最新)
latestVersion = &allVersions[0]
// 只有当最新版本ID大于客户端版本ID时才需要更新
if latestVersion.ID > clientVersionRecord.ID {
updateStrategy = latestVersion.UpdateStrategy // optional 或 forced
} else {
// 已经是最新版本
response.Success(c, gin.H{
"has_update": false,
"current_version": clientVersion,
"latest_version": clientVersionRecord.Version,
"download_url": "",
"update_notes": "",
"update_strategy": "",
"update_method": "",
"files": []interface{}{},
})
return
}
}
// 构建文件列表
files := make([]map[string]interface{}, len(latestVersion.Files))
for i, f := range latestVersion.Files {
files[i] = gin.H{
files[i] = map[string]interface{}{
"file_path": f.FilePath,
"file_name": f.FileName,
"file_size": f.FileSize,
@@ -94,20 +171,42 @@ func handleAppCheckUpdate(c *gin.Context) {
}
}
response.Success(c, gin.H{
"has_update": hasUpdate,
// 构建返回结果
result := map[string]interface{}{
"has_update": true,
"current_version": clientVersion,
"latest_version": latestVersion.Version,
"download_url": latestVersion.FilePath,
"file_size": latestVersion.FileSize,
"file_hash": latestVersion.FileHash,
"entry_file": latestVersion.EntryFile,
"update_notes": latestVersion.Description,
"update_strategy": latestVersion.UpdateStrategy,
"update_strategy": updateStrategy,
"update_type": latestVersion.UpdateType,
"update_method": latestVersion.UpdateMethod,
"changelog": latestVersion.Changelog,
"files": files,
})
}
// 检查是否有适合客户端版本的增量更新
// 查找以客户端版本为基础版本的 patch 更新
var patchVersion model.Version
err := database.DB.Where("application_id = ? AND update_type = ? AND base_version_id = ? AND status = ?",
app.ID, "patch", clientVersionRecord.ID, "active").
Order("id DESC").
Preload("Files").
First(&patchVersion).Error
if err == nil {
// 找到了增量更新
result["is_patch"] = true
result["base_version"] = clientVersion
result["patch_url"] = patchVersion.FilePath
result["patch_size"] = patchVersion.FileSize
result["patch_hash"] = patchVersion.FileHash
}
response.Success(c, result)
}
func handleAppGetAnnouncements(c *gin.Context) {
+876
View File
@@ -0,0 +1,876 @@
# 应用对接API文档
## 概述
本文档描述了验证平台应用对接API的详细说明,供第三方应用开发者集成使用。
**服务器地址**: `https://gendan.xyz`
**API基础路径**: `/api/v1/app/{appKey}`
---
## 认证与加密
### 请求认证
- 公开接口:无需认证
- 需认证接口:请求头携带 `Authorization: Bearer {token}`
- Token通过登录接口获取
### 数据加密
平台支持AES和RC4两种加密方式,具体加密类型由应用配置决定。
#### AES加密
- 算法:AES-GCM
- 密钥长度:16/24/32字节(自动填充至32字节)
- 格式:Base64编码(nonce + ciphertext
#### RC4加密
- 密钥长度:任意长度
- 格式:Base64编码
---
## API接口列表
### 1. 应用信息接口(公开)
#### 1.1 获取应用信息
**请求**
```
GET /api/v1/app/{appKey}/info
```
**响应**
```json
{
"code": 200,
"message": "success",
"data": {
"id": 1,
"name": "应用名称",
"description": "应用描述",
"icon_url": "图标URL",
"status": "active",
"billing_type": "balance",
"login_policy": "strict",
"max_devices": 1,
"multi_open_mode": "forbidden",
"max_instances": 1,
"enable_trial": false,
"trial_balance": 0,
"heartbeat_interval": 60,
"heartbeat_timeout": 300
}
}
```
#### 1.2 检查更新
**请求**
```
GET /api/v1/app/{appKey}/check-update?version=1.0.0
```
**参数说明**
- `version` (必填): 客户端当前版本号,必须存在于服务器版本列表中
**响应**
```json
{
"code": 200,
"message": "success",
"data": {
"has_update": true,
"current_version": "1.0.0",
"latest_version": "1.0.1",
"download_url": "下载地址",
"file_size": 1024000,
"file_hash": "文件哈希",
"entry_file": "入口文件",
"update_notes": "更新说明",
"update_strategy": "optional|forced",
"update_type": "full|patch",
"update_method": "auto|manual",
"changelog": "更新日志",
"files": [],
"is_patch": false
}
}
```
**错误响应**
```json
{
"code": 400,
"message": "请提供客户端版本号"
}
```
```json
{
"code": 404,
"message": "客户端版本不存在"
}
```
**更新判断逻辑**
基于版本ID(创建顺序)判断,与版本管理页面一致:
| has_update | update_strategy | 含义 |
|------------|-----------------|------|
| false | "" | 已是最新版本,无需更新 |
| true | optional | 有可选更新 |
| true | forced | 必须强制更新(存在更高ID的强制版本) |
**判断规则**
1. 如果存在 `ID > 客户端版本ID``update_strategy = "forced"` 的版本 → `has_update=true, update_strategy="forced"`
2. 如果存在 `ID > 客户端版本ID` 的版本(无强制更新) → `has_update=true, update_strategy="optional"`
3. 客户端版本ID已是最高 → `has_update=false`
**增量更新**
- 如果 `update_type``"patch"` 且存在以客户端版本为基础的增量包:
- `is_patch`: true
- `base_version`: 基础版本号
- `patch_url`: 增量包下载地址
- `patch_size`: 增量包大小
- `patch_hash`: 增量包哈希
#### 1.3 获取公告列表
**请求**
```
GET /api/v1/app/{appKey}/announcements
```
**响应**
```json
{
"code": 200,
"message": "success",
"data": [
{
"id": 1,
"title": "公告标题",
"content": "公告内容",
"type": "info",
"is_top": true,
"created_at": "2024-01-01T00:00:00Z"
}
]
}
```
---
### 2. 用户认证接口(公开)
#### 2.1 用户注册
**请求**
```
POST /api/v1/app/{appKey}/register
Content-Type: application/json
{
"username": "用户名",
"email": "邮箱(如需验证)",
"email_code": "邮箱验证码(如需验证)",
"phone": "手机号(如需短信验证)",
"sms_code": "短信验证码(如需短信验证)",
"password": "密码",
"device_id": "设备ID",
"device_name": "设备名称",
"device_type": "android|ios|windows|mac|linux|web",
"instance_id": "实例ID"
}
```
**响应**
```json
{
"code": 200,
"message": "注册成功",
"data": {
"user_id": 1
}
}
```
#### 2.2 用户登录
**请求**
```
POST /api/v1/app/{appKey}/login
Content-Type: application/json
{
"username": "用户名",
"password": "密码",
"device_id": "设备ID(必填)",
"device_name": "设备名称",
"device_type": "android|ios|windows|mac|linux|web",
"instance_id": "实例ID"
}
```
**响应**
```json
{
"code": 200,
"message": "登录成功",
"data": {
"user_id": 1,
"token": "JWT令牌"
}
}
```
**错误响应**
```json
{
"code": 403,
"message": "设备绑定数量已达上限,请解绑后再试",
"data": {
"error_code": "DEVICE_LIMIT_EXCEEDED",
"max_devices": 1,
"device_count": 1,
"devices": [
{
"id": 1,
"device_id": "xxx",
"device_name": "设备名",
"device_type": "windows",
"online_count": 0,
"created_at": "2024-01-01T00:00:00Z"
}
]
}
}
```
#### 2.3 发送邮箱验证码
**请求**
```
POST /api/v1/app/{appKey}/send-email-code
Content-Type: application/json
{
"email": "邮箱地址",
"purpose": "register|reset_password"
}
```
**响应**
```json
{
"code": 200,
"message": "success",
"data": {
"message": "验证码已发送"
}
}
```
#### 2.4 重置密码
**请求**
```
POST /api/v1/app/{appKey}/reset-password
Content-Type: application/json
{
"email": "邮箱地址",
"code": "验证码",
"password": "新密码(至少6位)"
}
```
**响应**
```json
{
"code": 200,
"message": "success",
"data": {
"message": "密码重置成功"
}
}
```
#### 2.5 修改密码
**请求**
```
POST /api/v1/app/{appKey}/change-password
Content-Type: application/json
{
"username": "用户名",
"old_password": "原密码",
"new_password": "新密码(至少6位)"
}
```
**响应**
```json
{
"code": 200,
"message": "success",
"data": {
"message": "密码修改成功"
}
}
```
---
### 3. 账户接口(需认证)
#### 3.1 获取账户信息
**请求**
```
POST /api/v1/app/{appKey}/account
Authorization: Bearer {token}
Content-Type: application/json
{
"user_id": 1
}
```
**响应**
```json
{
"code": 200,
"message": "success",
"data": {
"user_id": 1,
"username": "用户名",
"balance": 100.0,
"status": "active"
}
}
```
#### 3.2 心跳上报
**请求**
```
POST /api/v1/app/{appKey}/heartbeat
Authorization: Bearer {token}
Content-Type: application/json
{
"user_id": 1,
"device_id": "设备ID",
"instance_id": "实例ID"
}
```
**响应**
```json
{
"code": 200,
"message": "心跳成功",
"data": {
"message": "心跳成功",
"balance": 99.0
}
}
```
---
### 4. 充值接口(公开)
#### 4.1 卡密充值
**请求**
```
POST /api/v1/app/{appKey}/recharge
Content-Type: application/json
{
"username": "用户名",
"card_key": "卡密",
"device_id": "设备ID"
}
```
**响应**
```json
{
"code": 200,
"message": "充值成功",
"data": {
"message": "充值成功",
"value": 30.0
}
}
```
#### 4.2 试用
**请求**
```
POST /api/v1/app/{appKey}/trial
Content-Type: application/json
{
"user_id": 1
}
```
**响应**
```json
{
"code": 200,
"message": "试用成功",
"data": {
"message": "试用成功",
"trial_balance": 10.0
}
}
```
---
### 5. 设备管理接口(需认证)
#### 5.1 获取设备列表
**请求**
```
GET /api/v1/app/{appKey}/devices
Authorization: Bearer {token}
```
**响应**
```json
{
"code": 200,
"message": "success",
"data": [
{
"id": 1,
"device_id": "设备ID",
"device_name": "设备名称",
"device_type": "windows",
"status": "active",
"online_sessions": 1,
"created_at": "2024-01-01T00:00:00Z"
}
]
}
```
#### 5.2 获取设备数量
**请求**
```
POST /api/v1/app/{appKey}/device-count
Authorization: Bearer {token}
Content-Type: application/json
{
"user_id": 1
}
```
**响应**
```json
{
"code": 200,
"message": "success",
"data": {
"count": 1,
"max_devices": 1,
"remaining": 0
}
}
```
#### 5.3 解绑设备(需认证)
**请求**
```
POST /api/v1/app/{appKey}/unbind-device
Authorization: Bearer {token}
Content-Type: application/json
{
"user_id": 1,
"device_id": "设备ID"
}
```
**响应**
```json
{
"code": 200,
"message": "success",
"data": {
"message": "解绑成功"
}
}
```
#### 5.4 解绑设备(用户认证)
**请求**
```
POST /api/v1/app/{appKey}/unbind-device-with-auth
Content-Type: application/json
{
"username": "用户名",
"password": "密码",
"device_id": "设备ID"
}
```
**响应**
```json
{
"code": 200,
"message": "success",
"data": {
"message": "解绑成功"
}
}
```
#### 5.5 获取实例列表
**请求**
```
POST /api/v1/app/{appKey}/instances
Authorization: Bearer {token}
Content-Type: application/json
{
"user_id": 1
}
```
**响应**
```json
{
"code": 200,
"message": "success",
"data": [
{
"id": 1,
"instance_id": "实例ID",
"device_id": "设备ID",
"device_name": "设备名称",
"is_online": true,
"last_heartbeat": "2024-01-01T00:00:00Z",
"created_at": "2024-01-01T00:00:00Z"
}
]
}
```
#### 5.6 强制下线实例
**请求**
```
POST /api/v1/app/{appKey}/instances/{instance_id}/offline
Authorization: Bearer {token}
Content-Type: application/json
{
"user_id": 1
}
```
**响应**
```json
{
"code": 200,
"message": "success",
"data": {
"message": "已强制离线"
}
}
```
---
### 6. 云端数据接口(需认证)
#### 6.1 获取云端常量列表
**请求**
```
GET /api/v1/app/{appKey}/constants
Authorization: Bearer {token}
```
**响应**
```json
{
"code": 200,
"message": "success",
"data": {
"config_key": {
"type": "string",
"value": "配置值"
},
"file_key": {
"type": "binary",
"value": "/api/v1/app/{appKey}/constants/file_key/download",
"file_name": "文件名",
"file_size": 1024,
"md5": "文件MD5",
"mime_type": "application/octet-stream"
}
}
}
```
#### 6.2 获取单个云端常量
**请求**
```
GET /api/v1/app/{appKey}/constants/{key}
Authorization: Bearer {token}
```
#### 6.3 下载云端常量文件
**请求**
```
GET /api/v1/app/{appKey}/constants/{key}/download
Authorization: Bearer {token}
```
#### 6.4 获取云端变量列表
**请求**
```
GET /api/v1/app/{appKey}/variables
Authorization: Bearer {token}
```
#### 6.5 获取单个云端变量
**请求**
```
GET /api/v1/app/{appKey}/variables/{key}
Authorization: Bearer {token}
```
#### 6.6 下载云端变量文件
**请求**
```
GET /api/v1/app/{appKey}/variables/{key}/download
Authorization: Bearer {token}
```
#### 6.7 上传云端变量文件
**请求**
```
POST /api/v1/app/{appKey}/variables/{key}/upload
Authorization: Bearer {token}
Content-Type: multipart/form-data
file: 文件内容
```
**响应**
```json
{
"code": 200,
"message": "success",
"data": {
"message": "上传成功",
"file_url": "文件URL",
"file_size": 1024,
"mime_type": "application/octet-stream",
"original_name": "原文件名",
"download_url": "下载URL"
}
}
```
#### 6.8 更新云端变量
**请求**
```
POST /api/v1/app/{appKey}/variables
Authorization: Bearer {token}
Content-Type: application/json
{
"variables": {
"key1": "value1",
"key2": "value2"
}
}
```
#### 6.9 创建变量记录(Stream类型)
**请求**
```
POST /api/v1/app/{appKey}/variables/{key}/records
Authorization: Bearer {token}
Content-Type: application/json
{
"field1": "value1",
"field2": "value2"
}
```
**响应**
```json
{
"code": 200,
"message": "success",
"data": {
"id": 1,
"created_at": "2024-01-01T00:00:00Z"
}
}
```
#### 6.10 获取变量记录列表
**请求**
```
GET /api/v1/app/{appKey}/variables/{key}/records?page=1&page_size=20
Authorization: Bearer {token}
```
**响应**
```json
{
"code": 200,
"message": "success",
"data": {
"records": [
{
"id": 1,
"data": {"field": "value"},
"created_at": "2024-01-01T00:00:00Z"
}
],
"total": 100,
"page": 1,
"page_size": 20,
"total_pages": 5
}
}
```
#### 6.11 删除变量记录
**请求**
```
DELETE /api/v1/app/{appKey}/variables/{key}/records/{record_id}
Authorization: Bearer {token}
```
---
### 7. 动态代码接口(需认证)
#### 7.1 执行动态代码
**请求**
```
POST /api/v1/app/{appKey}/dynamic-code/{key}/execute
Authorization: Bearer {token}
Content-Type: application/json
{
"params": {
"param1": "value1"
},
"user_id": 1
}
```
**响应**
```json
{
"code": 200,
"message": "success",
"data": {
"result": "执行结果",
"execution_time": 10
}
}
```
---
## 错误码说明
| 错误码 | 说明 |
|-------|------|
| 200 | 成功 |
| 400 | 参数错误 |
| 401 | 未授权/Token无效 |
| 403 | 禁止访问/余额不足/设备限制等 |
| 404 | 资源不存在 |
| 500 | 服务器内部错误 |
## 特殊错误码
| 错误码 | 说明 |
|-------|------|
| DEVICE_LIMIT_EXCEEDED | 设备绑定数量已达上限 |
| IP_LIMIT_EXCEEDED | IP绑定数量已达上限 |
| MULTI_INSTANCE_LIMIT_EXCEEDED | 多开数量已达上限 |
---
## 集成流程
1. 调用 `/info` 获取应用配置信息
2. 调用 `/register``/login` 获取用户Token
3. 使用Token调用需认证的接口
4. 定期调用 `/heartbeat` 保持在线状态
5. 根据需要调用其他接口
---
## C++ SDK使用示例
```cpp
#include "verify_client.hpp"
int main() {
// 创建客户端
verify::Client client("https://gendan.xyz", "your_app_key");
// 获取应用信息
auto info = client.getAppInfo();
// 用户登录
auto loginResult = client.login("username", "password", "device_id");
std::string token = loginResult["token"].asString();
client.setToken(token);
// 心跳
client.heartbeat("user_id", "device_id", "instance_id");
// 充值
client.recharge("username", "card_key", "device_id");
return 0;
}
```
+62
View File
@@ -0,0 +1,62 @@
cmake_minimum_required(VERSION 3.10)
project(verify_sdk VERSION 1.0.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 选项
option(USE_OPENSSL "Enable OpenSSL for AES encryption" ON)
option(BUILD_TESTS "Build test executable" ON)
# 查找依赖
find_package(CURL REQUIRED)
find_package(nlohmann_json 3.0.0 REQUIRED)
if(USE_OPENSSL)
find_package(OpenSSL REQUIRED)
add_compile_definitions(USE_OPENSSL)
endif()
# 库头文件
set(HEADERS
verify_client.hpp
)
# 库源文件
set(SOURCES
verify_client.cpp
)
# 创建静态库
add_library(verify_sdk STATIC ${HEADERS} ${SOURCES})
target_include_directories(verify_sdk PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
)
target_link_libraries(verify_sdk PUBLIC
CURL::libcurl
nlohmann_json::nlohmann_json
)
if(USE_OPENSSL)
target_link_libraries(verify_sdk PUBLIC OpenSSL::SSL OpenSSL::Crypto)
endif()
# 测试可执行文件
if(BUILD_TESTS)
add_executable(verify_test test_main.cpp)
target_link_libraries(verify_test PRIVATE verify_sdk)
endif()
# 安装
install(FILES ${HEADERS} DESTINATION include/verify)
install(TARGETS verify_sdk ARCHIVE DESTINATION lib)
# 输出信息
message(STATUS "")
message(STATUS "Verify SDK Configuration:")
message(STATUS " Version: ${PROJECT_VERSION}")
message(STATUS " OpenSSL: ${USE_OPENSSL}")
message(STATUS " Build tests: ${BUILD_TESTS}")
message(STATUS "")
+141
View File
@@ -0,0 +1,141 @@
# 验证平台 C++ SDK
用于对接验证平台的C++ SDK库。
## 依赖
- C++17 或更高版本
- libcurl
- nlohmann/json
- OpenSSL (可选,用于AES加密)
## 编译
### 使用 CMake
```bash
mkdir build
cd build
cmake ..
make
```
### 编译选项
- `USE_OPENSSL=ON/OFF` - 启用/禁用 OpenSSL (默认: ON)
- `BUILD_TESTS=ON/OFF` - 构建/不构建测试程序 (默认: ON)
### Ubuntu/Debian 依赖安装
```bash
sudo apt-get install libcurl4-openssl-dev libssl-dev nlohmann-json3-dev cmake g++
```
### Windows (vcpkg)
```bash
vcpkg install curl nlohmann-json openssl
cmake -DCMAKE_TOOLCHAIN_FILE=[vcpkg路径]/scripts/buildsystems/vcpkg.cmake ..
```
## 使用示例
### 基本用法
```cpp
#include "verify_client.hpp"
int main() {
// 创建客户端
verify::Client client("https://gendan.xyz", "your_app_key");
// 获取应用信息
auto info = client.getAppInfo();
std::cout << "应用名称: " << info.name << std::endl;
// 用户登录
auto loginResult = client.login("username", "password", "device_id");
std::string token = loginResult["token"].get<std::string>();
std::cout << "Token: " << token << std::endl;
// 心跳上报
client.heartbeat(loginResult["user_id"].get<uint64_t>(), "device_id", "instance_id");
return 0;
}
```
### 使用加密
```cpp
// 设置AES加密
client.setEncryptType(verify::EncryptType::AES, "your_secret_key_32_bytes_long");
// 设置RC4加密
client.setEncryptType(verify::EncryptType::RC4, "your_secret_key");
```
### 设备管理
```cpp
// 获取设备列表
auto devices = client.getDevices();
for (const auto& d : devices) {
std::cout << "设备: " << d.deviceName << std::endl;
}
// 解绑设备
client.unbindDevice(userId, "device_id_to_unbind");
```
### 云端数据
```cpp
// 获取云端变量
auto variables = client.getVariables();
std::cout << "变量值: " << variables["key"]["value"] << std::endl;
// 更新云端变量
std::map<std::string, std::string> vars = {{"key1", "value1"}, {"key2", "value2"}};
client.updateVariables(vars);
// 创建变量记录
json record = {{"field1", "value1"}, {"field2", 123}};
client.createVariableRecord("stream_key", record);
```
## API 参考
### Client 类
| 方法 | 说明 |
|------|------|
| `getAppInfo()` | 获取应用信息 |
| `checkUpdate(version)` | 检查更新 |
| `getAnnouncements()` | 获取公告列表 |
| `registerUser(...)` | 用户注册 |
| `login(username, password, deviceId)` | 用户登录 |
| `getAccount(userId)` | 获取账户信息 |
| `heartbeat(userId, deviceId, instanceId)` | 心跳上报 |
| `recharge(username, cardKey, deviceId)` | 卡密充值 |
| `getDevices()` | 获取设备列表 |
| `unbindDevice(userId, deviceId)` | 解绑设备 |
| `getConstants()` | 获取云端常量 |
| `getVariables()` | 获取云端变量 |
| `updateVariables(vars)` | 更新云端变量 |
| `executeDynamicCode(key, params)` | 执行动态代码 |
## 运行测试
```bash
./verify_test
```
测试选项:
- `--help` 显示帮助
- `--crypto` 仅测试加密
- `--perf` 仅测试性能
## 许可证
MIT License
+353
View File
@@ -0,0 +1,353 @@
// 简化版JSON库 - 仅包含测试所需功能
#ifndef SIMPLE_JSON_HPP
#define SIMPLE_JSON_HPP
#include <string>
#include <vector>
#include <map>
#include <sstream>
#include <stdexcept>
#include <cstdint>
#include <cmath>
namespace simple {
class json {
public:
enum class type_t {
null,
boolean,
number,
string,
array,
object
};
json() : type_(type_t::null) {}
json(std::nullptr_t) : type_(type_t::null) {}
json(bool b) : type_(type_t::boolean), bool_val_(b) {}
json(int n) : type_(type_t::number), num_val_(n) {}
json(int64_t n) : type_(type_t::number), num_val_(static_cast<double>(n)) {}
json(uint64_t n) : type_(type_t::number), num_val_(static_cast<double>(n)) {}
json(double n) : type_(type_t::number), num_val_(n) {}
json(const char* s) : type_(type_t::string), str_val_(s) {}
json(const std::string& s) : type_(type_t::string), str_val_(s) {}
type_t type() const { return type_; }
bool is_null() const { return type_ == type_t::null; }
bool is_bool() const { return type_ == type_t::boolean; }
bool is_number() const { return type_ == type_t::number; }
bool is_string() const { return type_ == type_t::string; }
bool is_array() const { return type_ == type_t::array; }
bool is_object() const { return type_ == type_t::object; }
size_t size() const {
if (type_ == type_t::array) return arr_val_.size();
if (type_ == type_t::object) return obj_val_.size();
return 0;
}
// Array access
json& operator[](size_t idx) {
if (type_ != type_t::array) {
type_ = type_t::array;
arr_val_.clear();
}
if (idx >= arr_val_.size()) arr_val_.resize(idx + 1);
return arr_val_[idx];
}
const json& operator[](size_t idx) const {
static json null_val;
if (type_ != type_t::array || idx >= arr_val_.size()) return null_val;
return arr_val_[idx];
}
// Object access
json& operator[](const std::string& key) {
if (type_ != type_t::object) {
type_ = type_t::object;
obj_val_.clear();
}
return obj_val_[key];
}
const json& operator[](const std::string& key) const {
static json null_val;
if (type_ != type_t::object) return null_val;
auto it = obj_val_.find(key);
return (it != obj_val_.end()) ? it->second : null_val;
}
// const char* overload to avoid ambiguity
json& operator[](const char* key) {
return (*this)[std::string(key)];
}
const json& operator[](const char* key) const {
return (*this)[std::string(key)];
}
bool contains(const std::string& key) const {
return type_ == type_t::object && obj_val_.find(key) != obj_val_.end();
}
// Value getters
bool get_bool() const { return bool_val_; }
double get_double() const { return num_val_; }
int64_t get_int64() const { return static_cast<int64_t>(num_val_); }
uint64_t get_uint64() const { return static_cast<uint64_t>(num_val_); }
int get_int() const { return static_cast<int>(num_val_); }
const std::string& get_string() const { return str_val_; }
// Implicit conversions
operator bool() const { return type_ == type_t::boolean ? bool_val_ : false; }
operator int() const { return static_cast<int>(num_val_); }
operator int64_t() const { return static_cast<int64_t>(num_val_); }
operator uint64_t() const { return static_cast<uint64_t>(num_val_); }
operator double() const { return num_val_; }
operator std::string() const { return str_val_; }
// Push back for arrays
void push_back(const json& val) {
if (type_ != type_t::array) {
type_ = type_t::array;
arr_val_.clear();
}
arr_val_.push_back(val);
}
// Serialization
std::string dump(int indent = -1) const {
std::ostringstream ss;
dump_impl(ss, indent >= 0 ? indent : -1, 0);
return ss.str();
}
static json parse(const std::string& str) {
size_t pos = 0;
return parse_impl(str, pos);
}
static json array() {
json j;
j.type_ = type_t::array;
return j;
}
static json object() {
json j;
j.type_ = type_t::object;
return j;
}
// Iterator support for arrays
std::vector<json>::iterator begin() { return arr_val_.begin(); }
std::vector<json>::iterator end() { return arr_val_.end(); }
std::vector<json>::const_iterator begin() const { return arr_val_.begin(); }
std::vector<json>::const_iterator end() const { return arr_val_.end(); }
// Get object items
std::map<std::string, json>::const_iterator obj_begin() const { return obj_val_.begin(); }
std::map<std::string, json>::const_iterator obj_end() const { return obj_val_.end(); }
private:
type_t type_ = type_t::null;
bool bool_val_ = false;
double num_val_ = 0.0;
std::string str_val_;
std::vector<json> arr_val_;
std::map<std::string, json> obj_val_;
void dump_impl(std::ostringstream& ss, int indent, int level) const {
switch (type_) {
case type_t::null:
ss << "null";
break;
case type_t::boolean:
ss << (bool_val_ ? "true" : "false");
break;
case type_t::number:
if (num_val_ == static_cast<int64_t>(num_val_)) {
ss << static_cast<int64_t>(num_val_);
} else {
ss << num_val_;
}
break;
case type_t::string:
ss << '"';
for (char c : str_val_) {
switch (c) {
case '"': ss << "\\\""; break;
case '\\': ss << "\\\\"; break;
case '\n': ss << "\\n"; break;
case '\r': ss << "\\r"; break;
case '\t': ss << "\\t"; break;
default: ss << c;
}
}
ss << '"';
break;
case type_t::array:
ss << '[';
if (indent >= 0) {
ss << '\n';
for (int i = 0; i <= level; ++i) ss << std::string(indent, ' ');
}
for (size_t i = 0; i < arr_val_.size(); ++i) {
if (i > 0) {
ss << ',';
if (indent >= 0) ss << '\n' << std::string((level + 1) * indent, ' ');
}
arr_val_[i].dump_impl(ss, indent, level + 1);
}
if (indent >= 0) ss << '\n' << std::string(level * indent, ' ');
ss << ']';
break;
case type_t::object:
ss << '{';
if (indent >= 0) ss << '\n' << std::string((level + 1) * indent, ' ');
bool first = true;
for (const auto& p : obj_val_) {
if (!first) {
ss << ',';
if (indent >= 0) ss << '\n' << std::string((level + 1) * indent, ' ');
}
first = false;
ss << '"' << p.first << "\":";
if (indent >= 0) ss << ' ';
p.second.dump_impl(ss, indent, level + 1);
}
if (indent >= 0) ss << '\n' << std::string(level * indent, ' ');
ss << '}';
break;
}
}
static json parse_impl(const std::string& str, size_t& pos) {
skip_whitespace(str, pos);
if (pos >= str.size()) throw std::runtime_error("Unexpected end of input");
char c = str[pos];
if (c == 'n') {
pos += 4;
return json();
} else if (c == 't') {
pos += 4;
return json(true);
} else if (c == 'f') {
pos += 5;
return json(false);
} else if (c == '"') {
return json(parse_string(str, pos));
} else if (c == '[') {
return parse_array(str, pos);
} else if (c == '{') {
return parse_object(str, pos);
} else if (c == '-' || std::isdigit(c)) {
return parse_number(str, pos);
}
throw std::runtime_error(std::string("Unexpected character: ") + c);
}
static void skip_whitespace(const std::string& str, size_t& pos) {
while (pos < str.size() && std::isspace(str[pos])) ++pos;
}
static std::string parse_string(const std::string& str, size_t& pos) {
++pos; // skip opening quote
std::string result;
while (pos < str.size() && str[pos] != '"') {
if (str[pos] == '\\' && pos + 1 < str.size()) {
++pos;
switch (str[pos]) {
case '"': result += '"'; break;
case '\\': result += '\\'; break;
case 'n': result += '\n'; break;
case 'r': result += '\r'; break;
case 't': result += '\t'; break;
default: result += str[pos];
}
} else {
result += str[pos];
}
++pos;
}
if (pos < str.size()) ++pos; // skip closing quote
return result;
}
static json parse_number(const std::string& str, size_t& pos) {
size_t start = pos;
if (str[pos] == '-') ++pos;
while (pos < str.size() && std::isdigit(str[pos])) ++pos;
if (pos < str.size() && str[pos] == '.') {
++pos;
while (pos < str.size() && std::isdigit(str[pos])) ++pos;
}
if (pos < str.size() && (str[pos] == 'e' || str[pos] == 'E')) {
++pos;
if (pos < str.size() && (str[pos] == '+' || str[pos] == '-')) ++pos;
while (pos < str.size() && std::isdigit(str[pos])) ++pos;
}
return json(std::stod(str.substr(start, pos - start)));
}
static json parse_array(const std::string& str, size_t& pos) {
json arr = json::array();
++pos; // skip '['
skip_whitespace(str, pos);
if (pos < str.size() && str[pos] == ']') {
++pos;
return arr;
}
while (pos < str.size()) {
arr.push_back(parse_impl(str, pos));
skip_whitespace(str, pos);
if (pos >= str.size() || str[pos] == ']') {
++pos;
break;
}
if (str[pos] == ',') ++pos;
skip_whitespace(str, pos);
}
return arr;
}
static json parse_object(const std::string& str, size_t& pos) {
json obj = json::object();
++pos; // skip '{'
skip_whitespace(str, pos);
if (pos < str.size() && str[pos] == '}') {
++pos;
return obj;
}
while (pos < str.size()) {
skip_whitespace(str, pos);
if (str[pos] != '"') throw std::runtime_error("Expected string key");
std::string key = parse_string(str, pos);
skip_whitespace(str, pos);
if (pos >= str.size() || str[pos] != ':') throw std::runtime_error("Expected ':'");
++pos;
obj[key] = parse_impl(str, pos);
skip_whitespace(str, pos);
if (pos >= str.size() || str[pos] == '}') {
++pos;
break;
}
if (str[pos] == ',') ++pos;
}
return obj;
}
};
// Type alias for compatibility
using nlohmann_json = json;
} // namespace simple
namespace nlohmann {
using json = simple::json;
}
#endif // SIMPLE_JSON_HPP
+447
View File
@@ -0,0 +1,447 @@
// 独立测试程序 - 不依赖外部库
// 使用Windows原生WinHTTP进行HTTP请求
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <winhttp.h>
#pragma comment(lib, "winhttp.lib")
#else
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <unistd.h>
#endif
#include "json.hpp"
#include <iostream>
#include <string>
#include <vector>
#include <sstream>
#include <map>
using json = nlohmann::json;
// ============================================================================
// WinHTTP客户端
// ============================================================================
class HttpClient {
public:
HttpClient() {
#ifdef _WIN32
hSession_ = WinHttpOpen(L"VerifySDK/1.0", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);
#endif
}
~HttpClient() {
#ifdef _WIN32
if (hConnect_) WinHttpCloseHandle(hConnect_);
if (hSession_) WinHttpCloseHandle(hSession_);
#endif
}
std::string get(const std::string& url) {
return request(url, "GET", "");
}
std::string post(const std::string& url, const std::string& body) {
return request(url, "POST", body);
}
private:
#ifdef _WIN32
HINTERNET hSession_ = nullptr;
HINTERNET hConnect_ = nullptr;
#endif
std::string request(const std::string& url, const std::string& method, const std::string& body) {
std::string result;
#ifdef _WIN32
// 解析URL
std::wstring wurl(url.begin(), url.end());
URL_COMPONENTS uc = { sizeof(uc) };
wchar_t host[256] = {0};
wchar_t path[1024] = {0};
uc.lpszHostName = host;
uc.dwHostNameLength = 256;
uc.lpszUrlPath = path;
uc.dwUrlPathLength = 1024;
if (!WinHttpCrackUrl(wurl.c_str(), 0, 0, &uc)) {
throw std::runtime_error("Failed to parse URL");
}
// 连接服务器
std::wstring whost(host);
hConnect_ = WinHttpConnect(hSession_, whost.c_str(), uc.nPort, 0);
if (!hConnect_) {
throw std::runtime_error("Failed to connect to server");
}
// 创建请求
std::wstring wpath(path);
std::wstring wmethod(method.begin(), method.end());
DWORD flags = (uc.nScheme == INTERNET_SCHEME_HTTPS) ? WINHTTP_FLAG_SECURE : 0;
HINTERNET hRequest = WinHttpOpenRequest(hConnect_, wmethod.c_str(), wpath.c_str(),
nullptr, WINHTTP_NO_REFERER,
WINHTTP_DEFAULT_ACCEPT_TYPES, flags);
if (!hRequest) {
throw std::runtime_error("Failed to create request");
}
// 发送请求
LPCWSTR headers = L"Content-Type: application/json\r\n";
BOOL bResult = WinHttpSendRequest(hRequest, headers, -1,
(LPVOID)body.c_str(), body.length(),
body.length(), 0);
if (!bResult) {
WinHttpCloseHandle(hRequest);
throw std::runtime_error("Failed to send request");
}
// 接收响应
bResult = WinHttpReceiveResponse(hRequest, nullptr);
if (!bResult) {
WinHttpCloseHandle(hRequest);
throw std::runtime_error("Failed to receive response");
}
// 读取数据
DWORD dwSize = 0;
DWORD dwDownloaded = 0;
do {
dwSize = 0;
if (!WinHttpQueryDataAvailable(hRequest, &dwSize)) break;
if (dwSize == 0) break;
std::vector<char> buffer(dwSize + 1);
if (WinHttpReadData(hRequest, &buffer[0], dwSize, &dwDownloaded)) {
result.append(buffer.data(), dwDownloaded);
}
} while (dwSize > 0);
WinHttpCloseHandle(hRequest);
#else
// Linux实现(简化版)
result = curl_request(url, method, body);
#endif
return result;
}
#ifndef _WIN32
std::string curl_request(const std::string& url, const std::string& method, const std::string& body) {
// 简化的socket实现(仅用于演示)
return "{\"code\": 200, \"message\": \"success\", \"data\": {}}";
}
#endif
};
// ============================================================================
// 验证客户端
// ============================================================================
class VerifyClient {
public:
VerifyClient(const std::string& baseUrl, const std::string& appKey)
: baseUrl_(baseUrl), appKey_(appKey) {
// 移除末尾斜杠
if (!baseUrl_.empty() && baseUrl_.back() == '/') {
baseUrl_.pop_back();
}
}
void setToken(const std::string& token) {
token_ = token;
}
// 获取应用信息
json getAppInfo() {
std::string url = baseUrl_ + "/api/v1/app/" + appKey_ + "/info";
std::string response = http_.get(url);
return json::parse(response);
}
// 检查更新
json checkUpdate(const std::string& version = "") {
std::string url = baseUrl_ + "/api/v1/app/" + appKey_ + "/check-update";
if (!version.empty()) {
url += "?version=" + version;
}
std::string response = http_.get(url);
return json::parse(response);
}
// 用户登录
json login(const std::string& username, const std::string& password,
const std::string& deviceId) {
std::string url = baseUrl_ + "/api/v1/app/" + appKey_ + "/login";
json body;
body["username"] = username;
body["password"] = password;
body["device_id"] = deviceId;
std::string response = http_.post(url, body.dump());
json result = json::parse(response);
// 自动设置token
if (result["code"].get_int() == 200 && result["data"].contains("token")) {
token_ = result["data"]["token"].get_string();
}
return result;
}
// 用户注册
json registerUser(const std::string& username, const std::string& password,
const std::string& deviceId) {
std::string url = baseUrl_ + "/api/v1/app/" + appKey_ + "/register";
json body;
body["username"] = username;
body["password"] = password;
body["device_id"] = deviceId;
std::string response = http_.post(url, body.dump());
return json::parse(response);
}
// 心跳
json heartbeat(uint64_t userId, const std::string& deviceId,
const std::string& instanceId = "") {
std::string url = baseUrl_ + "/api/v1/app/" + appKey_ + "/heartbeat";
json body;
body["user_id"] = userId;
body["device_id"] = deviceId;
if (!instanceId.empty()) {
body["instance_id"] = instanceId;
}
std::string response = http_.post(url, body.dump());
return json::parse(response);
}
// 卡密充值
json recharge(const std::string& username, const std::string& cardKey,
const std::string& deviceId) {
std::string url = baseUrl_ + "/api/v1/app/" + appKey_ + "/recharge";
json body;
body["username"] = username;
body["card_key"] = cardKey;
body["device_id"] = deviceId;
std::string response = http_.post(url, body.dump());
return json::parse(response);
}
// 获取设备列表
json getDevices() {
std::string url = baseUrl_ + "/api/v1/app/" + appKey_ + "/devices";
std::string response = http_.get(url);
return json::parse(response);
}
// 获取云端变量
json getVariables() {
std::string url = baseUrl_ + "/api/v1/app/" + appKey_ + "/variables";
std::string response = http_.get(url);
return json::parse(response);
}
private:
std::string baseUrl_;
std::string appKey_;
std::string token_;
HttpClient http_;
};
// ============================================================================
// 测试函数
// ============================================================================
void printSeparator(const std::string& title) {
std::cout << "\n";
std::cout << "========================================\n";
std::cout << title << "\n";
std::cout << "========================================\n";
}
void testJson() {
printSeparator("JSON功能测试");
// 创建JSON对象
json obj = json::object();
obj["name"] = "测试应用";
obj["version"] = "1.0.0";
obj["active"] = true;
obj["count"] = 100;
std::cout << "[创建对象] ";
std::cout << obj.dump() << "\n";
// 创建数组
json arr = json::array();
arr.push_back("item1");
arr.push_back("item2");
arr.push_back(123);
std::cout << "[创建数组] ";
std::cout << arr.dump() << "\n";
// 解析JSON
std::string jsonStr = R"({"code":200,"message":"success","data":{"user_id":1,"token":"abc123"}})";
json parsed = json::parse(jsonStr);
std::cout << "[解析结果]\n";
std::cout << " code: " << parsed["code"].get_int() << "\n";
std::cout << " message: " << parsed["message"].get_string() << "\n";
std::cout << " user_id: " << parsed["data"]["user_id"].get_uint64() << "\n";
std::cout << "\nJSON测试通过!\n";
}
void testApi(const std::string& baseUrl, const std::string& appKey) {
printSeparator("API测试");
std::cout << "服务器: " << baseUrl << "\n";
std::cout << "AppKey: " << appKey << "\n\n";
VerifyClient client(baseUrl, appKey);
// 测试获取应用信息
std::cout << "[测试] 获取应用信息...\n";
try {
json info = client.getAppInfo();
if (info["code"].get_int() == 200) {
std::cout << " [成功] 应用名称: " << info["data"]["name"].get_string() << "\n";
std::cout << " [成功] 状态: " << info["data"]["status"].get_string() << "\n";
} else {
std::cout << " [错误] " << info["message"].get_string() << "\n";
}
} catch (const std::exception& e) {
std::cout << " [异常] " << e.what() << "\n";
}
// 测试检查更新
std::cout << "\n[测试] 检查更新...\n";
try {
json update = client.checkUpdate("1.0.0");
if (update["code"].get_int() == 200) {
std::cout << " [成功] 最新版本: " << update["data"]["latest_version"].get_string() << "\n";
std::cout << " [成功] 有更新: " << (update["data"]["has_update"].get_bool() ? "" : "") << "\n";
} else {
std::cout << " [错误] " << update["message"].get_string() << "\n";
}
} catch (const std::exception& e) {
std::cout << " [异常] " << e.what() << "\n";
}
// 测试不存在的版本
std::cout << "\n[测试] 检查不存在的版本...\n";
try {
json update = client.checkUpdate("99.99.99");
if (update["code"].get_int() == 200) {
std::cout << " [意外成功]\n";
} else {
std::cout << " [预期错误] " << update["message"].get_string() << "\n";
}
} catch (const std::exception& e) {
std::cout << " [预期异常] " << e.what() << "\n";
}
// 测试登录
std::cout << "\n[测试] 用户登录...\n";
try {
// 使用测试用户
json loginResult = client.login("test_cpp_user", "test123456", "cpp_test_device");
if (loginResult["code"].get_int() == 200) {
std::cout << " [成功] 用户ID: " << loginResult["data"]["user_id"].get_uint64() << "\n";
std::cout << " [成功] Token已获取\n";
uint64_t userId = loginResult["data"]["user_id"].get_uint64();
// 测试心跳
std::cout << "\n[测试] 心跳上报...\n";
json heartbeat = client.heartbeat(userId, "cpp_test_device", "instance_001");
if (heartbeat["code"].get_int() == 200) {
std::cout << " [成功] 心跳成功\n";
if (heartbeat["data"].contains("balance")) {
std::cout << " [信息] 余额: " << heartbeat["data"]["balance"].get_double() << "\n";
}
}
} else {
std::cout << " [错误] " << loginResult["message"].get_string() << "\n";
}
} catch (const std::exception& e) {
std::cout << " [异常] " << e.what() << "\n";
}
// 测试卡密充值(预期失败)
std::cout << "\n[测试] 卡密充值(无效卡密)...\n";
try {
json recharge = client.recharge("admin", "invalid_card_key", "cpp_test_device");
if (recharge["code"].get_int() == 200) {
std::cout << " [意外成功] 充值成功\n";
} else {
std::cout << " [预期错误] " << recharge["message"].get_string() << "\n";
}
} catch (const std::exception& e) {
std::cout << " [预期异常] " << e.what() << "\n";
}
}
// ============================================================================
// 主函数
// ============================================================================
int main(int argc, char* argv[]) {
std::cout << R"(
SDK C++
Verify Platform SDK C++ Test
)" << "\n";
// 配置
std::string baseUrl = "https://gendan.xyz";
std::string appKey = "your_app_key"; // 替换为实际的appKey
// 解析命令行参数
for (int i = 1; i < argc; ++i) {
std::string arg = argv[i];
if (arg == "--help" || arg == "-h") {
std::cout << "用法: " << argv[0] << " [选项]\n";
std::cout << "选项:\n";
std::cout << " --help, -h 显示帮助信息\n";
std::cout << " --url <URL> 设置服务器URL\n";
std::cout << " --key <APPKEY> 设置应用密钥\n";
std::cout << " --json-only 仅测试JSON功能\n";
return 0;
} else if (arg == "--url" && i + 1 < argc) {
baseUrl = argv[++i];
} else if (arg == "--key" && i + 1 < argc) {
appKey = argv[++i];
} else if (arg == "--json-only") {
testJson();
return 0;
}
}
try {
// JSON测试
testJson();
// API测试
testApi(baseUrl, appKey);
std::cout << "\n========================================\n";
std::cout << "所有测试完成!\n";
std::cout << "========================================\n";
} catch (const std::exception& e) {
std::cerr << "\n错误: " << e.what() << "\n";
return 1;
}
return 0;
}
+361
View File
@@ -0,0 +1,361 @@
#include "verify_client.hpp"
#include <iostream>
#include <cassert>
#include <thread>
#include <chrono>
using namespace verify;
// 测试配置
const std::string BASE_URL = "https://gendan.xyz";
const std::string APP_KEY = "your_app_key"; // 需要替换为实际的appKey
const std::string TEST_USERNAME = "test_user_cpp";
const std::string TEST_PASSWORD = "test123456";
const std::string TEST_DEVICE_ID = "cpp_test_device_001";
class TestRunner {
public:
void run() {
std::cout << "========================================\n";
std::cout << "验证平台SDK C++ 测试\n";
std::cout << "========================================\n\n";
// 初始化客户端
testClientCreation();
// 测试应用信息接口
testAppInfo();
// 测试用户注册和登录
testUserRegistration();
testUserLogin();
// 测试账户接口
if (!token_.empty()) {
testAccount();
testHeartbeat();
}
// 测试充值接口
testRecharge();
// 测试设备管理
if (!token_.empty()) {
testDeviceManagement();
}
// 测试云端数据
if (!token_.empty()) {
testCloudData();
}
std::cout << "\n========================================\n";
std::cout << "所有测试完成!\n";
std::cout << "========================================\n";
}
private:
std::unique_ptr<Client> client_;
std::string token_;
uint64_t userId_ = 0;
void testClientCreation() {
std::cout << "[测试] 客户端创建... ";
try {
client_ = std::make_unique<Client>(BASE_URL, APP_KEY);
client_->setTimeout(30);
std::cout << "通过\n";
} catch (const std::exception& e) {
std::cout << "失败: " << e.what() << "\n";
throw;
}
}
void testAppInfo() {
std::cout << "[测试] 获取应用信息... ";
try {
AppConfig config = client_->getAppInfo();
std::cout << "通过\n";
std::cout << " - 应用名称: " << config.name << "\n";
std::cout << " - 计费类型: " << config.billingType << "\n";
std::cout << " - 最大设备数: " << config.maxDevices << "\n";
std::cout << " - 心跳间隔: " << config.heartbeatInterval << "\n";
} catch (const std::exception& e) {
std::cout << "失败: " << e.what() << "\n";
}
std::cout << "[测试] 检查更新... ";
try {
VersionInfo version = client_->checkUpdate("1.0.0");
std::cout << "通过\n";
std::cout << " - 有更新: " << (version.hasUpdate ? "" : "") << "\n";
std::cout << " - 最新版本: " << version.latestVersion << "\n";
} catch (const std::exception& e) {
std::cout << "失败: " << e.what() << "\n";
}
std::cout << "[测试] 获取公告列表... ";
try {
auto announcements = client_->getAnnouncements();
std::cout << "通过 (共" << announcements.size() << "条公告)\n";
} catch (const std::exception& e) {
std::cout << "失败: " << e.what() << "\n";
}
}
void testUserRegistration() {
std::cout << "[测试] 用户注册... ";
try {
// 先尝试登录,如果用户存在则跳过注册
try {
auto result = client_->login(TEST_USERNAME, TEST_PASSWORD, TEST_DEVICE_ID);
userId_ = result["user_id"].get<uint64_t>();
token_ = result["token"].get<std::string>();
std::cout << "用户已存在,跳过注册\n";
return;
} catch (...) {
// 用户不存在,继续注册
}
userId_ = client_->registerUser(
TEST_USERNAME,
TEST_PASSWORD,
TEST_DEVICE_ID,
"C++测试设备",
"windows",
"instance_001"
);
std::cout << "通过 (用户ID: " << userId_ << ")\n";
// 注册后登录
auto result = client_->login(TEST_USERNAME, TEST_PASSWORD, TEST_DEVICE_ID);
token_ = result["token"].get<std::string>();
} catch (const ApiException& e) {
std::cout << "API错误 [" << e.code() << "]: " << e.what() << "\n";
} catch (const std::exception& e) {
std::cout << "失败: " << e.what() << "\n";
}
}
void testUserLogin() {
std::cout << "[测试] 用户登录... ";
try {
auto result = client_->login(TEST_USERNAME, TEST_PASSWORD, TEST_DEVICE_ID);
userId_ = result["user_id"].get<uint64_t>();
token_ = result["token"].get<std::string>();
std::cout << "通过\n";
std::cout << " - 用户ID: " << userId_ << "\n";
std::cout << " - Token: " << token_.substr(0, 30) << "...\n";
} catch (const ApiException& e) {
std::cout << "API错误 [" << e.code() << "]: " << e.what() << "\n";
} catch (const std::exception& e) {
std::cout << "失败: " << e.what() << "\n";
}
}
void testAccount() {
std::cout << "[测试] 获取账户信息... ";
try {
UserInfo info = client_->getAccount(userId_);
std::cout << "通过\n";
std::cout << " - 用户名: " << info.username << "\n";
std::cout << " - 余额: " << info.balance << "\n";
std::cout << " - 状态: " << info.status << "\n";
} catch (const std::exception& e) {
std::cout << "失败: " << e.what() << "\n";
}
}
void testHeartbeat() {
std::cout << "[测试] 心跳上报... ";
try {
auto result = client_->heartbeat(userId_, TEST_DEVICE_ID, "instance_001");
std::cout << "通过\n";
if (result.contains("balance")) {
std::cout << " - 当前余额: " << result["balance"] << "\n";
}
} catch (const std::exception& e) {
std::cout << "失败: " << e.what() << "\n";
}
}
void testRecharge() {
std::cout << "[测试] 卡密充值 (使用无效卡密测试错误处理)... ";
try {
auto result = client_->recharge(TEST_USERNAME, "invalid_card_key_12345", TEST_DEVICE_ID);
std::cout << "未预期的成功\n";
} catch (const ApiException& e) {
std::cout << "通过 (预期错误: " << e.what() << ")\n";
} catch (const std::exception& e) {
std::cout << "通过 (预期错误: " << e.what() << ")\n";
}
}
void testDeviceManagement() {
std::cout << "[测试] 获取设备列表... ";
try {
auto devices = client_->getDevices();
std::cout << "通过 (共" << devices.size() << "台设备)\n";
for (const auto& d : devices) {
std::cout << " - 设备ID: " << d.deviceId << " (" << d.deviceName << ")\n";
}
} catch (const std::exception& e) {
std::cout << "失败: " << e.what() << "\n";
}
std::cout << "[测试] 获取设备数量... ";
try {
auto result = client_->getDeviceCount(userId_);
std::cout << "通过\n";
std::cout << " - 已绑定: " << result["count"] << "\n";
std::cout << " - 最大数: " << result["max_devices"] << "\n";
std::cout << " - 剩余: " << result["remaining"] << "\n";
} catch (const std::exception& e) {
std::cout << "失败: " << e.what() << "\n";
}
std::cout << "[测试] 获取实例列表... ";
try {
auto instances = client_->getInstances(userId_);
std::cout << "通过 (共" << instances.size() << "个实例)\n";
} catch (const std::exception& e) {
std::cout << "失败: " << e.what() << "\n";
}
}
void testCloudData() {
std::cout << "[测试] 获取云端常量... ";
try {
auto constants = client_->getConstants();
std::cout << "通过 (共" << constants.size() << "个常量)\n";
} catch (const std::exception& e) {
std::cout << "失败: " << e.what() << "\n";
}
std::cout << "[测试] 获取云端变量... ";
try {
auto variables = client_->getVariables();
std::cout << "通过 (共" << variables.size() << "个变量)\n";
} catch (const std::exception& e) {
std::cout << "失败: " << e.what() << "\n";
}
}
};
// 加密测试
void testCrypto() {
std::cout << "\n[测试] 加密功能...\n";
// AES测试
{
std::cout << " [AES加密/解密] ";
try {
Crypto aes(EncryptType::AES, "test_key_12345678");
std::string plaintext = "Hello, World! 你好,世界!";
std::string encrypted = aes.encrypt(plaintext);
std::string decrypted = aes.decrypt(encrypted);
if (decrypted == plaintext) {
std::cout << "通过\n";
std::cout << " 原文: " << plaintext << "\n";
std::cout << " 密文: " << encrypted.substr(0, 50) << "...\n";
} else {
std::cout << "失败: 解密结果不匹配\n";
}
} catch (const CryptoException& e) {
std::cout << "失败: " << e.what() << "\n";
std::cout << " 提示: 请使用 -DUSE_OPENSSL 编译以启用AES加密\n";
}
}
// RC4测试
{
std::cout << " [RC4加密/解密] ";
try {
Crypto rc4(EncryptType::RC4, "rc4_secret_key");
std::string plaintext = "Test RC4 encryption";
std::string encrypted = rc4.encrypt(plaintext);
std::string decrypted = rc4.decrypt(encrypted);
if (decrypted == plaintext) {
std::cout << "通过\n";
} else {
std::cout << "失败: 解密结果不匹配\n";
}
} catch (const std::exception& e) {
std::cout << "失败: " << e.what() << "\n";
}
}
}
// 性能测试
void testPerformance() {
std::cout << "\n[测试] 性能测试...\n";
Client client(BASE_URL, APP_KEY);
client.setTimeout(10);
int successCount = 0;
int failCount = 0;
const int iterations = 10;
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < iterations; ++i) {
try {
client.getAppInfo();
++successCount;
} catch (...) {
++failCount;
}
}
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
std::cout << " - 请求次数: " << iterations << "\n";
std::cout << " - 成功次数: " << successCount << "\n";
std::cout << " - 失败次数: " << failCount << "\n";
std::cout << " - 总耗时: " << duration.count() << "ms\n";
std::cout << " - 平均耗时: " << (duration.count() / iterations) << "ms/请求\n";
}
int main(int argc, char* argv[]) {
std::cout << R"(
SDK C++
Verify Platform SDK C++ Test
)" << "\n";
// 解析命令行参数
for (int i = 1; i < argc; ++i) {
std::string arg = argv[i];
if (arg == "--help" || arg == "-h") {
std::cout << "用法: " << argv[0] << " [选项]\n";
std::cout << "选项:\n";
std::cout << " --help, -h 显示帮助信息\n";
std::cout << " --crypto 仅测试加密功能\n";
std::cout << " --perf 仅测试性能\n";
return 0;
}
}
try {
// 测试加密
testCrypto();
// 主测试
TestRunner runner;
runner.run();
// 性能测试
testPerformance();
} catch (const std::exception& e) {
std::cerr << "\n错误: " << e.what() << "\n";
return 1;
}
return 0;
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "verify-sdk",
"version": "1.0.0",
"description": "C++ SDK for Verify Platform",
"dependencies": [
"curl",
"nlohmann-json",
"openssl"
]
}
+830
View File
@@ -0,0 +1,830 @@
#include "verify_client.hpp"
#include <sstream>
#include <fstream>
#include <stdexcept>
#include <ctime>
#include <iomanip>
#include <random>
#include <algorithm>
#ifdef USE_OPENSSL
#include <openssl/bio.h>
#include <openssl/buffer.h>
#include <openssl/evp.h>
#include <openssl/rand.h>
#endif
namespace verify {
// ============================================================================
// Crypto Implementation
// ============================================================================
Crypto::Crypto(EncryptType type, const std::string& key)
: type_(type), key_(key) {
}
Crypto::~Crypto() = default;
std::vector<uint8_t> Crypto::padKey(const std::vector<uint8_t>& key, size_t targetLen) {
if (key.size() >= targetLen) {
return std::vector<uint8_t>(key.begin(), key.begin() + targetLen);
}
std::vector<uint8_t> padded(targetLen);
std::copy(key.begin(), key.end(), padded.begin());
for (size_t i = key.size(); i < targetLen; ++i) {
padded[i] = key[i % key.size()];
}
return padded;
}
std::string base64Encode(const std::vector<uint8_t>& data) {
#ifdef USE_OPENSSL
BIO* bio = BIO_new(BIO_s_mem());
BIO* b64 = BIO_new(BIO_f_base64());
BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);
BIO_push(b64, bio);
BIO_write(b64, data.data(), static_cast<int>(data.size()));
BIO_flush(b64);
BUF_MEM* buffer;
BIO_get_mem_ptr(b64, &buffer);
std::string result(buffer->data, buffer->length);
BIO_free_all(b64);
return result;
#else
static const char* chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
std::string result;
result.reserve(((data.size() + 2) / 3) * 4);
for (size_t i = 0; i < data.size(); i += 3) {
uint32_t n = (data[i] << 16);
if (i + 1 < data.size()) n |= (data[i + 1] << 8);
if (i + 2 < data.size()) n |= data[i + 2];
result.push_back(chars[(n >> 18) & 0x3F]);
result.push_back(chars[(n >> 12) & 0x3F]);
result.push_back((i + 1 < data.size()) ? chars[(n >> 6) & 0x3F] : '=');
result.push_back((i + 2 < data.size()) ? chars[n & 0x3F] : '=');
}
return result;
#endif
}
std::vector<uint8_t> base64Decode(const std::string& encoded) {
#ifdef USE_OPENSSL
BIO* bio = BIO_new_mem_buf(encoded.data(), static_cast<int>(encoded.size()));
BIO* b64 = BIO_new(BIO_f_base64());
BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);
BIO_push(b64, bio);
std::vector<uint8_t> result(encoded.size());
int len = BIO_read(b64, result.data(), static_cast<int>(result.size()));
result.resize(len > 0 ? len : 0);
BIO_free_all(b64);
return result;
#else
static const int table[] = {
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,62,-1,-1,-1,63,
52,53,54,55,56,57,58,59,60,61,-1,-1,-1,-1,-1,-1,
-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14,
15,16,17,18,19,20,21,22,23,24,25,-1,-1,-1,-1,-1,
-1,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,
41,42,43,44,45,46,47,48,49,50,51,-1,-1,-1,-1,-1
};
std::vector<uint8_t> result;
result.reserve(encoded.size() * 3 / 4);
int val = 0, bits = 0;
for (char c : encoded) {
if (c == '=') break;
int v = table[static_cast<uint8_t>(c)];
if (v < 0) continue;
val = (val << 6) | v;
bits += 6;
if (bits >= 8) {
bits -= 8;
result.push_back(static_cast<uint8_t>((val >> bits) & 0xFF));
}
}
return result;
#endif
}
std::string Crypto::encrypt(const std::string& plaintext) {
if (type_ == EncryptType::None || key_.empty()) {
return plaintext;
}
switch (type_) {
case EncryptType::AES:
return encryptAES(plaintext);
case EncryptType::RC4:
return encryptRC4(plaintext);
default:
return plaintext;
}
}
std::string Crypto::decrypt(const std::string& ciphertext) {
if (type_ == EncryptType::None || key_.empty()) {
return ciphertext;
}
switch (type_) {
case EncryptType::AES:
return decryptAES(ciphertext);
case EncryptType::RC4:
return decryptRC4(ciphertext);
default:
return ciphertext;
}
}
std::string Crypto::encryptAES(const std::string& plaintext) {
#ifdef USE_OPENSSL
std::vector<uint8_t> keyBytes = padKey(std::vector<uint8_t>(key_.begin(), key_.end()), 32);
EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
if (!ctx) throw CryptoException("Failed to create cipher context");
std::vector<uint8_t> nonce(12);
RAND_bytes(nonce.data(), 12);
if (EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), nullptr, nullptr, nullptr) != 1) {
EVP_CIPHER_CTX_free(ctx);
throw CryptoException("Failed to init encryption");
}
EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, 12, nullptr);
EVP_EncryptInit_ex(ctx, nullptr, nullptr, keyBytes.data(), nonce.data());
std::vector<uint8_t> ciphertext(plaintext.size() + 16);
int len;
EVP_EncryptUpdate(ctx, ciphertext.data(), &len,
reinterpret_cast<const uint8_t*>(plaintext.data()), plaintext.size());
int ciphertextLen = len;
EVP_EncryptFinal_ex(ctx, ciphertext.data() + len, &len);
ciphertextLen += len;
std::vector<uint8_t> tag(16);
EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, 16, tag.data());
EVP_CIPHER_CTX_free(ctx);
// nonce (12) + ciphertext + tag (16)
std::vector<uint8_t> result;
result.reserve(12 + ciphertextLen + 16);
result.insert(result.end(), nonce.begin(), nonce.end());
result.insert(result.end(), ciphertext.begin(), ciphertext.begin() + ciphertextLen);
result.insert(result.end(), tag.begin(), tag.end());
return base64Encode(result);
#else
throw CryptoException("OpenSSL not available. Compile with -DUSE_OPENSSL");
#endif
}
std::string Crypto::decryptAES(const std::string& ciphertext) {
#ifdef USE_OPENSSL
std::vector<uint8_t> data = base64Decode(ciphertext);
if (data.size() < 28) throw CryptoException("Ciphertext too short");
std::vector<uint8_t> keyBytes = padKey(std::vector<uint8_t>(key_.begin(), key_.end()), 32);
std::vector<uint8_t> nonce(data.begin(), data.begin() + 12);
std::vector<uint8_t> tag(data.end() - 16, data.end());
std::vector<uint8_t> encrypted(data.begin() + 12, data.end() - 16);
EVP_CIPHER_CTX* ctx = EVP_CIPHER_CTX_new();
if (!ctx) throw CryptoException("Failed to create cipher context");
EVP_DecryptInit_ex(ctx, EVP_aes_256_gcm(), nullptr, nullptr, nullptr);
EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, 12, nullptr);
EVP_DecryptInit_ex(ctx, nullptr, nullptr, keyBytes.data(), nonce.data());
std::vector<uint8_t> decrypted(encrypted.size());
int len;
EVP_DecryptUpdate(ctx, decrypted.data(), &len, encrypted.data(), encrypted.size());
EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, 16, tag.data());
int ret = EVP_DecryptFinal_ex(ctx, decrypted.data() + len, &len);
EVP_CIPHER_CTX_free(ctx);
if (ret != 1) throw CryptoException("Decryption failed: authentication error");
return std::string(decrypted.begin(), decrypted.begin() + len);
#else
throw CryptoException("OpenSSL not available. Compile with -DUSE_OPENSSL");
#endif
}
class RC4 {
public:
explicit RC4(const std::vector<uint8_t>& key) {
for (int i = 0; i < 256; ++i) s_[i] = static_cast<uint8_t>(i);
uint8_t j = 0;
for (int i = 0; i < 256; ++i) {
j = j + s_[i] + key[i % key.size()];
std::swap(s_[i], s_[j]);
}
}
void process(std::vector<uint8_t>& data) {
for (auto& byte : data) {
i_++;
j_ += s_[i_];
std::swap(s_[i_], s_[j_]);
byte ^= s_[(s_[i_] + s_[j_]) & 0xFF];
}
}
private:
uint8_t s_[256] = {};
uint8_t i_ = 0, j_ = 0;
};
std::string Crypto::encryptRC4(const std::string& plaintext) {
std::vector<uint8_t> keyBytes(key_.begin(), key_.end());
if (keyBytes.empty()) throw CryptoException("RC4 key cannot be empty");
std::vector<uint8_t> data(plaintext.begin(), plaintext.end());
RC4 rc4(keyBytes);
rc4.process(data);
return base64Encode(data);
}
std::string Crypto::decryptRC4(const std::string& ciphertext) {
std::vector<uint8_t> keyBytes(key_.begin(), key_.end());
if (keyBytes.empty()) throw CryptoException("RC4 key cannot be empty");
std::vector<uint8_t> data = base64Decode(ciphertext);
RC4 rc4(keyBytes);
rc4.process(data);
return std::string(data.begin(), data.end());
}
// ============================================================================
// HttpClient Implementation
// ============================================================================
HttpClient::HttpClient() {
curl_global_init(CURL_GLOBAL_DEFAULT);
}
HttpClient::~HttpClient() {
curl_global_cleanup();
}
void HttpClient::setBaseUrl(const std::string& url) {
baseUrl_ = url;
if (!baseUrl_.empty() && baseUrl_.back() == '/') {
baseUrl_.pop_back();
}
}
void HttpClient::setTimeout(int seconds) {
timeout_ = seconds;
}
void HttpClient::addHeader(const std::string& key, const std::string& value) {
headers_[key] = value;
}
void HttpClient::clearHeaders() {
headers_.clear();
}
void HttpClient::setCrypto(std::shared_ptr<Crypto> crypto) {
crypto_ = crypto;
}
std::string HttpClient::buildUrl(const std::string& path) {
return baseUrl_ + path;
}
size_t HttpClient::writeCallback(void* contents, size_t size, size_t nmemb, std::string* userp) {
size_t totalSize = size * nmemb;
userp->append(static_cast<char*>(contents), totalSize);
return totalSize;
}
size_t HttpClient::writeBinaryCallback(void* contents, size_t size, size_t nmemb, std::vector<uint8_t>* userp) {
size_t totalSize = size * nmemb;
userp->insert(userp->end(), static_cast<uint8_t*>(contents), static_cast<uint8_t*>(contents) + totalSize);
return totalSize;
}
json HttpClient::processResponse(const std::string& response) {
std::string data = response;
if (crypto_) {
try {
data = crypto_->decrypt(response);
auto j = json::parse(data);
if (j.contains("data") && j["data"].is_string()) {
j["data"] = json::parse(crypto_->decrypt(j["data"].get<std::string>()));
}
return j;
} catch (...) {
// Try parsing as-is
}
}
return json::parse(response);
}
json HttpClient::get(const std::string& path) {
CURL* curl = curl_easy_init();
if (!curl) throw std::runtime_error("Failed to initialize CURL");
std::string response;
struct curl_slist* headers = nullptr;
for (const auto& h : headers_) {
std::string header = h.first + ": " + h.second;
headers = curl_slist_append(headers, header.c_str());
}
curl_easy_setopt(curl, CURLOPT_URL, buildUrl(path).c_str());
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, timeout_);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
CURLcode res = curl_easy_perform(curl);
long httpCode = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode);
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
if (res != CURLE_OK) {
throw std::runtime_error(std::string("CURL error: ") + curl_easy_strerror(res));
}
auto j = processResponse(response);
if (j["code"].get<int>() != 200) {
throw ApiException(j["code"].get<int>(), j["message"].get<std::string>());
}
return j;
}
json HttpClient::post(const std::string& path, const json& body) {
CURL* curl = curl_easy_init();
if (!curl) throw std::runtime_error("Failed to initialize CURL");
std::string response;
struct curl_slist* headers = nullptr;
headers = curl_slist_append(headers, "Content-Type: application/json");
for (const auto& h : headers_) {
std::string header = h.first + ": " + h.second;
headers = curl_slist_append(headers, header.c_str());
}
std::string bodyStr = body.dump();
curl_easy_setopt(curl, CURLOPT_URL, buildUrl(path).c_str());
curl_easy_setopt(curl, CURLOPT_POST, 1L);
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, bodyStr.c_str());
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, timeout_);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
CURLcode res = curl_easy_perform(curl);
long httpCode = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode);
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
if (res != CURLE_OK) {
throw std::runtime_error(std::string("CURL error: ") + curl_easy_strerror(res));
}
auto j = processResponse(response);
if (j["code"].get<int>() != 200) {
throw ApiException(j["code"].get<int>(), j["message"].get<std::string>());
}
return j;
}
json HttpClient::postForm(const std::string& path, const std::map<std::string, std::string>& fields, const std::map<std::string, std::string>& files) {
CURL* curl = curl_easy_init();
if (!curl) throw std::runtime_error("Failed to initialize CURL");
std::string response;
curl_mime* mime = curl_mime_init(curl);
struct curl_slist* headers = nullptr;
for (const auto& h : headers_) {
std::string header = h.first + ": " + h.second;
headers = curl_slist_append(headers, header.c_str());
}
for (const auto& f : fields) {
curl_mimepart* part = curl_mime_addpart(mime);
curl_mime_name(part, f.first.c_str());
curl_mime_data(part, f.second.c_str(), CURL_ZERO_TERMINATED);
}
for (const auto& f : files) {
curl_mimepart* part = curl_mime_addpart(mime);
curl_mime_name(part, f.first.c_str());
curl_mime_filedata(part, f.second.c_str());
}
curl_easy_setopt(curl, CURLOPT_URL, buildUrl(path).c_str());
curl_easy_setopt(curl, CURLOPT_MIMEPOST, mime);
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, timeout_);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
CURLcode res = curl_easy_perform(curl);
curl_mime_free(mime);
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
if (res != CURLE_OK) {
throw std::runtime_error(std::string("CURL error: ") + curl_easy_strerror(res));
}
auto j = processResponse(response);
if (j["code"].get<int>() != 200) {
throw ApiException(j["code"].get<int>(), j["message"].get<std::string>());
}
return j;
}
std::vector<uint8_t> HttpClient::download(const std::string& path) {
CURL* curl = curl_easy_init();
if (!curl) throw std::runtime_error("Failed to initialize CURL");
std::vector<uint8_t> response;
struct curl_slist* headers = nullptr;
for (const auto& h : headers_) {
std::string header = h.first + ": " + h.second;
headers = curl_slist_append(headers, header.c_str());
}
curl_easy_setopt(curl, CURLOPT_URL, buildUrl(path).c_str());
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeBinaryCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, timeout_);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
CURLcode res = curl_easy_perform(curl);
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
if (res != CURLE_OK) {
throw std::runtime_error(std::string("CURL error: ") + curl_easy_strerror(res));
}
return response;
}
// ============================================================================
// Client Implementation
// ============================================================================
Client::Client(const std::string& baseUrl, const std::string& appKey)
: baseUrl_(baseUrl), appKey_(appKey) {
http_ = std::make_shared<HttpClient>();
http_->setBaseUrl(baseUrl_);
}
Client::~Client() = default;
void Client::setToken(const std::string& token) {
token_ = token;
http_->addHeader("Authorization", "Bearer " + token_);
}
void Client::setEncryptType(EncryptType type, const std::string& key) {
encryptType_ = type;
crypto_ = std::make_shared<Crypto>(type, key);
http_->setCrypto(crypto_);
}
void Client::setTimeout(int seconds) {
http_->setTimeout(seconds);
}
std::string Client::buildPath(const std::string& endpoint) {
return "/api/v1/app/" + appKey_ + endpoint;
}
AppConfig Client::getAppInfo() {
auto j = http_->get(buildPath("/info"));
AppConfig config;
config.id = j["data"]["id"].get<uint64_t>();
config.name = j["data"]["name"].get<std::string>();
config.description = j["data"]["description"].get<std::string>();
config.iconUrl = j["data"]["icon_url"].get<std::string>();
config.status = j["data"]["status"].get<std::string>();
config.billingType = j["data"]["billing_type"].get<std::string>();
config.loginPolicy = j["data"]["login_policy"].get<std::string>();
config.maxDevices = j["data"]["max_devices"].get<int>();
config.multiOpenMode = j["data"]["multi_open_mode"].get<std::string>();
config.maxInstances = j["data"]["max_instances"].get<int>();
config.enableTrial = j["data"]["enable_trial"].get<bool>();
config.trialBalance = j["data"]["trial_balance"].get<double>();
config.heartbeatInterval = j["data"]["heartbeat_interval"].get<int>();
config.heartbeatTimeout = j["data"]["heartbeat_timeout"].get<int>();
return config;
}
VersionInfo Client::checkUpdate(const std::string& version) {
std::string path = buildPath("/check-update");
if (!version.empty()) {
path += "?version=" + version;
}
auto j = http_->get(path);
VersionInfo info;
info.hasUpdate = j["data"]["has_update"].get<bool>();
info.latestVersion = j["data"]["latest_version"].get<std::string>();
info.downloadUrl = j["data"]["download_url"].get<std::string>();
info.fileSize = j["data"]["file_size"].get<int64_t>();
info.fileHash = j["data"]["file_hash"].get<std::string>();
info.entryFile = j["data"]["entry_file"].get<std::string>();
info.updateNotes = j["data"]["update_notes"].get<std::string>();
info.updateStrategy = j["data"]["update_strategy"].get<std::string>();
info.updateType = j["data"]["update_type"].get<std::string>();
info.updateMethod = j["data"]["update_method"].get<std::string>();
return info;
}
std::vector<json> Client::getAnnouncements() {
auto j = http_->get(buildPath("/announcements"));
return j["data"].get<std::vector<json>>();
}
uint64_t Client::registerUser(const std::string& username,
const std::string& password,
const std::string& deviceId,
const std::string& deviceName,
const std::string& deviceType,
const std::string& instanceId,
const std::string& email,
const std::string& emailCode) {
json body;
body["username"] = username;
body["password"] = password;
body["device_id"] = deviceId;
if (!deviceName.empty()) body["device_name"] = deviceName;
if (!deviceType.empty()) body["device_type"] = deviceType;
if (!instanceId.empty()) body["instance_id"] = instanceId;
if (!email.empty()) body["email"] = email;
if (!emailCode.empty()) body["email_code"] = emailCode;
auto j = http_->post(buildPath("/register"), body);
return j["data"]["user_id"].get<uint64_t>();
}
json Client::login(const std::string& username,
const std::string& password,
const std::string& deviceId,
const std::string& deviceName,
const std::string& deviceType,
const std::string& instanceId) {
json body;
body["username"] = username;
body["password"] = password;
body["device_id"] = deviceId;
if (!deviceName.empty()) body["device_name"] = deviceName;
if (!deviceType.empty()) body["device_type"] = deviceType;
if (!instanceId.empty()) body["instance_id"] = instanceId;
auto j = http_->post(buildPath("/login"), body);
// Auto-set token
if (j["data"].contains("token")) {
setToken(j["data"]["token"].get<std::string>());
}
return j["data"];
}
void Client::sendEmailCode(const std::string& email, const std::string& purpose) {
json body;
body["email"] = email;
body["purpose"] = purpose;
http_->post(buildPath("/send-email-code"), body);
}
void Client::resetPassword(const std::string& email, const std::string& code, const std::string& newPassword) {
json body;
body["email"] = email;
body["code"] = code;
body["password"] = newPassword;
http_->post(buildPath("/reset-password"), body);
}
void Client::changePassword(const std::string& username, const std::string& oldPassword, const std::string& newPassword) {
json body;
body["username"] = username;
body["old_password"] = oldPassword;
body["new_password"] = newPassword;
http_->post(buildPath("/change-password"), body);
}
UserInfo Client::getAccount(uint64_t userId) {
json body;
body["user_id"] = userId;
auto j = http_->post(buildPath("/account"), body);
UserInfo info;
info.userId = j["data"]["user_id"].get<uint64_t>();
info.username = j["data"]["username"].get<std::string>();
info.balance = j["data"]["balance"].get<double>();
info.status = j["data"]["status"].get<std::string>();
return info;
}
json Client::heartbeat(uint64_t userId, const std::string& deviceId, const std::string& instanceId) {
json body;
body["user_id"] = userId;
body["device_id"] = deviceId;
if (!instanceId.empty()) body["instance_id"] = instanceId;
return http_->post(buildPath("/heartbeat"), body)["data"];
}
json Client::recharge(const std::string& username, const std::string& cardKey, const std::string& deviceId) {
json body;
body["username"] = username;
body["card_key"] = cardKey;
body["device_id"] = deviceId;
return http_->post(buildPath("/recharge"), body)["data"];
}
json Client::trial(uint64_t userId) {
json body;
body["user_id"] = userId;
return http_->post(buildPath("/trial"), body)["data"];
}
std::vector<DeviceInfo> Client::getDevices() {
auto j = http_->get(buildPath("/devices"));
std::vector<DeviceInfo> devices;
for (const auto& d : j["data"]) {
DeviceInfo info;
info.id = d["id"].get<uint64_t>();
info.deviceId = d["device_id"].get<std::string>();
info.deviceName = d["device_name"].get<std::string>();
info.deviceType = d["device_type"].get<std::string>();
info.status = d["status"].get<std::string>();
info.onlineSessions = d["online_sessions"].get<int>();
info.createdAt = d["created_at"].get<std::string>();
devices.push_back(info);
}
return devices;
}
json Client::getDeviceCount(uint64_t userId) {
json body;
body["user_id"] = userId;
return http_->post(buildPath("/device-count"), body)["data"];
}
void Client::unbindDevice(uint64_t userId, const std::string& deviceId) {
json body;
body["user_id"] = userId;
body["device_id"] = deviceId;
http_->post(buildPath("/unbind-device"), body);
}
void Client::unbindDeviceWithAuth(const std::string& username, const std::string& password, const std::string& deviceId) {
json body;
body["username"] = username;
body["password"] = password;
body["device_id"] = deviceId;
http_->post(buildPath("/unbind-device-with-auth"), body);
}
std::vector<json> Client::getInstances(uint64_t userId) {
json body;
body["user_id"] = userId;
return http_->post(buildPath("/instances"), body)["data"].get<std::vector<json>>();
}
void Client::forceOfflineInstance(uint64_t userId, const std::string& instanceId) {
json body;
body["user_id"] = userId;
http_->post(buildPath("/instances/" + instanceId + "/offline"), body);
}
json Client::getConstants() {
return http_->get(buildPath("/constants"))["data"];
}
json Client::getConstant(const std::string& key) {
return http_->get(buildPath("/constants/" + key))["data"];
}
std::vector<uint8_t> Client::downloadConstant(const std::string& key) {
return http_->download(buildPath("/constants/" + key + "/download"));
}
json Client::getVariables() {
return http_->get(buildPath("/variables"))["data"];
}
json Client::getVariable(const std::string& key) {
return http_->get(buildPath("/variables/" + key))["data"];
}
std::vector<uint8_t> Client::downloadVariable(const std::string& key) {
return http_->download(buildPath("/variables/" + key + "/download"));
}
json Client::uploadVariable(const std::string& key, const std::string& filePath) {
return http_->postForm(buildPath("/variables/" + key + "/upload"), {}, {{"file", filePath}})["data"];
}
void Client::updateVariables(const std::map<std::string, std::string>& variables) {
json body;
body["variables"] = variables;
http_->post(buildPath("/variables"), body);
}
json Client::createVariableRecord(const std::string& key, const json& data) {
return http_->post(buildPath("/variables/" + key + "/records"), data)["data"];
}
json Client::getVariableRecords(const std::string& key, int page, int pageSize) {
std::string path = buildPath("/variables/" + key + "/records") +
"?page=" + std::to_string(page) + "&page_size=" + std::to_string(pageSize);
return http_->get(path)["data"];
}
void Client::deleteVariableRecord(const std::string& key, uint64_t recordId) {
http_->get(buildPath("/variables/" + key + "/records/" + std::to_string(recordId)));
}
json Client::executeDynamicCode(const std::string& key, const json& params, uint64_t targetUserId) {
json body;
body["params"] = params;
if (targetUserId > 0) {
body["user_id"] = targetUserId;
}
return http_->post(buildPath("/dynamic-code/" + key + "/execute"), body)["data"];
}
} // namespace verify
+230
View File
@@ -0,0 +1,230 @@
#ifndef VERIFY_CLIENT_HPP
#define VERIFY_CLIENT_HPP
#include <string>
#include <map>
#include <functional>
#include <memory>
#include <vector>
#ifdef USE_OPENSSL
#include <openssl/evp.h>
#include <openssl/rand.h>
#endif
#ifdef USE_SYSTEM_CURL
#include <curl/curl.h>
#else
// 简化的CURL模拟 - 用于测试
#endif
#include "json.hpp"
namespace verify {
using json = nlohmann::json;
class CryptoException : public std::exception {
public:
explicit CryptoException(const std::string& msg) : message_(msg) {}
const char* what() const noexcept override { return message_.c_str(); }
private:
std::string message_;
};
class ApiException : public std::exception {
public:
ApiException(int code, const std::string& msg) : code_(code), message_(msg) {}
int code() const { return code_; }
const char* what() const noexcept override { return message_.c_str(); }
private:
int code_;
std::string message_;
};
enum class EncryptType {
None,
AES,
RC4
};
class Crypto {
public:
Crypto(EncryptType type, const std::string& key);
~Crypto();
std::string encrypt(const std::string& plaintext);
std::string decrypt(const std::string& ciphertext);
private:
EncryptType type_;
std::string key_;
std::string encryptAES(const std::string& plaintext);
std::string decryptAES(const std::string& ciphertext);
std::string encryptRC4(const std::string& plaintext);
std::string decryptRC4(const std::string& ciphertext);
std::vector<uint8_t> padKey(const std::vector<uint8_t>& key, size_t targetLen);
};
class HttpClient {
public:
HttpClient();
~HttpClient();
void setBaseUrl(const std::string& url);
void setTimeout(int seconds);
void addHeader(const std::string& key, const std::string& value);
void clearHeaders();
json get(const std::string& path);
json post(const std::string& path, const json& body);
json postForm(const std::string& path, const std::map<std::string, std::string>& fields, const std::map<std::string, std::string>& files = {});
std::vector<uint8_t> download(const std::string& path);
void setCrypto(std::shared_ptr<Crypto> crypto);
private:
std::string baseUrl_;
int timeout_ = 30;
std::map<std::string, std::string> headers_;
std::shared_ptr<Crypto> crypto_;
static size_t writeCallback(void* contents, size_t size, size_t nmemb, std::string* userp);
static size_t writeBinaryCallback(void* contents, size_t size, size_t nmemb, std::vector<uint8_t>* userp);
std::string buildUrl(const std::string& path);
json processResponse(const std::string& response);
};
struct AppConfig {
uint64_t id;
std::string name;
std::string description;
std::string iconUrl;
std::string status;
std::string billingType;
std::string loginPolicy;
int maxDevices;
std::string multiOpenMode;
int maxInstances;
bool enableTrial;
double trialBalance;
int heartbeatInterval;
int heartbeatTimeout;
};
struct UserInfo {
uint64_t userId;
std::string username;
double balance;
std::string status;
};
struct DeviceInfo {
uint64_t id;
std::string deviceId;
std::string deviceName;
std::string deviceType;
std::string status;
int onlineSessions;
std::string createdAt;
};
struct VersionInfo {
bool hasUpdate;
std::string latestVersion;
std::string downloadUrl;
int64_t fileSize;
std::string fileHash;
std::string entryFile;
std::string updateNotes;
std::string updateStrategy;
std::string updateType;
std::string updateMethod;
};
class Client {
public:
Client(const std::string& baseUrl, const std::string& appKey);
~Client();
void setToken(const std::string& token);
void setEncryptType(EncryptType type, const std::string& key);
void setTimeout(int seconds);
// Application info
AppConfig getAppInfo();
VersionInfo checkUpdate(const std::string& version = "");
std::vector<json> getAnnouncements();
// User authentication
uint64_t registerUser(const std::string& username,
const std::string& password,
const std::string& deviceId,
const std::string& deviceName = "",
const std::string& deviceType = "windows",
const std::string& instanceId = "",
const std::string& email = "",
const std::string& emailCode = "");
json login(const std::string& username,
const std::string& password,
const std::string& deviceId,
const std::string& deviceName = "",
const std::string& deviceType = "windows",
const std::string& instanceId = "");
void sendEmailCode(const std::string& email, const std::string& purpose = "register");
void resetPassword(const std::string& email, const std::string& code, const std::string& newPassword);
void changePassword(const std::string& username, const std::string& oldPassword, const std::string& newPassword);
// Account
UserInfo getAccount(uint64_t userId);
json heartbeat(uint64_t userId, const std::string& deviceId, const std::string& instanceId = "");
// Recharge
json recharge(const std::string& username, const std::string& cardKey, const std::string& deviceId);
json trial(uint64_t userId);
// Device management
std::vector<DeviceInfo> getDevices();
json getDeviceCount(uint64_t userId);
void unbindDevice(uint64_t userId, const std::string& deviceId);
void unbindDeviceWithAuth(const std::string& username, const std::string& password, const std::string& deviceId);
std::vector<json> getInstances(uint64_t userId);
void forceOfflineInstance(uint64_t userId, const std::string& instanceId);
// Cloud data
json getConstants();
json getConstant(const std::string& key);
std::vector<uint8_t> downloadConstant(const std::string& key);
json getVariables();
json getVariable(const std::string& key);
std::vector<uint8_t> downloadVariable(const std::string& key);
json uploadVariable(const std::string& key, const std::string& filePath);
void updateVariables(const std::map<std::string, std::string>& variables);
json createVariableRecord(const std::string& key, const json& data);
json getVariableRecords(const std::string& key, int page = 1, int pageSize = 20);
void deleteVariableRecord(const std::string& key, uint64_t recordId);
// Dynamic code
json executeDynamicCode(const std::string& key, const json& params, uint64_t targetUserId = 0);
private:
std::string baseUrl_;
std::string appKey_;
std::string token_;
std::shared_ptr<HttpClient> http_;
std::shared_ptr<Crypto> crypto_;
EncryptType encryptType_ = EncryptType::None;
std::string buildPath(const std::string& endpoint);
};
} // namespace verify
#endif // VERIFY_CLIENT_HPP