朝花夕拾:回顾我的数据库课程设计-基于SqlServer2012+Flask+VUE3的电费收费管理系统

一. 选题背景

本文回顾了我在大学期间进行的一个数据库课程设计,本课设以电费收费管理系统为题材进行开发设计。该系统主要面向电力公司管理人员和普通用户,实现电费数据管理、用电信息统计、账单生成与查询、账费核对以及用户缴费等功能。
完整文档: 基于SqlServer2012+Flask+VUE3的电费收费管理系统说明书

Github链接: 基于SqlServer2012+Flask+VUE3的电费收费管理系统

二、系统设计

2.1 系统架构

采用前后端分离BS架构,前端使用VUE框架,后端使用Flask框架,使用SQL Server数据库存储数据。
前端(VUE3) <–> API接口 <–> 后端(Flask + SQL Server)

2.2 功能设计

系统主要功能包括:

  • 用户管理:开户、缴费用户信息管理
  • 账单管理:账单生成、查询
  • 缴费管理:线上缴费、线下缴费

2.3 概念结构设计

根据业务需求设计ER图,主要实体包括:

  • 用户、电表、用电记录、电费账单、收费记录、电价标准、缴费记录
    实体之间的关系包括:
  • 用户与电表一对一关系
  • 电表与用电记录一对多关系
  • 用电记录与电费账单一对一关系
  • 电费账单与收费记录、缴费记录一对多关系
  • 电价标准与电费账单多对多关系

三、 逻辑结构设计

3.1 关系模式设计

根据ER图,设计关系表:

  • 用户表(用户信息)
  • 电表表(电表信息)
  • 用电记录表(用电记录)
  • 电费账单表(电费账单信息)
  • 收费记录表(收费记录)
  • 电价标准表(电价标准)
  • 缴费记录表(用户缴费记录)
    关系模式符合1NF、2NF、3NF规范化设计。

3.2 索引与视图

添加有效索引,提高查询性能:

  • 用户表索引:用户名、身份证号
  • 电表表索引:电表编号
  • 用电记录表索引:用户ID、年月、电表ID、年月
  • 账单表索引:用户ID、年月、电表ID、年月
    添加重要视图:
  • 用户电量信息视图
  • 用户账单信息视图
  • 未付清电费账单视图

3.3 权限设计

系统设计了三种角色的权限控制:

  • 管理员:用户、账单管理
  • 收费员:账单查询、收费
  • 普通用户:自助缴费、信息查询
    采用Token、装饰器实现权限校验,控制接口访问权限。

四、系统实现

  • 数据库:SQL Server 2012,存储用户、账单、缴费数据
  • 后端:Python/Flask,实现接口、服务
  • 前端:VUE3,实现界面交互
  • 使用Token实现权限控制,JWT存储用户信息
  • 支持用户注册、登录、缴费、信息查询等功能
  • 管理员后台实现账单管理、用户管理、数据统计等

五、部分核心代码

5.1 数据库设计

5.1.1 表结构
    1. 创建用户信息表Users
CREATE TABLE Users(
  UserID INT PRIMARY KEY IDENTITY,    -- 用户ID,自增主键
  Username VARCHAR(50) UNIQUE NOT NULL,-- 用户名,唯一,不为空
  Password VARCHAR(255) NOT NULL,     -- 密码,不为空
  IDCard VARCHAR(18) UNIQUE NOT NULL, -- 身份证号,唯一,不为空
  Role INT UNIQUE NOT NULL,           -- 角色,不为空,预设可选值
  RealName VARCHAR(50),               -- 真实姓名 
  Phone VARCHAR(11),                  -- 电话
  Email VARCHAR(50),                  -- 邮箱
  Address VARCHAR(100),               -- 地址
  IsDeleted BIT DEFAULT 0              -- 软删除标志,默认未删除  
)  
    1. 创建电表信息表ElectricMeter
CREATE TABLE ElectricMeter(
  MeterID INT PRIMARY KEY IDENTITY,     -- 电表ID,自增主键
  UserID INT NOT NULL,                  -- 用户ID,外键引用Users(UserID)
  ElecType VARCHAR(50),        -- 用电类型
  FOREIGN KEY (UserID) REFERENCES Users(UserID) ON DELETE CASCADE  
)
    1. 创建用电记录表ElectricityUsage
CREATE TABLE ElectricityUsage(
  UsageID INT PRIMARY KEY IDENTITY,     -- 用电ID,自增主键
  UserID INT NOT NULL,                  -- 用户ID,外键引用Users(UserID)
  MeterID INT NOT NULL,                 -- 电表ID,外键引用ElectricMeter(MeterID) 
  Year INT NOT NULL,                    -- 用电年份
  Month INT NOT NULL,                   -- 用电月份
  TotalKwh DECIMAL(10,2) NOT NULL,     -- 用电量(度数)
  FOREIGN KEY (UserID) REFERENCES Users(UserID) ON DELETE CASCADE,  
   FOREIGN KEY (MeterID) REFERENCES ElectricMeter(MeterID) ON DELETE NO ACTION

)
    1. 创建电费账单表 ElectricityBill
CREATE TABLE ElectricityBill(
  BillID INT PRIMARY KEY IDENTITY,     -- 账单ID,自增主键
  UserID INT NOT NULL,                  -- 用户ID,外键引用Users(UserID) 
  MeterID INT NOT NULL,               -- 电表ID,外键引用ElectricMeter(MeterID)
  ElecType VARCHAR(50) NOT NULL,       -- 电费类型
  Year INT NOT NULL,                    -- 账单年份
  Month INT NOT NULL,                   -- 账单月份
  TotalUsed DECIMAL(10,2) NOT NULL,   -- 总用电量
  TotalCost DECIMAL(10,2) NOT NULL,   -- 总金额
  PaidStatus VARCHAR(10) DEFAULT 'Unpaid', -- 付费状态
  FOREIGN KEY (UserID) REFERENCES Users(UserID) ON DELETE CASCADE,   
  FOREIGN KEY (MeterID) REFERENCES ElectricMeter(MeterID) ON DELETE NO ACTION
)  
    1. 创建收费信息表ChargeInfo
CREATE TABLE ChargeInfo(
  ChargeID INT PRIMARY KEY IDENTITY,   --收费ID,自增主键
  BillID INT UNIQUE NOT NULL,                    --账单ID,外键引用ElectricityBill(BillID)
  ChargeDate DATETIME NOT NULL,        --收费日期
  PaidFee DECIMAL(10,2) NOT NULL,      --实收金额
  FOREIGN KEY (BillID) REFERENCES ElectricityBill(BillID) ON DELETE CASCADE
)  
    1. 创建电价标准表ChargeStandard
CREATE TABLE ChargeStandard(
  StandardID INT PRIMARY KEY IDENTITY, -- 标准ID,自增主键
  Year INT NOT NULL,                    -- 年份 
  Season INT NOT NULL,                  -- 季节  
  ElecType VARCHAR(50) NOT NULL,       -- 用电类型
  Price DECIMAL(10,2) NOT NULL         -- 电价  
)
    1. 创建支付信息表Payment
CREATE TABLE Payment(
  PaymentID INT PRIMARY KEY IDENTITY,   -- 支付ID,自增主键
  PayNo VARCHAR(50),                     -- 支付流水号
  PayTime DATETIME,                      -- 支付时间
  PayAmount DECIMAL(10,2),              -- 支付金额
  BillID INT UNIQUE NOT NULL,                    -- 账单ID,外键引用ElectricityBill(BillID)
  FOREIGN KEY (BillID) REFERENCES ElectricityBill(BillID) 
)
5.1.2 索引
-- Users表索引 
CREATE INDEX idx_username ON Users(Username)
CREATE INDEX idx_idcard ON Users(IDCard)

-- ElectricMeter表索引
CREATE UNIQUE INDEX idx_meterno ON ElectricMeter(MeterNo)  

-- ElectricityUsage表索引
CREATE INDEX idx_userid_year_month ON ElectricityUsage(UserID, Year, Month)
CREATE INDEX idx_meterid_year_month ON ElectricityUsage(MeterID, Year, Month)

