chore: track docs/readme updates and remove web dist from git

This commit is contained in:
2026-03-01 22:31:31 +08:00
parent 7e97aaa7fc
commit dbfec3645b
13 changed files with 43 additions and 47 deletions

2
.gitignore vendored
View File

@@ -5,3 +5,5 @@
.DS_Store .DS_Store
.env .env
data/
web/dist/

View File

@@ -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
View File

@@ -0,0 +1,4 @@
# 数据流
1.前端开始注册关键信息userpasswd传入后端接口执行注册流程此处注册后不自动登录密码通过哈希后存数据库不可逆存储
2.前端登录关键信息userpasswd传入后端接口后端校验成功后生成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
View 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到存储层。
同时上报指标和日志:送达率、重试次数、端到端延迟、失败原因分布。
告警系统基于阈值触发报警(如失败率突增、队列堆积)。

View File

@@ -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};

View File

@@ -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};

View File

@@ -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};

View File

@@ -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}

File diff suppressed because one or more lines are too long

View File

@@ -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};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

13
web/dist/index.html vendored
View File

@@ -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>