移动端图片基于后端框架的图片上传读取功能

浏览了大部分博客,似乎对于 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

这边自定义一个 抽屉层 ,模拟两种图片获取方式
- 文件
- 相机

抽屉层图片png

通过 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 生成的原因

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
贴吧
颜文字
Emoji
小恐龙
花!
上一篇