From d0cf2cb2c352bdeb146aad2bb57ba7d39cef3bbd Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期二, 03 十一月 2026 17:59:04 +0800
Subject: [PATCH] 完善会员审批及门户配置

---
 yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/auth/WxcpMembershipRegistration.java                          |    7 
 yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/controller/admin/application/vo/UmsApplicationRespVO.java                   |    5 
 yudao-module-ums/src/test/java/cn/iocoder/yudao/module/ums/service/member/WxcpInitialPasswordServiceTest.java                          |   49 +++
 yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskApprovalValidator.java                                  |    9 
 yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialUserRespDTO.java                                 |    7 
 sql/mysql/ums_member_password_setup.sql                                                                                                |    2 
 yudao-module-research/src/main/java/cn/iocoder/yudao/module/research/dal/mysql/application/ResearchApplicationMemberMapper.java        |    7 
 yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java                             |    6 
 yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/service/application/WxcpMembershipRegistrationImpl.java                     |  106 ++++++++
 yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/service/application/UmsApplicationServiceImpl.java                          |   42 +++
 yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/dal/dataobject/application/UmsApplicationDO.java                            |    8 
 yudao-module-ums/src/test/java/cn/iocoder/yudao/module/ums/service/application/WxcpMembershipRegistrationImplTest.java                 |   52 ++++
 yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/auth/WxcpPasswordSetupGrant.java                              |   21 +
 yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/controller/admin/application/vo/UmsApplicationSaveReqVO.java                |    5 
 yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/controller/admin/member/vo/UmsMemberRespVO.java                             |    5 
 yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/service/application/UmsApplicationService.java                              |    5 
 yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/dal/dataobject/member/UmsMemberDO.java                                      |    5 
 yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/controller/admin/application/UmsApplicationController.java                  |   16 +
 sql/mysql/research_application_member_type.sql                                                                                         |    3 
 yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/controller/admin/member/vo/profile/WxcpInitialPasswordReqVO.java            |   12 
 yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java                                |   25 +
 yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/homebanner/HomeBannerController.java                 |   40 +++
 yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java                                        |    3 
 yudao-module-research/src/main/java/cn/iocoder/yudao/module/research/controller/admin/application/vo/ResearchApplicationSaveReqVO.java |    7 
 yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/homebanner/HomeBannerDO.java                           |   13 +
 yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/user/dto/AdminUserCreateReqDTO.java                               |    5 
 yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialUserService.java                                 |    3 
 yudao-module-ums/src/test/java/cn/iocoder/yudao/module/ums/service/application/UmsApplicationFeeConfirmationTest.java                  |   33 ++
 yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/dal/mysql/member/UmsMemberMapper.java                                       |    6 
 yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java                                         |    1 
 yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/service/member/UmsMemberServiceImpl.java                                    |   55 +++
 yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/service/application/listener/UmsApplicationStatusListener.java              |   13 
 yudao-module-ums/src/test/java/cn/iocoder/yudao/module/ums/service/application/UmsApplicationApprovalValidatorTest.java                |   46 +++
 yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/service/member/WxcpInitialPasswordService.java                              |   38 ++
 sql/mysql/ums_application_fee_paid.sql                                                                                                 |    2 
 sql/mysql/system_home_banner.sql                                                                                                       |   16 +
 yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/homebanner/HomeBannerMapper.java                            |    5 
 yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/controller/admin/member/UmsMemberProfileChangeController.java               |   21 +
 yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/service/member/UmsMemberProfileChangeServiceImpl.java                       |    3 
 sql/mysql/ums_application_wxcp_openid.sql                                                                                              |    4 
 yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/enums/ErrorCodeConstants.java                                               |    9 
 yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialUserServiceImpl.java                             |   30 ++
 yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/service/application/UmsApplicationApprovalValidator.java                    |   29 ++
 43 files changed, 736 insertions(+), 43 deletions(-)

