tuqin
2026-10-30 b4cf81d9f299e55472bc2e2b7165a66da8211760
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
<template>
  <div class="app-container">
    <el-tabs v-model="activeTab" type="border-card">
      <!-- ==================== Tab1: 审计追踪 ==================== -->
      <el-tab-pane label="审计追踪" name="audit">
        <el-alert type="info" :closable="false" style="margin-bottom: 12px">
          <template #title>
            记录敏感数据的增删改与关键业务操作(样品出库、实验资源分配、告警处理等),
            包含操作人、IP、字段变更前后值,满足合规追溯要求。
          </template>
        </el-alert>
        <div class="search_form">
          <div>
            <span class="search_title">模块:</span>
            <el-input v-model="auditSearch.module" style="width: 140px" placeholder="如 样品管理" clearable @change="loadAudit" />
            <span class="search_title" style="margin-left: 14px">动作:</span>
            <el-select v-model="auditSearch.action" style="width: 150px" placeholder="全部" clearable @change="loadAudit">
              <el-option label="新增 create" value="create" />
              <el-option label="修改 update" value="update" />
              <el-option label="出库 stock_out" value="stock_out" />
              <el-option label="入库 stock_in" value="stock_in" />
              <el-option label="分配资源 assign_resource" value="assign_resource" />
              <el-option label="处理告警 handle_alert" value="handle_alert" />
            </el-select>
            <span class="search_title" style="margin-left: 14px">操作人:</span>
            <el-input v-model="auditSearch.operator" style="width: 130px" placeholder="请输入" clearable @change="loadAudit" />
            <span class="search_title" style="margin-left: 14px">结果:</span>
            <el-select v-model="auditSearch.execResult" style="width: 110px" placeholder="全部" clearable @change="loadAudit">
              <el-option label="成功" value="success" />
              <el-option label="失败" value="fail" />
            </el-select>
            <el-button type="primary" style="margin-left: 10px" @click="loadAudit">搜索</el-button>
          </div>
          <div>
            <el-button @click="loadAudit">刷新</el-button>
          </div>
        </div>
        <PIMTable rowKey="id" :column="auditColumns" :tableData="auditData" :page="auditPage"
          :tableLoading="auditLoading" @pagination="paginationAudit" />
      </el-tab-pane>
 
      <!-- ==================== Tab2: 访问控制 ==================== -->
      <el-tab-pane label="访问控制" name="access">
        <el-alert type="success" :closable="false" style="margin-bottom: 16px">
          <template #title>
            基于 RuoYi 权限体系:用户 → 角色 → 菜单/按钮权限。所有接口都有 @PreAuthorize 校验,
            未授权访问返回 401/403;登录密码用 BCrypt 加密存储,不落明文。
          </template>
        </el-alert>
 
        <el-row :gutter="16">
          <el-col :span="8">
            <el-card shadow="hover">
              <template #header>
                <div style="display: flex; justify-content: space-between; align-items: center">
                  <span style="font-weight: 600">用户管理</span>
                  <el-button link type="primary" @click="goSystem('user')">前往 →</el-button>
                </div>
              </template>
              <div style="color: #606266; font-size: 13px; line-height: 1.8">
                维护系统使用者账号,分配角色与部门。<br>
                当前用户:<b>{{ stat.userCount ?? '-' }}</b> 人
              </div>
            </el-card>
          </el-col>
          <el-col :span="8">
            <el-card shadow="hover">
              <template #header>
                <div style="display: flex; justify-content: space-between; align-items: center">
                  <span style="font-weight: 600">角色管理</span>
                  <el-button link type="primary" @click="goSystem('role')">前往 →</el-button>
                </div>
              </template>
              <div style="color: #606266; font-size: 13px; line-height: 1.8">
                按岗位划分角色,配置可访问的菜单和数据范围。<br>
                当前角色:<b>{{ stat.roleCount ?? '-' }}</b> 个
              </div>
            </el-card>
          </el-col>
          <el-col :span="8">
            <el-card shadow="hover">
              <template #header>
                <div style="display: flex; justify-content: space-between; align-items: center">
                  <span style="font-weight: 600">菜单权限</span>
                  <el-button link type="primary" @click="goSystem('menu')">前往 →</el-button>
                </div>
              </template>
              <div style="color: #606266; font-size: 13px; line-height: 1.8">
                控制每个角色能看到哪些菜单、能点哪些按钮。<br>
                当前菜单:<b>{{ stat.menuCount ?? '-' }}</b> 项
              </div>
            </el-card>
          </el-col>
        </el-row>
 
        <el-divider content-position="left">登录与操作审计</el-divider>
        <el-row :gutter="16">
          <el-col :span="8">
            <el-card shadow="hover">
              <template #header>
                <div style="display: flex; justify-content: space-between; align-items: center">
                  <span style="font-weight: 600">登录日志</span>
                  <el-button link type="primary" @click="goMonitor('logininfor')">前往 →</el-button>
                </div>
              </template>
              <div style="color: #606266; font-size: 13px">记录每次登录的 IP、地点、浏览器、成功/失败</div>
            </el-card>
          </el-col>
          <el-col :span="8">
            <el-card shadow="hover">
              <template #header>
                <div style="display: flex; justify-content: space-between; align-items: center">
                  <span style="font-weight: 600">操作日志</span>
                  <el-button link type="primary" @click="goMonitor('operlog')">前往 →</el-button>
                </div>
              </template>
              <div style="color: #606266; font-size: 13px">框架级操作日志(所有带 @Log 注解的接口)</div>
            </el-card>
          </el-col>
          <el-col :span="8">
            <el-card shadow="hover">
              <template #header>
                <div style="display: flex; justify-content: space-between; align-items: center">
                  <span style="font-weight: 600">在线用户</span>
                  <el-button link type="primary" @click="goMonitor('online')">前往 →</el-button>
                </div>
              </template>
              <div style="color: #606266; font-size: 13px">当前登录会话,可强制下线</div>
            </el-card>
          </el-col>
        </el-row>
 
        <el-divider content-position="left">安全措施说明</el-divider>
        <el-table :data="securityMeasures" border size="small">
          <el-table-column prop="item" label="措施" width="180" />
          <el-table-column prop="impl" label="实现方式" />
          <el-table-column prop="status" label="状态" width="100" align="center">
            <template #default="{ row }">
              <el-tag :type="row.status === '已实施' ? 'success' : 'info'" size="small">{{ row.status }}</el-tag>
            </template>
          </el-table-column>
        </el-table>
      </el-tab-pane>
 
      <!-- ==================== Tab3: 安全策略 ==================== -->
      <el-tab-pane label="安全策略" name="policy">
        <el-alert type="warning" :closable="false" style="margin-bottom: 12px">
          <template #title>
            以下参数控制安全相关行为,修改后立即生效(审计与参数服务有本地缓存,改完刷新页面即可)。
          </template>
        </el-alert>
        <div class="search_form">
          <div>
            <span class="search_title">参数名:</span>
            <el-input v-model="secSearch.settingName" style="width: 180px" placeholder="请输入" clearable @change="loadSecurity" />
            <el-button type="primary" style="margin-left: 10px" @click="loadSecurity">搜索</el-button>
          </div>
          <div>
            <el-button @click="loadSecurity">刷新</el-button>
          </div>
        </div>
        <el-table :data="secData" :loading="secLoading" border size="small">
          <el-table-column prop="settingKey" label="参数键" width="230" />
          <el-table-column prop="settingName" label="参数名" width="160" />
          <el-table-column prop="settingValue" label="当前值" width="140" />
          <el-table-column prop="defaultValue" label="默认值" width="110" />
          <el-table-column prop="description" label="说明" show-overflow-tooltip />
          <el-table-column label="操作" width="90" align="center">
            <template #default="{ row }">
              <el-button link type="primary" @click="openEdit(row)">修改</el-button>
            </template>
          </el-table-column>
        </el-table>
      </el-tab-pane>
    </el-tabs>
 
    <!-- ===== 参数编辑弹窗 ===== -->
    <el-dialog v-model="editVisible" title="修改安全策略" width="560px">
      <el-form :model="editForm" label-position="top">
        <el-form-item label="参数键"><el-input v-model="editForm.settingKey" disabled /></el-form-item>
        <el-form-item :label="editForm.settingName">
          <el-input v-model="editForm.settingValue" placeholder="请输入" />
          <div style="color: #909399; font-size: 12px; margin-top: 4px">{{ editForm.description }}</div>
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="editVisible = false">取消</el-button>
        <el-button type="primary" @click="submitEdit">保存</el-button>
      </template>
    </el-dialog>
  </div>
</template>
 
<script setup>
import { ref, reactive, onMounted, getCurrentInstance } from "vue";
import { useRouter } from "vue-router";
import { ElMessage } from "element-plus";
import { listAuditLog } from "@/api/lims/auditLog";
import { listSetting, updateSetting } from "@/api/lims/setting";
 
const { proxy } = getCurrentInstance();
const router = useRouter();
const activeTab = ref("audit");
 
/* ==================== 审计追踪 ==================== */
const auditSearch = ref({});
const auditData = ref([]);
const auditLoading = ref(false);
const auditPage = reactive({ pageNum: 1, pageSize: 20, total: 0 });
 
const auditColumns = ref([
  { label: "时间", prop: "operateTime", width: 160 },
  { label: "模块", prop: "module", width: 110 },
  { label: "动作", prop: "action", width: 140 },
  { label: "业务编号", prop: "bizCode", width: 130 },
  { label: "说明", prop: "description", showOverflowTooltip: true },
  { label: "字段", prop: "fieldName", width: 100 },
  { label: "旧值", prop: "oldValue", width: 100 },
  { label: "新值", prop: "newValue", width: 100 },
  { label: "操作人", prop: "operator", width: 100 },
  { label: "IP", prop: "operatorIp", width: 130 },
  { label: "结果", prop: "execResult", width: 80 },
]);
 
