Selaa lähdekoodia

1、把看课模板分离出来单独一个服务,把不相关的代码移除 整合模块

yfh 1 päivä sitten
vanhempi
commit
c914158bf6
59 muutettua tiedostoa jossa 4760 lisäystä ja 0 poistoa
  1. 140 0
      fs-user-course/pom.xml
  2. 14 0
      fs-user-course/src/main/java/com/fs/FSServletInitializer.java
  3. 26 0
      fs-user-course/src/main/java/com/fs/FsUserCourseApplication.java
  4. 12 0
      fs-user-course/src/main/java/com/fs/course/annotation/Login.java
  5. 15 0
      fs-user-course/src/main/java/com/fs/course/annotation/LoginUser.java
  6. 29 0
      fs-user-course/src/main/java/com/fs/course/config/WebMvcConfig.java
  7. 40 0
      fs-user-course/src/main/java/com/fs/course/config/WebSocketConfig.java
  8. 121 0
      fs-user-course/src/main/java/com/fs/course/controller/AppBaseController.java
  9. 165 0
      fs-user-course/src/main/java/com/fs/course/controller/CompanyUserController.java
  10. 321 0
      fs-user-course/src/main/java/com/fs/course/controller/CourseController.java
  11. 119 0
      fs-user-course/src/main/java/com/fs/course/controller/CourseH5Controller.java
  12. 67 0
      fs-user-course/src/main/java/com/fs/course/controller/H5Controller.java
  13. 245 0
      fs-user-course/src/main/java/com/fs/course/controller/UserController.java
  14. 267 0
      fs-user-course/src/main/java/com/fs/course/controller/WxMpController.java
  15. 371 0
      fs-user-course/src/main/java/com/fs/course/controller/WxUserController.java
  16. 51 0
      fs-user-course/src/main/java/com/fs/course/exception/FSException.java
  17. 68 0
      fs-user-course/src/main/java/com/fs/course/interceptor/AuthorizationInterceptor.java
  18. 14 0
      fs-user-course/src/main/java/com/fs/course/param/FsBindCompanyUserParam.java
  19. 95 0
      fs-user-course/src/main/java/com/fs/course/param/FsDoctorRegisterParam.java
  20. 24 0
      fs-user-course/src/main/java/com/fs/course/param/FsUserEditParam.java
  21. 13 0
      fs-user-course/src/main/java/com/fs/course/param/FsUserLoginByMpParam.java
  22. 88 0
      fs-user-course/src/main/java/com/fs/course/utils/JwtUtils.java
  23. 23 0
      fs-user-course/src/main/java/com/fs/course/vo/CityVO.java
  24. 182 0
      fs-user-course/src/main/java/com/fs/framework/aspectj/DataScopeAspect.java
  25. 73 0
      fs-user-course/src/main/java/com/fs/framework/aspectj/DataSourceAspect.java
  26. 244 0
      fs-user-course/src/main/java/com/fs/framework/aspectj/LogAspect.java
  27. 117 0
      fs-user-course/src/main/java/com/fs/framework/aspectj/RateLimiterAspect.java
  28. 31 0
      fs-user-course/src/main/java/com/fs/framework/config/ApplicationConfig.java
  29. 85 0
      fs-user-course/src/main/java/com/fs/framework/config/CaptchaConfig.java
  30. 92 0
      fs-user-course/src/main/java/com/fs/framework/config/DataSourceConfig.java
  31. 72 0
      fs-user-course/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  32. 59 0
      fs-user-course/src/main/java/com/fs/framework/config/FilterConfig.java
  33. 76 0
      fs-user-course/src/main/java/com/fs/framework/config/KaptchaTextCreator.java
  34. 149 0
      fs-user-course/src/main/java/com/fs/framework/config/MyBatisConfig.java
  35. 137 0
      fs-user-course/src/main/java/com/fs/framework/config/RedisConfig.java
  36. 65 0
      fs-user-course/src/main/java/com/fs/framework/config/ResourcesConfig.java
  37. 50 0
      fs-user-course/src/main/java/com/fs/framework/config/SecurityConfig.java
  38. 33 0
      fs-user-course/src/main/java/com/fs/framework/config/ServerConfig.java
  39. 124 0
      fs-user-course/src/main/java/com/fs/framework/config/SwaggerConfig.java
  40. 63 0
      fs-user-course/src/main/java/com/fs/framework/config/ThreadPoolConfig.java
  41. 77 0
      fs-user-course/src/main/java/com/fs/framework/config/properties/DruidProperties.java
  42. 27 0
      fs-user-course/src/main/java/com/fs/framework/datasource/DynamicDataSource.java
  43. 44 0
      fs-user-course/src/main/java/com/fs/framework/datasource/DynamicDataSourceContextHolder.java
  44. 56 0
      fs-user-course/src/main/java/com/fs/framework/interceptor/RepeatSubmitInterceptor.java
  45. 126 0
      fs-user-course/src/main/java/com/fs/framework/interceptor/impl/SameUrlDataInterceptor.java
  46. 56 0
      fs-user-course/src/main/java/com/fs/framework/manager/AsyncManager.java
  47. 40 0
      fs-user-course/src/main/java/com/fs/framework/manager/ShutdownManager.java
  48. 103 0
      fs-user-course/src/main/java/com/fs/framework/manager/factory/AsyncFactory.java
  49. 1 0
      fs-user-course/src/main/resources/META-INF/spring-devtools.properties
  50. BIN
      fs-user-course/src/main/resources/apiclient_cert.p12
  51. 15 0
      fs-user-course/src/main/resources/application.yml
  52. 37 0
      fs-user-course/src/main/resources/i18n/messages.properties
  53. 93 0
      fs-user-course/src/main/resources/logback.xml
  54. 20 0
      fs-user-course/src/main/resources/mybatis/mybatis-config.xml
  55. 21 0
      fs-user-course/src/main/resources/templates/healthCustomerService.html
  56. 21 0
      fs-user-course/src/main/resources/templates/privacyPolicy.html
  57. 21 0
      fs-user-course/src/main/resources/templates/userAgreement.html
  58. 21 0
      fs-user-course/src/main/resources/templates/vipService.html
  59. 1 0
      pom.xml

+ 140 - 0
fs-user-course/pom.xml

@@ -0,0 +1,140 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>fs</artifactId>
+        <groupId>com.fs</groupId>
+        <version>1.1.0</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>fs-user-course</artifactId>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-websocket</artifactId>
+        </dependency>
+        <!-- spring-boot-devtools -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-devtools</artifactId>
+            <optional>true</optional> <!-- 表示依赖不会传递 -->
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-cache</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-redis</artifactId>
+        </dependency>
+        <!-- swagger2-->
+        <dependency>
+            <groupId>io.springfox</groupId>
+            <artifactId>springfox-swagger2</artifactId>
+        </dependency>
+
+        <!-- swagger2-UI-->
+        <dependency>
+            <groupId>io.springfox</groupId>
+            <artifactId>springfox-swagger-ui</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.github.xiaoymin</groupId>
+            <artifactId>swagger-bootstrap-ui</artifactId>
+            <version>1.9.3</version>
+        </dependency>
+
+
+        <!-- Mysql驱动包 -->
+        <dependency>
+            <groupId>mysql</groupId>
+            <artifactId>mysql-connector-java</artifactId>
+        </dependency>
+
+        <!-- SpringBoot Web容器 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+
+        <!-- SpringBoot 拦截器 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-aop</artifactId>
+        </dependency>
+
+        <!-- 阿里数据库连接池 -->
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>druid-spring-boot-starter</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-thymeleaf</artifactId>
+        </dependency>
+
+        <!-- 验证码 -->
+        <dependency>
+            <groupId>com.github.penggle</groupId>
+            <artifactId>kaptcha</artifactId>
+            <exclusions>
+                <exclusion>
+                    <artifactId>javax.servlet-api</artifactId>
+                    <groupId>javax.servlet</groupId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+
+        <!-- 获取系统信息 -->
+        <dependency>
+            <groupId>com.github.oshi</groupId>
+            <artifactId>oshi-core</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.fs</groupId>
+            <artifactId>fs-service</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.rocketmq</groupId>
+            <artifactId>rocketmq-spring-boot-starter</artifactId>
+            <version>2.2.3</version>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <version>2.1.1.RELEASE</version>
+                <configuration>
+                    <fork>true</fork> <!-- 如果没有该配置,devtools不会生效 -->
+                </configuration>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>repackage</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-war-plugin</artifactId>
+                <version>3.1.0</version>
+                <configuration>
+                    <failOnMissingWebXml>false</failOnMissingWebXml>
+                    <warName>${project.artifactId}</warName>
+                </configuration>
+            </plugin>
+        </plugins>
+        <finalName>${project.artifactId}</finalName>
+    </build>
+
+</project>

+ 14 - 0
fs-user-course/src/main/java/com/fs/FSServletInitializer.java

@@ -0,0 +1,14 @@
+package com.fs;
+
+import org.springframework.boot.builder.SpringApplicationBuilder;
+import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
+
+
+public class FSServletInitializer extends SpringBootServletInitializer
+{
+    @Override
+    protected SpringApplicationBuilder configure(SpringApplicationBuilder application)
+    {
+        return application.sources(FsUserCourseApplication.class);
+    }
+}

+ 26 - 0
fs-user-course/src/main/java/com/fs/FsUserCourseApplication.java

@@ -0,0 +1,26 @@
+package com.fs;
+
+import com.github.xiaoymin.swaggerbootstrapui.annotations.EnableSwaggerBootstrapUI;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.springframework.scheduling.annotation.EnableScheduling;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+
+/**
+ * 启动程序
+ */
+@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
+@EnableTransactionManagement
+@EnableSwaggerBootstrapUI
+@EnableScheduling
+public class FsUserCourseApplication
+{
+    public static void main(String[] args)
+    {
+
+        // System.setProperty("spring.devtools.restart.enabled", "false");
+        SpringApplication.run(FsUserCourseApplication.class, args);
+        System.out.println("CourseAPI启动成功");
+    }
+}

+ 12 - 0
fs-user-course/src/main/java/com/fs/course/annotation/Login.java

@@ -0,0 +1,12 @@
+package com.fs.course.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * app登录效验
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface Login {
+}

+ 15 - 0
fs-user-course/src/main/java/com/fs/course/annotation/LoginUser.java

@@ -0,0 +1,15 @@
+package com.fs.course.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 登录用户信息
+ */
+@Target(ElementType.PARAMETER)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface LoginUser {
+
+}

+ 29 - 0
fs-user-course/src/main/java/com/fs/course/config/WebMvcConfig.java

@@ -0,0 +1,29 @@
+package com.fs.course.config;
+
+
+import com.fs.course.interceptor.AuthorizationInterceptor;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+/**
+ * MVC配置
+ */
+@Configuration
+public class WebMvcConfig implements WebMvcConfigurer {
+    @Autowired
+    private AuthorizationInterceptor authorizationInterceptor;
+//    @Autowired
+//    private LoginUserHandlerMethodArgumentResolver loginUserHandlerMethodArgumentResolver;
+
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        registry.addInterceptor(authorizationInterceptor).addPathPatterns("/app/**");
+    }
+//
+//    @Override
+//    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
+//        argumentResolvers.add(loginUserHandlerMethodArgumentResolver);
+//    }
+}

+ 40 - 0
fs-user-course/src/main/java/com/fs/course/config/WebSocketConfig.java

@@ -0,0 +1,40 @@
+package com.fs.course.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.socket.server.standard.ServerEndpointExporter;
+import org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean;
+
+@Configuration
+public class WebSocketConfig {
+    /**
+     * 自动注册使用了@ServerEndpoint注解声明的Websocket endpoint
+     *
+     * @return
+     */
+    @Bean
+    public ServerEndpointExporter serverEndpointExporter() {
+        return new ServerEndpointExporter();
+    }
+
+    /**
+     * 通信文本消息和二进制缓存区大小
+     * 避免对接 第三方 报文过大时,Websocket 1009 错误
+     *
+     * @return
+     */
+    @Bean
+    public ServletServerContainerFactoryBean createWebSocketContainer() {
+        ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
+        // 设置文本消息缓冲区大小
+        container.setMaxTextMessageBufferSize(10240000);
+        // 设置二进制消息缓冲区大小
+        container.setMaxBinaryMessageBufferSize(10240000);
+        // 设置最大会话空闲超时时间(单位:毫秒)
+        container.setMaxSessionIdleTimeout(20 * 60000L); // 15分钟
+        // 设置异步发送超时时间(单位:毫秒)
+        container.setAsyncSendTimeout(300 * 1000L);
+        return container;
+    }
+
+}

+ 121 - 0
fs-user-course/src/main/java/com/fs/course/controller/AppBaseController.java

@@ -0,0 +1,121 @@
+package com.fs.course.controller;
+
+
+import com.fs.course.utils.JwtUtils;
+import com.fs.common.constant.HttpStatus;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.PageDomain;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.core.page.TableSupport;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.common.exception.CustomException;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.common.utils.sql.SqlUtil;
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
+import io.jsonwebtoken.Claims;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import java.util.List;
+
+
+public class AppBaseController {
+	@Autowired
+	JwtUtils jwtUtils;
+	@Autowired
+	RedisCache redisCache;
+
+	public String getUserId()
+	{
+		String headValue =  ServletUtils.getRequest().getHeader("APPToken");
+		if (StringUtils.isNotEmpty(headValue)){
+			Claims claims=jwtUtils.getClaimByToken(headValue);
+			if(claims!=null){
+				String userId = claims.getSubject().toString();
+				return userId;
+			}
+			else{
+				return "";
+			}
+		}
+		return "";
+	}
+	public Long getCompanyUserId( )
+	{
+		Long companyUesrId=redisCache.getCacheObject("company-user-token:"+ ServletUtils.getRequest().getHeader("companyUserToken"));
+		if(companyUesrId== null){
+			throw  new CustomException("未登录",403);
+		}
+		return companyUesrId;
+	}
+
+	/**
+	 * 设置请求分页数据
+	 */
+	protected void startPage()
+	{
+		PageDomain pageDomain = TableSupport.buildPageRequest();
+		Integer pageNum = pageDomain.getPageNum();
+		Integer pageSize = pageDomain.getPageSize();
+		if (StringUtils.isNotNull(pageNum) && StringUtils.isNotNull(pageSize))
+		{
+			String orderBy = SqlUtil.escapeOrderBySql(pageDomain.getOrderBy());
+			Boolean reasonable = pageDomain.getReasonable();
+			PageHelper.startPage(pageNum, pageSize, orderBy).setReasonable(reasonable);
+		}
+	}
+
+	/**
+	 * 响应请求分页数据
+	 */
+	@SuppressWarnings({ "rawtypes", "unchecked" })
+	protected TableDataInfo getDataTable(List<?> list)
+	{
+		TableDataInfo rspData = new TableDataInfo();
+		rspData.setCode(HttpStatus.SUCCESS);
+		rspData.setMsg("查询成功");
+		rspData.setRows(list);
+		rspData.setTotal(new PageInfo(list).getTotal());
+		return rspData;
+	}
+
+	/**
+	 * 响应返回结果
+	 *
+	 * @param rows 影响行数
+	 * @return 操作结果
+	 */
+	protected AjaxResult toAjax(int rows)
+	{
+		return rows > 0 ? AjaxResult.success() : AjaxResult.error();
+	}
+
+	/**
+	 * 响应返回结果
+	 *
+	 * @param result 结果
+	 * @return 操作结果
+	 */
+	protected AjaxResult toAjax(boolean result)
+	{
+		return result ? success() : error();
+	}
+
+	/**
+	 * 返回成功
+	 */
+	public AjaxResult success()
+	{
+		return AjaxResult.success();
+	}
+
+	/**
+	 * 返回失败消息
+	 */
+	public AjaxResult error()
+	{
+		return AjaxResult.error();
+	}
+
+}

+ 165 - 0
fs-user-course/src/main/java/com/fs/course/controller/CompanyUserController.java

