Browse Source

实现登录(待测试登录是否成功)

master
前端-胡立永 1 week ago
parent
commit
52d56c1ee2
38 changed files with 4021 additions and 47 deletions
  1. +2
    -0
      .idea/compiler.xml
  2. +1
    -0
      .idea/encodings.xml
  3. +1
    -1
      .idea/misc.xml
  4. +144
    -0
      README-Applet-Filter.md
  5. +216
    -0
      README-Applet-Token-Config.md
  6. +234
    -0
      README-AppletUser-Config.md
  7. +169
    -0
      jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/system/vo/AppletUser.java
  8. +26
    -15
      jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/Swagger3Config.java
  9. +141
    -0
      jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/AppletJwtFilter.java
  10. +28
    -0
      jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/AppletJwtToken.java
  11. +198
    -0
      jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/AppletShiroRealm.java
  12. +31
    -3
      jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/ShiroConfig.java
  13. +272
    -0
      jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/README-Applet-Module.md
  14. +105
    -0
      jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/controller/AppletLoginController.java
  15. +100
    -0
      jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/controller/AppletUserInfoController.java
  16. +183
    -0
      jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/service/AppletLoginService.java
  17. +136
    -0
      jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/service/AppletUserService.java
  18. +172
    -0
      jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/service/WxAppletService.java
  19. +165
    -0
      jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/appletBackground/appletUser/controller/AppletUserController.java
  20. +14
    -0
      jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/appletBackground/appletUser/mapper/AppletUserMapper.java
  21. +5
    -0
      jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/appletBackground/appletUser/mapper/xml/AppletUserMapper.xml
  22. +14
    -0
      jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/appletBackground/appletUser/service/IAppletUserService.java
  23. +19
    -0
      jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/appletBackground/appletUser/service/impl/AppletUserServiceImpl.java
  24. +113
    -0
      jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/appletBackground/appletUser/uniapp/AppletUserForm.vue
  25. +44
    -0
      jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/appletBackground/appletUser/uniapp/AppletUserList.vue
  26. +34
    -0
      jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/appletBackground/appletUser/uniapp3/AppletUserData.ts
  27. +276
    -0
      jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/appletBackground/appletUser/uniapp3/AppletUserForm.vue
  28. +148
    -0
      jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/appletBackground/appletUser/uniapp3/AppletUserList.vue
  29. +72
    -0
      jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/appletBackground/appletUser/vue3Native/AppletUser.api.ts
  30. +48
    -0
      jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/appletBackground/appletUser/vue3Native/AppletUser.data.ts
  31. +249
    -0
      jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/appletBackground/appletUser/vue3Native/AppletUserList.vue
  32. +26
    -0
      jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/appletBackground/appletUser/vue3Native/V20250717_1__menu_insert_AppletUser.sql
  33. +180
    -0
      jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/appletBackground/appletUser/vue3Native/components/AppletUserForm.vue
  34. +81
    -0
      jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/appletBackground/appletUser/vue3Native/components/AppletUserModal.vue
  35. +264
    -0
      jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/common/wxUtils/WxHttpClientUtil.java
  36. +96
    -0
      jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/common/wxUtils/WxHttpUtils.java
  37. +0
    -28
      jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/healthApi/HealthManagerController.java
  38. +14
    -0
      jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-dev.yml

+ 2
- 0
.idea/compiler.xml View File

@ -8,6 +8,7 @@
<outputRelativeToContentRoot value="true" />
<module name="jeecg-boot-module-airag" />
<module name="jeecg-system-cloud-api" />
<module name="jeecgboot-boot-applet" />
<module name="jeecg-boot-base-core" />
<module name="jeecg-system-biz" />
<module name="jeecg-system-start" />
@ -29,6 +30,7 @@
<module name="jeecg-system-cloud-api" options="-parameters" />
<module name="jeecg-system-local-api" options="-parameters" />
<module name="jeecg-system-start" options="-parameters" />
<module name="jeecgboot-boot-applet" options="-parameters" />
</option>
</component>
</project>

+ 1
- 0
.idea/encodings.xml View File

@ -7,6 +7,7 @@
<file url="file://$PROJECT_DIR$/jeecg-boot/jeecg-boot-module/jeecg-boot-module-airag/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/jeecg-boot/jeecg-boot-module/jeecg-module-demo/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/jeecg-boot/jeecg-boot-module/jeecg-module-demo/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/jeecg-boot/jeecg-boot-module/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/jeecg-boot/jeecg-boot-module/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/jeecg-boot/jeecg-module-system/jeecg-system-api/jeecg-system-cloud-api/src/main/java" charset="UTF-8" />


+ 1
- 1
.idea/misc.xml View File

@ -8,7 +8,7 @@
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="corretto-21" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_20" project-jdk-name="17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

+ 144
- 0
README-Applet-Filter.md View File

@ -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接口文档"

+ 216
- 0
README-Applet-Token-Config.md View File

@ -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. **不同的用户体系**:可以为小程序配置独立的用户表和管理逻辑

+ 234
- 0
README-AppletUser-Config.md View File

@ -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. **实现会员系统**:实现会员等级和积分系统

+ 169
- 0
jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/system/vo/AppletUser.java View File

@ -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;
}

+ 26
- 15
jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/Swagger3Config.java View File

@ -63,24 +63,35 @@ public class Swagger3Config implements WebMvcConfigurer {
@Bean
public GlobalOpenApiCustomizer globalOpenApiCustomizer() {
return openApi -> {
// 全局添加鉴权参数
// 只显示以/applet开头的接口
if (openApi.getPaths() != null) {
// 创建一个新的Paths对象来存储过滤后的路径
io.swagger.v3.oas.models.Paths filteredPaths = new io.swagger.v3.oas.models.Paths();
openApi.getPaths().forEach((path, pathItem) -> {
//log.debug("path: {}", path);
// 检查当前路径是否在排除列表中
boolean isExcluded = excludedPaths.stream().anyMatch(excludedPath ->
excludedPath.equals(path) ||
(excludedPath.endsWith("**") && path.startsWith(excludedPath.substring(0, excludedPath.length() - 2)))
);
// 只保留以/applet开头的路径
if (path.startsWith("/applet")) {
// 检查当前路径是否在排除列表中
boolean isExcluded = excludedPaths.stream().anyMatch(excludedPath ->
excludedPath.equals(path) ||
(excludedPath.endsWith("**") && path.startsWith(excludedPath.substring(0, excludedPath.length() - 2)))
);
if (!isExcluded) {
// 接口添加鉴权参数
pathItem.readOperations()
.forEach(operation ->
operation.addSecurityItem(new SecurityRequirement().addList(CommonConstant.X_ACCESS_TOKEN))
);
if (!isExcluded) {
// 接口添加鉴权参数
pathItem.readOperations()
.forEach(operation ->
operation.addSecurityItem(new SecurityRequirement().addList(CommonConstant.X_ACCESS_TOKEN))
);
}
// 将过滤后的路径添加到新的Paths对象中
filteredPaths.put(path, pathItem);
}
});
// 用过滤后的Paths替换原来的Paths
openApi.setPaths(filteredPaths);
}
};
}
@ -89,10 +100,10 @@ public class Swagger3Config implements WebMvcConfigurer {
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("JeecgBoot 后台服务API接口文档")
.title("JeecgBoot 小程序API接口文档")
.version("3.8.1")
.contact(new Contact().name("北京国炬信息技术有限公司").url("www.jeccg.com").email("jeecgos@163.com"))
.description( "后台API接口")
.description( "小程序API接口 - 仅显示/applet开头的接口")
.termsOfService("NO terms of service")
.license(new License().name("Apache 2.0").url("http://www.apache.org/licenses/LICENSE-2.0.html")))
.addSecurityItem(new SecurityRequirement().addList(CommonConstant.X_ACCESS_TOKEN))


+ 141
- 0
jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/AppletJwtFilter.java View File

@ -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 forJT-355 OA聊天添加token验证获取token参数
if (oConvertUtils.isEmpty(token)) {
token = httpServletRequest.getParameter("token");
}
// update-end--Author:lvdandan Date:20210105 forJT-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();
}
}

+ 28
- 0
jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/AppletJwtToken.java View File

@ -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;
}
}

+ 198
- 0
jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/AppletShiroRealm.java View File

@ -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作为kv存储到cache缓存里面(这时候kv值一样)缓存有效期设置为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);
}
}

+ 31
- 3
jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/ShiroConfig.java View File

