feat:消费机更新

feat:人员调用钱包部门不调动的bug
This commit is contained in:
dzc committed 2026-09-09 11:56:03 +08:00
1 parent 1c86c227ee
commit 2ca14aa1e0
38 files changed
+1903 -35

No files matched your search

@@ -112,6 +112,12 @@
<groupId>com.bsd</groupId>
<artifactId>pjcloud-common-log</artifactId>
</dependency>
<!-- App 安装包上传到宝塔等服务器(SFTP) -->
<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jsch</artifactId>
<version>0.1.55</version>
</dependency>
<!--选配: mybatis 依赖 -->
<dependency>
<groupId>com.baomidou</groupId>
@@ -8,7 +8,9 @@ import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.common.log.annotation.SysLog;
import com.bsd.pjcloud.integrated.dto.appmodule.AppVersionDto;
import com.bsd.pjcloud.integrated.entity.app.AppVersionEntity;
import com.bsd.pjcloud.integrated.service.AppVersionPackageUploadService;
import com.bsd.pjcloud.integrated.service.AppVersionService;
import com.bsd.pjcloud.integrated.vo.appmodule.AppPackageUploadVo;
import com.bsd.pjcloud.integrated.vo.appmodule.AppVersionVo;
import org.springframework.security.access.prepost.PreAuthorize;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
@@ -18,6 +20,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
/**
* app版本控制表
@@ -30,12 +33,13 @@ import org.springframework.web.bind.annotation.*;
public class AppVersionController {
private final AppVersionService appVersionService;
private final AppVersionPackageUploadService appVersionPackageUploadService;
/* ************************************* start 设备端 start ************************************* */
@Operation(summary = "分页查询", description = "分页查询")
@GetMapping("/ad/page")
public R<Page<AppVersionVo>> hardPage(@ParameterObject Page page, @ParameterObject AppVersionDto dto) {
return R.ok(appVersionService.pageList(page, dto));
return R.ok(appVersionService.terminalPageList(page, dto));
}
/* ************************************* end 设备端 end ************************************* */
@@ -45,6 +49,16 @@ public class AppVersionController {
public R<AppVersionEntity> lastVersion(@ParameterObject AppVersionDto dto) {
return R.ok(appVersionService.lastVersion(dto));
}
/**
* 部门邀请等场景:取安卓最新已发布 APK 整包地址(排除 wgt 热更)
*/
@Operation(summary = "最新安卓APK整包", description = "平台含安卓且 downloadUrl 以 .apk 结尾的最新已发布版本")
@GetMapping("/latest-android-apk")
@PreAuthorize("@pms.hasPermission('integrated_appVersion_view') || @pms.hasPermission('sys_dept_edit') || @pms.hasPermission('sys_dept_view')")
public R<AppVersionEntity> latestAndroidApk() {
return R.ok(appVersionService.latestAndroidApk());
}
/* ************************************* end app end ************************************* */
/**
@@ -89,6 +103,29 @@ public class AppVersionController {
return R.ok(appVersionService.updateById(appVersion));
}
/**
* 上传安装包到指定服务器目录(SFTP),返回对外下载地址
*/
@Operation(summary = "上传安装包到远程服务器", description = "SFTP 上传 apk/wgt 等到宝塔等服务器指定目录")
@SysLog("上传App安装包到远程服务器")
@PostMapping("/upload-package")
@PreAuthorize("@pms.hasPermission('integrated_appVersion_add')")
public R<AppPackageUploadVo> uploadPackage(@RequestPart("file") MultipartFile file,
@RequestParam("host") String host,
@RequestParam(value = "port", required = false) Integer port,
@RequestParam("username") String username,
@RequestParam("password") String password,
@RequestParam("remoteDir") String remoteDir,
@RequestParam("publicBaseUrl") String publicBaseUrl) {
try {
return R.ok(appVersionPackageUploadService.uploadBySftp(file, host, port, username, password, remoteDir,
publicBaseUrl));
}
catch (IllegalArgumentException ex) {
return R.failed(ex.getMessage());
}
}
/**
* 通过id删除app版本控制表
*/
@@ -13,4 +13,13 @@ public interface AppVersionMapper extends PjcloudBaseMapper<AppVersionEntity> {
Page<AppVersionVo> pagList(Page page, @Param("dto") AppVersionDto dto);
/**
* 精确查询指定终端和平台的已发布版本,排序由 SQL 固定为版本码倒序。
*
* @param page 分页参数
* @param dto 终端 App 编码与平台参数
* @return 终端可见的已发布版本分页
*/
Page<AppVersionVo> terminalPageList(Page page, @Param("dto") AppVersionDto dto);
}
@@ -0,0 +1,24 @@
package com.bsd.pjcloud.integrated.service;
import com.bsd.pjcloud.integrated.vo.appmodule.AppPackageUploadVo;
import org.springframework.web.multipart.MultipartFile;
/**
* App 安装包上传到远程服务器(宝塔等)
*/
public interface AppVersionPackageUploadService {
/**
* 通过 SFTP 上传安装包,并返回对外可访问的下载地址。
*
* @param file 安装包文件
* @param host 服务器 IP/域名
* @param port SFTP 端口,默认 22
* @param username 登录用户名
* @param password 登录密码
* @param remoteDir 远程目录,如 /www/wwwroot/h5.xxx.com/download
* @param publicBaseUrl 对外访问前缀,如 https://h5.xxx.com/download
*/
AppPackageUploadVo uploadBySftp(MultipartFile file, String host, Integer port, String username, String password,
String remoteDir, String publicBaseUrl);
}
@@ -17,4 +17,20 @@ public interface AppVersionService extends IService<AppVersionEntity> {
* 获取最新版本
*/
AppVersionEntity lastVersion(AppVersionDto dto);
/**
* 按终端 App 编码和平台精确查询已发布版本。
*
* @param page 分页参数
* @param dto 终端 App 编码与平台参数
* @return 版本码倒序的终端版本分页
*/
Page<AppVersionVo> terminalPageList(Page page, AppVersionDto dto);
/**
* 取安卓平台最新已发布的整包安装地址(downloadUrl 以 .apk 结尾)。
*
* @return 安卓最新已发布整包版本;没有记录时为 {@code null}
*/
AppVersionEntity latestAndroidApk();
}
@@ -0,0 +1,128 @@
package com.bsd.pjcloud.integrated.service.impl;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.ssh.Sftp;
import com.bsd.pjcloud.integrated.service.AppVersionPackageUploadService;
import com.bsd.pjcloud.integrated.vo.appmodule.AppPackageUploadVo;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.Session;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.util.Locale;
import java.util.Set;
/**
* 将 APK/WGT 等安装包经 SFTP 上传到宝塔等目标机指定目录。
*/
@Slf4j
@Service
public class AppVersionPackageUploadServiceImpl implements AppVersionPackageUploadService {
private static final String TAG = "AppPackageUpload";
private static final Set<String> ALLOWED_EXT = Set.of("apk", "wgt", "ipa", "zip");
@Override
public AppPackageUploadVo uploadBySftp(MultipartFile file, String host, Integer port, String username,
String password, String remoteDir, String publicBaseUrl) {
if (file == null || file.isEmpty()) {
throw new IllegalArgumentException("请选择要上传的安装包文件");
}
if (StrUtil.hasBlank(host, username, password, remoteDir, publicBaseUrl)) {
throw new IllegalArgumentException("服务器地址、账号、密码、远程目录、对外访问前缀不能为空");
}
String safeDir = normalizeRemoteDir(remoteDir.trim());
String originalName = file.getOriginalFilename();
if (StrUtil.isBlank(originalName)) {
throw new IllegalArgumentException("无法识别文件名");
}
String fileName = FileUtil.getName(originalName);
String ext = FileUtil.extName(fileName);
if (StrUtil.isBlank(ext) || !ALLOWED_EXT.contains(ext.toLowerCase(Locale.ROOT))) {
throw new IllegalArgumentException("仅支持上传 apk/wgt/ipa/zip 文件");
}
int sftpPort = (port == null || port <= 0) ? 22 : port;
String remoteFilePath = safeDir + "/" + fileName;
String publicUrl = joinUrl(publicBaseUrl.trim(), fileName);
Session session = null;
Sftp sftp = null;
try {
JSch jsch = new JSch();
session = jsch.getSession(username.trim(), host.trim(), sftpPort);
session.setPassword(password);
session.setConfig("StrictHostKeyChecking", "no");
session.connect(15000);
sftp = new Sftp(session);
try {
if (!sftp.exist(safeDir)) {
sftp.mkDirs(safeDir);
}
}
catch (Exception ex) {
// 部分环境 exist 不稳定,直接尝试建目录
try {
sftp.mkDirs(safeDir);
}
catch (Exception mkdirEx) {
log.warn("|{}|创建目录失败(可能已存在):|{}|", TAG, mkdirEx.getMessage());
}
}
try (InputStream in = file.getInputStream()) {
boolean ok = sftp.upload(safeDir, fileName, in);
if (!ok) {
throw new IllegalArgumentException("SFTP 上传返回失败");
}
}
log.info("|{}|上传成功 host:|{}| remote:|{}| size:|{}|", TAG, host, remoteFilePath, file.getSize());
return AppPackageUploadVo.builder()
.url(publicUrl)
.fileName(fileName)
.remotePath(remoteFilePath)
.build();
}
catch (IllegalArgumentException ex) {
throw ex;
}
catch (Exception ex) {
log.error("|{}|SFTP上传失败 host:|{}| dir:|{}| error:|{}|", TAG, host, safeDir, ex.getMessage(), ex);
throw new IllegalArgumentException("上传到服务器失败:" + StrUtil.blankToDefault(ex.getMessage(), "请检查账号、目录与网络"));
}
finally {
if (sftp != null) {
try {
sftp.close();
}
catch (Exception ignore) {
// ignore
}
}
if (session != null && session.isConnected()) {
session.disconnect();
}
}
}
private static String normalizeRemoteDir(String remoteDir) {
String dir = remoteDir.replace('\\', '/');
if (dir.contains("..")) {
throw new IllegalArgumentException("远程目录非法");
}
if (!dir.startsWith("/")) {
dir = "/" + dir;
}
while (dir.endsWith("/") && dir.length() > 1) {
dir = dir.substring(0, dir.length() - 1);
}
return dir;
}
private static String joinUrl(String base, String fileName) {
String b = StrUtil.removeSuffix(base, "/");
return b + "/" + fileName;
}
}
@@ -4,6 +4,7 @@ import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bsd.pjcloud.integrated.constant.appmodule.AppPlatformEnum;
import com.bsd.pjcloud.integrated.constant.appmodule.AppVersionStatusEnum;
import com.bsd.pjcloud.integrated.dto.appmodule.AppVersionDto;
import com.bsd.pjcloud.integrated.entity.app.AppVersionEntity;
@@ -39,4 +40,30 @@ public class AppVersionServiceImpl extends ServiceImpl<AppVersionMapper, AppVers
.last("limit 1");
return baseMapper.selectOne(queryWrapper);
}
/**
* 按终端 App 编码和平台精确查询已发布版本。
*
* @param page 分页参数
* @param dto 终端 App 编码与平台参数
* @return 版本码倒序的终端版本分页
*/
@Override
public Page<AppVersionVo> terminalPageList(Page page, AppVersionDto dto) {
return baseMapper.terminalPageList(page, dto);
}
/**
* 取安卓平台最新已发布的整包安装地址(downloadUrl 以 .apk 结尾)。
* <p>排除 .wgt 热更补丁,供部门邀请下载二维码使用。</p>
*/
@Override
public AppVersionEntity latestAndroidApk() {
return baseMapper.selectOne(new LambdaQueryWrapper<AppVersionEntity>()
.eq(AppVersionEntity::getStatus, AppVersionStatusEnum.PUBLISHED.getCode())
.apply("FIND_IN_SET({0}, platform) > 0", AppPlatformEnum.ANDROID.getCode())
.apply("LOWER(download_url) LIKE '%.apk'")
.orderByDesc(AppVersionEntity::getVersionCode)
.last("LIMIT 1"));
}
}
@@ -0,0 +1,23 @@
package com.bsd.pjcloud.integrated.vo.appmodule;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;
/**
* App 安装包远程上传结果
*/
@Data
@Builder
@Schema(description = "App安装包上传结果")
public class AppPackageUploadVo {
@Schema(description = "对外下载地址")
private String url;
@Schema(description = "服务器上的文件名")
private String fileName;
@Schema(description = "服务器完整路径")
private String remotePath;
}
@@ -42,4 +42,20 @@
</where>
</select>
<select id="terminalPageList" resultType="com.bsd.pjcloud.integrated.vo.appmodule.AppVersionVo">
<include refid="selectA"/>
from app_version a
<where>
a.del_flag = '0'
and a.status = '1'
<if test="dto.appUniqueCode != null and dto.appUniqueCode != ''">
and a.app_unique_code = #{dto.appUniqueCode}
</if>
<if test="dto.platform != null and dto.platform != ''">
and a.platform = #{dto.platform}
</if>
</where>
order by a.version_code desc, a.create_time desc
</select>
</mapper>