Initial Dever 0.0.1
This commit is contained in:
commit
81233d098c
1479 files changed
+163782
No files matched your search
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in new issue
Block a user