11.16 首页发布

liuyuchen_part
yuchen 1 month ago
parent d22675fcd8
commit 496f77dbbc

@ -0,0 +1,142 @@
package com.startsmake.llrisetabbardemo.activity;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
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.ChatMessageAdapter;
import com.startsmake.llrisetabbardemo.model.ChatMessage;
import java.util.ArrayList;
import java.util.List;
public class ChatActivity extends AppCompatActivity {
private TextView tvTitle;
private ImageView ivBack;
private RecyclerView rvChatMessages;
private EditText etMessage;
private ImageButton btnSend;
private ChatMessageAdapter chatAdapter;
private List<ChatMessage> messageList;
private String chatTitle;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_chat);
// 获取传递过来的聊天标题
chatTitle = getIntent().getStringExtra("chat_title");
if (chatTitle == null) {
chatTitle = "聊天";
}
initView();
initData();
setupClickListeners();
}
private void initView() {
tvTitle = findViewById(R.id.tvTitle);
ivBack = findViewById(R.id.ivBack);
rvChatMessages = findViewById(R.id.rvChatMessages);
etMessage = findViewById(R.id.etMessage);
btnSend = findViewById(R.id.btnSend);
tvTitle.setText(chatTitle);
}
private void initData() {
messageList = new ArrayList<>();
// 添加一些初始消息
if ("通知消息".equals(chatTitle)) {
messageList.add(new ChatMessage("系统", "红包到账提醒", "10:30", false));
messageList.add(new ChatMessage("我", "收到了,谢谢!", "10:31", true));
} else if ("刑事组之虎".equals(chatTitle)) {
messageList.add(new ChatMessage("刑事组之虎", "快给ta一个评价吧", "04-19 14:20", false));
messageList.add(new ChatMessage("我", "已经评价了,商品很不错!", "04-19 14:25", true));
} else {
messageList.add(new ChatMessage(chatTitle, "你好!", "刚刚", false));
}
chatAdapter = new ChatMessageAdapter(this, messageList);
rvChatMessages.setLayoutManager(new LinearLayoutManager(this));
rvChatMessages.setAdapter(chatAdapter);
// 滚动到底部
rvChatMessages.scrollToPosition(messageList.size() - 1);
}
private void setupClickListeners() {
// 返回按钮
ivBack.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
});
// 发送按钮
btnSend.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
sendMessage();
}
});
}
private void sendMessage() {
String message = etMessage.getText().toString().trim();
if (!TextUtils.isEmpty(message)) {
// 添加新消息到列表
ChatMessage newMessage = new ChatMessage("我", message, "刚刚", true);
messageList.add(newMessage);
chatAdapter.notifyItemInserted(messageList.size() - 1);
// 清空输入框
etMessage.setText("");
// 滚动到底部
rvChatMessages.scrollToPosition(messageList.size() - 1);
// 模拟对方回复(可选)
simulateReply(message);
}
}
private void simulateReply(String userMessage) {
// 延迟模拟回复
rvChatMessages.postDelayed(new Runnable() {
@Override
public void run() {
String reply = generateReply(userMessage);
ChatMessage replyMessage = new ChatMessage(chatTitle, reply, "刚刚", false);
messageList.add(replyMessage);
chatAdapter.notifyItemInserted(messageList.size() - 1);
rvChatMessages.scrollToPosition(messageList.size() - 1);
}
}, 1000);
}
private String generateReply(String userMessage) {
// 简单的回复逻辑
if (userMessage.contains("你好") || userMessage.contains("在吗")) {
return "在的,有什么可以帮您?";
} else if (userMessage.contains("价格") || userMessage.contains("多少钱")) {
return "这个商品价格是xxx元";
} else if (userMessage.contains("谢谢")) {
return "不客气!";
} else {
return "收到您的消息了!";
}
}
}

@ -0,0 +1,92 @@
package com.startsmake.llrisetabbardemo.activity;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.FragmentTransaction;
import com.startsmake.llrisetabbardemo.R;
import com.startsmake.llrisetabbardemo.fragment.ItemDetailFragment;
import com.startsmake.llrisetabbardemo.model.Item;
import com.startsmake.llrisetabbardemo.model.Product;
import manager.DataManager;
public class ProductDetailActivity extends AppCompatActivity {
public static final String EXTRA_PRODUCT = "extra_product";
public static final String EXTRA_ITEM = "extra_item";
public static final String EXTRA_PRODUCT_ID = "extra_product_id";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_product_detail);
// 获取传递的数据
Product product = (Product) getIntent().getSerializableExtra(EXTRA_PRODUCT);
Item item = (Item) getIntent().getSerializableExtra(EXTRA_ITEM);
String productId = getIntent().getStringExtra(EXTRA_PRODUCT_ID);
// 根据不同的数据来源创建Fragment
Item detailItem = null;
if (item != null) {
// 如果直接传递了Item对象
detailItem = item;
} else if (product != null) {
// 如果传递了Product对象转换为Item
detailItem = convertProductToItem(product);
} else if (productId != null) {
// 如果传递了商品ID从DataManager查找
DataManager dataManager = DataManager.getInstance();
Item foundItem = dataManager.getItemById(productId);
if (foundItem != null) {
detailItem = foundItem;
} else {
// 如果找不到尝试从Product列表查找并转换
for (Product p : dataManager.getAllProducts()) {
if (p.getId().equals(productId)) {
detailItem = convertProductToItem(p);
break;
}
}
}
}
if (detailItem != null) {
// 显示商品详情Fragment
showItemDetailFragment(detailItem);
} else {
// 处理数据加载失败的情况
finish();
}
}
private void showItemDetailFragment(Item item) {
ItemDetailFragment fragment = ItemDetailFragment.newInstance(item);
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
transaction.replace(R.id.fragment_container, fragment);
transaction.commit();
}
/**
* ProductItem
*/
private Item convertProductToItem(Product product) {
Item item = new Item();
item.setId(product.getId());
item.setTitle(product.getName());
item.setDescription(product.getDescription());
item.setCategory(product.getCategory());
item.setPrice(product.getPrice());
item.setLocation(product.getLocation());
item.setContact(product.getContact());
item.setWantCount(product.getWantCount());
// 设置默认发布时间(如果没有的话)
if (item.getPublishTime() == 0) {
item.setPublishTime(System.currentTimeMillis());
}
return item;
}
}

@ -0,0 +1,74 @@
package com.startsmake.llrisetabbardemo.adapter;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.startsmake.llrisetabbardemo.R;
import com.startsmake.llrisetabbardemo.model.ChatMessage;
import java.util.List;
public class ChatMessageAdapter extends RecyclerView.Adapter<ChatMessageAdapter.ViewHolder> {
private Context context;
private List<ChatMessage> messageList;
public ChatMessageAdapter(Context context, List<ChatMessage> messageList) {
this.context = context;
this.messageList = messageList;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(context).inflate(R.layout.item_chat_message, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
ChatMessage message = messageList.get(position);
if (message.isMe()) {
// 自己发送的消息 - 右侧显示
holder.layoutLeft.setVisibility(View.GONE);
holder.layoutRight.setVisibility(View.VISIBLE);
holder.tvRightMessage.setText(message.getContent());
holder.tvRightTime.setText(message.getTime());
} else {
// 对方发送的消息 - 左侧显示
holder.layoutRight.setVisibility(View.GONE);
holder.layoutLeft.setVisibility(View.VISIBLE);
holder.tvLeftMessage.setText(message.getContent());
holder.tvLeftTime.setText(message.getTime());
}
}
@Override
public int getItemCount() {
return messageList.size();
}
public static class ViewHolder extends RecyclerView.ViewHolder {
LinearLayout layoutLeft;
LinearLayout layoutRight;
TextView tvLeftMessage;
TextView tvRightMessage;
TextView tvLeftTime;
TextView tvRightTime;
public ViewHolder(@NonNull View itemView) {
super(itemView);
layoutLeft = itemView.findViewById(R.id.layoutLeft);
layoutRight = itemView.findViewById(R.id.layoutRight);
tvLeftMessage = itemView.findViewById(R.id.tvLeftMessage);
tvRightMessage = itemView.findViewById(R.id.tvRightMessage);
tvLeftTime = itemView.findViewById(R.id.tvLeftTime);
tvRightTime = itemView.findViewById(R.id.tvRightTime);
}
}
}

@ -0,0 +1,84 @@
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;
}
}

@ -0,0 +1,102 @@
package com.startsmake.llrisetabbardemo.adapter;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.startsmake.llrisetabbardemo.R;
import com.startsmake.llrisetabbardemo.model.MessageItem;
import java.util.List;
public class MessageAdapter extends RecyclerView.Adapter<MessageAdapter.ViewHolder> {
private Context context;
private List<MessageItem> messageList;
private OnItemClickListener onItemClickListener;
public MessageAdapter(Context context, List<MessageItem> messageList) {
this.context = context;
this.messageList = messageList;
}
// 添加点击监听接口
public interface OnItemClickListener {
void onItemClick(MessageItem item);
}
public void setOnItemClickListener(OnItemClickListener listener) {
this.onItemClickListener = listener;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(context).inflate(R.layout.item_message, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
MessageItem item = messageList.get(position);
// 设置默认白色头像背景
holder.ivAvatar.setBackgroundResource(R.drawable.bg_avatar_placeholder);
holder.tvTitle.setText(item.getTitle());
holder.tvContent.setText(item.getContent());
holder.tvTime.setText(item.getTime());
// 未读消息数量
if (item.getUnreadCount() > 0) {
holder.tvUnreadCount.setVisibility(View.VISIBLE);
holder.tvUnreadCount.setText(String.valueOf(item.getUnreadCount()));
} else {
holder.tvUnreadCount.setVisibility(View.GONE);
}
// 官方标识
if (item.isOfficial()) {
holder.ivOfficial.setVisibility(View.VISIBLE);
} else {
holder.ivOfficial.setVisibility(View.GONE);
}
// 添加点击事件
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (onItemClickListener != null) {
onItemClickListener.onItemClick(item);
}
}
});
}
@Override
public int getItemCount() {
return messageList.size();
}
public static class ViewHolder extends RecyclerView.ViewHolder {
ImageView ivAvatar;
TextView tvTitle;
TextView tvContent;
TextView tvTime;
TextView tvUnreadCount;
ImageView ivOfficial;
public ViewHolder(@NonNull View itemView) {
super(itemView);
ivAvatar = itemView.findViewById(R.id.ivAvatar);
tvTitle = itemView.findViewById(R.id.tvTitle);
tvContent = itemView.findViewById(R.id.tvContent);
tvTime = itemView.findViewById(R.id.tvTime);
tvUnreadCount = itemView.findViewById(R.id.tvUnreadCount);
ivOfficial = itemView.findViewById(R.id.ivOfficial);
}
}
}

@ -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;
}
}
}

@ -0,0 +1,40 @@
package com.startsmake.llrisetabbardemo.decoration;
import android.graphics.Rect;
import android.view.View;
import androidx.recyclerview.widget.RecyclerView;
public class GridSpacingItemDecoration extends RecyclerView.ItemDecoration {
private int spanCount;
private int spacing;
private boolean includeEdge;
public GridSpacingItemDecoration(int spanCount, int spacing, boolean includeEdge) {
this.spanCount = spanCount;
this.spacing = spacing;
this.includeEdge = includeEdge;
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
int position = parent.getChildAdapterPosition(view);
int column = position % spanCount;
if (includeEdge) {
outRect.left = spacing - column * spacing / spanCount;
outRect.right = (column + 1) * spacing / spanCount;
if (position < spanCount) {
outRect.top = spacing;
}
outRect.bottom = spacing;
} else {
outRect.left = column * spacing / spanCount;
outRect.right = spacing - (column + 1) * spacing / spanCount;
if (position >= spanCount) {
outRect.top = spacing;
}
}
}
}

@ -0,0 +1,135 @@
package com.startsmake.llrisetabbardemo.fragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import com.startsmake.llrisetabbardemo.R;
import com.startsmake.llrisetabbardemo.model.Item;
import com.startsmake.llrisetabbardemo.model.Product;
import manager.DataManager;
public class ItemDetailFragment extends Fragment {
private static final String ARG_ITEM = "item";
private static final String ARG_PRODUCT = "product";
private Item item;
private Product product;
// 使用Item对象的构造方法
public static ItemDetailFragment newInstance(Item item) {
ItemDetailFragment fragment = new ItemDetailFragment();
Bundle args = new Bundle();
args.putSerializable(ARG_ITEM, item);
fragment.setArguments(args);
return fragment;
}
// 使用Product对象的构造方法
public static ItemDetailFragment newInstance(Product product) {
ItemDetailFragment fragment = new ItemDetailFragment();
Bundle args = new Bundle();
args.putSerializable(ARG_PRODUCT, product);
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments() != null) {
item = (Item) getArguments().getSerializable(ARG_ITEM);
product = (Product) getArguments().getSerializable(ARG_PRODUCT);
// 如果传递的是Product转换为Item统一处理
if (product != null && item == null) {
item = convertProductToItem(product);
}
}
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_item_detail, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (item == null) {
Toast.makeText(getContext(), "商品信息加载失败", Toast.LENGTH_SHORT).show();
return;
}
// 初始化视图
ImageView ivItemImage = view.findViewById(R.id.ivItemImage);
TextView tvTitle = view.findViewById(R.id.tvTitle);
TextView tvPrice = view.findViewById(R.id.tvPrice);
TextView tvDescription = view.findViewById(R.id.tvDescription);
TextView tvCategory = view.findViewById(R.id.tvCategory);
TextView tvLocation = view.findViewById(R.id.tvLocation);
TextView tvContact = view.findViewById(R.id.tvContact);
TextView tvPublishTime = view.findViewById(R.id.tvPublishTime);
// 设置商品信息
tvTitle.setText(item.getTitle());
tvPrice.setText(String.format("¥%.2f", item.getPrice()));
tvDescription.setText(item.getDescription());
tvCategory.setText("分类:" + item.getCategory());
tvLocation.setText("位置:" + item.getLocation());
tvContact.setText("联系方式:" + item.getContact());
// 设置发布时间
String time = android.text.format.DateFormat.format("yyyy-MM-dd HH:mm", item.getPublishTime()).toString();
tvPublishTime.setText("发布时间:" + time);
// 加载图片(这里使用第一张图片作为主图)
if (item.getImageUrls() != null && !item.getImageUrls().isEmpty()) {
// 实际项目中这里应该加载网络图片,这里用占位符
ivItemImage.setImageResource(R.mipmap.ic_launcher);
} else {
// 如果没有图片,使用默认图片
ivItemImage.setImageResource(R.mipmap.ic_launcher);
}
// 增加浏览数
item.incrementViewCount();
// 联系卖家按钮
view.findViewById(R.id.btnContact).setOnClickListener(v -> {
Toast.makeText(getContext(), "联系卖家:" + item.getContact(), Toast.LENGTH_SHORT).show();
// 这里可以跳转到聊天界面或拨打电话
});
}
/**
* ProductItem
*/
private Item convertProductToItem(Product product) {
Item convertedItem = new Item();
convertedItem.setId(product.getId());
convertedItem.setTitle(product.getName());
convertedItem.setDescription(product.getDescription());
convertedItem.setCategory(product.getCategory());
convertedItem.setPrice(product.getPrice());
convertedItem.setLocation(product.getLocation());
convertedItem.setContact(product.getContact());
convertedItem.setWantCount(product.getWantCount());
// 设置默认发布时间
convertedItem.setPublishTime(System.currentTimeMillis());
return convertedItem;
}
}

