云
5 天以前 d0cf2cb2c352bdeb146aad2bb57ba7d39cef3bbd
完善会员审批及门户配置

- 增加首页轮播图管理接口、权限及数据库脚本。
- 接入企业微信扫码入会申请和 BPM 审批,支持审批中确认缴费。
- 同步申请与会员缴费状态,支持扫码会员首次设置密码。
- 完善课题申报成员排序、联系方式校验及会员资料未关联处理。
- 增加相关数据库迁移脚本和审批、密码设置单元测试。

验证:后端编译及相关单元测试通过;真实企业微信扫码和审批流程尚未完成端到端验证。
已添加19个文件
已修改24个文件
779 ■■■■■ 文件已修改
sql/mysql/research_application_member_type.sql 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/system_home_banner.sql 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/ums_application_fee_paid.sql 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/ums_application_wxcp_openid.sql 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/ums_member_password_setup.sql 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskApprovalValidator.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-research/src/main/java/cn/iocoder/yudao/module/research/controller/admin/application/vo/ResearchApplicationSaveReqVO.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-research/src/main/java/cn/iocoder/yudao/module/research/dal/mysql/application/ResearchApplicationMemberMapper.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialUserRespDTO.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/user/dto/AdminUserCreateReqDTO.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/homebanner/HomeBannerController.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/homebanner/HomeBannerDO.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/homebanner/HomeBannerMapper.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/auth/WxcpMembershipRegistration.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/auth/WxcpPasswordSetupGrant.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialUserService.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialUserServiceImpl.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/controller/admin/application/UmsApplicationController.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/controller/admin/application/vo/UmsApplicationRespVO.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/controller/admin/application/vo/UmsApplicationSaveReqVO.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/controller/admin/member/UmsMemberProfileChangeController.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/controller/admin/member/vo/UmsMemberRespVO.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/controller/admin/member/vo/profile/WxcpInitialPasswordReqVO.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/dal/dataobject/application/UmsApplicationDO.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/dal/dataobject/member/UmsMemberDO.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/dal/mysql/member/UmsMemberMapper.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/enums/ErrorCodeConstants.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/service/application/UmsApplicationApprovalValidator.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/service/application/UmsApplicationService.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/service/application/UmsApplicationServiceImpl.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/service/application/WxcpMembershipRegistrationImpl.java 106 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/service/application/listener/UmsApplicationStatusListener.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/service/member/UmsMemberProfileChangeServiceImpl.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/service/member/UmsMemberServiceImpl.java 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/service/member/WxcpInitialPasswordService.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ums/src/test/java/cn/iocoder/yudao/module/ums/service/application/UmsApplicationApprovalValidatorTest.java 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ums/src/test/java/cn/iocoder/yudao/module/ums/service/application/UmsApplicationFeeConfirmationTest.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ums/src/test/java/cn/iocoder/yudao/module/ums/service/application/WxcpMembershipRegistrationImplTest.java 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-ums/src/test/java/cn/iocoder/yudao/module/ums/service/member/WxcpInitialPasswordServiceTest.java 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/research_application_member_type.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,3 @@
-- è¯¾é¢˜ç»„成员类型;旧成员保留空值,待业务人员核实后填写。
ALTER TABLE research_application_member
  ADD COLUMN member_type varchar(32) DEFAULT NULL COMMENT '成员类型:教师、学生、校外人员';
