Commit 863f0490 by giaogiao

Merge branch 'master' of http://119.28.51.83/hewei/Jumeirah into future/sys/userLogin

parents 281bd111 8b5b1790
<?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>
<groupId>io.geekidea.springbootplus</groupId>
<artifactId>parent</artifactId>
<version>2.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>
<artifactId>customer-service</artifactId>
<profiles>
<profile>
<!-- 开发环境 -->
<id>dev</id>
<properties>
<profiles.active>dev</profiles.active>
</properties>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
</profile>
<profile>
<!-- 测试环境 -->
<id>test</id>
<properties>
<profiles.active>test</profiles.active>
</properties>
</profile>
<profile>
<!-- 生产环境 -->
<id>prd</id>
<properties>
<profiles.active>prd</profiles.active>
</properties>
</profile>
</profiles>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.11</version>
</dependency>
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>${project.build.sourceEncoding}</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
\ No newline at end of file
package com.ym.im;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
/**
* @author JJww
*/
@EnableAsync
@MapperScan("com.ym.im.mapper")
@SpringBootApplication
public class CustomerServiceApplication {
public static void main(String[] args) {
SpringApplication.run(CustomerServiceApplication.class, args);
}
}
package com.ym.im.config;
import com.ym.im.entity.MsgBody;
import com.ym.im.exception.IMessageException;
import com.ym.im.util.MessageUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* 全局异常处理
*
* @author 陈俊雄
* @date 2019/5/31
**/
@Slf4j
@Configuration
public class ExceptionConfig {
/**
* REST异常处理类
*/
@RestControllerAdvice
public class RestResponseEntityExceptionHandler {
/**
* 系统级异常处理
*
* @param e e
* @return ResponseEntity
*/
@ExceptionHandler(value = Exception.class)
protected ResponseEntity<MsgBody> exceptionHandle(Exception e, HttpServletRequest request) {
// 错误信息编号:Error number,用于日志定位
final String errornum = UUID.randomUUID().toString();
log.error("Error number: " + errornum, e);
final MsgBody msgBody = new MsgBody<>()
.setCode(HttpStatus.INTERNAL_SERVER_ERROR.value())
.setMessage(MessageUtils.getMsg("error.system", request.getLocale(), errornum));
return new ResponseEntity<>(msgBody, HttpStatus.INTERNAL_SERVER_ERROR);
}
/**
* Request参数异常处理
*
* @return ResponseEntity
*/
@ExceptionHandler(value = HttpMessageNotReadableException.class)
protected ResponseEntity<MsgBody> httpMessageNotReadableExceptionHandle(HttpServletRequest request) {
final MsgBody msgBody = new MsgBody<>()
.setCode(HttpStatus.BAD_REQUEST.value())
.setMessage(MessageUtils.getMsg("error.missing_body", request.getLocale()));
return new ResponseEntity<>(msgBody, HttpStatus.BAD_REQUEST);
}
/**
* 聊天服务自定义异常处理
*
* @param e e
* @return ResponseEntity
*/
@ExceptionHandler(value = IMessageException.class)
protected ResponseEntity<MsgBody> iMessageExceptionHandle(IMessageException e) {
final MsgBody msgBody = new MsgBody<>().setCode(HttpStatus.INTERNAL_SERVER_ERROR.value()).setMessage("error.top_level_error");
return new ResponseEntity<>(msgBody, HttpStatus.INTERNAL_SERVER_ERROR);
}
/**
* JsonBean参数校验异常处理
* {@link javax.validation}
*
* @param e e
* @return ResponseEntity
*/
@ExceptionHandler(value = {MethodArgumentNotValidException.class, BindException.class})
protected ResponseEntity<MsgBody<List<String>>> methodArgumentNotValidExceptionHandle(Exception e, HttpServletRequest request) {
final ArrayList<String> errorMsg = new ArrayList<>();
if (e instanceof MethodArgumentNotValidException) {
final List<ObjectError> allErrors = ((MethodArgumentNotValidException)e).getBindingResult().getAllErrors();
for (ObjectError oe : allErrors) {
errorMsg.add(oe.getDefaultMessage() + "[" + ((FieldError) oe).getField() + " = " + ((FieldError) oe).getRejectedValue() + "]");
}
}
if (e instanceof BindException){
final List<FieldError> allErrors = ((BindException)e).getFieldErrors();
for (FieldError fe : allErrors) {
errorMsg.add(fe.getDefaultMessage() + "[" + fe.getField() + " = " + fe.getRejectedValue() + "]");
}
}
final MsgBody<List<String>> msgBody = new MsgBody<>();
msgBody.setCode(HttpStatus.BAD_REQUEST.value());
msgBody.setMessage(MessageUtils.getMsg("error.bad_parameter", request.getLocale()) + errorMsg.toString());
return new ResponseEntity<>(msgBody, HttpStatus.BAD_REQUEST);
}
}
}
package com.ym.im.config;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.ym.im.util.JsonUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
/**
* @author 陈俊雄
* @date 2019/5/30
**/
@Configuration
public class JacksonConfig {
@Autowired
private ObjectMapper objectMapper;
@PostConstruct
public void init() {
// Date类型对象转时间戳(毫秒数)
objectMapper.enable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
objectMapper.registerModule(getSerializerLong2String());
objectMapper.registerModule(getSerializerDouble2String());
JsonUtils.setObjectMapper(objectMapper);
}
/**
* 将Long类型字段转Json字符串时转换为String类型防止前端Js精度丢失
*
* @return simpleModule
*/
public Module getSerializerLong2String() {
final SimpleModule simpleModule = new SimpleModule();
simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
simpleModule.addSerializer(Long.TYPE, ToStringSerializer.instance);
return simpleModule;
}
/**
* 将Double类型字段转Json字符串时转换为String类型防止前端Js精度丢失
*
* @return simpleModule
*/
public Module getSerializerDouble2String() {
final SimpleModule simpleModule = new SimpleModule();
simpleModule.addSerializer(Double.class, ToStringSerializer.instance);
simpleModule.addSerializer(Double.TYPE, ToStringSerializer.instance);
return simpleModule;
}
}
package com.ym.im.config;
import com.ym.im.util.MessageUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import javax.annotation.PostConstruct;
import java.util.Locale;
/**
* @author 陈俊雄
* @date 2019/5/31
**/
@Configuration
public class LocaleConfig {
@Autowired
private WebMvcProperties webMvcProperties;
@PostConstruct
public void init() {
// 初始化多语言工具类
MessageUtils.setMessageSource(messageSource());
Locale.setDefault(webMvcProperties.getLocale());
}
@Bean
public MessageSource messageSource() {
final ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setDefaultEncoding("UTF-8");
messageSource.setUseCodeAsDefaultMessage(true);
messageSource.setBasenames("static.i18n.error", "static.i18n.message");
return messageSource;
}
/**
* validation注解返回异常消息多语言配置
*
* @return LocalValidatorFactoryBean
*/
@Bean
public LocalValidatorFactoryBean localValidatorFactoryBean() {
LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
bean.setValidationMessageSource(messageSource());
return bean;
}
}
package com.ym.im.config;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.util.concurrent.DefaultEventExecutorGroup;
import io.netty.util.concurrent.EventExecutorGroup;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author JJww
*/
@Configuration
public class NettyConfig {
/**
* 负责TCP连接建立操作 绝对不能阻塞
*
* @return
*/
@Bean(name = "bossGroup")
public NioEventLoopGroup bossGroup() {
return new NioEventLoopGroup();
}
/**
* 负责Socket读写操作 绝对不能阻塞
*
* @return
*/
@Bean(name = "workerGroup")
public NioEventLoopGroup workerGroup() {
return new NioEventLoopGroup();
}
/**
* Handler中出现IO操作(如数据库操作,网络操作)使用这个
*
* @return
*/
@Bean(name = "businessGroup")
public EventExecutorGroup businessGroup() {
return new DefaultEventExecutorGroup(16);
}
}
package com.ym.im.config;
import lombok.Data;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.CustomExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
/**
* @author: JJww
* @Date:2019-05-30
*/
@Data
@Configuration
@ConfigurationProperties(prefix = "spring.rabbitmq")
public class RabbitConfig {
private String delayQueueName;
private String staffOfflineQueueName;
private String exchangeName;
@Bean
public Queue delayQueue() {
return new Queue(delayQueueName);
}
@Bean
public Queue staffOfflineQueue() {
return new Queue(staffOfflineQueueName);
}
/**
* 配置默认的交换机
*/
@Bean
CustomExchange customExchange() {
Map<String, Object> args = new HashMap<>(1);
args.put("x-delayed-type", "direct"); //参数二为类型:必须是x-delayed-message
return new CustomExchange(exchangeName, "x-delayed-message", true, false, args);
}
/**
* 绑定队列到交换器
*/
@Bean
Binding bindingDelayQueue(Queue delayQueue, CustomExchange customExchange) {
return BindingBuilder.bind(delayQueue).to(customExchange).with(delayQueueName).noargs();
}
/**
* 绑定队列到交换器
*/
@Bean
Binding bindingStaffOfflineQueue(Queue staffOfflineQueue, CustomExchange customExchange) {
return BindingBuilder.bind(staffOfflineQueue).to(customExchange).with(staffOfflineQueueName).noargs();
}
}
package com.ym.im.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* 先放放到时候再收拾
*
* @author 陈俊雄
* @date 2019/6/18
**/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<Object, Object> myRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
final ObjectMapper objectMapper = new ObjectMapper();
final RedisSerializer<String> stringSerializer = new StringRedisSerializer();
final GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer(objectMapper);
final RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setKeySerializer(stringSerializer);
template.setValueSerializer(jsonRedisSerializer);
template.setHashKeySerializer(jsonRedisSerializer);
template.setHashValueSerializer(jsonRedisSerializer);
template.setDefaultSerializer(jsonRedisSerializer);
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}
package com.ym.im.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.ParameterBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.schema.ModelRef;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Parameter;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import java.util.ArrayList;
import java.util.List;
/**
* @author: JJww
* @Date:2019-06-13
*/
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket createRestApi() {
//全局header参数
ParameterBuilder authorization = new ParameterBuilder();
ParameterBuilder col = new ParameterBuilder();
List<Parameter> pars = new ArrayList<Parameter>();
authorization.name("Authorization").description("Bearer token").modelRef(new ModelRef("string")).parameterType("header").required(false).build(); //非必传
col.name("col").description("语言type(英文:en,中文:zh").modelRef(new ModelRef("string")).parameterType("header").required(false).build(); //非必穿
pars.add(authorization.build());
pars.add(col.build());
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.ym.im.controller"))
.paths(PathSelectors.any())
.build()
.globalOperationParameters(pars);
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("IM API")
.version("1.0")
.build();
}
}
package com.ym.im.controller;
import com.ym.im.entity.ChatRecord;
import com.ym.im.entity.MsgBody;
import com.ym.im.entity.PullChatRecord;
import com.ym.im.service.ChatRecordService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
import java.util.List;
/**
* @author 陈俊雄
* @date 2019/5/30
**/
@RestController
@RequestMapping("/chatRecord")
@Api(description = "聊天记录")
public class ChatRecordController {
@Autowired
private ChatRecordService chatRecordService;
@GetMapping("/pull")
@ApiOperation(value = "获取聊天记录")
public MsgBody<PullChatRecord> pull(@Valid PullChatRecord pull) {
return chatRecordService.getChatRecord(pull);
}
@PutMapping("/check")
@ApiOperation(value = "确认接收")
public MsgBody<List<ChatRecord>> check(List<ChatRecord> chatRecords) {
return chatRecordService.updateReceiveTime(chatRecords);
}
}
\ No newline at end of file
package com.ym.im.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.ym.im.entity.MsgBody;
import com.ym.im.entity.Session;
import com.ym.im.entity.enums.ResultStatus;
import com.ym.im.entity.model.SessionInfo;
import com.ym.im.service.SessionListService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* @author: JJww
* @Date:2020-10-21
*/
@RestController
@RequestMapping("/session")
@Api(description = "会话列表相关")
public class SessionController {
@Autowired
private SessionListService sessionListService;
@GetMapping("/list")
@ApiOperation(value = "获取用户会话列表")
public MsgBody<List<SessionInfo>> getSessionList(Long userId) {
return sessionListService.getSessionList(userId);
}
@DeleteMapping("/list")
@ApiOperation(value = "删除商户会话")
public MsgBody deleteSessionList(Long userId, Long merchantId) {
sessionListService.remove(new QueryWrapper<Session>().lambda().eq(Session::getUserId, userId).eq(Session::getMerchantId, merchantId));
return new MsgBody<>().setCode(ResultStatus.SUCCESS.getCode());
}
}
package com.ym.im.controller;
import com.ym.im.entity.MsgBody;
import com.ym.im.entity.model.IdModel;
import com.ym.im.service.StaffService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
/**
* @author: JJww
* 客服
* @Date:2019-05-22
*/
@RestController
@RequestMapping("/staff")
@Api(description = "客服相关")
public class StaffController {
@Autowired
private StaffService staffService;
@GetMapping(value = "/getStaffList")
@ApiOperation(value = "获取商户客服信息")
public MsgBody getStaffList(Long merchantId) {
return staffService.getMerchantStaffGroup(merchantId);
}
@PostMapping(value = "/forward")
@ApiOperation(value = "转发")
public MsgBody forward(@Valid @RequestBody IdModel idModel) {
return staffService.forward(idModel);
}
}
package com.ym.im.controller;
import com.ym.im.entity.model.IdModel;
import com.ym.im.entity.MsgBody;
import com.ym.im.service.UserService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
/**
* @author: JJww
* @Date:2019-07-24
*/
@RestController
@RequestMapping("/user")
@Api(description = "用户相关")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/list")
@ApiOperation(value = "获取当前用户列表(ID)")
@ApiImplicitParams({@ApiImplicitParam(name = "staffId", value = "客服ID", required = true, dataType = "Long", paramType = "query")})
public MsgBody getUserId(Long staffId) {
return userService.getUserList(staffId);
}
@DeleteMapping("/list")
@ApiOperation(value = "从列表中删除用户")
public MsgBody deleteUserFromList(@Valid @RequestBody IdModel idModel) {
return userService.deleteUserFromList(idModel);
}
@GetMapping("/checkBinding")
@ApiOperation(value = "查询用户绑定情况")
public MsgBody checkBinding(@Valid @ModelAttribute IdModel idModel) {
return userService.checkBinding(idModel);
}
}
package com.ym.im.core;
/**
* @author: JJww
* 对象解码器
* @Date:2019-05-17
*/
import com.ym.im.entity.ChatRecord;
import com.ym.im.entity.MsgBody;
import com.ym.im.util.JsonUtils;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageDecoder;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.List;
/**
* @author: JJww
* 对象解码器
* @Date:2019-11-21
*/
@Slf4j
@Component
@ChannelHandler.Sharable
public class MessageDecoder extends MessageToMessageDecoder<TextWebSocketFrame> {
@Override
protected void decode(ChannelHandlerContext ctx, TextWebSocketFrame textWebSocketFrame, List out) throws Exception {
final String jsonStr = textWebSocketFrame.text();
MsgBody<ChatRecord> msgBody = new MsgBody<ChatRecord>().setCode(MsgBody.ERROR);
try {
msgBody = JsonUtils.json2Obj(jsonStr, MsgBody.class, ChatRecord.class);
} catch (IOException e) {
log.error("Json转{}异常:\r\n" + "Json原串:{}\r\n" + "===异常栈信息===", "MsgBody.class", jsonStr, e);
msgBody.setCode(MsgBody.ERROR);
msgBody.setMessage("错误的Json格式:" + jsonStr);
ctx.channel().writeAndFlush(msgBody);
return;
}
out.add(msgBody);
}
}
\ No newline at end of file
package com.ym.im.core;
import com.ym.im.entity.MsgBody;
import com.ym.im.util.JsonUtils;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import org.springframework.stereotype.Component;
/**
* @author: JJww
* 对象编码器
* @Date:2019-05-17
*/
@Component
@ChannelHandler.Sharable
public class MessageEncoder extends MessageToByteEncoder<MsgBody> {
@Override
protected void encode(ChannelHandlerContext channelHandlerContext, MsgBody msgBody, ByteBuf byteBuf) throws Exception {
channelHandlerContext.writeAndFlush(new TextWebSocketFrame(JsonUtils.obj2Json(msgBody)));
}
}
package com.ym.im.core;
import com.ym.im.entity.base.NettyConstant;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
/**
* netty服务端启动类
*
* @author: JJww
* @Date:2019-01-15
*/
@Slf4j
@Component
public class NettyServer {
@Value("${netty.port}")
private int port;
@Autowired
@Qualifier("bossGroup")
private NioEventLoopGroup bossGroup;
@Autowired
@Qualifier("workerGroup")
private NioEventLoopGroup workerGroup;
@Autowired
private WebSocketChannelInitializer webSocketChannelInitializer;
@PostConstruct
public void start() throws InterruptedException {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.option(ChannelOption.SO_BACKLOG, NettyConstant.MAX_THREADS);//设置线程数
serverBootstrap.group(bossGroup, workerGroup)//绑定线程池
.channel(NioServerSocketChannel.class)//NioServerSocketChannel基于TCP协议的数据处理
.localAddress(port) // 绑定监听端口
.childOption(ChannelOption.TCP_NODELAY, true)//立即写出
.childOption(ChannelOption.SO_KEEPALIVE, true)//长连接
.childHandler(webSocketChannelInitializer); // 添加处理器
ChannelFuture channelFuture = serverBootstrap.bind(port).sync();
if (channelFuture.isSuccess()) {
log.info("客服服务 启动完毕,IP:{}", port);
}
}
@PreDestroy
public void destroy() {
bossGroup.shutdownGracefully().syncUninterruptibly();
workerGroup.shutdownGracefully().syncUninterruptibly();
log.info("客服服务 已关闭,IP:{}", port);
}
}
package com.ym.im.core;
import com.ym.im.entity.base.NettyConstant;
import com.ym.im.handler.SingleChatHandler;
import com.ym.im.handler.WebSocketHandshakerHandler;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.util.concurrent.EventExecutorGroup;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
/**
* @author: JJww
* @Date:2019-01-21
*/
@Component
public class WebSocketChannelInitializer extends ChannelInitializer<NioSocketChannel> {
@Autowired
private MessageEncoder messageEncoder;
@Autowired
private MessageDecoder messageDecoder;
@Autowired
@Qualifier("businessGroup")
private EventExecutorGroup businessGroup;
@Autowired
private SingleChatHandler singleChatHandler;
@Autowired
private WebSocketHandshakerHandler webSocketHandshakerHandler;
@Override
protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
nioSocketChannel.pipeline()
.addLast(new HttpServerCodec(),
new HttpObjectAggregator(NettyConstant.MAX_FRAME_LENGTH),
webSocketHandshakerHandler,
new WebSocketServerProtocolHandler(NettyConstant.CS),
messageDecoder,
messageEncoder)
.addLast(businessGroup, singleChatHandler);//复杂业务绑定businessGroup
}
}
package com.ym.im.entity;
import com.ym.im.validation.group.ChatRecordReceiveGroup;
import com.ym.im.validation.group.ChatRecordSaveGroup;
import com.ym.im.validation.group.ChatRecordSendGroup;
import com.ym.im.validation.group.StaffSendGroup;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Positive;
import javax.validation.groups.Default;
import java.io.Serializable;
import java.util.Date;
/**
* <p>
* 聊天记录
* </p>
*
* @author 陈俊雄
* @since 2019-05-28
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class ChatRecord implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "以用户为第一人称:发送")
public static final int SEND = 0;
@ApiModelProperty(value = "以用户为第一人称:接收")
public static final int RECEIVE = 1;
@NotNull(message = "{error.chat_id_empty}", groups = {Default.class, ChatRecordSaveGroup.class})
@Positive(message = "{error.chat_id_greater_than_zero}", groups = {Default.class, ChatRecordSaveGroup.class})
private Long id;
@NotNull(message = "{error.user_id_empty}", groups = {Default.class, ChatRecordSaveGroup.class, StaffSendGroup.class})
@Positive(message = "{error.user_id_greater_than_zero}", groups = {Default.class, ChatRecordSaveGroup.class, StaffSendGroup.class})
@ApiModelProperty(value = "用户Id")
private Long userId;
@NotNull(message = "{error.merchant_id_empty}", groups = {Default.class})
@Positive(message = "{error.merchant_id_greater_than_zero}", groups = {Default.class})
@ApiModelProperty(value = "商户ID")
private Long merchantId;
@NotNull(message = "{error.chat_msg_type_empty}", groups = {Default.class, ChatRecordSaveGroup.class, ChatRecordSendGroup.class})
@ApiModelProperty(value = "消息类型:1、聊天信息,2、PDF")
private Integer msgType;
@ApiModelProperty(value = "信息内容:msg_type为聊天信息时为聊天内容,msg_type为PDF时为下载地址")
private String msgInfo;
@Min(value = 0, message = "{error.chat_send_receive}", groups = {Default.class, ChatRecordSaveGroup.class})
@Max(value = 1, message = "{error.chat_send_receive}", groups = {Default.class, ChatRecordSaveGroup.class})
@ApiModelProperty(value = "收或发(以用户为第一人称,0:用户 1:客服)")
private Integer sendReceive;
@ApiModelProperty(value = "重试次数")
private Integer retryCount = 0;
@NotNull(message = "{error.chat_send_time_empty}", groups = {Default.class, ChatRecordSaveGroup.class, ChatRecordSendGroup.class})
@ApiModelProperty(value = "发送时间(前端设置)")
private Date sendTime;
@NotNull(message = "{error.chat_receive_time_empty}", groups = {Default.class, ChatRecordReceiveGroup.class})
@ApiModelProperty(value = "接收时间(前端设置)")
private Date receiveTime;
@NotNull(message = "{error.chat_create_time_empty}", groups = {Default.class, ChatRecordSaveGroup.class})
@ApiModelProperty(value = "创建时间(服务端设置)")
private Date createTime;
@NotNull(message = "{error.chat_modify_time_empty}", groups = {Default.class})
@ApiModelProperty(value = "修改时间(服务端设置)")
private Date modifyTime;
}
/*
* Copyright 2019-2029 geekidea(https://github.com/geekidea)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.ym.im.entity;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.Date;
/**
* JwtToken Redis缓存对象
*
* @author geekidea
* @date 2019-09-30
**/
@Data
@Accessors(chain = true)
public class JwtTokenRedisVo implements Serializable {
private static final long serialVersionUID = 1831633309466775223L;
/**
* 客户端类型
*/
private String type;
/**
* mcId
*/
private Long mcId;
/**
* 登录ip
*/
private String host;
/**
* 登录用户ID
*/
private Long userId;
/**
* 登录用户名称
*/
private String username;
/**
* 登录盐值
*/
private String salt;
/**
* 登录token
*/
private String token;
/**
* 创建时间
*/
private Date createDate;
/**
* 多长时间过期,默认一小时
*/
private long expireSecond;
/**
* 过期日期
*/
private Date expireDate;
}
package com.ym.im.entity;
import com.ym.im.validation.group.MsgBodyGroup;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
/**
* @author: JJww
* @Date:2019-05-17
*/
@Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
public class MsgBody<T> implements Serializable {
public static final int LOGOUT = -3;
public static final int FORCEDOFFLINE = -2;
public static final int ERROR = -1;
public static final int SEND_MSG = 1;
public static final int CHECK_MSG = 2;
public static final int HAVE_READ = 3;
public static final int USERS_ONLINE = 4;
public static final int ORDER = 5;
public static final int DISTRIBUTION_STAFF = 6;
public static final int BINDINGFAILURE = 7;
@NotNull(message = "{error.msg_body_status_empty}", groups = MsgBodyGroup.class)
@ApiModelProperty(value = " * 操作类型说明\n" +
" * -3、登出\n" +
" * -2、被迫下线\n" +
" * -1、错误\n" +
" * 1、发送聊天消息\n" +
" * 2、确认接收聊天消息\n" +
" * 3、已读\n" +
" * 4、用户上线\n" +
" * 5、订单" +
" * 6、分配客服" +
" * 7、绑定失败"
)
private Integer code;
@Valid
@NotNull(message = "{error.msg_body_data_empty}", groups = MsgBodyGroup.class)
@ApiModelProperty(value = "type值不同时data对应不同对象:1、发送聊天消息{@link ChatRecord} 2、确认接收聊天消息{@link ChatRecord}")
private T data;
@ApiModelProperty(value = "消息描述")
private String message;
}
\ No newline at end of file
package com.ym.im.entity;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Positive;
import java.io.Serializable;
import java.util.List;
/**
* @author 陈俊雄
* @date 2019/5/28
**/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class PullChatRecord implements Serializable {
@NotNull(message = "{error.user_id_empty}")
@Positive(message = "{error.user_id_greater_than_zero}")
@ApiModelProperty(value = "用户Id")
private Long userId;
@NotNull(message = "{error.merchant_id_empty}")
@Positive(message = "{error.merchant_id_greater_than_zero}")
@ApiModelProperty(value = "商户Id")
private Long merchantId;
@Positive(message = "{error.chat_id_greater_than_zero}")
@ApiModelProperty(value = "聊天记录Id,为获取坐标")
private Long chatRecordId;
@Pattern(regexp = "(?i)forward|backward", message = "{error.chat_id_direction}")
@ApiModelProperty(value = " 聊天记录Id,为获取坐标\n" +
" * 小于当前聊天记录Id:往前(forward)\n" +
" * 大于当前聊天记录Id:往后(backward)")
private String direction;
@Positive(message = "{error.chat_page_num}")
@ApiModelProperty(value = "分页页数")
private Integer pageNum = 1;
@Positive(message = "{error.chat_page_size}")
@ApiModelProperty(value = "分页大小")
private Integer pageSize = 20;
@ApiModelProperty(value = "总条数")
private Long total;
@ApiModelProperty(value = "总页数")
private Integer pages;
@NotEmpty(message = "{error.chat_order_by_empty}")
@ApiModelProperty(value = "聊天记录Id,为获取坐标的查询结果根据某字段做排序")
private String orderBy = "id";
@Pattern(regexp = "(?i)asc|desc", message = "{error.chat_sorting}")
@ApiModelProperty(value = "升序:asc 降序:desc")
private String sorting = "desc";
@ApiModelProperty(value = "查询结果")
private List<ChatRecord> chatRecords;
}
\ No newline at end of file
package com.ym.im.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.ym.im.validation.group.ChatRecordSaveGroup;
import com.ym.im.validation.group.StaffSendGroup;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Positive;
import javax.validation.groups.Default;
import java.io.Serializable;
import java.util.Date;
/**
* @author: JJww
* @Date:2020/10/21
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("session_list")
public class Session implements Serializable {
@NotNull(message = "{error.chat_id_empty}", groups = {Default.class, ChatRecordSaveGroup.class})
@Positive(message = "{error.chat_id_greater_than_zero}", groups = {Default.class, ChatRecordSaveGroup.class})
private Long id;
@NotNull(message = "{error.user_id_empty}", groups = {Default.class, ChatRecordSaveGroup.class, StaffSendGroup.class})
@Positive(message = "{error.user_id_greater_than_zero}", groups = {Default.class, ChatRecordSaveGroup.class, StaffSendGroup.class})
@ApiModelProperty(value = "用户Id")
private Long userId;
@NotNull(message = "{error.merchant_id_empty}", groups = {Default.class})
@Positive(message = "{error.merchant_id_greater_than_zero}", groups = {Default.class})
@ApiModelProperty(value = "商户ID")
private Long merchantId;
@NotNull(message = "{error.chat_create_time_empty}", groups = {Default.class, ChatRecordSaveGroup.class})
@ApiModelProperty(value = "创建时间")
private Date createTime;
@NotNull(message = "{error.chat_modify_time_empty}", groups = {Default.class})
@ApiModelProperty(value = "修改时间)")
private Date modifyTime;
}
package com.ym.im.entity;
import com.ym.im.entity.base.BaseSocketInfo;
import io.netty.channel.socket.nio.NioSocketChannel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.util.HashSet;
import java.util.Set;
/**
* @author: JJww
* 客服
* @Date:2019-05-21
*/
@Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
public class StaffSocketInfo extends BaseSocketInfo {
private Long staffId;
private Set<Long> userIds;
private Long merchantId;
public Set<Long> getUserIds() {
return userIds != null ? userIds : new HashSet<Long>();
}
public void setUserIds(Set<Long> userIds) {
this.userIds = userIds;
}
public StaffSocketInfo() {
}
public StaffSocketInfo(Long staffId, Set<Long> userIds) {
this.staffId = staffId;
this.userIds = userIds;
}
public StaffSocketInfo(Long staffId, NioSocketChannel channel, Set<Long> userIds) {
this.staffId = staffId;
this.channel = channel;
this.userIds = userIds;
}
}
package com.ym.im.entity;
import com.ym.im.entity.base.BaseSocketInfo;
import com.ym.im.entity.model.PushToken;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* @author: JJww
* @Date:2019-05-21
*/
@Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
public class UserSocketInfo extends BaseSocketInfo {
private Long userId;
private Map<Long, Long> staffIds = new HashMap<>();
private Set<Long> sessionList;
private PushToken pushToken;
private String col;
public Long getStaffId(Long merchantId) {
return staffIds.get(merchantId);
}
public void setStaff(Long merchantId, Long staffId) {
staffIds.put(merchantId, staffId);
}
}
package com.ym.im.entity.base;
import io.netty.channel.socket.nio.NioSocketChannel;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
/**
* @author: JJww
* @Date:2019-07-17
*/
@Data
@Accessors(chain = true)
public class BaseSocketInfo implements Serializable {
private static final long serialVersionUID = 1L;
public String token;
public NioSocketChannel channel;
public void writeAndFlush(Object object) {
channel.writeAndFlush(object);
}
public void close() {
channel.close();
}
}
package com.ym.im.entity.base;
import io.netty.util.AttributeKey;
/**
* @author: JJww
* @Date:2020/10/13
*/
public class ChannelAttributeKey {
/**
* 角色 用户ID
*/
public static final AttributeKey<Long> ROLE_ID = AttributeKey.valueOf("role_Id");
/**
* 商户ID
*/
public static final AttributeKey<Long> MERCHANT_ID = AttributeKey.valueOf("merchant_Id");
/**
* 角色类型
*/
public static final AttributeKey<String> ROLE_TYPE = AttributeKey.valueOf("role_type");
/**
* 当前token
*/
public static final AttributeKey<String> TOKEN_INFO = AttributeKey.valueOf("token");
/**
* 客户端当前语言
*/
public static final AttributeKey<String> COL_INFO = AttributeKey.valueOf("col");
}
package com.ym.im.entity.base;
/**
* @author: JJww
* @Date:2019-05-17
*/
public class NettyConstant {
/**
* websocket标识
*/
public static final String CS = "/cs";
/**
* 最大线程量
*/
public static final Integer MAX_THREADS = 1024;
/**
* 数据包最大长度
*/
public static final Integer MAX_FRAME_LENGTH = 65535;
/**
* redis 聊天记录KEY
*/
public static final String MSG_KEY = "MSG: ";
/**
* 客服socket 绑定的用户列表
*/
public static final String STAFF_USERIDS_KEY = "staff.userIds: ";
/**
* 用户socket 相关信息
*/
public static final String IM_USERS = "im.users";
/**
* 重发次数
*/
public static final Integer RETRY_COUNT = 3;
}
package com.ym.im.entity;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 语言类型枚举类
*
* @author hjp
* @Description
* @date 2019/5/16
*/
@Getter
@AllArgsConstructor
public enum LanguageEnum {
en("en", "英文"),
zh("zh", "中文");
private final String key;
private final String remark;
}
package com.ym.im.entity.enums;
/**
* 自定义请求状态码
*
* @author JJww
*/
public enum ResultStatus {
/**
* 成功
*/
SUCCESS(200, "Success", "成功"),
PRIVILEGE_IS_ERROR(401, "Unauthorized", "未授权限,请联系管理员"),
THERE_IS_NO_ONLINE_CUSTOMER_SERVICE(10000, "There is no online customer service", "当前没有客服在线"),
MOBILE_NO_REGISTER(20000, "The mobile phone number is registered", "手机号未注册"),
MOBILE_REGISTER(20001, "Mobile phone number has been registered", "手机号已注册"),
USER_INFO_NO_EXIST(20003, "User information does not exist", "用户信息不存在"),
REPEAT_COMMIT(20007, "your commit is repeat", "重复提交"),
FORWARD_FAILURE(40004, "Failed to forward, customer service or user offline", "转发失败,客服或用户已下线"),
CHECK_FAILURE(40005, "This user has binding other staff", "此用户已经绑定其他客服"),
PARAM_ERROR(9999, "Parameters error", "参数有误"),
SYS_BUSY(-9998, "The system is busy, please try again later!", "系统繁忙,请稍候再试!"),
SYS_ERROR(-9999, "System error", "系统错误!"),
REQUEST_ERROR(-9997, "Request error", "请求有误!"),
FORBIDDEN_ERROR(-9996, "forbidden error", "用户已被封禁");
/**
* 返回码
*/
private int code;
/**
* 默认返回英文结果描述 en
*/
private String message;
/**
* 返回中文结果描述 zh_simple
*/
private String zhMessage;
ResultStatus(int code, String message, String zhMessage) {
this.code = code;
this.message = message;
this.zhMessage = zhMessage;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String getZhMessage() {
return zhMessage;
}
public void setZhMessage(String zhMessage) {
this.zhMessage = zhMessage;
}
}
package com.ym.im.entity.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author JJww
*/
@Getter
@AllArgsConstructor
public enum RoleEnum {
APP("app", 0),
merchant("mer", 1),
system("sys", 2);
private final String type;
private final Integer key;
public static RoleEnum get(String type) {
for (RoleEnum roleEnum : values()) {
if (roleEnum.getType().equals(type)) {
//获取指定的枚举
return roleEnum;
}
}
return null;
}
public static RoleEnum get(Integer key) {
for (RoleEnum roleEnum : values()) {
if (roleEnum.getKey().equals(key)) {
//获取指定的枚举
return roleEnum;
}
}
return null;
}
}
package com.ym.im.entity.model;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.experimental.Accessors;
import javax.validation.constraints.NotNull;
/**
* @author: JJww
* @Date:2019-06-27
*/
@Data
@Accessors(chain = true)
public class IdModel {
@ApiModelProperty(value = "客服Id")
@NotNull(message = "{error.staffId_empty}")
private Long staffId;
@ApiModelProperty(value = "用户Id")
@NotNull(message = "{error.userId_empty}")
private Long userId;
@ApiModelProperty(value = "商户Id")
@NotNull(message = "{error.merchantId_empty}")
private Long merchantId;
}
package com.ym.im.entity.model;
import lombok.Data;
import java.io.Serializable;
/**
* @author: JJww
* @Date:2019-07-19
*/
@Data
public class OrderModel implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 订单类型
*/
private String type;
/**
* 订单ID
*/
private String orderId;
/**
* 订单状态
*/
private String orderStatus;
/**
* 用户Id
*/
private String userId;
private Long merchantId;
}
package com.ym.im.entity.model;
import lombok.Data;
/**
* @author xjx
* @Description
* @date 2019/11/18
*/
@Data
public class PushToken {
private String pushToken;
private String pushType;
}
package com.ym.im.entity.model;
import com.ym.im.entity.Session;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* @author: JJww
* @Date:2020/10/22
*/
@Data
@Accessors(chain = true)
public class SessionInfo extends Session {
private Long recordId;
private String latestRecord;
}
package com.ym.im.exception;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.util.CharsetUtil;
import lombok.extern.slf4j.Slf4j;
/**
* @author: JJww
* @Date:2020/6/3
*/
@Slf4j
public class HttpException extends RuntimeException {
public HttpException(ChannelHandlerContext ctx, FullHttpRequest request, FullHttpResponse response) {
super(response.toString());
sendHttpResponse(ctx, request, response);
log.error("HttpException: " + response.toString());
}
private static void sendHttpResponse(ChannelHandlerContext ctx, FullHttpRequest request, FullHttpResponse response) {
if (response.status().code() != HttpResponseStatus.OK.code()) {
ByteBuf buf = Unpooled.copiedBuffer(response.status().toString(), CharsetUtil.UTF_8);
response.content().writeBytes(buf);
buf.release();
response.headers();
HttpUtil.setContentLength(response, response.content().readableBytes());
}
ChannelFuture f = ctx.channel().writeAndFlush(response);
if (!HttpUtil.isKeepAlive(request) || response.status().code() != HttpResponseStatus.OK.code()) {
f.addListener(ChannelFutureListener.CLOSE);
}
}
}
package com.ym.im.exception;
import com.ym.im.entity.MsgBody;
import com.ym.im.util.MessageUtils;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Locale;
import java.util.UUID;
/**
* @author 陈俊雄
* @date 2019/5/31
**/
@Data
@EqualsAndHashCode(callSuper = true)
public class IMessageException extends RuntimeException {
private MsgBody msgBody;
private String i18nError;
private String errorNum = UUID.randomUUID().toString();
public IMessageException(String i18nError) {
super(MessageUtils.getMsg(i18nError, Locale.CHINA));
this.i18nError = i18nError;
this.msgBody = new MsgBody()
.setCode(MsgBody.ERROR)
.setMessage(MessageUtils.getMsg(i18nError));
}
}
package com.ym.im.factory;
import com.ym.im.entity.enums.RoleEnum;
import com.ym.im.service.ChatService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author: JJww
* @Date:2019-05-23
*/
@Component
public class SingleChatFactory {
@Autowired
private ChatService userSingleChatServiceImpl;
@Autowired
private ChatService staffSingleChatServiceImpl;
/**
* 创建对应实现类
*
* @param type
* @return
*/
public ChatService getService(String type) {
switch (RoleEnum.get(type)) {
case APP:
return userSingleChatServiceImpl;
case merchant:
return staffSingleChatServiceImpl;
default:
}
return null;
}
}
package com.ym.im.handler;
import com.ym.im.entity.base.ChannelAttributeKey;
import com.ym.im.factory.SingleChatFactory;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author: JJww
* @Date:2019/11/14
*/
@Component
@ChannelHandler.Sharable
public abstract class BaseHandler<T> extends SimpleChannelInboundHandler<T> {
@Autowired
private SingleChatFactory singleChatFactory;
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
try {
singleChatFactory.getService(ctx.channel().attr(ChannelAttributeKey.ROLE_TYPE).get()).offline(ctx);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
try {
singleChatFactory.getService(ctx.channel().attr(ChannelAttributeKey.ROLE_TYPE).get()).offline(ctx);
} catch (Exception e) {
e.printStackTrace();
}
}
}
package com.ym.im.handler;
import com.ym.im.entity.StaffSocketInfo;
import com.ym.im.entity.UserSocketInfo;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author: JJww
* @Date:2020/10/14
*/
@Component
public class ChannelGroupHandler {
/**
* 在线用户Group
*/
public final Map<Long, UserSocketInfo> USER_GROUP = new ConcurrentHashMap<>();
/**
* 在线客服Group
*/
public final Map<Long, Map<Long, StaffSocketInfo>> STAFF_GROUP = new ConcurrentHashMap<>();
/**
* 新增商户 客服
*
* @param merchantId
* @param staffSocketInfo
*/
public void putMerchantStaff(Long merchantId, StaffSocketInfo staffSocketInfo) {
final Map<Long, StaffSocketInfo> staffSocketInfoMap = STAFF_GROUP.get(merchantId) != null ? STAFF_GROUP.get(merchantId) : new HashMap<Long, StaffSocketInfo>();
staffSocketInfoMap.put(staffSocketInfo.getStaffId(), staffSocketInfo);
STAFF_GROUP.put(merchantId, staffSocketInfoMap);
}
/**
* 移除商户 客服
*
* @param merchantId
* @param staffId
*/
public void removeMerchantStaff(Long staffId) {
StaffSocketInfo staffSocketInfo = null;
for (Map<Long, StaffSocketInfo> staffGroup : STAFF_GROUP.values()) {
StaffSocketInfo staffInfo = staffGroup.get(staffId);
if (staffInfo != null) {
staffGroup.remove(staffId);
break;
}
}
}
public StaffSocketInfo getMerchantStaff(Long merchantId, Long staffId) {
return STAFF_GROUP.get(merchantId) != null ? STAFF_GROUP.get(merchantId).get(staffId) : null;
}
public StaffSocketInfo getMerchantStaff(Long staffId) {
StaffSocketInfo staffSocketInfo = null;
for (Map<Long, StaffSocketInfo> staffGroup : STAFF_GROUP.values()) {
StaffSocketInfo staffInfo = staffGroup.get(staffId);
if (staffInfo != null) {
staffSocketInfo = staffInfo;
break;
}
}
return staffSocketInfo;
}
}
\ No newline at end of file
package com.ym.im.handler;
import com.ym.im.entity.ChatRecord;
import com.ym.im.entity.MsgBody;
import com.ym.im.exception.IMessageException;
import com.ym.im.service.MsgBodyService;
import com.ym.im.util.MessageUtils;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.validator.internal.engine.path.PathImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.util.ArrayList;
/**
* @author: JJww
* 单聊
* @Date:2019-05-17
*/
@Slf4j
@Component
@ChannelHandler.Sharable
public class SingleChatHandler extends BaseHandler<MsgBody<ChatRecord>> {
@Autowired
private MsgBodyService msgBodyService;
@Override
protected void channelRead0(ChannelHandlerContext ctx, MsgBody<ChatRecord> msgBody) throws Exception {
try {
msgBodyService.msgBodyHandle(ctx, msgBody);
} catch (ConstraintViolationException e) {
log.error(e.getMessage());
ConstraintViolation<?> next;
final ArrayList<String> errorMsg = new ArrayList<>();
for (ConstraintViolation<?> constraintViolation : e.getConstraintViolations()) {
next = constraintViolation;
errorMsg.add(next.getMessage() + " " + ((PathImpl) next.getPropertyPath()).getLeafNode().asString() + " = " + next.getInvalidValue());
}
msgBody.setCode(MsgBody.ERROR);
msgBody.setMessage(errorMsg.toString());
ctx.channel().writeAndFlush(msgBody);
} catch (IMessageException e) {
msgBody.setCode(MsgBody.ERROR);
msgBody.setMessage(MessageUtils.getMsg(e.getI18nError()));
ctx.channel().writeAndFlush(msgBody);
} catch (Exception e) {
e.printStackTrace();
}
}
}
package com.ym.im.handler;
import com.ym.im.entity.JwtTokenRedisVo;
import com.ym.im.entity.MsgBody;
import com.ym.im.entity.base.BaseSocketInfo;
import com.ym.im.entity.base.ChannelAttributeKey;
import com.ym.im.entity.base.NettyConstant;
import com.ym.im.entity.enums.RoleEnum;
import com.ym.im.exception.HttpException;
import com.ym.im.factory.SingleChatFactory;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.LinkedHashMap;
import java.util.Optional;
import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
import static io.netty.handler.codec.http.HttpResponseStatus.UNAUTHORIZED;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
/**
* @author: JJww
* @Date:2020/10/10
*/
@Slf4j
@Component
@ChannelHandler.Sharable
public class WebSocketHandshakerHandler extends BaseHandler<FullHttpRequest> {
@Autowired
private ChannelGroupHandler channelGroup;
@Autowired
private SingleChatFactory singleChatFactory;
@Resource(name = "myRedisTemplate")
private RedisTemplate redisTemplate;
public static final String AUTHORIZATION = "Authorization";
/**
* 登录用户token信息key
* login:token:tokenMd5
*/
public static final String LOGIN_TOKEN = "login:token:%s";
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest fullHttpRequest) throws Exception {
final String token = fullHttpRequest.headers().get(AUTHORIZATION);
Optional.ofNullable(token).orElseThrow(() -> new HttpException(ctx, fullHttpRequest, new DefaultFullHttpResponse(HTTP_1_1, UNAUTHORIZED)));
final JwtTokenRedisVo tokenInfoForRedis = this.getTokenInfoForRedis(token);
Optional.ofNullable(tokenInfoForRedis).orElseThrow(() -> new HttpException(ctx, fullHttpRequest, new DefaultFullHttpResponse(HTTP_1_1, UNAUTHORIZED)));
final String roleType = tokenInfoForRedis.getType();
if (RoleEnum.merchant.getType().equals(roleType)) {
final Long merchantId = tokenInfoForRedis.getMcId();
Optional.ofNullable(merchantId).orElseThrow(() -> new HttpException(ctx, fullHttpRequest, new DefaultFullHttpResponse(HTTP_1_1, BAD_REQUEST)));
ctx.channel().attr(ChannelAttributeKey.MERCHANT_ID).set(merchantId);
}
final Long userId = tokenInfoForRedis.getUserId();
ctx.channel().attr(ChannelAttributeKey.ROLE_ID).set(userId);
ctx.channel().attr(ChannelAttributeKey.TOKEN_INFO).set(token);
ctx.channel().attr(ChannelAttributeKey.ROLE_TYPE).set(roleType);
this.sso(token, userId, roleType);
singleChatFactory.getService(roleType).init(ctx);
fullHttpRequest.setUri(NettyConstant.CS);
ctx.fireChannelRead(fullHttpRequest.retain());
}
private void sso(String token, Long roleId, String type) {
BaseSocketInfo baseSocketInfo = null;
switch (RoleEnum.get(type)) {
case APP:
baseSocketInfo = channelGroup.USER_GROUP.get(roleId);
channelGroup.USER_GROUP.remove(roleId);
break;
case merchant:
baseSocketInfo = channelGroup.getMerchantStaff(roleId);
channelGroup.removeMerchantStaff(roleId);
break;
default:
}
if (baseSocketInfo != null && !token.equals(baseSocketInfo.getToken())) {
baseSocketInfo.writeAndFlush(new MsgBody<>().setCode(MsgBody.FORCEDOFFLINE));
baseSocketInfo.close();
log.info("用户: " + roleId + " 被迫下线");
}
}
public JwtTokenRedisVo getTokenInfoForRedis(String token) {
final LinkedHashMap jwtTokenInfo = (LinkedHashMap) redisTemplate.opsForValue().get(String.format(LOGIN_TOKEN, DigestUtils.md5Hex(token)));
if (jwtTokenInfo == null) {
return null;
}
//@class 对象路径不一致 重新set
final String type = jwtTokenInfo.get("type").toString();
return new JwtTokenRedisVo()
.setUserId(Long.valueOf(jwtTokenInfo.get("userId").toString()))
.setType(type).setMcId(RoleEnum.merchant.getType().equals(type) ? Long.valueOf(jwtTokenInfo.get("mcId").toString()) : null);
}
}
package com.ym.im.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ym.im.entity.ChatRecord;
import com.ym.im.entity.PullChatRecord;
import com.ym.im.entity.Session;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* <p>
* 聊天记录 Mapper 接口
* </p>
*
* @author 陈俊雄
* @since 2019-05-28
*/
public interface ChatRecordMapper extends BaseMapper<ChatRecord> {
int insert(@Param("chat") ChatRecord chatRecord, @Param("index") int index);
int insertSelective(@Param("chat") ChatRecord chatRecord, @Param("index") int index);
int updateById(@Param("chat") ChatRecord chatRecord, @Param("index") int index);
int updateByIdSelective(@Param("chat") ChatRecord chatRecord, @Param("index") int index);
int updateBatchReceiveTimeById(@Param("chats") List<ChatRecord> chats, @Param("index") int index);
int updateReceiveTime(@Param("chat") ChatRecord chatRecord, @Param("index") int index);
ChatRecord selectById(@Param("id") Long id, @Param("index") int index);
List<ChatRecord> getChatRecord(@Param("pull") PullChatRecord pull, @Param("index") int index);
ChatRecord getLatestRecord(@Param("userId") Long userId, @Param("merchantId") Long merchantId, @Param("index") int index);
}
package com.ym.im.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ym.im.entity.Session;
/**
* @author: JJww
* @Date:2020/10/21
*/
public interface SessionListMapper extends BaseMapper<Session> {
}
package com.ym.im.mq;
import com.ym.im.config.RabbitConfig;
import com.ym.im.entity.MsgBody;
import com.ym.im.entity.StaffSocketInfo;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author: JJww
* @Date:2019-05-30
*/
@Component
public class Queue {
@Autowired
private AmqpTemplate rabbitTemplate;
@Autowired
private RabbitConfig rabbitConfig;
/**
* 延迟队列 重发未回应消息
*
* @param msgBody
*/
public void delaysQueue(MsgBody msgBody) {
// 通过广播模式发布延时消息 延时3秒 消费后销毁 这里无需指定路由,会广播至每个绑定此交换机的队列
rabbitTemplate.convertAndSend(rabbitConfig.getExchangeName(), rabbitConfig.getDelayQueueName(), msgBody, message -> {
message.getMessageProperties().setDelay(3 * 1000); // 毫秒为单位,指定此消息的延时时长
return message;
});
}
/**
* 客服离线 队列
*
* @param msgBody
*/
public void staffOfflineQueue(StaffSocketInfo staffSocketInfo) {
//客服下线后 转发用户到其他客服
rabbitTemplate.convertAndSend(rabbitConfig.getExchangeName(), rabbitConfig.getStaffOfflineQueueName(), staffSocketInfo, message -> {
message.getMessageProperties().setDelay((60 * 1000)); // 毫秒为单位,指定此消息的延时时长
return message;
});
}
}
\ No newline at end of file
package com.ym.im.mq;
import com.alibaba.fastjson.JSON;
import com.ym.im.entity.ChatRecord;
import com.ym.im.entity.MsgBody;
import com.ym.im.entity.StaffSocketInfo;
import com.ym.im.entity.UserSocketInfo;
import com.ym.im.entity.base.NettyConstant;
import com.ym.im.entity.enums.RoleEnum;
import com.ym.im.entity.model.IdModel;
import com.ym.im.entity.model.OrderModel;
import com.ym.im.handler.ChannelGroupHandler;
import com.ym.im.service.StaffService;
import com.ym.im.util.JsonUtils;
import lombok.extern.slf4j.Slf4j;
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.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Set;
/**
* @author: JJww
* @Date:2019-05-30
*/
@Slf4j
@Component
public class Receiver {
@Autowired
private Queue queue;
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private StaffService staffService;
@Autowired
private ChannelGroupHandler channelGroup;
/**
* 禁用用户 队列名称
*/
public static final String USER_QUEUE_NAME = "disable.user";
/**
* 订单队列
*/
public static final String ORDER_QUEUE_NAME = "push.order";
@RabbitListener(queues = "#{delayQueue.name}")
public void delayAckHandler(MsgBody msgBody) throws IOException {
log.info("接收时间:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + " 消息内容:" + JsonUtils.obj2Json(msgBody));
retry(msgBody);
}
@RabbitListener(queues = "#{staffOfflineQueue.name}")
public void offlineHandler(StaffSocketInfo staffSocketInfo) {
final Long staffId = staffSocketInfo.getStaffId();
//移除用户列表
redisTemplate.delete(NettyConstant.STAFF_USERIDS_KEY + staffId);
//客服真离线后 才转发
if (channelGroup.getMerchantStaff(staffId) == null) {
final Set<Long> userIds = staffSocketInfo.getUserIds();
log.info("客服离线队列: " + "ID: " + "UserIds:" + userIds);
userIds.forEach((Long userId) -> {
//用户在线才重新分配和转发
if (channelGroup.USER_GROUP.get(userId) != null) {
final StaffSocketInfo idleStaff = staffService.getIdleStaff(staffSocketInfo.getMerchantId(), userId);
if (idleStaff != null) {
idleStaff.writeAndFlush(new MsgBody<>().setCode(MsgBody.DISTRIBUTION_STAFF).setData(new IdModel().setStaffId(staffId).setUserId(userId)));
}
}
});
}
}
/**
* 禁用用户后 关闭socket
*
* @param userId
* @throws IOException
*/
@RabbitListener(queues = USER_QUEUE_NAME)
public void disableUserHandler(String userId) {
final UserSocketInfo userSocketInfo = channelGroup.USER_GROUP.get(Long.valueOf(userId));
if (userSocketInfo != null) {
userSocketInfo.writeAndFlush(new MsgBody<>().setCode(MsgBody.LOGOUT));
userSocketInfo.close();
log.info("用户: " + userId + "被禁用");
}
}
/**
* 订单相关处理
*
* @param orderModel
*/
@RabbitListener(queues = ORDER_QUEUE_NAME)
public void orderHandler(OrderModel orderModel) {
log.info("Constants.ORDER_QUEUE_NAME: " + JSON.toJSONString(orderModel));
final UserSocketInfo userSocketInfo = channelGroup.USER_GROUP.get(Long.valueOf(orderModel.getUserId()));
if (userSocketInfo == null) {
return;
}
StaffSocketInfo staffSocketInfo = channelGroup.getMerchantStaff(userSocketInfo.getStaffId(orderModel.getMerchantId()));
final MsgBody<OrderModel> orderInfo = new MsgBody<OrderModel>().setCode(MsgBody.ORDER).setData(orderModel);
/**
* 绑定客服在线,发送订单信息
*/
if (staffSocketInfo != null) {
staffSocketInfo.writeAndFlush(orderInfo);
log.info("客服订单: " + "给客服(" + staffSocketInfo.getStaffId() + ")发送订单:" + orderInfo.toString());
return;
}
/**
* 绑定客服不在线,给历史客服发送订单信息
*/
final Long staffId = (Long) redisTemplate.opsForHash().get(NettyConstant.IM_USERS, orderModel.getUserId());
if (staffId != null) {
log.info("客服订单: " + "尝试给历史客服(" + staffId + ")发送订单:" + orderInfo.toString());
staffSocketInfo = channelGroup.getMerchantStaff(staffId);
if (staffSocketInfo != null) {
staffSocketInfo.writeAndFlush(orderInfo);
log.info("客服订单: " + "给历史客服(" + staffId + ")发送订单:" + orderInfo.toString());
}
}
}
/**
* 重发未回执的消息
*
* @param msgBody
* @throws IOException
*/
public void retry(MsgBody<ChatRecord> msgBody) throws IOException {
final ChatRecord chatRecord = msgBody.getData();
final Long userId = Long.valueOf(chatRecord.getUserId());
final String recordId = String.valueOf(chatRecord.getId());
if (msgBody != null && chatRecord.getRetryCount().intValue() < NettyConstant.RETRY_COUNT.intValue()) {
UserSocketInfo userSocketInfo = channelGroup.USER_GROUP.get(userId);
if (userSocketInfo == null) {
return;
}
MsgBody<ChatRecord> msg = JsonUtils.json2Obj(String.valueOf(redisTemplate.opsForHash().get(NettyConstant.MSG_KEY + userId, recordId)), JsonUtils.getJavaType(MsgBody.class, ChatRecord.class));
if (msg != null && msg.getCode().equals(MsgBody.HAVE_READ)) {
redisTemplate.opsForHash().delete(NettyConstant.MSG_KEY + userId, recordId);
return;
}
final Integer sendReceive = chatRecord.getSendReceive();
switch (RoleEnum.get(sendReceive)) {
case APP:
Long staffId = userSocketInfo.getStaffId(chatRecord.getMerchantId());
StaffSocketInfo staffSocketInfo = channelGroup.getMerchantStaff(staffId);
if (staffSocketInfo != null) {
staffSocketInfo.writeAndFlush(msgBody);
}
break;
case merchant:
userSocketInfo.writeAndFlush(msgBody);
break;
default:
}
//重发三次
chatRecord.setRetryCount(chatRecord.getRetryCount() + 1);
queue.delaysQueue(msgBody);
} else {
//移除失败消息
redisTemplate.opsForHash().delete(NettyConstant.MSG_KEY + userId, recordId);
}
}
}
\ No newline at end of file
package com.ym.im.service;
import com.ym.im.entity.ChatRecord;
import com.ym.im.entity.MsgBody;
import com.ym.im.entity.PullChatRecord;
import com.ym.im.validation.group.ChatRecordSaveGroup;
import org.springframework.validation.annotation.Validated;
import javax.validation.Valid;
import java.util.List;
/**
* <p>
* 聊天记录 服务类
* </p>
*
* @author 陈俊雄
* @since 2019-05-28
*/
@Validated
public interface ChatRecordService {
/**
* 聊天记录表数量
*/
int NUMBER_OF_TABLE = 32;
@Validated({ChatRecordSaveGroup.class})
int insert(ChatRecord chatRecord);
@Validated({ChatRecordSaveGroup.class})
int insertSelective(@Valid ChatRecord chatRecord);
int updateByRecordId(ChatRecord chatRecord);
int updateByIdSelective(ChatRecord chatRecord);
int updateReceiveTime(ChatRecord chatRecord);
ChatRecord selectById(Long id, Long userId);
MsgBody<PullChatRecord> getChatRecord(PullChatRecord pull);
MsgBody<List<ChatRecord>> updateReceiveTime(List<ChatRecord> chats);
ChatRecord getLatestMsg(Long userId, Long merchantId);
}
package com.ym.im.service;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.ym.im.entity.ChatRecord;
import com.ym.im.entity.MsgBody;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.socket.nio.NioSocketChannel;
import javax.validation.Valid;
/**
* @author: JJww
* @Date:2019-05-21
*/
public interface ChatService {
/**
* 连接初始化
*
* @param ctx
* @return
*/
void init(ChannelHandlerContext ctx);
/**
* 发送
*
* @param msgBody
* @throws JsonProcessingException
*/
void send(NioSocketChannel channel, MsgBody<ChatRecord> msgBody) throws JsonProcessingException;
/**
* 离线
*
* @param ctx
* @throws JsonProcessingException
*/
void offline(ChannelHandlerContext ctx);
/**
* 回执
*
* @param msgBody
* @throws JsonProcessingException
*/
void ack(@Valid MsgBody<ChatRecord> msgBody) throws JsonProcessingException;
/**
* 保存消息
*
* @param id
* @param msgBody
*/
void save(Long id, @Valid MsgBody<ChatRecord> msgBody);
/**
* 分配
*
* @param id
* @param msgBody
* @return
*/
NioSocketChannel distribution(Long id,@Valid MsgBody<ChatRecord> msgBody);
}
package com.ym.im.service;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.ym.im.entity.ChatRecord;
import com.ym.im.entity.MsgBody;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.socket.nio.NioSocketChannel;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
/**
* @author 陈俊雄
* @date 2019/5/29
**/
public interface MsgBodyService {
void msgBodyHandle(@NotNull ChannelHandlerContext ctx, @Valid MsgBody<ChatRecord> msgBody) throws JsonProcessingException;
void sendAndAck(NioSocketChannel channel, MsgBody<ChatRecord> msgBody) throws JsonProcessingException;
}
package com.ym.im.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ym.im.entity.MsgBody;
import com.ym.im.entity.Session;
import com.ym.im.entity.model.SessionInfo;
import java.util.List;
/**
* @author: JJww
* @Date:2020/10/21
*/
public interface SessionListService extends IService<Session> {
/**
* 获取会话信息列表
*
* @param userId
* @return
*/
MsgBody<List<SessionInfo>> getSessionList(Long userId);
}
package com.ym.im.service;
import com.ym.im.entity.model.IdModel;
import com.ym.im.entity.MsgBody;
import com.ym.im.entity.StaffSocketInfo;
/**
* @author: JJww
* @Date:2019-05-22
*/
public interface StaffService {
/**
* 获取空闲的客服ID
*
* @param userId
* @return
*/
StaffSocketInfo getIdleStaff(Long merchantId, Long userId);
/**
* 转发客服
*
* @param idModel
* @return
*/
MsgBody forward(IdModel idModel);
/**
* 获取商户所有在线客服
*
* @return
*/
MsgBody getMerchantStaffGroup(Long merchantId);
}
package com.ym.im.service;
import com.ym.im.entity.model.IdModel;
import com.ym.im.entity.MsgBody;
/**
* @author: JJww
* @Date:2019-07-24
*/
public interface UserService {
/**
* 获取当前用户列表
*
* @param staffId
* @return
*/
MsgBody getUserList(Long staffId);
/**
* 从列表中删除用户
*
* @param idModel
* @return
*/
MsgBody deleteUserFromList(IdModel idModel);
/**
* 查询用户绑定情况
*
* @param idModel
* @return
*/
MsgBody checkBinding(IdModel idModel);
}
package com.ym.im.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.ym.im.entity.ChatRecord;
import com.ym.im.entity.MsgBody;
import com.ym.im.entity.PullChatRecord;
import com.ym.im.entity.enums.ResultStatus;
import com.ym.im.mapper.ChatRecordMapper;
import com.ym.im.service.ChatRecordService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* <p>
* 聊天记录 服务实现类
* </p>
*
* @author 陈俊雄
* @since 2019-05-28
*/
@Service
public class ChatRecordServiceImpl extends ServiceImpl<ChatRecordMapper, ChatRecord> implements ChatRecordService {
@Autowired
private ChatRecordMapper chatRecordMapper;
@Override
public int insert(ChatRecord chatRecord) {
return chatRecordMapper.insert(chatRecord, remainder(chatRecord.getUserId()));
}
@Override
public int insertSelective(ChatRecord chatRecord) {
return chatRecordMapper.insertSelective(chatRecord, remainder(chatRecord.getUserId()));
}
@Override
public int updateByRecordId(ChatRecord chatRecord) {
return chatRecordMapper.updateById(chatRecord, remainder(chatRecord.getUserId()));
}
@Override
public int updateByIdSelective(ChatRecord chatRecord) {
return chatRecordMapper.updateByIdSelective(chatRecord, remainder(chatRecord.getUserId()));
}
@Override
public int updateReceiveTime(ChatRecord chatRecord) {
return chatRecordMapper.updateReceiveTime(chatRecord, remainder(chatRecord.getUserId()));
}
@Override
public ChatRecord selectById(Long id, Long userId) {
return chatRecordMapper.selectById(id, remainder(userId));
}
@Override
public MsgBody<PullChatRecord> getChatRecord(PullChatRecord pull) {
PageHelper.startPage(pull.getPageNum(), pull.getPageSize(), pull.getOrderBy() + " " + pull.getSorting());
final List<ChatRecord> chatRecords = chatRecordMapper.getChatRecord(pull, remainder(pull.getUserId()));
final PageInfo<ChatRecord> pageInfo = new PageInfo<>(chatRecords);
pull.setChatRecords(chatRecords);
pull.setPages(pageInfo.getPages());
pull.setTotal(pageInfo.getTotal());
return new MsgBody<PullChatRecord>().setCode(ResultStatus.SUCCESS.getCode()).setData(pull);
}
@Override
public MsgBody<List<ChatRecord>> updateReceiveTime(List<ChatRecord> chats) {
chatRecordMapper.updateBatchReceiveTimeById(chats, remainder(chats.get(0).getUserId()));
return new MsgBody<List<ChatRecord>>().setCode(ResultStatus.SUCCESS.getCode()).setData(chats);
}
@Override
public ChatRecord getLatestMsg(Long userId, Long merchantId) {
return chatRecordMapper.getLatestRecord(userId, merchantId, remainder(userId));
}
/**
* 使用userId取聊天记录表数量余数
*
* @param userId 用户Id
* @return 余数
*/
private int remainder(long userId) {
return (int) (userId % NUMBER_OF_TABLE);
}
}
package com.ym.im.service.impl;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.ym.im.entity.ChatRecord;
import com.ym.im.entity.MsgBody;
import com.ym.im.entity.base.ChannelAttributeKey;
import com.ym.im.entity.base.NettyConstant;
import com.ym.im.factory.SingleChatFactory;
import com.ym.im.mq.Queue;
import com.ym.im.service.ChatService;
import com.ym.im.service.MsgBodyService;
import com.ym.im.util.JsonUtils;
import com.ym.im.validation.group.MsgBodyGroup;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.socket.nio.NioSocketChannel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import static com.ym.im.entity.MsgBody.CHECK_MSG;
import static com.ym.im.entity.MsgBody.SEND_MSG;
/**
* @author 陈俊雄
* @date 2019/5/29
**/
@Slf4j
@Service
@Validated({MsgBodyGroup.class})
public class MsgBodyServiceImpl implements MsgBodyService {
@Autowired
private Queue queue;
@Autowired
private SingleChatFactory singleChatFactory;
@Resource(name = "myRedisTemplate")
private RedisTemplate redisTemplate;
@Override
public void msgBodyHandle(@NotNull ChannelHandlerContext ctx, @Valid MsgBody<ChatRecord> msgBody) throws JsonProcessingException {
final ChatService chatService = singleChatFactory.getService(ctx.channel().attr(ChannelAttributeKey.ROLE_TYPE).get());
switch (msgBody.getCode()) {
case SEND_MSG:
// 获取用户、客服Id
final Long id = ctx.channel().attr(ChannelAttributeKey.ROLE_ID).get();
// 先保存聊天消息
chatService.save(id, msgBody);
// 再发送回执
ctx.channel().writeAndFlush(msgBody.setCode(CHECK_MSG)).addListener((ChannelFutureListener) future -> {
// 获取对应的channel
final NioSocketChannel channel = chatService.distribution(id, msgBody);
if (channel != null) {
// 最后发送聊天消息
chatService.send(channel, msgBody.setCode(SEND_MSG));
}
});
break;
case CHECK_MSG:
msgBody.setCode(MsgBody.HAVE_READ);//回执
chatService.ack(msgBody);
break;
default:
break;
}
}
@Override
public void sendAndAck(NioSocketChannel channel, MsgBody<ChatRecord> msgBody) throws JsonProcessingException {
msgBody.setCode(SEND_MSG);
// 先保存消息至Redis
redisTemplate.opsForHash().put(NettyConstant.MSG_KEY + msgBody.getData().getUserId(), msgBody.getData().getId(), JsonUtils.obj2Json(msgBody));
// 再默认以用户没有收到消息为前提,做循环、延迟通知
queue.delaysQueue(msgBody);
// 最后发送聊天信息
channel.writeAndFlush(msgBody);
}
}
package com.ym.im.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ym.im.entity.ChatRecord;
import com.ym.im.entity.MsgBody;
import com.ym.im.entity.Session;
import com.ym.im.entity.enums.ResultStatus;
import com.ym.im.entity.model.SessionInfo;
import com.ym.im.mapper.SessionListMapper;
import com.ym.im.service.ChatRecordService;
import com.ym.im.service.SessionListService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
* @author: JJww
* @Date:2020/10/21
*/
@Slf4j
@Service
public class SessionListServiceImpl extends ServiceImpl<SessionListMapper, Session> implements SessionListService {
@Autowired
private ChatRecordService chatRecordService;
@Override
public MsgBody<List<SessionInfo>> getSessionList(Long userId) {
final List<SessionInfo> sessions = new ArrayList<>();
baseMapper.selectList(new QueryWrapper<Session>().lambda().eq(Session::getUserId, userId)).forEach(session -> {
final ChatRecord latestMsg = chatRecordService.getLatestMsg(userId, session.getMerchantId());
final SessionInfo sessionModel = new SessionInfo().setLatestRecord(latestMsg.getMsgInfo()).setRecordId(latestMsg.getId());
BeanUtils.copyProperties(session, sessionModel);
sessions.add(sessionModel);
});
return new MsgBody<List<SessionInfo>>().setCode(ResultStatus.SUCCESS.getCode()).setData(sessions);
}
}
package com.ym.im.service.impl;
import com.ym.im.entity.MsgBody;
import com.ym.im.entity.StaffSocketInfo;
import com.ym.im.entity.UserSocketInfo;
import com.ym.im.entity.enums.ResultStatus;
import com.ym.im.entity.model.IdModel;
import com.ym.im.handler.ChannelGroupHandler;
import com.ym.im.service.StaffService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.*;
import static java.util.Map.Entry.comparingByValue;
import static java.util.stream.Collectors.toMap;
/**
* @author: JJww
* @Date:2019-05-22
*/
@Service
public class StaffServiceImpl implements StaffService {
@Autowired
private ChannelGroupHandler channelGroup;
@Override
public StaffSocketInfo getIdleStaff(Long merchantId, Long userId) {
final Map<Long, StaffSocketInfo> socketInfoMap = channelGroup.STAFF_GROUP.get(merchantId);
if (socketInfoMap == null) {
return null;
}
final LinkedHashMap<Long, StaffSocketInfo> collect = socketInfoMap
.entrySet()
.stream()
.sorted(comparingByValue(new Comparator<StaffSocketInfo>() {
@Override
public int compare(StaffSocketInfo o1, StaffSocketInfo o2) {
return Integer.valueOf(o1.getUserIds().size()).compareTo(Integer.valueOf(o2.getUserIds().size()));
}
})).collect(toMap(e -> e.getKey(), e -> e.getValue(), (e1, e2) -> e2, LinkedHashMap::new));
if (collect.size() == 0) {
return null;
}
//客服和用户绑定
StaffSocketInfo staffSocketInfo = collect.entrySet().iterator().next().getValue();
staffSocketInfo.getUserIds().add(userId);
//用户和客服绑定
final UserSocketInfo userSocketInfo = channelGroup.USER_GROUP.get(userId);
if (userSocketInfo != null) {
final Long staffId = staffSocketInfo.getStaffId();
userSocketInfo.setStaff(merchantId, staffId);
//通知用户 新的客服
userSocketInfo.writeAndFlush(new MsgBody<>().setCode(MsgBody.DISTRIBUTION_STAFF).setData(new IdModel().setStaffId(staffId).setUserId(userId)));
}
return staffSocketInfo;
}
@Override
public MsgBody forward(IdModel idModel) {
final Long userId = idModel.getUserId();
final Long staffId = idModel.getStaffId();
final Long merchantId = idModel.getMerchantId();
final UserSocketInfo userSocketInfo = channelGroup.USER_GROUP.get(userId);
final StaffSocketInfo staffSocketInfo = channelGroup.getMerchantStaff(staffId);
if (staffSocketInfo == null || userSocketInfo == null) {
return new MsgBody<>().setCode(MsgBody.BINDINGFAILURE).setMessage(ResultStatus.FORWARD_FAILURE.getMessage());
}
//移除原客服绑定
channelGroup.getMerchantStaff(userSocketInfo.getStaffId(merchantId)).getUserIds().remove(userId);
//设置新的客服
staffSocketInfo.getUserIds().add(userId);
userSocketInfo.setStaff(merchantId, staffId);
final MsgBody<IdModel> msgBody = new MsgBody<IdModel>().setCode(MsgBody.DISTRIBUTION_STAFF).setData(idModel);
//通知用户 新客服ID
userSocketInfo.writeAndFlush(msgBody);
//通知新客服
staffSocketInfo.writeAndFlush(msgBody);
return new MsgBody<>().setData(ResultStatus.SUCCESS.getCode());
}
@Override
public MsgBody getMerchantStaffGroup(Long merchantId) {
final List<StaffSocketInfo> staffs = new ArrayList<StaffSocketInfo>();
channelGroup.STAFF_GROUP.get(merchantId).forEach((k, v) -> {
staffs.add(v);
});
return new MsgBody<>().setCode(ResultStatus.SUCCESS.getCode()).setData(staffs);
}
}
package com.ym.im.service.impl;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.ym.im.entity.ChatRecord;
import com.ym.im.entity.MsgBody;
import com.ym.im.entity.StaffSocketInfo;
import com.ym.im.entity.UserSocketInfo;
import com.ym.im.entity.base.ChannelAttributeKey;
import com.ym.im.entity.base.NettyConstant;
import com.ym.im.entity.model.IdModel;
import com.ym.im.handler.ChannelGroupHandler;
import com.ym.im.mq.Queue;
import com.ym.im.service.ChatRecordService;
import com.ym.im.service.ChatService;
import com.ym.im.service.MsgBodyService;
import com.ym.im.util.JsonUtils;
import com.ym.im.validation.group.*;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.socket.nio.NioSocketChannel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
import javax.validation.Valid;
import java.util.Date;
import java.util.Set;
import static com.ym.im.entity.ChatRecord.RECEIVE;
/**
* @author: JJww
* @Date:2019-05-21
*/
@Slf4j
@Service
@Validated
public class StaffSingleChatServiceImpl implements ChatService {
@Autowired
private Queue queue;
@Resource(name = "myRedisTemplate")
private RedisTemplate redisTemplate;
@Autowired
private ChatRecordService chatRecordService;
@Autowired
private MsgBodyService msgBodyService;
@Autowired
private ChannelGroupHandler channelGroup;
// @Autowired
// private PushGatherService pushService;
@Override
public void init(ChannelHandlerContext ctx) {
final Long staffId = ctx.channel().attr(ChannelAttributeKey.ROLE_ID).get();
final Long merchantId = ctx.channel().attr(ChannelAttributeKey.MERCHANT_ID).get();
StaffSocketInfo staffSocketInfo = new StaffSocketInfo();
staffSocketInfo.setStaffId(staffId);
staffSocketInfo.setMerchantId(merchantId);
staffSocketInfo.setToken(ctx.channel().attr(ChannelAttributeKey.TOKEN_INFO).get());
staffSocketInfo.setChannel((NioSocketChannel) ctx.channel());
staffSocketInfo.setUserIds(redisTemplate.opsForSet().members(NettyConstant.STAFF_USERIDS_KEY + staffId));
channelGroup.putMerchantStaff(merchantId, staffSocketInfo);
log.info("客服: " + staffId + " 上线:");
}
@Override
public void offline(ChannelHandlerContext ctx) {
final Long staffId = ctx.channel().attr(ChannelAttributeKey.ROLE_ID).get();
final Long merchantId = ctx.channel().attr(ChannelAttributeKey.MERCHANT_ID).get();
final StaffSocketInfo staffSocketInfo = channelGroup.getMerchantStaff(merchantId, staffId);
if (ctx.channel().attr(ChannelAttributeKey.TOKEN_INFO).get().equals(staffSocketInfo.getToken())) {
final Set<Long> userIds = staffSocketInfo.getUserIds();
if (userIds.size() != 0) {
final String userListKey = NettyConstant.STAFF_USERIDS_KEY + staffId;
redisTemplate.delete(userListKey);
redisTemplate.opsForSet().add(userListKey, userIds.toArray(new Long[userIds.size()]));
queue.staffOfflineQueue(new StaffSocketInfo(staffId, userIds)); //NioSocketChannel无法序列化 所以new StaffSocketInfo
}
channelGroup.removeMerchantStaff(staffId);
ctx.close();
log.info("客服: " + staffId + " 下线:");
}
}
@Override
@Validated({MsgBodyGroup.class, ChatRecordSendGroup.class, StaffSendGroup.class})
public NioSocketChannel distribution(Long id, @Valid MsgBody<ChatRecord> msgBody) {
final ChatRecord chatRecord = msgBody.getData();
final Long userId = chatRecord.getUserId();
final Long merchantId = channelGroup.getMerchantStaff(id).getMerchantId();
final UserSocketInfo userSocketInfo = channelGroup.USER_GROUP.get(userId);
if (userSocketInfo == null) {
//用户不在线,保存最后发送消息的客服ID
redisTemplate.opsForHash().put(NettyConstant.IM_USERS, userId, id);
//推送通知
pushNotifications(userId);
return null;
}
final Long currentStaffId = userSocketInfo.getStaffId(merchantId);
if (currentStaffId == null) {
//通知用户 新的客服
userSocketInfo.setStaff(merchantId, id);
userSocketInfo.writeAndFlush(new MsgBody<>().setCode(MsgBody.DISTRIBUTION_STAFF).setData(new IdModel().setStaffId(id).setUserId(userId)));
channelGroup.getMerchantStaff(id).getUserIds().add(userId);
}
if (currentStaffId != null && !currentStaffId.equals(id)) {
//通知客服 绑定失败 当前用户已绑定客服
channelGroup.getMerchantStaff(id).writeAndFlush(new MsgBody<>().setCode(MsgBody.BINDINGFAILURE).setData(new IdModel().setStaffId(currentStaffId).setUserId(userId).setMerchantId(merchantId)));
return null;
}
return userSocketInfo.getChannel();
}
@Override
@Validated({MsgBodyGroup.class, ChatRecordSendGroup.class, StaffSendGroup.class})
public void save(Long id, @Valid MsgBody<ChatRecord> msgBody) {
// 设置聊天基本信息
final ChatRecord record = msgBody.getData();
record.setId(IdWorker.getId())
.setMerchantId(channelGroup.getMerchantStaff(id).getMerchantId())
.setSendReceive(RECEIVE)
.setCreateTime(new Date());
// 先保存至数据库,再发送消息(若颠倒顺序可能导致数据未保存,更新已读操作先执行导致消息一直是未读状态
chatRecordService.insertSelective(record);
log.info("客服 消息保存:" + record.getId());
}
/**
* 发送消息至用户端
*
* @param msgBody 消息对象
* @throws JsonProcessingException e
*/
@Override
public void send(NioSocketChannel channel, MsgBody<ChatRecord> msgBody) throws JsonProcessingException {
msgBodyService.sendAndAck(channel, msgBody);
pushNotifications(msgBody.getData().getUserId());
}
/**
* 通知用户端客服端收到消息
*
* @param msgBody 消息对象
* @throws JsonProcessingException e
*/
@Override
@Validated({MsgBodyGroup.class, ChatRecordReceiveGroup.class, ChatRecordSaveGroup.class})
public void ack(@Valid MsgBody<ChatRecord> msgBody) throws JsonProcessingException {
final ChatRecord record = msgBody.getData();
record.setModifyTime(new Date());
chatRecordService.updateReceiveTime(record);
UserSocketInfo userSocketInfo = channelGroup.USER_GROUP.get(record.getUserId());
if (userSocketInfo != null) {
userSocketInfo.writeAndFlush(msgBody);
redisTemplate.opsForHash().put(NettyConstant.MSG_KEY + record.getUserId(), record.getId(), JsonUtils.obj2Json(msgBody));
}
log.info("客服 消息回执:" + record.getId());
}
/**
* 推送通知
*
* @param userId
*/
private void pushNotifications(Long userId) {
// String title = null;
// String content = null;
// final UserSocketInfo userSocketInfo = ChannelGroupService.USER_GROUP.get(userId);
// PushTokenAndTypeModel pushToken = userSocketInfo != null ? userSocketInfo.getPushToken() : feignClientUsersService.getPushToken(String.valueOf(userId)).getData();
// final String col = userSocketInfo == null ? LanguageEnum.zh.getKey() : userSocketInfo.getCol();
// if (LanguageEnum.zh.getKey().equals(col)) {
// title = PushTitleEnum.customerService.getName();
// content = PushContentEnum.sndmsg.getName();
// } else {
// title = PushTitleEnum.customerService.getNameEnglish();
// content = PushContentEnum.sndmsg.getNameEnglish();
// }
// Map customBoundary = new HashMap<>();
// customBoundary.put("pushType", PushTypeEnum.customerServicePush.getKey());
// pushService.pushTokenGather(pushToken.getPushToken(), pushToken.getPushType(), title, content, customBoundary);
}
}
\ No newline at end of file
package com.ym.im.service.impl;
import com.ym.im.entity.MsgBody;
import com.ym.im.entity.StaffSocketInfo;
import com.ym.im.entity.UserSocketInfo;
import com.ym.im.entity.base.NettyConstant;
import com.ym.im.entity.enums.ResultStatus;
import com.ym.im.entity.model.IdModel;
import com.ym.im.handler.ChannelGroupHandler;
import com.ym.im.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Set;
/**
* @author: JJww
* @Date:2019-07-24
*/
@Slf4j
@Service
public class UserServiceImpl implements UserService {
@Resource(name = "myRedisTemplate")
private RedisTemplate redisTemplate;
@Autowired
private ChannelGroupHandler channelGroup;
@Override
public MsgBody getUserList(Long staffId) {
final Set<Long> usersId = redisTemplate.opsForSet().members(NettyConstant.STAFF_USERIDS_KEY + staffId);
if (usersId.size() > 0) {
redisTemplate.delete(NettyConstant.STAFF_USERIDS_KEY + staffId);
}
return new MsgBody<>().setCode(ResultStatus.SUCCESS.getCode()).setData(usersId);
}
@Override
public MsgBody deleteUserFromList(IdModel idModel) {
final StaffSocketInfo staffSocketInfo = channelGroup.getMerchantStaff(idModel.getStaffId());
if (staffSocketInfo == null) {
return new MsgBody<>().setCode(ResultStatus.REQUEST_ERROR.getCode()).setMessage(ResultStatus.REQUEST_ERROR.getMessage());
}
staffSocketInfo.getUserIds().remove(idModel.getUserId());
final UserSocketInfo userSocketInfo = channelGroup.USER_GROUP.get(idModel.getUserId());
if (userSocketInfo != null && idModel.getStaffId().equals(userSocketInfo.getStaffId(idModel.getMerchantId()))) {
userSocketInfo.getStaffIds().remove(idModel.getMerchantId());
}
return new MsgBody<>().setCode(ResultStatus.SUCCESS.getCode());
}
@Override
public MsgBody checkBinding(IdModel idModel) {
final Long merchantId = idModel.getMerchantId();
final UserSocketInfo userSocketInfo = channelGroup.USER_GROUP.get(idModel.getUserId());
/**
* 用户不在线 不校验绑定关系
* 用户在线,只有绑定的客服才能发送消息
* 用户在线未绑定客服
*/
if (userSocketInfo == null || (userSocketInfo != null && idModel.getStaffId().equals(userSocketInfo.getStaffId(merchantId)) || userSocketInfo.getStaffId(merchantId) == null)) {
return new MsgBody<>().setCode(ResultStatus.SUCCESS.getCode());
}
return new MsgBody<>().setCode(ResultStatus.CHECK_FAILURE.getCode()).setData(userSocketInfo.getStaffId(merchantId));
}
}
\ No newline at end of file
package com.ym.im.util;
import com.ym.im.exception.IMessageException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.lang.Nullable;
/**
* @author 陈俊雄
* @date 2019/5/29
**/
@Slf4j
public abstract class Assert {
public static void notNull(@Nullable Object object, String i18nError) {
if (object == null) {
throw new IMessageException(i18nError);
}
}
}
package com.ym.im.util;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.type.TypeFactory;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.List;
/**
* Json对象操作工具
*
* @author 陈俊雄
**/
public class JsonUtils {
private static ObjectMapper MAPPER = new ObjectMapper()
// 忽略Json对象在实体类中没有的字段
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
public static void setObjectMapper(ObjectMapper objectMapper) {
MAPPER = objectMapper;
}
/**
* 实体类转json字符串
*
* @param obj obj
* @return jsonStr
* @throws JsonProcessingException e
*/
public static String obj2Json(Object obj) throws JsonProcessingException {
return MAPPER.writeValueAsString(obj);
}
/**
* 实体类转json字符串,并忽略实体类为空的字段
*
* @param obj obj
* @return jsonStr
* @throws JsonProcessingException e
*/
public static String obj2JsonNonNull(Object obj) throws JsonProcessingException {
MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL);
return MAPPER.writeValueAsString(obj);
}
/**
* json字符串转实体类
*
* @param jsonData jsonStr
* @param beanType Class
* @param <T> T
* @return T
* @throws IOException e
*/
public static <T> T json2Obj(String jsonData, Class<T> beanType) throws IOException {
return MAPPER.readValue(jsonData, beanType);
}
/**
* json字符串转实体类
*
* @param jsonData jsonStr
* @param javaType javaType可通过{@link #getJavaType}获取
* @param <T> T
* @return T
* @throws IOException e
*/
public static <T> T json2Obj(String jsonData, JavaType javaType) throws IOException {
return MAPPER.readValue(jsonData, javaType);
}
/**
* json字符串转实体类
* {@link #getJavaType(Class, JavaType...)}
*
* @param jsonData jsonStr
* @param parametrized class
* @param parameterClasses class[]
* @param <T> T
* @return T
* @throws IOException e
*/
public static <T> T json2Obj(String jsonData, Class<?> parametrized, Class<?>... parameterClasses) throws IOException {
return MAPPER.readValue(jsonData, getJavaType(parametrized, parameterClasses));
}
/**
* 获取JavaType
* 如:JsonUtils.getJavaType(String.class);
*
* @param type class
* @return {@link JavaType}
*/
public static JavaType getJavaType(Type type) {
return MAPPER.getTypeFactory().constructType(type);
}
/**
* 获取JavaType,常用于复杂对象
* 如json转Map<String, Long>:
* JsonUtils.getJavaType(Map.class, String.class, Long.class);
* 更复杂的对象则是对上面示例进行嵌套,泛型同理
* 如json转Map<String, Map<Long, List<Integer>>:
* 首先使用{@link #getJavaType(Class, Class[])}获取List<Integer>的javaType
* JavaType listType = JsonUtils.getJavaType(List.class, Integer.class);
* 其次使用{@link #getJavaType(Class, JavaType...)}来创建Map的javaType
* JavaType mapType = JsonUtils.getJavaType(Map.class, listType);
* 最后使用{@link #json2Obj(String, JavaType)}将json字符串转换成map对象
* Map<String, Map<Long, List<Integer>> map = JsonUtils.json2Obj(jsonStr, mapType);
*
* @param parametrized class
* @param parameterClasses class[]
* @return {@link JavaType}
*/
public static JavaType getJavaType(Class<?> parametrized, Class<?>... parameterClasses) {
return MAPPER.getTypeFactory().constructParametricType(parametrized, parameterClasses);
}
/**
* 获取JavaType,常用于复杂对象
* 参考{@link #getJavaType(Class, Class[])}
*
* @param rawType class
* @param parameterTypes javaType[]
* @return {@link JavaType}
*/
public static JavaType getJavaType(Class<?> rawType, JavaType... parameterTypes) {
return MAPPER.getTypeFactory().constructParametricType(rawType, parameterTypes);
}
/**
* json字符串转Map
*
* @param jsonData jsonStr
* @param k key
* @param v value
* @param <K> key class
* @param <V> value class
* @return map
* @throws IOException e
*/
public static <K, V> HashMap<K, V> json2HashMap(String jsonData, Class<K> k, Class<V> v) throws IOException {
return MAPPER.readValue(jsonData, TypeFactory.defaultInstance().constructMapType(HashMap.class, k, v));
}
/**
* json字符串转list
*
* @param jsonData jsonStr
* @param t class
* @param <T> T
* @return list
* @throws IOException e
*/
public static <T> List<T> json2List(String jsonData, Class<T> t) throws IOException {
return MAPPER.readValue(jsonData, TypeFactory.defaultInstance().constructCollectionType(List.class, t));
}
/**
* map等对象转实体类,通过映射方式
*
* @param obj obj
* @param beanType class
* @param <T> T
* @return T
*/
public static <T> T obj2Class(Object obj, Class<T> beanType) {
return MAPPER.convertValue(obj, beanType);
}
}
package com.ym.im.util;
import org.springframework.context.MessageSource;
import java.util.Locale;
/**
* @author 陈俊雄
* @date 2019/6/3
**/
public class MessageUtils {
public static MessageSource messageSource;
public static void setMessageSource(MessageSource ms) {
messageSource = ms;
}
public MessageSource getMessageSource() {
return messageSource;
}
public static String getMsg(String code, Object[] args, Locale locale) {
return messageSource.getMessage(code, args, locale);
}
public static String getMsg(String code, Locale locale, Object... args) {
return messageSource.getMessage(code, args, locale);
}
public static String getMsg(String code, Object[] args, String locale) {
return messageSource.getMessage(code, args, new Locale(locale));
}
public static String getMsg(String code, Object[] args) {
return messageSource.getMessage(code, args, Locale.getDefault());
}
public static String getMsg(String code, Locale locale) {
return messageSource.getMessage(code, null, locale);
}
public static String getMsg(String code, String locale) {
return messageSource.getMessage(code, null, new Locale(locale));
}
public static String getMsg(String code) {
return messageSource.getMessage(code, null, Locale.getDefault());
}
}
//package com.ym.im.util;
//
//import cn.hutool.extra.template.TemplateConfig;
//import com.baomidou.mybatisplus.core.config.GlobalConfig;
//import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException;
//import com.baomidou.mybatisplus.core.toolkit.StringPool;
//import com.baomidou.mybatisplus.generator.AutoGenerator;
//import com.baomidou.mybatisplus.generator.InjectionConfig;
//import com.baomidou.mybatisplus.generator.config.po.TableInfo;
//import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
//import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
//import com.ym.util.StringUtils;
//
//import java.util.ArrayList;
//import java.util.List;
//import java.util.Scanner;
//
///**
// * 自动生成代码
// */
//public class MybatisGenerator {
//
//
// /**
// * <p>
// * 读取控制台内容
// * </p>
// */
// public static String scanner(String tip) {
// Scanner scanner = new Scanner(System.in);
// StringBuilder help = new StringBuilder();
// help.append("请输入" + tip + ":");
// System.out.println(help.toString());
// if (scanner.hasNext()) {
// String ipt = scanner.next();
// if (StringUtils.isNotEmpty(ipt)) {
// return ipt;
// }
// }
// throw new MybatisPlusException("请输入正确的" + tip + "!");
// }
//
// public static void main(String[] args) {
// // 代码生成器
// AutoGenerator mpg = new AutoGenerator();
//
// // 全局配置
// GlobalConfig gc = new GlobalConfig();
//
// //项目工程路径
// String projectPath = "D:\\JavaDevelopment\\IdeaProjects\\pathfinder\\pathfinder_im";
// //java代码路径
// gc.setOutputDir(projectPath + "/src/main/java/com/ym/im");
//
// gc.setAuthor("陈俊雄");
// gc.setOpen(false);
// mpg.setGlobalConfig(gc);
//
// // 数据源配置
// DataSourceConfig dsc = new DataSourceConfig();
// dsc.setUrl("jdbc:mysql://192.168.1.83:3306/pathfinder_im?useUnicode=true&useSSL=false&characterEncoding=utf-8");
// // dsc.setSchemaName("public");
// dsc.setDriverName("com.mysql.cj.jdbc.Driver");
// dsc.setUsername("root");
// dsc.setPassword("123456");
// mpg.setDataSource(dsc);
//
//
// // 包配置
// PackageConfig pc = new PackageConfig();
//// pc.setModuleName(scanner("模块名"));
// pc.setParent("com.ym.im");
// mpg.setPackageInfo(pc);
//
// // 自定义配置
// InjectionConfig cfg = new InjectionConfig() {
// @Override
// public void initMap() {
// // to do nothing
// }
// };
//
// // 如果模板引擎是 freemarker
// String templatePath = "/templates/mapper.xml.ftl";
// // 如果模板引擎是 velocity
// // String templatePath = "/templates/mapping.xml.vm";
//
// // 自定义输出配置
// List<FileOutConfig> focList = new ArrayList<>();
// // 自定义配置会被优先输出
// focList.add(new FileOutConfig(templatePath) {
// @Override
// public String outputFile(TableInfo tableInfo) {
// // 自定义输出文件名
// return projectPath + "/src/main/resources/com/ym/im/mapping/" //+ pc.getModuleName() + "/"
// + tableInfo.getEntityName() + "Mapping" + StringPool.DOT_XML;
// }
// });
//
// cfg.setFileOutConfigList(focList);
// mpg.setCfg(cfg);
//
// // 配置模板
// TemplateConfig templateConfig = new TemplateConfig();
//
// // 配置自定义输出模板
// // templateConfig.setEntity();
// // templateConfig.setService();
// // templateConfig.setController();
//
// templateConfig.setXml(null);
// mpg.setTemplate(templateConfig);
//
// // 策略配置
// StrategyConfig strategy = new StrategyConfig();
// strategy.setNaming(NamingStrategy.underline_to_camel);
// strategy.setColumnNaming(NamingStrategy.underline_to_camel);
//// strategy.setSuperEntityClass("com.ym.entity");
// strategy.setEntityLombokModel(true);
// strategy.setRestControllerStyle(true);
//// strategy.setSuperControllerClass("com.ym.controller");
//
//
// strategy.setInclude(scanner("表名"));
//// strategy.setSuperEntityColumns("id");
// strategy.setControllerMappingHyphenStyle(true);
//// strategy.setTablePrefix(pc.getModuleName() + "_");
// mpg.setStrategy(strategy);
// mpg.setTemplateEngine(new FreemarkerTemplateEngine());
// mpg.execute();
// }
//
//
//}
package com.ym.im.validation.group;
/**
* @author 陈俊雄
* @date 2019/6/11
**/
public interface ChatRecordReceiveGroup {
}
package com.ym.im.validation.group;
/**
* @author 陈俊雄
* @date 2019/6/11
**/
public interface ChatRecordSaveGroup {
}
package com.ym.im.validation.group;
/**
* @author 陈俊雄
* @date 2019/6/11
**/
public interface ChatRecordSendGroup {
}
package com.ym.im.validation.group;
/**
* @author 陈俊雄
* @date 2019/6/11
**/
public interface MsgBodyGroup {
}
package com.ym.im.validation.group;
/**
* @author 陈俊雄
* @date 2019/6/12
**/
public interface StaffSendGroup {
}
---
#开发环境
spring:
datasource:
# 数据库连接池配置
druid:
# 初始化时建立物理连接的个数
initial-size: 5
# 最大连接池数量
max-active: 10
# 用来检测连接是否有效的sql语句
validation-query: SELECT 1 FROM DUAL
# 最小连接池数量
min-idle: 5
# 获取连接时最大等待时间,单位毫秒
max-wait: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位毫秒
time-between-eviction-runs-millis: 60000
url: jdbc:mysql://127.0.0.1/customer_service?useUnicode=true&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 101020
driver-class-name: com.mysql.cj.jdbc.Driver
jackson:
default-property-inclusion: non_null
rabbitmq:
host: 127.0.0.1
port: 5672
username: admin
password: admin
delay-queue-name: delay.ack.dev
staff-offline-Queue-Name: staff.offline.dev
exchange-name: delay.exchange.dev
listener:
simple:
default-requeue-rejected: false
redis:
database: 0
host: 47.99.47.225
password: temple123456
port: 6379
# (重要!)设置mvc默认语言为zh_CN,默认语言必须为static.i18n目录下有的语言配置文件,否则跟随服务器语言
mvc:
locale: zh_CN
devtools:
restart:
log-condition-evaluation-delta: false
---
#生产环境
spring:
datasource:
# 数据库连接池配置
druid:
# 初始化时建立物理连接的个数
initial-size: 5
# 最大连接池数量
max-active: 10
# 用来检测连接是否有效的sql语句
validation-query: SELECT 1 FROM DUAL
# 最小连接池数量
min-idle: 5
# 获取连接时最大等待时间,单位毫秒
max-wait: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位毫秒
time-between-eviction-runs-millis: 60000
url: jdbc:mysql://172.31.33.14:3306/pathfinder_im?useUnicode=true&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: Yum123456
driver-class-name: com.mysql.cj.jdbc.Driver
jackson:
default-property-inclusion: non_null
rabbitmq:
host: 172.31.33.14
port: 5672
username: admin
password: Yum123456
delay-queue-name: delay.ack
staff-offline-Queue-Name: staff.offline
exchange-name: delayAck
listener:
simple:
default-requeue-rejected: false
redis:
database: 5
host: 172.31.33.14
password: Yum123456
port: 6379
# (重要!)设置mvc默认语言为zh_CN,默认语言必须为static.i18n目录下有的语言配置文件,否则跟随服务器语言
mvc:
locale: zh_CN
devtools:
restart:
log-condition-evaluation-delta: false
#日志配置
logging:
file: logs/${spring.application.name}.log
eureka:
client:
service-url:
defaultZone: http://172.31.41.108:20000/eureka/
instance:
prefer-ip-address: true
\ No newline at end of file
---
#开发环境
spring:
datasource:
# 数据库连接池配置
druid:
# 初始化时建立物理连接的个数
initial-size: 5
# 最大连接池数量
max-active: 10
# 用来检测连接是否有效的sql语句
validation-query: SELECT 1 FROM DUAL
# 最小连接池数量
min-idle: 5
# 获取连接时最大等待时间,单位毫秒
max-wait: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位毫秒
time-between-eviction-runs-millis: 60000
url: jdbc:mysql://192.168.1.237:3306/pathfinder_im?useUnicode=true&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
jackson:
default-property-inclusion: non_null
rabbitmq:
host: 127.0.0.1
port: 5672
username: admin
password: admin
delay-queue-name: delay.ack.dev
staff-offline-Queue-Name: staff.offline.dev
exchange-name: delay.exchange.dev
listener:
simple:
default-requeue-rejected: false
redis:
database: 5
host: 127.0.0.1
password:
port: 6379
# (重要!)设置mvc默认语言为zh_CN,默认语言必须为static.i18n目录下有的语言配置文件,否则跟随服务器语言
mvc:
locale: zh_CN
devtools:
restart:
log-condition-evaluation-delta: false
spring:
profiles:
active: @profiles.active@
application:
name: customer-service
# 404抛出异常
mvc:
throw-exception-if-no-handler-found: true
resources:
add-mappings: false
server:
port: 9090
# 关闭tomcat自带/error页面
error:
whitelabel:
enabled: false
netty:
port: 9095
#日志配置
logging:
level:
root: info
com.ym.im.mapper: debug
pagehelper:
helper-dialect: mysql
reasonable: true
support-methods-arguments: ture
params: count=contSql
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ym.im.mapper.ChatRecordMapper">
<resultMap id="BaseResultMap" type="com.ym.im.entity.ChatRecord">
<id column="id" jdbcType="BIGINT" property="id"/>
<result column="user_id" jdbcType="BIGINT" property="userId"/>
<result column="merchant_id" jdbcType="BIGINT" property="merchantId"/>
<result column="msg_type" jdbcType="TINYINT" property="msgType"/>
<result column="msg_info" jdbcType="VARCHAR" property="msgInfo"/>
<result column="send_receive" jdbcType="TINYINT" property="sendReceive"/>
<result column="send_time" jdbcType="TIMESTAMP" property="sendTime"/>
<result column="receive_time" jdbcType="TIMESTAMP" property="receiveTime"/>
<result column="create_time" jdbcType="TIMESTAMP" property="createTime"/>
<result column="modify_time" jdbcType="TIMESTAMP" property="modifyTime"/>
</resultMap>
<sql id="Base_Column_List">
id, user_id, merchant_id,msg_type, msg_info, send_receive, send_time, receive_time, create_time, modify_time
</sql>
<insert id="insert">
INSERT INTO chat_record_${index} (<include refid="Base_Column_List"/>)
VALUES (
#{chat.id},
#{chat.userId},
#{chat.merchant_id},
#{chat.msgType},
#{chat.msgInfo},
#{chat.sendReceive},
#{chat.sendTime},
#{chat.receiveTime},
#{chat.createTime},
#{chat.modifyTime})
</insert>
<insert id="insertSelective">
INSERT INTO chat_record_${index}
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="chat.id != null">
id,
</if>
<if test="chat.userId != null">
user_id,
</if>
<if test="chat.merchantId != null">
merchant_id,
</if>
<if test="chat.msgType != null">
msg_type,
</if>
<if test="chat.msgInfo != null">
msg_info,
</if>
<if test="chat.sendReceive != null">
send_receive,
</if>
<if test="chat.sendTime != null">
send_time,
</if>
<if test="chat.receiveTime != null">
receive_time,
</if>
<if test="chat.createTime != null">
create_time,
</if>
<if test="chat.modifyTime != null">
modify_time,
</if>
</trim>
<trim prefix="VALUES (" suffix=")" suffixOverrides=",">
<if test="chat.id != null">
#{chat.id},
</if>
<if test="chat.userId != null">
#{chat.userId},
</if>
<if test="chat.merchantId != null">
#{chat.merchantId},
</if>
<if test="chat.msgType != null">
#{chat.msgType},
</if>
<if test="chat.msgInfo != null">
#{chat.msgInfo},
</if>
<if test="chat.sendReceive != null">
#{chat.sendReceive},
</if>
<if test="chat.sendTime != null">
#{chat.sendTime},
</if>
<if test="chat.receiveTime != null">
#{chat.receiveTime},
</if>
<if test="chat.createTime != null">
#{chat.createTime},
</if>
<if test="chat.modifyTime != null">
#{chat.modifyTime},
</if>
</trim>
</insert>
<update id="updateById">
UPDATE chat_record_${index}
SET user_id = #{chat.userId},
merchant_id = #{chat.merchantId},
msg_type = #{chat.msgType},
msg_info = #{chat.msgInfo},
send_receive = #{chat.sendReceive},
send_time = #{chat.sendTime},
receive_time = #{chat.receiveTime},
create_time = #{chat.createTime},
modify_time = #{chat.modifyTime}
WHERE id = #{chat.id}
</update>
<update id="updateByIdSelective">
UPDATE chat_record_${index}
<set>
<if test="chat.id != null">
#{chat.id}
</if>
<if test="chat.userId != null">
#{chat.userId}
</if>
<if test="chat.merchantId != null">
#{chat.merchantId}
</if>
<if test="chat.msgType != null">
#{chat.msgType}
</if>
<if test="chat.msgInfo != null">
#{chat.msgInfo}
</if>
<if test="chat.sendReceive != null">
#{chat.sendReceive}
</if>
<if test="chat.sendTime != null">
#{chat.sendTime}
</if>
<if test="chat.receiveTime != null">
#{chat.receiveTime}
</if>
<if test="chat.createTime != null">
#{chat.createTime}
</if>
<if test="chat.modifyTime != null">
#{chat.modifyTime}
</if>
</set>
</update>
<update id="updateBatchReceiveTimeById">
UPDATE chat_record_${index}
<set>
receive_time =
<foreach collection="chats" item="chat" index="index" separator=" " open="CASE" close="END">
WHEN id = #{chat.id} THEN #{chat.receiveTime}
</foreach>
</set>
WHERE id IN
<foreach collection="chats" item="chat" index="index" separator="," open="(" close=")">
#{chat.id}
</foreach>
</update>
<update id="updateReceiveTime">
UPDATE chat_record_${index}
<set>
receive_time = #{chat.receiveTime},
modify_time = #{chat.modifyTime}
</set>
WHERE id = #{chat.id}
</update>
<select id="selectById" resultType="com.ym.im.entity.ChatRecord">
SELECT
<include refid="Base_Column_List"/>
FROM chat_record_${index} WHERE id = #{id}
</select>
<select id="getChatRecord" resultType="com.ym.im.entity.ChatRecord">
SELECT
<include refid="Base_Column_List"/>
FROM chat_record_${index}
<trim prefix="WHERE" suffixOverrides="AND">
<if test="pull.userId != null">
user_id = #{pull.userId} AND
</if>
<if test="pull.merchantId != null">
merchant_id = #{pull.merchantId} AND
</if>
<if test="pull.direction == 'forward' and pull.chatRecordId != null">
--id小于传入id
id &lt; #{pull.chatRecordId}
</if>
<if test="pull.direction == 'backward' and pull.chatRecordId != null">
--id大于传入id
id &gt; #{pull.chatRecordId}
</if>
</trim>
</select>
<select id="getLatestRecord" resultType="com.ym.im.entity.ChatRecord">
SELECT
<include refid="Base_Column_List"/>
FROM chat_record_${index}
where
user_id = #{userId}
and merchant_id = #{merchantId}
order by send_time desc
limit 1
</select>
</mapper>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ym.im.mapper.SessionListMapper">
</mapper>
<?xml version="1.0" encoding="UTF-8"?>
<!--
This is the JRebel configuration file. It maps the running application to your IDE workspace, enabling JRebel reloading for this project.
Refer to https://manuals.zeroturnaround.com/jrebel/standalone/config.html for more information.
-->
<application generated-by="intellij" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.zeroturnaround.com" xsi:schemaLocation="http://www.zeroturnaround.com http://update.zeroturnaround.com/jrebel/rebel-2_1.xsd">
<classpath>
<dir name="/Users/JJww/Documents/Workspaces/Jumeirah/customer-service/target/classes">
</dir>
</classpath>
</application>
error.top_level_error=Please contact the developer!
error.bad_parameter=Bad parameter!
error.chat_id_direction=Direction can only be forward or backward, and ignore case!
error.chat_id_greater_than_zero=The chat id should be greater than zero!
error.chat_order_by_empty=orderBy can't be empty!
error.chat_page_num=The page number must be greater than zero!
error.chat_page_size=The page size must be greater than zero!
error.chat_sorting=Sorting can only be asc or desc, and ignore case!
error.missing_body=Required request body is missing!
error.system=Error number:[{0}],Internal server error!
error.user_id_greater_than_zero=The user id should be greater than zero!
error.user_id_empty=User id cannot be empty!
error.chat_id_empty=Chat id cannot be empty!
error.staff_id_empty=Staff id cannot be empty!
error.staff_id_greater_than_zero=The staff id should be greater than zero!
error.chat_msg_type_empty=MsgType id cannot be empty!
error.chat_send_receive=sendReceive can only be 0(send) or 1(receive)!
error.chat_scope=Scope can only be 0(all) or 1(air tickets) or 2(hotel)!
error.chat_receive_time_empty=receiveTime cant not be empty!
error.chat_send_time_empty=sendTime cant not be empty!
error.chat_create_time_empty=createTime cant not be empty!
error.chat_modify_time_empty=modifyTime cant not be empty!
error.msg_body_data_empty=MsgBody data cant not be null!
error.msg_body_status_empty=MsgBody status cant not be empty!
error.service.user=Wrong service user!
\ No newline at end of file
error.chat_id_empty=\u804A\u5929Id\u4E0D\u53EF\u4E3A\u7A7A\uFF01
error.top_level_error=\u8BF7\u8054\u7CFB\u5F00\u53D1\u4EBA\u5458\uFF01
error.bad_parameter=\u53C2\u6570\u9519\u8BEF\uFF01
error.chat_id_direction=direction\u53EA\u80FD\u4E3Aforward\u6216backward\uFF0C\u5E76\u5FFD\u7565\u5927\u5C0F\u5199\uFF01
error.chat_id_greater_than_zero=\u804A\u5929\u8BB0\u5F55Id\u5FC5\u987B\u5927\u4E8E0\uFF01
error.chat_order_by_empty=orderBy\u4E0D\u53EF\u4E3A\u7A7A\uFF01
error.chat_page_num=\u9875\u7801\u5FC5\u987B\u5927\u4E8E0\uFF01
error.chat_page_size=\u5206\u9875\u5927\u5C0F\u5FC5\u987B\u5927\u4E8E0\uFF01
error.chat_sorting=sorting\u53EA\u80FD\u4E3Aasc\u6216desc\uFF0C\u5E76\u5FFD\u7565\u5927\u5C0F\u5199\uFF01
error.missing_body=\u7F3A\u5C11\u8BF7\u6C42\u4FE1\u606F\uFF01
error.system=\u9519\u8BEF\u7F16\u53F7\uFF1A[{0}]\uFF0C\u670D\u52A1\u5668\u5185\u90E8\u5F02\u5E38\uFF01
error.user_id_greater_than_zero=\u7528\u6237Id\u5FC5\u987B\u5927\u4E8E0\uFF01
error.user_id_empty=\u7528\u6237Id\u4E0D\u53EF\u4E3A\u7A7A\uFF01
error.staff_id_empty=\u5BA2\u670DId\u4E0D\u53EF\u4E3A\u7A7A\uFF01
error.staff_id_greater_than_zero=\u5BA2\u670DId\u5FC5\u987B\u5927\u4E8E0\uFF01
error.chat_msg_type_empty=\u6D88\u606F\u7C7B\u578B\u4E0D\u53EF\u4E3A\u7A7A\uFF01
error.chat_send_receive=\u6536\u6216\u53D1\u53EA\u80FD\u4E3A0\uFF08\u53D1\u9001\uFF09\u62161\uFF08\u63A5\u6536\uFF09\uFF01
error.chat_scope=\u5185\u5BB9\u8303\u56F4\u53EA\u80FD\u4E3A0\uFF08\u6240\u6709\uFF09\u30011\uFF08\u673A\u7968\uFF09\u30012\uFF08\u9152\u5E97\uFF09\uFF01
error.chat_receive_time_empty=\u63A5\u6536\u65F6\u95F4\u4E0D\u53EF\u4E3A\u7A7A\uFF01
error.chat_send_time_empty=\u53D1\u9001\u65F6\u95F4\u4E0D\u53EF\u4E3A\u7A7A\uFF01
error.chat_create_time_empty=\u521B\u5EFA\u65F6\u95F4\u4E0D\u53EF\u4E3A\u7A7A\uFF01
error.chat_modify_time_empty=\u4FEE\u6539\u65F6\u95F4\u4E0D\u53EF\u4E3A\u7A7A\uFF01
error.msg_body_data_empty=MsgBody data\u4E0D\u53EF\u4E3A\u7A7A\uFF01
error.msg_body_status_empty=MsgBody status\u4E0D\u53EF\u4E3A\u7A7A\uFF01
error.service.user=\u8BE5\u7528\u6237\u4E0D\u5728\u670D\u52A1\u5217\u8868\u5185\uFF01
\ No newline at end of file
---
#开发环境
spring:
datasource:
# 数据库连接池配置
druid:
# 初始化时建立物理连接的个数
initial-size: 5
# 最大连接池数量
max-active: 10
# 用来检测连接是否有效的sql语句
validation-query: SELECT 1 FROM DUAL
# 最小连接池数量
min-idle: 5
# 获取连接时最大等待时间,单位毫秒
max-wait: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位毫秒
time-between-eviction-runs-millis: 60000
url: jdbc:mysql://47.99.47.225:3306/pathfinder_im?useUnicode=true&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: temple123456
driver-class-name: com.mysql.cj.jdbc.Driver
jackson:
default-property-inclusion: non_null
rabbitmq:
host: 127.0.0.1
port: 5672
username: admin
password: admin
delay-queue-name: delay.ack.dev
staff-offline-Queue-Name: staff.offline.dev
exchange-name: delay.exchange.dev
listener:
simple:
default-requeue-rejected: false
redis:
database: 5
host: 127.0.0.1
password:
port: 6379
# (重要!)设置mvc默认语言为zh_CN,默认语言必须为static.i18n目录下有的语言配置文件,否则跟随服务器语言
mvc:
locale: zh_CN
devtools:
restart:
log-condition-evaluation-delta: false
---
#生产环境
spring:
datasource:
# 数据库连接池配置
druid:
# 初始化时建立物理连接的个数
initial-size: 5
# 最大连接池数量
max-active: 10
# 用来检测连接是否有效的sql语句
validation-query: SELECT 1 FROM DUAL
# 最小连接池数量
min-idle: 5
# 获取连接时最大等待时间,单位毫秒
max-wait: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位毫秒
time-between-eviction-runs-millis: 60000
url: jdbc:mysql://172.31.33.14:3306/pathfinder_im?useUnicode=true&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: Yum123456
driver-class-name: com.mysql.cj.jdbc.Driver
jackson:
default-property-inclusion: non_null
rabbitmq:
host: 172.31.33.14
port: 5672
username: admin
password: Yum123456
delay-queue-name: delay.ack
staff-offline-Queue-Name: staff.offline
exchange-name: delayAck
listener:
simple:
default-requeue-rejected: false
redis:
database: 5
host: 172.31.33.14
password: Yum123456
port: 6379
# (重要!)设置mvc默认语言为zh_CN,默认语言必须为static.i18n目录下有的语言配置文件,否则跟随服务器语言
mvc:
locale: zh_CN
devtools:
restart:
log-condition-evaluation-delta: false
#日志配置
logging:
file: logs/${spring.application.name}.log
eureka:
client:
service-url:
defaultZone: http://172.31.41.108:20000/eureka/
instance:
prefer-ip-address: true
\ No newline at end of file
---
#开发环境
spring:
datasource:
# 数据库连接池配置
druid:
# 初始化时建立物理连接的个数
initial-size: 5
# 最大连接池数量
max-active: 10
# 用来检测连接是否有效的sql语句
validation-query: SELECT 1 FROM DUAL
# 最小连接池数量
min-idle: 5
# 获取连接时最大等待时间,单位毫秒
max-wait: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位毫秒
time-between-eviction-runs-millis: 60000
url: jdbc:mysql://192.168.1.237:3306/pathfinder_im?useUnicode=true&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
jackson:
default-property-inclusion: non_null
rabbitmq:
host: 127.0.0.1
port: 5672
username: admin
password: admin
delay-queue-name: delay.ack.dev
staff-offline-Queue-Name: staff.offline.dev
exchange-name: delay.exchange.dev
listener:
simple:
default-requeue-rejected: false
redis:
database: 5
host: 127.0.0.1
password:
port: 6379
# (重要!)设置mvc默认语言为zh_CN,默认语言必须为static.i18n目录下有的语言配置文件,否则跟随服务器语言
mvc:
locale: zh_CN
devtools:
restart:
log-condition-evaluation-delta: false
spring:
profiles:
active: dev
application:
name: customer-service
# 404抛出异常
mvc:
throw-exception-if-no-handler-found: true
resources:
add-mappings: false
server:
port: 20002
# 关闭tomcat自带/error页面
error:
whitelabel:
enabled: false
netty:
port: 9095
#日志配置
logging:
level:
root: info
com.ym.im.mapper: debug
pagehelper:
helper-dialect: mysql
reasonable: true
support-methods-arguments: ture
params: count=contSql
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment