Initial Dever 0.0.1
This commit is contained in:
commit
81233d098c
1479 files changed
+163782
No files matched your search
@@ -0,0 +1,80 @@
|
||||
# Dever 示例
|
||||
|
||||
入门项目见 [随包示例](../skills/references/examples.md)。CMS 演示多租户内容管理,包括登录、权限、文章修订、上传和定时发布。
|
||||
|
||||
CMS 只维护一份 [Dever 源码](cms/dever/module/news/article/api/admin/manage.dever),领域说明存放在 README 和 doc 中:
|
||||
|
||||
```text
|
||||
module/
|
||||
platform/tenant/ 平台租户
|
||||
user/account/ 凭据、成员、会话、bootstrap CMD
|
||||
user/authorization/ 核心角色授权的业务入口
|
||||
content/category/ 分类
|
||||
content/revision/ 文章编辑与修订快照
|
||||
media/asset/ 上传、Storage Port/Adapter
|
||||
publishing/schedule/ 定时发布与 Job
|
||||
audit/operation/ 操作审计
|
||||
system/health/ 健康 CMD
|
||||
news/article/
|
||||
app/editing.dever
|
||||
app/publication.dever
|
||||
api/admin/manage.dever
|
||||
api/front/browse.dever
|
||||
domain.dever
|
||||
model.dever
|
||||
model/publication.dever
|
||||
test/
|
||||
content/category/lifecycle.dever
|
||||
news/article/publish.dever
|
||||
user/account/bootstrap.dever
|
||||
user/account/validation.dever
|
||||
user/account/lifecycle.dever
|
||||
```
|
||||
|
||||
## 数据和权限
|
||||
|
||||
平台租户、账号、成员和会话使用 `global type`,保存在平台库。文章、分类、修订、媒体、排期和审计放在各租户自己的数据库。
|
||||
|
||||
账户、成员和会话同属账户领域,避免登录调用与 Model ID 引用形成循环依赖。租户文章的作者编号来自已验证身份,使用 Int 存储;物理外键只连接同一个数据库中的 Model,不能从租户库指向平台账户库。
|
||||
|
||||
verify 先核实身份数据,核心再按站点和角色检查接口权限,通过后才访问租户数据。
|
||||
|
||||
文章接口位于 `/news/article/admin/manage/`:create 创建,list/detail 查询,remove 删除草稿,publish 发布。
|
||||
|
||||
编辑使用 `/content/revision/admin/manage/create`,传入文章 ID、expected_version、标题正文和可空分类/媒体 ID。版本冲突返回 409,文章更新与修订快照一起提交。文章不用自动 REST 写接口,以免跳过这些规则;列表返回 `app.ArticleView`,不泄漏私有 Model。
|
||||
|
||||
定时发布使用 `/publishing/schedule/admin/manage/schedule`,传入文章 ID 和 publish_at。创建时检查作者和草稿状态;每个排期有独立任务,不会合并同时发布的文章。
|
||||
|
||||
执行任务前,核心再次核验身份和权限。发布、审计和排期状态一起提交或回滚,完成任务重复执行不重复写入。手动重复发布返回 409。
|
||||
|
||||
## 先检查,再运行
|
||||
|
||||
在仓库根目录执行:
|
||||
|
||||
```bash
|
||||
dever check examples/cms/dever
|
||||
dever test examples/cms/dever --filter news.article.publish
|
||||
dever fmt examples/cms/dever --check
|
||||
```
|
||||
|
||||
测试使用临时 SQLite,不连接部署数据库。去掉 `--filter` 可执行全部应用测试。
|
||||
|
||||
实际部署前先替换 JWT secret,并把两个站点的 origin 和 Host 改成真实 HTTPS 入口;origin 是用户访问的外部地址,不是内部监听地址。
|
||||
|
||||
用 bootstrap CMD 建立平台租户、Owner 成员和密码:
|
||||
|
||||
```sh
|
||||
dever run examples/cms/dever -- user.account.bootstrap '{"tenant_key":"tenant-one","tenant_name":"Tenant One","email":"owner@example.com","display_name":"Owner","password":"change-me-now"}'
|
||||
```
|
||||
|
||||
替换示例账号与密码,再用返回的真实 tenant_id 和 user_id 执行以下命令,不要猜测 ID 是 1:
|
||||
|
||||
```text
|
||||
dever tenant migrate examples/cms/dever <tenant_id>
|
||||
dever tenant owner examples/cms/dever <tenant_id> admin <user_id>
|
||||
dever tenant owner examples/cms/dever <tenant_id> front <user_id>
|
||||
```
|
||||
|
||||
这会迁移租户数据库,并建立后台、前台的核心 Owner。之后各站点通过 `user/authorization/<site>/manage` 管理普通角色。运行配置在项目的 `config/setting.json`,数据在 `data/`;`dever run examples/cms/dever` 不带 CMD 参数时启动 HTTP 服务。
|
||||
|
||||
应用测试覆盖分类、发布规则、账号和会话。真实登录、HTTP、租户隔离及发布流程使用 [CMS 验收入口](../test/performance/cms.py)。
|
||||
@@ -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
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