初始化v1

This commit is contained in:
2026-01-11 18:14:42 +08:00
commit 641b03e8e2
297 changed files with 28656 additions and 0 deletions

8
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

18
.idea/compiler.xml generated Normal file
View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<annotationProcessing>
<profile name="Maven default annotation processors profile" enabled="true">
<sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
<module name="nl-shop" />
</profile>
</annotationProcessing>
</component>
<component name="JavacSettings">
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
<module name="nl-shop" options="-parameters" />
</option>
</component>
</project>

6
.idea/encodings.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
</component>
</project>

20
.idea/jarRepositories.xml generated Normal file
View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="https://repo.maven.apache.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
</component>
</project>

12
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_18" default="true" project-jdk-name="corretto-18" project-jdk-type="JavaSDK" />
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

118
1 Normal file
View File

@@ -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格式报表
所有功能模块需保证界面友好、操作便捷、响应迅速,并确保数据安全与用户隐私保护。

171
README.md Normal file
View File

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

157
pom.xml Normal file
View File

@@ -0,0 +1,157 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
<relativePath/>
</parent>
<groupId>com.nlshop</groupId>
<artifactId>nl-shop</artifactId>
<version>1.0.0</version>
<name>nl-shop</name>
<description>奶茶店管理系统</description>
<properties>
<java.version>1.8</java.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<mybatis-plus.version>3.5.3.1</mybatis-plus.version>
<pagehelper.version>1.4.6</pagehelper.version>
<jjwt.version>0.11.5</jjwt.version>
</properties>
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Thymeleaf -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- MyBatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.1</version>
</dependency>
<!-- MySQL -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<!-- PageHelper -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>${pagehelper.version}</version>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jjwt.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${jjwt.version}</version>
<scope>runtime</scope>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Commons Lang -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- JSON处理 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.43</version>
</dependency>
<!-- 文件上传 -->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.5</version>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Spring Boot Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

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

View File

@@ -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<ApplicationReadyEvent> {
@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<String> userPages = new ArrayList<>();
List<String> userApis = new ArrayList<>();
List<String> merchantPages = new ArrayList<>();
List<String> merchantApis = new ArrayList<>();
Map<RequestMappingInfo, HandlerMethod> handlerMethods = mapping.getHandlerMethods();
for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : handlerMethods.entrySet()) {
RequestMappingInfo mappingInfo = entry.getKey();
HandlerMethod handlerMethod = entry.getValue();
// 检查patternsCondition是否为null
if (mappingInfo.getPatternsCondition() == null) {
continue;
}
Set<String> 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<String> 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<String> 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();
}
}

View File

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

View File

@@ -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<Announcement> 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<List<Announcement>> getAnnouncements(HttpServletRequest request) {
Long merchantId = (Long) request.getAttribute("userId");
if (merchantId == null) {
return ApiResponse.error("请先登录");
}
List<Announcement> announcements = announcementService.getAnnouncements(merchantId);
return ApiResponse.success(announcements);
}
@GetMapping("/{id}")
public ApiResponse<Announcement> getAnnouncement(@PathVariable Long id) {
Announcement announcement = announcementService.getAnnouncement(id);
return ApiResponse.success(announcement);
}
@PutMapping("/{id}")
public ApiResponse<Void> updateAnnouncement(@PathVariable Long id,
@RequestBody Announcement announcement) {
announcement.setId(id);
announcementService.updateAnnouncement(announcement);
return ApiResponse.success(null);
}
@DeleteMapping("/{id}")
public ApiResponse<Void> deleteAnnouncement(@PathVariable Long id) {
announcementService.deleteAnnouncement(id);
return ApiResponse.success(null);
}
}

View File

