275 Commits

Author SHA1 Message Date
becomer 51f3641a0c v1.2.112:重构首页置顶公告;打卡日志改时间线、任务行精修
ci/woodpecker/tag/build Pipeline was successful
- 置顶公告新组件 PinnedNotice:一次一条上下翻页轮播(N/M 计数),长文本先停顿再横向滚到末尾,
  点击打开底部面板查看全部全文;切后台 / 减弱动态效果时暂停;柠檬暖色卡片,亮暗各一套配色
- 打卡日志:状态圆点 + 竖线时间线,相对时间(刚刚 / N 分钟前 / 今天 / 昨天),失败原因直接展示,空状态带说明
- 任务行:左侧状态色条,执行按钮改品牌渐变圆形主按钮,编辑/删除降级为图标按钮
- 贴边竖条(收藏本站 / 无法请销假 / 网课)位置统一上移避开引导入口;收藏本站改柠檬底深棕字
- 修复 scoped 样式中 `:global(.dark) .x` 被编译为仅 `.dark` 导致暗色覆盖失效(请销假头卡暗色此前未生效)
v1.2.112
2026-10-02 19:38:36 +08:00
becomer 9f2f705457 v1.2.111:修复线上暗色主题全站灰底;重做外观切换控件
ci/woodpecker/tag/build Pipeline was successful
- 根因:postbuild-css 解包 :where() 时直接去壳,Tailwind 暗色变体 `.x:where(.dark, .dark *)` 被拆成
  `.x.dark, .dark *`,`.dark *` 成为全局规则,暗色下所有元素被刷上同一背景/边框(开发服务器不经后处理,故本地截图正常)。
  改为先展开逗号列表(`.dark .x`),并在产物中出现全局 `.dark *` 规则时直接构建失败
- 外观切换改为整宽三段分段控件 + 滑动高亮,窄屏不再挤压换行
- 我的页对比度:续费 VIP / 兑换按钮改固定深棕字(≥4.6:1),奖励数字改 amber-600
v1.2.111
2026-10-02 15:21:16 +08:00
becomer 301a4fb971 v1.2.110:打卡/请销假页头像行上下间距统一为 16px(原上 12px、下 28px)
ci/woodpecker/tag/build Pipeline was successful
v1.2.110
2026-10-02 14:31:36 +08:00
becomer df40b2ae73 v1.2.109:顶部安全区仅在主屏幕全屏模式生效,修复浏览器/App 内顶栏下移
ci/woodpecker/tag/build Pipeline was successful
部分浏览器与 App 内置 WebView 已把网页放在状态栏下方,却仍上报 safe-area-inset-top,
叠加后头像行凭空下移约 47px。改为 --safe-top 变量,仅 display-mode: standalone/fullscreen 时取 env() 值。
v1.2.109
2026-10-02 14:13:41 +08:00
becomer 317bf4b5a1 v1.2.108:修复新手引导末步页面左移;新版本提示更及时
ci/woodpecker/tag/build Pipeline was successful
- 引导定位 scrollIntoView 改为 inline:nearest,不再横向滚动 overflow:hidden 的头卡容器(装饰光斑超出边界导致整块左移 45px);头卡改用 overflow:clip 兜底
- UpdateChecker:检查间隔 5 分钟→2 分钟、首检 30s→10s;页面回到前台(切回标签/唤起 App)时补查;修正 prompting 在 finally 中被清零导致的并发防护失效
v1.2.108
2026-10-02 13:59:40 +08:00
becomer 1c112fe3dc v1.2.107:修复打卡页横向撑宽、底部导航滚动时缩放抖动
ci/woodpecker/tag/build Pipeline was successful
- 页面包裹层 grid 列宽锁定为容器宽度(grid-cols-1),置顶公告走马灯不再把整页撑到数千像素宽
- TabBar 收窄逻辑防抖:可滚动距离 <200px 的页面不收窄;scrollTop 夹在 [0,max] 忽略 iOS 回弹;同向滚动超过 32px 才切换;到底部不再强制展开
v1.2.107
2026-10-02 13:47:41 +08:00
becomer 2f7113ba5e v1.2.106:前端改版——薄荷汽水配色 + 轻量液态玻璃(iOS 26 风格)
ci/woodpecker/tag/build Pipeline was successful
- 设计令牌:重映射 Tailwind 色阶为薄荷汽水色板(品牌薄荷青/天空蓝/柠檬/珊瑚),统一圆角、柔和阴影;亮暗两套色板按 WCAG AA 校验
- 外壳:悬浮液态玻璃胶囊 TabBar(滑动高亮、滚动收窄)、透明→玻璃顶栏、切页淡入、居中玻璃 Toast
- 主题:新增「自动/浅色/深色」三档切换(我的页),自动模式保留按时间切换
- 页面:首页/请销假渐变头卡、登录/绑定设备/工单/个人中心精修;后台换主色与导航样式
- 可读性:清理 8–11px 小字号(最小 12px),提高辅助文字灰度对比
- 降级:不支持 backdrop-filter / 减弱透明度 → 实底;减弱动态效果 → 关闭动画
v1.2.106
2026-10-02 13:33:46 +08:00
becomer 441c40a8cc v1.2.105:封号(禁用)后所有接口即时拦截,返回自定义封号原因
ci/woodpecker/tag/build Pipeline was successful
- 现状:禁用仅在登录时校验,已登录用户持有效 token 仍可调用所有接口。
- 修复:JWT 中间件在每个受保护接口校验用户状态,禁用则返回 HTTP 403 + 业务码 4030,
  message 为封号时自定义的原因(为空时兜底文案)。
- 前端:拦截 403/4030,清除登录态、弹出封号原因并跳登录页,不触发 token 刷新死循环。
- 复用现有后台「禁用/启用」入口与 disabled_reason(用户管理页已具备)。
v1.2.105
2026-10-02 11:09:29 +08:00
becomer 002c6b8570 v1.2.104:放开「今日已签到」拦截,允许对已签到实例重新提交
ci/woodpecker/tag/build Pipeline was successful
- 现象:用户想对已签到的任务重新打卡(如首次位置填错),被后台以
  「今日已签到,无需重复执行」拦截(该提示由 FetchTodayTaskDetail 本地判断返回,
  非今日校园)。
- 修复:执行打卡时若当天无未签到实例但存在已签到实例,不再拦截,改用该已签到实例
  重新提交;是否真正接受由今日校园服务端决定(实测已能发出请求,受学校侧风控约束)。
v1.2.104
2026-10-02 10:49:11 +08:00
becomer 2a53ab7041 v1.2.103:修复范围外签到被强制改回学校坐标(打卡地点显示成学校)
ci/woodpecker/tag/build Pipeline was successful
- 现象:用户 2025050021(安康学院)签到任务填的是家(广东省广州市天河区大观路,
  且「是否范围外签到=是」),执行后实际打卡地点变成学校(安康学院)。
