add websocket

This commit is contained in:
Donghuang 2022-05-31 10:37:50 +08:00
parent 2b11c0b496
commit 799a5f6b1f
14 changed files with 410 additions and 274 deletions

View File

@ -34,6 +34,14 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>

View File

@ -1,10 +1,14 @@
package com.pudonghot.ambition.crm;
import lombok.val;
import org.springframework.boot.SpringApplication;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
/**
* @author Shaun Chyxion <br>
@ -12,6 +16,7 @@ import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
* Mar 05, 2017 14:40:30
*/
@EnableAsync
@EnableScheduling
@EnableAspectJAutoProxy(proxyTargetClass = true)
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class AmbitionCRM {
@ -22,4 +27,14 @@ public class AmbitionCRM {
public static void main(String[] args) {
SpringApplication.run(AmbitionCRM.class, args);
}
@Bean("exportTaskExecutor")
public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
val threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
threadPoolTaskExecutor.setCorePoolSize(2);
threadPoolTaskExecutor.setMaxPoolSize(8);
threadPoolTaskExecutor.setKeepAliveSeconds(16);
threadPoolTaskExecutor.setQueueCapacity(64);
return threadPoolTaskExecutor;
}
}

View File

@ -1,9 +1,10 @@
package com.pudonghot.ambition.crm.service.support;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import org.springframework.core.task.TaskExecutor;
import com.pudonghot.ambition.crm.service.ExportTaskService;
import org.springframework.beans.factory.annotation.Autowired;
/**
* @author Donghuang
@ -12,7 +13,8 @@ import com.pudonghot.ambition.crm.service.ExportTaskService;
@Service
public class ExportTaskServiceSupport implements ExportTaskService {
@Autowired
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
@Qualifier("exportTaskExecutor")
private TaskExecutor threadPoolTaskExecutor;
/**
* {@inheritDoc}

View File

@ -0,0 +1,33 @@
package com.pudonghot.ambition.crm.ws.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
/**
* @author Donghuang
* @date Oct 12, 2021 15:31:50
*/
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
/**
* {@inheritDoc}
*/
@Override
public void configureMessageBroker(final MessageBrokerRegistry config) {
config.setApplicationDestinationPrefixes("/ws");
config.enableSimpleBroker("/topic");
}
/**
* {@inheritDoc}
*/
@Override
public void registerStompEndpoints(final StompEndpointRegistry registry) {
registry.addEndpoint("/stomp").withSockJS();
}
}

View File

@ -0,0 +1,21 @@
package com.pudonghot.ambition.crm.ws.service;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.NotBlank;
import org.springframework.validation.annotation.Validated;
/**
* @author Donghuang
* @date May 30, 2022 15:56:24
*/
@Validated
public interface WebSocketService {
/**
* publish websocket message
*
* @param employeeKey
* @param payload
*/
void publish(@NotBlank String employeeKey, @NotNull Object payload);
}

View File

@ -0,0 +1,25 @@
package com.pudonghot.ambition.crm.ws.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
import com.pudonghot.ambition.crm.ws.service.WebSocketService;
import org.springframework.messaging.simp.SimpMessagingTemplate;
/**
* @author Donghuang
* @date May 30, 2022 19:07:22
*/
@Slf4j
@Service
public class WebSocketServiceImpl implements WebSocketService {
@Autowired
private SimpMessagingTemplate message;
public void publish(final String employeeKey,
final Object payload) {
log.debug("Publish websocket message [{}] -> [{}].", employeeKey, payload);
message.convertAndSend("/topic/" + employeeKey, payload);
}
}

View File

@ -21,9 +21,6 @@
</RollingFile>
</Appenders>
<Loggers>
<Logger name="org.springframework" level="INFO" additivity="false">
<AppenderRef ref="File" />
</Logger>
<Logger name="org.apache" level="WARN" additivity="false">
<AppenderRef ref="File" />
</Logger>

View File

