├── .github ├── CODE_OF_CONDUCT.md └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── LICENSE ├── README.md ├── pom.xml └── src └── main ├── java └── com │ └── github │ └── coco │ ├── AbstractConnection.java │ ├── CocoApplication.java │ ├── CustomerConnection.java │ ├── GlobalState.java │ ├── ServiceConnection.java │ ├── config │ ├── GlobalConfig.java │ └── WebSocketConfig.java │ ├── controller │ ├── CustomerController.java │ └── ServiceController.java │ ├── pojo │ ├── AbstractResponseData.java │ ├── CustomerResponseData.java │ └── ServiceResponseData.java │ ├── util │ ├── JSONEncoder.java │ └── JwtUtil.java │ └── vo │ └── StatisticsVO.java └── resources ├── application.yml ├── banner.txt └── static ├── customer.html ├── js ├── coco.common.js ├── coco.customer.js └── coco.service.js └── service.html /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at 448099205@qq.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | src/test 2 | .idea 3 | *.iml 4 | out 5 | target -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Da Bing 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # coco 2 | 3 | [![License](https://img.shields.io/apm/l/vim-mode.svg)](https://github.com/db1995/anole/blob/master/LICENSE) 4 | 5 | > *English intro:* 6 | 7 | ## Tips 8 | The project is being refactored with Vue and Element(instead of Bootstrap). 9 | The project is still under development, you can view the [Projects](https://github.com/db1995/coco/projects) for details. 10 | If you have any suggestions, please submit an [Issue](https://github.com/db1995/coco/issues). 11 | 12 | ## Purpose 13 | Build web online customer service system easily and quickly. 14 | 15 | ## Features 16 | * Rich functionality 17 | * Highly flexible 18 | 19 | ## Quickstart 20 | See [Quickstart] for details in Wiki. 21 | 22 | **** 23 | 24 | > *中文简介:* 25 | 26 | ## 提示 27 | 该项目正在用Vue和Element重构中(取代Bootstrap)。 28 | 该项目仍在开发中,你可以查看[Projects](https://github.com/db1995/coco/projects)以了解详细信息。 29 | 如果你有任何建议,请提交[Issue](https://github.com/db1995/coco/issues)。 30 | 31 | ## 宗旨 32 | 便捷快速地构建网页在线客服系统。 33 | 34 | ## 特性 35 | * 丰富的功能 36 | * 高度灵活 37 | 38 | ### 快速开始 39 | 在Wiki中查看[快速开始](https://github.com/db1995/coco/wiki/主页)。 40 | 41 | **** 42 | 43 | ![coco-overview](https://github.com/db1995/images/blob/master/coco-overview.png) 44 | ![coco-statistics](https://github.com/db1995/images/blob/master/coco-statistics.png) 45 | ![coco-dialog-service](https://github.com/db1995/images/blob/master/coco-dialog-service.png) 46 | ![coco-dialog-customer](https://github.com/db1995/images/blob/master/coco-dialog-customer.png) 47 | ![coco-before-1](https://github.com/db1995/images/blob/master/coco-before-1.png) 48 | ![coco-before-2](https://github.com/db1995/images/blob/master/coco-before-2.png) 49 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.1.9.RELEASE 9 | 10 | 11 | com.github 12 | coco 13 | 0.2.0-alpha 14 | coco 15 | Web online customer service 16 | 17 | 18 | 1.8 19 | 20 | 21 | 22 | 23 | org.springframework.boot 24 | spring-boot-starter-web 25 | 26 | 27 | org.springframework.boot 28 | spring-boot-starter-websocket 29 | 30 | 31 | org.springframework.boot 32 | spring-boot-configuration-processor 33 | true 34 | 35 | 36 | com.alibaba 37 | fastjson 38 | 1.2.62 39 | 40 | 41 | io.jsonwebtoken 42 | jjwt 43 | 0.9.1 44 | 45 | 46 | javax.xml.bind 47 | jaxb-api 48 | 2.3.1 49 | 50 | 51 | 52 | 53 | 54 | 55 | org.springframework.boot 56 | spring-boot-maven-plugin 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /src/main/java/com/github/coco/AbstractConnection.java: -------------------------------------------------------------------------------- 1 | package com.github.coco; 2 | 3 | import javax.websocket.EncodeException; 4 | import javax.websocket.Session; 5 | import java.io.IOException; 6 | import java.util.Collection; 7 | 8 | /** 9 | * @author db1995 10 | */ 11 | public abstract class AbstractConnection { 12 | protected String id; 13 | protected Session session; 14 | protected long createTime = System.currentTimeMillis(); 15 | 16 | /** 17 | * Invoke when connection open 18 | * 19 | * @param session 20 | * @param token Authentication token, if any 21 | * @throws IOException 22 | */ 23 | protected abstract void onOpen(Session session, String token) throws IOException, EncodeException; 24 | 25 | /** 26 | * Invoke when receive message 27 | * 28 | * @param message Received message 29 | */ 30 | protected abstract void onMessage(Session session, String message) throws IOException, EncodeException; 31 | 32 | /** 33 | * Invoke when connection closed 34 | */ 35 | protected abstract void onClose() throws IOException, EncodeException; 36 | 37 | /** 38 | * Invoke when connection closed 39 | */ 40 | protected abstract void onError(Throwable error, Session session) throws IOException, EncodeException; 41 | 42 | /** 43 | * Send message to the session of this connection 44 | * 45 | * @param message The message you want to push 46 | * @throws IOException 47 | */ 48 | protected void sendAndReceiveMessage(String message) throws IOException { 49 | session.getBasicRemote().sendText(message); 50 | } 51 | 52 | /** 53 | * Notify some targets 54 | * 55 | * @param jsonMessage 56 | * @param targets The clients you want to notify 57 | */ 58 | protected void notifyClients(String jsonMessage, Collection... targets) { 59 | for (Collection collection : targets) { 60 | collection.forEach(c -> { 61 | try { 62 | c.sendAndReceiveMessage(jsonMessage); 63 | } catch (IOException e) { 64 | e.printStackTrace(); 65 | } 66 | }); 67 | } 68 | } 69 | 70 | public long getCreateTime() { 71 | return createTime; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/com/github/coco/CocoApplication.java: -------------------------------------------------------------------------------- 1 | package com.github.coco; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | /** 7 | * Main class 8 | * 9 | * @author db1995 10 | */ 11 | @SpringBootApplication 12 | public class CocoApplication { 13 | public static void main(String[] args) { 14 | SpringApplication.run(CocoApplication.class, args); 15 | } 16 | } -------------------------------------------------------------------------------- /src/main/java/com/github/coco/CustomerConnection.java: -------------------------------------------------------------------------------- 1 | package com.github.coco; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.github.coco.pojo.AbstractResponseData; 5 | import com.github.coco.pojo.CustomerResponseData; 6 | import com.github.coco.pojo.ServiceResponseData; 7 | import com.github.coco.util.JSONEncoder; 8 | import org.springframework.stereotype.Component; 9 | 10 | import javax.websocket.*; 11 | import javax.websocket.server.PathParam; 12 | import javax.websocket.server.ServerEndpoint; 13 | import java.io.EOFException; 14 | import java.io.IOException; 15 | 16 | import static com.github.coco.GlobalState.*; 17 | import static com.github.coco.config.GlobalConfig.*; 18 | import static com.github.coco.pojo.AbstractResponseData.Type; 19 | 20 | /** 21 | * WebSocket connection of customer 22 | * 23 | * @author db1995 24 | */ 25 | @ServerEndpoint(value = "/customer/{id}", encoders = JSONEncoder.class) 26 | @Component 27 | public class CustomerConnection extends AbstractConnection { 28 | private ServiceConnection serviceConnection; 29 | 30 | @OnOpen 31 | @Override 32 | public synchronized void onOpen(Session session, @PathParam("id") String id) throws IOException, EncodeException { 33 | // Initial 34 | this.id = id; 35 | this.session = session; 36 | session.setMaxIdleTimeout(600000); 37 | CUSTOMER_MAP.put(this.id, this); 38 | 39 | if (SERVICE_QUEUE.size() > 0) { // 如果有空闲的客服 40 | ServiceConnection sc = SERVICE_QUEUE.element(); 41 | this.serviceConnection = sc; 42 | sc.getCustomerConnectionMap().put(this.id, this); 43 | if (sc.getCustomerConnectionMap().size() == getMaxCustomerPerService()) { 44 | SERVICE_QUEUE.poll(); 45 | } 46 | this.session.getBasicRemote().sendObject(new CustomerResponseData(Type.START_SERVICE, isAlwaysDisplayUnifiedServiceName() ? getUnifiedServiceName() : sc.getServiceName(), getWelcome())); 47 | this.serviceConnection.session.getBasicRemote().sendObject(new ServiceResponseData(this.id, Type.START_SERVICE)); 48 | } else if (SERVICE_MAP.size() == 0) { // 没有客服在上班 49 | CUSTOMER_QUEUE.add(this); 50 | this.session.getBasicRemote().sendObject(new CustomerResponseData(Type.MESSAGE, getUnifiedServiceName(), getAutoReplyAfterWork())); 51 | } else { // 如果有客服在线,但服务人数已满 52 | CUSTOMER_QUEUE.add(this); 53 | this.session.getBasicRemote().sendObject(new CustomerResponseData(Type.WAIT_SERVICE, getUnifiedServiceName(), getWelcome())); 54 | // 通知所有客服,等待人数+1 55 | SERVICE_MAP.forEach((k, s) -> { 56 | try { 57 | s.session.getBasicRemote().sendObject(new ServiceResponseData(Type.WAIT_SERVICE)); 58 | } catch (IOException e) { 59 | e.printStackTrace(); 60 | } catch (EncodeException e) { 61 | e.printStackTrace(); 62 | } 63 | }); 64 | } 65 | } 66 | 67 | @OnMessage 68 | @Override 69 | public void onMessage(Session session, String message) throws IOException, EncodeException { 70 | if (this.serviceConnection != null) { 71 | this.serviceConnection.session.getBasicRemote().sendObject(new ServiceResponseData(this.id, Type.MESSAGE, message)); 72 | } 73 | } 74 | 75 | @OnClose 76 | @Override 77 | public void onClose() throws IOException, EncodeException { 78 | dealCloseAndError(); 79 | } 80 | 81 | @OnError 82 | @Override 83 | public void onError(Throwable error, Session session) throws IOException, EncodeException { 84 | error.printStackTrace(); 85 | if (error instanceof EOFException) { 86 | 87 | } else { 88 | dealCloseAndError(); 89 | } 90 | } 91 | 92 | private void dealCloseAndError() throws IOException, EncodeException { 93 | CustomerConnection cc = CUSTOMER_MAP.get(id); 94 | if (CUSTOMER_QUEUE.contains(cc)) { // 某顾客离开时正在队列中,通知其之后的所有顾客前进一位 95 | CUSTOMER_QUEUE.forEach(c -> { 96 | if (c.getCreateTime() > cc.getCreateTime()) { 97 | AbstractResponseData rd = new CustomerResponseData(Type.FORWARD); 98 | try { 99 | c.session.getBasicRemote().sendText(JSON.toJSONString(rd)); 100 | } catch (IOException e) { 101 | e.printStackTrace(); 102 | } 103 | } 104 | }); 105 | CUSTOMER_QUEUE.remove(cc); 106 | AbstractResponseData rd = new CustomerResponseData(Type.FORWARD); 107 | SERVICE_MAP.forEach((k, s) -> { 108 | try { 109 | s.session.getBasicRemote().sendText(JSON.toJSONString(rd)); 110 | } catch (IOException e) { 111 | e.printStackTrace(); 112 | } 113 | }); 114 | } else { // 某顾客离开时正在处于被服务状态,通知其他所有等待队列中的顾客前进一位 115 | AbstractResponseData rd = new CustomerResponseData(Type.FORWARD); 116 | CUSTOMER_QUEUE.forEach(c -> { 117 | try { 118 | if (c != null && c.session != null) { 119 | c.session.getBasicRemote().sendText(JSON.toJSONString(rd)); 120 | } 121 | } catch (IOException e) { 122 | e.printStackTrace(); 123 | } 124 | }); 125 | // 提示客服使其了解到顾客已离开 126 | if (cc.serviceConnection.session.isOpen()) { 127 | cc.serviceConnection.session.getBasicRemote().sendObject(new ServiceResponseData(this.id, Type.CUSTOMER_LEFT)); 128 | } 129 | SERVICE_MAP.forEach((k, s) -> { 130 | try { 131 | s.session.getBasicRemote().sendText(JSON.toJSONString(rd)); 132 | } catch (IOException e) { 133 | e.printStackTrace(); 134 | } 135 | }); 136 | // 让客服接入新的客户 137 | if (CUSTOMER_QUEUE.size() > 0) { 138 | CustomerConnection connection = CUSTOMER_QUEUE.poll(); 139 | connection.serviceConnection = this.serviceConnection; 140 | connection.serviceConnection.getCustomerConnectionMap().put(connection.id, connection); 141 | startService(connection, connection.serviceConnection); 142 | } else { 143 | SERVICE_QUEUE.add(this.serviceConnection); 144 | } 145 | } 146 | this.serviceConnection.getCustomerConnectionMap().remove(id); 147 | CUSTOMER_MAP.remove(id); 148 | try { 149 | session.close(); 150 | } catch (IOException e) { 151 | e.printStackTrace(); 152 | } 153 | } 154 | 155 | private void startService(CustomerConnection cc, ServiceConnection sc) throws IOException, EncodeException { 156 | cc.session.getBasicRemote().sendObject(new CustomerResponseData(Type.START_SERVICE, serviceConnection.getServiceName(), getWelcome())); 157 | sc.session.getBasicRemote().sendObject(new ServiceResponseData(cc.id, Type.START_SERVICE)); 158 | } 159 | 160 | public void setServiceConnection(ServiceConnection serviceConnection) { 161 | this.serviceConnection = serviceConnection; 162 | } 163 | } -------------------------------------------------------------------------------- /src/main/java/com/github/coco/GlobalState.java: -------------------------------------------------------------------------------- 1 | package com.github.coco; 2 | 3 | import java.util.Map; 4 | import java.util.Queue; 5 | import java.util.concurrent.ConcurrentHashMap; 6 | import java.util.concurrent.ConcurrentLinkedQueue; 7 | 8 | /** 9 | * @author db1995 10 | */ 11 | public final class GlobalState { 12 | public static final Queue CUSTOMER_QUEUE = new ConcurrentLinkedQueue<>(); 13 | public static final Queue SERVICE_QUEUE = new ConcurrentLinkedQueue<>(); 14 | 15 | /** 16 | * Map 17 | */ 18 | public static final Map CUSTOMER_MAP = new ConcurrentHashMap<>(); 19 | /** 20 | * Map 21 | */ 22 | public static final Map SERVICE_MAP = new ConcurrentHashMap<>(); 23 | 24 | private GlobalState() { 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/github/coco/ServiceConnection.java: -------------------------------------------------------------------------------- 1 | package com.github.coco; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.alibaba.fastjson.JSONObject; 5 | import com.github.coco.config.GlobalConfig; 6 | import com.github.coco.pojo.AbstractResponseData; 7 | import com.github.coco.pojo.CustomerResponseData; 8 | import com.github.coco.pojo.ServiceResponseData; 9 | import com.github.coco.util.JSONEncoder; 10 | import com.github.coco.util.JwtUtil; 11 | import org.springframework.stereotype.Component; 12 | 13 | import javax.websocket.*; 14 | import javax.websocket.server.PathParam; 15 | import javax.websocket.server.ServerEndpoint; 16 | import java.io.EOFException; 17 | import java.io.IOException; 18 | import java.util.HashMap; 19 | import java.util.Map; 20 | import java.util.UUID; 21 | 22 | import static com.github.coco.GlobalState.*; 23 | import static com.github.coco.config.GlobalConfig.getWelcome; 24 | import static com.github.coco.pojo.AbstractResponseData.Type; 25 | 26 | /** 27 | * WebSocket connection of service 28 | * 29 | * @author db1995 30 | */ 31 | @ServerEndpoint(value = "/service/{token}", encoders = JSONEncoder.class) 32 | @Component 33 | public class ServiceConnection extends AbstractConnection { 34 | private String serviceName; 35 | private Map customerConnectionMap = new HashMap<>(); 36 | 37 | @OnOpen 38 | @Override 39 | public synchronized void onOpen(Session session, @PathParam("token") String token) throws IOException, EncodeException { 40 | // Check identity 41 | if (!JwtUtil.checkToken(token)) { 42 | session.close(); 43 | return; 44 | } 45 | 46 | // Initial 47 | this.session = session; 48 | this.id = UUID.randomUUID().toString(); 49 | session.setMaxIdleTimeout(600000); 50 | this.serviceName = JwtUtil.getClaims().getBody().getSubject(); 51 | SERVICE_MAP.put(this.id, this); 52 | 53 | // Connect to a customer if exist a waiting custom 54 | while (CUSTOMER_QUEUE.size() > 0) { 55 | CustomerConnection customerConnection = CUSTOMER_QUEUE.poll(); 56 | customerConnection.setServiceConnection(this); 57 | this.customerConnectionMap.put(customerConnection.id, customerConnection); 58 | customerConnection.session.getBasicRemote().sendObject(new CustomerResponseData(Type.START_SERVICE, serviceName, getWelcome())); 59 | this.session.getBasicRemote().sendObject(new ServiceResponseData(customerConnection.id, Type.START_SERVICE)); 60 | CUSTOMER_QUEUE.forEach(c -> { 61 | try { 62 | c.session.getBasicRemote().sendObject(new CustomerResponseData(Type.FORWARD)); 63 | } catch (IOException e) { 64 | e.printStackTrace(); 65 | } catch (EncodeException e) { 66 | e.printStackTrace(); 67 | } 68 | }); 69 | SERVICE_MAP.forEach((k, s) -> { 70 | try { 71 | s.session.getBasicRemote().sendObject(new ServiceResponseData(Type.FORWARD)); 72 | } catch (IOException e) { 73 | e.printStackTrace(); 74 | } catch (EncodeException e) { 75 | e.printStackTrace(); 76 | } 77 | }); 78 | } 79 | SERVICE_QUEUE.add(this); 80 | } 81 | 82 | @OnMessage 83 | @Override 84 | public void onMessage(Session session, String jsonMessage) throws IOException, EncodeException { 85 | JSONObject jsonObject = JSON.parseObject(jsonMessage); 86 | String cid = (String) jsonObject.get("customerId"); 87 | String msg = (String) jsonObject.get("message"); 88 | this.customerConnectionMap.get(cid) 89 | .session.getBasicRemote().sendObject(new CustomerResponseData(Type.MESSAGE, serviceName, msg)); 90 | } 91 | 92 | @OnClose 93 | @Override 94 | public void onClose() throws IOException { 95 | handleCloseAndError(); 96 | } 97 | 98 | @OnError 99 | @Override 100 | public void onError(Throwable error, Session session) throws IOException { 101 | error.printStackTrace(); 102 | if (error instanceof EOFException) { 103 | 104 | } else { 105 | handleCloseAndError(); 106 | } 107 | } 108 | 109 | private void handleCloseAndError() throws IOException { 110 | // 告知所有正在服务的客户,客服已掉线 111 | this.customerConnectionMap.forEach((id, cc) -> { 112 | AbstractResponseData rd = new CustomerResponseData(Type.SERVICE_DOWN, GlobalConfig.getServiceDown()); 113 | try { 114 | cc.session.getBasicRemote().sendObject(rd); 115 | } catch (IOException e) { 116 | e.printStackTrace(); 117 | } catch (EncodeException e) { 118 | e.printStackTrace(); 119 | } 120 | }); 121 | SERVICE_MAP.remove(this.id); 122 | this.session.close(); 123 | } 124 | 125 | public Map getCustomerConnectionMap() { 126 | return customerConnectionMap; 127 | } 128 | 129 | public String getServiceName() { 130 | return serviceName; 131 | } 132 | } -------------------------------------------------------------------------------- /src/main/java/com/github/coco/config/GlobalConfig.java: -------------------------------------------------------------------------------- 1 | package com.github.coco.config; 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties; 4 | import org.springframework.stereotype.Component; 5 | 6 | /** 7 | * Global configuration 8 | * You can customize these by setting application.yml 9 | * 10 | * @author db1995 11 | */ 12 | @ConfigurationProperties("coco") 13 | @Component 14 | public final class GlobalConfig { 15 | private static int maxCustomerPerService = 5; 16 | private static String welcome = ""; 17 | private static String autoReplyAfterWork = ""; 18 | private static String unifiedServiceName = ""; 19 | private static boolean alwaysDisplayUnifiedServiceName = true; 20 | private static String serviceDown = ""; 21 | 22 | public static int getMaxCustomerPerService() { 23 | return maxCustomerPerService; 24 | } 25 | 26 | public void setMaxCustomerPerService(int maxCustomerPerService) { 27 | GlobalConfig.maxCustomerPerService = maxCustomerPerService; 28 | } 29 | 30 | public static String getWelcome() { 31 | return welcome; 32 | } 33 | 34 | public void setWelcome(String welcome) { 35 | GlobalConfig.welcome = welcome; 36 | } 37 | 38 | public static String getAutoReplyAfterWork() { 39 | return autoReplyAfterWork; 40 | } 41 | 42 | public void setAutoReplyAfterWork(String autoReplyAfterWork) { 43 | GlobalConfig.autoReplyAfterWork = autoReplyAfterWork; 44 | } 45 | 46 | public static String getUnifiedServiceName() { 47 | return unifiedServiceName; 48 | } 49 | 50 | public void setUnifiedServiceName(String unifiedServiceName) { 51 | GlobalConfig.unifiedServiceName = unifiedServiceName; 52 | } 53 | 54 | public static boolean isAlwaysDisplayUnifiedServiceName() { 55 | return alwaysDisplayUnifiedServiceName; 56 | } 57 | 58 | public void setAlwaysDisplayUnifiedServiceName(boolean alwaysDisplayUnifiedServiceName) { 59 | GlobalConfig.alwaysDisplayUnifiedServiceName = alwaysDisplayUnifiedServiceName; 60 | } 61 | 62 | public static String getServiceDown() { 63 | return serviceDown; 64 | } 65 | 66 | public void setServiceDown(String serviceDown) { 67 | GlobalConfig.serviceDown = serviceDown; 68 | } 69 | } -------------------------------------------------------------------------------- /src/main/java/com/github/coco/config/WebSocketConfig.java: -------------------------------------------------------------------------------- 1 | package com.github.coco.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.web.socket.server.standard.ServerEndpointExporter; 6 | 7 | /** 8 | * Configuration of WebSocket 9 | * 10 | * @author db1995 11 | */ 12 | @Configuration 13 | public class WebSocketConfig { 14 | @Bean 15 | public ServerEndpointExporter serverEndpointExporter() { 16 | return new ServerEndpointExporter(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/github/coco/controller/CustomerController.java: -------------------------------------------------------------------------------- 1 | package com.github.coco.controller; 2 | 3 | import org.springframework.web.bind.annotation.GetMapping; 4 | import org.springframework.web.bind.annotation.RequestMapping; 5 | import org.springframework.web.bind.annotation.RestController; 6 | 7 | import java.util.UUID; 8 | 9 | /** 10 | * Controller for customer 11 | * 12 | * @author db1995 13 | */ 14 | @RestController 15 | @RequestMapping("/customer") 16 | public class CustomerController { 17 | @GetMapping 18 | public String login() { 19 | String customerId = UUID.randomUUID().toString(); 20 | return customerId; 21 | } 22 | } -------------------------------------------------------------------------------- /src/main/java/com/github/coco/controller/ServiceController.java: -------------------------------------------------------------------------------- 1 | package com.github.coco.controller; 2 | 3 | import com.github.coco.util.JwtUtil; 4 | import com.github.coco.vo.StatisticsVO; 5 | import org.springframework.web.bind.annotation.GetMapping; 6 | import org.springframework.web.bind.annotation.PostMapping; 7 | import org.springframework.web.bind.annotation.RequestMapping; 8 | import org.springframework.web.bind.annotation.RestController; 9 | 10 | /** 11 | * Controller for service 12 | * 13 | * @author db1995 14 | */ 15 | @RestController 16 | @RequestMapping("/service") 17 | public class ServiceController { 18 | @PostMapping("/login") 19 | public String login(String username, String password) { 20 | // TODO Maybe you should use your database to replace this 21 | if (("test1@coco.com".equals(username) || "test2@coco.com".equals(username) || "test3@coco.com".equals(username)) 22 | && "123456".equals(password)) { 23 | return JwtUtil.generateToken(username); 24 | } 25 | return ""; 26 | } 27 | 28 | @GetMapping("/statistics") 29 | public StatisticsVO loadStatistics() { 30 | // TODO You should select data from database 31 | StatisticsVO sv = new StatisticsVO(); 32 | sv.setTodayServed(10); 33 | sv.setTodayOnline(90); 34 | sv.setTodayScore(4.6); 35 | sv.setTotalServed(85); 36 | sv.setTotalOnline(925); 37 | sv.setTotalScore(4.4); 38 | return sv; 39 | } 40 | } -------------------------------------------------------------------------------- /src/main/java/com/github/coco/pojo/AbstractResponseData.java: -------------------------------------------------------------------------------- 1 | package com.github.coco.pojo; 2 | 3 | /** 4 | * @author db1995 5 | */ 6 | public abstract class AbstractResponseData { 7 | protected String message; 8 | protected Type type; 9 | protected int customerInQueue; 10 | 11 | public String getMessage() { 12 | return message; 13 | } 14 | 15 | public void setMessage(String message) { 16 | this.message = message; 17 | } 18 | 19 | public int getCustomerInQueue() { 20 | return customerInQueue; 21 | } 22 | 23 | public void setCustomerInQueue(int customerInQueue) { 24 | this.customerInQueue = customerInQueue; 25 | } 26 | 27 | public enum Type { 28 | FORWARD, 29 | MESSAGE, 30 | WAIT_SERVICE, 31 | START_SERVICE, 32 | CUSTOMER_LEFT, 33 | SERVICE_DOWN 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/github/coco/pojo/CustomerResponseData.java: -------------------------------------------------------------------------------- 1 | package com.github.coco.pojo; 2 | 3 | import com.github.coco.GlobalState; 4 | 5 | /** 6 | * @author db1995 7 | */ 8 | public class CustomerResponseData extends AbstractResponseData { 9 | private String serviceName; 10 | 11 | public CustomerResponseData() { 12 | } 13 | 14 | public CustomerResponseData(Type type) { 15 | if (type != Type.MESSAGE) { 16 | customerInQueue = GlobalState.CUSTOMER_QUEUE.size(); 17 | } 18 | this.type = type; 19 | } 20 | 21 | public CustomerResponseData(Type type, String message) { 22 | if (type != Type.MESSAGE) { 23 | customerInQueue = GlobalState.CUSTOMER_QUEUE.size(); 24 | } 25 | this.type = type; 26 | this.message = message; 27 | } 28 | 29 | public CustomerResponseData(Type type, String serviceName, String message) { 30 | if (type != Type.MESSAGE) { 31 | customerInQueue = GlobalState.CUSTOMER_QUEUE.size(); 32 | } 33 | this.type = type; 34 | this.serviceName = serviceName; 35 | this.message = message; 36 | } 37 | 38 | public String getServiceName() { 39 | return serviceName; 40 | } 41 | 42 | public void setServiceName(String serviceName) { 43 | this.serviceName = serviceName; 44 | } 45 | 46 | public Type getType() { 47 | return type; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/github/coco/pojo/ServiceResponseData.java: -------------------------------------------------------------------------------- 1 | package com.github.coco.pojo; 2 | 3 | import com.github.coco.GlobalState; 4 | 5 | /** 6 | * @author db1995 7 | */ 8 | public class ServiceResponseData extends AbstractResponseData { 9 | private String customerId; 10 | 11 | public ServiceResponseData(Type type) { 12 | if (type != Type.MESSAGE) { 13 | customerInQueue = GlobalState.CUSTOMER_QUEUE.size(); 14 | } 15 | this.type = type; 16 | } 17 | 18 | public ServiceResponseData(String customerId, Type type) { 19 | if (type != Type.MESSAGE) { 20 | customerInQueue = GlobalState.CUSTOMER_QUEUE.size(); 21 | } 22 | this.customerId = customerId; 23 | this.type = type; 24 | } 25 | 26 | public ServiceResponseData(String customerId, Type type, String message) { 27 | if (type != Type.MESSAGE) { 28 | customerInQueue = GlobalState.CUSTOMER_QUEUE.size(); 29 | } 30 | this.customerId = customerId; 31 | this.type = type; 32 | this.message = message; 33 | } 34 | 35 | public String getCustomerId() { 36 | return customerId; 37 | } 38 | 39 | public void setCustomerId(String customerId) { 40 | this.customerId = customerId; 41 | } 42 | 43 | public Type getType() { 44 | return type; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/github/coco/util/JSONEncoder.java: -------------------------------------------------------------------------------- 1 | package com.github.coco.util; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.github.coco.pojo.AbstractResponseData; 5 | 6 | import javax.websocket.EncodeException; 7 | import javax.websocket.Encoder; 8 | import javax.websocket.EndpointConfig; 9 | 10 | /** 11 | * Encode Object to JSON 12 | * 13 | * @author db1995 14 | */ 15 | public class JSONEncoder implements Encoder.Text { 16 | @Override 17 | public String encode(AbstractResponseData responseData) throws EncodeException { 18 | return JSON.toJSONString(responseData); 19 | } 20 | 21 | @Override 22 | public void init(EndpointConfig endpointConfig) { 23 | 24 | } 25 | 26 | @Override 27 | public void destroy() { 28 | 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/github/coco/util/JwtUtil.java: -------------------------------------------------------------------------------- 1 | package com.github.coco.util; 2 | 3 | import io.jsonwebtoken.*; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | 7 | import java.io.UnsupportedEncodingException; 8 | import java.time.LocalDateTime; 9 | import java.time.ZoneOffset; 10 | 11 | /** 12 | * JWT authentication 13 | * 14 | * @author db1995 15 | */ 16 | public final class JwtUtil { 17 | private static Logger logger = LoggerFactory.getLogger(JwtUtil.class); 18 | private static Jws claims; 19 | 20 | private JwtUtil() { 21 | } 22 | 23 | /** 24 | * Give token to the identity passed by the certificate 25 | * 26 | * @param subject 27 | * @return 28 | */ 29 | public static String generateToken(String subject) { 30 | long now = LocalDateTime.now().toEpochSecond(ZoneOffset.ofHours(8)); 31 | String token = null; 32 | try { 33 | token = Jwts.builder() 34 | .claim("exp", now + 60 * 60 * 8) 35 | .signWith(SignatureAlgorithm.HS256, "https://github.com/db1995/coco".getBytes("UTF-8")) 36 | .setSubject(subject) 37 | .compact(); 38 | } catch (UnsupportedEncodingException e) { 39 | e.printStackTrace(); 40 | } 41 | logger.info("Token authentication succeeded: " + token); 42 | return token; 43 | } 44 | 45 | /** 46 | * Check if the token is valid 47 | * 48 | * @param token 49 | * @return 50 | */ 51 | public static boolean checkToken(String token) { 52 | try { 53 | JwtUtil.claims = Jwts.parser() 54 | .setSigningKey("https://github.com/db1995/coco".getBytes("UTF-8")) 55 | .parseClaimsJws(token); 56 | logger.info("Valid token"); 57 | return true; 58 | } catch (SignatureException e) { 59 | logger.warn("Invalid token"); 60 | } catch (ExpiredJwtException e) { 61 | logger.warn("Invalid token: expired"); 62 | } catch (Exception e) { 63 | logger.warn("Invalid token: other reason"); 64 | } 65 | return false; 66 | } 67 | 68 | public static Jws getClaims() { 69 | return claims; 70 | } 71 | } -------------------------------------------------------------------------------- /src/main/java/com/github/coco/vo/StatisticsVO.java: -------------------------------------------------------------------------------- 1 | package com.github.coco.vo; 2 | 3 | /** 4 | * @author db1995 5 | */ 6 | public class StatisticsVO { 7 | private int todayServed; 8 | private int todayOnline; 9 | private double todayScore; 10 | private int totalServed; 11 | private int totalOnline; 12 | private double totalScore; 13 | 14 | public int getTodayServed() { 15 | return todayServed; 16 | } 17 | 18 | public void setTodayServed(int todayServed) { 19 | this.todayServed = todayServed; 20 | } 21 | 22 | public int getTodayOnline() { 23 | return todayOnline; 24 | } 25 | 26 | public void setTodayOnline(int todayOnline) { 27 | this.todayOnline = todayOnline; 28 | } 29 | 30 | public double getTodayScore() { 31 | return todayScore; 32 | } 33 | 34 | public void setTodayScore(double todayScore) { 35 | this.todayScore = todayScore; 36 | } 37 | 38 | public int getTotalServed() { 39 | return totalServed; 40 | } 41 | 42 | public void setTotalServed(int totalServed) { 43 | this.totalServed = totalServed; 44 | } 45 | 46 | public int getTotalOnline() { 47 | return totalOnline; 48 | } 49 | 50 | public void setTotalOnline(int totalOnline) { 51 | this.totalOnline = totalOnline; 52 | } 53 | 54 | public double getTotalScore() { 55 | return totalScore; 56 | } 57 | 58 | public void setTotalScore(double totalScore) { 59 | this.totalScore = totalScore; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | coco: 2 | max-customer-per-service: 2 3 | welcome: Welcome! Any questions? 4 | auto-reply-after-work: Sorry, we are all off, you can leave your questions and we will respond to you. 5 | unified-service-name: Great Company 6 | always-display-unified-service-name: true 7 | service-down: Sorry, the service network is abnormal and disconnected -------------------------------------------------------------------------------- /src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | ${AnsiColor.BLUE} coco 2 | ${AnsiColor.BLUE} co co 3 | ${AnsiColor.BLUE} co 4 | ${AnsiColor.BLUE}co co co co 5 | ${AnsiColor.BLUE} co co co c c co co 6 | ${AnsiColor.BLUE} co co co co c co co 7 | ${AnsiColor.BLUE} coco co co co 8 | ${AnsiColor.GREEN} :: Coco :: ${application.formatted-version} -------------------------------------------------------------------------------- /src/main/resources/static/customer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Customer 6 | 7 | 8 | 9 |