@@ -0,0 +1,165 @@
+package com.fs.course.controller;
+
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.fs.course.annotation.Login;
+import com.fs.course.param.FsBindCompanyUserParam;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.common.utils.SecurityUtils;
+import com.fs.common.utils.sign.Md5Utils;
+import com.fs.company.domain.CompanyUser;
+import com.fs.company.domain.CompanyUserUser;
+import com.fs.company.mapper.CompanyUserMapper;
+import com.fs.company.param.CompanyUserLoginParam;
+import com.fs.company.param.companyUserAddPrintParam;
+import com.fs.company.service.ICompanyUserService;
+import com.fs.company.service.ICompanyUserUserService;
+import com.fs.fastgptApi.util.AudioUtils;
+import com.fs.system.oss.CloudStorageService;
+import com.fs.system.oss.OSSFactory;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.util.EntityUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletRequest;
+import java.io.File;
+import java.io.FileInputStream;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+
+@Api("销售接口")
+@RestController
+@RequestMapping(value="/app/companyUser")
+public class CompanyUserController extends  AppBaseController {
+
+
+    @Autowired
+    RedisCache redisCache;
+    @Autowired
+    private ICompanyUserService companyUserService;
+    @Autowired
+    private CompanyUserMapper companyUserMapper;
+    @Autowired
+    private ICompanyUserUserService companyUserUserService;
+    @PostMapping("/login")
+    public R Login(@RequestBody CompanyUserLoginParam param, HttpServletRequest request){
+        try {
+            CompanyUser companyUser=companyUserService.selectUserByUserName(param.getUserName());
+            if(companyUser==null||companyUser.getDelFlag().equals("1")){
+                return R.error("用户不存在");
+            }
+            if(!companyUser.getStatus().equals("0")){
+                return R.error("用户已禁用");
+
+            }
+            String pwd= SecurityUtils.encryptPassword(param.getPassword());
+            if(!SecurityUtils.matchesPassword(param.getPassword(),companyUser.getPassword())){
+                return R.error("密码不正确");
+            }
+            redisCache.setCacheObject("company-user-token:"+Md5Utils.hash(companyUser.getUserId().toString()),companyUser.getUserId(),100, TimeUnit.DAYS);
+            return R.ok().put("companyUserToken", Md5Utils.hash(companyUser.getUserId().toString())).put("user",companyUser);
+        } catch (Exception e){
+
+            return R.error("操作异常");
+        }
+    }
+
+    @ApiOperation("获取销售信息")
+    @GetMapping("/getUserInfo")
+    public R getUserInfo(HttpServletRequest request){
+
+        Long userId=getCompanyUserId();
+        if(userId==null){
+            return R.error(403,"用户失效");
+        }
+        CompanyUser companyUser=companyUserService.selectCompanyUserById(userId);
+        if(companyUser==null||companyUser.getDelFlag().equals("1")){
+            return R.error("用户不存在");
+        }
+        if(!companyUser.getStatus().equals("0")){
+            return R.error("用户已禁用");
+        }
+        return R.ok().put("data",companyUser);
+    }
+
+    @Login
+    @ApiOperation("绑定销售")
+    @PostMapping("/bindCompanyUser")
+    public R bindCompanyUser(@Validated @RequestBody FsBindCompanyUserParam param, HttpServletRequest request){
+        CompanyUserUser map=new CompanyUserUser();
+        map.setCompanyUserId(param.getCompanyUserId());
+        map.setUserId(Long.parseLong(getUserId()));
+        List<CompanyUserUser> list= companyUserUserService.selectCompanyUserUserList(map);
+        if(list==null||list.size()==0){
+            CompanyUser companyUser=companyUserService.selectCompanyUserById(param.getCompanyUserId());
+            if(companyUser!=null&&companyUser.getStatus().equals("0")){
+                map.setCompanyId(companyUser.getCompanyId());
+                companyUserUserService.insertCompanyUserUser(map);
+            }
+        }
+        return R.ok();
+    }
+
+
+    @Login
+    @ApiOperation("上传声纹")
+    @PostMapping("/addVoicePrintUrl")
+    public R addVoicePrintUrl(@RequestBody companyUserAddPrintParam param) throws Exception {
+        Long userId=getCompanyUserId();
+        if(userId==null){
+            return R.error(403,"用户失效");
+        }
+        CompanyUser companyUser = new CompanyUser();
+        companyUser.setUserId(userId);
+        companyUser.setVoicePrintUrl(param.getVoicePrintUrl());
+
+        String s = AudioUtils.audioWAVFromUrl(param.getVoicePrintUrl());
+
+        System.out.println(s);
+        File file = new File(s);
+        FileInputStream fileInputStream = new FileInputStream(file);
+        CloudStorageService storage = OSSFactory.build();
+        String wavUrl = storage.uploadSuffix(fileInputStream, ".wav");
+        companyUser.setVoicePrintUrl(wavUrl);
+        companyUserMapper.updateCompanyUser(companyUser);
+        try {
+            CloseableHttpClient httpClient = HttpClients.createDefault();
+            HttpPost httpPost = new HttpPost("http://118.24.209.192:7771/app/common/addCompanyAudio");
+            String json = "{\"url\":\""+wavUrl+"\",\"id\":\""+userId+"\"}";
+            StringEntity entity = new StringEntity(json);
+            httpPost.setEntity(entity);
+            httpPost.setHeader("Content-type", "application/json");
+            HttpResponse response = httpClient.execute(httpPost);
+
+            if (response.getStatusLine().getStatusCode() == 200) {
+                String responseBody = EntityUtils.toString(response.getEntity());
+                JSONObject jsonObject = JSON.parseObject(responseBody);
+                Integer code = (Integer)jsonObject.get("code");
+                if (code==200){
+                    return R.ok();
+                }
+            } else {
+                return R.error();
+            }
+
+            httpClient.close();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+
+        return R.error();
+
+    }
+
+}

+ 321 - 0
fs-user-course/src/main/java/com/fs/course/controller/CourseController.java

@@ -0,0 +1,321 @@
+package com.fs.course.controller;
+
+
+import cn.hutool.json.JSONUtil;
+import com.fs.course.annotation.Login;
+import com.fs.common.annotation.RepeatSubmit;
+import com.fs.common.core.domain.R;
+import com.fs.common.utils.DateUtils;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.core.utils.OrderCodeUtils;
+import com.fs.course.config.CourseConfig;
+import com.fs.course.domain.*;
+import com.fs.course.param.*;
+import com.fs.course.service.*;
+import com.fs.course.service.IFsUserCourseService;
+import com.fs.course.service.impl.TencentCloudCosService;
+import com.fs.course.vo.*;
+import com.fs.his.service.IFsIntegralGoodsService;
+import com.fs.his.vo.OptionsVO;
+import com.fs.sop.domain.QwSop;
+import com.fs.sop.service.IQwSopService;
+import com.fs.system.service.ISysConfigService;
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
+import io.jsonwebtoken.Claims;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import lombok.Synchronized;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.bind.annotation.*;
+import springfox.documentation.spring.web.readers.operation.CachingOperationNameGenerator;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.*;
+
+@Api("课堂接口")
+@RestController
+@RequestMapping(value="/app/course")
+public class CourseController extends  AppBaseController{
+    Logger logger= LoggerFactory.getLogger(getClass());
+    @Autowired
+    private IFsUserCourseService courseService;
+    @Autowired
+    private IFsUserCourseCategoryService courseCategoryService;
+
+    @Autowired
+    private IFsUserCourseVideoService courseVideoService;
+
+
+    @Autowired
+    private ISysConfigService configService;
+    @Autowired
+    private IFsCourseLinkService courseLinkService;
+
+    @Autowired
+    private IFsCourseQuestionBankService questionBankService;
+
+    @Autowired
+    private IFsCourseWatchLogService courseWatchLogService;
+
+    @Autowired
+    private IFsCourseWatchCommentService courseWatchCommentService;
+
+    @Autowired
+    private IFsIntegralGoodsService goodsService;
+
+    @Autowired
+    private IQwSopService qwSopService;
+
+    @ApiOperation("h5课程简介")
+    @GetMapping("/getH5CourseByVideoId")
+    public R getCourseByVideoId(@RequestParam("videoId") Long videoId,HttpServletRequest request)
+    {
+        FsUserCourseVideoH5VO course = courseService.selectFsUserCourseVideoH5VOByVideoId(videoId);
+        return R.ok().put("data",course);
+    }
+
+    @Login
+    @ApiOperation("h5课程详情加问答")
+    @GetMapping("/getH5CourseVideoDetails")
+    public R getCourseVideoDetails(FsUserCourseVideoFinishUParam param)
+    {
+        param.setUserId(Long.parseLong(getUserId()));
+        String json = configService.selectConfigByKey("course.config");
+        CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
+        FsUserCourseVideoH5DVO course = courseService.selectFsUserCourseVideoH5DVOByVideoId(param.getVideoId());
+        List<FsUserCourseVideoQuestionVO> questionVOList = new ArrayList<>();
+        if (StringUtils.isNotEmpty(course.getQuestionBankId())){
+            String[] questionIds = course.getQuestionBankId().split(",");
+            for (String questionId : questionIds){
+                FsUserCourseVideoQuestionVO vo = new FsUserCourseVideoQuestionVO();
+                FsCourseQuestionBank questionBank = questionBankService.selectFsCourseQuestionBankById(Long.parseLong(questionId));
+                if (questionBank!=null&&questionBank.getStatus()!=0){
+                    BeanUtils.copyProperties(questionBank,vo);
+                    questionVOList.add(vo);
+                }
+            }
+        }
+        Long duration = 0L;
+        long tipsTime = 0L;
+        int isFinish = 0;
+        if (param.getLinkType()!=null&&param.getLinkType()==1){
+            return R.ok().put("course",course).put("questions",questionVOList).put("config",config).put("playDuration",duration).put("tipsTime",tipsTime);
+        }
+
+        if (param.getIsRoom()!=null&&param.getIsRoom()==1&&param.getQwExternalId()==null){
+            return R.ok().put("course",course).put("questions",questionVOList).put("config",config).put("playDuration",duration).put("tipsTime",tipsTime);
+        }
+        // 从Redis中获取观看时长
+        String redisKey = "h5user:watch:duration:" + param.getQwUserId()+ ":" + param.getQwExternalId() + ":" + param.getVideoId();
+        String durationStr = redisCache.getCacheObject(redisKey);
+        FsCourseWatchLog log = courseWatchLogService.getWatchCourseVideo(param.getUserId(),param.getVideoId(),param.getQwUserId(),param.getQwExternalId());
+        if (log==null){
+            return R.error("记录不存在,请联系客服!");
+        }
+        //redis取不到查库
+        if (durationStr != null) {
+            duration = Long.parseLong(durationStr);
+        }else {
+            duration = log.getDuration();
+        }
+
+        if (course.getDuration()!=null){
+            tipsTime = course.getDuration()/2;
+        }
+        //判断是否完课
+        if (log.getLogType()==2){
+           isFinish=1;
+        }
+
+        //将视频时长也存到redis
+        String videoRedisKey = "h5user:video:duration:" + param.getVideoId();
+        Long videoDuration = redisCache.getCacheObject(videoRedisKey);
+        if (videoDuration==null){
+
+            redisCache.setCacheObject(videoRedisKey,course.getDuration());
+        }
+        // 返回是否开启评论/弹幕。优先获取sop任务配置的是否开启评论/弹幕,如果没有配置就取总后台的配置;
+        QwSop qwSop = qwSopService.selectQwSopById(log.getSopId());
+        if(qwSop != null && qwSop.getOpenCommentStatus() != null) {
+            config.setOpenCommentStatus(qwSop.getOpenCommentStatus());
+        }
+
+        return R.ok().put("course",course).put("questions",questionVOList).put("config",config).put("playDuration",duration).put("tipsTime",tipsTime).put("isFinish",isFinish);
+    }
+
+    @ApiOperation("答题")
+    @PostMapping("/courseAnswer")
+    @Login
+    public R courseAnswer(@RequestBody FsCourseQuestionAnswerUParam param)
+    {
+        logger.info("zyp \n【答题】:{}",param.getQuestions());
+        if (param.getDuration()==null){
+            logger.info("zyp \n【未识别到时长】:{}",param.getUserId());
+        }
+        param.setUserId(Long.parseLong(getUserId()));
+        return questionBankService.courseAnswer(param);
+    }
+
+
+    @Login
+    @ApiOperation("发放奖励")
+    @PostMapping("/sendReward")
+    @RepeatSubmit
+    public R sendReward(@RequestBody FsCourseSendRewardUParam param)
+    {
+        param.setUserId(Long.parseLong(getUserId()));
+        logger.info("zyp \n【发放奖励】:{}",param);
+        return courseVideoService.sendReward(param);
+    }
+
+    @ApiOperation("获取真实链接")
+    @GetMapping("/getRealLink")
+    public R getRealLink(@RequestParam("sortLink")String link)
+    {
+        return courseLinkService.getRealLink(link);
+    }
+
+    @Login
+    @ApiOperation("更新看课时长")
+    @PostMapping("/updateWatchDuration")
+    public R updateWatchDuration(@RequestBody FsUserCourseVideoFinishUParam param)
+    {
+        param.setUserId(Long.parseLong(getUserId()));
+        return courseVideoService.updateWatchDuration(param);
+    }
+
+    @Login
+    @ApiOperation("每十分钟获取积分")
+    @PostMapping("/getIntegralByH5Video")
+    public R getIntegralByH5Video(@RequestBody FsUserCourseVideoFinishUParam param)
+    {
+        param.setUserId(Long.parseLong(getUserId()));
+        return courseVideoService.getIntegralByH5Video(param);
+    }
+
+    /**
+     * 是否添加客服
+     */
+    @Login
+    @ApiOperation("是否添加客服")
+    @PostMapping("/isAddKf")
+    public R isAddKf(@RequestBody FsUserCourseVideoAddKfUParam param) {
+        Long userId = Long.parseLong(getUserId());
+        param.setUserId(userId);
+        return courseVideoService.isAddKf(param);
+    }
+
+    @Login
+    @ApiOperation("获取缓冲流量")
+    @PostMapping("/getInternetTraffic")
+    public R getInternetTraffic(@RequestBody FsUserCourseVideoFinishUParam param) {
+        Long userId = Long.parseLong(getUserId());
+        param.setUserId(userId);
+        return courseVideoService.getInternetTraffic(param);
+    }
+
+
+//    @Login
+//    @ApiOperation("H5看课绑定手机号")
+//    @PostMapping("/bindUserPhone")
+//    public R bindUserPhone(@RequestBody FsCourseH5PhoneParam param) {
+//        Long userId = Long.parseLong(getUserId());
+//        param.setUserId(userId);
+//        return courseVideoService.bindUserPhone(param);
+//    }
+
+    @GetMapping("/genOrderSn")
+    public R genOrderSn() {
+        String orderCode = OrderCodeUtils.getOrderSn();
+        System.out.println(orderCode);
+        redisCache.setCacheObject("orderCode"+orderCode,orderCode);
+        return R.ok(orderCode);
+    }
+
+
+    @Login
+    @PostMapping("/getErrMsg")
+    public void getErrMsg(@RequestParam("msg") String msg) {
+        Long userId = Long.parseLong(getUserId());
+        logger.error("zyp \n【看课中途报错】:{}","userId:"+userId+msg);
+    }
+
+
+    @Login
+    @ApiOperation("获取积分奖励信息")
+    @GetMapping("/getCourseIntegralGoods")
+    public R getCourseIntegralGoods() {
+        return goodsService.getCourseIntegralGoods(Long.parseLong(getUserId()));
+    }
+
+
+    @Autowired
+    TencentCloudCosService tencentCloudCosService;
+    @Autowired
+    IFsCourseLinkService linkService;
+
+    @GetMapping("/test1")
+    public void test1() {
+        linkService.updateLinks();
+    }
+
+
+    @GetMapping("/test2")
+    public void test2() {
+
+//        courseVideoService.updateVideoUrl();
+    }
+
+    @Login
+    @ApiOperation("保存评论数据")
+    @PostMapping("/saveMsg")
+    public R saveMsg(@RequestBody FsCourseWatchCommentSaveParam param)
+    {
+        param.setUserId(Long.parseLong(getUserId()));
+        return courseWatchCommentService.saveH5CourseWatchComment(param);
+    }
+
+    @Login
+    @ApiOperation("撤销评论")
+    @PutMapping("/revokeMsg")
+    public R revokeMsg(@ApiParam(value = "评论id", required = true) @RequestParam Long commentId)
+    {
+        return courseWatchCommentService.revokeH5CourseWatchComment(commentId);
+    }
+
+    @ApiOperation("获取历史评论数据")
+    @GetMapping("/getComments")
+    public R getCourseWatchComments(FsCourseWatchCommentListParam param)
+    {
+        //获取配置信息中需要查询的数据条数
+        String json = configService.selectConfigByKey("course.config");
+        CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
+        param.setListNum(config.getViewCommentNum() != null &&  config.getViewCommentNum() != 0 ? config.getViewCommentNum() : 200);
+
+        PageHelper.startPage(param.getPageNum(), param.getPageSize());
+        List<FsCourseWatchCommentVO> list = courseWatchCommentService.selectH5CourseWatchComments(param);
+        PageInfo<FsCourseWatchCommentVO> pageInfo = new PageInfo<>(list);
+        return R.ok().put("data", pageInfo);
+    }
+
+
+    /**
+     * 校验是否可以答题
+     *
+     * @param param
+     * @return
+     */
+    @PostMapping("/isAllowAnswer")
+    public R isAllowAnswer(@RequestBody FsUserCourseVideoFinishUParam param) {
+        return courseVideoService.isAllowAnswer(param);
+    }
+
+}

+ 119 - 0
fs-user-course/src/main/java/com/fs/course/controller/CourseH5Controller.java

@@ -0,0 +1,119 @@
+package com.fs.course.controller;
+
+
+import cn.hutool.json.JSONUtil;
+import com.fs.common.core.domain.R;
+import com.fs.course.config.CourseConfig;
+import com.fs.course.domain.*;
+import com.fs.course.param.*;
+import com.fs.course.service.*;
+import com.fs.course.vo.*;
+import com.fs.system.service.ISysConfigService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletRequest;
+
+@Api("h5课堂接口")
+@RestController
+@RequestMapping(value="/app/course/h5")
+public class CourseH5Controller extends  AppBaseController{
+    Logger logger= LoggerFactory.getLogger(getClass());
+    @Autowired
+    private IFsUserCourseService courseService;
+
+    @Autowired
+    private IFsUserCourseVideoService courseVideoService;
+
+    @Autowired
+    private ISysConfigService configService;
+    @Autowired
+    private IFsCourseLinkService courseLinkService;
+
+    @Autowired
+    private IFsCourseWatchLogService courseWatchLogService;
+
+    @ApiOperation("h5课程简介")
+    @GetMapping("/getH5CourseByVideoId")
+    public R getCourseByVideoId(@RequestParam("videoId") Long videoId,HttpServletRequest request)
+    {
+        FsUserCourseVideoH5VO course = courseService.selectFsUserCourseVideoH5VOByVideoId(videoId);
+        return R.ok().put("data",course);
+    }
+
+    @ApiOperation("h5课程详情")
+    @GetMapping("/getH5CourseVideoDetails")
+    public R getCourseVideoDetails(FsUserCourseVideoFinishUParam param)
+    {
+        String json = configService.selectConfigByKey("course.config");
+        CourseConfig config = JSONUtil.toBean(json, CourseConfig.class);
+        FsUserCourseVideoH5DVO course = courseService.selectFsUserCourseVideoH5DVOByVideoId(param.getVideoId());
+
+        Long duration = 0L;
+        long tipsTime = 0L;
+        int isFinish = 0;
+        if (param.getLinkType()!=null&&param.getLinkType()==1){
+            return R.ok().put("course",course).put("config",config).put("playDuration",duration).put("tipsTime",tipsTime);
+        }
+        // 从Redis中获取观看时长
+        String redisKey = "h5user:watch:duration:" + param.getQwUserId()+ ":" + param.getQwExternalId() + ":" + param.getVideoId();
+        String durationStr = redisCache.getCacheObject(redisKey);
+        FsCourseWatchLog log = courseWatchLogService.getWatchCourseVideoH5(param.getVideoId(),param.getQwUserId(),param.getQwExternalId());
+        //redis取不到查库
+        if (durationStr != null) {
+            duration = Long.parseLong(durationStr);
+        }else {
+            duration = log.getDuration();
+        }
+
+        if (course.getDuration()!=null){
+            tipsTime = course.getDuration()/2;
+        }
+        //判断是否完课
+        if (log.getLogType()==2){
+           isFinish=1;
+        }
+
+        //将视频时长也存到redis
+        String videoRedisKey = "h5user:video:duration:" + param.getVideoId();
+        Long videoDuration = redisCache.getCacheObject(videoRedisKey);
+        if (videoDuration==null){
+            redisCache.setCacheObject(videoRedisKey,course.getDuration());
+        }
+
+        return R.ok().put("course",course).put("config",config).put("playDuration",duration).put("tipsTime",tipsTime).put("isFinish",isFinish);
+    }
+
+
+
+    @ApiOperation("获取真实链接")
+    @GetMapping("/getRealLink")
+    public R getRealLink(@RequestParam("sortLink")String link)
+    {
+        return courseLinkService.getRealLink(link);
+    }
+
+
+    @ApiOperation("更新看课时长")
+    @PostMapping("/updateWatchDuration")
+    public R updateWatchDuration(@RequestBody FsUserCourseVideoFinishUParam param)
+    {
+        return courseVideoService.updateWatchDuration(param);
+    }
+
+
+    @ApiOperation("获取缓冲流量")
+    @PostMapping("/getInternetTraffic")
+    public R getInternetTraffic(@RequestBody FsUserCourseVideoFinishUParam param) {
+        return courseVideoService.getInternetTraffic(param);
+    }
+
+    @PostMapping("/getErrMsg")
+    public void getErrMsg(@RequestParam("msg") String msg) {
+        logger.error("zyp \n【h5看课中途报错】:{}",msg);
+    }
+}

+ 67 - 0
fs-user-course/src/main/java/com/fs/course/controller/H5Controller.java

@@ -0,0 +1,67 @@
+package com.fs.course.controller;
+
+import cn.hutool.json.JSONUtil;
+import com.fs.his.config.AgreementConfig;
+import com.fs.system.service.ISysConfigService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.servlet.ModelAndView;
+
+@Controller
+@RequestMapping(value="/web")
+public class H5Controller
+{
+
+    @Autowired
+    private ISysConfigService configService;
+
+
+    @GetMapping("/userAgreement")
+    public ModelAndView userAgreement( )
+    {
+
+        String json=configService.selectConfigByKey("his.agreementConfig");
+        AgreementConfig config= JSONUtil.toBean(json, AgreementConfig.class);
+
+        ModelAndView mv=new ModelAndView();
+        mv.addObject("userAgreement", config.getUserRegister());
+        mv.setViewName("userAgreement");
+        return mv;
+    }
+    @GetMapping("/privacyPolicy")
+    public ModelAndView privacyPolicy( )
+    {
+        String json=configService.selectConfigByKey("his.agreementConfig");
+        AgreementConfig config= JSONUtil.toBean(json, AgreementConfig.class);
+        ModelAndView mv=new ModelAndView();
+        mv.addObject("privacyPolicy", config.getUserPrivacy());
+        mv.setViewName("privacyPolicy");
+        return mv;
+    }
+
+    @GetMapping("/healthCustomerService")
+    public ModelAndView healthCustomerService( )
+    {
+        String json=configService.selectConfigByKey("his.agreementConfig");
+        AgreementConfig config= JSONUtil.toBean(json, AgreementConfig.class);
+        ModelAndView mv=new ModelAndView();
+        mv.addObject("healthCustomerService", config.getUserHealth());
+        mv.setViewName("healthCustomerService");
+        return mv;
+    }
+
+    @GetMapping("/vipService")
+    public ModelAndView vipService( )
+    {
+        String json=configService.selectConfigByKey("his.agreementConfig");
+        AgreementConfig config= JSONUtil.toBean(json, AgreementConfig.class);
+        ModelAndView mv=new ModelAndView();
+        mv.addObject("vipService", config.getVipService());
+        mv.setViewName("vipService");
+        return mv;
+    }
+
+
+}

+ 245 - 0
fs-user-course/src/main/java/com/fs/course/controller/UserController.java

@@ -0,0 +1,245 @@
+package com.fs.course.controller;
+
+
+import com.fs.course.annotation.Login;
+import com.fs.course.param.FsDoctorRegisterParam;
+import com.fs.course.param.FsUserEditParam;
+import com.fs.common.core.domain.R;
+import com.fs.common.exception.CustomException;
+import com.fs.common.utils.StringUtils;
+import com.fs.common.utils.sign.Md5Utils;
+import com.fs.his.domain.FsDoctor;
+import com.fs.his.domain.FsUser;
+import com.fs.his.param.FsUserCouponUParam;
+import com.fs.his.param.FsUserEditPushParam;
+import com.fs.his.service.IFsDoctorService;
+import com.fs.his.service.IFsPackageService;
+import com.fs.his.service.IFsUserCouponService;
+import com.fs.his.service.IFsUserService;
+import com.fs.his.vo.FsUserCouponCountUVO;
+import com.fs.his.vo.FsUserCouponListUVO;
+import com.fs.qw.service.IQwAppContactWayService;
+import com.fs.system.oss.CloudStorageService;
+import com.fs.system.oss.OSSFactory;
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.imageio.ImageIO;
+import javax.servlet.http.HttpServletRequest;
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.io.*;
+import java.math.BigDecimal;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static com.fs.common.utils.SecurityUtils.getUserId;
+import static com.fs.his.utils.PhoneUtil.decryptPhoneMk;
+
+
+@Api("个人中心")
+@RestController
+@RequestMapping(value="/app/user")
+public class UserController extends  AppBaseController {
+
+
+    @Autowired
+    private IFsUserService userService;
+    @Autowired
+    private IFsDoctorService doctorService;
+    @Autowired
+    private IFsPackageService packageService;
+    @Autowired
+    private IFsUserCouponService couponService;
+    @Autowired
+    private IQwAppContactWayService qwAppContactWayService;
+
+    /**
+     * 获取用户信息
+     * @param request
+     * @return     */
+    @Login
+    @ApiOperation("获取用户信息")
+    @GetMapping("/getUserInfo")
+    public R getUserInfo(HttpServletRequest request){
+        try {
+            FsUser user=userService.selectFsUserByUserId(Long.parseLong(getUserId()));
+            if (user.getPhone()!=null&&user.getPhone().length()>11&&!user.getPhone().matches("\\d+")){
+                user.setPhone(decryptPhoneMk(user.getPhone()));
+            }
+            Map<String,Object> map=new HashMap<>();
+            map.put("user",user);
+            return R.ok(map);
+        } catch (Exception e){
+            return R.error("操作异常");
+        }
+    }
+
+    @Login
+    @ApiOperation("查看推送设置")
+    @GetMapping("/getUserPushSetting")
+    public R getUserPushSetting(HttpServletRequest request){
+        FsUser user=userService.selectFsUserByUserId(Long.parseLong(getUserId()));
+        Map<String,Object> map=new HashMap<>();
+        map.put("isPush",user.getIsPush());
+        map.put("isIndividuationPush",user.getIsIndividuationPush());
+        return R.ok().put("data",map);
+    }
+
+    @Login
+    @ApiOperation("保存推送设置")
+    @PostMapping("/editUserPushSetting")
+    public R editUserPushSetting(@RequestBody FsUserEditPushParam param, HttpServletRequest request){
+        FsUser map=new FsUser();
+        BeanUtils.copyProperties(param,map);
+        map.setUserId(Long.parseLong(getUserId()));
+        if(userService.updateFsUser(map)>0){
+            return R.ok("保存成功");
+        }
+        else{
+            return R.error("保存失败");
+        }
+    }
+
+    @ApiOperation("检测是否登录")
+    @GetMapping("/checkLogin")
+    public R checkLogin(HttpServletRequest request){
+        if(StringUtils.isEmpty(getUserId())){
+            //未登录
+            return R.error("未登录");
+        }
+        else{
+            //登录
+            String token = jwtUtils.generateToken(Long.parseLong(getUserId()));
+            Map<String,Object> map=new HashMap<>();
+            map.put("token",token);
+            return R.ok("认证成功").put("userId",getUserId()).put("token",token);
+        }
+    }
+
+
+
+
+    @Login
+    @ApiOperation("修改用户信息")
+    @PostMapping("/editUser")
+    public R editUser(@RequestBody FsUserEditParam param, HttpServletRequest request){
+        FsUser map=new FsUser();
+        BeanUtils.copyProperties(param,map);
+        map.setUserId(Long.parseLong(getUserId()));
+        if(userService.updateFsUser(map)>0){
+            return R.ok("修改成功");
+        }
+        else{
+            return R.error("修改失败");
+        }
+    }
+
+
+    @Login
+    @ApiOperation("注册医生")
+    @PostMapping("/registerDoctor")
+    public R registerDoctor(@Validated  @RequestBody FsDoctorRegisterParam param, HttpServletRequest request){
+        FsDoctor doctor= doctorService.selectFsDoctorByUserId(Long.parseLong(getUserId()));
+        if(doctor!=null){
+            if(doctor.getIsAudit().equals(0)){
+                return R.error("您已提交申请,等待审核...");
+            }
+            else if(doctor.getIsAudit().equals(1)){
+                return R.error("您已提交申请");
+            }
+        }
+        doctor=new FsDoctor();
+        BeanUtils.copyProperties(param,doctor);
+        doctor.setUserId(Long.parseLong(getUserId()));
+        doctor.setIsAudit(0);
+        doctor.setPassword(Md5Utils.hash(param.getPassword()));
+        doctor.setStatus(0);
+        doctor.setBalance(new BigDecimal(0));
+        doctor.setPriceJson("[{\"price\":1.00,\"type\":1},{\"price\":1.00,\"type\":2}]");
+        if(doctorService.insertFsDoctor(doctor)>0){
+            return R.ok("注册成功");
+        }
+        else{
+            return R.error("注册失败");
+        }
+    }
+    @Login
+    @ApiOperation("获取用户优惠券")
+    @GetMapping("/getMyCouponList")
+    public R getMyCouponList(FsUserCouponUParam param, HttpServletRequest request){
+        PageHelper.startPage(param.getPageNum(), param.getPageSize());
+        param.setUserId(Long.parseLong(getUserId()));
+        List<FsUserCouponListUVO> list=couponService.selectFsUserCouponListUVO(param);
+        PageInfo<FsUserCouponListUVO> listPageInfo=new PageInfo<>(list);
+        return R.ok().put("data",listPageInfo);
+    }
+
+    @Login
+    @ApiOperation("获取用户可用优惠券")
+    @GetMapping("/getMyEnableCouponList")
+    public R getMyEnableCouponList(FsUserCouponUParam param, HttpServletRequest request){
+        param.setUserId(Long.parseLong(getUserId()));
+        List<FsUserCouponListUVO> list=couponService.getMyEnableCouponList(param);
+        return R.ok().put("data",list);
+    }
+    @Login
+    @ApiOperation("获取用户优惠券数量")
+    @GetMapping("/getMyCouponCount")
+    public R getMyCouponCount(FsUserCouponUParam param, HttpServletRequest request){
+        if (StringUtils.isNotEmpty(getUserId())) {
+            param.setUserId(Long.parseLong(getUserId()));
+        }
+        FsUserCouponCountUVO count =couponService.selectFsUserCouponCountUVO(param);
+        return R.ok().put("data",count);
+    }
+
+    @ApiOperation("获取APP客服活码")
+    @GetMapping("/getAppContactWay/{userId}")
+    public R getAppContactWay(@PathVariable("userId") Long userId)
+    {
+        if (userId==null||userId==0){
+            return R.error("UserId不能为空");
+        }
+        String appContactWayImgCode = qwAppContactWayService.getAppContactWayImgCode(userId);
+       // String appContactWayImgCode ="https://wework.qpic.cn/wwpic3az/769765_uPSW3IV5QP6-bsM_1733303905/0";
+        File mb = new File("C:\\fs\\qwCode.png");
+        if (!mb.exists()) {
+            // 创建目录
+            throw  new CustomException("模板文件不存在");
+        }
+        BufferedImage image = null;
+        try {
+            image = ImageIO.read(mb);
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+        Graphics2D pen = image.createGraphics();
+        try {
+            URL imageUrl = new URL(appContactWayImgCode);
+            BufferedImage img = ImageIO.read(imageUrl);
+            pen.drawImage(img, 1550, 3270, 500, 500, null);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        ByteArrayOutputStream os = new ByteArrayOutputStream();
+        try {
+            ImageIO.write(image, "png", os);
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+        InputStream inputStream = new ByteArrayInputStream(os.toByteArray());
+        CloudStorageService storage = OSSFactory.build();
+        String url = storage.uploadSuffix(inputStream, ".jpg");
+        return R.ok().put("data",url);
+    }
+
+}

+ 267 - 0
fs-user-course/src/main/java/com/fs/course/controller/WxMpController.java

@@ -0,0 +1,267 @@
+package com.fs.course.controller;
+
+import cn.hutool.core.date.DateTime;
+import com.fs.course.param.FsUserLoginByMpParam;
+import com.fs.course.utils.JwtUtils;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.course.mapper.FsCourseSopLogsMapper;
+import com.fs.course.mapper.FsCourseWatchLogMapper;
+import com.fs.his.domain.FsUser;
+import com.fs.his.service.IFsUserService;
+import com.fs.his.utils.ConfigUtil;
+import com.fs.qw.mapper.QwExternalContactMapper;
+import com.fs.sop.params.QwSopSettingTimeParam;
+import com.fs.system.mapper.SysConfigMapper;
+import com.fs.wx.mp.WxEeventType;
+import com.fs.wx.mp.WxMessageType;
+import com.fs.wx.mp.WxServiceMsgDto;
+import io.swagger.annotations.ApiOperation;
+import lombok.Synchronized;
+import me.chanjar.weixin.common.bean.WxJsapiSignature;
+import me.chanjar.weixin.common.bean.WxOAuth2UserInfo;
+import me.chanjar.weixin.common.bean.menu.WxMenu;
+import me.chanjar.weixin.common.bean.oauth2.WxOAuth2AccessToken;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.mp.api.WxMpMenuService;
+import me.chanjar.weixin.mp.api.WxMpService;
+import me.chanjar.weixin.mp.api.WxMpUserService;
+import me.chanjar.weixin.mp.bean.result.WxMpUser;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.bind.annotation.*;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import static com.fs.course.controller.WxUserController.genCode;
+
+
+@RestController
+@RequestMapping("/app/wx/mp")
+public class WxMpController {
+  Logger logger= LoggerFactory.getLogger(getClass());
+  @Autowired
+  private WxMpService wxMpService;
+  private static final String MP_TOKEN = "U2qmxEbsp0PJFoLRvUDvIjVi9XPzuVc2";
+
+  private static final String EncodingAESKey = "P3HE7Gd1PJVQqCLoOMop5uYfjx9LwfY53rnC3VUuLZS";
+
+  @Autowired
+  private IFsUserService userService;
+
+  @Autowired
+  JwtUtils jwtUtils;
+  @Autowired
+  RedisCache redisCache;
+
+  @Autowired
+  private ConfigUtil configUtil;
+
+  @Autowired
+  private SysConfigMapper sysConfigMapper;
+
+  @Autowired
+  FsCourseWatchLogMapper fsCourseWatchLogMapper;
+  @Autowired
+  QwExternalContactMapper qwExternalContactMapper;
+  @Autowired
+  private FsCourseSopLogsMapper courseSopLogsMapper;
+
+
+  //公众号事件回调
+  @PostMapping
+  public String checkWxToken(@RequestParam(value = "signature", required = false) String signature,
+                             @RequestParam(value = "timestamp", required = false) String timestamp,
+                             @RequestParam(name = "nonce", required = false) String nonce,
+                             @RequestParam(value = "echostr", required = false) String echostr,
+                             @RequestBody(required = false) WxServiceMsgDto wxServiceMsgDto) throws WxErrorException{
+    // 只处理订阅与取消订阅消息
+    if (null != wxServiceMsgDto && StringUtils.isNotEmpty(wxServiceMsgDto.getMsgType()) &&
+            StringUtils.isNotEmpty(wxServiceMsgDto.getEvent())) {
+      String msgType = wxServiceMsgDto.getMsgType();
+      String event = wxServiceMsgDto.getEvent();
+      // 判断是否是订阅或取消订阅事件
+      boolean isSubscribeEvent = WxMessageType.EVENT.getCode().equals(msgType) &&
+              (WxEeventType.SUBSCRIBE.getCode().equals(event) || WxEeventType.UNSUBSCRIBE.getCode().equals(event));
+      if (isSubscribeEvent) {
+        WxEeventType wxEeventType = WxEeventType.SUBSCRIBE.getCode().equals(event) ? WxEeventType.SUBSCRIBE : WxEeventType.UNSUBSCRIBE;
+        logger.info("公众号关注回调:{}",wxEeventType);
+        logger.info("公众号关注回调详情:{}",wxServiceMsgDto);
+        WxMpUserService wxMpUserService = wxMpService.getUserService();
+        WxMpUser wxMpUser = wxMpUserService.userInfo(wxServiceMsgDto.getFromUserName());
+        logger.info("获取用户信息:{}",wxMpUser);
+        if (wxMpUser != null && wxMpUser.getUnionId()!=null) {
+          FsUser user = userService.selectFsUserByUnionid(wxMpUser.getUnionId());
+          if (user != null) {
+            // 根据事件类型更新 isOfficialAccountAuth 字段
+            if (WxEeventType.SUBSCRIBE.equals(wxEeventType)) {
+              user.setIsOfficialAccountAuth(1); // 设置为已关注
+            } else if (WxEeventType.UNSUBSCRIBE.equals(wxEeventType)) {
+              user.setIsOfficialAccountAuth(0); // 设置为取消关注
+            }
+            userService.updateFsUser(user); // 保存更新后的用户信息
+          }else {
+            //写入
+            String code=genCode();
+            user=new FsUser();
+//            user.setPhone(code);
+            user.setNickName("匿名用户**");
+            user.setAvatar("https://hos-1309931967.cos.ap-chongqing.myqcloud.com/fs/20230725/a848605591384ec29d49773dd58d9345.jpg");
+            user.setStatus(1);
+            user.setMpOpenId(wxMpUser.getOpenId());
+            user.setUnionId(wxMpUser.getUnionId());
+            user.setIsOfficialAccountAuth(wxMpUser.getSubscribe()? 1 : 0);
+            user.setCreateTime(new Date());
+            userService.insertFsUser(user);
+          }
+        }
+      }
+    }
+    return "success";
+  }
+
+  @GetMapping
+  public String checkWxToken(
+          @RequestParam("signature") String signature,
+          @RequestParam("timestamp") String timestamp,
+          @RequestParam("nonce") String nonce,
+          @RequestParam("echostr") String echostr) {
+    logger.info("数据回调URLServer-微信调用dataGet请求");
+    boolean check = wxMpService.checkSignature(timestamp,nonce,signature);
+    logger.info("验证结果:{}",check);
+    if (check){
+      return echostr;
+    }
+    return "error";
+  }
+
+  @PostMapping("/createMenu")
+  public R createMenu(@RequestBody WxMenu wxMenu) throws WxErrorException{
+    WxMpMenuService wxMpMenuService = wxMpService.getMenuService();
+    try{
+      wxMpMenuService.menuCreate(wxMenu);
+      return R.ok("创建成功");
+    }catch (WxErrorException e){
+      logger.error(e.getMessage());
+      return R.error("创建失败");
+    }
+  }
+
+  @ApiOperation("课程短链公众号登录")
+  @PostMapping("/loginByMp")
+  @Transactional
+  public R loginByMp( @RequestBody FsUserLoginByMpParam param) {
+
+    if (StringUtils.isBlank(param.getCode())) {
+      return R.error("code不存在");
+    }
+    try{
+      WxOAuth2AccessToken wxMpOAuth2AccessToken = wxMpService.getOAuth2Service().getAccessToken(param.getCode());
+      WxOAuth2UserInfo wxMpUser = wxMpService.getOAuth2Service().getUserInfo(wxMpOAuth2AccessToken, null);
+      WxMpUserService wxMpUserService = wxMpService.getUserService();
+      WxMpUser userInfo = wxMpUserService.userInfo(wxMpUser.getOpenid());
+//      if (!userInfo.getSubscribe()){
+//        return R.error("请关注公众号进行登录");
+//      }
+      FsUser user=userService.selectFsUserByUnionid(wxMpUser.getUnionId());
+      if(user!=null){
+        FsUser userMap=new FsUser();
+        userMap.setUserId(user.getUserId());
+        userMap.setNickName(wxMpUser.getNickname());
+        userMap.setAvatar(wxMpUser.getHeadImgUrl());
+        userMap.setMpOpenId(wxMpUser.getOpenid());
+        userMap.setUpdateTime(new DateTime());
+        userService.updateFsUser(userMap);
+      }
+      else{
+        //写入
+        String code=genCode();
+        user=new FsUser();
+//        user.setPhone(code);
+        user.setNickName(wxMpUser.getNickname());
+        user.setAvatar(wxMpUser.getHeadImgUrl());
+        user.setStatus(1);
+        user.setSex(wxMpUser.getSex());
+        user.setMpOpenId(wxMpUser.getOpenid());
+        user.setUnionId(wxMpUser.getUnionId());
+        user.setCreateTime(new Date());
+        userService.insertFsUser(user);
+      }
+      System.out.println("user:id:"+user.getUserId());
+      String token = jwtUtils.generateToken(user.getUserId());
+      redisCache.setCacheObject("token:"+user.getUserId(),token,604800, TimeUnit.SECONDS);
+      Map<String,Object> map=new HashMap<>();
+      map.put("token",token);
+      map.put("user",user);
+      logger.info("zyp \n 【点播公众号登录】:{}",user.getUserId());
+      return R.ok(map);
+
+    }
+    catch (WxErrorException e){
+      if(e.getError().getErrorCode()==40163){
+        return R.error(40163,e.getError().getErrorMsg());
+      }
+      else{
+        return R.error("授权失败,"+e.getMessage());
+      }
+    }
+
+  }
+
+
+
+  private Date setSendTime(QwSopSettingTimeParam params) {
+    Date currentTime = new Date();
+    Calendar calendar = Calendar.getInstance();
+    calendar.setTime(currentTime);
+
+    try {
+      if (params.getType() == 1) {
+        int hourToAdd = Integer.parseInt(params.getHour());
+        int minuteToAdd = Integer.parseInt(params.getMinute());
+        calendar.add(Calendar.HOUR, hourToAdd);
+        calendar.add(Calendar.MINUTE, minuteToAdd);
+      } else if (params.getType() == 2) {
+        calendar.add(Calendar.DAY_OF_MONTH, Integer.parseInt(params.getDay()));
+        SimpleDateFormat timeFormatter = new SimpleDateFormat("HH:mm");
+        Date parsedTime = timeFormatter.parse(params.getTime());
+        Calendar timeCalendar = Calendar.getInstance();
+        timeCalendar.setTime(parsedTime);
+        calendar.set(Calendar.HOUR_OF_DAY, timeCalendar.get(Calendar.HOUR_OF_DAY));
+        calendar.set(Calendar.MINUTE, timeCalendar.get(Calendar.MINUTE));
+        calendar.set(Calendar.SECOND, 0);
+      }
+    } catch (Exception e) {
+      e.printStackTrace(); // 或者更好的异常处理
+    }
+
+    return calendar.getTime();
+  }
+    @GetMapping("/getWxConfig")
+    @Synchronized
+    public R getWxConfig(@RequestParam String url) throws WxErrorException {
+      try {
+        String sLink = URLDecoder.decode(url, "UTF-8");
+        final WxJsapiSignature jsapiSignature = wxMpService.createJsapiSignature(sLink);
+        return R.ok().put("data", jsapiSignature);
+      } catch (UnsupportedEncodingException e) {
+        // URL解码异常
+        return R.error(e.getMessage());
+      }
+
+    }
+
+
+
+
+}

+ 371 - 0
fs-user-course/src/main/java/com/fs/course/controller/WxUserController.java

@@ -0,0 +1,371 @@
+package com.fs.course.controller;
+
+import cn.binarywang.wx.miniapp.api.WxMaService;
+import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult;
+import cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo;
+import cn.binarywang.wx.miniapp.bean.WxMaUserInfo;
+import cn.hutool.core.date.DateTime;
+import com.alibaba.fastjson.JSON;
+import com.fs.course.annotation.Login;
+import com.fs.course.utils.JwtUtils;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.common.param.LoginParam;
+import com.fs.common.utils.DateUtils;
+import com.fs.core.config.WxMaConfiguration;
+import com.fs.course.config.CourseMaConfig;
+import com.fs.his.config.FsSysConfig;
+import com.fs.his.domain.FsUser;
+import com.fs.his.domain.FsUserLoginLog;
+import com.fs.his.mapper.FsUserLoginLogMapper;
+import com.fs.his.service.IFsUserService;
+import com.fs.his.utils.ConfigUtil;
+import com.fs.system.domain.SysConfig;
+import com.fs.system.mapper.SysConfigMapper;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.Synchronized;
+import me.chanjar.weixin.common.bean.WxOAuth2UserInfo;
+import me.chanjar.weixin.common.bean.oauth2.WxOAuth2AccessToken;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.mp.api.WxMpService;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.bind.annotation.*;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
+
+import static com.fs.his.utils.PhoneUtil.encryptPhone;
+
+/**
+ * 微信小程序用户接口
+ *
+ */
+@Api("微信接口")
+@RestController
+@RequestMapping(value="/app/wx")
+public class WxUserController extends AppBaseController{
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+    @Autowired
+    private ConfigUtil configUtil;
+    @Autowired
+    private IFsUserService userService;
+    @Autowired
+    FsUserLoginLogMapper fsUserLoginLogMapper;
+    @Autowired
+    private SysConfigMapper sysConfigMapper;
+    @Autowired
+    JwtUtils jwtUtils;
+    @Autowired
+    RedisCache redisCache;
+
+    @Autowired
+    private WxMpService wxMpService;
+
+
+
+
+    /**
+     * 登陆接口
+     */
+    /**
+     * 登陆接口
+     */
+    @ApiOperation("登录")
+    @PostMapping("/login")
+    @Transactional
+    public R login( @RequestBody LoginParam param) {
+        if (StringUtils.isBlank(param.getCode())) {
+            return R.error("code不存在");
+        }
+        FsSysConfig con = configUtil.getSysConfig();
+        final WxMaService wxService = WxMaConfiguration.getMaService(con.getAppid());
+        try {
+            WxMaJscode2SessionResult session = wxService.getUserService().getSessionInfo(param.getCode());
+            this.logger.info(session.getSessionKey());
+            this.logger.info(session.getOpenid());
+            // 解密
+            WxMaPhoneNumberInfo phoneNoInfo = wxService.getUserService().getPhoneNoInfo(session.getSessionKey(), param.getEncryptedData(), param.getIv());
+            //三端用户同步,先用unionid查询
+            FsUser user = new FsUser();
+            if (session.getUnionid()!=null){
+                user = userService.selectFsUserByUnionid(session.getUnionid());
+                if (user==null){
+                    user = userService.selectFsUserByOpenId(session.getOpenid());
+                }
+            }else {
+                user = userService.selectFsUserByOpenId(session.getOpenid());
+            }
+
+            if(user==null){
+                //新用户
+                String phoneNumber = phoneNoInfo.getPhoneNumber();
+
+
+                //查询手机号是否存在,如果存在,更新
+                FsUser checkPhone=userService.selectFsUserByPhone(encryptPhone(phoneNumber));
+                if (checkPhone==null){
+                    checkPhone=userService.selectFsUserByPhone(phoneNumber);
+                }
+                if(checkPhone!=null){
+                    user=checkPhone;
+                    FsUser userMap=new FsUser();
+                    userMap.setMaOpenId(session.getOpenid());
+                    userMap.setUserId(checkPhone.getUserId());
+                    userMap.setUpdateTime(new DateTime());
+                    if(session.getUnionid()!=null){
+                        userMap.setUnionId(session.getUnionid());
+                    }
+                    userService.updateFsUser(userMap);
+                }
+                else{
+                    //写入
+                    user=new FsUser();
+                    user.setPhone(phoneNoInfo.getPhoneNumber());
+                    user.setNickName("微信用户"+phoneNoInfo.getPhoneNumber().substring(phoneNoInfo.getPhoneNumber().length()-4));
+                    user.setAvatar("https://hos-1309931967.cos.ap-chongqing.myqcloud.com/fs/20230725/a848605591384ec29d49773dd58d9345.jpg");
+                    user.setStatus(1);
+                    user.setMaOpenId(session.getOpenid());
+                    user.setCreateTime(new Date());
+                    if(session.getUnionid()!=null){
+                        user.setUnionId(session.getUnionid());
+                    }
+                    userService.insertFsUser(user);
+                }
+            }
+            else{
+                if(user.getStatus().equals(0)){
+
+                    return R.error("此会员已禁用");
+                }
+                FsUser userMap=new FsUser();
+                userMap.setUserId(user.getUserId());
+                if(session.getUnionid()!=null){
+                    userMap.setUnionId(session.getUnionid());
+                }
+                userMap.setMaOpenId(session.getOpenid());
+                userMap.setPhone(phoneNoInfo.getPhoneNumber());
+                userMap.setUpdateTime(new DateTime());
+                userService.updateFsUser(userMap);
+            }
+            String token = jwtUtils.generateToken(user.getUserId());
+//            redisCache.setCacheObject("token:"+user.getUserId(),token,365, TimeUnit.DAYS);
+            Map<String,Object> map=new HashMap<>();
+            map.put("token",token);
+            user.setPhone(encryptPhone(phoneNoInfo.getPhoneNumber()));
+            map.put("user",user);
+            FsUserLoginLog log = new FsUserLoginLog();
+            log.setCode(param.getCode());
+            log.setLoginJson(JSON.toJSONString(param));
+//            log.setUserRegisterJson(JSON.toJSONString(jsonMap));
+            log.setStatus(1);
+            log.setUserId(user.getUserId());
+            log.setPhone(user.getPhone());
+            log.setMaOpenId(user.getMaOpenId());
+            log.setCreateTime(DateUtils.getNowDate());
+            fsUserLoginLogMapper.insertFsUserLoginLog(log);
+            return R.ok(map);
+        } catch (WxErrorException e) {
+            //this.logger.error(e.getMessage(), e);
+            return R.error("授权失败,"+e.getMessage());
+        }
+    }
+
+    @ApiOperation("小程序看课登录")
+    @PostMapping("/courseLogin")
+    @Transactional
+    public R courseLogin(@RequestBody LoginParam param) {
+        SysConfig sysConfig3 = sysConfigMapper.selectConfigByConfigKey("courseMa.config");
+        List<CourseMaConfig> courseMaConfigs = JSON.parseArray(sysConfig3.getConfigValue(), CourseMaConfig.class);
+        if (courseMaConfigs.isEmpty()){
+            return R.error("小程序配置为空");
+        }
+        CourseMaConfig courseMaConfig = courseMaConfigs.get(0);
+        return handleCourseLogin(param,
+                () -> WxMaConfiguration.getMaService(courseMaConfig.getAppid()),
+                courseMaConfig.getName());
+    }
+
+    /**
+     * 公共登录处理方法
+     * @param param 登录参数
+     * @param wxServiceSupplier 微信服务提供函数(差异化点1:不同方式获取WxMaService)
+     * @param logName 日志名称(差异化点2:不同场景标识)
+     */
+    private R handleCourseLogin(LoginParam param, Supplier<WxMaService> wxServiceSupplier, String logName) {
+        if (StringUtils.isBlank(param.getCode())) {
+            return R.error("code不存在");
+        }
+
+        try {
+            // 通过函数式接口获取不同的微信服务实例
+            final WxMaService wxService = wxServiceSupplier.get();
+            WxMaJscode2SessionResult session = wxService.getUserService().getSessionInfo(param.getCode());
+            this.logger.info("获取{} Session:{}", logName, session);
+
+            FsUser user = userService.selectFsUserByUnionid(session.getUnionid());
+            boolean isNewUser = false;
+
+            // 用户存在时的更新逻辑
+            if(user != null){
+                FsUser userMap = new FsUser();
+                userMap.setUserId(user.getUserId());
+                userMap.setCourseMaOpenId(session.getOpenid());
+                userMap.setUpdateTime(new DateTime());
+                userService.updateFsUser(userMap);
+            }
+            // 用户不存在时的创建逻辑
+            else {
+                user = new FsUser();
+                user.setNickName("微信用户");
+                user.setAvatar("https://hos-1309931967.cos.ap-chongqing.myqcloud.com/fs/20230725/a848605591384ec29d49773dd58d9345.jpg");
+                user.setStatus(1);
+                user.setCourseMaOpenId(session.getOpenid());
+                user.setUnionId(session.getUnionid());
+                user.setCreateTime(new Date());
+                userService.insertFsUser(user);
+                isNewUser = true;
+            }
+
+            // 生成Token
+            String token = jwtUtils.generateToken(user.getUserId());
+            Map<String,Object> map = new HashMap<>();
+            map.put("token", token);
+            map.put("user", user);
+            map.put("isNew", isNewUser);
+
+            logger.info("zyp \n 【点播{}登录】:{}", logName, user.getUserId());
+            return R.ok(map);
+        } catch (WxErrorException e){
+            logger.error("{}授权失败:{}", logName, e.getMessage(), e);
+            return R.error("授权失败," + e.getMessage());
+        }
+    }
+
+
+    public static String genCode() {
+        String year = new SimpleDateFormat("yy").format(new Date());
+        String day = String.format("%tj", new Date());
+        double random = Math.random() * 10000000;
+        while (random < 1000000) {
+            random = Math.random() * 10000000;
+        }
+        int intRandom = Double.valueOf(random).intValue();
+        String verifyCode = year + day + intRandom;
+        return verifyCode;
+    }
+
+
+
+    @ApiOperation("公众号登录")
+    @PostMapping("/loginByMp")
+    public R loginByMp( @RequestBody LoginParam param) {
+        if (StringUtils.isBlank(param.getCode())) {
+            return R.error("code不存在");
+        }
+        try{
+            WxOAuth2AccessToken wxMpOAuth2AccessToken = wxMpService.getOAuth2Service().getAccessToken(param.getCode());
+            WxOAuth2UserInfo wxMpUser = wxMpService.getOAuth2Service().getUserInfo(wxMpOAuth2AccessToken, null);
+            FsUser user=userService.selectFsUserByUnionid(wxMpUser.getUnionId());
+            if(user!=null){
+                FsUser userMap=new FsUser();
+                userMap.setUserId(user.getUserId());
+                userMap.setNickName(wxMpUser.getNickname());
+                userMap.setAvatar(wxMpUser.getHeadImgUrl());
+                userMap.setMpOpenId(wxMpUser.getOpenid());
+                userMap.setUpdateTime(new DateTime());
+                userService.updateFsUser(userMap);
+            }
+            else{
+                //写入
+                String code=genCode();
+                user=new FsUser();
+                user.setNickName(wxMpUser.getNickname());
+                user.setAvatar(wxMpUser.getHeadImgUrl());
+                user.setStatus(1);
+                user.setSex(wxMpUser.getSex());
+                user.setMpOpenId(wxMpUser.getOpenid());
+                user.setUnionId(wxMpUser.getUnionId());
+                user.setCreateTime(new Date());
+                userService.insertFsUser(user);
+            }
+            String token = jwtUtils.generateToken(user.getUserId());
+            redisCache.setCacheObject("token:"+user.getUserId(),token,604800, TimeUnit.SECONDS);
+            Map<String,Object> map=new HashMap<>();
+            map.put("token",token);
+            map.put("user",user);
+
+            FsUserLoginLog log = new FsUserLoginLog();
+            log.setCode(param.getCode());
+            log.setLoginJson(JSON.toJSONString(param));
+//            log.setUserRegisterJson(JSON.toJSONString(jsonMap));
+            log.setStatus(1);
+            log.setUserId(user.getUserId());
+            log.setPhone(user.getPhone());
+            log.setMaOpenId(user.getMaOpenId());
+            log.setCreateTime(DateUtils.getNowDate());
+            fsUserLoginLogMapper.insertFsUserLoginLog(log);
+            return R.ok(map);
+        }
+        catch (WxErrorException e){
+            if(e.getError().getErrorCode()==40163){
+                return R.error(40163,e.getError().getErrorMsg());
+            }
+            else{
+                return R.error("授权失败,"+e.getMessage());
+            }
+        }
+
+    }
+
+    /**
+     * <pre>
+     * 获取微信用户信息
+     * </pre>
+     */
+    @Login
+    @ApiOperation("获取微信小程序用户信息")
+    @PostMapping("/getWeixinInfo")
+    public R getWeixinInfo(@RequestBody LoginParam param) {
+        FsSysConfig con = configUtil.getSysConfig();
+        final WxMaService wxService = WxMaConfiguration.getMaService(con.getAppid());
+        try {
+            WxMaJscode2SessionResult session = wxService.getUserService().getSessionInfo(param.getCode());
+            // 用户信息校验
+            if (!wxService.getUserService().checkUserInfo(session.getSessionKey(), param.getRawData(), param.getSignature())) {
+                return R.error("user check failed");
+            }
+            // 解密用户信息
+            WxMaUserInfo userInfo = wxService.getUserService().getUserInfo(session.getSessionKey(), param.getEncryptedData(), param.getIv());
+            FsUser user=userService.selectFsUserByUserId(Long.parseLong(getUserId()));
+            user.setNickName(userInfo.getNickName());
+            user.setAvatar(userInfo.getAvatarUrl());
+            user.setIsWeixinAuth(1);
+            userService.updateFsUser(user);
+            return R.ok();
+        } catch (WxErrorException e) {
+            e.printStackTrace();
+        }
+        return R.ok("授权成功");
+    }
+
+
+    @GetMapping("/loginTest")
+    @Synchronized
+    public R loginByMp(String phone) {
+        //如果开启了UnionId
+        FsUser user=userService.selectFsUserByMpOpenId(phone);
+        String token = jwtUtils.generateToken(user.getUserId());
+        return R.ok("登录成功").put("token",token).put("user", user);
+    }
+
+}

+ 51 - 0
fs-user-course/src/main/java/com/fs/course/exception/FSException.java

@@ -0,0 +1,51 @@
+package com.fs.course.exception;
+
+/**
+ * 自定义异常
+ */
+public class FSException extends RuntimeException {
+	private static final long serialVersionUID = 1L;
+
+    private String msg;
+    private int code = 500;
+
+    public FSException(String msg) {
+		super(msg);
+		this.msg = msg;
+	}
+
+	public FSException(String msg, Throwable e) {
+		super(msg, e);
+		this.msg = msg;
+	}
+
+	public FSException(String msg, int code) {
+		super(msg);
+		this.msg = msg;
+		this.code = code;
+	}
+
+	public FSException(String msg, int code, Throwable e) {
+		super(msg, e);
+		this.msg = msg;
+		this.code = code;
+	}
+
+	public String getMsg() {
+		return msg;
+	}
+
+	public void setMsg(String msg) {
+		this.msg = msg;
+	}
+
+	public int getCode() {
+		return code;
+	}
+
+	public void setCode(int code) {
+		this.code = code;
+	}
+
+
+}

+ 68 - 0
fs-user-course/src/main/java/com/fs/course/interceptor/AuthorizationInterceptor.java

@@ -0,0 +1,68 @@
+package com.fs.course.interceptor;
+
+
+import com.fs.course.annotation.Login;
+import com.fs.course.exception.FSException;
+import com.fs.course.utils.JwtUtils;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.common.utils.StringUtils;
+import io.jsonwebtoken.Claims;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Component;
+import org.springframework.web.method.HandlerMethod;
+import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * 权限(Token)验证
+ */
+@Component
+public class AuthorizationInterceptor extends HandlerInterceptorAdapter {
+    @Autowired
+    private JwtUtils jwtUtils;
+    @Autowired
+    RedisCache redisCache;
+    public static final String USER_KEY = "userId";
+
+    @Override
+    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
+        Login annotation;
+        if(handler instanceof HandlerMethod) {
+            annotation = ((HandlerMethod) handler).getMethodAnnotation(Login.class);
+        }else{
+            return true;
+        }
+
+        if(annotation == null){
+            return true;
+        }
+
+        //获取用户凭证
+        String token = request.getHeader(jwtUtils.getHeader());
+        if(StringUtils.isBlank(token)){
+            token = request.getParameter(jwtUtils.getHeader());
+        }
+
+        //凭证为空
+        if(StringUtils.isBlank(token)){
+            throw new FSException(jwtUtils.getHeader() + "不能为空", HttpStatus.UNAUTHORIZED.value());
+        }
+
+        Claims claims = jwtUtils.getClaimByToken(token);
+        if(claims == null || jwtUtils.isTokenExpired(claims.getExpiration())){
+            throw new FSException(jwtUtils.getHeader() + "失效,请重新登录", HttpStatus.UNAUTHORIZED.value());
+        }
+        //查询用户的TOKEN是否和REDIS中的一样
+//        String redisToken=redisCache.getCacheObject("token:"+ Long.parseLong(claims.getSubject()));
+//        if(redisToken==null||!redisToken.equals(token)){
+//            throw new FSException(jwtUtils.getHeader() + "失效,请重新登录", HttpStatus.UNAUTHORIZED.value());
+//        }
+        //设置userId到request里,后续根据userId,获取用户信息
+        request.setAttribute(USER_KEY, Long.parseLong(claims.getSubject()));
+
+        return true;
+    }
+}

+ 14 - 0
fs-user-course/src/main/java/com/fs/course/param/FsBindCompanyUserParam.java

@@ -0,0 +1,14 @@
+package com.fs.course.param;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import java.io.Serializable;
+
+@Getter
+@Setter
+public class FsBindCompanyUserParam implements Serializable {
+
+    private Long companyUserId;
+
+}

+ 95 - 0
fs-user-course/src/main/java/com/fs/course/param/FsDoctorRegisterParam.java

@@ -0,0 +1,95 @@
+package com.fs.course.param;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import java.io.Serializable;
+import java.util.Date;
+
+@Getter
+@Setter
+public class FsDoctorRegisterParam implements Serializable {
+
+
+
+    @NotBlank(message = "医生姓名不能为空")
+    private String doctorName;
+    /** 简介 */
+    private String introduction;
+
+    /** 擅长领域 */
+    @NotBlank(message = "擅长领域不能为空")
+    private String speciality;
+
+    /** 资格证编号 */
+    private String certificateCode;
+
+    /** 证书照片 */
+    private String certificateImages;
+
+    /** 工作照 */
+    @NotBlank(message = "工作照不能为空")
+    private String avatar;
+
+
+
+    /** 所属医院 */
+    @NotNull(message = "所属医院不能为空")
+    private Long hospitalId;
+
+    /** 科室ID */
+    @NotNull(message = "所属科室不能为空")
+    private Long deptId;
+
+
+
+    /** 职务 */
+    @NotBlank(message = "职务不能为空")
+    private String position;
+
+
+    @NotBlank(message = "手机号不能为空")
+    private String mobile;
+
+    /** 医生类型 1医生 2药师 */
+    @NotNull(message = "医生类型不能为空")
+    private Integer doctorType;
+
+    /** 性别 */
+    @NotNull(message = "性别不能为空")
+    private Integer sex;
+
+
+    private Date bitthday;
+
+    /** 身份证号 */
+    @NotBlank(message = "身份证号不能为空")
+    private String idCard;
+
+    /** 身份证正面 */
+    @NotBlank(message = "身份证正面照片不能为空")
+    private String idCardFrontUrl;
+
+    /** 身份证反面 */
+    @NotBlank(message = "身份证反面照片不能为空")
+    private String idCardBackUrl;
+
+    /** 所属省市区 */
+    @NotBlank(message = "所属省市区不能为空")
+    private String cityIds;
+
+    private String province;
+
+    private String city;
+
+    private String district;
+
+    @NotBlank(message = "帐户不能为空")
+    private String account;
+    @NotBlank(message = "密码不能为空")
+//    @Pattern(regexp="^(?![a-zA-Z]+$)(?![A-Z0-9]+$)(?![A-Z\\W_]+$)(?![a-z0-9]+$)(?![a-z\\W_]+$)(?![0-9\\W_]+$)[a-zA-Z0-9\\W_]{8,}$",message="密码长度最少8位,由数字、大写字母、小写字母、特殊字符中的至少三种组成")
+    private String password;
+
+}

+ 24 - 0
fs-user-course/src/main/java/com/fs/course/param/FsUserEditParam.java

@@ -0,0 +1,24 @@
+package com.fs.course.param;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import java.io.Serializable;
+
+@Getter
+@Setter
+public class FsUserEditParam implements Serializable {
+
+    @NotBlank(message = "头像不能为空")
+    private String avatar;
+    @NotBlank(message = "昵称不能为空")
+    private String nickName;
+    @NotBlank(message = "手机号不能为空")
+    private String phone;
+    @NotNull(message = "性别不能为空")
+    private Integer sex;
+    private Integer isWeixinAuth;
+
+}

+ 13 - 0
fs-user-course/src/main/java/com/fs/course/param/FsUserLoginByMpParam.java

@@ -0,0 +1,13 @@
+package com.fs.course.param;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+import java.io.Serializable;
+
+@Data
+public class FsUserLoginByMpParam implements Serializable {
+    @NotBlank(message = "code参数缺失")
+    private String code;
+    private Long videoId;
+}

+ 88 - 0
fs-user-course/src/main/java/com/fs/course/utils/JwtUtils.java

@@ -0,0 +1,88 @@
+package com.fs.course.utils;
+
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+import java.util.Date;
+
+/**
+ * jwt工具类
+
+ */
+@ConfigurationProperties(prefix = "fs.jwt")
+@Component
+public class JwtUtils {
+    private Logger logger = LoggerFactory.getLogger(getClass());
+
+
+    private String secret;
+    private long expire;
+    private String header;
+
+    /**
+     * 生成jwt token
+     */
+    public String generateToken(long userId) {
+        Date nowDate = new Date();
+        //过期时间
+        Date expireDate = new Date(nowDate.getTime() + expire * 1000);
+//        System.out.println("==============================="+secret);
+//        logger.info("secret:{}",secret);
+        return Jwts.builder()
+                .setHeaderParam("typ", "JWT")
+                .setSubject(userId+"")
+                .setIssuedAt(nowDate)
+                .setExpiration(expireDate)
+                .signWith(SignatureAlgorithm.HS512, secret)
+                .compact();
+    }
+
+    public Claims getClaimByToken(String token) {
+        try {
+            return Jwts.parser()
+                    .setSigningKey(secret)
+                    .parseClaimsJws(token)
+                    .getBody();
+        }catch (Exception e){
+            logger.debug("validate is token error ", e);
+            return null;
+        }
+    }
+
+    /**
+     * token是否过期
+     * @return  true:过期
+     */
+    public boolean isTokenExpired(Date expiration) {
+        return expiration.before(new Date());
+    }
+
+    public String getSecret() {
+        return secret;
+    }
+
+    public void setSecret(String secret) {
+        this.secret = secret;
+    }
+
+    public long getExpire() {
+        return expire;
+    }
+
+    public void setExpire(long expire) {
+        this.expire = expire;
+    }
+
+    public String getHeader() {
+        return header;
+    }
+
+    public void setHeader(String header) {
+        this.header = header;
+    }
+}

+ 23 - 0
fs-user-course/src/main/java/com/fs/course/vo/CityVO.java

@@ -0,0 +1,23 @@
+package com.fs.course.vo;
+
+import lombok.Getter;
+import lombok.Setter;
+import lombok.ToString;
+
+import java.io.Serializable;
+import java.util.List;
+
+@Getter
+@Setter
+@ToString
+public class CityVO implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    private String v; //id
+
+    private String n; //名称
+
+    private String pid;
+
+    private List<CityVO> c; //子集
+}

+ 182 - 0
fs-user-course/src/main/java/com/fs/framework/aspectj/DataScopeAspect.java

@@ -0,0 +1,182 @@
+package com.fs.framework.aspectj;
+
+import com.fs.common.annotation.DataScope;
+import com.fs.common.core.domain.BaseEntity;
+import com.fs.common.core.domain.entity.SysRole;
+import com.fs.common.core.domain.entity.SysUser;
+import com.fs.common.core.domain.model.LoginUser;
+import com.fs.common.utils.SecurityUtils;
+import com.fs.common.utils.StringUtils;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.Signature;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Before;
+import org.aspectj.lang.annotation.Pointcut;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.stereotype.Component;
+
+import java.lang.reflect.Method;
+
+/**
+ * 数据过滤处理
+ *
+
+ */
+@Aspect
+@Component
+public class DataScopeAspect
+{
+    /**
+     * 全部数据权限
+     */
+    public static final String DATA_SCOPE_ALL = "1";
+
+    /**
+     * 自定数据权限
+     */
+    public static final String DATA_SCOPE_CUSTOM = "2";
+
+    /**
+     * 部门数据权限
+     */
+    public static final String DATA_SCOPE_DEPT = "3";
+
+    /**
+     * 部门及以下数据权限
+     */
+    public static final String DATA_SCOPE_DEPT_AND_CHILD = "4";
+
+    /**
+     * 仅本人数据权限
+     */
+    public static final String DATA_SCOPE_SELF = "5";
+
+    /**
+     * 数据权限过滤关键字
+     */
+    public static final String DATA_SCOPE = "dataScope";
+
+    // 配置织入点
+    @Pointcut("@annotation(com.fs.common.annotation.DataScope)")
+    public void dataScopePointCut()
+    {
+    }
+
+    @Before("dataScopePointCut()")
+    public void doBefore(JoinPoint point) throws Throwable
+    {
+        clearDataScope(point);
+        handleDataScope(point);
+    }
+
+    protected void handleDataScope(final JoinPoint joinPoint)
+    {
+        // 获得注解
+        DataScope controllerDataScope = getAnnotationLog(joinPoint);
+        if (controllerDataScope == null)
+        {
+            return;
+        }
+        // 获取当前的用户
+        LoginUser loginUser = SecurityUtils.getLoginUser();
+        if (StringUtils.isNotNull(loginUser))
+        {
+            SysUser currentUser = loginUser.getUser();
+            // 如果是超级管理员,则不过滤数据
+            if (StringUtils.isNotNull(currentUser) && !currentUser.isAdmin())
+            {
+                dataScopeFilter(joinPoint, currentUser, controllerDataScope.deptAlias(),
+                        controllerDataScope.userAlias());
+            }
+        }
+    }
+
+    /**
+     * 数据范围过滤
+     *
+     * @param joinPoint 切点
+     * @param user 用户
+     * @param userAlias 别名
+     */
+    public static void dataScopeFilter(JoinPoint joinPoint, SysUser user, String deptAlias, String userAlias)
+    {
+        StringBuilder sqlString = new StringBuilder();
+
+        for (SysRole role : user.getRoles())
+        {
+            String dataScope = role.getDataScope();
+            if (DATA_SCOPE_ALL.equals(dataScope))
+            {
+                sqlString = new StringBuilder();
+                break;
+            }
+            else if (DATA_SCOPE_CUSTOM.equals(dataScope))
+            {
+                sqlString.append(StringUtils.format(
+                        " OR {}.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id = {} ) ", deptAlias,
+                        role.getRoleId()));
+            }
+            else if (DATA_SCOPE_DEPT.equals(dataScope))
+            {
+                sqlString.append(StringUtils.format(" OR {}.dept_id = {} ", deptAlias, user.getDeptId()));
+            }
+            else if (DATA_SCOPE_DEPT_AND_CHILD.equals(dataScope))
+            {
+                sqlString.append(StringUtils.format(
+                        " OR {}.dept_id IN ( SELECT dept_id FROM sys_dept WHERE dept_id = {} or find_in_set( {} , ancestors ) )",
+                        deptAlias, user.getDeptId(), user.getDeptId()));
+            }
+            else if (DATA_SCOPE_SELF.equals(dataScope))
+            {
+                if (StringUtils.isNotBlank(userAlias))
+                {
+                    sqlString.append(StringUtils.format(" OR {}.user_id = {} ", userAlias, user.getUserId()));
+                }
+                else
+                {
+                    // 数据权限为仅本人且没有userAlias别名不查询任何数据
+                    sqlString.append(" OR 1=0 ");
+                }
+            }
+        }
+
+        if (StringUtils.isNotBlank(sqlString.toString()))
+        {
+            Object params = joinPoint.getArgs()[0];
+            if (StringUtils.isNotNull(params) && params instanceof BaseEntity)
+            {
+                BaseEntity baseEntity = (BaseEntity) params;
+                baseEntity.getParams().put(DATA_SCOPE, " AND (" + sqlString.substring(4) + ")");
+            }
+        }
+    }
+
+    /**
+     * 是否存在注解,如果存在就获取
+     */
+    private DataScope getAnnotationLog(JoinPoint joinPoint)
+    {
+        Signature signature = joinPoint.getSignature();
+        MethodSignature methodSignature = (MethodSignature) signature;
+        Method method = methodSignature.getMethod();
+
+        if (method != null)
+        {
+            return method.getAnnotation(DataScope.class);
+        }
+        return null;
+    }
+
+    /**
+     * 拼接权限sql前先清空params.dataScope参数防止注入
+     */
+    private void clearDataScope(final JoinPoint joinPoint)
+    {
+        Object params = joinPoint.getArgs()[0];
+        if (StringUtils.isNotNull(params) && params instanceof BaseEntity)
+        {
+            BaseEntity baseEntity = (BaseEntity) params;
+            baseEntity.getParams().put(DATA_SCOPE, "");
+        }
+    }
+}

+ 73 - 0
fs-user-course/src/main/java/com/fs/framework/aspectj/DataSourceAspect.java

@@ -0,0 +1,73 @@
+package com.fs.framework.aspectj;
+
+import com.fs.common.annotation.DataSource;
+import com.fs.common.utils.StringUtils;
+import com.fs.framework.datasource.DynamicDataSourceContextHolder;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Pointcut;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+
+import java.util.Objects;
+
+/**
+ * 多数据源处理
+ *
+
+ */
+@Aspect
+@Order(1)
+@Component
+public class DataSourceAspect
+{
+    protected Logger logger = LoggerFactory.getLogger(getClass());
+
+    @Pointcut("@annotation(com.fs.common.annotation.DataSource)"
+            + "|| @within(com.fs.common.annotation.DataSource)")
+    public void dsPointCut()
+    {
+
+    }
+
+    @Around("dsPointCut()")
+    public Object around(ProceedingJoinPoint point) throws Throwable
+    {
+        DataSource dataSource = getDataSource(point);
+
+        if (StringUtils.isNotNull(dataSource))
+        {
+            DynamicDataSourceContextHolder.setDataSourceType(dataSource.value().name());
+        }
+
+        try
+        {
+            return point.proceed();
+        }
+        finally
+        {
+            // 销毁数据源 在执行方法之后
+            DynamicDataSourceContextHolder.clearDataSourceType();
+        }
+    }
+
+    /**
+     * 获取需要切换的数据源
+     */
+    public DataSource getDataSource(ProceedingJoinPoint point)
+    {
+        MethodSignature signature = (MethodSignature) point.getSignature();
+        DataSource dataSource = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class);
+        if (Objects.nonNull(dataSource))
+        {
+            return dataSource;
+        }
+
+        return AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class);
+    }
+}

