Initial commit

This commit is contained in:
MaDaLei 2026-04-12 23:09:38 +08:00
commit e3e3e2bbdb
57 changed files with 1508 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
target/

96
pom.xml Normal file
View File

@ -0,0 +1,96 @@
<?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>3.2.3</version>
<relativePath/>
</parent>
<groupId>com.petstore</groupId>
<artifactId>petstore-backend</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>petstore-backend</name>
<description>宠伴生活馆 后端服务</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.44</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<source>17</source>
<target>17</target>
<fork>true</fork>
<compilerArgs>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED</arg>
</compilerArgs>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.44</version>
</path>
</annotationProcessorPaths>
</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,30 @@
package com.petstore;
import com.petstore.service.ServiceTypeService;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.jdbc.core.JdbcTemplate;
@SpringBootApplication
@RequiredArgsConstructor
public class PetstoreApplication {
public static void main(String[] args) {
System.out.println(">>> working dir: " + System.getProperty("user.dir"));
SpringApplication.run(PetstoreApplication.class, args);
}
@Bean
CommandLineRunner initRunner(ServiceTypeService serviceTypeService, JdbcTemplate jdbc) {
return args -> {
serviceTypeService.initDefaults();
// appointment_id 改为允许 NULL支持不挂预约直接填报告
jdbc.execute("ALTER TABLE t_report MODIFY COLUMN appointment_id BIGINT NULL");
// 修复旧图片URL/2026/xxx /api/upload/image/2026/xxx
jdbc.execute("UPDATE t_report SET before_photo = REPLACE(before_photo, 'http://localhost:8080/2026/', '/api/upload/image/2026/') WHERE before_photo LIKE 'http://localhost:8080/2026/%'");
jdbc.execute("UPDATE t_report SET after_photo = REPLACE(after_photo, 'http://localhost:8080/2026/', '/api/upload/image/2026/') WHERE after_photo LIKE 'http://localhost:8080/2026/%'");
};
}
}

View File

@ -0,0 +1,23 @@
package com.petstore.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOriginPattern("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}

View File

@ -0,0 +1,27 @@
package com.petstore.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.io.File;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Value("${upload.path:uploads}")
private String uploadPath;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 硬编码绝对路径确保能找到文件
String uploadDir = "/Users/wac/Desktop/www/_src/petstore/backend/uploads/";
System.out.println(">>> WebConfig uploadDir: " + uploadDir);
System.out.println(">>> /2026 exists: " + new File(uploadDir + "2026/04/01/").exists());
registry.addResourceHandler("/uploads/**")
.addResourceLocations("file:" + uploadDir);
registry.addResourceHandler("/2026/**")
.addResourceLocations("file:" + uploadDir);
}
}

View File

@ -0,0 +1,33 @@
package com.petstore.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
@Configuration
public class WechatConfig {
// TODO: 替换为实际的微信开放平台 AppID AppSecret
@Value("${wechat.appid:YOUR_APPID}")
private String appid;
@Value("${wechat.appsecret:YOUR_APPSECRET}")
private String appsecret;
@Value("${wechat.redirect_uri:http://localhost:8080/api/wechat/callback}")
private String redirectUri;
public String getAppid() { return appid; }
public String getAppsecret() { return appsecret; }
public String getRedirectUri() { return redirectUri; }
/**
* 获取微信授权跳转地址
*/
public String getAuthorizeUrl() {
return "https://open.weixin.qq.com/connect/qrconnect" +
"?appid=" + appid +
"&redirect_uri=" + redirectUri +
"&response_type=code" +
"&scope=snsapi_login" +
"&state=petstore#wechat_redirect";
}
}

View File