@ -0,0 +1,268 @@
package com.startsmake.llrisetabbardemo.fragment;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.GridView;
import android.widget.Spinner;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import com.startsmake.llrisetabbardemo.R;
import com.startsmake.llrisetabbardemo.activity.MainActivity;
import com.startsmake.llrisetabbardemo.adapter.ImageAdapter;
import com.startsmake.llrisetabbardemo.model.Item;
import com.startsmake.llrisetabbardemo.model.Product;
import manager.DataManager;
import java.util.ArrayList;
import java.util.List;
public class PublishFragment extends Fragment {
private static final int REQUEST_CODE_PICK_IMAGES = 1001;
private static final int MAX_IMAGE_COUNT = 9;
private EditText etTitle, etDescription, etPrice, etContact;
private Spinner spinnerCategory, spinnerLocation;
private GridView gridViewImages;
private Button btnPublish;
private ImageAdapter imageAdapter;
private List<Uri> selectedImages = new ArrayList<>();
public PublishFragment() {
// Required empty public constructor
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_publish, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
initViews(view);
setupSpinners();
setupImageGrid();
setupClickListeners();
}
private void initViews(View view) {
etTitle = view.findViewById(R.id.etTitle);
etDescription = view.findViewById(R.id.etDescription);
etPrice = view.findViewById(R.id.etPrice);
etContact = view.findViewById(R.id.etContact);
spinnerCategory = view.findViewById(R.id.spinnerCategory);
spinnerLocation = view.findViewById(R.id.spinnerLocation);
gridViewImages = view.findViewById(R.id.gridViewImages);
btnPublish = view.findViewById(R.id.btnPublish);
}
private void setupSpinners() {
String[] categories = {"数码产品", "服装鞋帽", "家居日用", "图书文具", "美妆个护", "运动户外", "其他"};
ArrayAdapter<String> categoryAdapter = new ArrayAdapter<>(
requireContext(), android.R.layout.simple_spinner_item, categories);
categoryAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinnerCategory.setAdapter(categoryAdapter);
String[] locations = {"北京", "上海", "广州", "深圳", "杭州", "成都", "武汉", "其他"};
ArrayAdapter<String> locationAdapter = new ArrayAdapter<>(
requireContext(), android.R.layout.simple_spinner_item, locations);
locationAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinnerLocation.setAdapter(locationAdapter);
}
private void setupImageGrid() {
imageAdapter = new ImageAdapter(requireContext(), selectedImages);
gridViewImages.setAdapter(imageAdapter);
}
private void setupClickListeners() {
gridViewImages.setOnItemClickListener((parent, view, position, id) -> {
if (position == selectedImages.size() && selectedImages.size() < MAX_IMAGE_COUNT) {
openImagePicker();
}
});
btnPublish.setOnClickListener(v -> publishItem());
}
private void openImagePicker() {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("image/*");
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
startActivityForResult(Intent.createChooser(intent, "选择图片"), REQUEST_CODE_PICK_IMAGES);
}
@Override
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_PICK_IMAGES && resultCode == Activity.RESULT_OK) {
if (data != null) {
if (data.getClipData() != null) {
int count = Math.min(data.getClipData().getItemCount(),
MAX_IMAGE_COUNT - selectedImages.size());
for (int i = 0; i < count; i++) {
Uri imageUri = data.getClipData().getItemAt(i).getUri();
selectedImages.add(imageUri);
}
} else if (data.getData() != null) {
selectedImages.add(data.getData());
}
imageAdapter.notifyDataSetChanged();
}
}
}
private void publishItem() {
String title = etTitle.getText().toString().trim();
String description = etDescription.getText().toString().trim();
String priceStr = etPrice.getText().toString().trim();
String contact = etContact.getText().toString().trim();
// 验证输入
if (title.isEmpty()) {
Toast.makeText(requireContext(), "请输入商品标题", Toast.LENGTH_SHORT).show();
return;
}
if (description.isEmpty()) {
Toast.makeText(requireContext(), "请输入商品描述", Toast.LENGTH_SHORT).show();
return;
}
if (priceStr.isEmpty()) {
Toast.makeText(requireContext(), "请输入商品价格", Toast.LENGTH_SHORT).show();
return;
}
if (contact.isEmpty()) {
Toast.makeText(requireContext(), "请输入联系方式", Toast.LENGTH_SHORT).show();
return;
}
try {
double price = Double.parseDouble(priceStr);
if (price <= 0) {
Toast.makeText(requireContext(), "价格必须大于0", Toast.LENGTH_SHORT).show();
return;
}
// 创建物品对象
Item item = new Item();
item.setTitle(title);
item.setDescription(description);
item.setPrice(price);
item.setContact(contact);
item.setCategory(spinnerCategory.getSelectedItem().toString());
item.setLocation(spinnerLocation.getSelectedItem().toString());
item.setPublishTime(System.currentTimeMillis());
item.setUserId("user_" + System.currentTimeMillis());
Log.d("PublishFragment", "准备发布商品: " + item.getTitle() + ", 价格: " + item.getPrice());
// 使用DataManager保存商品并获取对应的Product对象
DataManager dataManager = DataManager.getInstance();
Product newProduct = dataManager.addItemAndGetProduct(item);
Log.d("PublishFragment", "商品已保存到DataManager准备切换到首页");
// 发布成功
Toast.makeText(requireContext(), "发布成功!", Toast.LENGTH_SHORT).show();
clearForm();
// 切换到首页并刷新数据
if (getActivity() instanceof MainActivity) {
MainActivity mainActivity = (MainActivity) getActivity();
// 直接调用切换首页的方法
mainActivity.switchToHomeFragment();
Log.d("PublishFragment", "已切换到首页,准备刷新数据");
// 延迟一小段时间确保HomeFragment已加载
new android.os.Handler().postDelayed(() -> {
try {
HomeFragment homeFragment = mainActivity.getHomeFragment();
if (homeFragment != null) {
Log.d("PublishFragment", "成功获取HomeFragment实例准备刷新数据");
// 从DataManager获取最新数据并刷新首页
refreshHomeFragment(homeFragment);
} else {
Log.e("PublishFragment", "获取HomeFragment实例失败为null");
// 备选方案:直接重新初始化首页数据
reinitializeHomeData();
}
} catch (Exception e) {
Log.e("PublishFragment", "刷新首页数据时出错", e);
reinitializeHomeData();
}
}, 500); // 增加延迟时间确保Fragment完全加载
}
} catch (NumberFormatException e) {
Toast.makeText(requireContext(), "请输入有效的价格", Toast.LENGTH_SHORT).show();
}
}
// 在 PublishFragment.java 的 publishItem 方法末尾添加
// 发布成功后,通知所有相关页面刷新数据
private void refreshAllData() {
// 这里可以添加事件总线或其他通知机制
// 目前主要依赖各个页面在 onResume 时重新加载数据
Log.d("PublishFragment", "新商品发布成功,建议相关页面刷新数据");
}
// 新增方法:备选方案重新初始化首页数据
private void reinitializeHomeData() {
Log.d("PublishFragment", "使用备选方案重新初始化首页数据");
// 这里可以尝试其他方式刷新首页,比如广播或事件总线
Toast.makeText(requireContext(), "发布成功,请手动刷新首页", Toast.LENGTH_SHORT).show();
}
// 修改刷新首页方法
private void refreshHomeFragment(HomeFragment homeFragment) {
try {
DataManager dataManager = DataManager.getInstance();
List<Product> allProducts = dataManager.getAllProducts();
Log.d("PublishFragment", "从DataManager获取到 " + allProducts.size() + " 个商品");
for (Product product : allProducts) {
Log.d("PublishFragment", "商品: " + product.getName() + ", ID: " + product.getId());
}
// 调用HomeFragment的方法来更新显示
homeFragment.updateProductList(allProducts);
Log.d("PublishFragment", "已调用HomeFragment的updateProductList方法");
} catch (Exception e) {
Log.e("PublishFragment", "刷新首页数据时出错", e);
}
}
private void clearForm() {
etTitle.setText("");
etDescription.setText("");
etPrice.setText("");
etContact.setText("");
selectedImages.clear();
imageAdapter.notifyDataSetChanged();
spinnerCategory.setSelection(0);
spinnerLocation.setSelection(0);
}
}

@ -0,0 +1,350 @@
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) {
if (instance == null) {
instance = new UserManager(context);
}
return instance;
}
/**
*
*/
public boolean isLoggedIn() {
return preferences.getBoolean("is_logged_in", false);
}
/**
*
*/
public boolean isPhoneRegistered(String phone) {
Set<String> usersSet = preferences.getStringSet(KEY_USERS, new HashSet<>());
for (String userJson : usersSet) {
User user = gson.fromJson(userJson, User.class);
if (user.getPhone().equals(phone)) {
return true;
}
}
return false;
}
/**
*
*/
public boolean registerUser(String phone, String password) {
// 检查手机号是否已注册
if (isPhoneRegistered(phone)) {
return false;
}
// 创建新用户
User newUser = new User(phone, password);
String userJson = gson.toJson(newUser);
Set<String> usersSet = preferences.getStringSet(KEY_USERS, new HashSet<>());
Set<String> newUsersSet = new HashSet<>(usersSet);
newUsersSet.add(userJson);
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<>());
for (String userJson : usersSet) {
User user = gson.fromJson(userJson, User.class);
if (user.getPhone().equals(phone) && user.getPassword().equals(password)) {
// 保存当前用户信息
saveCurrentUser(user);
return user;
}
}
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;
}
/**
*
*/
public boolean resetPassword(String phone, String newPassword) {
Set<String> usersSet = preferences.getStringSet(KEY_USERS, new HashSet<>());
Set<String> newUsersSet = new HashSet<>();
boolean found = false;
for (String userJson : usersSet) {
User user = gson.fromJson(userJson, User.class);
if (user.getPhone().equals(phone)) {
// 更新密码
user.setPassword(newPassword);
found = true;
}
newUsersSet.add(gson.toJson(user));
}
if (found) {
boolean success = preferences.edit().putStringSet(KEY_USERS, newUsersSet).commit();
if (success) {
// 如果重置的是当前登录用户,更新当前用户信息
User currentUser = getCurrentUser();
if (currentUser != null && currentUser.getPhone().equals(phone)) {
currentUser.setPassword(newPassword);
saveCurrentUser(currentUser);
}
}
return success;
}
return false;
}
/**
*
*/
private void saveCurrentUser(User user) {
String userJson = gson.toJson(user);
preferences.edit()
.putString(KEY_CURRENT_USER, userJson)
.putBoolean("is_logged_in", true)
.putString("user_phone", user.getPhone())
.putString("user_name", user.getUsername())
.apply();
}
/**
*
*/
public User getCurrentUser() {
String userJson = preferences.getString(KEY_CURRENT_USER, null);
if (userJson != null) {
return gson.fromJson(userJson, User.class);
}
return null;
}
/**
*
*/
public void logout() {
preferences.edit()
.remove(KEY_CURRENT_USER)
.putBoolean("is_logged_in", false)
.remove("user_phone")
.remove("user_name")
.remove(KEY_USER_TOKEN) // 清除token
.apply();
Log.d(TAG, "用户已登出");
}
/**
*
*/
public void saveUserLogin(String phone, String password) {
User user = loginUser(phone, password);
if (user != null) {
saveCurrentUser(user);
}
}
/**
*
*/
public boolean validateLogin(String phone, String password) {
return loginUser(phone, password) != null;
}
/**
*
*/
public boolean isUserExists(String phone) {
return isPhoneRegistered(phone);
}
/**
*
*/
public boolean updatePassword(String phone, String newPassword) {
return resetPassword(phone, newPassword);
}
}

@ -0,0 +1,28 @@
package com.startsmake.llrisetabbardemo.model;
public class ChatMessage {
private String sender;
private String content;
private String time;
private boolean isMe;
public ChatMessage(String sender, String content, String time, boolean isMe) {
this.sender = sender;
this.content = content;
this.time = time;
this.isMe = isMe;
}
// Getter and Setter methods
public String getSender() { return sender; }
public void setSender(String sender) { this.sender = sender; }
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 boolean isMe() { return isMe; }
public void setMe(boolean me) { isMe = me; }
}

@ -0,0 +1,121 @@
package com.startsmake.llrisetabbardemo.model;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
public class Item implements Serializable {
private String id;
private String title;
private String description;
private int wantCount;
private double price;
private List<String> imageUrls;
private String category;
private String location;
private String contact;
private String contactQQ; // 新增QQ联系方式
private String contactWechat; // 新增微信联系方式
private long publishTime;
private String userId;
private int viewCount; // 浏览数
private int likeCount; // 点赞数
public Item() {
imageUrls = new ArrayList<>();
viewCount = 0;
likeCount = 0;
}
// Getter 和 Setter
public int getWantCount() {
return wantCount;
}
public void setWantCount(int wantCount) {
this.wantCount = wantCount;
}
// 构造函数
public Item(String title, String description, double price, String category, String location, String contact) {
this();
this.title = title;
this.description = description;
this.price = price;
this.category = category;
this.location = location;
this.contact = contact;
this.publishTime = System.currentTimeMillis();
this.userId = "user_" + System.currentTimeMillis();
}
// Getter 和 Setter 方法
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 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 void addImageUrl(String imageUrl) { this.imageUrls.add(imageUrl); }
public String getCategory() { return category; }
public void setCategory(String category) { this.category = category; }
public String getLocation() { return location; }
public void setLocation(String location) { this.location = location; }
public String getContact() { return contact; }
public void setContact(String contact) { this.contact = contact; }
public String getContactQQ() { return contactQQ; }
public void setContactQQ(String contactQQ) { this.contactQQ = contactQQ; }
public String getContactWechat() { return contactWechat; }
public void setContactWechat(String contactWechat) { this.contactWechat = contactWechat; }
public long getPublishTime() { return publishTime; }
public void setPublishTime(long publishTime) { this.publishTime = publishTime; }
public String getUserId() { return userId; }
public void setUserId(String userId) { this.userId = userId; }
public int getViewCount() { return viewCount; }
public void setViewCount(int viewCount) { this.viewCount = viewCount; }
public int getLikeCount() { return likeCount; }
public void setLikeCount(int likeCount) { this.likeCount = likeCount; }
/**
*
*/
public void incrementViewCount() {
this.viewCount++;
}
/**
*
*/
public void incrementLikeCount() {
this.likeCount++;
}
@Override
public String toString() {
return "Item{" +
"id='" + id + '\'' +
", title='" + title + '\'' +
", price=" + price +
", category='" + category + '\'' +
", location='" + location + '\'' +
", publishTime=" + publishTime +
'}';
}
}

@ -0,0 +1,33 @@
package com.startsmake.llrisetabbardemo.model;
public class MessageItem {
private String title;
private String content;
private String time;
private int unreadCount;
private boolean isOfficial;
public MessageItem(String title, String content, String time, int unreadCount, boolean isOfficial) {
this.title = title;
this.content = content;
this.time = time;
this.unreadCount = unreadCount;
this.isOfficial = isOfficial;
}
// Getter and Setter methods
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 int getUnreadCount() { return unreadCount; }
public void setUnreadCount(int unreadCount) { this.unreadCount = unreadCount; }
public boolean isOfficial() { return isOfficial; }
public void setOfficial(boolean official) { isOfficial = official; }
}

@ -0,0 +1,27 @@
package com.startsmake.llrisetabbardemo.model;
public class User {
private String phone;
private String password;
private String username;
private long registerTime;
public User(String phone, String password) {
this.phone = phone;
this.password = password;
this.username = "用户_" + phone.substring(7);
this.registerTime = System.currentTimeMillis();
}
// Getters and Setters
public String getPhone() { return phone; }
public void setPhone(String phone) { this.phone = phone; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public long getRegisterTime() { return registerTime; }
}

@ -0,0 +1,522 @@
package manager;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
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 com.startsmake.llrisetabbardemo.model.Product;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
/**
* - 使
* API
*/
public class DataManager {
private static final String TAG = "DataManager";
private static final String PREF_NAME = "product_data";
private static final String KEY_ITEMS = "items";
// 单例实例
private static DataManager instance;
// 商品列表
private List<Item> itemList;
// API服务实例
private ApiService apiService;
// Context 和 SharedPreferences
private Context context;
private SharedPreferences sharedPreferences;
private Gson gson;
// 数据变化监听器列表
private List<OnDataChangedListener> dataChangedListeners = new CopyOnWriteArrayList<>();
// 回调接口用于通知UI数据加载状态
public interface OnDataLoadedListener {
void onDataLoaded(List<Item> items);
void onError(String message);
}
// 数据变化监听接口
public interface OnDataChangedListener {
void onDataChanged(); // 数据发生变化时调用
void onProductWantCountChanged(String productId, int newCount); // 商品想要人数变化时调用
}
// 私有构造函数,防止外部创建实例
private DataManager() {
itemList = new ArrayList<>();
}
/**
* DataManager Application Activity
*/
public void init(Context context) {
this.context = context.getApplicationContext();
this.sharedPreferences = this.context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
this.gson = new Gson();
// 初始化API服务
apiService = ApiClient.getClient().create(ApiService.class);
// 从本地存储加载数据
loadItemsFromStorage();
}
/**
*
* @return DataManager
*/
public static DataManager getInstance() {
if (instance == null) {
instance = new DataManager();
}
return instance;
}
/**
*
*/
public void registerDataChangedListener(OnDataChangedListener listener) {
if (!dataChangedListeners.contains(listener)) {
dataChangedListeners.add(listener);
Log.d(TAG, "注册数据变化监听器,当前监听器数量: " + dataChangedListeners.size());
}
}
/**
*
*/
public void unregisterDataChangedListener(OnDataChangedListener listener) {
dataChangedListeners.remove(listener);
Log.d(TAG, "取消注册数据变化监听器,当前监听器数量: " + dataChangedListeners.size());
}
/**
*
*/
private void notifyDataChanged() {
for (OnDataChangedListener listener : dataChangedListeners) {
listener.onDataChanged();
}
}
/**
*
*/
private void notifyProductWantCountChanged(String productId, int newCount) {
for (OnDataChangedListener listener : dataChangedListeners) {
listener.onProductWantCountChanged(productId, newCount);
}
}
/**
*
*/
private void loadItemsFromStorage() {
String itemsJson = sharedPreferences.getString(KEY_ITEMS, null);
if (itemsJson != null) {
Type type = new TypeToken<List<Item>>(){}.getType();
List<Item> savedItems = gson.fromJson(itemsJson, type);
if (savedItems != null && !savedItems.isEmpty()) {
itemList = savedItems;
Log.d(TAG, "从本地存储加载商品数据,共" + itemList.size() + "个商品");
} else {
initSampleData();
saveItemsToStorage();
}
} else {
initSampleData();
saveItemsToStorage();
}
}
/**
*
*/
private void saveItemsToStorage() {
if (itemList != null) {
String itemsJson = gson.toJson(itemList);
sharedPreferences.edit().putString(KEY_ITEMS, itemsJson).apply();
Log.d(TAG, "商品数据已保存到本地存储,共" + itemList.size() + "个商品");
}
}
/**
*
*/
private void initSampleData() {
// 示例商品1 - 数码产品
Item item1 = new Item();
item1.setId("item_001");
item1.setTitle("全新iPhone 13 Pro");
item1.setDescription("全新未拆封256GB远峰蓝色国行正品带发票");
item1.setPrice(6999.00);
item1.setCategory("数码产品");
item1.setLocation("北京");
item1.setContact("138****1234");
item1.setPublishTime(System.currentTimeMillis() - 2 * 60 * 60 * 1000); // 2小时前发布
item1.setWantCount(5); // 设置初始想要人数
itemList.add(item1);
// 示例商品2 - 数码产品
Item item2 = new Item();
item2.setId("item_002");
item2.setTitle("二手笔记本电脑");
item2.setDescription("联想小新i5处理器8GB内存256GB固态硬盘95成新");
item2.setPrice(2500.00);
item2.setCategory("数码产品");
item2.setLocation("上海");
item2.setContact("微信abc123");
item2.setPublishTime(System.currentTimeMillis() - 5 * 60 * 60 * 1000); // 5小时前发布
item2.setWantCount(3); // 设置初始想要人数
itemList.add(item2);
// 示例商品3 - 服装鞋帽
Item item3 = new Item();
item3.setId("item_003");
item3.setTitle("品牌运动鞋");
item3.setDescription("耐克运动鞋42码只穿过几次几乎全新原盒在");
item3.setPrice(299.00);
item3.setCategory("服装鞋帽");
item3.setLocation("广州");
item3.setContact("159****5678");
item3.setPublishTime(System.currentTimeMillis() - 24 * 60 * 60 * 1000); // 1天前发布
item3.setWantCount(8); // 设置初始想要人数
itemList.add(item3);
// 示例商品4 - 图书文具
Item item4 = new Item();
item4.setId("item_004");
item4.setTitle("Java编程思想");
item4.setDescription("Java编程思想第5版几乎全新无笔记无划痕");
item4.setPrice(45.00);
item4.setCategory("图书文具");
item4.setLocation("杭州");
item4.setContact("QQ123456789");
item4.setPublishTime(System.currentTimeMillis() - 3 * 24 * 60 * 60 * 1000); // 3天前发布
item4.setWantCount(12); // 设置初始想要人数
itemList.add(item4);
}
/**
*
* @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;
// 保存到本地存储
saveItemsToStorage();
Log.d(TAG, "从API成功获取商品数据共" + items.size() + "个商品");
// 回调成功
if (listener != null) {
listener.onDataLoaded(items);
}
// 通知数据变化
notifyDataChanged();
} else {
// 返回空数据
if (listener != null) {
listener.onDataLoaded(new ArrayList<>());
}
}
} else {
Log.e(TAG, "API请求失败使用本地数据");
// 如果API请求失败使用本地缓存数据
if (listener != null) {
listener.onDataLoaded(itemList);
listener.onError("网络请求失败,显示本地数据");
}
}
}
@Override
public void onFailure(Call<BaseResponse<List<ProductResponse>>> call, Throwable t) {
Log.e(TAG, "网络请求失败: " + t.getMessage());
// 网络请求失败,使用本地缓存数据
if (listener != null) {
listener.onDataLoaded(itemList);
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());
// 设置默认想要人数
item.setWantCount(0);
// 设置默认位置,因为 ProductResponse 可能没有 location 字段
item.setLocation("北京"); // 或者从其他地方获取,或者设置为空
items.add(item);
}
return items;
}
/**
*
* @param item
*/
public void addItem(Item item) {
// 为新商品生成ID
item.setId("item_" + System.currentTimeMillis());
// 设置发布时间
item.setPublishTime(System.currentTimeMillis());
// 设置初始想要人数
if (item.getWantCount() == 0) {
item.setWantCount(0);
}
// 将新商品添加到列表最前面(最新发布的显示在最前面)
itemList.add(0, item);
// 保存到本地存储
saveItemsToStorage();
Log.d("DataManager", "成功添加新商品:" + item.getTitle() + ",当前商品总数:" + itemList.size());
// 通知数据变化
notifyDataChanged();
}
/**
*
* @param productId ID
* @param wantCount
*/
public void updateProductWantCount(String productId, int wantCount) {
boolean found = false;
for (Item item : itemList) {
if (item.getId().equals(productId)) {
item.setWantCount(wantCount);
found = true;
// 保存到本地存储
saveItemsToStorage();
Log.d(TAG, "更新商品 " + productId + " 的想要人数为: " + wantCount);
// 通知想要人数变化
notifyProductWantCountChanged(productId, wantCount);
break;
}
}
if (found) {
// 同时通知整体数据变化
notifyDataChanged();
}
}
/**
*
* @param productId ID
* @return
*/
public int getProductWantCount(String productId) {
for (Item item : itemList) {
if (item.getId().equals(productId)) {
return item.getWantCount();
}
}
return 0;
}
/**
* Product使
*/
public List<Product> getAllProducts() {
List<Product> products = new ArrayList<>();
Log.d("DataManager", "开始转换 " + itemList.size() + " 个Item到Product");
for (Item item : itemList) {
products.add(convertItemToProduct(item));
}
Log.d("DataManager", "转换完成,返回 " + products.size() + " 个Product");
return products;
}
/**
* ID
* @param id ID
* @return null
*/
public Item getItemById(String id) {
for (Item item : itemList) {
if (item.getId().equals(id)) {
return item;
}
}
return null;
}
/**
*
* @param category
* @return
*/
public List<Item> getItemsByCategory(String category) {
List<Item> result = new ArrayList<>();
for (Item item : itemList) {
if (item.getCategory().equals(category)) {
result.add(item);
}
}
return result;
}
/**
*
* @param keyword
* @return
*/
public List<Item> searchItems(String keyword) {
List<Item> result = new ArrayList<>();
for (Item item : itemList) {
if (item.getTitle().toLowerCase().contains(keyword.toLowerCase()) ||
item.getDescription().toLowerCase().contains(keyword.toLowerCase()) ||
item.getCategory().toLowerCase().contains(keyword.toLowerCase())) {
result.add(item);
}
}
Log.d(TAG, "搜索关键词: " + keyword + ", 找到 " + result.size() + " 个结果");
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);
}
}
});
}
/**
*
* @return
*/
public int getItemCount() {
return itemList.size();
}
/**
*
*/
public void clearAll() {
itemList.clear();
saveItemsToStorage();
// 通知数据变化
notifyDataChanged();
}
/**
* ItemProduct
*/
public Product convertItemToProduct(Item item) {
Product product = new Product();
product.setId(item.getId());
product.setName(item.getTitle());
product.setDescription(item.getDescription());
product.setCategory(item.getCategory());
product.setPrice(item.getPrice());
product.setLocation(item.getLocation());
product.setContact(item.getContact());
// 设置想要人数从Item中获取
product.setWantCount(item.getWantCount());
product.setSellerRating("卖家信用良好");
product.setFreeShipping(false);
return product;
}
/**
* Product
*/
public Product addItemAndGetProduct(Item item) {
addItem(item);
return convertItemToProduct(item);
}
}

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@android:color/white" />
<stroke
android:width="1dp"
android:color="#e0e0e0" />
<corners android:radius="4dp" />
</shape>

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#f5f5f5" />
<corners android:radius="24dp" />
<stroke
android:width="1dp"
android:color="#e0e0e0" />
</shape>

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@android:color/white" />
<corners
android:topLeftRadius="0dp"
android:topRightRadius="16dp"
android:bottomLeftRadius="16dp"
android:bottomRightRadius="16dp" />
<stroke
android:width="1dp"
android:color="#e0e0e0" />
</shape>

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#07C160" />
<corners
android:topLeftRadius="16dp"
android:topRightRadius="0dp"
android:bottomLeftRadius="16dp"
android:bottomRightRadius="16dp" />
</shape>

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#99000000" />
<corners android:radius="12dp" />
</shape>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#FFFFFF" />
<stroke android:width="1dp" android:color="#E0E0E0" />
<corners android:radius="8dp" />
</shape>

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#f0f0f0" />
<corners android:radius="4dp" />
<stroke
android:width="1dp"
android:color="#e0e0e0" />
</shape>

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#f5f5f5" />
<corners android:radius="18dp" />
</shape>

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#FF5000" />
<corners android:radius="9dp" />
</shape>

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#87CEEB"/>
<corners android:radius="12dp"/>
</shape>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@android:color/white" />
<stroke android:width="1dp" android:color="#e0e0e0" />
<corners android:radius="4dp" />
</shape>

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#2196F3"/>
<corners android:radius="16dp"/>
</shape>

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#2196F3"/>
<corners android:radius="12dp"/>
</shape>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#f8f9fa" />
<corners android:radius="8dp" />
<stroke android:width="1dp" android:color="#dee2e6" />
</shape>

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#FFFFFF"/>
<corners android:radius="12dp"/>
</shape>

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="#FFFFFFFF" />
<stroke
android:width="1dp"
android:color="#FFE0E0E0" />
<!-- 添加阴影效果 -->
<padding
android:left="2dp"
android:top="2dp"
android:right="2dp"
android:bottom="2dp" />
</shape>

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="12dp" />
<solid android:color="#FF00BCD4" />
<stroke
android:width="0.5dp"
android:color="#FF0097A7" />
<padding
android:left="8dp"
android:top="2dp"
android:right="8dp"
android:bottom="2dp" />
</shape>

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="22dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#F5F5F5"
android:pathData="M21,19V5c0,-1.1 -0.9,-2 -2,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2zM8.5,13.5l2.5,3.01L14.5,12l4.5,6H5l3.5,-4.5z"/>
</vector>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@android:color/white" />
<corners android:radius="16dp" />
<stroke android:width="1dp" android:color="#f0f0f0" />
</shape>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@android:color/white" />
<stroke android:width="1dp" android:color="#e0e0e0" />
<corners android:radius="4dp" />
</shape>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#f8f9fa" />
<corners android:radius="20dp" />
<stroke android:width="1dp" android:color="#e9ecef" />
</shape>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#fff2f2" />
<corners android:radius="20dp" />
<stroke android:width="1dp" android:color="#ff6b35" />
</shape>

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<gradient
android:startColor="#E3F2FD"
android:endColor="#BBDEFB"
android:angle="90"/>
</shape>

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF666666"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2z M12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8z"/>
<path
android:fillColor="#FF666666"
android:pathData="M12,6c-3.31,0 -6,2.69 -6,6s2.69,6 6,6 6,-2.69 6,-6 -2.69,-6 -6,-6z M12,16c-2.21,0 -4,-1.79 -4,-4s1.79,-4 4,-4 4,1.79 4,4 -1.79,4 -4,4z"/>
<path
android:fillColor="#FF666666"
android:pathData="M12,9.5c-1.38,0 -2.5,1.12 -2.5,2.5s1.12,2.5 2.5,2.5 2.5,-1.12 2.5,-2.5 -1.12,-2.5 -2.5,-2.5z"/>
</vector>

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/white">
<path
android:fillColor="@android:color/white"
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</vector>

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:strokeColor="#FF666666"
android:strokeWidth="2"
android:fillColor="@android:color/transparent"
android:pathData="M9,6l6,6l-6,6"/>
</vector>

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFE44D"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2z M12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8z"/>
<path
android:fillColor="#FFE44D"
android:pathData="M12.5,7H11v6l5.25,3.15l0.75,-1.23l-4.5,-2.67z"/>
<path
android:fillColor="#FFE44D"
android:pathData="M7,12.5h10v-1H7z"/>
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFE44D"
android:pathData="M20,6h-2.18c0.11,-0.31 0.18,-0.65 0.18,-1c0,-1.66 -1.34,-3 -3,-3c-1.05,0 -1.96,0.54 -2.5,1.35C12.96,2.54 12.05,2 11,2C9.34,2 8,3.34 8,5c0,0.35 0.07,0.69 0.18,1H4c-1.11,0 -1.99,0.89 -1.99,2L2,19c0,1.11 0.89,2 2,2h16c1.11,0 2,-0.89 2,-2V8C22,6.89 21.11,6 20,6z M15,4c0.55,0 1,0.45 1,1s-0.45,1 -1,1s-1,-0.45 -1,-1S14.45,4 15,4z M11,4c0.55,0 1,0.45 1,1s-0.45,1 -1,1s-1,-0.45 -1,-1S10.45,4 11,4z M4,19v-6h6v6H4z M14,19v-6h6v6H14z"/>
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF757575"
android:pathData="M12,12c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zM12,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z"/>
</vector>

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF4CAF50"
android:pathData="M20,2H4C2.9,2 2,2.9 2,4v12c0,1.1 0.9,2 2,2h4v3c0,0.55 0.45,1 1,1h0.5c0.25,0 0.49,-0.09 0.67,-0.26L13.9,18H20c1.1,0 2,-0.9 2,-2V4C22,2.9 21.1,2 20,2z M20,16h-6.09c-0.18,0 -0.35,0.06 -0.49,0.17l-1.42,1.42V16H4V4h16V16z"/>
<path
android:fillColor="#FF4CAF50"
android:pathData="M11,11h2v2h-2z M11,7h2v2h-2z"/>
</vector>

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFE44D"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2z M12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8z"/>
<path
android:fillColor="#FFE44D"
android:pathData="M11,16h2v2h-2z"/>
<path
android:fillColor="#FFE44D"
android:pathData="M12,6c-2.21,0 -4,1.79 -4,4h2c0,-1.1 0.9,-2 2,-2s2,0.9 2,2c0,2 -3,1.75 -3,5h2c0,-2.25 3,-2.5 3,-5C16,7.79 14.21,6 12,6z"/>
</vector>

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFE44D"
android:pathData="M16,11c1.66,0 2.99,-1.34 2.99,-3S17.66,5 16,5c-1.66,0 -3,1.34 -3,3S14.34,11 16,11z M8,11c1.66,0 2.99,-1.34 2.99,-3S9.66,5 8,5C6.34,5 5,6.34 5,8S6.34,11 8,11z M8,13c-2.33,0 -7,1.17 -7,3.5V19h14v-2.5C15,14.17 10.33,13 8,13z M16,13c-0.29,0 -0.62,0.02 -0.97,0.05c1.16,0.84 1.97,1.97 1.97,3.45V19h6v-2.5C23,14.17 18.33,13 16,13z"/>
<path
android:fillColor="#FFE44D"
android:pathData="M20,2H4C2.9,2 2,2.9 2,4v12c0,1.1 0.9,2 2,2h4v-2H4V4h16v12h-4v2h4c1.1,0 2,-0.9 2,-2V4C22,2.9 21.1,2 20,2z"/>
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF666666"
android:pathData="M17,7l-1.41,1.41L18.17,11H8v2h10.17l-2.58,2.58L17,17l5,-5L17,7z M4,5h8V3H4C2.9,3 2,3.9 2,5v14c0,1.1 0.9,2 2,2h8v-2H4V5z"/>
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF666666"
android:pathData="M6,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z M18,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z M12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z"/>
</vector>

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFE44D"
android:pathData="M19,3H5C3.9,3 3,3.9 3,5v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5C21,3.9 20.1,3 19,3zM19,19H5V5h14V19z"/>
<path
android:fillColor="#FFE44D"
android:pathData="M8.5,15H6.5l4,-7 4,7h-2l-1.5,-2.5L8.5,15z"/>
<path
android:fillColor="#FFE44D"
android:pathData="M17,12.5h-4V11h4V12.5z"/>
</vector>

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="12dp"
android:height="12dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF5000"
android:pathData="M12,2L4,5v6.09c0,5.05 3.41,9.76 8,10.91c4.59,-1.15 8,-5.86 8,-10.91V5L12,2z"/>
</vector>

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFE44D"
android:pathData="M12,22c1.1,0 2,-0.9 2,-2h-4C10,21.1 10.9,22 12,22z M18,16v-5c0,-3.07 -1.64,-5.64 -4.5,-6.32V4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.63,5.36 6,7.92 6,11v5l-2,2v1h16v-1L18,16z"/>
<path
android:fillColor="#FFE44D"
android:pathData="M15,9H9v2h1v3h4v-3h1V9z"/>
</vector>

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#BDBDBD"
android:pathData="M19,5v14H5V5h14m0-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2z"/>
<path
android:fillColor="#BDBDBD"
android:pathData="M12,12c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM14,16v2h-4v-2c0,-1.1 0.9,-2 2,-2s2,0.9 2,2z"/>
</vector>

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFE44D"
android:pathData="M13,3c-4.97,0 -9,4.03 -9,9H1l3.89,3.89l0.07,0.14L9,12H6c0,-3.87 3.13,-7 7,-7s7,3.13 7,7s-3.13,7 -7,7c-1.93,0 -3.68,-0.79 -4.94,-2.06l-1.42,1.42C8.27,19.99 10.51,21 13,21c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9z"/>
<path
android:fillColor="#FFE44D"
android:pathData="M12,8v5l4.25,2.52l0.77,-1.28l-3.52,-2.09V8z"/>
</vector>

