Compare commits
7 Commits
be63a3a377
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3604b4bb78 | ||
|
|
5b19922fd3 | ||
|
|
b4cbbb6939 | ||
|
|
d6c41f5db1 | ||
|
|
6e3c49d02d | ||
|
|
3c7c8d1f73 | ||
|
|
3c04019af4 |
@ -3,6 +3,7 @@ package com.petstore.controller;
|
||||
import com.petstore.entity.Appointment;
|
||||
import com.petstore.service.AppointmentService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
@ -21,24 +22,56 @@ public class AppointmentController {
|
||||
public Map<String, Object> list(
|
||||
@RequestParam(required = false) Long userId,
|
||||
@RequestParam(required = false) Long storeId,
|
||||
@RequestParam(required = false) String status) {
|
||||
@RequestParam(required = false) String status,
|
||||
@RequestParam(required = false) Integer page,
|
||||
@RequestParam(required = false) Integer pageSize) {
|
||||
|
||||
if (storeId == null && userId == null) {
|
||||
return Map.of("code", 400, "message", "userId或storeId必填");
|
||||
}
|
||||
|
||||
List<Appointment> appointments;
|
||||
boolean usePaging = page != null || pageSize != null;
|
||||
if (usePaging) {
|
||||
int pageNo = page == null ? 1 : page;
|
||||
int size = pageSize == null ? 20 : pageSize;
|
||||
pageNo = Math.max(pageNo, 1);
|
||||
size = Math.min(Math.max(size, 1), 200);
|
||||
Page<Appointment> paged = appointmentService.pageByScope(userId, storeId, status, pageNo - 1, size);
|
||||
return Map.of(
|
||||
"code", 200,
|
||||
"data", paged.getContent(),
|
||||
"page", pageNo,
|
||||
"pageSize", size,
|
||||
"total", paged.getTotalElements(),
|
||||
"totalPages", paged.getTotalPages(),
|
||||
"hasNext", paged.hasNext()
|
||||
);
|
||||
}
|
||||
|
||||
if (storeId != null) {
|
||||
appointments = (status != null && !status.isEmpty())
|
||||
? appointmentService.getByStoreIdAndStatus(storeId, status)
|
||||
: appointmentService.getByStoreId(storeId);
|
||||
} else if (userId != null) {
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
|
||||
/** 预约详情 */
|
||||
@GetMapping("/detail")
|
||||
public Map<String, Object> detail(@RequestParam Long id) {
|
||||
Appointment appointment = appointmentService.getById(id);
|
||||
if (appointment != null) {
|
||||
return Map.of("code", 200, "data", appointment);
|
||||
}
|
||||
return Map.of("code", 404, "message", "预约不存在");
|
||||
}
|
||||
|
||||
/** 创建预约 */
|
||||
@PostMapping("/create")
|
||||
public Map<String, Object> create(@RequestBody Map<String, Object> params) {
|
||||
@ -57,6 +90,9 @@ public class AppointmentController {
|
||||
if (params.containsKey("remark") && params.get("remark") != null) {
|
||||
appointment.setRemark(params.get("remark").toString());
|
||||
}
|
||||
if (params.containsKey("petId") && params.get("petId") != null && !params.get("petId").toString().isBlank()) {
|
||||
appointment.setPetId(Long.valueOf(params.get("petId").toString()));
|
||||
}
|
||||
|
||||
Appointment created = appointmentService.create(appointment);
|
||||
return Map.of("code", 200, "message", "创建成功", "data", created);
|
||||
|
||||
@ -26,38 +26,38 @@ import java.util.UUID;
|
||||
@CrossOrigin
|
||||
public class FileController {
|
||||
|
||||
private static final String UPLOAD_BASE = "/Users/wac/Desktop/www/_src/petstore/backend/uploads/";
|
||||
|
||||
@Value("${upload.path: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);
|
||||
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));
|
||||
.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);
|
||||
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));
|
||||
.contentType(MediaType.parseMediaType(contentType))
|
||||
.body(new FileSystemResource(file));
|
||||
}
|
||||
|
||||
@PostMapping("/image")
|
||||
@ -78,9 +78,9 @@ public class FileController {
|
||||
}
|
||||
|
||||
try {
|
||||
// 创建上传目录(使用绝对路径)
|
||||
// 创建上传目录
|
||||
String datePath = LocalDate.now().toString().replace("-", "/");
|
||||
String dirPath = UPLOAD_BASE + datePath; // /.../uploads/2026/04/01
|
||||
String dirPath = uploadPath.endsWith("/") ? uploadPath + datePath : uploadPath + "/" + datePath;
|
||||
File dir = new File(dirPath);
|
||||
if (!dir.exists()) dir.mkdirs();
|
||||
|
||||
|
||||
58
src/main/java/com/petstore/controller/PetController.java
Normal file
58
src/main/java/com/petstore/controller/PetController.java
Normal file
@ -0,0 +1,58 @@
|
||||
package com.petstore.controller;
|
||||
|
||||
import com.petstore.entity.Pet;
|
||||
import com.petstore.service.PetService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/pet")
|
||||
@RequiredArgsConstructor
|
||||
@CrossOrigin
|
||||
public class PetController {
|
||||
private final PetService petService;
|
||||
|
||||
/**
|
||||
* 列表:传 ownerUserId= 查该客户全部宠物;传 storeId= 查本店预约关联出现过的宠物(商家/员工)
|
||||
*/
|
||||
@GetMapping("/list")
|
||||
public Map<String, Object> list(@RequestParam(required = false) Long ownerUserId,
|
||||
@RequestParam(required = false) Long storeId) {
|
||||
if (ownerUserId != null) {
|
||||
return Map.of("code", 200, "data", petService.listByOwner(ownerUserId));
|
||||
}
|
||||
if (storeId != null) {
|
||||
return Map.of("code", 200, "data", petService.listByStoreServed(storeId));
|
||||
}
|
||||
return Map.of("code", 400, "message", "请传 ownerUserId 或 storeId");
|
||||
}
|
||||
|
||||
@PostMapping("/create")
|
||||
public Map<String, Object> create(@RequestBody Pet pet) {
|
||||
return petService.create(pet);
|
||||
}
|
||||
|
||||
@PutMapping("/update")
|
||||
public Map<String, Object> update(@RequestBody Map<String, Object> body) {
|
||||
Long operatorUserId = Long.valueOf(body.get("operatorUserId").toString());
|
||||
String role = body.get("role").toString();
|
||||
Pet input = new Pet();
|
||||
input.setId(Long.valueOf(body.get("id").toString()));
|
||||
if (body.containsKey("name")) input.setName(body.get("name").toString());
|
||||
if (body.containsKey("petType")) input.setPetType(body.get("petType").toString());
|
||||
if (body.containsKey("breed")) input.setBreed(body.get("breed") != null ? body.get("breed").toString() : null);
|
||||
if (body.containsKey("avatar")) input.setAvatar(body.get("avatar") != null ? body.get("avatar").toString() : null);
|
||||
if (body.containsKey("remark")) input.setRemark(body.get("remark") != null ? body.get("remark").toString() : null);
|
||||
return petService.update(operatorUserId, role, input);
|
||||
}
|
||||
|
||||
@DeleteMapping("/delete")
|
||||
public Map<String, Object> delete(@RequestParam Long id,
|
||||
@RequestParam Long operatorUserId,
|
||||
@RequestParam String role) {
|
||||
return petService.delete(id, operatorUserId, role);
|
||||
}
|
||||
}
|
||||
@ -6,6 +6,8 @@ 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 org.springframework.data.domain.Page;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@ -20,12 +22,13 @@ public class ReportController {
|
||||
private final ReportService reportService;
|
||||
private final StoreService storeService;
|
||||
|
||||
private static final String BASE_URL = "http://localhost:8080";
|
||||
@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 BASE_URL + path;
|
||||
return baseUrl + path;
|
||||
}
|
||||
|
||||
@PostMapping("/create")
|
||||
@ -45,8 +48,22 @@ public class ReportController {
|
||||
|
||||
@GetMapping("/list")
|
||||
public Map<String, Object> list(@RequestParam(required = false) Long storeId,
|
||||
@RequestParam(required = false) Long userId) {
|
||||
List<Report> reports = reportService.list(storeId, userId);
|
||||
@RequestParam(required = false) Long userId,
|
||||
@RequestParam(required = false) Integer page,
|
||||
@RequestParam(required = false) Integer pageSize) {
|
||||
boolean usePaging = page != null || pageSize != null;
|
||||
List<Report> reports;
|
||||
Page<Report> paged = null;
|
||||
Integer pageNo = null;
|
||||
Integer size = null;
|
||||
if (usePaging) {
|
||||
pageNo = Math.max(page == null ? 1 : page, 1);
|
||||
size = Math.min(Math.max(pageSize == null ? 20 : pageSize, 1), 200);
|
||||
paged = reportService.page(storeId, userId, pageNo - 1, size);
|
||||
reports = paged.getContent();
|
||||
} else {
|
||||
reports = reportService.list(storeId, userId);
|
||||
}
|
||||
// 附加技师名称,并补全图片URL
|
||||
List<Map<String, Object>> data = reports.stream().map(r -> {
|
||||
Map<String, Object> item = new HashMap<>();
|
||||
@ -64,6 +81,17 @@ public class ReportController {
|
||||
item.put("userId", r.getUserId());
|
||||
return item;
|
||||
}).collect(Collectors.toList());
|
||||
if (usePaging && paged != null) {
|
||||
return Map.of(
|
||||
"code", 200,
|
||||
"data", data,
|
||||
"page", pageNo,
|
||||
"pageSize", size,
|
||||
"total", paged.getTotalElements(),
|
||||
"totalPages", paged.getTotalPages(),
|
||||
"hasNext", paged.hasNext()
|
||||
);
|
||||
}
|
||||
return Map.of("code", 200, "data", data);
|
||||
}
|
||||
|
||||
|
||||
@ -17,8 +17,9 @@ public class ServiceTypeController {
|
||||
private final ServiceTypeService serviceTypeService;
|
||||
|
||||
@GetMapping("/list")
|
||||
public Map<String, Object> list(@RequestParam Long storeId) {
|
||||
List<ServiceType> list = serviceTypeService.getByStoreId(storeId);
|
||||
public Map<String, Object> list(@RequestParam(required = false) Long storeId) {
|
||||
List<ServiceType> list =
|
||||
storeId == null ? serviceTypeService.listSystemDefaultsOnly() : serviceTypeService.getByStoreId(storeId);
|
||||
return Map.of("code", 200, "data", list);
|
||||
}
|
||||
|
||||
|
||||
@ -6,7 +6,9 @@ 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/store")
|
||||
@ -25,6 +27,26 @@ public class StoreController {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户预约:门店列表(不含邀请码等敏感字段)
|
||||
*/
|
||||
@GetMapping("/list")
|
||||
public Map<String, Object> list() {
|
||||
List<Store> all = storeService.listAll();
|
||||
List<Map<String, Object>> rows = all.stream().map(s -> {
|
||||
Map<String, Object> m = new HashMap<>();
|
||||
m.put("id", s.getId());
|
||||
m.put("name", s.getName());
|
||||
m.put("address", s.getAddress());
|
||||
m.put("latitude", s.getLatitude());
|
||||
m.put("longitude", s.getLongitude());
|
||||
m.put("phone", s.getPhone());
|
||||
m.put("logo", s.getLogo());
|
||||
return m;
|
||||
}).collect(Collectors.toList());
|
||||
return Map.of("code", 200, "data", rows);
|
||||
}
|
||||
|
||||
@GetMapping("/get")
|
||||
public Map<String, Object> get(@RequestParam Long id) {
|
||||
Store store = storeService.findById(id);
|
||||
|
||||
@ -2,19 +2,23 @@ package com.petstore.controller;
|
||||
|
||||
import com.petstore.entity.User;
|
||||
import com.petstore.service.UserService;
|
||||
import com.petstore.service.WechatMiniProgramService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/user")
|
||||
@RequiredArgsConstructor
|
||||
@CrossOrigin
|
||||
public class UserController {
|
||||
private final UserService userService;
|
||||
private final WechatMiniProgramService wechatMiniProgramService;
|
||||
|
||||
/** 老板注册店铺 */
|
||||
@PostMapping("/register-boss")
|
||||
@ -34,6 +38,36 @@ public class UserController {
|
||||
return userService.login(phone, code);
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信小程序:button open-type=getPhoneNumber 拿到的 phoneCode 换手机号并登录。
|
||||
*/
|
||||
@PostMapping("/wx-phone-login")
|
||||
public Map<String, Object> wxPhoneLogin(@RequestBody(required = false) Map<String, String> params) {
|
||||
try {
|
||||
if (params == null) {
|
||||
return Map.of("code", 400, "message", "请求体不能为空");
|
||||
}
|
||||
String phoneCode = params.get("phoneCode");
|
||||
if (phoneCode == null || phoneCode.isBlank()) {
|
||||
return Map.of("code", 400, "message", "缺少手机号授权码");
|
||||
}
|
||||
var wx = wechatMiniProgramService.exchangePhoneCode(phoneCode);
|
||||
if (!wx.isOk()) {
|
||||
Map<String, Object> err = new HashMap<>();
|
||||
err.put("code", 400);
|
||||
err.put("message", wx.errorMessage() != null ? wx.errorMessage() : "微信登录失败");
|
||||
return err;
|
||||
}
|
||||
return userService.loginByVerifiedPhone(wx.phone());
|
||||
} catch (Exception e) {
|
||||
log.error("wx-phone-login 异常", e);
|
||||
Map<String, Object> err = new HashMap<>();
|
||||
err.put("code", 500);
|
||||
err.put("message", "登录处理失败: " + e.getMessage());
|
||||
return err;
|
||||
}
|
||||
}
|
||||
|
||||
/** 员工注册(邀请码方式) */
|
||||
@PostMapping("/register-staff")
|
||||
public Map<String, Object> registerStaff(@RequestBody Map<String, String> params) {
|
||||
|
||||
@ -6,7 +6,13 @@ import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "t_appointment")
|
||||
@Table(
|
||||
name = "t_appointment",
|
||||
indexes = {
|
||||
@Index(name = "idx_appt_store_status_time", columnList = "store_id,status,appointment_time"),
|
||||
@Index(name = "idx_appt_user_status_time", columnList = "user_id,status,appointment_time")
|
||||
}
|
||||
)
|
||||
public class Appointment {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@ -26,6 +32,10 @@ public class Appointment {
|
||||
@Column(name = "user_id")
|
||||
private Long userId;
|
||||
|
||||
/** 关联宠物档案(可选,用于本店「服务过的宠物」统计) */
|
||||
@Column(name = "pet_id")
|
||||
private Long petId;
|
||||
|
||||
/** 技师ID,开始服务时赋值 */
|
||||
@Column(name = "assigned_user_id")
|
||||
private Long assignedUserId;
|
||||
|
||||
43
src/main/java/com/petstore/entity/Pet.java
Normal file
43
src/main/java/com/petstore/entity/Pet.java
Normal file
@ -0,0 +1,43 @@
|
||||
package com.petstore.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 宠物档案(归属某客户;商家/员工通过预约关联查看本店服务过的宠物)
|
||||
*/
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "t_pet")
|
||||
public class Pet {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
/** 昵称 */
|
||||
private String name;
|
||||
|
||||
/** 类型:猫/狗/其他 */
|
||||
@Column(name = "pet_type")
|
||||
private String petType;
|
||||
|
||||
/** 品种(可选) */
|
||||
private String breed;
|
||||
|
||||
/** 头像相对路径 */
|
||||
private String avatar;
|
||||
|
||||
/** 所属用户(客户) */
|
||||
@Column(name = "owner_user_id")
|
||||
private Long ownerUserId;
|
||||
|
||||
private String remark;
|
||||
|
||||
@Column(name = "create_time")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@Column(name = "update_time")
|
||||
private LocalDateTime updateTime;
|
||||
}
|
||||
@ -7,7 +7,14 @@ import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "t_report")
|
||||
@Table(
|
||||
name = "t_report",
|
||||
indexes = {
|
||||
@Index(name = "idx_report_appointment_id", columnList = "appointment_id"),
|
||||
@Index(name = "idx_report_token", columnList = "report_token"),
|
||||
@Index(name = "idx_report_store_user_time", columnList = "store_id,user_id,create_time")
|
||||
}
|
||||
)
|
||||
public class Report {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
|
||||
@ -6,7 +6,12 @@ import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "t_service_type")
|
||||
@Table(
|
||||
name = "t_service_type",
|
||||
indexes = {
|
||||
@Index(name = "idx_service_type_store_id", columnList = "store_id")
|
||||
}
|
||||
)
|
||||
public class ServiceType {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
|
||||
@ -6,7 +6,13 @@ import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "t_store")
|
||||
@Table(
|
||||
name = "t_store",
|
||||
indexes = {
|
||||
@Index(name = "idx_store_invite_code", columnList = "invite_code"),
|
||||
@Index(name = "idx_store_owner_id", columnList = "owner_id")
|
||||
}
|
||||
)
|
||||
public class Store {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
|
||||
@ -6,7 +6,13 @@ import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "t_user")
|
||||
@Table(
|
||||
name = "t_user",
|
||||
indexes = {
|
||||
@Index(name = "idx_user_phone", columnList = "phone"),
|
||||
@Index(name = "idx_user_store_id", columnList = "store_id")
|
||||
}
|
||||
)
|
||||
public class User {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
package com.petstore.mapper;
|
||||
|
||||
import com.petstore.entity.Appointment;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
@ -10,4 +12,9 @@ public interface AppointmentMapper extends JpaRepository<Appointment, Long> {
|
||||
List<Appointment> findByUserIdAndStatus(Long userId, String status);
|
||||
List<Appointment> findByStoreId(Long storeId);
|
||||
List<Appointment> findByStoreIdAndStatus(Long storeId, String status);
|
||||
|
||||
Page<Appointment> findByUserId(Long userId, Pageable pageable);
|
||||
Page<Appointment> findByUserIdAndStatus(Long userId, String status, Pageable pageable);
|
||||
Page<Appointment> findByStoreId(Long storeId, Pageable pageable);
|
||||
Page<Appointment> findByStoreIdAndStatus(Long storeId, String status, Pageable pageable);
|
||||
}
|
||||
|
||||
17
src/main/java/com/petstore/mapper/PetMapper.java
Normal file
17
src/main/java/com/petstore/mapper/PetMapper.java
Normal file
@ -0,0 +1,17 @@
|
||||
package com.petstore.mapper;
|
||||
|
||||
import com.petstore.entity.Pet;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface PetMapper extends JpaRepository<Pet, Long> {
|
||||
|
||||
List<Pet> findByOwnerUserIdOrderByUpdateTimeDesc(Long ownerUserId);
|
||||
|
||||
/** 本店预约中关联过的宠物(预约.pet_id 指向该宠物) */
|
||||
@Query("SELECT DISTINCT p FROM Pet p, Appointment a WHERE a.petId = p.id AND a.storeId = :storeId ORDER BY p.updateTime DESC")
|
||||
List<Pet> findDistinctPetsServedAtStore(@Param("storeId") Long storeId);
|
||||
}
|
||||
@ -1,7 +1,29 @@
|
||||
package com.petstore.mapper;
|
||||
|
||||
import com.petstore.entity.Report;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface ReportMapper extends JpaRepository<Report, Long> {
|
||||
Optional<Report> findFirstByAppointmentIdOrderByCreateTimeDesc(Long appointmentId);
|
||||
|
||||
Optional<Report> findFirstByReportToken(String reportToken);
|
||||
|
||||
List<Report> findByStoreIdOrderByCreateTimeDesc(Long storeId);
|
||||
|
||||
List<Report> findByUserIdOrderByCreateTimeDesc(Long userId);
|
||||
|
||||
List<Report> findByStoreIdAndUserIdOrderByCreateTimeDesc(Long storeId, Long userId);
|
||||
|
||||
List<Report> findAllByOrderByCreateTimeDesc();
|
||||
|
||||
Page<Report> findByStoreId(Long storeId, Pageable pageable);
|
||||
|
||||
Page<Report> findByUserId(Long userId, Pageable pageable);
|
||||
|
||||
Page<Report> findByStoreIdAndUserId(Long storeId, Long userId, Pageable pageable);
|
||||
}
|
||||
|
||||
@ -8,4 +8,7 @@ import java.util.List;
|
||||
public interface ServiceTypeMapper extends JpaRepository<ServiceType, Long> {
|
||||
List<ServiceType> findByStoreIdOrStoreIdIsNull(Long storeId);
|
||||
List<ServiceType> findByStoreId(Long storeId);
|
||||
|
||||
/** 系统内置(store_id 为空) */
|
||||
List<ServiceType> findByStoreIdIsNull();
|
||||
}
|
||||
|
||||
@ -3,6 +3,10 @@ package com.petstore.service;
|
||||
import com.petstore.entity.Appointment;
|
||||
import com.petstore.mapper.AppointmentMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
@ -33,6 +37,30 @@ public class AppointmentService {
|
||||
return appointmentMapper.findByStoreIdAndStatus(storeId, status);
|
||||
}
|
||||
|
||||
public Page<Appointment> pageByScope(Long userId, Long storeId, String status, int pageNo, int pageSize) {
|
||||
Pageable pageable = PageRequest.of(
|
||||
Math.max(pageNo, 0),
|
||||
Math.max(pageSize, 1),
|
||||
Sort.by(Sort.Direction.DESC, "appointmentTime")
|
||||
);
|
||||
boolean hasStatus = status != null && !status.isBlank();
|
||||
if (storeId != null) {
|
||||
return hasStatus
|
||||
? appointmentMapper.findByStoreIdAndStatus(storeId, status, pageable)
|
||||
: appointmentMapper.findByStoreId(storeId, pageable);
|
||||
}
|
||||
if (userId != null) {
|
||||
return hasStatus
|
||||
? appointmentMapper.findByUserIdAndStatus(userId, status, pageable)
|
||||
: appointmentMapper.findByUserId(userId, pageable);
|
||||
}
|
||||
return Page.empty(pageable);
|
||||
}
|
||||
|
||||
public Appointment getById(Long id) {
|
||||
return appointmentMapper.findById(id).orElse(null);
|
||||
}
|
||||
|
||||
public Appointment create(Appointment appointment) {
|
||||
appointment.setCreateTime(LocalDateTime.now());
|
||||
appointment.setUpdateTime(LocalDateTime.now());
|
||||
|
||||
98
src/main/java/com/petstore/service/PetService.java
Normal file
98
src/main/java/com/petstore/service/PetService.java
Normal file
@ -0,0 +1,98 @@
|
||||
package com.petstore.service;
|
||||
|
||||
import com.petstore.entity.Pet;
|
||||
import com.petstore.mapper.PetMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class PetService {
|
||||
private final PetMapper petMapper;
|
||||
|
||||
/** 客户:仅自己的宠物 */
|
||||
public List<Pet> listByOwner(Long ownerUserId) {
|
||||
if (ownerUserId == null) {
|
||||
return List.of();
|
||||
}
|
||||
return petMapper.findByOwnerUserIdOrderByUpdateTimeDesc(ownerUserId);
|
||||
}
|
||||
|
||||
/** 商家/员工:本店预约中关联过的宠物(预约带 pet_id) */
|
||||
public List<Pet> listByStoreServed(Long storeId) {
|
||||
if (storeId == null) {
|
||||
return List.of();
|
||||
}
|
||||
return petMapper.findDistinctPetsServedAtStore(storeId);
|
||||
}
|
||||
|
||||
public Map<String, Object> create(Pet pet) {
|
||||
if (pet.getName() == null || pet.getName().isBlank()) {
|
||||
return Map.of("code", 400, "message", "请填写宠物名字");
|
||||
}
|
||||
if (pet.getPetType() == null || pet.getPetType().isBlank()) {
|
||||
return Map.of("code", 400, "message", "请选择宠物类型");
|
||||
}
|
||||
if (pet.getOwnerUserId() == null) {
|
||||
return Map.of("code", 400, "message", "缺少所属用户");
|
||||
}
|
||||
pet.setCreateTime(LocalDateTime.now());
|
||||
pet.setUpdateTime(LocalDateTime.now());
|
||||
Pet saved = petMapper.save(pet);
|
||||
return Map.of("code", 200, "message", "保存成功", "data", saved);
|
||||
}
|
||||
|
||||
public Map<String, Object> update(Long operatorUserId, String role, Pet input) {
|
||||
if (input.getId() == null) {
|
||||
return Map.of("code", 400, "message", "缺少宠物ID");
|
||||
}
|
||||
Optional<Pet> opt = petMapper.findById(input.getId());
|
||||
if (opt.isEmpty()) {
|
||||
return Map.of("code", 404, "message", "宠物不存在");
|
||||
}
|
||||
Pet pet = opt.get();
|
||||
if ("customer".equals(role)) {
|
||||
if (!pet.getOwnerUserId().equals(operatorUserId)) {
|
||||
return Map.of("code", 403, "message", "无权修改该宠物");
|
||||
}
|
||||
} else {
|
||||
return Map.of("code", 403, "message", "仅客户可编辑自己的宠物档案");
|
||||
}
|
||||
if (input.getName() != null && !input.getName().isBlank()) {
|
||||
pet.setName(input.getName());
|
||||
}
|
||||
if (input.getPetType() != null && !input.getPetType().isBlank()) {
|
||||
pet.setPetType(input.getPetType());
|
||||
}
|
||||
if (input.getBreed() != null) {
|
||||
pet.setBreed(input.getBreed());
|
||||
}
|
||||
if (input.getAvatar() != null) {
|
||||
pet.setAvatar(input.getAvatar());
|
||||
}
|
||||
if (input.getRemark() != null) {
|
||||
pet.setRemark(input.getRemark());
|
||||
}
|
||||
pet.setUpdateTime(LocalDateTime.now());
|
||||
petMapper.save(pet);
|
||||
return Map.of("code", 200, "message", "更新成功", "data", pet);
|
||||
}
|
||||
|
||||
public Map<String, Object> delete(Long petId, Long operatorUserId, String role) {
|
||||
Optional<Pet> opt = petMapper.findById(petId);
|
||||
if (opt.isEmpty()) {
|
||||
return Map.of("code", 404, "message", "宠物不存在");
|
||||
}
|
||||
Pet pet = opt.get();
|
||||
if (!"customer".equals(role) || !pet.getOwnerUserId().equals(operatorUserId)) {
|
||||
return Map.of("code", 403, "message", "无权删除");
|
||||
}
|
||||
petMapper.deleteById(petId);
|
||||
return Map.of("code", 200, "message", "已删除");
|
||||
}
|
||||
}
|
||||
@ -7,12 +7,15 @@ import com.petstore.mapper.AppointmentMapper;
|
||||
import com.petstore.mapper.ReportMapper;
|
||||
import com.petstore.mapper.UserMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@ -64,23 +67,47 @@ public class ReportService {
|
||||
}
|
||||
|
||||
public Report getByAppointmentId(Long appointmentId) {
|
||||
return reportMapper.findAll().stream()
|
||||
.filter(r -> r.getAppointmentId().equals(appointmentId))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
if (appointmentId == null) {
|
||||
return null;
|
||||
}
|
||||
return reportMapper.findFirstByAppointmentIdOrderByCreateTimeDesc(appointmentId).orElse(null);
|
||||
}
|
||||
|
||||
public Report getByToken(String token) {
|
||||
return reportMapper.findAll().stream()
|
||||
.filter(r -> token.equals(r.getReportToken()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
if (token == null || token.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
return reportMapper.findFirstByReportToken(token).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());
|
||||
if (storeId != null && userId != null) {
|
||||
return reportMapper.findByStoreIdAndUserIdOrderByCreateTimeDesc(storeId, userId);
|
||||
}
|
||||
if (storeId != null) {
|
||||
return reportMapper.findByStoreIdOrderByCreateTimeDesc(storeId);
|
||||
}
|
||||
if (userId != null) {
|
||||
return reportMapper.findByUserIdOrderByCreateTimeDesc(userId);
|
||||
}
|
||||
return reportMapper.findAllByOrderByCreateTimeDesc();
|
||||
}
|
||||
|
||||
public Page<Report> page(Long storeId, Long userId, int pageNo, int pageSize) {
|
||||
Pageable pageable = PageRequest.of(
|
||||
Math.max(pageNo, 0),
|
||||
Math.max(pageSize, 1),
|
||||
Sort.by(Sort.Direction.DESC, "createTime")
|
||||
);
|
||||
if (storeId != null && userId != null) {
|
||||
return reportMapper.findByStoreIdAndUserId(storeId, userId, pageable);
|
||||
}
|
||||
if (storeId != null) {
|
||||
return reportMapper.findByStoreId(storeId, pageable);
|
||||
}
|
||||
if (userId != null) {
|
||||
return reportMapper.findByUserId(userId, pageable);
|
||||
}
|
||||
return reportMapper.findAll(pageable);
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,6 +18,11 @@ public class ServiceTypeService {
|
||||
return serviceTypeMapper.findByStoreIdOrStoreIdIsNull(storeId);
|
||||
}
|
||||
|
||||
/** 仅系统默认(未传门店或 C 端尚未绑定门店时用于展示可选名称) */
|
||||
public List<ServiceType> listSystemDefaultsOnly() {
|
||||
return serviceTypeMapper.findByStoreIdIsNull();
|
||||
}
|
||||
|
||||
/** 老板新增服务类型 */
|
||||
public ServiceType create(Long storeId, String name) {
|
||||
ServiceType st = new ServiceType();
|
||||
|
||||
@ -6,6 +6,7 @@ import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
@ -28,6 +29,11 @@ public class StoreService {
|
||||
return storeMapper.findByInviteCode(code);
|
||||
}
|
||||
|
||||
/** C 端预约:列出全部门店供选择 */
|
||||
public List<Store> listAll() {
|
||||
return storeMapper.findAll();
|
||||
}
|
||||
|
||||
public Store update(Store store) {
|
||||
store.setUpdateTime(LocalDateTime.now());
|
||||
return storeMapper.save(store);
|
||||
|
||||
@ -55,13 +55,23 @@ public class UserService {
|
||||
if (!"123456".equals(code)) {
|
||||
return Map.of("code", 401, "message", "验证码错误");
|
||||
}
|
||||
return loginByVerifiedPhone(phone);
|
||||
}
|
||||
|
||||
/**
|
||||
* 已通过短信或微信授权校验后的手机号登录(与验证码登录成功后的逻辑一致)。
|
||||
*/
|
||||
public Map<String, Object> loginByVerifiedPhone(String phone) {
|
||||
if (phone == null || !phone.matches("^1\\d{10}$")) {
|
||||
return Map.of("code", 400, "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.setPassword("wx_no_password");
|
||||
user.setRole("customer");
|
||||
user.setCreateTime(LocalDateTime.now());
|
||||
user.setUpdateTime(LocalDateTime.now());
|
||||
|
||||
250
src/main/java/com/petstore/service/WechatMiniProgramService.java
Normal file
250
src/main/java/com/petstore/service/WechatMiniProgramService.java
Normal file
@ -0,0 +1,250 @@
|
||||
package com.petstore.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.petstore.config.WechatConfig;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URLEncoder;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 微信小程序:通过 getPhoneNumber 返回的 code 换取用户手机号。
|
||||
* 使用 JDK HttpClient 直连微信(避免部分环境下 RestTemplate 对响应解析为空)。
|
||||
* 文档:https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/phonenumber/phonenumber.getPhoneNumber.html
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class WechatMiniProgramService {
|
||||
private final WechatConfig wechatConfig;
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
/** 部分代理/网关对 HTTP/2 支持差,固定 HTTP/1.1 更稳 */
|
||||
private final HttpClient httpClient = HttpClient.newBuilder()
|
||||
.connectTimeout(Duration.ofSeconds(15))
|
||||
.followRedirects(HttpClient.Redirect.NORMAL)
|
||||
.version(HttpClient.Version.HTTP_1_1)
|
||||
.build();
|
||||
|
||||
private volatile String cachedAccessToken;
|
||||
private volatile long tokenExpiresAtEpochMs;
|
||||
|
||||
public record WxPhoneExchangeResult(String phone, String errorMessage) {
|
||||
public static WxPhoneExchangeResult ok(String phone) {
|
||||
return new WxPhoneExchangeResult(phone, null);
|
||||
}
|
||||
|
||||
public static WxPhoneExchangeResult fail(String errorMessage) {
|
||||
String msg = (errorMessage != null && !errorMessage.isBlank()) ? errorMessage : "未知错误";
|
||||
return new WxPhoneExchangeResult(null, msg);
|
||||
}
|
||||
|
||||
public boolean isOk() {
|
||||
return phone != null && !phone.isBlank();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isConfigured() {
|
||||
String id = wechatConfig.getAppid();
|
||||
String sec = wechatConfig.getAppsecret();
|
||||
return id != null && !id.isBlank()
|
||||
&& sec != null && !sec.isBlank()
|
||||
&& !"YOUR_APPID".equals(id)
|
||||
&& !"YOUR_APPSECRET".equals(sec);
|
||||
}
|
||||
|
||||
private void invalidateAccessToken() {
|
||||
cachedAccessToken = null;
|
||||
tokenExpiresAtEpochMs = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param phoneCode 前端 button getPhoneNumber 回调中的 detail.code
|
||||
*/
|
||||
public WxPhoneExchangeResult exchangePhoneCode(String phoneCode) {
|
||||
if (phoneCode == null || phoneCode.isBlank()) {
|
||||
return WxPhoneExchangeResult.fail("缺少手机号授权码");
|
||||
}
|
||||
if (!isConfigured()) {
|
||||
log.warn("微信 appid/appsecret 未配置或为占位符,无法换取手机号");
|
||||
return WxPhoneExchangeResult.fail("服务端未配置微信小程序 AppID/AppSecret,或仍为占位符 YOUR_APPID");
|
||||
}
|
||||
|
||||
for (int attempt = 0; attempt < 2; attempt++) {
|
||||
try {
|
||||
String accessToken = getAccessToken();
|
||||
if (accessToken == null) {
|
||||
return WxPhoneExchangeResult.fail("无法获取微信 access_token,请核对服务端 AppID/AppSecret 是否与微信公众平台一致");
|
||||
}
|
||||
String url = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=" + accessToken;
|
||||
String jsonBody = objectMapper.writeValueAsString(Map.of("code", phoneCode));
|
||||
HttpResult hr = httpPostJson(url, jsonBody);
|
||||
String body = hr.body();
|
||||
if (body == null || body.isBlank()) {
|
||||
log.warn("getuserphonenumber 响应为空, httpStatus={}, urlLen={}", hr.statusCode(), url.length());
|
||||
return WxPhoneExchangeResult.fail(emptyBodyHint("getuserphonenumber", hr.statusCode()));
|
||||
}
|
||||
JsonNode root;
|
||||
try {
|
||||
root = objectMapper.readTree(body);
|
||||
} catch (Exception parseEx) {
|
||||
log.warn("解析微信响应失败 body={}", body, parseEx);
|
||||
return WxPhoneExchangeResult.fail("微信返回非 JSON(HTTP " + hr.statusCode() + "),可能被网关替换");
|
||||
}
|
||||
|
||||
if (root.hasNonNull("phone_info")) {
|
||||
JsonNode pi = root.path("phone_info");
|
||||
String raw = pi.path("phoneNumber").asText("");
|
||||
if (raw.isBlank()) {
|
||||
raw = pi.path("purePhoneNumber").asText("");
|
||||
}
|
||||
String phone = normalizeMainlandPhone(raw);
|
||||
if (phone == null) {
|
||||
log.warn("无法解析手机号,原始响应: {}", body);
|
||||
return WxPhoneExchangeResult.fail("微信返回的手机号格式异常");
|
||||
}
|
||||
return WxPhoneExchangeResult.ok(phone);
|
||||
}
|
||||
|
||||
int errcode = root.path("errcode").asInt(-1);
|
||||
String errmsg = root.path("errmsg").asText("");
|
||||
log.warn("getuserphonenumber 无 phone_info: {}", body);
|
||||
if ((errcode == 40001 || errcode == 42001) && attempt == 0) {
|
||||
invalidateAccessToken();
|
||||
continue;
|
||||
}
|
||||
if (errcode <= 0 && errmsg.isBlank()) {
|
||||
return WxPhoneExchangeResult.fail("微信未返回手机号,请重新点击授权");
|
||||
}
|
||||
return WxPhoneExchangeResult.fail(formatWxError(errcode, errmsg));
|
||||
} catch (Exception e) {
|
||||
log.warn("换取手机号异常", e);
|
||||
return WxPhoneExchangeResult.fail("连接微信服务异常: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
return WxPhoneExchangeResult.fail("获取手机号失败,请重试");
|
||||
}
|
||||
|
||||
private static String emptyBodyHint(String api, int status) {
|
||||
return "微信 " + api + " 返回空内容(HTTP " + status + ")。"
|
||||
+ "请在该机器上测试能否访问 api.weixin.qq.com:443(curl/浏览器),检查防火墙、安全组、出站 HTTPS、公司代理;"
|
||||
+ "需代理时可设置 JVM 参数 https.proxyHost / https.proxyPort;若 IPv6 异常可试 -Djava.net.preferIPv4Stack=true";
|
||||
}
|
||||
|
||||
private static String formatWxError(int errcode, String errmsg) {
|
||||
String base = (errmsg != null && !errmsg.isBlank()) ? errmsg : "未知错误";
|
||||
String hint = switch (errcode) {
|
||||
case 40029 -> "(code 无效或已过期,请重新点击「微信授权登录」)";
|
||||
case 40163 -> "(code 已被使用,请重新授权)";
|
||||
case 40001, 42001 -> "(access_token 无效,已自动重试;若仍失败请检查 AppSecret)";
|
||||
case 40125 -> "(AppSecret 错误,请登录微信公众平台核对后更新服务端配置)";
|
||||
case 40013 -> "(AppID 无效,请核对是否为小程序 AppID)";
|
||||
default -> "";
|
||||
};
|
||||
if (errcode > 0) {
|
||||
return "微信接口错误 " + errcode + ":" + base + hint;
|
||||
}
|
||||
return "获取手机号失败:" + base;
|
||||
}
|
||||
|
||||
private String getAccessToken() {
|
||||
long now = System.currentTimeMillis();
|
||||
if (cachedAccessToken != null && now < tokenExpiresAtEpochMs - 60_000) {
|
||||
return cachedAccessToken;
|
||||
}
|
||||
synchronized (this) {
|
||||
now = System.currentTimeMillis();
|
||||
if (cachedAccessToken != null && now < tokenExpiresAtEpochMs - 60_000) {
|
||||
return cachedAccessToken;
|
||||
}
|
||||
try {
|
||||
String appid = URLEncoder.encode(wechatConfig.getAppid(), StandardCharsets.UTF_8);
|
||||
String secret = URLEncoder.encode(wechatConfig.getAppsecret(), StandardCharsets.UTF_8);
|
||||
String url = String.format(
|
||||
"https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s",
|
||||
appid, secret);
|
||||
HttpResult hr = httpGet(url);
|
||||
String body = hr.body();
|
||||
if (body == null || body.isBlank()) {
|
||||
log.warn("获取 access_token 微信返回空 body, http={}", hr.statusCode());
|
||||
return null;
|
||||
}
|
||||
JsonNode root;
|
||||
try {
|
||||
root = objectMapper.readTree(body);
|
||||
} catch (Exception parseEx) {
|
||||
log.warn("解析 token 响应失败 body={}", body, parseEx);
|
||||
return null;
|
||||
}
|
||||
if (root.hasNonNull("errcode") && root.path("errcode").asInt() != 0) {
|
||||
int ec = root.path("errcode").asInt();
|
||||
String em = root.path("errmsg").asText("");
|
||||
log.warn("获取 access_token 失败: errcode={} errmsg={} body={}", ec, em, body);
|
||||
return null;
|
||||
}
|
||||
String token = root.path("access_token").asText(null);
|
||||
int expiresIn = root.path("expires_in").asInt(7200);
|
||||
if (token == null || token.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
cachedAccessToken = token;
|
||||
tokenExpiresAtEpochMs = System.currentTimeMillis() + expiresIn * 1000L;
|
||||
return cachedAccessToken;
|
||||
} catch (Exception e) {
|
||||
log.warn("获取 access_token 异常", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private record HttpResult(int statusCode, String body) {}
|
||||
|
||||
private HttpResult httpGet(String url) throws Exception {
|
||||
HttpRequest req = HttpRequest.newBuilder()
|
||||
.uri(URI.create(url))
|
||||
.timeout(Duration.ofSeconds(25))
|
||||
.header("Accept", "application/json, text/plain, */*")
|
||||
.header("User-Agent", "PetstoreBackend/1.0")
|
||||
.GET()
|
||||
.build();
|
||||
HttpResponse<String> resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
|
||||
return new HttpResult(resp.statusCode(), resp.body());
|
||||
}
|
||||
|
||||
private HttpResult httpPostJson(String url, String jsonBody) throws Exception {
|
||||
HttpRequest req = HttpRequest.newBuilder()
|
||||
.uri(URI.create(url))
|
||||
.timeout(Duration.ofSeconds(25))
|
||||
.header("Content-Type", "application/json; charset=UTF-8")
|
||||
.header("Accept", "application/json, text/plain, */*")
|
||||
.header("User-Agent", "PetstoreBackend/1.0")
|
||||
.POST(HttpRequest.BodyPublishers.ofString(jsonBody, StandardCharsets.UTF_8))
|
||||
.build();
|
||||
HttpResponse<String> resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
|
||||
return new HttpResult(resp.statusCode(), resp.body());
|
||||
}
|
||||
|
||||
private String normalizeMainlandPhone(String raw) {
|
||||
if (raw == null || raw.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
String digits = raw.replaceAll("\\D", "");
|
||||
if (digits.startsWith("86") && digits.length() == 13) {
|
||||
digits = digits.substring(2);
|
||||
}
|
||||
if (digits.length() == 11) {
|
||||
return digits;
|
||||
}
|
||||
log.warn("无法解析为 11 位手机号: {}", raw);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -35,8 +35,8 @@ logging:
|
||||
level:
|
||||
com.petstore: debug
|
||||
|
||||
# 微信登录配置(需替换为实际值)
|
||||
# 微信小程序(与 manifest / 微信后台一致;生产环境建议用环境变量覆盖)
|
||||
wechat:
|
||||
appid: YOUR_APPID
|
||||
appsecret: YOUR_APPSECRET
|
||||
appid: ${WECHAT_APPID:wx8ca2dfa89af72edf}
|
||||
appsecret: ${WECHAT_APPSECRET:7afb49f3a31fe9b5a083c7c40be45c5b}
|
||||
redirect_uri: http://localhost:8080/api/wechat/callback
|
||||
|
||||
Loading…
Reference in New Issue
Block a user