Java 程序设计——站内短信系统
前期调研
QQ 邮箱
写信
打开普通邮件的编辑页面,该页面的布局较为简单,从该页面可以看出一封邮件需要提供收件人、邮件标题和正文 3 个基本的内容。
收\发件箱
打开 QQ 邮箱的收件箱查看某一篇邮件,页面上方显示了邮件标题、收件人、发件人和发件时间 4 个信息。中间是邮件的正文部分,右下角处可以切换上一篇或者下一篇邮件。
集美大学物理实验中心
集美大学物理实验中心的教学管理系统中,和实验课老师联系主要通过“站内短消息”,可以看做是只能在系统内使用的简化版邮件系统。“站内短消息”的界面就更简单了,写短消息只需要提供正文和用户,收件箱直接在下方显示。
课堂派
注意到课堂派也实现了私信系统,该系统实现了类似于 QQ 和微信的 GUI 界面。
调研总结
通过调研并联系实际情况,课程私信系统在实际情况下具有重要的作用。在大学经常有一位老师只上一门课中的某一节课的情况,例如大学物理实验,每次实验课的老师都是不一样的。在这种情况下我们往往不会留老师的联系方式,而站内短信为和老师取得联系提供了很大的便利。站内短信的功能对实时交互的要求不高,因此不需要像课堂派那样实现聊天室的功能。由于站内短信往往用于传输请假信息和通知等短消息,因此不需要实现像邮件系统那样大块头的功能。
当用户需要写短信时,需要提供收件人、邮件标题和正文 3 个基本的内容。封装一则短信时,还需要和用户绑定的用户名信息和当前的时间信息,然后程序需要把这则短信通过某种合适的方式推到数据的存储结构中。当用户要查看收件箱或者发件箱时,需要向存储结构拉取所需要的信息,并显示出来。如果把提交短信和拉取短信都当做是一种请求,则这个交互过程也可以用三层架构进行描述:
登录系统
登录系统将基于三层架构进行设计,使用 Socket 和 MySQL 进行实现,同时使用一定的信息加密提高安全性。详情见Java 程序设计——登录系统。
登录之后将自动进入菜单界面,菜单界面的 GUI 设计如图。
站内短信系统
二层架构
站内短信系统使用的是二层架构,也就是客户端直接向存储层请求数据,存储层更新或检索数据之后进行响应。此处使用 MySQL 数据库作为数据的存储层,这就需要客户端实现对数据库的远程连接。
程序的包结构
程序工作流程
类的设计思路
描述系统工作流程
首先用户需要在注册界面输入自己的用户名和密码进行注册,待注册成功之后返回登录界面进行登录。登录成功后用户会进入主菜单界面,通过选择可以查看用户的收件箱和发件箱进行查看。进入短信查看界面之后,界面将拉取用户的收件箱或者发件箱,接着显示一则短信的短信标题、发件人、收件人、发件时间和正文。当用户要写短信时,用户同样需要填写短信的短信标题、发件人、收件人、发件时间和正文,然后把短信发送到服务器中。
类的设计
从上述流程中我们可以知道,短信的收发有两个主体(蓝色字体标出),分别是用户和短信。短信是被操作的对象,一则短信需要具备短信标题、发件人、收件人、发件时间和正文这些基本要素(红色字体标出),因此应该将上述内容为类的属性封装出 Messages 类。
用户是完成一系列操作的主体,每个用户都有用户名(考虑信息安全,密码不存储),同时每个用户都有自己的发件箱和收件箱(红色字体标出),因此应该将上述内容为类的属性封装出 Customer 类。
用户对于短信的操作,无非是提交短信和查看短信两种操作(绿色字体标出),由于我设计短信存储于数据库,因此需要有 DAO 接口及其实现类对实现对数据库的信息交互。
通过对工作流程的叙述,我们也能得知短信系统需要哪些界面(黄色字体标出)。
主要的实体类
Message 类
无论是新生成的短信还是从数据库拉取的短信,在程序当做应该以 Message 类的形式存在。Message 类的设计很简单,只需要具备一封邮件需要具备的信息即可,方法仅需要提供属性访问器和修改器即可。
package model.message;
import java.sql.Timestamp;
/**
* Message 类为邮件对象,存储一封邮件的基本信息,并附带有属性访问器和修改器
* @author 林智凯
* @version 1.0
*/
public class Message {
private int id; //邮件 id
private String title; //邮件标题
private String user; //收件用户
private String addresser; //发件人
private String text; //邮件正文
private Timestamp time; //邮件发送/接收时间
public int getId() {
return id;
}
public String getTitle() {
return title;
}
public String getUser() {
return user;
}
public String getAddresser() {
return addresser;
}
public String getText() {
return text;
}
public Timestamp getTime() {
return time;
}
public void setId(int id) {
this.id = id;
}
public void setTitle(String title) {
this.title = title;
}
public void setUser(String user) {
this.user = user;
}
public void setAddresser(String addresser) {
this.addresser = addresser;
}
public void setText(String text) {
this.text = text;
}
public void setTime(Timestamp time) {
this.time = time;
}
}
Customer 类
同登录系统那篇博客所说,当用户登录成功后,应该把该用户信息实例化一个 Customer 来存储。由于实现的是站内短信系统,因此用户需要 2 个结构分别存储收件箱和发件箱,此处选用 LinkedList
import java.util.LinkedList;
import logic.email.*;
/**
* 这个类存储了用户名及其MD5加密,实例化后绑定收件箱和发件箱给用户。
* @author 乌漆 WhiteMoon
* @version 1.1
*/
public class Customer {
private final String username;
private final String username_md5;
private List<Message> Inbox; //收件箱
private List<Message> Outbox; //发件箱
/**
* 这个方法是customer对象的构造器
* @param username 用户名,String
* @return customer对象
*/
public Customer(String username) {
this.username = username;
this.username_md5 = MD5Util.getMD5Str(username);
}
public String getUsername() {
return username;
}
public String getUsername_md5() {
return username_md5;
}
public LinkedList<Message> getInbox() {
return Inbox;
}
public LinkedList<Message> getOutbox() {
return Outbox;
}
public void setInbox() {
Inbox = transformStructure.initializeInbox(this.username);
}
public void setOutbox() {
Outbox = transformStructure.initializeOutbox(this.username);
}
}
MessagesDAO 接口
由于存储短信的形式可以是多种数据库或文件,因此定义 MessagesDAO 接口指定了用户获取邮件资源的行为。站内短信系统中用户与存储结构交互的行为有 3 种,分别是提交一封短信、拉取收件箱和拉取发件箱。
import java.sql.ResultSet;
import logic.email.*;
/**
* PullMessagesDAO 接口指定了用户获取邮件资源的行为
* @author 乌漆 WhiteMoon
* @version 1.0
*/
public interface MessagesDAO {
/**
* 这个方法将以 ResultSet 的形式,从数据库获取已发送的邮件
* @param username 用户名,String
* @return 已发送的邮件集 LinkedList<Emails>
*/
public static ResultSet getSendedMessages(String username) {
return null;
}
/**
* 这个方法将以 ResultSet 的形式,从数据库获取接收到的邮件
* @param username 用户名,String
* @return 已接收的邮件集 LinkedList<Messages>
*/
public static ResultSet getReceivedMessages(String username) {
return null;
}
/**
* 这个方法将一个 Emails 对象存储到数据库中
* @param a_message 要提交的邮件,Messages
* @return 操作是否成功,boolean
*/
public static boolean sendMessage(Messages a_message) {
return false;
}
}
MessagesDaoJDBCImpl 类
MessagesJDBCImpl类是选择 MySQL 数据库作为存储结构实现的 MessagesDAO 接口,此处需要 MysqlConnect 类提供数据库远程连接的功能进行辅助。
类之间的关系
由于 Customer 类有 Inbox 收件箱和 outBox 发件箱的属性,这 2 个属性将使用 LinkedList 集合存储 Messages 类对象,因此 Customer 类对于 Messages 类具有依赖关系。每当用户查看收件箱或发件箱,Inbox 和 outBox 这 2 个属性就需要和存储结构进行数据交互,也就是要从数据库拉取短信信息,所以 Customer 类对于 MessagesDAO 接口及其实现类具有依赖关系。
MySQL 数据库设计
emails 表
emails 表用于存储每一封短信的各个信息,其中主索引 id 启用自动增量,text 字段设置为 “text” 类型存储长文本。表中各个字段设置如图所示:
例如已经存储 3 个短信记录的数据库状态如下:
Transformation 类
由于 MessagesDAO 接口实现的 getSendedEmails() 方法和 getReceivedEmails() 方法的返回值都是 ResultSet 结果集,这种结构并不利于其他方法处理,因此需要将 ResultSet 转换为其他结构。
此处将 ResultSet 转换为 LinkedList
写短信
MessagesDaoJDBCImpl.sendEmail 方法
MessagesDaoJDBCImpl.sendEmail() 方法接收一个 Messages 对象,并把该对象的各个字段写入数据库中。
/**
* 基于 MySql 数据库实现的 sendEmail() 方法
* @param a_email 要提交的邮件,Messages
* @return 操作是否成功,boolean
* @throws SQLException
*/
public static boolean sendEmail(Emails a_email) throws SQLException {
Connection conn = null; //创建 Connection 数据库连接对象
Statement statement = null; //创建静态 SQL 语句 Statement 对象
boolean flag = false;
//Timestamp time = new Timestamp(System.currentTimeMillis()); //获取当前时间
try {
conn = MysqlConnect.connectDatabase(); //数据库连接
statement = conn.createStatement(); //初始化静态 SQL语句
String sqlInsert = " INSERT INTO emails(title, user, addresser, text, sendtime) values('%s','%s','%s','%s','%s'); ";
//判断插入是否成功
if(statement.executeUpdate(String.format(sqlInsert, a_email.getTitle(), a_email.getUser(), a_email.getAddresser(), a_email.getText(), a_email.getTime())) != 0) {
flag = true;
}
else {
flag = false;
}
}catch (SQLException sqle) {
throw sqle;
}catch(Exception e){
throw e;
}finally{ //关闭所有资源
MysqlConnect.close(statement);
MysqlConnect.close(conn);
}
return flag;
}
GUI 设计
写短息的窗体界面如图所示,用户需要输入“标题”、“正文”和“收件人”,点击“发送”按钮把数据一则短信送插入到数据库中。
发送按钮被点击时,程序需要检查“标题”和“收件人”是否有填写,尤其是收件人,因为不存在没有收件人的短信。上述信息都有时,也需要检验收件人是否存在,在 users 表中搜索用户名的 SQL 语句为:
SELECT username FROM users WHERE binary username = '%s';
确保收件人存在后,将用户输入的“标题”、“正文”和“收件人”信息,和当前用户的用户名和系统时间实例化为一个 Emails 对象。调用 MySqlEmailsAction.sendEmail() 方法把该对象传送到数据库中,即完成一次写短信操作。
private void sendSMSActionPerformed(java.awt.event.ActionEvent evt) throws SQLException {
// TODO add your handling code here:
String SMS_title = titleContent.getText();
String SMS_addressee = addresseeContent.getText();
//检查标题是否输入
if(SMS_title.equals("") == true) {
JOptionPane.showMessageDialog(null,"请输入标题!", null, JOptionPane.ERROR_MESSAGE);
}
//检查收件人是否输入
else if(SMS_addressee.equals("") == true){
JOptionPane.showMessageDialog(null,"请输入收件人!", null, JOptionPane.ERROR_MESSAGE);
}
//检查收件人是否存在
else if(MySqlEmailsAction.selectUsername(MD5Util.getMD5Str(SMS_addressee)) == false) {
JOptionPane.showMessageDialog(null,"收件人不存在!", null, JOptionPane.ERROR_MESSAGE);
}
else {
Emails new_email = new Emails();
//读取界面中的短信信息
new_email.setTitle(SMS_title);
new_email.setUser(SMS_addressee);
new_email.setText(textContent.getText());
//写入用户名作为发件人信息
new_email.setAddresser(LoginGui.now_user.getUsername());
//获取当前系统时间
Timestamp now_time = new Timestamp(System.currentTimeMillis());
new_email.setTime(now_time);
//将短信发送到数据库中
if(MySqlEmailsAction.sendEmail(new_email) == true) {
JOptionPane.showMessageDialog(null,"发送成功!", null, JOptionPane.ERROR_MESSAGE);
this.dispose();
}
else {
JOptionPane.showMessageDialog(null,"发送失败!", null, JOptionPane.ERROR_MESSAGE);
}
}
}
功能测试
若用户写短信时没有输入标题,则程序需要提醒用户输入。
用户没有输入收件人时,也要提醒用户输入。
由于不能向不存在的用户发送短信,因此需要对收件人的存在性进行检查。
当输入存在的收件人时再发送短信,即可将该短信写入数据库中。
查看短信
查看收件箱
当用户要拉取收件箱时,需要调用已经实例化的用户对象的 setOutbox() 方法。
SQL 查询语句
想要获取接收到的邮件,可以根据 emails 表中的 user 字段查找。若一条记录的 user 字段是当期登录用户的用户名则添加到结果集中,使用的 SQL 查询语句为:
SELECT * FROM emails WHERE binary user = '%s' ORDER BY id DESC;
由于 emails 表的 id 字段设置了自动增量,因此新的短信会被放到 emails 表尾中。因此我们在查询时指定查询结果根据 id 字段降序排序,即可实现新消息在前的效果。
GUI 设计
界面初始化
当收件箱界面被打开时,当前 Customer 对象的 Inbox 字段会自动调用 setInbox() 方法拉取数据。若收件箱为空则返回菜单页面,若不为空则把最新的短信的各个字段填充到界面上。
界面初始化的代码如下:
public viewInboxGUI() {
initComponents();
setLocationRelativeTo(null);
//拉取收件箱
LoginGui.now_user.setInbox();
if(LoginGui.now_user.getInbox() == null || LoginGui.now_user.getInbox().size() == 0) {
JOptionPane.showMessageDialog(null,"未收到任何短信!", null, JOptionPane.ERROR_MESSAGE);
this.dispose();
}
else {
//初始化页面
addresserContent.setText(LoginGui.now_user.getInbox().get(email_idx).getAddresser());
timeContent.setText(LoginGui.now_user.getInbox().get(email_idx).getTime().toString());
titleContent.setText(LoginGui.now_user.getInbox().get(email_idx).getTitle());
addresseeContent.setText(LoginGui.now_user.getInbox().get(email_idx).getUser());
text.setText(LoginGui.now_user.getInbox().get(email_idx).getText());
}
}
查看上一篇
由于 Customer 对象的 Inbox 字段是 LinkedList 结构,因此上一篇只不过是访问当前短信的索引减 1 的邮件而已。若已是最新的一篇,则输出提示信息。
“上一篇”按钮的代码如下:
private void lastActionPerformed(java.awt.event.ActionEvent evt) {
// TODO add your handling code here:
if(email_idx - 1 < 0) {
JOptionPane.showMessageDialog(null,"已是第一篇!", null, JOptionPane.ERROR_MESSAGE);
}
else {
email_idx -= 1;
addresserContent.setText(LoginGui.now_user.getInbox().get(email_idx).getAddresser());
timeContent.setText(LoginGui.now_user.getInbox().get(email_idx).getTime().toString());
titleContent.setText(LoginGui.now_user.getInbox().get(email_idx).getTitle());
addresseeContent.setText(LoginGui.now_user.getInbox().get(email_idx).getUser());
text.setText(LoginGui.now_user.getInbox().get(email_idx).getText());
}
}
查看下一篇
和查看上一篇一样,只不过这里是查看当前短信的索引加 1 的邮件而已。
若已是最后一篇,则输出提示信息。
“下一篇”按钮的代码如下:
private void nextActionPerformed(java.awt.event.ActionEvent evt) {
// TODO add your handling code here:
if(email_idx + 1 == LoginGui.now_user.getInbox().size()) {
JOptionPane.showMessageDialog(null,"已是最后一篇!", null, JOptionPane.ERROR_MESSAGE);
}
else {
email_idx += 1;
addresserContent.setText(LoginGui.now_user.getInbox().get(email_idx).getAddresser());
timeContent.setText(LoginGui.now_user.getInbox().get(email_idx).getTime().toString());
titleContent.setText(LoginGui.now_user.getInbox().get(email_idx).getTitle());
addresseeContent.setText(LoginGui.now_user.getInbox().get(email_idx).getUser());
text.setText(LoginGui.now_user.getInbox().get(email_idx).getText());
}
}
查看发件箱
除了进行 SQL 查询时是对 addresser 字段查找,其他的细节和收件箱的完全一样。
SELECT * FROM emails WHERE binary addresser = '%s' ORDER BY id DESC;
GUI 的设计同理。
源码链接
参考资料
Java 程序设计——登录系统
Java DAO 模式
Mysql 局域网远程连接设置——Windows
MySQL学习----各种字符的长度总结
uml图六种箭头的含义