## <center> 华东师范大学数据科学与工程学院实验报告

| **课程名称：当代数据库管理系统** |**年级** ：2020级 |**上机实践成绩**： |
| --- | --- | --- |
| **指导教师** ：高明 | **姓名** ：杨舜、姚嘉和 | **学号** ：10205501415、10205501436 |
| **上机实践名称** ：BookStore ||**上机实践日期**：2022.11.28 —— 2022.12.10 |
| **上机实践编号** ： | **组号 ：21** |**上机实践时间**：2022.11.28 —— 2022.12.10 |



### 实验过程

#### 一. 分析原有的数据库结构

   分析demo中/be/model/store.py中创建数据库表的sql语句可知原有数据库的结构的ER图大致如下
   ![avatar](./figure_require/DB_struct_origin.svg)

   有上述ER图可以得到原有数据库表如下
   
   user表：
   |user_id|password|balance|token|terminal|
   |---|---|---|---|---|
   主键为user_id

   store表：
   |store_id|stock_level|
   |---|---|
   主键为store_id
   
   store_book表：
   |store_id|book_id|book_info|stock_level|
   |---|---|---|---|
   主键为联合主键(store_id,book_id)

   user_store表：
   |user_id|store_id|
   |---|---|
   外键为(user_id,store_id)

   new_order表：
   |order_id|user_id|store_id|
   |---|---|---|
   主键为（order_id）

   new_order_detail表：
   |oeder_id|book_id|count|price|
   |---|---|---|---|
   主键为联合主键(order_id,book_id)
   
   &nbsp;

#### 二. 依据上述分析构建数据库table（前60%）
   1. 利用sqlalchemy连接远程的aliyun数据库并创建上述table [postgreSQLORM.py](./modified/be/postgreSQLORM.py)
   ```python
   class con:
    def connect():
        '''Returns a connection and a metadata object'''
        # We connect with the help of the PostgreSQL URL
        
        url = 'postgresql://stu10205501415:Stu10205501415@dase-cdms-2022-pub.pg.rds.aliyuncs.com:5432/stu10205501415'

        # The return value of create_engine() is our connection object
        con = create_engine(url, client_encoding='utf8')

        # We then bind the connection to MetaData()
        meta = MetaData(bind=con)

        return con, meta

   class User(Base):
     __tablename__ = 'user'
    user_id = Column(TEXT, primary_key=True, comment="主键")
    password = Column(TEXT, nullable=False, comment="密码")
    balance = Column(Integer, nullable=False, comment="")
    token = Column(TEXT, comment="缓存的令牌")
    terminal = Column(TEXT, comment="终端代码")

   class Store(Base):
    __tablename__ = 'store'
    store_id = Column(TEXT, primary_key=True,comment="主键")
    stock_level = Column(Integer, comment = "货存")

   class Store_Book(Base):
    __tablename__ = 'store_book'
    store_id = Column(TEXT, comment="主键")
    book_id = Column(TEXT, comment="主键")
    book_info = Column(TEXT, comment="书籍信息")
    stock_level = Column(Integer, comment = "货存")

    __table_args__ = (
        PrimaryKeyConstraint('store_id', 'book_id'),
    )

   class User_Store(Base):
    __tablename__ = 'user_store'

    id = Column(Integer, primary_key=True, autoincrement=True, comment="主键")

    fk_user_id = Column(
        TEXT,
        ForeignKey(
            "user.user_id",
            ondelete="CASCADE",
            onupdate="CASCADE",
        ),
        nullable=False,
        comment="user外键"
    )
    fk_store_id = Column(
        TEXT,
        ForeignKey(
            "store.store_id",
            ondelete="CASCADE",
            onupdate="CASCADE",
        ),
        nullable=False,
        comment="store外键"
    )
    # 多对多关系的中间表必须使用联合唯一约束，防止出现重复数据
    __table_args__ = (
        UniqueConstraint("fk_user_id", "fk_store_id"),
    )

   class New_Order(Base):
    __tablename__ = 'new_order'
    order_id = Column(TEXT, primary_key = True, comment = '订单id')
    
    fk_user_id = Column(
        TEXT,
        ForeignKey(
            "user.user_id",
            ondelete="CASCADE",
            onupdate="CASCADE",
        ),
        nullable=False,
        comment="user外键"
    )
    fk_store_id = Column(
        TEXT,
        ForeignKey(
            "store.store_id",
            ondelete="CASCADE",
            onupdate="CASCADE",
        ),
        nullable=False,
        comment="store外键"
    )

   class New_Order_Detail(Base):
    __tablename__ = 'new_order_detail'
    order_id = Column(TEXT, comment='订单id')
    book_id = Column(TEXT, comment='订单书籍')
    count = Column(Integer, comment='购买书籍数')
    price = Column(Integer, comment='单价')

    __table_args__ = (
        PrimaryKeyConstraint('order_id','book_id'),
    )
    
   engine, meta = con.connect()
   Base.metadata.bind = engine
   DBSession = sessionmaker(bind=engine)
   session = DBSession()
   ```
   2. 在上述创建的table中添加初始数据并利用该数据测试后端服务器与数据库的连接(2022.11.29 15:10 杨舜)
   ![avatar](./figure_require/add_naive_userdata.png)
   ![avatar](./figure_require/conn_test.png)
   
   3. 类比原有demo分别为不同的路由绑定不同的蓝图
   ```python
    app.register_blueprint(auth.bp_auth)
    app.register_blueprint(seller.bp_seller)
    app.register_blueprint(buyer.bp_buyer)
   ```

   4. 在/be/model目录下创建User类用书实现User对于数据库的一些交互功能
   5. 修改[model/db_conn.py](./be/model/db_conn.py)中查询的操作为orm操作,其中修改DBConn中的连接conn为sqlachlemy中的session，将会话作为连接
   ```python
   class DBConn:
    def __init__(self):
        self.session = postgreSQLORM.session
        return
        # self.conn = store.get_db_conn()

    def user_id_exist(self, user_id):
        row = self.session.query(User).filter(User.user_id==user_id).first()
        # 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):
        row = self.session.query(Store_Book).filter(Store_Book.book_id==book_id and Store_Book.store_id==store_id).first()
        # 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):
        row = self.session.query(User_Store).filter(User_Store.fk_store_id==store_id).first()
        # 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


   ```
   6. 修改/be/model/user.py中User类以及其中函数的定义使其满足题目要求的ORM模型，同时仅对注册功能进行测试(2022.11.30 15:40 杨舜)
   ```python
   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):
        ## 判断用户是否注册过了
        if self.user_id_exist(user_id=user_id):
            return error.error_exist_user_id(user_id)
        else:
        # try:
            terminal = "terminal_{}".format(str(time.time()))
            token = jwt_encode(user_id, terminal)
            ## 为新注册的用户创建对象
            new_user = postgreSQLORM.User(user_id=user_id,password=password,balance=0,token=token,terminal=terminal)
            self.session.add(new_user)
            self.session.commit()
            
            # 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"
   ```
   ![avatar](./figure_require/register_test.png)
   
   7. 另外对于auth路由中的其他功能接口(注销、登录、登出、更改密码)进行类似上述注册接口的修改，此处不在单独贴出代码，只是给出postman的测试截图，至此auth中的路由全部实现(2022.11.30 17:50 杨舜)
   (1)注册接口思路：
   首先查找库中的user表对用户输入的id查重之后根据terminal和id生成一个token，之后为用户建立一个新对象并插入到表user中
   (2)登陆接口思路：
   首先调用check_password函数对用户输入的密码进行判断得到code和message，先对code判断密码是否正确，之后根据不同的错误返回相应的不同的code和message并返回，若无错误则code返回200，message返回ok
   (3)登出接口思路：
   根据id查看是否已登录，之后更新token和terminal
   (4)注销接口思路：
   通过check_password函数检查该用户是否已存在账户，之后从表中删除该条目
   (5)更改密码接口思路：
   首先根据id获取用户的旧密码，与用户输入的旧密码对比，若符合则更新新密码

   ![avatar](./figure_require/unregister_test.png)
   ![avatar](./figure_require/login_test.png)
   ![avatar](./figure_require/logout_test.png)
   ![avatar](./figure_require/password_test.png)

   8. 利用上述类似的实现auth路由接口的方式完成seller路由接口，并利用postman测试实现(2022.12.01 19:20 杨舜)
   ![avatar](./figure_require/creatstore_test.png)
   ![avatar](./figure_require/addbook_test.png)
   ![avatar](./figure_require/addstocklevel_test.png)

   9.  利用上述类似的实现auth路由接口的方式完成buyer路由接口,并利用postman测试实现(2022.12.02 12:10 杨舜)
   ![avatar](./figure_require/neworder_test.png)
   ![avatar](./figure_require/addfunds_test.png)
   ![avatar](./figure_require/payment_test.png)

#### 三、根据要求实现后续的40%的功能并为其编写测试接口
   1. 对数据库结构进行改造（添加table的列）
   
   为了实现发货，收获，订单状态的查询，可以在new_order的订单的table中添加status，并利用不同的状态码来表示当前次订单的状态
   
   |status code|status|
   |---|---|
   |-1|取消|
   |0|初始值(未付款)|
   |1|已付款|
   |2|已发货|
   |3|已收货|

   因此修改postgreSQLORM.py文件中New_Order的类
   
   2. 修改对应payment接口中删除订单的操作未修改订单状态未1
   ```python
   row = session.query(New_Order).filter(New_Order.order_id==order_id).update({'status':1})
   ```
   
   3. 为seller路由新增发货(修改订单状态为2)接口、buyer路由新增收货(修改订单状态为3)接口、buyer路由新增取消订单(修改订单状态为-1)接口
```python
   ## /view/seller.py
   @bp_seller.routr("/send_out",methods=["POST"])
   def send_out():
       order_id: str = request.json.get("order_id")
       user_id: str = request.json.get("user_id")
       s = seller.Seller()
       code, message = s.send_out(order_id)

       return jsonify({"message": message}), code
```
```python
   ## /model/seller.py
   def send_out(self, order_id:str):
        session = self.session
        try:
            if not self.user_id_exist(user_id):
                return error.error_non_exist_user_id(user_id)
                
            row = session.query(New_Order).filter(New_Order.order_id==order_id).first()
            if row is None:
                return error.error_invalid_order_id(order_id)
            if row.status != 1:
                return error.error_invalid_order_id(order_id)
            
            row = session.query(New_Order).filter(New_Order.order_id==order_id).update({'status':2})

            if row == 0:
                return error.error_invalid_order_id(order_id)

            session.commit()

        except SQLAlchemyError as e:
            return 528, "{}".format(str(e))
        except BaseException as e:
            # print('touch3')
            return 530, "{}".format(str(e))
        return 200, "ok"
```

(1)创建商铺接口实现：
首先检查user_id和store_id是否已存在，若未存在则创建新对象，并插入表user_store中
(2)添加书本信息接口实现：
首先检查user_id和store_id是否存在，再检查book_id是否已有对应的书本，最后将书本信息创建新对象插入表store_book中
(3)添加库存接口实现：
首先检查user_id和store_id和book_id是否存在，之后根据store_id和book_id找到对应店铺的对应书本库存并进行更新
(4)商家发货接口实现：
首先检查user_id是否存在，之后检查order_id是否有效，对new_order表进行更新

(5)买家下单接口实现：
首先检查user_id和store_id是否存在，之后检查对应店铺的所要买的书本库存是否足够，若足够则减去对应库存，生成order_id，对表new_order_detail更新
(6)买家充值接口实现：
首先根据user_id获取对应密码，与用户输入密码对比，相符合则在表user中更新余额
(7)买家付款接口实现：
首先检查order_id是否存在，若存在则根据user_id获取密码，与用户输入密码对比，之后检查store_id与卖家的user_id是否存在，若存在则比较买家余额和订单价格，若余额足够则扣除买家相应价格，在卖家余额上等额增加，更新user表
(8)买家收货接口实现：
首先检查user_id是否存在，检查order_id是否有效，之后更新new_order表
(9)买家取消订单接口实现：
首先检查user_id是否存在，之后根据订单信息查找对应的订单，将查找到的订单从表new_order中删去，更新
(10)买家查看历史订单接口实现：
首先检查user_id是否存在，之后根据user_id查找表new_order，之后根据order_id列出订单
(11)自动取消订单接口实现：
首先根据order_id查找表new_order，之后根据当前时间计算订单处于未付款状态的持续时长，若超过30s处于未付款状态则自动删去该条目，更新表new_order

商家发货
   
URL

POST http://[address]/seller/send_out

Request
Headers:

key | 类型 | 描述 | 是否可为空
---|---|---|---
token | string | 登录产生的会话标识 | N

Body:

```json
{
  "user_id": "$seller id$",
  "order_id": "$store id$",
}
```
key | 类型 | 描述 | 是否可为空
---|---|---|---
user_id | String | 卖家用户ID | N
order_id | String | 订单号 | N

Response

Status Code:

码 | 描述
--- | ---
200 | 发货成功
5XX | 买家用户ID不存在
5XX | 无效参数
   

```python
## /view/buyer.py
@bp_buyer.route("/take_over", methods=["POST"])
def take_over():
    user_id = request.json.get("user_id")
    order_id = request.json.get("order_id")
    b = Buyer()
    code, message = b.take_over(user_id, order_id)
    return jsonify({"message": message}), code

```

```python
## /model/buyer.py
def take_over(self, user_id, order_id):
    session = self.session
    try:
        if not self.user_id_exist(user_id):
            return error.error_non_exist_user_id(user_id)
            
        row = session.query(New_Order).filter(and_(New_Order.order_id==order_id,New_Order.fk_user_id==user_id)).first()
        if row is None:
            return error.error_invalid_order_id(order_id)
        if row.status != 2:
            return error.error_invalid_order_id(order_id)
        
        row = session.query(New_Order).filter(and_(New_Order.order_id==order_id,New_Order.fk_user_id==user_id)).update({'status':3})

        if row == 0:
            return error.error_invalid_order_id(order_id)

        session.commit()

    except SQLAlchemyError as e:
        return 528, "{}".format(str(e))
    except BaseException as e:
        # print('touch3')
        return 530, "{}".format(str(e))
    return 200, "ok"

```
卖家收货
   
URL

POST http://[address]/buyer/take_over

Request
Headers:

key | 类型 | 描述 | 是否可为空
---|---|---|---
token | string | 登录产生的会话标识 | N

Body:

```json
{
  "user_id": "$seller id$",
  "order_id": "$store id$",
}
```
key | 类型 | 描述 | 是否可为空
---|---|---|---
user_id | String | 买家用户ID | N
order_id | String | 订单号 | N

Response

Status Code:

码 | 描述
--- | ---
200 | 收货成功
5XX | 买家用户ID不存在
5XX | 无效参数

```python
## /view/buyer.py
@bp_buyer.route("/order_cancel", methods=["POST"])
def take_over():
    user_id = request.json.get("user_id")
    order_id = request.json.get("order_id")
    b = Buyer()
    code, message = b.order_cancel(user_id, order_id)
    return jsonify({"message": message}), code
```
```python
## /model/buyer.py
def order_cancel(self, user_id, order_id):
    session = self.session
    try:
        if not self.user_id_exist(user_id):
            return error.error_non_exist_user_id(user_id)
                
        row = session.query(New_Order).filter(and_(New_Order.order_id==order_id,New_Order.fk_user_id==user_id)).first()
        if row is None:
            return error.error_invalid_order_id(order_id)
        if row.status != 0:
            return error.error_invalid_order_id(order_id)
            
        row = session.query(New_Order).filter(and_(New_Order.order_id==order_id,New_Order.fk_user_id==user_id)).update({'status':-1})

        if row == 0:
            return error.error_invalid_order_id(order_id)

        session.commit()

    except SQLAlchemyError as e:
        return 528, "{}".format(str(e))
    except BaseException as e:
        # print('touch3')
        return 530, "{}".format(str(e))
    return 200, "ok"
```

买家取消订单
   
URL

POST http://[address]/buyer/order_cancel

Request
Headers:

key | 类型 | 描述 | 是否可为空
---|---|---|---
token | string | 登录产生的会话标识 | N

Body:

```json
{
  "user_id": "$seller id$",
  "order_id": "$store id$",
}
```
key | 类型 | 描述 | 是否可为空
---|---|---|---
user_id | String | 买家用户ID | N
order_id | String | 订单号 | N

Response

Status Code:

码 | 描述
--- | ---
200 | 收货成功
5XX | 买家用户ID不存在
5XX | 无效参数
   
   
   5. 像buyer路由中添加一个查询历史订单的接口,为此首先需要在原来的New_Order表中添加creat_time列，然后为buyer路由添加history_order接口

```python
## /view/buyer.py
@bp_buyer.route("/history_order", methods=["POST"])
def take_over():
    user_id = request.json.get("user_id")
    b = Buyer()
    code, message = b.history_order(user_id)
    return jsonify({"message": message}), code
```
```python
## /model/buyer.py

```

历史订单查询
   
URL

POST http://[address]/buyer/history_order

Request
Headers:

key | 类型 | 描述 | 是否可为空
---|---|---|---
token | string | 登录产生的会话标识 | N

Body:

```json
{
  "user_id": "$buyer id$"
}
```
key | 类型 | 描述 | 是否可为空
---|---|---|---
user_id | String | 买家用户ID | N

Response

Status Code:

码 | 描述
--- | ---
200 | 查询成功
5XX | 买家用户ID不存在
5XX | 无效参数
   
Body:

```json
{
  "order_id": ["uuid"]
}
```

变量名 | 类型 | 描述 | 是否可为空
---|---|---|---
order_id | string | 订单号，只有返回200时才有效 | N

   6. 为上面添加的路由编写测试接口并进行测试
   ![avatar](./figure_require/route_test01.png)

   7. 为实现书店的搜索图书的功能，稍微修改数据库的结构，为书籍添加数据表，搜索标题表，搜索标签表，搜索作者表，搜索书本内容表
   
   Book
   |book_id|title|author|publisher|original_title|translator|pub_year|pages|original_price|currency_unit|binding|isbn|author_intro|book_intro|content|tags|picture|
   |---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
   主键（book_id）

   Search_title
   |search_id|title|book_id|
   |---|---|---|
   联合主键（search_id,book_id）
   外键（book_id）

   Search_tags
   |search_id|tags|book_id|
   |---|---|---|
   联合主键（search_id，book_id）
   外键（book_id）

   Search_author
   |search_id|author|book_id|
   |---|---|---|
   联合主键（search_id，author）
   外键（book_id）

   Search_book_intro
   |search_id|book_intro|book_id|
   |---|---|---|
   联合主键（search_id，book_id）
   外键（book_id）

   8. 修改seller中的add_book的路由
   9. 在auth中添加搜索的路由（只包含全局搜索，没有店铺内搜索）
```python
@bp_auth.route("/search_author", methods=["POST"])
def search_author():
    author = request.json.get("author", "")
    page = request.json.get("page", "")
    u = user.User()
    code, message = u.search_author(author=author, page=page)
    return jsonify({"message": message}), code

@bp_auth.route("/search_book_intro", methods=["POST"])
def search_book_intro():
    book_intro = request.json.get("book_intro", "")
    page = request.json.get("page", "")
    u = user.User()
    code, message = u.search_book_intro(book_intro=book_intro, page=page)
    return jsonify({"message": message}), code

@bp_auth.route("/search_tags", methods=["POST"])
def search_tags():
    tags = request.json.get("tags", "")
    page = request.json.get("page", "")
    u = user.User()
    code, message = u.search_tags(tags=tags, page=page)
    return jsonify({"message": message}), code

@bp_auth.route("/search_title", methods=["POST"])
def search_title():
    title = request.json.get("title", "")
    page = request.json.get("page", "")
    u = user.User()
    code, message = u.search_title(title=title, page=page)
    return jsonify({"message": message}), code
```
   10. 为搜索编写测试接口

![avatar](./figure_require/route_test01.png)
![avatar](./figure_require/route_test02.png)
![avatar](./figure_require/route_test03.png)

### 测试结果
```bash
bash script/test.sh
```
首先针对要实现的功能编写相应test，再进行功能实现，符合测试驱动开发的方法，最后输入上述命令，从上面的图中可以看出绝大部分的测试都能够通过，代码覆盖率为54%，作为核心组件的buyer.py、seller.py和user.py的代码覆盖率均在50%-65%之间

### 总结
本次书店project结合了这学期目前为止所学的许多知识，er图、ORM的使用与查询等，是一个比较大的挑战，在本次大项目中在构思方面帮助我们更好的理解和掌握了er图的绘制，在代码方面帮助我们对orm的增删改查操作有了较多的熟悉，同时，由于本次为合作作业，也让我们对git的使用变得更加流畅与顺手，相信在下次的大项目中能有比较大的提升
### 分工
ys：代码、报告撰写、代码测试、git版本控制、数据库整合
yjh：代码、报告撰写、代码测试

