Compare commits

...

No commits in common. "e230317fe9b42f756d4abdb0b33bc54bb42fbb57" and "3c04019af4f29deff170ec5c77a74c88e8f90692" have entirely different histories.

33 changed files with 1783 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
target/
uploads/

201
LICENSE Normal file
View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

36
README.en.md Normal file
View File

@ -0,0 +1,36 @@
# petstore-backend
#### Description
宠伴生活馆
#### Software Architecture
Software architecture description
#### Installation
1. xxxx
2. xxxx
3. xxxx
#### Instructions
1. xxxx
2. xxxx
3. xxxx
#### Contribution
1. Fork the repository
2. Create Feat_xxx branch
3. Commit your code
4. Create Pull Request
#### Gitee Feature
1. You can use Readme\_XXX.md to support different languages, such as Readme\_en.md, Readme\_zh.md
2. Gitee blog [blog.gitee.com](https://blog.gitee.com)
3. Explore open source project [https://gitee.com/explore](https://gitee.com/explore)
4. The most valuable open source project [GVP](https://gitee.com/gvp)
5. The manual of Gitee [https://gitee.com/help](https://gitee.com/help)
6. The most popular members [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)

View File

@ -1,2 +1,37 @@
# petstore-backend # petstore-backend
#### 介绍
宠伴生活馆
#### 软件架构
软件架构说明
#### 安装教程
1. xxxx
2. xxxx
3. xxxx
#### 使用说明
1. xxxx
2. xxxx
3. xxxx
#### 参与贡献
1. Fork 本仓库
2. 新建 Feat_xxx 分支
3. 提交代码
4. 新建 Pull Request
#### 特技
1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md
2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com)
3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目
4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目
5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help)
6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)

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 {
@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", "");
String basePath = uploadPath.endsWith("/") ? uploadPath : uploadPath + "/";
File file = new File(basePath + 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", "");
String basePath = uploadPath.endsWith("/") ? uploadPath : uploadPath + "/";
File file = new File(basePath + 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 = uploadPath.endsWith("/") ? uploadPath + datePath : uploadPath + "/" + datePath;
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,119 @@
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 org.springframework.beans.factory.annotation.Value;
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;
@Value("${app.base-url:http://localhost:8080}")
private String baseUrl;
private String fullUrl(String path) {
if (path == null || path.isEmpty()) return path;
if (path.startsWith("http")) return path;
return baseUrl + 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