| @ -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("健康管理小程序模块运行正常"); | |||||
| } | |||||
| } | |||||