使用MinIO存储个人头像

This commit is contained in:
dataprince 2023-12-03 09:40:49 +08:00
parent c646c95c7b
commit 478c128ba2
43 changed files with 2844 additions and 364 deletions

View File

@ -0,0 +1,26 @@
import request from '@/utils/request';
// 查询OSS对象存储列表
export function listOss(query) {
return request({
url: '/resource/oss/list',
method: 'get',
params: query
});
}
// 查询OSS对象基于id串
export function listByIds(ossId) {
return request({
url: '/resource/oss/listByIds/' + ossId,
method: 'get'
});
}
// 删除OSS对象存储
export function delOss(ossId) {
return request({
url: '/resource/oss/' + ossId,
method: 'delete'
});
}

View File

@ -0,0 +1,58 @@
import request from '@/utils/request';
// 查询对象存储配置列表
export function listOssConfig(query) {
return request({
url: '/resource/oss/config/list',
method: 'get',
params: query
});
}
// 查询对象存储配置详细
export function getOssConfig(ossConfigId) {
return request({
url: '/resource/oss/config/' + ossConfigId,
method: 'get'
});
}
// 新增对象存储配置
export function addOssConfig(data) {
return request({
url: '/resource/oss/config',
method: 'post',
data: data
});
}
// 修改对象存储配置
export function updateOssConfig(data) {
return request({
url: '/resource/oss/config',
method: 'put',
data: data
});
}
// 删除对象存储配置
export function delOssConfig(ossConfigId) {
return request({
url: '/resource/oss/config/' + ossConfigId,
method: 'delete'
});
}
// 对象存储状态修改
export function changeOssConfigStatus(ossConfigId, status, configKey) {
const data = {
ossConfigId,
status,
configKey
};
return request({
url: '/resource/oss/config/changeStatus',
method: 'put',
data: data
});
}

View File

@ -104,6 +104,7 @@ export function updateUserPwd(oldPassword, newPassword) {
export function uploadAvatar(data) {
return request({
url: '/system/user/profile/avatar',
headers: { 'Content-Type': 'multipart/form-data' },
method: 'post',
data: data
})

View File

@ -39,7 +39,8 @@
</template>
<script setup>
import { getToken } from "@/utils/auth";
import { listByIds, delOss } from "@/api/system/oss";
import { globalHeaders } from "@/utils/request";
const props = defineProps({
modelValue: [String, Object, Array],
@ -56,7 +57,7 @@ const props = defineProps({
// , ['png', 'jpg', 'jpeg']
fileType: {
type: Array,
default: () => ["doc", "xls", "ppt", "txt", "pdf"],
default: () => ["doc","docx", "xls", "xlsx", "ppt", "txt", "pdf"],
},
//
isShowTip: {
@ -66,27 +67,34 @@ const props = defineProps({
});
const { proxy } = getCurrentInstance();
const emit = defineEmits();
const emit = defineEmits(['update:modelValue']);
const number = ref(0);
const uploadList = ref([]);
const baseUrl = import.meta.env.VITE_APP_BASE_API;
const uploadFileUrl = ref(import.meta.env.VITE_APP_BASE_API + "/common/upload"); //
const headers = ref({ Authorization: "Bearer " + getToken() });
const uploadFileUrl = ref(baseUrl + "/resource/oss/upload"); //
const headers = ref(globalHeaders());
const fileList = ref([]);
const showTip = computed(
() => props.isShowTip && (props.fileType || props.fileSize)
);
watch(() => props.modelValue, val => {
watch(() => props.modelValue, async val => {
if (val) {
let temp = 1;
//
const list = Array.isArray(val) ? val : props.modelValue.split(',');
let list = [];
if (Array.isArray(val)) {
list = val;
} else {
const res = await listByIds(val)
list = res.data.map((oss) => {
const data = { name: oss.originalName, url: oss.url, ossId: oss.ossId };
return data;
});
}
//
fileList.value = list.map(item => {
if (typeof item === "string") {
item = { name: item, url: item };
}
item = { name: item.name, url: item.url, ossId: item.ossId };
item.uid = item.uid || new Date().getTime() + temp++;
return item;
});
@ -134,7 +142,7 @@ function handleUploadError(err) {
//
function handleUploadSuccess(res, file) {
if (res.code === 200) {
uploadList.value.push({ name: res.fileName, url: res.fileName });
uploadList.value.push({ name: res.data.fileName, url: res.data.url, ossId: res.data.ossId });
uploadedSuccessfully();
} else {
number.value--;
@ -147,6 +155,8 @@ function handleUploadSuccess(res, file) {
//
function handleDelete(index) {
let ossId = fileList.value[index].ossId;
delOss(ossId);
fileList.value.splice(index, 1);
emit("update:modelValue", listToString(fileList.value));
}
@ -167,7 +177,7 @@ function getFileName(name) {
if (name.lastIndexOf("/") > -1) {
return name.slice(name.lastIndexOf("/") + 1);
} else {
return "";
return name;
}
}
@ -175,12 +185,12 @@ function getFileName(name) {
function listToString(list, separator) {
let strs = "";
separator = separator || ",";
for (let i in list) {
if (list[i].url) {
strs += list[i].url + separator;
list.forEach(item => {
if (item.ossId) {
strs += item.ossId + separator;
}
}
return strs != '' ? strs.substr(0, strs.length - 1) : '';
})
return strs != "" ? strs.substring(0, strs.length - 1) : "";
}
</script>

View File

@ -46,7 +46,8 @@
</template>
<script setup>
import { getToken } from "@/utils/auth";
import { listByIds, delOss } from "@/api/system/oss";
import {globalHeaders} from "@/utils/request";
const props = defineProps({
modelValue: [String, Object, Array],
@ -73,33 +74,40 @@ const props = defineProps({
});
const { proxy } = getCurrentInstance();
const emit = defineEmits();
const emit = defineEmits(['update:modelValue']);
const number = ref(0);
const uploadList = ref([]);
const dialogImageUrl = ref("");
const dialogVisible = ref(false);
const baseUrl = import.meta.env.VITE_APP_BASE_API;
const uploadImgUrl = ref(import.meta.env.VITE_APP_BASE_API + "/common/upload"); //
const headers = ref({ Authorization: "Bearer " + getToken() });
const uploadImgUrl = ref(baseUrl + "/resource/oss/upload"); //
const headers = ref(globalHeaders());
const fileList = ref([]);
const showTip = computed(
() => props.isShowTip && (props.fileType || props.fileSize)
);
watch(() => props.modelValue, val => {
watch(() => props.modelValue,async val => {
if (val) {
//
const list = Array.isArray(val) ? val : props.modelValue.split(",");
let list = [];
if (Array.isArray(val)) {
list = val;
} else {
const res = await listByIds(val)
list = res.data
}
//
fileList.value = list.map(item => {
// url id
let itemData;
if (typeof item === "string") {
if (item.indexOf(baseUrl) === -1) {
item = { name: baseUrl + item, url: baseUrl + item };
itemData = { name: item, url: item };
} else {
item = { name: item, url: item };
// name使ossId
itemData = { name: item.ossId, url: item.url, ossId: item.ossId };
}
}
return item;
return itemData;
});
} else {
fileList.value = [];
@ -148,7 +156,7 @@ function handleExceed() {
//
function handleUploadSuccess(res, file) {
if (res.code === 200) {
uploadList.value.push({ name: res.fileName, url: res.fileName });
uploadList.value.push({ name: res.data.fileName, url: res.data.url, ossId: res.data.ossId });
uploadedSuccessfully();
} else {
number.value--;
@ -163,6 +171,8 @@ function handleUploadSuccess(res, file) {
function handleDelete(file) {
const findex = fileList.value.map(f => f.name).indexOf(file.name);
if (findex > -1 && uploadList.value.length === number.value) {
let ossId = fileList.value[findex].ossId;
delOss(ossId);
fileList.value.splice(findex, 1);
emit("update:modelValue", listToString(fileList.value));
return false;
@ -197,11 +207,11 @@ function listToString(list, separator) {
let strs = "";
separator = separator || ",";
for (let i in list) {
if (undefined !== list[i].url && list[i].url.indexOf("blob:") !== 0) {
strs += list[i].url.replace(baseUrl, "") + separator;
if (undefined !== list[i].ossId && list[i].url.indexOf("blob:") !== 0) {
strs += list[i].ossId + separator;
}
}
return strs != "" ? strs.substr(0, strs.length - 1) : "";
return strs != "" ? strs.substring(0, strs.length - 1) : "";
}
</script>

View File

@ -36,7 +36,7 @@ const useUserStore = defineStore(
return new Promise((resolve, reject) => {
getInfo().then(res => {
const user = res.user
const avatar = (user.avatar == "" || user.avatar == null) ? defAva : import.meta.env.VITE_APP_BASE_API + user.avatar;
const avatar = (user.url == "" || user.url == null) ? defAva : user.url;
if (res.roles && res.roles.length > 0) { // 验证返回的roles是否是一个非空数组
this.roles = res.roles

View File

@ -0,0 +1,337 @@
<template>
<div class="p-2">
<div class="mb-[10px]" v-show="showSearch">
<el-card shadow="hover">
<el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
<el-form-item label="配置key" prop="configKey">
<el-input v-model="queryParams.configKey" placeholder="配置key" clearable style="width: 200px" @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="桶名称" prop="bucketName">
<el-input v-model="queryParams.bucketName" placeholder="请输入桶名称" clearable style="width: 200px" @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="是否默认" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable style="width: 200px">
<el-option key="0" label="是" value="0" />
<el-option key="1" label="否" value="1" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
<el-card shadow="hover">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['system:oss:add']">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['system:oss:edit']">修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['system:oss:remove']">
删除
</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<el-table v-loading="loading" :data="ossConfigList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="主建" align="center" prop="ossConfigId" v-if="columns[0].visible" />
<el-table-column label="配置key" align="center" prop="configKey" v-if="columns[1].visible" />
<el-table-column label="访问站点" align="center" prop="endpoint" v-if="columns[2].visible" width="200" />
<el-table-column label="自定义域名" align="center" prop="domain" v-if="columns[3].visible" width="200" />
<el-table-column label="桶名称" align="center" prop="bucketName" v-if="columns[4].visible" />
<el-table-column label="前缀" align="center" prop="prefix" v-if="columns[5].visible" />
<el-table-column label="域" align="center" prop="region" v-if="columns[6].visible" />
<el-table-column label="桶权限类型" align="center" prop="accessPolicy" v-if="columns[7].visible">
<template #default="scope">
<el-tag type="warning" v-if="scope.row.accessPolicy === '0'">private</el-tag>
<el-tag type="success" v-if="scope.row.accessPolicy === '1'">public</el-tag>
<el-tag type="info" v-if="scope.row.accessPolicy === '2'">custom</el-tag>
</template>
</el-table-column>
<el-table-column label="是否默认" align="center" prop="status" v-if="columns[8].visible">
<template #default="scope">
<el-switch v-model="scope.row.status" active-value="0" inactive-value="1" @change="handleStatusChange(scope.row)"></el-switch>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" align="center" width="150" class-name="small-padding">
<template #default="scope">
<el-tooltip content="修改" placement="top">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:oss:edit']"></el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:oss:remove']"></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card>
<!-- 添加或修改对象存储配置对话框 -->
<el-dialog :title="dialog.title" v-model="dialog.visible" width="800px" append-to-body>
<el-form ref="ossConfigFormRef" :model="form" :rules="rules" label-width="120px">
<el-form-item label="配置key" prop="configKey">
<el-input v-model="form.configKey" placeholder="请输入配置key" />
</el-form-item>
<el-form-item label="访问站点" prop="endpoint">
<el-input v-model="form.endpoint" placeholder="请输入访问站点" />
</el-form-item>
<el-form-item label="自定义域名" prop="domain">
<el-input v-model="form.domain" placeholder="请输入自定义域名" />
</el-form-item>
<el-form-item label="accessKey" prop="accessKey">
<el-input v-model="form.accessKey" placeholder="请输入accessKey" />
</el-form-item>
<el-form-item label="secretKey" prop="secretKey">
<el-input v-model="form.secretKey" placeholder="请输入秘钥" show-password />
</el-form-item>
<el-form-item label="桶名称" prop="bucketName">
<el-input v-model="form.bucketName" placeholder="请输入桶名称" />
</el-form-item>
<el-form-item label="前缀" prop="prefix">
<el-input v-model="form.prefix" placeholder="请输入前缀" />
</el-form-item>
<el-form-item label="是否HTTPS">
<el-radio-group v-model="form.isHttps">
<el-radio v-for="dict in sys_yes_no" :key="dict.value" :label="dict.value">{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="桶权限类型">
<el-radio-group v-model="form.accessPolicy">
<el-radio label="0">private</el-radio>
<el-radio label="1">public</el-radio>
<el-radio label="2">custom</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="域" prop="region">
<el-input v-model="form.region" placeholder="请输入域" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="OssConfig">
import {
listOssConfig,
getOssConfig,
delOssConfig,
addOssConfig,
updateOssConfig,
changeOssConfigStatus
} from "@/api/system/ossConfig";
const { proxy } = getCurrentInstance()
const { sys_yes_no } = toRefs(proxy?.useDict("sys_yes_no"));
const ossConfigList = ref([]);
const buttonLoading = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const dialog = reactive({
visible: false,
title: ''
});
//
const columns = ref([
{ key: 0, label: `主建`, visible: true },
{ key: 1, label: `配置key`, visible: false },
{ key: 2, label: `访问站点`, visible: true },
{ key: 3, label: `自定义域名`, visible: true },
{ key: 4, label: `桶名称`, visible: true },
{ key: 5, label: `前缀`, visible: true },
{ key: 6, label: ``, visible: true },
{ key: 7, label: `桶权限类型`, visible: true },
{ key: 8, label: `状态`, visible: true }
]);
const initFormData = {
ossConfigId: undefined,
configKey: '',
accessKey: '',
secretKey: '',
bucketName: '',
prefix: '',
endpoint: '',
domain: '',
isHttps: "N",
accessPolicy: "1",
region: '',
status: "1",
remark: '',
}
const data = reactive({
form: { ...initFormData },
//
queryParams: {
pageNum: 1,
pageSize: 10,
configKey: '',
bucketName: '',
status: '',
},
rules: {
configKey: [{ required: true, message: "configKey不能为空", trigger: "blur" },],
accessKey: [
{ required: true, message: "accessKey不能为空", trigger: "blur" },
{
min: 2,
max: 200,
message: "accessKey长度必须介于 2 和 100 之间",
trigger: "blur",
},
],
secretKey: [
{ required: true, message: "secretKey不能为空", trigger: "blur" },
{
min: 2,
max: 100,
message: "secretKey长度必须介于 2 和 100 之间",
trigger: "blur",
},
],
bucketName: [
{ required: true, message: "bucketName不能为空", trigger: "blur" },
{
min: 2,
max: 100,
message: "bucketName长度必须介于 2 和 100 之间",
trigger: "blur",
},
],
endpoint: [
{ required: true, message: "endpoint不能为空", trigger: "blur" },
{
min: 2,
max: 100,
message: "endpoint名称长度必须介于 2 和 100 之间",
trigger: "blur",
},
],
accessPolicy: [{ required: true, message: "accessPolicy不能为空", trigger: "blur" }]
}
});
const { queryParams, form, rules } = toRefs(data);
/** 查询对象存储配置列表 */
const getList = async () => {
loading.value = true;
const res = await listOssConfig(queryParams.value);
ossConfigList.value = res.rows;
total.value = res.total;
loading.value = false;
}
/** 取消按钮 */
const cancel = () => {
dialog.visible = false;
reset();
}
/** 表单重置 */
const reset = () => {
form.value = { ...initFormData };
proxy.resetForm("ossConfigFormRef");
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
proxy.resetForm("queryFormRef");
handleQuery();
}
/** 选择条数 */
const handleSelectionChange = (selection) => {
ids.value = selection.map(item => item.ossConfigId);
single.value = selection.length != 1;
multiple.value = !selection.length;
}
/** 新增按钮操作 */
const handleAdd = () => {
reset();
dialog.visible = true;
dialog.title = "添加对象存储配置";
}
/** 修改按钮操作 */
const handleUpdate = async (row) => {
reset();
const ossConfigId = row?.ossConfigId || ids.value[0];
const res = await getOssConfig(ossConfigId);
Object.assign(form.value, res.data);
dialog.visible = true;
dialog.title = "修改对象存储配置";
}
/** 提交按钮 */
const submitForm = () => {
proxy.$refs["ossConfigFormRef"].validate(async (valid) => {
if (valid) {
buttonLoading.value = true;
if (form.value.ossConfigId) {
await updateOssConfig(form.value).finally(() => buttonLoading.value = false);
} else {
await addOssConfig(form.value).finally(() => buttonLoading.value = false);
}
proxy?.$modal.msgSuccess("新增成功");
dialog.visible = false;
await getList();
}
});
}
/** 状态修改 */
const handleStatusChange = async (row) => {
let text = row.status === "0" ? "启用" : "停用";
try {
await proxy?.$modal.confirm('确认要"' + text + '""' + row.configKey + '"配置吗?');
await changeOssConfigStatus(row.ossConfigId, row.status, row.configKey);
await getList()
proxy?.$modal.msgSuccess(text + "成功");
} catch { return } finally {
row.status = row.status === "0" ? "1" : "0";
}
}
/** 删除按钮操作 */
const handleDelete = async (row) => {
const ossConfigIds = row?.ossConfigId || ids.value;
await proxy?.$modal.confirm('是否确认删除OSS配置编号为"' + ossConfigIds + '"的数据项?');
loading.value = true;
await delOssConfig(ossConfigIds).finally(() => loading.value = false);
await getList();
proxy?.$modal.msgSuccess("删除成功");
}
onMounted(() => {
getList();
})
</script>

View File

@ -0,0 +1,330 @@
<template>
<div class="p-2">
<div class="mb-[10px]" v-show="showSearch">
<el-card shadow="hover">
<el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
<el-form-item label="文件名" prop="fileName">
<el-input v-model="queryParams.fileName" placeholder="请输入文件名" clearable style="width: 200px" @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="原名" prop="originalName">
<el-input v-model="queryParams.originalName" placeholder="请输入原名" clearable style="width: 200px" @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="文件后缀" prop="fileSuffix">
<el-input v-model="queryParams.fileSuffix" placeholder="请输入文件后缀" clearable style="width: 200px" @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="创建时间">
<el-date-picker
v-model="dateRangeCreateTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
:default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
></el-date-picker>
</el-form-item>
<el-form-item label="服务商" prop="service">
<el-input v-model="queryParams.service" placeholder="请输入服务商" clearable style="width: 200px" @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
<el-card shadow="hover">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Upload" @click="handleFile" v-hasPermi="['system:oss:upload']">上传文件</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="primary" plain icon="Upload" @click="handleImage" v-hasPermi="['system:oss:upload']">上传图片</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['system:oss:remove']">
删除
</el-button>
</el-col>
<el-col :span="1.5">
<el-button
:type="previewListResource ? 'danger' : 'warning'"
plain
@click="handlePreviewListResource(!previewListResource)"
v-hasPermi="['system:oss:edit']"
>预览开关 :
{{
previewListResource ? "禁用" : "启用" }}</el-button
>
</el-col>
<el-col :span="1.5">
<el-button type="info" plain icon="Operation" @click="handleOssConfig" v-hasPermi="['system:oss:list']">配置管理</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<el-table
v-loading="loading"
:data="ossList"
@selection-change="handleSelectionChange"
:header-cell-class-name="handleHeaderClass"
@header-click="handleHeaderCLick"
v-if="showTable"
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="对象存储主键" align="center" prop="ossId" v-if="false" />
<el-table-column label="文件名" align="center" prop="fileName" />
<el-table-column label="原名" align="center" prop="originalName" />
<el-table-column label="文件后缀" align="center" prop="fileSuffix" />
<el-table-column label="文件展示" align="center" prop="url">
<template #default="scope">
<ImagePreview
v-if="previewListResource && checkFileSuffix(scope.row.fileSuffix)"
:width="100"
:height="100"
:src="scope.row.url"
:preview-src-list="[scope.row.url]"
/>
<span v-text="scope.row.url" v-if="!checkFileSuffix(scope.row.fileSuffix) || !previewListResource" />
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="180" sortable="custom">
<template #default="scope">
<span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="上传人" align="center" prop="createByName" />
<el-table-column label="服务商" align="center" prop="service" sortable="custom" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template #default="scope">
<el-tooltip content="下载" placement="top">
<el-button link type="primary" icon="Download" @click="handleDownload(scope.row)" v-hasPermi="['system:oss:download']"></el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:oss:remove']"></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</el-card>
<!-- 添加或修改OSS对象存储对话框 -->
<el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
<el-form ref="ossFormRef" :model="form" :rules="rules" label-width="80px">
<el-form-item label="文件名">
<fileUpload v-model="form.file" v-if="type === 0" />
<imageUpload v-model="form.file" v-if="type === 1" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button :loading="buttonLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="Oss">
import { listOss, delOss } from "@/api/system/oss";
import ImagePreview from "@/components/ImagePreview/index.vue";
import { getConfigKey,updateConfigByKey } from "@/api/system/config";
const router = useRouter();
const { proxy } = getCurrentInstance();
const ossList = ref([]);
const showTable = ref(true);
const buttonLoading = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const ids = ref([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const type = ref(0);
const previewListResource = ref(true);
const dateRangeCreateTime = ref(['', '']);
const dialog = reactive({
visible: false,
title: ''
});
//
const defaultSort = ref({ prop: 'createTime', order: 'ascending' });
const initFormData = {
file: undefined,
}
const data = reactive({
form: { ...initFormData },
//
queryParams: {
pageNum: 1,
pageSize: 10,
fileName: '',
originalName: '',
fileSuffix: '',
createTime: '',
service: '',
orderByColumn: defaultSort.value.prop,
isAsc: defaultSort.value.order
},
rules: {
file: [
{ required: true, message: "文件不能为空", trigger: "blur" }
]
}
});
const { queryParams, form, rules } = toRefs(data);
/** 查询OSS对象存储列表 */
const getList = async () => {
loading.value = true;
const res = await getConfigKey("sys.oss.previewListResource");
previewListResource.value = res?.data === undefined ? true : res.data === 'true';
const response = await listOss(proxy?.addDateRange(queryParams.value, dateRangeCreateTime.value, "CreateTime"));
ossList.value = response.rows;
total.value = response.total;
loading.value = false;
showTable.value = true;
}
function checkFileSuffix(fileSuffix) {
let arr = ["png", "jpg", "jpeg"];
return arr.some(type => {
return fileSuffix.indexOf(type) > -1;
});
}
/** 取消按钮 */
function cancel() {
dialog.visible = false;
reset();
}
/** 表单重置 */
function reset() {
form.value = { ...initFormData };
proxy.resetForm("ossFormRef");
}
/** 搜索按钮操作 */
function handleQuery() {
queryParams.value.pageNum = 1;
getList();
}
/** 重置按钮操作 */
function resetQuery() {
showTable.value = false;
dateRangeCreateTime.value = ['', ''];
proxy.resetForm("queryFormRef");
queryParams.value.orderByColumn = defaultSort.value.prop;
queryParams.value.isAsc = defaultSort.value.order;
handleQuery();
}
/** 选择条数 */
function handleSelectionChange(selection) {
ids.value = selection.map(item => item.ossId);
single.value = selection.length != 1;
multiple.value = !selection.length;
}
/** 设置列的排序为我们自定义的排序 */
const handleHeaderClass = ({ column }) => {
column.order = column.multiOrder
}
/** 点击表头进行排序 */
const handleHeaderCLick = (column) => {
if (column.sortable !== 'custom') {
return
}
switch (column.multiOrder) {
case 'descending':
column.multiOrder = 'ascending';
break;
case 'ascending':
column.multiOrder = '';
break;
default:
column.multiOrder = 'descending';
break;
}
handleOrderChange(column.property, column.multiOrder)
}
const handleOrderChange = (prop, order) => {
let orderByArr = queryParams.value.orderByColumn ? queryParams.value.orderByColumn.split(",") : [];
let isAscArr = queryParams.value.isAsc ? queryParams.value.isAsc.split(",") : [];
let propIndex = orderByArr.indexOf(prop)
if (propIndex !== -1) {
if (order) {
//
isAscArr[propIndex] = order;
} else {
//ordernull
isAscArr.splice(propIndex, 1);//
orderByArr.splice(propIndex, 1);//
}
} else {
//
orderByArr.push(prop);
isAscArr.push(order);
}
//
queryParams.value.orderByColumn = orderByArr.join(",");
queryParams.value.isAsc = isAscArr.join(",");
getList();
}
/** 任务日志列表查询 */
const handleOssConfig = () => {
router.push('/system/oss-config/index')
}
/** 文件按钮操作 */
const handleFile = () => {
reset();
type.value = 0;
dialog.visible = true;
dialog.title = "上传文件";
}
/** 图片按钮操作 */
const handleImage = () => {
reset();
type.value = 1;
dialog.visible = true;
dialog.title = "上传图片";
}
/** 提交按钮 */
const submitForm = () => {
dialog.visible = false;
getList();
}
/** 下载按钮操作 */
const handleDownload = (row) => {
proxy?.$download.oss(row.ossId)
}
/** 用户状态修改 */
const handlePreviewListResource = async (preview) => {
let text = preview ? "启用" : "停用";
try {
await proxy?.$modal.confirm('确认要"' + text + '""预览列表图片"配置吗?');
await updateConfigByKey("sys.oss.previewListResource", preview);
await getList()
proxy?.$modal.msgSuccess(text + "成功");
} catch { return }
}
/** 删除按钮操作 */
const handleDelete = async (row) => {
const ossIds = row?.ossId || ids.value;
await proxy?.$modal.confirm('是否确认删除OSS对象存储编号为"' + ossIds + '"的数据项?');
loading.value = true;
await delOss(ossIds).finally(() => loading.value = false);
await getList();
proxy?.$modal.msgSuccess("删除成功");
}
onMounted(() => {
getList();
})
</script>

View File

@ -1,6 +1,6 @@
<template>
<div class="app-container">
<el-row :gutter="20">
<el-row :gutter="10">
<el-col :span="6" :xs="24">
<el-card class="box-card">
<template v-slot:header>
@ -10,7 +10,7 @@
</template>
<div>
<div class="text-center">
<userAvatar :user="state.user" />
<userAvatar />
</div>
<ul class="list-group list-group-striped">
<li class="list-group-item">

View File

@ -1,5 +1,6 @@
<template>
<div class="user-info-head" @click="editCropper()"><img :src="options.img" title="点击上传头像" class="img-circle img-lg" /></div>
<div class="user-info-head" @click="editCropper()">
<img :src="options.img" title="点击上传头像" class="img-circle img-lg" />
<el-dialog :title="title" v-model="open" width="800px" append-to-body @opened="modalOpened" @close="closeDialog">
<el-row>
<el-col :xs="24" :md="12" :style="{ height: '350px' }">
@ -11,6 +12,7 @@
:autoCropWidth="options.autoCropWidth"
:autoCropHeight="options.autoCropHeight"
:fixedBox="options.fixedBox"
:outputType="options.outputType"
@realTime="realTime"
v-if="visible"
/>
@ -24,7 +26,13 @@
<br />
<el-row>
<el-col :lg="2" :md="2">
<el-upload action="#" :http-request="requestUpload" :show-file-list="false" :before-upload="beforeUpload">
<el-upload
action="#"
:headers="{'Content-Type':'multipart/form-data'}"
:http-request="requestUpload"
:show-file-list="false"
:before-upload="beforeUpload"
>
<el-button>
选择
<el-icon class="el-icon--right"><Upload /></el-icon>
@ -48,6 +56,7 @@
</el-col>
</el-row>
</el-dialog>
</div>
</template>
<script setup>
@ -71,33 +80,36 @@ const options = reactive({
autoCropHeight: 200, //
fixedBox: true, //
outputType: "png", // PNG
previews: {} //
fileName: "",
previews: {}, //
visible: false
});
/** 编辑头像 */
function editCropper() {
open.value = true;
};
}
/** 打开弹出层结束时的回调 */
function modalOpened() {
visible.value = true;
};
}
/** 覆盖默认上传行为 */
function requestUpload() {
};
function requestUpload() {}
/** 向左旋转 */
function rotateLeft() {
proxy.$refs.cropper.rotateLeft();
};
}
/** 向右旋转 */
function rotateRight() {
proxy.$refs.cropper.rotateRight();
};
}
/** 图片缩放 */
function changeScale(num) {
num = num || 1;
proxy.$refs.cropper.changeScale(num);
};
}
/** 上传预处理 */
function beforeUpload(file) {
if (file.type.indexOf("image/") == -1) {
@ -107,32 +119,33 @@ function beforeUpload(file) {
reader.readAsDataURL(file);
reader.onload = () => {
options.img = reader.result;
options.fileName = file.name;
};
}
};
}
/** 上传图片 */
function uploadImg() {
proxy.$refs.cropper.getCropBlob(data => {
let formData = new FormData();
formData.append("avatarfile", data);
formData.append("avatarfile", data, options.fileName);
uploadAvatar(formData).then(response => {
open.value = false;
options.img = import.meta.env.VITE_APP_BASE_API + response.imgUrl;
options.img = response.data.imgUrl;
userStore.avatar = options.img;
proxy.$modal.msgSuccess("修改成功");
visible.value = false;
});
});
};
}
/** 实时预览 */
function realTime(data) {
options.previews = data;
};
}
/** 关闭窗口 */
function closeDialog() {
options.img = userStore.avatar;
options.visible = false;
};
}
</script>
<style lang='scss' scoped>

View File

@ -0,0 +1,18 @@
package com.ruoyi.common.core.service;
/**
* 通用 OSS服务
*
* @author Lion Li
*/
public interface OssService {
/**
* 通过ossId查询对应的url
*
* @param ossIds ossId串逗号分隔
* @return url串逗号分隔
*/
String selectUrlByIds(String ossIds);
}

View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-common</artifactId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>ruoyi-common-oss</artifactId>
<description>
ruoyi-common-oss oss对象存储服务
</description>
<dependencies>
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-common-json</artifactId>
</dependency>
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-common-redis</artifactId>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-s3</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,38 @@
package com.ruoyi.common.oss.constant;
import java.util.Arrays;
import java.util.List;
/**
* 对象存储常量
*
* @author Lion Li
*/
public interface OssConstant {
/**
* 默认配置KEY
*/
String DEFAULT_CONFIG_KEY = "sys_oss:default_config";
/**
* 预览列表资源开关Key
*/
String PEREVIEW_LIST_RESOURCE_KEY = "sys.oss.previewListResource";
/**
* 系统数据ids
*/
List<Long> SYSTEM_DATA_IDS = Arrays.asList(1L, 2L, 3L, 4L);
/**
* 云服务商
*/
String[] CLOUD_SERVICE = new String[] {"aliyun", "qcloud", "qiniu", "obs"};
/**
* https 状态
*/
String IS_HTTPS = "Y";
}

View File

@ -0,0 +1,262 @@
package com.ruoyi.common.oss.core;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.IdUtil;
import com.amazonaws.ClientConfiguration;
import com.amazonaws.HttpMethod;
import com.amazonaws.Protocol;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSCredentialsProvider;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.client.builder.AwsClientBuilder;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import com.amazonaws.services.s3.model.*;
import com.ruoyi.common.core.utils.DateUtils;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.oss.constant.OssConstant;
import com.ruoyi.common.oss.entity.UploadResult;
import com.ruoyi.common.oss.enumd.AccessPolicyType;
import com.ruoyi.common.oss.enumd.PolicyType;
import com.ruoyi.common.oss.exception.OssException;
import com.ruoyi.common.oss.properties.OssProperties;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.InputStream;
import java.net.URL;
import java.util.Date;
/**
* S3 存储协议 所有兼容S3协议的云厂商均支持
* 阿里云 腾讯云 七牛云 minio
*
* @author Lion Li
*/
public class OssClient {
private final String configKey;
private final OssProperties properties;
private final AmazonS3 client;
public OssClient(String configKey, OssProperties ossProperties) {
this.configKey = configKey;
this.properties = ossProperties;
try {
AwsClientBuilder.EndpointConfiguration endpointConfig =
new AwsClientBuilder.EndpointConfiguration(properties.getEndpoint(), properties.getRegion());
AWSCredentials credentials = new BasicAWSCredentials(properties.getAccessKey(), properties.getSecretKey());
AWSCredentialsProvider credentialsProvider = new AWSStaticCredentialsProvider(credentials);
ClientConfiguration clientConfig = new ClientConfiguration();
if (OssConstant.IS_HTTPS.equals(properties.getIsHttps())) {
clientConfig.setProtocol(Protocol.HTTPS);
} else {
clientConfig.setProtocol(Protocol.HTTP);
}
AmazonS3ClientBuilder build = AmazonS3Client.builder()
.withEndpointConfiguration(endpointConfig)
.withClientConfiguration(clientConfig)
.withCredentials(credentialsProvider)
.disableChunkedEncoding();
if (!StringUtils.containsAny(properties.getEndpoint(), OssConstant.CLOUD_SERVICE)) {
// minio 使用https限制使用域名访问 需要此配置 站点填域名
build.enablePathStyleAccess();
}
this.client = build.build();
createBucket();
} catch (Exception e) {
if (e instanceof OssException) {
throw e;
}
throw new OssException("配置错误! 请检查系统配置:[" + e.getMessage() + "]");
}
}
public void createBucket() {
try {
String bucketName = properties.getBucketName();
if (client.doesBucketExistV2(bucketName)) {
return;
}
CreateBucketRequest createBucketRequest = new CreateBucketRequest(bucketName);
AccessPolicyType accessPolicy = getAccessPolicy();
createBucketRequest.setCannedAcl(accessPolicy.getAcl());
client.createBucket(createBucketRequest);
client.setBucketPolicy(bucketName, getPolicy(bucketName, accessPolicy.getPolicyType()));
} catch (Exception e) {
throw new OssException("创建Bucket失败, 请核对配置信息:[" + e.getMessage() + "]");
}
}
public UploadResult upload(byte[] data, String path, String contentType) {
return upload(new ByteArrayInputStream(data), path, contentType);
}
public UploadResult upload(InputStream inputStream, String path, String contentType) {
if (!(inputStream instanceof ByteArrayInputStream)) {
inputStream = new ByteArrayInputStream(IoUtil.readBytes(inputStream));
}
try {
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentType(contentType);
metadata.setContentLength(inputStream.available());
PutObjectRequest putObjectRequest = new PutObjectRequest(properties.getBucketName(), path, inputStream, metadata);
// 设置上传对象的 Acl 为公共读
putObjectRequest.setCannedAcl(getAccessPolicy().getAcl());
client.putObject(putObjectRequest);
} catch (Exception e) {
throw new OssException("上传文件失败,请检查配置信息:[" + e.getMessage() + "]");
}
return UploadResult.builder().url(getUrl() + "/" + path).filename(path).build();
}
public UploadResult upload(File file, String path) {
try {
PutObjectRequest putObjectRequest = new PutObjectRequest(properties.getBucketName(), path, file);
// 设置上传对象的 Acl 为公共读
putObjectRequest.setCannedAcl(getAccessPolicy().getAcl());
client.putObject(putObjectRequest);
} catch (Exception e) {
throw new OssException("上传文件失败,请检查配置信息:[" + e.getMessage() + "]");
}
return UploadResult.builder().url(getUrl() + "/" + path).filename(path).build();
}
public void delete(String path) {
path = path.replace(getUrl() + "/", "");
try {
client.deleteObject(properties.getBucketName(), path);
} catch (Exception e) {
throw new OssException("删除文件失败,请检查配置信息:[" + e.getMessage() + "]");
}
}
public UploadResult uploadSuffix(byte[] data, String suffix, String contentType) {
return upload(data, getPath(properties.getPrefix(), suffix), contentType);
}
public UploadResult uploadSuffix(InputStream inputStream, String suffix, String contentType) {
return upload(inputStream, getPath(properties.getPrefix(), suffix), contentType);
}
public UploadResult uploadSuffix(File file, String suffix) {
return upload(file, getPath(properties.getPrefix(), suffix));
}
/**
* 获取文件元数据
*
* @param path 完整文件路径
*/
public ObjectMetadata getObjectMetadata(String path) {
path = path.replace(getUrl() + "/", "");
S3Object object = client.getObject(properties.getBucketName(), path);
return object.getObjectMetadata();
}
public InputStream getObjectContent(String path) {
path = path.replace(getUrl() + "/", "");
S3Object object = client.getObject(properties.getBucketName(), path);
return object.getObjectContent();
}
public String getUrl() {
String domain = properties.getDomain();
String endpoint = properties.getEndpoint();
String header = OssConstant.IS_HTTPS.equals(properties.getIsHttps()) ? "https://" : "http://";
// 云服务商直接返回
if (StringUtils.containsAny(endpoint, OssConstant.CLOUD_SERVICE)) {
if (StringUtils.isNotBlank(domain)) {
return header + domain;
}
return header + properties.getBucketName() + "." + endpoint;
}
// minio 单独处理
if (StringUtils.isNotBlank(domain)) {
return header + domain + "/" + properties.getBucketName();
}
return header + endpoint + "/" + properties.getBucketName();
}
public String getPath(String prefix, String suffix) {
// 生成uuid
String uuid = IdUtil.fastSimpleUUID();
// 文件路径
String path = DateUtils.datePath() + "/" + uuid;
if (StringUtils.isNotBlank(prefix)) {
path = prefix + "/" + path;
}
return path + suffix;
}
public String getConfigKey() {
return configKey;
}
/**
* 获取私有URL链接
*
* @param objectKey 对象KEY
* @param second 授权时间
*/
public String getPrivateUrl(String objectKey, Integer second) {
GeneratePresignedUrlRequest generatePresignedUrlRequest =
new GeneratePresignedUrlRequest(properties.getBucketName(), objectKey)
.withMethod(HttpMethod.GET)
.withExpiration(new Date(System.currentTimeMillis() + 1000L * second));
URL url = client.generatePresignedUrl(generatePresignedUrlRequest);
return url.toString();
}
/**
* 检查配置是否相同
*/
public boolean checkPropertiesSame(OssProperties properties) {
return this.properties.equals(properties);
}
/**
* 获取当前桶权限类型
*
* @return 当前桶权限类型code
*/
public AccessPolicyType getAccessPolicy() {
return AccessPolicyType.getByType(properties.getAccessPolicy());
}
private static String getPolicy(String bucketName, PolicyType policyType) {
StringBuilder builder = new StringBuilder();
builder.append("{\n\"Statement\": [\n{\n\"Action\": [\n");
builder.append(switch (policyType) {
case WRITE -> "\"s3:GetBucketLocation\",\n\"s3:ListBucketMultipartUploads\"\n";
case READ_WRITE -> "\"s3:GetBucketLocation\",\n\"s3:ListBucket\",\n\"s3:ListBucketMultipartUploads\"\n";
default -> "\"s3:GetBucketLocation\"\n";
});
builder.append("],\n\"Effect\": \"Allow\",\n\"Principal\": \"*\",\n\"Resource\": \"arn:aws:s3:::");
builder.append(bucketName);
builder.append("\"\n},\n");
if (policyType == PolicyType.READ) {
builder.append("{\n\"Action\": [\n\"s3:ListBucket\"\n],\n\"Effect\": \"Deny\",\n\"Principal\": \"*\",\n\"Resource\": \"arn:aws:s3:::");
builder.append(bucketName);
builder.append("\"\n},\n");
}
builder.append("{\n\"Action\": ");
builder.append(switch (policyType) {
case WRITE -> "[\n\"s3:AbortMultipartUpload\",\n\"s3:DeleteObject\",\n\"s3:ListMultipartUploadParts\",\n\"s3:PutObject\"\n],\n";
case READ_WRITE -> "[\n\"s3:AbortMultipartUpload\",\n\"s3:DeleteObject\",\n\"s3:GetObject\",\n\"s3:ListMultipartUploadParts\",\n\"s3:PutObject\"\n],\n";
default -> "\"s3:GetObject\",\n";
});
builder.append("\"Effect\": \"Allow\",\n\"Principal\": \"*\",\n\"Resource\": \"arn:aws:s3:::");
builder.append(bucketName);
builder.append("/*\"\n}\n],\n\"Version\": \"2012-10-17\"\n}\n");
return builder.toString();
}
}

View File

@ -0,0 +1,24 @@
package com.ruoyi.common.oss.entity;
import lombok.Builder;
import lombok.Data;
/**
* 上传返回体
*
* @author Lion Li
*/
@Data
@Builder
public class UploadResult {
/**
* 文件路径
*/
private String url;
/**
* 文件名
*/
private String filename;
}

View File

@ -0,0 +1,55 @@
package com.ruoyi.common.oss.enumd;
import com.amazonaws.services.s3.model.CannedAccessControlList;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 桶访问策略配置
*
* @author 陈賝
*/
@Getter
@AllArgsConstructor
public enum AccessPolicyType {
/**
* private
*/
PRIVATE("0", CannedAccessControlList.Private, PolicyType.WRITE),
/**
* public
*/
PUBLIC("1", CannedAccessControlList.PublicRead, PolicyType.READ),
/**
* custom
*/
CUSTOM("2",CannedAccessControlList.PublicRead, PolicyType.READ);
/**
* 权限类型
*/
private final String type;
/**
* 文件对象 权限类型
*/
private final CannedAccessControlList acl;
/**
* 桶策略类型
*/
private final PolicyType policyType;
public static AccessPolicyType getByType(String type) {
for (AccessPolicyType value : values()) {
if (value.getType().equals(type)) {
return value;
}
}
throw new RuntimeException("'type' not found By " + type);
}
}

View File

@ -0,0 +1,35 @@
package com.ruoyi.common.oss.enumd;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* minio策略配置
*
* @author Lion Li
*/
@Getter
@AllArgsConstructor
public enum PolicyType {
/**
* 只读
*/
READ("read-only"),
/**
* 只写
*/
WRITE("write-only"),
/**
* 读写
*/
READ_WRITE("read-write");
/**
* 类型
*/
private final String type;
}

View File

@ -0,0 +1,19 @@
package com.ruoyi.common.oss.exception;
import java.io.Serial;
/**
* OSS异常类
*
* @author Lion Li
*/
public class OssException extends RuntimeException {
@Serial
private static final long serialVersionUID = 1L;
public OssException(String msg) {
super(msg);
}
}

View File

@ -0,0 +1,65 @@
package com.ruoyi.common.oss.factory;
import com.ruoyi.common.core.constant.CacheNames;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.json.utils.JsonUtils;
import com.ruoyi.common.oss.constant.OssConstant;
import com.ruoyi.common.oss.core.OssClient;
import com.ruoyi.common.oss.exception.OssException;
import com.ruoyi.common.oss.properties.OssProperties;
import com.ruoyi.common.redis.utils.CacheUtils;
import com.ruoyi.common.redis.utils.RedisUtils;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 文件上传Factory
*
* @author Lion Li
*/
@Slf4j
public class OssFactory {
private static final Map<String, OssClient> CLIENT_CACHE = new ConcurrentHashMap<>();
/**
* 获取默认实例
*/
public static OssClient instance() {
// 获取redis 默认类型
String configKey = RedisUtils.getCacheObject(OssConstant.DEFAULT_CONFIG_KEY);
if (StringUtils.isEmpty(configKey)) {
throw new OssException("文件存储服务类型无法找到!");
}
return instance(configKey);
}
/**
* 根据类型获取实例
*/
public static OssClient instance(String configKey) {
String json = CacheUtils.get(CacheNames.SYS_OSS_CONFIG, configKey);
if (json == null) {
throw new OssException("系统异常, '" + configKey + "'配置信息不存在!");
}
OssProperties properties = JsonUtils.parseObject(json, OssProperties.class);
// 使用租户标识避免多个租户相同key实例覆盖
String key = properties.getTenantId() + ":" + configKey;
OssClient client = CLIENT_CACHE.get(key);
if (client == null) {
CLIENT_CACHE.put(key, new OssClient(configKey, properties));
log.info("创建OSS实例 key => {}", configKey);
return CLIENT_CACHE.get(key);
}
// 配置不相同则重新构建
if (!client.checkPropertiesSame(properties)) {
CLIENT_CACHE.put(key, new OssClient(configKey, properties));
log.info("重载OSS实例 key => {}", configKey);
return CLIENT_CACHE.get(key);
}
return client;
}
}

View File

@ -0,0 +1,63 @@
package com.ruoyi.common.oss.properties;
import lombok.Data;
/**
* OSS对象存储 配置属性
*
* @author Lion Li
*/
@Data
public class OssProperties {
/**
* 租户id
*/
private String tenantId;
/**
* 访问站点
*/
private String endpoint;
/**
* 自定义域名
*/
private String domain;
/**
* 前缀
*/
private String prefix;
/**
* ACCESS_KEY
*/
private String accessKey;
/**
* SECRET_KEY
*/
private String secretKey;
/**
* 存储空间名
*/
private String bucketName;
/**
* 存储区域
*/
private String region;
/**
* 是否httpsY=,N=
*/
private String isHttps;
/**
* 桶权限类型(0private 1public 2custom)
*/
private String accessPolicy;
}

View File

@ -0,0 +1,124 @@
package com.ruoyi.system.controller.system;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.ruoyi.common.core.core.domain.R;
import com.ruoyi.common.core.validate.AddGroup;
import com.ruoyi.common.core.validate.EditGroup;
import com.ruoyi.common.core.validate.QueryGroup;
import com.ruoyi.common.web.annotation.RepeatSubmit;
import com.ruoyi.common.web.core.BaseController;
import com.ruoyi.common.log.annotation.Log;
import com.ruoyi.common.log.enums.BusinessType;
import com.ruoyi.common.orm.core.page.PageQuery;
import com.ruoyi.common.orm.core.page.TableDataInfo;
import com.ruoyi.system.domain.bo.SysOssConfigBo;
import com.ruoyi.system.domain.vo.SysOssConfigVo;
import com.ruoyi.system.service.ISysOssConfigService;
import jakarta.annotation.Resource;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 对象存储配置
*
* @author Lion Li
* @author 孤舟烟雨
* @author 数据小王子
* @date 2023-11-30
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/resource/oss/config")
public class SysOssConfigController extends BaseController {
@Resource
private final ISysOssConfigService ossConfigService;
/**
* 查询对象存储配置列表
*/
@SaCheckPermission("system:oss:list")
@GetMapping("/list")
public TableDataInfo<SysOssConfigVo> list(SysOssConfigBo bo) {
return ossConfigService.queryPageList(bo);
}
/**
* 获取对象存储配置详细信息
*
* @param ossConfigId OSS配置ID
*/
@SaCheckPermission("system:oss:query")
@GetMapping("/{ossConfigId}")
public R<SysOssConfigVo> getInfo(@NotNull(message = "主键不能为空")
@PathVariable Long ossConfigId) {
return R.ok(ossConfigService.queryById(ossConfigId));
}
/**
* 新增对象存储配置
*/
@SaCheckPermission("system:oss:add")
@Log(title = "对象存储配置", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping()
public R<Void> add(@Validated @RequestBody SysOssConfigBo bo) {
boolean inserted = ossConfigService.insertByBo(bo);
if (!inserted) {
return R.fail("新增对象存储配置记录失败!");
}
return R.ok();
}
/**
* 修改对象存储配置
*/
@SaCheckPermission("system:oss:edit")
@Log(title = "对象存储配置", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping()
public R<Void> edit(@Validated @RequestBody SysOssConfigBo bo) {
Boolean updated = ossConfigService.updateByBo(bo);
if (!updated) {
R.fail("修改对象存储配置记录失败!");
}
return R.ok();
}
/**
* 删除对象存储配置
*
* @param ossConfigIds OSS配置ID串
*/
@SaCheckPermission("system:oss:remove")
@Log(title = "对象存储配置", businessType = BusinessType.DELETE)
@DeleteMapping("/{ossConfigIds}")
public R<Void> remove(@NotEmpty(message = "主键不能为空")
@PathVariable Long[] ossConfigIds) {
boolean deleted = ossConfigService.deleteWithValidByIds(List.of(ossConfigIds), true);
if (!deleted) {
R.fail("删除对象存储配置记录失败!");
}
return R.ok();
}
/**
* 状态修改
*/
@SaCheckPermission("system:oss:edit")
@Log(title = "对象存储状态修改", businessType = BusinessType.UPDATE)
@PutMapping("/changeStatus")
public R<Void> changeStatus(@RequestBody SysOssConfigBo bo) {
boolean updated = ossConfigService.updateOssConfigStatus(bo);
if (!updated) {
R.fail("状态修改失败!");
}
return R.ok();
}
}

View File

@ -0,0 +1,110 @@
package com.ruoyi.system.controller.system;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.util.ObjectUtil;
import com.ruoyi.common.core.core.domain.R;
import com.ruoyi.common.web.core.BaseController;
import com.ruoyi.common.log.annotation.Log;
import com.ruoyi.common.log.enums.BusinessType;
import com.ruoyi.common.orm.core.page.TableDataInfo;
import com.ruoyi.system.domain.bo.SysOssBo;
import com.ruoyi.system.domain.vo.SysOssUploadVo;
import com.ruoyi.system.domain.vo.SysOssVo;
import com.ruoyi.system.service.ISysOssService;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.NotEmpty;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
/**
* 文件上传 控制层
*
* @author Lion Li
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/resource/oss")
public class SysOssController extends BaseController {
private final ISysOssService ossService;
/**
* 查询OSS对象存储列表
*/
@SaCheckPermission("system:oss:list")
@GetMapping("/list")
public TableDataInfo<SysOssVo> list(@Validated SysOssBo bo) {
return ossService.queryPageList(bo);
}
/**
* 查询OSS对象基于id串
*
* @param ossIds OSS对象ID串
*/
@SaCheckPermission("system:oss:list")
@GetMapping("/listByIds/{ossIds}")
public R<List<SysOssVo>> listByIds(@NotEmpty(message = "主键不能为空")
@PathVariable Long[] ossIds) {
List<SysOssVo> list = ossService.listSysOssByIds(Arrays.asList(ossIds));
return R.ok(list);
}
/**
* 上传OSS对象存储
*
* @param file 文件
*/
@SaCheckPermission("system:oss:upload")
@Log(title = "OSS对象存储", businessType = BusinessType.INSERT)
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public R<SysOssUploadVo> upload(@RequestPart("file") MultipartFile file) {
if (ObjectUtil.isNull(file)) {
return R.fail("上传文件不能为空");
}
SysOssVo oss = ossService.upload(file);
SysOssUploadVo uploadVo = new SysOssUploadVo();
uploadVo.setUrl(oss.getUrl());
uploadVo.setFileName(oss.getOriginalName());
uploadVo.setOssId(oss.getOssId().toString());
return R.ok(uploadVo);
}
/**
* 下载OSS对象
*
* @param ossId OSS对象ID
*/
@SaCheckPermission("system:oss:download")
@GetMapping("/download/{ossId}")
public void download(@PathVariable Long ossId, HttpServletResponse response) throws IOException {
ossService.download(ossId, response);
}
/**
* 删除OSS对象存储
*
* @param ossIds OSS对象ID串
*/
@SaCheckPermission("system:oss:remove")
@Log(title = "OSS对象存储", businessType = BusinessType.DELETE)
@DeleteMapping("/{ossIds}")
public R<Void> remove(@NotEmpty(message = "主键不能为空")
@PathVariable Long[] ossIds) {
Boolean deleted = ossService.deleteWithValidByIds(List.of(ossIds), true);
if (!deleted) {
R.fail("删除OSS对象存储记录失败!");
}
return R.ok();
}
}

View File

@ -11,7 +11,9 @@ import com.ruoyi.system.domain.bo.SysUserBo;
import com.ruoyi.system.domain.bo.SysUserProfileBo;
import com.ruoyi.system.domain.vo.AvatarVo;
import com.ruoyi.system.domain.vo.ProfileVo;
import com.ruoyi.system.domain.vo.SysOssVo;
import com.ruoyi.system.domain.vo.SysUserVo;
import com.ruoyi.system.service.ISysOssService;
import jakarta.annotation.Resource;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
@ -44,6 +46,8 @@ public class SysProfileController extends BaseController
{
@Resource
private ISysUserService userService;
@Resource
private ISysOssService ossService;
/**
* 个人信息
@ -126,13 +130,9 @@ public class SysProfileController extends BaseController
if (!StringUtils.equalsAnyIgnoreCase(extension, MimeTypeUtils.IMAGE_EXTENSION)) {
return R.fail("文件格式不正确,请上传" + Arrays.toString(MimeTypeUtils.IMAGE_EXTENSION) + "格式");
}
//TODO需要使用OSS来存储操作用户上传的头像
SysUserVo sysUser = userService.selectUserById(LoginHelper.getUserId());
String avatar = FileUploadUtils.upload(RuoYiConfig.getAvatarPath(), file, MimeTypeUtils.IMAGE_EXTENSION);
if (userService.updateUserAvatar(sysUser.getUserName(), avatar))
{
SysOssVo oss = ossService.upload(file);
String avatar = oss.getUrl();
if (userService.updateUserAvatar(LoginHelper.getUserId(), oss.getOssId())) {
AvatarVo avatarVo = new AvatarVo();
avatarVo.setImgUrl(avatar);
return R.ok(avatarVo);

View File

@ -0,0 +1,50 @@
package com.ruoyi.system.domain;
import com.mybatisflex.annotation.Id;
import com.mybatisflex.annotation.Table;
import com.ruoyi.common.orm.core.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* OSS对象存储服务对象
*
* @author Lion Li
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Table("sys_oss")
public class SysOss extends BaseEntity {
/**
* 对象存储主键
*/
@Id
private Long ossId;
/**
* 文件名
*/
private String fileName;
/**
* 原名
*/
private String originalName;
/**
* 文件后缀名
*/
private String fileSuffix;
/**
* URL地址
*/
private String url;
/**
* 服务商
*/
private String service;
}

View File

@ -0,0 +1,89 @@
package com.ruoyi.system.domain;
import com.mybatisflex.annotation.Id;
import com.mybatisflex.annotation.Table;
import com.ruoyi.common.orm.core.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 对象存储配置对象 sys_oss_config
*
* @author Lion Li
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Table("sys_oss_config")
public class SysOssConfig extends BaseEntity {
/**
* 主建
*/
@Id
private Long ossConfigId;
/**
* 配置key
*/
private String configKey;
/**
* accessKey
*/
private String accessKey;
/**
* 秘钥
*/
private String secretKey;
/**
* 桶名称
*/
private String bucketName;
/**
* 前缀
*/
private String prefix;
/**
* 访问站点
*/
private String endpoint;
/**
* 自定义域名
*/
private String domain;
/**
* 是否https0否 1是
*/
private String isHttps;
/**
*
*/
private String region;
/**
* 是否默认0=,1=
*/
private String status;
/**
* 扩展字段
*/
private String ext1;
/**
* 备注
*/
private String remark;
/**
* 桶权限类型(0private 1public 2custom)
*/
private String accessPolicy;
}

View File

@ -1,32 +1,23 @@
package com.ruoyi.system.domain;
import java.io.Serial;
import java.util.Date;
import java.util.List;
import com.mybatisflex.annotation.*;
import com.ruoyi.common.core.constant.UserConstants;
import com.ruoyi.common.orm.core.domain.BaseEntity;
import jakarta.validation.constraints.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.ruoyi.common.core.annotation.Excel;
import com.ruoyi.common.core.annotation.Excel.ColumnType;
import com.ruoyi.common.core.annotation.Excel.Type;
import com.ruoyi.common.core.annotation.Excels;
import com.ruoyi.common.core.xss.Xss;
/**
* 用户对象 sys_user
*
* @author ruoyi
*/
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
@Table(value = "sys_user")
public class SysUser extends BaseEntity
{
@ -66,7 +57,7 @@ public class SysUser extends BaseEntity
private String gender;
/** 用户头像 */
private String avatar;
private Long avatar;
/** 密码 */
private String password;
@ -100,26 +91,11 @@ public class SysUser extends BaseEntity
*/
private String remark;
public SysUser()
{
}
public SysUser(Long userId)
{
this.userId = userId;
}
public Long getUserId()
{
return userId;
}
public void setUserId(Long userId)
{
this.userId = userId;
}
public boolean isAdmin()
{
return isAdmin(this.userId);
@ -130,225 +106,6 @@ public class SysUser extends BaseEntity
return userId != null && 1L == userId;
}
public Long getDeptId()
{
return deptId;
}
public void setDeptId(Long deptId)
{
this.deptId = deptId;
}
@Xss(message = "用户昵称不能包含脚本字符")
@Size(min = 0, max = 30, message = "用户昵称长度不能超过30个字符")
public String getNickName()
{
return nickName;
}
public void setNickName(String nickName)
{
this.nickName = nickName;
}
@Xss(message = "用户账号不能包含脚本字符")
@NotBlank(message = "用户账号不能为空")
@Size(min = 0, max = 30, message = "用户账号长度不能超过30个字符")
public String getUserName()
{
return userName;
}
public void setUserName(String userName)
{
this.userName = userName;
}
public String getUserType() {
return userType;
}
public void setUserType(String userType) {
this.userType = userType;
}
@Email(message = "邮箱格式不正确")
@Size(min = 0, max = 50, message = "邮箱长度不能超过50个字符")
public String getEmail()
{
return email;
}
public void setEmail(String email)
{
this.email = email;
}
@Size(min = 0, max = 11, message = "手机号码长度不能超过11个字符")
public String getPhonenumber()
{
return phonenumber;
}
public void setPhonenumber(String phonenumber)
{
this.phonenumber = phonenumber;
}
public String getGender()
{
return gender;
}
public void setGender(String gender)
{
this.gender = gender;
}
public String getAvatar()
{
return avatar;
}
public void setAvatar(String avatar)
{
this.avatar = avatar;
}
@JsonIgnore
@JsonProperty
public String getPassword()
{
return password;
}
public void setPassword(String password)
{
this.password = password;
}
public String getStatus()
{
return status;
}
public void setStatus(String status)
{
this.status = status;
}
public String getDelFlag()
{
return delFlag;
}
public void setDelFlag(String delFlag)
{
this.delFlag = delFlag;
}
public String getLoginIp()
{
return loginIp;
}
public void setLoginIp(String loginIp)
{
this.loginIp = loginIp;
}
public Date getLoginDate()
{
return loginDate;
}
public void setLoginDate(Date loginDate)
{
this.loginDate = loginDate;
}
public SysDept getDept()
{
return dept;
}
public void setDept(SysDept dept)
{
this.dept = dept;
}
public List<SysRole> getRoles()
{
return roles;
}
public void setRoles(List<SysRole> roles)
{
this.roles = roles;
}
public Long[] getRoleIds()
{
return roleIds;
}
public void setRoleIds(Long[] roleIds)
{
this.roleIds = roleIds;
}
public Long[] getPostIds()
{
return postIds;
}
public void setPostIds(Long[] postIds)
{
this.postIds = postIds;
}
public String getRemark() {
return remark;
}
public void setRemark(String remark) {
this.remark = remark;
}
public Long getTenantId() {
return tenantId;
}
public void setTenantId(Long tenantId) {
this.tenantId = tenantId;
}
@Override
public String toString() {
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
.append("userId", getUserId())
.append("deptId", getDeptId())
.append("userName", getUserName())
.append("nickName", getNickName())
.append("userType", getUserType())
.append("email", getEmail())
.append("phonenumber", getPhonenumber())
.append("sex", getGender())
.append("avatar", getAvatar())
.append("password", getPassword())
.append("status", getStatus())
.append("delFlag", getDelFlag())
.append("loginIp", getLoginIp())
.append("loginDate", getLoginDate())
.append("createBy", getCreateBy())
.append("createTime", getCreateTime())
.append("updateBy", getUpdateBy())
.append("updateTime", getUpdateTime())
.append("remark", getRemark())
.append("dept", getDept())
.toString();
}
public boolean isSuperAdmin() {
return UserConstants.SUPER_ADMIN_ID.equals(this.userId);

View File

@ -0,0 +1,49 @@
package com.ruoyi.system.domain.bo;
import com.ruoyi.common.orm.core.domain.BaseEntity;
import com.ruoyi.system.domain.SysOss;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* OSS对象存储分页查询对象 sys_oss
*
* @author Lion Li
*/
@Data
@EqualsAndHashCode(callSuper = true)
@AutoMapper(target = SysOss.class, reverseConvertGenerate = false)
public class SysOssBo extends BaseEntity {
/**
* ossId
*/
private Long ossId;
/**
* 文件名
*/
private String fileName;
/**
* 原名
*/
private String originalName;
/**
* 文件后缀名
*/
private String fileSuffix;
/**
* URL地址
*/
private String url;
/**
* 服务商
*/
private String service;
}

View File

@ -0,0 +1,109 @@
package com.ruoyi.system.domain.bo;
import com.ruoyi.common.core.validate.AddGroup;
import com.ruoyi.common.core.validate.EditGroup;
import com.ruoyi.common.orm.core.domain.BaseEntity;
import com.ruoyi.system.domain.SysOssConfig;
import io.github.linpeilie.annotations.AutoMapper;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 对象存储配置业务对象 sys_oss_config
*
* @author Lion Li
* @author 孤舟烟雨
* @date 2021-08-13
*/
@Data
@EqualsAndHashCode(callSuper = true)
@AutoMapper(target = SysOssConfig.class, reverseConvertGenerate = false)
public class SysOssConfigBo extends BaseEntity {
/**
* 主建
*/
@NotNull(message = "主建不能为空", groups = {EditGroup.class})
private Long ossConfigId;
/**
* 配置key
*/
@NotBlank(message = "配置key不能为空", groups = {AddGroup.class, EditGroup.class})
@Size(min = 2, max = 100, message = "configKey长度必须介于{min}和{max} 之间")
private String configKey;
/**
* accessKey
*/
@NotBlank(message = "accessKey不能为空", groups = {AddGroup.class, EditGroup.class})
@Size(min = 2, max = 100, message = "accessKey长度必须介于{min}和{max} 之间")
private String accessKey;
/**
* 秘钥
*/
@NotBlank(message = "secretKey不能为空", groups = {AddGroup.class, EditGroup.class})
@Size(min = 2, max = 100, message = "secretKey长度必须介于{min}和{max} 之间")
private String secretKey;
/**
* 桶名称
*/
@NotBlank(message = "桶名称不能为空", groups = {AddGroup.class, EditGroup.class})
@Size(min = 2, max = 100, message = "bucketName长度必须介于{min}和{max}之间")
private String bucketName;
/**
* 前缀
*/
private String prefix;
/**
* 访问站点
*/
@NotBlank(message = "访问站点不能为空", groups = {AddGroup.class, EditGroup.class})
@Size(min = 2, max = 100, message = "endpoint长度必须介于{min}和{max}之间")
private String endpoint;
/**
* 自定义域名
*/
private String domain;
/**
* 是否httpsY=,N=
*/
private String isHttps;
/**
* 是否默认0=,1=
*/
private String status;
/**
*
*/
private String region;
/**
* 扩展字段
*/
private String ext1;
/**
* 备注
*/
private String remark;
/**
* 桶权限类型(0private 1public 2custom)
*/
@NotBlank(message = "桶权限类型不能为空", groups = {AddGroup.class, EditGroup.class})
private String accessPolicy;
}

View File

@ -0,0 +1,97 @@
package com.ruoyi.system.domain.vo;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.ruoyi.system.domain.SysOssConfig;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 对象存储配置视图对象 sys_oss_config
*
* @author Lion Li
* @author 孤舟烟雨
* @date 2021-08-13
*/
@Data
@ExcelIgnoreUnannotated
@AutoMapper(target = SysOssConfig.class)
public class SysOssConfigVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主建
*/
private Long ossConfigId;
/**
* 配置key
*/
private String configKey;
/**
* accessKey
*/
private String accessKey;
/**
* 秘钥
*/
private String secretKey;
/**
* 桶名称
*/
private String bucketName;
/**
* 前缀
*/
private String prefix;
/**
* 访问站点
*/
private String endpoint;
/**
* 自定义域名
*/
private String domain;
/**
* 是否httpsY=,N=
*/
private String isHttps;
/**
*
*/
private String region;
/**
* 是否默认0=,1=
*/
private String status;
/**
* 扩展字段
*/
private String ext1;
/**
* 备注
*/
private String remark;
/**
* 桶权限类型(0private 1public 2custom)
*/
private String accessPolicy;
}

View File

@ -0,0 +1,28 @@
package com.ruoyi.system.domain.vo;
import lombok.Data;
/**
* 上传对象信息
*
* @author Michelle.Chung
*/
@Data
public class SysOssUploadVo {
/**
* URL地址
*/
private String url;
/**
* 文件名
*/
private String fileName;
/**
* 对象存储主键
*/
private String ossId;
}

View File

@ -0,0 +1,69 @@
package com.ruoyi.system.domain.vo;
import com.ruoyi.system.domain.SysOss;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* OSS对象存储视图对象 sys_oss
*
* @author Lion Li
*/
@Data
@AutoMapper(target = SysOss.class)
public class SysOssVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 对象存储主键
*/
private Long ossId;
/**
* 文件名
*/
private String fileName;
/**
* 原名
*/
private String originalName;
/**
* 文件后缀名
*/
private String fileSuffix;
/**
* URL地址
*/
private String url;
/**
* 创建时间
*/
private Date createTime;
/**
* 上传人
*/
private Long createBy;
/**
* 上传人名称
*/
private String createByName;
/**
* 服务商
*/
private String service;
}

View File

@ -3,6 +3,7 @@ package com.ruoyi.system.domain.vo;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.mybatisflex.annotation.ColumnMask;
import com.mybatisflex.annotation.RelationOneToOne;
import com.mybatisflex.core.mask.Masks;
import com.ruoyi.common.translation.annotation.Translation;
import com.ruoyi.common.translation.constant.TransConstant;
@ -78,9 +79,19 @@ public class SysUserVo implements Serializable {
/**
* 头像地址
*/
@Translation(type = TransConstant.OSS_ID_TO_URL)
private Long avatar;
/**
* 头像地址URL
*/
@RelationOneToOne(
selfField = "avatar",
targetTable = "sys_oss",
targetField = "ossId",
valueField = "url"
)
private String url;
/**
* 密码
*/

View File

@ -0,0 +1,17 @@
package com.ruoyi.system.mapper;
import com.mybatisflex.core.BaseMapper;
import com.ruoyi.system.domain.SysOssConfig;
import org.apache.ibatis.annotations.Mapper;
/**
* 对象存储配置Mapper接口
*
* @author Lion Li
* @author 孤舟烟雨
* @date 2021-08-13
*/
@Mapper
public interface SysOssConfigMapper extends BaseMapper<SysOssConfig> {
}

View File

@ -0,0 +1,14 @@
package com.ruoyi.system.mapper;
import com.mybatisflex.core.BaseMapper;
import com.ruoyi.system.domain.SysOss;
import org.apache.ibatis.annotations.Mapper;
/**
* 文件上传 数据层
*
* @author Lion Li
*/
@Mapper
public interface SysOssMapper extends BaseMapper<SysOss> {
}

View File

@ -0,0 +1,28 @@
package com.ruoyi.system.runner;
import com.ruoyi.system.service.ISysOssConfigService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
/**
* 初始化 system 模块对应业务数据
*
* @author Lion Li
*/
@Slf4j
@RequiredArgsConstructor
@Component
public class SystemApplicationRunner implements ApplicationRunner {
private final ISysOssConfigService ossConfigService;
@Override
public void run(ApplicationArguments args) throws Exception {
ossConfigService.init();
log.info("初始化OSS配置成功");
}
}

View File

@ -0,0 +1,66 @@
package com.ruoyi.system.service;
import com.ruoyi.common.orm.core.page.TableDataInfo;
import com.ruoyi.common.orm.core.service.IBaseService;
import com.ruoyi.system.domain.SysOssConfig;
import com.ruoyi.system.domain.bo.SysOssConfigBo;
import com.ruoyi.system.domain.vo.SysOssConfigVo;
import java.util.Collection;
/**
* 对象存储配置Service接口
*
* @author Lion Li
* @author 孤舟烟雨
* @date 2021-08-13
*/
public interface ISysOssConfigService extends IBaseService<SysOssConfig> {
/**
* 初始化OSS配置
*/
void init();
/**
* 查询单个
*/
SysOssConfigVo queryById(Long ossConfigId);
/**
* 查询列表
*/
TableDataInfo<SysOssConfigVo> queryPageList(SysOssConfigBo bo);
/**
* 根据新增业务对象插入对象存储配置
*
* @param bo 对象存储配置新增业务对象
* @return
*/
Boolean insertByBo(SysOssConfigBo bo);
/**
* 根据编辑业务对象修改对象存储配置
*
* @param bo 对象存储配置编辑业务对象
* @return
*/
Boolean updateByBo(SysOssConfigBo bo);
/**
* 校验并删除数据
*
* @param ids 主键集合
* @param isValid 是否校验,true-删除前校验,false-不校验
* @return
*/
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
/**
* 转到非默认状态
*/
boolean updateOssConfigStatus(SysOssConfigBo bo);
}

View File

@ -0,0 +1,37 @@
package com.ruoyi.system.service;
import com.ruoyi.common.orm.core.page.TableDataInfo;
import com.ruoyi.common.orm.core.service.IBaseService;
import com.ruoyi.system.domain.SysOss;
import com.ruoyi.system.domain.bo.SysOssBo;
import com.ruoyi.system.domain.vo.SysOssVo;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
/**
* 文件上传 服务层
*
* @author Lion Li
*/
public interface ISysOssService extends IBaseService<SysOss> {
TableDataInfo<SysOssVo> queryPageList(SysOssBo sysOss);
List<SysOssVo> listSysOssByIds(Collection<Long> ossIds);
SysOssVo getById(Long ossId);
SysOssVo upload(MultipartFile file);
SysOssVo upload(File file);
void download(Long ossId, HttpServletResponse response) throws IOException;
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
}

View File

@ -195,11 +195,11 @@ public interface ISysUserService extends IBaseService<SysUser>
/**
* 修改用户头像
*
* @param userName 用户名
* @param userId 用户ID
* @param avatar 头像地址
* @return 结果:true 更新成功false 更新失败
* @return 结果
*/
boolean updateUserAvatar(String userName, String avatar);
boolean updateUserAvatar(Long userId, Long avatar);
/**
* 重置用户密码

View File

@ -0,0 +1,192 @@
package com.ruoyi.system.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import com.mybatisflex.core.paginate.Page;
import com.mybatisflex.core.query.QueryWrapper;
import com.ruoyi.common.orm.core.service.impl.BaseServiceImpl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import com.ruoyi.common.core.constant.CacheNames;
import com.ruoyi.common.core.exception.ServiceException;
import com.ruoyi.common.core.utils.MapstructUtils;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.json.utils.JsonUtils;
import com.ruoyi.common.orm.core.page.PageQuery;
import com.ruoyi.common.orm.core.page.TableDataInfo;
import com.ruoyi.common.oss.constant.OssConstant;
import com.ruoyi.common.redis.utils.CacheUtils;
import com.ruoyi.common.redis.utils.RedisUtils;
import com.ruoyi.system.domain.SysOssConfig;
import com.ruoyi.system.domain.bo.SysOssConfigBo;
import com.ruoyi.system.domain.vo.SysOssConfigVo;
import com.ruoyi.system.mapper.SysOssConfigMapper;
import com.ruoyi.system.service.ISysOssConfigService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Collection;
import java.util.List;
import static com.ruoyi.system.domain.table.SysOssConfigTableDef.SYS_OSS_CONFIG;
/**
* 对象存储配置Service业务层处理
*
* @author Lion Li
* @author 孤舟烟雨
* @author 数据小王子
* @date 2023-11-30
*/
@Slf4j
@RequiredArgsConstructor
@Service
public class SysOssConfigServiceImpl extends BaseServiceImpl<SysOssConfigMapper,SysOssConfig> implements ISysOssConfigService {
private final SysOssConfigMapper baseMapper;
@Override
public QueryWrapper query() {
return super.query().from(SYS_OSS_CONFIG);
}
/**
* 项目启动时初始化参数到缓存加载配置类
*/
@Override
public void init() {
List<SysOssConfig> list = this.list();
// 加载OSS初始化配置
for (SysOssConfig config : list) {
String configKey = config.getConfigKey();
if ("0".equals(config.getStatus())) {
RedisUtils.setCacheObject(OssConstant.DEFAULT_CONFIG_KEY, configKey);
}
CacheUtils.put(CacheNames.SYS_OSS_CONFIG, config.getConfigKey(), JsonUtils.toJsonString(config));
}
}
@Override
public SysOssConfigVo queryById(Long ossConfigId) {
return this.getOneAs(query().where(SYS_OSS_CONFIG.OSS_CONFIG_ID.eq(ossConfigId)), SysOssConfigVo.class);
}
/**
* 根据bo构建QueryWrapper查询条件
*
* @param bo
* @return 查询条件
*/
private QueryWrapper buildQueryWrapper(SysOssConfigBo bo) {
QueryWrapper queryWrapper = super.buildBaseQueryWrapper();
if (StringUtils.isNotEmpty(bo.getConfigKey())) {
queryWrapper.and(SYS_OSS_CONFIG.CONFIG_KEY.eq(bo.getConfigKey()));
}
if (StringUtils.isNotEmpty(bo.getBucketName())) {
queryWrapper.and(SYS_OSS_CONFIG.BUCKET_NAME.like(bo.getBucketName()));
}
if (ObjectUtil.isNotNull(bo.getStatus())) {
queryWrapper.and(SYS_OSS_CONFIG.STATUS.eq(bo.getStatus()));
}
queryWrapper.orderBy(SYS_OSS_CONFIG.OSS_CONFIG_ID.asc());
return queryWrapper;
}
@Override
public TableDataInfo<SysOssConfigVo> queryPageList(SysOssConfigBo bo) {
QueryWrapper queryWrapper = buildQueryWrapper(bo);
Page<SysOssConfigVo> page = this.pageAs(PageQuery.build(), queryWrapper, SysOssConfigVo.class);
return TableDataInfo.build(page);
}
@Override
public Boolean insertByBo(SysOssConfigBo bo) {
SysOssConfig config = MapstructUtils.convert(bo, SysOssConfig.class);
validEntityBeforeSave(config);
boolean flag = this.save(config);
if (flag) {
// 从数据库查询完整的数据做缓存
config = this.getById(config.getOssConfigId());
CacheUtils.put(CacheNames.SYS_OSS_CONFIG, config.getConfigKey(), JsonUtils.toJsonString(config));
}
return flag;
}
@Override
public Boolean updateByBo(SysOssConfigBo bo) {
SysOssConfig config = MapstructUtils.convert(bo, SysOssConfig.class);
validEntityBeforeSave(config);
boolean flag = this.updateById(config);
if (flag) {
// 从数据库查询完整的数据做缓存
config = this.getById(config.getOssConfigId());
CacheUtils.put(CacheNames.SYS_OSS_CONFIG, config.getConfigKey(), JsonUtils.toJsonString(config));
}
return flag;
}
/**
* 保存前的数据校验
*/
private void validEntityBeforeSave(SysOssConfig entity) {
if (StringUtils.isNotEmpty(entity.getConfigKey())
&& !checkConfigKeyUnique(entity)) {
throw new ServiceException("操作配置'" + entity.getConfigKey() + "'失败, 配置key已存在!");
}
}
@Override
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
if (isValid) {
if (CollUtil.containsAny(ids, OssConstant.SYSTEM_DATA_IDS)) {
throw new ServiceException("系统内置, 不可删除!");
}
}
List<SysOssConfig> list = CollUtil.newArrayList();
for (Long configId : ids) {
SysOssConfig config = this.getById(configId);
list.add(config);
}
boolean flag = this.removeByIds(ids);
if (flag) {
list.forEach(sysOssConfig ->
CacheUtils.evict(CacheNames.SYS_OSS_CONFIG, sysOssConfig.getConfigKey()));
}
return flag;
}
/**
* 判断configKey是否唯一
*/
private boolean checkConfigKeyUnique(SysOssConfig sysOssConfig) {
long ossConfigId = ObjectUtil.isNull(sysOssConfig.getOssConfigId()) ? -1L : sysOssConfig.getOssConfigId();
QueryWrapper queryWrapper = query();
if (StringUtils.isNotEmpty(sysOssConfig.getConfigKey())) {
queryWrapper.where(SYS_OSS_CONFIG.CONFIG_KEY.eq(sysOssConfig.getConfigKey()));
}
SysOssConfig info = this.getOne(queryWrapper);
if (ObjectUtil.isNotNull(info) && info.getOssConfigId() != ossConfigId) {
return false;
}
return true;
}
/**
* 转到非默认状态停用
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean updateOssConfigStatus(SysOssConfigBo bo) {
SysOssConfig sysOssConfig = MapstructUtils.convert(bo, SysOssConfig.class);
boolean updated = this.updateById(sysOssConfig);
if (updated) {
RedisUtils.setCacheObject(OssConstant.DEFAULT_CONFIG_KEY, sysOssConfig.getConfigKey());
}
return updated;
}
}

View File

@ -0,0 +1,221 @@
package com.ruoyi.system.service.impl;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.ObjectUtil;
import com.mybatisflex.core.paginate.Page;
import com.mybatisflex.core.query.QueryWrapper;
import com.ruoyi.common.core.constant.CacheNames;
import com.ruoyi.common.core.exception.ServiceException;
import com.ruoyi.common.core.service.OssService;
import com.ruoyi.common.core.utils.MapstructUtils;
import com.ruoyi.common.core.utils.SpringUtils;
import com.ruoyi.common.core.utils.StreamUtils;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.core.utils.file.FileUtils;
import com.ruoyi.common.orm.core.page.PageQuery;
import com.ruoyi.common.orm.core.page.TableDataInfo;
import com.ruoyi.common.orm.core.service.impl.BaseServiceImpl;
import com.ruoyi.common.oss.core.OssClient;
import com.ruoyi.common.oss.entity.UploadResult;
import com.ruoyi.common.oss.enumd.AccessPolicyType;
import com.ruoyi.common.oss.factory.OssFactory;
import com.ruoyi.system.domain.SysOss;
import com.ruoyi.system.domain.bo.SysOssBo;
import com.ruoyi.system.domain.vo.SysOssVo;
import com.ruoyi.system.mapper.SysOssMapper;
import com.ruoyi.system.service.ISysOssService;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
import static com.ruoyi.system.domain.table.SysOssTableDef.SYS_OSS;
/**
* 文件上传 服务层实现
*
* @author Lion Li
* @author 数据小王子
* @date 2023-11-30
*/
@RequiredArgsConstructor
@Service
public class SysOssServiceImpl extends BaseServiceImpl<SysOssMapper, SysOss> implements ISysOssService, OssService {
private final SysOssMapper baseMapper;
@Override
public QueryWrapper query() {
return super.query().from(SYS_OSS);
}
private QueryWrapper buildQueryWrapper(SysOssBo bo) {
QueryWrapper queryWrapper = super.buildBaseQueryWrapper();
if (StringUtils.isNotEmpty(bo.getFileName())) {
queryWrapper.where(SYS_OSS.FILE_NAME.like(bo.getFileName()));
}
if (StringUtils.isNotEmpty(bo.getOriginalName())) {
queryWrapper.and(SYS_OSS.ORIGINAL_NAME.like(bo.getOriginalName()));
}
if (StringUtils.isNotEmpty(bo.getFileSuffix())) {
queryWrapper.and(SYS_OSS.FILE_SUFFIX.eq(bo.getFileSuffix()));
}
if (StringUtils.isNotEmpty(bo.getUrl())) {
queryWrapper.and(SYS_OSS.URL.eq(bo.getUrl()));
}
Map<String, Object> params = bo.getParams();
if (params.get("beginCreateTime") != null && params.get("endCreateTime") != null) {
queryWrapper.and(SYS_OSS.CREATE_TIME.between(params.get("beginCreateTime"), params.get("endCreateTime")));
}
if (StringUtils.isNotEmpty(bo.getService())) {
queryWrapper.and(SYS_OSS.SERVICE.eq(bo.getService()));
}
queryWrapper.orderBy(SYS_OSS.OSS_ID.asc());
return queryWrapper;
}
@Override
public TableDataInfo<SysOssVo> queryPageList(SysOssBo bo) {
QueryWrapper queryWrapper = buildQueryWrapper(bo);
Page<SysOssVo> result = this.pageAs(PageQuery.build(), queryWrapper, SysOssVo.class);
List<SysOssVo> filterResult = StreamUtils.toList(result.getRecords(), this::matchingUrl);
result.setRecords(filterResult);
return TableDataInfo.build(result);
}
@Override
public List<SysOssVo> listSysOssByIds(Collection<Long> ossIds) {
List<SysOssVo> list = new ArrayList<>();
for (Long id : ossIds) {
SysOssVo vo = SpringUtils.getAopProxy(this).getById(id);
if (ObjectUtil.isNotNull(vo)) {
try {
list.add(this.matchingUrl(vo));
} catch (Exception ignored) {
// 如果oss异常无法连接则将数据直接返回
list.add(vo);
}
}
}
return list;
}
@Override
public String selectUrlByIds(String ossIds) {
List<String> list = new ArrayList<>();
for (Long id : StringUtils.splitTo(ossIds, Convert::toLong)) {
SysOssVo vo = SpringUtils.getAopProxy(this).getById(id);
if (ObjectUtil.isNotNull(vo)) {
try {
list.add(this.matchingUrl(vo).getUrl());
} catch (Exception ignored) {
// 如果oss异常无法连接则将数据直接返回
list.add(vo.getUrl());
}
}
}
return String.join(StringUtils.SEPARATOR, list);
}
@Cacheable(cacheNames = CacheNames.SYS_OSS, key = "#ossId")
@Override
public SysOssVo getById(Long ossId) {
QueryWrapper queryWrapper=query().where(SYS_OSS.OSS_ID.eq(ossId));
return this.getOneAs(queryWrapper,SysOssVo.class);
}
@Override
public void download(Long ossId, HttpServletResponse response) throws IOException {
SysOssVo sysOss = SpringUtils.getAopProxy(this).getById(ossId);
if (ObjectUtil.isNull(sysOss)) {
throw new ServiceException("文件数据不存在!");
}
FileUtils.setAttachmentResponseHeader(response, sysOss.getOriginalName());
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE + "; charset=UTF-8");
OssClient storage = OssFactory.instance(sysOss.getService());
try(InputStream inputStream = storage.getObjectContent(sysOss.getUrl())) {
int available = inputStream.available();
IoUtil.copy(inputStream, response.getOutputStream(), available);
response.setContentLength(available);
} catch (Exception e) {
throw new ServiceException(e.getMessage());
}
}
@Override
public SysOssVo upload(MultipartFile file) {
String originalfileName = file.getOriginalFilename();
String suffix = StringUtils.substring(originalfileName, originalfileName.lastIndexOf("."), originalfileName.length());
OssClient storage = OssFactory.instance();
UploadResult uploadResult;
try {
uploadResult = storage.uploadSuffix(file.getBytes(), suffix, file.getContentType());
} catch (IOException e) {
throw new ServiceException(e.getMessage());
}
// 保存文件信息
return buildResultEntity(originalfileName, suffix, storage.getConfigKey(), uploadResult);
}
@Override
public SysOssVo upload(File file) {
String originalfileName = file.getName();
String suffix = StringUtils.substring(originalfileName, originalfileName.lastIndexOf("."), originalfileName.length());
OssClient storage = OssFactory.instance();
UploadResult uploadResult = storage.uploadSuffix(file, suffix);
// 保存文件信息
return buildResultEntity(originalfileName, suffix, storage.getConfigKey(), uploadResult);
}
private SysOssVo buildResultEntity(String originalfileName, String suffix, String configKey, UploadResult uploadResult) {
SysOss oss = new SysOss();
oss.setUrl(uploadResult.getUrl());
oss.setFileSuffix(suffix);
oss.setFileName(uploadResult.getFilename());
oss.setOriginalName(originalfileName);
oss.setService(configKey);
this.save(oss);
SysOssVo sysOssVo = MapstructUtils.convert(oss, SysOssVo.class);
return this.matchingUrl(sysOssVo);
}
@Override
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
if (isValid) {
// 做一些业务上的校验,判断是否需要校验
}
List<SysOss> list = this.listByIds(ids);
for (SysOss sysOss : list) {
OssClient storage = OssFactory.instance(sysOss.getService());
storage.delete(sysOss.getUrl());
}
return this.removeByIds(ids);
}
/**
* 匹配Url
*
* @param oss OSS对象
* @return oss 匹配Url的OSS对象
*/
private SysOssVo matchingUrl(SysOssVo oss) {
OssClient storage = OssFactory.instance(oss.getService());
// 仅修改桶类型为 private 的URL临时URL时长为120s
if (AccessPolicyType.PRIVATE == storage.getAccessPolicy()) {
oss.setUrl(storage.getPrivateUrl(oss.getFileName(), 120));
}
return oss;
}
}

View File

@ -301,7 +301,7 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserMapper, SysUser>
if (ObjectUtil.isNotNull(userId)) {
queryWrapper.where(SYS_USER.USER_ID.eq(userId));
}
return this.getOneAs(queryWrapper,SysUserVo.class);
return userMapper.selectOneWithRelationsByQueryAs(queryWrapper,SysUserVo.class);//使用Relation注解从sys_oss中查询头像地址URL
}
/**
@ -565,15 +565,16 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserMapper, SysUser>
/**
* 修改用户头像
* update sys_user set avatar = #{avatar} where user_name = #{userName}
* @param userName 用户名
* update sys_user set avatar = #{avatar} where user_id = #{userId}
* @param userId 用户ID
* @param avatar 头像地址
* @return 结果:true 更新成功false 更新失败
*/
@Override
public boolean updateUserAvatar(String userName, String avatar) {
QueryWrapper queryWrapper = query().where(SYS_USER.USER_NAME.eq(userName));
public boolean updateUserAvatar(Long userId, Long avatar) {
QueryWrapper queryWrapper = query().where(SYS_USER.USER_ID.eq(userId));
SysUser sysUser = new SysUser();
sysUser.setUserId(userId);
sysUser.setAvatar(avatar);
return this.update(sysUser,queryWrapper);
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.system.mapper.SysOssConfigMapper">
</mapper>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.system.mapper.SysOssMapper">
</mapper>