编辑
2024-04-30
项目分享
00

项目介绍

阅读3服务器版,桌面端,iOS可用。后端 Kotlin + Spring Boot + Vert.x + Coroutine ;前端 Vue.js + Element。

项目地址

阅读3服务器版github地址

部署文档地址

项目部署

需要:

  1. 一台服务器
  2. 服务器部署宝塔面板

从宝塔面板之后的事情开始说

进入docker compose配置

点击新增模板,并配置 获取最新的配置文件

yml
version: '3.1' services: # reader 在线阅读 # 公开服务器(服务器位于日本):[https://reader.nxnow.top](https://reader.nxnow.top) 测试账号/密码分别为guest/guest123,也可自行创建账号添加书源,不定期删除长期未登录账号(2周) # 书源集合 : [https://legado.aoaostar.com/](https://legado.aoaostar.com/) 点击打开连接,添加远程书源即可 # 公众号汇总 : [https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MjM5MzMyMDgyMA==&action=getalbum&album_id=2397535253763801090#wechat_redirect](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MjM5MzMyMDgyMA==&action=getalbum&album_id=2397535253763801090#wechat_redirect) # 手动更新方式 : docker-compose pull && docker-compose up -d reader: image: hectorqin/reader #image: hectorqin/reader:openj9-latest #docker镜像,arm64架构或小内存机器优先使用此镜像.启用需删除上一行 container_name: reader #容器名 可自行修改 restart: always ports: - 4396:8080 #4396端口映射可自行修改 networks: - share_net volumes: - /home/reader/logs:/logs #log映射目录 /home/reader/logs 映射目录可自行修改 - /home/reader/storage:/storage #数据映射目录 /home/reader/storage 映射目录可自行修改 environment: - SPRING_PROFILES_ACTIVE=prod - READER_APP_USERLIMIT=50 #用户上限,默认50 - READER_APP_USERBOOKLIMIT=200 #用户书籍上限,默认200 - READER_APP_CACHECHAPTERCONTENT=true #开启缓存章节内容 V2.0 # 如果启用远程webview,需要取消注释下面的 remote-webview 服务 # - READER_APP_REMOTEWEBVIEWAPI=http://remote-webview:8050 #开启远程webview # 下面都是多用户模式配置 - READER_APP_SECURE=true #开启登录鉴权,开启后将支持多用户模式 - READER_APP_SECUREKEY=adminpwd #管理员密码 建议修改 - READER_APP_INVITECODE=registercode #注册邀请码 建议修改,如不需要可注释或删除 # remote-webview: # image: hectorqin/remote-webview # container_name: remote-webview #容器名 可自行修改 # restart: always # ports: # - 8050:8050 # networks: # - share_net # 自动更新docker镜像 watchtower: image: containrrr/watchtower container_name: watchtower restart: always # 环境变量,设置为上海时区 environment: - TZ=Asia/Shanghai volumes: - /var/run/docker.sock:/var/run/docker.sock command: reader watchtower --cleanup --schedule "0 0 4 * * *" networks: - share_net # 仅更新reader与watchtower容器,如需其他自行添加 '容器名' ,如:reader watchtower nginx # --cleanup 更新后清理旧版本镜像 # --schedule 自动检测更新 crontab定时(限定6位crontab) 此处代表凌晨4点整 networks: share_net: driver: bridge

根据自己要求修改配置

编辑
2024-04-29
学习记录
00

前提

由于业务需要,获取多个pdf的时候需要将多个pdf存入zip类型的压缩包中实现下载,导出的过程使用轮询的方式实现进度查询,每次操作将当前进度存储入redis中,获取进度的接口从redis查询信息,下面只详细展开zip下载的代码

代码实现

这个操作步骤主要分两步

1.生成压缩输出流

返回查询进度的id,操作完成后将把流信息存储入redis,提供下载使用

java
if (!redisUtil.isPing()) { throw new BusinessException(ResultCodeEnum.ERR_0x1000.getCode(), "导出失败,Redis服务未连接!"); } String id = StringUtil.generateId(); redisUtil.set(markKey, id, 3600); Map<String, Object> map = new HashMap<>(); map.put("current", 0); map.put("total", list.size()); String mapkey = markKey + id + ":"; redisUtil.set(mapkey, map, 3600); //单线程线程池 ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(1, 1, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), Executors.defaultThreadFactory(), new ThreadPoolExecutor.DiscardPolicy()); try { poolExecutor.execute(() -> { List<byte[]> pdfBytesList = new ArrayList<>(); List<String> filenames=new ArrayList<>(); for (PatientRequest p : list) { byte[] bytes = null; try { bytes = getPdfByte(p); } catch (Exception e) { log.error(e.getMessage()); e.printStackTrace(); } Map<String, Object> map1 = redisUtil.get(mapkey, Map.class); map1.put("current", (Integer) map1.get("current") + 1); if (ArrayUtil.isNotEmpty(bytes)) { filenames.add(String.format("%s_%s_%s_%s.pdf", province, name, p.getPatientId(), DateUtil.format(new Date(), "yyyyMMdd"))); pdfBytesList.add(bytes); } if (list.indexOf(p) == list.size() - 1) { try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); ZipOutputStream zos = new ZipOutputStream(baos)) { for (int i = 0; i < pdfBytesList.size(); i++) { byte[] pdfBytes = pdfBytesList.get(i); ZipEntry entry = new ZipEntry(filenames.get(i)); zos.putNextEntry(entry); zos.write(pdfBytes); zos.closeEntry(); } zos.flush(); zos.close(); baos.flush(); map1.put("data", Base64.getEncoder().encodeToString(baos.toByteArray())); } catch (IOException e) { // 处理可能的异常 e.printStackTrace(); // 实际应用中应使用合适的日志记录方式 map1.put("data", null); } } redisUtil.set(mapkey, map1); } }); } catch (Exception e) { throw new BusinessException(ResultCodeEnum.ERR_0x1000.getCode(), e.getMessage()); } finally { poolExecutor.shutdown(); } return id;
编辑
2024-04-28
学习记录
00

