解决移动端头像参数上传的前后台问题(三)

这是毕设系列的第三篇。其余两篇在:
本地服务器的搭建和前后端打通(一) https://www.jianshu.com/p/5d1fe5b24993
本地服务器的搭建和前后端打通(二) https://www.jianshu.com/p/565bfa10e926

年里面一直在走亲戚,一没好好休息,二没好好完成项目。不过还行,自己还算顺利。有几个难点也在公司做后台的大佬帮助下解决了,这里和大家说下上传头像文件的实现。

一:移动端部分
想必大家在网上搜索 Android 上传头像代码可以有一堆。但是只有 Android 的代码,没有后台,没有后台。那各位小哥哥小姐姐你能告诉我我们自己实现的话有哪些注意点吗。在踩了一堆乱坑之后我终于完成了,并把几个点和大家说下。【

上代码:

 /**
    * 上传头像的接口
    */
   @Multipart
   @POST("phoneUser/modifyAvatar")
   fun uploadAvatar(@Part("userid") userid: RequestBody, @Part imgs: MultipartBody.Part): Observable<BaseJackson<LoginUser>>

BaseJackson 这个根据自己后台返回的基本格式来写就可以

data class BaseJackson<T>(
        var result: String,
        var data: T,
        var code: Int,
        var msg: String
) : Serializable

LoginBean

/**
 *  author:Jiwenjie
 *  email:278630464@qq.com
 *  time:2018/12/17
 *  desc:登陆用户的 bean
 *  version:1.0
 */
class LoginUser : Serializable {

   var userid: String? = null
   var userphone: String? = null
   var username: String? = null
   var password: String? = null
   var continuesigndays: Int? = null         // indicate(表示, 表明) how many days have singed
   var signintoday: Boolean? = null  // 表示今天是否签到
   var signintime: String? = null      // 表示今天签到时间
   var signintotaldays: Int? = null     // 表示一共签到了几天
   var logintime: String? = null     // 表示第一次登陆时间 any day's the first login time
   var logouttime: String? = null     // 表示登出时间
   var totaltime: Long? = null        // 使用 App 的总时间
   var avatar: String? = null      // 用户头像在服务器存储的地址
   var signout: Boolean = false     // 标记是否退出账号

   var collectioncount: String? = null // 收藏了几篇文章
   var profile: String? = null   // profile
}

上传头像的工具类


/**
 *  author:Jiwenjie
 *  email:278630464@qq.com
 *  time:2019/02/09
 *  desc:上传文件图片的工具类
 *  version:1.0
 */
object UploadUtils {
    
    @SuppressLint("CheckResult")
    fun uploadAvatar(userid: String, imgPath: String, listener: UploadImageListener) {   // true 为上传成功,false 为上传失败
        var file: File? = null
        try {
            file = File(imgPath)
        } catch (e: URISyntaxException) {
            e.printStackTrace()
        }

        val requestBody = RequestBody.create(MediaType.parse("multipart/form-data"), file!!)
        val body = MultipartBody.Part.createFormData("image", file.name, requestBody)   // 这里的 key 要和后台接收的 body 名字相同才行

        /**
         * 参数和图片一起上传的时候两种方式,这是第一种,第二种是使用 query 关键字
          */
        val idBody = RequestBody.create(MediaType.parse("multipart/form-data"), userid)
        LogUtils.e("UploadUtils" + file.name)
        LogUtils.e("UploadUtils" + file.absolutePath)

        RetrofitManager.provideClient(ConstantConfig.JACKSON_BASE_URL)
            .create(JacksonApi::class.java)
            .uploadAvatar(idBody, body)
            .compose(RxJavaUtils.applyObservableAsync())
            .subscribe({
                if (it.result == "succeed") {
                    listener.uploadSuccess(it.data)
                } else {
                    listener.uploadFailed(it.msg)
                }
            }, {
                listener.uploadFailed(it.message.toString())
            })
    }

    interface UploadImageListener {

        fun uploadSuccess(user: LoginUser)

        fun uploadFailed(msg: String)
    }
}

基本就在这里了,具体的调用我们有展示出来。不过相信这点难不住各位啦。

这里我使用的是 Retrofit + RxJava 的网络请求。代码语言是 Kt。大家可以使用 MVP 模式来写,我页面都是使用 MVP 完成的。这里之所以只是单独封装是因为用的地方比较少而且这样会更加方便。

那么重点来了:
1)如何参数和图片一起上传。
答:首先上传图片必须使用 @POST 注解且 @Part imgs: MultipartBody.Part 参数中加上这句。不要问为什么,就可 Spring 处理图片文件上传的时候必须使用 MultipartFile 一样。都是特定的类,我们只要这样使用就行。

其次,我们正常使用 @POST 传递参数的时候需要加上 @Field("xxx")。这里不必,不但如此,而且变量名随便起,是的你没看错,随便起。我一直以为这个变量名称需要和后台的变量相对应,可是我错了。是需要对应但不在这里,下面我会提到。

key code

 val requestBody = RequestBody.create(MediaType.parse("multipart/form-data"), file!!)
        val body = MultipartBody.Part.createFormData("image", file.name, requestBody)   // 这里的 key 要和后台接收的 body 名字相同才行

这里我加了备注相信大家可以看的很清楚。这里传递的时候 createFormData 的第一个参数是一个名称,这就是 key。后台也是根据这个 key 来接收数据。到此如果你只传递图片的话已近完成了。关键代码就这些(前台)

2)如果参数和图片一起传递怎么办
答:别担心,有办法。

/**
         * 参数和图片一起上传的时候两种方式,这是第一种,第二种是使用 query 关键字
          */
        val idBody = RequestBody.create(MediaType.parse("multipart/form-data"), userid)