@ -0,0 +1,86 @@
package com.petstore.controller;
import com.petstore.entity.Appointment;
import com.petstore.service.AppointmentService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/appointment")
@RequiredArgsConstructor
@CrossOrigin
public class AppointmentController {
private final AppointmentService appointmentService;
/** 获取预约列表(员工查自己/老板查全店) */
@GetMapping("/list")
public Map<String, Object> list(
@RequestParam(required = false) Long userId,
@RequestParam(required = false) Long storeId,
@RequestParam(required = false) String status) {
List<Appointment> appointments;
if (storeId != null) {
appointments = (status != null && !status.isEmpty())
? appointmentService.getByStoreIdAndStatus(storeId, status)
: appointmentService.getByStoreId(storeId);
} else if (userId != null) {
appointments = (status != null && !status.isEmpty())
? appointmentService.getByUserIdAndStatus(userId, status)
: appointmentService.getByUserId(userId);
} else {
return Map.of("code", 400, "message", "userId或storeId必填");
}
return Map.of("code", 200, "data", appointments);
}
/** 创建预约 */
@PostMapping("/create")
public Map<String, Object> create(@RequestBody Map<String, Object> params) {
Appointment appointment = new Appointment();
appointment.setPetName(params.get("petName").toString());
appointment.setPetType(params.get("petType").toString());
appointment.setServiceType(params.get("serviceType").toString());
String timeStr = params.get("appointmentTime").toString();
appointment.setAppointmentTime(java.time.LocalDateTime.parse(timeStr));
appointment.setStoreId(Long.valueOf(params.get("storeId").toString()));
appointment.setUserId(Long.valueOf(params.get("userId").toString()));
appointment.setStatus("new");
if (params.containsKey("remark") && params.get("remark") != null) {
appointment.setRemark(params.get("remark").toString());
}
Appointment created = appointmentService.create(appointment);
return Map.of("code", 200, "message", "创建成功", "data", created);
}
/** 开始服务:状态变进行中 + 指定技师 */
@PostMapping("/start")
public Map<String, Object> start(@RequestBody Map<String, Object> params) {
Long appointmentId = Long.valueOf(params.get("appointmentId").toString());
Long staffUserId = Long.valueOf(params.get("staffUserId").toString());
Appointment updated = appointmentService.startService(appointmentId, staffUserId);
if (updated != null) {
return Map.of("code", 200, "message", "已开始服务", "data", updated);
}
return Map.of("code", 404, "message", "预约不存在");
}
/** 更新预约状态 */
@PutMapping("/status")
public Map<String, Object> updateStatus(@RequestParam Long id, @RequestParam String status) {
Appointment updated = appointmentService.updateStatus(id, status);
if (updated != null) {
return Map.of("code", 200, "message", "更新成功", "data", updated);
}
return Map.of("code", 404, "message", "预约不存在");
}
}

View File

@ -0,0 +1,114 @@
package com.petstore.controller;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
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.time.LocalDate;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@RestController
@RequestMapping("/api/upload")
@RequiredArgsConstructor
@CrossOrigin
public class FileController {
private static final String UPLOAD_BASE = "/Users/wac/Desktop/www/_src/petstore/backend/uploads/";
@Value("${upload.path:uploads}")
private String uploadPath;
@GetMapping("/image/**")
public ResponseEntity<Resource> getImage(HttpServletRequest request) throws IOException {
String path = request.getRequestURI().replace("/api/upload/image", "");
File file = new File(UPLOAD_BASE + path);
if (!file.exists()) {
return ResponseEntity.notFound().build();
}
String contentType = Files.probeContentType(file.toPath());
if (contentType == null) contentType = "image/jpeg";
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(contentType))
.body(new FileSystemResource(file));
}
// 兼容旧路径/2026/04/01/xxx.jpg
@GetMapping("/legacy/**")
public ResponseEntity<Resource> getLegacyImage(HttpServletRequest request) throws IOException {
String path = request.getRequestURI().replace("/api/upload/legacy", "");
File file = new File(UPLOAD_BASE + path);
if (!file.exists()) {
return ResponseEntity.notFound().build();
}
String contentType = Files.probeContentType(file.toPath());
if (contentType == null) contentType = "image/jpeg";
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(contentType))
.body(new FileSystemResource(file));
}
@PostMapping("/image")
public Map<String, Object> uploadImage(@RequestParam("file") MultipartFile file) {
Map<String, Object> result = new HashMap<>();
if (file.isEmpty()) {
result.put("code", 400);
result.put("message", "文件为空");
return result;
}
String contentType = file.getContentType();
if (contentType == null || !contentType.startsWith("image/")) {
result.put("code", 400);
result.put("message", "只能上传图片");
return result;
}
try {
// 创建上传目录使用绝对路径
String datePath = LocalDate.now().toString().replace("-", "/");
String dirPath = UPLOAD_BASE + datePath; // /.../uploads/2026/04/01
File dir = new File(dirPath);
if (!dir.exists()) dir.mkdirs();
// 生成文件名
String originalFilename = file.getOriginalFilename();
String ext = "";
if (originalFilename != null && originalFilename.contains(".")) {
ext = originalFilename.substring(originalFilename.lastIndexOf("."));
}
String filename = UUID.randomUUID().toString().replace("-", "") + ext;
// 保存文件
Path filePath = Paths.get(dirPath, filename);
Files.write(filePath, file.getBytes());
// 返回访问URL/api/upload/image/ + 日期路径 + 文件名
String url = "/api/upload/image/" + datePath + "/" + filename;
result.put("code", 200);
result.put("message", "上传成功");
result.put("data", Map.of("url", url));
return result;
} catch (IOException e) {
e.printStackTrace();
result.put("code", 500);
result.put("message", "上传失败");
return result;
}
}
}

View File

