From 0bde83e9c2594ce69e3634d3671fbf5a38a9d0ce Mon Sep 17 00:00:00 2001
From: zjf <1799650030@qq.com>
Date: Tue, 8 Sep 2026 16:23:34 +0800
Subject: [PATCH] =?UTF-8?q?1.=E9=83=A8=E9=97=A8=E9=82=80=E8=AF=B7=202.app?=
=?UTF-8?q?=E7=89=88=E6=9C=AC=E5=A2=9E=E5=8A=A0ssh=E4=B8=8A=E4=BC=A0?=
=?UTF-8?q?=EF=BC=8C3.=E6=B3=A8=E5=86=8C=E9=A1=B5=E9=9D=A2=E5=9B=BD?=
=?UTF-8?q?=E9=99=85=E5=8C=96=E9=80=82=E9=85=8D?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../src/main/resources/application.yml | 3 +
.../core/constant/enums/MerchantAttrEnum.java | 45 ++
.../component/PermitAllUrlProperties.java | 30 +-
.../service/PjcloudUserDetailsService.java | 14 +
.../security/util/OAuth2ErrorCodesExpand.java | 6 +
.../resources/errors/messages_en.properties | 2 +
.../errors/messages_zh_CN.properties | 2 +
.../errors/messages_zh_HK.properties | 2 +
.../api/feign/RemoteFlowApiFlowService.java | 97 ++--
.../task/controller/ProcessController.java | 11 +
.../pjcloud-integrated-biz/pom.xml | 8 +-
.../controller/app/AppVersionController.java | 37 ++
.../AppVersionPackageUploadService.java | 24 +
.../integrated/service/AppVersionService.java | 5 +
.../AppVersionPackageUploadServiceImpl.java | 128 +++++
.../impl/app/AppVersionServiceImpl.java | 15 +
.../integrated/vo/app/AppPackageUploadVo.java | 23 +
.../admin/api/dto/DeptInviteRegisterDTO.java | 30 +
.../admin/api/dto/SysDeptInviteSaveDTO.java | 44 ++
.../admin/api/entity/SysDeptInviteEntity.java | 71 +++
.../admin/api/vo/SysDeptInviteListVO.java | 49 ++
.../admin/api/vo/SysDeptInvitePreviewVO.java | 42 ++
.../controller/SysDeptInviteController.java | 74 +++
.../SysDeptInvitePublicController.java | 48 ++
.../admin/mapper/SysDeptInviteMapper.java | 9 +
.../admin/service/SysDeptInviteService.java | 32 ++
.../impl/SysDeptInviteServiceImpl.java | 543 ++++++++++++++++++
.../service/impl/SysUserServiceImpl.java | 8 +
.../pjcloud/admin/utils/flow/FlowUtil.java | 16 +-
.../src/main/resources/application.yml | 9 +-
.../api/entity/mch/MchManageEntity.java | 6 +
31 files changed, 1372 insertions(+), 61 deletions(-)
create mode 100644 pjcloud-common/pjcloud-common-core/src/main/java/com/bsd/pjcloud/common/core/constant/enums/MerchantAttrEnum.java
create mode 100644 pjcloud-integrated/pjcloud-integrated-biz/src/main/java/com/bsd/pjcloud/integrated/service/AppVersionPackageUploadService.java
create mode 100644 pjcloud-integrated/pjcloud-integrated-biz/src/main/java/com/bsd/pjcloud/integrated/service/impl/AppVersionPackageUploadServiceImpl.java
create mode 100644 pjcloud-integrated/pjcloud-integrated-biz/src/main/java/com/bsd/pjcloud/integrated/vo/app/AppPackageUploadVo.java
create mode 100644 pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/dto/DeptInviteRegisterDTO.java
create mode 100644 pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/dto/SysDeptInviteSaveDTO.java
create mode 100644 pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/entity/SysDeptInviteEntity.java
create mode 100644 pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/vo/SysDeptInviteListVO.java
create mode 100644 pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/vo/SysDeptInvitePreviewVO.java
create mode 100644 pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysDeptInviteController.java
create mode 100644 pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysDeptInvitePublicController.java
create mode 100644 pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/mapper/SysDeptInviteMapper.java
create mode 100644 pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/SysDeptInviteService.java
create mode 100644 pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysDeptInviteServiceImpl.java
diff --git a/pjcloud-boot/src/main/resources/application.yml b/pjcloud-boot/src/main/resources/application.yml
index 785ad1d6..3a304089 100644
--- a/pjcloud-boot/src/main/resources/application.yml
+++ b/pjcloud-boot/src/main/resources/application.yml
@@ -47,6 +47,9 @@ security:
- /druid/**
- /actuator/**
- /code/**
+ - /dept/invite/preview
+ - /dept/invite/register
+ - /register/**
#--------------如下配置尽量不要变动-------------
# mybatis-plus 配置
diff --git a/pjcloud-common/pjcloud-common-core/src/main/java/com/bsd/pjcloud/common/core/constant/enums/MerchantAttrEnum.java b/pjcloud-common/pjcloud-common-core/src/main/java/com/bsd/pjcloud/common/core/constant/enums/MerchantAttrEnum.java
new file mode 100644
index 00000000..94e7b81e
--- /dev/null
+++ b/pjcloud-common/pjcloud-common-core/src/main/java/com/bsd/pjcloud/common/core/constant/enums/MerchantAttrEnum.java
@@ -0,0 +1,45 @@
+package com.bsd.pjcloud.common.core.constant.enums;
+
+import cn.hutool.core.util.ObjectUtil;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+import java.util.Arrays;
+
+/**
+ * 商户属性(与业态 {@link MerchantTypeEnum} 正交)
+ */
+@RequiredArgsConstructor
+@Getter
+public enum MerchantAttrEnum {
+
+ NORMAL("NORMAL", "普通"),
+ SCHOOL("SCHOOL", "学校"),
+ GOVERNMENT("GOVERNMENT", "政府"),
+ FACTORY("FACTORY", "工厂"),
+ ;
+
+ private final String code;
+ private final String label;
+
+ public static MerchantAttrEnum valueOfByCode(String code) {
+ if (ObjectUtil.isEmpty(code)) {
+ return null;
+ }
+ return Arrays.stream(values())
+ .filter(v -> ObjectUtil.equal(code, v.getCode()))
+ .findFirst()
+ .orElse(null);
+ }
+
+ /**
+ * 学校类型默认学生身份,其余默认身份。
+ */
+ public static String resolveDefaultIdentityType(String merchantAttr) {
+ MerchantAttrEnum attr = valueOfByCode(merchantAttr);
+ if (attr == SCHOOL) {
+ return "STUDENT";
+ }
+ return "DEFAULT";
+ }
+}
diff --git a/pjcloud-common/pjcloud-common-security/src/main/java/com/bsd/pjcloud/common/security/component/PermitAllUrlProperties.java b/pjcloud-common/pjcloud-common-security/src/main/java/com/bsd/pjcloud/common/security/component/PermitAllUrlProperties.java
index 7c6786b8..0e98b26b 100644
--- a/pjcloud-common/pjcloud-common-security/src/main/java/com/bsd/pjcloud/common/security/component/PermitAllUrlProperties.java
+++ b/pjcloud-common/pjcloud-common-security/src/main/java/com/bsd/pjcloud/common/security/component/PermitAllUrlProperties.java
@@ -44,7 +44,14 @@ public class PermitAllUrlProperties implements InitializingBean {
private static final Pattern PATTERN = Pattern.compile("\\{(.*?)\\}");
- private static final String[] DEFAULT_IGNORE_URLS = new String[]{"/actuator/**", "/error", "/v3/api-docs"};
+ private static final String[] DEFAULT_IGNORE_URLS = new String[]{
+ "/actuator/**",
+ "/error",
+ "/v3/api-docs",
+ // 部门邀请公开接口(免登录);写入默认白名单,避免仅依赖注解扫描遗漏
+ "/dept/invite/preview",
+ "/dept/invite/register"
+ };
@Getter
@Setter
@@ -70,18 +77,23 @@ public class PermitAllUrlProperties implements InitializingBean {
// 获取方法上边的注解 替代path variable 为 *
Inner method = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), Inner.class);
- Optional.ofNullable(method)
- .ifPresent(inner -> Objects.requireNonNull(info.getPathPatternsCondition())
- .getPatternValues()
- .forEach(url -> ignoreUrls.add(ReUtil.replaceAll(url, PATTERN, "*"))));
+ Optional.ofNullable(method).ifPresent(inner -> addIgnoreUrls(info));
// 获取类上边的注解, 替代path variable 为 *
Inner controller = AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), Inner.class);
- Optional.ofNullable(controller)
- .ifPresent(inner -> Objects.requireNonNull(info.getPathPatternsCondition())
- .getPatternValues()
- .forEach(url -> ignoreUrls.add(ReUtil.replaceAll(url, PATTERN, "*"))));
+ Optional.ofNullable(controller).ifPresent(inner -> addIgnoreUrls(info));
});
}
+ private void addIgnoreUrls(RequestMappingInfo info) {
+ Set patterns = new HashSet<>();
+ if (info.getPathPatternsCondition() != null) {
+ patterns.addAll(info.getPathPatternsCondition().getPatternValues());
+ }
+ if (info.getPatternsCondition() != null) {
+ patterns.addAll(info.getPatternsCondition().getPatterns());
+ }
+ patterns.forEach(url -> ignoreUrls.add(ReUtil.replaceAll(url, PATTERN, "*")));
+ }
+
}
diff --git a/pjcloud-common/pjcloud-common-security/src/main/java/com/bsd/pjcloud/common/security/service/PjcloudUserDetailsService.java b/pjcloud-common/pjcloud-common-security/src/main/java/com/bsd/pjcloud/common/security/service/PjcloudUserDetailsService.java
index 6930f03b..c5b9fdd2 100644
--- a/pjcloud-common/pjcloud-common-security/src/main/java/com/bsd/pjcloud/common/security/service/PjcloudUserDetailsService.java
+++ b/pjcloud-common/pjcloud-common-security/src/main/java/com/bsd/pjcloud/common/security/service/PjcloudUserDetailsService.java
@@ -17,18 +17,22 @@
package com.bsd.pjcloud.common.security.service;
import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.core.util.StrUtil;
import com.bsd.pjcloud.admin.api.dto.UserInfo;
import com.bsd.pjcloud.admin.api.entity.SysUser;
import com.bsd.pjcloud.common.core.constant.CommonConstants;
import com.bsd.pjcloud.common.core.constant.SecurityConstants;
import com.bsd.pjcloud.common.core.constant.enums.UserTypeEnum;
import com.bsd.pjcloud.common.core.util.MsgUtils;
+import com.bsd.pjcloud.common.security.util.OAuth2ErrorCodesExpand;
import org.springframework.core.Ordered;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2Error;
import java.util.*;
@@ -96,6 +100,16 @@ public interface PjcloudUserDetailsService extends UserDetailsService, Ordered {
Collection extends GrantedAuthority> authorities = AuthorityUtils
.createAuthorityList(dbAuthsSet.toArray(new String[0]));
SysUser user = info.getSysUser();
+ // 与 TaskCheckStatusEnum 对齐:审核中/拒绝不可登录(邀请注册等场景仅写 processStatus,不锁账号)
+ String processStatus = user.getProcessStatus();
+ if (StrUtil.equals(processStatus, "1")) {
+ throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodesExpand.USER_PENDING_AUDIT,
+ MsgUtils.getSecurityMessage("AbstractUserDetailsAuthenticationProvider.pendingAudit"), ""));
+ }
+ if (StrUtil.equals(processStatus, "3")) {
+ throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodesExpand.USER_AUDIT_REJECTED,
+ MsgUtils.getSecurityMessage("AbstractUserDetailsAuthenticationProvider.auditRejected"), ""));
+ }
// 构造security用户
return new PjcloudUser(user.getUserId(), user.getUsername(), user.getDeptId(), user.getPhone(), user.getAvatar(),
diff --git a/pjcloud-common/pjcloud-common-security/src/main/java/com/bsd/pjcloud/common/security/util/OAuth2ErrorCodesExpand.java b/pjcloud-common/pjcloud-common-security/src/main/java/com/bsd/pjcloud/common/security/util/OAuth2ErrorCodesExpand.java
index e40f2073..a49b05cf 100644
--- a/pjcloud-common/pjcloud-common-security/src/main/java/com/bsd/pjcloud/common/security/util/OAuth2ErrorCodesExpand.java
+++ b/pjcloud-common/pjcloud-common-security/src/main/java/com/bsd/pjcloud/common/security/util/OAuth2ErrorCodesExpand.java
@@ -18,6 +18,12 @@ public interface OAuth2ErrorCodesExpand {
/** 用户禁用 */
String USER_DISABLE = "user_disable";
+ /** 用户审核中 */
+ String USER_PENDING_AUDIT = "user_pending_audit";
+
+ /** 用户审核拒绝 */
+ String USER_AUDIT_REJECTED = "user_audit_rejected";
+
/** 用户过期 */
String USER_EXPIRED = "user_expired";
diff --git a/pjcloud-common/pjcloud-common-security/src/main/resources/errors/messages_en.properties b/pjcloud-common/pjcloud-common-security/src/main/resources/errors/messages_en.properties
index 3278de58..ef5ea5b7 100644
--- a/pjcloud-common/pjcloud-common-security/src/main/resources/errors/messages_en.properties
+++ b/pjcloud-common/pjcloud-common-security/src/main/resources/errors/messages_en.properties
@@ -7,6 +7,8 @@ AbstractUserDetailsAuthenticationProvider.credentialsExpired=Credentials have ex
OAuth2ResourceOwnerBaseAuthenticationProvider.tokenExpired=Credentials have expired
AbstractUserDetailsAuthenticationProvider.disabled=User is not activated
AbstractUserDetailsAuthenticationProvider.locked=User account is locked
+AbstractUserDetailsAuthenticationProvider.pendingAudit=Account is pending approval. Please wait until approved before signing in
+AbstractUserDetailsAuthenticationProvider.auditRejected=Account approval was rejected. Sign-in is not allowed
AbstractUserDetailsAuthenticationProvider.expired=User account has expired
JdbcDaoImpl.notFound=User {0} not found
UserDetailsService.notFound=User does not exist
diff --git a/pjcloud-common/pjcloud-common-security/src/main/resources/errors/messages_zh_CN.properties b/pjcloud-common/pjcloud-common-security/src/main/resources/errors/messages_zh_CN.properties
index 0f225330..d4d746b6 100644
--- a/pjcloud-common/pjcloud-common-security/src/main/resources/errors/messages_zh_CN.properties
+++ b/pjcloud-common/pjcloud-common-security/src/main/resources/errors/messages_zh_CN.properties
@@ -29,6 +29,8 @@ OAuth2ResourceOwnerBaseAuthenticationProvider.tokenExpired=\u7528\u6237\u51ED\u8
AbstractUserDetailsAuthenticationProvider.disabled=\u7528\u6237\u672A\u6FC0\u6D3B
AbstractUserDetailsAuthenticationProvider.expired=\u7528\u6237\u5E10\u53F7\u5DF2\u8FC7\u671F
AbstractUserDetailsAuthenticationProvider.locked=\u7528\u6237\u5E10\u53F7\u5DF2\u88AB\u9501\u5B9A
+AbstractUserDetailsAuthenticationProvider.pendingAudit=\u8D26\u53F7\u5BA1\u6838\u4E2D\uFF0C\u8BF7\u7B49\u5F85\u5BA1\u6279\u901A\u8FC7\u540E\u518D\u767B\u5F55
+AbstractUserDetailsAuthenticationProvider.auditRejected=\u8D26\u53F7\u5BA1\u6838\u672A\u901A\u8FC7\uFF0C\u65E0\u6CD5\u767B\u5F55
AbstractUserDetailsAuthenticationProvider.onlySupports=\u4EC5\u4EC5\u652F\u6301UsernamePasswordAuthenticationToken
AbstractUserDetailsAuthenticationProvider.badTenantId=\u65E0\u6548\u7684\u79DF\u6237\u7F16\u53F7:{0}
AccountStatusUserDetailsChecker.credentialsExpired=\u7528\u6237\u51ED\u8BC1\u5DF2\u8FC7\u671F
diff --git a/pjcloud-common/pjcloud-common-security/src/main/resources/errors/messages_zh_HK.properties b/pjcloud-common/pjcloud-common-security/src/main/resources/errors/messages_zh_HK.properties
index ee158b63..d7302b0d 100644
--- a/pjcloud-common/pjcloud-common-security/src/main/resources/errors/messages_zh_HK.properties
+++ b/pjcloud-common/pjcloud-common-security/src/main/resources/errors/messages_zh_HK.properties
@@ -7,6 +7,8 @@ AbstractUserDetailsAuthenticationProvider.credentialsExpired=用戶憑證已過
OAuth2ResourceOwnerBaseAuthenticationProvider.tokenExpired=用戶憑證已過期
AbstractUserDetailsAuthenticationProvider.disabled=用戶未啟用
AbstractUserDetailsAuthenticationProvider.locked=用戶帳號已鎖定
+AbstractUserDetailsAuthenticationProvider.pendingAudit=帳號審核中,請等待審批通過後再登入
+AbstractUserDetailsAuthenticationProvider.auditRejected=帳號審核未通過,無法登入
AbstractUserDetailsAuthenticationProvider.expired=用戶帳號已過期
JdbcDaoImpl.notFound=未找到用戶{0}
UserDetailsService.notFound=用戶不存在
diff --git a/pjcloud-flow/pjcloud-flow-task/pjcloud-flow-task-api/src/main/java/com/bsd/pjcloud/flow/task/api/feign/RemoteFlowApiFlowService.java b/pjcloud-flow/pjcloud-flow-task/pjcloud-flow-task-api/src/main/java/com/bsd/pjcloud/flow/task/api/feign/RemoteFlowApiFlowService.java
index bf97b7dd..79a8effa 100644
--- a/pjcloud-flow/pjcloud-flow-task/pjcloud-flow-task-api/src/main/java/com/bsd/pjcloud/flow/task/api/feign/RemoteFlowApiFlowService.java
+++ b/pjcloud-flow/pjcloud-flow-task/pjcloud-flow-task-api/src/main/java/com/bsd/pjcloud/flow/task/api/feign/RemoteFlowApiFlowService.java
@@ -3,13 +3,13 @@ package com.bsd.pjcloud.flow.task.api.feign;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.bsd.pjcloud.common.core.constant.ServiceNameConstants;
import com.bsd.pjcloud.common.core.util.R;
+import com.bsd.pjcloud.common.feign.annotation.NoToken;
import com.bsd.pjcloud.flow.task.dto.ProcessInstanceParamDto;
import com.bsd.pjcloud.flow.task.dto.TaskDto;
import com.bsd.pjcloud.flow.task.dto.TaskQueryParamDto;
import com.bsd.pjcloud.flow.task.entity.Process;
import com.bsd.pjcloud.flow.task.vo.FormGroupVo;
import com.bsd.pjcloud.flow.task.vo.ProcessVO;
-import lombok.SneakyThrows;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
@@ -25,56 +25,59 @@ import java.util.List;
@FeignClient(contextId = "remoteAiFlowService", value = ServiceNameConstants.FLOW_TASK_SERVER)
public interface RemoteFlowApiFlowService {
- /**
- * 查询所有我可以发起的表单
- *
- * @return R
- */
- @GetMapping("/combination/group/listCurrentUserStartGroup")
- R> listCurrentUserStartGroup();
+ /**
+ * 查询所有我可以发起的表单
+ * @return R
+ */
+ @GetMapping("/combination/group/listCurrentUserStartGroup")
+ R> listCurrentUserStartGroup();
- /**
- * 获取流程详情
- *
- * @param flowId 流程id
- * @return R
- */
- @GetMapping("/process/getDetail")
- R getDetail(@RequestParam("flowId") String flowId);
+ /**
+ * 获取流程详情
+ * @param flowId 流程id
+ * @return R
+ */
+ @GetMapping("/process/getDetail")
+ R getDetail(@RequestParam("flowId") String flowId);
+ /**
+ * 启动流程实例
+ * @param paramDto param dto
+ * @return 流程实例ID
+ */
+ @PostMapping("/process-instance/startProcessInstance")
+ R startProcessInstance(@RequestBody ProcessInstanceParamDto paramDto);
- /**
- * 启动流程实例
- *
- * @param paramDto param dto
- * @return {@link R } 流程实例ID {@link String}
- */
- @PostMapping("/process-instance/startProcessInstance")
- R startProcessInstance(@RequestBody ProcessInstanceParamDto paramDto);
+ /**
+ * 内部启动流程实例(无登录态 / 公开业务回调场景)
+ */
+ @NoToken
+ @PostMapping("/process-instance/startInnerProcessInstance")
+ R startInnerProcessInstance(@RequestBody ProcessInstanceParamDto paramDto);
- /**
- * 查询当前用户的代办任务
- *
- * @param queryParamDto
- * @return {@link R } 流程实例ID {@link String}
- */
- @PostMapping("/process-instance/queryMineTask")
- R> queryMineTask(@RequestBody TaskQueryParamDto queryParamDto);
+ /**
+ * 查询当前用户的代办任务
+ */
+ @PostMapping("/process-instance/queryMineTask")
+ R> queryMineTask(@RequestBody TaskQueryParamDto queryParamDto);
- /**
- * 查询当前用户的已任务
- *
- * @param queryParamDto
- * @return {@link R } 流程实例ID {@link String}
- */
- @PostMapping("/process-instance/queryMineEndTask")
- R> queryMineEndTask(@RequestBody TaskQueryParamDto queryParamDto);
+ /**
+ * 查询当前用户的已办任务
+ */
+ @PostMapping("/process-instance/queryMineEndTask")
+ R> queryMineEndTask(@RequestBody TaskQueryParamDto queryParamDto);
- /**
- * 获取流程数据
- * @param businessKey 业务key
- * @return
- */
- @GetMapping("/process/getProcessByBusinessKey")
- R getProcessByBusinessKey(@RequestParam("businessKey")String businessKey);
+ /**
+ * 获取流程数据
+ * @param businessKey 业务key
+ */
+ @GetMapping("/process/getProcessByBusinessKey")
+ R getProcessByBusinessKey(@RequestParam("businessKey") String businessKey);
+
+ /**
+ * 内部获取流程数据(无登录态)
+ */
+ @NoToken
+ @GetMapping("/process/getInnerProcessByBusinessKey")
+ R getInnerProcessByBusinessKey(@RequestParam("businessKey") String businessKey);
}
diff --git a/pjcloud-flow/pjcloud-flow-task/pjcloud-flow-task-biz/src/main/java/com/bsd/pjcloud/flow/task/controller/ProcessController.java b/pjcloud-flow/pjcloud-flow-task/pjcloud-flow-task-biz/src/main/java/com/bsd/pjcloud/flow/task/controller/ProcessController.java
index 6af74206..c7a805b4 100644
--- a/pjcloud-flow/pjcloud-flow-task/pjcloud-flow-task-biz/src/main/java/com/bsd/pjcloud/flow/task/controller/ProcessController.java
+++ b/pjcloud-flow/pjcloud-flow-task/pjcloud-flow-task-biz/src/main/java/com/bsd/pjcloud/flow/task/controller/ProcessController.java
@@ -1,6 +1,7 @@
package com.bsd.pjcloud.flow.task.controller;
import com.bsd.pjcloud.common.core.util.R;
+import com.bsd.pjcloud.common.security.annotation.Inner;
import com.bsd.pjcloud.flow.task.entity.Process;
import com.bsd.pjcloud.flow.task.service.IProcessService;
import com.bsd.pjcloud.flow.task.vo.ProcessVO;
@@ -33,6 +34,16 @@ public class ProcessController {
public R getProcessByBusinessKey(@RequestParam("businessKey")String businessKey) {
return processService.getProcessByBusinessKey(businessKey);
}
+
+ /**
+ * 内部服务查询流程定义(免登录,需 FROM 内网标识)
+ */
+ @Inner
+ @GetMapping("/getInnerProcessByBusinessKey")
+ public R getInnerProcessByBusinessKey(@RequestParam("businessKey") String businessKey) {
+ return processService.getProcessByBusinessKey(businessKey);
+ }
+
/**
* 创建流程
* @param process
diff --git a/pjcloud-integrated/pjcloud-integrated-biz/pom.xml b/pjcloud-integrated/pjcloud-integrated-biz/pom.xml
index 821b1737..e1376208 100644
--- a/pjcloud-integrated/pjcloud-integrated-biz/pom.xml
+++ b/pjcloud-integrated/pjcloud-integrated-biz/pom.xml
@@ -106,11 +106,17 @@
pjcloud-common-seata
5.7.0
-
+
com.bsd
pjcloud-common-log
+
+
+ com.jcraft
+ jsch
+ 0.1.55
+
com.baomidou
diff --git a/pjcloud-integrated/pjcloud-integrated-biz/src/main/java/com/bsd/pjcloud/integrated/controller/app/AppVersionController.java b/pjcloud-integrated/pjcloud-integrated-biz/src/main/java/com/bsd/pjcloud/integrated/controller/app/AppVersionController.java
index c8c66f87..b09c85b0 100644
--- a/pjcloud-integrated/pjcloud-integrated-biz/src/main/java/com/bsd/pjcloud/integrated/controller/app/AppVersionController.java
+++ b/pjcloud-integrated/pjcloud-integrated-biz/src/main/java/com/bsd/pjcloud/integrated/controller/app/AppVersionController.java
@@ -7,7 +7,9 @@ import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.common.log.annotation.SysLog;
import com.bsd.pjcloud.integrated.dto.app.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.app.AppPackageUploadVo;
import com.bsd.pjcloud.integrated.vo.app.AppVersionVo;
import org.springframework.security.access.prepost.PreAuthorize;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
@@ -17,6 +19,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版本控制表
@@ -29,6 +32,7 @@ import org.springframework.web.bind.annotation.*;
public class AppVersionController {
private final AppVersionService appVersionService;
+ private final AppVersionPackageUploadService appVersionPackageUploadService;
/* ************************************* start 设备端 start ************************************* */
@Operation(summary = "分页查询", description = "分页查询")
@@ -45,6 +49,16 @@ public class AppVersionController {
public R 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 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 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版本控制表
*/
diff --git a/pjcloud-integrated/pjcloud-integrated-biz/src/main/java/com/bsd/pjcloud/integrated/service/AppVersionPackageUploadService.java b/pjcloud-integrated/pjcloud-integrated-biz/src/main/java/com/bsd/pjcloud/integrated/service/AppVersionPackageUploadService.java
new file mode 100644
index 00000000..d7f88e43
--- /dev/null
+++ b/pjcloud-integrated/pjcloud-integrated-biz/src/main/java/com/bsd/pjcloud/integrated/service/AppVersionPackageUploadService.java
@@ -0,0 +1,24 @@
+package com.bsd.pjcloud.integrated.service;
+
+import com.bsd.pjcloud.integrated.vo.app.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.zianxin.com/download
+ * @param publicBaseUrl 对外访问前缀,如 https://h5.zianxin.com/download
+ */
+ AppPackageUploadVo uploadBySftp(MultipartFile file, String host, Integer port, String username, String password,
+ String remoteDir, String publicBaseUrl);
+}
diff --git a/pjcloud-integrated/pjcloud-integrated-biz/src/main/java/com/bsd/pjcloud/integrated/service/AppVersionService.java b/pjcloud-integrated/pjcloud-integrated-biz/src/main/java/com/bsd/pjcloud/integrated/service/AppVersionService.java
index c8294d87..ddd51393 100755
--- a/pjcloud-integrated/pjcloud-integrated-biz/src/main/java/com/bsd/pjcloud/integrated/service/AppVersionService.java
+++ b/pjcloud-integrated/pjcloud-integrated-biz/src/main/java/com/bsd/pjcloud/integrated/service/AppVersionService.java
@@ -26,4 +26,9 @@ public interface AppVersionService extends IService {
* @return 仅包含匹配终端的已发布版本
*/
Page terminalPageList(Page page, AppVersionDto dto);
+
+ /**
+ * 安卓整包(.apk)最新已发布版本,供邀请下载二维码使用。
+ */
+ AppVersionEntity latestAndroidApk();
}
diff --git a/pjcloud-integrated/pjcloud-integrated-biz/src/main/java/com/bsd/pjcloud/integrated/service/impl/AppVersionPackageUploadServiceImpl.java b/pjcloud-integrated/pjcloud-integrated-biz/src/main/java/com/bsd/pjcloud/integrated/service/impl/AppVersionPackageUploadServiceImpl.java
new file mode 100644
index 00000000..7bd5b5d6
--- /dev/null
+++ b/pjcloud-integrated/pjcloud-integrated-biz/src/main/java/com/bsd/pjcloud/integrated/service/impl/AppVersionPackageUploadServiceImpl.java
@@ -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.app.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 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;
+ }
+}
diff --git a/pjcloud-integrated/pjcloud-integrated-biz/src/main/java/com/bsd/pjcloud/integrated/service/impl/app/AppVersionServiceImpl.java b/pjcloud-integrated/pjcloud-integrated-biz/src/main/java/com/bsd/pjcloud/integrated/service/impl/app/AppVersionServiceImpl.java
index 1355b227..ba3fc1d7 100755
--- a/pjcloud-integrated/pjcloud-integrated-biz/src/main/java/com/bsd/pjcloud/integrated/service/impl/app/AppVersionServiceImpl.java
+++ b/pjcloud-integrated/pjcloud-integrated-biz/src/main/java/com/bsd/pjcloud/integrated/service/impl/app/AppVersionServiceImpl.java
@@ -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.app.AppPlatformEnum;
import com.bsd.pjcloud.integrated.constant.app.AppVersionStatusEnum;
import com.bsd.pjcloud.integrated.dto.app.AppVersionDto;
import com.bsd.pjcloud.integrated.entity.app.AppVersionEntity;
@@ -58,4 +59,18 @@ public class AppVersionServiceImpl extends ServiceImpl排除 .wgt 热更补丁,供部门邀请下载二维码使用。
+ */
+ @Override
+ public AppVersionEntity latestAndroidApk() {
+ return baseMapper.selectOne(new LambdaQueryWrapper()
+ .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"));
+ }
}
diff --git a/pjcloud-integrated/pjcloud-integrated-biz/src/main/java/com/bsd/pjcloud/integrated/vo/app/AppPackageUploadVo.java b/pjcloud-integrated/pjcloud-integrated-biz/src/main/java/com/bsd/pjcloud/integrated/vo/app/AppPackageUploadVo.java
new file mode 100644
index 00000000..50029e39
--- /dev/null
+++ b/pjcloud-integrated/pjcloud-integrated-biz/src/main/java/com/bsd/pjcloud/integrated/vo/app/AppPackageUploadVo.java
@@ -0,0 +1,23 @@
+package com.bsd.pjcloud.integrated.vo.app;
+
+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;
+}
diff --git a/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/dto/DeptInviteRegisterDTO.java b/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/dto/DeptInviteRegisterDTO.java
new file mode 100644
index 00000000..43f81098
--- /dev/null
+++ b/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/dto/DeptInviteRegisterDTO.java
@@ -0,0 +1,30 @@
+package com.bsd.pjcloud.admin.api.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+/**
+ * 部门邀请注册请求
+ */
+@Data
+@Schema(description = "部门邀请注册")
+public class DeptInviteRegisterDTO {
+
+ @Schema(description = "邀请 token", requiredMode = Schema.RequiredMode.REQUIRED)
+ private String token;
+
+ @Schema(description = "账号(可与手机号相同)", requiredMode = Schema.RequiredMode.REQUIRED)
+ private String username;
+
+ @Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED)
+ private String password;
+
+ @Schema(description = "姓名", requiredMode = Schema.RequiredMode.REQUIRED)
+ private String name;
+
+ @Schema(description = "手机号", requiredMode = Schema.RequiredMode.REQUIRED)
+ private String phone;
+
+ @Schema(description = "性别 0未知 1男 2女")
+ private Integer gender;
+}
diff --git a/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/dto/SysDeptInviteSaveDTO.java b/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/dto/SysDeptInviteSaveDTO.java
new file mode 100644
index 00000000..b1afec15
--- /dev/null
+++ b/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/dto/SysDeptInviteSaveDTO.java
@@ -0,0 +1,44 @@
+package com.bsd.pjcloud.admin.api.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 保存/更新部门邀请配置
+ */
+@Data
+@Schema(description = "部门邀请配置")
+public class SysDeptInviteSaveDTO {
+
+ @Schema(description = "部门ID", requiredMode = Schema.RequiredMode.REQUIRED)
+ private Long deptId;
+
+ @Schema(description = "是否需要审核")
+ private Boolean needAudit;
+
+ @Schema(description = "是否启用,默认 true")
+ private Boolean enabled;
+
+ @Schema(description = "App 下载地址,用于生成二维码")
+ private String downloadUrl;
+
+ @Schema(description = "邀请码类型:DIGIT6 / DIGIT8 / UUID")
+ private String tokenType;
+
+ @Schema(description = "是否重新生成邀请码")
+ private Boolean regenerateToken;
+
+ @Schema(description = "生效开始时间,空=立即")
+ private LocalDateTime validFrom;
+
+ @Schema(description = "过期时间,空=长期有效")
+ private LocalDateTime expireTime;
+
+ @Schema(description = "是否清空过期时间(长期有效)")
+ private Boolean clearExpire;
+
+ @Schema(description = "备注")
+ private String remark;
+}
diff --git a/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/entity/SysDeptInviteEntity.java b/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/entity/SysDeptInviteEntity.java
new file mode 100644
index 00000000..7ab38ccc
--- /dev/null
+++ b/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/entity/SysDeptInviteEntity.java
@@ -0,0 +1,71 @@
+package com.bsd.pjcloud.admin.api.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.baomidou.mybatisplus.extension.activerecord.Model;
+import com.bsd.pjcloud.common.core.util.TenantTable;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.LocalDateTime;
+
+/**
+ * 部门邀请注册配置
+ */
+@Data
+@TenantTable
+@TableName("sys_dept_invite")
+@EqualsAndHashCode(callSuper = true)
+@Schema(description = "部门邀请注册配置")
+public class SysDeptInviteEntity extends Model {
+
+ @TableId(type = IdType.ASSIGN_ID)
+ private Long id;
+
+ @Schema(description = "目标加入部门")
+ private Long deptId;
+
+ @Schema(description = "邀请码")
+ private String token;
+
+ @Schema(description = "邀请码类型 DIGIT6/DIGIT8/UUID")
+ private String tokenType;
+
+ @Schema(description = "App 下载地址(二维码内容)")
+ private String downloadUrl;
+
+ @Schema(description = "是否需要审核 1是0否")
+ private String needAudit;
+
+ @Schema(description = "是否启用 1启用0停用")
+ private String enabled;
+
+ @Schema(description = "生效开始时间,空=立即")
+ @TableField(updateStrategy = FieldStrategy.ALWAYS)
+ private LocalDateTime validFrom;
+
+ @Schema(description = "过期时间,空=长期")
+ @TableField(updateStrategy = FieldStrategy.ALWAYS)
+ private LocalDateTime expireTime;
+
+ @Schema(description = "备注")
+ private String remark;
+
+ @TableField(fill = FieldFill.INSERT)
+ private String createBy;
+
+ @TableField(fill = FieldFill.INSERT)
+ private LocalDateTime createTime;
+
+ @TableField(fill = FieldFill.INSERT_UPDATE)
+ private String updateBy;
+
+ @TableField(fill = FieldFill.INSERT_UPDATE)
+ private LocalDateTime updateTime;
+
+ @TableLogic
+ @TableField(fill = FieldFill.INSERT)
+ private String delFlag;
+
+ private Long tenantId;
+}
diff --git a/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/vo/SysDeptInviteListVO.java b/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/vo/SysDeptInviteListVO.java
new file mode 100644
index 00000000..70151e74
--- /dev/null
+++ b/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/vo/SysDeptInviteListVO.java
@@ -0,0 +1,49 @@
+package com.bsd.pjcloud.admin.api.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 部门邀请列表项(管理端)
+ */
+@Data
+@Schema(description = "部门邀请列表")
+public class SysDeptInviteListVO {
+
+ private Long id;
+
+ private Long deptId;
+
+ @Schema(description = "部门名称")
+ private String deptName;
+
+ @Schema(description = "邀请码")
+ private String token;
+
+ @Schema(description = "邀请码类型")
+ private String tokenType;
+
+ @Schema(description = "是否需要审核 1是0否")
+ private String needAudit;
+
+ @Schema(description = "是否启用 1启用0停用")
+ private String enabled;
+
+ private LocalDateTime validFrom;
+
+ private LocalDateTime expireTime;
+
+ private String remark;
+
+ private LocalDateTime createTime;
+
+ private LocalDateTime updateTime;
+
+ @Schema(description = "状态:ENABLED/DISABLED/NOT_STARTED/EXPIRED")
+ private String status;
+
+ @Schema(description = "状态文案")
+ private String statusLabel;
+}
diff --git a/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/vo/SysDeptInvitePreviewVO.java b/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/vo/SysDeptInvitePreviewVO.java
new file mode 100644
index 00000000..cd4dbb5f
--- /dev/null
+++ b/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/vo/SysDeptInvitePreviewVO.java
@@ -0,0 +1,42 @@
+package com.bsd.pjcloud.admin.api.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+/**
+ * 邀请入口公开信息(注册页预览)
+ */
+@Data
+@Schema(description = "部门邀请预览")
+public class SysDeptInvitePreviewVO {
+
+ private String token;
+
+ private Long deptId;
+
+ private String deptName;
+
+ @Schema(description = "是否需要审核")
+ private Boolean needAudit;
+
+ @Schema(description = "解析到的商户ID")
+ private Long merchantId;
+
+ @Schema(description = "商户属性")
+ private String merchantAttr;
+
+ @Schema(description = "商户属性名称")
+ private String merchantAttrLabel;
+
+ @Schema(description = "默认身份 DEFAULT/STUDENT")
+ private String identityType;
+
+ @Schema(description = "默认身份名称")
+ private String identityTypeLabel;
+
+ @Schema(description = "生效开始时间")
+ private java.time.LocalDateTime validFrom;
+
+ @Schema(description = "过期时间")
+ private java.time.LocalDateTime expireTime;
+}
diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysDeptInviteController.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysDeptInviteController.java
new file mode 100644
index 00000000..fa7153ba
--- /dev/null
+++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysDeptInviteController.java
@@ -0,0 +1,74 @@
+package com.bsd.pjcloud.admin.controller;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.bsd.pjcloud.admin.api.dto.SysDeptInviteSaveDTO;
+import com.bsd.pjcloud.admin.api.entity.SysDeptInviteEntity;
+import com.bsd.pjcloud.admin.api.vo.SysDeptInviteListVO;
+import com.bsd.pjcloud.admin.service.SysDeptInviteService;
+import com.bsd.pjcloud.common.core.util.R;
+import com.bsd.pjcloud.common.log.annotation.SysLog;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import org.springdoc.core.annotations.ParameterObject;
+import org.springframework.http.HttpHeaders;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 部门邀请配置(管理端,需登录权限)
+ */
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/dept/invite")
+@Tag(description = "deptInvite", name = "部门邀请注册-管理")
+@SecurityRequirement(name = HttpHeaders.AUTHORIZATION)
+public class SysDeptInviteController {
+
+ private final SysDeptInviteService deptInviteService;
+
+ @Operation(summary = "获取或生成部门邀请配置")
+ @SysLog("生成部门邀请")
+ @PostMapping
+ @PreAuthorize("@pms.hasPermission('sys_dept_edit')")
+ public R getOrCreate(@RequestBody SysDeptInviteSaveDTO dto) {
+ try {
+ return R.ok(deptInviteService.getOrCreate(dto));
+ }
+ catch (IllegalArgumentException ex) {
+ return R.failed(ex.getMessage());
+ }
+ }
+
+ @Operation(summary = "按部门查询邀请配置")
+ @GetMapping("/by-dept/{deptId}")
+ @PreAuthorize("@pms.hasPermission('sys_dept_edit')")
+ public R getByDept(@PathVariable Long deptId) {
+ return R.ok(deptInviteService.getByDeptId(deptId));
+ }
+
+ @Operation(summary = "部门邀请分页列表")
+ @GetMapping("/page")
+ @PreAuthorize("@pms.hasPermission('sys_dept_edit')")
+ public R> page(@ParameterObject Page page,
+ @RequestParam(required = false) String deptName,
+ @RequestParam(required = false) String token,
+ @RequestParam(required = false) String enabled) {
+ return R.ok(deptInviteService.pageList(page, deptName, token, enabled));
+ }
+
+ @Operation(summary = "删除部门邀请配置")
+ @SysLog("删除部门邀请")
+ @DeleteMapping("/{id}")
+ @PreAuthorize("@pms.hasPermission('sys_dept_edit')")
+ public R remove(@PathVariable Long id) {
+ try {
+ return R.ok(deptInviteService.removeInvite(id));
+ }
+ catch (IllegalArgumentException ex) {
+ return R.failed(ex.getMessage());
+ }
+ }
+}
diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysDeptInvitePublicController.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysDeptInvitePublicController.java
new file mode 100644
index 00000000..9dd3c799
--- /dev/null
+++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysDeptInvitePublicController.java
@@ -0,0 +1,48 @@
+package com.bsd.pjcloud.admin.controller;
+
+import com.bsd.pjcloud.admin.api.dto.DeptInviteRegisterDTO;
+import com.bsd.pjcloud.admin.api.vo.SysDeptInvitePreviewVO;
+import com.bsd.pjcloud.admin.service.SysDeptInviteService;
+import com.bsd.pjcloud.common.core.util.R;
+import com.bsd.pjcloud.common.log.annotation.SysLog;
+import com.bsd.pjcloud.common.security.annotation.Inner;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 部门邀请公开接口(免登录),与管理端配置接口分离,避免鉴权白名单漏扫。
+ */
+@Inner(value = false)
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/dept/invite")
+@Tag(description = "deptInvitePublic", name = "部门邀请注册-公开")
+public class SysDeptInvitePublicController {
+
+ private final SysDeptInviteService deptInviteService;
+
+ @Operation(summary = "邀请入口预览(公开)")
+ @GetMapping("/preview")
+ public R preview(@RequestParam("token") String token) {
+ try {
+ return R.ok(deptInviteService.preview(token));
+ }
+ catch (IllegalArgumentException ex) {
+ return R.failed(ex.getMessage());
+ }
+ }
+
+ @SysLog("部门邀请注册")
+ @Operation(summary = "邀请注册(公开)")
+ @PostMapping("/register")
+ public R register(@RequestBody DeptInviteRegisterDTO dto) {
+ try {
+ return deptInviteService.register(dto);
+ }
+ catch (IllegalArgumentException ex) {
+ return R.failed(ex.getMessage());
+ }
+ }
+}
diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/mapper/SysDeptInviteMapper.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/mapper/SysDeptInviteMapper.java
new file mode 100644
index 00000000..0d53f12f
--- /dev/null
+++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/mapper/SysDeptInviteMapper.java
@@ -0,0 +1,9 @@
+package com.bsd.pjcloud.admin.mapper;
+
+import com.bsd.pjcloud.admin.api.entity.SysDeptInviteEntity;
+import com.bsd.pjcloud.common.data.datascope.PjcloudBaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface SysDeptInviteMapper extends PjcloudBaseMapper {
+}
diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/SysDeptInviteService.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/SysDeptInviteService.java
new file mode 100644
index 00000000..769e3e22
--- /dev/null
+++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/SysDeptInviteService.java
@@ -0,0 +1,32 @@
+package com.bsd.pjcloud.admin.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.bsd.pjcloud.admin.api.dto.DeptInviteRegisterDTO;
+import com.bsd.pjcloud.admin.api.dto.SysDeptInviteSaveDTO;
+import com.bsd.pjcloud.admin.api.entity.SysDeptInviteEntity;
+import com.bsd.pjcloud.admin.api.vo.SysDeptInviteListVO;
+import com.bsd.pjcloud.admin.api.vo.SysDeptInvitePreviewVO;
+import com.bsd.pjcloud.common.core.util.R;
+
+/**
+ * 部门邀请注册
+ */
+public interface SysDeptInviteService {
+
+ SysDeptInviteEntity getOrCreate(SysDeptInviteSaveDTO dto);
+
+ SysDeptInviteEntity updateConfig(SysDeptInviteSaveDTO dto);
+
+ SysDeptInviteEntity getByDeptId(Long deptId);
+
+ /** 管理端分页列表 */
+ IPage pageList(Page> page, String deptName, String token, String enabled);
+
+ /** 删除邀请配置(逻辑删除) */
+ boolean removeInvite(Long id);
+
+ SysDeptInvitePreviewVO preview(String token);
+
+ R register(DeptInviteRegisterDTO dto);
+}
diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysDeptInviteServiceImpl.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysDeptInviteServiceImpl.java
new file mode 100644
index 00000000..d61bcf38
--- /dev/null
+++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysDeptInviteServiceImpl.java
@@ -0,0 +1,543 @@
+package com.bsd.pjcloud.admin.service.impl;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.IdUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.RandomUtil;
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.bsd.pjcloud.admin.api.constant.UserExpireEnum;
+import com.bsd.pjcloud.admin.api.constant.UserStaffingTypeEnum;
+import com.bsd.pjcloud.admin.api.constant.UserStateEnum;
+import com.bsd.pjcloud.admin.api.constant.user.UserIdentityTypeEnum;
+import com.bsd.pjcloud.admin.api.dto.DeptInviteRegisterDTO;
+import com.bsd.pjcloud.admin.api.dto.SysDeptInviteSaveDTO;
+import com.bsd.pjcloud.admin.api.dto.UserDTO;
+import com.bsd.pjcloud.admin.api.entity.SysDept;
+import com.bsd.pjcloud.admin.api.entity.SysDeptInviteEntity;
+import com.bsd.pjcloud.admin.api.entity.SysRole;
+import com.bsd.pjcloud.admin.api.entity.SysUser;
+import com.bsd.pjcloud.admin.api.entity.SysUserRole;
+import com.bsd.pjcloud.admin.api.util.PersonNameUtils;
+import com.bsd.pjcloud.admin.api.vo.SysDeptInviteListVO;
+import com.bsd.pjcloud.admin.api.vo.SysDeptInvitePreviewVO;
+import com.bsd.pjcloud.admin.mapper.SysDeptInviteMapper;
+import com.bsd.pjcloud.admin.mapper.SysUserMapper;
+import com.bsd.pjcloud.admin.mapper.SysUserRoleMapper;
+import com.bsd.pjcloud.admin.service.SysDeptInviteService;
+import com.bsd.pjcloud.admin.service.SysDeptService;
+import com.bsd.pjcloud.admin.service.SysRoleService;
+import com.bsd.pjcloud.admin.service.SysUserIdentityService;
+import com.bsd.pjcloud.admin.utils.flow.FlowUtil;
+import com.bsd.pjcloud.admin.utils.flow.dto.UserApplyFlowDto;
+import com.bsd.pjcloud.common.core.constant.enums.MerchantAttrEnum;
+import com.bsd.pjcloud.common.core.exception.ErrorCodes;
+import com.bsd.pjcloud.common.core.util.MsgUtils;
+import com.bsd.pjcloud.common.core.util.R;
+import com.bsd.pjcloud.common.data.resolver.ParamResolver;
+import com.bsd.pjcloud.flow.task.constant.TaskCheckStatusEnum;
+import com.bsd.pjcloud.wallet.api.feign.RemoteMchService;
+import com.bsd.pjcloud.wallet.api.feign.RemoteWalletService;
+import com.bsd.pjcloud.wallet.api.vo.MchManageVo;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.context.request.RequestAttributes;
+import org.springframework.web.context.request.RequestContextHolder;
+
+import java.time.LocalDateTime;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+/**
+ * 部门邀请注册:生成链接/二维码入口,按部门→单位部门向上解析商户属性决定默认身份。
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class SysDeptInviteServiceImpl extends ServiceImpl
+ implements SysDeptInviteService {
+
+ private static final String TAG = "SysDeptInvite";
+ private static final PasswordEncoder ENCODER = new BCryptPasswordEncoder();
+
+ private final SysDeptService sysDeptService;
+ private final SysUserMapper sysUserMapper;
+ private final SysUserRoleMapper sysUserRoleMapper;
+ private final SysRoleService sysRoleService;
+ private final SysUserIdentityService sysUserIdentityService;
+ private final RemoteMchService remoteMchService;
+ private final RemoteWalletService remoteWalletService;
+ private final FlowUtil flowUtil;
+
+ /** 邀请码类型:6 位数字 / 8 位数字 / UUID(默认) */
+ private static final String TOKEN_DIGIT6 = "DIGIT6";
+ private static final String TOKEN_DIGIT8 = "DIGIT8";
+ private static final String TOKEN_UUID = "UUID";
+ /** 登录账号:4-20 位,字母开头,仅字母数字下划线 */
+ private static final Pattern USERNAME_PATTERN = Pattern.compile("^[a-zA-Z][a-zA-Z0-9_]{3,19}$");
+ /** 中国大陆手机号 */
+ private static final Pattern PHONE_PATTERN = Pattern.compile(
+ "^1(3\\d|4[01456879]|5[0-35-9]|6[2567]|7[0-8]|8\\d|9[0-35-9])\\d{8}$");
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public SysDeptInviteEntity getOrCreate(SysDeptInviteSaveDTO dto) {
+ if (dto == null || dto.getDeptId() == null) {
+ throw new IllegalArgumentException("部门不能为空");
+ }
+ SysDept dept = sysDeptService.getById(dto.getDeptId());
+ if (dept == null) {
+ throw new IllegalArgumentException("部门不存在");
+ }
+ validateExpireRange(dto.getValidFrom(), dto.getExpireTime());
+ String tokenType = normalizeTokenType(dto.getTokenType());
+ SysDeptInviteEntity exist = getByDeptId(dto.getDeptId());
+ if (exist != null) {
+ if (dto.getNeedAudit() != null) {
+ exist.setNeedAudit(Boolean.TRUE.equals(dto.getNeedAudit()) ? "1" : "0");
+ }
+ if (dto.getEnabled() != null) {
+ exist.setEnabled(Boolean.FALSE.equals(dto.getEnabled()) ? "0" : "1");
+ }
+ if (dto.getRemark() != null) {
+ exist.setRemark(dto.getRemark());
+ }
+ if (StrUtil.isNotBlank(dto.getDownloadUrl())) {
+ exist.setDownloadUrl(dto.getDownloadUrl().trim());
+ }
+ // 有效时段:clearExpire=长期;否则有入参才覆盖(避免仅改审核时误清空)
+ if (Boolean.TRUE.equals(dto.getClearExpire())) {
+ exist.setValidFrom(null);
+ exist.setExpireTime(null);
+ }
+ else if (dto.getValidFrom() != null || dto.getExpireTime() != null) {
+ exist.setValidFrom(dto.getValidFrom());
+ exist.setExpireTime(dto.getExpireTime());
+ }
+ boolean regenerate = Boolean.TRUE.equals(dto.getRegenerateToken())
+ || (StrUtil.isNotBlank(dto.getTokenType())
+ && !tokenType.equals(StrUtil.blankToDefault(exist.getTokenType(), TOKEN_UUID)));
+ if (regenerate) {
+ exist.setTokenType(tokenType);
+ exist.setToken(generateUniqueToken(tokenType, exist.getId()));
+ }
+ else if (StrUtil.isNotBlank(dto.getTokenType())) {
+ exist.setTokenType(tokenType);
+ }
+ updateById(exist);
+ return exist;
+ }
+ SysDeptInviteEntity entity = new SysDeptInviteEntity();
+ entity.setDeptId(dto.getDeptId());
+ entity.setTokenType(tokenType);
+ entity.setToken(generateUniqueToken(tokenType, null));
+ entity.setDownloadUrl(StrUtil.trim(dto.getDownloadUrl()));
+ entity.setNeedAudit(Boolean.TRUE.equals(dto.getNeedAudit()) ? "1" : "0");
+ entity.setEnabled(Boolean.FALSE.equals(dto.getEnabled()) ? "0" : "1");
+ entity.setValidFrom(dto.getValidFrom());
+ entity.setExpireTime(Boolean.TRUE.equals(dto.getClearExpire()) ? null : dto.getExpireTime());
+ entity.setRemark(dto.getRemark());
+ save(entity);
+ return entity;
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public SysDeptInviteEntity updateConfig(SysDeptInviteSaveDTO dto) {
+ return getOrCreate(dto);
+ }
+
+ @Override
+ public SysDeptInviteEntity getByDeptId(Long deptId) {
+ if (deptId == null) {
+ return null;
+ }
+ return getOne(Wrappers.lambdaQuery()
+ .eq(SysDeptInviteEntity::getDeptId, deptId)
+ .last("LIMIT 1"));
+ }
+
+ @Override
+ public IPage pageList(Page> page, String deptName, String token, String enabled) {
+ List deptIds = null;
+ if (StrUtil.isNotBlank(deptName)) {
+ List depts = sysDeptService.list(Wrappers.lambdaQuery()
+ .like(SysDept::getName, deptName.trim()));
+ if (CollUtil.isEmpty(depts)) {
+ Page empty = new Page<>(page.getCurrent(), page.getSize(), 0);
+ empty.setRecords(List.of());
+ return empty;
+ }
+ deptIds = depts.stream().map(SysDept::getDeptId).filter(Objects::nonNull).toList();
+ }
+ Page entityPage = page(new Page<>(page.getCurrent(), page.getSize()),
+ Wrappers.lambdaQuery()
+ .in(CollUtil.isNotEmpty(deptIds), SysDeptInviteEntity::getDeptId, deptIds)
+ .like(StrUtil.isNotBlank(token), SysDeptInviteEntity::getToken, StrUtil.trim(token))
+ .eq(StrUtil.isNotBlank(enabled), SysDeptInviteEntity::getEnabled, enabled)
+ .orderByDesc(SysDeptInviteEntity::getUpdateTime)
+ .orderByDesc(SysDeptInviteEntity::getCreateTime));
+ List records = entityPage.getRecords();
+ Map deptNameMap = new HashMap<>();
+ if (CollUtil.isNotEmpty(records)) {
+ List ids = records.stream().map(SysDeptInviteEntity::getDeptId).filter(Objects::nonNull).distinct()
+ .toList();
+ if (CollUtil.isNotEmpty(ids)) {
+ deptNameMap = sysDeptService.listByIds(ids).stream()
+ .filter(d -> d.getDeptId() != null)
+ .collect(Collectors.toMap(SysDept::getDeptId, SysDept::getName, (a, b) -> a));
+ }
+ }
+ Map finalDeptNameMap = deptNameMap;
+ Page voPage = new Page<>(entityPage.getCurrent(), entityPage.getSize(), entityPage.getTotal());
+ voPage.setRecords(records.stream().map(e -> toListVO(e, finalDeptNameMap.get(e.getDeptId()))).toList());
+ return voPage;
+ }
+
+ private SysDeptInviteListVO toListVO(SysDeptInviteEntity entity, String deptName) {
+ SysDeptInviteListVO vo = new SysDeptInviteListVO();
+ vo.setId(entity.getId());
+ vo.setDeptId(entity.getDeptId());
+ vo.setDeptName(deptName);
+ vo.setToken(entity.getToken());
+ vo.setTokenType(entity.getTokenType());
+ vo.setNeedAudit(entity.getNeedAudit());
+ vo.setEnabled(entity.getEnabled());
+ vo.setValidFrom(entity.getValidFrom());
+ vo.setExpireTime(entity.getExpireTime());
+ vo.setRemark(entity.getRemark());
+ vo.setCreateTime(entity.getCreateTime());
+ vo.setUpdateTime(entity.getUpdateTime());
+ LocalDateTime now = LocalDateTime.now();
+ if (!"1".equals(entity.getEnabled())) {
+ vo.setStatus("DISABLED");
+ vo.setStatusLabel("已停用");
+ }
+ else if (entity.getValidFrom() != null && entity.getValidFrom().isAfter(now)) {
+ vo.setStatus("NOT_STARTED");
+ vo.setStatusLabel("未生效");
+ }
+ else if (entity.getExpireTime() != null && entity.getExpireTime().isBefore(now)) {
+ vo.setStatus("EXPIRED");
+ vo.setStatusLabel("已过期");
+ }
+ else {
+ vo.setStatus("ENABLED");
+ vo.setStatusLabel("可用");
+ }
+ return vo;
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public boolean removeInvite(Long id) {
+ if (id == null) {
+ throw new IllegalArgumentException("邀请配置不能为空");
+ }
+ SysDeptInviteEntity exist = getById(id);
+ if (exist == null) {
+ throw new IllegalArgumentException("邀请配置不存在或已删除");
+ }
+ return removeById(id);
+ }
+
+ @Override
+ public SysDeptInvitePreviewVO preview(String token) {
+ SysDeptInviteEntity invite = requireValidInvite(token);
+ SysDept dept = sysDeptService.getById(invite.getDeptId());
+ MchManageVo mch = resolveMerchantAlongUnitChain(invite.getDeptId());
+ String attr = mch != null ? StrUtil.blankToDefault(mch.getMerchantAttr(), MerchantAttrEnum.NORMAL.getCode())
+ : MerchantAttrEnum.NORMAL.getCode();
+ MerchantAttrEnum attrEnum = MerchantAttrEnum.valueOfByCode(attr);
+ String identityType = MerchantAttrEnum.resolveDefaultIdentityType(attr);
+ UserIdentityTypeEnum identity = UserIdentityTypeEnum.valueOfByType(identityType);
+
+ SysDeptInvitePreviewVO vo = new SysDeptInvitePreviewVO();
+ vo.setToken(invite.getToken());
+ vo.setDeptId(invite.getDeptId());
+ vo.setDeptName(dept != null ? dept.getName() : null);
+ vo.setNeedAudit("1".equals(invite.getNeedAudit()));
+ vo.setMerchantId(mch != null ? mch.getId() : null);
+ vo.setMerchantAttr(attr);
+ vo.setMerchantAttrLabel(attrEnum != null ? attrEnum.getLabel() : MerchantAttrEnum.NORMAL.getLabel());
+ vo.setIdentityType(identityType);
+ vo.setIdentityTypeLabel(identity != null ? identity.getDescription() : UserIdentityTypeEnum.DEFAULT.getDescription());
+ vo.setValidFrom(invite.getValidFrom());
+ vo.setExpireTime(invite.getExpireTime());
+ return vo;
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public R register(DeptInviteRegisterDTO dto) {
+ if (dto == null || StrUtil.isBlank(dto.getToken())) {
+ return R.failed("邀请码无效");
+ }
+ if (StrUtil.isBlank(dto.getUsername()) || StrUtil.isBlank(dto.getPassword())
+ || StrUtil.isBlank(dto.getName()) || StrUtil.isBlank(dto.getPhone())) {
+ return R.failed("请完整填写账号、密码、姓名、手机号");
+ }
+ String username = dto.getUsername().trim();
+ String password = dto.getPassword();
+ String phone = dto.getPhone().trim();
+ String usernameErr = validateUsername(username);
+ if (usernameErr != null) {
+ return R.failed(usernameErr);
+ }
+ String passwordErr = validateInvitePassword(password);
+ if (passwordErr != null) {
+ return R.failed(passwordErr);
+ }
+ String nameErr = validateInviteName(dto.getName());
+ if (nameErr != null) {
+ return R.failed(nameErr);
+ }
+ if (!PHONE_PATTERN.matcher(phone).matches()) {
+ return R.failed("请输入正确的手机号");
+ }
+
+ SysDeptInviteEntity invite = requireValidInvite(dto.getToken());
+ boolean usernameExists = sysUserMapper.exists(Wrappers.lambdaQuery().eq(SysUser::getUsername, username));
+ if (usernameExists) {
+ return R.failed(MsgUtils.getMessage(ErrorCodes.SYS_USER_USERNAME_EXISTING, username));
+ }
+ boolean phoneExists = sysUserMapper.exists(Wrappers.lambdaQuery().eq(SysUser::getPhone, phone));
+ if (phoneExists) {
+ return R.failed(MsgUtils.getMessage(ErrorCodes.SYS_USER_PHONE_EXISTING, phone));
+ }
+
+ MchManageVo mch = resolveMerchantAlongUnitChain(invite.getDeptId());
+ String attr = mch != null ? StrUtil.blankToDefault(mch.getMerchantAttr(), MerchantAttrEnum.NORMAL.getCode())
+ : MerchantAttrEnum.NORMAL.getCode();
+ String identityType = MerchantAttrEnum.resolveDefaultIdentityType(attr);
+ boolean needAudit = "1".equals(invite.getNeedAudit());
+
+ SysUser sysUser = new SysUser();
+ sysUser.setUsername(username);
+ sysUser.setPhone(phone);
+ sysUser.setName(PersonNameUtils.normalize(dto.getName()));
+ sysUser.setNickname(sysUser.getName());
+ sysUser.setGender(dto.getGender());
+ sysUser.setDeptId(invite.getDeptId());
+ // 与后台「新增用户审批」一致:正常账号 + 审核中状态,并真正发起 ADD_USER_FLOW 写入 processId
+ sysUser.setLockFlag(UserStateEnum.NORMAL.getCode());
+ sysUser.setCreateBy(username);
+ sysUser.setUpdateBy(username);
+ sysUser.setPassword(ENCODER.encode(password));
+ sysUser.setPasswordModifyTime(LocalDateTime.now());
+ sysUser.setProcessStatus(needAudit ? TaskCheckStatusEnum.PADDING.getCode() : TaskCheckStatusEnum.RESULT.getCode());
+ sysUser.setProcessTime(LocalDateTime.now());
+ sysUser.setStaffingType(UserStaffingTypeEnum.NONE_STAFFING.getType());
+ // 邀请注册已自设密码,无需强制改密;EXPIRE(9) 会被当成凭证过期无法登录
+ sysUser.setPasswordExpireFlag(UserExpireEnum.NORMAL.getCode());
+ sysUserMapper.insert(sysUser);
+
+ List roleList = sysRoleService.list();
+ String walletAutoCreate = ParamResolver.getStr("WALLET_AUTO_CREATE");
+ String userDefaultRole = ParamResolver.getStr("USER_DEFAULT_ROLE");
+ List roleIds = roleList.stream()
+ .filter(v -> ObjectUtil.equals(v.getRoleCode(), walletAutoCreate)
+ || ObjectUtil.equals(v.getRoleCode(), userDefaultRole))
+ .map(SysRole::getRoleId)
+ .toList();
+ for (Long roleId : roleIds) {
+ SysUserRole userRole = new SysUserRole();
+ userRole.setUserId(sysUser.getUserId());
+ userRole.setRoleId(roleId);
+ sysUserRoleMapper.insert(userRole);
+ }
+
+ UserDTO identityDto = new UserDTO();
+ identityDto.setIdentityTypes(List.of(identityType));
+ sysUserIdentityService.saveUserIdentity(sysUser.getUserId(), identityDto);
+
+ if (needAudit) {
+ startInviteUserFlow(sysUser, invite.getDeptId());
+ return R.ok(true, "注册成功,请等待审核通过后再登录");
+ }
+
+ RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
+ CompletableFuture.runAsync(() -> {
+ try {
+ RequestContextHolder.setRequestAttributes(requestAttributes);
+ remoteWalletService.createWallet(sysUser.getUserId());
+ remoteWalletService.userChange(sysUser.getUserId(), Boolean.FALSE);
+ }
+ catch (Exception e) {
+ log.error("|{}|创建用户钱包失败error:|{}|", TAG, e.getMessage());
+ }
+ });
+ return R.ok(true, "注册成功");
+ }
+
+ /**
+ * 邀请注册开启审核:走与后台新增用户相同的 ADD_USER_FLOW,保证用户列表可点「开始处理」。
+ */
+ private void startInviteUserFlow(SysUser sysUser, Long inviteDeptId) {
+ try {
+ SysDept inviteDept = sysDeptService.getById(inviteDeptId);
+ UserApplyFlowDto flowDto = new UserApplyFlowDto();
+ if (inviteDept != null) {
+ flowDto.setApplyDeptName(inviteDept.getName());
+ }
+ // 发起人为注册用户本人;审批人由 ADD_USER_FLOW 流程配置决定
+ String processId = flowUtil.startFlow("ADD_USER_FLOW", sysUser, sysUser.getUserId(), flowDto);
+ if (StrUtil.isBlank(processId)) {
+ throw new IllegalArgumentException("发起用户审核流程失败,请稍后重试或联系管理员检查 ADD_USER_FLOW 配置");
+ }
+ SysUser updateObj = new SysUser();
+ updateObj.setUserId(sysUser.getUserId());
+ updateObj.setProcessId(processId);
+ updateObj.setProcessStatus(TaskCheckStatusEnum.PADDING.getCode());
+ sysUserMapper.updateById(updateObj);
+ }
+ catch (IllegalArgumentException ex) {
+ throw ex;
+ }
+ catch (Exception ex) {
+ log.error("|{}|邀请注册发起审批流失败 userId:|{}| error:|{}|", TAG, sysUser.getUserId(), ex.getMessage());
+ throw new IllegalArgumentException(StrUtil.blankToDefault(ex.getMessage(), "发起用户审核流程失败,请检查 ADD_USER_FLOW 配置"));
+ }
+ }
+
+ private String validateUsername(String username) {
+ if (!USERNAME_PATTERN.matcher(username).matches()) {
+ return "账号需为4-20位,字母开头,仅支持字母、数字、下划线";
+ }
+ return null;
+ }
+
+ /** 与改密一致:8-20 位字母+数字组合 */
+ private String validateInvitePassword(String password) {
+ if (StrUtil.length(password) < 8 || StrUtil.length(password) > 20) {
+ return MsgUtils.getMessage(ErrorCodes.SYS_USER_PASSWORD_LENGTH);
+ }
+ boolean hasLetter = false;
+ boolean hasDigit = false;
+ for (char c : password.toCharArray()) {
+ if (Character.isLetter(c)) {
+ hasLetter = true;
+ }
+ else if (Character.isDigit(c)) {
+ hasDigit = true;
+ }
+ else {
+ return MsgUtils.getMessage(ErrorCodes.SYS_USER_PASSWORD_CHARS_ONLY);
+ }
+ }
+ if (!(hasLetter && hasDigit)) {
+ return MsgUtils.getMessage(ErrorCodes.SYS_USER_PASSWORD_LETTER_DIGIT);
+ }
+ return null;
+ }
+
+ /** 姓名:走统一规则,并拒绝纯数字/单字符等弱填写 */
+ private String validateInviteName(String name) {
+ String nameErr = PersonNameUtils.validateOrNull(name, "姓名");
+ if (nameErr != null) {
+ return nameErr;
+ }
+ String normalized = PersonNameUtils.normalize(name);
+ if (normalized.length() < 2) {
+ return "姓名至少2个字符";
+ }
+ if (!normalized.matches(".*[\\u4E00-\\u9FA5A-Za-z].*")) {
+ return "姓名需包含中文或英文字母";
+ }
+ return null;
+ }
+
+ /**
+ * 当前部门有绑定商户则用;否则沿上级链路只匹配 is_unit=1 的单位部门(禁止水平匹配同级)。
+ */
+ private MchManageVo resolveMerchantAlongUnitChain(Long deptId) {
+ List chain = sysDeptService.getParentDeptList(deptId);
+ if (CollUtil.isEmpty(chain)) {
+ return null;
+ }
+ for (int i = 0; i < chain.size(); i++) {
+ SysDept dept = chain.get(i);
+ if (dept == null || dept.getDeptId() == null) {
+ continue;
+ }
+ // 首个为邀请部门本身;其后仅单位部门可参与匹配
+ if (i > 0 && !Integer.valueOf(1).equals(dept.getIsUnit())) {
+ continue;
+ }
+ R> resp = remoteMchService.getByDeptIds(List.of(dept.getDeptId()));
+ if (resp != null && resp.isOk() && CollUtil.isNotEmpty(resp.getData())) {
+ return resp.getData().get(0);
+ }
+ }
+ return null;
+ }
+
+ private SysDeptInviteEntity requireValidInvite(String token) {
+ if (StrUtil.isBlank(token)) {
+ throw new IllegalArgumentException("邀请码无效");
+ }
+ SysDeptInviteEntity invite = getOne(Wrappers.lambdaQuery()
+ .eq(SysDeptInviteEntity::getToken, token.trim())
+ .last("LIMIT 1"));
+ if (invite == null) {
+ throw new IllegalArgumentException("邀请码错误或已失效");
+ }
+ if (!"1".equals(invite.getEnabled())) {
+ throw new IllegalArgumentException("该邀请入口已停用");
+ }
+ LocalDateTime now = LocalDateTime.now();
+ if (invite.getValidFrom() != null && invite.getValidFrom().isAfter(now)) {
+ throw new IllegalArgumentException("邀请尚未生效");
+ }
+ if (invite.getExpireTime() != null && invite.getExpireTime().isBefore(now)) {
+ throw new IllegalArgumentException("邀请码已过期");
+ }
+ return invite;
+ }
+
+ private void validateExpireRange(LocalDateTime validFrom, LocalDateTime expireTime) {
+ if (validFrom != null && expireTime != null && !expireTime.isAfter(validFrom)) {
+ throw new IllegalArgumentException("过期时间必须晚于生效时间");
+ }
+ }
+
+ private String normalizeTokenType(String tokenType) {
+ String type = StrUtil.blankToDefault(tokenType, TOKEN_UUID).trim().toUpperCase();
+ if (TOKEN_DIGIT6.equals(type) || TOKEN_DIGIT8.equals(type) || TOKEN_UUID.equals(type)) {
+ return type;
+ }
+ throw new IllegalArgumentException("不支持的邀请码类型,请选择 6位数字 / 8位数字 / UUID");
+ }
+
+ /**
+ * 按类型生成唯一邀请码;短数字码碰撞时重试,防止唯一索引冲突。
+ */
+ private String generateUniqueToken(String tokenType, Long excludeId) {
+ for (int i = 0; i < 32; i++) {
+ String token = switch (tokenType) {
+ case TOKEN_DIGIT6 -> RandomUtil.randomNumbers(6);
+ case TOKEN_DIGIT8 -> RandomUtil.randomNumbers(8);
+ default -> IdUtil.fastSimpleUUID();
+ };
+ boolean exists = count(Wrappers.lambdaQuery()
+ .eq(SysDeptInviteEntity::getToken, token)
+ .ne(excludeId != null, SysDeptInviteEntity::getId, excludeId)) > 0;
+ if (!exists) {
+ return token;
+ }
+ }
+ throw new IllegalArgumentException("邀请码生成失败,请重试");
+ }
+}
diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysUserServiceImpl.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysUserServiceImpl.java
index 524a8148..778218cf 100644
--- a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysUserServiceImpl.java
+++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysUserServiceImpl.java
@@ -1163,7 +1163,15 @@ public class SysUserServiceImpl extends ServiceImpl impl
updateUser.setUserId(detail.getUserId());
updateUser.setProcessStatus(processStatus);
updateUser.setProcessTime(LocalDateTime.now());
+ if (isConsent) {
+ // 邀请注册等场景可能曾锁定;审批通过后必须恢复可登录,并清掉用户缓存
+ updateUser.setLockFlag(UserStateEnum.NORMAL.getCode());
+ }
baseMapper.updateById(updateUser);
+ Cache cache = cacheManager.getCache(CacheConstants.USER_DETAILS);
+ if (Objects.nonNull(cache)) {
+ cache.evict(detail.getUsername());
+ }
if (isConsent) {
// 创建用户默认钱包
remoteWalletService.createWallet(detail.getUserId());
diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/utils/flow/FlowUtil.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/utils/flow/FlowUtil.java
index 020823c8..9a8c175f 100644
--- a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/utils/flow/FlowUtil.java
+++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/utils/flow/FlowUtil.java
@@ -16,6 +16,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.bsd.pjcloud.admin.api.entity.SysUser;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.common.data.resolver.ParamResolver;
+import com.bsd.pjcloud.common.security.util.SecurityUtils;
import com.bsd.pjcloud.flow.task.api.feign.RemoteFlowApiFlowService;
import com.bsd.pjcloud.flow.task.api.feign.RemoteFlowEngineService;
import com.bsd.pjcloud.flow.task.constant.NodeUserTypeEnum;
@@ -58,8 +59,11 @@ public class FlowUtil {
log.error("|{}|请配置跨单位审批流程ID;userId:|{}|businessId:|{}|", TAG, user.getUserId(), businessId);
throw new RuntimeException("请配置跨单位审批流程ID");
}
- R detail = flowApiFlowService.getProcessByBusinessKey(defaultRole);
- if (Optional.of(detail.getData()).map(Process::getId).isEmpty()) {
+ // 公开接口无登录态时走内部 Feign(@NoToken + @Inner),避免 424
+ boolean innerCall = SecurityUtils.getUser() == null;
+ R detail = innerCall ? flowApiFlowService.getInnerProcessByBusinessKey(defaultRole)
+ : flowApiFlowService.getProcessByBusinessKey(defaultRole);
+ if (Optional.ofNullable(detail).map(R::getData).map(Process::getId).isEmpty()) {
log.error("|{}|未查询到该审批流程;userId:|{}|businessId:|{}|", TAG, user.getUserId(), businessId);
throw new RuntimeException("未查询到该审批流程");
}
@@ -85,7 +89,13 @@ public class FlowUtil {
processDto.setFlowId(processVo.getFlowId());
processDto.setParamMap(paramMap);
processDto.setStartUserId(String.valueOf(user.getUserId()));
- R startVo = flowApiFlowService.startProcessInstance(processDto);
+ R startVo = innerCall ? flowApiFlowService.startInnerProcessInstance(processDto)
+ : flowApiFlowService.startProcessInstance(processDto);
+ if (startVo == null || !startVo.isOk() || ObjectUtil.isEmpty(startVo.getData())) {
+ log.error("|{}|启动审批流程失败;userId:|{}|businessId:|{}|resp:|{}|", TAG, user.getUserId(), businessId,
+ startVo);
+ throw new RuntimeException(Optional.ofNullable(startVo).map(R::getMsg).orElse("启动审批流程失败"));
+ }
return startVo.getData();
}
diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/resources/application.yml b/pjcloud-upms/pjcloud-upms-biz/src/main/resources/application.yml
index ea0bfaf4..0a91b09f 100644
--- a/pjcloud-upms/pjcloud-upms-biz/src/main/resources/application.yml
+++ b/pjcloud-upms/pjcloud-upms-biz/src/main/resources/application.yml
@@ -40,4 +40,11 @@ security:
encrypt:
enable: true # 开启请求加解密
default-encrypt-type: AES # 可选:AES、SM4,默认 AES
- aes-key: pigxpigxpigxpigx # AES 密钥必须为 16 位
\ No newline at end of file
+ aes-key: pigxpigxpigxpigx # AES 密钥必须为 16 位
+ oauth2:
+ client:
+ # 公开接口兜底白名单(与 @Inner(false) 双保险;Nacos 刷新时不会丢)
+ ignore-urls:
+ - /dept/invite/preview
+ - /dept/invite/register
+ - /druid/**
\ No newline at end of file
diff --git a/pjcloud-wallet/pjcloud-wallet-api/src/main/java/com/bsd/pjcloud/wallet/api/entity/mch/MchManageEntity.java b/pjcloud-wallet/pjcloud-wallet-api/src/main/java/com/bsd/pjcloud/wallet/api/entity/mch/MchManageEntity.java
index c818e448..4e76055e 100644
--- a/pjcloud-wallet/pjcloud-wallet-api/src/main/java/com/bsd/pjcloud/wallet/api/entity/mch/MchManageEntity.java
+++ b/pjcloud-wallet/pjcloud-wallet-api/src/main/java/com/bsd/pjcloud/wallet/api/entity/mch/MchManageEntity.java
@@ -63,6 +63,12 @@ public class MchManageEntity extends Model {
@Schema(description = "店铺类型,逗号分隔")
private String types;
+ /**
+ * 商户属性:NORMAL普通/SCHOOL学校/GOVERNMENT政府/FACTORY工厂
+ */
+ @Schema(description = "商户属性")
+ private String merchantAttr;
+
/**
* 食堂是否对外营业
*/