mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-19 12:34:22 +08:00
feat(kv-storage): 实现 KV 存储服务及管理界面
- 重构 KV 存储服务,遵循 SECTL-One-Stop SDK 规范 - 新增 KV 存储管理组件,支持增删改查操作 - 优化 setKV 接口,移除冗余参数 - 添加类型定义和列表查询选项
This commit is contained in:
+1
-1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,436 @@
|
|||||||
|
/**
|
||||||
|
* SECTL KV 存储管理组件
|
||||||
|
* 基于 SECTL-One-Stop SDK 规范实现
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from "react"
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
Table,
|
||||||
|
Button,
|
||||||
|
Space,
|
||||||
|
Modal,
|
||||||
|
Input,
|
||||||
|
message,
|
||||||
|
Popconfirm,
|
||||||
|
Typography,
|
||||||
|
Tag,
|
||||||
|
Form,
|
||||||
|
Switch,
|
||||||
|
InputNumber,
|
||||||
|
Tooltip,
|
||||||
|
} from "antd"
|
||||||
|
import {
|
||||||
|
DatabaseOutlined,
|
||||||
|
PlusOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
EditOutlined,
|
||||||
|
ReloadOutlined,
|
||||||
|
CodeOutlined,
|
||||||
|
FieldStringOutlined,
|
||||||
|
} from "@ant-design/icons"
|
||||||
|
import { sectlKVStorage, KVItem, ListKVOptions } from "../services/sectlKVStorage"
|
||||||
|
import { useSectl } from "../contexts/SectlContext"
|
||||||
|
|
||||||
|
const { Title, Text } = Typography
|
||||||
|
const { TextArea } = Input
|
||||||
|
|
||||||
|
export const SectlKVStorageManager: React.FC = () => {
|
||||||
|
const { isAuthenticated } = useSectl()
|
||||||
|
const [kvList, setKvList] = useState<KVItem[]>([])
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [total, setTotal] = useState(0)
|
||||||
|
const [hasMore, setHasMore] = useState(false)
|
||||||
|
const [offset, setOffset] = useState(0)
|
||||||
|
const [prefix, setPrefix] = useState("")
|
||||||
|
|
||||||
|
// 编辑/创建对话框状态
|
||||||
|
const [editModal, setEditModal] = useState<{
|
||||||
|
visible: boolean
|
||||||
|
isEdit: boolean
|
||||||
|
key?: string
|
||||||
|
}>({ visible: false, isEdit: false })
|
||||||
|
|
||||||
|
// 查看详情对话框状态
|
||||||
|
const [detailModal, setDetailModal] = useState<{
|
||||||
|
visible: boolean
|
||||||
|
item?: KVItem
|
||||||
|
}>({ visible: false })
|
||||||
|
|
||||||
|
// 表单
|
||||||
|
const [form] = Form.useForm()
|
||||||
|
|
||||||
|
// 加载 KV 列表
|
||||||
|
const loadKVList = async (resetOffset = false) => {
|
||||||
|
if (!isAuthenticated) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true)
|
||||||
|
const currentOffset = resetOffset ? 0 : offset
|
||||||
|
const options: ListKVOptions = {
|
||||||
|
limit: 20,
|
||||||
|
offset: currentOffset,
|
||||||
|
}
|
||||||
|
if (prefix) {
|
||||||
|
options.prefix = prefix
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await sectlKVStorage.listKV(options)
|
||||||
|
setKvList(resetOffset ? result.kv_list : [...kvList, ...result.kv_list])
|
||||||
|
setTotal(result.total)
|
||||||
|
setHasMore(result.has_more)
|
||||||
|
if (resetOffset) {
|
||||||
|
setOffset(20)
|
||||||
|
} else {
|
||||||
|
setOffset(currentOffset + 20)
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(`加载键值对列表失败:${error.message}`)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始加载
|
||||||
|
useEffect(() => {
|
||||||
|
if (isAuthenticated) {
|
||||||
|
loadKVList(true)
|
||||||
|
}
|
||||||
|
}, [isAuthenticated, prefix])
|
||||||
|
|
||||||
|
// 处理创建/更新
|
||||||
|
const handleSave = async (values: {
|
||||||
|
key: string
|
||||||
|
value: string
|
||||||
|
isJson: boolean
|
||||||
|
ttl?: number
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
let parsedValue: unknown
|
||||||
|
|
||||||
|
if (values.isJson) {
|
||||||
|
try {
|
||||||
|
parsedValue = JSON.parse(values.value)
|
||||||
|
} catch {
|
||||||
|
message.error("JSON 格式无效")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
parsedValue = values.value
|
||||||
|
}
|
||||||
|
|
||||||
|
await sectlKVStorage.setKV(values.key, parsedValue, values.ttl)
|
||||||
|
message.success(editModal.isEdit ? "键值对已更新" : "键值对已创建")
|
||||||
|
setEditModal({ visible: false, isEdit: false })
|
||||||
|
form.resetFields()
|
||||||
|
loadKVList(true)
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(`保存失败:${error.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理删除
|
||||||
|
const handleDelete = async (key: string) => {
|
||||||
|
try {
|
||||||
|
await sectlKVStorage.deleteKV(key)
|
||||||
|
message.success("键值对已删除")
|
||||||
|
loadKVList(true)
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(`删除失败:${error.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开编辑对话框
|
||||||
|
const openEditModal = (item: KVItem) => {
|
||||||
|
form.setFieldsValue({
|
||||||
|
key: item.key,
|
||||||
|
value: item.is_json ? JSON.stringify(item.value, null, 2) : String(item.value),
|
||||||
|
isJson: item.is_json,
|
||||||
|
})
|
||||||
|
setEditModal({ visible: true, isEdit: true, key: item.key })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开创建对话框
|
||||||
|
const openCreateModal = () => {
|
||||||
|
form.resetFields()
|
||||||
|
form.setFieldsValue({ isJson: true })
|
||||||
|
setEditModal({ visible: true, isEdit: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开详情对话框
|
||||||
|
const openDetailModal = (item: KVItem) => {
|
||||||
|
setDetailModal({ visible: true, item })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化值显示
|
||||||
|
const formatValue = (value: unknown, isJson: boolean): string => {
|
||||||
|
if (isJson) {
|
||||||
|
return JSON.stringify(value).slice(0, 100) + (JSON.stringify(value).length > 100 ? "..." : "")
|
||||||
|
}
|
||||||
|
return String(value).slice(0, 100) + (String(value).length > 100 ? "..." : "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表格列定义
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: "键名",
|
||||||
|
dataIndex: "key",
|
||||||
|
key: "key",
|
||||||
|
render: (text: string, record: KVItem) => (
|
||||||
|
<Space>
|
||||||
|
<DatabaseOutlined />
|
||||||
|
<a onClick={() => openDetailModal(record)}>{text}</a>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "类型",
|
||||||
|
dataIndex: "is_json",
|
||||||
|
key: "is_json",
|
||||||
|
width: 100,
|
||||||
|
render: (isJson: boolean) =>
|
||||||
|
isJson ? (
|
||||||
|
<Tag color="blue" icon={<CodeOutlined />}>
|
||||||
|
JSON
|
||||||
|
</Tag>
|
||||||
|
) : (
|
||||||
|
<Tag icon={<FieldStringOutlined />}>文本</Tag>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "大小",
|
||||||
|
dataIndex: "size",
|
||||||
|
key: "size",
|
||||||
|
width: 100,
|
||||||
|
render: (size: number) => {
|
||||||
|
if (size < 1024) return `${size} B`
|
||||||
|
if (size < 1024 * 1024) return `${(size / 1024).toFixed(2)} KB`
|
||||||
|
return `${(size / (1024 * 1024)).toFixed(2)} MB`
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "值预览",
|
||||||
|
dataIndex: "value",
|
||||||
|
key: "value",
|
||||||
|
ellipsis: true,
|
||||||
|
render: (_: unknown, record: KVItem) => (
|
||||||
|
<Text type="secondary" style={{ fontSize: "12px" }}>
|
||||||
|
{formatValue(record.value, record.is_json)}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "更新时间",
|
||||||
|
dataIndex: "updated_at",
|
||||||
|
key: "updated_at",
|
||||||
|
width: 180,
|
||||||
|
render: (text: string) => new Date(text).toLocaleString("zh-CN"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "操作",
|
||||||
|
key: "action",
|
||||||
|
width: 150,
|
||||||
|
render: (_: unknown, record: KVItem) => (
|
||||||
|
<Space>
|
||||||
|
<Tooltip title="编辑">
|
||||||
|
<Button type="link" icon={<EditOutlined />} onClick={() => openEditModal(record)} />
|
||||||
|
</Tooltip>
|
||||||
|
<Popconfirm
|
||||||
|
title="确定要删除这个键值对吗?"
|
||||||
|
onConfirm={() => handleDelete(record.key)}
|
||||||
|
okText="确定"
|
||||||
|
cancelText="取消"
|
||||||
|
>
|
||||||
|
<Button type="link" danger icon={<DeleteOutlined />} />
|
||||||
|
</Popconfirm>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<div style={{ textAlign: "center", padding: "40px" }}>
|
||||||
|
<DatabaseOutlined style={{ fontSize: 48, color: "#999" }} />
|
||||||
|
<Title level={4} style={{ marginTop: 16 }}>
|
||||||
|
请先登录 SECTL 以使用 KV 存储服务
|
||||||
|
</Title>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Card
|
||||||
|
title={
|
||||||
|
<Space>
|
||||||
|
<DatabaseOutlined />
|
||||||
|
<span>KV 存储管理</span>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
extra={
|
||||||
|
<Space>
|
||||||
|
<Input
|
||||||
|
placeholder="键名前缀过滤"
|
||||||
|
value={prefix}
|
||||||
|
onChange={(e) => setPrefix(e.target.value)}
|
||||||
|
style={{ width: 200 }}
|
||||||
|
allowClear
|
||||||
|
/>
|
||||||
|
<Button icon={<ReloadOutlined />} onClick={() => loadKVList(true)} loading={loading}>
|
||||||
|
刷新
|
||||||
|
</Button>
|
||||||
|
<Button type="primary" icon={<PlusOutlined />} onClick={openCreateModal}>
|
||||||
|
新建键值对
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{/* 统计信息 */}
|
||||||
|
<div style={{ marginBottom: 16 }}>
|
||||||
|
<Text type="secondary">
|
||||||
|
共 {total} 个键值对
|
||||||
|
{hasMore && `(已加载 ${kvList.length} 个)`}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* KV 列表 */}
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
dataSource={kvList}
|
||||||
|
rowKey="key"
|
||||||
|
loading={loading}
|
||||||
|
pagination={false}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 加载更多 */}
|
||||||
|
{hasMore && (
|
||||||
|
<div style={{ textAlign: "center", marginTop: 16 }}>
|
||||||
|
<Button onClick={() => loadKVList(false)} loading={loading}>
|
||||||
|
加载更多
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 创建/编辑对话框 */}
|
||||||
|
<Modal
|
||||||
|
title={editModal.isEdit ? "编辑键值对" : "新建键值对"}
|
||||||
|
open={editModal.visible}
|
||||||
|
onOk={() => form.submit()}
|
||||||
|
onCancel={() => {
|
||||||
|
setEditModal({ visible: false, isEdit: false })
|
||||||
|
form.resetFields()
|
||||||
|
}}
|
||||||
|
okText="保存"
|
||||||
|
cancelText="取消"
|
||||||
|
width={600}
|
||||||
|
>
|
||||||
|
<Form form={form} layout="vertical" onFinish={handleSave}>
|
||||||
|
<Form.Item
|
||||||
|
name="key"
|
||||||
|
label="键名"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: "请输入键名" },
|
||||||
|
{ max: 255, message: "键名最大 255 字符" },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input placeholder="请输入键名" disabled={editModal.isEdit} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item name="isJson" label="数据类型" valuePropName="checked">
|
||||||
|
<Switch checkedChildren="JSON" unCheckedChildren="文本" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item name="value" label="值" rules={[{ required: true, message: "请输入值" }]}>
|
||||||
|
<TextArea
|
||||||
|
rows={8}
|
||||||
|
placeholder={
|
||||||
|
form.getFieldValue("isJson")
|
||||||
|
? '请输入 JSON 格式数据,例如:\n{\n "theme": "dark",\n "language": "zh-CN"\n}'
|
||||||
|
: "请输入文本值"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item name="ttl" label="过期时间(秒)">
|
||||||
|
<InputNumber style={{ width: "100%" }} placeholder="留空表示永不过期" min={1} />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* 详情对话框 */}
|
||||||
|
<Modal
|
||||||
|
title="键值对详情"
|
||||||
|
open={detailModal.visible}
|
||||||
|
onCancel={() => setDetailModal({ visible: false })}
|
||||||
|
footer={[
|
||||||
|
<Button key="close" onClick={() => setDetailModal({ visible: false })}>
|
||||||
|
关闭
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
|
width={700}
|
||||||
|
>
|
||||||
|
{detailModal.item && (
|
||||||
|
<Space direction="vertical" style={{ width: "100%" }} size="large">
|
||||||
|
<div>
|
||||||
|
<Text strong>键名:</Text>
|
||||||
|
<Text copyable>{detailModal.item.key}</Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Text strong>类型:</Text>
|
||||||
|
{detailModal.item.is_json ? (
|
||||||
|
<Tag color="blue" icon={<CodeOutlined />}>
|
||||||
|
JSON
|
||||||
|
</Tag>
|
||||||
|
) : (
|
||||||
|
<Tag icon={<FieldStringOutlined />}>文本</Tag>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Text strong>大小:</Text>
|
||||||
|
<Text>
|
||||||
|
{detailModal.item.size < 1024
|
||||||
|
? `${detailModal.item.size} B`
|
||||||
|
: detailModal.item.size < 1024 * 1024
|
||||||
|
? `${(detailModal.item.size / 1024).toFixed(2)} KB`
|
||||||
|
: `${(detailModal.item.size / (1024 * 1024)).toFixed(2)} MB`}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Text strong>创建时间:</Text>
|
||||||
|
<Text>{new Date(detailModal.item.created_at).toLocaleString("zh-CN")}</Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Text strong>更新时间:</Text>
|
||||||
|
<Text>{new Date(detailModal.item.updated_at).toLocaleString("zh-CN")}</Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Text strong>值:</Text>
|
||||||
|
<pre
|
||||||
|
style={{
|
||||||
|
background: "#f5f5f5",
|
||||||
|
padding: 16,
|
||||||
|
borderRadius: 8,
|
||||||
|
overflow: "auto",
|
||||||
|
maxHeight: 400,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{detailModal.item.is_json
|
||||||
|
? JSON.stringify(detailModal.item.value, null, 2)
|
||||||
|
: String(detailModal.item.value)}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -13,6 +13,8 @@ import {
|
|||||||
import { useSectl } from "../contexts/SectlContext"
|
import { useSectl } from "../contexts/SectlContext"
|
||||||
import { SectlLoginButton } from "./SectlLoginButton"
|
import { SectlLoginButton } from "./SectlLoginButton"
|
||||||
import { ScoreSyncPanel } from "./ScoreSyncPanel"
|
import { ScoreSyncPanel } from "./ScoreSyncPanel"
|
||||||
|
import { SectlCloudStorageManager } from "./SectlCloudStorageManager"
|
||||||
|
import { SectlKVStorageManager } from "./SectlKVStorageManager"
|
||||||
|
|
||||||
export const SectlSettingsPanel: React.FC = () => {
|
export const SectlSettingsPanel: React.FC = () => {
|
||||||
const { isAuthenticated, isLoading, userInfo, platformId, setPlatformId, refreshUserInfo } =
|
const { isAuthenticated, isLoading, userInfo, platformId, setPlatformId, refreshUserInfo } =
|
||||||
@@ -152,6 +154,12 @@ export const SectlSettingsPanel: React.FC = () => {
|
|||||||
|
|
||||||
{/* 数据同步面板 */}
|
{/* 数据同步面板 */}
|
||||||
<ScoreSyncPanel />
|
<ScoreSyncPanel />
|
||||||
|
|
||||||
|
{/* 云存储管理 */}
|
||||||
|
<SectlCloudStorageManager />
|
||||||
|
|
||||||
|
{/* KV 存储管理 */}
|
||||||
|
<SectlKVStorageManager />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -836,7 +836,7 @@ const api = {
|
|||||||
}> => invoke("plugin_get_runtime_modules"),
|
}> => invoke("plugin_get_runtime_modules"),
|
||||||
|
|
||||||
// Generic invoke wrapper for backward compatibility with callers using `api.invoke`
|
// Generic invoke wrapper for backward compatibility with callers using `api.invoke`
|
||||||
invoke: async (channel: string, ..._args: any[]): Promise<any> => {
|
invoke: async (channel: string): Promise<any> => {
|
||||||
switch (channel) {
|
switch (channel) {
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unsupported legacy invoke channel: ${channel}`)
|
throw new Error(`Unsupported legacy invoke channel: ${channel}`)
|
||||||
|
|||||||
@@ -60,9 +60,7 @@ class ScoreSyncService {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
// 保存到云端
|
// 保存到云端
|
||||||
await sectlKVStorage.setKV(DATA_KEYS.STUDENTS, scoreData, {
|
await sectlKVStorage.setKV(DATA_KEYS.STUDENTS, scoreData)
|
||||||
is_json: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log("学生数据同步成功")
|
console.log("学生数据同步成功")
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -107,9 +105,7 @@ class ScoreSyncService {
|
|||||||
createdAt: event.created_at,
|
createdAt: event.created_at,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
await sectlKVStorage.setKV(DATA_KEYS.EVENTS, scoreEvents, {
|
await sectlKVStorage.setKV(DATA_KEYS.EVENTS, scoreEvents)
|
||||||
is_json: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log("积分事件同步成功")
|
console.log("积分事件同步成功")
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -146,9 +142,7 @@ class ScoreSyncService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await sectlKVStorage.setKV(DATA_KEYS.SETTINGS, settings, {
|
await sectlKVStorage.setKV(DATA_KEYS.SETTINGS, settings)
|
||||||
is_json: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log("设置数据同步成功")
|
console.log("设置数据同步成功")
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -186,9 +180,7 @@ class ScoreSyncService {
|
|||||||
platform: "secscore",
|
platform: "secscore",
|
||||||
}
|
}
|
||||||
|
|
||||||
await sectlKVStorage.setKV(DATA_KEYS.SYNC_META, metadata, {
|
await sectlKVStorage.setKV(DATA_KEYS.SYNC_META, metadata)
|
||||||
is_json: true,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,30 +1,47 @@
|
|||||||
/**
|
/**
|
||||||
* SECTL KV 键值对存储服务
|
* SECTL KV 键值对存储服务
|
||||||
|
* 基于 SECTL-One-Stop SDK 规范实现
|
||||||
* 提供键值对存储功能,支持 JSON 格式和字段级操作
|
* 提供键值对存储功能,支持 JSON 格式和字段级操作
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { SECTL_CONFIG, sectlAuth } from "./sectlAuth"
|
import { SECTL_CONFIG, sectlAuth } from "./sectlAuth"
|
||||||
import { KVData } from "./sectlCloudStorage"
|
|
||||||
|
// KV 数据类型
|
||||||
|
export interface KVItem {
|
||||||
|
key: string
|
||||||
|
value: any
|
||||||
|
size: number
|
||||||
|
is_json: boolean
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// KV 列表选项
|
||||||
|
export interface ListKVOptions {
|
||||||
|
prefix?: string
|
||||||
|
limit?: number
|
||||||
|
offset?: number
|
||||||
|
}
|
||||||
|
|
||||||
class SectlKVStorageService {
|
class SectlKVStorageService {
|
||||||
/**
|
/**
|
||||||
* 创建或更新键值对
|
* 设置 KV 键值对
|
||||||
|
* @param key 键名(最大 255 字符)
|
||||||
|
* @param value 值(任意 JSON 可序列化数据,最大 64KB)
|
||||||
|
* @param ttl 过期时间(秒),可选
|
||||||
*/
|
*/
|
||||||
async setKV(
|
async setKV(
|
||||||
key: string,
|
key: string,
|
||||||
value: any,
|
value: unknown,
|
||||||
options?: {
|
ttl?: number
|
||||||
is_json?: boolean
|
|
||||||
}
|
|
||||||
): Promise<{
|
): Promise<{
|
||||||
success: boolean
|
success: boolean
|
||||||
kv_id: string
|
|
||||||
key: string
|
key: string
|
||||||
value: any
|
|
||||||
is_json: boolean
|
|
||||||
size: number
|
size: number
|
||||||
created_at: string
|
is_json: boolean
|
||||||
updated_at: string
|
created_at?: string
|
||||||
|
updated_at?: string
|
||||||
|
message: string
|
||||||
}> {
|
}> {
|
||||||
const accessToken = sectlAuth.getAccessToken()
|
const accessToken = sectlAuth.getAccessToken()
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
@@ -33,26 +50,15 @@ class SectlKVStorageService {
|
|||||||
|
|
||||||
const url = `${SECTL_CONFIG.baseUrl}/api/cloud/kv`
|
const url = `${SECTL_CONFIG.baseUrl}/api/cloud/kv`
|
||||||
|
|
||||||
// 自动检测是否为 JSON 格式
|
const body: Record<string, unknown> = {
|
||||||
let valueStr: string
|
client_id: SECTL_CONFIG.platformId,
|
||||||
let isJson = options?.is_json
|
user_id: sectlAuth.getToken()?.user_id || "",
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof value === "object") {
|
if (ttl !== undefined) {
|
||||||
valueStr = JSON.stringify(value)
|
body.ttl = ttl
|
||||||
if (isJson === undefined) {
|
|
||||||
isJson = true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
valueStr = String(value)
|
|
||||||
if (isJson === undefined) {
|
|
||||||
// 尝试检测是否为 JSON 字符串
|
|
||||||
try {
|
|
||||||
JSON.parse(valueStr)
|
|
||||||
isJson = true
|
|
||||||
} catch {
|
|
||||||
isJson = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -62,13 +68,7 @@ class SectlKVStorageService {
|
|||||||
Authorization: `Bearer ${accessToken}`,
|
Authorization: `Bearer ${accessToken}`,
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify(body),
|
||||||
client_id: SECTL_CONFIG.platformId,
|
|
||||||
user_id: sectlAuth.getToken()?.user_id || "",
|
|
||||||
key,
|
|
||||||
value: valueStr,
|
|
||||||
is_json: isJson,
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -84,14 +84,17 @@ class SectlKVStorageService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取键值对
|
* 获取 KV 键值对
|
||||||
|
* @param key 键名
|
||||||
|
* @param field JSON 字段路径,如 "theme" 或 "user.name",可选
|
||||||
*/
|
*/
|
||||||
async getKV<T = any>(
|
async getKV<T = unknown>(
|
||||||
key: string,
|
key: string,
|
||||||
options?: {
|
field?: string
|
||||||
field?: string // 获取 JSON 中的特定字段
|
): Promise<
|
||||||
}
|
| (KVItem & { key: string; value: T })
|
||||||
): Promise<KVData | { key: string; field: string; value: T }> {
|
| { key: string; field: string; value: T; is_json: boolean }
|
||||||
|
> {
|
||||||
const accessToken = sectlAuth.getAccessToken()
|
const accessToken = sectlAuth.getAccessToken()
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
throw new Error("未授权,请先登录")
|
throw new Error("未授权,请先登录")
|
||||||
@@ -102,8 +105,8 @@ class SectlKVStorageService {
|
|||||||
user_id: sectlAuth.getToken()?.user_id || "",
|
user_id: sectlAuth.getToken()?.user_id || "",
|
||||||
})
|
})
|
||||||
|
|
||||||
if (options?.field) {
|
if (field) {
|
||||||
params.append("field", options.field)
|
params.append("field", field)
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = `${SECTL_CONFIG.baseUrl}/api/cloud/kv/${encodeURIComponent(key)}?${params.toString()}`
|
const url = `${SECTL_CONFIG.baseUrl}/api/cloud/kv/${encodeURIComponent(key)}?${params.toString()}`
|
||||||
@@ -128,12 +131,14 @@ class SectlKVStorageService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取键值对列表
|
* 获取 KV 键值对列表
|
||||||
|
* @param options 列表选项
|
||||||
*/
|
*/
|
||||||
async listKV(options?: {
|
async listKV(options: ListKVOptions = {}): Promise<{
|
||||||
limit?: number
|
kv_list: KVItem[]
|
||||||
offset?: number
|
total: number
|
||||||
}): Promise<{ kvs: KVData[]; total: number; has_more: boolean }> {
|
has_more: boolean
|
||||||
|
}> {
|
||||||
const accessToken = sectlAuth.getAccessToken()
|
const accessToken = sectlAuth.getAccessToken()
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
throw new Error("未授权,请先登录")
|
throw new Error("未授权,请先登录")
|
||||||
@@ -142,10 +147,14 @@ class SectlKVStorageService {
|
|||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
client_id: SECTL_CONFIG.platformId,
|
client_id: SECTL_CONFIG.platformId,
|
||||||
user_id: sectlAuth.getToken()?.user_id || "",
|
user_id: sectlAuth.getToken()?.user_id || "",
|
||||||
limit: String(options?.limit || 100),
|
limit: String(options.limit || 100),
|
||||||
offset: String(options?.offset || 0),
|
offset: String(options.offset || 0),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (options.prefix) {
|
||||||
|
params.append("prefix", options.prefix)
|
||||||
|
}
|
||||||
|
|
||||||
const url = `${SECTL_CONFIG.baseUrl}/api/cloud/kv?${params.toString()}`
|
const url = `${SECTL_CONFIG.baseUrl}/api/cloud/kv?${params.toString()}`
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -168,18 +177,22 @@ class SectlKVStorageService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新 JSON 字段
|
* 更新 KV JSON 字段
|
||||||
|
* @param key 键名
|
||||||
|
* @param field JSON 字段路径,支持嵌套如 "user.name" 或数组索引 "items.0.id"
|
||||||
|
* @param value 要设置的值
|
||||||
*/
|
*/
|
||||||
async updateKVField(
|
async updateKVField(
|
||||||
key: string,
|
key: string,
|
||||||
field: string,
|
field: string,
|
||||||
value: any
|
value: unknown
|
||||||
): Promise<{
|
): Promise<{
|
||||||
success: boolean
|
success: boolean
|
||||||
key: string
|
key: string
|
||||||
field: string
|
field: string
|
||||||
value: any
|
size: number
|
||||||
updated_at: string
|
updated_at: string
|
||||||
|
message: string
|
||||||
}> {
|
}> {
|
||||||
const accessToken = sectlAuth.getAccessToken()
|
const accessToken = sectlAuth.getAccessToken()
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
@@ -216,7 +229,8 @@ class SectlKVStorageService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除键值对
|
* 删除 KV 键值对
|
||||||
|
* @param key 键名
|
||||||
*/
|
*/
|
||||||
async deleteKV(key: string): Promise<{ success: boolean; message: string }> {
|
async deleteKV(key: string): Promise<{ success: boolean; message: string }> {
|
||||||
const accessToken = sectlAuth.getAccessToken()
|
const accessToken = sectlAuth.getAccessToken()
|
||||||
|
|||||||
Reference in New Issue
Block a user