├── .gitignore
├── .pre-commit-config.yaml
├── .travis.yml
├── README.md
├── be
├── __init__.py
├── app.py
├── model
│ ├── __init__.py
│ ├── buyer.py
│ ├── db_conn.py
│ ├── error.py
│ ├── seller.py
│ ├── store.py
│ └── user.py
├── serve.py
└── view
│ ├── __init__.py
│ ├── auth.py
│ ├── buyer.py
│ └── seller.py
├── doc
├── auth.md
├── buyer.md
└── seller.md
├── fe
├── __init__.py
├── access
│ ├── __init__.py
│ ├── auth.py
│ ├── book.py
│ ├── buyer.py
│ ├── new_buyer.py
│ ├── new_seller.py
│ └── seller.py
├── bench
│ ├── __init__.py
│ ├── bench.md
│ ├── run.py
│ ├── session.py
│ └── workload.py
├── conf.py
├── conftest.py
├── data
│ ├── book.db
│ └── scraper.py
└── test
│ ├── gen_book_data.py
│ ├── test.md
│ ├── test_add_book.py
│ ├── test_add_funds.py
│ ├── test_add_stock_level.py
│ ├── test_bench.py
│ ├── test_create_store.py
│ ├── test_login.py
│ ├── test_new_order.py
│ ├── test_password.py
│ ├── test_payment.py
│ └── test_register.py
├── requirements.txt
├── script
└── test.sh
└── setup.py
/.gitignore:
--------------------------------------------------------------------------------
1 | .coverage
2 | .idea
3 | .pytest_cache
4 | *.log
5 | *.db
6 | htmlcov
7 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: https://github.com/pre-commit/pre-commit-hooks
3 | rev: v2.3.0
4 | hooks:
5 | - id: check-ast
6 | - id: check-yaml
7 | - id: end-of-file-fixer
8 | - id: trailing-whitespace
9 | - id: mixed-line-ending
10 | - repo: https://github.com/psf/black
11 | rev: 19.3b0
12 | hooks:
13 | - id: black
14 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 | python:
3 | - "3.6" # current default Python on Travis CI
4 | - "3.7"
5 | - "3.8"
6 | - "3.8-dev" # 3.8 development branch
7 | - "nightly" # nightly build
8 |
9 | # Install the codecov pip dependency
10 | install:
11 | - pip install -r requirements.txt
12 |
13 | # Run the unit test
14 | script:
15 | - export PATHONPATH=`pwd`
16 | - coverage run --timid --branch --source fe,be --concurrency=thread -m pytest -v --ignore=fe/data
17 |
18 | # Push the results back to codecov
19 | after_success:
20 | - coverage combine
21 | - coverage report
22 | - codecov
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # bookstore
2 | [](https://travis-ci.com/DaSE-DBMS/bookstore)
3 | [](https://codecov.io/gh/DaSE-DBMS/bookstore)
4 |
5 | ## 功能
6 | 实现一个提供网上购书功能的网站后端。
7 | 网站支持书商在上面开商店,购买者可能通过网站购买。
8 | 买家和卖家都可以注册自己的账号。
9 | 一个卖家可以开一个或多个网上商店,
10 | 买家可以为自已的账户充值,在任意商店购买图书。
11 | 支持下单->付款->发货->收货,流程。
12 |
13 | 1.实现对应接口的功能,见doc下面的.md文件描述 (60%分数)
14 |
15 | 其中包括:
16 |
17 | 1)用户权限接口,如注册、登录、登出、注销
18 |
19 | 2)买家用户接口,如充值、下单、付款
20 |
21 | 3)卖家用户接口,如创建店铺、填加书籍信息及描述、增加库存
22 | 通过对应的功能测试,所有test case都pass
23 | 测试下单及付款两个接口的性能(最好分离负载生成和后端),测出支持的每分钟交易数,延迟等
24 |
25 | 2.为项目添加其它功能 :(40%分数)
26 |
27 | 1)实现后续的流程
28 | 发货 -> 收货
29 |
30 | 2)搜索图书
31 | 用户可以通过关键字搜索,参数化的搜索方式;
32 | 如搜索范围包括,题目,标签,目录,内容;全站搜索或是当前店铺搜索。
33 | 如果显示结果较大,需要分页
34 | (使用全文索引优化查找)
35 |
36 | 3)订单状态,订单查询和取消定单
37 | 用户可以查自已的历史订单,用户也可以取消订单。
38 | 取消定单(可选项,加分 +5~10),买家主动地取消定单,如果买家下单经过一段时间超时后,如果买家未付款,定单也会自动取消。
39 |
40 | ## 要求
41 | 1.可以扩展.md中的接口,但不要修改(字段可以多,不可以减少或修改参数名,减分项 -2\~5分),也不要修改test case(减分项 -2\~5分)。
42 | 测试程序如果有问题可以提bug (加分项,每提1个bug +2, 提1个pull request +5)。
43 |
44 | 2.核心数据使用关系型数据库(PostgreSQL或MySQL数据库)。
45 | blob数据(如图片和大段的文字描述)可以分离出来存其它NoSQL数据库或文件系统。
46 |
47 | 3.对所有的接口都要写test case,通过测试并计算代码覆盖率(有较高的覆盖率是加分项 +2~5)。
48 |
49 | 4.尽量使用正确的软件工程方法及工具,如,版本控制,测试驱动开发 (利用版本控制是加分项 +2~5)
50 |
51 | 5.后端使用技术,实现语言不限;不要复制这个项目上的后端代码(不是正确的实践, 减分项 -2~5)
52 |
53 | 6.不需要实现页面
54 |
55 | 7.最后评估分数时考虑以下要素:
56 | 1)实现完整度,全部测试通过,效率合理
57 | 2)正确地使用数据库和设计分析工具,ER图,从ER图导出关系模式,规范化,事务处理,索引等
58 | 3)其它...
59 |
60 | 8.3个人一组,做好分工,量化每个人的贡献度
61 |
62 |
63 |
64 | ## 项目目录结构
65 | ```
66 | bookstore
67 | |-- be mock的后端
68 | |-- model
69 | |-- view
70 | |-- ....
71 | |-- doc JSON API规范说明
72 | |-- fe 前端代码
73 | |-- access
74 | |-- bench 效率测试
75 | |-- data
76 | |-- book.db sqlite 数据库(book.db,较少量的测试数据)
77 | |-- book_lx.db sqlite 数据库(book_lx.db, 较大量的测试数据,要从网盘下载)
78 | |-- scraper.py 从豆瓣爬取的图书信息数据
79 | |-- test 功能性测试(不要修改这里的文件,可以提pull request或bug)
80 | |-- conf.py 测试参数,修改这个文件以适应自己的需要
81 | |-- conftest.py pytest初始化配置,修改这个文件以适应自己的需要
82 | |-- ....
83 | |-- ....
84 | ```
85 |
86 | ### 安装配置
87 | 安装python (需要python3.6以上)
88 |
89 | 安装依赖
90 |
91 | pip install -r requirements.txt
92 |
93 |
94 | 执行测试
95 |
96 | bash script/test.sh
97 |
98 |
99 | bookstore/fe/data/book.db中包含测试的数据,从豆瓣网抓取的图书信息,
100 | 其DDL为:
101 |
102 | create table book
103 | (
104 | id TEXT primary key,
105 | title TEXT,
106 | author TEXT,
107 | publisher TEXT,
108 | original_title TEXT,
109 | translator TEXT,
110 | pub_year TEXT,
111 | pages INTEGER,
112 | price INTEGER,
113 | currency_unit TEXT,
114 | binding TEXT,
115 | isbn TEXT,
116 | author_intro TEXT,
117 | book_intro text,
118 | content TEXT,
119 | tags TEXT,
120 | picture BLOB
121 | );
122 |
123 |
124 | 更多的数据可以从网盘下载,下载地址为,链接:
125 |
126 | https://pan.baidu.com/s/1bjCOW8Z5N_ClcqU54Pdt8g
127 |
128 | 提取码:
129 |
130 | hj6q
131 |
132 | 这份数据同bookstore/fe/data/book.db的schema相同,但是有更多的数据(约3.5GB, 40000+行)
133 |
134 |
135 |
136 |
137 |
--------------------------------------------------------------------------------
/be/__init__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
--------------------------------------------------------------------------------
/be/app.py:
--------------------------------------------------------------------------------
1 | from be import serve
2 |
3 | if __name__ == "__main__":
4 | serve.be_run()
5 |
--------------------------------------------------------------------------------
/be/model/__init__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
--------------------------------------------------------------------------------
/be/model/buyer.py:
--------------------------------------------------------------------------------
1 | import sqlite3 as sqlite
2 | import uuid
3 | import json
4 | import logging
5 | from be.model import db_conn
6 | from be.model import error
7 |
8 |
9 | class Buyer(db_conn.DBConn):
10 | def __init__(self):
11 | db_conn.DBConn.__init__(self)
12 |
13 | def new_order(self, user_id: str, store_id: str, id_and_count: [(str, int)]) -> (int, str, str):
14 | order_id = ""
15 | try:
16 | if not self.user_id_exist(user_id):
17 | return error.error_non_exist_user_id(user_id) + (order_id, )
18 | if not self.store_id_exist(store_id):
19 | return error.error_non_exist_store_id(store_id) + (order_id, )
20 | uid = "{}_{}_{}".format(user_id, store_id, str(uuid.uuid1()))
21 |
22 | for book_id, count in id_and_count:
23 | cursor = self.conn.execute(
24 | "SELECT book_id, stock_level, book_info FROM store "
25 | "WHERE store_id = ? AND book_id = ?;",
26 | (store_id, book_id))
27 | row = cursor.fetchone()
28 | if row is None:
29 | return error.error_non_exist_book_id(book_id) + (order_id, )
30 |
31 | stock_level = row[1]
32 | book_info = row[2]
33 | book_info_json = json.loads(book_info)
34 | price = book_info_json.get("price")
35 |
36 | if stock_level < count:
37 | return error.error_stock_level_low(book_id) + (order_id,)
38 |
39 | cursor = self.conn.execute(
40 | "UPDATE store set stock_level = stock_level - ? "
41 | "WHERE store_id = ? and book_id = ? and stock_level >= ?; ",
42 | (count, store_id, book_id, count))
43 | if cursor.rowcount == 0:
44 | return error.error_stock_level_low(book_id) + (order_id, )
45 |
46 | self.conn.execute(
47 | "INSERT INTO new_order_detail(order_id, book_id, count, price) "
48 | "VALUES(?, ?, ?, ?);",
49 | (uid, book_id, count, price))
50 |
51 | self.conn.execute(
52 | "INSERT INTO new_order(order_id, store_id, user_id) "
53 | "VALUES(?, ?, ?);",
54 | (uid, store_id, user_id))
55 | self.conn.commit()
56 | order_id = uid
57 | except sqlite.Error as e:
58 | logging.info("528, {}".format(str(e)))
59 | return 528, "{}".format(str(e)), ""
60 | except BaseException as e:
61 | logging.info("530, {}".format(str(e)))
62 | return 530, "{}".format(str(e)), ""
63 |
64 | return 200, "ok", order_id
65 |
66 | def payment(self, user_id: str, password: str, order_id: str) -> (int, str):
67 | conn = self.conn
68 | try:
69 | cursor = conn.execute("SELECT order_id, user_id, store_id FROM new_order WHERE order_id = ?", (order_id,))
70 | row = cursor.fetchone()
71 | if row is None:
72 | return error.error_invalid_order_id(order_id)
73 |
74 | order_id = row[0]
75 | buyer_id = row[1]
76 | store_id = row[2]
77 |
78 | if buyer_id != user_id:
79 | return error.error_authorization_fail()
80 |
81 | cursor = conn.execute("SELECT balance, password FROM user WHERE user_id = ?;", (buyer_id,))
82 | row = cursor.fetchone()
83 | if row is None:
84 | return error.error_non_exist_user_id(buyer_id)
85 | balance = row[0]
86 | if password != row[1]:
87 | return error.error_authorization_fail()
88 |
89 | cursor = conn.execute("SELECT store_id, user_id FROM user_store WHERE store_id = ?;", (store_id,))
90 | row = cursor.fetchone()
91 | if row is None:
92 | return error.error_non_exist_store_id(store_id)
93 |
94 | seller_id = row[1]
95 |
96 | if not self.user_id_exist(seller_id):
97 | return error.error_non_exist_user_id(seller_id)
98 |
99 | cursor = conn.execute("SELECT book_id, count, price FROM new_order_detail WHERE order_id = ?;", (order_id,))
100 | total_price = 0
101 | for row in cursor:
102 | count = row[1]
103 | price = row[2]
104 | total_price = total_price + price * count
105 |
106 | if balance < total_price:
107 | return error.error_not_sufficient_funds(order_id)
108 |
109 | cursor = conn.execute("UPDATE user set balance = balance - ?"
110 | "WHERE user_id = ? AND balance >= ?",
111 | (total_price, buyer_id, total_price))
112 | if cursor.rowcount == 0:
113 | return error.error_not_sufficient_funds(order_id)
114 |
115 | cursor = conn.execute("UPDATE user set balance = balance + ?"
116 | "WHERE user_id = ?",
117 | (total_price, buyer_id))
118 |
119 | if cursor.rowcount == 0:
120 | return error.error_non_exist_user_id(buyer_id)
121 |
122 | cursor = conn.execute("DELETE FROM new_order WHERE order_id = ?", (order_id, ))
123 | if cursor.rowcount == 0:
124 | return error.error_invalid_order_id(order_id)
125 |
126 | cursor = conn.execute("DELETE FROM new_order_detail where order_id = ?", (order_id, ))
127 | if cursor.rowcount == 0:
128 | return error.error_invalid_order_id(order_id)
129 |
130 | conn.commit()
131 |
132 | except sqlite.Error as e:
133 | return 528, "{}".format(str(e))
134 |
135 | except BaseException as e:
136 | return 530, "{}".format(str(e))
137 |
138 | return 200, "ok"
139 |
140 | def add_funds(self, user_id, password, add_value) -> (int, str):
141 | try:
142 | cursor = self.conn.execute("SELECT password from user where user_id=?", (user_id,))
143 | row = cursor.fetchone()
144 | if row is None:
145 | return error.error_authorization_fail()
146 |
147 | if row[0] != password:
148 | return error.error_authorization_fail()
149 |
150 | cursor = self.conn.execute(
151 | "UPDATE user SET balance = balance + ? WHERE user_id = ?",
152 | (add_value, user_id))
153 | if cursor.rowcount == 0:
154 | return error.error_non_exist_user_id(user_id)
155 |
156 | self.conn.commit()
157 | except sqlite.Error as e:
158 | return 528, "{}".format(str(e))
159 | except BaseException as e:
160 | return 530, "{}".format(str(e))
161 |
162 | return 200, "ok"
163 |
--------------------------------------------------------------------------------
/be/model/db_conn.py:
--------------------------------------------------------------------------------
1 | from be.model import store
2 |
3 |
4 | class DBConn:
5 | def __init__(self):
6 | self.conn = store.get_db_conn()
7 |
8 | def user_id_exist(self, user_id):
9 | cursor = self.conn.execute("SELECT user_id FROM user WHERE user_id = ?;", (user_id,))
10 | row = cursor.fetchone()
11 | if row is None:
12 | return False
13 | else:
14 | return True
15 |
16 | def book_id_exist(self, store_id, book_id):
17 | cursor = self.conn.execute("SELECT book_id FROM store WHERE store_id = ? AND book_id = ?;", (store_id, book_id))
18 | row = cursor.fetchone()
19 | if row is None:
20 | return False
21 | else:
22 | return True
23 |
24 | def store_id_exist(self, store_id):
25 | cursor = self.conn.execute("SELECT store_id FROM user_store WHERE store_id = ?;", (store_id,))
26 | row = cursor.fetchone()
27 | if row is None:
28 | return False
29 | else:
30 | return True
31 |
--------------------------------------------------------------------------------
/be/model/error.py:
--------------------------------------------------------------------------------
1 |
2 | error_code = {
3 | 401: "authorization fail.",
4 | 511: "non exist user id {}",
5 | 512: "exist user id {}",
6 | 513: "non exist store id {}",
7 | 514: "exist store id {}",
8 | 515: "non exist book id {}",
9 | 516: "exist book id {}",
10 | 517: "stock level low, book id {}",
11 | 518: "invalid order id {}",
12 | 519: "not sufficient funds, order id {}",
13 | 520: "",
14 | 521: "",
15 | 522: "",
16 | 523: "",
17 | 524: "",
18 | 525: "",
19 | 526: "",
20 | 527: "",
21 | 528: "",
22 | }
23 |
24 |
25 | def error_non_exist_user_id(user_id):
26 | return 511, error_code[511].format(user_id)
27 |
28 |
29 | def error_exist_user_id(user_id):
30 | return 512, error_code[512].format(user_id)
31 |
32 |
33 | def error_non_exist_store_id(store_id):
34 | return 513, error_code[513].format(store_id)
35 |
36 |
37 | def error_exist_store_id(store_id):
38 | return 514, error_code[514].format(store_id)
39 |
40 |
41 | def error_non_exist_book_id(book_id):
42 | return 515, error_code[515].format(book_id)
43 |
44 |
45 | def error_exist_book_id(book_id):
46 | return 516, error_code[516].format(book_id)
47 |
48 |
49 | def error_stock_level_low(book_id):
50 | return 517, error_code[517].format(book_id)
51 |
52 |
53 | def error_invalid_order_id(order_id):
54 | return 518, error_code[518].format(order_id)
55 |
56 |
57 | def error_not_sufficient_funds(order_id):
58 | return 519, error_code[518].format(order_id)
59 |
60 |
61 | def error_authorization_fail():
62 | return 401, error_code[401]
63 |
64 |
65 | def error_and_message(code, message):
66 | return code, message
67 |
--------------------------------------------------------------------------------
/be/model/seller.py:
--------------------------------------------------------------------------------
1 | import sqlite3 as sqlite
2 | from be.model import error
3 | from be.model import db_conn
4 |
5 |
6 | class Seller(db_conn.DBConn):
7 |
8 | def __init__(self):
9 | db_conn.DBConn.__init__(self)
10 |
11 | def add_book(self, user_id: str, store_id: str, book_id: str, book_json_str: str, stock_level: int):
12 | try:
13 | if not self.user_id_exist(user_id):
14 | return error.error_non_exist_user_id(user_id)
15 | if not self.store_id_exist(store_id):
16 | return error.error_non_exist_store_id(store_id)
17 | if self.book_id_exist(store_id, book_id):
18 | return error.error_exist_book_id(book_id)
19 |
20 | self.conn.execute("INSERT into store(store_id, book_id, book_info, stock_level)"
21 | "VALUES (?, ?, ?, ?)", (store_id, book_id, book_json_str, stock_level))
22 | self.conn.commit()
23 | except sqlite.Error as e:
24 | return 528, "{}".format(str(e))
25 | except BaseException as e:
26 | return 530, "{}".format(str(e))
27 | return 200, "ok"
28 |
29 | def add_stock_level(self, user_id: str, store_id: str, book_id: str, add_stock_level: int):
30 | try:
31 | if not self.user_id_exist(user_id):
32 | return error.error_non_exist_user_id(user_id)
33 | if not self.store_id_exist(store_id):
34 | return error.error_non_exist_store_id(store_id)
35 | if not self.book_id_exist(store_id, book_id):
36 | return error.error_non_exist_book_id(book_id)
37 |
38 | self.conn.execute("UPDATE store SET stock_level = stock_level + ? "
39 | "WHERE store_id = ? AND book_id = ?", (add_stock_level, store_id, book_id))
40 | self.conn.commit()
41 | except sqlite.Error as e:
42 | return 528, "{}".format(str(e))
43 | except BaseException as e:
44 | return 530, "{}".format(str(e))
45 | return 200, "ok"
46 |
47 | def create_store(self, user_id: str, store_id: str) -> (int, str):
48 | try:
49 | if not self.user_id_exist(user_id):
50 | return error.error_non_exist_user_id(user_id)
51 | if self.store_id_exist(store_id):
52 | return error.error_exist_store_id(store_id)
53 | self.conn.execute("INSERT into user_store(store_id, user_id)"
54 | "VALUES (?, ?)", (store_id, user_id))
55 | self.conn.commit()
56 | except sqlite.Error as e:
57 | return 528, "{}".format(str(e))
58 | except BaseException as e:
59 | return 530, "{}".format(str(e))
60 | return 200, "ok"
61 |
--------------------------------------------------------------------------------
/be/model/store.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os
3 | import sqlite3 as sqlite
4 |
5 |
6 | class Store:
7 | database: str
8 |
9 | def __init__(self, db_path):
10 | self.database = os.path.join(db_path, "be.db")
11 | self.init_tables()
12 |
13 | def init_tables(self):
14 | try:
15 | conn = self.get_db_conn()
16 | conn.execute(
17 | "CREATE TABLE IF NOT EXISTS user ("
18 | "user_id TEXT PRIMARY KEY, password TEXT NOT NULL, "
19 | "balance INTEGER NOT NULL, token TEXT, terminal TEXT);"
20 | )
21 |
22 | conn.execute(
23 | "CREATE TABLE IF NOT EXISTS user_store("
24 | "user_id TEXT, store_id, PRIMARY KEY(user_id, store_id));"
25 | )
26 |
27 | conn.execute(
28 | "CREATE TABLE IF NOT EXISTS store( "
29 | "store_id TEXT, book_id TEXT, book_info TEXT, stock_level INTEGER,"
30 | " PRIMARY KEY(store_id, book_id))"
31 | )
32 |
33 | conn.execute(
34 | "CREATE TABLE IF NOT EXISTS new_order( "
35 | "order_id TEXT PRIMARY KEY, user_id TEXT, store_id TEXT)"
36 | )
37 |
38 | conn.execute(
39 | "CREATE TABLE IF NOT EXISTS new_order_detail( "
40 | "order_id TEXT, book_id TEXT, count INTEGER, price INTEGER, "
41 | "PRIMARY KEY(order_id, book_id))"
42 | )
43 |
44 | conn.commit()
45 | except sqlite.Error as e:
46 | logging.error(e)
47 | conn.rollback()
48 |
49 | def get_db_conn(self) -> sqlite.Connection:
50 | return sqlite.connect(self.database)
51 |
52 |
53 | database_instance: Store = None
54 |
55 |
56 | def init_database(db_path):
57 | global database_instance
58 | database_instance = Store(db_path)
59 |
60 |
61 | def get_db_conn():
62 | global database_instance
63 | return database_instance.get_db_conn()
64 |
--------------------------------------------------------------------------------
/be/model/user.py:
--------------------------------------------------------------------------------
1 | import jwt
2 | import time
3 | import logging
4 | import sqlite3 as sqlite
5 | from be.model import error
6 | from be.model import db_conn
7 |
8 | # encode a json string like:
9 | # {
10 | # "user_id": [user name],
11 | # "terminal": [terminal code],
12 | # "timestamp": [ts]} to a JWT
13 | # }
14 |
15 |
16 | def jwt_encode(user_id: str, terminal: str) -> str:
17 | encoded = jwt.encode(
18 | {"user_id": user_id, "terminal": terminal, "timestamp": time.time()},
19 | key=user_id,
20 | algorithm="HS256",
21 | )
22 | return encoded.decode("utf-8")
23 |
24 |
25 | # decode a JWT to a json string like:
26 | # {
27 | # "user_id": [user name],
28 | # "terminal": [terminal code],
29 | # "timestamp": [ts]} to a JWT
30 | # }
31 | def jwt_decode(encoded_token, user_id: str) -> str:
32 | decoded = jwt.decode(encoded_token, key=user_id, algorithms="HS256")
33 | return decoded
34 |
35 |
36 | class User(db_conn.DBConn):
37 | token_lifetime: int = 3600 # 3600 second
38 |
39 | def __init__(self):
40 | db_conn.DBConn.__init__(self)
41 |
42 | def __check_token(self, user_id, db_token, token) -> bool:
43 | try:
44 | if db_token != token:
45 | return False
46 | jwt_text = jwt_decode(encoded_token=token, user_id=user_id)
47 | ts = jwt_text["timestamp"]
48 | if ts is not None:
49 | now = time.time()
50 | if self.token_lifetime > now - ts >= 0:
51 | return True
52 | except jwt.exceptions.InvalidSignatureError as e:
53 | logging.error(str(e))
54 | return False
55 |
56 | def register(self, user_id: str, password: str):
57 | try:
58 | terminal = "terminal_{}".format(str(time.time()))
59 | token = jwt_encode(user_id, terminal)
60 | self.conn.execute(
61 | "INSERT into user(user_id, password, balance, token, terminal) "
62 | "VALUES (?, ?, ?, ?, ?);",
63 | (user_id, password, 0, token, terminal), )
64 | self.conn.commit()
65 | except sqlite.Error:
66 | return error.error_exist_user_id(user_id)
67 | return 200, "ok"
68 |
69 | def check_token(self, user_id: str, token: str) -> (int, str):
70 | cursor = self.conn.execute("SELECT token from user where user_id=?", (user_id,))
71 | row = cursor.fetchone()
72 | if row is None:
73 | return error.error_authorization_fail()
74 | db_token = row[0]
75 | if not self.__check_token(user_id, db_token, token):
76 | return error.error_authorization_fail()
77 | return 200, "ok"
78 |
79 | def check_password(self, user_id: str, password: str) -> (int, str):
80 | cursor = self.conn.execute("SELECT password from user where user_id=?", (user_id,))
81 | row = cursor.fetchone()
82 | if row is None:
83 | return error.error_authorization_fail()
84 |
85 | if password != row[0]:
86 | return error.error_authorization_fail()
87 |
88 | return 200, "ok"
89 |
90 | def login(self, user_id: str, password: str, terminal: str) -> (int, str, str):
91 | token = ""
92 | try:
93 | code, message = self.check_password(user_id, password)
94 | if code != 200:
95 | return code, message, ""
96 |
97 | token = jwt_encode(user_id, terminal)
98 | cursor = self.conn.execute(
99 | "UPDATE user set token= ? , terminal = ? where user_id = ?",
100 | (token, terminal, user_id), )
101 | if cursor.rowcount == 0:
102 | return error.error_authorization_fail() + ("", )
103 | self.conn.commit()
104 | except sqlite.Error as e:
105 | return 528, "{}".format(str(e)), ""
106 | except BaseException as e:
107 | return 530, "{}".format(str(e)), ""
108 | return 200, "ok", token
109 |
110 | def logout(self, user_id: str, token: str) -> bool:
111 | try:
112 | code, message = self.check_token(user_id, token)
113 | if code != 200:
114 | return code, message
115 |
116 | terminal = "terminal_{}".format(str(time.time()))
117 | dummy_token = jwt_encode(user_id, terminal)
118 |
119 | cursor = self.conn.execute(
120 | "UPDATE user SET token = ?, terminal = ? WHERE user_id=?",
121 | (dummy_token, terminal, user_id), )
122 | if cursor.rowcount == 0:
123 | return error.error_authorization_fail()
124 |
125 | self.conn.commit()
126 | except sqlite.Error as e:
127 | return 528, "{}".format(str(e))
128 | except BaseException as e:
129 | return 530, "{}".format(str(e))
130 | return 200, "ok"
131 |
132 | def unregister(self, user_id: str, password: str) -> (int, str):
133 | try:
134 | code, message = self.check_password(user_id, password)
135 | if code != 200:
136 | return code, message
137 |
138 | cursor = self.conn.execute("DELETE from user where user_id=?", (user_id,))
139 | if cursor.rowcount == 1:
140 | self.conn.commit()
141 | else:
142 | return error.error_authorization_fail()
143 | except sqlite.Error as e:
144 | return 528, "{}".format(str(e))
145 | except BaseException as e:
146 | return 530, "{}".format(str(e))
147 | return 200, "ok"
148 |
149 | def change_password(self, user_id: str, old_password: str, new_password: str) -> bool:
150 | try:
151 | code, message = self.check_password(user_id, old_password)
152 | if code != 200:
153 | return code, message
154 |
155 | terminal = "terminal_{}".format(str(time.time()))
156 | token = jwt_encode(user_id, terminal)
157 | cursor = self.conn.execute(
158 | "UPDATE user set password = ?, token= ? , terminal = ? where user_id = ?",
159 | (new_password, token, terminal, user_id), )
160 | if cursor.rowcount == 0:
161 | return error.error_authorization_fail()
162 |
163 | self.conn.commit()
164 | except sqlite.Error as e:
165 | return 528, "{}".format(str(e))
166 | except BaseException as e:
167 | return 530, "{}".format(str(e))
168 | return 200, "ok"
169 |
170 |
--------------------------------------------------------------------------------
/be/serve.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os
3 | from flask import Flask
4 | from flask import Blueprint
5 | from flask import request
6 | from be.view import auth
7 | from be.view import seller
8 | from be.view import buyer
9 | from be.model.store import init_database
10 |
11 | bp_shutdown = Blueprint("shutdown", __name__)
12 |
13 |
14 | def shutdown_server():
15 | func = request.environ.get("werkzeug.server.shutdown")
16 | if func is None:
17 | raise RuntimeError("Not running with the Werkzeug Server")
18 | func()
19 |
20 |
21 | @bp_shutdown.route("/shutdown")
22 | def be_shutdown():
23 | shutdown_server()
24 | return "Server shutting down..."
25 |
26 |
27 | def be_run():
28 | this_path = os.path.dirname(__file__)
29 | parent_path = os.path.dirname(this_path)
30 | log_file = os.path.join(parent_path, "app.log")
31 | init_database(parent_path)
32 |
33 | logging.basicConfig(filename=log_file, level=logging.ERROR)
34 | handler = logging.StreamHandler()
35 | formatter = logging.Formatter(
36 | "%(asctime)s [%(threadName)-12.12s] [%(levelname)-5.5s] %(message)s"
37 | )
38 | handler.setFormatter(formatter)
39 | logging.getLogger().addHandler(handler)
40 |
41 | app = Flask(__name__)
42 | app.register_blueprint(bp_shutdown)
43 | app.register_blueprint(auth.bp_auth)
44 | app.register_blueprint(seller.bp_seller)
45 | app.register_blueprint(buyer.bp_buyer)
46 | app.run()
47 |
--------------------------------------------------------------------------------
/be/view/__init__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
--------------------------------------------------------------------------------
/be/view/auth.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint
2 | from flask import request
3 | from flask import jsonify
4 | from be.model import user
5 |
6 | bp_auth = Blueprint("auth", __name__, url_prefix="/auth")
7 |
8 |
9 | @bp_auth.route("/login", methods=["POST"])
10 | def login():
11 | user_id = request.json.get("user_id", "")
12 | password = request.json.get("password", "")
13 | terminal = request.json.get("terminal", "")
14 | u = user.User()
15 | code, message, token = u.login(user_id=user_id, password=password, terminal=terminal)
16 | return jsonify({"message": message, "token": token}), code
17 |
18 |
19 | @bp_auth.route("/logout", methods=["POST"])
20 | def logout():
21 | user_id: str = request.json.get("user_id")
22 | token: str = request.headers.get("token")
23 | u = user.User()
24 | code, message = u.logout(user_id=user_id, token=token)
25 | return jsonify({"message": message}), code
26 |
27 |
28 | @bp_auth.route("/register", methods=["POST"])
29 | def register():
30 | user_id = request.json.get("user_id", "")
31 | password = request.json.get("password", "")
32 | u = user.User()
33 | code, message = u.register(user_id=user_id, password=password)
34 | return jsonify({"message": message}), code
35 |
36 |
37 | @bp_auth.route("/unregister", methods=["POST"])
38 | def unregister():
39 | user_id = request.json.get("user_id", "")
40 | password = request.json.get("password", "")
41 | u = user.User()
42 | code, message = u.unregister(user_id=user_id, password=password)
43 | return jsonify({"message": message}), code
44 |
45 |
46 | @bp_auth.route("/password", methods=["POST"])
47 | def change_password():
48 | user_id = request.json.get("user_id", "")
49 | old_password = request.json.get("oldPassword", "")
50 | new_password = request.json.get("newPassword", "")
51 | u = user.User()
52 | code, message = u.change_password(user_id=user_id, old_password=old_password, new_password=new_password)
53 | return jsonify({"message": message}), code
54 |
--------------------------------------------------------------------------------
/be/view/buyer.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint
2 | from flask import request
3 | from flask import jsonify
4 | from be.model.buyer import Buyer
5 |
6 | bp_buyer = Blueprint("buyer", __name__, url_prefix="/buyer")
7 |
8 |
9 | @bp_buyer.route("/new_order", methods=["POST"])
10 | def new_order():
11 | user_id: str = request.json.get("user_id")
12 | store_id: str = request.json.get("store_id")
13 | books: [] = request.json.get("books")
14 | id_and_count = []
15 | for book in books:
16 | book_id = book.get("id")
17 | count = book.get("count")
18 | id_and_count.append((book_id, count))
19 |
20 | b = Buyer()
21 | code, message, order_id = b.new_order(user_id, store_id, id_and_count)
22 | return jsonify({"message": message, "order_id": order_id}), code
23 |
24 |
25 | @bp_buyer.route("/payment", methods=["POST"])
26 | def payment():
27 | user_id: str = request.json.get("user_id")
28 | order_id: str = request.json.get("order_id")
29 | password: str = request.json.get("password")
30 | b = Buyer()
31 | code, message = b.payment(user_id, password, order_id)
32 | return jsonify({"message": message}), code
33 |
34 |
35 | @bp_buyer.route("/add_funds", methods=["POST"])
36 | def add_funds():
37 | user_id = request.json.get("user_id")
38 | password = request.json.get("password")
39 | add_value = request.json.get("add_value")
40 | b = Buyer()
41 | code, message = b.add_funds(user_id, password, add_value)
42 | return jsonify({"message": message}), code
43 |
--------------------------------------------------------------------------------
/be/view/seller.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint
2 | from flask import request
3 | from flask import jsonify
4 | from be.model import seller
5 | import json
6 |
7 | bp_seller = Blueprint("seller", __name__, url_prefix="/seller")
8 |
9 |
10 | @bp_seller.route("/create_store", methods=["POST"])
11 | def seller_create_store():
12 | user_id: str = request.json.get("user_id")
13 | store_id: str = request.json.get("store_id")
14 | s = seller.Seller()
15 | code, message = s.create_store(user_id, store_id)
16 | return jsonify({"message": message}), code
17 |
18 |
19 | @bp_seller.route("/add_book", methods=["POST"])
20 | def seller_add_book():
21 | user_id: str = request.json.get("user_id")
22 | store_id: str = request.json.get("store_id")
23 | book_info: str = request.json.get("book_info")
24 | stock_level: str = request.json.get("stock_level", 0)
25 |
26 | s = seller.Seller()
27 | code, message = s.add_book(user_id, store_id, book_info.get("id"), json.dumps(book_info), stock_level)
28 |
29 | return jsonify({"message": message}), code
30 |
31 |
32 | @bp_seller.route("/add_stock_level", methods=["POST"])
33 | def add_stock_level():
34 | user_id: str = request.json.get("user_id")
35 | store_id: str = request.json.get("store_id")
36 | book_id: str = request.json.get("book_id")
37 | add_num: str = request.json.get("add_stock_level", 0)
38 |
39 | s = seller.Seller()
40 | code, message = s.add_stock_level(user_id, store_id, book_id, add_num)
41 |
42 | return jsonify({"message": message}), code
43 |
--------------------------------------------------------------------------------
/doc/auth.md:
--------------------------------------------------------------------------------
1 | ## 注册用户
2 |
3 | #### URL:
4 | POST http://$address$/auth/register
5 |
6 | #### Request
7 |
8 | Body:
9 | ```
10 | {
11 | "user_id":"$user name$",
12 | "password":"$user password$"
13 | }
14 | ```
15 |
16 | 变量名 | 类型 | 描述 | 是否可为空
17 | ---|---|---|---
18 | user_id | string | 用户名 | N
19 | password | string | 登陆密码 | N
20 |
21 | #### Response
22 |
23 | Status Code:
24 |
25 |
26 | 码 | 描述
27 | --- | ---
28 | 200 | 注册成功
29 | 5XX | 注册失败,用户名重复
30 |
31 | Body:
32 | ```
33 | {
34 | "message":"$error message$"
35 | }
36 | ```
37 | 变量名 | 类型 | 描述 | 是否可为空
38 | ---|---|---|---
39 | message | string | 返回错误消息,成功时为"ok" | N
40 |
41 | ## 注销用户
42 |
43 | #### URL:
44 | POST http://$address$/auth/unregister
45 |
46 | #### Request
47 |
48 | Body:
49 | ```
50 | {
51 | "user_id":"$user name$",
52 | "password":"$user password$"
53 | }
54 | ```
55 |
56 | 变量名 | 类型 | 描述 | 是否可为空
57 | ---|---|---|---
58 | user_id | string | 用户名 | N
59 | password | string | 登陆密码 | N
60 |
61 | #### Response
62 |
63 | Status Code:
64 |
65 |
66 | 码 | 描述
67 | --- | ---
68 | 200 | 注销成功
69 | 401 | 注销失败,用户名不存在或密码不正确
70 |
71 |
72 | Body:
73 | ```
74 | {
75 | "message":"$error message$"
76 | }
77 | ```
78 | 变量名 | 类型 | 描述 | 是否可为空
79 | ---|---|---|---
80 | message | string | 返回错误消息,成功时为"ok" | N
81 |
82 | ## 用户登录
83 |
84 | #### URL:
85 | POST http://$address$/auth/login
86 |
87 | #### Request
88 |
89 | Body:
90 | ```
91 | {
92 | "user_id":"$user name$",
93 | "password":"$user password$",
94 | "terminal":"$terminal code$"
95 | }
96 | ```
97 |
98 | 变量名 | 类型 | 描述 | 是否可为空
99 | ---|---|---|---
100 | user_id | string | 用户名 | N
101 | password | string | 登陆密码 | N
102 | terminal | string | 终端代码 | N
103 |
104 | #### Response
105 |
106 | Status Code:
107 |
108 | 码 | 描述
109 | --- | ---
110 | 200 | 登录成功
111 | 401 | 登录失败,用户名或密码错误
112 |
113 | Body:
114 | ```
115 | {
116 | "message":"$error message$",
117 | "token":"$access token$"
118 | }
119 | ```
120 | 变量名 | 类型 | 描述 | 是否可为空
121 | ---|---|---|---
122 | message | string | 返回错误消息,成功时为"ok" | N
123 | token | string | 访问token,用户登录后每个需要授权的请求应在headers中传入这个token | 成功时不为空
124 |
125 | #### 说明
126 |
127 | 1.terminal标识是哪个设备登录的,不同的设备拥有不同的ID,测试时可以随机生成。
128 |
129 | 2.token是登录后,在客户端中缓存的令牌,在用户登录时由服务端生成,用户在接下来的访问请求时不需要密码。token会定期地失效,对于不同的设备,token是不同的。token只对特定的时期特定的设备是有效的。
130 |
131 | ## 用户更改密码
132 |
133 | #### URL:
134 | POST http://$address$/auth/password
135 |
136 | #### Request
137 |
138 | Body:
139 | ```
140 | {
141 | "user_id":"$user name$",
142 | "oldPassword":"$old password$",
143 | "newPassword":"$new password$"
144 | }
145 | ```
146 |
147 | 变量名 | 类型 | 描述 | 是否可为空
148 | ---|---|---|---
149 | user_id | string | 用户名 | N
150 | oldPassword | string | 旧的登陆密码 | N
151 | newPassword | string | 新的登陆密码 | N
152 |
153 | #### Response
154 |
155 | Status Code:
156 |
157 | 码 | 描述
158 | --- | ---
159 | 200 | 更改密码成功
160 | 401 | 更改密码失败
161 |
162 | Body:
163 | ```
164 | {
165 | "message":"$error message$",
166 | }
167 | ```
168 | 变量名 | 类型 | 描述 | 是否可为空
169 | ---|---|---|---
170 | message | string | 返回错误消息,成功时为"ok" | N
171 |
172 | ## 用户登出
173 |
174 | #### URL:
175 | POST http://$address$/auth/logout
176 |
177 | #### Request
178 |
179 | Headers:
180 |
181 | key | 类型 | 描述
182 | ---|---|---
183 | token | string | 访问token
184 |
185 | Body:
186 | ```
187 | {
188 | "user_id":"$user name$"
189 | }
190 | ```
191 |
192 | 变量名 | 类型 | 描述 | 是否可为空
193 | ---|---|---|---
194 | user_id | string | 用户名 | N
195 |
196 | #### Response
197 |
198 | Status Code:
199 |
200 | 码 | 描述
201 | --- | ---
202 | 200 | 登出成功
203 | 401 | 登出失败,用户名或token错误
204 |
205 | Body:
206 | ```
207 | {
208 | "message":"$error message$"
209 | }
210 | ```
211 | 变量名 | 类型 | 描述 | 是否可为空
212 | ---|---|---|---
213 | message | string | 返回错误消息,成功时为"ok" | N
214 |
--------------------------------------------------------------------------------
/doc/buyer.md:
--------------------------------------------------------------------------------
1 | ## 买家下单
2 |
3 | #### URL:
4 | POST http://[address]/buyer/new_order
5 |
6 | #### Request
7 |
8 | ##### Header:
9 |
10 | key | 类型 | 描述 | 是否可为空
11 | ---|---|---|---
12 | token | string | 登录产生的会话标识 | N
13 |
14 | ##### Body:
15 | ```json
16 | {
17 | "user_id": "buyer_id",
18 | "store_id": "store_id",
19 | "books": [
20 | {
21 | "id": "1000067",
22 | "count": 1
23 | },
24 | {
25 | "id": "1000134",
26 | "count": 4
27 | }
28 | ]
29 | }
30 | ```
31 |
32 | ##### 属性说明:
33 |
34 | 变量名 | 类型 | 描述 | 是否可为空
35 | ---|---|---|---
36 | user_id | string | 买家用户ID | N
37 | store_id | string | 商铺ID | N
38 | books | class | 书籍购买列表 | N
39 |
40 | books数组:
41 |
42 | 变量名 | 类型 | 描述 | 是否可为空
43 | ---|---|---|---
44 | id | string | 书籍的ID | N
45 | count | string | 购买数量 | N
46 |
47 |
48 | #### Response
49 |
50 | Status Code:
51 |
52 | 码 | 描述
53 | --- | ---
54 | 200 | 下单成功
55 | 5XX | 买家用户ID不存在
56 | 5XX | 商铺ID不存在
57 | 5XX | 购买的图书不存在
58 | 5XX | 商品库存不足
59 |
60 | ##### Body:
61 | ```json
62 | {
63 | "order_id": "uuid"
64 | }
65 | ```
66 |
67 | ##### 属性说明:
68 |
69 | 变量名 | 类型 | 描述 | 是否可为空
70 | ---|---|---|---
71 | order_id | string | 订单号,只有返回200时才有效 | N
72 |
73 |
74 | ## 买家付款
75 |
76 | #### URL:
77 | POST http://[address]/buyer/payment
78 |
79 | #### Request
80 |
81 | ##### Body:
82 | ```json
83 | {
84 | "user_id": "buyer_id",
85 | "order_id": "order_id",
86 | "password": "password"
87 | }
88 | ```
89 |
90 | ##### 属性说明:
91 |
92 | 变量名 | 类型 | 描述 | 是否可为空
93 | ---|---|---|---
94 | user_id | string | 买家用户ID | N
95 | order_id | string | 订单ID | N
96 | password | string | 买家用户密码 | N
97 |
98 |
99 | #### Response
100 |
101 | Status Code:
102 |
103 | 码 | 描述
104 | --- | ---
105 | 200 | 付款成功
106 | 5XX | 账户余额不足
107 | 5XX | 无效参数
108 | 401 | 授权失败
109 |
110 |
111 | ## 买家充值
112 |
113 | #### URL:
114 | POST http://[address]/buyer/add_funds
115 |
116 | #### Request
117 |
118 |
119 |
120 | ##### Body:
121 | ```json
122 | {
123 | "user_id": "user_id",
124 | "password": "password",
125 | "add_value": 10
126 | }
127 | ```
128 |
129 | ##### 属性说明:
130 |
131 | key | 类型 | 描述 | 是否可为空
132 | ---|---|---|---
133 | user_id | string | 买家用户ID | N
134 | password | string | 用户密码 | N
135 | add_value | int | 充值金额,以分为单位 | N
136 |
137 |
138 | Status Code:
139 |
140 | 码 | 描述
141 | --- | ---
142 | 200 | 充值成功
143 | 401 | 授权失败
144 | 5XX | 无效参数
145 |
--------------------------------------------------------------------------------
/doc/seller.md:
--------------------------------------------------------------------------------
1 | ## 创建商铺
2 |
3 |
4 |
5 | #### URL
6 |
7 | POST http://[address]/seller/create_store
8 |
9 | #### Request
10 | Headers:
11 |
12 | key | 类型 | 描述 | 是否可为空
13 | ---|---|---|---
14 | token | string | 登录产生的会话标识 | N
15 |
16 | Body:
17 |
18 | ```json
19 | {
20 | "user_id": "$seller id$",
21 | "store_id": "$store id$"
22 | }
23 | ```
24 |
25 | key | 类型 | 描述 | 是否可为空
26 | ---|---|---|---
27 | user_id | string | 卖家用户ID | N
28 | store_id | string | 商铺ID | N
29 |
30 | #### Response
31 |
32 | Status Code:
33 |
34 | 码 | 描述
35 | --- | ---
36 | 200 | 创建商铺成功
37 | 5XX | 商铺ID已存在
38 |
39 |
40 | ## 商家添加书籍信息
41 |
42 | #### URL:
43 | POST http://[address]/seller/add_book
44 |
45 | #### Request
46 | Headers:
47 |
48 | key | 类型 | 描述 | 是否可为空
49 | ---|---|---|---
50 | token | string | 登录产生的会话标识 | N
51 |
52 | Body:
53 |
54 | ```json
55 | {
56 | "user_id": "$seller user id$",
57 | "store_id": "$store id$",
58 | "book_info": {
59 | "tags": [
60 | "tags1",
61 | "tags2",
62 | "tags3",
63 | "..."
64 | ],
65 | "pictures": [
66 | "$Base 64 encoded bytes array1$",
67 | "$Base 64 encoded bytes array2$",
68 | "$Base 64 encoded bytes array3$",
69 | "..."
70 | ],
71 | "id": "$book id$",
72 | "title": "$book title$",
73 | "author": "$book author$",
74 | "publisher": "$book publisher$",
75 | "original_title": "$original title$",
76 | "translator": "translater",
77 | "pub_year": "$pub year$",
78 | "pages": 10,
79 | "price": 10,
80 | "binding": "平装",
81 | "isbn": "$isbn$",
82 | "author_intro": "$author introduction$",
83 | "book_intro": "$book introduction$",
84 | "content": "$chapter1 ...$"
85 | },
86 | "stock_level": 0
87 | }
88 |
89 | ```
90 |
91 | 属性说明:
92 |
93 | 变量名 | 类型 | 描述 | 是否可为空
94 | ---|---|---|---
95 | user_id | string | 卖家用户ID | N
96 | store_id | string | 商铺ID | N
97 | book_info | class | 书籍信息 | N
98 | stock_level | int | 初始库存,大于等于0 | N
99 |
100 | book_info类:
101 |
102 | 变量名 | 类型 | 描述 | 是否可为空
103 | ---|---|---|---
104 | id | string | 书籍ID | N
105 | title | string | 书籍题目 | N
106 | author | string | 作者 | Y
107 | publisher | string | 出版社 | Y
108 | original_title | string | 原书题目 | Y
109 | translator | string | 译者 | Y
110 | pub_year | string | 出版年月 | Y
111 | pages | int | 页数 | Y
112 | price | int | 价格(以分为单位) | N
113 | binding | string | 装帧,精状/平装 | Y
114 | isbn | string | ISBN号 | Y
115 | author_intro | string | 作者简介 | Y
116 | book_intro | string | 书籍简介 | Y
117 | content | string | 样章试读 | Y
118 | tags | array | 标签 | Y
119 | pictures | array | 照片 | Y
120 |
121 | tags和pictures:
122 |
123 | tags 中每个数组元素都是string类型
124 | picture 中每个数组元素都是string(base64表示的bytes array)类型
125 |
126 |
127 | #### Response
128 |
129 | Status Code:
130 |
131 | 码 | 描述
132 | --- | ---
133 | 200 | 添加图书信息成功
134 | 5XX | 卖家用户ID不存在
135 | 5XX | 商铺ID不存在
136 | 5XX | 图书ID已存在
137 |
138 |
139 | ## 商家添加书籍库存
140 |
141 |
142 | #### URL
143 |
144 | POST http://[address]/seller/add_stock_level
145 |
146 | #### Request
147 | Headers:
148 |
149 | key | 类型 | 描述 | 是否可为空
150 | ---|---|---|---
151 | token | string | 登录产生的会话标识 | N
152 |
153 | Body:
154 |
155 | ```json
156 | {
157 | "user_id": "$seller id$",
158 | "store_id": "$store id$",
159 | "book_id": "$book id$",
160 | "add_stock_level": 10
161 | }
162 | ```
163 | key | 类型 | 描述 | 是否可为空
164 | ---|---|---|---
165 | user_id | string | 卖家用户ID | N
166 | store_id | string | 商铺ID | N
167 | book_id | string | 书籍ID | N
168 | add_stock_level | int | 增加的库存量 | N
169 |
170 | #### Response
171 |
172 | Status Code:
173 |
174 | 码 | 描述
175 | --- | :--
176 | 200 | 创建商铺成功
177 | 5XX | 商铺ID不存在
178 | 5XX | 图书ID不存在
179 |
--------------------------------------------------------------------------------
/fe/__init__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
--------------------------------------------------------------------------------
/fe/access/__init__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
--------------------------------------------------------------------------------
/fe/access/auth.py:
--------------------------------------------------------------------------------
1 | import requests
2 | from urllib.parse import urljoin
3 |
4 |
5 | class Auth:
6 | def __init__(self, url_prefix):
7 | self.url_prefix = urljoin(url_prefix, "auth/")
8 |
9 | def login(self, user_id: str, password: str, terminal: str) -> (int, str):
10 | json = {"user_id": user_id, "password": password, "terminal": terminal}
11 | url = urljoin(self.url_prefix, "login")
12 | r = requests.post(url, json=json)
13 | return r.status_code, r.json().get("token")
14 |
15 | def register(
16 | self,
17 | user_id: str,
18 | password: str
19 | ) -> int:
20 | json = {
21 | "user_id": user_id,
22 | "password": password
23 | }
24 | url = urljoin(self.url_prefix, "register")
25 | r = requests.post(url, json=json)
26 | return r.status_code
27 |
28 | def password(self, user_id: str, old_password: str, new_password: str) -> int:
29 | json = {
30 | "user_id": user_id,
31 | "oldPassword": old_password,
32 | "newPassword": new_password,
33 | }
34 | url = urljoin(self.url_prefix, "password")
35 | r = requests.post(url, json=json)
36 | return r.status_code
37 |
38 | def logout(self, user_id: str, token: str) -> int:
39 | json = {"user_id": user_id}
40 | headers = {"token": token}
41 | url = urljoin(self.url_prefix, "logout")
42 | r = requests.post(url, headers=headers, json=json)
43 | return r.status_code
44 |
45 | def unregister(self, user_id: str, password: str) -> int:
46 | json = {"user_id": user_id, "password": password}
47 | url = urljoin(self.url_prefix, "unregister")
48 | r = requests.post(url, json=json)
49 | return r.status_code
50 |
--------------------------------------------------------------------------------
/fe/access/book.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sqlite3 as sqlite
3 | import random
4 | import base64
5 | import simplejson as json
6 |
7 |
8 | class Book:
9 | id: str
10 | title: str
11 | author: str
12 | publisher: str
13 | original_title: str
14 | translator: str
15 | pub_year: str
16 | pages: int
17 | price: int
18 | binding: str
19 | isbn: str
20 | author_intro: str
21 | book_intro: str
22 | content: str
23 | tags: [str]
24 | pictures: [bytes]
25 |
26 | def __init__(self):
27 | self.tags = []
28 | self.pictures = []
29 |
30 |
31 | class BookDB:
32 | def __init__(self, large: bool = False):
33 | parent_path = os.path.dirname(os.path.dirname(__file__))
34 | self.db_s = os.path.join(parent_path, "data/book.db")
35 | self.db_l = os.path.join(parent_path, "data/book_lx.db")
36 | if large:
37 | self.book_db = self.db_l
38 | else:
39 | self.book_db = self.db_s
40 |
41 | def get_book_count(self):
42 | conn = sqlite.connect(self.book_db)
43 | cursor = conn.execute(
44 | "SELECT count(id) FROM book")
45 | row = cursor.fetchone()
46 | return row[0]
47 |
48 | def get_book_info(self, start, size) -> [Book]:
49 | books = []
50 | conn = sqlite.connect(self.book_db)
51 | cursor = conn.execute(
52 | "SELECT id, title, author, "
53 | "publisher, original_title, "
54 | "translator, pub_year, pages, "
55 | "price, currency_unit, binding, "
56 | "isbn, author_intro, book_intro, "
57 | "content, tags, picture FROM book ORDER BY id "
58 | "LIMIT ? OFFSET ?", (size, start))
59 | for row in cursor:
60 | book = Book()
61 | book.id = row[0]
62 | book.title = row[1]
63 | book.author = row[2]
64 | book.publisher = row[3]
65 | book.original_title = row[4]
66 | book.translator = row[5]
67 | book.pub_year = row[6]
68 | book.pages = row[7]
69 | book.price = row[8]
70 |
71 | book.currency_unit = row[9]
72 | book.binding = row[10]
73 | book.isbn = row[11]
74 | book.author_intro = row[12]
75 | book.book_intro = row[13]
76 | book.content = row[14]
77 | tags = row[15]
78 |
79 | picture = row[16]
80 |
81 | for tag in tags.split("\n"):
82 | if tag.strip() != "":
83 | book.tags.append(tag)
84 | for i in range(0, random.randint(0, 9)):
85 | if picture is not None:
86 | encode_str = base64.b64encode(picture).decode('utf-8')
87 | book.pictures.append(encode_str)
88 | books.append(book)
89 | # print(tags.decode('utf-8'))
90 |
91 | # print(book.tags, len(book.picture))
92 | # print(book)
93 | # print(tags)
94 |
95 | return books
96 |
97 |
98 |
--------------------------------------------------------------------------------
/fe/access/buyer.py:
--------------------------------------------------------------------------------
1 | import requests
2 | import simplejson
3 | from urllib.parse import urljoin
4 | from fe.access.auth import Auth
5 |
6 |
7 | class Buyer:
8 | def __init__(self, url_prefix, user_id, password):
9 | self.url_prefix = urljoin(url_prefix, "buyer/")
10 | self.user_id = user_id
11 | self.password = password
12 | self.token = ""
13 | self.terminal = "my terminal"
14 | self.auth = Auth(url_prefix)
15 | code, self.token = self.auth.login(self.user_id, self.password, self.terminal)
16 | assert code == 200
17 |
18 | def new_order(self, store_id: str, book_id_and_count: [(str, int)]) -> (int, str):
19 | books = []
20 | for id_count_pair in book_id_and_count:
21 | books.append({"id": id_count_pair[0], "count": id_count_pair[1]})
22 | json = {"user_id": self.user_id, "store_id": store_id, "books": books}
23 | #print(simplejson.dumps(json))
24 | url = urljoin(self.url_prefix, "new_order")
25 | headers = {"token": self.token}
26 | r = requests.post(url, headers=headers, json=json)
27 | response_json = r.json()
28 | return r.status_code, response_json.get("order_id")
29 |
30 | def payment(self, order_id: str):
31 | json = {"user_id": self.user_id, "password": self.password, "order_id": order_id}
32 | url = urljoin(self.url_prefix, "payment")
33 | headers = {"token": self.token}
34 | r = requests.post(url, headers=headers, json=json)
35 | return r.status_code
36 |
37 | def add_funds(self, add_value: str) -> int:
38 | json = {"user_id": self.user_id, "password": self.password, "add_value": add_value}
39 | url = urljoin(self.url_prefix, "add_funds")
40 | headers = {"token": self.token}
41 | r = requests.post(url, headers=headers, json=json)
42 | return r.status_code
43 |
--------------------------------------------------------------------------------
/fe/access/new_buyer.py:
--------------------------------------------------------------------------------
1 | from fe import conf
2 | from fe.access import buyer, auth
3 |
4 |
5 | def register_new_buyer(user_id, password) -> buyer.Buyer:
6 | a = auth.Auth(conf.URL)
7 | code = a.register(user_id, password)
8 | assert code == 200
9 | s = buyer.Buyer(conf.URL, user_id, password)
10 | return s
11 |
--------------------------------------------------------------------------------
/fe/access/new_seller.py:
--------------------------------------------------------------------------------
1 | from fe import conf
2 | from fe.access import seller, auth
3 |
4 |
5 | def register_new_seller(user_id, password) -> seller.Seller:
6 | a = auth.Auth(conf.URL)
7 | code = a.register(user_id, password)
8 | assert code == 200
9 | s = seller.Seller(conf.URL, user_id, password)
10 | return s
11 |
--------------------------------------------------------------------------------
/fe/access/seller.py:
--------------------------------------------------------------------------------
1 | import requests
2 | from urllib.parse import urljoin
3 | from fe.access import book
4 | from fe.access.auth import Auth
5 |
6 |
7 | class Seller:
8 | def __init__(self, url_prefix, seller_id: str, password: str):
9 | self.url_prefix = urljoin(url_prefix, "seller/")
10 | self.seller_id = seller_id
11 | self.password = password
12 | self.terminal = "my terminal"
13 | self.auth = Auth(url_prefix)
14 | code, self.token = self.auth.login(self.seller_id, self.password, self.terminal)
15 | assert code == 200
16 |
17 | def create_store(self, store_id):
18 | json = {
19 | "user_id": self.seller_id,
20 | "store_id": store_id,
21 | }
22 | #print(simplejson.dumps(json))
23 | url = urljoin(self.url_prefix, "create_store")
24 | headers = {"token": self.token}
25 | r = requests.post(url, headers=headers, json=json)
26 | return r.status_code
27 |
28 | def add_book(self, store_id: str, stock_level: int, book_info: book.Book) -> int:
29 | json = {
30 | "user_id": self.seller_id,
31 | "store_id": store_id,
32 | "book_info": book_info.__dict__,
33 | "stock_level": stock_level
34 | }
35 | #print(simplejson.dumps(json))
36 | url = urljoin(self.url_prefix, "add_book")
37 | headers = {"token": self.token}
38 | r = requests.post(url, headers=headers, json=json)
39 | return r.status_code
40 |
41 | def add_stock_level(self, seller_id: str, store_id: str, book_id: str, add_stock_num: int) -> int:
42 | json = {
43 | "user_id": seller_id,
44 | "store_id": store_id,
45 | "book_id": book_id,
46 | "add_stock_level": add_stock_num
47 | }
48 | #print(simplejson.dumps(json))
49 | url = urljoin(self.url_prefix, "add_stock_level")
50 | headers = {"token": self.token}
51 | r = requests.post(url, headers=headers, json=json)
52 | return r.status_code
53 |
--------------------------------------------------------------------------------
/fe/bench/__init__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
--------------------------------------------------------------------------------
/fe/bench/bench.md:
--------------------------------------------------------------------------------
1 | add performance test here
2 |
--------------------------------------------------------------------------------
/fe/bench/run.py:
--------------------------------------------------------------------------------
1 | from fe.bench.workload import Workload
2 | from fe.bench.session import Session
3 |
4 |
5 | def run_bench():
6 | wl = Workload()
7 | wl.gen_database()
8 |
9 | sessions = []
10 | for i in range(0, wl.session):
11 | ss = Session(wl)
12 | sessions.append(ss)
13 |
14 | for ss in sessions:
15 | ss.start()
16 |
17 | for ss in sessions:
18 | ss.join()
19 |
20 |
21 | #if __name__ == "__main__":
22 | # run_bench()
--------------------------------------------------------------------------------
/fe/bench/session.py:
--------------------------------------------------------------------------------
1 | from fe.bench.workload import Workload
2 | from fe.bench.workload import NewOrder
3 | from fe.bench.workload import Payment
4 | import time
5 | import threading
6 |
7 |
8 | class Session(threading.Thread):
9 | def __init__(self, wl: Workload):
10 | threading.Thread.__init__(self)
11 | self.workload = wl
12 | self.new_order_request = []
13 | self.payment_request = []
14 | self.payment_i = 0
15 | self.new_order_i = 0
16 | self.payment_ok = 0
17 | self.new_order_ok = 0
18 | self.time_new_order = 0
19 | self.time_payment = 0
20 | self.thread = None
21 | self.gen_procedure()
22 |
23 | def gen_procedure(self):
24 | for i in range(0, self.workload.procedure_per_session):
25 | new_order = self.workload.get_new_order()
26 | self.new_order_request.append(new_order)
27 |
28 | def run(self):
29 | self.run_gut()
30 |
31 | def run_gut(self):
32 | for new_order in self.new_order_request:
33 | before = time.time()
34 | ok, order_id = new_order.run()
35 | after = time.time()
36 | self.time_new_order = self.time_new_order + after - before
37 | self.new_order_i = self.new_order_i + 1
38 | if ok:
39 | self.new_order_ok = self.new_order_ok + 1
40 | payment = Payment(new_order.buyer, order_id)
41 | self.payment_request.append(payment)
42 | if self.new_order_i % 100 or self.new_order_i == len(self.new_order_request):
43 | self.workload.update_stat(self.new_order_i, self.payment_i, self.new_order_ok, self.payment_ok,
44 | self.time_new_order, self.time_payment)
45 | for payment in self.payment_request:
46 | before = time.time()
47 | ok = payment.run()
48 | after = time.time()
49 | self.time_payment = self.time_payment + after - before
50 | self.payment_i = self.payment_i + 1
51 | if ok:
52 | self.payment_ok = self.payment_ok + 1
53 | self.payment_request = []
54 |
--------------------------------------------------------------------------------
/fe/bench/workload.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import uuid
3 | import random
4 | import threading
5 | from fe.access import book
6 | from fe.access.new_seller import register_new_seller
7 | from fe.access.new_buyer import register_new_buyer
8 | from fe.access.buyer import Buyer
9 | from fe import conf
10 |
11 |
12 | class NewOrder:
13 | def __init__(self, buyer: Buyer, store_id, book_id_and_count):
14 | self.buyer = buyer
15 | self.store_id = store_id
16 | self.book_id_and_count = book_id_and_count
17 |
18 | def run(self) -> (bool, str):
19 | code, order_id = self.buyer.new_order(self.store_id, self.book_id_and_count)
20 | return code == 200, order_id
21 |
22 |
23 | class Payment:
24 | def __init__(self, buyer:Buyer, order_id):
25 | self.buyer = buyer
26 | self.order_id = order_id
27 |
28 | def run(self) -> bool:
29 | code = self.buyer.payment(self.order_id)
30 | return code == 200
31 |
32 |
33 | class Workload:
34 | def __init__(self):
35 | self.uuid = str(uuid.uuid1())
36 | self.book_ids = []
37 | self.buyer_ids = []
38 | self.store_ids = []
39 | self.book_db = book.BookDB(conf.Use_Large_DB)
40 | self.row_count = self.book_db.get_book_count()
41 |
42 | self.book_num_per_store = conf.Book_Num_Per_Store
43 | if self.row_count < self.book_num_per_store:
44 | self.book_num_per_store = self.row_count
45 | self.store_num_per_user = conf.Store_Num_Per_User
46 | self.seller_num = conf.Seller_Num
47 | self.buyer_num = conf.Buyer_Num
48 | self.session = conf.Session
49 | self.stock_level = conf.Default_Stock_Level
50 | self.user_funds = conf.Default_User_Funds
51 | self.batch_size = conf.Data_Batch_Size
52 | self.procedure_per_session = conf.Request_Per_Session
53 |
54 | self.n_new_order = 0
55 | self.n_payment = 0
56 | self.n_new_order_ok = 0
57 | self.n_payment_ok = 0
58 | self.time_new_order = 0
59 | self.time_payment = 0
60 | self.lock = threading.Lock()
61 | # 存储上一次的值,用于两次做差
62 | self.n_new_order_past = 0
63 | self.n_payment_past = 0
64 | self.n_new_order_ok_past = 0
65 | self.n_payment_ok_past = 0
66 |
67 | def to_seller_id_and_password(self, no: int) -> (str, str):
68 | return "seller_{}_{}".format(no, self.uuid), "password_seller_{}_{}".format(no, self.uuid)
69 |
70 | def to_buyer_id_and_password(self, no: int) -> (str, str):
71 | return "buyer_{}_{}".format(no, self.uuid), "buyer_seller_{}_{}".format(no, self.uuid)
72 |
73 | def to_store_id(self, seller_no: int, i):
74 | return "store_s_{}_{}_{}".format(seller_no, i, self.uuid)
75 |
76 | def gen_database(self):
77 | logging.info("load data")
78 | for i in range(1, self.seller_num + 1):
79 | user_id, password = self.to_seller_id_and_password(i)
80 | seller = register_new_seller(user_id, password)
81 | for j in range(1, self.store_num_per_user + 1):
82 | store_id = self.to_store_id(i, j)
83 | code = seller.create_store(store_id)
84 | assert code == 200
85 | self.store_ids.append(store_id)
86 | row_no = 0
87 |
88 | while row_no < self.book_num_per_store:
89 | books = self.book_db.get_book_info(row_no, self.batch_size)
90 | if len(books) == 0:
91 | break
92 | for bk in books:
93 | code = seller.add_book(store_id, self.stock_level, bk)
94 | assert code == 200
95 | if i == 1 and j == 1:
96 | self.book_ids.append(bk.id)
97 | row_no = row_no + len(books)
98 | logging.info("seller data loaded.")
99 | for k in range(1, self.buyer_num + 1):
100 | user_id, password = self.to_buyer_id_and_password(k)
101 | buyer = register_new_buyer(user_id, password)
102 | buyer.add_funds(self.user_funds)
103 | self.buyer_ids.append(user_id)
104 | logging.info("buyer data loaded.")
105 |
106 | def get_new_order(self) -> NewOrder:
107 | n = random.randint(1, self.buyer_num)
108 | buyer_id, buyer_password = self.to_buyer_id_and_password(n)
109 | store_no = int(random.uniform(0, len(self.store_ids) - 1))
110 | store_id = self.store_ids[store_no]
111 | books = random.randint(1, 10)
112 | book_id_and_count = []
113 | book_temp = []
114 | for i in range(0, books):
115 | book_no = int(random.uniform(0, len(self.book_ids) - 1))
116 | book_id = self.book_ids[book_no]
117 | if book_id in book_temp:
118 | continue
119 | else:
120 | book_temp.append(book_id)
121 | count = random.randint(1, 10)
122 | book_id_and_count.append((book_id, count))
123 | b = Buyer(url_prefix=conf.URL, user_id=buyer_id, password=buyer_password)
124 | new_ord = NewOrder(b, store_id, book_id_and_count)
125 | return new_ord
126 |
127 | def update_stat(self, n_new_order, n_payment,
128 | n_new_order_ok, n_payment_ok,
129 | time_new_order, time_payment):
130 | # 获取当前并发数
131 | thread_num = len(threading.enumerate())
132 | # 加索
133 | self.lock.acquire()
134 | self.n_new_order = self.n_new_order + n_new_order
135 | self.n_payment = self.n_payment + n_payment
136 | self.n_new_order_ok = self.n_new_order_ok + n_new_order_ok
137 | self.n_payment_ok = self.n_payment_ok + n_payment_ok
138 | self.time_new_order = self.time_new_order + time_new_order
139 | self.time_payment = self.time_payment + time_payment
140 | # 计算这段时间内新创建订单的总数目
141 | n_new_order_diff = self.n_new_order - self.n_new_order_past
142 | # 计算这段时间内新付款订单的总数目
143 | n_payment_diff = self.n_payment - self.n_payment_past
144 |
145 | if self.n_payment != 0 and self. n_new_order != 0 \
146 | and (self.time_payment + self.time_new_order):
147 | # TPS_C(吞吐量):成功创建订单数量/(提交订单时间/提交订单并发数 + 提交付款订单时间/提交付款订单并发数)
148 | # NO=OK:新创建订单数量
149 | # Thread_num:以新提交订单的数量作为并发数(这一次的TOTAL-上一次的TOTAL)
150 | # TOTAL:总提交订单数量
151 | # LATENCY:提交订单时间/处理订单笔数(只考虑该线程延迟,未考虑并发)
152 | # P=OK:新创建付款订单数量
153 | # Thread_num:以新提交付款订单的数量作为并发数(这一次的TOTAL-上一次的TOTAL)
154 | # TOTAL:总付款提交订单数量
155 | # LATENCY:提交付款订单时间/处理付款订单笔数(只考虑该线程延迟,未考虑并发)
156 | logging.info("TPS_C={}, NO=OK:{} Thread_num:{} TOTAL:{} LATENCY:{} , P=OK:{} Thread_num:{} TOTAL:{} LATENCY:{}".format(
157 | int(self.n_new_order_ok / (self.time_payment / n_payment_diff + self.time_new_order / n_new_order_diff)), # 吞吐量:完成订单数/((付款所用时间+订单所用时间)/并发数)
158 | self.n_new_order_ok, n_new_order_diff, self.n_new_order, self.time_new_order / self.n_new_order, # 订单延迟:(创建订单所用时间/并发数)/新创建订单数
159 | self.n_payment_ok, n_payment_diff, self.n_payment, self.time_payment / self.n_payment # 付款延迟:(付款所用时间/并发数)/付款订单数
160 | ))
161 | self.lock.release()
162 | # 旧值更新为新值,便于下一轮计算
163 | self.n_new_order_past = self.n_new_order
164 | self.n_payment_past = self.n_payment
165 | self.n_new_order_ok_past = self.n_new_order_ok
166 | self.n_payment_ok_past = self.n_payment_ok
167 |
--------------------------------------------------------------------------------
/fe/conf.py:
--------------------------------------------------------------------------------
1 | URL = "http://127.0.0.1:5000/"
2 | Book_Num_Per_Store = 2000
3 | Store_Num_Per_User = 2
4 | Seller_Num = 2
5 | Buyer_Num = 10
6 | Session = 1
7 | Request_Per_Session = 1000
8 | Default_Stock_Level = 1000000
9 | Default_User_Funds = 10000000
10 | Data_Batch_Size = 100
11 | Use_Large_DB = False
12 |
--------------------------------------------------------------------------------
/fe/conftest.py:
--------------------------------------------------------------------------------
1 | import requests
2 | import threading
3 | from urllib.parse import urljoin
4 | from be import serve
5 | from fe import conf
6 |
7 | thread: threading.Thread = None
8 |
9 |
10 | # 修改这里启动后端程序,如果不需要可删除这行代码
11 | def run_backend():
12 | # rewrite this if rewrite backend
13 | serve.be_run()
14 |
15 |
16 | def pytest_configure(config):
17 | global thread
18 | print("frontend begin test")
19 | thread = threading.Thread(target=run_backend)
20 | thread.start()
21 |
22 |
23 | def pytest_unconfigure(config):
24 | url = urljoin(conf.URL, "shutdown")
25 | requests.get(url)
26 | thread.join()
27 | print("frontend end test")
28 |
--------------------------------------------------------------------------------
/fe/data/book.db:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dasedb/bookstore/c86af02f7bb533cae7c7b069bc461225b23f48f9/fe/data/book.db
--------------------------------------------------------------------------------
/fe/data/scraper.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 |
3 | from lxml import etree
4 | import sqlite3
5 | import re
6 | import requests
7 | import random
8 | import time
9 | import logging
10 |
11 | user_agent = [
12 | "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; en-us) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 "
13 | "Safari/534.50",
14 | "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-us) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 "
15 | "Safari/534.50",
16 | "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:38.0) Gecko/20100101 Firefox/38.0",
17 | "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; .NET4.0C; .NET4.0E; .NET CLR 2.0.50727; .NET CLR "
18 | "3.0.30729; .NET CLR 3.5.30729; InfoPath.3; rv:11.0) like Gecko",
19 | "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)",
20 | "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0)",
21 | "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0)",
22 | "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)",
23 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:2.0.1) Gecko/20100101 Firefox/4.0.1",
24 | "Mozilla/5.0 (Windows NT 6.1; rv:2.0.1) Gecko/20100101 Firefox/4.0.1",
25 | "Opera/9.80 (Macintosh; Intel Mac OS X 10.6.8; U; en) Presto/2.8.131 Version/11.11",
26 | "Opera/9.80 (Windows NT 6.1; U; en) Presto/2.8.131 Version/11.11",
27 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_0) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 "
28 | "Safari/535.11",
29 | "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Maxthon 2.0)",
30 | "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; TencentTraveler 4.0)",
31 | "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)",
32 | "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; The World)",
33 | "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Trident/4.0; SE 2.X MetaSr 1.0; SE 2.X MetaSr 1.0; .NET "
34 | "CLR 2.0.50727; SE 2.X MetaSr 1.0)",
35 | "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; 360SE)",
36 | "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Avant Browser)",
37 | "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)",
38 | "Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_3_3 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) "
39 | "Version/5.0.2 Mobile/8J2 Safari/6533.18.5",
40 | "Mozilla/5.0 (iPod; U; CPU iPhone OS 4_3_3 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) "
41 | "Version/5.0.2 Mobile/8J2 Safari/6533.18.5",
42 | "Mozilla/5.0 (iPad; U; CPU OS 4_3_3 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) "
43 | "Version/5.0.2 Mobile/8J2 Safari/6533.18.5",
44 | "Mozilla/5.0 (Linux; U; Android 2.3.7; en-us; Nexus One Build/FRF91) AppleWebKit/533.1 (KHTML, like Gecko) "
45 | "Version/4.0 Mobile Safari/533.1",
46 | "MQQBrowser/26 Mozilla/5.0 (Linux; U; Android 2.3.7; zh-cn; MB200 Build/GRJ22; CyanogenMod-7) "
47 | "AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
48 | "Opera/9.80 (Android 2.3.4; Linux; Opera Mobi/build-1107180945; U; en-GB) Presto/2.8.149 Version/11.10",
49 | "Mozilla/5.0 (Linux; U; Android 3.0; en-us; Xoom Build/HRI39) AppleWebKit/534.13 (KHTML, like Gecko) "
50 | "Version/4.0 Safari/534.13",
51 | "Mozilla/5.0 (BlackBerry; U; BlackBerry 9800; en) AppleWebKit/534.1+ (KHTML, like Gecko) Version/6.0.0.337 "
52 | "Mobile Safari/534.1+",
53 | "Mozilla/5.0 (hp-tablet; Linux; hpwOS/3.0.0; U; en-US) AppleWebKit/534.6 (KHTML, like Gecko) "
54 | "wOSBrowser/233.70 Safari/534.6 TouchPad/1.0",
55 | "Mozilla/5.0 (SymbianOS/9.4; Series60/5.0 NokiaN97-1/20.0.019; Profile/MIDP-2.1 Configuration/CLDC-1.1) "
56 | "AppleWebKit/525 (KHTML, like Gecko) BrowserNG/7.1.18124",
57 | "Mozilla/5.0 (compatible; MSIE 9.0; Windows Phone OS 7.5; Trident/5.0; IEMobile/9.0; HTC; Titan)",
58 | "UCWEB7.0.2.37/28/999",
59 | "NOKIA5700/ UCWEB7.0.2.37/28/999",
60 | "Openwave/ UCWEB7.0.2.37/28/999",
61 | "Mozilla/4.0 (compatible; MSIE 6.0; ) Opera/UCWEB7.0.2.37/28/999",
62 | # iPhone 6:
63 | "Mozilla/6.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/8.0 "
64 | "Mobile/10A5376e Safari/8536.25",
65 | ]
66 |
67 |
68 | def get_user_agent():
69 | headers = {"User-Agent": random.choice(user_agent)}
70 | return headers
71 |
72 |
73 | class Scraper:
74 | database: str
75 | tag: str
76 | page: int
77 |
78 | def __init__(self):
79 | self.database = "book.db"
80 | self.tag = ""
81 | self.page = 0
82 | self.pattern_number = re.compile(r"\d+\.?\d*")
83 | logging.basicConfig(filename="scraper.log", level=logging.ERROR)
84 |
85 | def get_current_progress(self) -> ():
86 | conn = sqlite3.connect(self.database)
87 | results = conn.execute("SELECT tag, page from progress where id = '0'")
88 | for row in results:
89 | return row[0], row[1]
90 | return "", 0
91 |
92 | def save_current_progress(self, current_tag, current_page):
93 | conn = sqlite3.connect(self.database)
94 | conn.execute(
95 | "UPDATE progress set tag = '{}', page = {} where id = '0'".format(
96 | current_tag, current_page
97 | )
98 | )
99 | conn.commit()
100 | conn.close()
101 |
102 | def start_grab(self) -> bool:
103 | self.create_tables()
104 | scraper.grab_tag()
105 | current_tag, current_page = self.get_current_progress()
106 | tags = self.get_tag_list()
107 | for i in range(0, len(tags)):
108 | no = 0
109 | if i == 0 and current_tag == tags[i]:
110 | no = current_page
111 | while self.grab_book_list(tags[i], no):
112 | no = no + 20
113 | return True
114 |
115 | def create_tables(self):
116 | conn = sqlite3.connect(self.database)
117 | try:
118 | conn.execute("CREATE TABLE tags (tag TEXT PRIMARY KEY)")
119 | conn.commit()
120 | except sqlite3.Error as e:
121 | logging.error(str(e))
122 | conn.rollback()
123 |
124 | try:
125 | conn.execute(
126 | "CREATE TABLE book ("
127 | "id TEXT PRIMARY KEY, title TEXT, author TEXT, "
128 | "publisher TEXT, original_title TEXT, "
129 | "translator TEXT, pub_year TEXT, pages INTEGER, "
130 | "price INTEGER, currency_unit TEXT, binding TEXT, "
131 | "isbn TEXT, author_intro TEXT, book_intro text, "
132 | "content TEXT, tags TEXT, picture BLOB)"
133 | )
134 | conn.commit()
135 | except sqlite3.Error as e:
136 | logging.error(str(e))
137 | conn.rollback()
138 |
139 | try:
140 | conn.execute(
141 | "CREATE TABLE progress (id TEXT PRIMARY KEY, tag TEXT, page integer )"
142 | )
143 | conn.execute("INSERT INTO progress values('0', '', 0)")
144 | conn.commit()
145 | except sqlite3.Error as e:
146 | logging.error(str(e))
147 | conn.rollback()
148 |
149 | def grab_tag(self):
150 | url = "https://book.douban.com/tag/?view=cloud"
151 | r = requests.get(url, headers=get_user_agent())
152 | r.encoding = "utf-8"
153 | h: etree.ElementBase = etree.HTML(r.text)
154 | tags: [] = h.xpath(
155 | '/html/body/div[@id="wrapper"]/div[@id="content"]'
156 | '/div[@class="grid-16-8 clearfix"]/div[@class="article"]'
157 | '/div[@class=""]/div[@class="indent tag_cloud"]'
158 | "/table/tbody/tr/td/a/@href"
159 | )
160 | conn = sqlite3.connect(self.database)
161 | c = conn.cursor()
162 | try:
163 | for tag in tags:
164 | t: str = tag.strip("/tag")
165 | c.execute("INSERT INTO tags VALUES ('{}')".format(t))
166 | c.close()
167 | conn.commit()
168 | conn.close()
169 | except sqlite3.Error as e:
170 | logging.error(str(e))
171 | conn.rollback()
172 | return False
173 | return True
174 |
175 | def grab_book_list(self, tag="小说", pageno=1) -> bool:
176 | logging.info("start to grab tag {} page {}...".format(tag, pageno))
177 | self.save_current_progress(tag, pageno)
178 | url = "https://book.douban.com/tag/{}?start={}&type=T".format(tag, pageno)
179 | r = requests.get(url, headers=get_user_agent())
180 | r.encoding = "utf-8"
181 | h: etree.Element = etree.HTML(r.text)
182 |
183 | li_list: [] = h.xpath(
184 | '/html/body/div[@id="wrapper"]/div[@id="content"]'
185 | '/div[@class="grid-16-8 clearfix"]'
186 | '/div[@class="article"]/div[@id="subject_list"]'
187 | '/ul/li/div[@class="info"]/h2/a/@href'
188 | )
189 | next_page = h.xpath(
190 | '/html/body/div[@id="wrapper"]/div[@id="content"]'
191 | '/div[@class="grid-16-8 clearfix"]'
192 | '/div[@class="article"]/div[@id="subject_list"]'
193 | '/div[@class="paginator"]/span[@class="next"]/a[@href]'
194 | )
195 | has_next = True
196 | if len(next_page) == 0:
197 | has_next = False
198 | if len(li_list) == 0:
199 | return False
200 |
201 | for li in li_list:
202 | li.strip("")
203 | book_id = li.strip("/").split("/")[-1]
204 | try:
205 | delay = float(random.randint(0, 200)) / 100.0
206 | time.sleep(delay)
207 | self.crow_book_info(book_id)
208 | except BaseException as e:
209 | logging.error(
210 | logging.error("error when scrape {}, {}".format(book_id, str(e)))
211 | )
212 | return has_next
213 |
214 | def get_tag_list(self) -> [str]:
215 | ret = []
216 | conn = sqlite3.connect(self.database)
217 | results = conn.execute(
218 | "SELECT tags.tag from tags join progress where tags.tag >= progress.tag"
219 | )
220 | for row in results:
221 | ret.append(row[0])
222 | return ret
223 |
224 | def crow_book_info(self, book_id) -> bool:
225 | conn = sqlite3.connect(self.database)
226 | for _ in conn.execute("SELECT id from book where id = ('{}')".format(book_id)):
227 | return
228 |
229 | url = "https://book.douban.com/subject/{}/".format(book_id)
230 | r = requests.get(url, headers=get_user_agent())
231 | r.encoding = "utf-8"
232 | h: etree.Element = etree.HTML(r.text)
233 | e_text = h.xpath('/html/body/div[@id="wrapper"]/h1/span/text()')
234 | if len(e_text) == 0:
235 | return False
236 |
237 | title = e_text[0]
238 |
239 | elements = h.xpath(
240 | '/html/body/div[@id="wrapper"]'
241 | '/div[@id="content"]/div[@class="grid-16-8 clearfix"]'
242 | '/div[@class="article"]'
243 | )
244 | if len(elements) == 0:
245 | return False
246 |
247 | e_article = elements[0]
248 |
249 | book_intro = ""
250 | author_intro = ""
251 | content = ""
252 | tags = ""
253 |
254 | e_book_intro = e_article.xpath(
255 | 'div[@class="related_info"]'
256 | '/div[@class="indent"][@id="link-report"]/*'
257 | '/div[@class="intro"]/*/text()'
258 | )
259 | for line in e_book_intro:
260 | line = line.strip()
261 | if line != "":
262 | book_intro = book_intro + line + "\n"
263 |
264 | e_author_intro = e_article.xpath(
265 | 'div[@class="related_info"]'
266 | '/div[@class="indent "]/*'
267 | '/div[@class="intro"]/*/text()'
268 | )
269 | for line in e_author_intro:
270 | line = line.strip()
271 | if line != "":
272 | author_intro = author_intro + line + "\n"
273 |
274 | e_content = e_article.xpath(
275 | 'div[@class="related_info"]'
276 | '/div[@class="indent"][@id="dir_' + book_id + '_full"]/text()'
277 | )
278 | for line in e_content:
279 | line = line.strip()
280 | if line != "":
281 | content = content + line + "\n"
282 |
283 | e_tags = e_article.xpath(
284 | 'div[@class="related_info"]/'
285 | 'div[@id="db-tags-section"]/'
286 | 'div[@class="indent"]/span/a/text()'
287 | )
288 | for line in e_tags:
289 | line = line.strip()
290 | if line != "":
291 | tags = tags + line + "\n"
292 |
293 | e_subject = e_article.xpath(
294 | 'div[@class="indent"]'
295 | '/div[@class="subjectwrap clearfix"]'
296 | '/div[@class="subject clearfix"]'
297 | )
298 | pic_href = e_subject[0].xpath('div[@id="mainpic"]/a/@href')
299 | picture = None
300 | if len(pic_href) > 0:
301 | res = requests.get(pic_href[0], headers=get_user_agent())
302 | picture = res.content
303 |
304 | info_children = e_subject[0].xpath('div[@id="info"]/child::node()')
305 |
306 | e_array = []
307 | e_dict = dict()
308 |
309 | for e in info_children:
310 | if isinstance(e, etree._ElementUnicodeResult):
311 | e_dict["text"] = e
312 | elif isinstance(e, etree._Element):
313 | if e.tag == "br":
314 | e_array.append(e_dict)
315 | e_dict = dict()
316 | else:
317 | e_dict[e.tag] = e
318 |
319 | book_info = dict()
320 | for d in e_array:
321 | label = ""
322 | span = d.get("span")
323 | a_label = span.xpath("span/text()")
324 | if len(a_label) > 0 and label == "":
325 | label = a_label[0].strip()
326 | a_label = span.xpath("text()")
327 | if len(a_label) > 0 and label == "":
328 | label = a_label[0].strip()
329 | label = label.strip(":")
330 | text = d.get("text").strip()
331 | e_a = d.get("a")
332 | text.strip()
333 | text.strip(":")
334 | if label == "作者" or label == "译者":
335 | a = span.xpath("a/text()")
336 | if text == "" and len(a) == 1:
337 | text = a[0].strip()
338 | if text == "" and e_a is not None:
339 | text_a = e_a.xpath("text()")
340 | if len(text_a) > 0:
341 | text = text_a[0].strip()
342 | text = re.sub(r"\s+", " ", text)
343 | if text != "":
344 | book_info[label] = text
345 |
346 | sql = (
347 | "INSERT INTO book("
348 | "id, title, author, "
349 | "publisher, original_title, translator, "
350 | "pub_year, pages, price, "
351 | "currency_unit, binding, isbn, "
352 | "author_intro, book_intro, content, "
353 | "tags, picture)"
354 | "VALUES("
355 | "?, ?, ?, "
356 | "?, ?, ?, "
357 | "?, ?, ?, "
358 | "?, ?, ?, "
359 | "?, ?, ?, "
360 | "?, ?)"
361 | )
362 |
363 | unit = None
364 | price = None
365 | pages = None
366 | conn = sqlite3.connect(self.database)
367 | try:
368 | s_price = book_info.get("定价")
369 | if s_price is None:
370 | # price cannot be NULL
371 | logging.error(
372 | "error when scrape book_id {}, cannot retrieve price...", book_id
373 | )
374 | return None
375 | else:
376 | e = re.findall(self.pattern_number, s_price)
377 | if len(e) != 0:
378 | number = e[0]
379 | unit = s_price.replace(number, "").strip()
380 | price = int(float(number) * 100)
381 |
382 | s_pages = book_info.get("页数")
383 | if s_pages is not None:
384 | # pages can be NULL
385 | e = re.findall(self.pattern_number, s_pages)
386 | if len(e) != 0:
387 | pages = int(e[0])
388 |
389 | conn.execute(
390 | sql,
391 | (
392 | book_id,
393 | title,
394 | book_info.get("作者"),
395 | book_info.get("出版社"),
396 | book_info.get("原作名"),
397 | book_info.get("译者"),
398 | book_info.get("出版年"),
399 | pages,
400 | price,
401 | unit,
402 | book_info.get("装帧"),
403 | book_info.get("ISBN"),
404 | author_intro,
405 | book_intro,
406 | content,
407 | tags,
408 | picture,
409 | ),
410 | )
411 | conn.commit()
412 | except sqlite3.Error as e:
413 | logging(str(e))
414 | conn.rollback()
415 | except TypeError as e:
416 | logging.error("error when scrape {}, {}".format(book_id, str(e)))
417 | conn.rollback()
418 | return False
419 | conn.close()
420 | return True
421 |
422 |
423 | if __name__ == "__main__":
424 | scraper = Scraper()
425 | scraper.start_grab()
426 |
--------------------------------------------------------------------------------
/fe/test/gen_book_data.py:
--------------------------------------------------------------------------------
1 | import random
2 | from fe.access import book
3 | from fe.access.new_seller import register_new_seller
4 |
5 |
6 | class GenBook:
7 | def __init__(self, user_id, store_id):
8 | self.user_id = user_id
9 | self.store_id = store_id
10 | self.password = self.user_id
11 | self.seller = register_new_seller(self.user_id, self.password)
12 | code = self.seller.create_store(store_id)
13 | assert code == 200
14 | self.__init_book_list__()
15 |
16 | def __init_book_list__(self):
17 | self.buy_book_info_list = []
18 | self.buy_book_id_list = []
19 |
20 | def gen(self, non_exist_book_id: bool, low_stock_level, max_book_count: int = 100) -> (bool, []):
21 | self.__init_book_list__()
22 | ok = True
23 | book_db = book.BookDB()
24 | rows = book_db.get_book_count()
25 | start = 0
26 | if rows > max_book_count:
27 | start = random.randint(0, rows - max_book_count)
28 | size = random.randint(1, max_book_count)
29 | books = book_db.get_book_info(start, size)
30 | book_id_exist = []
31 | book_id_stock_level = {}
32 | for bk in books:
33 | if low_stock_level:
34 | stock_level = random.randint(0, 100)
35 | else:
36 | stock_level = random.randint(2, 100)
37 | code = self.seller.add_book(self.store_id, stock_level, bk)
38 | assert code == 200
39 | book_id_stock_level[bk.id] = stock_level
40 | book_id_exist.append(bk)
41 |
42 | for bk in book_id_exist:
43 | stock_level = book_id_stock_level[bk.id]
44 | if stock_level > 1:
45 | buy_num = random.randint(1, stock_level)
46 | else:
47 | buy_num = 0
48 | # add a new pair
49 | if non_exist_book_id:
50 | bk.id = bk.id + "_x"
51 | if low_stock_level:
52 | buy_num = stock_level + 1
53 | self.buy_book_info_list.append((bk, buy_num))
54 |
55 | for item in self.buy_book_info_list:
56 | self.buy_book_id_list.append((item[0].id, item[1]))
57 | return ok, self.buy_book_id_list
58 |
--------------------------------------------------------------------------------
/fe/test/test.md:
--------------------------------------------------------------------------------
1 | add functionality test here
2 |
--------------------------------------------------------------------------------
/fe/test/test_add_book.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from fe.access.new_seller import register_new_seller
4 | from fe.access import book
5 | import uuid
6 |
7 |
8 | class TestAddBook:
9 | @pytest.fixture(autouse=True)
10 | def pre_run_initialization(self):
11 | # do before test
12 | self.seller_id = "test_add_books_seller_id_{}".format(str(uuid.uuid1()))
13 | self.store_id = "test_add_books_store_id_{}".format(str(uuid.uuid1()))
14 | self.password = self.seller_id
15 | self.seller = register_new_seller(self.seller_id, self.password)
16 |
17 | code = self.seller.create_store(self.store_id)
18 | assert code == 200
19 | book_db = book.BookDB()
20 | self.books = book_db.get_book_info(0, 2)
21 |
22 | yield
23 | # do after test
24 |
25 | def test_ok(self):
26 | for b in self.books:
27 | code = self.seller.add_book(self.store_id, 0, b)
28 | assert code == 200
29 |
30 | def test_error_non_exist_store_id(self):
31 | for b in self.books:
32 | # non exist store id
33 | code = self.seller.add_book(self.store_id + "x", 0, b)
34 | assert code != 200
35 |
36 | def test_error_exist_book_id(self):
37 | for b in self.books:
38 | code = self.seller.add_book(self.store_id, 0, b)
39 | assert code == 200
40 | for b in self.books:
41 | # exist book id
42 | code = self.seller.add_book(self.store_id, 0, b)
43 | assert code != 200
44 |
45 | def test_error_non_exist_user_id(self):
46 | for b in self.books:
47 | # non exist user id
48 | self.seller.seller_id = self.seller.seller_id + "_x"
49 | code = self.seller.add_book(self.store_id, 0, b)
50 | assert code != 200
51 |
52 |
--------------------------------------------------------------------------------
/fe/test/test_add_funds.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | import uuid
3 | from fe.access.new_buyer import register_new_buyer
4 |
5 |
6 | class TestAddFunds:
7 | @pytest.fixture(autouse=True)
8 | def pre_run_initialization(self):
9 | self.user_id = "test_add_funds_{}".format(str(uuid.uuid1()))
10 | self.password = self.user_id
11 | self.buyer = register_new_buyer(self.user_id, self.password)
12 | yield
13 |
14 | def test_ok(self):
15 | code = self.buyer.add_funds(1000)
16 | assert code == 200
17 |
18 | code = self.buyer.add_funds(-1000)
19 | assert code == 200
20 |
21 | def test_error_user_id(self):
22 | self.buyer.user_id = self.buyer.user_id + "_x"
23 | code = self.buyer.add_funds(10)
24 | assert code != 200
25 |
26 | def test_error_password(self):
27 | self.buyer.password = self.buyer.password + "_x"
28 | code = self.buyer.add_funds(10)
29 | assert code != 200
30 |
--------------------------------------------------------------------------------
/fe/test/test_add_stock_level.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from fe.access.new_seller import register_new_seller
3 | from fe.access import book
4 | import uuid
5 |
6 |
7 | class TestAddStockLevel:
8 | @pytest.fixture(autouse=True)
9 | def pre_run_initialization(self):
10 | self.user_id = "test_add_book_stock_level1_user_{}".format(str(uuid.uuid1()))
11 | self.store_id = "test_add_book_stock_level1_store_{}".format(str(uuid.uuid1()))
12 | self.password = self.user_id
13 | self.seller = register_new_seller(self.user_id, self.password)
14 |
15 | code = self.seller.create_store(self.store_id)
16 | assert code == 200
17 | book_db = book.BookDB()
18 | self.books = book_db.get_book_info(0, 5)
19 | for bk in self.books:
20 | code = self.seller.add_book(self.store_id, 0, bk)
21 | assert code == 200
22 | yield
23 |
24 | def test_error_user_id(self):
25 | for b in self.books:
26 | book_id = b.id
27 | code = self.seller.add_stock_level(self.user_id + "_x", self.store_id, book_id, 10)
28 | assert code != 200
29 |
30 | def test_error_store_id(self):
31 | for b in self.books:
32 | book_id = b.id
33 | code = self.seller.add_stock_level(self.user_id, self.store_id + "_x", book_id, 10)
34 | assert code != 200
35 |
36 | def test_error_book_id(self):
37 | for b in self.books:
38 | book_id = b.id
39 | code = self.seller.add_stock_level(self.user_id, self.store_id, book_id + "_x", 10)
40 | assert code != 200
41 |
42 | def test_ok(self):
43 | for b in self.books:
44 | book_id = b.id
45 | code = self.seller.add_stock_level(self.user_id, self.store_id, book_id, 10)
46 | assert code == 200
47 |
--------------------------------------------------------------------------------
/fe/test/test_bench.py:
--------------------------------------------------------------------------------
1 | from fe.bench.run import run_bench
2 |
3 |
4 | def test_bench():
5 | try:
6 | run_bench()
7 | except Exception as e:
8 | assert 200==100,"test_bench过程出现异常"
--------------------------------------------------------------------------------
/fe/test/test_create_store.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from fe.access.new_seller import register_new_seller
4 | import uuid
5 |
6 |
7 | class TestCreateStore:
8 | @pytest.fixture(autouse=True)
9 | def pre_run_initialization(self):
10 | self.user_id = "test_create_store_user_{}".format(str(uuid.uuid1()))
11 | self.store_id = "test_create_store_store_{}".format(str(uuid.uuid1()))
12 | self.password = self.user_id
13 | yield
14 |
15 | def test_ok(self):
16 | self.seller = register_new_seller(self.user_id, self.password)
17 | code = self.seller.create_store(self.store_id)
18 | assert code == 200
19 |
20 | def test_error_exist_store_id(self):
21 | self.seller = register_new_seller(self.user_id, self.password)
22 | code = self.seller.create_store(self.store_id)
23 | assert code == 200
24 |
25 | code = self.seller.create_store(self.store_id)
26 | assert code != 200
27 |
--------------------------------------------------------------------------------
/fe/test/test_login.py:
--------------------------------------------------------------------------------
1 | import time
2 |
3 | import pytest
4 |
5 | from fe.access import auth
6 | from fe import conf
7 |
8 |
9 | class TestLogin:
10 | @pytest.fixture(autouse=True)
11 | def pre_run_initialization(self):
12 | self.auth = auth.Auth(conf.URL)
13 | # register a user
14 | self.user_id = "test_login_{}".format(time.time())
15 | self.password = "password_" + self.user_id
16 | self.terminal = "terminal_" + self.user_id
17 | assert self.auth.register(self.user_id, self.password) == 200
18 | yield
19 |
20 | def test_ok(self):
21 | code, token = self.auth.login(self.user_id, self.password, self.terminal)
22 | assert code == 200
23 |
24 | code = self.auth.logout(self.user_id + "_x", token)
25 | assert code == 401
26 |
27 | code = self.auth.logout(self.user_id, token + "_x")
28 | assert code == 401
29 |
30 | code = self.auth.logout(self.user_id, token)
31 | assert code == 200
32 |
33 | def test_error_user_id(self):
34 | code, token = self.auth.login(self.user_id + "_x", self.password, self.terminal)
35 | assert code == 401
36 |
37 | def test_error_password(self):
38 | code, token = self.auth.login(self.user_id, self.password + "_x", self.terminal)
39 | assert code == 401
40 |
--------------------------------------------------------------------------------
/fe/test/test_new_order.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from fe.test.gen_book_data import GenBook
4 | from fe.access.new_buyer import register_new_buyer
5 | import uuid
6 |
7 |
8 | class TestNewOrder:
9 | @pytest.fixture(autouse=True)
10 | def pre_run_initialization(self):
11 | self.seller_id = "test_new_order_seller_id_{}".format(str(uuid.uuid1()))
12 | self.store_id = "test_new_order_store_id_{}".format(str(uuid.uuid1()))
13 | self.buyer_id = "test_new_order_buyer_id_{}".format(str(uuid.uuid1()))
14 | self.password = self.seller_id
15 | self.buyer = register_new_buyer(self.buyer_id, self.password)
16 | self.gen_book = GenBook(self.seller_id, self.store_id)
17 | yield
18 |
19 | def test_non_exist_book_id(self):
20 | ok, buy_book_id_list = self.gen_book.gen(non_exist_book_id=True, low_stock_level=False)
21 | assert ok
22 | code, _ = self.buyer.new_order(self.store_id, buy_book_id_list)
23 | assert code != 200
24 |
25 | def test_low_stock_level(self):
26 | ok, buy_book_id_list = self.gen_book.gen(non_exist_book_id=False, low_stock_level=True)
27 | assert ok
28 | code, _ = self.buyer.new_order(self.store_id, buy_book_id_list)
29 | assert code != 200
30 |
31 | def test_ok(self):
32 | ok, buy_book_id_list = self.gen_book.gen(non_exist_book_id=False, low_stock_level=False)
33 | assert ok
34 | code, _ = self.buyer.new_order(self.store_id, buy_book_id_list)
35 | assert code == 200
36 |
37 | def test_non_exist_user_id(self):
38 | ok, buy_book_id_list = self.gen_book.gen(non_exist_book_id=False, low_stock_level=False)
39 | assert ok
40 | self.buyer.user_id = self.buyer.user_id + "_x"
41 | code, _ = self.buyer.new_order(self.store_id, buy_book_id_list)
42 | assert code != 200
43 |
44 | def test_non_exist_store_id(self):
45 | ok, buy_book_id_list = self.gen_book.gen(non_exist_book_id=False, low_stock_level=False)
46 | assert ok
47 | code, _ = self.buyer.new_order(self.store_id + "_x", buy_book_id_list)
48 | assert code != 200
49 |
--------------------------------------------------------------------------------
/fe/test/test_password.py:
--------------------------------------------------------------------------------
1 | import uuid
2 |
3 | import pytest
4 |
5 | from fe.access import auth
6 | from fe import conf
7 |
8 |
9 | class TestPassword:
10 | @pytest.fixture(autouse=True)
11 | def pre_run_initialization(self):
12 | self.auth = auth.Auth(conf.URL)
13 | # register a user
14 | self.user_id = "test_password_{}".format(str(uuid.uuid1()))
15 | self.old_password = "old_password_" + self.user_id
16 | self.new_password = "new_password_" + self.user_id
17 | self.terminal = "terminal_" + self.user_id
18 |
19 | assert self.auth.register(self.user_id, self.old_password) == 200
20 | yield
21 |
22 | def test_ok(self):
23 | code = self.auth.password(self.user_id, self.old_password, self.new_password)
24 | assert code == 200
25 |
26 | code, new_token = self.auth.login(self.user_id, self.old_password, self.terminal)
27 | assert code != 200
28 |
29 | code, new_token = self.auth.login(self.user_id, self.new_password, self.terminal)
30 | assert code == 200
31 |
32 | code = self.auth.logout(self.user_id, new_token)
33 | assert code == 200
34 |
35 | def test_error_password(self):
36 | code = self.auth.password(self.user_id, self.old_password + "_x", self.new_password)
37 | assert code != 200
38 |
39 | code, new_token = self.auth.login(self.user_id, self.new_password, self.terminal)
40 | assert code != 200
41 |
42 | def test_error_user_id(self):
43 | code = self.auth.password(self.user_id + "_x", self.old_password, self.new_password)
44 | assert code != 200
45 |
46 | code, new_token = self.auth.login(self.user_id, self.new_password, self.terminal)
47 | assert code != 200
48 |
--------------------------------------------------------------------------------
/fe/test/test_payment.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from fe.access.buyer import Buyer
4 | from fe.test.gen_book_data import GenBook
5 | from fe.access.new_buyer import register_new_buyer
6 | from fe.access.book import Book
7 | import uuid
8 |
9 |
10 | class TestPayment:
11 | seller_id: str
12 | store_id: str
13 | buyer_id: str
14 | password:str
15 | buy_book_info_list: [Book]
16 | total_price: int
17 | order_id: str
18 | buyer: Buyer
19 |
20 | @pytest.fixture(autouse=True)
21 | def pre_run_initialization(self):
22 | self.seller_id = "test_payment_seller_id_{}".format(str(uuid.uuid1()))
23 | self.store_id = "test_payment_store_id_{}".format(str(uuid.uuid1()))
24 | self.buyer_id = "test_payment_buyer_id_{}".format(str(uuid.uuid1()))
25 | self.password = self.seller_id
26 | gen_book = GenBook(self.seller_id, self.store_id)
27 | ok, buy_book_id_list = gen_book.gen(non_exist_book_id=False, low_stock_level=False, max_book_count=5)
28 | self.buy_book_info_list = gen_book.buy_book_info_list
29 | assert ok
30 | b = register_new_buyer(self.buyer_id, self.password)
31 | self.buyer = b
32 | code, self.order_id = b.new_order(self.store_id, buy_book_id_list)
33 | assert code == 200
34 | self.total_price = 0
35 | for item in self.buy_book_info_list:
36 | book: Book = item[0]
37 | num = item[1]
38 | if book.price is None:
39 | continue
40 | else:
41 | self.total_price = self.total_price + book.price * num
42 | yield
43 |
44 | def test_ok(self):
45 | code = self.buyer.add_funds(self.total_price)
46 | assert code == 200
47 | code = self.buyer.payment(self.order_id)
48 | assert code == 200
49 |
50 | def test_authorization_error(self):
51 | code = self.buyer.add_funds(self.total_price)
52 | assert code == 200
53 | self.buyer.password = self.buyer.password + "_x"
54 | code = self.buyer.payment(self.order_id)
55 | assert code != 200
56 |
57 | def test_not_suff_funds(self):
58 | code = self.buyer.add_funds(self.total_price - 1)
59 | assert code == 200
60 | code = self.buyer.payment(self.order_id)
61 | assert code != 200
62 |
63 | def test_repeat_pay(self):
64 | code = self.buyer.add_funds(self.total_price)
65 | assert code == 200
66 | code = self.buyer.payment(self.order_id)
67 | assert code == 200
68 |
69 | code = self.buyer.payment(self.order_id)
70 | assert code != 200
71 |
--------------------------------------------------------------------------------
/fe/test/test_register.py:
--------------------------------------------------------------------------------
1 | import time
2 |
3 | import pytest
4 |
5 | from fe.access import auth
6 | from fe import conf
7 |
8 |
9 | class TestRegister:
10 | @pytest.fixture(autouse=True)
11 | def pre_run_initialization(self):
12 | self.user_id = "test_register_user_{}".format(time.time())
13 | self.password = "test_register_password_{}".format(time.time())
14 | self.auth = auth.Auth(conf.URL)
15 | yield
16 |
17 | def test_register_ok(self):
18 | code = self.auth.register(self.user_id, self.password)
19 | assert code == 200
20 |
21 | def test_unregister_ok(self):
22 | code = self.auth.register(self.user_id, self.password)
23 | assert code == 200
24 |
25 | code = self.auth.unregister(self.user_id, self.password)
26 | assert code == 200
27 |
28 | def test_unregister_error_authorization(self):
29 | code = self.auth.register(self.user_id, self.password)
30 | assert code == 200
31 |
32 | code = self.auth.unregister(self.user_id + "_x", self.password)
33 | assert code != 200
34 |
35 | code = self.auth.unregister(self.user_id, self.password + "_x")
36 | assert code != 200
37 |
38 | def test_register_error_exist_user_id(self):
39 | code = self.auth.register(self.user_id, self.password)
40 | assert code == 200
41 |
42 | code = self.auth.register(self.user_id, self.password)
43 | assert code != 200
44 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | simplejson
2 | lxml
3 | codecov
4 | coverage
5 | flask
6 | pre-commit
7 | pytest
8 | PyJWT
9 | requests
10 |
--------------------------------------------------------------------------------
/script/test.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | export PATHONPATH=`pwd`
3 | coverage run --timid --branch --source fe,be --concurrency=thread -m pytest -v --ignore=fe/data
4 | coverage combine
5 | coverage report
6 | coverage html
7 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import setuptools
2 |
3 | with open("README.md", "r") as fh:
4 | long_description = fh.read()
5 |
6 | setuptools.setup(
7 | name="bookstore",
8 | version="0.0.1",
9 | author="DaSE-DBMS",
10 | author_email="DaSE-DBMS@DaSE-DBMS.com",
11 | description="Buy Books Online",
12 | long_description=long_description,
13 | long_description_content_type="text/markdown",
14 | url="https://github.com/DaSE-DBMS/bookstore.git",
15 | packages=setuptools.find_packages(),
16 | classifiers=[
17 | "Programming Language :: Python :: 3",
18 | "License :: OSI Approved :: MIT License",
19 | "Operating System :: OS Independent",
20 | ],
21 | python_requires=">=3.6",
22 | )
23 |
--------------------------------------------------------------------------------