@ -0,0 +1,39 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF666666"
android:pathData="M3,11h8V3H3V11zM5,5h4v4H5V5z"/>
<path
android:fillColor="#FF666666"
android:pathData="M3,21h8v-8H3V21zM5,15h4v4H5V15z"/>
<path
android:fillColor="#FF666666"
android:pathData="M13,3v8h8V3H13zM19,9h-4V5h4V9z"/>
<path
android:fillColor="#FF666666"
android:pathData="M21,19h-2v2h2V19z"/>
<path
android:fillColor="#FF666666"
android:pathData="M13,13h2v2h-2V13z"/>
<path
android:fillColor="#FF666666"
android:pathData="M15,15h2v2h-2V15z"/>
<path
android:fillColor="#FF666666"
android:pathData="M13,17h2v2h-2V17z"/>
<path
android:fillColor="#FF666666"
android:pathData="M15,19h2v2h-2V19z"/>
<path
android:fillColor="#FF666666"
android:pathData="M17,17h2v2h-2V17z"/>
<path
android:fillColor="#FF666666"
android:pathData="M19,15h2v2h-2V15z"/>
<path
android:fillColor="#FF666666"
android:pathData="M17,13h2v2h-2V13z"/>
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF666666"
android:pathData="M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.82,11.69 4.82,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z"/>
</vector>

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#2196F3"
android:pathData="M19,6h-2c0,-2.8 -2.2,-5 -5,-5S7,3.2 7,6H5C3.9,6 3,6.9 3,8v12c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V8C21,6.9 20.1,6 19,6z M12,3c1.7,0 3,1.3 3,3H9C9,4.3 10.3,3 12,3z M19,20H5V8h2v2c0,0.6 0.4,1 1,1s1,-0.4 1,-1V8h6v2c0,0.6 0.4,1 1,1s1,-0.4 1,-1V8h2V20z"/>
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFE44D"
android:pathData="M12,17.27L18.18,21l-1.64,-7.03L22,9.24l-7.19,-0.61L12,2L9.19,8.63L2,9.24l5.46,4.73L5.82,21z"/>
</vector>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#F5F5F5"/>
<corners android:radius="8dp"/>
<stroke android:width="1dp" android:color="#E3F2FD"/>
</shape>

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="12dp" />
<!-- 银白色渐变 -->
<gradient
android:type="linear"
android:startColor="#FFE8E8E8"
android:centerColor="#FFD6D6D6"
android:endColor="#FFC0C0C0"
android:angle="45" />
<stroke
android:width="0.8dp"
android:color="#FFA0A0A0" />
<padding
android:left="8dp"
android:top="2dp"
android:right="8dp"
android:bottom="2dp" />
</shape>

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#2196F3" />
<corners android:radius="8dp" />
</shape>

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@android:color/white" />
<corners android:radius="@dimen/product_card_corner_radius" />
<stroke
android:width="1dp"
android:color="#E3F2FD" />
</shape>

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 阴影 -->
<item>
<shape android:shape="rectangle">
<solid android:color="#E3F2FD" />
<corners android:radius="@dimen/product_card_corner_radius" />
</shape>
</item>
<!-- 内容 -->
<item android:top="2dp" android:bottom="2dp" android:left="2dp" android:right="2dp">
<shape android:shape="rectangle">
<solid android:color="@android:color/white" />
<corners android:radius="@dimen/product_card_corner_radius" />
<stroke
android:width="1dp"
android:color="#BBDEFB" />
</shape>
</item>
</layer-list>

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="25dp" />
<!-- 渐变背景 -->
<gradient
android:type="linear"
android:startColor="#FFFFFF"
android:endColor="#F8F9FA"
android:angle="90" />
<stroke
android:width="1dp"
android:color="#E0E0E0" />
</shape>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#FFFFFF"/>
<corners android:radius="24dp"/>
<stroke android:width="1dp" android:color="#E3F2FD"/>
</shape>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#FFFFFF"/>
<corners android:radius="18dp"/>
<stroke android:width="1dp" android:color="#E0E0E0"/>
</shape>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#E3F2FD"/>
<corners android:radius="18dp"/>
<stroke android:width="1dp" android:color="#2196F3"/>
</shape>

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<!-- 圆角 -->
<corners android:radius="16dp" />
<!-- 背景色 - 白色 -->
<solid android:color="@color/white" />
<!-- 灰色边框 -->
<stroke
android:width="1dp"
android:color="@color/gray_300" />
<!-- 内边距 -->
<padding
android:left="8dp"
android:top="4dp"
android:right="8dp"
android:bottom="4dp" />
</shape>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#f5f5f5" />
<stroke android:width="1dp" android:color="#e0e0e0" />
<corners android:radius="16dp" />
</shape>

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="16dp" />
<!-- 渐变背景 -->
<gradient
android:type="linear"
android:startColor="@color/primary_blue_light"
android:endColor="@color/primary_blue"
android:angle="90" />
<stroke
android:width="1dp"
android:color="@color/primary_blue_dark" />
</shape>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#fff2f2" />
<stroke android:width="1dp" android:color="#ff6b35" />
<corners android:radius="16dp" />
</shape>

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#E3F2FD"/>
<corners android:radius="10dp"/>
</shape>

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#E3F2FD" />
<corners android:radius="6dp" />
<stroke
android:width="1dp"
android:color="#2196F3" />
</shape>