sql/mysql/system_home_banner.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,16 @@
CREATE TABLE IF NOT EXISTS `system_home_banner` (
 `id` bigint NOT NULL AUTO_INCREMENT, `title` varchar(100) NOT NULL, `image_url` varchar(1000) NOT NULL,
 `sort` int NOT NULL DEFAULT 0, `status` tinyint NOT NULL DEFAULT 0,
 `creator` varchar(64) DEFAULT '', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
 `updater` varchar(64) DEFAULT '', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
 `deleted` bit(1) NOT NULL DEFAULT b'0', PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='首页轮播图';
-- æ‰§è¡ŒåŽè¯·åœ¨è§’色管理中授权。
INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
SELECT '首页轮播图', 'system:home-banner:query', 2, 90, id, 'home-banner', 'lucide:images', 'system/home-banner/index', 'SystemHomeBanner', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
FROM system_menu WHERE parent_id = 0 AND path = '/system' AND deleted = b'0'
AND NOT EXISTS (SELECT 1 FROM (SELECT * FROM system_menu) m WHERE m.component = 'system/home-banner/index' AND m.deleted = b'0');
INSERT INTO system_menu (name, permission, type, sort, parent_id, path, icon, component, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
SELECT '轮播图维护', 'system:home-banner:update', 3, 1, id, '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'
FROM system_menu WHERE component = 'system/home-banner/index' AND deleted = b'0'
AND NOT EXISTS (SELECT 1 FROM (SELECT * FROM system_menu) m WHERE m.permission = 'system:home-banner:update' AND m.deleted = b'0');
sql/mysql/ums_application_fee_paid.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,2 @@
-- åŽ†å²ç”³è¯·çš„ç¼´è´¹çŠ¶æ€æœªçŸ¥ï¼Œä¿ç•™ NULL。
ALTER TABLE ums_application ADD COLUMN fee_paid bit(1) DEFAULT NULL COMMENT '申请时是否缴费';
sql/mysql/ums_application_wxcp_openid.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,4 @@
-- æ‰«ç èº«ä»½ä¸Žç”³è¯·å”¯ä¸€å…³è”;普通申请保留 NULL。
ALTER TABLE ums_application
  ADD COLUMN wxcp_openid varchar(191) DEFAULT NULL COMMENT '企业微信扫码身份',
  ADD UNIQUE KEY uk_ums_application_wxcp_openid (wxcp_openid);
sql/mysql/ums_member_password_setup.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,2 @@
-- ä»…新的无手机号企业微信扫码会员需要首次设置密码。
ALTER TABLE ums_member ADD COLUMN password_setup_required bit(1) NOT NULL DEFAULT b'0' COMMENT '是否需要首次设置密码';
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskApprovalValidator.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,9 @@
package cn.iocoder.yudao.module.bpm.service.task;
import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.task.api.Task;
/** ä¸šåŠ¡æ¨¡å—åœ¨å®¡æ‰¹ä»»åŠ¡å®Œæˆå‰è¿›è¡Œæ ¡éªŒã€‚ */
public interface BpmTaskApprovalValidator {
    void validate(Long userId, Task task, ProcessInstance instance);
}
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java
@@ -93,6 +93,8 @@
    @Resource
    private BpmProcessInstanceService processInstanceService;
    @org.springframework.beans.factory.annotation.Autowired
    private org.springframework.beans.factory.ObjectProvider<BpmTaskApprovalValidator> approvalValidators;
    @Resource
    private BpmProcessDefinitionService bpmProcessDefinitionService;
    @Resource
@@ -574,6 +576,7 @@
        if (instance == null) {
            throw exception(PROCESS_INSTANCE_NOT_EXISTS);
        }
        approvalValidators.orderedStream().forEach(validator -> validator.validate(userId, task, instance));
        // 1.3 æ ¡éªŒç­¾å
        BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(task.getProcessDefinitionId());
        Boolean signEnable = parseSignEnable(bpmnModel, task.getTaskDefinitionKey());
yudao-module-research/src/main/java/cn/iocoder/yudao/module/research/controller/admin/application/vo/ResearchApplicationSaveReqVO.java
@@ -4,6 +4,8 @@
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Size;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
@@ -47,7 +49,10 @@
    @Pattern(regexp = "^1[3-9]\\d{9}$", message = "联系电话格式不正确,请输入正确的 11 ä½æ‰‹æœºå·")
    private String contactPhone;
    @Schema(description = "联系邮箱", example = "zhangsan@example.com")
    @Schema(description = "联系邮箱", requiredMode = Schema.RequiredMode.REQUIRED, example = "zhangsan@example.com")
    @NotBlank(message = "联系邮箱不能为空")
    @Email(message = "联系邮箱格式不正确")
    @Size(max = 128, message = "联系邮箱长度不能超过128个字符")
    private String contactEmail;
    @Schema(description = "课题组成员(旧版文本字段,仅保留兼容;新数据请使用 members)")
yudao-module-research/src/main/java/cn/iocoder/yudao/module/research/dal/mysql/application/ResearchApplicationMemberMapper.java
@@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.research.dal.mysql.application;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.research.dal.dataobject.application.ResearchApplicationMemberDO;
import org.apache.ibatis.annotations.Mapper;
@@ -10,7 +11,11 @@
public interface ResearchApplicationMemberMapper extends BaseMapperX<ResearchApplicationMemberDO> {
    default List<ResearchApplicationMemberDO> selectListByApplicationId(Long applicationId) {
        return selectList(ResearchApplicationMemberDO::getApplicationId, applicationId);
        LambdaQueryWrapperX<ResearchApplicationMemberDO> query = new LambdaQueryWrapperX<>();
        query.eq(ResearchApplicationMemberDO::getApplicationId, applicationId);
        query.orderByAsc(ResearchApplicationMemberDO::getSortNo);
        query.orderByAsc(ResearchApplicationMemberDO::getId);
        return selectList(query);
    }
    default int deleteByApplicationId(Long applicationId) {
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialUserRespDTO.java
@@ -32,4 +32,11 @@
     */
    private Long userId;
    /** ä¼ä¸šå¾®ä¿¡æŽˆæƒåŽå¯èŽ·å–çš„æ‰‹æœºå·ï¼Œå¯èƒ½ä¸ºç©ºã€‚ */
    private String mobile;
    public SocialUserRespDTO(String openid, String nickname, String avatar, Long userId) {
        this.openid = openid; this.nickname = nickname; this.avatar = avatar; this.userId = userId;
    }
}
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/user/dto/AdminUserCreateReqDTO.java
@@ -33,8 +33,7 @@
    @Schema(description = "角色编号数组", example = "[1, 2]")
    private Set<Long> roleIds;
    @Schema(description = "手机号码", requiredMode = Schema.RequiredMode.REQUIRED, example = "15601691300")
    @NotEmpty(message = "手机号码不能为空")
    @Schema(description = "手机号码(未获取到时可为空)", example = "15601691300")
    private String mobile;
    @Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456")
@@ -53,4 +52,4 @@
    @Schema(description = "备注", example = "备注信息")
    private String remark;
}
}
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java
@@ -170,7 +170,11 @@
    @PermitAll
    @Operation(summary = "社交快捷登录,使用 code æŽˆæƒç ", description = "适合未登录的用户,但是社交账号已绑定用户")
    public CommonResult<AuthLoginRespVO> socialQuickLogin(@RequestBody @Valid AuthSocialLoginReqVO reqVO) {
        return success(authService.socialLogin(reqVO));
        AuthLoginRespVO result = authService.socialLogin(reqVO);
        if (result == null) {
            return CommonResult.error(cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.AUTH_WXCP_MEMBERSHIP_PENDING);
        }
        return success(result);
    }
}
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/homebanner/HomeBannerController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,40 @@
package cn.iocoder.yudao.module.system.controller.admin.homebanner;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.system.dal.dataobject.homebanner.HomeBannerDO;
import cn.iocoder.yudao.module.system.dal.mysql.homebanner.HomeBannerMapper;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import jakarta.validation.constraints.*;
import lombok.Data;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@RestController @RequestMapping("/system/home-banner") @Validated
public class HomeBannerController {
 @Resource private HomeBannerMapper mapper;
 private LambdaQueryWrapperX<HomeBannerDO> ordered() {
  LambdaQueryWrapperX<HomeBannerDO> query = new LambdaQueryWrapperX<>();
  query.orderByAsc(HomeBannerDO::getSort); query.orderByDesc(HomeBannerDO::getId); return query;
 }
 @GetMapping("/list") @PreAuthorize("@ss.hasPermission('system:home-banner:query')")
 public CommonResult<List<HomeBannerDO>> list() { return success(mapper.selectList(ordered())); }
 @GetMapping("/enabled-list")
 public CommonResult<List<HomeBannerDO>> enabledList() { return success(mapper.selectList(ordered().eq(HomeBannerDO::getStatus, 0))); }
 @PostMapping("/save") @PreAuthorize("@ss.hasPermission('system:home-banner:update')")
 public CommonResult<Boolean> save(@Valid @RequestBody SaveRequest req) {
  HomeBannerDO item = new HomeBannerDO(); item.setId(req.getId()); item.setTitle(req.getTitle().trim()); item.setImageUrl(req.getImageUrl().trim()); item.setSort(req.getSort()); item.setStatus(req.getStatus());
  if (item.getId() == null) mapper.insert(item); else mapper.updateById(item); return success(true);
 }
 @DeleteMapping("/delete") @PreAuthorize("@ss.hasPermission('system:home-banner:update')")
 public CommonResult<Boolean> delete(@RequestParam("id") Long id) { mapper.deleteById(id); return success(true); }
 @Data public static class SaveRequest {
  private Long id;
  @NotBlank @Size(max=100) private String title;
  @NotBlank @Size(max=1000) private String imageUrl;
  @NotNull @Min(0) private Integer sort;
  @NotNull @Min(0) @Max(1) private Integer status;
 }
}
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/homebanner/HomeBannerDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,13 @@
package cn.iocoder.yudao.module.system.dal.dataobject.homebanner;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data @EqualsAndHashCode(callSuper = true) @TableName("system_home_banner")
public class HomeBannerDO extends BaseDO {
 private Long id;
 private String title;
 private String imageUrl;
 private Integer sort;
 private Integer status;
}
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/homebanner/HomeBannerMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,5 @@
package cn.iocoder.yudao.module.system.dal.mysql.homebanner;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.module.system.dal.dataobject.homebanner.HomeBannerDO;
import org.apache.ibatis.annotations.Mapper;
@Mapper public interface HomeBannerMapper extends BaseMapperX<HomeBannerDO> {}
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java
@@ -16,6 +16,7 @@
    ErrorCode AUTH_THIRD_LOGIN_NOT_BIND = new ErrorCode(1_002_000_005, "未绑定账号,需要进行绑定");
    ErrorCode AUTH_MOBILE_NOT_EXISTS = new ErrorCode(1_002_000_007, "手机号不存在");
    ErrorCode AUTH_REGISTER_CAPTCHA_CODE_ERROR = new ErrorCode(1_002_000_008, "验证码不正确,原因:{}");
    ErrorCode AUTH_WXCP_MEMBERSHIP_PENDING = new ErrorCode(1_002_000_009, "入会申请已登记,请等待管理员确认缴费并完成审批后再次扫码登录");
    // ========== èœå•模块 1-002-001-000 ==========
    ErrorCode MENU_NAME_DUPLICATE = new ErrorCode(1_002_001_000, "已经存在该名字的菜单");
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java
@@ -40,6 +40,7 @@
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
@@ -101,6 +102,10 @@
    private RoleService roleService;
    @Resource
    private PermissionService permissionService;
    @Autowired(required = false)
    private WxcpMembershipRegistration wxcpMembershipRegistration;
    @Resource
    private WxcpPasswordSetupGrant wxcpPasswordSetupGrant;
    /**
     * éªŒè¯ç çš„开关,默认为 true
@@ -118,6 +123,7 @@
            createLoginLog(null, username, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS);
            throw exception(AUTH_LOGIN_BAD_CREDENTIALS);
        }
        if (!userService.isPasswordMatch(password, user.getPassword())) {
            createLoginLog(user.getId(), username, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS);
            throw exception(AUTH_LOGIN_BAD_CREDENTIALS);
@@ -141,6 +147,14 @@
        // å¦‚æžœ socialType éžç©ºï¼Œè¯´æ˜Žéœ€è¦ç»‘定社交用户
        if (reqVO.getSocialType() != null) {
            if (WECHAT_ENTERPRISE_TYPE.equals(reqVO.getSocialType())) {
                SocialUserRespDTO social = socialUserService.getSocialUserByCode(getUserType().getValue(),
                        reqVO.getSocialType(), reqVO.getSocialCode(), reqVO.getSocialState());
                if (wxcpMembershipRegistration == null || social == null
                        || wxcpMembershipRegistration.hasPendingApplication(social.getOpenid())) {
                    throw exception(AUTH_WXCP_MEMBERSHIP_PENDING);
                }
            }
            socialUserService.bindSocialUser(new SocialUserBindReqDTO(user.getId(), getUserType().getValue(),
                    reqVO.getSocialType(), reqVO.getSocialCode(), reqVO.getSocialState()));
        }
@@ -228,6 +242,11 @@
        AdminUserDO user = bindUserId != null ? userService.getUser(bindUserId) : null;
        if (user == null) {
            if (WECHAT_ENTERPRISE_TYPE.equals(reqVO.getType())) {
                if (wxcpMembershipRegistration == null) throw new IllegalStateException("会员注册服务不可用");
                wxcpMembershipRegistration.startIfAbsent(socialUser.getOpenid(), socialUser.getNickname(), socialUser.getMobile());
                return null; // æäº¤äº‹åŠ¡åŽç”± Controller è¿”回待审核提示,不签发令牌
            }
            // 2.1 é¦–次登录,或原绑定用户已被删除:自动注册新用户并(改)绑定
            //
            // åŽŸç»‘å®šç”¨æˆ·å·²åˆ é™¤æ—¶å¿…é¡»èµ°è¿™æ¡è·¯ï¼šå¦åˆ™è¯¥ä¼ä¸šæˆå‘˜ä¼šæ°¸ä¹…å¡åœ¨ã€Œç”¨æˆ·ä¸å­˜åœ¨ã€â€”â€”
@@ -245,8 +264,12 @@
                    user.getId(), user.getUsername(), reqVO.getType());
        }
        if (CommonStatusEnum.isDisable(user.getStatus())) throw exception(AUTH_LOGIN_USER_DISABLED);
        // 3. åˆ›å»º Token ä»¤ç‰Œï¼Œè®°å½•登录日志
        AuthLoginRespVO result = createTokenAfterLoginSuccess(user.getId(), user.getUsername(), LoginLogTypeEnum.LOGIN_SOCIAL);
        if (WECHAT_ENTERPRISE_TYPE.equals(reqVO.getType())) {
            wxcpPasswordSetupGrant.issue(result.getAccessToken(), user.getId());
        }
        // 4. å‘布社交登录事件(供其他模块监听,如发送企业微信通知)
        eventPublisher.publishEvent(new SocialLoginEvent(
@@ -435,4 +458,4 @@
        userService.updateUserPassword(userByMobile.getId(), reqVO.getPassword());
    }
}
}
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/auth/WxcpMembershipRegistration.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,7 @@
package cn.iocoder.yudao.module.system.service.auth;
/** ä¼ä¸šå¾®ä¿¡é¦–次扫码时创建待审核的入会申请,由会员模块实现。 */
public interface WxcpMembershipRegistration {
    void startIfAbsent(String openid, String nickname, String mobile);
    boolean hasPendingApplication(String openid);
}
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/auth/WxcpPasswordSetupGrant.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,21 @@
package cn.iocoder.yudao.module.system.service.auth;
import cn.hutool.crypto.digest.DigestUtil;
import jakarta.annotation.Resource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.time.Duration;
/** ç»‘定到本次企业微信扫码登录令牌的短期密码设置授权。 */
@Service
public class WxcpPasswordSetupGrant {
    @Resource private StringRedisTemplate redis;
    private String key(String token) { return "wxcp:password-setup:" + DigestUtil.sha256Hex(token); }
    public void issue(String token, Long userId) {
        redis.opsForValue().set(key(token), userId.toString(), Duration.ofMinutes(10));
    }
    public boolean valid(String token, Long userId) {
        return token != null && userId != null && userId.toString().equals(redis.opsForValue().get(key(token)));
    }
    public void revoke(String token) { redis.delete(key(token)); }
}
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialUserService.java
@@ -18,6 +18,9 @@
 */
