[Project Combat: Платформа обнаружения нуклеиновых кислот] Глава пятая

Цели этой главы

Полная передача персонала, приемный персонал и персональный терминал загрузки данных

Используемая технология:

  • EasyExcel、ElementUIPlus
  • lodopРаспечатать

обзор

В этой главе нам нужно доделать бизнес-модули перегрузочного персонала, принимающего персонала и персонала выгрузки данных.Я не нашел соответствующий интерфейс из информации в Интернете.

вставьте сюда описание изображения

Транспортный персонал несет ответственность за передачу коробок для передачи образцов. Когда я был волонтером, я встречал нескольких сотрудников, занимающихся транспортировкой. Многие из них также были волонтерами. Короче говоря, есть люди из всех слоев общества. Напротив, количество персонала, осуществляющего передачу, намного меньше, чем количество персонала, занимающегося сбором. Их обязанности также очень просты: достаточно доставить образцы из пункта сбора в назначенное учреждение/больницу для тестирования. Конечно, у них тоже есть APP.Хотя я не видел их APP, но подумав, могу сделать вывод, что их функции должны быть очень простыми.Вам нужно только отсканировать код, чтобы получить коробку передачи и запустить передача.

Однако они могут передавать несколько ящиков за раз Один и тот же сотрудник может передавать ящики из нескольких пунктов сбора один раз. Поэтому необходимо каждый раз записывать, какие раздаточные коробки отгружаются.

Следующий шаг – принимающий персонал, а также представьте себе их обязанности: отсканируйте код перегрузочной коробки, зафиксируйте, какие перегрузочные коробки были получены, а затем сдайте их в испытательную лабораторию. Их APP также должен быть очень простым.

Наконец, загрузчики данных продолжают принимать решения, их обязанность — загружать данные обнаружения. В большинстве случаев результаты теста должны быть отрицательными, а если есть положительные, то он должен быть индивидуальным, поэтому говоря простым языком, вам нужно загрузить только код положительной пробирки, но следует отметить, что даже отрицательная пробирка код должен быть загружен.Необходимо сохранить результаты своих тестов, иначе обычные люди не смогут проверить записи теста на нуклеиновые кислоты.

В то же время, чтобы попрактиковаться в импорте/экспорте Excel, мы можем создать таблицу данных, чтобы сначала экспортировать код пробирки и сохранить список результатов теста. Затем загрузите форму, заполненную результатами теста, и сохраните ее в базе данных.При загрузке необходимо проверить данные.Нельзя просто создать одни данные EXCEL и сохранить их в базе данных для других.Это неправильно.

Кроме того, есть еще функция печати кодов ящиков и кодов пробирок.Мы поместили эту функцию на стороне загрузчика, потому что печать с мобильных телефонов еще не до конца популяризирована, а печать с ПК более распространена в бизнес-сценариях.

Вообще говоря, функции передающего, принимающего и выгружаемого персонала не сложны, при мозговом штурме надо учитывать, как на самом деле использовать систему.

Другой бэкэнд

С точки зрения общего дизайна, мы объединили три типа персонала передачи, персонала получателя и персонала загрузки в один внутренний модуль, поэтому при разработке внутреннего кода требуется некоторая специальная обработка.

На самом деле между этими тремя типами интеграции или слияния с точки зрения разработки нет большой разницы, с точки зрения реальной работы их лучше разделить, но если вы это сделаете, то код back-end на самом деле очень простой. Собрать все это сейчас немного сложнее. Где сложность?

Поскольку три типа персонала объединены вместе, все они нуждаются в государственном управлении, и в то же время они должны хорошо справляться с контролем полномочий. Не может быть, чтобы загрузчик имел фоновые полномочия загрузчика после входа в систему, это очень страшно.

Как решить эту проблему? В третьей главе мы реализовали операцию входа в систему, а также использовали Token для сохранения статуса входа на стороне браузера.

    @Override
    public Collector login(LoginModel model) throws BusinessException, UnsupportedEncodingException, NoSuchAlgorithmException {
    
    
        String md5Password = Md5Util.encode(model.getPassword());
        Collector collector = collectorDao.login(model.getTel(), md5Password);
        if (collector == null) {
    
    
            throw new BusinessException("用户名或密码不正确", ResultCodeEnum.LOGIN_ERROR);
        }
        String token = getToken(collector);

        //把token存到cokkie中,并设置过期时间,一天
        Cookie cookie = new Cookie("token", token);
        cookie.setPath("/");
        cookie.setMaxAge(7 * 24 * 60 * 60);
        Global.response.addCookie(cookie);
        //返回前端之前要把密文的密码清除掉。
        collector.setPassword(null);
        return collector;
    }

И мы специально сделали перехватчик, в перехватчике мы также PassTokenсами определили аннотацию, чтобы использовать контроль разрешения пропуска.


    /***
     * 在请求处理之前进行调用(Controller方法调用之前)
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    
    
        //统一拦截(查询当前session是否存在collecotr)
        Collector user = SessionUtil.getCurrentUser();
        if (user != null) {
    
    
            return true;
        }
//        return false;

        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        //检查是否有passtoken注释,有则跳过认证
        if (method.isAnnotationPresent(PassToken.class)) {
    
    
            PassToken passToken = method.getAnnotation(PassToken.class);
            if (passToken.required()) {
    
    
                return true;
            }
        }
        String token = Global.request.getHeader("token");// 从 http 请求头中取出 token
        if (token == null) {
    
    
            throw new BusinessException("非法请求,无登录令牌", ResultCodeEnum.NOT_LOGIN);
        }
        Collector collector = collectorService.getCollectorByToken(token);
        if (collector == null) {
    
    
            throw new BusinessException("登录状态过期", ResultCodeEnum.NOT_LOGIN);
        }
        JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(collector.getPassword())).build();
        try {
    
    
            jwtVerifier.verify(token);
        } catch (JWTVerificationException e) {
    
    
            throw new BusinessException("登录令牌过期,请重新登录",ResultCodeEnum.LOGIN_ERROR);
        }
        //如果令牌有效,把登录信息存到session中,这样如果需要用到登录信息不用总到数据库查询。
        SessionUtil.setCurrentUser(collector);
        return true;
        //该方法没有做异常处理,因为在SessionUtil中已经处理了登录状态的异常。只要getCurrentUser()返回有值肯定就是成功的。
    }

Поскольку здесь можно использовать PassToken, чтобы пропустить управление, можно ли использовать тот же метод в перехватчике для управления тем, какие интерфейсы доступны для каких ролей?

Ответ должен быть да.

Итак, в другом внутреннем модуле мы должны выполнить следующую обработку.

Во-первых, добавьте аннотации разрешений для приема персонала, загрузки персонала и перегрузки персонала.

Примечания от экспедитора:

package com.hawkon.other.common;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 转运人员Token验证的注解
 */
@Target({
    
    ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface TransferApi {
    
    
    boolean required() default true;
}

Примечания получателя:

package com.hawkon.other.common;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 接收人员的Token验证的注解
 */
@Target({
    
    ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RecieverAPI {
    
    
    boolean required() default true;
}


Комментарии от загрузившего:

package com.hawkon.other.common;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 上传人员Token验证的注解
 */
@Target({
    
    ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface UploaderApi {
    
    
    boolean required() default true;
}

Как использовать эти аннотации? Давайте сначала взглянем на настройки в перехватчике.В следующем коде я буду судить, содержит ли метод интерфейса аннотации трех разных кадров, и судить


    /***
     * 在请求处理之前进行调用(Controller方法调用之前)
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    
    

        if(!(handler instanceof HandlerMethod))
            return true;

        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        //检查是否有passtoken注释,有则跳过认证
        if (method.isAnnotationPresent(PassToken.class)) {
    
    
            PassToken passToken = method.getAnnotation(PassToken.class);
            if (passToken.required()) {
    
    
                return true;
            }
        }
        String token = Global.request.getHeader("token");// 从 http 请求头中取出 token
        //根据Method上的注解决定方法对哪个角色生效。
        //如果一个方法多个角色都需要用到,可以给方法加多个注解
        boolean pass = false;
        if(method.isAnnotationPresent(TransferApi.class)){
    
    
            pass =  checkTransfer(token);
            //要想实现一个接口允许多个角色可以调用,就需要依次去判断每一种角色,当一种角色判断失败的时候不能够直接返回,而是要继续判断另外一个。
            if(pass) return pass;
        }
        if(method.isAnnotationPresent(RecieverAPI.class)){
    
    
            pass = checkReciever(token);
            if(pass) return pass;
        }
        if(method.isAnnotationPresent(UploaderApi.class)){
    
    
            pass =  checkUploader(token);
            if(pass) return pass;
        }
        //如果以上三种角色都不是,直接拒绝
        return false;
    }
    //验证上传人员token
    private boolean checkUploader(String token) throws BusinessException {
    
    
        Uploader uploader = uploaderService.getUploaderByToken(token);
        if (uploader == null) {
    
    
            throw new BusinessException("登录状态过期", ResultCodeEnum.NOT_LOGIN);
        }
        JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(uploader.getPassword())).build();
        try {
    
    
            jwtVerifier.verify(token);
        } catch (JWTVerificationException e) {
    
    
            throw new BusinessException("登录令牌过期,请重新登录",ResultCodeEnum.LOGIN_ERROR);
        }
        SessionUtil.setCurrentUploader(uploader);
        return true;
    }
    //验证接收人员token
    private boolean checkReciever(String token) throws BusinessException {
    
    
        Reciever reciever = recieverService.getRecieverByToken(token);
        if (reciever == null) {
    
    
            throw new BusinessException("登录状态过期", ResultCodeEnum.NOT_LOGIN);
        }
        JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(reciever.getPassword())).build();
        try {
    
    
            jwtVerifier.verify(token);
        } catch (JWTVerificationException e) {
    
    
            throw new BusinessException("登录令牌过期,请重新登录",ResultCodeEnum.LOGIN_ERROR);
        }
        SessionUtil.setCurrentReciever(reciever);
        return true;
    }

    //验证转运人员token
    private boolean checkTransfer(String token) throws BusinessException {
    
    
        Transfer transfer = transferService.getTransferByToken(token);
        if (transfer == null) {
    
    
            throw new BusinessException("登录状态过期", ResultCodeEnum.NOT_LOGIN);
        }
        JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(transfer.getPassword())).build();
        try {
    
    
            jwtVerifier.verify(token);
        } catch (JWTVerificationException e) {
    
    
            throw new BusinessException("登录令牌过期,请重新登录",ResultCodeEnum.LOGIN_ERROR);
        }
        SessionUtil.setCurrentTransfer(transfer);
        return true;
    }

Затем в контроллере просто добавьте соответствующую аннотацию роли к каждому интерфейсу.


    @TransferApi //转运人员API
    @PostMapping("getNeedTransferBoxList")
    public ResultModel<List<BoxVO>> getNeedTransferBoxList() throws BusinessException {
    
    
        List<BoxVO> list = boxService.getNeedTransferBoxList();
        return ResultModel.success(list);
    }

    @TransferApi//转运人员API
    @PostMapping("transferBox")
    public ResultModel<Object> transferBox(@RequestBody Box model) throws BusinessException {
    
    
        boxService.transferBox(model);
        return ResultModel.success(null);
    }

    @RecieverAPI//接收人员API
    @PostMapping("recieveBox")
    public ResultModel<Object> recieveBox(@RequestBody Box model) throws BusinessException {
    
    
        boxService.recieveBox(model);
        return ResultModel.success(null);
    }

    @RecieverAPI//接收人员API
    @PostMapping("getRecievedBoxList")
    public ResultModel<List<BoxVO>> getRecievedBoxList() throws BusinessException {
    
    
        List<BoxVO> list = boxService.getRecievedBoxList();
        return ResultModel.success(list);
    }

С помощью описанного выше метода идеально, если в одном проекте появятся три разных персонажа, и они не смогут получить доступ друг к другу.

Затем вы можете плавно перейти к бизнес-интерфейсу.

Трансферный персонал

список функций

  • Авторизоваться
  • регистр
  • забыть пароль
  • изменить пароль
  • Отсканируйте код ящика, чтобы подтвердить перевод

Первые четыре функции аналогичны функциям коллекторов, поэтому я не буду подробно останавливаться на них. Просто скажите о последнем коде окна сканирования, чтобы подтвердить функцию передачи.

Персонал по перегрузке также должен записывать весь процесс во время процесса перегрузки.Кто какую коробку перегружает, когда она перегружается и какое агентство по тестированию отправляется к ней, все это записывается на протяжении всего процесса.

Предприниматели должны обратить внимание на следующие вопросы:

  • Для облегчения записи при перегрузке также необходимо поддерживать сканирование штрих-кода, а также поддерживать ручной ввод.
  • При входе в ящик перевалки обращайте внимание на статус кода перевалки.Перевалку нельзя вводить повторно, а незапечатанный кейс нельзя вводить.

Передняя часть терминала передачи персонала представляет собой отдельный модуль, а инженерная структура модуля выглядит следующим образом:

[Не удалось передать изображение по внешней ссылке, исходный сайт может иметь механизм защиты от пиявки, рекомендуется сохранить изображение и загрузить его напрямую (img-AzndZ7Qk-1671588636946) (E:\Hawkon\My Information\Writing\Miscellaneous\Project Combat Series\Платформа обнаружения нуклеиновых кислот\Project Combat: Платформа обнаружения нуклеиновых кислот (5).assets\image-20221205144907328.png)]

Видно, что логин, регистрация, забытый пароль и смена пароля — это общие функции, которые аналогичны функциям сборщиков, поэтому они не будут выкладываться повторно. Давайте сосредоточимся на странице Box.vue:

<script setup>
import {ref} from 'vue';
import {useRouter} from "vue-router";
import api from '@/common/api.js';
import { BrowserMultiFormatReader } from "@zxing/library";
const router = useRouter();
//登出操作
const logout = () => {
	api.post("/transfer/logout")
		.then(res => {
			router.push("/");
		})
};
//读取当前用户
const transfer = ref({});
transfer.value = JSON.parse(sessionStorage["user"]);
const active = ref("");

const showCodeScanner = ref(false);
const showEditor = ref(false);
const codeReader = ref(null);
const boxCode = ref("");
const tipMsg = ref("");
//初始化扫码器
codeReader.value = new BrowserMultiFormatReader();
const openEditor = () => {
	showEditor.value = true;
	active.value = "editor";
}
const closeEditor = () => {
	showEditor.value = false;
	active.value = "";
}
//打开/关闭扫码功能
const switchScanner = () => {
	if (showEditor.value) {
		closeEditor();
	}
	if (showCodeScanner.value) {
		closeScanner();
	} else {
		openScanner();
	}
}
//打开、关闭手工编辑功能
const switchEditor = () => {
	if (showCodeScanner.value) {
		closeScanner();
	}
	if (showEditor.value) {
		closeEditor();
	} else {
		openEditor();
	}
}
//打开扫码器
const openScanner = () => {
	showCodeScanner.value = true;
	active.value = "scanner";
	openCamera();
}
const closeScanner = () => {
	showCodeScanner.value = false;
	active.value = "";
	closeCamera();
}
//调用摄像头
const openCamera = () => {
	codeReader.value.getVideoInputDevices().then((videoInputDevices) => {
		tipMsg.value = "正在调用摄像头...";
		let firstDeviceId = videoInputDevices[0].deviceId;
		if (videoInputDevices.length > 1) {
			let deviceLength = videoInputDevices.length;
			--deviceLength;
			firstDeviceId = videoInputDevices[deviceLength].deviceId;
		}
		decodeFromInputVideoFunc(firstDeviceId);
	}).catch((err) => {
		tipMsg.value = JSON.stringify(err);
		console.error(err);
	});
}
//扫描回调函数
const decodeFromInputVideoFunc = (firstDeviceId) => {
	codeReader.value.reset(); // 重置
	codeReader.value.decodeFromInputVideoDeviceContinuously(firstDeviceId, "video", (result, err) => {
		tipMsg.value = "正在尝试识别...";
		if (result) {
			boxCode.value = result.text;
			tipMsg.value = "识别成功:" + boxCode.value;
			console.log(boxCode.value)
			openBox();
		}
		if (err && !err) {
			tipMsg.value = JSON.stringify(err);
			console.error(err);
		}
	});
}
//关闭摄像头
const closeCamera = () => {
	codeReader.value.stopContinuousDecode();
	codeReader.value.reset();
}
//调用后台转运接口
const transferBox = () => {
	api.post("/box/transferBox", {
		boxCode: boxCode.value
	})
		.then(res => {
			getBoxList();
			closeEditor();
		})
}

//定义底部工具栏高度
const tabbarThemeVars = ref({
	tabbarHeight: "6rem"
})
const boxList = ref([]);
//获取已转运箱码列表
const getBoxList = () => {
	api.post("/box/getNeedTransferBoxList")
		.then(res => {
			boxList.value = res.data;
			loading.value = false;
			finished.value = true;
			refreshing.value = false;
		}).catch(err => {
			loading.value = false;
			finished.value = true;
			refreshing.value = false;
		})
}
const loading = ref(false);
const refreshing = ref(false);
const finished = ref(false);
const onRefresh = () => {
	loading.value = true;
	finished.value = false;
	getBoxList();
};
const toChangePwd = () => {
	router.push("/ChangePwd");
}
</script>

<template>
	<div>
		<van-nav-bar title="全场景疫情病原体检测信息系统" right-text="修改密码" @click-right="toChangePwd" left-arrow
			@click-left="logout">
		</van-nav-bar>
		<van-row>
			<van-col span="24" style="padding: 1rem;">
				<h1>{
   
   { transfer.name }},您好。待转运{
   
   { boxList.length }}箱</h1>
			</van-col>
		</van-row>
		<van-pull-refresh class="fullheight" v-model="refreshing" @refresh="onRefresh">
			<van-list :finished="finished" :loading="loading" finished-text="没有更多了" @load="getBoxList">
				<van-cell v-for="item in boxList" center :key="item" :title="item">
					<template #title>
						<span class="boxCode">{
   
   { item.boxCode }}</span>
						<br>
						<span class="count">试管数:{
   
   { item.testTubeCount }};</span>
						<span class="count">样品数:{
   
   { item.peopleCount }};</span>
						<span class="count">采集人:{
   
   { item.collector }}</span>
						<span class="count">[{
   
   { item.openTime }}]</span>
					</template>
				</van-cell>
			</van-list>

			<div class="van-safe-area-bottom"></div>
		</van-pull-refresh>

		<van-overlay :show="showCodeScanner" class="scanner">
			<video ref="video" id="video" class="scan-video" autoplay></video>
			<div>{
   
   { tipMsg }}</div>
		</van-overlay>
		<van-overlay :show="showEditor">
			<div class="scan-video"></div>
			<van-cell-group inset>
				<van-row class="padding">
					<van-col span="24">
						<van-field v-model="boxCode" label="输入箱码" placeholder="请输入箱码" />
					</van-col>
				</van-row>
				<van-row class="padding">
					<van-col span="24">
						<van-button round block type="primary" @click="transferBox">确定转运</van-button>
					</van-col>
				</van-row>
			</van-cell-group>
		</van-overlay>

		<van-config-provider :theme-vars="tabbarThemeVars">
			<van-tabbar v-model="active">
				<van-tabbar-item @click="switchScanner" name="scanner">
					<span>扫码转运</span>
					<template #icon="props">
						<van-icon class-prefix="iconfont i-saoma" name="extra" size="3rem" />
					</template>
				</van-tabbar-item>
				<van-tabbar-item @click="switchEditor" name="editor">
					<span>手动转运</span>
					<template #icon="props">
						<van-icon name="edit" size="3rem" />
					</template>
				</van-tabbar-item>
			</van-tabbar>
		</van-config-provider>
	</div>
</template>
<style>
.t-center {
	text-align: center;
}

.fullheight {
	height: 80vh;
}

.scan-video {
	width: 100%;
	height: 50vh;
}

.scanner {
	color: #fff;
}

.padding {
	padding: 1rem;
}

.boxCode {
	font-size: 1.5rem;
}
</style>

Есть два внутренних интерфейса:

Один из них — получить список перегруженных ящиков, но он не может содержать коды ящиков, которые получило агентство по тестированию. Адрес интерфейса: /box/getNeedTransferBoxList.

@RestController
@RequestMapping("/box")
public class BoxController {
    
    

    @Autowired
    IBoxService boxService;

    @TransferApi
    @PostMapping("getNeedTransferBoxList")
    public ResultModel<List<BoxVO>> getNeedTransferBoxList() throws BusinessException {
    
    
        List<BoxVO> list = boxService.getNeedTransferBoxList();
        return ResultModel.success(list);
    }
  	....
}

Этот интерфейс очень прост, это условный запрос, и когда он возвращается, он должен иметь возможность отображать коллектор раздаточной коробки, время распаковки, количество пробирок, количество образцов и т. д., поэтому оператор SQL должен быть объединен несколькими таблицами. Давайте посмотрим на оператор SQL:


    <select id="getBoxListByTransferId" resultType="com.hawkon.other.pojo.vo.BoxVO">
        select b.*
             , c.name  as collector
             , tf.name as transfer
             , r.name  as reciever
             , u.name  as uploader
             , (select count(0) from testTube where testTube.boxId = b.boxId) 
                       as testTubeCount
             , (select count(0)
                from testTube tt inner join sample s on tt.testTubeId = s.testTubeId
                where tt.boxId = b.boxId)  
                       as peopleCount
        from box b
                 left join collector c on b.collectorId = c.collectorId
                 left join transfer tf on b.transferId = tf.transferId
                 left join reciever r on b.recieverId = r.recieverId
                 left join uploader u on b.uploaderId = u.uploaderId
        where b.transferId = #{transferId} and status = 3
    </select>

Статистика двух величин в приведенном выше операторе SQL использует два подзапроса. Это то, на что стоит обратить внимание.

Еще одна вещь, которую следует отметить в отношении этого интерфейса, заключается в том, что основным содержимым возвращаемого результата является Box, но есть еще одно поле, которое не является содержимым Box, но было расширено.В этом случае как насчет возвращаемого класса сущности? На самом деле это очень просто, достаточно определить класс VO и позволить ему наследовать Box. Конечно, если вы не считаете, что кодов слишком много, можно также написать один класс VO.

package com.hawkon.other.pojo.vo;

import com.hawkon.common.pojo.Box;
import lombok.Data;

@Data
public class BoxVO extends Box {
    
    
    private String collector;
    private String transfer;
    private String pointName;
    private Integer testTubeCount;
    private Integer peopleCount;
}

Другой - отправить код коробки для передачи, адрес: /box/transferBox

@RestController
@RequestMapping("/box")
public class BoxController {
    
    

    @Autowired
    IBoxService boxService;

    @TransferApi
    @PostMapping("getNeedTransferBoxList")
    public ResultModel<List<BoxVO>> getNeedTransferBoxList() throws BusinessException {
    
    
        List<BoxVO> list = boxService.getNeedTransferBoxList();
        return ResultModel.success(list);
    }

    @TransferApi
    @PostMapping("transferBox")
    public ResultModel<Object> transferBox(@RequestBody Box model) throws BusinessException {
    
    
        boxService.transferBox(model);
        return ResultModel.success(null);
    }
  	....
}

Код сервисного уровня выглядит следующим образом:

    @Override
    public void transferBox(Box model) throws BusinessException {
    
    
      //转运前务必做好状态判断
        if (model.getBoxCode() == null) {
    
    
            throw new BusinessException("转运箱码不能为空", ResultCodeEnum.BUSSINESS_ERROR);
        }
        Box boxDb = boxDao.getBoxByBoxCode(model.getBoxCode());
        if (boxDb == null) {
    
    
            throw new BusinessException("转运箱码无效", ResultCodeEnum.BUSSINESS_ERROR);
        }
        if(boxDb.getStatus().equals(1)){
    
    
            throw new BusinessException("转运箱码未封箱",ResultCodeEnum.BUSSINESS_ERROR);
        }
        if(boxDb.getStatus().equals(3)){
    
    
            throw new BusinessException("转运箱码已接收",ResultCodeEnum.BUSSINESS_ERROR);
        }
        if(boxDb.getStatus()>3){
    
    
            throw new BusinessException("转运箱码已完成转运",ResultCodeEnum.BUSSINESS_ERROR);
        }
        Transfer transfer = SessionUtil.getCurrentTransfer();
        boxDb.setTransferId(transfer.getTransferId());
        boxDao.transferBox(boxDb);
    }

Маппер файл:


    <update id="transferBox">
        update box
        set status=3
          , transferId=#{transferId}
          , transferTime=now()
        where boxId = #{boxId}
    </update>

Окончательный эффект очень прост:

вставьте сюда описание изображения

Получатель

Основным объектом работы модуля приемника также является поворотный ящик, поэтому инженерная структура на этом конце точно такая же, как и у передаточного персонала, и интерфейс такой же. Но есть разница в функциях.

вставьте сюда описание изображения

Интерфейс работы получателя:

вставьте сюда описание изображения

Внутренний код также в основном такой же, только немного отличается в оценке состояния на уровне службы.

@Override
public void recieveBox(Box model) throws BusinessException {
    
    
    if (model.getBoxCode() == null) {
    
    
        throw new BusinessException("转运箱码不能为空", ResultCodeEnum.BUSSINESS_ERROR);
    }
    Box boxDb = boxDao.getBoxByBoxCode(model.getBoxCode());
    if (boxDb == null) {
    
    
        throw new BusinessException("转运箱码无效", ResultCodeEnum.BUSSINESS_ERROR);
    }
    if(boxDb.getStatus()<=2){
    
    
        throw new BusinessException("转运箱码未转运",ResultCodeEnum.BUSSINESS_ERROR);
    }
    if(boxDb.getStatus()>=4){
    
    
        throw new BusinessException("转运箱码已接收",ResultCodeEnum.BUSSINESS_ERROR);
    }
    Reciever reciever = SessionUtil.getCurrentReciever();
    boxDb.setRecieverId(reciever.getRecieverId());
    boxDb.setTestOrganizationId(reciever.getOrganizationId());
    boxDao.recieveBox(boxDb);
}

Оператор SQL в Mapper также немного отличается:

<update id="recieveBox">
    update box
    set status=4
      , recieverId=#{recieverId}
      , testOrganizationId = #{testOrganizationId}
      , recieveTime=now()
    where boxId = #{boxId}
</update>

загрузчик

Должен признать, что эта глава — относительно скучная часть серии: код экспедитора и приемника почти одинаков, с небольшой разницей в бэкенде.

Есть еще о чем поговорить о функции выгрузки кадров. Во-первых, давайте посмотрим, какие функции мы добавили на сторону загрузчика:

вставьте сюда описание изображения

В основном это включает в себя: печать штрих-кодов, пробирки для проверки, загрузку данных и управление учетными записями получателей.

Сначала посмотрите на функцию печати штрих-кода. Из-за этих функций нашей добавки для мозга нам все еще необходимо учитывать, как персонал по профилактике эпидемий использует ее в реальной работе при разработке.

Штрих-коды, которые необходимо распечатать, включают коды коробок и коды пробирок.Когда я был волонтером, я наблюдал штрих-коды кодов коробок и кодов пробирок и не нашел очевидных следов кодирования.

При разработке функции печати необходимо учитывать следующие вопросы:

  • Затем, когда мы делаем это, мы предполагаем, что эти штрих-коды генерируются автоматически, и при печати штрих-кодов могут возникнуть некоторые проблемы, например, половина распечатанной бумаги закончилась, принтер сломался и задача печати не выполняется. печатать повторно.
  • При печати будет распечатано много листов за раз
  • Необходимо распечатать как код коробки, так и код пробирки.
  • проблема с печатью в браузере

Исходя из вышеуказанных соображений, мы добавили в нашу базу данных таблицу задач печати для хранения и печати задач, а также в качестве основы для создания задач печати при повторной печати.

Имя таблицы называется: code_apply_record, а имя таблицы MYSQL по умолчанию нечувствительно к регистру, поэтому лучше всего использовать символы подчеркивания, чтобы различать слова при именовании имени таблицы. Структура таблицы следующая:

вставьте сюда описание изображения

Таким образом, каждый раз, когда необходимо напечатать новый штрих-код, введите количество для печати, и штрих-код соответствующих данных будет сгенерирован автоматически, а примененные данные будут сохранены в таблице code_apply_record. Если требуется повторная печать, ее можно перепечатать по этим параметрам.

Здесь я еще представил себе функцию: если распечатанный штрих-код не используется, его можно распечатать заново.

Кроме того, существует также проблема печати в браузере, из-за особенностей браузеров, если вы печатаете напрямую, некоторая информация о веб-странице будет напечатана на бумаге по умолчанию, например, адрес, заголовок страницы и т. д., а различные браузеры не очень хорошо То же самое. В реальной разработке вызов принтеров с веб-страниц всегда был головной болью.

Некоторые разработают собственные подключаемые модули для печати, а некоторые купят компоненты для печати страниц и так далее. По моему опыту, в реальном использовании есть очень удобный в использовании коммерческий компонент lodop , и мы используем это решение в этом проекте.

Поскольку необходимо вызвать компонент печати, загрузчик должен использовать сторону ПК. Компоненты на стороне ПК на основе Vue в настоящее время elementUI Plusиспользуются , почему он называется plus, потому что Vue3.0 внес относительно большие изменения в Vue, и elementUI Plusэто компонент, соответствующий Vue3.0.

Таким образом, интерфейсный модуль необходимо повторно интегрировать, на самом деле vantзаменить его elementUI Plusнесложно .

интегрироватьelementUI Plus

Поскольку предыдущие модули построили базовую структуру, теперь нам просто нужно заменить vant на elementUI, чтобы мы могли вносить коррективы на основе других модулей.

Для начала скопируйте код модуля исходного проекта.Обратите внимание, что папку node_modules при копировании приносить не нужно, т.к. ее нужно переконфигурировать после настройки npm install. Как показано на рисунке ниже, представления содержат коды бизнес-логики, поскольку ранее использовался vant, поэтому он бесполезен и может быть удален напрямую.

вставьте сюда описание изображения

Для других файлов начнем package.jsonс настроек. Эту "vant": "^3.6.5",строку можно удалить, и есть "@zxing/library": "^0.19.1",другие компоненты, которые можно использовать, поэтому удалять ее нет необходимости.

  "dependencies": {
    
    
    "@vitejs/plugin-basic-ssl": "^0.1.2",
    "@zxing/library": "^0.19.1",
    "axios": "^1.1.3",
    "dayjs": "^1.11.6",
    "vant": "^3.6.5",
    "vue": "^3.2.38",
    "vue-cookie": "^1.1.4",
    "vue-router": "^4.1.5"
  },

Далее вы можете установить elemntUI Plus, установить команду:npm install element-plus --save

После установки package.jsonзависимости станут такими:

  "dependencies": {
    
    
    "@vitejs/plugin-basic-ssl": "^0.1.2",
    "axios": "^1.1.3",
    "dayjs": "^1.11.6",
    "element-plus": "^2.2.26",
    "vue": "^3.2.38",
    "vue-cookie": "^1.1.4",
    "vue-router": "^4.1.5"
  }

Преимущество использования команды для установки заключается в том, что она автоматически генерирует версию.Конечно, вы также можете напрямую изменить файл, а затем выполнить унифицированную установку package.jsonпосле завершения модификации .npm install

импорт по требованию

elementUI PlusОн может быть импортирован полностью или импортирован по требованию, разница в том, что окончательный JS-файл полного импорта будет относительно большим, и если он импортируется по требованию, он может быть упакован столько, сколько он используется. Для наших небольших проектов импорт по требованию является более подходящим способом.

Для начала нужно установить unplugin-vue-componentsи unplugin-auto-importвот эти два плагина

npm install -D unplugin-vue-components unplugin-auto-import

Затем вставьте следующий код в vite.config.jsфайл конфигурации

// vite.config.ts
import {
    
     defineConfig } from 'vite'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import {
    
     ElementPlusResolver } from 'unplugin-vue-components/resolvers'

export default defineConfig({
    
    
  // ...
  plugins: [
    // ...
    AutoImport({
    
    
      resolvers: [ElementPlusResolver()],
    }),
    Components({
    
    
      resolvers: [ElementPlusResolver()],
    }),
  ],
})

из коробки

Мы также можем добавить готовые функции, чтобы вы могли писать elementUI Plusтеги напрямую.

Добавьте в vite.config.js, обратите внимание, unplugin-element-plus/viteчто он должен быть установлен:npm install unplugin-element-plus/vite

// vite.config.ts
import {
    
     defineConfig } from 'vite'
import ElementPlus from 'unplugin-element-plus/vite'

export default defineConfig({
    
    
  // ...
  plugins: [ElementPlus()],
})

Чтобы использовать готовую функцию, вам нужно сослаться на глобальный стиль в main.js:

import 'element-plus/dist/index.css'	

КорректированиеApi.js

Зачем ссылаться на стили? Потому что наш интерфейсный фреймворк axiosпри инкапсуляции использует рамку подсказки. Раньше она использовалась vant, но теперь ее нужно заменить elemtnUI Plus. Если стиль не указывать в кавычках, окно подсказки не может отображаться нормально.

Затем в файл api.js, который инкапсулирует axios, добавьте следующую строку, чтобы представить окно подсказки:

import {
    
     ElMessage } from 'element-plus'

Затем измените его на api.js, а остальные менять не нужно. Toast.fail("服务器错误");ElMessage.error("服务器错误");

Авторизоваться

Страница входа в основном такая же, как и в других модулях. Из-за готовой функции очень удобно писать теги без ссылок на компоненты.

<script setup>
import {
	reactive,
	ref
} from 'vue';
import {
	RouterLink, useRouter
} from 'vue-router';
import api from "../common/api.js";
const router = useRouter();
const loginForm = reactive({
	tel: '18911110000',
	password: '011109',
})
const formRef = ref(null);
const rules = ref({
	tel: [{ required: true, message: '请填写手机号' }],
	password: [{ required: true, message: '请填写密码' }]
});
const login = (formEl) => {
	formEl.validate((valid, fields) => {
		if (valid) {
			api.post("/uploader/login",loginForm)
			.then(res=>{
				sessionStorage["user"] = JSON.stringify(res.data);
				router.push("/Home");
			})
		} 
	})
}
</script>

<template>
	<div id="login">
		<div class="form">
			<h2>核酸检测平台-数据上传人员登录</h2>
			<el-form ref="formRef" label-position="right" label-width="80" :model="loginForm" :rules="rules">
				<el-form-item label="手机号" required prop="tel">
					<el-input v-model="loginForm.tel" placeholder="登录手机号" />
				</el-form-item>
				<el-form-item label="密码" required prop="password">
					<el-input v-model="loginForm.password" type="password"  placeholder="请输入密码" />
				</el-form-item>
				<el-form-item>
					<el-button type="primary" @click="login(formRef)">登录</el-button>
					<RouterLink to="/Forget">忘记密码</RouterLink>
				</el-form-item>
			</el-form>
		</div>
	</div>
</template>
<style scoped>
#login {
	display: flex;
	place-items: center;
	width: 100vh;
	height: 100vh;
}

.form {
	display: grid;
	grid-template-columns: 1fr;
	padding: 0 2rem;
	margin: 0 auto;
}
</style>

//App.vue
<script setup>
import { RouterLink, RouterView } from 'vue-router'
</script>

<template>
  <RouterView />
</template>

<style scoped>
</style>

app.vueФайл на самом деле является эквивалентом vueконтейнера маршрутизации, и его не нужно перемещать.

Страница фоновой рамки

Разница между фоновой страницей на стороне ПК и страницей на стороне мобильного телефона заключается в том, что в основном это будет страница с фреймом, включая ЛОГОТИП системы, меню верхнего уровня, операции входа в учетную запись и т.п.

Наша страница фрейма относительно проста и предназначена только для справки.

Home.vue

<template>
	<el-container>
		<el-header>
			<el-menu :default-active="activeIndex" mode="horizontal" :ellipsis="false" @select="handleSelect">
				<el-menu-item index="0">核酸检测平台-检测机构数据上传</el-menu-item>
				<div class="flex-grow" />
				<el-menu-item index="PrintCode">打印条码</el-menu-item>
				<el-menu-item index="TestTubeList">待检试管</el-menu-item>
				<el-menu-item index="Upload">数据上传</el-menu-item>
				<el-menu-item index="Reciever">接收人员管理</el-menu-item>
				<el-sub-menu index="userInfo">
						<template #title>{
   
   {user.name}}</template>
					<el-menu-item index="ChangePwd">修改密码</el-menu-item>
					<el-menu-item index="Logout" @click="logout">退出</el-menu-item>
				</el-sub-menu>
			</el-menu>
		</el-header>
		<el-main>
			<RouterView></RouterView>
		</el-main>
	</el-container>
</template>
<script lang="ts"  setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router';
import api from "../common/api";
const router = useRouter();
const activeIndex = ref('TestTubeList')
const handleSelect = (key: string, keyPath: string[]) => {
	router.push(key);
}
activeIndex.value = router.currentRoute.value.name;
let user = JSON.parse(sessionStorage["user"]);
const userInfo = ref(user);
const logout = ()=>{
	sessionStorage.clear();
	api.post("/uploader/logout")
	.then(()=>{
		router.push("/");
	})
}
</script>

<style>
.header {
	background-color: #76aee6;
	color: #fff;
}

.flex-grow {
	flex-grow: 1;
}
</style>

Привязывая события для меню на странице, реализуется унифицированный переход маршрутизации.


const handleSelect = (key: string, keyPath: string[]) => {
    
    
	router.push(key);
}

распечатать штрих-код

Во внешнем интерфейсе было сказано, что lodop можно использовать для печати, а также очень просто использовать lodop в vue.Во-первых, необходимо сослаться на js-файл lodop.

Есть два способа цитирования, более простой — index.htmlпрямое цитирование фронтенд-проекта.

Строка 8 в следующем коде такова. Затем, когда его нужно вызвать в бизнес-коде, его можно использовать в соответствии с официальным документом lodop. Именно этот метод используется в нашем проекте.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <link rel="icon" href="/favicon.ico">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>核酸检测平台数据上传人员</title>
    <script src="LodopFuncs.js"></script>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>

Другой — преобразовать файл lodop в библиотеку классов модуля экспорта. Поскольку lodop официально закодирован в традиционном javascript, он не поддерживает модуль экспорта es6, поэтому после загрузки официального файла lodop вам необходимо добавить следующий код в конец lodopFunc.js:

export {
    
     getLodop }; //导出getLodop

Затем скопируйте измененный файл lodopFuncs.js в папку вашего проекта. При вызове компонента печати в vue его можно вызывать, как и другие js-библиотеки:

<template>
    <div class="Print">
        <button class="print-btn" v-on:click="btnClickPrint">
            <span>{
   
   {msg}}</span>
        </button>
    </div>
</template>
<script>
import { getLodop } from '../assets/LodopFuncs' //导入模块
export default {
    name: 'Print',
    data () {
     return {
        msg: '点击按钮打印'
        }
    },
methods: {
    btnClickPrint: function () {
        let LODOP = getLodop()//调用getLodop获取LODOP对象
        LODOP.PRINT_INIT("")
        LODOP.ADD_PRINT_TEXT(50, 231, 260, 39, "打印内容")
        LODOP.PREVIEW()
        //LODOP.PRINT()
          }
     }
  }
</script>

При разработке функции печати вы можете использовать смоделированный принтер для преобразования материалов для печати в PDF для тестирования, что может значительно снизить сложность разработки и отладки.

Распечатать передний штрих-код

<template>
	<div>
		<h1>打印箱码</h1>
		<el-form :model="form" :inline="true">
			<el-form-item label="条码类型">
				<el-radio-group v-model="form.codeType">
					<el-radio size="large" :label="1" border>箱码</el-radio>
					<el-radio size="large" :label="2" border>试管码</el-radio>
				</el-radio-group>
			</el-form-item>
			<el-form-item label="生成箱码数量">
				<el-input v-model="form.codeCount" placeholder="请输入箱码数量"></el-input>
			</el-form-item>
			<el-form-item>
				<el-button type="primary" @click="printCodeApply()">生成并打印</el-button>
			</el-form-item>
		</el-form>
		<el-pagination :current-page="pagedSearchModel.page" :page-size="pagedSearchModel.size"
			:hide-on-single-page="false" :page-sizes="[10, 50, 100, 500]" background
			layout="total, sizes, prev, pager, next, jumper" :total="pagedSearchModel.total"
			@size-change="handleSizeChange" @current-change="handleCurrentChange">
		</el-pagination>

		<el-table :data="codeApplyRecorders" border style="width: 100%">
			<el-table-column prop="codeType" label="条码类型" width="120">
				<template #default="scope">
					{
   
   { scope.row.codeType == 1 ? "箱码" : "试管码" }}
				</template>
			</el-table-column>
			<el-table-column prop="organizationName" label="机构" width="180" />
			<el-table-column prop="uploader" label="打印人" />
			<el-table-column prop="applyTime" label="申请时间" />
			<el-table-column prop="beginCode" label="开始码" />
			<el-table-column prop="endCode" label="结束码" />
			<el-table-column prop="unUsedCount" label="未使用数量" />
			<el-table-column>
				<template #header>
					<el-button type="success" :icon="Refresh" @click="getCodeApplyRecords()"></el-button>
				</template>
				<template #default="scope">
					<el-button type="success" @click="printAgain(scope.row)">重新打印</el-button>
				</template>
			</el-table-column>
		</el-table>
		<el-dialog v-model="printDialogVisabel" title="打印条码">
			<el-select v-model="currentPrinter.printerIndex" class="m-2" placeholder="Select" size="large">
				<el-option v-for="printer in printerList" :key="printer.printerIndex" :label="printer.name"
					:value="printer.printerIndex" />
			</el-select>
			<el-divider content-position="left">打印{
   
   { (printModel.codeType == 1 ? "箱码" : "试管码") }}</el-divider>
			<el-tag v-for="code in printModel.codeList" :key="code" type="" effect="plain" round>
				{
   
   { code }}
			</el-tag>

			<template #footer>
				<span class="dialog-footer">
					<el-button @click="printDialogVisabel = false">取消</el-button>
					<el-button type="primary" @click="printCode">
						打印
					</el-button>
				</span>
			</template>
		</el-dialog>
	</div>
</template>
<script lang="ts"  setup>
import { ref, reactive } from 'vue'
import common from '../common/common.js';
import api from "../common/api.js";
import { Refresh } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'

const form = ref({ codeType: 1, codeCount: 0 });
const pagedSearchModel = ref({
	size: 10,
	page: 1,
	total: 0
});
const printModel = ref({});
const codeApplyRecorders = ref([]);
const printCodeApply = () => {
	api.post('/code/printCode', form.value)
		.then(res => {
			printModel.value = res.data;
			form.value = { codeType: form.value.codeType, codeCount: 0 }
			getCodeApplyRecords();
			openPrintDialog();
		})
}
const LODOP = getLodop();
const printerList = ref([]);
const currentPrinter = ref({});
const printDialogVisabel = ref(false);
const openPrintDialog = () => {
  //读取打印机列表,以便选择打印。
	var iPrinterCount = LODOP.GET_PRINTER_COUNT();
	printerList.value = [];
	for (var i = 0; i < iPrinterCount; i++) {
		printerList.value.push({ name: LODOP.GET_PRINTER_NAME(i), printerIndex: i });
	};
	printDialogVisabel.value = true;
	currentPrinter.value = printerList.value[0];
}
const printCode = () => {
	var codeList = printModel.value.codeList;
  //初始化打印机
	LODOP.PRINT_INIT("");
  //设置打印纸张为宽7cm,高4cm的标签纸
	LODOP.SET_PRINT_PAGESIZE(0, "7cm", "4cm", "标签纸");
  //设置采用哪个打印机打印。
	LODOP.SET_PRINTER_INDEX(currentPrinter.value.printerIndex);
  //根据条码类型的不同在条码上打印“箱码”或“试管码”字样,防止混乱。
	var codeType = printModel.value.codeType==1?"箱码":"试管码";
	for (var i = 0; i < codeList.length; i++) {
		console.log(codeList[i])
    LODOP.ADD_PRINT_TEXT("5mm", "5mm", "60mm", "5mm", codeType);
		LODOP.ADD_PRINT_BARCODE("10mm", "5mm", "60mm", "20mm", "Codabar", codeList[i]);
		LODOP.NEWPAGE();
	}
	LODOP.PRINT();
}
const printAgain = (row) => {
	api.post('/code/getCodeByCodeApplyRecordId', row)
		.then(res => {
			printModel.value = res.data;
			openPrintDialog();
		})
}
const getCodeApplyRecords = () => {
	api.post('/code/getCodeApplyRecords', pagedSearchModel.value)
		.then(res => {
			codeApplyRecorders.value = res.data.list;
			pagedSearchModel.value.total = res.data.total;
		})
}
getCodeApplyRecords();
const handleSizeChange = (val) => {
	pagedSearchModel.value.size = val;
	getCodeApplyRecords();
}
const handleCurrentChange = (val) => {
	pagedSearchModel.value.page = val;
	getCodeApplyRecords();
}
</script>

<style>

</style>

внутренний код

package com.hawkon.other.controller;

import com.hawkon.common.exception.BusinessException;
import com.hawkon.common.pojo.BasePagedSearchModel;
import com.hawkon.common.pojo.ResultModel;
import com.hawkon.common.pojo.vo.PagedResult;
import com.hawkon.other.common.UploaderApi;
import com.hawkon.other.pojo.CodeApplyRecord;
import com.hawkon.other.pojo.bo.GetCodeApplyRecordsModel;
import com.hawkon.other.pojo.bo.PrintCodeMobel;
import com.hawkon.other.pojo.vo.CodeApplyRecordVO;
import com.hawkon.other.service.ICodeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/code")
public class CodeController {
    
    

    @Autowired
    ICodeService codeService;
    @UploaderApi
    @PostMapping("printCode")
    public ResultModel<CodeApplyRecord> printCode(@RequestBody PrintCodeMobel model) throws BusinessException {
    
    
        CodeApplyRecord result = codeService.printCode(model);
        return ResultModel.success(result);
    }

    @UploaderApi
    @PostMapping("getCodeApplyRecords")
    public ResultModel<PagedResult<CodeApplyRecord>> getCodeApplyRecords(@RequestBody GetCodeApplyRecordsModel model) throws BusinessException {
    
    
        PagedResult<CodeApplyRecord> result =  codeService.getCodeApplyRecords(model);
        return ResultModel.success(result);
    }
    @UploaderApi
    @PostMapping("getCodeByCodeApplyRecordId")
    public ResultModel<CodeApplyRecord> getCodeByCodeApplyRecordId(@RequestBody CodeApplyRecord model) throws BusinessException {
    
    
        CodeApplyRecordVO result =  codeService.getCodeByCodeApplyRecordId(model.getRecordId());
        return ResultModel.success(result);
    }
}

package com.hawkon.other.service.impl;

import com.hawkon.common.enums.ResultCodeEnum;
import com.hawkon.common.exception.BusinessException;
import com.hawkon.common.pojo.Box;
import com.hawkon.common.pojo.TestTube;
import com.hawkon.common.pojo.Uploader;
import com.hawkon.common.pojo.vo.PagedResult;
import com.hawkon.other.dao.BoxDao;
import com.hawkon.other.dao.CodeApplyRecordDao;
import com.hawkon.other.dao.TestTubeDao;
import com.hawkon.other.pojo.CodeApplyRecord;
import com.hawkon.other.pojo.bo.GetCodeApplyRecordsModel;
import com.hawkon.other.pojo.bo.PrintCodeMobel;
import com.hawkon.other.pojo.vo.CodeApplyRecordVO;
import com.hawkon.other.service.ICodeService;
import com.hawkon.other.utils.SessionUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

@Service
public class CodeServiceImpl implements ICodeService {
    
    
    @Autowired
    BoxDao boxDao;

    @Autowired
    TestTubeDao testTubeDao;
    @Autowired
    CodeApplyRecordDao codeApplyRecordDao;

    @Transactional
    @Override
    public CodeApplyRecordVO printCode(PrintCodeMobel model) throws BusinessException {
    
    
        if (model.getCodeCount() <= 0) {
    
    
            throw new BusinessException("条码数量必须大于0", ResultCodeEnum.BUSSINESS_ERROR);
        }
        Uploader uploader = SessionUtil.getCurrentUploader();
        CodeApplyRecordVO applyRecord = new CodeApplyRecordVO();
        applyRecord.setUploaderId(uploader.getUploaderId());
        applyRecord.setOrganizationId(uploader.getOrganizationId());
        applyRecord.setCodeType(model.getCodeType());
        if (model.getCodeType().equals(1)) {
    
    
            //箱码走这里
            Long maxBoxCode = boxDao.getMaxBoxCode();
            Long beginCode = maxBoxCode + 1;
            applyRecord.setBeginCode(beginCode);
            applyRecord.setEndCode(beginCode + model.getCodeCount() - 1);
            //生成箱码对象。
            List<Box> boxList = new ArrayList<>();
            for (long i = beginCode; i <= applyRecord.getEndCode(); i++) {
    
    
                Box box = new Box();
                box.setBoxCode(i);
                boxList.add(box);
            }
            boxDao.insertBoxList(boxList);
            applyRecord.setCodeList(boxList.stream().map(item -> item.getBoxCode()).collect(Collectors.toList()));
        } else if (model.getCodeType().equals(2)) {
    
    
            //试管码走这里
            Long maxTestTubeCode = testTubeDao.getMaxTestTubeCode();
            Long beginCode = maxTestTubeCode + 1;
            applyRecord.setBeginCode(beginCode);
            applyRecord.setEndCode(beginCode + model.getCodeCount() - 1);
            //生成试管码对象。
            List<TestTube> testTubeList = new ArrayList<>();
            for (long i = beginCode; i <= applyRecord.getEndCode(); i++) {
    
    
                TestTube testTube = new TestTube();
                testTube.setTestTubeCode(i);
                testTubeList.add(testTube);
            }
            testTubeDao.insertTestTubeList(testTubeList);
            applyRecord.setCodeList(testTubeList.stream().map(item -> item.getTestTubeCode()).collect(Collectors.toList()));
        }
        codeApplyRecordDao.insertRecord(applyRecord);
        return applyRecord;
    }

    @Override
    public PagedResult<CodeApplyRecord> getCodeApplyRecords(GetCodeApplyRecordsModel model) throws BusinessException {
    
    
        Uploader uploader = SessionUtil.getCurrentUploader();
        model.setOrganizationId(uploader.getOrganizationId());
        List<CodeApplyRecord> list = codeApplyRecordDao.getCodeApplyRecords(model);
        int count = codeApplyRecordDao.getCodeApplyRecordsCount(model);
        PagedResult<CodeApplyRecord> result = new PagedResult<>(model.getPage(), count, model.getSize(), list);
        return result;
    }

    @Override
    public CodeApplyRecordVO getCodeByCodeApplyRecordId(Integer recordId) {
    
    
        CodeApplyRecordVO record = codeApplyRecordDao.getCodeApplyRecordById(recordId);
        if (record.getCodeType().equals(1)) {
    
    
            List<Box> list = boxDao.getBoxByRecord(record);
            record.setCodeList(list.stream().map(item -> item.getBoxCode()).collect(Collectors.toList()));
        } else {
    
    
            List<TestTube> list = testTubeDao.getTestTubeListByRecord(record);
            record.setCodeList(list.stream().map((item -> item.getTestTubeCode())).collect(Collectors.toList()));
        }
        return record;
    }
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.hawkon.other.dao.CodeApplyRecordDao">
    <select id="getCodeApplyRecords" resultType="com.hawkon.other.pojo.vo.CodeApplyRecordVO">
        select r.*,u.name  as uploader,o.organizationName
        ,(select count(0) from box where box.boxCode between r.beginCode and r.endCode and status =0 )
             +(select count(0) from testTube t where t.testTubeCode between r.beginCode and r.endCode and status =0 )
        as unUsedCount
        from code_apply_record r
        left join uploader u on r.uploaderId = u.uploaderId
        left  join organization o on o.organizationId = r.organizationId
        where r.organizationId = #{organizationId}
        limit #{rowBegin},#{size}
    </select>
    <select id="getCodeApplyRecordsCount" resultType="int">
        select Count(0)
        from code_apply_record r
        where r.organizationId = #{organizationId}
    </select>
    <select id="getCodeApplyRecordById" resultType="com.hawkon.other.pojo.vo.CodeApplyRecordVO">
        select *
        from code_apply_record r
        where r.recordId = #{recordId}
    </select>
    <insert id="insertRecord">
        INSERT INTO code_apply_record
            ( codeType, organizationId, uploaderId, applyTime, beginCode, endCode)
        VALUES
           ( #{codeType}, #{organizationId}, #{uploaderId}, now(), #{beginCode}, #{endCode});
    </insert>
</mapper>

распечатать демо

вставьте сюда описание изображения

Тестовая пробирка для тестирования (экспорт файла Excel)

Загрузчик данных отвечает за выгрузку результатов тестирования.При реальном тестировании в большинстве случаев положительных случаев в городе может быть очень мало, но после начала эпидемии положительных случаев может быть довольно много. Чтобы облегчить загрузчику ввод результатов теста, мы можем экспортировать все коды пробирок, которые необходимо загрузить.

После ручного редактирования и изменения результатов обнаружения загрузите результаты, чтобы завершить загрузку.

Проверяемая страница пробирки должна экспортировать шаблон загрузки Excel.Экспортируемый файл шаблона должен содержать код пробирки, который необходимо загрузить.Поэтому этот файл Excel не является пустым шаблоном, но данные в базе данных должны быть быть заполненным в excel.

Когда дело доходит до чтения и письма в Excel, мы должны упомянуть компонент poi, который очень удобен в использовании, но после преобразования Alibaba он превратился в EasyExcel, который еще более приятен в использовании, поэтому мы используем EasyExcel для его реализации. .

Как загружать файлы с помощью API в стиле Rest?

При реализации этой функции мы должны обратить внимание на проблему, потому что мы должны обратить внимание на то, что экспорт Excel заключается в загрузке файла, а наш apiинтерфейс весь restв стиле.Вы должны знать, что jsonэто определенно невозможно использовать формат при скачивании файла Его httpголова Content-type в сообщении не может быть application/json, но есть application/octet-stream. Что с этим делать?

В этом случае нам нужна специальная обработка.Когда axios запрашивает результат воздействия, мы можем передать полученные двоичные данные в атрибут href тега a, а затем использовать js для имитации эффекта клика тега a, поэтому чтобы открыть диалоговое окно сохранения файла для завершения загрузки файла.

Поскольку мы инкапсулируем axios в api.js, эта функция должна начинаться с api.js.

Добавьте метод downExcel в api.js, чтобы специально загружать файлы Excel.


api.downExcel = (url, method, data) => {
    
    
	api({
    
    
		method: method,// 设置请求方式
		url: url,// 设置请求地址
		data: data,
		responseType: 'blob'// 设置相应数据的类型,设置后后台返回的数据会被强制转为blob类型;如果后台返回代表失败的data,前端也无法得知,依然会下载得到名为undefined的文件。
	}).then(function (res) {
    
    
		// 得到请求到的数据后,对数据进行处理
		let blob = new Blob([res.data], {
    
     type: 'application/vnd.ms-excel;charset=utf-8' });// 创建一个类文件对象:Blob对象表示一个不可变的、原始数据的类文件对象
		let fileName = decodeURI(res.headers['content-disposition']);// 设置文件名称,decodeURI:可以对后端使用encodeURI() 函数编码过的 URI 进行解码。encodeURI() 是后端为了解决中文乱码问题
		if (fileName) {
    
    // 根据后端返回的解析出文件名
			fileName = fileName.split(";")[2].split("=")[1].replaceAll("\"","");
		}
		const elink = document.createElement('a')// 创建一个a标签
		elink.download = fileName;// 设置a标签的下载属性
		elink.style.display = 'none';// 将a标签设置为隐藏
		elink.href = URL.createObjectURL(blob);// 把之前处理好的地址赋给a标签的href
		document.body.appendChild(elink);// 将a标签添加到body中
		elink.click();// 执行a标签的点击方法
		URL.revokeObjectURL(elink.href) // 下载完成释放URL 对象
		document.body.removeChild(elink)// 移除a标签
	})
}

Таким образом, вызов интерфейса для экспорта файлов в фоновом режиме так же прост, как вызов обычного интерфейса отдыха.


const exportToExcel = () => {
    
    
	searchModel.beginTime = searchForm.timeRange[0];
	searchModel.endTime = searchForm.timeRange[1];
	api.downExcel("/testTube/exportTestingTestTube", "post", searchModel);
}

Полный интерфейсный код ( TestTubeList.vue):

<template>
	<div>
		<el-form :inline="true" :model="searchForm">
			<el-form-item label="时间范围">
				<el-date-picker v-model="searchForm.timeRange" type="datetimerange" :shortcuts="shortcuts"
					range-separator="至" start-placeholder="开始时间" end-placeholder="结束时间" format="YYYY-MM-DD HH:00"
					value-format="YYYY-MM-DD HH:00:00" time-arrow-control="true" />
			</el-form-item>
			<el-form-item>
				<el-button type="primary" @click="search">查询</el-button>
			</el-form-item>
			<el-form-item>
				<el-button type="success" @click="exportToExcel">导出Excel数据上传模板</el-button>
			</el-form-item>
		</el-form>
		<el-table :data="testTubeList" border style="width: 100%">
			<el-table-column prop="boxCode" label="箱码" width="180" />
			<el-table-column prop="testTubeCode" label="试管码" width="180" />
			<el-table-column prop="peopleCount" label="样本数量" />
			<el-table-column prop="reciever" label="接收人" />
			<el-table-column prop="recieveTime" label="接收时间" />
			<el-table-column prop="transfer" label="转运人" />
			<el-table-column prop="transferTime" label="转运时间" />
		</el-table>
		<el-pagination :current-page="searchModel.page" :page-size="searchModel.size"
			:page-sizes="[10, 50, 100, 500]" background layout="total, sizes, prev, pager, next, jumper"
			:total="searchModel.total" @size-change="handleSizeChange" @current-change="handleCurrentChange">
		</el-pagination>
	</div>
</template>
<script lang="ts"  setup>
import { ref, reactive } from 'vue'
import common from '../common/common.js';
import api from "../common/api.js";

const searchForm = reactive({ timeRange: ['', ''] });
const search = () => {
	searchModel.beginTime = searchForm.timeRange[0];
	searchModel.endTime = searchForm.timeRange[1];
	api.post("/testTube/searchTestingTestTube", searchModel)
		.then(res => {
			testTubeList.value = res.data.list;
			searchModel.page = res.data.page;
			searchModel.size = res.data.size;
			searchModel.total = res.data.total;
		});
}
const exportToExcel = () => {
	searchModel.beginTime = searchForm.timeRange[0];
	searchModel.endTime = searchForm.timeRange[1];
	api.downExcel("/testTube/exportTestingTestTube", "post", searchModel);
}
const searchModel = reactive({
	beginTime: null,
	endTime: null,
	size: 10,
	page: 1,
	total: 0
})

const shortcuts = [
	{
		text: '24小时',
		value: () => {
			const end = new Date()
			const start = new Date()
			start.setTime(start.getTime() - 3600 * 1000 * 24)
			return [start, end]
		},
	},
	{
		text: '48小时',
		value: () => {
			const end = new Date()
			const start = new Date()
			start.setTime(start.getTime() - 3600 * 1000 * 48)
			return [start, end]
		},
	},
]

const handleSizeChange = (val) => {
	searchModel.size = val;
	search();
}
const handleCurrentChange = (val) => {
	searchModel.page = val;
	search();
}
const testTubeList = ref([]);
</script>

<style>

</style>

Тогда давайте посмотрим, как используется бэкэнд EasyExcel.

EasyExcel

Официальный веб-сайт EasyExcel: https://easyexcel.opensource.alibaba.com/

Он сильно отличается от обычного инструмента экспорта в Excel. Предыдущие инструменты экспорта требовали одной строки кода для изменения содержимого ячейки. А EasyExcel может автоматически генерировать файлы Excel, определяя класс сущности.

Классы сущностей следующие:

package com.hawkon.other.pojo.vo;

import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import lombok.Data;

@Data
public class TestTubeExcelVO  {
    
    

    @ColumnWidth(10) //定义列宽
    @ExcelProperty(value = "箱码",converter = CodeConverter.class)
    private String boxCode;

    @ColumnWidth(20)
    @ExcelProperty(value="试管码",converter = CodeConverter.class)
    private String testTubeCode;

    @ColumnWidth(30)
    @ExcelProperty("检测结果\n(1为阳,0为阴)")
    private String testResult;

}

@ExcelProperty(value = "箱码",converter = CodeConverter.class)Он используется для определения имени столбца, где конвертер указывает пользовательский класс преобразования, который используется для преобразования типа в базе данных в данные ячейки Excel.Возможно, данные используют поле типа BigInt, а EasyExcel не может автоматически преобразовать его.При отладке В то время сообщалось об ошибке преобразования, и этот метод был найден после проверки информации.Код класса преобразования выглядит следующим образом:

package com.hawkon.other.pojo.vo;


import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.converters.ReadConverterContext;
import com.alibaba.excel.converters.WriteConverterContext;
import com.alibaba.excel.metadata.data.WriteCellData;

public class CodeConverter implements Converter<String> {
    
    

    /**
     * 这里是写的时候会调用 不用管
     *
     * @return
     */
    @Override
    public WriteCellData<?> convertToExcelData(WriteConverterContext<String> context) {
    
    
        return new WriteCellData<>(context.getValue().toString());
    }

    @Override
    public String convertToJavaData(ReadConverterContext<?> context) throws Exception {
    
    
        return context.getReadCellData().getData().toString();
//        return Converter.super.convertToJavaData(context);
    }
}

Глядя на родительский класс Converter, унаследованный классом преобразования, вы можете увидеть код ниже.

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.alibaba.excel.converters;

import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.data.ReadCellData;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.metadata.property.ExcelContentProperty;

public interface Converter<T> {
    
    
    default Class<?> supportJavaTypeKey() {
    
    
        throw new UnsupportedOperationException("The current operation is not supported by the current converter.");
    }

    default CellDataTypeEnum supportExcelTypeKey() {
    
    
        throw new UnsupportedOperationException("The current operation is not supported by the current converter.");
    }

    default T convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {
    
    
        throw new UnsupportedOperationException("The current operation is not supported by the current converter.");
    }

    default T convertToJavaData(ReadConverterContext<?> context) throws Exception {
    
    
        return this.convertToJavaData(context.getReadCellData(), context.getContentProperty(), context.getAnalysisContext().currentReadHolder().globalConfiguration());
    }

    default WriteCellData<?> convertToExcelData(T value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {
    
    
        throw new UnsupportedOperationException("The current operation is not supported by the current converter.");
    }

    default WriteCellData<?> convertToExcelData(WriteConverterContext<T> context) throws Exception {
    
    
        return this.convertToExcelData(context.getValue(), context.getContentProperty(), context.getWriteContext().currentWriteHolder().globalConfiguration());
    }
}

Интерфейс определяет ряд методов, из которых нужно написать только два метода для обработки обратного и обратного преобразования:

default WriteCellData<?> convertToExcelData(WriteConverterContext<T> context)

default T convertToJavaData(ReadConverterContext<?> context)

Реализация по умолчанию, заданная интерфейсом, должна сообщать об ошибке.Если какой-либо метод перехватывает ошибку, можно реализовать этот метод. Что касается принципа, то я не буду здесь углубляться в него, если будет возможность, я представлю его в другой статье.

Код финального контроллера

package com.hawkon.other.controller;
//import ....
@RestController
@RequestMapping("/testTube")
public class TestTubeController {
    
    

    @Autowired
    ITestTubeService testTubeService;

    @UploaderApi
    @PostMapping("searchTestingTestTube")
    public ResultModel<PagedResult<TestTubeVO>> searchTestingTestTube(@RequestBody SearchTestTubeModel model) throws BusinessException {
    
    
        PagedResult<TestTubeVO> pagedData = testTubeService.searchTestingTestTube(model);
        return ResultModel.success(pagedData);
    }
    @UploaderApi
    @PostMapping("uploadTestResult")
    public ResultModel<Object> uploadTestResult(MultipartFile file) throws IOException {
    
    
        final List<ExcelDataRow> excelDataRows = testTubeService.saveTestResultFromFile(file);
        return ResultModel.success(excelDataRows);
    }

    @SneakyThrows
    @UploaderApi
    @PostMapping("exportTestingTestTube")
    public ResponseEntity<byte[]> exportTestingTestTube(@RequestBody SearchTestTubeModel model) throws Exception {
    
    
      //导出文件与分页查询调用的Service接口一样,但要注意把页面大小设为最大,这样省得再写一套SQL了。
        model.setPage(1);
        model.setSize(Integer.MAX_VALUE);
        Uploader uploader = SessionUtil.getCurrentUploader();
        model.setOrganizationId(uploader.getOrganizationId());
        PagedResult<TestTubeExcelVO> pagedData = testTubeService.exportTestingTestTube(model);
        List<TestTubeExcelVO> list = pagedData.getList();
        String tempDirPath = ResourceUtils.getURL("classpath:").getPath() + "temp";
        File tempDir = new File(tempDirPath);
        if (!tempDir.exists()) {
    
    
            tempDir.mkdir();
        }
        String fileName = tempDirPath + "/检测结果导入-" + uploader.getOrganizationId() + "-" + System.currentTimeMillis() + ".xlsx";
      
     	//定义表头格式
        WriteCellStyle headWriteCellStyle = new WriteCellStyle();        		
     headWriteCellStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
        headWriteCellStyle.setBorderBottom(BorderStyle.THIN);
        headWriteCellStyle.setBorderLeft(BorderStyle.THIN);
        headWriteCellStyle.setBorderRight(BorderStyle.THIN);
        headWriteCellStyle.setBorderTop(BorderStyle.THIN);
        // 定义单元格样式
        WriteCellStyle contentWriteCellStyle = new WriteCellStyle();
        // 这里需要指定 FillPatternType 为FillPatternType.SOLID_FOREGROUND 不然无法显示背景颜色.头默认了 FillPatternType所以可以不指定
        contentWriteCellStyle.setBorderLeft(BorderStyle.THIN);
        contentWriteCellStyle.setBorderTop(BorderStyle.THIN);
        contentWriteCellStyle.setBorderRight(BorderStyle.THIN);
        contentWriteCellStyle.setBorderBottom(BorderStyle.THIN);
        // 这个策略是 头是头的样式 内容是内容的样式 其他的策略可以自己实现
        HorizontalCellStyleStrategy horizontalCellStyleStrategy =
                new HorizontalCellStyleStrategy(headWriteCellStyle, contentWriteCellStyle);

        try(ExcelWriter excelWriter = EasyExcel.write(fileName, TestTubeExcelVO.class)
                .registerWriteHandler(horizontalCellStyleStrategy).build()) {
    
    
            WriteSheet writeSheet = EasyExcel.writerSheet("检测样本").build();
            //一次写5000条数据,按照官方文档超过5000条数据建议多次写入。
            for (int i = 0; i < list.size(); i += 5000) {
    
    
                // 分页去数据库查询数据 这里可以去数据库查询每一页的数据
                int end = Math.min(list.size(), i + 5000);
                List<TestTubeExcelVO> data = list.subList(i, end);
                excelWriter.write(data, writeSheet);
            }
        }

        File reportFile = new File(fileName);
        ResponseEntity<byte[]> responseEntity = ResponseEntityUtils.getResponseEntity(reportFile);
        return responseEntity;
    }
}

Сервисный слой предназначен для запроса данных, поэтому нет необходимости публиковать код.Код для экспорта файлов Excel выглядит много.На самом деле, если вы экспортируете данные только в соответствии с официальным документом, коду нужны только следующие строки:


        try(ExcelWriter excelWriter = EasyExcel.write(fileName, TestTubeExcelVO.class)
                .build()) {
    
    
            WriteSheet writeSheet = EasyExcel.writerSheet("检测样本").build();
            //一次写5000条数据,按照官方文档超过5000条数据建议多次写入。
            for (int i = 0; i < list.size(); i += 5000) {
    
    
                // 分页去数据库查询数据 这里可以去数据库查询每一页的数据
                int end = Math.min(list.size(), i + 5000);
                List<TestTubeExcelVO> data = list.subList(i, end);
                excelWriter.write(data, writeSheet);
            }
        }

Как насчет этого, неужели это выглядит очень лаконично, в отличие от предыдущего Excel, назначение по ячейке действительно крашится.

Причина, по которой существуют другие коды, заключается в том, что по умолчанию реализация формата экспорта в Excel немного сбивает с толку. Часть содержимого данных по умолчанию не имеет границ. Добавленная часть формата фактически предназначена для изменения заголовка и цвета таблицы, а также строки таблицы части содержимого.

Стоит отметить, что в EasyExcelофициальном документе говорится, что лучше не записывать более 5000 единиц данных за раз.Я думаю, что если вы напрямую записываете десятки тысяч единиц данных за раз, это должно быть успешным, но могут быть некоторые проблемы с производительностью, поэтому мы будем нажимать Официальные рекомендации, записывая только 5000 за раз.

Наконец, код для вывода файла в поток выглядит следующим образом:



        File reportFile = new File(fileName);
        ResponseEntity<byte[]> responseEntity = ResponseEntityUtils.getResponseEntity(reportFile);
        return responseEntity; 

ResponseEntityUtils.getResponseEntity(reportFile);Это класс инструментов, который специально используется для обработки двоичных данных, возвращаемых при загрузке файлов.

package com.hawkon.other.utils;

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;

import java.io.*;
import java.net.URLEncoder;
import java.util.Base64;

/**
 * @author :hawkon
 */
public class ResponseEntityUtils {
    
    

    public static ResponseEntity<byte[]> getResponseEntity(File file) {
    
    
        String fileName = file.getName();
        return getResponseEntity(file, fileName);
    }

    public static ResponseEntity<byte[]> getResponseEntity(File file, String fileName) {
    
    

        BufferedInputStream bis = null;
        ByteArrayOutputStream os = null;
        HttpHeaders httpHeaders = new HttpHeaders();
        ResponseEntity<byte[]> filebyte = null;

        try {
    
    
            bis = new BufferedInputStream(new FileInputStream(file));
            os = new ByteArrayOutputStream();
            int len = 0;
            byte[] bytes = new byte[1024];
            while ((len = bis.read(bytes)) != -1) {
    
    
                os.write(bytes, 0, len);
            }
            fileName = URLEncoder.encode(fileName, "utf-8");
            httpHeaders.setContentDispositionFormData("attachment", fileName);
            httpHeaders.setContentType(MediaType.APPLICATION_OCTET_STREAM);
            filebyte = new ResponseEntity<byte[]>(os.toByteArray(), httpHeaders, HttpStatus.OK);
        } catch (FileNotFoundException e) {
    
    
            e.printStackTrace();
        } catch (IOException e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            try {
    
    
                if (os != null)
                    os.close();
            } catch (IOException e) {
    
    
                e.printStackTrace();
            }
            try {
    
    
                if (bis != null)
                    bis.close();
            } catch (IOException e) {
    
    
                e.printStackTrace();
            }
        }
        return filebyte;
    }

    public static ResponseEntity<byte[]> getFileNotExist(File notFoundFile) {
    
    

        BufferedInputStream bis = null;
        ByteArrayOutputStream os = null;
        HttpHeaders httpHeaders = new HttpHeaders();
        ResponseEntity<byte[]> filebyte = null;

        try {
    
    
            bis = new BufferedInputStream(new FileInputStream(notFoundFile));
            os = new ByteArrayOutputStream();
            int len = 0;
            byte[] bytes = new byte[1024];
            while ((len = bis.read(bytes)) != -1) {
    
    
                os.write(bytes, 0, len);
            }
            httpHeaders.setContentType(MediaType.TEXT_HTML);
            filebyte = new ResponseEntity<byte[]>(os.toByteArray(), httpHeaders, HttpStatus.NOT_FOUND);
        } catch (FileNotFoundException e) {
    
    
            e.printStackTrace();
        } catch (IOException e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            try {
    
    
                if (os != null)
                    os.close();
            } catch (IOException e) {
    
    
                e.printStackTrace();
            }
            try {
    
    
                if (bis != null)
                    bis.close();
            } catch (IOException e) {
    
    
                e.printStackTrace();
            }
        }
        return filebyte;
    }

}

Окончательный экспортированный файл Excel выглядит следующим образом:

вставьте сюда описание изображения

загрузка данных

Скачанный файл можно загрузить после ручного заполнения результатов теста. Загруженный интерфейсный код больше не вставляется, и мы сразу увидим, как с ним справится серверная часть.

Методы чтения и записи EasyExcel аналогичны, при чтении, поскольку вы хотите вернуть ему загруженные результаты, класс сущности все же немного отличается от при экспорте:

package com.hawkon.other.pojo.bo;

import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@EqualsAndHashCode
public class ExcelDataRow {
    
    
    private String boxCode;
    private String testTubeCode;
    private String testResult;
    private String importStatus;
    /**
     * 表示数据是否已经检测完成
     */
    private Boolean tested;
}

Читать код бизнес-логики:

@Override
public List<ExcelDataRow> saveTestResultFromFile(MultipartFile file) throws IOException {
    
    
    List<ExcelDataRow> result = new ArrayList<>();
    EasyExcel.read(file.getInputStream(), ExcelDataRow.class, new PageReadListener<ExcelDataRow>(dataList -> {
    
    
        result.addAll(dataList);
    })).sheet().doRead();
    List<ExcelDataRow> testedSampleList = testTubeDao.getTestedDataStatus(result);
    //将已经检测完成的数据tested设置为true
    for (ExcelDataRow row : result) {
    
    
        if (testedSampleList.stream().filter(t -> t.getTestTubeCode().equals(row.getTestTubeCode())).count() > 0) {
    
    
            row.setTested(true);
            row.setImportStatus("已检测,导入失败");
        }
        else{
    
    
            row.setTested(false);
        }
    }
    //过滤已经tested是true的数据
    List<ExcelDataRow> importList = result.stream()
            .filter(item -> !item.getTested())
            .collect(Collectors.toList());
    testTubeDao.saveTestResult(importList);
    return result;
}

Среди них код для чтения файла Excel в Excel состоит всего из трех строк, не правда ли, очень приятно.

List<ExcelDataRow> result = new ArrayList<>();
EasyExcel.read(file.getInputStream(), ExcelDataRow.class, new PageReadListener<ExcelDataRow>(dataList -> {
    
    
    result.addAll(dataList);
})).sheet().doRead();

Оперативный персонал

Платформа обычно имеет порт или функцию для операторов, которых мы обычно называем суперадминистратором.

Много раз, когда люди пишут программы, они устанавливают суперадминистратора с самыми полными разрешениями.Для этой платформы у суперадминистратора на самом деле очень мало разрешений или ему нужно очень мало функций.Ему нужно только установить точку сбора , агентство тестирования, управлять аккаунтом агентства тестирования, а дальше может понадобиться большой экран данных, а других функций нет.

Большие экраны с данными стали популярными в последние несколько лет, и многие из них сделаны с помощью инструментов визуализации данных, таких как Tableau за границей и Fanruan в Китае. Стоимость этих инструментов визуализации данных все еще довольно высока.У меня есть друг, который работает в компании, специализирующейся на Tableau.Он выиграл клиента и только сделал некоторые отчеты.Клиент вносит сотни тысяч или даже миллионы в год. Это относительно крупные клиенты с большим количеством пользователей. Если объем данных вашего проекта относительно невелик, нужно ли вам использовать эти инструменты визуализации данных? Вы можете использовать Echart, чтобы заполнить его самостоятельно. Просто напишите SQL для фоновых данных самостоятельно.

Если производительность запроса относительно низкая, вы можете сделать снимок данных или кэшировать в соответствии с бизнес-логикой, и ситуация может быть значительно улучшена.

Что касается кода, то большинство функций похожи на предыдущие, поэтому больше их выкладывать не буду.

Заканчивать

Вообще говоря, вещи, используемые в этой главе, несложны.80% инструментов в реальной работе — это CRUD, и лишь небольшая часть работы — это другие вещи, но 20% работы потребляют 80% ваших мозговых клеток. мы используем сторонние компоненты, но чтобы объединить эти вещи, мы не можем просто использовать кирпичи. И когда мы ищем работу, то, что определяет вашу ценность, должно быть не 80% CRUD, а 20% вашей способности решать специальные технические сценарии и решения.

На этом этапе проекта работа над кирпичиками завершена.Если у вас есть какие-либо вопросы, вы можете подписаться на мой официальный аккаунт (комната для интервью Яо сэра), чтобы ответить на тест на нуклеиновую кислоту и получить способ связаться со мной по поводу этот проект.

Другие статьи из этой серии:

Глава 1 Обратный инжиниринг

Глава вторая

Глава третья Острое оружие

Глава четвертая Обвинение

Supongo que te gusta

Origin blog.csdn.net/aley/article/details/128393496
Recomendado
Clasificación