介绍

oracle 和mysql不同,没有自增主键,需要自己定义主键

使用UUID

可以使用mybatisplus的UUID工具根据自己要求生成主键

mybatisPlus官方文档说明

UUid有个弊端就是太长的话,不利于索引的建立,会影响查询效率

使用oracle序列

使用序列也有两种方式

第一种就是实体类中定义主键类型

比如我之前这篇文章

第二种就是直接通过数据库的触发器实现

sql
CREATE TABLE rec_supplement_record ( ID NUMBER(10) PRIMARY KEY, patient_id VARCHAR2(255) NOT NULL, visit_no VARCHAR2(255) NOT NULL, doc_type VARCHAR2(255) NOT NULL, supplement_send_id NUMBER(10) NOT NULL, supplement_send_name VARCHAR2(255) NOT NULL, supplement_receive_id NUMBER(10), supplement_receive_name VARCHAR2(255), supplement_receive_time Date, supplement_send_reason VARCHAR2(400) ); CREATE SEQUENCE rec_supplement_record_seq START WITH 1 INCREMENT BY 1 MAXVALUE 999999999999999999999999999 NOCYCLE NOCACHE; CREATE OR REPLACE TRIGGER rec_supplement_record_trg BEFORE INSERT ON rec_supplement_record FOR EACH ROW WHEN (NEW.ID IS NULL) BEGIN SELECT rec_supplement_record_seq.NEXTVAL INTO :NEW.ID FROM dual; END;

这样的话,很像mysql的自增主键的形式,每次插入数据时不需要定义主键,即可自动生成

编辑
2024-04-25
遇到的问题
00

前提

业务需要,得通过javaweb下载用户的个人信息的pdf,多个人批量下载时就需要将pdf文件存放在zip类型中,但是下载得到的文件解压时提示错误,里面的文件大小为空

排查

首先,生成zip文件时服务器并未提示,系统错误,导致的文件异常,上网查找发现,可能是流未关闭的问题,然后我使用的是try(reason) catch的方式,流会自动关闭 代码如下

java
try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); ZipOutputStream zipOut = new ZipOutputStream(baos)) { // 遍历文件数据列表,将每个byte数组添加为zip条目 for (int i = 0; i < fileDataList.size(); i++) { byte[] fileData = JSON.toJSONBytes(fileDataList.get(i).get("bytes")); String entryName = (String) fileDataList.get(i).get("fileName"); ZipEntry zipEntry = new ZipEntry(entryName); zipOut.putNextEntry(zipEntry); zipOut.write(fileData); zipOut.closeEntry(); } zipOut.flush(); baos.flush(); // 设置HTTP响应头 response.setContentType("application/zip"); String fileName = String.format("%s_%s_全病案_%s.zip", province, name, DateUtil.format(new Date(), "yyyyMMdd")); response.setHeader("Content-Disposition", "attachment; filename=" + fileName); // 将zip数据写入响应流 IoUtil.copy(new ByteArrayInputStream(baos.toByteArray()), response.getOutputStream()); } catch (IOException e) { e.printStackTrace(); }

深入挖掘

自动关闭流是在整个业务流程走完之后才会一一关闭流,但是zipout这个流使用完毕之后就需要立即关闭, 即在 zipOut.flush();之后就需要关闭

修改代码后就不会提示了

java
try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); ZipOutputStream zipOut = new ZipOutputStream(baos)) { // 遍历文件数据列表,将每个byte数组添加为zip条目 for (int i = 0; i < fileDataList.size(); i++) { byte[] fileData = JSON.toJSONBytes(fileDataList.get(i).get("bytes")); String entryName = (String) fileDataList.get(i).get("fileName"); ZipEntry zipEntry = new ZipEntry(entryName); zipOut.putNextEntry(zipEntry); zipOut.write(fileData); zipOut.closeEntry(); } zipOut.flush(); zipOut.close(); baos.flush(); // 设置HTTP响应头 response.setContentType("application/zip"); String fileName = String.format("%s_%s_全病案_%s.zip", province, name, DateUtil.format(new Date(), "yyyyMMdd")); response.setHeader("Content-Disposition", "attachment; filename=" + fileName); // 将zip数据写入响应流 IoUtil.copy(new ByteArrayInputStream(baos.toByteArray()), response.getOutputStream()); } catch (IOException e) { e.printStackTrace(); }
编辑
2024-04-25
学习记录
00

原因

现场服务可能会出现Redis服务异常的问题,每次使用redis之前都需要对redis进行连通性判断

代码

java
//判断redis是否可用 public boolean isPing(){ try { RedisConnection connection = redisTemplate.getConnectionFactory().getConnection(); String response = connection.ping(); return "PONG".equals(response); } catch (Exception e) { log.error(e.getMessage) return false; } }