@@ -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<List<Inventory>> getInventory(HttpServletRequest request) {
Long merchantId = (Long) request.getAttribute("userId");
if (merchantId == null) {
return ApiResponse.error("请先登录");
}
List<Inventory> inventory = inventoryService.getInventory(merchantId);
return ApiResponse.success(inventory);
}
@PostMapping
public ApiResponse<Inventory> 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<Void> stockIn(@PathVariable Long id,
@RequestParam BigDecimal quantity,
@RequestParam String operator) {
inventoryService.stockIn(id, quantity, operator);
return ApiResponse.success(null);
}
@PostMapping("/{id}/out")
public ApiResponse<Void> stockOut(@PathVariable Long id,
@RequestParam BigDecimal quantity,
@RequestParam String operator) {
inventoryService.stockOut(id, quantity, operator);
return ApiResponse.success(null);
}
@GetMapping("/alerts")
public ApiResponse<List<Inventory>> getLowStockAlerts(HttpServletRequest request) {
Long merchantId = (Long) request.getAttribute("userId");
if (merchantId == null) {
return ApiResponse.error("请先登录");
}
List<Inventory> alerts = inventoryService.getLowStockAlerts(merchantId);
return ApiResponse.success(alerts);
}
@PutMapping("/{id}")
public ApiResponse<Void> updateInventory(@PathVariable Long id,
@RequestBody Inventory inventory) {
inventory.setId(id);
inventoryService.updateInventory(inventory);
return ApiResponse.success(null);
}
@DeleteMapping("/{id}")
public ApiResponse<Void> deleteInventory(@PathVariable Long id) {
inventoryService.deleteInventory(id);
return ApiResponse.success(null);
}
}

View File

@@ -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<Merchant> register(@Validated @RequestBody MerchantRegisterRequest request) {
Merchant merchant = merchantService.register(request);
return ApiResponse.success(merchant);
}
@PostMapping("/login")
public ApiResponse<LoginResponse> 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<Void> logout(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session != null) {
session.invalidate();
}
return ApiResponse.success(null);
}
@GetMapping("/profile")
public ApiResponse<Merchant> 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<Merchant> 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);
}
}

View File

@@ -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<List<Message>> getMessages(HttpServletRequest request) {
Long merchantId = (Long) request.getAttribute("userId");
if (merchantId == null) {
return ApiResponse.error("请先登录");
}
List<Message> messages = messageService.getMessages(merchantId);
return ApiResponse.success(messages);
}
@GetMapping("/{id}")
public ApiResponse<Message> getMessage(@PathVariable Long id) {
Message message = messageService.getMessage(id);
return ApiResponse.success(message);
}
@PostMapping("/{id}/reply")
public ApiResponse<Void> replyMessage(@PathVariable Long id,
@RequestParam String reply) {
messageService.replyMessage(id, reply);
return ApiResponse.success(null);
}
}

View File

@@ -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<PageResponse<Order>> 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<Order> response = orderManageService.getOrders(merchantId, status, pageNum, pageSize);
return ApiResponse.success(response);
}
@GetMapping("/{id}")
public ApiResponse<OrderDetailResponse> 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<Void> 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<Void> 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<Void> 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);
}
}

View File

