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