Customer

10 |

Before you: 0

11 |
12 |
13 |
14 |
15 |
16 |
18 |
19 |
20 | 23 |
24 | 25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/main/resources/static/js/coco.common.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | window.onresize = function () { 3 | $('.coco-dialog').css("height", window.innerHeight * 0.6); 4 | }; 5 | resize(); 6 | }); 7 | 8 | function getTime() { 9 | let today = new Date(); 10 | let h = today.getHours(); 11 | let m = today.getMinutes(); 12 | let s = today.getSeconds(); 13 | return h + ':' + (m > 9 ? m : "0" + m) + ':' + (s > 9 ? s : "0" + s); 14 | } 15 | 16 | function resize() { 17 | $('.coco-dialog').css("height", window.innerHeight * 0.6); 18 | } -------------------------------------------------------------------------------- /src/main/resources/static/js/coco.customer.js: -------------------------------------------------------------------------------- 1 | const serverAddress = "localhost"; 2 | const serverPort = 8080; 3 | let id = ""; 4 | let closeTipTimeout; 5 | let closeTimeout; 6 | 7 | function handleTimeout(ws) { 8 | closeTipTimeout = window.setTimeout(function () { 9 | $('#coco-dialog').append('

Connection will be closed after 1 minute

'); 10 | closeTimeout = window.setTimeout(function () { 11 | ws.close(); 12 | }, 120 * 1000); 13 | }, 60 * 1000); 14 | } 15 | 16 | function socket() { 17 | //Determine whether the current browser supports WebSocket 18 | if ('WebSocket' in window) { 19 | const ws = new WebSocket("ws://" + serverAddress + ":" + serverPort + "/customer/" + id); 20 | let before = null; 21 | //Connect failed callback 22 | ws.onerror = function () { 23 | $('#coco-dialog').append('

Connection error

'); 24 | }; 25 | //Connect succeed callback 26 | ws.onopen = function () { 27 | $('#coco-dialog').append('

Connection succeeded

'); 28 | handleTimeout(ws); 29 | }; 30 | //Receive message callback 31 | ws.onmessage = function (event) { 32 | let json = JSON.parse(event.data); 33 | /*if (json.message != null) { 34 | alert(json.message) 35 | }*/ 36 | if (before == null) { 37 | before = json.customerInQueue; 38 | } 39 | let dialog = $('#coco-dialog'); 40 | let waitingBeforeYouObj = $("#waitingBeforeYou"); 41 | switch (json.type) { 42 | case "MESSAGE": 43 | dialog.append('

' 44 | + json.serviceName + ' ' + getTime() + '
' 45 | + json.message + '

'); 46 | break; 47 | case "FORWARD": 48 | if (before !== 0) { 49 | waitingBeforeYouObj.text(--before); 50 | } 51 | break; 52 | case "START_SERVICE": 53 | dialog.append('

' + 'Connected service' + '

') 54 | .append('

' + json.serviceName + ' ' + getTime() + '
' 55 | + json.message + '

'); 56 | before = 0; 57 | waitingBeforeYouObj.text(0); 58 | break; 59 | case "WAIT_SERVICE": 60 | waitingBeforeYouObj.text(json.customerInQueue); 61 | break; 62 | case "SERVICE_DOWN": 63 | dialog.append('

' + json.message + '

'); 64 | ws.close(); 65 | break; 66 | } 67 | let scrollHeight = dialog.prop('scrollHeight'); 68 | dialog.scrollTop(scrollHeight); 69 | }; 70 | //Connect closed callback 71 | ws.onclose = function () { 72 | $('#coco-dialog').append('

Connection closed

'); 73 | }; 74 | //Close the websocket connection when the window is closed, preventing throwing exceptions. 75 | window.onbeforeunload = function () { 76 | ws.close(); 77 | }; 78 | //Send message 79 | $('#send').click(function () { 80 | let messageObj = $('#message'); 81 | let messageVal = messageObj.val(); 82 | let dialog = $('#coco-dialog'); 83 | if (messageVal !== '') { 84 | ws.send(messageVal); 85 | dialog.append('

' + getTime() + '
' + messageVal + '

'); 86 | messageObj.val('').focus(); 87 | } 88 | let scrollHeight = dialog.prop('scrollHeight'); 89 | dialog.scrollTop(scrollHeight); 90 | window.clearTimeout(closeTimeout); 91 | window.clearTimeout(closeTipTimeout); 92 | handleTimeout(ws); 93 | }); 94 | // Press enter to send message 95 | $('#message').bind('keyup', function (event) { 96 | if (event.keyCode === 13) { 97 | $('#send').trigger('click'); 98 | } 99 | }); 100 | } else { 101 | alert('Your browser does not support WebSocket. Please change your browser and try again.'); 102 | } 103 | } 104 | 105 | $(function () { 106 | $.ajax({ 107 | url: "/customer", 108 | type: "get", 109 | success: function (result) { 110 | id = result; 111 | socket(id); 112 | } 113 | }); 114 | }); -------------------------------------------------------------------------------- /src/main/resources/static/js/coco.service.js: -------------------------------------------------------------------------------- 1 | const serverAddress = "localhost"; 2 | const serverPort = 8080; 3 | const customerIdSet = new Set(); 4 | 5 | function socket(token) { 6 | //Determine whether the current browser supports WebSocket 7 | if ('WebSocket' in window) { 8 | loadStatistics(); 9 | const ws = new WebSocket("ws://" + serverAddress + ":" + serverPort + "/service/" + token); 10 | let waiting = null; 11 | //Connect failed callback 12 | ws.onerror = function () { 13 | //$('.coco-dialog').append('

Connection error

'); 14 | }; 15 | //Connect succeed callback 16 | ws.onopen = function () { 17 | //$('.coco-dialog').append('

Connection succeeded

'); 18 | }; 19 | //Receive message callback 20 | ws.onmessage = function (event) { 21 | let json = JSON.parse(event.data); 22 | if (waiting == null) { 23 | waiting = json.customerInQueue; 24 | } 25 | let cocoDialogObject = $("#" + json.customerId).children(".coco-dialog"); 26 | let customerId = json.customerId; 27 | let hrefCustomerId = $("[href='#" + customerId + "']"); 28 | let waitingObj = $("#waiting"); 29 | switch (json.type) { 30 | case "MESSAGE": 31 | cocoDialogObject.append('

' + getTime() + '
' + json.message + '

'); 32 | const e = hrefCustomerId.children(".badge"); 33 | if (e.hasClass("badge-danger") || hrefCustomerId.hasClass("active")) { 34 | break; 35 | } 36 | if (e.text() === "") { 37 | e.text(1); 38 | } else { 39 | let num = parseInt(e.text()); 40 | e.text(++num); 41 | } 42 | break; 43 | case "FORWARD": 44 | if (waiting !== 0) { 45 | waitingObj.text(--waiting); 46 | } 47 | break; 48 | case "START_SERVICE": 49 | customerIdSet.add(customerId); 50 | $(".list-group").append('CUST ' + customerIdSet.size + 'NEW'); 51 | const ch = '
\n' + 52 | '
\n' + 54 | '
\n' + 55 | '
\n' + 56 | ' \n' + 59 | '
\n' + 60 | ' \n' + 61 | '
\n' + 62 | '
\n' + 63 | '
'; 64 | $("#nav-tabContent").append(ch); 65 | cocoDialogObject.append('

' + 'Connected service' + '

'); 66 | resize(); 67 | if (waiting !== 0) { 68 | waitingObj.text(--waiting); 69 | } 70 | $("#list-tab").on("click", "[href='#" + customerId + "']", function () { 71 | $(".tab-pane").removeClass("active"); 72 | $("#" + customerId).addClass("active"); 73 | $("[href='#" + customerId + "']").children(".badge") 74 | .removeClass("badge-danger").addClass("badge-warning").text(""); 75 | $("#message_" + customerId).focus(); 76 | }); 77 | $(".tab-pane").on("click", ".send", function () { 78 | let customerId = ($(this).attr("id").split("_"))[1]; 79 | let messageObj = $(this).parent().prev(".message"); 80 | if (messageObj.val() !== '') { 81 | let msg = {"customerId": customerId, "message": messageObj.val()}; 82 | ws.send(JSON.stringify(msg)); 83 | $("#" + customerId).children('.coco-dialog').append('

' + getTime() + '
' + messageObj.val() + '

'); 84 | } 85 | let dialog = $('.coco-dialog'); 86 | messageObj.val('').focus(); 87 | let scrollHeight = dialog.prop('scrollHeight'); 88 | dialog.scrollTop(scrollHeight); 89 | }); 90 | // Press enter to send message 91 | $('#message_' + customerId).bind('keyup', function (event) { 92 | if (event.keyCode === 13) { 93 | $('#send_' + customerId).trigger('click'); 94 | } 95 | }); 96 | break; 97 | case "WAIT_SERVICE": 98 | waitingObj.text(++waiting); 99 | break; 100 | case "CUSTOMER_LEFT": 101 | $('#coco-dialog_' + customerId).append('
\n' + 102 | '
\n' + 103 | '
Customer has left
\n' + 104 | '

The service time: 12m

\n' + 105 | ' Close dialog\n' + 106 | '
\n' + 107 | '
'); 108 | hrefCustomerId.addClass("bg-secondary").css("border-color", "#ffffff"); 109 | $('#nav-tabContent').on('click', '#close_' + customerId, function () { 110 | $("[href='#statistics']").trigger('click'); 111 | $("[href='#" + customerId + "'], #coco-dialog_" + customerId + ", #" + customerId).remove(); 112 | }); 113 | break; 114 | default: 115 | break; 116 | } 117 | let scrollHeight = cocoDialogObject.prop('scrollHeight'); 118 | cocoDialogObject.scrollTop(scrollHeight); 119 | }; 120 | //Connect closed callback 121 | ws.onclose = function () { 122 | //$('.coco-dialog').append('

Connection closed

'); 123 | }; 124 | //Close the websocket connection when the window is closed, preventing throwing exceptions. 125 | window.onbeforeunload = function () { 126 | ws.close(); 127 | }; 128 | //Send message 129 | $('.send').click(function () { 130 | let dialog = $('.coco-dialog'); 131 | let customerId = ($(this).attr("id").split("_"))[1]; 132 | // alert(customerId) 133 | let message = $(this).parent().prev(".message"); 134 | if (message !== '') { 135 | let msg = {"customerId": customerId, "message": message.val()}; 136 | ws.send(JSON.stringify(msg)); 137 | $("#" + customerId).children('.coco-dialog').append('

' + getTime() + '
' + message + '

'); 138 | message.val('').focus(); 139 | } 140 | let scrollHeight = dialog.prop('scrollHeight'); 141 | dialog.scrollTop(scrollHeight); 142 | }); 143 | } else { 144 | alert('Your browser does not support WebSocket. Please change your browser and try again.'); 145 | } 146 | } 147 | 148 | $(function () { 149 | $("#login-form").trigger("click"); 150 | $("#login").click(function () { 151 | $.ajax({ 152 | url: "/service/login", 153 | data: {"username": $("#email").val(), "password": $("#password").val()}, 154 | type: "post", 155 | success: function (token) { 156 | if (token !== "") { 157 | socket(token); 158 | $(".close").trigger("click"); 159 | } 160 | } 161 | }); 162 | }); 163 | 164 | $("#pill-statistics").click(function () { 165 | loadStatistics(); 166 | }); 167 | }); 168 | 169 | // Load statistics 170 | function loadStatistics() { 171 | $.ajax({ 172 | url: "/service/statistics", 173 | dataType: "json", 174 | success: function (data) { 175 | $("#todayServed").text(data.todayServed); 176 | $("#todayOnline").text(data.todayOnline); 177 | $("#todayScore").text(data.todayScore); 178 | $("#totalServed").text(data.totalServed); 179 | $("#totalOnline").text(data.totalOnline); 180 | $("#totalScore").text(data.totalScore); 181 | } 182 | }); 183 | } -------------------------------------------------------------------------------- /src/main/resources/static/service.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Service 6 | 7 | 8 | 9 |

Service

10 |

Waiting: 0

11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | Statistics 23 |
24 |
25 |
26 | 48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 | 60 | 89 | 90 | 91 | 92 | 93 | 94 | --------------------------------------------------------------------------------