小程序稻草人圖牀神器,前後端開源

稻草人圖牀神器

源碼地址:

小程序源碼 https://github.com/w77996/mini-straw  
後臺源碼 https://github.com/w77996/hi-straw  

體驗一下

image

之前乘着換工作的間隙擼的,一方面練習一下小程序,學過之後沒怎麼應用,一方面寫點筆記啥的,項目斷斷續續做了兩個月,只是一個簡單的圖片上傳工具,覺得不錯的話記得給個star

小程序

  1. 小程序授權,登錄
  2. 父組件與子組件相互通信
  3. 小程序分享,意見與建議,客服功能,文件上傳
  4. flex佈局的使用
  5. Promise的使用,業務model封裝
  6. 插槽的使用,動畫效果

項目目錄結構

mini-straw
    ├── component -- 組件
    |    ├── file -- 文件組件
    |    ├── image-button -- 圖片按鈕組件
    |    ├── search -- 查找頁面組件
    |    ├── tag -- 標籤組件
    ├── images -- 圖片目錄
    |    ├── icon -- icon圖片
    |    ├── tab -- tab圖片
    ├── model -- 封裝的model
    ├── pages -- 頁面
    |    ├── about -- 關於頁
    |    ├── auth -- 授權頁
    |    ├── file -- 文件頁
    |    ├── index -- 首頁
    |    ├── launch -- 啓動頁面
    |    ├── my -- 個人中心
    └── utils -- 工具

小程序文檔

後臺:

技術棧:spring boot + druid + mybatis + jwt

  1. 微信登錄,jwt授權
  2. 註解及AOP的使用
  3. maven多環境打包,docker使用
  4. shell腳本自動化部署
  5. nginx反向代理及https配置
  6. 七牛雲文件操作

項目目錄結構

hi-straw
├── common -- 公共模塊
├── config -- 配置模塊
├── controller -- controller接口
├── core -- 核心業務模塊
|    ├── annontaion -- 註解
|    ├── aop -- aop實現
|    ├── constant -- 常量
|    ├── filter -- 攔截器
|    ├── jwt -- jwt相關
|    └── result -- 結果返回
├── entity -- 實體類
|    ├── dto -- 數據傳輸
|    └── vo -- 頁面傳輸
├── exception -- 全局異常
├── mapper -- dao層
├── service -- service層
└── util -- 工具類

後臺文檔

小程序詳解


mini-straw

項目結構

mini-straw
    ├── component -- 組件
    |    ├── file -- 文件組件
    |    ├── image-button -- 圖片按鈕組件
    |    ├── search -- 查找頁面組件
    |    ├── tag -- 標籤組件
    ├── images -- 圖片目錄
    |    ├── icon -- icon圖片
    |    ├── tab -- tab圖片
    ├── model -- 封裝的model
    ├── pages -- 頁面
    |    ├── about -- 關於頁
    |    ├── auth -- 授權頁
    |    ├── file -- 文件頁
    |    ├── index -- 首頁
    |    ├── launch -- 啓動頁面
    |    ├── my -- 個人中心
    └── utils -- 工具

開屏頁

1.判斷網絡狀態

使用wx.getNetworkType({})可獲取當前網絡狀態,networkTypewifi/2g/3g/4g/unknown(Android下不常見的網絡類型)/none(無網絡)

 //判斷網絡狀態
wx.getNetworkType({
    success: res => {
    if (res.networkType == "none") {
        wx.showToast({
            title: '嗷~~網絡不可用',
            icon: 'none',
            duration: 2000
        })
        return;
    }
    },
})

2.判斷授權狀態

使用wx.getSetting({})獲取授權狀態,在獲得data後取data.authSetting['scope.userInfo']判斷授權狀態

 // 獲取授權的狀態
wx.getSetting({
    success: data => {
    if (data.authSetting['scope.userInfo']) {
        //已授權,執行登陸
        wx.getUserInfo({
            success: data => {
                    console.log("userInfo {}", data)
                    let userInfo = data.userInfo;
                    wx.setStorageSync('userInfo', userInfo);
                    //執行登陸操作
                    this._userLoginGetCode(userInfo);
            }
        });
        wx.setStorageSync('authorized', true);
    } else {
        console.log("未授權")
        //跳轉至授權頁
        let timer = setTimeout(() => {
                wx.redirectTo({
                        url: '/pages/auth/auth'
                })
        }, 2000)

    }
    }
});

若授權,則調用wx.getUserInfo({})獲取微信用戶信息,信息獲取完成後調用wx.login({})獲取小程序的code,通過code向後臺獲取用戶openId及token。