diff --git a/sql/mysql/research_application_member_type.sql b/sql/mysql/research_application_member_type.sql
new file mode 100644
index 0000000..5e59ae6
--- /dev/null
+++ b/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 '鎴愬憳绫诲瀷锛氭暀甯堛�佸鐢熴�佹牎澶栦汉鍛�';
diff --git a/sql/mysql/system_home_banner.sql b/sql/mysql/system_home_banner.sql
new file mode 100644
index 0000000..cc43ac0
--- /dev/null
+++ b/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');
diff --git a/sql/mysql/ums_application_fee_paid.sql b/sql/mysql/ums_application_fee_paid.sql
new file mode 100644
index 0000000..cb0b313
--- /dev/null
+++ b/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 '鐢宠鏃舵槸鍚︾即璐�';
diff --git a/sql/mysql/ums_application_wxcp_openid.sql b/sql/mysql/ums_application_wxcp_openid.sql
new file mode 100644
index 0000000..e51e55f
--- /dev/null
+++ b/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);
diff --git a/sql/mysql/ums_member_password_setup.sql b/sql/mysql/ums_member_password_setup.sql
new file mode 100644
index 0000000..77b3e04
--- /dev/null
+++ b/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 '鏄惁闇�瑕侀娆¤缃瘑鐮�';
diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskApprovalValidator.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskApprovalValidator.java
new file mode 100644
index 0000000..df5c988
--- /dev/null
+++ b/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);
+}
diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java
index b32d85f..9e9fcc2 100644
--- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java
+++ b/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());
diff --git a/yudao-module-research/src/main/java/cn/iocoder/yudao/module/research/controller/admin/application/vo/ResearchApplicationSaveReqVO.java b/yudao-module-research/src/main/java/cn/iocoder/yudao/module/research/controller/admin/application/vo/ResearchApplicationSaveReqVO.java
index 806de20..407eab4 100644
--- a/yudao-module-research/src/main/java/cn/iocoder/yudao/module/research/controller/admin/application/vo/ResearchApplicationSaveReqVO.java
+++ b/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锛�")
diff --git a/yudao-module-research/src/main/java/cn/iocoder/yudao/module/research/dal/mysql/application/ResearchApplicationMemberMapper.java b/yudao-module-research/src/main/java/cn/iocoder/yudao/module/research/dal/mysql/application/ResearchApplicationMemberMapper.java
index 099c3de..4cd0ce7 100644
--- a/yudao-module-research/src/main/java/cn/iocoder/yudao/module/research/dal/mysql/application/ResearchApplicationMemberMapper.java
+++ b/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) {
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialUserRespDTO.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialUserRespDTO.java
index 0492c3b..5df2584 100644
--- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/social/dto/SocialUserRespDTO.java
+++ b/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;
+    }
+
 }
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/user/dto/AdminUserCreateReqDTO.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/user/dto/AdminUserCreateReqDTO.java
index 2474d99..490546c 100644
--- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/user/dto/AdminUserCreateReqDTO.java
+++ b/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;
 
-}
\ No newline at end of file
+}
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java
index 974585a..5bf1cfc 100644
--- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java
+++ b/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);
     }
 
 }
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/homebanner/HomeBannerController.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/homebanner/HomeBannerController.java
new file mode 100644
index 0000000..249788e
--- /dev/null
+++ b/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;
+ }
+}
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/homebanner/HomeBannerDO.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/homebanner/HomeBannerDO.java
new file mode 100644
index 0000000..1c584c7
--- /dev/null
+++ b/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;
+}
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/homebanner/HomeBannerMapper.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/homebanner/HomeBannerMapper.java
new file mode 100644
index 0000000..2d474be
--- /dev/null
+++ b/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> {}
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java
index 5af11a3..42374ee 100644
--- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java
+++ b/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, "楠岃瘉鐮佷笉姝g‘锛屽師鍥狅細{}");
+    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, "宸茬粡瀛樺湪璇ュ悕瀛楃殑鑿滃崟");
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java
index 72de06f..4f21422 100644
--- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java
+++ b/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());
     }
-}
\ No newline at end of file
+}
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/auth/WxcpMembershipRegistration.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/auth/WxcpMembershipRegistration.java
new file mode 100644
index 0000000..407647a
--- /dev/null
+++ b/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);
+}
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/auth/WxcpPasswordSetupGrant.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/auth/WxcpPasswordSetupGrant.java
new file mode 100644
index 0000000..f5d782e
--- /dev/null
+++ b/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)); }
+}
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialUserService.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialUserService.java
index f7901da..d1abcb9 100644
--- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialUserService.java
+++ b/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);
+
     /**
      * 鑾峰緱鎸囧畾鐢ㄦ埛鐨勭ぞ浜ょ敤鎴峰垪琛�
      *
diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialUserServiceImpl.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialUserServiceImpl.java
index 2ee215b..63efddb 100644
--- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/social/SocialUserServiceImpl.java
+++ b/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;
     }
 
     /**
diff --git a/yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/controller/admin/application/UmsApplicationController.java b/yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/controller/admin/application/UmsApplicationController.java
index 8cd451a..2d06523 100644
--- a/yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/controller/admin/application/UmsApplicationController.java
+++ b/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 @@
         });
     }
 
-}
\ No newline at end of file
+}
diff --git a/yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/controller/admin/application/vo/UmsApplicationRespVO.java b/yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/controller/admin/application/vo/UmsApplicationRespVO.java
index c2d0b09..8fd7c18 100644
--- a/yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/controller/admin/application/vo/UmsApplicationRespVO.java
+++ b/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;
 
-}
\ No newline at end of file
+}
diff --git a/yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/controller/admin/application/vo/UmsApplicationSaveReqVO.java b/yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/controller/admin/application/vo/UmsApplicationSaveReqVO.java
index a5d6c57..9b7e0d4 100644
--- a/yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/controller/admin/application/vo/UmsApplicationSaveReqVO.java
+++ b/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 = "鍦板尯缂栫爜锛圓rea 鏍戞湯绾э級", example = "110000")
     private String regionCode;
 
@@ -58,4 +61,4 @@
     @Schema(description = "闄勪欢BlobId鍒楄〃")
     private List<Long> blobIds;
 
-}
\ No newline at end of file
+}
diff --git a/yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/controller/admin/member/UmsMemberProfileChangeController.java b/yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/controller/admin/member/UmsMemberProfileChangeController.java
index bd382e6..2f3b70a 100644
--- a/yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/controller/admin/member/UmsMemberProfileChangeController.java
+++ b/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")
diff --git a/yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/controller/admin/member/vo/UmsMemberRespVO.java b/yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/controller/admin/member/vo/UmsMemberRespVO.java
index 7944a0a..2c9203f 100644
--- a/yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/controller/admin/member/vo/UmsMemberRespVO.java
+++ b/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;
 
-}
\ No newline at end of file
+}
diff --git a/yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/controller/admin/member/vo/profile/WxcpInitialPasswordReqVO.java b/yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/controller/admin/member/vo/profile/WxcpInitialPasswordReqVO.java
new file mode 100644
index 0000000..7388e45
--- /dev/null
+++ b/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;
+}
diff --git a/yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/dal/dataobject/application/UmsApplicationDO.java b/yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/dal/dataobject/application/UmsApplicationDO.java
index 32f8d5b..1035e9f 100644
--- a/yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/dal/dataobject/application/UmsApplicationDO.java
+++ b/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;
+
     /** 鍦板尯缂栫爜锛圓rea 鏍戞湯绾э級 */
     private String regionCode;
 
@@ -74,4 +80,4 @@
     /** 澶囨敞 */
     private String remark;
 
-}
\ No newline at end of file
+}
diff --git a/yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/dal/dataobject/member/UmsMemberDO.java b/yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/dal/dataobject/member/UmsMemberDO.java
index 5b7994e..f1421c3 100644
--- a/yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/dal/dataobject/member/UmsMemberDO.java
+++ b/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;
 
-}
\ No newline at end of file
+}
diff --git a/yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/dal/mysql/member/UmsMemberMapper.java b/yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/dal/mysql/member/UmsMemberMapper.java
index af4b970..efb7b89 100644
--- a/yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/dal/mysql/member/UmsMemberMapper.java
+++ b/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));
     }
 
-}
\ No newline at end of file
+}
diff --git a/yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/enums/ErrorCodeConstants.java b/yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/enums/ErrorCodeConstants.java
index 99deaa6..e23377e 100644
--- a/yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/enums/ErrorCodeConstants.java
+++ b/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, "鍏ヤ細瀹℃壒鍒嗙被涓嶅瓨鍦紝璇峰厛鍦˙PM妯″潡鍒涘缓鍏ヤ細瀹℃壒娴佺▼鍒嗙被");
+    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, "閲嶅褰掓。锛氫細鍛�({})宸蹭簬姝ゅ墠褰掓。骞剁敓鎴愭。妗�(缂栧彿={})锛岃鍕块噸澶嶅綊妗o紝濡傞渶鏌ョ湅璇峰墠寰�妗f绠$悊妯″潡");
     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, "璧勬枡鏈彂鐢熶换浣曞彉鏇达紝鏃犻渶鎻愪氦瀹℃牳");
 
