diff --git a/README.md b/README.md
index 7fa07d6..c595163 100644
--- a/README.md
+++ b/README.md
@@ -39,12 +39,12 @@ pm2 start node --name kana -- index
## 特征
-
`/:name/:_id`
-RESTful 风格 API, URL形式为两段 name是对象类型, _id是对象id
+RESTful 风格 API, url 形式为两段 name是对象类型, _id是对象id. (与 vue 的 router 类同)
+
+如发表一篇文章, 文章类型是 article, 文章 id 是 2333, 则 url为 `/article/2333`
-如发表一篇文章,
@@ -78,7 +78,7 @@ fetch('/user', {
}
```
-* 创建的第一个账户默认为管理员
+* 创建的第一个账户默认为管理员账户
* 可以使用管理员权限设置其他账户为管理员
* 默认并没有验证邮箱等检查步骤, 允许直接设置, 也允许用户重名
@@ -86,6 +86,8 @@ fetch('/user', {
#### 登录会话
+登录行为被认为是创建一个终端到服务器的会话. 因此不是使用 login 或 signin, 而是 session.
+
```javascript
fetch('/session', {
method: 'POST',
@@ -103,6 +105,8 @@ fetch('/session', {
#### 会话列表
+因此, 可以查看和管理自己所有的终端会话
+
```javascript
fetch('/session', {
method: 'GET',
@@ -120,12 +124,11 @@ fetch('/session', {
-
-
#### 注销会话
-注销当前会话
+相应地, 退出行为被认为是删除一个终端到服务端的会话, 因此不使用 loguot 或 signout, 而是 session.
+注销当前会话
```javascript
fetch('/session', {
method: 'DELETE',
@@ -176,6 +179,9 @@ fetch('/user/ApSXNLoUy', {
#### 上传头像
+实际分为两步,
+第一步先上传附件到自己的账户
+
```html
@@ -196,6 +202,7 @@ function upload() {
```
+第二步修改自己的头像路径为返回的图像路径, (!注意此处未作安全检查)
```javascript
fetch('/user/ApSXNLoUy', {
method: 'PATCH',
@@ -212,6 +219,7 @@ fetch('/user/ApSXNLoUy', {
#### 删除用户
+管理员可以直接删除指定用户, 普通用户可以删除自己
```javascript
fetch('/user/ApSXNLoUy', {
method: 'DELETE',
@@ -224,6 +232,8 @@ fetch('/user/ApSXNLoUy', {
#### 创建文章
+此处 book 路径是未作限制的, 也可以是其他未被限制的路径.
+
```javascript
fetch('/book', {
method: 'POST',
@@ -248,6 +258,11 @@ fetch('/book', {
#### 评论文章
+这里假设文章类型是 book, 文章 id 是 ppNXLoUK
+
+attach 意为附属于指定对象类型
+aid 意为附属于指定对象 id
+
```javascript
fetch('/post?attach=book&aid=ppNXLoUK', {
method: 'POST',
@@ -265,10 +280,34 @@ fetch('/post?attach=book&aid=ppNXLoUK', {
}
```
+#### 评论评论
+(二级评论)
+这里假设评论类型是 post, 评论 id 是 spNkjLA
+受益于评论的实现结构, 也可以对二级评论继续增加三级评论, 也可以无限深度
+
+```javascript
+fetch('/post?attach=post&aid=spNkjLA', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ data: 'yaaaaaaaaaa~'
+ }),
+}).then(Response => Response.json()).then(data => {
+ console.log(data)
+})
+
+{
+ _id: 'adjkasj',
+ data: 'ahahahha~'
+}
+```
+
#### 点赞评论
+受益于评论的实现结构, 也可以对二级评论作点赞操作, 也可以对任意对象点赞操作
+
```javascript
fetch('/like?attach=post&aid=spNkjLA', {
method: 'POST',
@@ -299,6 +338,9 @@ fetch('/like/SOAPSAdaw', {
#### 调频广播
+通过频道订阅模式实现广播, 其中心思想是保障一个终端只维持一个 websocket 连接,
+所有消息都通过一个连接通道发送到终端, 如聊天室频道, 系统消息, 全局广播, 消息盒子通知等
+
```javascript
let socket = new WebSocket("ws://localhost:2333");
@@ -355,7 +397,6 @@ socket.onerror = function(error) {
#### 游客广播
允许未登录会话加入订阅, 将未登录会话加入到游客账户
-
因此不能再通过账户登录状态进行拦截
@@ -368,8 +409,8 @@ socket.onerror = function(error) {
```javascript
{
- page: Number, // 当前页码(默认为1)
- pagesize: Number, // 分页大小(默认20)
+ page: Number, // 当前页码(默认为1)
+ pagesize: Number, // 分页大小(默认20)
sort: string, // 排序方式(只能是对象的通用属性名)
desc: Number, // 0或1, 正序和倒序
uid: string, // 指定发布者uid查询
diff --git a/admin/home.js b/admin/home.js
new file mode 100644
index 0000000..e69de29
diff --git a/fmhub.js b/fmhub.js
index 5f26231..dba83e6 100644
--- a/fmhub.js
+++ b/fmhub.js
@@ -1,69 +1,42 @@
+import interrelated from 'interrelated'
+
export default class {
constructor() {
- this.channels = new Map()
- this.users = new Map()
+ this.用户订阅 = new interrelated()
+ this.用户会话 = new interrelated()
}
订阅频道(fid, uid) {
- console.log(`用户 ${uid} 订阅了 ${fid}`)
- let channel = this.channels.get(fid) || new Map()
- if (!channel.size) this.channels.set(fid, channel)
- channel.set(uid, true)
+ this.用户订阅.set(fid, uid)
}
取消订阅(fid, uid) {
- console.log(`用户 ${uid} 取消订阅 ${fid} 频道`)
- let channel = this.channels.get(fid)
- channel.delete(uid)
- // TODO: 如果此频道没有用户订阅了, 则将此频道也移除
- // TODO: 如果此用户没有任何订阅了, 则将此用户也移除
+ this.用户订阅.delete(fid, uid)
}
增加会话(uid, ws) {
- console.log(`用户 ${uid} 建立了新的会话连接`)
- let user = this.users.get(uid) || new Map()
- if (!user.size) this.users.set(uid, user)
- user.set(ws, true)
+ this.用户会话.set(uid, ws)
}
移除会话(uid, ws) {
- console.log(`用户 ${uid} 结束了当前会话连接`)
- let user = this.users.get(uid) || new Map()
- user.delete(ws)
- console.log(`此用户还剩 ${user.size} 个 ws 连接`)
- if (user.size < 1) {
- console.log(`由于用户 ${uid} 已经没有会话, 直接移除此用户记录`)
- this.users.delete(uid)
- // 理论上在会话结束后移除 (但为了避免反复遍历, 可以放在发送消息时)
- this.channels.forEach((channel, fm) => {
- channel.delete(uid)
- })
- }
+ this.用户会话.delete(uid, ws)
}
发送消息(fm, uid, data) {
- console.log("发送消息", fm, uid, data)
let msg = JSON.stringify({ fm, uid, data })
- let channel = this.channels.get(fm) || new Map()
- if (!channel.size) this.channels.set(fm, channel)
- channel.forEach((value, userid) => {
- //console.log(userid, value)
- let user = this.users.get(userid) || new Map()
- if (!user.size) {
- return console.log(`订阅频道的用户 ${userid} 没有会话连接, 应移除此订阅记录`);
- //console.log(user, "在这里移除可能更安全点, 因为用户可能只是断线, 立即就会重连")
- //console.log("但是, 就不能清除闲置频道占用")
- //return channel.delete(userid)
- }
- user.forEach((value, ws) => {
+ this.用户订阅.atob(fm, (uid) => {
+ //console.log(`用户 ${uid} 订阅的所有频道`)
+ this.用户会话.atob(uid, (ws) => {
+ //console.log(`用户 ${uid} 的会话`)
ws.send(msg)
})
})
+ //console.log(`用户 ${uid} 订阅的所有频道`)
+ //this.用户订阅.aall(uid, (fid) => {
+ // console.log(fid)
+ //})
+ //console.log(`频道 ${fm} 下的所有用户`)
+ //this.用户订阅.ball(fm, (uid) => {
+ // console.log(uid)
+ //})
}
移除用户(uid) {
- console.log(`移除 ${uid} 的所有会话`)
- let user = this.users.get(uid)
- if (user) {
- user.forEach((value, ws) => ws.close()) // 断开用户所有会话
- this.users.delete(uid) // 删除用户(所有会话)
- }
- // TODO: 移除所有频道中的此用户
- // TODO: 移除此用户的记录
- // TODO: 断开所有此用户的连接
+ this.用户订阅.adelete(uid)
+ this.用户会话.adelete(uid)
}
}
diff --git a/index.js b/index.js
index 2d34d90..07010ce 100644
--- a/index.js
+++ b/index.js
@@ -7,9 +7,7 @@ import random from 'string-random'
import formidable from 'formidable'
import md5 from 'md5-node'
import HUB from './fmhub.js'
-//import site from './collection.js'
-const app = expressWs(express()).app
const databases = new Map() // 所有数据库
const FM = new HUB() // 频道消息分发器
@@ -228,6 +226,27 @@ const object_create = async function (req, res) {
req.body.views = 0 // 再生计数
}
+ // 如果包含标签
+ if (req.body.tags && Array.isArray(req.body.tags)) {
+ req.body.tags.forEach(item => {
+ // 先查询是否存在, 存在则使用返回的_id进行挂载, 不存在则创建新的
+ db('tag').findOne({ name: item }, function (err, doc) {
+ if (err && !doc) {
+ return // 创建新的
+ } else {
+ return // 使用这个 _id, 向它写入
+ }
+ })
+ })
+
+ // 是否可以创建一个复杂关系型数据库?
+ // 以应对映射的共同对象
+ // 例如在使用 tag 时, 向 idea 表的 tag 段读写, 即是 tag表的 idea 索引范围
+ // (自动构建和维护双向索引)
+ // 当删除此 idea 时, 也自动清理掉 tag 对 idea 的连接
+
+ }
+
// 如果是挂载对象到指定目标
if (req.body.attach && req.body.aid) {
let count = await count_load(req.body.attach, { _id: req.body.aid })
@@ -247,6 +266,40 @@ const object_create = async function (req, res) {
})
}
+// 修改对象
+const object_patch = function (req, res) {
+ return db(req.params.name).findOne({ _id: req.params._id }, function (err, doc) {
+ if (!doc) return res.status(404).send('目标对象不存在')
+ // 如果是 user 做一些特殊处理
+ if (req.params.name === 'user') {
+ if (req.session.account.uid !== doc._id && req.session.account.gid !== 1) {
+ return res.status(403).send('没有权限修改账户')
+ }
+ if (req.body.gid && req.session.account.gid !== 1) {
+ return res.status(403).send('没有权限修改权限')
+ }
+ if (req.body.password) {
+ req.body.salt = random(32) // 密码加盐
+ req.body.password = md5(req.body.password + req.body.salt) // 设置密码
+ }
+ if (req.body.name) {
+ // 检查用户名是否可用
+ }
+ } else {
+ if (req.session.account.uid !== doc.uid && req.session.account.gid !== 1) {
+ return res.status(403).send('没有权限修改对象')
+ }
+ if (req.body.uid && req.session.account.gid !== 1) {
+ return res.status(403).send('没有权限修改归属')
+ }
+ }
+ return db(req.params.name).update({ _id: req.params._id }, { $set: req.body }, function (err, count) {
+ if (!count) return res.status(500).send('修改失败')
+ return res.send('修改成功')
+ })
+ })
+}
+
// 删除对象
const object_remove = function (req, res) {
return db(req.params.name).findOne({ _id: req.params._id }, async function (err, doc) {
@@ -298,40 +351,6 @@ const object_load = function (req, res) {
})
}
-// 修改对象
-const object_patch = function (req, res) {
- return db(req.params.name).findOne({ _id: req.params._id }, function (err, doc) {
- if (!doc) return res.status(404).send('目标对象不存在')
- // 如果是 user 做一些特殊处理
- if (req.params.name === 'user') {
- if (req.session.account.uid !== doc._id && req.session.account.gid !== 1) {
- return res.status(403).send('没有权限修改账户')
- }
- if (req.body.gid && req.session.account.gid !== 1) {
- return res.status(403).send('没有权限修改权限')
- }
- if (req.body.password) {
- req.body.salt = random(32) // 密码加盐
- req.body.password = md5(req.body.password + req.body.salt) // 设置密码
- }
- if (req.body.name) {
- // 检查用户名是否可用
- }
- } else {
- if (req.session.account.uid !== doc.uid && req.session.account.gid !== 1) {
- return res.status(403).send('没有权限修改对象')
- }
- if (req.body.uid && req.session.account.gid !== 1) {
- return res.status(403).send('没有权限修改归属')
- }
- }
- return db(req.params.name).update({ _id: req.params._id }, { $set: req.body }, function (err, count) {
- if (!count) return res.status(500).send('修改失败')
- return res.send('修改成功')
- })
- })
-}
-
// 附件上传
const file_upload = function (req, res) {
return db(req.params.name).findOne({ _id: req.params._id }, function (err, doc) {
@@ -355,18 +374,55 @@ const file_upload = function (req, res) {
})
}
+// 头像上传
+const uploadavatar = function (req, res) {
+
+ let idable = formidable({
+ multiples: true,
+ uploadDir: 'data/file',
+ keepExtensions: true,
+ maxFieldsSize: 200 * 1024 * 1024,
+ })
+
+ idable.parse(req, (err, fields, files) => {
+
+ let list = []
+ for (let key in files) {
+ (Array.isArray(files[key]) ? files[key] : [files[key]]).map((data) => {
+ let { filepath, mimetype, newFilename, originalFilename, size } = data
+ list.push({ filepath, mimetype, newFilename, originalFilename, size })
+ })
+ }
+
+ if (!list[0]) return res.status(400).send('未获得图像')
+
+ let query = { _id: req.session.account.uid }
+ let data = {
+ $addToSet: { file: { $each: list } }, // 保存记录
+ $set: { avatar: '/data/file/' + list[0].newFilename }, // 替换头像
+ }
+
+ db('user').update(query, data, (err, count) => {
+ if (!count) return res.status(500).send('附件挂载对象失败')
+ res.json(list[0]) // 返回唯一图像
+ })
+
+ })
+}
+
const db_compact = function (req, res) {
db(req.params.name).persistence.compactDatafile()
return res.send("ok")
}
+const app = expressWs(express()).app
app.use(express.json())
app.use(express.urlencoded({ extended: false }))
app.use(session({ secret: 'kana', name: 'sid', resave: false, saveUninitialized: false, cookie: { maxAge: 180 * 24 * 3600000 }, store: session_store }))
app.use('/data/file/', express.static('data/file'))
app.ws('/', websocketer)
app.route('/').get((req, res) => res.send(` Hello World
`))
-app.route('/account').get(profile)
+app.route('/account').get(profile).post(online, uploadavatar)
app.route('/session').get(online, session_list).post(session_create).delete(online, sessionDeleteSelf)
app.route('/session/:sid').delete(online, session_delete)
app.route('/:name').get(object_list).post(object_create).put(db_compact)
diff --git a/package.json b/package.json
index e2bf82a..f092484 100644
--- a/package.json
+++ b/package.json
@@ -12,6 +12,7 @@
"express-session-nedb": "^1.0.1",
"express-ws": "^5.0.2",
"formidable": "^2.0.1",
+ "interrelated": "^2.0.0-0",
"md5-node": "^1.0.1",
"nedb": "^1.8.0",
"string-random": "^0.1.3"
diff --git a/start.sh b/start.sh
index adf6cde..93306fd 100755
--- a/start.sh
+++ b/start.sh
@@ -4,3 +4,19 @@ chmod 777 start.sh
pm2 delete kana
pm2 start node --name kana -- index
+
+
+if [ ! -d "./data/" ];then
+ mkdir ./data
+ chmod 777 ./data
+fi
+
+if [ ! -d "./data/db/" ];then
+ mkdir ./data/db
+ chmod 777 ./data/db
+fi
+
+if [ ! -d "./data/file/" ];then
+ mkdir ./data/file
+ chmod 777 ./data/file
+fi
diff --git a/yarn.lock b/yarn.lock
index 26a142b..f8453b2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -274,6 +274,11 @@ inherits@2.0.4:
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
+interrelated@^2.0.0-0:
+ version "2.0.0-0"
+ resolved "https://registry.yarnpkg.com/interrelated/-/interrelated-2.0.0-0.tgz#5e7798c4035051fa5f53efbf94ea27bf491d0162"
+ integrity sha512-FfLlAsmLBiJRLVdIPhuAiwy6q0QpZIl7fAQTBs4WPAE1/8PdvEpA0OzThvN+3x1pt3bVPYzghTWMhzwrjlfBIw==
+
ipaddr.js@1.9.1:
version "1.9.1"
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"