Эх сурвалжийг харах

feat(auth): 新增微信小程序手机号Code登录功能

- 新增微信小程序手机号Code登录接口(/wx/miniapp/phone-code-login)
- 新增WxMiniAppPhoneCodeLoginDTO数据传输对象
- 新增WxMiniAppPhoneCodeAuthenticationToken认证令牌
- 新增WxMiniAppPhoneCodeAuthenticationProvider认证提供者
- 更新SecurityConfig配置,注册新的认证Provider
- 新增微信支付相关工具类和常量配置
- 新增wechatpay-apache-httpclient依赖
- 更新应用配置文件中的微信小程序配置信息
- 扩展安全白名单路径支持微信相关所有接口
- 优化用户注册逻辑,支持仅通过手机号注册场景
- 新增获取当前登录用户信息接口(/current)
wzq 1 өдөр өмнө
parent
commit
63be287ccd
19 өөрчлөгдсөн 554 нэмэгдсэн , 45 устгасан
  1. 6 0
      pom.xml
  2. 10 0
      src/main/java/com/zsElectric/boot/auth/controller/AuthController.java
  3. 9 0
      src/main/java/com/zsElectric/boot/auth/service/AuthService.java
  4. 25 0
      src/main/java/com/zsElectric/boot/auth/service/impl/AuthServiceImpl.java
  5. 7 0
      src/main/java/com/zsElectric/boot/business/controller/UserInfoController.java
  6. 9 0
      src/main/java/com/zsElectric/boot/business/converter/UserInfoConverter.java
  7. 2 1
      src/main/java/com/zsElectric/boot/business/mapper/CouponTemplateMapper.java
  8. 16 0
      src/main/java/com/zsElectric/boot/business/model/query/CouponTemplateQuery.java
  9. 9 0
      src/main/java/com/zsElectric/boot/business/service/UserInfoService.java
  10. 40 1
      src/main/java/com/zsElectric/boot/business/service/impl/UserInfoServiceImpl.java
  11. 13 1
      src/main/java/com/zsElectric/boot/config/SecurityConfig.java
  12. 28 0
      src/main/java/com/zsElectric/boot/core/pay/WechatConstants.java
  13. 247 0
      src/main/java/com/zsElectric/boot/core/pay/WechatPayV3Utils.java
  14. 40 0
      src/main/java/com/zsElectric/boot/core/pay/WechatUrlConstants.java
  15. 21 1
      src/main/java/com/zsElectric/boot/platform/codegen/service/impl/GenConfigServiceImpl.java
  16. 12 3
      src/main/java/com/zsElectric/boot/system/service/UserService.java
  17. 46 29
      src/main/java/com/zsElectric/boot/system/service/impl/UserServiceImpl.java
  18. 6 4
      src/main/resources/application-dev.yml
  19. 8 5
      src/main/resources/application-prod.yml

+ 6 - 0
pom.xml

@@ -98,6 +98,12 @@
             <version>0.11.5</version>
             <scope>runtime</scope>
         </dependency>
+        <!-- 微信支付官方SDK -->
+        <dependency>
+            <groupId>com.github.wechatpay-apiv3</groupId>
+            <artifactId>wechatpay-apache-httpclient</artifactId>
+            <version>0.4.8</version>
+        </dependency>
 
 
         <dependency>

+ 10 - 0
src/main/java/com/zsElectric/boot/auth/controller/AuthController.java

@@ -2,6 +2,7 @@ package com.zsElectric.boot.auth.controller;
 
 import com.zsElectric.boot.auth.model.vo.CaptchaVO;
 import com.zsElectric.boot.auth.model.dto.WxMiniAppPhoneLoginDTO;
+import com.zsElectric.boot.auth.model.dto.WxMiniAppPhoneCodeLoginDTO;
 import com.zsElectric.boot.common.enums.LogModuleEnum;
 import com.zsElectric.boot.core.web.Result;
 import com.zsElectric.boot.auth.service.AuthService;
@@ -94,6 +95,15 @@ public class AuthController {
         return Result.success(token);
     }
 
