这是毕设系列的第三篇。其余两篇在:
本地服务器的搭建和前后端打通(一) 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 的虚拟路径映射才行。这里我就不多说了,大家可以搜下,很简单,只要改一下配置文件的参数即可。
至于其中的正则替换符号是因为这里默认的是 “\”。但是网络请求我需要的是 "/"。所以做了替换。
好了代码就到这里。下次再见。
愿我们都能走过迷茫,
愿我们不负青春,
愿我们回首可以笑看而不后悔,
你要相信所有的付出都会在某一天给你回报,
愿我们成为真实的自己。