public interface SocialUserService {
    /** å®¡æ‰¹é€šè¿‡åŽç»‘定已认证的企业微信身份;不得覆盖其他用户的绑定。 */
    void bindApprovedWxcpUser(String openid, Long userId);
    /**
     * èŽ·å¾—æŒ‡å®šç”¨æˆ·çš„ç¤¾äº¤ç”¨æˆ·åˆ—è¡¨
     *
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialUserServiceImpl.java
@@ -47,6 +47,24 @@
    private SocialClientService socialClientService;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void bindApprovedWxcpUser(String openid, Long userId) {
        int type = 30;
        int userType = cn.iocoder.yudao.framework.common.enums.UserTypeEnum.ADMIN.getValue();
        SocialUserDO socialUser = socialUserMapper.selectByTypeAndOpenid(type, openid);
        if (socialUser == null) throw new IllegalStateException("企业微信身份不存在,请重新扫码");
        SocialUserBindDO existing = socialUserBindMapper.selectByUserTypeAndSocialUserId(userType, socialUser.getId());
        if (existing != null) {
            if (!existing.getUserId().equals(userId)) throw new IllegalStateException("该企业微信身份已绑定其他账号");
            return;
        }
        if (socialUserBindMapper.selectByUserIdAndUserTypeAndSocialType(userId, userType, type) != null)
            throw new IllegalStateException("该账号已绑定其他企业微信身份");
        socialUserBindMapper.insert(SocialUserBindDO.builder().userId(userId).userType(userType)
                .socialUserId(socialUser.getId()).socialType(type).build());
    }
    @Override
    public List<SocialUserDO> getSocialUserList(Long userId, Integer userType) {
        // èŽ·å¾—ç»‘å®š
        List<SocialUserBindDO> socialUserBinds = socialUserBindMapper.selectListByUserIdAndUserType(userId, userType);
@@ -115,8 +133,18 @@
        // èŽ·å¾—ç»‘å®šç”¨æˆ·
        SocialUserBindDO socialUserBind = socialUserBindMapper.selectByUserTypeAndSocialUserId(userType,
                socialUser.getId());
        return new SocialUserRespDTO(socialUser.getOpenid(), socialUser.getNickname(), socialUser.getAvatar(),
        SocialUserRespDTO result = new SocialUserRespDTO(socialUser.getOpenid(), socialUser.getNickname(), socialUser.getAvatar(),
                socialUserBind != null ? socialUserBind.getUserId() : null);
        if (socialType == 30 && cn.hutool.core.util.StrUtil.isNotBlank(socialUser.getRawUserInfo())) {
            try {
                cn.hutool.json.JSONObject raw = cn.hutool.json.JSONUtil.parseObj(socialUser.getRawUserInfo());
                result.setMobile(raw.getStr("mobile"));
                if (cn.hutool.core.util.StrUtil.isBlank(result.getNickname())) result.setNickname(raw.getStr("name"));
            } catch (Exception ignored) {
                // æ•æ„Ÿä¿¡æ¯ä¸å¯ç”¨æ—¶ä¸å½±å“æ‰«ç ï¼Œç”³è¯·ä½¿ç”¨éšæœºè´¦å·æ–¹æ¡ˆã€‚
            }
        }
        return result;
    }
    /**
yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/controller/admin/application/UmsApplicationController.java
@@ -31,6 +31,7 @@
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.framework.common.util.collection.MapUtils.findAndThen;
@@ -51,6 +52,19 @@
    @Resource
    private StorageAttachmentApi storageAttachmentApi;
    @GetMapping("/fee-confirm-available")
    @Operation(summary = "判断当前用户能否确认缴费状态")
    public CommonResult<Boolean> canConfirmFee(@RequestParam("id") Long id) {
        return success(applicationService.canConfirmFee(id, getLoginUserId()));
    }
    @PutMapping("/confirm-fee")
    @Operation(summary = "当前审批人确认缴费状态")
    public CommonResult<Boolean> confirmFee(@RequestParam("id") Long id, @RequestParam("feePaid") Boolean feePaid) {
        applicationService.confirmFee(id, getLoginUserId(), feePaid);
        return success(true);
    }
    @PostMapping("/create")
    @Operation(summary = "创建入会申请")
@@ -143,4 +157,4 @@
        });
    }
}
}
yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/controller/admin/application/vo/UmsApplicationRespVO.java
@@ -47,6 +47,9 @@
    @Schema(description = "申请等级", example = "1")
    private String memberGrade;
    @Schema(description = "是否缴费")
    private Boolean feePaid;
    @Schema(description = "地区编码", example = "110000")
    private String regionCode;
@@ -77,4 +80,4 @@
    @Schema(description = "是否有附件")
    private Boolean hasAttachment;
}
}
yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/controller/admin/application/vo/UmsApplicationSaveReqVO.java
@@ -46,6 +46,9 @@
    @Schema(description = "申请等级", example = "1")
    private String memberGrade;
    @Schema(description = "是否缴费")
    private Boolean feePaid;
    @Schema(description = "地区编码(Area æ ‘末级)", example = "110000")
    private String regionCode;
@@ -58,4 +61,4 @@
    @Schema(description = "附件BlobId列表")
    private List<Long> blobIds;
}
}
yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/controller/admin/member/UmsMemberProfileChangeController.java
@@ -8,7 +8,14 @@
import cn.iocoder.yudao.module.ums.controller.admin.member.vo.profile.UmsMemberProfileChangeRespVO;
import cn.iocoder.yudao.module.ums.controller.admin.member.vo.profile.UmsMemberProfileChangeSaveReqVO;
import cn.iocoder.yudao.module.ums.dal.dataobject.member.UmsMemberProfileChangeDO;
import cn.iocoder.yudao.module.ums.dal.dataobject.member.UmsMemberDO;
import cn.iocoder.yudao.module.ums.service.member.UmsMemberProfileChangeService;
import cn.iocoder.yudao.module.ums.service.member.WxcpInitialPasswordService;
import cn.iocoder.yudao.module.ums.controller.admin.member.vo.profile.WxcpInitialPasswordReqVO;
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
import cn.iocoder.yudao.framework.security.config.SecurityProperties;
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
import jakarta.servlet.http.HttpServletRequest;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -19,6 +26,7 @@
import org.springframework.web.bind.annotation.*;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@Tag(name = "管理后台 - ä¼šå‘˜èµ„料自主维护")
@RestController
@@ -28,11 +36,22 @@
    @Resource
    private UmsMemberProfileChangeService profileChangeService;
    @Resource private WxcpInitialPasswordService initialPasswordService;
    @Resource private SecurityProperties securityProperties;
    @PostMapping("/initial-password")
    @Operation(summary = "企业微信扫码会员首次设置密码")
    @ApiAccessLog(requestEnable = false)
    public CommonResult<String> setInitialPassword(@Valid @RequestBody WxcpInitialPasswordReqVO req, HttpServletRequest request) {
        String token = SecurityFrameworkUtils.obtainAuthorization(request, securityProperties.getTokenHeader(), securityProperties.getTokenParameter());
        return success(initialPasswordService.setPassword(getLoginUserId(), token, req.getPassword()));
    }
    @GetMapping("/get")
    @Operation(summary = "获得当前登录会员的个人资料")
    public CommonResult<UmsMemberRespVO> getMyProfile() {
        return success(BeanUtils.toBean(profileChangeService.getMyProfile(), UmsMemberRespVO.class));
        UmsMemberDO member = profileChangeService.getMyProfile();
        return success(member == null ? null : BeanUtils.toBean(member, UmsMemberRespVO.class));
    }
    @GetMapping("/change/latest")
yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/controller/admin/member/vo/UmsMemberRespVO.java
@@ -78,6 +78,9 @@
    @Schema(description = "是否缴纳会费", example = "true")
    private Boolean feePaid;
    @Schema(description = "是否需要首次设置密码")
    private Boolean passwordSetupRequired;
    @Schema(description = "缴费年度", example = "2026")
    private Integer feeYear;
@@ -102,4 +105,4 @@
    /** æ˜¯å¦æœ‰é™„件(通讯录/列表用) */
    private Boolean hasAttachment;
}
}
yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/controller/admin/member/vo/profile/WxcpInitialPasswordReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,12 @@
package cn.iocoder.yudao.module.ums.controller.admin.member.vo.profile;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
@Data
public class WxcpInitialPasswordReqVO {
    @NotBlank
    @Length(min = 8, max = 16, message = "密码长度须为 8 è‡³ 16 ä½")
    private String password;
}
yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/dal/dataobject/application/UmsApplicationDO.java
@@ -59,6 +59,12 @@
    /** ç”³è¯·ç­‰çº§ï¼ˆæŒ‰ member_type å–不同字典) */
    private String memberGrade;
    /** ç”³è¯·æ—¶æ˜¯å¦ç¼´è´¹ */
    private Boolean feePaid;
    /** é¦–次扫码申请对应的企业微信身份 */
    private String wxcpOpenid;
    /** åœ°åŒºç¼–码(Area æ ‘末级) */
    private String regionCode;