@ -0,0 +1,71 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#f5f5f5">
<!-- 标题栏 -->
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="@android:color/white"
android:elevation="2dp">
<ImageView
android:id="@+id/ivBack"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@android:drawable/ic_menu_close_clear_cancel"
android:layout_centerVertical="true"
android:layout_marginStart="16dp" />
<TextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="聊天"
android:textSize="18sp"
android:textStyle="bold"
android:layout_centerInParent="true" />
</RelativeLayout>
<!-- 聊天消息列表 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvChatMessages"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<!-- 输入框区域 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/white"
android:orientation="horizontal"
android:padding="8dp"
android:elevation="4dp">
<EditText
android:id="@+id/etMessage"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@drawable/bg_chat_input"
android:hint="输入消息..."
android:maxLines="3"
android:padding="12dp"
android:textSize="14sp" />
<ImageButton
android:id="@+id/btnSend"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="?android:attr/selectableItemBackground"
android:src="@android:drawable/ic_menu_send"
android:layout_marginStart="8dp" />
</LinearLayout>
</LinearLayout>

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />

@ -0,0 +1,457 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/dialog_background"
android:padding="0dp">
<!-- 标题栏 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:padding="20dp"
android:background="#fafafa">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="筛选"
android:textColor="#333333"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:id="@+id/btn_reset"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="重置"
android:textColor="#666666"
android:textSize="14sp"
android:padding="8dp"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true" />
</LinearLayout>
<!-- 内容区域 -->
<ScrollView
android:layout_width="match_parent"
android:layout_height="400dp"
android:padding="20dp">
<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="价格区间"
android:textColor="#333333"
android:textSize="16sp"
android:textStyle="bold"
android:layout_marginBottom="12dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="24dp">
<EditText
android:id="@+id/et_min_price"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_weight="1"
android:hint="最低价"
android:inputType="numberDecimal"
android:background="@drawable/edittext_border"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:textSize="14sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="至"
android:textColor="#666666"
android:textSize="14sp"
android:paddingHorizontal="12dp" />
<EditText
android:id="@+id/et_max_price"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_weight="1"
android:hint="最高价"
android:inputType="numberDecimal"
android:background="@drawable/edittext_border"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:textSize="14sp" />
</LinearLayout>
<!-- 商品类别 -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="商品类别"
android:textColor="#333333"
android:textSize="16sp"
android:textStyle="bold"
android:layout_marginBottom="12dp" />
<LinearLayout
android:id="@+id/category_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="24dp">
<!-- 第一行类别 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="8dp">
<TextView
android:id="@+id/category_digital"
android:layout_width="0dp"
android:layout_height="36dp"
android:layout_weight="1"
android:layout_marginEnd="8dp"
android:gravity="center"
android:text="数码产品"
android:textColor="#666666"
android:textSize="13sp"
android:background="@drawable/filter_tag_normal"
android:clickable="true"
android:focusable="true" />
<TextView
android:id="@+id/category_clothing"
android:layout_width="0dp"
android:layout_height="36dp"
android:layout_weight="1"
android:layout_marginEnd="8dp"
android:gravity="center"
android:text="服装鞋帽"
android:textColor="#666666"
android:textSize="13sp"
android:background="@drawable/filter_tag_normal"
android:clickable="true"
android:focusable="true" />
<TextView
android:id="@+id/category_home"
android:layout_width="0dp"
android:layout_height="36dp"
android:layout_weight="1"
android:gravity="center"
android:text="家居日用"
android:textColor="#666666"
android:textSize="13sp"
android:background="@drawable/filter_tag_normal"
android:clickable="true"
android:focusable="true" />
</LinearLayout>
<!-- 第二行类别 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="8dp">
<TextView
android:id="@+id/category_books"
android:layout_width="0dp"
android:layout_height="36dp"
android:layout_weight="1"
android:layout_marginEnd="8dp"
android:gravity="center"
android:text="图书文具"
android:textColor="#666666"
android:textSize="13sp"
android:background="@drawable/filter_tag_normal"
android:clickable="true"
android:focusable="true" />
<TextView
android:id="@+id/category_beauty"
android:layout_width="0dp"
android:layout_height="36dp"
android:layout_weight="1"
android:layout_marginEnd="8dp"
android:gravity="center"
android:text="美妆个护"
android:textColor="#666666"
android:textSize="13sp"
android:background="@drawable/filter_tag_normal"
android:clickable="true"
android:focusable="true" />
<TextView
android:id="@+id/category_sports"
android:layout_width="0dp"
android:layout_height="36dp"
android:layout_weight="1"
android:gravity="center"
android:text="运动户外"
android:textColor="#666666"
android:textSize="13sp"
android:background="@drawable/filter_tag_normal"
android:clickable="true"
android:focusable="true" />
</LinearLayout>
<!-- 第三行类别 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/category_other"
android:layout_width="0dp"
android:layout_height="36dp"
android:layout_weight="1"
android:layout_marginEnd="8dp"
android:gravity="center"
android:text="其他"
android:textColor="#666666"
android:textSize="13sp"
android:background="@drawable/filter_tag_normal"
android:clickable="true"
android:focusable="true" />
<!-- 占位空间,保持布局平衡 -->
<View
android:layout_width="0dp"
android:layout_height="36dp"
android:layout_weight="1"
android:layout_marginEnd="8dp" />
<View
android:layout_width="0dp"
android:layout_height="36dp"
android:layout_weight="1" />
</LinearLayout>
</LinearLayout>
<!-- 区域筛选 -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="区域"
android:textColor="#333333"
android:textSize="16sp"
android:textStyle="bold"
android:layout_marginBottom="12dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="32dp">
<!-- 第一行区域 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="8dp">
<TextView
android:id="@+id/region_beijing"
android:layout_width="0dp"
android:layout_height="36dp"
android:layout_weight="1"
android:layout_marginEnd="8dp"
android:gravity="center"
android:text="北京"
android:textColor="#666666"
android:textSize="13sp"
android:background="@drawable/filter_tag_normal"
android:clickable="true"
android:focusable="true" />
<TextView
android:id="@+id/region_shanghai"
android:layout_width="0dp"
android:layout_height="36dp"
android:layout_weight="1"
android:layout_marginEnd="8dp"
android:gravity="center"
android:text="上海"
android:textColor="#666666"
android:textSize="13sp"
android:background="@drawable/filter_tag_normal"
android:clickable="true"
android:focusable="true" />
<TextView
android:id="@+id/region_guangzhou"
android:layout_width="0dp"
android:layout_height="36dp"
android:layout_weight="1"
android:gravity="center"
android:text="广州"
android:textColor="#666666"
android:textSize="13sp"
android:background="@drawable/filter_tag_normal"
android:clickable="true"
android:focusable="true" />
</LinearLayout>
<!-- 第二行区域 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="8dp">
<TextView
android:id="@+id/region_shenzhen"
android:layout_width="0dp"
android:layout_height="36dp"
android:layout_weight="1"
android:layout_marginEnd="8dp"
android:gravity="center"
android:text="深圳"
android:textColor="#666666"
android:textSize="13sp"
android:background="@drawable/filter_tag_normal"
android:clickable="true"
android:focusable="true" />
<TextView
android:id="@+id/region_hangzhou"
android:layout_width="0dp"
android:layout_height="36dp"
android:layout_weight="1"
android:layout_marginEnd="8dp"
android:gravity="center"
android:text="杭州"
android:textColor="#666666"
android:textSize="13sp"
android:background="@drawable/filter_tag_normal"
android:clickable="true"
android:focusable="true" />
<TextView
android:id="@+id/region_chengdu"
android:layout_width="0dp"
android:layout_height="36dp"
android:layout_weight="1"
android:gravity="center"
android:text="成都"
android:textColor="#666666"
android:textSize="13sp"
android:background="@drawable/filter_tag_normal"
android:clickable="true"
android:focusable="true" />
</LinearLayout>
<!-- 第三行区域 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/region_wuhan"
android:layout_width="0dp"
android:layout_height="36dp"
android:layout_weight="1"
android:layout_marginEnd="8dp"
android:gravity="center"
android:text="武汉"
android:textColor="#666666"
android:textSize="13sp"
android:background="@drawable/filter_tag_normal"
android:clickable="true"
android:focusable="true" />
<TextView
android:id="@+id/region_other"
android:layout_width="0dp"
android:layout_height="36dp"
android:layout_weight="1"
android:layout_marginEnd="8dp"
android:gravity="center"
android:text="其他"
android:textColor="#666666"
android:textSize="13sp"
android:background="@drawable/filter_tag_normal"
android:clickable="true"
android:focusable="true" />
<!-- 占位空间,保持布局平衡 -->
<View
android:layout_width="0dp"
android:layout_height="36dp"
android:layout_weight="1" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</ScrollView>
<!-- 底部按钮 -->
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#f0f0f0" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp">
<Button
android:id="@+id/btn_cancel"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_weight="1"
android:layout_marginEnd="8dp"
android:text="取消"
android:textColor="#666666"
android:textSize="16sp"
android:background="@drawable/button_secondary" />
<Button
android:id="@+id/btn_confirm"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_weight="1"
android:layout_marginStart="8dp"
android:text="确定"
android:textColor="@android:color/white"
android:textSize="16sp"
android:textStyle="bold"
android:background="@drawable/button_primary" />
</LinearLayout>
</LinearLayout>

