master
刘骐瑞 6 months ago
parent d5fe0481ac
commit 4c7a4236c0

@ -54,6 +54,12 @@ dependencies {
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
//gson
implementation 'com.google.code.gson:gson:2.10.1'
//
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.squareup.okhttp3:okhttp:4.10.0'
implementation 'com.squareup.okhttp3:logging-interceptor:4.10.0'
//
implementation project(':mainnavigatetabbar')

@ -2,7 +2,14 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.startsmake.llrisetabbardemo">
<!-- 添加相机权限 -->
<!-- 适配Android 13+需要的通知权限 -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- 网络权限 -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- 相机权限 -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

@ -1,84 +0,0 @@
package com.startsmake.llrisetabbardemo.adapter;
import android.content.Context;
import android.net.Uri;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import com.bumptech.glide.Glide;
import com.startsmake.llrisetabbardemo.R;
import java.util.List;
public class ImageAdapter extends BaseAdapter {
private Context context;
private List<Uri> imageUris;
private static final int MAX_IMAGES = 9;
public ImageAdapter(Context context, List<Uri> imageUris) {
this.context = context;
this.imageUris = imageUris;
}
@Override
public int getCount() {
return Math.min(imageUris.size() + 1, MAX_IMAGES);
}
@Override
public Object getItem(int position) {
if (position < imageUris.size()) {
return imageUris.get(position);
}
return null;
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if (convertView == null) {
convertView = LayoutInflater.from(context).inflate(R.layout.item_image, parent, false);
holder = new ViewHolder();
holder.imageView = convertView.findViewById(R.id.imageView);
holder.deleteButton = convertView.findViewById(R.id.btnDelete);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
if (position < imageUris.size()) {
// 显示已选择的图片
Uri imageUri = imageUris.get(position);
Glide.with(context)
.load(imageUri)
.placeholder(android.R.drawable.ic_menu_gallery) // 使用系统图标作为占位符
.into(holder.imageView);
holder.deleteButton.setVisibility(View.VISIBLE);
holder.deleteButton.setOnClickListener(v -> {
imageUris.remove(position);
notifyDataSetChanged();
});
} else {
// 显示添加按钮
holder.imageView.setImageResource(android.R.drawable.ic_input_add); // 使用系统图标
holder.deleteButton.setVisibility(View.GONE);
}
return convertView;
}
static class ViewHolder {
ImageView imageView;
ImageView deleteButton;
}
}

@ -3,13 +3,16 @@ package com.startsmake.llrisetabbardemo.activity;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.startsmake.llrisetabbardemo.R;
import com.startsmake.llrisetabbardemo.adapter.SearchAdapter;
import manager.DataManager;
import com.startsmake.llrisetabbardemo.model.Product;
import com.startsmake.llrisetabbardemo.model.Item;
import java.util.ArrayList;
import java.util.List;
@ -53,7 +56,7 @@ public class SearchResultsActivity extends AppCompatActivity {
String query = getIntent().getStringExtra("search_query");
if ("image".equals(searchType)) {
// 图片搜索结果
// 图片搜索结果(保持原逻辑,因为图片搜索是本地实现的)
List<Product> similarProducts = (List<Product>) getIntent().getSerializableExtra("similar_products");
if (similarProducts != null && !similarProducts.isEmpty()) {
searchQueryText.setText("图片搜索结果");
@ -66,20 +69,58 @@ public class SearchResultsActivity extends AppCompatActivity {
resultsRecyclerView.setVisibility(View.GONE);
}
} else {
// 文本搜索结果(原有逻辑)
// 文本搜索结果 - 使用DataManager的API搜索功能
if (query != null) {
searchQueryText.setText("搜索结果: " + query);
List<Product> searchResults = searchProducts(query);
if (searchResults.isEmpty()) {
findViewById(R.id.no_results_text).setVisibility(View.VISIBLE);
resultsRecyclerView.setVisibility(View.GONE);
} else {
findViewById(R.id.no_results_text).setVisibility(View.GONE);
resultsRecyclerView.setVisibility(View.VISIBLE);
searchAdapter.updateData(searchResults);
}
// 显示加载状态如果UI中有加载指示器
findViewById(R.id.no_results_text).setVisibility(View.GONE);
resultsRecyclerView.setVisibility(View.GONE);
// 使用DataManager从API获取搜索结果
DataManager.getInstance().searchItemsFromApi(query, new DataManager.OnDataLoadedListener() {
@Override
public void onDataLoaded(List<Item> items) {
if (items != null && !items.isEmpty()) {
// 转换Item列表为Product列表
List<Product> products = new ArrayList<>();
for (Item item : items) {
Product product = new Product();
product.setName(item.getTitle());
product.setDescription(item.getDescription());
product.setPrice(item.getPrice());
product.setCategory(item.getCategory());
// 获取第一张图片URL如果有
if (!item.getImageUrls().isEmpty()) {
product.setImageUrl(item.getImageUrls().get(0));
}
products.add(product);
}
searchAdapter.updateData(products);
findViewById(R.id.no_results_text).setVisibility(View.GONE);
resultsRecyclerView.setVisibility(View.VISIBLE);
} else {
findViewById(R.id.no_results_text).setVisibility(View.VISIBLE);
resultsRecyclerView.setVisibility(View.GONE);
}
}
@Override
public void onError(String errorMessage) {
// 网络请求失败时,尝试使用本地搜索
Toast.makeText(SearchResultsActivity.this, "网络请求失败,使用本地搜索", Toast.LENGTH_SHORT).show();
List<Product> localResults = searchProducts(query);
if (localResults.isEmpty()) {
findViewById(R.id.no_results_text).setVisibility(View.VISIBLE);
resultsRecyclerView.setVisibility(View.GONE);
} else {
searchAdapter.updateData(localResults);
findViewById(R.id.no_results_text).setVisibility(View.GONE);
resultsRecyclerView.setVisibility(View.VISIBLE);
}
}
});
} else {
// 处理没有查询参数的情况
searchQueryText.setText("搜索结果");

@ -0,0 +1,29 @@
package com.startsmake.llrisetabbardemo.api;
import okhttp3.OkHttpClient;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
import java.util.concurrent.TimeUnit;
public class ApiClient {
private static final String BASE_URL = "http://localhost:8080/";
private static Retrofit retrofit = null;
public static Retrofit getClient() {
if (retrofit == null) {
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build();
retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build();
}
return retrofit;
}
}

@ -0,0 +1,43 @@
package com.startsmake.llrisetabbardemo.api;
import com.startsmake.llrisetabbardemo.api.response.BaseResponse;
import com.startsmake.llrisetabbardemo.api.response.ProductResponse;
import com.startsmake.llrisetabbardemo.api.response.UserResponse;
import java.util.List;
import retrofit2.Call;
import retrofit2.http.Field;
import retrofit2.http.FormUrlEncoded;
import retrofit2.http.GET;
import retrofit2.http.POST;
import retrofit2.http.Query;
public interface ApiService {
// 获取API状态
@GET("api")
Call<BaseResponse> getApiStatus();
// 用户登录
@FormUrlEncoded
@POST("api/login")
Call<UserResponse> login(@Field("phone") String phone, @Field("password") String password);
// 用户注册
@FormUrlEncoded
@POST("api/register")
Call<UserResponse> register(@Field("phone") String phone, @Field("password") String password,
@Field("username") String username);
// 获取商品列表
@GET("api/products")
Call<BaseResponse<List<ProductResponse>>> getProducts();
// 搜索商品
@GET("api/products/search")
Call<BaseResponse<List<ProductResponse>>> searchProducts(@Query("keyword") String keyword);
// 获取商品详情
@GET("api/products/detail")
Call<BaseResponse<ProductResponse>> getProductDetail(@Query("id") String productId);
}

@ -0,0 +1,43 @@
package com.startsmake.llrisetabbardemo.api.response;
import com.google.gson.annotations.SerializedName;
public class BaseResponse<T> {
@SerializedName("status")
private String status;
@SerializedName("message")
private String message;
@SerializedName("data")
private T data;
// Getters and Setters
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public boolean isSuccess() {
return "success".equals(status);
}
}

@ -0,0 +1,107 @@
package com.startsmake.llrisetabbardemo.api.response;
import com.google.gson.annotations.SerializedName;
import java.util.List;
public class ProductResponse {
@SerializedName("id")
private String id;
@SerializedName("title")
private String title;
@SerializedName("description")
private String description;
@SerializedName("category")
private String category;
@SerializedName("price")
private double price;
@SerializedName("image_urls")
private List<String> imageUrls;
@SerializedName("contact")
private String contact;
@SerializedName("publish_time")
private long publishTime;
@SerializedName("seller_id")
private String sellerId;
// Getters and Setters
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getCategory() {
return category;
}
public void setCategory(String category) {
this.category = category;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
public List<String> getImageUrls() {
return imageUrls;
}
public void setImageUrls(List<String> imageUrls) {
this.imageUrls = imageUrls;
}
public String getContact() {
return contact;
}
public void setContact(String contact) {
this.contact = contact;
}
public long getPublishTime() {
return publishTime;
}
public void setPublishTime(long publishTime) {
this.publishTime = publishTime;
}
public String getSellerId() {
return sellerId;
}
public void setSellerId(String sellerId) {
this.sellerId = sellerId;
}
}

@ -0,0 +1,52 @@
package com.startsmake.llrisetabbardemo.api.response;
import com.google.gson.annotations.SerializedName;
public class UserResponse extends BaseResponse<UserResponse.UserInfo> {
public static class UserInfo {
@SerializedName("id")
private String id;
@SerializedName("username")
private String username;
@SerializedName("phone")
private String phone;
@SerializedName("token")
private String token;
// Getters and Setters
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
}
}

@ -165,14 +165,20 @@ public class PublishFragment extends Fragment {
if (getActivity() instanceof MainActivity) {
MainActivity mainActivity = (MainActivity) getActivity();
// 获取HomeFragment并添加新商品
HomeFragment homeFragment = mainActivity.getHomeFragment();
if (homeFragment != null) {
homeFragment.addNewItem(item);
}
// 返回首页
// 先切换到HomeFragment确保其完全初始化
mainActivity.switchToHomeFragment();
// 延迟一小段时间后再获取HomeFragment并添加新商品
// 这确保HomeFragment已经完全创建和显示
new android.os.Handler().postDelayed(new Runnable() {
@Override
public void run() {
HomeFragment homeFragment = mainActivity.getHomeFragment();
if (homeFragment != null) {
homeFragment.addNewItem(item);
}
}
}, 100);
}
} catch (NumberFormatException e) {

@ -2,23 +2,40 @@ package com.startsmake.llrisetabbardemo.manager;
import android.content.SharedPreferences;
import android.content.Context;
import android.util.Log;
import com.startsmake.llrisetabbardemo.api.ApiClient;
import com.startsmake.llrisetabbardemo.api.ApiService;
import com.startsmake.llrisetabbardemo.api.response.UserResponse;
import com.startsmake.llrisetabbardemo.model.User;
import com.google.gson.Gson;
import java.util.HashSet;
import java.util.Set;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class UserManager {
private static final String TAG = "UserManager";
private static final String PREF_NAME = "user_data";
private static final String KEY_USERS = "registered_users";
private static final String KEY_CURRENT_USER = "current_user";
private static final String KEY_USER_TOKEN = "user_token";
private static UserManager instance;
private SharedPreferences preferences;
private Gson gson;
private ApiService apiService;
// 认证回调接口
public interface AuthCallback {
void onSuccess(User user);
void onError(String message);
}
private UserManager(Context context) {
preferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
gson = new Gson();
apiService = ApiClient.getClient().create(ApiService.class);
}
public static synchronized UserManager getInstance(Context context) {
@ -50,7 +67,7 @@ public class UserManager {
}
/**
*
*
*/
public boolean registerUser(String phone, String password) {
// 检查手机号是否已注册
@ -68,9 +85,73 @@ public class UserManager {
return preferences.edit().putStringSet(KEY_USERS, newUsersSet).commit();
}
/**
* API
*/
public void registerUserWithApi(String phone, String password, String username, AuthCallback callback) {
apiService.register(phone, password, username).enqueue(new Callback<UserResponse>() {
@Override
public void onResponse(Call<UserResponse> call, Response<UserResponse> response) {
if (response.isSuccessful() && response.body() != null && response.body().isSuccess()) {
UserResponse.UserInfo userInfo = response.body().getData();
if (userInfo != null) {
// 创建User对象
User user = new User(userInfo.getPhone(), password);
// 设置用户名如果API返回了不同的用户名
user.setUsername(userInfo.getUsername());
// 保存token
saveUserToken(userInfo.getToken());
// 同时保存到本地(以便离线使用)
registerUser(phone, password);
saveCurrentUser(user);
Log.d(TAG, "用户注册成功: " + userInfo.getUsername());
if (callback != null) {
callback.onSuccess(user);
}
}
} else {
// API注册失败尝试本地注册
boolean localRegisterSuccess = registerUser(phone, password);
if (localRegisterSuccess) {
User user = new User(phone, password);
user.setUsername(username);
saveCurrentUser(user);
if (callback != null) {
callback.onSuccess(user);
}
} else {
if (callback != null) {
callback.onError("手机号已被注册");
}
}
}
}
@Override
public void onFailure(Call<UserResponse> call, Throwable t) {
Log.e(TAG, "网络请求失败: " + t.getMessage());
// 网络请求失败,尝试本地注册
boolean localRegisterSuccess = registerUser(phone, password);
if (localRegisterSuccess) {
User user = new User(phone, password);
user.setUsername(username);
saveCurrentUser(user);
if (callback != null) {
callback.onSuccess(user);
}
} else {
if (callback != null) {
callback.onError("网络连接失败,且本地注册失败");
}
}
}
});
}
/**
*
*
*/
public User loginUser(String phone, String password) {
Set<String> usersSet = preferences.getStringSet(KEY_USERS, new HashSet<>());
@ -85,6 +166,85 @@ public class UserManager {
}
return null;
}
/**
* API
*/
public void loginUserWithApi(String phone, String password, AuthCallback callback) {
apiService.login(phone, password).enqueue(new Callback<UserResponse>() {
@Override
public void onResponse(Call<UserResponse> call, Response<UserResponse> response) {
if (response.isSuccessful() && response.body() != null && response.body().isSuccess()) {
UserResponse.UserInfo userInfo = response.body().getData();
if (userInfo != null) {
// 创建User对象
User user = new User(userInfo.getPhone(), password);
// 设置用户名如果API返回了不同的用户名
user.setUsername(userInfo.getUsername());
// 保存token
saveUserToken(userInfo.getToken());
// 保存用户信息
saveCurrentUser(user);
Log.d(TAG, "用户登录成功: " + userInfo.getUsername());
if (callback != null) {
callback.onSuccess(user);
}
}
} else {
Log.e(TAG, "API登录失败回退到本地登录");
// API登录失败尝试本地登录
User localUser = loginUser(phone, password);
if (localUser != null) {
if (callback != null) {
callback.onSuccess(localUser);
}
} else {
if (callback != null) {
callback.onError("手机号或密码错误");
}
}
}
}
@Override
public void onFailure(Call<UserResponse> call, Throwable t) {
Log.e(TAG, "网络请求失败: " + t.getMessage());
// 网络请求失败,尝试本地登录
User localUser = loginUser(phone, password);
if (localUser != null) {
if (callback != null) {
callback.onSuccess(localUser);
}
} else {
if (callback != null) {
callback.onError("网络连接失败,且本地账号不存在");
}
}
}
});
}
/**
* token
*/
private void saveUserToken(String token) {
preferences.edit().putString(KEY_USER_TOKEN, token).apply();
}
/**
* token
*/
public String getUserToken() {
return preferences.getString(KEY_USER_TOKEN, null);
}
/**
* token
*/
public boolean hasValidToken() {
return getUserToken() != null;
}
/**
*
@ -152,7 +312,9 @@ public class UserManager {
.putBoolean("is_logged_in", false)
.remove("user_phone")
.remove("user_name")
.remove(KEY_USER_TOKEN) // 清除token
.apply();
Log.d(TAG, "用户已登出");
}
/**

@ -1,26 +1,49 @@
package manager;
import android.util.Log;
import com.startsmake.llrisetabbardemo.api.ApiClient;
import com.startsmake.llrisetabbardemo.api.ApiService;
import com.startsmake.llrisetabbardemo.api.response.BaseResponse;
import com.startsmake.llrisetabbardemo.api.response.ProductResponse;
import com.startsmake.llrisetabbardemo.model.Item;
import java.util.ArrayList;
import java.util.List;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
/**
* - 使
*
* API
*/
public class DataManager {
private static final String TAG = "DataManager";
// 单例实例
private static DataManager instance;
// 商品列表
private List<Item> itemList;
// API服务实例
private ApiService apiService;
// 回调接口用于通知UI数据加载状态
public interface OnDataLoadedListener {
void onDataLoaded(List<Item> items);
void onError(String message);
}
// 私有构造函数,防止外部创建实例
private DataManager() {
itemList = new ArrayList<>();
// 初始化时添加一些示例数据
// 初始化API服务
apiService = ApiClient.getClient().create(ApiService.class);
// 初始化时添加一些示例数据(作为备选)
initSampleData();
}
@ -89,12 +112,79 @@ public class DataManager {
}
/**
*
*
* @return
*/
public List<Item> getAllItems() {
return itemList;
}
/**
* API
* @param listener
*/
public void fetchItemsFromApi(OnDataLoadedListener listener) {
apiService.getProducts().enqueue(new Callback<BaseResponse<List<ProductResponse>>>() {
@Override
public void onResponse(Call<BaseResponse<List<ProductResponse>>> call,
Response<BaseResponse<List<ProductResponse>>> response) {
if (response.isSuccessful() && response.body() != null && response.body().isSuccess()) {
List<ProductResponse> productResponses = response.body().getData();
if (productResponses != null) {
// 转换为本地Item对象
List<Item> items = convertToItems(productResponses);
// 更新本地缓存
itemList = items;
Log.d(TAG, "从API成功获取商品数据共" + items.size() + "个商品");
// 回调成功
if (listener != null) {
listener.onDataLoaded(items);
}
} else {
// 返回空数据
if (listener != null) {
listener.onDataLoaded(new ArrayList<>());
}
}
} else {
Log.e(TAG, "API请求失败使用本地数据");
// 如果API请求失败使用本地缓存数据
if (listener != null) {
listener.onError("网络请求失败,显示本地数据");
}
}
}
@Override
public void onFailure(Call<BaseResponse<List<ProductResponse>>> call, Throwable t) {
Log.e(TAG, "网络请求失败: " + t.getMessage());
// 网络请求失败,使用本地缓存数据
if (listener != null) {
listener.onError("网络连接失败,请检查网络设置");
}
}
});
}
/**
* ProductResponseItem
*/
private List<Item> convertToItems(List<ProductResponse> productResponses) {
List<Item> items = new ArrayList<>();
for (ProductResponse response : productResponses) {
Item item = new Item();
item.setId(response.getId());
item.setTitle(response.getTitle());
item.setDescription(response.getDescription());
item.setCategory(response.getCategory());
item.setPrice(response.getPrice());
item.setImageUrls(response.getImageUrls());
item.setContact(response.getContact());
item.setPublishTime(response.getPublishTime());
items.add(item);
}
return items;
}
/**
*
@ -139,7 +229,7 @@ public class DataManager {
}
/**
*
*
* @param keyword
* @return
*/
@ -153,6 +243,49 @@ public class DataManager {
}
return result;
}
/**
* API
* @param keyword
* @param listener
*/
public void searchItemsFromApi(String keyword, OnDataLoadedListener listener) {
apiService.searchProducts(keyword).enqueue(new Callback<BaseResponse<List<ProductResponse>>>() {
@Override
public void onResponse(Call<BaseResponse<List<ProductResponse>>> call,
Response<BaseResponse<List<ProductResponse>>> response) {
if (response.isSuccessful() && response.body() != null && response.body().isSuccess()) {
List<ProductResponse> productResponses = response.body().getData();
if (productResponses != null) {
List<Item> items = convertToItems(productResponses);
if (listener != null) {
listener.onDataLoaded(items);
}
} else {
if (listener != null) {
listener.onDataLoaded(new ArrayList<>());
}
}
} else {
// 如果API搜索失败回退到本地搜索
List<Item> localResults = searchItems(keyword);
if (listener != null) {
listener.onDataLoaded(localResults);
}
}
}
@Override
public void onFailure(Call<BaseResponse<List<ProductResponse>>> call, Throwable t) {
Log.e(TAG, "搜索请求失败: " + t.getMessage());
// 网络请求失败,回退到本地搜索
List<Item> localResults = searchItems(keyword);
if (listener != null) {
listener.onDataLoaded(localResults);
}
}
});
}
/**
*

@ -19,7 +19,7 @@
android:layout_height="48dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_arrow_back"
android:tint="#333333" />
app:tint="#333333" />
<!-- 标题 -->
<TextView

@ -19,7 +19,7 @@
android:layout_height="48dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_arrow_back"
android:tint="#333333" />
app:tint="#333333" />
<!-- 标题 -->
<TextView

@ -0,0 +1,133 @@
# 二手市场后端项目
## 项目结构
```
backend/
├── src/
│ ├── main/
│ │ ├── java/com/example/secondhandmarket/
│ │ │ ├── SecondHandMarketApplication.java # 应用入口类
│ │ │ ├── model/entity/
│ │ │ │ ├── User.java # 用户实体类
│ │ │ │ ├── Item.java # 商品实体类
│ │ │ │ └── ChatMessage.java # 聊天消息实体类
│ │ │ ├── repository/
│ │ │ │ ├── UserRepository.java # 用户数据访问接口
│ │ │ │ ├── ItemRepository.java # 商品数据访问接口
│ │ │ │ └── ChatMessageRepository.java # 聊天消息数据访问接口
│ │ │ ├── service/
│ │ │ │ ├── UserService.java # 用户服务接口
│ │ │ │ ├── ItemService.java # 商品服务接口
│ │ │ │ ├── ChatMessageService.java # 聊天消息服务接口
│ │ │ │ └── impl/
│ │ │ │ ├── UserServiceImpl.java # 用户服务实现
│ │ │ │ ├── ItemServiceImpl.java # 商品服务实现
│ │ │ │ └── ChatMessageServiceImpl.java # 聊天消息服务实现
│ │ │ ├── controller/
│ │ │ │ ├── UserController.java # 用户控制器
│ │ │ │ ├── ItemController.java # 商品控制器
│ │ │ │ └── ChatMessageController.java # 聊天消息控制器
│ │ │ └── config/
│ │ │ ├── WebSecurityConfig.java # 安全配置
│ │ │ ├── GlobalExceptionHandler.java # 全局异常处理
│ │ │ └── CorsConfig.java # 跨域配置
│ │ └── resources/
│ │ └── application.properties # 应用配置文件
│ └── test/ # 测试目录
├── pom.xml # Maven配置文件
└── README.md # 项目说明文档
```
## 技术栈
- Spring Boot 2.7.15
- Spring Data JPA
- Spring Security
- MySQL数据库
- Java 8+
## 功能模块
1. **用户模块**
- 用户注册、登录
- 用户信息查询和更新
- 手机号验证
2. **商品模块**
- 商品发布、编辑、删除
- 商品分类浏览
- 关键词搜索
- 价格范围筛选
- 浏览次数统计和点赞功能
3. **聊天模块**
- 买卖双方实时聊天
- 未读消息管理
- 会话列表获取
## 如何运行
### 前提条件
- JDK 8+
- Maven 3.6+
- MySQL 5.7+
### 配置步骤
1. 修改 `application.properties` 文件中的数据库配置:
```properties
spring.datasource.url=jdbc:mysql://localhost:3306/secondhandmarket?useSSL=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=your_password
```
2. 创建数据库:
```sql
CREATE DATABASE secondhandmarket CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
```
3. 构建项目:
```bash
mvn clean install
```
4. 运行应用:
```bash
mvn spring-boot:run
```
或者直接运行生成的jar包
```bash
java -jar target/secondhandmarket-0.0.1-SNAPSHOT.jar
```
## API接口说明
### 用户相关接口
- POST /users/register - 用户注册
- POST /users/login - 用户登录
- GET /users/{id} - 获取用户信息
- PUT /users/{id} - 更新用户信息
- GET /users/exists/phone/{phone} - 检查手机号是否已注册
### 商品相关接口
- POST /items - 创建商品
- GET /items/{id} - 获取商品详情
- GET /items - 获取所有商品列表
- GET /items/category/{category} - 按分类获取商品
- GET /items/seller/{sellerId} - 获取卖家的所有商品
- GET /items/search?keyword=xxx - 搜索商品
- GET /items/price-range?minPrice=xx&maxPrice=xx - 按价格范围筛选商品
- PUT /items/{id} - 更新商品信息
- DELETE /items/{id} - 删除商品
- PUT /items/{id}/status - 更改商品状态
- POST /items/{id}/like - 点赞商品
### 聊天相关接口
- POST /messages - 发送消息
- GET /messages/conversation/{userId1}/{userId2} - 获取用户间对话
- GET /messages/unread/{userId} - 获取未读消息
- PUT /messages/{id}/read - 标记单条消息已读
- PUT /messages/read-all/{userId}/{senderId} - 标记所有消息已读
- GET /messages/conversations/{userId} - 获取用户的所有会话

@ -0,0 +1,217 @@
# 应用启动与测试连接指南
本文档提供了多种启动Spring Boot应用程序的方法以及如何测试API连接。
## 前提条件
在启动应用之前,请确保:
1. 已完成数据库创建(参考 `database_setup_guide.md`
2. `application.properties` 中的数据库配置正确
3. JDK已正确安装已验证系统有Java 17
## 方法一使用IDE启动推荐
如果您使用IntelliJ IDEA或Eclipse等IDE
1. 打开IDE导入`backend`目录作为Maven项目
2. 等待IDE下载依赖并构建项目
3. 找到主类 `SecondHandMarketApplication.java`
4. 右键点击该类,选择"Run"或"Debug"选项
5. 观察控制台输出,确认应用是否成功启动
## 方法二使用内置Maven包装器
如果系统中没有安装Maven但项目中有Maven包装器
1. 检查`backend`目录下是否有`mvnw`Windows下为`mvnw.cmd`)文件
2. 如果没有我们需要先创建Maven包装器
### 创建Maven包装器如果不存在
打开命令提示符CMD或PowerShell进入`backend`目录:
```bash
cd C:\Users\asus\AndroidStudioProjects\Project\LLRiseTabBarDemo\backend
```
然后执行以下命令创建Maven包装器
```bash
java -cp .\pom.xml org.apache.maven.wrapper.MavenWrapperMain --version
```
注意如果上述命令失败您可以手动下载Maven包装器文件。
### 使用Maven包装器启动应用
```bash
# Windows命令
mvnw.cmd spring-boot:run
# 或者使用PowerShell
./mvnw spring-boot:run
```
## 方法三使用Java直接运行
如果您能获取到编译好的JAR文件
1. 首先构建项目如果有Maven
```bash
mvn clean package
```
2. 然后运行生成的JAR文件
```bash
java -jar target\secondhandmarket-0.0.1-SNAPSHOT.jar
```
## 方法四使用Spring Boot DashboardIntelliJ IDEA
如果您使用IntelliJ IDEA Ultimate版本
1. 打开IDE导入项目
2. 找到底部的"Spring Boot Dashboard"选项卡
3. 在仪表板中找到您的应用
4. 点击绿色运行按钮启动应用
## 验证应用是否成功启动
应用成功启动后,您会在控制台看到类似以下输出:
```
2024-xx-xx xx:xx:xx.xxx INFO xxx --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http)
2024-xx-xx xx:xx:xx.xxx INFO xxx --- [ main] c.e.secondhandmarket.SecondHandMarketApplication : Started SecondHandMarketApplication in x.xxx seconds
```
这表示应用已成功启动并在8080端口监听HTTP请求。
## 测试API连接
### 使用浏览器测试
打开浏览器访问以下URL测试基本连接
```
http://localhost:8080/users/exists/phone/13800138000
```
您应该会看到类似以下JSON响应
```json
{"exists":false}
```
### 使用Postman测试推荐
1. 下载并安装Postmanhttps://www.postman.com/downloads/
2. 打开Postman创建一个新的请求
#### 测试用户注册
- 请求类型POST
- URLhttp://localhost:8080/users/register
- 请求体Body选择raw和JSON格式
```json
{
"phone": "13800138000",
"password": "123456",
"nickname": "测试用户"
}
```
- 点击Send按钮
#### 测试用户登录
- 请求类型POST
- URLhttp://localhost:8080/users/login
- 请求体Body
```json
{
"phone": "13800138000",
"password": "123456"
}
```
- 点击Send按钮
#### 测试商品列表获取
- 请求类型GET
- URLhttp://localhost:8080/items
- 点击Send按钮
### 使用curl命令测试Windows 10/11
如果您的Windows系统已安装curlWindows 10/11通常预装可以使用以下命令测试
```bash
# 测试手机号是否存在
curl http://localhost:8080/users/exists/phone/13800138000
# 测试用户注册
curl -X POST -H "Content-Type: application/json" -d "{\"phone\":\"13800138000\",\"password\":\"123456\",\"nickname\":\"测试用户\"}" http://localhost:8080/users/register
```
## 常见启动问题及解决方案
### 1. 数据库连接失败
错误信息可能包含:`Failed to obtain JDBC Connection`
**解决方案:**
- 确认MySQL服务是否正在运行
- 检查`application.properties`中的数据库配置是否正确
- 确保数据库`secondhandmarket`已创建
- 验证用户名和密码是否正确
### 2. 端口被占用
错误信息可能包含:`Address already in use`
**解决方案:**
- 修改`application.properties`中的端口号:
```properties
server.port=8081
```
- 或者关闭占用8080端口的其他应用
### 3. 依赖下载失败
错误信息可能包含:`Could not resolve dependencies`
**解决方案:**
- 确保网络连接正常
- 可以尝试使用国内Maven镜像修改pom.xml
### 4. 类找不到错误
错误信息可能包含:`ClassNotFoundException`
**解决方案:**
- 重新构建项目
- 确保所有依赖都已正确下载
## 应用停止方法
1. 在IDE中点击停止按钮
2. 在命令行中按Ctrl + C组合键
3. 在Windows任务管理器中结束Java进程
## 开发环境提示
1. 在开发环境中,可以在`application.properties`中添加以下配置以获得更好的调试体验:
```properties
# 启用热重载
spring.devtools.restart.enabled=true
# 设置日志级别
logging.level.root=INFO
logging.level.com.example.secondhandmarket=DEBUG
```
2. 如果您需要频繁修改代码并测试建议使用IDE的Debug模式启动应用这样可以
- 设置断点
- 单步执行代码
- 查看变量值
祝您测试顺利!

@ -0,0 +1,2 @@
'vac' 不是内部或外部命令,也不是可运行的程序
或批处理文件。

@ -0,0 +1,216 @@
# 数据库创建详细操作步骤
## 1. MySQL数据库安装
如果还没有安装MySQL请按照以下步骤安装
### Windows系统安装MySQL
1. 访问MySQL官方网站下载页面[MySQL Community Server](https://dev.mysql.com/downloads/mysql/)
2. 选择Windows版本下载安装包推荐使用MySQL Installer
3. 运行安装程序,选择"Developer Default"或"Server only"安装类型
4. 按照安装向导提示完成安装设置root用户密码请记住这个密码
5. 确保MySQL服务设置为自动启动
### 验证MySQL安装
打开命令提示符CMD或PowerShell输入以下命令验证MySQL是否正确安装
```bash
mysql --version
```
如果安装成功会显示MySQL的版本信息。
## 2. 启动MySQL服务
### Windows系统
1. 按下Win + R输入`services.msc`打开服务管理器
2. 找到"MySQL80"服务(或类似名称)
3. 确保服务状态为"正在运行",如果未运行,右键点击选择"启动"
## 3. 连接到MySQL服务器
### 使用命令行连接
打开命令提示符CMD或PowerShell输入以下命令连接到MySQL
```bash
mysql -u root -p
```
系统会提示输入密码输入您在安装MySQL时设置的root密码。
### 使用MySQL Workbench连接可选
如果您安装了MySQL Workbench可以通过图形界面连接
1. 打开MySQL Workbench
2. 点击左侧的"Local instance MySQL80"连接
3. 输入密码并连接
## 4. 创建数据库
连接成功后执行以下SQL命令创建数据库
```sql
CREATE DATABASE secondhandmarket CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
```
执行成功后,您会看到类似以下输出:
```
Query OK, 1 row affected (0.02 sec)
```
## 5. 选择数据库
执行以下命令选择刚创建的数据库:
```sql
USE secondhandmarket;
```
输出:
```
Database changed
```
## 6. 创建表结构
执行以下SQL语句创建所需的表结构
### 创建users表
```sql
CREATE TABLE users (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
phone VARCHAR(20) NOT NULL UNIQUE,
password VARCHAR(100) NOT NULL,
nickname VARCHAR(50),
avatar VARCHAR(255),
gender VARCHAR(10),
bio VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_login TIMESTAMP,
status VARCHAR(20) DEFAULT 'ACTIVE'
);
```
### 创建items表
```sql
CREATE TABLE items (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(100) NOT NULL,
description TEXT,
price DECIMAL(10, 2) NOT NULL,
category VARCHAR(50) NOT NULL,
status VARCHAR(20) DEFAULT 'AVAILABLE',
images TEXT,
seller_id BIGINT NOT NULL,
view_count INT DEFAULT 0,
like_count INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (seller_id) REFERENCES users(id)
);
```
### 创建chat_messages表
```sql
CREATE TABLE chat_messages (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
sender_id BIGINT NOT NULL,
receiver_id BIGINT NOT NULL,
item_id BIGINT NOT NULL,
content TEXT NOT NULL,
is_read BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (sender_id) REFERENCES users(id),
FOREIGN KEY (receiver_id) REFERENCES users(id),
FOREIGN KEY (item_id) REFERENCES items(id)
);
```
## 7. 创建索引(提高查询性能)
为常用查询字段创建索引:
```sql
-- 为users表的phone字段创建索引
CREATE INDEX idx_users_phone ON users(phone);
-- 为items表的常用查询字段创建索引
CREATE INDEX idx_items_seller_id ON items(seller_id);
CREATE INDEX idx_items_category ON items(category);
CREATE INDEX idx_items_status ON items(status);
CREATE INDEX idx_items_price ON items(price);
-- 为chat_messages表的查询字段创建索引
CREATE INDEX idx_chat_messages_sender_receiver ON chat_messages(sender_id, receiver_id);
CREATE INDEX idx_chat_messages_receiver_id ON chat_messages(receiver_id);
CREATE INDEX idx_chat_messages_item_id ON chat_messages(item_id);
```
## 8. 验证表结构
执行以下命令查看创建的表:
```sql
SHOW TABLES;
```
输出应该包括:
```
+---------------------------+
| Tables_in_secondhandmarket |
+---------------------------+
| chat_messages |
| items |
| users |
+---------------------------+
```
## 9. 配置应用程序连接
确保`application.properties`文件中的数据库配置正确:
```properties
spring.datasource.url=jdbc:mysql://localhost:3306/secondhandmarket?useSSL=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=061723 # 这是您设置的MySQL密码
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# JPA配置
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
```
## 10. 测试连接
当您启动Spring Boot应用程序时它应该能够成功连接到MySQL数据库。如果连接失败请检查以下几点
1. MySQL服务是否正在运行
2. 数据库名称是否正确
3. 用户名和密码是否正确
4. 防火墙是否允许连接
5. MySQL是否允许root用户从localhost连接
## 11. 注意事项
1. 在生产环境中不建议使用root用户应该创建一个具有适当权限的新用户
2. 确保数据库密码安全,不要硬编码在应用程序中
3. 定期备份数据库
4. 考虑使用连接池来优化数据库连接管理
## 故障排除
如果遇到连接问题请尝试以下命令检查MySQL用户权限
```sql
SELECT user, host FROM mysql.user;
GRANT ALL PRIVILEGES ON secondhandmarket.* TO 'root'@'localhost';
FLUSH PRIVILEGES;
```

@ -0,0 +1,120 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>secondhand-market</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>SecondHandMarket</name>
<description>二手市场后端应用</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.15</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<java.version>17</java.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- Spring Boot 核心依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Data JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- H2 内存数据库 (用于开发测试) -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Spring Boot 测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Spring Security 测试 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Lombok (简化实体类编写) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- 用于密码加密的 BCrypt -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>central</id>
<url>https://repo1.maven.org/maven2/</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>central</id>
<url>https://repo1.maven.org/maven2/</url>
</pluginRepository>
</pluginRepositories>
</project>

@ -0,0 +1,89 @@
@echo off
chcp 65001 > nul
cls
echo =========================================
echo 二手交易市场后端测试服务
REM 检查Java环境
java -version > nul 2>&1
if %errorlevel% neq 0 (
echo 错误未找到Java运行环境
pause
exit /b 1
)
REM 创建一个简单的TestServer.java文件
echo 正在创建测试服务器...
(
echo import java.io.*;
echo import java.net.*;
echo
echo public class TestServer {
echo public static void main(String[] args) {
echo try {
echo int port = 8080;
echo ServerSocket server = new ServerSocket(port);
echo System.out.println("\n测试服务器启动成功");
echo System.out.println("正在监听端口:" + port);
echo System.out.println("访问地址http://localhost:" + port + "/api");
echo System.out.println("\n按 Ctrl+C 停止服务\n");
echo
echo while (true) {
echo Socket client = server.accept();
echo handle(client);
echo }
echo } catch (Exception e) {
echo System.err.println("服务器错误: " + e.getMessage());
echo }
echo }
echo
echo private static void handle(Socket client) {
echo new Thread(() -> {
echo try (BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
echo OutputStream out = client.getOutputStream()) {
echo
echo String line = in.readLine();
echo System.out.println("请求: " + line);
echo
echo // 读取所有请求头
echo while ((line = in.readLine()) != null && !line.isEmpty()) {}
echo
echo // 响应
echo String response = "{\"status\":\"success\",\"message\":\"后端服务已启动!\"}";
echo
echo String headers = "HTTP/1.1 200 OK\r\n"
echo + "Content-Type: application/json\r\n"
echo + "Access-Control-Allow-Origin: *\r\n"
echo + "Content-Length: " + response.length() + "\r\n"
echo + "\r\n";
echo
echo out.write(headers.getBytes());
echo out.write(response.getBytes());
echo out.flush();
echo
echo } catch (Exception e) {
echo System.err.println("处理错误: " + e.getMessage());
echo } finally {
echo try { client.close(); } catch (Exception e) {}
echo }
echo }).start();
echo }
echo }
) > TestServer.java
REM 编译并运行
echo 正在编译测试服务器...
javac TestServer.java
if %errorlevel% neq 0 (
echo 编译失败!
pause
exit /b 1
)
echo 编译成功!正在启动服务...
java TestServer
pause

@ -0,0 +1,13 @@
package com.example.secondhandmarket;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SecondHandMarketApplication {
public static void main(String[] args) {
SpringApplication.run(SecondHandMarketApplication.class, args);
}
}

@ -0,0 +1,41 @@
package com.example.secondhandmarket.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import java.util.Arrays;
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration corsConfig = new CorsConfiguration();
// 允许所有来源
corsConfig.setAllowedOrigins(Arrays.asList("*"));
// 允许所有HTTP方法
corsConfig.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
// 允许的请求头
corsConfig.setAllowedHeaders(Arrays.asList("Origin", "Content-Type", "Accept", "Authorization", "X-Requested-With"));
// 允许暴露的响应头
corsConfig.setExposedHeaders(Arrays.asList("Content-Length", "Content-Type", "X-Total-Count"));
// 允许携带认证信息
corsConfig.setAllowCredentials(true);
// 预检请求的缓存时间
corsConfig.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfig);
return new CorsFilter(source);
}
}

@ -0,0 +1,48 @@
package com.example.secondhandmarket.config;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import java.util.HashMap;
import java.util.Map;
@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
// 处理一般运行时异常
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<?> handleRuntimeException(RuntimeException ex, WebRequest request) {
Map<String, Object> body = new HashMap<>();
body.put("error", ex.getMessage());
body.put("status", HttpStatus.BAD_REQUEST.value());
return new ResponseEntity<>(body, HttpStatus.BAD_REQUEST);
}
// 处理空指针异常
@ExceptionHandler(NullPointerException.class)
public ResponseEntity<?> handleNullPointerException(NullPointerException ex, WebRequest request) {
Map<String, Object> body = new HashMap<>();
body.put("error", "请求处理时发生错误,请稍后再试");
body.put("status", HttpStatus.INTERNAL_SERVER_ERROR.value());
return new ResponseEntity<>(body, HttpStatus.INTERNAL_SERVER_ERROR);
}
// 处理异常的通用方法
@ExceptionHandler(Exception.class)
public ResponseEntity<?> handleGenericException(Exception ex, WebRequest request) {
Map<String, Object> body = new HashMap<>();
body.put("error", "服务器内部错误");
body.put("status", HttpStatus.INTERNAL_SERVER_ERROR.value());
// 记录异常日志
ex.printStackTrace();
return new ResponseEntity<>(body, HttpStatus.INTERNAL_SERVER_ERROR);
}
}

@ -0,0 +1,34 @@
package com.example.secondhandmarket.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf().disable() // 禁用CSRF保护因为我们使用的是JWT
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无状态会话
.and()
.authorizeRequests()
.antMatchers("/users/register", "/users/login", "/users/exists/phone/**",
"/items", "/items/**", "/messages/**").permitAll() // 允许公开访问的端点
.anyRequest().authenticated(); // 其他所有请求都需要身份验证
return http.build();
}
}

@ -0,0 +1,72 @@
package com.example.secondhandmarket.controller;
import com.example.secondhandmarket.model.entity.ChatMessage;
import com.example.secondhandmarket.service.ChatMessageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/messages")
public class ChatMessageController {
@Autowired
private ChatMessageService chatMessageService;
@PostMapping
public ResponseEntity<?> sendMessage(@RequestBody ChatMessage message) {
try {
ChatMessage sentMessage = chatMessageService.sendMessage(message);
return ResponseEntity.ok(sentMessage);
} catch (Exception e) {
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
}
}
@GetMapping("/conversation/{userId1}/{userId2}")
public ResponseEntity<?> getConversation(
@PathVariable Long userId1,
@PathVariable Long userId2) {
List<ChatMessage> messages = chatMessageService.getConversationBetweenUsers(userId1, userId2);
// 标记消息为已读
chatMessageService.markAllAsRead(userId2, userId1);
return ResponseEntity.ok(messages);
}
@GetMapping("/unread/{userId}")
public ResponseEntity<?> getUnreadMessages(@PathVariable Long userId) {
List<ChatMessage> unreadMessages = chatMessageService.getUnreadMessagesByUserId(userId);
return ResponseEntity.ok(unreadMessages);
}
@PutMapping("/{id}/read")
public ResponseEntity<?> markAsRead(@PathVariable Long id) {
try {
chatMessageService.markAsRead(id);
return ResponseEntity.ok(Map.of("message", "消息已标记为已读"));
} catch (Exception e) {
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
}
}
@PutMapping("/read-all/{userId}/{senderId}")
public ResponseEntity<?> markAllAsRead(
@PathVariable Long userId,
@PathVariable Long senderId) {
try {
chatMessageService.markAllAsRead(userId, senderId);
return ResponseEntity.ok(Map.of("message", "所有消息已标记为已读"));
} catch (Exception e) {
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
}
}
@GetMapping("/conversations/{userId}")
public ResponseEntity<?> getUserConversations(@PathVariable Long userId) {
List<Long> conversationUserIds = chatMessageService.getUserConversations(userId);
return ResponseEntity.ok(conversationUserIds);
}
}

@ -0,0 +1,132 @@
package com.example.secondhandmarket.controller;
import com.example.secondhandmarket.model.entity.Item;
import com.example.secondhandmarket.service.ItemService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/items")
public class ItemController {
@Autowired
private ItemService itemService;
@PostMapping
public ResponseEntity<?> createItem(@RequestBody Item item) {
try {
Item createdItem = itemService.createItem(item);
return ResponseEntity.ok(createdItem);
} catch (Exception e) {
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
}
}
@GetMapping("/{id}")
public ResponseEntity<?> getItemById(@PathVariable Long id) {
// 增加浏览次数
itemService.incrementViewCount(id);
return itemService.findById(id)
.map(item -> ResponseEntity.ok(item))
.orElseGet(() -> ResponseEntity.notFound().build());
}
@GetMapping
public ResponseEntity<?> getAllItems(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
Pageable pageable = PageRequest.of(page, size);
Page<Item> items = itemService.findAllAvailable(pageable);
return ResponseEntity.ok(items);
}
@GetMapping("/category/{category}")
public ResponseEntity<?> getItemsByCategory(
@PathVariable String category,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
Pageable pageable = PageRequest.of(page, size);
Page<Item> items = itemService.findByCategory(category, pageable);
return ResponseEntity.ok(items);
}
@GetMapping("/seller/{sellerId}")
public ResponseEntity<?> getItemsBySeller(
@PathVariable Long sellerId,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
Pageable pageable = PageRequest.of(page, size);
Page<Item> items = itemService.findBySeller(sellerId, pageable);
return ResponseEntity.ok(items);
}
@GetMapping("/search")
public ResponseEntity<?> searchItems(
@RequestParam String keyword,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
Pageable pageable = PageRequest.of(page, size);
Page<Item> items = itemService.searchItems(keyword, pageable);
return ResponseEntity.ok(items);
}
@GetMapping("/price-range")
public ResponseEntity<?> getItemsByPriceRange(
@RequestParam Double minPrice,
@RequestParam Double maxPrice,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
Pageable pageable = PageRequest.of(page, size);
Page<Item> items = itemService.findByPriceRange(minPrice, maxPrice, pageable);
return ResponseEntity.ok(items);
}
@PutMapping("/{id}")
public ResponseEntity<?> updateItem(@PathVariable Long id, @RequestBody Item item) {
try {
item.setId(id);
Item updatedItem = itemService.updateItem(item);
return ResponseEntity.ok(updatedItem);
} catch (RuntimeException e) {
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
}
}
@DeleteMapping("/{id}")
public ResponseEntity<?> deleteItem(@PathVariable Long id) {
try {
itemService.deleteItem(id);
return ResponseEntity.ok(Map.of("message", "商品删除成功"));
} catch (RuntimeException e) {
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
}
}
@PutMapping("/{id}/status")
public ResponseEntity<?> changeItemStatus(@PathVariable Long id, @RequestBody Map<String, String> statusMap) {
try {
String status = statusMap.get("status");
itemService.changeStatus(id, status);
return ResponseEntity.ok(Map.of("message", "状态更新成功"));
} catch (Exception e) {
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
}
}
@PostMapping("/{id}/like")
public ResponseEntity<?> likeItem(@PathVariable Long id) {
try {
itemService.incrementLikeCount(id);
return ResponseEntity.ok(Map.of("message", "点赞成功"));
} catch (Exception e) {
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
}
}
}

@ -0,0 +1,60 @@
package com.example.secondhandmarket.controller;
import com.example.secondhandmarket.model.entity.User;
import com.example.secondhandmarket.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/register")
public ResponseEntity<?> register(@RequestBody User user) {
try {
User registeredUser = userService.register(user);
return ResponseEntity.ok(registeredUser);
} catch (RuntimeException e) {
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
}
}
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody Map<String, String> credentials) {
String phone = credentials.get("phone");
String password = credentials.get("password");
return userService.login(phone, password)
.map(user -> ResponseEntity.ok(user))
.orElseGet(() -> ResponseEntity.badRequest().body(Map.of("error", "手机号或密码错误")));
}
@GetMapping("/{id}")
public ResponseEntity<?> getUserById(@PathVariable Long id) {
return userService.findById(id)
.map(user -> ResponseEntity.ok(user))
.orElseGet(() -> ResponseEntity.notFound().build());
}
@PutMapping("/{id}")
public ResponseEntity<?> updateUser(@PathVariable Long id, @RequestBody User user) {
try {
user.setId(id);
User updatedUser = userService.update(user);
return ResponseEntity.ok(updatedUser);
} catch (RuntimeException e) {
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
}
}
@GetMapping("/exists/phone/{phone}")
public ResponseEntity<?> checkPhoneExists(@PathVariable String phone) {
return ResponseEntity.ok(Map.of("exists", userService.existsByPhone(phone)));
}
}

@ -0,0 +1,35 @@
package com.example.secondhandmarket.model.entity;
import lombok.Data;
import javax.persistence.*;
import java.util.Date;
@Entity
@Table(name = "chat_messages")
@Data
public class ChatMessage {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "sender_id", nullable = false)
private User sender;
@ManyToOne
@JoinColumn(name = "receiver_id", nullable = false)
private User receiver;
@ManyToOne
@JoinColumn(name = "item_id")
private Item item;
@Column(nullable = false, columnDefinition = "TEXT")
private String content;
@Column(nullable = false)
private Date time;
private boolean read = false;
private String type = "text";
}

@ -0,0 +1,49 @@
package com.example.secondhandmarket.model.entity;
import lombok.Data;
import javax.persistence.*;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
@Entity
@Table(name = "items")
@Data
public class Item {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String title;
private String description;
@Column(nullable = false)
private BigDecimal price;
private String category;
private String subcategory;
private String condition;
private String status = "available";
@ElementCollection
private List<String> images;
@Column(name = "view_count")
private int viewCount = 0;
@Column(name = "like_count")
private int likeCount = 0;
@Column(name = "created_at")
private Date createdAt;
@Column(name = "updated_at")
private Date updatedAt;
@ManyToOne
@JoinColumn(name = "seller_id", nullable = false)
private User seller;
}

@ -0,0 +1,34 @@
package com.example.secondhandmarket.model.entity;
import lombok.Data;
import javax.persistence.*;
import java.util.Date;
@Entity
@Table(name = "users")
@Data
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String phone;
@Column(nullable = false)
private String password;
@Column(nullable = false)
private String username;
private String avatar;
private String bio;
@Column(name = "register_time")
private Date registerTime;
@Column(name = "last_login_time")
private Date lastLoginTime;
private boolean enabled = true;
}

@ -0,0 +1,24 @@
package com.example.secondhandmarket.repository;
import com.example.secondhandmarket.model.entity.ChatMessage;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface ChatMessageRepository extends JpaRepository<ChatMessage, Long> {
@Query("SELECT m FROM ChatMessage m WHERE (m.sender.id = :userId1 AND m.receiver.id = :userId2) OR (m.sender.id = :userId2 AND m.receiver.id = :userId1) ORDER BY m.time ASC")
List<ChatMessage> findConversationBetweenUsers(@Param("userId1") Long userId1, @Param("userId2") Long userId2);
@Query("SELECT m FROM ChatMessage m WHERE m.receiver.id = :userId AND m.read = false ORDER BY m.time DESC")
List<ChatMessage> findUnreadMessagesByUserId(@Param("userId") Long userId);
List<ChatMessage> findByItemIdAndSenderIdAndReceiverId(Long itemId, Long senderId, Long receiverId);
@Query("SELECT DISTINCT CASE WHEN m.sender.id = :userId THEN m.receiver.id ELSE m.sender.id END FROM ChatMessage m WHERE m.sender.id = :userId OR m.receiver.id = :userId")
List<Long> findUserConversations(@Param("userId") Long userId);
}

@ -0,0 +1,27 @@
package com.example.secondhandmarket.repository;
import com.example.secondhandmarket.model.entity.Item;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface ItemRepository extends JpaRepository<Item, Long> {
Page<Item> findByStatus(String status, Pageable pageable);
Page<Item> findByCategoryAndStatus(String category, String status, Pageable pageable);
Page<Item> findBySellerIdAndStatus(Long sellerId, String status, Pageable pageable);
@Query("SELECT i FROM Item i WHERE i.status = 'available' AND (i.title LIKE %:keyword% OR i.description LIKE %:keyword%)")
Page<Item> searchItems(@Param("keyword") String keyword, Pageable pageable);
@Query("SELECT i FROM Item i WHERE i.status = 'available' AND i.price BETWEEN :minPrice AND :maxPrice")
Page<Item> findByPriceRange(@Param("minPrice") Double minPrice, @Param("maxPrice") Double maxPrice, Pageable pageable);
void incrementViewCount(Long id);
void incrementLikeCount(Long id);
}

@ -0,0 +1,13 @@
package com.example.secondhandmarket.repository;
import com.example.secondhandmarket.model.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByPhone(String phone);
boolean existsByPhone(String phone);
}

@ -0,0 +1,15 @@
package com.example.secondhandmarket.service;
import com.example.secondhandmarket.model.entity.ChatMessage;
import java.util.List;
import java.util.Optional;
public interface ChatMessageService {
ChatMessage sendMessage(ChatMessage message);
List<ChatMessage> getConversationBetweenUsers(Long userId1, Long userId2);
List<ChatMessage> getUnreadMessagesByUserId(Long userId);
void markAsRead(Long messageId);
void markAllAsRead(Long userId, Long senderId);
List<Long> getUserConversations(Long userId);
Optional<ChatMessage> findById(Long id);
}

@ -0,0 +1,21 @@
package com.example.secondhandmarket.service;
import com.example.secondhandmarket.model.entity.Item;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import java.util.Optional;
public interface ItemService {
Item createItem(Item item);
Optional<Item> findById(Long id);
Page<Item> findAllAvailable(Pageable pageable);
Page<Item> findByCategory(String category, Pageable pageable);
Page<Item> findBySeller(Long sellerId, Pageable pageable);
Page<Item> searchItems(String keyword, Pageable pageable);
Page<Item> findByPriceRange(Double minPrice, Double maxPrice, Pageable pageable);
Item updateItem(Item item);
void deleteItem(Long id);
void changeStatus(Long id, String status);
void incrementViewCount(Long id);
void incrementLikeCount(Long id);
}

@ -0,0 +1,14 @@
package com.example.secondhandmarket.service;
import com.example.secondhandmarket.model.entity.User;
import java.util.Optional;
public interface UserService {
User register(User user);
Optional<User> login(String phone, String password);
Optional<User> findById(Long id);
Optional<User> findByPhone(String phone);
User update(User user);
boolean existsByPhone(String phone);
void updateLastLoginTime(Long userId);
}

@ -0,0 +1,65 @@
package com.example.secondhandmarket.service.impl;
import com.example.secondhandmarket.model.entity.ChatMessage;
import com.example.secondhandmarket.repository.ChatMessageRepository;
import com.example.secondhandmarket.service.ChatMessageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.List;
import java.util.Optional;
@Service
public class ChatMessageServiceImpl implements ChatMessageService {
@Autowired
private ChatMessageRepository chatMessageRepository;
@Override
public ChatMessage sendMessage(ChatMessage message) {
message.setTime(new Date());
message.setRead(false);
return chatMessageRepository.save(message);
}
@Override
public List<ChatMessage> getConversationBetweenUsers(Long userId1, Long userId2) {
return chatMessageRepository.findConversationBetweenUsers(userId1, userId2);
}
@Override
public List<ChatMessage> getUnreadMessagesByUserId(Long userId) {
return chatMessageRepository.findUnreadMessagesByUserId(userId);
}
@Override
public void markAsRead(Long messageId) {
Optional<ChatMessage> messageOpt = chatMessageRepository.findById(messageId);
messageOpt.ifPresent(message -> {
message.setRead(true);
chatMessageRepository.save(message);
});
}
@Override
public void markAllAsRead(Long userId, Long senderId) {
List<ChatMessage> messages = chatMessageRepository.findConversationBetweenUsers(userId, senderId);
for (ChatMessage message : messages) {
if (message.getReceiver().getId().equals(userId) && !message.isRead()) {
message.setRead(true);
chatMessageRepository.save(message);
}
}
}
@Override
public List<Long> getUserConversations(Long userId) {
return chatMessageRepository.findUserConversations(userId);
}
@Override
public Optional<ChatMessage> findById(Long id) {
return chatMessageRepository.findById(id);
}
}

@ -0,0 +1,133 @@
package com.example.secondhandmarket.service.impl;
import com.example.secondhandmarket.model.entity.Item;
import com.example.secondhandmarket.repository.ItemRepository;
import com.example.secondhandmarket.service.ItemService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.Optional;
@Service
public class ItemServiceImpl implements ItemService {
@Autowired
private ItemRepository itemRepository;
@Override
public Item createItem(Item item) {
item.setCreatedAt(new Date());
item.setUpdatedAt(new Date());
item.setViewCount(0);
item.setLikeCount(0);
item.setStatus("available");
return itemRepository.save(item);
}
@Override
public Optional<Item> findById(Long id) {
return itemRepository.findById(id);
}
@Override
public Page<Item> findAllAvailable(Pageable pageable) {
return itemRepository.findByStatus("available", pageable);
}
@Override
public Page<Item> findByCategory(String category, Pageable pageable) {
return itemRepository.findByCategoryAndStatus(category, "available", pageable);
}
@Override
public Page<Item> findBySeller(Long sellerId, Pageable pageable) {
return itemRepository.findBySellerIdAndStatus(sellerId, "available", pageable);
}
@Override
public Page<Item> searchItems(String keyword, Pageable pageable) {
return itemRepository.searchItems(keyword, pageable);
}
@Override
public Page<Item> findByPriceRange(Double minPrice, Double maxPrice, Pageable pageable) {
return itemRepository.findByPriceRange(minPrice, maxPrice, pageable);
}
@Override
public Item updateItem(Item item) {
Optional<Item> existingItemOpt = itemRepository.findById(item.getId());
if (!existingItemOpt.isPresent()) {
throw new RuntimeException("商品不存在");
}
Item existingItem = existingItemOpt.get();
// 更新字段
if (item.getTitle() != null) {
existingItem.setTitle(item.getTitle());
}
if (item.getDescription() != null) {
existingItem.setDescription(item.getDescription());
}
if (item.getPrice() != null) {
existingItem.setPrice(item.getPrice());
}
if (item.getCategory() != null) {
existingItem.setCategory(item.getCategory());
}
if (item.getSubcategory() != null) {
existingItem.setSubcategory(item.getSubcategory());
}
if (item.getCondition() != null) {
existingItem.setCondition(item.getCondition());
}
if (item.getImages() != null) {
existingItem.setImages(item.getImages());
}
existingItem.setUpdatedAt(new Date());
return itemRepository.save(existingItem);
}
@Override
public void deleteItem(Long id) {
if (!itemRepository.existsById(id)) {
throw new RuntimeException("商品不存在");
}
itemRepository.deleteById(id);
}
@Override
public void changeStatus(Long id, String status) {
Optional<Item> itemOpt = itemRepository.findById(id);
if (itemOpt.isPresent()) {
Item item = itemOpt.get();
item.setStatus(status);
item.setUpdatedAt(new Date());
itemRepository.save(item);
}
}
@Override
public void incrementViewCount(Long id) {
Optional<Item> itemOpt = itemRepository.findById(id);
if (itemOpt.isPresent()) {
Item item = itemOpt.get();
item.setViewCount(item.getViewCount() + 1);
itemRepository.save(item);
}
}
@Override
public void incrementLikeCount(Long id) {
Optional<Item> itemOpt = itemRepository.findById(id);
if (itemOpt.isPresent()) {
Item item = itemOpt.get();
item.setLikeCount(item.getLikeCount() + 1);
itemRepository.save(item);
}
}
}

@ -0,0 +1,96 @@
package com.example.secondhandmarket.service.impl;
import com.example.secondhandmarket.model.entity.User;
import com.example.secondhandmarket.repository.UserRepository;
import com.example.secondhandmarket.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.Optional;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private BCryptPasswordEncoder passwordEncoder;
@Override
public User register(User user) {
// 检查手机号是否已存在
if (existsByPhone(user.getPhone())) {
throw new RuntimeException("手机号已被注册");
}
// 加密密码
user.setPassword(passwordEncoder.encode(user.getPassword()));
user.setRegisterTime(new Date());
user.setLastLoginTime(new Date());
return userRepository.save(user);
}
@Override
public Optional<User> login(String phone, String password) {
Optional<User> userOpt = userRepository.findByPhone(phone);
if (userOpt.isPresent()) {
User user = userOpt.get();
if (passwordEncoder.matches(password, user.getPassword())) {
// 更新最后登录时间
updateLastLoginTime(user.getId());
return Optional.of(user);
}
}
return Optional.empty();
}
@Override
public Optional<User> findById(Long id) {
return userRepository.findById(id);
}
@Override
public Optional<User> findByPhone(String phone) {
return userRepository.findByPhone(phone);
}
@Override
public User update(User user) {
Optional<User> existingUserOpt = userRepository.findById(user.getId());
if (!existingUserOpt.isPresent()) {
throw new RuntimeException("用户不存在");
}
User existingUser = existingUserOpt.get();
// 只更新允许修改的字段
if (user.getUsername() != null) {
existingUser.setUsername(user.getUsername());
}
if (user.getAvatar() != null) {
existingUser.setAvatar(user.getAvatar());
}
if (user.getBio() != null) {
existingUser.setBio(user.getBio());
}
return userRepository.save(existingUser);
}
@Override
public boolean existsByPhone(String phone) {
return userRepository.existsByPhone(phone);
}
@Override
public void updateLastLoginTime(Long userId) {
Optional<User> userOpt = userRepository.findById(userId);
userOpt.ifPresent(user -> {
user.setLastLoginTime(new Date());
userRepository.save(user);
});
}
}

@ -0,0 +1,37 @@
# 服务器配置
server.port=8080
server.servlet.context-path=/api
# 数据库配置 - 建议在MySQL安装后根据实际情况修改
spring.datasource.url=jdbc:mysql://localhost:3306/secondhand_market?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=061723
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# 为了开发测试方便可以暂时使用H2内存数据库如果需要
# 取消下面的注释并注释掉上面的MySQL配置以使用H2
# spring.datasource.url=jdbc:h2:mem:testdb
# spring.datasource.driverClassName=org.h2.Driver
# spring.datasource.username=sa
# spring.datasource.password=
# spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
# spring.h2.console.enabled=true
# spring.h2.console.path=/h2-console
# JPA配置
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
# 文件上传配置
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=50MB
# CORS配置
spring.mvc.cors.allowed-origins=*
spring.mvc.cors.allowed-methods=GET,POST,PUT,DELETE,OPTIONS
spring.mvc.cors.allowed-headers=*
# 日志配置
logging.level.root=INFO
logging.level.com.example.secondhandmarket=DEBUG

@ -0,0 +1,14 @@
package com.example.secondhandmarket;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SecondHandMarketApplicationTests {
@Test
void contextLoads() {
// 测试应用上下文是否能正常加载
}
}

@ -0,0 +1,47 @@
from http.server import BaseHTTPRequestHandler, HTTPServer
import json
from datetime import datetime
class RequestHandler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
response = {
"status": "success",
"message": "二手交易市场后端服务已启动!",
"timestamp": datetime.now().isoformat(),
"path": self.path
}
self.wfile.write(json.dumps(response).encode('utf-8'))
print(f"收到请求: {self.path}")
def do_POST(self):
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
response = {
"status": "success",
"message": "POST请求已成功处理",
"timestamp": datetime.now().isoformat()
}
self.wfile.write(json.dumps(response).encode('utf-8'))
print(f"收到POST请求: {self.path}")
def run(server_class=HTTPServer, handler_class=RequestHandler, port=8080):
server_address = ('', port)
httpd = server_class(server_address, handler_class)
print(f'\n测试服务器启动成功!')
print(f'正在监听端口: {port}')
print(f'访问地址: http://localhost:{port}/api')
print(f'\n按 Ctrl+C 停止服务\n')
httpd.serve_forever()
if __name__ == '__main__':
run()

@ -185,9 +185,26 @@ public class MainNavigateTabBar extends LinearLayout implements View.OnClickList
Object object = v.getTag();
if (object != null && object instanceof ViewHolder) {
ViewHolder holder = (ViewHolder) v.getTag();
showFragment(holder);
if (mTabSelectListener != null) {
mTabSelectListener.onTabSelected(holder);
// 特殊处理发布按钮 - 如果Fragment类为null这是发布按钮
if (holder.fragmentClass == null) {
// 使用反射方式调用Activity的onClickPublish方法避免直接依赖
try {
// 查找名为"onClickPublish"的方法参数为View类型
java.lang.reflect.Method publishMethod = mFragmentActivity.getClass()
.getMethod("onClickPublish", View.class);
// 调用该方法
publishMethod.invoke(mFragmentActivity, v);
} catch (Exception e) {
// 如果找不到方法或调用失败,记录错误但不崩溃
e.printStackTrace();
}
} else {
// 正常Tab显示对应的Fragment
showFragment(holder);
if (mTabSelectListener != null) {
mTabSelectListener.onTabSelected(holder);
}
}
}
}

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 113 KiB

Before

Width:  |  Height:  |  Size: 134 KiB

After

Width:  |  Height:  |  Size: 134 KiB

Before

Width:  |  Height:  |  Size: 184 KiB

After

Width:  |  Height:  |  Size: 184 KiB

Loading…
Cancel
Save