| @ -0,0 +1,144 @@ | |||
| # 小程序登录拦截器分离配置说明 | |||
| ## 配置目标 | |||
| 将系统的登录拦截器分成后台管理系统和小程序两个独立的拦截器,实现不同的认证策略。 | |||
| ## 实现方案 | |||
| ### 1. 创建小程序专用拦截器 | |||
| #### 1.1 新增文件 | |||
| - `jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/filters/AppletJwtFilter.java` | |||
| #### 1.2 核心特性 | |||
| ```java | |||
| @Override | |||
| protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { | |||
| try { | |||
| HttpServletRequest httpRequest = (HttpServletRequest) request; | |||
| String requestPath = httpRequest.getServletPath(); | |||
| // 只处理以/applet开头的请求 | |||
| if (!requestPath.startsWith("/applet")) { | |||
| return true; // 不是applet请求,直接放行 | |||
| } | |||
| // 判断当前路径是不是注解了@IngoreAuth路径,如果是,则放开验证 | |||
| if (InMemoryIgnoreAuth.contains(requestPath)) { | |||
| return true; | |||
| } | |||
| executeLogin(request, response); | |||
| return true; | |||
| } catch (Exception e) { | |||
| JwtUtil.responseError(response,401,CommonConstant.TOKEN_IS_INVALID_MSG); | |||
| return false; | |||
| } | |||
| } | |||
| ``` | |||
| ### 2. 修改Shiro配置 | |||
| #### 2.1 修改文件 | |||
| - `jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/ShiroConfig.java` | |||
| #### 2.2 配置变更 | |||
| ```java | |||
| // 添加自己的过滤器并且取名为jwt和applet | |||
| Map<String, Filter> filterMap = new HashMap<String, Filter>(2); | |||
| //如果cloudServer为空 则说明是单体 需要加载跨域配置【微服务跨域切换】 | |||
| Object cloudServer = env.getProperty(CommonConstant.CLOUD_SERVER_KEY); | |||
| filterMap.put("jwt", new JwtFilter(cloudServer==null)); | |||
| filterMap.put("applet", new AppletJwtFilter(cloudServer==null)); | |||
| shiroFilterFactoryBean.setFilters(filterMap); | |||
| // <!-- 过滤链定义,从上向下顺序执行 | |||
| // applet专用过滤器,只处理/applet开头的请求 | |||
| filterChainDefinitionMap.put("/applet/**", "applet"); | |||
| // 其他请求使用jwt过滤器 | |||
| filterChainDefinitionMap.put("/**", "jwt"); | |||
| ``` | |||
| ## 拦截器分离效果 | |||
| ### 1. 后台管理系统拦截器 (JwtFilter) | |||
| - **作用范围**:除了`/applet/**`之外的所有请求 | |||
| - **认证策略**:标准的JWT Token认证 | |||
| - **适用场景**:后台管理系统的所有接口 | |||
| ### 2. 小程序拦截器 (AppletJwtFilter) | |||
| - **作用范围**:只处理`/applet/**`开头的请求 | |||
| - **认证策略**: | |||
| - 支持`@IgnoreAuth`注解的免登录接口 | |||
| - 其他接口需要JWT Token认证 | |||
| - **适用场景**:小程序相关的所有接口 | |||
| ## 配置优势 | |||
| ### 1. 独立认证策略 | |||
| - 后台和小程序可以使用不同的认证方式 | |||
| - 可以为小程序配置更宽松的认证策略 | |||
| - 支持不同的Token格式和验证逻辑 | |||
| ### 2. 更好的安全性 | |||
| - 小程序接口与后台接口完全隔离 | |||
| - 可以针对小程序特点定制安全策略 | |||
| - 减少攻击面 | |||
| ### 3. 便于维护 | |||
| - 小程序相关的认证逻辑独立管理 | |||
| - 可以独立升级和配置 | |||
| - 便于调试和问题排查 | |||
| ## 使用示例 | |||
| ### 1. 小程序免登录接口 | |||
| ```java | |||
| @GetMapping("/health") | |||
| @IgnoreAuth | |||
| @Operation(summary = "健康检查", description = "检查健康管理小程序模块运行状态") | |||
| public Result<String> health() { | |||
| return Result.OK("健康管理小程序模块运行正常"); | |||
| } | |||
| ``` | |||
| ### 2. 小程序需要登录的接口 | |||
| ```java | |||
| @GetMapping("/user/info") | |||
| @Operation(summary = "获取用户信息", description = "获取小程序用户信息") | |||
| public Result<UserInfo> getUserInfo() { | |||
| // 需要登录才能访问 | |||
| return Result.OK(userInfo); | |||
| } | |||
| ``` | |||
| ### 3. 后台管理接口 | |||
| ```java | |||
| @GetMapping("/sys/user/list") | |||
| @Operation(summary = "用户列表", description = "获取后台用户列表") | |||
| public Result<IPage<SysUser>> getUserList() { | |||
| // 使用标准的后台认证 | |||
| return Result.OK(userList); | |||
| } | |||
| ``` | |||
| ## 注意事项 | |||
| 1. **路径匹配顺序**:`/applet/**` 必须在 `/**` 之前配置 | |||
| 2. **跨域支持**:两个拦截器都支持跨域配置 | |||
| 3. **多租户支持**:都支持多租户的TenantContext | |||
| 4. **异步支持**:如果需要异步支持,需要在FilterRegistrationBean中配置 | |||
| ## 验证方法 | |||
| 1. **小程序接口**: | |||
| - 访问 `/applet/health/health` 应该能正常访问(有@IgnoreAuth注解) | |||
| - 访问 `/applet/user/info` 需要Token认证 | |||
| 2. **后台接口**: | |||
| - 访问 `/sys/user/list` 需要Token认证 | |||
| - 访问 `/sys/login` 不需要认证(在anon列表中) | |||
| 3. **Swagger文档**: | |||
| - 只显示 `/applet` 开头的接口 | |||
| - 文档标题已更新为"小程序API接口文档" | |||
| @ -0,0 +1,216 @@ | |||
| # 小程序和后台不同Token配置说明 | |||
| ## 配置目标 | |||
| 为小程序和后台管理系统配置不同的Token解析对象,实现独立的认证体系。 | |||
| ## 实现方案 | |||
| ### 1. 创建小程序专用Token类 | |||
| #### 1.1 新增文件 | |||
| - `jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/AppletJwtToken.java` | |||
| #### 1.2 核心特性 | |||
| ```java | |||
| public class AppletJwtToken implements AuthenticationToken { | |||
| private String token; | |||
| public AppletJwtToken(String token) { | |||
| this.token = token; | |||
| } | |||
| @Override | |||
| public Object getPrincipal() { | |||
| return token; | |||
| } | |||
| @Override | |||
| public Object getCredentials() { | |||
| return token; | |||
| } | |||
| } | |||
| ``` | |||
| ### 2. 创建小程序专用Realm | |||
| #### 2.1 新增文件 | |||
| - `jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/AppletShiroRealm.java` | |||
| #### 2.2 核心特性 | |||
| ```java | |||
| @Component | |||
| @Slf4j | |||
| public class AppletShiroRealm extends AuthorizingRealm { | |||
| @Override | |||
| public boolean supports(AuthenticationToken token) { | |||
| return token instanceof AppletJwtToken; | |||
| } | |||
| @Override | |||
| protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException { | |||
| // 小程序专用的身份认证逻辑 | |||
| String token = (String) auth.getCredentials(); | |||
| LoginUser loginUser = this.checkAppletUserTokenIsEffect(token); | |||
| return new SimpleAuthenticationInfo(loginUser, token, getName()); | |||
| } | |||
| @Override | |||
| protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { | |||
| // 小程序专用的权限认证逻辑 | |||
| // 可以配置不同的角色和权限体系 | |||
| return info; | |||
| } | |||
| } | |||
| ``` | |||
| ### 3. 修改小程序拦截器 | |||
| #### 3.1 修改文件 | |||
| - `jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/AppletJwtFilter.java` | |||
| #### 3.2 配置变更 | |||
| ```java | |||
| @Slf4j | |||
| public class AppletJwtFilter extends BasicHttpAuthenticationFilter { | |||
| @Resource | |||
| private AppletShiroRealm appletRealm; | |||
| @Override | |||
| protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception { | |||
| // 使用小程序专用的Token和Realm | |||
| AppletJwtToken jwtToken = new AppletJwtToken(token); | |||
| getSubject(request, response).login(jwtToken); | |||
| return true; | |||
| } | |||
| } | |||
| ``` | |||
| ### 4. 配置独立的SecurityManager | |||
| #### 4.1 修改文件 | |||
| - `jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/ShiroConfig.java` | |||
| #### 4.2 配置变更 | |||
| ```java | |||
| @Bean("appletSecurityManager") | |||
| public DefaultWebSecurityManager appletSecurityManager(AppletShiroRealm appletRealm) { | |||
| DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); | |||
| securityManager.setRealm(appletRealm); | |||
| // 配置小程序专用的SecurityManager | |||
| return securityManager; | |||
| } | |||
| ``` | |||
| ## Token解析对象分离效果 | |||
| ### 1. 后台管理系统Token (JwtToken) | |||
| - **Token类**:`JwtToken` | |||
| - **Realm**:`ShiroRealm` | |||
| - **SecurityManager**:`securityManager` | |||
| - **适用场景**:后台管理系统的所有接口 | |||
| ### 2. 小程序Token (AppletJwtToken) | |||
| - **Token类**:`AppletJwtToken` | |||
| - **Realm**:`AppletShiroRealm` | |||
| - **SecurityManager**:`appletSecurityManager` | |||
| - **适用场景**:小程序相关的所有接口 | |||
| ## 配置优势 | |||
| ### 1. 独立认证体系 | |||
| - 后台和小程序使用不同的Token类 | |||
| - 可以配置不同的认证逻辑 | |||
| - 支持不同的用户体系和权限模型 | |||
| ### 2. 更好的安全性 | |||
| - 小程序和后台的认证完全隔离 | |||
| - 可以针对小程序特点定制安全策略 | |||
| - 减少攻击面和风险 | |||
| ### 3. 便于维护和扩展 | |||
| - 小程序相关的认证逻辑独立管理 | |||
| - 可以独立升级和配置 | |||
| - 便于调试和问题排查 | |||
| ## 使用示例 | |||
| ### 1. 小程序接口(使用AppletJwtToken) | |||
| ```java | |||
| @GetMapping("/user/info") | |||
| @Operation(summary = "获取小程序用户信息") | |||
| public Result<UserInfo> getAppletUserInfo() { | |||
| // 使用AppletJwtToken进行认证 | |||
| // 通过AppletShiroRealm进行权限验证 | |||
| return Result.OK(userInfo); | |||
| } | |||
| ``` | |||
| ### 2. 后台接口(使用JwtToken) | |||
| ```java | |||
| @GetMapping("/sys/user/list") | |||
| @Operation(summary = "获取后台用户列表") | |||
| public Result<IPage<SysUser>> getBackendUserList() { | |||
| // 使用JwtToken进行认证 | |||
| // 通过ShiroRealm进行权限验证 | |||
| return Result.OK(userList); | |||
| } | |||
| ``` | |||
| ### 3. 小程序免登录接口 | |||
| ```java | |||
| @GetMapping("/health") | |||
| @IgnoreAuth | |||
| @Operation(summary = "健康检查") | |||
| public Result<String> health() { | |||
| // 使用@IgnoreAuth注解,跳过认证 | |||
| return Result.OK("健康管理小程序模块运行正常"); | |||
| } | |||
| ``` | |||
| ## 拦截器配置 | |||
| ### 1. 路径匹配规则 | |||
| ```java | |||
| // applet专用过滤器,只处理/applet开头的请求 | |||
| filterChainDefinitionMap.put("/applet/**", "applet"); | |||
| // 其他请求使用jwt过滤器 | |||
| filterChainDefinitionMap.put("/**", "jwt"); | |||
| ``` | |||
| ### 2. 过滤器配置 | |||
| ```java | |||
| Map<String, Filter> filterMap = new HashMap<String, Filter>(2); | |||
| filterMap.put("jwt", new JwtFilter(cloudServer==null)); | |||
| filterMap.put("applet", new AppletJwtFilter(cloudServer==null)); | |||
| ``` | |||
| ## 注意事项 | |||
| 1. **Token类型隔离**:后台使用`JwtToken`,小程序使用`AppletJwtToken` | |||
| 2. **Realm隔离**:后台使用`ShiroRealm`,小程序使用`AppletShiroRealm` | |||
| 3. **路径匹配顺序**:`/applet/**` 必须在 `/**` 之前配置 | |||
| 4. **跨域支持**:两个拦截器都支持跨域配置 | |||
| 5. **多租户支持**:都支持多租户的TenantContext | |||
| ## 验证方法 | |||
| 1. **小程序接口**: | |||
| - 访问 `/applet/health/health` 应该能正常访问(有@IgnoreAuth注解) | |||
| - 访问 `/applet/user/info` 需要AppletJwtToken认证 | |||
| 2. **后台接口**: | |||
| - 访问 `/sys/user/list` 需要JwtToken认证 | |||
| - 访问 `/sys/login` 不需要认证(在anon列表中) | |||
| 3. **Token隔离**: | |||
| - 后台Token无法用于小程序接口 | |||
| - 小程序Token无法用于后台接口 | |||
| ## 扩展建议 | |||
| 1. **不同的Token格式**:可以为小程序和后台配置不同的JWT签名算法 | |||
| 2. **不同的权限模型**:可以为小程序配置更简单的权限体系 | |||
| 3. **不同的缓存策略**:可以为小程序配置不同的Token缓存时间 | |||
| 4. **不同的用户体系**:可以为小程序配置独立的用户表和管理逻辑 | |||
| @ -0,0 +1,234 @@ | |||
| # 小程序用户实体类配置说明 | |||
| ## 配置目标 | |||
| 完善小程序用户实体类,直接使用AppletUser作为登录对象,实现与后台用户体系的完全分离。 | |||
| ## 实现方案 | |||
| ### 1. 完善AppletUser实体类 | |||
| #### 1.1 修改文件 | |||
| - `jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/appletBackground/appletUser/entity/AppletUser.java` | |||
| #### 1.2 新增字段 | |||
| ```java | |||
| // 基础信息 | |||
| private String username; // 用户名 | |||
| private String password; // 密码 | |||
| private String realname; // 真实姓名 | |||
| private Date birthday; // 生日 | |||
| private Integer sex; // 性别(1:男 2:女) | |||
| private String email; // 邮箱 | |||
| private Integer status; // 状态(1:正常 2:冻结) | |||
| private Integer delFlag; // 删除标志(0代表存在 1代表删除) | |||
| private Integer userIdentity; // 用户身份(1 普通用户 2 VIP用户) | |||
| // 健康信息 | |||
| private BigDecimal height; // 身高(cm) | |||
| private BigDecimal weight; // 体重(kg) | |||
| private Integer age; // 年龄 | |||
| // 联系信息 | |||
| private String address; // 地址 | |||
| private String emergencyContact; // 紧急联系人 | |||
| private String emergencyPhone; // 紧急联系人电话 | |||
| // 会员信息 | |||
| private String memberLevel; // 会员等级 | |||
| private Integer points; // 积分 | |||
| // 系统信息 | |||
| private Date lastLoginTime; // 最后登录时间 | |||
| private String deviceId; // 设备ID | |||
| private String loginIp; // 登录IP | |||
| private String remark; // 备注 | |||
| ``` | |||
| ### 2. 直接使用AppletUser作为登录对象 | |||
| #### 2.1 配置说明 | |||
| - 直接使用`AppletUser`实体类作为登录对象 | |||
| - 不需要额外的VO类转换 | |||
| - 简化了代码结构,减少了维护成本 | |||
| #### 2.2 核心特性 | |||
| ```java | |||
| @Data | |||
| @TableName("applet_user") | |||
| @Accessors(chain = true) | |||
| @EqualsAndHashCode(callSuper = false) | |||
| @Schema(description="小程序用户") | |||
| public class AppletUser implements Serializable { | |||
| // 基础信息 | |||
| private String id; // 用户ID | |||
| private String username; // 用户名 | |||
| private String realname; // 真实姓名 | |||
| private String name; // 昵称 | |||
| private String password; // 密码 | |||
| private String openid; // 第三方认证id | |||
| private String avatar; // 头像 | |||
| // 健康信息 | |||
| private BigDecimal bmi; // 体总指数 | |||
| private BigDecimal fat; // 脂肪 | |||
| private BigDecimal height; // 身高 | |||
| private BigDecimal weight; // 体重 | |||
| private Integer age; // 年龄 | |||
| // 其他信息 | |||
| private Integer userIdentity; // 用户身份 | |||
| private String memberLevel; // 会员等级 | |||
| private Integer points; // 积分 | |||
| private String deviceId; // 设备ID | |||
| // ... 其他字段 | |||
| } | |||
| ``` | |||
| ### 3. 修改小程序Realm | |||
| #### 3.1 修改文件 | |||
| - `jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/AppletShiroRealm.java` | |||
| #### 3.2 配置变更 | |||
| ```java | |||
| @Override | |||
| protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { | |||
| if (principals != null) { | |||
| AppletUser appletUser = (AppletUser) principals.getPrimaryPrincipal(); | |||
| username = appletUser.getUsername(); | |||
| userId = appletUser.getId(); | |||
| } | |||
| // 小程序专用的权限认证逻辑 | |||
| return info; | |||
| } | |||
| @Override | |||
| protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException { | |||
| // 直接使用AppletUser进行身份认证 | |||
| AppletUser loginUser = this.checkAppletUserTokenIsEffect(token); | |||
| return new SimpleAuthenticationInfo(loginUser, token, getName()); | |||
| } | |||
| ``` | |||
| ## 用户体系分离效果 | |||
| ### 1. 后台管理系统用户 | |||
| - **实体类**:`SysUser` | |||
| - **登录对象**:`LoginUser` | |||
| - **Realm**:`ShiroRealm` | |||
| - **适用场景**:后台管理系统的所有用户 | |||
| ### 2. 小程序用户 | |||
| - **实体类**:`AppletUser` | |||
| - **登录对象**:`AppletUser`(直接使用实体类) | |||
| - **Realm**:`AppletShiroRealm` | |||
| - **适用场景**:小程序相关的所有用户 | |||
| ## 配置优势 | |||
| ### 1. 完全独立的用户体系 | |||
| - 小程序和后台使用不同的用户表 | |||
| - 可以配置不同的用户属性和业务逻辑 | |||
| - 支持不同的用户管理策略 | |||
| ### 2. 更好的业务适配 | |||
| - 小程序用户包含健康相关的字段(BMI、身高、体重等) | |||
| - 支持会员等级和积分系统 | |||
| - 包含紧急联系人等小程序特有功能 | |||
| ### 3. 便于维护和扩展 | |||
| - 小程序用户相关的逻辑独立管理 | |||
| - 可以独立升级和配置 | |||
| - 便于调试和问题排查 | |||
| ## 使用示例 | |||
| ### 1. 小程序用户注册 | |||
| ```java | |||
| @PostMapping("/register") | |||
| @Operation(summary = "小程序用户注册") | |||
| public Result<AppletUser> register(@RequestBody AppletUser appletUser) { | |||
| // 小程序用户注册逻辑 | |||
| return Result.OK(appletUser); | |||
| } | |||
| ``` | |||
| ### 2. 小程序用户登录 | |||
| ```java | |||
| @PostMapping("/login") | |||
| @Operation(summary = "小程序用户登录") | |||
| public Result<AppletUser> login(@RequestBody LoginRequest request) { | |||
| // 小程序用户登录逻辑 | |||
| AppletUser loginUser = appletUserService.login(request); | |||
| return Result.OK(loginUser); | |||
| } | |||
| ``` | |||
| ### 3. 获取小程序用户信息 | |||
| ```java | |||
| @GetMapping("/user/info") | |||
| @Operation(summary = "获取小程序用户信息") | |||
| public Result<AppletUser> getUserInfo() { | |||
| // 获取当前登录的小程序用户信息 | |||
| AppletUser user = (AppletUser) SecurityUtils.getSubject().getPrincipal(); | |||
| return Result.OK(user); | |||
| } | |||
| ``` | |||
| ## 数据库表结构 | |||
| ### applet_user表字段 | |||
| ```sql | |||
| CREATE TABLE `applet_user` ( | |||
| `id` varchar(32) NOT NULL COMMENT '主键', | |||
| `create_by` varchar(32) DEFAULT NULL COMMENT '创建人', | |||
| `create_time` datetime DEFAULT NULL COMMENT '创建日期', | |||
| `update_by` varchar(32) DEFAULT NULL COMMENT '更新人', | |||
| `update_time` datetime DEFAULT NULL COMMENT '更新日期', | |||
| `sys_org_code` varchar(64) DEFAULT NULL COMMENT '所属部门', | |||
| `name` varchar(100) DEFAULT NULL COMMENT '昵称', | |||
| `openid` varchar(100) DEFAULT NULL COMMENT '第三方认证id', | |||
| `phone` varchar(45) DEFAULT NULL COMMENT '手机号', | |||
| `bmi` decimal(10,2) DEFAULT NULL COMMENT '体总指数', | |||
| `fat` decimal(10,2) DEFAULT NULL COMMENT '脂肪', | |||
| `avatar` varchar(255) DEFAULT NULL COMMENT '头像', | |||
| `username` varchar(100) DEFAULT NULL COMMENT '用户名', | |||
| `password` varchar(255) DEFAULT NULL COMMENT '密码', | |||
| `realname` varchar(100) DEFAULT NULL COMMENT '真实姓名', | |||
| `birthday` date DEFAULT NULL COMMENT '生日', | |||
| `sex` int DEFAULT NULL COMMENT '性别(1:男 2:女)', | |||
| `email` varchar(45) DEFAULT NULL COMMENT '邮箱', | |||
| `status` int DEFAULT NULL COMMENT '状态(1:正常 2:冻结)', | |||
| `del_flag` int DEFAULT NULL COMMENT '删除标志(0代表存在 1代表删除)', | |||
| `user_identity` int DEFAULT NULL COMMENT '用户身份(1 普通用户 2 VIP用户)', | |||
| `height` decimal(10,2) DEFAULT NULL COMMENT '身高(cm)', | |||
| `weight` decimal(10,2) DEFAULT NULL COMMENT '体重(kg)', | |||
| `age` int DEFAULT NULL COMMENT '年龄', | |||
| `address` varchar(500) DEFAULT NULL COMMENT '地址', | |||
| `emergency_contact` varchar(100) DEFAULT NULL COMMENT '紧急联系人', | |||
| `emergency_phone` varchar(45) DEFAULT NULL COMMENT '紧急联系人电话', | |||
| `member_level` varchar(50) DEFAULT NULL COMMENT '会员等级', | |||
| `points` int DEFAULT NULL COMMENT '积分', | |||
| `last_login_time` datetime DEFAULT NULL COMMENT '最后登录时间', | |||
| `device_id` varchar(100) DEFAULT NULL COMMENT '设备ID', | |||
| `login_ip` varchar(100) DEFAULT NULL COMMENT '登录IP', | |||
| `remark` varchar(500) DEFAULT NULL COMMENT '备注', | |||
| PRIMARY KEY (`id`) | |||
| ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='小程序用户表'; | |||
| ``` | |||
| ## 注意事项 | |||
| 1. **用户体系隔离**:小程序和后台使用完全不同的用户表 | |||
| 2. **字段差异**:小程序用户包含健康相关的字段 | |||
| 3. **业务逻辑**:小程序用户支持会员等级和积分系统 | |||
| 4. **安全策略**:可以为小程序配置不同的安全策略 | |||
| 5. **缓存策略**:可以为小程序用户配置独立的缓存策略 | |||
| ## 后续开发建议 | |||
| 1. **实现getAppletUser方法**:需要根据实际的AppletUser服务来实现 | |||
| 2. **添加小程序用户服务**:创建AppletUserService来处理用户相关业务 | |||
| 3. **配置小程序用户权限**:为小程序用户配置独立的角色和权限体系 | |||
| 4. **添加健康数据管理**:实现BMI、体重等健康数据的计算和管理 | |||
| 5. **实现会员系统**:实现会员等级和积分系统 | |||
| @ -0,0 +1,169 @@ | |||
| package org.jeecg.common.system.vo; | |||
| import java.io.Serializable; | |||
| import java.io.UnsupportedEncodingException; | |||
| import java.util.Date; | |||
| import java.math.BigDecimal; | |||
| import com.baomidou.mybatisplus.annotation.IdType; | |||
| import com.baomidou.mybatisplus.annotation.TableId; | |||
| import com.baomidou.mybatisplus.annotation.TableName; | |||
| import com.baomidou.mybatisplus.annotation.TableLogic; | |||
| import org.jeecg.common.constant.ProvinceCityArea; | |||
| import org.jeecg.common.util.SpringContextUtils; | |||
| import lombok.Data; | |||
| import com.fasterxml.jackson.annotation.JsonFormat; | |||
| import org.springframework.format.annotation.DateTimeFormat; | |||
| import org.jeecgframework.poi.excel.annotation.Excel; | |||
| import org.jeecg.common.aspect.annotation.Dict; | |||
| import io.swagger.v3.oas.annotations.media.Schema; | |||
| import lombok.EqualsAndHashCode; | |||
| import lombok.experimental.Accessors; | |||
| /** | |||
| * @Description: 用户 | |||
| * @Author: jeecg-boot | |||
| * @Date: 2025-07-17 | |||
| * @Version: V1.0 | |||
| */ | |||
| @Data | |||
| @TableName("applet_user") | |||
| @Accessors(chain = true) | |||
| @EqualsAndHashCode(callSuper = false) | |||
| @Schema(description="用户") | |||
| public class AppletUser implements Serializable { | |||
| private static final long serialVersionUID = 1L; | |||
| /**主键*/ | |||
| @TableId(type = IdType.ASSIGN_ID) | |||
| @Schema(description = "主键") | |||
| private java.lang.String id; | |||
| /**创建人*/ | |||
| @Schema(description = "创建人") | |||
| private java.lang.String createBy; | |||
| /**创建日期*/ | |||
| @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") | |||
| @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") | |||
| @Schema(description = "创建日期") | |||
| private java.util.Date createTime; | |||
| /**更新人*/ | |||
| @Schema(description = "更新人") | |||
| private java.lang.String updateBy; | |||
| /**更新日期*/ | |||
| @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") | |||
| @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") | |||
| @Schema(description = "更新日期") | |||
| private java.util.Date updateTime; | |||
| /**所属部门*/ | |||
| @Schema(description = "所属部门") | |||
| private java.lang.String sysOrgCode; | |||
| /**昵称*/ | |||
| @Excel(name = "昵称", width = 15) | |||
| @Schema(description = "昵称") | |||
| private java.lang.String name; | |||
| /**第三方认证id*/ | |||
| @Excel(name = "第三方认证id", width = 15) | |||
| @Schema(description = "第三方认证id") | |||
| private java.lang.String openid; | |||
| /**手机号*/ | |||
| @Excel(name = "手机号", width = 15) | |||
| @Schema(description = "手机号") | |||
| private java.lang.String phone; | |||
| /**体总指数*/ | |||
| @Excel(name = "体总指数", width = 15) | |||
| @Schema(description = "体总指数") | |||
| private java.math.BigDecimal bmi; | |||
| /**脂肪*/ | |||
| @Excel(name = "脂肪", width = 15) | |||
| @Schema(description = "脂肪") | |||
| private java.math.BigDecimal fat; | |||
| /**头像*/ | |||
| @Excel(name = "头像", width = 15) | |||
| @Schema(description = "头像") | |||
| private java.lang.String avatar; | |||
| //================================== | |||
| /**用户名*/ | |||
| @Excel(name = "用户名", width = 15) | |||
| @Schema(description = "用户名") | |||
| private java.lang.String username; | |||
| /**密码*/ | |||
| @Schema(description = "密码") | |||
| private java.lang.String password; | |||
| /**生日*/ | |||
| @Excel(name = "生日", width = 15, format = "yyyy-MM-dd") | |||
| @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd") | |||
| @DateTimeFormat(pattern="yyyy-MM-dd") | |||
| @Schema(description = "生日") | |||
| private java.util.Date birthday; | |||
| /**性别(1:男 2:女)*/ | |||
| @Excel(name = "性别", width = 15, dicCode = "sex") | |||
| @Dict(dicCode = "sex") | |||
| @Schema(description = "性别(1:男 2:女)") | |||
| private java.lang.Integer sex; | |||
| /**邮箱*/ | |||
| @Excel(name = "邮箱", width = 15) | |||
| @Schema(description = "邮箱") | |||
| private java.lang.String email; | |||
| /**状态(1:正常 2:冻结)*/ | |||
| @Excel(name = "状态", width = 15, dicCode = "user_status") | |||
| @Dict(dicCode = "user_status") | |||
| @Schema(description = "状态(1:正常 2:冻结)") | |||
| private java.lang.Integer status; | |||
| /**删除标志(0代表存在 1代表删除)*/ | |||
| @Schema(description = "删除标志(0代表存在 1代表删除)") | |||
| private java.lang.Integer delFlag; | |||
| /**用户身份(1 普通用户 2 VIP用户)*/ | |||
| @Excel(name = "用户身份", width = 15, dicCode = "user_identity") | |||
| @Dict(dicCode = "user_identity") | |||
| @Schema(description = "用户身份(1 普通用户 2 VIP用户)") | |||
| private java.lang.Integer userIdentity; | |||
| /**身高(cm)*/ | |||
| @Excel(name = "身高", width = 15) | |||
| @Schema(description = "身高(cm)") | |||
| private java.math.BigDecimal height; | |||
| /**体重(kg)*/ | |||
| @Excel(name = "体重", width = 15) | |||
| @Schema(description = "体重(kg)") | |||
| private java.math.BigDecimal weight; | |||
| /**年龄*/ | |||
| @Excel(name = "年龄", width = 15) | |||
| @Schema(description = "年龄") | |||
| private java.lang.Integer age; | |||
| /**地址*/ | |||
| @Excel(name = "地址", width = 15) | |||
| @Schema(description = "地址") | |||
| private java.lang.String address; | |||
| /**紧急联系人*/ | |||
| @Excel(name = "紧急联系人", width = 15) | |||
| @Schema(description = "紧急联系人") | |||
| private java.lang.String emergencyContact; | |||
| /**紧急联系人电话*/ | |||
| @Excel(name = "紧急联系人电话", width = 15) | |||
| @Schema(description = "紧急联系人电话") | |||
| private java.lang.String emergencyPhone; | |||
| /**会员等级*/ | |||
| @Excel(name = "会员等级", width = 15) | |||
| @Schema(description = "会员等级") | |||
| private java.lang.String memberLevel; | |||
| /**积分*/ | |||
| @Excel(name = "积分", width = 15) | |||
| @Schema(description = "积分") | |||
| private java.lang.Integer points; | |||
| /**最后登录时间*/ | |||
| @Excel(name = "最后登录时间", width = 15, format = "yyyy-MM-dd HH:mm:ss") | |||
| @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss") | |||
| @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") | |||
| @Schema(description = "最后登录时间") | |||
| private java.util.Date lastLoginTime; | |||
| /**设备ID*/ | |||
| @Schema(description = "设备ID") | |||
| private java.lang.String deviceId; | |||
| /**登录IP*/ | |||
| @Schema(description = "登录IP") | |||
| private java.lang.String loginIp; | |||
| /**备注*/ | |||
| @Excel(name = "备注", width = 15) | |||
| @Schema(description = "备注") | |||
| private java.lang.String remark; | |||
| } | |||
| @ -0,0 +1,141 @@ | |||
| package org.jeecg.config.shiro; | |||
| import lombok.extern.slf4j.Slf4j; | |||
| import org.apache.commons.lang.StringUtils; | |||
| import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter; | |||
| import org.jeecg.common.config.TenantContext; | |||
| import org.jeecg.common.constant.CommonConstant; | |||
| import org.jeecg.common.system.util.JwtUtil; | |||
| import org.jeecg.common.util.oConvertUtils; | |||
| import org.jeecg.config.shiro.AppletJwtToken; | |||
| import org.jeecg.config.shiro.AppletShiroRealm; | |||
| import org.jeecg.config.shiro.ignore.InMemoryIgnoreAuth; | |||
| import org.springframework.http.HttpHeaders; | |||
| import org.springframework.http.HttpStatus; | |||
| import org.springframework.web.bind.annotation.RequestMethod; | |||
| import javax.annotation.Resource; | |||
| import javax.servlet.ServletRequest; | |||
| import javax.servlet.ServletResponse; | |||
| import javax.servlet.http.HttpServletRequest; | |||
| import javax.servlet.http.HttpServletResponse; | |||
| /** | |||
| * @Description: 小程序专用鉴权登录拦截器 | |||
| * @Author: jeecg | |||
| * @Date: 2024/12/19 | |||
| **/ | |||
| @Slf4j | |||
| public class AppletJwtFilter extends BasicHttpAuthenticationFilter { | |||
| @Resource | |||
| private AppletShiroRealm appletRealm; | |||
| /** | |||
| * 默认开启跨域设置(使用单体) | |||
| * 微服务情况下,此属性设置为false | |||
| */ | |||
| private boolean allowOrigin = true; | |||
| public AppletJwtFilter(){} | |||
| public AppletJwtFilter(boolean allowOrigin){ | |||
| this.allowOrigin = allowOrigin; | |||
| } | |||
| /** | |||
| * 执行登录认证 | |||
| * | |||
| * @param request | |||
| * @param response | |||
| * @param mappedValue | |||
| * @return | |||
| */ | |||
| @Override | |||
| protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { | |||
| try { | |||
| HttpServletRequest httpRequest = (HttpServletRequest) request; | |||
| String requestPath = httpRequest.getServletPath(); | |||
| // 只处理以/applet开头的请求 | |||
| if (!requestPath.startsWith("/applet")) { | |||
| return true; // 不是applet请求,直接放行 | |||
| } | |||
| // 判断当前路径是不是注解了@IngoreAuth路径,如果是,则放开验证 | |||
| if (InMemoryIgnoreAuth.contains(requestPath)) { | |||
| return true; | |||
| } | |||
| executeLogin(request, response); | |||
| return true; | |||
| } catch (Exception e) { | |||
| JwtUtil.responseError(response,401,CommonConstant.TOKEN_IS_INVALID_MSG); | |||
| return false; | |||
| } | |||
| } | |||
| /** | |||
| * | |||
| */ | |||
| @Override | |||
| protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception { | |||
| HttpServletRequest httpServletRequest = (HttpServletRequest) request; | |||
| String token = httpServletRequest.getHeader(CommonConstant.X_ACCESS_TOKEN); | |||
| // update-begin--Author:lvdandan Date:20210105 for:JT-355 OA聊天添加token验证,获取token参数 | |||
| if (oConvertUtils.isEmpty(token)) { | |||
| token = httpServletRequest.getParameter("token"); | |||
| } | |||
| // update-end--Author:lvdandan Date:20210105 for:JT-355 OA聊天添加token验证,获取token参数 | |||
| AppletJwtToken jwtToken = new AppletJwtToken(token); | |||
| // 提交给小程序专用realm进行登入,如果错误他会抛出异常并被捕获 | |||
| getSubject(request, response).login(jwtToken); | |||
| // 如果没有抛出异常则代表登入成功,返回true | |||
| return true; | |||
| } | |||
| /** | |||
| * 对跨域提供支持 | |||
| */ | |||
| @Override | |||
| protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception { | |||
| HttpServletRequest httpServletRequest = (HttpServletRequest) request; | |||
| HttpServletResponse httpServletResponse = (HttpServletResponse) response; | |||
| if(allowOrigin){ | |||
| httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, httpServletRequest.getHeader(HttpHeaders.ORIGIN)); | |||
| // 允许客户端请求方法 | |||
| httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, "GET,POST,OPTIONS,PUT,DELETE"); | |||
| // 允许客户端提交的Header | |||
| String requestHeaders = httpServletRequest.getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS); | |||
| if (StringUtils.isNotEmpty(requestHeaders)) { | |||
| httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, requestHeaders); | |||
| } | |||
| // 允许客户端携带凭证信息(是否允许发送Cookie) | |||
| httpServletResponse.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); | |||
| } | |||
| // 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态 | |||
| if (RequestMethod.OPTIONS.name().equalsIgnoreCase(httpServletRequest.getMethod())) { | |||
| httpServletResponse.setStatus(HttpStatus.OK.value()); | |||
| return false; | |||
| } | |||
| //update-begin-author:taoyan date:20200708 for:多租户用到 | |||
| String tenantId = httpServletRequest.getHeader(CommonConstant.TENANT_ID); | |||
| TenantContext.setTenant(tenantId); | |||
| //update-end-author:taoyan date:20200708 for:多租户用到 | |||
| return super.preHandle(request, response); | |||
| } | |||
| /** | |||
| * AppletJwtFilter中ThreadLocal需要及时清除 | |||
| * | |||
| * @param request | |||
| * @param response | |||
| * @param exception | |||
| * @throws Exception | |||
| */ | |||
| @Override | |||
| public void afterCompletion(ServletRequest request, ServletResponse response, Exception exception) throws Exception { | |||
| TenantContext.clear(); | |||
| } | |||
| } | |||
| @ -0,0 +1,28 @@ | |||
| package org.jeecg.config.shiro; | |||
| import org.apache.shiro.authc.AuthenticationToken; | |||
| /** | |||
| * @Description: 小程序专用JWT Token | |||
| * @Author: jeecg | |||
| * @Date: 2024/12/19 | |||
| **/ | |||
| public class AppletJwtToken implements AuthenticationToken { | |||
| private static final long serialVersionUID = 1L; | |||
| private String token; | |||
| public AppletJwtToken(String token) { | |||
| this.token = token; | |||
| } | |||
| @Override | |||
| public Object getPrincipal() { | |||
| return token; | |||
| } | |||
| @Override | |||
| public Object getCredentials() { | |||
| return token; | |||
| } | |||
| } | |||
| @ -0,0 +1,198 @@ | |||
| package org.jeecg.config.shiro; | |||
| import lombok.extern.slf4j.Slf4j; | |||
| import org.apache.shiro.authc.AuthenticationException; | |||
| import org.apache.shiro.authc.AuthenticationInfo; | |||
| import org.apache.shiro.authc.AuthenticationToken; | |||
| import org.apache.shiro.authc.SimpleAuthenticationInfo; | |||
| import org.apache.shiro.authz.AuthorizationInfo; | |||
| import org.apache.shiro.authz.SimpleAuthorizationInfo; | |||
| import org.apache.shiro.realm.AuthorizingRealm; | |||
| import org.apache.shiro.subject.PrincipalCollection; | |||
| import org.jeecg.common.api.CommonAPI; | |||
| import org.jeecg.common.config.TenantContext; | |||
| import org.jeecg.common.constant.CacheConstant; | |||
| import org.jeecg.common.constant.CommonConstant; | |||
| import org.jeecg.common.system.util.JwtUtil; | |||
| import org.jeecg.common.system.vo.AppletUser; | |||
| import org.jeecg.common.util.RedisUtil; | |||
| import org.jeecg.common.util.SpringContextUtils; | |||
| import org.jeecg.common.util.TokenUtils; | |||
| import org.jeecg.common.util.oConvertUtils; | |||
| import org.jeecg.config.mybatis.MybatisPlusSaasConfig; | |||
| import org.springframework.context.annotation.Lazy; | |||
| import org.springframework.stereotype.Component; | |||
| import javax.annotation.Resource; | |||
| import javax.servlet.http.HttpServletRequest; | |||
| import java.util.Set; | |||
| /** | |||
| * @Description: 小程序用户登录鉴权和获取用户授权 | |||
| * @Author: jeecg | |||
| * @Date: 2024/12/19 | |||
| **/ | |||
| @Component | |||
| @Slf4j | |||
| public class AppletShiroRealm extends AuthorizingRealm { | |||
| @Lazy | |||
| @Resource | |||
| private CommonAPI commonApi; | |||
| @Lazy | |||
| @Resource | |||
| private RedisUtil redisUtil; | |||
| /** | |||
| * 必须重写此方法,不然Shiro会报错 | |||
| */ | |||
| @Override | |||
| public boolean supports(AuthenticationToken token) { | |||
| return token instanceof AppletJwtToken; | |||
| } | |||
| /** | |||
| * 权限信息认证(包括角色以及权限)是用户访问controller的时候才进行验证(redis存储的此处权限信息) | |||
| * 触发检测用户权限时才会调用此方法,例如checkRole,checkPermission | |||
| * | |||
| * @param principals 身份信息 | |||
| * @return AuthorizationInfo 权限信息 | |||
| */ | |||
| @Override | |||
| protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { | |||
| log.debug("===============小程序Shiro权限认证开始============ [ roles、permissions]=========="); | |||
| String username = null; | |||
| String userId = null; | |||
| if (principals != null) { | |||
| AppletUser appletUser = (AppletUser) principals.getPrimaryPrincipal(); | |||
| username = appletUser.getUsername(); | |||
| userId = appletUser.getId(); | |||
| } | |||
| SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); | |||
| log.info("===============小程序Shiro权限认证成功=============="); | |||
| return info; | |||
| } | |||
| /** | |||
| * 用户信息认证是在用户进行登录的时候进行验证(不存redis) | |||
| * 也就是说验证用户输入的账号和密码是否正确,错误抛出异常 | |||
| * | |||
| * @param auth 用户登录的账号密码信息 | |||
| * @return 返回封装了用户信息的 AuthenticationInfo 实例 | |||
| * @throws AuthenticationException | |||
| */ | |||
| @Override | |||
| protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException { | |||
| log.debug("===============小程序Shiro身份认证开始============doGetAuthenticationInfo=========="); | |||
| String token = (String) auth.getCredentials(); | |||
| if (token == null) { | |||
| HttpServletRequest req = SpringContextUtils.getHttpServletRequest(); | |||
| log.info("————————小程序身份认证失败——————————IP地址: "+ oConvertUtils.getIpAddrByRequest(req) +",URL:"+req.getRequestURI()); | |||
| throw new AuthenticationException("小程序token为空!"); | |||
| } | |||
| // 校验token有效性 | |||
| AppletUser loginUser = null; | |||
| try { | |||
| loginUser = this.checkAppletUserTokenIsEffect(token); | |||
| } catch (AuthenticationException e) { | |||
| JwtUtil.responseError(SpringContextUtils.getHttpServletResponse(),401,e.getMessage()); | |||
| e.printStackTrace(); | |||
| return null; | |||
| } | |||
| return new SimpleAuthenticationInfo(loginUser, token, getName()); | |||
| } | |||
| /** | |||
| * 校验小程序token的有效性 | |||
| * | |||
| * @param token | |||
| */ | |||
| public AppletUser checkAppletUserTokenIsEffect(String token) throws AuthenticationException { | |||
| // 解密获得username,用于和数据库进行对比 | |||
| String username = JwtUtil.getUsername(token); | |||
| if (username == null) { | |||
| throw new AuthenticationException("小程序token非法无效!"); | |||
| } | |||
| // 查询用户信息 | |||
| log.debug("———校验小程序token是否有效————checkAppletUserTokenIsEffect——————— "+ token); | |||
| AppletUser loginUser = this.getAppletUser(username); | |||
| if (loginUser == null) { | |||
| throw new AuthenticationException("小程序用户不存在!"); | |||
| } | |||
| // 判断用户状态 | |||
| if (loginUser.getStatus() != 1) { | |||
| throw new AuthenticationException("小程序账号已被锁定,请联系管理员!"); | |||
| } | |||
| // 校验token是否超时失效 & 或者账号密码是否错误 | |||
| if (!jwtTokenRefresh(token, username, loginUser.getPassword())) { | |||
| throw new AuthenticationException(CommonConstant.TOKEN_IS_INVALID_MSG); | |||
| } | |||
| return loginUser; | |||
| } | |||
| /** | |||
| * 获取小程序用户信息 | |||
| * | |||
| * @param username | |||
| * @return | |||
| */ | |||
| private AppletUser getAppletUser(String username) { | |||
| // TODO: 这里需要根据实际的小程序用户服务来获取用户信息 | |||
| // 可以从数据库查询AppletUser实体 | |||
| // 或者从Redis缓存中获取 | |||
| // 示例代码: | |||
| // AppletUser appletUser = appletUserService.getByUsername(username); | |||
| // return appletUser; | |||
| // 临时返回null,需要根据实际业务实现 | |||
| return null; | |||
| } | |||
| /** | |||
| * JWTToken刷新生命周期 (实现: 用户在线操作不掉线功能) | |||
| * 1、登录成功后将用户的JWT生成的Token作为k、v存储到cache缓存里面(这时候k、v值一样),缓存有效期设置为Jwt有效时间的2倍 | |||
| * 2、当该用户再次请求时,通过JWTFilter层层校验之后会进入到doGetAuthenticationInfo进行身份验证 | |||
| * 3、当该用户这次请求jwt生成的token值已经超时,但该token对应cache中的k还是存在,则表示该用户一直在操作只是JWT的token失效了,程序会给token对应的k映射的v值重新生成JWTToken并覆盖v值,该缓存生命周期重新计算 | |||
| * 4、当该用户这次请求jwt在生成的token值已经超时,并在cache中不存在对应的k,则表示该用户账户空闲超时,返回用户信息已失效,请重新登录。 | |||
| * 注意: 前端请求Header中设置Authorization保持不变,校验有效性以缓存中的token为准。 | |||
| * 用户过期时间 = Jwt有效时间 * 2。 | |||
| * | |||
| * @param userName | |||
| * @param passWord | |||
| * @return | |||
| */ | |||
| public boolean jwtTokenRefresh(String token, String userName, String passWord) { | |||
| String cacheToken = String.valueOf(redisUtil.get(CommonConstant.PREFIX_USER_TOKEN + token)); | |||
| if (oConvertUtils.isNotEmpty(cacheToken)) { | |||
| // 校验token有效性 | |||
| if (!JwtUtil.verify(cacheToken, userName, passWord)) { | |||
| String newAuthorization = JwtUtil.sign(userName, passWord); | |||
| redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, newAuthorization); | |||
| // 设置超时时间 | |||
| redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME * 2 / 1000); | |||
| log.debug("——————————小程序用户存在操作,token已经刷新——————————"); | |||
| } else { | |||
| redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, cacheToken); | |||
| // 设置超时时间 | |||
| redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME * 2 / 1000); | |||
| log.debug("——————————小程序用户存在操作,token有效——————————"); | |||
| } | |||
| return true; | |||
| } | |||
| return false; | |||
| } | |||
| /** | |||
| * 清除当前用户的权限认证缓存 | |||
| * | |||
| * @param principals 权限信息 | |||
| */ | |||
| @Override | |||
| public void clearCache(PrincipalCollection principals) { | |||
| super.clearCache(principals); | |||
| } | |||
| } | |||
| @ -0,0 +1,272 @@ | |||
| # 小程序模块使用说明 | |||
| ## 概述 | |||
| 本模块为健康管理小程序提供后端服务支持,包含登录、用户管理、微信功能等核心功能。 | |||
| ## 目录结构 | |||
| ``` | |||
| jeecgboot-boot-applet/ | |||
| ├── src/main/java/org/jeecg/modules/ | |||
| │ ├── applet/ # 小程序核心模块 | |||
| │ │ ├── controller/ # 控制器层 | |||
| │ │ │ ├── AppletLoginController.java # 登录控制器 | |||
| │ │ │ ├── AppletUserController.java # 用户控制器 | |||
| │ │ │ └── WxAppletController.java # 微信控制器 | |||
| │ │ └── service/ # 服务层 | |||
| │ │ ├── AppletLoginService.java # 登录服务 | |||
| │ │ ├── AppletUserService.java # 用户服务 | |||
| │ │ └── WxAppletService.java # 微信服务 | |||
| │ └── common/ | |||
| │ └── wxUtils/ # 微信工具类 | |||
| │ ├── WxHttpUtils.java # 微信HTTP工具 | |||
| │ └── WxHttpClientUtil.java # 微信HTTP客户端 | |||
| └── src/main/resources/ | |||
| └── application-applet.yml # 小程序配置文件 | |||
| ``` | |||
| ## 功能模块 | |||
| ### 1. 登录模块 (AppletLoginController) | |||
| #### 接口列表 | |||
| - `POST /applet/login/wxLogin` - 微信小程序登录 | |||
| - `POST /applet/login/getPhoneNumber` - 获取用户手机号 | |||
| - `POST /applet/login/refreshToken` - 刷新token | |||
| - `POST /applet/login/logout` - 退出登录 | |||
| - `GET /applet/login/checkLogin` - 检查登录状态 | |||
| #### 使用示例 | |||
| ```javascript | |||
| // 小程序登录 | |||
| wx.login({ | |||
| success: (res) => { | |||
| if (res.code) { | |||
| // 发送 res.code 到后台换取 openId, sessionKey, unionId | |||
| wx.request({ | |||
| url: 'http://your-domain/applet/login/wxLogin', | |||
| method: 'POST', | |||
| data: { | |||
| code: res.code | |||
| }, | |||
| success: (result) => { | |||
| console.log('登录成功', result.data); | |||
| // 保存token | |||
| wx.setStorageSync('token', result.data.result.token); | |||
| } | |||
| }); | |||
| } | |||
| } | |||
| }); | |||
| ``` | |||
| ### 2. 用户模块 (AppletUserController) | |||
| #### 接口列表 | |||
| - `GET /applet/user/info` - 获取用户信息 | |||
| - `POST /applet/user/update` - 更新用户信息 | |||
| - `GET /applet/user/health` - 获取健康信息 | |||
| - `POST /applet/user/health/update` - 更新健康信息 | |||
| - `GET /applet/user/member` - 获取会员信息 | |||
| #### 使用示例 | |||
| ```javascript | |||
| // 获取用户信息 | |||
| wx.request({ | |||
| url: 'http://your-domain/applet/user/info', | |||
| method: 'GET', | |||
| data: { | |||
| userId: 'applet_user_id' | |||
| }, | |||
| header: { | |||
| 'Authorization': 'Bearer ' + wx.getStorageSync('token') | |||
| }, | |||
| success: (result) => { | |||
| console.log('用户信息', result.data); | |||
| } | |||
| }); | |||
| ``` | |||
| ### 3. 微信模块 (WxAppletController) | |||
| #### 接口列表 | |||
| - `POST /applet/wx/qrcode` - 获取小程序码 | |||
| - `POST /applet/wx/subscribe/send` - 发送订阅消息 | |||
| - `GET /applet/wx/config` - 获取小程序配置 | |||
| - `GET /applet/wx/check` - 检查微信服务器 | |||
| - `GET /applet/wx/user/info` - 获取微信用户信息 | |||
| #### 使用示例 | |||
| ```javascript | |||
| // 获取小程序码 | |||
| wx.request({ | |||
| url: 'http://your-domain/applet/wx/qrcode', | |||
| method: 'POST', | |||
| data: { | |||
| scene: 'user_id_123', | |||
| page: 'pages/index/index' | |||
| }, | |||
| success: (result) => { | |||
| console.log('小程序码', result.data); | |||
| } | |||
| }); | |||
| ``` | |||
| ## 配置说明 | |||
| ### 1. 微信配置 | |||
| 在 `application-applet.yml` 中配置微信小程序信息: | |||
| ```yaml | |||
| applet: | |||
| wechat: | |||
| mpAppId: your_applet_appid | |||
| mpAppSecret: your_applet_secret | |||
| pay: | |||
| mchId: your_mch_id | |||
| mchKey: your_mch_key | |||
| ``` | |||
| ### 2. 环境变量 | |||
| 可以通过环境变量覆盖配置: | |||
| ```bash | |||
| export WECHAT_MP_APPID=your_applet_appid | |||
| export WECHAT_MP_APPSECRET=your_applet_secret | |||
| export WECHAT_MCH_ID=your_mch_id | |||
| export WECHAT_MCH_KEY=your_mch_key | |||
| ``` | |||
| ### 3. 功能开关 | |||
| 可以通过配置文件控制功能模块的启用: | |||
| ```yaml | |||
| applet: | |||
| features: | |||
| login: true # 登录功能 | |||
| userInfo: true # 用户信息功能 | |||
| healthInfo: true # 健康信息功能 | |||
| member: true # 会员功能 | |||
| subscribe: true # 订阅消息功能 | |||
| qrcode: true # 小程序码功能 | |||
| ``` | |||
| ## 安全配置 | |||
| ### 1. Token配置 | |||
| ```yaml | |||
| applet: | |||
| security: | |||
| tokenExpireTime: 7200 # token过期时间(秒) | |||
| refreshTokenExpireTime: 604800 # 刷新token过期时间(秒) | |||
| enableTokenBlacklist: true # 启用token黑名单 | |||
| ``` | |||
| ### 2. 接口权限 | |||
| 所有小程序接口都使用了 `@IgnoreAuth` 注解,表示不需要登录验证。在实际使用中,可以根据需要添加token验证。 | |||
| ## 日志配置 | |||
| ```yaml | |||
| applet: | |||
| logging: | |||
| level: INFO | |||
| logWxApi: true # 记录微信API调用日志 | |||
| logUserAction: true # 记录用户操作日志 | |||
| ``` | |||
| ## 缓存配置 | |||
| ```yaml | |||
| applet: | |||
| cache: | |||
| accessTokenExpire: 7000 # 微信access_token缓存时间(秒) | |||
| userInfoExpire: 3600 # 用户信息缓存时间(秒) | |||
| qrcodeExpire: 86400 # 小程序码缓存时间(秒) | |||
| ``` | |||
| ## 开发说明 | |||
| ### 1. 数据库集成 | |||
| 当前版本使用模拟数据,实际使用时需要: | |||
| 1. 创建用户表 `applet_user` | |||
| 2. 创建健康信息表 `applet_health_info` | |||
| 3. 创建会员信息表 `applet_member_info` | |||
| 4. 在Service层实现数据库操作 | |||
| ### 2. 微信API集成 | |||
| 已集成以下微信API: | |||
| - 登录:`/sns/jscode2session` | |||
| - 获取手机号:`/wxa/business/getuserphonenumber` | |||
| - 获取access_token:`/cgi-bin/token` | |||
| - 获取小程序码:`/wxa/getwxacodeunlimit` | |||
| - 发送订阅消息:`/cgi-bin/message/subscribe/send` | |||
| ### 3. 错误处理 | |||
| 所有接口都包含完整的异常处理: | |||
| - 微信API调用失败 | |||
| - 参数验证失败 | |||
| - 数据库操作失败 | |||
| - 网络连接失败 | |||
| ### 4. 扩展开发 | |||
| 如需添加新功能,可以: | |||
| 1. 在 `service` 包下创建新的服务类 | |||
| 2. 在 `controller` 包下创建对应的控制器 | |||
| 3. 在配置文件中添加相关配置 | |||
| 4. 更新本文档 | |||
| ## 部署说明 | |||
| ### 1. 打包 | |||
| ```bash | |||
| mvn clean package -Dmaven.test.skip=true | |||
| ``` | |||
| ### 2. 运行 | |||
| ```bash | |||
| java -jar jeecgboot-boot-applet.jar --spring.profiles.active=prod | |||
| ``` | |||
| ### 3. Docker部署 | |||
| ```dockerfile | |||
| FROM openjdk:8-jre-alpine | |||
| COPY jeecgboot-boot-applet.jar app.jar | |||
| EXPOSE 8080 | |||
| ENTRYPOINT ["java", "-jar", "/app.jar"] | |||
| ``` | |||
| ## 注意事项 | |||
| 1. **安全性**:生产环境中请务必配置正确的微信小程序密钥 | |||
| 2. **性能**:建议对微信API调用结果进行缓存 | |||
| 3. **监控**:建议添加接口调用监控和日志收集 | |||
| 4. **测试**:请在小程序开发工具中充分测试所有功能 | |||
| 5. **文档**:接口文档可通过Swagger UI查看:`http://your-domain/swagger-ui.html` | |||
| ## 技术支持 | |||
| 如有问题,请联系开发团队或查看项目文档。 | |||
| @ -0,0 +1,105 @@ | |||
| package org.jeecg.modules.applet.controller; | |||
| import io.swagger.v3.oas.annotations.Operation; | |||
| import io.swagger.v3.oas.annotations.tags.Tag; | |||
| import lombok.extern.slf4j.Slf4j; | |||
| import org.jeecg.common.api.vo.Result; | |||
| import org.jeecg.config.shiro.IgnoreAuth; | |||
| import org.jeecg.modules.applet.service.AppletLoginService; | |||
| import org.springframework.beans.factory.annotation.Autowired; | |||
| import org.springframework.web.bind.annotation.*; | |||
| import java.util.Map; | |||
| /** | |||
| * 小程序登录控制器 | |||
| * | |||
| * @author system | |||
| * @date 2025-01-25 | |||
| */ | |||
| @Slf4j | |||
| @RestController | |||
| @RequestMapping("/applet/login") | |||
| @Tag(name = "小程序登录", description = "小程序登录相关接口") | |||
| public class AppletLoginController { | |||
| @Autowired | |||
| private AppletLoginService appletLoginService; | |||
| /** | |||
| * 小程序登录 | |||
| * | |||
| * @param code 微信登录code | |||
| * @return 登录结果 | |||
| */ | |||
| @PostMapping("/wxLogin") | |||
| @Operation(summary = "微信小程序登录", description = "通过微信code进行小程序登录") | |||
| @IgnoreAuth | |||
| public Result<Map<String, Object>> wxLogin(@RequestParam String code) { | |||
| log.info("收到小程序登录请求,code: {}", code); | |||
| return appletLoginService.login(code); | |||
| } | |||
| /** | |||
| * 获取用户手机号 | |||
| * | |||
| * @param code 手机号获取code | |||
| * @return 手机号信息 | |||
| */ | |||
| @PostMapping("/getPhoneNumber") | |||
| @Operation(summary = "获取用户手机号", description = "通过微信code获取用户手机号") | |||
| @IgnoreAuth | |||
| public Result<String> getPhoneNumber(@RequestParam String code) { | |||
| log.info("收到获取手机号请求,code: {}", code); | |||
| return appletLoginService.getPhoneNumber(code); | |||
| } | |||
| /** | |||
| * 刷新token | |||
| * | |||
| * @param token 原token | |||
| * @return 新token | |||
| */ | |||
| @PostMapping("/refreshToken") | |||
| @Operation(summary = "刷新token", description = "刷新用户登录token") | |||
| @IgnoreAuth | |||
| public Result<String> refreshToken(@RequestParam String token) { | |||
| log.info("收到刷新token请求"); | |||
| return appletLoginService.refreshToken(token); | |||
| } | |||
| /** | |||
| * 退出登录 | |||
| * | |||
| * @param token 用户token | |||
| * @return 退出结果 | |||
| */ | |||
| @PostMapping("/logout") | |||
| @Operation(summary = "退出登录", description = "用户退出登录") | |||
| @IgnoreAuth | |||
| public Result<String> logout(@RequestParam String token) { | |||
| log.info("收到退出登录请求"); | |||
| return appletLoginService.logout(token); | |||
| } | |||
| /** | |||
| * 检查登录状态 | |||
| * | |||
| * @param token 用户token | |||
| * @return 登录状态 | |||
| */ | |||
| @GetMapping("/checkLogin") | |||
| @Operation(summary = "检查登录状态", description = "检查用户登录状态") | |||
| @IgnoreAuth | |||
| public Result<Boolean> checkLogin(@RequestParam String token) { | |||
| log.info("收到检查登录状态请求"); | |||
| try { | |||
| // 这里可以验证token的有效性 | |||
| // 暂时返回true,实际应该验证token | |||
| return Result.OK("检查成功", true); | |||
| } catch (Exception e) { | |||
| log.error("检查登录状态异常", e); | |||
| return Result.error("检查失败: " + e.getMessage()); | |||
| } | |||
| } | |||
| } | |||
| @ -0,0 +1,100 @@ | |||
| package org.jeecg.modules.applet.controller; | |||
| import io.swagger.v3.oas.annotations.Operation; | |||
| import io.swagger.v3.oas.annotations.tags.Tag; | |||
| import lombok.extern.slf4j.Slf4j; | |||
| import org.jeecg.common.api.vo.Result; | |||
| import org.jeecg.config.shiro.IgnoreAuth; | |||
| import org.jeecg.modules.applet.service.AppletUserService; | |||
| import org.jeecg.common.system.vo.AppletUser; | |||
| import org.springframework.beans.factory.annotation.Autowired; | |||
| import org.springframework.web.bind.annotation.*; | |||
| import java.util.Map; | |||
| /** | |||
| * 小程序用户信息控制器 | |||
| * | |||
| * @author system | |||
| * @date 2025-01-25 | |||
| */ | |||
| @Slf4j | |||
| @RestController | |||
| @RequestMapping("/applet/userInfo") | |||
| @Tag(name = "小程序用户信息", description = "小程序用户信息管理相关接口") | |||
| public class AppletUserInfoController { | |||
| @Autowired | |||
| private AppletUserService appletUserService; | |||
| /** | |||
| * 获取用户信息 | |||
| * | |||
| * @param userId 用户ID | |||
| * @return 用户信息 | |||
| */ | |||
| @GetMapping("/info") | |||
| @Operation(summary = "获取用户信息", description = "获取小程序用户基本信息") | |||
| @IgnoreAuth | |||
| public Result<AppletUser> getUserInfo(@RequestParam String userId) { | |||
| log.info("收到获取用户信息请求,userId: {}", userId); | |||
| return appletUserService.getUserInfo(userId); | |||
| } | |||
| /** | |||
| * 更新用户信息 | |||
| * | |||
| * @param user 用户信息 | |||
| * @return 更新结果 | |||
| */ | |||
| @PostMapping("/update") | |||
| @Operation(summary = "更新用户信息", description = "更新小程序用户基本信息") | |||
| @IgnoreAuth | |||
| public Result<String> updateUserInfo(@RequestBody AppletUser user) { | |||
| log.info("收到更新用户信息请求,userId: {}", user.getId()); | |||
| return appletUserService.updateUserInfo(user); | |||
| } | |||
| /** | |||
| * 获取用户健康信息 | |||
| * | |||
| * @param userId 用户ID | |||
| * @return 健康信息 | |||
| */ | |||
| @GetMapping("/health") | |||
| @Operation(summary = "获取健康信息", description = "获取小程序用户健康信息") | |||
| @IgnoreAuth | |||
| public Result<Map<String, Object>> getHealthInfo(@RequestParam String userId) { | |||
| log.info("收到获取健康信息请求,userId: {}", userId); | |||
| return appletUserService.getHealthInfo(userId); | |||
| } | |||
| /** | |||
| * 更新用户健康信息 | |||
| * | |||
| * @param userId 用户ID | |||
| * @param healthInfo 健康信息 | |||
| * @return 更新结果 | |||
| */ | |||
| @PostMapping("/health/update") | |||
| @Operation(summary = "更新健康信息", description = "更新小程序用户健康信息") | |||
| @IgnoreAuth | |||
| public Result<String> updateHealthInfo(@RequestParam String userId, @RequestBody Map<String, Object> healthInfo) { | |||
| log.info("收到更新健康信息请求,userId: {}", userId); | |||
| return appletUserService.updateHealthInfo(userId, healthInfo); | |||
| } | |||
| /** | |||
| * 获取用户会员信息 | |||
| * | |||
| * @param userId 用户ID | |||
| * @return 会员信息 | |||
| */ | |||
| @GetMapping("/member") | |||
| @Operation(summary = "获取会员信息", description = "获取小程序用户会员信息") | |||
| @IgnoreAuth | |||
| public Result<Map<String, Object>> getMemberInfo(@RequestParam String userId) { | |||
| log.info("收到获取会员信息请求,userId: {}", userId); | |||
| return appletUserService.getMemberInfo(userId); | |||
| } | |||
| } | |||
| @ -0,0 +1,183 @@ | |||
| package org.jeecg.modules.applet.service; | |||
| import com.alibaba.fastjson.JSON; | |||
| import com.alibaba.fastjson.JSONObject; | |||
| import lombok.extern.slf4j.Slf4j; | |||
| import org.jeecg.common.api.vo.Result; | |||
| import org.jeecg.common.system.util.JwtUtil; | |||
| import org.jeecg.common.system.vo.AppletUser; | |||
| import org.jeecg.modules.common.wxUtils.WxHttpClientUtil; | |||
| import org.jeecg.modules.common.wxUtils.WxHttpUtils; | |||
| import org.springframework.beans.factory.annotation.Autowired; | |||
| import org.springframework.stereotype.Service; | |||
| import java.util.HashMap; | |||
| import java.util.Map; | |||
| /** | |||
| * 小程序登录服务类 | |||
| * | |||
| * @author system | |||
| * @date 2025-01-25 | |||
| */ | |||
| @Slf4j | |||
| @Service | |||
| public class AppletLoginService { | |||
| @Autowired | |||
| private WxHttpUtils wxHttpUtils; | |||
| /** | |||
| * 小程序登录 | |||
| * | |||
| * @param code 微信登录code | |||
| * @return 登录结果 | |||
| */ | |||
| public Result<Map<String, Object>> login(String code) { | |||
| try { | |||
| log.info("开始小程序登录,code: {}", code); | |||
| // 调用微信API获取openid和session_key | |||
| String loginUrl = "https://api.weixin.qq.com/sns/jscode2session"; | |||
| Map<String, String> params = new HashMap<>(); | |||
| params.put("appid", wxHttpUtils.getAppid()); | |||
| params.put("secret", wxHttpUtils.getSecret()); | |||
| params.put("js_code", code); | |||
| params.put("grant_type", "authorization_code"); | |||
| String response = WxHttpClientUtil.doGet(loginUrl, params); | |||
| JSONObject jsonResponse = JSON.parseObject(response); | |||
| // 检查微信API返回结果 | |||
| if (jsonResponse.containsKey("errcode") && jsonResponse.getInteger("errcode") != 0) { | |||
| log.error("微信登录失败: {}", response); | |||
| return Result.error("微信登录失败: " + jsonResponse.getString("errmsg")); | |||
| } | |||
| String openid = jsonResponse.getString("openid"); | |||
| String sessionKey = jsonResponse.getString("session_key"); | |||
| String unionid = jsonResponse.getString("unionid"); | |||
| log.info("微信登录成功,openid: {}", openid); | |||
| // 查找或创建用户 | |||
| AppletUser appletUser = findOrCreateUser(openid, unionid); | |||
| // 生成JWT token | |||
| String token = JwtUtil.sign(appletUser.getUsername(), appletUser.getId()); | |||
| // 构建返回结果 | |||
| Map<String, Object> result = new HashMap<>(); | |||
| result.put("token", token); | |||
| result.put("userInfo", appletUser); | |||
| result.put("openid", openid); | |||
| log.info("小程序登录成功,用户: {}", appletUser.getUsername()); | |||
| return Result.OK("登录成功", result); | |||
| } catch (Exception e) { | |||
| log.error("小程序登录异常", e); | |||
| return Result.error("登录失败: " + e.getMessage()); | |||
| } | |||
| } | |||
| /** | |||
| * 获取用户手机号 | |||
| * | |||
| * @param code 手机号获取code | |||
| * @return 手机号信息 | |||
| */ | |||
| public Result<String> getPhoneNumber(String code) { | |||
| try { | |||
| log.info("开始获取用户手机号,code: {}", code); | |||
| String phoneResponse = wxHttpUtils.getPhoneNumber(code); | |||
| JSONObject jsonResponse = JSON.parseObject(phoneResponse); | |||
| if (jsonResponse.getInteger("errcode") != 0) { | |||
| log.error("获取手机号失败: {}", phoneResponse); | |||
| return Result.error("获取手机号失败: " + jsonResponse.getString("errmsg")); | |||
| } | |||
| JSONObject phoneInfo = jsonResponse.getJSONObject("phone_info"); | |||
| String phoneNumber = phoneInfo.getString("phoneNumber"); | |||
| log.info("获取手机号成功: {}", phoneNumber); | |||
| return Result.OK("获取成功", phoneNumber); | |||
| } catch (Exception e) { | |||
| log.error("获取手机号异常", e); | |||
| return Result.error("获取手机号失败: " + e.getMessage()); | |||
| } | |||
| } | |||
| /** | |||
| * 查找或创建用户 | |||
| * | |||
| * @param openid 微信openid | |||
| * @param unionid 微信unionid | |||
| * @return 用户信息 | |||
| */ | |||
| private AppletUser findOrCreateUser(String openid, String unionid) { | |||
| // TODO: 这里应该查询数据库,暂时返回模拟数据 | |||
| AppletUser user = new AppletUser(); | |||
| user.setId("applet_" + openid.substring(0, 8)); | |||
| user.setUsername("小程序用户_" + openid.substring(0, 8)); | |||
| user.setPassword(""); // 小程序用户不需要密码 | |||
| user.setStatus(1); // 1-正常 | |||
| user.setOpenid(openid); | |||
| // TODO: 保存到数据库 | |||
| log.info("创建小程序用户: {}", user.getUsername()); | |||
| return user; | |||
| } | |||
| /** | |||
| * 刷新token | |||
| * | |||
| * @param token 原token | |||
| * @return 新token | |||
| */ | |||
| public Result<String> refreshToken(String token) { | |||
| try { | |||
| // 验证原token | |||
| String username = JwtUtil.getUsername(token); | |||
| if (username == null) { | |||
| return Result.error("token无效"); | |||
| } | |||
| // 生成新token | |||
| String newToken = JwtUtil.sign(username, "applet_user_id"); | |||
| log.info("刷新token成功,用户: {}", username); | |||
| return Result.OK("刷新成功", newToken); | |||
| } catch (Exception e) { | |||
| log.error("刷新token异常", e); | |||
| return Result.error("刷新失败: " + e.getMessage()); | |||
| } | |||
| } | |||
| /** | |||
| * 退出登录 | |||
| * | |||
| * @param token 用户token | |||
| * @return 退出结果 | |||
| */ | |||
| public Result<String> logout(String token) { | |||
| try { | |||
| String username = JwtUtil.getUsername(token); | |||
| if (username != null) { | |||
| // TODO: 这里可以将token加入黑名单 | |||
| log.info("用户退出登录: {}", username); | |||
| } | |||
| return Result.OK("退出成功"); | |||
| } catch (Exception e) { | |||
| log.error("退出登录异常", e); | |||
| return Result.error("退出失败: " + e.getMessage()); | |||
| } | |||
| } | |||
| } | |||
| @ -0,0 +1,136 @@ | |||
| package org.jeecg.modules.applet.service; | |||
| import lombok.extern.slf4j.Slf4j; | |||
| import org.jeecg.common.api.vo.Result; | |||
| import org.jeecg.common.system.vo.AppletUser; | |||
| import org.springframework.stereotype.Service; | |||
| import java.util.HashMap; | |||
| import java.util.Map; | |||
| /** | |||
| * 小程序用户服务类 | |||
| * | |||
| * @author system | |||
| * @date 2025-01-25 | |||
| */ | |||
| @Slf4j | |||
| @Service | |||
| public class AppletUserService { | |||
| /** | |||
| * 获取用户信息 | |||
| * | |||
| * @param userId 用户ID | |||
| * @return 用户信息 | |||
| */ | |||
| public Result<AppletUser> getUserInfo(String userId) { | |||
| try { | |||
| log.info("获取用户信息,userId: {}", userId); | |||
| // TODO: 从数据库查询用户信息 | |||
| AppletUser user = new AppletUser(); | |||
| user.setId(userId); | |||
| user.setUsername("小程序用户_" + userId.substring(0, 8)); | |||
| user.setStatus(1); | |||
| return Result.OK("获取成功", user); | |||
| } catch (Exception e) { | |||
| log.error("获取用户信息异常", e); | |||
| return Result.error("获取失败: " + e.getMessage()); | |||
| } | |||
| } | |||
| /** | |||
| * 更新用户信息 | |||
| * | |||
| * @param user 用户信息 | |||
| * @return 更新结果 | |||
| */ | |||
| public Result<String> updateUserInfo(AppletUser user) { | |||
| try { | |||
| log.info("更新用户信息,userId: {}", user.getId()); | |||
| // TODO: 更新数据库中的用户信息 | |||
| return Result.OK("更新成功"); | |||
| } catch (Exception e) { | |||
| log.error("更新用户信息异常", e); | |||
| return Result.error("更新失败: " + e.getMessage()); | |||
| } | |||
| } | |||
| /** | |||
| * 获取用户健康信息 | |||
| * | |||
| * @param userId 用户ID | |||
| * @return 健康信息 | |||
| */ | |||
| public Result<Map<String, Object>> getHealthInfo(String userId) { | |||
| try { | |||
| log.info("获取用户健康信息,userId: {}", userId); | |||
| // TODO: 从数据库查询用户健康信息 | |||
| Map<String, Object> healthInfo = new HashMap<>(); | |||
| healthInfo.put("height", 170); | |||
| healthInfo.put("weight", 65); | |||
| healthInfo.put("bmi", 22.5); | |||
| healthInfo.put("bloodType", "A"); | |||
| healthInfo.put("allergies", "无"); | |||
| return Result.OK("获取成功", healthInfo); | |||
| } catch (Exception e) { | |||
| log.error("获取健康信息异常", e); | |||
| return Result.error("获取失败: " + e.getMessage()); | |||
| } | |||
| } | |||
| /** | |||
| * 更新用户健康信息 | |||
| * | |||
| * @param userId 用户ID | |||
| * @param healthInfo 健康信息 | |||
| * @return 更新结果 | |||
| */ | |||
| public Result<String> updateHealthInfo(String userId, Map<String, Object> healthInfo) { | |||
| try { | |||
| log.info("更新用户健康信息,userId: {}", userId); | |||
| // TODO: 更新数据库中的健康信息 | |||
| return Result.OK("更新成功"); | |||
| } catch (Exception e) { | |||
| log.error("更新健康信息异常", e); | |||
| return Result.error("更新失败: " + e.getMessage()); | |||
| } | |||
| } | |||
| /** | |||
| * 获取用户会员信息 | |||
| * | |||
| * @param userId 用户ID | |||
| * @return 会员信息 | |||
| */ | |||
| public Result<Map<String, Object>> getMemberInfo(String userId) { | |||
| try { | |||
| log.info("获取用户会员信息,userId: {}", userId); | |||
| // TODO: 从数据库查询用户会员信息 | |||
| Map<String, Object> memberInfo = new HashMap<>(); | |||
| memberInfo.put("memberLevel", "VIP"); | |||
| memberInfo.put("memberPoints", 1000); | |||
| memberInfo.put("expireDate", "2025-12-31"); | |||
| memberInfo.put("discount", 0.9); | |||
| return Result.OK("获取成功", memberInfo); | |||
| } catch (Exception e) { | |||
| log.error("获取会员信息异常", e); | |||
| return Result.error("获取失败: " + e.getMessage()); | |||
| } | |||
| } | |||
| } | |||
| @ -0,0 +1,172 @@ | |||
| package org.jeecg.modules.applet.service; | |||
| import com.alibaba.fastjson.JSON; | |||
| import com.alibaba.fastjson.JSONObject; | |||
| import lombok.extern.slf4j.Slf4j; | |||
| import org.jeecg.common.api.vo.Result; | |||
| import org.jeecg.modules.common.wxUtils.WxHttpUtils; | |||
| import org.springframework.beans.factory.annotation.Autowired; | |||
| import org.springframework.stereotype.Service; | |||
| import java.util.HashMap; | |||
| import java.util.Map; | |||
| /** | |||
| * 微信小程序服务类 | |||
| * | |||
| * @author system | |||
| * @date 2025-01-25 | |||
| */ | |||
| @Slf4j | |||
| @Service | |||
| public class WxAppletService { | |||
| @Autowired | |||
| private WxHttpUtils wxHttpUtils; | |||
| /** | |||
| * 获取小程序码 | |||
| * | |||
| * @param scene 场景值 | |||
| * @param page 页面路径 | |||
| * @return 小程序码URL | |||
| */ | |||
| public Result<String> getWxacode(String scene, String page) { | |||
| try { | |||
| log.info("获取小程序码,scene: {}, page: {}", scene, page); | |||
| String accessToken = wxHttpUtils.getAccessToken(); | |||
| String url = "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=" + accessToken; | |||
| JSONObject requestBody = new JSONObject(); | |||
| requestBody.put("scene", scene); | |||
| requestBody.put("page", page); | |||
| requestBody.put("width", 430); | |||
| requestBody.put("auto_color", false); | |||
| // TODO: 这里应该返回图片数据,暂时返回URL | |||
| String result = org.jeecg.modules.common.wxUtils.WxHttpClientUtil.doPost(url, requestBody.toString()); | |||
| log.info("获取小程序码成功"); | |||
| return Result.OK("获取成功", result); | |||
| } catch (Exception e) { | |||
| log.error("获取小程序码异常", e); | |||
| return Result.error("获取失败: " + e.getMessage()); | |||
| } | |||
| } | |||
| /** | |||
| * 发送订阅消息 | |||
| * | |||
| * @param openid 用户openid | |||
| * @param templateId 模板ID | |||
| * @param data 模板数据 | |||
| * @param page 跳转页面 | |||
| * @return 发送结果 | |||
| */ | |||
| public Result<String> sendSubscribeMessage(String openid, String templateId, Map<String, Object> data, String page) { | |||
| try { | |||
| log.info("发送订阅消息,openid: {}, templateId: {}", openid, templateId); | |||
| String accessToken = wxHttpUtils.getAccessToken(); | |||
| String url = "https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=" + accessToken; | |||
| JSONObject requestBody = new JSONObject(); | |||
| requestBody.put("touser", openid); | |||
| requestBody.put("template_id", templateId); | |||
| requestBody.put("page", page); | |||
| requestBody.put("data", data); | |||
| String result = org.jeecg.modules.common.wxUtils.WxHttpClientUtil.doPost(url, requestBody.toString()); | |||
| JSONObject response = JSON.parseObject(result); | |||
| if (response.getInteger("errcode") == 0) { | |||
| log.info("发送订阅消息成功"); | |||
| return Result.OK("发送成功"); | |||
| } else { | |||
| log.error("发送订阅消息失败: {}", result); | |||
| return Result.error("发送失败: " + response.getString("errmsg")); | |||
| } | |||
| } catch (Exception e) { | |||
| log.error("发送订阅消息异常", e); | |||
| return Result.error("发送失败: " + e.getMessage()); | |||
| } | |||
| } | |||
| /** | |||
| * 获取小程序配置 | |||
| * | |||
| * @return 小程序配置信息 | |||
| */ | |||
| public Result<Map<String, String>> getAppletConfig() { | |||
| try { | |||
| log.info("获取小程序配置"); | |||
| Map<String, String> config = new HashMap<>(); | |||
| config.put("appId", wxHttpUtils.getAppid()); | |||
| config.put("appSecret", "***"); // 不返回真实密钥 | |||
| config.put("version", "1.0.0"); | |||
| config.put("env", "production"); | |||
| return Result.OK("获取成功", config); | |||
| } catch (Exception e) { | |||
| log.error("获取小程序配置异常", e); | |||
| return Result.error("获取失败: " + e.getMessage()); | |||
| } | |||
| } | |||
| /** | |||
| * 检查微信服务器连通性 | |||
| * | |||
| * @return 检查结果 | |||
| */ | |||
| public Result<Boolean> checkWxServer() { | |||
| try { | |||
| log.info("检查微信服务器连通性"); | |||
| String accessToken = wxHttpUtils.getAccessToken(); | |||
| if (accessToken != null && !accessToken.isEmpty()) { | |||
| log.info("微信服务器连通正常"); | |||
| return Result.OK("检查成功", true); | |||
| } else { | |||
| log.error("微信服务器连通异常"); | |||
| return Result.error("检查失败: 无法获取access_token"); | |||
| } | |||
| } catch (Exception e) { | |||
| log.error("检查微信服务器连通性异常", e); | |||
| return Result.error("检查失败: " + e.getMessage()); | |||
| } | |||
| } | |||
| /** | |||
| * 获取微信用户信息(通过openid) | |||
| * | |||
| * @param openid 用户openid | |||
| * @return 用户信息 | |||
| */ | |||
| public Result<Map<String, Object>> getWxUserInfo(String openid) { | |||
| try { | |||
| log.info("获取微信用户信息,openid: {}", openid); | |||
| // 注意:这里只能获取到用户的基本信息,详细信息需要用户授权 | |||
| Map<String, Object> userInfo = new HashMap<>(); | |||
| userInfo.put("openid", openid); | |||
| userInfo.put("nickname", "微信用户"); | |||
| userInfo.put("avatar", ""); | |||
| userInfo.put("gender", 0); | |||
| userInfo.put("country", ""); | |||
| userInfo.put("province", ""); | |||
| userInfo.put("city", ""); | |||
| return Result.OK("获取成功", userInfo); | |||
| } catch (Exception e) { | |||
| log.error("获取微信用户信息异常", e); | |||
| return Result.error("获取失败: " + e.getMessage()); | |||
| } | |||
| } | |||
| } | |||
| @ -0,0 +1,165 @@ | |||
| package org.jeecg.modules.appletBackground.appletUser.controller; | |||
| import java.util.Arrays; | |||
| import javax.servlet.http.HttpServletRequest; | |||
| import javax.servlet.http.HttpServletResponse; | |||
| import org.jeecg.common.api.vo.Result; | |||
| import org.jeecg.common.system.query.QueryGenerator; | |||
| import org.jeecg.modules.appletBackground.appletUser.service.IAppletUserService; | |||
| import org.jeecg.common.system.vo.AppletUser; | |||
| import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; | |||
| import com.baomidou.mybatisplus.core.metadata.IPage; | |||
| import com.baomidou.mybatisplus.extension.plugins.pagination.Page; | |||
| import lombok.extern.slf4j.Slf4j; | |||
| import org.jeecg.common.system.base.controller.JeecgController; | |||
| import org.springframework.beans.factory.annotation.Autowired; | |||
| import org.springframework.web.bind.annotation.*; | |||
| import org.springframework.web.servlet.ModelAndView; | |||
| import io.swagger.v3.oas.annotations.tags.Tag; | |||
| import io.swagger.v3.oas.annotations.Operation; | |||
| import org.jeecg.common.aspect.annotation.AutoLog; | |||
| import org.apache.shiro.authz.annotation.RequiresPermissions; | |||
| /** | |||
| * @Description: 用户 | |||
| * @Author: jeecg-boot | |||
| * @Date: 2025-07-17 | |||
| * @Version: V1.0 | |||
| */ | |||
| @Tag(name="用户") | |||
| @RestController | |||
| @RequestMapping("/commonUser/appletUser") | |||
| @Slf4j | |||
| public class AppletUserController extends JeecgController<AppletUser, IAppletUserService> { | |||
| @Autowired | |||
| private IAppletUserService appletUserService; | |||
| /** | |||
| * 分页列表查询 | |||
| * | |||
| * @param appletUser | |||
| * @param pageNo | |||
| * @param pageSize | |||
| * @param req | |||
| * @return | |||
| */ | |||
| //@AutoLog(value = "用户-分页列表查询") | |||
| @Operation(summary="用户-分页列表查询") | |||
| @GetMapping(value = "/list") | |||
| public Result<IPage<AppletUser>> queryPageList(AppletUser appletUser, | |||
| @RequestParam(name="pageNo", defaultValue="1") Integer pageNo, | |||
| @RequestParam(name="pageSize", defaultValue="10") Integer pageSize, | |||
| HttpServletRequest req) { | |||
| QueryWrapper<AppletUser> queryWrapper = QueryGenerator.initQueryWrapper(appletUser, req.getParameterMap()); | |||
| Page<AppletUser> page = new Page<AppletUser>(pageNo, pageSize); | |||
| IPage<AppletUser> pageList = appletUserService.page(page, queryWrapper); | |||
| return Result.OK(pageList); | |||
| } | |||
| /** | |||
| * 添加 | |||
| * | |||
| * @param appletUser | |||
| * @return | |||
| */ | |||
| @AutoLog(value = "用户-添加") | |||
| @Operation(summary="用户-添加") | |||
| @RequiresPermissions("appletUser:applet_user:add") | |||
| @PostMapping(value = "/add") | |||
| public Result<String> add(@RequestBody AppletUser appletUser) { | |||
| appletUserService.save(appletUser); | |||
| return Result.OK("添加成功!"); | |||
| } | |||
| /** | |||
| * 编辑 | |||
| * | |||
| * @param appletUser | |||
| * @return | |||
| */ | |||
| @AutoLog(value = "用户-编辑") | |||
| @Operation(summary="用户-编辑") | |||
| @RequiresPermissions("appletUser:applet_user:edit") | |||
| @RequestMapping(value = "/edit", method = {RequestMethod.PUT,RequestMethod.POST}) | |||
| public Result<String> edit(@RequestBody AppletUser appletUser) { | |||
| appletUserService.updateById(appletUser); | |||
| return Result.OK("编辑成功!"); | |||
| } | |||
| /** | |||
| * 通过id删除 | |||
| * | |||
| * @param id | |||
| * @return | |||
| */ | |||
| @AutoLog(value = "用户-通过id删除") | |||
| @Operation(summary="用户-通过id删除") | |||
| @RequiresPermissions("appletUser:applet_user:delete") | |||
| @DeleteMapping(value = "/delete") | |||
| public Result<String> delete(@RequestParam(name="id",required=true) String id) { | |||
| appletUserService.removeById(id); | |||
| return Result.OK("删除成功!"); | |||
| } | |||
| /** | |||
| * 批量删除 | |||
| * | |||
| * @param ids | |||
| * @return | |||
| */ | |||
| @AutoLog(value = "用户-批量删除") | |||
| @Operation(summary="用户-批量删除") | |||
| @RequiresPermissions("appletUser:applet_user:deleteBatch") | |||
| @DeleteMapping(value = "/deleteBatch") | |||
| public Result<String> deleteBatch(@RequestParam(name="ids",required=true) String ids) { | |||
| this.appletUserService.removeByIds(Arrays.asList(ids.split(","))); | |||
| return Result.OK("批量删除成功!"); | |||
| } | |||
| /** | |||
| * 通过id查询 | |||
| * | |||
| * @param id | |||
| * @return | |||
| */ | |||
| //@AutoLog(value = "用户-通过id查询") | |||
| @Operation(summary="用户-通过id查询") | |||
| @GetMapping(value = "/queryById") | |||
| public Result<AppletUser> queryById(@RequestParam(name="id",required=true) String id) { | |||
| AppletUser appletUser = appletUserService.getById(id); | |||
| if(appletUser==null) { | |||
| return Result.error("未找到对应数据"); | |||
| } | |||
| return Result.OK(appletUser); | |||
| } | |||
| /** | |||
| * 导出excel | |||
| * | |||
| * @param request | |||
| * @param appletUser | |||
| */ | |||
| @RequiresPermissions("appletUser:applet_user:exportXls") | |||
| @RequestMapping(value = "/exportXls") | |||
| public ModelAndView exportXls(HttpServletRequest request, AppletUser appletUser) { | |||
| return super.exportXls(request, appletUser, AppletUser.class, "用户"); | |||
| } | |||
| /** | |||
| * 通过excel导入数据 | |||
| * | |||
| * @param request | |||
| * @param response | |||
| * @return | |||
| */ | |||
| @RequiresPermissions("appletUser:applet_user:importExcel") | |||
| @RequestMapping(value = "/importExcel", method = RequestMethod.POST) | |||
| public Result<?> importExcel(HttpServletRequest request, HttpServletResponse response) { | |||
| return super.importExcel(request, response, AppletUser.class); | |||
| } | |||
| } | |||
| @ -0,0 +1,14 @@ | |||
| package org.jeecg.modules.appletBackground.appletUser.mapper; | |||
| import org.jeecg.common.system.vo.AppletUser; | |||
| import com.baomidou.mybatisplus.core.mapper.BaseMapper; | |||
| /** | |||
| * @Description: 用户 | |||
| * @Author: jeecg-boot | |||
| * @Date: 2025-07-17 | |||
| * @Version: V1.0 | |||
| */ | |||
| public interface AppletUserMapper extends BaseMapper<AppletUser> { | |||
| } | |||
| @ -0,0 +1,5 @@ | |||
| <?xml version="1.0" encoding="UTF-8"?> | |||
| <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> | |||
| <mapper namespace="org.jeecg.modules.appletBackground.appletUser.mapper.AppletUserMapper"> | |||
| </mapper> | |||
| @ -0,0 +1,14 @@ | |||
| package org.jeecg.modules.appletBackground.appletUser.service; | |||
| import org.jeecg.common.system.vo.AppletUser; | |||
| import com.baomidou.mybatisplus.extension.service.IService; | |||
| /** | |||
| * @Description: 用户 | |||
| * @Author: jeecg-boot | |||
| * @Date: 2025-07-17 | |||
| * @Version: V1.0 | |||
| */ | |||
| public interface IAppletUserService extends IService<AppletUser> { | |||
| } | |||
| @ -0,0 +1,19 @@ | |||
| package org.jeecg.modules.appletBackground.appletUser.service.impl; | |||
| import org.jeecg.common.system.vo.AppletUser; | |||
| import org.jeecg.modules.appletBackground.appletUser.mapper.AppletUserMapper; | |||
| import org.jeecg.modules.appletBackground.appletUser.service.IAppletUserService; | |||
| import org.springframework.stereotype.Service; | |||
| import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; | |||
| /** | |||
| * @Description: 用户 | |||
| * @Author: jeecg-boot | |||
| * @Date: 2025-07-17 | |||
| * @Version: V1.0 | |||
| */ | |||
| @Service | |||
| public class AppletUserServiceImpl extends ServiceImpl<AppletUserMapper, AppletUser> implements IAppletUserService { | |||
| } | |||
| @ -0,0 +1,113 @@ | |||
| <template> | |||
| <view> | |||
| <!--标题和返回--> | |||
| <cu-custom :bgColor="NavBarColor" isBack :backRouterName="backRouteName"> | |||
| <block slot="backText">返回</block> | |||
| <block slot="content">用户</block> | |||
| </cu-custom> | |||
| <!--表单区域--> | |||
| <view> | |||
| <form> | |||
| <view class="cu-form-group"> | |||
| <view class="flex align-center"> | |||
| <view class="title"><text space="ensp">昵称:</text></view> | |||
| <input placeholder="请输入昵称" v-model="model.name"/> | |||
| </view> | |||
| </view> | |||
| <view class="cu-form-group"> | |||
| <view class="flex align-center"> | |||
| <view class="title"><text space="ensp">第三方认证id:</text></view> | |||
| <input placeholder="请输入第三方认证id" v-model="model.openid"/> | |||
| </view> | |||
| </view> | |||
| <view class="cu-form-group"> | |||
| <view class="flex align-center"> | |||
| <view class="title"><text space="ensp">手机号:</text></view> | |||
| <input placeholder="请输入手机号" v-model="model.phone"/> | |||
| </view> | |||
| </view> | |||
| <view class="cu-form-group"> | |||
| <view class="flex align-center"> | |||
| <view class="title"><text space="ensp">体总指数:</text></view> | |||
| <input type="number" placeholder="请输入体总指数" v-model="model.bmi"/> | |||
| </view> | |||
| </view> | |||
| <view class="cu-form-group"> | |||
| <view class="flex align-center"> | |||
| <view class="title"><text space="ensp">脂肪:</text></view> | |||
| <input type="number" placeholder="请输入脂肪" v-model="model.fat"/> | |||
| </view> | |||
| </view> | |||
| <view class="cu-form-group"> | |||
| <view class="flex align-center"> | |||
| <view class="title"><text space="ensp">头像:</text></view> | |||
| <input placeholder="请输入头像" v-model="model.avatar"/> | |||
| </view> | |||
| </view> | |||
| <view class="padding"> | |||
| <button class="cu-btn block bg-blue margin-tb-sm lg" @click="onSubmit"> | |||
| <text v-if="loading" class="cuIcon-loading2 cuIconfont-spin"></text>提交 | |||
| </button> | |||
| </view> | |||
| </form> | |||
| </view> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| import myDate from '@/components/my-componets/my-date.vue' | |||
| export default { | |||
| name: "AppletUserForm", | |||
| components:{ myDate }, | |||
| props:{ | |||
| formData:{ | |||
| type:Object, | |||
| default:()=>{}, | |||
| required:false | |||
| } | |||
| }, | |||
| data(){ | |||
| return { | |||
| CustomBar: this.CustomBar, | |||
| NavBarColor: this.NavBarColor, | |||
| loading:false, | |||
| model: {}, | |||
| backRouteName:'index', | |||
| url: { | |||
| queryById: "/appletUser/appletUser/queryById", | |||
| add: "/appletUser/appletUser/add", | |||
| edit: "/appletUser/appletUser/edit", | |||
| }, | |||
| } | |||
| }, | |||
| created(){ | |||
| this.initFormData(); | |||
| }, | |||
| methods:{ | |||
| initFormData(){ | |||
| if(this.formData){ | |||
| let dataId = this.formData.dataId; | |||
| this.$http.get(this.url.queryById,{params:{id:dataId}}).then((res)=>{ | |||
| if(res.data.success){ | |||
| console.log("表单数据",res); | |||
| this.model = res.data.result; | |||
| } | |||
| }) | |||
| } | |||
| }, | |||
| onSubmit() { | |||
| let myForm = {...this.model}; | |||
| this.loading = true; | |||
| let url = myForm.id?this.url.edit:this.url.add; | |||
| this.$http.post(url,myForm).then(res=>{ | |||
| console.log("res",res) | |||
| this.loading = false | |||
| this.$Router.push({name:this.backRouteName}) | |||
| }).catch(()=>{ | |||
| this.loading = false | |||
| }); | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| @ -0,0 +1,44 @@ | |||
| <template> | |||
| <view> | |||
| <!--标题和返回--> | |||
| <cu-custom :bgColor="NavBarColor" isBack> | |||
| <block slot="backText">返回</block> | |||
| <block slot="content">用户</block> | |||
| </cu-custom> | |||
| <!--滚动加载列表--> | |||
| <mescroll-body ref="mescrollRef" bottom="88" @init="mescrollInit" :up="upOption" :down="downOption" @down="downCallback" @up="upCallback"> | |||
| <view class="cu-list menu"> | |||
| <view class="cu-item" v-for="(item,index) in list" :key="index" @click="goHome"> | |||
| <view class="flex" style="width:100%"> | |||
| <text class="text-lg" style="color: #000;"> | |||
| {{ item.createBy}} | |||
| </text> | |||
| </view> | |||
| </view> | |||
| </view> | |||
| </mescroll-body> | |||
| </view> | |||
| </template> | |||
| <script> | |||
| import MescrollMixin from "@/components/mescroll-uni/mescroll-mixins.js"; | |||
| import Mixin from "@/common/mixin/Mixin.js"; | |||
| export default { | |||
| name: '用户', | |||
| mixins: [MescrollMixin,Mixin], | |||
| data() { | |||
| return { | |||
| CustomBar:this.CustomBar, | |||
| NavBarColor:this.NavBarColor, | |||
| url: "/appletUser/appletUser/list", | |||
| }; | |||
| }, | |||
| methods: { | |||
| goHome(){ | |||
| this.$Router.push({name: "index"}) | |||
| } | |||
| } | |||
| } | |||
| </script> | |||
| @ -0,0 +1,34 @@ | |||
| import { render } from '@/common/renderUtils'; | |||
| //列表数据 | |||
| export const columns = [ | |||
| { | |||
| title: '昵称', | |||
| align:"center", | |||
| dataIndex: 'name' | |||
| }, | |||
| { | |||
| title: '第三方认证id', | |||
| align:"center", | |||
| dataIndex: 'openid' | |||
| }, | |||
| { | |||
| title: '手机号', | |||
| align:"center", | |||
| dataIndex: 'phone' | |||
| }, | |||
| { | |||
| title: '体总指数', | |||
| align:"center", | |||
| dataIndex: 'bmi' | |||
| }, | |||
| { | |||
| title: '脂肪', | |||
| align:"center", | |||
| dataIndex: 'fat' | |||
| }, | |||
| { | |||
| title: '头像', | |||
| align:"center", | |||
| dataIndex: 'avatar' | |||
| }, | |||
| ]; | |||
| @ -0,0 +1,276 @@ | |||
| <route lang="json5" type="page"> | |||
| { | |||
| layout: 'default', | |||
| style: { | |||
| navigationStyle: 'custom', | |||
| navigationBarTitleText: '用户', | |||
| }, | |||
| } | |||
| </route> | |||
| <template> | |||
| <PageLayout :navTitle="navTitle" :backRouteName="backRouteName"> | |||
| <scroll-view class="scrollArea" scroll-y> | |||
| <view class="form-container"> | |||
| <wd-form ref="form" :model="myFormData"> | |||
| <wd-cell-group border> | |||
| <view class="{ 'mt-14px': 0 == 0 }"> | |||
| <wd-input | |||
| label-width="100px" | |||
| v-model="myFormData['name']" | |||
| :label="get4Label('昵称')" | |||
| name='name' | |||
| prop='name' | |||
| placeholder="请选择昵称" | |||
| :rules="[ | |||
| ]" | |||
| clearable | |||
| /> | |||
| </view> | |||
| <view class="{ 'mt-14px': 1 == 0 }"> | |||
| <wd-input | |||
| label-width="100px" | |||
| v-model="myFormData['openid']" | |||
| :label="get4Label('第三方认证id')" | |||
| name='openid' | |||
| prop='openid' | |||
| placeholder="请选择第三方认证id" | |||
| :rules="[ | |||
| ]" | |||
| clearable | |||
| /> | |||
| </view> | |||
| <view class="{ 'mt-14px': 0 == 0 }"> | |||
| <wd-input | |||
| label-width="100px" | |||
| v-model="myFormData['phone']" | |||
| :label="get4Label('手机号')" | |||
| name='phone' | |||
| prop='phone' | |||
| placeholder="请选择手机号" | |||
| :rules="[ | |||
| ]" | |||
| clearable | |||
| /> | |||
| </view> | |||
| <view class="{ 'mt-14px': 1 == 0 }"> | |||
| <wd-input | |||
| label-width="100px" | |||
| v-model="myFormData['bmi']" | |||
| :label="get4Label('体总指数')" | |||
| name='bmi' | |||
| prop='bmi' | |||
| placeholder="请选择体总指数" | |||
| inputMode="numeric" | |||
| :rules="[ | |||
| ]" | |||
| clearable | |||
| /> | |||
| </view> | |||
| <view class="{ 'mt-14px': 0 == 0 }"> | |||
| <wd-input | |||
| label-width="100px" | |||
| v-model="myFormData['fat']" | |||
| :label="get4Label('脂肪')" | |||
| name='fat' | |||
| prop='fat' | |||
| placeholder="请选择脂肪" | |||
| inputMode="numeric" | |||
| :rules="[ | |||
| ]" | |||
| clearable | |||
| /> | |||
| </view> | |||
| <view class="{ 'mt-14px': 1 == 0 }"> | |||
| <wd-input | |||
| label-width="100px" | |||
| v-model="myFormData['avatar']" | |||
| :label="get4Label('头像')" | |||
| name='avatar' | |||
| prop='avatar' | |||
| placeholder="请选择头像" | |||
| :rules="[ | |||
| ]" | |||
| clearable | |||
| /> | |||
| </view> | |||
| </wd-cell-group> | |||
| </wd-form> | |||
| </view> | |||
| </scroll-view> | |||
| <view class="footer"> | |||
| <wd-button :disabled="loading" block :loading="loading" @click="handleSubmit">提交</wd-button> | |||
| </view> | |||
| </PageLayout> | |||
| </template> | |||
| <script lang="ts" setup> | |||
| import { onLoad } from '@dcloudio/uni-app' | |||
| import { http } from '@/utils/http' | |||
| import { useToast } from 'wot-design-uni' | |||
| import { useRouter } from '@/plugin/uni-mini-router' | |||
| import { ref, onMounted, computed,reactive } from 'vue' | |||
| import OnlineImage from '@/components/online/view/online-image.vue' | |||
| import OnlineFile from '@/components/online/view/online-file.vue' | |||
| import OnlineFileCustom from '@/components/online/view/online-file-custom.vue' | |||
| import OnlineSelect from '@/components/online/view/online-select.vue' | |||
| import OnlineTime from '@/components/online/view/online-time.vue' | |||
| import OnlineDate from '@/components/online/view/online-date.vue' | |||
| import OnlineRadio from '@/components/online/view/online-radio.vue' | |||
| import OnlineCheckbox from '@/components/online/view/online-checkbox.vue' | |||
| import OnlineMulti from '@/components/online/view/online-multi.vue' | |||
| import OnlinePopupLinkRecord from '@/components/online/view/online-popup-link-record.vue' | |||
| import OnlinePca from '@/components/online/view/online-pca.vue' | |||
| import SelectDept from '@/components/SelectDept/SelectDept.vue' | |||
| import SelectUser from '@/components/SelectUser/SelectUser.vue' | |||
| import {duplicateCheck} from "@/service/api"; | |||
| defineOptions({ | |||
| name: 'AppletUserForm', | |||
| options: { | |||
| styleIsolation: 'shared', | |||
| }, | |||
| }) | |||
| const toast = useToast() | |||
| const router = useRouter() | |||
| const form = ref(null) | |||
| // 定义响应式数据 | |||
| const myFormData = reactive({}) | |||
| const loading = ref(false) | |||
| const navTitle = ref('新增') | |||
| const dataId = ref('') | |||
| const backRouteName = ref('AppletUserList') | |||
| // 定义 initForm 方法 | |||
| const initForm = (item) => { | |||
| console.log('initForm item', item) | |||
| if(item?.dataId){ | |||
| dataId.value = item.dataId; | |||
| navTitle.value = item.dataId?'编辑':'新增'; | |||
| initData(); | |||
| } | |||
| } | |||
| // 初始化数据 | |||
| const initData = () => { | |||
| http.get("/appletUser/appletUser/queryById",{id:dataId.value}).then((res) => { | |||
| if (res.success) { | |||
| let obj = res.result | |||
| Object.assign(myFormData, { ...obj }) | |||
| }else{ | |||
| toast.error(res?.message || '表单加载失败!') | |||
| } | |||
| }) | |||
| } | |||
| const handleSuccess = () => { | |||
| uni.$emit('refreshList'); | |||
| router.back() | |||
| } | |||
| /** | |||
| * 校验唯一 | |||
| * @param values | |||
| * @returns {boolean} | |||
| */ | |||
| async function fieldCheck(values: any) { | |||
| const onlyField = [ | |||
| ]; | |||
| for (const field of onlyField) { | |||
| if (values[field]) { | |||
| // 仅校验有值的字段 | |||
| const res: any = await duplicateCheck({ | |||
| tableName: 'applet_user', | |||
| fieldName: field, // 使用处理后的字段名 | |||
| fieldVal: values[field], | |||
| dataId: values.id, | |||
| }); | |||
| if (!res.success) { | |||
| toast.warning(res.message); | |||
| return true; // 校验失败 | |||
| } | |||
| } | |||
| } | |||
| return false; // 校验通过 | |||
| } | |||
| // 提交表单 | |||
| const handleSubmit = async () => { | |||
| // 判断字段必填和正则 | |||
| if (await fieldCheck(myFormData)) { | |||
| return | |||
| } | |||
| let url = dataId.value?'/appletUser/appletUser/edit':'/appletUser/appletUser/add'; | |||
| form.value | |||
| .validate() | |||
| .then(({ valid, errors }) => { | |||
| if (valid) { | |||
| loading.value = true; | |||
| http.post(url,myFormData).then((res) => { | |||
| loading.value = false; | |||
| if (res.success) { | |||
| toast.success('保存成功'); | |||
| handleSuccess() | |||
| }else{ | |||
| toast.error(res?.message || '表单保存失败!') | |||
| } | |||
| }) | |||
| } | |||
| }) | |||
| .catch((error) => { | |||
| console.log(error, 'error') | |||
| loading.value = false; | |||
| }) | |||
| } | |||
| // 标题 | |||
| const get4Label = computed(() => { | |||
| return (label) => { | |||
| return label && label.length > 4 ? label.substring(0, 4) : label; | |||
| } | |||
| }) | |||
| // 标题 | |||
| const getFormSchema = computed(() => { | |||
| return (dictTable,dictCode,dictText) => { | |||
| return { | |||
| dictCode, | |||
| dictTable, | |||
| dictText | |||
| }; | |||
| } | |||
| }) | |||
| /** | |||
| * 获取日期控件的扩展类型 | |||
| * @param picker | |||
| * @returns {string} | |||
| */ | |||
| const getDateExtendType = (picker: string) => { | |||
| let mapField = { | |||
| month: 'year-month', | |||
| year: 'year', | |||
| quarter: 'quarter', | |||
| week: 'week', | |||
| day: 'date', | |||
| } | |||
| return picker && mapField[picker] | |||
| ? mapField[picker] | |||
| : 'date' | |||
| } | |||
| //设置pop返回值 | |||
| const setFieldsValue = (data) => { | |||
| Object.assign(myFormData, {...data }) | |||
| } | |||
| // onLoad 生命周期钩子 | |||
| onLoad((option) => { | |||
| initForm(option) | |||
| }) | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .footer { | |||
| width: 100%; | |||
| padding: 10px 20px; | |||
| padding-bottom: calc(constant(safe-area-inset-bottom) + 10px); | |||
| padding-bottom: calc(env(safe-area-inset-bottom) + 10px); | |||
| } | |||
| :deep(.wd-cell__label) { | |||
| font-size: 14px; | |||
| color: #444; | |||
| } | |||
| :deep(.wd-cell__value) { | |||
| text-align: left; | |||
| } | |||
| </style> | |||
| @ -0,0 +1,148 @@ | |||
| <route lang="json5" type="page"> | |||
| { | |||
| layout: 'default', | |||
| style: { | |||
| navigationBarTitleText: '用户', | |||
| navigationStyle: 'custom', | |||
| }, | |||
| } | |||
| </route> | |||
| <template> | |||
| <PageLayout navTitle="用户" backRouteName="index" routeMethod="pushTab"> | |||
| <view class="wrap"> | |||
| <z-paging | |||
| ref="paging" | |||
| :fixed="false" | |||
| v-model="dataList" | |||
| @query="queryList" | |||
| :default-page-size="15" | |||
| > | |||
| <template v-for="item in dataList" :key="item.id"> | |||
| <wd-swipe-action> | |||
| <view class="list" @click="handleEdit(item)"> | |||
| <template v-for="(cItem, cIndex) in columns" :key="cIndex"> | |||
| <view v-if="cIndex < 3" class="box" :style="getBoxStyle"> | |||
| <view class="field ellipsis">{{ cItem.title }}</view> | |||
| <view class="value cu-text-grey">{{ item[cItem.dataIndex] }}</view> | |||
| </view> | |||
| </template> | |||
| </view> | |||
| <template #right> | |||
| <view class="action"> | |||
| <view class="button" @click="handleAction('del', item)">删除</view> | |||
| </view> | |||
| </template> | |||
| </wd-swipe-action> | |||
| </template> | |||
| </z-paging> | |||
| <view class="add u-iconfont u-icon-add" @click="handleAdd"></view> | |||
| </view> | |||
| </PageLayout> | |||
| </template> | |||
| <script setup lang="ts"> | |||
| import { ref, onMounted, computed } from 'vue' | |||
| import { http } from '@/utils/http' | |||
| import usePageList from '@/hooks/usePageList' | |||
| import {columns} from './AppletUserData'; | |||
| defineOptions({ | |||
| name: 'AppletUserList', | |||
| options: { | |||
| styleIsolation: 'shared', | |||
| } | |||
| }) | |||
| //分页加载配置 | |||
| let { toast, router, paging, dataList, queryList } = usePageList('/appletUser/appletUser/list'); | |||
| //样式 | |||
| const getBoxStyle = computed(() => { | |||
| return { width: "calc(33% - 5px)" } | |||
| }) | |||
| // 其他操作 | |||
| const handleAction = (val, item) => { | |||
| if (val == 'del') { | |||
| http.delete("/appletUser/appletUser/delete?id="+item.id,{id:item.id}).then((res) => { | |||
| toast.success('删除成功~') | |||
| paging.value.reload() | |||
| }) | |||
| } | |||
| } | |||
| // go 新增页 | |||
| const handleAdd = () => { | |||
| router.push({ | |||
| name: 'AppletUserForm' | |||
| }) | |||
| } | |||
| //go 编辑页 | |||
| const handleEdit = (record) => { | |||
| router.push({ | |||
| name: 'AppletUserForm', | |||
| params: {dataId: record.id}, | |||
| }) | |||
| } | |||
| onMounted(() => { | |||
| // 监听刷新列表事件 | |||
| uni.$on('refreshList', () => { | |||
| queryList(1,10) | |||
| }) | |||
| }) | |||
| </script> | |||
| <style lang="scss" scoped> | |||
| .wrap { | |||
| height: 100%; | |||
| } | |||
| :deep(.wd-swipe-action) { | |||
| margin-top: 10px; | |||
| background-color: #fff; | |||
| } | |||
| .list { | |||
| padding: 10px 10px; | |||
| width: 100%; | |||
| text-align: left; | |||
| display: flex; | |||
| justify-content: space-between; | |||
| .box { | |||
| width: 33%; | |||
| .field { | |||
| margin-bottom: 10px; | |||
| line-height: 20px; | |||
| } | |||
| } | |||
| } | |||
| .action { | |||
| width: 60px; | |||
| height: 100%; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| .button { | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| flex: 1; | |||
| height: 100%; | |||
| color: #fff; | |||
| &:first-child { | |||
| background-color: #fa4350; | |||
| } | |||
| } | |||
| } | |||
| .add { | |||
| height: 70upx; | |||
| width: 70upx; | |||
| text-align: center; | |||
| line-height: 70upx; | |||
| background-color: #fff; | |||
| border-radius: 50%; | |||
| position: fixed; | |||
| bottom: 80upx; | |||
| right: 30upx; | |||
| box-shadow: 0 0 5px 2px rgba(0, 0, 0, 0.1); | |||
| color: #666; | |||
| } | |||
| </style> | |||
| @ -0,0 +1,72 @@ | |||
| import { defHttp } from '/@/utils/http/axios'; | |||
| import { useMessage } from "/@/hooks/web/useMessage"; | |||
| const { createConfirm } = useMessage(); | |||
| enum Api { | |||
| list = '/appletUser/appletUser/list', | |||
| save='/appletUser/appletUser/add', | |||
| edit='/appletUser/appletUser/edit', | |||
| deleteOne = '/appletUser/appletUser/delete', | |||
| deleteBatch = '/appletUser/appletUser/deleteBatch', | |||
| importExcel = '/appletUser/appletUser/importExcel', | |||
| exportXls = '/appletUser/appletUser/exportXls', | |||
| } | |||
| /** | |||
| * 导出api | |||
| * @param params | |||
| */ | |||
| export const getExportUrl = Api.exportXls; | |||
| /** | |||
| * 导入api | |||
| */ | |||
| export const getImportUrl = Api.importExcel; | |||
| /** | |||
| * 列表接口 | |||
| * @param params | |||
| */ | |||
| export const list = (params) => defHttp.get({ url: Api.list, params }); | |||
| /** | |||
| * 删除单个 | |||
| * @param params | |||
| * @param handleSuccess | |||
| */ | |||
| export const deleteOne = (params,handleSuccess) => { | |||
| return defHttp.delete({url: Api.deleteOne, params}, {joinParamsToUrl: true}).then(() => { | |||
| handleSuccess(); | |||
| }); | |||
| } | |||
| /** | |||
| * 批量删除 | |||
| * @param params | |||
| * @param handleSuccess | |||
| */ | |||
| export const batchDelete = (params, handleSuccess) => { | |||
| createConfirm({ | |||
| iconType: 'warning', | |||
| title: '确认删除', | |||
| content: '是否删除选中数据', | |||
| okText: '确认', | |||
| cancelText: '取消', | |||
| onOk: () => { | |||
| return defHttp.delete({url: Api.deleteBatch, data: params}, {joinParamsToUrl: true}).then(() => { | |||
| handleSuccess(); | |||
| }); | |||
| } | |||
| }); | |||
| } | |||
| /** | |||
| * 保存或者更新 | |||
| * @param params | |||
| * @param isUpdate | |||
| */ | |||
| export const saveOrUpdate = (params, isUpdate) => { | |||
| let url = isUpdate ? Api.edit : Api.save; | |||
| return defHttp.post({ url: url, params }, { isTransformResponse: false }); | |||
| } | |||
| @ -0,0 +1,48 @@ | |||
| import {BasicColumn} from '/@/components/Table'; | |||
| import {FormSchema} from '/@/components/Table'; | |||
| import { rules} from '/@/utils/helper/validator'; | |||
| import { render } from '/@/utils/common/renderUtils'; | |||
| import { getWeekMonthQuarterYear } from '/@/utils'; | |||
| //列表数据 | |||
| export const columns: BasicColumn[] = [ | |||
| { | |||
| title: '昵称', | |||
| align: "center", | |||
| dataIndex: 'name' | |||
| }, | |||
| { | |||
| title: '第三方认证id', | |||
| align: "center", | |||
| dataIndex: 'openid' | |||
| }, | |||
| { | |||
| title: '手机号', | |||
| align: "center", | |||
| dataIndex: 'phone' | |||
| }, | |||
| { | |||
| title: '体总指数', | |||
| align: "center", | |||
| dataIndex: 'bmi' | |||
| }, | |||
| { | |||
| title: '脂肪', | |||
| align: "center", | |||
| dataIndex: 'fat' | |||
| }, | |||
| { | |||
| title: '头像', | |||
| align: "center", | |||
| dataIndex: 'avatar' | |||
| }, | |||
| ]; | |||
| // 高级查询数据 | |||
| export const superQuerySchema = { | |||
| name: {title: '昵称',order: 0,view: 'text', type: 'string',}, | |||
| openid: {title: '第三方认证id',order: 1,view: 'text', type: 'string',}, | |||
| phone: {title: '手机号',order: 2,view: 'text', type: 'string',}, | |||
| bmi: {title: '体总指数',order: 3,view: 'number', type: 'number',}, | |||
| fat: {title: '脂肪',order: 4,view: 'number', type: 'number',}, | |||
| avatar: {title: '头像',order: 5,view: 'text', type: 'string',}, | |||
| }; | |||
| @ -0,0 +1,249 @@ | |||
| <template> | |||
| <div class="p-2"> | |||
| <!--查询区域--> | |||
| <div class="jeecg-basic-table-form-container"> | |||
| <a-form ref="formRef" @keyup.enter.native="searchQuery" :model="queryParam" :label-col="labelCol" :wrapper-col="wrapperCol"> | |||
| <a-row :gutter="24"> | |||
| </a-row> | |||
| </a-form> | |||
| </div> | |||
| <!--引用表格--> | |||
| <BasicTable @register="registerTable" :rowSelection="rowSelection"> | |||
| <!--插槽:table标题--> | |||
| <template #tableTitle> | |||
| <a-button type="primary" v-auth="'appletUser:applet_user:add'" @click="handleAdd" preIcon="ant-design:plus-outlined"> 新增</a-button> | |||
| <a-button type="primary" v-auth="'appletUser:applet_user:exportXls'" preIcon="ant-design:export-outlined" @click="onExportXls"> 导出</a-button> | |||
| <j-upload-button type="primary" v-auth="'appletUser:applet_user:importExcel'" preIcon="ant-design:import-outlined" @click="onImportXls">导入</j-upload-button> | |||
| <a-dropdown v-if="selectedRowKeys.length > 0"> | |||
| <template #overlay> | |||
| <a-menu> | |||
| <a-menu-item key="1" @click="batchHandleDelete"> | |||
| <Icon icon="ant-design:delete-outlined"></Icon> | |||
| 删除 | |||
| </a-menu-item> | |||
| </a-menu> | |||
| </template> | |||
| <a-button v-auth="'appletUser:applet_user:deleteBatch'">批量操作 | |||
| <Icon icon="mdi:chevron-down"></Icon> | |||
| </a-button> | |||
| </a-dropdown> | |||
| <!-- 高级查询 --> | |||
| <super-query :config="superQueryConfig" @search="handleSuperQuery" /> | |||
| </template> | |||
| <!--操作栏--> | |||
| <template #action="{ record }"> | |||
| <TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)"/> | |||
| </template> | |||
| <template v-slot:bodyCell="{ column, record, index, text }"> | |||
| </template> | |||
| </BasicTable> | |||
| <!-- 表单区域 --> | |||
| <AppletUserModal ref="registerModal" @success="handleSuccess"></AppletUserModal> | |||
| </div> | |||
| </template> | |||
| <script lang="ts" name="appletUser-appletUser" setup> | |||
| import { ref, reactive } from 'vue'; | |||
| import { BasicTable, useTable, TableAction } from '/@/components/Table'; | |||
| import { useListPage } from '/@/hooks/system/useListPage'; | |||
| import { columns, superQuerySchema } from './AppletUser.data'; | |||
| import { list, deleteOne, batchDelete, getImportUrl, getExportUrl } from './AppletUser.api'; | |||
| import { downloadFile } from '/@/utils/common/renderUtils'; | |||
| import AppletUserModal from './components/AppletUserModal.vue' | |||
| import { useUserStore } from '/@/store/modules/user'; | |||
| import { useMessage } from '/@/hooks/web/useMessage'; | |||
| import {useModal} from '/@/components/Modal'; | |||
| import { getDateByPicker } from '/@/utils'; | |||
| const fieldPickers = reactive({ | |||
| }); | |||
| const formRef = ref(); | |||
| const queryParam = reactive<any>({}); | |||
| const toggleSearchStatus = ref<boolean>(false); | |||
| const registerModal = ref(); | |||
| const userStore = useUserStore(); | |||
| const { createMessage } = useMessage(); | |||
| //注册table数据 | |||
| const { prefixCls, tableContext, onExportXls, onImportXls } = useListPage({ | |||
| tableProps: { | |||
| title: '用户', | |||
| api: list, | |||
| columns, | |||
| canResize:true, | |||
| useSearchForm: false, | |||
| actionColumn: { | |||
| width: 120, | |||
| fixed: 'right', | |||
| }, | |||
| beforeFetch: async (params) => { | |||
| for (let key in fieldPickers) { | |||
| if (queryParam[key] && fieldPickers[key]) { | |||
| queryParam[key] = getDateByPicker(queryParam[key], fieldPickers[key]); | |||
| } | |||
| } | |||
| return Object.assign(params, queryParam); | |||
| }, | |||
| }, | |||
| exportConfig: { | |||
| name: "用户", | |||
| url: getExportUrl, | |||
| params: queryParam, | |||
| }, | |||
| importConfig: { | |||
| url: getImportUrl, | |||
| success: handleSuccess | |||
| }, | |||
| }); | |||
| const [registerTable, { reload, collapseAll, updateTableDataRecord, findTableDataRecord, getDataSource }, { rowSelection, selectedRowKeys }] = tableContext; | |||
| const labelCol = reactive({ | |||
| xs:24, | |||
| sm:4, | |||
| xl:6, | |||
| xxl:4 | |||
| }); | |||
| const wrapperCol = reactive({ | |||
| xs: 24, | |||
| sm: 20, | |||
| }); | |||
| // 高级查询配置 | |||
| const superQueryConfig = reactive(superQuerySchema); | |||
| /** | |||
| * 高级查询事件 | |||
| */ | |||
| function handleSuperQuery(params) { | |||
| Object.keys(params).map((k) => { | |||
| queryParam[k] = params[k]; | |||
| }); | |||
| searchQuery(); | |||
| } | |||
| /** | |||
| * 新增事件 | |||
| */ | |||
| function handleAdd() { | |||
| registerModal.value.disableSubmit = false; | |||
| registerModal.value.add(); | |||
| } | |||
| /** | |||
| * 编辑事件 | |||
| */ | |||
| function handleEdit(record: Recordable) { | |||
| registerModal.value.disableSubmit = false; | |||
| registerModal.value.edit(record); | |||
| } | |||
| /** | |||
| * 详情 | |||
| */ | |||
| function handleDetail(record: Recordable) { | |||
| registerModal.value.disableSubmit = true; | |||
| registerModal.value.edit(record); | |||
| } | |||
| /** | |||
| * 删除事件 | |||
| */ | |||
| async function handleDelete(record) { | |||
| await deleteOne({ id: record.id }, handleSuccess); | |||
| } | |||
| /** | |||
| * 批量删除事件 | |||
| */ | |||
| async function batchHandleDelete() { | |||
| await batchDelete({ ids: selectedRowKeys.value }, handleSuccess); | |||
| } | |||
| /** | |||
| * 成功回调 | |||
| */ | |||
| function handleSuccess() { | |||
| (selectedRowKeys.value = []) && reload(); | |||
| } | |||
| /** | |||
| * 操作栏 | |||
| */ | |||
| function getTableAction(record) { | |||
| return [ | |||
| { | |||
| label: '编辑', | |||
| onClick: handleEdit.bind(null, record), | |||
| auth: 'appletUser:applet_user:edit' | |||
| }, | |||
| ]; | |||
| } | |||
| /** | |||
| * 下拉操作栏 | |||
| */ | |||
| function getDropDownAction(record) { | |||
| return [ | |||
| { | |||
| label: '详情', | |||
| onClick: handleDetail.bind(null, record), | |||
| }, { | |||
| label: '删除', | |||
| popConfirm: { | |||
| title: '是否确认删除', | |||
| confirm: handleDelete.bind(null, record), | |||
| placement: 'topLeft', | |||
| }, | |||
| auth: 'appletUser:applet_user:delete' | |||
| } | |||
| ] | |||
| } | |||
| /** | |||
| * 查询 | |||
| */ | |||
| function searchQuery() { | |||
| reload(); | |||
| } | |||
| /** | |||
| * 重置 | |||
| */ | |||
| function searchReset() { | |||
| formRef.value.resetFields(); | |||
| selectedRowKeys.value = []; | |||
| //刷新数据 | |||
| reload(); | |||
| } | |||
| </script> | |||
| <style lang="less" scoped> | |||
| .jeecg-basic-table-form-container { | |||
| padding: 0; | |||
| .table-page-search-submitButtons { | |||
| display: block; | |||
| margin-bottom: 24px; | |||
| white-space: nowrap; | |||
| } | |||
| .query-group-cust{ | |||
| min-width: 100px !important; | |||
| } | |||
| .query-group-split-cust{ | |||
| width: 30px; | |||
| display: inline-block; | |||
| text-align: center | |||
| } | |||
| .ant-form-item:not(.ant-form-item-with-help){ | |||
| margin-bottom: 16px; | |||
| height: 32px; | |||
| } | |||
| :deep(.ant-picker),:deep(.ant-input-number){ | |||
| width: 100%; | |||
| } | |||
| } | |||
| </style> | |||
| @ -0,0 +1,26 @@ | |||
| -- 注意:该页面对应的前台目录为views/appletUser文件夹下 | |||
| -- 如果你想更改到其他目录,请修改sql中component字段对应的值 | |||
| INSERT INTO sys_permission(id, parent_id, name, url, component, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_route, is_leaf, keep_alive, hidden, hide_tab, description, status, del_flag, rule_flag, create_by, create_time, update_by, update_time, internal_or_external) | |||
| VALUES ('2025071707392390060', NULL, '用户', '/appletUser/appletUserList', 'appletUser/AppletUserList', NULL, NULL, 0, NULL, '1', 0.00, 0, NULL, 1, 0, 0, 0, 0, NULL, '1', 0, 0, 'admin', '2025-07-17 19:39:06', NULL, NULL, 0); | |||
| -- 权限控制sql | |||
| -- 新增 | |||
| INSERT INTO sys_permission(id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) | |||
| VALUES ('2025071707392390061', '2025071707392390060', '添加用户', NULL, NULL, 0, NULL, NULL, 2, 'appletUser:applet_user:add', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2025-07-17 19:39:06', NULL, NULL, 0, 0, '1', 0); | |||
| -- 编辑 | |||
| INSERT INTO sys_permission(id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) | |||
| VALUES ('2025071707392390062', '2025071707392390060', '编辑用户', NULL, NULL, 0, NULL, NULL, 2, 'appletUser:applet_user:edit', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2025-07-17 19:39:06', NULL, NULL, 0, 0, '1', 0); | |||
| -- 删除 | |||
| INSERT INTO sys_permission(id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) | |||
| VALUES ('2025071707392390063', '2025071707392390060', '删除用户', NULL, NULL, 0, NULL, NULL, 2, 'appletUser:applet_user:delete', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2025-07-17 19:39:06', NULL, NULL, 0, 0, '1', 0); | |||
| -- 批量删除 | |||
| INSERT INTO sys_permission(id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) | |||
| VALUES ('2025071707392390064', '2025071707392390060', '批量删除用户', NULL, NULL, 0, NULL, NULL, 2, 'appletUser:applet_user:deleteBatch', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2025-07-17 19:39:06', NULL, NULL, 0, 0, '1', 0); | |||
| -- 导出excel | |||
| INSERT INTO sys_permission(id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) | |||
| VALUES ('2025071707392390065', '2025071707392390060', '导出excel_用户', NULL, NULL, 0, NULL, NULL, 2, 'appletUser:applet_user:exportXls', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2025-07-17 19:39:06', NULL, NULL, 0, 0, '1', 0); | |||
| -- 导入excel | |||
| INSERT INTO sys_permission(id, parent_id, name, url, component, is_route, component_name, redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_leaf, keep_alive, hidden, hide_tab, description, create_by, create_time, update_by, update_time, del_flag, rule_flag, status, internal_or_external) | |||
| VALUES ('2025071707392390066', '2025071707392390060', '导入excel_用户', NULL, NULL, 0, NULL, NULL, 2, 'appletUser:applet_user:importExcel', '1', NULL, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2025-07-17 19:39:06', NULL, NULL, 0, 0, '1', 0); | |||
| @ -0,0 +1,180 @@ | |||
| <template> | |||
| <a-spin :spinning="confirmLoading"> | |||
| <JFormContainer :disabled="disabled"> | |||
| <template #detail> | |||
| <a-form ref="formRef" class="antd-modal-form" :labelCol="labelCol" :wrapperCol="wrapperCol" name="AppletUserForm"> | |||
| <a-row> | |||
| <a-col :span="24"> | |||
| <a-form-item label="昵称" v-bind="validateInfos.name" id="AppletUserForm-name" name="name"> | |||
| <a-input v-model:value="formData.name" placeholder="请输入昵称" allow-clear ></a-input> | |||
| </a-form-item> | |||
| </a-col> | |||
| <a-col :span="24"> | |||
| <a-form-item label="第三方认证id" v-bind="validateInfos.openid" id="AppletUserForm-openid" name="openid"> | |||
| <a-input v-model:value="formData.openid" placeholder="请输入第三方认证id" allow-clear ></a-input> | |||
| </a-form-item> | |||
| </a-col> | |||
| <a-col :span="24"> | |||
| <a-form-item label="手机号" v-bind="validateInfos.phone" id="AppletUserForm-phone" name="phone"> | |||
| <a-input v-model:value="formData.phone" placeholder="请输入手机号" allow-clear ></a-input> | |||
| </a-form-item> | |||
| </a-col> | |||
| <a-col :span="24"> | |||
| <a-form-item label="体总指数" v-bind="validateInfos.bmi" id="AppletUserForm-bmi" name="bmi"> | |||
| <a-input-number v-model:value="formData.bmi" placeholder="请输入体总指数" style="width: 100%" /> | |||
| </a-form-item> | |||
| </a-col> | |||
| <a-col :span="24"> | |||
| <a-form-item label="脂肪" v-bind="validateInfos.fat" id="AppletUserForm-fat" name="fat"> | |||
| <a-input-number v-model:value="formData.fat" placeholder="请输入脂肪" style="width: 100%" /> | |||
| </a-form-item> | |||
| </a-col> | |||
| <a-col :span="24"> | |||
| <a-form-item label="头像" v-bind="validateInfos.avatar" id="AppletUserForm-avatar" name="avatar"> | |||
| <a-input v-model:value="formData.avatar" placeholder="请输入头像" allow-clear ></a-input> | |||
| </a-form-item> | |||
| </a-col> | |||
| </a-row> | |||
| </a-form> | |||
| </template> | |||
| </JFormContainer> | |||
| </a-spin> | |||
| </template> | |||
| <script lang="ts" setup> | |||
| import { ref, reactive, defineExpose, nextTick, defineProps, computed, onMounted } from 'vue'; | |||
| import { defHttp } from '/@/utils/http/axios'; | |||
| import { useMessage } from '/@/hooks/web/useMessage'; | |||
| import { getDateByPicker, getValueType } from '/@/utils'; | |||
| import { saveOrUpdate } from '../AppletUser.api'; | |||
| import { Form } from 'ant-design-vue'; | |||
| import JFormContainer from '/@/components/Form/src/container/JFormContainer.vue'; | |||
| const props = defineProps({ | |||
| formDisabled: { type: Boolean, default: false }, | |||
| formData: { type: Object, default: () => ({})}, | |||
| formBpm: { type: Boolean, default: true } | |||
| }); | |||
| const formRef = ref(); | |||
| const useForm = Form.useForm; | |||
| const emit = defineEmits(['register', 'ok']); | |||
| const formData = reactive<Record<string, any>>({ | |||
| id: '', | |||
| name: '', | |||
| openid: '', | |||
| phone: '', | |||
| bmi: undefined, | |||
| fat: undefined, | |||
| avatar: '', | |||
| }); | |||
| const { createMessage } = useMessage(); | |||
| const labelCol = ref<any>({ xs: { span: 24 }, sm: { span: 5 } }); | |||
| const wrapperCol = ref<any>({ xs: { span: 24 }, sm: { span: 16 } }); | |||
| const confirmLoading = ref<boolean>(false); | |||
| //表单验证 | |||
| const validatorRules = reactive({ | |||
| }); | |||
| const { resetFields, validate, validateInfos } = useForm(formData, validatorRules, { immediate: false }); | |||
| //日期个性化选择 | |||
| const fieldPickers = reactive({ | |||
| }); | |||
| // 表单禁用 | |||
| const disabled = computed(()=>{ | |||
| if(props.formBpm === true){ | |||
| if(props.formData.disabled === false){ | |||
| return false; | |||
| }else{ | |||
| return true; | |||
| } | |||
| } | |||
| return props.formDisabled; | |||
| }); | |||
| /** | |||
| * 新增 | |||
| */ | |||
| function add() { | |||
| edit({}); | |||
| } | |||
| /** | |||
| * 编辑 | |||
| */ | |||
| function edit(record) { | |||
| nextTick(() => { | |||
| resetFields(); | |||
| const tmpData = {}; | |||
| Object.keys(formData).forEach((key) => { | |||
| if(record.hasOwnProperty(key)){ | |||
| tmpData[key] = record[key] | |||
| } | |||
| }) | |||
| //赋值 | |||
| Object.assign(formData, tmpData); | |||
| }); | |||
| } | |||
| /** | |||
| * 提交数据 | |||
| */ | |||
| async function submitForm() { | |||
| try { | |||
| // 触发表单验证 | |||
| await validate(); | |||
| } catch ({ errorFields }) { | |||
| if (errorFields) { | |||
| const firstField = errorFields[0]; | |||
| if (firstField) { | |||
| formRef.value.scrollToField(firstField.name, { behavior: 'smooth', block: 'center' }); | |||
| } | |||
| } | |||
| return Promise.reject(errorFields); | |||
| } | |||
| confirmLoading.value = true; | |||
| const isUpdate = ref<boolean>(false); | |||
| //时间格式化 | |||
| let model = formData; | |||
| if (model.id) { | |||
| isUpdate.value = true; | |||
| } | |||
| //循环数据 | |||
| for (let data in model) { | |||
| // 更新个性化日期选择器的值 | |||
| model[data] = getDateByPicker(model[data], fieldPickers[data]); | |||
| //如果该数据是数组并且是字符串类型 | |||
| if (model[data] instanceof Array) { | |||
| let valueType = getValueType(formRef.value.getProps, data); | |||
| //如果是字符串类型的需要变成以逗号分割的字符串 | |||
| if (valueType === 'string') { | |||
| model[data] = model[data].join(','); | |||
| } | |||
| } | |||
| } | |||
| await saveOrUpdate(model, isUpdate.value) | |||
| .then((res) => { | |||
| if (res.success) { | |||
| createMessage.success(res.message); | |||
| emit('ok'); | |||
| } else { | |||
| createMessage.warning(res.message); | |||
| } | |||
| }) | |||
| .finally(() => { | |||
| confirmLoading.value = false; | |||
| }); | |||
| } | |||
| defineExpose({ | |||
| add, | |||
| edit, | |||
| submitForm, | |||
| }); | |||
| </script> | |||
| <style lang="less" scoped> | |||
| .antd-modal-form { | |||
| padding: 14px; | |||
| } | |||
| </style> | |||
| @ -0,0 +1,81 @@ | |||
| <template> | |||
| <j-modal :title="title" :width="width" :visible="visible" @ok="handleOk" :okButtonProps="{ class: { 'jee-hidden': disableSubmit } }" @cancel="handleCancel" cancelText="关闭"> | |||
| <AppletUserForm ref="registerForm" @ok="submitCallback" :formDisabled="disableSubmit" :formBpm="false"></AppletUserForm> | |||
| <template #footer> | |||
| <a-button @click="handleCancel">取消</a-button> | |||
| <a-button :class="{ 'jee-hidden': disableSubmit }" type="primary" @click="handleOk">确认</a-button> | |||
| </template> | |||
| </j-modal> | |||
| </template> | |||
| <script lang="ts" setup> | |||
| import { ref, nextTick, defineExpose } from 'vue'; | |||
| import AppletUserForm from './AppletUserForm.vue' | |||
| import JModal from '/@/components/Modal/src/JModal/JModal.vue'; | |||
| import { useMessage } from '/@/hooks/web/useMessage'; | |||
| const { createMessage } = useMessage(); | |||
| const title = ref<string>(''); | |||
| const width = ref<number>(800); | |||
| const visible = ref<boolean>(false); | |||
| const disableSubmit = ref<boolean>(false); | |||
| const registerForm = ref(); | |||
| const emit = defineEmits(['register', 'success']); | |||
| /** | |||
| * 新增 | |||
| */ | |||
| function add() { | |||
| title.value = '新增'; | |||
| visible.value = true; | |||
| nextTick(() => { | |||
| registerForm.value.add(); | |||
| }); | |||
| } | |||
| /** | |||
| * 编辑 | |||
| * @param record | |||
| */ | |||
| function edit(record) { | |||
| title.value = disableSubmit.value ? '详情' : '编辑'; | |||
| visible.value = true; | |||
| nextTick(() => { | |||
| registerForm.value.edit(record); | |||
| }); | |||
| } | |||
| /** | |||
| * 确定按钮点击事件 | |||
| */ | |||
| function handleOk() { | |||
| registerForm.value.submitForm(); | |||
| } | |||
| /** | |||
| * form保存回调事件 | |||
| */ | |||
| function submitCallback() { | |||
| handleCancel(); | |||
| emit('success'); | |||
| } | |||
| /** | |||
| * 取消按钮回调事件 | |||
| */ | |||
| function handleCancel() { | |||
| visible.value = false; | |||
| } | |||
| defineExpose({ | |||
| add, | |||
| edit, | |||
| disableSubmit, | |||
| }); | |||
| </script> | |||
| <style lang="less"> | |||
| /**隐藏样式-modal确定按钮 */ | |||
| .jee-hidden { | |||
| display: none !important; | |||
| } | |||
| </style> | |||
| <style lang="less" scoped></style> | |||
| @ -0,0 +1,264 @@ | |||
| package org.jeecg.modules.common.wxUtils; | |||
| import lombok.extern.slf4j.Slf4j; | |||
| import org.apache.http.HttpStatus; | |||
| import org.apache.http.client.config.RequestConfig; | |||
| 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.conn.ssl.SSLConnectionSocketFactory; | |||
| import org.apache.http.conn.ssl.SSLContextBuilder; | |||
| import org.apache.http.conn.ssl.TrustStrategy; | |||
| import org.apache.http.entity.StringEntity; | |||
| import org.apache.http.impl.client.CloseableHttpClient; | |||
| import org.apache.http.impl.client.DefaultHttpRequestRetryHandler; | |||
| import org.apache.http.impl.client.HttpClients; | |||
| import org.apache.http.util.EntityUtils; | |||
| import javax.net.ssl.SSLContext; | |||
| import java.io.IOException; | |||
| import java.net.URI; | |||
| import java.security.KeyManagementException; | |||
| import java.security.KeyStoreException; | |||
| import java.security.NoSuchAlgorithmException; | |||
| import java.security.cert.CertificateException; | |||
| import java.security.cert.X509Certificate; | |||
| import java.util.Map; | |||
| /** | |||
| * 微信API专用HTTP客户端工具类 | |||
| * 具有超时配置、重试机制和异常处理 | |||
| * | |||
| * @author system | |||
| * @date 2025-01-25 | |||
| */ | |||
| @Slf4j | |||
| public class WxHttpClientUtil { | |||
| // 超时配置常量 | |||
| private static final int CONNECTION_REQUEST_TIMEOUT = 10000; // 10秒 | |||
| private static final int CONNECT_TIMEOUT = 15000; // 15秒 | |||
| private static final int SOCKET_TIMEOUT = 30000; // 30秒 | |||
| private static final int MAX_RETRY_COUNT = 3; // 最大重试次数 | |||
| /** | |||
| * 创建带超时配置的SSL客户端 | |||
| */ | |||
| private static CloseableHttpClient createWxHttpClient() { | |||
| try { | |||
| // SSL配置 - 信任所有证书 | |||
| SSLContext sslContext = new SSLContextBuilder() | |||
| .loadTrustMaterial(null, new TrustStrategy() { | |||
| @Override | |||
| public boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException { | |||
| return true; | |||
| } | |||
| }).build(); | |||
| SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory( | |||
| sslContext, | |||
| SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER | |||
| ); | |||
| // 请求配置 | |||
| RequestConfig requestConfig = RequestConfig.custom() | |||
| .setConnectionRequestTimeout(CONNECTION_REQUEST_TIMEOUT) | |||
| .setConnectTimeout(CONNECT_TIMEOUT) | |||
| .setSocketTimeout(SOCKET_TIMEOUT) | |||
| .build(); | |||
| // 重试处理器 | |||
| DefaultHttpRequestRetryHandler retryHandler = new DefaultHttpRequestRetryHandler(MAX_RETRY_COUNT, true); | |||
| return HttpClients.custom() | |||
| .setSSLSocketFactory(sslsf) | |||
| .setDefaultRequestConfig(requestConfig) | |||
| .setRetryHandler(retryHandler) | |||
| .build(); | |||
| } catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException e) { | |||
| log.error("创建SSL客户端失败: {}", e.getMessage(), e); | |||
| // 返回默认客户端作为后备 | |||
| return HttpClients.createDefault(); | |||
| } | |||
| } | |||
| /** | |||
| * 执行GET请求(微信API专用) | |||
| * @param url 请求URL | |||
| * @param params 请求参数 | |||
| * @return 响应字符串 | |||
| */ | |||
| public static String doGet(String url, Map<String, String> params) { | |||
| return doGetWithRetry(url, params, 0); | |||
| } | |||
| /** | |||
| * 执行GET请求(微信API专用) | |||
| * @param url 请求URL | |||
| * @return 响应字符串 | |||
| */ | |||
| public static String doGet(String url) { | |||
| return doGet(url, null); | |||
| } | |||
| /** | |||
| * 带重试机制的GET请求 | |||
| */ | |||
| private static String doGetWithRetry(String url, Map<String, String> params, int retryCount) { | |||
| CloseableHttpClient httpClient = null; | |||
| CloseableHttpResponse response = null; | |||
| try { | |||
| log.info("开始请求微信API: {}, 重试次数: {}", url, retryCount); | |||
| httpClient = createWxHttpClient(); | |||
| // 构建URI | |||
| URIBuilder builder = new URIBuilder(url); | |||
| if (params != null) { | |||
| for (Map.Entry<String, String> entry : params.entrySet()) { | |||
| builder.addParameter(entry.getKey(), entry.getValue()); | |||
| } | |||
| } | |||
| URI uri = builder.build(); | |||
| // 创建GET请求 | |||
| HttpGet httpGet = new HttpGet(uri); | |||
| httpGet.setHeader("User-Agent", "WxHttpClient/1.0"); | |||
| httpGet.setHeader("Accept", "application/json, text/plain, */*"); | |||
| // 执行请求 | |||
| response = httpClient.execute(httpGet); | |||
| // 检查响应状态 | |||
| int statusCode = response.getStatusLine().getStatusCode(); | |||
| if (statusCode == HttpStatus.SC_OK) { | |||
| String result = EntityUtils.toString(response.getEntity(), "UTF-8"); | |||
| log.info("微信API请求成功: {}", url); | |||
| return result; | |||
| } else { | |||
| log.warn("微信API返回非200状态码: {}, URL: {}", statusCode, url); | |||
| throw new RuntimeException("HTTP状态码异常: " + statusCode); | |||
| } | |||
| } catch (Exception e) { | |||
| log.error("微信API请求失败: {}, 错误: {}, 重试次数: {}", url, e.getMessage(), retryCount); | |||
| // 如果还有重试机会,进行重试 | |||
| if (retryCount < MAX_RETRY_COUNT) { | |||
| log.info("准备进行第{}次重试...", retryCount + 1); | |||
| try { | |||
| Thread.sleep(1000 * (retryCount + 1)); // 递增延迟 | |||
| } catch (InterruptedException ie) { | |||
| Thread.currentThread().interrupt(); | |||
| } | |||
| return doGetWithRetry(url, params, retryCount + 1); | |||
| } | |||
| // 重试次数用尽,抛出异常 | |||
| throw new RuntimeException("微信API请求失败,已重试" + MAX_RETRY_COUNT + "次: " + e.getMessage(), e); | |||
| } finally { | |||
| // 关闭资源 | |||
| if (response != null) { | |||
| try { | |||
| response.close(); | |||
| } catch (IOException e) { | |||
| log.error("关闭响应失败: {}", e.getMessage()); | |||
| } | |||
| } | |||
| if (httpClient != null) { | |||
| try { | |||
| httpClient.close(); | |||
| } catch (IOException e) { | |||
| log.error("关闭客户端失败: {}", e.getMessage()); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| /** | |||
| * 执行POST请求(微信API专用) | |||
| * @param url 请求URL | |||
| * @param jsonBody JSON请求体 | |||
| * @return 响应字符串 | |||
| */ | |||
| public static String doPost(String url, String jsonBody) { | |||
| return doPostWithRetry(url, jsonBody, 0); | |||
| } | |||
| /** | |||
| * 带重试机制的POST请求 | |||
| */ | |||
| private static String doPostWithRetry(String url, String jsonBody, int retryCount) { | |||
| CloseableHttpClient httpClient = null; | |||
| CloseableHttpResponse response = null; | |||
| try { | |||
| log.info("开始POST请求微信API: {}, 重试次数: {}", url, retryCount); | |||
| httpClient = createWxHttpClient(); | |||
| // 创建POST请求 | |||
| HttpPost httpPost = new HttpPost(url); | |||
| httpPost.setHeader("Content-Type", "application/json; charset=UTF-8"); | |||
| httpPost.setHeader("User-Agent", "WxHttpClient/1.0"); | |||
| // 设置请求体 | |||
| if (jsonBody != null) { | |||
| StringEntity entity = new StringEntity(jsonBody, "UTF-8"); | |||
| httpPost.setEntity(entity); | |||
| } | |||
| // 执行请求 | |||
| response = httpClient.execute(httpPost); | |||
| // 检查响应状态 | |||
| int statusCode = response.getStatusLine().getStatusCode(); | |||
| if (statusCode == HttpStatus.SC_OK) { | |||
| String result = EntityUtils.toString(response.getEntity(), "UTF-8"); | |||
| log.info("微信API POST请求成功: {}", url); | |||
| return result; | |||
| } else { | |||
| log.warn("微信API POST返回非200状态码: {}, URL: {}", statusCode, url); | |||
| throw new RuntimeException("HTTP状态码异常: " + statusCode); | |||
| } | |||
| } catch (Exception e) { | |||
| log.error("微信API POST请求失败: {}, 错误: {}, 重试次数: {}", url, e.getMessage(), retryCount); | |||
| // 如果还有重试机会,进行重试 | |||
| if (retryCount < MAX_RETRY_COUNT) { | |||
| log.info("准备进行第{}次重试...", retryCount + 1); | |||
| try { | |||
| Thread.sleep(1000 * (retryCount + 1)); // 递增延迟 | |||
| } catch (InterruptedException ie) { | |||
| Thread.currentThread().interrupt(); | |||
| } | |||
| return doPostWithRetry(url, jsonBody, retryCount + 1); | |||
| } | |||
| // 重试次数用尽,抛出异常 | |||
| throw new RuntimeException("微信API POST请求失败,已重试" + MAX_RETRY_COUNT + "次: " + e.getMessage(), e); | |||
| } finally { | |||
| // 关闭资源 | |||
| if (response != null) { | |||
| try { | |||
| response.close(); | |||
| } catch (IOException e) { | |||
| log.error("关闭响应失败: {}", e.getMessage()); | |||
| } | |||
| } | |||
| if (httpClient != null) { | |||
| try { | |||
| httpClient.close(); | |||
| } catch (IOException e) { | |||
| log.error("关闭客户端失败: {}", e.getMessage()); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @ -0,0 +1,96 @@ | |||
| package org.jeecg.modules.common.wxUtils; | |||
| import com.alibaba.fastjson.JSON; | |||
| import com.alibaba.fastjson.JSONObject; | |||
| import com.alibaba.fastjson.TypeReference; | |||
| import org.springframework.beans.factory.annotation.Value; | |||
| import org.springframework.stereotype.Component; | |||
| import java.io.BufferedReader; | |||
| import java.io.DataOutputStream; | |||
| import java.io.InputStreamReader; | |||
| import java.net.HttpURLConnection; | |||
| import java.net.URL; | |||
| import java.nio.charset.StandardCharsets; | |||
| import java.util.Map; | |||
| @Component | |||
| public class WxHttpUtils { | |||
| @Value("${wechat.mpAppId}") | |||
| private String appid; | |||
| @Value("${wechat.mpAppSecret}") | |||
| private String secret;// | |||
| @Value("${wechat.merchantId}") | |||
| private String mchId;// | |||
| private static String shipmentUrl = "https://api.weixin.qq.com/wxa/sec/order/upload_shipping_info?access_token="; | |||
| private static final String GET_USER_PHONE_NUMBER = "https://api.weixin.qq.com/wxa/business/getuserphonenumber"; | |||
| /** | |||
| * 获取appid | |||
| */ | |||
| public String getAppid() { | |||
| return appid; | |||
| } | |||
| /** | |||
| * 获取secret | |||
| */ | |||
| public String getSecret() { | |||
| return secret; | |||
| } | |||
| /** | |||
| * 获取令牌 | |||
| * | |||
| * @return | |||
| */ | |||
| public String getAccessToken() { | |||
| String requestUrl = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" + appid + "&secret=" + secret; | |||
| try { | |||
| // 使用增强版HTTP客户端,具有超时配置和重试机制 | |||
| String response = WxHttpClientUtil.doGet(requestUrl); | |||
| Map<String, String> map = JSON.parseObject(response, new TypeReference<Map<String, String>>() {}); | |||
| String accessToken = map.get("access_token"); | |||
| if (accessToken == null || accessToken.isEmpty()) { | |||
| throw new RuntimeException("获取access_token失败,响应: " + response); | |||
| } | |||
| return accessToken; | |||
| } catch (Exception e) { | |||
| throw new RuntimeException("获取微信access_token失败: " + e.getMessage(), e); | |||
| } | |||
| } | |||
| public String getPhoneNumber(String code) throws Exception { | |||
| URL url = new URL(GET_USER_PHONE_NUMBER + "?access_token=" + this.getAccessToken()); | |||
| HttpURLConnection conn = (HttpURLConnection) url.openConnection(); | |||
| conn.setRequestMethod("POST"); | |||
| conn.setRequestProperty("Content-Type", "application/json; utf-8"); | |||
| conn.setRequestProperty("Accept", "application/json"); | |||
| conn.setDoOutput(true); | |||
| JSONObject jsonInput = new JSONObject(); | |||
| jsonInput.put("code", code); | |||
| try (DataOutputStream os = new DataOutputStream(conn.getOutputStream())) { | |||
| byte[] input = jsonInput.toString().getBytes(StandardCharsets.UTF_8); | |||
| os.write(input, 0, input.length); | |||
| } | |||
| try (BufferedReader br = new BufferedReader( | |||
| new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) { | |||
| StringBuilder response = new StringBuilder(); | |||
| String responseLine; | |||
| while ((responseLine = br.readLine()) != null) { | |||
| response.append(responseLine.trim()); | |||
| } | |||
| //获取手机号码 | |||
| return response.toString(); | |||
| } | |||
| } | |||
| } | |||
| @ -1,28 +0,0 @@ | |||
| package org.jeecg.modules.healthApi; | |||
| import lombok.extern.slf4j.Slf4j; | |||
| import org.jeecg.common.api.vo.Result; | |||
| import org.jeecg.config.shiro.IgnoreAuth; | |||
| import org.springframework.web.bind.annotation.GetMapping; | |||
| import org.springframework.web.bind.annotation.RequestMapping; | |||
| import org.springframework.web.bind.annotation.RestController; | |||
| /** | |||
| * 健康管理小程序模块控制器 | |||
| * | |||
| * @author jeecg | |||
| * @since 2024-01-01 | |||
| */ | |||
| @RestController | |||
| @RequestMapping("/applet/health") | |||
| @Slf4j | |||
| public class HealthManagerController { | |||
| /** | |||
| * 健康检查接口 | |||
| */ | |||
| @GetMapping("/health") | |||
| public Result<String> health() { | |||
| return Result.OK("健康管理小程序模块运行正常"); | |||
| } | |||
| } | |||