+ 244 - 0
fs-user-course/src/main/java/com/fs/framework/aspectj/LogAspect.java

@@ -0,0 +1,244 @@
+package com.fs.framework.aspectj;
+
+import com.alibaba.fastjson.JSON;
+import com.fs.common.annotation.Log;
+import com.fs.common.core.domain.model.LoginUser;
+import com.fs.common.enums.BusinessStatus;
+import com.fs.common.enums.HttpMethod;
+import com.fs.common.utils.SecurityUtils;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.common.utils.ip.IpUtils;
+import com.fs.framework.manager.AsyncManager;
+import com.fs.framework.manager.factory.AsyncFactory;
+import com.fs.system.domain.SysOperLog;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.Signature;
+import org.aspectj.lang.annotation.AfterReturning;
+import org.aspectj.lang.annotation.AfterThrowing;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Pointcut;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+import org.springframework.validation.BindingResult;
+import org.springframework.web.multipart.MultipartFile;
+import org.springframework.web.servlet.HandlerMapping;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.lang.reflect.Method;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Map;
+
+/**
+ * 操作日志记录处理
+ *
+
+ */
+@Aspect
+@Component
+public class LogAspect
+{
+    private static final Logger log = LoggerFactory.getLogger(LogAspect.class);
+
+    // 配置织入点
+    @Pointcut("@annotation(com.fs.common.annotation.Log)")
+    public void logPointCut()
+    {
+    }
+
+    /**
+     * 处理完请求后执行
+     *
+     * @param joinPoint 切点
+     */
+    @AfterReturning(pointcut = "logPointCut()", returning = "jsonResult")
+    public void doAfterReturning(JoinPoint joinPoint, Object jsonResult)
+    {
+        handleLog(joinPoint, null, jsonResult);
+    }
+
+    /**
+     * 拦截异常操作
+     *
+     * @param joinPoint 切点
+     * @param e 异常
+     */
+    @AfterThrowing(value = "logPointCut()", throwing = "e")
+    public void doAfterThrowing(JoinPoint joinPoint, Exception e)
+    {
+        handleLog(joinPoint, e, null);
+    }
+
+    protected void handleLog(final JoinPoint joinPoint, final Exception e, Object jsonResult)
+    {
+        try
+        {
+            // 获得注解
+            Log controllerLog = getAnnotationLog(joinPoint);
+            if (controllerLog == null)
+            {
+                return;
+            }
+
+            // 获取当前的用户
+            LoginUser loginUser = SecurityUtils.getLoginUser();
+
+            // *========数据库日志=========*//
+            SysOperLog operLog = new SysOperLog();
+            operLog.setStatus(BusinessStatus.SUCCESS.ordinal());
+            // 请求的地址
+            String ip = IpUtils.getIpAddr(ServletUtils.getRequest());
+            operLog.setOperIp(ip);
+            // 返回参数
+            operLog.setJsonResult(JSON.toJSONString(jsonResult));
+
+            operLog.setOperUrl(ServletUtils.getRequest().getRequestURI());
+            if (loginUser != null)
+            {
+                operLog.setOperName(loginUser.getUsername());
+            }
+
+            if (e != null)
+            {
+                operLog.setStatus(BusinessStatus.FAIL.ordinal());
+                operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000));
+            }
+            // 设置方法名称
+            String className = joinPoint.getTarget().getClass().getName();
+            String methodName = joinPoint.getSignature().getName();
+            operLog.setMethod(className + "." + methodName + "()");
+            // 设置请求方式
+            operLog.setRequestMethod(ServletUtils.getRequest().getMethod());
+            // 处理设置注解上的参数
+            getControllerMethodDescription(joinPoint, controllerLog, operLog);
+            // 保存数据库
+            AsyncManager.me().execute(AsyncFactory.recordOper(operLog));
+        }
+        catch (Exception exp)
+        {
+            // 记录本地异常日志
+            log.error("==前置通知异常==");
+            log.error("异常信息:{}", exp.getMessage());
+            exp.printStackTrace();
+        }
+    }
+
+    /**
+     * 获取注解中对方法的描述信息 用于Controller层注解
+     *
+     * @param log 日志
+     * @param operLog 操作日志
+     * @throws Exception
+     */
+    public void getControllerMethodDescription(JoinPoint joinPoint, Log log, SysOperLog operLog) throws Exception
+    {
+        // 设置action动作
+        operLog.setBusinessType(log.businessType().ordinal());
+        // 设置标题
+        operLog.setTitle(log.title());
+        // 设置操作人类别
+        operLog.setOperatorType(log.operatorType().ordinal());
+        // 是否需要保存request,参数和值
+        if (log.isSaveRequestData())
+        {
+            // 获取参数的信息,传入到数据库中。
+            setRequestValue(joinPoint, operLog);
+        }
+    }
+
+    /**
+     * 获取请求的参数,放到log中
+     *
+     * @param operLog 操作日志
+     * @throws Exception 异常
+     */
+    private void setRequestValue(JoinPoint joinPoint, SysOperLog operLog) throws Exception
+    {
+        String requestMethod = operLog.getRequestMethod();
+        if (HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod))
+        {
+            String params = argsArrayToString(joinPoint.getArgs());
+            operLog.setOperParam(StringUtils.substring(params, 0, 2000));
+        }
+        else
+        {
+            Map<?, ?> paramsMap = (Map<?, ?>) ServletUtils.getRequest().getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
+            operLog.setOperParam(StringUtils.substring(paramsMap.toString(), 0, 2000));
+        }
+    }
+
+    /**
+     * 是否存在注解,如果存在就获取
+     */
+    private Log getAnnotationLog(JoinPoint joinPoint) throws Exception
+    {
+        Signature signature = joinPoint.getSignature();
+        MethodSignature methodSignature = (MethodSignature) signature;
+        Method method = methodSignature.getMethod();
+
+        if (method != null)
+        {
+            return method.getAnnotation(Log.class);
+        }
+        return null;
+    }
+
+    /**
+     * 参数拼装
+     */
+    private String argsArrayToString(Object[] paramsArray)
+    {
+        String params = "";
+        if (paramsArray != null && paramsArray.length > 0)
+        {
+            for (int i = 0; i < paramsArray.length; i++)
+            {
+                if (StringUtils.isNotNull(paramsArray[i]) && !isFilterObject(paramsArray[i]))
+                {
+                    Object jsonObj = JSON.toJSON(paramsArray[i]);
+                    params += jsonObj.toString() + " ";
+                }
+            }
+        }
+        return params.trim();
+    }
+
+    /**
+     * 判断是否需要过滤的对象。
+     *
+     * @param o 对象信息。
+     * @return 如果是需要过滤的对象,则返回true;否则返回false。
+     */
+    @SuppressWarnings("rawtypes")
+    public boolean isFilterObject(final Object o)
+    {
+        Class<?> clazz = o.getClass();
+        if (clazz.isArray())
+        {
+            return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
+        }
+        else if (Collection.class.isAssignableFrom(clazz))
+        {
+            Collection collection = (Collection) o;
+            for (Iterator iter = collection.iterator(); iter.hasNext();)
+            {
+                return iter.next() instanceof MultipartFile;
+            }
+        }
+        else if (Map.class.isAssignableFrom(clazz))
+        {
+            Map map = (Map) o;
+            for (Iterator iter = map.entrySet().iterator(); iter.hasNext();)
+            {
+                Map.Entry entry = (Map.Entry) iter.next();
+                return entry.getValue() instanceof MultipartFile;
+            }
+        }
+        return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
+                || o instanceof BindingResult;
+    }
+}

