feat: 优化日志页面/支付配置/设备列表/菜单详情数据解析

- 支付配置: privateKeyPath改为privateKey,支持textarea粘贴和pem文件上传
- 审计日志: 优化列表展示(head字段/result解析)和详情弹窗
- 橘视厅调用日志: 接口路径截断显示/JSON格式化/耗时颜色分级
- 第三方设备列表: 新增来源/WiFi MAC/UID/心跳时间列,状态改为已激活/未激活
- 菜单/角色/用户详情: 统一用 data.{entity} 提取业务对象
- 设备权益: 去除双层data嵌套,适配后端统一响应结构
This commit is contained in:
freeboss
2026-08-11 16:07:10 +08:00
parent 630dfddc59
commit e39b88d1db
12 changed files with 318 additions and 108 deletions

View File

@@ -10,15 +10,17 @@ export interface AuditLogItem {
actorUserId: number actorUserId: number
/** 操作人名 */ /** 操作人名 */
name?: string name?: string
/** 请求方法 */ /** 操作描述(中文,如"创建天卡订单(设备端)") */
head?: string
/** 请求方法枚举(如 DAYCARD_ORDER_CREATE) */
method?: string method?: string
/** 接口路径 */ /** 接口路径 */
api?: string api?: string
/** 请求参数 */ /** 请求参数 */
param?: string param?: string
/** 中文描述 */ /** 中文描述(旧字段,多为空) */
paramDesc?: string paramDesc?: string
/** 操作结果 */ /** 操作结果(完整返回字符串,含 code=/msg=) */
result?: string result?: string
/** 变更前 */ /** 变更前 */
before?: string before?: string
@@ -41,6 +43,8 @@ export interface AuditLogListParams extends ManagePageParams {
actorUserId?: number actorUserId?: number
api?: string api?: string
method?: string method?: string
head?: string
ip?: string
ticketNo?: string ticketNo?: string
} }

View File

@@ -46,6 +46,7 @@ export interface DeviceListParams extends ManagePageParams {
status?: string status?: string
deviceName?: string deviceName?: string
mac?: string mac?: string
deviceSn?: string
/** 设备来源:DIRECT / PARTNER */ /** 设备来源:DIRECT / PARTNER */
source?: string source?: string
/** 合作伙伴UUID(source=PARTNER 时筛选) */ /** 合作伙伴UUID(source=PARTNER 时筛选) */

View File

@@ -6,7 +6,7 @@ import type { ManageListResponse, ManagePageParams } from './types'
*/ */
export interface JstCallLogItem { export interface JstCallLogItem {
id: number id: number
/** 请求URL */ /** 请求URL(可能带反引号和query参数) */
apiPath: string apiPath: string
/** 请求方法 */ /** 请求方法 */
requestMethod?: string requestMethod?: string
@@ -20,6 +20,12 @@ export interface JstCallLogItem {
success: number success: number
/** 耗时(ms) */ /** 耗时(ms) */
duration?: number duration?: number
/** 关联数据ID */
dataId?: number
/** 请求头(JSON字符串) */
requestHeaders?: string
/** 客户端IP */
clientIp?: string
/** 错误信息 */ /** 错误信息 */
errorMessage?: string errorMessage?: string
/** 关联设备MAC */ /** 关联设备MAC */
@@ -28,8 +34,14 @@ export interface JstCallLogItem {
deviceSn?: string deviceSn?: string
/** 关联设备UID */ /** 关联设备UID */
deviceUid?: string deviceUid?: string
/** 设备所属门店 */ /** 门店组织ID */
storeOrgId?: number
/** 设备所属门店名称 */
storeOrgName?: string storeOrgName?: string
/** 门店编码 */
storeOrgCode?: string
/** 门店类型 */
storeOrgType?: string
createTime: string createTime: string
} }
@@ -42,6 +54,8 @@ export interface JstCallLogListParams extends ManagePageParams {
endTime?: string endTime?: string
apiPath?: string apiPath?: string
requestBody?: string requestBody?: string
/** 结果筛选:1=成功,0=失败 */
success?: number
} }
/** 橘视厅调用日志列表 */ /** 橘视厅调用日志列表 */

View File

@@ -16,7 +16,8 @@ export interface PayConfigInfo {
mchId: string mchId: string
apiV3Key: string apiV3Key: string
certSerialNo: string certSerialNo: string
privateKeyPath: string /** 商户 API 私钥(apiclient_key.pem 的 PEM 内容) */
privateKey: string
notifyUrl: string notifyUrl: string
status: string status: string
remark: string remark: string
@@ -49,7 +50,8 @@ export interface PayConfigSaveData {
mchId: string mchId: string
apiV3Key: string apiV3Key: string
certSerialNo?: string certSerialNo?: string
privateKeyPath?: string /** 商户 API 私钥(apiclient_key.pem 的 PEM 内容) */
privateKey?: string
notifyUrl: string notifyUrl: string
status?: string status?: string
remark?: string remark?: string

View File

@@ -2,16 +2,22 @@
<div class="mod-audit-log"> <div class="mod-audit-log">
<el-form :inline="true" :model="dataForm" @keyup.enter="getDataList()"> <el-form :inline="true" :model="dataForm" @keyup.enter="getDataList()">
<el-form-item> <el-form-item>
<el-input v-model="dataForm.name" placeholder="操作人" clearable style="width:140px" disabled title="按操作人ID查询,此处仅展示"></el-input> <el-input v-model="dataForm.api" placeholder="接口路径" clearable style="width:220px"></el-input>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-input v-model="dataForm.api" placeholder="接口路径" clearable style="width:200px"></el-input> <el-input v-model="dataForm.method" placeholder="方法枚举" clearable style="width:180px"></el-input>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-input v-model="dataForm.method" placeholder="请求方法" clearable style="width:100px"></el-input> <el-input v-model="dataForm.head" placeholder="操作描述" clearable style="width:180px"></el-input>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-input v-model="dataForm.ticketNo" placeholder="工单号" clearable style="width:160px"></el-input> <el-input v-model="dataForm.ip" placeholder="IP" clearable style="width:150px"></el-input>
</el-form-item>
<el-form-item>
<el-select v-model="dataForm.resultStatus" placeholder="结果" clearable style="width:120px">
<el-option label="成功" value="SUCCESS"></el-option>
<el-option label="失败" value="FAIL"></el-option>
</el-select>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button @click="getDataList()">查询</el-button> <el-button @click="getDataList()">查询</el-button>
@@ -19,28 +25,27 @@
</el-form> </el-form>
<div class="table-page-wrap"> <div class="table-page-wrap">
<el-table :data="dataList" border v-loading="dataListLoading" style="width: 100%"> <el-table :data="dataList" border v-loading="dataListLoading" style="width: 100%">
<el-table-column type="index" header-align="center" align="center" width="60" label="序号" /> <el-table-column type="index" header-align="center" align="center" width="55" label="序号" />
<el-table-column prop="name" header-align="center" align="center" min-width="120" label="操作人"> <el-table-column header-align="center" align="center" min-width="160" label="操作描述">
<template #default="scope">{{ scope.row.name || scope.row.actorUserId || '-' }}</template>
</el-table-column>
<el-table-column prop="method" header-align="center" align="center" width="80" label="方法" />
<el-table-column prop="api" header-align="center" align="center" min-width="200" label="接口" show-overflow-tooltip />
<el-table-column prop="paramDesc" header-align="center" align="center" min-width="160" label="操作描述" show-overflow-tooltip>
<template #default="scope">{{ scope.row.paramDesc || '-' }}</template>
</el-table-column>
<el-table-column prop="result" header-align="center" align="center" width="100" label="结果">
<template #default="scope"> <template #default="scope">
<el-tag v-if="scope.row.result === 'SUCCESS' || scope.row.result === '成功'" size="small" type="success">成功</el-tag> <span>{{ scope.row.head || '-' }}</span>
<el-tag v-else-if="scope.row.result" size="small" type="danger">{{ scope.row.result }}</el-tag>
<span v-else>-</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="ip" header-align="center" align="center" width="130" label="IP"> <el-table-column prop="method" header-align="center" align="center" width="170" label="方法" show-overflow-tooltip />
<template #default="scope">{{ scope.row.ip || '-' }}</template> <el-table-column prop="api" header-align="center" align="center" min-width="200" label="接口" show-overflow-tooltip />
<el-table-column header-align="center" align="center" width="100" label="操作人">
<template #default="scope">
<span v-if="scope.row.name">{{ scope.row.name }}</span>
<span v-else-if="scope.row.actorUserId">用户#{{ scope.row.actorUserId }}</span>
<el-tag v-else size="small" type="info">系统</el-tag>
</template>
</el-table-column> </el-table-column>
<el-table-column prop="ticketNo" header-align="center" align="center" min-width="140" label="工单号"> <el-table-column header-align="center" align="center" width="90" label="结果">
<template #default="scope">{{ scope.row.ticketNo || '-' }}</template> <template #default="scope">
<el-tag :type="resultTag(scope.row).type" size="small">{{ resultTag(scope.row).label }}</el-tag>
</template>
</el-table-column> </el-table-column>
<el-table-column prop="ip" header-align="center" align="center" width="140" label="IP" show-overflow-tooltip />
<el-table-column prop="createdAt" header-align="center" align="center" width="170" label="操作时间" /> <el-table-column prop="createdAt" header-align="center" align="center" width="170" label="操作时间" />
<el-table-column fixed="right" header-align="center" align="center" width="90" label="操作"> <el-table-column fixed="right" header-align="center" align="center" width="90" label="操作">
<template #default="scope"> <template #default="scope">
@@ -60,27 +65,37 @@
</div> </div>
<!-- 详情弹窗 --> <!-- 详情弹窗 -->
<el-dialog v-model="detailVisible" title="审计日志详情" width="760px" top="5vh"> <el-dialog v-model="detailVisible" title="审计日志详情" width="820px" top="5vh">
<el-descriptions :column="1" border> <el-descriptions :column="1" border>
<el-descriptions-item label="操作人">{{ detailData.name || detailData.actorUserId || '-' }}</el-descriptions-item> <el-descriptions-item label="操作描述">{{ detailData.head || '-' }}</el-descriptions-item>
<el-descriptions-item label="方法">{{ detailData.method || '-' }}</el-descriptions-item> <el-descriptions-item label="方法">{{ detailData.method || '-' }}</el-descriptions-item>
<el-descriptions-item label="接口">{{ detailData.api || '-' }}</el-descriptions-item> <el-descriptions-item label="接口">{{ detailData.api || '-' }}</el-descriptions-item>
<el-descriptions-item label="操作描述">{{ detailData.paramDesc || '-' }}</el-descriptions-item> <el-descriptions-item label="操作人">
<el-descriptions-item label="结果">{{ detailData.result || '-' }}</el-descriptions-item> <span v-if="detailData.name">{{ detailData.name }}</span>
<el-descriptions-item label="工单号">{{ detailData.ticketNo || '-' }}</el-descriptions-item> <span v-else-if="detailData.actorUserId">用户#{{ detailData.actorUserId }}</span>
<span v-else>系统</span>
</el-descriptions-item>
<el-descriptions-item label="结果">
<el-tag :type="resultTag(detailData).type" size="small">{{ resultTag(detailData).label }}</el-tag>
<span style="margin-left:8px; color:#909399; font-size:12px;">{{ resultSummary(detailData.result) }}</span>
</el-descriptions-item>
<el-descriptions-item label="IP">{{ detailData.ip || '-' }}</el-descriptions-item> <el-descriptions-item label="IP">{{ detailData.ip || '-' }}</el-descriptions-item>
<el-descriptions-item label="设备信息">{{ detailData.deviceInfo || '-' }}</el-descriptions-item> <el-descriptions-item label="设备信息" :content-style="{ wordBreak: 'break-all' }">{{ detailData.deviceInfo || '-' }}</el-descriptions-item>
<el-descriptions-item label="工单号">{{ detailData.ticketNo || '-' }}</el-descriptions-item>
<el-descriptions-item label="原因">{{ detailData.reason || '-' }}</el-descriptions-item> <el-descriptions-item label="原因">{{ detailData.reason || '-' }}</el-descriptions-item>
<el-descriptions-item label="操作时间">{{ detailData.createdAt || '-' }}</el-descriptions-item>
<el-descriptions-item label="请求参数"> <el-descriptions-item label="请求参数">
<pre style="white-space: pre-wrap; word-break: break-all; max-height: 200px; overflow: auto; margin: 0">{{ detailData.param || '-' }}</pre> <pre class="log-pre">{{ detailData.param || '-' }}</pre>
</el-descriptions-item>
<el-descriptions-item label="返回结果">
<pre class="log-pre">{{ detailData.result || '-' }}</pre>
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="变更前"> <el-descriptions-item label="变更前">
<pre style="white-space: pre-wrap; word-break: break-all; max-height: 160px; overflow: auto; margin: 0">{{ detailData.before || '-' }}</pre> <pre class="log-pre">{{ detailData.before || '-' }}</pre>
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="变更后"> <el-descriptions-item label="变更后">
<pre style="white-space: pre-wrap; word-break: break-all; max-height: 160px; overflow: auto; margin: 0">{{ detailData.after || '-' }}</pre> <pre class="log-pre">{{ detailData.after || '-' }}</pre>
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="操作时间">{{ detailData.createdAt || '-' }}</el-descriptions-item>
</el-descriptions> </el-descriptions>
<template #footer> <template #footer>
<el-button type="primary" @click="detailVisible = false">关闭</el-button> <el-button type="primary" @click="detailVisible = false">关闭</el-button>
@@ -94,30 +109,70 @@ import { reactive, ref, onMounted } from 'vue'
import { getAuditLogList, type AuditLogItem } from '@/api/manage/auditLog' import { getAuditLogList, type AuditLogItem } from '@/api/manage/auditLog'
interface DataForm { interface DataForm {
name: string
api: string api: string
method: string method: string
ticketNo: string head: string
ip: string
resultStatus: string
} }
const dataForm = reactive<DataForm>({ name: '', api: '', method: '', ticketNo: '' }) const dataForm = reactive<DataForm>({ api: '', method: '', head: '', ip: '', resultStatus: '' })
const dataList = ref<AuditLogItem[]>([]) const dataList = ref<AuditLogItem[]>([])
const pageIndex = ref<number>(1) const pageIndex = ref<number>(1)
const pageSize = ref<number>(10) const pageSize = ref<number>(10)
const totalPage = ref<number>(0) const totalPage = ref<number>(0)
const dataListLoading = ref<boolean>(false) const dataListLoading = ref<boolean>(false)
/** 解析 result 字符串,返回标签类型与文案 */
function resultTag (row: Partial<AuditLogItem> | undefined): { type: 'success' | 'danger' | 'warning' | 'info', label: string } {
if (!row) return { type: 'info', label: '-' }
const r = row.result || ''
if (!r) return { type: 'info', label: '-' }
// 异常
if (r.startsWith('exception') || /Exception/i.test(r)) return { type: 'danger', label: '异常' }
// 解析 code=N
const m = r.match(/code=(\d+)/)
if (m) {
const code = Number(m[1])
if (code === 0) return { type: 'success', label: '成功' }
return { type: 'danger', label: `失败(${code})` }
}
// 兜底:msg=success
if (/msg=success/i.test(r)) return { type: 'success', label: '成功' }
if (/msg=/i.test(r) && !/msg=success/i.test(r)) return { type: 'danger', label: '失败' }
return { type: 'warning', label: '未知' }
}
/** 返回结果摘要(取 msg 部分) */
function resultSummary (result?: string): string {
if (!result) return ''
const m = result.match(/msg=([^,}]+)/)
return m ? `msg: ${m[1]}` : ''
}
async function getDataList (): Promise<void> { async function getDataList (): Promise<void> {
dataListLoading.value = true dataListLoading.value = true
try { try {
const res = await getAuditLogList({ const params: any = {
page: pageIndex.value, page: pageIndex.value,
limit: pageSize.value, limit: pageSize.value,
api: dataForm.api || undefined, api: dataForm.api || undefined,
method: dataForm.method || undefined, method: dataForm.method || undefined,
ticketNo: dataForm.ticketNo || undefined head: dataForm.head || undefined,
}) ip: dataForm.ip || undefined
}
// 结果状态由前端筛选(后端无此参数)
const res = await getAuditLogList(params)
if (res && res.code === 0) { if (res && res.code === 0) {
dataList.value = res.data?.page?.list || [] let list = res.data?.page?.list || []
if (dataForm.resultStatus) {
const wantSuccess = dataForm.resultStatus === 'SUCCESS'
list = list.filter((item: AuditLogItem) => {
const tag = resultTag(item)
const isSuccess = tag.type === 'success'
return wantSuccess ? isSuccess : !isSuccess
})
}
dataList.value = list
totalPage.value = res.data?.page?.totalCount || 0 totalPage.value = res.data?.page?.totalCount || 0
} else { } else {
dataList.value = [] dataList.value = []
@@ -156,4 +211,15 @@ onMounted(() => {
.mod-audit-log { .mod-audit-log {
padding: 10px; padding: 10px;
} }
.log-pre {
white-space: pre-wrap;
word-break: break-all;
max-height: 220px;
overflow: auto;
margin: 0;
background: #f5f7fa;
padding: 8px;
border-radius: 4px;
font-size: 12px;
}
</style> </style>

View File

@@ -499,10 +499,9 @@ async function showBenefits (row: DeviceInfo): Promise<void> {
} }
try { try {
const res = await getDeviceBenefits(row.id) const res = await getDeviceBenefits(row.id)
if (res && res.code === 0 && res.data) { const benefits = res?.data
benefitsData.value = { ...res.data, deviceName: row.deviceName, deviceId: row.id, mac: row.mac } if (benefits) {
} else if (res && res.code === 200 && res.data) { benefitsData.value = { ...benefits, deviceName: row.deviceName, deviceId: row.id, mac: row.mac }
benefitsData.value = { ...res.data, deviceName: row.deviceName, deviceId: row.id, mac: row.mac }
} else { } else {
ElMessage.error(res?.msg || '查询权益失败') ElMessage.error(res?.msg || '查询权益失败')
} }

View File

@@ -7,6 +7,12 @@
<el-form-item> <el-form-item>
<el-input v-model="dataForm.apiPath" placeholder="接口路径" clearable style="width:200px"></el-input> <el-input v-model="dataForm.apiPath" placeholder="接口路径" clearable style="width:200px"></el-input>
</el-form-item> </el-form-item>
<el-form-item>
<el-select v-model="dataForm.success" placeholder="结果" clearable style="width:120px">
<el-option label="成功" :value="1"></el-option>
<el-option label="失败" :value="0"></el-option>
</el-select>
</el-form-item>
<el-form-item label="开始"> <el-form-item label="开始">
<el-date-picker v-model="dataForm.beginTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" placeholder="开始时间" style="width:200px" /> <el-date-picker v-model="dataForm.beginTime" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" placeholder="开始时间" style="width:200px" />
</el-form-item> </el-form-item>
@@ -19,26 +25,35 @@
</el-form> </el-form>
<div class="table-page-wrap"> <div class="table-page-wrap">
<el-table :data="dataList" border v-loading="dataListLoading" style="width: 100%"> <el-table :data="dataList" border v-loading="dataListLoading" style="width: 100%">
<el-table-column type="index" header-align="center" align="center" width="60" label="序号" /> <el-table-column type="index" header-align="center" align="center" width="55" label="序号" />
<el-table-column prop="apiPath" header-align="center" align="center" min-width="220" label="接口" show-overflow-tooltip /> <el-table-column header-align="center" align="center" min-width="260" label="接口路径" show-overflow-tooltip>
<el-table-column prop="requestMethod" header-align="center" align="center" width="80" label="方法" /> <template #default="scope">
<el-table-column prop="success" header-align="center" align="center" width="90" label="结果"> <span>{{ apiShort(scope.row.apiPath) }}</span>
</template>
</el-table-column>
<el-table-column prop="requestMethod" header-align="center" align="center" width="70" label="方法" />
<el-table-column header-align="center" align="center" width="90" label="结果">
<template #default="scope"> <template #default="scope">
<el-tag v-if="scope.row.success === 1" size="small" type="success">成功</el-tag> <el-tag v-if="scope.row.success === 1" size="small" type="success">成功</el-tag>
<el-tag v-else size="small" type="danger">失败</el-tag> <el-tag v-else size="small" type="danger">失败</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="responseCode" header-align="center" align="center" width="90" label="状态码"> <el-table-column header-align="center" align="center" width="90" label="状态码">
<template #default="scope">{{ scope.row.responseCode || '-' }}</template> <template #default="scope">
<el-tag v-if="scope.row.responseCode && scope.row.responseCode >= 200 && scope.row.responseCode < 300" size="small" type="success">{{ scope.row.responseCode }}</el-tag>
<el-tag v-else-if="scope.row.responseCode" size="small" type="danger">{{ scope.row.responseCode }}</el-tag>
<span v-else>-</span>
</template>
</el-table-column> </el-table-column>
<el-table-column prop="duration" header-align="center" align="center" width="90" label="耗时(ms)" /> <el-table-column header-align="center" align="center" width="100" label="耗时">
<el-table-column prop="mac" header-align="center" align="center" width="140" label="MAC"> <template #default="scope">
<span :class="durationClass(scope.row.duration)">{{ scope.row.duration ?? '-' }} ms</span>
</template>
</el-table-column>
<el-table-column prop="mac" header-align="center" align="center" width="140" label="MAC" show-overflow-tooltip>
<template #default="scope">{{ scope.row.mac || '-' }}</template> <template #default="scope">{{ scope.row.mac || '-' }}</template>
</el-table-column> </el-table-column>
<el-table-column prop="deviceSn" header-align="center" align="center" min-width="140" label="设备SN" show-overflow-tooltip> <el-table-column prop="storeOrgName" header-align="center" align="center" min-width="140" label="门店" show-overflow-tooltip>
<template #default="scope">{{ scope.row.deviceSn || '-' }}</template>
</el-table-column>
<el-table-column prop="storeOrgName" header-align="center" align="center" min-width="140" label="门店">
<template #default="scope">{{ scope.row.storeOrgName || '-' }}</template> <template #default="scope">{{ scope.row.storeOrgName || '-' }}</template>
</el-table-column> </el-table-column>
<el-table-column prop="createTime" header-align="center" align="center" width="170" label="调用时间" /> <el-table-column prop="createTime" header-align="center" align="center" width="170" label="调用时间" />
@@ -60,26 +75,34 @@
</div> </div>
<!-- 详情弹窗 --> <!-- 详情弹窗 -->
<el-dialog v-model="detailVisible" title="调用详情" width="760px" top="5vh"> <el-dialog v-model="detailVisible" title="橘视厅调用详情" width="860px" top="5vh">
<el-descriptions :column="1" border> <el-descriptions :column="1" border>
<el-descriptions-item label="接口">{{ detailData.apiPath }}</el-descriptions-item> <el-descriptions-item label="接口路径">
<el-descriptions-item label="方法">{{ detailData.requestMethod || '-' }}</el-descriptions-item> <div style="word-break: break-all">{{ cleanUrl(detailData.apiPath) }}</div>
</el-descriptions-item>
<el-descriptions-item label="请求方法">{{ detailData.requestMethod || '-' }}</el-descriptions-item>
<el-descriptions-item label="结果"> <el-descriptions-item label="结果">
<el-tag v-if="detailData.success === 1" size="small" type="success">成功</el-tag> <el-tag v-if="detailData.success === 1" size="small" type="success">成功</el-tag>
<el-tag v-else size="small" type="danger">失败</el-tag> <el-tag v-else size="small" type="danger">失败</el-tag>
<span style="margin-left:8px; color:#909399; font-size:12px;">HTTP {{ detailData.responseCode || '-' }}</span>
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="状态码">{{ detailData.responseCode || '-' }}</el-descriptions-item>
<el-descriptions-item label="耗时">{{ detailData.duration ?? '-' }} ms</el-descriptions-item> <el-descriptions-item label="耗时">{{ detailData.duration ?? '-' }} ms</el-descriptions-item>
<el-descriptions-item label="MAC">{{ detailData.mac || '-' }}</el-descriptions-item> <el-descriptions-item label="MAC">{{ detailData.mac || '-' }}</el-descriptions-item>
<el-descriptions-item label="设备SN">{{ detailData.deviceSn || '-' }}</el-descriptions-item> <el-descriptions-item label="设备SN">{{ detailData.deviceSn || '-' }}</el-descriptions-item>
<el-descriptions-item label="设备UID">{{ detailData.deviceUid || '-' }}</el-descriptions-item> <el-descriptions-item label="设备UID">{{ detailData.deviceUid || '-' }}</el-descriptions-item>
<el-descriptions-item label="门店">{{ detailData.storeOrgName || '-' }}</el-descriptions-item> <el-descriptions-item label="门店">{{ detailData.storeOrgName || '-' }}</el-descriptions-item>
<el-descriptions-item v-if="detailData.errorMessage" label="错误信息">{{ detailData.errorMessage }}</el-descriptions-item> <el-descriptions-item v-if="detailData.clientIp" label="客户端IP">{{ detailData.clientIp }}</el-descriptions-item>
<el-descriptions-item v-if="detailData.errorMessage" label="错误信息">
<pre class="log-pre">{{ detailData.errorMessage }}</pre>
</el-descriptions-item>
<el-descriptions-item label="请求头">
<pre class="log-pre">{{ formatJson(detailData.requestHeaders) }}</pre>
</el-descriptions-item>
<el-descriptions-item label="请求体"> <el-descriptions-item label="请求体">
<pre style="white-space: pre-wrap; word-break: break-all; max-height: 200px; overflow: auto; margin: 0">{{ detailData.requestBody || '-' }}</pre> <pre class="log-pre">{{ formatJson(detailData.requestBody) }}</pre>
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="响应体"> <el-descriptions-item label="响应体">
<pre style="white-space: pre-wrap; word-break: break-all; max-height: 200px; overflow: auto; margin: 0">{{ detailData.responseBody || '-' }}</pre> <pre class="log-pre">{{ formatJson(detailData.responseBody) }}</pre>
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="调用时间">{{ detailData.createTime || '-' }}</el-descriptions-item> <el-descriptions-item label="调用时间">{{ detailData.createTime || '-' }}</el-descriptions-item>
</el-descriptions> </el-descriptions>
@@ -97,16 +120,54 @@ import { getJstCallLogList, type JstCallLogItem } from '@/api/manage/jstCallLog'
interface DataForm { interface DataForm {
mac: string mac: string
apiPath: string apiPath: string
success: number | ''
beginTime: string beginTime: string
endTime: string endTime: string
} }
const dataForm = reactive<DataForm>({ mac: '', apiPath: '', beginTime: '', endTime: '' }) const dataForm = reactive<DataForm>({ mac: '', apiPath: '', success: '', beginTime: '', endTime: '' })
const dataList = ref<JstCallLogItem[]>([]) const dataList = ref<JstCallLogItem[]>([])
const pageIndex = ref<number>(1) const pageIndex = ref<number>(1)
const pageSize = ref<number>(10) const pageSize = ref<number>(10)
const totalPage = ref<number>(0) const totalPage = ref<number>(0)
const dataListLoading = ref<boolean>(false) const dataListLoading = ref<boolean>(false)
/** 清理 URL 首尾的反引号和空格 */
function cleanUrl (url?: string): string {
if (!url) return '-'
return url.trim().replace(/^`|`$/g, '')
}
/** 列表展示用:仅取 host + path,去掉 query 串 */
function apiShort (url?: string): string {
const clean = cleanUrl(url)
if (clean === '-') return clean
// 去掉 query
const qIdx = clean.indexOf('?')
const noQuery = qIdx > -1 ? clean.substring(0, qIdx) : clean
// 提取 host + path
const m = noQuery.match(/^https?:\/\/[^/]+(.*)$/i)
if (m && m[1]) return m[1]
return noQuery
}
/** 耗时颜色:<300ms 绿,<1000ms 普通,>=1000ms 红 */
function durationClass (duration?: number): string {
if (duration == null) return ''
if (duration < 300) return 'dur-fast'
if (duration >= 1000) return 'dur-slow'
return ''
}
/** 尝试格式化 JSON 字符串,失败则原样返回 */
function formatJson (str?: string): string {
if (!str) return '-'
try {
return JSON.stringify(JSON.parse(str), null, 2)
} catch {
return str
}
}
async function getDataList (): Promise<void> { async function getDataList (): Promise<void> {
dataListLoading.value = true dataListLoading.value = true
try { try {
@@ -115,6 +176,7 @@ async function getDataList (): Promise<void> {
limit: pageSize.value, limit: pageSize.value,
mac: dataForm.mac || undefined, mac: dataForm.mac || undefined,
apiPath: dataForm.apiPath || undefined, apiPath: dataForm.apiPath || undefined,
success: dataForm.success === '' ? undefined : (dataForm.success as number),
beginTime: dataForm.beginTime || undefined, beginTime: dataForm.beginTime || undefined,
endTime: dataForm.endTime || undefined endTime: dataForm.endTime || undefined
}) })
@@ -158,4 +220,21 @@ onMounted(() => {
.mod-jst-call-log { .mod-jst-call-log {
padding: 10px; padding: 10px;
} }
.log-pre {
white-space: pre-wrap;
word-break: break-all;
max-height: 240px;
overflow: auto;
margin: 0;
background: #f5f7fa;
padding: 8px;
border-radius: 4px;
font-size: 12px;
}
.dur-fast {
color: #67c23a;
}
.dur-slow {
color: #f56c6c;
}
</style> </style>

View File

@@ -2,15 +2,15 @@
<div class="mod-partner-device"> <div class="mod-partner-device">
<el-form :inline="true" :model="dataForm" @keyup.enter="getDataList()"> <el-form :inline="true" :model="dataForm" @keyup.enter="getDataList()">
<el-form-item> <el-form-item>
<el-select v-model="dataForm.partnerId" placeholder="合作商" clearable filterable style="width:180px"> <el-select v-model="dataForm.partnerId" placeholder="合作伙伴" clearable filterable style="width:180px">
<el-option v-for="p in partnerOptions" :key="p.id" :label="p.name" :value="p.partnerId"></el-option> <el-option v-for="p in partnerOptions" :key="p.id" :label="p.name" :value="p.partnerId"></el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-input v-model="dataForm.deviceName" placeholder="设备名称" clearable style="width:160px"></el-input> <el-input v-model="dataForm.mac" placeholder="MAC地址" clearable style="width:160px"></el-input>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-input v-model="dataForm.mac" placeholder="MAC地址" clearable style="width:160px"></el-input> <el-input v-model="dataForm.deviceSn" placeholder="设备SN" clearable style="width:160px"></el-input>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button @click="getDataList()">查询</el-button> <el-button @click="getDataList()">查询</el-button>
@@ -18,25 +18,37 @@
</el-form> </el-form>
<div class="table-page-wrap"> <div class="table-page-wrap">
<el-table :data="dataList" border v-loading="dataListLoading" style="width: 100%"> <el-table :data="dataList" border v-loading="dataListLoading" style="width: 100%">
<el-table-column type="index" header-align="center" align="center" width="60" label="序号" /> <el-table-column type="index" header-align="center" align="center" width="55" label="序号" />
<el-table-column header-align="center" align="center" min-width="140" label="合作商"> <el-table-column header-align="center" align="center" width="90" label="来源">
<template #default="scope">
<el-tag v-if="scope.row.source === 'PARTNER'" size="small" type="warning">第三方</el-tag>
<el-tag v-else-if="scope.row.source === 'DIRECT'" size="small" type="success">自营</el-tag>
<span v-else>{{ scope.row.source || '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="mac" header-align="center" align="center" width="150" label="MAC" show-overflow-tooltip />
<el-table-column prop="wifiMac" header-align="center" align="center" width="150" label="WiFi MAC" show-overflow-tooltip>
<template #default="scope">{{ scope.row.wifiMac || '-' }}</template>
</el-table-column>
<el-table-column prop="deviceSn" header-align="center" align="center" min-width="160" label="SN" show-overflow-tooltip>
<template #default="scope">{{ scope.row.deviceSn || '-' }}</template>
</el-table-column>
<el-table-column prop="deviceUid" header-align="center" align="center" min-width="140" label="设备UID" show-overflow-tooltip>
<template #default="scope">{{ scope.row.deviceUid || '-' }}</template>
</el-table-column>
<el-table-column header-align="center" align="center" min-width="140" label="合作伙伴">
<template #default="scope">{{ partnerNameOf(scope.row.partnerId) }}</template> <template #default="scope">{{ partnerNameOf(scope.row.partnerId) }}</template>
</el-table-column> </el-table-column>
<el-table-column prop="deviceName" header-align="center" align="center" min-width="140" label="设备名称" /> <el-table-column prop="status" header-align="center" align="center" width="90" label="状态">
<el-table-column prop="mac" header-align="center" align="center" width="140" label="MAC" />
<el-table-column prop="deviceSn" header-align="center" align="center" min-width="160" label="设备SN" show-overflow-tooltip />
<el-table-column prop="deviceModel" header-align="center" align="center" width="120" label="型号" />
<el-table-column prop="status" header-align="center" align="center" width="100" label="状态">
<template #default="scope"> <template #default="scope">
<el-tag v-if="scope.row.status === 'ACTIVE'" size="small" type="success">启用</el-tag> <el-tag v-if="scope.row.status === 'ACTIVE'" size="small" type="success">已激活</el-tag>
<el-tag v-else-if="scope.row.status === 'INACTIVE'" size="small" type="danger">禁用</el-tag> <el-tag v-else-if="scope.row.status === 'INACTIVE'" size="small" type="info">未激活</el-tag>
<el-tag v-else size="small">{{ scope.row.status }}</el-tag> <el-tag v-else size="small">{{ scope.row.status }}</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="storeName" header-align="center" align="center" min-width="140" label="绑定门店"> <el-table-column prop="lastHeartbeat" header-align="center" align="center" width="170" label="心跳时间">
<template #default="scope">{{ scope.row.storeName || '-' }}</template> <template #default="scope">{{ scope.row.lastHeartbeat || '-' }}</template>
</el-table-column> </el-table-column>
<el-table-column prop="createdAt" header-align="center" align="center" width="170" label="注册时间" />
</el-table> </el-table>
<el-pagination <el-pagination
@size-change="sizeChangeHandle" @size-change="sizeChangeHandle"
@@ -58,10 +70,10 @@ import { getPartnerList, type PartnerInfo } from '@/api/manage/partner'
interface DataForm { interface DataForm {
partnerId: string | null partnerId: string | null
deviceName: string
mac: string mac: string
deviceSn: string
} }
const dataForm = reactive<DataForm>({ partnerId: null, deviceName: '', mac: '' }) const dataForm = reactive<DataForm>({ partnerId: null, mac: '', deviceSn: '' })
const dataList = ref<DeviceInfo[]>([]) const dataList = ref<DeviceInfo[]>([])
const pageIndex = ref<number>(1) const pageIndex = ref<number>(1)
const pageSize = ref<number>(10) const pageSize = ref<number>(10)
@@ -94,8 +106,8 @@ async function getDataList (): Promise<void> {
limit: pageSize.value, limit: pageSize.value,
source: 'PARTNER', source: 'PARTNER',
partnerId: dataForm.partnerId ?? undefined, partnerId: dataForm.partnerId ?? undefined,
deviceName: dataForm.deviceName || undefined, mac: dataForm.mac || undefined,
mac: dataForm.mac || undefined deviceSn: dataForm.deviceSn || undefined
}) })
if (res && res.code === 0) { if (res && res.code === 0) {
dataList.value = res.data?.page?.list || [] dataList.value = res.data?.page?.list || []

View File

@@ -86,8 +86,26 @@
<el-form-item label="证书序列号" prop="certSerialNo"> <el-form-item label="证书序列号" prop="certSerialNo">
<el-input v-model="addOrUpdateForm.certSerialNo" placeholder="请输入证书序列号"></el-input> <el-input v-model="addOrUpdateForm.certSerialNo" placeholder="请输入证书序列号"></el-input>
</el-form-item> </el-form-item>
<el-form-item label="私钥路径" prop="privateKeyPath"> <el-form-item label="商户API私钥" prop="privateKey">
<el-input v-model="addOrUpdateForm.privateKeyPath" placeholder="请输入私钥路径"></el-input> <div style="display:flex; flex-direction:column; gap:8px; width:100%;">
<el-input
v-model="addOrUpdateForm.privateKey"
type="textarea"
:rows="6"
placeholder="请粘贴 apiclient_key.pem 文件内容(-----BEGIN PRIVATE KEY----- 开头)或上传文件">
</el-input>
<el-upload
:auto-upload="false"
:show-file-list="false"
:limit="1"
accept=".pem"
:on-change="onPemFileChange">
<el-button size="small" type="primary" plain>上传 .pem 文件</el-button>
<template #tip>
<span style="margin-left:8px; color:#909399; font-size:12px;">仅读取本地文件内容,不会上传到服务器</span>
</template>
</el-upload>
</div>
</el-form-item> </el-form-item>
<el-form-item label="回调地址" prop="notifyUrl"> <el-form-item label="回调地址" prop="notifyUrl">
<el-input v-model="addOrUpdateForm.notifyUrl" placeholder="请输入回调地址"></el-input> <el-input v-model="addOrUpdateForm.notifyUrl" placeholder="请输入回调地址"></el-input>
@@ -109,7 +127,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { reactive, ref, computed, onMounted } from 'vue' import { reactive, ref, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus' import { ElMessage, ElMessageBox, type FormInstance, type FormRules, type UploadFile } from 'element-plus'
import { getPayConfigList, saveOrUpdatePayConfig, deletePayConfig, type PayConfigInfo } from '@/api/manage/payConfig' import { getPayConfigList, saveOrUpdatePayConfig, deletePayConfig, type PayConfigInfo } from '@/api/manage/payConfig'
import OrgTreeSelect from '@/components/org-tree-select/index.vue' import OrgTreeSelect from '@/components/org-tree-select/index.vue'
@@ -175,7 +193,7 @@ interface AddOrUpdateForm {
mchId: string mchId: string
apiV3Key: string apiV3Key: string
certSerialNo: string certSerialNo: string
privateKeyPath: string privateKey: string
notifyUrl: string notifyUrl: string
status: string status: string
remark: string remark: string
@@ -189,7 +207,7 @@ const addOrUpdateForm = reactive<AddOrUpdateForm>({
mchId: '', mchId: '',
apiV3Key: '', apiV3Key: '',
certSerialNo: '', certSerialNo: '',
privateKeyPath: '', privateKey: '',
notifyUrl: '', notifyUrl: '',
status: 'ACTIVE', status: 'ACTIVE',
remark: '' remark: ''
@@ -218,6 +236,28 @@ function onLevelChange (): void {
addOrUpdateFormRef.value?.clearValidate('orgId') addOrUpdateFormRef.value?.clearValidate('orgId')
} }
/** 读取本地 .pem 文件内容到 privateKey(不上传服务器) */
function onPemFileChange (file: UploadFile): void {
if (!file.raw) return
const reader = new FileReader()
reader.onload = () => {
const text = String(reader.result || '').trim()
if (!text) {
ElMessage.warning('文件内容为空')
return
}
if (!text.includes('BEGIN') && !text.includes('PRIVATE KEY')) {
ElMessage.warning('文件内容不像 PEM 私钥,请确认文件正确')
}
addOrUpdateForm.privateKey = text
ElMessage.success('已读取私钥文件内容')
}
reader.onerror = () => {
ElMessage.error('读取文件失败')
}
reader.readAsText(file.raw)
}
function resetForm (): void { function resetForm (): void {
addOrUpdateForm.id = null addOrUpdateForm.id = null
addOrUpdateForm.configLevel = 'GLOBAL' addOrUpdateForm.configLevel = 'GLOBAL'
@@ -226,7 +266,7 @@ function resetForm (): void {
addOrUpdateForm.mchId = '' addOrUpdateForm.mchId = ''
addOrUpdateForm.apiV3Key = '' addOrUpdateForm.apiV3Key = ''
addOrUpdateForm.certSerialNo = '' addOrUpdateForm.certSerialNo = ''
addOrUpdateForm.privateKeyPath = '' addOrUpdateForm.privateKey = ''
addOrUpdateForm.notifyUrl = '' addOrUpdateForm.notifyUrl = ''
addOrUpdateForm.status = 'ACTIVE' addOrUpdateForm.status = 'ACTIVE'
addOrUpdateForm.remark = '' addOrUpdateForm.remark = ''
@@ -247,7 +287,7 @@ function editHandle (row: PayConfigInfo): void {
addOrUpdateForm.mchId = row.mchId addOrUpdateForm.mchId = row.mchId
addOrUpdateForm.apiV3Key = row.apiV3Key addOrUpdateForm.apiV3Key = row.apiV3Key
addOrUpdateForm.certSerialNo = row.certSerialNo addOrUpdateForm.certSerialNo = row.certSerialNo
addOrUpdateForm.privateKeyPath = row.privateKeyPath addOrUpdateForm.privateKey = row.privateKey
addOrUpdateForm.notifyUrl = row.notifyUrl addOrUpdateForm.notifyUrl = row.notifyUrl
addOrUpdateForm.status = row.status addOrUpdateForm.status = row.status
addOrUpdateForm.remark = row.remark addOrUpdateForm.remark = row.remark
@@ -278,7 +318,7 @@ function submitForm (): void {
mchId: addOrUpdateForm.mchId, mchId: addOrUpdateForm.mchId,
apiV3Key: addOrUpdateForm.apiV3Key, apiV3Key: addOrUpdateForm.apiV3Key,
certSerialNo: addOrUpdateForm.certSerialNo, certSerialNo: addOrUpdateForm.certSerialNo,
privateKeyPath: addOrUpdateForm.privateKeyPath, privateKey: addOrUpdateForm.privateKey,
notifyUrl: addOrUpdateForm.notifyUrl, notifyUrl: addOrUpdateForm.notifyUrl,
status: addOrUpdateForm.status, status: addOrUpdateForm.status,
remark: addOrUpdateForm.remark remark: addOrUpdateForm.remark

View File

@@ -198,9 +198,8 @@ function init (id?: number): void {
// 修改 // 修改
getMenuInfo(dataForm.id).then((data) => { getMenuInfo(dataForm.id).then((data) => {
if (!data || data.code !== 0) return if (!data || data.code !== 0) return
const body = data as unknown as { data?: MenuInfo; menu?: MenuInfo } const menu = data.data?.menu
const menu = body.data ?? body.menu if (menu && typeof menu === 'object' && menu.id) {
if (menu && typeof menu === 'object') {
dataForm.id = menu.id dataForm.id = menu.id
dataForm.type = menu.type ?? 1 dataForm.type = menu.type ?? 1
dataForm.name = menu.name dataForm.name = menu.name

View File

@@ -104,13 +104,8 @@ function init (id?: number): void {
if (dataForm.id) { if (dataForm.id) {
getRoleInfo(dataForm.id).then((body) => { getRoleInfo(dataForm.id).then((body) => {
if (!body || body.code !== 0) return if (!body || body.code !== 0) return
const env = body as unknown as { const role = body.data?.role as (RoleInfo & { menuIdList?: number[] }) | undefined
data?: RoleInfo & { menuIdList?: number[] } if (!role || typeof role !== 'object') return
role?: RoleInfo & { menuIdList?: number[] }
}
const rolePayload = env.data ?? env.role
if (!rolePayload || typeof rolePayload !== 'object') return
const role = rolePayload as RoleInfo & { menuIdList: number[] }
dataForm.roleName = role.roleName || '' dataForm.roleName = role.roleName || ''
dataForm.remark = role.remark || '' dataForm.remark = role.remark || ''
const menuIdList = Array.isArray(role.menuIdList) ? role.menuIdList : [] const menuIdList = Array.isArray(role.menuIdList) ? role.menuIdList : []

View File

@@ -265,8 +265,7 @@ function init (id?: number | string | null): void {
if (id !== null && id !== undefined && id !== '') { if (id !== null && id !== undefined && id !== '') {
getUserInfo(Number(id)).then((body) => { getUserInfo(Number(id)).then((body) => {
if (!body || body.code !== 0) return if (!body || body.code !== 0) return
const env = body as unknown as { data?: UserInfo; user?: UserInfo } const user = body.data?.user
const user = env.data ?? env.user
if (!user || typeof user !== 'object') return if (!user || typeof user !== 'object') return
dataForm.id = user.id dataForm.id = user.id
dataForm.userName = user.username || '' dataForm.userName = user.username || ''