Save/Write/Read image from/to a database table

本文介绍如何使用Java在数据库中创建包含BLOB字段的表,并演示了插入和检索图像文件的具体步骤。

The sample table is created as below to have a blob field to store file.

CREATE TABLE t1 (c1 INT PRIMARY KEY NOT NULL, c2 BLOB(5M));

Then use the below code snippet to insert an image file as follows.
 
PreparedStatement pstmt = conn.prepareStatement ("INSERT INTO t1 VALUES (?,?)");
pstmt.setInt (1, 100);
File fBlob = new File ( "image1.gif" );
FileInputStream is = new FileInputStream ( fBlob );
pstmt.setBinaryStream (2, is, (int) fBlob.length() );
pstmt.execute ();
...
 

Retrieving a BLOB or in other words retrieving the image file stored in the database as follows:
 
Statement stmt = conn.createStatement ();
ResultSet rs= stmt.executeQuery("SELECT * FROM t1");
while(rs.next()) {
int val1 = rs.getInt(1);
InputStream val2 = rs.getBinaryStream(2);
...
} rs.close();

from selenium import webdriver from selenium.webdriver.chrome.service import Service from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.common.exceptions import ElementClickInterceptedException import csv import pymysql import os import requests import time import datetime import re def download_image(url, save_path, filename): if not url or url.startswith(('data:image', 'javascript:')): print(f"无效的图片URL: {url}") return # 补全 URL(如果以 // 开头) if url.startswith('//'): url = 'https:' + url # 当当网图片通常支持 HTTPS elif not url.startswith(('http://', 'https://')): url = 'https://' + url # 其他情况默认加 HTTPS # 清洗文件名 def sanitize_filename(name): return re.sub(r'[\\/*?:"<>|]', "_", name) safe_filename = sanitize_filename(filename) save_to = os.path.join(save_path, safe_filename) # 请求配置 headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 'Referer': 'http://www.dangdang.com/' } # 重试机制 max_retries = 3 for attempt in range(max_retries): try: response = requests.get(url, headers=headers, timeout=10) if response.status_code == 200: if not os.path.exists(save_path): os.makedirs(save_path) with open(save_to, 'wb') as f: f.write(response.content) print(f'图片已保存: {save_to}') break else: print(f"HTTP错误: {response.status_code} (URL: {url})") except Exception as e: print(f"第 {attempt + 1} 次尝试失败: {str(e)}") time.sleep(2) else: print(f"下载失败(超过最大重试次数): {url}") # 初始化浏览器 driver_path = r"D:\chromedriver-win32\chromedriver.exe" options = webdriver.ChromeOptions() options.add_argument('--headless') service = Service(driver_path) driver = webdriver.Chrome(service=service, options=options) try: driver.get('http://www.dangdang.com/') # 设置窗口大小,可能避免某些元素遮挡 driver.set_window_size(1920, 1080) # 使用更健壮的方式获取搜索框 search_term = input("请输入要搜索的图书类型:") search_box = WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.ID, "key_S")) ) search_box.clear() search_box.send_keys(search_term) # 等待搜索按钮可点击 search_btn = WebDriverWait(driver, 10).until( EC.element_to_be_clickable((By.CSS_SELECTOR, ".search .button")) ) # 尝试处理可能遮挡的浮动元素 try: WebDriverWait(driver, 5).until( EC.invisibility_of_element_located((By.CSS_SELECTOR, "a.robot")) ) except: pass # 尝试点击搜索按钮 try: search_btn.click() except ElementClickInterceptedException: print("常规点击被拦截,尝试使用JavaScript点击") driver.execute_script("arguments[0].click();", search_btn) id = 0 data = [['图书编号', '图书名称', '图书作者', '图书出版社', '图书图片url', '图书价格', '图书简介']] # 使用更可靠的翻页方式 for _ in range(3): # 限制爬取3页防止无限循环 # 等待结果加载 WebDriverWait(driver, 15).until( EC.presence_of_element_located((By.CSS_SELECTOR, "#search_nature_rg")) ) book_items = driver.find_elements(By.CSS_SELECTOR, "#search_nature_rg li") for item in book_items: books = [] id += 1 books.append(id) # 标题 try: title = item.find_element(By.CSS_SELECTOR, "a[dd_name='单品标题']").get_attribute("title") if not title: title = item.find_element(By.CSS_SELECTOR, "a:first-child").text.strip() books.append(title) except: books.append("未知标题") # 作者 try: author = item.find_element(By.CSS_SELECTOR, ".search_book_author a").text books.append(author) except: books.append("未知作者") # 出版社 try: press_span = item.find_element(By.CSS_SELECTOR, ".search_book_author span:nth-child(3)") press = press_span.find_element(By.TAG_NAME, "a").text books.append(press) except: books.append("未知出版社") # 图片URL try: img = item.find_element(By.CSS_SELECTOR, "a img") src = img.get_attribute("src") if "url_none.png" in src: src = img.get_attribute("data-original") or "" books.append(src) except: books.append("") # 价格 try: price = item.find_element(By.CSS_SELECTOR, ".search_now_price").text.replace("¥", "") books.append(price) except: books.append("0.00") # 简介 try: intro = item.find_element(By.CSS_SELECTOR, ".detail").text books.append(intro) except: books.append("无简介") data.append(books) # 翻页 try: next_btn = WebDriverWait(driver, 10).until( EC.element_to_be_clickable((By.CSS_SELECTOR, ".next")) ) next_btn.click() time.sleep(2) except: print("已到达最后一页或翻页失败") break # 保存到CSV timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") csv_filename = f'dangdang_books_{timestamp}.csv' with open(csv_filename, 'w', newline='', encoding='utf-8-sig') as f: writer = csv.writer(f) writer.writerows(data) # 数据库操作 conn = pymysql.connect( user="root", password="123456", host="localhost", port=3306, charset='utf8mb4' ) cursor = conn.cursor() try: cursor.execute("CREATE DATABASE IF NOT EXISTS xyw CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci") except Exception as e: print(f"创建数据库失败: {str(e)}") exit() # 选择数据库 conn.select_db("xyw") # 创建表(如果不存在) create_table_sql = """ CREATE TABLE IF NOT EXISTS dangdang( id INT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(255) CHARACTER SET utf8mb4, author VARCHAR(100) CHARACTER SET utf8mb4, press VARCHAR(100) CHARACTER SET utf8mb4, src VARCHAR(255), price DECIMAL(10,2), introduction TEXT CHARACTER SET utf8mb4 ); """ cursor.execute(create_table_sql) # 插入数据 insert_sql = """ INSERT INTO dangdang (title, author, press, src, price, introduction) VALUES (%s, %s, %s, %s, %s, %s) """ # 新增:统计成功插入的记录数 inserted_count = 0 for row in data[1:]: try: price_value = float(row[5]) if row[5].replace('.', '', 1).isdigit() else 0.0 cursor.execute(insert_sql, (row[1], row[2], row[3], row[4], price_value, row[6])) inserted_count += 1 # 每成功插入一条,计数器加1 except Exception as e: print(f"插入数据失败: {str(e)}") conn.commit() print(f"成功插入 {inserted_count} 条数据到数据库") # 使用自定义计数器 # 下载图片(修正缩进) save_directory = 'download' if not os.path.exists(save_directory): os.makedirs(save_directory) for i, row in enumerate(data[1:], 1): image_url = row[4] if image_url: new_filename = f'{str(i).zfill(4)}.jpg' download_image(image_url, save_directory, new_filename) finally: # 确保关闭资源 if 'conn' in locals() and conn.open: cursor.close() conn.close() driver.quit()将这个代码改为selenium+scrapy,功能不要改变xyw/ ├── scrapy.cfg # Scrapy 项目部署配置文件(定义项目名称、部署目标等) └── xyw/ # 项目核心模块目录(与项目名同名) ├── __init__.py # Python 包标识文件(空文件或做包初始化) ├── items.py # 定义爬取数据的结构化字段(类似数据模型) ├── middlewares.py # 中间件文件(处理请求、响应的自定义逻辑,如 UA 伪装、代理等) ├── pipelines.py # 管道文件(数据清洗、存储逻辑,如存数据库、去重等) ├── run.py # 自定义运行入口(可能用于启动爬虫,替代 `scrapy crawl` 命令) ├── settings.py # 项目全局配置(爬虫开关、并发数、日志、管道启用等) ├── show.py # 自定义脚本(可能用于展示爬取结果、调试数据等,非 Scrapy 标准文件) └── spiders/ # 爬虫脚本目录 ├── __init__.py # Python 包标识文件(空文件或做爬虫包初始化) └── dangdang_spider.py # 当当网爬虫脚本(具体的爬取逻辑实现) 这是项目框架,每个文件的内容是什么
06-13
package com.example.labmemotest; import androidx.appcompat.app.AppCompatActivity; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import androidx.core.content.FileProvider; import android.Manifest; import android.content.ActivityNotFoundException; import android.content.ContentValues; import android.content.Intent; import android.content.pm.PackageManager; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.graphics.Color; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.os.Handler; import android.os.Looper; import android.provider.MediaStore; import android.util.Log; import android.view.View; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.EditText; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.Spinner; import android.widget.Toast; import com.bumptech.glide.Glide; import com.example.labmemotest.bean.CustomOption; import com.example.labmemotest.db.labDbHelper; import java.io.File; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Random; import java.util.concurrent.Executor; import java.util.concurrent.Executors; public class AddInfoActivity extends AppCompatActivity { private EditText edit_title, edit_content, edit_location; private Button btn_camera, btn_photo, btn_save; private LinearLayout previewContainer; private String tmp_path; private labDbHelper mhelper; private SQLiteDatabase db; private List<String> imagePaths = new ArrayList<>(); private Handler handler = new Handler(Looper.getMainLooper()); private ActivityResultLauncher<Intent> cameraLauncher; private ActivityResultLauncher<Intent> galleryLauncher; private static final int CAMERA_PERMISSION_CODE = 100; private static final int GALLERY_PERMISSION_CODE = 200; private LinearLayout containerPrefixGroups; private List<Spinner> spinners = new ArrayList<>(); private String currentUserId = "USER001"; // 应从登录系统获取 //新增,修改 private Executor executor = Executors.newSingleThreadExecutor(); // 添加线程池 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_add_info); // 修复初始化顺序 initView(); // 先初始化视图 initDb(); // 再初始化数据库 initLaunchers(); // 初始化结果启动器 loadCustomIdSelectors(); // 最后加载选择器 btnOnClick(); // 设置按钮监听 } private void initLaunchers() { cameraLauncher = registerForActivityResult( new ActivityResultContracts.StartActivityForResult(), result -> { if (result.getResultCode() == RESULT_OK) { File imgFile = new File(tmp_path); if (imgFile.exists()) { try { Uri imageUri = FileProvider.getUriForFile( this, getPackageName() + ".fileprovider", imgFile ); // 添加持久化权限 grantPersistablePermission(imageUri); imagePaths.add(imageUri.toString()); updateImagePreviews(); } catch (Exception e) { Toast.makeText(this, "URI转换失败: " + e.getMessage(), Toast.LENGTH_LONG).show(); } } else { Toast.makeText(this, "拍摄的图片未保存", Toast.LENGTH_LONG).show(); } } else if (result.getResultCode() == RESULT_CANCELED) { // 删除临时文件 new File(tmp_path).delete(); } } ); galleryLauncher = registerForActivityResult( new ActivityResultContracts.StartActivityForResult(), result -> { if (result.getResultCode() == RESULT_OK && result.getData() != null) { Intent data = result.getData(); try { if (data.getClipData() != null) { int count = data.getClipData().getItemCount(); for (int i = 0; i < count; i++) { Uri uri = data.getClipData().getItemAt(i).getUri(); // 添加持久化权限 grantPersistablePermission(uri); imagePaths.add(uri.toString()); } } else if (data.getData() != null) { Uri uri = data.getData(); grantPersistablePermission(uri); imagePaths.add(uri.toString()); } updateImagePreviews(); } catch (Exception e) { Log.e("GalleryError", "相册选择失败", e); Toast.makeText(this, "图片选择失败: " + e.getMessage(), Toast.LENGTH_SHORT).show(); } } } ); } // 统一处理持久化权限授予 private void grantPersistablePermission(Uri uri) { try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { // 获取持久化URI权限 getContentResolver().takePersistableUriPermission( uri, Intent.FLAG_GRANT_READ_URI_PERMISSION ); } else { // 旧版本使用临时权限 grantUriPermission( getPackageName(), uri, Intent.FLAG_GRANT_READ_URI_PERMISSION ); } // 显式添加URI权限(双重保障) grantUriPermission( getPackageName(), uri, Intent.FLAG_GRANT_READ_URI_PERMISSION ); } catch (SecurityException e) { Log.e("PermissionError", "权限授予失败: " + uri, e); Toast.makeText(this, "权限获取失败: " + e.getMessage(), Toast.LENGTH_SHORT).show(); } } private void initView() { edit_title = findViewById(R.id.editText_title); edit_content = findViewById(R.id.editText_content); edit_location = findViewById(R.id.editText_location); btn_camera = findViewById(R.id.button_camera); btn_photo = findViewById(R.id.button_photo); btn_save = findViewById(R.id.button_save); previewContainer = findViewById(R.id.image_preview_container); // ... 原有 findViewById ... containerPrefixGroups = findViewById(R.id.container_prefix_groups); // loadCustomIdSelectors(); // 加载编号选择器 } // private void initDb() { // try { // mhelper = new labDbHelper(AddInfoActivity.this); // db = mhelper.getWritableDatabase(); // } catch (Exception e) { // Log.e("DbInitError", "数据库初始化失败", e); // Toast.makeText(this, "数据库初始化失败", Toast.LENGTH_SHORT).show(); // } // } private void btnOnClick() { btn_camera.setOnClickListener(v -> checkCameraPermission()); btn_photo.setOnClickListener(v -> checkGalleryPermission()); btn_save.setOnClickListener(v -> btnSave()); } // 检查相机权限兼容Android各个版本 private void checkCameraPermission() { List<String> permissionsNeeded = new ArrayList<>(); // 1. 相机权限是必须的 if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { permissionsNeeded.add(Manifest.permission.CAMERA); } // 2. 根据Android版本处理存储权限 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { // Android 13+ 需要READ_MEDIA_IMAGES权限 if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_MEDIA_IMAGES) != PackageManager.PERMISSION_GRANTED) { permissionsNeeded.add(Manifest.permission.READ_MEDIA_IMAGES); } } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { // Android 10-12 不需要额外权限(使用分区存储) } else { // Android 9及以下需要WRITE_EXTERNAL_STORAGE权限 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { permissionsNeeded.add(Manifest.permission.WRITE_EXTERNAL_STORAGE); } } if (permissionsNeeded.isEmpty()) { openCamera(); } else { ActivityCompat.requestPermissions( this, permissionsNeeded.toArray(new String[0]), CAMERA_PERMISSION_CODE ); } } // 检查相册权限兼容Android各个版本 private void checkGalleryPermission() { List<String> permissionsNeeded = new ArrayList<>(); // 根据Android版本处理读取权限 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { // Android 13+ 需要READ_MEDIA_IMAGES权限 if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_MEDIA_IMAGES) != PackageManager.PERMISSION_GRANTED) { permissionsNeeded.add(Manifest.permission.READ_MEDIA_IMAGES); } } else { // Android 12及以下需要READ_EXTERNAL_STORAGE权限 if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { permissionsNeeded.add(Manifest.permission.READ_EXTERNAL_STORAGE); } } if (permissionsNeeded.isEmpty()) { openGallery(); } else { ActivityCompat.requestPermissions( this, permissionsNeeded.toArray(new String[0]), GALLERY_PERMISSION_CODE ); } } // 打开相机并创建临时文件 private void openCamera() { try { // 确保图片目录存在 File storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES); if (storageDir == null) { Toast.makeText(this, "无法访问存储空间", Toast.LENGTH_SHORT).show(); return; } if (!storageDir.exists() && !storageDir.mkdirs()) { Toast.makeText(this, "无法创建图片目录", Toast.LENGTH_SHORT).show(); return; } // 创建带时间戳的唯一文件名 String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date()); String imageFileName = "JPEG_" + timeStamp + ".jpg"; File imageFile = new File(storageDir, imageFileName); tmp_path = imageFile.getAbsolutePath(); // 创建FileProvider URI Uri photoURI = FileProvider.getUriForFile( this, getPackageName() + ".fileprovider", imageFile ); // 启动相机Intent Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI); takePictureIntent.addFlags( Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION ); // 确保有相机应用可以处理该Intent if (takePictureIntent.resolveActivity(getPackageManager()) != null) { cameraLauncher.launch(takePictureIntent); } else { Toast.makeText(this, "未找到相机应用", Toast.LENGTH_SHORT).show(); } } catch (ActivityNotFoundException e) { Toast.makeText(this, "未找到相机应用", Toast.LENGTH_SHORT).show(); } catch (Exception e) { Log.e("CameraError", "相机启动失败", e); Toast.makeText(this, "相机启动失败: " + e.getMessage(), Toast.LENGTH_LONG).show(); } } // 打开相册选择图片 private void openGallery() { try { Intent galleryIntent = new Intent(Intent.ACTION_OPEN_DOCUMENT); galleryIntent.addCategory(Intent.CATEGORY_OPENABLE); galleryIntent.setType("image/*"); galleryIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); // 添加权限Flag确保可以获取持久化权限 galleryIntent.addFlags( Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION ); galleryLauncher.launch(galleryIntent); } catch (ActivityNotFoundException e) { Toast.makeText(this, "未找到图库应用", Toast.LENGTH_SHORT).show(); } catch (Exception e) { Log.e("GalleryError", "图库打开失败", e); Toast.makeText(this, "图库打开失败: " + e.getMessage(), Toast.LENGTH_SHORT).show(); } } // 更新图片预览区域 private void updateImagePreviews() { previewContainer.removeAllViews(); for (String uriString : imagePaths) { try { Uri uri = Uri.parse(uriString); // 创建图片预览视图 ImageView imageView = new ImageView(this); LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( 300, 300 ); params.setMargins(8, 8, 8, 8); imageView.setLayoutParams(params); imageView.setScaleType(ImageView.ScaleType.CENTER_CROP); imageView.setBackgroundColor(Color.parseColor("#F5F5F5")); // 使用Glide加载图片 Glide.with(this) .load(uri) .placeholder(R.drawable.ic_default_image) .error(R.drawable.ic_error_image) .into(imageView); previewContainer.addView(imageView); } catch (Exception e) { Log.e("PreviewError", "图片预览失败: " + uriString, e); Toast.makeText(this, "图片预览失败: " + e.getMessage(), Toast.LENGTH_SHORT).show(); } } } // 修复1:确保在UI线程操作视图 private void loadCustomIdSelectors() { // 确保在主线程操作UI runOnUiThread(() -> { try { // 安全移除视图 if (containerPrefixGroups != null) { containerPrefixGroups.removeAllViews(); } spinners.clear(); // 检查数据库可用性 if (db == null || !db.isOpen()) { Log.e("LoadSelector", "数据库不可用"); return; } // 使用线程池执行数据库查询 executor.execute(() -> { Cursor cursor = null; try { cursor = db.rawQuery( "SELECT group_id, option_index, display_text, code_char " + "FROM tb_custom_config WHERE user_id=? ORDER BY group_id", new String[]{currentUserId} ); final Map<Integer, List<CustomOption>> groupsMap = new LinkedHashMap<>(); while (cursor != null && cursor.moveToNext()) { int groupId = cursor.getInt(0); int optionIdx = cursor.getInt(1); String text = cursor.getString(2); String code = cursor.getString(3); groupsMap.computeIfAbsent(groupId, k -> new ArrayList<>()) .add(new CustomOption(currentUserId, groupId, optionIdx, text, code)); } // 回到主线程更新UI runOnUiThread(() -> { if (groupsMap.isEmpty()) { View selectorLayout = findViewById(R.id.layout_id_selection); if (selectorLayout != null) { selectorLayout.setVisibility(View.GONE); } return; } for (List<CustomOption> options : groupsMap.values()) { addSpinnerForGroup(options); } }); } catch (Exception e) { Log.e("LoadSelector", "查询失败", e); } finally { if (cursor != null) { cursor.close(); } } }); } catch (Exception e) { Log.e("LoadSelector", "初始化失败", e); } }); } // 修复2:增强异常处理 private void addSpinnerForGroup(List<CustomOption> options) { try { LinearLayout row = new LinearLayout(this); row.setLayoutParams(new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, 120)); row.setOrientation(LinearLayout.HORIZONTAL); row.setPadding(0, 0, 0, 8); Spinner spinner = new Spinner(this); spinner.setLayoutParams(new LinearLayout.LayoutParams(0, 100, 1f)); spinner.setPadding(16, 0, 16, 0); ArrayAdapter<CustomOption> adapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, options); adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); spinner.setAdapter(adapter); spinners.add(spinner); row.addView(spinner); // 安全添加视图 if (containerPrefixGroups != null) { containerPrefixGroups.addView(row); } } catch (Exception e) { Log.e("AddSpinner", "创建选择器失败", e); } } // 生成唯一备忘录ID // private String generateUniqueMemoId() { // if (db == null || !db.isOpen()) { // initDb(); // } // // if (db == null) { // Toast.makeText(this, "数据库不可用", Toast.LENGTH_SHORT).show(); // return ""; // } // // Random random = new Random(); // String memoId; // int attempts = 0; // boolean idExists; // // do { // // 生成9位随机数 // memoId = String.valueOf(100000000 + random.nextInt(900000000)); // idExists = false; // // // // // try { // // 检查ID是否已存在 // Cursor cursor = db.query( // "tb_memory", // new String[]{"memo_id"}, // "memo_id = ?", // new String[]{memoId}, // null, null, null // ); // idExists = cursor.getCount() > 0; // cursor.close(); // } catch (Exception e) { // Log.e("GenerateIdError", "ID检查失败", e); // Toast.makeText(this, "ID检查失败", Toast.LENGTH_SHORT).show(); // return ""; // } // // attempts++; // // 防止无限循环 // if (attempts > 100) { // Toast.makeText(this, "无法生成唯一ID", Toast.LENGTH_SHORT).show(); // return ""; // } // } while (idExists); // // return memoId; // } // 替换原有的 generateUniqueMemoId() // String uniqueMemoId = generateMemoIdFromSelection(); // 修复3:增强ID生成稳定性 private String generateMemoIdFromSelection() { try { StringBuilder prefix = new StringBuilder(); if (spinners == null || spinners.isEmpty()) { return "X" + String.format(Locale.getDefault(), "%05d", new Random().nextInt(100000)); } for (Spinner spinner : spinners) { if (spinner == null) continue; Object selected = spinner.getSelectedItem(); if (selected instanceof CustomOption) { CustomOption option = (CustomOption) selected; if (option.codeChar != null) { prefix.append(option.codeChar); } } } if (prefix.length() == 0) prefix.append("X"); String suffix = String.format(Locale.getDefault(), "%05d", new Random().nextInt(100000)); return prefix.toString() + suffix; } catch (Exception e) { Log.e("GenID", "生成ID失败", e); return "ERR" + System.currentTimeMillis(); } } // 修复4:增强数据库操作安全性 private void initDb() { executor.execute(() -> { try { mhelper = new labDbHelper(AddInfoActivity.this); db = mhelper.getWritableDatabase(); } catch (Exception e) { Log.e("DbInit", "数据库初始化失败", e); runOnUiThread(() -> Toast.makeText(this, "数据库初始化失败", Toast.LENGTH_SHORT).show()); } }); } // 保存按钮点击事件 private void btnSave() { String title = edit_title.getText().toString().trim(); String content = edit_content.getText().toString().trim(); String location = edit_location.getText().toString().trim(); String uniqueMemoId = generateMemoIdFromSelection(); // ✅ 正确位置 if (uniqueMemoId.isEmpty()) { Toast.makeText(this, "无法生成有效ID", Toast.LENGTH_SHORT).show(); return; } // 验证输入 if (title.isEmpty()) { Toast.makeText(this, "请输入标题", Toast.LENGTH_SHORT).show(); return; } // 确保数据库可用 if (db == null || !db.isOpen()) { initDb(); if (db == null || !db.isOpen()) { Toast.makeText(this, "数据库未准备好", Toast.LENGTH_SHORT).show(); return; } } try { // 格式化当前时间 Date date = new Date(); SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm", Locale.getDefault()); String mtime = sdf.format(date); // 插入主备忘录数据 ContentValues memoryValues = new ContentValues(); memoryValues.put("memo_id", uniqueMemoId); memoryValues.put("title", title); memoryValues.put("content", content); memoryValues.put("location", location); memoryValues.put("mtime", mtime); long insertResult = db.insert("tb_memory", null, memoryValues); if (insertResult != -1) { // 插入关联图片 for (String imagePath : imagePaths) { ContentValues imageValues = new ContentValues(); imageValues.put("memo_id", uniqueMemoId); imageValues.put("imgpath", imagePath); db.insert("tb_image", null, imageValues); } Toast.makeText(this, "保存成功,包含 " + imagePaths.size() + " 张图片", Toast.LENGTH_SHORT).show(); // 延迟1秒后返回主页面 handler.postDelayed(() -> { startActivity(new Intent(AddInfoActivity.this, MainActivity.class)); finish(); }, 1000); } else { Toast.makeText(this, "保存失败,请重试", Toast.LENGTH_SHORT).show(); } } catch (Exception e) { Log.e("SaveError", "保存失败", e); Toast.makeText(this, "保存失败: " + e.getMessage(), Toast.LENGTH_LONG).show(); } finally { // 确保关闭数据库 if (db != null && db.isOpen()) { try { db.close(); } catch (Exception e) { Log.e("DbClose", "数据库关闭失败", e); } } } } @Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == CAMERA_PERMISSION_CODE) { boolean allGranted = true; for (int result : grantResults) { if (result != PackageManager.PERMISSION_GRANTED) { allGranted = false; break; } } if (allGranted) { openCamera(); } else { Toast.makeText(this, "需要所有权限才能使用相机", Toast.LENGTH_SHORT).show(); } } else if (requestCode == GALLERY_PERMISSION_CODE) { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { openGallery(); } else { Toast.makeText(this, "需要权限才能访问相册", Toast.LENGTH_SHORT).show(); } } } @Override public void onBackPressed() { super.onBackPressed(); goBackToMain(); } @Override protected void onDestroy() { super.onDestroy(); // 清理资源 if (db != null && db.isOpen()) { try { db.close(); } catch (Exception e) { Log.e("DestroyError", "数据库关闭失败", e); } } } private void goBackToMain() { startActivity(new Intent(AddInfoActivity.this, MainActivity.class)); finish(); } } // 文件路径:com/example/labmemotest/SetupCustomIdActivity.java 用途:让用户首次或随时设置自定义编号组(最多6组,每组0-9项) package com.example.labmemotest; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.os.Bundle; import android.text.TextUtils; import android.view.View; import android.widget.Button; import android.widget.EditText; import android.widget.LinearLayout; import android.widget.Toast; import androidx.appcompat.app.AppCompatActivity; import com.example.labmemotest.db.labDbHelper; public class SetupCustomIdActivity extends AppCompatActivity { private EditText[][] inputs = new EditText[6][10]; private Button btnSave; private labDbHelper dbHelper; private SQLiteDatabase db; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_setup_custom_id); dbHelper = new labDbHelper(this); db = dbHelper.getWritableDatabase(); loadInputFields(); btnSave = findViewById(R.id.btn_save_config); btnSave.setOnClickListener(v -> saveConfiguration()); } private void loadInputFields() { int[] groupLayoutIds = { R.id.group1, R.id.group2, R.id.group3, R.id.group4, R.id.group5, R.id.group6 }; for (int g = 0; g < 6; g++) { LinearLayout groupLayout = findViewById(groupLayoutIds[g]); for (int i = 0; i < 10; i++) { EditText et = new EditText(this); et.setHint("选项 " + i); et.setTextSize(14); et.setPadding(8, 8, 8, 8); et.setTag("input_" + g + "_" + i); groupLayout.addView(et); inputs[g][i] = et; } } } private void saveConfiguration() { String userId = getCurrentUserId(); // 如从 SharedPreferences 获取 if (TextUtils.isEmpty(userId)) userId = "default_user"; db.delete("tb_custom_config", "user_id=?", new String[]{userId}); boolean hasValidGroup = false; for (int g = 0; g < 6; g++) { boolean hasAny = false; for (int i = 0; i < 10; i++) { String text = inputs[g][i].getText().toString().trim(); if (!text.isEmpty()) { String codeChar = String.valueOf((char) ('A' + (g * 10 + i) % 26)); // 自动分配字母 insertOption(userId, g + 1, i, text, codeChar); hasAny = true; } } if (hasAny) hasValidGroup = true; } if (!hasValidGroup) { Toast.makeText(this, "至少填写一组编号!", Toast.LENGTH_SHORT).show(); return; } Toast.makeText(this, "配置保存成功!", Toast.LENGTH_SHORT).show(); finish(); } private void insertOption(String userId, int groupId, int optionIndex, String displayText, String codeChar) { db.execSQL("INSERT INTO tb_custom_config(user_id,group_id,option_index,display_text,code_char) VALUES(?,?,?,?,?)", new Object[]{userId, groupId, optionIndex, displayText, codeChar}); } private String getCurrentUserId() { // TODO: 可从 SharedPreferences 或登录状态获取真实用户ID return "USER001"; } @Override protected void onDestroy() { super.onDestroy(); if (db != null && db.isOpen()) db.close(); if (dbHelper != null) dbHelper.close(); } } package com.example.labmemotest; import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.CheckBox; import android.widget.EditText; import android.widget.Toast; import androidx.appcompat.app.AppCompatActivity; //登录页面注册页面 public class LoginActivity extends AppCompatActivity { // 定义对象 private EditText edit_inputname, edit_inputpwd; private CheckBox check_reme; private Button btn_login, btn_cancel, btn_register; // 新增:取消、注册按钮 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_login); // 1. 绑定控件(新增取消、注册按钮) initView(); // 新增:检查注册状态,控制注册按钮显示/隐藏 checkRegistrationStatus(); // 2. 绑定按钮点击事件(新增取消、注册按钮逻辑) initClick(); // 3. 下次启动,直接显示记住的账号(原逻辑保留,修改为“显示账号”) displayRememberedAccount(); } // 1. 绑定控件(新增取消、注册按钮) private void initView() { edit_inputname = findViewById(R.id.editText_inputname); edit_inputpwd = findViewById(R.id.editText_inputpwd); check_reme = findViewById(R.id.checkBox_reme); btn_login = findViewById(R.id.button_login); btn_cancel = findViewById(R.id.button_cancel); // 绑定取消按钮 btn_register = findViewById(R.id.button_register); // 绑定注册按钮 } // 新增:检查注册状态,控制注册按钮显示/隐藏 private void checkRegistrationStatus() { SharedPreferences sp = getSharedPreferences("UserData", MODE_PRIVATE); // 判断是否已有用户注册 boolean hasRegistered = sp.getBoolean("HAS_REGISTERED", false); if (hasRegistered) { // 已有用户注册,隐藏注册按钮 btn_register.setVisibility(View.GONE); } else { // 未注册,显示注册按钮 btn_register.setVisibility(View.VISIBLE); } } // 2. 绑定按钮点击事件(整合原有登录逻辑,新增取消、注册) private void initClick() { // 取消按钮:清空输入 btn_cancel.setOnClickListener(v -> { edit_inputname.setText(""); edit_inputpwd.setText(""); }); // 注册按钮:跳转到注册页 btn_register.setOnClickListener(v -> { Intent intent = new Intent(LoginActivity.this, RegisterActivity.class); startActivity(intent); }); // 原有登录按钮逻辑:新增密码验证 btn_login.setOnClickListener(v -> { String username = edit_inputname.getText().toString().trim(); String inputPwd = edit_inputpwd.getText().toString().trim(); // 1. 校验输入不为空 if (username.isEmpty()) { Toast.makeText(LoginActivity.this, "请输入账号", Toast.LENGTH_SHORT).show(); return; } if (inputPwd.isEmpty()) { Toast.makeText(LoginActivity.this, "请输入密码", Toast.LENGTH_SHORT).show(); return; } // 2. 从SharedPreferences读取已注册的密码 SharedPreferences sp = getSharedPreferences("UserData", MODE_PRIVATE); String savedPwd = sp.getString("USER_" + username, null); // 3. 验证账号是否存在 if (savedPwd == null) { Toast.makeText(LoginActivity.this, "该账号未注册", Toast.LENGTH_SHORT).show(); return; } // 4. 验证密码是否正确 if (!savedPwd.equals(inputPwd)) { Toast.makeText(LoginActivity.this, "密码不正确,无法登录", Toast.LENGTH_SHORT).show(); edit_inputpwd.setText(""); // 清空错误密码 return; } // 5. 登录成功:保存“记住账号”状态 SharedPreferences.Editor editor = sp.edit(); editor.putBoolean("REMEMBER_ACCOUNT", check_reme.isChecked()); // 是否记住账号 editor.putString("LAST_ACCOUNT", username); // 记录上次登录的账号 editor.apply(); // 跳转到MainActivity Intent intent = new Intent(LoginActivity.this, MainActivity.class); startActivity(intent); finish(); // 关闭登录页,避免返回 }); } // 下次启动,直接显示记住的账号(不自动填充密码) private void displayRememberedAccount() { SharedPreferences sp = getSharedPreferences("UserData", MODE_PRIVATE); boolean isRemember = sp.getBoolean("REMEMBER_ACCOUNT", false); String lastAccount = sp.getString("LAST_ACCOUNT", ""); if (isRemember && !lastAccount.isEmpty()) { edit_inputname.setText(lastAccount); // 只填充账号 check_reme.setChecked(true); } else { edit_inputname.setText(""); check_reme.setChecked(false); } edit_inputpwd.setText(""); // 密码每次都清空,需用户输入 } // 从注册页面返回时重新检查注册状态 @Override protected void onResume() { super.onResume(); checkRegistrationStatus(); } } package com.example.labmemotest.db; import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.util.Log; //数据库 public class labDbHelper extends SQLiteOpenHelper { // 定义数据库名、版本号 private static final String DBNAME = "zsmemo.db"; private static final int VERSION = 3; // 版本号从2升级到3,支持多图存储 private static final String TAG = "labDbHelper"; public labDbHelper(Context context) { super(context, DBNAME, null, VERSION); } // 创建数据库(仅首次安装时执行) @Override public void onCreate(SQLiteDatabase db) { // 启用外键约束(关键:SQLite默认关闭外键约束) db.execSQL("PRAGMA foreign_keys = ON;"); // 1. 日志表(移除imgpath字段) String createMemoryTable = "CREATE TABLE tb_memory (" + "_id INTEGER PRIMARY KEY AUTOINCREMENT, " + "memo_id TEXT unique," + "title TEXT, " + "content TEXT, " + "location TEXT," + "mtime TEXT)"; // 已移除原imgpath字段 // 2. 新增图片表(存储多图,关联日志表的memo_id) String createImageTable = "CREATE TABLE tb_image (" + "_id INTEGER PRIMARY KEY AUTOINCREMENT, " + "memo_id TEXT, " + "imgpath TEXT, " + "FOREIGN KEY(memo_id) REFERENCES tb_memory(memo_id))"; // 在 onCreate 方法内追加:添加新表 TB_CUSTOM_CONFIG用于储存自定义数据与编号 String createConfigTable = "CREATE TABLE tb_custom_config (" + "user_id TEXT NOT NULL," + "group_id INTEGER NOT NULL," + "option_index INTEGER NOT NULL," + "display_text TEXT," + "code_char TEXT," + "PRIMARY KEY (user_id, group_id, option_index)" + ")"; db.execSQL(createConfigTable); try { db.execSQL(createMemoryTable); db.execSQL(createImageTable); Log.d(TAG, "日志表和图片表创建成功"); } catch (Exception e) { Log.e(TAG, "创建表失败:" + e.getMessage(), e); // 移除强制崩溃的代码,改为友好处理 Log.e(TAG, "数据库初始化失败,应用可能无法正常工作", e); } } // 升级数据库(版本号提升时执行) @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // 升级时启用外键约束 db.execSQL("PRAGMA foreign_keys = ON;"); // 处理版本2升级到3的情况(支持多图存储) if (oldVersion < 4) { try { // 兼容低版本SQLite,使用临时表迁移方式移除imgpath字段 // 1. 创建临时表(不含imgpath字段) db.execSQL("CREATE TABLE tb_memory_temp (" + "_id INTEGER PRIMARY KEY AUTOINCREMENT, " + "memo_id TEXT unique," + "title TEXT, " + "content TEXT, " + "location TEXT," + "mtime TEXT)"); // 2. 迁移旧表数据(排除imgpath字段) db.execSQL("INSERT INTO tb_memory_temp " + "(_id, memo_id, title, content, location, mtime) " + "SELECT _id, memo_id, title, content, location, mtime FROM tb_memory"); // 3. 删除旧表 db.execSQL("DROP TABLE tb_memory"); // 4. 重命名临时表为正式表 db.execSQL("ALTER TABLE tb_memory_temp RENAME TO tb_memory"); // 5. 创建图片表 db.execSQL("CREATE TABLE tb_image (" + "_id INTEGER PRIMARY KEY AUTOINCREMENT, " + "memo_id TEXT, " + "imgpath TEXT, " + "FOREIGN KEY(memo_id) REFERENCES tb_memory(memo_id))"); // 在 if(oldVersion < 3) 块中升级后添加:新建自定义数据与编号表 db.execSQL("CREATE TABLE IF NOT EXISTS tb_custom_config (...)"); Log.d(TAG, "数据库升级到版本3,支持多图存储"); } catch (Exception e) { Log.e(TAG, "数据库升级失败:" + e.getMessage(), e); } } } } // 文件路径:com/example/labmemotest/bean/CustomOption.java 用途:封装编号选项实体 package com.example.labmemotest.bean; public class CustomOption { public String userId; public int groupId; public int optionIndex; public String displayText; public String codeChar; public CustomOption(String userId, int groupId, int optionIndex, String displayText, String codeChar) { this.userId = userId; this.groupId = groupId; this.optionIndex = optionIndex; this.displayText = displayText; this.codeChar = codeChar; } @Override public String toString() { return optionIndex + ": " + displayText + " (" + codeChar + ")"; } } package com.example.labmemotest.adapter; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.graphics.Color; import android.graphics.drawable.GradientDrawable; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.animation.ScaleAnimation; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import androidx.appcompat.app.AlertDialog; import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; import com.example.labmemotest.DetailActivity; import com.example.labmemotest.R; import com.example.labmemotest.bean.MemoBean; import com.example.labmemotest.db.labDbHelper; import java.util.List; import java.util.Random; public class MemoAdapter extends RecyclerView.Adapter<MemoAdapter.ViewHolder> { private Context mcontext; private List<MemoBean> arr1; // 移除类级别的数据库变量,改为在使用时局部声明 private static final int MAX_TITLE_LENGTH = 10; private static final int MAX_CONTENT_LENGTH = 8; private static final int COLOR_MIN = 180; private static final int COLOR_MAX = 255; public MemoAdapter(Context mcontext, List<MemoBean> arr1) { this.mcontext = mcontext; this.arr1 = arr1; } @NonNull @Override public MemoAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(mcontext).inflate(R.layout.recy_item, parent, false); return new ViewHolder(view); } @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN) @Override public void onBindViewHolder(@NonNull MemoAdapter.ViewHolder holder, int position) { // 增加空数据判断,避免数组越界 if (arr1 == null || position >= arr1.size()) { return; } final MemoBean memoBean = arr1.get(position); // 防止memoBean为空导致的空指针 if (memoBean == null) { return; } // 显示编号(增加非空判断) holder.item_memo_id.setText("编号:" + (memoBean.getMemoId() != null ? memoBean.getMemoId() : "未知")); // 处理标题长度 String title = memoBean.getTitle(); if (title == null) title = ""; // 防止空指针 if (title.length() > MAX_TITLE_LENGTH) { title = title.substring(0, MAX_TITLE_LENGTH) + "..."; } holder.item_title.setText(title); // 处理内容显示 String content = memoBean.getContent(); if (content == null) content = ""; // 防止空指针 if (content.length() > MAX_CONTENT_LENGTH) { content = content.substring(0, MAX_CONTENT_LENGTH) + "..."; } holder.item_content.setText(content); // 处理时间显示 String time = memoBean.getTime(); holder.item_time.setText(time != null ? time : ""); // 处理位置信息 String location = memoBean.getLocation(); if (location != null && !location.isEmpty()) { holder.item_location.setText("位置:" + location); } else { holder.item_location.setText("地点:无"); } holder.item_location.setVisibility(View.VISIBLE); // 从tb_image表查询对应memo_id的第一张图片 // 使用局部变量处理数据库操作,避免多线程问题 labDbHelper dbHelper = new labDbHelper(mcontext); SQLiteDatabase db = null; Cursor cursor = null; try { db = dbHelper.getReadableDatabase(); // 确保memoId不为空 if (memoBean.getMemoId() != null) { cursor = db.query( "tb_image", new String[]{"imgpath"}, "memo_id = ?", new String[]{memoBean.getMemoId()}, null, null, null, "1" ); if (cursor != null && cursor.moveToFirst()) { String firstImgPath = cursor.getString(0); if (firstImgPath != null && !firstImgPath.isEmpty()) { // 加载图片 Glide.with(mcontext) .load(Uri.parse(firstImgPath)) .error(R.drawable.ic_default_image) .placeholder(R.drawable.ic_default_image) // 新增占位图 .into(holder.item_img); } else { holder.item_img.setImageResource(R.drawable.ic_default_image); } } else { holder.item_img.setImageResource(R.drawable.ic_default_image); } } else { holder.item_img.setImageResource(R.drawable.ic_default_image); } } catch (Exception e) { Log.e("MemoAdapter", "图片加载失败", e); holder.item_img.setImageResource(R.drawable.ic_default_image); } finally { // 确保资源关闭 if (cursor != null) { try { cursor.close(); } catch (Exception e) { Log.e("MemoAdapter", "关闭cursor失败", e); } } if (db != null && db.isOpen()) { try { db.close(); } catch (Exception e) { Log.e("MemoAdapter", "关闭数据库失败", e); } } } // 设置背景颜色和形状 Random random = new Random(); int red = COLOR_MIN + random.nextInt(COLOR_MAX - COLOR_MIN); int green = COLOR_MIN + random.nextInt(COLOR_MAX - COLOR_MIN); int blue = COLOR_MIN + random.nextInt(COLOR_MAX - COLOR_MIN); int color = Color.argb(200, red, green, blue); GradientDrawable gradientDrawable = new GradientDrawable(); gradientDrawable.setShape(GradientDrawable.RECTANGLE); gradientDrawable.setCornerRadius(10f); gradientDrawable.setColor(color); gradientDrawable.setStroke(2, Color.argb(50, 0, 0, 0)); holder.item_layout.setBackground(gradientDrawable); // 子项点击事件 - 跳转到详情页 holder.item_layout.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { ScaleAnimation scaleAnim = new ScaleAnimation( 1.0f, 0.95f, 1.0f, 0.95f, v.getWidth() / 2, v.getHeight() / 2); scaleAnim.setDuration(100); v.startAnimation(scaleAnim); Intent intent = new Intent(mcontext, DetailActivity.class); Bundle bundle = new Bundle(); bundle.putString("title", memoBean.getTitle()); bundle.putString("content", memoBean.getContent()); bundle.putString("time", memoBean.getTime()); bundle.putString("memo_id", memoBean.getMemoId()); bundle.putString("location", memoBean.getLocation()); intent.putExtras(bundle); mcontext.startActivity(intent); } }); // 长按事件 - 删除功能 holder.item_layout.setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(View view) { new AlertDialog.Builder(mcontext) .setMessage("确定删除吗?") .setPositiveButton("确定", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int which) { // 使用局部变量处理数据库 labDbHelper deleteHelper = new labDbHelper(mcontext); SQLiteDatabase deleteDb = null; try { deleteDb = deleteHelper.getWritableDatabase(); int pos = holder.getAdapterPosition(); if (pos != RecyclerView.NO_POSITION && arr1.get(pos) != null && arr1.get(pos).getMemoId() != null) { // 删除主表记录 deleteDb.delete("tb_memory", "memo_id = ?", new String[]{arr1.get(pos).getMemoId()}); // 删除关联图片 deleteDb.delete("tb_image", "memo_id = ?", new String[]{arr1.get(pos).getMemoId()}); // 更新列表 arr1.remove(pos); notifyItemRemoved(pos); notifyItemRangeChanged(pos, arr1.size()); } } catch (Exception e) { Log.e("MemoAdapter", "删除失败", e); } finally { if (deleteDb != null && deleteDb.isOpen()) { try { deleteDb.close(); } catch (Exception e) { Log.e("MemoAdapter", "关闭数据库失败", e); } } } dialogInterface.dismiss(); } }) .setNegativeButton("取消", null) .setCancelable(false) .show(); return true; } }); } @Override public int getItemCount() { return arr1 == null ? 0 : arr1.size(); // 防止空指针 } public boolean isEmpty() { return arr1 == null || arr1.isEmpty(); } public class ViewHolder extends RecyclerView.ViewHolder { TextView item_title, item_content, item_time, item_memo_id, item_location; ImageView item_img; LinearLayout item_layout; public ViewHolder(@NonNull View itemView) { super(itemView); // 增加控件绑定的空判断 item_memo_id = itemView.findViewById(R.id.item_memo_id); item_title = itemView.findViewById(R.id.item_title); item_content = itemView.findViewById(R.id.item_content); item_img = itemView.findViewById(R.id.item_image); item_location = itemView.findViewById(R.id.item_location); item_time = itemView.findViewById(R.id.item_time); item_layout = itemView.findViewById(R.id.item_layout); } } } package com.example.labmemotest.bean; public class MemoBean { private String memoId; // 9位随机编号(memo_id) private String title; // 备忘录标题 private String content; // 备忘录内容 private String location; // 备忘录地点 private String time; // 时间 // 构造方法(移除imgpath参数) public MemoBean(String title, String content, String time, String location, String memoId) { this.title = title; this.content = content; this.time = time; this.location = location; this.memoId = memoId; } // Getter和Setter方法(移除imgpath相关) public String getMemoId() { return memoId; } public void setMemoId(String memoId) { this.memoId = memoId; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } public String getTime() { return time; } public void setTime(String time) { this.time = time; } public String getLocation() { return location; } public void setLocation(String location) { this.location = location; } } <?xml version = "1.0" encoding = "utf-8"?> <LinearLayout xmlns:android = "http://schemas.android.com/apk/res/android" xmlns:tools = "http://schemas.android.com/tools" android:layout_width = "match_parent" android:layout_height = "match_parent" android:orientation = "vertical" android:background = "@drawable/bgthree" android:padding = "15dp" tools:context = ".AddInfoActivity"> <LinearLayout android:id="@+id/layout_id_selection" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="12dp" android:padding="5dp" android:background="@drawable/edittext_bg" android:orientation="vertical"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="选择编号前缀" android:textSize="12sp" android:textStyle="bold" android:textColor="#999999" android:layout_marginBottom="6dp" /> <LinearLayout android:id="@+id/container_prefix_groups" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" /> </LinearLayout> <EditText android:id = "@+id/editText_title" android:layout_width = "match_parent" android:layout_height = "60dp" android:layout_marginBottom = "12dp" android:hint = "标题" android:textStyle = "bold" android:textColor = "#000000" android:textSize = "22sp" android:background = "@drawable/edittext_bg" android:paddingHorizontal = "12dp" android:paddingVertical = "8dp" android:maxLines = "1" android:singleLine = "true"/> <EditText android:id = "@+id/editText_content" android:layout_width = "match_parent" android:layout_height = "0dp" android:layout_weight = "1" android:layout_marginBottom = "12dp" android:hint = "内容" android:textStyle = "bold" android:textColor = "#000000" android:textSize = "18sp" android:gravity = "top" android:background = "@drawable/edittext_bg" android:paddingHorizontal = "12dp" android:paddingVertical = "10dp" android:minLines = "3" /> <EditText android:id = "@+id/editText_location" android:layout_width = "match_parent" android:layout_height = "50dp" android:layout_marginBottom = "12dp" android:hint = "地点(可选)尽可能详细" android:textStyle = "bold" android:textColor = "#000000" android:textSize = "18sp" android:background = "@drawable/edittext_bg" android:paddingHorizontal = "12dp" android:paddingVertical = "8dp" android:maxLines = "1" android:singleLine = "true" android:inputType = "textPostalAddress" /> <TextView android:layout_width = "match_parent" android:layout_height = "wrap_content" android:layout_marginBottom = "8dp" android:text = "请选用下面的任一种方式,添加图片:" android:textStyle = "bold" android:textColor = "#333333" android:textSize = "16sp" android:gravity = "start" /> <LinearLayout android:layout_width = "match_parent" android:layout_height = "wrap_content" android:layout_marginBottom = "12dp" android:orientation = "horizontal" android:gravity = "center"> <Button android:id = "@+id/button_camera" android:layout_width = "0dp" android:layout_height = "50dp" android:layout_weight = "1" android:layout_marginRight = "10dp" android:text = "拍照" android:textStyle = "bold" android:background = "@drawable/btnstyle" android:textColor = "#000000" android:textSize = "16sp" android:paddingHorizontal = "8dp" /> <Button android:id = "@+id/button_photo" android:layout_width = "0dp" android:layout_height = "50dp" android:layout_weight = "1" android:layout_marginLeft = "10dp" android:text = "从图库中选择" android:textStyle = "bold" android:background = "@drawable/btnstyle" android:textColor = "#000000" android:textSize = "16sp" android:paddingHorizontal = "8dp" /> </LinearLayout> <!-- 移除原ImageView_preview,替换为水平滚动的预览容器 --> <HorizontalScrollView android:layout_width="match_parent" android:layout_height="180dp" android:layout_marginBottom="15dp"> <LinearLayout android:id="@+id/image_preview_container" android:layout_width="wrap_content" android:layout_height="match_parent" android:orientation="horizontal"/> </HorizontalScrollView> <!--<ImageView--> <!--android:id = "@+id/imageView_preview"--> <!--android:layout_width = "match_parent"--> <!-- android:layout_height = "180dp"--> <!-- android:layout_marginBottom = "15dp"--> <!-- android:src = "@drawable/sunshine"--> <!-- android:scaleType = "centerCrop"--> <!-- android:background = "#F5F5F5" />--> <Button android:id = "@+id/button_save" android:layout_width = "match_parent" android:layout_height = "60dp" android:text = "保存" android:textStyle = "bold" android:background = "@drawable/btnstyle" android:textColor = "#000000" android:textSize = "20sp" android:paddingHorizontal = "8dp" /> </LinearLayout> <?xml version="1.0" encoding="utf-8"?> <ScrollView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="16dp"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="设置自定义编号(最多6组)" android:textSize="18sp" android:textStyle="bold" android:gravity="center" android:padding="16dp" /> <LinearLayout android:id="@+id/group1" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:layout_marginBottom="16dp"/> <LinearLayout android:id="@+id/group2" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:layout_marginBottom="16dp"/> <LinearLayout android:id="@+id/group3" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:layout_marginBottom="16dp"/> <LinearLayout android:id="@+id/group4" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:layout_marginBottom="16dp"/> <LinearLayout android:id="@+id/group5" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:layout_marginBottom="16dp"/> <LinearLayout android:id="@+id/group6" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:layout_marginBottom="16dp"/> <Button android:id="@+id/btn_save_config" android:layout_width="match_parent" android:layout_height="60dp" android:text="保存配置" android:textSize="18sp" android:background="@drawable/btnstyle" android:textColor="#000000" /> </LinearLayout> </ScrollView> 根据以上内容,在不改变原有代码的基础上,修改完善,按照以下要求:我想重新设计一下在创建日志时生成组合编号,要求是:多组自定义编号(也就是设置几个多选,每个多选有10个选项,每个选项对应不同的内容和编号,内容是用户自定义的)+随机生成5位数编号共同组成一个memo_id。比如:我在第1次注册账号之后进入一个自定义编号的页面,设置6组自定义编号,供用户选择1组到6组均可;每组编号设置10个输入框,每个输入框对应0-9,用户可以根据自己的需要进行填写内容进行定义,填写多少个输入框就用多少个(最少填写1个,最多10个);用户最少填写1组自定义编号,最多填写6组;填写完成后点击保存后退出该页面。在创建日志时,生成的编号包括用户写日志时选择的选项加上5位随机数组合成一个完整的编号。(在main页面添加一个按钮原来跳转进入到自定义编号页面进行编辑操作,在日志创建页面只提供已经编辑好的多个选项框,用户设置了多少组自定义编号,就提供多少个选项框)
09-29
import sqlite3 from dataclasses import dataclass from datetime import datetime @dataclass class Employee: id: str # 工号 name: str # 姓名 avatar_path: str # 头像路径 register_time: str # 注册时间 class EmployeeDB: def __init__(self, db_path="employees.db"): self.conn = sqlite3.connect(db_path) self._create_table() def _create_table(self): cursor = self.conn.cursor() cursor.execute(''' CREATE TABLE IF NOT EXISTS employees ( id TEXT PRIMARY KEY, name TEXT NOT NULL, avatar_path TEXT NOT NULL, register_time TEXT NOT NULL ) ''') self.conn.commit() def add_employee(self, employee: Employee): cursor = self.conn.cursor() cursor.execute(''' INSERT INTO employees VALUES (?, ?, ?, ?) ''', (employee.id, employee.name, employee.avatar_path, employee.register_time)) self.conn.commit() def get_employee(self, employee_id: str) -> Employee: cursor = self.conn.cursor() cursor.execute('SELECT * FROM employees WHERE id = ?', (employee_id,)) result = cursor.fetchone() if result: return Employee(*result) return None def get_all_employees(self) -> list[Employee]: cursor = self.conn.cursor() cursor.execute('SELECT * FROM employees') return [Employee(*row) for row in cursor.fetchall()] def close(self): self.conn.close()——这个是前端目前已确定好的初步框架,目前已确定前端和后端会在同一台主机上运行,不需要再进行ping是否连通等测试,以这个框架为基础,对原来的前端部分的module进行修改,同样写成一个个函数并封装成一个module供外部调用,输入包括其他模块给的姓名、工号、图片、打卡事件会触发的打卡时间,功能包括则增加员工信息、删除员工信息、更新员工信息、查询某员工信息(返回包括该员工姓名、工号、图片有无、status、record_count,最好作为可以外部引用的变量>>可以为查询专门定义数组存储查询后的个人信息)、查询所有员工信息、获取员工图像(返回base64编码),以下为原本的前端数据库模块:mport requests import base64 import os from datetime import datetime class DatabaseService: def __init__(self, base_url="http://10.160.64.218:5606"): """ 初始化数据库服务 :param base_url: 后端API基础URL """ self.base_url = base_url # def _request(self, endpoint, method, payload=None, files=None): # """ # 通用请求方法 # :param endpoint: API端点 (如 '/employee/add') # :param method: HTTP方法 ('GET', 'POST', 'DELETE') # :param payload: JSON格式的请求数据 # :param files: 文件上传数据 # :return: 响应数据字典 # """ # url = f"{self.base_url}{endpoint}" # headers = {'Content-Type': 'application/json'} # try: # if method == 'GET': # response = requests.get(url, params=payload) # elif method == 'POST': # response = requests.post(url, json=payload, files=files, headers=headers) # elif method == 'DELETE': # response = requests.delete(url, json=payload) # else: # raise ValueError(f"不支持的HTTP方法: {method}") # response.raise_for_status() # 检查HTTP错误 # return response.json() # except requests.exceptions.HTTPError as e: # error_msg = f"HTTP错误 ({e.response.status_code}): {e.response.text}" # print(f"请求失败: {url}\n{error_msg}") # return {"code": e.response.status_code, "msg": error_msg, "data": None} # except Exception as e: # error_msg = f"请求异常: {str(e)}" # print(f" 请求失败: {url}\n{error_msg}") # return {"code": 500, "msg": error_msg, "data": None} # ------------------ 员工管理接口 ------------------ def add_employee(self, data, image_path=None): """ 添加员工 :param data: 员工数据字典 {emp_id, name} :param image_path: 员工照片路径 (可选) :return: 操作结果 """ payload = data.copy() # 处理图像上传 files = None if image_path and os.path.exists(image_path): try: with open(image_path, 'rb') as img_file: image_data = base64.b64encode(img_file.read()).decode('utf-8') payload['image'] = image_data except Exception as e: print(f"图像处理失败: {str(e)}") return self._request('/employee/add', 'POST', payload) def get_all_employees(self): """获取所有员工列表""" return self._request('/employee/list', 'GET') def get_employee_details(self, emp_id): """获取员工详细信息及打卡记录""" return self._request(f'/employee/{emp_id}', 'GET') def update_employee(self, emp_id, update_data, new_image_path=None): """ 更新员工信息 :param emp_id: 员工ID :param update_data: 更新数据字典 {name?, status?} :param new_image_path: 新照片路径 (可选) :return: 操作结果 """ payload = {'emp_id': emp_id, **update_data} # 处理图像更新 if new_image_path and os.path.exists(new_image_path): try: with open(new_image_path, 'rb') as img_file: image_data = base64.b64encode(img_file.read()).decode('utf-8') payload['image'] = image_data except Exception as e: print(f"图像处理失败: {str(e)}") return self._request('/employee/update', 'POST', payload) def delete_employee(self, emp_id): """删除员工及其打卡记录""" return self._request(f'/employee/delete/{emp_id}', 'DELETE') # ------------------ 打卡管理接口 ------------------ def clock_in(self, emp_id): """员工打卡""" return self._request('/employee/clock_in', 'POST', {'emp_id': emp_id}) def get_employee_image(self, emp_id, save_path=None): """ 获取员工照片 :param emp_id: 员工ID :param save_path: 保存路径 (可选) :return: 图像数据或保存状态 """ try: response = requests.get(f"{self.base_url}/employee/image/{emp_id}") response.raise_for_status() # 保存到文件 if save_path: with open(save_path, 'wb') as f: f.write(response.content) return {"code": 200, "msg": f"图像已保存至 {save_path}", "data": save_path} # 返回二进制数据 return {"code": 200, "msg": "成功", "data": response.content} except requests.exceptions.HTTPError as e: error_msg = f"获取图像失败: {e.response.status_code} {e.response.text}" return {"code": e.response.status_code, "msg": error_msg, "data": None} except Exception as e: return {"code": 500, "msg": str(e), "data": None} # ------------------ 实用功能 ------------------ def reset_daily_status(self): """手动触发每日状态重置(仅用于测试)""" return self._request('/reset_daily_status', 'POST') def server_info(self): """获取服务器信息""" return self._request('/ping', 'GET') # 单例模式 - 全局数据库服务实例 db_service = DatabaseService()
08-22
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值