@@ -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<PageResponse<Product>> 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<Product> response = productManageService.getProducts(merchantId, pageNum, pageSize);
return ApiResponse.success(response);
}
@GetMapping("/{id}")
public ApiResponse<Map<String, Object>> getProduct(@PathVariable Long id,
HttpServletRequest request) {
Long merchantId = (Long) request.getAttribute("userId");
if (merchantId == null) {
return ApiResponse.error("请先登录");
}
Map<String, Object> product = productManageService.getProduct(id, merchantId);
return ApiResponse.success(product);
}
@PostMapping
public ApiResponse<Product> 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<Product> 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<Void> 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<Void> 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<List<ProductSpec>> getSpecs(@PathVariable Long id) {
List<ProductSpec> specs = productManageService.getSpecs(id);
return ApiResponse.success(specs);
}
@PostMapping("/{id}/specs")
public ApiResponse<ProductSpec> addSpec(@PathVariable Long id,
@RequestBody ProductSpec spec) {
ProductSpec added = productManageService.addSpec(id, spec);
return ApiResponse.success(added);
}
@PutMapping("/specs/{specId}")
public ApiResponse<Void> updateSpec(@PathVariable Long specId,
@RequestBody ProductSpec spec) {
spec.setId(specId);
productManageService.updateSpec(spec);
return ApiResponse.success(null);
}
@DeleteMapping("/specs/{specId}")
public ApiResponse<Void> deleteSpec(@PathVariable Long specId) {
productManageService.deleteSpec(specId);
return ApiResponse.success(null);
}
@GetMapping("/{id}/toppings")
public ApiResponse<List<ProductTopping>> getToppings(@PathVariable Long id) {
List<ProductTopping> toppings = productManageService.getToppings(id);
return ApiResponse.success(toppings);
}
@PostMapping("/{id}/toppings")
public ApiResponse<ProductTopping> addTopping(@PathVariable Long id,
@RequestBody ProductTopping topping) {
ProductTopping added = productManageService.addTopping(id, topping);
return ApiResponse.success(added);
}
@PutMapping("/toppings/{toppingId}")
public ApiResponse<Void> updateTopping(@PathVariable Long toppingId,
@RequestBody ProductTopping topping) {
topping.setId(toppingId);
productManageService.updateTopping(topping);
return ApiResponse.success(null);
}
@DeleteMapping("/toppings/{toppingId}")
public ApiResponse<Void> deleteTopping(@PathVariable Long toppingId) {
productManageService.deleteTopping(toppingId);
return ApiResponse.success(null);
}
@GetMapping("/{id}/customs")
public ApiResponse<List<ProductCustom>> getCustoms(@PathVariable Long id) {
List<ProductCustom> customs = productManageService.getCustoms(id);
return ApiResponse.success(customs);
}
@PostMapping("/{id}/customs")
public ApiResponse<ProductCustom> addCustom(@PathVariable Long id,
@RequestBody ProductCustom custom) {
ProductCustom added = productManageService.addCustom(id, custom);
return ApiResponse.success(added);
}
@PutMapping("/customs/{customId}")
public ApiResponse<Void> updateCustom(@PathVariable Long customId,
@RequestBody ProductCustom custom) {
custom.setId(customId);
productManageService.updateCustom(custom);
return ApiResponse.success(null);
}
@DeleteMapping("/customs/{customId}")
public ApiResponse<Void> deleteCustom(@PathVariable Long customId) {
productManageService.deleteCustom(customId);
return ApiResponse.success(null);
}
}

View File

@@ -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<List<Review>> getReviews(HttpServletRequest request) {
Long merchantId = (Long) request.getAttribute("userId");
if (merchantId == null) {
return ApiResponse.error("请先登录");
}
List<Review> reviews = reviewService.getReviews(merchantId);
return ApiResponse.success(reviews);
}
@GetMapping("/{id}")
public ApiResponse<Review> getReview(@PathVariable Long id) {
Review review = reviewService.getReview(id);
return ApiResponse.success(review);
}
@PostMapping("/{id}/reply")
public ApiResponse<Void> 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<Void> 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());
}
}
}

View File

@@ -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<Map<String, Object>> getSalesStatistics(
@RequestParam(defaultValue = "day") String period,
HttpServletRequest request) {
Long merchantId = (Long) request.getAttribute("userId");
if (merchantId == null) {
return ApiResponse.error("请先登录");
}
Map<String, Object> stats = statisticsService.getSalesStatistics(merchantId, period);
return ApiResponse.success(stats);
}
@GetMapping("/revenue")
public ApiResponse<Map<String, Object>> getRevenueStatistics(
@RequestParam(defaultValue = "day") String period,
HttpServletRequest request) {
Long merchantId = (Long) request.getAttribute("userId");
if (merchantId == null) {
return ApiResponse.error("请先登录");
}
Map<String, Object> stats = statisticsService.getRevenueStatistics(merchantId, period);
return ApiResponse.success(stats);
}
@GetMapping("/hot-products")
public ApiResponse<List<Map<String, Object>>> getHotProducts(
@RequestParam(defaultValue = "10") int limit,
HttpServletRequest request) {
Long merchantId = (Long) request.getAttribute("userId");
if (merchantId == null) {
return ApiResponse.error("请先登录");
}
List<Map<String, Object>> products = statisticsService.getHotProducts(merchantId, limit);
return ApiResponse.success(products);
}
@GetMapping("/active-users")
public ApiResponse<List<Map<String, Object>>> getActiveUsers(
@RequestParam(defaultValue = "30") int days,
HttpServletRequest request) {
Long merchantId = (Long) request.getAttribute("userId");
if (merchantId == null) {
return ApiResponse.error("请先登录");
}
List<Map<String, Object>> users = statisticsService.getActiveUsers(merchantId, days);
return ApiResponse.success(users);
}
@GetMapping("/sticky-users")
public ApiResponse<List<Map<String, Object>>> 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<Map<String, Object>> users = statisticsService.getStickyUsers(merchantId, days, minOrders);
return ApiResponse.success(users);
}
}

View File

@@ -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<PageResponse<User>> 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<User> response = userManageService.getUsers(pageNum, pageSize);
return ApiResponse.success(response);
}
}

View File

@@ -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<List<Announcement>> getAnnouncements() {
List<Announcement> announcements = announcementService.getPublishedAnnouncements();
return ApiResponse.success(announcements);
}
}

View File

@@ -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<CartItem> 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<List<CartItem>> getCart(HttpServletRequest request) {
Long userId = (Long) request.getAttribute("userId");
if (userId == null) {
return ApiResponse.error("请先登录");
}
List<CartItem> items = cartService.getCart(userId);
return ApiResponse.success(items);
}
@PutMapping("/{id}")
public ApiResponse<Void> 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<Void> 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<Void> deleteBatch(@RequestBody List<Long> 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<Void> syncCart(@RequestBody List<CartAddRequest> 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); // 返回成功,避免影响登录流程
}
}
}

View File

@@ -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<Message> 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<List<Message>> getMessages(HttpServletRequest request) {
Long userId = (Long) request.getAttribute("userId");
if (userId == null) {
return ApiResponse.error("请先登录");
}
List<Message> messages = messageService.getMessages(userId);
return ApiResponse.success(messages);
}
}

View File

@@ -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<Order> 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<Void> 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<List<Order>> getOrders(@RequestParam(required = false) String status,
HttpServletRequest request) {
Long userId = (Long) request.getAttribute("userId");
if (userId == null) {
return ApiResponse.error("请先登录");
}
List<Order> orders = orderService.getOrders(userId, status);
return ApiResponse.success(orders);
}
@GetMapping("/orders/{id}")
public ApiResponse<OrderDetailResponse> 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<List<OrderTrack>> getOrderTrack(@PathVariable Long id,
HttpServletRequest request) {
Long userId = (Long) request.getAttribute("userId");
if (userId == null) {
return ApiResponse.error("请先登录");
}
try {
List<OrderTrack> tracks = orderService.getOrderTrack(userId, id);
return ApiResponse.success(tracks);
} catch (BusinessException e) {
return ApiResponse.error(e.getMessage());
}
}
}

View File

@@ -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<PageResponse<Product>> getProducts(
@RequestParam(required = false) String category,
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize) {
PageResponse<Product> response = productService.getProducts(category, pageNum, pageSize);
return ApiResponse.success(response);
}
@GetMapping("/{id}")
public ApiResponse<Map<String, Object>> getProductDetail(@PathVariable Long id,
HttpServletRequest request) {
Long userId = null;
try {
userId = (Long) request.getAttribute("userId");
} catch (Exception e) {
// 未登录用户也可以查看商品详情
}
Map<String, Object> detail = productService.getProductDetail(id, userId);
return ApiResponse.success(detail);
}
@GetMapping("/hot")
public ApiResponse<List<Product>> getHotProducts(@RequestParam(defaultValue = "10") int limit) {
List<Product> products = productService.getHotProducts(limit);
return ApiResponse.success(products);
}
/**
* 批量记录浏览行为用于登录后同步cookies中的浏览历史
*/
@PostMapping("/record-views")
public ApiResponse<Void> recordViews(@RequestBody List<Long> 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);
}
}

View File

@@ -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<List<Product>> getRecommendations(
@RequestParam(defaultValue = "10") int limit,
HttpServletRequest request) {
Long userId = (Long) request.getAttribute("userId");
// 推荐功能可以允许未登录用户userId可以为null
List<Product> products = recommendService.getRecommendations(userId, limit);
return ApiResponse.success(products);
}
}

View File

@@ -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<Review> 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<List<Review>> getReviewsByOrders(@RequestParam String orderIds,
HttpServletRequest request) {
Long userId = (Long) request.getAttribute("userId");
if (userId == null) {
return ApiResponse.error("请先登录");
}
try {
List<Long> orderIdList = Arrays.stream(orderIds.split(","))
.map(Long::parseLong)
.collect(Collectors.toList());
List<Review> reviews = reviewMapper.selectByOrderIdList(orderIdList);
return ApiResponse.success(reviews);
} catch (Exception e) {
return ApiResponse.error("参数错误");
}
}
@GetMapping("/by-order")
public ApiResponse<Review> 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);
}
}

View File

@@ -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<User> register(@Validated @RequestBody UserRegisterRequest request) {
User user = userService.register(request);
return ApiResponse.success(user);
}
@PostMapping("/login")
public ApiResponse<LoginResponse> 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<Void> logout(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session != null) {
session.invalidate();
}
return ApiResponse.success(null);
}
@GetMapping("/profile")
public ApiResponse<User> 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<User> 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<String> 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<List<UserAddress>> getAddresses(HttpServletRequest request) {
Long userId = (Long) request.getAttribute("userId");
if (userId == null) {
return ApiResponse.error("请先登录");
}
List<UserAddress> addresses = userService.getAddresses(userId);
return ApiResponse.success(addresses);
}
@PostMapping("/address")
public ApiResponse<UserAddress> 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<UserAddress> 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<Void> 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());
}
}
}

View File

@@ -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格式
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,40 @@
package com.nlshop.dto.response;
import lombok.Data;
@Data
public class ApiResponse<T> {
private Integer code;
private String message;
private T data;
public static <T> ApiResponse<T> success(T data) {
ApiResponse<T> response = new ApiResponse<>();
response.setCode(200);
response.setMessage("操作成功");
response.setData(data);
return response;
}
public static <T> ApiResponse<T> success(String message, T data) {
ApiResponse<T> response = new ApiResponse<>();
response.setCode(200);
response.setMessage(message);
response.setData(data);
return response;
}
public static <T> ApiResponse<T> error(String message) {
ApiResponse<T> response = new ApiResponse<>();
response.setCode(500);
response.setMessage(message);
return response;
}
public static <T> ApiResponse<T> error(Integer code, String message) {
ApiResponse<T> response = new ApiResponse<>();
response.setCode(code);
response.setMessage(message);
return response;
}
}

View File

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

View File

@@ -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<OrderItem> items;
private List<OrderTrack> tracks;
private Map<Long, String> productImages; // 商品ID到图片URL的映射
public static OrderDetailResponse of(Order order, List<OrderItem> items, List<OrderTrack> tracks, Map<Long, String> productImages) {
OrderDetailResponse response = new OrderDetailResponse();
response.setOrder(order);
response.setItems(items);
response.setTracks(tracks);
response.setProductImages(productImages);
return response;
}
}

View File

@@ -0,0 +1,23 @@
package com.nlshop.dto.response;
import lombok.Data;
import java.util.List;
@Data
public class PageResponse<T> {
private List<T> list;
private Long total;
private Integer pageNum;
private Integer pageSize;
private Integer pages;
public static <T> PageResponse<T> of(List<T> list, Long total, Integer pageNum, Integer pageSize) {
PageResponse<T> 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;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<ConstraintViolation<?>> violations = e.getConstraintViolations();
String message = violations.iterator().next().getMessage();
return ApiResponse.error(400, message);
}
}

View File

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

View File

@@ -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<Announcement> selectByMerchantId(Long merchantId);
List<Announcement> selectPublished();
Announcement selectById(Long id);
int update(Announcement announcement);
int delete(Long id);
}

View File

@@ -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<CartItem> 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<Long> ids, @Param("userId") Long userId);
}

View File

@@ -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<InventoryLog> selectByInventoryId(Long inventoryId);
}

View File

@@ -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<Inventory> 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<Inventory> selectLowStock(Long merchantId);
}

View File

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

View File

@@ -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<Message> selectByUserId(Long userId);
List<Message> selectByMerchantId(Long merchantId);
Message selectById(Long id);
int update(Message message);
}

View File

@@ -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<OrderItem> selectByOrderId(Long orderId);
OrderItem selectById(Long id);
}

View File

@@ -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<Order> selectByUserId(@Param("userId") Long userId, @Param("status") String status);
List<Order> 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<java.util.Map<String, Object>> selectStickyUsers(@Param("merchantId") Long merchantId,
@Param("days") int days,
@Param("minOrders") int minOrders);
}

View File

@@ -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<OrderTrack> selectByOrderId(Long orderId);
}

View File

@@ -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<ProductCustom> selectByProductId(Long productId);
ProductCustom selectById(Long id);
int update(ProductCustom custom);
int delete(Long id);
int deleteByProductId(Long productId);
}

View File

@@ -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<Product> selectByMerchantId(@Param("merchantId") Long merchantId, @Param("status") Integer status);
List<Product> selectAll(@Param("category") String category, @Param("status") Integer status);
int update(Product product);
int updateStatus(@Param("id") Long id, @Param("status") Integer status);
List<Product> selectHotProducts(int limit);
}

View File

@@ -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<ProductSpec> selectByProductId(Long productId);
ProductSpec selectById(Long id);
int update(ProductSpec spec);
int delete(Long id);
int deleteByProductId(Long productId);
}

View File

@@ -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<ProductTopping> selectByProductId(Long productId);
ProductTopping selectById(Long id);
int update(ProductTopping topping);
int delete(Long id);
int deleteByProductId(Long productId);
}

View File

@@ -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<Review> selectByMerchantId(Long merchantId);
List<Review> selectByProductId(Long productId);
Review selectById(Long id);
Review selectByOrderId(Long orderId);
List<Review> selectByOrderIdList(@Param("orderIds") List<Long> orderIds);
int update(Review review);
}

View File

@@ -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<UserAddress> selectByUserId(Long userId);
UserAddress selectById(Long id);
int update(UserAddress address);
int delete(Long id);
UserAddress selectDefaultByUserId(Long userId);
}

View File

@@ -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<UserBehavior> selectByUserId(Long userId);
List<UserBehavior> selectByProductId(Long productId);
UserBehavior selectByUserAndProduct(@Param("userId") Long userId, @Param("productId") Long productId,
@Param("behaviorType") String behaviorType);
int update(UserBehavior behavior);
List<Long> selectProductIdsByUserId(Long userId);
List<Long> selectUserIdsByProductId(Long productId);
List<java.util.Map<String, Object>> selectActiveUsers(@Param("merchantId") Long merchantId, @Param("days") int days);
}

View File

@@ -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<User> selectAll();
}

View File

@@ -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<UserSimilarity> selectSimilarUsers(@Param("userId") Long userId, @Param("limit") int limit);
}

View File

@@ -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<Long> productIds = userBehaviorMapper.selectProductIdsByUserId(userId);
if (productIds.isEmpty()) {
return;
}
// 获取所有用户
Set<Long> allUserIds = new HashSet<>();
for (Long productId : productIds) {
allUserIds.addAll(userBehaviorMapper.selectUserIdsByProductId(productId));
}
allUserIds.remove(userId);
// 计算与每个用户的相似度
Map<Long, Map<Long, BigDecimal>> 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<Long, Map<Long, BigDecimal>> buildUserProductMatrix(Long userId, Set<Long> otherUserIds) {
Map<Long, Map<Long, BigDecimal>> matrix = new HashMap<>();
// 当前用户的评分
Map<Long, BigDecimal> userScores = new HashMap<>();
List<UserBehavior> 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<Long, BigDecimal> otherScores = new HashMap<>();
List<UserBehavior> 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<Long, BigDecimal> user1Scores, Map<Long, BigDecimal> user2Scores) {
Set<Long> 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<Product> getRecommendations(Long userId, int limit) {
// 获取用户已购买的商品
List<Long> purchasedProductIds = userBehaviorMapper.selectProductIdsByUserId(userId);
Set<Long> purchasedSet = new HashSet<>(purchasedProductIds);
// 获取相似用户
List<UserSimilarity> similarUsers = userSimilarityMapper.selectSimilarUsers(userId, 10);
// 用于存储已推荐的商品ID避免重复
Set<Long> recommendedProductIds = new HashSet<>();
List<Product> recommendations = new ArrayList<>();
if (similarUsers.isEmpty()) {
// 冷启动:返回热门商品(排除已购买的)
List<Product> 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<Long, BigDecimal> productScores = new HashMap<>();
for (UserSimilarity similarity : similarUsers) {
Long similarUserId = similarity.getUser1Id().equals(userId) ?
similarity.getUser2Id() : similarity.getUser1Id();
List<UserBehavior> 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<Map.Entry<Long, BigDecimal>> sorted = productScores.entrySet().stream()
.sorted((e1, e2) -> e2.getValue().compareTo(e1.getValue()))
.limit(limit)
.collect(Collectors.toList());
// 添加协同过滤推荐的商品
for (Map.Entry<Long, BigDecimal> 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<Product> 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();
}
}
}

View File

@@ -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<Announcement> 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<Announcement> getPublishedAnnouncements() {
return announcementMapper.selectPublished();
}
}

View File

@@ -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<Inventory> 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<Inventory> getLowStockAlerts(Long merchantId) {
return inventoryMapper.selectLowStock(merchantId);
}
public void updateInventory(Inventory inventory) {
inventoryMapper.update(inventory);
}
public void deleteInventory(Long inventoryId) {
inventoryMapper.delete(inventoryId);
}
}

View File

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

View File

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

View File

@@ -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<Order> getOrders(Long merchantId, String status, Integer pageNum, Integer pageSize) {
PageHelper.startPage(pageNum, pageSize);
List<Order> orders = orderMapper.selectByMerchantId(merchantId, status);
PageInfo<Order> 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<OrderItem> items = orderItemMapper.selectByOrderId(orderId);
List<OrderTrack> tracks = orderTrackMapper.selectByOrderId(orderId);
// 获取商品图片信息
Map<Long, String> 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);
}
}

View File

@@ -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<Product> getProducts(Long merchantId, Integer pageNum, Integer pageSize) {
PageHelper.startPage(pageNum, pageSize);
List<Product> products = productMapper.selectByMerchantId(merchantId, null);
PageInfo<Product> pageInfo = new PageInfo<>(products);
return PageResponse.of(products, pageInfo.getTotal(), pageNum, pageSize);
}
/**
* 获取商品完整信息(包括规格、配料、定制选项)
*/
public Map<String, Object> getProduct(Long productId, Long merchantId) {
Product product = productMapper.selectById(productId);
if (product == null) {
throw new BusinessException(404, "商品不存在");
}
// 验证商品归属
validateProductOwnership(productId, merchantId);
Map<String, Object> 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<ProductSpec> 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<ProductTopping> 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<ProductCustom> 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);
}
}

View File

@@ -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<Review> 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<Review> getProductReviews(Long productId) {
return reviewMapper.selectByProductId(productId);
}
}

View File

@@ -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<String, Object> getSalesStatistics(Long merchantId, String period) {
LocalDateTime startTime = getStartTime(period);
List<Order> 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<String, Object> result = new HashMap<>();
result.put("totalOrders", orders.size());
result.put("totalAmount", totalAmount);
result.put("avgOrderAmount", avgOrderAmount);
return result;
}
public Map<String, Object> getRevenueStatistics(Long merchantId, String period) {
Map<String, Object> 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<String, Object> result = new HashMap<>();
result.put("totalRevenue", totalRevenue);
result.put("totalCost", totalCost);
result.put("netProfit", netProfit);
return result;
}
public List<Map<String, Object>> getHotProducts(Long merchantId, int limit) {
List<Order> orders = orderMapper.selectByMerchantId(merchantId, "COMPLETED");
// 统计商品销量和销售额
Map<Long, Integer> productSales = new HashMap<>();
Map<Long, BigDecimal> productAmounts = new HashMap<>();
for (Order order : orders) {
List<OrderItem> 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<String, Object> 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<Map<String, Object>> getActiveUsers(Long merchantId, int days) {
return userBehaviorMapper.selectActiveUsers(merchantId, days);
}
public List<Map<String, Object>> 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);
}
}
}

View File

@@ -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<User> getUsers(Integer pageNum, Integer pageSize) {
PageHelper.startPage(pageNum, pageSize);
List<User> users = userMapper.selectAll();
PageInfo<User> pageInfo = new PageInfo<>(users);
return PageResponse.of(users, pageInfo.getTotal(), pageNum, pageSize);
}
}

View File

@@ -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<CartItem> 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<Long> cartIds) {
if (cartIds == null || cartIds.isEmpty()) {
return;
}
// 验证所有商品都属于当前用户
List<CartItem> 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<CartAddRequest> 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());
}
}
}
}

View File

@@ -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<Message> getMessages(Long userId) {
return messageMapper.selectByUserId(userId);
}
}

View File

@@ -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<CartItem> 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<Long> toppingIds = JSON.parseObject(item.getToppings(), new TypeReference<List<Long>>(){});
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<Long> toppingIds = JSON.parseObject(item.getToppings(), new TypeReference<List<Long>>(){});
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<OrderItem> items = orderItemMapper.selectByOrderId(orderId);
for (OrderItem item : items) {
cfService.updateUserPreference(order.getUserId(), item.getProductId());
}
}
public List<Order> 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<OrderItem> items = orderItemMapper.selectByOrderId(orderId);
List<OrderTrack> tracks = orderTrackMapper.selectByOrderId(orderId);
// 获取商品图片信息
Map<Long, String> 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<OrderTrack> 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);
}
}

View File

@@ -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<Product> getProducts(String category, Integer pageNum, Integer pageSize) {
PageHelper.startPage(pageNum, pageSize);
List<Product> products = productMapper.selectAll(category, 1); // 只查询上架商品
PageInfo<Product> pageInfo = new PageInfo<>(products);
return PageResponse.of(products, pageInfo.getTotal(), pageNum, pageSize);
}
public Map<String, Object> 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<String, Object> result = new HashMap<>();
result.put("product", product);
result.put("specs", productSpecMapper.selectByProductId(productId));
result.put("toppings", productToppingMapper.selectByProductId(productId));
// 获取定制选项
List<ProductCustom> customs = productCustomMapper.selectByProductId(productId);
Map<String, List<ProductCustom>> 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<Product> getHotProducts(int limit) {
return productMapper.selectHotProducts(limit);
}
}

View File

@@ -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<Product> getRecommendations(Long userId, int limit) {
return cfService.getRecommendations(userId, limit);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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`);

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