@ -0,0 +1,117 @@
package com.petstore.controller;
import com.petstore.entity.Report;
import com.petstore.entity.Store;
import com.petstore.service.ReportService;
import com.petstore.service.StoreService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/api/report")
@RequiredArgsConstructor
@CrossOrigin
public class ReportController {
private final ReportService reportService;
private final StoreService storeService;
private static final String BASE_URL = "http://localhost:8080";
private String fullUrl(String path) {
if (path == null || path.isEmpty()) return path;
if (path.startsWith("http")) return path;
return BASE_URL + path;
}
@PostMapping("/create")
public Map<String, Object> create(@RequestBody Report report) {
System.out.println(">>> Report create received: appointmentId=" + report.getAppointmentId() + ", userId=" + report.getUserId() + ", before=" + report.getBeforePhoto());
Report created = reportService.create(report);
Map<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("message", "提交成功");
result.put("data", Map.of(
"reportToken", created.getReportToken(),
"reportId", created.getId()
));
return result;
}
@GetMapping("/list")
public Map<String, Object> list(@RequestParam(required = false) Long storeId,
@RequestParam(required = false) Long userId) {
List<Report> reports = reportService.list(storeId, userId);
// 附加技师名称并补全图片URL
List<Map<String, Object>> data = reports.stream().map(r -> {
Map<String, Object> item = new HashMap<>();
item.put("id", r.getId());
item.put("appointmentId", r.getAppointmentId());
item.put("petName", r.getPetName());
item.put("serviceType", r.getServiceType());
item.put("appointmentTime", r.getAppointmentTime());
item.put("staffName", r.getStaffName());
item.put("reportToken", r.getReportToken());
item.put("createTime", r.getCreateTime());
item.put("beforePhoto", fullUrl(r.getBeforePhoto()));
item.put("afterPhoto", fullUrl(r.getAfterPhoto()));
item.put("storeId", r.getStoreId());
item.put("userId", r.getUserId());
return item;
}).collect(Collectors.toList());
return Map.of("code", 200, "data", data);
}
@GetMapping("/get")
public Map<String, Object> getByAppointmentId(@RequestParam(required = false) Long appointmentId,
@RequestParam(required = false) String token) {
Report report = null;
if (token != null && !token.isEmpty()) {
report = reportService.getByToken(token);
} else if (appointmentId != null) {
report = reportService.getByAppointmentId(appointmentId);
}
Map<String, Object> result = new HashMap<>();
if (report != null) {
// 附加店铺信息
Store store = null;
if (report.getStoreId() != null) {
store = storeService.findById(report.getStoreId());
}
Map<String, Object> data = new HashMap<>();
data.put("id", report.getId());
data.put("appointmentId", report.getAppointmentId());
data.put("beforePhoto", report.getBeforePhoto());
data.put("afterPhoto", report.getAfterPhoto());
data.put("remark", report.getRemark());
data.put("userId", report.getUserId());
data.put("storeId", report.getStoreId());
data.put("reportToken", report.getReportToken());
data.put("petName", report.getPetName());
data.put("serviceType", report.getServiceType());
data.put("appointmentTime", report.getAppointmentTime());
data.put("staffName", report.getStaffName());
data.put("createTime", report.getCreateTime());
if (store != null) {
Map<String, Object> storeInfo = new HashMap<>();
storeInfo.put("name", store.getName());
storeInfo.put("logo", store.getLogo());
storeInfo.put("phone", store.getPhone());
storeInfo.put("address", store.getAddress());
data.put("store", storeInfo);
}
result.put("code", 200);
result.put("data", data);
} else {
result.put("code", 404);
result.put("message", "报告不存在");
}
return result;
}
}

View File

@ -0,0 +1,52 @@
package com.petstore.controller;
import com.petstore.entity.ServiceType;
import com.petstore.service.ServiceTypeService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/service-type")
@RequiredArgsConstructor
@CrossOrigin
public class ServiceTypeController {
private final ServiceTypeService serviceTypeService;
@GetMapping("/list")
public Map<String, Object> list(@RequestParam Long storeId) {
List<ServiceType> list = serviceTypeService.getByStoreId(storeId);
return Map.of("code", 200, "data", list);
}
@PostMapping("/create")
public Map<String, Object> create(@RequestBody Map<String, Object> params) {
Long storeId = Long.valueOf(params.get("storeId").toString());
String name = params.get("name").toString();
ServiceType created = serviceTypeService.create(storeId, name);
return Map.of("code", 200, "data", created);
}
@PutMapping("/update")
public Map<String, Object> update(@RequestBody Map<String, Object> params) {
Long id = Long.valueOf(params.get("id").toString());
String name = params.get("name").toString();
ServiceType updated = serviceTypeService.update(id, name);
return Map.of("code", 200, "data", updated);
}
@DeleteMapping("/delete")
public Map<String, Object> delete(@RequestParam Long id) {
serviceTypeService.delete(id);
return Map.of("code", 200, "message", "删除成功");
}
@PostMapping("/init")
public Map<String, Object> init() {
serviceTypeService.initDefaults();
return Map.of("code", 200, "message", "初始化成功");
}
}