const loadAudit = () => {
  auditLoading.value = true;
  listAuditLog({ ...auditSearch.value, ...auditPage }).then((res) => {
    auditLoading.value = false;
    auditData.value = res.data.records || [];
    auditPage.total = res.data.total || 0;
  }).catch(() => (auditLoading.value = false));
};
const paginationAudit = (p) => { auditPage.pageNum = p.pageNum; auditPage.pageSize = p.pageSize; loadAudit(); };
 
/* ==================== 访问控制 ==================== */
const stat = ref({});
const securityMeasures = ref([
  { item: "密码存储", impl: "BCrypt 加盐哈希(不可逆),数据库中不落明文", status: "已实施" },
  { item: "接口鉴权", impl: "JWT Token + Spring Security 拦截,未授权返回 401", status: "已实施" },
  { item: "菜单/按钮权限", impl: "用户→角色→菜单三级,按钮级 perms 校验", status: "已实施" },
  { item: "数据隔离", impl: "多租户插件自动拼接 tenant_id 条件", status: "已实施" },
  { item: "敏感操作审计", impl: "样品出入库、实验资源分配、告警处理写 lims_audit_log", status: "已实施" },
  { item: "审计留痕内容", impl: "操作人、IP、字段变更前后值、操作结果", status: "已实施" },
  { item: "登录日志", impl: "记录每次登录的 IP、地点、浏览器、成功失败", status: "已实施" },
  { item: "字段级加密", impl: "未做,如客户有等保要求需另行开发", status: "待定" },
  { item: "列表脱敏显示", impl: "参数 security.maskSensitive 已预留,前端展示未实现", status: "待定" },
]);
 
const goSystem = (path) => { router.push("/system/" + path); };
const goMonitor = (path) => { router.push("/monitor/" + path); };
 
// 统计(用已有的接口顺手取数,失败不影响展示)
const loadStat = () => {
  Promise.all([
    import("@/api/system/user").then((m) => m.listUser({ pageNum: 1, pageSize: 1 })).catch(() => null),
    import("@/api/system/role").then((m) => m.listRole({ pageNum: 1, pageSize: 1 })).catch(() => null),
    import("@/api/system/menu").then((m) => m.listMenu({})).catch(() => null),
  ]).then(([u, r, m]) => {
    stat.value = {
      userCount: u && u.total !== undefined ? u.total : (u && u.data && u.data.total),
      roleCount: r && r.total !== undefined ? r.total : (r && r.data && r.data.total),
      menuCount: Array.isArray(m) ? m.length : (m && m.data ? m.data.length : undefined),
    };
  }).catch(() => {});
};
 
/* ==================== 安全策略 ==================== */
const secSearch = ref({});
const secData = ref([]);
const secLoading = ref(false);
const editVisible = ref(false);
const editForm = ref({});
 
const loadSecurity = () => {
  secLoading.value = true;
  listSetting({ ...secSearch.value, category: "security", pageNum: 1, pageSize: 50 }).then((res) => {
    secLoading.value = false;
    secData.value = res.data.records || [];
  }).catch(() => (secLoading.value = false));
};
 
const openEdit = (row) => {
  editForm.value = { ...row };
  editVisible.value = true;
};
const submitEdit = () => {
  updateSetting(editForm.value).then(() => {
    ElMessage.success("已保存,刷新页面后生效");
    editVisible.value = false;
    loadSecurity();
  });
};
 
onMounted(() => {
  loadAudit();
  loadStat();
  loadSecurity();
});
</script>
 
<style scoped>
.search_form {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 12px;
  flex-wrap: wrap;
  gap: 8px;
}
.search_title {
  font-size: 14px;
  color: #606266;
}
</style>