//後臺獲取code
_userLoginGetCode(userInfo) {
    console.log("發起_userLoginGetCode請求");
    wx.login({
        success(res) {
        console.log("wx.login {}", res);
        if (res.code) {
            // 發起網絡請求
            const code = res.code;
            userInfo.code = code;
            userModel.getTokenByCode(userInfo).then((res) => {
                console.log("userModel getUserInfo {}", res);
                wx.setStorageSync("token", res.data.data.token);
                let timer = setTimeout(() =>
                    wx.switchTab({
                            url: '/pages/index/index',
                    }), 2000)
            });
        } else {
                console.log('登錄失敗!' + res.errMsg)
        }
        }
    })
},

3.跳轉頁面

  • 跳轉/pages/auth/auth頁面使用的是wx.redirectTo({})
  • 跳轉/pages/index/index頁面使用的是wx.switchTab({})
    因爲/pages/index/index是小程序tab頁,使用wx.redirectTo({})無法跳轉

授權頁

授權需制定button按鈕,加入open-type='getUserInfo'屬性,bindgetuserinfo調用自定義方法onGetUserInfo

 <button class="auth-button" open-type='getUserInfo' bindgetuserinfo="onGetUserInfo">好的</button>

onGetUserInfo接受授權狀態及授權獲取的用戶信息,再進行code獲取,通過code向後臺獲取用戶openId及token。

onGetUserInfo: function(e) {
    console.log(e)
    const userInfo = e.detail.userInfo;
    if (userInfo) {
            //通過`code`向後臺獲取用戶openId及token。
            this._userLoginGetCode(userInfo);
    }
},

主頁

1. 圖片按鈕插槽組件

component目錄下的images-button組件,做了簡單的圖片插槽統一,在分享按鈕,用戶登錄按鈕,文件上傳按鈕均可以使用。plain="{{true}}"代表button背景透明

<button  open-type="{{openType}}" plain="{{true}}" class="container">
  <slot name="img"></slot>
</button>

options需要開啓插槽功能,添加multipleSlots: true
open-type="{{openType}}"父組件將參數傳入子組件,子組件在properties屬性中可以獲取到父組件傳來的openType數據,通過this.properties.openType可以獲取屬性值

options: {
    // 開啓插槽
    multipleSlots: true
  },
/**
   * 組件的屬性列表
   */
  properties: {
    openType: {
      type: String
    }
  },

index頁面引入組件,需要在index.json中添加組件路徑

{
  "usingComponents": {
    "btn-cmp": "/component/image-button/index"
  }
}

2. 上傳文件

主要使用到wx.chooseImage({})進行圖片的選擇,選擇後使用wx.uploadFile({})上傳圖片至服務器

//上傳文件
onUpload(event) {
let _this = this;
wx.chooseImage({
  count: 1, // 默認9
  sizeType: ['original', 'compressed'], // 可以指定是原圖還是壓縮圖,默認二者都有
  sourceType: ['album', 'camera'], // 可以指定來源是相冊還是相機,默認二者都有
  success: function(res) {
    // 返回選定照片的本地文件路徑列表,tempFilePath可以作爲img標籤的src屬性顯示圖片
    let tempFilePaths = res.tempFilePaths;
    console.log(tempFilePaths)
    wx.uploadFile({
      header: {
        "Authorization": "Bearer " + wx.getStorageSync("token")
      },
      url: config.apiBaseUrl + '/file/upload',
      filePath: tempFilePaths[0],
      name: 'file',
      success: (res) => {
        wx.showToast({
          title: "上傳成功~",
          icon: 'none',
          duration: 2000
        })
      },
      fail: (res) => {
        wx.showToast({
          title: res.data.msg,
          icon: 'none',
          duration: 2000
        })
      }
    })

  }
})
}

列表頁

1.search組件的顯示和隱藏

固定列表頁搜索header位置,點擊header顯示search組件,在search組件點擊取消則隱藏search組件,此處設計子組件向父組件傳遞消息

  • 引入search組件
 "usingComponents": {
    ...
    "search-cmp": "/component/search/index"
  }
  • 使用searchPage參數判斷search組件的,默認爲false,在點擊header時更新searchPage爲true,顯示search組件
<view wx:if="{{!searchPage}}" class="container">
  ...
</view>

<search-cmp  wx:if="{{searchPage}}" ></search-cmp>
  • search頁面點擊取消,向父組件發送一個this.triggerEvent('cancel', {}, {});事件,在xml中的search-cmp添加cancel事件的通知綁定
#file頁面中的search-cmp組件
<search-cmp  wx:if="{{searchPage}}" bind:cancel="onCancel"></search-cmp>

父組件file頁面綁定子組件傳來的cancel事件通知,就調用onCancel方法,
onCancel方法中獲取事件響應,將searchPage參數修改爲false,search組件就隱藏起來了

//cancel searching page 
onCancel(event) {
  console.info(event)
  this.triggerEvent('cancel', {}, {});
 
},

2.文件列表

1.獲取列表信息傳遞給file組件

page中的file頁面,獲取到後臺傳來的fileList數據,引入file組件,file="{{item}}"將數據傳入子組件

<view wx:if="{{fileList}}">
  <block wx:for="{{fileList}}" wx:key="{{item.id}}" file="{{item}}">
    <file-cmp file="{{item}}" bind:del="onDelete"></file-cmp>
    <view class="line"></view>
  </block>
</view>

component中的file組件,在properties添加屬性file來接收父組件傳來的數據

  /**
   * 組件的屬性列表
   */
  properties: {
    file: Object
  }

file組件在xml頁面中使用{{file.fileName}}即可獲取到對象信息,相應的數據也會呈現在頁面上

3.粘貼板操作

<image src="images/copy.png" bindtap="onCopy"></image>

圖片點擊響應方法onCopyonCopy調用wx.setClipboardData({})可以將數據複製到粘貼板

    
    onCopy: function (event) {
      console.info(event)
      let _this = this;
      wx.setClipboardData({
        data: _this.properties.file.filePath,
        success: function(res) {
          wx.showToast({
            title: '圖片地址複製成功',
          })
        }
      });

4.刪除操作

<image src="images/del.png" bindtap="onDelete"></image>

子組件將數據傳遞給父組件,點擊刪除圖片出發onDelete方法,通過this.triggerEvent('del', {fileId}, {});將文件ID發送到父組件

    onDelete: function (event) {
      console.info(event)
      let fileId = this.properties.file.id;
      this.triggerEvent('del', {fileId}, {});
    },

父組件file頁面綁定子組件傳來的del事件

<file-cmp file="{{item}}" bind:del="onDelete"></file-cmp>

調用onDelete出發網絡請求去完成刪除文件的邏輯,刪除成功後重新刷新文件列表

 //刪除圖片
onDelete(event) {
    console.info("DEL")
    console.info(event)
    let fileId = event.detail.fileId;
    fileModel.delFileById(fileId).then((res) => {
        console.info(res);
        wx.showToast({
                title: '刪除成功',
        })
        this.setData({
                pageNum: 1,
                fileList: []
        });
        this._getFileList();
    })
},

我的頁面

1.意見和建議

小程序自帶用戶反饋功能,使用button跳轉至網頁,用戶可以填寫相關反饋,open-type設置爲feedback

<button class="about-btn" plain="true" open-type="feedback">
    <text class="about-btn-text">反饋建議</text>
</button>

2.小程序客服

小程序的button中的open-type擁有開放能力,在微信公衆平臺中啓用客服功能,添加客服人員,在代碼中添加button即可彈出客服聊天界面,open-type設置爲contact

 <button class="about-btn" plain="true" open-type="contact" bindcontact="handleContact">
    <text class="about-btn-text">聯繫客服</text>
</button>

3.小程序分享

此處使用插槽,button中的open-type設置爲share

<btn-cmp open-type="share">
    <image slot="img" src="images/share.png" />
</btn-cmp>

動畫

小程序動畫官方文檔
開屏動畫,設置文字透明度,從0到1,漸漸顯示,主要使用到opacity去設置組件的透明度,先創建一個動畫animationTip,持續800ms,然後在setTimeout(function () {})中設置動畫出現時間

 var animationTip = wx.createAnimation({
      //持續時間800ms
      duration: 800,
      timingFunction: 'ease',
    });
    this.animationTip = animationTip;
    animationTip.opacity(0).step()
    this.setData({
      animationTip: animationTip.export()
    })
    setTimeout(function () {
      animationTip.opacity(1).step()
      this.setData({
        animationTip: animationTip.export()
      })
    }.bind(this), 500)

部署

  1. 修改utils目錄下的config.apiBaseUrl,改成自己的域名,上傳到微信公衆號平臺,在版本管理中進行發佈
const config ={
   apiBaseUrl: "你自己的域名或服務器地址"
}

後臺功能詳解

hi-straw

項目結構

hi-straw
├── common -- 公共模塊
├── config -- 配置模塊
├── controller -- controller接口
├── core -- 核心業務模塊
|    ├── annontaion -- 註解
|    ├── aop -- aop實現
|    ├── constant -- 常量
|    ├── filter -- 攔截器
|    ├── jwt -- jwt相關
|    └── result -- 結果返回
├── entity -- 實體類
|    ├── dto -- 數據傳輸
|    └── vo -- 頁面傳輸
├── exception -- 全局異常
├── mapper -- dao層
├── service -- service層
└── util -- 工具類

數據庫設計

CREATE TABLE `t_straw_file` (
  `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
  `user_id` int(11) UNSIGNED DEFAULT NULL COMMENT '用戶ID',
  `file_path` varchar(255) DEFAULT NULL COMMENT '文件路徑',
  `file_name` varchar(100) DEFAULT NULL COMMENT '文件名',
  `file_size` varchar(10) DEFAULT NULL COMMENT '文件大小',
  `props` varchar(255) DEFAULT NULL,
  `status` tinyint(5) UNSIGNED DEFAULT '0' COMMENT '0.正常 -1.刪除',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT'創建時間',
  PRIMARY KEY (`id`),
    KEY `user_id` (`user_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=27 DEFAULT CHARSET=utf8mb4 COMMENT '用戶文件列表';
CREATE TABLE `t_straw_user` (
  `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
  `user_name` varchar(20) DEFAULT NULL COMMENT '用戶名',
  `nickname` varchar(20) DEFAULT NULL COMMENT '用戶暱稱',
  `user_logo` varchar(250) DEFAULT NULL COMMENT '用戶logo',
  `phone_num` varchar(20) DEFAULT NULL COMMENT '手機號',
  `open_id` varchar(55) DEFAULT NULL COMMENT '微信openId',
  `union_id` varchar(20) DEFAULT NULL COMMENT '微信union_id',
  `password` varchar(50) DEFAULT NULL COMMENT '密碼',
  `uuid` varchar(20) DEFAULT NULL COMMENT '自定義生成的uuid',
  `last_login` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '最後登陸時間',
	`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間',
  PRIMARY KEY (`id`),
	UNIQUE KEY `open_id` (`open_id`) USING HASH
) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4 COMMENT '用戶表';
CREATE TABLE `t_straw_user_file_info` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) UNSIGNED NOT NULL COMMENT '用戶ID',
  `file_size` int(11) UNSIGNED DEFAULT '0' COMMENT '用戶文件大小',
  `left_size` int(11) UNSIGNED DEFAULT '5242880' COMMENT '剩餘文件大小',
  `total_size` int(11)UNSIGNED DEFAULT '5242880' COMMENT '用戶文件空間大小',
  `file_num` int(11) UNSIGNED DEFAULT '0' COMMENT '文件數量',
  `is_vip` tinyint(5) UNSIGNED DEFAULT '0' COMMENT '是否爲vip,1.是 0.否',
  PRIMARY KEY (`id`),
  UNIQUE KEY `user_id` (`user_id`) USING HASH
) ENGINE=InnoDB AUTO_INCREMENT=43 DEFAULT CHARSET=utf8mb4 COMMENT'用戶文件信息';
CREATE TABLE `t_straw_user_info` (
  `user_id` int(11)  UNSIGNED NOT NULL COMMENT '用戶ID',
  `sex` tinyint(5) UNSIGNED DEFAULT NULL COMMENT '性別',
  `location` varchar(55) DEFAULT NULL COMMENT '位置',
  `platform` varchar(55) DEFAULT NULL COMMENT '平臺',
  `birthday` datetime DEFAULT NULL COMMENT '生日',
  PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT '用戶詳細表';

代碼詳解

前期準備

七牛雲文件上傳

代碼com.w77996.straw.util.QiNiuUtil

1.七牛雲賬號申請

七牛雲官網申請賬號,獲得AccessKey,SecretKey,並設置七牛雲圖片bucket

    /**
     * 七牛accessKey
     */
    @Value("${QiNiu.accessKey}")
    private String accessKey;
    /**
     * 七牛密鑰
     */
    @Value("${QiNiu.secretKey}")
    private String secretKey;
    /**
     * 七牛bucket
     */
    @Value("${QiNiu.bucket}")
    private String bucket;

2.七牛雲SDK引入

pom.xml文件引入七牛雲倉庫

    <dependency>
        <groupId>com.qiniu</groupId>
        <artifactId>qiniu-java-sdk</artifactId>
        <version>[7.2.0, 7.2.99]</version>
    </dependency>

2. 七牛雲token生成

    /**
     * 七牛雲生成token
     *
     * @param fileName
     * @return
     */
    public QiNiuAuth generateToken(String userId, String fileName) {
        Auth auth = Auth.create(accessKey, secretKey);
        String key = "upload/file/000/" + userId + "/" + fileName;
        StringMap putPolicy = new StringMap();
        putPolicy.put("returnBody", "{\"key\":\"$(key)\",\"hash\":\"$(etag)\",\"bucket\":\"$(bucket)\",\"fsize\":$(fsize)}");
        long expireSeconds = 3600;
        String upToken = auth.uploadToken(bucket, key, expireSeconds, putPolicy);
        Map<String, String> resultMap = Maps.newHashMapWithExpectedSize(3);
        resultMap.put("domain", "https://www.w77996.cn");
        resultMap.put("key", key);
        resultMap.put("upToken", upToken);
        return new QiNiuAuth("https://www.w77996.cn", key, upToken);
    }

3.上傳文件代碼編寫

    /**
     * 上傳圖片
     *
     * @param file
     * @param key
     * @param token
     * @return
     */
    public String uploadImage(MultipartFile file, String key, String token) {
        Configuration cfg = new Configuration(Zone.zone2());
        UploadManager uploadManager = new UploadManager(cfg);
        String filePath = null;
        //生成上傳憑證,不指定key的情況下,以文件內容的hash值作爲文件名
        Response response = null;
        try {
            byte[] uploadBytes = file.getBytes();
            Auth auth = Auth.create(accessKey, secretKey);
            String upToken = auth.uploadToken(bucket);
            try {
                response = uploadManager.put(uploadBytes, key, upToken);
                //解析上傳成功的結果
                DefaultPutRet putRet = new Gson().fromJson(response.bodyString(), DefaultPutRet.class);
                log.info("上傳結果 {} {}", putRet.hash, putRet.key);
                filePath = putRet.key;
            } catch (QiniuException ex) {
                try {
                    response = ex.response;
                    log.error(response.bodyString());
                } catch (QiniuException ex2) {
                    //ignore
                    ex.printStackTrace();
                }
            }
        } catch (Exception ex) {
            //ignore
            ex.printStackTrace();
        }
        return filePath;
    }

4.刪除圖片

    /**
     * 刪除圖片
     *
     * @param key
     */
    public void delete(String key) {
        Configuration cfg = new Configuration(Zone.zone2());
        Auth auth = Auth.create(accessKey, secretKey);
        //實例化一個BucketManager對象
        BucketManager bucketManager = new BucketManager(auth, cfg);
        try {
            //調用delete方法移動文件
            bucketManager.delete(bucket, key);
        } catch (QiniuException e) {
            //捕獲異常信息
            throw new GlobalException(ResultCode.ERROR);
        }
    }

註解+AOP接口限流

1. 註解編寫

代碼com.w77996.straw.core.annotation.Limiter

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Limiter {

    /**
     *
     * @return
     */
    String value() default "";

    /**
     * 每秒向桶中放入令牌的數量   默認最大即不做限流
     * @return
     */
    double perSecond() default Double.MAX_VALUE;

    /**
     * 獲取令牌的等待時間  默認0
     * @return
     */
    int timeOut() default 0;

    /**
     * 超時時間單位
     * @return
     */
    TimeUnit timeOutUnit() default TimeUnit.MILLISECONDS;
}

2.AOP實現

代碼com.w77996.straw.core.aop.RateLimitAspect

@Aspect
@Component
@Slf4j
public class RateLimitAspect {

    private RateLimiter rateLimiter = RateLimiter.create(Double.MAX_VALUE);

    /**
     * 定義切點
     * 1、通過掃包切入
     * 2、帶有指定註解切入
     */
    @Pointcut("@annotation(com.w77996.straw.core.annotation.Limiter)")
    public void checkPointcut() {
    }

    @ResponseBody
    @Around(value = "checkPointcut()")
    public Object aroundNotice(ProceedingJoinPoint pjp) throws Throwable {
        log.info("攔截到了{}方法...", pjp.getSignature().getName());
        Signature signature = pjp.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        //獲取目標方法
        Method targetMethod = methodSignature.getMethod();
        if (targetMethod.isAnnotationPresent(Limiter.class)) {
            //獲取目標方法的@Limiter註解
            Limiter limiter = targetMethod.getAnnotation(Limiter.class);
            rateLimiter.setRate(limiter.perSecond());
            if (!rateLimiter.tryAcquire(limiter.timeOut(), limiter.timeOutUnit())) {
                log.info("rateLimiter lock");
                return Result.error(ResultCode.BUSY);
            }
        }
        return pjp.proceed();
    }
}

3. 註解使用

限定每秒只能調用一次,如果超出,則返回Result.error(ResultCode.BUSY)

    @GetMapping("/limit")
    @Limiter(perSecond = 1.0, timeOut = 500)
    public String testLimiter() {
        return " success";
    }

JWT實現

1.jwt生成

使用JwtUtil生成jwt Token

     /**
     * 生成jwt
     *
     * @param userId
     * @return
     */
    public static String createJWT(String userId) {
        String token = JwtHelper.createJWT(userId, Constant.JWT_CLIENT_ID,
                Constant.JWT_NAME, Constant.JWT_EXPIRES_SECOND, Constant.JWT_BASE64_SECRET);
        return token;
    }

2.token解析成userId

將userId放入token中,在請求接口時可以通過請求Header獲取Bearer token{token},然後對{token}進行解碼,從而獲取userId。

    /**
     * 通過token獲取用戶信息
     *
     * @return
     */
    public String getUserIdByToken() {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String accessToken = request.getHeader("Authorization");
        if (StringUtils.isEmpty(accessToken) || accessToken.length() < 20) {
            throw new GlobalException(ResultCode.ERROR_TOKEN_NULL);
        }
        accessToken = accessToken.substring(7);
        if ("admin".equals(accessToken)) {
            return "1";
        }
        Claims claims = JwtHelper.parseJWT(accessToken, Constant.JWT_BASE64_SECRET);
        return claims.getSubject();
    }

3.攔截器+註解方式進行token鑑權

代碼 com.w77996.straw.core.annotation.IgnoreToken
先設置忽略token的註解

/**
 * @description: 忽略token
 * @author: w77996
 **/
@Retention(RetentionPolicy.RUNTIME)
@Target(value={ElementType.METHOD,ElementType.TYPE})
public @interface IgnoreToken {
}

代碼 com.w77996.straw.core.filter.TokenFilter
攔截器TokenFilter實現HandlerInterceptor,在每次請求進來時進行攔截,在調用controller之前都會調用perHandle,所以在perHandler內獲取方法名的註解,判斷是否有ignoreToken的註解,然後進行jwt的校驗。

 @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        IgnoreToken ignoreToken = handlerMethod.getBeanType().getAnnotation(IgnoreToken.class);
        log.info("enter preHandle {}",request.getRequestURL());
        if (ignoreToken == null) {
            ignoreToken = handlerMethod.getMethodAnnotation(IgnoreToken.class);
        }
        if (ignoreToken != null) {
            log.info("ignoreToken not null");
            return true;
        }
        log.info("ignoreToken  null");
        String token = request.getHeader("Authorization");
        if(token != null){
            log.info("token is {}",token);
            if ("admin".equals(token.substring(7))) {
                return true;
            }
            Claims claims = JwtHelper.parseJWT(token.substring(7), Audience.BASE64SECRET);
            if(claims != null){
                log.info("claims is {} {}",claims.toString(),claims.getSubject());
                return true;
            }else{
                log.info("claims is null");
                throw new GlobalException(ResultCode.ERROR_AUTH);
            }
        }
        return false;
    }

4.實現web攔截器

代碼com.w77996.straw.config.WebMvcAdapterConfig
不攔截/druid/*的接口

/**
 * @description: web攔截器
 * @author: w77996
 **/
@Component
public class WebMvcAdapterConfig extends WebMvcConfigurationSupport {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new TokenFilter()).excludePathPatterns("/druid/*");
    }
}

Druid監控配置

代碼com.w77996.straw.config.DruidConfig
項目運行後訪問http://ip:port/druid,輸入賬號admin密碼amdin即可訪問

@Configuration
public class DruidConfig {

    @Bean
    @ConfigurationProperties("spring.datasource")
    public DataSource druidDataSource() {
        DruidDataSource dataSource = new DruidDataSource();
        return dataSource;
    }

    @Bean
    public ServletRegistrationBean druidStatViewServlet() {
        ServletRegistrationBean registrationBean = new ServletRegistrationBean(new StatViewServlet(), "/druid/*");
        // IP白名單 (沒有配置或者爲空,則允許所有訪問)e
        registrationBean.addInitParameter("allow", "");
        // IP黑名單 (存在共同時,deny優先於allow)
        registrationBean.addInitParameter("deny", "");
        registrationBean.addInitParameter("loginUsername", "admin");
        registrationBean.addInitParameter("loginPassword", "admin");
        registrationBean.addInitParameter("resetEnable", "false");
        return registrationBean;
    }

