前期調研
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圖六種箭頭的含義