元旦三天假期,实现一个电商退单管理系统【二】
一、仓库扫码监听客户端实现
(一)功能分析
快递小哥骑着小三轮,运送退货快递到仓库,库管打开客户端,选择快递公司后,递给快递一把扫码枪,小哥滴滴滴,滴滴滴,一顿操作猛如虎,打完收功。仓管将数据提交服务器,打印回单,整个客户端流程结束。
仓库的客户端需要监听扫码枪输入,计划使用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) { } } } }
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>
登录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"); } } }
登录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; } } }
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>
有点不太习惯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(); } } }
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; } } }
建立一个事件,当页面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(); }
同步服务器的代码,把当前快递下所有未同步服务器的订单找出来,转成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....