Commit 21f4f0c1 by tangyi

优化

parent fd8c285d
Version v3.7.0 (2020-03-39)
Version v3.7.0 (2020-04-05)
--------------------------
改进:
* 优化附件上传模块,支持存储方式支持本地、fastDfs、七牛云
* 优化题目编辑页面,统一采用富文本输入,支持数学公式
Version v3.7.0 (2020-03-31)
--------------------------
改进:
......
......@@ -45,7 +45,7 @@
- 构建工具:`Maven`
- 后台 API 文档:`Swagger`
- 消息队列:`RabbitMQ`
- 文件系统:`七牛云``FastDfs`
- 文件系统:`本地目录``七牛云``FastDfs`
- 缓存:`Redis`
- 前端:`vue`
- 小程序:`wepy`
......@@ -94,7 +94,7 @@
- 知识库:知识库增删改查、上传附件
附件管理:项目的所有附件存储在`fastDfs`里,提供统一的管理入口
- 附件列表:管理所有附件,如用户头像、考试附件、知识库附件等
- 附件列表:管理所有附件,如用户头像、考试附件、知识库附件等,存储方式支持服务器本地目录、`fastDfs`,七牛云
个人管理:管理个人资料和修改密码
- 个人资料:姓名、头像等基本信息的修改
......
......@@ -14,11 +14,6 @@ public class SysConfigDto implements Serializable {
private static final long serialVersionUID = 1L;
/**
* fastDfs服务器的HTTP地址
*/
private String fdfsHttpHost;
/**
* 上传地址
*/
private String uploadUrl;
......
......@@ -54,4 +54,19 @@ public class SysProperties {
* 二维码生成链接
*/
private String qrCodeUrl;
/**
* 上传类型,1:本地目录,2:fastDfs,3:七牛云
*/
private String attachUploadType;
/**
* 附件上传目录
*/
private String attachPath;
/**
* 支持预览的附件后缀名,多个用逗号隔开,如:png,jpeg
*/
private String canPreview;
}
......@@ -3,17 +3,22 @@ package com.github.tangyi.common.config.handler;
import com.github.tangyi.common.core.constant.ApiMsg;
import com.github.tangyi.common.core.exceptions.CommonException;
import com.github.tangyi.common.core.model.ResponseBean;
import com.github.tangyi.common.core.utils.JsonMapper;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageConversionException;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
......@@ -22,8 +27,8 @@ import java.util.stream.Collectors;
* @author tangyi
* @date 2019/05/25 15:36
*/
@ControllerAdvice
public class CustomGlobalExceptionHandler extends ResponseEntityExceptionHandler {
@RestControllerAdvice
public class CustomGlobalExceptionHandler {
/**
* 处理参数校验异常
......@@ -31,13 +36,12 @@ public class CustomGlobalExceptionHandler extends ResponseEntityExceptionHandler
* @param ex ex
* @param headers headers
* @param status status
* @param request request
* @return ResponseEntity
*/
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
HttpHeaders headers,
HttpStatus status, WebRequest request) {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Object> validationBodyException(MethodArgumentNotValidException ex,
HttpHeaders headers,
HttpStatus status) {
// 获取所有异常信息
List<String> errors = ex.getBindingResult()
.getFieldErrors()
......@@ -49,6 +53,18 @@ public class CustomGlobalExceptionHandler extends ResponseEntityExceptionHandler
}
/**
* 参数类型转换错误
*
* @param exception 错误
* @return 错误信息 
*/
@ExceptionHandler(HttpMessageConversionException.class)
public ResponseEntity<ResponseBean<String>> parameterTypeException(HttpMessageConversionException exception) {
ResponseBean<String> responseBean = new ResponseBean<>(exception.getMessage(), ApiMsg.KEY_PARAM_VALIDATE, ApiMsg.ERROR);
return new ResponseEntity<>(responseBean, HttpStatus.OK);
}
/**
* 处理CommonException
*
* @param e e
......@@ -60,9 +76,40 @@ public class CustomGlobalExceptionHandler extends ResponseEntityExceptionHandler
return new ResponseEntity<>(responseBean, HttpStatus.OK);
}
/**
* 捕获@Validate校验抛出的异常
*
* @param e e
* @return ResponseEntity
*/
@ExceptionHandler(BindException.class)
public ResponseEntity<Object> validExceptionHandler(BindException e) {
Exception ex = parseBindingResult(e.getBindingResult());
ResponseBean<String> responseBean = new ResponseBean<>(ex.getMessage(), ApiMsg.KEY_PARAM_VALIDATE, ApiMsg.ERROR);
return new ResponseEntity<>(responseBean, HttpStatus.OK);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ResponseBean<String>> handleException(Exception e) {
ResponseBean<String> responseBean = new ResponseBean<>(e.getMessage(), ApiMsg.KEY_ERROR, ApiMsg.ERROR);
return new ResponseEntity<>(responseBean, HttpStatus.OK);
}
/**
* 提取Validator产生的异常错误
*
* @param bindingResult bindingResult
* @return Exception
*/
private Exception parseBindingResult(BindingResult bindingResult) {
Map<String, String> errorMsgs = new HashMap<>();
for (FieldError error : bindingResult.getFieldErrors()) {
errorMsgs.put(error.getField(), error.getDefaultMessage());
}
if (errorMsgs.isEmpty()) {
return new CommonException(ApiMsg.KEY_PARAM_VALIDATE + "");
} else {
return new CommonException(JsonMapper.toJsonString(errorMsgs));
}
}
}
\ No newline at end of file
......@@ -101,6 +101,11 @@ public class ApiMsg {
public static final int KEY_AUTHENTICATION = 405;
/**
* 参数校验
*/
public static final int KEY_PARAM_VALIDATE = 406;
/**
* code和提示内容的对应关系
*/
private static final Map<Integer, String> CODE_MAP = new HashMap<>();
......@@ -130,6 +135,7 @@ public class ApiMsg {
KEY_MAP.put(KEY_VALIDATE_CODE, "VALIDATE CODE");
KEY_MAP.put(KEY_TOKEN, "TOKEN");
KEY_MAP.put(KEY_ACCESS, "ACCESS");
KEY_MAP.put(KEY_PARAM_VALIDATE, "PARAM_VALIDATE");
}
public static String code2Msg(int codeKey, int msgKey) {
......
package com.github.tangyi.common.core.utils;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
/**
* 文件工具类
*
* @author tangyi
* @date 2018/10/30 22:05
*/
@Slf4j
public class FileUtil {
/**
......@@ -25,4 +30,108 @@ public class FileUtil {
}
return "";
}
/**
* 创建目录
*
* @param descDirName descDirName
* @return String
*/
public static boolean createDirectory(String descDirName) {
String descDirNames = descDirName;
if (!descDirNames.endsWith(File.separator)) {
descDirNames = descDirNames + File.separator;
}
File descDir = new File(descDirNames);
if (descDir.exists()) {
log.debug("dir " + descDirNames + " already exits!");
return false;
}
if (descDir.mkdirs()) {
log.debug("dir " + descDirNames + " create success!");
return true;
} else {
log.debug("dir " + descDirNames + " create failure!");
return false;
}
}
/**
*
* 删除目录及目录下的文件
*
* @param dirName 被删除的目录所在的文件路径
* @return 如果目录删除成功,则返回true,否则返回false
*/
public static boolean deleteDirectory(String dirName) {
String dirNames = dirName;
if (!dirNames.endsWith(File.separator)) {
dirNames = dirNames + File.separator;
}
File dirFile = new File(dirNames);
if (!dirFile.exists() || !dirFile.isDirectory()) {
log.debug(dirNames + " dir not exists!");
return true;
}
boolean flag = true;
// 列出全部文件及子目录
File[] files = dirFile.listFiles();
if (files != null) {
for (File file : files) {
// 删除子文件
if (file.isFile()) {
flag = FileUtil.deleteFile(file.getAbsolutePath());
// 如果删除文件失败,则退出循环
if (!flag) {
break;
}
}
// 删除子目录
else if (file.isDirectory()) {
flag = FileUtil.deleteDirectory(file
.getAbsolutePath());
// 如果删除子目录失败,则退出循环
if (!flag) {
break;
}
}
}
}
if (!flag) {
log.debug("delete dir failure!");
return false;
}
// 删除当前目录
if (dirFile.delete()) {
log.debug("delete dir " + dirName + " success!");
return true;
} else {
log.debug("delete dir " + dirName + " failure!");
return false;
}
}
/**
*
* 删除单个文件
*
* @param fileName 被删除的文件名
* @return 如果删除成功,则返回true,否则返回false
*/
public static boolean deleteFile(String fileName) {
File file = new File(fileName);
if (file.exists() && file.isFile()) {
if (file.delete()) {
log.debug("delete file " + fileName + " success!");
return true;
} else {
log.debug("delete file " + fileName + " failure!");
return false;
}
} else {
log.debug(fileName + " not exists!");
return true;
}
}
}
......@@ -27,7 +27,6 @@ import java.util.Set;
* @author tangyi
* @date 2018-01-04 10:34
*/
@Deprecated
@Slf4j
@AllArgsConstructor
@Service
......
......@@ -2,6 +2,7 @@ package com.github.tangyi.oss.service;
import com.github.tangyi.common.core.constant.CommonConstant;
import com.github.tangyi.common.core.utils.JsonMapper;
import com.github.tangyi.common.core.utils.SpringContextHolder;
import com.github.tangyi.oss.config.QiNiuConfig;
import com.github.tangyi.oss.exceptions.OssException;
import com.qiniu.common.QiniuException;
......@@ -14,10 +15,7 @@ import com.qiniu.storage.model.DefaultPutRet;
import com.qiniu.util.Auth;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
......@@ -27,10 +25,7 @@ import java.net.URLEncoder;
* @date 2019/12/8 20:25
*/
@Slf4j
@Service
public class QiNiuService {
private final QiNiuConfig qiNiuConfig;
public class QiNiuUtil {
private Auth auth;
......@@ -38,18 +33,25 @@ public class QiNiuService {
private BucketManager bucketManager;
@Autowired
public QiNiuService(QiNiuConfig qiNiuConfig) {
this.qiNiuConfig = qiNiuConfig;
private QiNiuConfig qiNiuConfig;
private static QiNiuUtil instance;
public synchronized static QiNiuUtil getInstance() {
if (instance == null) {
instance = new QiNiuUtil();
}
return instance;
}
@PostConstruct
public void init() {
public QiNiuUtil() {
qiNiuConfig = SpringContextHolder.getApplicationContext().getBean(QiNiuConfig.class);
if (StringUtils.isNotBlank(qiNiuConfig.getAccessKey()) && StringUtils.isNotBlank(qiNiuConfig.getSecretKey())) {
auth = Auth.create(qiNiuConfig.getAccessKey(), qiNiuConfig.getSecretKey());
instance = new QiNiuUtil();
instance.auth = Auth.create(qiNiuConfig.getAccessKey(), qiNiuConfig.getSecretKey());
Configuration config = new Configuration(Region.region2());
uploadManager = new UploadManager(config);
bucketManager = new BucketManager(auth, config);
instance.uploadManager = new UploadManager(config);
instance.bucketManager = new BucketManager(instance.auth, config);
}
}
......@@ -59,7 +61,7 @@ public class QiNiuService {
* @return String
*/
public String getQiNiuToken() {
return auth.uploadToken(qiNiuConfig.getBucket());
return auth.uploadToken(getInstance().qiNiuConfig.getBucket());
}
/**
......
......@@ -112,6 +112,9 @@ sys:
defaultAvatar: /static/img/avatar/
key: '1234567887654321'
cacheExpire: 86400 # 缓存失效时间,单位秒,默认一天
attachtUploadType: 1 # 上传类型,1:本地目录,2:fastDfs,3:七牛云
attachPath: ${ATTACH_PATH:C:/attach} # 附件上传目录
canPreview: jpg,png,jpeg,gif # 支持预览的格式
# feign相关配置
feign:
......@@ -158,6 +161,7 @@ ignore:
- /v1/menu/anonymousUser/**
- /v1/code/**
- /v1/attachment/download
- /v1/attachment/preview
- /v1/log/**
- /authentication/**
- /v1/authentication/**
......
DB_AUTH=dev_microservice_auth
DB_USER=dev_microservice_user
DB_EXAM=dev_microservice_exam
DB_GATEWAY=dev_microservice_gateway
# 租户标识,默认gitee
TENANT_CODE=gitee
# 网关token转换
GATEWAY_TOKEN_TRANSFER=false
# 网关secret
GATEWAY_SECRET=15521089185
# 环境配置
SPRING_PROFILES_ACTIVE=native
# consul配置
CONSUL_HOST=localhost
CONSUL_PORT=8500
# rabbitMq配置
RABBIT_HOST=localhost
RABBIT_PORT=5672
RABBITMQ_DEFAULT_USER=guest
RABBITMQ_DEFAULT_PASS=guest
# Redis配置
REDIS_HOST=localhost
# 数据库配置
MYSQL_HOST=118.25.138.130
MYSQL_USERNAME=root
MYSQL_PASSWORD=15521089185
# ID生成配置
CLUSTER_WORK_ID=1
CLUSTER_DATA_CENTER_ID=1
\ No newline at end of file
......@@ -97,6 +97,8 @@ services:
- redis
ports:
- "9181:9181"
volumes:
- ./logs/config-service:/logs/config-service
networks:
- net
......
......@@ -10,6 +10,8 @@ services:
restart: always
ports:
- "9180:9180"
volumes:
- ./logs/gateway-service:/logs/gateway-service
networks:
- net
......@@ -23,6 +25,8 @@ services:
restart: always
ports:
- "9182:9182"
volumes:
- ./logs/auth-service:/logs/auth-service
networks:
- net
......@@ -36,6 +40,9 @@ services:
restart: always
ports:
- "9183:9183"
volumes:
- ./logs/user-service:/logs/user-service
- ./attach:/attach
networks:
- net
......@@ -49,6 +56,8 @@ services:
restart: always
ports:
- "9184:9184"
volumes:
- ./logs/exam-service:/logs/exam-service
networks:
- net
......@@ -62,6 +71,8 @@ services:
restart: always
ports:
- "9185:9185"
volumes:
- ./logs/msc-service:/logs/msc-service
networks:
- net
......@@ -75,6 +86,8 @@ services:
restart: always
ports:
- "9186:9186"
volumes:
- ./logs/monitor-service:/logs/monitor-service
networks:
- net
......
......@@ -83,4 +83,7 @@ LOGSTASH_HOST=localhost:5044
# 微信配置
WX_APP_ID=test
WX_APP_SECRET=test
WX_GRANT_TYPE=authorization_code
\ No newline at end of file
WX_GRANT_TYPE=authorization_code
# 附件上传配置
ATTACH_PATH=/attach
\ No newline at end of file
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
......@@ -32,6 +33,6 @@ CREATE TABLE `oauth_client_details` (
-- ----------------------------
-- Records of oauth_client_details
-- ----------------------------
INSERT INTO `oauth_client_details` VALUES (607150228717572096, 'web_app', NULL, 'spring-microservice-exam-secret', '$2a$10$IWLD8r7e0rKMW.ZhGsGCUO./MZUNDKudIswSToa9puXJwty.h59.u', 'read,write', 'password,authorization_code,refresh_token,implicit', NULL, NULL, '3600', '86400', NULL, NULL, 'admin', '2019-03-30 23:43:07', 'admin', '2019-12-24 21:56:19', 0, 'EXAM', 'gitee');
INSERT INTO `oauth_client_details` VALUES (607150228717572096, 'web_app', NULL, 'spring-microservice-exam-secret', '$2a$10$IWLD8r7e0rKMW.ZhGsGCUO./MZUNDKudIswSToa9puXJwty.h59.u', 'read,write', 'password,authorization_code,refresh_token,implicit', NULL, NULL, '86400', '86400', NULL, NULL, 'admin', '2019-03-30 23:43:07', 'admin', '2020-03-28 20:01:31', 0, 'EXAM', 'gitee');
SET FOREIGN_KEY_CHECKS = 1;
ALTER TABLE `microservice-user`.`sys_user`
ADD COLUMN `signature` varchar(255) NULL COMMENT '个性签名' AFTER `wechat`;
\ No newline at end of file
ADD COLUMN `signature` varchar(255) NULL COMMENT '个性签名' AFTER `wechat`;
ALTER TABLE `microservice-user`.`sys_attachment`
ADD COLUMN `attach_type` varchar(128) NULL COMMENT '附件类型' AFTER `attach_name`,
ADD COLUMN `upload_type` tinyint(4) NULL COMMENT '上传类型' AFTER `preview_url`;
\ No newline at end of file
{
"name": "spring-microservice-exam-ui",
"version": "3.5.0",
"version": "3.7.0",
"description": "在线考试",
"author": "tangyi <1633736729@qq.com>",
"private": true,
......
......@@ -24,9 +24,9 @@ export function getObj (id) {
})
}
export function preview (id) {
export function canPreview (id) {
return request({
url: baseAttachmentUrl + id + '/preview',
url: baseAttachmentUrl + id + '/canPreview',
method: 'get'
})
}
......
<template>
<el-form ref="dataSubjectForm" :rules="subjectRules" :model="subjectInfo" :label-position="labelPosition" label-width="100px">
<el-row>
<el-col :span="10">
<el-col :span="20" :offset="2">
<div class="subject-info">
<el-row>
<el-col :span="12">
......@@ -17,17 +17,17 @@
</el-row>
<el-row>
<el-col :span="24">
<el-form-item :label="$t('table.subjectName')" prop="subjectName">
<el-input v-model="subjectInfo.subjectName" @focus="updateTinymceContent(subjectInfo.subjectName, tinymceEdit.subjectName)"/>
<el-form-item :label="$t('table.subject.answer')" prop="answer">
<el-radio-group v-model="subjectInfo.answer.answer">
<el-radio v-for="(option) in options" :label="option.optionName" :key="option.optionName">{{ option.optionName }}</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item :label="$t('table.subject.answer')" prop="answer">
<el-radio-group v-model="subjectInfo.answer.answer">
<el-radio v-for="(option) in options" :label="option.optionName" :key="option.optionName">{{ option.optionName }}</el-radio>
</el-radio-group>
<el-form-item :label="$t('table.subjectName')" prop="subjectName">
<tinymce ref="subjectNameEditor" :height="60" v-model="subjectInfo.subjectName"/>
</el-form-item>
</el-col>
</el-row>
......@@ -35,15 +35,20 @@
<el-col :span="24">
<el-divider>选项列表</el-divider>
<el-form-item v-for="(option, index) in options" :label="option.optionName" :key="option.optionName"
:prop="'options.' + index + '.optionContent'">
:prop="'options.' + index + '.optionContent'" label-width="15px">
<el-row :gutter="5">
<el-col :span="4">
<el-col :span="2">
<el-input v-model="option.optionName"/>
</el-col>
<el-col :span="18">
<el-input v-model="option.optionContent" @input="updateTinymceContent(option.optionContent, index, '1')">
<el-button slot="append" @click.prevent="removeOption(option)">删除</el-button>
</el-input>
<el-col :span="21">
<el-row :gutter="5">
<el-col :span="23">
<tinymce :height="60" v-model="option.optionContent"/>
</el-col>
<el-col :span="1">
<el-button @click.prevent="removeOption(option)">删除</el-button>
</el-col>
</el-row>
</el-col>
</el-row>
</el-form-item>
......@@ -53,17 +58,12 @@
<el-row>
<el-col :span="24">
<el-form-item :label="$t('table.subject.analysis')" prop="analysis" class="analysis-form-item">
<el-input v-model="subjectInfo.analysis" @input="updateTinymceContent(subjectInfo.analysis, tinymceEdit.analysis)"/>
<tinymce ref="analysisEditor" :height="60" v-model="subjectInfo.analysis"/>
</el-form-item>
</el-col>
</el-row>
</div>
</el-col>
<el-col :span="14">
<div class="subject-tinymce">
<tinymce ref="choicesEditor" :height="350" v-model="choicesContent" @hasClick="hasClick"/>
</div>
</el-col>
</el-row>
</el-form>
</template>
......@@ -124,36 +124,11 @@ export default {
score: [{ required: true, message: '请输入题目分值', trigger: 'change' }],
answer: [{ required: true, message: '请输入答案', trigger: 'change' }]
},
tinymce: {
type: 1, // 类型 0:题目名称,1:选项
dialogTinymceVisible: false,
tempValue: '',
currentEdit: -1
},
// 编辑对象
tinymceEdit: {
subjectName: -1,
answer: 4,
analysis: 5
},
options: [],
optionCollapseActives: ['1'],
analysisCollapseActives: ['2']
}
},
watch: {
// 监听富文本编辑器的输入
choicesContent: {
handler: function (choicesContent) {
if (isNotEmpty(this.$refs.choicesEditor)) {
if (this.editType === 1 && this.$refs.choicesEditor.getHasClick()) {
this.saveTinymceContent(choicesContent)
}
}
},
immediate: true
}
},
methods: {
initDefaultOptions () {
this.options = [
......@@ -162,6 +137,7 @@ export default {
{ subjectChoicesId: '', optionName: 'C', optionContent: '' },
{ subjectChoicesId: '', optionName: 'D', optionContent: '' }
]
this.subjectInfo.answer.answer = 'A'
},
setSubjectInfo (subject) {
this.subjectInfo = subject
......@@ -181,36 +157,6 @@ export default {
getChoicesContent () {
return this.choicesContent
},
// 绑定富文本的内容
updateTinymceContent (content, currentEdit, type) {
// 重置富文本
this.choicesContent = ''
// 绑定当前编辑的对象
this.tinymce.currentEdit = currentEdit
this.tinymce.type = type
// 选择题
this.$refs.choicesEditor.setContent(content || '')
this.editType = 0
this.$refs.choicesEditor.setHashClick(false)
},
// 保存题目时绑定富文本的内容到subjectInfo
saveTinymceContent (content) {
if (this.tinymce.type !== '1') {
switch (this.tinymce.currentEdit) {
case this.tinymceEdit.subjectName:
this.subjectInfo.subjectName = content
break
case this.tinymceEdit.answer:
this.subjectInfo.answer.answer = content
break
case this.tinymceEdit.analysis:
this.subjectInfo.analysis = content
break
}
} else {
this.options[this.tinymce.currentEdit].optionContent = content
}
},
// 表单校验
validate () {
let valid = false
......@@ -251,6 +197,8 @@ export default {
this.subjectInfo.score = score
}
this.initDefaultOptions()
this.$refs['subjectNameEditor'].setContent('')
this.$refs['analysisEditor'].setContent('')
},
addOption () {
// 校验
......@@ -270,10 +218,6 @@ export default {
if (index !== -1) {
this.options.splice(index, 1)
}
},
// 点击事件回调
hasClick (hasClick) {
this.editType = 1
}
}
}
......
<template>
<el-form ref="dataSubjectForm" :rules="subjectRules" :model="subjectInfo" :label-position="labelPosition" label-width="100px">
<el-row>
<el-col :span="10">
<el-col :span="20" :offset="2">
<div class="subject-info">
<el-row>
<el-col :span="12">
......@@ -17,34 +17,29 @@
</el-row>
<el-row>
<el-col :span="24">
<el-form-item :label="$t('table.subjectName')" prop="subjectName">
<el-input v-model="subjectInfo.subjectName" @focus="updateTinymceContent(subjectInfo.subjectName, tinymceEdit.subjectName)"/>
<el-form-item :label="$t('table.subject.answer')" prop="answer">
<el-radio-group v-model="subjectInfo.answer.answer">
<el-radio v-for="(option) in options" :label="option.optionName" :key="option.optionName">{{ option.optionName }}</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item :label="$t('table.subject.answer')" prop="answer">
<el-radio-group v-model="subjectInfo.answer.answer">
<el-radio v-for="(option) in options" :label="option.optionName" :key="option.optionName">{{ option.optionName }}</el-radio>
</el-radio-group>
<el-form-item :label="$t('table.subjectName')" prop="subjectName">
<tinymce ref="subjectNameEditor" :height="60" v-model="subjectInfo.subjectName"/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item :label="$t('table.subject.analysis')" prop="analysis" class="analysis-form-item">
<el-input v-model="subjectInfo.analysis" @input="updateTinymceContent(subjectInfo.analysis, tinymceEdit.analysis)"/>
<tinymce ref="analysisEditor" :height="60" v-model="subjectInfo.analysis"/>
</el-form-item>
</el-col>
</el-row>
</div>
</el-col>
<el-col :span="14">
<div class="subject-tinymce">
<tinymce ref="choicesEditor" :height="350" v-model="choicesContent" @hasClick="hasClick"/>
</div>
</el-col>
</el-row>
</el-form>
</template>
......@@ -102,40 +97,16 @@ export default {
score: [{ required: true, message: '请输入题目分值', trigger: 'change' }],
answer: [{ required: true, message: '请输入答案', trigger: 'change' }]
},
tinymce: {
type: 1, // 类型 0:题目名称,1:选项
dialogTinymceVisible: false,
tempValue: '',
currentEdit: -1
},
// 编辑对象
tinymceEdit: {
subjectName: -1,
answer: 4,
analysis: 5
},
options: []
}
},
watch: {
// 监听富文本编辑器的输入
choicesContent: {
handler: function (choicesContent) {
if (isNotEmpty(this.$refs.choicesEditor)) {
if (this.editType === 1 && this.$refs.choicesEditor.getHasClick()) {
this.saveTinymceContent(choicesContent)
}
}
},
immediate: true
}
},
methods: {
initDefaultOptions () {
this.options = [
{ subjectChoicesId: '', optionName: '正确', optionContent: '正确' },
{ subjectChoicesId: '', optionName: '错误', optionContent: '错误' }
]
this.subjectInfo.answer.answer = '正确'
},
setSubjectInfo (subject) {
this.subjectInfo = subject
......@@ -144,30 +115,6 @@ export default {
getSubjectInfo () {
return this.subjectInfo
},
// 绑定富文本的内容
updateTinymceContent (content, currentEdit, type) {
// 重置富文本
this.choicesContent = ''
// 绑定当前编辑的对象
this.tinymce.currentEdit = currentEdit
this.tinymce.type = type
this.$refs.choicesEditor.setContent(content || '')
this.editType = 0
this.$refs.choicesEditor.setHashClick(false)
},
saveTinymceContent (content) {
switch (this.tinymce.currentEdit) {
case this.tinymceEdit.subjectName:
this.subjectInfo.subjectName = content
break
case this.tinymceEdit.answer:
this.subjectInfo.answer.answer = content
break
case this.tinymceEdit.analysis:
this.subjectInfo.analysis = content
break
}
},
// 表单校验
validate () {
let valid = false
......@@ -202,10 +149,8 @@ export default {
this.subjectInfo.score = score
}
this.initDefaultOptions()
},
// 点击事件回调
hasClick (hasClick) {
this.editType = 1
this.$refs['subjectNameEditor'].setContent('')
this.$refs['analysisEditor'].setContent('')
}
}
}
......
<template>
<el-form ref="dataSubjectForm" :rules="subjectRules" :model="subjectInfo" :label-position="labelPosition" label-width="100px">
<el-row>
<el-col :span="10">
<el-col :span="20" :offset="2">
<div class="subject-info" v-if="subjectInfo.type === 3">
<el-row>
<el-col :span="12">
......@@ -18,64 +17,53 @@
</el-row>
<el-row>
<el-col :span="24">
<el-form-item :label="$t('table.subjectName')" prop="subjectName">
<el-input v-model="subjectInfo.subjectName"/>
<el-form-item :label="$t('table.subject.answer')" prop="answer">
<el-checkbox-group v-model="multipleAnswers">
<el-checkbox v-for="(option) in options" :label="option.optionName" :key="option.optionName" :name="option.optionName">{{ option.optionName }}</el-checkbox>
</el-checkbox-group>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item :label="$t('table.subject.answer')" prop="answer">
<el-checkbox-group v-model="multipleAnswers">
<el-checkbox v-for="(option) in options" :label="option.optionName" :key="option.optionName" :name="option.optionName">{{ option.optionName }}</el-checkbox>
</el-checkbox-group>
<el-form-item :label="$t('table.subjectName')" prop="subjectName">
<tinymce ref="subjectNameEditor" :height="60" v-model="subjectInfo.subjectName"/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-collapse v-model="optionCollapseActives">
<el-collapse-item title="选项列表" name="1">
<el-row class="collapse-top">
<el-col :span="24">
<el-form-item v-for="(option, index) in options" :label="option.optionName" :key="option.optionName"
:prop="'options.' + index + '.optionContent'">
<el-row :gutter="5">
<el-col :span="4">
<el-input v-model="option.optionName"/>
</el-col>
<el-col :span="18">
<el-input v-model="option.optionContent" @input="updateTinymceContent(option.optionContent, index, '1')">
<el-button slot="append" @click.prevent="removeOption(option)">删除</el-button>
</el-input>
</el-col>
</el-row>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-button type="success" @click.prevent="addOption()" style="display:block;margin:0 auto">新增选项</el-button>
</el-col>
</el-row>
</el-collapse-item>
</el-collapse>
<el-divider>选项列表</el-divider>
<el-form-item v-for="(option, index) in options" :label="option.optionName" :key="option.optionName"
:prop="'options.' + index + '.optionContent'" label-width="15px">
<el-row :gutter="5">
<el-col :span="2">
<el-input v-model="option.optionName"/>
</el-col>
<el-col :span="21">
<el-row :gutter="5">
<el-col :span="23">
<tinymce :height="60" v-model="option.optionContent"/>
</el-col>
<el-col :span="1">
<el-button @click.prevent="removeOption(option)">删除</el-button>
</el-col>
</el-row>
</el-col>
</el-row>
</el-form-item>
<el-button type="success" @click.prevent="addOption()" style="display:block;margin:0 auto">新增选项</el-button>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item :label="$t('table.subject.analysis')" prop="analysis" class="analysis-form-item">
<el-input v-model="subjectInfo.analysis" @input="updateTinymceContent(subjectInfo.analysis, tinymceEdit.analysis)"/>
<tinymce ref="analysisEditor" :height="60" v-model="subjectInfo.analysis"/>
</el-form-item>
</el-col>
</el-row>
</div>
</el-col>
<el-col :span="14">
<div class="subject-tinymce">
<tinymce ref="choicesEditor" :height="350" v-model="choicesContent" @hasClick="hasClick"/>
</div>
</el-col>
</el-row>
</el-form>
</template>
......@@ -127,37 +115,12 @@ export default {
score: [{ required: true, message: '请输入题目分值', trigger: 'change' }],
answer: [{ required: true, message: '请输入答案', trigger: 'change' }]
},
tinymce: {
type: 1, // 类型 0:题目名称,1:选项
dialogTinymceVisible: false,
tempValue: '',
currentEdit: -1
},
// 编辑对象
tinymceEdit: {
subjectName: -1,
answer: 4,
analysis: 5
},
options: [],
optionCollapseActives: ['1'],
analysisCollapseActives: ['2'],
multipleAnswers: []
}
},
watch: {
// 监听富文本编辑器的输入
choicesContent: {
handler: function (choicesContent) {
if (isNotEmpty(this.$refs.choicesEditor)) {
if (this.editType === 1 && this.$refs.choicesEditor.getHasClick()) {
this.saveTinymceContent(choicesContent)
}
}
},
immediate: true
}
},
methods: {
initDefaultOptions () {
this.options = [
......@@ -166,6 +129,7 @@ export default {
{ subjectChoicesId: '', optionName: 'C', optionContent: '' },
{ subjectChoicesId: '', optionName: 'D', optionContent: '' }
]
this.subjectInfo.answer.answer = 'A'
},
setSubjectInfo (subject) {
this.subjectInfo = subject
......@@ -187,36 +151,6 @@ export default {
getChoicesContent () {
return this.choicesContent
},
// 绑定富文本的内容
updateTinymceContent (content, currentEdit, type) {
// 重置富文本
this.choicesContent = ''
// 绑定当前编辑的对象
this.tinymce.currentEdit = currentEdit
this.tinymce.type = type
// 选择题
this.$refs.choicesEditor.setContent(content || '')
this.editType = 0
this.$refs.choicesEditor.setHashClick(false)
},
// 保存题目时绑定富文本的内容到subjectInfo
saveTinymceContent (content) {
if (this.tinymce.type !== '1') {
switch (this.tinymce.currentEdit) {
case this.tinymceEdit.subjectName:
this.subjectInfo.subjectName = content
break
case this.tinymceEdit.answer:
this.subjectInfo.answer.answer = content
break
case this.tinymceEdit.analysis:
this.subjectInfo.analysis = content
break
}
} else {
this.options[this.tinymce.currentEdit].optionContent = content
}
},
// 表单校验
validate () {
let valid = false
......@@ -257,6 +191,8 @@ export default {
this.subjectInfo.score = score
}
this.initDefaultOptions()
this.$refs['subjectNameEditor'].setContent('')
this.$refs['analysisEditor'].setContent('')
},
addOption () {
// 校验
......@@ -277,10 +213,6 @@ export default {
this.options.splice(index, 1)
}
},
// 点击事件回调
hasClick (hasClick) {
this.editType = 1
},
initMultipleAnswers () {
if (isNotEmpty(this.subjectInfo.answer)) {
this.multipleAnswers = this.subjectInfo.answer.answer.split(',')
......
<template>
<el-form ref="dataSubjectForm" :rules="subjectRules" :model="subjectInfo" :label-position="labelPosition" label-width="100px">
<el-row>
<el-col :span="10">
<el-col :span="20" :offset="2">
<el-row>
<el-col :span="12">
<el-form-item :label="$t('table.subject.score')" prop="score">
......@@ -17,30 +17,25 @@
<el-row>
<el-col :span="24">
<el-form-item :label="$t('table.subjectName')" prop="subjectName">
<el-input v-model="subjectInfo.subjectName" @focus="updateTinymceContent(subjectInfo.subjectName, tinymceEdit.subjectName)"/>
<tinymce ref="subjectNameEditor" :height="60" v-model="subjectInfo.subjectName"/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item :label="$t('table.subject.answer')" prop="answer">
<el-input v-model="subjectInfo.answer.answer" @focus="updateTinymceContent(subjectInfo.answer.answer, tinymceEdit.answer)"/>
<tinymce ref="answerEditor" :height="60" v-model="subjectInfo.answer.answer"/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item :label="$t('table.subject.analysis')" prop="analysis">
<el-input v-model="subjectInfo.analysis" @focus="updateTinymceContent(subjectInfo.analysis, tinymceEdit.analysis)"/>
<tinymce ref="analysisEditor" :height="60" v-model="subjectInfo.analysis"/>
</el-form-item>
</el-col>
</el-row>
</el-col>
<el-col :span="14">
<div class="subject-tinymce">
<tinymce ref="shortAnswerEditor" :height="350" v-model="shortAnswerEditorContent"/>
</div>
</el-col>
</el-row>
</el-form>
</template>
......@@ -85,37 +80,15 @@ export default {
data () {
return {
subjectInfo: this.subject,
shortAnswerEditorContent: this.content,
labelPosition: 'right',
// 表单校验规则
subjectRules: {
subjectName: [{ required: true, message: '请输入题目名称', trigger: 'change' }],
score: [{ required: true, message: '请输入题目分值', trigger: 'change' }],
answer: [{ required: true, message: '请输入答案', trigger: 'change' }]
},
tinymce: {
type: 1, // 类型 0:题目名称,1:选项A,2:选择B,3:选项C,4:选项D
dialogTinymceVisible: false,
tempValue: '',
currentEdit: -1
},
// 编辑对象
tinymceEdit: {
subjectName: -1,
answer: 4,
analysis: 5
}
}
},
watch: {
// 监听富文本编辑器的输入
shortAnswerEditorContent: {
handler: function (shortAnswerEditorContent) {
this.saveTinymceContent(shortAnswerEditorContent)
},
immediate: true
}
},
methods: {
setSubjectInfo (subject) {
this.subjectInfo = subject
......@@ -123,27 +96,6 @@ export default {
getSubjectInfo () {
return this.subjectInfo
},
// 绑定富文本的内容
updateTinymceContent (content, currentEdit) {
// 绑定当前编辑的对象
this.tinymce.currentEdit = currentEdit
// 选择题
this.$refs.shortAnswerEditor.setContent(content || '')
},
// 保存题目时绑定富文本的内容到subjectInfo
saveTinymceContent (content) {
switch (this.tinymce.currentEdit) {
case this.tinymceEdit.subjectName:
this.subjectInfo.subjectName = content
break
case this.tinymceEdit.answer:
this.subjectInfo.answer.answer = content
break
case this.tinymceEdit.analysis:
this.subjectInfo.analysis = content
break
}
},
// 表单校验
validate () {
let valid = false
......@@ -177,6 +129,9 @@ export default {
if (isNotEmpty(score)) {
this.subjectInfo.score = score
}
this.$refs['subjectNameEditor'].setContent('')
this.$refs['answerEditor'].setContent('')
this.$refs['analysisEditor'].setContent('')
},
initDefaultOptions () {
......
......@@ -32,7 +32,7 @@ export default {
},
menubar: {
type: String,
default: 'file edit insert view format table'
default: ''
},
height: {
type: Number,
......@@ -42,8 +42,6 @@ export default {
},
data () {
return {
hasChange: false,
hasClick: false,
hasInit: false,
tinymceId: this.id,
fullscreen: false,
......@@ -94,6 +92,7 @@ export default {
toolbar: this.toolbar.length > 0 ? this.toolbar : toolbar,
menubar: this.menubar,
plugins: plugins,
external_plugins: { tiny_mce_wiris: 'https://www.wiris.net/demo/plugins/tiny_mce/plugin.js' },
end_container_on_empty_block: true,
powerpaste_word_import: 'clean',
code_dialog_height: 450,
......@@ -103,6 +102,7 @@ export default {
imagetools_cors_hosts: ['www.tinymce.com', 'codepen.io'],
default_link_target: '_blank',
link_title: false,
statusbar: false,
nonbreaking_force_tab: true, // inserting nonbreaking space &nbsp; need Nonbreaking Space Plugin
init_instance_callback: editor => {
if (_this.value) {
......@@ -113,11 +113,6 @@ export default {
this.hasChange = true
this.$emit('input', editor.getContent())
})
editor.on('Click', () => {
this.hasClick = true
this.$emit('hasClick', this.hasClick)
})
},
setup (editor) {
editor.on('FullscreenStateChanged', (e) => {
......@@ -136,12 +131,6 @@ export default {
},
getContent () {
return window.tinymce.get(this.tinymceId).getContent()
},
getHasClick () {
return this.hasClick
},
setHashClick (click) {
this.hasClick = click
}
}
}
......
// Here is a list of the toolbar
// Detail list see https://www.tinymce.com/docs/advanced/editor-control-identifiers/#toolbarcontrols
const toolbar = ['bold italic underline strikethrough alignleft aligncenter alignright outdent indent blockquote undo redo removeformat subscript superscript code codesample', 'hr bullist numlist link image charmap preview anchor pagebreak insertdatetime media table emoticons forecolor backcolor fullscreen']
const toolbar = ['tiny_mce_wiris_formulaEditor tiny_mce_wiris_formulaEditorChemistry bold italic underline strikethrough alignleft aligncenter alignright outdent indent blockquote superscript codesample bullist numlist link image charmap media table forecolor backcolor preview fullscreen']
export default toolbar
......@@ -169,6 +169,7 @@ export default {
attachSize: '附件大小',
upload: '上传',
download: '下载',
downloadUrl: '复制下载链接',
uploader: '上传者',
uploadDate: '上传时间',
courseName: '课程名称',
......
......@@ -15,10 +15,10 @@
:data="params"
class="upload-demo"
multiple>
<el-button v-waves type="primary" class="filter-item">上传<i class="el-icon-upload el-icon--right" style="margin-left: 10px;"/></el-button>
<el-progress v-if="uploading === true" :percentage="percentage" :text-inside="true" :stroke-width="18" status="success"/>
<el-button v-waves type="success" class="filter-item">上传<i class="el-icon-upload el-icon--right" style="margin-left: 10px;"/></el-button>
</el-upload>
</div>
<el-progress v-if="uploading === true" :percentage="percentage" :text-inside="true" :stroke-width="18" status="success"/>
<spinner-loading v-if="listLoading"/>
<el-table
......@@ -28,7 +28,7 @@
style="width: 100%;"
@sort-change="sortChange">
<el-table-column type="selection" width="55"/>
<el-table-column prop="id" label="流水号" min-width="100">
<el-table-column prop="流水号" label="id" min-width="100">
<template slot-scope="scope">
<span>{{ scope.row.id }}</span>
</template>
......@@ -43,6 +43,11 @@
<span>{{ scope.row.busiType | attachmentTypeFilter }}</span>
</template>
</el-table-column>
<el-table-column label="附件大小" min-width="90">
<template slot-scope="scope">
<span>{{ scope.row.attachSize | attachmentSizeFilter }}</span>
</template>
</el-table-column>
<el-table-column :label="$t('table.uploader')" min-width="50">
<template slot-scope="scope">
<span>{{ scope.row.creator }}</span>
......@@ -50,14 +55,38 @@
</el-table-column>
<el-table-column :label="$t('table.uploadDate')" min-width="70">
<template slot-scope="scope">
<span>{{ scope.row.createDate | timeFilter }}</span>
<span>{{ scope.row.createDate | fmtDate('yyyy-MM-dd hh:mm') }}</span>
</template>
</el-table-column>
<el-table-column :label="$t('table.actions')" class-name="status-col" width="300px">
<el-table-column :label="$t('table.actions')" class-name="status-col" width="100">
<template slot-scope="scope">
<el-button type="text" @click="handleDownload(scope.row)">{{ $t('table.download') }}</el-button>
<el-button type="text" @click="handleDelete(scope.row)">{{ $t('table.delete') }}
</el-button>
<el-dropdown>
<span class="el-dropdown-link">
操作<i class="el-icon-caret-bottom el-icon--right"></i>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item>
<a @click="handlePreview(scope.row)">
<span><i class="el-icon-view"></i>{{ $t('table.preview') }}</span>
</a>
</el-dropdown-item>
<el-dropdown-item>
<a @click="handleDownloadUrl(scope.row)">
<span><i class="el-icon-document-copy"></i>{{ $t('table.downloadUrl') }}</span>
</a>
</el-dropdown-item>
<el-dropdown-item>
<a @click="handleDownload(scope.row)">
<span><i class="el-icon-download"></i>{{ $t('table.download') }}</span>
</a>
</el-dropdown-item>
<el-dropdown-item>
<a @click="handleDelete(scope.row)">
<span><i class="el-icon-delete"></i>{{ $t('table.delete') }}</span>
</a>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
</el-table-column>
</el-table>
......@@ -65,14 +94,21 @@
<div class="pagination-container">
<el-pagination v-show="total>0" :current-page="listQuery.pageNum" :page-sizes="[10,20,30, 50]" :page-size="listQuery.pageSize" :total="total" background layout="total, sizes, prev, pager, next, jumper" @size-change="handleSizeChange" @current-change="handleCurrentChange"/>
</div>
<!-- 预览 -->
<el-dialog :visible.sync="dialogPreviewVisible" title="预览" width="50%" top="12vh">
<div class="preview">
<img :src="previewUrl" alt="二维码">
</div>
</el-dialog>
</div>
</template>
<script>
import { fetchList, addObj, putObj, delAttachment, getDownloadUrl } from '@/api/admin/attachment'
import { fetchList, addObj, putObj, delAttachment, canPreview } from '@/api/admin/attachment'
import waves from '@/directive/waves'
import { getToken } from '@/utils/auth' // getToken from cookie
import { notifySuccess, messageSuccess, isNotEmpty, formatDate } from '@/utils/util'
import { getToken } from '@/utils/auth'
import { messageSuccess, messageWarn } from '@/utils/util'
import SpinnerLoading from '@/components/SpinnerLoading'
export default {
......@@ -105,8 +141,16 @@ export default {
}
return attachType
},
timeFilter (time) {
return formatDate(new Date(time), 'yyyy-MM-dd hh:mm')
attachmentSizeFilter (attachSize) {
let fileSizeByte = attachSize
let fileSizeMsg = ''
if (fileSizeByte < 1048576) fileSizeMsg = (fileSizeByte / 1024).toFixed(2) + 'KB'
else if (fileSizeByte === 1048576) fileSizeMsg = '1MB'
else if (fileSizeByte > 1048576 && fileSizeByte < 1073741824) fileSizeMsg = (fileSizeByte / (1024 * 1024)).toFixed(2) + 'MB'
else if (fileSizeByte > 1048576 && fileSizeByte === 1073741824) fileSizeMsg = '1GB'
else if (fileSizeByte > 1073741824 && fileSizeByte < 1099511627776) fileSizeMsg = (fileSizeByte / (1024 * 1024 * 1024)).toFixed(2) + 'GB'
else fileSizeMsg = '文件超过1TB'
return fileSizeMsg
}
},
data () {
......@@ -151,7 +195,9 @@ export default {
}
],
uploading: false,
percentage: 0
percentage: 0,
dialogPreviewVisible: false,
previewUrl: ''
}
},
created () {
......@@ -204,25 +250,38 @@ export default {
addObj(this.temp).then(() => {
this.list.unshift(this.temp)
this.getList()
notifySuccess(this, '创建成功')
messageSuccess(this, '创建成功')
})
}
})
},
handleDownload (row) {
getDownloadUrl(row.id).then(response => {
if (isNotEmpty(response.data)) {
window.open('http://' + response.data.data, '_blank')
window.location.href = '/api/user/v1/attachment/download?id=' + row.id
},
handlePreview (row) {
this.previewUrl = ''
canPreview(row.id).then(response => {
if (response.data.data) {
this.previewUrl = '/api/user/v1/attachment/preview?id=' + row.id
this.dialogPreviewVisible = true
} else {
messageWarn(this, '暂不支持预览该格式的附件')
}
}).catch(error => {
console.error(error)
})
},
handleDownloadUrl (row) {
const url = 'http://' + window.location.host + '/api/user/v1/attachment/download?id=' + row.id
this.$alert(url, '下载链接', { confirmButtonText: '确定' })
},
updateData () {
this.$refs['dataForm'].validate((valid) => {
if (valid) {
const tempData = Object.assign({}, this.temp)
putObj(tempData).then(() => {
this.getList()
notifySuccess(this, '更新成功')
messageSuccess(this, '更新成功')
})
}
})
......@@ -236,14 +295,14 @@ export default {
}).then(() => {
delAttachment(row.id).then(() => {
this.getList()
notifySuccess(this, '删除成功')
messageSuccess(this, '删除成功')
})
}).catch(() => {})
},
handleUploadSuccess () {
this.uploading = false
this.getList()
notifySuccess(this, '上传成功')
messageSuccess(this, '上传成功')
},
handleUploadProgress (event, file, fileList) {
this.uploading = true
......@@ -252,3 +311,13 @@ export default {
}
}
</script>
<style lang="scss" scoped>
.upload-demo {
display: inline-block;
}
.preview {
text-align: center;
overflow: hidden;
}
</style>
......@@ -234,7 +234,7 @@ import waves from '@/directive/waves'
import { mapGetters, mapState } from 'vuex'
import { getToken } from '@/utils/auth'
import { checkMultipleSelect, isNotEmpty, notifySuccess, notifyFail, messageSuccess } from '@/utils/util'
import { delAttachment, preview } from '@/api/admin/attachment'
import { delAttachment } from '@/api/admin/attachment'
import Tinymce from '@/components/Tinymce'
import SpinnerLoading from '@/components/SpinnerLoading'
import Choices from '@/components/Subjects/Choices'
......@@ -465,6 +465,7 @@ export default {
},
handleUpdate (row) {
this.temp = Object.assign({}, row)
this.avatar = ''
if (!isNotEmpty(this.temp.course)) {
this.temp.course = {
id: '',
......@@ -473,9 +474,7 @@ export default {
}
// 获取图片的预览地址
if (isNotEmpty(this.temp.avatarId)) {
preview(this.temp.avatarId).then(response => {
this.avatar = response.data.data
})
this.avatar = '/api/user/v1/attachment/preview?id=' + this.temp.avatarId
}
this.dialogStatus = 'update'
this.dialogFormVisible = true
......
......@@ -394,7 +394,6 @@ export default {
</script>
<style lang="scss" rel="stylesheet/scss" scoped>
/* 题目 */
.subject-title {
font-size: 18px;
line-height: 22px;
......
......@@ -86,6 +86,11 @@
<span><i class="el-icon-edit"></i>{{ $t('table.edit') }}</span>
</a>
</el-dropdown-item>
<el-dropdown-item>
<a @click="handleViewSubject(scope.row)">
<span><i class="el-icon-view"></i>{{ $t('table.preview') }}</span>
</a>
</el-dropdown-item>
<el-dropdown-item v-if="subject_bank_btn_del">
<a @click="handleDeleteSubject(scope.row)">
<span><i class="el-icon-delete"></i>{{ $t('table.delete') }}</span>
......@@ -183,12 +188,39 @@
</el-col>
</el-row>
</el-dialog>
<!-- 预览题目 -->
<el-dialog title="预览题目" :visible.sync="dialogViewVisible" width="60%" top="10vh">
<div class="subject-title">
<span class="subject-title-content" v-html="tempSubject.subjectName"/>
<span class="subject-title-content">&nbsp;({{tempSubject.score}})分</span>
</div>
<ul v-if="tempSubject.type === 0 || tempSubject.type === 3" class="subject-options">
<li class="subject-option" v-for="(option) in tempSubject.options" :key="option.id">
<input class="toggle" type="checkbox">
<label><span class="subject-option-prefix">{{option.optionName}}&nbsp;</span><span v-html="option.optionContent" class="subject-option-prefix"></span></label>
</li>
</ul>
<ul v-if="tempSubject.type === 2" class="subject-options">
<li class="subject-option">
<input class="toggle" type="checkbox">
<label><span class="subject-option-prefix">正确</span></label>
</li>
<li class="subject-option">
<input class="toggle" type="checkbox">
<label><span class="subject-option-prefix">错误</span></label>
</li>
</ul>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="dialogViewVisible = false">{{ $t('table.confirm') }}</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { fetchCategoryTree, getCategory, addCategory, delCategory, putCategory } from '@/api/exam/subjectCategory'
import { fetchSubjectList, addSubject, putSubject, delSubject, delAllSubject, exportSubject } from '@/api/exam/subject'
import { fetchSubjectList, addSubject, getSubject, putSubject, delSubject, delAllSubject, exportSubject } from '@/api/exam/subject'
import { mapGetters } from 'vuex'
import { getToken } from '@/utils/auth'
import { checkMultipleSelect, exportExcel, notifySuccess, isNotEmpty } from '@/utils/util'
......@@ -308,6 +340,8 @@ export default {
dialogImportVisible: false,
// 导出窗口状态
dialogExportVisible: false,
// 预览窗口状态
dialogViewVisible: false,
// 选择的菜单
multipleSelection: [],
importUrl: '/api/exam/v1/subject/import',
......@@ -699,6 +733,14 @@ export default {
}).catch(() => {})
}
},
// 查看题目
handleViewSubject (row) {
// 加载题目信息
getSubject(row.id, { type: row.type }).then(response => {
this.tempSubject = response.data.data
this.dialogViewVisible = true
})
},
// 点击排序按钮
sortSubjectChange (column, prop, order) {
this.listQuery.sort = column.prop
......@@ -813,16 +855,88 @@ export default {
}
</script>
<style scoped>
<style lang="scss" rel="stylesheet/scss" scoped>
.category-header {
margin: 12px;
}
.tree-container{
.tree-container {
padding-top: 10px;
}
.category-btn {
margin: 5px;
padding: 6px 13px;
}
.filter-tree {
overflow: hidden;
}
.subject-title {
font-size: 18px;
line-height: 22px;
.subject-title-number {
display: inline-block;
line-height: 22px;
}
.subject-title-content {
display: inline-block;
}
}
.subject-options {
margin: 0;
padding: 0;
list-style: none;
> li {
position: relative;
font-size: 24px;
.toggle {
opacity: 0;
text-align: center;
width: 35px;
/* auto, since non-WebKit browsers doesn't support input styling */
height: auto;
position: absolute;
top: 0;
bottom: 0;
margin: auto 0;
border: none;
/* Mobile Safari */
-webkit-appearance: none;
appearance: none;
}
.toggle + label {
background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E');
background-repeat: no-repeat;
background-position: center left;
background-size: 30px;
}
.toggle:checked + label {
background-size: 30px;
background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E');
}
label {
word-break: break-all;
padding: 10px 10px 10px 45px;
display: block;
line-height: 1.0;
transition: color 0.4s;
}
/* 选项名称 */
.subject-option-prefix {
font-size: 16px;
display: inline-block
}
}
}
</style>
......@@ -95,7 +95,6 @@
import { updateObjInfo, updateAvatar } from '@/api/admin/user'
import { mapState } from 'vuex'
import { getToken } from '@/utils/auth'
import { preview } from '@/api/admin/attachment'
import { isNotEmpty, notifySuccess, notifyFail } from '@/utils/util'
import store from '@/store'
......@@ -152,9 +151,7 @@ export default {
return
}
// 重新获取预览地址
preview(res.data.id).then(response => {
this.userInfo.avatarUrl = response.data.data
})
this.userInfo.avatarUrl = '/api/user/v1/attachment/preview?id=' + res.data.id
this.userInfo.avatarId = res.data.id
updateAvatar(this.userInfo).then(response => {
notifySuccess(this, '头像上传成功')
......
......@@ -428,4 +428,7 @@ export default {
.tab-container{
margin: 30px;
}
.filter-tree {
overflow: hidden;
}
</style>
{
"name": "spring-microservice-exam-web",
"version": "3.5.0",
"version": "3.7.0",
"description": "spring-microservice-exam-web",
"author": "tangyi <1633736729@qq.com>",
"private": true,
......
......@@ -45,7 +45,7 @@
</el-submenu>
<el-submenu v-if="login" index="/user-info">
<template slot="title">
<img src="https://colorlib.com/preview/theme/clever/img/bg-img/t1.png" style="height: 30px;border-radius: 50%;margin-right: 6px;"/>
<img :src="userInfo.avatarUrl" style="height: 30px;border-radius: 50%;margin-right: 6px;"/>
{{userInfo.identifier}}
</template>
<el-menu-item index="account" @click="open('/account')">个人中心</el-menu-item>
......
......@@ -44,7 +44,7 @@
</el-submenu>
<el-submenu v-if="login" index="/user-info">
<template slot="title">
<img src="https://colorlib.com/preview/theme/clever/img/bg-img/t1.png" style="height: 30px;border-radius: 50%;margin-right: 6px;"/>
<img :src="userInfo.avatarUrl" style="height: 30px;border-radius: 50%;margin-right: 6px;"/>
{{userInfo.identifier}}
</template>
<el-menu-item index="account" @click="open('/account')">个人中心</el-menu-item>
......
......@@ -91,7 +91,6 @@ import { updateObjInfo, updateAvatar } from '@/api/admin/user'
import OFooter from '../common/footer'
import { getToken } from '@/utils/auth'
import { mapState } from 'vuex'
import { preview } from '@/api/admin/attachment'
import { isNotEmpty, notifySuccess, notifyFail } from '@/utils/util'
import store from '@/store'
......@@ -144,14 +143,12 @@ export default {
})
},
handleAvatarSuccess (res, file) {
if (!isNotEmpty(res.data) || !isNotEmpty(res.data.fastFileId)) {
if (!isNotEmpty(res.data)) {
notifyFail(this, '头像上传失败')
return
}
// 重新获取预览地址
preview(res.data.id).then(response => {
this.userInfo.avatarUrl = response.data.data
})
this.userInfo.avatarUrl = '/api/user/v1/attachment/preview?id=' + res.data.id
this.userInfo.avatarId = res.data.id
updateAvatar(this.userInfo).then(response => {
notifySuccess(this, '头像上传成功')
......
......@@ -6,6 +6,7 @@ import com.github.tangyi.common.core.constant.CommonConstant;
import com.github.tangyi.common.core.service.CrudService;
import com.github.tangyi.exam.api.module.Course;
import com.github.tangyi.exam.mapper.CourseMapper;
import com.github.tangyi.user.api.constant.AttachmentConstant;
import com.github.tangyi.user.api.module.Attachment;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
......@@ -141,9 +142,7 @@ public class CourseService extends CrudService<CourseMapper, Course> {
courseList.forEach(course -> {
// 获取配置默认头像地址
if (course.getLogoId() != null && course.getLogoId() != 0L) {
Attachment attachment = new Attachment();
attachment.setId(course.getLogoId());
course.setLogoUrl(sysProperties.getLogoUrl() + course.getLogoId() + sysProperties.getLogoSuffix());
course.setLogoUrl(AttachmentConstant.ATTACHMENT_PREVIEW_URL + course.getLogoId());
} else {
Long index = new Random().nextInt(sysProperties.getLogoCount()) + 1L;
course.setLogoUrl(sysProperties.getLogoUrl() + index + sysProperties.getLogoSuffix());
......
......@@ -14,6 +14,7 @@ import com.github.tangyi.exam.api.module.Examination;
import com.github.tangyi.exam.api.module.ExaminationSubject;
import com.github.tangyi.exam.enums.ExaminationTypeEnum;
import com.github.tangyi.exam.mapper.ExaminationMapper;
import com.github.tangyi.user.api.constant.AttachmentConstant;
import com.github.tangyi.user.api.module.Attachment;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
......@@ -297,7 +298,7 @@ public class ExaminationService extends CrudService<ExaminationMapper, Examinati
if (examinationDto.getAvatarId() != null && examinationDto.getAvatarId() != 0L) {
Attachment attachment = new Attachment();
attachment.setId(examinationDto.getAvatarId());
examinationDto.setLogoUrl(sysProperties.getLogoUrl() + examinationDto.getAvatarId() + sysProperties.getLogoSuffix());
examinationDto.setLogoUrl(AttachmentConstant.ATTACHMENT_PREVIEW_URL + examinationDto.getAvatarId());
} else {
Long index = new Random().nextInt(sysProperties.getLogoCount()) + 1L;
examinationDto.setLogoUrl(sysProperties.getLogoUrl() + index + sysProperties.getLogoSuffix());
......
......@@ -20,4 +20,9 @@ public class AttachmentConstant {
* 知识库附件
*/
public static final String BUSI_TYPE_KNOWLEDGE_ATTACHMENT = "2";
/**
* 附件预览地址
*/
public static final String ATTACHMENT_PREVIEW_URL = "/api/user/v1/attachment/preview?id=";
}
package com.github.tangyi.user.api.module;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.github.tangyi.common.core.persistence.BaseEntity;
import com.github.tangyi.user.api.constant.AttachmentConstant;
import lombok.Data;
import javax.validation.constraints.NotBlank;
/**
* 附件信息
*
......@@ -18,16 +17,19 @@ public class Attachment extends BaseEntity<Attachment> {
/**
* 附件名称
*/
@NotBlank(message = "附件名称不能为空")
private String attachName;
/**
* 附件大小
*/
@NotBlank(message = "附件大小不能为空")
private String attachSize;
/**
* 附件类型
*/
private String attachType;
/**
* 组名称
*/
private String groupName;
......@@ -35,12 +37,12 @@ public class Attachment extends BaseEntity<Attachment> {
/**
* 文件ID
*/
@JsonIgnore
private String fastFileId;
/**
* 业务流水号
*/
@NotBlank(message = "附件业务流水号不能为空")
private String busiId;
/**
......@@ -57,4 +59,14 @@ public class Attachment extends BaseEntity<Attachment> {
* 预览地址
*/
private String previewUrl;
/**
* 上传类型,1:本地目录,2:fastDfs,3:七牛云
*/
private Integer uploadType;
/**
* 上传结果
*/
private String uploadResult;
}
package com.github.tangyi.user.controller;
import com.github.pagehelper.PageInfo;
import com.github.tangyi.common.basic.properties.SysProperties;
import com.github.tangyi.common.basic.vo.AttachmentVo;
import com.github.tangyi.common.core.constant.CommonConstant;
import com.github.tangyi.common.core.exceptions.CommonException;
import com.github.tangyi.common.core.model.ResponseBean;
import com.github.tangyi.common.core.utils.FileUtil;
import com.github.tangyi.common.core.utils.PageUtil;
import com.github.tangyi.common.core.utils.Servlets;
import com.github.tangyi.common.core.web.BaseController;
import com.github.tangyi.common.log.annotation.Log;
import com.github.tangyi.common.security.utils.SysUtil;
import com.github.tangyi.user.api.module.Attachment;
import com.github.tangyi.user.service.AttachmentService;
import com.github.tangyi.user.uploader.UploadInvoker;
import com.google.common.net.HttpHeaders;
import io.swagger.annotations.*;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.constraints.NotBlank;
import java.io.*;
import java.net.URLEncoder;
import java.util.List;
import java.util.stream.Collectors;
......@@ -39,6 +50,8 @@ public class AttachmentController extends BaseController {
private final AttachmentService attachmentService;
private final SysProperties sysProperties;
/**
* 根据ID获取
*
......@@ -104,9 +117,19 @@ public class AttachmentController extends BaseController {
@Log("上传文件")
public ResponseBean<Attachment> upload(@ApiParam(value = "要上传的文件", required = true) @RequestParam("file") MultipartFile file,
Attachment attachment) {
if (file.isEmpty())
return new ResponseBean<>(new Attachment());
return new ResponseBean<>(attachmentService.upload(file, attachment));
if (!file.isEmpty()) {
try {
attachment.setCommonValue(SysUtil.getUser(), SysUtil.getSysCode(), SysUtil.getTenantCode());
attachment.setAttachType(FileUtil.getFileNameEx(file.getOriginalFilename()));
attachment.setAttachSize(String.valueOf(file.getSize()));
attachment.setAttachName(file.getOriginalFilename());
attachment.setBusiId(attachment.getId().toString());
attachment = UploadInvoker.getInstance().upload(attachment, file.getBytes());
} catch (Exception e) {
log.error("upload attachment error: {}", e.getMessage(), e);
}
}
return new ResponseBean<>(attachment);
}
/**
......@@ -119,19 +142,36 @@ public class AttachmentController extends BaseController {
@GetMapping("download")
@ApiOperation(value = "下载附件", notes = "根据ID下载附件")
@ApiImplicitParam(name = "id", value = "附件ID", required = true, dataType = "Long")
public ResponseBean<String> download(@NotBlank Long id) {
String downloadUrl = "";
public void download(HttpServletRequest request, HttpServletResponse response, @NotBlank Long id) {
try {
Attachment attachment = new Attachment();
attachment.setId(id);
attachment = attachmentService.get(attachment);
if (attachment == null)
throw new CommonException("Attachment does not exist");
downloadUrl = attachmentService.download(attachment);
InputStream inputStream = UploadInvoker.getInstance().download(attachment);
if (inputStream == null) {
log.info("attachment is not exists");
return;
}
OutputStream outputStream = response.getOutputStream();
response.setContentType("application/zip");
response.setHeader(HttpHeaders.CACHE_CONTROL, "max-age=10");
// IE之外的浏览器使用编码输出名称
String contentDisposition = "";
String httpUserAgent = request.getHeader("User-Agent");
if (StringUtils.isNotEmpty(httpUserAgent)) {
httpUserAgent = httpUserAgent.toLowerCase();
String fileName = attachment.getAttachName();
contentDisposition = httpUserAgent.contains("wps") ? "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8") : Servlets.getDownName(request, fileName);
}
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, contentDisposition);
response.setContentLength(inputStream.available());
FileCopyUtils.copy(inputStream, outputStream);
log.info("download {} success", attachment.getAttachName());
} catch (Exception e) {
log.error("Download attachment failed: {}", e.getMessage(), e);
}
return new ResponseBean<>(downloadUrl);
}
/**
......@@ -152,7 +192,7 @@ public class AttachmentController extends BaseController {
attachment = attachmentService.get(attachment);
boolean success = false;
if (attachment != null)
success = attachmentService.delete(attachment) > 0;
success = UploadInvoker.getInstance().delete(attachment);
return new ResponseBean<>(success);
}
......@@ -172,7 +212,7 @@ public class AttachmentController extends BaseController {
boolean success = false;
try {
if (ArrayUtils.isNotEmpty(ids))
success = attachmentService.deleteAll(ids) > 0;
success = UploadInvoker.getInstance().deleteAll(ids);
} catch (Exception e) {
log.error("Delete attachment failed", e);
}
......@@ -205,19 +245,49 @@ public class AttachmentController extends BaseController {
}
/**
* 获取预览地址
* 是否支持预览
*
* @param id id
* @return ResponseBean
* @author tangyi
* @date 2019/06/19 15:47
*/
@GetMapping("/{id}/preview")
@ApiOperation(value = "获取预览地址", notes = "根据附件ID获取预览地址")
@GetMapping("/{id}/canPreview")
@ApiOperation(value = "判断附件是否支持预览", notes = "根据附件ID判断附件是否支持预览")
@ApiImplicitParam(name = "id", value = "附件id", required = true, dataType = "Long", paramType = "path")
public ResponseBean<String> getPreviewUrl(@PathVariable Long id) {
public ResponseBean<Boolean> canPreview(@PathVariable Long id) {
Attachment attachment = new Attachment();
attachment.setId(id);
return new ResponseBean<>(attachmentService.getPreviewUrl(attachment));
attachment = attachmentService.get(attachment);
return new ResponseBean<>(attachment != null && ArrayUtils.contains(sysProperties.getCanPreview().split(","), attachment.getAttachType()));
}
/**
* 预览附件
*
* @param response response
* @param id id
* @author tangyi
* @date 2019/06/19 15:47
*/
@GetMapping("/preview")
@ApiOperation(value = "预览附件", notes = "根据附件ID预览附件")
@ApiImplicitParam(name = "id", value = "附件id", required = true, dataType = "Long")
public void preview(HttpServletResponse response, @RequestParam Long id) throws Exception {
Attachment attachment = new Attachment();
attachment.setId(id);
attachment = attachmentService.get(attachment);
FileInputStream stream = new FileInputStream(new File(attachment.getFastFileId() + File.separator + attachment.getAttachName()));
ByteArrayOutputStream out = new ByteArrayOutputStream(1000);
byte[] b = new byte[1000];
int n;
while ((n = stream.read(b)) != -1) {
out.write(b, 0, n);
}
response.setHeader("Content-Type", "image/png");
response.getOutputStream().write(out.toByteArray());
response.getOutputStream().flush();
out.close();
stream.close();
}
}
package com.github.tangyi.user.enums;
import lombok.Getter;
/**
* 附件存储类型
* @author tangyi
* @date 2020/04/05 14:01
*/
@Getter
public enum AttachUploaderEnum {
FILE(1, "文件", "com.github.tangyi.user.uploader.FileUploader"), FAST_DFS(2, "FastDfs", "com.github.tangyi.user.uploader.FastDfsUploader"),
QI_NIU(3, "七牛云", "com.github.tangyi.user.uploader.QiNiuUploader");
private Integer value;
private String desc;
private String implClass;
AttachUploaderEnum(int value, String desc, String implClass) {
this.value = value;
this.desc = desc;
this.implClass = implClass;
}
public static AttachUploaderEnum matchByValue(Integer value) {
for (AttachUploaderEnum item : AttachUploaderEnum.values()) {
if (item.value.equals(value)) {
return item;
}
}
return FILE;
}
}
package com.github.tangyi.user.service;
import com.github.tangyi.common.core.constant.CommonConstant;
import com.github.tangyi.common.core.exceptions.CommonException;
import com.github.tangyi.common.core.service.CrudService;
import com.github.tangyi.common.security.utils.SysUtil;
import com.github.tangyi.oss.service.QiNiuService;
import com.github.tangyi.user.api.constant.AttachmentConstant;
import com.github.tangyi.user.api.module.Attachment;
import com.github.tangyi.user.mapper.AttachmentMapper;
import com.github.tangyi.user.uploader.UploadInvoker;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.nio.charset.StandardCharsets;
import java.io.InputStream;
/**
* @author tangyi
......@@ -27,8 +25,6 @@ import java.nio.charset.StandardCharsets;
@Service
public class AttachmentService extends CrudService<AttachmentMapper, Attachment> {
private final QiNiuService qiNiuService;
/**
* 根据id查询
*
......@@ -55,50 +51,14 @@ public class AttachmentService extends CrudService<AttachmentMapper, Attachment>
}
/**
* 上传
*
* @param file file
* @param attachment attachment
* @return int
*/
@Transactional
public Attachment upload(MultipartFile file, Attachment attachment) {
try {
long start = System.currentTimeMillis();
long attachSize = file.getSize();
if (StringUtils.isNotBlank(file.getOriginalFilename())) {
String fileName = new String(file.getOriginalFilename().getBytes(), StandardCharsets.UTF_8);
String previewUrl = qiNiuService.upload(file.getBytes(), fileName);
Attachment newAttachment = new Attachment();
newAttachment.setCommonValue(SysUtil.getUser(), SysUtil.getSysCode(), SysUtil.getTenantCode());
newAttachment.setPreviewUrl(previewUrl);
newAttachment.setAttachName(fileName);
newAttachment.setGroupName(qiNiuService.getDomainOfBucket());
newAttachment.setFastFileId(fileName);
newAttachment.setAttachSize(Long.toString(attachSize));
newAttachment.setBusiId(attachment.getBusiId());
newAttachment.setBusiModule(attachment.getBusiModule());
newAttachment.setBusiType(attachment.getBusiType());
super.insert(newAttachment);
log.info("Upload attachment success, fileName: {}, time: {}ms", file.getName(), System.currentTimeMillis() - start);
return newAttachment;
}
return null;
} catch (Exception e) {
log.error(e.getMessage(), e);
throw new CommonException(e);
}
}
/**
* 下载
*
* @param attachment attachment
* @return InputStream
*/
public String download(Attachment attachment) throws Exception {
public InputStream download(Attachment attachment) throws Exception {
// 下载附件
return qiNiuService.getDownloadUrl(attachment.getAttachName());
return UploadInvoker.getInstance().download(attachment);
}
/**
......@@ -140,8 +100,11 @@ public class AttachmentService extends CrudService<AttachmentMapper, Attachment>
attachment = this.get(attachment);
if (attachment != null) {
String preview = attachment.getPreviewUrl();
if (!preview.startsWith("http"))
if (StringUtils.isNotBlank(preview) && !preview.startsWith("http")) {
preview = "http://" + preview;
} else {
preview = AttachmentConstant.ATTACHMENT_PREVIEW_URL + attachment.getId();
}
log.debug("GetPreviewUrl id: {}, preview url: {}", attachment.getId(), preview);
return preview;
}
......
package com.github.tangyi.user.uploader;
import com.github.tangyi.common.basic.properties.SysProperties;
import com.github.tangyi.common.core.utils.SpringContextHolder;
import com.github.tangyi.common.security.utils.SysUtil;
import com.github.tangyi.user.api.module.Attachment;
import com.github.tangyi.user.service.AttachmentService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import java.io.File;
import java.io.InputStream;
/**
* @author tangyi
* @date 2020/04/05 13:37
*/
public abstract class AbstractUploader implements IUploader {
@Override
public int save(Attachment attachment) {
return SpringContextHolder.getApplicationContext().getBean(AttachmentService.class).insert(attachment);
}
@Override
public boolean delete(Attachment attachment) {
return SpringContextHolder.getApplicationContext().getBean(AttachmentService.class).delete(attachment) > 0;
}
@Override
public abstract Attachment upload(Attachment attachment, byte[] bytes);
@Override
public abstract InputStream download(Attachment attachment);
/**
* 获取附件存储目录
*
* @param attachment attachment
* @param id id
* @return String
*/
public String getFileRealDirectory(Attachment attachment, String id) {
String applicationCode = attachment.getApplicationCode();
String busiId = attachment.getBusiId();
String fileName = attachment.getAttachName();
String fileRealDirectory = SpringContextHolder.getApplicationContext().getBean(SysProperties.class).getAttachPath() + File.separator
+ applicationCode + File.separator;
// 有分类就加上
if (StringUtils.isNotBlank(attachment.getBusiModule())) {
String busiModule = attachment.getBusiModule();
fileRealDirectory = fileRealDirectory + busiModule + File.separator;
}
if (StringUtils.isNotBlank(attachment.getBusiType())) {
String busiType = attachment.getBusiType();
fileRealDirectory = fileRealDirectory + busiType + File.separator;
}
fileRealDirectory = fileRealDirectory + busiId;
return fileRealDirectory;
}
}
package com.github.tangyi.user.uploader;
import com.github.tangyi.oss.service.FastDfsService;
import com.github.tangyi.user.api.module.Attachment;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
/**
* 上传到FastDfs
* @author tangyi
* @date 2020/04/05 13:36
*/
@Slf4j
@Service
public class FastDfsUploader extends AbstractUploader {
@Autowired
private FastDfsService fastDfsService;
@Override
public Attachment upload(Attachment attachment, byte[] bytes) {
try {
attachment.setAttachSize(String.valueOf(bytes.length));
String fastFileId = fastDfsService.uploadFile(new ByteArrayInputStream(bytes), bytes.length, attachment.getAttachType());
String groupName = fastFileId.substring(0, fastFileId.indexOf("/"));
attachment.setFastFileId(fastFileId);
attachment.setGroupName(groupName);
return attachment;
} catch (Exception e) {
log.error("上传附件至网盘失败:"+ attachment.getAttachName() + e.getMessage());
return null;
}
}
@Override
public InputStream download(Attachment attachment) {
return fastDfsService.downloadStream(attachment.getGroupName(), attachment.getFastFileId());
}
@Override
public boolean delete(Attachment attachment) {
if (StringUtils.isNotEmpty(attachment.getGroupName()) && StringUtils.isNotEmpty(attachment.getFastFileId())) {
fastDfsService.deleteFile(attachment.getGroupName(), attachment.getFastFileId());
}
return Boolean.TRUE;
}
@Override
public boolean deleteAll(Attachment attachment) {
return false;
}
}
package com.github.tangyi.user.uploader;
import com.github.tangyi.common.core.exceptions.CommonException;
import com.github.tangyi.common.core.utils.FileUtil;
import com.github.tangyi.user.api.module.Attachment;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import java.io.*;
/**
* 上传到本地目录
*
* @author tangyi
* @date 2020/04/05 13:36
*/
@Slf4j
@Service
public class FileUploader extends AbstractUploader {
@Override
public Attachment upload(Attachment attachment, byte[] bytes) {
try {
String fileRealDirectory = getFileRealDirectory(attachment, attachment.getId().toString());
fileRealDirectory = fileRealDirectory.replaceAll("\\\\", "/");
String fileName = attachment.getAttachName();
attachment.setAttachSize(String.valueOf(bytes.length));
log.info("file read directory: {}", fileRealDirectory);
FileUtil.createDirectory(fileRealDirectory);
log.info("start write file: {}", fileName);
saveFileFormByteArray(bytes, fileRealDirectory, fileName);
log.info("write file finished: {}", fileName);
attachment.setFastFileId(fileRealDirectory);
return attachment;
} catch (Exception e) {
log.error("FileUploader error:{}, {}", attachment.getAttachName(), e.getMessage(), e);
return null;
}
}
@Override
public InputStream download(Attachment attachment) {
String path = attachment.getFastFileId() + File.separator + attachment.getAttachName();
InputStream input = null;
try {
String fileRealDirectory = getFileRealDirectory(attachment, attachment.getId().toString());
fileRealDirectory = fileRealDirectory.replaceAll("\\\\", "/");
if (StringUtils.isNotBlank(fileRealDirectory) && !fileRealDirectory.equals(attachment.getFastFileId()))
throw new CommonException("attach path validate failure!attachPath:" + attachment.getFastFileId() + ", fileRealDirectory:" + fileRealDirectory);
input = new FileInputStream(new File(path));
} catch (Exception e) {
log.error("download attachment failure: {}", e.getMessage(), e);
}
return input;
}
@Override
public boolean delete(Attachment attachment) {
String path = attachment.getFastFileId()
+ File.separator
+ attachment.getAttachName();
File file = new File(path);
if (file.delete()) {
FileUtil.deleteDirectory(attachment.getFastFileId());
return super.delete(attachment);
}
return Boolean.FALSE;
}
@Override
public boolean deleteAll(Attachment attachment) {
return false;
}
private void saveFileFormByteArray(byte[] b, String path, String fileName) throws IOException {
BufferedOutputStream fs = new BufferedOutputStream(new FileOutputStream(path + "/" + fileName, true));
fs.write(b);
fs.flush();
fs.close();
}
}
package com.github.tangyi.user.uploader;
import com.github.tangyi.user.api.module.Attachment;
import java.io.InputStream;
/**
* @author tangyi
* @date 2020/04/05 13:36
*/
public interface IUploader {
/**
* 上传附件
* @param attachment attachment
* @param bytes bytes
* @return Attachment
*/
Attachment upload(Attachment attachment, byte[] bytes);
/**
* 保存附件信息
* @param attachment attachment
* @return int
*/
int save(Attachment attachment);
/**
* 下载附件
* @param attachment attachment
* @return InputStream
*/
InputStream download(Attachment attachment);
/**
* 删除附件
* @param attachment attachment
* @return boolean
*/
boolean delete(Attachment attachment);
/**
* 批量删除
* @param attachment attachment
* @return boolean
*/
boolean deleteAll(Attachment attachment);
}
package com.github.tangyi.user.uploader;
import com.github.tangyi.oss.service.QiNiuUtil;
import com.github.tangyi.user.api.module.Attachment;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.io.InputStream;
/**
* 上传到七牛云
*
* @author tangyi
* @date 2020/04/05 13:36
*/
@Slf4j
@Service
public class QiNiuUploader extends AbstractUploader {
@Override
public Attachment upload(Attachment attachment, byte[] bytes) {
String result = QiNiuUtil.getInstance().upload(bytes, attachment.getAttachName());
attachment.setUploadResult(result);
attachment.setPreviewUrl(attachment.getUploadResult());
return attachment;
}
@Override
public InputStream download(Attachment attachment) {
return null;
}
@Override
public boolean delete(Attachment attachment) {
return QiNiuUtil.getInstance().delete(attachment.getAttachName());
}
@Override
public boolean deleteAll(Attachment attachment) {
return false;
}
}
package com.github.tangyi.user.uploader;
import com.github.tangyi.common.basic.properties.SysProperties;
import com.github.tangyi.common.core.exceptions.CommonException;
import com.github.tangyi.common.core.utils.SpringContextHolder;
import com.github.tangyi.user.api.module.Attachment;
import com.github.tangyi.user.enums.AttachUploaderEnum;
import com.github.tangyi.user.service.AttachmentService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
/**
* @author tangyi
* @date 2020/04/05 14:16
*/
@Slf4j
public class UploadInvoker {
private Map<Integer, IUploader> uploaderMap = null;
private static UploadInvoker instance;
private AttachmentService attachmentService;
public UploadInvoker(AttachmentService attachmentService) {
this.attachmentService = attachmentService;
}
public synchronized static UploadInvoker getInstance() {
if (instance == null) {
instance = new UploadInvoker(SpringContextHolder.getApplicationContext().getBean(AttachmentService.class));
}
return instance;
}
/**
* 上传附件
*
* @param attachment attachment
* @param bytes bytes
* @return Attachment
* @author tangyi
* @date 2020/04/05 14:27
*/
public Attachment upload(Attachment attachment, byte[] bytes) {
if (attachment == null || bytes == null)
return null;
if (attachment.getUploadType() == null) {
String uploadType = SpringContextHolder.getApplicationContext().getBean(SysProperties.class).getAttachUploadType();
if (StringUtils.isNotBlank(uploadType)) {
attachment.setUploadType(Integer.parseInt(uploadType));
}
}
IUploader uploader = this.getUploader(attachment.getUploadType());
if (uploader == null)
throw new CommonException("uploader is null");
attachment = uploader.upload(attachment, bytes);
if (attachment != null) {
uploader.save(attachment);
}
return attachment;
}
/**
* 下载附件
*
* @param attachment attachment
* @return Attachment
* @author tangyi
* @date 2020/04/05 14:29
*/
public InputStream download(Attachment attachment) {
if (attachment == null)
return null;
IUploader uploader = this.getUploader(attachment.getUploadType());
if (uploader == null)
throw new CommonException("uploader is null");
return uploader.download(attachment);
}
/**
* 删除附件
*
* @param attachment attachment
* @return Attachment
* @author tangyi
* @date 2020/04/05 14:29
*/
public boolean delete(Attachment attachment) {
if (attachment == null)
return Boolean.FALSE;
IUploader uploader = this.getUploader(attachment.getUploadType());
if (uploader == null)
throw new CommonException("uploader is null");
return uploader.delete(attachment);
}
/**
* 批量删除附件
*
* @param ids ids
* @return Attachment
* @author tangyi
* @date 2020/04/05 15:03
*/
public boolean deleteAll(Long[] ids) {
boolean result = false;
for (Long id : ids) {
// 查询出实体
Attachment attachmentSearch = new Attachment();
attachmentSearch.setId(id);
attachmentSearch = attachmentService.get(attachmentSearch);
IUploader uploader = getUploader(attachmentSearch.getUploadType());
// 删除对应存储方式中的附件
result = uploader.delete(attachmentSearch);
if (result) {
uploader.delete(attachmentSearch);
}
}
return result;
}
/**
* 获取附件实现类
*
* @param uploadType uploadType
* @return IUploader
* @author tangyi
* @date 2020/04/05 14:17
*/
private IUploader getUploader(Integer uploadType) {
IUploader uploader;
if (uploaderMap == null) {
uploaderMap = new HashMap<>();
}
uploader = uploaderMap.get(uploadType);
try {
if (uploader == null) {
// 如果没有初始化则创建
String implClass = AttachUploaderEnum.matchByValue(uploadType).getImplClass();
Class<?> clazz = Class.forName(implClass);
uploader = (IUploader) clazz.newInstance();
uploaderMap.put(uploadType, uploader);
}
} catch (Exception e) {
log.error("getUploader error:{}", e.getMessage(), e);
return null;
}
return uploader;
}
}
......@@ -4,6 +4,7 @@
<resultMap id="attachmentResultMap" type="com.github.tangyi.user.api.module.Attachment">
<id column="id" property="id"/>
<result column="attach_name" property="attachName"/>
<result column="attach_type" property="attachType"/>
<result column="attach_size" property="attachSize"/>
<result column="group_name" property="groupName"/>
<result column="fast_file_id" property="fastFileId"/>
......@@ -11,6 +12,7 @@
<result column="busi_module" property="busiModule"/>
<result column="busi_type" property="busiType"/>
<result column="preview_url" property="previewUrl"/>
<result column="upload_type" property="uploadType"/>
<result column="creator" property="creator"/>
<result column="create_date" property="createDate" javaType="java.util.Date" jdbcType="TIMESTAMP"/>
<result column="modifier" property="modifier"/>
......@@ -23,6 +25,7 @@
<sql id="attachmentColumns">
a.id,
a.attach_name,
a.attach_type,
a.attach_size,
a.group_name,
a.fast_file_id,
......@@ -30,6 +33,7 @@
a.busi_module,
a.busi_type,
a.preview_url,
a.upload_type,
a.creator,
a.create_date,
a.modifier,
......@@ -92,6 +96,7 @@
INSERT INTO sys_attachment (
id,
attach_name,
attach_type,
attach_size,
group_name,
fast_file_id,
......@@ -99,6 +104,7 @@
busi_module,
busi_type,
preview_url,
upload_type,
creator,
create_date,
modifier,
......@@ -109,6 +115,7 @@
) VALUES (
#{id},
#{attachName},
#{attachType},
#{attachSize},
#{groupName},
#{fastFileId},
......@@ -116,6 +123,7 @@
#{busiModule},
#{busiType},
#{previewUrl},
#{uploadType},
#{creator},
#{createDate, jdbcType=TIMESTAMP, javaType=java.util.Date},
#{modifier},
......@@ -131,6 +139,9 @@
<if test="attachName != null">
attach_name = #{attachName},
</if>
<if test="attachType != null">
attach_type = #{attachType},
</if>
<if test="attachSize != null">
attach_size = #{attachSize},
</if>
......@@ -152,6 +163,9 @@
<if test="previewUrl != null">
preview_url = #{previewUrl},
</if>
<if test="uploadType != null">
upload_type = #{uploadType},
</if>
<if test="delFlag != null">
del_flag = #{delFlag},
</if>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment