仓储系统(简介)

仓库管理系统 -- SpringBoot + Vue3前后端分离的项目:

一、项目介绍:
1.项目描述 √
    为满足日益扩大的仓库管理需求,对仓库中商品进销存进行统一管理,而开发了此系统。系统主要包含:
    RBAC:用户角色权限控制
    1>用户管理(查询用户、添加用户、修改用户、删除用户、导出数据、批量删除、禁用/启用用户、重置密码、分配角色、更改权限)
    2>角色管理(查询角色、添加角色、修改角色、删除角色、导出数据、禁用/启用角色、更改权限)
    3>权限管理(查询权限、添加权限、修改权限、删除角色、禁用/启用权限)

    4>商品管理(查询商品、添加商品、修改商品、商品审核等)
    5>商品分类管理(商品分类的添加、商品分类的查询、商品分类的修改、商品分类的删除等)
    6>采购管理(我的采购单、添加采购单、采购单的审核等)
    7>入库管理(入库单、保存入库单、确认入库等)
    8>出库管理(出库单、保存出库单、审核出库单等)
    9>统计管理(各个仓库库存信息、仓库占有比、仓库存储走势、出入库信息统计、采购量占比、实时数据监测)
    10>调货管理(调货单查询、确认调货)
    11>仓库管理(查询仓库、添加仓库、修改仓库、删除仓库、导出数据)
    12>供货商管理(供货商添加、供货商修改、供货商的查询等)
    13>品牌管理(品牌添加、品牌修改、品牌的查询等)
    14>产地管理(产地添加、产地修改、产地的查询等)
    15>站内信管理(我的站内信、未读消息、站内信发送、站内信查询等)

2.技术选型 √
  SpringBoot + MyBatis + MySql + Redis + Vue3 + Axios + Element-Plus + Echarts等

3.模块划分

4.个人职责 √

二、还原数据库:

三、搭建前端项目环境并启动项目:
   1>安装node:
     node的介绍:
     node是一个基于Chrome V8引擎的JavaScript运行环境,让JavaScript运行在服务端的开发平台。
     
     npm包管理器的介绍:
     node安装之后一般都会默认安装npm包管理器;类似于linux的yum包管理器,可以给Vue项目下载相关插件、依赖;

     1)将安装压缩包解压就是安装

     2)配置path环境变量

     3)测试安装是否成功:
       node -v:查看node版本
       npm -v:查看npm包管理器的版本
       如果出现警告将node安装目录中的npm.cmd文件中的prefix -g改为prefix --location=global

   2>给npm包管理器配置镜像加速器:
     npm config set registry https://registry.npm.taobao.org 
     npm config get registry  -- 返回https://registry.npm.taobao.org,说明配置成功

   3>使用npm包管理器下载安装yarn包管理器,同时配置镜像加速器:
     yarn包管理器的介绍:
     yarn包管理器跟npm包管理器的作用是一样的,区别就是npm包管理器是node自己的,而yarn包管理器属于第三方(facebook的);
     
     安装yarn包管理器:
     npm install -g yarn

     给yarn包管理器配置镜像加速器:
     yarn config set registry https://registry.npm.taobao.org 
     yarn config get registry  -- 返回https://registry.npm.taobao.org,说明配置成功

   4>使用yarn包管理器为前端Vue项目下载安装所需插件:
     在vue项目目录下执行命令:yarn

   5>启动前端Vue项目:
     在vue项目目录下执行命令:yarn dev

   细节:
   前端vue项目对后台项目的访问路径:
   vue项目目录下的.env文件:
   VITE_WAREHOUSE_CONTEXT_PATH=http://localhost:9999/warehouse
   1)可以在.env文件中通过VITE_WAREHOUSE_CONTEXT_PATH变量修改设置前端Vue项目访问的后台项目的访问路径;
   2)如果不做修改设置那么就要求我们的后台项目的项目路径必须是/warehouse,访问端口必须是9999;


四、搭建后台项目的环境:

1.创建个boot项目 -- 划分包层次:

2.导入依赖

3.启动类配置

4.配置文件的配置

五、登录相关业务:

接口:
1.interface:表示对外暴露的规则,里面定义的全是抽象方法和全局常量。
2.api接口 url接口:请求的url地址。

1.生成验证码图片
  http://localhost:9999/warehouse
  
  /captcha/captchaImage -- 服务器后台生成一张验证码图片,然后响应给前端,前端以img标签展示。

2.登录
  http://localhost:3000/ -- 是前端vue项目的访问地址
  
  前端项目目录下的vite.config.js文件:
      //服务端代理设置
      proxy: {
        //如果访问地址以"/api"开头,则自动代理到变量VITE_WAREHOUSE_CONTEXT_PATH所表示的
        //服务端地址http://localhost:9999/warehouse
        '/api': {
          target: env.VITE_WAREHOUSE_CONTEXT_PATH,
          changeOrigin: true,
          rewrite: path => path.replace(/^\/api/, '')
        }
      }

  url接口/api是前端vue项目的接口,是个代理接口,代理的是后台项目的访问路径;

  所以只要是前端vue项目发出的请求的地址是http://localhost:3000/api/xxx都是发给后台项目的,访问的后台向的具体的url接口;


  http://localhost:3000/api/login -- 访问的是后台项目的/login接口;
  {
   userCode:"admin"
   userPwd:"123456"
   verificationCode:"xax4"
  }


3.登录限制

4.获取登录用户信息

5.加载权限菜单树

6.退出登录
     


--------------------------------------------------------------------

token -- 令牌 -- 会话技术 -- 登录成功之后,在一段时间之内不需要重复登录便可以直接访问系统资源;

常见的会话技术:
session:
 弊端是只适合单体应用,不适用于分布式微服务集群的项目;

token -- 令牌 -- 一段字符串:
 是适用于分布式微服务集群的项目的会话技术;


jwt token:

头部:
{
  "alg": "HS256",
  "typ": "JWT"
}
载体 -- 存放有用户信息:
{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}
签名:
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret
)

base64UrlEncode(header) . base64UrlEncode(payload) . HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret))


五、登录相关业务:

3.登录限制
  必须登录成功之后才能访问系统资源。

  使用过滤器实现:
  在后台项目中配置一个过滤器,会拦截前端发出的所有请求,拦截之后判断用户是否已经登录
  来决定是否允许访问系统资源(url接口);

  回顾SpringBoot中配置原生Servlet中的过滤器:
  1>自定义过滤器 -- 定义一个Filter接口的实现类并重写doFilter()方法,doFilter()方法中
    就是过滤器拦截到请求执行的内容;
  2>向IOC容器中配置FilterRegistrationBean的bean对象,再将我们自定义的Servlet的过滤器
    注册给FilterRegistrationBean的bean对象 -- 注册过滤器

4.获取登录用户信息
  /curr-user -- 拿到前端归还的token,使用token工具类解析token的封装方法拿到从token中
  解析出的用户信息并封装到的CurrentUser对象;

5.加载权限菜单树
  /user/auth-list

  加载用户权限菜单树的方式:
  1.后台系统中是查询出用户权限下的所有菜单List<Auth>,然后将用户所有菜单的List<Auth>响应
    给前端,前端框架就使用菜单树组件通过用户所有菜单的List<Auth>中每个菜单的id和pid的关系
    生成菜单树; -- 菜单树是前端生成的;
  
  2.后台系统中是查询出用户权限下的所有菜单List<Auth>,然后将用户所有菜单的List<Auth>转成
    菜单树List<Auth>,最后将菜单树List<Auth>响应给前端,前端只需要做个循环迭代展示菜单树;
    --- 菜单树是在后端生成的;  √
  
  RBAC:用户角色权限控制
  通过给用户分配不同的角色,再给角色分配不同的菜单权限,进而实现给用户分配不同的菜单权限;
  实现RBAC至少设计到5张表:
  1>用户表user_info:存放用户信息 userId userName...
  2>角色表role:存放角色信息 roleId roleName
  3>用户角色中间表user_role:存放用户角色关系的,体现了给用户分配的角色,而且用户和角色是多对多关系 userId roleId
  4>菜单权限表auth_info:存放菜单信息 authId pid authName 
  5>角色菜单权限中间表role_auth:存放的是角色菜单关系,体现了给角色分配的菜单权限,而且角色和菜单是多对多关系 roleId menuId

6.退出登录
  /logout -- 从redis中删除当前登录的用户的token的键;


--------------------------------------------------------------------------------------------------------------------------

用户管理模块:

1.用户列表
  /user/user-list?userCode=&userType=&userState=&pageSize=5&pageNum=1&totalNum=0
  
  /user/user-list?userCode=a&userType=&userState=1&pageSize=5&pageNum=1&totalNum=0

  pageNum是页码  pageSize是每页行数 -- 分页查询
  

  1)如果参数userCode userType userState没有值,是查询所有用户并分页
    select * from user_info limit 起始行,每页行数;
    select count(*) from user_info;

  2)如果参数userCode userType userState有值,根据账号 用户状态 用户类型分页查询用户并分页
    select * from user_info where user_code like ? and user_type = ? and user_state = 1 limit 起始行,每页行数;
    select count(*) from user_info where user_code like ? and user_type = ? and user_state = 1

  接参问题:
  参数userCode userType userState使用User对象接收并封装;
  参数pageNum页码  pageSize每页行数使用Page对象接收并封装;

  响应数据问题:
  给前端响应封装了所有分页信息的Page对象;

2.添加用户
  /user/addUser
  {
    userCode:"zhangsan"
    userName:"张三"
    userPwd:"123456"
  }

3.启用和禁用用户
  /user/updateState
  {
    createBy:1
    createTime:"2023-06-17 16:38:30"
    getCode:"admin"
    isDelete:"0"
    updateBy:0
    updateTime:null
    userCode: "zhangsan"
    userId:34  √
    userName:"张三"
    userPwd:"c431d451c81e75ffac75a640590ed0a1"
    userState:"1"  √
    userType:null
  }

4.给用户分配角色
  /role/role-list -- 查询所有角色(展示角色出来)
  
  /user/user-role-list/{userId} -- 查询用户已分配的角色(如果是修改角色,需要回显用户已分配角色)
  
  /user/assignRole -- 给用户分配角色
  {
    userId:34
    roleCheckList:["调货", "采购", "入库"]
  }


用户管理模块:

5.删除用户
  /user/deleteUser/{userId} -- 根据用户id删除用户

  /user/deleteUserList -- 根据用户ids批量删除用户
  [34, 33]

  update user_info set is_delete = 1 where user_id in(1,2,3...)

6.修改用户
  /user/updateUser -- 根据用户id修改用户昵称
  {
    userCode:"zhangsan"
    userId:34
    userName:"张三三"
  }

7.重置密码
  /user/updatePwd/{userId} -- 将用户密码根据用户id还原为123456(加密)

-----------------------------------------------------------------------------

角色管理模块:

1.角色列表 -- 分页查询角色
  /role/role-page-list?roleName=&roleCode=&roleState=&pageSize=5&pageNum=1&totalNum=0
  查询所有角色并分页

  role/role-page-list?roleName=a&roleCode=b&roleState=1&pageSize=5&pageNum=1&totalNum=0
  根据角色名称、角色代码、角色状态查询角色并分页

2.添加角色
  /role/role-add
  {
    roleCode:"pandian"
    roleDesc:"盘点"
    roleName:"盘点"
  }
  
  还需要考虑新增的角色是否已经存在 -- 根据角色名称或者角色代码查询是否已有角色;

3.启用和禁用角色
  /role/role-state-update
  {
    createBy:1
    createTime:"2023-06-19 10:46:44"
    getCode:"admin"
    roleCode:"kuaiji"
    roleDesc:"会计"
    roleId:18  √
    roleName:"会计"
    roleState:"1"  √
  }

4.给角色分配权限

  /auth/auth-tree -- 查询所有的权限菜单树

  /role/role-auth?roleId=17 -- 根据角色id查询角色已分配的所有权限
  
  /role/auth-grant
  {
    roleId:"17"
    authIds:[61, 62, 63, 64]
  }
  

5.删除角色
  /role/role-delete/{roleId} -- 根据角色id删除角色
  
6.修改角色
  /role/role-update
  {
    roleDesc:"盘点库存"
    roleId:17
  }


