元旦三天假期,實現一個電商退單管理系統【二】
一、倉庫掃碼監聽客戶端實現
(一)功能分析
快遞小哥騎著小三輪,運送退貨快遞到倉庫,庫管開啟客戶端,選擇快遞公司後,遞給快遞一把掃碼槍,小哥滴滴滴,滴滴滴,一頓操作猛如虎,打完收功。倉管將資料提交伺服器,列印回單,整個客戶端流程結束。
倉庫的客戶端需要監聽掃碼槍輸入,計劃使用C#編寫一個托盤程式,負責訂單的接收,以及提交伺服器端、列印回單等任務,同時還能查詢歷史訂單資訊等。
主要功能如下:
使用者登入:呼叫服務端介面,驗證使用者身份。
選擇快遞公司:呼叫服務端介面,查詢所有快遞公司列表,為了直觀,直接顯示快遞公司的Logo,供使用者選擇。
掃碼監聽:監聽輸入,語音提醒,並顯示日誌,對於不符合快遞單號長度、重複單號等錯誤予以提醒。
提交伺服器:將本地快取的掃碼單,傳到伺服器端,再根據服務端返回結果,更新本地快取狀態(同步成功、訂單號重複、其他錯誤等)。
列印今日回單:列印該快遞當前日期的回單。
列印當前頁面回單:列印本次掃碼訂單。
檢視歷史訂單:檢視歷史訂單,顯示是否已同步成功。
(二)程式碼實現
1. 基礎類編寫
好久沒有寫c#了,現在居然是MVVM了,找了一個MVVMLight,然後又找了一個materialDesign的面板(網上文件太少,專案中用的不是太多),把介面搭建起來。
因為要與服務端通訊,從網上找了一個Http傳輸的類庫,做了點修改,加了一個async的請求方法。其他業務只要呼叫HttpGet、HttpPost、HttpGetAsync就可以了。基本上都是用的get方法,只有資料同步介面,因為要post json上去,才用了post方法。
using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Net.Security; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading.Tasks; namespace ordermanage.Common { public class HttpRequestHelper { public static string HttpPost(string Url, string postDataStr) { //獲取提交的位元組 byte[] bs = Encoding.UTF8.GetBytes(postDataStr); //設定提交的相關引數 HttpWebRequest req = (HttpWebRequest)HttpWebRequest.Create(Url); req.Method = "POST"; req.ContentType = "application/x-www-form-urlencoded"; req.ContentLength = bs.Length; //提交請求資料 Stream reqStream = req.GetRequestStream(); reqStream.Write(bs, 0, bs.Length); reqStream.Close(); //接收返回的頁面,必須的,不能省略 WebResponse wr = req.GetResponse(); System.IO.Stream respStream = wr.GetResponseStream(); System.IO.StreamReader reader = new System.IO.StreamReader(respStream, System.Text.Encoding.GetEncoding("utf-8")); string t = reader.ReadToEnd(); return t; } public static string HttpGet(string Url, string postDataStr) { HttpWebRequest request = (HttpWebRequest)WebRequest.Create(Url + (postDataStr == "" ? "" : "?") + postDataStr); request.Method = "GET"; request.ContentType = "application/json"; string retString; HttpWebResponse response = (HttpWebResponse)request.GetResponse(); Stream myResponseStream = response.GetResponseStream(); StreamReader myStreamReader = new StreamReader(myResponseStream, Encoding.GetEncoding("utf-8")); retString = myStreamReader.ReadToEnd(); myStreamReader.Close(); myResponseStream.Close(); return retString; } public static async Task<string> HttpGetAsync(string Url, string postDataStr) { HttpWebRequest request = (HttpWebRequest)WebRequest.Create(Url + (postDataStr == "" ? "" : "?") + postDataStr); request.Method = "GET"; request.ContentType = "application/json"; string retString; HttpWebResponse response = (HttpWebResponse)await getServerResponseSync(request); Stream myResponseStream = response.GetResponseStream(); StreamReader myStreamReader = new StreamReader(myResponseStream, Encoding.GetEncoding("utf-8")); retString = myStreamReader.ReadToEnd(); myStreamReader.Close(); myResponseStream.Close(); return retString; } public static async Task<WebResponse> getServerResponseSync(HttpWebRequest request) { return await request.GetResponseAsync(); } /// <summary> /// 建立GET方式的HTTP請求 /// </summary> //public static HttpWebResponse CreateGetHttpResponse(string url, int timeout, string userAgent, CookieCollection cookies) public static HttpWebResponse CreateGetHttpResponse(string url) { HttpWebRequest request = null; if (url.StartsWith("https", StringComparison.OrdinalIgnoreCase)) { //對服務端證書進行有效性校驗(非第三方權威機構頒發的證書,如自己生成的,不進行驗證,這裡返回true) ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(CheckValidationResult); request = WebRequest.Create(url) as HttpWebRequest; request.ProtocolVersion = HttpVersion.Version10; //http版本,預設是1.1,這裡設定為1.0 } else { request = WebRequest.Create(url) as HttpWebRequest; } request.Method = "GET"; //設定代理UserAgent和超時 //request.UserAgent = userAgent; //request.Timeout = timeout; //if (cookies != null) //{ // request.CookieContainer = new CookieContainer(); // request.CookieContainer.Add(cookies); //} return request.GetResponse() as HttpWebResponse; } /// <summary> /// 建立POST方式的HTTP請求 /// </summary> //public static HttpWebResponse CreatePostHttpResponse(string url, IDictionary<string, string> parameters, int timeout, string userAgent, CookieCollection cookies) public static HttpWebResponse CreatePostHttpResponse(string url, IDictionary<string, string> parameters) { HttpWebRequest request = null; //如果是傳送HTTPS請求 if (url.StartsWith("https", StringComparison.OrdinalIgnoreCase)) { //ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(CheckValidationResult); request = WebRequest.Create(url) as HttpWebRequest; //request.ProtocolVersion = HttpVersion.Version10; } else { request = WebRequest.Create(url) as HttpWebRequest; } request.Method = "POST"; request.ContentType = "application/json"; //設定代理UserAgent和超時 //request.UserAgent = userAgent; //request.Timeout = timeout; //if (cookies != null) //{ // request.CookieContainer = new CookieContainer(); // request.CookieContainer.Add(cookies); //} //傳送POST資料 if (!(parameters == null || parameters.Count == 0)) { StringBuilder buffer = new StringBuilder(); int i = 0; foreach (string key in parameters.Keys) { if (i > 0) { buffer.AppendFormat("&{0}={1}", key, parameters[key]); } else { buffer.AppendFormat("{0}={1}", key, parameters[key]); i++; } } byte[] data = Encoding.ASCII.GetBytes(buffer.ToString()); using (Stream stream = request.GetRequestStream()) { stream.Write(data, 0, data.Length); } } string[] values = request.Headers.GetValues("Content-Type"); return request.GetResponse() as HttpWebResponse; } /// <summary> /// 獲取請求的資料 /// </summary> public static string GetResponseString(HttpWebResponse webresponse) { using (Stream s = webresponse.GetResponseStream()) { StreamReader reader = new StreamReader(s, Encoding.UTF8); return reader.ReadToEnd(); } } /// <summary> /// 驗證證書 /// </summary> private static bool CheckValidationResult(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors errors) { if (errors == SslPolicyErrors.None) return true; return false; } } }
再寫一個通用返回體,c#裡的泛型真是太好用了,居然可以ResponseMsg<List<Backorder>>這樣直接與json之間直接轉換。code應該設定個列舉值。這裡偷懶了。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; /// <summary> /// 通用返回體,用於伺服器互動 /// </summary> namespace ordermanage.Common { public class ResponseMsg<T> { public int code { get; set; } public string msg { get; set; } public T data { get; set; } public int mark1; public string mark2; } }
再封裝幾個方法用與網路請求,並返回ResponseMsg型別。服務端介面是與之對應的返回體。code,msg,data三個欄位。mark1,mark2備用,如部分介面需要傳回總記錄數或總頁數等。
getSign是簽名認證方法,對伺服器端介面進行保護,大概就是流水和引數拼接,再字典排序,再加密。這裡就不放出來了
private static async Task<string> getServerResponseAsync(string method, string otherParams) { string ts = getTimeStamp(); string transid = getTrnasID(ts); string sign = getSign(transid); string strParams = "method=" + method + "&transid="+transid+"&ts="+ts + "&sign="+sign; if (!string.IsNullOrEmpty(otherParams)) { strParams += "&" + otherParams; } try { string result = await HttpRequestHelper.HttpGetAsync(ServerUrl, strParams); return result; } catch (Exception e) { return "{\"code\":500,\"msg\":\"" + e.Message + "\",\"data\":null}"; } } private static string getServerResponseSync(string method, string otherParams) { string ts = getTimeStamp(); string transid = getTrnasID(ts); string sign = getSign(transid); string strParams = "method=" + method + "&transid=" + transid + "&ts=" + ts + "&sign=" + sign; if (!string.IsNullOrEmpty(otherParams)) { strParams += "&" + otherParams; } try { string result = HttpRequestHelper.HttpGet(ServerUrl, strParams); return result; } catch (Exception e) { return "{\"code\":500,\"msg\":\"" + e.Message + "\",\"data\":null}"; } } private static string getServerResponseWithPostMethod(string method, string otherParams) { string ts = getTimeStamp(); string transid = getTrnasID(ts); string sign = getSign(transid); string strParams = "method=" + method + "&transid=" + transid + "&ts=" + ts + "&sign=" + sign; if (!string.IsNullOrEmpty(otherParams)) { strParams += "&" + otherParams; } try { string result = HttpRequestHelper.HttpPost(ServerUrl, strParams); return result; } catch (Exception e) { return "{\"code\":500,\"msg\":\"" + e.Message + "\",\"data\":null}"; } }
本地資料快取使用的是SQLite,SqLiteBaseRepository類用來獲取Connection,如果資料庫檔案不存在,則從安裝目錄拷貝一份空庫。
namespace ordermanage.DB { public class SqLiteBaseRepository { public static string DbFile { get { return Environment.CurrentDirectory + "\\orderdb.sqlite"; } } public static SQLiteConnection DbConnection() { //如果資料庫檔案不存在,則從源位置複製一份 string dbFolder = @"C:\退單管理"; string dbFileName = "orderdb.sqlite"; if (!Directory.Exists(dbFolder)) { Directory.CreateDirectory(dbFolder); } if (!File.Exists(dbFolder + "\\" + dbFileName)) { File.Copy(DbFile, dbFolder + "\\orderdb.sqlite",true); } return new SQLiteConnection("Data Source=" + dbFolder + "\\" + dbFileName + ";Pooling=true;FailIfMissing=false;Version=3;UTF8Encoding=True;Journal Mode=Off;"); } } }
業務比較簡,沒有分層,在業務邏輯層寫了SQL語句。就一張表,所有業務全部在BackorderBLL裡了。
using Dapper; using ordermanage.Common; using ordermanage.Model; using System; using System.Collections.Generic; using System.Data.SQLite; using System.Linq; namespace ordermanage.DB { public class BackorderBLL { private SQLiteConnection conn = null; public BackorderBLL() { conn = SqLiteBaseRepository.DbConnection(); } /// <summary> /// 根據快遞公司獲取所有退單列表 /// </summary> /// <param name="express_id"></param> /// <returns></returns> public ResponseMsg<List<Backorder>> getBackorderList(int express_id) { conn.Open(); List<Backorder> list = conn.Query<Backorder>(@"SELECT * FROM tb_backorder WHERE express_id=@express_id ORDER BY backorder_date DESC", new { express_id}).ToList(); conn.Close(); return new ResponseMsg<List<Backorder>>() {code=100,msg="",data=list }; } /// <summary> /// 獲取待同步清單(指定快遞公司) /// </summary> /// <param name="express_id"></param> /// <returns></returns> public ResponseMsg<List<Backorder>> getWaitforSyncBackorderList(int express_id) { conn.Open(); List<Backorder> list = conn.Query<Backorder>(@"SELECT * FROM tb_backorder WHERE express_id=@express_id AND sync_flag=0 ORDER BY backorder_date ASC", new { express_id }).ToList(); conn.Close(); return new ResponseMsg<List<Backorder>>() { code = 100, msg = "", data = list }; } /// <summary> /// 獲取待同步伺服器訂單(所有快遞公司) /// </summary> /// <returns></returns> public ResponseMsg<List<Backorder>> getWaitforSyncBackorderList() { conn.Open(); List<Backorder> list = conn.Query<Backorder>(@"SELECT * FROM tb_backorder WHERE sync_flag=0 ORDER BY backorder_date ASC").ToList(); conn.Close(); return new ResponseMsg<List<Backorder>>() { code = 100, msg = "", data = list }; } /// <summary> /// 新增一行退單 /// </summary> /// <param name="order"></param> /// <returns></returns> public ResponseMsg<Backorder> addNewBackorder(Backorder order) { conn.Open(); //如果訂單長度不符合要求的話? if (order.backorder_code.Length < 12) { conn.Close(); return new ResponseMsg<Backorder> { code = 202, msg = "快遞單號長度不符合要求", data = null }; } else { //如果訂單號存在的話? Backorder backorder = conn.Query<Backorder>(@"SELECT * FROM tb_backorder WHERE backorder_code=@backorder_code", new { order.backorder_code }).FirstOrDefault(); if (backorder != null) { conn.Close(); return new ResponseMsg<Backorder> { code = 203, msg = "快遞單號已存在", data = null }; } else { string sql = "INSERT INTO tb_backorder(backorder_code,backorder_date,userid,express_id,remark,seq_no) VALUES (@backorder_code,@backorder_date,@userid,@express_id,@remark,@seq_no)"; int result = conn.Execute(sql, new { order.backorder_code, order.backorder_date, order.userid, order.express_id, order.remark,order.seq_no }); //單機模式,可以立即獲取當前記錄 order = conn.Query<Backorder>("SELECT * FROM tb_backorder ORDER BY backorder_id DESC").FirstOrDefault(); //同時更新今日退單數量 int count = conn.Query<int>("SELECT * FROM tb_backorder WHERE express_id=? AND backorder_date between datetime('now','start of day','+0 seconds') and datetime('now','start of day','+1 days','-1 seconds')", new { order.express_id }).Count(); //再同時更新待同步伺服器數量 int count2 = conn.Query<int>("SELECT * FROM tb_backorder WHERE express_id=? AND sync_flag=0", new { order.express_id }).Count(); conn.Close(); return new ResponseMsg<Backorder> { code = 100, msg = "", data = order, mark1 = count,mark2=count2.ToString() }; } } } //更新一個訂單的同步狀態和備註 public ResponseMsg<bool> updateBackorderSysncStatus(Backorder backorder) { string sql = "UPDATE tb_backorder SET sync_flag=@sync_flag,remark=@remark WHERE backorder_code=@backorder_code"; int result = conn.Execute(sql, new { backorder.sync_flag,backorder.remark,backorder.backorder_code}); conn.Close(); return new ResponseMsg<bool>() { code = 100, msg = "", data = result > 0 }; } /// <summary> /// 當日退單數量及待同步服務的訂單數 /// </summary> /// <param name="express_id"></param> /// <returns></returns> public ResponseMsg<int> getDayBackorderCount(int express_id) { conn.Open(); int count = conn.Query<int>("SELECT * FROM tb_backorder WHERE express_id=? AND backorder_date between datetime('now','start of day','+0 seconds') and datetime('now','start of day','+1 days','-1 seconds')", new { express_id }).Count(); int count2 = conn.Query<int>("SELECT * FROM tb_backorder WHERE express_id=? AND sync_flag=0",new { express_id}).Count(); conn.Close(); return new ResponseMsg<int>() { code = 100, msg = "", data = count, mark1 = count2 }; } /// <summary> /// 統計當日退單數量及待同步伺服器數量(所有快遞公司) /// </summary> /// <returns></returns> public ResponseMsg<int> getDayBackorderCount() { conn.Open(); int count = conn.Query<int>("SELECT * FROM tb_backorder WHERE backorder_date between datetime('now','start of day','+0 seconds') and datetime('now','start of day','+1 days','-1 seconds')").Count(); int count2 = conn.Query<int>("SELECT * FROM tb_backorder WHERE sync_flag=0").Count(); conn.Close(); return new ResponseMsg<int>() { code = 100, msg = "", data = count, mark1 = count2 }; } /// <summary> /// 執行一條SQL語句 /// </summary> public void ExecuteSql(string sql) { try { conn.Execute(sql); } catch (Exception) { } } } }View Code
2. 登入實現
好了,接下來就可以呼叫網路方法了,通過NewtonSoft.json轉換為通用返回物件,以系統登入為例:
/// <summary> /// 登入方法 /// </summary> /// <param name="user_code"></param> /// <param name="password"></param> /// <returns></returns> public static async Task<ResponseMsg<UserModel>> Login(string user_code, string password) { string result= await getServerResponseAsync("login", "uname=" + user_code + "&ucode=" + password); return JsonConvert.DeserializeObject<ResponseMsg<UserModel>>(result); }
登入介面:
<Window x:Class="ordermanage.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:ordermanage" xmlns:uc="clr-namespace:ordermanage.UC" xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity" xmlns:common="clr-namespace:ordermanage.Common" mc:Ignorable="d" DataContext="{Binding Source={StaticResource Locator},Path=Main}" TextElement.Foreground="{DynamicResource MaterialDesignBody}" TextElement.FontWeight="Medium" TextElement.FontSize="14" WindowStartupLocation="CenterScreen" FontFamily="pack://application:,,,/MaterialDesignThemes.Wpf;component/Resources/Roboto/#Roboto" Title="MainWindow" Height="450" Width="800" ResizeMode="NoResize" WindowStyle="None" MouseLeftButtonDown="Window_MouseLeftButtonDown" Icon="Images/掃碼-01.png"> <Window.Resources> <ResourceDictionary> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Button.xaml" /> <ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.DialogHost.xaml" /> </ResourceDictionary.MergedDictionaries> </ResourceDictionary> </Window.Resources> <Window.Background> <ImageBrush ImageSource="Images/bg.jpg"/> </Window.Background> <Grid> <Grid.RowDefinitions> <RowDefinition Height="40"></RowDefinition> <RowDefinition Height="300"></RowDefinition> <RowDefinition Height="*"></RowDefinition> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="100"></ColumnDefinition> <ColumnDefinition Width="600"></ColumnDefinition> <ColumnDefinition Width="*"></ColumnDefinition> </Grid.ColumnDefinitions> <uc:LoadingWait x:Name="_loading" Visibility="Collapsed" Grid.Row="0" Grid.Column="0" Grid.RowSpan="3" Grid.ColumnSpan="3" Height="450" Width="800" HorizontalAlignment="Center" VerticalAlignment="Center"></uc:LoadingWait> <TextBlock Text="電商退單管理系統" Grid.Row="0" Grid.Column="1" VerticalAlignment="Center" HorizontalAlignment="Center" Foreground="#FFFFFEFE" FontSize="24" FontWeight="Bold" ></TextBlock> <materialDesign:PackIcon Kind="Close" Foreground="White" Grid.Row="0" Grid.Column="2" Cursor="Hand" HorizontalAlignment="Right" VerticalAlignment="Center" Margin="0,0,6,0" Height="24" Width="24" > <i:Interaction.Triggers> <i:EventTrigger EventName="MouseLeftButtonDown"> <i:InvokeCommandAction Command="{Binding ExitCommand}"></i:InvokeCommandAction> </i:EventTrigger> </i:Interaction.Triggers> </materialDesign:PackIcon> <materialDesign:PackIcon Kind="Cog" Grid.Row="0" Grid.Column="2" Width="24" Height="24" Foreground="White" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0,0,35,0" Cursor="Hand"> <i:Interaction.Triggers> <i:EventTrigger EventName="MouseLeftButtonDown"> <i:InvokeCommandAction Command="{Binding SystemSetCommand}"></i:InvokeCommandAction> </i:EventTrigger> </i:Interaction.Triggers> </materialDesign:PackIcon> <TextBox Name="txtUserCode" Grid.Row="1" Grid.Column="1" Width="260" VerticalAlignment="Center" HorizontalAlignment="Center" Style="{StaticResource MaterialDesignFloatingHintTextBox}" materialDesign:HintAssist.Hint="登入名" Text="{Binding UserCode}" Foreground="#FF030B55" FontWeight="Bold"></TextBox> <PasswordBox x:Name="txtPassword" Grid.Row="1" Grid.Column="1" VerticalAlignment="Center" HorizontalAlignment="Center" Width="260" Margin="0,110,0,0" common:PasswordBoxHelper.Password="{Binding Password,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" materialDesign:HintAssist.Hint="密碼" Foreground="#FF030B55" Style="{StaticResource MaterialDesignFloatingHintPasswordBox}" FontWeight="Bold" ></PasswordBox> <Button Name="btnLogin" Grid.Row="3" Grid.Column="1" Content="登入" VerticalAlignment="Top" Width="100" IsDefault="True" Command="{Binding LoginCommand}" Click="btnLogin_Click" ></Button> </Grid> </Window>View Code
登入viewmodel:
using GalaSoft.MvvmLight; using GalaSoft.MvvmLight.Command; using GalaSoft.MvvmLight.Messaging; using ordermanage.Common; using ordermanage.Model; using System; using System.Threading; using System.Threading.Tasks; using System.Windows; namespace ordermanage.ViewModel { /// <summary> /// This class contains properties that the main View can data bind to. /// <para> /// Use the <strong>mvvminpc</strong> snippet to add bindable properties to this ViewModel. /// </para> /// <para> /// You can also use Blend to data bind with the tool's support. /// </para> /// <para> /// See http://www.galasoft.ch/mvvm /// </para> /// </summary> public class MainViewModel : ViewModelBase { /// <summary> /// Initializes a new instance of the MainViewModel class. /// </summary> public MainViewModel() { ////if (IsInDesignMode) ////{ //// // Code runs in Blend --> create design time data. ////} ////else ////{ //// // Code runs "for real" ////} } private string _usercode; private string _password; public string UserCode { get { return _usercode; } set { _usercode = value;RaisePropertyChanged(); } } public string Password { get { return _password; } set { _password = value;RaisePropertyChanged(); } } /// <summary> /// 退出程式 /// </summary> public RelayCommand ExitCommand { get { return new RelayCommand(() => { Messenger.Default.Send<string>("exit", "ApplicationExitToken"); }); } } /// <summary> /// 系統設定 /// </summary> public RelayCommand SystemSetCommand { get { return new RelayCommand(()=> { }); } } public RelayCommand LoginCommand { get { return new RelayCommand(() => { if (string.IsNullOrEmpty(UserCode) || string.IsNullOrEmpty(Password)) { Messenger.Default.Send<ResponseMsg<UserModel>>(new ResponseMsg<UserModel>() { code = 200, msg = "使用者名稱密碼不能為空", data = null }, "LoginToken"); } else { Login(); } }); } } /// <summary> /// 登入實現 /// </summary> private async void Login() { ResponseMsg<UserModel> responseMsg = await HttpMethod.Login(UserCode, Password); Messenger.Default.Send<ResponseMsg<UserModel>>(responseMsg, "LoginToken"); } } }View Code
登入cs程式碼:
using GalaSoft.MvvmLight.Messaging; using ordermanage.Common; using ordermanage.Model; using System; using System.Windows; using System.Configuration; using ordermanage.View; namespace ordermanage { /// <summary> /// MainWindow.xaml 的互動邏輯 /// </summary> public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); Messenger.Default.Register<string>(this, "ApplicationExitToken", AppExit); Messenger.Default.Register<ResponseMsg<UserModel>>(this, "LoginToken", Login); } private void Login(ResponseMsg<UserModel> res) { this._loading.Visibility = Visibility.Collapsed; if (res.code == 100) { //登入成功 UserModel user = res.data; Configuration config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None); config.AppSettings.Settings["userid"].Value = user.userid.ToString(); config.AppSettings.Settings["user_code"].Value = user.user_code; config.AppSettings.Settings["user_name"].Value = user.user_name; config.Save(ConfigurationSaveMode.Modified); ConfigurationManager.RefreshSection("appSettings"); SelectExpress selectExpress = new SelectExpress(); selectExpress.Show(); this.Close(); } else { //登入失敗,顯示失敗資訊 MessageBox.Show(res.msg, "錯誤", MessageBoxButton.OK, MessageBoxImage.Error); } } private void AppExit(string obj) { if (MessageBox.Show("確實要退出程式嗎?", "提示", MessageBoxButton.OKCancel, MessageBoxImage.Question, MessageBoxResult.OK) == MessageBoxResult.OK) { this.Close(); } } public static void openWindow() { SelectExpress selectExpress = new SelectExpress(); selectExpress.Show(); } private void Window_Loaded(object sender, RoutedEventArgs e) { } private void WindowsHander_WindowsEvent1() { throw new NotImplementedException(); } private void Window_MouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e) { this.DragMove(); } private void btnLogin_Click(object sender, RoutedEventArgs e) { this._loading.Visibility = Visibility.Visible; } } }View Code
3. 選擇快遞
快遞公司不太多,圖片也沒有非同步獲取了。
介面佈局
<Window x:Class="ordermanage.View.SelectExpress" x:Name="SelectExpressWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:ordermanage.View" xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity" xmlns:common="clr-namespace:ordermanage.Common" DataContext="{Binding Source={StaticResource Locator},Path=SelectExpress}" TextElement.Foreground="{DynamicResource MaterialDesignBody}" TextElement.FontWeight="Medium" TextElement.FontSize="16" FontFamily="pack://application:,,,/MaterialDesignThemes.Wpf;component/Resources/Roboto/#Roboto" mc:Ignorable="d" ResizeMode="NoResize" WindowStartupLocation="CenterScreen" Title="SelectExpress" Height="600" Width="1000" WindowStyle="None" Activated="SelectExpressWindow_Activated"> <Window.Background> <ImageBrush ImageSource="/ordermanage;component/Images/bg.jpg"/> </Window.Background> <Window.Resources> </Window.Resources> <Grid> <Grid.RowDefinitions> <RowDefinition Height="60"></RowDefinition> <RowDefinition Height="60"></RowDefinition> <RowDefinition Height="*"></RowDefinition> </Grid.RowDefinitions> <TextBlock Text="請選擇需要掃碼退單的快遞公司" Grid.Row="0" FontSize="20" HorizontalAlignment="Center" VerticalAlignment="Center" Foreground="LawnGreen"> </TextBlock> <materialDesign:PackIcon Kind="Close" Foreground="White" Grid.Row="0" Cursor="Hand" Background="LightSeaGreen" Opacity="0.5" HorizontalAlignment="Right" VerticalAlignment="Center" Margin="0,0,6,0" Height="24" Width="24"> <i:Interaction.Triggers> <i:EventTrigger EventName="MouseLeftButtonDown"> <i:InvokeCommandAction Command="{Binding ExitCommand}"></i:InvokeCommandAction> </i:EventTrigger> </i:Interaction.Triggers> </materialDesign:PackIcon> <StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Center"> <TextBlock VerticalAlignment="Top" x:Name="tbInfo"></TextBlock> <Button Margin="10,0,0,0" VerticalAlignment="Top" Content="同步伺服器" x:Name="btnSync" Click="btnSync_Click"></Button> </StackPanel> <ListBox x:Name="ImageList" Grid.Row="2"> <ListBox.ItemsPanel> <ItemsPanelTemplate> <UniformGrid Columns="4"></UniformGrid> </ItemsPanelTemplate> </ListBox.ItemsPanel> <ListBox.ItemTemplate> <DataTemplate> <Button Width="240" Height="Auto" Command="{Binding DataContext.ExpressImageCommand,ElementName=SelectExpressWindow}" CommandParameter="{Binding express_id}" BorderThickness="0" Background="Transparent"> <Image Stretch="Fill" Source="{Binding Path=express_log}"> </Image> </Button> </DataTemplate> </ListBox.ItemTemplate> </ListBox> </Grid> </Window>View Code
有點不太習慣c#的雙向繫結方式,感覺不如vue方便。所以大部分程式碼寫到了cs檔案裡
using GalaSoft.MvvmLight.Messaging; using ordermanage.Common; using ordermanage.DB; using ordermanage.Model; using System; using System.Collections.Generic; using System.Configuration; using System.Linq; using System.Windows; namespace ordermanage.View { /// <summary> /// SelectExpress.xaml 的互動邏輯 /// </summary> public partial class SelectExpress : Window { private string ServerUrl { get { return ConfigurationManager.AppSettings["server_url"]; } } public SelectExpress() { InitializeComponent(); Messenger.Default.Register<int>(this, "SelectExpressToken", openWindow); Messenger.Default.Register<string>(this, "SelectApplicationExitToken", AppExit); ShowInfo(); ResponseMsg<List<ExpressModel>> response = HttpMethod.ExpressList(); if (response.code == 100) { List<ExpressModel> list = response.data; for (int i = 0; i < list.Count(); i++) { list[i].express_log = this.ServerUrl + "/Public/Uploads/express/" + list[i].express_log; } this.ImageList.ItemsSource = list; } //UpdateTable(); } /// <summary> /// 首次開啟,升級資料庫 /// </summary> private void UpdateTable() { var update_sql = ConfigurationManager.AppSettings["update_sql"]; Configuration config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None); if (string.IsNullOrEmpty(update_sql)) { MessageBox.Show("檔案讀取許可權出現問題,請以管理員身份開啟"+ConfigurationManager.AppSettings["userid"], "提示", MessageBoxButton.OK, MessageBoxImage.Error); this.Close(); } if ("1".Equals(update_sql)) { //更新資料庫 string sql = config.AppSettings.Settings["sql"].Value; if (!string.IsNullOrEmpty(sql)) { new BackorderBLL().ExecuteSql(sql); //更新配置 config.AppSettings.Settings["update_sql"].Value = "0"; config.Save(ConfigurationSaveMode.Modified); ConfigurationManager.RefreshSection("appSettings"); } } } private void ShowInfo() { //統計資訊 ResponseMsg<int> Count = new BackorderBLL().getDayBackorderCount(); this.tbInfo.Text = string.Format("今日共錄入退單:{0}件,待同步伺服器:{1}件", Count.data, Count.mark1); if (Count.mark1 > 0) { this.btnSync.Visibility = Visibility.Visible; } else { this.btnSync.Visibility = Visibility.Collapsed; } } private void AppExit(string obj) { string tips = "確實要退出程式嗎?"; int count = new BackorderBLL().getDayBackorderCount().mark1; if (count > 0) { tips = string.Format("你還有{0}條記錄待同步至伺服器,確定要退出了嗎?",count); } if (MessageBox.Show(tips, "提示", MessageBoxButton.OKCancel, MessageBoxImage.Question, MessageBoxResult.OK) == MessageBoxResult.OK) { this.Close(); } } private void openWindow(int obj) { Home home = new Home(obj); home.txtExpressID.Text = obj.ToString(); home.ShowDialog(); } private void btnSync_Click(object sender, RoutedEventArgs e) { //呼叫網路同步訂單 List<Backorder> list = new BackorderBLL().getWaitforSyncBackorderList().data; ResponseMsg<List<Backorder>> response = HttpMethod.SyncBackorders(list); if (response.code == 100) { //同步成功,重新整理本地資料庫狀態 foreach (Backorder order in response.data) { bool result = new BackorderBLL().updateBackorderSysncStatus(order).data; if (result) { //本地庫更新成功 } } //重新整理按鈕上的文字 ShowInfo(); MessageBox.Show("同步成功", "提示", MessageBoxButton.OK, MessageBoxImage.Information); } else { MessageBox.Show(response.msg, "錯誤", MessageBoxButton.OK, MessageBoxImage.Error); } } private void SelectExpressWindow_Activated(object sender, EventArgs e) { this.ShowInfo(); } } }View Code
4. 退貨單入庫
監聽掃碼輸入程式碼是從網上找的,會監聽所有輸入,包括鍵盤等外接裝置輸入,對單號做了一定規則判斷:
ScanHook
using System; using System.Collections.Generic; using System.Diagnostics; using System.Runtime.InteropServices; using System.Text; namespace ordermanage.Common { public class ScanHook { public delegate void ScanerDelegate(ScanerCodes codes); public event ScanerDelegate ScanerEvent; delegate int HookProc(int nCode, Int32 wParam, IntPtr lParam); private int hKeyboardHook = 0; private ScanerCodes codes = new ScanerCodes(); private HookProc hookproc; [DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)] private static extern int SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hInstance, int threadId); [DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)] private static extern bool UnhookWindowsHookEx(int idHook); [DllImport("user32", EntryPoint = "GetKeyNameText")] private static extern int GetKeyNameText(int IParam, StringBuilder lpBuffer, int nSize); [DllImport("user32", EntryPoint = "GetKeyboardState")] private static extern int GetKeyboardState(byte[] pbKeyState); [DllImport("user32", EntryPoint = "ToAscii")] private static extern bool ToAscii(int VirtualKey, int ScanCode, byte[] lpKeySate, ref uint lpChar, int uFlags); [DllImport("kernel32.dll")] public static extern IntPtr GetModuleHandle(string name); public ScanHook() { } public bool Start() { if (hKeyboardHook == 0) { hookproc = new HookProc(KeyboardHookProc); //GetModuleHandle 函式 替代 Marshal.GetHINSTANCE //防止在 framework4.0中 註冊鉤子不成功 IntPtr modulePtr = GetModuleHandle(Process.GetCurrentProcess().MainModule.ModuleName); //WH_KEYBOARD_LL=13 //全域性鉤子 WH_KEYBOARD_LL // hKeyboardHook = SetWindowsHookEx(13, hookproc, Marshal.GetHINSTANCE(Assembly.GetExecutingAssembly().GetModules()[0]), 0); hKeyboardHook = SetWindowsHookEx(13, hookproc, modulePtr, 0); } return (hKeyboardHook != 0); } public bool Stop() { if (hKeyboardHook != 0) { return UnhookWindowsHookEx(hKeyboardHook); } return true; } private int KeyboardHookProc(int nCode, Int32 wParam, IntPtr lParam) { EventMsg msg = (EventMsg)Marshal.PtrToStructure(lParam, typeof(EventMsg)); codes.Add(msg); if (ScanerEvent != null && msg.message == 13 && msg.paramH > 0 && !string.IsNullOrEmpty(codes.Result)) { ScanerEvent(codes); } return 0; } public class ScanerCodes { private int ts = 300; // 指定輸入間隔為300毫秒以內時為連續輸入 private List<List<EventMsg>> _keys = new List<List<EventMsg>>(); private List<int> _keydown = new List<int>(); // 儲存組合鍵狀態 private List<string> _result = new List<string>(); // 返回結果集 private DateTime _last = DateTime.Now; private byte[] _state = new byte[256]; private string _key = string.Empty; private string _cur = string.Empty; public EventMsg Event { get { if (_keys.Count == 0) { return new EventMsg(); } else { return _keys[_keys.Count - 1][_keys[_keys.Count - 1].Count - 1]; } } } public List<int> KeyDowns { get { return _keydown; } } public DateTime LastInput { get { return _last; } } public byte[] KeyboardState { get { return _state; } } public int KeyDownCount { get { return _keydown.Count; } } public string Result { get { if (_result.Count > 0) { return _result[_result.Count - 1].Trim(); } else { return null; } } } public string CurrentKey { get { return _key; } } public string CurrentChar { get { return _cur; } } public bool isShift { get { return _keydown.Contains(160); } } public void Add(EventMsg msg) { #region 記錄按鍵資訊 // 首次按下按鍵 if (_keys.Count == 0) { _keys.Add(new List<EventMsg>()); _keys[0].Add(msg); _result.Add(string.Empty); } // 未釋放其他按鍵時按下按鍵 else if (_keydown.Count > 0) { _keys[_keys.Count - 1].Add(msg); } // 單位時間內按下按鍵 else if (((TimeSpan)(DateTime.Now - _last)).TotalMilliseconds < ts) { _keys[_keys.Count - 1].Add(msg); } // 從新記錄輸入內容 else { _keys.Add(new List<EventMsg>()); _keys[_keys.Count - 1].Add(msg); _result.Add(string.Empty); } #endregion _last = DateTime.Now; #region 獲取鍵盤狀態 // 記錄正在按下的按鍵 if (msg.paramH == 0 && !_keydown.Contains(msg.message)) { _keydown.Add(msg.message); } // 清除已鬆開的按鍵 if (msg.paramH > 0 && _keydown.Contains(msg.message)) { _keydown.Remove(msg.message); } #endregion #region 計算按鍵資訊 int v = msg.message & 0xff; int c = msg.paramL & 0xff; StringBuilder strKeyName = new StringBuilder(500); if (GetKeyNameText(c * 65536, strKeyName, 255) > 0) { _key = strKeyName.ToString().Trim(new char[] { ' ', '\0' }); GetKeyboardState(_state); if (_key.Length == 1 && msg.paramH == 0) { // 根據鍵盤狀態和shift快取判斷輸出字元 _cur = ShiftChar(_key, isShift, _state).ToString(); _result[_result.Count - 1] += _cur; } else { _cur = string.Empty; } } #endregion } private char ShiftChar(string k, bool isShiftDown, byte[] state) { bool capslock = state[0x14] == 1; bool numlock = state[0x90] == 1; bool scrolllock = state[0x91] == 1; bool shiftdown = state[0xa0] == 1; char chr = (capslock ? k.ToUpper() : k.ToLower()).ToCharArray()[0]; if (isShiftDown) { if (chr >= 'a' && chr <= 'z') { chr = (char)((int)chr - 32); } else if (chr >= 'A' && chr <= 'Z') { chr = (char)((int)chr + 32); } else { string s = "`1234567890-=[];',./"; string u = "~!@#$%^&*()_+{}:\"<>?"; if (s.IndexOf(chr) >= 0) { return (u.ToCharArray())[s.IndexOf(chr)]; } } } return chr; } } public struct EventMsg { public int message; public int paramL; public int paramH; public int Time; public int hwnd; } } }View Code
建立一個事件,當頁面Load時開啟監聽,頁面Unload時關閉監聽
private ScanHook listener = new ScanHook(); private string express_name { get; set; } public Home(int express_id) { InitializeComponent(); listener.ScanerEvent += Listener_ScanerEvent; }
private void Window_Loaded(object sender, RoutedEventArgs e) { listener.Start(); } private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e) { listener.Stop(); Messenger.Default.Unregister(this); }
private void Listener_ScanerEvent(ScanHook.ScanerCodes codes) { //codes.KeyDownCount, codes.Event.message, codes.Event.paramH, codes.Event.paramL, codes.CurrentChar, codes.Result, codes.isShift, codes.CurrentKey //先入庫 MediaPlayer player = new MediaPlayer(); string userid = ConfigurationManager.AppSettings["userid"]; ResponseMsg<Backorder> result = new DB.BackorderBLL().addNewBackorder(new Backorder { backorder_code=codes.Result,userid=int.Parse(userid),express_id=id,seq_no=this.txtSeqNO.Text,backorder_date=System.DateTime.Now});// 改為存在本地資料庫 // HttpMethod.scan(codes.Result, userid,this.txtExpressID.Text); if (result.code == 100) { player.Open(new Uri(Environment.CurrentDirectory + "\\Sound\\success.mp3")); this.lstView.Items.Insert(0, result.data); //前臺訂單數量重新整理一下 this.btnShowDetail.Content = string.Format("你今日共退單:{0}件,待同步伺服器:{1}件(點選檢視清單)",result.mark1,result.mark2); } else { Uri mp3 = new Uri(Environment.CurrentDirectory + "\\Sound\\fail.mp3"); player.Open(mp3); Backorder backorder = new Backorder(); backorder.backorder_code = codes.Result; backorder.backorder_date = System.DateTime.Now; backorder.remark = result.msg; this.lstView.Items.Insert(0, backorder); } player.Play(); }View Code
同步伺服器的程式碼,把當前快遞下所有未同步伺服器的訂單找出來,轉成json格式,然後post到伺服器端
//呼叫網路同步訂單 List<Backorder> list = new BackorderBLL().getWaitforSyncBackorderList(id).data; ResponseMsg<List<Backorder>> response = HttpMethod.SyncBackorders(list); if (response.code == 100) { //同步成功,重新整理本地資料庫狀態 foreach (Backorder order in response.data) { bool result = new BackorderBLL().updateBackorderSysncStatus(order).data; if (result) { //本地庫更新成功 } } MessageBox.Show("同步成功", "提示", MessageBoxButton.OK, MessageBoxImage.Information); } else { MessageBox.Show(response.msg, "錯誤", MessageBoxButton.OK, MessageBoxImage.Error); }
目前專案裡還有很多硬程式碼,優化後,再放開github的private。
to be continued....
&n