唯杰地图 logo唯杰地图
VJMAP
EN

键值数据存储 (KV)

vjmap-service/references/api/08-data-storage.md 9.3 KB268 行
原文

分类代号: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(Go Value string 绑定):传对象/数字会绑定失败 → 走 raw 分支 → 静默存成 ""。对象请先 JSON.stringify。

二进制请 base64 后放进 value:SDK saveCustomData(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}],四个字段都是 Go string——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)

最佳实践

  1. 前缀分层:用 : 建立命名空间(不能用 /,路由不吃斜杠;_/% 在前缀匹配里是 LIKE 通配符,别当分隔符),如 myproj:user42:tool-state,前缀统一以 : 结尾再批量删除、列出。
  2. 大文件不要存 KV:value 单条一般 < 1 MB;大图/DWG 用 /map/uploadmap 或独立对象存储。
  3. TTL 清理:用 ttl 让后端帮忙过期;别自己写 crontab。注意到期读取是 HTTP 403 key ttl expire(抛异常),不是 keyNoExist。
  4. keyNoExist ≠ 错误:200 正常响应里 status == "keyNoExist" 表示没值,而不是失败。