Q

Q pushed to master at yuansiwei/DEV-MERGE-LEGACY-PC-FRONTEND-2026

  • 26a9d5125f chore(mock): academic mock 按契约 v1.6~v1.9 全量回改 + 消费方同步 + vite proxy 去前缀 mock 侧(academic 63 端点契约对齐 v1.9,权威 = unified/03-migration/academic-frontend-contract-v1.md): - v1.6 A1: 欠费字段 unpaid → remainderReceivable(端点 53/55) - v1.6 A2: 分账 quota 改金额(元,两位小数字符串;0.00 合法);3111 判据=子额度>父额度(端点 40) - v1.6 B1: 端点 10 spend 不写流水,仅回 {spend} 无 logId - v1.6 B2: 端点 12 工单时间改 requestTime/responseTime(createdAt 不下发) - v1.6 B3: 端点 11 流水行删 institutionId/campusId(流水表无作用域列) - v1.6 B4: 端点 21 仪表盘改账户横截面 5 字段 + balanceDistribution(删 periodIncome 等) - v1.6 B5: 端点 23 删 bizNo;端点 31 items 删 memo - v1.6 C1/C2/C3: createAt→createTime;startAt/endAt→startTime/endTime;打卡 type/pay/cost/giveCost - v1.6 E5/E7/E8/E9: numberSign 不可写→2002;3107 按状态判;ULID 统一 26 位;status 出数字码 - v1.7: 端点 51 divide[] = [{userId,type,amount}](删 level/quota) - v1.8 课程域: 删 room(只有 address);week 改字符串("-1"/"0"~"6"/"1,3,5");to 含当日; realName 可空(null);3143 重复报名;端点 54 补入参且不写流水不动余额 - v1.9 课酬/收支/维护: 端点 58 两级结构(方案+明细,PUT 整体替换 details);端点 59 试算 9 字段 (含 schemeId/schemeCampusId/schemeScope/ruleTier 溯源,只回规则参数);端点 60 删 direction、 userId→createUserId、补 expenditureId、字典 {id,type,name,parentId}、POST /types 不做; 端点 63 mode 缺失→2001 / 非法→2002 消费方同步(防静默空白列,mock+DTO+view 三方一致): - api/academic/finance.ts: ExpenseLogDTO(createTime、删两作用域字段)、BalanceRequestDTO (requestTime/responseTime 可空)、spend() 返回删 logId - api/academic/activity.ts: ActivityDTO(startTime/endTime、money、numberSign 改 number) - views/academic/fee: 流水列 createTime、工单列 requestTime - views/academic/activity: 起止列 startTime/endTime vite.config.ts(契约 §4 四层口径 D1/D2): - proxy /api 加 rewrite 去前缀(无条件、无例外) - defineConfig 函数式 + loadEnv,VITE_API_PROXY_TARGET 从 .env 读(shell env 不可见) 质量门禁:vue-tsc 0 错 / vite build 0 错(主 chunk 31.13kB 远低于 1024 上限)/ eslint 0 错 / dist grep 无 mock 特征串(VITE_API_MOCK/mulberry32/mock-invalid-token 均 0 命中)

11 minutes ago

Q pushed to master at yuansiwei/DEV-MERGE-LEGACY-PC-FRONTEND-2026

  • fdb94de38c feat(org): 补齐 org 域 mock + 10 业务页 + 费用页操作面(打通业务页 mock) 背景:mock 原只做 academic 58 端点,落地页 /org/institutions 与菜单「机构/校区」 「成员/班级」打 /api/org/*(org 域零 mock)→ 进工作台首屏即 3001「mock 未覆盖」。 - org 域 mock(routes/org.ts,3 端点,注册进 index.ts):institutions/campuses/ members,复用 data.campuses/users 种子,越界 1004 - 路由:layout children 1→12(org/campus、org/member、academic/fee、academic/ schedule、academic/activity、academic/divide、teaching/exam、teaching/analysis、 system/users、system/tasks、system/logs),菜单 path 全对齐 - 费用页(views/academic/fee/index.vue,对齐旧 PHP 财务可操作面):账户+校区选择、 仪表盘(端点21 el-statistic)、流水(端点11 分页)、8 账户操作(充值/消费/退费/自定义 退费/坏账/清零/spend+转结,敏感操作二次确认+requestId 幂等)、工单 4 操作(通过/ 驳回/撤销) - 教学/系统域 5 页做占位(common/placeholder.vue,不发请求避免 toast) - 新增 api 封装(一资源一文件):api/org/{campus,member}、api/academic/{finance, activity,divide-into,course} 门禁:vue-tsc 0 错 / build 成功 / dist mock 特征串 0 泄漏 / dev 7 新模块转换 200
  • d4b563853f feat(ui): 登录/作用域页对齐原型 + 记住上次作用域 - 登录页:左墨蓝品牌区(logo+slogan)+ 右白登录卡,grid 分栏贴原型;脚本逻辑不动 - 作用域页:墨蓝全屏 + 白卡 + 步骤条 + 卡片式单选(替换原显示 0 的下拉框) - 作用域记忆:localStorage 持久化 ysw:last-scope,applyLogin 写入/登出不清除, scope-select onMounted 默认选中上次并打"上次"角标;无记录默认首项 - 样式复用 variables.scss 品牌 token,窄屏 @media 隐藏品牌区
  • 32ed17b544 feat(academic): 落地 PC 端 mock 联调(axios adapter 拦截,58 端点 + 8 异常分支) 按 academic-mock-plan v1.0 全量实施(M1~M8): - 零依赖:http 实例挂自定义 adapter,dev + VITE_API_MOCK 门控,生产 DCE 不进 dist - 信封/金额/时间辅助(envelope/money/time)+ 确定性内存数据(data,每用户 320 流水验证分页边界) - 7 域路由:auth(5) / finance(17, 端点1~16,21) / activities(16, 22~37) / divide-into(6, 38~43) / courses(13, 44~56) / payroll(3, 58~60) / maintenance(3, 61~63) - 异常分支:1001/1004/3102/3103(warn)/3111/3112/3120/3121/3122 + repeated=true 幂等回放 - 维护型端点 super_admin 门禁(requireRole) - 门禁:vue-tsc 0 错 / pnpm build 0 错 / dist 无 mock 特征串(DCE 验证通过) .env.example 补 VITE_API_MOCK 说明;.env.development(gitignore,本地开 mock)
  • View comparison for these 3 commits »

8 hours ago

Q pushed to master at yuansiwei/DEV-MERGE-LEGACY-BACKEND-2026

  • e1ce7ce182 feat(academic): AC-13d 第三批——端点60 收支台账(3 条路由,平台通用字典必须放行) ## 形状(契约 v1.9 已裁决,本次只落地) - GET /admin/expenditures 记录分页;行 9 字段(补 expenditureId、删 direction、userId→createUserId) - POST /admin/expenditures 记账 → {id,bizNo} - GET /admin/expenditures/types 字典 → {types:[{id,type,name,parentId}]} 无 POST /types;路由表 62 → **65 条 method+path**(AC-14 40 + AC-13c 15 + AC-13d 10)。 ## 四处口径(每处都有"写错就红"的用例) 1. **平台通用字典 institution_id=0 必须放行**:`t_expenditure` 的 0 不是"未归属"而是全平台类型 (房租/水电/学费收入)。若套作用域过滤,记录还在、金额分页都对,**只有 type/name 静默变空**; 更严重的是记账时字典看不见 ⇒ 机构**根本记不了账**(3001)。字典读取因此刻意不走 Scoped, 显式 `institution_id = 0 OR institution_id = ?`;`type` 过滤的子查询同口径(否则按方向筛选时整批消失)。 2. **不做 SQL JOIN**:记录表走 Scoped 生成裸列名,JOIN 会 `ambiguous column name: institution_id` (ListStudentSchedule 已踩过)。改为"分页取记录 → 按 expenditure_id 批量取字典", 让"平台字典必须放行"这条规则只出现在一处。 3. **year/month 按库认定的落库时刻算**:create_time 由 DB 默认值填,插入后同事务回读再回写分桶; 用应用时钟会在跨月那一秒把账记到相邻月份,报表只表现为"某月少一条"。 4. **cost 恒存正数**:money ≤ 0 → 2002(方向由字典 type 决定;放行负数会让"支出 -100"与 "收入 +100"成为同一条记录的两种写法)。bizNo 后端生成 26 位 ULID,前端不可指定。 ## 顺带 - `teacherIDOrNil` → `optionalTrimmedPtr`(两处复用:teacher_id 通用档 + create_user_id 记账人)。 - from 闭下界 / to 开上界,纯日期形态自动补到次日 00:00(`to=2026-04-10` = 含当日整天)。 五个变异实测变红(见交付报告):字典过滤平台行 → 记不了账 + 字典缺项;type 子查询过滤平台行 → 方向筛选 0 条;不回写分桶 → year/month NULL;不补作用域校区 → 1004;to 不补次日 → 当天记录查不到。
  • 066cb3942a fix(academic): 端点58 teacherId 可空下发 null + 出口定点化(比例1位/金额2位/教龄3位)+ 纯函数单测 三处出口口径统一到「列的 scale」,并各自有字节级断言兜着: 1. `teacherId` 改 `*string`:`teacher_id` NULL 是「通用档」这一正常业务,下发 "" 会让前端 `if (teacherId)` 把「写了老师但 ID 是空」的脏数据也当通用档 —— 数值全对、课酬算错。 空串/空白一律收口为 null,出口只剩 `null` / 合法 ULID 两种取值。 2. 新增 service/decimal_format.go(零 float64、纯函数): percentage 恒 1 位(decimal(5,1))、金额 2 位、教龄 3 位。 不定点化则 SQLite 回 "85"、MySQL 回 "85.0",前端编辑弹窗回填后 diff 误判为"用户改过"; 而单测只能退化成比数值 —— 恰恰丢掉「位数」这个量纲指纹("85.00" 说明比例被当成金额格式化)。 3. decimal_format_test.go 脱离数据库断言字节:`"85"→"85.0"`、`"100"→"100.00"`。 四个变异均实测变红(见交付报告):比例改用金额位数 → 纯函数+HTTP 双层红; NULL 折叠为空串 → teacherId 用例红;去掉进位 → "85.05"→"85.0" 红; 去掉右侧补零 → FormatYears("2.5")→"2.005"(数值放大 10 倍)红。
  • 2adee30f34 docs(academic): 写下「同一次匹配 ⇒ 同一份输出」纪律(detailId 与金额必须同源) 结课侧用教师档案算出教龄 → 匹配档 B(金额 20.00),溯源侧 lookupPayrollDetailID 又自己试算一遍且漏了教龄 → 返回档 A(detailId 显示 1)。两个字段都非空、类型都对, 只有对账时才会发现"金额与档位不是同一次决定的"。把这条固化为注释,并指明 TestFinishRecordResolvesTeacherExpFromProfile 必须同时断言 reward 与 detailId。
  • 0680aa366b feat(academic): AC-13d 第二批——端点58 课酬方案(两级形状/明细整体替换)+ 端点63 拆 2001/2002
  • b29d1fdbfa feat(academic): 教龄接上教师档案(calculatingAge 口径:365 天/年 + round 1 位)
  • View comparison for these 17 commits »

8 hours ago

Q pushed to master at yuansiwei/DEV-MERGE-LEGACY-BACKEND-2026

  • e5c21fff15 fix(academic): AC-13b 回执——端点25 错误码 3140→2002 + 端点22/24 补活动级 money 按契约 v1.6 的两处裁决落地: 1) 端点 25(PUT /admin/activities/:id):`numberSign` 传即 **2002**,不再是 3140。 依据设计 §10.1:3140 只收「两个及以上参数互相矛盾」;"传了不可写字段"属单一 参数取值非法 ⇒ 2002(`infraerrors.ErrBadFormat`)。若塞进 3140,3140 会退化成 垃圾桶(禁令见 §10.1 v0.1.19)。连带把 4 处注释/用例名从 3140 更正为 2002, 并移除因此变为未使用的 `apperr` 导入。 2) 端点 22/24(活动列表/详情):补活动级 `money`(← `t_courses_activity.money`), 两位小数字符串。契约 v1.6 新增该字段,判定依据 = 「有列 + 属主干属性 → 加入」, 与先前拒绝的 `campusAccountExists`(无列 + 纯派生)区分。 注释中写明 **活动级 money ≠ signeds[].money**(前者是配置额度、后者是报名实扣)。 用例守护(都会在改错时变红): - 端点25:断言 2002 且 number_sign 一字不动(2);把错误码改回 3140 即红。 - 端点22:`ActivityViewForTest.Money` 声明为 string ⇒ 后端若下发 number 则 unmarshal 失败;另用「无报名的停用活动 102」断言 money=0.00,防止拿 signeds[].money 顶上。 gofmt 零差异;go build/vet 干净;academic 7/7 包全绿(handler 25.7s / service 96.6s / repository 16.4s)。
  • aaacdc0a1f feat(academic): AC-13b 活动 Handler(端点 22~37)
  • View comparison for these 2 commits »

11 hours ago

Q pushed to master at yuansiwei/DEV-MERGE-LEGACY-BACKEND-2026

  • 970f7ad303 fix(academic): 契约 v1.5 对齐(端点 1 扁平 / 12 业务时间 / 21 分布)+ 端点 11 注释 ## 端点 1:改扁平(契约 v1.5 —— 后端错,契约不变) `{userId, institutionId, campusId, totalBalance, campusBalance, specialExpenses, cumulativeIncome}` 来源列(逐字段可追溯,契约 §3.4 ③ 要求): - `totalBalance` ← `t_user_expense.balance` - `campusBalance` ← `t_user_expense_school.balance`(未指定/无账户 → 0) - `specialExpenses` / `cumulativeIncome` ← 对应表 `special_expenses` / `total_income`(有校区账户取校区列) - `institutionId`/`campusId` 是**入参回显**,**不从账户行取**(`t_user_expense` 无作用域列) 顺带**删掉**已无人使用的 `AccountBalanceView` / `CampusAccountBalanceView` 与两个 builder —— 避免"嵌套与扁平两种形状并存"被下一个人误接。 **已知代价(写进注释)**:扁平形状无法用 `null` 表达"该校区未建户",`campusBalance=0` 与"未建户"不可区分; 要区分需另加标志位,本期不加(契约未要求)。 ## 端点 12:时间字段改业务列(契约 v1.5 —— 契约错,实质缺陷) - `requestTime` ← **`request_time`**(业务时间) - 新增 `responseTime` ← **`response_time`**(未审批 → `null`) - `applicantId` ← `request_user_id`、`approverId` ← `response_user_id`(**未审批 → null**,用指针保留语义) - **下线 `createdAt`** —— 它是迁移期新加列 `created_at DEFAULT CURRENT_TIMESTAMP(3)`, 迁移后**所有历史工单的值都是迁移那一刻**,下发会把全部历史工单排在同一天。 ### 横切断言 + 两组变异(team-lead 要求) `TestRequestsUseBizTimeNotCreatedAt`:造 `request_time = 2020-01-01` 的工单,断言 ① `requestTime == request_time`;② `requestTime != created_at`;③ **原始 JSON 无 `createdAt`/`createTime` 键**。 | 变异 | 实测 | |---|---| | `RequestTime` 改取 `row.CreatedAt` | `requestTime = 2026-11-09 11:07:15,期望 2020-01-01 08:00:00(← request_time 业务时间)` **红** | | 把 `createdAt` 加回 json tag | `响应含非业务时间键 "createdAt" —— created_at 是迁移期入库时间…` **红**(并打印完整响应体) | ③ 用原始 JSON 断言的原因写进注释:结构体没有该字段时,结构体断言**恒真**,等于没测。 ## 端点 21:补 `balanceDistribution`(契约 v1.5 唯一可算项) - 新增 `balanceDistribution:[{campusId, balance}]` ← `t_user_expense_school` 按 `campus_id` 聚合 (该表**有**作用域列;流水表没有 ⇒ 这正是删 `periodIncome/periodExpense/net` 却保留本项的原因)。 - 新增仓储 `ListCampusBalances`(Go 侧定点解析,禁 `SUM(decimal)`)。 - 用例 `TestDashboardBalanceDistribution`:两个校区 → 分布 2 项、按 campusId 升序、金额字符串、合计 350.00。 ## 端点 11:只补注释(契约删字段,代码本就没有) 写明「`t_user_expense_log` 无作用域列 ⇒ 响应不含 `institutionId`/`campusId`;也正因如此 非超管强制带 `userId`——不带等于跨用户查全机构流水,是真实越权而非过度收紧」。 ## 端点 39:补 `quota` 默认值断言(team-lead 点名) 新节点 `quota` 必须是**金额零**(`"0.00"`),不是 null/空串:`0` 是"不分成"的合法配置值, 用 null 表达会把"不分成"与"未配置"混为一谈。断言按 `money.Amount` 比较(sqlite 会把 "0.00" 读回 "0"), 并单独断言"不能用空串表达"。 handler 26 用例全绿;go build / go vet / go test -short ./... 全绿。
  • 49ad084f3b feat(academic): AC-13a 分账 6 端点(38~43)+ 两树隔离/树不变式断言 ## 范围(13a 全量) | 端点 | 方法 | 说明 | |---|---|---| | 38 | `GetDivideTree` | + `rootUserId?` 过滤;Nested Set 不变式 I1~I4 | | 39 | `AddDivideMember` | 转调 AC-09 `AddSubordinate`,返回 `{ancestorIds:[]}`(新增 `ListAncestorIDs` 原语,**root 限定不可省**) | | 40 | `SetDivideQuota` | 转调 AC-09 `UpdateQuota`(3111 = **新额度 > 父额度**),返回 `{affectedUserIds:[]}` | | 41 | `CreateDivideLogRecord` | 转调 AC-09 `AddLog`;**无金额入参**(分成额取节点 quota) | | 42 | `DeleteDivideLogRecord` | 定位方式是 **`studentId`+`orderNo`**(不是 `:id`,契约 §2.3);转调 `DeleteLog`,注入真实 `FirstChargedRecordID` | | 43 | `ListDivideLogs` | query 参数名按契约改 **`userId`**(响应行字段仍是 `findUserId`),右开窗口 | AC-09 的 service **未改逻辑**:新增方法都在我自己的 `divide_tree_service.go` 里做**编排**, `AddSubordinate/UpdateQuota/AddLog/DeleteLog` 一行未动(符合"不该大改 service"的要求)。 ## 量纲 `quota` = **金额字符串**(`"100.00"`),端点 40 的 3111 判据是 **「新额度 > 父额度」**(不是 `quota > 1`)。 ## 两组关键变异(全部实测,原文) | 变异 | 结果 | |---|---| | 插入点 `parentRight + 1`(off-by-one,D-3) | `插入后 A 树不变式被破坏(返回 5001)—— 检查插入点是否取了**位移前**的 parentRight(D-3)` **红** | | shift 去掉 `root_user_id` 限定(D5) | `B 树节点 01HROOTB1… 区间被平移:(left,right,layer) 期望 (1,4,1),实得 (1,6,1) —— 说明位移漏了 root_user_id 过滤(D5)` **红** | ⚠ 第二组我**调整了断言顺序**才发现价值:原先"插入后校验 A 树不变式"排在前面, D5 变异会先被 A 树不变式 5001 拦下 ⇒ 看不出"逐值比对 B 树"这条断言是否真的有效。 现在**先逐值比对 B 树、再校验 A 树不变式**,两种失败形态各命中各的文案 (D5 → 逐值文案;D-3 → 不变式文案)。这正是"断言顺序会影响可诊断性"的一个实例。 ## 其它判定(按你的四段要求) - 错误码分流:2002(quota/orderNo 非法)、3111(子超父)、2001(缺参)、1001(无作用域)各有用例; - 端点 43 的 `total` 与 `list` 同口径(右开:`to` 时刻那笔不进); - `quota`/`cumulativeIncome` 直接断言原始 JSON 片段 `"100.00"`(number/比率两种错法都拦得住)。 ## 顺手更正(你点名的一行) `models_test.go` 门禁说明由「**唯一**防线」改为「**第一道**防线(管"只改一侧"); "两侧一起改回"由 `TestPayrollTeacherExpPrecisionLocked` 兜」—— 留"唯一"会和下一段"它抓不住两边一起改"自相矛盾,容易被后人删掉后半段。 ## 状态 handler 24 用例全绿;go build / go vet / go test -short ./... 全绿;工作区干净;未 push。
  • 86cc4afd54 feat(academic): AC-13 第 1 批(端点 38 分账树 + 43 分账流水)+ Nested Set 不变式校验 按 team-lead 指定顺序**先做 38/39/40(分账树)**:本批交付 38 + 43(两条读端点), 39/40(额度)与 41/42(记/删流水,写路径)明确留到下批(文件末列了范围表,防"以为做完了")。 ## 新增 - `service/divide_tree_service.go`: · `GetTree` 组装森林 + **校验 Nested Set 四条不变式**(I1 区间值恰为 1..2N 无重复无空洞 / I2 任意两节点嵌套或相离 / I3 父区间严格包含子 / I4 `layer = parent.layer + 1`), 不成立即 **5001**(不返回"看起来对"的树——位移错位无法从数据推断原值,猜测修复会把确定性错误变成随机正确); · `ListLogs`(端点 43,右开窗口、total 与 list 同 WHERE)。 - `repository/divide_repo.go`(纯新增):`ListAllNodes`(Nested Set 先序)、`ListLogsFiltered`。 - `handler/divide_handler.go`:`GetDivideTree` / `ListDivideLogs` + 路径常量(**不含 `/api`**)。 - `handler/divide_handler_test.go`:6 个用例。 ## 量纲红线(mock 曾错) `quota` = **金额字符串**(`"100.00"`,`DECIMAL(12,2)`),**不是** 0~1 比率、不是 number。 `TestDivideTreeQuotaIsMoneyString` 直接断言原始 JSON 片段 `"100.00"`(带引号两位小数)。 ## 三组变异(team-lead 要求的"写错会变红",全部实测) | 变异 | 结果(原文) | |---|---| | 关掉 `assertNestedSetInvariants` | `坏树应返回 5001(数据损坏),实得 200 —— 说明不变式校验没生效` **红** | | `Quota` 改 float64 且输出 0~1 比率 | `quota = 0.01,期望 "100.00"(金额字符串…不是 0~1 比率、不是 number)` **红** | | 流水窗口 `create_time < to` 改 `<=` | `total/list = 2/2,期望 1/1(右开窗口:to 时刻那笔应被排除)` **红** | 三组均"还原即绿"。 ## 另一件事(**必须你裁决,未擅自改动**) 契约 v1.4 §3.4 **A 表**(端点 1/11/12/21)的字段名,与我 AC-12 的实现**不一致**: | 端点 | 契约 v1.4(〔mock〕) | AC-12 现状 | |---|---|---| | 1 | `userId,institutionId,campusId,totalBalance,campusBalance,specialExpenses,cumulativeIncome` | `{userId, global:{balance,spend,totalIncome,…}, campus:{balance,…}}` | | 11 | `id,userId,institutionId,campusId,type,money,balanceAfter,createTime,remark` | `{id,userId,type,typeTitle,logId,money,balanceAfter,remark,createTime}`(**无 institutionId/campusId** —— 流水表没有这两列,契约字段在数据层不可得) | | 12 | `…,applicantId,approverId,createTime` | `…,requestUserId,responseUserId,createdAt` | | 21 | `periodIncome,periodExpense,net,balanceDistribution[]` | `{balance,totalIncome,totalExpenditure,netReceivable,accounts,flowsByCampusSupported,…}` | 契约总原则是「**名字听前端的**」⇒ 按契约我需要改 4 个端点的 DTO;但: - 端点 11 的 `institutionId/campusId` **在 `t_user_expense_log` 里不存在**(§3.8-f 约束 2), 改名字也变不出数据 ⇒ 需要契约侧删字段或改由 user→membership 反查(有性能与语义代价); - 其余三个是纯改名(有前端读取路径影响)。 **按你的纪律"实现与文档冲突先报",我没有静默改。** 等你裁决后我一次性改 DTO(含 handler 用例同步)。 ## 状态 go build / go vet / go test -short ./... 全绿;工作区干净;未 push。
  • 0e7728215a docs(academic): AC-12 收尾——A21 注释改三条 + 补 A21 去重用例 + T-TEST-1 门禁说明 ## 一、`bill_service.go` 注释修正(**这是会引来回归的一类注释**) 原注释写「RIGHT JOIN 的**两个**后果(**必须照抄,不得"修好"**)」,只列了 2 条, 而实现恰好"修好"了第三条(扇出)——注释在说"必须照抄",代码却没照抄, 设计 §3.8-**A21** 已把这个"修好"登记为正确处置(`DUMP-BUGFIX`)。 **若有人读注释把第三条补回去,等于主动引入 A21 已排除的缺陷。** 改为**三条 + 逐条标注处置**: 1. 活动从未产生 used_log ⇒ 其 signed 额完全不计入结余 —— **复刻**; 2. 只有 used_log 无 signed 行 ⇒ signed 侧 NULL,SUM 按 0 计 —— **复刻**; 3. 同活动 k 条 signed ⇒ `activity_money_used` 汇总被**重复计入 k 次**(SQL 扇出) —— **刻意不复刻**(A21 / `DUMP-BUGFIX`,按 `activity_id` 去重各计一次); 并写明真实数据仅 1 例(`user 32596 / activity 1`,两条各 1000.000, cutoff 2020-01-01 时 dump=737.022 / Go=1737.022,差 1000.000)与 **⚠ 不要照抄第 3 条**。文件头的 dump 骨架段也加了同一指向,避免两处表述冲突。 ## 二、补 A21 用例(设计侧标注"必须补用例,现有 36 例零覆盖") `TestGetBillGiveBalanceDeduplicatesSignedRows`:造"1 活动 2 条 signed(各 1000.00)+ used −100.00", 断言 `giveEndBalance = 1900.00`(去重)而**不是 1800.00**(扇出),并显式排除 1800.00 这个值。 目标态有 UK(user_id, activity_id) 造不出重复行,故用例先 `DROP INDEX uk_user_activity` 模拟**旧库形态**(老库无此 UK,正是 A21 那 1 例脏数据的成因)—— 这一点写进用例注释。 **变异实测**:把 `total.Add(usedSum)` 改成 `usedSum.MulInt(2)`(复刻 k=2 扇出)→ `期末赠送结余 = 1800.00,期望 1900.00(…若得 1800.00 说明复刻了 A21 的扇出缺陷)` **红**;还原即绿。 ## 三、`models_test.go`:把 T-TEST-1 写进门禁上方 在 `TestModelsColumnByColumn` 上加说明:**它是精度类问题的唯一防线**,不得跳过、 不得放宽为"仅名字比对"。原因(`tech-debt-backlog.md` **T-TEST-1**):sqlite 的 NUMERIC 亲和性 **不强制 DECIMAL 标度** ⇒ 把 `(6,3)` 改回 `(6,2)` 时插入 `0.999` 依然原样存下, "0.999 舍成 1.00 ⇒ 相邻区间并列命中"这类**精度语义差异在单测里复现不出来**。 由此三条纪律:① 精度差异只能靠真库 e2e / D7 双跑证明;② 防精度回退的断言对象必须是**列类型** (不是计算结果,否则两种精度都绿);③ 本用例必须保持对 `type` 的严格比对。 并注明它抓不住"两边一起改回",后者由 `TestPayrollTeacherExpPrecisionLocked` 兜。 ## 四、`pages` 字段:**已在 `ec174ee` 落地**(本条消息里再确认一次) `BillResult.Pages int \`json:"pages"\`` + `billPages` 纯函数(`PageSize<=0 → 0`;否则整数向上取整); handler 直接透传 `res.Pages`;边界用例覆盖 `0/10→0`、`25/10→3`、`5/0→0`、`11/10→2` 等。 go build / go vet / go test -short ./... 全绿。
  • ec174ee1ec test(academic): 【P0 守护】教龄精度口径锁 + 账单 pages 字段(P1 明细窗口已在 86172ad) ## 一、P0 守护补强:`TestPayrollTeacherExpPrecisionLocked`(新增) **为什么 P0 的修复还缺一道门禁**:P0(`a10707b`,教龄 `decimal(6,2)`→`(6,3)`)依赖的既有门禁 `TestModelsColumnByColumn` 断言的是「struct tag ↔ DDL fixture **一致**」—— 它抓得住"只改了一边",但**抓不住"两边一起改回 (6,2)"**:那时两边仍一致,门禁**照样绿**, 而精度已悄悄收敛(0.999 舍成 1.00 ⇒ 相邻教龄档并列命中 ⇒ 课酬按错档算)。 这正是"守卫本身要能被证伪"(§10.3)的同类问题:门禁的失败形态恰恰是「**恒过**」。 新增口径锁把"3 位"钉死在**两个独立位置**(DDL 声明行 + struct tag 串),任一边被改回都红, **两边一起改也红**。用"列声明行"正则匹配而非全文搜 `DECIMAL(6,3)`:否则只在注释里留一句"6,3" 就能骗过门禁。 ### 「改回红 / 还原绿」实测(变异方式:tag + DDL fixture **一起**回退 (6,2),并同步重算指纹 ### 以排除哈希门禁干扰 —— 这样只剩"语义守护"在起作用) ① 既有门禁 `TestModelsColumnByColumn`:**ok(绿)** ← 盲区实证 ② 新增 `TestPayrollTeacherExpPrecisionLocked`:**FAIL(红)** ``` --- FAIL: TestPayrollTeacherExpPrecisionLocked (0.00s) DDL fixture 中 `teacher_exp_min` 不是 DECIMAL(6,3):精度收敛会把 0.999 舍成 1.00,使相邻教龄档并列命中、课酬按错档算(P0,见本文件头) DDL fixture 中 `teacher_exp_max` 不是 DECIMAL(6,3):…(同上) models_payroll.go 缺少 `column:teacher_exp_min;type:decimal(6,3)`(教龄列 tag 必须是 decimal(6,3)) models_payroll.go 缺少 `column:teacher_exp_max;type:decimal(6,3)`(同上) ``` ③ 语义用例 `TestPayrollRepoTeacherExpThreeDecimalScale`:**ok(绿)** —— sqlite 的 NUMERIC 亲和性 **不强制 decimal scale**,所以"(6,2) 把 0.999 舍成 1.00"这一形态在本仓单测栈**物理上无法复现**。 这就是为什么精度守护必须落在 ② 这种 schema/口径锁上,而不是靠语义用例。 还原 (6,3) 后:① ② ③ 全绿。**结论:P0 现在有了能自证失败的门禁。** ## 二、`pages` 字段(契约 v1.4 rev.2:端点 17 `data` 平铺五字段齐全) - `BillResult` 新增 `Pages int \`json:"pages"\``;新增纯函数 `billPages(total int64, pageSize int) int`: `pageSize <= 0 → 0`(防除零)、`total <= 0 → 0`、否则**整数向上取整** `(total+size-1)/size` (**不用 `math.Ceil(float64)`**:1e16 量级会丢精度,且多一条 import)。 - **入参用"窗口内 total + 钳制后 pageSize"**,不是请求原始值与历史全量。 - handler 改为直接用 `res.Pages`,**删掉本地重复取整**(同一份分页元信息若有两处算法, 迟早出现"某处四舍五入、某处向上取整");顺带把明细数组键名对齐为 **`details`** (以 `BillResult` 的 json tag 为准;契约此前两版对"嵌套/键名"各错一半,本次按代码定稿)。 - 用例:`TestBillPagesBoundaries`(`0/10→0`、`25/10→3`、`10/10→1`、`11/10→2`、`5/0→0`、`-1/10→0`); `TestGetBillPagesFollowsWindowedTotal`(窗口内 2 条 / pageSize 2 → 1 页;请求 pageSize=5000 被钳到 200 后 pages 仍按 200 算);handler 侧断言 `total=1 → pages=1` 且 `total != 历史 3 笔` (后者专门抓"只改 PageLogs 不改 CountLogs"的改一半形态)。 go build / go vet / go test -short ./... 全绿。
  • View comparison for these 34 commits »

13 hours ago

Q pushed to master at yuansiwei/DEV-MERGE-LEGACY-PC-FRONTEND-2026

  • d08c5d435a style: 落地 PC 端品牌设计 token(对齐 Logo 配色) - variables.scss 追加品牌 6 色 + 派生语义/侧栏/状态色/涨跌/浮层阴影 (基础 token 原值不动,只增不删) - 新建 element-plus-theme.scss:覆盖 EP CSS 变量(primary/success/ warning/danger + 6 个 -rgb 变体 + 边框/文字/背景/阴影/圆角) - main.ts 引入 EP 主题(紧跟 variables 之后) 设计源:工作区 frontend-pc/00-design/design-tokens.md v1.1「青屿」 (Logo 取样;原型 prototype/index.html 可交互预览)。 门禁:pnpm build 0 错 / vue-tsc exit 0 / 品牌覆盖实证在主 CSS 且晚于 EP 默认值。

18 hours ago

Q pushed to master at yuansiwei/DEV-MERGE-LEGACY-BACKEND-2026

  • 027c26377f feat(rbac): S1 引入 Casbin v3 策略引擎 + 自写 GORM Adapter(第②层接口强制) - casbin/casbin/v3@v3.8.1(弃官方 gorm-adapter,仅 +doublestar/govaluate 传递依赖,无 DB driver) - internal/infra/rbac/casbin_model.conf: RBAC with Domains(sub=角色码/dom=机构/obj=path keyMatch2/act=method regexMatch;embed 内嵌) - internal/infra/rbac/enforcer.go: 自写 gormAdapter(persist.Adapter 6 方法 + ruleRow 映射)+ NewEnforcer/Reload/Get - internal/infra/rbac/enforcer_test.go: 单测 5 项全 PASS(model.conf 解析/无 g 自匹配/白名单矩阵/regexMatch/keyMatch2 通配) - 配套 DDL: core-model-ddl.sql v1.3 已加 casbin_rule 表 + 策略种子 14 条 + ERD(12→13 表) 设计: unified/01-architecture/casbin-rbac-design.md v1.2 验证: go build ./... 通过 / go vet 通过 / go test 5 项 PASS

20 hours ago

Q pushed to master at yuansiwei/DEV-MERGE-LEGACY-BACKEND-2026

  • 5afb640731 fix(identity): B4 打通 phone+密码 登录链路(Handler 解锁 + e2e 合并门禁) 根因:Handler.Login 硬编码 IdentityTypeUsername,phone 身份进不了登录入口 (Service 层 AuthService.Login 早已支持 phone:isValidPhone + (phone,号,"") 唯一键命中)。 改动: - LoginReq 增 Phone 字段;username/phone 用 required_without 交叉约束(至少一个非空), phone 类型 max=11 兜底(权威 11 位校验在 Service isValidPhone) - Handler.Login 按 phone 存在与否选 identityType(同传时 phone 优先), 与 username 共用同一套两步①②协议 + 统一 RS256 token - 新增 seedPhoneUser + TestPhoneLoginE2E(全链路 login①→issue②→me→重放 1001) + TestPhoneLoginE2E_BadParam(非 11 位 → 2001) 验收:go build/vet ./... 全绿;go test ./... -short 全绿;真库 e2e_test 跑 TestPhoneLoginE2E(_BadParam) PASS + identity/org 各包 E2E 全 PASS(无回归)。 注:identity 与 org 两包 e2e 共享 e2e_test 库的同名表,并行跑会 DDL 互撞 (既有现象,非本次引入),逐包单独跑全绿。 合并门禁含 phone 登录 e2e(B4 拍板:合并前打通)。

2 days ago

Q pushed to master at yuansiwei/DEV-MERGE-LEGACY-BACKEND-2026

  • 870dc89f83 fix(migrate): 真库 e2e 验收发现两处缺陷并修复(O(N) 逐行 COUNT + 归一漏剔不可见字符) TestMigrateE2E 真库验收(yuansiwei_i 68107 用户 → e2e_test,首次 ~64s) 暴露两处已交付代码的真实缺陷: 1. 性能 O(N):writeUser/writeIdentities/upsertIdentity/writeWechatUnion/ writeLegacyMapping/writeMemberships 逐行 SELECT count(*) 查重(68107 用户 ≈ 20 万+ 串行往返,10 分钟跑不完;生产割接窗口 PolarDB 上不可接受)。 修复:事务内一次性批量装载 5 表现有唯一键到内存 set(existingUserCanons/ existingIdentityKeys/existingUnionids/existingMappingCanons/existingMemberKeys), 写前 O(1) 查表,插入成功后 mark-on-insert 维持"首个占用者胜出"语义; 往返量减半,68107 用户单次迁移 ~60s。 2. 归一缺口(生产炸事务风险):legacy username 含 bidi 控制符(实测 yuansiwei_i t_user id=44008 = U+202D + 号码 + U+202C 包裹,全库仅此 1 条)→ norm.Username (lower+trim)不剔(Go TrimSpace 不把 Cf 类当空白)→ 与干净账号字节不同、绕过 首占用去重双双写入 → MySQL utf8mb4_0900_ai_ci 视同值,verify 判 username 重复; 生产 t_user 带 UNIQUE uk_username,第二行将撞唯一键回滚整个迁移事务。 修复:norm 增 stripInvisible(剔 Cc/Cf/Cs:bidi 控制/零宽/软连字符等), Username 与 Email 均套用(二者都进 t_identity 唯一键);Phone 有 11 位硬正则 兜底不受影响。归一后 44008 与 42020 收敛同串 → 命中首占用规则(后者 username 置 NULL 转 P0-08),行为与 phone 撞号一致。 3. verify.go 隐性失效修复:原 4 项唯一键完整性检查用 GORM Find(&struct) 触发 SELECT * + GROUP BY,MySQL 8 ONLY_FULL_GROUP_BY(生产 PolarDB 同款)报 Error 1055,而 `if err == nil` 静默跳过 → 门禁形同虚设误报 PASS。 改 raw SQL 仅 SELECT 分组列 + 出错即 return error 硬失败。 回归单测:norm 增 6 例(bidi 包裹/零宽空格/零宽非连接符+软连字符/双向嵌入/ 碰撞收敛断言/email 剔不可见)。 验收:TestMigrateE2E 真库全绿——首次 run 用户 68107/机构 23/校区 62/ identity phone=39125 wechat=3406/union 3383/成员 26059/映射 68107; verify 全过(无 phone 撞号/username 重复/unionid 重复/canonical 重复/孤儿); 重跑幂等 UsersCreated=0 / UsersSkipped=68107 / MembershipsCreated=0。 报告残留:phone 撞号 27 项 + phone_invalid 25 项 + 微信孤儿 8 项(均转 P0-08 人工, 符合设计);membership 孤儿绑定警告 210 条(legacy userid 不在 t_user,跳过)。

2 days ago

Q pushed to master at yuansiwei/DEV-MERGE-LEGACY-BACKEND-2026

  • 38f6ee646b test: T8 测试基线固化(credential/id/scope 单测 + CGO 端口性) 新增 3 个零测试包的单元测试(17 例,全绿): - internal/identity/credential:argon2 往返 / md5_salt / sha_salt 路由 + 惰性升级标记、hex 大小写折叠、未知 version、空哈希 7 例 - internal/identity/id:ULID 生成(26位+Validate) / 时间序 / Crockford 校验 (拒 I/L/O/U) / 同毫秒 1000 唯一性 4 例 - internal/infra/scope:DryRun 捕获 SQL 验证 scoping 注入 (普通双过滤 / 机构级仅 inst / 超管零过滤 / nil panic / campus=0 不加校区) + fiber Set/FromFiber 局部值往返 6 例 CGO 端口性:scope DryRun 句柄选用 glebarez/sqlite(纯 Go,modernc, CGO_ENABLED=0 可编译可运行),替代 mysql(Open 时 ping TCP)与 gorm.io/driver/sqlite(mattn CGO,CGO=0 为 stub)。 go test ./... -short 在 CGO_ENABLED=1 与 =0 双模式均 0 失败。
  • e579b3e4b2 fix(migrate): t_legacy_mapping 补 legacy_teaching_user_id(P1-05 §2 三源语义拍定) 种子 16077 用户的阅卷/标记系统 id(t_user_account_association.marking_system_userid) 落 legacy_teaching_user_id;legacy_i_user_id=t_user.id 不变;legacy_academic_user_id 本地恒 NULL。 对齐 P1-05 字段覆盖矩阵 v2 §2 拍定的三源归属(收口 P0-07' I 侧' vs P0-13'marking_v2_db' 命名差异)。 门禁:go build/vet 0 错误,单测全绿。
  • c754c2161f feat(migrate): P1-04/T5 数据迁移工具(幂等 upsert + 事务 + 置信分级) - internal/migrate/norm: 标识归一纯函数(Phone 强归一 11 位/WechatOpenID 仅 trim/Email 小写/Username 小写)+ 单测 - internal/migrate: 迁移引擎 - source.go: legacy 双库只读数据源(yuansiwei_i 教学 + finance_bk 教务,7 表读取) - match.go: 置信分级匹配引擎(seed 100/phone 90/unionid 70/unmapped 50/conflict 0 + 撞号检测) - runner.go: 单事务编排(机构→校区→用户/身份/锚点/映射→成员;ULID 复用幂等 + phone/username 全局唯一键冲突处理) - verify.go: 目标态一致性校验(计数对账 + 唯一键完整性 + 引用完整性) - report.go: 迁移报告(含 P0-08 冲突/异常清单) - migrate_e2e_test.go: 真库端到端(首次 run + verify 唯一键 + 重跑幂等断言,DSN 走 UNIFIED_MIGRATE_*_DSN 环境变量,缺失即 skip) - cmd/migrate: CLI(dry-run/run/verify,敏感 DSN 走环境变量不入库) 门禁:go build/vet 0 错误,单测全绿(norm 12 + match 3 组断言),e2e 无 DSN 时正确 skip P1-04 验收:重跑幂等(ULID 复用 + 唯一键去重)+ 事务边界(单事务提交/回滚)
  • View comparison for these 3 commits »

2 days ago

Q pushed to master at yuansiwei/DEV-MERGE-LEGACY-BACKEND-2026

  • 665e7eb479 fix(identity e2e): seedWechatUser 补 AppID 对齐 v1.2 唯一键 v1.2 把 t_identity 唯一键扩成 (type,identifier,appid),wechat_service callback 改传 campus.AppID 查询。但 TestWechatE2E 的 seedWechatUser 只落 AppID 零值 "",而 WechatCampus 配 AppID="appid-e2e",callback 按 appid=appid-e2e 查不到 appid="" 的种子行 → 误返 unbound。 go test ./... -short 跳过 e2e(无 UNIFIED_TEST_DSN),上轮 commit 门禁 未捕获,跑真库 e2e 才暴露。修复:e2eApp 增 wechatAppID 字段(=campus AppID),seedWechatUser 用它给 wechat 身份补 AppID,使 seed 与 campus 配置一致。 门禁:build/vet/short 全绿;真库 e2e(infra/db reconcile + identity handler 全链路 + org handler + identity repo)逐包 -p 1 串行跑 PASS (并行跑会因共享 e2e_test 表建/删竞态误报,非代码缺陷)。
  • ee4ab1dfb7 feat(identity): core-model-ddl v1.2 表结构对齐 拍板 18 项(2026-11-07)触发的 DDL v1.1→v1.2 GORM model 对齐: - t_identity 唯一键 (type,identifier) → (type,identifier,appid) · Identity.AppID *string → string(NOT NULL DEFAULT '') · 三列加 uniqueIndex:uk_type_identifier_appid (priority 1/2/3) - 新增 t_wechat_union(unionid 跨 appid 归一锚点,第 12 表) · WechatUnion struct + AllTables() 加表 - IdentityRepo.GetByTypeIdentifier → GetByTypeIdentifierAppID · 签名 +appid 参数(非微信 ,微信必填) · interfaces.go 接口同步更新 · auth_service 传 ;wechat_service 传 campus.AppID - 测试对齐:mock 三级 map (type,identifier,appid); identity_repo_test 改名 + 传 appid; db_test 加 t_wechat_union 列断言 + DropTable 加表 - e2e AutoMigrate 对账 12 表全绿;go build/vet/test 全绿
  • View comparison for these 2 commits »

2 days ago

Q pushed to master at yuansiwei/DEV-MERGE-LEGACY-MOBILE-FRONTEND-...

  • 03cdb0027f feat(teacher): 移动端 App 内微信 webview 授权登录(snsapi_userinfo) - shared wechat.ts: 新增 fetchWechatAuthorize(GET /api/auth/wechat/authorize → {state, authorizeUrl, campusId, expiresIn})+ fetchWechatCallback(GET /api/auth/wechat/callback ?code=&state= → status+loginTicket+memberships);注释区分 PC 扫码(snsapi_login/qrconnect) 与移动 webview(snsapi_userinfo/oauth2/authorize),callback 两 scope 共用 - teacher login/index.vue: 微信 tab 由占位改真实流程——检测 UA MicroMessenger → fetchWechatAuthorize → location.href 整页跳 authorizeUrl(非微信环境提示); 成功授权后微信重定向回 /wechat/callback 落地页 - 新增 views/wechat/callback.vue: 公开落地页,读 code/state → fetchWechatCallback 换 loginTicket → 自动取 memberships[0] 走 ② issue → applyLogin → scope.set → 跳首页; unbound/fail/缺参/网络异常 直显 message + 返回登录;已登录幂等跳首页 - router: 新增 /wechat/callback 公开路由(meta.public + scopeRequired:false,守卫①放行) - 门禁: typecheck(×2)/lint/build 全绿 + dist grep 无 Vue 运行时内部标识

3 days ago

Q pushed to master at yuansiwei/DEV-MERGE-LEGACY-BACKEND-2026

  • 058da50f3f feat(identity): 移动端 App 内微信 webview 授权端点 /auth/wechat/authorize - wechat.go: BuildQRConnectURL 重构为 scope 参数化 BuildAuthorizeURL + 常量 ScopeLogin(snsapi_login, PC 扫码, connect/qrconnect + #wechat_redirect) / ScopeUserinfo(snsapi_userinfo, 移动 webview, connect/oauth2/authorize 无尾段); BuildQRConnectURL 保留薄封装给 PC 链路 - service: 新增 WechatService.Authorize(复用 newState/SetState 写 Redis pending, 返 AuthorizeResult{state, authorizeUrl, campusId, expiresIn},URL 用 ScopeUserinfo) - handler: 新增 WechatAuthorize(wechat==nil→4004) - main.go: 挂公开路由 GET /auth/wechat/authorize - 单测: TestBuildAuthorizeURL_Userinfo/_Login、TestWechatAuthorize_Success/ReuseCallback、 TestWechat_NotConfigured 加 authorize;PC 扫码 BuildQRConnectURL 行为不变 - 与 PC 扫码共用同一 state 状态机 / 同一 /auth/wechat/callback 换 openid / 同一套 ② issue

3 days ago

Q pushed to master at yuansiwei/DEV-MERGE-LEGACY-MOBILE-FRONTEND-...

  • 83a83f1aef auth: 移动端对齐两步登录契约 + roles string[] + org 出参 - shared/api/auth/login.ts:单步 LoginResult 改两步——LoginStep1{loginTicket,user, memberships[]} + TokenPair{accessToken,refreshToken,expiresAt,user,roles:string[], institutionId,campusId,menuTree,permissions};新增 issue();roles number[]→string[]。 - shared/store/login.ts:applyLogin 入参 LoginResult→TokenPair;roles Ref<number[]>→ Ref<string[]>;tabTree 改从 data.menuTree 取(后端 ② 下发字段名为 menuTree,旧 tabTree 字段名错误);realName 从 data.user.realName 取。 - shared/api/auth/wechat.ts:占位 per-campus authorize 改真实契约——GET qrcode 返 {state,qrUrl,campusId,expiresIn};GET status 返 {status:pending|success|unbound|fail, loginTicket?,memberships?,expired?};success 不再直发 token,改为回 loginTicket+ memberships 复用两步②。 - teacher login/index.vue:账密改两步(① login→自动取 memberships[0] 立即② issue→ applyLogin+scope.set→跳首页;memberships 空提示联系管理员);微信 tab 改诚实占位 (后端微信扫码为 PC 端扫码流程,移动端 App 内授权方案待拍板,不做假轮询)。 - teacher org/institution.ts + list.vue:InstitutionDTO 出参对齐后端 model(institutionName/ institutionCode?/address?/phone?/status:'active'|'disabled'/createdAt/updatedAt), 列表渲染 name/code/status:number 同步改。 - 门禁:typecheck(×2)/lint/build 全绿;dist grep 无 Vue 运行时(shims 经 globalThis.Vue, 231kB 大 chunk 为 Vant(npm 编译)+业务逻辑)。

3 days ago

Q pushed to master at yuansiwei/DEV-MERGE-LEGACY-PC-FRONTEND-2026

  • b629606fb8 auth: PC 微信扫码对齐后端真实契约(qrcode 编码 + 复用两步②) - wechat.ts:占位契约改真实契约——GET /auth/wechat/qrcode 返 {state,qrUrl,campusId,expiresIn}; GET /auth/wechat/status 返 {status:pending|success|unbound|fail,campusId,loginTicket?,userId?, memberships?,expired?}。success 不再直发 TokenPair,改为回 loginTicket+memberships。 - login/index.vue:qrUrl 是微信网页授权 URL(非图片),前端用 qrcode 库编码成二维码图渲染; 抽出 completeStep2(loginTicket, memberships) 统一②分流(单条自动 issue / 多条 applyStep1→ /org/scope / 空提示),账密与微信扫码复用同一套②;status 状态机 pending/success/unbound/ fail/expired 各自处理(success 走②、unbound/fail/expired 停轮询+提示)。 - deps:+qrcode(运行时业务依赖,Vite 打进业务层,非框架 UMD) +@types/qrcode。 - 门禁:typecheck/lint/build 全绿;dist grep 无 Vue 运行时(shims 转发层经 globalThis.Vue, 登录 chunk 11.76kB 仅含 qrcode 编码 + 业务逻辑,未泄漏 ~400kB 运行时)。
  • 5660212bf3 auth: PC 接入 /auth/me(刷新后恢复 roles/menuTree/scope) 刷新后 token 三件套在 token-store,但 roles/permissions/menuTree 是② 下发的内存态(不落盘)。 凭有效 access 调 GET /auth/me 按已验签 claims 回读用户+作用域+按角色重新解析菜单/权限, 恢复响应式视图: - api/auth/me.ts 新增:fetchMe()→R<MeResult>(MeResult extends LoginUser + roles/scope/ memberships/menuTree/permissions,对齐后端 identity MeResult;启动场景 timeout 5s) - store/login.ts 新增 restoreMe()(in-flight 单例防并发导航重复请求;同步 realName/roles/ permissions/menuTree + 设 scope;code=3001 用户被删→强制登出)+ restored 标记 (applyLogin/restoreMe 成功置 true,hydrate/doLogout 置 false) - router/index.ts 守卫加 ①.5 视图恢复步:已登录但 !restored 且需 scope 的业务页,先 await restoreMe() 再走 ④ 作用域判定;scope-select(scopeRequired=false)跳过 门禁:vue-tsc typecheck 0 错 + eslint 0 错 + vite build 通过。
  • View comparison for these 2 commits »

3 days ago

Q pushed to master at yuansiwei/DEV-MERGE-LEGACY-BACKEND-2026

  • e2a00041b3 feat(identity): 微信扫码 success 回显 memberships(复用两步②分流) status/callback success 出参增 membersips(机构/校区/角色),与账密登录 LoginStep1 同源——前端拿到 loginTicket + memberships 后即可走**同一套** ② /auth/issue 分流(单条自动②、多条进作用域选择页),不新增"微信专属 直发 token"链路。 - redis: WechatState 载荷增 memberships(冗余自 t_membership,使轮询与 callback 两条读法一致、5min 窗口内免回查 DB);新增 MembershipInfo (infra 自持最小 DTO,不反向依赖 identity) - service: WechatService 增 memberships 仓储依赖;Callback 已绑定分支 取 ListByCanonicalUserID 写入 state;Status 回显转 MembershipView; 新增 toMembershipInfos / toMembershipViewsFromState 两个映射(后者 与 auth_service 的 toMembershipViews 区分,避免同包重名) - main/测试装配注入 Memberships 仓储;handler 测试 success 断言 memberships
  • a5fae3ac12 feat(identity): 微信扫码登录端点(Redis 状态机 + 复用两步②票据) 新增 /auth/wechat/qrcode(生成 state+二维码URL)、/auth/wechat/status?state= (轮询状态机 pending/success/unbound/fail/expire)、/auth/wechat/callback (微信重定向落点,code 换 openid→查 t_identity(wechat)→已绑定发 loginTicket)。 扫码成功后前端携 loginTicket 走**同一套** ② /auth/issue 签 token,不重造签发逻辑。 - infra/redis: 新增 WechatQrcodeStore(wxqrcode:{state} 状态机,GET/SET 可变载荷, fail-closed;与 loginticket: 前缀对称,双实例共享) - identity/wechat: 第三方客户端封装(Client 接口 + HTTPClient code 换 openid, httptest 可打桩;BuildQRConnectURL 构造 open.weixin.qq.com 扫码授权 URL) - identity/service: 新增 WechatService(Qrcode/Status/Callback 状态机编排, 复用 LoginTicketStore 发 ticket;依赖倒置 wechat.Client 供单测注入) - handler: WechatQrcode/WechatStatus 由占位改真实实现 + 新增 WechatCallback; Handler.Config 增 Wechat(可选,nil 时端点返 4004 未配置,不阻断账密登录) - config: WeChatCfg 增 CallbackURL(对外回调地址,环境变量 CONFIG_WECHAT__CALLBACKURL) - errors: 新增 CodeThirdNotConfigured=4004(第三方服务未配置) - main: 仅当配齐 per-campus appid + 回调地址时装配 WechatService(fail-safe) 测试:wechat 客户端 6 项(httptest 打桩)+ redis 状态机 6 项 + handler 9 项 (miniredis,含 callback→issue 复用②全链路/防重放/未配置4004)+ DB-gated e2e 2 项 (TestWechatE2E 真库 code→openid→真查 t_identity→success→复用②签 token)。
  • View comparison for these 2 commits »

3 days ago

Q pushed to master at yuansiwei/DEV-MERGE-LEGACY-PC-FRONTEND-2026

  • bf9931c8cd auth: PC 登录契约对齐后端两步协议 + roles string[] 对齐后端 identity ADR-014 / T4 两步登录契约(原前端按单步假设,roles 误为 number[]): - api/auth/login.ts 拆两步:login()→LoginStep1{loginTicket,user,memberships[]}、 issue()→TokenPair{accessToken,refreshToken,roles:[]string,institutionId,campusId, menuTree,permissions};删旧单步 LoginResult(number[] roles) - store/login.ts:roles: number[]→string[];新增 applyStep1(暂存票据+作用域列表, 内存过渡态不落盘)+ pendingTicket/pendingMemberships;applyLogin(TokenPair) 同步 roles/permissions/menuTree + 设 scope;doLogout/hydrate 清过渡态 - views/login/index.vue:① 验账密后单条 membership 自动 ② issue→跳机构;多条→ applyStep1→跳作用域选择页;空 memberships 提示联系管理员 - views/org/scope-select.vue:由硬编码机构选项改为接 pendingMemberships,选定后 issue(pendingTicket, 作用域)→applyLogin;pendingTicket 丢失引导回登录页 - router/index.ts:守卫 ① 加 ①→② 过渡态例外(pendingTicket 在手未签 token 时放行 ScopeSelect),避免票据在手却被 isLoggedIn 判定踢回登录页 - api/auth/wechat.ts:占位 token 类型 LoginResult→TokenPair(后端就绪前占位) 门禁:vue-tsc typecheck 0 错 + eslint 0 错 + vite build 通过(dist 无 Vue 运行时打包, define 注入 Vue.* 全局生效)。
  • c8d23a1327 org: PC 前端 institution 出参对齐后端 model.Institution - InstitutionDTO 字段对齐 GORM 出参:id→institutionId、name→institutionName、 creditCode→institutionCode(可选)、新增 address/phone(可选)、status 由 number(0/1) 改为字符串枚举 'active'|'disabled'、补 updatedAt - InstitutionFilter.status 由 number 改为 'active'|'disabled' - list.vue 列 prop 改 institutionName/institutionCode,状态筛选值改字符串枚举 - formatStatus 入参 number→string,匹配 VARCHAR(16) status - ListPage.vue 文档示例同步更新
  • View comparison for these 2 commits »

3 days ago

Q pushed to master at yuansiwei/DEV-MERGE-LEGACY-BACKEND-2026

  • 460da46dc8 fix(identity e2e): BadPassword 用合法长度错密,真正走到防枚举 1001 路径 原测试 password="wrong"(5 位)被 validate min=6 拦成 2001,从未走到 "错密→1001 防枚举统一文案"路径(DB-gated e2e 无 DSN 时 skip,CI 未捕获)。 改为 ≥6 位合法长度错密,使该用例真正覆盖防枚举契约。

3 days ago

Q pushed to master at yuansiwei/DEV-MERGE-LEGACY-BACKEND-2026

  • cef0d125ae feat(org): T6.5-e 超管角色-权限点矩阵配置端点 + 缓存联动 - service: RolePermissionService(平台级,不走 scoping)List/Save; validateRole(∈t_role)+validateCodes(∈t_permission) 防脏 code 污染矩阵; 保存后 Resolver.Invalidate() 清本实例缓存(跨实例靠 TTL 30s) - handler: RBACHandler(GET/PUT /org/role-permissions/:roleCode)+ saveRolePermReq(空 body=清空矩阵;非 JSON→2002) - main:独立兄弟组 /org/role-permissions 挂 RequireRole("super_admin") (非超管 1003,超管恒放行;与 /org staff 白名单组并列不继承) - e2e:TestOrgE2E_RolePermConfig(非超管 1003 / 读取 4码+字典 / 全量替换 4→2 码 / 含未知码 2001 / 非法 roleCode 2001 / Invalidate 联动) build/vet/test 全绿(DB-gated e2e 未设 DSN 时 skip)。
  • be1630c3eb feat(identity+org): T6.5-d issue/me 下发 menuTree/permissions + 路由角色白名单 - identity/service:MenuProvider 接口(依赖倒置,复用 rbac.MenuNode 类型)+ 可选注入;Issue/Me/Refresh 经 resolveMenu 下发 menuTree + permissions (nil 兜底空集,不阻断登录;3 项单测:注入/超管全量/nil 空集) - identity/handler/rbac.go:RequireRole(roles...) 路由角色白名单中间件 (超管 SuperAdmin 恒放行 / 角色命中放行 / 不符 1003 / 无 scope 1001) - org/handler e2e:seedRBAC 最小集权限点+矩阵 + 路由挂 RequireRole (academic/finance/teacher)+ 2 项 DB-gated e2e(角色不符 1003 + 真 DB menuTree 解析) - main:rbac.Resolver 注入 MenuProvider + 路由挂 RequireRole + 编译期断言 build/vet/test 全绿(DB-gated e2e 未设 DSN 时 skip)。
  • 541f2e2784 feat(org): T6.5-b/c RBAC model + 平台级仓储 + 菜单树解析 - model: Permission/RolePermission 两表 + AllTables() 扩到 7 表 - repository: PermissionRepo(平台级,不继承 BaseRepo)AllActive/ RolePermissionCodes/SaveRolePermissions(删旧插新事务) - rbac: Resolver(role→权限点 code 集→菜单树)+ MenuNode(对齐 PC 契约)+ 进程内 TTL 30s 缓存 + Invalidate;Store 接口使 fake 可注入 - 测试: rbac 7 项纯单测(超管全量/普通角色/空矩阵/父缺失提升/空树非nil) build/vet/test 全绿。
  • View comparison for these 3 commits »

3 days ago

Q pushed to master at yuansiwei/DEV-MERGE-LEGACY-BACKEND-2026

  • b8f4538b68 feat(org): T6 机构/校区仓储 + 强制 Scoping + 越权拦截 + /org 端点 org 域只读落地(架构规格 §3.5 多租户防线): - infra/scope:scoping→Scoping 导出(跨包组合调用须导出方法,修复 org repo "cannot refer to unexported method" 编译错误);BaseRepo 增 CampusScoped 标记, 机构级表(t_institution 无 campus_id 列)只加 institution_id 过滤, 避免 campus_id Unknown column。 - org/repository:InstitutionRepo/CampusRepo 继承 BaseRepo 强制 Scoping, PagedList + Get(GORM 纪律:Count *int64 / NotFound→ErrNotFound / nil 切片非 nil)。 - org/service:OrgService.assertInScope(请求显式 id 与 token 作用域冲突→code 1004, 服务层第一道防线)+ List/Get 编排(仓储层 Scoping 为第二道纵深防御)。 - org/handler:/org/institutions[/:id] + /org/campuses[/:id],scope 经中间件注入。 - cmd/api:org 装配 + /org 路由(经同一验签中间件;公开路由铁律 8 不受影响)。 - 测试:assertInScope 纯单测 + 越权 e2e(DB-gated,跨机构/跨校区被拒 1004、 数据层 Scoping 只给本机构/本校区、超管跨机构全见、无 token 1001)。 build/vet/test 全绿;测试函数 99(org 域 5,其中 4 e2e DB-gated 未设 DSN 则 skip)。

3 days ago