@@ -74,4 +80,4 @@
    /** å¤‡æ³¨ */
    private String remark;
}
}
yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/dal/dataobject/member/UmsMemberDO.java
@@ -86,6 +86,9 @@
    /** æ˜¯å¦ç¼´çº³ä¼šè´¹ */
    private Boolean feePaid;
    /** æ˜¯å¦éœ€è¦é¦–次设置密码 */
    private Boolean passwordSetupRequired;
    /** ç¼´è´¹å¹´åº¦ï¼ˆå¦‚ 2026) */
    private Integer feeYear;
@@ -98,4 +101,4 @@
    /** å¤‡æ³¨ */
    private String remark;
}
}
yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/dal/mysql/member/UmsMemberMapper.java
@@ -18,6 +18,10 @@
        return selectOne(UmsMemberDO::getUserId, userId);
    }
    default UmsMemberDO selectByApplicationId(Long applicationId) {
        return selectOne(UmsMemberDO::getApplicationId, applicationId);
    }
    default PageResult<UmsMemberDO> selectPage(UmsMemberPageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<UmsMemberDO>()
                .likeIfPresent(UmsMemberDO::getNo, reqVO.getNo())
@@ -31,4 +35,4 @@
                .orderByDesc(UmsMemberDO::getId));
    }
}
}
yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/enums/ErrorCodeConstants.java
@@ -16,6 +16,10 @@
    ErrorCode APPLICATION_DELETE_FAIL_NOT_DRAFT = new ErrorCode(1_030_108_003, "入会申请({})不是草稿状态,无法删除");
    ErrorCode APPLICATION_SUBMIT_FAIL_STATUS = new ErrorCode(1_030_108_004, "提交审批失败,只有草稿或已驳回状态的入会申请才能提交");
    ErrorCode APPLICATION_BPM_CATEGORY_NOT_EXISTS = new ErrorCode(1_030_108_005, "入会审批分类不存在,请先在BPM模块创建入会审批流程分类");
    ErrorCode APPLICATION_FEE_CONFIRM_REQUIRED = new ErrorCode(1_030_108_006, "请先核实并填写是否缴纳会费,再提交审批");
    ErrorCode APPLICATION_WXCP_INFO_REQUIRED = new ErrorCode(1_030_108_007, "请完善会员等级及有效手机号;企业微信扫码申请可不填手机号");
    ErrorCode APPLICATION_WXCP_DELETE_FORBIDDEN = new ErrorCode(1_030_108_008, "扫码产生的入会申请不可删除,请完善后提交审批");
    ErrorCode APPLICATION_FEE_CONFIRM_FORBIDDEN = new ErrorCode(1_030_108_009, "只有当前审批人可以确认缴费状态");
    // ========== ä¼šå‘˜ï¼ˆ1-030-108-100 ~ 108-109) ==========
    ErrorCode MEMBER_NOT_EXISTS = new ErrorCode(1_030_108_100, "会员不存在");