    @Bean
    public FilterRegistrationBean druidWebStatViewFilter() {
        FilterRegistrationBean registrationBean = new FilterRegistrationBean(new WebStatFilter());
        registrationBean.addInitParameter("urlPatterns", "/*");
        registrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*");
        return registrationBean;
    }
}

全局異常攔截

全局異常攔截主要是依靠@RestControllerAdvice註解,在方法上使用@ExceptionHandler(value = Exception.class)代表攔截所有Exception,然後進行對應的操作

@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 全局錯誤攔截
     *
     * @param e
     * @return
     */
    @ExceptionHandler(value = Exception.class)
    private Result<Object> exceptionHandler(Exception e) {
        if (e instanceof GlobalException) {
            GlobalException ex = (GlobalException) e;
            return Result.error(ex.getCode());
        }
        return Result.error(ResultCode.ERROR.getCode(),e.getMessage());
    }
}

微信登錄

需要在微信公共平臺獲取對應的appId,appSec,小程序獲取到code之後發送給後臺,後臺獲取code向微信發送http請求,使用的是restTemplate,但是需要注意編碼,微信編碼返回是ISO-8859-1;調用成功後可以拿到用戶的openId,再去數據庫中獲取對應的用戶信息,進行登陸更新及用戶創建的邏輯處理