-}
\ No newline at end of file
+}
diff --git a/yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/service/application/UmsApplicationApprovalValidator.java b/yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/service/application/UmsApplicationApprovalValidator.java
new file mode 100644
index 0000000..cb6b284
--- /dev/null
+++ b/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);
+    }
+}
diff --git a/yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/service/application/UmsApplicationService.java b/yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/service/application/UmsApplicationService.java
index f1208a5..4f75a1e 100644
--- a/yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/service/application/UmsApplicationService.java
+++ b/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);
 
-}
\ No newline at end of file
+}
diff --git a/yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/service/application/UmsApplicationServiceImpl.java b/yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/service/application/UmsApplicationServiceImpl.java
index 3b005e8..5d17c2e 100644
--- a/yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/service/application/UmsApplicationServiceImpl.java
+++ b/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);
     }
 
-}
\ No newline at end of file
+}
diff --git a/yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/service/application/WxcpMembershipRegistrationImpl.java b/yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/service/application/WxcpMembershipRegistrationImpl.java
new file mode 100644
index 0000000..81856ea
--- /dev/null
+++ b/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);
+    }
+}
diff --git a/yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/service/application/listener/UmsApplicationStatusListener.java b/yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/service/application/listener/UmsApplicationStatusListener.java
index 3d1984c..4d9e2fd 100644
--- a/yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/service/application/listener/UmsApplicationStatusListener.java
+++ b/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);
-            }
-        }
     }
 
-}
\ No newline at end of file
+}
diff --git a/yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/service/member/UmsMemberProfileChangeServiceImpl.java b/yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/service/member/UmsMemberProfileChangeServiceImpl.java
index fb118dc..55e45b4 100644
--- a/yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/service/member/UmsMemberProfileChangeServiceImpl.java
+++ b/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
diff --git a/yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/service/member/UmsMemberServiceImpl.java b/yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/service/member/UmsMemberServiceImpl.java
index 5b5f9b4..77c45b0 100644
--- a/yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/service/member/UmsMemberServiceImpl.java
+++ b/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锛坰ystem_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;
     }
 
-}
\ No newline at end of file
+}
diff --git a/yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/service/member/WxcpInitialPasswordService.java b/yudao-module-ums/src/main/java/cn/iocoder/yudao/module/ums/service/member/WxcpInitialPasswordService.java
new file mode 100644
index 0000000..baddee1
--- /dev/null
+++ b/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;
+    }
+}
diff --git a/yudao-module-ums/src/test/java/cn/iocoder/yudao/module/ums/service/application/UmsApplicationApprovalValidatorTest.java b/yudao-module-ums/src/test/java/cn/iocoder/yudao/module/ums/service/application/UmsApplicationApprovalValidatorTest.java
new file mode 100644
index 0000000..27406e1
--- /dev/null
+++ b/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);
+    }
+}
diff --git a/yudao-module-ums/src/test/java/cn/iocoder/yudao/module/ums/service/application/UmsApplicationFeeConfirmationTest.java b/yudao-module-ums/src/test/java/cn/iocoder/yudao/module/ums/service/application/UmsApplicationFeeConfirmationTest.java
new file mode 100644
index 0000000..3e78a78
--- /dev/null
+++ b/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);
+    }
+}
diff --git a/yudao-module-ums/src/test/java/cn/iocoder/yudao/module/ums/service/application/WxcpMembershipRegistrationImplTest.java b/yudao-module-ums/src/test/java/cn/iocoder/yudao/module/ums/service/application/WxcpMembershipRegistrationImplTest.java
new file mode 100644
index 0000000..38b6bc7
--- /dev/null
+++ b/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);
+    }
+}
diff --git a/yudao-module-ums/src/test/java/cn/iocoder/yudao/module/ums/service/member/WxcpInitialPasswordServiceTest.java b/yudao-module-ums/src/test/java/cn/iocoder/yudao/module/ums/service/member/WxcpInitialPasswordServiceTest.java
new file mode 100644
index 0000000..0768342
--- /dev/null
+++ b/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");
+    }
+}

--
Gitblit v1.9.3