Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions database/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,88 @@ _一般来说,一个实体对应一张表,多对多的关系也可对应一
| | created_at | timestamp | | |

注:由于使用的是 PostgreSQL,其`text`类型指长度可变的字符串,与其他数据库可能不同([PostgreSQL: Documentation: 16: Chapter 8. Data Types](https://www.postgresql.org/docs/current/datatype.html))

---

## 作业补充:会议便签纸(选项 C)

> 需求:用户可以为每个会议创建一张便签纸,便签纸仅创建者本人可见。

### 1. 分析需求

在原有「用户注册、登录 / 会议创建、加入 / 会议中的聊天室」基础上新增:

- 用户在某会议中拥有一张自己的便签纸,可在上面记录要点与待办
- 同一用户在同一会议只有一张便签纸;不同用户的便签互相不可见

### 2.1 标识实体

- 用户(用户名、密码)
- 会议(名称、介绍、邀请码、创建时间)
- **便签(内容、创建时间、修改时间)** ← 新增

### 2.2 标识关系

- 用户加入会议(用户——会议)
- 用户在会议中发出消息(用户——会议——消息)
- **用户在某会议拥有便签纸(用户——会议——便签)** ← 新增
- “每个会议一张”意味着约束 `(user_uuid, room_uuid)` 唯一

### 2.3 E-R 图

```mermaid
erDiagram
user{
String username
String password
}
room{
String name
String intro
String invite_code
Time created_at
}
message{
String content
Time created_at
}
note{
String content
Time created_at
Time updated_at
}
user }|--o{ room : join
user ||--o{ message : send
room ||--o{ message : contain
user ||--o{ note : own
room ||--o{ note : contain
```

### 3.1 新增表设计

| 表 | 字段 | 数据类型 | 主键 | 外键 | 约束 |
| ---- | ---------- | --------- | ---- | --------- | --------------------------------------------- |
| note | id | uuid | 是 | | 默认 `gen_random_uuid()` |
| | user_uuid | uuid | | user.uuid | 级联删除/更新;`(user_uuid, room_uuid)` 唯一 |
| | room_uuid | uuid | | room.uuid | 级联删除/更新 |
| | content | text | | | |
| | created_at | timestamp | | | 默认当前时间 |
| | updated_at | timestamp | | | 默认当前时间,更新时自动刷新 |

### 4. 仅自己可见的两种保障

1. 数据库层:便签行包含 `user_uuid`(属主),查询与修改都带上属主条件;
2. API 层(Hasura):`user` 角色的 `insert/select/update/delete` 权限均绑定会话变量
`X-Hasura-User-Id`,且 `insert` 使用 `set: { user_uuid: X-Hasura-User-Id }`,
客户端无法伪造属主。配置文件见
`database/hasura/metadata/databases/workshop/tables/public_note.yaml`。

### 5. 落地文件

| 文件 | 说明 |
| ---- | ---- |
| `database/sql/note.sql` | PostgreSQL 版建表 + 触发器 + 示例数据(Hasura/Neon 环境执行) |
| `database/sql/note_mysql.sql` | 同设计的 MySQL 9.x 版(本机实测通过) |
| `database/graphql/note.graphql` | 创建/查询/修改/删除便签的 GraphQL 操作 |
| `database/hasura/metadata/databases/workshop/tables/public_note.yaml` | note 表的 Hasura 元数据与权限 |

53 changes: 53 additions & 0 deletions database/graphql/note.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# 便签纸由 Hasura 的 insert set 自动写入当前登录用户(见 public_note.yaml),
# 客户端无需也不能传 user_uuid,因此仅需要 room_uuid 与 content。

mutation createNote($room_uuid: uuid!, $content: String!) {
insert_note_one(object: {room_uuid: $room_uuid, content: $content}) {
id
room_uuid
content
created_at
updated_at
}
}

query getMyNoteInRoom($room_uuid: uuid!) {
note(where: {room_uuid: {_eq: $room_uuid}}) {
id
content
created_at
updated_at
room {
uuid
name
}
}
}

query getMyNotes {
note(order_by: {updated_at: desc}) {
id
room_uuid
content
created_at
updated_at
room {
uuid
name
}
}
}

mutation updateNote($id: uuid!, $content: String!) {
update_note_by_pk(pk_columns: {id: $id}, _set: {content: $content}) {
id
content
updated_at
}
}

mutation deleteNote($id: uuid!) {
delete_note_by_pk(id: $id) {
id
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
table:
name: note
schema: public
object_relationships:
- name: room
using:
foreign_key_constraint_on: room_uuid
- name: user
using:
foreign_key_constraint_on: user_uuid
insert_permissions:
- role: user
permission:
check: {}
set:
user_uuid: X-Hasura-User-Id
columns:
- content
- room_uuid
comment: 便签仅能作为当前登录用户创建(user_uuid 由会话变量写入)
select_permissions:
- role: user
permission:
columns:
- content
- created_at
- updated_at
- id
- room_uuid
- user_uuid
filter:
user_uuid:
_eq: X-Hasura-User-Id
comment: 只能看到自己创建的便签(仅自己可见)
update_permissions:
- role: user
permission:
columns:
- content
filter:
user_uuid:
_eq: X-Hasura-User-Id
check:
user_uuid:
_eq: X-Hasura-User-Id
comment: 只能修改自己便签的内容
delete_permissions:
- role: user
permission:
filter:
user_uuid:
_eq: X-Hasura-User-Id
comment: 只能删除自己的便签
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
- "!include public_message.yaml"
- "!include public_note.yaml"
- "!include public_room.yaml"
- "!include public_user.yaml"
- "!include public_user_room.yaml"
38 changes: 38 additions & 0 deletions database/sql/note.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
-- PostgreSQL
-- 第11讲作业(选项 C:会议便签纸)
-- 需求:用户可以为每个会议创建一张便签纸,便签纸仅自己可见。
-- * 每人每会议最多一张:由 UNIQUE(user_uuid, room_uuid) 保证
-- * 仅创建者本人可见:由 Hasura 权限层(session 变量 X-Hasura-User-Id)
-- 控制,见 database/hasura/metadata/databases/workshop/tables/public_note.yaml
-- 本文件依赖 user.sql、room.sql 已先执行(外键引用 user、room)。
create table if not exists public.note (
id uuid default gen_random_uuid() not null,
user_uuid uuid not null,
room_uuid uuid not null,
content text not null,
created_at timestamp default current_timestamp not null,
updated_at timestamp default current_timestamp not null,
primary key (id),
unique (user_uuid, room_uuid)
);
alter table public.note
add constraint note_user_uuid_fkey foreign key (user_uuid) references public.user (uuid) on update cascade on delete cascade;
alter table public.note
add constraint note_room_uuid_fkey foreign key (room_uuid) references public.room (uuid) on update cascade on delete cascade;

-- 触发器:修改便签内容时自动刷新 updated_at
create or replace function public.note_set_updated_at() returns trigger language plpgsql as $$
begin
new.updated_at := current_timestamp;
return new;
end;
$$;
create trigger trg_note_set_updated_at before update on public.note
for each row execute function public.note_set_updated_at();

insert into public.note (user_uuid, room_uuid, content) values
('00000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-100000000001', '大家好,我是张三!这是咱们房间的便签纸。待办:今天 15:00 讨论暑培答辩分组。'),
('00000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-100000000001', '收到!我来准备演示用的投影和电脑。'),
('00000000-0000-0000-0000-000000000003', '00000000-0000-0000-0000-100000000001', '(王五)我负责记录会议纪要~'),
('00000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-100000000002', '(张三在聊天室1的私人便签)别忘了下周还书。'),
('00000000-0000-0000-0000-000000000004', '00000000-0000-0000-0000-100000000003', '(赵六)成语接龙战绩存档:心安理得→得过且过→过目不忘。');
80 changes: 80 additions & 0 deletions database/sql/note_mysql.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
-- MySQL(9.7 实测可用)——第11讲作业(选项 C:会议便签纸)的 MySQL 版本
-- 官方讲解环境为 PostgreSQL + Hasura,请优先使用同目录 user.sql、room.sql、note.sql;
-- 本文件把同一套设计翻译为 MySQL 语法,可在本机 MySQL 直接跑通并验证(已实测)。
-- 注意:MySQL 9.x 已移除 md5(),故密码示例改用 sha2(),仅作占位演示。

SET NAMES utf8mb4;

CREATE DATABASE IF NOT EXISTS web_workshop DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
USE web_workshop;

-- 1. user(同 user.sql)
CREATE TABLE IF NOT EXISTS `user` (
uuid CHAR(36) NOT NULL,
username VARCHAR(64) NOT NULL,
password VARCHAR(64) NOT NULL,
PRIMARY KEY (uuid),
UNIQUE KEY uk_user_username (username)
) ENGINE = InnoDB;

INSERT INTO `user` (uuid, username, password) VALUES
('00000000-0000-0000-0000-000000000000', 'admin', sha2('123456', 256)),
('00000000-0000-0000-0000-000000000001', '张三', sha2('张三', 256)),
('00000000-0000-0000-0000-000000000002', '李四', sha2('李四', 256)),
('00000000-0000-0000-0000-000000000003', '王五', sha2('王五', 256)),
('00000000-0000-0000-0000-000000000004', '赵六', sha2('赵六', 256));

-- 2. room(同 room.sql)
CREATE TABLE IF NOT EXISTS room (
uuid CHAR(36) NOT NULL,
name VARCHAR(64) NOT NULL,
intro VARCHAR(255) NOT NULL,
invite_code CHAR(6) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (uuid),
UNIQUE KEY uk_room_name (name),
UNIQUE KEY uk_room_invite_code (invite_code)
) ENGINE = InnoDB;

INSERT INTO room (uuid, name, intro, invite_code) VALUES
('00000000-0000-0000-0000-100000000001', '公共聊天室', '欢迎加入公共聊天室', 'gD9jE4'),
('00000000-0000-0000-0000-100000000002', '聊天室1', '这是一个聊天室', 'oC3kY5'),
('00000000-0000-0000-0000-100000000003', '聊天室2', '这是一个聊天室', 'uE8aY9'),
('00000000-0000-0000-0000-100000000004', '聊天室3', '这是一个聊天室', 'aF2jR6');

-- 3. note:会议便签纸(本讲新增表)
-- * UNIQUE(user_uuid, room_uuid):每个用户在每个会议最多一张便签纸
-- * 外键级联:用户或会议被删除时其便签随之删除
-- * updated_at 在修改行时自动刷新(等价于 note.sql 中的触发器)
CREATE TABLE IF NOT EXISTS note (
id CHAR(36) NOT NULL DEFAULT (UUID()),
user_uuid CHAR(36) NOT NULL,
room_uuid CHAR(36) NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_note_user_room (user_uuid, room_uuid),
KEY idx_note_room (room_uuid),
CONSTRAINT fk_note_user FOREIGN KEY (user_uuid) REFERENCES `user` (uuid)
ON UPDATE CASCADE ON DELETE CASCADE,
CONSTRAINT fk_note_room FOREIGN KEY (room_uuid) REFERENCES room (uuid)
ON UPDATE CASCADE ON DELETE CASCADE
) ENGINE = InnoDB;

INSERT INTO note (user_uuid, room_uuid, content) VALUES
('00000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-100000000001', '大家好,我是张三!这是咱们房间的便签纸。待办:今天 15:00 讨论暑培答辩分组。'),
('00000000-0000-0000-0000-000000000002', '00000000-0000-0000-0000-100000000001', '收到!我来准备演示用的投影和电脑。'),
('00000000-0000-0000-0000-000000000003', '00000000-0000-0000-0000-100000000001', '(王五)我负责记录会议纪要~'),
('00000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-100000000002', '(张三在聊天室1的私人便签)别忘了下周还书。'),
('00000000-0000-0000-0000-000000000004', '00000000-0000-0000-0000-100000000003', '(赵六)成语接龙战绩存档:心安理得→得过且过→过目不忘。');

-- ================= 常见验证查询 =================
-- 1) 某人某会议的便签(Hasura 中相当于 user_uuid 被 X-Hasura-User-Id 锁定)
-- SELECT r.name, u.username, n.content, n.updated_at
-- FROM note n JOIN room r ON n.room_uuid = r.uuid JOIN `user` u ON n.user_uuid = u.uuid
-- WHERE n.user_uuid = '00000000-0000-0000-0000-000000000001'
-- AND n.room_uuid = '00000000-0000-0000-0000-100000000001';
-- 2) 同一个人在另一房间再插一张应报 ERROR 1062(唯一约束生效)
-- INSERT INTO note (user_uuid, room_uuid, content) VALUES
-- ('00000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-100000000001', '重复便签,应失败');
Loading