Cloud Save 服务端API接入指南
Cloud Save 服务端API接入指南
Cloud Save 服务端 API
Cloud Save 服务端 API 是面向需要在服务端集成 Cloud Save 的开发者提供的服务端集成的 API 列表。
API Endpoint: https://save.unity.cn
Cloud Save 服务端 API 授权
使用 Basic Authorization 来进行鉴权 (推荐服务器端程序使用). 步骤如下:
在 UOS 网站上获取当前需要使用的 UOS APP 的 AppId 和 AppServiceSecret
注:此处 AppId 和 AppServiceSecret,可在 UOS 网站上获取


在请求任意 API 的 Request Header 中增加 Header:
- Authorization: Basic base64(appId:appServiceSecret)
示例代码
C## C# 使用示例 - Basic 授权方式 using System; using System.Collections.Generic; using System.Net.Http; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; using Newtonsoft.Json; class Program { private static string GetBasicAuthorization(string appId, string appServiceSecret) { string credentials = $"{appId}:{appServiceSecret}"; byte[] credentialsBytes = Encoding.UTF8.GetBytes(credentials); string encodedCredentials = Convert.ToBase64String(credentialsBytes); return $"Basic {encodedCredentials}"; } public static async Task Main(string[] args) { using (HttpClient client = new HttpClient()) { // 使用项目的 APP_ID, APP_SERVICE_SECRET 获取 headers client.DefaultRequestHeaders.Add("Authorization", GetBasicAuthorization(APP_ID, APP_SERVICE_SECRET)); // 以get方法为例,替换 url 为你需要请求的 url HttpResponseMessage response = await client.GetAsync(url); response.EnsureSuccessStatusCode(); // 其他处理…… } } }Python# Python 使用示例 - Basic 授权方式 import base64 import requests def get_basic_authorization(app_id, app_service_secret): """ 获取basic auth的Header """ credentials = f'{app_id}:{app_service_secret}' encoded_credentials = base64.b64encode(credentials.encode("utf-8")).decode("utf-8") return {'Authorization': f'Basic {encoded_credentials}'} # 使用 APP_ID, APP_SERVICE_SECRET 获取Basic Token Header headers = get_basic_authorization(APP_ID, APP_SERVICE_SECRET) # 以get方法为例,替换 url 为你需要请求的 url response = requests.get(url, headers=headers)JavaScript// JavaScript 使用示例 - Basic 授权方式 const axios = require('axios'); function getBasicAuthorization(appId, appServiceSecret) { /** 获取 basic auth 的 Header */ const credentials = `${appId}:${appServiceSecret}`; const encodedCredentials = btoa(credentials); return { 'Authorization': `Basic ${encodedCredentials}` }; } // 使用 APP_ID, APP_SERVICE_SECRET 获取Basic Token Header const headers = getBasicAuthorization(APP_ID, APP_SERVICE_SECRET); // 以get方法为例,替换 url 为你需要请求的 url const response = await axios.get(url, { headers });Go// Go 使用示例 - Basic 授权方式 import ( "bytes" "encoding/base64" "encoding/json" "fmt" "io" "net/http" ) func GetBasicAuthorization(appID, appServiceSecret string) string { credentials := appID + ":" + appServiceSecret encodedCredentials := base64.StdEncoding.EncodeToString([]byte(credentials)) return "Basic " + encodedCredentials } func main() { // 以get方法为例,替换 url 为你需要请求的 url req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, err } // 使用 APP_ID, APP_SERVICE_SECRET 获取Basic Token Header req.Header.Set("Authorization", GetBasicAuthorization(APP_ID, APP_SERVICE_SECRET)) client := &http.Client{} response, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() // ……其他处理 }
Cloud Save 服务端 API 列表
ProfilePicture
用于获取、创建、更新和删除玩家头像等操作。
API 调用流程
- POST /v1/pfps/upload-token 获取默认头像上传 Token
- 使用临时 Token 上传默认头像至存储桶
- POST /v1/pfps 确认上传并创建默认头像
- GET /v1/pfps 获取默认头像列表
- POST /v1/pfps 玩家1直接使用默认头像
- POST /v1/pfps/upload-token 获取玩家2头像上传 Token
- 使用临时 Token 上传玩家2头像至存储桶
- POST /v1/pfps 确认上传并创建玩家2头像
- GET /v1/pfps 根据玩家ID获取头像列表
- DELETE /v1/pfps 批量删除头像和云端文件对象 (代码已注释,默认不执行)
玩家头像 API 调用示例
# [UOS Save - 云存档]服务端API使用示例
# 功能模块具体使用方法可参考 https://uos.unity.cn/docs/crud/save/concept.html
# API完整参数列表可参考 https://uos.unity.cn/docs/crud/save/server-api.html
# 注意:运行前请安装 qcloud cos SDK 和 Python Imaging Library (PIL)
# pip install -U cos-python-sdk-v5
# pip install -U Pillow
# -*-coding: utf-8-*-
import base64
import os
import uuid
import requests
from datetime import datetime
from PIL import Image
from qcloud_cos import CosConfig
from qcloud_cos import CosS3Client
# 配置
BASE_URL = 'https://save.unity.cn'
APP_ID = '' # App ID
APP_SERVICE_SECRET = '' # App Service Secret
USER_ID_1 = '' # 用户ID 1 (必填)
USER_ID_2 = '' # 用户ID 2 (必填)
PERSONA_ID_1 = '' # 角色ID 1 (选填)
PERSONA_ID_2 = '' # 角色ID 2 (选填)
if len(APP_ID) == 0 or len(APP_SERVICE_SECRET) == 0:
print("请填入自己UOS App的App ID和App Service Secret,可前往https://uos.unity.cn/apps下进入App后在「设置」中获取")
exit(1)
RAND_ID = datetime.today().strftime('%Y%m%d') + "-" + str(uuid.uuid4())[:8]
if len(USER_ID_1) == 0 or len(USER_ID_1) == 0:
USER_ID_1 = "user-" + RAND_ID + "-1"
USER_ID_2 = "user-" + RAND_ID + "-2"
def get_basic_auth_header():
""" 生成 Auth Header """
credentials = f"{APP_ID}:{APP_SERVICE_SECRET}".encode("utf-8")
encoded_credentials = base64.b64encode(credentials).decode("utf-8")
return f"Basic {encoded_credentials}"
HEADERS = {
"Content-Type": "application/json",
"Accept": "*/*",
"Authorization": get_basic_auth_header()
}
def handle_response(response):
""" 处理返回 """
try:
response.raise_for_status()
except requests.exceptions.HTTPError as e:
print(f"HTTP Error: {e}")
print(response.text)
raise
except requests.exceptions.RequestException as e:
print(f"Error: {e}")
raise
else:
return response.json()
def get_pfp_upload_token(is_default, user_id, persona_id, pfp_format):
""" 获取头像上传Token """
url = BASE_URL + "/v1/pfps/upload-token"
data = {
"isDefault": is_default,
"format": pfp_format,
}
if user_id != "":
data["userId"] = user_id
if persona_id != "":
data["personaId"] = persona_id
response = requests.post(url, headers=HEADERS, json=data)
return handle_response(response)
def create_or_update_pfp(pfp_id, user_id, persona_id):
""" 创建或更新头像 """
url = BASE_URL + "/v1/pfps"
data = {"pfpId": pfp_id}
if user_id != "":
data["userId"] = user_id
if persona_id != "":
data["personaId"] = persona_id
response = requests.post(url, headers=HEADERS, json=data)
return handle_response(response)
def list_pfp(default_only, start, count, user_id, persona_id):
""" 获取头像列表 """
url = BASE_URL + "/v1/pfps"
params = {"defaultOnly": default_only}
if start > 0:
params["start"] = start
if count > 0:
params["count"] = count
if len(user_id) > 0:
params["userId"] = user_id
if len(persona_id) > 0:
params["personaId"] = persona_id
response = requests.get(url, headers=HEADERS, params=params)
return handle_response(response)
def delete_pfp(pfp_id_list):
""" 删除存档 """
params = "&pfpIds=".join(pfp_id_list)
url = BASE_URL + "/v1/pfps" + "?pfpIds=" + params
response = requests.delete(url, headers=HEADERS)
if response.status_code == 204:
return {"message": "Successfully deleted."}
else:
return handle_response(response)
def upload_object(uos_upload_token, object_local_path):
""" 根据Token上传对象 """
token = uos_upload_token["token"]
secret_id = uos_upload_token["tmpSecretId"]
secret_key = uos_upload_token["tmpSecretKey"]
region = uos_upload_token["region"]
bucket = uos_upload_token["bucketName"]
object_storage_key = uos_upload_token["objectDir"] + "/" + uos_upload_token["objectName"]
config = CosConfig(Region=region, SecretId=secret_id, SecretKey=secret_key, Token=token, Scheme='https')
client = CosS3Client(config)
response = client.upload_file(
Bucket=bucket,
Key=object_storage_key,
LocalFilePath=object_local_path,
EnableMD5=False,
progress_callback=None
)
return response
def prepare_pfp(filename: str, color) -> None:
""" 生成头像图片 """
if not os.path.exists(filename):
size = (10, 10)
image = Image.new("RGB", size, color)
image.save(filename, "PNG")
################
# Example usage
################
print("AppId:", APP_ID)
print("UserId1:", USER_ID_1)
print("PersonaId1:", PERSONA_ID_1)
print("UserId2:", USER_ID_2)
print("PersonaId2:", PERSONA_ID_2)
# 准备文件
if not os.path.exists("data"):
os.makedirs("data")
# 准备默认头像
# - 默认头像由游戏开发者上传并管理
# - 玩家可以选择开发者提供的默认头像
prepare_pfp("data/default_pfp.png", (224, 17, 95)) # Ruby (224, 17, 95)
# 准备玩家头像
# - 玩家头像由每个玩家自行管理
prepare_pfp("data/player_pfp.png", (0, 128, 128)) # Teal (0, 128, 128)
# 1. 获取默认头像上传Token
token_response = get_pfp_upload_token(True, "", "", "png")
print("Upload token:", token_response)
default_pfp_upload_token = token_response.get("uploadToken", {})
default_pfp_id = token_response.get("pfpId", "")
print("Default PFP ID:", default_pfp_id)
# 2. 上传默认头像
upload_response = upload_object(default_pfp_upload_token, "data/default_pfp.png")
print("Upload object response:", upload_response)
# 3. 确认上传并创建默认头像
create_response = create_or_update_pfp(default_pfp_id, "", "")
print("Default PFP created:", create_response)
# 4. 获取默认头像列表
list_response = list_pfp(True, 0, 0, "", "")
print("Default PFP list:", list_response)
# 5. 玩家1直接使用默认头像
create_response = create_or_update_pfp(default_pfp_id, USER_ID_1, PERSONA_ID_1)
print("Linked player 1 to Default PFP:", create_response)
# 6. 玩家2获取玩家头像上传Token
token_response = get_pfp_upload_token(False, USER_ID_2, PERSONA_ID_2, "png")
print("Upload token:", token_response)
player_pfp_upload_token = token_response.get("uploadToken", {})
player_pfp_id = token_response.get("pfpId", "")
print("Player PFP ID:", player_pfp_id)
# 7. 上传玩家头像
upload_response = upload_object(player_pfp_upload_token, "data/player_pfp.png")
print("Upload object response:", upload_response)
# 8. 确认上传并创建玩家头像
create_response = create_or_update_pfp(player_pfp_id, USER_ID_2, PERSONA_ID_2)
print("Player PFP created:", create_response)
# 9. 根据玩家ID获取头像列表
list_response = list_pfp(False, 0, 20, "", "")
print("Player PFP list:", list_response)
# 10. 批量删除头像
# delete_pfp_list = [default_pfp_id, player_pfp_id]
# delete_status = delete_pfp(delete_pfp_list)
# print("PFP Deleted:", delete_status)Save
用于获取、创建、更新和删除存档等操作。
API 调用流程
- POST /v1/saves/upload-token 获取临时上传 Token
- 使用临时 Token 上传本地文件和封面至存储桶
- POST /v1/saves 创建存档,确认文件和封面上传完成并传入存档元数据
- GET /v1/saves/{saveId} 根据存档 ID 获取存档细节
- POST /v1/saves/upload-token 针对刚刚创建的存档,再次获取新的临时上传 Token
- 使用新的临时 Token 上传新的文件和封面至存储桶
- POST /v1/saves 更新存档,更新文件和封面以及存档元数据
- DELETE /v1/saves/{saveId} 根据存档 ID 删除存档和云端文件对象 (代码已注释,默认不执行)
存档 API 调用示例
# [UOS Save - 云存档]服务端API使用示例
# 功能模块具体使用方法可参考 https://uos.unity.cn/docs/crud/save/concept.html
# API完整参数列表可参考 https://uos.unity.cn/docs/crud/save/server-api.html
# 注意:运行前请安装 qcloud cos SDK 和 Python Imaging Library (PIL)
# pip install -U cos-python-sdk-v5
# pip install -U Pillow
# -*-coding: utf-8-*-
import base64
import os
import uuid
import requests
from datetime import datetime
from PIL import Image
from qcloud_cos import CosConfig
from qcloud_cos import CosS3Client
# 配置
BASE_URL = 'https://save.unity.cn'
APP_ID = '' # App ID
APP_SERVICE_SECRET = '' # App Service Secret
USER_ID = '' # 用户ID (必填)
PERSONA_ID = '' # 角色ID (选填)
if len(APP_ID) == 0 or len(APP_SERVICE_SECRET) == 0:
print("请填入自己UOS App的App ID和App Service Secret,可前往https://uos.unity.cn/apps下进入App后在「设置」中获取")
exit(1)
if len(USER_ID) == 0:
USER_ID = "user-" + datetime.today().strftime('%Y%m%d') + "-" + str(uuid.uuid4())[:8]
def get_basic_auth_header():
""" 生成 Auth Header """
credentials = f"{APP_ID}:{APP_SERVICE_SECRET}".encode("utf-8")
encoded_credentials = base64.b64encode(credentials).decode("utf-8")
return f"Basic {encoded_credentials}"
HEADERS = {
"Content-Type": "application/json",
"Accept": "*/*",
"Authorization": get_basic_auth_header()
}
def handle_response(response):
""" 处理返回 """
try:
response.raise_for_status()
except requests.exceptions.HTTPError as e:
print(f"HTTP Error: {e}")
print(response.text)
raise
except requests.exceptions.RequestException as e:
print(f"Error: {e}")
raise
else:
return response.json()
def get_upload_token(user_id, persona_id, save_id):
""" 获取上传Token """
url = BASE_URL + "/v1/saves/upload-token"
data = {"userId": user_id,
"coverUploadRequest": {"format": "png", "originalName": "cover1.png"},
"fileUploadRequest": {"format": "", "originalName": "file1"}
}
if persona_id != "":
data["personaId"] = persona_id
if save_id != "":
data["saveId"] = save_id
response = requests.post(url, headers=HEADERS, json=data)
return handle_response(response)
def create_or_update_save(user_id, persona_id, save_id, save_data, cover_id, file_id):
""" 创建或更新存档 """
url = BASE_URL + "/v1/saves"
data = save_data
data["userId"] = user_id
data["saveId"] = save_id
if persona_id != "":
data["personaId"] = persona_id
if cover_id != "":
data["coverUploadRequest"] = {
"clearExisting": False,
"objectId": cover_id
}
if file_id != "":
data["fileUploadRequest"] = {
"clearExisting": False,
"objectId": file_id
}
response = requests.post(url, headers=HEADERS, json=data)
return handle_response(response)
def get_save(save_id):
""" 获取存档细节 """
url = BASE_URL + "/v1/saves/" + save_id
response = requests.get(url, headers=HEADERS)
return handle_response(response)
def delete_save(save_id, params):
""" 删除存档 """
url = BASE_URL + "/v1/saves/" + save_id
response = requests.delete(url, headers=HEADERS, params=params)
if response.status_code == 204:
return {"message": "Successfully deleted."}
else:
return handle_response(response)
def upload_object(uos_upload_token, object_local_path):
""" 根据Token上传对象 """
token = uos_upload_token["token"]
secret_id = uos_upload_token["tmpSecretId"]
secret_key = uos_upload_token["tmpSecretKey"]
region = uos_upload_token["region"]
bucket = uos_upload_token["bucketName"]
object_storage_key = uos_upload_token["objectDir"] + "/" + uos_upload_token["objectName"]
config = CosConfig(Region=region, SecretId=secret_id, SecretKey=secret_key, Token=token, Scheme='https')
client = CosS3Client(config)
response = client.upload_file(
Bucket=bucket,
Key=object_storage_key,
LocalFilePath=object_local_path,
EnableMD5=False,
progress_callback=None
)
return response
def prepare_cover(filename: str, color) -> None:
""" 生成封面图片 """
if not os.path.exists(filename):
size = (10, 10)
image = Image.new("RGB", size, color)
image.save(filename, "PNG")
def prepare_file(file_path: str, text: str) -> None:
""" 生成文件 """
with open(file_path, 'w', encoding='utf-8') as file:
file.write(text)
################
# Example usage
################
print("AppId:", APP_ID)
print("UserId:", USER_ID)
print("PersonaId:", PERSONA_ID)
# 准备文件
if not os.path.exists("data"):
os.makedirs("data")
prepare_cover("data/cover1.png", (255, 165, 0)) # orange (255, 165, 0)
prepare_cover("data/cover2.png", (15, 82, 186)) # deep blue (15, 82, 186)
prepare_file("data/file1", "test1")
prepare_file("data/file2", "test2")
# 1. 获取上传 Token
token_response = get_upload_token(USER_ID, PERSONA_ID, "")
print("Upload Token:", token_response)
SAVE_ID = token_response["saveId"]
print("SaveId:", SAVE_ID)
# 2. 上传文件和封面
# 上传封面
upload_token = token_response["coverUploadToken"]
cover_id_1 = upload_token["objectId"]
upload_response = upload_object(upload_token, "data/cover1.png")
print("Upload object response:", upload_response)
# 上传文件
upload_token = token_response["fileUploadToken"]
file_id_1 = upload_token["objectId"]
upload_response = upload_object(upload_token, "data/file1")
print("Upload object response:", upload_response)
# 3. 创建存档
new_save_data = {
"name": "New Save",
"description": "A new game save entry",
"namespace": "test",
"properties": {"total_time": 500}
}
create_response = create_or_update_save(USER_ID, PERSONA_ID, SAVE_ID, new_save_data, cover_id_1, file_id_1)
print("Save Created:", create_response)
# 4. 获取存档信息
save_details = get_save(SAVE_ID)
print("Save Details:", save_details)
# 5. 获取新的上传Token
token_response_update = get_upload_token(USER_ID, PERSONA_ID, SAVE_ID) # 更新操作需要传入 Save ID
print("New Upload Token:", token_response_update)
# 6. 上传更新后的文件和封面
# 上传封面
upload_token = token_response_update["coverUploadToken"]
cover_id_2 = upload_token["objectId"]
upload_response = upload_object(upload_token, "data/cover2.png")
print("Upload object response:", upload_response)
# 上传文件
upload_token = token_response_update["fileUploadToken"]
file_id_2 = upload_token["objectId"]
upload_response = upload_object(upload_token, "data/file2")
print("Upload object response:", upload_response)
# 7. 更新存档
update_save_data = {
"name": "Updated Save",
"description": "An updated game save entry",
"namespace": "test",
"properties": {"total_time": 1000}
}
update_response = create_or_update_save(USER_ID, PERSONA_ID, SAVE_ID, update_save_data, cover_id_2, file_id_2)
print("Save Updated:", update_response)
# 8. 删除存档
# delete_params = {"hardDelete": True} # 删除文件
# delete_status = delete_save(SAVE_ID, delete_params)
# print("Save Deleted:", delete_status)Common
用于读取存档限制。