-------------------------------------------------------------------------

商品管理模块:
1.商品列表:
  1>分页查询商品
     /product/store-list -- 查询所有仓库的 -- 给搜索商品仓库下拉框组装数据的

     /product/brand-list -- 查询所有品牌 -- 给搜索商品皮牌下拉框组装数据的

     分页查询商品:
     /product/product-page-list?storeId=&productName=&brandName=&typeName=&supplyName=&placeName=&upDownState=&isOverDate=
     &pageSize=5&pageNum=1&totalNum=0
     查询所有商品并分页

     /product/product-page-list?storeId=1&productName=a&brandName=美的&typeName=b&supplyName=c&placeName=d&upDownState=1&isOverDate=0
     &pageSize=5&pageNum=1&totalNum=0
     根据仓库 商品名称 品牌 类型 供应商 产地 上下架状态 过期与否查询商品并分页 

     请求参数的处理:
     页码pageNum  每页行数pageSize -- Page对象接收并封装
     
     仓库id storeId、商品名称productName、品牌名称brandName、商品分类名称typeName、供应商名称supplyName、产地名placeName、 
     上下架状态upDownState、过期与否isOverDate -- Product对象接收并封装 -- 追加属性品牌名称brandName、商品分类名称typeName、
     供应商名称supplyName、产地名placeName、过期与否isOverDate

     商品列表展示的信息:
     仓库名称 -- Product对象还要追加属性 --- storeName
     单位 -- Product对象还要追加属性 --- unitName


商品管理模块:
1.商品列表:
    
  2>添加商品
     /product/category-tree -- 查询所有分类树 -- 给添加商品提供的

     /product/supply-list -- 查询所有供应商 -- 给添加商品提供的

     /product/place-list -- 查询所有产地 -- 给添加商品提供的

     /product/unit-list -- 查询所有单位 -- 给添加商品提供的
     

     /product/img-upload -- 上传图片
     file:上传的图片的字节数据

     /product/product-add -- 添加商品
     {
       brandId:2
       imgs:"midea_ph.jpg" --- /img/upload/midea_ph.jpg
       inPrice:"2000"
       introduce:"美的 变频  中央空调"
       memPrice:"2400"
       placeId:5
       productDate:"2023-06-20"
       productInvent:50
       productName:"中央空调"
       productNum:"midea_ph_001"
       salePrice:"2500"
       storeId:1
       suppDate:"2026-06-20"
       supplyId:5
       typeId:7
       typeName:"空调"
       unitId:6
     }

  3>修改商品上下架状态
    /product/state-change
    {
      productId:31
      upDownState:1
    }

  4>删除商品
    /product/product-delete/{productId} -- 根据商品id删除单个商品

    /product/product-list-delete --- 批量删除商品
    [31, 25]

    delete from product where product_id in(1,2,3...)

  5>修改商品
    /product/product-update
    {
      brandId:2
      brandName:"美的"
      createBy:1
      createTime:"2023-06-20T10:07:02.000+00:00"
      imgs:"/img/upload/midea_ph.jpg"  "midea_zh.jpg" -- 如果图片没有被修改则还是完整的访问地址,如果被修改了只有图片名称
      inPrice:2000 
      introduce:"美的 变频  中央空调 透心凉"
      isOverDate:null
      memPrice:"2300"
      placeId:5
      placeName:"山东"
      productDate:"2023-06-20"
      productId:31
      productInvent:50
      productName:"空调"
      productNum:"midea_ph_002"
      salePrice:"2400"
      storeId:1
      storeName:"西安仓库"
      suppDate:"2026-06-20"
      supplyId:5
      supplyName:"美的集团股份有限公司"
      typeId:7
      typeName:"空调"
      unitId:6
      unitName:"台"
      upDownState:"0"
    }

  6>添加采购单
    采购流程:
    1)在商品列表针对具体的商品添加采购单 -- 向采购单表buy_list表添加记录(状态是0未入库) -- 预买商品
    2)当商品采购到之后进行入库 -- 在采购单列表做入库操作 -- 向入库单表in_store表添加记录(状态是0未入库),同时修改采购单表buy_list表
      由0改为1入库 -- 准备入库
    3)商品真正的入库 -- 在入库单列表做确认入库操作 -- 将入库单表in_store表的入库单状态由0改为1入库
    
       /purchase/purchase-add
       {
         brandId: 3
     brandName: "海尔"
     buyNum: "50" √   -- fact_buy_num实际采购数量,初值跟buyNum一致
     buyUser: "张三" √
     createBy: 1
     createTime: "2023-06-20T10:07:02.000+00:00"
     imgs: "/img/upload/midea_zh.jpg"
     inPrice: 2000
     introduce: "海尔 变频 中央空调 透心凉"
     isOverDate: null
     memPrice: 2300
     phone: "13666666666" √
     placeId: 5 √
     placeName: "山东"
     productDate: "2023-06-20"
     productId: 31  √
     productInvent: 50
     productName: "空调"
     productNum: "midea_ph_002"
     salePrice: 2400
     storeId: 1 √
     storeName: "西安仓库"
     suppDate: "2026-06-20"
     supplyId: 4  √
     supplyName: "海尔集团"
     typeId: 7
     typeName: "空调"
     unitId: 6
     unitName: "台"
     upDownState: "0"
     updateBy: 1
     updateTime: "2023-06-20T11:46:43.000+00:00"
       }

  7>添加出库单
    出库流程:
    1)在商品列表针对具体的商品添加出库单 -- 向出库单表out_store表添加出库单(状态是0未出库) -- 准备出库
    2)商品真正出库之后,在出库单列表做确认出库操作 -- 将出库单表out_store表的出库状态由0改为1已出库
    
        /outstore/outstore-add
        {
         brandId: 3
     brandName: "海尔"
     createBy: 1
     createTime: "2023-06-20T10:07:02.000+00:00"
     imgs: "/img/upload/midea_zh.jpg"
     inPrice: 2000
     introduce: "海尔 变频 中央空调 透心凉"
     isOverDate: null
     memPrice: 2300
     outNum: "10" √ 
     placeId: 5
     placeName: "山东"
     productDate: "2023-06-20"
     productId: 31 √
     productInvent: 50
     productName: "空调"
     productNum: "midea_ph_002"
     salePrice: 2400 -- out_price字段出库价格给salePrice属性的值
     storeId: 1  √
     storeName: "西安仓库"
     suppDate: "2026-06-20"
     supplyId: 4
     supplyName: "海尔集团"
     typeId: 7
     typeName: "空调"
     unitId: 6
     unitName: "台"
     upDownState: "1"
     updateBy: 1
     updateTime: "2023-06-20T11:46:43.000+00:00"
        }

