浏览了大部分博客,似乎对于
Android 原生+Kotlin构造的移动端,如何进行与后端交互图片信息,少且欠缺,通过 Ai 的辅助和美化,模拟了一种可能是常见的用户个人信息的后端操作,一种伪用户个人信息聚合数据其实这个功能倒是一个月或者一个半月前实现的,只是一直懒得写
就和我现在的博客 上一次更新是 2023 年一样,慵懒
仅仅是现在代码敲累了,想忙点别的
后端
采用
SpringBoot,无脑搭建一个 Api 供给移动端使用
基础框架
真的就是很基础
实现逻辑
本来我设想的是,可能需要对表进行操作,用
字节流的方式
后来 Ai 的逻辑是,对每一个用户新建个人用户信息文件夹,然后我对其进行的逻辑优化,后端的文件格式都是统一的,便于 Up Down 和管理
用户实体
@TableName(value ="user")
@Data
public class User {
@TableId(type = IdType.AUTO)
private Integer userid;
private String userphonenumber;
private String username;
private String userpassword;
private Integer signindays;
private String state;
// 省略多余代码
}
采用的是 MybatisX 框架进行自主生成的实体对象
关键唯一值字段是 userphonenumber
注册逻辑
@PostMapping("/register")
String register(String userphone , String username , String password){
User user = userService.selectByUserphonenumber(userphone);
if(user == null){
int r = userService.insertUser(userphone,username,password);
System.out.println(r);
if (r > 0) {
// 注册成功后创建y用户文件夹
File userFolder = new File(UserDir, userphone);
if (!userFolder.exists()) {
boolean isCreated = userFolder.mkdirs();
if (isCreated) {
System.out.println("Folder created for " + username);
} else {
System.out.println("Failed to create folder for " + username);
}
}
}
return "OK" ;
}
return "No";
}
注册完成后,根据唯一值字段,创建对应文件夹
不过个人项目没有任何密码或者是信息上的加密,其实常见应该是把个人信息数据进行 MD5 加密后丢进 Sql,又或者采用别的方法
这是我创建的个人信息文件夹
里面的头像默认重命名为 headshot
同理 个人背景图的设计逻辑应该也是可以这么处理
这边是微信的逻辑
上传读取逻辑
Controller 类下的 静态常量
// 图片上传路径 项目根目录下的uploads
private static final String UserDir = System.getProperty("user.dir") + "/userInfo";
// 扩展名白名单
private static final List ALLOWED_FILE_TYPES = Arrays.asList("image/jpeg", "image/png");
上传逻辑
// MultipartFile 文件类型对象
@PostMapping("/uploadHeadshot")
public String uploadHeadshot(@RequestParam("image") MultipartFile image ,@RequestParam("userPhoneNumber") String userPhoneNumber) {
System.out.println("upload in");
// 检查文件是否为空
if (image.isEmpty()) {
return "File Is Empty";
}
// 检查文件类型
String contentType = image.getContentType();
if (!ALLOWED_FILE_TYPES.contains(contentType)) {
return "File Type Is Error";
}
// 获取文件的扩展名
String extension = "";
String originalFilename = image.getOriginalFilename();
int i = originalFilename.lastIndexOf('.');
if (i > 0) {
extension = originalFilename.substring(i);
}
try {
// 构建文件存储路径,并使用新的文件名"headshot"
Path destinationDir = Paths.get(UserDir, userPhoneNumber).toAbsolutePath().normalize();
Path destinationFile = destinationDir.resolve("headshot" + extension);
// 确保目录存在
if (!destinationDir.toFile().exists()) {
destinationDir.toFile().mkdirs();
}
// 删除旧的头像文件
Files.walk(destinationDir)
.filter(path -> path.getFileName().toString().startsWith("headshot"))
.forEach(path -> {
try {
Files.deleteIfExists(path);
System.out.println("Replace Succeed");
} catch (IOException ex) {
// 处理删除文件时可能发生的异常
ex.printStackTrace();
}
});
// 保存文件
image.transferTo(destinationFile);
System.out.println("File Upload Succeed");
return "File Upload Succeed";
} catch (IOException e) {
e.printStackTrace();
System.out.println("File Upload Failed");
return "File Upload Failed:" + e.getMessage();
}
}
这部分 Ai 的手笔比较大
具体逻辑就是
- 存在用户信息根目录
- 前端直接传递文件字节流
- 先异常捕获
- 空判定
- 类型判定
- 其实这里最好,不论前端还是后端,进行一下 木马注入 判定,这里我没写,否则常见可能会造成 文件包含 文件上传 ,会被 上线远控
- 如果存在头像,替换
- 存储成功
读取逻辑
@GetMapping("/getHeadshot/{userPhoneNumber}")
public ResponseEntity getHeadshotImage(@PathVariable String userPhoneNumber) {
try {
// 构建图片文件的基本路径
Path userDirPath = Paths.get(UserDir, userPhoneNumber);
// 在目录中查找名为"headshot"的文件
Path headshotPath = Files.list(userDirPath)
.filter(path -> path.getFileName().toString().startsWith("headshot"))
.findFirst()
.orElse(null);
// 如果找到了文件,则返回它
if (headshotPath != null && Files.exists(headshotPath)) {
Resource resource = new UrlResource(headshotPath.toUri());
String contentType = Files.probeContentType(headshotPath);
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(contentType))
.header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + headshotPath.getFileName() + "\"")
.body(resource);
} else {
// 文件不存在
return ResponseEntity.notFound().build();
}
} catch (IOException e) {
e.printStackTrace();
return ResponseEntity.badRequest().build();
}
}
同样,Ai 润色痕迹很重
前端采用 Path 传递 userPhoneNumber 给 Api ,然后读取信息
测试功能
非常的 nice 啊!
小祥可爱捏 QwQ
移动端
移动端采用的是 Android 原生开发
界面实现
HomeView
采用 CircleImageView 生成适配的圆形头像界面
添加两个提示对话框
UserSettingView
这边自定义一个
抽屉层,模拟两种图片获取方式
- 文件
- 相机
通过 BottomSheetDialogFragment 对话框,调用回调函数,实现上传
实现逻辑
图片加载
图片加载采用了
Glide进行读取 , 直接通过 网络方式 访问图片
fun loadUserHeadshot(userPhone: String, headshot: ImageView): RequestBuilder {
return Glide.with(MoApplication.context)
.load(Repository.BASE_URL + "/user/getHeadshot/" + userPhone)
.apply(RequestOptions().diskCacheStrategy(DiskCacheStrategy.NONE))
.apply(RequestOptions().error(R.drawable.headshot))
.apply(RequestOptions().dontTransform()) // 防止错误日志输出
}
这边进行了 二次封装
传入 控件对象 和 用户唯一标识值
若未加载到图片 , 加载默认的
由于多个地方需要用到 头像加载 逻辑
所以进行封装
调用如下
Repository.loadUserHeadshot(user.phoneNumber, headshot).into(headshot)
Path 访问 后端 Api
@GET("/user/getHeadshot/{userPhoneNumber}")
fun getUserHeadshot(@Path("userPhoneNumber") userPhoneNumber:String):Call
图片上传
调用回调函数 文件选择或者启动相机
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
REQUEST_CODE_PERMISSIONS_CAMERA -> {
if (grantResults.all { it == PackageManager.PERMISSION_GRANTED }) {
if (hasStoragePermissions()) {
showChooseImageDialog()
}
} else {
if (!shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) {
showPermissionSettingsDialog("相机")
} else {
Toast.makeText(this, "相机权限未授权", Toast.LENGTH_SHORT).show()
}
}
}
REQUEST_CODE_PERMISSIONS_STORAGE -> {
if (grantResults.all { it == PackageManager.PERMISSION_GRANTED }) {
if (hasCameraPermissions()) {
showChooseImageDialog()
}
} else {
if (!shouldShowRequestPermissionRationale(Manifest.permission.READ_EXTERNAL_STORAGE)) {
showPermissionSettingsDialog("文件")
} else {
Toast.makeText(this, "文件权限未授权", Toast.LENGTH_SHORT).show()
}
}
}
}
}
未授权处理
private fun showPermissionSettingsDialog(mode:String) {
AlertDialog.Builder(this)
.setMessage("此应用需要 $mode 权限才能正常工作。请前往设置以授权此权限。")
.setPositiveButton("去设置") { _, _ ->
// 导航到设置页面
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
val uri = Uri.fromParts("package", packageName, null)
intent.data = uri
startActivity(intent)
}
.setNegativeButton("取消", null)
.create()
.show()
}
上传图片字节流
suspend fun uploadHeadshot(image: Bitmap, userphone: String): ResponseBody {
val byteArrayOutputStream = ByteArrayOutputStream()
image.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream)
val imageBytes = byteArrayOutputStream.toByteArray()
val imagePart = MultipartBody.Part.createFormData("image", "headshot.png", imageRequestBody)
return UserService.uploadHeadshot(imagePart, userPhoneNumberRequestBody).await()
}
Post 访问 后端 Api
@Multipart
@POST("/user/uploadHeadshot")
fun uploadHeadshot(
@Part image: MultipartBody.Part,
@Part("userPhoneNumber") userPhoneNumber: RequestBody
): Call
测试功能


题外话
写的时候,移动端的网络模块炸了,不清楚是为什么,换手机热点,换个ip,又出现了超时错误
第二天下午三点了,依旧没处理成功
但是切换手机调试,任何功能就趋于正常了,不是很理解,但是界面动画还是存在掉帧卡顿,可能是采用 View 生成的原因