@@ -26,6 +30,9 @@
    ErrorCode MEMBER_ARCHIVE_FAIL_STATUS = new ErrorCode(1_030_108_105, "归档失败:会员({})当前状态为〔{}〕,只有「正常」状态的会员才能归档到档案管理,请先启用该会员");
    ErrorCode MEMBER_ALREADY_ARCHIVED = new ErrorCode(1_030_108_106, "重复归档:会员({})已于此前归档并生成档案(编号={}),请勿重复归档,如需查看请前往档案管理模块");
    ErrorCode MEMBER_FEE_YEAR_REQUIRED = new ErrorCode(1_030_108_107, "会费缴纳年度为空:已标记为「已缴纳会费」,请选择缴费年度(例如 2026)后再保存");
    ErrorCode MEMBER_WXCP_ROLE_NOT_AVAILABLE = new ErrorCode(1_030_108_120, "企业微信扫码用户角色不存在或已停用,请联系管理员配置角色编码 wxcp_user");
    ErrorCode MEMBER_PASSWORD_SETUP_EXPIRED = new ErrorCode(1_030_108_121, "扫码授权已过期,请重新扫码登录后设置密码");
    ErrorCode MEMBER_PASSWORD_SETUP_NOT_REQUIRED = new ErrorCode(1_030_108_122, "当前账号无需首次设置密码,请在个人中心修改密码");
    // ========== ä¼šå‘˜èµ„料变更申请(1-030-108-108 ~ 108-119) ==========
    ErrorCode MEMBER_PROFILE_NOT_LINKED = new ErrorCode(1_030_108_108, "当前登录账号未关联会员,无法维护个人资料,请联系学会管理员绑定会员账号");
