chore: track docs/readme updates and remove web dist from git
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -5,3 +5,5 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
.env
|
.env
|
||||||
|
data/
|
||||||
|
web/dist/
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ The server listens on `:8080`.
|
|||||||
## Frontend
|
## Frontend
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd vue
|
cd web
|
||||||
npm install
|
npm install
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|||||||
4
doc/iam.md
Normal file
4
doc/iam.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# 数据流
|
||||||
|
1.前端开始注册,关键信息(user,passwd)传入后端接口,执行注册流程(此处注册后不自动登录),密码通过哈希后存数据库(不可逆存储)
|
||||||
|
2.前端登录,关键信息(user,passwd)传入后端接口,后端校验,成功后生成refresh token和access token,将accesstoken对应的session存入redis而不存token,校验由拿到token中获取的sid匹配,refresh token哈希后连带session的其他信息存入数据库,也可以存入redis进行加速读取,并将二者返回给前端,前端收到后,将access token存入localstroage或内存(防止xss), refresh token存入httponly cookie,使用时代credential: include
|
||||||
|
3.业务使用,每次业务http请求前先检查access token的过期时间,如果少于1-5分钟触发刷新token,携带refreshtoken向后端发送请求进行更换token,否则则直接进行业务,若在后端业务开始鉴权时返回401,则前端也要重试refresh,此处refresh的时候可以同时更新refresh token和accesstoken,并且作废旧token,如果refresh token过期,即前端响应退出登录(此处可以模拟7天免登录),此处重试仅一次即可,多个同token refresh请求仅允许一个执行
|
||||||
36
doc/notify.md
Normal file
36
doc/notify.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# 数据流
|
||||||
|
业务触发
|
||||||
|
业务系统(如任务到期、任务创建、状态变更)先产生日志化业务事件。
|
||||||
|
事件带基础字段:event_id、event_type、user_id、occurred_at、payload。
|
||||||
|
|
||||||
|
通知规则判断
|
||||||
|
规则引擎判断这条事件是否需要通知、通知谁、用什么模板。
|
||||||
|
同时做用户偏好过滤(是否订阅、免打扰时段、语言、时区)。
|
||||||
|
|
||||||
|
调度器编排
|
||||||
|
立即通知:直接进入投递流程。
|
||||||
|
定时通知:调度器按 cron/延时规则在目标时间生成通知任务。
|
||||||
|
这一层负责时区换算与批量窗口控制(避免瞬时洪峰)。
|
||||||
|
|
||||||
|
写入 MQ
|
||||||
|
通知任务由生产者写入 MQ(如 notification.jobs)。
|
||||||
|
任务包含:job_id、channel=email、to、template_id、params、trace_id、retry_count 等。
|
||||||
|
|
||||||
|
消费者处理
|
||||||
|
Worker 从 MQ 拉取任务,先做幂等检查(idempotency_key)。
|
||||||
|
如果判定“已发送过”,直接 ACK 丢弃重复任务。
|
||||||
|
否则进入渠道网关(Email Adapter)。
|
||||||
|
|
||||||
|
渠道网关发送(Email)
|
||||||
|
网关将模板渲染后的邮件通过 SMTP/邮件服务商发出。
|
||||||
|
返回成功或失败结果,并带错误码。
|
||||||
|
|
||||||
|
可靠性机制
|
||||||
|
成功:记录 sent 状态并 ACK MQ。
|
||||||
|
可重试失败(超时、限流、临时网络故障):写入重试队列,按指数退避再次投递。
|
||||||
|
不可重试失败(地址非法、模板错误)或超最大重试:进入 DLQ 死信队列,等待人工/自动补偿。
|
||||||
|
|
||||||
|
状态回写与观测
|
||||||
|
每次处理都会回写通知状态(pending/sent/failed/dead)到存储层。
|
||||||
|
同时上报指标和日志:送达率、重试次数、端到端延迟、失败原因分布。
|
||||||
|
告警系统基于阈值触发报警(如失败率突增、队列堆积)。
|
||||||
1
web/dist/assets/LoginView-8H94ytcZ.js
vendored
1
web/dist/assets/LoginView-8H94ytcZ.js
vendored
@@ -1 +0,0 @@
|
|||||||
import{d as y,c,a as e,t as s,u as l,b as a,n as g,e as m,w as v,v as _,f as w,r as p,g as k,h as C,l as V,s as x,i as B,o as b}from"./index-BJLwFBib.js";const N={class:"page login-page"},D={class:"page-head"},S={class:"auth-card"},T={class:"segmented"},U=["disabled"],$={key:0,class:"error"},L=y({__name:"LoginView",setup(z){const f=B(),n=p("login"),r=p(!1),u=p(""),o=k({email:"",password:""});async function h(){r.value=!0,u.value="";try{n.value==="register"&&await C(o);const d=await V(o);x(d.token,o.email),f.push("/todos")}catch{u.value=a("auth_failed")}finally{r.value=!1}}return(d,t)=>(b(),c("section",N,[e("header",D,[e("div",null,[e("h2",null,s(l(a)("login_title")),1),e("p",null,s(l(a)("login_subtitle")),1)])]),e("div",S,[e("h1",null,s(l(a)("brand_name")),1),e("p",null,s(l(a)("login_hint")),1),e("div",T,[e("button",{class:g(["btn",{primary:n.value==="login"}]),onClick:t[0]||(t[0]=i=>n.value="login")},s(l(a)("login_tab")),3),e("button",{class:g(["btn",{primary:n.value==="register"}]),onClick:t[1]||(t[1]=i=>n.value="register")},s(l(a)("register_tab")),3)]),e("label",null,[m(s(l(a)("email"))+" ",1),v(e("input",{"onUpdate:modelValue":t[2]||(t[2]=i=>o.email=i),type:"email",placeholder:"you@company.com"},null,512),[[_,o.email]])]),e("label",null,[m(s(l(a)("password"))+" ",1),v(e("input",{"onUpdate:modelValue":t[3]||(t[3]=i=>o.password=i),type:"password",placeholder:"******"},null,512),[[_,o.password]])]),e("button",{class:"btn primary full",disabled:r.value,onClick:h},s(r.value?l(a)("loading_wait"):n.value==="login"?l(a)("login_tab"):l(a)("register_and_login")),9,U),u.value?(b(),c("p",$,s(u.value),1)):w("",!0)])]))}});export{L as default};
|
|
||||||
1
web/dist/assets/TeamSettingsView-D1JkSdqf.js
vendored
1
web/dist/assets/TeamSettingsView-D1JkSdqf.js
vendored
@@ -1 +0,0 @@
|
|||||||
import{d,c as p,a as e,t as l,u as o,b as n,e as u,w as r,v as i,k as m,g,o as _}from"./index-BJLwFBib.js";const f={class:"page"},b={class:"page-head"},c={class:"btn primary",type:"submit"},v=d({__name:"TeamSettingsView",setup(w){const s=g({teamName:"Core Product Team",workspace:"supertodo",defaultAssignee:"Product Operator",doneRule:"At least 1 review before done"});return(x,t)=>(_(),p("section",f,[e("header",b,[e("h2",null,l(o(n)("team_settings_title")),1),e("p",null,l(o(n)("team_settings_subtitle")),1)]),e("form",{class:"card settings-form",onSubmit:t[4]||(t[4]=m(()=>{},["prevent"]))},[e("label",null,[u(l(o(n)("team_name"))+" ",1),r(e("input",{"onUpdate:modelValue":t[0]||(t[0]=a=>s.teamName=a),type:"text"},null,512),[[i,s.teamName]])]),e("label",null,[u(l(o(n)("workspace_slug"))+" ",1),r(e("input",{"onUpdate:modelValue":t[1]||(t[1]=a=>s.workspace=a),type:"text"},null,512),[[i,s.workspace]])]),e("label",null,[u(l(o(n)("default_assignee"))+" ",1),r(e("input",{"onUpdate:modelValue":t[2]||(t[2]=a=>s.defaultAssignee=a),type:"text"},null,512),[[i,s.defaultAssignee]])]),e("label",null,[u(l(o(n)("completion_rule"))+" ",1),r(e("textarea",{"onUpdate:modelValue":t[3]||(t[3]=a=>s.doneRule=a),rows:"4"},null,512),[[i,s.doneRule]])]),e("button",c,l(o(n)("save_team_settings")),1)],32)]))}});export{v as default};
|
|
||||||
1
web/dist/assets/TeamView-CPjxYYz8.js
vendored
1
web/dist/assets/TeamView-CPjxYYz8.js
vendored
@@ -1 +0,0 @@
|
|||||||
import{d as l,c as o,a as e,t as s,u as a,b as n,F as m,q as _,o as r}from"./index-BJLwFBib.js";const d={class:"page"},i={class:"page-head"},p={class:"cards two-col"},g=l({__name:"TeamView",setup(u){const c=[{id:"core",name:"Core Product Team",members:7,open:12},{id:"growth",name:"Growth Team",members:5,open:8}];return(h,b)=>(r(),o("section",d,[e("header",i,[e("h2",null,s(a(n)("team_entry")),1),e("p",null,s(a(n)("team_subtitle")),1)]),e("div",p,[(r(),o(m,null,_(c,t=>e("article",{key:t.id,class:"card"},[e("h3",null,s(t.name),1),e("p",null,s(a(n)("team_members",{count:t.members})),1),e("p",null,s(a(n)("team_open_todos",{count:t.open})),1)])),64))])]))}});export{g as default};
|
|
||||||
1
web/dist/assets/TodoView-BN3VasB8.css
vendored
1
web/dist/assets/TodoView-BN3VasB8.css
vendored
@@ -1 +0,0 @@
|
|||||||
.filter-group[data-v-23b5de78]{display:flex;gap:.5rem;margin-right:1rem}.empty-state[data-v-23b5de78]{text-align:center;padding:3rem;color:#666;background:#ffffff80;border-radius:8px;grid-column:1 / -1}.btn.sm[data-v-23b5de78]{padding:.25rem .75rem;font-size:.85rem}
|
|
||||||
1
web/dist/assets/TodoView-scAhY5bP.js
vendored
1
web/dist/assets/TodoView-scAhY5bP.js
vendored
File diff suppressed because one or more lines are too long
1
web/dist/assets/UserSettingsView-CDfYoYne.js
vendored
1
web/dist/assets/UserSettingsView-CDfYoYne.js
vendored
@@ -1 +0,0 @@
|
|||||||
import{d as p,g as u,E as d,c as b,a as e,t as i,u as n,b as l,e as o,w as r,v as m,G as c,k as f,o as _}from"./index-BJLwFBib.js";const y={class:"page"},g={class:"page-head"},x={class:"inline"},v={class:"btn primary",type:"submit"},U=p({__name:"UserSettingsView",setup(V){const s=u({displayName:"Product Operator",email:d.email,timezone:"Asia/Shanghai",notifications:!0});return(k,t)=>(_(),b("section",y,[e("header",g,[e("h2",null,i(n(l)("user_settings_title")),1),e("p",null,i(n(l)("user_settings_subtitle")),1)]),e("form",{class:"card settings-form",onSubmit:t[4]||(t[4]=f(()=>{},["prevent"]))},[e("label",null,[o(i(n(l)("display_name"))+" ",1),r(e("input",{"onUpdate:modelValue":t[0]||(t[0]=a=>s.displayName=a),type:"text"},null,512),[[m,s.displayName]])]),e("label",null,[o(i(n(l)("email"))+" ",1),r(e("input",{"onUpdate:modelValue":t[1]||(t[1]=a=>s.email=a),type:"email"},null,512),[[m,s.email]])]),e("label",null,[o(i(n(l)("timezone"))+" ",1),r(e("input",{"onUpdate:modelValue":t[2]||(t[2]=a=>s.timezone=a),type:"text"},null,512),[[m,s.timezone]])]),e("label",x,[r(e("input",{"onUpdate:modelValue":t[3]||(t[3]=a=>s.notifications=a),type:"checkbox"},null,512),[[c,s.notifications]]),o(" "+i(n(l)("reminders")),1)]),e("button",v,i(n(l)("save_settings")),1)],32)]))}});export{U as default};
|
|
||||||
26
web/dist/assets/index-BJLwFBib.js
vendored
26
web/dist/assets/index-BJLwFBib.js
vendored
File diff suppressed because one or more lines are too long
1
web/dist/assets/index-th0r845L.css
vendored
1
web/dist/assets/index-th0r845L.css
vendored
File diff suppressed because one or more lines are too long
13
web/dist/index.html
vendored
13
web/dist/index.html
vendored
@@ -1,13 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>SuperTodo</title>
|
|
||||||
<script type="module" crossorigin src="/assets/index-BJLwFBib.js"></script>
|
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-th0r845L.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="app"></div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
Reference in New Issue
Block a user