├── .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 | [![Build Status](https://travis-ci.com/DaSE-DBMS/bookstore.svg?branch=master)](https://travis-ci.com/DaSE-DBMS/bookstore) 3 | [![codecov](https://codecov.io/gh/DaSE-DBMS/bookstore/branch/master/graph/badge.svg)](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 | --------------------------------------------------------------------------------