@@ -35,4 +42,4 @@
    ErrorCode MEMBER_PROFILE_CHANGE_REJECT_REASON_REQUIRED = new ErrorCode(1_030_108_112, "驳回资料变更申请时必须填写驳回原因,请补充原因后再操作");
    ErrorCode MEMBER_PROFILE_CHANGE_EMPTY = new ErrorCode(1_030_108_113, "资料未发生任何变更,无需提交审核");
}
}
yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/service/application/UmsApplicationApprovalValidator.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
package cn.iocoder.yudao.module.ums.service.application;
import cn.iocoder.yudao.module.bpm.service.task.BpmTaskApprovalValidator;
import cn.iocoder.yudao.module.ums.dal.dataobject.application.UmsApplicationDO;
import cn.iocoder.yudao.module.ums.dal.mysql.application.UmsApplicationMapper;
import jakarta.annotation.Resource;
import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.task.api.Task;
import org.springframework.stereotype.Component;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.ums.enums.ErrorCodeConstants.*;
@Component
public class UmsApplicationApprovalValidator implements BpmTaskApprovalValidator {
    @Resource private UmsApplicationMapper mapper;
    @Override
    public void validate(Long userId, Task task, ProcessInstance instance) {
        if (!"ums_membership_approve".equals(instance.getProcessDefinitionKey())) return;
        // å‘起人节点由 BPM åœ¨æµç¨‹å¯åŠ¨æ—¶è‡ªåŠ¨æäº¤ï¼›ç¼´è´¹ç”±åŽç»­å®¡æ‰¹äººç¡®è®¤ã€‚
        if ("StartUserNode".equals(task.getTaskDefinitionKey())) return;
        Long id;
        try { id = Long.valueOf(instance.getBusinessKey()); }
        catch (Exception e) { throw exception(APPLICATION_NOT_EXISTS); }
        UmsApplicationDO application = mapper.selectById(id);
        if (application == null) throw exception(APPLICATION_NOT_EXISTS);
        if (application.getFeePaid() == null) throw exception(APPLICATION_FEE_CONFIRM_REQUIRED);
    }
}
yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/service/application/UmsApplicationService.java
@@ -23,10 +23,13 @@
    void submitApplication(Long id, String processDefinitionKey);
    boolean canConfirmFee(Long id, Long userId);
    void confirmFee(Long id, Long userId, boolean feePaid);
    List<Map<String, Object>> getApproveProcessDefinitionList();
    void updateApplicationAuditStatus(Long id, Integer bpmResult);
    void createMemberFromApplicationOnApproved(Long id);
}
}
yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/service/application/UmsApplicationServiceImpl.java
@@ -9,6 +9,8 @@
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO;
import cn.iocoder.yudao.module.bpm.service.definition.BpmCategoryService;
import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
import cn.iocoder.yudao.module.bpm.service.task.BpmTaskService;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
import cn.iocoder.yudao.module.ums.controller.admin.application.vo.UmsApplicationPageReqVO;
import cn.iocoder.yudao.module.ums.controller.admin.application.vo.UmsApplicationSaveReqVO;
@@ -61,6 +63,26 @@
    @Resource
    private UmsMemberService memberService;
    @Resource private BpmTaskService bpmTaskService;
    @Override
    public boolean canConfirmFee(Long id, Long userId) {
        UmsApplicationDO application = validateApplicationExists(id);
        return userId != null && UmsApplicationStatusEnum.APPROVING.getStatus().equals(application.getStatus())
                && StrUtil.isNotBlank(application.getProcessInstanceId())
                && bpmTaskService.getTodoTask(userId, null, application.getProcessInstanceId()) != null;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void confirmFee(Long id, Long userId, boolean feePaid) {
        if (!canConfirmFee(id, userId)) throw exception(APPLICATION_FEE_CONFIRM_FORBIDDEN);
        int updated = applicationMapper.update(null, new LambdaUpdateWrapper<UmsApplicationDO>()
                .eq(UmsApplicationDO::getId, id)
                .eq(UmsApplicationDO::getStatus, UmsApplicationStatusEnum.APPROVING.getStatus())
                .set(UmsApplicationDO::getFeePaid, feePaid));
        if (updated != 1) throw exception(APPLICATION_SUBMIT_FAIL_STATUS);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
@@ -102,6 +124,9 @@
    @Transactional(rollbackFor = Exception.class)
    public void deleteApplication(Long id) {
        UmsApplicationDO application = validateApplicationExists(id);
        if (StrUtil.isNotBlank(application.getWxcpOpenid())) {
            throw exception(APPLICATION_WXCP_DELETE_FORBIDDEN);
        }
        if (!UmsApplicationStatusEnum.DRAFT.getStatus().equals(application.getStatus())) {
            throw exception(APPLICATION_DELETE_FAIL_NOT_DRAFT, application.getNo());
        }
@@ -128,6 +153,15 @@
        if (!UmsApplicationStatusEnum.DRAFT.getStatus().equals(status)
                && !UmsApplicationStatusEnum.REJECTED.getStatus().equals(status)) {
            throw exception(APPLICATION_SUBMIT_FAIL_STATUS);
        }
        if (application.getFeePaid() == null) {
            throw exception(APPLICATION_FEE_CONFIRM_REQUIRED);
        }
        if (StrUtil.isBlank(application.getMemberGrade())
                || (StrUtil.isBlank(application.getContactPhone()) && StrUtil.isBlank(application.getWxcpOpenid()))
                || (StrUtil.isNotBlank(application.getContactPhone())
                && !application.getContactPhone().trim().matches("^1[3-9]\\d{9}$"))) {
            throw exception(APPLICATION_WXCP_INFO_REQUIRED);
        }
        String processInstanceId = processInstanceApi.createProcessInstance(getLoginUserId(),
@@ -185,8 +219,14 @@
        Integer newStatus = convertBpmResultToStatus(bpmResult);
        if (newStatus != null) {
            if (UmsApplicationStatusEnum.APPROVED.getStatus().equals(newStatus) && application.getFeePaid() == null) {
                throw exception(APPLICATION_FEE_CONFIRM_REQUIRED);
            }
            applicationMapper.updateById(new UmsApplicationDO()
                    .setId(id).setStatus(newStatus));
            if (UmsApplicationStatusEnum.APPROVED.getStatus().equals(newStatus)) {
                memberService.createMemberFromApplication(application);
            }
        }
    }
@@ -230,4 +270,4 @@
        memberService.createMemberFromApplication(application);
    }
}
}
yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/service/application/WxcpMembershipRegistrationImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,106 @@
package cn.iocoder.yudao.module.ums.service.application;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import cn.iocoder.yudao.module.system.service.auth.WxcpMembershipRegistration;
import cn.iocoder.yudao.module.ums.dal.dataobject.application.UmsApplicationDO;
import cn.iocoder.yudao.module.ums.dal.mysql.application.UmsApplicationMapper;
import cn.iocoder.yudao.module.ums.dal.redis.UmsNoRedisDAO;
import cn.iocoder.yudao.module.ums.enums.UmsApplicationStatusEnum;
import cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi;
import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
import cn.iocoder.yudao.module.system.service.user.AdminUserService;
import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
import cn.iocoder.yudao.module.system.controller.admin.user.vo.user.UserSaveReqVO;
import cn.iocoder.yudao.module.system.service.permission.PermissionService;
import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Collections;
import java.util.UUID;
@Service
public class WxcpMembershipRegistrationImpl implements WxcpMembershipRegistration {
    @Resource private UmsApplicationMapper mapper;
    @Resource private UmsNoRedisDAO noRedisDAO;
    @Resource private BpmProcessInstanceApi processInstanceApi;
    @Resource private AdminUserService userService;
    @Resource private PermissionService permissionService;
    @Override
    public boolean hasPendingApplication(String openid) {
        UmsApplicationDO application = mapper.selectOne(new LambdaQueryWrapperX<UmsApplicationDO>()
                .eq(UmsApplicationDO::getWxcpOpenid, openid));
        return application != null && !UmsApplicationStatusEnum.APPROVED.getStatus().equals(application.getStatus());
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    @DataPermission(enable = false)
    public void startIfAbsent(String openid, String nickname, String mobile) {
        if (StrUtil.isBlank(openid)) throw new IllegalArgumentException("企业微信身份为空");
        UmsApplicationDO existing = mapper.selectOne(new LambdaQueryWrapperX<UmsApplicationDO>()
                .eq(UmsApplicationDO::getWxcpOpenid, openid));
        if (existing != null) {
            // å…¼å®¹æ—§ç‰ˆæœ¬é¦–次扫码产生的草稿;已在审批或已结束的申请不重复发起。
            if (UmsApplicationStatusEnum.DRAFT.getStatus().equals(existing.getStatus())
                    && StrUtil.isBlank(existing.getProcessInstanceId())) {
                existing.setStatus(UmsApplicationStatusEnum.APPROVING.getStatus());
                if (StrUtil.isBlank(existing.getMemberGrade())) existing.setMemberGrade("1");
                if (StrUtil.isBlank(existing.getContactPhone()) && StrUtil.isNotBlank(mobile)
                        && mobile.trim().matches("^1[3-9]\\d{9}$")) existing.setContactPhone(mobile.trim());
                int claimed = mapper.update(null, new LambdaUpdateWrapper<UmsApplicationDO>()
                        .eq(UmsApplicationDO::getId, existing.getId())
                        .eq(UmsApplicationDO::getStatus, UmsApplicationStatusEnum.DRAFT.getStatus())
                        .set(UmsApplicationDO::getStatus, UmsApplicationStatusEnum.APPROVING.getStatus())
                        .set(UmsApplicationDO::getMemberGrade, existing.getMemberGrade())
                        .set(UmsApplicationDO::getContactPhone, existing.getContactPhone()));
                if (claimed == 1) startProcess(existing);
            }
            return;
        }
        UmsApplicationDO application = new UmsApplicationDO();
        application.setNo(noRedisDAO.generate(UmsNoRedisDAO.APPLICATION_NO_PREFIX));
        application.setWxcpOpenid(openid);
        application.setApplicantName(StrUtil.maxLength(StrUtil.blankToDefault(nickname, "待完善"), 64));
        application.setMemberType(1);
        application.setMemberGrade("1");
        if (StrUtil.isNotBlank(mobile) && mobile.trim().matches("^1[3-9]\\d{9}$")) {
            application.setContactPhone(mobile.trim());
        }
        application.setStatus(UmsApplicationStatusEnum.APPROVING.getStatus());
        mapper.insert(application);
        startProcess(application);
    }
    private void startProcess(UmsApplicationDO application) {
        Long initiatorId = getServiceUserId();
        String processId = processInstanceApi.createProcessInstance(initiatorId,
                new BpmProcessInstanceCreateReqDTO().setProcessDefinitionKey("ums_membership_approve")
                        .setBusinessKey(String.valueOf(application.getId())));
        mapper.updateById(new UmsApplicationDO().setId(application.getId()).setProcessInstanceId(processId));
    }
    private Long getServiceUserId() {
        String username = "wxcpservice";
        AdminUserDO existing = userService.getUserByUsername(username);
        if (existing != null) {
            if (cn.iocoder.yudao.framework.common.enums.CommonStatusEnum.isDisable(existing.getStatus()))
                throw new IllegalStateException("入会审批服务账号已停用");
            if (!"仅用于系统自动发起入会审批".equals(existing.getRemark())
                    || !permissionService.getUserRoleIdListByUserId(existing.getId()).isEmpty())
                throw new IllegalStateException("入会审批服务账号配置不符合要求,请联系管理员核查");
            return existing.getId();
        }
        UserSaveReqVO req = new UserSaveReqVO();
        req.setUsername(username);
        req.setNickname("入会审批服务");
        req.setPassword(UUID.randomUUID().toString().replace("-", "").substring(0, 16));
        req.setPostIds(Collections.emptySet());
        req.setRoleIds(Collections.emptySet());
        req.setRemark("仅用于系统自动发起入会审批");
        return userService.createUser(req);
    }
}
yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/service/application/listener/UmsApplicationStatusListener.java
@@ -44,18 +44,9 @@
            log.info("[onEvent] å…¥ä¼šç”³è¯·({}) å®¡æ‰¹çŠ¶æ€æ›´æ–°å®Œæˆ", applicationId);
        } catch (Exception e) {
            log.error("[onEvent] æ›´æ–°å…¥ä¼šç”³è¯·å®¡æ‰¹çŠ¶æ€å¤±è´¥ï¼ŒapplicationId={}", applicationId, e);
            return;
            throw e;
        }
        // 2. å®¡æ‰¹é€šè¿‡åŽè‡ªåŠ¨åˆ›å»ºä¼šå‘˜
        if (cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum.APPROVE.getStatus().equals(event.getStatus())) {
            try {
                applicationService.createMemberFromApplicationOnApproved(applicationId);
                log.info("[onEvent] å…¥ä¼šç”³è¯·({}) å®¡æ‰¹é€šè¿‡ï¼Œä¼šå‘˜åˆ›å»ºå®Œæˆ", applicationId);
            } catch (Exception e) {
                log.error("[onEvent] å…¥ä¼šç”³è¯·({}) å®¡æ‰¹é€šè¿‡åŽåˆ›å»ºä¼šå‘˜å¤±è´¥", applicationId, e);
            }
        }
    }
}
}
yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/service/member/UmsMemberProfileChangeServiceImpl.java
@@ -50,7 +50,8 @@
    @Override
    public UmsMemberDO getMyProfile() {
        return getLoginMember();
        Long userId = getLoginUserId();
        return userId != null ? memberMapper.selectByUserId(userId) : null;
    }
    @Override
yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/service/member/UmsMemberServiceImpl.java
@@ -11,10 +11,15 @@
import cn.iocoder.yudao.module.system.api.storage.dto.StorageBlobRespDTO;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserCreateReqDTO;
import cn.iocoder.yudao.module.system.service.social.SocialUserService;
import cn.iocoder.yudao.module.system.service.permission.RoleService;
import cn.iocoder.yudao.module.system.dal.dataobject.permission.RoleDO;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.module.ums.controller.admin.member.vo.UmsMemberPageReqVO;
import cn.iocoder.yudao.module.ums.controller.admin.member.vo.UmsMemberSaveReqVO;
import cn.iocoder.yudao.module.ums.dal.dataobject.application.UmsApplicationDO;
import cn.iocoder.yudao.module.ums.dal.dataobject.member.UmsMemberDO;
import cn.iocoder.yudao.module.ums.dal.mysql.application.UmsApplicationMapper;
import cn.iocoder.yudao.module.ums.dal.mysql.member.UmsMemberMapper;
import cn.iocoder.yudao.module.ums.dal.redis.UmsNoRedisDAO;
import jakarta.annotation.Resource;
@@ -27,6 +32,7 @@
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -43,11 +49,9 @@
    private static final Long MEMBER_DEPT_ID = 129L;
    /**
     * æ™®é€šä¼šå‘˜è§’色 ID(system_role「普通会员」)
     *
     * <p>该角色只分配「我的资料」菜单,不包含档案管理等后台菜单,用于实现权限隔离。
     * ä¼ä¸šå¾®ä¿¡æ‰«ç ç”¨æˆ·è§’色编码;按编码查询,避免不同环境角色编号不一致。
     */
    private static final Long MEMBER_ROLE_ID = 9100L;
    private static final String MEMBER_ROLE_CODE = "wxcp_user";
    public static final String MEMBER_RECORD_TYPE = "ums_member";