-- ElectricityBill表索引
CREATE INDEX idx_userid_year_month ON ElectricityBill(UserID, Year, Month) 
CREATE INDEX idx_meterid_year_month ON ElectricityBill(MeterID, Year, Month)

-- ChargeInfo表索引
CREATE INDEX idx_billid ON ChargeInfo(BillID)
在Flask中使用索引无需特别调用,相关查询效率会被自动提高。
5.1.3 视图:
-- 管理员视图:查询所有用户  
-- 创建数据库视图UserElecInfo,包含用户信息和电表信息  
CREATE VIEW UserElecInfo  
AS
SELECT   
U.Username, U.IDCard, U.Role, 
U.RealName,U.Phone, U.Email, U.Address,
EM.ElecType  
FROM Users U 
JOIN ElectricMeter EM ON U.UserID = EM.UserID  
CREATE VIEW v_personal_info
AS 
SELECT u.UserID AS User_ID, u.Username, u.RealName, u.Phone,  
       eb.BillID, eb.MeterID, eb.Year, eb.Month, eb.TotalUsed, eb.TotalCost, eb.PaidStatus
FROM Users u 
JOIN ElectricityBill eb
ON u.UserID = eb.UserID
-- 收费员视图:查询未付费账单和用户信息
CREATE VIEW v_unpaid_bills
AS SELECT eb.*, u.Username, u.RealName, u.Phone  
FROM ElectricityBill eb JOIN Users u
ON eb.UserID = u.UserID
WHERE eb.PaidStatus = 'Unpaid'  
5.1.4 存储过程:

用户信息查询存储过程

CREATE PROCEDURE usp_GetUserInfo 
-- @id INT修该为@user_id INT,表示用户ID
@user_id INT  
AS
BEGIN
-- 添加异常处理
BEGIN TRY  

SELECT  
--用户名
U.Username,   
--身份证号 
U.IDCard,
--用户角色
U.Role, 
--真实姓名
U.RealName,
--电话
U.Phone,        
--邮箱
U.Email,
--地址
U.Address,
--用电类型
EM.ElecType
-- Users表与ElectricMeter表相关联,基于用户ID
FROM Users U 
INNER JOIN ElectricMeter EM ON U.UserID = EM.UserID   
-- 查询条件使用参数@user_id,而不硬编码
WHERE U.UserID = @user_id  

END TRY
BEGIN CATCH
 -- 若查询失败,则抛出自定义错误并回滚
 RAISERROR('用户信息查询失败',16,1)

 ROLLBACK  
END CATCH

END

exec usp_GetUserInfo @user_id = 5

–用户信息更新存储过程

-- =============================================  
-- Description: 更新Users与ElectricMeter表中的用户信息
-- =============================================  

CREATE PROCEDURE usp_UpdateUserElecInfo
-- 用户ID,必填
@UserID INT,     
-- 用户名   
@Username VARCHAR(50) = NULL,
-- 真实姓名
@RealName VARCHAR(50) = NULL,  
-- 电话
@Phone VARCHAR(20) = NULL,
-- 邮箱
@Email VARCHAR(50) = NULL,
-- 用电类型
@ElecType VARCHAR(50) = NULL 
AS
BEGIN
 -- 开启事务
 BEGIN TRY   

  -- 若用户名不为空,更新Users表 
  IF @Username IS NOT NULL   
     UPDATE Users SET Username = @Username WHERE UserID = @UserID
  
   -- 若真实姓名不为空,更新Users表     
  IF @RealName IS NOT NULL    
     UPDATE Users SET RealName = @RealName WHERE UserID = @UserID
 
   -- 若电话不为空,更新Users表        
  IF @Phone IS NOT NULL     
     UPDATE Users SET Phone = @Phone WHERE UserID = @UserID
  
   -- 若邮箱不为空,更新Users表      
  IF @Email IS NOT NULL    
     UPDATE Users SET Email = @Email WHERE UserID = @UserID
  
   -- 若用电类型不为空,更新ElectricMeter表    
  IF @ElecType IS NOT NULL   
     UPDATE ElectricMeter SET ElecType = @ElecType 
     WHERE UserID = @UserID  

 -- 提交事务 
 COMMIT   
 END TRY
 BEGIN CATCH
  -- 若更新失败,则回滚并抛出自定义错误  
  ROLLBACK
  RAISERROR('更新用户信息失败',16,1)
 END CATCH
END

– 管理员存储过程:账单生成

CREATE PROC usp_generate_bill
@Year INT,                    -- 指定年份
@Month INT   -- 指定月份
AS
-- 生成指定年月的电费账单
INSERT INTO ElectricityBill(UserID, MeterID, ElecType, Year, Month,
                           TotalUsed, TotalCost, PaidStatus)
SELECT
  Users.UserID,                -- 用户ID
  ElectricMeter.MeterID,       -- 电表ID
  ElectricMeter.ElecType,      -- 用电类型
  @Year,                       -- 用电年份
  @Month,                      -- 用电月份
  SUM(ElectricityUsage.TotalKwh),-- 总用电量
  (SELECT Price FROM ChargeStandard   -- 查询当月电价
   WHERE ElecType = ElectricMeter.ElecType AND
         Year = @Year AND
         --计算当前Season
         Season = (@Month-1)/3 + 1) * SUM(ElectricityUsage.TotalKwh), -- 总金额
  'Unpaid'                     -- 未付费状态
FROM Users
LEFT JOIN ElectricityUsage
ON Users.UserID = ElectricityUsage.UserID
LEFT JOIN ElectricMeter
ON ElectricityUsage.MeterID = ElectricMeter.MeterID
WHERE Year = @Year AND Month = @Month
GROUP BY Users.UserID,ElectricMeter.MeterID,ElectricMeter.ElecType
-- 计算当前Season(季节)
-- Spring: 3,4,5   Season = 1
-- Summer: 6,7,8    Season = 2
-- Autumn: 9,10,11  Season = 3
-- Winter: 12,1,2   Season = 4

– 收费员存储过程:手工收费

CREATE PROC usp_manual_charge 
@BillID int,  
@PaidFee money
AS
BEGIN
  SET NOCOUNT ON;
  
  -- 定义自定义错误
  DECLARE @ErrMsg nvarchar(50) = 'Payment record already exists.'

  -- 开始事务
  BEGIN TRY
    BEGIN TRAN

    -- 判断ChargeInfo表是否已存在该账单的支付记录
    IF EXISTS (SELECT * FROM ChargeInfo WHERE BillID = @BillID)
    BEGIN
      THROW 50000, @ErrMsg, 1
    END

    -- 判断Payment表是否已存在该账单的支付记录
    IF EXISTS (SELECT * FROM Payment WHERE BillID = @BillID)
    BEGIN
      THROW 50000, @ErrMsg, 1
    END

    -- 更新账单状态为"已付清"
    UPDATE ElectricityBill
    SET PaidStatus = 'Paid'
    WHERE BillID = @BillID

    -- 事务提交
    COMMIT TRAN
  END TRY
  -- 回滚事务
  BEGIN CATCH
    ROLLBACK TRAN
  END CATCH

  -- 插入收费信息记录
  INSERT INTO ChargeInfo(BillID, ChargeDate, PaidFee)
  VALUES(@BillID, GETDATE(), @PaidFee)  

  -- 插入支付信息记录
  INSERT INTO Payment(BillID, PayTime, PayAmount)
  VALUES(@BillID, GETDATE(), @PaidFee)
END
  • python-flask调用管理员存储过程
    db.session.execute(‘EXEC usp_generate_bills’)

  • python-flask收费员存储过程
    db.session.execute(‘EXEC usp_manual_charge @BillID=?, @PaidFee=?’, [bill_id, paid_fee])

5.1.5 触发器:
-- 创建一个名为 tr_create_user_electric_meter 的触发器
CREATE TRIGGER tr_create_user_electric_meter
-- 指定在 Users 表中插入数据时触发此操作
ON Users
AFTER INSERT
AS
BEGIN
    -- 在插入操作后,将新的 UserID 添加到 ElectricMeter 表中
    INSERT INTO ElectricMeter (UserID)
    SELECT i.UserID
    FROM inserted AS i
    -- 确保仅为Meter 表中不存在的 UserID 添加新记录
    WHERE NOT EXISTS (
          SELECT 1
          FROM ElectricMeter AS em
          WHERE em.UserID = i.UserID
    )