+ 117 - 0
fs-user-course/src/main/java/com/fs/framework/aspectj/RateLimiterAspect.java

@@ -0,0 +1,117 @@
+package com.fs.framework.aspectj;
+
+import com.fs.common.annotation.RateLimiter;
+import com.fs.common.enums.LimitType;
+import com.fs.common.exception.ServiceException;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.common.utils.ip.IpUtils;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.Signature;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Before;
+import org.aspectj.lang.annotation.Pointcut;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.script.RedisScript;
+import org.springframework.stereotype.Component;
+
+import java.lang.reflect.Method;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * 限流处理
+ *
+
+ */
+@Aspect
+@Component
+public class RateLimiterAspect
+{
+    private static final Logger log = LoggerFactory.getLogger(RateLimiterAspect.class);
+
+    private RedisTemplate<Object, Object> redisTemplate;
+
+    private RedisScript<Long> limitScript;
+
+    @Autowired
+    public void setRedisTemplate1(RedisTemplate<Object, Object> redisTemplate)
+    {
+        this.redisTemplate = redisTemplate;
+    }
+
+    @Autowired
+    public void setLimitScript(RedisScript<Long> limitScript)
+    {
+        this.limitScript = limitScript;
+    }
+
+    // 配置织入点
+    @Pointcut("@annotation(com.fs.common.annotation.RateLimiter)")
+    public void rateLimiterPointCut()
+    {
+    }
+
+    @Before("rateLimiterPointCut()")
+    public void doBefore(JoinPoint point) throws Throwable
+    {
+        RateLimiter rateLimiter = getAnnotationRateLimiter(point);
+        String key = rateLimiter.key();
+        int time = rateLimiter.time();
+        int count = rateLimiter.count();
+
+        String combineKey = getCombineKey(rateLimiter, point);
+        List<Object> keys = Collections.singletonList(combineKey);
+        try
+        {
+            Long number = redisTemplate.execute(limitScript, keys, count, time);
+            if (StringUtils.isNull(number) || number.intValue() > count)
+            {
+                throw new ServiceException("访问过于频繁,请稍后再试");
+            }
+            log.info("限制请求'{}',当前请求'{}',缓存key'{}'", count, number.intValue(), key);
+        }
+        catch (ServiceException e)
+        {
+            throw e;
+        }
+        catch (Exception e)
+        {
+            throw new RuntimeException("服务器限流异常,请稍后再试");
+        }
+    }
+
+    /**
+     * 是否存在注解,如果存在就获取
+     */
+    private RateLimiter getAnnotationRateLimiter(JoinPoint joinPoint)
+    {
+        Signature signature = joinPoint.getSignature();
+        MethodSignature methodSignature = (MethodSignature) signature;
+        Method method = methodSignature.getMethod();
+
+        if (method != null)
+        {
+            return method.getAnnotation(RateLimiter.class);
+        }
+        return null;
+    }
+
+    public String getCombineKey(RateLimiter rateLimiter, JoinPoint point)
+    {
+        StringBuffer stringBuffer = new StringBuffer(rateLimiter.key());
+        if (rateLimiter.limitType() == LimitType.IP)
+        {
+            stringBuffer.append(IpUtils.getIpAddr(ServletUtils.getRequest()));
+        }
+        MethodSignature signature = (MethodSignature) point.getSignature();
+        Method method = signature.getMethod();
+        Class<?> targetClass = method.getDeclaringClass();
+        stringBuffer.append("-").append(targetClass.getName()).append("- ").append(method.getName());
+        return stringBuffer.toString();
+    }
+}

+ 31 - 0
fs-user-course/src/main/java/com/fs/framework/config/ApplicationConfig.java

@@ -0,0 +1,31 @@
+package com.fs.framework.config;
+
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.EnableAspectJAutoProxy;
+
+import java.util.TimeZone;
+
+/**
+ * 程序注解配置
+ *
+
+ */
+@Configuration
+// 表示通过aop框架暴露该代理对象,AopContext能够访问
+@EnableAspectJAutoProxy(exposeProxy = true)
+// 指定要扫描的Mapper类的包的路径
+@MapperScan("com.fs.**.mapper")
+public class ApplicationConfig
+{
+    /**
+     * 时区配置
+     */
+    @Bean
+    public Jackson2ObjectMapperBuilderCustomizer jacksonObjectMapperCustomization()
+    {
+        return jacksonObjectMapperBuilder -> jacksonObjectMapperBuilder.timeZone(TimeZone.getDefault());
+    }
+}

+ 85 - 0
fs-user-course/src/main/java/com/fs/framework/config/CaptchaConfig.java

@@ -0,0 +1,85 @@
+package com.fs.framework.config;
+
+import com.google.code.kaptcha.impl.DefaultKaptcha;
+import com.google.code.kaptcha.util.Config;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.Properties;
+
+import static com.google.code.kaptcha.Constants.*;
+
+/**
+ * 验证码配置
+ *
+
+ */
+@Configuration
+public class CaptchaConfig
+{
+    @Bean(name = "captchaProducer")
+    public DefaultKaptcha getKaptchaBean()
+    {
+        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
+        Properties properties = new Properties();
+        // 是否有边框 默认为true 我们可以自己设置yes,no
+        properties.setProperty(KAPTCHA_BORDER, "yes");
+        // 验证码文本字符颜色 默认为Color.BLACK
+        properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "black");
+        // 验证码图片宽度 默认为200
+        properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160");
+        // 验证码图片高度 默认为50
+        properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60");
+        // 验证码文本字符大小 默认为40
+        properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "38");
+        // KAPTCHA_SESSION_KEY
+        properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCode");
+        // 验证码文本字符长度 默认为5
+        properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4");
+        // 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize)
+        properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier");
+        // 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy
+        properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy");
+        Config config = new Config(properties);
+        defaultKaptcha.setConfig(config);
+        return defaultKaptcha;
+    }
+
+    @Bean(name = "captchaProducerMath")
+    public DefaultKaptcha getKaptchaBeanMath()
+    {
+        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
+        Properties properties = new Properties();
+        // 是否有边框 默认为true 我们可以自己设置yes,no
+        properties.setProperty(KAPTCHA_BORDER, "yes");
+        // 边框颜色 默认为Color.BLACK
+        properties.setProperty(KAPTCHA_BORDER_COLOR, "105,179,90");
+        // 验证码文本字符颜色 默认为Color.BLACK
+        properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "blue");
+        // 验证码图片宽度 默认为200
+        properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160");
+        // 验证码图片高度 默认为50
+        properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60");
+        // 验证码文本字符大小 默认为40
+        properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "35");
+        // KAPTCHA_SESSION_KEY
+        properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCodeMath");
+        // 验证码文本生成器
+        properties.setProperty(KAPTCHA_TEXTPRODUCER_IMPL, "com.fs.framework.config.KaptchaTextCreator");
+        // 验证码文本字符间距 默认为2
+        properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_SPACE, "3");
+        // 验证码文本字符长度 默认为5
+        properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "6");
+        // 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize)
+        properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier");
+        // 验证码噪点颜色 默认为Color.BLACK
+        properties.setProperty(KAPTCHA_NOISE_COLOR, "white");
+        // 干扰实现类
+        properties.setProperty(KAPTCHA_NOISE_IMPL, "com.google.code.kaptcha.impl.NoNoise");
+        // 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy
+        properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy");
+        Config config = new Config(properties);
+        defaultKaptcha.setConfig(config);
+        return defaultKaptcha;
+    }
+}