@@ -65,6 +69,12 @@
    @Resource
    private UmsMemberMapper memberMapper;
    @Resource
    private UmsApplicationMapper applicationMapper;
    @Resource
    private SocialUserService socialUserService;
    @Resource
    private RoleService roleService;
    @Resource
    private UmsNoRedisDAO noRedisDAO;
@@ -81,6 +91,8 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public UmsMemberDO createMemberFromApplication(UmsApplicationDO application) {
        UmsMemberDO existing = memberMapper.selectByApplicationId(application.getId());
        if (existing != null) return existing;
        // 1. ç”Ÿæˆä¼šå‘˜ç¼–号
        String no = noRedisDAO.generate(UmsNoRedisDAO.MEMBER_NO_PREFIX);
        if (memberMapper.selectByNo(no) != null) {
@@ -89,6 +101,9 @@
        // 2. åˆ›å»ºç³»ç»Ÿç”¨æˆ·è´¦å·
        Long userId = createSystemUser(application);
        if (StrUtil.isNotBlank(application.getWxcpOpenid())) {
            socialUserService.bindApprovedWxcpUser(application.getWxcpOpenid(), userId);
        }
        // 3. åˆ›å»ºä¼šå‘˜è®°å½•
        UmsMemberDO member = UmsMemberDO.builder()
@@ -97,6 +112,8 @@
                .applicationId(application.getId())
                .memberType(application.getMemberType())
                .memberGrade(application.getMemberGrade())
                .feePaid(application.getFeePaid())
                .passwordSetupRequired(StrUtil.isBlank(application.getContactPhone()))
                .name(application.getApplicantName())
                .contactPhone(application.getContactPhone())
                .contactEmail(application.getContactEmail())
@@ -142,7 +159,7 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateMember(UmsMemberSaveReqVO updateReqVO) {
        validateMemberExists(updateReqVO.getId());
        UmsMemberDO existingMember = validateMemberExists(updateReqVO.getId());
        validateMemberGrade(updateReqVO.getMemberType(), updateReqVO.getMemberGrade());
        validateFeePaid(updateReqVO);
@@ -154,6 +171,10 @@
        UmsMemberDO updateObj = BeanUtils.toBean(updateReqVO, UmsMemberDO.class);
        memberMapper.updateById(updateObj);
        if (existingMember.getApplicationId() != null && updateReqVO.getFeePaid() != null) {
            applicationMapper.updateById(new UmsApplicationDO()
                    .setId(existingMember.getApplicationId()).setFeePaid(updateReqVO.getFeePaid()));
        }
        if (clearFeeYear) {
            memberMapper.update(null, new LambdaUpdateWrapper<UmsMemberDO>()
                    .eq(UmsMemberDO::getId, updateReqVO.getId())
@@ -224,9 +245,19 @@
    private Long createSystemUser(UmsApplicationDO application) {
        // æ‰‹æœºå·å¯ä½œä¸ºç”¨æˆ·å/密码(参考 HRM å‘˜å·¥æ¨¡å¼ï¼‰
        String mobile = StrUtil.blankToDefault(application.getContactPhone(), "unknown");
        String username = mobile;
        String nickname = application.getApplicantName();
        String mobile = StrUtil.trim(application.getContactPhone());
        if ((StrUtil.isBlank(mobile) && StrUtil.isBlank(application.getWxcpOpenid()))
                || (StrUtil.isNotBlank(mobile) && !mobile.matches("^1[3-9]\\d{9}$"))) {
            throw exception(APPLICATION_WXCP_INFO_REQUIRED);
        }
        RoleDO role = roleService.getRoleByCode(MEMBER_ROLE_CODE);
        if (role == null || CommonStatusEnum.isDisable(role.getStatus())) {
            throw exception(MEMBER_WXCP_ROLE_NOT_AVAILABLE);
        }
        boolean hasMobile = StrUtil.isNotBlank(mobile);
        String username = hasMobile ? mobile : "wx" + UUID.randomUUID().toString().replace("-", "").substring(0, 16);
        String password = hasMobile ? mobile : UUID.randomUUID().toString().replace("-", "").substring(0, 16);
        String nickname = StrUtil.maxLength(application.getApplicantName(), 30);
        try {
            return adminUserApi.createUser(new AdminUserCreateReqDTO()
@@ -234,9 +265,9 @@
                    .setNickname(nickname)
                    .setDeptId(MEMBER_DEPT_ID)
                    .setPostIds(Collections.emptySet())
                    .setRoleIds(Collections.singleton(MEMBER_ROLE_ID))
                    .setMobile(mobile)
                    .setPassword(mobile) // é»˜è®¤å¯†ç ä¸ºæ‰‹æœºå·
                    .setRoleIds(Collections.singleton(role.getId()))
                    .setMobile(hasMobile ? mobile : null)
                    .setPassword(password)
                    .setEmail(application.getContactEmail())
                    .setRemark("由入会申请自动创建:" + application.getNo()));
        } catch (Exception e) {
@@ -291,4 +322,4 @@
        return archive != null ? archive.getNo() : null;
    }
}
}
yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/service/member/WxcpInitialPasswordService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,38 @@
package cn.iocoder.yudao.module.ums.service.member;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import cn.iocoder.yudao.module.system.service.auth.WxcpPasswordSetupGrant;
import cn.iocoder.yudao.module.system.service.user.AdminUserService;
import cn.iocoder.yudao.module.ums.dal.dataobject.member.UmsMemberDO;
import cn.iocoder.yudao.module.ums.dal.mysql.member.UmsMemberMapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.ums.enums.ErrorCodeConstants.*;
@Service
public class WxcpInitialPasswordService {
    @Resource private UmsMemberMapper memberMapper;
    @Resource private AdminUserService userService;
    @Resource private WxcpPasswordSetupGrant grant;
    @Transactional(rollbackFor = Exception.class)
    public String setPassword(Long userId, String token, String password) {
        if (!grant.valid(token, userId)) throw exception(MEMBER_PASSWORD_SETUP_EXPIRED);
        UmsMemberDO member = memberMapper.selectByUserId(userId);
        if (member == null || !Boolean.TRUE.equals(member.getPasswordSetupRequired())) {
            throw exception(MEMBER_PASSWORD_SETUP_NOT_REQUIRED);
        }
        int changed = memberMapper.update(null, new LambdaUpdateWrapper<UmsMemberDO>()
                .eq(UmsMemberDO::getId, member.getId())
                .eq(UmsMemberDO::getUserId, userId)
                .eq(UmsMemberDO::getPasswordSetupRequired, true)
                .set(UmsMemberDO::getPasswordSetupRequired, false));
        if (changed != 1) throw exception(MEMBER_PASSWORD_SETUP_NOT_REQUIRED);
        userService.updateUserPassword(userId, password);
        String username = userService.getUser(userId).getUsername();
        grant.revoke(token);
        return username;
    }
}
yudao-module-ums/src/test/java/cn/iocoder/yudao/module/ums/service/application/UmsApplicationApprovalValidatorTest.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,46 @@
package cn.iocoder.yudao.module.ums.service.application;
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
import cn.iocoder.yudao.module.ums.dal.dataobject.application.UmsApplicationDO;
import cn.iocoder.yudao.module.ums.dal.mysql.application.UmsApplicationMapper;
import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.task.api.Task;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
class UmsApplicationApprovalValidatorTest extends BaseMockitoUnitTest {
    @InjectMocks private UmsApplicationApprovalValidator validator;
    @Mock private UmsApplicationMapper mapper;
    @Mock private ProcessInstance instance;
    @Mock private Task task;
    @Test void requiresConfirmedFeeBeforeApproval() {
        when(instance.getProcessDefinitionKey()).thenReturn("ums_membership_approve");
        when(instance.getBusinessKey()).thenReturn("12");
        when(mapper.selectById(12L)).thenReturn(new UmsApplicationDO());
        assertThrows(RuntimeException.class, () -> validator.validate(1L, task, instance));
    }
    @Test void permitsExplicitUnpaid() {
        when(instance.getProcessDefinitionKey()).thenReturn("ums_membership_approve");
        when(instance.getBusinessKey()).thenReturn("12");
        when(mapper.selectById(12L)).thenReturn(new UmsApplicationDO().setFeePaid(false));
        assertDoesNotThrow(() -> validator.validate(1L, task, instance));
    }
    @Test void ignoresOtherProcesses() {
        when(instance.getProcessDefinitionKey()).thenReturn("other");
        validator.validate(1L, task, instance);
        verifyNoInteractions(mapper);
    }
    @Test void permitsAutomaticSubmissionOfStarterNode() {
        when(instance.getProcessDefinitionKey()).thenReturn("ums_membership_approve");
        when(task.getTaskDefinitionKey()).thenReturn("StartUserNode");
        validator.validate(1L, task, instance);
        verifyNoInteractions(mapper);
    }
}
yudao-module-ums/src/test/java/cn/iocoder/yudao/module/ums/service/application/UmsApplicationFeeConfirmationTest.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,33 @@
package cn.iocoder.yudao.module.ums.service.application;
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
import cn.iocoder.yudao.module.bpm.service.task.BpmTaskService;
import cn.iocoder.yudao.module.ums.dal.dataobject.application.UmsApplicationDO;
import cn.iocoder.yudao.module.ums.dal.mysql.application.UmsApplicationMapper;
import cn.iocoder.yudao.module.ums.enums.UmsApplicationStatusEnum;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
class UmsApplicationFeeConfirmationTest extends BaseMockitoUnitTest {
    @InjectMocks private UmsApplicationServiceImpl service;
    @Mock private UmsApplicationMapper mapper;
    @Mock private BpmTaskService taskService;
    @Test void nonAssigneeCannotConfirm() {
        when(mapper.selectById(1L)).thenReturn(new UmsApplicationDO()
                .setStatus(UmsApplicationStatusEnum.APPROVING.getStatus()).setProcessInstanceId("p1"));
        when(taskService.getTodoTask(9L, null, "p1")).thenReturn(null);
        assertThrows(RuntimeException.class, () -> service.confirmFee(1L, 9L, true));
        verify(mapper, never()).update(any(), any());
    }
    @Test void completedApplicationCannotBeChanged() {
        when(mapper.selectById(1L)).thenReturn(new UmsApplicationDO()
                .setStatus(UmsApplicationStatusEnum.APPROVED.getStatus()).setProcessInstanceId("p1"));
        assertFalse(service.canConfirmFee(1L, 9L));
        verifyNoInteractions(taskService);
    }
}
yudao-module-ums/src/test/java/cn/iocoder/yudao/module/ums/service/application/WxcpMembershipRegistrationImplTest.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,52 @@
package cn.iocoder.yudao.module.ums.service.application;
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
import cn.iocoder.yudao.module.ums.dal.dataobject.application.UmsApplicationDO;
import cn.iocoder.yudao.module.ums.dal.mysql.application.UmsApplicationMapper;
import cn.iocoder.yudao.module.ums.dal.redis.UmsNoRedisDAO;
import cn.iocoder.yudao.module.ums.enums.UmsApplicationStatusEnum;
import cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi;
import cn.iocoder.yudao.module.system.service.user.AdminUserService;
import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
import cn.iocoder.yudao.module.system.service.permission.PermissionService;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
class WxcpMembershipRegistrationImplTest extends BaseMockitoUnitTest {
    @InjectMocks private WxcpMembershipRegistrationImpl service;
    @Mock private UmsApplicationMapper mapper;
    @Mock private UmsNoRedisDAO noRedisDAO;
    @Mock private BpmProcessInstanceApi processInstanceApi;
    @Mock private AdminUserService userService;
    @Mock private PermissionService permissionService;
    @Test
    void firstScanStartsApprovalWithUnconfirmedFee() {
        when(noRedisDAO.generate(anyString())).thenReturn("SQ001");
        when(userService.getUserByUsername("wxcpservice")).thenReturn(new AdminUserDO().setId(5L).setStatus(0).setRemark("仅用于系统自动发起入会审批"));
        when(processInstanceApi.createProcessInstance(eq(5L), any())).thenReturn("process-1");
        service.startIfAbsent("openid-1", "会员", "13800138000");
        ArgumentCaptor<UmsApplicationDO> captor = ArgumentCaptor.forClass(UmsApplicationDO.class);
        verify(mapper).insert(captor.capture());
        UmsApplicationDO draft = captor.getValue();
        assertEquals("openid-1", draft.getWxcpOpenid());
        assertEquals(UmsApplicationStatusEnum.APPROVING.getStatus(), draft.getStatus());
        assertNull(draft.getFeePaid());
        assertEquals("13800138000", draft.getContactPhone());
        verify(processInstanceApi).createProcessInstance(eq(5L), any());
    }
    @Test
    void repeatScanDoesNotCreateAnotherDraft() {
        when(mapper.selectOne(any(Wrapper.class))).thenReturn(new UmsApplicationDO());
        service.startIfAbsent("openid-1", "会员", null);
        verify(mapper, never()).insert(any(UmsApplicationDO.class));
        verifyNoInteractions(processInstanceApi);
    }
}
yudao-module-ums/src/test/java/cn/iocoder/yudao/module/ums/service/member/WxcpInitialPasswordServiceTest.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,49 @@
package cn.iocoder.yudao.module.ums.service.member;
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
import cn.iocoder.yudao.module.system.service.auth.WxcpPasswordSetupGrant;
import cn.iocoder.yudao.module.system.service.user.AdminUserService;
import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
import cn.iocoder.yudao.module.ums.dal.dataobject.member.UmsMemberDO;
import cn.iocoder.yudao.module.ums.dal.mysql.member.UmsMemberMapper;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
class WxcpInitialPasswordServiceTest extends BaseMockitoUnitTest {
    @InjectMocks private WxcpInitialPasswordService service;
    @Mock private UmsMemberMapper memberMapper;
    @Mock private AdminUserService userService;
    @Mock private WxcpPasswordSetupGrant grant;
    @Test void rejectsSessionWithoutWxcpGrant() {
        when(grant.valid("token", 1L)).thenReturn(false);
        assertThrows(RuntimeException.class, () -> service.setPassword(1L, "token", "password1"));
        verifyNoInteractions(memberMapper, userService);
    }
    @Test void rejectsAccountWithoutPendingSetup() {
        when(grant.valid("token", 1L)).thenReturn(true);
        when(memberMapper.selectByUserId(1L)).thenReturn(new UmsMemberDO().setPasswordSetupRequired(false));
        assertThrows(RuntimeException.class, () -> service.setPassword(1L, "token", "password1"));
        verifyNoInteractions(userService);
    }
    @Test void setsPasswordOnceAndReturnsOwnUsername() {
        TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), UmsMemberDO.class);
        when(grant.valid("token", 1L)).thenReturn(true);
        when(memberMapper.selectByUserId(1L)).thenReturn(new UmsMemberDO().setId(2L).setPasswordSetupRequired(true));
        when(memberMapper.update(isNull(), any(Wrapper.class))).thenReturn(1);
        when(userService.getUser(1L)).thenReturn(new AdminUserDO().setUsername("wx_example"));
        assertEquals("wx_example", service.setPassword(1L, "token", "password1"));
        verify(userService).updateUserPassword(1L, "password1");
        verify(grant).revoke("token");
    }
}