Initial Dever 0.0.1

This commit is contained in:
Codex committed 2026-11-03 03:32:07 +00:00
commit 81233d098c
1479 files changed
+163782

No files matched your search

+58
View File
@@ -0,0 +1,58 @@
{
"runtime": {
"mode": "all",
"shutdown_ms": 30000
},
"job": {
"workers": 1,
"poll_ms": 1000,
"lease_ms": 60000,
"retry_base_ms": 1000,
"retry_max_ms": 300000
},
"http": {
"host": "127.0.0.1",
"port": 8080
},
"database": {
"default": {
"type": "sqlite",
"path": "data/db/platform.db",
"tenant_directory": "data/tenants",
"max_connections": 4,
"max_page_size": 100
}
},
"tenant": {
"database": "default",
"max_pools": 32,
"idle_timeout_ms": 300000
},
"auth": {
"providers": {
"session": {
"verify": "user.account.verify",
"jwtSecret": "replace-this-example-secret-before-deployment-2026",
"ttlSeconds": 3600,
"cookie": "cms_session"
}
}
},
"sites": {
"admin": {
"path": "admin",
"hosts": ["cms.example.com"],
"auth": "session",
"origin": "https://cms.example.com"
},
"front": {
"path": "front",
"hosts": ["cms.example.com"],
"auth": "session",
"origin": "https://cms.example.com"
}
},
"log": {
"level": "info"
}
}
+1
View File
@@ -0,0 +1 @@
@@ -0,0 +1,7 @@
# 操作审计
保存登录、退出、发布、上传和授权等操作记录。
- [操作审计应用](doc/app.md)
- [操作审计模型](doc/model.md)
- [调用流程](link.md)
@@ -0,0 +1,10 @@
record(kind: model.OperationKind, resource: Text, resource_id: Text) () {
stored = model.create(
{
kind = kind
resource = resource
resource_id = resource_id
}
)
discarded = stored
}
@@ -0,0 +1,5 @@
# 操作审计应用
## audit.operation.record
保存一次操作审计,记录操作类型、资源名称和资源标识。
@@ -0,0 +1,9 @@
# 操作审计模型
## audit.operation.model.OperationKind
审计操作类型:登录、退出、发布、上传和授权。
## audit.operation.model.Operation
租户操作审计记录,包含操作者、操作类型和目标资源。
@@ -0,0 +1,60 @@
<!-- Generated by dever inspect --links. Do not edit. -->
<!-- source-sha256: 5cc5a514d99bdf266632507f65376a89a8ba16e0a87baa6f0f2be4435fa35943 -->
# audit.operation
按求值顺序展示调用、分支和任务调度,包含 API 与自动 REST 入口。点击节点打开源码;双框为调用,虚线为调度或错误路径。外部库和驱动不展开。
图不是某次运行的日志,分句和 Adapter 按输入或配置选择。修改源码后运行 `dever inspect . --links`。
## 入口
没有已收录的入口。
## 外部调用方
| 能力 | 调用方 | 方式 | 调用位置 |
| --- | --- | --- | --- |
| [audit.operation.record](app.dever#L1) | [media.asset.record](../../media/asset/app.dever#L17) | 调用 | [../../media/asset/app.dever:26:3](../../media/asset/app.dever#L26,3) |
| [audit.operation.record](app.dever#L1) | [news.article.publishDraft](../../news/article/app/publication.dever#L48) | 调用 | [../../news/article/app/publication.dever:65:3](../../news/article/app/publication.dever#L65,3) |
## 能力与内部调用
### audit.operation.model.Operation
定义:[model.dever:9:1](model.dever#L9,1)
没有已收录的下游调用。
### audit.operation.model.OperationKind
定义:[model.dever:1:1](model.dever#L1,1)
没有已收录的下游调用。
### audit.operation.record
定义:[app.dever:1:1](app.dever#L1,1)
<a id="flow-c1586d946bef8bfb-1"></a>
```mermaid
flowchart TD
n0(["record"])
click n0 href "app.dever#L1,1" "打开源码"
n1(["返回"])
n3["audit.operation.model.create"]
click n3 href "app.dever#L2,12" "打开源码"
n4["赋值 stored"]
click n4 href "app.dever#L2,12" "打开源码"
n5["赋值 discarded"]
click n5 href "app.dever#L9,15" "打开源码"
n0 --> n3
n3 --> n4
n4 --> n5
n5 --> n1
```
| 目标 | 归属 | 方式 | 调用位置 |
| --- | --- | --- | --- |
| [audit.operation.model.create](model.dever#L9) | 领域内 | 内建操作 | [app.dever:2:12](app.dever#L2,12) |
@@ -0,0 +1,14 @@
type OperationKind {
Login = "登录"
Logout = "登出"
Publish = "发布"
Upload = "上传"
Access = "授权"
}
type Operation {
actor_id: Int?
kind: OperationKind index
resource: Text(1, 120)
resource_id: Text(1, 120)
}
@@ -0,0 +1,8 @@
# 内容分类
创建分类并管理启用状态,供文章校验所选分类。
- [分类接口](doc/api-admin-manage.md)
- [内容分类应用](doc/app.md)
- [内容分类模型](doc/model.md)
- [调用流程](link.md)
@@ -0,0 +1,3 @@
post create = app.create
delete disable = app.disable
@@ -0,0 +1,39 @@
type CategoryView {
id: model.id
name: Text
slug: Text
status: Text
}
create(name: Text) (category: CategoryView) {
stored = model.create(
{
name = name
slug = name
}
)
category = CategoryView {
id = stored.id
name = stored.name
slug = stored.slug
status = "active"
}
}
disable(id: model.id) (ok: Bool) {
changed = model.update(
id,
{
status = model.CategoryStatus.Disabled
}
)
discarded = changed
ok = true
}
requireActive(id: model.id) () {
stored = model.get(id)
dever.api.require(stored.status == model.CategoryStatus.Active, dever.api.Error.Conflict)
}
requireActive(id: null) () {}
@@ -0,0 +1,9 @@
# 分类接口
## content.category.api.admin.manage.create
创建分类。
## content.category.api.admin.manage.disable
禁用分类。
@@ -0,0 +1,17 @@
# 内容分类应用
## content.category.CategoryView
分类接口返回的编号、名称、slug 和状态。
## content.category.create
创建分类。名称去除首尾空白,slug 同时转为小写并保持唯一。
## content.category.disable
把已有分类标为禁用,记录仍保留。
## content.category.requireActive
分类编号为空时跳过;否则要求分类已启用,禁用时返回冲突。
@@ -0,0 +1,9 @@
# 内容分类模型
## content.category.model.CategoryStatus
分类的启用或禁用状态。
## content.category.model.Category
分类名称、唯一 slug 和状态。
@@ -0,0 +1,402 @@
<!-- Generated by dever inspect --links. Do not edit. -->
<!-- source-sha256: 22ff06f2e26c68d4bf521c52d2ecba992a5af0f615c4f380876f2fe34a8b4aa0 -->
# content.category
按求值顺序展示调用、分支和任务调度,包含 API 与自动 REST 入口。点击节点打开源码;双框为调用,虚线为调度或错误路径。外部库和驱动不展开。
图不是某次运行的日志,分句和 Adapter 按输入或配置选择。修改源码后运行 `dever inspect . --links`。
## 入口
- API [content.category.api.admin.manage.create](api/admin/manage.dever#L1) → [content.category.create](app.dever#L8)
- API [content.category.api.admin.manage.disable](api/admin/manage.dever#L3) → [content.category.disable](app.dever#L23)
## 外部调用方
| 能力 | 调用方 | 方式 | 调用位置 |
| --- | --- | --- | --- |
| [content.category.requireActive](app.dever#L34) | [news.article.create](../../news/article/app/editing.dever#L16) | 调用 | [../../news/article/app/editing.dever:24:3](../../news/article/app/editing.dever#L24,3) |
| [content.category.requireActive](app.dever#L34) | [news.article.edit](../../news/article/app/editing.dever#L77) | 调用 | [../../news/article/app/editing.dever:89:3](../../news/article/app/editing.dever#L89,3) |
| [content.category.requireActive](app.dever#L34) | [news.article.publishDraft](../../news/article/app/publication.dever#L48) | 调用 | [../../news/article/app/publication.dever:51:3](../../news/article/app/publication.dever#L51,3) |
## 能力与内部调用
### content.category.CategoryView
定义:[app.dever:1:1](app.dever#L1,1)
没有已收录的下游调用。
### content.category.api.admin.manage.create
定义:[api/admin/manage.dever:1:1](api/admin/manage.dever#L1,1)
<a id="flow-d5f276e1d0182184-1"></a>
```mermaid
flowchart TD
n0(["content.category.api.admin.manage.create"])
click n0 href "api/admin/manage.dever#L1,15" "打开源码"
n1{"路由 POST /content/category/admin/manage/create,topic #34;admin/manage#34; 接受请求?"}
n2(["返回 HTTP 404"])
n3["进入 API"]
n4["取得认证时间"]
n5(["返回 HTTP 500"])
n6["按 topic #34;admin/manage#34; 选择站点与 Provider"]
n7(["返回 HTTP 403"])
n8{"读取 Bearer 或站点 Cookie;两者互斥"}
n9(["返回 HTTP 401"])
n10["认证上下文就绪"]
n11(["返回 HTTP 401"])
n12["校验 Token 的签名、有效期及站点"]
n13{"Cookie 且非 GET / HEAD / OPTIONS?"}
n14["检查站点 Origin"]
n15(["返回 HTTP 403"])
n16(["返回 HTTP 401"])
n17{"按 Provider.verify 选择验证函数"}
n18[["user.account.verify"]]
click n18 href "api/admin/manage.dever#L1,15" "打开源码"
n19(["返回 HTTP 业务错误映射状态;其他故障 500"])
n20["用验证结果建立身份"]
n21(["返回 HTTP 认证错误状态"])
n22(["返回 HTTP 500"])
n23["建立请求数据库上下文"]
n24{"有已验证租户?"}
n25["数据库上下文就绪"]
n26{"部署启用了租户存储?"}
n27(["返回 HTTP 403"])
n28["按已验证租户选择数据库"]
n29(["返回 HTTP 503"])
n30{"检查租户组件启用状态:content"}
n31(["返回 HTTP 403"])
n32(["返回 HTTP 503"])
n33["进入写入口事务"]
n34["失败:保留 HTTP 错误状态"]
n35["回滚入口拥有的事务"]
n36(["返回 HTTP 500"])
n37(["返回错误响应"])
n38{"权限检查:content.category.{site}.create"}
n39["权限通过"]
n40["读取 API 输入"]
n41["解码具体参数并检查字段合同"]
n42[["content.category.create"]]
click n42 href "api/admin/manage.dever#L1,15" "打开源码"
n43["按输出类型检查可序列化值并编码响应"]
n44["提交入口拥有的事务"]
n45(["返回 HTTP 500"])
n46["发布暂存的认证 Header / Cookie"]
n47(["返回成功响应"])
n0 --> n1
n1 -->|"未匹配此入口:继续路由;均未匹配"| n2
n1 --> n3
n3 --> n4
n4 -->|"失败"| n5
n4 --> n6
n6 -->|"无站点绑定"| n7
n6 --> n8
n8 -->|"凭据格式错误或冲突"| n9
n8 -->|"无凭据"| n11
n8 -->|"有凭据"| n13
n13 -->|"否"| n12
n13 -->|"是"| n14
n14 -->|"不符合"| n15
n14 -->|"通过"| n12
n12 -->|"无 Provider 或 Token 无效"| n16
n12 --> n17
n17 -->|"user.account.verify"| n18
n18 -->|"验证函数失败"| n19
n18 --> n20
n20 -->|"身份不合法"| n21
n20 -->|"通过"| n10
n17 -->|"无匹配验证函数"| n22
n10 --> n23
n23 --> n24
n24 -->|"否"| n26
n26 -->|"是:缺少租户身份"| n27
n26 -->|"否"| n25
n24 -->|"是"| n28
n28 -->|"选择失败"| n29
n28 --> n30
n30 -->|"未启用"| n31
n30 -->|"查询失败"| n32
n30 -->|"允许"| n25
n25 --> n33
n34 --> n35
n35 -->|"事务结束失败"| n36
n35 --> n37
n33 --> n38
n38 -->|"拒绝:Access 状态;查询故障:500"| n34
n38 -->|"允许"| n39
n39 --> n40
n40 -->|"失败:输入错误状态"| n34
n40 --> n41
n41 -->|"失败:400"| n34
n41 --> n42
n42 -->|"业务错误映射状态;其他故障:500"| n34
n42 --> n43
n43 -->|"编码失败:500"| n34
n43 --> n44
n44 -->|"提交失败"| n45
n44 --> n46
n46 --> n47
```
<details>
<summary>源码与子流程</summary>
| 节点 | 源码 | 子流程 |
| --- | --- | --- |
| user.account.verify | [api/admin/manage.dever:1:15](api/admin/manage.dever#L1,15) | [user.account.verify](../../user/account/link.md#flow-aaaacb87d61f819c-1) |
| content.category.create | [api/admin/manage.dever:1:15](api/admin/manage.dever#L1,15) | [content.category.create](link.md#flow-dffb2069d02e287c-1) |
</details>
| 目标 | 归属 | 方式 | 调用位置 |
| --- | --- | --- | --- |
| [content.category.create](app.dever#L8) | 领域内 | API 绑定 | [api/admin/manage.dever:1:15](api/admin/manage.dever#L1,15) |
| [user.account.verify](../../user/account/app/account.dever#L133) | 跨领域 [user.account](../../user/account/link.md) | 身份验证 | [api/admin/manage.dever:1:15](api/admin/manage.dever#L1,15) |
### content.category.api.admin.manage.disable
定义:[api/admin/manage.dever:3:1](api/admin/manage.dever#L3,1)
<a id="flow-5e559918fcf97f3a-1"></a>
```mermaid
flowchart TD
n0(["content.category.api.admin.manage.disable"])
click n0 href "api/admin/manage.dever#L3,18" "打开源码"
n1{"路由 DELETE /content/category/admin/manage/disable,topic #34;admin/manage#34; 接受请求?"}
n2(["返回 HTTP 404"])
n3["进入 API"]
n4["取得认证时间"]
n5(["返回 HTTP 500"])
n6["按 topic #34;admin/manage#34; 选择站点与 Provider"]
n7(["返回 HTTP 403"])
n8{"读取 Bearer 或站点 Cookie;两者互斥"}
n9(["返回 HTTP 401"])
n10["认证上下文就绪"]
n11(["返回 HTTP 401"])
n12["校验 Token 的签名、有效期及站点"]
n13{"Cookie 且非 GET / HEAD / OPTIONS?"}
n14["检查站点 Origin"]
n15(["返回 HTTP 403"])
n16(["返回 HTTP 401"])
n17{"按 Provider.verify 选择验证函数"}
n18[["user.account.verify"]]
click n18 href "api/admin/manage.dever#L3,18" "打开源码"
n19(["返回 HTTP 业务错误映射状态;其他故障 500"])
n20["用验证结果建立身份"]
n21(["返回 HTTP 认证错误状态"])
n22(["返回 HTTP 500"])
n23["建立请求数据库上下文"]
n24{"有已验证租户?"}
n25["数据库上下文就绪"]
n26{"部署启用了租户存储?"}
n27(["返回 HTTP 403"])
n28["按已验证租户选择数据库"]
n29(["返回 HTTP 503"])
n30{"检查租户组件启用状态:content"}
n31(["返回 HTTP 403"])
n32(["返回 HTTP 503"])
n33["进入写入口事务"]
n34["失败:保留 HTTP 错误状态"]
n35["回滚入口拥有的事务"]
n36(["返回 HTTP 500"])
n37(["返回错误响应"])
n38{"权限检查:content.category.{site}.disable"}
n39["权限通过"]
n40["读取 API 输入"]
n41["解码具体参数并检查字段合同"]
n42[["content.category.disable"]]
click n42 href "api/admin/manage.dever#L3,18" "打开源码"
n43["按输出类型检查可序列化值并编码响应"]
n44["提交入口拥有的事务"]
n45(["返回 HTTP 500"])
n46["发布暂存的认证 Header / Cookie"]
n47(["返回成功响应"])
n0 --> n1
n1 -->|"未匹配此入口:继续路由;均未匹配"| n2
n1 --> n3
n3 --> n4
n4 -->|"失败"| n5
n4 --> n6
n6 -->|"无站点绑定"| n7
n6 --> n8
n8 -->|"凭据格式错误或冲突"| n9
n8 -->|"无凭据"| n11
n8 -->|"有凭据"| n13
n13 -->|"否"| n12
n13 -->|"是"| n14
n14 -->|"不符合"| n15
n14 -->|"通过"| n12
n12 -->|"无 Provider 或 Token 无效"| n16
n12 --> n17
n17 -->|"user.account.verify"| n18
n18 -->|"验证函数失败"| n19
n18 --> n20
n20 -->|"身份不合法"| n21
n20 -->|"通过"| n10
n17 -->|"无匹配验证函数"| n22
n10 --> n23
n23 --> n24
n24 -->|"否"| n26
n26 -->|"是:缺少租户身份"| n27
n26 -->|"否"| n25
n24 -->|"是"| n28
n28 -->|"选择失败"| n29
n28 --> n30
n30 -->|"未启用"| n31
n30 -->|"查询失败"| n32
n30 -->|"允许"| n25
n25 --> n33
n34 --> n35
n35 -->|"事务结束失败"| n36
n35 --> n37
n33 --> n38
n38 -->|"拒绝:Access 状态;查询故障:500"| n34
n38 -->|"允许"| n39
n39 --> n40
n40 -->|"失败:输入错误状态"| n34
n40 --> n41
n41 -->|"失败:400"| n34
n41 --> n42
n42 -->|"业务错误映射状态;其他故障:500"| n34
n42 --> n43
n43 -->|"编码失败:500"| n34
n43 --> n44
n44 -->|"提交失败"| n45
n44 --> n46
n46 --> n47
```
<details>
<summary>源码与子流程</summary>
| 节点 | 源码 | 子流程 |
| --- | --- | --- |
| user.account.verify | [api/admin/manage.dever:3:18](api/admin/manage.dever#L3,18) | [user.account.verify](../../user/account/link.md#flow-aaaacb87d61f819c-1) |
| content.category.disable | [api/admin/manage.dever:3:18](api/admin/manage.dever#L3,18) | [content.category.disable](link.md#flow-85101fdf961a71c3-1) |
</details>
| 目标 | 归属 | 方式 | 调用位置 |
| --- | --- | --- | --- |
| [content.category.disable](app.dever#L23) | 领域内 | API 绑定 | [api/admin/manage.dever:3:18](api/admin/manage.dever#L3,18) |
| [user.account.verify](../../user/account/app/account.dever#L133) | 跨领域 [user.account](../../user/account/link.md) | 身份验证 | [api/admin/manage.dever:3:18](api/admin/manage.dever#L3,18) |
### content.category.create
定义:[app.dever:8:1](app.dever#L8,1)
<a id="flow-dffb2069d02e287c-1"></a>
```mermaid
flowchart TD
n0(["create"])
click n0 href "app.dever#L8,1" "打开源码"
n1(["返回"])
n3["content.category.model.create"]
click n3 href "app.dever#L9,12" "打开源码"
n4["赋值 stored"]
click n4 href "app.dever#L9,12" "打开源码"
n5["赋值 category"]
click n5 href "app.dever#L15,14" "打开源码"
n0 --> n3
n3 --> n4
n4 --> n5
n5 --> n1
```
| 目标 | 归属 | 方式 | 调用位置 |
| --- | --- | --- | --- |
| [content.category.model.create](model.dever#L6) | 领域内 | 内建操作 | [app.dever:9:12](app.dever#L9,12) |
### content.category.disable
定义:[app.dever:23:1](app.dever#L23,1)
<a id="flow-85101fdf961a71c3-1"></a>
```mermaid
flowchart TD
n0(["disable"])
click n0 href "app.dever#L23,1" "打开源码"
n1(["返回"])
n3["content.category.model.update"]
click n3 href "app.dever#L24,13" "打开源码"
n4["赋值 changed"]
click n4 href "app.dever#L24,13" "打开源码"
n5["赋值 discarded"]
click n5 href "app.dever#L30,15" "打开源码"
n6["赋值 ok"]
click n6 href "app.dever#L31,8" "打开源码"
n0 --> n3
n3 --> n4
n4 --> n5
n5 --> n6
n6 --> n1
```
| 目标 | 归属 | 方式 | 调用位置 |
| --- | --- | --- | --- |
| [content.category.model.update](model.dever#L6) | 领域内 | 内建操作 | [app.dever:24:13](app.dever#L24,13) |
### content.category.model.Category
定义:[model.dever:6:1](model.dever#L6,1)
没有已收录的下游调用。
### content.category.model.CategoryStatus
定义:[model.dever:1:1](model.dever#L1,1)
没有已收录的下游调用。
### content.category.requireActive
定义:[app.dever:34:1](app.dever#L34,1)、[app.dever:39:1](app.dever#L39,1)
<a id="flow-eee0931d93e09e79-1"></a>
```mermaid
flowchart TD
n0(["requireActive"])
click n0 href "app.dever#L34,1" "打开源码"
n1(["返回"])
n3{"按实参匹配分句(互斥)"}
n4["id: model.id"]
click n4 href "app.dever#L34,1" "打开源码"
n5["content.category.model.get"]
click n5 href "app.dever#L35,12" "打开源码"
n6["赋值 stored"]
click n6 href "app.dever#L35,12" "打开源码"
n7[["dever.api.require"]]
click n7 href "app.dever#L36,3" "打开源码"
n8["id: null"]
click n8 href "app.dever#L39,1" "打开源码"
n0 --> n3
n3 -->|"id: model.id"| n4
n4 --> n5
n5 --> n6
n6 --> n7
n7 --> n1
n3 -->|"id: null"| n8
n8 --> n1
```
<details>
<summary>源码与子流程</summary>
| 节点 | 源码 | 子流程 |
| --- | --- | --- |
| dever.api.require | [app.dever:36:3](app.dever#L36,3) | 依赖或生成的分派(本页不展开) |
</details>
| 目标 | 归属 | 方式 | 调用位置 |
| --- | --- | --- | --- |
| [content.category.model.get](model.dever#L6) | 领域内 | 内建操作 | [app.dever:35:12](app.dever#L35,12) |
| `dever.api.require` | 依赖 | 调用 | [app.dever:36:3](app.dever#L36,3) |
@@ -0,0 +1,10 @@
type CategoryStatus {
Active = "启用"
Disabled = "禁用"
}
type Category {
name: Text(1, 80)
slug: Text(1, 100) lower unique
status: CategoryStatus default CategoryStatus.Active index
}
@@ -0,0 +1,8 @@
# 内容修订
在编辑文章的同一事务中保存版本快照。
- [修订接口](doc/api-admin-manage.md)
- [内容修订应用](doc/app.md)
- [内容修订模型](doc/model.md)
- [调用流程](link.md)
@@ -0,0 +1 @@
post create = app.create
@@ -0,0 +1,45 @@
type RevisionView {
id: model.id
article_id: news.article.model.id
version: Int
title: Text
body: Text
status: Text
}
transaction create(
selected_article_id: news.article.model.id,
expected_version: Int,
title: Text,
body: Text,
category_id: content.category.model.id?,
asset_id: media.asset.model.id?
) (revision: RevisionView) {
edited = news.article.edit(
selected_article_id,
expected_version,
title,
body,
category_id,
asset_id
)
stored = model.create(
{
article_id = selected_article_id
version = edited.version
title = edited.title
body = edited.body
category_id = edited.category_id
asset_id = edited.asset_id
status = model.RevisionStatus.Draft
}
)
revision = RevisionView {
id = stored.id
article_id = stored.article_id
version = stored.version
title = stored.title
body = stored.body
status = "draft"
}
}
@@ -0,0 +1,5 @@
# 修订接口
## content.revision.api.admin.manage.create
按版本编辑文章,并保存修订快照。
@@ -0,0 +1,9 @@
# 内容修订应用
## content.revision.RevisionView
编辑后返回的修订编号、文章编号、版本及内容。
## content.revision.create
按 expected_version 编辑文章并保存修订快照,两步在同一事务中完成。版本冲突、分类或媒体不可用时整次操作失败。
@@ -0,0 +1,9 @@
# 内容修订模型
## content.revision.model.RevisionStatus
修订的草稿或已发布状态。
## content.revision.model.Revision
文章某一版本的内容快照;同一文章和版本只能保存一条。
@@ -0,0 +1,212 @@
<!-- Generated by dever inspect --links. Do not edit. -->
<!-- source-sha256: 3a206461ddeadfa4087f3a0db7ee697474811b4cc2f983c8d57592e0bf48eeed -->
# content.revision
按求值顺序展示调用、分支和任务调度,包含 API 与自动 REST 入口。点击节点打开源码;双框为调用,虚线为调度或错误路径。外部库和驱动不展开。
图不是某次运行的日志,分句和 Adapter 按输入或配置选择。修改源码后运行 `dever inspect . --links`。
## 入口
- API [content.revision.api.admin.manage.create](api/admin/manage.dever#L1) → [content.revision.create](app.dever#L10)
## 外部调用方
当前加载的源码中没有已收录的外部调用。
## 能力与内部调用
### content.revision.RevisionView
定义:[app.dever:1:1](app.dever#L1,1)
没有已收录的下游调用。
### content.revision.api.admin.manage.create
定义:[api/admin/manage.dever:1:1](api/admin/manage.dever#L1,1)
<a id="flow-61096d20b01e1b77-1"></a>
```mermaid
flowchart TD
n0(["content.revision.api.admin.manage.create"])
click n0 href "api/admin/manage.dever#L1,15" "打开源码"
n1{"路由 POST /content/revision/admin/manage/create,topic #34;admin/manage#34; 接受请求?"}
n2(["返回 HTTP 404"])
n3["进入 API"]
n4["取得认证时间"]
n5(["返回 HTTP 500"])
n6["按 topic #34;admin/manage#34; 选择站点与 Provider"]
n7(["返回 HTTP 403"])
n8{"读取 Bearer 或站点 Cookie;两者互斥"}
n9(["返回 HTTP 401"])
n10["认证上下文就绪"]
n11(["返回 HTTP 401"])
n12["校验 Token 的签名、有效期及站点"]
n13{"Cookie 且非 GET / HEAD / OPTIONS?"}
n14["检查站点 Origin"]
n15(["返回 HTTP 403"])
n16(["返回 HTTP 401"])
n17{"按 Provider.verify 选择验证函数"}
n18[["user.account.verify"]]
click n18 href "api/admin/manage.dever#L1,15" "打开源码"
n19(["返回 HTTP 业务错误映射状态;其他故障 500"])
n20["用验证结果建立身份"]
n21(["返回 HTTP 认证错误状态"])
n22(["返回 HTTP 500"])
n23["建立请求数据库上下文"]
n24{"有已验证租户?"}
n25["数据库上下文就绪"]
n26{"部署启用了租户存储?"}
n27(["返回 HTTP 403"])
n28["按已验证租户选择数据库"]
n29(["返回 HTTP 503"])
n30{"检查租户组件启用状态:content, media, news"}
n31(["返回 HTTP 403"])
n32(["返回 HTTP 503"])
n33["进入写入口事务"]
n34["失败:保留 HTTP 错误状态"]
n35["回滚入口拥有的事务"]
n36(["返回 HTTP 500"])
n37(["返回错误响应"])
n38{"权限检查:content.revision.{site}.create"}
n39["权限通过"]
n40["读取 API 输入"]
n41["解码具体参数并检查字段合同"]
n42[["content.revision.create"]]
click n42 href "api/admin/manage.dever#L1,15" "打开源码"
n43["按输出类型检查可序列化值并编码响应"]
n44["提交入口拥有的事务"]
n45(["返回 HTTP 500"])
n46["发布暂存的认证 Header / Cookie"]
n47(["返回成功响应"])
n0 --> n1
n1 -->|"未匹配此入口:继续路由;均未匹配"| n2
n1 --> n3
n3 --> n4
n4 -->|"失败"| n5
n4 --> n6
n6 -->|"无站点绑定"| n7
n6 --> n8
n8 -->|"凭据格式错误或冲突"| n9
n8 -->|"无凭据"| n11
n8 -->|"有凭据"| n13
n13 -->|"否"| n12
n13 -->|"是"| n14
n14 -->|"不符合"| n15
n14 -->|"通过"| n12
n12 -->|"无 Provider 或 Token 无效"| n16
n12 --> n17
n17 -->|"user.account.verify"| n18
n18 -->|"验证函数失败"| n19
n18 --> n20
n20 -->|"身份不合法"| n21
n20 -->|"通过"| n10
n17 -->|"无匹配验证函数"| n22
n10 --> n23
n23 --> n24
n24 -->|"否"| n26
n26 -->|"是:缺少租户身份"| n27
n26 -->|"否"| n25
n24 -->|"是"| n28
n28 -->|"选择失败"| n29
n28 --> n30
n30 -->|"未启用"| n31
n30 -->|"查询失败"| n32
n30 -->|"允许"| n25
n25 --> n33
n34 --> n35
n35 -->|"事务结束失败"| n36
n35 --> n37
n33 --> n38
n38 -->|"拒绝:Access 状态;查询故障:500"| n34
n38 -->|"允许"| n39
n39 --> n40
n40 -->|"失败:输入错误状态"| n34
n40 --> n41
n41 -->|"失败:400"| n34
n41 --> n42
n42 -->|"业务错误映射状态;其他故障:500"| n34
n42 --> n43
n43 -->|"编码失败:500"| n34
n43 --> n44
n44 -->|"提交失败"| n45
n44 --> n46
n46 --> n47
```
<details>
<summary>源码与子流程</summary>
| 节点 | 源码 | 子流程 |
| --- | --- | --- |
| user.account.verify | [api/admin/manage.dever:1:15](api/admin/manage.dever#L1,15) | [user.account.verify](../../user/account/link.md#flow-aaaacb87d61f819c-1) |
| content.revision.create | [api/admin/manage.dever:1:15](api/admin/manage.dever#L1,15) | [content.revision.create](link.md#flow-8c2beaa3a2e4fcf0-1) |
</details>
| 目标 | 归属 | 方式 | 调用位置 |
| --- | --- | --- | --- |
| [content.revision.create](app.dever#L10) | 领域内 | API 绑定 | [api/admin/manage.dever:1:15](api/admin/manage.dever#L1,15) |
| [user.account.verify](../../user/account/app/account.dever#L133) | 跨领域 [user.account](../../user/account/link.md) | 身份验证 | [api/admin/manage.dever:1:15](api/admin/manage.dever#L1,15) |
### content.revision.create
定义:[app.dever:10:1](app.dever#L10,1)
<a id="flow-8c2beaa3a2e4fcf0-1"></a>
```mermaid
flowchart TD
n0(["create"])
click n0 href "app.dever#L10,1" "打开源码"
n1["开始事务"]
n2(["提交事务,返回"])
n3(["回滚事务,传播错误"])
n4[["news.article.edit"]]
click n4 href "app.dever#L18,12" "打开源码"
n5["赋值 edited"]
click n5 href "app.dever#L18,12" "打开源码"
n6["content.revision.model.create"]
click n6 href "app.dever#L26,12" "打开源码"
n7["赋值 stored"]
click n7 href "app.dever#L26,12" "打开源码"
n8["赋值 revision"]
click n8 href "app.dever#L37,14" "打开源码"
n0 --> n1
n1 -.->|"事务体任一未捕获错误"| n3
n1 --> n4
n4 --> n5
n5 --> n6
n6 --> n7
n7 --> n8
n8 --> n2
```
<details>
<summary>源码与子流程</summary>
| 节点 | 源码 | 子流程 |
| --- | --- | --- |
| news.article.edit | [app.dever:18:12](app.dever#L18,12) | [news.article.edit](../../news/article/link.md#flow-2c557350a87b85a1-1) |
</details>
| 目标 | 归属 | 方式 | 调用位置 |
| --- | --- | --- | --- |
| [news.article.edit](../../news/article/app/editing.dever#L77) | 跨领域 [news.article](../../news/article/link.md) | 调用 | [app.dever:18:12](app.dever#L18,12) |
| [content.revision.model.create](model.dever#L6) | 领域内 | 内建操作 | [app.dever:26:12](app.dever#L26,12) |
### content.revision.model.Revision
定义:[model.dever:6:1](model.dever#L6,1)
没有已收录的下游调用。
### content.revision.model.RevisionStatus
定义:[model.dever:1:1](model.dever#L1,1)
没有已收录的下游调用。
@@ -0,0 +1,16 @@
type RevisionStatus {
Draft = "草稿"
Published = "已发布"
}
type Revision {
article_id: news.article.model.id index
version: Int
title: Text(1, 160)
body: Text
category_id: content.category.model.id?
asset_id: media.asset.model.id?
status: RevisionStatus default RevisionStatus.Draft index
}
unique(article_id, version)
@@ -0,0 +1,10 @@
# 媒体资源
保存上传文件及元数据,供文章引用可用媒体。
- [媒体本地适配器](doc/adapter.md)
- [上传接口](doc/api-admin-manage.md)
- [媒体资源应用](doc/app.md)
- [媒体资源模型](doc/model.md)
- [媒体存储端口](doc/port.md)
- [调用流程](link.md)
@@ -0,0 +1,3 @@
port.store(file: Upload) (key: Uuid) {
key = dever.storage.put(file)
}
@@ -0,0 +1 @@
long post upload = app.store
@@ -0,0 +1,41 @@
type StoreResult {
id: model.id
storage_key: Uuid
filename: Text
content_type: Text
size: Int
}
store(file: Upload) (result: StoreResult) {
filename = dever.api.uploadFilename(file)
content_type = dever.api.uploadContentType(file)
size = dever.api.uploadSize(file)
key = port.store(file)
result = record(key, filename, content_type, size)
}
transaction record(key: Uuid, filename: Text, content_type: Text, size: Int) (result: StoreResult) {
asset = model.create(
{
storage_key = key
filename = filename
content_type = content_type
size = size
}
)
audit.operation.record(audit.operation.model.OperationKind.Upload, "asset", filename)
result = StoreResult {
id = asset.id
storage_key = asset.storage_key
filename = asset.filename
content_type = asset.content_type
size = asset.size
}
}
requireReady(id: model.id) () {
stored = model.get(id)
dever.api.require(stored.status == model.AssetStatus.Ready, dever.api.Error.Conflict)
}
requireReady(id: null) () {}
@@ -0,0 +1,5 @@
# 媒体本地适配器
## media.asset.adapter.port.store
使用本地 storage 保存上传内容,返回文件 Uuid。
@@ -0,0 +1,5 @@
# 上传接口
## media.asset.api.admin.manage.upload
接收 multipart 文件上传,保存文件及资源元数据。
@@ -0,0 +1,17 @@
# 媒体资源应用
## media.asset.StoreResult
上传完成后返回资源编号、存储键、文件名、内容类型和字节数。
## media.asset.store
读取上传元数据并保存文件,再写入资源记录。文件存储不占用数据库事务。
## media.asset.record
在同一事务中写入资源元数据和上传审计。
## media.asset.requireReady
媒体编号为空时跳过;否则要求资源处于 Ready 状态,不可用时返回冲突。
@@ -0,0 +1,9 @@
# 媒体资源模型
## media.asset.model.AssetStatus
媒体资源的就绪或已删除状态。
## media.asset.model.Asset
已存文件的唯一存储键、元数据及状态。
@@ -0,0 +1,5 @@
# 媒体存储端口
## media.asset.port.store
保存一次性上传内容,返回存储键;失败使用 dever.storage.PutResult。
@@ -0,0 +1,350 @@
<!-- Generated by dever inspect --links. Do not edit. -->
<!-- source-sha256: 11ad386ddcac8cef62a4a2d0330b0c2b4678196a227a7676c7e6a55aba63b5f1 -->
# media.asset
按求值顺序展示调用、分支和任务调度,包含 API 与自动 REST 入口。点击节点打开源码;双框为调用,虚线为调度或错误路径。外部库和驱动不展开。
图不是某次运行的日志,分句和 Adapter 按输入或配置选择。修改源码后运行 `dever inspect . --links`。
## 入口
- API [media.asset.api.admin.manage.upload](api/admin/manage.dever#L1) → [media.asset.store](app.dever#L9)
## 外部调用方
| 能力 | 调用方 | 方式 | 调用位置 |
| --- | --- | --- | --- |
| [media.asset.requireReady](app.dever#L36) | [news.article.create](../../news/article/app/editing.dever#L16) | 调用 | [../../news/article/app/editing.dever:25:3](../../news/article/app/editing.dever#L25,3) |
| [media.asset.requireReady](app.dever#L36) | [news.article.edit](../../news/article/app/editing.dever#L77) | 调用 | [../../news/article/app/editing.dever:90:3](../../news/article/app/editing.dever#L90,3) |
| [media.asset.requireReady](app.dever#L36) | [news.article.publishDraft](../../news/article/app/publication.dever#L48) | 调用 | [../../news/article/app/publication.dever:52:3](../../news/article/app/publication.dever#L52,3) |
## 能力与内部调用
### media.asset.StoreResult
定义:[app.dever:1:1](app.dever#L1,1)
没有已收录的下游调用。
### media.asset.adapter.port.store
定义:[adapter.dever:1:1](adapter.dever#L1,1)
<a id="flow-eb9faa6e4f022f76-1"></a>
```mermaid
flowchart TD
n0(["port.store"])
click n0 href "adapter.dever#L1,1" "打开源码"
n1(["返回"])
n3[["dever.storage.put"]]
click n3 href "adapter.dever#L2,9" "打开源码"
n4["赋值 key"]
click n4 href "adapter.dever#L2,9" "打开源码"
n0 --> n3
n3 --> n4
n4 --> n1
```
<details>
<summary>源码与子流程</summary>
| 节点 | 源码 | 子流程 |
| --- | --- | --- |
| dever.storage.put | [adapter.dever:2:9](adapter.dever#L2,9) | 依赖或生成的分派(本页不展开) |
</details>
| 目标 | 归属 | 方式 | 调用位置 |
| --- | --- | --- | --- |
| `dever.storage.put` | 依赖 | 调用 | [adapter.dever:2:9](adapter.dever#L2,9) |
### media.asset.api.admin.manage.upload
定义:[api/admin/manage.dever:1:1](api/admin/manage.dever#L1,1)
<a id="flow-7e3fa2eda06052de-1"></a>
```mermaid
flowchart TD
n0(["media.asset.api.admin.manage.upload"])
click n0 href "api/admin/manage.dever#L1,20" "打开源码"
n1{"路由 POST /media/asset/admin/manage/upload,topic #34;admin/manage#34; 接受请求?"}
n2(["返回 HTTP 404"])
n3["进入 API"]
n4["取得认证时间"]
n5(["返回 HTTP 500"])
n6["按 topic #34;admin/manage#34; 选择站点与 Provider"]
n7(["返回 HTTP 403"])
n8{"读取 Bearer 或站点 Cookie;两者互斥"}
n9(["返回 HTTP 401"])
n10["认证上下文就绪"]
n11(["返回 HTTP 401"])
n12["校验 Token 的签名、有效期及站点"]
n13{"Cookie 且非 GET / HEAD / OPTIONS?"}
n14["检查站点 Origin"]
n15(["返回 HTTP 403"])
n16(["返回 HTTP 401"])
n17{"按 Provider.verify 选择验证函数"}
n18[["user.account.verify"]]
click n18 href "api/admin/manage.dever#L1,20" "打开源码"
n19(["返回 HTTP 业务错误映射状态;其他故障 500"])
n20["用验证结果建立身份"]
n21(["返回 HTTP 认证错误状态"])
n22(["返回 HTTP 500"])
n23["建立请求数据库上下文"]
n24{"有已验证租户?"}
n25["数据库上下文就绪"]
n26{"部署启用了租户存储?"}
n27(["返回 HTTP 403"])
n28["按已验证租户选择数据库"]
n29(["返回 HTTP 503"])
n30{"检查租户组件启用状态:audit, media"}
n31(["返回 HTTP 403"])
n32(["返回 HTTP 503"])
n33["失败:保留 HTTP 错误状态"]
n34(["返回错误响应"])
n35{"权限检查:media.asset.{site}.upload"}
n36["权限通过"]
n37["读取 multipart、取得 Upload 和字段"]
n38["解码具体参数并检查字段合同"]
n39[["media.asset.store"]]
click n39 href "api/admin/manage.dever#L1,20" "打开源码"
n40["按输出类型检查可序列化值并编码响应"]
n41["发布暂存的认证 Header / Cookie"]
n42(["返回成功响应"])
n0 --> n1
n1 -->|"未匹配此入口:继续路由;均未匹配"| n2
n1 --> n3
n3 --> n4
n4 -->|"失败"| n5
n4 --> n6
n6 -->|"无站点绑定"| n7
n6 --> n8
n8 -->|"凭据格式错误或冲突"| n9
n8 -->|"无凭据"| n11
n8 -->|"有凭据"| n13
n13 -->|"否"| n12
n13 -->|"是"| n14
n14 -->|"不符合"| n15
n14 -->|"通过"| n12
n12 -->|"无 Provider 或 Token 无效"| n16
n12 --> n17
n17 -->|"user.account.verify"| n18
n18 -->|"验证函数失败"| n19
n18 --> n20
n20 -->|"身份不合法"| n21
n20 -->|"通过"| n10
n17 -->|"无匹配验证函数"| n22
n10 --> n23
n23 --> n24
n24 -->|"否"| n26
n26 -->|"是:缺少租户身份"| n27
n26 -->|"否"| n25
n24 -->|"是"| n28
n28 -->|"选择失败"| n29
n28 --> n30
n30 -->|"未启用"| n31
n30 -->|"查询失败"| n32
n30 -->|"允许"| n25
n33 --> n34
n25 --> n35
n35 -->|"拒绝:Access 状态;查询故障:500"| n33
n35 -->|"允许"| n36
n36 --> n37
n37 -->|"失败:上传错误状态"| n33
n37 --> n38
n38 -->|"失败:400"| n33
n38 --> n39
n39 -->|"业务错误映射状态;其他故障:500"| n33
n39 --> n40
n40 -->|"编码失败:500"| n33
n40 --> n41
n41 --> n42
```
<details>
<summary>源码与子流程</summary>
| 节点 | 源码 | 子流程 |
| --- | --- | --- |
| user.account.verify | [api/admin/manage.dever:1:20](api/admin/manage.dever#L1,20) | [user.account.verify](../../user/account/link.md#flow-aaaacb87d61f819c-1) |
| media.asset.store | [api/admin/manage.dever:1:20](api/admin/manage.dever#L1,20) | [media.asset.store](link.md#flow-9ce0fcefee26c58b-1) |
</details>
| 目标 | 归属 | 方式 | 调用位置 |
| --- | --- | --- | --- |
| [media.asset.store](app.dever#L9) | 领域内 | API 绑定 | [api/admin/manage.dever:1:20](api/admin/manage.dever#L1,20) |
| [user.account.verify](../../user/account/app/account.dever#L133) | 跨领域 [user.account](../../user/account/link.md) | 身份验证 | [api/admin/manage.dever:1:20](api/admin/manage.dever#L1,20) |
### media.asset.model.Asset
定义:[model.dever:6:1](model.dever#L6,1)
没有已收录的下游调用。
### media.asset.model.AssetStatus
定义:[model.dever:1:1](model.dever#L1,1)
没有已收录的下游调用。
### media.asset.port.store
定义:[port.dever:1:1](port.dever#L1,1)
没有已收录的下游调用。
### media.asset.record
定义:[app.dever:17:1](app.dever#L17,1)
<a id="flow-b00da9b3a217d427-1"></a>
```mermaid
flowchart TD
n0(["record"])
click n0 href "app.dever#L17,1" "打开源码"
n1["开始事务"]
n2(["提交事务,返回"])
n3(["回滚事务,传播错误"])
n4["media.asset.model.create"]
click n4 href "app.dever#L18,11" "打开源码"
n5["赋值 asset"]
click n5 href "app.dever#L18,11" "打开源码"
n6[["audit.operation.record"]]
click n6 href "app.dever#L26,3" "打开源码"
n7["赋值 result"]
click n7 href "app.dever#L27,12" "打开源码"
n0 --> n1
n1 -.->|"事务体任一未捕获错误"| n3
n1 --> n4
n4 --> n5
n5 --> n6
n6 --> n7
n7 --> n2
```
<details>
<summary>源码与子流程</summary>
| 节点 | 源码 | 子流程 |
| --- | --- | --- |
| audit.operation.record | [app.dever:26:3](app.dever#L26,3) | [audit.operation.record](../../audit/operation/link.md#flow-c1586d946bef8bfb-1) |
</details>
| 目标 | 归属 | 方式 | 调用位置 |
| --- | --- | --- | --- |
| [media.asset.model.create](model.dever#L6) | 领域内 | 内建操作 | [app.dever:18:11](app.dever#L18,11) |
| [audit.operation.record](../../audit/operation/app.dever#L1) | 跨领域 [audit.operation](../../audit/operation/link.md) | 调用 | [app.dever:26:3](app.dever#L26,3) |
### media.asset.requireReady
定义:[app.dever:36:1](app.dever#L36,1)、[app.dever:41:1](app.dever#L41,1)
<a id="flow-2c00b9e4b7136711-1"></a>
```mermaid
flowchart TD
n0(["requireReady"])
click n0 href "app.dever#L36,1" "打开源码"
n1(["返回"])
n3{"按实参匹配分句(互斥)"}
n4["id: model.id"]
click n4 href "app.dever#L36,1" "打开源码"
n5["media.asset.model.get"]
click n5 href "app.dever#L37,12" "打开源码"
n6["赋值 stored"]
click n6 href "app.dever#L37,12" "打开源码"
n7[["dever.api.require"]]
click n7 href "app.dever#L38,3" "打开源码"
n8["id: null"]
click n8 href "app.dever#L41,1" "打开源码"
n0 --> n3
n3 -->|"id: model.id"| n4
n4 --> n5
n5 --> n6
n6 --> n7
n7 --> n1
n3 -->|"id: null"| n8
n8 --> n1
```
<details>
<summary>源码与子流程</summary>
| 节点 | 源码 | 子流程 |
| --- | --- | --- |
| dever.api.require | [app.dever:38:3](app.dever#L38,3) | 依赖或生成的分派(本页不展开) |
</details>
| 目标 | 归属 | 方式 | 调用位置 |
| --- | --- | --- | --- |
| [media.asset.model.get](model.dever#L6) | 领域内 | 内建操作 | [app.dever:37:12](app.dever#L37,12) |
| `dever.api.require` | 依赖 | 调用 | [app.dever:38:3](app.dever#L38,3) |
### media.asset.store
定义:[app.dever:9:1](app.dever#L9,1)
<a id="flow-9ce0fcefee26c58b-1"></a>
```mermaid
flowchart TD
n0(["store"])
click n0 href "app.dever#L9,1" "打开源码"
n1(["返回"])
n3["dever.api.uploadFilename"]
click n3 href "app.dever#L10,14" "打开源码"
n4["赋值 filename"]
click n4 href "app.dever#L10,14" "打开源码"
n5["dever.api.uploadContentType"]
click n5 href "app.dever#L11,18" "打开源码"
n6["赋值 content_type"]
click n6 href "app.dever#L11,18" "打开源码"
n7["dever.api.uploadSize"]
click n7 href "app.dever#L12,10" "打开源码"
n8["赋值 size"]
click n8 href "app.dever#L12,10" "打开源码"
n9[["media.asset.adapter.port.store"]]
click n9 href "app.dever#L13,9" "打开源码"
n10["赋值 key"]
click n10 href "app.dever#L13,9" "打开源码"
n11[["media.asset.record"]]
click n11 href "app.dever#L14,12" "打开源码"
n12["赋值 result"]
click n12 href "app.dever#L14,12" "打开源码"
n0 --> n3
n3 --> n4
n4 --> n5
n5 --> n6
n6 --> n7
n7 --> n8
n8 --> n9
n9 --> n10
n10 --> n11
n11 --> n12
n12 --> n1
```
<details>
<summary>源码与子流程</summary>
| 节点 | 源码 | 子流程 |
| --- | --- | --- |
| media.asset.adapter.port.store | [app.dever:13:9](app.dever#L13,9) | [media.asset.adapter.port.store](link.md#flow-eb9faa6e4f022f76-1) |
| media.asset.record | [app.dever:14:12](app.dever#L14,12) | [media.asset.record](link.md#flow-b00da9b3a217d427-1) |
</details>
| 目标 | 归属 | 方式 | 调用位置 |
| --- | --- | --- | --- |
| `dever.api.uploadFilename` | 内建操作 | 内建操作 | [app.dever:10:14](app.dever#L10,14) |
| `dever.api.uploadContentType` | 内建操作 | 内建操作 | [app.dever:11:18](app.dever#L11,18) |
| `dever.api.uploadSize` | 内建操作 | 内建操作 | [app.dever:12:10](app.dever#L12,10) |
| [media.asset.adapter.port.store](adapter.dever#L1) | 领域内 | 调用 | [app.dever:13:9](app.dever#L13,9) |
| [media.asset.record](app.dever#L17) | 领域内 | 调用 | [app.dever:14:12](app.dever#L14,12) |
@@ -0,0 +1,12 @@
type AssetStatus {
Ready = "就绪"
Removed = "已删除"
}
type Asset {
storage_key: Uuid unique
filename: Text(1, 255)
content_type: Text(1, 160)
size: Int
status: AssetStatus default AssetStatus.Ready index
}
@@ -0,0 +1 @@
store(file: Upload) (key: Uuid) fails dever.storage.PutResult
@@ -0,0 +1,12 @@
# 文章
管理作者草稿和版本,手动或定时发布为前台快照。
- [文章管理接口](doc/api-admin-manage.md)
- [文章浏览接口](doc/api-front-browse.md)
- [文章草稿](doc/app-editing.md)
- [文章发布](doc/app-publication.md)
- [文章规则](doc/domain.md)
- [文章公开读模型](doc/model-publication.md)
- [文章模型](doc/model.md)
- [调用流程](link.md)
@@ -0,0 +1,9 @@
get list = app.list
get detail = app.detail
post create = app.create
delete remove = app.remove
post publish = app.publish
@@ -0,0 +1,3 @@
get list = app.published
get detail = app.publishedDetail
@@ -0,0 +1,111 @@
type ArticleView {
id: model.id
slug: Text
title: Text
body: Text
version: Int
category_id: content.category.model.id?
asset_id: media.asset.model.id?
}
type ArticlePage {
rows: List<ArticleView>
total: Int
}
create(
slug: Text,
title: Text,
body: Text,
category_id: content.category.model.id?,
asset_id: media.asset.model.id?
) (article: ArticleView) {
dever.api.require(text.trim(body) != "", dever.api.Error.Invalid)
content.category.requireActive(category_id)
media.asset.requireReady(asset_id)
stored = model.create(
{
author_id = user.account.currentUser()
slug = slug
title = title
body = body
category_id = category_id
asset_id = asset_id
}
)
article = detail(stored.id)
}
list(size: Int) (articles: ArticlePage) {
author = user.account.currentUser()
articles = ArticlePage {
rows = model.ownedArticles(author, domain.pageSize(size))
total = model.count(
{
where = author_id == author
}
)
}
}
detail(id: model.id) (article: ArticleView) {
stored = model.get(id)
dever.api.require(stored.author_id == user.account.currentUser(), dever.api.Error.Forbidden)
article = ArticleView {
id = stored.id
slug = stored.slug
title = stored.title
body = stored.body
version = stored.version
category_id = stored.category_id
asset_id = stored.asset_id
}
}
remove(id: model.id) (removed: Int) {
stored = model.get(id)
dever.api.require(stored.author_id == user.account.currentUser(), dever.api.Error.Forbidden)
dever.api.require(stored.status == model.ArticleStatus.Draft, dever.api.Error.Conflict)
removed = model.delete(
{
where = id == stored.id and version == stored.version
}
)
domain.requireUpdated(removed)
}
edit(
id: model.id,
expected_version: Int,
title: Text,
body: Text,
category_id: content.category.model.id?,
asset_id: media.asset.model.id?
) (article: ArticleView) {
stored = model.get(id)
dever.api.require(stored.author_id == user.account.currentUser(), dever.api.Error.Forbidden)
dever.api.require(stored.status == model.ArticleStatus.Draft, dever.api.Error.Conflict)
dever.api.require(text.trim(body) != "", dever.api.Error.Invalid)
content.category.requireActive(category_id)
media.asset.requireReady(asset_id)
changed = model.update(
{
where = id == stored.id and version == expected_version and status == model.ArticleStatus.Draft
},
{
title = title
body = body
category_id = category_id
asset_id = asset_id
version = expected_version + 1
}
)
domain.requireUpdated(changed)
article = detail(stored.id)
}
requireSchedulable(article_id: model.id) () {
stored = model.get(article_id)
dever.api.require(stored.author_id == user.account.currentUser(), dever.api.Error.Forbidden)
dever.api.require(stored.status == model.ArticleStatus.Draft, dever.api.Error.Conflict)
}
@@ -0,0 +1,95 @@
transaction publish(article_id: model.id) (publication: model.publication.Publication) {
stored = model.get(article_id)
dever.api.require(stored.author_id == user.account.currentUser(), dever.api.Error.Forbidden)
dever.api.require(stored.status == model.ArticleStatus.Draft, dever.api.Error.Conflict)
publication = publishDraft(article_id)
}
published(size: Int) (articles: model.publication.Page) {
articles = model.publication.list(
{
order = [published_at.desc, id.desc]
size = domain.pageSize(size)
}
)
}
publishedDetail(id: model.publication.id) (article: model.publication.Publication) {
article = model.publication.get(id)
dever.api.require(text.trim(article.body) != "", dever.api.Error.NotFound)
}
transaction publishScheduled(article_id: model.id) (
publication: model.publication.Publication
) {
stored = model.get(article_id)
publication = scheduledPublication(stored.status, article_id)
}
scheduledPublication(status: model.ArticleStatus.Published, article_id: model.id) (
publication: model.publication.Publication
) {
stored = model.get(article_id)
publication = existingPublication(
model.publication.first(
{
where = slug == stored.slug
}
)
)
}
scheduledPublication(status: model.ArticleStatus.Draft, article_id: model.id) (
publication: model.publication.Publication
) {
publication = publishDraft(article_id)
}
publishDraft(article_id: model.id) (publication: model.publication.Publication) {
stored = model.get(article_id)
dever.api.require(stored.status == model.ArticleStatus.Draft, dever.api.Error.Conflict)
content.category.requireActive(stored.category_id)
media.asset.requireReady(stored.asset_id)
published_at = dever.time.now()
changed = model.update(
{
where = id == stored.id and version == stored.version and status == model.ArticleStatus.Draft
},
{
status = model.ArticleStatus.Published
published_at = published_at
version = stored.version + 1
}
)
domain.requireUpdated(changed)
audit.operation.record(
audit.operation.model.OperationKind.Publish,
"article",
stored.slug
)
publication = model.publication.upsert(
{
slug = stored.slug
},
{
title = stored.title
body = stored.body
published_at = published_at
},
{
title = stored.title
body = stored.body
published_at = published_at
}
)
}
existingPublication(stored: model.publication.Publication) (
publication: model.publication.Publication
) {
publication = stored
}
existingPublication(stored: null) (publication: model.publication.Publication) {
fail(dever.api.Error.NotFound)
}
@@ -0,0 +1,21 @@
# 文章管理接口
## news.article.api.admin.manage.list
读取当前作者的文章列表与总数。
## news.article.api.admin.manage.detail
读取当前作者的文章详情。
## news.article.api.admin.manage.create
创建当前作者的草稿。
## news.article.api.admin.manage.remove
删除当前作者的草稿。
## news.article.api.admin.manage.publish
发布当前作者的草稿。
@@ -0,0 +1,9 @@
# 文章浏览接口
## news.article.api.front.browse.list
读取当前租户的已发布文章。
## news.article.api.front.browse.detail
读取已发布文章详情。
@@ -0,0 +1,33 @@
# 文章草稿
## news.article.ArticleView
编辑接口返回的文章内容、版本、分类和媒体编号。
## news.article.ArticlePage
当前作者的文章列表及总数。
## news.article.create
为当前作者创建草稿。正文不能为空白,所选分类和媒体须可用;正文保留原文。
## news.article.list
读取当前作者的最新文章和总数,size 必须为 1–100。
## news.article.detail
读取当前作者的一篇文章,访问其他作者的文章返回禁止访问。
## news.article.remove
删除当前作者的草稿,并检查版本未被并发修改。已发布文章不能删除。
## news.article.edit
按 expected_version 修改当前作者的草稿,成功后版本加一。正文为空白或分类、媒体不可用时失败。
## news.article.requireSchedulable
要求文章属于当前作者且仍为草稿,供创建发布计划使用。
@@ -0,0 +1,29 @@
# 文章发布
## news.article.publish
发布当前作者的草稿;状态更新、审计和公开快照在同一事务中完成。
## news.article.published
按发布时间和编号倒序读取发布快照,size 必须为 1–100。
## news.article.publishedDetail
读取一份发布快照;正文为空白时返回不存在。
## news.article.publishScheduled
执行定时发布。已发布文章复用现有快照,草稿进入发布流程。
## news.article.scheduledPublication
根据文章状态选择复用快照或发布草稿。
## news.article.publishDraft
检查草稿、分类和媒体,更新文章状态与版本,写入审计并保存发布快照;并发修改返回冲突。
## news.article.existingPublication
取出已有发布快照;找不到时返回不存在。
@@ -0,0 +1,9 @@
# 文章规则
## news.article.domain.pageSize
只接受 1–100 的列表大小,其他值返回请求无效。
## news.article.domain.requireUpdated
要求数据库恰好修改一条记录,否则返回冲突。
@@ -0,0 +1,5 @@
# 文章公开读模型
## news.article.model.publication.Publication
前台读取的发布快照,保存 slug、标题、正文和发布时间。
@@ -0,0 +1,9 @@
# 文章模型
## news.article.model.ArticleStatus
文章的草稿或已发布状态。
## news.article.model.Article
作者所属的文章草稿和发布状态。版本、作者与发布状态不直接作为公开字段返回。
@@ -0,0 +1,13 @@
pageSize(value: Int >= 1 and <= 100) (size: Int) pure {
size = value
}
pageSize(value: other) (size: Int) pure {
fail(dever.api.Error.Invalid)
}
requireUpdated(changed: 1) () pure {}
requireUpdated(changed: other) () pure {
fail(dever.api.Error.Conflict)
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,25 @@
type ArticleStatus {
Draft = "草稿"
Published = "已发布"
}
type Article {
private owner author_id: Int = dever.auth.userId()
slug: Text(1, 180) lower unique
title: Text(1, 160)
body: Text raw
category_id: content.category.model.id?
asset_id: media.asset.model.id?
private version: Int default 1
private status: ArticleStatus default ArticleStatus.Draft index
private published_at: DateTime?
}
index(author_id, created_at)
sql ownedArticles(author: Int, size: Int) (
articles: List<app.ArticleView>
) {
sqlite = "SELECT id, slug, title, body, version, category_id, asset_id FROM news_article_model WHERE author_id = ?1 ORDER BY id DESC LIMIT ?2"
postgres = "SELECT id, slug, title, body, version, category_id, asset_id FROM news_article_model WHERE author_id = $1 ORDER BY id DESC LIMIT $2"
}
@@ -0,0 +1,6 @@
type Publication {
slug: Text(1, 180) unique
title: Text(1, 160)
body: Text
published_at: DateTime index
}
@@ -0,0 +1,8 @@
# 租户
管理平台租户记录和启用状态,数据库准备由部署命令完成。
- [租户应用](doc/app.md)
- [租户规则](doc/domain.md)
- [租户模型](doc/model.md)
- [调用流程](link.md)
@@ -0,0 +1,41 @@
type TenantView {
id: model.id
key: Text
name: Text
}
provision(key: Text, name: Text) (tenant: TenantView) {
stored = model.create(
{
key = key
name = name
}
)
tenant = TenantView {
id = stored.id
key = stored.key
name = stored.name
}
}
active(tenant_key: Text) (tenant: TenantView) {
stored = model.first(
{
where = key == domain.normalizeKey(tenant_key)
}
)
tenant = activeView(stored)
}
activeView(stored: model.Tenant) (tenant: TenantView) {
dever.api.require(stored.status == model.TenantStatus.Active, dever.api.Error.Unauthorized)
tenant = TenantView {
id = stored.id
key = stored.key
name = stored.name
}
}
activeView(stored: null) (tenant: TenantView) {
fail(dever.api.Error.Unauthorized)
}
@@ -0,0 +1,17 @@
# 租户应用
## platform.tenant.TenantView
租户编号、唯一键和显示名称。
## platform.tenant.provision
创建租户记录,key 去除首尾空白后转为小写并保持唯一。
## platform.tenant.active
按规范化 key 查找启用租户,不存在或已禁用时返回未授权。
## platform.tenant.activeView
检查租户启用状态并转换为 TenantView,空记录返回未授权。
@@ -0,0 +1,5 @@
# 租户规则
## platform.tenant.domain.normalizeKey
去掉租户键两端空白并转为小写,用于与 Model 写入规则一致地查询。
@@ -0,0 +1,9 @@
# 租户模型
## platform.tenant.model.TenantStatus
租户的启用或禁用状态。
## platform.tenant.model.Tenant
平台库中的租户记录;key 唯一,UUID 自动生成。
@@ -0,0 +1,3 @@
normalizeKey(value: Text) (key: Text) pure {
key = text.lower(text.trim(value))
}
@@ -0,0 +1,197 @@
<!-- Generated by dever inspect --links. Do not edit. -->
<!-- source-sha256: 3d8386ca09494f4f62fa83613bf8379bbfa858632449d9d19fe86f3a86ad1d9c -->
# platform.tenant
按求值顺序展示调用、分支和任务调度,包含 API 与自动 REST 入口。点击节点打开源码;双框为调用,虚线为调度或错误路径。外部库和驱动不展开。
图不是某次运行的日志,分句和 Adapter 按输入或配置选择。修改源码后运行 `dever inspect . --links`。
## 入口
没有已收录的入口。
## 外部调用方
| 能力 | 调用方 | 方式 | 调用位置 |
| --- | --- | --- | --- |
| [platform.tenant.active](app.dever#L21) | [user.account.login](../../user/account/app/account.dever#L70) | 调用 | [../../user/account/app/account.dever:78:14](../../user/account/app/account.dever#L78,14) |
| [platform.tenant.active](app.dever#L21) | [user.account.establishLogin](../../user/account/app/account.dever#L91) | 调用 | [../../user/account/app/account.dever:99:14](../../user/account/app/account.dever#L99,14) |
| [platform.tenant.active](app.dever#L21) | [user.account.verify](../../user/account/app/account.dever#L133) | 调用 | [../../user/account/app/account.dever:137:14](../../user/account/app/account.dever#L137,14) |
| [platform.tenant.provision](app.dever#L7) | [user.account.registerAccount](../../user/account/app/account.dever#L41) | 调用 | [../../user/account/app/account.dever:48:12](../../user/account/app/account.dever#L48,12) |
## 能力与内部调用
### platform.tenant.TenantView
定义:[app.dever:1:1](app.dever#L1,1)
没有已收录的下游调用。
### platform.tenant.active
定义:[app.dever:21:1](app.dever#L21,1)
<a id="flow-720cd3e6ee01541e-1"></a>
```mermaid
flowchart TD
n0(["active"])
click n0 href "app.dever#L21,1" "打开源码"
n1(["返回"])
n3[["platform.tenant.domain.normalizeKey"]]
click n3 href "app.dever#L24,22" "打开源码"
n4["platform.tenant.model.first"]
click n4 href "app.dever#L22,12" "打开源码"
n5["赋值 stored"]
click n5 href "app.dever#L22,12" "打开源码"
n6[["platform.tenant.activeView"]]
click n6 href "app.dever#L27,12" "打开源码"
n7["赋值 tenant"]
click n7 href "app.dever#L27,12" "打开源码"
n0 --> n3
n3 --> n4
n4 --> n5
n5 --> n6
n6 --> n7
n7 --> n1
```
<details>
<summary>源码与子流程</summary>
| 节点 | 源码 | 子流程 |
| --- | --- | --- |
| platform.tenant.domain.normalizeKey | [app.dever:24:22](app.dever#L24,22) | [platform.tenant.domain.normalizeKey](link.md#flow-675cb56e9567397f-1) |
| platform.tenant.activeView | [app.dever:27:12](app.dever#L27,12) | [platform.tenant.activeView](link.md#flow-b450477f16968f30-1) |
</details>
| 目标 | 归属 | 方式 | 调用位置 |
| --- | --- | --- | --- |
| [platform.tenant.model.first](model.dever#L6) | 领域内 | 内建操作 | [app.dever:22:12](app.dever#L22,12) |
| [platform.tenant.domain.normalizeKey](domain.dever#L1) | 领域内 | 调用 | [app.dever:24:22](app.dever#L24,22) |
| [platform.tenant.activeView](app.dever#L30) | 领域内 | 调用 | [app.dever:27:12](app.dever#L27,12) |
### platform.tenant.activeView
定义:[app.dever:30:1](app.dever#L30,1)、[app.dever:39:1](app.dever#L39,1)
<a id="flow-b450477f16968f30-1"></a>
```mermaid
flowchart TD
n0(["activeView"])
click n0 href "app.dever#L30,1" "打开源码"
n1(["返回"])
n2(["传播错误"])
n3{"按实参匹配分句(互斥)"}
n4["stored: model.Tenant"]
click n4 href "app.dever#L30,1" "打开源码"
n5[["dever.api.require"]]
click n5 href "app.dever#L31,3" "打开源码"
n6["赋值 tenant"]
click n6 href "app.dever#L32,12" "打开源码"
n7["stored: null"]
click n7 href "app.dever#L39,1" "打开源码"
n8["fail dever.api.Error.Unauthorized"]
click n8 href "app.dever#L40,8" "打开源码"
n0 --> n3
n3 -->|"stored: model.Tenant"| n4
n4 --> n5
n5 --> n6
n6 --> n1
n3 -->|"stored: null"| n7
n7 --> n8
n8 --> n2
```
<details>
<summary>源码与子流程</summary>
| 节点 | 源码 | 子流程 |
| --- | --- | --- |
| dever.api.require | [app.dever:31:3](app.dever#L31,3) | 依赖或生成的分派(本页不展开) |
</details>
| 目标 | 归属 | 方式 | 调用位置 |
| --- | --- | --- | --- |
| `dever.api.require` | 依赖 | 调用 | [app.dever:31:3](app.dever#L31,3) |
### platform.tenant.domain.normalizeKey
定义:[domain.dever:1:1](domain.dever#L1,1)
<a id="flow-675cb56e9567397f-1"></a>
```mermaid
flowchart TD
n0(["normalizeKey"])
click n0 href "domain.dever#L1,1" "打开源码"
n1(["返回"])
n3[["text.trim"]]
click n3 href "domain.dever#L2,20" "打开源码"
n4[["text.lower"]]
click n4 href "domain.dever#L2,9" "打开源码"
n5["赋值 key"]
click n5 href "domain.dever#L2,9" "打开源码"
n0 --> n3
n3 --> n4
n4 --> n5
n5 --> n1
```
<details>
<summary>源码与子流程</summary>
| 节点 | 源码 | 子流程 |
| --- | --- | --- |
| text.trim | [domain.dever:2:20](domain.dever#L2,20) | 依赖或生成的分派(本页不展开) |
| text.lower | [domain.dever:2:9](domain.dever#L2,9) | 依赖或生成的分派(本页不展开) |
</details>
| 目标 | 归属 | 方式 | 调用位置 |
| --- | --- | --- | --- |
| `text.lower` | 依赖 | 调用 | [domain.dever:2:9](domain.dever#L2,9) |
| `text.trim` | 依赖 | 调用 | [domain.dever:2:20](domain.dever#L2,20) |
### platform.tenant.model.Tenant
定义:[model.dever:6:1](model.dever#L6,1)
没有已收录的下游调用。
### platform.tenant.model.TenantStatus
定义:[model.dever:1:1](model.dever#L1,1)
没有已收录的下游调用。
### platform.tenant.provision
定义:[app.dever:7:1](app.dever#L7,1)
<a id="flow-cc4bbca9579ab41a-1"></a>
```mermaid
flowchart TD
n0(["provision"])
click n0 href "app.dever#L7,1" "打开源码"
n1(["返回"])
n3["platform.tenant.model.create"]
click n3 href "app.dever#L8,12" "打开源码"
n4["赋值 stored"]
click n4 href "app.dever#L8,12" "打开源码"
n5["赋值 tenant"]
click n5 href "app.dever#L14,12" "打开源码"
n0 --> n3
n3 --> n4
n4 --> n5
n5 --> n1
```
| 目标 | 归属 | 方式 | 调用位置 |
| --- | --- | --- | --- |
| [platform.tenant.model.create](model.dever#L6) | 领域内 | 内建操作 | [app.dever:8:12](app.dever#L8,12) |
@@ -0,0 +1,11 @@
type TenantStatus {
Active = "启用"
Disabled = "禁用"
}
global type Tenant {
uuid: Uuid generated unique
key: Text(1, 64) lower unique
name: Text(1, 100)
status: TenantStatus default TenantStatus.Active index
}
@@ -0,0 +1,9 @@
# 发布计划
为文章安排一次定时发布,重复任务不会重复发布。
- [发布计划接口](doc/api-admin-manage.md)
- [发布计划应用](doc/app.md)
- [发布计划任务](doc/job.md)
- [发布计划模型](doc/model.md)
- [调用流程](link.md)
@@ -0,0 +1 @@
post schedule = app.create
@@ -0,0 +1,74 @@
type PublishRequest {
schedule_id: model.id
}
type ScheduleView {
id: model.id
article_id: news.article.model.id
publish_at: DateTime
status: Text
}
transaction create(article_id: news.article.model.id, publish_at: DateTime) (
schedule: ScheduleView
) {
news.article.requireSchedulable(article_id)
stored = model.create(
{
article_id = article_id
publish_at = publish_at
}
)
queued = dever.job.enqueueAt(
job.publish,
PublishRequest {
schedule_id = stored.id
},
uuid.toText(stored.uuid),
publish_at
)
discarded = queued
schedule = ScheduleView {
id = stored.id
article_id = stored.article_id
publish_at = stored.publish_at
status = "pending"
}
}
transaction publish(input: PublishRequest) () {
scheduled = model.get(input.schedule_id)
complete(scheduled.status, scheduled.id)
}
complete(status: model.ScheduleStatus.Completed, schedule_id: model.id) () {}
complete(status: model.ScheduleStatus.Cancelled, schedule_id: model.id) () {}
complete(status: model.ScheduleStatus.Pending, schedule_id: model.id) () {
scheduled = model.get(schedule_id)
dever.api.require(model.exists(
{
where = id == schedule_id and publish_at <= dever.time.now()
}
), dever.api.Error.Invalid)
changed = model.update(
{
where = id == scheduled.id and status == model.ScheduleStatus.Pending
},
{
status = model.ScheduleStatus.Completed
}
)
publishClaimed(changed, scheduled.article_id)
}
publishClaimed(changed: 0, article_id: news.article.model.id) () {}
publishClaimed(changed: 1, article_id: news.article.model.id) () {
publication = news.article.publishScheduled(article_id)
}
publishClaimed(changed: other, article_id: news.article.model.id) () {
fail(dever.api.Error.Conflict)
}
@@ -0,0 +1,5 @@
# 发布计划接口
## publishing.schedule.api.admin.manage.schedule
为当前作者的草稿建立定时发布计划。
@@ -0,0 +1,25 @@
# 发布计划应用
## publishing.schedule.PublishRequest
发布任务的输入,只保存计划编号。
## publishing.schedule.ScheduleView
创建计划后返回文章编号、计划时间和状态。
## publishing.schedule.create
为当前作者的草稿建立发布计划并按时入队;计划记录和入队在同一事务内完成。每篇文章只能有一条计划。
## publishing.schedule.publish
加载发布计划并按其状态执行,供 Job 调用。
## publishing.schedule.complete
仅处理已经到时的 Pending 计划;已完成或取消的计划不再执行。状态更新与文章发布在同一事务中完成。
## publishing.schedule.publishClaimed
成功更新一条计划后发布文章;零条表示无需重复执行,其他数量返回冲突。
@@ -0,0 +1,5 @@
# 发布计划任务
## publishing.schedule.job.publish
执行发布计划,最多执行 3 次(含首次),单次期限 30 秒。
@@ -0,0 +1,9 @@
# 发布计划模型
## publishing.schedule.model.ScheduleStatus
计划的待发布、已完成或已取消状态。
## publishing.schedule.model.Schedule
每篇文章的唯一发布计划,保存执行时间和状态。
Whitespace-only changes.
@@ -0,0 +1,5 @@
database default
job publish(input: app.PublishRequest) () retry(3) timeout(30000) {
app.publish(input)
}
@@ -0,0 +1,430 @@
<!-- Generated by dever inspect --links. Do not edit. -->
<!-- source-sha256: be17003a018467df95914a96c87fcb8890e4ade3118c3511d7e00fde98087ea7 -->
# publishing.schedule
按求值顺序展示调用、分支和任务调度,包含 API 与自动 REST 入口。点击节点打开源码;双框为调用,虚线为调度或错误路径。外部库和驱动不展开。
图不是某次运行的日志,分句和 Adapter 按输入或配置选择。修改源码后运行 `dever inspect . --links`。
## 入口
- API [publishing.schedule.api.admin.manage.schedule](api/admin/manage.dever#L1) → [publishing.schedule.create](app.dever#L12)
- Job [publishing.schedule.job.publish](job.dever#L3)
## 外部调用方
当前加载的源码中没有已收录的外部调用。
## 能力与内部调用
### publishing.schedule.PublishRequest
定义:[app.dever:1:1](app.dever#L1,1)
没有已收录的下游调用。
### publishing.schedule.ScheduleView
定义:[app.dever:5:1](app.dever#L5,1)
没有已收录的下游调用。
### publishing.schedule.api.admin.manage.schedule
定义:[api/admin/manage.dever:1:1](api/admin/manage.dever#L1,1)
<a id="flow-4a14d9ba44a23d84-1"></a>
```mermaid
flowchart TD
n0(["publishing.schedule.api.admin.manage.schedule"])
click n0 href "api/admin/manage.dever#L1,17" "打开源码"
n1{"路由 POST /publishing/schedule/admin/manage/schedule,topic #34;admin/manage#34; 接受请求?"}
n2(["返回 HTTP 404"])
n3["进入 API"]
n4["取得认证时间"]
n5(["返回 HTTP 500"])
n6["按 topic #34;admin/manage#34; 选择站点与 Provider"]
n7(["返回 HTTP 403"])
n8{"读取 Bearer 或站点 Cookie;两者互斥"}
n9(["返回 HTTP 401"])
n10["认证上下文就绪"]
n11(["返回 HTTP 401"])
n12["校验 Token 的签名、有效期及站点"]
n13{"Cookie 且非 GET / HEAD / OPTIONS?"}
n14["检查站点 Origin"]
n15(["返回 HTTP 403"])
n16(["返回 HTTP 401"])
n17{"按 Provider.verify 选择验证函数"}
n18[["user.account.verify"]]
click n18 href "api/admin/manage.dever#L1,17" "打开源码"
n19(["返回 HTTP 业务错误映射状态;其他故障 500"])
n20["用验证结果建立身份"]
n21(["返回 HTTP 认证错误状态"])
n22(["返回 HTTP 500"])
n23["建立请求数据库上下文"]
n24{"有已验证租户?"}
n25["数据库上下文就绪"]
n26{"部署启用了租户存储?"}
n27(["返回 HTTP 403"])
n28["按已验证租户选择数据库"]
n29(["返回 HTTP 503"])
n30{"检查租户组件启用状态:news, publishing"}
n31(["返回 HTTP 403"])
n32(["返回 HTTP 503"])
n33["进入写入口事务"]
n34["失败:保留 HTTP 错误状态"]
n35["回滚入口拥有的事务"]
n36(["返回 HTTP 500"])
n37(["返回错误响应"])
n38{"权限检查:publishing.schedule.{site}.schedule"}
n39["权限通过"]
n40["从可信身份建立 Job 执行上下文"]
n41["读取 API 输入"]
n42["解码具体参数并检查字段合同"]
n43[["publishing.schedule.create"]]
click n43 href "api/admin/manage.dever#L1,17" "打开源码"
n44["按输出类型检查可序列化值并编码响应"]
n45["提交入口拥有的事务"]
n46(["返回 HTTP 500"])
n47["发布暂存的认证 Header / Cookie"]
n48(["返回成功响应"])
n0 --> n1
n1 -->|"未匹配此入口:继续路由;均未匹配"| n2
n1 --> n3
n3 --> n4
n4 -->|"失败"| n5
n4 --> n6
n6 -->|"无站点绑定"| n7
n6 --> n8
n8 -->|"凭据格式错误或冲突"| n9
n8 -->|"无凭据"| n11
n8 -->|"有凭据"| n13
n13 -->|"否"| n12
n13 -->|"是"| n14
n14 -->|"不符合"| n15
n14 -->|"通过"| n12
n12 -->|"无 Provider 或 Token 无效"| n16
n12 --> n17
n17 -->|"user.account.verify"| n18
n18 -->|"验证函数失败"| n19
n18 --> n20
n20 -->|"身份不合法"| n21
n20 -->|"通过"| n10
n17 -->|"无匹配验证函数"| n22
n10 --> n23
n23 --> n24
n24 -->|"否"| n26
n26 -->|"是:缺少租户身份"| n27
n26 -->|"否"| n25
n24 -->|"是"| n28
n28 -->|"选择失败"| n29
n28 --> n30
n30 -->|"未启用"| n31
n30 -->|"查询失败"| n32
n30 -->|"允许"| n25
n25 --> n33
n34 --> n35
n35 -->|"事务结束失败"| n36
n35 --> n37
n33 --> n38
n38 -->|"拒绝:Access 状态;查询故障:500"| n34
n38 -->|"允许"| n39
n39 --> n40
n40 -->|"失败:认证错误状态"| n34
n40 --> n41
n41 -->|"失败:输入错误状态"| n34
n41 --> n42
n42 -->|"失败:400"| n34
n42 --> n43
n43 -->|"业务错误映射状态;其他故障:500"| n34
n43 --> n44
n44 -->|"编码失败:500"| n34
n44 --> n45
n45 -->|"提交失败"| n46
n45 --> n47
n47 --> n48
```
<details>
<summary>源码与子流程</summary>
| 节点 | 源码 | 子流程 |
| --- | --- | --- |
| user.account.verify | [api/admin/manage.dever:1:17](api/admin/manage.dever#L1,17) | [user.account.verify](../../user/account/link.md#flow-aaaacb87d61f819c-1) |
| publishing.schedule.create | [api/admin/manage.dever:1:17](api/admin/manage.dever#L1,17) | [publishing.schedule.create](link.md#flow-a5c2828f7faa5041-1) |
</details>
| 目标 | 归属 | 方式 | 调用位置 |
| --- | --- | --- | --- |
| [publishing.schedule.create](app.dever#L12) | 领域内 | API 绑定 | [api/admin/manage.dever:1:17](api/admin/manage.dever#L1,17) |
| [user.account.verify](../../user/account/app/account.dever#L133) | 跨领域 [user.account](../../user/account/link.md) | 身份验证 | [api/admin/manage.dever:1:17](api/admin/manage.dever#L1,17) |
### publishing.schedule.complete
定义:[app.dever:44:1](app.dever#L44,1)、[app.dever:46:1](app.dever#L46,1)、[app.dever:48:1](app.dever#L48,1)
<a id="flow-87bb9cb4f5f0c2f6-1"></a>
```mermaid
flowchart TD
n0(["complete"])
click n0 href "app.dever#L44,1" "打开源码"
n1(["返回"])
n3{"按实参匹配分句(互斥)"}
n4["status: model.ScheduleStatus.Completed"]
click n4 href "app.dever#L44,1" "打开源码"
n5["status: model.ScheduleStatus.Cancelled"]
click n5 href "app.dever#L46,1" "打开源码"
n6["status: model.ScheduleStatus.Pending"]
click n6 href "app.dever#L48,1" "打开源码"
n7["publishing.schedule.model.get"]
click n7 href "app.dever#L49,15" "打开源码"
n8["赋值 scheduled"]
click n8 href "app.dever#L49,15" "打开源码"
n9[["dever.time.now"]]
click n9 href "app.dever#L52,53" "打开源码"
n10["publishing.schedule.model.exists"]
click n10 href "app.dever#L50,21" "打开源码"
n11[["dever.api.require"]]
click n11 href "app.dever#L50,3" "打开源码"
n12["publishing.schedule.model.update"]
click n12 href "app.dever#L55,13" "打开源码"
n13["赋值 changed"]
click n13 href "app.dever#L55,13" "打开源码"
n14[["publishing.schedule.publishClaimed"]]
click n14 href "app.dever#L63,3" "打开源码"
n0 --> n3
n3 -->|"status: model.ScheduleStatus.Completed"| n4
n4 --> n1
n3 -->|"status: model.ScheduleStatus.Cancelled"| n5
n5 --> n1
n3 -->|"status: model.ScheduleStatus.Pending"| n6
n6 --> n7
n7 --> n8
n8 --> n9
n9 --> n10
n10 --> n11
n11 --> n12
n12 --> n13
n13 --> n14
n14 --> n1
```
<details>
<summary>源码与子流程</summary>
| 节点 | 源码 | 子流程 |
| --- | --- | --- |
| dever.time.now | [app.dever:52:53](app.dever#L52,53) | 依赖或生成的分派(本页不展开) |
| dever.api.require | [app.dever:50:3](app.dever#L50,3) | 依赖或生成的分派(本页不展开) |
| publishing.schedule.publishClaimed | [app.dever:63:3](app.dever#L63,3) | [publishing.schedule.publishClaimed](link.md#flow-c4a292464addac3f-1) |
</details>
| 目标 | 归属 | 方式 | 调用位置 |
| --- | --- | --- | --- |
| [publishing.schedule.model.get](model.dever#L7) | 领域内 | 内建操作 | [app.dever:49:15](app.dever#L49,15) |
| `dever.api.require` | 依赖 | 调用 | [app.dever:50:3](app.dever#L50,3) |
| [publishing.schedule.model.exists](model.dever#L7) | 领域内 | 内建操作 | [app.dever:50:21](app.dever#L50,21) |
| `dever.time.now` | 依赖 | 调用 | [app.dever:52:53](app.dever#L52,53) |
| [publishing.schedule.model.update](model.dever#L7) | 领域内 | 内建操作 | [app.dever:55:13](app.dever#L55,13) |
| [publishing.schedule.publishClaimed](app.dever#L66) | 领域内 | 调用 | [app.dever:63:3](app.dever#L63,3) |
### publishing.schedule.create
定义:[app.dever:12:1](app.dever#L12,1)
<a id="flow-a5c2828f7faa5041-1"></a>
```mermaid
flowchart TD
n0(["create"])
click n0 href "app.dever#L12,1" "打开源码"
n1["开始事务"]
n2(["提交事务,返回"])
n3(["回滚事务,传播错误"])
n4[["news.article.requireSchedulable"]]
click n4 href "app.dever#L15,3" "打开源码"
n5["publishing.schedule.model.create"]
click n5 href "app.dever#L16,12" "打开源码"
n6["赋值 stored"]
click n6 href "app.dever#L16,12" "打开源码"
n7[["uuid.toText"]]
click n7 href "app.dever#L27,5" "打开源码"
n8["持久化任务入队"]
click n8 href "app.dever#L22,12" "打开源码"
n9[["Job:publishing.schedule.job.publish"]]
click n9 href "app.dever#L22,12" "打开源码"
n10["赋值 queued"]
click n10 href "app.dever#L22,12" "打开源码"
n11["赋值 discarded"]
click n11 href "app.dever#L30,15" "打开源码"
n12["赋值 schedule"]
click n12 href "app.dever#L31,14" "打开源码"
n0 --> n1
n1 -.->|"事务体任一未捕获错误"| n3
n1 --> n4
n4 --> n5
n5 --> n6
n6 --> n7
n7 --> n8
n8 -.->|"由 Worker 后续调度,不等待执行"| n9
n8 --> n10
n10 --> n11
n11 --> n12
n12 --> n2
```
<details>
<summary>源码与子流程</summary>
| 节点 | 源码 | 子流程 |
| --- | --- | --- |
| news.article.requireSchedulable | [app.dever:15:3](app.dever#L15,3) | [news.article.requireSchedulable](../../news/article/link.md#flow-dc698efe57d88983-1) |
| uuid.toText | [app.dever:27:5](app.dever#L27,5) | 依赖或生成的分派(本页不展开) |
| Job:publishing.schedule.job.publish | [app.dever:22:12](app.dever#L22,12) | [publishing.schedule.job.publish](link.md#flow-c9df8cbe8f8cd013-1) |
</details>
| 目标 | 归属 | 方式 | 调用位置 |
| --- | --- | --- | --- |
| [news.article.requireSchedulable](../../news/article/app/editing.dever#L107) | 跨领域 [news.article](../../news/article/link.md) | 调用 | [app.dever:15:3](app.dever#L15,3) |
| [publishing.schedule.model.create](model.dever#L7) | 领域内 | 内建操作 | [app.dever:16:12](app.dever#L16,12) |
| [publishing.schedule.job.publish](job.dever#L3) | 领域内 | 入队 | [app.dever:23:5](app.dever#L23,5) |
| `uuid.toText` | 依赖 | 调用 | [app.dever:27:5](app.dever#L27,5) |
### publishing.schedule.job.publish
定义:[job.dever:3:1](job.dever#L3,1)
<a id="flow-c9df8cbe8f8cd013-1"></a>
```mermaid
flowchart TD
n0(["publish"])
click n0 href "job.dever#L3,1" "打开源码"
n1(["返回"])
n3[["publishing.schedule.publish"]]
click n3 href "job.dever#L4,3" "打开源码"
n0 --> n3
n3 --> n1
```
<details>
<summary>源码与子流程</summary>
| 节点 | 源码 | 子流程 |
| --- | --- | --- |
| publishing.schedule.publish | [job.dever:4:3](job.dever#L4,3) | [publishing.schedule.publish](link.md#flow-1452f352cf5266e6-1) |
</details>
| 目标 | 归属 | 方式 | 调用位置 |
| --- | --- | --- | --- |
| [publishing.schedule.publish](app.dever#L39) | 领域内 | 调用 | [job.dever:4:3](job.dever#L4,3) |
### publishing.schedule.model.Schedule
定义:[model.dever:7:1](model.dever#L7,1)
没有已收录的下游调用。
### publishing.schedule.model.ScheduleStatus
定义:[model.dever:1:1](model.dever#L1,1)
没有已收录的下游调用。
### publishing.schedule.publish
定义:[app.dever:39:1](app.dever#L39,1)
<a id="flow-1452f352cf5266e6-1"></a>
```mermaid
flowchart TD
n0(["publish"])
click n0 href "app.dever#L39,1" "打开源码"
n1["开始事务"]
n2(["提交事务,返回"])
n3(["回滚事务,传播错误"])
n4["publishing.schedule.model.get"]
click n4 href "app.dever#L40,15" "打开源码"
n5["赋值 scheduled"]
click n5 href "app.dever#L40,15" "打开源码"
n6[["publishing.schedule.complete"]]
click n6 href "app.dever#L41,3" "打开源码"
n0 --> n1
n1 -.->|"事务体任一未捕获错误"| n3
n1 --> n4
n4 --> n5
n5 --> n6
n6 --> n2
```
<details>
<summary>源码与子流程</summary>
| 节点 | 源码 | 子流程 |
| --- | --- | --- |
| publishing.schedule.complete | [app.dever:41:3](app.dever#L41,3) | [publishing.schedule.complete](link.md#flow-87bb9cb4f5f0c2f6-1) |
</details>
| 目标 | 归属 | 方式 | 调用位置 |
| --- | --- | --- | --- |
| [publishing.schedule.model.get](model.dever#L7) | 领域内 | 内建操作 | [app.dever:40:15](app.dever#L40,15) |
| [publishing.schedule.complete](app.dever#L44) | 领域内 | 调用 | [app.dever:41:3](app.dever#L41,3) |
### publishing.schedule.publishClaimed
定义:[app.dever:66:1](app.dever#L66,1)、[app.dever:68:1](app.dever#L68,1)、[app.dever:72:1](app.dever#L72,1)
<a id="flow-c4a292464addac3f-1"></a>
```mermaid
flowchart TD
n0(["publishClaimed"])
click n0 href "app.dever#L66,1" "打开源码"
n1(["返回"])
n2(["传播错误"])
n3{"按实参匹配分句(互斥)"}
n4["changed: 0"]
click n4 href "app.dever#L66,1" "打开源码"
n5["changed: 1"]
click n5 href "app.dever#L68,1" "打开源码"
n6[["news.article.publishScheduled"]]
click n6 href "app.dever#L69,17" "打开源码"
n7["赋值 publication"]
click n7 href "app.dever#L69,17" "打开源码"
n8["changed: other"]
click n8 href "app.dever#L72,1" "打开源码"
n9["fail dever.api.Error.Conflict"]
click n9 href "app.dever#L73,8" "打开源码"
n0 --> n3
n3 -->|"changed: 0"| n4
n4 --> n1
n3 -->|"changed: 1"| n5
n5 --> n6
n6 --> n7
n7 --> n1
n3 -->|"changed: other"| n8
n8 --> n9
n9 --> n2
```
<details>
<summary>源码与子流程</summary>
| 节点 | 源码 | 子流程 |
| --- | --- | --- |
| news.article.publishScheduled | [app.dever:69:17](app.dever#L69,17) | [news.article.publishScheduled](../../news/article/link.md#flow-5ba4105cff8222e5-1) |
</details>
| 目标 | 归属 | 方式 | 调用位置 |
| --- | --- | --- | --- |
| [news.article.publishScheduled](../../news/article/app/publication.dever#L22) | 跨领域 [news.article](../../news/article/link.md) | 调用 | [app.dever:69:17](app.dever#L69,17) |
@@ -0,0 +1,12 @@
type ScheduleStatus {
Pending = "待发布"
Completed = "已完成"
Cancelled = "已取消"
}
type Schedule {
uuid: Uuid generated unique
article_id: news.article.model.id unique
publish_at: DateTime index
status: ScheduleStatus default ScheduleStatus.Pending index
}
@@ -0,0 +1,7 @@
# 健康检查
提供命令行健康检查。
- [健康检查命令](doc/api.md)
- [系统健康](doc/app.md)
- [调用流程](link.md)
@@ -0,0 +1 @@
cmd ping = app.ping
@@ -0,0 +1,9 @@
type HealthView {
status: Text
}
ping() (result: HealthView) {
result = HealthView {
status = "ready"
}
}
@@ -0,0 +1,5 @@
# 健康检查命令
## system.health.api.ping
返回 ready 状态。
@@ -0,0 +1,9 @@
# 系统健康
## system.health.HealthView
健康检查返回的状态。
## system.health.ping
返回 ready,供命令行健康检查使用。
@@ -0,0 +1,73 @@
<!-- Generated by dever inspect --links. Do not edit. -->
<!-- source-sha256: 763ebcc3cdd50ee3c2f028ca31712656106fab555c878ad702f7f58bca5f0737 -->
# system.health
按求值顺序展示调用、分支和任务调度,包含 API 与自动 REST 入口。点击节点打开源码;双框为调用,虚线为调度或错误路径。外部库和驱动不展开。
图不是某次运行的日志,分句和 Adapter 按输入或配置选择。修改源码后运行 `dever inspect . --links`。
## 入口
- CMD [system.health.api.ping](api.dever#L1) → [system.health.ping](app.dever#L5)
## 外部调用方
当前加载的源码中没有已收录的外部调用。
## 能力与内部调用
### system.health.HealthView
定义:[app.dever:1:1](app.dever#L1,1)
没有已收录的下游调用。
### system.health.api.ping
定义:[api.dever:1:1](api.dever#L1,1)
<a id="flow-97bc3f948e0bd788-1"></a>
```mermaid
flowchart TD
n0(["system.health.api.ping"])
click n0 href "api.dever#L1,12" "打开源码"
n1[["system.health.ping"]]
click n1 href "api.dever#L1,12" "打开源码"
n2(["返回命令结果;未捕获错误终止命令"])
n0 --> n1
n1 --> n2
```
<details>
<summary>源码与子流程</summary>
| 节点 | 源码 | 子流程 |
| --- | --- | --- |
| system.health.ping | [api.dever:1:12](api.dever#L1,12) | [system.health.ping](link.md#flow-e4086f731ad69c62-1) |
</details>
| 目标 | 归属 | 方式 | 调用位置 |
| --- | --- | --- | --- |
| [system.health.ping](app.dever#L5) | 领域内 | CMD 绑定 | [api.dever:1:12](api.dever#L1,12) |
### system.health.ping
定义:[app.dever:5:1](app.dever#L5,1)
<a id="flow-e4086f731ad69c62-1"></a>
```mermaid
flowchart TD
n0(["ping"])
click n0 href "app.dever#L5,1" "打开源码"
n1(["返回"])
n3["赋值 result"]
click n3 href "app.dever#L6,12" "打开源码"
n0 --> n3
n3 --> n1
```
没有已收录的下游调用。
@@ -0,0 +1,17 @@
# 账户与会话
管理用户、凭据、租户成员和站点会话。
- [后台会话接口](doc/api-admin-session.md)
- [前台会话接口](doc/api-front-session.md)
- [账户引导命令](doc/api.md)
- [账户应用](doc/app.md)
- [账户规则](doc/domain.md)
- [用户主体模型](doc/model-principal.md)
- [账户凭据模型](doc/model.md)
- [成员应用](doc/membership-app.md)
- [租户成员模型](doc/membership-model.md)
- [会话应用](doc/session-app.md)
- [会话模型](doc/session-model.md)
- [会话清理任务](doc/session-job.md)
- [调用流程](link.md)
@@ -0,0 +1 @@
long cmd bootstrap = app.bootstrap
@@ -0,0 +1,3 @@
public long post login = app.login
post logout = app.logout
@@ -0,0 +1,3 @@
public long post login = app.login
post logout = app.logout
@@ -0,0 +1,163 @@
type AccountView {
id: model.principal.id
uuid: Uuid
email: Text
display_name: Text
}
type BootstrapView {
tenant_id: platform.tenant.model.id
user_id: model.principal.id
credential_id: model.id
}
type LoginView {
account: AccountView
tenant_id: platform.tenant.model.id
site: Text
}
type LogoutView {
revoked: Bool
}
type Identity {
id: Text
user_id: model.principal.id?
tenant_id: platform.tenant.model.id?
}
bootstrap(
tenant_key: Text,
tenant_name: Text,
email: Text,
display_name: Text,
password: Secret
) (result: BootstrapView) {
hash = blocking(dever.crypto.passwordHash(password))
result = registerAccount(tenant_key, tenant_name, email, display_name, hash)
}
transaction registerAccount(
tenant_key: Text,
tenant_name: Text,
email: Text,
display_name: Text,
hash: Text
) (result: BootstrapView) {
tenant = platform.tenant.provision(tenant_key, tenant_name)
principal = model.principal.create(
{
display_name = display_name
}
)
stored = model.create(
{
principal_id = principal.id
email = email
password_hash = hash
}
)
domain.validateEmail(stored.email)
grantOwner(principal.id, tenant.id)
result = BootstrapView {
tenant_id = tenant.id
user_id = principal.id
credential_id = stored.id
}
}
login(credential_id: model.id, password: Secret, tenant: Text) (result: LoginView) {
dever.api.require(model.exists(
{
where = id == credential_id
}
), dever.api.Error.Unauthorized)
stored = model.get(credential_id)
principal = model.principal.get(stored.principal_id)
selected = platform.tenant.active(tenant)
dever.api.require(stored.status == model.AccountStatus.Active, dever.api.Error.Unauthorized)
dever.api.require(principal.status == model.principal.PrincipalStatus.Active, dever.api.Error.Unauthorized)
dever.api.require(blocking(dever.crypto.passwordVerify(password, stored.password_hash)), dever.api.Error.Unauthorized)
authenticated = establishLogin(credential_id, selected.key, dever.site.key())
dever.auth.issueCookie(
authenticated.subject,
uuid.toText(authenticated.session_id),
authenticated.tenant_key
)
result = authenticated.result
}
transaction establishLogin(credential_id: model.id, tenant: Text, site: Text) (
result: LoginView,
subject: Text,
session_id: Uuid,
tenant_key: Text
) {
stored = model.get(credential_id)
principal = model.principal.get(stored.principal_id)
selected = platform.tenant.active(tenant)
dever.api.require(stored.status == model.AccountStatus.Active, dever.api.Error.Unauthorized)
dever.api.require(principal.status == model.principal.PrincipalStatus.Active, dever.api.Error.Unauthorized)
requireMembership(principal.id, selected.id)
session = open(
uuid.toText(principal.uuid),
principal.id,
selected.id,
selected.key,
site
)
result = LoginView {
account = AccountView {
id = principal.id
uuid = principal.uuid
email = stored.email
display_name = principal.display_name
}
tenant_id = selected.id
site = site
}
subject = uuid.toText(principal.uuid)
session_id = session.uuid
tenant_key = selected.key
}
logout() (result: LogoutView) {
revoke(uuid.parse(dever.auth.session()))
dever.auth.clearCookie()
result = LogoutView {
revoked = true
}
}
verify(claims: dever.auth.Claims) (identity: Identity) {
session = authenticate(uuid.parse(claims.session), claims.site)
dever.api.require(session.subject == claims.subject, dever.api.Error.Unauthorized)
domain.requireTenant(session.tenant_key, claims.tenant)
selected = platform.tenant.active(session.tenant_key)
dever.api.require(selected.id == session.tenant_id, dever.api.Error.Unauthorized)
active(session.user_id)
requireMembership(session.user_id, session.tenant_id)
identity = Identity {
id = claims.subject
user_id = session.user_id
tenant_id = session.tenant_id
}
}
active(user_id: model.principal.id) () {
principal = model.principal.get(user_id)
dever.api.require(principal.status == model.principal.PrincipalStatus.Active, dever.api.Error.Unauthorized)
}
currentUser() (id: Int) {
id = requireUser(dever.auth.userId())
}
requireUser(value: Int) (id: Int) {
id = value
}
requireUser(value: null) (id: Int) {
fail(dever.api.Error.Unauthorized)
}
@@ -0,0 +1,31 @@
grantOwner(
account_id: model.principal.id,
selected_tenant_id: platform.tenant.model.id
) () {
stored = model.membership.create(
{
user_id = account_id
tenant_id = selected_tenant_id
role = model.membership.MembershipRole.Owner
}
)
discarded = stored
}
requireMembership(
account_id: model.principal.id,
selected_tenant_id: platform.tenant.model.id
) () {
stored = model.membership.first(
{
where = user_id == account_id and tenant_id == selected_tenant_id and status == model.membership.MembershipStatus.Active
}
)
activeMembership(stored)
}
activeMembership(stored: model.membership.Membership) () {}
activeMembership(stored: null) () {
fail(dever.api.Error.Unauthorized)
}
@@ -0,0 +1,94 @@
type SessionView {
uuid: Uuid
}
type AuthSession {
subject: Text
user_id: model.principal.id
tenant_id: platform.tenant.model.id
tenant_key: Text
}
open(
subject: Text,
account_id: model.principal.id,
selected_tenant_id: platform.tenant.model.id,
selected_tenant_key: Text,
site_key: Text
) (session: SessionView) {
stored = model.session.create(
{
subject = subject
user_id = account_id
tenant_id = selected_tenant_id
tenant_key = selected_tenant_key
site = site_key
expires_at = dever.time.add(dever.time.now(), dever.time.duration(3600000))
}
)
session = SessionView {
uuid = stored.uuid
}
}
authenticate(session_id: Uuid, site_key: Text) (result: AuthSession) {
stored = model.session.first(
{
where = uuid == session_id and site == site_key and revoked == false and expires_at > dever.time.now(
)
}
)
result = authSession(stored)
}
authenticate(session_id: null, site_key: Text) (result: AuthSession) {
fail(dever.api.Error.Unauthorized)
}
revoke(session_id: Uuid) () {
stored = model.session.first(
{
where = uuid == session_id and revoked == false
}
)
revokeFound(stored)
}
revoke(session_id: null) () {
fail(dever.api.Error.Unauthorized)
}
authSession(stored: model.session.Session) (result: AuthSession) {
result = AuthSession {
subject = stored.subject
user_id = stored.user_id
tenant_id = stored.tenant_id
tenant_key = stored.tenant_key
}
}
authSession(stored: null) (result: AuthSession) {
fail(dever.api.Error.Unauthorized)
}
revokeFound(stored: model.session.Session) () {
changed = model.session.update(
stored.id,
{
revoked = true
}
)
discarded = changed
}
revokeFound(stored: null) () {
fail(dever.api.Error.Unauthorized)
}
cleanup() () {
removed = model.session.delete(
{
where = revoked == true or expires_at <= dever.time.now()
}
)
}
@@ -0,0 +1,9 @@
# 后台会话接口
## user.account.api.admin.session.login
校验凭据并建立后台站点会话,成功后设置认证 Cookie。
## user.account.api.admin.session.logout
撤销当前会话并清除认证 Cookie。
@@ -0,0 +1,9 @@
# 前台会话接口
## user.account.api.front.session.login
校验凭据并建立前台站点会话,成功后设置认证 Cookie。
## user.account.api.front.session.logout
撤销当前会话并清除认证 Cookie。
@@ -0,0 +1,5 @@
# 账户引导命令
## user.account.api.bootstrap
创建租户、用户、凭据和 Owner 成员关系,返回部署所需编号。
@@ -0,0 +1,57 @@
# 账户应用
## user.account.AccountView
登录后公开的用户编号、UUID、邮箱和显示名,不含密码哈希。
## user.account.BootstrapView
引导完成后返回租户、用户和凭据编号,供后续部署命令使用。
## user.account.LoginView
登录成功后的公开账户、租户编号和站点。
## user.account.LogoutView
退出操作是否已撤销会话。
## user.account.Identity
Provider 返回的可信主体、用户编号和租户编号。
## user.account.bootstrap
在线程池计算密码哈希,再创建租户、用户、凭据和 Owner 成员关系。核心站点 Owner 权限仍由部署命令建立。
## user.account.registerAccount
在同一事务中创建租户、用户、邮箱凭据和 Owner 成员记录;邮箱验证失败会回滚。
## user.account.login
校验凭据、用户和租户状态及密码,创建会话并设置当前站点 Cookie。
## user.account.establishLogin
事务中重新检查账号状态和成员关系,创建站点会话,返回签发 Cookie 所需信息。
## user.account.logout
撤销当前会话并清除认证 Cookie。
## user.account.verify
校验会话、主体、租户、用户和成员关系,返回可信身份供核心鉴权。
## user.account.active
要求用户主体已启用,否则返回未授权。
## user.account.currentUser
读取当前认证用户编号,没有用户身份时返回未授权。
## user.account.requireUser
取出非空用户编号,null 返回未授权。
@@ -0,0 +1,9 @@
# 账户规则
## user.account.domain.validateEmail
要求邮箱包含非首尾位置的 @,不验证邮件能否投递。
## user.account.domain.requireTenant
要求认证声明中的租户键与会话一致,缺失或不同返回未授权。
@@ -0,0 +1,13 @@
# 成员应用
## user.account.grantOwner
创建租户 Owner 成员记录,不修改核心站点角色。
## user.account.requireMembership
要求用户与租户之间存在启用的成员关系。
## user.account.activeMembership
找到有效成员记录时通过,否则返回未授权。
@@ -0,0 +1,13 @@
# 租户成员模型
## user.account.model.membership.MembershipRole
租户成员的 Owner、Editor 或 Reader 身份。
## user.account.model.membership.MembershipStatus
成员关系的启用或禁用状态。
## user.account.model.membership.Membership
平台库中的用户与租户关系,每对用户和租户只能有一条。
@@ -0,0 +1,9 @@
# 用户主体模型
## user.account.model.principal.PrincipalStatus
用户主体的启用或禁用状态。
## user.account.model.principal.Principal
平台用户主体,保存自动生成的 UUID、显示名和状态。
@@ -0,0 +1,9 @@
# 账户凭据模型
## user.account.model.AccountStatus
登录凭据的启用或禁用状态。
## user.account.model.Account
平台登录凭据,关联用户主体;邮箱规范化且唯一,密码哈希为私有原文。
@@ -0,0 +1,33 @@
# 会话应用
## user.account.SessionView
新会话的 UUID。
## user.account.AuthSession
已认证会话的主体、用户和租户信息。
## user.account.open
创建有效期一小时的站点会话。
## user.account.authenticate
按会话 UUID 和站点查找未撤销、未过期的会话;无效时返回未授权。
## user.account.revoke
按 UUID 查找并撤销尚未撤销的会话;无效编号或缺失记录返回未授权。
## user.account.authSession
把已找到的会话转换为认证信息;空记录返回未授权。
## user.account.revokeFound
标记已有会话为已撤销;空记录返回未授权。
## user.account.cleanup
删除已撤销或已过期的会话。
@@ -0,0 +1,5 @@
# 会话清理任务
## user.account.job.cleanup
每小时清理失效会话,最多执行 3 次(含首次),单次期限 30 秒。
@@ -0,0 +1,5 @@
# 会话模型
## user.account.model.session.Session
平台会话记录,包含主体、用户、租户、站点、到期时间和撤销标记。
@@ -0,0 +1,17 @@
validateEmail(address: Text) () pure {
dever.api.require(
text.contains(address, "@") and not text.startsWith(address, "@") and not text.endsWith(
address,
"@"
),
dever.api.Error.Invalid
)
}
requireTenant(expected: Text, actual: Text) () pure {
dever.api.require(expected == actual, dever.api.Error.Unauthorized)
}
requireTenant(expected: Text, actual: null) () pure {
fail(dever.api.Error.Unauthorized)
}
@@ -0,0 +1,7 @@
database default
job cleanup() () retry(3) timeout(30000) {
app.cleanup()
}
schedule cleanup = "0 * * * *"
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,11 @@
type AccountStatus {
Active = "启用"
Disabled = "禁用"
}
global type Account {
principal_id: user.account.model.principal.id unique
email: Text(254) lower unique
private password_hash: Text(255) raw
status: AccountStatus default AccountStatus.Active index
}
Loaded 100 of 1479 files, more files were not shown because too many files have changed in this diff. Show more