diff --git a/README.md b/README.md index 833364e..60339cc 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,161 @@ -# BookStore-PJ2 - -当代数据库管理系统课程实验二 \ No newline at end of file +# bookstore +[![Build Status](https://travis-ci.com/DaSE-DBMS/bookstore.svg?branch=master)](https://travis-ci.com/DaSE-DBMS/bookstore) +[![codecov](https://codecov.io/gh/DaSE-DBMS/bookstore/branch/master/graph/badge.svg)](https://codecov.io/gh/DaSE-DBMS/bookstore) + + +## 功能 + +实现一个提供网上购书功能的网站后端。
+网站支持书商在上面开商店,购买者可以通过网站购买。
+买家和卖家都可以注册自己的账号。
+一个卖家可以开一个或多个网上商店, +买家可以为自已的账户充值,在任意商店购买图书。
+支持 下单->付款->发货->收货 流程。
+ +1.实现对应接口的功能,见项目的doc文件夹下面的.md文件描述 (60%)
+ +其中包括: + +1)用户权限接口,如注册、登录、登出、注销
+ +2)买家用户接口,如充值、下单、付款
+ +3)卖家用户接口,如创建店铺、填加书籍信息及描述、增加库存
+ +通过对应的功能测试,所有test case都pass
+ + +2.为项目添加其它功能 :(40%)
+ +1)实现后续的流程
+发货 -> 收货 + +2)搜索图书
+用户可以通过关键字搜索,参数化的搜索方式; +如搜索范围包括,题目,标签,目录,内容;全站搜索或是当前店铺搜索。 +如果显示结果较大,需要分页 +(使用全文索引优化查找) + +3)订单状态,订单查询和取消定单
+用户可以查自已的历史订单,用户也可以取消订单。
+取消定单可由买家主动地取消定单,或者买家下单后,经过一段时间超时仍未付款,定单也会自动取消。
+ + +## bookstore目录结构 +``` +bookstore + |-- be 后端 + |-- model 后端逻辑代码 + |-- view 访问后端接口 + |-- .... + |-- doc JSON API规范说明 + |-- fe 前端访问与测试代码 + |-- access + |-- bench 效率测试 + |-- data + |-- book.db sqlite 数据库(book.db,较少量的测试数据) + |-- book_lx.db sqlite 数据库(book_lx.db, 较大量的测试数据,要从网盘下载) + |-- scraper.py 从豆瓣爬取的图书信息数据的代码 + |-- test 功能性测试(包含对前60%功能的测试,不要修改已有的文件,可以提pull request或bug) + |-- conf.py 测试参数,修改这个文件以适应自己的需要 + |-- conftest.py pytest初始化配置,修改这个文件以适应自己的需要 + |-- .... + |-- .... +``` + + +## 安装配置 +安装python (需要python3.6以上) + +进入bookstore文件夹下: + +安装依赖 + + pip install -r requirements.txt + +执行测试 + + bash script/test.sh + +bookstore/fe/data/book.db中包含测试的数据,从豆瓣网抓取的图书信息,其DDL为: + + create table book + ( + id TEXT primary key, + title TEXT, + author TEXT, + publisher TEXT, + original_title TEXT, + translator TEXT, + pub_year TEXT, + pages INTEGER, + price INTEGER, + currency_unit TEXT, + binding TEXT, + isbn TEXT, + author_intro TEXT, + book_intro text, + content TEXT, + tags TEXT, + picture BLOB + ); + + +## 要求 + +3人一组,做好分工,完成下述内容: + +1.bookstore文件夹是该项目的demo,采用flask后端框架与sqlite数据库,实现了前60%功能以及对应的测试用例代码。要求利用ORM使用postgreSQL数据库实现前60%功能,可以在demo的基础上进行修改,也可以采用其他后端框架重新实现。需要通过fe/test/下已有的全部测试用例。 + +2.在完成前60%功能的基础上,继续实现后40%功能,要有接口、后端逻辑实现、数据库操作、代码测试。对所有接口都要写test case,通过测试并计算测试覆盖率(尽量提高测试覆盖率)。 + +3.尽量使用索引、事务处理等关系数据库特性,对程序与数据库执行的性能有考量 + +4.尽量使用git等版本管理工具 + +5.不需要实现界面,通过代码测试体现功能与正确性 + + +## 报告内容 + +1.每位组员的学号、姓名,以及分工 + +2.关系数据库设计:概念设计、ER图、关系模式等 + +3.对60%基础功能和40%附加功能的接口、后端逻辑、数据库操作、测试用例进行介绍,展示测试结果与测试覆盖率。 + +4.如果完成,可以展示本次大作业的亮点,比如要求中的“3 4”两点。 + +注:验收依据为报告,本次大作业所作的工作要完整展示在报告中。 + + +## 验收与考核准测 + +- 提交 **代码+报告** 压缩包到 **第二次大作业提交** 入口,命名规则:2022_CDMS_PJ2_第几组 +- 提交截止日期:**2022.12.10 22:00** + +本次大作业不需要提交演示视频,验收的依据是报告: + +1. 没有提交或没有实质的工作,得D +2. 完成"要求"中的第1点,可得C +3. 完成前2点,通过全部测试用例且有较高的测试覆盖率,可得B +4. 完成前2点的基础上,体现出第3 4点,可得A + + +## 附加任务 + +本次考核不做要求 + +学有余力的同学可以尝试下述内容,可以写在报告里: + +更多的数据 book_lx.db 可以从网盘下载,下载地址为: + + https://pan.baidu.com/s/1bjCOW8Z5N_ClcqU54Pdt8g + +提取码: + + hj6q + +这份数据同bookstore/fe/data/book.db的schema相同,但是有更多的数据(约3.5GB, 40000+行) + +可以将book_lx.db导入到数据库中,测试下单及付款两个接口的性能(最好分离负载生成和后端),测出支持的每分钟交易数,延迟等。 diff --git a/be/__init__.py b/be/__init__.py new file mode 100644 index 0000000..e5a0d9b --- /dev/null +++ b/be/__init__.py @@ -0,0 +1 @@ +#!/usr/bin/env python3 diff --git a/be/app.py b/be/app.py new file mode 100644 index 0000000..e0a6207 --- /dev/null +++ b/be/app.py @@ -0,0 +1,4 @@ +from be import serve + +if __name__ == "__main__": + serve.be_run() diff --git a/be/model/__init__.py b/be/model/__init__.py new file mode 100644 index 0000000..e5a0d9b --- /dev/null +++ b/be/model/__init__.py @@ -0,0 +1 @@ +#!/usr/bin/env python3 diff --git a/be/model/buyer.py b/be/model/buyer.py new file mode 100644 index 0000000..f8b6595 --- /dev/null +++ b/be/model/buyer.py @@ -0,0 +1,162 @@ +import sqlite3 as sqlite +import uuid +import json +import logging +from be.model import db_conn +from be.model import error + + +class Buyer(db_conn.DBConn): + def __init__(self): + db_conn.DBConn.__init__(self) + + def new_order(self, user_id: str, store_id: str, id_and_count: [(str, int)]) -> (int, str, str): + order_id = "" + try: + if not self.user_id_exist(user_id): + return error.error_non_exist_user_id(user_id) + (order_id, ) + if not self.store_id_exist(store_id): + return error.error_non_exist_store_id(store_id) + (order_id, ) + uid = "{}_{}_{}".format(user_id, store_id, str(uuid.uuid1())) + + for book_id, count in id_and_count: + cursor = self.conn.execute( + "SELECT book_id, stock_level, book_info FROM store " + "WHERE store_id = ? AND book_id = ?;", + (store_id, book_id)) + row = cursor.fetchone() + if row is None: + return error.error_non_exist_book_id(book_id) + (order_id, ) + + stock_level = row[1] + book_info = row[2] + book_info_json = json.loads(book_info) + price = book_info_json.get("price") + + if stock_level < count: + return error.error_stock_level_low(book_id) + (order_id,) + + cursor = self.conn.execute( + "UPDATE store set stock_level = stock_level - ? " + "WHERE store_id = ? and book_id = ? and stock_level >= ?; ", + (count, store_id, book_id, count)) + if cursor.rowcount == 0: + return error.error_stock_level_low(book_id) + (order_id, ) + + self.conn.execute( + "INSERT INTO new_order_detail(order_id, book_id, count, price) " + "VALUES(?, ?, ?, ?);", + (uid, book_id, count, price)) + + self.conn.execute( + "INSERT INTO new_order(order_id, store_id, user_id) " + "VALUES(?, ?, ?);", + (uid, store_id, user_id)) + self.conn.commit() + order_id = uid + except sqlite.Error as e: + logging.info("528, {}".format(str(e))) + return 528, "{}".format(str(e)), "" + except BaseException as e: + logging.info("530, {}".format(str(e))) + return 530, "{}".format(str(e)), "" + + return 200, "ok", order_id + + def payment(self, user_id: str, password: str, order_id: str) -> (int, str): + conn = self.conn + try: + cursor = conn.execute("SELECT order_id, user_id, store_id FROM new_order WHERE order_id = ?", (order_id,)) + row = cursor.fetchone() + if row is None: + return error.error_invalid_order_id(order_id) + + order_id = row[0] + buyer_id = row[1] + store_id = row[2] + + if buyer_id != user_id: + return error.error_authorization_fail() + + cursor = conn.execute("SELECT balance, password FROM user WHERE user_id = ?;", (buyer_id,)) + row = cursor.fetchone() + if row is None: + return error.error_non_exist_user_id(buyer_id) + balance = row[0] + if password != row[1]: + return error.error_authorization_fail() + + cursor = conn.execute("SELECT store_id, user_id FROM user_store WHERE store_id = ?;", (store_id,)) + row = cursor.fetchone() + if row is None: + return error.error_non_exist_store_id(store_id) + + seller_id = row[1] + + if not self.user_id_exist(seller_id): + return error.error_non_exist_user_id(seller_id) + + cursor = conn.execute("SELECT book_id, count, price FROM new_order_detail WHERE order_id = ?;", (order_id,)) + total_price = 0 + for row in cursor: + count = row[1] + price = row[2] + total_price = total_price + price * count + + if balance < total_price: + return error.error_not_sufficient_funds(order_id) + + cursor = conn.execute("UPDATE user set balance = balance - ?" + "WHERE user_id = ? AND balance >= ?", + (total_price, buyer_id, total_price)) + if cursor.rowcount == 0: + return error.error_not_sufficient_funds(order_id) + + cursor = conn.execute("UPDATE user set balance = balance + ?" + "WHERE user_id = ?", + (total_price, buyer_id)) + + if cursor.rowcount == 0: + return error.error_non_exist_user_id(buyer_id) + + cursor = conn.execute("DELETE FROM new_order WHERE order_id = ?", (order_id, )) + if cursor.rowcount == 0: + return error.error_invalid_order_id(order_id) + + cursor = conn.execute("DELETE FROM new_order_detail where order_id = ?", (order_id, )) + if cursor.rowcount == 0: + return error.error_invalid_order_id(order_id) + + conn.commit() + + except sqlite.Error as e: + return 528, "{}".format(str(e)) + + except BaseException as e: + return 530, "{}".format(str(e)) + + return 200, "ok" + + def add_funds(self, user_id, password, add_value) -> (int, str): + try: + cursor = self.conn.execute("SELECT password from user where user_id=?", (user_id,)) + row = cursor.fetchone() + if row is None: + return error.error_authorization_fail() + + if row[0] != password: + return error.error_authorization_fail() + + cursor = self.conn.execute( + "UPDATE user SET balance = balance + ? WHERE user_id = ?", + (add_value, user_id)) + if cursor.rowcount == 0: + return error.error_non_exist_user_id(user_id) + + self.conn.commit() + except sqlite.Error as e: + return 528, "{}".format(str(e)) + except BaseException as e: + return 530, "{}".format(str(e)) + + return 200, "ok" diff --git a/be/model/db_conn.py b/be/model/db_conn.py new file mode 100644 index 0000000..f322cfc --- /dev/null +++ b/be/model/db_conn.py @@ -0,0 +1,30 @@ +from be.model import store + + +class DBConn: + def __init__(self): + self.conn = store.get_db_conn() + + def user_id_exist(self, user_id): + cursor = self.conn.execute("SELECT user_id FROM user WHERE user_id = ?;", (user_id,)) + row = cursor.fetchone() + if row is None: + return False + else: + return True + + def book_id_exist(self, store_id, book_id): + cursor = self.conn.execute("SELECT book_id FROM store WHERE store_id = ? AND book_id = ?;", (store_id, book_id)) + row = cursor.fetchone() + if row is None: + return False + else: + return True + + def store_id_exist(self, store_id): + cursor = self.conn.execute("SELECT store_id FROM user_store WHERE store_id = ?;", (store_id,)) + row = cursor.fetchone() + if row is None: + return False + else: + return True diff --git a/be/model/error.py b/be/model/error.py new file mode 100644 index 0000000..0b25f2a --- /dev/null +++ b/be/model/error.py @@ -0,0 +1,66 @@ + +error_code = { + 401: "authorization fail.", + 511: "non exist user id {}", + 512: "exist user id {}", + 513: "non exist store id {}", + 514: "exist store id {}", + 515: "non exist book id {}", + 516: "exist book id {}", + 517: "stock level low, book id {}", + 518: "invalid order id {}", + 519: "not sufficient funds, order id {}", + 520: "", + 521: "", + 522: "", + 523: "", + 524: "", + 525: "", + 526: "", + 527: "", + 528: "", +} + + +def error_non_exist_user_id(user_id): + return 511, error_code[511].format(user_id) + + +def error_exist_user_id(user_id): + return 512, error_code[512].format(user_id) + + +def error_non_exist_store_id(store_id): + return 513, error_code[513].format(store_id) + + +def error_exist_store_id(store_id): + return 514, error_code[514].format(store_id) + + +def error_non_exist_book_id(book_id): + return 515, error_code[515].format(book_id) + + +def error_exist_book_id(book_id): + return 516, error_code[516].format(book_id) + + +def error_stock_level_low(book_id): + return 517, error_code[517].format(book_id) + + +def error_invalid_order_id(order_id): + return 518, error_code[518].format(order_id) + + +def error_not_sufficient_funds(order_id): + return 519, error_code[518].format(order_id) + + +def error_authorization_fail(): + return 401, error_code[401] + + +def error_and_message(code, message): + return code, message diff --git a/be/model/seller.py b/be/model/seller.py new file mode 100644 index 0000000..2cf47aa --- /dev/null +++ b/be/model/seller.py @@ -0,0 +1,60 @@ +import sqlite3 as sqlite +from be.model import error +from be.model import db_conn + + +class Seller(db_conn.DBConn): + + def __init__(self): + db_conn.DBConn.__init__(self) + + def add_book(self, user_id: str, store_id: str, book_id: str, book_json_str: str, stock_level: int): + try: + if not self.user_id_exist(user_id): + return error.error_non_exist_user_id(user_id) + if not self.store_id_exist(store_id): + return error.error_non_exist_store_id(store_id) + if self.book_id_exist(store_id, book_id): + return error.error_exist_book_id(book_id) + + self.conn.execute("INSERT into store(store_id, book_id, book_info, stock_level)" + "VALUES (?, ?, ?, ?)", (store_id, book_id, book_json_str, stock_level)) + self.conn.commit() + except sqlite.Error as e: + return 528, "{}".format(str(e)) + except BaseException as e: + return 530, "{}".format(str(e)) + return 200, "ok" + + def add_stock_level(self, user_id: str, store_id: str, book_id: str, add_stock_level: int): + try: + if not self.user_id_exist(user_id): + return error.error_non_exist_user_id(user_id) + if not self.store_id_exist(store_id): + return error.error_non_exist_store_id(store_id) + if not self.book_id_exist(store_id, book_id): + return error.error_non_exist_book_id(book_id) + + self.conn.execute("UPDATE store SET stock_level = stock_level + ? " + "WHERE store_id = ? AND book_id = ?", (add_stock_level, store_id, book_id)) + self.conn.commit() + except sqlite.Error as e: + return 528, "{}".format(str(e)) + except BaseException as e: + return 530, "{}".format(str(e)) + return 200, "ok" + + def create_store(self, user_id: str, store_id: str) -> (int, str): + try: + if not self.user_id_exist(user_id): + return error.error_non_exist_user_id(user_id) + if self.store_id_exist(store_id): + return error.error_exist_store_id(store_id) + self.conn.execute("INSERT into user_store(store_id, user_id)" + "VALUES (?, ?)", (store_id, user_id)) + self.conn.commit() + except sqlite.Error as e: + return 528, "{}".format(str(e)) + except BaseException as e: + return 530, "{}".format(str(e)) + return 200, "ok" diff --git a/be/model/store.py b/be/model/store.py new file mode 100644 index 0000000..d82aeeb --- /dev/null +++ b/be/model/store.py @@ -0,0 +1,63 @@ +import logging +import os +import sqlite3 as sqlite + + +class Store: + database: str + + def __init__(self, db_path): + self.database = os.path.join(db_path, "be.db") + self.init_tables() + + def init_tables(self): + try: + conn = self.get_db_conn() + conn.execute( + "CREATE TABLE IF NOT EXISTS user (" + "user_id TEXT PRIMARY KEY, password TEXT NOT NULL, " + "balance INTEGER NOT NULL, token TEXT, terminal TEXT);" + ) + + conn.execute( + "CREATE TABLE IF NOT EXISTS user_store(" + "user_id TEXT, store_id, PRIMARY KEY(user_id, store_id));" + ) + + conn.execute( + "CREATE TABLE IF NOT EXISTS store( " + "store_id TEXT, book_id TEXT, book_info TEXT, stock_level INTEGER," + " PRIMARY KEY(store_id, book_id))" + ) + + conn.execute( + "CREATE TABLE IF NOT EXISTS new_order( " + "order_id TEXT PRIMARY KEY, user_id TEXT, store_id TEXT)" + ) + + conn.execute( + "CREATE TABLE IF NOT EXISTS new_order_detail( " + "order_id TEXT, book_id TEXT, count INTEGER, price INTEGER, " + "PRIMARY KEY(order_id, book_id))" + ) + + conn.commit() + except sqlite.Error as e: + logging.error(e) + conn.rollback() + + def get_db_conn(self) -> sqlite.Connection: + return sqlite.connect(self.database) + + +database_instance: Store = None + + +def init_database(db_path): + global database_instance + database_instance = Store(db_path) + + +def get_db_conn(): + global database_instance + return database_instance.get_db_conn() diff --git a/be/model/user.py b/be/model/user.py new file mode 100644 index 0000000..acd58c2 --- /dev/null +++ b/be/model/user.py @@ -0,0 +1,169 @@ +import jwt +import time +import logging +import sqlite3 as sqlite +from be.model import error +from be.model import db_conn + +# encode a json string like: +# { +# "user_id": [user name], +# "terminal": [terminal code], +# "timestamp": [ts]} to a JWT +# } + + +def jwt_encode(user_id: str, terminal: str) -> str: + encoded = jwt.encode( + {"user_id": user_id, "terminal": terminal, "timestamp": time.time()}, + key=user_id, + algorithm="HS256", + ) + return encoded.encode("utf-8").decode("utf-8") + + +# decode a JWT to a json string like: +# { +# "user_id": [user name], +# "terminal": [terminal code], +# "timestamp": [ts]} to a JWT +# } +def jwt_decode(encoded_token, user_id: str) -> str: + decoded = jwt.decode(encoded_token, key=user_id, algorithms="HS256") + return decoded + + +class User(db_conn.DBConn): + token_lifetime: int = 3600 # 3600 second + + def __init__(self): + db_conn.DBConn.__init__(self) + + def __check_token(self, user_id, db_token, token) -> bool: + try: + if db_token != token: + return False + jwt_text = jwt_decode(encoded_token=token, user_id=user_id) + ts = jwt_text["timestamp"] + if ts is not None: + now = time.time() + if self.token_lifetime > now - ts >= 0: + return True + except jwt.exceptions.InvalidSignatureError as e: + logging.error(str(e)) + return False + + def register(self, user_id: str, password: str): + try: + terminal = "terminal_{}".format(str(time.time())) + token = jwt_encode(user_id, terminal) + self.conn.execute( + "INSERT into user(user_id, password, balance, token, terminal) " + "VALUES (?, ?, ?, ?, ?);", + (user_id, password, 0, token, terminal), ) + self.conn.commit() + except sqlite.Error: + return error.error_exist_user_id(user_id) + return 200, "ok" + + def check_token(self, user_id: str, token: str) -> (int, str): + cursor = self.conn.execute("SELECT token from user where user_id=?", (user_id,)) + row = cursor.fetchone() + if row is None: + return error.error_authorization_fail() + db_token = row[0] + if not self.__check_token(user_id, db_token, token): + return error.error_authorization_fail() + return 200, "ok" + + def check_password(self, user_id: str, password: str) -> (int, str): + cursor = self.conn.execute("SELECT password from user where user_id=?", (user_id,)) + row = cursor.fetchone() + if row is None: + return error.error_authorization_fail() + + if password != row[0]: + return error.error_authorization_fail() + + return 200, "ok" + + def login(self, user_id: str, password: str, terminal: str) -> (int, str, str): + token = "" + try: + code, message = self.check_password(user_id, password) + if code != 200: + return code, message, "" + + token = jwt_encode(user_id, terminal) + cursor = self.conn.execute( + "UPDATE user set token= ? , terminal = ? where user_id = ?", + (token, terminal, user_id), ) + if cursor.rowcount == 0: + return error.error_authorization_fail() + ("", ) + self.conn.commit() + except sqlite.Error as e: + return 528, "{}".format(str(e)), "" + except BaseException as e: + return 530, "{}".format(str(e)), "" + return 200, "ok", token + + def logout(self, user_id: str, token: str) -> bool: + try: + code, message = self.check_token(user_id, token) + if code != 200: + return code, message + + terminal = "terminal_{}".format(str(time.time())) + dummy_token = jwt_encode(user_id, terminal) + + cursor = self.conn.execute( + "UPDATE user SET token = ?, terminal = ? WHERE user_id=?", + (dummy_token, terminal, user_id), ) + if cursor.rowcount == 0: + return error.error_authorization_fail() + + self.conn.commit() + except sqlite.Error as e: + return 528, "{}".format(str(e)) + except BaseException as e: + return 530, "{}".format(str(e)) + return 200, "ok" + + def unregister(self, user_id: str, password: str) -> (int, str): + try: + code, message = self.check_password(user_id, password) + if code != 200: + return code, message + + cursor = self.conn.execute("DELETE from user where user_id=?", (user_id,)) + if cursor.rowcount == 1: + self.conn.commit() + else: + return error.error_authorization_fail() + except sqlite.Error as e: + return 528, "{}".format(str(e)) + except BaseException as e: + return 530, "{}".format(str(e)) + return 200, "ok" + + def change_password(self, user_id: str, old_password: str, new_password: str) -> bool: + try: + code, message = self.check_password(user_id, old_password) + if code != 200: + return code, message + + terminal = "terminal_{}".format(str(time.time())) + token = jwt_encode(user_id, terminal) + cursor = self.conn.execute( + "UPDATE user set password = ?, token= ? , terminal = ? where user_id = ?", + (new_password, token, terminal, user_id), ) + if cursor.rowcount == 0: + return error.error_authorization_fail() + + self.conn.commit() + except sqlite.Error as e: + return 528, "{}".format(str(e)) + except BaseException as e: + return 530, "{}".format(str(e)) + return 200, "ok" + diff --git a/be/serve.py b/be/serve.py new file mode 100644 index 0000000..6499a7c --- /dev/null +++ b/be/serve.py @@ -0,0 +1,46 @@ +import logging +import os +from flask import Flask +from flask import Blueprint +from flask import request +from be.view import auth +from be.view import seller +from be.view import buyer +from be.model.store import init_database + +bp_shutdown = Blueprint("shutdown", __name__) + + +def shutdown_server(): + func = request.environ.get("werkzeug.server.shutdown") + if func is None: + raise RuntimeError("Not running with the Werkzeug Server") + func() + + +@bp_shutdown.route("/shutdown") +def be_shutdown(): + shutdown_server() + return "Server shutting down..." + + +def be_run(): + this_path = os.path.dirname(__file__) + parent_path = os.path.dirname(this_path) + log_file = os.path.join(parent_path, "app.log") + init_database(parent_path) + + logging.basicConfig(filename=log_file, level=logging.ERROR) + handler = logging.StreamHandler() + formatter = logging.Formatter( + "%(asctime)s [%(threadName)-12.12s] [%(levelname)-5.5s] %(message)s" + ) + handler.setFormatter(formatter) + logging.getLogger().addHandler(handler) + + app = Flask(__name__) + app.register_blueprint(bp_shutdown) + app.register_blueprint(auth.bp_auth) + app.register_blueprint(seller.bp_seller) + app.register_blueprint(buyer.bp_buyer) + app.run() diff --git a/be/view/__init__.py b/be/view/__init__.py new file mode 100644 index 0000000..e5a0d9b --- /dev/null +++ b/be/view/__init__.py @@ -0,0 +1 @@ +#!/usr/bin/env python3 diff --git a/be/view/auth.py b/be/view/auth.py new file mode 100644 index 0000000..8d9528a --- /dev/null +++ b/be/view/auth.py @@ -0,0 +1,53 @@ +from flask import Blueprint +from flask import request +from flask import jsonify +from be.model import user + +bp_auth = Blueprint("auth", __name__, url_prefix="/auth") + + +@bp_auth.route("/login", methods=["POST"]) +def login(): + user_id = request.json.get("user_id", "") + password = request.json.get("password", "") + terminal = request.json.get("terminal", "") + u = user.User() + code, message, token = u.login(user_id=user_id, password=password, terminal=terminal) + return jsonify({"message": message, "token": token}), code + + +@bp_auth.route("/logout", methods=["POST"]) +def logout(): + user_id: str = request.json.get("user_id") + token: str = request.headers.get("token") + u = user.User() + code, message = u.logout(user_id=user_id, token=token) + return jsonify({"message": message}), code + + +@bp_auth.route("/register", methods=["POST"]) +def register(): + user_id = request.json.get("user_id", "") + password = request.json.get("password", "") + u = user.User() + code, message = u.register(user_id=user_id, password=password) + return jsonify({"message": message}), code + + +@bp_auth.route("/unregister", methods=["POST"]) +def unregister(): + user_id = request.json.get("user_id", "") + password = request.json.get("password", "") + u = user.User() + code, message = u.unregister(user_id=user_id, password=password) + return jsonify({"message": message}), code + + +@bp_auth.route("/password", methods=["POST"]) +def change_password(): + user_id = request.json.get("user_id", "") + old_password = request.json.get("oldPassword", "") + new_password = request.json.get("newPassword", "") + u = user.User() + code, message = u.change_password(user_id=user_id, old_password=old_password, new_password=new_password) + return jsonify({"message": message}), code diff --git a/be/view/buyer.py b/be/view/buyer.py new file mode 100644 index 0000000..ce26221 --- /dev/null +++ b/be/view/buyer.py @@ -0,0 +1,42 @@ +from flask import Blueprint +from flask import request +from flask import jsonify +from be.model.buyer import Buyer + +bp_buyer = Blueprint("buyer", __name__, url_prefix="/buyer") + + +@bp_buyer.route("/new_order", methods=["POST"]) +def new_order(): + user_id: str = request.json.get("user_id") + store_id: str = request.json.get("store_id") + books: [] = request.json.get("books") + id_and_count = [] + for book in books: + book_id = book.get("id") + count = book.get("count") + id_and_count.append((book_id, count)) + + b = Buyer() + code, message, order_id = b.new_order(user_id, store_id, id_and_count) + return jsonify({"message": message, "order_id": order_id}), code + + +@bp_buyer.route("/payment", methods=["POST"]) +def payment(): + user_id: str = request.json.get("user_id") + order_id: str = request.json.get("order_id") + password: str = request.json.get("password") + b = Buyer() + code, message = b.payment(user_id, password, order_id) + return jsonify({"message": message}), code + + +@bp_buyer.route("/add_funds", methods=["POST"]) +def add_funds(): + user_id = request.json.get("user_id") + password = request.json.get("password") + add_value = request.json.get("add_value") + b = Buyer() + code, message = b.add_funds(user_id, password, add_value) + return jsonify({"message": message}), code diff --git a/be/view/seller.py b/be/view/seller.py new file mode 100644 index 0000000..3f56eed --- /dev/null +++ b/be/view/seller.py @@ -0,0 +1,42 @@ +from flask import Blueprint +from flask import request +from flask import jsonify +from be.model import seller +import json + +bp_seller = Blueprint("seller", __name__, url_prefix="/seller") + + +@bp_seller.route("/create_store", methods=["POST"]) +def seller_create_store(): + user_id: str = request.json.get("user_id") + store_id: str = request.json.get("store_id") + s = seller.Seller() + code, message = s.create_store(user_id, store_id) + return jsonify({"message": message}), code + + +@bp_seller.route("/add_book", methods=["POST"]) +def seller_add_book(): + user_id: str = request.json.get("user_id") + store_id: str = request.json.get("store_id") + book_info: str = request.json.get("book_info") + stock_level: str = request.json.get("stock_level", 0) + + s = seller.Seller() + code, message = s.add_book(user_id, store_id, book_info.get("id"), json.dumps(book_info), stock_level) + + return jsonify({"message": message}), code + + +@bp_seller.route("/add_stock_level", methods=["POST"]) +def add_stock_level(): + user_id: str = request.json.get("user_id") + store_id: str = request.json.get("store_id") + book_id: str = request.json.get("book_id") + add_num: str = request.json.get("add_stock_level", 0) + + s = seller.Seller() + code, message = s.add_stock_level(user_id, store_id, book_id, add_num) + + return jsonify({"message": message}), code diff --git a/doc/auth.md b/doc/auth.md new file mode 100644 index 0000000..ba7bc4b --- /dev/null +++ b/doc/auth.md @@ -0,0 +1,213 @@ +## 注册用户 + +#### URL: +POST http://$address$/auth/register + +#### Request + +Body: +``` +{ + "user_id":"$user name$", + "password":"$user password$" +} +``` + +变量名 | 类型 | 描述 | 是否可为空 +---|---|---|--- +user_id | string | 用户名 | N +password | string | 登陆密码 | N + +#### Response + +Status Code: + + +码 | 描述 +--- | --- +200 | 注册成功 +5XX | 注册失败,用户名重复 + +Body: +``` +{ + "message":"$error message$" +} +``` +变量名 | 类型 | 描述 | 是否可为空 +---|---|---|--- +message | string | 返回错误消息,成功时为"ok" | N + +## 注销用户 + +#### URL: +POST http://$address$/auth/unregister + +#### Request + +Body: +``` +{ + "user_id":"$user name$", + "password":"$user password$" +} +``` + +变量名 | 类型 | 描述 | 是否可为空 +---|---|---|--- +user_id | string | 用户名 | N +password | string | 登陆密码 | N + +#### Response + +Status Code: + + +码 | 描述 +--- | --- +200 | 注销成功 +401 | 注销失败,用户名不存在或密码不正确 + + +Body: +``` +{ + "message":"$error message$" +} +``` +变量名 | 类型 | 描述 | 是否可为空 +---|---|---|--- +message | string | 返回错误消息,成功时为"ok" | N + +## 用户登录 + +#### URL: +POST http://$address$/auth/login + +#### Request + +Body: +``` +{ + "user_id":"$user name$", + "password":"$user password$", + "terminal":"$terminal code$" +} +``` + +变量名 | 类型 | 描述 | 是否可为空 +---|---|---|--- +user_id | string | 用户名 | N +password | string | 登陆密码 | N +terminal | string | 终端代码 | N + +#### Response + +Status Code: + +码 | 描述 +--- | --- +200 | 登录成功 +401 | 登录失败,用户名或密码错误 + +Body: +``` +{ + "message":"$error message$", + "token":"$access token$" +} +``` +变量名 | 类型 | 描述 | 是否可为空 +---|---|---|--- +message | string | 返回错误消息,成功时为"ok" | N +token | string | 访问token,用户登录后每个需要授权的请求应在headers中传入这个token | 成功时不为空 + +#### 说明 + +1.terminal标识是哪个设备登录的,不同的设备拥有不同的ID,测试时可以随机生成。 + +2.token是登录后,在客户端中缓存的令牌,在用户登录时由服务端生成,用户在接下来的访问请求时不需要密码。token会定期地失效,对于不同的设备,token是不同的。token只对特定的时期特定的设备是有效的。 + +## 用户更改密码 + +#### URL: +POST http://$address$/auth/password + +#### Request + +Body: +``` +{ + "user_id":"$user name$", + "oldPassword":"$old password$", + "newPassword":"$new password$" +} +``` + +变量名 | 类型 | 描述 | 是否可为空 +---|---|---|--- +user_id | string | 用户名 | N +oldPassword | string | 旧的登陆密码 | N +newPassword | string | 新的登陆密码 | N + +#### Response + +Status Code: + +码 | 描述 +--- | --- +200 | 更改密码成功 +401 | 更改密码失败 + +Body: +``` +{ + "message":"$error message$", +} +``` +变量名 | 类型 | 描述 | 是否可为空 +---|---|---|--- +message | string | 返回错误消息,成功时为"ok" | N + +## 用户登出 + +#### URL: +POST http://$address$/auth/logout + +#### Request + +Headers: + +key | 类型 | 描述 +---|---|--- +token | string | 访问token + +Body: +``` +{ + "user_id":"$user name$" +} +``` + +变量名 | 类型 | 描述 | 是否可为空 +---|---|---|--- +user_id | string | 用户名 | N + +#### Response + +Status Code: + +码 | 描述 +--- | --- +200 | 登出成功 +401 | 登出失败,用户名或token错误 + +Body: +``` +{ + "message":"$error message$" +} +``` +变量名 | 类型 | 描述 | 是否可为空 +---|---|---|--- +message | string | 返回错误消息,成功时为"ok" | N diff --git a/doc/buyer.md b/doc/buyer.md new file mode 100644 index 0000000..5f415d2 --- /dev/null +++ b/doc/buyer.md @@ -0,0 +1,144 @@ +## 买家下单 + +#### URL: +POST http://[address]/buyer/new_order + +#### Request + +##### Header: + +key | 类型 | 描述 | 是否可为空 +---|---|---|--- +token | string | 登录产生的会话标识 | N + +##### Body: +```json +{ + "user_id": "buyer_id", + "store_id": "store_id", + "books": [ + { + "id": "1000067", + "count": 1 + }, + { + "id": "1000134", + "count": 4 + } + ] +} +``` + +##### 属性说明: + +变量名 | 类型 | 描述 | 是否可为空 +---|---|---|--- +user_id | string | 买家用户ID | N +store_id | string | 商铺ID | N +books | class | 书籍购买列表 | N + +books数组: + +变量名 | 类型 | 描述 | 是否可为空 +---|---|---|--- +id | string | 书籍的ID | N +count | string | 购买数量 | N + + +#### Response + +Status Code: + +码 | 描述 +--- | --- +200 | 下单成功 +5XX | 买家用户ID不存在 +5XX | 商铺ID不存在 +5XX | 购买的图书不存在 +5XX | 商品库存不足 + +##### Body: +```json +{ + "order_id": "uuid" +} +``` + +##### 属性说明: + +变量名 | 类型 | 描述 | 是否可为空 +---|---|---|--- +order_id | string | 订单号,只有返回200时才有效 | N + + +## 买家付款 + +#### URL: +POST http://[address]/buyer/payment + +#### Request + +##### Body: +```json +{ + "user_id": "buyer_id", + "order_id": "order_id", + "password": "password" +} +``` + +##### 属性说明: + +变量名 | 类型 | 描述 | 是否可为空 +---|---|---|--- +user_id | string | 买家用户ID | N +order_id | string | 订单ID | N +password | string | 买家用户密码 | N + + +#### Response + +Status Code: + +码 | 描述 +--- | --- +200 | 付款成功 +5XX | 账户余额不足 +5XX | 无效参数 +401 | 授权失败 + + +## 买家充值 + +#### URL: +POST http://[address]/buyer/add_funds + +#### Request + + + +##### Body: +```json +{ + "user_id": "user_id", + "password": "password", + "add_value": 10 +} +``` + +##### 属性说明: + +key | 类型 | 描述 | 是否可为空 +---|---|---|--- +user_id | string | 买家用户ID | N +password | string | 用户密码 | N +add_value | int | 充值金额,以分为单位 | N + + +Status Code: + +码 | 描述 +--- | --- +200 | 充值成功 +401 | 授权失败 +5XX | 无效参数 diff --git a/doc/seller.md b/doc/seller.md new file mode 100644 index 0000000..0513122 --- /dev/null +++ b/doc/seller.md @@ -0,0 +1,178 @@ +## 创建商铺 + + + +#### URL + +POST http://[address]/seller/create_store + +#### Request +Headers: + +key | 类型 | 描述 | 是否可为空 +---|---|---|--- +token | string | 登录产生的会话标识 | N + +Body: + +```json +{ + "user_id": "$seller id$", + "store_id": "$store id$" +} +``` + +key | 类型 | 描述 | 是否可为空 +---|---|---|--- +user_id | string | 卖家用户ID | N +store_id | string | 商铺ID | N + +#### Response + +Status Code: + +码 | 描述 +--- | --- +200 | 创建商铺成功 +5XX | 商铺ID已存在 + + +## 商家添加书籍信息 + +#### URL: +POST http://[address]/seller/add_book + +#### Request +Headers: + +key | 类型 | 描述 | 是否可为空 +---|---|---|--- +token | string | 登录产生的会话标识 | N + +Body: + +```json +{ + "user_id": "$seller user id$", + "store_id": "$store id$", + "book_info": { + "tags": [ + "tags1", + "tags2", + "tags3", + "..." + ], + "pictures": [ + "$Base 64 encoded bytes array1$", + "$Base 64 encoded bytes array2$", + "$Base 64 encoded bytes array3$", + "..." + ], + "id": "$book id$", + "title": "$book title$", + "author": "$book author$", + "publisher": "$book publisher$", + "original_title": "$original title$", + "translator": "translater", + "pub_year": "$pub year$", + "pages": 10, + "price": 10, + "binding": "平装", + "isbn": "$isbn$", + "author_intro": "$author introduction$", + "book_intro": "$book introduction$", + "content": "$chapter1 ...$" + }, + "stock_level": 0 +} + +``` + +属性说明: + +变量名 | 类型 | 描述 | 是否可为空 +---|---|---|--- +user_id | string | 卖家用户ID | N +store_id | string | 商铺ID | N +book_info | class | 书籍信息 | N +stock_level | int | 初始库存,大于等于0 | N + +book_info类: + +变量名 | 类型 | 描述 | 是否可为空 +---|---|---|--- +id | string | 书籍ID | N +title | string | 书籍题目 | N +author | string | 作者 | Y +publisher | string | 出版社 | Y +original_title | string | 原书题目 | Y +translator | string | 译者 | Y +pub_year | string | 出版年月 | Y +pages | int | 页数 | Y +price | int | 价格(以分为单位) | N +binding | string | 装帧,精状/平装 | Y +isbn | string | ISBN号 | Y +author_intro | string | 作者简介 | Y +book_intro | string | 书籍简介 | Y +content | string | 样章试读 | Y +tags | array | 标签 | Y +pictures | array | 照片 | Y + +tags和pictures: + + tags 中每个数组元素都是string类型 + picture 中每个数组元素都是string(base64表示的bytes array)类型 + + +#### Response + +Status Code: + +码 | 描述 +--- | --- +200 | 添加图书信息成功 +5XX | 卖家用户ID不存在 +5XX | 商铺ID不存在 +5XX | 图书ID已存在 + + +## 商家添加书籍库存 + + +#### URL + +POST http://[address]/seller/add_stock_level + +#### Request +Headers: + +key | 类型 | 描述 | 是否可为空 +---|---|---|--- +token | string | 登录产生的会话标识 | N + +Body: + +```json +{ + "user_id": "$seller id$", + "store_id": "$store id$", + "book_id": "$book id$", + "add_stock_level": 10 +} +``` +key | 类型 | 描述 | 是否可为空 +---|---|---|--- +user_id | string | 卖家用户ID | N +store_id | string | 商铺ID | N +book_id | string | 书籍ID | N +add_stock_level | int | 增加的库存量 | N + +#### Response + +Status Code: + +码 | 描述 +--- | :-- +200 | 创建商铺成功 +5XX | 商铺ID不存在 +5XX | 图书ID不存在 diff --git a/fe/__init__.py b/fe/__init__.py new file mode 100644 index 0000000..e5a0d9b --- /dev/null +++ b/fe/__init__.py @@ -0,0 +1 @@ +#!/usr/bin/env python3 diff --git a/fe/access/__init__.py b/fe/access/__init__.py new file mode 100644 index 0000000..e5a0d9b --- /dev/null +++ b/fe/access/__init__.py @@ -0,0 +1 @@ +#!/usr/bin/env python3 diff --git a/fe/access/auth.py b/fe/access/auth.py new file mode 100644 index 0000000..04309fb --- /dev/null +++ b/fe/access/auth.py @@ -0,0 +1,49 @@ +import requests +from urllib.parse import urljoin + + +class Auth: + def __init__(self, url_prefix): + self.url_prefix = urljoin(url_prefix, "auth/") + + def login(self, user_id: str, password: str, terminal: str) -> (int, str): + json = {"user_id": user_id, "password": password, "terminal": terminal} + url = urljoin(self.url_prefix, "login") + r = requests.post(url, json=json) + return r.status_code, r.json().get("token") + + def register( + self, + user_id: str, + password: str + ) -> int: + json = { + "user_id": user_id, + "password": password + } + url = urljoin(self.url_prefix, "register") + r = requests.post(url, json=json) + return r.status_code + + def password(self, user_id: str, old_password: str, new_password: str) -> int: + json = { + "user_id": user_id, + "oldPassword": old_password, + "newPassword": new_password, + } + url = urljoin(self.url_prefix, "password") + r = requests.post(url, json=json) + return r.status_code + + def logout(self, user_id: str, token: str) -> int: + json = {"user_id": user_id} + headers = {"token": token} + url = urljoin(self.url_prefix, "logout") + r = requests.post(url, headers=headers, json=json) + return r.status_code + + def unregister(self, user_id: str, password: str) -> int: + json = {"user_id": user_id, "password": password} + url = urljoin(self.url_prefix, "unregister") + r = requests.post(url, json=json) + return r.status_code diff --git a/fe/access/book.py b/fe/access/book.py new file mode 100644 index 0000000..faa64bd --- /dev/null +++ b/fe/access/book.py @@ -0,0 +1,97 @@ +import os +import sqlite3 as sqlite +import random +import base64 +import simplejson as json + + +class Book: + id: str + title: str + author: str + publisher: str + original_title: str + translator: str + pub_year: str + pages: int + price: int + binding: str + isbn: str + author_intro: str + book_intro: str + content: str + tags: [str] + pictures: [bytes] + + def __init__(self): + self.tags = [] + self.pictures = [] + + +class BookDB: + def __init__(self, large: bool = False): + parent_path = os.path.dirname(os.path.dirname(__file__)) + self.db_s = os.path.join(parent_path, "data/book.db") + self.db_l = os.path.join(parent_path, "data/book_lx.db") + if large: + self.book_db = self.db_l + else: + self.book_db = self.db_s + + def get_book_count(self): + conn = sqlite.connect(self.book_db) + cursor = conn.execute( + "SELECT count(id) FROM book") + row = cursor.fetchone() + return row[0] + + def get_book_info(self, start, size) -> [Book]: + books = [] + conn = sqlite.connect(self.book_db) + cursor = conn.execute( + "SELECT id, title, author, " + "publisher, original_title, " + "translator, pub_year, pages, " + "price, currency_unit, binding, " + "isbn, author_intro, book_intro, " + "content, tags, picture FROM book ORDER BY id " + "LIMIT ? OFFSET ?", (size, start)) + for row in cursor: + book = Book() + book.id = row[0] + book.title = row[1] + book.author = row[2] + book.publisher = row[3] + book.original_title = row[4] + book.translator = row[5] + book.pub_year = row[6] + book.pages = row[7] + book.price = row[8] + + book.currency_unit = row[9] + book.binding = row[10] + book.isbn = row[11] + book.author_intro = row[12] + book.book_intro = row[13] + book.content = row[14] + tags = row[15] + + picture = row[16] + + for tag in tags.split("\n"): + if tag.strip() != "": + book.tags.append(tag) + for i in range(0, random.randint(0, 9)): + if picture is not None: + encode_str = base64.b64encode(picture).decode('utf-8') + book.pictures.append(encode_str) + books.append(book) + # print(tags.decode('utf-8')) + + # print(book.tags, len(book.picture)) + # print(book) + # print(tags) + + return books + + diff --git a/fe/access/buyer.py b/fe/access/buyer.py new file mode 100644 index 0000000..507c62a --- /dev/null +++ b/fe/access/buyer.py @@ -0,0 +1,42 @@ +import requests +import simplejson +from urllib.parse import urljoin +from fe.access.auth import Auth + + +class Buyer: + def __init__(self, url_prefix, user_id, password): + self.url_prefix = urljoin(url_prefix, "buyer/") + self.user_id = user_id + self.password = password + self.token = "" + self.terminal = "my terminal" + self.auth = Auth(url_prefix) + code, self.token = self.auth.login(self.user_id, self.password, self.terminal) + assert code == 200 + + def new_order(self, store_id: str, book_id_and_count: [(str, int)]) -> (int, str): + books = [] + for id_count_pair in book_id_and_count: + books.append({"id": id_count_pair[0], "count": id_count_pair[1]}) + json = {"user_id": self.user_id, "store_id": store_id, "books": books} + #print(simplejson.dumps(json)) + url = urljoin(self.url_prefix, "new_order") + headers = {"token": self.token} + r = requests.post(url, headers=headers, json=json) + response_json = r.json() + return r.status_code, response_json.get("order_id") + + def payment(self, order_id: str): + json = {"user_id": self.user_id, "password": self.password, "order_id": order_id} + url = urljoin(self.url_prefix, "payment") + headers = {"token": self.token} + r = requests.post(url, headers=headers, json=json) + return r.status_code + + def add_funds(self, add_value: str) -> int: + json = {"user_id": self.user_id, "password": self.password, "add_value": add_value} + url = urljoin(self.url_prefix, "add_funds") + headers = {"token": self.token} + r = requests.post(url, headers=headers, json=json) + return r.status_code diff --git a/fe/access/new_buyer.py b/fe/access/new_buyer.py new file mode 100644 index 0000000..d67092a --- /dev/null +++ b/fe/access/new_buyer.py @@ -0,0 +1,10 @@ +from fe import conf +from fe.access import buyer, auth + + +def register_new_buyer(user_id, password) -> buyer.Buyer: + a = auth.Auth(conf.URL) + code = a.register(user_id, password) + assert code == 200 + s = buyer.Buyer(conf.URL, user_id, password) + return s diff --git a/fe/access/new_seller.py b/fe/access/new_seller.py new file mode 100644 index 0000000..0782991 --- /dev/null +++ b/fe/access/new_seller.py @@ -0,0 +1,10 @@ +from fe import conf +from fe.access import seller, auth + + +def register_new_seller(user_id, password) -> seller.Seller: + a = auth.Auth(conf.URL) + code = a.register(user_id, password) + assert code == 200 + s = seller.Seller(conf.URL, user_id, password) + return s diff --git a/fe/access/seller.py b/fe/access/seller.py new file mode 100644 index 0000000..6e20ae8 --- /dev/null +++ b/fe/access/seller.py @@ -0,0 +1,52 @@ +import requests +from urllib.parse import urljoin +from fe.access import book +from fe.access.auth import Auth + + +class Seller: + def __init__(self, url_prefix, seller_id: str, password: str): + self.url_prefix = urljoin(url_prefix, "seller/") + self.seller_id = seller_id + self.password = password + self.terminal = "my terminal" + self.auth = Auth(url_prefix) + code, self.token = self.auth.login(self.seller_id, self.password, self.terminal) + assert code == 200 + + def create_store(self, store_id): + json = { + "user_id": self.seller_id, + "store_id": store_id, + } + #print(simplejson.dumps(json)) + url = urljoin(self.url_prefix, "create_store") + headers = {"token": self.token} + r = requests.post(url, headers=headers, json=json) + return r.status_code + + def add_book(self, store_id: str, stock_level: int, book_info: book.Book) -> int: + json = { + "user_id": self.seller_id, + "store_id": store_id, + "book_info": book_info.__dict__, + "stock_level": stock_level + } + #print(simplejson.dumps(json)) + url = urljoin(self.url_prefix, "add_book") + headers = {"token": self.token} + r = requests.post(url, headers=headers, json=json) + return r.status_code + + def add_stock_level(self, seller_id: str, store_id: str, book_id: str, add_stock_num: int) -> int: + json = { + "user_id": seller_id, + "store_id": store_id, + "book_id": book_id, + "add_stock_level": add_stock_num + } + #print(simplejson.dumps(json)) + url = urljoin(self.url_prefix, "add_stock_level") + headers = {"token": self.token} + r = requests.post(url, headers=headers, json=json) + return r.status_code diff --git a/fe/bench/__init__.py b/fe/bench/__init__.py new file mode 100644 index 0000000..e5a0d9b --- /dev/null +++ b/fe/bench/__init__.py @@ -0,0 +1 @@ +#!/usr/bin/env python3 diff --git a/fe/bench/bench.md b/fe/bench/bench.md new file mode 100644 index 0000000..ab70cd1 --- /dev/null +++ b/fe/bench/bench.md @@ -0,0 +1 @@ +add performance test here diff --git a/fe/bench/run.py b/fe/bench/run.py new file mode 100644 index 0000000..80043db --- /dev/null +++ b/fe/bench/run.py @@ -0,0 +1,22 @@ +from fe.bench.workload import Workload +from fe.bench.session import Session + + +def run_bench(): + wl = Workload() + wl.gen_database() + + sessions = [] + for i in range(0, wl.session): + ss = Session(wl) + sessions.append(ss) + + for ss in sessions: + ss.start() + + for ss in sessions: + ss.join() + + +#if __name__ == "__main__": +# run_bench() \ No newline at end of file diff --git a/fe/bench/session.py b/fe/bench/session.py new file mode 100644 index 0000000..e0ef1df --- /dev/null +++ b/fe/bench/session.py @@ -0,0 +1,53 @@ +from fe.bench.workload import Workload +from fe.bench.workload import NewOrder +from fe.bench.workload import Payment +import time +import threading + + +class Session(threading.Thread): + def __init__(self, wl: Workload): + threading.Thread.__init__(self) + self.workload = wl + self.new_order_request = [] + self.payment_request = [] + self.payment_i = 0 + self.new_order_i = 0 + self.payment_ok = 0 + self.new_order_ok = 0 + self.time_new_order = 0 + self.time_payment = 0 + self.thread = None + self.gen_procedure() + + def gen_procedure(self): + for i in range(0, self.workload.procedure_per_session): + new_order = self.workload.get_new_order() + self.new_order_request.append(new_order) + + def run(self): + self.run_gut() + + def run_gut(self): + for new_order in self.new_order_request: + before = time.time() + ok, order_id = new_order.run() + after = time.time() + self.time_new_order = self.time_new_order + after - before + self.new_order_i = self.new_order_i + 1 + if ok: + self.new_order_ok = self.new_order_ok + 1 + payment = Payment(new_order.buyer, order_id) + self.payment_request.append(payment) + if self.new_order_i % 100 or self.new_order_i == len(self.new_order_request): + self.workload.update_stat(self.new_order_i, self.payment_i, self.new_order_ok, self.payment_ok, + self.time_new_order, self.time_payment) + for payment in self.payment_request: + before = time.time() + ok = payment.run() + after = time.time() + self.time_payment = self.time_payment + after - before + self.payment_i = self.payment_i + 1 + if ok: + self.payment_ok = self.payment_ok + 1 + self.payment_request = [] diff --git a/fe/bench/workload.py b/fe/bench/workload.py new file mode 100644 index 0000000..d7c9e6e --- /dev/null +++ b/fe/bench/workload.py @@ -0,0 +1,166 @@ +import logging +import uuid +import random +import threading +from fe.access import book +from fe.access.new_seller import register_new_seller +from fe.access.new_buyer import register_new_buyer +from fe.access.buyer import Buyer +from fe import conf + + +class NewOrder: + def __init__(self, buyer: Buyer, store_id, book_id_and_count): + self.buyer = buyer + self.store_id = store_id + self.book_id_and_count = book_id_and_count + + def run(self) -> (bool, str): + code, order_id = self.buyer.new_order(self.store_id, self.book_id_and_count) + return code == 200, order_id + + +class Payment: + def __init__(self, buyer:Buyer, order_id): + self.buyer = buyer + self.order_id = order_id + + def run(self) -> bool: + code = self.buyer.payment(self.order_id) + return code == 200 + + +class Workload: + def __init__(self): + self.uuid = str(uuid.uuid1()) + self.book_ids = [] + self.buyer_ids = [] + self.store_ids = [] + self.book_db = book.BookDB(conf.Use_Large_DB) + self.row_count = self.book_db.get_book_count() + + self.book_num_per_store = conf.Book_Num_Per_Store + if self.row_count < self.book_num_per_store: + self.book_num_per_store = self.row_count + self.store_num_per_user = conf.Store_Num_Per_User + self.seller_num = conf.Seller_Num + self.buyer_num = conf.Buyer_Num + self.session = conf.Session + self.stock_level = conf.Default_Stock_Level + self.user_funds = conf.Default_User_Funds + self.batch_size = conf.Data_Batch_Size + self.procedure_per_session = conf.Request_Per_Session + + self.n_new_order = 0 + self.n_payment = 0 + self.n_new_order_ok = 0 + self.n_payment_ok = 0 + self.time_new_order = 0 + self.time_payment = 0 + self.lock = threading.Lock() + # 存储上一次的值,用于两次做差 + self.n_new_order_past = 0 + self.n_payment_past = 0 + self.n_new_order_ok_past = 0 + self.n_payment_ok_past = 0 + + def to_seller_id_and_password(self, no: int) -> (str, str): + return "seller_{}_{}".format(no, self.uuid), "password_seller_{}_{}".format(no, self.uuid) + + def to_buyer_id_and_password(self, no: int) -> (str, str): + return "buyer_{}_{}".format(no, self.uuid), "buyer_seller_{}_{}".format(no, self.uuid) + + def to_store_id(self, seller_no: int, i): + return "store_s_{}_{}_{}".format(seller_no, i, self.uuid) + + def gen_database(self): + logging.info("load data") + for i in range(1, self.seller_num + 1): + user_id, password = self.to_seller_id_and_password(i) + seller = register_new_seller(user_id, password) + for j in range(1, self.store_num_per_user + 1): + store_id = self.to_store_id(i, j) + code = seller.create_store(store_id) + assert code == 200 + self.store_ids.append(store_id) + row_no = 0 + + while row_no < self.book_num_per_store: + books = self.book_db.get_book_info(row_no, self.batch_size) + if len(books) == 0: + break + for bk in books: + code = seller.add_book(store_id, self.stock_level, bk) + assert code == 200 + if i == 1 and j == 1: + self.book_ids.append(bk.id) + row_no = row_no + len(books) + logging.info("seller data loaded.") + for k in range(1, self.buyer_num + 1): + user_id, password = self.to_buyer_id_and_password(k) + buyer = register_new_buyer(user_id, password) + buyer.add_funds(self.user_funds) + self.buyer_ids.append(user_id) + logging.info("buyer data loaded.") + + def get_new_order(self) -> NewOrder: + n = random.randint(1, self.buyer_num) + buyer_id, buyer_password = self.to_buyer_id_and_password(n) + store_no = int(random.uniform(0, len(self.store_ids) - 1)) + store_id = self.store_ids[store_no] + books = random.randint(1, 10) + book_id_and_count = [] + book_temp = [] + for i in range(0, books): + book_no = int(random.uniform(0, len(self.book_ids) - 1)) + book_id = self.book_ids[book_no] + if book_id in book_temp: + continue + else: + book_temp.append(book_id) + count = random.randint(1, 10) + book_id_and_count.append((book_id, count)) + b = Buyer(url_prefix=conf.URL, user_id=buyer_id, password=buyer_password) + new_ord = NewOrder(b, store_id, book_id_and_count) + return new_ord + + def update_stat(self, n_new_order, n_payment, + n_new_order_ok, n_payment_ok, + time_new_order, time_payment): + # 获取当前并发数 + thread_num = len(threading.enumerate()) + # 加索 + self.lock.acquire() + self.n_new_order = self.n_new_order + n_new_order + self.n_payment = self.n_payment + n_payment + self.n_new_order_ok = self.n_new_order_ok + n_new_order_ok + self.n_payment_ok = self.n_payment_ok + n_payment_ok + self.time_new_order = self.time_new_order + time_new_order + self.time_payment = self.time_payment + time_payment + # 计算这段时间内新创建订单的总数目 + n_new_order_diff = self.n_new_order - self.n_new_order_past + # 计算这段时间内新付款订单的总数目 + n_payment_diff = self.n_payment - self.n_payment_past + + if self.n_payment != 0 and self. n_new_order != 0 \ + and (self.time_payment + self.time_new_order): + # TPS_C(吞吐量):成功创建订单数量/(提交订单时间/提交订单并发数 + 提交付款订单时间/提交付款订单并发数) + # NO=OK:新创建订单数量 + # Thread_num:以新提交订单的数量作为并发数(这一次的TOTAL-上一次的TOTAL) + # TOTAL:总提交订单数量 + # LATENCY:提交订单时间/处理订单笔数(只考虑该线程延迟,未考虑并发) + # P=OK:新创建付款订单数量 + # Thread_num:以新提交付款订单的数量作为并发数(这一次的TOTAL-上一次的TOTAL) + # TOTAL:总付款提交订单数量 + # LATENCY:提交付款订单时间/处理付款订单笔数(只考虑该线程延迟,未考虑并发) + logging.info("TPS_C={}, NO=OK:{} Thread_num:{} TOTAL:{} LATENCY:{} , P=OK:{} Thread_num:{} TOTAL:{} LATENCY:{}".format( + int(self.n_new_order_ok / (self.time_payment / n_payment_diff + self.time_new_order / n_new_order_diff)), # 吞吐量:完成订单数/((付款所用时间+订单所用时间)/并发数) + self.n_new_order_ok, n_new_order_diff, self.n_new_order, self.time_new_order / self.n_new_order, # 订单延迟:(创建订单所用时间/并发数)/新创建订单数 + self.n_payment_ok, n_payment_diff, self.n_payment, self.time_payment / self.n_payment # 付款延迟:(付款所用时间/并发数)/付款订单数 + )) + self.lock.release() + # 旧值更新为新值,便于下一轮计算 + self.n_new_order_past = self.n_new_order + self.n_payment_past = self.n_payment + self.n_new_order_ok_past = self.n_new_order_ok + self.n_payment_ok_past = self.n_payment_ok diff --git a/fe/conf.py b/fe/conf.py new file mode 100644 index 0000000..a0fbb2b --- /dev/null +++ b/fe/conf.py @@ -0,0 +1,11 @@ +URL = "http://127.0.0.1:5000/" +Book_Num_Per_Store = 2000 +Store_Num_Per_User = 2 +Seller_Num = 2 +Buyer_Num = 10 +Session = 1 +Request_Per_Session = 1000 +Default_Stock_Level = 1000000 +Default_User_Funds = 10000000 +Data_Batch_Size = 100 +Use_Large_DB = False diff --git a/fe/conftest.py b/fe/conftest.py new file mode 100644 index 0000000..e93cee2 --- /dev/null +++ b/fe/conftest.py @@ -0,0 +1,27 @@ +import requests +import threading +from urllib.parse import urljoin +from be import serve +from fe import conf + +thread: threading.Thread = None + + +# 修改这里启动后端程序,如果不需要可删除这行代码 +def run_backend(): + # rewrite this if rewrite backend + serve.be_run() + + +def pytest_configure(config): + global thread + print("frontend begin test") + thread = threading.Thread(target=run_backend) + thread.start() + + +def pytest_unconfigure(config): + url = urljoin(conf.URL, "shutdown") + requests.get(url) + thread.join() + print("frontend end test") diff --git a/fe/data/book.db b/fe/data/book.db new file mode 100644 index 0000000..5ee601c Binary files /dev/null and b/fe/data/book.db differ diff --git a/fe/data/scraper.py b/fe/data/scraper.py new file mode 100644 index 0000000..c85e3d1 --- /dev/null +++ b/fe/data/scraper.py @@ -0,0 +1,425 @@ +# coding=utf-8 + +from lxml import etree +import sqlite3 +import re +import requests +import random +import time +import logging + +user_agent = [ + "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; en-us) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 " + "Safari/534.50", + "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-us) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 " + "Safari/534.50", + "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:38.0) Gecko/20100101 Firefox/38.0", + "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; .NET4.0C; .NET4.0E; .NET CLR 2.0.50727; .NET CLR " + "3.0.30729; .NET CLR 3.5.30729; InfoPath.3; rv:11.0) like Gecko", + "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)", + "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0)", + "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0)", + "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:2.0.1) Gecko/20100101 Firefox/4.0.1", + "Mozilla/5.0 (Windows NT 6.1; rv:2.0.1) Gecko/20100101 Firefox/4.0.1", + "Opera/9.80 (Macintosh; Intel Mac OS X 10.6.8; U; en) Presto/2.8.131 Version/11.11", + "Opera/9.80 (Windows NT 6.1; U; en) Presto/2.8.131 Version/11.11", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_0) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 " + "Safari/535.11", + "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Maxthon 2.0)", + "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; TencentTraveler 4.0)", + "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)", + "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; The World)", + "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 " + "CLR 2.0.50727; SE 2.X MetaSr 1.0)", + "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; 360SE)", + "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Avant Browser)", + "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)", + "Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_3_3 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) " + "Version/5.0.2 Mobile/8J2 Safari/6533.18.5", + "Mozilla/5.0 (iPod; U; CPU iPhone OS 4_3_3 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) " + "Version/5.0.2 Mobile/8J2 Safari/6533.18.5", + "Mozilla/5.0 (iPad; U; CPU OS 4_3_3 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) " + "Version/5.0.2 Mobile/8J2 Safari/6533.18.5", + "Mozilla/5.0 (Linux; U; Android 2.3.7; en-us; Nexus One Build/FRF91) AppleWebKit/533.1 (KHTML, like Gecko) " + "Version/4.0 Mobile Safari/533.1", + "MQQBrowser/26 Mozilla/5.0 (Linux; U; Android 2.3.7; zh-cn; MB200 Build/GRJ22; CyanogenMod-7) " + "AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1", + "Opera/9.80 (Android 2.3.4; Linux; Opera Mobi/build-1107180945; U; en-GB) Presto/2.8.149 Version/11.10", + "Mozilla/5.0 (Linux; U; Android 3.0; en-us; Xoom Build/HRI39) AppleWebKit/534.13 (KHTML, like Gecko) " + "Version/4.0 Safari/534.13", + "Mozilla/5.0 (BlackBerry; U; BlackBerry 9800; en) AppleWebKit/534.1+ (KHTML, like Gecko) Version/6.0.0.337 " + "Mobile Safari/534.1+", + "Mozilla/5.0 (hp-tablet; Linux; hpwOS/3.0.0; U; en-US) AppleWebKit/534.6 (KHTML, like Gecko) " + "wOSBrowser/233.70 Safari/534.6 TouchPad/1.0", + "Mozilla/5.0 (SymbianOS/9.4; Series60/5.0 NokiaN97-1/20.0.019; Profile/MIDP-2.1 Configuration/CLDC-1.1) " + "AppleWebKit/525 (KHTML, like Gecko) BrowserNG/7.1.18124", + "Mozilla/5.0 (compatible; MSIE 9.0; Windows Phone OS 7.5; Trident/5.0; IEMobile/9.0; HTC; Titan)", + "UCWEB7.0.2.37/28/999", + "NOKIA5700/ UCWEB7.0.2.37/28/999", + "Openwave/ UCWEB7.0.2.37/28/999", + "Mozilla/4.0 (compatible; MSIE 6.0; ) Opera/UCWEB7.0.2.37/28/999", + # iPhone 6: + "Mozilla/6.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/8.0 " + "Mobile/10A5376e Safari/8536.25", +] + + +def get_user_agent(): + headers = {"User-Agent": random.choice(user_agent)} + return headers + + +class Scraper: + database: str + tag: str + page: int + + def __init__(self): + self.database = "book.db" + self.tag = "" + self.page = 0 + self.pattern_number = re.compile(r"\d+\.?\d*") + logging.basicConfig(filename="scraper.log", level=logging.ERROR) + + def get_current_progress(self) -> (): + conn = sqlite3.connect(self.database) + results = conn.execute("SELECT tag, page from progress where id = '0'") + for row in results: + return row[0], row[1] + return "", 0 + + def save_current_progress(self, current_tag, current_page): + conn = sqlite3.connect(self.database) + conn.execute( + "UPDATE progress set tag = '{}', page = {} where id = '0'".format( + current_tag, current_page + ) + ) + conn.commit() + conn.close() + + def start_grab(self) -> bool: + self.create_tables() + scraper.grab_tag() + current_tag, current_page = self.get_current_progress() + tags = self.get_tag_list() + for i in range(0, len(tags)): + no = 0 + if i == 0 and current_tag == tags[i]: + no = current_page + while self.grab_book_list(tags[i], no): + no = no + 20 + return True + + def create_tables(self): + conn = sqlite3.connect(self.database) + try: + conn.execute("CREATE TABLE tags (tag TEXT PRIMARY KEY)") + conn.commit() + except sqlite3.Error as e: + logging.error(str(e)) + conn.rollback() + + try: + conn.execute( + "CREATE TABLE book (" + "id TEXT PRIMARY KEY, title TEXT, author TEXT, " + "publisher TEXT, original_title TEXT, " + "translator TEXT, pub_year TEXT, pages INTEGER, " + "price INTEGER, currency_unit TEXT, binding TEXT, " + "isbn TEXT, author_intro TEXT, book_intro text, " + "content TEXT, tags TEXT, picture BLOB)" + ) + conn.commit() + except sqlite3.Error as e: + logging.error(str(e)) + conn.rollback() + + try: + conn.execute( + "CREATE TABLE progress (id TEXT PRIMARY KEY, tag TEXT, page integer )" + ) + conn.execute("INSERT INTO progress values('0', '', 0)") + conn.commit() + except sqlite3.Error as e: + logging.error(str(e)) + conn.rollback() + + def grab_tag(self): + url = "https://book.douban.com/tag/?view=cloud" + r = requests.get(url, headers=get_user_agent()) + r.encoding = "utf-8" + h: etree.ElementBase = etree.HTML(r.text) + tags: [] = h.xpath( + '/html/body/div[@id="wrapper"]/div[@id="content"]' + '/div[@class="grid-16-8 clearfix"]/div[@class="article"]' + '/div[@class=""]/div[@class="indent tag_cloud"]' + "/table/tbody/tr/td/a/@href" + ) + conn = sqlite3.connect(self.database) + c = conn.cursor() + try: + for tag in tags: + t: str = tag.strip("/tag") + c.execute("INSERT INTO tags VALUES ('{}')".format(t)) + c.close() + conn.commit() + conn.close() + except sqlite3.Error as e: + logging.error(str(e)) + conn.rollback() + return False + return True + + def grab_book_list(self, tag="小说", pageno=1) -> bool: + logging.info("start to grab tag {} page {}...".format(tag, pageno)) + self.save_current_progress(tag, pageno) + url = "https://book.douban.com/tag/{}?start={}&type=T".format(tag, pageno) + r = requests.get(url, headers=get_user_agent()) + r.encoding = "utf-8" + h: etree.Element = etree.HTML(r.text) + + li_list: [] = h.xpath( + '/html/body/div[@id="wrapper"]/div[@id="content"]' + '/div[@class="grid-16-8 clearfix"]' + '/div[@class="article"]/div[@id="subject_list"]' + '/ul/li/div[@class="info"]/h2/a/@href' + ) + next_page = h.xpath( + '/html/body/div[@id="wrapper"]/div[@id="content"]' + '/div[@class="grid-16-8 clearfix"]' + '/div[@class="article"]/div[@id="subject_list"]' + '/div[@class="paginator"]/span[@class="next"]/a[@href]' + ) + has_next = True + if len(next_page) == 0: + has_next = False + if len(li_list) == 0: + return False + + for li in li_list: + li.strip("") + book_id = li.strip("/").split("/")[-1] + try: + delay = float(random.randint(0, 200)) / 100.0 + time.sleep(delay) + self.crow_book_info(book_id) + except BaseException as e: + logging.error( + logging.error("error when scrape {}, {}".format(book_id, str(e))) + ) + return has_next + + def get_tag_list(self) -> [str]: + ret = [] + conn = sqlite3.connect(self.database) + results = conn.execute( + "SELECT tags.tag from tags join progress where tags.tag >= progress.tag" + ) + for row in results: + ret.append(row[0]) + return ret + + def crow_book_info(self, book_id) -> bool: + conn = sqlite3.connect(self.database) + for _ in conn.execute("SELECT id from book where id = ('{}')".format(book_id)): + return + + url = "https://book.douban.com/subject/{}/".format(book_id) + r = requests.get(url, headers=get_user_agent()) + r.encoding = "utf-8" + h: etree.Element = etree.HTML(r.text) + e_text = h.xpath('/html/body/div[@id="wrapper"]/h1/span/text()') + if len(e_text) == 0: + return False + + title = e_text[0] + + elements = h.xpath( + '/html/body/div[@id="wrapper"]' + '/div[@id="content"]/div[@class="grid-16-8 clearfix"]' + '/div[@class="article"]' + ) + if len(elements) == 0: + return False + + e_article = elements[0] + + book_intro = "" + author_intro = "" + content = "" + tags = "" + + e_book_intro = e_article.xpath( + 'div[@class="related_info"]' + '/div[@class="indent"][@id="link-report"]/*' + '/div[@class="intro"]/*/text()' + ) + for line in e_book_intro: + line = line.strip() + if line != "": + book_intro = book_intro + line + "\n" + + e_author_intro = e_article.xpath( + 'div[@class="related_info"]' + '/div[@class="indent "]/*' + '/div[@class="intro"]/*/text()' + ) + for line in e_author_intro: + line = line.strip() + if line != "": + author_intro = author_intro + line + "\n" + + e_content = e_article.xpath( + 'div[@class="related_info"]' + '/div[@class="indent"][@id="dir_' + book_id + '_full"]/text()' + ) + for line in e_content: + line = line.strip() + if line != "": + content = content + line + "\n" + + e_tags = e_article.xpath( + 'div[@class="related_info"]/' + 'div[@id="db-tags-section"]/' + 'div[@class="indent"]/span/a/text()' + ) + for line in e_tags: + line = line.strip() + if line != "": + tags = tags + line + "\n" + + e_subject = e_article.xpath( + 'div[@class="indent"]' + '/div[@class="subjectwrap clearfix"]' + '/div[@class="subject clearfix"]' + ) + pic_href = e_subject[0].xpath('div[@id="mainpic"]/a/@href') + picture = None + if len(pic_href) > 0: + res = requests.get(pic_href[0], headers=get_user_agent()) + picture = res.content + + info_children = e_subject[0].xpath('div[@id="info"]/child::node()') + + e_array = [] + e_dict = dict() + + for e in info_children: + if isinstance(e, etree._ElementUnicodeResult): + e_dict["text"] = e + elif isinstance(e, etree._Element): + if e.tag == "br": + e_array.append(e_dict) + e_dict = dict() + else: + e_dict[e.tag] = e + + book_info = dict() + for d in e_array: + label = "" + span = d.get("span") + a_label = span.xpath("span/text()") + if len(a_label) > 0 and label == "": + label = a_label[0].strip() + a_label = span.xpath("text()") + if len(a_label) > 0 and label == "": + label = a_label[0].strip() + label = label.strip(":") + text = d.get("text").strip() + e_a = d.get("a") + text.strip() + text.strip(":") + if label == "作者" or label == "译者": + a = span.xpath("a/text()") + if text == "" and len(a) == 1: + text = a[0].strip() + if text == "" and e_a is not None: + text_a = e_a.xpath("text()") + if len(text_a) > 0: + text = text_a[0].strip() + text = re.sub(r"\s+", " ", text) + if text != "": + book_info[label] = text + + sql = ( + "INSERT INTO book(" + "id, title, author, " + "publisher, original_title, translator, " + "pub_year, pages, price, " + "currency_unit, binding, isbn, " + "author_intro, book_intro, content, " + "tags, picture)" + "VALUES(" + "?, ?, ?, " + "?, ?, ?, " + "?, ?, ?, " + "?, ?, ?, " + "?, ?, ?, " + "?, ?)" + ) + + unit = None + price = None + pages = None + conn = sqlite3.connect(self.database) + try: + s_price = book_info.get("定价") + if s_price is None: + # price cannot be NULL + logging.error( + "error when scrape book_id {}, cannot retrieve price...", book_id + ) + return None + else: + e = re.findall(self.pattern_number, s_price) + if len(e) != 0: + number = e[0] + unit = s_price.replace(number, "").strip() + price = int(float(number) * 100) + + s_pages = book_info.get("页数") + if s_pages is not None: + # pages can be NULL + e = re.findall(self.pattern_number, s_pages) + if len(e) != 0: + pages = int(e[0]) + + conn.execute( + sql, + ( + book_id, + title, + book_info.get("作者"), + book_info.get("出版社"), + book_info.get("原作名"), + book_info.get("译者"), + book_info.get("出版年"), + pages, + price, + unit, + book_info.get("装帧"), + book_info.get("ISBN"), + author_intro, + book_intro, + content, + tags, + picture, + ), + ) + conn.commit() + except sqlite3.Error as e: + logging(str(e)) + conn.rollback() + except TypeError as e: + logging.error("error when scrape {}, {}".format(book_id, str(e))) + conn.rollback() + return False + conn.close() + return True + + +if __name__ == "__main__": + scraper = Scraper() + scraper.start_grab() diff --git a/fe/test/gen_book_data.py b/fe/test/gen_book_data.py new file mode 100644 index 0000000..4bda418 --- /dev/null +++ b/fe/test/gen_book_data.py @@ -0,0 +1,57 @@ +import random +from fe.access import book +from fe.access.new_seller import register_new_seller + + +class GenBook: + def __init__(self, user_id, store_id): + self.user_id = user_id + self.store_id = store_id + self.password = self.user_id + self.seller = register_new_seller(self.user_id, self.password) + code = self.seller.create_store(store_id) + assert code == 200 + self.__init_book_list__() + + def __init_book_list__(self): + self.buy_book_info_list = [] + self.buy_book_id_list = [] + + def gen(self, non_exist_book_id: bool, low_stock_level, max_book_count: int = 100) -> (bool, []): + self.__init_book_list__() + ok = True + book_db = book.BookDB() + rows = book_db.get_book_count() + start = 0 + if rows > max_book_count: + start = random.randint(0, rows - max_book_count) + size = random.randint(1, max_book_count) + books = book_db.get_book_info(start, size) + book_id_exist = [] + book_id_stock_level = {} + for bk in books: + if low_stock_level: + stock_level = random.randint(0, 100) + else: + stock_level = random.randint(2, 100) + code = self.seller.add_book(self.store_id, stock_level, bk) + assert code == 200 + book_id_stock_level[bk.id] = stock_level + book_id_exist.append(bk) + + for bk in book_id_exist: + stock_level = book_id_stock_level[bk.id] + if stock_level > 1: + buy_num = random.randint(1, stock_level) + else: + buy_num = 0 + # add a new pair + if non_exist_book_id: + bk.id = bk.id + "_x" + if low_stock_level: + buy_num = stock_level + 1 + self.buy_book_info_list.append((bk, buy_num)) + + for item in self.buy_book_info_list: + self.buy_book_id_list.append((item[0].id, item[1])) + return ok, self.buy_book_id_list diff --git a/fe/test/test.md b/fe/test/test.md new file mode 100644 index 0000000..c7fe16e --- /dev/null +++ b/fe/test/test.md @@ -0,0 +1 @@ +add functionality test here diff --git a/fe/test/test_add_book.py b/fe/test/test_add_book.py new file mode 100644 index 0000000..8491c4d --- /dev/null +++ b/fe/test/test_add_book.py @@ -0,0 +1,51 @@ +import pytest + +from fe.access.new_seller import register_new_seller +from fe.access import book +import uuid + + +class TestAddBook: + @pytest.fixture(autouse=True) + def pre_run_initialization(self): + # do before test + self.seller_id = "test_add_books_seller_id_{}".format(str(uuid.uuid1())) + self.store_id = "test_add_books_store_id_{}".format(str(uuid.uuid1())) + self.password = self.seller_id + self.seller = register_new_seller(self.seller_id, self.password) + + code = self.seller.create_store(self.store_id) + assert code == 200 + book_db = book.BookDB() + self.books = book_db.get_book_info(0, 2) + + yield + # do after test + + def test_ok(self): + for b in self.books: + code = self.seller.add_book(self.store_id, 0, b) + assert code == 200 + + def test_error_non_exist_store_id(self): + for b in self.books: + # non exist store id + code = self.seller.add_book(self.store_id + "x", 0, b) + assert code != 200 + + def test_error_exist_book_id(self): + for b in self.books: + code = self.seller.add_book(self.store_id, 0, b) + assert code == 200 + for b in self.books: + # exist book id + code = self.seller.add_book(self.store_id, 0, b) + assert code != 200 + + def test_error_non_exist_user_id(self): + for b in self.books: + # non exist user id + self.seller.seller_id = self.seller.seller_id + "_x" + code = self.seller.add_book(self.store_id, 0, b) + assert code != 200 + diff --git a/fe/test/test_add_funds.py b/fe/test/test_add_funds.py new file mode 100644 index 0000000..de5b525 --- /dev/null +++ b/fe/test/test_add_funds.py @@ -0,0 +1,29 @@ +import pytest +import uuid +from fe.access.new_buyer import register_new_buyer + + +class TestAddFunds: + @pytest.fixture(autouse=True) + def pre_run_initialization(self): + self.user_id = "test_add_funds_{}".format(str(uuid.uuid1())) + self.password = self.user_id + self.buyer = register_new_buyer(self.user_id, self.password) + yield + + def test_ok(self): + code = self.buyer.add_funds(1000) + assert code == 200 + + code = self.buyer.add_funds(-1000) + assert code == 200 + + def test_error_user_id(self): + self.buyer.user_id = self.buyer.user_id + "_x" + code = self.buyer.add_funds(10) + assert code != 200 + + def test_error_password(self): + self.buyer.password = self.buyer.password + "_x" + code = self.buyer.add_funds(10) + assert code != 200 diff --git a/fe/test/test_add_stock_level.py b/fe/test/test_add_stock_level.py new file mode 100644 index 0000000..ea61f06 --- /dev/null +++ b/fe/test/test_add_stock_level.py @@ -0,0 +1,46 @@ +import pytest +from fe.access.new_seller import register_new_seller +from fe.access import book +import uuid + + +class TestAddStockLevel: + @pytest.fixture(autouse=True) + def pre_run_initialization(self): + self.user_id = "test_add_book_stock_level1_user_{}".format(str(uuid.uuid1())) + self.store_id = "test_add_book_stock_level1_store_{}".format(str(uuid.uuid1())) + self.password = self.user_id + self.seller = register_new_seller(self.user_id, self.password) + + code = self.seller.create_store(self.store_id) + assert code == 200 + book_db = book.BookDB() + self.books = book_db.get_book_info(0, 5) + for bk in self.books: + code = self.seller.add_book(self.store_id, 0, bk) + assert code == 200 + yield + + def test_error_user_id(self): + for b in self.books: + book_id = b.id + code = self.seller.add_stock_level(self.user_id + "_x", self.store_id, book_id, 10) + assert code != 200 + + def test_error_store_id(self): + for b in self.books: + book_id = b.id + code = self.seller.add_stock_level(self.user_id, self.store_id + "_x", book_id, 10) + assert code != 200 + + def test_error_book_id(self): + for b in self.books: + book_id = b.id + code = self.seller.add_stock_level(self.user_id, self.store_id, book_id + "_x", 10) + assert code != 200 + + def test_ok(self): + for b in self.books: + book_id = b.id + code = self.seller.add_stock_level(self.user_id, self.store_id, book_id, 10) + assert code == 200 diff --git a/fe/test/test_bench.py b/fe/test/test_bench.py new file mode 100644 index 0000000..969c18b --- /dev/null +++ b/fe/test/test_bench.py @@ -0,0 +1,8 @@ +from fe.bench.run import run_bench + + +def test_bench(): + try: + run_bench() + except Exception as e: + assert 200==100,"test_bench过程出现异常" \ No newline at end of file diff --git a/fe/test/test_create_store.py b/fe/test/test_create_store.py new file mode 100644 index 0000000..fc48eb0 --- /dev/null +++ b/fe/test/test_create_store.py @@ -0,0 +1,26 @@ +import pytest + +from fe.access.new_seller import register_new_seller +import uuid + + +class TestCreateStore: + @pytest.fixture(autouse=True) + def pre_run_initialization(self): + self.user_id = "test_create_store_user_{}".format(str(uuid.uuid1())) + self.store_id = "test_create_store_store_{}".format(str(uuid.uuid1())) + self.password = self.user_id + yield + + def test_ok(self): + self.seller = register_new_seller(self.user_id, self.password) + code = self.seller.create_store(self.store_id) + assert code == 200 + + def test_error_exist_store_id(self): + self.seller = register_new_seller(self.user_id, self.password) + code = self.seller.create_store(self.store_id) + assert code == 200 + + code = self.seller.create_store(self.store_id) + assert code != 200 diff --git a/fe/test/test_login.py b/fe/test/test_login.py new file mode 100644 index 0000000..ee5a118 --- /dev/null +++ b/fe/test/test_login.py @@ -0,0 +1,39 @@ +import time + +import pytest + +from fe.access import auth +from fe import conf + + +class TestLogin: + @pytest.fixture(autouse=True) + def pre_run_initialization(self): + self.auth = auth.Auth(conf.URL) + # register a user + self.user_id = "test_login_{}".format(time.time()) + self.password = "password_" + self.user_id + self.terminal = "terminal_" + self.user_id + assert self.auth.register(self.user_id, self.password) == 200 + yield + + def test_ok(self): + code, token = self.auth.login(self.user_id, self.password, self.terminal) + assert code == 200 + + code = self.auth.logout(self.user_id + "_x", token) + assert code == 401 + + code = self.auth.logout(self.user_id, token + "_x") + assert code == 401 + + code = self.auth.logout(self.user_id, token) + assert code == 200 + + def test_error_user_id(self): + code, token = self.auth.login(self.user_id + "_x", self.password, self.terminal) + assert code == 401 + + def test_error_password(self): + code, token = self.auth.login(self.user_id, self.password + "_x", self.terminal) + assert code == 401 diff --git a/fe/test/test_new_order.py b/fe/test/test_new_order.py new file mode 100644 index 0000000..288bc57 --- /dev/null +++ b/fe/test/test_new_order.py @@ -0,0 +1,48 @@ +import pytest + +from fe.test.gen_book_data import GenBook +from fe.access.new_buyer import register_new_buyer +import uuid + + +class TestNewOrder: + @pytest.fixture(autouse=True) + def pre_run_initialization(self): + self.seller_id = "test_new_order_seller_id_{}".format(str(uuid.uuid1())) + self.store_id = "test_new_order_store_id_{}".format(str(uuid.uuid1())) + self.buyer_id = "test_new_order_buyer_id_{}".format(str(uuid.uuid1())) + self.password = self.seller_id + self.buyer = register_new_buyer(self.buyer_id, self.password) + self.gen_book = GenBook(self.seller_id, self.store_id) + yield + + def test_non_exist_book_id(self): + ok, buy_book_id_list = self.gen_book.gen(non_exist_book_id=True, low_stock_level=False) + assert ok + code, _ = self.buyer.new_order(self.store_id, buy_book_id_list) + assert code != 200 + + def test_low_stock_level(self): + ok, buy_book_id_list = self.gen_book.gen(non_exist_book_id=False, low_stock_level=True) + assert ok + code, _ = self.buyer.new_order(self.store_id, buy_book_id_list) + assert code != 200 + + def test_ok(self): + ok, buy_book_id_list = self.gen_book.gen(non_exist_book_id=False, low_stock_level=False) + assert ok + code, _ = self.buyer.new_order(self.store_id, buy_book_id_list) + assert code == 200 + + def test_non_exist_user_id(self): + ok, buy_book_id_list = self.gen_book.gen(non_exist_book_id=False, low_stock_level=False) + assert ok + self.buyer.user_id = self.buyer.user_id + "_x" + code, _ = self.buyer.new_order(self.store_id, buy_book_id_list) + assert code != 200 + + def test_non_exist_store_id(self): + ok, buy_book_id_list = self.gen_book.gen(non_exist_book_id=False, low_stock_level=False) + assert ok + code, _ = self.buyer.new_order(self.store_id + "_x", buy_book_id_list) + assert code != 200 diff --git a/fe/test/test_password.py b/fe/test/test_password.py new file mode 100644 index 0000000..8ef999d --- /dev/null +++ b/fe/test/test_password.py @@ -0,0 +1,47 @@ +import uuid + +import pytest + +from fe.access import auth +from fe import conf + + +class TestPassword: + @pytest.fixture(autouse=True) + def pre_run_initialization(self): + self.auth = auth.Auth(conf.URL) + # register a user + self.user_id = "test_password_{}".format(str(uuid.uuid1())) + self.old_password = "old_password_" + self.user_id + self.new_password = "new_password_" + self.user_id + self.terminal = "terminal_" + self.user_id + + assert self.auth.register(self.user_id, self.old_password) == 200 + yield + + def test_ok(self): + code = self.auth.password(self.user_id, self.old_password, self.new_password) + assert code == 200 + + code, new_token = self.auth.login(self.user_id, self.old_password, self.terminal) + assert code != 200 + + code, new_token = self.auth.login(self.user_id, self.new_password, self.terminal) + assert code == 200 + + code = self.auth.logout(self.user_id, new_token) + assert code == 200 + + def test_error_password(self): + code = self.auth.password(self.user_id, self.old_password + "_x", self.new_password) + assert code != 200 + + code, new_token = self.auth.login(self.user_id, self.new_password, self.terminal) + assert code != 200 + + def test_error_user_id(self): + code = self.auth.password(self.user_id + "_x", self.old_password, self.new_password) + assert code != 200 + + code, new_token = self.auth.login(self.user_id, self.new_password, self.terminal) + assert code != 200 diff --git a/fe/test/test_payment.py b/fe/test/test_payment.py new file mode 100644 index 0000000..e7f54b6 --- /dev/null +++ b/fe/test/test_payment.py @@ -0,0 +1,70 @@ +import pytest + +from fe.access.buyer import Buyer +from fe.test.gen_book_data import GenBook +from fe.access.new_buyer import register_new_buyer +from fe.access.book import Book +import uuid + + +class TestPayment: + seller_id: str + store_id: str + buyer_id: str + password:str + buy_book_info_list: [Book] + total_price: int + order_id: str + buyer: Buyer + + @pytest.fixture(autouse=True) + def pre_run_initialization(self): + self.seller_id = "test_payment_seller_id_{}".format(str(uuid.uuid1())) + self.store_id = "test_payment_store_id_{}".format(str(uuid.uuid1())) + self.buyer_id = "test_payment_buyer_id_{}".format(str(uuid.uuid1())) + self.password = self.seller_id + gen_book = GenBook(self.seller_id, self.store_id) + ok, buy_book_id_list = gen_book.gen(non_exist_book_id=False, low_stock_level=False, max_book_count=5) + self.buy_book_info_list = gen_book.buy_book_info_list + assert ok + b = register_new_buyer(self.buyer_id, self.password) + self.buyer = b + code, self.order_id = b.new_order(self.store_id, buy_book_id_list) + assert code == 200 + self.total_price = 0 + for item in self.buy_book_info_list: + book: Book = item[0] + num = item[1] + if book.price is None: + continue + else: + self.total_price = self.total_price + book.price * num + yield + + def test_ok(self): + code = self.buyer.add_funds(self.total_price) + assert code == 200 + code = self.buyer.payment(self.order_id) + assert code == 200 + + def test_authorization_error(self): + code = self.buyer.add_funds(self.total_price) + assert code == 200 + self.buyer.password = self.buyer.password + "_x" + code = self.buyer.payment(self.order_id) + assert code != 200 + + def test_not_suff_funds(self): + code = self.buyer.add_funds(self.total_price - 1) + assert code == 200 + code = self.buyer.payment(self.order_id) + assert code != 200 + + def test_repeat_pay(self): + code = self.buyer.add_funds(self.total_price) + assert code == 200 + code = self.buyer.payment(self.order_id) + assert code == 200 + + code = self.buyer.payment(self.order_id) + assert code != 200 diff --git a/fe/test/test_register.py b/fe/test/test_register.py new file mode 100644 index 0000000..42acc5e --- /dev/null +++ b/fe/test/test_register.py @@ -0,0 +1,43 @@ +import time + +import pytest + +from fe.access import auth +from fe import conf + + +class TestRegister: + @pytest.fixture(autouse=True) + def pre_run_initialization(self): + self.user_id = "test_register_user_{}".format(time.time()) + self.password = "test_register_password_{}".format(time.time()) + self.auth = auth.Auth(conf.URL) + yield + + def test_register_ok(self): + code = self.auth.register(self.user_id, self.password) + assert code == 200 + + def test_unregister_ok(self): + code = self.auth.register(self.user_id, self.password) + assert code == 200 + + code = self.auth.unregister(self.user_id, self.password) + assert code == 200 + + def test_unregister_error_authorization(self): + code = self.auth.register(self.user_id, self.password) + assert code == 200 + + code = self.auth.unregister(self.user_id + "_x", self.password) + assert code != 200 + + code = self.auth.unregister(self.user_id, self.password + "_x") + assert code != 200 + + def test_register_error_exist_user_id(self): + code = self.auth.register(self.user_id, self.password) + assert code == 200 + + code = self.auth.register(self.user_id, self.password) + assert code != 200 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f25106e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,9 @@ +simplejson +lxml +codecov +coverage +flask +pre-commit +pytest +PyJWT +requests diff --git a/script/test.sh b/script/test.sh new file mode 100644 index 0000000..ae7bfb0 --- /dev/null +++ b/script/test.sh @@ -0,0 +1,6 @@ +#!/bin/sh +export PATHONPATH=`pwd` +coverage run --timid --branch --source fe,be --concurrency=thread -m pytest -v --ignore=fe/data +coverage combine +coverage report +coverage html diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..3910635 --- /dev/null +++ b/setup.py @@ -0,0 +1,22 @@ +import setuptools + +with open("README.md", "r") as fh: + long_description = fh.read() + +setuptools.setup( + name="bookstore", + version="0.0.1", + author="DaSE-DBMS", + author_email="DaSE-DBMS@DaSE-DBMS.com", + description="Buy Books Online", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/DaSE-DBMS/bookstore.git", + packages=setuptools.find_packages(), + classifiers=[ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + ], + python_requires=">=3.6", +)