2.商品分类:
  1>查询商品分类树
    /productCategory/product-category-tree -- 定义新的url接口,调用添加商品时编写的查询商品分类的业务即可;

  2>添加商品分类
    /productCategory/verify-type-code?typeCode=lingshi -- 校验分类编码是否已存在的
       select * from product_type where type_code = ? or type_name = ? -- 根据分类编码或者分类名称查询商品分类,以此判断
       分类编码或分类名称是否已存在

    
    productCategory/type-add
    {
      parentId:0
      typeCode: "xiaomi"
      typeDesc:"发烧  性价比高"
      typeName:"小米手机"
    }

  3>删除商品分类
    /productCategory/type-delete/{typeId} -- 根据分类id或父级分类id删除分类

  4>修改商品分类
    /productCategory/type-update
    {
      parentId:11
      typeCode:"xiaomi"
      typeDesc:"发烧  性价比高  拍照高清"
      typeId:18
      typeName:"红米手机"
    }

采购单管理模块:

1.采购列表
  /purchase/store-list -- 查询所有仓库

  /purchase/purchase-page-list?storeId=1&startTime=&endTime=&productName=&buyUser=&isIn=&pageSize=5&pageNum=1&totalNum=0
  查询所有采购单并分页 或者是 根据仓库id、起止时间、商品名称、采购员、是否入库查询采购单并分页;

  请求参数分析:
  页码pageNum  每页行数pageSize --- Page接收封装
  
  storeId  startTime  endTime productName buyUser isIn -- 使用Purchase对象接收并封装,但是Purchase类中没有属性startTime  
  endTime productName,需要追加;

  采购单列表展示的数据分析:
  额外需要仓库名 商品名 -- 需要给Purchase追加属性storeName productName;
  
  statr <= buyTime <= end