@ -10,8 +10,8 @@
<PatternLayout pattern="${pattern}" />
</Console>
<RollingFile name="File"
fileName="${log.dir}/${project.artifactId}.log"
filePattern="${log.dir}/$${date:yyyy-MM}/${project.artifactId}-%d{yyyy-MM-dd}-%i.log.gz">
fileName="${log.dir}/app.log"
filePattern="${log.dir}/$${date:yyyy-MM}/app-%d{yyyy-MM-dd}-%i.log.gz">
<PatternLayout pattern="${pattern}" />
<Policies>
<TimeBasedTriggeringPolicy />

View File

@ -10,12 +10,12 @@
<PatternLayout pattern="${pattern}" />
</Console>
<RollingFile name="File"
fileName="${log.dir}/${project.artifactId}.log"
filePattern="${log.dir}/$${date:yyyy-MM}/${project.artifactId}-%d{yyyy-MM-dd}-%i.log.gz">
fileName="${log.dir}/app.log"
filePattern="${log.dir}/$${date:yyyy-MM}/app-%d{yyyy-MM-dd}-%i.log.gz">
<PatternLayout pattern="${pattern}" />
<Policies>
<TimeBasedTriggeringPolicy />
<SizeBasedTriggeringPolicy size="16 MB" />
<SizeBasedTriggeringPolicy size="64 MB" />
</Policies>
<DefaultRolloverStrategy max="32" />
</RollingFile>

View File

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--<bean class="com.alibaba.druid.pool.DruidDataSource"-->
<!--init-method="init" -->
<!--destroy-method="close" -->
<!--p:url="${db.url}"-->
<!--p:username="${db.user}" -->
<!--p:password="${db.password}" />-->
</beans>

View File

@ -1,25 +1,27 @@
import Ember from 'ember';
export default Ember.Route.extend({
activate() {
this.controllerFor('application').set('login', true);
},
deactivate() {
this.controllerFor('application').set('login', false);
},
actions: {
doLogin(model) {
let me = this;
me.get('ajax').doPost('auth/login', model,
function(user) {
Ember.Logger.debug(`User ${user} Loggedin`);
me.set('ajax.user', user);
me.message.alert('Sign in successfully');
// me.transitionTo('/');
window.location.href = '/';
}, function(msg) {
me.get('message').warn(msg);
});
}
}
});
import Ember from 'ember';
import $ from 'jquery';
export default Ember.Route.extend({
activate() {
this.controllerFor('application').set('login', true);
},
deactivate() {
this.controllerFor('application').set('login', false);
},
actions: {
doLogin(model) {
let me = this;
me.get('ajax').doPost('auth/login', model,
function(user) {
Ember.Logger.debug(`User ${user} Loggedin`);
me.set('ajax.user', user);
me.message.alert('Sign in successfully');
// me.transitionTo('/');
window.location.href = '/';
$.trigger('LOGINED', true);
}, function(msg) {
me.get('message').warn(msg);
});
}
}
});

View File