View File

@ -0,0 +1,45 @@
package com.petstore.controller;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
@RestController
@RequestMapping("/api/sms")
@RequiredArgsConstructor
@CrossOrigin
public class SmsController {
// TODO: 接入真实的短信服务商阿里云/腾讯云/华为云等
// 这里用演示模式随机生成6位验证码
@PostMapping("/send")
public Map<String, Object> send(@RequestBody Map<String, String> params) {
String phone = params.get("phone");
Map<String, Object> result = new HashMap<>();
if (phone == null || phone.length() != 11) {
result.put("code", 400);
result.put("message", "手机号格式不正确");
return result;
}
// 演示生成6位验证码
String code = String.format("%06d", new Random().nextInt(999999));
// TODO: 调用真实短信服务商发送验证码
// 阿里云示例 dysmsapi.aliyuncs.com
// 腾讯云示例 sms.tencentcloudapi.com
System.out.println("【宠伴生活馆】验证码:" + code + "您正在登录5分钟内有效。");
result.put("code", 200);
result.put("message", "发送成功");
result.put("data", Map.of("code", code)); // 演示用实际生产环境不应返回code
return result;
}
}

View File

@ -0,0 +1,65 @@
package com.petstore.controller;
import com.petstore.entity.Store;
import com.petstore.service.StoreService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api/store")
@RequiredArgsConstructor
@CrossOrigin
public class StoreController {
private final StoreService storeService;
@PostMapping("/register")
public Map<String, Object> register(@RequestBody Store store) {
Store created = storeService.create(store);
Map<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("message", "注册成功");
result.put("data", created);
return result;
}
@GetMapping("/get")
public Map<String, Object> get(@RequestParam Long id) {
Store store = storeService.findById(id);
Map<String, Object> result = new HashMap<>();
if (store != null) {
result.put("code", 200);
result.put("data", store);
} else {
result.put("code", 404);
result.put("message", "店铺不存在");
}
return result;
}
@PutMapping("/update")
public Map<String, Object> update(@RequestBody Store store) {
Store updated = storeService.update(store);
Map<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("message", "更新成功");
result.put("data", updated);
return result;
}
@GetMapping("/invite-code")
public Map<String, Object> getByInviteCode(@RequestParam String code) {
Store store = storeService.findByInviteCode(code);
Map<String, Object> result = new HashMap<>();
if (store != null) {
result.put("code", 200);
result.put("data", store);
} else {
result.put("code", 404);
result.put("message", "邀请码无效");
}
return result;
}
}

View File

@ -0,0 +1,85 @@
package com.petstore.controller;
import com.petstore.entity.User;
import com.petstore.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/user")
@RequiredArgsConstructor
@CrossOrigin
public class UserController {
private final UserService userService;
/** 老板注册店铺 */
@PostMapping("/register-boss")
public Map<String, Object> registerBoss(@RequestBody Map<String, String> params) {
String storeName = params.get("storeName");
String bossName = params.get("bossName");
String phone = params.get("phone");
String password = params.get("password");
return userService.registerBoss(storeName, bossName, phone, password);
}
/** 登录(老板/员工) */
@PostMapping("/login")
public Map<String, Object> login(@RequestBody Map<String, String> params) {
String phone = params.get("phone");
String code = params.get("code");
return userService.login(phone, code);
}
/** 员工注册(邀请码方式) */
@PostMapping("/register-staff")
public Map<String, Object> registerStaff(@RequestBody Map<String, String> params) {
String phone = params.get("phone");
String password = params.get("password");
String name = params.get("name");
String inviteCode = params.get("inviteCode");
return userService.registerStaff(phone, password, name, inviteCode);
}
/** 老板:创建员工 */
@PostMapping("/create-staff")
public Map<String, Object> createStaff(@RequestBody Map<String, Object> params) {
Long storeId = Long.valueOf(params.get("storeId").toString());
String name = params.get("name").toString();
String phone = params.get("phone").toString();
return userService.createStaff(storeId, name, phone);
}
/** 老板:员工列表 */
@GetMapping("/staff-list")
public Map<String, Object> staffList(@RequestParam Long storeId) {
List<User> list = userService.getStaffList(storeId);
// 不返回密码
list.forEach(u -> u.setPassword(null));
return Map.of("code", 200, "data", list);
}
/** 老板:删除员工 */
@DeleteMapping("/staff")
public Map<String, Object> deleteStaff(@RequestParam Long staffId) {
userService.deleteStaff(staffId);
return Map.of("code", 200, "message", "删除成功");
}
/** 获取用户信息 */
@GetMapping("/info")
public Map<String, Object> info(@RequestParam Long userId) {
User user = userService.findById(userId);
if (user != null) user.setPassword(null);
return Map.of("code", 200, "data", user);
}
/** 更新用户信息(头像/姓名/手机号) */
@PutMapping("/update")
public Map<String, Object> updateUser(@RequestBody Map<String, Object> params) {
return userService.updateUser(params);
}
}

