优化代码

This commit is contained in:
hero920103 committed 2026-11-05 18:28:46 +08:00
1 parent 8ba15d87b3
commit 7b1c94b5ff
5 files changed
+261 -2

No files matched your search

@@ -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 代码(互踢规则是合理安全设计;根因是开发环境认证指向了旧版远程中心)
+5
View File
@@ -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) {
+143 -2
View File
@@ -78,6 +78,12 @@
>
<el-button :icon="ArrowRight" circle @click="changeMonth(1)" />
<el-button @click="goToday">今天</el-button>
<span
v-if="mySchedLoaded && mySchedCount === 0"
class="my-sched-tip"
title="当前登录账号未匹配到排班(账号需要有对应员工档案且已排班),因此不渲染上班/休息背景"
>当前账号暂无排班</span
>
</div>
</div>
<div class="calendar-week-header">
@@ -94,7 +100,9 @@
selected: cell.date === selectedDate,
'has-items': cell.items.length > 0,
'is-off': cell.tag === 'off',
'is-work': cell.tag === 'work'
'is-work': cell.tag === 'work',
'is-my-work': cell.myTag === 'work',
'is-my-rest': cell.myTag === 'rest'
}"
@click="selectedDate = cell.date"
@dblclick="openCreate(cell.date)"
@@ -109,6 +117,13 @@
<span v-else-if="cell.day === 1" class="month-tag"
>{{ cell.month }}月</span
>
<span
v-if="cell.myEntry"
class="my-shift-chip"
:style="{ background: cell.myEntry.color }"
:title="`${cell.myEntry.shift_name} ${cell.myEntry.work_start}-${cell.myEntry.work_end}`"
>{{ cell.myEntry.short_name || cell.myEntry.shift_name }}</span
>
</div>
<div v-if="cell.holidayName" class="cell-holiday" :title="cell.holidayName">
{{ cell.holidayName }}
@@ -164,6 +179,28 @@
<span v-else-if="selectedIsRestDay" class="day-title-holiday"
>周末</span
>
<span
v-if="selectedMyEntry"
class="day-title-holiday my-shift-title"
>
<span
class="holiday-tag"
:style="{
background: selectedMyEntry.color,
color: '#fff',
borderColor: selectedMyEntry.color
}"
>{{ selectedMyEntry.short_name || selectedMyEntry.shift_name }}</span
>
{{ selectedMyEntry.work_start }}-{{
selectedMyEntry.work_end
}}
</span>
<span
v-else-if="mySchedCount > 0"
class="day-title-holiday my-rest-title"
>休息</span
>
</div>
<div class="day-sub">
{{ selectedItems.length }} 项日程<template v-if="pendingCount > 0"
@@ -445,6 +482,7 @@ import {
getScheduleList,
getScheduleStats
} from "@/api/oaSchedule";
import { getMyScheduleBoard } from "@/api/oaScheduling";
const viewMode = ref("calendar");
const stats = ref({});
@@ -516,6 +554,39 @@ const calendarMonth = ref(now.getMonth() + 1);
const calendarItems = ref([]);
const selectedDate = ref(todayStr());
// ---------- 我的排班(上班 / 休息背景) ----------
// mySchedMap: { 'YYYY-MM-DD': 排班记录 };mySchedCount 为区间内排班天数。
// 只有区间内存在排班时才渲染背景,未排班的月份不显示,避免误导。
const mySchedMap = ref({});
const mySchedCount = ref(0);
// 是否已完成加载:用于区分“还没查完”与“查了但没有”,后者在日历标题旁提示
const mySchedLoaded = ref(false);
async function loadMySchedule(startStr, endStr) {
try {
const res = await getMyScheduleBoard({
start_date: startStr,
end_date: endStr
});
const list = res?.data?.list || [];
const map = {};
for (const item of list) {
map[normDate(item.date)] = item;
}
mySchedMap.value = map;
mySchedCount.value = list.length;
mySchedLoaded.value = true;
} catch {
// 排班背景属于辅助信息,失败时静默降级为无背景
mySchedMap.value = {};
mySchedCount.value = 0;
mySchedLoaded.value = true;
}
}
// 选中日期的排班,供侧栏标题展示班次
const selectedMyEntry = computed(() => 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);
@@ -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() {
+1
View File
@@ -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")