diff --git a/flex-ui/src/api/system/oss.js b/flex-ui/src/api/system/oss.js new file mode 100644 index 0000000..5cb1b01 --- /dev/null +++ b/flex-ui/src/api/system/oss.js @@ -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' + }); +} diff --git a/flex-ui/src/api/system/ossConfig.js b/flex-ui/src/api/system/ossConfig.js new file mode 100644 index 0000000..29a8549 --- /dev/null +++ b/flex-ui/src/api/system/ossConfig.js @@ -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 + }); +} diff --git a/flex-ui/src/api/system/user.js b/flex-ui/src/api/system/user.js index f2f76ef..7064297 100644 --- a/flex-ui/src/api/system/user.js +++ b/flex-ui/src/api/system/user.js @@ -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 }) diff --git a/flex-ui/src/components/FileUpload/index.vue b/flex-ui/src/components/FileUpload/index.vue index 16e1fb0..383f526 100644 --- a/flex-ui/src/components/FileUpload/index.vue +++ b/flex-ui/src/components/FileUpload/index.vue @@ -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> diff --git a/flex-ui/src/components/ImageUpload/index.vue b/flex-ui/src/components/ImageUpload/index.vue index 55dafb8..b0635f4 100644 --- a/flex-ui/src/components/ImageUpload/index.vue +++ b/flex-ui/src/components/ImageUpload/index.vue @@ -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 }; - } else { - item = { name: item, url: item }; - } + itemData = { name: item, url: item }; + } else { + // 此处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> @@ -210,4 +220,4 @@ function listToString(list, separator) { :deep(.hide .el-upload--picture-card) { display: none; } -</style> \ No newline at end of file +</style> diff --git a/flex-ui/src/store/modules/user.js b/flex-ui/src/store/modules/user.js index 434815c..29be1ba 100644 --- a/flex-ui/src/store/modules/user.js +++ b/flex-ui/src/store/modules/user.js @@ -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 diff --git a/flex-ui/src/views/system/oss/config.vue b/flex-ui/src/views/system/oss/config.vue new file mode 100644 index 0000000..ccf2a31 --- /dev/null +++ b/flex-ui/src/views/system/oss/config.vue @@ -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> diff --git a/flex-ui/src/views/system/oss/index.vue b/flex-ui/src/views/system/oss/index.vue new file mode 100644 index 0000000..1435649 --- /dev/null +++ b/flex-ui/src/views/system/oss/index.vue @@ -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 { + //如果order为null 则删除排序字段和属性 + 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> diff --git a/flex-ui/src/views/system/user/profile/index.vue b/flex-ui/src/views/system/user/profile/index.vue index 77dbe8b..3bb80e7 100644 --- a/flex-ui/src/views/system/user/profile/index.vue +++ b/flex-ui/src/views/system/user/profile/index.vue @@ -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"> diff --git a/flex-ui/src/views/system/user/profile/userAvatar.vue b/flex-ui/src/views/system/user/profile/userAvatar.vue index 6950d83..b3b5da1 100644 --- a/flex-ui/src/views/system/user/profile/userAvatar.vue +++ b/flex-ui/src/views/system/user/profile/userAvatar.vue @@ -1,53 +1,62 @@ <template> - <div class="user-info-head" @click="editCropper()"><img :src="options.img" title="点击上传头像" class="img-circle img-lg" /></div> - <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'}"> - <vue-cropper - ref="cropper" - :img="options.img" - :info="true" - :autoCrop="options.autoCrop" - :autoCropWidth="options.autoCropWidth" - :autoCropHeight="options.autoCropHeight" - :fixedBox="options.fixedBox" - @realTime="realTime" - v-if="visible" - /> - </el-col> - <el-col :xs="24" :md="12" :style="{height: '350px'}"> - <div class="avatar-upload-preview"> - <img :src="options.previews.url" :style="options.previews.img"/> - </div> - </el-col> - </el-row> - <br/> - <el-row> - <el-col :lg="2" :md="2"> - <el-upload action="#" :http-request="requestUpload" :show-file-list="false" :before-upload="beforeUpload"> - <el-button> - 选择 - <el-icon class="el-icon--right"><Upload /></el-icon> - </el-button> - </el-upload> - </el-col> - <el-col :lg="{span: 1, offset: 2}" :md="2"> - <el-button icon="Plus" @click="changeScale(1)"></el-button> - </el-col> - <el-col :lg="{span: 1, offset: 1}" :md="2"> - <el-button icon="Minus" @click="changeScale(-1)"></el-button> - </el-col> - <el-col :lg="{span: 1, offset: 1}" :md="2"> - <el-button icon="RefreshLeft" @click="rotateLeft()"></el-button> - </el-col> - <el-col :lg="{span: 1, offset: 1}" :md="2"> - <el-button icon="RefreshRight" @click="rotateRight()"></el-button> - </el-col> - <el-col :lg="{span: 2, offset: 6}" :md="2"> - <el-button type="primary" @click="uploadImg()">提 交</el-button> - </el-col> - </el-row> - </el-dialog> + <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' }"> + <vue-cropper + ref="cropper" + :img="options.img" + :info="true" + :autoCrop="options.autoCrop" + :autoCropWidth="options.autoCropWidth" + :autoCropHeight="options.autoCropHeight" + :fixedBox="options.fixedBox" + :outputType="options.outputType" + @realTime="realTime" + v-if="visible" + /> + </el-col> + <el-col :xs="24" :md="12" :style="{ height: '350px' }"> + <div class="avatar-upload-preview"> + <img :src="options.previews.url" :style="options.previews.img" /> + </div> + </el-col> + </el-row> + <br /> + <el-row> + <el-col :lg="2" :md="2"> + <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> + </el-button> + </el-upload> + </el-col> + <el-col :lg="{ span: 1, offset: 2 }" :md="2"> + <el-button icon="Plus" @click="changeScale(1)"></el-button> + </el-col> + <el-col :lg="{ span: 1, offset: 1 }" :md="2"> + <el-button icon="Minus" @click="changeScale(-1)"></el-button> + </el-col> + <el-col :lg="{ span: 1, offset: 1 }" :md="2"> + <el-button icon="RefreshLeft" @click="rotateLeft()"></el-button> + </el-col> + <el-col :lg="{ span: 1, offset: 1 }" :md="2"> + <el-button icon="RefreshRight" @click="rotateRight()"></el-button> + </el-col> + <el-col :lg="{ span: 2, offset: 6 }" :md="2"> + <el-button type="primary" @click="uploadImg()">提 交</el-button> + </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> diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/ruoyi/common/core/service/OssService.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/ruoyi/common/core/service/OssService.java new file mode 100644 index 0000000..65dda7c --- /dev/null +++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/ruoyi/common/core/service/OssService.java @@ -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); + +} diff --git a/ruoyi-common/ruoyi-common-oss/pom.xml b/ruoyi-common/ruoyi-common-oss/pom.xml new file mode 100644 index 0000000..3725533 --- /dev/null +++ b/ruoyi-common/ruoyi-common-oss/pom.xml @@ -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> diff --git a/ruoyi-common/ruoyi-common-oss/src/main/java/com/ruoyi/common/oss/constant/OssConstant.java b/ruoyi-common/ruoyi-common-oss/src/main/java/com/ruoyi/common/oss/constant/OssConstant.java new file mode 100644 index 0000000..50a3292 --- /dev/null +++ b/ruoyi-common/ruoyi-common-oss/src/main/java/com/ruoyi/common/oss/constant/OssConstant.java @@ -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"; + +} diff --git a/ruoyi-common/ruoyi-common-oss/src/main/java/com/ruoyi/common/oss/core/OssClient.java b/ruoyi-common/ruoyi-common-oss/src/main/java/com/ruoyi/common/oss/core/OssClient.java new file mode 100644 index 0000000..d22f540 --- /dev/null +++ b/ruoyi-common/ruoyi-common-oss/src/main/java/com/ruoyi/common/oss/core/OssClient.java @@ -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(); + } + +} diff --git a/ruoyi-common/ruoyi-common-oss/src/main/java/com/ruoyi/common/oss/entity/UploadResult.java b/ruoyi-common/ruoyi-common-oss/src/main/java/com/ruoyi/common/oss/entity/UploadResult.java new file mode 100644 index 0000000..fd2e7fc --- /dev/null +++ b/ruoyi-common/ruoyi-common-oss/src/main/java/com/ruoyi/common/oss/entity/UploadResult.java @@ -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; +} diff --git a/ruoyi-common/ruoyi-common-oss/src/main/java/com/ruoyi/common/oss/enumd/AccessPolicyType.java b/ruoyi-common/ruoyi-common-oss/src/main/java/com/ruoyi/common/oss/enumd/AccessPolicyType.java new file mode 100644 index 0000000..140f67a --- /dev/null +++ b/ruoyi-common/ruoyi-common-oss/src/main/java/com/ruoyi/common/oss/enumd/AccessPolicyType.java @@ -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); + } + +} diff --git a/ruoyi-common/ruoyi-common-oss/src/main/java/com/ruoyi/common/oss/enumd/PolicyType.java b/ruoyi-common/ruoyi-common-oss/src/main/java/com/ruoyi/common/oss/enumd/PolicyType.java new file mode 100644 index 0000000..c019d3b --- /dev/null +++ b/ruoyi-common/ruoyi-common-oss/src/main/java/com/ruoyi/common/oss/enumd/PolicyType.java @@ -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; + +} diff --git a/ruoyi-common/ruoyi-common-oss/src/main/java/com/ruoyi/common/oss/exception/OssException.java b/ruoyi-common/ruoyi-common-oss/src/main/java/com/ruoyi/common/oss/exception/OssException.java new file mode 100644 index 0000000..cfc7520 --- /dev/null +++ b/ruoyi-common/ruoyi-common-oss/src/main/java/com/ruoyi/common/oss/exception/OssException.java @@ -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); + } + +} diff --git a/ruoyi-common/ruoyi-common-oss/src/main/java/com/ruoyi/common/oss/factory/OssFactory.java b/ruoyi-common/ruoyi-common-oss/src/main/java/com/ruoyi/common/oss/factory/OssFactory.java new file mode 100644 index 0000000..b723230 --- /dev/null +++ b/ruoyi-common/ruoyi-common-oss/src/main/java/com/ruoyi/common/oss/factory/OssFactory.java @@ -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; + } + +} diff --git a/ruoyi-common/ruoyi-common-oss/src/main/java/com/ruoyi/common/oss/properties/OssProperties.java b/ruoyi-common/ruoyi-common-oss/src/main/java/com/ruoyi/common/oss/properties/OssProperties.java new file mode 100644 index 0000000..8145fb9 --- /dev/null +++ b/ruoyi-common/ruoyi-common-oss/src/main/java/com/ruoyi/common/oss/properties/OssProperties.java @@ -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; + + /** + * 是否https(Y=是,N=否) + */ + private String isHttps; + + /** + * 桶权限类型(0private 1public 2custom) + */ + private String accessPolicy; + +} diff --git a/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/controller/system/SysOssConfigController.java b/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/controller/system/SysOssConfigController.java new file mode 100644 index 0000000..ca118d7 --- /dev/null +++ b/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/controller/system/SysOssConfigController.java @@ -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(); + } +} diff --git a/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/controller/system/SysOssController.java b/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/controller/system/SysOssController.java new file mode 100644 index 0000000..001b8bc --- /dev/null +++ b/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/controller/system/SysOssController.java @@ -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(); + } + +} diff --git a/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/controller/system/SysProfileController.java b/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/controller/system/SysProfileController.java index 07fc85d..4521ce4 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/controller/system/SysProfileController.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/controller/system/SysProfileController.java @@ -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); diff --git a/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysOss.java b/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysOss.java new file mode 100644 index 0000000..e7c1c9b --- /dev/null +++ b/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysOss.java @@ -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; + +} diff --git a/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysOssConfig.java b/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysOssConfig.java new file mode 100644 index 0000000..24bff59 --- /dev/null +++ b/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysOssConfig.java @@ -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; + + /** + * 是否https(0否 1是) + */ + private String isHttps; + + /** + * 域 + */ + private String region; + + /** + * 是否默认(0=是,1=否) + */ + private String status; + + /** + * 扩展字段 + */ + private String ext1; + + /** + * 备注 + */ + private String remark; + + /** + * 桶权限类型(0private 1public 2custom) + */ + private String accessPolicy; +} diff --git a/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUser.java b/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUser.java index 65cc201..dc913b5 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUser.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysUser.java @@ -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); diff --git a/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/domain/bo/SysOssBo.java b/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/domain/bo/SysOssBo.java new file mode 100644 index 0000000..e991400 --- /dev/null +++ b/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/domain/bo/SysOssBo.java @@ -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; + +} diff --git a/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/domain/bo/SysOssConfigBo.java b/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/domain/bo/SysOssConfigBo.java new file mode 100644 index 0000000..3795d43 --- /dev/null +++ b/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/domain/bo/SysOssConfigBo.java @@ -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; + + /** + * 是否https(Y=是,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; + +} diff --git a/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/SysOssConfigVo.java b/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/SysOssConfigVo.java new file mode 100644 index 0000000..11ef727 --- /dev/null +++ b/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/SysOssConfigVo.java @@ -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; + + /** + * 是否https(Y=是,N=否) + */ + private String isHttps; + + /** + * 域 + */ + private String region; + + /** + * 是否默认(0=是,1=否) + */ + private String status; + + /** + * 扩展字段 + */ + private String ext1; + + /** + * 备注 + */ + private String remark; + + /** + * 桶权限类型(0private 1public 2custom) + */ + private String accessPolicy; + +} diff --git a/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/SysOssUploadVo.java b/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/SysOssUploadVo.java new file mode 100644 index 0000000..c7630f0 --- /dev/null +++ b/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/SysOssUploadVo.java @@ -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; + +} diff --git a/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/SysOssVo.java b/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/SysOssVo.java new file mode 100644 index 0000000..8fe1d37 --- /dev/null +++ b/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/SysOssVo.java @@ -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; + + +} diff --git a/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/SysUserVo.java b/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/SysUserVo.java index e3ebef8..1f9e961 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/SysUserVo.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/SysUserVo.java @@ -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; + /** * 密码 */ diff --git a/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysOssConfigMapper.java b/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysOssConfigMapper.java new file mode 100644 index 0000000..181b432 --- /dev/null +++ b/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysOssConfigMapper.java @@ -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> { + +} diff --git a/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysOssMapper.java b/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysOssMapper.java new file mode 100644 index 0000000..2880948 --- /dev/null +++ b/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysOssMapper.java @@ -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> { +} diff --git a/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/runner/SystemApplicationRunner.java b/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/runner/SystemApplicationRunner.java new file mode 100644 index 0000000..e44cf87 --- /dev/null +++ b/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/runner/SystemApplicationRunner.java @@ -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配置成功"); + } + +} diff --git a/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysOssConfigService.java b/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysOssConfigService.java new file mode 100644 index 0000000..09cd538 --- /dev/null +++ b/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysOssConfigService.java @@ -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); + +} diff --git a/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysOssService.java b/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysOssService.java new file mode 100644 index 0000000..c730764 --- /dev/null +++ b/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysOssService.java @@ -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); + +} diff --git a/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserService.java b/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserService.java index 145e612..00a5a74 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserService.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysUserService.java @@ -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); /** * 重置用户密码 diff --git a/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysOssConfigServiceImpl.java b/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysOssConfigServiceImpl.java new file mode 100644 index 0000000..7d9ee8c --- /dev/null +++ b/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysOssConfigServiceImpl.java @@ -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; + } + +} diff --git a/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysOssServiceImpl.java b/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysOssServiceImpl.java new file mode 100644 index 0000000..f5ac461 --- /dev/null +++ b/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysOssServiceImpl.java @@ -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; + } +} diff --git a/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java b/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java index 7a72e3a..77b90fa 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysUserServiceImpl.java @@ -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 用户名 - * @param avatar 头像地址 + * 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); } diff --git a/ruoyi-modules/ruoyi-system/src/main/resources/mapper/system/SysOssConfigMapper.xml b/ruoyi-modules/ruoyi-system/src/main/resources/mapper/system/SysOssConfigMapper.xml new file mode 100644 index 0000000..8af22fe --- /dev/null +++ b/ruoyi-modules/ruoyi-system/src/main/resources/mapper/system/SysOssConfigMapper.xml @@ -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> diff --git a/ruoyi-modules/ruoyi-system/src/main/resources/mapper/system/SysOssMapper.xml b/ruoyi-modules/ruoyi-system/src/main/resources/mapper/system/SysOssMapper.xml new file mode 100644 index 0000000..24714f6 --- /dev/null +++ b/ruoyi-modules/ruoyi-system/src/main/resources/mapper/system/SysOssMapper.xml @@ -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>