@ -1,167 +1,169 @@
import Ember from 'ember';
export default Ember.Service.extend({
/**
* Ajax.doGet('url.get',
* {id: 001, name: 'foo'},
* function(data) {
* // success
* }, function(msg) {
* // fail
* });
* no params
* Ajax.doGet('url.get',
* function(data) {
* // success
* }, function(msg){
* // fail
* });
* @param {Boolean} [loadmask], option
* @param {String} url
* @param {Object} [params]
* @param {Function} [fnSucc]
* @param {Function} [fnFail]
*/
doGet() {
this._process_request(arguments, 'GET');
},
/**
* @see #get
*/
doPost() {
this._process_request(arguments, 'POST');
},
/**
* @see #get
*/
doPut() {
this._process_request(arguments, 'PUT');
},
/**
* @see #get
*/
doDel() {
this._process_request(arguments, 'DELETE');
},
/**
* private
*/
_request(p) {
var me = this;
p.loadmask && me.set('loading', true);
var formDataCfg;
if (p.params instanceof FormData) {
formDataCfg = false;
}
Ember.$.ajax({
url: p.url,
data: p.params,
type: (/get/i).test(p.method) ? 'GET' : 'POST',
contentType: formDataCfg,
processData: formDataCfg,
success(r, status, resp) {
if (r.success) {
// ignore success callback
if (p.fnSucc !== false) {
let keys = Object.keys(r);
me._isFunc(p.fnSucc) ?
p.fnSucc(keys.length === 3 &&
keys.includes('data') ? r.data : r) :
me.message.alert(r.data || 'Ajax Request Successfully');
}
}
else if (r.errcode === 4011) {
Ember.getOwner(me).lookup('route:application').transitionTo('login');
}
// ignore fail callback
else if (p.fnFail !== false) {
me._isFunc(p.fnFail) ? p.fnFail(r.errmsg, r) :
me.dialog.error(r.errmsg || 'Ajax Request Error Caused');
}
else {
me.dialog.error(r.errmsg || 'Ajax Request Error Caused');
}
p.loadmask && me.set('loading', false);
},
error(jqXHR, textStatus) {
Ember.Logger.error('Ajax Request Error Caused', arguments);
if (textStatus === 'timeout') {
me.dialog.error('Ajax Request Timeout Error Caused.');
}
else if (textStatus === 'abort') {
me.dialog.error('Ajax Request Client Abort Error Caused.');
}
else {
me.dialog.error('Ajax Request Error Caused.');
}
p.loadmask && me.set('loading', false);
}
});
},
/**
* {
* [lm], [url], [params], [fnSucc], [fnFail]
* }
*/
_process_request(args, method) {
var me = this;
if (args && args.length) {
// args to array
args = Array.prototype.slice.call(args);
var lm = args.shift(), // loadmask
url,
p,
fnSucc,
fnFail;
// loadmask
if (Ember.$.type(lm) === 'boolean') {
url = args.shift();
}
else {
url = lm;
// default loadmask
lm = true;
}
me._parse_params_and_callbacks(args);
// params
p = args.shift();
// callbacks
fnSucc = args.shift();
fnFail = args.shift();
// Ajax Request
me._request({
loadmask: lm,
method: method || 'GET',
url: url,
params: p,
fnSucc: fnSucc,
fnFail: fnFail
});
}
else {
console.error('Ajax No Args Given.');
}
},
/**
* params, fnSucc, fnFail
* fnSucc, fnFail
* fnFail
*/
_parse_params_and_callbacks(args) {
// has callbacks
if (this._isFunc(args[1]) && this._isFunc(args[2])) {
this._isFunc(args[0]) && (args[0] = args[0]());
}
// no params, params is success callback
else if (this._isFunc(args[0])) {
args[2] = args[1];
args[1] = args[0];
args[0] = null;
}
},
_isFunc(f) {
return Ember.$.type(f) === 'function';
}
});
import Ember from 'ember';
import $ from 'jquery';
export default Ember.Service.extend({
/**
* Ajax.doGet('url.get',
* {id: 001, name: 'foo'},
* function(data) {
* // success
* }, function(msg) {
* // fail
* });
* no params
* Ajax.doGet('url.get',
* function(data) {
* // success
* }, function(msg){
* // fail
* });
* @param {Boolean} [loadmask], option
* @param {String} url
* @param {Object} [params]
* @param {Function} [fnSucc]
* @param {Function} [fnFail]
*/
doGet() {
this._process_request(arguments, 'GET');
},
/**
* @see #get
*/
doPost() {
this._process_request(arguments, 'POST');
},
/**
* @see #get
*/
doPut() {
this._process_request(arguments, 'PUT');
},
/**
* @see #get
*/
doDel() {
this._process_request(arguments, 'DELETE');
},
/**
* private
*/
_request(p) {
var me = this;
p.loadmask && me.set('loading', true);
var formDataCfg;
if (p.params instanceof FormData) {
formDataCfg = false;
}
$.ajax({
url: p.url,
data: p.params,
type: (/get/i).test(p.method) ? 'GET' : 'POST',
contentType: formDataCfg,
processData: formDataCfg,
success(r, status, resp) {
if (r.success) {
// ignore success callback
if (p.fnSucc !== false) {
let keys = Object.keys(r);
me._isFunc(p.fnSucc) ?
p.fnSucc(keys.length === 3 &&
keys.includes('data') ? r.data : r) :
me.message.alert(r.data || 'Ajax Request Successfully');
}
}
else if (r.errcode === 4011) {
$.trigger('NOT_LOGIN', true);
Ember.getOwner(me).lookup('route:application').transitionTo('login');
}
// ignore fail callback
else if (p.fnFail !== false) {
me._isFunc(p.fnFail) ? p.fnFail(r.errmsg, r) :
me.dialog.error(r.errmsg || 'Ajax Request Error Caused');
}
else {
me.dialog.error(r.errmsg || 'Ajax Request Error Caused');
}
p.loadmask && me.set('loading', false);
},
error(jqXHR, textStatus) {
Ember.Logger.error('Ajax Request Error Caused', arguments);
if (textStatus === 'timeout') {
me.dialog.error('Ajax Request Timeout Error Caused.');
}
else if (textStatus === 'abort') {
me.dialog.error('Ajax Request Client Abort Error Caused.');
}
else {
me.dialog.error('Ajax Request Error Caused.');
}
p.loadmask && me.set('loading', false);
}
});
},
/**
* {
* [lm], [url], [params], [fnSucc], [fnFail]
* }
*/
_process_request(args, method) {
var me = this;
if (args && args.length) {
// args to array
args = Array.prototype.slice.call(args);
var lm = args.shift(), // loadmask
url,
p,
fnSucc,
fnFail;
// loadmask
if ($.type(lm) === 'boolean') {
url = args.shift();
}
else {
url = lm;
// default loadmask
lm = true;
}
me._parse_params_and_callbacks(args);
// params
p = args.shift();
// callbacks
fnSucc = args.shift();
fnFail = args.shift();
// Ajax Request
me._request({
loadmask: lm,
method: method || 'GET',
url: url,
params: p,
fnSucc: fnSucc,
fnFail: fnFail
});
}
else {
console.error('Ajax No Args Given.');
}
},
/**
* params, fnSucc, fnFail
* fnSucc, fnFail
* fnFail
*/
_parse_params_and_callbacks(args) {
// has callbacks
if (this._isFunc(args[1]) && this._isFunc(args[2])) {
this._isFunc(args[0]) && (args[0] = args[0]());
}
// no params, params is success callback
else if (this._isFunc(args[0])) {
args[2] = args[1];
args[1] = args[0];
args[0] = null;
}
},
_isFunc(f) {
return $.type(f) === 'function';
}
});

View File

@ -0,0 +1,42 @@
import Service from '@ember/service';
import { aliasMethod } from '@ember/object';
import $ from 'jquery';
export default Service.extend({
connect() {
let me = this;
if (me.get('connected')) {
Ember.Logger.info('Websocket is connected, reconnect.');
me.disconnect();
}
// connect
if (!me.get('stompClient')) {
me.set('stompClient', Stomp.over(new SockJS('/stomp')));
}
me.get('stompClient').connect({}, function() {
Ember.Logger.info('Connect websocket.');
me.stompClient.subscribe('/topic/websocket', function(msg) {
Ember.Logger.info('On websocket message: ', msg);
$.trigger('websocket', JSON.parse(msg.body));
});
me.set('connected', true);
Ember.Logger.info('Websocket connected.');
});
},
willDestroy() {
var me = this;
me._super(...arguments);
if (me.get('connected') && me.get('stompClient')) {
me.get('stompClient').disconnect();
me.set('connected', false);
Ember.Logger.info('Websocket disconnected.');
}
else {
Ember.Logger.info('Websocket is not connected, ignore disconnect.');
}
},
disconnect: aliasMethod('willDestroy')
});

View File

@ -1,56 +1,59 @@
{
"name": "ambition-crm",
"version": "0.0.1",
"description": "Ambition CRM",
"private": true,
"directories": {
"doc": "doc",
"test": "tests"
},
"scripts": {
"build": "ember build",
"start": "ember server",
"test": "ember test"
},
"repository": "",
"author": "Shaun Chyxion",
"license": "MIT",
"devDependencies": {
"broccoli-asset-rev": "^2.4.5",
"broccoli-funnel": "^1.0.1",
"ember-ajax": "^3.0.0",
"ember-browserify": "^1.1.11",
"ember-cli": "~2.14.2",
"ember-cli-app-version": "^3.0.0",
"ember-cli-babel": "^6.3.0",
"ember-cli-dependency-checker": "^1.3.0",
"ember-cli-eslint": "^3.0.0",
"ember-cli-htmlbars": "^2.0.1",
"ember-cli-htmlbars-inline-precompile": "^0.4.3",
"ember-cli-inject-live-reload": "^1.4.1",
"ember-cli-jshint": "^1.0.0",
"ember-cli-less": "^1.5.5",
"ember-cli-moment-shim": "^1.2.0",
"ember-cli-qunit": "^4.0.0",
"ember-cli-release": "0.2.8",
"ember-cli-shims": "^1.1.0",
"ember-cli-sri": "^2.1.0",
"ember-cli-uglify": "^1.2.0",
"ember-export-application-global": "^2.0.0",
"ember-load-initializers": "^1.0.0",
"ember-moment": "^6.1.0",
"ember-radio-button": "1.1.1",
"ember-resolver": "^4.0.0",
"ember-route-action-helper": "2.0.3",
"ember-source": "~2.16.0",
"ember-truth-helpers": "1.1.0",
"emberx-select": "3.0.1",
"glob": "^4.5.3",
"loader.js": "^4.2.3",
"morgan": "^1.7.0"
},
"dependencies": {},
"engines": {
"node": "^4.5 || 6.* || >= 7.*"
}
}
{
"name": "ambition-crm",
"version": "0.0.1",
"description": "Ambition CRM",
"private": true,
"directories": {
"doc": "doc",
"test": "tests"
},
"scripts": {
"build": "ember build",
"start": "ember server",
"test": "ember test"
},
"repository": "",
"author": "Shaun Chyxion",
"license": "MIT",
"devDependencies": {
"broccoli-asset-rev": "^2.4.5",
"broccoli-funnel": "^1.0.1",
"ember-ajax": "^3.0.0",
"ember-browserify": "^1.1.11",
"ember-cli": "~2.14.2",
"ember-cli-app-version": "^3.0.0",
"ember-cli-babel": "^6.3.0",
"ember-cli-dependency-checker": "^1.3.0",
"ember-cli-eslint": "^3.0.0",
"ember-cli-htmlbars": "^2.0.1",
"ember-cli-htmlbars-inline-precompile": "^0.4.3",
"ember-cli-inject-live-reload": "^1.4.1",
"ember-cli-jshint": "^1.0.0",
"ember-cli-less": "^1.5.5",
"ember-cli-moment-shim": "^1.2.0",
"ember-cli-qunit": "^4.0.0",
"ember-cli-release": "0.2.8",
"ember-cli-shims": "^1.1.0",
"ember-cli-sri": "^2.1.0",
"ember-cli-uglify": "^1.2.0",
"ember-export-application-global": "^2.0.0",
"ember-load-initializers": "^1.0.0",
"ember-moment": "^6.1.0",
"ember-radio-button": "1.1.1",
"ember-resolver": "^4.0.0",
"ember-route-action-helper": "2.0.3",
"ember-source": "~2.16.0",
"ember-truth-helpers": "1.1.0",
"emberx-select": "3.0.1",
"glob": "^4.5.3",
"loader.js": "^4.2.3",
"morgan": "^1.7.0"
},
"dependencies": {
"sockjs-client": "^1.6.1",
"stomp-websocket": "^2.3.4-next"
},
"engines": {
"node": "^4.5 || 6.* || >= 7.*"
}
}