Compare commits

..

2 Commits

Author SHA1 Message Date
神码-方晓辉 7e327e2131 fix: 新增应用时自动生成编码和密钥
- 自动生成应用编码:YY + 6位序号
- 自动生成32位应用密钥
2026-02-02 17:14:24 +08:00
神码-方晓辉 ec2d69a09f feat: 完善应用管理模块前后端
- 后端:添加重置密钥接口和API列表接口
- 后端:新增 IPgApiDictService 服务
- 前端:修复API路径(/api -> /business)
- 前端:修复字段名映射(contactName -> contactPerson)
- 前端:添加 SecretDialog 密钥弹窗组件
- 数据:初始化应用数据和API字典数据
2026-02-02 17:11:06 +08:00
8 changed files with 243 additions and 49 deletions

View File

@ -1,6 +1,7 @@
package org.dromara.pangu.application.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.util.IdUtil;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
import org.dromara.common.log.annotation.Log;
@ -8,11 +9,15 @@ import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.web.core.BaseController;
import org.dromara.pangu.application.domain.PgApiDict;
import org.dromara.pangu.application.domain.PgApplication;
import org.dromara.pangu.application.service.IPgApiDictService;
import org.dromara.pangu.application.service.IPgApplicationService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 第三方应用管理
*
@ -25,6 +30,7 @@ import org.springframework.web.bind.annotation.*;
public class PgApplicationController extends BaseController {
private final IPgApplicationService applicationService;
private final IPgApiDictService apiDictService;
@SaCheckPermission("business:application:list")
@GetMapping("/list")
@ -58,4 +64,31 @@ public class PgApplicationController extends BaseController {
public R<Void> remove(@PathVariable Long[] appIds) {
return toAjax(applicationService.deleteByIds(appIds));
}
/**
* 重置应用密钥
*/
@SaCheckPermission("business:application:edit")
@Log(title = "应用管理", businessType = BusinessType.UPDATE)
@PutMapping("/resetSecret/{appId}")
public R<PgApplication> resetSecret(@PathVariable Long appId) {
PgApplication app = applicationService.selectById(appId);
if (app == null) {
return R.fail("应用不存在");
}
// 生成新的32位密钥
String newSecret = IdUtil.fastSimpleUUID();
app.setAppSecret(newSecret);
applicationService.update(app);
return R.ok(app);
}
/**
* 获取API接口列表用于授权选择
*/
@SaCheckPermission("business:application:list")
@GetMapping("/apiList")
public R<List<PgApiDict>> apiList() {
return R.ok(apiDictService.selectList());
}
}

View File

@ -0,0 +1,18 @@
package org.dromara.pangu.application.service;
import org.dromara.pangu.application.domain.PgApiDict;
import java.util.List;
/**
* API接口字典 Service 接口
*
* @author pangu
*/
public interface IPgApiDictService {
/**
* 查询所有启用的API接口列表
*/
List<PgApiDict> selectList();
}

View File

@ -0,0 +1,30 @@
package org.dromara.pangu.application.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.RequiredArgsConstructor;
import org.dromara.pangu.application.domain.PgApiDict;
import org.dromara.pangu.application.mapper.PgApiDictMapper;
import org.dromara.pangu.application.service.IPgApiDictService;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* API接口字典 Service 实现
*
* @author pangu
*/
@RequiredArgsConstructor
@Service
public class PgApiDictServiceImpl implements IPgApiDictService {
private final PgApiDictMapper baseMapper;
@Override
public List<PgApiDict> selectList() {
LambdaQueryWrapper<PgApiDict> lqw = new LambdaQueryWrapper<>();
lqw.eq(PgApiDict::getStatus, "0");
lqw.orderByAsc(PgApiDict::getOrderNum);
return baseMapper.selectList(lqw);
}
}

View File

@ -1,5 +1,6 @@
package org.dromara.pangu.application.service.impl;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
@ -44,9 +45,39 @@ public class PgApplicationServiceImpl implements IPgApplicationService {
@Override
public int insert(PgApplication app) {
// 自动生成应用编码YY + 6位序号
String appCode = generateAppCode();
app.setAppCode(appCode);
// 自动生成32位应用密钥
app.setAppSecret(IdUtil.fastSimpleUUID());
return baseMapper.insert(app);
}
/**
* 生成应用编码
*/
private String generateAppCode() {
// 查询当前最大编码
LambdaQueryWrapper<PgApplication> lqw = new LambdaQueryWrapper<>();
lqw.select(PgApplication::getAppCode);
lqw.likeRight(PgApplication::getAppCode, "YY");
lqw.orderByDesc(PgApplication::getAppCode);
lqw.last("LIMIT 1");
PgApplication lastApp = baseMapper.selectOne(lqw);
int nextNum = 1;
if (lastApp != null && StrUtil.isNotBlank(lastApp.getAppCode())) {
String lastCode = lastApp.getAppCode();
if (lastCode.length() > 2) {
try {
nextNum = Integer.parseInt(lastCode.substring(2)) + 1;
} catch (NumberFormatException ignored) {
}
}
}
return String.format("YY%06d", nextNum);
}
@Override
public int update(PgApplication app) {
return baseMapper.updateById(app);

View File

@ -9,7 +9,7 @@ import request from '@/utils/request'
*/
export function getApplicationList(params) {
return request({
url: '/api/application/list',
url: '/business/application/list',
method: 'get',
params
})
@ -18,9 +18,9 @@ export function getApplicationList(params) {
/**
* 获取应用详情
*/
export function getApplicationDetail(id) {
export function getApplicationDetail(appId) {
return request({
url: `/api/application/${id}`,
url: `/business/application/${appId}`,
method: 'get'
})
}
@ -30,7 +30,7 @@ export function getApplicationDetail(id) {
*/
export function addApplication(data) {
return request({
url: '/api/application',
url: '/business/application',
method: 'post',
data
})
@ -41,7 +41,7 @@ export function addApplication(data) {
*/
export function updateApplication(data) {
return request({
url: '/api/application',
url: '/business/application',
method: 'put',
data
})
@ -50,9 +50,9 @@ export function updateApplication(data) {
/**
* 删除应用
*/
export function deleteApplication(id) {
export function deleteApplication(appId) {
return request({
url: `/api/application/${id}`,
url: `/business/application/${appId}`,
method: 'delete'
})
}
@ -60,10 +60,10 @@ export function deleteApplication(id) {
/**
* 重置应用密钥
*/
export function resetAppSecret(id) {
export function resetAppSecret(appId) {
return request({
url: `/api/application/${id}/resetSecret`,
method: 'post'
url: `/business/application/resetSecret/${appId}`,
method: 'put'
})
}
@ -72,7 +72,7 @@ export function resetAppSecret(id) {
*/
export function getApiAuthOptions() {
return request({
url: '/api/application/authOptions',
url: '/business/application/apiList',
method: 'get'
})
}

View File

@ -19,18 +19,18 @@
<el-form-item label="应用编码" prop="appCode">
<el-input v-model="form.appCode" placeholder="保存后自动生成" disabled />
</el-form-item>
<el-form-item label="应用描述" prop="description">
<el-form-item label="备注" prop="remark">
<el-input
v-model="form.description"
v-model="form.remark"
type="textarea"
placeholder="请输入应用描述"
placeholder="请输入备注说明"
:rows="3"
maxlength="200"
show-word-limit
/>
</el-form-item>
<el-form-item label="联系人" prop="contactName">
<el-input v-model="form.contactName" placeholder="请输入联系人" maxlength="20" />
<el-form-item label="联系人" prop="contactPerson">
<el-input v-model="form.contactPerson" placeholder="请输入联系人" maxlength="20" />
</el-form-item>
<el-form-item label="联系电话" prop="contactPhone">
<el-input v-model="form.contactPhone" placeholder="请输入联系电话" maxlength="11" />
@ -44,14 +44,14 @@
inactive-text="停用"
/>
</el-form-item>
<el-form-item label="接口授权" prop="apiAuth">
<el-checkbox-group v-model="form.apiAuth">
<el-form-item label="接口授权" prop="apiCodes">
<el-checkbox-group v-model="form.apiCodes">
<el-checkbox
v-for="item in apiAuthOptions"
:key="item.value"
:label="item.value"
:key="item.apiCode"
:label="item.apiCode"
>
{{ item.label }}
{{ item.apiName }}
</el-checkbox>
</el-checkbox-group>
</el-form-item>
@ -68,9 +68,9 @@
* 应用管理 - 新增/编辑弹窗
* @author pangu
*/
import { addApplication, getApiAuthOptions, updateApplication } from '@/api/pangu/application'
import { ElMessage } from 'element-plus'
import { computed, nextTick, reactive, ref } from 'vue'
import request from '@/utils/request'
const emit = defineEmits(['success'])
@ -81,14 +81,14 @@ const isEdit = ref(false)
const apiAuthOptions = ref([])
const form = reactive({
id: null,
appId: null,
appName: '',
appCode: '',
description: '',
contactName: '',
remark: '',
contactPerson: '',
contactPhone: '',
status: '0',
apiAuth: []
apiCodes: []
})
const rules = {
@ -105,9 +105,9 @@ const dialogTitle = computed(() => isEdit.value ? '编辑应用' : '新增应用
//
const loadAuthOptions = async () => {
try {
const res = await getApiAuthOptions()
const res = await request.get('/business/application/apiList')
if (res.code === 200) {
apiAuthOptions.value = res.data
apiAuthOptions.value = res.data || []
}
} catch (error) {
console.error('加载接口授权选项失败:', error)
@ -123,14 +123,14 @@ const open = (row = null) => {
isEdit.value = true
nextTick(() => {
Object.assign(form, {
id: row.id,
appId: row.appId,
appName: row.appName,
appCode: row.appCode,
description: row.description || '',
contactName: row.contactName || '',
remark: row.remark || '',
contactPerson: row.contactPerson || '',
contactPhone: row.contactPhone || '',
status: row.status || '0',
apiAuth: row.apiAuth || []
apiCodes: row.apiCodes || []
})
})
} else {
@ -144,8 +144,8 @@ const handleSubmit = async () => {
await formRef.value.validate()
submitLoading.value = true
const api = isEdit.value ? updateApplication : addApplication
const res = await api(form)
const method = isEdit.value ? 'put' : 'post'
const res = await request[method]('/business/application', form)
if (res.code === 200) {
ElMessage.success(isEdit.value ? '修改成功' : '新增成功')
@ -165,14 +165,14 @@ const handleSubmit = async () => {
const handleClosed = () => {
formRef.value?.resetFields()
Object.assign(form, {
id: null,
appId: null,
appName: '',
appCode: '',
description: '',
contactName: '',
remark: '',
contactPerson: '',
contactPhone: '',
status: '0',
apiAuth: []
apiCodes: []
})
}

View File

@ -0,0 +1,88 @@
<template>
<el-dialog
v-model="visible"
title="应用密钥"
width="500px"
:close-on-click-modal="false"
>
<el-alert
title="请妥善保管密钥,密钥重置后旧密钥将立即失效"
type="warning"
:closable="false"
show-icon
style="margin-bottom: 16px;"
/>
<el-form label-width="100px">
<el-form-item label="应用名称">
<span>{{ appInfo.appName }}</span>
</el-form-item>
<el-form-item label="应用编码">
<span>{{ appInfo.appCode }}</span>
</el-form-item>
<el-form-item label="应用密钥">
<el-input
v-model="appInfo.appSecret"
readonly
style="width: 280px;"
>
<template #append>
<el-button @click="handleCopy">
<el-icon><DocumentCopy /></el-icon>
复制
</el-button>
</template>
</el-input>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="visible = false">关闭</el-button>
</template>
</el-dialog>
</template>
<script setup>
/**
* 应用管理 - 密钥展示弹窗
* @author pangu
*/
import { DocumentCopy } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { reactive, ref } from 'vue'
const visible = ref(false)
const appInfo = reactive({
appName: '',
appCode: '',
appSecret: ''
})
//
const open = (row) => {
visible.value = true
Object.assign(appInfo, {
appName: row.appName,
appCode: row.appCode,
appSecret: row.appSecret
})
}
//
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(appInfo.appSecret)
ElMessage.success('复制成功')
} catch (error) {
//
const textarea = document.createElement('textarea')
textarea.value = appInfo.appSecret
document.body.appendChild(textarea)
textarea.select()
document.execCommand('copy')
document.body.removeChild(textarea)
ElMessage.success('复制成功')
}
}
defineExpose({ open })
</script>

View File

@ -33,14 +33,9 @@
<el-table v-loading="loading" :data="tableData" border stripe :header-cell-style="{ background: '#f5f7fa', color: '#606266' }" style="width: 100%">
<el-table-column prop="appName" label="应用名称" min-width="150" show-overflow-tooltip />
<el-table-column prop="appCode" label="应用编码" width="120" />
<el-table-column prop="apis" label="授权接口" min-width="250">
<template #default="{ row }">
<el-tag v-for="api in (row.apis || []).slice(0, 3)" :key="api" size="small" style="margin-right: 4px; margin-bottom: 2px">
{{ api }}
</el-tag>
<el-tag v-if="(row.apis || []).length > 3" size="small" type="info">+{{ row.apis.length - 3 }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="contactPerson" label="联系人" width="100" />
<el-table-column prop="contactPhone" label="联系电话" width="120" />
<el-table-column prop="remark" label="备注" min-width="200" show-overflow-tooltip />
<el-table-column prop="status" label="状态" width="80" align="center">
<template #default="{ row }">
<el-tag :type="row.status === '0' ? 'success' : 'danger'">
@ -49,7 +44,6 @@
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="160" />
<el-table-column prop="createBy" label="创建人" width="100" />
<el-table-column label="操作" width="180" fixed="right" align="center">
<template #default="{ row }">
<el-button type="primary" link :icon="Edit" @click="handleEdit(row)">编辑</el-button>
@ -156,7 +150,7 @@ const handleResetSecret = async (row) => {
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
const res = await request.put(`/business/application/resetSecret/${row.id}`)
const res = await request.put(`/business/application/resetSecret/${row.appId}`)
if (res.code === 200) {
ElMessage.success('重置成功')
secretDialogRef.value?.open(res.data)
@ -171,7 +165,7 @@ const handleDelete = (row) => {
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
const res = await request.delete(`/business/application/${row.id}`)
const res = await request.delete(`/business/application/${row.appId}`)
if (res.code === 200) {
ElMessage.success('删除成功')
getList()