蓝天筹项目开发记录

项目功能分析

  1. 做这个小程序的本意是,我曾经参加过我家乡的志愿者活动,然后加入的志愿者组织是家乡独自成立的一支

    有着上千名成员的志愿者团队。因为名为蓝天高凉志愿服务队,所以起了名字叫蓝天筹,希望能做出一个为家乡服务的小程序。

  2. 首页显示的功能:显示所有(由蓝天志愿队的会长或部长发起的众筹项目,这样确保都是经过组织上鉴定和实地考察帮助者的真实性)

  3. 首页有根据不同类型的排序功能,比如根据不同众筹项目的类型,孤寡老人,贫困学生,留守儿童等。

  4. 还有根据众筹项目的进展进度排序,有未完成,即将完成,已完成,已结束。(根据当前众筹金额与目标筹集金额做比例运算,而动态修改类型)

  5. 有根据目标筹集金额的高低排序

  6. 首页具有上拉到底加载更多的功能。

  7. 底部导航栏的第二个为添加项目(设计为只能通过管理员账号登陆实现,确保项目的真实性,必须由志愿者组织发起)

  8. 添加项目详情页则填写一些帮助者的信息,详情,上传相关图片。

  9. 首页里点击具体项目,能跳转项目详情页,能查看项目和帮助者的信息,还能查看照片。

  10. 在详情页具有我要帮帮他的按钮,(设计为模拟捐款和留言的功能)

  11. 筹集人的微信头像和昵称,还有众筹金额,留言都会显示在详情页。

技术选型

  1. SSM框架
  2. mysql
  3. linux作为服务器
  4. 前端是微信小程序

项目流程设计

1. 界面设计

1.1 添加筹集项目的管理员登陆页
1.1.1 后端用salt+password的方式校验和存储密码

加密:

public class PasswordEncryptor {
    //这是一个自定义的hexDigits,如果黑客不知道这串东西,是不能穷举破解出来的,知道salt也没用
    private final static String[] hexDigits = {"0", "1", "2", "3", "4", "5",
            "6", "!", "#", "@", "a", "b", "c", "d", "*", "f", "g", "F"};

    private String salt;    //salt
    private String algorithm;   //散列算法

    public PasswordEncryptor(String salt,String algorithm) {
        this.salt = salt;
        this.algorithm = algorithm;
    }

    //加密
    public String encode(String rawPassword){
        try {
            MessageDigest digest = MessageDigest.getInstance(algorithm);
            return byteArrayToHex(digest.digest(mergePasswordAndSalt(rawPassword).getBytes("UTF-8")));
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return null;
    }

    //合并salt + password
    private  String mergePasswordAndSalt(String rawPassword){
        if(rawPassword==null){
            rawPassword = "";
        }

        if(salt.equals("")||salt==null){
            return rawPassword;
        }else{
            return rawPassword+"{"+salt+"}";        //用特殊的方式拼接salt
        }
    }

    /**
     * 字节数组转16进制
     */

    private static String byteArrayToHex(byte[] b){
        StringBuffer stringBuffer = new StringBuffer();
        for(int i=0;i<b.length;i++){
            stringBuffer.append(byteToHex(b[i]));
        }
        return stringBuffer.toString();
    }

    private static String byteToHex(byte b){
        int n = b;
        if(n<0){
            n+=256;
        }
        int d1 = n / hexDigits.length;
        int d2 = n % hexDigits.length;

        return hexDigits[d1]+hexDigits[d2];
    }
    
    //初始的管理员密码
    public static void main(String[] args) {
        String salt = UUID.randomUUID().toString();
        PasswordEncryptor encoderMd5 = new PasswordEncryptor(salt, "sha-256");
        String encodedPassword = encoderMd5.encode("csyzhanpeng123456");
        System.out.println("加密后密码:" + encodedPassword + "\n密码长度:" + encodedPassword.length());
        System.out.println("salt:" + salt);
    }
}

前后端校验

@Service
public class UserPasswordServiceImpl implements UserPasswordService {
    @Autowired
    private SysAdminMapper sysAdminMapper;

    @Override
    public boolean isValid(String username, String password) {
        SysAdminExample example = new SysAdminExample();
        example.or().andUsernameEqualTo(username);
        List<SysAdmin> admins = sysAdminMapper.selectByExample(example);

        SysAdmin sysAdmin = new SysAdmin();

        //说明找到了这个username,后面就是检测密码
        if(admins!=null&&admins.size()!=0){
            sysAdmin = admins.get(0);

            //校验
            PasswordEncryptor encryptor = new PasswordEncryptor(sysAdmin.getSalt(), "sha-256");
            String encodePassword = encryptor.encode(password);

            if(encodePassword.equals(sysAdmin.getPassword())){
                return true;
            }else{
                return false;
            }
        }else{
            return false;
        }
    }
}

salt+password参考链接:http://www.cnblogs.com/xiaochangwei/p/5942456.html

前端通过存在本地缓存,缓存管理员的登陆态。

通过在page的onShow生命周期,通过判断缓存,来达到拦截页面(检测是否具有权限)

  onShow:function(e){
    let that = this;
    wx.getStorage({
      key: 'login_key',
      success: function(res) {
        wx.request({
          url: baseUrl + 'item/itemTypes',
          method: "GET",
          success: function (res) {
            console.log(res);
            that.setData({
              itemTypes: res.data.extend.itemTypes
            })
          }
        })
      },
      fail:function(){
        wx.navigateTo({
          url: '../login/login',
        })
      }
    })

  },
  formSubmit: function (e) {
    wx.showLoading({
      title: '登录中...',
    })
    console.log(e);
    this.setData({ disabled: true });
    wx.request({
      url: baseUrl+"login",
      method:"POST",
      data: {
        username: e.detail.value.no,
        password: e.detail.value.pwd
      },
      header: {
        'content-type': 'application/x-www-form-urlencoded'
      },
      success: function (res) {
        console.log(res);
        if (res.data.code == 200) {
          // 设置本地缓存
          wx.setStorageSync('login_key', res.data.extend.login_key);
          wx.showToast({
            title: "管理员登录成功",
            icon: 'success',
            duration: 2000
          })
          setTimeout(function () {
            wx.switchTab({
              url: '../add/add',
            })
          }, 2000)
        } else {
          wx.showToast({
            title: "用户名或密码错误",
            icon: 'none',
            duration: 2000
          })
        }
      }
    })
  },
1.2 登陆后个人信息填写页
1.3 筹款项目首页展示

1364699-20190531162437577-888236126.png

1.3.1 轮播图(仅宣传用)
1.3.2 多选下拉菜单的实现

功能:三级级联菜单 项目类型 + 项目进度情况 + 按目标筹集金额从低到高(从高到低)

总结:

  1. 下拉小按钮

    /* 这里的icon用border渲染出来,而不是用字体图标 */
    
    .icon{
      margin-left: 10rpx;
      margin-top: 16rpx;
      display: inline-block;
    
      border: 10rpx solid transparent;
      border-top: 10rpx solid #666;
    }

效果:

1364699-20190531162554791-1061721714.png

参考链接:https://www.cnblogs.com/pssp/p/5884956.html

1364699-20190531162615864-1032593602.png

  1. 下拉多选菜单

    思路:用大view包小view,通过点击状态和 标志记录位来 打开(使下拉菜单显示), 当选中其中一个或者再点

    一次按钮时,将会切换打开状态或者关闭下拉菜单

    核心:切换 ,记录打开状态位,通过点击事件进行状态位切换(这里的状态位为下拉导航索引)。前端根据状态位判断是否渲染出来

      data:{
        //筹集项目列表
        items:[],
        // 筹集项目类型
        itemTypes:[],
    
        // 进度类型
        processTypes:[],
    
        //排序类型
        sortTypes:[
          { id: 1, sortTypeName:"按目标金额从低到高"},
          { id: 2, sortTypeName: "按目标金额从高到低"}
        ],
    
        // 选中的项目类型
        selectItemType:-1,
    
        // 选中的进度类型
        selectProcessType:-1,
    
        // 选中的排序类型
        selectSortType:-1,
    
        // 显示下拉菜单导航索引
        showNavIndex:0,
    
        // 各类型菜单打开(滑动))状态
        itemTypeOpen:false,
    
        processOpen:false,
    
        sortOpen:false

    这里只写其中的项目下拉菜单的显示

    下拉菜单触发按钮

        <view class="nav-child" bindtap='listItemType' data-nav="1">
          <view class='content'>项目类型</view>
          <view class="icon"></view>
        </view>

    下拉菜单条目

      <view class="itemTypeMenu {{itemTypeOpen? 'slidown':'slidup'}}"  
            wx:if='{{showNavIndex==1}}' wx:for-index="index">
        <view class="itemType" bindtap='selectItemType' data-itemTypeId='-1'>
          不限
        </view>
        <view class="itemType {{selectItemType==(index+1)?'highlight':''}}" wx:for="{{itemTypes}}" wx:for-item="itemType" wx:key="itemTypeId"
          data-itemTypeId="{{itemType.itemTypeId}}"
          bindtap='selectItemType'
        >
          {{itemType.itemTypeName}}
        </view>
      </view>

    点击事件的处理逻辑:

      listItemType:function(e){
        console.log(this.data.itemTypeOpen)
        // 如果已经打开了,再按一次就是关闭。
        if (this.data.itemTypeOpen){
          this.setData({
            itemTypeOpen: false,
            showNavIndex:0
          })
        }else{
          this.setData({
            itemTypeOpen: true,
              //切换 要显示的菜单导航
            showNavIndex:e.currentTarget.dataset.nav
          }) 
        }
      },

    选中事件的处理逻辑:

      selectItemType: function (e) {
        console.log(e.currentTarget)
        // 注意;这里的data-传过来的属性会自动换成小写
        let id = e.currentTarget.dataset.itemtypeid;
    
        if (id == -1) {
          this.setData({
            // 不限,也就是提交的这个筛选条件为空
            selectItemType: -1,
            itemTypeOpen: false,
            showNavIndex: 0
          })
        } else {
          this.setData({
    
            selectItemType: id,
            itemTypeOpen: false,
            showNavIndex: 0
          })
        }
    
        let that = this;
    
        // 找出符合条件的items
        that.request_findItem(that);
      },

    根据条件,查询符合的项目

      // 封装一个根据条件查询得请求函数
      request_findItem:function(that){
        wx.request({
          url: baseUrl + 'item/findItems',
          data: {
            itemType: that.data.selectItemType,
            itemProcessType: that.data.selectProcessType,
            sortType: that.data.selectSortType
          },
          method: "GET",
          success: function (res) {
            that.setData({
              items: res.data.extend.items
            })
          }
        })
      }

    总结:

    1. css就不贴出来了,我选择了调一下margin,让菜单条目对应菜单导肮,以及选中高亮

    选中高亮的实现原理:判断Id与列表渲染的索引index是否匹配。如果匹配了就渲染高亮class

        <view class="itemType 
                     {{selectItemType==(index+1)?'highlight':''}}" 
              wx:for="{{itemTypes}}" wx:for-item="itemType" wx:key="itemTypeId"
          data-itemTypeId="{{itemType.itemTypeId}}"
          bindtap='selectItemType'
        >
    1. 不设置筛选条件,菜单条目为 不限,通过设置为-1来让后端根据-1 做条件的判断

          @Override
          public List<RaiseItem> getItemsByType(FindItemDTO dto) {
              RaiseItemExample example = new RaiseItemExample();
      
              RaiseItemExample.Criteria criteria = example.or();
      
              Integer itemType = dto.getItemType();
              Integer itemProcessType = dto.getItemProcessType();
              if(itemType!=-1){
                  criteria.andItemTypeEqualTo(itemType);
              }
      
              if(itemProcessType!=-1){
                  criteria.andItemProcessTypeEqualTo(itemProcessType);
              }
      
              Integer sortType = dto.getSortType();
      
              if(sortType!=-1){
                  if(sortType.equals(1)){
                      example.setOrderByClause("raise_target ASC");
                  }else if(sortType.equals(2)){
                      example.setOrderByClause("raise_target DESC");
                  }
              }
              return raiseItemMapper.selectByExample(example);
          }

      效果演示:

1364699-20190531165348609-693616232.png

1364699-20190531165506544-547126770.png

1.3.3 列表每个条目的设计
1.3.4 上拉加载更多的分页实现

参考链接:https://blog.csdn.net/u012927188/article/details/73369201/

思路 :

  1. 后端返回分页数据

  2. 小程序:

    1. 加载更多组件




      ​ 正在加载

      ​ ```

       /* 上拉加载更多 */
    
       .weui-loading {
         margin: 0 5px;
         width: 20px;
         height: 20px;
         display: inline-block;
         vertical-align: middle;
         -webkit-animation: weuiLoading 1s steps(12, end) infinite;
         animation: weuiLoading 1s steps(12, end) infinite;
           /* base64的格式 */
         background: transparent url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMjAiIGhlaWdodD0iMTIwIiB2aWV3Qm94PSIwIDAgMTAwIDEwMCI+PHBhdGggZmlsbD0ibm9uZSIgZD0iTTAgMGgxMDB2MTAwSDB6Ii8+PHJlY3Qgd2lkdGg9IjciIGhlaWdodD0iMjAiIHg9IjQ2LjUiIHk9IjQwIiBmaWxsPSIjRTlFOUU5IiByeD0iNSIgcnk9IjUiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAgLTMwKSIvPjxyZWN0IHdpZHRoPSI3IiBoZWlnaHQ9IjIwIiB4PSI0Ni41IiB5PSI0MCIgZmlsbD0iIzk4OTY5NyIgcng9IjUiIHJ5PSI1IiB0cmFuc2Zvcm09InJvdGF0ZSgzMCAxMDUuOTggNjUpIi8+PHJlY3Qgd2lkdGg9IjciIGhlaWdodD0iMjAiIHg9IjQ2LjUiIHk9IjQwIiBmaWxsPSIjOUI5OTlBIiByeD0iNSIgcnk9IjUiIHRyYW5zZm9ybT0icm90YXRlKDYwIDc1Ljk4IDY1KSIvPjxyZWN0IHdpZHRoPSI3IiBoZWlnaHQ9IjIwIiB4PSI0Ni41IiB5PSI0MCIgZmlsbD0iI0EzQTFBMiIgcng9IjUiIHJ5PSI1IiB0cmFuc2Zvcm09InJvdGF0ZSg5MCA2NSA2NSkiLz48cmVjdCB3aWR0aD0iNyIgaGVpZ2h0PSIyMCIgeD0iNDYuNSIgeT0iNDAiIGZpbGw9IiNBQkE5QUEiIHJ4PSI1IiByeT0iNSIgdHJhbnNmb3JtPSJyb3RhdGUoMTIwIDU4LjY2IDY1KSIvPjxyZWN0IHdpZHRoPSI3IiBoZWlnaHQ9IjIwIiB4PSI0Ni41IiB5PSI0MCIgZmlsbD0iI0IyQjJCMiIgcng9IjUiIHJ5PSI1IiB0cmFuc2Zvcm09InJvdGF0ZSgxNTAgNTQuMDIgNjUpIi8+PHJlY3Qgd2lkdGg9IjciIGhlaWdodD0iMjAiIHg9IjQ2LjUiIHk9IjQwIiBmaWxsPSIjQkFCOEI5IiByeD0iNSIgcnk9IjUiIHRyYW5zZm9ybT0icm90YXRlKDE4MCA1MCA2NSkiLz48cmVjdCB3aWR0aD0iNyIgaGVpZ2h0PSIyMCIgeD0iNDYuNSIgeT0iNDAiIGZpbGw9IiNDMkMwQzEiIHJ4PSI1IiByeT0iNSIgdHJhbnNmb3JtPSJyb3RhdGUoLTE1MCA0NS45OCA2NSkiLz48cmVjdCB3aWR0aD0iNyIgaGVpZ2h0PSIyMCIgeD0iNDYuNSIgeT0iNDAiIGZpbGw9IiNDQkNCQ0IiIHJ4PSI1IiByeT0iNSIgdHJhbnNmb3JtPSJyb3RhdGUoLTEyMCA0MS4zNCA2NSkiLz48cmVjdCB3aWR0aD0iNyIgaGVpZ2h0PSIyMCIgeD0iNDYuNSIgeT0iNDAiIGZpbGw9IiNEMkQyRDIiIHJ4PSI1IiByeT0iNSIgdHJhbnNmb3JtPSJyb3RhdGUoLTkwIDM1IDY1KSIvPjxyZWN0IHdpZHRoPSI3IiBoZWlnaHQ9IjIwIiB4PSI0Ni41IiB5PSI0MCIgZmlsbD0iI0RBREFEQSIgcng9IjUiIHJ5PSI1IiB0cmFuc2Zvcm09InJvdGF0ZSgtNjAgMjQuMDIgNjUpIi8+PHJlY3Qgd2lkdGg9IjciIGhlaWdodD0iMjAiIHg9IjQ2LjUiIHk9IjQwIiBmaWxsPSIjRTJFMkUyIiByeD0iNSIgcnk9IjUiIHRyYW5zZm9ybT0icm90YXRlKC0zMCAtNS45OCA2NSkiLz48L3N2Zz4=) no-repeat;
         background-size: 100%;
       }
       .weui-loadmore {
         width: 65%;
         margin: 1.5em auto;
         line-height: 1.6em;
         font-size: 14px;
         text-align: center;
       }
    
       .weui-loadmore__tips {
         display: inline-block;
         vertical-align: middle;
       }
    
    1. 微信小程序 自带的触底函数
         onReachBottom:function(){
           let that = this;
           // 模拟延时,显示加载更多
             // wx.request({
             //   url: baseUrl+'',
             // })
             // that.setData({
    
             // })
           let isLastPage = that.data.pageInfo.isLastPage;
    
           // 不是最后一页,才要请求分页
           if (!isLastPage){
             setTimeout(() => {
               // 判断一下这个触底是常规触底,还是带着条件的触底事件
               let f1 = that.data.selectItemType;
               let f2 = that.data.selectProcessType;
               let f3 = that.data.selectSortType;
               if (f1 != -1 || f2!=-1 || f3!=-1){
                 // 带条件查询 (其实带条件和不带条件其实在后端可以合并为一个接口的))
                 wx.request({
                   url: baseUrl + 'item/findItems',
                   data: {
                     itemType: that.data.selectItemType,
                     itemProcessType: that.data.selectProcessType,
                     sortType: that.data.selectSortType,
                     pn: that.data.pageInfo.pageNum + 1
                   },
                   method: "GET",
                   success: function (res) {
                     let oldItems = that.data.items;
                     let newItems = res.data.extend.pageInfo.list;
                     let pageInfo = res.data.extend.pageInfo;
                     // concat拼接后返回一个新的数组
                     newItems = oldItems.concat(newItems);
                     that.setData({
                       pageInfo: pageInfo,
                       items: newItems,
                       isHideLoadMore: pageInfo.isLastPage ? true : false
                     })
                   }
                 })
               }else{
                  // 不带条件查询
                 wx.request({
                   url: baseUrl + 'item/all',
                   data: {
                     pn: that.data.pageInfo.pageNum + 1
                   },
                   method: "GET",
                   success: function (res) {
                     let oldItems = that.data.items;
                     let newItems = res.data.extend.pageInfo.list;
                     // concat拼接后返回一个新的数组
                     newItems = oldItems.concat(newItems);
                     let pageInfo = res.data.extend.pageInfo;
                     that.setData({
                       pageInfo: pageInfo,
                       items: newItems,
                       isHideLoadMore: pageInfo.isLastPage ? true : false
                     })
                   }
                 })
               }
             }, 1000);
           }
         }
  3. 后端返回分页

    //    获取所有的项目基本信息
        @RequestMapping(value = "/all",method = RequestMethod.GET)
        @ResponseBody
        public Msg getAllItems(@RequestParam(value = "pn",defaultValue = "1") Integer page_num){
            PageHelper.startPage(page_num,5);
    
            List<RaiseItem> items = raiseItemService.getAllItems();
    
            PageInfo<RaiseItem> pageInfo = new PageInfo<>(items);
            return Msg.success().add("pageInfo",pageInfo);
        }
    //    根据类型来查询符合的项目
        @RequestMapping(value = "/findItems",method = RequestMethod.GET)
        @ResponseBody
        public Msg findItems(@RequestParam(value = "pn",defaultValue = "1") Integer page_num,FindItemDTO dto){
    //        开始分页
            PageHelper.startPage(page_num,5);
    
            List<RaiseItem> items = raiseItemService.getItemsByType(dto);
    
            PageInfo<RaiseItem> pageInfo = new PageInfo<>(items);
    
            return Msg.success().add("pageInfo",pageInfo);
        }

    效果:

1364699-20190531165535125-1334224309.png

加载完成后:

1364699-20190531165543544-1168357733.png

1.3.5 新增筹款项目
  1. 普通表单UI

  2. 文件(图片)上传UI

  3. 长按图片删除的操作

    参考链接:https://www.jb51.net/article/140388.htm

    参考JS语法:

    splice() 方法向/从数组中添加/删除项目,然后返回被删除的项目。

    注释:该方法会改变原始数组。

    语法

    arrayObject.splice(index,howmany,item1,.....,itemX)
    参数描述
    index必需。整数,规定添加/删除项目的位置,使用负数可从数组结尾处规定位置。
    howmany必需。要删除的项目数量。如果设置为 0,则不会删除项目。
    item1, ..., itemX可选。向数组添加的新项目。

    返回值

    类型描述
    Array包含被删除项目的新数组,如果有的话。
    data: {  
            Data: [{id:0,value:'a',name:'A' },{id:1,value:'b',name:'B' }], 
            Index: 0,
          currentId
        },

    对象数组的下拉菜单的使用

    <picker class="picker" bindchange="bindChange" value="{{Index}}"  range="{{Data}}" range-key="name">
        <view >
          当前选择:{{Data[Index].name}}
        </view>
    </picker>
      itemTypePickerChange:function(e){
    
       this.setData({
    
         Index:e.detail.value
    
       })
    
      },
1.4 筹款项目详情页
  1. 编写基础的wxml,wxss,js,获取item表和detail表的基础信息,布局用flex嵌套flex合理布局

    效果如下:

1364699-20190531165556849-1737545383.png

1364699-20190531165605979-116257719.png

  1. 这个按钮悬浮固定在底部的实现:

    ```css
    /* 按钮固定高为46px */
    .detail_box{
    margin-bottom: 92rpx;
    }

    /* 固定在底部,这样可以避免内容区的内容过多,让按钮一直看不到 */
    .chou_button{
    1364699-20190531172602535-2075251053.png

 z-index: 999;
    position: fixed;
    bottom: 0;
    width: 100%;
  }
  ```
1.4.1 模仿样式
1.4.2 项目进展(含进度条,时间线+图片+留言)
1.4.3 参与筹款人的名单+留言

效果:
1364699-20190531172711424-397438884.png

  <!-- 筹集人的名单列表 -->
  <view class="raise_person_box">
    <view class="raise_person_title">
    捐助人名单
  </view>
    <view wx:for="{{persons}}" wx:for-key="person.item_person_id" wx:for-item="person" class="raise_person_item">
      <view class="raise_person_item_left">
        <view class="index_pic">
          <image src="{{person.userAvatarUrl}}"></image>
        </view>
      </view>
      <view class="raise_person_item_right">
        <view class="raise_person_item_right_top">
          {{person.userNickName}}
        </view>
        <view class="raise_person_item_right_mid">
          支持了: <text class="mid_money">{{person.raiseMoney}} 元</text>
        </view>
        <view class="raise_person_item_right_bottom">
          {{person.comment}}
        </view>
        <view class="raise_person_item_right_time">
          {{person.raiseTime}}
        </view>
      </view>
    </view>
  </view>

总结:html页面用了flex嵌套布局吧

js部分:onShow()用于获取捐助人名单+留言信息

    wx.request({
      url: baseUrl + 'item/person',
      data:{
        itemId: that.data.itemId,
      },
      method: "GET",
      success: function (res) {

        //调用 处理留言时间的函数,修改返回的数据
        let list = res.data.extend.pageInfo.list;
        for (let i=0;i<list.length;i++){
          let last_time = timeHandle(list[i].raiseTime)
          list[i].raiseTime = last_time;
        }

        that.setData({
          persons: list,
        })
      }
    })

此处要提的是一个特殊的常用需求:就是根据返回的时间戳计算出几天前,几个月前,又或者是具体的月份,年份

js部分:用了一个专门的函数放在单独的js文件,放在utils目录下,被其他的js import引入使用

function commentTimeHandle(dateStr) {
  // dateStr = 2018-09-06 18:47:00" 测试时间
   //获取dataStr的秒数  打印结果--1536230820000
  var publishTime = dateStr / 1000, 
    date = new Date(publishTime * 1000), //获取dateStr的标准格式 console.log(date) 打印结果  Thu Sep 06 2018 18:47:00 GMT+0800 (中国标准时间)
    // 获取date 中的 年 月 日 时 分 秒
    Y = date.getFullYear(),
    M = date.getMonth() + 1,
    D = date.getDate(),
    H = date.getHours(),
    m = date.getMinutes(),
    s = date.getSeconds();
  // 对 月 日 时 分 秒 小于10时, 加0显示 例如: 09-09 09:01
  if (M < 10) {
    M = '0' + M;
  }
  if (D < 10) {
    D = '0' + D;
  }
  if (H < 10) {
    H = '0' + H;
  }
  if (m < 10) {
    m = '0' + m;
  }
  if (s < 10) {
    s = '0' + s;
  }
  // console.log("年", Y); // 年 2018
  // console.log("月", M); // 月 09
  // console.log("日", D); // 日 06
  // console.log("时", H); // 时 18
  // console.log("分", m); // 分 47
  // console.log("秒", s); // 秒 00

  //获取此时此刻日期的秒数
  var nowTime = new Date().getTime() / 1000, 
    diffValue = nowTime - publishTime,  // 获取此时 秒数 与 要处理的日期秒数 之间的差值

    // 一天86400秒 获取相差的天数 取整
    diff_days = parseInt(diffValue / 86400),    
    
    // 一时3600秒
    diff_hours = parseInt(diffValue / 3600),    
    diff_minutes = parseInt(diffValue / 60),
    diff_secodes = parseInt(diffValue);

  if (diff_days > 0 && diff_days < 3) {  //相差天数 0 < diff_days < 3 时, 直接返出
    return diff_days + "天前";
  } else if (diff_days <= 0 && diff_hours > 0) {
    return diff_hours + "小时前";
  } else if (diff_hours <= 0 && diff_minutes > 0) {
    return diff_minutes + "分钟前";
  } else if (diff_secodes < 60) {
    if (diff_secodes <= 0) {
      return "刚刚";
    } else {
      return diff_secodes + "秒前";
    }
  } else if (diff_days >= 3 && diff_days < 30) {
    return M + '-' + D + ' ' + H + ':' + m;
  } else if (diff_days >= 30) {
    return Y + '-' + M + '-' + D + ' ' + H + ':' + m;
  }
}
module.exports = {
  timeHandle: commentTimeHandle
}

如何使用:在JS里引入这个JS文件的函数

import { timeHandle } from '../../utils/timehandle';

分页后端:

//    获取筹集人列表
    @RequestMapping(value = "/person",method = RequestMethod.GET)
    @ResponseBody
    public Msg getPersons(@RequestParam(value = "pn",defaultValue = "1")Integer page_num, Integer itemId){
        PageHelper.startPage(page_num,5);
        List<RaiseItemPerson> persons = raiseItemService.getRaisePersons(itemId);
        PageInfo<RaiseItemPerson> pageInfo = new PageInfo<>(persons);

        return Msg.success().add("pageInfo",pageInfo);
    }
1.4.4 参与众筹的按钮(涉及到微信支付,暂时无法完成。可以模拟)
1.4.4.1 获取微信用户ID,头像,昵称

​ 总结:

  1. 通过微信最新官方文档,用button标签,设置open-type属性,然后绑定指定的事件,可以在JS中

​ 获取到用户头像,昵称 (可在一个按钮绑定两个事件,一个用来获取用户信息,一个用来发出请求)

    <button open-type='getUserInfo' type='primary' bindgetuserinfo="bindGetUserInfo" bindtap='donate'>我要帮帮他</button>
  //获取用户信息
  bindGetUserInfo: function (e) {
    console.log(e.detail.userInfo)
    this.setData({
      userNickName: e.detail.userInfo.nickName,
      userAvatarUrl: e.detail.userInfo.avatarUrl
    })
  }
1.4.4.2 模拟支付页面的模态框

效果:

1364699-20190531172736020-1128721373.png

总结:就是通过按钮点击切换模态框的显示,然后在模态框里模拟微信支付功能以及添加留言

  <!-- modal支付模态框 -->
    <modal id="modal" hidden="{{hiddenmodal}}" title="支付页面" confirm-text="确定" cancel-text="取消" bindcancel="cancel" bindconfirm="confirm">  
      <text style="font-weight:bolder;font-size:35rpx">捐助金额:</text> <input type='text' placeholder="请填写资助金额" class='weui-input' bindinput="bindKeyInput" auto-focus/>
      <text style="font-weight:bolder;font-size:35rpx">留言:</text> <input type='text' placeholder="留言" class='weui-input brief_description' bindinput="bindKeyComment"></input>
  </modal>
  confirm:function(){
    let openId = getApp().globalData.openId;
    console.log("openId: " + openId);

    let that = this;
    wx.request({
      url: baseUrl+'item/donate',
      data:{
        donate_money: that.data.donate_money,
        itemId: that.data.itemId,
        comment: that.data.comment,
        openId: openId,
        userNickName:that.data.userNickName,
        userAvatarUrl: that.data.userAvatarUrl
      },
      method:"POST",
      header:{
        "content-type": "application/x-www-form-urlencoded"
      },
      success:function(res){
        that.setData({
          comment: "",
          donate_money: "",
          hiddenmodal: true,
          hiddenmodal:true
        })
        // 发起请求
        wx.request({
          url: baseUrl + 'item/detail',
          data: {
            itemId: that.data.itemId
          },
          success: function (res) {
            if (res.data.code == 200) {
              that.setData({
                currentTarget: res.data.extend.detail.currentTarget,
                raisePersonNum: res.data.extend.detail.raisePersonNum,
              })
            }
          }
        })
      }
    })
  }
  cancel:function(){
    this.setData({
      donate_money:"",
      comment:"",
      hiddenmodal: true,
    })
  },
      
      //此处省略其他input的处理事件
1.5 新增筹款项目填写页

总结:

    // 发送首页图片。对应首页图片的处理
    wx.uploadFile({
      url: baseUrl + 'item/imageIndex',
      filePath: files[0],
      name: 'img',
      header:{
        'content-type':'application/json'
      },
      success: function (res) {
        
        console.log("res: "+res.data);
        // 微信小程序 uploadFile的坑是接收的是JSON字符串,不会帮你自动转JS对象。所以需要自己解析data
        let data = JSON.parse(res.data);
        

        //获取返回值
        that.setData({
          server_file_index: data.extend.file_index_path
        })
      }
    })

点击提交按钮,图片如何处理

  1. 分成两个接口,一个是首页图片,另一个是详情多个图片urls.把文件名存放在数据库中

      add_submit:function(){
        let that = this;
    
        let item_index = that.data.Index;
    
    
        // 先上传图片,后端处理成功后(通过返回值包含了首页图片路径,
        //以及多个图片展示的路径)回调进行insert
        let files = that.data.files;
    
        // 发送首页图片。对应首页图片的处理
        wx.uploadFile({
          url: baseUrl + 'item/imageIndex',
          filePath: files[0],
          name: 'img',
          header:{
            'content-type':'application/json'
          },
       success: function (res) {
    
              console.log("res: " + res.data);
              // 微信小程序 uploadFile的坑是接收的是JSON字符串,不会帮你自动转JS对象。所以需要自己解析data
              let data = JSON.parse(res.data);
    
    
              //获取返回值
              that.setData({
                server_file_index: data.extend.file_index_path
              })
    
    
              //等server_file_index成功获取后再执行下面的add操作
              let i;
              //循环发送多个详情的图片
              for (i = 1; i < files.length; i++) {
                // 采用闭包,保证索引值正确
                (function (i) {
                  //调用promise处理异步
                  that.GetImage(i, that).then((index) => {
                    //最后一张处理完成
                    if (that.data.server_detail_files.length == (that.data.files.length - 1)) {
                      console.log("开始执行提交add");
                      console.log("index: " + index);
                      console.log("server_detail_file:" + that.data.server_detail_file);
                      // 提交插入请求
                      wx.request({
                        url: baseUrl + '/item/add',
                        method: 'POST',
                        header: {
                          "content-type": "application/x-www-form-urlencoded"
                        },
                        data: {
                          targetPerson: name,
                          itemDescription: description,
                          raiseTarget: money,
                          itemType: that.data.itemTypes[that.data.Index].itemTypeId,
                          createTime: date,
                          description: detail_description,
    
                          picIndexUrl: that.data.server_file_index,
                          picDetailUrls: that.data.server_detail_files.join(',')
                        },
                        success: function (res) {
                          if (res.data.code == 200) {
                            // 清空
                            that.setData({
                              targetPerson: "",
                              itemDescription: "",
                              raiseTarget: "",
                              Index: 0,
                              date: "",
                              detail_description: "",
                              server_file_index: "",
                              server_detail_files: "",
                              files: ""
                            })
                            wx.switchTab({
                              url: '/pages/index/index',
                            })
                          }
    
                        }
                      })
                    }
                  });
                })(i)
              }
            }
    
        }

    基础补习之闭包

    因为for循环,的索引index不会从1,2,3这样,而是执行完了,显示最后一个索引值。需要闭包控制一下。

1364699-20190531172754595-295568524.png

演示:

1364699-20190531172804079-941619070.png

文本测试:

18岁花季少女突发心脏病。急需救助!

小红成绩优异,家里经济贫困,在石鼓镇。父母残疾,只能在家里下田。小红下课后就回家做饭做菜给他们吃,自己暑假出去打工赚学费。学校老师说她的成绩非常好,是年级前三的学生,模拟成绩很可能考上211学校。

该案例已经过蓝天志愿组织实地考察,经多名志愿者核实,情况属实。希望大家能给予帮助,奉献大爱。

1. 微信小程序 uploadFile的坑是接收的是JSON字符串,不会帮你自动转JS对象。所以需要自己解析data

  1. for循环里有异步请求,想要for里面的异步请求都执行完再执行其他的怎么做?

    参考链接:https://blog.csdn.net/canot/article/details/73505891

    异步请求:

    //   promise
      GetImage:function(i,that){
        console.log("当前循环:"+i);
        return new Promise(function (resolve, reject) {
          wx.uploadFile({
            url: baseUrl + '/item/images',
            filePath: that.data.files[i],
            name: 'img',
            success: (res) => {
              // console.log("这是第"+i+"次循环")
              console.log(that.data);
    
              //先拿到旧的
              var server_detail_files = that.data.server_detail_files;
              console.log("server_detail_files" + server_detail_files);
    
              //服务端返回的
              let data = JSON.parse(res.data);
              let files_detail_path = data.extend.files_detail_path;
              console.log("files_detail_path:" + files_detail_path)
    
              //如果是拼的第一个,加入数组
              console.log("server_detail_files:" + server_detail_files)
              //push是在原数组上操作,并返回新的长度。
              server_detail_files.push(files_detail_path);
              //获取返回值
              that.setData({
                server_detail_files: server_detail_files
              })
              resolve(server_detail_files);
            }
          })
        })
      }

    for循环里异步,并且通过判断i==要执行下一步的值去执行add请求

          //循环发送多个详情的图片
          for (i = 1; i < files.length; i++) {
            // 采用闭包,保证索引值正确
            (function (i) {
                //调用promise处理异步
              that.GetImage(i,that).then(()=>{
                //最后一张处理完成
                console.log("i: "+i);
                  //在then里判断是否是最后一张图片,从而达到完成所有的for循环后再执行这个提交插入的请求
                if (that.data.server_detail_files.length == (that.data.files.length-1))              {
                  // 提交插入请求
                  wx.request({
                    url: baseUrl + '/item/add',
                    method: 'POST',
                    header:{
                      "content-type":"application/x-www-form-urlencoded"
                    },
                    data: {
                      targetPerson: name,
                      itemDescription: description,
                      raiseTarget: money,
                      itemType: that.data.itemTypes[that.data.Index].itemTypeId,
                      createTime: date,
                      description: detail_description,
    
                      picIndexUrl: that.data.server_file_index,
                      picDetailUrls: that.data.server_detail_files.join(',')
                    },
                    success: function (res) {
                      if (res.code == 200) {
                        // 清空
                        that.setData({
                          targetPerson: "",
                          itemDescription: "",
                          raiseTarget: "",
                          Index: 0,
                          date: "",
                          detail_description: "",
                          server_file_index: "",
                          server_detail_files: "",
                          files: ""
                        })
                        wx.navigateTo({
                          url: 'pages/index/index',
                        })
                        return;
                      }
    
                    }
                  })
                }
              });
            })(i)
          }
  2. 如果微信小程序使用post请求,后端没数据的话,说明小程序没有设置header为

        header: {
          'content-type': 'application/json'
        },

2. 需求分析

3. 数据库设计

PowerDesigner的使用

安装: https://blog.csdn.net/sinat_34104446/article/details/79885141

3.1 筹款项目表

sys_admin:

idusernamepasswordsalt
1zhanp@gd5@a6#ca1f5b@30@3a@2bcc#5F0b0f40@f@5a6@1!a4a5b6b0F1#b1!0a1cfa2d4171b48-fca9-45b1-9bb7-716ea057aa25

raise_item:

item_idtarget_personraise_targetcurrent_targetraise_person_numpic_index_urlitem_descriptionitem_type_iditem_process_type_id
1小江5000100060http://localhost/image/po1.jpgxxxx加油,战胜病魔11
2小洋6000200070http://localhost/image/po2.jpgxxx加油,努力读书22
3.2 筹集项目进度类型表

item_process_type

item_process_type_iditem_process_type_name
1未完成
2即将完成
3已完成
4已结束

(已结束是时间已过,该项目取消筹款了)

3.3 筹集项目类型表

raise_item_type

item_type_iditem_type_name
1孤寡老人
2贫困学生
3留守儿童
4患病在身
5其他
3.4 筹集项目详情表

raise_item_detail

item_detail_iditem_iddescriptionpic_detail_urlscreate_time
3.5 筹集项目进展表

raise_item_process (一个项目可以有多次进展)

item_process_iditem_idpic_process_urlsdescription
11
21
3.6 筹集项目捐助人表

raise_item_person

item_person_iditem_iduser_avatar_urluser_nick_nameraise_moneycommentraise_timeopen_id

后续还有排行榜

4. 后台设计

4.1 用户管理
4.2 角色管理
4.3 权限管理

5. 接口编写

项目编写流程

1. Mysql数据库的准备

1364699-20190531172828679-1553788130.png

2. SSM环境搭建

单元测试模拟数据的过程中遇到的bug

<!--在spring单元测试中,由于引入validator而导致的Tomcat7及以下的EL表达式版本不一致-->
<dependency>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>tomcat-el-api</artifactId>
    <version>8.5.24</version>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>tomcat-jasper-el</artifactId>
    <version>8.5.24</version>
    <scope>provided</scope>
</dependency>

报错:

1364699-20190531172838904-1414571738.png

解决方法:因为@ResponseBody,但是底层的jackson忘记引入了

<!--jackson支持-->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.9.8</version>
</dependency>
3. 日志
    <context-param>
        <param-name>log4jConfigLocation</param-name>
        <param-value>classpath*:log/log4j.properties</param-value>
    </context-param>
    <listener>
        <description>log4j</description>
        <listener-class>org.springframework.web.util.Log4jConfigListener</listener-class>
    </listener>
3.小程序的错误提示

一定要仔细看报错的部分,会显示哪一行报错,不要自己瞎找。不然改一天你都不知道哪里错。

1364699-20190531172851299-633961606.png

指示add.js 221行错了

测试:

1364699-20190531172859610-1072638000.png

部署问题

为什么服务器端的mysql一直连不上去?

因为root只允许localhost访问,所以要修改。

1364699-20190531172908560-169389572.png

别忘了flush一下

1364699-20190531172918299-912116071.png

成功:

1364699-20190531172927324-1335226785.png

linux下mysql忘记密码怎么办

前言

今天在服务器安装mysql之后,登录发现密码错误,但是我没有设置密码呀,最后百度之后得知,mysql在5.7版本之后会自动创建一个初始密码。
报错如下:

[root@mytestlnx02 ~]# mysql -u root -p
Enter password: 
ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: YES)

修改密码

1. 检查mysql服务是否启动,如果启动,关闭mysql服务

//查看mysql服务状态
[root@mytestlnx02 ~]# ps -ef | grep -i mysql
root     22972     1  0 14:18 pts/0    00:00:00 /bin/sh /usr/bin/mysqld_safe --datadir=/var/lib/mysql --socket=/var/lib/mysql/mysql.sock --pid-file=/var/run/mysqld/mysqld.pid --basedir=/usr --user=mysql
mysql    23166 22972  0 14:18 pts/0    00:00:00 /usr/sbin/mysqld --basedir=/usr --datadir=/var/lib/mysql --plugin-dir=/usr/lib/mysql/plugin --user=mysql --log-error=/var/log/mysqld.log --pid-file=/var/run/mysqld/mysqld.pid --socket=/var/lib/mysql/mysql.sock
root     23237 21825  0 14:22 pts/0    00:00:00 grep -i mysql

//关闭服务
[root@mytestlnx02 ~]# service mysql stop
[root@mytestlnx02 ~]#

2. 修改mysql的配置文件my.cnf

my.cnf`配置文件的位置,一般在`/etc/my.cnf`,有些版本在`/etc/mysql/my.cnf

在配置文件中,增加2行代码

[mysqld]

skip-grant-tables

作用是登录mysql的时候跳过密码验证

然后启动mysql服务,并进入mysql

[root@mytestlnx02 ~]# service mysqld start
[root@mytestlnx02 ~]#
[root@mytestlnx02 ~]# mysql -u root 
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
 
mysql>

3. 修改密码

连接mysql这个数据库,修改用户密码

mysql> use mysql;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
 
Database changed
mysql> update mysql.user set authentication_string=password('root_password') where user='root';
Query OK, 1 row affected, 1 warning (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 1
 
mysql> flush privileges;
Query OK, 0 rows affected (0.00 sec)
 
mysql> exit

4. 重启mysql服务

先将之前加在配置文件里面的2句代码注释或删除掉,然后重启mysql服务,就可以使用刚刚设置的密码登录了。

[root@mytestlnx02 ~]# service mysql start
[root@mytestlnx02 ~]#
[root@mytestlnx02 ~]# mysql -u root -p
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.

p.s.

CentOS上的操作方式有所不同。

执行修改密码的命令一直报错

mysql> update user set authentication_string=password('xxxxxxxx') where User='root';       
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '('root_password') where User='root'' at line 1

不可能是语法问题,检查了很多遍,最后发现CentOS下应该这样操作:

查看初始密码

[root@VM_0_8_centos ~]# grep 'temporary password' /var/log/mysqld.log
2018-09-26T04:25:54.927944Z 5 [Note] [MY-010454] [Server] A temporary password is generated for root@localhost: DN34N/=?aIfZ

可以看到初始密码为DN34N/=?aIfZ

使用初始密码登录

[root@VM_0_8_centos ~]# mysql -u root -p
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 8
Server version: 8.0.12 MySQL Community Server - GPL

Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved.

修改密码

mysql> ALTER USER 'root' IDENTIFIED BY 'xxxxxxxxx';  
ERROR 1820 (HY000): You must reset your password using ALTER USER statement before executing this statement.
mysql> ALTER USER 'root'@'localhost' IDENTIFIED BY 'xxxxxxxx';
Query OK, 0 rows affected (0.11 sec)

mysql> flush privileges;
Query OK, 0 rows affected (0.01 sec)

mysql> exit
Bye

重启服务就生效了

[root@VM_0_8_centos ~]# service mysqld stop 
Redirecting to /bin/systemctl stop  mysqld.service
[root@VM_0_8_centos ~]# service mysqld start
Redirecting to /bin/systemctl start  mysqld.service

1364699-20190531172951435-2135807498.png

部署到服务器上的Mysql连接参数

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://xxxx/sky_chou?useUnicode=true&characterEncoding=UTF-8
jdbc.username=root
jdbc.password=xxxxx

记住:一定不要添加useSSL=true这种配置信息,不然会SQL报错

改成localhost,不然不会识别服务器的ip。!

升级到HTTPS

通过nginx升级到HTTPS

  1. 要去购买的云服务器上下载SSL证书

1364699-20190531173003109-1596785475.png

  1. 把nginx的SSL证书复制到linux服务器上的nginx的conf目录下

1364699-20190531173010774-808493364.png

  1. 修改nginx.conf文件

    #HTTPS server
    
    server {
        listen       443;
        server_name  你的域名;
    
        ssl                  on;
        ssl_certificate      xxxx_bundle.crt;
        ssl_certificate_key  xxx.key;
    
        ssl_session_timeout  5m;
    
        ssl_protocols  TLSv1 TLSv1.1 TLSv1.2;
        ssl_ciphers  ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;
        ssl_prefer_server_ciphers   on;
    
        location / {
              client_max_body_size    16m;
              client_body_buffer_size 128k;
              proxy_pass                          http://127.0.0.1:9999/;
              proxy_set_header        Host $host;
              proxy_set_header        X-Real-IP $remote_addr;
              proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;
          proxy_set_header           X-Forwarded-Proto https;
              proxy_next_upstream   off;
    
              proxy_connect_timeout   30;
              proxy_read_timeout      300;
              proxy_send_timeout      300;
        }
    } 

    要着重修改的SSL相关地方:

        ssl                  on;
        ssl_certificate     xxxxx.crt;
        ssl_certificate_key  xxxx.key;

    这些是网上的固定配置

              proxy_set_header        Host $host;
              proxy_set_header        X-Real-IP $remote_addr;
              proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;
          proxy_set_header           X-Forwarded-Proto https;
              proxy_next_upstream   off;
    
              proxy_connect_timeout   30;
              proxy_read_timeout      300;
              proxy_send_timeout      300;

    nginx代理443端口的配置部分

    server {
        listen       443;
        server_name  你的域名;
    
            location / {
              client_max_body_size    16m;
              client_body_buffer_size 128k;
              proxy_pass                          http://127.0.0.1:9999/;

    记住所有的server模块的配置都应该包含在http块里面,不然会报错的!

    成功标志:

1364699-20190531173116329-1216488396.png

linux上运行多个Tomcat

  1. 修改环境变量:一般的都是/etc/profile
  2. 加入以下代码(tomcat路径要配置自己实际的tomcat安装目录)

1364699-20190531173125305-1211891422.png

4.保存退出。
5.再输入:source /etc/profilecond tomcat在生效

6.第一个tomcat,保持解压后的原状不用修改,

来到第二个tomcat的bin目录下打开catalina.sh ,找到下面红字,

# OS specific support. $var must be set to either true or false.

在下面增加如下代码

export CATALINA_BASE=$CATALINA_2_BASE
export CATALINA_HOME=$CATALINA_2_HOME

1364699-20190531173135395-2013989069.png

7.来到第二个tomcat的conf目录下
打开server.xml更改端口:

修改server.xml配置和第一个不同的启动、关闭监听端口。
修改后示例如下:
    端口:8005->9005

​ <Connector port="9080" maxHttpHeaderSize="8192"  端口:8080->9080
maxThreads="150" minSpareThreads="25" maxSpareThreads="75"
​ enableLookups="false" redirectPort="8443" acceptCount="100"
​ connectionTimeout="20000" disableUploadTimeout="true" />

​ <Connector port="9009" 端口:8009->9009
​ enableLookups="false" redirectPort="8443" protocol="AJP/1.3" />

8.分别进入两个tomcat的bin目录,启动tomcat--./startup.sh

9.然后访问http://localhost:8080http://localhost:9080 都可以看到熟悉的tomcat欢迎界面

1364699-20190531173149504-1212227549.png

转载于:https://www.cnblogs.com/zhanp/p/10956543.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值