@RestController
@RequestMapping("/wx")
@Slf4j
public class WxController {

    @Autowired
    private IUserService iUserService;

    @Value("${wx.appId}")
    private String wxAppId;

    @Value("${wx.appSec}")
    private String wxAppSec;

    /**
     * 通過code獲取openId
     *
     * @param wxLoginDto
     * @return
     */
    @IgnoreToken
    @PostMapping("/code")
    public Result getUserInfoByCode(@RequestBody WxLoginDto wxLoginDto) {
        log.info("enter getUserInfoByCode");
        //微信授權獲取openId
        String reqUrl = "https://api.weixin.qq.com/sns/jscode2session?appid=" + wxAppId + "&secret=" + wxAppSec + "&js_code=" + wxLoginDto.getCode() + "&grant_type=authorization_code";
        JSONObject wxAuthObject = RestHttpClient.client(reqUrl, HttpMethod.GET, null);
        log.info("wxAuthObject {}", wxAuthObject.toJSONString());
        WxTokenDto wxTokenDto = JSONObject.parseObject(wxAuthObject.toJSONString(), WxTokenDto.class);
        log.info("wxTokenDto {}", wxTokenDto.toString());
        Map<String, Object> tokenMapper = Maps.newHashMapWithExpectedSize(2);
        //生成新用戶
        UserEntity userEntity = iUserService.getUserByOpenId(wxTokenDto.getOpenid());
        if (!ObjectUtils.allNotNull(userEntity)) {
            WxUserInfoDto wxUserInfoDto = new WxUserInfoDto();
            wxUserInfoDto.setNickname(wxLoginDto.getNickName());
            wxUserInfoDto.setUserLogo(wxLoginDto.getUserLogo());
            wxUserInfoDto.setSex(wxLoginDto.getSex());
            wxUserInfoDto.setLastLogin(new Date());
            wxUserInfoDto.setOpenId(wxTokenDto.getOpenid());
            wxUserInfoDto.setLocation(StringUtils.join(new String[]{wxLoginDto.getCountry(), wxLoginDto.getProvince(), wxLoginDto.getCity()}, "-"));
            iUserService.createNewUser(wxUserInfoDto);
            log.info("create new user {}", wxUserInfoDto);
        }
        tokenMapper.put("token", JwtHelper.createJWT(userEntity.getId() + ""));
        return Result.success(tokenMapper);
    }
}