View File

@ -0,0 +1,49 @@
package com.petstore.controller;
import com.petstore.config.WechatConfig;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api/wechat")
@RequiredArgsConstructor
@CrossOrigin
public class WechatController {
private final WechatConfig wechatConfig;
/**
* 获取微信授权跳转地址
*/
@GetMapping("/authorize")
public Map<String, Object> getAuthorizeUrl() {
Map<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("data", wechatConfig.getAuthorizeUrl());
return result;
}
/**
* 微信授权回调
* 通过 code 换取 access_token返回用户信息
*/
@GetMapping("/callback")
public Map<String, Object> callback(@RequestParam String code, @RequestParam String state) {
Map<String, Object> result = new HashMap<>();
// TODO: 通过 code 调用微信接口换取 openid access_token
// https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code
// 演示用直接返回成功
result.put("code", 200);
result.put("message", "微信授权成功");
result.put("data", Map.of(
"openid", "demo_openid_" + code,
"nickname", "微信用户",
"avatar", ""
));
return result;
}
}

View File

@ -0,0 +1,40 @@
package com.petstore.entity;
import jakarta.persistence.*;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@Entity
@Table(name = "t_appointment")
public class Appointment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String petName;
private String petType;
private String serviceType;
private LocalDateTime appointmentTime;
/** 状态: new/doing/done/cancel */
private String status;
@Column(name = "store_id")
private Long storeId;
@Column(name = "user_id")
private Long userId;
/** 技师ID开始服务时赋值 */
@Column(name = "assigned_user_id")
private Long assignedUserId;
private String remark;
@Column(name = "create_time")
private LocalDateTime createTime;
@Column(name = "update_time")
private LocalDateTime updateTime;
}

View File

@ -0,0 +1,47 @@
package com.petstore.entity;
import jakarta.persistence.*;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@Entity
@Table(name = "t_report")
public class Report {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@JsonProperty("appointmentId")
@Column(name = "appointment_id")
private Long appointmentId;
@Column(name = "before_photo", columnDefinition = "TEXT")
private String beforePhoto;
@Column(name = "after_photo", columnDefinition = "TEXT")
private String afterPhoto;
private String remark;
@Column(name = "user_id")
private Long userId;
@Column(name = "store_id")
private Long storeId;
@Column(name = "report_token")
private String reportToken;
private String petName;
private String serviceType;
private LocalDateTime appointmentTime;
private String staffName;
@Column(name = "create_time")
private LocalDateTime createTime;
@Column(name = "update_time")
private LocalDateTime updateTime;
}

View File

@ -0,0 +1,22 @@
package com.petstore.entity;
import jakarta.persistence.*;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@Entity
@Table(name = "t_service_type")
public class ServiceType {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 店铺IDNULL表示系统默认 */
private Long storeId;
private String name;
@Column(name = "create_time")
private LocalDateTime createTime;
}

View File

@ -0,0 +1,36 @@
package com.petstore.entity;
import jakarta.persistence.*;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@Entity
@Table(name = "t_store")
public class Store {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String logo;
private String phone;
private String address;
/** 地图选点纬度WGS84 / 各端与地图接口一致) */
private Double latitude;
/** 地图选点经度 */
private Double longitude;
private String intro;
@Column(name = "owner_id")
private Long ownerId;
@Column(name = "invite_code")
private String inviteCode;
@Column(name = "create_time")
private LocalDateTime createTime;
@Column(name = "update_time")
private LocalDateTime updateTime;
}

View File

@ -0,0 +1,32 @@
package com.petstore.entity;
import jakarta.persistence.*;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@Entity
@Table(name = "t_user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
private String name;
private String phone;
private String avatar;
@Column(name = "store_id")
private Long storeId;
/** 角色boss / staff / customer */
private String role;
@Column(name = "create_time")
private LocalDateTime createTime;
@Column(name = "update_time")
private LocalDateTime updateTime;
}

View File

@ -0,0 +1,13 @@
package com.petstore.mapper;
import com.petstore.entity.Appointment;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface AppointmentMapper extends JpaRepository<Appointment, Long> {
List<Appointment> findByUserId(Long userId);
List<Appointment> findByUserIdAndStatus(Long userId, String status);
List<Appointment> findByStoreId(Long storeId);
List<Appointment> findByStoreIdAndStatus(Long storeId, String status);
}

View File

@ -0,0 +1,7 @@
package com.petstore.mapper;
import com.petstore.entity.Report;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ReportMapper extends JpaRepository<Report, Long> {
}