+ 92 - 0
fs-user-course/src/main/java/com/fs/framework/config/DataSourceConfig.java

@@ -0,0 +1,92 @@
+package com.fs.framework.config;
+
+import com.alibaba.druid.pool.DruidDataSource;
+import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties;
+import com.alibaba.druid.util.Utils;
+import com.fs.common.enums.DataSourceType;
+import com.fs.framework.datasource.DynamicDataSource;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
+
+import javax.servlet.*;
+import javax.sql.DataSource;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+@Configuration
+public class DataSourceConfig {
+
+    @Bean
+    @ConfigurationProperties(prefix = "spring.datasource.sop.druid.master")
+    public DataSource sopDataSource() {
+        return new DruidDataSource();
+    }
+
+    @Bean
+    @ConfigurationProperties(prefix = "spring.datasource.mysql.druid.master")
+    public DataSource masterDataSource() {
+        return new DruidDataSource();
+    }
+
+
+
+    @Bean
+    @Primary
+    public DynamicDataSource dataSource(@Qualifier("masterDataSource") DataSource masterDataSource, @Qualifier("sopDataSource") DataSource sopDataSource) {
+        Map<Object, Object> targetDataSources = new HashMap<>();
+        targetDataSources.put(DataSourceType.SOP.name(), sopDataSource);
+        return new DynamicDataSource(masterDataSource, targetDataSources);
+    }
+
+    /**
+     * 去除监控页面底部的广告
+     */
+    @SuppressWarnings({ "rawtypes", "unchecked" })
+    @Bean
+    @ConditionalOnProperty(name = "spring.datasource.mysql.druid.statViewServlet.enabled", havingValue = "true")
+    public FilterRegistrationBean removeDruidFilterRegistrationBean(DruidStatProperties properties)
+    {
+        // 获取web监控页面的参数
+        DruidStatProperties.StatViewServlet config = properties.getStatViewServlet();
+        // 提取common.js的配置路径
+        String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*";
+        String commonJsPattern = pattern.replaceAll("\\*", "js/common.js");
+        final String filePath = "support/http/resources/js/common.js";
+        // 创建filter进行过滤
+        Filter filter = new Filter()
+        {
+            @Override
+            public void init(javax.servlet.FilterConfig filterConfig) throws ServletException
+            {
+            }
+            @Override
+            public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+                    throws IOException, ServletException
+            {
+                chain.doFilter(request, response);
+                // 重置缓冲区,响应头不会被重置
+                response.resetBuffer();
+                // 获取common.js
+                String text = Utils.readFromResource(filePath);
+                // 正则替换banner, 除去底部的广告信息
+                text = text.replaceAll("<a.*?banner\"></a><br/>", "");
+                text = text.replaceAll("powered.*?shrek.wang</a>", "");
+                response.getWriter().write(text);
+            }
+            @Override
+            public void destroy()
+            {
+            }
+        };
+        FilterRegistrationBean registrationBean = new FilterRegistrationBean();
+        registrationBean.setFilter(filter);
+        registrationBean.addUrlPatterns(commonJsPattern);
+        return registrationBean;
+    }
+}

+ 72 - 0
fs-user-course/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -0,0 +1,72 @@
+package com.fs.framework.config;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.parser.ParserConfig;
+import com.alibaba.fastjson.serializer.SerializerFeature;
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.type.TypeFactory;
+import org.springframework.data.redis.serializer.RedisSerializer;
+import org.springframework.data.redis.serializer.SerializationException;
+import org.springframework.util.Assert;
+
+import java.nio.charset.Charset;
+
+/**
+ * Redis使用FastJson序列化
+ *
+
+ */
+public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
+{
+    @SuppressWarnings("unused")
+    private ObjectMapper objectMapper = new ObjectMapper();
+
+    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
+
+    private Class<T> clazz;
+
+    static
+    {
+        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
+    }
+
+    public FastJson2JsonRedisSerializer(Class<T> clazz)
+    {
+        super();
+        this.clazz = clazz;
+    }
+
+    @Override
+    public byte[] serialize(T t) throws SerializationException
+    {
+        if (t == null)
+        {
+            return new byte[0];
+        }
+        return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
+    }
+
+    @Override
+    public T deserialize(byte[] bytes) throws SerializationException
+    {
+        if (bytes == null || bytes.length <= 0)
+        {
+            return null;
+        }
+        String str = new String(bytes, DEFAULT_CHARSET);
+
+        return JSON.parseObject(str, clazz);
+    }
+
+    public void setObjectMapper(ObjectMapper objectMapper)
+    {
+        Assert.notNull(objectMapper, "'objectMapper' must not be null");
+        this.objectMapper = objectMapper;
+    }
+
+    protected JavaType getJavaType(Class<?> clazz)
+    {
+        return TypeFactory.defaultInstance().constructType(clazz);
+    }
+}

+ 59 - 0
fs-user-course/src/main/java/com/fs/framework/config/FilterConfig.java

@@ -0,0 +1,59 @@
+package com.fs.framework.config;
+
+import com.fs.common.filter.RepeatableFilter;
+import com.fs.common.filter.XssFilter;
+import com.fs.common.utils.StringUtils;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import javax.servlet.DispatcherType;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Filter配置
+ *
+
+ */
+@Configuration
+@ConditionalOnProperty(value = "xss.enabled", havingValue = "true")
+public class FilterConfig
+{
+    @Value("${xss.excludes}")
+    private String excludes;
+
+    @Value("${xss.urlPatterns}")
+    private String urlPatterns;
+
+    @SuppressWarnings({ "rawtypes", "unchecked" })
+    @Bean
+    public FilterRegistrationBean xssFilterRegistration()
+    {
+        FilterRegistrationBean registration = new FilterRegistrationBean();
+        registration.setDispatcherTypes(DispatcherType.REQUEST);
+        registration.setFilter(new XssFilter());
+        registration.addUrlPatterns(StringUtils.split(urlPatterns, ","));
+        registration.setName("xssFilter");
+        registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE);
+        Map<String, String> initParameters = new HashMap<String, String>();
+        initParameters.put("excludes", excludes);
+        registration.setInitParameters(initParameters);
+        return registration;
+    }
+
+    @SuppressWarnings({ "rawtypes", "unchecked" })
+    @Bean
+    public FilterRegistrationBean someFilterRegistration()
+    {
+        FilterRegistrationBean registration = new FilterRegistrationBean();
+        registration.setFilter(new RepeatableFilter());
+        registration.addUrlPatterns("/*");
+        registration.setName("repeatableFilter");
+        registration.setOrder(FilterRegistrationBean.LOWEST_PRECEDENCE);
+        return registration;
+    }
+
+}

+ 76 - 0
fs-user-course/src/main/java/com/fs/framework/config/KaptchaTextCreator.java

@@ -0,0 +1,76 @@
+package com.fs.framework.config;
+
+import com.google.code.kaptcha.text.impl.DefaultTextCreator;
+
+import java.util.Random;
+
+/**
+ * 验证码文本生成器
+ *
+
+ */
+public class KaptchaTextCreator extends DefaultTextCreator
+{
+    private static final String[] CNUMBERS = "0,1,2,3,4,5,6,7,8,9,10".split(",");
+
+    @Override
+    public String getText()
+    {
+        Integer result = 0;
+        Random random = new Random();
+        int x = random.nextInt(10);
+        int y = random.nextInt(10);
+        StringBuilder suChinese = new StringBuilder();
+        int randomoperands = (int) Math.round(Math.random() * 2);
+        if (randomoperands == 0)
+        {
+            result = x * y;
+            suChinese.append(CNUMBERS[x]);
+            suChinese.append("*");
+            suChinese.append(CNUMBERS[y]);
+        }
+        else if (randomoperands == 1)
+        {
+            if (!(x == 0) && y % x == 0)
+            {
+                result = y / x;
+                suChinese.append(CNUMBERS[y]);
+                suChinese.append("/");
+                suChinese.append(CNUMBERS[x]);
+            }
+            else
+            {
+                result = x + y;
+                suChinese.append(CNUMBERS[x]);
+                suChinese.append("+");
+                suChinese.append(CNUMBERS[y]);
+            }
+        }
+        else if (randomoperands == 2)
+        {
+            if (x >= y)
+            {
+                result = x - y;
+                suChinese.append(CNUMBERS[x]);
+                suChinese.append("-");
+                suChinese.append(CNUMBERS[y]);
+            }
+            else
+            {
+                result = y - x;
+                suChinese.append(CNUMBERS[y]);
+                suChinese.append("-");
+                suChinese.append(CNUMBERS[x]);
+            }
+        }
+        else
+        {
+            result = x + y;
+            suChinese.append(CNUMBERS[x]);
+            suChinese.append("+");
+            suChinese.append(CNUMBERS[y]);
+        }
+        suChinese.append("=?@" + result);
+        return suChinese.toString();
+    }
+}

+ 149 - 0
fs-user-course/src/main/java/com/fs/framework/config/MyBatisConfig.java

@@ -0,0 +1,149 @@
+package com.fs.framework.config;
+
+import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
+import org.apache.ibatis.io.VFS;
+import org.apache.ibatis.session.SqlSessionFactory;
+import org.mybatis.spring.boot.autoconfigure.SpringBootVFS;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.env.Environment;
+import org.springframework.core.io.DefaultResourceLoader;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
+import org.springframework.core.io.support.ResourcePatternResolver;
+import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
+import org.springframework.core.type.classreading.MetadataReader;
+import org.springframework.core.type.classreading.MetadataReaderFactory;
+import org.springframework.util.ClassUtils;
+
+import javax.sql.DataSource;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+
+/**
+ * Mybatis支持*匹配扫描包
+ *
+
+ */
+@Configuration
+public class MyBatisConfig
+{
+    @Autowired
+    private Environment env;
+
+    static final String DEFAULT_RESOURCE_PATTERN = "**/*.class";
+
+    public static String setTypeAliasesPackage(String typeAliasesPackage)
+    {
+        ResourcePatternResolver resolver = (ResourcePatternResolver) new PathMatchingResourcePatternResolver();
+        MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resolver);
+        List<String> allResult = new ArrayList<String>();
+        try
+        {
+            for (String aliasesPackage : typeAliasesPackage.split(","))
+            {
+                List<String> result = new ArrayList<String>();
+                aliasesPackage = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX
+                        + ClassUtils.convertClassNameToResourcePath(aliasesPackage.trim()) + "/" + DEFAULT_RESOURCE_PATTERN;
+                Resource[] resources = resolver.getResources(aliasesPackage);
+                if (resources != null && resources.length > 0)
+                {
+                    MetadataReader metadataReader = null;
+                    for (Resource resource : resources)
+                    {
+                        if (resource.isReadable())
+                        {
+                            metadataReader = metadataReaderFactory.getMetadataReader(resource);
+                            try
+                            {
+                                result.add(Class.forName(metadataReader.getClassMetadata().getClassName()).getPackage().getName());
+                            }
+                            catch (ClassNotFoundException e)
+                            {
+                                e.printStackTrace();
+                            }
+                        }
+                    }
+                }
+                if (result.size() > 0)
+                {
+                    HashSet<String> hashResult = new HashSet<String>(result);
+                    allResult.addAll(hashResult);
+                }
+            }
+            if (allResult.size() > 0)
+            {
+                typeAliasesPackage = String.join(",", (String[]) allResult.toArray(new String[0]));
+            }
+            else
+            {
+                throw new RuntimeException("mybatis typeAliasesPackage 路径扫描错误,参数typeAliasesPackage:" + typeAliasesPackage + "未找到任何包");
+            }
+        }
+        catch (IOException e)
+        {
+            e.printStackTrace();
+        }
+        return typeAliasesPackage;
+    }
+
+    public Resource[] resolveMapperLocations(String[] mapperLocations)
+    {
+        ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
+        List<Resource> resources = new ArrayList<Resource>();
+        if (mapperLocations != null)
+        {
+            for (String mapperLocation : mapperLocations)
+            {
+                try
+                {
+                    Resource[] mappers = resourceResolver.getResources(mapperLocation);
+                    resources.addAll(Arrays.asList(mappers));
+                }
+                catch (IOException e)
+                {
+                    // ignore
+                }
+            }
+        }
+        return resources.toArray(new Resource[resources.size()]);
+    }
+
+//    @Bean
+//    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception
+//    {
+//        String typeAliasesPackage = env.getProperty("mybatis.typeAliasesPackage");
+//        String mapperLocations = env.getProperty("mybatis.mapperLocations");
+//        String configLocation = env.getProperty("mybatis.configLocation");
+//        typeAliasesPackage = setTypeAliasesPackage(typeAliasesPackage);
+//        VFS.addImplClass(SpringBootVFS.class);
+//
+//        final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
+//        sessionFactory.setDataSource(dataSource);
+//        sessionFactory.setTypeAliasesPackage(typeAliasesPackage);
+//        sessionFactory.setMapperLocations(resolveMapperLocations(StringUtils.split(mapperLocations, ",")));
+//        sessionFactory.setConfigLocation(new DefaultResourceLoader().getResource(configLocation));
+//        return sessionFactory.getObject();
+//    }
+
+    @Bean
+    public SqlSessionFactory sqlSessionFactorys(DataSource dataSource) throws Exception
+    {
+        String typeAliasesPackage = env.getProperty("mybatis-plus.typeAliasesPackage");
+        String mapperLocations = env.getProperty("mybatis-plus.mapperLocations");
+        String configLocation = env.getProperty("mybatis-plus.configLocation");
+        typeAliasesPackage = setTypeAliasesPackage(typeAliasesPackage);
+        VFS.addImplClass(SpringBootVFS.class);
+
+        final MybatisSqlSessionFactoryBean sessionFactory = new MybatisSqlSessionFactoryBean();
+        sessionFactory.setDataSource(dataSource);
+        sessionFactory.setTypeAliasesPackage(typeAliasesPackage);
+        sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
+        sessionFactory.setConfigLocation(new DefaultResourceLoader().getResource(configLocation));
+        return sessionFactory.getObject();
+    }
+}