2.删除采购单
  /purchase/purchase-delete/{buyId} -- 根据id删除采购单

3.修改采购单
   /purchase/purchase-update
   {
        buyId: 49  √
    buyNum: 50  √
    buyTime: "2023-06-21T09:59:39.000+00:00"
    buyUser: "张三"
    endTime: null
    factBuyNum: "40"  √
    isIn: "0"
    phone: "13666666666"
    placeId: 5
    productId: 31
    productName: "空调"
    startTime: null
    storeId: 1
    storeName: "西安仓库"
    supplyId: 4
   }

4.生成入库单
  /purchase/in-warehouse-record-add
  {
        buyId: 49
    buyNum: 50
    buyTime: "2023-06-21T09:59:39.000+00:00"
    buyUser: "张三"
    endTime: null
    factBuyNum: 40
    isIn: "0"
    phone: "13666666666"
    placeId: 5
    productId: 31
    productName: "空调"
    startTime: null
    storeId: 1
    storeName: "西安仓库"
    supplyId: 4
  }

-------------------------------------------------------------------

入库单管理模块:

1.入库单列表
  /instore/store-list -- 查询所有仓库

  /instore/instore-page-list?storeId=&productName=&startTime=&endTime=&pageSize=5&pageNum=1&totalNum=0
  查询所有入库单并分页 或者 根据仓库id、商品名称、起止时间查询入库单并分页

  请求参数分析:
  页码pageNum 每页行数pageSize --- Page对象接收并封装
  storeId  productName  startTime  endTime --- InStore对象接收并封装 -- InStore追加属性productName  startTime  endTime

  入库单列表展示的信息分析:
  仓库名称 商品名 入库价格 创建人 -- InStore追加属性storeName productName inPrice userCode

2.确定入库
  1>将入库单状态改为已入库
  2>增加商品的库存
  /instore/instore-confirm
  {
        createBy: 1
    createTime: "2022-04-20T17:30:25.000+00:00"
    endTime: null
    inNum: 50
    inPrice: 2000
    insId: 35
    isIn: "0"
    productId: 22
    productName: "空调"
    startTime: null
    storeId: 1
    storeName: "西安仓库"
    userCode: "admin"
  }

----------------------------------------------------------------------------------

出库单管理模块:

1.出库列表
  /outstore/store-list -- 查询所有仓库

  /outstore/outstore-page-list?storeId=&productName=&startTime=&endTime=&isOut=&pageSize=5&pageNum=1&totalNum=0
  查询所有出库单并分页 或者 根据仓库id 商品名称 起止时间 是否出库查询出库单并分页;

  请求参数的分析:
  页码pageNum 每页行数pageSize --- Page对象接收并封装
  storeId  productName  startTime  endTime  isOut --- OutStore对象接收并封装 -- OutStore类追加属性productName  startTime  endTime 

  出库单列表展示的数据的分析:
  仓库名称 商品名称 创建人  --- OutStore类追加属性storeName productName userCode

2.确定出库
  1>将出库单状态改为1已出库
  2>修改商品的库存 -- 减库存

  /outstore/outstore-confirm
  {
        createBy: 1
    createTime: "2023-06-20T16:06:11.000+00:00"
    endTime: null
    isOut: "0"
    outNum: 10
    outPrice: 2400
    outsId: 14
    productId: 31
    productName: "空调"
    salePrice: null
    startTime: null
    storeId: 1
    storeName: "西安仓库"
    tallyId: null
    userCode: "admin"
   }

---------------------------------------------------------------------

统计查询:
前端使用echarts框架,后台负责查询组装数据,然后将后台查询到的数据响应给前端,前端再以
echarts框架所需的图形结构  格式填充到图形中。

/statistics/store-invent -- 查询每个仓库存储的商品的数量;

select t1.store_id, t1_store_name, ifnull(sum(product_invent),0) 
from store t1, product t2
where t1.store_id = t2.store_id
group by t1.store_id, t1_store_name

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值