- 根因:执行打卡前无条件调用 AdjustSubmitFormToSignRange——当填写坐标不在任何
  学校签到范围内时,自动把坐标+地址改成最近的学校签到点。用户本就是「范围外签到」,
  被强行改回学校坐标,导致实际打卡点变成学校。
- 修复:仅在「范围内签到」时执行坐标自动回退;「范围外签到」(isMalposition=是/1)
  尊重用户填写的位置,不调整。新增 isOutOfRangeSign 判定与单测。
v1.2.103
2026-10-02 10:24:10 +08:00
becomer 12db9217d1 v1.2.102:修复兰州理工大学登录(CAS-PaaS 定制 CAS,密码 RSA/base64 加密)
ci/woodpecker/tag/build Pipeline was successful
- 现象:用户 262030500019 登录报「当前学校已有用户正常登录,请检查…」,
  实际是账密登录失败(IAP doLogin 报 FAIL_UPNOTMATCH)。
- 根因:该校平台标 joinType=CLOUD(IsIap),但实际为 CAS-PaaS 定制统一身份认证
  (cas-paas.lut.edu.cn/cas/login),密码需 RSA(JSEncrypt)加密后以
  "__RSA__"+密文 提交;通用 casLogin 提交明文密码被拒。
  且 JSEncrypt 的密文是 base64,而本仓库 RSA 工具输出 hex,直接提交亦不通过。
- 修复:
  1) 新增 CAS-PaaS 适配(login_caspaas.go):按登录页特征(/cas/jwt/publicKey、
     __RSA__)自动识别,拉公钥、RSA(PKCS#1 v1.5) 加密密码并转 base64,前缀 "__RSA__"。
  2) casLogin 解析表单后自动命中 CAS-PaaS 加密。
  3) 新增兰州理工特判(login_lut.go)强制走 CAS(其 IAP 不可用)。
- 实测:真实账号 262030500019 登录成功,拿到 MOD_AUTH_CAS/CASTGC。
v1.2.102
2026-09-30 23:52:25 +08:00
becomer bd274a0414 v1.2.101:销假错误透传学校原始 message,修正 2210990000 被误译为「需上传附件」
ci/woodpecker/tag/build Pipeline was successful
- 现象:用户 3241804227(旧套)已上传附件仍报「需要上传附件」。
- 根因:学校端错误码 2210990000 是通用码,可表示多种原因(实测:1161 为
  「需要添加附件才能完成销假」,1219 为「设备更换,无法查看,请联系管理老师」);
  原实现把所有 2210990000 一律硬编码为「该请假销假需要上传附件图片」,覆盖了
  学校真实 message,导致「设备更换」被误报成「需上传附件」。
- 修复:优先透传学校返回的 message;仅当 message 为空/failed 时按码兜底。
v1.2.101
2026-09-30 15:46:28 +08:00
becomer ad9dd3e638 v1.2.100:验证码限流 IP 改用 utils.ClientIP,与登录 IP 取值一致
ci/woodpecker/tag/build Pipeline was successful
短信验证码与图片验证码的限流此前用 c.IP(),与登录 IP 记录所用的 utils.ClientIP 不一致,
导致限流按错误的 IP 计数(可能所有用户共用一个桶,出现"刚打开就提示需等 15 秒")。
统一改用 utils.ClientIP(c)。
v1.2.100
2026-09-29 22:22:47 +08:00
becomer 0a49dc0080 v1.2.99:修复新套请假「是否离校」被类型默认值覆盖导致提交成「否」
ci/woodpecker/tag/build Pipeline was successful
- 根因:新套 leaveType.extendInfoBean.isLeaveOut 官方语义是「是否离校」的初始默认值
  (0=无默认 / 1=默认是 / 2=默认否),并非强制开关;且提交体 leaveOutInfo.isLeaveOut
  学校端只认 1=是 / 2=否。原实现把它当强制值,当类型默认值=0 时把用户选择的「是」
  覆盖成 0,学校端因 0 非合法枚举而显示为「否」(兰州交通大学 12261216 即此现象)。
- 修复:提交值以用户选择为准并规范为枚举 1/2(leaveNewOutStatusValue);不再用类型默认值
  覆盖用户选择;类型列表不再把 isLeaveOut 误映射为 mustOut,改为透传 OutStatusDefault
  供前端设置开关初始状态(1=默认是,其余=默认否,用户可改)。
- 新增单测锁定枚举映射。
v1.2.99
2026-09-29 21:50:23 +08:00
becomer 48926ed72f v1.2.98:新手引导步骤调整(设备码前置 + 填写邀请码/兑换积分拆为两步)
ci/woodpecker/tag/build Pipeline was successful
- 前两步对调:第 1 步改为「先绑定设备码」,第 2 步拆分为两步——
  「填写邀请码」与「兑换积分」,其后依次为新建任务、执行签到、推广赚积分、VIP。
- 同步更新按步骤索引的硬编码逻辑(示例卡 mock 场景 2/3 → 3/4)。
- GUIDE_VERSION v3→v4(前后端),让老用户重新看到新引导。
v1.2.98
2026-09-29 11:57:44 +08:00
becomer c1573f77ac v1.2.97:邀请逻辑整改(登录去邀请码 + 推广信息合并 + 填邀请码绑定)
ci/woodpecker/tag/build Pipeline was successful
1. 登录不再需要邀请码:登录页移除邀请码展示/URL ?invite= 传参;登录/注册不再携带
   邀请关系;取消「直接注册奖励(register_reward)」。新用户注册不发放任何积分。
2. 底部导航「个人中心」→「我的」。
3. 个人中心将「邀请链接」「邀请信息」合并为「推广信息」卡片:
   - 左:邀请码 + 复制按钮(改为只复制邀请码)
   - 右:奖励积分 + 兑换按钮
   - 下方:邀请好友即可获得 x 积分
   - 无邀请人用户显示「填写邀请码」→ 弹窗输入 → 提交校验设备码(未绑定提示
     「请先绑定设备码」)→ 绑定邀请人并给双方发积分(每人仅可绑定一次)。
4. 后端新增 POST /auth/bind-invite;GetMyInviteInfo/Generate 返回 hasInviter。
5. 新手引导文案与锚点更新(去掉登录携码/链接语义),GUIDE_VERSION v2→v3。
6. 后台设置移除「直接注册奖励积分」项。
v1.2.97
2026-09-29 11:40:31 +08:00
becomer d588d10c4c v1.2.96:底部导航移除网课 + 「可执行次数」文案 + 置顶公告满宽右→左无缝滚动
ci/woodpecker/tag/build Pipeline was successful
1. 底部导航栏取消「网课」tab。
2. 个人中心「可签到次数」改为「可执行次数」(VIP 文案同步为「VIP 执行次数」)。
3. 置顶公告:去掉 warning 边框、整行满宽;改为右→左循环滚动,渲染两组内容
   translateX(-50%) 无缝接回开头,单条也持续滚动。