View File

@ -0,0 +1,11 @@
package com.petstore.mapper;
import com.petstore.entity.ServiceType;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface ServiceTypeMapper extends JpaRepository<ServiceType, Long> {
List<ServiceType> findByStoreIdOrStoreIdIsNull(Long storeId);
List<ServiceType> findByStoreId(Long storeId);
}

View File

@ -0,0 +1,8 @@
package com.petstore.mapper;
import com.petstore.entity.Store;
import org.springframework.data.jpa.repository.JpaRepository;
public interface StoreMapper extends JpaRepository<Store, Long> {
Store findByInviteCode(String inviteCode);
}

View File

@ -0,0 +1,12 @@
package com.petstore.mapper;
import com.petstore.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface UserMapper extends JpaRepository<User, Long> {
User findByUsername(String username);
User findByPhone(String phone);
List<User> findByStoreId(Long storeId);
}

View File

@ -0,0 +1,63 @@
package com.petstore.service;
import com.petstore.entity.Appointment;
import com.petstore.mapper.AppointmentMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.List;
@Service
@RequiredArgsConstructor
public class AppointmentService {
private final AppointmentMapper appointmentMapper;
// 员工查看自己的预约
public List<Appointment> getByUserId(Long userId) {
return appointmentMapper.findByUserId(userId);
}
// 老板查看本店所有预约
public List<Appointment> getByStoreId(Long storeId) {
return appointmentMapper.findByStoreId(storeId);
}
// 员工按状态查
public List<Appointment> getByUserIdAndStatus(Long userId, String status) {
return appointmentMapper.findByUserIdAndStatus(userId, status);
}
// 老板按状态查
public List<Appointment> getByStoreIdAndStatus(Long storeId, String status) {
return appointmentMapper.findByStoreIdAndStatus(storeId, status);
}
public Appointment create(Appointment appointment) {
appointment.setCreateTime(LocalDateTime.now());
appointment.setUpdateTime(LocalDateTime.now());
return appointmentMapper.save(appointment);
}
public Appointment updateStatus(Long id, String status) {
Appointment appointment = appointmentMapper.findById(id).orElse(null);
if (appointment != null) {
appointment.setStatus(status);
appointment.setUpdateTime(LocalDateTime.now());
return appointmentMapper.save(appointment);
}
return null;
}
/** 开始服务:状态变为进行中,同时指定技师为当前用户 */
public Appointment startService(Long appointmentId, Long staffUserId) {
Appointment appointment = appointmentMapper.findById(appointmentId).orElse(null);
if (appointment != null) {
appointment.setStatus("doing");
appointment.setAssignedUserId(staffUserId);
appointment.setUpdateTime(LocalDateTime.now());
return appointmentMapper.save(appointment);
}
return null;
}
}

View File

@ -0,0 +1,86 @@
package com.petstore.service;
import com.petstore.entity.Appointment;
import com.petstore.entity.Report;
import com.petstore.entity.User;
import com.petstore.mapper.AppointmentMapper;
import com.petstore.mapper.ReportMapper;
import com.petstore.mapper.UserMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class ReportService {
private final ReportMapper reportMapper;
private final AppointmentMapper appointmentMapper;
private final UserMapper userMapper;
public Report create(Report report) {
// 生成唯一令牌
report.setReportToken(UUID.randomUUID().toString().replace("-", ""));
// 填充冗余字段并自动完成预约
if (report.getAppointmentId() != null) {
Appointment appt = appointmentMapper.findById(report.getAppointmentId()).orElse(null);
if (appt != null) {
report.setPetName(appt.getPetName());
report.setServiceType(appt.getServiceType());
report.setAppointmentTime(appt.getAppointmentTime());
report.setStoreId(appt.getStoreId());
// 技师取预约分配的技师开始服务时指定的
if (appt.getAssignedUserId() != null) {
User staff = userMapper.findById(appt.getAssignedUserId()).orElse(null);
if (staff != null) {
report.setUserId(staff.getId());
report.setStaffName(staff.getName());
}
}
// 填写完报告自动标记预约为已完成
appt.setStatus("done");
appt.setUpdateTime(LocalDateTime.now());
appointmentMapper.save(appt);
}
}
// 如果预约没分配技师则用当前操作人
if (report.getUserId() != null && report.getStaffName() == null) {
User staff = userMapper.findById(report.getUserId()).orElse(null);
if (staff != null) {
report.setStaffName(staff.getName());
if (report.getStoreId() == null) {
report.setStoreId(staff.getStoreId());
}
}
}
report.setCreateTime(LocalDateTime.now());
report.setUpdateTime(LocalDateTime.now());
return reportMapper.save(report);
}
public Report getByAppointmentId(Long appointmentId) {
return reportMapper.findAll().stream()
.filter(r -> r.getAppointmentId().equals(appointmentId))
.findFirst()
.orElse(null);
}
public Report getByToken(String token) {
return reportMapper.findAll().stream()
.filter(r -> token.equals(r.getReportToken()))
.findFirst()
.orElse(null);
}
public List<Report> list(Long storeId, Long userId) {
return reportMapper.findAll().stream()
.filter(r -> storeId == null || storeId.equals(r.getStoreId()))
.filter(r -> userId == null || userId.equals(r.getUserId()))
.collect(Collectors.toList());
}
}

