目录
一. OAuth
1. 初始化
父工程pom.xml
<?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">
<modelVersion>4.0.0</modelVersion>
<groupId>com.changgou</groupId>
<artifactId>changgou-parent</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<description>畅购商城的父工程</description>
<modules>
<module>changgou-gateway</module>
<module>changgou-service</module>
<module>changgou-service-api</module>
<module>changgou-web</module>
<module>changgou-eureka</module>
<module>changgou-common</module>
<module>changgou-common-db</module>
<module>changgou-user-oauth</module>
</modules>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.15.RELEASE</version>
</parent>
<!-- 跳过测试 -->
<properties>
<skipTests>true</skipTests>
</properties>
<!-- 依赖包 -->
<dependencies>
<!-- 测试包 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.51</version>
</dependency>
<!-- swagger 文档 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.6.1</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.6.1</version>
</dependency>
<!-- 鉴权 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Greenwich.SR1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
changgou-user-oauth
子工程pom.xml
<?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>changgou-parent</artifactId>
<groupId>com.changgou</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>changgou-user-oauth</artifactId>
<description>
OAuth2.0认证环境搭建
</description>
<dependencies>
<!--查询数据库数据-->
<dependency>
<groupId>com.changgou</groupId>
<artifactId>changgou-common-db</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-data</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>com.changgou</groupId>
<artifactId>changgou-service-user-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--thymeleaf-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>
</project>
application.yml
server:
port: 9001
spring:
application:
name: user-auth
redis:
host: 192.168.211.132
port: 6379
password:
jedis:
pool:
max-active: 8
max-idle: 8
min-idle: 0
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://192.168.211.132:3306/changgou_oauth?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true&serverTimezone=UTC
username: root
password: 123456
main:
allow-bean-definition-overriding: true
eureka:
instance:
prefer-ip-address: true
client:
service-url:
defaultZone: http://127.0.0.1:7001/eureka
auth:
ttl: 3600 #token存储到redis的过期时间
clientId: changgou
clientSecret: changgou
cookieDomain: localhost
cookieMaxAge: -1
encrypt:
key-store:
location: classpath:/changgou.jks
secret: changgou
alias: changgou
password: changgou
OAuthApplication
package com.changgou;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
import tk.mybatis.spring.annotation.MapperScan;
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients(basePackages = {"com.changgou.user.feign"}) //feign调用api com.changgou.user.feign.UserFeign
@MapperScan(basePackages = "com.changgou.auth.dao") //通用Mapper
public class OAuthApplication {
public static void main(String[] args) {
SpringApplication.run(OAuthApplication.class, args);
}
@Bean(name = "restTemplate")
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
2. Config配置
com.changgou.oauth.config文件目录下
AuthorizationServerConfig
package com.changgou.oauth.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.cloud.bootstrap.encrypt.KeyProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;
import javax.annotation.Resource;
import javax.sql.DataSource;
import java.security.KeyPair;
@Configuration
@EnableAuthorizationServer
class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
/**
* 数据源,用于从数据库获取数据进行认证操作,测试可以从内存中获取
*/
@Autowired
private DataSource dataSource;
/**
* jwt令牌转换器
*/
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
/**
* SpringSecurity 用户自定义授权认证类
*/
@Qualifier("userDetailsServiceImpl")
@Autowired
UserDetailsService userDetailsService;
/**
* 授权认证管理器
*/
@Autowired
AuthenticationManager authenticationManager;
/**
* 令牌持久化存储接口
*/
@Autowired
TokenStore tokenStore;
@Autowired
private CustomUserAuthenticationConverter customUserAuthenticationConverter;
/**
* 客户端信息配置
*
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 从数据库加载客户端信息
clients.jdbc(dataSource).clients(clientDetails());
}
/**
* 授权服务器端点配置
*
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.accessTokenConverter(jwtAccessTokenConverter)
.authenticationManager(authenticationManager)// 认证管理器
.tokenStore(tokenStore) // 令牌存储
.userDetailsService(userDetailsService); // 用户信息service
}
/**
* 授权服务器的安全配置
*
* @param oauthServer
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer.allowFormAuthenticationForClients()
.passwordEncoder(new BCryptPasswordEncoder())
.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()");
}
/**
* 读取密钥的配置
*/
@Bean("keyProp")
public KeyProperties keyProperties() {
return new KeyProperties();
}
@Resource(name = "keyProp")
private KeyProperties keyProperties;
//客户端配置 oauth_client_details 从数据库中加载了客户端的信息
@Bean
public ClientDetailsService clientDetails() {
return new JdbcClientDetailsService(dataSource);
}
@Bean
@Autowired
public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) {
return new JwtTokenStore(jwtAccessTokenConverter);
}
/****
* JWT令牌转换器
* @param customUserAuthenticationConverter
* @return
*/
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(CustomUserAuthenticationConverter customUserAuthenticationConverter) {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
KeyPair keyPair = new KeyStoreKeyFactory(
keyProperties.getKeyStore().getLocation(), //证书路径 changgou.jks
keyProperties.getKeyStore().getSecret().toCharArray()) //证书秘钥 changgouapp
.getKeyPair(
keyProperties.getKeyStore().getAlias(), //证书别名 changgou
keyProperties.getKeyStore().getPassword().toCharArray()); //证书密码 changgou
converter.setKeyPair(keyPair);
//配置自定义的CustomUserAuthenticationConverter
DefaultAccessTokenConverter accessTokenConverter = (DefaultAccessTokenConverter) converter.getAccessTokenConverter();
accessTokenConverter.setUserTokenConverter(customUserAuthenticationConverter);
return converter;
}
}
CustomUserAuthenticationConverter
package com.changgou.oauth.config;
import com.changgou.oauth.util.UserJwt;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.provider.token.DefaultUserAuthenticationConverter;
import org.springframework.stereotype.Component;
import java.util.LinkedHashMap;
import java.util.Map;
@Component
public class CustomUserAuthenticationConverter extends DefaultUserAuthenticationConverter {
@Autowired
UserDetailsService userDetailsService;
@Override
public Map<String, ?> convertUserAuthentication(Authentication authentication) {
LinkedHashMap response = new LinkedHashMap();
String name = authentication.getName();
response.put("username", name);
Object principal = authentication.getPrincipal();
UserJwt userJwt = null;
if(principal instanceof UserJwt){
userJwt = (UserJwt) principal;
}else{
//refresh_token默认不去调用userdetailService获取用户信息,这里我们手动去调用,得到 UserJwt
UserDetails userDetails = userDetailsService.loadUserByUsername(name);
userJwt = (UserJwt) userDetails;
}
response.put("name", userJwt.getName());
response.put("id", userJwt.getId());
//公司 response.put("compy", "songsi");
if (authentication.getAuthorities() != null && !authentication.getAuthorities().isEmpty()) {
response.put("authorities", AuthorityUtils.authorityListToSet(authentication.getAuthorities()));
}
return response;
}
}
UserDetailsServiceImpl
package com.changgou.oauth.config;
import com.changgou.oauth.util.UserJwt;
import com.changgou.user.feign.UserFeign;
import entity.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.List;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private ClientDetailsService clientDetailsService;
@Autowired
private UserFeign userFeign;
/**
* 自定义授权认证
*
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// ========== 客户端信息认证 start ==========
// 取出身份,如果身份为空说明没有认证
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// 没有认证统一采用 httpbasic 认证,httpbasic中存储了client_id和client_secret,开始认证client_id和client_secret
if (authentication == null) {
// 查询数据库 oauth_client_details 数据库查客户端信息
ClientDetails clientDetails = clientDetailsService.loadClientByClientId(username);
if (clientDetails != null) {
// 秘钥
String clientSecret = clientDetails.getClientSecret();
// 数据库查找方式
return new User(username, clientSecret, AuthorityUtils.commaSeparatedStringToAuthorityList(""));
}
}
// ========== 客户端信息认证 end ==========
// ========== 用户账号密码信息 start ==========
if (StringUtils.isEmpty(username)) {
return null;
}
// 从数据库加载查询用户信息 调用Feign去数据库中查询用户信息了
Result<com.changgou.user.pojo.User> userResult = userFeign.findById(username);
if (userResult == null || userResult.getData() == null) {
return null;
}
// 根据用户名查询用户信息
String pwd = userResult.getData().getPassword();
// 创建User对象
String permissions = "user, vip";
return new UserJwt(username, pwd, AuthorityUtils.commaSeparatedStringToAuthorityList(permissions));
// ========== 用户账号密码信息 end ==========
}
}
oauth微服务Feign调用user用户微服务里面的controller路径方法
package com.changgou.user.feign;
import com.changgou.user.pojo.User;
import entity.Result;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
@FeignClient(name = "user")
@RequestMapping("/user")
public interface UserFeign {
/**
* 添加用户积分
*
* @param points
* @return
*/
@GetMapping("/points/add")
public Result addPoints(@RequestParam Integer points);
/**
* 根据 ID 查询用户信息
*
* @param id
* @return
*/
@GetMapping("/load/{id}")
public Result<User> findById(@PathVariable String id);
}
WebSecurityConfig
package com.changgou.oauth.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@EnableWebSecurity
@Order(-1) //Spring IOC容器中Bean的执行顺序的优先级 默认是最低优先级,值越小优先级越高
class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 忽略安全拦截的 URL
*
* @param web
* @throws Exception
*/
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers(
"/user/login",
"/css/**",
"/oauth/login",
"/login.html",
"/login",
"/data/**",
"/fonts/**",
"/img/**",
"/js/**",
"/user/logout");
}
/**
* 创建授权管理认证对象
*
* @return
* @throws Exception
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
AuthenticationManager manager = super.authenticationManagerBean();
return manager;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
super.configure(auth);
}
/**
* 采用 BCryptPasswordEncoder 对密码进行编码
*
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* @param http
* @throws Exception
*/
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.httpBasic() //启用Http基本身份验证
.and()
.formLogin() //启用表单身份验证
.and()
.authorizeRequests() //限制基于Request请求访问
.anyRequest()
.authenticated(); //其他请求都需要经过验证
http.formLogin().
loginPage("/oauth/login").
loginProcessingUrl("/user/login");
}
}
3. util工具
adminToken管理员令牌发放
package com.changgou.oauth.util;
import com.alibaba.fastjson.JSON;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.jwt.Jwt;
import org.springframework.security.jwt.JwtHelper;
import org.springframework.security.jwt.crypto.sign.RsaSigner;
import org.springframework.security.rsa.crypto.KeyStoreKeyFactory;
import java.security.KeyPair;
import java.security.interfaces.RSAPrivateKey;
import java.util.HashMap;
public class AdminToken {
/**
* 管理员令牌发放
* @return
*/
public static String adminToken() {
// 加载证书,读取类路径中的文件
ClassPathResource resource = new ClassPathResource("changgou.jks");
// 读取证书数据,加载读取证书数据
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(resource, "changgou".toCharArray());
// 获取证书中的一对秘钥
KeyPair keyPair = keyStoreKeyFactory.getKeyPair("changgou", "changgou".toCharArray());
// 获取私钥->RSA算法
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
// 创建令牌,需要私钥加盐(RSA算法)
HashMap<String, Object> payload = new HashMap<>();
payload.put("nickname","tomcat");
payload.put("address","sz");
payload.put("authorities",new String[]{"admin", "oauth"});
Jwt jwt = JwtHelper.encode(JSON.toJSONString(payload), new RsaSigner(privateKey));
// 获取令牌数据
String token = jwt.getEncoded();
return token;
}
}
AuthToken用户令牌封装
package com.changgou.oauth.util;
import java.io.Serializable;
public class AuthToken implements Serializable {
//令牌信息
String accessToken;
//刷新token(refresh_token)
String refreshToken;
//jwt短令牌
String jti;
public String getAccessToken() {
return accessToken;
}
public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}
public String getRefreshToken() {
return refreshToken;
}
public void setRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
}
public String getJti() {
return jti;
}
public void setJti(String jti) {
this.jti = jti;
}
}
CookieUtil
package com.changgou.oauth.util;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
public class CookieUtil {
/**
* 设置cookie
*
* @param response
* @param name cookie名字
* @param value cookie值
* @param maxAge cookie生命周期 以秒为单位
*/
public static void addCookie(HttpServletResponse response, String domain, String path, String name,
String value, int maxAge, boolean httpOnly) {
Cookie cookie = new Cookie(name, value);
cookie.setDomain(domain);
cookie.setPath(path);
cookie.setMaxAge(maxAge);
cookie.setHttpOnly(httpOnly);
response.addCookie(cookie);
}
/**
* 根据cookie名称读取cookie
* @param request
* @return map<cookieName,cookieValue>
*/
public static Map<String,String> readCookie(HttpServletRequest request, String ... cookieNames) {
Map<String,String> cookieMap = new HashMap<String,String>();
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
String cookieName = cookie.getName();
String cookieValue = cookie.getValue();
for(int i=0;i<cookieNames.length;i++){
if(cookieNames[i].equals(cookieName)){
cookieMap.put(cookieName,cookieValue);
}
}
}
}
return cookieMap;
}
}
UserJwt
package com.changgou.oauth.util;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import java.util.Collection;
public class UserJwt extends User {
private String id; //用户ID
private String name; //用户名字
private String comny;//设置公司
public UserJwt(String username, String password, Collection<? extends GrantedAuthority> authorities) {
super(username, password, authorities);
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
4. Feign拦截器
TokenRequestInterceptor
package com.changgou.oauth.interceptor;
import com.changgou.oauth.util.AdminToken;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.context.annotation.Configuration;
@Configuration
public class TokenRequestInterceptor implements RequestInterceptor {
/**
* feign 执行之前进行拦截
*
* @param template
*/
@Override
public void apply(RequestTemplate template) {
// 生成 admin 令牌
String token = AdminToken.adminToken();
template.header("Authorization", "bearer " + token);
}
}
5. Contoller控制
AuthController
package com.changgou.oauth.controller;
import com.changgou.oauth.service.AuthService;
import com.changgou.oauth.util.AuthToken;
import com.changgou.oauth.util.CookieUtil;
import entity.Result;
import entity.StatusCode;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletResponse;
@RestController
@RequestMapping(value = "/userx")
public class AuthController {
//客户端ID
@Value("${auth.clientId}")
private String clientId;
//秘钥
@Value("${auth.clientSecret}")
private String clientSecret;
//Cookie存储的域名
@Value("${auth.cookieDomain}")
private String cookieDomain;
//Cookie生命周期
@Value("${auth.cookieMaxAge}")
private int cookieMaxAge;
@Autowired
AuthService authService;
@PostMapping("/login")
public Result login(String username, String password) {
if(StringUtils.isEmpty(username)){
throw new RuntimeException("用户名不允许为空");
}
if(StringUtils.isEmpty(password)){
throw new RuntimeException("密码不允许为空");
}
//申请令牌
AuthToken authToken = authService.login(username,password,clientId,clientSecret);
//用户身份令牌
String access_token = authToken.getAccessToken();
//将令牌存储到cookie
saveCookie(access_token);
return new Result(true, StatusCode.OK,"登录成功!");
}
/***
* 将令牌存储到cookie
* @param token
*/
private void saveCookie(String token){
HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
CookieUtil.addCookie(response,cookieDomain,"/","Authorization",token,cookieMaxAge,false);
}
}
UserLoginController
package com.changgou.oauth.controller;
import com.changgou.oauth.service.LoginService;
import com.changgou.oauth.util.AuthToken;
import com.changgou.oauth.util.CookieUtil;
import entity.Result;
import entity.StatusCode;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
@RestController
@RequestMapping("/user")
public class UserLoginController {
@Autowired
private LoginService loginService;
@Value("${auth.clientId}")
private String clientId;
@Value("${auth.clientSecret}")
private String clientSecret;
/**
* 授权模式 密码模式
*/
private static final String GRAND_TYPE = "password";
@Value("${auth.cookieDomain}")
private String cookieDomain;
/**
* Cookie生命周期
*/
@Value("${auth.cookieMaxAge}")
private int cookieMaxAge;
/**
* 密码模式认证
*
* @param username
* @param password
* @return
*/
@RequestMapping("/login")
public Result<Map> login(String username, String password) {
// 登录之后生成令牌的数据返回
AuthToken authToken = loginService.login(username, password, clientId, clientSecret, GRAND_TYPE);
// 设置到cookie中
saveCookie(authToken.getAccessToken());
if (authToken != null) {
return new Result<>(true, StatusCode.OK, "登陆成功!", authToken);
}
return new Result<>(false, StatusCode.LOGINERROR, "登陆失败!");
}
private void saveCookie(String token) {
HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
CookieUtil.addCookie(response, cookieDomain, "/", "Authorization", token, cookieMaxAge, false);
}
}
6. service.impl实现
AuthServiceImpl
package com.changgou.oauth.service.impl;
import com.alibaba.fastjson.JSON;
import com.changgou.oauth.service.AuthService;
import com.changgou.oauth.util.AuthToken;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Service;
import org.springframework.util.Base64Utils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.DefaultResponseErrorHandler;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;
import java.io.IOException;
import java.util.Map;
@Service
public class AuthServiceImpl implements AuthService {
@Autowired
private LoadBalancerClient loadBalancerClient;
@Autowired
private RestTemplate restTemplate;
/***
* 授权认证方法
* @param username
* @param password
* @param clientId
* @param clientSecret
* @return
*/
@Override
public AuthToken login(String username, String password, String clientId, String clientSecret) {
//申请令牌
AuthToken authToken = applyToken(username,password,clientId, clientSecret);
if(authToken == null){
throw new RuntimeException("申请令牌失败");
}
return authToken;
}
/****
* 认证方法
* @param username:用户登录名字
* @param password:用户密码
* @param clientId:配置文件中的客户端ID
* @param clientSecret:配置文件中的秘钥
* @return
*/
private AuthToken applyToken(String username, String password, String clientId, String clientSecret) {
//选中认证服务的地址
ServiceInstance serviceInstance = loadBalancerClient.choose("user-auth");
if (serviceInstance == null) {
throw new RuntimeException("找不到对应的服务");
}
//获取令牌的url
String path = serviceInstance.getUri().toString() + "/oauth/token";
//定义body
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
//授权方式
formData.add("grant_type", "password");
//账号
formData.add("username", username);
//密码
formData.add("password", password);
//定义头
MultiValueMap<String, String> header = new LinkedMultiValueMap<>();
header.add("Authorization", httpbasic(clientId, clientSecret));
//指定 restTemplate当遇到400或401响应时候也不要抛出异常,也要正常返回值
restTemplate.setErrorHandler(new DefaultResponseErrorHandler() {
@Override
public void handleError(ClientHttpResponse response) throws IOException {
//当响应的值为400或401时候也要正常响应,不要抛出异常
if (response.getRawStatusCode() != 400 && response.getRawStatusCode() != 401) {
super.handleError(response);
}
}
});
Map map = null;
try {
//http请求spring security的申请令牌接口
ResponseEntity<Map> mapResponseEntity = restTemplate.exchange(path, HttpMethod.POST,new HttpEntity<MultiValueMap<String, String>>(formData, header), Map.class);
//获取响应数据
map = mapResponseEntity.getBody();
} catch (RestClientException e) {
throw new RuntimeException(e);
}
if(map == null || map.get("access_token") == null || map.get("refresh_token") == null || map.get("jti") == null) {
//jti是jwt令牌的唯一标识作为用户身份令牌
throw new RuntimeException("创建令牌失败!");
}
//将响应数据封装成AuthToken对象
AuthToken authToken = new AuthToken();
//访问令牌(jwt)
String accessToken = (String) map.get("access_token");
//刷新令牌(jwt)
String refreshToken = (String) map.get("refresh_token");
//jti,作为用户的身份标识
String jwtToken= (String) map.get("jti");
authToken.setJti(jwtToken);
authToken.setAccessToken(accessToken);
authToken.setRefreshToken(refreshToken);
return authToken;
}
/***
* base64编码
* @param clientId
* @param clientSecret
* @return
*/
private String httpbasic(String clientId,String clientSecret){
//将客户端id和客户端密码拼接,按“客户端id:客户端密码”
String string = clientId+":"+clientSecret;
//进行base64编码
byte[] encode = Base64Utils.encode(string.getBytes());
return "Basic "+new String(encode);
}
}
LoginServiceImpl
package com.changgou.oauth.service.impl;
import com.changgou.oauth.service.LoginService;
import com.changgou.oauth.util.AuthToken;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import java.util.Base64;
import java.util.Map;
@Service
public class LoginServiceImpl implements LoginService {
@Autowired
private RestTemplate restTemplate;
@Autowired
private LoadBalancerClient loadBalancerClient;
@Override
public AuthToken login(String username, String password, String clientId, String clientSecret, String grandType) {
// 1.定义url (申请令牌的url)
// 获取指定服务的注册信息
ServiceInstance choose = loadBalancerClient.choose("user-auth");
String url = choose.getUri().toString() + "/oauth/token";
// 2.定义头信息 (有client id 和client 秘钥)
MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
headers.add("Authorization", "Basic " + Base64.getEncoder().encodeToString(new String(clientId + ":" + clientSecret).getBytes()));
// 3. 定义请求体 有授权模式 用户的名称 和密码
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
formData.add("grant_type", grandType);
formData.add("username", username);
formData.add("password", password);
// 4.模拟浏览器 发送POST 请求 携带 头 和请求体 到认证服务器
/**
* 参数1 指定要发送的请求的url
* 参数2 指定要发送的请求的方法 PSOT
* 参数3 指定请求实体(包含头和请求体数据)
*/
HttpEntity<MultiValueMap> requestEntity = new HttpEntity<MultiValueMap>(formData, headers);
ResponseEntity<Map> responseEntity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, Map.class);
// 5.接收到返回的响应(就是:令牌的信息)
Map<String, String> map = responseEntity.getBody();
// 处理返回的信息
AuthToken authToken = new AuthToken();
// jti,作为用户的身份标识
authToken.setJti(map.get("jti"));
// 访问令牌(jwt)
authToken.setAccessToken(map.get("access_token"));
// 刷新令牌(jwt)
authToken.setRefreshToken(map.get("refresh_token"));
return authToken;
}
public static void main(String[] args) {
byte[] decode = Base64.getDecoder().decode(new String("Y2hhbmdnb3UxOmNoYW5nZ291Mg==").getBytes());
System.out.println(new String(decode));
}
}
7. test测试
CreateJwtTest创建JWT令牌,使用私钥加密
package com.changgou.token;
import com.alibaba.fastjson.JSON;
import io.jsonwebtoken.Jwts;
import org.junit.Test;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.jwt.Jwt;
import org.springframework.security.jwt.JwtHelper;
import org.springframework.security.jwt.crypto.sign.RsaSigner;
import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;
import java.security.KeyPair;
import java.security.interfaces.RSAPrivateKey;
import java.util.HashMap;
import java.util.Map;
public class CreateJwtTest {
/***
* 创建令牌测试
*/
@Test
public void testCreateToken(){
//证书文件路径
String key_location="changgou.jks";
//秘钥库密码
String key_password="changgou";
//秘钥密码
String keypwd = "changgou";
//秘钥别名
String alias = "changgou";
//访问证书路径 读取jks的文件
ClassPathResource resource = new ClassPathResource(key_location);
//创建秘钥工厂
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(resource,key_password.toCharArray());
//读取秘钥对(公钥、私钥)
KeyPair keyPair = keyStoreKeyFactory.getKeyPair(alias,keypwd.toCharArray());
//获取私钥
RSAPrivateKey rsaPrivate = (RSAPrivateKey) keyPair.getPrivate();
//自定义Payload
Map<String, Object> tokenMap = new HashMap<>();
tokenMap.put("id", "1");
tokenMap.put("name", "itheima");
tokenMap.put("roles", "ROLE_VIP,ROLE_USER");
//生成Jwt令牌
Jwt jwt = JwtHelper.encode(JSON.toJSONString(tokenMap), new RsaSigner(rsaPrivate));
//取出令牌
String encoded = jwt.getEncoded();
System.out.println(encoded);
}
}
ParseJwtTest使用公钥解密令牌数据
package com.changgou.token;
import org.junit.Test;
import org.springframework.security.jwt.Jwt;
import org.springframework.security.jwt.JwtHelper;
import org.springframework.security.jwt.crypto.sign.RsaVerifier;
public class ParseJwtTest {
/***
* 校验令牌
*/
@Test
public void testParseToken(){
//令牌
String token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6WyJhcHAiXSwibmFtZSI6bnVsbCwiaWQiOm51bGwsImV4cCI6MTU2NzE2NTUxNCwiYXV0aG9yaXRpZXMiOlsic2Vja2lsbF9saXN0IiwiZ29vZHNfbGlzdCJdLCJqdGkiOiJkN2U4NWViNy03ZWYwLTQyZDUtYTgwZC0wNzEzZjZlNGY1NmMiLCJjbGllbnRfaWQiOiJjaGFuZ2dvdSIsInVzZXJuYW1lIjoiYWZhZmFmZHNhYSJ9.hU-tjZlR62LbTvmBEM7CrRw8W6Jzd2gYo4WfPyT_m4UrHgnnhnKpq0AnuoRRIUnjc2sXzsrA9ziQPgYRRLHxcesPcPALXZiGE_V3g7YIAmo1qzXyW1KYwZad0DrnxNNXokWdYveq2AgZZlfp_gtzKpgfdlYaZQLBKsedNMaiuJBzAYwLC8rtRnEmgKRYbRBvCatavHeycWYgvVW5z9Ii9Sd7uMIIr9T-J6Coj5cF4G_mHTakx3vQeFr_UQ6zFBKbEt5hiKTvAqcVJwHgRZTvBxWSV3nfRnH_JYoy3uk0K_awGWwMxvxvEjWX10GPLsRyfRK8SGl8xC35j5bH1UKDgg";
//公钥
String publickey = "-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAi7Drp7TubteIxAM71vQfH1trXsobxVrCAdONO3Moh6e+St0pP1IcLXBS5QtwF3dCIeCp9h9Tug0WZ3NRPJxBOl+h23nKgfnBpbqjQRa4/pZty4T4R9pqeVQtXpyUD1SyDCfy8hqVbd5wX+3l8+zHgKf3DmpEvfRxh0eRXcRV/5luU6T7Cu+7fu0eTbQpKT7gwDFRNRwhDIe+1uLgzmn/9ZpwtM7f3aumN97wFltsTMFlVFCr/3UDJXRt8opm2Qm3Z+vDA4x7qFgW5dVmXU3nCp7pjBK1zRMDnemRjiizo3Ha1mR9SJBA6zYgt1ZV71kndOjn5pPnq9f3RIZIAMgDyQIDAQAB-----END PUBLIC KEY-----";
//校验Jwt
Jwt jwt = JwtHelper.decodeAndVerify(token, new RsaVerifier(publickey));
//获取Jwt原始内容 载荷
String claims = jwt.getClaims();
System.out.println(claims);
//jwt令牌
String encoded = jwt.getEncoded();
System.out.println(encoded);
}
}
二. Canal
1. 初始化配置
pom.xml
<?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>changgou-service</artifactId>
<groupId>com.changgou</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>changgou-service-canal</artifactId>
<description>Canal 微服务, 实现数据库监控</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!--canal依赖, 下载 jar 包 starter-canal-0.0.1-SNAPSHOT 到本地-->
<!--本地导入jar包 starter-canal-0.0.1-SNAPSHOT.jar-->
<!-- <dependency>-->
<!-- <groupId>com.xpand</groupId>-->
<!-- <artifactId>starter-canal</artifactId>-->
<!-- <version>0.0.1-SNAPSHOT</version>-->
<!--<!– <scope>system</scope>–>-->
<!--<!– <systemPath>${project.basedir}/lib/starter-canal-0.0.1-SNAPSHOT.jar</systemPath>–>-->
<!-- </dependency>-->
<dependency>
<groupId>com.alibaba.otter</groupId>
<artifactId>canal.client</artifactId>
<version>1.0.25</version>
</dependency>
<dependency>
<groupId>com.changgou</groupId>
<artifactId>changgou-service-content-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--静态页API 服务-->
<dependency>
<groupId>com.changgou</groupId>
<artifactId>changgou-web-item-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
application.yml
server:
port: 18083
spring:
application:
name: canal
redis:
host: 192.168.211.132
port: 6379
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:7001/eureka
instance:
prefer-ip-address: true
feign:
hystrix:
enabled: true
#hystrix 配置
hystrix:
command:
default:
execution:
timeout:
#如果enabled设置为false,则请求超时交给ribbon控制
enabled: true
isolation:
#信号量隔离是多线程 (默认的线程隔离是单线程)
strategy: SEMAPHORE
#canal配置
canal:
client:
instances:
example:
host: 192.168.211.132
port: 11111
CanalApplication
package com.changgou;
import com.xpand.starter.canal.annotation.EnableCanalClient;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
/**
* @description canal的客户端 目的: 监听服务端的数据的变化
*/
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
@EnableEurekaClient
@EnableCanalClient //启动canal
@EnableFeignClients(basePackages = {"com.changgou.content.feign","com.changgou.item.feign"}) //需要在Canal微服务中调用广告微服务中的方法
public class CanalApplication {
public static void main(String[] args) {
SpringApplication.run(CanalApplication.class, args);
}
}
ContentFeign
com.changgou.content.feign
package com.changgou.content.feign;
import com.changgou.content.pojo.Content;
import entity.Result;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@FeignClient(name = "content")
@RequestMapping("/content")
public interface ContentFeign {
/**
* 根据分类的 ID 获取到广告列表
*
* 因为我们需要在Canal微服务中调用广告微服务中的方法
*
* @param id
* @return
*/
@GetMapping(value = "/list/category/{id}")
Result<List<Content>> findByCategory(@PathVariable(name = "id") Long id);
}
PageFeign
com.changgou.item.feign
package com.changgou.item.feign;
import entity.Result;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
@FeignClient(name = "item")
@RequestMapping("/page")
public interface PageFeign {
/**
* 根据 SpuID 生成静态页
*
* @param id
* @return
*/
@RequestMapping("/createHtml/{id}")
Result createHtml(@PathVariable Long id);
}
2. 实现监听
CanalDataEventListener
@CanalEventListener
package com.changgou.canal;
import com.alibaba.fastjson.JSON;
import com.alibaba.otter.canal.protocol.CanalEntry;
import com.changgou.content.feign.ContentFeign;
import com.changgou.content.pojo.Content;
import com.changgou.item.feign.PageFeign;
import com.xpand.starter.canal.annotation.*;
import entity.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.List;
/**
* @description 实现对表增删改操作的监听
*/
@CanalEventListener
public class CanalDataEventListener {
@Autowired
private ContentFeign contentFeign;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private PageFeign pageFeign;
/**
* 监听商品数据库的 tb_spu 的数据变化, 当数据变化的时候生成静态页或者删除静态页
* @param eventType
* @param rowData
*/
@ListenPoint(destination = "example",
schema = "changgou_goods",
table = {"tb_spu"},
eventType = {CanalEntry.EventType.UPDATE, CanalEntry.EventType.INSERT, CanalEntry.EventType.DELETE})
public void onEventCustomSpu(CanalEntry.EventType eventType, CanalEntry.RowData rowData) {
// 判断操作类型
if (eventType == CanalEntry.EventType.DELETE) {
String spuId = "";
List<CanalEntry.Column> beforeColumnsList = rowData.getBeforeColumnsList();
for (CanalEntry.Column column : beforeColumnsList) {
if (column.getName().equals("id")) {
spuId = column.getValue();
break;
}
}
// todo 删除静态页
} else {
// 新增或更新
List<CanalEntry.Column> afterColumnsList = rowData.getAfterColumnsList();
String spuId = "";
for (CanalEntry.Column column : afterColumnsList) {
if (column.getName().equals("id")) {
spuId = column.getValue();
break;
}
}
//更新 生成静态页
pageFeign.createHtml(Long.valueOf(spuId));
}
}
/**
* 监听自定义数据库的操作, 增删改
* 监听数据变化并将变化的数据写到Redis中
*
* @param eventType
* @param rowData
*/
@ListenPoint(destination = "example",
schema = "changgou_content",
table = {"tb_content", "tb_content_category"},
eventType = {
CanalEntry.EventType.UPDATE,
CanalEntry.EventType.DELETE,
CanalEntry.EventType.INSERT}
)
public void onEventCustomUpdate(CanalEntry.EventType eventType, CanalEntry.RowData rowData) {
//1. 获取列名为 category_id 的值
String categoryId = getColumnValue(eventType, rowData);
//2. 调用 feign 获取该分类下的所有的广告集合
Result<List<Content>> categoryList = contentFeign.findByCategory(Long.valueOf(categoryId));
List<Content> data = categoryList.getData();
//3. 使用 redisTemplate 存储到 redis 中
stringRedisTemplate.boundValueOps("content_" + categoryId).set(JSON.toJSONString(data));
}
private String getColumnValue(CanalEntry.EventType eventType, CanalEntry.RowData rowData) {
String categoryId = "";
// 判断如果是删除, 则获取 beforeList
if (eventType == CanalEntry.EventType.DELETE) {
for (CanalEntry.Column column : rowData.getBeforeColumnsList()) {
if ("category_id".equalsIgnoreCase(column.getName())) {
categoryId = column.getValue();
return categoryId;
}
}
} else {
// 判断如果是添加或者是更新, 获取 afterList
for (CanalEntry.Column column : rowData.getAfterColumnsList()) {
if ("category_id".equalsIgnoreCase(column.getName())) {
categoryId = column.getValue();
return categoryId;
}
}
}
return categoryId;
}
}
三. FastDFS
1. 初始化配置
pom.xml
<?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>changgou-service</artifactId>
<groupId>com.changgou</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>changgou-service-file</artifactId>
<description>文件上传工程</description>
<!--依赖包-->
<dependencies>
<!-- FastDFS 客户端 -->
<dependency>
<groupId>net.oschina.zcx7878</groupId>
<artifactId>fastdfs-client-java</artifactId>
<version>1.27.0.0</version>
</dependency>
<dependency>
<groupId>com.changgou</groupId>
<artifactId>changgou-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
application.yml
spring:
servlet:
multipart:
max-file-size: 10MB
max-request-size: 10MB
application:
name: file
server:
port: 18082
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:7001/eureka
instance:
prefer-ip-address: true
feign:
hystrix:
enabled: true
fdfs_client.conf
connect_timeout=60
network_timeout=60
charset=UTF-8
http.tracker_http_port=8080
tracker_server=192.168.211.132:22122
FileApplication
package com.changgou;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@SpringBootApplication(exclude={DataSourceAutoConfiguration.class})
@EnableEurekaClient
public class FileApplication {
public static void main(String[] args) {
SpringApplication.run(FileApplication.class, args);
}
}
2. 文件管理
FastDFS文件管理 FastDFSUtil
package com.changgou.util;
import com.changgou.file.FastDFSFile;
import entity.Result;
import org.csource.common.MyException;
import org.csource.common.NameValuePair;
import org.csource.fastdfs.*;
import org.springframework.core.io.ClassPathResource;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* @description 实现FastDFS文件管理
* 文件上传, 下载, 删除, 文件信息获取, Storage/tracker 信息获取
*/
public class FastDFSUtil {
static {
try {
// 查找 classpath 下的文件路径
String filename = new ClassPathResource("fdfs_client.conf").getPath();
//加载 Tracker 链接信息
ClientGlobal.init(filename);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 获取 StorageClient
*
* @return StorageClient
* @throws IOException 异常
*/
private static StorageClient getStorageClient() throws Exception {
// 获取 trackerServer 对象
TrackerServer trackerServer = getTrackerServer();
// 通过 TrackerServer 的链接信息可以获取 Storage 的链接信息, 创建 StorageClient 对象存储 Storage 的链接信息
return new StorageClient(trackerServer, null);
}
/**
* 获取 TrackerServer
*
* @return TrackerServer
* @throws IOException 异常
*/
private static TrackerServer getTrackerServer() throws Exception {
// 创建一个 tracker 访问的客户端对象 trackerClient
TrackerClient trackerClient = new TrackerClient();
// 通过 trackerClient 访问 TrackerServer 服务, 获取连接信息
return trackerClient.getConnection();
}
/**
* 文件上传
*
* @param fastDFSFile 上传的文件信息封装
* @return 上传文件存储在 Storage 的信息
*/
public static String[] upload(FastDFSFile fastDFSFile) throws Exception {
// 附加参数
NameValuePair[] metaList = new NameValuePair[1];
metaList[0] = new NameValuePair("author", fastDFSFile.getAuthor());
// 获取 StorageClient 对象
StorageClient storageClient = getStorageClient();
// 通过 StorageClient 访问 Storage, 实现文件上传, 并且获取文件上传后的存储信息
// 1.上传文件的直接数组 2.文件的扩展名 3.附加参数
// uploads[]:
// uploads[0]: 文件上传所存储的 Storage 的组名字 ex: group1
// uploads[1]: 文件存储到 Storage 的文件名字 ex: M00/01/02/abc.jpg
String[] uploads = storageClient.upload_file(fastDFSFile.getContent(), fastDFSFile.getExt(), metaList);
return uploads;
}
/**
* 获取文件信息
*
* @param groupName 文件组名 group1
* @param remoteFileName 文件的存储路径 M00/01/02/abc.jpg
* @return 文件信息
*/
public static FileInfo getFile(String groupName, String remoteFileName) throws Exception {
// 获取 StorageClient 对象
StorageClient storageClient = getStorageClient();
// 获取文件信息
return storageClient.get_file_info(groupName, remoteFileName);
}
/**
* 文件下载
*
* @param groupName 文件组名 group1
* @param remoteFileName 文件的存储路径 M00/01/02/abc.jpg
* @return 输入字节流
*/
public static InputStream downloadFile(String groupName, String remoteFileName) throws Exception {
// 获取 StorageClient 对象
StorageClient storageClient = getStorageClient();
// 文件下载
byte[] buffer = storageClient.download_file(groupName, remoteFileName);
return new ByteArrayInputStream(buffer);
}
/**
* 文件删除
*
* @param groupName 文件组名 group1
* @param remoteFileName 文件的存储路径 M00/01/02/abc.jpg
*/
public static void deleteFile(String groupName, String remoteFileName) throws Exception {
// 获取 StorageClient 对象
StorageClient storageClient = getStorageClient();
// 文件删除
storageClient.delete_file(groupName, remoteFileName);
}
/**
* 获取 Storage 信息
*
* @return Storage
* @throws Exception 异常
*/
public static StorageServer getStorages() throws Exception {
// 创建一个 tracker 访问的客户端对象 trackerClient
TrackerClient trackerClient = new TrackerClient();
// 通过 trackerClient 访问 TrackerServer 服务, 获取连接信息
TrackerServer trackerServer = trackerClient.getConnection();
// 获取 Storage 信息
return trackerClient.getStoreStorage(trackerServer);
}
/**
* 获取 Storage 的 IP 和端口信息
*
* @return Storage 组信息
* @throws Exception 异常
*/
public static ServerInfo[] getServerInfo(String groupName, String remoteFileName) throws Exception {
// 创建一个 tracker 访问的客户端对象 trackerClient
TrackerClient trackerClient = new TrackerClient();
// 通过 trackerClient 访问 TrackerServer 服务, 获取连接信息
TrackerServer trackerServer = trackerClient.getConnection();
// 获取 Storage 的 IP 和端口信息
return trackerClient.getFetchStorages(trackerServer, groupName, remoteFileName);
}
/**
* 获取 Tracker 信息
*
* @return Tracker 信息: http://192.168.211.132:8080
* @throws Exception 异常
*/
public static String getTrackerInfo() throws Exception {
// 获取 trackerServer
TrackerServer trackerServer = getTrackerServer();
// Tracker 的 IP, Http 端口
String ip = trackerServer.getInetSocketAddress().getHostString();
int trackerHttpPort = ClientGlobal.getG_tracker_http_port();
String url = "http://" + ip + ":" + trackerHttpPort;
return url + "/";
}
public static void main(String[] args) throws Exception {
// FileInfo fileInfo = getFile("group1", "M00/00/00/wKjThF9jWliAJtdxAASoXdziGaU130.jpg");
// System.out.println(fileInfo.getSourceIpAddr());
// System.out.println(fileInfo.getFileSize());
// deleteFile("group1", "M00/00/00/wKjThF9jSsCAKt4oAADaM-iueBo736.jpg");
// StorageServer storages = getStorages();
// System.out.println(storages.getStorePathIndex());
// ServerInfo[] groups = getServerInfo("group1", "M00/00/00/wKjThF9jX92AY-kPAAFd4XVtbCo293.jpg");
// for (ServerInfo group : groups) {
// System.out.println(group.getIpAddr());
// System.out.println(group.getPort());
// }
System.out.println(getTrackerInfo());
}
}
封装文件上传信息 FastDFSFile
package com.changgou.file;
import java.io.Serializable;
import java.util.Arrays;
/**
* @description 封装文件上传信息: 时间, author, type, size, 附加信息, 后缀, 文件内容
*/
public class FastDFSFile implements Serializable {
/**
* 文件名字
*/
private String name;
/**
* 文件内容
*/
private byte[] content;
/**
* 文件扩展名
*/
private String ext;
/**
* 文件MD5摘要值
*/
private String md5;
/**
* 文件创建作者
*/
private String author;
@Override
public String toString() {
return "FastDFSFile{" +
"name='" + name + '\'' +
", content=" + Arrays.toString(content) +
", ext='" + ext + '\'' +
", md5='" + md5 + '\'' +
", author='" + author + '\'' +
'}';
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public byte[] getContent() {
return content;
}
public void setContent(byte[] content) {
this.content = content;
}
public String getExt() {
return ext;
}
public void setExt(String ext) {
this.ext = ext;
}
public String getMd5() {
return md5;
}
public void setMd5(String md5) {
this.md5 = md5;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public FastDFSFile(String name, byte[] content, String ext, String md5, String author) {
this.name = name;
this.content = content;
this.ext = ext;
this.md5 = md5;
this.author = author;
}
public FastDFSFile(String name, byte[] content, String ext) {
this.name = name;
this.content = content;
this.ext = ext;
}
}
FileUploadController
package com.changgou.search.controller;
import com.changgou.file.FastDFSFile;
import com.changgou.util.FastDFSUtil;
import entity.Result;
import entity.StatusCode;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
@RestController
@RequestMapping("/upload")
@CrossOrigin
public class FileUploadController {
/**
* 文件上传
*
* @param file 文件
* @return 消息结果
* @throws Exception 异常
*/
@PostMapping
public Result upload(@RequestParam("file") MultipartFile file) throws Exception {
// 封装文件信息
FastDFSFile fastDFSFile = new FastDFSFile(
// 文件名字
file.getOriginalFilename(),
// 文件字节数组
file.getBytes(),
// 获取文件扩展名
StringUtils.getFilenameExtension(file.getOriginalFilename())
);
// 调用 FastDFSUtil 工具类将文件传入到 FastDFS 中
String[] uploads = FastDFSUtil.upload(fastDFSFile);
// 拼接访问地址 ex: url = http://192.168.211.132:8080/group1/M00/01/02/abc.jpg
String url = FastDFSUtil.getTrackerInfo() + uploads[0] + "/" + uploads[1];
return new Result(true, StatusCode.OK, "上传成功!", url);
}
}
四. WeiXinPay
1. 初始化配置
application.yml
server:
port: 18089
spring:
application:
name: pay
main:
allow-bean-definition-overriding: true
rabbitmq:
host: 192.168.211.132
port: 5672
username: guest
password: guest
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:7001/eureka
instance:
prefer-ip-address: true
feign:
hystrix:
enabled: true
#hystrix 配置
hystrix:
command:
default:
execution:
timeout:
#如果enabled设置为false,则请求超时交给ribbon控制
enabled: true
isolation:
strategy: SEMAPHORE
#微信支付信息配置
weixin:
# 应用ID
appid: wx8397f8696b538317
# 商户ID
partner: 1473426802
# 秘钥
partnerkey: T6m9iK73b0kn9g5v426MKfHQH7X8rKwb
# 支付回调地址
notifyurl: http://34b8620115.eicp.vip/weixin/pay/notify/url
#位置支付交换机和队列
mq:
pay:
exchange:
order: exchange.order
seckillorder: exchange.seckillorder
queue:
order: queue.order
seckillorder: queue.seckillorder
routing:
key: queue.order
seckillkey: queue.seckillorder
pom.xml
<?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>changgou-service</artifactId>
<groupId>com.changgou</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<description>支付微服务,与微信服务器对接</description>
<artifactId>changgou-service-pay</artifactId>
<dependencies>
<!-- 加入 ampq -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
</dependencies>
</project>
2. 代码实现
WeixinPayApplication
package com.changgou;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
@EnableEurekaClient
public class WeixinPayApplication {
public static void main(String[] args) {
SpringApplication.run(WeixinPayApplication.class, args);
}
}
MQConfig消息队列配置
package com.changgou.pay.mq;
import org.springframework.amqp.core.*;
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;
@Configuration
public class MQConfig {
/**
* 读取配置文件中的对象
*/
@Autowired
private Environment env;
/**
* 创建队列
*
* @return
*/
@Bean
public Queue orderQueue() {
return new Queue(env.getProperty("mq.pay.queue.order"));
}
/**
* 创建交换机
*
* @return
*/
public Exchange orderExchange() {
return new DirectExchange(env.getProperty("mq.pay.exchange.order"), true, false);
}
/**
* 队列绑定交换机
*
* @param orderQueue
* @param orderExchange
* @return
*/
public Binding orderQueueExchange(Queue orderQueue, Exchange orderExchange) {
return BindingBuilder.bind(orderQueue).to(orderExchange).with(env.getProperty("mq.pay.routing.order")).noargs();
}
/**
* 创建秒杀队列
*
* @return
*/
@Bean
public Queue orderSeckillQueue() {
return new Queue(env.getProperty("mq.pay.queue.seckillorder"));
}
/**
* 创建秒杀交换机
*
* @return
*/
public Exchange orderSeckillExchange() {
return new DirectExchange(env.getProperty("mq.pay.exchange.seckillorder"), true, false);
}
/**
* 秒杀队列绑定秒杀交换机
*
* @param orderSeckillQueue
* @param orderSeckillExchange
* @return
*/
public Binding orderSeckillQueueExchange(Queue orderSeckillQueue, Exchange orderSeckillExchange) {
return BindingBuilder.bind(orderSeckillQueue).to(orderSeckillExchange).with(env.getProperty("mq.pay.routing.seckillkey")).noargs();
}
}
WeixinPayController
package com.changgou.pay.controller;
import com.alibaba.fastjson.JSON;
import com.changgou.pay.service.WeixinPayService;
import com.github.wxpay.sdk.WXPayUtil;
import entity.Result;
import entity.StatusCode;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import java.io.ByteArrayOutputStream;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/weixin/pay")
public class WeixinPayController {
@Autowired
private WeixinPayService weixinPayService;
@Autowired
private RabbitTemplate rabbitTemplate;
@RequestMapping("/notify/url")
public String notifyUrl(HttpServletRequest request) throws Exception {
// 获取网络输入流
ServletInputStream is = request.getInputStream();
// 创建一个 OutputStream, 输入文件中
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len = 0;
while ((len = is.read(buffer)) != -1) {
baos.write(buffer, 0, len);
}
// 微信支付结果的字节数组
byte[] bytes = baos.toByteArray();
String xmlResult = new String(bytes, "UTF-8");
System.out.println(xmlResult);
// XML 字符串转换成 Map
Map<String, String> resultMap = WXPayUtil.xmlToMap(xmlResult);
System.out.println(resultMap);
// 获取自定义参数
String attach = resultMap.get("attach");
Map<String, String> attachMap = JSON.parseObject(attach,Map.class);
// 发送支付结果给 MQ
rabbitTemplate.convertAndSend("exchange", "routingKey", JSON.toJSONString(resultMap));
//响应数据设置
Map<String, String> respMap = new HashMap<>();
respMap.put("return_code", "SUCCESS");
respMap.put("return_msg", "OK");
return WXPayUtil.mapToXml(respMap);
}
/**
* 查询微信支付状态
*
* @param outtradeno
* @return
*/
@GetMapping("/status/query")
public Result queryStatus(String outtradeno) {
// 查询支付状态
Map map = weixinPayService.queryStatus(outtradeno);
return new Result(true, StatusCode.OK, "查询支付状态成功!", map);
}
/**
* 创建二维码,区分普通订单和秒杀订单
*
* @param parameterMap
* @return
*/
@RequestMapping("/create/native")
public Result createNative(@RequestParam Map<String, String> parameterMap) {
Map<String, String> resultMap = weixinPayService.createNative(parameterMap);
return new Result(true, StatusCode.OK, "创建二维码预付订单成功!", resultMap);
}
}
WeixinPayServiceImpl
package com.changgou.pay.service.impl;
import com.alibaba.fastjson.JSON;
import com.changgou.pay.service.WeixinPayService;
import com.github.wxpay.sdk.WXPayUtil;
import entity.HttpClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.HashMap;
import java.util.Map;
@Service
public class WeixinPayServiceImpl implements WeixinPayService {
/**
* 应用ID
*/
@Value("${weixin.appid}")
private String appid;
/**
* 商户号
*/
@Value("${weixin.partner}")
private String partner;
/**
* 秘钥
*/
@Value("${weixin.partnerkey}")
private String partnerkey;
/**
* 支付回调地址
*/
@Value("${weixin.notifyurl}")
private String notifyurl;
/**
* 发送 http 请求,获取结果
*
* @param paramMap
* @param url
* @return
* @throws Exception
*/
private Map<String, String> getHttpResult(Map<String, String> paramMap, String url) throws Exception {
// 将Map数据转成XML字符
String xmlParam = WXPayUtil.generateSignedXml(paramMap, partnerkey);
// 发送请求
HttpClient httpClient = new HttpClient(url);
// 提交参数
httpClient.setXmlParam(xmlParam);
// 提交
httpClient.post();
// 获取返回数据
String content = httpClient.getContent();
// 将返回数据解析成Map
Map<String, String> resultMap = WXPayUtil.xmlToMap(content);
return resultMap;
}
/**
* 关闭微信支付
*
* @param orderId
* @return
* @throws Exception
*/
@Override
public Map<String, String> closePay(Long orderId) throws Exception {
// 参数设置
Map<String, String> paramMap = new HashMap<>();
paramMap.put("appid", appid);
paramMap.put("mch_id", partner);
paramMap.put("nonce_str", WXPayUtil.generateNonceStr());
paramMap.put("out_trade_no", String.valueOf(orderId));
// 确定url
String url = "https://api.mch.weixin.qq.com/pay/closeorder";
return getHttpResult(paramMap, url);
}
/**
* 查询支付状态
*
* @param outtradeno
* @return
*/
@Override
public Map queryStatus(String outtradeno) {
try {
// 参数
Map<String, String> paramMap = new HashMap<>();
// 应用ID
paramMap.put("appid", appid);
// 商户号
paramMap.put("mch_id", partner);
// 随机字符
paramMap.put("nonce_str", WXPayUtil.generateNonceStr());
// 商户订单号
paramMap.put("out_trade_no", outtradeno);
// URL 地址
String url = "https://api.mch.weixin.qq.com/pay/orderquery";
return getHttpResult(paramMap, url);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 创建二维码
*
* @param parameterMap
* @return
*/
@Override
public Map createNative(Map<String, String> parameterMap) {
try {
// 参数
Map<String, String> paramMap = new HashMap<>();
// 应用ID
paramMap.put("appid", appid);
// 商户号
paramMap.put("mch_id", partner);
// 随机字符
paramMap.put("nonce_str", WXPayUtil.generateNonceStr());
// 订单描述
paramMap.put("body", "畅购");
// 商户订单号
paramMap.put("out_trade_no", parameterMap.get("outtradeno"));
// 交易金额
paramMap.put("total_fee", parameterMap.get("totalfee"));
// 终端IP
paramMap.put("spbill_create_ip", "127.0.0.1");
// 回调地址
paramMap.put("notify_url", notifyurl);
// 交易类型
paramMap.put("trade_type", "NATIVE");
/* 秒杀支付的流程和之前做的类似,只不过现在秒杀订单的支付状态发送到Queue2中,
普通订单还是发送到queue1中,但是我们怎么知道该将订单的支付状态发送给queue1还是queue2呢?
如果微信服务器可以将MQ队列的exchange和routingKey返回给我们就好了,这样我们就可以动态地指定要发送的MQ了。
从微信支付的官方文档中我们可以知道在创建二维码和接收支付结果的参数中都有一个attach参数
普通订单:exchange:exchange.order routingKey:routing.order
秒杀订单:exchange:exchange.seckill_order routingKey:routing.seckill_order
*/
// 获取自定义数据,传递用于区分秒杀/普通订单的识别队列信息
String exchange = parameterMap.get("exchange");
String routingKey = parameterMap.get("routingKey");
Map<String, String> attachMap = new HashMap<>();
attachMap.put("exchange",exchange);
attachMap.put("routingKey",routingKey);
// 如果是秒杀订单,需要传username
String username = parameterMap.get("username");
if (!StringUtils.isEmpty(username)) {
attachMap.put("username", username);
}
String attach = JSON.toJSONString(attachMap);
paramMap.put("attach", attach);
// URL 地址
String url = "https://api.mch.weixin.qq.com/pay/unifiedorder";
return getHttpResult(paramMap, url);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
五. ElasticSearch
1. 初始化配置
pom.xml
<dependencies>
<!--goods API依赖-->
<dependency>
<groupId>com.changgou</groupId>
<artifactId>changgou-service-goods-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--SpringDataES依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
</dependencies>
application.yml
server:
port: 18085
spring:
application:
name: search
data:
elasticsearch:
cluster-name: my-application
#访问端口是9200,Java程序端口是9300
cluster-nodes: 192.168.211.132:9300
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:7001/eureka
instance:
prefer-ip-address: true
feign:
hystrix:
enabled: true
# Feign 请求读取数据超时时间
ribbon:
ReadTimeout: 300000
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 10000
SearchApplication
package com.changgou.search;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories;
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableEurekaClient
@EnableFeignClients(basePackages = "com.changgou.goods.feign")
@EnableElasticsearchRepositories(basePackages = "com.changgou.search.dao")
public class SearchApplication {
public static void main(String[] args) {
// Springboot 整合 Elasticsearch 在项目启动前设置一下的属性,防止报错
// 解决 netty 冲突后初始化 client 时还会抛出异常
// availableProcessors is already set to [12], rejecting [12]
System.setProperty("es.set.netty.runtime.available.processors", "false");
SpringApplication.run(SearchApplication.class, args);
}
}
2. 代码实现
SearchController
package com.changgou.search.controller;
import com.changgou.search.service.SearchService;
import entity.Result;
import entity.StatusCode;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@CrossOrigin
@RequestMapping("/search")
public class SearchController {
@Autowired
private SearchService searchService;
/**
* 搜索
*
* @param searchMap
* @return
*/
@GetMapping
public Map search(@RequestParam(required = false) Map<String, String> searchMap) throws Exception {
return searchService.search(searchMap);
}
/**
* 导入数据
*
* @return
*/
@GetMapping("/import")
public Result importData() {
searchService.importSku();
return new Result(true, StatusCode.OK, "导入数据到索引库中成功!");
}
}
SearchEsMapper
package com.changgou.search.dao;
import com.changgou.search.pojo.SkuInfo;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface SearchEsMapper extends ElasticsearchRepository<SkuInfo, Long> {
}
SearchServiceImpl
package com.changgou.search.service.impl;
import com.alibaba.fastjson.JSON;
import com.changgou.search.dao.SearchEsMapper;
import com.changgou.goods.feign.SkuFeign;
import com.changgou.goods.pojo.Sku;
import com.changgou.search.pojo.SkuInfo;
import com.changgou.search.service.SearchService;
import entity.Result;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.common.text.Text;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.bucket.terms.StringTerms;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.elasticsearch.search.sort.FieldSortBuilder;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
import org.springframework.data.elasticsearch.core.SearchResultMapper;
import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage;
import org.springframework.data.elasticsearch.core.aggregation.impl.AggregatedPageImpl;
import org.springframework.data.elasticsearch.core.query.NativeSearchQuery;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.*;
@Service
public class SearchServiceImpl implements SearchService {
@Autowired
private SkuFeign skuFeign;
@Autowired
private SearchEsMapper searchEsMapper;
@Autowired
private ElasticsearchTemplate esTemplate;
@Override
public Map<String, Object> search(Map<String, String> searchMap) throws Exception {
// 基本搜索条件构造,分类品牌规格价格过滤
NativeSearchQueryBuilder builder = buildBasicQuery(searchMap);
// 集合搜索
Map<String, Object> resultMap = searchList(builder);
// 分组搜索
Map<String, Object> groupMap = searchGroupList(builder, searchMap);
resultMap.putAll(groupMap);
return resultMap;
}
/**
* 分组查询: 分类分组,规格分组,品牌分组
*
* @param builder 查询对象的构件对象
* @return categoryList
*/
private Map<String, Object> searchGroupList(NativeSearchQueryBuilder builder, Map<String, String> searchMap) {
// 存储所有分组树结果
HashMap<String, Object> groupMapResult = new HashMap<>();
// 当用户选择了分类,将分类作为搜索条件,则不需要对分类进行分组搜索,因为分组搜索的数据是用于显示分类搜索条件
if (searchMap == null || StringUtils.isEmpty(searchMap.get("category"))) {
builder.addAggregation(AggregationBuilders.terms("skuCategory").field("categoryName"));
}
// 当用户选择了品牌,将品牌作为搜索条件,则不需要对品牌进行分组搜索,因为分组搜索的数据是用于显示品牌搜索条件
if (searchMap == null || StringUtils.isEmpty(searchMap.get("brand"))) {
builder.addAggregation(AggregationBuilders.terms("skuBrand").field("brandName"));
}
// 规格查询
builder.addAggregation(AggregationBuilders.terms("skuSpec").field("spec.keyword"));
AggregatedPage<SkuInfo> aggregatedPage = esTemplate.queryForPage(builder.build(), SkuInfo.class);
// 获取分类分组数据
if (searchMap == null || StringUtils.isEmpty(searchMap.get("category"))) {
StringTerms categoryTerms = aggregatedPage.getAggregations().get("skuCategory");
List<String> categoryList = getGroupList(categoryTerms);
groupMapResult.put("categoryList", categoryList);
}
// 获取品牌分组数据
if (searchMap == null || StringUtils.isEmpty(searchMap.get("brand"))) {
StringTerms brandTerms = aggregatedPage.getAggregations().get("skuBrand");
List<String> brandList = getGroupList(brandTerms);
groupMapResult.put("brandList", brandList);
}
// 获取规格分组集合数据
StringTerms specTerms = aggregatedPage.getAggregations().get("skuSpec");
List<String> specList = getGroupList(specTerms);
// 实现合并操作
Map<String, Set<String>> specMap = putAllSpec(specList);
groupMapResult.put("specList", specMap);
return groupMapResult;
}
/**
* 获取分组集合数据
*
* @param stringTerms
* @return
*/
private List<String> getGroupList(StringTerms stringTerms) {
List<String> groupList = new ArrayList<>();
for (StringTerms.Bucket bucket : stringTerms.getBuckets()) {
// bucket 为其中一个分类
groupList.add(bucket.getKeyAsString());
}
return groupList;
}
/**
* 基本搜索条件构造,分类品牌规格价格过滤
*
* @param searchMap
* @return
*/
private NativeSearchQueryBuilder buildBasicQuery(Map<String, String> searchMap) {
// 创建查询对象的构件对象, 用于封装各种搜索条件
NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder();
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
if (searchMap != null && searchMap.size() > 0) {
// 获取关键字
String keywords = searchMap.get("keywords");
// 如果关键词不为空, 则搜索关键词数据
if (!StringUtils.isEmpty(keywords)) {
// 设置查询条件
boolQueryBuilder.must(QueryBuilders.queryStringQuery(keywords).field("name"));
}
// 输入了分类过滤 -> category
if (!StringUtils.isEmpty(searchMap.get("category"))) {
// 设置查询条件
boolQueryBuilder.must(QueryBuilders.termQuery("categoryName", searchMap.get("category")));
}
// 输入了品牌过滤 -> brand
if (!StringUtils.isEmpty(searchMap.get("brand"))) {
// 设置查询条件
boolQueryBuilder.must(QueryBuilders.termQuery("brandName", searchMap.get("brand")));
}
// 规格过滤
for (Map.Entry<String, String> entry : searchMap.entrySet()) {
String key = entry.getKey();
// 如果 key 以 spec_ 开始,则表示规格筛选查询
if (key.startsWith("spec_")) {
// 规格条件的值
String value = entry.getValue();
// spec_网络,前 5 个去掉 = 网络
boolQueryBuilder.must(QueryBuilders.termQuery("specMap." + key.substring(5) + ".keyword", value));
}
}
// 价格区间过滤
String price = searchMap.get("price");
if (!StringUtils.isEmpty(price)) {
// 0-500元 500-1000元 1000-5000元 5000元以上
// 去掉中文 元 和 以上
price = price.replace("元", "").replace("以上", "");
// prices[] 根据 - 分割
String[] prices = price.split("-");
// prices[x, y] x 一定不为空,y 有可能为 null
if (prices.length > 0) {
// price > prices[0]
boolQueryBuilder.must(QueryBuilders.rangeQuery("price").gt(Integer.parseInt(prices[0])));
// price <= prices[1]
if (prices.length == 2) {
boolQueryBuilder.must(QueryBuilders.rangeQuery("price").lte(Integer.parseInt(prices[1])));
}
}
}
}
// 排序
String sortField = searchMap.get("sortField");
String sortRule = searchMap.get("sortRule");
if (!StringUtils.isEmpty(sortField) && !StringUtils.isEmpty(sortRule)) {
builder.withSort(
// 指定排序字段fieldName
new FieldSortBuilder(sortField)
// 指定排序规则ASC DESC
.order(SortOrder.valueOf(sortRule)));
}
// 分页,用户如果不传分页参数,则默认第一页
Integer pageNum = covertPage(searchMap);
// 默认查询的数据条数
int size = 30;
//页数减1 请求地址
builder.withPageable(PageRequest.of(pageNum - 1, size));
// 将 boolQueryBuilder 对象填充给 nativeSearchQueryBuilder
builder.withQuery(boolQueryBuilder);
return builder;
}
/**
* 接收前端传入的分页参数
*
* @param searchMap
* @return
*/
private Integer covertPage(Map<String, String> searchMap) {
if (searchMap != null) {
String pageNum = searchMap.get("pageNum");
try {
return Integer.parseInt(pageNum);
} catch (NumberFormatException e) {
}
}
return 1;
}
/**
* 集合搜索,高亮功能
*
* @param builder 查询对象的构件对象
* @return resultMap
*/
private Map<String, Object> searchList(NativeSearchQueryBuilder builder) {
// 高亮配置, 指定高亮字段
HighlightBuilder.Field field = new HighlightBuilder.Field("name");
// 前缀 <em style="color:red;">
field.preTags("<em style=\"color:red;\">");
// 后缀 </em>
field.postTags("</em>");
// 碎片长度: 关键词数据的长度
field.fragmentSize(100);
// 添加高亮
builder.withHighlightFields(field);
// 执行搜索, 返回搜索结果集的封装, SkuInfo.class 为搜索的结果集需要转换的类型
AggregatedPage<SkuInfo> skuPage = esTemplate.queryForPage(
// 搜索条件封装
builder.build(),
// 数据集合要转换的对象
SkuInfo.class,
// 执行搜索后,将结果集封装到该对象中
new SearchResultMapper() {
@Override
public <T> AggregatedPage<T> mapResults(SearchResponse response, Class<T> clazz, Pageable pageable) {
// 存储所有转换后的高亮数据对象
List<T> list = new ArrayList<>();
// 执行查询,获取所有数据->结果集[非高亮数据|高亮数据]
for (SearchHit hit : response.getHits()) {
// 分析结果集数据,获取非高亮数据
SkuInfo skuInfo = JSON.parseObject(hit.getSourceAsString(), SkuInfo.class);
// 分析结果集数据,获取高亮数据->只有某个字段的高亮数据
HighlightField highlightField = hit.getHighlightFields().get("name");
if (highlightField != null && highlightField.getFragments() != null) {
// 高亮数据读取出来
Text[] fragments = highlightField.getFragments();
StringBuffer buffer = new StringBuffer();
for (Text fragment : fragments) {
buffer.append(fragment);
}
// 非高亮数据中指定的字段替换成高亮数据
skuInfo.setName(buffer.toString());
}
// 将高亮数据中指定的字段替换成高亮数据
list.add((T) skuInfo);
}
// 将数据返回
// 参数:List<T> content, Pageable pageable, long total
return new AggregatedPageImpl<T>(list, pageable, response.getHits().getTotalHits());
}
});
// 返回结果
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("rows", skuPage.getContent());
resultMap.put("total", skuPage.getTotalElements());
resultMap.put("totalPages", skuPage.getTotalPages());
// 获取搜索封装信息
NativeSearchQuery query = builder.build();
Pageable pageable = query.getPageable();
int pageSize = pageable.getPageSize();
int pageNumber = pageable.getPageNumber();
// 分页数据
resultMap.put("pageSize", pageSize);
resultMap.put("pageNumber", pageNumber);
return resultMap;
}
/**
* 规格分组查询
*
* @param builder 查询对象的构件对象
* @return brandList
*/
private Map<String, Set<String>> searchSpecList(NativeSearchQueryBuilder builder) {
// 分组查询规格集合
builder.addAggregation(AggregationBuilders.terms("skuSpec").field("spec.keyword").size(10000));
AggregatedPage<SkuInfo> aggregatedPage = esTemplate.queryForPage(builder.build(), SkuInfo.class);
// 获取分组数据
StringTerms stringTerms = aggregatedPage.getAggregations().get("skuSpec");
List<String> specList = new ArrayList<>();
for (StringTerms.Bucket bucket : stringTerms.getBuckets()) {
// bucket 为其中一个规格名字
specList.add(bucket.getKeyAsString());
}
return putAllSpec(specList);
}
/**
* 规格汇总合并
*
* @param specList
* @return
*/
private Map<String, Set<String>> putAllSpec(List<String> specList) {
// 合并后的 Map 对象
Map<String, Set<String>> allSpec = new HashMap<>();
// 1.循环 specList
for (String spec : specList) {
// 2.将每个 JSON 字符串转成 Map
Map<String, String> specMap = JSON.parseObject(spec, Map.class);
// 4.合并流程
for (Map.Entry<String, String> entry : specMap.entrySet()) {
// 4.1取出当前 Map, 并且获取对应的 Key 以及对应 value
String key = entry.getKey();
String value = entry.getValue();
// 4.2将当前循环的数据合并到一个 Map<String, Set<String>> 中
// 从 allSpec 中获取当前规格对应的 Set 集合数据
Set<String> specSet = allSpec.get(key);
if (specSet == null) {
// 如果之前的 allSpec 中没有该规格
specSet = new HashSet<>();
}
specSet.add(value);
allSpec.put(key, specSet);
}
}
return allSpec;
}
/**
* 从数据库导入数据到 ES
*/
@Override
public void importSku() {
// Feign 调用, 查询List<Sku>
Result<List<Sku>> skuListResult = skuFeign.findByStatus("1");
// 将 List<Sku> 转成 List<SkuInfo>
List<SkuInfo> skuInfos = JSON.parseArray(JSON.toJSONString(skuListResult.getData()), SkuInfo.class);
// 调用 Dao 实现数据批量导入
for (SkuInfo skuInfo : skuInfos) {
Map<String, Object> specMap = JSON.parseObject(skuInfo.getSpec());
skuInfo.setSpecMap(specMap);
}
searchEsMapper.saveAll(skuInfos);
}
}
六. MQ
1. 初始化配置
pom.xml
<?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>changgou-service</artifactId>
<groupId>com.changgou</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<description>秒杀微服务</description>
<artifactId>changgou-service-seckill</artifactId>
<dependencies>
<dependency>
<groupId>com.changgou</groupId>
<artifactId>changgou-service-seckill-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- 加入 ampq -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
</dependencies>
</project>
application.yml
server:
port: 18092
spring:
application:
name: seckill
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://192.168.211.132:3306/changgou_seckill?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
username: root
password: 123456
rabbitmq:
host: 192.168.211.132 #mq的服务器地址
username: guest #账号
password: guest #密码
main:
allow-bean-definition-overriding: true
redis:
host: 192.168.211.132
port: 6379
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:7001/eureka
instance:
prefer-ip-address: true
feign:
hystrix:
enabled: true
#hystrix 配置
hystrix:
command:
default:
execution:
timeout:
#如果enabled设置为false,则请求超时交给ribbon控制
enabled: true
isolation:
thread:
timeoutInMilliseconds: 10000
strategy: SEMAPHORE
#位置支付交换机和队列
mq:
pay:
exchange:
seckillorder: exchange.seckillorder
queue:
seckillorder: queue.seckillorder
routing:
seckillkey: queue.seckillorder
SeckillApplication
package com.changgou;
import entity.IdWorker;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import tk.mybatis.spring.annotation.MapperScan;
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
@MapperScan(basePackages = {"com.changgou.seckill.dao"})
@EnableScheduling // 在配置类上使用,开启计划任务的支持(类上) @Scheduled 来申明这是一个任务,包括cron,fixDelay,fixRate等类型(方法上,需先开启计划任务的支持)
@EnableAsync //@EnableAsync通过向Spring引入后置处理器AsyncAnnotationBeanPostProcessor,在bean的创建过程中对bean进行advisor增强,对@Async标识的bean增强异步功能
public class SeckillApplication {
public static void main(String[] args) {
SpringApplication.run(SeckillApplication.class, args);
}
@Bean
public IdWorker idWorker() {
return new IdWorker(1, 1);
}
}
2. 代码实现
2.1. controller
SeckillGoodsController
package com.changgou.seckill.controller;
import com.changgou.seckill.pojo.SeckillGoods;
import com.changgou.seckill.service.SeckillGoodsService;
import com.github.pagehelper.PageInfo;
import entity.DateUtil;
import entity.Result;
import entity.StatusCode;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Date;
import java.util.List;
@RestController
@RequestMapping("/seckill/goods")
@CrossOrigin
public class SeckillGoodsController {
@Autowired
private SeckillGoodsService seckillGoodsService;
/**
* 根据时间和秒杀商品ID查询秒杀商品数据
*
* @param time
* @param id
* @return
*/
@GetMapping("/one")
public Result<SeckillGoods> one(String time, Long id) {
SeckillGoods seckillGoods = seckillGoodsService.one(time, id);
return new Result<SeckillGoods>(true, StatusCode.OK, "查询秒杀商品详情成功!", seckillGoods);
}
/**
* 查询秒杀商品时间菜单
*
* @return
*/
@GetMapping("/menus")
public Result<List<Date>> menus() {
List<Date> dateMenus = DateUtil.getDateMenus();
return new Result<List<Date>>(true, StatusCode.OK, "查询秒杀时间菜单成功!", dateMenus);
}
/**
* 根据时间区间查询秒杀商品频道列表数据
*
* @param time
* @return
*/
@GetMapping("/list")
public Result<List<SeckillGoods>> list(String time) {
List<SeckillGoods> seckillGoods = seckillGoodsService.list(time);
return new Result<List<SeckillGoods>>(true, StatusCode.OK, "秒杀商品列表查询成功!", seckillGoods);
}
/**
* SeckillGoods 分页条件搜索实现
*
* @param seckillGoods
* @param page 当前页
* @param size 每页显示多少条
* @return
*/
@PostMapping("/search/{page}/{size}")
public Result<PageInfo> findPage(@RequestBody(required = false) SeckillGoods seckillGoods, @PathVariable int page, @PathVariable int size) {
// 调用 SeckillGoodsService 实现分页条件查询 SeckillGoods
PageInfo<SeckillGoods> pageInfo = seckillGoodsService.findPage(seckillGoods, page, size);
return new Result(true, StatusCode.OK, "分页条件查询成功!", pageInfo);
}
/**
* SeckillGoods 分页搜索实现
*
* @param page 当前页
* @param size 每页显示多少条
* @return
*/
@GetMapping("/search/{page}/{size}")
public Result<PageInfo> findPage(@PathVariable int page, @PathVariable int size) {
// 调用 SeckillGoodsService 实现分页查询 SeckillGoods
PageInfo<SeckillGoods> pageInfo = seckillGoodsService.findPage(page, size);
return new Result<PageInfo>(true, StatusCode.OK, "分页查询成功!", pageInfo);
}
/**
* 多条件搜索品牌数据
*
* @param seckillGoods
* @return
*/
@PostMapping("/search")
public Result<List<SeckillGoods>> findList(@RequestBody(required = false) SeckillGoods seckillGoods) {
//调用 SeckillGoodsService 实现条件查询 SeckillGoods
List<SeckillGoods> list = seckillGoodsService.findList(seckillGoods);
return new Result<List<SeckillGoods>>(true, StatusCode.OK, "条件查询成功!", list);
}
/**
* 根据 ID 删除品牌数据
*
* @param id
* @return
*/
@DeleteMapping("/{id}")
public Result delete(@PathVariable Long id) {
// 调用 SeckillGoodsService 实现根据主键删除
seckillGoodsService.delete(id);
return new Result(true, StatusCode.OK, "删除成功!");
}
/**
* 修改 SeckillGoods数据
*
* @param seckillGoods
* @param id
* @return
*/
@PutMapping("/{id}")
public Result update(@RequestBody SeckillGoods seckillGoods, @PathVariable Long id) {
// 设置主键值
seckillGoods.setId(id);
// 调用 SeckillGoodsService 实现修改 SeckillGoods
seckillGoodsService.update(seckillGoods);
return new Result(true, StatusCode.OK, "修改成功!");
}
/**
* 新增 SeckillGoods 数据
*
* @param seckillGoods
* @return
*/
@PostMapping
public Result add(@RequestBody SeckillGoods seckillGoods) {
// 调用 SeckillGoodsService 实现添加 SeckillGoods
seckillGoodsService.add(seckillGoods);
return new Result(true, StatusCode.OK, "添加成功!");
}
/**
* 根据 ID 查询 SeckillGoods 数据
*
* @param id
* @return
*/
@GetMapping("/{id}")
public Result<SeckillGoods> findById(@PathVariable Long id) {
// 调用 SeckillGoodsService 实现根据主键查询 SeckillGoods
SeckillGoods seckillGoods = seckillGoodsService.findById(id);
return new Result<SeckillGoods>(true, StatusCode.OK, "ID 查询成功!", seckillGoods);
}
/**
* 查询 SeckillGoods 全部数据
*
* @return
*/
@GetMapping
public Result<List<SeckillGoods>> findAll() {
// 调用 SeckillGoodsService 实现查询所有 SeckillGoods
List<SeckillGoods> list = seckillGoodsService.findAll();
return new Result<List<SeckillGoods>>(true, StatusCode.OK, "查询成功!", list);
}
}
SeckillOrderController
package com.changgou.seckill.controller;
import com.changgou.seckill.service.SeckillOrderService;
import entity.Result;
import entity.SeckillStatus;
import entity.StatusCode;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/seckillOrder")
@CrossOrigin
public class SeckillOrderController {
@Autowired
private SeckillOrderService seckillOrderService;
/**
* 抢单状态查询
*
* @return
*/
@GetMapping("/query")
public Result queryStatus() {
String username = "szitheima";
SeckillStatus seckillStatus = seckillOrderService.queryStatus(username);
// 查询成功
if (seckillStatus != null) {
return new Result(true,StatusCode.OK,"查询状态成功!",seckillStatus);
}
return new Result(false,StatusCode.NOTFOUNDERROR, "抢单失败!");
}
/**
* 添加秒杀订单
*
* @param time
* @param id
* @return
*/
@RequestMapping("/add")
public Result add(String time, Long id) {
String username = "szitheima";
seckillOrderService.add(time, id, username);
return new Result(true, StatusCode.OK, "正在排队...");
}
}
2.2. service.impl
SeckillGoodsServiceImpl
package com.changgou.seckill.service.impl;
import com.changgou.seckill.dao.SeckillGoodsMapper;
import com.changgou.seckill.pojo.SeckillGoods;
import com.changgou.seckill.service.SeckillGoodsService;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import tk.mybatis.mapper.entity.Example;
import java.util.List;
@Service
public class SeckillGoodsServiceImpl implements SeckillGoodsService {
@Autowired
private SeckillGoodsMapper seckillGoodsMapper;
@Autowired
private RedisTemplate redisTemplate;
/**
* 根据时间和秒杀商品ID查询秒杀商品数据
*
* @param time
* @param id
* @return
*/
@Override
public SeckillGoods one(String time, Long id) {
return (SeckillGoods) redisTemplate.boundHashOps("SeckillGoods_" + time).get(id);
}
/**
* 根据时间区间查询秒杀商品频道列表数据
*
* @param time
*/
@Override
public List<SeckillGoods> list(String time) {
return redisTemplate.boundHashOps("SeckillGoods_" + time).values();
}
/**
* SeckillGoods 条件分页查询
*
* @param seckillGoods 查询条件
* @param page 页码
* @param size 页大小
* @return 分页结果
*/
@Override
public PageInfo<SeckillGoods> findPage(SeckillGoods seckillGoods, int page, int size) {
// 分页
PageHelper.startPage(page, size);
// 搜索条件构建
Example example = createExample(seckillGoods);
// 执行搜索
return new PageInfo<SeckillGoods>(seckillGoodsMapper.selectByExample(example));
}
/**
* SeckillGoods 分页查询
*
* @param page 页码
* @param size 页大小
* @return 分页结果
*/
@Override
public PageInfo<SeckillGoods> findPage(int page, int size) {
// 静态分页
PageHelper.startPage(page, size);
// 分页查询
return new PageInfo<SeckillGoods>(seckillGoodsMapper.selectAll());
}
/**
* SeckillGoods 条件查询
*
* @param seckillGoods
* @return
*/
@Override
public List<SeckillGoods> findList(SeckillGoods seckillGoods) {
// 构建查询条件
Example example = createExample(seckillGoods);
// 根据构建的条件查询数据
return seckillGoodsMapper.selectByExample(example);
}
/**
* SeckillGoods 构建查询对象
*
* @param seckillGoods
* @return
*/
public Example createExample(SeckillGoods seckillGoods) {
Example example = new Example(SeckillGoods.class);
Example.Criteria criteria = example.createCriteria();
if (seckillGoods != null) {
//
if (!StringUtils.isEmpty(seckillGoods.getId())) {
criteria.andEqualTo("id", seckillGoods.getId());
}
// spu ID
if (!StringUtils.isEmpty(seckillGoods.getSupId())) {
criteria.andEqualTo("supId", seckillGoods.getSupId());
}
// sku ID
if (!StringUtils.isEmpty(seckillGoods.getSkuId())) {
criteria.andEqualTo("skuId", seckillGoods.getSkuId());
}
// 标题
if (!StringUtils.isEmpty(seckillGoods.getName())) {
criteria.andLike("name", "%" + seckillGoods.getName() + "%");
}
// 商品图片
if (!StringUtils.isEmpty(seckillGoods.getSmallPic())) {
criteria.andEqualTo("smallPic", seckillGoods.getSmallPic());
}
// 原价格
if (!StringUtils.isEmpty(seckillGoods.getPrice())) {
criteria.andEqualTo("price", seckillGoods.getPrice());
}
// 秒杀价格
if (!StringUtils.isEmpty(seckillGoods.getCostPrice())) {
criteria.andEqualTo("costPrice", seckillGoods.getCostPrice());
}
// 添加日期
if (!StringUtils.isEmpty(seckillGoods.getCreateTime())) {
criteria.andEqualTo("createTime", seckillGoods.getCreateTime());
}
// 审核日期
if (!StringUtils.isEmpty(seckillGoods.getCheckTime())) {
criteria.andEqualTo("checkTime", seckillGoods.getCheckTime());
}
// 审核状态,0未审核,1审核通过,2审核不通过
if (!StringUtils.isEmpty(seckillGoods.getStatus())) {
criteria.andEqualTo("status", seckillGoods.getStatus());
}
// 开始时间
if (!StringUtils.isEmpty(seckillGoods.getStartTime())) {
criteria.andEqualTo("startTime", seckillGoods.getStartTime());
}
// 结束时间
if (!StringUtils.isEmpty(seckillGoods.getEndTime())) {
criteria.andEqualTo("endTime", seckillGoods.getEndTime());
}
// 秒杀商品数
if (!StringUtils.isEmpty(seckillGoods.getNum())) {
criteria.andEqualTo("num", seckillGoods.getNum());
}
// 剩余库存数
if (!StringUtils.isEmpty(seckillGoods.getStockCount())) {
criteria.andEqualTo("stockCount", seckillGoods.getStockCount());
}
// 描述
if (!StringUtils.isEmpty(seckillGoods.getIntroduction())) {
criteria.andEqualTo("introduction", seckillGoods.getIntroduction());
}
}
return example;
}
/**
* 删除
*
* @param id
*/
@Override
public void delete(Long id) {
seckillGoodsMapper.deleteByPrimaryKey(id);
}
/**
* 修改 SeckillGoods
*
* @param seckillGoods
*/
@Override
public void update(SeckillGoods seckillGoods) {
seckillGoodsMapper.updateByPrimaryKey(seckillGoods);
}
/**
* 增加 SeckillGoods
*
* @param seckillGoods
*/
@Override
public void add(SeckillGoods seckillGoods) {
seckillGoodsMapper.insert(seckillGoods);
}
/**
* 根据 ID 查询 SeckillGoods
*
* @param id
* @return
*/
@Override
public SeckillGoods findById(Long id) {
return seckillGoodsMapper.selectByPrimaryKey(id);
}
/**
* 查询 SeckillGoods 全部数据
*
* @return
*/
@Override
public List<SeckillGoods> findAll() {
return seckillGoodsMapper.selectAll();
}
}
SeckillOrderServiceImpl
package com.changgou.seckill.service.impl;
import com.changgou.seckill.dao.SeckillGoodsMapper;
import com.changgou.seckill.dao.SeckillOrderMapper;
import com.changgou.seckill.pojo.SeckillGoods;
import com.changgou.seckill.pojo.SeckillOrder;
import com.changgou.seckill.service.SeckillOrderService;
import com.changgou.seckill.task.MultiThreadingCreateOrder;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import entity.SeckillStatus;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import tk.mybatis.mapper.entity.Example;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
@Service
public class SeckillOrderServiceImpl implements SeckillOrderService {
@Autowired
private SeckillOrderMapper seckillOrderMapper;
@Autowired
private MultiThreadingCreateOrder multiThreadingCreateOrder;
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private SeckillGoodsMapper seckillGoodsMapper;
/**
* 删除订单
*
* @param username
*/
@Override
public void deleteOrder(String username) {
// 删除订单
redisTemplate.boundHashOps("SeckillOrder").delete(username);
// 查询用户排队信息
SeckillStatus seckillStatus = (SeckillStatus) redisTemplate.boundHashOps("UserQueueStatus").get(username);
// 删除排队信息
clearUserQueue(username);
// 回滚库存->redis 递增-> redis 不一定有商品
String namespace = "SeckillGoods_" + seckillStatus.getTime();
SeckillGoods seckillGoods = (SeckillGoods) redisTemplate.boundHashOps(namespace).get(seckillStatus.getGoodsId());
// 如果商品为空
if (seckillGoods == null) {
// 数据库中查询
seckillGoods = seckillGoodsMapper.selectByPrimaryKey(seckillStatus.getGoodsId());
// 更新数据库库存
seckillGoods.setStockCount(1); // seckillGoods.setStockCount(seckillGoods.getStockCount()+1)
seckillGoodsMapper.updateByPrimaryKeySelective(seckillGoods);
} else {
seckillGoods.setStockCount(seckillGoods.getStockCount() + 1);
}
redisTemplate.boundHashOps(namespace).put(seckillGoods.getId(), seckillGoods);
// 队列
redisTemplate.boundListOps("SeckillGoodsCountList_" + seckillGoods.getId()).leftPush(seckillGoods.getId());
}
/**
* 修改秒杀订单状态
*
* @param username
* @param transactionid
* @param endtime
*/
@Override
public void updatePayStatus(String username, String transactionid, String endtime) {
// 查询订单
SeckillOrder seckillOrder = (SeckillOrder) redisTemplate.boundHashOps("SeckillOrder").get(username);
if (seckillOrder != null) {
try {
// 修改订单状态信息
seckillOrder.setStatus("1");
seckillOrder.setTransactionId(transactionid);
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMddHHmmss");
Date payTimeInfo = simpleDateFormat.parse(endtime);
seckillOrder.setPayTime(payTimeInfo);
seckillOrderMapper.insertSelective(seckillOrder);
// 删除 redis 中的订单
redisTemplate.boundHashOps("SeckillOrder").delete(username);
// 删除用户排队信息
clearUserQueue(username);
} catch (ParseException e) {
e.printStackTrace();
}
}
}
/**
* 清理用户排队抢单信息
*
* @param username
*/
public void clearUserQueue(String username) {
// 排队标识
redisTemplate.boundHashOps("UserQueueCount").delete(username);
// 排队信息清理
redisTemplate.boundHashOps("UserQueueStatus").delete(username);
}
/**
* 抢单状态查询
*
* @param username
* @return
*/
@Override
public SeckillStatus queryStatus(String username) {
return (SeckillStatus) redisTemplate.boundHashOps("UserQueueStatus").get(username);
}
/**
* 秒杀下单-排队
*
* @param time
* @param id
* @param username
* @return
*/
@Override
public Boolean add(String time, Long id, String username) {
// 记录用户排队次数
Long userQueueCount = redisTemplate.boundHashOps("UserQueueCount").increment(username, 1);
if (userQueueCount > 1) {
throw new RuntimeException("重复排队!");
}
// 创建排队对象
SeckillStatus seckillStatus = new SeckillStatus(username, new Date(), 1, id, time);
// redis 中 list 为队列,有序,SeckillOrderQueue 为用户抢单排队
redisTemplate.boundListOps("SeckillOrderQueue").leftPush(seckillStatus);
// 用户抢单状态->用于查询
redisTemplate.boundHashOps("UserQueueStatus").put(username, seckillStatus);
// 异步执行
multiThreadingCreateOrder.createOrder();
return true;
}
/**
* SeckillOrder 条件分页查询
*
* @param seckillOrder 查询条件
* @param page 页码
* @param size 页大小
* @return 分页结果
*/
@Override
public PageInfo<SeckillOrder> findPage(SeckillOrder seckillOrder, int page, int size) {
// 分页
PageHelper.startPage(page, size);
// 搜索条件构建
Example example = createExample(seckillOrder);
// 执行搜索
return new PageInfo<SeckillOrder>(seckillOrderMapper.selectByExample(example));
}
/**
* SeckillOrder 分页查询
*
* @param page 页码
* @param size 页大小
* @return 分页结果
*/
@Override
public PageInfo<SeckillOrder> findPage(int page, int size) {
// 静态分页
PageHelper.startPage(page, size);
// 分页查询
return new PageInfo<SeckillOrder>(seckillOrderMapper.selectAll());
}
/**
* SeckillOrder 条件查询
*
* @param seckillOrder
* @return
*/
@Override
public List<SeckillOrder> findList(SeckillOrder seckillOrder) {
// 构建查询条件
Example example = createExample(seckillOrder);
// 根据构建的条件查询数据
return seckillOrderMapper.selectByExample(example);
}
/**
* SeckillOrder 构建查询对象
*
* @param seckillOrder
* @return
*/
public Example createExample(SeckillOrder seckillOrder) {
Example example = new Example(SeckillOrder.class);
Example.Criteria criteria = example.createCriteria();
if (seckillOrder != null) {
// 主键
if (!StringUtils.isEmpty(seckillOrder.getId())) {
criteria.andEqualTo("id", seckillOrder.getId());
}
// 秒杀商品ID
if (!StringUtils.isEmpty(seckillOrder.getSeckillId())) {
criteria.andEqualTo("seckillId", seckillOrder.getSeckillId());
}
// 支付金额
if (!StringUtils.isEmpty(seckillOrder.getMoney())) {
criteria.andEqualTo("money", seckillOrder.getMoney());
}
// 用户
if (!StringUtils.isEmpty(seckillOrder.getUserId())) {
criteria.andEqualTo("userId", seckillOrder.getUserId());
}
// 创建时间
if (!StringUtils.isEmpty(seckillOrder.getCreateTime())) {
criteria.andEqualTo("createTime", seckillOrder.getCreateTime());
}
// 支付时间
if (!StringUtils.isEmpty(seckillOrder.getPayTime())) {
criteria.andEqualTo("payTime", seckillOrder.getPayTime());
}
// 状态,0未支付,1已支付
if (!StringUtils.isEmpty(seckillOrder.getStatus())) {
criteria.andEqualTo("status", seckillOrder.getStatus());
}
// 收货人地址
if (!StringUtils.isEmpty(seckillOrder.getReceiverAddress())) {
criteria.andEqualTo("receiverAddress", seckillOrder.getReceiverAddress());
}
// 收货人电话
if (!StringUtils.isEmpty(seckillOrder.getReceiverMobile())) {
criteria.andEqualTo("receiverMobile", seckillOrder.getReceiverMobile());
}
// 收货人
if (!StringUtils.isEmpty(seckillOrder.getReceiver())) {
criteria.andEqualTo("receiver", seckillOrder.getReceiver());
}
// 交易流水
if (!StringUtils.isEmpty(seckillOrder.getTransactionId())) {
criteria.andEqualTo("transactionId", seckillOrder.getTransactionId());
}
}
return example;
}
/**
* 删除
*
* @param id
*/
@Override
public void delete(Long id) {
seckillOrderMapper.deleteByPrimaryKey(id);
}
/**
* 修改 SeckillOrder
*
* @param seckillOrder
*/
@Override
public void update(SeckillOrder seckillOrder) {
seckillOrderMapper.updateByPrimaryKey(seckillOrder);
}
/**
* 根据 ID 查询 SeckillOrder
*
* @param id
* @return
*/
@Override
public SeckillOrder findById(Long id) {
return seckillOrderMapper.selectByPrimaryKey(id);
}
/**
* 查询 SeckillOrder 全部数据
*
* @return
*/
@Override
public List<SeckillOrder> findAll() {
return seckillOrderMapper.selectAll();
}
}
2.3. mq
DelaySeckillMessageListener
package com.changgou.seckill.mq;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.changgou.seckill.service.SeckillOrderService;
import entity.SeckillStatus;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
@Component
@RabbitListener(queues = "seckillQueue")
public class DelaySeckillMessageListener {
@Autowired
private SeckillOrderService seckillOrderService;
@Autowired
private RedisTemplate redisTemplate;
/**
* 消息监听
*
* @param message
*/
@RabbitHandler
public void getMessage(String message) throws Exception {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd HH:mm:ss");
System.out.println("回滚时间:" +simpleDateFormat.format(new Date()));
// 获取用户排队信息
SeckillStatus seckillStatus = JSON.parseObject(message, SeckillStatus.class);
// 如果此时redis中没有用户排队信息,则表明该订单已经处理,如果有用户排队信息,则表示用户尚未完成支付,关闭订单
Object userQueueStatus = redisTemplate.boundHashOps("UserQueueStatus").get(seckillStatus.getUsername());
if (userQueueStatus != null) {
// 关闭微信支付
// 删除订单
seckillOrderService.deleteOrder(seckillStatus.getUsername());
}
}
}
QueueConfig
package com.changgou.seckill.mq;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @description 延时超时队列
*/
@Configuration
public class QueueConfig {
/**
* 延时队列,队列1
*
* @return
*/
@Bean
public Queue delaySeckillQueue() {
return QueueBuilder.durable("delaySeckillQueue")
// 当前队列的消息一旦过期,则进入死信交换机
.withArgument("x-dead-letter-exchange", "seckillExchange")
// 将死信队列的数据路由到队列2
.withArgument("x-dead-letter-routing-key", "seckillQueue")
.build();
}
/**
* 真正监听队列,队列2
*
* @return
*/
@Bean
public Queue seckillQueue() {
return new Queue("seckillQueue");
}
/**
* 秒杀交换机
*
* @return
*/
@Bean
public Exchange seckillExchange() {
return new DirectExchange("seckillExchange");
}
/**
* 队列绑定交换机
*
* @param seckillQueue
* @param seckillExchange
* @return
*/
@Bean
public Binding seckillQueueBindingExchange(Queue seckillQueue, Exchange seckillExchange) {
return BindingBuilder.bind(seckillQueue).to(seckillExchange).with("seckillQueue").noargs();
}
}
SeckillMessageListener
package com.changgou.seckill.mq;
import com.alibaba.fastjson.JSON;
import com.changgou.seckill.service.SeckillOrderService;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Map;
@Component
@RabbitListener(queues = "${mq.pay.queue.seckillorder}")
public class SeckillMessageListener {
@Autowired
private SeckillOrderService seckillOrderService;
/**
* 消息监听
*
* @param message
*/
@RabbitHandler
public void getMessage(String message) throws Exception {
// 将支付信息转成 Map
Map<String, String> resultMap = JSON.parseObject(message, Map.class);
// return_code->通信标识-SUCCESS
String return_code = resultMap.get("return_code");
// out_trade_no->订单号
String outtradeno = resultMap.get("out_trade_no");
// 自定义数据
String attach = resultMap.get("attach");
Map<String, String> attachMap = JSON.parseObject(attach, Map.class);
if (return_code.equals("SUCCESS")) {
// result_code->业务结果-SUCCESS->该订单状态
String result_code = resultMap.get("result_code");
if (result_code.equals("SUCCESS")) {
// 改订单状态
seckillOrderService.updatePayStatus(attachMap.get("username"), resultMap.get("transaction_id"), resultMap.get("time_end"));
} else {
// FAIL->删除订单->回滚库存
seckillOrderService.deleteOrder(attachMap.get("username"));
}
}
}
}
2.4. task
MultiThreadingCreateOrder
package com.changgou.seckill.task;
import com.alibaba.fastjson.JSON;
import com.changgou.seckill.dao.SeckillGoodsMapper;
import com.changgou.seckill.pojo.SeckillGoods;
import com.changgou.seckill.pojo.SeckillOrder;
import entity.IdWorker;
import entity.SeckillStatus;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.text.SimpleDateFormat;
import java.util.Date;
@Component
public class MultiThreadingCreateOrder {
@Autowired
private SeckillGoodsMapper seckillGoodsMapper;
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private IdWorker idWorker;
@Autowired
private RabbitTemplate rabbitTemplate;
//要在SpringBoot中开启多线程,首先在启动类上添加一个注解**@EnableAsync**去开启对异步任务的支持。
//异步抢单
// @Async //声明该方法是个异步任务,另开一个线程去运行
public void createOrder() {
try {
System.out.println("等待一会再下单!");
Thread.sleep(10000);
// 从 redis 队列中获取用户排队信息
SeckillStatus seckillStatus = (SeckillStatus) redisTemplate.boundListOps("SeckillOrderQueue").rightPop();
if (seckillStatus == null) {
return;
}
// 定义要购买商品的信息
String time = seckillStatus.getTime();
Long id = seckillStatus.getGoodsId();
String username = seckillStatus.getUsername();
// 先到 SeckillGoodsCountList_ID 队列中获取商品信息,能获取则可下单
Object sgoods = redisTemplate.boundListOps("SeckillGoodsCountList_" + seckillStatus.getGoodsId()).rightPop();
// 如不能获取,则代表无库存,清理排队信息
if (sgoods == null) {
clearUserQueue(username);
return;
}
// 查询秒杀商品
String namespace = "SeckillGoods_" + time;
SeckillGoods seckillGoods = (SeckillGoods) redisTemplate.boundHashOps(namespace).get(id);
// 判断有没有库存
if (seckillGoods == null || seckillGoods.getStockCount() <= 0) {
throw new RuntimeException("已售罄!");
}
// 创建订单对象
SeckillOrder seckillOrder = new SeckillOrder();
seckillOrder.setId(idWorker.nextId());
seckillOrder.setSeckillId(id);
seckillOrder.setMoney(seckillGoods.getCostPrice());
seckillOrder.setUserId(username);
seckillOrder.setCreateTime(new Date());
seckillOrder.setStatus("0");
// 将订单对象存储起来
redisTemplate.boundHashOps("SeckillOrder").put(username, seckillOrder);
// 库存递减
seckillGoods.setStockCount(seckillGoods.getStockCount() - 1);
Thread.sleep(10000);
// 获取商品对应的队列数量
Long size = redisTemplate.boundListOps("SeckillGoodsCountList_" + seckillStatus.getGoodsId()).size();
//减库存,如果库存没了就从redis中删除,并将库存数据写到MySQL中
//seckillGoods.setStockCount(seckillGoods.getStockCount()-1);
if (size <= 0) {
// 同步数量
seckillGoods.setStockCount(size.intValue());
// 同步数据到 mysql
seckillGoodsMapper.updateByPrimaryKeySelective(seckillGoods);
// 删除 redis 中的数据
redisTemplate.boundHashOps(namespace).delete(id);
} else {
// 同步数据到 redis
redisTemplate.boundHashOps(namespace).put(id, seckillGoods);
}
// 更新下单状态
seckillStatus.setOrderId(seckillOrder.getId());
seckillStatus.setMoney(Float.valueOf(seckillGoods.getCostPrice()));
seckillStatus.setStatus(2);
redisTemplate.boundHashOps("UserQueueStatus").put(username, seckillStatus);
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd HH:mm:ss");
System.out.println("下单时间:"+ simpleDateFormat.format(new Date()));
// 发送消息给延时队列
rabbitTemplate.convertAndSend("delaySeckillQueue", (Object) JSON.toJSONString(seckillStatus), new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
message.getMessageProperties().setExpiration("10000");
return message;
}
});
System.out.println("下单成功!");
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 清理用户排队抢单信息
*
* @param username
*/
public void clearUserQueue(String username) {
// 排队标识
redisTemplate.boundHashOps("UserQueueCount").delete(username);
// 排队信息清理
redisTemplate.boundHashOps("UserQueueStatus").delete(username);
}
}
2.5. timer
SeckillGoodsPushTask
package com.changgou.seckill.timer;
import com.changgou.seckill.dao.SeckillGoodsMapper;
import com.changgou.seckill.pojo.SeckillGoods;
import entity.DateUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import tk.mybatis.mapper.entity.Example;
import java.util.Date;
import java.util.List;
import java.util.Set;
/**
* @description 定时将秒杀商品存入 redis 缓存
*/
@Component
public class SeckillGoodsPushTask {
@Autowired
private SeckillGoodsMapper seckillGoodsMapper;
@Autowired
private RedisTemplate redisTemplate;
/**
* 定时执行
*/
@Scheduled(cron = "0/5 * * * * ?")
public void loadGoodsPushRedis() {
/**
* 1. 查询符合当前参与秒杀的时间菜单
* 2. 秒杀商品库存 > 0
* 3. 审核状态->审核通过
* 4. 时间菜单的开始时间 <= start_time && end_time < 时间菜单的开始时间 + 2小时
*/
// 求时间菜单
List<Date> dateMenus = DateUtil.getDateMenus();
// 循环查询每个时间区间的秒杀商品
for (Date dateMenu : dateMenus) {
// 时间的字符串格式 yyyyMMddHH
String timespace = "SeckillGoods_" + DateUtil.data2str(dateMenu, "yyyyMMddHH");
// 审核状态->审核通过
Example example = new Example(SeckillGoods.class);
Example.Criteria criteria = example.createCriteria();
// status=1
criteria.andEqualTo("status", "1");
// 秒杀商品库存 > 0
criteria.andGreaterThan("stockCount", 0);
// 时间菜单的开始时间 <= start_time && end_time < 时间菜单的开始时间 + 2小时
criteria.andGreaterThanOrEqualTo("startTime", dateMenu);
criteria.andLessThan("endTime", DateUtil.addDateHour(dateMenu, 2));
// 排除已经存到 redis 中的数据
Set keys = redisTemplate.boundHashOps(timespace).keys();
if (keys != null && keys.size() > 0) {
criteria.andNotIn("id", keys);
}
// 查询数据
List<SeckillGoods> seckillGoods = seckillGoodsMapper.selectByExample(example);
// 存入到 redis
for (SeckillGoods seckillGood : seckillGoods) {
System.out.println("商品ID:" + seckillGood.getId() + "---存入到 Redis---" + timespace);
redisTemplate.boundHashOps(timespace).put(seckillGood.getId(), seckillGood);
//因为Redis是单线程的,所以不会出现多个线程同时访问数据出错的情况,这样就可以避免并发超卖的问题。
// 给每个商品做个队列
redisTemplate.boundListOps("SeckillGoodsCountList_" + seckillGood.getId()).leftPushAll(putAllIds(seckillGood.getStockCount(), seckillGood.getId()));
}
}
}
/**
* 获取每个商品的ID集合
*
* @param number
* @param id
* @return
*/
public Long[] putAllIds(Integer number, Long id) {
Long[] ids = new Long[number];
for (int i = 0; i < ids.length; i++) {
ids[i] = id;
}
return ids;
}
}