Merge remote-tracking branch 'origin/main'

This commit is contained in:
2026-02-26 10:49:03 +08:00
118 changed files with 1234 additions and 568 deletions

271
.gitignore vendored
View File

@@ -1,23 +1,83 @@
# Dependencies
# ============================================
# 爱维宠物医院管理系统 - Gitignore
# Java Spring Boot + Vue.js 全栈项目
# ============================================
# ============================================
# 依赖目录
# ============================================
node_modules/
jspm_packages/
bower_components/
.mvn/wrapper/maven-wrapper.jar
# ============================================
# 构建输出
# ============================================
# Frontend
dist/
frontend/dist/
# Backend
target/
backend/target/
build/
build/Release
# ============================================
# 日志文件
# ============================================
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
.lerna-debug.log*
# Production builds
dist/
build/
target/
# 项目特定的日志文件
backend/*.log
frontend/*.log
backend_*.log
frontend_*.log
# IDE files
.vscode/
# ============================================
# IDE 和编辑器
# ============================================
# IntelliJ IDEA
.idea/
*.iws
*.iml
*.ipr
# VS Code
.vscode/
*.code-workspace
# Eclipse
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
# NetBeans
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
# Vim/Emacs
*.swp
*.swo
*~
# OS generated files
# ============================================
# 操作系统文件
# ============================================
.DS_Store
.DS_Store?
._*
@@ -26,107 +86,148 @@ target/
ehthumbs.db
Thumbs.db
desktop.ini
Icon?
Network Trash Folder
Temporary Items
.apdisk
# Environment variables
# ============================================
# 环境变量和敏感配置
# ============================================
.env
.env.local
.env.*.local
.env.development
.env.test
.env.production
# Java/Maven
# 应用配置(可能包含敏感信息)
application-local.yml
application-dev-local.yml
application-prod.yml
# ============================================
# Java / Maven / Spring Boot
# ============================================
*.class
*.jar
*.war
*.ear
*.tar.gz
target/
!.mvn/wrapper/maven-wrapper.jar
*.zip
# Maven
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
.mvn/wrapper/maven-wrapper.properties
# Logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.lerna-debug.log*
# Gradle
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
gradle-app.setting
.gradletasknamecache
# Coverage directory used by tools like istanbul
# Spring Boot
spring-boot-devtools.properties
# ============================================
# Node.js / 前端
# ============================================
# Package locks (根据需要选择保留或忽略)
# package-lock.json
# yarn.lock
# pnpm-lock.yaml
# TypeScript
*.tsbuildinfo
typings/
# Vue.js
.vue-temp/
# Vite
.vite/
# Sass/SCSS
.sass-cache
# Coverage
coverage/
lib-cov/
.nyc_output
# Database
# ============================================
# 数据库
# ============================================
*.sqlite
*.sqlite-journal
*.db
*.db-journal
# Runtime data
pids
# ============================================
# 运行时数据
# ============================================
pids/
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# ============================================
# 测试和临时文件
# ============================================
tmp/
temp/
*.tmp
*.temp
.cache/
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#files-in-tmp-and-tmp-grunt-tasks)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# TypeDoc docs
docs/
# Sass
.sass-cache
# Output of 'bundle gem {NAME}'
*.gem
*.rbc
# Configuration files
config/
*.ini
*.toml
# Maven wrapper
!.mvn/wrapper/maven-wrapper.jar
# Backend build artifacts
backend/target/
backend/*.log
# Frontend build artifacts
frontend/dist/
frontend/node_modules/
frontend/*.log
# FRP binaries (large files)
# ============================================
# FRP 内网穿透工具
# ============================================
frp/frpc
frp/frps
frp/*.ini
frp/*.toml
# System files
Thumbs.db
.DS_Store
.DS_Store?
._*
ehthumbs.db
Icon?
Network Trash Folder
Temporary Items
.apdisk
# ============================================
# 文档生成(保留源文件,忽略生成的)
# ============================================
# PlantUML 生成的图片
*.png
*.svg
# 但保留 docs/drawio 和 docs/plantuml 源文件
!docs/**/*.drawio
!docs/**/*.puml
# ============================================
# 其他
# ============================================
# 备份文件
*.bak
*.backup
*.orig
# 压缩文件
*.7z
*.rar
# 证书和密钥
*.pem
*.key
*.crt
*.p12
*.pfx
# 上传文件目录(开发环境)
uploads/
files/

View File