@ -0,0 +1,132 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@android:color/white"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="选择区域"
android:textColor="#333333"
android:textSize="18sp"
android:textStyle="bold"
android:layout_marginBottom="16dp" />
<TextView
android:id="@+id/region_all"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="全区域"
android:textColor="#333333"
android:textSize="16sp"
android:padding="12dp"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#f0f0f0"
android:layout_marginVertical="8dp" />
<TextView
android:id="@+id/region_beijing"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="北京"
android:textColor="#333333"
android:textSize="16sp"
android:padding="12dp"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true" />
<TextView
android:id="@+id/region_shanghai"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="上海"
android:textColor="#333333"
android:textSize="16sp"
android:padding="12dp"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true" />
<TextView
android:id="@+id/region_guangzhou"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="广州"
android:textColor="#333333"
android:textSize="16sp"
android:padding="12dp"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true" />
<TextView
android:id="@+id/region_shenzhen"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="深圳"
android:textColor="#333333"
android:textSize="16sp"
android:padding="12dp"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true" />
<TextView
android:id="@+id/region_hangzhou"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="杭州"
android:textColor="#333333"
android:textSize="16sp"
android:padding="12dp"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true" />
<TextView
android:id="@+id/region_chengdu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="成都"
android:textColor="#333333"
android:textSize="16sp"
android:padding="12dp"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true" />
<TextView
android:id="@+id/region_wuhan"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="武汉"
android:textColor="#333333"
android:textSize="16sp"
android:padding="12dp"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true" />
<TextView
android:id="@+id/region_other"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="其他"
android:textColor="#333333"
android:textSize="16sp"
android:padding="12dp"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true" />
</LinearLayout>

@ -0,0 +1,142 @@
<?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"
android:background="#f5f5f5">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- 商品图片 -->
<ImageView
android:id="@+id/ivItemImage"
android:layout_width="match_parent"
android:layout_height="200dp"
android:scaleType="centerCrop"
android:background="@drawable/bg_image_border"
android:src="@mipmap/ic_launcher" />
<!-- 商品基本信息 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@android:color/white"
android:padding="16dp"
android:layout_marginTop="8dp">
<TextView
android:id="@+id/tvTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="商品标题"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvPrice"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="¥0.00"
android:textColor="#ff4444"
android:textSize="20sp"
android:textStyle="bold"
android:layout_marginTop="8dp" />
</LinearLayout>
<!-- 商品详情 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@android:color/white"
android:padding="16dp"
android:layout_marginTop="8dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="商品详情"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvDescription"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="商品描述"
android:textSize="14sp"
android:layout_marginTop="8dp"
android:lineSpacingExtra="4dp" />
</LinearLayout>
<!-- 商品信息 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@android:color/white"
android:padding="16dp"
android:layout_marginTop="8dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="商品信息"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvCategory"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="分类:"
android:textSize="14sp"
android:layout_marginTop="8dp" />
<TextView
android:id="@+id/tvLocation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="位置:"
android:textSize="14sp"
android:layout_marginTop="4dp" />
<TextView
android:id="@+id/tvContact"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="联系方式:"
android:textSize="14sp"
android:layout_marginTop="4dp" />
<TextView
android:id="@+id/tvPublishTime"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="发布时间:"
android:textSize="14sp"
android:layout_marginTop="4dp" />
</LinearLayout>
<!-- 联系卖家按钮 -->
<Button
android:id="@+id/btnContact"
android:layout_width="match_parent"
android:layout_height="50dp"
android:text="联系卖家"
android:textSize="16sp"
android:textStyle="bold"
android:background="@drawable/button_blue"
android:textColor="@android:color/white"
android:layout_marginTop="16dp" />
</LinearLayout>
</ScrollView>

@ -0,0 +1,148 @@
<?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"
android:background="#f5f5f5">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- 图片上传区域 -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="上传图片"
android:textSize="16sp"
android:textStyle="bold"
android:layout_marginBottom="8dp" />
<GridView
android:id="@+id/gridViewImages"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:numColumns="3"
android:verticalSpacing="8dp"
android:horizontalSpacing="8dp"
android:stretchMode="columnWidth"
android:background="@drawable/bg_edittext"
android:padding="8dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="最多可上传9张图片"
android:textSize="12sp"
android:textColor="#666"
android:layout_marginTop="4dp"
android:layout_marginBottom="16dp" />
<!-- 商品信息区域 -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="商品信息"
android:textSize="16sp"
android:textStyle="bold"
android:layout_marginBottom="8dp" />
<EditText
android:id="@+id/etTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="商品标题"
android:maxLines="1"
android:background="@drawable/bg_edittext"
android:padding="12dp" />
<EditText
android:id="@+id/etDescription"
android:layout_width="match_parent"
android:layout_height="120dp"
android:hint="商品描述"
android:gravity="top"
android:maxLines="5"
android:background="@drawable/bg_edittext"
android:padding="12dp"
android:layout_marginTop="8dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="8dp">
<EditText
android:id="@+id/etPrice"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="价格"
android:inputType="numberDecimal"
android:background="@drawable/bg_edittext"
android:padding="12dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="元"
android:textSize="14sp"
android:gravity="center_vertical"
android:paddingStart="8dp"
android:paddingEnd="8dp" />
</LinearLayout>
<!-- 分类和位置 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="8dp">
<Spinner
android:id="@+id/spinnerCategory"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@drawable/bg_edittext"
android:padding="8dp" />
<Spinner
android:id="@+id/spinnerLocation"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="8dp"
android:background="@drawable/bg_edittext"
android:padding="8dp" />
</LinearLayout>
<!-- 联系方式 -->
<EditText
android:id="@+id/etContact"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="联系方式(微信/电话)"
android:maxLines="1"
android:background="@drawable/bg_edittext"
android:padding="12dp"
android:layout_marginTop="8dp" />
<!-- 发布按钮 -->
<Button
android:id="@+id/btnPublish"
android:layout_width="match_parent"
android:layout_height="50dp"
android:text="发布商品"
android:textSize="16sp"
android:textStyle="bold"
android:background="@drawable/button_blue"
android:textColor="@android:color/white"
android:layout_marginTop="24dp" />
</LinearLayout>
</ScrollView>

@ -0,0 +1,118 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp">
<!-- 对方消息(左侧) -->
<LinearLayout
android:id="@+id/layoutLeft"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:visibility="gone">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:paddingHorizontal="16dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingEnd="48dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/bg_chat_message_left"
android:orientation="vertical"
android:padding="12dp">
<TextView
android:id="@+id/tvLeftMessage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="对方消息"
android:textColor="@android:color/black"
android:textSize="14sp" />
</LinearLayout>
</LinearLayout>
<TextView
android:id="@+id/tvLeftTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="时间"
android:textColor="#999999"
android:textSize="10sp"
android:layout_marginTop="4dp"
android:layout_marginStart="12dp" />
</LinearLayout>
</LinearLayout>
<!-- 自己消息(右侧) -->
<LinearLayout
android:id="@+id/layoutRight"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:visibility="gone">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:paddingHorizontal="16dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_gravity="end"
android:paddingStart="48dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/bg_chat_message_right"
android:orientation="vertical"
android:padding="12dp">
<TextView
android:id="@+id/tvRightMessage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="我的消息"
android:textColor="@android:color/white"
android:textSize="14sp" />
</LinearLayout>
</LinearLayout>
<TextView
android:id="@+id/tvRightTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="时间"
android:textColor="#999999"
android:textSize="10sp"
android:layout_marginTop="4dp"
android:layout_gravity="end"
android:layout_marginEnd="12dp" />
</LinearLayout>
</LinearLayout>
</LinearLayout>

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="100dp"
android:padding="2dp">
<ImageView
android:id="@+id/imageView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:background="@drawable/bg_image_border" />
<ImageView
android:id="@+id/btnDelete"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_marginTop="4dp"
android:layout_marginEnd="4dp"
android:src="@android:drawable/ic_delete"
android:background="@drawable/bg_delete_button" />
</RelativeLayout>

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="72dp"
android:background="@android:color/white"
android:padding="12dp">
<!-- 白色头像占位 -->
<ImageView
android:id="@+id/ivAvatar"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="@drawable/bg_avatar_placeholder" />
<!-- 官方标识 -->
<ImageView
android:id="@+id/ivOfficial"
android:layout_width="12dp"
android:layout_height="12dp"
android:src="@drawable/ic_official"
android:layout_alignTop="@id/ivAvatar"
android:layout_alignEnd="@id/ivAvatar" />
<!-- 标题 -->
<TextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="标题"
android:textSize="16sp"
android:textStyle="bold"
android:layout_toEndOf="@id/ivAvatar"
android:layout_marginStart="12dp"
android:layout_alignTop="@id/ivAvatar" />
<!-- 内容 -->
<TextView
android:id="@+id/tvContent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="内容"
android:textSize="14sp"
android:textColor="#666666"
android:maxLines="1"
android:ellipsize="end"
android:layout_toEndOf="@id/ivAvatar"
android:layout_below="@id/tvTitle"
android:layout_marginStart="12dp"
android:layout_marginTop="4dp" />
<!-- 时间 -->
<TextView
android:id="@+id/tvTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="时间"
android:textSize="12sp"
android:textColor="#999999"
android:layout_alignParentEnd="true"
android:layout_alignTop="@id/tvTitle" />
<!-- 未读消息计数 -->
<TextView
android:id="@+id/tvUnreadCount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="18dp"
android:background="@drawable/bg_unread_count"
android:textColor="@android:color/white"
android:textSize="10sp"
android:gravity="center"
android:layout_alignParentEnd="true"
android:layout_below="@id/tvTime"
android:layout_marginTop="4dp" />
</RelativeLayout>

@ -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";
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save