@ -182,13 +182,18 @@ public class ShiroConfig {
filterChainDefinitionMap.put("/openapi/call/**", "anon");
// 添加自己的过滤器并且取名为jwt
Map<String, Filter> filterMap = new HashMap<String, Filter>(1);
// 添加自己的过滤器并且取名为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");
// 未授权界面返回JSON
@ -249,6 +254,29 @@ public class ShiroConfig {
return securityManager;
}
/**
* 小程序专用SecurityManager
*/
@Bean("appletSecurityManager")
public DefaultWebSecurityManager appletSecurityManager(AppletShiroRealm appletRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(appletRealm);
/*
* 关闭shiro自带的session详情见文档
* http://shiro.apache.org/session-management.html#SessionManagement-
* StatelessApplications%28Sessionless%29
*/
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
//自定义缓存实现,使用redis
securityManager.setCacheManager(redisCacheManager());
return securityManager;
}
/**
* 下面的代码是添加注解支持
* @return


+ 272
- 0
jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/README-Applet-Module.md View File

@ -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`
## 技术支持
如有问题,请联系开发团队或查看项目文档。

+ 105
- 0
jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/controller/AppletLoginController.java View File

@ -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());
}
}
}

+ 100
- 0
jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/controller/AppletUserInfoController.java View File

@ -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);
}
}

+ 183
- 0
jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/service/AppletLoginService.java View File

@ -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());
}
}
}

+ 136
- 0
jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/service/AppletUserService.java View File

@ -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());
}
}
}

+ 172
- 0
jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/applet/service/WxAppletService.java View File

@ -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());
}
}
}

+ 165
- 0
jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/appletBackground/appletUser/controller/AppletUserController.java View File

@ -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);
}
}

+ 14
- 0
jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/appletBackground/appletUser/mapper/AppletUserMapper.java View File

@ -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> {
}

+ 5
- 0
jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/appletBackground/appletUser/mapper/xml/AppletUserMapper.xml View File

@ -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>

+ 14
- 0
jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/appletBackground/appletUser/service/IAppletUserService.java View File

@ -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> {
}

+ 19
- 0
jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/appletBackground/appletUser/service/impl/AppletUserServiceImpl.java View File

@ -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 {
}

+ 113
- 0
jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/appletBackground/appletUser/uniapp/AppletUserForm.vue View File

@ -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>

+ 44
- 0
jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/appletBackground/appletUser/uniapp/AppletUserList.vue View File

@ -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>

+ 34
- 0
jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/appletBackground/appletUser/uniapp3/AppletUserData.ts View File

@ -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'
},
];

+ 276
- 0
jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/appletBackground/appletUser/uniapp3/AppletUserForm.vue View File

@ -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>

+ 148
- 0
jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/appletBackground/appletUser/uniapp3/AppletUserList.vue View File

@ -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>

+ 72
- 0
jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/appletBackground/appletUser/vue3Native/AppletUser.api.ts View File

@ -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 });
}

+ 48
- 0
jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/appletBackground/appletUser/vue3Native/AppletUser.data.ts View File

@ -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',},
};

+ 249
- 0
jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/appletBackground/appletUser/vue3Native/AppletUserList.vue View File

@ -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>

+ 26
- 0
jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/appletBackground/appletUser/vue3Native/V20250717_1__menu_insert_AppletUser.sql View File

@ -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);

+ 180
- 0
jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/appletBackground/appletUser/vue3Native/components/AppletUserForm.vue View File

@ -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>

+ 81
- 0
jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/appletBackground/appletUser/vue3Native/components/AppletUserModal.vue View File

@ -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>

+ 264
- 0
jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/common/wxUtils/WxHttpClientUtil.java View File

@ -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());
}
}
}
}
}

+ 96
- 0
jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/common/wxUtils/WxHttpUtils.java View File

@ -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();
}
}
}

+ 0
- 28
jeecg-boot/jeecg-boot-module/jeecgboot-boot-applet/src/main/java/org/jeecg/modules/healthApi/HealthManagerController.java View File

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

+ 14
- 0
jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-dev.yml View File

@ -325,3 +325,17 @@ justauth:
type: default
prefix: 'demo::'
timeout: 1h
##配置微信##爱简收旧衣按件回收小程序
wechat:
mpAppId: wx2bd656ae9704dbee # 微信小程序appid
mpAppSecret: b07f82b16e9598bc23de604f49c57e23 # 微信小程序密钥
merchantId: 1701841654 # 商户号
privateKeyPath: module-system/src/main/resources/apiclient_key.pem #本地私钥路径
publicKeyPath: module-system/src/main/resources/pub_key.pem #本地公钥路径
# privateKeyPath: /data/app-test/hly/cerFile/apiclient_key.pem #线上私钥路径
# publicKeyPath: /data/app-test/hly/cerFile/pub_key.pem #线上公钥路径
publicKeyId: PUB_KEY_ID_0117018416542025022100395100001649 #公钥
merchantSerialNumber: 525971050851A99F315A970D3055192779652E03 # 商户API证书序列号
apiV3Key: # 商户APIV3密钥
refundNotifyUrl: # 退款通知地址(正式环境)

Loading…
Cancel
Save