【底座】用户头像、通知公告封面等上传限定只能图片,DevFileController增加限定图片、文档等4个文件上传接口,优化通知公告管理页面、业务首页中通知公告详情的展示样式

This commit is contained in:
xuyuxiang
2026-06-26 02:19:21 +08:00
parent cfd8c0609b
commit 2ffbb3aff2
12 changed files with 556 additions and 177 deletions
@@ -1,56 +1,71 @@
<template>
<xn-form-container title="详情" :width="1000" v-model:open="open" :destroy-on-close="true" @close="onClose">
<a-descriptions bordered>
<a-descriptions-item label="标题">{{ formData.title }}</a-descriptions-item>
<a-descriptions-item label="类型">
<a-tag :bordered="false" :color="$TOOL.dictTypeColor('BIZ_NOTICE_TYPE', formData.type)">
{{ $TOOL.dictTypeData('BIZ_NOTICE_TYPE', formData.type) }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="封面图">
<div v-if="formData.image">
<a-image :src="formData.image" style="width: 100px; height: 50px; margin-bottom: -10px; margin-top: -10px" />
<xn-form-container title="通知公告详情" :width="900" v-model:open="open" :destroy-on-close="true" @close="onClose">
<div class="notice-detail-container">
<!-- 文章头部 -->
<div class="notice-header">
<h1 class="notice-title">{{ formData.title }}</h1>
<div class="notice-meta">
<span class="meta-item">
<a-tag :bordered="false" :color="$TOOL.dictTypeColor('BIZ_NOTICE_TYPE', formData.type)">
{{ $TOOL.dictTypeData('BIZ_NOTICE_TYPE', formData.type) }}
</a-tag>
</span>
<span class="meta-item" v-if="formData.createUserName">
<UserOutlined />
{{ formData.createUserName }}
</span>
<span class="meta-item" v-if="formData.createTime">
<ClockCircleOutlined />
{{ $TOOL.parseTime(formData.createTime, '{y}-{m}-{d} {h}:{i}') }}
</span>
</div>
<span v-else>未上传</span>
</a-descriptions-item>
<a-descriptions-item label="内容"><div v-html="formData.content"></div></a-descriptions-item>
</a-descriptions>
<a-descriptions bordered :column="2" class="mt-2">
<a-descriptions-item label="摘要">{{ formData.digest }}</a-descriptions-item>
<a-descriptions-item label="备注"
><span>{{ formData.remark }}</span></a-descriptions-item
>
<a-descriptions-item label="排序"
><span>{{ formData.sortCode }}</span></a-descriptions-item
>
<a-descriptions-item label="发布位置">
<div v-if="formData.place">
<a-tag v-for="textValue in JSON.parse(formData.place)" :key="textValue" :color="$TOOL.dictTypeColor('BIZ_NOTICE_PLACE', textValue)">
{{ $TOOL.dictTypeData('BIZ_NOTICE_PLACE', textValue) }}
</a-tag>
</div>
<!-- 摘要 -->
<div v-if="formData.digest" class="notice-digest">
{{ formData.digest }}
</div>
<!-- 封面图 -->
<div v-if="formData.image" class="notice-cover">
<a-image :src="formData.image" :preview="true" />
</div>
<!-- 正文内容 -->
<div class="notice-content" v-html="formData.content"></div>
<!-- 附加信息 -->
<div class="notice-footer">
<a-divider />
<div class="footer-info">
<div v-if="formData.place" class="info-row">
<span class="info-label">发布位置:</span>
<a-tag
v-for="textValue in JSON.parse(formData.place)"
:key="textValue"
:color="$TOOL.dictTypeColor('BIZ_NOTICE_PLACE', textValue)"
>
{{ $TOOL.dictTypeData('BIZ_NOTICE_PLACE', textValue) }}
</a-tag>
</div>
<div v-if="formData.remark" class="info-row">
<span class="info-label">备注:</span>
<span>{{ formData.remark }}</span>
</div>
<div v-if="formData.updateUserName && formData.updateTime" class="info-row meta-secondary">
<span>最后更新:{{ formData.updateUserName }} · {{ $TOOL.parseTime(formData.updateTime, '{y}-{m}-{d} {h}:{i}') }}</span>
</div>
</div>
</a-descriptions-item>
<a-descriptions-item label="创建人"
><span>{{ formData.createUserName }}</span></a-descriptions-item
>
<a-descriptions-item label="创建时间"
><span>{{ formData.createTime }}</span></a-descriptions-item
>
<a-descriptions-item label="修改人"
><span>{{ formData.updateUserName }}</span></a-descriptions-item
>
<a-descriptions-item label="修改时间"
><span>{{ formData.updateTime }}</span></a-descriptions-item
>
</a-descriptions>
</div>
</div>
<template #footer>
<a-button style="margin-right: 8px" @click="onClose">关闭</a-button>
<a-button type="primary" @click="onClose" :loading="submitLoading">确定</a-button>
<a-button type="primary" @click="onClose">关闭</a-button>
</template>
</xn-form-container>
</template>
<script setup name="bizNoticeDetail">
import { UserOutlined, ClockCircleOutlined } from '@ant-design/icons-vue'
import bizIndexApi from '@/api/biz/bizIndexApi'
import { message } from 'ant-design-vue'
// 抽屉状态
@@ -58,7 +73,6 @@
const emit = defineEmits({ successful: null })
// 表单数据
const formData = ref({})
const submitLoading = ref(false)
// 打开抽屉
const onOpen = (id) => {
open.value = true
@@ -83,3 +97,150 @@
onOpen
})
</script>
<style lang="less" scoped>
.notice-detail-container {
max-width: 100%;
padding: 20px 0;
}
.notice-header {
text-align: center;
padding-bottom: 20px;
border-bottom: 1px solid #f0f0f0;
margin-bottom: 20px;
}
.notice-title {
font-size: 28px;
font-weight: 600;
line-height: 1.4;
margin: 0 0 16px 0;
color: rgba(0, 0, 0, 0.88);
}
.notice-meta {
display: flex;
align-items: center;
justify-content: center;
gap: 24px;
color: rgba(0, 0, 0, 0.45);
font-size: 14px;
}
.meta-item {
display: flex;
align-items: center;
gap: 6px;
}
.notice-digest {
background: #f5f5f5;
padding: 16px 20px;
border-left: 4px solid #1890ff;
margin-bottom: 24px;
font-size: 15px;
line-height: 1.6;
color: rgba(0, 0, 0, 0.65);
}
.notice-cover {
text-align: center;
margin-bottom: 24px;
:deep(.ant-image) {
max-width: 100%;
max-height: 400px;
border-radius: 4px;
overflow: hidden;
}
:deep(img) {
max-width: 100%;
max-height: 400px;
height: auto;
border-radius: 4px;
object-fit: cover;
}
}
.notice-content {
font-size: 16px;
line-height: 1.8;
color: rgba(0, 0, 0, 0.88);
margin-bottom: 32px;
:deep(p) {
margin-bottom: 16px;
}
:deep(img) {
max-width: 100%;
height: auto;
display: block;
margin: 16px auto;
}
:deep(h1),
:deep(h2),
:deep(h3),
:deep(h4),
:deep(h5),
:deep(h6) {
margin-top: 24px;
margin-bottom: 16px;
font-weight: 600;
}
:deep(ul),
:deep(ol) {
padding-left: 28px;
margin-bottom: 16px;
}
:deep(blockquote) {
border-left: 4px solid #e8e8e8;
padding-left: 16px;
margin: 16px 0;
color: rgba(0, 0, 0, 0.65);
}
:deep(code) {
background: #f5f5f5;
padding: 2px 6px;
border-radius: 3px;
font-family: 'Courier New', Courier, monospace;
}
:deep(pre) {
background: #f5f5f5;
padding: 12px;
border-radius: 4px;
overflow-x: auto;
margin-bottom: 16px;
}
}
.notice-footer {
margin-top: 32px;
}
.footer-info {
.info-row {
margin-bottom: 12px;
font-size: 14px;
line-height: 1.6;
&.meta-secondary {
color: rgba(0, 0, 0, 0.45);
margin-top: 16px;
}
}
.info-label {
font-weight: 500;
color: rgba(0, 0, 0, 0.65);
margin-right: 8px;
}
}
</style>
+206 -49
View File
@@ -1,60 +1,71 @@
<template>
<xn-form-container title="详情" :width="1000" v-model:open="open" :destroy-on-close="true" @close="onClose">
<a-descriptions bordered>
<a-descriptions-item label="标题">{{ formData.title }}</a-descriptions-item>
<a-descriptions-item label="类型">
<a-tag :bordered="false" :color="$TOOL.dictTypeColor('BIZ_NOTICE_TYPE', formData.type)">
{{ $TOOL.dictTypeData('BIZ_NOTICE_TYPE', formData.type) }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="封面图">
<div v-if="formData.image">
<a-image :src="formData.image" style="width: 100px; height: 50px; margin-bottom: -10px; margin-top: -10px" />
<xn-form-container title="通知公告详情" :width="900" v-model:open="open" :destroy-on-close="true" @close="onClose">
<div class="notice-detail-container">
<!-- 文章头部 -->
<div class="notice-header">
<h1 class="notice-title">{{ formData.title }}</h1>
<div class="notice-meta">
<span class="meta-item">
<a-tag :bordered="false" :color="$TOOL.dictTypeColor('BIZ_NOTICE_TYPE', formData.type)">
{{ $TOOL.dictTypeData('BIZ_NOTICE_TYPE', formData.type) }}
</a-tag>
</span>
<span class="meta-item" v-if="formData.createUserName">
<UserOutlined />
{{ formData.createUserName }}
</span>
<span class="meta-item" v-if="formData.createTime">
<ClockCircleOutlined />
{{ $TOOL.parseTime(formData.createTime, '{y}-{m}-{d} {h}:{i}') }}
</span>
</div>
<span v-else>未上传</span>
</a-descriptions-item>
<a-descriptions-item label="内容"><div v-html="formData.content"></div></a-descriptions-item>
</a-descriptions>
<a-descriptions bordered :column="1" class="mt-2">
<a-descriptions-item label="摘要">{{ formData.digest }}</a-descriptions-item>
<a-descriptions-item label="备注"
><span>{{ formData.remark }}</span></a-descriptions-item
>
<a-descriptions-item label="排序"
><span>{{ formData.sortCode }}</span></a-descriptions-item
>
<a-descriptions-item label="发布位置">
<div v-if="formData.place">
<a-tag
v-for="textValue in JSON.parse(formData.place)"
:key="textValue"
:color="$TOOL.dictTypeColor('BIZ_NOTICE_PLACE', textValue)"
>
{{ $TOOL.dictTypeData('BIZ_NOTICE_PLACE', textValue) }}
</a-tag>
</div>
<!-- 摘要 -->
<div v-if="formData.digest" class="notice-digest">
{{ formData.digest }}
</div>
<!-- 封面图 -->
<div v-if="formData.image" class="notice-cover">
<a-image :src="formData.image" :preview="true" />
</div>
<!-- 正文内容 -->
<div class="notice-content" v-html="formData.content"></div>
<!-- 附加信息 -->
<div class="notice-footer">
<a-divider />
<div class="footer-info">
<div v-if="formData.place" class="info-row">
<span class="info-label">发布位置:</span>
<a-tag
v-for="textValue in JSON.parse(formData.place)"
:key="textValue"
:color="$TOOL.dictTypeColor('BIZ_NOTICE_PLACE', textValue)"
>
{{ $TOOL.dictTypeData('BIZ_NOTICE_PLACE', textValue) }}
</a-tag>
</div>
<div v-if="formData.remark" class="info-row">
<span class="info-label">备注:</span>
<span>{{ formData.remark }}</span>
</div>
<div v-if="formData.updateUserName && formData.updateTime" class="info-row meta-secondary">
<span>最后更新:{{ formData.updateUserName }} · {{ $TOOL.parseTime(formData.updateTime, '{y}-{m}-{d} {h}:{i}') }}</span>
</div>
</div>
</a-descriptions-item>
<a-descriptions-item label="创建人"
><span>{{ formData.createUserName }}</span></a-descriptions-item
>
<a-descriptions-item label="创建时间"
><span>{{ formData.createTime }}</span></a-descriptions-item
>
<a-descriptions-item label="修改人"
><span>{{ formData.updateUserName }}</span></a-descriptions-item
>
<a-descriptions-item label="修改时间"
><span>{{ formData.updateTime }}</span></a-descriptions-item
>
</a-descriptions>
</div>
</div>
<template #footer>
<a-button style="margin-right: 8px" @click="onClose">关闭</a-button>
<a-button type="primary" @click="onClose" :loading="submitLoading">确定</a-button>
<a-button type="primary" @click="onClose">关闭</a-button>
</template>
</xn-form-container>
</template>
<script setup name="bizNoticeDetail">
import { UserOutlined, ClockCircleOutlined } from '@ant-design/icons-vue'
import bizNoticeApi from '@/api/biz/bizNoticeApi'
import { message } from 'ant-design-vue'
// 抽屉状态
@@ -62,7 +73,6 @@
const emit = defineEmits({ successful: null })
// 表单数据
const formData = ref({})
const submitLoading = ref(false)
// 打开抽屉
const onOpen = (id) => {
open.value = true
@@ -87,3 +97,150 @@
onOpen
})
</script>
<style lang="less" scoped>
.notice-detail-container {
max-width: 100%;
padding: 20px 0;
}
.notice-header {
text-align: center;
padding-bottom: 20px;
border-bottom: 1px solid #f0f0f0;
margin-bottom: 20px;
}
.notice-title {
font-size: 28px;
font-weight: 600;
line-height: 1.4;
margin: 0 0 16px 0;
color: rgba(0, 0, 0, 0.88);
}
.notice-meta {
display: flex;
align-items: center;
justify-content: center;
gap: 24px;
color: rgba(0, 0, 0, 0.45);
font-size: 14px;
}
.meta-item {
display: flex;
align-items: center;
gap: 6px;
}
.notice-digest {
background: #f5f5f5;
padding: 16px 20px;
border-left: 4px solid #1890ff;
margin-bottom: 24px;
font-size: 15px;
line-height: 1.6;
color: rgba(0, 0, 0, 0.65);
}
.notice-cover {
text-align: center;
margin-bottom: 24px;
:deep(.ant-image) {
max-width: 100%;
max-height: 400px;
border-radius: 4px;
overflow: hidden;
}
:deep(img) {
max-width: 100%;
max-height: 400px;
height: auto;
border-radius: 4px;
object-fit: cover;
}
}
.notice-content {
font-size: 16px;
line-height: 1.8;
color: rgba(0, 0, 0, 0.88);
margin-bottom: 32px;
:deep(p) {
margin-bottom: 16px;
}
:deep(img) {
max-width: 100%;
height: auto;
display: block;
margin: 16px auto;
}
:deep(h1),
:deep(h2),
:deep(h3),
:deep(h4),
:deep(h5),
:deep(h6) {
margin-top: 24px;
margin-bottom: 16px;
font-weight: 600;
}
:deep(ul),
:deep(ol) {
padding-left: 28px;
margin-bottom: 16px;
}
:deep(blockquote) {
border-left: 4px solid #e8e8e8;
padding-left: 16px;
margin: 16px 0;
color: rgba(0, 0, 0, 0.65);
}
:deep(code) {
background: #f5f5f5;
padding: 2px 6px;
border-radius: 3px;
font-family: 'Courier New', Courier, monospace;
}
:deep(pre) {
background: #f5f5f5;
padding: 12px;
border-radius: 4px;
overflow-x: auto;
margin-bottom: 16px;
}
}
.notice-footer {
margin-top: 32px;
}
.footer-info {
.info-row {
margin-bottom: 12px;
font-size: 14px;
line-height: 1.6;
&.meta-secondary {
color: rgba(0, 0, 0, 0.45);
margin-top: 16px;
}
}
.info-label {
font-weight: 500;
color: rgba(0, 0, 0, 0.65);
margin-right: 8px;
}
}
</style>
@@ -25,7 +25,11 @@
</a-col>
<a-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
<a-form-item label="封面图:" name="image">
<xn-upload v-model:value="formData.image" uploadMode="image" />
<xn-upload
v-model:value="formData.image"
uploadMode="image"
uploadDynamicReturnUrlApi="/dev/file/uploadImageDynamicReturnUrl"
/>
</a-form-item>
</a-col>
<a-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
@@ -18,7 +18,11 @@
</a-col>
<a-col :xs="24" :sm="24" :md="24" :lg="6" :xl="6">
<a-form-item label="图片:" name="image">
<xn-upload v-model:value="formData.image" uploadMode="image" />
<xn-upload
v-model:value="formData.image"
uploadMode="image"
uploadDynamicReturnUrlApi="/dev/file/uploadImageDynamicReturnUrl"
/>
</a-form-item>
</a-col>
<a-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
@@ -24,7 +24,6 @@ import vip.xiaonuo.biz.modular.index.param.BizIndexNoticeIdParam;
import vip.xiaonuo.biz.modular.index.param.BizIndexNoticeListParam;
import vip.xiaonuo.biz.modular.index.param.BizIndexSlideshowListParam;
import vip.xiaonuo.biz.modular.index.result.BizIndexNoticeListResult;
import vip.xiaonuo.biz.modular.index.result.BizIndexSlideshowDetailResult;
import vip.xiaonuo.biz.modular.index.result.BizIndexSlideshowListResult;
import vip.xiaonuo.biz.modular.index.service.BizIndexService;
import vip.xiaonuo.biz.modular.notice.entity.BizNotice;
@@ -90,7 +89,7 @@ public class BizIndexController {
*/
@Operation(summary = "获取通知公告详情")
@GetMapping("/biz/index/notice/detail")
public CommonResult<BizIndexSlideshowDetailResult> noticeDetailById(@Valid BizIndexNoticeIdParam bizIndexNoticeIdParam) {
public CommonResult<BizNotice> noticeDetailById(@Valid BizIndexNoticeIdParam bizIndexNoticeIdParam) {
return CommonResult.data(bizIndexService.noticeDetailById(bizIndexNoticeIdParam));
}
}
@@ -1,70 +0,0 @@
/*
* Copyright [2022] [https://www.xiaonuo.vip]
*
* Snowy采用APACHE LICENSE 2.0开源协议,您在使用过程中,需要注意以下几点:
*
* 1.请不要删除和修改根目录下的LICENSE文件。
* 2.请不要删除和修改Snowy源码头部的版权声明。
* 3.本项目代码可免费商业使用,商业使用请保留源码和相关描述文件的项目出处,作者声明等。
* 4.分发源码时候,请注明软件出处 https://www.xiaonuo.vip
* 5.不可二次分发开源参与同类竞品,如有想法可联系团队xiaonuobase@qq.com商议合作。
* 6.若您的项目无法满足以上几点,需要更多功能代码,获取Snowy商业授权许可,请在官网购买授权,地址为 https://www.xiaonuo.vip
*/
package vip.xiaonuo.biz.modular.index.result;
import com.baomidou.mybatisplus.annotation.TableId;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
/**
* 轮播图结果
*
* @author yubaoshan
* @date 2024/07/13 00:31
*/
@Getter
@Setter
public class BizIndexSlideshowDetailResult {
/** 主键 */
@TableId
@Schema(description = "主键")
private String id;
/** 标题 */
@Schema(description = "标题")
private String title;
/** 封面图 */
@Schema(description = "封面图")
private String image;
/** 内容 */
@Schema(description = "内容")
private String content;
/** 摘要 */
@Schema(description = "摘要")
private String digest;
/** 类型 */
@Schema(description = "类型")
private String type;
/** 状态 */
@Schema(description = "状态")
private String status;
/** 备注 */
@Schema(description = "备注")
private String remark;
/** 排序 */
@Schema(description = "排序")
private Integer sortCode;
/** 扩展信息 */
@Schema(description = "扩展信息")
private String extJson;
}
@@ -18,7 +18,6 @@ import vip.xiaonuo.biz.modular.index.param.BizIndexNoticeIdParam;
import vip.xiaonuo.biz.modular.index.param.BizIndexNoticeListParam;
import vip.xiaonuo.biz.modular.index.param.BizIndexSlideshowListParam;
import vip.xiaonuo.biz.modular.index.result.BizIndexNoticeListResult;
import vip.xiaonuo.biz.modular.index.result.BizIndexSlideshowDetailResult;
import vip.xiaonuo.biz.modular.index.result.BizIndexSlideshowListResult;
import vip.xiaonuo.biz.modular.notice.entity.BizNotice;
@@ -62,5 +61,5 @@ public interface BizIndexService {
* @author yubaoshan
* @date 2024/07/13 00:31
*/
BizIndexSlideshowDetailResult noticeDetailById(BizIndexNoticeIdParam bizIndexNoticeIdParam);
BizNotice noticeDetailById(BizIndexNoticeIdParam bizIndexNoticeIdParam);
}
@@ -12,7 +12,6 @@
*/
package vip.xiaonuo.biz.modular.index.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
@@ -23,7 +22,6 @@ import vip.xiaonuo.biz.modular.index.param.BizIndexNoticeIdParam;
import vip.xiaonuo.biz.modular.index.param.BizIndexNoticeListParam;
import vip.xiaonuo.biz.modular.index.param.BizIndexSlideshowListParam;
import vip.xiaonuo.biz.modular.index.result.BizIndexNoticeListResult;
import vip.xiaonuo.biz.modular.index.result.BizIndexSlideshowDetailResult;
import vip.xiaonuo.biz.modular.index.result.BizIndexSlideshowListResult;
import vip.xiaonuo.biz.modular.index.service.BizIndexService;
import vip.xiaonuo.biz.modular.notice.entity.BizNotice;
@@ -76,9 +74,7 @@ public class BizIndexServiceImpl implements BizIndexService {
}
@Override
public BizIndexSlideshowDetailResult noticeDetailById(BizIndexNoticeIdParam bizIndexNoticeIdParam) {
BizIndexSlideshowDetailResult result = new BizIndexSlideshowDetailResult();
BeanUtil.copyProperties(bizNoticeService.getById(bizIndexNoticeIdParam.getId()), result);
return result;
public BizNotice noticeDetailById(BizIndexNoticeIdParam bizIndexNoticeIdParam) {
return bizNoticeService.getById(bizIndexNoticeIdParam.getId());
}
}
@@ -36,6 +36,7 @@ import vip.xiaonuo.dev.modular.file.service.DevFileService;
import javax.validation.Valid;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
/**
@@ -84,6 +85,66 @@ public class DevFileController {
return CommonResult.data(devFileService.uploadReturnUrl(devConfigApi.getValueByKey(SNOWY_SYS_DEFAULT_FILE_ENGINE_KEY), file));
}
/**
* 动态上传图片文件返回id(仅允许图片格式)
*
* @author xuyuxiang
* @date 2025/6/26 14:01
**/
@Operation(summary = "动态上传图片文件返回id")
@CommonLog("动态上传图片文件返回id")
@PostMapping("/dev/file/uploadImageDynamicReturnId")
public CommonResult<String> uploadImageDynamicReturnId(@RequestPart("file") MultipartFile file) {
List<String> allowedExt = Arrays.asList("jpg", "jpeg", "png", "gif", "bmp", "webp");
return CommonResult.data(devFileService.uploadReturnIdWithValidation(
devConfigApi.getValueByKey(SNOWY_SYS_DEFAULT_FILE_ENGINE_KEY), file, allowedExt));
}
/**
* 动态上传图片文件返回url(仅允许图片格式)
*
* @author xuyuxiang
* @date 2025/6/26 14:01
**/
@Operation(summary = "动态上传图片文件返回url")
@CommonLog("动态上传图片文件返回url")
@PostMapping("/dev/file/uploadImageDynamicReturnUrl")
public CommonResult<String> uploadImageDynamicReturnUrl(@RequestPart("file") MultipartFile file) {
List<String> allowedExt = Arrays.asList("jpg", "jpeg", "png", "gif", "bmp", "webp");
return CommonResult.data(devFileService.uploadReturnUrlWithValidation(
devConfigApi.getValueByKey(SNOWY_SYS_DEFAULT_FILE_ENGINE_KEY), file, allowedExt));
}
/**
* 动态上传文档文件返回id(仅允许文档格式)
*
* @author xuyuxiang
* @date 2025/6/26 14:01
**/
@Operation(summary = "动态上传文档文件返回id")
@CommonLog("动态上传文档文件返回id")
@PostMapping("/dev/file/uploadDocumentDynamicReturnId")
public CommonResult<String> uploadDocumentDynamicReturnId(@RequestPart("file") MultipartFile file) {
List<String> allowedExt = Arrays.asList("pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt");
return CommonResult.data(devFileService.uploadReturnIdWithValidation(
devConfigApi.getValueByKey(SNOWY_SYS_DEFAULT_FILE_ENGINE_KEY), file, allowedExt));
}
/**
* 动态上传文档文件返回url(仅允许文档格式)
*
* @author xuyuxiang
* @date 2025/6/26 14:01
**/
@Operation(summary = "动态上传文档文件返回url")
@CommonLog("动态上传文档文件返回url")
@PostMapping("/dev/file/uploadDocumentDynamicReturnUrl")
public CommonResult<String> uploadDocumentDynamicReturnUrl(@RequestPart("file") MultipartFile file) {
List<String> allowedExt = Arrays.asList("pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt");
return CommonResult.data(devFileService.uploadReturnUrlWithValidation(
devConfigApi.getValueByKey(SNOWY_SYS_DEFAULT_FILE_ENGINE_KEY), file, allowedExt));
}
/**
* 本地文件上传,返回文件id
*
@@ -42,6 +42,14 @@ public interface DevFileService extends IService<DevFile> {
**/
String uploadReturnId(String engine, MultipartFile file);
/**
* MultipartFile文件上传,返回文件id(带后缀校验)
*
* @author xuyuxiang
* @date 2025/6/26 15:53
**/
String uploadReturnIdWithValidation(String engine, MultipartFile file, List<String> allowedExtensions);
/**
* MultipartFile文件上传,返回文件Url
*
@@ -50,6 +58,14 @@ public interface DevFileService extends IService<DevFile> {
**/
String uploadReturnUrl(String engine, MultipartFile file);
/**
* MultipartFile文件上传,返回文件Url(带后缀校验)
*
* @author xuyuxiang
* @date 2025/6/26 15:53
**/
String uploadReturnUrlWithValidation(String engine, MultipartFile file, List<String> allowedExtensions);
/**
* 文件分页列表接口
*
@@ -13,6 +13,7 @@
package vip.xiaonuo.dev.modular.file.service.impl;
import cn.hutool.core.collection.CollStreamUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.img.ImgUtil;
@@ -81,11 +82,23 @@ public class DevFileServiceImpl extends ServiceImpl<DevFileMapper, DevFile> impl
return this.storageFile(engine, file, true, true);
}
@Override
public String uploadReturnIdWithValidation(String engine, MultipartFile file, List<String> allowedExtensions) {
validateFileExtension(file, allowedExtensions);
return this.storageFile(engine, file, true, true);
}
@Override
public String uploadReturnUrl(String engine, MultipartFile file) {
return this.storageFile(engine, file, false, false);
}
@Override
public String uploadReturnUrlWithValidation(String engine, MultipartFile file, List<String> allowedExtensions) {
validateFileExtension(file, allowedExtensions);
return this.storageFile(engine, file, false, false);
}
@Override
public Page<DevFile> page(DevFilePageParam devFilePageParam) {
QueryWrapper<DevFile> queryWrapper = new QueryWrapper<DevFile>().checkSqlInjection();
@@ -421,4 +434,33 @@ public class DevFileServiceImpl extends ServiceImpl<DevFileMapper, DevFile> impl
return false;
}
}
/**
* 验证文件后缀名
*
* @param file 上传的文件
* @param allowedExtensions 允许的后缀列表(不含点号,如:jpg, png)
*/
private void validateFileExtension(MultipartFile file, List<String> allowedExtensions) {
if (file == null || file.isEmpty()) {
throw new CommonException("上传文件不能为空");
}
String originalFileName = file.getOriginalFilename();
if (StrUtil.isEmpty(originalFileName)) {
throw new CommonException("文件名不能为空");
}
// 获取文件后缀(小写)
String fileSuffix = FileUtil.getSuffix(originalFileName).toLowerCase();
if (StrUtil.isEmpty(fileSuffix)) {
throw new CommonException("文件必须有后缀名");
}
// 校验后缀是否在白名单中
if (CollUtil.isEmpty(allowedExtensions) || !allowedExtensions.contains(fileSuffix)) {
throw new CommonException("不允许上传该文件类型,仅支持:{}",
CollUtil.isEmpty(allowedExtensions) ? "无" : String.join("、", allowedExtensions));
}
}
}
@@ -1111,7 +1111,17 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
public String updateAvatar(MultipartFile file) {
SysUser sysUser = this.queryEntity(StpUtil.getLoginIdAsString());
try {
String suffix = Objects.requireNonNull(FileUtil.getSuffix(file.getOriginalFilename())).toLowerCase();
// 验证文件后缀(只允许图片格式)
String suffix = FileUtil.getSuffix(file.getOriginalFilename());
if (StrUtil.isEmpty(suffix)) {
throw new CommonException("文件必须有后缀名");
}
suffix = suffix.toLowerCase();
List<String> allowedExt = Arrays.asList("jpg", "jpeg", "png", "gif", "bmp", "webp");
if (!allowedExt.contains(suffix)) {
throw new CommonException("头像仅支持图片格式:jpg、jpeg、png、gif、bmp、webp");
}
BufferedImage image = ImgUtil.toImage(file.getBytes());
String base64;
if(image.getWidth() <= 200 && image.getHeight() <= 200) {