spring boot + maven多環境打包

1.resouce下的yml文件

項目環境分爲devprod兩種,resource文件下默認加載application.yml

  • dev環境:application-dev.yml
  • prod環境:application-prod.yml
    application.yml
spring:
    profiles:
      active: @spring.profiles.active@

@spring.profiles.active@對應的爲pom.xml文件中profiles下的spring.profiles.active屬性

2.pom.xml配置

默認情況下使用dev環境下的配置信息

    <profiles>
        <profile>
            <id>dev</id>
            <activation>
                <activeByDefault>true</activeByDefault>
            </activation>
            <properties>
                <!-- default Spring profiles -->
                <spring.profiles.active>dev</spring.profiles.active>
            </properties>
        </profile>
        <profile>
            <id>prod</id>
            <properties>
                <!-- default Spring profiles -->
                <spring.profiles.active>prod</spring.profiles.active>
            </properties>
        </profile>
    </profiles>

3.不同環境打包

  • 打包prod環境:執行mvn package -Pprod -DskipTests
  • 打包dev環境:執行mvn package -Pdev -DskipTests

4.項目打包命名

properties屬性中添加時間格式,然後再build中添加fileName格式化文件名。

    <artifactId>hi-straw</artifactId>
    <version>1.0.0</version>
    <properties>
        ...
        <maven.build.timestamp.format>yyyy-MM-ddHHmm</maven.build.timestamp.format>
    </properties>
    <build>
        ...
        <finalName>
            ${project.artifactId}-${project.version}-${spring.profiles.active}_${maven.build.timestamp}
        </finalName>
    </build>