就在这。这句话我的理解就是把你传递的参数以表单的形式放在 RequestBody 中。如果有错欢迎打脸。

到这里基本前台的代码就结束了。

为什么我说网上的代码很坑,
其一网上的代码没说后台接收的关键字是在哪里设置,我们默认都感觉实在接口的参数变量一样。而且很多图片 name 传递的时候有 image,有 png,啥啥都有,我一开始以为必须是图片的意思就行。。。。

其二网上关于参数和图片一起传递只是给出了方法而没有实际使用 demo。这让我们这些菜鸟怎么接受的了。

不过各位不用担心,我是实用主义。给的代码就在自己的毕设项目中亲测可以。如果有任何问题都可以私信我。话不多说,下面看后台代码。

后台代码:

 /**
     * 用户头像上传
     */
    @RequestMapping(value = "/modifyAvatar", method = RequestMethod.POST)
    @ResponseBody
    public Map<String, Object> uploadAvatar(@RequestParam("userid") String userid, @RequestBody MultipartFile image) {
        // 在 spring 家族中上传头像都是使用 MultipartFile 类。多个文件则是数组类型
        System.out.println("文件名:" + image.getOriginalFilename() + "\n" + "userid:" + String.valueOf(userid));
        Map<String, Object> map = new HashMap<>();
        if (!image.isEmpty()) {
            String originalFileName = image.getOriginalFilename();
            String mimeType = request.getServletContext().getMimeType(originalFileName);
            System.out.println("mimeType: " + mimeType);
            // 是否图片文件
            if (mimeType != null && mimeType.startsWith("image/")) {
                try {
//                    PhoneUser user = (PhoneUser) session.getAttribute(Constant.SESSION_PHONE_USER);
                    String suffix = originalFileName.split("\\.")[1];   // 扩展名

                    // 上传到项目根目录的 upload 文件夹
                    String avatarPath = request.getSession().getServletContext().getRealPath("/upload") +
//                            File.separator + user.getUsername() +
                            File.separator + "avatar" +
                            File.separator + System.currentTimeMillis() + "." + suffix;

                    String savePath = avatarPath.substring(avatarPath.indexOf("\\upload"));
                    String finPath = savePath.replaceAll("\\\\", "/");
                    System.out.println("savePath:" + savePath);
                    System.out.println("finPath:" + finPath);

                    /**
                     * 上传到具体的硬盘路径,此时需要配置 tomcat 虚拟路径
                     */
//                    String avatarPath = "I:" + File.separator + "ProjectsFolder" + File.separator + "IdeaProject"
//                            + File.separator + "MovieProject" + File.separator + "src" + File.separator + "main"
//                            + File.separator + "webapp" + File.separator + "upload" + File.separator + user.getUsername()
//                            + File.separator + "avatar" + File.separator + System.currentTimeMillis() + "." + suffix;

                    System.out.println("tomcatPath: " + avatarPath);

                    File saveFile = new File(avatarPath);
                    if (!saveFile.getParentFile().exists()) {
                        saveFile.getParentFile().mkdirs();
                        saveFile.createNewFile();
                    }
                    image.transferTo(saveFile);    //将文件上传到指定的服务器的位置
                    int rows = userService.updateUserAvatar(userid, finPath.substring(1));  // 存储在数据库中的路径就从 upload 开始就可以了,
                                                                                            // 这里的 sub 是为了去除第一个 ‘/’
                    if (rows > 0) {
                        System.out.println("上传头像成功");
//                        // 上传文件成功之后查询 user,之后把最新的 user 返回
                        PhoneUser user = userService.getUserInfo(userid);
                        if (user != null) {
                            map.put("data", user);
                            map = CommonUtils.operationSucceed(map);
                        } else {
                            map = CommonUtils.operationFailed(map, "other error", HttpStatus.NOT_FOUND.value());
                        }
                    } else {
                        System.out.println("上传头像失败");
                        map = CommonUtils.operationFailed(map,
                                "change data failed", HttpStatus.BAD_REQUEST.value());
                    }
                } catch (IOException e) {
                    // 上传过程出错
                    System.out.println(e.getMessage());
                    map = CommonUtils.operationFailed(map, "upload fail", HttpStatus.INTERNAL_SERVER_ERROR.value());
                    e.printStackTrace();
                }
            } else {
                // 不是图片文件返回相关信息
                map = CommonUtils.operationFailed(map, "please upload an image file", HttpStatus.BAD_REQUEST.value());
            }
            // 空文件返回相关
        } else {
            System.out.println("empty file");
            map = CommonUtils.operationFailed(map, "empty file", HttpStatus.BAD_REQUEST.value());
        }
        return map;
    }

这里我都加了注释相信大家应该都能明白。这里的 upload 地址其实就是你的 tomcat 的地址。换句话说就是 localhost:8080 + upload 路径 就可以在浏览器中访问了。当然前提是你上传成功了之后才行。

这里也有几个问题。你如果不关注我注释的代码的话没什么问题,如果你想把路径改为具体的硬盘上的某个地址你需要重新配置下 tomcat 的虚拟路径映射才行。这里我就不多说了,大家可以搜下,很简单,只要改一下配置文件的参数即可。

至于其中的正则替换符号是因为这里默认的是 “\”。但是网络请求我需要的是 "/"。所以做了替换。

好了代码就到这里。下次再见。

愿我们都能走过迷茫,
愿我们不负青春,
愿我们回首可以笑看而不后悔,
你要相信所有的付出都会在某一天给你回报,
愿我们成为真实的自己。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,179评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,229评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,032评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,533评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,531评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,539评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,916评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,813评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,568评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,654评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,354评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,937评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,918评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,152评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,852评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,378评论 2 342