键值数据存储 (KV)
分类代号:C.8 本章共 5 个接口
vjmap 内置了一个 KV 数据表(SQLite),用来存用户配置、缩略图、缓存数据等零碎东西。注意它是全局共享的——后端不接受 workspace 参数,多工作区部署时用 key 前缀自行区分。"零碎"并不代表"不好用"——二开里非常常用,比如存每个用户的图层可见性、自定义地图标注等。
存储位置:后端 data/customData.db。
自动前缀:所有用户可见 key 都加了隐形 data_ 前缀,本 SDK 已屏蔽此细节。
TTL:可选过期时间(秒),到期自动清理。
批量 key 分隔符:路径里 ;;(双分号)分隔多个 key。
⚠️ key 字符限制:key 不能含
/——saveCustomData('ws_a/theme')无论编码%2F还是原样都 404(路由data/{key}是单段匹配)。命名空间分隔推荐:,如ws_a:theme;不要用_/%当分隔符(前缀查询/删除里它们是 SQL 通配符,见下),也不要含'。⚠️ TTL 到期语义:到期后 GET 不是返回
{"status":"keyNoExist"},而是 HTTP 403{code:7,msg:"key ttl expire"}——SDK 会当 HTTP 错误抛异常,必须try/catch,不能if (d.status==='keyNoExist')。残留键仍可deleteCustomData正常删除。⚠️ 前缀匹配是 SQL
LIKE(后端拼成key like '<data_ + prefix>%',字符串直接拼进 SQL):
- 无分隔符保护:
deleteCustomData('ws_a', {isPrefix:true})会把ws_ax、ws_a2一并删掉——前缀要自带分隔尾缀(如ws_a:)_匹配任意单字符、%匹配任意串:前缀a_b:也会命中axb:...- SQLite
LIKE对 ASCII 不区分大小写:前缀ABC:会命中abc:...- 前缀里含
'会破坏 SQL(报错)
datakeys?prefix=列键与isPrefix删除同受影响。
快速索引
| 序号 | SDK 方法 | URL |
|---|---|---|
| 53 | get_custom_data / getCustomData |
GET /api/v1/service/data/{key} |
| 54 | save_custom_data / saveCustomData |
POST /api/v1/service/data/{key} |
| 55 | delete_custom_data / deleteCustomData |
DELETE /api/v1/service/data/{key} |
| 56 | save_multiple_custom_data / saveMultipleCustomData |
POST /api/v1/service/datas |
| 57 | get_custom_data_keys / getCustomDataKeys |
GET /api/v1/service/datakeys?prefix= |
53 · 获取值 data/{key} GET
查询参数
| 字段 | 取值 | 说明 |
|---|---|---|
retDataType |
"" / "value" / "prop" |
默认返回 {data, prop};value 只要 data;prop 只要 prop |
contentType |
"" / "image" |
image 时后端把 base64 dataURL 解出,直接返回 PNG 二进制(不是 JSON) |
批量:key 路径段用 ;; 拼接多 key,SDK 传字符串列表即可。
响应
- 存在:
{ "status": "ok", "data": <value>, "prop": <prop> } - 不存在:
{ "status": "keyNoExist" }(不是 404) - 批量:
{ "values": [...], "props": [...], "status": [...] }
实采
// get_custom_data.json
{
"data": "hello-vjmap",
"prop": "text/plain",
"status": "ok"
}
// get_missing.json —— 一定要判断 status
{ "status": "keyNoExist" }
Python
d = svc.get_custom_data("myproj:theme")
if d["status"] == "keyNoExist":
...
else:
theme = d["data"]
Node.js
const d = await svc.getCustomData('myproj:theme');
if (d.status === 'keyNoExist') { /* ... */ }
批量读
svc.get_custom_data(["a", "b"])
# { "values": ["va","vb"], "props":["pa","pb"], "status":["ok","ok"] }
54 · 保存值 data/{key} POST
查询参数
| 字段 | 说明 |
|---|---|
ttl |
秒;到点清除。省略或 0 = 长期 |
Body
application/json:{"value": "...", "prop": "..."};prop 可存元数据(MIME、扩展字段)。
⚠️ 不要用 raw body:后端先
ShouldBindJSON,失败才GetRawData()——但 body 已被 JSON 解码器读走,取到的只是剩余部分:小 body 存成"",大 body 被截掉开头(20000 字节只剩 19488),响应却照样{"status":"ok"}。同理value/prop必须是 string(GoValue string绑定):传对象/数字会绑定失败 → 走 raw 分支 → 静默存成""。对象请先JSON.stringify。二进制请 base64 后放进
value:SDKsaveCustomData(key, {binary, mime})/save_custom_data(key, binary=..., mime=...)自动 base64,并把prop记为base64;<mime>(缺省application/octet-stream);读回后Buffer.from(d.data, 'base64')/base64.b64decode(d["data"])。
响应(实采)
{
"data": "nb_kv_a_1776832185",
"status": "ok"
}
Python
svc.save_custom_data("myproj:theme",
value='{"mode":"dark"}',
prop="application/json")
# 带 TTL
svc.save_custom_data("tmp:token",
value="eyJ...", ttl=3600)
# 二进制(SDK 转 base64 存进 value,prop="base64;image/png")
svc.save_custom_data("user42:avatar",
binary=open("avatar.png", "rb").read(), mime="image/png")
png = base64.b64decode(svc.get_custom_data("user42:avatar")["data"])
Node.js
await svc.saveCustomData('myproj:theme', {
value: JSON.stringify({ mode: 'dark' }),
prop: 'application/json',
});
await svc.saveCustomData('tmp:token', { value: tokenStr, ttl: 3600 });
await svc.saveCustomData('user42:avatar', { binary: fs.readFileSync('avatar.png'), mime: 'image/png' });
const png = Buffer.from((await svc.getCustomData('user42:avatar')).data, 'base64');
55 · 删除值 data/{key} DELETE
查询参数
| 字段 | 说明 |
|---|---|
isPrefix |
"true" 时把 key 当前缀,批量清除;否则只删单条 |
响应
- 单条(实采):
{"data":"nb_kv_a_...","status":"ok"} - 前缀(实采):
{"data":"nb_prefix_...","isPrefix":true,"status":"ok"}
Python
svc.delete_custom_data("myproj:theme")
# 前缀批量
svc.delete_custom_data("myproj:user42:", is_prefix=True)
# 列表批量(走 ;; 分隔)
svc.delete_custom_data(["a", "b", "c"])
56 · 批量存值 datas POST
URL:POST /api/v1/service/datas
Body
[{"key":..,"value":..,"prop":..,"ttl":"60"}],注意 ttl 是字符串秒数。
响应(实采)
{ "status": ["ok"] }
Python
svc.save_multiple_custom_data([
{"key": "cfg:a", "value": "1"},
{"key": "cfg:b", "value": "2", "ttl": "3600"},
])
Node.js
await svc.saveMultipleCustomData([
{ key: 'cfg:a', value: '1' },
{ key: 'cfg:b', value: '2', ttl: '3600' },
]);
⚠️ 403 多半是 body 绑定失败,不是权限:后端用
ShouldBindJSON绑[{key,value,prop,ttl}],四个字段都是 Gostring——ttl: 3600(数字)或非字符串 value 会绑定失败,走失败响应 →code:7+ HTTP 403(网关的失败响应一律映射 403),msg是json: cannot unmarshal number into Go struct field DataPostReqType.ttl of type string这类。同 token 下全字符串 body 正常返回{"status":["ok"]}。真权限问题的msg是权限不足。
57 · 按前缀列键 datakeys GET
查询参数
| 字段 | 说明 |
|---|---|
prefix |
前缀(自动加 data_;SQL LIKE 匹配,注意 _/% 通配与大小写不敏感,见文首) |
retDataType |
value / prop / valueprop —— 同时取出值,省一次往返 |
isSys |
true 时 prefix 不补 data_,且返回的是整行对象数组 {"keys":[{"Key","Value","Prop","Ttl"}]}(Key 为库内原始键如 data_xxx,Ttl 为到期 Unix 秒、0=永久;不做过期过滤;retDataType 不生效) |
响应
- 仅 keys(实采):
{ "keys": ["nb_kv_a_1776832185", "nb_kv_b_1776832185"] }
- 带值(
retDataType=valueprop,实采):
{
"keys": ["nb_kv_a_1776832185", "nb_kv_b_1776832185"],
"props": ["text/plain", "bp-1"],
"values": ["hello-vjmap", "batch-1"],
"status": ["ok", "ok"]
}
Python
keys = svc.get_custom_data_keys(prefix="myproj:")["keys"]
# 同时取值
bundle = svc.get_custom_data_keys(prefix="myproj:", ret_data_type="valueprop")
for k, v, p in zip(bundle["keys"], bundle["values"], bundle["props"]):
print(k, v, p)
最佳实践
- 前缀分层:用
:建立命名空间(不能用/,路由不吃斜杠;_/%在前缀匹配里是 LIKE 通配符,别当分隔符),如myproj:user42:tool-state,前缀统一以:结尾再批量删除、列出。 - 大文件不要存 KV:value 单条一般 < 1 MB;大图/DWG 用
/map/uploadmap或独立对象存储。 - TTL 清理:用
ttl让后端帮忙过期;别自己写 crontab。注意到期读取是 HTTP 403key ttl expire(抛异常),不是keyNoExist。 keyNoExist≠ 错误:200 正常响应里status == "keyNoExist"表示没值,而不是失败。