v1.2.96
2026-09-29 10:39:38 +08:00
becomer 7dac485769 v1.2.95:兑换弹窗验证码错误改为在验证码行提示,并在失败后自动刷新验证码
ci/woodpecker/tag/build Pipeline was successful
- 问题:CDK 兑换/VIP 续费弹窗中,验证码错误或过期时错误红字显示在「兑换码」框下方,
  让用户误以为是兑换码出错。
- 修复:验证码类错误(message 含「验证码」)改为在「验证码」输入行下方红字提示,
  兑换码框不再误标红;验证码错误后自动刷新验证码并清空输入,便于直接重试。
v1.2.95
2026-09-29 10:22:26 +08:00
becomer 195702c14f v1.2.94:修复销假图片上传入口被 enableAttach 隐藏导致无法上传附件
ci/woodpecker/tag/build Pipeline was successful
- 现象:用户 25074105001(吉林医药学院,旧套)销假时表单只显示经纬度/地址,
  提交报「该请假销假需要上传图片附件」,无法完成销假。
- 根因:旧套 leaveType.enableAttach 字段不可靠(多所旧套学校恒为 0),
  前端以此字段控制销假图片上传入口显隐,导致该字段为 0 时上传入口不显示;
  而部分学校销假实际强制要求附件(提交返回 2210990000)。
- 修复:销假图片上传入口改为始终显示(可选,不填则不携带),
  并附提示「部分学校销假要求上传,如提交提示需要附件请在此上传」。
v1.2.94
2026-09-28 15:26:42 +08:00
becomer df550bf2ac v1.2.93:工单未读回复徽标 + 公告面板未关闭工单通知排序靠前
ci/woodpecker/tag/build Pipeline was successful
1. 个人中心「工单与反馈」入口新增未读回复条数徽标(工单通知未读数)。
2. 右上角公告(铃铛)面板:工单通知分区把「未关闭工单」的通知排到前面
   (后端 ListTicketNotifications 附带 ticketStatus,前端按 status≠closed 优先排序)。
v1.2.93
2026-09-28 15:21:00 +08:00
becomer 75a642f3a2 v1.2.92:兑换签到次数弹窗增加「邀请积分可免费兑换」引导提示
ci/woodpecker/tag/build Pipeline was successful
当邀请奖励积分满 1(canRedeem,可免费兑换)时,在「兑换签到次数」弹窗内
提示用户关闭本弹窗,改用下方「邀请信息」处的兑换按钮用积分兑换(无需兑换码)。
v1.2.92
2026-09-28 11:32:07 +08:00
becomer d2ca858816 v1.2.91:修复请假审批人获取失败被静默吞掉导致误报「未配置审批人」
ci/woodpecker/tag/build Pipeline was successful
- 现象:用户 124509440(西藏民族大学)提交请假时提示「未配置审批人」,
  实际学校有 2 个审批人(谷一墨、黄操)。经 headless 浏览器复现,根因是:
  当请假起止时间填写有误(如开始≥结束)时,后端 getApproveFlowList 返回
  「结束时间要晚于开始时间」,前端 loadApplyApprovers 的 catch 静默清空候选,
  界面遂显示「该校该请假类型未配置审批人,可不选」,误导用户以为接口取不到人。
- 修复:捕获并展示真实错误(applyApproversError),该错误时不再显示
  「未配置审批人」文案;切换类型/时间时重置错误态。
- 说明:后端各接口实测正常(对多种类型/时间均返回 2 个审批人),本次仅改前端提示。
v1.2.91
2026-09-28 11:23:15 +08:00
becomer 84f746fa2d v1.2.90:后台用户管理执行次数弹窗补全「返校登记」类型中文
ci/woodpecker/tag/build Pipeline was successful
taskTypeLabel 未覆盖 return_register,导致该类型日志显示英文原名,
补上「返校登记」中文映射。
v1.2.90
2026-09-27 18:25:20 +08:00
becomer 7f81d54ea2 v1.2.89:返校登记接入打卡任务体系(按 taskType 分流)
ci/woodpecker/tag/build Pipeline was successful
- 新增任务类型 return_register,与签到/查寝共用统一 task 接口
  (titles/create/list/form/update/delete/execute/logs),按 taskType 走不同逻辑。
