commit 641b03e8e24ab908a7491c15bee4132e2be1324a Author: liqi Date: Sun Jan 11 18:14:42 2026 +0800 初始化v1 diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..35410ca --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..f41040f --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..63e9001 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..712ab9d --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..26f4fe2 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/1 b/1 new file mode 100644 index 0000000..b6aa4e5 --- /dev/null +++ b/1 @@ -0,0 +1,118 @@ +# 奶茶店线上订单系统需求规格说明书 + +## 顾客端功能模块 + +### 1. 用户账户管理 +- 实现用户注册功能,支持上传完整个人信息,包括:姓名(必填)、性别(可选)、手机号码(必填,需短信验证)、密码(需符合安全规范)、收货/取餐地址(支持多地址管理,包含省市区、详细地址、联系人、联系电话、是否默认地址)、头像图片(支持JPG/PNG格式,最大2MB) +- 开发登录功能,支持手机号+密码登录及验证码快捷登录 +- 提供个人信息修改界面,允许用户更新除手机号外的所有注册信息,修改手机号需通过原手机号验证 + +### 2. 奶茶产品选购系统 +- 设计产品分类展示功能,按产品类型(如奶茶类、果茶类、咖啡类等)进行分类导航 +- 实现产品详情页,完整展示产品信息:名称、价格、规格(大/中/小杯)、基础配料、产品图片、营养成分、用户评价 +- 开发个性化定制功能,支持: + - 甜度调节(全糖、七分糖、五分糖、三分糖、无糖) + - 冰度选择(正常冰、少冰、去冰、热饮) + - 配料添加/移除(额外收费配料需明确标注价格) +- 实现"加入购物车"功能,支持选择数量(1-10杯)并加入购物车 + +### 3. 购物车管理系统 +- 开发购物车页面,展示所有已选商品,包含:产品名称、定制信息、单价、数量、小计金额 +- 实现购物车商品管理功能: + - 修改商品数量(1-10杯范围内) + - 移除单个商品 + - 清空购物车 + - 选择/取消选择商品 +- 实时计算并展示所选商品的总计金额 + +### 4. 订单下单与支付流程 +- 开发订单确认页面,展示所选商品清单、定制信息、数量、单价、小计、配送费(如适用)、总计金额 +- 实现收货/取餐地址选择功能,支持从地址库选择或临时添加新地址 +- 集成支付系统,支持微信支付和支付宝两种支付方式 +- 开发订单提交功能,提交后生成唯一订单号,跳转至支付页面 +- 实现支付状态同步,支付成功后跳转至订单成功页面,支付失败提供重试入口 + +### 5. 订单信息查询系统 +- 开发订单中心,按状态分类展示订单: + - 待支付订单(30分钟内未支付自动取消) + - 制作中订单 + - 待取餐/配送中订单 + - 已完成订单 + - 已取消订单 +- 实现订单详情页,展示完整订单信息:订单号、下单时间、商品清单、定制信息、数量、单价、总计金额、支付方式、支付时间、取餐/配送地址、订单状态 + +### 6. 公告信息展示系统 +- 开发公告列表页面,按发布时间倒序展示门店公告 +- 实现公告详情页,完整展示公告内容、发布时间、发布人 +- 支持公告分类:活动公告、通知公告、新品上市等 + +### 7. 在线留言反馈系统 +- 开发留言提交功能,支持用户选择留言类型(点单问题、取餐问题、产品问题、其他建议)并输入详细内容(最多500字),可上传图片(最多3张) +- 实现留言记录查询功能,展示历史留言内容、提交时间、处理状态、商家回复 +- 开发留言通知功能,当商家回复后推送消息提醒用户 + +### 8. 订单跟踪系统 +- 实现订单全流程状态跟踪,展示关键节点时间: + - 订单提交时间 + - 支付完成时间 + - 商家接单时间 + - 开始制作时间 + - 制作完成时间 + - 取餐/配送完成时间 +- 开发订单状态实时更新功能,状态变更时推送消息通知用户 + +## 商家端功能模块 + +### 1. 商家账户与门店管理 +- 实现商家注册功能,支持上传完整门店及负责人信息,包括: + - 门店信息:门店名称、详细地址(需定位确认)、联系电话、营业时间、门店图片(3-5张)、门店简介 + - 负责人信息:姓名、手机号码(需验证)、密码(需符合安全规范)、邮箱(需验证)、头像图片 +- 开发商家登录功能,支持手机号+密码登录 +- 提供信息修改界面,允许商家更新门店信息及负责人信息,修改关键信息(如门店地址、联系电话)需审核 + +### 2. 产品管理系统 +- 开发产品管理功能,支持: + - 新增产品:填写产品名称、选择分类、设置价格、规格(大/中/小杯,可分别定价)、上传产品图片、设置基础配料、添加产品描述 + - 编辑产品:修改现有产品的所有信息 + - 下架产品:支持临时下架和永久删除功能 +- 实现配料管理功能,支持添加、编辑、删除配料,设置配料是否收费及单价 +- 开发产品上下架状态管理,支持批量操作 + +### 3. 订单管理系统 +- 开发新订单提醒功能,当有新订单时通过系统通知和声音提醒 +- 实现订单处理功能: + - 接单/拒单操作(拒单需选择原因) + - 更新订单制作状态(开始制作、制作完成) + - 标记订单状态(待取餐、已完成、已取消) +- 开发订单查询功能,支持按时间(今日/昨日/近7天/自定义)、订单状态、订单号进行查询 +- 实现订单详情查看,展示完整订单信息及客户信息 + +### 4. 库存管理系统 +- 开发原料管理功能,支持添加、编辑、删除原料信息(名称、单位、规格、初始库存) +- 实现库存操作功能: + - 入库记录(原料名称、数量、单价、供应商、入库日期) + - 出库记录(原料名称、数量、用途、出库日期) +- 开发库存预警功能,支持设置各原料的低库存阈值,当库存低于阈值时发出预警提醒 +- 实现库存报表功能,展示当前库存余量、库存变动记录、库存预警列表 + +### 5. 公告发布系统 +- 开发公告编辑功能,支持富文本编辑,可插入图片、链接 +- 实现公告发布功能,支持设置公告类型、发布范围、生效时间 +- 开发公告管理功能,支持查看、编辑、删除已发布公告,置顶重要公告 + +### 6. 评价管理系统 +- 开发评价查看功能,展示用户对产品的评价,包括:评价内容、评分(1-5星)、评价时间、评价图片、对应的订单信息 +- 实现评价回复功能,支持商家对用户评价进行文字回复 +- 开发负面评价处理功能,支持对低分评价(1-2星)进行申诉处理,记录处理结果 + +### 7. 数据统计分析系统 +- 开发销售数据统计功能,支持按日、周、月查看: + - 销售总量(杯数) + - 销售总额 + - 订单数量 + - 客单价 +- 实现热门产品排行功能,按销量和销售额分别排行 +- 开发数据可视化功能,通过图表展示销售趋势、产品销售分布、用户评价分析 +- 支持数据导出功能,可导出Excel格式报表 + +所有功能模块需保证界面友好、操作便捷、响应迅速,并确保数据安全与用户隐私保护。 \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..c4bdf38 --- /dev/null +++ b/README.md @@ -0,0 +1,171 @@ +# 奶茶店管理系统 + +基于Spring Boot + MyBatis + MySQL + Thymeleaf的奶茶店管理系统,包含用户端和商家端两个主要模块,集成协同过滤推荐算法。 + +## 技术栈 + +- **后端框架**: Spring Boot 2.7.18 +- **数据库**: MySQL 8.0+ +- **ORM**: MyBatis +- **认证**: JWT Token +- **前端**: HTML + Thymeleaf + Bootstrap 5 +- **算法**: 协同过滤推荐算法(基于用户的协同过滤) + +## 功能模块 + +### 用户端功能 +1. 用户注册登录 +2. 个人信息管理 +3. 商品浏览(支持分类筛选) +4. 商品个性化定制(甜度、冰度、配料) +5. 购物车管理 +6. 订单下单支付(模拟微信/支付宝) +7. 订单跟踪 +8. 个性化商品推荐(协同过滤算法) +9. 公告查看 +10. 留言反馈 + +### 商家端功能 +1. 商家注册登录 +2. 商家信息管理 +3. 商品管理(新增/编辑/下架) +4. 商品规格和配料管理 +5. 订单管理(接单/拒单/状态更新) +6. 库存管理(入库/出库/预警) +7. 公告发布 +8. 评价管理(查看/回复) +9. 数据统计(日/周/月销量、营收、热门商品) + +## 数据库设计 + +数据库表结构请参考 `src/main/resources/db/schema.sql` + +主要表包括: +- user, user_address - 用户相关 +- merchant - 商家 +- product, product_spec, product_topping, product_custom - 商品相关 +- cart - 购物车 +- order, order_item, order_track - 订单相关 +- inventory, inventory_log - 库存相关 +- announcement - 公告 +- message - 留言反馈 +- review - 评价 +- user_behavior, user_similarity - 协同过滤算法相关 + +## 安装和运行 + +### 1. 环境要求 +- JDK 1.8+ +- Maven 3.6+ +- MySQL 8.0+ + +### 2. 数据库配置 +1. 创建数据库: +```sql +CREATE DATABASE nl_shop CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +``` + +2. 执行建表SQL: +```bash +mysql -u root -p nl_shop < src/main/resources/db/schema.sql +``` + +3. 修改配置文件 `src/main/resources/application.yml`: +```yaml +spring: + datasource: + url: jdbc:mysql://localhost:3306/nl_shop?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai + username: root + password: your_password +``` + +### 3. 编译运行 +```bash +mvn clean install +mvn spring-boot:run +``` + +### 4. 访问系统 +- 用户端登录: http://localhost:8080/user/login.html +- 商家端登录: http://localhost:8080/merchant/login.html + +## 协同过滤算法 + +系统实现了基于用户的协同过滤推荐算法: + +1. **用户行为记录**: 记录用户的浏览、购买、评分行为 +2. **相似度计算**: 使用余弦相似度计算用户之间的相似度 +3. **推荐生成**: 基于相似用户的偏好生成推荐列表 +4. **冷启动处理**: 新用户推荐热门商品 + +算法实现在 `CollaborativeFilteringService` 中。 + +## API接口 + +### 用户端接口 +- POST `/user/register` - 用户注册 +- POST `/user/login` - 用户登录 +- GET `/user/products` - 商品列表 +- GET `/user/products/{id}` - 商品详情 +- GET `/user/recommend` - 个性化推荐 +- POST `/user/cart/add` - 加入购物车 +- POST `/user/order/create` - 创建订单 +- POST `/user/order/{id}/pay` - 支付订单 + +### 商家端接口 +- POST `/merchant/register` - 商家注册 +- POST `/merchant/login` - 商家登录 +- GET `/merchant/products` - 商品列表 +- POST `/merchant/products` - 新增商品 +- GET `/merchant/orders` - 订单列表 +- PUT `/merchant/orders/{id}/accept` - 接单 +- GET `/merchant/statistics/sales` - 销量统计 + +详细API文档请参考Controller代码。 + +## 项目结构 + +``` +nl_shop/ +├── src/main/java/com/nlshop/ +│ ├── config/ # 配置类 +│ ├── controller/ # 控制器层 +│ │ ├── user/ # 用户端控制器 +│ │ └── merchant/ # 商家端控制器 +│ ├── service/ # 业务逻辑层 +│ │ ├── user/ +│ │ ├── merchant/ +│ │ └── algorithm/ # 协同过滤算法 +│ ├── mapper/ # MyBatis Mapper +│ ├── entity/ # 实体类 +│ ├── dto/ # 数据传输对象 +│ ├── util/ # 工具类 +│ └── interceptor/ # 拦截器 +└── src/main/resources/ + ├── mapper/ # MyBatis XML映射 + ├── templates/ # Thymeleaf模板 + └── db/ # 数据库脚本 +``` + +## 注意事项 + +1. JWT Token默认有效期为24小时,可在 `application.yml` 中配置 +2. 文件上传路径配置在 `application.yml` 的 `file.upload-path` +3. 密码使用MD5加密(生产环境建议使用BCrypt) +4. 支付功能为模拟实现,实际项目需要对接真实支付接口 + +## 开发计划 + +- [x] 项目基础架构 +- [x] 数据库设计 +- [x] 用户端功能 +- [x] 商家端功能 +- [x] 协同过滤算法 +- [x] 前端页面 +- [ ] 单元测试 +- [ ] 性能优化 +- [ ] Redis缓存集成 + +## 许可证 + +MIT License diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..09d084f --- /dev/null +++ b/pom.xml @@ -0,0 +1,157 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 2.7.18 + + + + com.nlshop + nl-shop + 1.0.0 + nl-shop + 奶茶店管理系统 + + + 1.8 + 1.8 + 1.8 + UTF-8 + 3.5.3.1 + 1.4.6 + 0.11.5 + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + + org.mybatis.spring.boot + mybatis-spring-boot-starter + 2.3.1 + + + + + mysql + mysql-connector-java + 8.0.33 + + + + + com.github.pagehelper + pagehelper-spring-boot-starter + ${pagehelper.version} + + + + + io.jsonwebtoken + jjwt-api + ${jjwt.version} + + + io.jsonwebtoken + jjwt-impl + ${jjwt.version} + runtime + + + io.jsonwebtoken + jjwt-jackson + ${jjwt.version} + runtime + + + + + org.projectlombok + lombok + true + + + + + org.springframework.boot + spring-boot-starter-validation + + + + + org.apache.commons + commons-lang3 + + + + + com.alibaba + fastjson + 2.0.43 + + + + + commons-fileupload + commons-fileupload + 1.5 + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 1.8 + 1.8 + UTF-8 + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + diff --git a/src/main/java/com/nlshop/NlShopApplication.java b/src/main/java/com/nlshop/NlShopApplication.java new file mode 100644 index 0000000..128752b --- /dev/null +++ b/src/main/java/com/nlshop/NlShopApplication.java @@ -0,0 +1,13 @@ +package com.nlshop; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +@MapperScan("com.nlshop.mapper") +public class NlShopApplication { + public static void main(String[] args) { + SpringApplication.run(NlShopApplication.class, args); + } +} diff --git a/src/main/java/com/nlshop/config/StartupUrlPrinter.java b/src/main/java/com/nlshop/config/StartupUrlPrinter.java new file mode 100644 index 0000000..d4f0b3a --- /dev/null +++ b/src/main/java/com/nlshop/config/StartupUrlPrinter.java @@ -0,0 +1,142 @@ +package com.nlshop.config; + +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.mvc.method.RequestMappingInfo; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +@Component +@Order(1) +public class StartupUrlPrinter implements ApplicationListener { + + @Override + public void onApplicationEvent(ApplicationReadyEvent event) { + RequestMappingHandlerMapping mapping = event.getApplicationContext() + .getBean(RequestMappingHandlerMapping.class); + + String baseUrl = "http://localhost:14001"; + int port = event.getApplicationContext().getEnvironment() + .getProperty("server.port", Integer.class, 14001); + if (port != 14001) { + baseUrl = "http://localhost:" + port; + } + + List userPages = new ArrayList<>(); + List userApis = new ArrayList<>(); + List merchantPages = new ArrayList<>(); + List merchantApis = new ArrayList<>(); + + Map handlerMethods = mapping.getHandlerMethods(); + + for (Map.Entry entry : handlerMethods.entrySet()) { + RequestMappingInfo mappingInfo = entry.getKey(); + HandlerMethod handlerMethod = entry.getValue(); + + // 检查patternsCondition是否为null + if (mappingInfo.getPatternsCondition() == null) { + continue; + } + + Set patterns = mappingInfo.getPatternsCondition().getPatterns(); + if (patterns == null || patterns.isEmpty()) { + continue; + } + + for (String pattern : patterns) { + String url = baseUrl + pattern; + String className = handlerMethod.getBeanType().getSimpleName(); + + if (pattern.startsWith("/merchant")) { + if (pattern.endsWith(".html")) { + merchantPages.add(url); + } else { + merchantApis.add(url + " [" + getHttpMethods(mappingInfo) + "]"); + } + } else if (!pattern.startsWith("/error") && !pattern.startsWith("/static") + && !pattern.startsWith("/upload") && !pattern.equals("/")) { + if (pattern.endsWith(".html")) { + userPages.add(url); + } else { + userApis.add(url + " [" + getHttpMethods(mappingInfo) + "]"); + } + } + } + } + + // 添加视图控制器映射的页面 + userPages.add(baseUrl + "/login.html"); + userPages.add(baseUrl + "/register.html"); + userPages.add(baseUrl + "/index.html"); + userPages.add(baseUrl + "/cart.html"); + userPages.add(baseUrl + "/orders.html"); + userPages.add(baseUrl + "/order-detail.html"); + userPages.add(baseUrl + "/product-detail.html"); + userPages.add(baseUrl + "/checkout.html"); + userPages.add(baseUrl + "/profile.html"); + + merchantPages.add(baseUrl + "/merchant/login.html"); + merchantPages.add(baseUrl + "/merchant/dashboard.html"); + merchantPages.add(baseUrl + "/merchant/products.html"); + merchantPages.add(baseUrl + "/merchant/orders.html"); + + // 打印所有URL + printUrls("用户端页面", userPages, baseUrl); + printUrls("用户端API", userApis, baseUrl); + printUrls("商家端页面", merchantPages, baseUrl); + printUrls("商家端API", merchantApis, baseUrl); + } + + private String getHttpMethods(RequestMappingInfo mappingInfo) { + if (mappingInfo.getMethodsCondition() == null) { + return "GET"; + } + + Set methods = mappingInfo.getMethodsCondition().getMethods().stream() + .map(m -> m.name()) + .collect(Collectors.toSet()); + if (methods.isEmpty()) { + return "GET"; + } + StringBuilder sb = new StringBuilder(); + for (String method : methods) { + if (sb.length() > 0) { + sb.append(","); + } + sb.append(method); + } + return sb.toString(); + } + + private void printUrls(String category, List urls, String baseUrl) { + if (urls.isEmpty()) return; + + String separator = createSeparator(80); + System.out.println("\n" + separator); + System.out.println("📋 " + category + " (" + urls.size() + " 个)"); + System.out.println(separator); + + for (int i = 0; i < urls.size(); i++) { + String url = urls.get(i); + System.out.println(String.format("%3d. %s", i + 1, url)); + } + + System.out.println(separator); + } + + private String createSeparator(int length) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < length; i++) { + sb.append("="); + } + return sb.toString(); + } +} diff --git a/src/main/java/com/nlshop/config/WebMvcConfig.java b/src/main/java/com/nlshop/config/WebMvcConfig.java new file mode 100644 index 0000000..83a2ddc --- /dev/null +++ b/src/main/java/com/nlshop/config/WebMvcConfig.java @@ -0,0 +1,71 @@ +package com.nlshop.config; + +import com.nlshop.interceptor.AuthInterceptor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebMvcConfig implements WebMvcConfigurer { + + @Autowired + private AuthInterceptor authInterceptor; + + @Override + public void addViewControllers(ViewControllerRegistry registry) { + // 用户端页面(去掉/user前缀) + registry.addViewController("/login.html").setViewName("user/login"); + registry.addViewController("/register.html").setViewName("user/register"); + registry.addViewController("/index.html").setViewName("user/index"); + registry.addViewController("/cart.html").setViewName("user/cart"); + registry.addViewController("/orders.html").setViewName("user/orders"); + registry.addViewController("/order-detail.html").setViewName("user/order-detail"); + registry.addViewController("/product-detail.html").setViewName("user/product-detail"); + registry.addViewController("/checkout.html").setViewName("user/checkout"); + registry.addViewController("/payment.html").setViewName("user/payment"); + registry.addViewController("/payment-success.html").setViewName("user/payment-success"); + registry.addViewController("/profile.html").setViewName("user/profile"); + + // 商家端页面(保持/merchant前缀) + registry.addViewController("/merchant/login.html").setViewName("merchant/login"); + registry.addViewController("/merchant/register.html").setViewName("merchant/register"); + registry.addViewController("/merchant/dashboard.html").setViewName("merchant/dashboard"); + registry.addViewController("/merchant/products.html").setViewName("merchant/products"); + registry.addViewController("/merchant/product-edit.html").setViewName("merchant/product-edit"); + registry.addViewController("/merchant/orders.html").setViewName("merchant/orders"); + registry.addViewController("/merchant/inventory.html").setViewName("merchant/inventory"); + registry.addViewController("/merchant/statistics.html").setViewName("merchant/statistics"); + registry.addViewController("/merchant/announcements.html").setViewName("merchant/announcements"); + registry.addViewController("/merchant/messages.html").setViewName("merchant/messages"); + registry.addViewController("/merchant/reviews.html").setViewName("merchant/reviews"); + registry.addViewController("/merchant/users.html").setViewName("merchant/users"); + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(authInterceptor) + .addPathPatterns("/**") + .excludePathPatterns( + // 登录/注册API接口 + "/login", "/register", + "/merchant/login", "/merchant/register", + // 登录/注册HTML页面 + "/login.html", "/register.html", + "/merchant/login.html", "/merchant/register.html", + // 静态资源 + "/static/**", "/upload/**", + "/error", "/favicon.ico" + ); + } + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler("/upload/**") + .addResourceLocations("file:src/main/resources/static/upload/"); + registry.addResourceHandler("/static/**") + .addResourceLocations("classpath:/static/"); + } +} diff --git a/src/main/java/com/nlshop/controller/merchant/AnnouncementController.java b/src/main/java/com/nlshop/controller/merchant/AnnouncementController.java new file mode 100644 index 0000000..d3acaec --- /dev/null +++ b/src/main/java/com/nlshop/controller/merchant/AnnouncementController.java @@ -0,0 +1,59 @@ +package com.nlshop.controller.merchant; + +import com.nlshop.dto.response.ApiResponse; +import com.nlshop.entity.Announcement; +import com.nlshop.service.merchant.AnnouncementService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletRequest; +import java.util.List; + +@RestController("merchantAnnouncementController") +@RequestMapping("/merchant/announcements") +public class AnnouncementController { + + @Autowired + private AnnouncementService announcementService; + + @PostMapping + public ApiResponse createAnnouncement(@RequestBody Announcement announcement, + HttpServletRequest request) { + Long merchantId = (Long) request.getAttribute("userId"); + if (merchantId == null) { + return ApiResponse.error("请先登录"); + } + Announcement created = announcementService.createAnnouncement(merchantId, announcement); + return ApiResponse.success(created); + } + + @GetMapping + public ApiResponse> getAnnouncements(HttpServletRequest request) { + Long merchantId = (Long) request.getAttribute("userId"); + if (merchantId == null) { + return ApiResponse.error("请先登录"); + } + List announcements = announcementService.getAnnouncements(merchantId); + return ApiResponse.success(announcements); + } + + @GetMapping("/{id}") + public ApiResponse getAnnouncement(@PathVariable Long id) { + Announcement announcement = announcementService.getAnnouncement(id); + return ApiResponse.success(announcement); + } + + @PutMapping("/{id}") + public ApiResponse updateAnnouncement(@PathVariable Long id, + @RequestBody Announcement announcement) { + announcement.setId(id); + announcementService.updateAnnouncement(announcement); + return ApiResponse.success(null); + } + + @DeleteMapping("/{id}") + public ApiResponse deleteAnnouncement(@PathVariable Long id) { + announcementService.deleteAnnouncement(id); + return ApiResponse.success(null); + } +} diff --git a/src/main/java/com/nlshop/controller/merchant/InventoryController.java b/src/main/java/com/nlshop/controller/merchant/InventoryController.java new file mode 100644 index 0000000..c068ef0 --- /dev/null +++ b/src/main/java/com/nlshop/controller/merchant/InventoryController.java @@ -0,0 +1,80 @@ +package com.nlshop.controller.merchant; + +import com.nlshop.dto.response.ApiResponse; +import com.nlshop.entity.Inventory; +import com.nlshop.service.merchant.InventoryService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletRequest; +import java.math.BigDecimal; +import java.util.List; + +@RestController +@RequestMapping("/merchant/inventory") +public class InventoryController { + + @Autowired + private InventoryService inventoryService; + + @GetMapping + public ApiResponse> getInventory(HttpServletRequest request) { + Long merchantId = (Long) request.getAttribute("userId"); + if (merchantId == null) { + return ApiResponse.error("请先登录"); + } + List inventory = inventoryService.getInventory(merchantId); + return ApiResponse.success(inventory); + } + + @PostMapping + public ApiResponse addInventory(@RequestBody Inventory inventory, + HttpServletRequest request) { + Long merchantId = (Long) request.getAttribute("userId"); + if (merchantId == null) { + return ApiResponse.error("请先登录"); + } + Inventory added = inventoryService.addInventory(merchantId, inventory); + return ApiResponse.success(added); + } + + @PostMapping("/{id}/in") + public ApiResponse stockIn(@PathVariable Long id, + @RequestParam BigDecimal quantity, + @RequestParam String operator) { + inventoryService.stockIn(id, quantity, operator); + return ApiResponse.success(null); + } + + @PostMapping("/{id}/out") + public ApiResponse stockOut(@PathVariable Long id, + @RequestParam BigDecimal quantity, + @RequestParam String operator) { + inventoryService.stockOut(id, quantity, operator); + return ApiResponse.success(null); + } + + @GetMapping("/alerts") + public ApiResponse> getLowStockAlerts(HttpServletRequest request) { + Long merchantId = (Long) request.getAttribute("userId"); + if (merchantId == null) { + return ApiResponse.error("请先登录"); + } + List alerts = inventoryService.getLowStockAlerts(merchantId); + return ApiResponse.success(alerts); + } + + @PutMapping("/{id}") + public ApiResponse updateInventory(@PathVariable Long id, + @RequestBody Inventory inventory) { + inventory.setId(id); + inventoryService.updateInventory(inventory); + return ApiResponse.success(null); + } + + @DeleteMapping("/{id}") + public ApiResponse deleteInventory(@PathVariable Long id) { + inventoryService.deleteInventory(id); + return ApiResponse.success(null); + } +} diff --git a/src/main/java/com/nlshop/controller/merchant/MerchantController.java b/src/main/java/com/nlshop/controller/merchant/MerchantController.java new file mode 100644 index 0000000..b3b7053 --- /dev/null +++ b/src/main/java/com/nlshop/controller/merchant/MerchantController.java @@ -0,0 +1,80 @@ +package com.nlshop.controller.merchant; + +import com.nlshop.dto.request.LoginRequest; +import com.nlshop.dto.request.MerchantRegisterRequest; +import com.nlshop.dto.response.ApiResponse; +import com.nlshop.dto.response.LoginResponse; +import com.nlshop.entity.Merchant; +import com.nlshop.service.merchant.MerchantService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; + +@RestController +@RequestMapping("/merchant") +public class MerchantController { + + @Autowired + private MerchantService merchantService; + + @PostMapping("/register") + public ApiResponse register(@Validated @RequestBody MerchantRegisterRequest request) { + Merchant merchant = merchantService.register(request); + return ApiResponse.success(merchant); + } + + @PostMapping("/login") + public ApiResponse login(@Validated @RequestBody LoginRequest request, + HttpServletRequest httpRequest) { + Merchant merchant = merchantService.login(request.getUsername(), request.getPassword()); + + // 将用户信息存入Session + HttpSession session = httpRequest.getSession(true); + session.setAttribute("userId", merchant.getId()); + session.setAttribute("userType", "merchant"); + session.setAttribute("username", merchant.getUsername()); + + LoginResponse response = new LoginResponse(); + response.setToken(""); // 不再使用token,设为空字符串 + response.setUserId(merchant.getId()); + response.setUserType("merchant"); + response.setUsername(merchant.getUsername()); + response.setName(merchant.getStoreName()); + + return ApiResponse.success(response); + } + + @PostMapping("/logout") + public ApiResponse logout(HttpServletRequest request) { + HttpSession session = request.getSession(false); + if (session != null) { + session.invalidate(); + } + return ApiResponse.success(null); + } + + @GetMapping("/profile") + public ApiResponse getProfile(HttpServletRequest request) { + Long merchantId = (Long) request.getAttribute("userId"); + if (merchantId == null) { + return ApiResponse.error("请先登录"); + } + Merchant merchant = merchantService.getProfile(merchantId); + return ApiResponse.success(merchant); + } + + @PutMapping("/profile") + public ApiResponse updateProfile(@RequestBody Merchant merchant, + HttpServletRequest request) { + Long merchantId = (Long) request.getAttribute("userId"); + if (merchantId == null) { + return ApiResponse.error("请先登录"); + } + Merchant updated = merchantService.updateProfile(merchantId, merchant); + return ApiResponse.success(updated); + } +} diff --git a/src/main/java/com/nlshop/controller/merchant/MessageController.java b/src/main/java/com/nlshop/controller/merchant/MessageController.java new file mode 100644 index 0000000..a8a9b12 --- /dev/null +++ b/src/main/java/com/nlshop/controller/merchant/MessageController.java @@ -0,0 +1,41 @@ +package com.nlshop.controller.merchant; + +import com.nlshop.dto.response.ApiResponse; +import com.nlshop.entity.Message; +import com.nlshop.service.merchant.MessageService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletRequest; +import java.util.List; + +@RestController("merchantMessageController") +@RequestMapping("/merchant/messages") +public class MessageController { + + @Autowired + private MessageService messageService; + + @GetMapping + public ApiResponse> getMessages(HttpServletRequest request) { + Long merchantId = (Long) request.getAttribute("userId"); + if (merchantId == null) { + return ApiResponse.error("请先登录"); + } + List messages = messageService.getMessages(merchantId); + return ApiResponse.success(messages); + } + + @GetMapping("/{id}") + public ApiResponse getMessage(@PathVariable Long id) { + Message message = messageService.getMessage(id); + return ApiResponse.success(message); + } + + @PostMapping("/{id}/reply") + public ApiResponse replyMessage(@PathVariable Long id, + @RequestParam String reply) { + messageService.replyMessage(id, reply); + return ApiResponse.success(null); + } +} diff --git a/src/main/java/com/nlshop/controller/merchant/OrderManageController.java b/src/main/java/com/nlshop/controller/merchant/OrderManageController.java new file mode 100644 index 0000000..d578d71 --- /dev/null +++ b/src/main/java/com/nlshop/controller/merchant/OrderManageController.java @@ -0,0 +1,78 @@ +package com.nlshop.controller.merchant; + +import com.nlshop.dto.response.ApiResponse; +import com.nlshop.dto.response.OrderDetailResponse; +import com.nlshop.dto.response.PageResponse; +import com.nlshop.entity.Order; +import com.nlshop.service.merchant.OrderManageService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletRequest; + +@RestController +@RequestMapping("/merchant/orders") +public class OrderManageController { + + @Autowired + private OrderManageService orderManageService; + + @GetMapping + public ApiResponse> getOrders( + @RequestParam(required = false) String status, + @RequestParam(defaultValue = "1") Integer pageNum, + @RequestParam(defaultValue = "10") Integer pageSize, + HttpServletRequest request) { + Long merchantId = (Long) request.getAttribute("userId"); + if (merchantId == null) { + return ApiResponse.error("请先登录"); + } + PageResponse response = orderManageService.getOrders(merchantId, status, pageNum, pageSize); + return ApiResponse.success(response); + } + + @GetMapping("/{id}") + public ApiResponse getOrderDetail(@PathVariable Long id, + HttpServletRequest request) { + Long merchantId = (Long) request.getAttribute("userId"); + if (merchantId == null) { + return ApiResponse.error("请先登录"); + } + OrderDetailResponse orderDetail = orderManageService.getOrderDetail(id, merchantId); + return ApiResponse.success(orderDetail); + } + + @PutMapping("/{id}/accept") + public ApiResponse acceptOrder(@PathVariable Long id, + HttpServletRequest request) { + Long merchantId = (Long) request.getAttribute("userId"); + if (merchantId == null) { + return ApiResponse.error("请先登录"); + } + orderManageService.acceptOrder(id, merchantId); + return ApiResponse.success(null); + } + + @PutMapping("/{id}/reject") + public ApiResponse rejectOrder(@PathVariable Long id, + HttpServletRequest request) { + Long merchantId = (Long) request.getAttribute("userId"); + if (merchantId == null) { + return ApiResponse.error("请先登录"); + } + orderManageService.rejectOrder(id, merchantId); + return ApiResponse.success(null); + } + + @PutMapping("/{id}/status") + public ApiResponse updateOrderStatus(@PathVariable Long id, + @RequestParam String status, + HttpServletRequest request) { + Long merchantId = (Long) request.getAttribute("userId"); + if (merchantId == null) { + return ApiResponse.error("请先登录"); + } + orderManageService.updateOrderStatus(id, merchantId, status); + return ApiResponse.success(null); + } +} diff --git a/src/main/java/com/nlshop/controller/merchant/ProductManageController.java b/src/main/java/com/nlshop/controller/merchant/ProductManageController.java new file mode 100644 index 0000000..6e4e75d --- /dev/null +++ b/src/main/java/com/nlshop/controller/merchant/ProductManageController.java @@ -0,0 +1,222 @@ +package com.nlshop.controller.merchant; + +import com.nlshop.dto.response.ApiResponse; +import com.nlshop.dto.response.PageResponse; +import com.nlshop.entity.Product; +import com.nlshop.entity.ProductCustom; +import com.nlshop.entity.ProductSpec; +import com.nlshop.entity.ProductTopping; +import com.nlshop.service.merchant.ProductManageService; +import com.nlshop.util.FileUploadUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.http.HttpServletRequest; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/merchant/products") +public class ProductManageController { + + @Autowired + private ProductManageService productManageService; + + @Autowired + private FileUploadUtil fileUploadUtil; + + @GetMapping + public ApiResponse> getProducts( + @RequestParam(defaultValue = "1") Integer pageNum, + @RequestParam(defaultValue = "10") Integer pageSize, + HttpServletRequest request) { + Long merchantId = (Long) request.getAttribute("userId"); + if (merchantId == null) { + return ApiResponse.error("请先登录"); + } + PageResponse response = productManageService.getProducts(merchantId, pageNum, pageSize); + return ApiResponse.success(response); + } + + @GetMapping("/{id}") + public ApiResponse> getProduct(@PathVariable Long id, + HttpServletRequest request) { + Long merchantId = (Long) request.getAttribute("userId"); + if (merchantId == null) { + return ApiResponse.error("请先登录"); + } + Map product = productManageService.getProduct(id, merchantId); + return ApiResponse.success(product); + } + + @PostMapping + public ApiResponse createProduct(@RequestParam String name, + @RequestParam(required = false) String category, + @RequestParam(required = false) String description, + @RequestParam String basePrice, + @RequestParam(required = false) MultipartFile image, + HttpServletRequest httpRequest) { + Long merchantId = (Long) httpRequest.getAttribute("userId"); + if (merchantId == null) { + return ApiResponse.error("请先登录"); + } + + Product product = new Product(); + product.setName(name); + product.setCategory(category); + product.setDescription(description); + if (basePrice != null && !basePrice.isEmpty()) { + product.setBasePrice(new java.math.BigDecimal(basePrice)); + } + + if (image != null && !image.isEmpty()) { + try { + String imagePath = fileUploadUtil.uploadProductImage(image); + product.setImage(imagePath); + } catch (Exception e) { + return ApiResponse.error("图片上传失败: " + e.getMessage()); + } + } + + Product created = productManageService.createProduct(merchantId, product); + return ApiResponse.success(created); + } + + @PutMapping("/{id}") + public ApiResponse updateProduct(@PathVariable Long id, + @RequestParam String name, + @RequestParam(required = false) String category, + @RequestParam(required = false) String description, + @RequestParam String basePrice, + @RequestParam(required = false) MultipartFile image, + HttpServletRequest request) { + Long merchantId = (Long) request.getAttribute("userId"); + if (merchantId == null) { + return ApiResponse.error("请先登录"); + } + + Product product = new Product(); + product.setName(name); + product.setCategory(category); + product.setDescription(description); + if (basePrice != null && !basePrice.isEmpty()) { + product.setBasePrice(new java.math.BigDecimal(basePrice)); + } + + if (image != null && !image.isEmpty()) { + try { + String imagePath = fileUploadUtil.uploadProductImage(image); + product.setImage(imagePath); + } catch (Exception e) { + return ApiResponse.error("图片上传失败: " + e.getMessage()); + } + } + + Product updated = productManageService.updateProduct(id, merchantId, product); + return ApiResponse.success(updated); + } + + @PutMapping("/{id}/offline") + public ApiResponse offlineProduct(@PathVariable Long id, + HttpServletRequest request) { + Long merchantId = (Long) request.getAttribute("userId"); + if (merchantId == null) { + return ApiResponse.error("请先登录"); + } + productManageService.offlineProduct(id, merchantId); + return ApiResponse.success(null); + } + + @PutMapping("/{id}/online") + public ApiResponse onlineProduct(@PathVariable Long id, + HttpServletRequest request) { + Long merchantId = (Long) request.getAttribute("userId"); + if (merchantId == null) { + return ApiResponse.error("请先登录"); + } + productManageService.onlineProduct(id, merchantId); + return ApiResponse.success(null); + } + + @GetMapping("/{id}/specs") + public ApiResponse> getSpecs(@PathVariable Long id) { + List specs = productManageService.getSpecs(id); + return ApiResponse.success(specs); + } + + @PostMapping("/{id}/specs") + public ApiResponse addSpec(@PathVariable Long id, + @RequestBody ProductSpec spec) { + ProductSpec added = productManageService.addSpec(id, spec); + return ApiResponse.success(added); + } + + @PutMapping("/specs/{specId}") + public ApiResponse updateSpec(@PathVariable Long specId, + @RequestBody ProductSpec spec) { + spec.setId(specId); + productManageService.updateSpec(spec); + return ApiResponse.success(null); + } + + @DeleteMapping("/specs/{specId}") + public ApiResponse deleteSpec(@PathVariable Long specId) { + productManageService.deleteSpec(specId); + return ApiResponse.success(null); + } + + @GetMapping("/{id}/toppings") + public ApiResponse> getToppings(@PathVariable Long id) { + List toppings = productManageService.getToppings(id); + return ApiResponse.success(toppings); + } + + @PostMapping("/{id}/toppings") + public ApiResponse addTopping(@PathVariable Long id, + @RequestBody ProductTopping topping) { + ProductTopping added = productManageService.addTopping(id, topping); + return ApiResponse.success(added); + } + + @PutMapping("/toppings/{toppingId}") + public ApiResponse updateTopping(@PathVariable Long toppingId, + @RequestBody ProductTopping topping) { + topping.setId(toppingId); + productManageService.updateTopping(topping); + return ApiResponse.success(null); + } + + @DeleteMapping("/toppings/{toppingId}") + public ApiResponse deleteTopping(@PathVariable Long toppingId) { + productManageService.deleteTopping(toppingId); + return ApiResponse.success(null); + } + + @GetMapping("/{id}/customs") + public ApiResponse> getCustoms(@PathVariable Long id) { + List customs = productManageService.getCustoms(id); + return ApiResponse.success(customs); + } + + @PostMapping("/{id}/customs") + public ApiResponse addCustom(@PathVariable Long id, + @RequestBody ProductCustom custom) { + ProductCustom added = productManageService.addCustom(id, custom); + return ApiResponse.success(added); + } + + @PutMapping("/customs/{customId}") + public ApiResponse updateCustom(@PathVariable Long customId, + @RequestBody ProductCustom custom) { + custom.setId(customId); + productManageService.updateCustom(custom); + return ApiResponse.success(null); + } + + @DeleteMapping("/customs/{customId}") + public ApiResponse deleteCustom(@PathVariable Long customId) { + productManageService.deleteCustom(customId); + return ApiResponse.success(null); + } +} diff --git a/src/main/java/com/nlshop/controller/merchant/ReviewController.java b/src/main/java/com/nlshop/controller/merchant/ReviewController.java new file mode 100644 index 0000000..d75de18 --- /dev/null +++ b/src/main/java/com/nlshop/controller/merchant/ReviewController.java @@ -0,0 +1,67 @@ +package com.nlshop.controller.merchant; + +import com.nlshop.dto.response.ApiResponse; +import com.nlshop.entity.Review; +import com.nlshop.exception.BusinessException; +import com.nlshop.service.merchant.ReviewService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletRequest; +import java.util.List; + +@RestController("merchantReviewController") +@RequestMapping("/merchant/reviews") +public class ReviewController { + + @Autowired + private ReviewService reviewService; + + @GetMapping + public ApiResponse> getReviews(HttpServletRequest request) { + Long merchantId = (Long) request.getAttribute("userId"); + if (merchantId == null) { + return ApiResponse.error("请先登录"); + } + List reviews = reviewService.getReviews(merchantId); + return ApiResponse.success(reviews); + } + + @GetMapping("/{id}") + public ApiResponse getReview(@PathVariable Long id) { + Review review = reviewService.getReview(id); + return ApiResponse.success(review); + } + + @PostMapping("/{id}/reply") + public ApiResponse replyReview(@PathVariable Long id, + @RequestParam String reply, + HttpServletRequest request) { + Long merchantId = (Long) request.getAttribute("userId"); + if (merchantId == null) { + return ApiResponse.error("请先登录"); + } + try { + reviewService.replyReview(merchantId, id, reply); + return ApiResponse.success(null); + } catch (BusinessException e) { + return ApiResponse.error(e.getMessage()); + } + } + + @PutMapping("/{id}/handle") + public ApiResponse handleComplaint(@PathVariable Long id, + @RequestParam String reply, + HttpServletRequest request) { + Long merchantId = (Long) request.getAttribute("userId"); + if (merchantId == null) { + return ApiResponse.error("请先登录"); + } + try { + reviewService.handleComplaint(merchantId, id, reply); + return ApiResponse.success(null); + } catch (BusinessException e) { + return ApiResponse.error(e.getMessage()); + } + } +} diff --git a/src/main/java/com/nlshop/controller/merchant/StatisticsController.java b/src/main/java/com/nlshop/controller/merchant/StatisticsController.java new file mode 100644 index 0000000..fdbc7e3 --- /dev/null +++ b/src/main/java/com/nlshop/controller/merchant/StatisticsController.java @@ -0,0 +1,79 @@ +package com.nlshop.controller.merchant; + +import com.nlshop.dto.response.ApiResponse; +import com.nlshop.service.merchant.StatisticsService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletRequest; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/merchant/statistics") +public class StatisticsController { + + @Autowired + private StatisticsService statisticsService; + + @GetMapping("/sales") + public ApiResponse> getSalesStatistics( + @RequestParam(defaultValue = "day") String period, + HttpServletRequest request) { + Long merchantId = (Long) request.getAttribute("userId"); + if (merchantId == null) { + return ApiResponse.error("请先登录"); + } + Map stats = statisticsService.getSalesStatistics(merchantId, period); + return ApiResponse.success(stats); + } + + @GetMapping("/revenue") + public ApiResponse> getRevenueStatistics( + @RequestParam(defaultValue = "day") String period, + HttpServletRequest request) { + Long merchantId = (Long) request.getAttribute("userId"); + if (merchantId == null) { + return ApiResponse.error("请先登录"); + } + Map stats = statisticsService.getRevenueStatistics(merchantId, period); + return ApiResponse.success(stats); + } + + @GetMapping("/hot-products") + public ApiResponse>> getHotProducts( + @RequestParam(defaultValue = "10") int limit, + HttpServletRequest request) { + Long merchantId = (Long) request.getAttribute("userId"); + if (merchantId == null) { + return ApiResponse.error("请先登录"); + } + List> products = statisticsService.getHotProducts(merchantId, limit); + return ApiResponse.success(products); + } + + @GetMapping("/active-users") + public ApiResponse>> getActiveUsers( + @RequestParam(defaultValue = "30") int days, + HttpServletRequest request) { + Long merchantId = (Long) request.getAttribute("userId"); + if (merchantId == null) { + return ApiResponse.error("请先登录"); + } + List> users = statisticsService.getActiveUsers(merchantId, days); + return ApiResponse.success(users); + } + + @GetMapping("/sticky-users") + public ApiResponse>> getStickyUsers( + @RequestParam(defaultValue = "30") int days, + @RequestParam(defaultValue = "5") int minOrders, + HttpServletRequest request) { + Long merchantId = (Long) request.getAttribute("userId"); + if (merchantId == null) { + return ApiResponse.error("请先登录"); + } + List> users = statisticsService.getStickyUsers(merchantId, days, minOrders); + return ApiResponse.success(users); + } +} diff --git a/src/main/java/com/nlshop/controller/merchant/UserManageController.java b/src/main/java/com/nlshop/controller/merchant/UserManageController.java new file mode 100644 index 0000000..83601e3 --- /dev/null +++ b/src/main/java/com/nlshop/controller/merchant/UserManageController.java @@ -0,0 +1,31 @@ +package com.nlshop.controller.merchant; + +import com.nlshop.dto.response.ApiResponse; +import com.nlshop.dto.response.PageResponse; +import com.nlshop.entity.User; +import com.nlshop.service.merchant.UserManageService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletRequest; + +@RestController +@RequestMapping("/merchant/users") +public class UserManageController { + + @Autowired + private UserManageService userManageService; + + @GetMapping + public ApiResponse> getUsers( + @RequestParam(defaultValue = "1") Integer pageNum, + @RequestParam(defaultValue = "10") Integer pageSize, + HttpServletRequest request) { + Long merchantId = (Long) request.getAttribute("userId"); + if (merchantId == null) { + return ApiResponse.error("请先登录"); + } + PageResponse response = userManageService.getUsers(pageNum, pageSize); + return ApiResponse.success(response); + } +} diff --git a/src/main/java/com/nlshop/controller/user/AnnouncementController.java b/src/main/java/com/nlshop/controller/user/AnnouncementController.java new file mode 100644 index 0000000..0958c84 --- /dev/null +++ b/src/main/java/com/nlshop/controller/user/AnnouncementController.java @@ -0,0 +1,23 @@ +package com.nlshop.controller.user; + +import com.nlshop.dto.response.ApiResponse; +import com.nlshop.entity.Announcement; +import com.nlshop.service.merchant.AnnouncementService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController("userAnnouncementController") +@RequestMapping("/announcements") +public class AnnouncementController { + + @Autowired + private AnnouncementService announcementService; + + @GetMapping + public ApiResponse> getAnnouncements() { + List announcements = announcementService.getPublishedAnnouncements(); + return ApiResponse.success(announcements); + } +} diff --git a/src/main/java/com/nlshop/controller/user/CartController.java b/src/main/java/com/nlshop/controller/user/CartController.java new file mode 100644 index 0000000..f1e61a7 --- /dev/null +++ b/src/main/java/com/nlshop/controller/user/CartController.java @@ -0,0 +1,116 @@ +package com.nlshop.controller.user; + +import com.nlshop.dto.request.CartAddRequest; +import com.nlshop.dto.response.ApiResponse; +import com.nlshop.entity.CartItem; +import com.nlshop.service.user.CartService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletRequest; +import java.util.List; + +@RestController +@RequestMapping("/cart") +public class CartController { + + @Autowired + private CartService cartService; + + @PostMapping("/add") + public ApiResponse addToCart(@Validated @RequestBody CartAddRequest request, + HttpServletRequest httpRequest) { + Long userId = (Long) httpRequest.getAttribute("userId"); + if (userId == null) { + return ApiResponse.error("请先登录"); + } + CartItem item = cartService.addToCart(userId, request); + return ApiResponse.success(item); + } + + @GetMapping + public ApiResponse> getCart(HttpServletRequest request) { + Long userId = (Long) request.getAttribute("userId"); + if (userId == null) { + return ApiResponse.error("请先登录"); + } + List items = cartService.getCart(userId); + return ApiResponse.success(items); + } + + @PutMapping("/{id}") + public ApiResponse updateQuantity(@PathVariable Long id, + @RequestParam Integer quantity, + HttpServletRequest request) { + Long userId = (Long) request.getAttribute("userId"); + if (userId == null) { + return ApiResponse.error("请先登录"); + } + try { + cartService.updateQuantity(userId, id, quantity); + return ApiResponse.success(null); + } catch (RuntimeException e) { + return ApiResponse.error(e.getMessage()); + } + } + + @DeleteMapping("/{id}") + public ApiResponse deleteCartItem(@PathVariable Long id, + HttpServletRequest request) { + Long userId = (Long) request.getAttribute("userId"); + if (userId == null) { + return ApiResponse.error("请先登录"); + } + try { + cartService.deleteCartItem(userId, id); + return ApiResponse.success(null); + } catch (RuntimeException e) { + return ApiResponse.error(e.getMessage()); + } + } + + @DeleteMapping("/batch") + public ApiResponse deleteBatch(@RequestBody List ids, + HttpServletRequest request) { + Long userId = (Long) request.getAttribute("userId"); + if (userId == null) { + return ApiResponse.error("请先登录"); + } + if (ids == null || ids.isEmpty()) { + return ApiResponse.error("请选择要删除的商品"); + } + try { + cartService.deleteBatch(userId, ids); + return ApiResponse.success(null); + } catch (RuntimeException e) { + return ApiResponse.error(e.getMessage()); + } + } + + /** + * 同步cookies中的购物车数据(用于登录后同步) + */ + @PostMapping("/sync") + public ApiResponse syncCart(@RequestBody List items, + HttpServletRequest request) { + Long userId = (Long) request.getAttribute("userId"); + if (userId == null) { + return ApiResponse.error("请先登录"); + } + + if (items == null || items.isEmpty()) { + return ApiResponse.success(null); + } + + try { + cartService.syncCartFromCookies(userId, items); + return ApiResponse.success(null); + } catch (Exception e) { + // 同步失败不应该影响登录流程,只记录日志 + System.err.println("同步购物车失败: userId=" + userId + ", error=" + e.getMessage()); + e.printStackTrace(); + return ApiResponse.success(null); // 返回成功,避免影响登录流程 + } + } +} diff --git a/src/main/java/com/nlshop/controller/user/MessageController.java b/src/main/java/com/nlshop/controller/user/MessageController.java new file mode 100644 index 0000000..abd83c3 --- /dev/null +++ b/src/main/java/com/nlshop/controller/user/MessageController.java @@ -0,0 +1,41 @@ +package com.nlshop.controller.user; + +import com.nlshop.dto.response.ApiResponse; +import com.nlshop.entity.Message; +import com.nlshop.service.user.MessageService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletRequest; +import java.util.List; + +@RestController("userMessageController") +@RequestMapping("/message") +public class MessageController { + + @Autowired + private MessageService messageService; + + @PostMapping + public ApiResponse createMessage(@RequestParam Long merchantId, + @RequestParam(required = false) Long orderId, + @RequestParam String content, + HttpServletRequest request) { + Long userId = (Long) request.getAttribute("userId"); + if (userId == null) { + return ApiResponse.error("请先登录"); + } + Message message = messageService.createMessage(userId, merchantId, orderId, content); + return ApiResponse.success(message); + } + + @GetMapping + public ApiResponse> getMessages(HttpServletRequest request) { + Long userId = (Long) request.getAttribute("userId"); + if (userId == null) { + return ApiResponse.error("请先登录"); + } + List messages = messageService.getMessages(userId); + return ApiResponse.success(messages); + } +} diff --git a/src/main/java/com/nlshop/controller/user/OrderController.java b/src/main/java/com/nlshop/controller/user/OrderController.java new file mode 100644 index 0000000..6e55b0b --- /dev/null +++ b/src/main/java/com/nlshop/controller/user/OrderController.java @@ -0,0 +1,88 @@ +package com.nlshop.controller.user; + +import com.nlshop.dto.request.OrderCreateRequest; +import com.nlshop.dto.request.PaymentRequest; +import com.nlshop.dto.response.ApiResponse; +import com.nlshop.dto.response.OrderDetailResponse; +import com.nlshop.entity.Order; +import com.nlshop.entity.OrderTrack; +import com.nlshop.exception.BusinessException; +import com.nlshop.service.user.OrderService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletRequest; +import java.util.List; + +@RestController +@RequestMapping("/order") +public class OrderController { + + @Autowired + private OrderService orderService; + + @PostMapping("/create") + public ApiResponse createOrder(@Validated @RequestBody OrderCreateRequest request, + HttpServletRequest httpRequest) { + Long userId = (Long) httpRequest.getAttribute("userId"); + if (userId == null) { + return ApiResponse.error("请先登录"); + } + Order order = orderService.createOrder(userId, request); + return ApiResponse.success(order); + } + + @PostMapping("/{id}/pay") + public ApiResponse payOrder(@PathVariable Long id, + @Validated @RequestBody PaymentRequest request, + HttpServletRequest httpRequest) { + Long userId = (Long) httpRequest.getAttribute("userId"); + if (userId == null) { + return ApiResponse.error("请先登录"); + } + try { + orderService.payOrder(userId, id, request); + return ApiResponse.success(null); + } catch (BusinessException e) { + return ApiResponse.error(e.getMessage()); + } + } + + @GetMapping("/orders") + public ApiResponse> getOrders(@RequestParam(required = false) String status, + HttpServletRequest request) { + Long userId = (Long) request.getAttribute("userId"); + if (userId == null) { + return ApiResponse.error("请先登录"); + } + List orders = orderService.getOrders(userId, status); + return ApiResponse.success(orders); + } + + @GetMapping("/orders/{id}") + public ApiResponse getOrderDetail(@PathVariable Long id, + HttpServletRequest request) { + Long userId = (Long) request.getAttribute("userId"); + if (userId == null) { + return ApiResponse.error("请先登录"); + } + OrderDetailResponse orderDetail = orderService.getOrderDetail(id, userId); + return ApiResponse.success(orderDetail); + } + + @GetMapping("/orders/{id}/track") + public ApiResponse> getOrderTrack(@PathVariable Long id, + HttpServletRequest request) { + Long userId = (Long) request.getAttribute("userId"); + if (userId == null) { + return ApiResponse.error("请先登录"); + } + try { + List tracks = orderService.getOrderTrack(userId, id); + return ApiResponse.success(tracks); + } catch (BusinessException e) { + return ApiResponse.error(e.getMessage()); + } + } +} diff --git a/src/main/java/com/nlshop/controller/user/ProductController.java b/src/main/java/com/nlshop/controller/user/ProductController.java new file mode 100644 index 0000000..4bf20c5 --- /dev/null +++ b/src/main/java/com/nlshop/controller/user/ProductController.java @@ -0,0 +1,80 @@ +package com.nlshop.controller.user; + +import com.nlshop.dto.response.ApiResponse; +import com.nlshop.dto.response.PageResponse; +import com.nlshop.entity.Product; +import com.nlshop.service.algorithm.CollaborativeFilteringService; +import com.nlshop.service.user.ProductService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletRequest; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/products") +public class ProductController { + + @Autowired + private ProductService productService; + + @Autowired + private CollaborativeFilteringService cfService; + + @GetMapping + public ApiResponse> getProducts( + @RequestParam(required = false) String category, + @RequestParam(defaultValue = "1") Integer pageNum, + @RequestParam(defaultValue = "10") Integer pageSize) { + PageResponse response = productService.getProducts(category, pageNum, pageSize); + return ApiResponse.success(response); + } + + @GetMapping("/{id}") + public ApiResponse> getProductDetail(@PathVariable Long id, + HttpServletRequest request) { + Long userId = null; + try { + userId = (Long) request.getAttribute("userId"); + } catch (Exception e) { + // 未登录用户也可以查看商品详情 + } + Map detail = productService.getProductDetail(id, userId); + return ApiResponse.success(detail); + } + + @GetMapping("/hot") + public ApiResponse> getHotProducts(@RequestParam(defaultValue = "10") int limit) { + List products = productService.getHotProducts(limit); + return ApiResponse.success(products); + } + + /** + * 批量记录浏览行为(用于登录后同步cookies中的浏览历史) + */ + @PostMapping("/record-views") + public ApiResponse recordViews(@RequestBody List productIds, + HttpServletRequest request) { + Long userId = (Long) request.getAttribute("userId"); + if (userId == null) { + return ApiResponse.error("请先登录"); + } + + if (productIds == null || productIds.isEmpty()) { + return ApiResponse.success(null); + } + + // 批量记录浏览行为 + for (Long productId : productIds) { + try { + cfService.recordView(userId, productId); + } catch (Exception e) { + // 单个失败不影响其他,只记录日志 + System.err.println("记录浏览行为失败: userId=" + userId + ", productId=" + productId + ", error=" + e.getMessage()); + } + } + + return ApiResponse.success(null); + } +} diff --git a/src/main/java/com/nlshop/controller/user/RecommendController.java b/src/main/java/com/nlshop/controller/user/RecommendController.java new file mode 100644 index 0000000..4b9d326 --- /dev/null +++ b/src/main/java/com/nlshop/controller/user/RecommendController.java @@ -0,0 +1,28 @@ +package com.nlshop.controller.user; + +import com.nlshop.dto.response.ApiResponse; +import com.nlshop.entity.Product; +import com.nlshop.service.user.RecommendService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletRequest; +import java.util.List; + +@RestController +@RequestMapping("/recommend") +public class RecommendController { + + @Autowired + private RecommendService recommendService; + + @GetMapping + public ApiResponse> getRecommendations( + @RequestParam(defaultValue = "10") int limit, + HttpServletRequest request) { + Long userId = (Long) request.getAttribute("userId"); + // 推荐功能可以允许未登录用户,userId可以为null + List products = recommendService.getRecommendations(userId, limit); + return ApiResponse.success(products); + } +} diff --git a/src/main/java/com/nlshop/controller/user/ReviewController.java b/src/main/java/com/nlshop/controller/user/ReviewController.java new file mode 100644 index 0000000..9518f09 --- /dev/null +++ b/src/main/java/com/nlshop/controller/user/ReviewController.java @@ -0,0 +1,97 @@ +package com.nlshop.controller.user; + +import com.nlshop.dto.response.ApiResponse; +import com.nlshop.entity.Review; +import com.nlshop.mapper.ReviewMapper; +import com.nlshop.service.algorithm.CollaborativeFilteringService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletRequest; +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +@RestController("userReviewController") +@RequestMapping("/review") +public class ReviewController { + + @Autowired + private ReviewMapper reviewMapper; + + @Autowired + private CollaborativeFilteringService cfService; + + @PostMapping + public ApiResponse createReview(@RequestParam Long orderId, + @RequestParam Long productId, + @RequestParam Integer rating, + @RequestParam(required = false) String content, + HttpServletRequest request) { + Long userId = (Long) request.getAttribute("userId"); + if (userId == null) { + return ApiResponse.error("请先登录"); + } + + // 检查是否已经评价过 + Review existing = reviewMapper.selectByOrderId(orderId); + if (existing != null) { + return ApiResponse.error("该订单已评价"); + } + + Review review = new Review(); + review.setUserId(userId); + review.setOrderId(orderId); + review.setProductId(productId); + review.setRating(rating); + review.setContent(content); + reviewMapper.insert(review); + + // 记录评分行为到user_behavior表(用于协同过滤算法) + cfService.recordRating(userId, productId, new BigDecimal(rating)); + + return ApiResponse.success(review); + } + + @GetMapping("/by-orders") + public ApiResponse> getReviewsByOrders(@RequestParam String orderIds, + HttpServletRequest request) { + Long userId = (Long) request.getAttribute("userId"); + if (userId == null) { + return ApiResponse.error("请先登录"); + } + + try { + List orderIdList = Arrays.stream(orderIds.split(",")) + .map(Long::parseLong) + .collect(Collectors.toList()); + + List reviews = reviewMapper.selectByOrderIdList(orderIdList); + return ApiResponse.success(reviews); + } catch (Exception e) { + return ApiResponse.error("参数错误"); + } + } + + @GetMapping("/by-order") + public ApiResponse getReviewByOrder(@RequestParam Long orderId, + HttpServletRequest request) { + Long userId = (Long) request.getAttribute("userId"); + if (userId == null) { + return ApiResponse.error("请先登录"); + } + + Review review = reviewMapper.selectByOrderId(orderId); + if (review == null) { + return ApiResponse.error("未找到评论"); + } + + // 验证评论是否属于当前用户 + if (!review.getUserId().equals(userId)) { + return ApiResponse.error("无权访问"); + } + + return ApiResponse.success(review); + } +} diff --git a/src/main/java/com/nlshop/controller/user/UserController.java b/src/main/java/com/nlshop/controller/user/UserController.java new file mode 100644 index 0000000..b5777fa --- /dev/null +++ b/src/main/java/com/nlshop/controller/user/UserController.java @@ -0,0 +1,145 @@ +package com.nlshop.controller.user; + +import com.nlshop.dto.request.LoginRequest; +import com.nlshop.dto.request.UserRegisterRequest; +import com.nlshop.dto.response.ApiResponse; +import com.nlshop.dto.response.LoginResponse; +import com.nlshop.entity.User; +import com.nlshop.entity.UserAddress; +import com.nlshop.exception.BusinessException; +import com.nlshop.service.user.UserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; +import java.util.List; + +@RestController +@RequestMapping("/") +public class UserController { + + @Autowired + private UserService userService; + + @PostMapping("/register") + public ApiResponse register(@Validated @RequestBody UserRegisterRequest request) { + User user = userService.register(request); + return ApiResponse.success(user); + } + + @PostMapping("/login") + public ApiResponse login(@Validated @RequestBody LoginRequest request, + HttpServletRequest httpRequest) { + User user = userService.login(request.getUsername(), request.getPassword()); + + // 将用户信息存入Session + HttpSession session = httpRequest.getSession(true); + session.setAttribute("userId", user.getId()); + session.setAttribute("userType", "user"); + session.setAttribute("username", user.getUsername()); + + LoginResponse response = new LoginResponse(); + response.setToken(""); // 不再使用token,设为空字符串 + response.setUserId(user.getId()); + response.setUserType("user"); + response.setUsername(user.getUsername()); + response.setName(user.getName()); + + return ApiResponse.success(response); + } + + @PostMapping("/logout") + public ApiResponse logout(HttpServletRequest request) { + HttpSession session = request.getSession(false); + if (session != null) { + session.invalidate(); + } + return ApiResponse.success(null); + } + + @GetMapping("/profile") + public ApiResponse getProfile(HttpServletRequest request) { + Long userId = (Long) request.getAttribute("userId"); + if (userId == null) { + return ApiResponse.error("请先登录"); + } + User user = userService.getProfile(userId); + return ApiResponse.success(user); + } + + @PutMapping("/profile") + public ApiResponse updateProfile(@RequestBody User user, HttpServletRequest request) { + Long userId = (Long) request.getAttribute("userId"); + if (userId == null) { + return ApiResponse.error("请先登录"); + } + User updated = userService.updateProfile(userId, user); + return ApiResponse.success(updated); + } + + @PostMapping("/avatar") + public ApiResponse uploadAvatar(@RequestParam("file") MultipartFile file, + HttpServletRequest request) { + Long userId = (Long) request.getAttribute("userId"); + if (userId == null) { + return ApiResponse.error("请先登录"); + } + // 文件上传逻辑在Controller中处理,这里简化 + return ApiResponse.success("上传成功"); + } + + @GetMapping("/address") + public ApiResponse> getAddresses(HttpServletRequest request) { + Long userId = (Long) request.getAttribute("userId"); + if (userId == null) { + return ApiResponse.error("请先登录"); + } + List addresses = userService.getAddresses(userId); + return ApiResponse.success(addresses); + } + + @PostMapping("/address") + public ApiResponse addAddress(@RequestBody UserAddress address, + HttpServletRequest request) { + Long userId = (Long) request.getAttribute("userId"); + if (userId == null) { + return ApiResponse.error("请先登录"); + } + UserAddress added = userService.addAddress(userId, address); + return ApiResponse.success(added); + } + + @PutMapping("/address/{id}") + public ApiResponse updateAddress(@PathVariable Long id, + @RequestBody UserAddress address, + HttpServletRequest request) { + Long userId = (Long) request.getAttribute("userId"); + if (userId == null) { + return ApiResponse.error("请先登录"); + } + try { + UserAddress updated = userService.updateAddress(userId, id, address); + return ApiResponse.success(updated); + } catch (BusinessException e) { + return ApiResponse.error(e.getMessage()); + } + } + + @DeleteMapping("/address/{id}") + public ApiResponse deleteAddress(@PathVariable Long id, + HttpServletRequest request) { + Long userId = (Long) request.getAttribute("userId"); + if (userId == null) { + return ApiResponse.error("请先登录"); + } + try { + userService.deleteAddress(userId, id); + return ApiResponse.success(null); + } catch (BusinessException e) { + return ApiResponse.error(e.getMessage()); + } + } +} diff --git a/src/main/java/com/nlshop/dto/request/CartAddRequest.java b/src/main/java/com/nlshop/dto/request/CartAddRequest.java new file mode 100644 index 0000000..4f67b45 --- /dev/null +++ b/src/main/java/com/nlshop/dto/request/CartAddRequest.java @@ -0,0 +1,19 @@ +package com.nlshop.dto.request; + +import lombok.Data; +import javax.validation.constraints.NotNull; + +@Data +public class CartAddRequest { + @NotNull(message = "商品ID不能为空") + private Long productId; + + private Long specId; + + @NotNull(message = "数量不能为空") + private Integer quantity; + + private String customSweetness; + private String customIce; + private String toppings; // JSON格式 +} diff --git a/src/main/java/com/nlshop/dto/request/LoginRequest.java b/src/main/java/com/nlshop/dto/request/LoginRequest.java new file mode 100644 index 0000000..3f0fc8e --- /dev/null +++ b/src/main/java/com/nlshop/dto/request/LoginRequest.java @@ -0,0 +1,13 @@ +package com.nlshop.dto.request; + +import lombok.Data; +import javax.validation.constraints.NotBlank; + +@Data +public class LoginRequest { + @NotBlank(message = "用户名不能为空") + private String username; + + @NotBlank(message = "密码不能为空") + private String password; +} diff --git a/src/main/java/com/nlshop/dto/request/MerchantRegisterRequest.java b/src/main/java/com/nlshop/dto/request/MerchantRegisterRequest.java new file mode 100644 index 0000000..b627d6a --- /dev/null +++ b/src/main/java/com/nlshop/dto/request/MerchantRegisterRequest.java @@ -0,0 +1,21 @@ +package com.nlshop.dto.request; + +import lombok.Data; +import javax.validation.constraints.NotBlank; + +@Data +public class MerchantRegisterRequest { + @NotBlank(message = "用户名不能为空") + private String username; + + @NotBlank(message = "密码不能为空") + private String password; + + @NotBlank(message = "门店名称不能为空") + private String storeName; + + private String address; + private String phone; + private String email; + private String managerName; +} diff --git a/src/main/java/com/nlshop/dto/request/OrderCreateRequest.java b/src/main/java/com/nlshop/dto/request/OrderCreateRequest.java new file mode 100644 index 0000000..6b7dcf6 --- /dev/null +++ b/src/main/java/com/nlshop/dto/request/OrderCreateRequest.java @@ -0,0 +1,17 @@ +package com.nlshop.dto.request; + +import lombok.Data; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +@Data +public class OrderCreateRequest { + @NotNull(message = "商家ID不能为空") + private Long merchantId; + + @NotBlank(message = "收货地址不能为空") + private String address; + + @NotBlank(message = "联系电话不能为空") + private String contactPhone; +} diff --git a/src/main/java/com/nlshop/dto/request/PaymentRequest.java b/src/main/java/com/nlshop/dto/request/PaymentRequest.java new file mode 100644 index 0000000..b915b3d --- /dev/null +++ b/src/main/java/com/nlshop/dto/request/PaymentRequest.java @@ -0,0 +1,10 @@ +package com.nlshop.dto.request; + +import lombok.Data; +import javax.validation.constraints.NotBlank; + +@Data +public class PaymentRequest { + @NotBlank(message = "支付方式不能为空") + private String paymentMethod; // wechat/alipay +} diff --git a/src/main/java/com/nlshop/dto/request/UserRegisterRequest.java b/src/main/java/com/nlshop/dto/request/UserRegisterRequest.java new file mode 100644 index 0000000..f5d5f48 --- /dev/null +++ b/src/main/java/com/nlshop/dto/request/UserRegisterRequest.java @@ -0,0 +1,24 @@ +package com.nlshop.dto.request; + +import lombok.Data; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; + +@Data +public class UserRegisterRequest { + @NotBlank(message = "用户名不能为空") + private String username; + + @NotBlank(message = "密码不能为空") + @Pattern(regexp = ".{6,}", message = "密码长度至少6位") + private String password; + + private String name; + private Integer gender; + + @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确") + private String phone; + + private String email; + private String address; +} diff --git a/src/main/java/com/nlshop/dto/response/ApiResponse.java b/src/main/java/com/nlshop/dto/response/ApiResponse.java new file mode 100644 index 0000000..c1fb95b --- /dev/null +++ b/src/main/java/com/nlshop/dto/response/ApiResponse.java @@ -0,0 +1,40 @@ +package com.nlshop.dto.response; + +import lombok.Data; + +@Data +public class ApiResponse { + private Integer code; + private String message; + private T data; + + public static ApiResponse success(T data) { + ApiResponse response = new ApiResponse<>(); + response.setCode(200); + response.setMessage("操作成功"); + response.setData(data); + return response; + } + + public static ApiResponse success(String message, T data) { + ApiResponse response = new ApiResponse<>(); + response.setCode(200); + response.setMessage(message); + response.setData(data); + return response; + } + + public static ApiResponse error(String message) { + ApiResponse response = new ApiResponse<>(); + response.setCode(500); + response.setMessage(message); + return response; + } + + public static ApiResponse error(Integer code, String message) { + ApiResponse response = new ApiResponse<>(); + response.setCode(code); + response.setMessage(message); + return response; + } +} diff --git a/src/main/java/com/nlshop/dto/response/LoginResponse.java b/src/main/java/com/nlshop/dto/response/LoginResponse.java new file mode 100644 index 0000000..2f03ef9 --- /dev/null +++ b/src/main/java/com/nlshop/dto/response/LoginResponse.java @@ -0,0 +1,12 @@ +package com.nlshop.dto.response; + +import lombok.Data; + +@Data +public class LoginResponse { + private String token; + private Long userId; + private String userType; // user/merchant + private String username; + private String name; +} diff --git a/src/main/java/com/nlshop/dto/response/OrderDetailResponse.java b/src/main/java/com/nlshop/dto/response/OrderDetailResponse.java new file mode 100644 index 0000000..9d9810e --- /dev/null +++ b/src/main/java/com/nlshop/dto/response/OrderDetailResponse.java @@ -0,0 +1,25 @@ +package com.nlshop.dto.response; + +import com.nlshop.entity.Order; +import com.nlshop.entity.OrderItem; +import com.nlshop.entity.OrderTrack; +import lombok.Data; +import java.util.List; +import java.util.Map; + +@Data +public class OrderDetailResponse { + private Order order; + private List items; + private List tracks; + private Map productImages; // 商品ID到图片URL的映射 + + public static OrderDetailResponse of(Order order, List items, List tracks, Map productImages) { + OrderDetailResponse response = new OrderDetailResponse(); + response.setOrder(order); + response.setItems(items); + response.setTracks(tracks); + response.setProductImages(productImages); + return response; + } +} diff --git a/src/main/java/com/nlshop/dto/response/PageResponse.java b/src/main/java/com/nlshop/dto/response/PageResponse.java new file mode 100644 index 0000000..d56c9d4 --- /dev/null +++ b/src/main/java/com/nlshop/dto/response/PageResponse.java @@ -0,0 +1,23 @@ +package com.nlshop.dto.response; + +import lombok.Data; +import java.util.List; + +@Data +public class PageResponse { + private List list; + private Long total; + private Integer pageNum; + private Integer pageSize; + private Integer pages; + + public static PageResponse of(List list, Long total, Integer pageNum, Integer pageSize) { + PageResponse response = new PageResponse<>(); + response.setList(list); + response.setTotal(total); + response.setPageNum(pageNum); + response.setPageSize(pageSize); + response.setPages((int) Math.ceil((double) total / pageSize)); + return response; + } +} diff --git a/src/main/java/com/nlshop/entity/Announcement.java b/src/main/java/com/nlshop/entity/Announcement.java new file mode 100644 index 0000000..84f722d --- /dev/null +++ b/src/main/java/com/nlshop/entity/Announcement.java @@ -0,0 +1,15 @@ +package com.nlshop.entity; + +import lombok.Data; +import java.time.LocalDateTime; + +@Data +public class Announcement { + private Long id; + private Long merchantId; + private String title; + private String content; + private String type; // ACTIVITY-活动,NOTICE-通知 + private Integer status; // 0-下架,1-发布 + private LocalDateTime createTime; +} diff --git a/src/main/java/com/nlshop/entity/CartItem.java b/src/main/java/com/nlshop/entity/CartItem.java new file mode 100644 index 0000000..517a924 --- /dev/null +++ b/src/main/java/com/nlshop/entity/CartItem.java @@ -0,0 +1,21 @@ +package com.nlshop.entity; + +import lombok.Data; +import java.time.LocalDateTime; + +@Data +public class CartItem { + private Long id; + private Long userId; + private Long productId; + private Long specId; + private Integer quantity; + private String customSweetness; + private String customIce; + private String toppings; // JSON格式 + private LocalDateTime createTime; + + // 关联查询字段 + private Product product; + private ProductSpec spec; +} diff --git a/src/main/java/com/nlshop/entity/Inventory.java b/src/main/java/com/nlshop/entity/Inventory.java new file mode 100644 index 0000000..f6e89fb --- /dev/null +++ b/src/main/java/com/nlshop/entity/Inventory.java @@ -0,0 +1,17 @@ +package com.nlshop.entity; + +import lombok.Data; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Data +public class Inventory { + private Long id; + private Long merchantId; + private String materialName; + private BigDecimal quantity; + private String unit; + private BigDecimal minThreshold; + private LocalDateTime lastInTime; + private LocalDateTime lastOutTime; +} diff --git a/src/main/java/com/nlshop/entity/InventoryLog.java b/src/main/java/com/nlshop/entity/InventoryLog.java new file mode 100644 index 0000000..e99f3a7 --- /dev/null +++ b/src/main/java/com/nlshop/entity/InventoryLog.java @@ -0,0 +1,15 @@ +package com.nlshop.entity; + +import lombok.Data; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Data +public class InventoryLog { + private Long id; + private Long inventoryId; + private String operationType; // IN-入库,OUT-出库 + private BigDecimal quantity; + private String operator; + private LocalDateTime createTime; +} diff --git a/src/main/java/com/nlshop/entity/Merchant.java b/src/main/java/com/nlshop/entity/Merchant.java new file mode 100644 index 0000000..52369a2 --- /dev/null +++ b/src/main/java/com/nlshop/entity/Merchant.java @@ -0,0 +1,19 @@ +package com.nlshop.entity; + +import lombok.Data; +import java.time.LocalDateTime; + +@Data +public class Merchant { + private Long id; + private String username; + private String password; + private String storeName; + private String address; + private String phone; + private String email; + private String managerName; + private String avatar; + private LocalDateTime createTime; + private LocalDateTime updateTime; +} diff --git a/src/main/java/com/nlshop/entity/Message.java b/src/main/java/com/nlshop/entity/Message.java new file mode 100644 index 0000000..3bc7e3d --- /dev/null +++ b/src/main/java/com/nlshop/entity/Message.java @@ -0,0 +1,21 @@ +package com.nlshop.entity; + +import lombok.Data; +import java.time.LocalDateTime; + +@Data +public class Message { + private Long id; + private Long userId; + private Long merchantId; + private Long orderId; + private String content; + private String reply; + private Integer status; // 0-未回复,1-已回复 + private LocalDateTime createTime; + private LocalDateTime replyTime; + + // 关联查询字段 + private User user; + private Order order; +} diff --git a/src/main/java/com/nlshop/entity/Order.java b/src/main/java/com/nlshop/entity/Order.java new file mode 100644 index 0000000..f6c49a2 --- /dev/null +++ b/src/main/java/com/nlshop/entity/Order.java @@ -0,0 +1,25 @@ +package com.nlshop.entity; + +import lombok.Data; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Data +public class Order { + private Long id; + private String orderNo; + private Long userId; + private Long merchantId; + private BigDecimal totalAmount; + private String paymentMethod; // wechat/alipay + private Integer paymentStatus; // 0-未支付,1-已支付 + private String orderStatus; // PENDING_PAY, MAKING, READY, COMPLETED, CANCELLED + private String address; + private String contactPhone; + private LocalDateTime createTime; + private LocalDateTime payTime; + private LocalDateTime completeTime; + + // 关联查询字段 + private Merchant merchant; +} diff --git a/src/main/java/com/nlshop/entity/OrderItem.java b/src/main/java/com/nlshop/entity/OrderItem.java new file mode 100644 index 0000000..8dd3d70 --- /dev/null +++ b/src/main/java/com/nlshop/entity/OrderItem.java @@ -0,0 +1,19 @@ +package com.nlshop.entity; + +import lombok.Data; +import java.math.BigDecimal; + +@Data +public class OrderItem { + private Long id; + private Long orderId; + private Long productId; + private String productName; + private String specName; + private Integer quantity; + private BigDecimal price; + private String customSweetness; + private String customIce; + private String toppings; // JSON格式 + private BigDecimal subtotal; +} diff --git a/src/main/java/com/nlshop/entity/OrderTrack.java b/src/main/java/com/nlshop/entity/OrderTrack.java new file mode 100644 index 0000000..9b3bc53 --- /dev/null +++ b/src/main/java/com/nlshop/entity/OrderTrack.java @@ -0,0 +1,13 @@ +package com.nlshop.entity; + +import lombok.Data; +import java.time.LocalDateTime; + +@Data +public class OrderTrack { + private Long id; + private Long orderId; + private String status; + private String description; + private LocalDateTime createTime; +} diff --git a/src/main/java/com/nlshop/entity/Product.java b/src/main/java/com/nlshop/entity/Product.java new file mode 100644 index 0000000..94f7a51 --- /dev/null +++ b/src/main/java/com/nlshop/entity/Product.java @@ -0,0 +1,18 @@ +package com.nlshop.entity; + +import lombok.Data; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Data +public class Product { + private Long id; + private Long merchantId; + private String name; + private String category; + private String description; + private BigDecimal basePrice; + private String image; + private Integer status; // 0-下架,1-上架 + private LocalDateTime createTime; +} diff --git a/src/main/java/com/nlshop/entity/ProductCustom.java b/src/main/java/com/nlshop/entity/ProductCustom.java new file mode 100644 index 0000000..a412f1a --- /dev/null +++ b/src/main/java/com/nlshop/entity/ProductCustom.java @@ -0,0 +1,13 @@ +package com.nlshop.entity; + +import lombok.Data; +import java.math.BigDecimal; + +@Data +public class ProductCustom { + private Long id; + private Long productId; + private String optionType; // sweetness-甜度,ice-冰度 + private String optionValue; + private BigDecimal priceAdjust; +} diff --git a/src/main/java/com/nlshop/entity/ProductSpec.java b/src/main/java/com/nlshop/entity/ProductSpec.java new file mode 100644 index 0000000..a0ab34a --- /dev/null +++ b/src/main/java/com/nlshop/entity/ProductSpec.java @@ -0,0 +1,12 @@ +package com.nlshop.entity; + +import lombok.Data; +import java.math.BigDecimal; + +@Data +public class ProductSpec { + private Long id; + private Long productId; + private String specName; + private BigDecimal priceAdjust; +} diff --git a/src/main/java/com/nlshop/entity/ProductTopping.java b/src/main/java/com/nlshop/entity/ProductTopping.java new file mode 100644 index 0000000..1ff533c --- /dev/null +++ b/src/main/java/com/nlshop/entity/ProductTopping.java @@ -0,0 +1,12 @@ +package com.nlshop.entity; + +import lombok.Data; +import java.math.BigDecimal; + +@Data +public class ProductTopping { + private Long id; + private Long productId; + private String toppingName; + private BigDecimal price; +} diff --git a/src/main/java/com/nlshop/entity/Review.java b/src/main/java/com/nlshop/entity/Review.java new file mode 100644 index 0000000..6dfcffc --- /dev/null +++ b/src/main/java/com/nlshop/entity/Review.java @@ -0,0 +1,20 @@ +package com.nlshop.entity; + +import lombok.Data; +import java.time.LocalDateTime; + +@Data +public class Review { + private Long id; + private Long userId; + private Long orderId; + private Long productId; + private Integer rating; // 1-5 + private String content; + private String reply; + private LocalDateTime createTime; + + // 关联查询字段 + private User user; + private Product product; +} diff --git a/src/main/java/com/nlshop/entity/User.java b/src/main/java/com/nlshop/entity/User.java new file mode 100644 index 0000000..fb261fa --- /dev/null +++ b/src/main/java/com/nlshop/entity/User.java @@ -0,0 +1,19 @@ +package com.nlshop.entity; + +import lombok.Data; +import java.time.LocalDateTime; + +@Data +public class User { + private Long id; + private String username; + private String password; + private String name; + private Integer gender; // 0-女,1-男 + private String phone; + private String email; + private String avatar; + private String address; + private LocalDateTime createTime; + private LocalDateTime updateTime; +} diff --git a/src/main/java/com/nlshop/entity/UserAddress.java b/src/main/java/com/nlshop/entity/UserAddress.java new file mode 100644 index 0000000..c96ac78 --- /dev/null +++ b/src/main/java/com/nlshop/entity/UserAddress.java @@ -0,0 +1,15 @@ +package com.nlshop.entity; + +import lombok.Data; +import java.time.LocalDateTime; + +@Data +public class UserAddress { + private Long id; + private Long userId; + private String address; + private String contactName; + private String contactPhone; + private Integer isDefault; // 0-否,1-是 + private LocalDateTime createTime; +} diff --git a/src/main/java/com/nlshop/entity/UserBehavior.java b/src/main/java/com/nlshop/entity/UserBehavior.java new file mode 100644 index 0000000..e270003 --- /dev/null +++ b/src/main/java/com/nlshop/entity/UserBehavior.java @@ -0,0 +1,15 @@ +package com.nlshop.entity; + +import lombok.Data; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Data +public class UserBehavior { + private Long id; + private Long userId; + private Long productId; + private String behaviorType; // VIEW-浏览,PURCHASE-购买,RATING-评分 + private BigDecimal score; // 0-5 + private LocalDateTime createTime; +} diff --git a/src/main/java/com/nlshop/entity/UserSimilarity.java b/src/main/java/com/nlshop/entity/UserSimilarity.java new file mode 100644 index 0000000..67b0a68 --- /dev/null +++ b/src/main/java/com/nlshop/entity/UserSimilarity.java @@ -0,0 +1,13 @@ +package com.nlshop.entity; + +import lombok.Data; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Data +public class UserSimilarity { + private Long user1Id; + private Long user2Id; + private BigDecimal similarityScore; + private LocalDateTime updateTime; +} diff --git a/src/main/java/com/nlshop/exception/BusinessException.java b/src/main/java/com/nlshop/exception/BusinessException.java new file mode 100644 index 0000000..f967af4 --- /dev/null +++ b/src/main/java/com/nlshop/exception/BusinessException.java @@ -0,0 +1,18 @@ +package com.nlshop.exception; + +import lombok.Getter; + +@Getter +public class BusinessException extends RuntimeException { + private Integer code; + + public BusinessException(String message) { + super(message); + this.code = 500; + } + + public BusinessException(Integer code, String message) { + super(message); + this.code = code; + } +} diff --git a/src/main/java/com/nlshop/exception/GlobalExceptionHandler.java b/src/main/java/com/nlshop/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..ca62595 --- /dev/null +++ b/src/main/java/com/nlshop/exception/GlobalExceptionHandler.java @@ -0,0 +1,54 @@ +package com.nlshop.exception; + +import com.nlshop.dto.response.ApiResponse; +import org.springframework.validation.BindException; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; + +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import java.util.Set; + +@ControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(Exception.class) + @ResponseBody + public ApiResponse handleException(Exception e) { + e.printStackTrace(); + return ApiResponse.error("系统错误: " + e.getMessage()); + } + + @ExceptionHandler(BusinessException.class) + @ResponseBody + public ApiResponse handleBusinessException(BusinessException e) { + return ApiResponse.error(e.getCode(), e.getMessage()); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + @ResponseBody + public ApiResponse handleValidationException(MethodArgumentNotValidException e) { + FieldError fieldError = e.getBindingResult().getFieldError(); + String message = fieldError != null ? fieldError.getDefaultMessage() : "参数验证失败"; + return ApiResponse.error(400, message); + } + + @ExceptionHandler(BindException.class) + @ResponseBody + public ApiResponse handleBindException(BindException e) { + FieldError fieldError = e.getBindingResult().getFieldError(); + String message = fieldError != null ? fieldError.getDefaultMessage() : "参数验证失败"; + return ApiResponse.error(400, message); + } + + @ExceptionHandler(ConstraintViolationException.class) + @ResponseBody + public ApiResponse handleConstraintViolationException(ConstraintViolationException e) { + Set> violations = e.getConstraintViolations(); + String message = violations.iterator().next().getMessage(); + return ApiResponse.error(400, message); + } +} diff --git a/src/main/java/com/nlshop/interceptor/AuthInterceptor.java b/src/main/java/com/nlshop/interceptor/AuthInterceptor.java new file mode 100644 index 0000000..1ff34ee --- /dev/null +++ b/src/main/java/com/nlshop/interceptor/AuthInterceptor.java @@ -0,0 +1,74 @@ +package com.nlshop.interceptor; + +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.io.IOException; + +@Component +public class AuthInterceptor implements HandlerInterceptor { + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + // 允许OPTIONS请求 + if ("OPTIONS".equals(request.getMethod())) { + return true; + } + + String requestPath = request.getRequestURI(); + boolean isHtmlPage = requestPath.endsWith(".html"); + boolean isMerchantPath = requestPath.startsWith("/merchant"); + + // 从Session中获取用户信息 + HttpSession session = request.getSession(false); + Long userId = null; + String userType = null; + + if (session != null) { + userId = (Long) session.getAttribute("userId"); + userType = (String) session.getAttribute("userType"); + + if (userId != null && userType != null) { + // 将用户信息存入request + request.setAttribute("userId", userId); + request.setAttribute("userType", userType); + } + } + + // 检查用户是否已登录 + boolean isLoggedIn = (userId != null && userType != null); + + // 如果未登录,进行拦截 + if (!isLoggedIn) { + // 保存原始URL到session,登录后可以跳转回来 + if (session != null && !requestPath.equals("/login.html") && + !requestPath.equals("/register.html") && + !requestPath.equals("/merchant/login.html") && + !requestPath.equals("/merchant/register.html")) { + session.setAttribute("returnUrl", requestPath); + } + + if (isHtmlPage) { + // HTML页面请求:重定向到登录页 + if (isMerchantPath) { + response.sendRedirect("/merchant/login.html"); + } else { + response.sendRedirect("/login.html"); + } + return false; + } else { + // API请求:返回401 JSON错误 + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write("{\"code\":401,\"message\":\"请先登录\"}"); + return false; + } + } + + // 已登录,允许继续 + return true; + } +} diff --git a/src/main/java/com/nlshop/mapper/AnnouncementMapper.java b/src/main/java/com/nlshop/mapper/AnnouncementMapper.java new file mode 100644 index 0000000..31dd757 --- /dev/null +++ b/src/main/java/com/nlshop/mapper/AnnouncementMapper.java @@ -0,0 +1,15 @@ +package com.nlshop.mapper; + +import com.nlshop.entity.Announcement; +import org.apache.ibatis.annotations.Mapper; +import java.util.List; + +@Mapper +public interface AnnouncementMapper { + int insert(Announcement announcement); + List selectByMerchantId(Long merchantId); + List selectPublished(); + Announcement selectById(Long id); + int update(Announcement announcement); + int delete(Long id); +} diff --git a/src/main/java/com/nlshop/mapper/CartMapper.java b/src/main/java/com/nlshop/mapper/CartMapper.java new file mode 100644 index 0000000..590f2b4 --- /dev/null +++ b/src/main/java/com/nlshop/mapper/CartMapper.java @@ -0,0 +1,21 @@ +package com.nlshop.mapper; + +import com.nlshop.entity.CartItem; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import java.util.List; + +@Mapper +public interface CartMapper { + int insert(CartItem item); + List selectByUserId(Long userId); + CartItem selectById(Long id); + CartItem selectByUserAndProduct(@Param("userId") Long userId, @Param("productId") Long productId, + @Param("specId") Long specId, @Param("customSweetness") String customSweetness, + @Param("customIce") String customIce, @Param("toppings") String toppings); + int update(CartItem item); + int updateQuantity(@Param("id") Long id, @Param("quantity") Integer quantity); + int delete(Long id); + int deleteByUserId(Long userId); + int deleteBatch(@Param("ids") List ids, @Param("userId") Long userId); +} diff --git a/src/main/java/com/nlshop/mapper/InventoryLogMapper.java b/src/main/java/com/nlshop/mapper/InventoryLogMapper.java new file mode 100644 index 0000000..2a72a8e --- /dev/null +++ b/src/main/java/com/nlshop/mapper/InventoryLogMapper.java @@ -0,0 +1,11 @@ +package com.nlshop.mapper; + +import com.nlshop.entity.InventoryLog; +import org.apache.ibatis.annotations.Mapper; +import java.util.List; + +@Mapper +public interface InventoryLogMapper { + int insert(InventoryLog log); + List selectByInventoryId(Long inventoryId); +} diff --git a/src/main/java/com/nlshop/mapper/InventoryMapper.java b/src/main/java/com/nlshop/mapper/InventoryMapper.java new file mode 100644 index 0000000..78a532b --- /dev/null +++ b/src/main/java/com/nlshop/mapper/InventoryMapper.java @@ -0,0 +1,17 @@ +package com.nlshop.mapper; + +import com.nlshop.entity.Inventory; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import java.util.List; + +@Mapper +public interface InventoryMapper { + int insert(Inventory inventory); + List selectByMerchantId(Long merchantId); + Inventory selectById(Long id); + Inventory selectByMerchantAndMaterial(@Param("merchantId") Long merchantId, @Param("materialName") String materialName); + int update(Inventory inventory); + int delete(Long id); + List selectLowStock(Long merchantId); +} diff --git a/src/main/java/com/nlshop/mapper/MerchantMapper.java b/src/main/java/com/nlshop/mapper/MerchantMapper.java new file mode 100644 index 0000000..4c545c9 --- /dev/null +++ b/src/main/java/com/nlshop/mapper/MerchantMapper.java @@ -0,0 +1,12 @@ +package com.nlshop.mapper; + +import com.nlshop.entity.Merchant; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface MerchantMapper { + int insert(Merchant merchant); + Merchant selectById(Long id); + Merchant selectByUsername(String username); + int update(Merchant merchant); +} diff --git a/src/main/java/com/nlshop/mapper/MessageMapper.java b/src/main/java/com/nlshop/mapper/MessageMapper.java new file mode 100644 index 0000000..afc7d65 --- /dev/null +++ b/src/main/java/com/nlshop/mapper/MessageMapper.java @@ -0,0 +1,14 @@ +package com.nlshop.mapper; + +import com.nlshop.entity.Message; +import org.apache.ibatis.annotations.Mapper; +import java.util.List; + +@Mapper +public interface MessageMapper { + int insert(Message message); + List selectByUserId(Long userId); + List selectByMerchantId(Long merchantId); + Message selectById(Long id); + int update(Message message); +} diff --git a/src/main/java/com/nlshop/mapper/OrderItemMapper.java b/src/main/java/com/nlshop/mapper/OrderItemMapper.java new file mode 100644 index 0000000..2bbe7f2 --- /dev/null +++ b/src/main/java/com/nlshop/mapper/OrderItemMapper.java @@ -0,0 +1,12 @@ +package com.nlshop.mapper; + +import com.nlshop.entity.OrderItem; +import org.apache.ibatis.annotations.Mapper; +import java.util.List; + +@Mapper +public interface OrderItemMapper { + int insert(OrderItem item); + List selectByOrderId(Long orderId); + OrderItem selectById(Long id); +} diff --git a/src/main/java/com/nlshop/mapper/OrderMapper.java b/src/main/java/com/nlshop/mapper/OrderMapper.java new file mode 100644 index 0000000..067b112 --- /dev/null +++ b/src/main/java/com/nlshop/mapper/OrderMapper.java @@ -0,0 +1,22 @@ +package com.nlshop.mapper; + +import com.nlshop.entity.Order; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import java.util.List; + +@Mapper +public interface OrderMapper { + int insert(Order order); + Order selectById(Long id); + Order selectByOrderNo(String orderNo); + List selectByUserId(@Param("userId") Long userId, @Param("status") String status); + List selectByMerchantId(@Param("merchantId") Long merchantId, @Param("status") String status); + int update(Order order); + int updateStatus(@Param("id") Long id, @Param("status") String status); + int updatePayment(@Param("id") Long id, @Param("paymentStatus") Integer paymentStatus, + @Param("paymentMethod") String paymentMethod); + List> selectStickyUsers(@Param("merchantId") Long merchantId, + @Param("days") int days, + @Param("minOrders") int minOrders); +} diff --git a/src/main/java/com/nlshop/mapper/OrderTrackMapper.java b/src/main/java/com/nlshop/mapper/OrderTrackMapper.java new file mode 100644 index 0000000..6bf8e99 --- /dev/null +++ b/src/main/java/com/nlshop/mapper/OrderTrackMapper.java @@ -0,0 +1,11 @@ +package com.nlshop.mapper; + +import com.nlshop.entity.OrderTrack; +import org.apache.ibatis.annotations.Mapper; +import java.util.List; + +@Mapper +public interface OrderTrackMapper { + int insert(OrderTrack track); + List selectByOrderId(Long orderId); +} diff --git a/src/main/java/com/nlshop/mapper/ProductCustomMapper.java b/src/main/java/com/nlshop/mapper/ProductCustomMapper.java new file mode 100644 index 0000000..017e0b7 --- /dev/null +++ b/src/main/java/com/nlshop/mapper/ProductCustomMapper.java @@ -0,0 +1,15 @@ +package com.nlshop.mapper; + +import com.nlshop.entity.ProductCustom; +import org.apache.ibatis.annotations.Mapper; +import java.util.List; + +@Mapper +public interface ProductCustomMapper { + int insert(ProductCustom custom); + List selectByProductId(Long productId); + ProductCustom selectById(Long id); + int update(ProductCustom custom); + int delete(Long id); + int deleteByProductId(Long productId); +} diff --git a/src/main/java/com/nlshop/mapper/ProductMapper.java b/src/main/java/com/nlshop/mapper/ProductMapper.java new file mode 100644 index 0000000..0f271b8 --- /dev/null +++ b/src/main/java/com/nlshop/mapper/ProductMapper.java @@ -0,0 +1,17 @@ +package com.nlshop.mapper; + +import com.nlshop.entity.Product; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import java.util.List; + +@Mapper +public interface ProductMapper { + int insert(Product product); + Product selectById(Long id); + List selectByMerchantId(@Param("merchantId") Long merchantId, @Param("status") Integer status); + List selectAll(@Param("category") String category, @Param("status") Integer status); + int update(Product product); + int updateStatus(@Param("id") Long id, @Param("status") Integer status); + List selectHotProducts(int limit); +} diff --git a/src/main/java/com/nlshop/mapper/ProductSpecMapper.java b/src/main/java/com/nlshop/mapper/ProductSpecMapper.java new file mode 100644 index 0000000..95fab65 --- /dev/null +++ b/src/main/java/com/nlshop/mapper/ProductSpecMapper.java @@ -0,0 +1,15 @@ +package com.nlshop.mapper; + +import com.nlshop.entity.ProductSpec; +import org.apache.ibatis.annotations.Mapper; +import java.util.List; + +@Mapper +public interface ProductSpecMapper { + int insert(ProductSpec spec); + List selectByProductId(Long productId); + ProductSpec selectById(Long id); + int update(ProductSpec spec); + int delete(Long id); + int deleteByProductId(Long productId); +} diff --git a/src/main/java/com/nlshop/mapper/ProductToppingMapper.java b/src/main/java/com/nlshop/mapper/ProductToppingMapper.java new file mode 100644 index 0000000..86e7f8e --- /dev/null +++ b/src/main/java/com/nlshop/mapper/ProductToppingMapper.java @@ -0,0 +1,15 @@ +package com.nlshop.mapper; + +import com.nlshop.entity.ProductTopping; +import org.apache.ibatis.annotations.Mapper; +import java.util.List; + +@Mapper +public interface ProductToppingMapper { + int insert(ProductTopping topping); + List selectByProductId(Long productId); + ProductTopping selectById(Long id); + int update(ProductTopping topping); + int delete(Long id); + int deleteByProductId(Long productId); +} diff --git a/src/main/java/com/nlshop/mapper/ReviewMapper.java b/src/main/java/com/nlshop/mapper/ReviewMapper.java new file mode 100644 index 0000000..9364e37 --- /dev/null +++ b/src/main/java/com/nlshop/mapper/ReviewMapper.java @@ -0,0 +1,17 @@ +package com.nlshop.mapper; + +import com.nlshop.entity.Review; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import java.util.List; + +@Mapper +public interface ReviewMapper { + int insert(Review review); + List selectByMerchantId(Long merchantId); + List selectByProductId(Long productId); + Review selectById(Long id); + Review selectByOrderId(Long orderId); + List selectByOrderIdList(@Param("orderIds") List orderIds); + int update(Review review); +} diff --git a/src/main/java/com/nlshop/mapper/UserAddressMapper.java b/src/main/java/com/nlshop/mapper/UserAddressMapper.java new file mode 100644 index 0000000..1277dd0 --- /dev/null +++ b/src/main/java/com/nlshop/mapper/UserAddressMapper.java @@ -0,0 +1,15 @@ +package com.nlshop.mapper; + +import com.nlshop.entity.UserAddress; +import org.apache.ibatis.annotations.Mapper; +import java.util.List; + +@Mapper +public interface UserAddressMapper { + int insert(UserAddress address); + List selectByUserId(Long userId); + UserAddress selectById(Long id); + int update(UserAddress address); + int delete(Long id); + UserAddress selectDefaultByUserId(Long userId); +} diff --git a/src/main/java/com/nlshop/mapper/UserBehaviorMapper.java b/src/main/java/com/nlshop/mapper/UserBehaviorMapper.java new file mode 100644 index 0000000..d910c84 --- /dev/null +++ b/src/main/java/com/nlshop/mapper/UserBehaviorMapper.java @@ -0,0 +1,19 @@ +package com.nlshop.mapper; + +import com.nlshop.entity.UserBehavior; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import java.util.List; + +@Mapper +public interface UserBehaviorMapper { + int insert(UserBehavior behavior); + List selectByUserId(Long userId); + List selectByProductId(Long productId); + UserBehavior selectByUserAndProduct(@Param("userId") Long userId, @Param("productId") Long productId, + @Param("behaviorType") String behaviorType); + int update(UserBehavior behavior); + List selectProductIdsByUserId(Long userId); + List selectUserIdsByProductId(Long productId); + List> selectActiveUsers(@Param("merchantId") Long merchantId, @Param("days") int days); +} diff --git a/src/main/java/com/nlshop/mapper/UserMapper.java b/src/main/java/com/nlshop/mapper/UserMapper.java new file mode 100644 index 0000000..2321887 --- /dev/null +++ b/src/main/java/com/nlshop/mapper/UserMapper.java @@ -0,0 +1,16 @@ +package com.nlshop.mapper; + +import com.nlshop.entity.User; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import java.util.List; + +@Mapper +public interface UserMapper { + int insert(User user); + User selectById(Long id); + User selectByUsername(String username); + int update(User user); + int updatePassword(@Param("id") Long id, @Param("password") String password); + List selectAll(); +} diff --git a/src/main/java/com/nlshop/mapper/UserSimilarityMapper.java b/src/main/java/com/nlshop/mapper/UserSimilarityMapper.java new file mode 100644 index 0000000..07f44c1 --- /dev/null +++ b/src/main/java/com/nlshop/mapper/UserSimilarityMapper.java @@ -0,0 +1,13 @@ +package com.nlshop.mapper; + +import com.nlshop.entity.UserSimilarity; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import java.util.List; + +@Mapper +public interface UserSimilarityMapper { + int insertOrUpdate(UserSimilarity similarity); + UserSimilarity selectByUserIds(@Param("user1Id") Long user1Id, @Param("user2Id") Long user2Id); + List selectSimilarUsers(@Param("userId") Long userId, @Param("limit") int limit); +} diff --git a/src/main/java/com/nlshop/service/algorithm/CollaborativeFilteringService.java b/src/main/java/com/nlshop/service/algorithm/CollaborativeFilteringService.java new file mode 100644 index 0000000..e025e3b --- /dev/null +++ b/src/main/java/com/nlshop/service/algorithm/CollaborativeFilteringService.java @@ -0,0 +1,324 @@ +package com.nlshop.service.algorithm; + +import com.nlshop.entity.Product; +import com.nlshop.entity.UserBehavior; +import com.nlshop.entity.UserSimilarity; +import com.nlshop.mapper.ProductMapper; +import com.nlshop.mapper.UserBehaviorMapper; +import com.nlshop.mapper.UserSimilarityMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.*; +import java.util.stream.Collectors; + +@Service +public class CollaborativeFilteringService { + + @Autowired + private UserBehaviorMapper userBehaviorMapper; + + @Autowired + private UserSimilarityMapper userSimilarityMapper; + + @Autowired + private ProductMapper productMapper; + + /** + * 计算用户相似度矩阵 + */ + public void calculateUserSimilarity(Long userId) { + List productIds = userBehaviorMapper.selectProductIdsByUserId(userId); + if (productIds.isEmpty()) { + return; + } + + // 获取所有用户 + Set allUserIds = new HashSet<>(); + for (Long productId : productIds) { + allUserIds.addAll(userBehaviorMapper.selectUserIdsByProductId(productId)); + } + allUserIds.remove(userId); + + // 计算与每个用户的相似度 + Map> userProductScores = buildUserProductMatrix(userId, allUserIds); + + for (Long otherUserId : allUserIds) { + BigDecimal similarity = calculateCosineSimilarity( + userProductScores.get(userId), + userProductScores.get(otherUserId) + ); + + if (similarity.compareTo(BigDecimal.ZERO) > 0) { + UserSimilarity userSimilarity = new UserSimilarity(); + userSimilarity.setUser1Id(Math.min(userId, otherUserId)); + userSimilarity.setUser2Id(Math.max(userId, otherUserId)); + userSimilarity.setSimilarityScore(similarity); + userSimilarityMapper.insertOrUpdate(userSimilarity); + } + } + } + + /** + * 构建用户-商品评分矩阵 + */ + private Map> buildUserProductMatrix(Long userId, Set otherUserIds) { + Map> matrix = new HashMap<>(); + + // 当前用户的评分 + Map userScores = new HashMap<>(); + List userBehaviors = userBehaviorMapper.selectByUserId(userId); + for (UserBehavior behavior : userBehaviors) { + BigDecimal score = calculateBehaviorScore(behavior); + userScores.put(behavior.getProductId(), score); + } + matrix.put(userId, userScores); + + // 其他用户的评分 + for (Long otherUserId : otherUserIds) { + Map otherScores = new HashMap<>(); + List otherBehaviors = userBehaviorMapper.selectByUserId(otherUserId); + for (UserBehavior behavior : otherBehaviors) { + BigDecimal score = calculateBehaviorScore(behavior); + otherScores.put(behavior.getProductId(), score); + } + matrix.put(otherUserId, otherScores); + } + + return matrix; + } + + /** + * 计算行为评分(综合购买、评分、浏览) + */ + private BigDecimal calculateBehaviorScore(UserBehavior behavior) { + BigDecimal score = BigDecimal.ZERO; + + if ("PURCHASE".equals(behavior.getBehaviorType())) { + score = score.add(new BigDecimal("3.0")); // 购买权重3 + } else if ("RATING".equals(behavior.getBehaviorType())) { + score = score.add(behavior.getScore()); // 评分直接使用 + } else if ("VIEW".equals(behavior.getBehaviorType())) { + score = score.add(new BigDecimal("1.0")); // 浏览权重1 + } + + return score; + } + + /** + * 计算余弦相似度 + */ + private BigDecimal calculateCosineSimilarity(Map user1Scores, Map user2Scores) { + Set commonProducts = new HashSet<>(user1Scores.keySet()); + commonProducts.retainAll(user2Scores.keySet()); + + if (commonProducts.isEmpty()) { + return BigDecimal.ZERO; + } + + BigDecimal dotProduct = BigDecimal.ZERO; + BigDecimal norm1 = BigDecimal.ZERO; + BigDecimal norm2 = BigDecimal.ZERO; + + for (Long productId : commonProducts) { + BigDecimal score1 = user1Scores.get(productId); + BigDecimal score2 = user2Scores.get(productId); + dotProduct = dotProduct.add(score1.multiply(score2)); + norm1 = norm1.add(score1.multiply(score1)); + norm2 = norm2.add(score2.multiply(score2)); + } + + if (norm1.compareTo(BigDecimal.ZERO) == 0 || norm2.compareTo(BigDecimal.ZERO) == 0) { + return BigDecimal.ZERO; + } + + BigDecimal denominator = BigDecimal.valueOf(Math.sqrt(norm1.doubleValue() * norm2.doubleValue())); + return dotProduct.divide(denominator, 6, RoundingMode.HALF_UP); + } + + /** + * 获取推荐商品列表 + */ + public List getRecommendations(Long userId, int limit) { + // 获取用户已购买的商品 + List purchasedProductIds = userBehaviorMapper.selectProductIdsByUserId(userId); + Set purchasedSet = new HashSet<>(purchasedProductIds); + + // 获取相似用户 + List similarUsers = userSimilarityMapper.selectSimilarUsers(userId, 10); + + // 用于存储已推荐的商品ID,避免重复 + Set recommendedProductIds = new HashSet<>(); + List recommendations = new ArrayList<>(); + + if (similarUsers.isEmpty()) { + // 冷启动:返回热门商品(排除已购买的) + List hotProducts = productMapper.selectHotProducts(limit * 2); // 多取一些,避免过滤后不足 + for (Product product : hotProducts) { + if (recommendations.size() >= limit) { + break; + } + if (!purchasedSet.contains(product.getId()) + && product.getStatus() == 1 + && !recommendedProductIds.contains(product.getId())) { + recommendations.add(product); + recommendedProductIds.add(product.getId()); + } + } + return recommendations; + } + + // 计算推荐分数 + Map productScores = new HashMap<>(); + + for (UserSimilarity similarity : similarUsers) { + Long similarUserId = similarity.getUser1Id().equals(userId) ? + similarity.getUser2Id() : similarity.getUser1Id(); + + List behaviors = userBehaviorMapper.selectByUserId(similarUserId); + for (UserBehavior behavior : behaviors) { + if (!purchasedSet.contains(behavior.getProductId())) { + BigDecimal score = calculateBehaviorScore(behavior) + .multiply(similarity.getSimilarityScore()); + productScores.merge(behavior.getProductId(), score, BigDecimal::add); + } + } + } + + // 按分数排序,获取Top N + List> sorted = productScores.entrySet().stream() + .sorted((e1, e2) -> e2.getValue().compareTo(e1.getValue())) + .limit(limit) + .collect(Collectors.toList()); + + // 添加协同过滤推荐的商品 + for (Map.Entry entry : sorted) { + Product product = productMapper.selectById(entry.getKey()); + if (product != null && product.getStatus() == 1) { + recommendations.add(product); + recommendedProductIds.add(product.getId()); + } + } + + // 如果推荐数量不足,补充热门商品(排除已购买和已推荐的) + if (recommendations.size() < limit) { + int needCount = limit - recommendations.size(); + List hotProducts = productMapper.selectHotProducts(needCount * 2); // 多取一些,避免过滤后不足 + for (Product product : hotProducts) { + if (recommendations.size() >= limit) { + break; + } + if (!purchasedSet.contains(product.getId()) + && product.getStatus() == 1 + && !recommendedProductIds.contains(product.getId())) { + recommendations.add(product); + recommendedProductIds.add(product.getId()); + } + } + } + + return recommendations; + } + + /** + * 更新用户偏好(购买后) + */ + public void updateUserPreference(Long userId, Long productId) { + if (userId == null || productId == null) { + return; // 参数无效,不记录 + } + + try { + // 记录购买行为 + UserBehavior behavior = userBehaviorMapper.selectByUserAndProduct(userId, productId, "PURCHASE"); + if (behavior == null) { + behavior = new UserBehavior(); + behavior.setUserId(userId); + behavior.setProductId(productId); + behavior.setBehaviorType("PURCHASE"); + behavior.setScore(new BigDecimal("3.0")); + int result = userBehaviorMapper.insert(behavior); + if (result <= 0) { + System.err.println("警告: 用户购买行为记录插入失败 - userId: " + userId + ", productId: " + productId); + } + } + + // 重新计算相似度 + calculateUserSimilarity(userId); + } catch (Exception e) { + // 记录行为失败不应该影响主流程,只打印错误日志 + System.err.println("记录用户购买行为失败: " + e.getMessage()); + e.printStackTrace(); + } + } + + /** + * 记录浏览行为 + */ + public void recordView(Long userId, Long productId) { + if (userId == null || productId == null) { + return; // 参数无效,不记录 + } + + try { + UserBehavior behavior = userBehaviorMapper.selectByUserAndProduct(userId, productId, "VIEW"); + if (behavior == null) { + behavior = new UserBehavior(); + behavior.setUserId(userId); + behavior.setProductId(productId); + behavior.setBehaviorType("VIEW"); + behavior.setScore(new BigDecimal("1.0")); + int result = userBehaviorMapper.insert(behavior); + if (result <= 0) { + System.err.println("警告: 用户行为记录插入失败 - userId: " + userId + ", productId: " + productId); + } + } + } catch (Exception e) { + // 记录行为失败不应该影响主流程,只打印错误日志 + System.err.println("记录用户浏览行为失败: " + e.getMessage()); + e.printStackTrace(); + } + } + + /** + * 记录评分行为 + */ + public void recordRating(Long userId, Long productId, BigDecimal rating) { + if (userId == null || productId == null || rating == null) { + return; // 参数无效,不记录 + } + + try { + // 查找是否已有该商品的评分记录 + UserBehavior behavior = userBehaviorMapper.selectByUserAndProduct(userId, productId, "RATING"); + if (behavior == null) { + // 新建评分记录 + behavior = new UserBehavior(); + behavior.setUserId(userId); + behavior.setProductId(productId); + behavior.setBehaviorType("RATING"); + behavior.setScore(rating); + int result = userBehaviorMapper.insert(behavior); + if (result <= 0) { + System.err.println("警告: 用户评分行为记录插入失败 - userId: " + userId + ", productId: " + productId); + } + } else { + // 更新已有评分记录 + behavior.setScore(rating); + int result = userBehaviorMapper.update(behavior); + if (result <= 0) { + System.err.println("警告: 用户评分行为记录更新失败 - userId: " + userId + ", productId: " + productId); + } + } + + // 重新计算相似度 + calculateUserSimilarity(userId); + } catch (Exception e) { + // 记录行为失败不应该影响主流程,只打印错误日志 + System.err.println("记录用户评分行为失败: " + e.getMessage()); + e.printStackTrace(); + } + } +} diff --git a/src/main/java/com/nlshop/service/merchant/AnnouncementService.java b/src/main/java/com/nlshop/service/merchant/AnnouncementService.java new file mode 100644 index 0000000..91f64a6 --- /dev/null +++ b/src/main/java/com/nlshop/service/merchant/AnnouncementService.java @@ -0,0 +1,42 @@ +package com.nlshop.service.merchant; + +import com.nlshop.entity.Announcement; +import com.nlshop.mapper.AnnouncementMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class AnnouncementService { + + @Autowired + private AnnouncementMapper announcementMapper; + + public Announcement createAnnouncement(Long merchantId, Announcement announcement) { + announcement.setMerchantId(merchantId); + announcement.setStatus(1); // 默认发布 + announcementMapper.insert(announcement); + return announcement; + } + + public List getAnnouncements(Long merchantId) { + return announcementMapper.selectByMerchantId(merchantId); + } + + public Announcement getAnnouncement(Long id) { + return announcementMapper.selectById(id); + } + + public void updateAnnouncement(Announcement announcement) { + announcementMapper.update(announcement); + } + + public void deleteAnnouncement(Long id) { + announcementMapper.delete(id); + } + + public List getPublishedAnnouncements() { + return announcementMapper.selectPublished(); + } +} diff --git a/src/main/java/com/nlshop/service/merchant/InventoryService.java b/src/main/java/com/nlshop/service/merchant/InventoryService.java new file mode 100644 index 0000000..3edac76 --- /dev/null +++ b/src/main/java/com/nlshop/service/merchant/InventoryService.java @@ -0,0 +1,109 @@ +package com.nlshop.service.merchant; + +import com.nlshop.entity.Inventory; +import com.nlshop.entity.InventoryLog; +import com.nlshop.exception.BusinessException; +import com.nlshop.mapper.InventoryLogMapper; +import com.nlshop.mapper.InventoryMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +@Service +public class InventoryService { + + @Autowired + private InventoryMapper inventoryMapper; + + @Autowired + private InventoryLogMapper inventoryLogMapper; + + public List getInventory(Long merchantId) { + return inventoryMapper.selectByMerchantId(merchantId); + } + + public Inventory getInventoryById(Long inventoryId) { + return inventoryMapper.selectById(inventoryId); + } + + @Transactional + public Inventory addInventory(Long merchantId, Inventory inventory) { + // 检查是否已存在 + Inventory existing = inventoryMapper.selectByMerchantAndMaterial( + merchantId, inventory.getMaterialName()); + + if (existing != null) { + // 更新数量 + existing.setQuantity(existing.getQuantity().add(inventory.getQuantity())); + existing.setLastInTime(LocalDateTime.now()); + inventoryMapper.update(existing); + return existing; + } else { + // 新增 + inventory.setMerchantId(merchantId); + inventory.setLastInTime(LocalDateTime.now()); + inventoryMapper.insert(inventory); + return inventory; + } + } + + @Transactional + public void stockIn(Long inventoryId, BigDecimal quantity, String operator) { + Inventory inventory = inventoryMapper.selectById(inventoryId); + if (inventory == null) { + throw new BusinessException(404, "库存记录不存在"); + } + + inventory.setQuantity(inventory.getQuantity().add(quantity)); + inventory.setLastInTime(LocalDateTime.now()); + inventoryMapper.update(inventory); + + // 记录日志 + InventoryLog log = new InventoryLog(); + log.setInventoryId(inventoryId); + log.setOperationType("IN"); + log.setQuantity(quantity); + log.setOperator(operator); + inventoryLogMapper.insert(log); + } + + @Transactional + public void stockOut(Long inventoryId, BigDecimal quantity, String operator) { + Inventory inventory = inventoryMapper.selectById(inventoryId); + if (inventory == null) { + throw new BusinessException(404, "库存记录不存在"); + } + + if (inventory.getQuantity().compareTo(quantity) < 0) { + throw new BusinessException(400, "库存不足"); + } + + inventory.setQuantity(inventory.getQuantity().subtract(quantity)); + inventory.setLastOutTime(LocalDateTime.now()); + inventoryMapper.update(inventory); + + // 记录日志 + InventoryLog log = new InventoryLog(); + log.setInventoryId(inventoryId); + log.setOperationType("OUT"); + log.setQuantity(quantity); + log.setOperator(operator); + inventoryLogMapper.insert(log); + } + + public List getLowStockAlerts(Long merchantId) { + return inventoryMapper.selectLowStock(merchantId); + } + + public void updateInventory(Inventory inventory) { + inventoryMapper.update(inventory); + } + + public void deleteInventory(Long inventoryId) { + inventoryMapper.delete(inventoryId); + } +} diff --git a/src/main/java/com/nlshop/service/merchant/MerchantService.java b/src/main/java/com/nlshop/service/merchant/MerchantService.java new file mode 100644 index 0000000..bc1a263 --- /dev/null +++ b/src/main/java/com/nlshop/service/merchant/MerchantService.java @@ -0,0 +1,47 @@ +package com.nlshop.service.merchant; + +import com.nlshop.dto.request.MerchantRegisterRequest; +import com.nlshop.entity.Merchant; +import com.nlshop.exception.BusinessException; +import com.nlshop.mapper.MerchantMapper; +import com.nlshop.util.PasswordUtil; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class MerchantService { + + @Autowired + private MerchantMapper merchantMapper; + + public Merchant register(MerchantRegisterRequest request) { + if (merchantMapper.selectByUsername(request.getUsername()) != null) { + throw new BusinessException(400, "用户名已存在"); + } + + Merchant merchant = new Merchant(); + BeanUtils.copyProperties(request, merchant); + merchant.setPassword(PasswordUtil.encrypt(request.getPassword())); + merchantMapper.insert(merchant); + return merchant; + } + + public Merchant login(String username, String password) { + Merchant merchant = merchantMapper.selectByUsername(username); + if (merchant == null || !PasswordUtil.verify(password, merchant.getPassword())) { + throw new BusinessException(401, "用户名或密码错误"); + } + return merchant; + } + + public Merchant getProfile(Long merchantId) { + return merchantMapper.selectById(merchantId); + } + + public Merchant updateProfile(Long merchantId, Merchant merchant) { + merchant.setId(merchantId); + merchantMapper.update(merchant); + return merchantMapper.selectById(merchantId); + } +} diff --git a/src/main/java/com/nlshop/service/merchant/MessageService.java b/src/main/java/com/nlshop/service/merchant/MessageService.java new file mode 100644 index 0000000..455ce84 --- /dev/null +++ b/src/main/java/com/nlshop/service/merchant/MessageService.java @@ -0,0 +1,34 @@ +package com.nlshop.service.merchant; + +import com.nlshop.entity.Message; +import com.nlshop.mapper.MessageMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.List; + +@Service("merchantMessageService") +public class MessageService { + + @Autowired + private MessageMapper messageMapper; + + public List getMessages(Long merchantId) { + return messageMapper.selectByMerchantId(merchantId); + } + + public Message getMessage(Long id) { + return messageMapper.selectById(id); + } + + public void replyMessage(Long messageId, String reply) { + Message message = messageMapper.selectById(messageId); + if (message != null) { + message.setReply(reply); + message.setStatus(1); + message.setReplyTime(LocalDateTime.now()); + messageMapper.update(message); + } + } +} diff --git a/src/main/java/com/nlshop/service/merchant/OrderManageService.java b/src/main/java/com/nlshop/service/merchant/OrderManageService.java new file mode 100644 index 0000000..3de2486 --- /dev/null +++ b/src/main/java/com/nlshop/service/merchant/OrderManageService.java @@ -0,0 +1,126 @@ +package com.nlshop.service.merchant; + +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import com.nlshop.dto.response.OrderDetailResponse; +import com.nlshop.dto.response.PageResponse; +import com.nlshop.entity.Order; +import com.nlshop.entity.OrderItem; +import com.nlshop.entity.OrderTrack; +import com.nlshop.exception.BusinessException; +import com.nlshop.entity.Product; +import com.nlshop.mapper.OrderItemMapper; +import com.nlshop.mapper.OrderMapper; +import com.nlshop.mapper.OrderTrackMapper; +import com.nlshop.mapper.ProductMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service +public class OrderManageService { + + @Autowired + private OrderMapper orderMapper; + + @Autowired + private OrderTrackMapper orderTrackMapper; + + @Autowired + private OrderItemMapper orderItemMapper; + + @Autowired + private ProductMapper productMapper; + + public PageResponse getOrders(Long merchantId, String status, Integer pageNum, Integer pageSize) { + PageHelper.startPage(pageNum, pageSize); + List orders = orderMapper.selectByMerchantId(merchantId, status); + PageInfo pageInfo = new PageInfo<>(orders); + return PageResponse.of(orders, pageInfo.getTotal(), pageNum, pageSize); + } + + public OrderDetailResponse getOrderDetail(Long orderId, Long merchantId) { + Order order = orderMapper.selectById(orderId); + if (order == null || !order.getMerchantId().equals(merchantId)) { + throw new BusinessException(404, "订单不存在"); + } + List items = orderItemMapper.selectByOrderId(orderId); + List tracks = orderTrackMapper.selectByOrderId(orderId); + + // 获取商品图片信息 + Map productImages = new HashMap<>(); + for (OrderItem item : items) { + Product product = productMapper.selectById(item.getProductId()); + if (product != null) { + productImages.put(item.getProductId(), product.getImage()); + } + } + + return OrderDetailResponse.of(order, items, tracks, productImages); + } + + @Transactional + public void acceptOrder(Long orderId, Long merchantId) { + Order order = orderMapper.selectById(orderId); + if (order == null || !order.getMerchantId().equals(merchantId)) { + throw new BusinessException(404, "订单不存在"); + } + if (!"MAKING".equals(order.getOrderStatus())) { + throw new BusinessException(400, "订单状态不正确"); + } + + // 订单状态已经是MAKING,这里可以添加其他业务逻辑 + addOrderTrack(orderId, "MAKING", "商家已接单,正在制作中"); + } + + @Transactional + public void rejectOrder(Long orderId, Long merchantId) { + Order order = orderMapper.selectById(orderId); + if (order == null || !order.getMerchantId().equals(merchantId)) { + throw new BusinessException(404, "订单不存在"); + } + if (!"MAKING".equals(order.getOrderStatus())) { + throw new BusinessException(400, "订单状态不正确"); + } + + orderMapper.updateStatus(orderId, "CANCELLED"); + addOrderTrack(orderId, "CANCELLED", "商家拒单"); + } + + @Transactional + public void updateOrderStatus(Long orderId, Long merchantId, String status) { + Order order = orderMapper.selectById(orderId); + if (order == null || !order.getMerchantId().equals(merchantId)) { + throw new BusinessException(404, "订单不存在"); + } + + String currentStatus = order.getOrderStatus(); + String description = ""; + + if ("READY".equals(status) && "MAKING".equals(currentStatus)) { + description = "制作完成,等待取餐"; + } else if ("COMPLETED".equals(status) && "READY".equals(currentStatus)) { + description = "订单已完成"; + order.setCompleteTime(LocalDateTime.now()); + orderMapper.update(order); + } else { + throw new BusinessException(400, "订单状态转换不正确"); + } + + orderMapper.updateStatus(orderId, status); + addOrderTrack(orderId, status, description); + } + + private void addOrderTrack(Long orderId, String status, String description) { + OrderTrack track = new OrderTrack(); + track.setOrderId(orderId); + track.setStatus(status); + track.setDescription(description); + orderTrackMapper.insert(track); + } +} diff --git a/src/main/java/com/nlshop/service/merchant/ProductManageService.java b/src/main/java/com/nlshop/service/merchant/ProductManageService.java new file mode 100644 index 0000000..8bb1652 --- /dev/null +++ b/src/main/java/com/nlshop/service/merchant/ProductManageService.java @@ -0,0 +1,161 @@ +package com.nlshop.service.merchant; + +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import com.nlshop.dto.response.PageResponse; +import com.nlshop.entity.Product; +import com.nlshop.entity.ProductCustom; +import com.nlshop.entity.ProductSpec; +import com.nlshop.entity.ProductTopping; +import com.nlshop.mapper.ProductCustomMapper; +import com.nlshop.mapper.ProductMapper; +import com.nlshop.mapper.ProductSpecMapper; +import com.nlshop.mapper.ProductToppingMapper; +import com.nlshop.exception.BusinessException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service +public class ProductManageService { + + @Autowired + private ProductMapper productMapper; + + @Autowired + private ProductSpecMapper productSpecMapper; + + @Autowired + private ProductToppingMapper productToppingMapper; + + @Autowired + private ProductCustomMapper productCustomMapper; + + public PageResponse getProducts(Long merchantId, Integer pageNum, Integer pageSize) { + PageHelper.startPage(pageNum, pageSize); + List products = productMapper.selectByMerchantId(merchantId, null); + PageInfo pageInfo = new PageInfo<>(products); + return PageResponse.of(products, pageInfo.getTotal(), pageNum, pageSize); + } + + /** + * 获取商品完整信息(包括规格、配料、定制选项) + */ + public Map getProduct(Long productId, Long merchantId) { + Product product = productMapper.selectById(productId); + if (product == null) { + throw new BusinessException(404, "商品不存在"); + } + + // 验证商品归属 + validateProductOwnership(productId, merchantId); + + Map result = new HashMap<>(); + result.put("product", product); + result.put("specs", productSpecMapper.selectByProductId(productId)); + result.put("toppings", productToppingMapper.selectByProductId(productId)); + result.put("customs", productCustomMapper.selectByProductId(productId)); + + return result; + } + + /** + * 验证商品是否属于指定商家 + */ + public void validateProductOwnership(Long productId, Long merchantId) { + Product product = productMapper.selectById(productId); + if (product == null) { + throw new BusinessException(404, "商品不存在"); + } + if (!product.getMerchantId().equals(merchantId)) { + throw new BusinessException(403, "无权操作此商品"); + } + } + + @Transactional + public Product createProduct(Long merchantId, Product product) { + product.setMerchantId(merchantId); + product.setStatus(1); // 默认上架 + productMapper.insert(product); + return product; + } + + public Product updateProduct(Long productId, Long merchantId, Product product) { + // 验证商品归属 + validateProductOwnership(productId, merchantId); + + product.setId(productId); + productMapper.update(product); + return productMapper.selectById(productId); + } + + public void offlineProduct(Long productId, Long merchantId) { + // 验证商品归属 + validateProductOwnership(productId, merchantId); + productMapper.updateStatus(productId, 0); + } + + public void onlineProduct(Long productId, Long merchantId) { + // 验证商品归属 + validateProductOwnership(productId, merchantId); + productMapper.updateStatus(productId, 1); + } + + public List getSpecs(Long productId) { + return productSpecMapper.selectByProductId(productId); + } + + public ProductSpec addSpec(Long productId, ProductSpec spec) { + spec.setProductId(productId); + productSpecMapper.insert(spec); + return spec; + } + + public void updateSpec(ProductSpec spec) { + productSpecMapper.update(spec); + } + + public void deleteSpec(Long specId) { + productSpecMapper.delete(specId); + } + + public List getToppings(Long productId) { + return productToppingMapper.selectByProductId(productId); + } + + public ProductTopping addTopping(Long productId, ProductTopping topping) { + topping.setProductId(productId); + productToppingMapper.insert(topping); + return topping; + } + + public void updateTopping(ProductTopping topping) { + productToppingMapper.update(topping); + } + + public void deleteTopping(Long toppingId) { + productToppingMapper.delete(toppingId); + } + + public List getCustoms(Long productId) { + return productCustomMapper.selectByProductId(productId); + } + + public ProductCustom addCustom(Long productId, ProductCustom custom) { + custom.setProductId(productId); + productCustomMapper.insert(custom); + return custom; + } + + public void updateCustom(ProductCustom custom) { + productCustomMapper.update(custom); + } + + public void deleteCustom(Long customId) { + productCustomMapper.delete(customId); + } +} diff --git a/src/main/java/com/nlshop/service/merchant/ReviewService.java b/src/main/java/com/nlshop/service/merchant/ReviewService.java new file mode 100644 index 0000000..c23acc5 --- /dev/null +++ b/src/main/java/com/nlshop/service/merchant/ReviewService.java @@ -0,0 +1,57 @@ +package com.nlshop.service.merchant; + +import com.nlshop.entity.Product; +import com.nlshop.entity.Review; +import com.nlshop.exception.BusinessException; +import com.nlshop.mapper.ProductMapper; +import com.nlshop.mapper.ReviewMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.List; + +@Service +public class ReviewService { + + @Autowired + private ReviewMapper reviewMapper; + + @Autowired + private ProductMapper productMapper; + + public List getReviews(Long merchantId) { + return reviewMapper.selectByMerchantId(merchantId); + } + + public Review getReview(Long id) { + return reviewMapper.selectById(id); + } + + public void replyReview(Long merchantId, Long reviewId, String reply) { + Review review = reviewMapper.selectById(reviewId); + if (review == null) { + throw new BusinessException(404, "评价不存在"); + } + + // 验证评价对应的商品是否属于当前商家 + Product product = productMapper.selectById(review.getProductId()); + if (product == null) { + throw new BusinessException(404, "商品不存在"); + } + if (!product.getMerchantId().equals(merchantId)) { + throw new BusinessException(403, "无权回复该评价"); + } + + review.setReply(reply); + reviewMapper.update(review); + } + + public void handleComplaint(Long merchantId, Long reviewId, String reply) { + replyReview(merchantId, reviewId, reply); + } + + public List getProductReviews(Long productId) { + return reviewMapper.selectByProductId(productId); + } +} diff --git a/src/main/java/com/nlshop/service/merchant/StatisticsService.java b/src/main/java/com/nlshop/service/merchant/StatisticsService.java new file mode 100644 index 0000000..56a8fb8 --- /dev/null +++ b/src/main/java/com/nlshop/service/merchant/StatisticsService.java @@ -0,0 +1,136 @@ +package com.nlshop.service.merchant; + +import com.nlshop.entity.Order; +import com.nlshop.entity.OrderItem; +import com.nlshop.entity.Product; +import com.nlshop.mapper.OrderItemMapper; +import com.nlshop.mapper.OrderMapper; +import com.nlshop.mapper.ProductMapper; +import com.nlshop.mapper.UserBehaviorMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Service +public class StatisticsService { + + @Autowired + private OrderMapper orderMapper; + + @Autowired + private OrderItemMapper orderItemMapper; + + @Autowired + private ProductMapper productMapper; + + @Autowired + private UserBehaviorMapper userBehaviorMapper; + + public Map getSalesStatistics(Long merchantId, String period) { + LocalDateTime startTime = getStartTime(period); + List orders = orderMapper.selectByMerchantId(merchantId, null); + + // 过滤时间范围和已完成订单 + orders = orders.stream() + .filter(order -> order.getOrderStatus() != null && + order.getOrderStatus().equals("COMPLETED") && + order.getCompleteTime() != null && + order.getCompleteTime().isAfter(startTime)) + .collect(Collectors.toList()); + + BigDecimal totalAmount = orders.stream() + .map(Order::getTotalAmount) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + BigDecimal avgOrderAmount = orders.size() > 0 ? + totalAmount.divide(new BigDecimal(orders.size()), 2, RoundingMode.HALF_UP) : + BigDecimal.ZERO; + + Map result = new HashMap<>(); + result.put("totalOrders", orders.size()); + result.put("totalAmount", totalAmount); + result.put("avgOrderAmount", avgOrderAmount); + + return result; + } + + public Map getRevenueStatistics(Long merchantId, String period) { + Map salesStats = getSalesStatistics(merchantId, period); + BigDecimal totalRevenue = (BigDecimal) salesStats.get("totalAmount"); + + // 简化处理:假设成本为收入的70%(实际应该从库存等计算) + BigDecimal totalCost = totalRevenue.multiply(new BigDecimal("0.7")); + BigDecimal netProfit = totalRevenue.subtract(totalCost); + + Map result = new HashMap<>(); + result.put("totalRevenue", totalRevenue); + result.put("totalCost", totalCost); + result.put("netProfit", netProfit); + + return result; + } + + public List> getHotProducts(Long merchantId, int limit) { + List orders = orderMapper.selectByMerchantId(merchantId, "COMPLETED"); + + // 统计商品销量和销售额 + Map productSales = new HashMap<>(); + Map productAmounts = new HashMap<>(); + + for (Order order : orders) { + List items = orderItemMapper.selectByOrderId(order.getId()); + for (OrderItem item : items) { + productSales.merge(item.getProductId(), item.getQuantity(), Integer::sum); + BigDecimal itemAmount = item.getSubtotal(); + productAmounts.merge(item.getProductId(), itemAmount, BigDecimal::add); + } + } + + // 按销量排序并获取商品信息 + return productSales.entrySet().stream() + .sorted((e1, e2) -> e2.getValue().compareTo(e1.getValue())) + .limit(limit) + .map(entry -> { + Long productId = entry.getKey(); + Product product = productMapper.selectById(productId); + + Map map = new HashMap<>(); + map.put("productId", productId); + map.put("productName", product != null ? product.getName() : "未知商品"); + map.put("salesCount", entry.getValue()); + map.put("salesAmount", productAmounts.getOrDefault(productId, BigDecimal.ZERO)); + return map; + }) + .collect(Collectors.toList()); + } + + public List> getActiveUsers(Long merchantId, int days) { + return userBehaviorMapper.selectActiveUsers(merchantId, days); + } + + public List> getStickyUsers(Long merchantId, int days, int minOrders) { + return orderMapper.selectStickyUsers(merchantId, days, minOrders); + } + + private LocalDateTime getStartTime(String period) { + LocalDateTime now = LocalDateTime.now(); + switch (period) { + case "day": + return now.minusDays(1); + case "week": + return now.minusWeeks(1); + case "month": + return now.minusMonths(1); + default: + return now.minusDays(1); + } + } +} diff --git a/src/main/java/com/nlshop/service/merchant/UserManageService.java b/src/main/java/com/nlshop/service/merchant/UserManageService.java new file mode 100644 index 0000000..c26dd8d --- /dev/null +++ b/src/main/java/com/nlshop/service/merchant/UserManageService.java @@ -0,0 +1,25 @@ +package com.nlshop.service.merchant; + +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import com.nlshop.dto.response.PageResponse; +import com.nlshop.entity.User; +import com.nlshop.mapper.UserMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class UserManageService { + + @Autowired + private UserMapper userMapper; + + public PageResponse getUsers(Integer pageNum, Integer pageSize) { + PageHelper.startPage(pageNum, pageSize); + List users = userMapper.selectAll(); + PageInfo pageInfo = new PageInfo<>(users); + return PageResponse.of(users, pageInfo.getTotal(), pageNum, pageSize); + } +} diff --git a/src/main/java/com/nlshop/service/user/CartService.java b/src/main/java/com/nlshop/service/user/CartService.java new file mode 100644 index 0000000..4dc1245 --- /dev/null +++ b/src/main/java/com/nlshop/service/user/CartService.java @@ -0,0 +1,127 @@ +package com.nlshop.service.user; + +import com.nlshop.dto.request.CartAddRequest; +import com.nlshop.entity.CartItem; +import com.nlshop.mapper.CartMapper; +import com.nlshop.service.algorithm.CollaborativeFilteringService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class CartService { + + @Autowired + private CartMapper cartMapper; + + @Autowired + private CollaborativeFilteringService cfService; + + public CartItem addToCart(Long userId, CartAddRequest request) { + // 检查是否已存在相同的商品配置 + CartItem existing = cartMapper.selectByUserAndProduct( + userId, request.getProductId(), request.getSpecId(), + request.getCustomSweetness(), request.getCustomIce(), request.getToppings() + ); + + if (existing != null) { + // 更新数量 + existing.setQuantity(existing.getQuantity() + request.getQuantity()); + cartMapper.updateQuantity(existing.getId(), existing.getQuantity()); + return cartMapper.selectById(existing.getId()); + } else { + // 新增 + CartItem item = new CartItem(); + item.setUserId(userId); + item.setProductId(request.getProductId()); + item.setSpecId(request.getSpecId()); + item.setQuantity(request.getQuantity()); + item.setCustomSweetness(request.getCustomSweetness()); + item.setCustomIce(request.getCustomIce()); + item.setToppings(request.getToppings()); + cartMapper.insert(item); + + // 记录用户行为(加入购物车表示用户对商品有兴趣) + // 如果用户已经浏览过该商品,recordView不会重复记录 + // 使用try-catch确保行为记录失败不影响购物车添加 + try { + cfService.recordView(userId, request.getProductId()); + } catch (Exception e) { + // 记录行为失败不应该影响购物车添加,只打印错误日志 + System.err.println("记录购物车行为失败: " + e.getMessage()); + } + + return cartMapper.selectById(item.getId()); + } + } + + public List getCart(Long userId) { + return cartMapper.selectByUserId(userId); + } + + public void updateQuantity(Long userId, Long cartId, Integer quantity) { + // 验证购物车项是否属于当前用户 + CartItem item = cartMapper.selectById(cartId); + if (item == null) { + throw new RuntimeException("购物车项不存在"); + } + if (!item.getUserId().equals(userId)) { + throw new RuntimeException("无权修改该商品"); + } + cartMapper.updateQuantity(cartId, quantity); + } + + public void deleteCartItem(Long userId, Long cartId) { + // 验证购物车项是否属于当前用户 + CartItem item = cartMapper.selectById(cartId); + if (item == null) { + throw new RuntimeException("购物车项不存在"); + } + if (!item.getUserId().equals(userId)) { + throw new RuntimeException("无权删除该商品"); + } + cartMapper.delete(cartId); + } + + public void deleteBatch(Long userId, List cartIds) { + if (cartIds == null || cartIds.isEmpty()) { + return; + } + // 验证所有商品都属于当前用户 + List items = cartMapper.selectByUserId(userId); + for (Long cartId : cartIds) { + boolean belongsToUser = items.stream().anyMatch(item -> item.getId().equals(cartId)); + if (!belongsToUser) { + throw new RuntimeException("无权删除该商品"); + } + } + // 执行批量删除 + cartMapper.deleteBatch(cartIds, userId); + } + + public void clearCart(Long userId) { + cartMapper.deleteByUserId(userId); + } + + /** + * 从cookies同步购物车数据(登录后同步) + * 相同商品会自动合并数量 + */ + public void syncCartFromCookies(Long userId, List items) { + if (items == null || items.isEmpty()) { + return; + } + + // 遍历每个商品,调用addToCart会自动合并相同商品 + for (CartAddRequest request : items) { + try { + addToCart(userId, request); + } catch (Exception e) { + // 单个商品同步失败不影响其他,只记录日志 + System.err.println("同步购物车商品失败: userId=" + userId + + ", productId=" + request.getProductId() + ", error=" + e.getMessage()); + } + } + } +} diff --git a/src/main/java/com/nlshop/service/user/MessageService.java b/src/main/java/com/nlshop/service/user/MessageService.java new file mode 100644 index 0000000..a019621 --- /dev/null +++ b/src/main/java/com/nlshop/service/user/MessageService.java @@ -0,0 +1,31 @@ +package com.nlshop.service.user; + +import com.nlshop.entity.Message; +import com.nlshop.mapper.MessageMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.List; + +@Service("userMessageService") +public class MessageService { + + @Autowired + private MessageMapper messageMapper; + + public Message createMessage(Long userId, Long merchantId, Long orderId, String content) { + Message message = new Message(); + message.setUserId(userId); + message.setMerchantId(merchantId); + message.setOrderId(orderId); + message.setContent(content); + message.setStatus(0); + messageMapper.insert(message); + return message; + } + + public List getMessages(Long userId) { + return messageMapper.selectByUserId(userId); + } +} diff --git a/src/main/java/com/nlshop/service/user/OrderService.java b/src/main/java/com/nlshop/service/user/OrderService.java new file mode 100644 index 0000000..41bb6e2 --- /dev/null +++ b/src/main/java/com/nlshop/service/user/OrderService.java @@ -0,0 +1,239 @@ +package com.nlshop.service.user; + +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import com.nlshop.dto.request.OrderCreateRequest; +import com.nlshop.dto.request.PaymentRequest; +import com.nlshop.dto.response.OrderDetailResponse; +import com.nlshop.entity.*; +import com.nlshop.exception.BusinessException; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.TypeReference; +import com.nlshop.mapper.*; +import com.nlshop.service.algorithm.CollaborativeFilteringService; +import com.nlshop.util.OrderNoUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +@Service +public class OrderService { + + @Autowired + private OrderMapper orderMapper; + + @Autowired + private OrderItemMapper orderItemMapper; + + @Autowired + private OrderTrackMapper orderTrackMapper; + + @Autowired + private CartMapper cartMapper; + + @Autowired + private ProductMapper productMapper; + + @Autowired + private ProductSpecMapper productSpecMapper; + + @Autowired + private ProductToppingMapper productToppingMapper; + + @Autowired + private CollaborativeFilteringService cfService; + + @Transactional + public Order createOrder(Long userId, OrderCreateRequest request) { + // 获取购物车商品 + List cartItems = cartMapper.selectByUserId(userId); + if (cartItems.isEmpty()) { + throw new BusinessException(400, "购物车为空"); + } + + // 计算总金额 + BigDecimal totalAmount = BigDecimal.ZERO; + Long merchantId = null; + + for (CartItem item : cartItems) { + Product product = productMapper.selectById(item.getProductId()); + if (product == null || product.getStatus() == 0) { + throw new BusinessException(400, "商品不存在或已下架: " + product.getName()); + } + + if (merchantId == null) { + merchantId = product.getMerchantId(); + } else if (!merchantId.equals(product.getMerchantId())) { + throw new BusinessException(400, "购物车中包含不同商家的商品"); + } + + BigDecimal price = product.getBasePrice(); + if (item.getSpecId() != null) { + ProductSpec spec = productSpecMapper.selectById(item.getSpecId()); + if (spec != null) { + price = price.add(spec.getPriceAdjust()); + } + } + + // 计算配料价格 + if (item.getToppings() != null && !item.getToppings().isEmpty()) { + try { + List toppingIds = JSON.parseObject(item.getToppings(), new TypeReference>(){}); + if (toppingIds != null && !toppingIds.isEmpty()) { + for (Long toppingId : toppingIds) { + ProductTopping topping = productToppingMapper.selectById(toppingId); + if (topping != null && topping.getPrice() != null) { + price = price.add(topping.getPrice()); + } + } + } + } catch (Exception e) { + // 如果解析失败,忽略配料价格 + } + } + + totalAmount = totalAmount.add(price.multiply(new BigDecimal(item.getQuantity()))); + } + + // 创建订单 + Order order = new Order(); + order.setOrderNo(OrderNoUtil.generate()); + order.setUserId(userId); + order.setMerchantId(merchantId); + order.setTotalAmount(totalAmount); + order.setOrderStatus("PENDING_PAY"); + order.setPaymentStatus(0); + order.setAddress(request.getAddress()); + order.setContactPhone(request.getContactPhone()); + orderMapper.insert(order); + + // 创建订单明细 + for (CartItem item : cartItems) { + Product product = productMapper.selectById(item.getProductId()); + BigDecimal price = product.getBasePrice(); + String specName = null; + + if (item.getSpecId() != null) { + ProductSpec spec = productSpecMapper.selectById(item.getSpecId()); + if (spec != null) { + price = price.add(spec.getPriceAdjust()); + specName = spec.getSpecName(); + } + } + + // 计算配料价格 + if (item.getToppings() != null && !item.getToppings().isEmpty()) { + try { + List toppingIds = JSON.parseObject(item.getToppings(), new TypeReference>(){}); + if (toppingIds != null && !toppingIds.isEmpty()) { + for (Long toppingId : toppingIds) { + ProductTopping topping = productToppingMapper.selectById(toppingId); + if (topping != null && topping.getPrice() != null) { + price = price.add(topping.getPrice()); + } + } + } + } catch (Exception e) { + // 如果解析失败,忽略配料价格 + } + } + + OrderItem orderItem = new OrderItem(); + orderItem.setOrderId(order.getId()); + orderItem.setProductId(item.getProductId()); + orderItem.setProductName(product.getName()); + orderItem.setSpecName(specName); + orderItem.setQuantity(item.getQuantity()); + orderItem.setPrice(price); + orderItem.setCustomSweetness(item.getCustomSweetness()); + orderItem.setCustomIce(item.getCustomIce()); + orderItem.setToppings(item.getToppings()); + orderItem.setSubtotal(price.multiply(new BigDecimal(item.getQuantity()))); + orderItemMapper.insert(orderItem); + } + + // 记录订单跟踪 + addOrderTrack(order.getId(), "PENDING_PAY", "订单已创建,等待支付"); + + // 清空购物车 + cartMapper.deleteByUserId(userId); + + return orderMapper.selectById(order.getId()); + } + + @Transactional + public void payOrder(Long userId, Long orderId, PaymentRequest request) { + Order order = orderMapper.selectById(orderId); + if (order == null) { + throw new BusinessException(404, "订单不存在"); + } + if (!order.getUserId().equals(userId)) { + throw new BusinessException(403, "无权操作该订单"); + } + if (!"PENDING_PAY".equals(order.getOrderStatus())) { + throw new BusinessException(400, "订单状态不正确"); + } + + // 更新支付信息 + orderMapper.updatePayment(orderId, 1, request.getPaymentMethod()); + orderMapper.updateStatus(orderId, "MAKING"); + + // 记录订单跟踪 + addOrderTrack(orderId, "MAKING", "支付成功,商家开始制作"); + + // 更新用户偏好(协同过滤) + List items = orderItemMapper.selectByOrderId(orderId); + for (OrderItem item : items) { + cfService.updateUserPreference(order.getUserId(), item.getProductId()); + } + } + + public List getOrders(Long userId, String status) { + return orderMapper.selectByUserId(userId, status); + } + + public OrderDetailResponse getOrderDetail(Long orderId, Long userId) { + Order order = orderMapper.selectById(orderId); + if (order == null || !order.getUserId().equals(userId)) { + throw new BusinessException(404, "订单不存在"); + } + List items = orderItemMapper.selectByOrderId(orderId); + List tracks = orderTrackMapper.selectByOrderId(orderId); + + // 获取商品图片信息 + Map productImages = new java.util.HashMap<>(); + for (OrderItem item : items) { + Product product = productMapper.selectById(item.getProductId()); + if (product != null) { + productImages.put(item.getProductId(), product.getImage()); + } + } + + return OrderDetailResponse.of(order, items, tracks, productImages); + } + + public List getOrderTrack(Long userId, Long orderId) { + // 验证订单是否属于当前用户 + Order order = orderMapper.selectById(orderId); + if (order == null) { + throw new BusinessException(404, "订单不存在"); + } + if (!order.getUserId().equals(userId)) { + throw new BusinessException(403, "无权查看该订单"); + } + return orderTrackMapper.selectByOrderId(orderId); + } + + private void addOrderTrack(Long orderId, String status, String description) { + OrderTrack track = new OrderTrack(); + track.setOrderId(orderId); + track.setStatus(status); + track.setDescription(description); + orderTrackMapper.insert(track); + } +} diff --git a/src/main/java/com/nlshop/service/user/ProductService.java b/src/main/java/com/nlshop/service/user/ProductService.java new file mode 100644 index 0000000..508c33e --- /dev/null +++ b/src/main/java/com/nlshop/service/user/ProductService.java @@ -0,0 +1,77 @@ +package com.nlshop.service.user; + +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import com.nlshop.dto.response.PageResponse; +import com.nlshop.entity.Product; +import com.nlshop.entity.ProductCustom; +import com.nlshop.entity.ProductSpec; +import com.nlshop.entity.ProductTopping; +import com.nlshop.mapper.ProductCustomMapper; +import com.nlshop.mapper.ProductMapper; +import com.nlshop.mapper.ProductSpecMapper; +import com.nlshop.mapper.ProductToppingMapper; +import com.nlshop.service.algorithm.CollaborativeFilteringService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service +public class ProductService { + + @Autowired + private ProductMapper productMapper; + + @Autowired + private ProductSpecMapper productSpecMapper; + + @Autowired + private ProductToppingMapper productToppingMapper; + + @Autowired + private ProductCustomMapper productCustomMapper; + + @Autowired + private CollaborativeFilteringService cfService; + + public PageResponse getProducts(String category, Integer pageNum, Integer pageSize) { + PageHelper.startPage(pageNum, pageSize); + List products = productMapper.selectAll(category, 1); // 只查询上架商品 + PageInfo pageInfo = new PageInfo<>(products); + return PageResponse.of(products, pageInfo.getTotal(), pageNum, pageSize); + } + + public Map getProductDetail(Long productId, Long userId) { + Product product = productMapper.selectById(productId); + if (product == null || product.getStatus() == 0) { + throw new com.nlshop.exception.BusinessException(404, "商品不存在或已下架"); + } + + // 记录浏览行为 + if (userId != null) { + cfService.recordView(userId, productId); + } + + Map result = new HashMap<>(); + result.put("product", product); + result.put("specs", productSpecMapper.selectByProductId(productId)); + result.put("toppings", productToppingMapper.selectByProductId(productId)); + + // 获取定制选项 + List customs = productCustomMapper.selectByProductId(productId); + Map> customMap = new HashMap<>(); + for (ProductCustom custom : customs) { + customMap.computeIfAbsent(custom.getOptionType(), k -> new java.util.ArrayList<>()).add(custom); + } + result.put("customs", customMap); + + return result; + } + + public List getHotProducts(int limit) { + return productMapper.selectHotProducts(limit); + } +} diff --git a/src/main/java/com/nlshop/service/user/RecommendService.java b/src/main/java/com/nlshop/service/user/RecommendService.java new file mode 100644 index 0000000..fbafd99 --- /dev/null +++ b/src/main/java/com/nlshop/service/user/RecommendService.java @@ -0,0 +1,19 @@ +package com.nlshop.service.user; + +import com.nlshop.entity.Product; +import com.nlshop.service.algorithm.CollaborativeFilteringService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class RecommendService { + + @Autowired + private CollaborativeFilteringService cfService; + + public List getRecommendations(Long userId, int limit) { + return cfService.getRecommendations(userId, limit); + } +} diff --git a/src/main/java/com/nlshop/service/user/UserService.java b/src/main/java/com/nlshop/service/user/UserService.java new file mode 100644 index 0000000..f2b11e5 --- /dev/null +++ b/src/main/java/com/nlshop/service/user/UserService.java @@ -0,0 +1,113 @@ +package com.nlshop.service.user; + +import com.nlshop.dto.request.UserRegisterRequest; +import com.nlshop.entity.User; +import com.nlshop.entity.UserAddress; +import com.nlshop.exception.BusinessException; +import com.nlshop.mapper.UserAddressMapper; +import com.nlshop.mapper.UserMapper; +import com.nlshop.util.PasswordUtil; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +public class UserService { + + @Autowired + private UserMapper userMapper; + + @Autowired + private UserAddressMapper userAddressMapper; + + public User register(UserRegisterRequest request) { + // 检查用户名是否已存在 + if (userMapper.selectByUsername(request.getUsername()) != null) { + throw new BusinessException(400, "用户名已存在"); + } + + User user = new User(); + BeanUtils.copyProperties(request, user); + user.setPassword(PasswordUtil.encrypt(request.getPassword())); + userMapper.insert(user); + return user; + } + + public User login(String username, String password) { + User user = userMapper.selectByUsername(username); + if (user == null || !PasswordUtil.verify(password, user.getPassword())) { + throw new BusinessException(401, "用户名或密码错误"); + } + return user; + } + + public User getProfile(Long userId) { + return userMapper.selectById(userId); + } + + public User updateProfile(Long userId, User user) { + user.setId(userId); + userMapper.update(user); + return userMapper.selectById(userId); + } + + public List getAddresses(Long userId) { + return userAddressMapper.selectByUserId(userId); + } + + @Transactional + public UserAddress addAddress(Long userId, UserAddress address) { + address.setUserId(userId); + // 如果设置为默认地址,取消其他默认地址 + if (address.getIsDefault() != null && address.getIsDefault() == 1) { + UserAddress defaultAddr = userAddressMapper.selectDefaultByUserId(userId); + if (defaultAddr != null) { + defaultAddr.setIsDefault(0); + userAddressMapper.update(defaultAddr); + } + } + userAddressMapper.insert(address); + return address; + } + + public UserAddress updateAddress(Long userId, Long addressId, UserAddress address) { + // 验证地址是否属于当前用户 + UserAddress existing = userAddressMapper.selectById(addressId); + if (existing == null) { + throw new BusinessException(404, "地址不存在"); + } + if (!existing.getUserId().equals(userId)) { + throw new BusinessException(403, "无权修改该地址"); + } + + address.setId(addressId); + address.setUserId(userId); + + // 如果设置为默认地址,取消其他默认地址 + if (address.getIsDefault() != null && address.getIsDefault() == 1) { + UserAddress defaultAddr = userAddressMapper.selectDefaultByUserId(userId); + if (defaultAddr != null && !defaultAddr.getId().equals(addressId)) { + defaultAddr.setIsDefault(0); + userAddressMapper.update(defaultAddr); + } + } + + userAddressMapper.update(address); + return userAddressMapper.selectById(addressId); + } + + public void deleteAddress(Long userId, Long addressId) { + // 验证地址是否属于当前用户 + UserAddress existing = userAddressMapper.selectById(addressId); + if (existing == null) { + throw new BusinessException(404, "地址不存在"); + } + if (!existing.getUserId().equals(userId)) { + throw new BusinessException(403, "无权删除该地址"); + } + userAddressMapper.delete(addressId); + } +} diff --git a/src/main/java/com/nlshop/util/FileUploadUtil.java b/src/main/java/com/nlshop/util/FileUploadUtil.java new file mode 100644 index 0000000..b27ccec --- /dev/null +++ b/src/main/java/com/nlshop/util/FileUploadUtil.java @@ -0,0 +1,70 @@ +package com.nlshop.util; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.UUID; + +@Component +public class FileUploadUtil { + + @Value("${file.upload-path}") + private String uploadPath; + + public String uploadAvatar(MultipartFile file) throws IOException { + return uploadFile(file, "avatar"); + } + + public String uploadProductImage(MultipartFile file) throws IOException { + return uploadFile(file, "product"); + } + + private String uploadFile(MultipartFile file, String subDir) throws IOException { + if (file == null || file.isEmpty()) { + return null; + } + + // 创建目录 + String dir = uploadPath + File.separator + subDir; + Path path = Paths.get(dir); + if (!Files.exists(path)) { + Files.createDirectories(path); + } + + // 生成文件名 + String originalFilename = file.getOriginalFilename(); + String extension = ""; + if (originalFilename != null && originalFilename.contains(".")) { + extension = originalFilename.substring(originalFilename.lastIndexOf(".")); + } + String filename = UUID.randomUUID().toString() + extension; + + // 保存文件 + Path filePath = Paths.get(dir, filename); + Files.write(filePath, file.getBytes()); + + // 返回相对路径 + return "/upload/" + subDir + "/" + filename; + } + + public boolean deleteFile(String filePath) { + if (filePath == null || filePath.isEmpty()) { + return false; + } + + try { + // 移除开头的 /upload/ + String relativePath = filePath.replace("/upload/", ""); + Path path = Paths.get(uploadPath, relativePath); + return Files.deleteIfExists(path); + } catch (IOException e) { + return false; + } + } +} diff --git a/src/main/java/com/nlshop/util/OrderNoUtil.java b/src/main/java/com/nlshop/util/OrderNoUtil.java new file mode 100644 index 0000000..7b0d379 --- /dev/null +++ b/src/main/java/com/nlshop/util/OrderNoUtil.java @@ -0,0 +1,16 @@ +package com.nlshop.util; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.concurrent.atomic.AtomicInteger; + +public class OrderNoUtil { + private static final AtomicInteger counter = new AtomicInteger(0); + private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss"); + + public static String generate() { + String timestamp = LocalDateTime.now().format(formatter); + int seq = counter.incrementAndGet() % 10000; + return "ORD" + timestamp + String.format("%04d", seq); + } +} diff --git a/src/main/java/com/nlshop/util/PasswordUtil.java b/src/main/java/com/nlshop/util/PasswordUtil.java new file mode 100644 index 0000000..989fafa --- /dev/null +++ b/src/main/java/com/nlshop/util/PasswordUtil.java @@ -0,0 +1,25 @@ +package com.nlshop.util; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +public class PasswordUtil { + + public static String encrypt(String password) { + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] bytes = md.digest(password.getBytes()); + StringBuilder sb = new StringBuilder(); + for (byte b : bytes) { + sb.append(String.format("%02x", b)); + } + return sb.toString(); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("密码加密失败", e); + } + } + + public static boolean verify(String password, String encryptedPassword) { + return encrypt(password).equals(encryptedPassword); + } +} diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml new file mode 100644 index 0000000..c312981 --- /dev/null +++ b/src/main/resources/application-dev.yml @@ -0,0 +1,5 @@ +spring: + datasource: + url: jdbc:mysql://localhost:3306/nl_shop?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai + username: root + password: root diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..d1cd230 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,47 @@ +server: + port: 14001 + +spring: + application: + name: nl-shop + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://127.0.0.1:3306/z_wb_nc_shop?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true + username: root + password: root + redis: + host: 127.0.0.1 + port: 6379 + password: redis_pBjaRs + timeout: 3000ms + lettuce: + pool: + max-active: 8 + max-idle: 8 + min-idle: 0 + servlet: + multipart: + enabled: true + max-file-size: 10MB + max-request-size: 10MB + thymeleaf: + prefix: classpath:/templates/ + suffix: .html + mode: HTML + encoding: UTF-8 + cache: false + +mybatis: + mapper-locations: classpath:mapper/*.xml + type-aliases-package: com.nlshop.entity + configuration: + map-underscore-to-camel-case: true + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + +pagehelper: + helper-dialect: mysql + reasonable: true + support-methods-arguments: true + +file: + upload-path: ${user.dir}/src/main/resources/static/upload diff --git a/src/main/resources/db/schema.sql b/src/main/resources/db/schema.sql new file mode 100644 index 0000000..4e3a445 --- /dev/null +++ b/src/main/resources/db/schema.sql @@ -0,0 +1,248 @@ +-- 奶茶店管理系统数据库表结构 + +-- 用户表 +CREATE TABLE IF NOT EXISTS `user` ( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT, + `username` VARCHAR(50) UNIQUE NOT NULL COMMENT '用户名', + `password` VARCHAR(255) NOT NULL COMMENT '密码(加密)', + `name` VARCHAR(50) COMMENT '姓名', + `gender` TINYINT COMMENT '性别:0-女,1-男', + `phone` VARCHAR(20) COMMENT '手机号', + `email` VARCHAR(100) COMMENT '邮箱', + `avatar` VARCHAR(255) COMMENT '头像路径', + `address` VARCHAR(255) COMMENT '默认地址', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; + +-- 用户地址表 +CREATE TABLE IF NOT EXISTS `user_address` ( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT, + `user_id` BIGINT NOT NULL COMMENT '用户ID', + `address` VARCHAR(255) NOT NULL COMMENT '详细地址', + `contact_name` VARCHAR(50) COMMENT '联系人姓名', + `contact_phone` VARCHAR(20) COMMENT '联系人电话', + `is_default` TINYINT DEFAULT 0 COMMENT '是否默认:0-否,1-是', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户地址表'; + +-- 商家表 +CREATE TABLE IF NOT EXISTS `merchant` ( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT, + `username` VARCHAR(50) UNIQUE NOT NULL COMMENT '用户名', + `password` VARCHAR(255) NOT NULL COMMENT '密码(加密)', + `store_name` VARCHAR(100) NOT NULL COMMENT '门店名称', + `address` VARCHAR(255) COMMENT '门店地址', + `phone` VARCHAR(20) COMMENT '联系电话', + `email` VARCHAR(100) COMMENT '邮箱', + `manager_name` VARCHAR(50) COMMENT '负责人姓名', + `avatar` VARCHAR(255) COMMENT '头像路径', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, + `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商家表'; + +-- 商品表 +CREATE TABLE IF NOT EXISTS `product` ( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT, + `merchant_id` BIGINT NOT NULL COMMENT '商家ID', + `name` VARCHAR(100) NOT NULL COMMENT '商品名称', + `category` VARCHAR(50) COMMENT '分类', + `description` TEXT COMMENT '商品描述', + `base_price` DECIMAL(10,2) NOT NULL COMMENT '基础价格', + `image` VARCHAR(255) COMMENT '商品图片', + `status` TINYINT DEFAULT 1 COMMENT '状态:0-下架,1-上架', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`merchant_id`) REFERENCES `merchant`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表'; + +-- 商品规格表 +CREATE TABLE IF NOT EXISTS `product_spec` ( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT, + `product_id` BIGINT NOT NULL COMMENT '商品ID', + `spec_name` VARCHAR(50) NOT NULL COMMENT '规格名称(大杯/中杯/小杯)', + `price_adjust` DECIMAL(10,2) DEFAULT 0 COMMENT '价格调整(相对于基础价格)', + FOREIGN KEY (`product_id`) REFERENCES `product`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品规格表'; + +-- 配料表 +CREATE TABLE IF NOT EXISTS `product_topping` ( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT, + `product_id` BIGINT NOT NULL COMMENT '商品ID', + `topping_name` VARCHAR(50) NOT NULL COMMENT '配料名称', + `price` DECIMAL(10,2) DEFAULT 0 COMMENT '配料价格', + FOREIGN KEY (`product_id`) REFERENCES `product`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='配料表'; + +-- 商品定制选项表 +CREATE TABLE IF NOT EXISTS `product_custom` ( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT, + `product_id` BIGINT NOT NULL COMMENT '商品ID', + `option_type` VARCHAR(20) NOT NULL COMMENT '选项类型:sweetness-甜度,ice-冰度', + `option_value` VARCHAR(50) NOT NULL COMMENT '选项值', + `price_adjust` DECIMAL(10,2) DEFAULT 0 COMMENT '价格调整', + FOREIGN KEY (`product_id`) REFERENCES `product`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品定制选项表'; + +-- 购物车表 +CREATE TABLE IF NOT EXISTS `cart` ( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT, + `user_id` BIGINT NOT NULL COMMENT '用户ID', + `product_id` BIGINT NOT NULL COMMENT '商品ID', + `spec_id` BIGINT COMMENT '规格ID', + `quantity` INT DEFAULT 1 COMMENT '数量', + `custom_sweetness` VARCHAR(20) COMMENT '甜度定制', + `custom_ice` VARCHAR(20) COMMENT '冰度定制', + `toppings` TEXT COMMENT '配料JSON', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE CASCADE, + FOREIGN KEY (`product_id`) REFERENCES `product`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='购物车表'; + +-- 订单表 +CREATE TABLE IF NOT EXISTS `order` ( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT, + `order_no` VARCHAR(50) UNIQUE NOT NULL COMMENT '订单号', + `user_id` BIGINT NOT NULL COMMENT '用户ID', + `merchant_id` BIGINT NOT NULL COMMENT '商家ID', + `total_amount` DECIMAL(10,2) NOT NULL COMMENT '订单总金额', + `payment_method` VARCHAR(20) COMMENT '支付方式:wechat/alipay', + `payment_status` TINYINT DEFAULT 0 COMMENT '支付状态:0-未支付,1-已支付', + `order_status` VARCHAR(20) DEFAULT 'PENDING_PAY' COMMENT '订单状态:PENDING_PAY-待支付,MAKING-制作中,READY-待取餐,COMPLETED-已完成,CANCELLED-已取消', + `address` VARCHAR(255) NOT NULL COMMENT '收货地址', + `contact_phone` VARCHAR(20) NOT NULL COMMENT '联系电话', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `pay_time` DATETIME COMMENT '支付时间', + `complete_time` DATETIME COMMENT '完成时间', + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`), + FOREIGN KEY (`merchant_id`) REFERENCES `merchant`(`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表'; + +-- 订单明细表 +CREATE TABLE IF NOT EXISTS `order_item` ( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT, + `order_id` BIGINT NOT NULL COMMENT '订单ID', + `product_id` BIGINT NOT NULL COMMENT '商品ID', + `product_name` VARCHAR(100) NOT NULL COMMENT '商品名称', + `spec_name` VARCHAR(50) COMMENT '规格名称', + `quantity` INT NOT NULL COMMENT '数量', + `price` DECIMAL(10,2) NOT NULL COMMENT '单价', + `custom_sweetness` VARCHAR(20) COMMENT '甜度', + `custom_ice` VARCHAR(20) COMMENT '冰度', + `toppings` TEXT COMMENT '配料JSON', + `subtotal` DECIMAL(10,2) NOT NULL COMMENT '小计', + FOREIGN KEY (`order_id`) REFERENCES `order`(`id`) ON DELETE CASCADE, + FOREIGN KEY (`product_id`) REFERENCES `product`(`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单明细表'; + +-- 订单跟踪表 +CREATE TABLE IF NOT EXISTS `order_track` ( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT, + `order_id` BIGINT NOT NULL COMMENT '订单ID', + `status` VARCHAR(20) NOT NULL COMMENT '状态', + `description` VARCHAR(255) COMMENT '描述', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`order_id`) REFERENCES `order`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单跟踪表'; + +-- 公告表 +CREATE TABLE IF NOT EXISTS `announcement` ( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT, + `merchant_id` BIGINT NOT NULL COMMENT '商家ID', + `title` VARCHAR(200) NOT NULL COMMENT '标题', + `content` TEXT COMMENT '内容', + `type` VARCHAR(20) DEFAULT 'NOTICE' COMMENT '类型:ACTIVITY-活动,NOTICE-通知', + `status` TINYINT DEFAULT 1 COMMENT '状态:0-下架,1-发布', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`merchant_id`) REFERENCES `merchant`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='公告表'; + +-- 留言反馈表 +CREATE TABLE IF NOT EXISTS `message` ( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT, + `user_id` BIGINT NOT NULL COMMENT '用户ID', + `merchant_id` BIGINT NOT NULL COMMENT '商家ID', + `order_id` BIGINT COMMENT '订单ID', + `content` TEXT NOT NULL COMMENT '留言内容', + `reply` TEXT COMMENT '回复内容', + `status` TINYINT DEFAULT 0 COMMENT '状态:0-未回复,1-已回复', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, + `reply_time` DATETIME COMMENT '回复时间', + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`), + FOREIGN KEY (`merchant_id`) REFERENCES `merchant`(`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='留言反馈表'; + +-- 评价表 +CREATE TABLE IF NOT EXISTS `review` ( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT, + `user_id` BIGINT NOT NULL COMMENT '用户ID', + `order_id` BIGINT NOT NULL COMMENT '订单ID', + `product_id` BIGINT NOT NULL COMMENT '商品ID', + `rating` TINYINT NOT NULL COMMENT '评分:1-5', + `content` TEXT COMMENT '评价内容', + `reply` TEXT COMMENT '商家回复', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`), + FOREIGN KEY (`order_id`) REFERENCES `order`(`id`), + FOREIGN KEY (`product_id`) REFERENCES `product`(`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='评价表'; + +-- 库存表 +CREATE TABLE IF NOT EXISTS `inventory` ( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT, + `merchant_id` BIGINT NOT NULL COMMENT '商家ID', + `material_name` VARCHAR(100) NOT NULL COMMENT '原料名称', + `quantity` DECIMAL(10,2) NOT NULL COMMENT '数量', + `unit` VARCHAR(20) COMMENT '单位', + `min_threshold` DECIMAL(10,2) DEFAULT 0 COMMENT '最低库存阈值', + `last_in_time` DATETIME COMMENT '最后入库时间', + `last_out_time` DATETIME COMMENT '最后出库时间', + FOREIGN KEY (`merchant_id`) REFERENCES `merchant`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存表'; + +-- 库存操作日志表 +CREATE TABLE IF NOT EXISTS `inventory_log` ( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT, + `inventory_id` BIGINT NOT NULL COMMENT '库存ID', + `operation_type` VARCHAR(20) NOT NULL COMMENT '操作类型:IN-入库,OUT-出库', + `quantity` DECIMAL(10,2) NOT NULL COMMENT '操作数量', + `operator` VARCHAR(50) COMMENT '操作人', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`inventory_id`) REFERENCES `inventory`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存操作日志表'; + +-- 用户行为表(用于协同过滤) +CREATE TABLE IF NOT EXISTS `user_behavior` ( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT, + `user_id` BIGINT NOT NULL COMMENT '用户ID', + `product_id` BIGINT NOT NULL COMMENT '商品ID', + `behavior_type` VARCHAR(20) NOT NULL COMMENT '行为类型:VIEW-浏览,PURCHASE-购买,RATING-评分', + `score` DECIMAL(5,2) DEFAULT 0 COMMENT '评分(0-5)', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE CASCADE, + FOREIGN KEY (`product_id`) REFERENCES `product`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户行为表'; + +-- 用户相似度缓存表(可选) +CREATE TABLE IF NOT EXISTS `user_similarity` ( + `user1_id` BIGINT NOT NULL COMMENT '用户1ID', + `user2_id` BIGINT NOT NULL COMMENT '用户2ID', + `similarity_score` DECIMAL(10,6) NOT NULL COMMENT '相似度分数', + `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`user1_id`, `user2_id`), + FOREIGN KEY (`user1_id`) REFERENCES `user`(`id`) ON DELETE CASCADE, + FOREIGN KEY (`user2_id`) REFERENCES `user`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户相似度缓存表'; + +-- 创建索引 +CREATE INDEX idx_user_username ON `user`(`username`); +CREATE INDEX idx_merchant_username ON `merchant`(`username`); +CREATE INDEX idx_product_merchant ON `product`(`merchant_id`); +CREATE INDEX idx_product_status ON `product`(`status`); +CREATE INDEX idx_cart_user ON `cart`(`user_id`); +CREATE INDEX idx_order_user ON `order`(`user_id`); +CREATE INDEX idx_order_merchant ON `order`(`merchant_id`); +CREATE INDEX idx_order_status ON `order`(`order_status`); +CREATE INDEX idx_order_no ON `order`(`order_no`); +CREATE INDEX idx_user_behavior_user ON `user_behavior`(`user_id`); +CREATE INDEX idx_user_behavior_product ON `user_behavior`(`product_id`); diff --git a/src/main/resources/mapper/AnnouncementMapper.xml b/src/main/resources/mapper/AnnouncementMapper.xml new file mode 100644 index 0000000..7beccba --- /dev/null +++ b/src/main/resources/mapper/AnnouncementMapper.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + INSERT INTO announcement (merchant_id, title, content, type, status) + VALUES (#{merchantId}, #{title}, #{content}, #{type}, #{status}) + + + + + + + + + + UPDATE announcement + + title = #{title}, + content = #{content}, + type = #{type}, + status = #{status}, + + WHERE id = #{id} + + + + DELETE FROM announcement WHERE id = #{id} + + diff --git a/src/main/resources/mapper/CartMapper.xml b/src/main/resources/mapper/CartMapper.xml new file mode 100644 index 0000000..5d0cd6b --- /dev/null +++ b/src/main/resources/mapper/CartMapper.xml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO cart (user_id, product_id, spec_id, quantity, custom_sweetness, custom_ice, toppings) + VALUES (#{userId}, #{productId}, #{specId}, #{quantity}, #{customSweetness}, #{customIce}, #{toppings}) + + + + + + + + + + UPDATE cart + + quantity = #{quantity}, + custom_sweetness = #{customSweetness}, + custom_ice = #{customIce}, + toppings = #{toppings}, + + WHERE id = #{id} + + + + UPDATE cart SET quantity = #{quantity} WHERE id = #{id} + + + + DELETE FROM cart WHERE id = #{id} + + + + DELETE FROM cart WHERE user_id = #{userId} + + + + DELETE FROM cart + WHERE id IN + + #{id} + + AND user_id = #{userId} + + diff --git a/src/main/resources/mapper/InventoryLogMapper.xml b/src/main/resources/mapper/InventoryLogMapper.xml new file mode 100644 index 0000000..7d8e843 --- /dev/null +++ b/src/main/resources/mapper/InventoryLogMapper.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + INSERT INTO inventory_log (inventory_id, operation_type, quantity, operator) + VALUES (#{inventoryId}, #{operationType}, #{quantity}, #{operator}) + + + + diff --git a/src/main/resources/mapper/InventoryMapper.xml b/src/main/resources/mapper/InventoryMapper.xml new file mode 100644 index 0000000..3b5442e --- /dev/null +++ b/src/main/resources/mapper/InventoryMapper.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + INSERT INTO inventory (merchant_id, material_name, quantity, unit, min_threshold) + VALUES (#{merchantId}, #{materialName}, #{quantity}, #{unit}, #{minThreshold}) + + + + + + + + + + + + UPDATE inventory + + quantity = #{quantity}, + unit = #{unit}, + min_threshold = #{minThreshold}, + last_in_time = #{lastInTime}, + last_out_time = #{lastOutTime}, + + WHERE id = #{id} + + + + DELETE FROM inventory WHERE id = #{id} + + diff --git a/src/main/resources/mapper/MerchantMapper.xml b/src/main/resources/mapper/MerchantMapper.xml new file mode 100644 index 0000000..c01565a --- /dev/null +++ b/src/main/resources/mapper/MerchantMapper.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + INSERT INTO merchant (username, password, store_name, address, phone, email, manager_name, avatar) + VALUES (#{username}, #{password}, #{storeName}, #{address}, #{phone}, #{email}, #{managerName}, #{avatar}) + + + + + + + + UPDATE merchant + + store_name = #{storeName}, + address = #{address}, + phone = #{phone}, + email = #{email}, + manager_name = #{managerName}, + avatar = #{avatar}, + + WHERE id = #{id} + + diff --git a/src/main/resources/mapper/MessageMapper.xml b/src/main/resources/mapper/MessageMapper.xml new file mode 100644 index 0000000..8c2a5f1 --- /dev/null +++ b/src/main/resources/mapper/MessageMapper.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + INSERT INTO message (user_id, merchant_id, order_id, content, status) + VALUES (#{userId}, #{merchantId}, #{orderId}, #{content}, #{status}) + + + + + + + + + + UPDATE message + + reply = #{reply}, + status = #{status}, + reply_time = #{replyTime}, + + WHERE id = #{id} + + diff --git a/src/main/resources/mapper/OrderItemMapper.xml b/src/main/resources/mapper/OrderItemMapper.xml new file mode 100644 index 0000000..bc83853 --- /dev/null +++ b/src/main/resources/mapper/OrderItemMapper.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + INSERT INTO order_item (order_id, product_id, product_name, spec_name, quantity, price, + custom_sweetness, custom_ice, toppings, subtotal) + VALUES (#{orderId}, #{productId}, #{productName}, #{specName}, #{quantity}, #{price}, + #{customSweetness}, #{customIce}, #{toppings}, #{subtotal}) + + + + + + diff --git a/src/main/resources/mapper/OrderMapper.xml b/src/main/resources/mapper/OrderMapper.xml new file mode 100644 index 0000000..a440055 --- /dev/null +++ b/src/main/resources/mapper/OrderMapper.xml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO `order` (order_no, user_id, merchant_id, total_amount, payment_method, + payment_status, order_status, address, contact_phone, pay_time, complete_time) + VALUES (#{orderNo}, #{userId}, #{merchantId}, #{totalAmount}, #{paymentMethod}, + #{paymentStatus}, #{orderStatus}, #{address}, #{contactPhone}, #{payTime}, #{completeTime}) + + + + + + + + + + + + UPDATE `order` + + total_amount = #{totalAmount}, + payment_method = #{paymentMethod}, + payment_status = #{paymentStatus}, + order_status = #{orderStatus}, + address = #{address}, + contact_phone = #{contactPhone}, + pay_time = #{payTime}, + complete_time = #{completeTime}, + + WHERE id = #{id} + + + + UPDATE `order` SET order_status = #{status} WHERE id = #{id} + + + + UPDATE `order` + SET payment_status = #{paymentStatus}, + payment_method = #{paymentMethod}, + pay_time = NOW() + WHERE id = #{id} + + + + diff --git a/src/main/resources/mapper/OrderTrackMapper.xml b/src/main/resources/mapper/OrderTrackMapper.xml new file mode 100644 index 0000000..6f371e3 --- /dev/null +++ b/src/main/resources/mapper/OrderTrackMapper.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + INSERT INTO order_track (order_id, status, description) + VALUES (#{orderId}, #{status}, #{description}) + + + + diff --git a/src/main/resources/mapper/ProductCustomMapper.xml b/src/main/resources/mapper/ProductCustomMapper.xml new file mode 100644 index 0000000..e166c67 --- /dev/null +++ b/src/main/resources/mapper/ProductCustomMapper.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + INSERT INTO product_custom (product_id, option_type, option_value, price_adjust) + VALUES (#{productId}, #{optionType}, #{optionValue}, #{priceAdjust}) + + + + + + + + UPDATE product_custom + SET option_type = #{optionType}, option_value = #{optionValue}, price_adjust = #{priceAdjust} + WHERE id = #{id} + + + + DELETE FROM product_custom WHERE id = #{id} + + + + DELETE FROM product_custom WHERE product_id = #{productId} + + diff --git a/src/main/resources/mapper/ProductMapper.xml b/src/main/resources/mapper/ProductMapper.xml new file mode 100644 index 0000000..cc78848 --- /dev/null +++ b/src/main/resources/mapper/ProductMapper.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + INSERT INTO product (merchant_id, name, category, description, base_price, image, status) + VALUES (#{merchantId}, #{name}, #{category}, #{description}, #{basePrice}, #{image}, #{status}) + + + + + + + + + + + + UPDATE product + + name = #{name}, + category = #{category}, + description = #{description}, + base_price = #{basePrice}, + image = #{image}, + status = #{status}, + + WHERE id = #{id} + + + + UPDATE product SET status = #{status} WHERE id = #{id} + + diff --git a/src/main/resources/mapper/ProductSpecMapper.xml b/src/main/resources/mapper/ProductSpecMapper.xml new file mode 100644 index 0000000..412e9ab --- /dev/null +++ b/src/main/resources/mapper/ProductSpecMapper.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + INSERT INTO product_spec (product_id, spec_name, price_adjust) + VALUES (#{productId}, #{specName}, #{priceAdjust}) + + + + + + + + UPDATE product_spec + SET spec_name = #{specName}, price_adjust = #{priceAdjust} + WHERE id = #{id} + + + + DELETE FROM product_spec WHERE id = #{id} + + + + DELETE FROM product_spec WHERE product_id = #{productId} + + diff --git a/src/main/resources/mapper/ProductToppingMapper.xml b/src/main/resources/mapper/ProductToppingMapper.xml new file mode 100644 index 0000000..eed4f8a --- /dev/null +++ b/src/main/resources/mapper/ProductToppingMapper.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + INSERT INTO product_topping (product_id, topping_name, price) + VALUES (#{productId}, #{toppingName}, #{price}) + + + + + + + + UPDATE product_topping + SET topping_name = #{toppingName}, price = #{price} + WHERE id = #{id} + + + + DELETE FROM product_topping WHERE id = #{id} + + + + DELETE FROM product_topping WHERE product_id = #{productId} + + diff --git a/src/main/resources/mapper/ReviewMapper.xml b/src/main/resources/mapper/ReviewMapper.xml new file mode 100644 index 0000000..e53b279 --- /dev/null +++ b/src/main/resources/mapper/ReviewMapper.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + INSERT INTO review (user_id, order_id, product_id, rating, content) + VALUES (#{userId}, #{orderId}, #{productId}, #{rating}, #{content}) + + + + + + + + + + + + + + UPDATE review + + reply = #{reply}, + + WHERE id = #{id} + + diff --git a/src/main/resources/mapper/UserAddressMapper.xml b/src/main/resources/mapper/UserAddressMapper.xml new file mode 100644 index 0000000..7ce2cd7 --- /dev/null +++ b/src/main/resources/mapper/UserAddressMapper.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + INSERT INTO user_address (user_id, address, contact_name, contact_phone, is_default) + VALUES (#{userId}, #{address}, #{contactName}, #{contactPhone}, #{isDefault}) + + + + + + + + + + UPDATE user_address + + address = #{address}, + contact_name = #{contactName}, + contact_phone = #{contactPhone}, + is_default = #{isDefault}, + + WHERE id = #{id} + + + + DELETE FROM user_address WHERE id = #{id} + + diff --git a/src/main/resources/mapper/UserBehaviorMapper.xml b/src/main/resources/mapper/UserBehaviorMapper.xml new file mode 100644 index 0000000..87a8fb4 --- /dev/null +++ b/src/main/resources/mapper/UserBehaviorMapper.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + INSERT INTO user_behavior (user_id, product_id, behavior_type, score) + VALUES (#{userId}, #{productId}, #{behaviorType}, #{score}) + + + + + + + + + + UPDATE user_behavior + SET score = #{score}, create_time = NOW() + WHERE id = #{id} + + + + + + + + diff --git a/src/main/resources/mapper/UserMapper.xml b/src/main/resources/mapper/UserMapper.xml new file mode 100644 index 0000000..516dd07 --- /dev/null +++ b/src/main/resources/mapper/UserMapper.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + INSERT INTO user (username, password, name, gender, phone, email, avatar, address) + VALUES (#{username}, #{password}, #{name}, #{gender}, #{phone}, #{email}, #{avatar}, #{address}) + + + + + + + + UPDATE user + + name = #{name}, + gender = #{gender}, + phone = #{phone}, + email = #{email}, + avatar = #{avatar}, + address = #{address}, + + WHERE id = #{id} + + + + UPDATE user SET password = #{password} WHERE id = #{id} + + + + diff --git a/src/main/resources/mapper/UserSimilarityMapper.xml b/src/main/resources/mapper/UserSimilarityMapper.xml new file mode 100644 index 0000000..104f24f --- /dev/null +++ b/src/main/resources/mapper/UserSimilarityMapper.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + INSERT INTO user_similarity (user1_id, user2_id, similarity_score) + VALUES (#{user1Id}, #{user2Id}, #{similarityScore}) + ON DUPLICATE KEY UPDATE + similarity_score = #{similarityScore}, update_time = NOW() + + + + + + diff --git a/src/main/resources/static/js/cookie-utils.js b/src/main/resources/static/js/cookie-utils.js new file mode 100644 index 0000000..8d12a57 --- /dev/null +++ b/src/main/resources/static/js/cookie-utils.js @@ -0,0 +1,70 @@ +// Cookie工具类 +const CookieUtils = { + /** + * 设置Cookie + * @param {string} name Cookie名称 + * @param {string} value Cookie值 + * @param {number} days 过期天数,默认7天 + */ + set(name, value, days = 7) { + const expires = new Date(); + expires.setTime(expires.getTime() + days * 24 * 60 * 60 * 1000); + document.cookie = `${name}=${encodeURIComponent(value)};expires=${expires.toUTCString()};path=/`; + }, + + /** + * 获取Cookie + * @param {string} name Cookie名称 + * @returns {string|null} Cookie值,不存在返回null + */ + get(name) { + const nameEQ = name + "="; + const ca = document.cookie.split(';'); + for (let i = 0; i < ca.length; i++) { + let c = ca[i]; + while (c.charAt(0) === ' ') c = c.substring(1, c.length); + if (c.indexOf(nameEQ) === 0) { + return decodeURIComponent(c.substring(nameEQ.length, c.length)); + } + } + return null; + }, + + /** + * 删除Cookie + * @param {string} name Cookie名称 + */ + remove(name) { + document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 UTC;path=/;`; + }, + + /** + * 获取JSON格式的Cookie + * @param {string} name Cookie名称 + * @returns {any|null} 解析后的JSON对象,不存在或解析失败返回null + */ + getJSON(name) { + const value = this.get(name); + if (!value) return null; + try { + return JSON.parse(value); + } catch (e) { + console.error(`Failed to parse cookie ${name}:`, e); + return null; + } + }, + + /** + * 设置JSON格式的Cookie + * @param {string} name Cookie名称 + * @param {any} value 要存储的对象 + * @param {number} days 过期天数,默认7天 + */ + setJSON(name, value, days = 7) { + try { + this.set(name, JSON.stringify(value), days); + } catch (e) { + console.error(`Failed to stringify cookie ${name}:`, e); + } + } +}; diff --git a/src/main/resources/static/js/custom-modal.js b/src/main/resources/static/js/custom-modal.js new file mode 100644 index 0000000..e71a063 --- /dev/null +++ b/src/main/resources/static/js/custom-modal.js @@ -0,0 +1,356 @@ +/** + * 自定义模态框组件 + * 符合项目UI设计规范,具有平滑过渡动画和响应式显示 + */ + +class CustomModal { + constructor(options = {}) { + this.id = options.id || 'customModal_' + Date.now(); + this.title = options.title || ''; + this.content = options.content || ''; + this.size = options.size || 'medium'; // small, medium, large + this.showClose = options.showClose !== false; + this.onClose = options.onClose || null; + this.onConfirm = options.onConfirm || null; + this.confirmText = options.confirmText || '确定'; + this.cancelText = options.cancelText || '取消'; + this.showFooter = options.showFooter !== false; + this.backdrop = options.backdrop !== false; + this.modal = null; + } + + show() { + // 创建模态框HTML + const modalHtml = this._createModalHtml(); + + // 移除已存在的模态框 + const existing = document.getElementById(this.id); + if (existing) { + existing.remove(); + } + + // 添加到body + document.body.insertAdjacentHTML('beforeend', modalHtml); + this.modal = document.getElementById(this.id); + this.backdropEl = document.getElementById(this.id + '_backdrop'); + + // 添加显示动画 + requestAnimationFrame(() => { + this.modal.classList.add('show'); + if (this.backdropEl) { + this.backdropEl.classList.add('show'); + } + document.body.style.overflow = 'hidden'; + }); + + // 绑定事件 + this._bindEvents(); + } + + hide() { + if (!this.modal) return; + + this.modal.classList.remove('show'); + if (this.backdropEl) { + this.backdropEl.classList.remove('show'); + } + document.body.style.overflow = ''; + + // 动画结束后移除 + setTimeout(() => { + if (this.modal && this.modal.parentNode) { + this.modal.remove(); + } + if (this.backdropEl && this.backdropEl.parentNode) { + this.backdropEl.remove(); + } + if (this.onClose) { + this.onClose(); + } + }, 300); + } + + _createModalHtml() { + const sizeClass = { + small: 'modal-sm', + medium: '', + large: 'modal-lg' + }[this.size]; + + const backdropHtml = this.backdrop ? + `
` : ''; + + return backdropHtml + ` +
+
+
+ ${this.title || this.showClose ? ` +
+ ${this.title ? `
${this.title}
` : ''} + ${this.showClose ? `` : ''} +
+ ` : ''} +
+ ${this.content} +
+ ${this.showFooter ? ` + + ` : ''} +
+
+
+ `; + } + + _bindEvents() { + // 关闭按钮 + const closeBtn = this.modal.querySelector('[data-dismiss="modal"]'); + if (closeBtn) { + closeBtn.addEventListener('click', () => this.hide()); + } + + // 确认按钮 + const confirmBtn = this.modal.querySelector('.custom-modal-btn-confirm'); + if (confirmBtn && this.onConfirm) { + confirmBtn.addEventListener('click', () => { + if (this.onConfirm() !== false) { + this.hide(); + } + }); + } + + // 点击背景关闭 + if (this.backdropEl) { + this.backdropEl.addEventListener('click', () => this.hide()); + } + + // 点击内容区域不关闭 + const content = this.modal.querySelector('.custom-modal-content'); + if (content) { + content.addEventListener('click', (e) => e.stopPropagation()); + } + } + + updateContent(content) { + const body = this.modal?.querySelector('.custom-modal-body'); + if (body) { + body.innerHTML = content; + } + } +} + +// CSS样式(通过JavaScript注入,或单独引入CSS文件) +const customModalStyles = ` + +`; + +// 注入样式 +if (!document.getElementById('custom-modal-styles')) { + document.head.insertAdjacentHTML('beforeend', customModalStyles); +} + +// 导出到全局 +window.CustomModal = CustomModal; + +// 便捷方法 +window.showCustomModal = function(options) { + const modal = new CustomModal(options); + modal.show(); + return modal; +}; + +window.showCustomConfirm = function(message, title = '确认操作', onConfirm = null) { + return new CustomModal({ + title: title, + content: `

${message}

`, + onConfirm: onConfirm, + showFooter: true + }); +}; diff --git a/src/main/resources/static/js/merchant-common.js b/src/main/resources/static/js/merchant-common.js new file mode 100644 index 0000000..c302084 --- /dev/null +++ b/src/main/resources/static/js/merchant-common.js @@ -0,0 +1,338 @@ +/** + * 商家后台通用脚本 + * 提供侧边栏、加载状态、交互反馈等功能 + */ + +// 侧边栏HTML模板 +function getSidebarHTML(activePage) { + const menuItems = [ + { href: '/merchant/dashboard.html', icon: 'bi-speedometer2', text: '首页', page: 'dashboard' }, + { href: '/merchant/products.html', icon: 'bi-box-seam', text: '商品管理', page: 'products' }, + { href: '/merchant/orders.html', icon: 'bi-receipt-cutoff', text: '订单管理', page: 'orders' }, + { href: '/merchant/inventory.html', icon: 'bi-archive', text: '库存管理', page: 'inventory' }, + { href: '/merchant/statistics.html', icon: 'bi-bar-chart', text: '数据统计', page: 'statistics' }, + { href: '/merchant/announcements.html', icon: 'bi-megaphone', text: '公告管理', page: 'announcements' }, + { href: '/merchant/messages.html', icon: 'bi-chat-dots', text: '留言管理', page: 'messages' }, + { href: '/merchant/reviews.html', icon: 'bi-star', text: '评价管理', page: 'reviews' }, + { href: '/merchant/users.html', icon: 'bi-people', text: '用户管理', page: 'users' } + ]; + + const menuHTML = menuItems.map(item => { + const activeClass = item.page === activePage ? 'active' : ''; + return ` + + `; + }).join(''); + + return ` + + + + `; +} + +// 侧边栏CSS样式 +const sidebarCSS = ` + .sidebar { + position: fixed; + top: 0; + left: 0; + width: 260px; + height: 100vh; + background: linear-gradient(180deg, #2c3e50 0%, #34495e 100%); + color: #fff; + z-index: 1050; + display: flex; + flex-direction: column; + transition: transform 0.3s ease; + box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1); + } + .sidebar-header { + padding: 1.5rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + display: flex; + justify-content: space-between; + align-items: center; + } + .sidebar-brand { + font-size: 1.25rem; + font-weight: 600; + } + .sidebar-nav { + flex: 1; + overflow-y: auto; + padding: 1rem 0; + } + .sidebar-nav .nav-link { + color: rgba(255, 255, 255, 0.8); + padding: 0.75rem 1.5rem; + transition: all 0.3s; + border-left: 3px solid transparent; + display: flex; + align-items: center; + text-decoration: none; + } + .sidebar-nav .nav-link:hover { + background-color: rgba(255, 255, 255, 0.1); + color: #fff; + border-left-color: #0d6efd; + } + .sidebar-nav .nav-link.active { + background-color: rgba(13, 110, 253, 0.2); + color: #fff; + border-left-color: #0d6efd; + font-weight: 600; + } + .sidebar-nav .nav-link i { + width: 24px; + font-size: 1.1rem; + margin-right: 0.75rem; + } + .sidebar-nav .nav-text { + flex: 1; + } + .sidebar-footer { + padding: 1rem; + border-top: 1px solid rgba(255, 255, 255, 0.1); + } + @media (max-width: 767.98px) { + .sidebar { + transform: translateX(-100%); + } + .sidebar.show { + transform: translateX(0); + } + .sidebar-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + z-index: 1049; + display: none; + transition: opacity 0.3s; + } + .sidebar-overlay.show { + display: block; + } + .top-navbar { + position: fixed; + top: 0; + left: 0; + right: 0; + height: 56px; + background: linear-gradient(180deg, #2c3e50 0%, #34495e 100%); + z-index: 1048; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + } + body { + padding-top: 56px; + } + } + @media (min-width: 768px) { + .main-content { + margin-left: 260px; + min-height: 100vh; + } + } + .sidebar-nav::-webkit-scrollbar { + width: 6px; + } + .sidebar-nav::-webkit-scrollbar-track { + background: rgba(255, 255, 255, 0.05); + } + .sidebar-nav::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.2); + border-radius: 3px; + } + .sidebar-nav::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.3); + } +`; + +// 初始化侧边栏 +function initSidebar(activePage) { + // 添加CSS样式 + if (!document.getElementById('merchant-sidebar-styles')) { + const style = document.createElement('style'); + style.id = 'merchant-sidebar-styles'; + style.textContent = sidebarCSS; + document.head.appendChild(style); + } + + // 替换导航栏 + const oldNav = document.querySelector('nav.navbar'); + if (oldNav) { + oldNav.outerHTML = getSidebarHTML(activePage); + + // 初始化交互 + const sidebar = document.getElementById('sidebar'); + const sidebarOverlay = document.getElementById('sidebarOverlay'); + const sidebarToggleBtn = document.getElementById('sidebarToggleBtn'); + const sidebarCloseBtn = document.getElementById('sidebarCloseBtn'); + + if (sidebarToggleBtn) { + sidebarToggleBtn.addEventListener('click', function() { + sidebar.classList.add('show'); + sidebarOverlay.classList.add('show'); + document.body.style.overflow = 'hidden'; + }); + } + + function closeSidebar() { + sidebar.classList.remove('show'); + sidebarOverlay.classList.remove('show'); + document.body.style.overflow = ''; + } + + if (sidebarCloseBtn) { + sidebarCloseBtn.addEventListener('click', closeSidebar); + } + + if (sidebarOverlay) { + sidebarOverlay.addEventListener('click', closeSidebar); + } + + const navLinks = document.querySelectorAll('.sidebar-nav .nav-link'); + navLinks.forEach(link => { + link.addEventListener('click', function() { + if (window.innerWidth < 768) { + closeSidebar(); + } + }); + }); + } + + // 包装主内容区域 + const container = document.querySelector('.container, .container-fluid'); + if (container && !container.closest('.main-content')) { + const mainContent = document.createElement('div'); + mainContent.className = 'main-content'; + container.parentNode.insertBefore(mainContent, container); + mainContent.appendChild(container); + } +} + +// 显示加载状态 +function showLoading(elementId, message = '加载中...') { + const element = document.getElementById(elementId); + if (element) { + element.innerHTML = ` +
+
+ ${message} +
+

${message}

+
+ `; + } +} + +// 显示成功提示 +function showSuccess(message, duration = 3000) { + const toast = document.createElement('div'); + toast.className = 'toast align-items-center text-white bg-success border-0'; + toast.setAttribute('role', 'alert'); + toast.innerHTML = ` +
+
${message}
+ +
+ `; + + const toastContainer = document.getElementById('toastContainer') || createToastContainer(); + toastContainer.appendChild(toast); + + const bsToast = new bootstrap.Toast(toast); + bsToast.show(); + + toast.addEventListener('hidden.bs.toast', () => toast.remove()); +} + +// 显示错误提示 +function showError(message, duration = 3000) { + const toast = document.createElement('div'); + toast.className = 'toast align-items-center text-white bg-danger border-0'; + toast.setAttribute('role', 'alert'); + toast.innerHTML = ` +
+
${message}
+ +
+ `; + + const toastContainer = document.getElementById('toastContainer') || createToastContainer(); + toastContainer.appendChild(toast); + + const bsToast = new bootstrap.Toast(toast); + bsToast.show(); + + toast.addEventListener('hidden.bs.toast', () => toast.remove()); +} + +// 创建Toast容器 +function createToastContainer() { + const container = document.createElement('div'); + container.id = 'toastContainer'; + container.className = 'toast-container position-fixed top-0 end-0 p-3'; + container.style.zIndex = '1060'; + document.body.appendChild(container); + return container; +} + +// 检测当前页面 +function detectCurrentPage() { + const path = window.location.pathname; + if (path.includes('products')) return 'products'; + if (path.includes('orders')) return 'orders'; + if (path.includes('inventory')) return 'inventory'; + if (path.includes('statistics')) return 'statistics'; + if (path.includes('announcements')) return 'announcements'; + if (path.includes('messages')) return 'messages'; + if (path.includes('reviews')) return 'reviews'; + if (path.includes('users')) return 'users'; + return 'dashboard'; +} + +// 自动初始化 +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', function() { + initSidebar(detectCurrentPage()); + }); +} else { + initSidebar(detectCurrentPage()); +} diff --git a/src/main/resources/static/js/merchant-sidebar.js b/src/main/resources/static/js/merchant-sidebar.js new file mode 100644 index 0000000..26871e6 --- /dev/null +++ b/src/main/resources/static/js/merchant-sidebar.js @@ -0,0 +1,297 @@ +/** + * 商家后台侧边栏通用脚本 + * 提供侧边栏的HTML结构和交互逻辑 + */ + +// 生成侧边栏HTML +function generateSidebarHTML(currentPage) { + const menuItems = [ + { href: '/merchant/dashboard.html', icon: 'bi-speedometer2', text: '首页', page: 'dashboard' }, + { href: '/merchant/products.html', icon: 'bi-box-seam', text: '商品管理', page: 'products' }, + { href: '/merchant/orders.html', icon: 'bi-receipt-cutoff', text: '订单管理', page: 'orders' }, + { href: '/merchant/inventory.html', icon: 'bi-archive', text: '库存管理', page: 'inventory' }, + { href: '/merchant/statistics.html', icon: 'bi-bar-chart', text: '数据统计', page: 'statistics' }, + { href: '/merchant/announcements.html', icon: 'bi-megaphone', text: '公告管理', page: 'announcements' }, + { href: '/merchant/messages.html', icon: 'bi-chat-dots', text: '留言管理', page: 'messages' }, + { href: '/merchant/reviews.html', icon: 'bi-star', text: '评价管理', page: 'reviews' }, + { href: '/merchant/users.html', icon: 'bi-people', text: '用户管理', page: 'users' } + ]; + + const menuHTML = menuItems.map(item => { + const activeClass = item.page === currentPage ? 'active' : ''; + return ` + + `; + }).join(''); + + return ` + + + + + + + + + `; +} + +// 添加侧边栏CSS样式 +function addSidebarStyles() { + if (document.getElementById('sidebar-styles')) return; + + const style = document.createElement('style'); + style.id = 'sidebar-styles'; + style.textContent = ` + /* 侧边栏样式 */ + .sidebar { + position: fixed; + top: 0; + left: 0; + width: 260px; + height: 100vh; + background: linear-gradient(180deg, #2c3e50 0%, #34495e 100%); + color: #fff; + z-index: 1050; + display: flex; + flex-direction: column; + transition: transform 0.3s ease; + box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1); + } + + .sidebar-header { + padding: 1.5rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + display: flex; + justify-content: space-between; + align-items: center; + } + + .sidebar-brand { + font-size: 1.25rem; + font-weight: 600; + } + + .sidebar-nav { + flex: 1; + overflow-y: auto; + padding: 1rem 0; + } + + .sidebar-nav .nav-link { + color: rgba(255, 255, 255, 0.8); + padding: 0.75rem 1.5rem; + transition: all 0.3s; + border-left: 3px solid transparent; + display: flex; + align-items: center; + text-decoration: none; + } + + .sidebar-nav .nav-link:hover { + background-color: rgba(255, 255, 255, 0.1); + color: #fff; + border-left-color: #0d6efd; + } + + .sidebar-nav .nav-link.active { + background-color: rgba(13, 110, 253, 0.2); + color: #fff; + border-left-color: #0d6efd; + font-weight: 600; + } + + .sidebar-nav .nav-link i { + width: 24px; + font-size: 1.1rem; + margin-right: 0.75rem; + } + + .sidebar-nav .nav-text { + flex: 1; + } + + .sidebar-footer { + padding: 1rem; + border-top: 1px solid rgba(255, 255, 255, 0.1); + } + + /* 移动端样式 */ + @media (max-width: 767.98px) { + .sidebar { + transform: translateX(-100%); + } + + .sidebar.show { + transform: translateX(0); + } + + .sidebar-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + z-index: 1049; + display: none; + transition: opacity 0.3s; + } + + .sidebar-overlay.show { + display: block; + } + + .top-navbar { + position: fixed; + top: 0; + left: 0; + right: 0; + height: 56px; + background: linear-gradient(180deg, #2c3e50 0%, #34495e 100%); + z-index: 1048; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + } + + body { + padding-top: 56px; + } + } + + /* 桌面端主内容区域 */ + @media (min-width: 768px) { + .main-content { + margin-left: 260px; + min-height: 100vh; + } + } + + /* 滚动条样式 */ + .sidebar-nav::-webkit-scrollbar { + width: 6px; + } + + .sidebar-nav::-webkit-scrollbar-track { + background: rgba(255, 255, 255, 0.05); + } + + .sidebar-nav::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.2); + border-radius: 3px; + } + + .sidebar-nav::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.3); + } + `; + document.head.appendChild(style); +} + +// 初始化侧边栏交互 +function initSidebar() { + const sidebar = document.getElementById('sidebar'); + const sidebarOverlay = document.getElementById('sidebarOverlay'); + const sidebarToggleBtn = document.getElementById('sidebarToggleBtn'); + const sidebarCloseBtn = document.getElementById('sidebarCloseBtn'); + + if (!sidebar) return; + + // 移动端切换侧边栏 + if (sidebarToggleBtn) { + sidebarToggleBtn.addEventListener('click', function() { + sidebar.classList.add('show'); + if (sidebarOverlay) sidebarOverlay.classList.add('show'); + document.body.style.overflow = 'hidden'; + }); + } + + // 关闭侧边栏 + function closeSidebar() { + sidebar.classList.remove('show'); + if (sidebarOverlay) sidebarOverlay.classList.remove('show'); + document.body.style.overflow = ''; + } + + if (sidebarCloseBtn) { + sidebarCloseBtn.addEventListener('click', closeSidebar); + } + + if (sidebarOverlay) { + sidebarOverlay.addEventListener('click', closeSidebar); + } + + // 点击导航项时,移动端自动关闭侧边栏 + const navLinks = document.querySelectorAll('.sidebar-nav .nav-link'); + navLinks.forEach(link => { + link.addEventListener('click', function() { + if (window.innerWidth < 768) { + closeSidebar(); + } + }); + }); +} + +// 初始化侧边栏(自动检测当前页面) +function initMerchantSidebar() { + addSidebarStyles(); + + // 检测当前页面 + const path = window.location.pathname; + let currentPage = 'dashboard'; + + if (path.includes('products')) currentPage = 'products'; + else if (path.includes('orders')) currentPage = 'orders'; + else if (path.includes('inventory')) currentPage = 'inventory'; + else if (path.includes('statistics')) currentPage = 'statistics'; + else if (path.includes('announcements')) currentPage = 'announcements'; + else if (path.includes('messages')) currentPage = 'messages'; + else if (path.includes('reviews')) currentPage = 'reviews'; + else if (path.includes('users')) currentPage = 'users'; + + // 查找并替换旧的导航栏 + const oldNav = document.querySelector('nav.navbar'); + if (oldNav) { + oldNav.outerHTML = generateSidebarHTML(currentPage); + initSidebar(); + } +} + +// 页面加载完成后初始化 +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initMerchantSidebar); +} else { + initMerchantSidebar(); +} diff --git a/src/main/resources/static/js/sidebar-replacer.js b/src/main/resources/static/js/sidebar-replacer.js new file mode 100644 index 0000000..d3186e1 --- /dev/null +++ b/src/main/resources/static/js/sidebar-replacer.js @@ -0,0 +1,300 @@ +/** + * 侧边栏自动替换脚本 + * 自动检测并替换旧的导航栏为侧边栏 + */ +(function() { + 'use strict'; + + // 侧边栏CSS + const sidebarCSS = ` + .sidebar { + position: fixed; + top: 0; + left: 0; + width: 260px; + height: 100vh; + background: linear-gradient(180deg, #2c3e50 0%, #34495e 100%); + color: #fff; + z-index: 1000; + display: flex; + flex-direction: column; + transition: transform 0.3s ease; + box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1); + } + .sidebar-header { + padding: 1.5rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + display: flex; + justify-content: space-between; + align-items: center; + } + .sidebar-brand { + font-size: 1.25rem; + font-weight: 600; + } + .sidebar-nav { + flex: 1; + overflow-y: auto; + padding: 1rem 0; + } + .sidebar-nav .nav-link { + color: rgba(255, 255, 255, 0.8); + padding: 0.75rem 1.5rem; + transition: all 0.3s; + border-left: 3px solid transparent; + display: flex; + align-items: center; + text-decoration: none; + } + .sidebar-nav .nav-link:hover { + background-color: rgba(255, 255, 255, 0.1); + color: #fff; + border-left-color: #0d6efd; + } + .sidebar-nav .nav-link.active { + background-color: rgba(13, 110, 253, 0.2); + color: #fff; + border-left-color: #0d6efd; + font-weight: 600; + } + .sidebar-nav .nav-link i { + width: 24px; + font-size: 1.1rem; + margin-right: 0.75rem; + } + .sidebar-nav .nav-text { + flex: 1; + } + .sidebar-footer { + padding: 1rem; + border-top: 1px solid rgba(255, 255, 255, 0.1); + } + @media (max-width: 767.98px) { + .sidebar { + transform: translateX(-100%); + } + .sidebar.show { + transform: translateX(0); + } + .sidebar-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + z-index: 1049; + display: none; + transition: opacity 0.3s; + } + .sidebar-overlay.show { + display: block; + } + .top-navbar { + position: fixed; + top: 0; + left: 0; + right: 0; + height: 56px; + background: linear-gradient(180deg, #2c3e50 0%, #34495e 100%); + z-index: 1048; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + } + body { + padding-top: 56px; + } + } + @media (min-width: 768px) { + .main-content { + margin-left: 260px; + min-height: 100vh; + position: relative; + z-index: 1; + width: calc(100% - 260px); + } + } + .sidebar-nav::-webkit-scrollbar { + width: 6px; + } + .sidebar-nav::-webkit-scrollbar-track { + background: rgba(255, 255, 255, 0.05); + } + .sidebar-nav::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.2); + border-radius: 3px; + } + .sidebar-nav::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.3); + } + `; + + // 菜单配置 + const menuConfig = [ + { href: '/merchant/dashboard.html', icon: 'bi-speedometer2', text: '首页', page: 'dashboard' }, + { href: '/merchant/products.html', icon: 'bi-box-seam', text: '商品管理', page: 'products' }, + { href: '/merchant/orders.html', icon: 'bi-receipt-cutoff', text: '订单管理', page: 'orders' }, + { href: '/merchant/inventory.html', icon: 'bi-archive', text: '库存管理', page: 'inventory' }, + { href: '/merchant/statistics.html', icon: 'bi-bar-chart-line', text: '数据统计', page: 'statistics' }, + { href: '/merchant/announcements.html', icon: 'bi-megaphone', text: '公告管理', page: 'announcements' }, + { href: '/merchant/messages.html', icon: 'bi-chat-dots', text: '留言管理', page: 'messages' }, + { href: '/merchant/reviews.html', icon: 'bi-star', text: '评价管理', page: 'reviews' }, + { href: '/merchant/users.html', icon: 'bi-people', text: '用户管理', page: 'users' } + ]; + + // 检测当前页面 + function detectCurrentPage() { + const path = window.location.pathname; + for (const item of menuConfig) { + if (path.includes(item.page)) { + return item.page; + } + } + return 'dashboard'; + } + + // 生成侧边栏HTML + function generateSidebar(activePage) { + const menuHTML = menuConfig.map(item => { + const activeClass = item.page === activePage ? 'active' : ''; + return ` + + `; + }).join(''); + + return ` + + + + `; + } + + // 初始化侧边栏 + function initSidebar() { + // 添加CSS + if (!document.getElementById('merchant-sidebar-styles')) { + const style = document.createElement('style'); + style.id = 'merchant-sidebar-styles'; + style.textContent = sidebarCSS; + document.head.appendChild(style); + } + + // 替换导航栏 + const oldNav = document.querySelector('nav.navbar'); + if (oldNav && !document.getElementById('sidebar')) { + const activePage = detectCurrentPage(); + oldNav.outerHTML = generateSidebar(activePage); + + // 包装主内容 - 确保不重复包装 + let mainContent = document.querySelector('.main-content'); + if (!mainContent) { + // 查找body的直接子元素中的container + const bodyChildren = Array.from(document.body.children); + let container = null; + + // 优先查找container-fluid,然后是container + for (const child of bodyChildren) { + if (child.classList.contains('container-fluid') || child.classList.contains('container')) { + if (!child.closest('.main-content') && !child.closest('.sidebar') && !child.closest('.sidebar-overlay') && !child.closest('.top-navbar')) { + container = child; + break; + } + } + } + + if (container) { + mainContent = document.createElement('div'); + mainContent.className = 'main-content'; + container.parentNode.insertBefore(mainContent, container); + mainContent.appendChild(container); + } + } + + // 初始化交互 + setupSidebarInteractions(); + } + } + + // 设置侧边栏交互 + function setupSidebarInteractions() { + const sidebar = document.getElementById('sidebar'); + const sidebarOverlay = document.getElementById('sidebarOverlay'); + const sidebarToggleBtn = document.getElementById('sidebarToggleBtn'); + const sidebarCloseBtn = document.getElementById('sidebarCloseBtn'); + + if (!sidebar) return; + + function openSidebar() { + sidebar.classList.add('show'); + if (sidebarOverlay) sidebarOverlay.classList.add('show'); + document.body.style.overflow = 'hidden'; + } + + function closeSidebar() { + sidebar.classList.remove('show'); + if (sidebarOverlay) sidebarOverlay.classList.remove('show'); + document.body.style.overflow = ''; + } + + if (sidebarToggleBtn) { + sidebarToggleBtn.addEventListener('click', openSidebar); + } + + if (sidebarCloseBtn) { + sidebarCloseBtn.addEventListener('click', closeSidebar); + } + + if (sidebarOverlay) { + sidebarOverlay.addEventListener('click', closeSidebar); + } + + // 点击导航项时,移动端自动关闭 + const navLinks = document.querySelectorAll('.sidebar-nav .nav-link'); + navLinks.forEach(link => { + link.addEventListener('click', function() { + if (window.innerWidth < 768) { + closeSidebar(); + } + }); + }); + } + + // 页面加载完成后初始化 + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initSidebar); + } else { + initSidebar(); + } +})(); diff --git a/src/main/resources/static/js/ui-enhancements.js b/src/main/resources/static/js/ui-enhancements.js new file mode 100644 index 0000000..7313a68 --- /dev/null +++ b/src/main/resources/static/js/ui-enhancements.js @@ -0,0 +1,289 @@ +/** + * UI交互增强脚本 + * 提供加载状态、视觉反馈、操作提示等功能 + */ + +// 创建Toast容器 +function ensureToastContainer() { + let container = document.getElementById('toastContainer'); + if (!container) { + container = document.createElement('div'); + container.id = 'toastContainer'; + container.className = 'toast-container position-fixed top-0 end-0 p-3'; + container.style.zIndex = '1060'; + document.body.appendChild(container); + } + return container; +} + +// 显示成功提示 +function showSuccess(message, duration = 3000) { + const container = ensureToastContainer(); + const toast = document.createElement('div'); + toast.className = 'toast align-items-center text-white bg-success border-0'; + toast.setAttribute('role', 'alert'); + toast.setAttribute('aria-live', 'assertive'); + toast.setAttribute('aria-atomic', 'true'); + toast.innerHTML = ` +
+
+ ${message} +
+ +
+ `; + container.appendChild(toast); + const bsToast = new bootstrap.Toast(toast, { delay: duration }); + bsToast.show(); + toast.addEventListener('hidden.bs.toast', () => toast.remove()); +} + +// 显示错误提示 +function showError(message, duration = 4000) { + const container = ensureToastContainer(); + const toast = document.createElement('div'); + toast.className = 'toast align-items-center text-white bg-danger border-0'; + toast.setAttribute('role', 'alert'); + toast.setAttribute('aria-live', 'assertive'); + toast.setAttribute('aria-atomic', 'true'); + toast.innerHTML = ` +
+
+ ${message} +
+ +
+ `; + container.appendChild(toast); + const bsToast = new bootstrap.Toast(toast, { delay: duration }); + bsToast.show(); + toast.addEventListener('hidden.bs.toast', () => toast.remove()); +} + +// 显示警告提示 +function showWarning(message, duration = 3000) { + const container = ensureToastContainer(); + const toast = document.createElement('div'); + toast.className = 'toast align-items-center text-white bg-warning border-0'; + toast.setAttribute('role', 'alert'); + toast.setAttribute('aria-live', 'assertive'); + toast.setAttribute('aria-atomic', 'true'); + toast.innerHTML = ` +
+
+ ${message} +
+ +
+ `; + container.appendChild(toast); + const bsToast = new bootstrap.Toast(toast, { delay: duration }); + bsToast.show(); + toast.addEventListener('hidden.bs.toast', () => toast.remove()); +} + +// 显示信息提示 +function showInfo(message, duration = 3000) { + const container = ensureToastContainer(); + const toast = document.createElement('div'); + toast.className = 'toast align-items-center text-white bg-info border-0'; + toast.setAttribute('role', 'alert'); + toast.setAttribute('aria-live', 'assertive'); + toast.setAttribute('aria-atomic', 'true'); + toast.innerHTML = ` +
+
+ ${message} +
+ +
+ `; + container.appendChild(toast); + const bsToast = new bootstrap.Toast(toast, { delay: duration }); + bsToast.show(); + toast.addEventListener('hidden.bs.toast', () => toast.remove()); +} + +// 显示加载状态 +function showLoading(elementId, message = '加载中...') { + const element = document.getElementById(elementId); + if (element) { + element.innerHTML = ` +
+
+ ${message} +
+

${message}

+
+ `; + } +} + +// 按钮加载状态 +function setButtonLoading(button, loading = true, originalText = null) { + if (loading) { + if (!button.dataset.originalText) { + button.dataset.originalText = button.innerHTML; + } + button.disabled = true; + button.innerHTML = ` + + 处理中... + `; + } else { + button.disabled = false; + button.innerHTML = button.dataset.originalText || originalText || '提交'; + delete button.dataset.originalText; + } +} + +// 增强的fetch函数(带加载状态和错误处理) +async function enhancedFetch(url, options = {}) { + const { showLoading: showLoadingId, button: loadingButton, ...fetchOptions } = options; + + // 显示加载状态 + if (showLoadingId) { + showLoading(showLoadingId); + } + + // 按钮加载状态 + if (loadingButton) { + setButtonLoading(loadingButton, true); + } + + try { + const response = await fetch(url, fetchOptions); + const data = await response.json(); + + // 恢复按钮状态 + if (loadingButton) { + setButtonLoading(loadingButton, false); + } + + return { response, data }; + } catch (error) { + // 恢复按钮状态 + if (loadingButton) { + setButtonLoading(loadingButton, false); + } + + showError('网络请求失败,请检查网络连接'); + throw error; + } +} + +// 确认对话框(使用自定义模态框) +function confirmAction(message, title = '确认操作', confirmText = '确定', cancelText = '取消') { + return new Promise((resolve) => { + // 确保自定义模态框已加载 + if (typeof CustomModal === 'undefined') { + console.error('CustomModal未加载,请先引入custom-modal.js'); + // 降级到原生confirm + resolve(confirm(message)); + return; + } + + const modal = new CustomModal({ + title: title, + content: `

${message}

`, + confirmText: confirmText, + cancelText: cancelText, + onConfirm: () => { + resolve(true); + return true; + }, + onClose: () => { + resolve(false); + } + }); + modal.show(); + }); +} + +// 表单验证增强 +function validateForm(formId) { + const form = document.getElementById(formId); + if (!form) return false; + + const requiredFields = form.querySelectorAll('[required]'); + let isValid = true; + + requiredFields.forEach(field => { + if (!field.value.trim()) { + field.classList.add('is-invalid'); + isValid = false; + } else { + field.classList.remove('is-invalid'); + field.classList.add('is-valid'); + } + }); + + return isValid; +} + +// 数字格式化 +function formatNumber(num, decimals = 2) { + return parseFloat(num || 0).toFixed(decimals); +} + +// 日期格式化 +function formatDate(date, format = 'YYYY-MM-DD HH:mm:ss') { + if (!date) return '-'; + const d = new Date(date); + const year = d.getFullYear(); + const month = String(d.getMonth() + 1).padStart(2, '0'); + const day = String(d.getDate()).padStart(2, '0'); + const hours = String(d.getHours()).padStart(2, '0'); + const minutes = String(d.getMinutes()).padStart(2, '0'); + const seconds = String(d.getSeconds()).padStart(2, '0'); + + return format + .replace('YYYY', year) + .replace('MM', month) + .replace('DD', day) + .replace('HH', hours) + .replace('mm', minutes) + .replace('ss', seconds); +} + +// 防抖函数 +function debounce(func, wait) { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; +} + +// 节流函数 +function throttle(func, limit) { + let inThrottle; + return function(...args) { + if (!inThrottle) { + func.apply(this, args); + inThrottle = true; + setTimeout(() => inThrottle = false, limit); + } + }; +} + +// 导出到全局 +window.UIEnhancements = { + showSuccess, + showError, + showWarning, + showInfo, + showLoading, + setButtonLoading, + enhancedFetch, + confirmAction, + validateForm, + formatNumber, + formatDate, + debounce, + throttle +}; diff --git a/src/main/resources/static/upload/product/3a9cb9e4-97a9-4be5-ba2f-6939bebe5ba5.jpg b/src/main/resources/static/upload/product/3a9cb9e4-97a9-4be5-ba2f-6939bebe5ba5.jpg new file mode 100644 index 0000000..21985b8 Binary files /dev/null and b/src/main/resources/static/upload/product/3a9cb9e4-97a9-4be5-ba2f-6939bebe5ba5.jpg differ diff --git a/src/main/resources/static/upload/product/ad501998-b3c1-4568-ae4f-31e2d9ce0e3d.jpg b/src/main/resources/static/upload/product/ad501998-b3c1-4568-ae4f-31e2d9ce0e3d.jpg new file mode 100644 index 0000000..359f637 Binary files /dev/null and b/src/main/resources/static/upload/product/ad501998-b3c1-4568-ae4f-31e2d9ce0e3d.jpg differ diff --git a/src/main/resources/static/upload/product/f6f49393-42c0-4c27-92fe-5d54aa04888f.jpg b/src/main/resources/static/upload/product/f6f49393-42c0-4c27-92fe-5d54aa04888f.jpg new file mode 100644 index 0000000..38768ac Binary files /dev/null and b/src/main/resources/static/upload/product/f6f49393-42c0-4c27-92fe-5d54aa04888f.jpg differ diff --git a/src/main/resources/templates/merchant/announcements.html b/src/main/resources/templates/merchant/announcements.html new file mode 100644 index 0000000..ca4525a --- /dev/null +++ b/src/main/resources/templates/merchant/announcements.html @@ -0,0 +1,291 @@ + + + + + + 公告管理 - 商家后台 + + + + + + +
+
+

公告管理

+ +
+
+
+
+
+
+
+ + + + + + + + + + diff --git a/src/main/resources/templates/merchant/common/sidebar.html b/src/main/resources/templates/merchant/common/sidebar.html new file mode 100644 index 0000000..bd9bc82 --- /dev/null +++ b/src/main/resources/templates/merchant/common/sidebar.html @@ -0,0 +1,283 @@ + + + + + + + + + + + + diff --git a/src/main/resources/templates/merchant/dashboard.html b/src/main/resources/templates/merchant/dashboard.html new file mode 100644 index 0000000..443cb8d --- /dev/null +++ b/src/main/resources/templates/merchant/dashboard.html @@ -0,0 +1,413 @@ + + + + + + 商家后台 - 奶茶店管理系统 + + + + + + + + + + + + + + +
+
+

数据概览

+
+
+
+
+
+
+
今日订单
+

-

+
+ +
+
+
+
+
+
+
+
+
+
今日营收
+

-

+
+ +
+
+
+
+
+
+
+
+
+
待处理订单
+

-

+
+ +
+
+
+
+
+
+ + + + + + + diff --git a/src/main/resources/templates/merchant/inventory.html b/src/main/resources/templates/merchant/inventory.html new file mode 100644 index 0000000..e9f8cd6 --- /dev/null +++ b/src/main/resources/templates/merchant/inventory.html @@ -0,0 +1,466 @@ + + + + + + 库存管理 - 商家后台 + + + + + + + +
+
+

库存管理

+ +
+ +
+ +
+ +
+
+ + + + + + + + + + + + + diff --git a/src/main/resources/templates/merchant/login.html b/src/main/resources/templates/merchant/login.html new file mode 100644 index 0000000..f692850 --- /dev/null +++ b/src/main/resources/templates/merchant/login.html @@ -0,0 +1,81 @@ + + + + + + 商家登录 - 奶茶店管理系统 + + + + + +
+
+
+ +
+
+
+ + + + diff --git a/src/main/resources/templates/merchant/messages.html b/src/main/resources/templates/merchant/messages.html new file mode 100644 index 0000000..03fc047 --- /dev/null +++ b/src/main/resources/templates/merchant/messages.html @@ -0,0 +1,209 @@ + + + + + + 留言管理 - 商家后台 + + + + + + +
+

留言管理

+
+
+
+
+
+
+ + + + + + + + + + diff --git a/src/main/resources/templates/merchant/orders.html b/src/main/resources/templates/merchant/orders.html new file mode 100644 index 0000000..216168f --- /dev/null +++ b/src/main/resources/templates/merchant/orders.html @@ -0,0 +1,503 @@ + + + + + + 订单管理 - 商家后台 + + + + + + + +
+
+

订单管理

+
+ + + + +
+
+ +
+
+ + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/templates/merchant/product-edit.html b/src/main/resources/templates/merchant/product-edit.html new file mode 100644 index 0000000..8203bcb --- /dev/null +++ b/src/main/resources/templates/merchant/product-edit.html @@ -0,0 +1,651 @@ + + + + + + 商品编辑 - 商家后台 + + + + + +
+

新增商品

+ +
+ + +
+
基本信息
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+
+ +
+
+ 规格管理 + +
+
+
+
+
+ +
+
+ 配料管理 + +
+
+
+
+
+ +
+
+ 定制选项管理(甜度、冰度) +
+ + +
+
+
+
+
甜度选项
+
+
+
+
冰度选项
+
+
+
+
+ +
+ + 取消 +
+
+
+ + + + + + + diff --git a/src/main/resources/templates/merchant/products.html b/src/main/resources/templates/merchant/products.html new file mode 100644 index 0000000..254e191 --- /dev/null +++ b/src/main/resources/templates/merchant/products.html @@ -0,0 +1,470 @@ + + + + + + 商品管理 - 商家后台 + + + + + + + + + + + + + + +
+
+
+

商品管理

+ +
+
+
+
+
+
+
+ + + + + + + diff --git a/src/main/resources/templates/merchant/register.html b/src/main/resources/templates/merchant/register.html new file mode 100644 index 0000000..c439806 --- /dev/null +++ b/src/main/resources/templates/merchant/register.html @@ -0,0 +1,120 @@ + + + + + + 商家注册 - 奶茶店管理系统 + + + + + +
+
+
+
+
+

+ 商家后台 +

+

商家注册

+
+
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+ +
+ +
+
+
+
+
+ + + + diff --git a/src/main/resources/templates/merchant/reviews.html b/src/main/resources/templates/merchant/reviews.html new file mode 100644 index 0000000..e146a67 --- /dev/null +++ b/src/main/resources/templates/merchant/reviews.html @@ -0,0 +1,212 @@ + + + + + + 评价管理 - 商家后台 + + + + + + +
+

评价管理

+
+
+
+
+
+
+ + + + + + + + + + diff --git a/src/main/resources/templates/merchant/statistics.html b/src/main/resources/templates/merchant/statistics.html new file mode 100644 index 0000000..819087b --- /dev/null +++ b/src/main/resources/templates/merchant/statistics.html @@ -0,0 +1,491 @@ + + + + + + 数据统计 - 商家后台 + + + + + + + +
+

数据统计

+ +
+
+ + + +
+
+ +
+
+
+
+
销售统计
+
+
+

请选择时间段

+
+
+
+
+
+
+
营收统计
+
+
+

请选择时间段

+
+
+
+
+ +
+
+
+
+
活跃用户
+
+
+

加载中...

+
+
+
+
+
+
+
粘性用户
+
+
+

加载中...

+
+
+
+
+ +
+
+
热销商品
+
+
+
+
+
+
+ + + + + + + diff --git a/src/main/resources/templates/merchant/users.html b/src/main/resources/templates/merchant/users.html new file mode 100644 index 0000000..c3fd638 --- /dev/null +++ b/src/main/resources/templates/merchant/users.html @@ -0,0 +1,163 @@ + + + + + + 用户管理 - 商家后台 + + + + + + +
+

用户管理

+
+
+
+ +
+
+
+ + + + + + diff --git a/src/main/resources/templates/user/cart.html b/src/main/resources/templates/user/cart.html new file mode 100644 index 0000000..904399b --- /dev/null +++ b/src/main/resources/templates/user/cart.html @@ -0,0 +1,389 @@ + + + + + + 购物袋 + + + + + +
购物袋
+
+
+ +
加载中...
+
+
+ + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/templates/user/checkout.html b/src/main/resources/templates/user/checkout.html new file mode 100644 index 0000000..490e3fa --- /dev/null +++ b/src/main/resources/templates/user/checkout.html @@ -0,0 +1,616 @@ + + + + + + 确认订单 + + + + + + + + +
+ + +
+
配送地址
+
+
+
+
+ 加载地址... +
+
+ +
+ +
+ + +
+
订单商品
+
+
+ + + + +
+
订单备注
+ +
+ + +
+
+ 合计 + ¥0.00 +
+ +
+ + + + +
+ + +
+ +
+ + +
+ + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/templates/user/index.html b/src/main/resources/templates/user/index.html new file mode 100644 index 0000000..f798e08 --- /dev/null +++ b/src/main/resources/templates/user/index.html @@ -0,0 +1,652 @@ + + + + + + 首页 - 荣光奶茶店 + + + + + +
+ +
+
+ +
+
+

荣光奶茶店

+
+ + 科技园南区C栋 + + + 4.9 + +
+
+
+ +
+
+ + + + + +
+ +

店长推荐

+
+
+ + +
+ +

全部饮品

+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/templates/user/login.html b/src/main/resources/templates/user/login.html new file mode 100644 index 0000000..2782cd6 --- /dev/null +++ b/src/main/resources/templates/user/login.html @@ -0,0 +1,236 @@ + + + + + + 登录 + + + + + +
+
欢迎回来
+
登录您的账号以享受美味
+
+ +
+
+ + +
+ +
+ + +
+ + +
+ + + + + + + \ No newline at end of file diff --git a/src/main/resources/templates/user/order-detail.html b/src/main/resources/templates/user/order-detail.html new file mode 100644 index 0000000..92c447c --- /dev/null +++ b/src/main/resources/templates/user/order-detail.html @@ -0,0 +1,356 @@ + + + + + + 订单详情 + + + + + + + + +
加载中...
+ + + +
+ +
+ + + + + + + \ No newline at end of file diff --git a/src/main/resources/templates/user/orders.html b/src/main/resources/templates/user/orders.html new file mode 100644 index 0000000..407da0b --- /dev/null +++ b/src/main/resources/templates/user/orders.html @@ -0,0 +1,441 @@ + + + + + + 我的订单 + + + + + +
+
全部
+
待支付
+
制作中
+
待取餐
+
已完成
+
+ +
+ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/templates/user/payment-success.html b/src/main/resources/templates/user/payment-success.html new file mode 100644 index 0000000..b4bbb63 --- /dev/null +++ b/src/main/resources/templates/user/payment-success.html @@ -0,0 +1,214 @@ + + + + + + 支付成功 + + + + + +
+
+ +
+
支付成功
+
感谢您的购买,订单已提交成功
+ +
+
+ 订单编号 + -- +
+
+ 支付金额 + -- +
+
+ +
+ + +
+
+ + + + diff --git a/src/main/resources/templates/user/payment.html b/src/main/resources/templates/user/payment.html new file mode 100644 index 0000000..2f7a69c --- /dev/null +++ b/src/main/resources/templates/user/payment.html @@ -0,0 +1,348 @@ + + + + + + 支付订单 + + + + + + + + +
+ + +
+
+
需支付
+
¥0.00
+
+
+ + +
+
订单商品
+
+
+ + +
+
选择支付方式
+ + +
+ + +
+
+ 合计 + ¥0.00 +
+ +
+ + + + + + + diff --git a/src/main/resources/templates/user/product-detail.html b/src/main/resources/templates/user/product-detail.html new file mode 100644 index 0000000..51bf1e0 --- /dev/null +++ b/src/main/resources/templates/user/product-detail.html @@ -0,0 +1,356 @@ + + + + + + 商品详情 + + + + + + + + + + +
+

Loading...

+

+ +
+ + + +
+ +
+
数量
+
+ + + +
+
+
+
+ +
+
+ 总计金额 + ¥0.00 +
+ +
+ + + + + + \ No newline at end of file diff --git a/src/main/resources/templates/user/profile.html b/src/main/resources/templates/user/profile.html new file mode 100644 index 0000000..94ef48d --- /dev/null +++ b/src/main/resources/templates/user/profile.html @@ -0,0 +1,401 @@ + + + + + + 个人中心 + + + + + + +
+
+
--
+
--
+ +
+ + + + +
+ +
+ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/templates/user/register.html b/src/main/resources/templates/user/register.html new file mode 100644 index 0000000..03eb5bb --- /dev/null +++ b/src/main/resources/templates/user/register.html @@ -0,0 +1,184 @@ + + + + + + 注册 + + + + + +
+
创建账号
+
填写以下信息完成注册
+
+ +
+ +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + + + + + \ No newline at end of file diff --git a/target/classes/application-dev.yml b/target/classes/application-dev.yml new file mode 100644 index 0000000..c312981 --- /dev/null +++ b/target/classes/application-dev.yml @@ -0,0 +1,5 @@ +spring: + datasource: + url: jdbc:mysql://localhost:3306/nl_shop?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai + username: root + password: root diff --git a/target/classes/application.yml b/target/classes/application.yml new file mode 100644 index 0000000..d1cd230 --- /dev/null +++ b/target/classes/application.yml @@ -0,0 +1,47 @@ +server: + port: 14001 + +spring: + application: + name: nl-shop + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://127.0.0.1:3306/z_wb_nc_shop?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true + username: root + password: root + redis: + host: 127.0.0.1 + port: 6379 + password: redis_pBjaRs + timeout: 3000ms + lettuce: + pool: + max-active: 8 + max-idle: 8 + min-idle: 0 + servlet: + multipart: + enabled: true + max-file-size: 10MB + max-request-size: 10MB + thymeleaf: + prefix: classpath:/templates/ + suffix: .html + mode: HTML + encoding: UTF-8 + cache: false + +mybatis: + mapper-locations: classpath:mapper/*.xml + type-aliases-package: com.nlshop.entity + configuration: + map-underscore-to-camel-case: true + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + +pagehelper: + helper-dialect: mysql + reasonable: true + support-methods-arguments: true + +file: + upload-path: ${user.dir}/src/main/resources/static/upload diff --git a/target/classes/com/nlshop/NlShopApplication.class b/target/classes/com/nlshop/NlShopApplication.class new file mode 100644 index 0000000..5613cd1 Binary files /dev/null and b/target/classes/com/nlshop/NlShopApplication.class differ diff --git a/target/classes/com/nlshop/config/StartupUrlPrinter.class b/target/classes/com/nlshop/config/StartupUrlPrinter.class new file mode 100644 index 0000000..3d8b112 Binary files /dev/null and b/target/classes/com/nlshop/config/StartupUrlPrinter.class differ diff --git a/target/classes/com/nlshop/config/WebMvcConfig.class b/target/classes/com/nlshop/config/WebMvcConfig.class new file mode 100644 index 0000000..527b067 Binary files /dev/null and b/target/classes/com/nlshop/config/WebMvcConfig.class differ diff --git a/target/classes/com/nlshop/controller/merchant/AnnouncementController.class b/target/classes/com/nlshop/controller/merchant/AnnouncementController.class new file mode 100644 index 0000000..569efb8 Binary files /dev/null and b/target/classes/com/nlshop/controller/merchant/AnnouncementController.class differ diff --git a/target/classes/com/nlshop/controller/merchant/InventoryController.class b/target/classes/com/nlshop/controller/merchant/InventoryController.class new file mode 100644 index 0000000..6769aa9 Binary files /dev/null and b/target/classes/com/nlshop/controller/merchant/InventoryController.class differ diff --git a/target/classes/com/nlshop/controller/merchant/MerchantController.class b/target/classes/com/nlshop/controller/merchant/MerchantController.class new file mode 100644 index 0000000..4ede4c5 Binary files /dev/null and b/target/classes/com/nlshop/controller/merchant/MerchantController.class differ diff --git a/target/classes/com/nlshop/controller/merchant/MessageController.class b/target/classes/com/nlshop/controller/merchant/MessageController.class new file mode 100644 index 0000000..885c2a4 Binary files /dev/null and b/target/classes/com/nlshop/controller/merchant/MessageController.class differ diff --git a/target/classes/com/nlshop/controller/merchant/OrderManageController.class b/target/classes/com/nlshop/controller/merchant/OrderManageController.class new file mode 100644 index 0000000..acfbb2e Binary files /dev/null and b/target/classes/com/nlshop/controller/merchant/OrderManageController.class differ diff --git a/target/classes/com/nlshop/controller/merchant/ProductManageController.class b/target/classes/com/nlshop/controller/merchant/ProductManageController.class new file mode 100644 index 0000000..5c2073e Binary files /dev/null and b/target/classes/com/nlshop/controller/merchant/ProductManageController.class differ diff --git a/target/classes/com/nlshop/controller/merchant/ReviewController.class b/target/classes/com/nlshop/controller/merchant/ReviewController.class new file mode 100644 index 0000000..c348ade Binary files /dev/null and b/target/classes/com/nlshop/controller/merchant/ReviewController.class differ diff --git a/target/classes/com/nlshop/controller/merchant/StatisticsController.class b/target/classes/com/nlshop/controller/merchant/StatisticsController.class new file mode 100644 index 0000000..1189d0d Binary files /dev/null and b/target/classes/com/nlshop/controller/merchant/StatisticsController.class differ diff --git a/target/classes/com/nlshop/controller/merchant/UserManageController.class b/target/classes/com/nlshop/controller/merchant/UserManageController.class new file mode 100644 index 0000000..4be2a32 Binary files /dev/null and b/target/classes/com/nlshop/controller/merchant/UserManageController.class differ diff --git a/target/classes/com/nlshop/controller/user/AnnouncementController.class b/target/classes/com/nlshop/controller/user/AnnouncementController.class new file mode 100644 index 0000000..0036f50 Binary files /dev/null and b/target/classes/com/nlshop/controller/user/AnnouncementController.class differ diff --git a/target/classes/com/nlshop/controller/user/CartController.class b/target/classes/com/nlshop/controller/user/CartController.class new file mode 100644 index 0000000..8db4773 Binary files /dev/null and b/target/classes/com/nlshop/controller/user/CartController.class differ diff --git a/target/classes/com/nlshop/controller/user/MessageController.class b/target/classes/com/nlshop/controller/user/MessageController.class new file mode 100644 index 0000000..b43930a Binary files /dev/null and b/target/classes/com/nlshop/controller/user/MessageController.class differ diff --git a/target/classes/com/nlshop/controller/user/OrderController.class b/target/classes/com/nlshop/controller/user/OrderController.class new file mode 100644 index 0000000..4990e23 Binary files /dev/null and b/target/classes/com/nlshop/controller/user/OrderController.class differ diff --git a/target/classes/com/nlshop/controller/user/ProductController.class b/target/classes/com/nlshop/controller/user/ProductController.class new file mode 100644 index 0000000..e58cbd9 Binary files /dev/null and b/target/classes/com/nlshop/controller/user/ProductController.class differ diff --git a/target/classes/com/nlshop/controller/user/RecommendController.class b/target/classes/com/nlshop/controller/user/RecommendController.class new file mode 100644 index 0000000..63e0226 Binary files /dev/null and b/target/classes/com/nlshop/controller/user/RecommendController.class differ diff --git a/target/classes/com/nlshop/controller/user/ReviewController.class b/target/classes/com/nlshop/controller/user/ReviewController.class new file mode 100644 index 0000000..e6332c6 Binary files /dev/null and b/target/classes/com/nlshop/controller/user/ReviewController.class differ diff --git a/target/classes/com/nlshop/controller/user/UserController.class b/target/classes/com/nlshop/controller/user/UserController.class new file mode 100644 index 0000000..c499385 Binary files /dev/null and b/target/classes/com/nlshop/controller/user/UserController.class differ diff --git a/target/classes/com/nlshop/dto/request/CartAddRequest.class b/target/classes/com/nlshop/dto/request/CartAddRequest.class new file mode 100644 index 0000000..9e43691 Binary files /dev/null and b/target/classes/com/nlshop/dto/request/CartAddRequest.class differ diff --git a/target/classes/com/nlshop/dto/request/LoginRequest.class b/target/classes/com/nlshop/dto/request/LoginRequest.class new file mode 100644 index 0000000..f4baf1d Binary files /dev/null and b/target/classes/com/nlshop/dto/request/LoginRequest.class differ diff --git a/target/classes/com/nlshop/dto/request/MerchantRegisterRequest.class b/target/classes/com/nlshop/dto/request/MerchantRegisterRequest.class new file mode 100644 index 0000000..3d70621 Binary files /dev/null and b/target/classes/com/nlshop/dto/request/MerchantRegisterRequest.class differ diff --git a/target/classes/com/nlshop/dto/request/OrderCreateRequest.class b/target/classes/com/nlshop/dto/request/OrderCreateRequest.class new file mode 100644 index 0000000..05236df Binary files /dev/null and b/target/classes/com/nlshop/dto/request/OrderCreateRequest.class differ diff --git a/target/classes/com/nlshop/dto/request/PaymentRequest.class b/target/classes/com/nlshop/dto/request/PaymentRequest.class new file mode 100644 index 0000000..3afdb17 Binary files /dev/null and b/target/classes/com/nlshop/dto/request/PaymentRequest.class differ diff --git a/target/classes/com/nlshop/dto/request/UserRegisterRequest.class b/target/classes/com/nlshop/dto/request/UserRegisterRequest.class new file mode 100644 index 0000000..20a24fa Binary files /dev/null and b/target/classes/com/nlshop/dto/request/UserRegisterRequest.class differ diff --git a/target/classes/com/nlshop/dto/response/ApiResponse.class b/target/classes/com/nlshop/dto/response/ApiResponse.class new file mode 100644 index 0000000..74948d0 Binary files /dev/null and b/target/classes/com/nlshop/dto/response/ApiResponse.class differ diff --git a/target/classes/com/nlshop/dto/response/LoginResponse.class b/target/classes/com/nlshop/dto/response/LoginResponse.class new file mode 100644 index 0000000..9c3d737 Binary files /dev/null and b/target/classes/com/nlshop/dto/response/LoginResponse.class differ diff --git a/target/classes/com/nlshop/dto/response/OrderDetailResponse.class b/target/classes/com/nlshop/dto/response/OrderDetailResponse.class new file mode 100644 index 0000000..b4386ac Binary files /dev/null and b/target/classes/com/nlshop/dto/response/OrderDetailResponse.class differ diff --git a/target/classes/com/nlshop/dto/response/PageResponse.class b/target/classes/com/nlshop/dto/response/PageResponse.class new file mode 100644 index 0000000..d33aa45 Binary files /dev/null and b/target/classes/com/nlshop/dto/response/PageResponse.class differ diff --git a/target/classes/com/nlshop/entity/Announcement.class b/target/classes/com/nlshop/entity/Announcement.class new file mode 100644 index 0000000..540d466 Binary files /dev/null and b/target/classes/com/nlshop/entity/Announcement.class differ diff --git a/target/classes/com/nlshop/entity/CartItem.class b/target/classes/com/nlshop/entity/CartItem.class new file mode 100644 index 0000000..b33ebec Binary files /dev/null and b/target/classes/com/nlshop/entity/CartItem.class differ diff --git a/target/classes/com/nlshop/entity/Inventory.class b/target/classes/com/nlshop/entity/Inventory.class new file mode 100644 index 0000000..4f11649 Binary files /dev/null and b/target/classes/com/nlshop/entity/Inventory.class differ diff --git a/target/classes/com/nlshop/entity/InventoryLog.class b/target/classes/com/nlshop/entity/InventoryLog.class new file mode 100644 index 0000000..4d6d3e0 Binary files /dev/null and b/target/classes/com/nlshop/entity/InventoryLog.class differ diff --git a/target/classes/com/nlshop/entity/Merchant.class b/target/classes/com/nlshop/entity/Merchant.class new file mode 100644 index 0000000..6126be5 Binary files /dev/null and b/target/classes/com/nlshop/entity/Merchant.class differ diff --git a/target/classes/com/nlshop/entity/Message.class b/target/classes/com/nlshop/entity/Message.class new file mode 100644 index 0000000..5401013 Binary files /dev/null and b/target/classes/com/nlshop/entity/Message.class differ diff --git a/target/classes/com/nlshop/entity/Order.class b/target/classes/com/nlshop/entity/Order.class new file mode 100644 index 0000000..581e889 Binary files /dev/null and b/target/classes/com/nlshop/entity/Order.class differ diff --git a/target/classes/com/nlshop/entity/OrderItem.class b/target/classes/com/nlshop/entity/OrderItem.class new file mode 100644 index 0000000..44d75a6 Binary files /dev/null and b/target/classes/com/nlshop/entity/OrderItem.class differ diff --git a/target/classes/com/nlshop/entity/OrderTrack.class b/target/classes/com/nlshop/entity/OrderTrack.class new file mode 100644 index 0000000..866bc66 Binary files /dev/null and b/target/classes/com/nlshop/entity/OrderTrack.class differ diff --git a/target/classes/com/nlshop/entity/Product.class b/target/classes/com/nlshop/entity/Product.class new file mode 100644 index 0000000..e4378f5 Binary files /dev/null and b/target/classes/com/nlshop/entity/Product.class differ diff --git a/target/classes/com/nlshop/entity/ProductCustom.class b/target/classes/com/nlshop/entity/ProductCustom.class new file mode 100644 index 0000000..951ee8a Binary files /dev/null and b/target/classes/com/nlshop/entity/ProductCustom.class differ diff --git a/target/classes/com/nlshop/entity/ProductSpec.class b/target/classes/com/nlshop/entity/ProductSpec.class new file mode 100644 index 0000000..b3a48ec Binary files /dev/null and b/target/classes/com/nlshop/entity/ProductSpec.class differ diff --git a/target/classes/com/nlshop/entity/ProductTopping.class b/target/classes/com/nlshop/entity/ProductTopping.class new file mode 100644 index 0000000..c85312e Binary files /dev/null and b/target/classes/com/nlshop/entity/ProductTopping.class differ diff --git a/target/classes/com/nlshop/entity/Review.class b/target/classes/com/nlshop/entity/Review.class new file mode 100644 index 0000000..b047375 Binary files /dev/null and b/target/classes/com/nlshop/entity/Review.class differ diff --git a/target/classes/com/nlshop/entity/User.class b/target/classes/com/nlshop/entity/User.class new file mode 100644 index 0000000..51b4c0f Binary files /dev/null and b/target/classes/com/nlshop/entity/User.class differ diff --git a/target/classes/com/nlshop/entity/UserAddress.class b/target/classes/com/nlshop/entity/UserAddress.class new file mode 100644 index 0000000..5f5658d Binary files /dev/null and b/target/classes/com/nlshop/entity/UserAddress.class differ diff --git a/target/classes/com/nlshop/entity/UserBehavior.class b/target/classes/com/nlshop/entity/UserBehavior.class new file mode 100644 index 0000000..419d9ca Binary files /dev/null and b/target/classes/com/nlshop/entity/UserBehavior.class differ diff --git a/target/classes/com/nlshop/entity/UserSimilarity.class b/target/classes/com/nlshop/entity/UserSimilarity.class new file mode 100644 index 0000000..1f6de20 Binary files /dev/null and b/target/classes/com/nlshop/entity/UserSimilarity.class differ diff --git a/target/classes/com/nlshop/exception/BusinessException.class b/target/classes/com/nlshop/exception/BusinessException.class new file mode 100644 index 0000000..4c2acf9 Binary files /dev/null and b/target/classes/com/nlshop/exception/BusinessException.class differ diff --git a/target/classes/com/nlshop/exception/GlobalExceptionHandler.class b/target/classes/com/nlshop/exception/GlobalExceptionHandler.class new file mode 100644 index 0000000..30f0c93 Binary files /dev/null and b/target/classes/com/nlshop/exception/GlobalExceptionHandler.class differ diff --git a/target/classes/com/nlshop/interceptor/AuthInterceptor.class b/target/classes/com/nlshop/interceptor/AuthInterceptor.class new file mode 100644 index 0000000..26db7c4 Binary files /dev/null and b/target/classes/com/nlshop/interceptor/AuthInterceptor.class differ diff --git a/target/classes/com/nlshop/mapper/AnnouncementMapper.class b/target/classes/com/nlshop/mapper/AnnouncementMapper.class new file mode 100644 index 0000000..7a80908 Binary files /dev/null and b/target/classes/com/nlshop/mapper/AnnouncementMapper.class differ diff --git a/target/classes/com/nlshop/mapper/CartMapper.class b/target/classes/com/nlshop/mapper/CartMapper.class new file mode 100644 index 0000000..d0b3b93 Binary files /dev/null and b/target/classes/com/nlshop/mapper/CartMapper.class differ diff --git a/target/classes/com/nlshop/mapper/InventoryLogMapper.class b/target/classes/com/nlshop/mapper/InventoryLogMapper.class new file mode 100644 index 0000000..6dbd1da Binary files /dev/null and b/target/classes/com/nlshop/mapper/InventoryLogMapper.class differ diff --git a/target/classes/com/nlshop/mapper/InventoryMapper.class b/target/classes/com/nlshop/mapper/InventoryMapper.class new file mode 100644 index 0000000..c441a87 Binary files /dev/null and b/target/classes/com/nlshop/mapper/InventoryMapper.class differ diff --git a/target/classes/com/nlshop/mapper/MerchantMapper.class b/target/classes/com/nlshop/mapper/MerchantMapper.class new file mode 100644 index 0000000..f1148ca Binary files /dev/null and b/target/classes/com/nlshop/mapper/MerchantMapper.class differ diff --git a/target/classes/com/nlshop/mapper/MessageMapper.class b/target/classes/com/nlshop/mapper/MessageMapper.class new file mode 100644 index 0000000..0adfdae Binary files /dev/null and b/target/classes/com/nlshop/mapper/MessageMapper.class differ diff --git a/target/classes/com/nlshop/mapper/OrderItemMapper.class b/target/classes/com/nlshop/mapper/OrderItemMapper.class new file mode 100644 index 0000000..3fc0609 Binary files /dev/null and b/target/classes/com/nlshop/mapper/OrderItemMapper.class differ diff --git a/target/classes/com/nlshop/mapper/OrderMapper.class b/target/classes/com/nlshop/mapper/OrderMapper.class new file mode 100644 index 0000000..3d991c0 Binary files /dev/null and b/target/classes/com/nlshop/mapper/OrderMapper.class differ diff --git a/target/classes/com/nlshop/mapper/OrderTrackMapper.class b/target/classes/com/nlshop/mapper/OrderTrackMapper.class new file mode 100644 index 0000000..a740bf1 Binary files /dev/null and b/target/classes/com/nlshop/mapper/OrderTrackMapper.class differ diff --git a/target/classes/com/nlshop/mapper/ProductCustomMapper.class b/target/classes/com/nlshop/mapper/ProductCustomMapper.class new file mode 100644 index 0000000..d3795ba Binary files /dev/null and b/target/classes/com/nlshop/mapper/ProductCustomMapper.class differ diff --git a/target/classes/com/nlshop/mapper/ProductMapper.class b/target/classes/com/nlshop/mapper/ProductMapper.class new file mode 100644 index 0000000..aa1a175 Binary files /dev/null and b/target/classes/com/nlshop/mapper/ProductMapper.class differ diff --git a/target/classes/com/nlshop/mapper/ProductSpecMapper.class b/target/classes/com/nlshop/mapper/ProductSpecMapper.class new file mode 100644 index 0000000..641a918 Binary files /dev/null and b/target/classes/com/nlshop/mapper/ProductSpecMapper.class differ diff --git a/target/classes/com/nlshop/mapper/ProductToppingMapper.class b/target/classes/com/nlshop/mapper/ProductToppingMapper.class new file mode 100644 index 0000000..fb39540 Binary files /dev/null and b/target/classes/com/nlshop/mapper/ProductToppingMapper.class differ diff --git a/target/classes/com/nlshop/mapper/ReviewMapper.class b/target/classes/com/nlshop/mapper/ReviewMapper.class new file mode 100644 index 0000000..98793db Binary files /dev/null and b/target/classes/com/nlshop/mapper/ReviewMapper.class differ diff --git a/target/classes/com/nlshop/mapper/UserAddressMapper.class b/target/classes/com/nlshop/mapper/UserAddressMapper.class new file mode 100644 index 0000000..847b665 Binary files /dev/null and b/target/classes/com/nlshop/mapper/UserAddressMapper.class differ diff --git a/target/classes/com/nlshop/mapper/UserBehaviorMapper.class b/target/classes/com/nlshop/mapper/UserBehaviorMapper.class new file mode 100644 index 0000000..3f5399d Binary files /dev/null and b/target/classes/com/nlshop/mapper/UserBehaviorMapper.class differ diff --git a/target/classes/com/nlshop/mapper/UserMapper.class b/target/classes/com/nlshop/mapper/UserMapper.class new file mode 100644 index 0000000..ca9a259 Binary files /dev/null and b/target/classes/com/nlshop/mapper/UserMapper.class differ diff --git a/target/classes/com/nlshop/mapper/UserSimilarityMapper.class b/target/classes/com/nlshop/mapper/UserSimilarityMapper.class new file mode 100644 index 0000000..0ba6c27 Binary files /dev/null and b/target/classes/com/nlshop/mapper/UserSimilarityMapper.class differ diff --git a/target/classes/com/nlshop/service/algorithm/CollaborativeFilteringService.class b/target/classes/com/nlshop/service/algorithm/CollaborativeFilteringService.class new file mode 100644 index 0000000..c25d551 Binary files /dev/null and b/target/classes/com/nlshop/service/algorithm/CollaborativeFilteringService.class differ diff --git a/target/classes/com/nlshop/service/merchant/AnnouncementService.class b/target/classes/com/nlshop/service/merchant/AnnouncementService.class new file mode 100644 index 0000000..e275673 Binary files /dev/null and b/target/classes/com/nlshop/service/merchant/AnnouncementService.class differ diff --git a/target/classes/com/nlshop/service/merchant/InventoryService.class b/target/classes/com/nlshop/service/merchant/InventoryService.class new file mode 100644 index 0000000..77b8fa9 Binary files /dev/null and b/target/classes/com/nlshop/service/merchant/InventoryService.class differ diff --git a/target/classes/com/nlshop/service/merchant/MerchantService.class b/target/classes/com/nlshop/service/merchant/MerchantService.class new file mode 100644 index 0000000..6486972 Binary files /dev/null and b/target/classes/com/nlshop/service/merchant/MerchantService.class differ diff --git a/target/classes/com/nlshop/service/merchant/MessageService.class b/target/classes/com/nlshop/service/merchant/MessageService.class new file mode 100644 index 0000000..8135bb3 Binary files /dev/null and b/target/classes/com/nlshop/service/merchant/MessageService.class differ diff --git a/target/classes/com/nlshop/service/merchant/OrderManageService.class b/target/classes/com/nlshop/service/merchant/OrderManageService.class new file mode 100644 index 0000000..396f063 Binary files /dev/null and b/target/classes/com/nlshop/service/merchant/OrderManageService.class differ diff --git a/target/classes/com/nlshop/service/merchant/ProductManageService.class b/target/classes/com/nlshop/service/merchant/ProductManageService.class new file mode 100644 index 0000000..b6cf95d Binary files /dev/null and b/target/classes/com/nlshop/service/merchant/ProductManageService.class differ diff --git a/target/classes/com/nlshop/service/merchant/ReviewService.class b/target/classes/com/nlshop/service/merchant/ReviewService.class new file mode 100644 index 0000000..09579d8 Binary files /dev/null and b/target/classes/com/nlshop/service/merchant/ReviewService.class differ diff --git a/target/classes/com/nlshop/service/merchant/StatisticsService.class b/target/classes/com/nlshop/service/merchant/StatisticsService.class new file mode 100644 index 0000000..498ae30 Binary files /dev/null and b/target/classes/com/nlshop/service/merchant/StatisticsService.class differ diff --git a/target/classes/com/nlshop/service/merchant/UserManageService.class b/target/classes/com/nlshop/service/merchant/UserManageService.class new file mode 100644 index 0000000..5f9555b Binary files /dev/null and b/target/classes/com/nlshop/service/merchant/UserManageService.class differ diff --git a/target/classes/com/nlshop/service/user/CartService.class b/target/classes/com/nlshop/service/user/CartService.class new file mode 100644 index 0000000..2c6b541 Binary files /dev/null and b/target/classes/com/nlshop/service/user/CartService.class differ diff --git a/target/classes/com/nlshop/service/user/MessageService.class b/target/classes/com/nlshop/service/user/MessageService.class new file mode 100644 index 0000000..4a0fb56 Binary files /dev/null and b/target/classes/com/nlshop/service/user/MessageService.class differ diff --git a/target/classes/com/nlshop/service/user/OrderService$1.class b/target/classes/com/nlshop/service/user/OrderService$1.class new file mode 100644 index 0000000..8960708 Binary files /dev/null and b/target/classes/com/nlshop/service/user/OrderService$1.class differ diff --git a/target/classes/com/nlshop/service/user/OrderService$2.class b/target/classes/com/nlshop/service/user/OrderService$2.class new file mode 100644 index 0000000..52943d8 Binary files /dev/null and b/target/classes/com/nlshop/service/user/OrderService$2.class differ diff --git a/target/classes/com/nlshop/service/user/OrderService.class b/target/classes/com/nlshop/service/user/OrderService.class new file mode 100644 index 0000000..161a808 Binary files /dev/null and b/target/classes/com/nlshop/service/user/OrderService.class differ diff --git a/target/classes/com/nlshop/service/user/ProductService.class b/target/classes/com/nlshop/service/user/ProductService.class new file mode 100644 index 0000000..dc0a67f Binary files /dev/null and b/target/classes/com/nlshop/service/user/ProductService.class differ diff --git a/target/classes/com/nlshop/service/user/RecommendService.class b/target/classes/com/nlshop/service/user/RecommendService.class new file mode 100644 index 0000000..c12606e Binary files /dev/null and b/target/classes/com/nlshop/service/user/RecommendService.class differ diff --git a/target/classes/com/nlshop/service/user/UserService.class b/target/classes/com/nlshop/service/user/UserService.class new file mode 100644 index 0000000..64cc702 Binary files /dev/null and b/target/classes/com/nlshop/service/user/UserService.class differ diff --git a/target/classes/com/nlshop/util/FileUploadUtil.class b/target/classes/com/nlshop/util/FileUploadUtil.class new file mode 100644 index 0000000..a4bd0c2 Binary files /dev/null and b/target/classes/com/nlshop/util/FileUploadUtil.class differ diff --git a/target/classes/com/nlshop/util/OrderNoUtil.class b/target/classes/com/nlshop/util/OrderNoUtil.class new file mode 100644 index 0000000..a61b5c1 Binary files /dev/null and b/target/classes/com/nlshop/util/OrderNoUtil.class differ diff --git a/target/classes/com/nlshop/util/PasswordUtil.class b/target/classes/com/nlshop/util/PasswordUtil.class new file mode 100644 index 0000000..b45c180 Binary files /dev/null and b/target/classes/com/nlshop/util/PasswordUtil.class differ diff --git a/target/classes/db/schema.sql b/target/classes/db/schema.sql new file mode 100644 index 0000000..4e3a445 --- /dev/null +++ b/target/classes/db/schema.sql @@ -0,0 +1,248 @@ +-- 奶茶店管理系统数据库表结构 + +-- 用户表 +CREATE TABLE IF NOT EXISTS `user` ( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT, + `username` VARCHAR(50) UNIQUE NOT NULL COMMENT '用户名', + `password` VARCHAR(255) NOT NULL COMMENT '密码(加密)', + `name` VARCHAR(50) COMMENT '姓名', + `gender` TINYINT COMMENT '性别:0-女,1-男', + `phone` VARCHAR(20) COMMENT '手机号', + `email` VARCHAR(100) COMMENT '邮箱', + `avatar` VARCHAR(255) COMMENT '头像路径', + `address` VARCHAR(255) COMMENT '默认地址', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; + +-- 用户地址表 +CREATE TABLE IF NOT EXISTS `user_address` ( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT, + `user_id` BIGINT NOT NULL COMMENT '用户ID', + `address` VARCHAR(255) NOT NULL COMMENT '详细地址', + `contact_name` VARCHAR(50) COMMENT '联系人姓名', + `contact_phone` VARCHAR(20) COMMENT '联系人电话', + `is_default` TINYINT DEFAULT 0 COMMENT '是否默认:0-否,1-是', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户地址表'; + +-- 商家表 +CREATE TABLE IF NOT EXISTS `merchant` ( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT, + `username` VARCHAR(50) UNIQUE NOT NULL COMMENT '用户名', + `password` VARCHAR(255) NOT NULL COMMENT '密码(加密)', + `store_name` VARCHAR(100) NOT NULL COMMENT '门店名称', + `address` VARCHAR(255) COMMENT '门店地址', + `phone` VARCHAR(20) COMMENT '联系电话', + `email` VARCHAR(100) COMMENT '邮箱', + `manager_name` VARCHAR(50) COMMENT '负责人姓名', + `avatar` VARCHAR(255) COMMENT '头像路径', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, + `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商家表'; + +-- 商品表 +CREATE TABLE IF NOT EXISTS `product` ( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT, + `merchant_id` BIGINT NOT NULL COMMENT '商家ID', + `name` VARCHAR(100) NOT NULL COMMENT '商品名称', + `category` VARCHAR(50) COMMENT '分类', + `description` TEXT COMMENT '商品描述', + `base_price` DECIMAL(10,2) NOT NULL COMMENT '基础价格', + `image` VARCHAR(255) COMMENT '商品图片', + `status` TINYINT DEFAULT 1 COMMENT '状态:0-下架,1-上架', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`merchant_id`) REFERENCES `merchant`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表'; + +-- 商品规格表 +CREATE TABLE IF NOT EXISTS `product_spec` ( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT, + `product_id` BIGINT NOT NULL COMMENT '商品ID', + `spec_name` VARCHAR(50) NOT NULL COMMENT '规格名称(大杯/中杯/小杯)', + `price_adjust` DECIMAL(10,2) DEFAULT 0 COMMENT '价格调整(相对于基础价格)', + FOREIGN KEY (`product_id`) REFERENCES `product`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品规格表'; + +-- 配料表 +CREATE TABLE IF NOT EXISTS `product_topping` ( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT, + `product_id` BIGINT NOT NULL COMMENT '商品ID', + `topping_name` VARCHAR(50) NOT NULL COMMENT '配料名称', + `price` DECIMAL(10,2) DEFAULT 0 COMMENT '配料价格', + FOREIGN KEY (`product_id`) REFERENCES `product`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='配料表'; + +-- 商品定制选项表 +CREATE TABLE IF NOT EXISTS `product_custom` ( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT, + `product_id` BIGINT NOT NULL COMMENT '商品ID', + `option_type` VARCHAR(20) NOT NULL COMMENT '选项类型:sweetness-甜度,ice-冰度', + `option_value` VARCHAR(50) NOT NULL COMMENT '选项值', + `price_adjust` DECIMAL(10,2) DEFAULT 0 COMMENT '价格调整', + FOREIGN KEY (`product_id`) REFERENCES `product`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品定制选项表'; + +-- 购物车表 +CREATE TABLE IF NOT EXISTS `cart` ( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT, + `user_id` BIGINT NOT NULL COMMENT '用户ID', + `product_id` BIGINT NOT NULL COMMENT '商品ID', + `spec_id` BIGINT COMMENT '规格ID', + `quantity` INT DEFAULT 1 COMMENT '数量', + `custom_sweetness` VARCHAR(20) COMMENT '甜度定制', + `custom_ice` VARCHAR(20) COMMENT '冰度定制', + `toppings` TEXT COMMENT '配料JSON', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE CASCADE, + FOREIGN KEY (`product_id`) REFERENCES `product`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='购物车表'; + +-- 订单表 +CREATE TABLE IF NOT EXISTS `order` ( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT, + `order_no` VARCHAR(50) UNIQUE NOT NULL COMMENT '订单号', + `user_id` BIGINT NOT NULL COMMENT '用户ID', + `merchant_id` BIGINT NOT NULL COMMENT '商家ID', + `total_amount` DECIMAL(10,2) NOT NULL COMMENT '订单总金额', + `payment_method` VARCHAR(20) COMMENT '支付方式:wechat/alipay', + `payment_status` TINYINT DEFAULT 0 COMMENT '支付状态:0-未支付,1-已支付', + `order_status` VARCHAR(20) DEFAULT 'PENDING_PAY' COMMENT '订单状态:PENDING_PAY-待支付,MAKING-制作中,READY-待取餐,COMPLETED-已完成,CANCELLED-已取消', + `address` VARCHAR(255) NOT NULL COMMENT '收货地址', + `contact_phone` VARCHAR(20) NOT NULL COMMENT '联系电话', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `pay_time` DATETIME COMMENT '支付时间', + `complete_time` DATETIME COMMENT '完成时间', + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`), + FOREIGN KEY (`merchant_id`) REFERENCES `merchant`(`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表'; + +-- 订单明细表 +CREATE TABLE IF NOT EXISTS `order_item` ( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT, + `order_id` BIGINT NOT NULL COMMENT '订单ID', + `product_id` BIGINT NOT NULL COMMENT '商品ID', + `product_name` VARCHAR(100) NOT NULL COMMENT '商品名称', + `spec_name` VARCHAR(50) COMMENT '规格名称', + `quantity` INT NOT NULL COMMENT '数量', + `price` DECIMAL(10,2) NOT NULL COMMENT '单价', + `custom_sweetness` VARCHAR(20) COMMENT '甜度', + `custom_ice` VARCHAR(20) COMMENT '冰度', + `toppings` TEXT COMMENT '配料JSON', + `subtotal` DECIMAL(10,2) NOT NULL COMMENT '小计', + FOREIGN KEY (`order_id`) REFERENCES `order`(`id`) ON DELETE CASCADE, + FOREIGN KEY (`product_id`) REFERENCES `product`(`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单明细表'; + +-- 订单跟踪表 +CREATE TABLE IF NOT EXISTS `order_track` ( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT, + `order_id` BIGINT NOT NULL COMMENT '订单ID', + `status` VARCHAR(20) NOT NULL COMMENT '状态', + `description` VARCHAR(255) COMMENT '描述', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`order_id`) REFERENCES `order`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单跟踪表'; + +-- 公告表 +CREATE TABLE IF NOT EXISTS `announcement` ( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT, + `merchant_id` BIGINT NOT NULL COMMENT '商家ID', + `title` VARCHAR(200) NOT NULL COMMENT '标题', + `content` TEXT COMMENT '内容', + `type` VARCHAR(20) DEFAULT 'NOTICE' COMMENT '类型:ACTIVITY-活动,NOTICE-通知', + `status` TINYINT DEFAULT 1 COMMENT '状态:0-下架,1-发布', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`merchant_id`) REFERENCES `merchant`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='公告表'; + +-- 留言反馈表 +CREATE TABLE IF NOT EXISTS `message` ( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT, + `user_id` BIGINT NOT NULL COMMENT '用户ID', + `merchant_id` BIGINT NOT NULL COMMENT '商家ID', + `order_id` BIGINT COMMENT '订单ID', + `content` TEXT NOT NULL COMMENT '留言内容', + `reply` TEXT COMMENT '回复内容', + `status` TINYINT DEFAULT 0 COMMENT '状态:0-未回复,1-已回复', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, + `reply_time` DATETIME COMMENT '回复时间', + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`), + FOREIGN KEY (`merchant_id`) REFERENCES `merchant`(`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='留言反馈表'; + +-- 评价表 +CREATE TABLE IF NOT EXISTS `review` ( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT, + `user_id` BIGINT NOT NULL COMMENT '用户ID', + `order_id` BIGINT NOT NULL COMMENT '订单ID', + `product_id` BIGINT NOT NULL COMMENT '商品ID', + `rating` TINYINT NOT NULL COMMENT '评分:1-5', + `content` TEXT COMMENT '评价内容', + `reply` TEXT COMMENT '商家回复', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`), + FOREIGN KEY (`order_id`) REFERENCES `order`(`id`), + FOREIGN KEY (`product_id`) REFERENCES `product`(`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='评价表'; + +-- 库存表 +CREATE TABLE IF NOT EXISTS `inventory` ( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT, + `merchant_id` BIGINT NOT NULL COMMENT '商家ID', + `material_name` VARCHAR(100) NOT NULL COMMENT '原料名称', + `quantity` DECIMAL(10,2) NOT NULL COMMENT '数量', + `unit` VARCHAR(20) COMMENT '单位', + `min_threshold` DECIMAL(10,2) DEFAULT 0 COMMENT '最低库存阈值', + `last_in_time` DATETIME COMMENT '最后入库时间', + `last_out_time` DATETIME COMMENT '最后出库时间', + FOREIGN KEY (`merchant_id`) REFERENCES `merchant`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存表'; + +-- 库存操作日志表 +CREATE TABLE IF NOT EXISTS `inventory_log` ( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT, + `inventory_id` BIGINT NOT NULL COMMENT '库存ID', + `operation_type` VARCHAR(20) NOT NULL COMMENT '操作类型:IN-入库,OUT-出库', + `quantity` DECIMAL(10,2) NOT NULL COMMENT '操作数量', + `operator` VARCHAR(50) COMMENT '操作人', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`inventory_id`) REFERENCES `inventory`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存操作日志表'; + +-- 用户行为表(用于协同过滤) +CREATE TABLE IF NOT EXISTS `user_behavior` ( + `id` BIGINT PRIMARY KEY AUTO_INCREMENT, + `user_id` BIGINT NOT NULL COMMENT '用户ID', + `product_id` BIGINT NOT NULL COMMENT '商品ID', + `behavior_type` VARCHAR(20) NOT NULL COMMENT '行为类型:VIEW-浏览,PURCHASE-购买,RATING-评分', + `score` DECIMAL(5,2) DEFAULT 0 COMMENT '评分(0-5)', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE CASCADE, + FOREIGN KEY (`product_id`) REFERENCES `product`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户行为表'; + +-- 用户相似度缓存表(可选) +CREATE TABLE IF NOT EXISTS `user_similarity` ( + `user1_id` BIGINT NOT NULL COMMENT '用户1ID', + `user2_id` BIGINT NOT NULL COMMENT '用户2ID', + `similarity_score` DECIMAL(10,6) NOT NULL COMMENT '相似度分数', + `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`user1_id`, `user2_id`), + FOREIGN KEY (`user1_id`) REFERENCES `user`(`id`) ON DELETE CASCADE, + FOREIGN KEY (`user2_id`) REFERENCES `user`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户相似度缓存表'; + +-- 创建索引 +CREATE INDEX idx_user_username ON `user`(`username`); +CREATE INDEX idx_merchant_username ON `merchant`(`username`); +CREATE INDEX idx_product_merchant ON `product`(`merchant_id`); +CREATE INDEX idx_product_status ON `product`(`status`); +CREATE INDEX idx_cart_user ON `cart`(`user_id`); +CREATE INDEX idx_order_user ON `order`(`user_id`); +CREATE INDEX idx_order_merchant ON `order`(`merchant_id`); +CREATE INDEX idx_order_status ON `order`(`order_status`); +CREATE INDEX idx_order_no ON `order`(`order_no`); +CREATE INDEX idx_user_behavior_user ON `user_behavior`(`user_id`); +CREATE INDEX idx_user_behavior_product ON `user_behavior`(`product_id`); diff --git a/target/classes/mapper/AnnouncementMapper.xml b/target/classes/mapper/AnnouncementMapper.xml new file mode 100644 index 0000000..7beccba --- /dev/null +++ b/target/classes/mapper/AnnouncementMapper.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + INSERT INTO announcement (merchant_id, title, content, type, status) + VALUES (#{merchantId}, #{title}, #{content}, #{type}, #{status}) + + + + + + + + + + UPDATE announcement + + title = #{title}, + content = #{content}, + type = #{type}, + status = #{status}, + + WHERE id = #{id} + + + + DELETE FROM announcement WHERE id = #{id} + + diff --git a/target/classes/mapper/CartMapper.xml b/target/classes/mapper/CartMapper.xml new file mode 100644 index 0000000..5d0cd6b --- /dev/null +++ b/target/classes/mapper/CartMapper.xml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO cart (user_id, product_id, spec_id, quantity, custom_sweetness, custom_ice, toppings) + VALUES (#{userId}, #{productId}, #{specId}, #{quantity}, #{customSweetness}, #{customIce}, #{toppings}) + + + + + + + + + + UPDATE cart + + quantity = #{quantity}, + custom_sweetness = #{customSweetness}, + custom_ice = #{customIce}, + toppings = #{toppings}, + + WHERE id = #{id} + + + + UPDATE cart SET quantity = #{quantity} WHERE id = #{id} + + + + DELETE FROM cart WHERE id = #{id} + + + + DELETE FROM cart WHERE user_id = #{userId} + + + + DELETE FROM cart + WHERE id IN + + #{id} + + AND user_id = #{userId} + + diff --git a/target/classes/mapper/InventoryLogMapper.xml b/target/classes/mapper/InventoryLogMapper.xml new file mode 100644 index 0000000..7d8e843 --- /dev/null +++ b/target/classes/mapper/InventoryLogMapper.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + INSERT INTO inventory_log (inventory_id, operation_type, quantity, operator) + VALUES (#{inventoryId}, #{operationType}, #{quantity}, #{operator}) + + + + diff --git a/target/classes/mapper/InventoryMapper.xml b/target/classes/mapper/InventoryMapper.xml new file mode 100644 index 0000000..3b5442e --- /dev/null +++ b/target/classes/mapper/InventoryMapper.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + INSERT INTO inventory (merchant_id, material_name, quantity, unit, min_threshold) + VALUES (#{merchantId}, #{materialName}, #{quantity}, #{unit}, #{minThreshold}) + + + + + + + + + + + + UPDATE inventory + + quantity = #{quantity}, + unit = #{unit}, + min_threshold = #{minThreshold}, + last_in_time = #{lastInTime}, + last_out_time = #{lastOutTime}, + + WHERE id = #{id} + + + + DELETE FROM inventory WHERE id = #{id} + + diff --git a/target/classes/mapper/MerchantMapper.xml b/target/classes/mapper/MerchantMapper.xml new file mode 100644 index 0000000..c01565a --- /dev/null +++ b/target/classes/mapper/MerchantMapper.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + INSERT INTO merchant (username, password, store_name, address, phone, email, manager_name, avatar) + VALUES (#{username}, #{password}, #{storeName}, #{address}, #{phone}, #{email}, #{managerName}, #{avatar}) + + + + + + + + UPDATE merchant + + store_name = #{storeName}, + address = #{address}, + phone = #{phone}, + email = #{email}, + manager_name = #{managerName}, + avatar = #{avatar}, + + WHERE id = #{id} + + diff --git a/target/classes/mapper/MessageMapper.xml b/target/classes/mapper/MessageMapper.xml new file mode 100644 index 0000000..8c2a5f1 --- /dev/null +++ b/target/classes/mapper/MessageMapper.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + INSERT INTO message (user_id, merchant_id, order_id, content, status) + VALUES (#{userId}, #{merchantId}, #{orderId}, #{content}, #{status}) + + + + + + + + + + UPDATE message + + reply = #{reply}, + status = #{status}, + reply_time = #{replyTime}, + + WHERE id = #{id} + + diff --git a/target/classes/mapper/OrderItemMapper.xml b/target/classes/mapper/OrderItemMapper.xml new file mode 100644 index 0000000..bc83853 --- /dev/null +++ b/target/classes/mapper/OrderItemMapper.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + INSERT INTO order_item (order_id, product_id, product_name, spec_name, quantity, price, + custom_sweetness, custom_ice, toppings, subtotal) + VALUES (#{orderId}, #{productId}, #{productName}, #{specName}, #{quantity}, #{price}, + #{customSweetness}, #{customIce}, #{toppings}, #{subtotal}) + + + + + + diff --git a/target/classes/mapper/OrderMapper.xml b/target/classes/mapper/OrderMapper.xml new file mode 100644 index 0000000..a440055 --- /dev/null +++ b/target/classes/mapper/OrderMapper.xml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO `order` (order_no, user_id, merchant_id, total_amount, payment_method, + payment_status, order_status, address, contact_phone, pay_time, complete_time) + VALUES (#{orderNo}, #{userId}, #{merchantId}, #{totalAmount}, #{paymentMethod}, + #{paymentStatus}, #{orderStatus}, #{address}, #{contactPhone}, #{payTime}, #{completeTime}) + + + + + + + + + + + + UPDATE `order` + + total_amount = #{totalAmount}, + payment_method = #{paymentMethod}, + payment_status = #{paymentStatus}, + order_status = #{orderStatus}, + address = #{address}, + contact_phone = #{contactPhone}, + pay_time = #{payTime}, + complete_time = #{completeTime}, + + WHERE id = #{id} + + + + UPDATE `order` SET order_status = #{status} WHERE id = #{id} + + + + UPDATE `order` + SET payment_status = #{paymentStatus}, + payment_method = #{paymentMethod}, + pay_time = NOW() + WHERE id = #{id} + + + + diff --git a/target/classes/mapper/OrderTrackMapper.xml b/target/classes/mapper/OrderTrackMapper.xml new file mode 100644 index 0000000..6f371e3 --- /dev/null +++ b/target/classes/mapper/OrderTrackMapper.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + INSERT INTO order_track (order_id, status, description) + VALUES (#{orderId}, #{status}, #{description}) + + + + diff --git a/target/classes/mapper/ProductCustomMapper.xml b/target/classes/mapper/ProductCustomMapper.xml new file mode 100644 index 0000000..e166c67 --- /dev/null +++ b/target/classes/mapper/ProductCustomMapper.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + INSERT INTO product_custom (product_id, option_type, option_value, price_adjust) + VALUES (#{productId}, #{optionType}, #{optionValue}, #{priceAdjust}) + + + + + + + + UPDATE product_custom + SET option_type = #{optionType}, option_value = #{optionValue}, price_adjust = #{priceAdjust} + WHERE id = #{id} + + + + DELETE FROM product_custom WHERE id = #{id} + + + + DELETE FROM product_custom WHERE product_id = #{productId} + + diff --git a/target/classes/mapper/ProductMapper.xml b/target/classes/mapper/ProductMapper.xml new file mode 100644 index 0000000..cc78848 --- /dev/null +++ b/target/classes/mapper/ProductMapper.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + INSERT INTO product (merchant_id, name, category, description, base_price, image, status) + VALUES (#{merchantId}, #{name}, #{category}, #{description}, #{basePrice}, #{image}, #{status}) + + + + + + + + + + + + UPDATE product + + name = #{name}, + category = #{category}, + description = #{description}, + base_price = #{basePrice}, + image = #{image}, + status = #{status}, + + WHERE id = #{id} + + + + UPDATE product SET status = #{status} WHERE id = #{id} + + diff --git a/target/classes/mapper/ProductSpecMapper.xml b/target/classes/mapper/ProductSpecMapper.xml new file mode 100644 index 0000000..412e9ab --- /dev/null +++ b/target/classes/mapper/ProductSpecMapper.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + INSERT INTO product_spec (product_id, spec_name, price_adjust) + VALUES (#{productId}, #{specName}, #{priceAdjust}) + + + + + + + + UPDATE product_spec + SET spec_name = #{specName}, price_adjust = #{priceAdjust} + WHERE id = #{id} + + + + DELETE FROM product_spec WHERE id = #{id} + + + + DELETE FROM product_spec WHERE product_id = #{productId} + + diff --git a/target/classes/mapper/ProductToppingMapper.xml b/target/classes/mapper/ProductToppingMapper.xml new file mode 100644 index 0000000..eed4f8a --- /dev/null +++ b/target/classes/mapper/ProductToppingMapper.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + INSERT INTO product_topping (product_id, topping_name, price) + VALUES (#{productId}, #{toppingName}, #{price}) + + + + + + + + UPDATE product_topping + SET topping_name = #{toppingName}, price = #{price} + WHERE id = #{id} + + + + DELETE FROM product_topping WHERE id = #{id} + + + + DELETE FROM product_topping WHERE product_id = #{productId} + + diff --git a/target/classes/mapper/ReviewMapper.xml b/target/classes/mapper/ReviewMapper.xml new file mode 100644 index 0000000..e53b279 --- /dev/null +++ b/target/classes/mapper/ReviewMapper.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + INSERT INTO review (user_id, order_id, product_id, rating, content) + VALUES (#{userId}, #{orderId}, #{productId}, #{rating}, #{content}) + + + + + + + + + + + + + + UPDATE review + + reply = #{reply}, + + WHERE id = #{id} + + diff --git a/target/classes/mapper/UserAddressMapper.xml b/target/classes/mapper/UserAddressMapper.xml new file mode 100644 index 0000000..7ce2cd7 --- /dev/null +++ b/target/classes/mapper/UserAddressMapper.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + INSERT INTO user_address (user_id, address, contact_name, contact_phone, is_default) + VALUES (#{userId}, #{address}, #{contactName}, #{contactPhone}, #{isDefault}) + + + + + + + + + + UPDATE user_address + + address = #{address}, + contact_name = #{contactName}, + contact_phone = #{contactPhone}, + is_default = #{isDefault}, + + WHERE id = #{id} + + + + DELETE FROM user_address WHERE id = #{id} + + diff --git a/target/classes/mapper/UserBehaviorMapper.xml b/target/classes/mapper/UserBehaviorMapper.xml new file mode 100644 index 0000000..87a8fb4 --- /dev/null +++ b/target/classes/mapper/UserBehaviorMapper.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + INSERT INTO user_behavior (user_id, product_id, behavior_type, score) + VALUES (#{userId}, #{productId}, #{behaviorType}, #{score}) + + + + + + + + + + UPDATE user_behavior + SET score = #{score}, create_time = NOW() + WHERE id = #{id} + + + + + + + + diff --git a/target/classes/mapper/UserMapper.xml b/target/classes/mapper/UserMapper.xml new file mode 100644 index 0000000..516dd07 --- /dev/null +++ b/target/classes/mapper/UserMapper.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + INSERT INTO user (username, password, name, gender, phone, email, avatar, address) + VALUES (#{username}, #{password}, #{name}, #{gender}, #{phone}, #{email}, #{avatar}, #{address}) + + + + + + + + UPDATE user + + name = #{name}, + gender = #{gender}, + phone = #{phone}, + email = #{email}, + avatar = #{avatar}, + address = #{address}, + + WHERE id = #{id} + + + + UPDATE user SET password = #{password} WHERE id = #{id} + + + + diff --git a/target/classes/mapper/UserSimilarityMapper.xml b/target/classes/mapper/UserSimilarityMapper.xml new file mode 100644 index 0000000..104f24f --- /dev/null +++ b/target/classes/mapper/UserSimilarityMapper.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + INSERT INTO user_similarity (user1_id, user2_id, similarity_score) + VALUES (#{user1Id}, #{user2Id}, #{similarityScore}) + ON DUPLICATE KEY UPDATE + similarity_score = #{similarityScore}, update_time = NOW() + + + + + + diff --git a/target/classes/static/js/cookie-utils.js b/target/classes/static/js/cookie-utils.js new file mode 100644 index 0000000..8d12a57 --- /dev/null +++ b/target/classes/static/js/cookie-utils.js @@ -0,0 +1,70 @@ +// Cookie工具类 +const CookieUtils = { + /** + * 设置Cookie + * @param {string} name Cookie名称 + * @param {string} value Cookie值 + * @param {number} days 过期天数,默认7天 + */ + set(name, value, days = 7) { + const expires = new Date(); + expires.setTime(expires.getTime() + days * 24 * 60 * 60 * 1000); + document.cookie = `${name}=${encodeURIComponent(value)};expires=${expires.toUTCString()};path=/`; + }, + + /** + * 获取Cookie + * @param {string} name Cookie名称 + * @returns {string|null} Cookie值,不存在返回null + */ + get(name) { + const nameEQ = name + "="; + const ca = document.cookie.split(';'); + for (let i = 0; i < ca.length; i++) { + let c = ca[i]; + while (c.charAt(0) === ' ') c = c.substring(1, c.length); + if (c.indexOf(nameEQ) === 0) { + return decodeURIComponent(c.substring(nameEQ.length, c.length)); + } + } + return null; + }, + + /** + * 删除Cookie + * @param {string} name Cookie名称 + */ + remove(name) { + document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 UTC;path=/;`; + }, + + /** + * 获取JSON格式的Cookie + * @param {string} name Cookie名称 + * @returns {any|null} 解析后的JSON对象,不存在或解析失败返回null + */ + getJSON(name) { + const value = this.get(name); + if (!value) return null; + try { + return JSON.parse(value); + } catch (e) { + console.error(`Failed to parse cookie ${name}:`, e); + return null; + } + }, + + /** + * 设置JSON格式的Cookie + * @param {string} name Cookie名称 + * @param {any} value 要存储的对象 + * @param {number} days 过期天数,默认7天 + */ + setJSON(name, value, days = 7) { + try { + this.set(name, JSON.stringify(value), days); + } catch (e) { + console.error(`Failed to stringify cookie ${name}:`, e); + } + } +}; diff --git a/target/classes/static/js/custom-modal.js b/target/classes/static/js/custom-modal.js new file mode 100644 index 0000000..e71a063 --- /dev/null +++ b/target/classes/static/js/custom-modal.js @@ -0,0 +1,356 @@ +/** + * 自定义模态框组件 + * 符合项目UI设计规范,具有平滑过渡动画和响应式显示 + */ + +class CustomModal { + constructor(options = {}) { + this.id = options.id || 'customModal_' + Date.now(); + this.title = options.title || ''; + this.content = options.content || ''; + this.size = options.size || 'medium'; // small, medium, large + this.showClose = options.showClose !== false; + this.onClose = options.onClose || null; + this.onConfirm = options.onConfirm || null; + this.confirmText = options.confirmText || '确定'; + this.cancelText = options.cancelText || '取消'; + this.showFooter = options.showFooter !== false; + this.backdrop = options.backdrop !== false; + this.modal = null; + } + + show() { + // 创建模态框HTML + const modalHtml = this._createModalHtml(); + + // 移除已存在的模态框 + const existing = document.getElementById(this.id); + if (existing) { + existing.remove(); + } + + // 添加到body + document.body.insertAdjacentHTML('beforeend', modalHtml); + this.modal = document.getElementById(this.id); + this.backdropEl = document.getElementById(this.id + '_backdrop'); + + // 添加显示动画 + requestAnimationFrame(() => { + this.modal.classList.add('show'); + if (this.backdropEl) { + this.backdropEl.classList.add('show'); + } + document.body.style.overflow = 'hidden'; + }); + + // 绑定事件 + this._bindEvents(); + } + + hide() { + if (!this.modal) return; + + this.modal.classList.remove('show'); + if (this.backdropEl) { + this.backdropEl.classList.remove('show'); + } + document.body.style.overflow = ''; + + // 动画结束后移除 + setTimeout(() => { + if (this.modal && this.modal.parentNode) { + this.modal.remove(); + } + if (this.backdropEl && this.backdropEl.parentNode) { + this.backdropEl.remove(); + } + if (this.onClose) { + this.onClose(); + } + }, 300); + } + + _createModalHtml() { + const sizeClass = { + small: 'modal-sm', + medium: '', + large: 'modal-lg' + }[this.size]; + + const backdropHtml = this.backdrop ? + `
` : ''; + + return backdropHtml + ` +
+
+
+ ${this.title || this.showClose ? ` +
+ ${this.title ? `
${this.title}
` : ''} + ${this.showClose ? `` : ''} +
+ ` : ''} +
+ ${this.content} +
+ ${this.showFooter ? ` + + ` : ''} +
+
+
+ `; + } + + _bindEvents() { + // 关闭按钮 + const closeBtn = this.modal.querySelector('[data-dismiss="modal"]'); + if (closeBtn) { + closeBtn.addEventListener('click', () => this.hide()); + } + + // 确认按钮 + const confirmBtn = this.modal.querySelector('.custom-modal-btn-confirm'); + if (confirmBtn && this.onConfirm) { + confirmBtn.addEventListener('click', () => { + if (this.onConfirm() !== false) { + this.hide(); + } + }); + } + + // 点击背景关闭 + if (this.backdropEl) { + this.backdropEl.addEventListener('click', () => this.hide()); + } + + // 点击内容区域不关闭 + const content = this.modal.querySelector('.custom-modal-content'); + if (content) { + content.addEventListener('click', (e) => e.stopPropagation()); + } + } + + updateContent(content) { + const body = this.modal?.querySelector('.custom-modal-body'); + if (body) { + body.innerHTML = content; + } + } +} + +// CSS样式(通过JavaScript注入,或单独引入CSS文件) +const customModalStyles = ` + +`; + +// 注入样式 +if (!document.getElementById('custom-modal-styles')) { + document.head.insertAdjacentHTML('beforeend', customModalStyles); +} + +// 导出到全局 +window.CustomModal = CustomModal; + +// 便捷方法 +window.showCustomModal = function(options) { + const modal = new CustomModal(options); + modal.show(); + return modal; +}; + +window.showCustomConfirm = function(message, title = '确认操作', onConfirm = null) { + return new CustomModal({ + title: title, + content: `

${message}

`, + onConfirm: onConfirm, + showFooter: true + }); +}; diff --git a/target/classes/static/js/merchant-common.js b/target/classes/static/js/merchant-common.js new file mode 100644 index 0000000..c302084 --- /dev/null +++ b/target/classes/static/js/merchant-common.js @@ -0,0 +1,338 @@ +/** + * 商家后台通用脚本 + * 提供侧边栏、加载状态、交互反馈等功能 + */ + +// 侧边栏HTML模板 +function getSidebarHTML(activePage) { + const menuItems = [ + { href: '/merchant/dashboard.html', icon: 'bi-speedometer2', text: '首页', page: 'dashboard' }, + { href: '/merchant/products.html', icon: 'bi-box-seam', text: '商品管理', page: 'products' }, + { href: '/merchant/orders.html', icon: 'bi-receipt-cutoff', text: '订单管理', page: 'orders' }, + { href: '/merchant/inventory.html', icon: 'bi-archive', text: '库存管理', page: 'inventory' }, + { href: '/merchant/statistics.html', icon: 'bi-bar-chart', text: '数据统计', page: 'statistics' }, + { href: '/merchant/announcements.html', icon: 'bi-megaphone', text: '公告管理', page: 'announcements' }, + { href: '/merchant/messages.html', icon: 'bi-chat-dots', text: '留言管理', page: 'messages' }, + { href: '/merchant/reviews.html', icon: 'bi-star', text: '评价管理', page: 'reviews' }, + { href: '/merchant/users.html', icon: 'bi-people', text: '用户管理', page: 'users' } + ]; + + const menuHTML = menuItems.map(item => { + const activeClass = item.page === activePage ? 'active' : ''; + return ` + + `; + }).join(''); + + return ` + + + + `; +} + +// 侧边栏CSS样式 +const sidebarCSS = ` + .sidebar { + position: fixed; + top: 0; + left: 0; + width: 260px; + height: 100vh; + background: linear-gradient(180deg, #2c3e50 0%, #34495e 100%); + color: #fff; + z-index: 1050; + display: flex; + flex-direction: column; + transition: transform 0.3s ease; + box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1); + } + .sidebar-header { + padding: 1.5rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + display: flex; + justify-content: space-between; + align-items: center; + } + .sidebar-brand { + font-size: 1.25rem; + font-weight: 600; + } + .sidebar-nav { + flex: 1; + overflow-y: auto; + padding: 1rem 0; + } + .sidebar-nav .nav-link { + color: rgba(255, 255, 255, 0.8); + padding: 0.75rem 1.5rem; + transition: all 0.3s; + border-left: 3px solid transparent; + display: flex; + align-items: center; + text-decoration: none; + } + .sidebar-nav .nav-link:hover { + background-color: rgba(255, 255, 255, 0.1); + color: #fff; + border-left-color: #0d6efd; + } + .sidebar-nav .nav-link.active { + background-color: rgba(13, 110, 253, 0.2); + color: #fff; + border-left-color: #0d6efd; + font-weight: 600; + } + .sidebar-nav .nav-link i { + width: 24px; + font-size: 1.1rem; + margin-right: 0.75rem; + } + .sidebar-nav .nav-text { + flex: 1; + } + .sidebar-footer { + padding: 1rem; + border-top: 1px solid rgba(255, 255, 255, 0.1); + } + @media (max-width: 767.98px) { + .sidebar { + transform: translateX(-100%); + } + .sidebar.show { + transform: translateX(0); + } + .sidebar-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + z-index: 1049; + display: none; + transition: opacity 0.3s; + } + .sidebar-overlay.show { + display: block; + } + .top-navbar { + position: fixed; + top: 0; + left: 0; + right: 0; + height: 56px; + background: linear-gradient(180deg, #2c3e50 0%, #34495e 100%); + z-index: 1048; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + } + body { + padding-top: 56px; + } + } + @media (min-width: 768px) { + .main-content { + margin-left: 260px; + min-height: 100vh; + } + } + .sidebar-nav::-webkit-scrollbar { + width: 6px; + } + .sidebar-nav::-webkit-scrollbar-track { + background: rgba(255, 255, 255, 0.05); + } + .sidebar-nav::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.2); + border-radius: 3px; + } + .sidebar-nav::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.3); + } +`; + +// 初始化侧边栏 +function initSidebar(activePage) { + // 添加CSS样式 + if (!document.getElementById('merchant-sidebar-styles')) { + const style = document.createElement('style'); + style.id = 'merchant-sidebar-styles'; + style.textContent = sidebarCSS; + document.head.appendChild(style); + } + + // 替换导航栏 + const oldNav = document.querySelector('nav.navbar'); + if (oldNav) { + oldNav.outerHTML = getSidebarHTML(activePage); + + // 初始化交互 + const sidebar = document.getElementById('sidebar'); + const sidebarOverlay = document.getElementById('sidebarOverlay'); + const sidebarToggleBtn = document.getElementById('sidebarToggleBtn'); + const sidebarCloseBtn = document.getElementById('sidebarCloseBtn'); + + if (sidebarToggleBtn) { + sidebarToggleBtn.addEventListener('click', function() { + sidebar.classList.add('show'); + sidebarOverlay.classList.add('show'); + document.body.style.overflow = 'hidden'; + }); + } + + function closeSidebar() { + sidebar.classList.remove('show'); + sidebarOverlay.classList.remove('show'); + document.body.style.overflow = ''; + } + + if (sidebarCloseBtn) { + sidebarCloseBtn.addEventListener('click', closeSidebar); + } + + if (sidebarOverlay) { + sidebarOverlay.addEventListener('click', closeSidebar); + } + + const navLinks = document.querySelectorAll('.sidebar-nav .nav-link'); + navLinks.forEach(link => { + link.addEventListener('click', function() { + if (window.innerWidth < 768) { + closeSidebar(); + } + }); + }); + } + + // 包装主内容区域 + const container = document.querySelector('.container, .container-fluid'); + if (container && !container.closest('.main-content')) { + const mainContent = document.createElement('div'); + mainContent.className = 'main-content'; + container.parentNode.insertBefore(mainContent, container); + mainContent.appendChild(container); + } +} + +// 显示加载状态 +function showLoading(elementId, message = '加载中...') { + const element = document.getElementById(elementId); + if (element) { + element.innerHTML = ` +
+
+ ${message} +
+

${message}

+
+ `; + } +} + +// 显示成功提示 +function showSuccess(message, duration = 3000) { + const toast = document.createElement('div'); + toast.className = 'toast align-items-center text-white bg-success border-0'; + toast.setAttribute('role', 'alert'); + toast.innerHTML = ` +
+
${message}
+ +
+ `; + + const toastContainer = document.getElementById('toastContainer') || createToastContainer(); + toastContainer.appendChild(toast); + + const bsToast = new bootstrap.Toast(toast); + bsToast.show(); + + toast.addEventListener('hidden.bs.toast', () => toast.remove()); +} + +// 显示错误提示 +function showError(message, duration = 3000) { + const toast = document.createElement('div'); + toast.className = 'toast align-items-center text-white bg-danger border-0'; + toast.setAttribute('role', 'alert'); + toast.innerHTML = ` +
+
${message}
+ +
+ `; + + const toastContainer = document.getElementById('toastContainer') || createToastContainer(); + toastContainer.appendChild(toast); + + const bsToast = new bootstrap.Toast(toast); + bsToast.show(); + + toast.addEventListener('hidden.bs.toast', () => toast.remove()); +} + +// 创建Toast容器 +function createToastContainer() { + const container = document.createElement('div'); + container.id = 'toastContainer'; + container.className = 'toast-container position-fixed top-0 end-0 p-3'; + container.style.zIndex = '1060'; + document.body.appendChild(container); + return container; +} + +// 检测当前页面 +function detectCurrentPage() { + const path = window.location.pathname; + if (path.includes('products')) return 'products'; + if (path.includes('orders')) return 'orders'; + if (path.includes('inventory')) return 'inventory'; + if (path.includes('statistics')) return 'statistics'; + if (path.includes('announcements')) return 'announcements'; + if (path.includes('messages')) return 'messages'; + if (path.includes('reviews')) return 'reviews'; + if (path.includes('users')) return 'users'; + return 'dashboard'; +} + +// 自动初始化 +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', function() { + initSidebar(detectCurrentPage()); + }); +} else { + initSidebar(detectCurrentPage()); +} diff --git a/target/classes/static/js/merchant-sidebar.js b/target/classes/static/js/merchant-sidebar.js new file mode 100644 index 0000000..26871e6 --- /dev/null +++ b/target/classes/static/js/merchant-sidebar.js @@ -0,0 +1,297 @@ +/** + * 商家后台侧边栏通用脚本 + * 提供侧边栏的HTML结构和交互逻辑 + */ + +// 生成侧边栏HTML +function generateSidebarHTML(currentPage) { + const menuItems = [ + { href: '/merchant/dashboard.html', icon: 'bi-speedometer2', text: '首页', page: 'dashboard' }, + { href: '/merchant/products.html', icon: 'bi-box-seam', text: '商品管理', page: 'products' }, + { href: '/merchant/orders.html', icon: 'bi-receipt-cutoff', text: '订单管理', page: 'orders' }, + { href: '/merchant/inventory.html', icon: 'bi-archive', text: '库存管理', page: 'inventory' }, + { href: '/merchant/statistics.html', icon: 'bi-bar-chart', text: '数据统计', page: 'statistics' }, + { href: '/merchant/announcements.html', icon: 'bi-megaphone', text: '公告管理', page: 'announcements' }, + { href: '/merchant/messages.html', icon: 'bi-chat-dots', text: '留言管理', page: 'messages' }, + { href: '/merchant/reviews.html', icon: 'bi-star', text: '评价管理', page: 'reviews' }, + { href: '/merchant/users.html', icon: 'bi-people', text: '用户管理', page: 'users' } + ]; + + const menuHTML = menuItems.map(item => { + const activeClass = item.page === currentPage ? 'active' : ''; + return ` + + `; + }).join(''); + + return ` + + + + + + + + + `; +} + +// 添加侧边栏CSS样式 +function addSidebarStyles() { + if (document.getElementById('sidebar-styles')) return; + + const style = document.createElement('style'); + style.id = 'sidebar-styles'; + style.textContent = ` + /* 侧边栏样式 */ + .sidebar { + position: fixed; + top: 0; + left: 0; + width: 260px; + height: 100vh; + background: linear-gradient(180deg, #2c3e50 0%, #34495e 100%); + color: #fff; + z-index: 1050; + display: flex; + flex-direction: column; + transition: transform 0.3s ease; + box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1); + } + + .sidebar-header { + padding: 1.5rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + display: flex; + justify-content: space-between; + align-items: center; + } + + .sidebar-brand { + font-size: 1.25rem; + font-weight: 600; + } + + .sidebar-nav { + flex: 1; + overflow-y: auto; + padding: 1rem 0; + } + + .sidebar-nav .nav-link { + color: rgba(255, 255, 255, 0.8); + padding: 0.75rem 1.5rem; + transition: all 0.3s; + border-left: 3px solid transparent; + display: flex; + align-items: center; + text-decoration: none; + } + + .sidebar-nav .nav-link:hover { + background-color: rgba(255, 255, 255, 0.1); + color: #fff; + border-left-color: #0d6efd; + } + + .sidebar-nav .nav-link.active { + background-color: rgba(13, 110, 253, 0.2); + color: #fff; + border-left-color: #0d6efd; + font-weight: 600; + } + + .sidebar-nav .nav-link i { + width: 24px; + font-size: 1.1rem; + margin-right: 0.75rem; + } + + .sidebar-nav .nav-text { + flex: 1; + } + + .sidebar-footer { + padding: 1rem; + border-top: 1px solid rgba(255, 255, 255, 0.1); + } + + /* 移动端样式 */ + @media (max-width: 767.98px) { + .sidebar { + transform: translateX(-100%); + } + + .sidebar.show { + transform: translateX(0); + } + + .sidebar-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + z-index: 1049; + display: none; + transition: opacity 0.3s; + } + + .sidebar-overlay.show { + display: block; + } + + .top-navbar { + position: fixed; + top: 0; + left: 0; + right: 0; + height: 56px; + background: linear-gradient(180deg, #2c3e50 0%, #34495e 100%); + z-index: 1048; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + } + + body { + padding-top: 56px; + } + } + + /* 桌面端主内容区域 */ + @media (min-width: 768px) { + .main-content { + margin-left: 260px; + min-height: 100vh; + } + } + + /* 滚动条样式 */ + .sidebar-nav::-webkit-scrollbar { + width: 6px; + } + + .sidebar-nav::-webkit-scrollbar-track { + background: rgba(255, 255, 255, 0.05); + } + + .sidebar-nav::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.2); + border-radius: 3px; + } + + .sidebar-nav::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.3); + } + `; + document.head.appendChild(style); +} + +// 初始化侧边栏交互 +function initSidebar() { + const sidebar = document.getElementById('sidebar'); + const sidebarOverlay = document.getElementById('sidebarOverlay'); + const sidebarToggleBtn = document.getElementById('sidebarToggleBtn'); + const sidebarCloseBtn = document.getElementById('sidebarCloseBtn'); + + if (!sidebar) return; + + // 移动端切换侧边栏 + if (sidebarToggleBtn) { + sidebarToggleBtn.addEventListener('click', function() { + sidebar.classList.add('show'); + if (sidebarOverlay) sidebarOverlay.classList.add('show'); + document.body.style.overflow = 'hidden'; + }); + } + + // 关闭侧边栏 + function closeSidebar() { + sidebar.classList.remove('show'); + if (sidebarOverlay) sidebarOverlay.classList.remove('show'); + document.body.style.overflow = ''; + } + + if (sidebarCloseBtn) { + sidebarCloseBtn.addEventListener('click', closeSidebar); + } + + if (sidebarOverlay) { + sidebarOverlay.addEventListener('click', closeSidebar); + } + + // 点击导航项时,移动端自动关闭侧边栏 + const navLinks = document.querySelectorAll('.sidebar-nav .nav-link'); + navLinks.forEach(link => { + link.addEventListener('click', function() { + if (window.innerWidth < 768) { + closeSidebar(); + } + }); + }); +} + +// 初始化侧边栏(自动检测当前页面) +function initMerchantSidebar() { + addSidebarStyles(); + + // 检测当前页面 + const path = window.location.pathname; + let currentPage = 'dashboard'; + + if (path.includes('products')) currentPage = 'products'; + else if (path.includes('orders')) currentPage = 'orders'; + else if (path.includes('inventory')) currentPage = 'inventory'; + else if (path.includes('statistics')) currentPage = 'statistics'; + else if (path.includes('announcements')) currentPage = 'announcements'; + else if (path.includes('messages')) currentPage = 'messages'; + else if (path.includes('reviews')) currentPage = 'reviews'; + else if (path.includes('users')) currentPage = 'users'; + + // 查找并替换旧的导航栏 + const oldNav = document.querySelector('nav.navbar'); + if (oldNav) { + oldNav.outerHTML = generateSidebarHTML(currentPage); + initSidebar(); + } +} + +// 页面加载完成后初始化 +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initMerchantSidebar); +} else { + initMerchantSidebar(); +} diff --git a/target/classes/static/js/sidebar-replacer.js b/target/classes/static/js/sidebar-replacer.js new file mode 100644 index 0000000..d3186e1 --- /dev/null +++ b/target/classes/static/js/sidebar-replacer.js @@ -0,0 +1,300 @@ +/** + * 侧边栏自动替换脚本 + * 自动检测并替换旧的导航栏为侧边栏 + */ +(function() { + 'use strict'; + + // 侧边栏CSS + const sidebarCSS = ` + .sidebar { + position: fixed; + top: 0; + left: 0; + width: 260px; + height: 100vh; + background: linear-gradient(180deg, #2c3e50 0%, #34495e 100%); + color: #fff; + z-index: 1000; + display: flex; + flex-direction: column; + transition: transform 0.3s ease; + box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1); + } + .sidebar-header { + padding: 1.5rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + display: flex; + justify-content: space-between; + align-items: center; + } + .sidebar-brand { + font-size: 1.25rem; + font-weight: 600; + } + .sidebar-nav { + flex: 1; + overflow-y: auto; + padding: 1rem 0; + } + .sidebar-nav .nav-link { + color: rgba(255, 255, 255, 0.8); + padding: 0.75rem 1.5rem; + transition: all 0.3s; + border-left: 3px solid transparent; + display: flex; + align-items: center; + text-decoration: none; + } + .sidebar-nav .nav-link:hover { + background-color: rgba(255, 255, 255, 0.1); + color: #fff; + border-left-color: #0d6efd; + } + .sidebar-nav .nav-link.active { + background-color: rgba(13, 110, 253, 0.2); + color: #fff; + border-left-color: #0d6efd; + font-weight: 600; + } + .sidebar-nav .nav-link i { + width: 24px; + font-size: 1.1rem; + margin-right: 0.75rem; + } + .sidebar-nav .nav-text { + flex: 1; + } + .sidebar-footer { + padding: 1rem; + border-top: 1px solid rgba(255, 255, 255, 0.1); + } + @media (max-width: 767.98px) { + .sidebar { + transform: translateX(-100%); + } + .sidebar.show { + transform: translateX(0); + } + .sidebar-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + z-index: 1049; + display: none; + transition: opacity 0.3s; + } + .sidebar-overlay.show { + display: block; + } + .top-navbar { + position: fixed; + top: 0; + left: 0; + right: 0; + height: 56px; + background: linear-gradient(180deg, #2c3e50 0%, #34495e 100%); + z-index: 1048; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + } + body { + padding-top: 56px; + } + } + @media (min-width: 768px) { + .main-content { + margin-left: 260px; + min-height: 100vh; + position: relative; + z-index: 1; + width: calc(100% - 260px); + } + } + .sidebar-nav::-webkit-scrollbar { + width: 6px; + } + .sidebar-nav::-webkit-scrollbar-track { + background: rgba(255, 255, 255, 0.05); + } + .sidebar-nav::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.2); + border-radius: 3px; + } + .sidebar-nav::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.3); + } + `; + + // 菜单配置 + const menuConfig = [ + { href: '/merchant/dashboard.html', icon: 'bi-speedometer2', text: '首页', page: 'dashboard' }, + { href: '/merchant/products.html', icon: 'bi-box-seam', text: '商品管理', page: 'products' }, + { href: '/merchant/orders.html', icon: 'bi-receipt-cutoff', text: '订单管理', page: 'orders' }, + { href: '/merchant/inventory.html', icon: 'bi-archive', text: '库存管理', page: 'inventory' }, + { href: '/merchant/statistics.html', icon: 'bi-bar-chart-line', text: '数据统计', page: 'statistics' }, + { href: '/merchant/announcements.html', icon: 'bi-megaphone', text: '公告管理', page: 'announcements' }, + { href: '/merchant/messages.html', icon: 'bi-chat-dots', text: '留言管理', page: 'messages' }, + { href: '/merchant/reviews.html', icon: 'bi-star', text: '评价管理', page: 'reviews' }, + { href: '/merchant/users.html', icon: 'bi-people', text: '用户管理', page: 'users' } + ]; + + // 检测当前页面 + function detectCurrentPage() { + const path = window.location.pathname; + for (const item of menuConfig) { + if (path.includes(item.page)) { + return item.page; + } + } + return 'dashboard'; + } + + // 生成侧边栏HTML + function generateSidebar(activePage) { + const menuHTML = menuConfig.map(item => { + const activeClass = item.page === activePage ? 'active' : ''; + return ` + + `; + }).join(''); + + return ` + + + + `; + } + + // 初始化侧边栏 + function initSidebar() { + // 添加CSS + if (!document.getElementById('merchant-sidebar-styles')) { + const style = document.createElement('style'); + style.id = 'merchant-sidebar-styles'; + style.textContent = sidebarCSS; + document.head.appendChild(style); + } + + // 替换导航栏 + const oldNav = document.querySelector('nav.navbar'); + if (oldNav && !document.getElementById('sidebar')) { + const activePage = detectCurrentPage(); + oldNav.outerHTML = generateSidebar(activePage); + + // 包装主内容 - 确保不重复包装 + let mainContent = document.querySelector('.main-content'); + if (!mainContent) { + // 查找body的直接子元素中的container + const bodyChildren = Array.from(document.body.children); + let container = null; + + // 优先查找container-fluid,然后是container + for (const child of bodyChildren) { + if (child.classList.contains('container-fluid') || child.classList.contains('container')) { + if (!child.closest('.main-content') && !child.closest('.sidebar') && !child.closest('.sidebar-overlay') && !child.closest('.top-navbar')) { + container = child; + break; + } + } + } + + if (container) { + mainContent = document.createElement('div'); + mainContent.className = 'main-content'; + container.parentNode.insertBefore(mainContent, container); + mainContent.appendChild(container); + } + } + + // 初始化交互 + setupSidebarInteractions(); + } + } + + // 设置侧边栏交互 + function setupSidebarInteractions() { + const sidebar = document.getElementById('sidebar'); + const sidebarOverlay = document.getElementById('sidebarOverlay'); + const sidebarToggleBtn = document.getElementById('sidebarToggleBtn'); + const sidebarCloseBtn = document.getElementById('sidebarCloseBtn'); + + if (!sidebar) return; + + function openSidebar() { + sidebar.classList.add('show'); + if (sidebarOverlay) sidebarOverlay.classList.add('show'); + document.body.style.overflow = 'hidden'; + } + + function closeSidebar() { + sidebar.classList.remove('show'); + if (sidebarOverlay) sidebarOverlay.classList.remove('show'); + document.body.style.overflow = ''; + } + + if (sidebarToggleBtn) { + sidebarToggleBtn.addEventListener('click', openSidebar); + } + + if (sidebarCloseBtn) { + sidebarCloseBtn.addEventListener('click', closeSidebar); + } + + if (sidebarOverlay) { + sidebarOverlay.addEventListener('click', closeSidebar); + } + + // 点击导航项时,移动端自动关闭 + const navLinks = document.querySelectorAll('.sidebar-nav .nav-link'); + navLinks.forEach(link => { + link.addEventListener('click', function() { + if (window.innerWidth < 768) { + closeSidebar(); + } + }); + }); + } + + // 页面加载完成后初始化 + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initSidebar); + } else { + initSidebar(); + } +})(); diff --git a/target/classes/static/js/ui-enhancements.js b/target/classes/static/js/ui-enhancements.js new file mode 100644 index 0000000..7313a68 --- /dev/null +++ b/target/classes/static/js/ui-enhancements.js @@ -0,0 +1,289 @@ +/** + * UI交互增强脚本 + * 提供加载状态、视觉反馈、操作提示等功能 + */ + +// 创建Toast容器 +function ensureToastContainer() { + let container = document.getElementById('toastContainer'); + if (!container) { + container = document.createElement('div'); + container.id = 'toastContainer'; + container.className = 'toast-container position-fixed top-0 end-0 p-3'; + container.style.zIndex = '1060'; + document.body.appendChild(container); + } + return container; +} + +// 显示成功提示 +function showSuccess(message, duration = 3000) { + const container = ensureToastContainer(); + const toast = document.createElement('div'); + toast.className = 'toast align-items-center text-white bg-success border-0'; + toast.setAttribute('role', 'alert'); + toast.setAttribute('aria-live', 'assertive'); + toast.setAttribute('aria-atomic', 'true'); + toast.innerHTML = ` +
+
+ ${message} +
+ +
+ `; + container.appendChild(toast); + const bsToast = new bootstrap.Toast(toast, { delay: duration }); + bsToast.show(); + toast.addEventListener('hidden.bs.toast', () => toast.remove()); +} + +// 显示错误提示 +function showError(message, duration = 4000) { + const container = ensureToastContainer(); + const toast = document.createElement('div'); + toast.className = 'toast align-items-center text-white bg-danger border-0'; + toast.setAttribute('role', 'alert'); + toast.setAttribute('aria-live', 'assertive'); + toast.setAttribute('aria-atomic', 'true'); + toast.innerHTML = ` +
+
+ ${message} +
+ +
+ `; + container.appendChild(toast); + const bsToast = new bootstrap.Toast(toast, { delay: duration }); + bsToast.show(); + toast.addEventListener('hidden.bs.toast', () => toast.remove()); +} + +// 显示警告提示 +function showWarning(message, duration = 3000) { + const container = ensureToastContainer(); + const toast = document.createElement('div'); + toast.className = 'toast align-items-center text-white bg-warning border-0'; + toast.setAttribute('role', 'alert'); + toast.setAttribute('aria-live', 'assertive'); + toast.setAttribute('aria-atomic', 'true'); + toast.innerHTML = ` +
+
+ ${message} +
+ +
+ `; + container.appendChild(toast); + const bsToast = new bootstrap.Toast(toast, { delay: duration }); + bsToast.show(); + toast.addEventListener('hidden.bs.toast', () => toast.remove()); +} + +// 显示信息提示 +function showInfo(message, duration = 3000) { + const container = ensureToastContainer(); + const toast = document.createElement('div'); + toast.className = 'toast align-items-center text-white bg-info border-0'; + toast.setAttribute('role', 'alert'); + toast.setAttribute('aria-live', 'assertive'); + toast.setAttribute('aria-atomic', 'true'); + toast.innerHTML = ` +
+
+ ${message} +
+ +
+ `; + container.appendChild(toast); + const bsToast = new bootstrap.Toast(toast, { delay: duration }); + bsToast.show(); + toast.addEventListener('hidden.bs.toast', () => toast.remove()); +} + +// 显示加载状态 +function showLoading(elementId, message = '加载中...') { + const element = document.getElementById(elementId); + if (element) { + element.innerHTML = ` +
+
+ ${message} +
+

${message}

+
+ `; + } +} + +// 按钮加载状态 +function setButtonLoading(button, loading = true, originalText = null) { + if (loading) { + if (!button.dataset.originalText) { + button.dataset.originalText = button.innerHTML; + } + button.disabled = true; + button.innerHTML = ` + + 处理中... + `; + } else { + button.disabled = false; + button.innerHTML = button.dataset.originalText || originalText || '提交'; + delete button.dataset.originalText; + } +} + +// 增强的fetch函数(带加载状态和错误处理) +async function enhancedFetch(url, options = {}) { + const { showLoading: showLoadingId, button: loadingButton, ...fetchOptions } = options; + + // 显示加载状态 + if (showLoadingId) { + showLoading(showLoadingId); + } + + // 按钮加载状态 + if (loadingButton) { + setButtonLoading(loadingButton, true); + } + + try { + const response = await fetch(url, fetchOptions); + const data = await response.json(); + + // 恢复按钮状态 + if (loadingButton) { + setButtonLoading(loadingButton, false); + } + + return { response, data }; + } catch (error) { + // 恢复按钮状态 + if (loadingButton) { + setButtonLoading(loadingButton, false); + } + + showError('网络请求失败,请检查网络连接'); + throw error; + } +} + +// 确认对话框(使用自定义模态框) +function confirmAction(message, title = '确认操作', confirmText = '确定', cancelText = '取消') { + return new Promise((resolve) => { + // 确保自定义模态框已加载 + if (typeof CustomModal === 'undefined') { + console.error('CustomModal未加载,请先引入custom-modal.js'); + // 降级到原生confirm + resolve(confirm(message)); + return; + } + + const modal = new CustomModal({ + title: title, + content: `

${message}

`, + confirmText: confirmText, + cancelText: cancelText, + onConfirm: () => { + resolve(true); + return true; + }, + onClose: () => { + resolve(false); + } + }); + modal.show(); + }); +} + +// 表单验证增强 +function validateForm(formId) { + const form = document.getElementById(formId); + if (!form) return false; + + const requiredFields = form.querySelectorAll('[required]'); + let isValid = true; + + requiredFields.forEach(field => { + if (!field.value.trim()) { + field.classList.add('is-invalid'); + isValid = false; + } else { + field.classList.remove('is-invalid'); + field.classList.add('is-valid'); + } + }); + + return isValid; +} + +// 数字格式化 +function formatNumber(num, decimals = 2) { + return parseFloat(num || 0).toFixed(decimals); +} + +// 日期格式化 +function formatDate(date, format = 'YYYY-MM-DD HH:mm:ss') { + if (!date) return '-'; + const d = new Date(date); + const year = d.getFullYear(); + const month = String(d.getMonth() + 1).padStart(2, '0'); + const day = String(d.getDate()).padStart(2, '0'); + const hours = String(d.getHours()).padStart(2, '0'); + const minutes = String(d.getMinutes()).padStart(2, '0'); + const seconds = String(d.getSeconds()).padStart(2, '0'); + + return format + .replace('YYYY', year) + .replace('MM', month) + .replace('DD', day) + .replace('HH', hours) + .replace('mm', minutes) + .replace('ss', seconds); +} + +// 防抖函数 +function debounce(func, wait) { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; +} + +// 节流函数 +function throttle(func, limit) { + let inThrottle; + return function(...args) { + if (!inThrottle) { + func.apply(this, args); + inThrottle = true; + setTimeout(() => inThrottle = false, limit); + } + }; +} + +// 导出到全局 +window.UIEnhancements = { + showSuccess, + showError, + showWarning, + showInfo, + showLoading, + setButtonLoading, + enhancedFetch, + confirmAction, + validateForm, + formatNumber, + formatDate, + debounce, + throttle +}; diff --git a/target/classes/static/upload/product/3a9cb9e4-97a9-4be5-ba2f-6939bebe5ba5.jpg b/target/classes/static/upload/product/3a9cb9e4-97a9-4be5-ba2f-6939bebe5ba5.jpg new file mode 100644 index 0000000..21985b8 Binary files /dev/null and b/target/classes/static/upload/product/3a9cb9e4-97a9-4be5-ba2f-6939bebe5ba5.jpg differ diff --git a/target/classes/static/upload/product/ad501998-b3c1-4568-ae4f-31e2d9ce0e3d.jpg b/target/classes/static/upload/product/ad501998-b3c1-4568-ae4f-31e2d9ce0e3d.jpg new file mode 100644 index 0000000..359f637 Binary files /dev/null and b/target/classes/static/upload/product/ad501998-b3c1-4568-ae4f-31e2d9ce0e3d.jpg differ diff --git a/target/classes/static/upload/product/f6f49393-42c0-4c27-92fe-5d54aa04888f.jpg b/target/classes/static/upload/product/f6f49393-42c0-4c27-92fe-5d54aa04888f.jpg new file mode 100644 index 0000000..38768ac Binary files /dev/null and b/target/classes/static/upload/product/f6f49393-42c0-4c27-92fe-5d54aa04888f.jpg differ diff --git a/target/classes/templates/merchant/announcements.html b/target/classes/templates/merchant/announcements.html new file mode 100644 index 0000000..ca4525a --- /dev/null +++ b/target/classes/templates/merchant/announcements.html @@ -0,0 +1,291 @@ + + + + + + 公告管理 - 商家后台 + + + + + + +
+
+

公告管理

+ +
+
+
+
+
+
+
+ + + + + + + + + + diff --git a/target/classes/templates/merchant/common/sidebar.html b/target/classes/templates/merchant/common/sidebar.html new file mode 100644 index 0000000..bd9bc82 --- /dev/null +++ b/target/classes/templates/merchant/common/sidebar.html @@ -0,0 +1,283 @@ + + + + + + + + + + + + diff --git a/target/classes/templates/merchant/dashboard.html b/target/classes/templates/merchant/dashboard.html new file mode 100644 index 0000000..443cb8d --- /dev/null +++ b/target/classes/templates/merchant/dashboard.html @@ -0,0 +1,413 @@ + + + + + + 商家后台 - 奶茶店管理系统 + + + + + + + + + + + + + + +
+
+

数据概览

+
+
+
+
+
+
+
今日订单
+

-

+
+ +
+
+
+
+
+
+
+
+
+
今日营收
+

-

+
+ +
+
+
+
+
+
+
+
+
+
待处理订单
+

-

+
+ +
+
+
+
+
+
+ + + + + + + diff --git a/target/classes/templates/merchant/inventory.html b/target/classes/templates/merchant/inventory.html new file mode 100644 index 0000000..e9f8cd6 --- /dev/null +++ b/target/classes/templates/merchant/inventory.html @@ -0,0 +1,466 @@ + + + + + + 库存管理 - 商家后台 + + + + + + + +
+
+

库存管理

+ +
+ +
+ +
+ +
+
+ + + + + + + + + + + + + diff --git a/target/classes/templates/merchant/login.html b/target/classes/templates/merchant/login.html new file mode 100644 index 0000000..f692850 --- /dev/null +++ b/target/classes/templates/merchant/login.html @@ -0,0 +1,81 @@ + + + + + + 商家登录 - 奶茶店管理系统 + + + + + +
+
+
+ +
+
+
+ + + + diff --git a/target/classes/templates/merchant/messages.html b/target/classes/templates/merchant/messages.html new file mode 100644 index 0000000..03fc047 --- /dev/null +++ b/target/classes/templates/merchant/messages.html @@ -0,0 +1,209 @@ + + + + + + 留言管理 - 商家后台 + + + + + + +
+

留言管理

+
+
+
+
+
+
+ + + + + + + + + + diff --git a/target/classes/templates/merchant/orders.html b/target/classes/templates/merchant/orders.html new file mode 100644 index 0000000..216168f --- /dev/null +++ b/target/classes/templates/merchant/orders.html @@ -0,0 +1,503 @@ + + + + + + 订单管理 - 商家后台 + + + + + + + +
+
+

订单管理

+
+ + + + +
+
+ +
+
+ + + + + + + + + + \ No newline at end of file diff --git a/target/classes/templates/merchant/product-edit.html b/target/classes/templates/merchant/product-edit.html new file mode 100644 index 0000000..8203bcb --- /dev/null +++ b/target/classes/templates/merchant/product-edit.html @@ -0,0 +1,651 @@ + + + + + + 商品编辑 - 商家后台 + + + + + +
+

新增商品

+ +
+ + +
+
基本信息
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+
+ +
+
+ 规格管理 + +
+
+
+
+
+ +
+
+ 配料管理 + +
+
+
+
+
+ +
+
+ 定制选项管理(甜度、冰度) +
+ + +
+
+
+
+
甜度选项
+
+
+
+
冰度选项
+
+
+
+
+ +
+ + 取消 +
+
+
+ + + + + + + diff --git a/target/classes/templates/merchant/products.html b/target/classes/templates/merchant/products.html new file mode 100644 index 0000000..254e191 --- /dev/null +++ b/target/classes/templates/merchant/products.html @@ -0,0 +1,470 @@ + + + + + + 商品管理 - 商家后台 + + + + + + + + + + + + + + +
+
+
+

商品管理

+ +
+
+
+
+
+
+
+ + + + + + + diff --git a/target/classes/templates/merchant/register.html b/target/classes/templates/merchant/register.html new file mode 100644 index 0000000..c439806 --- /dev/null +++ b/target/classes/templates/merchant/register.html @@ -0,0 +1,120 @@ + + + + + + 商家注册 - 奶茶店管理系统 + + + + + +
+
+
+
+
+

+ 商家后台 +

+

商家注册

+
+
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+ +
+ +
+
+
+
+
+ + + + diff --git a/target/classes/templates/merchant/reviews.html b/target/classes/templates/merchant/reviews.html new file mode 100644 index 0000000..e146a67 --- /dev/null +++ b/target/classes/templates/merchant/reviews.html @@ -0,0 +1,212 @@ + + + + + + 评价管理 - 商家后台 + + + + + + +
+

评价管理

+
+
+
+
+
+
+ + + + + + + + + + diff --git a/target/classes/templates/merchant/statistics.html b/target/classes/templates/merchant/statistics.html new file mode 100644 index 0000000..819087b --- /dev/null +++ b/target/classes/templates/merchant/statistics.html @@ -0,0 +1,491 @@ + + + + + + 数据统计 - 商家后台 + + + + + + + +
+

数据统计

+ +
+
+ + + +
+
+ +
+
+
+
+
销售统计
+
+
+

请选择时间段

+
+
+
+
+
+
+
营收统计
+
+
+

请选择时间段

+
+
+
+
+ +
+
+
+
+
活跃用户
+
+
+

加载中...

+
+
+
+
+
+
+
粘性用户
+
+
+

加载中...

+
+
+
+
+ +
+
+
热销商品
+
+
+
+
+
+
+ + + + + + + diff --git a/target/classes/templates/merchant/users.html b/target/classes/templates/merchant/users.html new file mode 100644 index 0000000..c3fd638 --- /dev/null +++ b/target/classes/templates/merchant/users.html @@ -0,0 +1,163 @@ + + + + + + 用户管理 - 商家后台 + + + + + + +
+

用户管理

+
+
+
+ +
+
+
+ + + + + + diff --git a/target/classes/templates/user/cart.html b/target/classes/templates/user/cart.html new file mode 100644 index 0000000..904399b --- /dev/null +++ b/target/classes/templates/user/cart.html @@ -0,0 +1,389 @@ + + + + + + 购物袋 + + + + + +
购物袋
+
+
+ +
加载中...
+
+
+ + + + + + + + + + + + \ No newline at end of file diff --git a/target/classes/templates/user/checkout.html b/target/classes/templates/user/checkout.html new file mode 100644 index 0000000..490e3fa --- /dev/null +++ b/target/classes/templates/user/checkout.html @@ -0,0 +1,616 @@ + + + + + + 确认订单 + + + + + + + + +
+ + +
+
配送地址
+
+
+
+
+ 加载地址... +
+
+ +
+ +
+ + +
+
订单商品
+
+
+ + + + +
+
订单备注
+ +
+ + +
+
+ 合计 + ¥0.00 +
+ +
+ + + + +
+ + +
+ +
+ + +
+ + + + + + + + + \ No newline at end of file diff --git a/target/classes/templates/user/index.html b/target/classes/templates/user/index.html new file mode 100644 index 0000000..f798e08 --- /dev/null +++ b/target/classes/templates/user/index.html @@ -0,0 +1,652 @@ + + + + + + 首页 - 荣光奶茶店 + + + + + +
+ +
+
+ +
+
+

荣光奶茶店

+
+ + 科技园南区C栋 + + + 4.9 + +
+
+
+ +
+
+ + + + + +
+ +

店长推荐

+
+
+ + +
+ +

全部饮品

+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/target/classes/templates/user/login.html b/target/classes/templates/user/login.html new file mode 100644 index 0000000..2782cd6 --- /dev/null +++ b/target/classes/templates/user/login.html @@ -0,0 +1,236 @@ + + + + + + 登录 + + + + + +
+
欢迎回来
+
登录您的账号以享受美味
+
+ +
+
+ + +
+ +
+ + +
+ + +
+ + + + + + + \ No newline at end of file diff --git a/target/classes/templates/user/order-detail.html b/target/classes/templates/user/order-detail.html new file mode 100644 index 0000000..92c447c --- /dev/null +++ b/target/classes/templates/user/order-detail.html @@ -0,0 +1,356 @@ + + + + + + 订单详情 + + + + + + + + +
加载中...
+ + + +
+ +
+ + + + + + + \ No newline at end of file diff --git a/target/classes/templates/user/orders.html b/target/classes/templates/user/orders.html new file mode 100644 index 0000000..407da0b --- /dev/null +++ b/target/classes/templates/user/orders.html @@ -0,0 +1,441 @@ + + + + + + 我的订单 + + + + + +
+
全部
+
待支付
+
制作中
+
待取餐
+
已完成
+
+ +
+ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/target/classes/templates/user/payment-success.html b/target/classes/templates/user/payment-success.html new file mode 100644 index 0000000..b4bbb63 --- /dev/null +++ b/target/classes/templates/user/payment-success.html @@ -0,0 +1,214 @@ + + + + + + 支付成功 + + + + + +
+
+ +
+
支付成功
+
感谢您的购买,订单已提交成功
+ +
+
+ 订单编号 + -- +
+
+ 支付金额 + -- +
+
+ +
+ + +
+
+ + + + diff --git a/target/classes/templates/user/payment.html b/target/classes/templates/user/payment.html new file mode 100644 index 0000000..2f7a69c --- /dev/null +++ b/target/classes/templates/user/payment.html @@ -0,0 +1,348 @@ + + + + + + 支付订单 + + + + + + + + +
+ + +
+
+
需支付
+
¥0.00
+
+
+ + +
+
订单商品
+
+
+ + +
+
选择支付方式
+ + +
+ + +
+
+ 合计 + ¥0.00 +
+ +
+ + + + + + + diff --git a/target/classes/templates/user/product-detail.html b/target/classes/templates/user/product-detail.html new file mode 100644 index 0000000..51bf1e0 --- /dev/null +++ b/target/classes/templates/user/product-detail.html @@ -0,0 +1,356 @@ + + + + + + 商品详情 + + + + + + + + + + +
+

Loading...

+

+ +
+ + + +
+ +
+
数量
+
+ + + +
+
+
+
+ +
+
+ 总计金额 + ¥0.00 +
+ +
+ + + + + + \ No newline at end of file diff --git a/target/classes/templates/user/profile.html b/target/classes/templates/user/profile.html new file mode 100644 index 0000000..94ef48d --- /dev/null +++ b/target/classes/templates/user/profile.html @@ -0,0 +1,401 @@ + + + + + + 个人中心 + + + + + + +
+
+
--
+
--
+ +
+ + + + +
+ +
+ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/target/classes/templates/user/register.html b/target/classes/templates/user/register.html new file mode 100644 index 0000000..03eb5bb --- /dev/null +++ b/target/classes/templates/user/register.html @@ -0,0 +1,184 @@ + + + + + + 注册 + + + + + +
+
创建账号
+
填写以下信息完成注册
+
+ +
+ +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + + + + + \ No newline at end of file diff --git a/z_wb_nc_shop.sql b/z_wb_nc_shop.sql new file mode 100644 index 0000000..d651365 --- /dev/null +++ b/z_wb_nc_shop.sql @@ -0,0 +1,506 @@ +/* + Navicat Premium Dump SQL + + Source Server : 本地 + Source Server Type : MySQL + Source Server Version : 80406 (8.4.6) + Source Host : localhost:3306 + Source Schema : z_wb_nc_shop + + Target Server Type : MySQL + Target Server Version : 80406 (8.4.6) + File Encoding : 65001 + + Date: 10/01/2026 17:01:47 +*/ + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for announcement +-- ---------------------------- +DROP TABLE IF EXISTS `announcement`; +CREATE TABLE `announcement` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `merchant_id` bigint NOT NULL COMMENT '商家ID', + `title` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '标题', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '内容', + `type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'NOTICE' COMMENT '类型:ACTIVITY-活动,NOTICE-通知', + `status` tinyint NULL DEFAULT 1 COMMENT '状态:0-下架,1-发布', + `create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + INDEX `merchant_id`(`merchant_id` ASC) USING BTREE, + CONSTRAINT `announcement_ibfk_1` FOREIGN KEY (`merchant_id`) REFERENCES `merchant` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '公告表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of announcement +-- ---------------------------- +INSERT INTO `announcement` VALUES (1, 1, '新品上市,快来品尝', '新品上市,快来品尝\n新品上市,快来品尝', 'ACTIVITY', 1, '2026-01-09 16:59:12'); +INSERT INTO `announcement` VALUES (2, 1, '店庆将于2月30日开始', '当天全部免单!!!', 'ACTIVITY', 1, '2026-01-10 16:12:20'); + +-- ---------------------------- +-- Table structure for cart +-- ---------------------------- +DROP TABLE IF EXISTS `cart`; +CREATE TABLE `cart` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL COMMENT '用户ID', + `product_id` bigint NOT NULL COMMENT '商品ID', + `spec_id` bigint NULL DEFAULT NULL COMMENT '规格ID', + `quantity` int NULL DEFAULT 1 COMMENT '数量', + `custom_sweetness` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '甜度定制', + `custom_ice` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '冰度定制', + `toppings` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '配料JSON', + `create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + INDEX `product_id`(`product_id` ASC) USING BTREE, + INDEX `idx_cart_user`(`user_id` ASC) USING BTREE, + CONSTRAINT `cart_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT, + CONSTRAINT `cart_ibfk_2` FOREIGN KEY (`product_id`) REFERENCES `product` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 12 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '购物车表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of cart +-- ---------------------------- + +-- ---------------------------- +-- Table structure for inventory +-- ---------------------------- +DROP TABLE IF EXISTS `inventory`; +CREATE TABLE `inventory` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `merchant_id` bigint NOT NULL COMMENT '商家ID', + `material_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '原料名称', + `quantity` decimal(10, 2) NOT NULL COMMENT '数量', + `unit` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '单位', + `min_threshold` decimal(10, 2) NULL DEFAULT 0.00 COMMENT '最低库存阈值', + `last_in_time` datetime NULL DEFAULT NULL COMMENT '最后入库时间', + `last_out_time` datetime NULL DEFAULT NULL COMMENT '最后出库时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `merchant_id`(`merchant_id` ASC) USING BTREE, + CONSTRAINT `inventory_ibfk_1` FOREIGN KEY (`merchant_id`) REFERENCES `merchant` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '库存表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of inventory +-- ---------------------------- +INSERT INTO `inventory` VALUES (1, 1, '橙子', 9998.00, 'kg', 0.00, NULL, '2026-01-09 15:16:54'); + +-- ---------------------------- +-- Table structure for inventory_log +-- ---------------------------- +DROP TABLE IF EXISTS `inventory_log`; +CREATE TABLE `inventory_log` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `inventory_id` bigint NOT NULL COMMENT '库存ID', + `operation_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '操作类型:IN-入库,OUT-出库', + `quantity` decimal(10, 2) NOT NULL COMMENT '操作数量', + `operator` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '操作人', + `create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + INDEX `inventory_id`(`inventory_id` ASC) USING BTREE, + CONSTRAINT `inventory_log_ibfk_1` FOREIGN KEY (`inventory_id`) REFERENCES `inventory` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '库存操作日志表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of inventory_log +-- ---------------------------- +INSERT INTO `inventory_log` VALUES (1, 1, 'OUT', 1.00, '', '2026-01-09 15:16:54'); + +-- ---------------------------- +-- Table structure for merchant +-- ---------------------------- +DROP TABLE IF EXISTS `merchant`; +CREATE TABLE `merchant` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户名', + `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '密码(加密)', + `store_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '门店名称', + `address` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '门店地址', + `phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '联系电话', + `email` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '邮箱', + `manager_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '负责人姓名', + `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '头像路径', + `create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP, + `update_time` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `username`(`username` ASC) USING BTREE, + INDEX `idx_merchant_username`(`username` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '商家表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of merchant +-- ---------------------------- +INSERT INTO `merchant` VALUES (1, 'l', '4297f44b13955235245b2497399d7a93', 'lq', NULL, '15100000000', 'aaa@qq.com', NULL, NULL, '2026-01-09 12:52:08', '2026-01-09 12:52:08'); + +-- ---------------------------- +-- Table structure for message +-- ---------------------------- +DROP TABLE IF EXISTS `message`; +CREATE TABLE `message` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL COMMENT '用户ID', + `merchant_id` bigint NOT NULL COMMENT '商家ID', + `order_id` bigint NULL DEFAULT NULL COMMENT '订单ID', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '留言内容', + `reply` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '回复内容', + `status` tinyint NULL DEFAULT 0 COMMENT '状态:0-未回复,1-已回复', + `create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP, + `reply_time` datetime NULL DEFAULT NULL COMMENT '回复时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `user_id`(`user_id` ASC) USING BTREE, + INDEX `merchant_id`(`merchant_id` ASC) USING BTREE, + CONSTRAINT `message_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT, + CONSTRAINT `message_ibfk_2` FOREIGN KEY (`merchant_id`) REFERENCES `merchant` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '留言反馈表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of message +-- ---------------------------- + +-- ---------------------------- +-- Table structure for order +-- ---------------------------- +DROP TABLE IF EXISTS `order`; +CREATE TABLE `order` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `order_no` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '订单号', + `user_id` bigint NOT NULL COMMENT '用户ID', + `merchant_id` bigint NOT NULL COMMENT '商家ID', + `total_amount` decimal(10, 2) NOT NULL COMMENT '订单总金额', + `payment_method` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '支付方式:wechat/alipay', + `payment_status` tinyint NULL DEFAULT 0 COMMENT '支付状态:0-未支付,1-已支付', + `order_status` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT 'PENDING_PAY' COMMENT '订单状态:PENDING_PAY-待支付,MAKING-制作中,READY-待取餐,COMPLETED-已完成,CANCELLED-已取消', + `address` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '收货地址', + `contact_phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '联系电话', + `create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `pay_time` datetime NULL DEFAULT NULL COMMENT '支付时间', + `complete_time` datetime NULL DEFAULT NULL COMMENT '完成时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `order_no`(`order_no` ASC) USING BTREE, + INDEX `idx_order_user`(`user_id` ASC) USING BTREE, + INDEX `idx_order_merchant`(`merchant_id` ASC) USING BTREE, + INDEX `idx_order_status`(`order_status` ASC) USING BTREE, + INDEX `idx_order_no`(`order_no` ASC) USING BTREE, + CONSTRAINT `order_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT, + CONSTRAINT `order_ibfk_2` FOREIGN KEY (`merchant_id`) REFERENCES `merchant` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '订单表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of order +-- ---------------------------- +INSERT INTO `order` VALUES (1, 'ORD202601091502550001', 1, 1, 4.00, 'wechat', 1, 'COMPLETED', '测试地址', '15110000000', '2026-01-09 15:02:55', '2026-01-09 15:03:00', '2026-01-09 15:03:21'); +INSERT INTO `order` VALUES (2, 'ORD202601091536100001', 1, 1, 4.00, 'wechat', 1, 'COMPLETED', 'c街道101号', '15100000000', '2026-01-09 15:36:10', '2026-01-09 15:36:16', '2026-01-09 15:36:43'); +INSERT INTO `order` VALUES (3, 'ORD202601101522530001', 1, 1, 22.40, 'wechat', 1, 'MAKING', '1111111', '13100000000', '2026-01-10 15:22:53', '2026-01-10 15:22:57', NULL); +INSERT INTO `order` VALUES (4, 'ORD202601101557240001', 1, 1, 6.00, 'alipay', 1, 'MAKING', 'c街道101号', '15100000000', '2026-01-10 15:57:24', '2026-01-10 16:04:09', NULL); +INSERT INTO `order` VALUES (5, 'ORD202601101600170001', 1, 1, 6.00, NULL, 0, 'PENDING_PAY', 'c街道101号', '15100000000', '2026-01-10 16:00:17', NULL, NULL); +INSERT INTO `order` VALUES (6, 'ORD202601101605020001', 2, 1, 30.36, 'wechat', 1, 'READY', '1\n1', '13111111111', '2026-01-10 16:05:02', '2026-01-10 16:05:04', NULL); +INSERT INTO `order` VALUES (7, 'ORD202601101617430001', 1, 1, 27.18, 'wechat', 1, 'MAKING', 'c街道101号', '15100000000', '2026-01-10 16:17:43', '2026-01-10 16:17:46', NULL); + +-- ---------------------------- +-- Table structure for order_item +-- ---------------------------- +DROP TABLE IF EXISTS `order_item`; +CREATE TABLE `order_item` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `order_id` bigint NOT NULL COMMENT '订单ID', + `product_id` bigint NOT NULL COMMENT '商品ID', + `product_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '商品名称', + `spec_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '规格名称', + `quantity` int NOT NULL COMMENT '数量', + `price` decimal(10, 2) NOT NULL COMMENT '单价', + `custom_sweetness` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '甜度', + `custom_ice` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '冰度', + `toppings` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '配料JSON', + `subtotal` decimal(10, 2) NOT NULL COMMENT '小计', + PRIMARY KEY (`id`) USING BTREE, + INDEX `order_id`(`order_id` ASC) USING BTREE, + INDEX `product_id`(`product_id` ASC) USING BTREE, + CONSTRAINT `order_item_ibfk_1` FOREIGN KEY (`order_id`) REFERENCES `order` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT, + CONSTRAINT `order_item_ibfk_2` FOREIGN KEY (`product_id`) REFERENCES `product` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 10 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '订单明细表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of order_item +-- ---------------------------- +INSERT INTO `order_item` VALUES (1, 1, 1, '棒打鲜橙', '大杯', 1, 4.00, '', '', '[]', 4.00); +INSERT INTO `order_item` VALUES (2, 2, 1, '棒打鲜橙', '大杯', 1, 4.00, '', '', '[]', 4.00); +INSERT INTO `order_item` VALUES (3, 3, 1, '棒打鲜橙', '大杯', 2, 7.20, '七分糖', '少冰', '[\"4\"]', 14.40); +INSERT INTO `order_item` VALUES (4, 3, 1, '棒打鲜橙', NULL, 2, 4.00, '无糖', '去冰', '[]', 8.00); +INSERT INTO `order_item` VALUES (5, 4, 1, '棒打鲜橙', '大杯', 1, 6.00, '无糖', '去冰', '[]', 6.00); +INSERT INTO `order_item` VALUES (6, 5, 1, '棒打鲜橙', '大杯', 1, 6.00, '无糖', '去冰', '[]', 6.00); +INSERT INTO `order_item` VALUES (7, 6, 2, '满杯百香果', '大杯', 2, 15.18, '少糖', '少冰', '[\"5\",\"6\",\"7\"]', 30.36); +INSERT INTO `order_item` VALUES (8, 7, 2, '满杯百香果', '大杯', 1, 15.18, '少糖', '少冰', '[\"5\",\"6\",\"7\"]', 15.18); +INSERT INTO `order_item` VALUES (9, 7, 1, '棒打鲜橙', '大杯', 2, 6.00, '无糖', '去冰', '[]', 12.00); + +-- ---------------------------- +-- Table structure for order_track +-- ---------------------------- +DROP TABLE IF EXISTS `order_track`; +CREATE TABLE `order_track` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `order_id` bigint NOT NULL COMMENT '订单ID', + `status` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '状态', + `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '描述', + `create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + INDEX `order_id`(`order_id` ASC) USING BTREE, + CONSTRAINT `order_track_ibfk_1` FOREIGN KEY (`order_id`) REFERENCES `order` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 19 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '订单跟踪表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of order_track +-- ---------------------------- +INSERT INTO `order_track` VALUES (1, 1, 'PENDING_PAY', '订单已创建,等待支付', '2026-01-09 15:02:55'); +INSERT INTO `order_track` VALUES (2, 1, 'MAKING', '支付成功,商家开始制作', '2026-01-09 15:03:00'); +INSERT INTO `order_track` VALUES (3, 1, 'READY', '制作完成,等待取餐', '2026-01-09 15:03:15'); +INSERT INTO `order_track` VALUES (4, 1, 'COMPLETED', '订单已完成', '2026-01-09 15:03:21'); +INSERT INTO `order_track` VALUES (5, 2, 'PENDING_PAY', '订单已创建,等待支付', '2026-01-09 15:36:10'); +INSERT INTO `order_track` VALUES (6, 2, 'MAKING', '支付成功,商家开始制作', '2026-01-09 15:36:16'); +INSERT INTO `order_track` VALUES (7, 2, 'READY', '制作完成,等待取餐', '2026-01-09 15:36:36'); +INSERT INTO `order_track` VALUES (8, 2, 'COMPLETED', '订单已完成', '2026-01-09 15:36:43'); +INSERT INTO `order_track` VALUES (9, 3, 'PENDING_PAY', '订单已创建,等待支付', '2026-01-10 15:22:53'); +INSERT INTO `order_track` VALUES (10, 3, 'MAKING', '支付成功,商家开始制作', '2026-01-10 15:22:57'); +INSERT INTO `order_track` VALUES (11, 4, 'PENDING_PAY', '订单已创建,等待支付', '2026-01-10 15:57:24'); +INSERT INTO `order_track` VALUES (12, 5, 'PENDING_PAY', '订单已创建,等待支付', '2026-01-10 16:00:17'); +INSERT INTO `order_track` VALUES (13, 4, 'MAKING', '支付成功,商家开始制作', '2026-01-10 16:04:09'); +INSERT INTO `order_track` VALUES (14, 6, 'PENDING_PAY', '订单已创建,等待支付', '2026-01-10 16:05:02'); +INSERT INTO `order_track` VALUES (15, 6, 'MAKING', '支付成功,商家开始制作', '2026-01-10 16:05:04'); +INSERT INTO `order_track` VALUES (16, 7, 'PENDING_PAY', '订单已创建,等待支付', '2026-01-10 16:17:43'); +INSERT INTO `order_track` VALUES (17, 7, 'MAKING', '支付成功,商家开始制作', '2026-01-10 16:17:46'); +INSERT INTO `order_track` VALUES (18, 6, 'READY', '制作完成,等待取餐', '2026-01-10 16:39:45'); + +-- ---------------------------- +-- Table structure for product +-- ---------------------------- +DROP TABLE IF EXISTS `product`; +CREATE TABLE `product` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `merchant_id` bigint NOT NULL COMMENT '商家ID', + `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '商品名称', + `category` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '分类', + `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '商品描述', + `base_price` decimal(10, 2) NOT NULL COMMENT '基础价格', + `image` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '商品图片', + `status` tinyint NULL DEFAULT 1 COMMENT '状态:0-下架,1-上架', + `create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_product_merchant`(`merchant_id` ASC) USING BTREE, + INDEX `idx_product_status`(`status` ASC) USING BTREE, + CONSTRAINT `product_ibfk_1` FOREIGN KEY (`merchant_id`) REFERENCES `merchant` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '商品表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of product +-- ---------------------------- +INSERT INTO `product` VALUES (1, 1, '棒打鲜橙', '奶茶', '1', 4.00, '/upload/product/ad501998-b3c1-4568-ae4f-31e2d9ce0e3d.jpg', 1, '2026-01-09 15:01:13'); +INSERT INTO `product` VALUES (2, 1, '满杯百香果', '果茶', '这个是用了一百个百香果', 7.00, '/upload/product/f6f49393-42c0-4c27-92fe-5d54aa04888f.jpg', 1, '2026-01-10 14:13:38'); + +-- ---------------------------- +-- Table structure for product_custom +-- ---------------------------- +DROP TABLE IF EXISTS `product_custom`; +CREATE TABLE `product_custom` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `product_id` bigint NOT NULL COMMENT '商品ID', + `option_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '选项类型:sweetness-甜度,ice-冰度', + `option_value` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '选项值', + `price_adjust` decimal(10, 2) NULL DEFAULT 0.00 COMMENT '价格调整', + PRIMARY KEY (`id`) USING BTREE, + INDEX `product_id`(`product_id` ASC) USING BTREE, + CONSTRAINT `product_custom_ibfk_1` FOREIGN KEY (`product_id`) REFERENCES `product` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 33 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '商品定制选项表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of product_custom +-- ---------------------------- +INSERT INTO `product_custom` VALUES (17, 1, 'sweetness', '无糖', 0.00); +INSERT INTO `product_custom` VALUES (18, 1, 'sweetness', '三分糖', 0.00); +INSERT INTO `product_custom` VALUES (19, 1, 'sweetness', '七分糖', 0.00); +INSERT INTO `product_custom` VALUES (20, 1, 'sweetness', '多糖', 0.00); +INSERT INTO `product_custom` VALUES (21, 1, 'ice', '去冰', 0.00); +INSERT INTO `product_custom` VALUES (22, 1, 'ice', '少冰', 0.00); +INSERT INTO `product_custom` VALUES (23, 1, 'ice', '多冰', 0.00); +INSERT INTO `product_custom` VALUES (24, 1, 'ice', '热', 0.00); +INSERT INTO `product_custom` VALUES (25, 2, 'sweetness', '无糖', 0.00); +INSERT INTO `product_custom` VALUES (26, 2, 'sweetness', '少糖', 0.00); +INSERT INTO `product_custom` VALUES (27, 2, 'sweetness', '正常', 0.00); +INSERT INTO `product_custom` VALUES (28, 2, 'sweetness', '多糖', 0.00); +INSERT INTO `product_custom` VALUES (29, 2, 'ice', '去冰', 0.00); +INSERT INTO `product_custom` VALUES (30, 2, 'ice', '少冰', 0.00); +INSERT INTO `product_custom` VALUES (31, 2, 'ice', '正常', 0.00); +INSERT INTO `product_custom` VALUES (32, 2, 'ice', '多冰', 0.00); + +-- ---------------------------- +-- Table structure for product_spec +-- ---------------------------- +DROP TABLE IF EXISTS `product_spec`; +CREATE TABLE `product_spec` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `product_id` bigint NOT NULL COMMENT '商品ID', + `spec_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '规格名称(大杯/中杯/小杯)', + `price_adjust` decimal(10, 2) NULL DEFAULT 0.00 COMMENT '价格调整(相对于基础价格)', + PRIMARY KEY (`id`) USING BTREE, + INDEX `product_id`(`product_id` ASC) USING BTREE, + CONSTRAINT `product_spec_ibfk_1` FOREIGN KEY (`product_id`) REFERENCES `product` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 12 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '商品规格表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of product_spec +-- ---------------------------- +INSERT INTO `product_spec` VALUES (7, 1, '大杯', 2.00); +INSERT INTO `product_spec` VALUES (8, 1, '中杯', 0.00); +INSERT INTO `product_spec` VALUES (9, 2, '大杯', 3.00); +INSERT INTO `product_spec` VALUES (10, 2, '中杯', 2.00); +INSERT INTO `product_spec` VALUES (11, 2, '小杯', 0.00); + +-- ---------------------------- +-- Table structure for product_topping +-- ---------------------------- +DROP TABLE IF EXISTS `product_topping`; +CREATE TABLE `product_topping` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `product_id` bigint NOT NULL COMMENT '商品ID', + `topping_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '配料名称', + `price` decimal(10, 2) NULL DEFAULT 0.00 COMMENT '配料价格', + PRIMARY KEY (`id`) USING BTREE, + INDEX `product_id`(`product_id` ASC) USING BTREE, + CONSTRAINT `product_topping_ibfk_1` FOREIGN KEY (`product_id`) REFERENCES `product` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '配料表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of product_topping +-- ---------------------------- +INSERT INTO `product_topping` VALUES (4, 1, '椰果', 1.20); +INSERT INTO `product_topping` VALUES (5, 2, '更多的百香果', 1.20); +INSERT INTO `product_topping` VALUES (6, 2, '椰果', 2.00); +INSERT INTO `product_topping` VALUES (7, 2, '布丁', 1.98); + +-- ---------------------------- +-- Table structure for review +-- ---------------------------- +DROP TABLE IF EXISTS `review`; +CREATE TABLE `review` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL COMMENT '用户ID', + `order_id` bigint NOT NULL COMMENT '订单ID', + `product_id` bigint NOT NULL COMMENT '商品ID', + `rating` tinyint NOT NULL COMMENT '评分:1-5', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '评价内容', + `reply` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '商家回复', + `create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + INDEX `user_id`(`user_id` ASC) USING BTREE, + INDEX `order_id`(`order_id` ASC) USING BTREE, + INDEX `product_id`(`product_id` ASC) USING BTREE, + CONSTRAINT `review_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT, + CONSTRAINT `review_ibfk_2` FOREIGN KEY (`order_id`) REFERENCES `order` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT, + CONSTRAINT `review_ibfk_3` FOREIGN KEY (`product_id`) REFERENCES `product` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '评价表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of review +-- ---------------------------- +INSERT INTO `review` VALUES (1, 1, 2, 1, 5, '1', '感谢支持', '2026-01-09 16:59:29'); +INSERT INTO `review` VALUES (2, 1, 1, 1, 3, '2', NULL, '2026-01-10 14:06:42'); + +-- ---------------------------- +-- Table structure for user +-- ---------------------------- +DROP TABLE IF EXISTS `user`; +CREATE TABLE `user` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户名', + `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '密码(加密)', + `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '姓名', + `gender` tinyint NULL DEFAULT NULL COMMENT '性别:0-女,1-男', + `phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '手机号', + `email` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '邮箱', + `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '头像路径', + `address` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '默认地址', + `create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `username`(`username` ASC) USING BTREE, + INDEX `idx_user_username`(`username` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '用户表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of user +-- ---------------------------- +INSERT INTO `user` VALUES (1, 'l', '4297f44b13955235245b2497399d7a93', 'lq', NULL, '15100000000', 'aaa@qq.com', NULL, NULL, '2026-01-09 12:52:08', '2026-01-09 12:52:08'); +INSERT INTO `user` VALUES (2, 'l1', '4297f44b13955235245b2497399d7a93', '测试1', NULL, '15100000000', '', NULL, NULL, '2026-01-10 14:14:29', '2026-01-10 14:14:29'); + +-- ---------------------------- +-- Table structure for user_address +-- ---------------------------- +DROP TABLE IF EXISTS `user_address`; +CREATE TABLE `user_address` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL COMMENT '用户ID', + `address` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '详细地址', + `contact_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '联系人姓名', + `contact_phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '联系人电话', + `is_default` tinyint NULL DEFAULT 0 COMMENT '是否默认:0-否,1-是', + `create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + INDEX `user_id`(`user_id` ASC) USING BTREE, + CONSTRAINT `user_address_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '用户地址表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of user_address +-- ---------------------------- +INSERT INTO `user_address` VALUES (1, 1, 'c街道101号', '李二狗', '15100000000', 1, '2026-01-09 15:03:58'); +INSERT INTO `user_address` VALUES (2, 1, '1111111', '李二狗2', '13100000000', 0, '2026-01-10 14:06:16'); +INSERT INTO `user_address` VALUES (3, 2, '1\n1', '测试', '13111111111', 0, '2026-01-10 16:04:58'); +INSERT INTO `user_address` VALUES (4, 2, '1', '测试', '15100000000', 0, '2026-01-10 16:08:06'); + +-- ---------------------------- +-- Table structure for user_behavior +-- ---------------------------- +DROP TABLE IF EXISTS `user_behavior`; +CREATE TABLE `user_behavior` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL COMMENT '用户ID', + `product_id` bigint NOT NULL COMMENT '商品ID', + `behavior_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '行为类型:VIEW-浏览,PURCHASE-购买,RATING-评分', + `score` decimal(5, 2) NULL DEFAULT 0.00 COMMENT '评分(0-5)', + `create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_user_behavior_user`(`user_id` ASC) USING BTREE, + INDEX `idx_user_behavior_product`(`product_id` ASC) USING BTREE, + CONSTRAINT `user_behavior_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT, + CONSTRAINT `user_behavior_ibfk_2` FOREIGN KEY (`product_id`) REFERENCES `product` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '用户行为表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of user_behavior +-- ---------------------------- +INSERT INTO `user_behavior` VALUES (1, 1, 1, 'PURCHASE', 3.00, '2026-01-09 15:03:00'); +INSERT INTO `user_behavior` VALUES (2, 2, 2, 'PURCHASE', 3.00, '2026-01-10 16:05:04'); +INSERT INTO `user_behavior` VALUES (3, 1, 2, 'PURCHASE', 3.00, '2026-01-10 16:17:46'); + +-- ---------------------------- +-- Table structure for user_similarity +-- ---------------------------- +DROP TABLE IF EXISTS `user_similarity`; +CREATE TABLE `user_similarity` ( + `user1_id` bigint NOT NULL COMMENT '用户1ID', + `user2_id` bigint NOT NULL COMMENT '用户2ID', + `similarity_score` decimal(10, 6) NOT NULL COMMENT '相似度分数', + `update_time` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`user1_id`, `user2_id`) USING BTREE, + INDEX `user2_id`(`user2_id` ASC) USING BTREE, + CONSTRAINT `user_similarity_ibfk_1` FOREIGN KEY (`user1_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT, + CONSTRAINT `user_similarity_ibfk_2` FOREIGN KEY (`user2_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '用户相似度缓存表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of user_similarity +-- ---------------------------- +INSERT INTO `user_similarity` VALUES (1, 2, 1.000000, '2026-01-10 16:17:46'); + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/系统测试报告.md b/系统测试报告.md new file mode 100644 index 0000000..9305352 --- /dev/null +++ b/系统测试报告.md @@ -0,0 +1,584 @@ +# 奶茶店管理系统测试报告 + +## 一、测试概述 + +### 1.1 测试目的 +对奶茶店管理系统进行全面功能测试,识别未完成的功能模块和存在缺陷的功能,确保系统质量和安全性。 + +### 1.2 测试范围 +- 用户端功能模块(9个模块) +- 商家端功能模块(9个模块) +- 权限验证和数据安全 +- 功能完整性和正确性 + +### 1.3 测试时间 +2024年测试 + +### 1.4 测试环境 +- 开发环境:Spring Boot 2.7.18 +- 数据库:MySQL 8.0+ +- 前端:Bootstrap 5 + Thymeleaf + +--- + +## 二、已修复的问题 + +### 2.1 购物车权限验证问题(已修复) + +**问题类型**:功能缺陷 - 安全漏洞 + +**问题描述**: +- `CartController.updateQuantity()` 方法缺少用户权限验证,任何用户都可以修改其他用户的购物车商品数量 +- `CartController.deleteCartItem()` 方法缺少用户权限验证,任何用户都可以删除其他用户的购物车商品 + +**复现步骤**: +1. 用户A登录系统,添加商品到购物车 +2. 用户B登录系统 +3. 用户B通过API直接调用 `/cart/{用户A的购物车ID}` 进行修改或删除操作 +4. 操作成功,用户A的购物车被修改/删除 + +**预期行为**: +- 用户只能修改/删除自己的购物车商品 +- 尝试操作其他用户的购物车应返回权限错误 + +**实际行为**: +- 用户可以修改/删除任何用户的购物车商品 + +**严重程度**:高 + +**相关文件**: +- `src/main/java/com/nlshop/controller/user/CartController.java` +- `src/main/java/com/nlshop/service/user/CartService.java` + +**修复方案**: +- 在 `CartService.updateQuantity()` 和 `deleteCartItem()` 方法中添加用户ID参数 +- 在Service层验证购物车项是否属于当前用户 +- 在Controller层传递userId参数 + +--- + +### 2.2 订单支付权限验证问题(已修复) + +**问题类型**:功能缺陷 - 安全漏洞 + +**问题描述**: +- `OrderService.payOrder()` 方法缺少用户权限验证,任何用户都可以支付其他用户的订单 + +**复现步骤**: +1. 用户A创建订单 +2. 用户B登录系统 +3. 用户B通过API调用 `/order/{用户A的订单ID}/pay` 进行支付 +4. 支付成功,用户A的订单被支付 + +**预期行为**: +- 用户只能支付自己的订单 +- 尝试支付其他用户的订单应返回权限错误 + +**实际行为**: +- 用户可以支付任何用户的订单 + +**严重程度**:高 + +**相关文件**: +- `src/main/java/com/nlshop/controller/user/OrderController.java` +- `src/main/java/com/nlshop/service/user/OrderService.java` + +**修复方案**: +- 在 `OrderService.payOrder()` 方法中添加userId参数 +- 在Service层验证订单是否属于当前用户 + +--- + +### 2.3 地址管理权限验证问题(已修复) + +**问题类型**:功能缺陷 - 安全漏洞 + +**问题描述**: +- `UserService.updateAddress()` 和 `deleteAddress()` 方法缺少用户权限验证,任何用户都可以修改/删除其他用户的收货地址 + +**复现步骤**: +1. 用户A添加收货地址 +2. 用户B登录系统 +3. 用户B通过API调用 `/address/{用户A的地址ID}` 进行修改或删除 +4. 操作成功,用户A的地址被修改/删除 + +**预期行为**: +- 用户只能修改/删除自己的收货地址 +- 尝试操作其他用户的地址应返回权限错误 + +**实际行为**: +- 用户可以修改/删除任何用户的收货地址 + +**严重程度**:高 + +**相关文件**: +- `src/main/java/com/nlshop/controller/user/UserController.java` +- `src/main/java/com/nlshop/service/user/UserService.java` + +**修复方案**: +- 在Service层方法中添加userId参数 +- 验证地址是否属于当前用户 +- 在Controller层传递userId参数 + +--- + +## 三、已实现的新功能 + +### 3.1 购物车批量删除功能(已实现) + +**功能描述**: +实现了购物车批量删除功能,包括: +- 全选/反选功能 +- 单个商品复选框选择 +- 批量删除按钮(仅在选择商品时显示) +- 批量删除确认提示 +- 删除后实时更新购物车数量和总金额 + +**实现文件**: +- 后端: + - `src/main/java/com/nlshop/controller/user/CartController.java` - 添加批量删除接口 + - `src/main/java/com/nlshop/service/user/CartService.java` - 添加批量删除服务方法(含权限验证) + - `src/main/java/com/nlshop/mapper/CartMapper.java` - 添加批量删除Mapper方法 + - `src/main/resources/mapper/CartMapper.xml` - 添加批量删除SQL +- 前端: + - `src/main/resources/templates/user/cart.html` - 添加全选复选框、商品复选框、批量删除按钮和相关JavaScript逻辑 + +**功能特点**: +- 权限验证:确保只能删除当前用户的商品 +- 确认提示:批量删除前显示确认对话框,显示删除数量 +- 实时更新:删除成功后自动刷新购物车列表 +- 错误处理:妥善处理删除失败的情况 + +--- + +## 四、发现的其他问题 + +### 4.1 头像上传功能未完整实现 + +**问题类型**:未完成功能 + +**问题描述**: +- `UserController.uploadAvatar()` 方法只返回"上传成功"的固定消息,没有实际的文件上传处理逻辑 +- 缺少文件保存、路径返回等实现 + +**复现步骤**: +1. 用户登录系统 +2. 进入个人中心 +3. 尝试上传头像 +4. 虽然显示"上传成功",但实际文件未保存 + +**预期行为**: +- 上传头像文件 +- 保存文件到服务器 +- 更新用户头像路径 +- 返回文件访问路径 + +**实际行为**: +- 只返回固定成功消息,文件未实际保存 + +**严重程度**:中 + +**相关文件**: +- `src/main/java/com/nlshop/controller/user/UserController.java` (第82-91行) + +**建议修复**: +- 实现完整的文件上传逻辑 +- 使用 `FileUploadUtil` 保存文件 +- 更新用户表中的头像路径字段 + +--- + +### 4.2 订单跟踪接口缺少权限验证 + +**问题类型**:功能缺陷 - 安全漏洞 + +**问题描述**: +- `OrderController.getOrderTrack()` 方法缺少用户权限验证,任何用户都可以查看任何订单的跟踪信息 + +**复现步骤**: +1. 用户A创建订单 +2. 用户B登录系统 +3. 用户B通过API调用 `/order/orders/{用户A的订单ID}/track` 查看订单跟踪 +4. 成功获取用户A的订单跟踪信息 + +**预期行为**: +- 用户只能查看自己订单的跟踪信息 +- 尝试查看其他用户的订单跟踪应返回权限错误 + +**实际行为**: +- 用户可以查看任何订单的跟踪信息 + +**严重程度**:中 + +**相关文件**: +- `src/main/java/com/nlshop/controller/user/OrderController.java` (第73-77行) + +**建议修复**: +- 在 `OrderService.getOrderTrack()` 方法中添加userId参数 +- 验证订单是否属于当前用户 + +--- + +### 4.3 商品搜索功能缺失 + +**问题类型**:未完成功能 + +**问题描述**: +- 根据README和功能规划,系统应支持商品搜索功能 +- 但实际代码中未找到商品搜索的实现 +- `ProductController` 只有分类筛选,没有搜索功能 + +**复现步骤**: +1. 用户登录系统 +2. 在商品列表页面尝试搜索商品 +3. 无法找到搜索功能 + +**预期行为**: +- 提供商品搜索功能 +- 支持按商品名称、描述等关键词搜索 + +**实际行为**: +- 无搜索功能 + +**严重程度**:低 + +**相关文件**: +- `src/main/java/com/nlshop/controller/user/ProductController.java` +- `src/main/java/com/nlshop/service/user/ProductService.java` + +**建议修复**: +- 在 `ProductService` 中添加搜索方法 +- 在 `ProductController` 中添加搜索接口 +- 在前端添加搜索输入框和搜索按钮 + +--- + +### 4.4 订单取消功能缺失 + +**问题类型**:未完成功能 + +**问题描述**: +- 系统支持订单状态包括 `CANCELLED`(已取消) +- 但用户端没有提供取消订单的接口和功能 +- 用户无法主动取消自己的订单 + +**复现步骤**: +1. 用户创建订单 +2. 订单状态为 `PENDING_PAY`(待支付) +3. 用户想要取消订单 +4. 无法找到取消订单的功能 + +**预期行为**: +- 用户可以在订单支付前取消订单 +- 提供取消订单的接口和前端功能 + +**实际行为**: +- 无取消订单功能 + +**严重程度**:中 + +**相关文件**: +- `src/main/java/com/nlshop/controller/user/OrderController.java` +- `src/main/java/com/nlshop/service/user/OrderService.java` + +**建议修复**: +- 在 `OrderService` 中添加 `cancelOrder()` 方法 +- 在 `OrderController` 中添加取消订单接口 +- 在前端订单列表和订单详情页添加取消按钮 + +--- + +### 4.5 商家端订单拒单功能缺少状态验证 + +**问题类型**:功能缺陷 + +**问题描述**: +- `OrderManageService.rejectOrder()` 方法可能缺少订单状态的验证 +- 需要确认是否只能拒绝特定状态的订单(如 `PENDING_PAY` 或 `MAKING`) + +**复现步骤**: +1. 商家登录系统 +2. 查看订单列表 +3. 尝试拒绝不同状态的订单 +4. 检查是否所有状态的订单都可以被拒绝 + +**预期行为**: +- 只能拒绝特定状态的订单(如待支付、制作中) +- 已完成的订单不应允许拒绝 + +**实际行为**: +- 需要进一步测试确认 + +**严重程度**:低 + +**相关文件**: +- `src/main/java/com/nlshop/service/merchant/OrderManageService.java` + +**建议修复**: +- 检查并完善订单状态验证逻辑 +- 确保只能拒绝允许状态的订单 + +--- + +### 4.6 库存预警功能可能未完整实现 + +**问题类型**:未完成功能(需确认) + +**问题描述**: +- README中提到库存预警功能 +- 需要确认库存预警的触发机制、预警阈值设置、预警通知等是否完整实现 + +**复现步骤**: +1. 商家登录系统 +2. 进入库存管理页面 +3. 检查是否有库存预警功能 +4. 测试库存预警是否正常工作 + +**预期行为**: +- 当库存低于设定阈值时,系统应显示预警 +- 提供预警阈值设置功能 +- 预警信息清晰可见 + +**实际行为**: +- 需要进一步测试确认 + +**严重程度**:中 + +**相关文件**: +- `src/main/java/com/nlshop/controller/merchant/InventoryController.java` +- `src/main/java/com/nlshop/service/merchant/InventoryService.java` +- `src/main/resources/templates/merchant/inventory.html` + +**建议修复**: +- 检查库存预警功能的完整性 +- 如未实现,需要添加预警逻辑和前端展示 + +--- + +### 4.7 评价回复功能可能缺少权限验证 + +**问题类型**:功能缺陷 - 安全漏洞(需确认) + +**问题描述**: +- 商家端评价回复功能需要确认是否验证了评价是否属于当前商家的商品 +- 防止商家回复其他商家的商品评价 + +**复现步骤**: +1. 商家A登录系统 +2. 查看评价列表 +3. 尝试回复商家B的商品评价 +4. 检查是否成功 + +**预期行为**: +- 商家只能回复自己商品的评价 +- 尝试回复其他商家的评价应返回权限错误 + +**实际行为**: +- 需要进一步测试确认 + +**严重程度**:中 + +**相关文件**: +- `src/main/java/com/nlshop/controller/merchant/ReviewController.java` +- `src/main/java/com/nlshop/service/merchant/ReviewService.java` + +**建议修复**: +- 检查评价回复功能的权限验证 +- 确保商家只能回复自己商品的评价 + +--- + +## 五、功能完整性检查 + +### 5.1 用户端功能模块检查 + +| 功能模块 | 状态 | 说明 | +|---------|------|------| +| 用户注册 | ✅ 完成 | 功能正常 | +| 用户登录 | ✅ 完成 | 功能正常 | +| 用户登出 | ✅ 完成 | 功能正常 | +| 个人信息查看/编辑 | ✅ 完成 | 功能正常 | +| 头像上传 | ⚠️ 未完整 | 只返回固定消息,未实际保存文件 | +| 收货地址管理 | ✅ 完成 | 已修复权限验证问题 | +| 商品列表 | ✅ 完成 | 功能正常 | +| 商品详情 | ✅ 完成 | 功能正常 | +| 商品分类筛选 | ✅ 完成 | 功能正常 | +| 商品搜索 | ❌ 缺失 | 未实现搜索功能 | +| 购物车添加 | ✅ 完成 | 功能正常 | +| 购物车数量修改 | ✅ 完成 | 已修复权限验证问题 | +| 购物车单个删除 | ✅ 完成 | 已修复权限验证问题 | +| 购物车批量删除 | ✅ 完成 | 新实现功能 | +| 订单创建 | ✅ 完成 | 功能正常 | +| 订单支付 | ✅ 完成 | 已修复权限验证问题 | +| 订单列表 | ✅ 完成 | 功能正常 | +| 订单详情 | ✅ 完成 | 功能正常 | +| 订单跟踪 | ⚠️ 有缺陷 | 缺少权限验证 | +| 订单取消 | ❌ 缺失 | 未实现取消功能 | +| 个性化推荐 | ✅ 完成 | 功能正常 | +| 商品评价 | ✅ 完成 | 功能正常 | +| 评价列表 | ✅ 完成 | 功能正常 | +| 留言反馈 | ✅ 完成 | 功能正常 | +| 消息查看 | ✅ 完成 | 功能正常 | +| 公告列表 | ✅ 完成 | 功能正常 | +| 公告详情 | ✅ 完成 | 功能正常 | + +### 5.2 商家端功能模块检查 + +| 功能模块 | 状态 | 说明 | +|---------|------|------| +| 商家注册 | ✅ 完成 | 功能正常 | +| 商家登录 | ✅ 完成 | 功能正常 | +| 商家登出 | ✅ 完成 | 功能正常 | +| 商家信息管理 | ✅ 完成 | 功能正常 | +| 商品列表 | ✅ 完成 | 功能正常 | +| 商品新增 | ✅ 完成 | 功能正常 | +| 商品编辑 | ✅ 完成 | 功能正常 | +| 商品下架 | ✅ 完成 | 功能正常 | +| 商品规格管理 | ✅ 完成 | 功能正常 | +| 商品配料管理 | ✅ 完成 | 功能正常 | +| 商品定制选项管理 | ✅ 完成 | 功能正常 | +| 订单列表 | ✅ 完成 | 功能正常 | +| 订单接单 | ✅ 完成 | 功能正常 | +| 订单拒单 | ⚠️ 需确认 | 需检查状态验证 | +| 订单状态更新 | ✅ 完成 | 功能正常 | +| 库存列表 | ✅ 完成 | 功能正常 | +| 库存入库 | ✅ 完成 | 功能正常 | +| 库存出库 | ✅ 完成 | 功能正常 | +| 库存预警 | ⚠️ 需确认 | 需检查完整性 | +| 评价查看 | ✅ 完成 | 功能正常 | +| 评价回复 | ⚠️ 需确认 | 需检查权限验证 | +| 消息列表 | ✅ 完成 | 功能正常 | +| 消息回复 | ✅ 完成 | 功能正常 | +| 公告列表 | ✅ 完成 | 功能正常 | +| 公告发布 | ✅ 完成 | 功能正常 | +| 公告编辑 | ✅ 完成 | 功能正常 | +| 公告删除 | ✅ 完成 | 功能正常 | +| 用户列表 | ✅ 完成 | 功能正常 | +| 用户信息查看 | ✅ 完成 | 功能正常 | +| 销量统计 | ✅ 完成 | 功能正常 | +| 营收统计 | ✅ 完成 | 功能正常 | +| 热门商品统计 | ✅ 完成 | 功能正常 | + +--- + +## 六、安全性问题总结 + +### 6.1 已修复的安全问题 + +1. **购物车权限验证漏洞**(高) - 已修复 +2. **订单支付权限验证漏洞**(高) - 已修复 +3. **地址管理权限验证漏洞**(高) - 已修复 + +### 6.2 待修复的安全问题 + +1. **订单跟踪权限验证漏洞**(中) - 待修复 +2. **评价回复权限验证**(中) - 需确认 + +--- + +## 七、改进建议 + +### 7.1 功能完善建议 + +1. **实现头像上传功能** + - 完善文件上传逻辑 + - 添加文件类型和大小验证 + - 实现文件保存和路径更新 + +2. **实现商品搜索功能** + - 添加搜索接口 + - 实现关键词搜索逻辑 + - 前端添加搜索UI + +3. **实现订单取消功能** + - 添加取消订单接口 + - 实现订单状态验证 + - 前端添加取消按钮 + +4. **完善库存预警功能** + - 检查预警逻辑完整性 + - 添加预警阈值设置 + - 优化预警展示 + +### 7.2 安全性改进建议 + +1. **统一权限验证机制** + - 考虑使用AOP或拦截器统一处理权限验证 + - 减少重复代码,提高安全性 + +2. **完善订单跟踪权限验证** + - 添加用户权限检查 + +3. **完善评价回复权限验证** + - 确保商家只能回复自己商品的评价 + +### 7.3 代码质量改进建议 + +1. **异常处理** + - 统一异常处理机制 + - 使用自定义异常类 + - 完善错误消息 + +2. **代码规范** + - 统一代码风格 + - 添加必要的注释 + - 优化方法命名 + +3. **测试覆盖** + - 添加单元测试 + - 添加集成测试 + - 提高测试覆盖率 + +--- + +## 八、测试结论 + +### 8.1 总体评价 + +系统整体功能较为完整,核心业务流程已实现。但在权限验证方面存在一些安全漏洞,部分功能未完整实现。经过本次测试和修复,已解决了3个高危安全漏洞,并实现了购物车批量删除功能。 + +### 8.2 主要成果 + +1. ✅ 修复了3个高危权限验证漏洞 +2. ✅ 实现了购物车批量删除功能 +3. ✅ 完善了权限验证机制 +4. ✅ 识别了未完成功能和待改进点 + +### 8.3 后续工作 + +1. 修复订单跟踪权限验证问题 +2. 实现头像上传功能 +3. 实现商品搜索功能 +4. 实现订单取消功能 +5. 完善库存预警功能 +6. 检查并修复评价回复权限验证 + +--- + +## 九、附录 + +### 9.1 测试文件清单 + +- 用户端控制器:8个 +- 商家端控制器:9个 +- 服务类:16个 +- 前端页面:18个 + +### 9.2 修改文件清单 + +**本次修复和新增的文件:** +1. `src/main/java/com/nlshop/controller/user/CartController.java` - 修复权限验证,添加批量删除 +2. `src/main/java/com/nlshop/service/user/CartService.java` - 修复权限验证,添加批量删除 +3. `src/main/java/com/nlshop/mapper/CartMapper.java` - 添加批量删除方法 +4. `src/main/resources/mapper/CartMapper.xml` - 添加批量删除SQL +5. `src/main/resources/templates/user/cart.html` - 添加批量删除UI +6. `src/main/java/com/nlshop/controller/user/OrderController.java` - 修复支付权限验证 +7. `src/main/java/com/nlshop/service/user/OrderService.java` - 修复支付权限验证 +8. `src/main/java/com/nlshop/controller/user/UserController.java` - 修复地址管理权限验证 +9. `src/main/java/com/nlshop/service/user/UserService.java` - 修复地址管理权限验证 + +--- + +**报告生成时间**:2024年 +**测试人员**:AI Assistant +**审核状态**:待审核