View File

@ -0,0 +1,59 @@
package com.petstore.service;
import com.petstore.entity.ServiceType;
import com.petstore.mapper.ServiceTypeMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.List;
@Service
@RequiredArgsConstructor
public class ServiceTypeService {
private final ServiceTypeMapper serviceTypeMapper;
/** 获取服务类型(系统默认 + 当前店铺自定义) */
public List<ServiceType> getByStoreId(Long storeId) {
return serviceTypeMapper.findByStoreIdOrStoreIdIsNull(storeId);
}
/** 老板新增服务类型 */
public ServiceType create(Long storeId, String name) {
ServiceType st = new ServiceType();
st.setStoreId(storeId);
st.setName(name);
st.setCreateTime(LocalDateTime.now());
return serviceTypeMapper.save(st);
}
/** 老板编辑服务类型 */
public ServiceType update(Long id, String name) {
ServiceType st = serviceTypeMapper.findById(id).orElse(null);
if (st != null) {
st.setName(name);
serviceTypeMapper.save(st);
}
return st;
}
/** 老板删除服务类型(仅能删除自己店铺的) */
public void delete(Long id) {
serviceTypeMapper.deleteById(id);
}
/** 初始化系统默认服务类型(如果不存在) */
public void initDefaults() {
List<ServiceType> defaults = serviceTypeMapper.findByStoreIdOrStoreIdIsNull(null);
if (defaults.isEmpty()) {
String[] names = {"洗澡", "美容", "洗澡+美容", "剪指甲", "驱虫"};
for (String name : names) {
ServiceType st = new ServiceType();
st.setStoreId(null);
st.setName(name);
st.setCreateTime(LocalDateTime.now());
serviceTypeMapper.save(st);
}
}
}
}

View File

@ -0,0 +1,39 @@
package com.petstore.service;
import com.petstore.entity.Store;
import com.petstore.mapper.StoreMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.UUID;
@Service
@RequiredArgsConstructor
public class StoreService {
private final StoreMapper storeMapper;
public Store create(Store store) {
store.setInviteCode(generateInviteCode());
store.setCreateTime(LocalDateTime.now());
store.setUpdateTime(LocalDateTime.now());
return storeMapper.save(store);
}
public Store findById(Long id) {
return storeMapper.findById(id).orElse(null);
}
public Store findByInviteCode(String code) {
return storeMapper.findByInviteCode(code);
}
public Store update(Store store) {
store.setUpdateTime(LocalDateTime.now());
return storeMapper.save(store);
}
private String generateInviteCode() {
return UUID.randomUUID().toString().replace("-", "").substring(0, 8).toUpperCase();
}
}

View File

