forked from xqmmcqs/BUPT-Projects
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Report.tex
1137 lines (840 loc) · 55.1 KB
/
Report.tex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
\documentclass[lang=cn,11pt,a4paper,cite=authornum]{paper}
\title{面向对象程序设计与实践(C++):电商交易平台设计与实现\ 实验报告}
\author{毛子恒 \\ 2019211397}
\institute{北京邮电大学\ 计算机学院}
\date{\zhtoday}
% 本文档命令
\usepackage{array}
\newcommand{\ccr}[1]{\makecell{{\color{#1}\rule{1cm}{1cm}}}}
\nocite{*}
\begin{document}
\maketitle
\section{概览}
\subsection{任务描述}
使用C++语言,基于面向对象的程序设计方法,设计并实现一个简单的电商交易平台,提供用户管理、商品管理、交易管理等功能。
\subsection{开发环境}
\begin{itemize}
\item macOS Big Sur 11.3
\item Apple clang version 12.0.5
\item cmake version 3.19.1
\item QMake version 3.1
\item Clion 2021.1.1
\item Qt Creator 4.14.2
\item Visual Studio Code 1.56.2
\end{itemize}
\section{功能需求}
\paragraph{用户管理和商品管理功能}
\begin{itemize}
\item 用户注册和登录:支持新用户注册平台账号,已注册用户用平台账号登录平台,要求已注册用户的信息长久保留。
\item 修改账户密码:支持登录后对用户账号的密码修改。
\item 余额管理:支持用户账号中余额的查询、充值、消费等。
\item 添加商品:支持商家添加新商品,要求已添加的商品信息长久保留。
\item 展示平台商品信息:支持针对不同类型用户、无论登录与否均展示平台商品信息。
\item 搜索平台商品信息:支持依据某种条件对平台商品进行筛选,并展示筛选结果。
\item 商品信息管理:支持商家对其商品的信息进行管理。
\item 账户至少需要账号名、密码、账户余额、账户类型(商家/消费者)等内容。
\item 把所有的用户账户信息写入文件(要求使用文件存储各类信息),注册新账户的时候,要求注册的账户名不能已经存在于文件中,即账户名唯一。
\item 至少设计一层继承体系(用户基类-用户子类)。设计一个用户基类,然后让商家类和消费者类等用户子类继承它,具体的用户是用户子类的实例对象。用户基类为抽象类,不能实例化,至少具有一个纯虚函数\mintinline{C}{getUserType()}用于指示用户类别。
\item 电商平台上至少有三类商品,每类商品中至少有三个具体的商品,每个具体的商品至少包含商品描述、商品原价、商品剩余量等数据。所有的商品信息需要存储在文件或数据库中,不能写在代码中。
\item 至少设计一层继承体系(商品基类-商品子类)。设计一个商品基类,然后让商品子类继承它,具体的商品是商品子类的实例对象。商品基类请至少具有一个虚函数\mintinline{C}{getPrice()}用于计算具体商品的价格。
\item 支持对同一品类下所有产品打折的活动。
\item 支持一定的错误场景处理能力。
\end{itemize}
\paragraph{交易管理功能}
\begin{itemize}
\item 购物车管理:支持消费者向购物车添加、删除指定数量的商品,也支持消费者修改当前购物车商品的拟购买数量。
\item 订单生产:选择购物车的商品生成订单,计算并显示订单总金额。
\item 网上支付:消费者使用余额支付订单,支付成功后,消费者被扣除的余额应转至商家余额中。
\item 当订单生成后,处于未支付状态时,应将对应数量的商品冻结,不能参与新订单的产生,避免商品被超额售卖。
\item 支持一定的错误场景处理能力。
\end{itemize}
\paragraph{网络版交易平台功能}
\begin{itemize}
\item 设计网络版交易平台客户端。
\item 要求采用C/S结构,客户端与服务器系统之间使用socket进行通信。
\item 支持一定的错误场景处理能力。
\end{itemize}
\paragraph{其他附加功能}
\begin{itemize}
\item 客户端支持美观的GUI。
\item 采用SQLite数据库维护用户、商品和订单信息。
\item 客户端和服务端采用\mintinline{text}{json}格式通信,并且采用\href{https://jwt.io/}{\mintinline{text}{JWT}}鉴权。
\end{itemize}
\section{模块介绍}
\subsection{模块划分}
各模块及其关系如\figref{fig:structure}。
\begin{figure}[htbp]
\centering
\includegraphics[width=\linewidth]{./Images/structure.png}
\caption{模块关系图\label{fig:structure}}
\end{figure}
电商管理平台采用C/S架构,有客户端和服务端两部分组成。客户端部分,GUI采用QML实现,\mintinline{text}{clientmodel}模块为集成到QML的数据结构,其中定义了一些用于信息展示的类和可供调用的接口。\mintinline{text}{client}模块负责生成报文,并且通过socket与服务端通信。
服务端部分,\mintinline{text}{server}模块与客户端通信、解析报文和生成回复报文、调用相应的处理函数;\mintinline{text}{user}模块负责处理用户鉴权,以及用户管理操作;\mintinline{text}{commodity}模块负责处理商品管理操作;\mintinline{text}{order}模块负责处理订单管理操作;\mintinline{text}{database}负责数据库管理,包括表创建、信息增删和查询等操作。
\subsection{服务端}
根据功能需求,服务端会对如下25种请求进行响应:
\label{op}
\begin{enumerate}
\item 用户登录。
\item 用户注册。
\item 用户登出。
\item 获取用户信息。
\item 修改用户密码。
\item 增加用户余额。
\item 减少用户余额。
\item 获取所有商品信息。
\item 获取本用户的商品信息。
\item 获取商品详情。
\item 通过筛选条件获取商品信息。
\item 设置商品信息。
\item 添加商品。
\item 删除商品。
\item 商品打折。
\item 获取购物车信息。
\item 添加商品到购物车。
\item 修改购物车中商品数量。
\item 从购物车中删除商品。
\item 添加订单。
\item 获取本用户的订单。
\item 获取订单详情。
\item 取消订单。
\item 支付订单。
\item 退款/确认退款订单。
\end{enumerate}
服务端的功能主要在\mintinline{C++}{Server}、\mintinline{C++}{UserManage}、\mintinline{C++}{CommodityManage}、\mintinline{C++}{OrderManage}和\mintinline{C++}{Database}类中实现,这些类的关系如\figref{fig:serverstructure}所示。
\begin{figure}[htbp]
\centering
\includegraphics[width=0.5\linewidth]{./APIdoc/class_server__coll__graph.png}
\caption{服务端类关系图\label{fig:serverstructure}}
\end{figure}
\subsubsection{用户管理}
请求1至7属于用户管理功能,在\mintinline{C++}{UserManage}类中实现。该类中实现的功能除了用户注册、登录、登出和用户信息管理之外,还有用户鉴权功能,因此该类中也定义了其他所有请求的接口,在进行鉴权后再调用其他类中相应的处理函数。此外,对于部分参数复杂的请求,\mintinline{C++}{UserManage}类负责解析\mintinline{text}{json}格式的请求参数。该类也实现了将回复信息打包成\mintinline{text}{json}格式,以及返回报错信息的工作。
仅举一例,对于以下查询商品信息的请求:
\begin{code}
\begin{minted}{C++}
QString queryCommodity(const QJsonObject &info, QJsonArray &ret) const;
\end{minted}
\end{code}
查询的参数在\mintinline{C++}{info}变量中,查询结果保存在\mintinline{C++}{ret}中,函数的返回值为报错信息,如果返回字符串为空,表示操作没有错误。
调用此函数时,首先解析\mintinline{C++}{info}中的查询参数,之后根据需要进行鉴权操作,再调用\mintinline{C++}{CommodityManage}类中的相应查询方法。
\paragraph{用户注册、登录与登出}
用户注册时传入用户名、密码和用户类型,服务端会首先检查用户名是否重复,如果不重复,则会在数据库和另一个文本文件中同时添加一个记录,保存用户信息。
用户登录时传入用户名和密码,服务端进行校验后返回一个凭据,之后进行特定用户的请求时都需要附加这个凭据,凭据中包含用户名,以验证用户身份。凭据采用JWT格式,\mintinline{C++}{UserManage}类中只对\mintinline{text}{json}格式的Payload部分进行处理,其余部分的生成和解析由\mintinline{C++}{Server}类负责,将在稍后说明。
用户类采用用户基类-用户子类继承体系,其主要结构如下:
\begin{code}
\begin{minted}{C++}
class User
{
protected:
int balance;
public:
enum USER_TYPE
{
MERCHANT, CUSTOMER
};
User(int _balance) : balance(_balance) {}
virtual USER_TYPE getUserType() const = 0;
int getBalance() const { return balance; }
void addBalance(int num) { balance += num; }
virtual ~User() = default;
};
class Customer : public User
{
public:
Customer(int _balance) : User(_balance) {}
USER_TYPE getUserType() const override { return CUSTOMER; };
};
class Merchant : public User
{
public:
Merchant(int _balance) : User(_balance) {}
USER_TYPE getUserType() const override { return MERCHANT; };
};
\end{minted}
\end{code}
\mintinline{C++}{User}类为用户基类,\mintinline{C++}{Customer}和\mintinline{C++}{Merchant}类为\mintinline{C++}{User}类的子类,分别表示消费者和商家。消费者类中的购物车功能被省略,将在稍后说明。
用户象在登录时被创建,登出时被销毁,为了支持多用户的同时登录,\mintinline{C++}{UserManage}类中保存用户名向用户对象指针的映射,形如\mintinline{C++}{QMap<QString, QSharedPointer<User>> map}。由于访问\mintinline{C++}{User}对象的唯一方式就是通过用户名映射到对应的对象,所以\mintinline{C++}{User}类中不需要保存用户名的成员。所有用户的信息保存在数据库中,当用户登录时,从数据库中取出用户信息,以此创建用户对象。
对数据库的管理操作在\mintinline{C++}{Database}类中实现。数据库采用SQLite,利用\mintinline{C++}{QSqlDatabase}类,实现对数据库文件的增删和修改操作。此外,为了满足功能需求中文件相关的要求,另外增加一个保存用户名的文件,用于检验用户名的唯一性。
利用\mintinline{C++}{QSqlQuery}类执行SQL语句,同时输出调试信息。
数据库中\mintinline{text}{user}表的格式如下:
\begin{code}
\begin{minted}{text}
username TEXT PRIMARY KEY NOT NULL
passwd TEXT NOT NULL
type INT NOT NULL
balance INT NOT NULL
\end{minted}
\end{code}
分别表示用户名、密码、类型、余额。由于涉及到精度问题,余额采用整数表示,以分为单位。
\paragraph{用户信息查询与修改}
获取用户信息、修改密码、增减余额操作都需要在用户已登录的状态下进行,所以需要用凭据鉴权后执行操作。修改密码会直接修改数据库中的信息,增减余额操作会同时对用户对象中的余额变量和数据库中的余额信息进行修改。
\subsubsection{商品管理}
请求8至15属于商品管理功能,在\mintinline{C++}{CommodityManage}类中实现,其中,除了请求8、10、11、15之外,其余操作都需要鉴权。
\paragraph{商品添加}
商品添加请求只能由商家用户发起。商品信息保存在数据库中,\mintinline{C++}{commodity}表的格式如下:
\begin{code}
\begin{minted}{text}
id INT PRIMARY KEY NOT NULL
name TEXT NOT NULL
intro TEXT NOT NULL
type INT NOT NULL
belong TEXT NOT NULL
price INT NOT NULL
amount INT NOT NULL
\end{minted}
\end{code}
分别表示商品ID、名称、介绍、类型、所属商家、价格、数量。价格也以分为单位。与用户不同,通过商品ID来区分不同的商品,ID在创建新商品时自动分配,商品所属商家即为发起创建商品操作的用户。
\paragraph{商品信息修改}
商品信息修改请求只能由商家用户发起,并且只能修改此用户拥有的商品。可以修改商品的名称、介绍、价格和数量。直接从数据库中修改该商品的表项。
\paragraph{商品删除}
商品删除请求只能由商家用户发起,并且只能删除此用户拥有的商品。删除时直接从数据库中删除该商品的表项。
\paragraph{商品信息查询}
商品信息查询有四种情况:查询所有商品、按照筛选条件搜索商品、查询某个商品的详情、查找某个用户拥有的商品。前三种情况不需要鉴权,最后一种情况需要鉴权。
其中筛选条件包括:商品名关键词、商品类型、商品最小价格和最大价格、是否有货。
商品类采用商品基类-商品子类继承体系,其主要结构如下:
\begin{code}
\begin{minted}{C++}
class Commodity
{
protected:
int id;
QString name;
QString intro;
QString belong;
int price;
int amount;
public:
enum Type
{
CLOTHES, BOOKS, ELECTRONIC
};
Commodity(int _id, QString _name, QString _intro, QString _belong, int _price, int _amount) : id(_id), name(std::move(_name)), intro(std::move(_intro)), belong(std::move(_belong)), price(_price), amount(_amount) {}
virtual int getCommodityType() const = 0;
virtual int getDiscount() const = 0;
int getId() const { return id; }
const QString &getName() const { return name; }
const QString &getIntro() const { return intro; }
const QString &getBelong() const { return belong; }
int getPrice() const { return price; }
int getAmount() const { return amount; }
virtual ~Commodity() = default;
};
class Clothes : public Commodity
{
private:
static int discount;
public:
Clothes(int _id, QString _name, QString _intro, QString _belong, int _price, int _amount) : Commodity(_id, std::move(_name), std::move(_intro), std::move(_belong), _price, _amount) {}
static void setDiscount(int _discount) { discount = _discount; }
int getDiscount() const override { return discount; }
int getCommodityType() const override { return CLOTHES; }
};
class Books : public Commodity
{
private:
static int discount;
public:
Books(int _id, QString _name, QString _intro, QString _belong, int _price, int _amount) : Commodity(_id, std::move(_name), std::move(_intro), std::move(_belong), _price, _amount) {}
static void setDiscount(int _discount) { discount = _discount; }
int getDiscount() const override { return discount; }
int getCommodityType() const override { return BOOKS; }
};
class Electronic : public Commodity
{
private:
static int discount;
public:
Electronic(int _id, QString _name, QString _intro, QString _belong, int _price, int _amount) : Commodity(_id, std::move(_name), std::move(_intro), std::move(_belong), _price, _amount) {}
static void setDiscount(int _discount) { discount = _discount; }
int getDiscount() const override { return discount; }
int getCommodityType() const override { return ELECTRONIC; }
};
\end{minted}
\end{code}
\mintinline{C++}{Commodity}类为商品基类,\mintinline{C++}{Clothes}、\mintinline{C++}{Books}和\mintinline{C++}{Electronic}类为服饰、书籍和电子产品商品子类。
由于商品信息保存在数据库中,在数据库中查询到商品后创建商品对象(或者商品对象的列表),这些对象用于在函数之间传递商品信息,在查询操作完成后,商品对象被销毁。
\paragraph{商品打折}
商品打折操作可以由任何人发起,支持对某一类的所有商品打折,打折的倍率存储在对应类的一个静态成员中,由于精度的限制,由整数存储,默认为100,表示原价。
\subsubsection{购物车管理}
请求16至19属于购物车管理功能,所有操作都需要鉴权。仅有消费者用户有购物车,购物车在消费者用户对象中保存,当用户登出时,购物车的内容会和用户对象一起销毁。
包含购物车功能的消费者类的定义如下:
\begin{code}
\begin{minted}{C++}
class Customer : public User
{
private:
QMap<int, int> cart;
public:
Customer(int _balance) : User(_balance) {}
USER_TYPE getUserType() const override { return CUSTOMER; };
int getItemNum(int id) const;
void addToCart(int id, int number);
void removeFromCart(int id, int number);
void eraseFromCart(int id);
const QMap<int, int> &getCart() const { return cart; }
};
\end{minted}
\end{code}
添加商品到购物车和修改商品数量时,如果数量上限/下限超出允许的范围,则会数量会自动被约束到对应的范围,不会产生报错。
\subsubsection{订单管理}
请求20至25属于订单管理功能,在\mintinline{C++}{OrderManage}类中实现,所有操作都需要鉴权。
\paragraph{订单创建}
订单创建请求只能由消费者用户发起。消费者选取一些商品ID,服务端首先判断ID是否在购物车内,如果在,则根据购物车中该商品的数量创建订单,从将对应商品的数量减去购物车中商品的数量。
订单信息保存在数据库中,\mintinline{C++}{orders}表的格式如下:
\begin{code}
\begin{minted}{text}
id INT PRIMARY KEY NOT NULL
mainid INT NOT NULL
status INT NOT NULL
total INT NOT NULL
buyer TEXT NOT NULL
seller TEXT NOT NULL
content TEXT NOT NULL
\end{minted}
\end{code}
分别表示订单ID、主订单ID、订单状态、总额、买家用户名、买家用户名、订单内容。
系统中实现的订单包含一个ID和主订单ID,当消费者创建订单时,会根据订单中商品归属的商家,分成多个子订单,一个子订单中的商品都属于同一个商家,这些子订单的共同构成一个主订单。主订单的ID并不连续,由子订单的最小ID决定。
订单有五种状态:未支付、已取消、已支付、正在退款、已退款。默认创建订单时,订单处于未支付状态。
订单内容为商品ID、商品数量二元组的列表,由于生成订单后订单内容就不发生变化,所以在数据库中以字符串保存,格式为“\mintinline{text}{商品ID,商品数量;商品ID,商品数量;...}”。
\paragraph{订单信息查询}
订单信息查询有两种情况:查询某个用户的订单、查询某个订单的详情。查询用户订单时,只会返回订单ID和订单总额。查询订单详情时,会根据用户类型区分,如果用户为消费者,则按主订单ID查询,否则按子订单ID查询,以保证用户只能查询到与自己有关的订单信息。
订单类的主要结构如下:
\begin{code}
\begin{minted}{C++}
class Order
{
public:
enum Status
{
UNPAID, CANCELLED, PAID, REFUNDING, REFUNDED
};
private:
int id;
int mainId;
Status status;
int total;
QString buyer;
QString seller;
QList<QPair<int, int>> content;
public:
Order(int _id, int _mainId, int _status, int _total, QString _buyer, QString _seller, const QString &_content);
int getId() const { return id; }
int getMainId() const { return mainId; }
int getStatus() const { return status; }
int getTotal() const { return total; }
const QString &getBuyer() const { return buyer; }
const QString &getSeller() const { return seller; }
const QList<QPair<int, int>> &getContent() const { return content; }
};
\end{minted}
\end{code}
与商品类类似,订单类在数据库查询后创建,并且在操作完成后销毁。
\paragraph{订单取消}
该请求只能由消费者发起,将未支付的订单改为已取消。
\paragraph{订单支付}
该请求只能由消费者发起,首先确认消费者余额充足,从消费者用户余额中扣除订单对应的金额,并且将未支付的订单改为已支付。
\paragraph{订单退款}
该请求只能由消费者发起,将已支付的订单改为退款中。
\paragraph{确认订单退款}
该请求只能由商家发起,首先确认商家余额充足,并且订单中的商品仍然存在,之后从商家用户余额中扣除子订单对应的金额,将订单中的商品返还给商家,将退款中的订单改为已退款。
\subsubsection{网络通信与报文解析}
\mintinline{C++}{Server}类的功能是通过UDP协议与客户端通信,将收到的报文解析为\mintinline{text}{json}格式,并且将返回的\mintinline{text}{json}格式报文转化为字节流。
\paragraph{网络通信}
服务器将socket绑定值本地的某个端口,并且监听此端口,当有报文到达时调用\mintinline{C++}{messageHandler}处理函数。
\paragraph{报文解析}
收到UDP报文为字节流,需要将其转化成\mintinline{text}{json}格式,报文的\mintinline{text}{type}字段指明了请求的类型,\mintinline{text}{payload}字段指明了请求的内容,服务端根据请求的类型调用相应的处理函数,例如,对于登录请求,调用\mintinline{C++}{userLoginHandler}函数。
\paragraph{JWT鉴权}
\label{JWT}JWT分为三个部分,分别为Header、Payload和Signature,用户在登录时,如果登录成功,\mintinline{text}{UserManage}类的函数会返回\mintinline{text}{json}格式的Payload部分,\mintinline{text}{Server}类的\mintinline{C++}{jwtEncoding}函数需要负责加上Header部分,利用密钥生成Signature部分,将这三部分通过Base64编码后连接成完整的JWT字符串,附在回复报文中。
之后,每当收到的请求需要鉴权,就在请求的内容中查找\mintinline{text}{token}字段,利用\mintinline{C++}{jwtVerify}函数验证Signature是否正确,再利用利用\mintinline{C++}{jwtGetPayload}函数并且将\mintinline{text}{body}转化为\mintinline{text}{json}格式,完成鉴权。
\paragraph{请求处理和回复报文构建}
对于每种请求,服务端都需要从请求的内容中提取出需要的信息,进行(可选的)鉴权,之后调用\mintinline{text}{UserManage}类中相应的函数。
\mintinline{text}{UserManage}类中的函数返回值为\mintinline{C++}{QString}类型,如果操作没有产生错误,则该字符串为空,否则该字符串的内容是报错信息。\mintinline{text}{Server}根据请求的不同类型构造回复报文,报文的\mintinline{text}{status}字段表示请求是否成功,\mintinline{text}{payload}字段根据请求的类型给出请求的结果。
构造后的回复报文为\mintinline{text}{json}格式,需要转化成字节流,之后通过原本的UDP连接发送回客户端。
\subsection{客户端}
\subsubsection{网络通信与报文解析}
\mintinline{text}{Client}类负责通过指定端口和服务器通信,该类保存服务端生成的凭据,将其附加在报文中,并且设置\mintinline{C++}{type}字段,最后将报文转换成字节流。
发送报文后,客户端监听socket直至回复报文到达。收到报文后,客户端转换报文为\mintinline{text}{json}格式。
\subsubsection{客户端数据结构}
客户端中定义了三个数据结构\mintinline{C++}{UserModel}、\mintinline{C++}{CommodityModel}和\mintinline{C++}{OrderModel},用于集成到QML中,展示数据。
此外,\mintinline{C++}{ClientModel}类定义了供QML调用的接口,包含\ref{op}节中规定的25个操作,以及可供QML访问的数据成员。如下:
\begin{code}
\begin{minted}{C++}
class UserModel : public QObject
{
Q_OBJECT
Q_PROPERTY(QString username READ username CONSTANT)
Q_PROPERTY(int type READ type CONSTANT)
Q_PROPERTY(int balance READ balance CONSTANT)
public:
UserModel(QString _username, int _type, int _balance) : m_username(std::move(_username)), m_type(_type), m_balance(_balance) {}
QString username() const { return m_username; }
int type() const { return m_type; }
int balance() const { return m_balance; }
private:
QString m_username;
int m_type;
int m_balance;
};
class CommodityModel : public QObject
{
Q_OBJECT
Q_PROPERTY(int id READ id CONSTANT)
Q_PROPERTY(QString name READ name CONSTANT)
Q_PROPERTY(QString intro READ intro CONSTANT)
Q_PROPERTY(int type READ type CONSTANT)
Q_PROPERTY(int price READ price CONSTANT)
Q_PROPERTY(int discount READ discount CONSTANT)
Q_PROPERTY(int amount READ amount CONSTANT)
Q_PROPERTY(int order READ order WRITE setOrder NOTIFY orderChanged)
public:
CommodityModel(int _id, QString _name, QString _intro, int _type, int _price, int _discount, int _amount) : m_id(_id), m_name(std::move(_name)), m_intro(std::move(_intro)), m_type(_type), m_price(_price), m_discount(_discount), m_amount(_amount) {}
CommodityModel(int _id, QString _name, int _type, int _price, int _discount, int _amount) : m_id(_id), m_name(std::move(_name)), m_type(_type), m_price(_price), m_discount(_discount), m_amount(_amount), m_order(0) {}
CommodityModel(int _id, QString _name, int _type, int _amount, int _order) : m_id(_id), m_name(std::move(_name)), m_type(_type), m_amount(_amount), m_order(_order) {}
int id() const { return m_id; }
QString name() const { return m_name; }
QString intro() const { return m_intro; }
int type() const { return m_type; }
int price() const { return m_price; }
int discount() const { return m_discount; }
int amount() const { return m_amount; }
int order() const { return m_order; }
void setOrder(int _order)
{
m_order = _order;
emit orderChanged();
}
Q_SIGNALS:
void orderChanged();
private:
int m_id;
QString m_name;
QString m_intro;
int m_type;
int m_price;
int m_discount;
int m_amount;
int m_order;
};
class OrderModel : public QObject
{
Q_OBJECT
Q_PROPERTY(int id READ id CONSTANT)
Q_PROPERTY(int mainid READ mainid CONSTANT)
Q_PROPERTY(int status READ status CONSTANT)
Q_PROPERTY(int total READ total CONSTANT)
Q_PROPERTY(QString buyer READ buyer CONSTANT)
Q_PROPERTY(QString seller READ seller CONSTANT)
public:
OrderModel(int _id, int _mainid, int _status, int _total, QString _buyer, QString _seller) : m_id(_id), m_mainid(_mainid), m_status(_status), m_total(_total), m_buyer(std::move(_buyer)), m_seller(std::move(_seller)) {}
OrderModel(int _id, int _status, int _total) : m_id(_id), m_status(_status), m_total(_total) {}
OrderModel(int _id, int _total) : m_id(_id), m_total(_total) {}
int id() const { return m_id; }
int mainid() const { return m_mainid; }
int status() const { return m_status; }
int total() const { return m_total; }
QString buyer() const { return m_buyer; }
QString seller() const { return m_seller; }
private:
int m_id;
int m_mainid;
int m_status;
int m_total;
QString m_buyer;
QString m_seller;
};
class ClientModel : public QObject
{
Q_OBJECT
Q_PROPERTY(bool userActive READ userActive NOTIFY userActiveChanged)
Q_PROPERTY(UserModel * user READ user NOTIFY userChanged)
Q_PROPERTY(QList<CommodityModel *> commodity READ commodity NOTIFY commodityChanged)
Q_PROPERTY(int commoditySize READ commoditySize NOTIFY commoditySizeChanged)
Q_PROPERTY(CommodityModel * singleCommodity READ singleCommodity NOTIFY singleCommodityChanged)
Q_PROPERTY(QList<OrderModel *> order READ order NOTIFY orderChanged)
Q_PROPERTY(OrderModel * mainOrder READ mainOrder NOTIFY mainOrderChanged)
public:
ClientModel(QObject * parent, quint16 _port) : QObject(parent), m_userActive(0), m_user(nullptr), m_commoditySize(0), m_singleCommodity(nullptr), m_mainOrder(nullptr), client(this, _port) {}
virtual ~ClientModel() = default;
bool userActive() const { return m_userActive; }
UserModel * user() const { return m_user; }
QList<CommodityModel *> commodity() const { return m_commodity; }
int commoditySize() const { return m_commoditySize; }
CommodityModel * singleCommodity() const { return m_singleCommodity; }
QList<OrderModel *> order() const { return m_order; }
OrderModel * mainOrder() const { return m_mainOrder; }
Q_INVOKABLE QString userLogin(QString username, QString password);
Q_INVOKABLE QString userRegister(QString username, QString password, int type);
Q_INVOKABLE void userLogout();
Q_INVOKABLE QString userGetInfo();
Q_INVOKABLE QString userSetPasswd(QString password);
Q_INVOKABLE QString userAddMoney(QString money);
Q_INVOKABLE QString userSubMoney(QString money);
Q_INVOKABLE QString itemGetAll();
Q_INVOKABLE QString itemGetMine();
Q_INVOKABLE QString itemGetDetail(int id);
Q_INVOKABLE QString itemGetFilter(QString name, int type, QString min_price, QString max_price, int remain);
Q_INVOKABLE QString itemSet(int id, QString name, QString intro, QString price, QString amount);
Q_INVOKABLE QString itemAdd(QString name, QString intro, int type, QString price, QString amount);
Q_INVOKABLE QString itemDelete(int id);
Q_INVOKABLE QString itemDiscount(int type, QString discount);
Q_INVOKABLE QString cartGet();
Q_INVOKABLE QString cartAdd(int id, QString number);
Q_INVOKABLE QString cartSet(int id, QString number);
Q_INVOKABLE QString cartDelete(int id);
Q_INVOKABLE QString orderAdd();
Q_INVOKABLE QString orderGetMine();
Q_INVOKABLE QString orderGetDetail(int id);
Q_INVOKABLE QString orderCancel(int id);
Q_INVOKABLE QString orderPay(int id);
Q_INVOKABLE QString orderRefund(int id);
Q_SIGNALS:
void userChanged();
void userActiveChanged();
void commodityChanged();
void singleCommodityChanged();
void commoditySizeChanged();
void orderChanged();
void mainOrderChanged();
private:
static QString checkStatus(const QJsonObject &ret);
bool m_userActive;
UserModel * m_user;
QList<CommodityModel *> m_commodity;
int m_commoditySize;
void clearCommodity();
CommodityModel * m_singleCommodity;
QList<OrderModel *> m_order;
void clearOrder();
OrderModel * m_mainOrder;
Client client;
};
\end{minted}
\end{code}
\mintinline{C++}{Q_PROPERTY}指明的属性可以直接在QML中访问,\mintinline{C++}{READ}后指明了访问的方法名,当变量的值发生变化时,会激活\mintinline{C++}{NOTIFY}后指明的信号。仅以\mintinline{C++}{singleCommodity}对象为例:
\begin{code}
\begin{minted}{C++}
Q_PROPERTY(CommodityModel * singleCommodity READ singleCommodity NOTIFY singleCommodityChanged)
CommodityModel * singleCommodity() const { return m_singleCommodity; }
void singleCommodityChanged();
CommodityModel * m_singleCommodity;
\end{minted}
\end{code}
该属性对应\mintinline{C++}{m_singleCommodity}私有成员,可以通过\mintinline{C++}{singleCommodity}函数访问,当变量的值发生变化时,激活\mintinline{C++}{singleCommodityChanged}信号。
\mintinline{C++}{Q_INVOKABLE}指明的方法可以直接在QML中调用。
\mintinline{C++}{UserModel}类中有用户名、类型、余额属性。\mintinline{C++}{CommodityModel}类中有商品ID、名称、介绍、类型、价格、折扣、数量、所属订单属性。其中\mintinline{C++}{order}属性在订单详情页用于展示商品所属的子订单ID,在购物车页用于表示商品是否选中。\mintinline{C++}{OrderModel}类中有订单ID、主订单ID、订单状态、总额、买家、卖家属性。
\mintinline{C++}{ClientModel}中有一个\mintinline{C++}{UserModel}对象\mintinline{C++}{user},一个\mintinline{C++}{CommodityModel}对象的列表\mintinline{C++}{commodity}和一个单独的\mintinline{C++}{CommodityModel}对象\mintinline{C++}{singleCommodity},也有一个\mintinline{C++}{OrderModel}对象的列表\mintinline{C++}{order}和一个单独的\mintinline{C++}{OrderModel}对象\mintinline{C++}{mainOrder}。\mintinline{C++}{commodity}用于展示商品列表,\mintinline{C++}{singleCommodity}用于商品详情页,\mintinline{C++}{order}用于订单列表,\mintinline{C++}{mainOrder}用于订单详情页。
\subsubsection{可视化界面}
可视化界面采用QML实现,其中所有的图标采用来自\href{https://www.flaticon.com/}{flaticon}的免费icon,使得界面风格现代化。另外,客户端内置开源的\href{https://source.typekit.com/source-han-serif/cn/}{思源宋体}作为字体。
界面采用\mintinline{C++}{StackView}布局,即存在一个页面栈,当访问新页面时向栈中\mintinline{C++}{push}一个页面,退出该页面时将该页面从栈中\mintinline{C++}{pop}。默认栈中只有一个主页页面。
我实现了如下12个页面:
\begin{enumerate}
\item 主页
\item 登录页
\item 用户页
\item 添加商品页
\item 修改商品页
\item 商品详情页
\item 搜索页
\item 搜索结果页
\item 购物车页
\item 打折页
\item 订单列表页
\item 订单详情页
\end{enumerate}
此外,我还实现了一些供复用的文本、图标、输入框和对话框,提供了统一的视觉风格。
界面中的元素大部分都是网格化布局和相对定位结合,以满足在窗口大小可动态调整的基础上,在任何状态下都能够提供正确的显示效果。界面中的商品列表和订单列表均采用\mintinline{C++}{ScrollView}和\mintinline{C++}{ListView}配合,实现变长的可滚动的列表。
可视化界面的具体成果见\ref{GUI}节的说明。
\section{用户指南}
\subsection{安装依赖}
所需环境:\href{https://mirrors.tuna.tsinghua.edu.cn/qt/official_releases/online_installers/}{Qt}。
选择系统对应的在线安装包,安装Qt 5.15.2和Qt Creator即可。
可以通过设置镜像源的方法加快下载速度。
\subsection{编译项目}
\subsubsection{命令行编译}
在\mintinline{text}{server}目录中执行以下命令来编译(以macOS为例):
\begin{code}
\begin{minted}{shell}
mkdir build
cd build
qmake ./server.pro -spec macx-clang CONFIG+=x86_64 CONFIG+=qtquickcompiler
make qmake_all
make
\end{minted}
\end{code}
或者采用CMake来编译,修改\mintinline{text}{CMakeLists.txt}文件中\mintinline{text}{CMAKE_PREFIX_PATH}变量的前缀为本机Qt目录的路径,之后在\mintinline{text}{server}目录中执行以下命令:
\begin{code}
\begin{minted}{shell}
mkdir build
cd build
cmake ..
make
\end{minted}
\end{code}
在\mintinline{text}{client}目录中执行以下命令来编译(以macOS为例):
\begin{code}
\begin{minted}{shell}
mkdir build
cd build
qmake ./client.pro -spec macx-clang CONFIG+=x86_64 CONFIG+=qtquickcompiler
make qmake_all
make
\end{minted}
\end{code}
\subsubsection{在Qt Creator中编译}
使用Qt Creator打开项目文件\mintinline{text}{server.pro},进入“项目”界面,在“编辑构建配置”后的下拉菜单中选择“Release”,设置“Build directory”的后缀为\mintinline{text}{Phase3/server/build}。
之后使用Release模式构建并运行项目。
客户端操作类似。
\subsection{使用手册}
\label{GUI}
\subsubsection{主页}
启动服务端和客户端后,进入主页,如\figref{fig:home}所示。
\begin{figure}[htbp]
\centering
\includegraphics[width=0.7\linewidth]{./Images/home.png}
\caption{主页\label{fig:home}}
\end{figure}
主页上方四个图标分别是搜索页、订单页、购物车页和用户页的入口,中间为所有商品的列表,点击图标或者商品名称可以进入商品详情页。左下角为打折页的入口。
\subsubsection{登录页}
默认未登录情况下,在主页点击订单、购物车和用户页图标都会进入登录页,如\figref{fig:login}所示。
\begin{figure}[htbp]
\centering
\subfigure[登录页\label{fig:login}]{
\begin{minipage}[t]{0.7\linewidth}
\centering
\includegraphics[width=\linewidth]{./Images/login.png}
\end{minipage}
}
\subfigure[登录失败弹窗\label{fig:loginfail}]{
\begin{minipage}[t]{0.3\linewidth}
\centering
\includegraphics[width=\linewidth]{./Images/loginfail.png}
\end{minipage}
}
\subfigure[选择用户类型弹窗\label{fig:register}]{
\begin{minipage}[t]{0.3\linewidth}
\centering
\includegraphics[width=\linewidth]{./Images/register.png}
\end{minipage}
}
\subfigure[注册成功弹窗\label{fig:resigtersuc}]{
\begin{minipage}[t]{0.3\linewidth}
\centering
\includegraphics[width=\linewidth]{./Images/registersuc.png}
\end{minipage}
}
\caption{登录页}
\end{figure}
登录页中间为用户名和密码输入框,点击左边的图标即可登录,如果登录失败,会弹出报错弹窗,如\figref{fig:loginfail}所示。如果登录成功,会自动退出登录页。
点击右边的图标即可注册,会弹出选择用户类型的弹窗,如\figref{fig:register}所示。如果注册成功,会弹出注册成功弹窗,如\figref{fig:resigtersuc}所示。
\subsubsection{用户页}
登录成功后,在主页点击用户图标会进入用户页,根据用户类型的不同,用户页的显示内容会有所不同。
\begin{figure}[htbp]
\centering
\subfigure[商家用户页\label{fig:merchant}]{
\begin{minipage}[t]{0.45\linewidth}
\centering
\includegraphics[width=\linewidth]{./Images/merchant.png}
\end{minipage}
}
\subfigure[消费者用户页\label{fig:customer}]{
\begin{minipage}[t]{0.45\linewidth}
\centering
\includegraphics[width=\linewidth]{./Images/customer.png}
\end{minipage}
}
\subfigure[修改密码弹窗\label{fig:newpasswd}]{
\begin{minipage}[t]{0.3\linewidth}
\centering
\includegraphics[width=\linewidth]{./Images/newpasswd.png}
\end{minipage}
}
\subfigure[充值失败弹窗\label{fig:addfail}]{
\begin{minipage}[t]{0.3\linewidth}
\centering
\includegraphics[width=\linewidth]{./Images/addfail.png}
\end{minipage}
}
\subfigure[提现失败弹窗\label{fig:subfail}]{
\begin{minipage}[t]{0.3\linewidth}
\centering
\includegraphics[width=\linewidth]{./Images/subfail.png}
\end{minipage}
}
\caption{用户页}
\end{figure}
商家用户页如\figref{fig:merchant}所示,页面上方显示用户信息,余额右边有增减余额的按钮,余额下方两个按钮分别表示修改密码和登出。页面下方显示商家拥有的商品,其中有增加商品、修改商品和删除商品的按钮。
消费者用户页如\figref{fig:customer}所示,其上半部分和商家用户页一致。
点击修改密码按钮,会弹出如\figref{fig:newpasswd}所示的弹窗,输入新密码后点击确认,会弹出提示弹窗。
点击增/减余额按钮,会弹出类似的弹窗,在框中输入正整数后点击确认即可修改余额。如果修改成功,则页面上的余额会自动刷新。如果修改失败,会弹出如\figref{fig:addfail}和\figref{fig:subfail}的弹窗,显示错误提示信息。
点击登出按钮,用户会登出,并且自动退出用户页。
\subsubsection{添加商品页}
在用户页点击“我的商品”右方的+图标,进入添加商品页,如\figref{fig:newcom}所示。
\begin{figure}[htbp]
\centering
\includegraphics[width=0.7\linewidth]{./Images/newcom.png}
\caption{添加商品页\label{fig:newcom}}
\end{figure}
在各输入框中输入商品信息,并且通过下拉菜单选择商品类型,点击蓝色对勾确认添加,如果添加成功,将自动退出本页面,并且新的商品会显示在用户页中,如果失败则会弹出报错信息。
点击红色叉取消添加。
\subsubsection{修改商品页}
在用户页的商品列表中点击编辑图标,即进入修改商品页,如\figref{fig:editcom}所示。
\begin{figure}[htbp]
\centering
\includegraphics[width=0.7\linewidth]{./Images/editcom.png}
\caption{修改商品页\label{fig:editcom}}
\end{figure}
在该页面可以修改商品的名称、介绍、价格和数量,如果某个输入框留空表示不修改该属性。点击蓝色对勾确认修改,如果修改成功,将自动退出本页面,如果失败则会弹出报错信息。
点击红色叉取消修改。
\subsubsection{商品详情页}
在任何商品列表中点击商品图标或商品名称即可进入商品详情页,如\figref{fig:comdetail}所示。
\begin{figure}[htbp]
\centering
\includegraphics[width=0.7\linewidth]{./Images/comdetail.png}
\caption{商品详情页\label{fig:comdetail}}
\end{figure}
该页面中主要显示商品的各项信息,另有一个添加至购物车按钮,点击后弹出弹窗,在框中输入需要添加的商品的数量,如果添加成功,则弹出成功提示窗口,否则弹出报错信息窗口。
\subsubsection{搜索页}
在主页中点击搜索按钮,进入搜索页,如\figref{fig:search}所示。
\begin{figure}[htbp]
\centering
\includegraphics[width=0.7\linewidth]{./Images/search.png}
\caption{搜索页\label{fig:search}}
\end{figure}
可以在输入框中输入名称、价格区间等筛选条件,也可以通过下拉菜单选择商品类型,或者选择忽略无货的商品。如果输入框为空,则表示不设该筛选条件。点击下方的搜索按钮进行搜索。
\subsubsection{搜索结果页}
在搜索页点击搜索按钮后,如果输入无误,则跳转到搜索结果页,如\figref{fig:searchres}所示,其中包含一个商品列表,即搜索的结果。
\begin{figure}[htbp]
\centering
\includegraphics[width=0.7\linewidth]{./Images/searchres.png}
\caption{搜索结果页\label{fig:searchres}}
\end{figure}
\subsubsection{购物车页}
在首页点击购物车按钮,即进入购物车页,如\figref{fig:cart}所示,其中包含从商品详情页添加进购物车的商品。
\begin{figure}[htbp]
\centering
\includegraphics[width=0.7\linewidth]{./Images/cart.png}
\caption{购物车页\label{fig:cart}}
\end{figure}
购物车页中间为一个商品列表,每个商品前有一个复选框,表示是否选中该商品,选中的商品的总金额会在右下角动态显示。每个商品右边有修改商品数量和移出购物车按钮。
当点击修改商品数量按钮时,会弹出弹窗,在框中输入想要修改的数量,之后点击确认即可确认修改,如果修改成功,购物车页会显示新的商品数量,如果修改失败,则会弹出错误信息弹窗。
当点击移出购物车按钮时,会弹出确认弹窗,确认移出后,该商品会从商品列表中消失。
点击右下角的下单按钮,会弹出确认弹窗,确认下单后,则会用选中的商品创建订单,并且选中的商品会被移出购物车。
如果商品的实际数量少于购物车中的数量,则会按实际数量下单,如果购物车中的商品不存在,则订单中不会有该商品。
\subsubsection{打折页}
在主页中点击左下角的打折按钮,即进入打折页,如\figref{fig:discount}所示。
\begin{figure}[htbp]
\centering
\includegraphics[width=0.7\linewidth]{./Images/discount.png}
\caption{打折页\label{fig:discount}}
\end{figure}
打折页中包含一个商品类型的下拉菜单和一个输入框,在输入框中输入折扣信息,点击红色对勾即可开始打折。如果成功,则会自动退出本页面,否则会弹出错误信息弹窗。
\subsubsection{订单列表页}
在主页中点击订单按钮,即进入订单列表页,如\figref{fig:orderlist}所示。
\begin{figure}[htbp]
\centering
\includegraphics[width=0.7\linewidth]{./Images/orderlist.png}
\caption{订单页\label{fig:orderlist}}
\end{figure}
订单列表页中只显示与本用户有关的订单ID和订单金额。
\subsubsection{订单详情页}
在订单列表页中点击订单ID或者图标,即进入订单详情页,如\figref{fig:orderunpaid}所示。
\begin{figure}[htbp]
\centering
\includegraphics[width=0.7\linewidth]{./Images/orderunpaid.png}
\caption{订单详情页\label{fig:orderunpaid}}
\end{figure}
页面右上方显示主订单和子订单的有关信息,当主订单处于未支付状态时,显示支付和取消图标。
页面下方显示订单中的商品信息。
点击支付图标,弹出确认弹窗,确认后即支付此订单,如果支付失败,则弹出错误信息,如\figref{fig:payfail}所示。否则订单状态被刷新,如\figref{fig:orderpaid}所示。