END;
  1. 管理员触发器:
  • 在用户删除时,同步删除与该用户相关的电表信息、用电记录和账单信息:
    sql
CREATE TRIGGER trg_delete_user_info
ON Users 
FOR DELETE
AS
BEGIN
  DELETE FROM ElectricMeter 
  WHERE UserID IN (SELECT UserID FROM DELETED)

  DELETE FROM ElectricityUsage
  WHERE UserID IN (SELECT UserID FROM DELETED)
  
  DELETE FROM ElectricityBill
  WHERE UserID IN (SELECT UserID FROM DELETED)
END
  1. 收费员触发器:
--在账单付费时,自动更新账单的付费状态为“已付费”:
CREATE TRIGGER trg_update_bill_status 
ON ChargeInfo
FOR INSERT
AS
BEGIN
  UPDATE ElectricityBill
  SET PaidStatus = 'Paid'
  WHERE BillID IN (SELECT BillID FROM INSERTED)
END

5.2 后端接口

5.2.1 用户注册:
  • 用户通过UI界面完成注册信息填写与提交。
  • 应用层接收注册信息,验证所有必填字段是否为空及格式是否正确。如果验证不通过,提示用户"注册信息填写错误,请重新填写!"
  • 所有验证通过后,将用户信息插入Users表,生成自增的UserID。
  • 提交事务,用户注册完成。
    用户注册接口
@app.route('/users/register', methods=['POST'])
def create_user():
    username = request.json.get('Username')
    password = request.json.get('Password')
    idcard = request.json.get('IDCard')
    if not all([username, password, idcard]):
        return jsonify(errno=4001, errmsg='参数不完整')
    if Users.query.filter(Users.Username == username).first():
        return jsonify(errno=3001, errmsg='用户名 {} 已存在'.format(username))
    if Users.query.filter(Users.IDCard == idcard).first():
        return jsonify(errno=3002, errmsg='身份证号 {} 已存在'.format(idcard))
    role = 3  # 默认role从3开始
    # 判断role是否唯一
    while Users.query.filter_by(Role=role).first():
        role += 1  # 如果角色存在,递增role值
        # 校验输入
        # 加密密码
    hash_pw = generate_password_hash(password)
    # 添加用户
    user = Users(Username=username, Password=hash_pw, Role=role, IDCard=idcard)
    try:
        # 添加用户并提交
        db.session.add(user)
        # 在插入用户前禁用触发器
        db.session.execute(text("ALTER TABLE Users DISABLE TRIGGER tr_create_user_electric_meter"))
        db.session.commit()
        # 获取刚刚插入的用户的 UserID
        inserted_user = Users.query.filter_by(Username=username).first()
        if inserted_user:
            # 添加电表记录并提交
            electric_meter = ElectricMeter(UserID=inserted_user.UserID)
            db.session.add(electric_meter)
            db.session.commit()
        # 在插入用户后启用触发器
        db.session.execute(text("ALTER TABLE Users ENABLE TRIGGER tr_create_user_electric_meter"))
        db.session.commit()
        return jsonify(status='success', msg='用户注册成功')
    except Exception as e:
        db.session.rollback()
        return jsonify(status='error', msg='用户注册失败:{}'.format(str(e)))
  • 用到触发器tr_create_user_electric_meter用于自动创建用户对应的电表,用户与电表一一对应
5.2.2 用户登录登出与个人信息查询与完善
  • 登录接口
@app.route('/login', methods=['POST'])
def login():
    if not request.is_json:
        return jsonify({"msg": "Missing JSON in request"}), 400
    # JSON 校验
    username = request.json.get('Username', None)  # 获取请求中的用户名
    password = request.json.get('Password', None)  # 获取请求中的密码
    user = Users.query.filter_by(Username=username).first()
    print(username, password)
    # 登录校验
    if not user or not check_password_hash(user.Password, password):
        print('用户名或密码错误')
        return jsonify(errno=RET.DATAERR, errmsg='用户名或密码错误')
    # 访问令牌黑名单校验
    if cache.get(access_token_blacklist_key.format(user.UserID)):
        # 清除用户登录信息缓存
        cache.delete(user_info_key.format(user.UserID))
        # 设置登录Session过期
        session.pop(user.UserID, None)
    if user and check_password_hash(user.Password, password):
        login_user(user)
        # 生成访问和刷新token
        access_token = create_access_token(identity=user.UserID, additional_claims={'privilege': user.Role},
                                           expires_delta=timedelta(minutes=30))
        # 生成刷新令牌
        refresh_token = create_refresh_token(identity=user.UserID, expires_delta=timedelta(days=7))
        # 存储用户登录信息至缓存
        cache.set(user_info_key.format(user.UserID), user, timeout=30 * 24 * 60 * 60)
        print('登陆成功')
        return jsonify(access_token=access_token, refresh_token=refresh_token), 200
    else:
        return jsonify({"msg": "Bad username or password"}), 401
  • 登出接口
@app.route('/logout', methods=['POST'])
@jwt_required()
def logout():
    """用户登出"""
    user_id = get_jwt_identity()
    # 校验是否在黑名单中,如果在,则表示已经退出,直接返回
    if cache.get(access_token_blacklist_key.format(user_id)):
        return jsonify(errno=RET.OK, errmsg='已登出')
    # 如果不在,则退出登录
    # 删除用户登录缓存中的信息
    cache.delete(user_info_key.format(user_id))
    # 设置登录session过期
    session.pop(user_id, None)
    # 清空访问令牌缓存
    cache.delete(access_token_blacklist_key.format(user_id))
    # 记录日志
    current_app.logger.info('用户 {} 登出'.format(user_id))
    # 将access token加入黑名单
    cache.set(access_token_blacklist_key.format(user_id), 1, timeout=60)
    # 返回成功信息
    logout_user()
    return jsonify(errno=RET.OK, errmsg='退出成功')
@app.route('/profile')
@jwt_required()
def get_user_profile():
    user_id = get_jwt_identity()
    current_app.logger.debug('用户 %s 访问个人信息接口', user_id)
    result = db.session.execute(text(f'EXEC usp_GetUserInfo @user_id = {user_id}'))
    data = []
    for row in result:
        # 直接转为字典,不需要排序
        d = {col: val for col, val in zip(result.keys(), row)}
        data.append(d)
    db.session.commit()
    return jsonify(data)
  • 用户信息查询存储过程usp_GetUserInfo
@app.route('/profile/update', methods=['PUT'])
@jwt_required()
def update_profile():
    u_id = get_jwt_identity()
    data = request.get_json()
    username = data.get('Username')
    real_name = data.get('RealName')
    phone = data.get('Phone')
    email = data.get('Email')
    elec_type = data.get('ElecType')
    sql = f'EXEC usp_UpdateUserElecInfo @UserID = {u_id}'
    if username is not None:
        sql = sql + f', @Username = {username}'
    elif real_name is not None:
        sql = sql + f', @RealName = {real_name}'
    elif phone is not None:
        sql = sql + f', @Phone = {phone}'
    elif email is not None:
        sql = sql + f', @Email = {email}'
    elif elec_type is not None:
        sql = sql + f', @ElecType = {elec_type}'
    db.session.execute(text(sql))
    db.session.commit()  # 手动提交事务
    user = Users.query.get(u_id)
    users_data = UsersSchema().dump(user)
    return jsonify(users_data)
  • 用户信息更新存储过程sp_UpdateUserElecInfo
5.2.3 权限设计
def admin_required():
    def decorator(fn):
        @wraps(fn)
        def wrapper(*args, **kwargs):
            try:
                token = request.headers.get('Authorization').split(' ')[1]
                print(token)
            except:
                return jsonify(errno=RET.ROLEERR, errmsg='token error'), 401
            try:
                claims = jwtt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256'])
                print(claims)
            except:
                return jsonify(errno=RET.ROLEERR, errmsg='claims error'), 401
            role = claims['privilege']
            if role != 0:  # ROLE = {0: '管理员'}
                return jsonify(errno=RET.ROLEERR, errmsg='用户权限不足'), 401
            try:
                return fn(*args, **kwargs)
            except DatabaseError:
                return '数据库异常,请稍后再试。', 500
        return wrapper
    return decorator
def admin_or_charger_required():
    def decorator(fn):
        @wraps(fn)
        def wrapper(*args, **kwargs):
            token = request.headers.get('Authorization').split(' ')[1]
            claims = jwtt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256'])
            role = claims['privilege']
            if role > 1:
                return jsonify(errno=RET.ROLEERR, errmsg='用户权限不足'), 401
            try:
                return fn(*args, **kwargs)
            except DatabaseError:
                return '数据库异常,请稍后再试。', 500
        return wrapper
    return decorator
5.2.4 未付费账单生成流程:
  • 调用usp_generate_bill存储过程,生成电费账单
@app.route('/bills', methods=['POST'])
@admin_or_charger_required()
def generate_bill():
    # 获取参数
    year = request.json.get('year')
    month = request.json.get('month')
    # 校验参数
    if not year or not month:
        abort(400, description='The year and month are required!')
    # 调用存储过程生成账单
    db.session.execute('CALL usp_generate_bill(:year, :month)', {'year': year, 'month': month})
    db.session.commit()
    # 返回成功
return jsonify(errno=RET.OK, errmsg='生成成功')
5.2.5 网上缴费流程:
    1. 用户登录系统,调用/bills接口查询自己的未付费账单。
@app.route('/bills')
@jwt_required()
def query_bill():
    # 获取参数
    u_id = current_user.get_id()
    # 查询账单
    bills = ElectricityBill.query.filter_by(PaidStatus='Unpaid', UserID=u_id).all()
    result = ElectricityBillSchema(many=True).dump(bills)
    # 额外返回每个账单的BillID
    for bill in result:
        bill['bill_id'] = bill.pop('BillID')
        print(bill['bill_id'])
    # 返回账单
    return jsonify(errno=RET.OK, errmsg='OK', data=result)
    1. 用户选择需要缴费的账单,拿到BillID。
    1. 用户调用/payments接口进行网上支付,传入BillID。
  • 插入Payment表,调用相关触发器和存储过程
@app.route('/payments', methods=['POST'])
@jwt_required()
def make_payment():
    # 获取参数
    bill_id = request.json.get('billid')
    # 插入支付记录
    bill = ElectricityBill.query.get(bill_id)
    # 支付相关逻辑...
    pay_no = '#1234'  # 支付流水号
    pay_time = datetime.now()  # 支付时间
    pay_amount = bill.TotalCost  # 支付金额

    # 写入Payment表
    payment = Payment(PayNo=pay_no, PayTime=pay_time, PayAmount=pay_amount, BillID=bill_id)
    db.session.add(payment)
    db.session.add()

    bill.PaidStatus = 'Paid'
    db.session.commit()
    # 返回支付记录ID
    return jsonify(errno=RET.OK, errmsg='支付成功')
    1. 系统接收到BillID,插入Payment表一条支付记录。
    1. Payment表插入触发trg_update_bill_status触发器。
    1. 触发器自动更新对应BillID的账单状态为’Paid’。
    1. 提交事务,完成支付。
5.2.6 收费员手工收费流程:
    1. 收费员登录系统查询v_unpaid_bills视图,获取未付费账单列表和用户信息。
@app.route('/clerk/unpaid_bills')
@admin_or_charger_required()  # 自定义装饰器检查收费员权限
def get_unpaid_bills():
    # 获取待收费账单
    bills = ElectricityBill.query.filter_by(PaidStatus='Unpaid').all()
    result = ElectricityBillSchema(many=True).dump(bills)
    result2 = db.session.execute('SELECT * FROM v_unpaid_bills')

    return jsonify(errno=RET.OK, errmsg='成功', data=result)
    1. 收费员联系用户,确认应收费用金额。
    1. 收费员调用usp_manual_charge存储过程进行手工收费。传入BillID和收费金额。
  • 收费员存储过程:手工收费
@app.route('/clerk/charge', methods=['POST'])
@admin_or_charger_required()
def charge_bill():
    # 收费员选择账单并输入实收金额
    bill_id = request.json.get('bill_id')
    paid_fee = request.json.get('paid_fee')

    # 执行存储过程完成收费操作
    db.session.execute('CALL usp_manual_charge(:bill_id, :paid_fee)', {'bill_id': bill_id, 'month': paid_fee})
    db.session.commit()
    return jsonify(errno=RET.OK, errmsg='收费成功')
    1. 存储过程更新对应BillID的账单状态为’Paid’。
    1. 存储过程判断是否有未支付余额,如果有则插入Payment表一条支付记录。
    1. 存储过程插入ChargeInfo表一条手工收费记录。
    1. 提交事务,完成手工收费。

5.3 前端设计

5.3.1 项目的结构和配置
  1. 创建pyCharm项目python-flask-vue-web,用于存放Flask后端项目和Vue前端build后的文件。里面有app.py(Flask入口文件),static(静态资源文件夹),templates(模板文件夹)
  2. 创建Vue前端项目vue-web,用于开发前端页面
  3. 在vue-web中添加vue.config.js,配置:
    • outputDir: ‘…/static’ 将build后的文件输出到python-flask-vue-web/static文件夹
    • indexPath: ‘…/templates/index.html’ 将index.html文件输出到python-flask-vue-web/templates文件夹
      目的是为了Flask可以找到和渲染这些文件
  4. 启动Vue项目进行前端开发,yarn serve
  5. build Vue项目,yarn run build, 此时python-flask-vue-web/static文件夹下会有js, css, img等文件夹,包含Vue build后的静态资源
  6. 在app.py(Flask入口文件)中配置:
    • static_folder=‘./static’ 设置静态资源文件夹的路径
    • 添加路由’/‘和index.html的映射关系 @app.route(’/‘) def index(): return render_template(‘index.html’)
      目的是告诉Flask,静态资源文件夹的位置,以及访问’/'的时候渲染index.html模板
  7. 启动Flask,访问localhost:5000,Vue项目可以跑起来,但是js,css等资源404,因为我们没有告诉Vuebuild后的资源在哪里,需要配置
  8. 在vue.config.js中配置assetsDir:‘static/’,build Vue项目,然后重启Flask
    目的是告诉Vue,将build后的js,css,img资源都输出到static文件夹,这样Flask就可以找到它们了
  9. 此时项目可以正常访问,但是favicon.ico找不到,因为它没有输出到static文件夹,需要在Vue的public文件夹中添加static文件夹,并将favicon.ico放入
  10. 修改index.html中的favicon.ico路径,让它指向static/favicon.ico,然后再build Vue项目,重启Flask,favicon.ico就正常了
5.3.2 项目目前的目录结构和各目录/文件作用

python-flask-vue-web
|- app.py Flask入口文件,启动Flask
|- static 存放Vue build后的静态资源文件夹
|- templates 存放Vue build后的index.html文件
|- vue-web Vue前端项目主目录
vue-web
|- node_modules 项目依赖包文件夹
|- public
| |- index.html
| |- static 存放第三方js和favicon.ico
|- src Vue源码目录
| |- api.js 定义接口api规则
| |- assets 资源文件夹
| |- components 组件文件夹
| |- router 路由文件夹
| | |- index.js 配置路由规则和视图映射
| |- views 视图/页面文件夹
| | |- HomePage.vue
| | |- UserLogin.vue
| | |- UserProfile.vue
| | |- UserRegister.vue
| | |- HomePage.vue

| |- App.vue 根组件
| |- main.js 入口文件
|- .gitignore git忽略文件规则
|- babel.config.js babel配置
|- package.json 项目依赖包配置
|- vue.config.js Vue项目配置,和Flask集成配置
|- yarn.lock yarn包版本锁定文件
总结:

  • python-flask-vue-web:Flask后端项目主目录
  • vue-web:Vue前端项目主目录
  • src:Vue源码目录,我们主要在这里开发
    • api.js:定义接口规则
    • router:路由配置
    • views:视图/页面组件
    • App.vue:根组件
    • main.ts:入口文件
  • vue.config.js:Vue项目配置,和Flask集成的配置
  • public:存放静态资源与index.html
  • package.json:项目依赖配置
    Vue和Flask通过vue.config.js, templates, static和app.py实现集成。
    这个目录结构已经很清晰,我们的项目开发主要也是在src目录下进行。有任何不理解的地方我们可以随时讨论。
5.3.3 HomePage.vue




5.3.4 ClerkPage.vue
<template>
  <div> 
    <el-menu :default-active="activeIndex" class="el-menu-demo" mode="horizontal">
      <el-menu-item index="1"><router-link to="/">首页</router-link></el-menu-item>  
      <el-menu-item index="2"><router-link to="/login">登录</router-link></el-menu-item>
      <el-menu-item index="5"><router-link to="/bills/generate">账单生成</router-link></el-menu-item>   
      <el-menu-item index="8"><router-link to="/clerk/unpaid_bills">收费员查账</router-link></el-menu-item>  
      <el-menu-item index="9"><router-link to="/clerk/charge">收费员收费</router-link></el-menu-item>
    </el-menu>
   </div>
   <el-button @click="logout">登出</el-button>
</template>

<script>
import { logout } from '@/api'

export default {
  data() {
    return {
      activeIndex: '1' 
    }
  },
  methods: {
    logout() {  
      logout()  
        .then(() => {  
          this.$router.push('/')  
          localStorage.removeItem('access_token')  
        })  
        .catch(err => {  
          if (err.message === 'No access_token found.') {  
            console.log(err.message)  
            return  
          }  
          console.log(err.message)  
        })
    }
  }
}  
</script>

<style scoped>
.el-menu-demo {
  background-color: #545C64; 
}
</style>
5.3.5 UserRegister.vue
<!-- UserRegister.vue -->
<template>
  <!-- 用户注册页面 -->
  <el-form :model="data" :rules="rules" ref="form" label-width="100px">
    <el-form-item label="Username" prop="Username">
      <el-input v-model="data.Username"></el-input>
    </el-form-item> 
    <el-form-item label="Password" prop="Password">
      <el-input type="password" v-model="data.Password"></el-input>
    </el-form-item>
    <el-form-item label="PasswordConfirm" prop="PasswordConfirm">
      <el-input type="password" v-model="data.PasswordConfirm"></el-input>
    </el-form-item>
    <el-form-item label="IDCard" prop="IDCard">    
      <el-input v-model="data.IDCard"></el-input>
    </el-form-item>
    <el-form-item>
      <el-button type="primary" @click="register('form')">注册</el-button>
      <el-button>取消</el-button>
    </el-form-item>
  </el-form>
</template>


<script>
// 导入axios和注册接口  
import { register } from '@/api'   

export default {
  data() {     
    return {     
      // 用户名、密码、确认密码、身份证号     
      data: {
        Username: '',      
        Password: '',       
        PasswordConfirm: '',
        IDCard: ''    
      },
      // 表单验证规则     
      rules: {
        // 用户名必填         
        Username: [          
          { required: true, message: '请输入用户名', trigger: 'blur' }       
        ],
        // 密码必填、6位以上          
        Password: [           
          { required: true, message: '请输入密码', trigger: 'blur' },          
          { min: 6, message: '密码长度不能少于6位', trigger: 'blur' }         
        ],         
        // 确认密码必填、与密码一致        
        PasswordConfirm: [         
          { required: true, message: '请再次输入密码', trigger: 'blur' },   
          { validator: this.confirmPassword, trigger: 'blur' }          
        ],          
        // 身份证号必填           
        IDCard: [            
          { required: true, message: '请输入身份证号', trigger: 'blur' }         
        ]    
      }   
    }
  },
  methods: {
    // 确认密码自定义校验方法
    confirmPassword(rule, value, callback) {  
      console.log('调用confirmPassword方法,Password值为:', this.data.Password) 
      console.log('调用confirmPassword方法,PasswordConfirm值为:', value)   
      if (value !== this.data.Password) {
        callback(new Error('两次输入的密码不一致!'))  
        console.log('两次密码输入不一致!')  
      } else {   
        callback()  
        console.log('两次密码输入一致!')  
      }  
    },
    register(formName) {   
    this.$refs[formName].validate((valid) => { 
      if (valid) {    
        const params = {      
          Username: this.data.Username,      
          Password: this.data.Password,       
          IDCard: this.data.IDCard    
        } 
      register(params)   
        .then(res => {  
          
    if (res.data.errno === 0) {
      this.$message.success('注册成功!')
      this.$router.push('/login')  // 跳转到登录页
    }else{
          console.log('接受到结果为:', res.data.errmsg) 
          this.$message.error(res.data.errmsg) 
    }
        })
        .catch(err => {
          if (err.response.data.errno === 3001) {
            this.$message.error(`${err.response.data.errmsg}`)  
          } else if (err.response.data.errno === 3002) {
            this.$message.error(`${err.response.data.errmsg}`)  
          } else if (err.response.data.errno === 4001) {
            this.$message.error(`${err.response.data.errmsg}`)  
          }else {
            this.$message.error('注册失败!')
          } 
        })     
     } else {    
       this.$message.error('注册信息不完整!')    
       return false   
     }  
   })
  }


  }
}
</script>
5.3.6 UserLogin.vue
<template>
  <form @submit.prevent="submitLogin">
    <div>
      <label for="Username">用户名</label>
      <input v-model="user.Username" name="Username">
    </div>
    <div>
      <label for="Password">密码</label>
      <input v-model="user.Password" name="Password" type="password">
    </div>
    <button type="submit">登录</button>
  </form>
</template>

<script>
import { login } from '@/api'
import jwt_decode from 'jwt-decode'

export default {
  data() {
    return {
      user: {
        Username: '',
        Password: ''
      }
    }
  },
  methods: {
    async login() {
      console.log('login 方法被调用')
      try {
        if (!this.user.Username || !this.user.Password) {
          throw new Error('请输入用户名和密码')
        }
        console.log('发起登录请求')
        const res = await login(this.user)
        console.log('登录请求成功,获取响应结果')
        if (res.data && res.data.access_token) {
          // 存储token  
          localStorage.setItem('access_token', res.data.access_token)
          console.log(`用户 ${this.user.Username} 登录成功!access_token ${res.data.access_token} 已存储`)
 
          const accessToken = localStorage.getItem('access_token')
          const claims = jwt_decode(accessToken, 'secret')
          console.log('claims:', claims)
          const role = claims.privilege
          if (role == 1 || role ==0) {  
            this.$router.push({ path: '/c' })
          }
          else{
            this.$router.push({ path: '/profile' })
          }
          
        } else {
          console.log(`用户 ${this.user.Username} 登录失败,请检查用户名 ${this.user.Username} 和密码`)
        } 
      } catch (err) {
        console.log(err.message)
      }
    },
    submitLogin() {
      console.log('submitLogin 方法被调用')
      this.login()
    }
  }
}
</script>
5.3.7 UserProfile.vue
<!-- src\views\UpdateProfile.vue -->
<template>
  <div>
   <el-form ref="updateForm" :model="user" status-icon :rules="rules" @submit.prevent="submitUpdateProfile">
      <el-form-item label="真实姓名" prop="RealName">
        <el-input v-model="user.RealName" />
      </el-form-item>
      <el-form-item label="手机号" prop="Phone">
        <el-input v-model="user.Phone" />
      </el-form-item>
      <el-form-item label="邮箱" prop="Email">
        <el-input v-model="user.Email" />
      </el-form-item>
      <el-form-item label="地址" prop="Address">
        <el-input v-model="user.Address" />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" native-type="submit">更新</el-button>
      </el-form-item>
    </el-form>
  </div>
</template>
<script>
import { ElForm, ElFormItem, ElInput, ElButton } from 'element-plus';
import { updateProfile } from '@/api';

export default {
  components: {
    ElForm,
    ElFormItem,
    ElInput,
    ElButton,
  },
  data() {
    return {
      user: {
        RealName: '',
        Phone: '',
        Email: '',
        Address: '',
      },
      rules: {
        RealName: [{ required: true, message: '请输入真实姓名', trigger: 'blur' }],
        Phone: [
          { required: true, message: '请输入手机号', trigger: 'blur' },
          { pattern: /^1[3459]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' },
        ],
        Email: [
          { required: true, message: '请输入邮箱地址', trigger: 'blur' },
          { type: 'email', message: '邮箱格式不正确', trigger: 'blur' },
        ],
        Address: [{ required: true, message: '请输入地址', trigger: 'blur' }],
      },
    };
  },
  methods: {
    submitUpdateProfile() {
      this.$refs.updateForm.validate(valid => {
        if (valid) {
          this.updateProfile();
        } else {
          return false;
        }
      });
    },
    updateProfile() {
      updateProfile(this.user)
        .then(() => {
          this.$message({
            message: '资料更新成功',
            type: 'success',
          });
          this.$router.push('/Profile');
          console.log('user对象填充成功:', this.user)
        })
        .catch(err => {
          console.log(err.message);
          this.$router.push('/');
        });
    },
  },
};

</script>
<!-- <template>
  <div>
    <form @submit.prevent="updateProfile">
      <div>
        <label for="RealName">真实姓名</label>
        <input v-model="user.RealName">
      </div>
      <div>
        <label for="Phone">手机号</label>
        <input v-model="user.Phone"> 
      </div>
      <div>
        <label for="Email">邮箱</label>
        <input v-model="user.Email">
      </div>
      <div>
        <label for="Address">地址</label>
        <input v-model="user.Address">
      </div>
      <button type="submit">更新</button>
    </form>
  </div>
</template>

<script>
import { updateProfile } from '@/api'

export default {
  data() { 
    return {
      user: {}  
    }
  },
  methods: {
    updateProfile() {     
      updateProfile(this.user)
        .then(() => {  
          this.$router.push('/Profile') 
        })
        .catch(err => {
          console.log(err.message)
        })
    }
  }
}
</script> -->
5.3.8 GenerateUnpaidBill.vue
<template>
  <div>
    <form @submit.prevent="generateUnpaidBill">
      <div>
        <label for="year">Year:</label> 
        <input id="year" v-model="unpaidBillForm.year">
      </div>
      <div>
        <label for="month">Month:</label> 
        <input id="month" v-model="unpaidBillForm.month">
      </div>
      <button type="submit">Generate Unpaid Bill</button>
    </form>
  </div>
</template>


<script>
import { generateUnpaidBill } from '@/api'


export default {
  data() {
    return {
      unpaidBillForm: {
        year: '',
        month: ''  
      }
    }
  },
  methods: {
    generateUnpaidBill() {   
      // 获取 access_token  
      const accessToken = localStorage.getItem('access_token')  

      // 调用方法,传入 accessToken  
      generateUnpaidBill(this.unpaidBillForm, accessToken)  
        .then(res => {
          alert(res.data.errmsg)
        })
        .catch(err => {
          if (err === '权限不足,无法生成未付账单') {
            alert(err)
          } else {
            alert('Generate unpaid bill failed. Please try again!')
          }
        })
    }
  }
}
</script>
5.3.9 QueryUnpaidBills.vue
<template>
  <div>
   <button @click="getUnpaidBills">获取未付账单</button>
     <button @click="toHome">回首页</button>
   <table>
     <thead>
       <tr>
         <th>账单ID</th>  
         <th>用户ID</th>   
         <th>应收金额</th> 
         <th>账期</th>
         <th>还款状态</th>
         <th>操作</th>
       </tr>
     </thead>
     <tbody>
       <tr v-for="bill in bills" :key="bill.BillID">
         <td>{{ bill.BillID }}</td>
         <td>{{ bill.UserID }}</td>  
         <td>{{ bill.TotalCost }}</td>  
         <td>{{ bill.Year }}{{ bill.Month }}</td>
         <td>{{ bill.PaidStatus }}</td>
         <td>
           <button @click="toPay(bill)">支付</button>
         </td>
       </tr>
     </tbody>
   </table>
  </div>
</template>

<script>
import { queryUnpaidBills } from '@/api'

export default {
  data() {
    return {
      bills: []
    }
  },
  methods: {
    getUnpaidBills() {
      console.log('调用获取未付账单方法')
      queryUnpaidBills().then(res => {
        console.log('获取未付账单成功,结果:', res)
        this.bills = res.data.data
        console.log('bills:', this.bills)
      }).catch(err => {
        console.log('获取未付账单失败,错误:', err)
      })
    },
    toHome() {
      this.$router.push('/')
    },
    toPay(bill) {  
      const dataToSave = { billID: bill.BillID, totalCost: bill.TotalCost };
      localStorage.setItem("chargeData", JSON.stringify(dataToSave));
      this.$router.push({ name: "pay" });
      // console.log('跳转到支付路由,传入账单ID:', bill.bill_id)  
      // this.$router.push({
      //   name: 'pay',
      //   params: {
      //     billID: bill.BillID,
      //   }
      // })
    }
  }
}
</script>
5.3.10 MakePayment.vue
	<template>
  <div>
    <h2>网上支付</h2>
    <form @submit.prevent="chargeBill" novalidate>
      <p>账单ID: {{ billId }}</p>
      <p>应付金额: {{ totalCost }}</p>
      <input
        type="number"
        placeholder="输入缴费金额"
        v-model="paidFee"
    />
      <div v-if="errorMsg" class="error">{{ errorMsg }}</div>
      <button type="submit">支付</button>
    </form>
  </div>
</template>

<script>
import { makePayment } from '@/api'

export default {
  data() {
    const receivedData = JSON.parse(localStorage.getItem("chargeData"));
    return {
      billId: receivedData.billID,
      totalCost: receivedData.totalCost,
      paidFee: '',
      errorMsg: '',
    };
  },

  methods: {
    chargeBill() {
      if (!this.validatePaidFee()) {
        return;
      }
      const data = {
        bill_id: this.billId,
        paid_fee: this.paidFee,
      };
      makePayment(data)
        .then((res) => {
          console.log('支付成功', res);
          this.errorMsg = '';
        })
        .catch((err) => {
          console.log('支付失败', err);
        });
    },
    validatePaidFee() {
      if (parseFloat(this.paidFee).toFixed(1) !== parseFloat(this.totalCost).toFixed(1)) {
        this.errorMsg = '支付金额和账单金额不一致,请重新输入';
        return false;
      }
      return true;
    },
  },
    mounted() {
    localStorage.removeItem("chargeData");
  },
};
</script>

<style scoped>
.error {
  color: red;
}
</style>
5.3.11 GetClerkUnpaidBills.vue
<template>
  <div>
    <h2>职员收费页面</h2>
    <form @submit.prevent="chargeBill" novalidate>
      <p>账单ID: {{ billId }}</p>
      <p>应收金额: {{ totalCost }}</p>
      <input
        type="number"
        placeholder="输入实收金额"
        v-model="paidFee"
    />
      <div v-if="errorMsg" class="error">{{ errorMsg }}</div>
      <button type="submit">收费</button>
    </form>
  </div>
</template>
<script>
import { clerkCharge } from '@/api';

export default {
  data() {
    const receivedData = JSON.parse(localStorage.getItem("chargeData"));
    return {
      billId: receivedData.billID,
      totalCost: receivedData.totalCost,
      paidFee: '',
      errorMsg: '',
    };
  },

  methods: {
    chargeBill() {
      if (!this.validatePaidFee()) {
        return;
      }
      const data = {
        bill_id: this.billId,
        paid_fee: this.paidFee,
      };
      clerkCharge(data)
        .then((res) => {
          console.log('收费成功', res);
          this.errorMsg = '';
        })
        .catch((err) => {
          console.log('收费失败', err);
        });
    },
    validatePaidFee() {
      if (parseFloat(this.paidFee).toFixed(1) !== parseFloat(this.totalCost).toFixed(1)) {
        this.errorMsg = '收费金额和账单金额不一致,请重新输入';
        return false;
      }
      return true;
    },
  },
    mounted() {
    localStorage.removeItem("chargeData");
  },
};
</script>

<style scoped>
.error {
  color: red;
}
</style>
5.3.12 ClerkCharge.vue
<template>
  <div>
    <h2>职员收费页面</h2>
    <el-form @submit.prevent="chargeBill" novalidate>
      <p>账单ID: {{ billId }}</p>
      <p>应收金额: {{ totalCost }}</p>
      <el-input type="number" placeholder="输入实收金额" v-model="paidFee" />
      <div v-if="errorMsg" class="error">{{ errorMsg }}</div>
      <el-button type="primary" native-type="submit">收费</el-button>
    </el-form>
  </div>
</template>

<script>
import { clerkCharge } from "@/api";

export default {
  data() {
    const receivedData = JSON.parse(localStorage.getItem("chargeData"));
    return {
      billId: receivedData.billID,
      totalCost: receivedData.totalCost,
      paidFee: "",
      errorMsg: "",
    };
  },

  methods: {
    async chargeBill() {
      if (!this.validatePaidFee()) {
        return;
      }
      const data = {
        bill_id: this.billId,
        paid_fee: this.paidFee,
      };
      try {
        const res = await clerkCharge(data);
        console.log("收费成功", res);
        this.errorMsg = "";
      } catch (err) {
        console.log("收费失败", err);
      }
    },

    validatePaidFee() {
      if (
        parseFloat(this.paidFee).toFixed(1) !==
        parseFloat(this.totalCost).toFixed(1)
      ) {
        this.errorMsg = "收费金额和账单金额不一致,请重新输入";
        return false;
      }
      return true;
    },
  },

  mounted() {
    localStorage.removeItem("chargeData");
  },
};
</script>

<style scoped>
.error {
  color: red;
}
</style>
5.3.13 GetClerkUnpaidBills.vue
<template>
  <div>
    <button @click="getUnpaidBills">获取未付账单</button>
    <button @click="toHome">回首页</button>
    <table>
      <thead>
        <tr>
          <th>账单ID</th>
          <th>用户ID</th>
          <th>应收金额</th>
          <th>账期</th>
          <th>还款状态</th>
          <th>操作</th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="bill in bills" :key="bill.BillID">
          <td>{{ bill.BillID }}</td>
          <td>{{ bill.UserID }}</td>
          <td>{{ bill.TotalCost }}</td>
          <td>{{ bill.Year }}{{ bill.Month }}</td>
          <td>{{ bill.PaidStatus }}</td>
          <td>
            <button @click="toCharge(bill)">收费</button>
          </td>
        </tr>
      </tbody>
    </table>
  </div>
</template>
<script>
import { getClerkUnpaidBills } from '@/api';

export default {
  data() {
    return {
      bills: [],
    };
  },
  methods: {
    getUnpaidBills() {
      console.log('调用获取未付账单方法');
      getClerkUnpaidBills()
        .then((res) => {
          console.log('获取未付账单成功,结果:', res);
          this.bills = res.data.data;
          console.log('bills:', this.bills);
        })
        .catch((err) => {
          console.log('获取未付账单失败,错误:', err);
        });
    },
    toHome() {
      this.$router.push('/');
    },
    // toCharge(bill) {
    //   this.$router.push({
    //     name: 'charge',
    //     // query: { billID: bill.BillID, totalCost: bill.TotalCost },
    //   });
    // },   
     toCharge(bill) {
      const dataToSave = { billID: bill.BillID, totalCost: bill.TotalCost };
    localStorage.setItem("chargeData", JSON.stringify(dataToSave));
    this.$router.push({ name: "charge" });
    },
  },
};
</script>
5.3.14 UsersoftDel.vue
<template>
  <div>
     <h1>软删除用户</h1>
     <el-form ref="form" :model="formData" label-width="100px">
       <el-form-item label="用户ID">
         <el-input v-model="formData.user_id"></el-input>
       </el-form-item>
       <el-form-item>
         <el-button type="danger" @click="softDeleteUser()">软删除</el-button>
       </el-form-item>
     </el-form>
   </div>
 </template>
 
 <script>
 import { softDelete } from '@/api';
 
 export default {
   data() {
     return {
       formData: {
         user_id: '',
       },
     };
   },
   methods: {
     async softDeleteUser() {
       try {
         console.log('软删除方法开始调用', this.formData);
 
         await softDelete({ user_id: this.formData.user_id });
 
         console.log('软删除成功', this.formData);
 
         this.$message({
           message: '软删除成功!',
           type: 'success',
         });
         // 在这里添加其他代码以更新您的 UI 或进行其他操作
       } catch (err) {
         console.error('软删除失败', err);
 
         this.$message({
           message:'软删除失败!',
           type: 'error',
         });
         // 在这里添加其他代码以更新您的 UI 或进行其他操作
       }
     },
   },
 };
 </script>
5.3.15 index.js
// vue-web\src\router\index.js
import { createRouter, createWebHistory } from 'vue-router';
import HomePage from '../views/HomePage.vue'
import ClerkPage from '../views/ClerkPage.vue'
import UserRegister from '../views/UserRegister.vue'  
import UserLogin from '../views/UserLogin.vue'  
import UserhardDel from '../views/UserhardDel.vue'  
import UsersoftDel from '../views/UsersoftDel.vue'  
import UserProfile from '../views/UserProfile.vue'
import UpdateProfile from '../views/UpdateProfile.vue'
import GenerateUnpaidBill from '../views/GenerateUnpaidBill.vue';
import QueryUnpaidBills from '../views/QueryUnpaidBills';
import MakePayment from '../views/MakePayment.vue';
import GetClerkUnpaidBills from '../views/GetClerkUnpaidBills.vue';
import ClerkCharge from '../views/ClerkCharge.vue';


// rest of your router configurations ...

const routes = [
  {
    path: '/',
    component: HomePage
  },  
  {
    path: '/c',
    component:ClerkPage 
  },

  {
    path: '/login',
    component: UserLogin
  },  
  {
    path: '/users/register',
    component: UserRegister
  },
  {
    path: '/users/soft_delete',
    component: UsersoftDel
  }, {
    path: '/users/hard_delete',
    component: UserhardDel
  },
  {
    path: '/profile',
    component: UserProfile,
  },
  {
    path: '/profile/update',
    component: UpdateProfile
  },
  {
    path: '/bills/generate',
    component: GenerateUnpaidBill
  },
  {
    path: '/bills',
    component: QueryUnpaidBills
  },
  {
    name: 'pay',
    path: '/payments',
    component: MakePayment
  },
  {
    path: '/clerk/unpaid_bills',
    component: GetClerkUnpaidBills
  },
  {
    name: 'charge',
    path: '/clerk/charge',
    component: ClerkCharge
  }
];

const router = createRouter({
  history: createWebHistory(),
  routes,
}); 

export default router;
5.3.16 vue.config.js
// vue-web\src\router\index.js
import { createRouter, createWebHistory } from 'vue-router';
import HomePage from '../views/HomePage.vue'
import ClerkPage from '../views/ClerkPage.vue'
import UserRegister from '../views/UserRegister.vue'  
import UserLogin from '../views/UserLogin.vue'  
import UserProfile from '../views/UserProfile.vue'
import UpdateProfile from '../views/UpdateProfile.vue'
import GenerateUnpaidBill from '../views/GenerateUnpaidBill.vue';
import QueryUnpaidBills from '../views/QueryUnpaidBills';
import MakePayment from '../views/MakePayment.vue';
import GetClerkUnpaidBills from '../views/GetClerkUnpaidBills.vue';
import ClerkCharge from '../views/ClerkCharge.vue';


// rest of your router configurations ...

const routes = [
  {
    path: '/',
    component: HomePage
  },  
  {
    path: '/c',
    component:ClerkPage 
  },

  {
    path: '/login',
    component: UserLogin
  },  
  {
    path: '/register',
    component: UserRegister
  },
  {
    path: '/profile',
    component: UserProfile,
  },
  {
    path: '/profile/update',
    component: UpdateProfile
  },
  {
    path: '/bills/generate',
    component: GenerateUnpaidBill
  },
  {
    path: '/bills',
    component: QueryUnpaidBills
  },
  {
    name: 'pay',
    path: '/payments',
    component: MakePayment
  },
  {
    path: '/clerk/unpaid_bills',
    component: GetClerkUnpaidBills
  },
  {
    name: 'charge',
    path: '/clerk/charge',
    component: ClerkCharge
  }
];

const router = createRouter({
  history: createWebHistory(),
  routes,
}); 

export default router;
5.3.17 api.js
// vue-web\src\api.js
import axios from 'axios'
import jwt_decode from 'jwt-decode'
const API_URL = 'http://localhost:5000'
// axios配置
axios.defaults.baseURL = API_URL
axios.defaults.headers.post['Content-Type'] = 'application/json'
axios.defaults.headers.delete['Content-Type'] = 'application/json'
axios.defaults.headers.get['Authorization'] = `Bearer ${localStorage.getItem('access_token')}`

export const login = (data) => {
  return new Promise((resolve, reject) => {
    axios.post('/login', data)
      .then(res => {
        resolve(res)
      })
      .catch(err => {
        reject(err)
      })
  })
}
// 获取用户信息
export const getProfile = () => {
  return new Promise((resolve, reject) => {
    axios.get('/profile', {   // 增加配置对象,设置请求头
      headers: {
        Authorization: `Bearer ${localStorage.getItem('access_token')}`
      }
    })  
      .then(res => {
        resolve(res)
      })
      .catch(err => {
        reject(err)
      })
  })
}
 

// 更新用户信息
export const updateProfile = (user) => {
  return new Promise((resolve, reject) => {
    axios.put('/profile/update', user, {
      headers: {
        Authorization: `Bearer ${localStorage.getItem('access_token')}`  
      }
    })
    .then(res => {
      resolve(res)
    })
    .catch(err => {
      reject(err) 
    })
  })
}

export const logout = () => {
  return new Promise((resolve, reject) => {
    const access_token = localStorage.getItem('access_token')
    if (!access_token) {
      reject(new Error('No access_token found.'))
    }
    axios.post('/logout', {}, {
      headers: {
        Authorization: `Bearer ${access_token}`
      }
    })
    .then(res => {
      resolve(res) 
      localStorage.removeItem('access_token')
    })
    .catch(err => {
      reject(err)
    })
  })
}

export const register = (data) => { 
  // 使用用户名、密码和身份证号进行注册
  console.log('调用注册接口,参数:', data)
  return new Promise((resolve, reject) => {
    axios.post('/users/register', data)  
    .then(res => {  
      // 注册成功,返回结果  
      console.log('注册成功:', res.data)
      resolve(res)
    })
    .catch(err => {
      // 注册失败,返回错误 
      console.log('注册失败:', err.response.data)  
      reject(err)
    })
  })
} 



export const generateUnpaidBill = (data) => {
  // 获取本地存储的用户角色
  // 解码 access_token,获取 claims
  const accessToken = localStorage.getItem('access_token')
  const claims = jwt_decode(accessToken, 'secret')
  console.log('claims:', claims)
  const role = claims.privilege
  console.log('调用生成未付账单接口, 参数:', data,role)
  // 方法主体结构
  return new Promise((resolve, reject) => {
    // 权限判断
    if (role !== 0) {        
      return reject('权限不足,无法生成未付账单')
    }
    
    axios.post('/bills/generate', data, {
      headers: {
        Authorization: `Bearer ${accessToken}`  
      }
    })
    .then(res => {
      console.log('生成未付账单成功, 结果:', res.data)  
      resolve(res)
    }).catch(err => {
      console.log('生成未付账单失败, 错误:', err.response.data)    
      reject(err)
    })
  })   
}

export const queryUnpaidBills = () => {
  
  return new Promise((resolve, reject) => {
   
    axios.get('/bills', {
      headers: {
        Authorization: `Bearer ${localStorage.getItem('access_token')}`  
      }
    }).then(res => {
    console.log('查询未付账单成功, 结果:', res.data);
    resolve(res)
  }).catch(err => {
    console.log('查询未付账单失败, 错误:', err);
    reject(err)
  });
})
}

export const makePayment = (data) => {
  
  console.log('调用付款接口, 参数:', data);
  return new Promise((resolve, reject) => {
    axios.post('/payments', data, {
      headers: {
        Authorization: `Bearer ${localStorage.getItem('access_token')}`  
      }
    }).then(res => {
    console.log('付款成功, 结果:', res.data);
    resolve(res)
  }).catch(err => {
    console.log('付款失败, 错误:', err);
    reject(err)
  });
})
}

export const getClerkUnpaidBills = () => {
  // 获取本地存储的用户角色
  const accessToken = localStorage.getItem('access_token')
  const claims = jwt_decode(accessToken, 'secret')
  console.log('claims:', claims)
  const role = claims.privilege

  console.log('调用获取职员未付账单接口', {
    headers: {
      Authorization: `Bearer ${localStorage.getItem('access_token')}`  
    }

  });
  return new Promise((resolve, reject) => {
     // 权限判断
     if (role !== 1) {  
      return reject('权限不足,无法生成未付账单')
    }
    axios.get('/clerk/unpaid_bills').then(res => {
    console.log('获取职员未付账单成功,结果:', res);
    resolve(res)
  }).catch(err => {
    console.log('获取职员未付账单失败, 错误:', err.data);
    reject(err)
  });
})
}

export const clerkCharge = (data) => {
  // 获取本地存储的用户角色
  const accessToken = localStorage.getItem('access_token')
  const claims = jwt_decode(accessToken, 'secret')
  console.log('claims:', claims)
  const role = claims.privilege
  console.log('调用职员收费接口, 参数:', data, {
    headers: {
      Authorization: `Bearer ${localStorage.getItem('access_token')}`  
    }
  });
  return new Promise((resolve, reject) => {
    if (role !== 1) {  
      return reject('权限不足,无法生成未付账单')
    }
    axios.post('/clerk/charge', data).then(res => {
    console.log('职员收费成功, 结果:', res.data);
    resolve(res)
  }).catch(err => {
    console.log('职员收费失败, 错误:', err.data);
    reject(err)
  });
})
}
export const hardDelete = (data) => {
  // 获取本地存储的用户角色
  const accessToken = localStorage.getItem('access_token')
  const claims = jwt_decode(accessToken, 'secret')
  console.log('claims:', claims)
  // const role = claims.privilege
  console.log('调用删除接口, 参数:', data, {
    headers: {
      Authorization: `Bearer ${localStorage.getItem('access_token')}`  
    }
  });
  return new Promise((resolve, reject) => {
    // if (role !== 1) {  
    //   return reject('权限不足,无法删除用户')
    // }
    axios.delete('/users/hard_delete', data).then(res => {
    console.log('删除成功, 结果:', res.data);
    resolve(res)
  }).catch(err => {
    console.log('删除失败, 错误:', err.data);
    reject(err)
  });
})
}
export const softDelete = (data) => {
  // 获取本地存储的用户角色
  const accessToken = localStorage.getItem('access_token')
  const claims = jwt_decode(accessToken, 'secret')
  console.log('claims:', claims)
  // const role = claims.privilege
  console.log('调用删除接口, 参数:', data, {
    headers: {
      Authorization: `Bearer ${localStorage.getItem('access_token')}`  
    }
  });
  return new Promise((resolve, reject) => {
    // if (role !== 1) {  
    //   return reject('权限不足,无法删除用户')
    // }
    axios.post('/users/soft_delete', data).then(res => {
    console.log('删除成功, 结果:', res.data);
    resolve(res)
  }).catch(err => {
    console.log('删除失败, 错误:', err.data);
    reject(err)
  });
})
}

// export const register = (data) => axios.post(`${API_URL}/users/register`, data)
export default axios 

六、总结

通过这个课程设计,我对数据库系统设计有了更深的理解,也掌握了使用流行技术构建项目的方法。这是一次非常棒的学习经历,让我对数据库系统的概念有了全新的认识。

你可能感兴趣的:(朝花夕拾,vue.js,python,数据库,课程设计,flask,vue.js)