- 后端:
  - TaskType 增加 return_register;FetchAllRecentTasks 聚合返校登记待办为任务标题
    (SignWid="taskWid:stuWid")。
  - ListTasks 对 return_register 按待办集合判定 matched_unsigned/matched_signed。
  - GetTaskForm 无形表单(返回空);ExecuteTask 分流到 executeReturnRegisterTask
    (取配置校区坐标提交确认到达,扣打卡次数、VIP 免扣、写日志,SSE 体验一致)。
  - 移除 v1.2.88 的 /leave/return-register/* 路由与 handler。
- 前端:打卡页(HomeView)接入——标题列表出现返校登记、选中即创建(跳过名字匹配校验、
  无形表单直接保存)、列表/日志显示"返校登记"标签(琥珀色)、执行复用同一 SSE。
  移除请销假页的返校登记卡片与 API 封装。
v1.2.89
2026-09-27 18:06:31 +08:00
becomer a802912a55 v1.2.88:新增节假日离返校「返校登记」(确认到达)
ci/woodpecker/tag/build Pipeline was successful
- 背景:部分学校(如合肥大学)在「节假日离返校」应用要求假期结束做返校登记,
  原系统只支持 信息收集/签到/查寝 等,无法处理该类型。
- 通过应用大厅 getAppInfos 枚举定位到 wec-counselor-leave-return-apps 应用;
  实测该校接口:queryReturnConfigList(校区坐标+半径)、queryStuRegisterList(列表)、
  submitStuReturnRegisterInfo(提交返校位置)。
- 待办判定:status=="1" 且 returnRegisterInfo 为空;提交参数
  {taskWid,stuWid,returnAddress,returnLongitude,returnLatitude,returnSignType}。
- 后端:leave_return.go 服务 + handler + 路由 /leave/return-register/{pending,execute}
  (默认用学校配置校区坐标,范围内登记;支持范围外带原因)。
- 前端:请销假页展示「返校登记」卡片,有则显示、一键提交。
v1.2.88
2026-09-27 17:31:24 +08:00
becomer 9074118889 v1.2.87:修复西藏民族大学登录(定制 CAS,教科书 RSA 小端 hex 密码)
ci/woodpecker/tag/build Pipeline was successful
- 该校平台标记 joinType=CLOUD(IsIap=true),但网关 IAP 账密接口不可用,
  提交报 FAIL_UPNOTMATCH("用户名或密码不匹配"),误导为密码错误。
- 实测真实链路:/iap 302 → cas.xzmu.edu.cn/lyuapServer/login(定制 CAS)→
  表单 POST /lyuapServer/v1/tickets(密码加密方式特殊)→ 得 TGT/ST →
  回调 /iap/loginSuccess?sessionToken=...&ticket=ST 建立网关会话(MOD_AUTH_CAS)。
- 关键差异:密码加密为该站前端库的"教科书式 RSA(无填充)+ 小端序零填充 + hex",
  与通用 wisedu 的 PKCS#1 v1.5 完全不同(用 PKCS#1 会被解成乱码 → PASSERROR)。
- 新增 login_xzmy.go 单独适配该校(真实账号 124509440 实测登录成功),
  并在 CpdailyLogin 分派;含加密/票据解析/错误码翻译单测。
v1.2.87
2026-09-27 14:12:24 +08:00
becomer 176c43251c v1.2.86:新套请假支持手写承诺书签名(isSign 类型)
ci/woodpecker/tag/build Pipeline was successful
- 背景:部分新套学校(如成都职业技术学院)请假要求先手写《请假安全承诺书》
  并上传签名图,缺失时服务端报「手写承诺书不能为空」。
- 字段来源:抓取官方 H5(prod v0.0.279)SafetyLetter/Apply chunk 反查确认——
  签名图经 OBS getUploadPolicy 上传后,作为 attachs 中
  {attachType:1,attachUrl,attachSize,attachName:"学生手写承诺书",obsKey,isStuSign:true} 提交。
- 后端:类型列表透传 requireSign(queryLeaveType.isSign);提交表单支持
  signatureUrl 生成签名 attach;提交前复核未签名直接拦截。
- 前端:选中 requireSign 类型即弹出 canvas 手写签名板,签名图上传 OBS 后随提交携带。
v1.2.86
2026-09-26 18:04:22 +08:00
becomer 839637b3d3 v1.2.85:置顶公告改为「标题:详情」循环滚动,且不进入用户公告列表
ci/woodpecker/tag/build Pipeline was successful
- 打卡页顶部置顶公告由「仅标题,点击弹详情」改为「标题:详情」整体左右循环滚动播放。
- 置顶公告从用户公告列表、未读数与聚合信息中排除,也不可被标记已读。
- 置顶仅对普通用户公告开放(后台开关在选代理时禁用并复位,后端同步校验)。
v1.2.85
2026-09-25 13:31:35 +08:00
becomer 5ee8e47b0f v1.2.84:扣款额度可配置(支持小数/0)+ 置顶公告横向滚动
ci/woodpecker/tag/build Pipeline was successful
- 新增系统设置 deduct_checkin/deduct_leave/deduct_report(默认 1),
  打卡/请假/销假从数据库读取扣减额度,支持小数与 0(0=不扣费);
  sign_count 由 int 改为 decimal(12,2) 以支持小数次数。
- 新增置顶公告:announcements.is_pinned 字段 + /announcements/pinned 接口,
  打卡页顶部横向无缝滚动展示,点击查看详情;后台公告管理支持置顶开关与标识。
- 后台系统设置新增三项扣款额度配置;用户/后台的签到次数展示改为去尾零格式。
v1.2.84
2026-09-25 13:07:14 +08:00
becomer af24472e2a v1.2.83:新建打卡任务名称增加匹配校验——改到匹配不上现有任务时直接提示并拦截
ci/woodpecker/tag/build Pipeline was successful
新建流程 Step 1 编辑任务名称后此前不校验,而执行打卡时后端按该名称 globMatch
实时匹配今日校园任务实例(FetchTodayTaskDetail),名称改坏即永远匹配失败。
现前端按与后端一致的 globMatch 规则、限同 taskType 比对现有任务名,命中则实时
提示,未命中在点「下一步」时弹窗拦截,不创建任务也不进入 Step 2。
v1.2.83
2026-09-23 23:30:12 +08:00
becomer 518f3506df v1.2.82:修复新套请假审批人查询失败——时间格式统一裁秒
ci/woodpecker/tag/build Pipeline was successful
新套 getApproveFlowList 按 "yyyy-MM-dd HH:mm" 严格解析(如 hbgyzy 病假类型 7151
报 unparsed text found at index 16),前端补秒的 19 字符时间导致接口报错,
前端静默降级后误显示"该校该请假类型未配置审批人"。在 FetchLeaveNewApproveFlow
服务层统一裁秒,覆盖前端预取与提交执行两条调用路径。
v1.2.82
2026-09-23 00:14:53 +08:00
becomer 21b37c6ad8 v1.2.81:请销假链路健壮性与设备码校验
ci/woodpecker/tag/build Pipeline was successful
- leavePost 统一校验 HTTP 状态码与空 body:非 2xx 报「学校服务端响应异常(HTTP xxx)」,
  空 body 报「可能触发风控或瞬时故障」,不再抛出无意义的 JSON 解析错误
- 旧套销假识别 2210990000(需附件):提示上传附件后重试
- 打卡/请假/销假执行入口补后端设备码硬校验;请假页前端入口补绑定引导弹窗
- SMS 注册密码不再使用设备码样式的随机值(避免与设备码绑定语义混淆)
v1.2.81
2026-09-22 19:14:26 +08:00
becomer a7180eebba v1.2.80:根治 cookie 失效误报——统一识别登录态失效并自动续期
ci/woodpecker/tag/build Pipeline was successful
① 问题:用户赵佳威(芜湖职业技术大学)反馈「无法获取签到任务」。
   日志显示 whit.campusphere.net 两类查询均报
   "解析任务列表响应失败: invalid character '<' looking for beginning of value"
   —— 今日校园返回 HTML(请求被 302 到 authserver 登录页),即 cookie 已失效。
   其 MOD_AUTH_CAS 票为 09-11 签发、早已过期;而 last_login_at 显示 09-21 是
   jwt 中间件 touchDailyLogin 刷新的"当天使用"标记,并非真实重登,故造成
   "看起来刚登录过、却取不到任务"的假象。

② 根因:原 isCookieExpired 仅匹配错误文案「未登录/登录已过期」,而 HTML 响应的
   报错是 invalid character '<',匹配不到 → 既无法识别、更不会触发续期,
   用户只看到空任务列表。该问题影响所有「已登录后调用今日校园」的接口
   (任务列表、用户信息同步、请销假等)。

③ 修复:
   - cpdaily 层新增 ErrCookieExpired 与 unmarshalBody:解析响应体前先判断
     是否为 HTML,是则返回 ErrCookieExpired(42 处 body 解析统一接入;
     登录/发码/滑块等登录流程不涉及用户 cookie,保持原样不误伤);
   - FetchAllRecentTasks 改为返回 error(两类查询都失败时),供上层识别续期;
   - isCookieExpired 合并 ErrCookieExpired 判定 → 既有 25 处调用点自动受益;
   - 新增 withCookieRefresh(通用包装):检测到登录态失效 → refreshCpdailyCookie
     (长期凭证优先,其次密码重登)→ 用新 cookie 重试一次;续期失败返回
     ErrCookieExpired,上层转「登录凭证已过期,请重新登录」;
   - 应用于 GetTaskTitles、GetPendingLeaves(含新/旧套件)、syncUserProfile(用户信息)。
   - 防递归:refreshCpdailyCookie 内部同步用户信息时禁续期
     (syncUserProfileWith allowRefresh=false),避免「续期→同步→再续期」风暴。

④ MFA 学校说明:MFA 通道登录签发短期 JWT,到期即要求重新登录,本就不需自动续期;
   非 MFA 学校(如本例)的 cookie 由上述链路自动续期,用户不再被莫名踢出。

⑤ 验证(真实数据,只读):
   - 用赵佳威过期 cookie 调 FetchAllRecentTasks:由原来的 invalid character '<'
     变为 cpdaily_cookie_expired,errors.Is(err, ErrCookieExpired)=true;
   - 模拟完整链路:旧 cookie 失败 → 用其保存密码重登成功(6 个 cookie、含新 MOD_AUTH_CAS、
     无 MFA)→ 新 cookie 重试成功取到任务「查寝」;
   - go build / go vet / go test ./... 全过。
v1.2.80
2026-09-21 16:57:21 +08:00
becomer 386c6e2bf8 v1.2.79:工单改为软删除——用户删除后管理员仍可见(保留处理痕迹)
ci/woodpecker/tag/build Pipeline was successful
① 问题:用户可删除自己的工单,且管理员完全看不到。根因是 Ticket 无软删除,
   删除为物理删除并级联删除全部消息,记录从表中消失,管理端列表/统计均无痕迹,
   用户可借此抹掉工单内容与管理员的全部回复。

② 修复:
   - model.Ticket 增加 DeletedAt gorm.DeletedAt(AutoMigrate 自动加列,
     既有数据 deleted_at 为 NULL 视为未删除,不受影响);
   - DeleteTicket 改为软删除,且不再级联删除 TicketMessage / TicketNotification,
     保留完整对话与通知,供管理端追溯;
   - 管理端 AdminListTickets / AdminGetTicket / AdminReplyTicket /
     AdminUpdateTicketStatus 改用 Unscoped(),可查看并继续处理已删工单,
     列表与详情响应新增 deleted 字段;
   - 用户端(ListMyTickets / GetTicketDetail / AddTicketMessage / CloseTicket)
     不加 Unscoped,GORM 默认过滤 deleted_at,用户看不到自己已删除的工单;
     未关闭工单计数同样自动排除已删除(不再计入"进行中")。

③ 前端:管理员工单列表与详情在状态标签旁展示「用户已删除」标记
   (含 title 提示);api/ticket.ts 的 AdminTicketListItem / TicketDetail 增加 deleted。

④ 验证:在真实 MySQL 上用临时表(tmp_softdel_probe,验证后已删除)确认三项语义——
   默认查询过滤软删除(count=0)、Unscoped 可见且 deleted_at 可读、
   管理端 Unscoped().Table().Select("t.*") + 内嵌结构体扫描能读出 deleted_at;
   go build / go vet / go test ./... 通过;vue-tsc --noEmit、npm run build 通过。

注:此前已被物理删除的工单无法找回,本改动仅对之后生效。
v1.2.79
2026-09-21 13:35:39 +08:00
becomer 2dd1180bf8 v1.2.78:修复手机号登录失败无提示、页面被重置回登录页
ci/woodpecker/tag/build Pipeline was successful
① 现象(用户实测手机号 13551503012):点击登录后页面看似跳转,
   实则被重置回 /login、输入信息清空,且没有任何错误提示。

② 根因:后端 SmsLogin 登录失败返回 HTTP 401(如「登录失败: 验证码错误…」),
   而前端 axios 响应拦截器把 401 一律视为「登录态过期」→ 尝试用 refresh_token
   刷新 → 登录页无 refresh_token → catch 清除 access/refresh token 并
   window.location.href='/login'。
   既有白名单只排除了 /auth/login 与 /auth/refresh,新增的 /auth/sms/* 未覆盖
   ('/auth/sms/login' 并不包含子串 '/auth/login'),故被误判。
   真实错误消息(后端 message)也随之被吞掉,用户只看到页面被重置。

③ 修复(frondend/src/api/index.ts):401 刷新分支增加
   !originalRequest.url?.includes('/auth/sms/') 排除项,
   使手机号登录(发码/登录)的 401 作为普通业务错误抛给调用方 catch,
   由登录页 toast 展示真实原因。

④ 实测复现与验证:容器内用真实图片验证码调用 /api/auth/sms/login,
   服务端返回 401 {"code":401,"message":"登录失败: 验证码错误次数上限5次,您已输错1次"},
   与该手机号毕业/验证码状态一致的业务错误;修复后该消息会正常显示而非重置页面。
   vue-tsc --noEmit、npm run build 通过。
v1.2.78
2026-09-20 09:42:16 +08:00
becomer a0064881b3 v1.2.77:手机号登录增加两个独立图片验证码(发码 / 登录各一)
ci/woodpecker/tag/build Pipeline was successful
① 需求:手机号登录的「获取验证码」与「输入验证码登录」两步都需要各自的图片验证码,
   且两者相互独立(不共用同一个 captchaId)。

② 后端(backed/internal/handler/sms_login.go):
   - SmsSendRequest / SmsLoginRequest 增加 captchaId + captchaCode 字段;
   - SmsSend 与 SmsLogin 各自调用 service.VerifyCaptcha 校验,
     缺失或错误分别返回「请输入图片验证码」/「验证码错误或已过期」。
   (原 SmsSend 仅有 IP 限流,现补上图片验证码,防脚本刷短信。)

③ 前端(LoginView.vue + api/auth.ts):
   - 新增两组独立验证码状态:发送用(smsSendCaptcha*)与登录用(smsLoginCaptcha*),
     各自有 id / image / 输入 / 错误 / loading;
   - 通用刷新函数 loadSmsCaptcha('send'|'login');进入手机号模式时两组各取一份;
   - 手机号模式下渲染两个验证码输入框(发送前一个、登录前一个);
   - sendSmsCode / smsLogin 增加 captchaId + captchaCode 参数;
   - 发码失败或登录失败分别刷新对应验证码。

④ 邀请码兼容:手机号登录路径继续透传 inviteCode(URL ?invite= 自动带入),
   新用户注册时经通用注册流程发放邀请/注册奖励;已有账号匹配时不重复发奖。

⑤ 验证:go build / go vet 通过;vue-tsc --noEmit、npm run build 通过。
v1.2.77
2026-09-19 23:41:43 +08:00
becomer 4ca952f2ac v1.2.76:新增手机号验证码登录(无需选学校)+ 与学工号登录账号互通
ci/woodpecker/tag/build Pipeline was successful
① 手机号验证码登录(对标旧项目 likedream-go login/sms.go):
   - 短信发码:POST https://mobile.campushoy.com/app/auth/authentication/mobile/messageCode/v-8222
     (手机号 AES 加密后以 a=参数提交);
   - 验证码登录:POST .../mobileLogin/v-8222,返回 a=base64(AES(json)),解密后含
     tenantId(学校租户)、openId(学工号)、sessionToken、tgc(CASTGC);
     —— tenantId 由登录响应直接给出,故**无需用户先选择学校**;
   - 换取 CAS cookie:GET {学校域名}/wec-im-group/group/imUserToken/getImUserToken,
     带 AmpCookies 头(DES 加密 {AMP1,AMP2:[{name:sessionToken}]})与预置
     clientType/tenantId/standAlone/sessionToken/CASTGC cookie,换回含 MOD_AUTH_CAS 的 cookie。

② 长期凭证续期:cookie 含 sessionToken(长期有效),cookie 过期时优先用
   RefreshCasCookie 直接换取新 CAS cookie,不再依赖 raw_password——
   手机号验证码登录的账号没有密码,原「密码重登」路径无法续期;
   续期失败再回退原密码重登逻辑(学工号登录用户不受影响)。

③ 账号互通(兼容手机号与学工号登录指向同一用户):
   - 手机号登录:按「同校 + 同学工号(openId)」匹配已有账号并复用;未命中则按
     用户名(学校短码+学工号)再匹配;仍未命中才走通用注册流程(复用邀请/注册奖励);
   - 手机号登录后回填 student_id 与 mobile_phone,使其之后用学工号登录也能匹配到;
   - 学工号登录(findOrCreateUser)增加「同校 + 同学工号」兜底匹配,
     反向兼容「先手机号注册、后学工号登录」的场景。

④ 前端登录页:新增「学工号登录 / 手机号登录」切换;手机号模式下仅需
   手机号 + 短信验证码(含 60s 倒计时),不选学校。

⑤ 验证:go build / go vet / go test ./... 通过;vue-tsc --noEmit、npm run build 通过。
   加密键与旧项目一致(AES-128-CBC / DES-CBC,PKCS7),复用既有 crypto.go。
v1.2.76
2026-09-19 23:14:29 +08:00
becomer edeabb0fc0 v1.2.75:回滚「每日签到积分」功能(保留验证码框修复)
ci/woodpecker/tag/build Pipeline was successful
v1.2.74 的签到积分功能经评估效果不佳,按需求回滚:
- 移除 User.checkin_points 字段;
- 移除 POST /api/auth/checkin 接口及路由;
- 打卡/请销假恢复为仅扣 sign_count(移除「优先消耗 1 积分」分支与 deductQuota);
- 移除个人中心「签到积分」入口、签到弹窗与 api/auth.ts 的 checkin 方法及类型字段。

保留 v1.2.74 中的「验证码框留白修复」(与签到积分无关):
- 个人中心验证码容器 w-24 → w-[120px](3:1 匹配生成图 180x60)、图片 object-cover;
- 登录页两处验证码图片 max-w-full/max-h-full object-contain → w-full h-full object-cover。

验证:grep 确认无 checkin 残留;go build / go vet 通过;vue-tsc --noEmit 通过。
v1.2.75
2026-09-19 11:41:42 +08:00
becomer 559b146408 v1.2.74:新增每日签到积分(个人中心)+ 打卡/请销假优先消耗积分;修复验证码框空隙
ci/woodpecker/tag/build Pipeline was successful
① 签到积分体系:
   - User 新增 checkin_points(decimal(10,4),0~1),AutoMigrate 自动加列;
   - 新增 POST /api/auth/checkin:校验已有图片验证码后,随机发放 0.01~0.30
     积分(保留 4 位小数),封顶 1.0;达 1 后不可再签到(原子条件更新防并发超发);
   - 个人中心新增「签到积分」入口:显示 当前积分/1 与签到按钮(达上限置灰「已达上限」),
     弹窗内含积分规则、当前积分与图片验证码。

② 打卡 / 请销假改为优先消耗签到积分:
   - 额度检查与扣减逻辑由「仅 signCount」改为「积分≥1 优先扣 1 积分,否则扣 1 次数」;
   - 覆盖 3 处执行点:任务执行(task.go)、销假新套与旧套(leave.go),
     抽公共函数 deductQuota 复用;均用条件更新原子扣减;VIP 仍免扣。

③ 修复验证码框留白:
   - 生成图为 180x60(3:1,代码强制下限),而个人中心验证码容器原为 w-24 h-10(2.4:1),
     图片按宽度缩放后上下留空。统一容器宽度为 w-[120px](3:1),
     并把图片改为 w-full h-full object-cover 填满(登录页两处同改)。

④ 验证:go build / go vet 通过;vue-tsc --noEmit、npm run build 通过。
v1.2.74
2026-09-19 11:15:25 +08:00
becomer d8f2c3676c v1.2.73:待销假列表过滤已驳回的请假记录
ci/woodpecker/tag/build Pipeline was successful
① 问题:销假时「待销假列表」出现已驳回状态的请假记录(用户 陈莎莎/1038
   即是此情况),这类记录无法销假,不应出现在列表中。

② 根因:旧套待销假列表 FetchLeaveList 只过滤了 status=="4"(已完成),
   未过滤驳回状态。实测该用户旧套接口返回的记录 status="3"(前端映射「已驳回」,
   事假/教资认定),被原样放进列表。
   (新套 fetchLeaveNewPendingList 已按 3/4 白名单过滤,无此问题;该校走旧套。)

③ 修复:FetchLeaveList 中一并过滤驳回状态——
   status=="3"(已驳回)与 status=="10"(逾期自动驳回)。

④ 验证:go build ./... 通过。只读实测确认该用户旧套记录 status=3 已被排除。
v1.2.73
2026-09-19 10:54:12 +08:00
becomer cd963d3834 v1.2.72:修复用户管理执行弹窗「已删除」标签内部换行
ci/woodpecker/tag/build Pipeline was successful
v1.2.71 把状态列改为纵向排列后,标签仍缺 whitespace-nowrap,
在窄列内文字被折行(「已删/除」)。为「成功/失败」与「已删除」两个标签
补上 whitespace-nowrap,保证文字单行显示。
v1.2.72
2026-09-19 10:44:01 +08:00
becomer c5e0888639 v1.2.71:修复后台 CDK 复制无效;用户管理执行弹窗状态标签换行
ci/woodpecker/tag/build Pipeline was successful
① 用户管理「当日执行」弹窗:状态列的「成功/失败」与「已删除」两个标签原在同一行
   (td 带 whitespace-nowrap,列宽 w-[60px]),在窄列下被挤到溢出、视觉上串到
   「说明」列。改为纵向排列(flex-col + gap-1,各占一行),表头宽度放宽到 w-[72px]。

② CDK 管理复制「提示成功但实际粘贴不出内容」:
   根因是 copyToClipboard 把临时 textarea 挂到 document.body,而复制按钮位于
   AppDialog(reka-ui)弹窗内——DialogContent 用 FocusScope 锁定焦点,document 上的
   focusin 监听发现焦点跑到 dialog 容器外就立即抢回,导致 ta.focus()/select() 失效,
   选区为空;execCommand('copy') 仍返回 true,于是「显示已复制、实际为空」。
   修复:
   - textarea 优先挂到当前打开的 dialog 容器内(role="dialog" 中最靠后可见者),
     避免焦点被 FocusScope 抢回;
   - 复制前校验选区确实落在 textarea 上(selectionStart/End 与 activeElement),
     校验不过则不再盲信 execCommand 的返回值,转而走 Clipboard API 兜底;
   - 移除 textarea 改为按实际父节点(不再假设是 body)。
   该修复同时惠及所有位于弹窗内的复制按钮(CDK 单个/全部复制、待审学校等)。

   注:AdminUsers 的设备码复制(copyDeviceCode)未在本次范围内,保持原样。
v1.2.71
2026-09-19 10:38:42 +08:00
becomer 3bcc32b74e v1.2.70:网课页新增「我有网课,点击这里」贴边按钮(跳转工单)
ci/woodpecker/tag/build Pipeline was successful
模仿打卡页「收藏本站」与请假页「无法请/销假?提交工单」的右侧贴边窄竖条样式,
在网课页加入「我有网课,点击这里」按钮,点击跳转 /ticket 工单页。

- CourseView.vue:新增按钮(fixed right-0 bottom-32, bg-rose-500/95)与 goTicket();
- 复用同款竖排文字样式 .bookmark-vtext(writing-mode: vertical-rl)。
v1.2.70
2026-09-18 22:36:27 +08:00
becomer 8de7470b9d v1.2.69:底部 TabBar 新增「网课」入口(开发中占位页)
ci/woodpecker/tag/build Pipeline was successful
① 需求:底部导航新增「网课」,放在个人中心左侧(第三个位置):
   打卡 → 请/销假 → 网课 → 个人中心。

② 改动:
   - 新增 src/views/course/CourseView.vue:顶部标题栏 + 居中占位「正在努力开发中」;
   - src/layouts/MobileLayout.vue:tabs 数组插入 { name: 'course', label: '网课', icon: BookOpen },
     并补充 BookOpen 的 import;
   - src/router/index.ts:MobileLayout 子路由新增 /course(置于 leave 与 profile 之间),
     注册 CourseView。

③ 说明:网课页不走 home/leave 的顶部蓝色渐变头(第 258 行仅这两者显示),
   使用自身标题栏。引导流程仅涉及 home/profile,不受影响。
v1.2.69
2026-09-18 22:20:49 +08:00
becomer c90784de67 v1.2.68:编辑表单优先取历史记录的真实经纬度/地址(并校验在打卡范围内)
ci/woodpecker/tag/build Pipeline was successful
① 问题:编辑打卡任务表单时,经纬度与地址取到的是学校配置的"打卡范围信息"
   (如 signPlaceSelected[0].address = "新建的签到地点"),而非历史记录里的真实
   签到位置。实测用户 1095(兰州财经大学,晚归寝打卡)确认。

② 根因:ParseTaskForm 的定位取值顺序是「signPlaceSelected[0] 优先」——
   该字段是学校配置的范围点(含范围点名与范围点坐标),被排在历史记录的实际
   签到经纬度之前,于是历史真实坐标/地址永远取不到。

③ 修复(backed/internal/service/cpdaily/task.go):
   - 改为「历史记录优先」:先取 histRows 的 longitude/latitude 与 signAddress/position;
   - 但新增范围校验 coordsWithinSignRange:历史坐标必须落在打卡范围内
     (复用已有 parseSignPlaces + haversineMeters,逐点比对 radius);
     超出全部范围点时放弃历史坐标与地址,回退到 signPlaceSelected[0] 范围点,
     避免服务端以"位置不在签到范围内"拒绝提交;
   - 无范围点配置时不做限制(沿用历史坐标);历史坐标缺失同样回退范围点。
   - 无历史记录(histRows == rows)时行为与改动前一致,无回归。

④ 验证(真实数据,只读):
   - 修复前 lon=103.89427283364513 / lat=36.053086828712225 / address=新建的签到地点;
     修复后 lon=103.894854 / lat=36.052122 /
     address=中国甘肃省兰州市城关区拱星墩街道段家滩路426号(历史真实值);
   - 实测该历史坐标距范围点 119.3m < 240m,判定在范围内 → 正确采用历史值;
   - 超范围/空坐标/无范围点三类边界均按预期;
   - 无历史记录分支输出与改动前一致。
   - go build / go vet / go test ./... 全过。
     顺带修正 v1.2.66 调序后未同步的两个测试断言(二维码项已移至末尾)。
v1.2.68
2026-09-18 21:48:33 +08:00
becomer 186af6b604 v1.2.67:修复 MFA 登录报「生成验证会话失败」——运维配置 Redis 改用容器名
ci/woodpecker/tag/build Pipeline was successful
① 问题:中国矿业大学(cumt,需 MFA)登录报「生成验证会话失败」。
   日志证据:MFA参数解析 userId=TS26040188P31(中国矿业大学)后,
   ERROR MFA会话暂存失败 error="dial tcp 172.22.0.7:6379: connect: connection refused"。

② 根因:宿主机 /opt/cpdaily-xiaozhushou/config.toml 的 [redis] host 写死为
   "172.22.0.7"。该 IP 曾是 Redis 容器地址,但容器重启后 IP 漂移,现已被
   likedream-go 占用;原 Redis 现为 172.22.0.8 / likedream-redis 为 172.22.0.6。
   即写死 IP 的经典陷阱:容器重建后 IP 易主,配置指向了非 Redis 的容器。
   MFA 中间会话必须跨请求保留(start → send → verify 三步),硬依赖 Redis,
   故 Redis 不可用时该步直接失败——captcha 有内存降级,MFA 没有(设计上不可降级)。

③ 修复(仅运维配置,不入库):
   /opt/cpdaily-xiaozhushou/config.toml 的 host 由 "172.22.0.7" 改为容器名
   "1Panel-redis-zDUv"(同属 likedream-net,容器名 DNS 稳定,不随 IP 漂移;
   端口 6379 与密码 redis_4APk8B 本就对应此实例,无需改动)。
   修改前已备份为 config.toml.bak.<时间戳>。

④ 验证:两个槽位(8884/8885)重启后,
   - 启动日志不再出现 "Redis 连接失败(...connection refused)";
   - 调用 /api/auth/captcha 后,Redis 相关警告行数由每次必现降为 0
     (此前每次操作必打「验证码写入 Redis 失败,降级内存存储」)。
   已确认 Redis 连通,未复测完整登录流程。

⑤ 本提交为空提交(--allow-empty):本次修复全部在宿主机运维配置,
   无仓库代码变更,仅以此 commit 记录版本事件与排查结论。
v1.2.67
2026-09-18 21:28:43 +08:00
becomer a3ec85f4d3 v1.2.66:扫码签到表单项顺序调整——经纬度/地址在前,二维码置于最后
ci/woodpecker/tag/build Pipeline was successful
扫码签到表单原顺序为「二维码ID → 经度 → 纬度 → 地址 → 照片 → 额外字段」,
与普通签到(定位在前)不一致。现调整为「经度 → 纬度 → 地址 → 照片 → 额外字段 → 二维码ID」,
定位字段顺序与普通签到对齐,二维码作为扫码任务特有项放在最后。

仅调整 parseScanTaskForm 中表单项的 append 顺序;fillScanSubmitForm 按 colName 查找字段,
不依赖顺序,提交逻辑不变。go build ./... 通过。
v1.2.66
2026-09-18 18:05:50 +08:00
becomer ee466b253d v1.2.65:修复扫码签到报错 empty String——补上经纬度/地址
ci/woodpecker/tag/build Pipeline was successful
① 问题:用户陈莎莎(昆明文理学院,任务 139「2027届毕业生体检签到」)扫码签到
   始终报 "empty String"。
   查日志:submitSign 响应 code=-1 message="empty String",即今日校园服务端返回的
   校验错误(我们代码里无此字符串,仅原样透传)。

② 根因:该任务 signMode=2(扫码签到)。v1.2.40 把扫码签到提交体改为
   「用 qrUuid 替代 signInstanceWid、不携带经纬度」,但实测服务端仍要求定位字段:
   SubmitSign 的提交信封里 lat/lon 取 submitForm 的 latitude/longitude,
   扫码分支被留空 → 提交外层 {"lon":"","lat":""} → 服务端把空串转数字抛
   NumberFormatException: empty String。对照官方 qrsign 页面 JS(counselor-front),
   其 submitSign 的 body 明确含 position 与 ...currentPosition(经纬度),
   印证定位字段是必需的。历史上扫码签到从未成功过一次(成功记录均为普通定位签到)。

③ 修复(对齐普通签到):
   - parseScanTaskForm 新增「经度/纬度/签到具体地址」三个表单项(lon/lat/address),
     取值优先级与普通签到一致:signPlaceSelected[0] → 历史/详情的 longitude|latitude
     → 详情的 position|signAddress;抽为 extractScanPosition 复用。
   - fillScanSubmitForm 提交体补上 longitude/latitude/position,
     表单未提供时回退任务详情的学校默认坐标。
   - 前端按 colName 渲染,新字段自动显示,无需改动。

④ 验证:go build ./... 、go vet ./... 、go test ./... 全过
   (同步更新扫码任务的表单/提交体断言)。不检查部署。
v1.2.65
2026-09-18 18:00:41 +08:00
becomer 2effdbaaf9 v1.2.64:删除等操作的原生 confirm/alert 统一改为项目 AppDialog
ci/woodpecker/tag/build Pipeline was successful
① 问题:删除任务的确认框是浏览器自带弹窗,与全站统一样式不一致。
   排查确认这是 v1.2.55「弹窗统一化」的遗留缺口——那次只扫描并迁移了手写
   role="dialog" 元素(提交信息称「全项目手写 role=dialog 残留归零」),
   而 confirm()/alert() 是浏览器原生调用、不产生 role="dialog" 元素,
   故从未被纳入扫描范围。「归零」对 role="dialog" 属实,但原生弹窗不在其内。

② 全项目原生弹窗共 9 处 / 4 文件:
   HomeView:删日志、删任务(confirm);保存失败 ×2、获取表单失败(alert);
   TicketList:关闭工单、删除工单(confirm);
   LeaveView:删日志(confirm);AdminAnnouncements:删公告(confirm)。

③ 修复:
   - 新增 composables/useConfirm.ts:模块级单例服务(对齐既有 useMessage 风格),
     confirmDialog()/alertDialog() 返回 Promise;重复调用时旧的以 false 结清,
     避免 Promise 悬挂;respond 延迟 200ms 清空 state 以配合离场动画。
   - 新增 components/ConfirmHost.vue:全站唯一弹窗宿主,挂到 App.vue,
     z-index 取 z-[300](高于版本更新弹窗的 z-[200])。
     视觉对齐现有居中弹窗(圆形图标底 + 标题 + 灰色说明 + 双按钮);
     danger=true 时红色图标底 + destructive 确认按钮(--color-destructive #ef4444)。
   - 9 处调用点改为 await confirmDialog/alertDialog;
     删除类置 danger:true,关闭工单等非破坏性操作不置。

④ 验证(CDP 驱动真实 Chrome 实测,非仅静态检查):
   - 弹窗为 role="dialog" 的项目组件,原生 confirm/alert 调用次数均为 0;
   - 取消返回 false、确定返回 true、Esc 视为取消(false),按钮文案/颜色符合预期;
   - 截图确认三种形态(危险确认/普通确认/单按钮提示)视觉与现有弹窗一致;
   - 源码与构建产物双重扫描:代码级原生弹窗残留 0 处;
   - vue-tsc --noEmit、npm run build 通过。不检查部署。
v1.2.64
2026-09-18 17:20:20 +08:00
becomer e979e3cc9f v1.2.63:修复二维码解析失败——改用两阶段多尺度扫描 + Otsu 兜底
ci/woodpecker/tag/build Pipeline was successful
① 问题:用户上传二维码报「未识别到二维码」,但同一张图用草料二维码能正常解析。
   实测该图(1176x1568,斜拍屏幕、二维码区域约 674x674,周围有深色文字噪声):
   jsQR 在原生分辨率与放大后均无法解码,仅在特定缩小档位可解。

② 根因:jsQR 使用固定的 8x8 分块自适应二值化,只有当"二维码模块边长"与块尺寸
   量级相当时才能正确二值化,因此对缩放极其敏感——同一张图某档能解、相邻档即失效。
   原实现只试 [1, 0.6, 0.4, 1.5] 四档,恰好只有 0.4 落在可解窗口内,属侥幸命中;
   而不同浏览器/WebView 的 canvas 重采样算法不同,可解档位会整体漂移,
   故出现"桌面偶尔能过、手机端始终失败"。实测在 Google Chrome 下将重采样设为
   nearest 时,原实现即失败。

③ 修复(HomeView.vue 的 decodeQrInBrowser 重写):
   - 多尺度细扫:按"目标长边"从 min(原图长边,1400) 起以 0.72 递减至 200,
     覆盖连续可解窗口,不再依赖单档侥幸命中;
   - 两阶段:阶段一逐档直接解码;阶段二对每档先做全局 Otsu 二值化再解码,
     补足对比度差/斜拍屏幕的图片(阶段一失败改为 break 而非 return,
     并只分配一半预算,确保 Otsu 兜底始终有机会执行);
   - 时间预算 3000ms 封顶;逐档 await 让出主线程,避免"解析中"loading 卡死。
   - 新增 loadImageFromFile / otsuBinarizeInPlace 辅助函数。

④ 验证(Google Chrome headless,用真实构建产物抽取解码函数运行):
   - 用户原图 + 10 个扰动变体(旋转/亮度/模糊/噪声/重压缩/低分辨率)共 12 张全部解出正确 uuid;
   - 2 张无二维码图片正确返回空;
   - 四种重采样模式(high/medium/low/nearest)全部通过(原实现 nearest 失败);
   - 真实 CPU 耗时:成功约 0.3–0.9s,失败(无二维码)约 4.6s 且全程逐档让出主线程;
   - vue-tsc --noEmit、npm run build 通过。不检查部署。
v1.2.63
2026-09-18 16:57:27 +08:00