打包完成後生成的jar:hi-straw-1.0.0-prod_2019-04-091533.jar

shell腳本編寫

登陸服務器,clone項目至/root/repo_git/目錄下,執行進入script目錄下,執行./build.sh,需要將RELEASE_HOST換成你自己的服務器地址,方便做保存備份

#!/bin/sh
set -e
#打包的服務器地址
RELEASE_HOST="你自己的服務器地址"
#打包的環境
RELEASE_ENV=prod
#項目目錄
BASE_DIR=/root/repo_git/Histraw
#進入項目目錄
cd ${BASE_DIR}
#執行git拉去最新的代碼
echo "pulling changes..."
git pull origin master
echo "pulling changes... finish"
echo "building..."
#執行mvn命令打包
mvn clean
mvn package -P${RELEASE_ENV} -DskipTests docker:build
echo "building...finish"
echo "env =${RELEASE_ENV}"
#for HOST in ${RELEASE_HOST}; do
#進行拷貝及備份
RELEASE_TARGET=root@${RELEASE_HOST}:~/release/
echo "copying to $RELEASE_TARGET..."
scp ${BASE_DIR}/target/*.jar ${RELEASE_TARGET}
echo "copying to $RELEASE_TARGET...done"
#done

執行build.sh
執行docker images查看剛剛打包好的docker鏡像
docker鏡像

maven + docker 打包部署

1.docker環境安裝

卸載老舊的版本(若未安裝過可省略此步):

sudo apt-get remove docker docker-engine docker.io

安裝最新的docker:

curl -fsSL get.docker.com -o get-docker.sh
sudo sh get-docker.sh

確認Docker成功安裝:

docker run hello-world

2.項目編譯打包

src/main/docker下建立dockerFile文件

FROM openjdk:8-jdk-alpine
VOLUME /tmp
ADD *.jar app.jar
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]

pom.xml配置docker打包,配合shell腳本在linux實現maven自動打包docker

 <!-- Docker maven plugin -->
    <plugin>
        <groupId>com.spotify</groupId>
        <artifactId>docker-maven-plugin</artifactId>
        <version>1.0.0</version>
        <configuration>
            <imageName>${project.artifactId}</imageName>
            <dockerDirectory>src/main/docker</dockerDirectory>
            <resources>
                <resource>
                    <targetPath>/</targetPath>
                    <directory>${project.build.directory}</directory>
                    <include>${project.build.finalName}.jar</include>
                </resource>
            </resources>
        </configuration>
    </plugin>
<!-- Docker maven plugin -->

執行docker images查看剛剛打包的docker鏡像

執行docker run --name hi-straw -p 8989:8989 -t hi-straw啓動鏡像

執行dockers ps查看已啓動docker鏡像
已啓動

nginx配置https

1.安裝nginx

登陸到服務器,執行

$ apt-get update // 更新軟件
$ apt-get install nginx // 安裝nginx

2. 獲取證書

可以去阿里雲獲取免費證書
將生成的證書放入/etc/nginx/sites-enabled/cert/(具體看你將nginx安裝在哪個目錄下)

3. 配置nginx文件

新建一個https.conf

server {
    listen 443;
    server_name 你自己的域名;
    ssl on;
    ssl_certificate  cert/你自己的證書.pem;
    ssl_certificate_key cert/你自己的證書.key;
    ssl_session_timeout 5m;
    ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
    ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2;
    ssl_prefer_server_ciphers on;
    location / {
        #項目部署的ip和端口
    	proxy_pass http://localhost:port;

    }
}

配置完成後,檢查一下nginx配置文件是否可用

nginx -t //檢查nginx配置文件

配置正確後,重新加載配置文件使配置生效

nginx -s reload //使配置生效

如需重啓nginx,用以下命令:

service nginx stop //停止
service nginx start //啓動
service nginx restart //重啓

部署

修改resource下的application-dev.ymlapplication-prod.yml中你自己申請的微信息及七牛雲信息修改,修改數據庫地址,用戶名和密碼

#七牛
qiNiu:
  accessKey: 你申請的七牛雲key
  secretKey: 你申請的七牛雲sec
  bucket: 你申請的七牛雲bucket
  domain: 你申請的七牛雲domain
#微信
wx:
  appId: 你申請的微信id
  appSec: 你申請的微信sec
spring:
  datasource:
    name: graduate
    driver-class-name: com.mysql.jdbc.Driver
    url: 數據庫地址
    username: 數據庫用戶名
    password: 數據庫密碼

修改script目錄下build.sh

RELEASE_HOST="你自己的服務器地址"

項目的服務器7月15號到期了……哪位大佬資助一下服務器,感激不盡

捐助

請作者喝咖啡

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章