+ 137 - 0
fs-user-course/src/main/java/com/fs/framework/config/RedisConfig.java

@@ -0,0 +1,137 @@
+package com.fs.framework.config;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import com.fasterxml.jackson.annotation.PropertyAccessor;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
+import org.springframework.cache.annotation.CachingConfigurerSupport;
+import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.script.DefaultRedisScript;
+import org.springframework.data.redis.serializer.GenericToStringSerializer;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+
+/**
+ * redis配置
+ *
+
+ */
+@Configuration
+@EnableCaching
+public class RedisConfig extends CachingConfigurerSupport
+{
+//    @Bean
+//    public CacheManager cacheManager() {
+//        SimpleCacheManager cacheManager = new SimpleCacheManager();
+//        cacheManager.setCaches(Arrays.asList(
+//                new ConcurrentMapCache("getCourseCate"),
+//                new ConcurrentMapCache("getProductCateByPid"),
+//                new ConcurrentMapCache("getCourseList"),
+//                new ConcurrentMapCache("getAppAdvList"),
+//                new ConcurrentMapCache("getAdvList")
+//        ));
+//        return cacheManager;
+//    }
+
+
+
+    @Bean
+    @SuppressWarnings(value = { "unchecked", "rawtypes" })
+    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
+    {
+        RedisTemplate<Object, Object> template = new RedisTemplate<>();
+        template.setConnectionFactory(connectionFactory);
+
+        FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
+
+        ObjectMapper mapper = new ObjectMapper();
+        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
+        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
+        serializer.setObjectMapper(mapper);
+
+        // 使用StringRedisSerializer来序列化和反序列化redis的key值
+        template.setKeySerializer(new StringRedisSerializer());
+        template.setValueSerializer(serializer);
+
+        // Hash的key也采用StringRedisSerializer的序列化方式
+        template.setHashKeySerializer(new StringRedisSerializer());
+        template.setHashValueSerializer(serializer);
+
+        template.afterPropertiesSet();
+        return template;
+    }
+
+    @Bean
+    public RedisTemplate<String, Boolean> redisTemplateForBoolean(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Boolean> template = new RedisTemplate<>();
+        template.setConnectionFactory(connectionFactory);
+
+        // 使用StringRedisSerializer来序列化和反序列化redis的key值
+        template.setKeySerializer(new StringRedisSerializer());
+        template.setValueSerializer(new GenericToStringSerializer<>(Boolean.class));
+
+        // Hash的key也采用StringRedisSerializer的序列化方式
+        template.setHashKeySerializer(new StringRedisSerializer());
+        template.setHashValueSerializer(new GenericToStringSerializer<>(Boolean.class));
+
+        template.afterPropertiesSet();
+        return template;
+    }
+
+    @Bean
+    @SuppressWarnings(value = { "unchecked", "rawtypes" })
+    public RedisTemplate<String, Object> redisTemplateForObject(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Object> template = new RedisTemplate<>();
+        template.setConnectionFactory(connectionFactory);
+
+        FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
+
+        ObjectMapper mapper = new ObjectMapper();
+        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
+        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
+        serializer.setObjectMapper(mapper);
+
+        // 使用StringRedisSerializer来序列化和反序列化redis的key值
+        template.setKeySerializer(new StringRedisSerializer());
+        template.setValueSerializer(serializer);
+
+        // Hash的key也采用StringRedisSerializer的序列化方式
+        template.setHashKeySerializer(new StringRedisSerializer());
+        template.setHashValueSerializer(serializer);
+
+        template.afterPropertiesSet();
+        return template;
+    }
+
+    @Bean
+    public DefaultRedisScript<Long> limitScript()
+    {
+        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
+        redisScript.setScriptText(limitScriptText());
+        redisScript.setResultType(Long.class);
+        return redisScript;
+    }
+
+    /**
+     * 限流脚本
+     */
+    private String limitScriptText()
+    {
+        return "local key = KEYS[1]\n" +
+                "local count = tonumber(ARGV[1])\n" +
+                "local time = tonumber(ARGV[2])\n" +
+                "local current = redis.call('get', key);\n" +
+                "if current and tonumber(current) > count then\n" +
+                "    return current;\n" +
+                "end\n" +
+                "current = redis.call('incr', key)\n" +
+                "if tonumber(current) == 1 then\n" +
+                "    redis.call('expire', key, time)\n" +
+                "end\n" +
+                "return current;";
+    }
+}

+ 65 - 0
fs-user-course/src/main/java/com/fs/framework/config/ResourcesConfig.java

@@ -0,0 +1,65 @@
+package com.fs.framework.config;
+
+import com.fs.common.config.FSConfig;
+import com.fs.common.constant.Constants;
+import com.fs.framework.interceptor.RepeatSubmitInterceptor;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
+import org.springframework.web.filter.CorsFilter;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+/**
+ * 通用配置
+ *
+
+ */
+@Configuration
+public class ResourcesConfig implements WebMvcConfigurer
+{
+    @Autowired
+    private RepeatSubmitInterceptor repeatSubmitInterceptor;
+
+    @Override
+    public void addResourceHandlers(ResourceHandlerRegistry registry)
+    {
+        /** 本地文件上传路径 */
+        registry.addResourceHandler(Constants.RESOURCE_PREFIX + "/**").addResourceLocations("file:" + FSConfig.getProfile() + "/");
+
+        /** swagger配置 */
+        registry.addResourceHandler("/swagger-ui/**").addResourceLocations("classpath:/META-INF/resources/webjars/springfox-swagger-ui/");
+    }
+
+    /**
+     * 自定义拦截规则
+     */
+    @Override
+    public void addInterceptors(InterceptorRegistry registry)
+    {
+        registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**");
+    }
+
+    /**
+     * 跨域配置
+     */
+    @Bean
+    public CorsFilter corsFilter()
+    {
+        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+        CorsConfiguration config = new CorsConfiguration();
+        config.setAllowCredentials(true);
+        // 设置访问源地址
+        config.addAllowedOrigin("*");
+        // 设置访问源请求头
+        config.addAllowedHeader("*");
+        // 设置访问源请求方法
+        config.addAllowedMethod("*");
+        // 对接口配置跨域设置
+        source.registerCorsConfiguration("/**", config);
+        return new CorsFilter(source);
+    }
+}

+ 50 - 0
fs-user-course/src/main/java/com/fs/framework/config/SecurityConfig.java

@@ -0,0 +1,50 @@
+package com.fs.framework.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.config.BeanIds;
+import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+
+/**
+ * spring security配置
+ *
+
+ */
+@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
+public class SecurityConfig extends WebSecurityConfigurerAdapter
+{
+
+    /**
+     * anyRequest          |   匹配所有请求路径
+     * access              |   SpringEl表达式结果为true时可以访问
+     * anonymous           |   匿名可以访问
+     * denyAll             |   用户不能访问
+     * fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)
+     * hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问
+     * hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问
+     * hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问
+     * hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
+     * hasRole             |   如果有参数,参数表示角色,则其角色可以访问
+     * permitAll           |   用户可以任意访问
+     * rememberMe          |   允许通过remember-me登录的用户访问
+     * authenticated       |   用户登录后可访问
+     */
+    @Override
+    protected void configure(HttpSecurity http) throws Exception
+    {
+        http.authorizeRequests()
+                .antMatchers("/**").permitAll()
+                .anyRequest().authenticated()
+                .and().csrf().disable();
+    }
+
+    @Bean(name = BeanIds.AUTHENTICATION_MANAGER)
+    @Override
+    public AuthenticationManager authenticationManagerBean() throws Exception {
+        return super.authenticationManagerBean();
+    }
+
+
+}

+ 33 - 0
fs-user-course/src/main/java/com/fs/framework/config/ServerConfig.java

@@ -0,0 +1,33 @@
+package com.fs.framework.config;
+
+import com.fs.common.utils.ServletUtils;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * 服务相关配置
+ *
+
+ */
+@Component
+public class ServerConfig
+{
+    /**
+     * 获取完整的请求路径,包括:域名,端口,上下文访问路径
+     *
+     * @return 服务地址
+     */
+    public String getUrl()
+    {
+        HttpServletRequest request = ServletUtils.getRequest();
+        return getDomain(request);
+    }
+
+    public static String getDomain(HttpServletRequest request)
+    {
+        StringBuffer url = request.getRequestURL();
+        String contextPath = request.getServletContext().getContextPath();
+        return url.delete(url.length() - request.getRequestURI().length(), url.length()).append(contextPath).toString();
+    }
+}

+ 124 - 0
fs-user-course/src/main/java/com/fs/framework/config/SwaggerConfig.java

@@ -0,0 +1,124 @@
+package com.fs.framework.config;
+
+import com.fs.common.config.FSConfig;
+import com.github.xiaoymin.swaggerbootstrapui.annotations.EnableSwaggerBootstrapUI;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import springfox.documentation.builders.ApiInfoBuilder;
+import springfox.documentation.builders.PathSelectors;
+import springfox.documentation.builders.RequestHandlerSelectors;
+import springfox.documentation.service.*;
+import springfox.documentation.spi.DocumentationType;
+import springfox.documentation.spi.service.contexts.SecurityContext;
+import springfox.documentation.spring.web.plugins.Docket;
+import springfox.documentation.swagger2.annotations.EnableSwagger2;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Swagger2的接口配置
+ *
+
+ */
+@Configuration
+@EnableSwagger2
+@EnableSwaggerBootstrapUI
+public class SwaggerConfig
+{
+    /** 系统基础配置 */
+    @Autowired
+    private FSConfig fsConfig;
+
+    /** 是否开启swagger */
+    @Value("${swagger.enabled}")
+    private boolean enabled;
+
+    /** 设置请求的统一前缀 */
+    @Value("${swagger.pathMapping}")
+    private String pathMapping;
+
+    /**
+     * 创建API
+     */
+    @Bean
+    public Docket createRestApi()
+    {
+        return new Docket(DocumentationType.SWAGGER_2)
+                // 是否启用Swagger
+                .enable(enabled)
+                // 用来创建该API的基本信息,展示在文档的页面中(自定义展示的信息)
+                .apiInfo(apiInfo())
+                // 设置哪些接口暴露给Swagger展示
+                .select()
+                // 扫描所有有注解的api,用这种方式更灵活
+                .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
+                // 扫描指定包中的swagger注解
+                // .apis(RequestHandlerSelectors.basePackage("com.fs.project.tool.swagger"))
+                // 扫描所有 .apis(RequestHandlerSelectors.any())
+                .paths(PathSelectors.any())
+                .build()
+                /* 设置安全模式,swagger可以设置访问token */
+                .securitySchemes(securitySchemes())
+                .securityContexts(securityContexts())
+                .pathMapping(pathMapping);
+    }
+
+    /**
+     * 安全模式,这里指定token通过Authorization头请求头传递
+     */
+    private List<ApiKey> securitySchemes()
+    {
+        List<ApiKey> apiKeyList = new ArrayList<ApiKey>();
+        apiKeyList.add(new ApiKey("Authorization", "AppToken", "header"));
+        return apiKeyList;
+    }
+
+    /**
+     * 安全上下文
+     */
+    private List<SecurityContext> securityContexts()
+    {
+        List<SecurityContext> securityContexts = new ArrayList<>();
+        securityContexts.add(
+                SecurityContext.builder()
+                        .securityReferences(defaultAuth())
+                        .forPaths(PathSelectors.regex("^(?!auth).*$"))
+                        .build());
+        return securityContexts;
+    }
+
+    /**
+     * 默认的安全上引用
+     */
+    private List<SecurityReference> defaultAuth()
+    {
+        AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
+        AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
+        authorizationScopes[0] = authorizationScope;
+        List<SecurityReference> securityReferences = new ArrayList<>();
+        securityReferences.add(new SecurityReference("Authorization", authorizationScopes));
+        return securityReferences;
+    }
+
+    /**
+     * 添加摘要信息
+     */
+    private ApiInfo apiInfo()
+    {
+        // 用ApiInfoBuilder进行定制
+        return new ApiInfoBuilder()
+                // 设置标题
+                .title("标题:FS管理系统_接口文档")
+                // 描述
+                .description("描述:用于管理集团旗下公司的人员信息,具体包括XXX,XXX模块...")
+                // 作者信息
+                .contact(new Contact(fsConfig.getName(), null, null))
+                // 版本
+                .version("版本号:" + fsConfig.getVersion())
+                .build();
+    }
+}

+ 63 - 0
fs-user-course/src/main/java/com/fs/framework/config/ThreadPoolConfig.java

@@ -0,0 +1,63 @@
+package com.fs.framework.config;
+
+import com.fs.common.utils.Threads;
+import org.apache.commons.lang3.concurrent.BasicThreadFactory;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.ThreadPoolExecutor;
+
+/**
+ * 线程池配置
+ *
+
+ **/
+@Configuration
+public class ThreadPoolConfig
+{
+    // 核心线程池大小
+    private int corePoolSize = 50;
+
+    // 最大可创建的线程数
+    private int maxPoolSize = 200;
+
+    // 队列最大长度
+    private int queueCapacity = 1000;
+
+    // 线程池维护线程所允许的空闲时间
+    private int keepAliveSeconds = 300;
+
+    @Bean(name = "threadPoolTaskExecutor")
+    public ThreadPoolTaskExecutor threadPoolTaskExecutor()
+    {
+        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+        executor.setMaxPoolSize(maxPoolSize);
+        executor.setCorePoolSize(corePoolSize);
+        executor.setQueueCapacity(queueCapacity);
+        executor.setKeepAliveSeconds(keepAliveSeconds);
+        // 线程池对拒绝任务(无线程可用)的处理策略
+        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
+        return executor;
+    }
+
+    /**
+     * 执行周期性或定时任务
+     */
+    @Bean(name = "scheduledExecutorService")
+    protected ScheduledExecutorService scheduledExecutorService()
+    {
+        return new ScheduledThreadPoolExecutor(corePoolSize,
+                new BasicThreadFactory.Builder().namingPattern("schedule-pool-%d").daemon(true).build())
+        {
+            @Override
+            protected void afterExecute(Runnable r, Throwable t)
+            {
+                super.afterExecute(r, t);
+                Threads.printException(r, t);
+            }
+        };
+    }
+}

+ 77 - 0
fs-user-course/src/main/java/com/fs/framework/config/properties/DruidProperties.java

@@ -0,0 +1,77 @@
+package com.fs.framework.config.properties;
+
+import com.alibaba.druid.pool.DruidDataSource;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * druid 配置属性
+ *
+
+ */
+@Configuration
+public class DruidProperties
+{
+    @Value("${spring.datasource.mysql.druid.initialSize}")
+    private int initialSize;
+
+    @Value("${spring.datasource.mysql.druid.minIdle}")
+    private int minIdle;
+
+    @Value("${spring.datasource.mysql.druid.maxActive}")
+    private int maxActive;
+
+    @Value("${spring.datasource.mysql.druid.maxWait}")
+    private int maxWait;
+
+    @Value("${spring.datasource.mysql.druid.timeBetweenEvictionRunsMillis}")
+    private int timeBetweenEvictionRunsMillis;
+
+    @Value("${spring.datasource.mysql.druid.minEvictableIdleTimeMillis}")
+    private int minEvictableIdleTimeMillis;
+
+    @Value("${spring.datasource.mysql.druid.maxEvictableIdleTimeMillis}")
+    private int maxEvictableIdleTimeMillis;
+
+    @Value("${spring.datasource.mysql.druid.validationQuery}")
+    private String validationQuery;
+
+    @Value("${spring.datasource.mysql.druid.testWhileIdle}")
+    private boolean testWhileIdle;
+
+    @Value("${spring.datasource.mysql.druid.testOnBorrow}")
+    private boolean testOnBorrow;
+
+    @Value("${spring.datasource.mysql.druid.testOnReturn}")
+    private boolean testOnReturn;
+
+    public DruidDataSource dataSource(DruidDataSource datasource)
+    {
+        /** 配置初始化大小、最小、最大 */
+        datasource.setInitialSize(initialSize);
+        datasource.setMaxActive(maxActive);
+        datasource.setMinIdle(minIdle);
+
+        /** 配置获取连接等待超时的时间 */
+        datasource.setMaxWait(maxWait);
+
+        /** 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 */
+        datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
+
+        /** 配置一个连接在池中最小、最大生存的时间,单位是毫秒 */
+        datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
+        datasource.setMaxEvictableIdleTimeMillis(maxEvictableIdleTimeMillis);
+
+        /**
+         * 用来检测连接是否有效的sql,要求是一个查询语句,常用select 'x'。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。
+         */
+        datasource.setValidationQuery(validationQuery);
+        /** 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 */
+        datasource.setTestWhileIdle(testWhileIdle);
+        /** 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */
+        datasource.setTestOnBorrow(testOnBorrow);
+        /** 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */
+        datasource.setTestOnReturn(testOnReturn);
+        return datasource;
+    }
+}

+ 27 - 0
fs-user-course/src/main/java/com/fs/framework/datasource/DynamicDataSource.java

@@ -0,0 +1,27 @@
+package com.fs.framework.datasource;
+
+import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
+
+import javax.sql.DataSource;
+import java.util.Map;
+
+/**
+ * 动态数据源
+ *
+
+ */
+public class DynamicDataSource extends AbstractRoutingDataSource
+{
+    public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources)
+    {
+        super.setDefaultTargetDataSource(defaultTargetDataSource);
+        super.setTargetDataSources(targetDataSources);
+        super.afterPropertiesSet();
+    }
+
+    @Override
+    protected Object determineCurrentLookupKey()
+    {
+        return DynamicDataSourceContextHolder.getDataSourceType();
+    }
+}

+ 44 - 0
fs-user-course/src/main/java/com/fs/framework/datasource/DynamicDataSourceContextHolder.java

@@ -0,0 +1,44 @@
+package com.fs.framework.datasource;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * 数据源切换处理
+ *
+
+ */
+public class DynamicDataSourceContextHolder {
+    public static final Logger log = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class);
+
+    /**
+     * 使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本,
+     *  所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
+     */
+    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
+
+    /**
+     * 设置数据源的变量
+     */
+    public static void setDataSourceType(String dsType)
+    {
+//        log.info("切换到{}数据源", dsType);
+        CONTEXT_HOLDER.set(dsType);
+    }
+
+    /**
+     * 获得数据源的变量
+     */
+    public static String getDataSourceType()
+    {
+        return CONTEXT_HOLDER.get();
+    }
+
+    /**
+     * 清空数据源变量
+     */
+    public static void clearDataSourceType()
+    {
+        CONTEXT_HOLDER.remove();
+    }
+}

+ 56 - 0
fs-user-course/src/main/java/com/fs/framework/interceptor/RepeatSubmitInterceptor.java