@ -0,0 +1,168 @@
package com.petstore.service;
import com.petstore.entity.Store;
import com.petstore.entity.User;
import com.petstore.mapper.StoreMapper;
import com.petstore.mapper.UserMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.*;
@Service
@RequiredArgsConstructor
public class UserService {
private final UserMapper userMapper;
private final StoreMapper storeMapper;
public Map<String, Object> registerBoss(String storeName, String bossName, String phone, String password) {
if (userMapper.findByPhone(phone) != null) {
return Map.of("code", 400, "message", "手机号已注册");
}
Store store = new Store();
store.setName(storeName);
store.setPhone(phone);
store.setOwnerId(0L);
store.setInviteCode(UUID.randomUUID().toString().replace("-", "").substring(0, 8).toUpperCase());
store.setCreateTime(LocalDateTime.now());
store.setUpdateTime(LocalDateTime.now());
store = storeMapper.save(store);
User boss = new User();
boss.setUsername(phone);
boss.setName(bossName);
boss.setPhone(phone);
boss.setPassword(password);
boss.setStoreId(store.getId());
boss.setRole("boss");
boss.setCreateTime(LocalDateTime.now());
boss.setUpdateTime(LocalDateTime.now());
boss = userMapper.save(boss);
store.setOwnerId(boss.getId());
storeMapper.save(store);
Map<String, Object> data = new HashMap<>();
data.put("user", boss);
data.put("store", store);
return Map.of("code", 200, "message", "注册成功", "data", data);
}
public Map<String, Object> login(String phone, String code) {
// 演示模式验证码 123456 万能
if (!"123456".equals(code)) {
return Map.of("code", 401, "message", "验证码错误");
}
User user = userMapper.findByPhone(phone);
if (user == null) {
// 自动注册为 C 端用户 (customer)
user = new User();
user.setUsername(phone);
user.setPhone(phone);
user.setName("微信用户" + phone.substring(7));
user.setRole("customer");
user.setCreateTime(LocalDateTime.now());
user.setUpdateTime(LocalDateTime.now());
user = userMapper.save(user);
}
Store store = null;
if (user.getStoreId() != null) {
store = storeMapper.findById(user.getStoreId()).orElse(null);
}
Map<String, Object> data = new HashMap<>();
data.put("user", user);
data.put("store", store);
return Map.of("code", 200, "message", "登录成功", "data", data);
}
public Map<String, Object> registerStaff(String phone, String password, String name, String inviteCode) {
if (userMapper.findByPhone(phone) != null) {
return Map.of("code", 400, "message", "手机号已注册");
}
Store store = storeMapper.findByInviteCode(inviteCode);
if (store == null) {
return Map.of("code", 400, "message", "邀请码无效");
}
User staff = new User();
staff.setUsername(phone);
staff.setPhone(phone);
staff.setPassword(password);
staff.setName(name);
staff.setStoreId(store.getId());
staff.setRole("staff");
staff.setCreateTime(LocalDateTime.now());
staff.setUpdateTime(LocalDateTime.now());
staff = userMapper.save(staff);
Map<String, Object> data = new HashMap<>();
data.put("user", staff);
data.put("store", store);
return Map.of("code", 200, "message", "注册成功", "data", data);
}
public Map<String, Object> createStaff(Long storeId, String name, String phone) {
if (storeId == null) {
return Map.of("code", 400, "message", "店铺ID不能为空");
}
if (userMapper.findByPhone(phone) != null) {
return Map.of("code", 400, "message", "手机号已存在");
}
String pwd = String.format("%06d", (int)(Math.random() * 999999));
User staff = new User();
staff.setUsername(phone);
staff.setName(name);
staff.setPhone(phone);
staff.setPassword(pwd);
staff.setStoreId(storeId);
staff.setRole("staff");
staff.setCreateTime(LocalDateTime.now());
staff.setUpdateTime(LocalDateTime.now());
staff = userMapper.save(staff);
return Map.of("code", 200, "message", "创建成功,初始密码:" + pwd, "data", staff);
}
public List<User> getStaffList(Long storeId) {
return userMapper.findByStoreId(storeId);
}
public void deleteStaff(Long staffId) {
userMapper.deleteById(staffId);
}
public User findById(Long id) {
return userMapper.findById(id).orElse(null);
}
public Map<String, Object> updateUser(Map<String, Object> params) {
Long userId = Long.valueOf(params.get("id").toString());
User user = userMapper.findById(userId).orElse(null);
if (user == null) {
return Map.of("code", 404, "message", "用户不存在");
}
if (params.containsKey("name") && params.get("name") != null) {
user.setName(params.get("name").toString());
}
if (params.containsKey("phone") && params.get("phone") != null) {
String newPhone = params.get("phone").toString();
// 验证码校验
String code = params.get("code") != null ? params.get("code").toString() : "";
if (!"123456".equals(code)) {
return Map.of("code", 400, "message", "验证码错误");
}
// 检查手机号是否被占用
User existing = userMapper.findByPhone(newPhone);
if (existing != null && !existing.getId().equals(userId)) {
return Map.of("code", 400, "message", "手机号已被占用");
}
user.setPhone(newPhone);
}
if (params.containsKey("avatar") && params.get("avatar") != null) {
user.setAvatar(params.get("avatar").toString());
}
user.setUpdateTime(LocalDateTime.now());
userMapper.save(user);
user.setPassword(null);
return Map.of("code", 200, "message", "更新成功", "data", user);
}
}

View File

@ -0,0 +1,42 @@
spring:
application:
name: petstore-backend
datasource:
url: jdbc:mysql://192.144.152.238:3306/petstore?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&autoReconnect=true
username: root
password: Wabjtam123@
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
max-lifetime: 1800000 # 30分钟必须小于MySQL的wait_timeout默认8小时
connection-test-query: SELECT 1
validation-timeout: 3000
idle-timeout: 600000 # 10分钟
jpa:
hibernate:
ddl-auto: update
show-sql: true
properties:
hibernate:
dialect: org.hibernate.dialect.MySQLDialect
format_sql: true
servlet:
multipart:
max-file-size: 10MB
server:
port: 8080
servlet:
context-path:
upload:
path: uploads
logging:
level:
com.petstore: debug
# 微信登录配置(需替换为实际值)
wechat:
appid: YOUR_APPID
appsecret: YOUR_APPSECRET
redirect_uri: http://localhost:8080/api/wechat/callback

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB