From 7b1c94b5ff422209facd36c945e31b53a41a5cbf Mon Sep 17 00:00:00 2001
From: lzq <357099073@qq.com>
Date: Thu, 5 Nov 2026 18:28:46 +0800
Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=BB=A3=E7=A0=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.trae/documents/fix-login-infinite-loop.md | 51 ++++++
backend/src/api/oaScheduling.js | 5 +
backend/src/views/apps/oa/schedule/index.vue | 145 +++++++++++++++++-
go/controllers/backend_oa_scheduling_board.go | 61 ++++++++
go/routers/backend/backend.go | 1 +
5 files changed, 261 insertions(+), 2 deletions(-)
create mode 100644 .trae/documents/fix-login-infinite-loop.md
diff --git a/.trae/documents/fix-login-infinite-loop.md b/.trae/documents/fix-login-infinite-loop.md
new file mode 100644
index 0000000..e46d1d5
--- /dev/null
+++ b/.trae/documents/fix-login-infinite-loop.md
@@ -0,0 +1,51 @@
+# 修复登录无限循环(登录成功后弹回登录页)
+
+## Context(问题与证据链)
+
+用户反馈:登录成功后又回到登录界面,无限循环。排查已确认(含用户亲证):
+
+**用户确认的现象**
+- 登录页地址栏为 `api.yunzer.cn`(远程线上认证中心,非本地 9000)
+- 登录后见过「选择要进入的企业」界面(hero920103 绑定 2 家企业),**选完企业后又弹回登录页**
+
+**数据库证据(go-platform 库)**
+- hero920103 = identity_id 2(李志强),绑定两家企业(tid=1、tid=2)→ 登录必走"待选企业"分支
+- `yz_auth_session`:18:09~18:12 连续 4 条会话被踢(revoked=1, revoke_reason='kicked');最新会话 id=386(18:18:29,tid=2,client_id 为空)
+- 18:24:42 登录日志(id=218,user_id=1)与 hero920103 实际 identity_id=2 不符 → **该日志由远程旧版代码写入**(字段语义不同),且 IP=127.0.0.1/::1 符合"远程 nginx 反代回环"特征
+
+**代码机制(go/services/auth/session.go L61-134)**
+- `CreateSession` 按 identity_id **全局**统计活跃会话,超 MaxSession 即踢最旧会话(kicked),不区分 tid/client/服务实例
+- 登录 → 远程建会话 → 企业选择 → 新会话又创建 → 互踢 → authorize 发现会话已吊销 → 送回登录页 → 再登录 → **无限循环**
+- 远程为旧版代码(字段错位可证),其企业选择后的会话处理存在缺陷,无法本地修复
+
+## 修复方案
+
+### 第一步:本地开发认证切换到本地 9000 实例(核心修复)
+
+修改 `backend/.env.development`:
+
+```
+VITE_AUTH_BASE='http://127.0.0.1:9000/auth'
+```
+
+本地 9000 已注册全套认证端点(/auth/login、/auth/authorize、/auth/token、/auth/userinfo、/auth/switch-tenant,见 `go/routers/auth/auth.go`),且为最新代码(企业选择、cookie、PKCE 逻辑完整)。
+
+### 第二步:配套校验与清理
+
+1. 核对 `yz_auth_client` 表 yz-backend 的 redirect_uris 白名单包含 `http://127.0.0.1:4401/auth-callback.html`(不匹配则用 MySQL MCP 补登)
+2. 清理脏数据:将 hero920103(identity_id=2)名下 revoked=0 的旧会话标记 revoked=1(reason='cleanup'),避免旧会话占用互踢名额
+3. 重启 Vite dev server 使 env 生效
+4. 验证本地登录页 HTML 表单嵌入 client_id(curl 抓取确认,防止空 client 会话再现)
+
+### 第三步:端到端验证
+
+1. 无痕窗口(避免残留 localStorage token/userInfo)访问 `http://127.0.0.1:4401`
+2. 完整走:跳本地登录页 → 输入 hero920103 → 企业选择(展示 2 家)→ 选 tid=2 → 回跳 auth-callback → 进入工作台,无循环
+3. MySQL MCP 复查 `yz_auth_session` 无异常互踢
+4. 登录恢复后,验证原任务功能:apps/oa/schedule 日历按 hero920103 排班显示上班(绿)/休息(橙)背景(后端 `/backend/oa/scheduling/my` 已就绪)
+
+## 涉及文件
+
+- `backend/.env.development`(唯一代码修改:VITE_AUTH_BASE 指向本地)
+- 数据库:`yz_auth_client`(白名单核对)、`yz_auth_session`(脏数据清理)
+- 不改 Go 代码(互踢规则是合理安全设计;根因是开发环境认证指向了旧版远程中心)
diff --git a/backend/src/api/oaScheduling.js b/backend/src/api/oaScheduling.js
index 0ff0ff7..7c82280 100644
--- a/backend/src/api/oaScheduling.js
+++ b/backend/src/api/oaScheduling.js
@@ -34,6 +34,11 @@ export function getScheduleBoard(params) {
return request({ url: '/backend/oa/scheduling/board', method: 'get', params })
}
+// 当前登录用户的排班(按账号匹配员工档案),供日程日历显示上班/休息背景
+export function getMyScheduleBoard(params) {
+ return request({ url: '/backend/oa/scheduling/my', method: 'get', params })
+}
+
// 批量按格子编辑排班:cells 为排班板多选的「员工 × 日期」任意组合
// shift_id>0 设为该班次(有排班=调班,无排班=新增);shift_id=0 清除(休息/请假)
export function saveScheduleEntryCells(data) {
diff --git a/backend/src/views/apps/oa/schedule/index.vue b/backend/src/views/apps/oa/schedule/index.vue
index 2c4251d..d94077d 100644
--- a/backend/src/views/apps/oa/schedule/index.vue
+++ b/backend/src/views/apps/oa/schedule/index.vue
@@ -78,6 +78,12 @@
>
今天
+ 当前账号暂无排班
{{ cell.holidayName }}
@@ -164,6 +179,28 @@
周末
+
+ {{ selectedMyEntry.short_name || selectedMyEntry.shift_name }}
+ {{ selectedMyEntry.work_start }}-{{
+ selectedMyEntry.work_end
+ }}
+
+ 休息
{{ selectedItems.length }} 项日程 mySchedMap.value[selectedDate.value] || null);
+
// ---------- 中国法定节假日 / 调休 ----------
// 后端返回 { 'YYYY-MM-DD': {name, type, wage} },type: 0 调休上班 / 1 法定放假。
// 普通周末不在此表内,由 isWeekend 按星期判定,避免与节假日数据重复。
@@ -643,6 +714,7 @@ const calendarCells = computed(() => {
start.getDate() + i
);
const date = fmtDate(d);
+ const my = mySchedMap.value[date];
cells.push({
date,
day: d.getDate(),
@@ -652,7 +724,10 @@ const calendarCells = computed(() => {
items: map[date] || [],
// 节假日相关:tag 用于格内角标与整格底色,holidayName 用于节日名文案
tag: dayTag(date),
- holidayName: holidayName(date)
+ holidayName: holidayName(date),
+ // 我的排班:work=上班(有排班记录)/ rest=休息(区间内有排班但当天无记录)
+ myEntry: my || null,
+ myTag: mySchedCount.value === 0 ? "" : my ? "work" : "rest"
});
}
return cells;
@@ -732,6 +807,8 @@ async function loadCalendarData() {
);
const startStr = fmtDate(start);
const endStr = fmtDate(end);
+ // 我的排班与节假日一样按可视区间加载,用于日历格子的上班/休息背景
+ loadMySchedule(startStr, endStr);
// 节假日不在这里加载:由上方 watch([calendarYear, calendarMonth]) 统一驱动,
// 保证"翻月即加载对应月份所需年份",且首次挂载、二次进入都能自动补齐。
const res = await getScheduleList({
@@ -1265,6 +1342,16 @@ onMounted(() => {
text-align: center;
}
+/* “当前账号暂无排班”提示:说明为什么没有上班/休息背景 */
+.my-sched-tip {
+ font-size: 12px;
+ color: var(--el-text-color-secondary);
+ background: var(--el-fill-color-light);
+ border-radius: 10px;
+ padding: 2px 10px;
+ cursor: help;
+}
+
.calendar-week-header {
display: grid;
grid-template-columns: repeat(7, 1fr);
@@ -1381,6 +1468,40 @@ onMounted(() => {
background: #fff;
}
+/* 我的排班背景:上班浅绿 / 休息浅橙。排班是实际作息,覆盖节假日底色,
+ 但保留休/班角标与节日名;today / selected 在后面定义,优先级仍最高 */
+.calendar-cell.is-my-work {
+ background: #f0f9eb;
+}
+
+.calendar-cell.is-my-work:hover {
+ background: #e6f5db;
+}
+
+.calendar-cell.is-my-rest {
+ background: #fdf6ec;
+}
+
+.calendar-cell.is-my-rest:hover {
+ background: #faecd8;
+}
+
+/* 格内班次角标:用班次定义色做实底小色块 */
+.my-shift-chip {
+ display: inline-flex;
+ align-items: center;
+ height: 16px;
+ padding: 0 4px;
+ border-radius: 3px;
+ font-size: 10px;
+ line-height: 1;
+ color: #fff;
+ flex-shrink: 0;
+ max-width: 56px;
+ overflow: hidden;
+ white-space: nowrap;
+}
+
/* 今天:整格实蓝底,最醒目。放在 is-off / is-work 之后,
保证今天即使逢节假日也保留蓝色标识。
实底较深,格内文字统一转白(或白底反白)以保证对比度。 */
@@ -1753,6 +1874,20 @@ html.dark & {
.calendar-cell.is-work {
background: var(--el-bg-color);
}
+ .calendar-cell.is-my-work {
+ background: rgba(103, 194, 58, 0.1);
+
+ &:hover {
+ background: rgba(103, 194, 58, 0.16);
+ }
+ }
+ .calendar-cell.is-my-rest {
+ background: rgba(230, 162, 60, 0.1);
+
+ &:hover {
+ background: rgba(230, 162, 60, 0.16);
+ }
+ }
/* 今天:整格主色实底,与浅色主题保持一致 */
.calendar-cell.today {
background: var(--el-color-primary);
@@ -1795,6 +1930,12 @@ html.dark & {
.day-title-holiday .holiday-tag.is-work {
color: var(--el-text-color-secondary);
}
+ .day-title-holiday.my-shift-title {
+ color: var(--el-text-color-secondary);
+ }
+ .day-title-holiday.my-rest-title {
+ color: var(--el-color-warning);
+ }
.cell-item {
background: var(--el-fill-color-light);
diff --git a/go/controllers/backend_oa_scheduling_board.go b/go/controllers/backend_oa_scheduling_board.go
index a7585ed..3d2884a 100644
--- a/go/controllers/backend_oa_scheduling_board.go
+++ b/go/controllers/backend_oa_scheduling_board.go
@@ -778,6 +778,67 @@ func (c *BackendOaSchedulingController) ClearEntries() {
c.scOK(map[string]interface{}{"cleared": len(entries)})
}
+// MyBoard 当前登录用户的排班:按登录账号匹配员工档案,返回区间内每天的排班,
+// 供日程日历渲染上班/休息背景。
+// GET /backend/oa/scheduling/my?start_date=&end_date=
+func (c *BackendOaSchedulingController) MyBoard() {
+ tid, _, username, ok := c.scOperator()
+ if !ok {
+ return
+ }
+
+ startDate := strings.TrimSpace(c.GetString("start_date"))
+ endDate := strings.TrimSpace(c.GetString("end_date"))
+ if startDate == "" || endDate == "" {
+ now := time.Now()
+ startDate = now.Format("2006-01-02")
+ endDate = startDate
+ }
+ dates, msg := scDateSpan(startDate, endDate)
+ if msg != "" {
+ c.scFail(400, 400, msg)
+ return
+ }
+ if len(dates) > scBoardMaxDays {
+ c.scFail(400, 400, fmt.Sprintf("一次最多查询 %d 天", scBoardMaxDays))
+ return
+ }
+
+ // 账号未关联员工档案时返回空列表,前端静默不渲染排班背景
+ var employee models.BackendEmployee
+ if err := c.scBaseEmployeeQuery(tid).Filter("account", username).One(&employee); err != nil {
+ c.scOK(map[string]interface{}{"employee": nil, "list": []map[string]interface{}{}})
+ return
+ }
+
+ var entries []models.OaScheduleEntry
+ _, _ = c.scEntryQuery(tid).
+ Filter("employee_id", employee.ID).
+ Filter("schedule_date__gte", startDate).
+ Filter("schedule_date__lte", endDate).
+ OrderBy("schedule_date").
+ All(&entries)
+
+ list := make([]map[string]interface{}, 0, len(entries))
+ for i := range entries {
+ e := entries[i]
+ list = append(list, map[string]interface{}{
+ "date": e.ScheduleDate,
+ "shift_id": e.ShiftID,
+ "shift_name": e.ShiftName,
+ "short_name": e.ShortName,
+ "color": e.Color,
+ "work_start": e.WorkStart,
+ "work_end": e.WorkEnd,
+ "note": e.Note,
+ })
+ }
+ c.scOK(map[string]interface{}{
+ "employee": map[string]interface{}{"id": employee.ID, "name": employee.Name},
+ "list": list,
+ })
+}
+
// ChangeList 排班变更记录(最近 100 条),用于追溯临时调配。
// GET /backend/oa/scheduling/changes?start_date=&end_date=&employee_id=
func (c *BackendOaSchedulingController) ChangeList() {
diff --git a/go/routers/backend/backend.go b/go/routers/backend/backend.go
index d5e5ae9..e743002 100644
--- a/go/routers/backend/backend.go
+++ b/go/routers/backend/backend.go
@@ -347,6 +347,7 @@ func RegisterAuthRoutes() {
beego.Router("/backend/oa/scheduling/shifts/:id", &controllers.BackendOaSchedulingController{}, "post:UpdateShift;delete:DeleteShift")
beego.Router("/backend/oa/scheduling/employees", &controllers.BackendOaSchedulingController{}, "get:ScheduleEmployeeOptions")
beego.Router("/backend/oa/scheduling/board", &controllers.BackendOaSchedulingController{}, "get:Board")
+ beego.Router("/backend/oa/scheduling/my", &controllers.BackendOaSchedulingController{}, "get:MyBoard")
beego.Router("/backend/oa/scheduling/plans", &controllers.BackendOaSchedulingController{}, "get:PlanList")
beego.Router("/backend/oa/scheduling/templates", &controllers.BackendOaSchedulingController{}, "get:TemplateList")
beego.Router("/backend/oa/scheduling/templates/apply", &controllers.BackendOaSchedulingController{}, "post:TemplateApply")