@@ -0,0 +1,56 @@
+package com.fs.framework.interceptor;
+
+import com.alibaba.fastjson.JSONObject;
+import com.fs.common.annotation.RepeatSubmit;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.utils.ServletUtils;
+import org.springframework.stereotype.Component;
+import org.springframework.web.method.HandlerMethod;
+import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.lang.reflect.Method;
+
+/**
+ * 防止重复提交拦截器
+ *
+
+ */
+@Component
+public abstract class RepeatSubmitInterceptor extends HandlerInterceptorAdapter
+{
+    @Override
+    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception
+    {
+        if (handler instanceof HandlerMethod)
+        {
+            HandlerMethod handlerMethod = (HandlerMethod) handler;
+            Method method = handlerMethod.getMethod();
+            RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
+            if (annotation != null)
+            {
+                if (this.isRepeatSubmit(request))
+                {
+                    AjaxResult ajaxResult = AjaxResult.error("不允许重复提交,请稍后再试");
+                    ServletUtils.renderString(response, JSONObject.toJSONString(ajaxResult));
+                    return false;
+                }
+            }
+            return true;
+        }
+        else
+        {
+            return super.preHandle(request, response, handler);
+        }
+    }
+
+    /**
+     * 验证是否重复提交由子类实现具体的防重复提交的规则
+     *
+     * @param request
+     * @return
+     * @throws Exception
+     */
+    public abstract boolean isRepeatSubmit(HttpServletRequest request);
+}

+ 126 - 0
fs-user-course/src/main/java/com/fs/framework/interceptor/impl/SameUrlDataInterceptor.java

@@ -0,0 +1,126 @@
+package com.fs.framework.interceptor.impl;
+
+import com.alibaba.fastjson.JSONObject;
+import com.fs.common.constant.Constants;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.common.filter.RepeatedlyRequestWrapper;
+import com.fs.common.utils.StringUtils;
+import com.fs.common.utils.http.HttpHelper;
+import com.fs.framework.interceptor.RepeatSubmitInterceptor;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 判断请求url和数据是否和上一次相同,
+ * 如果和上次相同,则是重复提交表单。 有效时间为10秒内。
+ *
+
+ */
+@Component
+public class SameUrlDataInterceptor extends RepeatSubmitInterceptor
+{
+    public final String REPEAT_PARAMS = "repeatParams";
+
+    public final String REPEAT_TIME = "repeatTime";
+
+    // 令牌自定义标识
+    @Value("${token.header}")
+    private String header;
+
+    @Autowired
+    private RedisCache redisCache;
+
+    /**
+     * 间隔时间,单位:秒 默认10秒
+     *
+     * 两次相同参数的请求,如果间隔时间大于该参数,系统不会认定为重复提交的数据
+     */
+    private int intervalTime = 10;
+
+    public void setIntervalTime(int intervalTime)
+    {
+        this.intervalTime = intervalTime;
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public boolean isRepeatSubmit(HttpServletRequest request)
+    {
+        String nowParams = "";
+        if (request instanceof RepeatedlyRequestWrapper)
+        {
+            RepeatedlyRequestWrapper repeatedlyRequest = (RepeatedlyRequestWrapper) request;
+            nowParams = HttpHelper.getBodyString(repeatedlyRequest);
+        }
+
+        // body参数为空,获取Parameter的数据
+        if (StringUtils.isEmpty(nowParams))
+        {
+            nowParams = JSONObject.toJSONString(request.getParameterMap());
+        }
+        Map<String, Object> nowDataMap = new HashMap<String, Object>();
+        nowDataMap.put(REPEAT_PARAMS, nowParams);
+        nowDataMap.put(REPEAT_TIME, System.currentTimeMillis());
+
+        // 请求地址(作为存放cache的key值)
+        String url = request.getRequestURI();
+
+        // 唯一值(没有消息头则使用请求地址)
+        String submitKey = request.getHeader(header);
+        if (StringUtils.isEmpty(submitKey))
+        {
+            submitKey = url;
+        }
+
+        // 唯一标识(指定key + 消息头)
+        String cacheRepeatKey = Constants.REPEAT_SUBMIT_KEY + submitKey;
+
+        Object sessionObj = redisCache.getCacheObject(cacheRepeatKey);
+        if (sessionObj != null)
+        {
+            Map<String, Object> sessionMap = (Map<String, Object>) sessionObj;
+            if (sessionMap.containsKey(url))
+            {
+                Map<String, Object> preDataMap = (Map<String, Object>) sessionMap.get(url);
+                if (compareParams(nowDataMap, preDataMap) && compareTime(nowDataMap, preDataMap))
+                {
+                    return true;
+                }
+            }
+        }
+        Map<String, Object> cacheMap = new HashMap<String, Object>();
+        cacheMap.put(url, nowDataMap);
+        redisCache.setCacheObject(cacheRepeatKey, cacheMap, intervalTime, TimeUnit.SECONDS);
+        return false;
+    }
+
+    /**
+     * 判断参数是否相同
+     */
+    private boolean compareParams(Map<String, Object> nowMap, Map<String, Object> preMap)
+    {
+        String nowParams = (String) nowMap.get(REPEAT_PARAMS);
+        String preParams = (String) preMap.get(REPEAT_PARAMS);
+        return nowParams.equals(preParams);
+    }
+
+    /**
+     * 判断两次间隔时间
+     */
+    private boolean compareTime(Map<String, Object> nowMap, Map<String, Object> preMap)
+    {
+        long time1 = (Long) nowMap.get(REPEAT_TIME);
+        long time2 = (Long) preMap.get(REPEAT_TIME);
+        if ((time1 - time2) < (this.intervalTime * 1000))
+        {
+            return true;
+        }
+        return false;
+    }
+}

+ 56 - 0
fs-user-course/src/main/java/com/fs/framework/manager/AsyncManager.java

@@ -0,0 +1,56 @@
+package com.fs.framework.manager;
+
+import com.fs.common.utils.Threads;
+import com.fs.common.utils.spring.SpringUtils;
+
+import java.util.TimerTask;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 异步任务管理器
+ *
+
+ */
+public class AsyncManager
+{
+    /**
+     * 操作延迟10毫秒
+     */
+    private final int OPERATE_DELAY_TIME = 10;
+
+    /**
+     * 异步操作任务调度线程池
+     */
+    private ScheduledExecutorService executor = SpringUtils.getBean("scheduledExecutorService");
+
+    /**
+     * 单例模式
+     */
+    private AsyncManager(){}
+
+    private static AsyncManager me = new AsyncManager();
+
+    public static AsyncManager me()
+    {
+        return me;
+    }
+
+    /**
+     * 执行任务
+     *
+     * @param task 任务
+     */
+    public void execute(TimerTask task)
+    {
+        executor.schedule(task, OPERATE_DELAY_TIME, TimeUnit.MILLISECONDS);
+    }
+
+    /**
+     * 停止任务线程池
+     */
+    public void shutdown()
+    {
+        Threads.shutdownAndAwaitTermination(executor);
+    }
+}

+ 40 - 0
fs-user-course/src/main/java/com/fs/framework/manager/ShutdownManager.java

@@ -0,0 +1,40 @@
+package com.fs.framework.manager;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PreDestroy;
+
+/**
+ * 确保应用退出时能关闭后台线程
+ *
+
+ */
+@Component
+public class ShutdownManager
+{
+    private static final Logger logger = LoggerFactory.getLogger("sys-user");
+
+    @PreDestroy
+    public void destroy()
+    {
+        shutdownAsyncManager();
+    }
+
+    /**
+     * 停止异步执行任务
+     */
+    private void shutdownAsyncManager()
+    {
+        try
+        {
+            logger.info("====关闭后台任务任务线程池====");
+            AsyncManager.me().shutdown();
+        }
+        catch (Exception e)
+        {
+            logger.error(e.getMessage(), e);
+        }
+    }
+}

+ 103 - 0
fs-user-course/src/main/java/com/fs/framework/manager/factory/AsyncFactory.java

@@ -0,0 +1,103 @@
+package com.fs.framework.manager.factory;
+
+import com.fs.common.constant.Constants;
+import com.fs.common.utils.LogUtils;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.common.utils.ip.AddressUtils;
+import com.fs.common.utils.ip.IpUtils;
+import com.fs.common.utils.spring.SpringUtils;
+import com.fs.system.domain.SysLogininfor;
+import com.fs.system.domain.SysOperLog;
+import com.fs.system.service.ISysLogininforService;
+import com.fs.system.service.ISysOperLogService;
+import eu.bitwalker.useragentutils.UserAgent;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.TimerTask;
+
+/**
+ * 异步工厂(产生任务用)
+ *
+
+ */
+public class AsyncFactory
+{
+    private static final Logger sys_user_logger = LoggerFactory.getLogger("sys-user");
+
+    /**
+     * 记录登录信息
+     *
+     * @param username 用户名
+     * @param status 状态
+     * @param message 消息
+     * @param args 列表
+     * @return 任务task
+     */
+    public static TimerTask recordLogininfor(final String username, final String status, final String message,
+            final Object... args)
+    {
+        final UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));
+        final String ip = IpUtils.getIpAddr(ServletUtils.getRequest());
+        return new TimerTask()
+        {
+            @Override
+            public void run()
+            {
+                String address = AddressUtils.getRealAddressByIP(ip);
+                StringBuilder s = new StringBuilder();
+                s.append(LogUtils.getBlock(ip));
+                s.append(address);
+                s.append(LogUtils.getBlock(username));
+                s.append(LogUtils.getBlock(status));
+                s.append(LogUtils.getBlock(message));
+                // 打印信息到日志
+                sys_user_logger.info(s.toString(), args);
+                // 获取客户端操作系统
+                String os = userAgent.getOperatingSystem().getName();
+                // 获取客户端浏览器
+                String browser = userAgent.getBrowser().getName();
+                // 封装对象
+                SysLogininfor logininfor = new SysLogininfor();
+                logininfor.setUserName(username);
+                logininfor.setIpaddr(ip);
+                logininfor.setLoginLocation(address);
+                logininfor.setBrowser(browser);
+                logininfor.setOs(os);
+                logininfor.setMsg(message);
+                // 日志状态
+                if (StringUtils.equalsAny(status, Constants.LOGIN_SUCCESS, Constants.LOGOUT, Constants.REGISTER))
+                {
+                    logininfor.setStatus(Constants.SUCCESS);
+                }
+                else if (Constants.LOGIN_FAIL.equals(status))
+                {
+                    logininfor.setStatus(Constants.FAIL);
+                }
+                // 插入数据
+                SpringUtils.getBean(ISysLogininforService.class).insertLogininfor(logininfor);
+            }
+        };
+    }
+
+    /**
+     * 操作日志记录
+     *
+     * @param operLog 操作日志信息
+     * @return 任务task
+     */
+    public static TimerTask recordOper(final SysOperLog operLog)
+    {
+        return new TimerTask()
+        {
+            @Override
+            public void run()
+            {
+                // 远程查询操作地点
+                operLog.setOperLocation(AddressUtils.getRealAddressByIP(operLog.getOperIp()));
+                SpringUtils.getBean(ISysOperLogService.class).insertOperlog(operLog);
+            }
+        };
+    }
+}

+ 1 - 0
fs-user-course/src/main/resources/META-INF/spring-devtools.properties

@@ -0,0 +1 @@
+restart.include.json=/com.alibaba.fastjson.*.jar

BIN
fs-user-course/src/main/resources/apiclient_cert.p12


+ 15 - 0
fs-user-course/src/main/resources/application.yml

@@ -0,0 +1,15 @@
+# 开发环境配置
+server:
+  # 服务器的HTTP端口,默认为8113
+  port: 8114
+
+# Spring配置
+spring:
+  profiles:
+    active: dev
+#    active: druid-jzzx
+#    active: druid-yzt
+#    active: druid-hdt
+#    active: druid-sxjz
+#    active: druid-yzt
+#    active: druid-sft

+ 37 - 0
fs-user-course/src/main/resources/i18n/messages.properties

@@ -0,0 +1,37 @@
+#错误消息
+not.null=* 必须填写
+user.jcaptcha.error=验证码错误
+user.jcaptcha.expire=验证码已失效
+user.not.exists=用户不存在/密码错误
+user.password.not.match=用户不存在/密码错误
+user.password.retry.limit.count=密码输入错误{0}次
+user.password.retry.limit.exceed=密码输入错误{0}次,帐户锁定10分钟
+user.password.delete=对不起,您的账号已被删除
+user.blocked=用户已封禁,请联系管理员
+role.blocked=角色已封禁,请联系管理员
+user.logout.success=退出成功
+
+length.not.valid=长度必须在{min}到{max}个字符之间
+
+user.username.not.valid=* 2到20个汉字、字母、数字或下划线组成,且必须以非数字开头
+user.password.not.valid=* 5-50个字符
+ 
+user.email.not.valid=邮箱格式错误
+user.mobile.phone.number.not.valid=手机号格式错误
+user.login.success=登录成功
+user.register.success=注册成功
+user.notfound=请重新登录
+user.forcelogout=管理员强制退出,请重新登录
+user.unknown.error=未知错误,请重新登录
+
+##文件上传消息
+upload.exceed.maxSize=上传的文件大小超出限制的文件大小!<br/>允许的文件最大大小是:{0}MB!
+upload.filename.exceed.length=上传的文件名最长{0}个字符
+
+##权限
+no.permission=您没有数据的权限,请联系管理员添加权限 [{0}]
+no.create.permission=您没有创建数据的权限,请联系管理员添加权限 [{0}]
+no.update.permission=您没有修改数据的权限,请联系管理员添加权限 [{0}]
+no.delete.permission=您没有删除数据的权限,请联系管理员添加权限 [{0}]
+no.export.permission=您没有导出数据的权限,请联系管理员添加权限 [{0}]
+no.view.permission=您没有查看数据的权限,请联系管理员添加权限 [{0}]

+ 93 - 0
fs-user-course/src/main/resources/logback.xml

@@ -0,0 +1,93 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration>
+    <!-- 日志存放路径 -->
+	<property name="log.path" value="/home/fs-user-course/logs" />
+    <!-- 日志输出格式 -->
+	<property name="log.pattern" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />
+
+	<!-- 控制台输出 -->
+	<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
+		<encoder>
+			<pattern>${log.pattern}</pattern>
+		</encoder>
+	</appender>
+
+	<!-- 系统日志输出 -->
+	<appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
+	    <file>${log.path}/sys-info.log</file>
+        <!-- 循环政策:基于时间创建日志文件 -->
+		<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!-- 日志文件名格式 -->
+			<fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.log</fileNamePattern>
+			<!-- 日志最大的历史 30 -->
+			<maxHistory>30</maxHistory>
+		</rollingPolicy>
+		<encoder>
+			<pattern>${log.pattern}</pattern>
+		</encoder>
+		<filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <!-- 过滤的级别 -->
+            <level>INFO</level>
+            <!-- 匹配时的操作:接收(记录) -->
+            <onMatch>ACCEPT</onMatch>
+            <!-- 不匹配时的操作:拒绝(不记录) -->
+            <onMismatch>DENY</onMismatch>
+        </filter>
+	</appender>
+
+	<appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
+	    <file>${log.path}/sys-error.log</file>
+        <!-- 循环政策:基于时间创建日志文件 -->
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!-- 日志文件名格式 -->
+            <fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.log</fileNamePattern>
+			<!-- 日志最大的历史 30 -->
+			<maxHistory>30</maxHistory>
+        </rollingPolicy>
+        <encoder>
+            <pattern>${log.pattern}</pattern>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <!-- 过滤的级别 -->
+            <level>ERROR</level>
+			<!-- 匹配时的操作:接收(记录) -->
+            <onMatch>ACCEPT</onMatch>
+			<!-- 不匹配时的操作:拒绝(不记录) -->
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+
+	<!-- 用户访问日志输出  -->
+    <appender name="sys-user" class="ch.qos.logback.core.rolling.RollingFileAppender">
+		<file>${log.path}/sys-user.log</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!-- 按天回滚 daily -->
+            <fileNamePattern>${log.path}/sys-user.%d{yyyy-MM-dd}.log</fileNamePattern>
+            <!-- 日志最大的历史 30 -->
+            <maxHistory>30</maxHistory>
+        </rollingPolicy>
+        <encoder>
+            <pattern>${log.pattern}</pattern>
+        </encoder>
+    </appender>
+
+	<!-- 系统模块日志级别控制  -->
+	<logger name="com.fs" level="info" />
+	<!-- Spring日志级别控制  -->
+	<logger name="org.springframework" level="warn" />
+
+	<root level="info">
+		<appender-ref ref="console" />
+	</root>
+
+	<!--系统操作日志-->
+    <root level="info">
+        <appender-ref ref="file_info" />
+        <appender-ref ref="file_error" />
+    </root>
+
+	<!--系统用户操作日志-->
+    <logger name="sys-user" level="info">
+        <appender-ref ref="sys-user"/>
+    </logger>
+</configuration>

+ 20 - 0
fs-user-course/src/main/resources/mybatis/mybatis-config.xml

@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE configuration
+PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-config.dtd">
+<configuration>
+
+	<settings>
+		<setting name="cacheEnabled"             value="true" />  <!-- 全局映射器启用缓存 -->
+		<setting name="useGeneratedKeys"         value="true" />  <!-- 允许 JDBC 支持自动生成主键 -->
+		<setting name="defaultExecutorType"      value="REUSE" /> <!-- 配置默认的执行器 -->
+		<setting name="logImpl"                  value="SLF4J" /> <!-- 指定 MyBatis 所用日志的具体实现 -->
+		 <setting name="mapUnderscoreToCamelCase" value="true"/>
+	</settings>
+
+	<typeHandlers>
+		<typeHandler handler="com.fs.watch.handler.IntegerArrayTypeHandler" javaType="java.util.List"/>
+		<!--		<typeHandler handler="com.fs.watchApi.handler.ShortArrayTypeHandler" javaType="java.lang.Short[]" />-->
+		<typeHandler handler="com.fs.watch.handler.ShortListTypeHandler" javaType="java.util.List" />
+	</typeHandlers>
+</configuration>

+ 21 - 0
fs-user-course/src/main/resources/templates/healthCustomerService.html

@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml"
+      xmlns:th="http://www.thymeleaf.org">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+    <title >健康客服协议</title>
+    <link  rel="stylesheet"/>
+    <style>
+        body{
+            background-color:#F8F8F8;
+        }
+    </style>
+<body>
+<div th:utext="${healthCustomerService}" ></div>
+<script th:inline="javascript">
+
+</script>
+
+</body>
+</html>

+ 21 - 0
fs-user-course/src/main/resources/templates/privacyPolicy.html

@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml"
+      xmlns:th="http://www.thymeleaf.org">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+    <title >隐私政策</title>
+    <link  rel="stylesheet"/>
+    <style>
+        body{
+            background-color:#F8F8F8;
+        }
+    </style>
+<body>
+<div th:utext="${privacyPolicy}" ></div>
+<script th:inline="javascript">
+
+</script>
+
+</body>
+</html>

+ 21 - 0
fs-user-course/src/main/resources/templates/userAgreement.html

@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml"
+      xmlns:th="http://www.thymeleaf.org">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+    <title >用户协议</title>
+    <link  rel="stylesheet"/>
+    <style>
+        body{
+            background-color:#F8F8F8;
+        }
+    </style>
+<body>
+<div th:utext="${userAgreement}" ></div>
+<script th:inline="javascript">
+
+</script>
+
+</body>
+</html>

+ 21 - 0
fs-user-course/src/main/resources/templates/vipService.html

@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml"
+      xmlns:th="http://www.thymeleaf.org">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+    <title >会员服务协议</title>
+    <link  rel="stylesheet"/>
+    <style>
+        body{
+            background-color:#F8F8F8;
+        }
+    </style>
+<body>
+<div th:utext="${vipService}" ></div>
+<script th:inline="javascript">
+
+</script>
+
+</body>
+</html>

+ 1 - 0
pom.xml

@@ -261,6 +261,7 @@
         <module>fs-qw-voice</module>
         <module>fs-qwhook-msg</module>
         <module>fs-repeat-api</module>
+        <module>fs-user-course</module>
     </modules>
 
     <packaging>pom</packaging>