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:
@@ -48,6 +48,8 @@ func handleAppGetInfo(c *gin.Context) {
|
|||||||
|
|
||||||
func handleAppCheckUpdate(c *gin.Context) {
|
func handleAppCheckUpdate(c *gin.Context) {
|
||||||
appKey := c.Param("appKey")
|
appKey := c.Param("appKey")
|
||||||
|
clientVersion := c.Query("version")
|
||||||
|
|
||||||
var app model.Application
|
var app model.Application
|
||||||
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
|
||||||
response.Error(c, 404, "应用不存在")
|
response.Error(c, 404, "应用不存在")
|
||||||
@@ -59,14 +61,21 @@ func handleAppCheckUpdate(c *gin.Context) {
|
|||||||
return
|
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 len(allVersions) == 0 {
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
response.Success(c, gin.H{
|
response.Success(c, gin.H{
|
||||||
"has_update": false,
|
"has_update": false,
|
||||||
|
"current_version": clientVersion,
|
||||||
"latest_version": "",
|
"latest_version": "",
|
||||||
"download_url": "",
|
"download_url": "",
|
||||||
"update_notes": "",
|
"update_notes": "",
|
||||||
@@ -77,14 +86,82 @@ func handleAppCheckUpdate(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
hasUpdate := clientVersion != latestVersion.Version
|
// 客户端必须提供版本号
|
||||||
if latestVersion.UpdateStrategy == "forced" {
|
if clientVersion == "" {
|
||||||
hasUpdate = true
|
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 {
|
for i, f := range latestVersion.Files {
|
||||||
files[i] = gin.H{
|
files[i] = map[string]interface{}{
|
||||||
"file_path": f.FilePath,
|
"file_path": f.FilePath,
|
||||||
"file_name": f.FileName,
|
"file_name": f.FileName,
|
||||||
"file_size": f.FileSize,
|
"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,
|
"latest_version": latestVersion.Version,
|
||||||
"download_url": latestVersion.FilePath,
|
"download_url": latestVersion.FilePath,
|
||||||
"file_size": latestVersion.FileSize,
|
"file_size": latestVersion.FileSize,
|
||||||
"file_hash": latestVersion.FileHash,
|
"file_hash": latestVersion.FileHash,
|
||||||
"entry_file": latestVersion.EntryFile,
|
"entry_file": latestVersion.EntryFile,
|
||||||
"update_notes": latestVersion.Description,
|
"update_notes": latestVersion.Description,
|
||||||
"update_strategy": latestVersion.UpdateStrategy,
|
"update_strategy": updateStrategy,
|
||||||
"update_type": latestVersion.UpdateType,
|
"update_type": latestVersion.UpdateType,
|
||||||
"update_method": latestVersion.UpdateMethod,
|
"update_method": latestVersion.UpdateMethod,
|
||||||
"changelog": latestVersion.Changelog,
|
"changelog": latestVersion.Changelog,
|
||||||
"files": files,
|
"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) {
|
func handleAppGetAnnouncements(c *gin.Context) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -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 "")
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"name": "verify-sdk",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "C++ SDK for Verify Platform",
|
||||||
|
"dependencies": [
|
||||||
|
"curl",
|
||||||
|
"nlohmann-json",
|
||||||
|
"openssl"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user