@@ -47,7 +47,7 @@ public class AppointmentController {
return ApiResponse.success(appointmentService.page(new Page<>(page, size), wrapper));
}
@// @PreAuthorize("hasAnyRole('ADMIN','DOCTOR')")
// @PreAuthorize("hasAnyRole('ADMIN','DOCTOR')")
@GetMapping("/admin")
public ApiResponse<?> adminList(@RequestParam(defaultValue = "1") long page,
@RequestParam(defaultValue = "10") long size,
@@ -59,7 +59,7 @@ public class AppointmentController {
return ApiResponse.success(appointmentService.page(new Page<>(page, size), wrapper));
}
@// @PreAuthorize("hasAnyRole('ADMIN','DOCTOR')")
// @PreAuthorize("hasAnyRole('ADMIN','DOCTOR')")
@PutMapping("/{id}/status")
public ApiResponse<?> updateStatus(@PathVariable Long id, @RequestParam String status) {
Appointment update = new Appointment();

View File

@@ -17,7 +17,7 @@ public class DrugController {
this.drugService = drugService;
}
@// @PreAuthorize("hasAnyRole('ADMIN','DOCTOR')")
// @PreAuthorize("hasAnyRole('ADMIN','DOCTOR')")
@GetMapping
public ApiResponse<?> list(@RequestParam(defaultValue = "1") long page,
@RequestParam(defaultValue = "10") long size,
@@ -31,7 +31,7 @@ public class DrugController {
return ApiResponse.success(drugService.page(new Page<>(page, size), wrapper));
}
@// @PreAuthorize("hasRole('ADMIN')")
// @PreAuthorize("hasRole('ADMIN')")
@PostMapping
public ApiResponse<?> create(@RequestBody Drug drug) {
if (drug.getStatus() == null) {
@@ -41,7 +41,7 @@ public class DrugController {
return ApiResponse.success("created", null);
}
@// @PreAuthorize("hasRole('ADMIN')")
// @PreAuthorize("hasRole('ADMIN')")
@PutMapping("/{id}")
public ApiResponse<?> update(@PathVariable Long id, @RequestBody Drug drug) {
drug.setId(id);
@@ -49,7 +49,7 @@ public class DrugController {
return ApiResponse.success("updated", null);
}
@// @PreAuthorize("hasRole('ADMIN')")
// @PreAuthorize("hasRole('ADMIN')")
@DeleteMapping("/{id}")
public ApiResponse<?> delete(@PathVariable Long id) {
drugService.removeById(id);

View File

@@ -16,7 +16,7 @@ public class MedicalRecordController {
this.medicalRecordService = medicalRecordService;
}
@// @PreAuthorize("hasAnyRole('ADMIN','DOCTOR')")
// @PreAuthorize("hasAnyRole('ADMIN','DOCTOR')")
@PostMapping
public ApiResponse<?> create(@RequestBody MedicalRecord record) {
if (record.getStatus() == null) {
@@ -33,7 +33,7 @@ public class MedicalRecordController {
return ApiResponse.success(medicalRecordService.list(wrapper));
}
@// @PreAuthorize("hasAnyRole('ADMIN','DOCTOR')")
// @PreAuthorize("hasAnyRole('ADMIN','DOCTOR')")
@PutMapping("/{id}")
public ApiResponse<?> update(@PathVariable Long id, @RequestBody MedicalRecord record) {
record.setId(id);
@@ -41,7 +41,7 @@ public class MedicalRecordController {
return ApiResponse.success("updated", null);
}
@// @PreAuthorize("hasRole('ADMIN')")
// @PreAuthorize("hasRole('ADMIN')")
@DeleteMapping("/{id}")
public ApiResponse<?> delete(@PathVariable Long id) {
medicalRecordService.removeById(id);

View File

@@ -35,7 +35,7 @@ public class MessageController {
return ApiResponse.success("created", null);
}
@// @PreAuthorize("hasRole('ADMIN')")
// @PreAuthorize("hasRole('ADMIN')")
@GetMapping("/admin")
public ApiResponse<?> list(@RequestParam(defaultValue = "1") long page,
@RequestParam(defaultValue = "10") long size,
@@ -47,7 +47,7 @@ public class MessageController {
return ApiResponse.success(messageService.page(new Page<>(page, size), wrapper));
}
@// @PreAuthorize("hasRole('ADMIN')")
// @PreAuthorize("hasRole('ADMIN')")
@PutMapping("/admin/{id}/reply")
public ApiResponse<?> reply(@PathVariable Long id, @Valid @RequestBody ReplyRequest request) {
AuthUser user = SecurityUtils.currentUser();

View File

@@ -34,14 +34,14 @@ public class NoticeController {
return ApiResponse.success(noticeService.getById(id));
}
@// @PreAuthorize("hasRole('ADMIN')")
// @PreAuthorize("hasRole('ADMIN')")
@GetMapping("/notices")
public ApiResponse<?> list(@RequestParam(defaultValue = "1") long page,
@RequestParam(defaultValue = "10") long size) {
return ApiResponse.success(noticeService.page(new Page<>(page, size)));
}
@// @PreAuthorize("hasRole('ADMIN')")
// @PreAuthorize("hasRole('ADMIN')")
@PostMapping("/notices")
public ApiResponse<?> create(@RequestBody Notice notice) {
if (notice.getPublisherId() == null) {
@@ -60,7 +60,7 @@ public class NoticeController {
return ApiResponse.success("created", null);
}
@// @PreAuthorize("hasRole('ADMIN')")
// @PreAuthorize("hasRole('ADMIN')")
@PutMapping("/notices/{id}")
public ApiResponse<?> update(@PathVariable Long id, @RequestBody Notice notice) {
notice.setId(id);
@@ -68,7 +68,7 @@ public class NoticeController {
return ApiResponse.success("updated", null);
}
@// @PreAuthorize("hasRole('ADMIN')")
// @PreAuthorize("hasRole('ADMIN')")
@DeleteMapping("/notices/{id}")
public ApiResponse<?> delete(@PathVariable Long id) {
noticeService.removeById(id);

View File

@@ -44,7 +44,7 @@ public class OrderController {
/**
* 根据处方生成订单
*/
@// @PreAuthorize("hasAnyRole('ADMIN','DOCTOR')")
// @PreAuthorize("hasAnyRole('ADMIN','DOCTOR')")
@PostMapping("/from-prescription/{prescriptionId}")
public ApiResponse<?> createFromPrescription(@PathVariable Long prescriptionId) {
// 1. 查询处方
@@ -164,7 +164,7 @@ public class OrderController {
return ApiResponse.success(orderService.page(new Page<>(page, size), wrapper));
}
@// @PreAuthorize("hasAnyRole('ADMIN','DOCTOR')")
// @PreAuthorize("hasAnyRole('ADMIN','DOCTOR')")
@PutMapping("/{id}")
public ApiResponse<?> update(@PathVariable Long id, @RequestBody Order order) {
order.setId(id);

View File

@@ -72,7 +72,7 @@ public class PetController {
return ApiResponse.success("deleted", null);
}
@// @PreAuthorize("hasRole('ADMIN')")
// @PreAuthorize("hasRole('ADMIN')")
@GetMapping("/admin/all")
public ApiResponse<?> adminList(@RequestParam(defaultValue = "1") long page,
@RequestParam(defaultValue = "10") long size) {

View File

@@ -19,7 +19,7 @@ public class PrescriptionController {
this.prescriptionService = prescriptionService;
}
@// @PreAuthorize("hasAnyRole('ADMIN','DOCTOR')")
// @PreAuthorize("hasAnyRole('ADMIN','DOCTOR')")
@PostMapping
public ApiResponse<?> create(@RequestBody Prescription prescription) {
if (prescription.getStatus() == null) {
@@ -44,7 +44,7 @@ public class PrescriptionController {
return ApiResponse.success(prescriptionService.page(new Page<>(page, size), wrapper));
}
@// @PreAuthorize("hasAnyRole('ADMIN','DOCTOR')")
// @PreAuthorize("hasAnyRole('ADMIN','DOCTOR')")
@PutMapping("/{id}")
public ApiResponse<?> update(@PathVariable Long id, @RequestBody Prescription prescription) {
prescription.setId(id);

View File

@@ -23,14 +23,14 @@ public class PrescriptionItemController {
return ApiResponse.success(prescriptionItemService.list(wrapper));
}
@// @PreAuthorize("hasAnyRole('ADMIN','DOCTOR')")
// @PreAuthorize("hasAnyRole('ADMIN','DOCTOR')")
@PostMapping
public ApiResponse<?> create(@RequestBody PrescriptionItem item) {
prescriptionItemService.save(item);
return ApiResponse.success("created", null);
}
@// @PreAuthorize("hasAnyRole('ADMIN','DOCTOR')")
// @PreAuthorize("hasAnyRole('ADMIN','DOCTOR')")
@PutMapping("/{id}")
public ApiResponse<?> update(@PathVariable Long id, @RequestBody PrescriptionItem item) {
item.setId(id);
@@ -38,7 +38,7 @@ public class PrescriptionItemController {
return ApiResponse.success("updated", null);
}
@// @PreAuthorize("hasAnyRole('ADMIN','DOCTOR')")
// @PreAuthorize("hasAnyRole('ADMIN','DOCTOR')")
@DeleteMapping("/{id}")
public ApiResponse<?> delete(@PathVariable Long id) {
prescriptionItemService.removeById(id);

View File

@@ -19,7 +19,7 @@ public class ReportController {
this.reportService = reportService;
}
@// @PreAuthorize("hasAnyRole('ADMIN','DOCTOR')")
// @PreAuthorize("hasAnyRole('ADMIN','DOCTOR')")
@PostMapping
public ApiResponse<?> create(@RequestBody Report report) {
AuthUser user = SecurityUtils.currentUser();
@@ -45,7 +45,7 @@ public class ReportController {
return ApiResponse.success(reportService.page(new Page<>(page, size), wrapper));
}
@// @PreAuthorize("hasAnyRole('ADMIN','DOCTOR')")
// @PreAuthorize("hasAnyRole('ADMIN','DOCTOR')")
@PutMapping("/{id}")
public ApiResponse<?> update(@PathVariable Long id, @RequestBody Report report) {
report.setId(id);
@@ -53,7 +53,7 @@ public class ReportController {
return ApiResponse.success("updated", null);
}
@// @PreAuthorize("hasRole('ADMIN')")
// @PreAuthorize("hasRole('ADMIN')")
@DeleteMapping("/{id}")
public ApiResponse<?> delete(@PathVariable Long id) {
reportService.removeById(id);

View File

@@ -6,12 +6,14 @@ import com.gpf.pethospital.common.ApiResponse;
import com.gpf.pethospital.entity.Appointment;
import com.gpf.pethospital.entity.Order;
import com.gpf.pethospital.entity.Pet;
import com.gpf.pethospital.entity.PrescriptionItem;
import com.gpf.pethospital.entity.User;
import com.gpf.pethospital.entity.Visit;
import com.gpf.pethospital.service.AppointmentService;
import com.gpf.pethospital.service.DrugService;
import com.gpf.pethospital.service.OrderService;
import com.gpf.pethospital.service.PetService;
import com.gpf.pethospital.service.PrescriptionItemService;
import com.gpf.pethospital.service.UserService;
import com.gpf.pethospital.service.VisitService;
// import org.springframework.security.access.prepost.PreAuthorize;
@@ -26,8 +28,11 @@ import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.Map;
import java.util.stream.Collectors;
@@ -40,22 +45,25 @@ public class StatsController {
private final PetService petService;
private final UserService userService;
private final DrugService drugService;
private final PrescriptionItemService prescriptionItemService;
public StatsController(OrderService orderService,
AppointmentService appointmentService,
VisitService visitService,
PetService petService,
UserService userService,
DrugService drugService) {
DrugService drugService,
PrescriptionItemService prescriptionItemService) {
this.orderService = orderService;
this.appointmentService = appointmentService;
this.visitService = visitService;
this.petService = petService;
this.userService = userService;
this.drugService = drugService;
this.prescriptionItemService = prescriptionItemService;
}
@// @PreAuthorize("hasRole('ADMIN')")
// @PreAuthorize("hasRole('ADMIN')")
@GetMapping
public ApiResponse<?> summary() {
Map<String, Object> data = new HashMap<>();
@@ -124,7 +132,7 @@ public class StatsController {
return ApiResponse.success(data);
}
@// @PreAuthorize("hasRole('ADMIN')")
// @PreAuthorize("hasRole('ADMIN')")
@GetMapping("/trends")
public ApiResponse<?> trends(@RequestParam(defaultValue = "week") String period) {
Map<String, Object> data = new HashMap<>();
@@ -208,8 +216,253 @@ public class StatsController {
return ApiResponse.success(data);
}
@GetMapping("/report/revenue")
public ApiResponse<?> revenueReport(@RequestParam(defaultValue = "month") String period,
@RequestParam(required = false) String startDate,
@RequestParam(required = false) String endDate) {
DateRange range = resolveDateRange(period, startDate, endDate);
List<Order> paidOrders = orderService.list(
new LambdaQueryWrapper<Order>()
.eq(Order::getStatus, "PAID")
.ge(Order::getCreateTime, range.start())
.lt(Order::getCreateTime, range.end())
);
Map<LocalDate, BigDecimal> amountByDay = new HashMap<>();
BigDecimal totalAmount = BigDecimal.ZERO;
for (Order order : paidOrders) {
BigDecimal amount = order.getAmount() == null ? BigDecimal.ZERO : order.getAmount();
totalAmount = totalAmount.add(amount);
if (order.getCreateTime() != null) {
amountByDay.merge(order.getCreateTime().toLocalDate(), amount, BigDecimal::add);
}
}
List<Map<String, Object>> trend = amountByDay.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.map(entry -> {
Map<String, Object> point = new HashMap<>();
point.put("date", entry.getKey().toString());
point.put("amount", entry.getValue());
return point;
})
.collect(Collectors.toList());
DateRange previous = range.shiftBack();
BigDecimal previousTotalAmount = sumPaidOrderAmount(previous.start(), previous.end());
double growthRate = BigDecimal.ZERO.compareTo(previousTotalAmount) == 0
? 0D
: totalAmount.subtract(previousTotalAmount)
.multiply(BigDecimal.valueOf(100))
.divide(previousTotalAmount, 2, java.math.RoundingMode.HALF_UP)
.doubleValue();
Map<String, Object> data = new HashMap<>();
data.put("period", period);
data.put("startDate", range.start().toLocalDate().toString());
data.put("endDate", range.end().minusDays(1).toLocalDate().toString());
data.put("totalAmount", totalAmount);
data.put("orderCount", paidOrders.size());
data.put("averageAmount", paidOrders.isEmpty()
? BigDecimal.ZERO
: totalAmount.divide(BigDecimal.valueOf(paidOrders.size()), 2, java.math.RoundingMode.HALF_UP));
data.put("growthRate", growthRate);
data.put("trend", trend);
return ApiResponse.success(data);
}
@GetMapping("/report/revenue-sources")
public ApiResponse<?> revenueSources(@RequestParam(defaultValue = "month") String period,
@RequestParam(required = false) String startDate,
@RequestParam(required = false) String endDate) {
DateRange range = resolveDateRange(period, startDate, endDate);
QueryWrapper<Order> wrapper = new QueryWrapper<>();
wrapper.select("payment_method AS paymentMethod", "COUNT(*) AS orderCount", "COALESCE(SUM(amount), 0) AS totalAmount");
wrapper.eq("status", "PAID");
wrapper.ge("create_time", range.start());
wrapper.lt("create_time", range.end());
wrapper.groupBy("payment_method");
wrapper.orderByDesc("totalAmount");
List<Map<String, Object>> rows = orderService.listMaps(wrapper);
BigDecimal totalAmount = rows.stream()
.map(row -> toBigDecimal(getMapValue(row, "totalAmount", "totalamount", "TOTALAMOUNT")))
.reduce(BigDecimal.ZERO, BigDecimal::add);
List<Map<String, Object>> sources = new ArrayList<>();
for (Map<String, Object> row : rows) {
BigDecimal amount = toBigDecimal(getMapValue(row, "totalAmount", "totalamount", "TOTALAMOUNT"));
Map<String, Object> item = new HashMap<>();
item.put("paymentMethod", normalizePaymentMethod(String.valueOf(getMapValue(row, "paymentMethod", "payment_method", "PAYMENTMETHOD"))));
item.put("orderCount", toLong(getMapValue(row, "orderCount", "ordercount", "ORDERCOUNT")));
item.put("totalAmount", amount);
item.put("ratio", BigDecimal.ZERO.compareTo(totalAmount) == 0
? BigDecimal.ZERO
: amount.multiply(BigDecimal.valueOf(100)).divide(totalAmount, 2, java.math.RoundingMode.HALF_UP));
sources.add(item);
}
Map<String, Object> data = new HashMap<>();
data.put("period", period);
data.put("startDate", range.start().toLocalDate().toString());
data.put("endDate", range.end().minusDays(1).toLocalDate().toString());
data.put("totalAmount", totalAmount);
data.put("sources", sources);
return ApiResponse.success(data);
}
@GetMapping("/report/drug-sales")
public ApiResponse<?> drugSales(@RequestParam(defaultValue = "month") String period,
@RequestParam(required = false) String startDate,
@RequestParam(required = false) String endDate,
@RequestParam(defaultValue = "10") int limit) {
DateRange range = resolveDateRange(period, startDate, endDate);
QueryWrapper<PrescriptionItem> wrapper = new QueryWrapper<>();
wrapper.select("drug_id AS drugId", "drug_name AS drugName", "COALESCE(SUM(quantity), 0) AS totalQuantity", "COALESCE(SUM(subtotal), 0) AS totalAmount");
wrapper.ge("create_time", range.start());
wrapper.lt("create_time", range.end());
wrapper.groupBy("drug_id", "drug_name");
wrapper.orderByDesc("totalQuantity");
wrapper.orderByDesc("totalAmount");
wrapper.last("LIMIT " + safeLimit(limit, 100));
List<Map<String, Object>> rows = prescriptionItemService.listMaps(wrapper);
List<Map<String, Object>> ranking = new ArrayList<>();
int rank = 1;
for (Map<String, Object> row : rows) {
Map<String, Object> item = new HashMap<>();
item.put("rank", rank++);
item.put("drugId", toLong(getMapValue(row, "drugId", "drug_id", "DRUGID")));
item.put("drugName", normalizeDrugName(getMapValue(row, "drugName", "drug_name", "DRUGNAME")));
item.put("totalQuantity", toLong(getMapValue(row, "totalQuantity", "totalquantity", "TOTALQUANTITY")));
item.put("totalAmount", toBigDecimal(getMapValue(row, "totalAmount", "totalamount", "TOTALAMOUNT")));
ranking.add(item);
}
Map<String, Object> data = new HashMap<>();
data.put("period", period);
data.put("startDate", range.start().toLocalDate().toString());
data.put("endDate", range.end().minusDays(1).toLocalDate().toString());
data.put("ranking", ranking);
return ApiResponse.success(data);
}
@GetMapping("/report/doctor-performance")
public ApiResponse<?> doctorPerformance(@RequestParam(defaultValue = "month") String period,
@RequestParam(required = false) String startDate,
@RequestParam(required = false) String endDate,
@RequestParam(defaultValue = "10") int limit) {
DateRange range = resolveDateRange(period, startDate, endDate);
QueryWrapper<Visit> wrapper = new QueryWrapper<>();
wrapper.select("doctor_id AS doctorId", "COUNT(*) AS visitCount", "COALESCE(SUM(total_amount), 0) AS totalAmount", "COALESCE(AVG(total_amount), 0) AS averageAmount");
wrapper.ge("create_time", range.start());
wrapper.lt("create_time", range.end());
wrapper.isNotNull("doctor_id");
wrapper.groupBy("doctor_id");
wrapper.orderByDesc("totalAmount");
wrapper.last("LIMIT " + safeLimit(limit, 100));
List<Map<String, Object>> rows = visitService.listMaps(wrapper);
Set<Long> doctorIds = rows.stream()
.map(row -> toLong(getMapValue(row, "doctorId", "doctor_id", "DOCTORID")))
.filter(id -> id > 0)
.collect(Collectors.toSet());
Map<Long, String> doctorNameMap = userService.listByIds(doctorIds).stream()
.collect(Collectors.toMap(User::getId, User::getUsername, (left, right) -> left));
List<Map<String, Object>> ranking = new ArrayList<>();
int rank = 1;
for (Map<String, Object> row : rows) {
Long doctorId = toLong(getMapValue(row, "doctorId", "doctor_id", "DOCTORID"));
Map<String, Object> item = new HashMap<>();
item.put("rank", rank++);
item.put("doctorId", doctorId);
item.put("doctorName", doctorNameMap.getOrDefault(doctorId, "医生#" + doctorId));
item.put("visitCount", toLong(getMapValue(row, "visitCount", "visitcount", "VISITCOUNT")));
item.put("totalAmount", toBigDecimal(getMapValue(row, "totalAmount", "totalamount", "TOTALAMOUNT")));
item.put("averageAmount", toBigDecimal(getMapValue(row, "averageAmount", "averageamount", "AVERAGEAMOUNT")));
ranking.add(item);
}
Map<String, Object> data = new HashMap<>();
data.put("period", period);
data.put("startDate", range.start().toLocalDate().toString());
data.put("endDate", range.end().minusDays(1).toLocalDate().toString());
data.put("ranking", ranking);
return ApiResponse.success(data);
}
@GetMapping("/report/department-performance")
public ApiResponse<?> departmentPerformance(@RequestParam(defaultValue = "month") String period,
@RequestParam(required = false) String startDate,
@RequestParam(required = false) String endDate) {
DateRange range = resolveDateRange(period, startDate, endDate);
QueryWrapper<Appointment> appointmentWrapper = new QueryWrapper<>();
appointmentWrapper.select("department", "COUNT(*) AS appointmentCount");
appointmentWrapper.ge("create_time", range.start());
appointmentWrapper.lt("create_time", range.end());
appointmentWrapper.groupBy("department");
List<Map<String, Object>> appointmentRows = appointmentService.listMaps(appointmentWrapper);
Map<String, Long> appointmentCountMap = new HashMap<>();
for (Map<String, Object> row : appointmentRows) {
String department = normalizeDepartment(getMapValue(row, "department", "DEPARTMENT"));
appointmentCountMap.put(department, toLong(getMapValue(row, "appointmentCount", "appointmentcount", "APPOINTMENTCOUNT")));
}
List<Visit> visits = visitService.list(
new LambdaQueryWrapper<Visit>()
.ge(Visit::getCreateTime, range.start())
.lt(Visit::getCreateTime, range.end())
.isNotNull(Visit::getAppointmentId)
);
Set<Long> appointmentIds = visits.stream()
.map(Visit::getAppointmentId)
.filter(id -> id != null && id > 0)
.collect(Collectors.toSet());
Map<Long, Appointment> appointmentMap = appointmentIds.isEmpty()
? new HashMap<>()
: appointmentService.listByIds(appointmentIds).stream()
.collect(Collectors.toMap(Appointment::getId, appointment -> appointment, (left, right) -> left));
Map<String, Long> visitCountMap = new HashMap<>();
Map<String, BigDecimal> revenueMap = new HashMap<>();
for (Visit visit : visits) {
Appointment appointment = appointmentMap.get(visit.getAppointmentId());
String department = normalizeDepartment(appointment == null ? null : appointment.getDepartment());
visitCountMap.merge(department, 1L, Long::sum);
revenueMap.merge(department, visit.getTotalAmount() == null ? BigDecimal.ZERO : visit.getTotalAmount(), BigDecimal::add);
}
Set<String> departments = new HashSet<>();
departments.addAll(appointmentCountMap.keySet());
departments.addAll(visitCountMap.keySet());
List<Map<String, Object>> departmentsData = departments.stream()
.map(department -> {
Map<String, Object> row = new HashMap<>();
row.put("department", department);
row.put("appointmentCount", appointmentCountMap.getOrDefault(department, 0L));
row.put("visitCount", visitCountMap.getOrDefault(department, 0L));
row.put("totalAmount", revenueMap.getOrDefault(department, BigDecimal.ZERO));
return row;
})
.sorted((left, right) -> toBigDecimal(right.get("totalAmount")).compareTo(toBigDecimal(left.get("totalAmount"))))
.collect(Collectors.toList());
Map<String, Object> data = new HashMap<>();
data.put("period", period);
data.put("startDate", range.start().toLocalDate().toString());
data.put("endDate", range.end().minusDays(1).toLocalDate().toString());
data.put("departments", departmentsData);
return ApiResponse.success(data);
}
@// @PreAuthorize("hasRole('ADMIN')")
// @PreAuthorize("hasRole('ADMIN')")
@GetMapping("/today-todos")
public ApiResponse<?> todayTodos() {
LocalDate today = LocalDate.now();
@@ -243,4 +496,139 @@ public class StatsController {
return ApiResponse.success(todoList);
}
private DateRange resolveDateRange(String period, String startDate, String endDate) {
if (hasText(startDate) && hasText(endDate)) {
LocalDate start = LocalDate.parse(startDate);
LocalDate end = LocalDate.parse(endDate);
if (end.isBefore(start)) {
LocalDate temp = start;
start = end;
end = temp;
}
return new DateRange(start.atStartOfDay(), end.plusDays(1).atStartOfDay());
}
LocalDate today = LocalDate.now();
LocalDate start;
LocalDate end = today.plusDays(1);
switch (period) {
case "day":
start = today;
break;
case "week":
start = today.minusDays(6);
break;
case "year":
start = today.minusMonths(11).withDayOfMonth(1);
end = today.plusMonths(1).withDayOfMonth(1);
break;
case "month":
default:
start = today.minusDays(29);
break;
}
return new DateRange(start.atStartOfDay(), end.atStartOfDay());
}
private BigDecimal sumPaidOrderAmount(LocalDateTime start, LocalDateTime end) {
QueryWrapper<Order> wrapper = new QueryWrapper<>();
wrapper.select("COALESCE(SUM(amount), 0) AS totalAmount");
wrapper.eq("status", "PAID");
wrapper.ge("create_time", start);
wrapper.lt("create_time", end);
List<Map<String, Object>> rows = orderService.listMaps(wrapper);
if (rows.isEmpty()) {
return BigDecimal.ZERO;
}
return toBigDecimal(getMapValue(rows.get(0), "totalAmount", "totalamount", "TOTALAMOUNT"));
}
private boolean hasText(String value) {
return value != null && !value.isBlank();
}
private int safeLimit(int value, int maxLimit) {
if (value <= 0) {
return 10;
}
return Math.min(value, maxLimit);
}
private BigDecimal toBigDecimal(Object value) {
if (value == null) {
return BigDecimal.ZERO;
}
if (value instanceof BigDecimal) {
return (BigDecimal) value;
}
try {
return new BigDecimal(value.toString());
} catch (NumberFormatException ex) {
return BigDecimal.ZERO;
}
}
private long toLong(Object value) {
if (value == null) {
return 0L;
}
if (value instanceof Number) {
return ((Number) value).longValue();
}
try {
return Long.parseLong(value.toString());
} catch (NumberFormatException ex) {
return 0L;
}
}
private Object getMapValue(Map<String, Object> row, String... keys) {
for (String key : keys) {
if (row.containsKey(key)) {
return row.get(key);
}
}
for (String key : keys) {
for (Map.Entry<String, Object> entry : row.entrySet()) {
if (entry.getKey().equalsIgnoreCase(key)) {
return entry.getValue();
}
}
}
return null;
}
private String normalizeDepartment(Object value) {
String department = value == null ? "" : value.toString().trim();
return department.isEmpty() ? "未分配科室" : department;
}
private String normalizePaymentMethod(String value) {
if (value == null || value.isBlank()) {
return "未知支付方式";
}
switch (value) {
case "ALIPAY":
return "支付宝";
case "WECHAT":
return "微信支付";
case "OFFLINE":
return "线下支付";
default:
return value;
}
}
private String normalizeDrugName(Object value) {
String name = value == null ? "" : value.toString().trim();
return name.isEmpty() ? "未知药品" : name;
}
private record DateRange(LocalDateTime start, LocalDateTime end) {
private DateRange shiftBack() {
long days = Math.max(1, ChronoUnit.DAYS.between(start.toLocalDate(), end.toLocalDate()));
return new DateRange(start.minusDays(days), start);
}
}
}

View File

@@ -22,7 +22,7 @@ public class StockInController {
this.drugService = drugService;
}
@// @PreAuthorize("hasRole('ADMIN')")
// @PreAuthorize("hasRole('ADMIN')")
@GetMapping
public ApiResponse<?> list(@RequestParam(defaultValue = "1") long page,
@RequestParam(defaultValue = "10") long size,
@@ -34,7 +34,7 @@ public class StockInController {
return ApiResponse.success(stockInService.page(new Page<>(page, size), wrapper));
}
@// @PreAuthorize("hasRole('ADMIN')")
// @PreAuthorize("hasRole('ADMIN')")
@PostMapping
@Transactional
public ApiResponse<?> create(@RequestBody StockIn stockIn) {

View File

@@ -22,7 +22,7 @@ public class StockOutController {
this.drugService = drugService;
}
@// @PreAuthorize("hasRole('ADMIN')")
// @PreAuthorize("hasRole('ADMIN')")
@GetMapping
public ApiResponse<?> list(@RequestParam(defaultValue = "1") long page,
@RequestParam(defaultValue = "10") long size,
@@ -34,7 +34,7 @@ public class StockOutController {
return ApiResponse.success(stockOutService.page(new Page<>(page, size), wrapper));
}
@// @PreAuthorize("hasRole('ADMIN')")
// @PreAuthorize("hasRole('ADMIN')")
@PostMapping
@Transactional
public ApiResponse<?> create(@RequestBody StockOut stockOut) {

View File

@@ -67,7 +67,7 @@ public class UserController {
return ApiResponse.success(result);
}
@// @PreAuthorize("hasRole('ADMIN')")
// @PreAuthorize("hasRole('ADMIN')")
@PostMapping
public ApiResponse<?> create(@RequestBody User user) {
if (user.getPassword() == null || user.getPassword().isBlank()) {
@@ -81,7 +81,7 @@ public class UserController {
return ApiResponse.success("created", null);
}
@// @PreAuthorize("hasRole('ADMIN')")
// @PreAuthorize("hasRole('ADMIN')")
@PutMapping("/{id}/status")
public ApiResponse<?> updateStatus(@PathVariable Long id, @RequestParam Integer status) {
User update = new User();
@@ -91,7 +91,7 @@ public class UserController {
return ApiResponse.success("updated", null);
}
@// @PreAuthorize("hasRole('ADMIN')")
// @PreAuthorize("hasRole('ADMIN')")
@PutMapping("/{id}/reset-password")
public ApiResponse<?> resetPassword(@PathVariable Long id, @RequestParam String newPassword) {
User update = new User();
@@ -101,7 +101,7 @@ public class UserController {
return ApiResponse.success("updated", null);
}
@// @PreAuthorize("hasRole('ADMIN')")
// @PreAuthorize("hasRole('ADMIN')")
@GetMapping("/stats")
public ApiResponse<?> stats() {
Map<String, Object> data = new HashMap<>();

View File

@@ -19,7 +19,7 @@ public class VisitController {
this.visitService = visitService;
}
@// @PreAuthorize("hasAnyRole('ADMIN','DOCTOR')")
// @PreAuthorize("hasAnyRole('ADMIN','DOCTOR')")
@PostMapping
public ApiResponse<?> create(@RequestBody Visit visit) {
if (visit.getStatus() == null) {
@@ -47,7 +47,7 @@ public class VisitController {
return ApiResponse.success(visitService.page(new Page<>(page, size), wrapper));
}
@// @PreAuthorize("hasAnyRole('ADMIN','DOCTOR')")
// @PreAuthorize("hasAnyRole('ADMIN','DOCTOR')")
@PutMapping("/{id}")
public ApiResponse<?> update(@PathVariable Long id, @RequestBody Visit visit) {
visit.setId(id);

View File

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.experimental.Accessors;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
@@ -52,6 +53,16 @@ public class Report {
*/
private Long doctorId;
private String reportType;
private String reportData;
private LocalDate periodStart;
private LocalDate periodEnd;
private Long generatedBy;
/**
* 创建时间
*/

View File

@@ -1,59 +0,0 @@
server:
port: 8081
address: 0.0.0.0
servlet:
context-path: /api
spring:
application:
name: pet-hospital
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/pet_hospital_dev?useUnicode=true&characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=GMT%2B8
username: root
password: qq5211314
hikari:
maximum-pool-size: 20
minimum-idle: 10
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
jackson:
time-zone: GMT+8
date-format: yyyy-MM-dd HH:mm:ss
jpa:
hibernate:
ddl-auto: update
show-sql: true
sql:
init:
mode: never
schema-locations: classpath*:schema.sql
data-locations: classpath*:data.sql
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
id-type: auto
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
mapper-locations: classpath*:mapper/**/*.xml
ddl-auto: create-drop
# JWT配置
jwt:
secret: petHospitalSecretKey2024GuanPengFeiGraduateDesign
expiration: 86400000 # 24小时
# 文件上传配置
file:
upload-path: /tmp/pet-hospital/uploads/
max-size: 10MB

View File

@@ -1,39 +0,0 @@
server:
port: 8080
servlet:
context-path: /api
spring:
profiles:
active: dev
application:
name: pet-hospital
jackson:
time-zone: GMT+8
date-format: yyyy-MM-dd HH:mm:ss
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
id-type: auto
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
mapper-locations: classpath*:mapper/**/*.xml
# 数据库配置
database:
type: mysql
# JWT配置
jwt:
secret: pet-hospital-secret-key-2024-guanpengfei-graduate-design
expiration: 86400000 # 24小时
# 文件上传配置
file:
upload-path: /tmp/pet-hospital/uploads/
max-size: 10MB

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