先說一下,我們公司是六點下班,超過7點開始算加班,但是加班的時間是從六點開始計算,以0.5個小時為計數,就是你到了六點半,不算加班半小時,但是加班到七點半,就是加班了一個半小時。
一、打卡記錄
首先,看一下我們公司的打卡記錄,公司的打卡工具是不區分上下班的,而且一天可以打多次,也可能忘記打卡,這都是有可能的,人在觀察這些數據的時候,可以輕易的分辨出什么是上班時間,什么是下班時間,並且是不是我忘打卡了,但是一旦放到程序里,判斷的邏輯就復雜了。
二、加班申請單
加班申請單,也就是程序最后需要導出的Word文件,樣子是這樣的,頁眉處是公司的LOGO,中部是標題,然后是一個表格
三、小工具效果演示
姓名部分如果不用選擇按鈕,選擇部門人員名單的話,也可以手動填寫
備注、申請人和申請時間是可以不填寫的,如果不填寫,則最后導出的Word相應的位置就是空。
四、開發過程
1、由於我們的加班申請單,是有固定的幾個部分組成的,姓名,加班類型,加班時間起、止,小時數和備注,因此,創建了一個實體類OverTimeModel

using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.ComponentModel; using System.Collections; namespace SelectWorkOvertime { public class OverTimeModel : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; private void INotifyPropertyChanged(string name) { if(PropertyChanged!=null) { PropertyChanged(this, new PropertyChangedEventArgs(name)); } } private string name;//姓名 private string overtimeType;//加班類型 private string overtimeStart;//加班時間起 private string overtimeEnd;//加班時間止 private string overtimeHours;//小時數 private string remark;//備注 public string Name//姓名 { get { return name; } set { name = value; INotifyPropertyChanged("Name"); } } public string OvertimeType//加班類型 { get { return overtimeType; } set { overtimeType = value; INotifyPropertyChanged("OvertimeType"); } } public string OvertimeStart//加班時間起 { get { return overtimeStart; } set { overtimeStart = value; INotifyPropertyChanged("OvertimeStart"); } } public string OvertimeEnd//加班時間止 { get { return overtimeEnd; } set { overtimeEnd = value; INotifyPropertyChanged("OvertimeEnd"); } } public string OvertimeHours//小時數 { get { return overtimeHours; } set { overtimeHours = value; INotifyPropertyChanged("OvertimeHours"); } } public string Remark//備注 { get { return remark; } set { remark = value; INotifyPropertyChanged("Remark"); } } } }
2、需要寫要給工具類,這個工具類包含的內容如下:
讀取Excel(打卡記錄是Excel文件)
讀取TXT文檔(部門人員名單是TXT文件)
判斷Excel中的所有時間是工作日還周末,還是節假日(加班類型是有這三種的,因為有的時候節假日加班我們不用調休,給雙倍工資)
計算兩個時間的小時差、獲取上一天等

using System; using System.Collections.Generic; using System.Data; using System.Data.OleDb; using System.IO; using System.Linq; using System.Net; using System.Text; using System.Threading.Tasks; namespace SelectWorkOvertime { public class Tools { /// <summary> /// 讀取TXT文檔 /// </summary> /// <param name="path"></param> /// <returns></returns> public string ReadTxt(string path) { string lines = ""; StreamReader streamReader = new StreamReader(path, Encoding.Default); string line; while ((line = streamReader.ReadLine()) != null) { lines += line.ToString() + " "; } return lines; } /// <summary> /// 調用遠程接口 /// </summary> /// <param name="date"></param> /// <returns></returns> public string IsHoliday(string date) { string url = @"http://www.easybots.cn/api/holiday.php?d="; url = url + date; HttpWebRequest httpRequest = (HttpWebRequest)WebRequest.Create(url); httpRequest.Timeout = 20000; httpRequest.Method = "GET"; HttpWebResponse httpResponse = (HttpWebResponse)httpRequest.GetResponse(); StreamReader sr = new StreamReader(httpResponse.GetResponseStream(), System.Text.Encoding.GetEncoding("gb2312")); string result = sr.ReadToEnd(); result = result.Replace("\r", "").Replace("\n", "").Replace("\t", ""); int status = (int)httpResponse.StatusCode; sr.Close(); return result; } /// <summary> /// 讀取Excel到Dataset /// </summary> /// <param name="Path"></param> /// <param name="fileName"></param> /// <returns></returns> public DataSet ExcelToDS(string Path, string fileName) { string strConn = "Provider=Microsoft.Jet.OLEDB.4.0;" + "Data Source=" + Path + ";" + "Extended Properties=Excel 8.0;"; OleDbConnection conn = new OleDbConnection(strConn); conn.Open(); string strExcel = ""; OleDbDataAdapter myCommand = null; DataSet ds = null; strExcel = "select * from [" + fileName + "$]"; myCommand = new OleDbDataAdapter(strExcel, strConn); ds = new DataSet(); myCommand.Fill(ds, "table1"); return ds; } /// <summary> /// 獲取日期的類型,是工作日,周末或節假日 /// </summary> /// <param name="dateTime"></param> /// <returns></returns> public string getDateType(string dateTime) { string date = Convert.ToDateTime(dateTime.Split(' ')[0].ToString()).ToString("yyyyMMdd");//獲得到日期 string isHoliday = IsHoliday(date); string numHoliday = isHoliday.Substring(isHoliday.Length - 3, 1); if (numHoliday == "1" || numHoliday == "2")//判斷是不是節假日{2},或者周末{1} { return numHoliday; } else return "0";//返回工作日{0} } /// <summary> /// 獲取小時值 /// </summary> /// <param name="dateTime"></param> /// <returns></returns> public string getTimeHour(string dateTime) { string timeSFM = dateTime.Split(' ')[1].ToString();//時分秒 string timeHour = timeSFM.Split(':')[0].ToString(); return timeHour; } /// <summary> /// 計算小時差值 正常情況 /// </summary> /// <param name="dateStart"></param> /// <param name="dateEnd"></param> /// <returns></returns> public string CalTimesNormal(DateTime dateStart, DateTime dateEnd) { string numTime; TimeSpan ts = dateEnd.Subtract(dateStart); if (ts.Minutes >= 30) { numTime = (ts.Hours + 0.5).ToString(); } else { numTime = ts.Hours.ToString(); } return numTime; } /// <summary> /// 計算小時差值 上班時間在12點之前的加班要減去1小時 /// </summary> /// <param name="dateStart"></param> /// <param name="dateEnd"></param> /// <returns></returns> public string CalTimesAllDay(DateTime dateStart, DateTime dateEnd) { string numTime; TimeSpan ts = dateEnd.Subtract(dateStart); if (ts.Minutes >= 30) { numTime = (ts.Hours + 0.5 - 1).ToString(); } else { numTime = (ts.Hours - 1).ToString(); } return numTime; } /// <summary> /// 獲取上一天 /// </summary> /// <param name="dateTime"></param> /// <returns></returns> public string getDateBefore(string dateTime) { string dateBefore = Convert.ToDateTime(dateTime).AddDays(-1).ToString(); return dateBefore; } /// <summary> /// 比較兩個時間是否相同 /// </summary> /// <param name="dateTime1"></param> /// <param name="dateTime2"></param> /// <returns></returns> public int getCompareDate(string dateTime1, string dateTime2) { DateTime dt1 = Convert.ToDateTime(dateTime1.Split(' ')[0].ToString()); DateTime dt2 = Convert.ToDateTime(dateTime2.Split(' ')[0].ToString()); if (dt1 == dt2) return 1; return 0; } } }
3、寫一個設定加班開始時間的類
因為由於規則不一樣,因此加班的開始時間是不一樣的,例如:
周一到周五 18:00以后下班,加班不超過當天或者加班到第二天
周末或者假日 上班時間為9:00 之前或9:00之后
周末或者假日 打卡時間在12點到13點之間

using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace SelectWorkOvertime { /// <summary> /// 計算在不同的規則下加班的開始時間 /// </summary> public class OverTimeStart { Tools tools = new Tools(); /// <summary> /// 周一到周五 18:00以后下班,加班不超過當天 /// </summary> /// <param name="dateTime">當天下班打卡時間</param> /// <returns></returns> public string OverTimeStart1(string dateTime) { DateTime dateTimeStart = Convert.ToDateTime(dateTime.Split(' ')[0].ToString() + " 18:00:00"); return dateTimeStart.ToString("yyyy-MM-dd HH:mm"); } /// <summary> /// 周一到周五 18:00以后下班,加班到第二天 /// </summary> /// <param name="dateTime">當天下班打卡時間</param> /// <returns></returns> public string OverTimeStart2(string dateTime) { DateTime dateTimeStart = Convert.ToDateTime(tools.getDateBefore(dateTime).Split(' ')[0].ToString() + " 18:00:00"); return dateTimeStart.ToString("yyyy-MM-dd HH:mm"); } /// <summary> /// 周末或者假日 上班時間為9:00 之前 /// </summary> /// <param name="dateTime">當天上班打卡時間</param> /// <returns></returns> public string OverTimeStart3(string dateTime) { DateTime dateTimeStart = Convert.ToDateTime(dateTime.Split(' ')[0].ToString() + " 9:00:00"); return dateTimeStart.ToString("yyyy-MM-dd HH:mm"); } /// <summary> /// 周末或者假日 上班時間為9:00 之后 /// </summary> /// <param name="dateTime">當天上班打卡時間</param> /// <returns></returns> public string OverTimeStart4(string dateTime) { DateTime dateTimeStart = Convert.ToDateTime(dateTime); return dateTimeStart.ToString("yyyy-MM-dd HH:mm"); } /// <summary> /// 周末或者假日 打卡時間在12點到13點之間 /// </summary> /// <param name="dateTime">當天上班打卡時間</param> /// <returns></returns> public string OverTimeStart5(string dateTime) { DateTime dateTimeStart = Convert.ToDateTime(dateTime.Split(' ')[0].ToString() + " 13:00:00"); return dateTimeStart.ToString("yyyy-MM-dd HH:mm"); } } }
4、寫主頁面的內容
由於公司沒有重名的,因此在讀取了Excel后,只從里面取了姓名和時間字段,其他字段都拋棄掉,而加班申請表上的“加班類型,加班時間起、止,小時數”這四列,都是根據時間字段來進行計算得出的。
由於打卡時間本身的問題,我在前面也提到了,可能存在重復打卡,漏打的問題,所以,在計算加班上,就會出現不准的現象,所以為了提醒小伙伴,就用了一個轉換器,把加班小於0的,顯示為紅色;小於0.5小時的,顯示為黃色;大於12小時的顯示為綠色,大於12小時是有可能正確的,但是一般不會出現,所以提醒一下比較好,而小於0的,就是數據不正確了,如圖
小冉和王哥對應的周末加班上,由於顏色標注,就知道應該是數據方面可能存在問題了,這樣就要去查看原始的打卡記錄,如圖
小冉的是沒有問題的
王哥是因為都打了兩次卡,導致軟件在邏輯判別時出現了問題,所以加班記錄里的那條負值就可以在導出的Word里刪除掉了。
5、導出
導出Word的話,就是常用的C#操作Word,沒什么。
6、總結
軟件本身在技術上不是很難,用到異步線程、Linq、對文件的操作、轉換器,基本都是常用的技術,難點其實就是在邏輯的判斷上,無法做到全面的無誤的去合理的計算出加班時間和加班類型,十分感謝彭哥,因為在寫第一個版本的時候,讓邏輯都快搞崩潰了,是彭哥告訴我,可以寫每一個小的邏輯,然后再把小的邏輯組合成大的邏輯。總起來說,做這個小軟件還有有技術上和思維上的收獲的,仁者見仁智者見智吧,最起碼,以后基本上不需要人手動的去寫加班申請了,誰讓懶是程序員的天性呢。
注:軟件存在以下BUG
1、還是邏輯上不完善
2、由於是調用的WEB上提供的接口去判斷,是工作日、節假日、周末,例如閱兵就是節假日,所以,軟件運行時需要聯網
希望園里的朋友批評指正,大家一起探討,共同進步。