+    @Operation(summary = "微信小程序登录(手机号Code-新版)")
+    @PostMapping("/wx/miniapp/phone-code-login")
+    public Result<AuthenticationToken> loginByWxMiniAppPhoneCode(@RequestBody @Valid WxMiniAppPhoneCodeLoginDTO loginDTO) {
+        log.info("收到手机号Code登录请求, code: {}", loginDTO.getCode());
+        AuthenticationToken token = authService.loginByWxMiniAppPhoneCode(loginDTO);
+        log.info("手机号Code登录成功");
+        return Result.success(token);
+    }
+
     @Operation(summary = "运营商获取token")
     @PostMapping("/query_token")
     public Result<AuthenticationToken> loginBy(@RequestBody @Valid WxMiniAppPhoneLoginDTO loginDTO) {

+ 9 - 0
src/main/java/com/zsElectric/boot/auth/service/AuthService.java

@@ -1,5 +1,6 @@
 package com.zsElectric.boot.auth.service;
 
+import com.zsElectric.boot.auth.model.dto.WxMiniAppPhoneCodeLoginDTO;
 import com.zsElectric.boot.auth.model.vo.CaptchaVO;
 import com.zsElectric.boot.auth.model.dto.WxMiniAppPhoneLoginDTO;
 import com.zsElectric.boot.security.model.AuthenticationToken;
@@ -66,6 +67,14 @@ public interface AuthService {
      */
     AuthenticationToken loginByWxMiniAppPhone(WxMiniAppPhoneLoginDTO loginDTO);
 
+    /**
+     * 微信小程序手机号Code登录(新版接口)
+     *
+     * @param loginDTO 登录参数
+     * @return 访问令牌
+     */
+    AuthenticationToken loginByWxMiniAppPhoneCode(WxMiniAppPhoneCodeLoginDTO loginDTO);
+
     /**
      * 发送短信验证码
      *

+ 25 - 0
src/main/java/com/zsElectric/boot/auth/service/impl/AuthServiceImpl.java

@@ -6,6 +6,7 @@ import cn.hutool.captcha.generator.CodeGenerator;
 import cn.hutool.core.util.IdUtil;
 import cn.hutool.core.util.StrUtil;
 import com.zsElectric.boot.auth.model.dto.WxMiniAppCodeLoginDTO;
+import com.zsElectric.boot.auth.model.dto.WxMiniAppPhoneCodeLoginDTO;
 import com.zsElectric.boot.auth.model.dto.WxMiniAppPhoneLoginDTO;
 import com.zsElectric.boot.auth.model.vo.CaptchaVO;
 import com.zsElectric.boot.auth.service.AuthService;
@@ -19,6 +20,7 @@ import com.zsElectric.boot.security.model.AuthenticationToken;
 import com.zsElectric.boot.security.model.SmsAuthenticationToken;
 import com.zsElectric.boot.security.model.WxMiniAppCodeAuthenticationToken;
 import com.zsElectric.boot.security.model.WxMiniAppPhoneAuthenticationToken;
+import com.zsElectric.boot.security.model.WxMiniAppPhoneCodeAuthenticationToken;
 import com.zsElectric.boot.security.token.TokenManager;
 import com.zsElectric.boot.security.util.SecurityUtils;
 import lombok.RequiredArgsConstructor;
@@ -266,4 +268,27 @@ public class AuthServiceImpl implements AuthService {
 
         return token;
     }
+
+    /**
+     * 微信小程序手机号Code登录(新版接口)
+     *
+     * @param loginDTO 登录参数
+     * @return 访问令牌
+     */
+    @Override
+    public AuthenticationToken loginByWxMiniAppPhoneCode(WxMiniAppPhoneCodeLoginDTO loginDTO) {
+        // 创建微信小程序手机号Code认证Token
+        WxMiniAppPhoneCodeAuthenticationToken authenticationToken = new WxMiniAppPhoneCodeAuthenticationToken(
+                loginDTO.getCode()
+        );
+
+        // 执行认证
+        Authentication authentication = authenticationManager.authenticate(authenticationToken);
+
+        // 认证成功后生成JWT令牌,并存入Security上下文
+        AuthenticationToken token = tokenManager.generateToken(authentication);
+        SecurityContextHolder.getContext().setAuthentication(authentication);
+
+        return token;
+    }
 }

+ 7 - 0
src/main/java/com/zsElectric/boot/business/controller/UserInfoController.java

@@ -32,6 +32,13 @@ public class UserInfoController  {
 
     private final UserInfoService userInfoService;
 
+    @Operation(summary = "获取当前登录用户信息", description = "小程序登录后获取当前用户详细信息")
+    @GetMapping("/current")
+    public Result<UserInfoVO> getCurrentUserInfo() {
+        UserInfoVO userInfo = userInfoService.getCurrentUserInfo();
+        return Result.success(userInfo);
+    }
+
     @Operation(summary = "个人用户信息分页列表")
     @GetMapping("/page")
     @PreAuthorize("@ss.hasPerm('business:user-info:query')")

+ 9 - 0
src/main/java/com/zsElectric/boot/business/converter/UserInfoConverter.java

@@ -1,5 +1,6 @@
 package com.zsElectric.boot.business.converter;
 
+import com.zsElectric.boot.business.model.vo.UserInfoVO;
 import org.mapstruct.Mapper;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.zsElectric.boot.business.model.entity.UserInfo;
@@ -17,4 +18,12 @@ public interface UserInfoConverter{
     UserInfoForm toForm(UserInfo entity);
 
     UserInfo toEntity(UserInfoForm formData);
+    
+    /**
+     * 实体转换为VO
+     *
+     * @param entity 实体对象
+     * @return VO对象
+     */
+    UserInfoVO toVO(UserInfo entity);
 }

+ 2 - 1
src/main/java/com/zsElectric/boot/business/mapper/CouponTemplateMapper.java

@@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.zsElectric.boot.business.model.query.CouponTemplateQuery;
 import com.zsElectric.boot.business.model.vo.CouponTemplateVO;
 import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
 
 /**
  * 优惠劵模板Mapper接口
@@ -23,6 +24,6 @@ public interface CouponTemplateMapper extends BaseMapper<CouponTemplate> {
      * @param queryParams 查询参数
      * @return {@link Page<CouponTemplateVO>} 优惠劵模板分页列表
      */
-    Page<CouponTemplateVO> getCouponTemplatePage(Page<CouponTemplateVO> page, CouponTemplateQuery queryParams);
+    Page<CouponTemplateVO> getCouponTemplatePage(Page<CouponTemplateVO> page,@Param("queryParams") CouponTemplateQuery queryParams);
 
 }

+ 16 - 0
src/main/java/com/zsElectric/boot/business/model/query/CouponTemplateQuery.java

@@ -4,6 +4,8 @@ import com.zsElectric.boot.common.base.BasePageQuery;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Getter;
 import lombok.Setter;
+import org.springframework.format.annotation.DateTimeFormat;
+
 import java.time.LocalDateTime;
 import java.util.List;
 
@@ -18,4 +20,18 @@ import java.util.List;
 @Setter
 public class CouponTemplateQuery extends BasePageQuery {
 
+    @Schema(description = "优惠劵模板名称")
+    private String name;
+
+    @Schema(description = "优惠劵模板状态")
+    private Integer status;
+
+    @Schema(description = "开始时间")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime startTime;
+
+    @Schema(description = "结束时间")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime endTime;
+
 }

+ 9 - 0
src/main/java/com/zsElectric/boot/business/service/UserInfoService.java

@@ -55,4 +55,13 @@ public interface UserInfoService extends IService<UserInfo> {
      */
     boolean deleteUserInfos(String ids);
 
+    /**
+     * 获取当前登录用户信息(小程序用)
+     * <p>
+     * 根据当前登录用户的手机号获取用户信息
+     *
+     * @return 当前用户信息
+     */
+    UserInfoVO getCurrentUserInfo();
+
 }

+ 40 - 1
src/main/java/com/zsElectric/boot/business/service/impl/UserInfoServiceImpl.java

@@ -1,7 +1,9 @@
 package com.zsElectric.boot.business.service.impl;
 
 import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
@@ -15,7 +17,6 @@ import com.zsElectric.boot.business.converter.UserInfoConverter;
 
 import java.util.Arrays;
 import java.util.List;
-import java.util.stream.Collectors;
 
 import cn.hutool.core.lang.Assert;
 import cn.hutool.core.util.StrUtil;
@@ -28,6 +29,7 @@ import cn.hutool.core.util.StrUtil;
  */
 @Service
 @RequiredArgsConstructor
+@Slf4j
 public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo> implements UserInfoService {
 
     private final UserInfoConverter userInfoConverter;
@@ -100,4 +102,41 @@ public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo> i
         return this.removeByIds(idList);
     }
 
+    /**
+     * 获取当前登录用户信息(小程序用)
+     * <p>
+     * 根据当前登录用户的手机号获取用户信息
+     * 注意:由于小程序登录创建的是 UserInfo,需要通过手机号查询
+     *
+     * @return 当前用户信息
+     */
+    @Override
+    public UserInfoVO getCurrentUserInfo() {
+        // 从 Spring Security 上下文获取当前登录用户的用户名(即手机号)
+        String mobile = com.zsElectric.boot.security.util.SecurityUtils.getUsername();
+        
+        log.info("获取当前用户信息,手机号: {}", mobile);
+        
+        if (StrUtil.isBlank(mobile)) {
+            log.warn("未获取到当前登录用户信息");
+            return null;
+        }
+        
+        // 根据手机号查询 UserInfo
+        UserInfo userInfo = this.getOne(
+                new LambdaQueryWrapper<UserInfo>()
+                        .eq(UserInfo::getPhone, mobile)
+        );
+        
+        if (userInfo == null) {
+            log.warn("未找到手机号 {} 对应的用户信息", mobile);
+            return null;
+        }
+        
+        log.info("获取用户信息成功,ID: {}, 昵称: {}", userInfo.getId(), userInfo.getNickName());
+        
+        // 实体转换为VO
+        return userInfoConverter.toVO(userInfo);
+    }
+
 }

+ 13 - 1
src/main/java/com/zsElectric/boot/config/SecurityConfig.java

@@ -13,6 +13,7 @@ import com.zsElectric.boot.security.handler.MyAuthenticationEntryPoint;
 import com.zsElectric.boot.security.provider.SmsAuthenticationProvider;
 import com.zsElectric.boot.security.provider.WxMiniAppCodeAuthenticationProvider;
 import com.zsElectric.boot.security.provider.WxMiniAppPhoneAuthenticationProvider;
+import com.zsElectric.boot.security.provider.WxMiniAppPhoneCodeAuthenticationProvider;
 import com.zsElectric.boot.security.token.TokenManager;
 import com.zsElectric.boot.security.service.SysUserDetailsService;
 import com.zsElectric.boot.system.service.ConfigService;
@@ -109,17 +110,18 @@ public class SecurityConfig {
         
         log.info("========== 配置 SecurityFilterChain ==========");
 
-
         return http
                 .authorizeHttpRequests(requestMatcherRegistry -> {
                             // 配置无需登录即可访问的公开接口
                             String[] ignoreUrls = securityProperties.getIgnoreUrls();
+                    log.info("安全白名单 ignore-urls: {}", (Object) ignoreUrls);
                             if (ArrayUtil.isNotEmpty(ignoreUrls)) {
                                 requestMatcherRegistry.requestMatchers(ignoreUrls).permitAll();
                             }
                             
                             // 配置完全绕过安全检查的路径(原 unsecuredUrls)
                             String[] unsecuredUrls = securityProperties.getUnsecuredUrls();
+                    log.info("非安全端点 unsecured-urls: {}", (Object) unsecuredUrls);
                             if (ArrayUtil.isNotEmpty(unsecuredUrls)) {
                                 requestMatcherRegistry.requestMatchers(unsecuredUrls).permitAll();
                             }
@@ -178,6 +180,14 @@ public class SecurityConfig {
         return new WxMiniAppPhoneAuthenticationProvider(userService, wxMaService);
     }
 
+    /**
+     * 微信小程序手机号Code认证Provider(新版接口)
+     */
+    @Bean
+    public WxMiniAppPhoneCodeAuthenticationProvider wxMiniAppPhoneCodeAuthenticationProvider() {
+        return new WxMiniAppPhoneCodeAuthenticationProvider(userService, wxMaService);
+    }
+
     /**
      * 短信验证码认证 Provider
      */
@@ -194,12 +204,14 @@ public class SecurityConfig {
             DaoAuthenticationProvider daoAuthenticationProvider,
             WxMiniAppCodeAuthenticationProvider wxMiniAppCodeAuthenticationProvider,
             WxMiniAppPhoneAuthenticationProvider wxMiniAppPhoneAuthenticationProvider,
+            WxMiniAppPhoneCodeAuthenticationProvider wxMiniAppPhoneCodeAuthenticationProvider,
             SmsAuthenticationProvider smsAuthenticationProvider
     ) {
         return new ProviderManager(
                 daoAuthenticationProvider,
                 wxMiniAppCodeAuthenticationProvider,
                 wxMiniAppPhoneAuthenticationProvider,
+                wxMiniAppPhoneCodeAuthenticationProvider,
                 smsAuthenticationProvider
         );
     }

+ 28 - 0
src/main/java/com/zsElectric/boot/core/pay/WechatConstants.java

@@ -0,0 +1,28 @@
+package com.zsElectric.boot.core.pay;
+
+/**
+ * @author wzq
+ * @Date 2025/12/22
+ * @Desc 微信支付相关配置
+ */
+public interface WechatConstants {
+
+    //微信支付商户号
+    String WECHAT_MCH_ID = "1523499681";
+
+    //微信商户平台v3密钥
+    String WECHAT_MCH_SECRET_V3 = "b63646f9b01e573c5bed89dc0f21039e";
+
+    //微信商户平台商户API证书序列号
+    String WECHAT_MCH_SERIAL_NUM = "6CDB06258529D6EA00DE5C0AD9D09A5789EBF735";
+
+    //微信商户平台证书私钥 即证书中的apiclient_key.pem文件中的内容 可以直接写在这里 也可以用流读取文件
+    String WECHAT_MCH_PRIVATE_KEY = "";
+
+    //微信小程序appid
+    String WECHAT_MP_APPID = "wx9b0396a7507e3d66";
+
+    //微信小程序密钥
+    String WECHAT_MP_SECRET = "22b78f76ab5282030ffa08208b223efd";
+
+}

+ 247 - 0
src/main/java/com/zsElectric/boot/core/pay/WechatPayV3Utils.java

@@ -0,0 +1,247 @@
+package com.zsElectric.boot.core.pay;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
+import com.wechat.pay.contrib.apache.httpclient.auth.*;
+import com.wechat.pay.contrib.apache.httpclient.cert.CertificatesManager;
+import com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders;
+import com.wechat.pay.contrib.apache.httpclient.notification.Notification;
+import com.wechat.pay.contrib.apache.httpclient.notification.NotificationHandler;
+import com.wechat.pay.contrib.apache.httpclient.notification.NotificationRequest;
+import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
+import com.zsElectric.boot.common.util.StringUtils;
+import jakarta.annotation.PostConstruct;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.utils.URIBuilder;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.util.EntityUtils;
+import org.springframework.stereotype.Component;
+
+import java.io.BufferedReader;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.security.PrivateKey;
+
+@Slf4j
+@Component
+public class WechatPayV3Utils {
+
+    private CloseableHttpClient httpClient;
+    private Verifier verifier;
+    private PrivateKey merchantPrivateKey;
+    private Gson gson = new Gson();
+
+
+    // 初始化证书和客户端
+    @PostConstruct
+    public void init() {
+        try {
+            // 加载商户私钥
+            setMerchantPrivateKey();
+            // 初始化验证器
+            setVerifier();
+            // 初始化HTTP客户端
+            setHttpClient();
+            log.info("微信支付初始化成功");
+        } catch (Exception e) {
+            log.error("微信支付初始化失败", e);
+        }
+    }
+
+    /**
+     * 加载商户私钥
+     */
+    private void setMerchantPrivateKey() {
+        try {
+            // 如果WECHAT_MCH_PRIVATE_KEY是文件路径
+            if (StringUtils.isNotEmpty(WechatConstants.WECHAT_MCH_PRIVATE_KEY)) {
+                // 如果是文件路径,从文件加载
+                if (WechatConstants.WECHAT_MCH_PRIVATE_KEY.endsWith(".pem")) {
+                    InputStream inputStream = new FileInputStream(WechatConstants.WECHAT_MCH_PRIVATE_KEY);
+                    this.merchantPrivateKey = PemUtil.loadPrivateKey(inputStream);
+                } else {
+                    // 如果是私钥内容字符串,直接从字符串加载
+                    this.merchantPrivateKey = PemUtil.loadPrivateKey(WechatConstants.WECHAT_MCH_PRIVATE_KEY);
+                }
+            } else {
+                log.warn("微信商户私钥未配置");
+            }
+        } catch (Exception e) {
+            log.error("加载商户私钥失败", e);
+            throw new RuntimeException("加载商户私钥失败", e);
+        }
+    }
+
+    /**
+     * 获取微信证书
+     *
+     * @throws Exception
+     */
+    private void setVerifier() throws Exception {
+        if (merchantPrivateKey == null) {
+            setMerchantPrivateKey();
+        }
+        // 获取证书管理器实例
+        CertificatesManager certificatesManager = CertificatesManager.getInstance();
+        // 向证书管理器增加需要自动更新平台证书的商户信息
+        certificatesManager.putMerchant(WechatConstants.WECHAT_MCH_ID,
+                new WechatPay2Credentials(WechatConstants.WECHAT_MCH_ID, new PrivateKeySigner(WechatConstants.WECHAT_MCH_SERIAL_NUM, merchantPrivateKey)),
+                WechatConstants.WECHAT_MCH_SECRET_V3.getBytes(StandardCharsets.UTF_8));
+        // ... 若有多个商户号,可继续调用putMerchant添加商户信息
+
+        // 从证书管理器中获取verifier
+        verifier = certificatesManager.getVerifier(WechatConstants.WECHAT_MCH_ID);
+    }
+
+    /**
+     * 创建请求客户端
+     *
+     * @throws Exception
+     */
+    private void setHttpClient() throws Exception {
+        if (verifier == null) {
+            setVerifier();
+        }
+        WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
+                .withMerchant(WechatConstants.WECHAT_MCH_ID, WechatConstants.WECHAT_MCH_SERIAL_NUM, merchantPrivateKey)
+                .withValidator(response -> true);
+        // ... 接下来,你仍然可以通过builder设置各种参数,来配置你的HttpClient
+
+        httpClient = builder.build();
+    }
+
+    /**
+     * 发送POST请求
+     *
+     * @param url    请求地址
+     * @param params json参数
+     * @return
+     */
+    public JsonObject sendPost(String url, JsonObject params) {
+        try {
+            if (httpClient == null) {
+                setHttpClient();
+            }
+            HttpPost httpPost = new HttpPost(url);
+            httpPost.addHeader("Accept", "application/json");
+            httpPost.addHeader("Content-type", "application/json; charset=utf-8");
+            httpPost.setEntity(new StringEntity(gson.toJson(params), StandardCharsets.UTF_8));
+            CloseableHttpResponse response = httpClient.execute(httpPost);
+            String bodyAsString = EntityUtils.toString(response.getEntity());
+            log.info("微信返回的内容:" + bodyAsString);
+            if (StringUtils.isEmpty(bodyAsString)) {
+                return null;
+            }
+            return gson.fromJson(bodyAsString, JsonObject.class);
+        } catch (Exception e) {
+            log.error("微信支付V3请求失败");
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+    /**
+     * 发送get请求
+     *
+     * @param url 请求地址 参数直接在地址上拼接
+     * @return
+     */
+    public JsonObject sendGet(String url) {
+        try {
+            if (httpClient == null) {
+                setHttpClient();
+            }
+            URIBuilder uriBuilder = new URIBuilder(url);
+            HttpGet httpGet = new HttpGet(uriBuilder.build());
+            httpGet.addHeader("Accept", "application/json");
+            CloseableHttpResponse response = httpClient.execute(httpGet);
+            String bodyAsString = EntityUtils.toString(response.getEntity());
+            log.info("微信返回的内容:" + bodyAsString);
+            if (StringUtils.isEmpty(bodyAsString)) {
+                return null;
+            }
+            return gson.fromJson(bodyAsString, JsonObject.class);
+        } catch (Exception e) {
+            log.error("微信支付V3请求失败");
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+    /**
+     * 回调通知验签与解密
+     *
+     * @param request
+     * @return
+     */
+    public JsonObject getCallbackData(HttpServletRequest request) {
+        try {
+            if (verifier == null) {
+                setVerifier();
+            }
+            String wechatPaySerial = request.getHeader(WechatPayHttpHeaders.WECHAT_PAY_SERIAL);
+            String nonce = request.getHeader(WechatPayHttpHeaders.WECHAT_PAY_NONCE);
+            String timestamp = request.getHeader(WechatPayHttpHeaders.WECHAT_PAY_TIMESTAMP);
+            String signature = request.getHeader(WechatPayHttpHeaders.WECHAT_PAY_SIGNATURE);
+            String body;
+            BufferedReader reader = request.getReader();
+            String line ;
+            StringBuilder inputString = new StringBuilder();
+            while ((line = reader.readLine()) != null) {
+                inputString.append(line);
+            }
+            body = inputString.toString();
+            log.info("body数据:"+body);
+            reader.close();
+            // 构建request,传入必要参数
+            NotificationRequest res = new NotificationRequest.Builder().withSerialNumber(wechatPaySerial)
+                    .withNonce(nonce)
+                    .withTimestamp(timestamp)
+                    .withSignature(signature)
+                    .withBody(body)
+                    .build();
+            NotificationHandler handler = new NotificationHandler(verifier, WechatConstants.WECHAT_MCH_SECRET_V3.getBytes(StandardCharsets.UTF_8));
+            // 验签和解析请求体
+            Notification notification = handler.parse(res);
+            log.info("回调通知数据:" + notification.toString());
+            // 解析开数据
+            String decryptData = notification.getDecryptData();
+            log.info("回调解析数据:" + decryptData);
+            if (StringUtils.isEmpty(decryptData)) {
+                return null;
+            }
+            return gson.fromJson(decryptData, JsonObject.class);
+        } catch (Exception e) {
+            log.error("微信支付V3回调失败");
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+    /**
+     * 微信支付v3签名 RSA签名
+     *
+     * @param message 要签名的字符串
+     * @return
+     */
+    public String signRSA(String message) {
+        try {
+            if (merchantPrivateKey == null) {
+                setMerchantPrivateKey();
+            }
+            Signer signer = new PrivateKeySigner(WechatConstants.WECHAT_MCH_SERIAL_NUM, merchantPrivateKey);
+            Signer.SignatureResult signature = signer.sign(message.getBytes(StandardCharsets.UTF_8));
+            return signature.getSign();
+        } catch (Exception e) {
+            e.printStackTrace();
+            return "";
+        }
+    }
+}

+ 40 - 0
src/main/java/com/zsElectric/boot/core/pay/WechatUrlConstants.java

@@ -0,0 +1,40 @@
+package com.zsElectric.boot.core.pay;
+
+/**
+ * @author wangzhiqiang
+ * @Date 2022/12/22
+ * @Desc 微信相关接口请求地址 其中最后四个通知接口地址,是自己服务器的接口地址,必须为外网可访问的url,不能携带参数。 公网域名必须为https,如果是走专线接入,使用专线NAT IP或者私有回调域名可使用http。
+ */
+public interface WechatUrlConstants {
+
+    //小程序code获取openid
+    String CODE_2_SESSION = "https://api.weixin.qq.com/sns/jscode2session";
+
+    //微信支付v3 jsapi下单
+    String PAY_V3_JSAPI = "https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi";
+
+    //微信支付v3 H5下单
+    String PAY_V3_H5 = "https://api.mch.weixin.qq.com/v3/pay/transactions/h5";
+
+    //微信支付v3 申请退款
+    String PAY_V3_REFUND = "https://api.mch.weixin.qq.com/v3/refund/domestic/refunds";
+
+    //微信支付v3 通过商户订单号查询订单
+    String PAY_V3_QUERY_OUT = "https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/%s?mchid=%s";
+
+    //微信支付v3 查询单笔退款
+    String PAY_V3_QUERY_REFUND = "https://api.mch.weixin.qq.com/v3/refund/domestic/refunds/%s";
+
+    //微信支付v2 支付通知接口地址
+    String PAY_V2_NOTIFY = "https://xxx.com/api/wechatPay/wechatPayNotify";
+
+    //微信支付v2 退款通知接口地址
+    String PAY_V2_REFUND_NOTIFY = "https://xxx.com/api/wechatPay/wechatRefundNotify";
+
+    //微信支付v3 支付通知接口地址
+    String PAY_V3_NOTIFY = "https://barcodeapp.gzspy.org.cn/applet/wechatPayNotify";
+
+    //微信支付v3 退款通知接口地址
+    String PAY_V3_REFUND_NOTIFY = "https://xxx.com/api/wechatPay/v3/wechatRefundNotify";
+
+}

+ 21 - 1
src/main/java/com/zsElectric/boot/platform/codegen/service/impl/GenConfigServiceImpl.java

@@ -182,6 +182,15 @@ public class GenConfigServiceImpl extends ServiceImpl<GenConfigMapper, GenConfig
     @Override
     public void saveGenConfig(GenConfigForm formData) {
         GenConfig genConfig = codegenConverter.toGenConfig(formData);
+        
+        // 检查是否存在相同的tableName记录,如果存在则使用现有记录的ID避免违反唯一约束
+        GenConfig existingConfig = this.getOne(new LambdaQueryWrapper<GenConfig>()
+                .eq(GenConfig::getTableName, genConfig.getTableName()));
+        if (existingConfig != null) {
+            // 如果存在,设置ID以便更新现有记录而不是插入新记录
+            genConfig.setId(existingConfig.getId());
+        }
+        
         this.saveOrUpdate(genConfig);
 
         // 如果选择上级菜单且当前环境不是生产环境,则保存菜单
@@ -195,10 +204,21 @@ public class GenConfigServiceImpl extends ServiceImpl<GenConfigMapper, GenConfig
         if (CollectionUtil.isEmpty(genFieldConfigs)) {
             throw new BusinessException("字段配置不能为空");
         }
+        
+        // 为每个字段配置设置configId
         genFieldConfigs.forEach(genFieldConfig -> {
             genFieldConfig.setConfigId(genConfig.getId());
         });
-        genFieldConfigService.saveOrUpdateBatch(genFieldConfigs);
+        
+        // 如果是更新现有配置,需要先删除旧的字段配置再插入新的
+        if (existingConfig != null) {
+            // 删除现有的字段配置
+            genFieldConfigService.remove(new LambdaQueryWrapper<GenFieldConfig>()
+                    .eq(GenFieldConfig::getConfigId, existingConfig.getId()));
+        }
+        
+        // 批量保存字段配置
+        genFieldConfigService.saveBatch(genFieldConfigs);
     }
 
     /**

+ 12 - 3
src/main/java/com/zsElectric/boot/system/service/UserService.java

@@ -188,9 +188,18 @@ public interface UserService extends IService<User> {
 
     /**
      * 根据手机号和OpenID注册用户
-     *
-     * @param mobile 手机号
-     * @param openId 微信OpenID
+     * <p>
+     * 支持两种场景:
+     * 1. 仅通过手机号注册:openId 可为 null(如:手机号Code登录)
+     * 2. 通过手机号和openId注册:同时绑定微信账号
+     * <p>
+     * 如果用户已存在:
+     * - 未绑定openId且提供了openId:绑定openId
+     * - 已绑定不同openId且提供了新openId:更新openId
+     * - 其他情况:直接返回成功
+     *
+     * @param mobile 手机号(必填)
+     * @param openId 微信OpenID(可选,可为 null)
      * @return 是否成功
      */
     boolean registerUserByMobileAndOpenId(String mobile, String openId);

+ 46 - 29
src/main/java/com/zsElectric/boot/system/service/impl/UserServiceImpl.java

@@ -45,6 +45,7 @@ import org.springframework.security.crypto.password.PasswordEncoder;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
+import java.math.BigDecimal;
 import java.time.LocalDateTime;
 import java.util.*;
 import java.util.concurrent.TimeUnit;
@@ -80,6 +81,8 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
     private final DictItemService dictItemService;
 
     private final UserConverter userConverter;
+    
+    private final com.zsElectric.boot.business.service.UserInfoService userInfoService;
 
     /**
      * 获取用户分页列表
@@ -311,45 +314,59 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
     @Override
     @Transactional(rollbackFor = Exception.class)
     public boolean registerUserByMobileAndOpenId(String mobile, String openId) {
-        if (StrUtil.isBlank(mobile) || StrUtil.isBlank(openId)) {
+        // 修改验证逻辑:允许 openId 为 null,支持仅通过手机号注册
+        if (StrUtil.isBlank(mobile)) {
+            log.warn("注册用户失败:手机号为空");
             return false;
         }
 
-        // 先查询是否已存在手机号对应的用户
-        User existingUser = this.getOne(
-                new LambdaQueryWrapper<User>()
-                        .eq(User::getMobile, mobile)
+        // 先查询 c_user_info 表中是否已存在手机号对应的用户
+        com.zsElectric.boot.business.model.entity.UserInfo existingUserInfo = userInfoService.getOne(
+                new LambdaQueryWrapper<com.zsElectric.boot.business.model.entity.UserInfo>()
+                        .eq(com.zsElectric.boot.business.model.entity.UserInfo::getPhone, mobile)
         );
 
-        if (existingUser != null) {
-            // 如果存在用户但没绑定openId,则绑定openId
-            if (StrUtil.isBlank(existingUser.getOpenid())) {
-                return bindUserOpenId(existingUser.getId(), openId);
+        if (existingUserInfo != null) {
+            log.info("手机号 {} 对应的UserInfo已存在,ID: {}", mobile, existingUserInfo.getId());
+            // 如果存在用户但没绑定openId,且提供了openId,则绑定openId
+            if (StrUtil.isNotBlank(openId) && StrUtil.isBlank(existingUserInfo.getWechat())) {
+                log.info("为UserInfo {} 绑定 OpenID", existingUserInfo.getId());
+                existingUserInfo.setWechat(openId);
+                existingUserInfo.setUpdateTime(LocalDateTime.now());
+                return userInfoService.updateById(existingUserInfo);
             }
-            // 如果已经绑定了其他openId,则判断是否需要更新
-            else if (!openId.equals(existingUser.getOpenid())) {
-                return bindUserOpenId(existingUser.getId(), openId);
+            // 如果提供了不同的openId,则更新
+            else if (StrUtil.isNotBlank(openId) && !openId.equals(existingUserInfo.getWechat())) {
+                log.info("更新UserInfo {} 的 OpenID", existingUserInfo.getId());
+                existingUserInfo.setWechat(openId);
+                existingUserInfo.setUpdateTime(LocalDateTime.now());
+                return userInfoService.updateById(existingUserInfo);
             }
-            // 如果已经绑定了相同的openId,则不需要任何操作
+            // 如果已经绑定了相同的openId或未提供openId,则不需要任何操作
             return true;
         }
 
-        // 不存在用户,创建新用户
-        User newUser = new User();
-        newUser.setMobile(mobile);
-        newUser.setOpenid(openId);
-        newUser.setUsername(mobile); // 使用手机号作为用户名
-        newUser.setNickname("微信用户_" + mobile.substring(mobile.length() - 4)); // 使用手机号后4位作为昵称
-        newUser.setPassword(SystemConstants.DEFAULT_PASSWORD); // 使用加密的openId作为初始密码
-        newUser.setGender(0); // 保密
-        newUser.setCreateTime(LocalDateTime.now());
-        newUser.setUpdateTime(LocalDateTime.now());
-        this.save(newUser);
-        // 为了默认系统管理员角色,这里按需调整,实际情况绑定已存在的系统用户,另一种情况是给默认游客角色,然后由系统管理员设置用户的角色
-        UserRole userRole = new UserRole();
-        userRole.setUserId(newUser.getId());
-        userRole.setRoleId(1L);  // TODO 系统管理员
-        userRoleService.save(userRole);
+        // 不存在用户,创建新的 UserInfo
+        log.info("创建新UserInfo,手机号: {}, OpenID: {}", mobile, openId);
+        com.zsElectric.boot.business.model.entity.UserInfo newUserInfo = new com.zsElectric.boot.business.model.entity.UserInfo();
+        newUserInfo.setPhone(mobile);
+        newUserInfo.setWechat(openId); // openId 可以为 null
+        newUserInfo.setNickName("微信用户_" + mobile.substring(mobile.length() - 4)); // 使用手机号后4位作为昵称
+        newUserInfo.setIntegralNum(BigDecimal.ZERO); // 初始积分为0
+        newUserInfo.setCreateTime(LocalDateTime.now());
+        newUserInfo.setUpdateTime(LocalDateTime.now());
+        
+        boolean saved = userInfoService.save(newUserInfo);
+        if (!saved) {
+            log.error("保存UserInfo失败,手机号: {}", mobile);
+            return false;
+        }
+        
+        log.info("UserInfo创建成功,ID: {}, 手机号: {}", newUserInfo.getId(), mobile);
+        
+        // 注意:UserInfo 不需要分配角色,因为它是业务用户表,不是系统用户表
+        // 如果需要系统登录权限,应该额外创建 User 记录并分配角色
+        
         return true;
     }
 

+ 6 - 4
src/main/resources/application-dev.yml

@@ -102,7 +102,7 @@ security:
     - /api/v1/auth/captcha # 验证码获取接口
     - /api/v1/auth/refresh-token # 刷新令牌接口
     - /api/v1/auth/logout # 开放退出登录
-    - /api/v1/auth/wx/miniapp/code-login # 微信小程序code登陆
+    - /api/v1/auth/wx/** # 微信相关所有接口
     - /ws/** # WebSocket接口
 
   # 非安全端点路径,完全绕过 Spring Security 的安全控制
@@ -273,11 +273,13 @@ captcha:
   # 验证码有效期(秒)
   expire-seconds: 120
 
-# 微信小程配置
+# 微信小程配置
 wx:
   miniapp:
-    app-id: xxxxxx
-    app-secret: xxxxxx
+    # 微信小程序 AppID(在微信公众平台获取)
+    app-id: wx9894a01b9e92c368
+    # 微信小程序 AppSecret(在微信公众平台获取)
+    app-secret: b1e83dbcf83af310c38c0a138739ddcf
 
 # ==================== AI 命令系统配置 ====================
 ai:

+ 8 - 5
src/main/resources/application-prod.yml

@@ -85,12 +85,13 @@ security:
       secret-key: SecretKey012345678901234567890123456789012345678901234567890123456789 # JWT密钥(HS256算法至少32字符)
     redis-token:
       allow-multi-login: true # 是否允许多设备登录
-  # 安全白名单路径,仅跳过 AuthorizationFilter 过滤器还是会走 Spring Security 的其他过滤器(CSRF、CORS等)
+  # 安全白名单路径,仅跳过 AuthorizationFilter 过滤器,还是会走 Spring Security 的其他过滤器(CSRF、CORS等)
   ignore-urls:
     - /api/v1/auth/login/**       # 登录接口(账号密码登录、手机验证码登录和微信登录)
     - /api/v1/auth/captcha        # 验证码获取接口
     - /api/v1/auth/refresh-token  # 刷新令牌接口
-    - //api/v1/auth/wx/miniapp/code-login # 微信小程序code登陆
+    - /api/v1/auth/logout         # 开放退出登录
+    - /api/v1/auth/wx/**          # 微信相关所有接口(包括code登录、手机号登录、手机号Code登录)
     - /ws/**                      # WebSocket接口
 #    - /dev/v1/linkData/** #互联互通
   # 非安全端点路径,完全绕过 Spring Security 的过滤器
@@ -231,11 +232,13 @@ captcha:
   # 验证码有效期(秒)
   expire-seconds: 120
 
-# 微信小程配置
+# 微信小程配置
 wx:
   miniapp:
-    app-id: xxxxxx
-    app-secret: xxxxxx
+    # 微信小程序 AppID(在微信公众平台获取)
+    app-id: wx9894a01b9e92c368
+    # 微信小程序 AppSecret(在微信公众平台获取)
+    app-secret: b1e83dbcf83af310c38c0a138739ddcf
 
 
 # ==================== AI 命令系统配置 ====================