From 814f7f71fe3499c32493f17fc96f4dacfd244ef3 Mon Sep 17 00:00:00 2001 From: dzc <2388224167@qq.com> Date: Mon, 31 Aug 2026 15:04:07 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E8=AE=A2=E5=8D=95=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E5=8F=B3=E4=BE=A7drawer=E7=BB=9F=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 106 +++++++++++++ .../dto/hard/HardTerminalFaceDeleteDto.java | 21 +++ .../vo/order/HardOrderDailyStatisticsVo.java | 19 +++ .../vo/order/HardOrderMealStatisticsVo.java | 39 +++++ .../order/HardOrderStatisticsSummaryVo.java | 42 +++++ .../api/vo/order/HardOrderStatisticsVo.java | 21 +++ .../controller/hard/HardApiController.java | 17 +++ .../controller/order/HardOrderController.java | 26 +++- .../hard/mapper/order/HardOrderMapper.java | 10 ++ .../model/HardOrderStatisticsAggregate.java | 51 +++++++ .../service/HardOrderStatisticsService.java | 20 +++ .../hard/service/HardTerminalFaceService.java | 19 +++ .../manager/HardTerminalFaceServiceImpl.java | 75 +++++++++ .../order/HardOrderStatisticsServiceImpl.java | 126 +++++++++++++++ .../mapper/order/HardOrderMapper.xml | 80 ++++++++++ .../hard/HardApiControllerTest.java | 7 +- .../HardTerminalFaceServiceImplTest.java | 131 ++++++++++++++++ .../HardOrderStatisticsServiceImplTest.java | 111 ++++++++++++++ .../api/dto/SysUserFaceTerminalDeleteDto.java | 27 ++++ .../api/feign/RemoteUserFaceService.java | 12 ++ .../api/vo/SysUserFaceTerminalDeleteVo.java | 28 ++++ .../controller/SysUserFaceController.java | 15 ++ .../admin/mapper/SysUserFaceMapper.java | 15 ++ .../admin/service/SysUserFaceService.java | 10 ++ .../service/impl/SysUserFaceServiceImpl.java | 77 ++++++++++ ...UserFaceTerminalDeleteServiceImplTest.java | 144 ++++++++++++++++++ 26 files changed, 1247 insertions(+), 2 deletions(-) create mode 100644 AGENTS.md create mode 100644 pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/hard/HardTerminalFaceDeleteDto.java create mode 100644 pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardOrderDailyStatisticsVo.java create mode 100644 pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardOrderMealStatisticsVo.java create mode 100644 pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardOrderStatisticsSummaryVo.java create mode 100644 pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardOrderStatisticsVo.java create mode 100644 pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/mapper/order/model/HardOrderStatisticsAggregate.java create mode 100644 pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardOrderStatisticsService.java create mode 100644 pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardTerminalFaceService.java create mode 100644 pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/manager/HardTerminalFaceServiceImpl.java create mode 100644 pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardOrderStatisticsServiceImpl.java create mode 100644 pjcloud-hardware/pjcloud-hardware-biz/src/test/java/com/bsd/pjcloud/hard/service/impl/manager/HardTerminalFaceServiceImplTest.java create mode 100644 pjcloud-hardware/pjcloud-hardware-biz/src/test/java/com/bsd/pjcloud/hard/service/impl/order/HardOrderStatisticsServiceImplTest.java create mode 100644 pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/dto/SysUserFaceTerminalDeleteDto.java create mode 100644 pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/vo/SysUserFaceTerminalDeleteVo.java create mode 100644 pjcloud-upms/pjcloud-upms-biz/src/test/java/com/bsd/pjcloud/admin/service/impl/SysUserFaceTerminalDeleteServiceImplTest.java diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..aca325182 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,106 @@ +# `biz-api-cloud-zax` 开发规则 + +> 适用于 Java 后端多模块项目。执行任务前必须先完整阅读 [HQ Platform 通用规则](../agents/common.md),并继续检查目标模块内更具体的说明和现有同类实现。 + +## 技术栈基线 + +以下为当前父 POM 和 BOM 的扫描结果。实际版本以根 `pom.xml` 与 `pjcloud-common/pjcloud-common-bom/pom.xml` 为准,禁止在子模块中随意覆盖统一版本。 + +- 工程版本:`com.bsd:pjcloud:5.7.0`,Maven 多模块工程。 +- 运行环境:Java 17、Spring Boot `3.3.6`、Spring Cloud `2023.0.4`、Spring Cloud Alibaba `2023.0.3.2`。 +- 服务治理:Nacos 注册发现与配置中心、Spring Cloud Gateway、OpenFeign、Feign OkHttp、Spring Cloud LoadBalancer、Sentinel、Caffeine。 +- 安全认证:Spring Security、OAuth2/Jose、Spring Authorization Server `1.4.0`,项目内统一安全组件与方法级 `@PreAuthorize` 权限校验。 +- 数据访问:MyBatis-Plus `3.5.9`、MyBatis-Plus Join `1.5.2`、动态数据源 `4.3.1`、Druid `1.2.23`、MySQL Connector/J `8.3.0`。 +- 缓存:Spring Data Redis;网关还使用 Reactive Redis。缓存访问应通过项目公共组件和既有模板,不自行创建不一致的序列化方案。 +- 分布式事务:Seata `1.7.0`,由 `pjcloud-common-seata` 统一封装。 +- 接口与文档:Spring MVC / WebFlux、Springdoc OpenAPI `2.1.0`、Knife4j;统一响应通常使用项目 `R`。 +- 服务容器与构建:业务 Web 服务普遍使用 Undertow,Maven Spring Boot 插件负责打包,Fabric8 Docker Maven Plugin 提供镜像构建能力。 +- 流程与任务:Flowable `7.0.0`、XXL-Job、Quartz、ElasticJob 相关模块。 +- 消息与实时能力:Spring Integration MQTT、WebSocket、SSE。 +- 公共业务能力:OSS、Excel、日志、审计、幂等、接口加解密、数据脱敏、XSS 过滤、分布式序列、灰度路由、数据权限等均已有公共模块。 +- 常用基础库:Lombok、Hutool、Jasypt 等。是否可用仍需看目标模块 POM,不能因为 BOM 管理了版本就直接在任意模块引用。 + +## 顶层服务与模块职责 + +- `pjcloud-register`:内置 Nacos 注册/配置中心相关模块。 +- `pjcloud-gateway`:Spring Cloud Gateway 网关,负责动态路由、Reactive Redis、Sentinel 网关限流和聚合接口文档等。 +- `pjcloud-auth`:OAuth2 认证授权中心,依赖统一安全、Feign、Redis、验证码和灰度路由能力。 +- `pjcloud-upms`:用户、部门、角色、菜单、租户、字典等基础权限管理,拆分为 `api` 与 `biz`。 +- `pjcloud-app-server`:移动端应用配置、页面、TabBar 及 App 侧公共服务,拆分为 `api` 与 `biz`。 +- `pjcloud-canteen`:食堂、菜品、库存、预订、配送等业务,拆分为 `api` 与 `biz`。 +- `pjcloud-hardware`:门禁、设备、硬件商品/订单等能力,拆分为 `api` 与 `biz`。 +- `pjcloud-integrated`:访客、会议、反馈、公告、车辆、广告等综合业务,拆分为 `api` 与 `biz`。 +- `pjcloud-wallet`:钱包、充值、交易、商户结算等业务,拆分为 `api` 与 `biz`。 +- `pjcloud-supermarket`:商城、商品、SKU、库存和 ERP 等业务,拆分为 `api` 与 `biz`。 +- `pjcloud-thirdparty`:第三方认证、支付、门禁和外部系统集成,拆分为 `api` 与 `biz`。 +- `pjcloud-mqtt`:MQTT 接入与消息能力,拆分为 `api` 与 `biz`。 +- `pjcloud-pass`:通行相关业务,拆分为 `api` 与 `biz`。 +- `pjcloud-flow`:流程引擎与流程任务,内部继续拆分 engine/task 的 `api` 和 `biz`。 +- `pjcloud-visual`:代码生成、报表、监控、微信/支付平台、Quartz、ElasticJob、XXL-Job 管理端等辅助服务。 +- `pjcloud-boot`:聚合多个业务模块的单体启动方式;修改公共业务时需同时考虑微服务模式和 Boot 聚合模式。 + +## `api` / `biz` 分层规则 + +- 典型业务模块使用父聚合模块 + `*-api` + `*-biz` 结构。 +- `*-api` 放跨模块契约:Feign Client、DTO、VO、Entity、常量及必要接口。不得把 Controller、具体 Service 实现或仅本服务内部使用的实现细节放入 API 模块。 +- `*-biz` 放运行实现:启动类、Controller、Service/Impl、Mapper、配置、任务和资源文件。 +- 跨服务只能依赖对方 `*-api` 和 Feign 契约,不得直接依赖对方 `*-biz`、Mapper 或 ServiceImpl。 +- 新增类型前先搜索现有 DTO、VO、Entity 和公共 `R`;请求对象、响应对象、持久化对象职责要分开,不用 Map 或 Entity 随意穿透所有层。 +- Controller 负责参数接收、权限和响应包装,复杂业务放 Service;Mapper 负责数据访问,不在 Controller 堆叠查询与事务逻辑。 + +## 数据、缓存、事务与远程调用 + +- 持久化沿用 MyBatis-Plus、项目 `PjcloudBaseMapper`、现有分页 `Page`、Wrapper 与 XML/Mapper 风格。使用 MP Join 或自定义 SQL 前先看同模块已有写法。 +- 多租户、数据权限和审计逻辑可能由公共 Mapper、拦截器或上下文提供。禁止用手写 SQL 绕过租户/数据范围约束。 +- **不得修改数据库字段和表结构。** 需求缺少字段时先使用现有模型评估替代方案;确实必须调整时停止实现并请求用户明确授权。 +- 事务方法应放在 Spring 管理的 Service 层,明确 `rollbackFor`;跨库/跨服务一致性需要评估本地事务、Seata、补偿和幂等,不能把一次远程调用误认为本地事务的一部分。 +- Redis 必须复用已有 Key、序列化、Template/工具类和过期策略。修改数据库数据时同步评估缓存更新或失效;不要新增永久缓存或无命名空间 Key。 +- Redis 基础设施会根据当前租户上下文自动处理租户 Key 前缀;使用 `RedisTemplate`、`StringRedisTemplate`、Redisson 锁及其他 Redis 对象时,业务代码只声明稳定的业务 Key,禁止读取或拼接 `TenantContextHolder.getTenantId()` 等租户编号。跨租户任务应通过 `TenantBroker` 等既有能力切换租户上下文;确需全局共享的 Key 必须沿用项目已有全局 Key 契约,不得用手工拼接方式绕过隔离。 +- 远程调用通过已有 Feign Client,复用统一请求头、内部调用标识、LoadBalancer、Sentinel 降级和异常约定。禁止在业务代码中手写服务地址进行 HTTP 调用。 +- Feign 返回 `R` 时必须检查业务状态与空数据;不得只判断 HTTP 成功。 +- 接口加解密已有 `pjcloud-common-encrypt-api`,前端也已有对应拦截器。不得在单个接口另造不兼容的加密协议。 + +## 配置、安全与接口约定 + +- 各服务 `application.yml` 通常只保存启动端口、服务名和 Nacos 引导配置,并导入 `application-{profile}.yml` 与服务级配置。修改配置前同时检查本地文件、环境变量占位符和 Nacos 配置来源。 +- 不得把账号、密码、密钥、Token、证书或生产地址硬编码进仓库。发现现有敏感配置也不得在无关任务中扩散或复制。 +- 方法权限遵循现有 `@PreAuthorize("@pms.hasPermission('...')")` 风格,权限字符串需与管理端菜单/按钮权限对应。 +- 认证、Token、租户头、内部调用头和数据权限由公共安全组件处理;新增公开接口、匿名接口或内部接口必须先核对现有注解和安全白名单机制。 +- 接口路径、HTTP 方法、分页参数、统一返回码、异常处理和 OpenAPI 注解跟随同 Controller 的现有风格。 +- 删除或改变已有字段、枚举值、接口路径和返回结构前必须评估 PC、App 与其他服务调用方;默认保持向后兼容。 + +## 公共模块复用指引 + +- 核心工具与返回结构:`pjcloud-common-core`。 +- 数据与 MyBatis 扩展:`pjcloud-common-data`、`pjcloud-common-datasource`。 +- 认证授权:`pjcloud-common-security`。 +- 远程调用:`pjcloud-common-feign`。 +- 限流降级与分布式事务:`pjcloud-common-sentinel`、`pjcloud-common-seata`。 +- 文档与网关:`pjcloud-common-swagger`、`pjcloud-common-gateway`。 +- 日志、审计、幂等:`pjcloud-common-log`、`pjcloud-common-audit`、`pjcloud-common-idempotent`。 +- 文件、Excel、任务:`pjcloud-common-oss`、`pjcloud-common-excel`、`pjcloud-common-job`。 +- 实时通信:`pjcloud-common-websocket`、`pjcloud-common-sse`。 +- 安全辅助:`pjcloud-common-encrypt-api`、`pjcloud-common-sensitive`、`pjcloud-common-xss`。 +- 其他基础设施:`pjcloud-common-gray`、`pjcloud-common-sequence`。新增通用能力前先确认这里是否已有实现。 + +## 常用命令与验证 + +- 在后端根目录按模块验证:`mvn -pl <模块路径> -am test`。 +- 仅需编译/打包时:`mvn -pl <模块路径> -am -DskipTests package`,但不能用跳过测试的结果冒充测试通过。 +- 修改 `*-api`、公共模块或 BOM 时,应验证所有直接依赖模块,必要时执行更大范围构建。 +- 修改网关、安全、数据、Feign、缓存、加解密或 Boot 聚合模块时,属于高影响改动,必须扩大测试范围并检查微服务与单体两种启动关系。 +- 当前自动化测试分布不均。集中验证时,无现成测试的模块也应完成针对性编译,并对关键业务补充与现有测试风格一致的测试。 + +## 开发规则 + +- 修改前先确认所属业务模块及其 `api` / `biz` 边界,沿用现有 Controller、Service、Mapper、DTO、VO、Feign Client、异常处理、鉴权和统一响应结构。 +- 跨服务调用优先遵循项目现有 OpenFeign 封装、服务名、请求模型、降级与异常处理方式,不得绕过已有边界直接耦合其他服务内部实现。 +- Redis 使用必须复用已有缓存工具、序列化方式、键命名规则和过期策略。新增或修改缓存逻辑时,必须考虑缓存穿透、数据一致性、失效时机和空值处理。 +- MySQL 持久化必须沿用已有 Mapper/ORM、事务和分页方式。未经用户明确同意,不得以任何形式修改数据库结构。 +- 关键业务方法、事务边界、缓存一致性处理、远程调用容错和复杂查询必须添加必要的中文注释。 +- 接口变更必须评估后台管理端、移动端及其他服务调用方的兼容性;优先保持向后兼容。 +- 日常修改不自动运行 Maven 测试、编译或打包;收到用户明确的集中验证指令后,再验证受影响模块,多模块依赖变更同时验证直接依赖模块。 + +## 完成前项目检查 + +- [ ] 后端改动已考虑接口兼容、事务、缓存和远程调用影响。 diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/hard/HardTerminalFaceDeleteDto.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/hard/HardTerminalFaceDeleteDto.java new file mode 100644 index 000000000..c3fb365ca --- /dev/null +++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/hard/HardTerminalFaceDeleteDto.java @@ -0,0 +1,21 @@ +package com.bsd.pjcloud.hard.api.dto.hard; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.util.List; + +/** + * Android 终端批量删除服务端人脸请求。 + */ +@Data +public class HardTerminalFaceDeleteDto { + + /** + * 待删除的虹软样本编号,设备部门由请求头解析,客户端不得指定。 + */ + @NotNull(message = "人脸样本编号不能为空") + @Size(min = 1, max = 500, message = "单次最多删除500条人脸") + private List sampleIds; +} diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardOrderDailyStatisticsVo.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardOrderDailyStatisticsVo.java new file mode 100644 index 000000000..d52ec565e --- /dev/null +++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardOrderDailyStatisticsVo.java @@ -0,0 +1,19 @@ +package com.bsd.pjcloud.hard.api.vo.order; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDate; + +/** + * 指定预计使用日期的订单统计。 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Schema(description = "订单每日统计") +public class HardOrderDailyStatisticsVo extends HardOrderStatisticsSummaryVo { + + @Schema(description = "统计日期") + private LocalDate date; +} diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardOrderMealStatisticsVo.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardOrderMealStatisticsVo.java new file mode 100644 index 000000000..f5a13bb7c --- /dev/null +++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardOrderMealStatisticsVo.java @@ -0,0 +1,39 @@ +package com.bsd.pjcloud.hard.api.vo.order; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 订单按餐段聚合的统计数据,所有金额统一使用分。 + */ +@Data +@Schema(description = "订单餐段统计") +public class HardOrderMealStatisticsVo { + + @Schema(description = "餐段编码") + private String hourType; + + @Schema(description = "订单数") + private Long orderCount = 0L; + + @Schema(description = "商品份数") + private Long productCount = 0L; + + @Schema(description = "商品总价,单位为分") + private Long totalPrice = 0L; + + @Schema(description = "额外支付,单位为分") + private Long extraPrice = 0L; + + @Schema(description = "配送费,单位为分") + private Long deliveryPrice = 0L; + + @Schema(description = "实际支付,单位为分") + private Long payPrice = 0L; + + @Schema(description = "已退款,单位为分") + private Long refundPrice = 0L; + + @Schema(description = "订单实际,单位为分") + private Long actualPrice = 0L; +} diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardOrderStatisticsSummaryVo.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardOrderStatisticsSummaryVo.java new file mode 100644 index 000000000..cd5e36563 --- /dev/null +++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardOrderStatisticsSummaryVo.java @@ -0,0 +1,42 @@ +package com.bsd.pjcloud.hard.api.vo.order; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * 订单统计的通用汇总数据,所有金额统一使用分。 + */ +@Data +@Schema(description = "订单统计汇总") +public class HardOrderStatisticsSummaryVo { + + @Schema(description = "订单数") + private Long orderCount = 0L; + + @Schema(description = "商品份数") + private Long productCount = 0L; + + @Schema(description = "商品总价,单位为分") + private Long totalPrice = 0L; + + @Schema(description = "额外支付,单位为分") + private Long extraPrice = 0L; + + @Schema(description = "配送费,单位为分") + private Long deliveryPrice = 0L; + + @Schema(description = "实际支付,单位为分") + private Long payPrice = 0L; + + @Schema(description = "已退款,单位为分") + private Long refundPrice = 0L; + + @Schema(description = "订单实际,单位为分") + private Long actualPrice = 0L; + + @Schema(description = "餐段统计") + private List meals = new ArrayList<>(); +} diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardOrderStatisticsVo.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardOrderStatisticsVo.java new file mode 100644 index 000000000..79d957a2b --- /dev/null +++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardOrderStatisticsVo.java @@ -0,0 +1,21 @@ +package com.bsd.pjcloud.hard.api.vo.order; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +/** + * 订单列表统计响应,同时提供当前筛选范围总计和每日明细。 + */ +@Data +@Schema(description = "订单列表统计") +public class HardOrderStatisticsVo { + + @Schema(description = "当前筛选范围总计") + private HardOrderStatisticsSummaryVo total = new HardOrderStatisticsSummaryVo(); + + @Schema(description = "按预计使用日期拆分的统计") + private List daily = new ArrayList<>(); +} diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/hard/HardApiController.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/hard/HardApiController.java index d5f552950..295bfe3c4 100644 --- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/hard/HardApiController.java +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/hard/HardApiController.java @@ -2,6 +2,7 @@ package com.bsd.pjcloud.hard.controller.hard; import com.bsd.pjcloud.common.api.encrypt.annotation.crypto.ApiCryptoAes; +import com.bsd.pjcloud.admin.api.vo.SysUserFaceTerminalDeleteVo; import cn.hutool.core.util.ObjectUtil; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.bsd.pjcloud.common.core.util.R; @@ -59,6 +60,7 @@ public class HardApiController { private final HardOrderTotalService hardOrderTotalService; private final ReservationMenuQueryService reservationMenuQueryService; private final VerificationRecordService verificationRecordService; + private final HardTerminalFaceService hardTerminalFaceService; /** * 食堂、超市 Android 终端心跳状态服务。 @@ -92,6 +94,21 @@ public class HardApiController { return R.ok(); } + /** + * 删除当前设备商户部门及子部门范围内的人脸。 + * + * @param hardId 当前绑定设备 ID + * @param dto 最多 500 条样本编号 + * @return 实际删除及已不存在的样本编号 + */ + @PostMapping("/face/delete") + @Operation(summary = "终端批量删除人脸") + public R deleteTerminalFaces( + @RequestHeader("DEVICE-ID") Long hardId, + @Valid @RequestBody HardTerminalFaceDeleteDto dto) { + return R.ok(hardTerminalFaceService.delete(hardId, dto)); + } + @GetMapping("/get") @Operation(summary = "获取硬件配置信息") public R get(@RequestParam Long id) { diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/order/HardOrderController.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/order/HardOrderController.java index ebf7559f7..fadbc2607 100644 --- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/order/HardOrderController.java +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/order/HardOrderController.java @@ -41,6 +41,7 @@ import com.bsd.pjcloud.hard.api.dto.order.remote.HardUpdateShopStatusDto; import com.bsd.pjcloud.hard.api.dto.order.remote.HardUpdateTakeOutStatusDto; import com.bsd.pjcloud.hard.api.entity.order.HardOrderEntity; import com.bsd.pjcloud.hard.api.vo.order.HardOrderVo; +import com.bsd.pjcloud.hard.api.vo.order.HardOrderStatisticsVo; import com.bsd.pjcloud.hard.api.vo.order.HardPendingReservationCountVo; import com.bsd.pjcloud.hard.api.vo.order.app.HardAppCreateReserveOrdersVo; import com.bsd.pjcloud.hard.api.vo.order.app.HardAppScanPayOrderVo; @@ -85,6 +86,7 @@ public class HardOrderController { private final HardOnlineCanteenStatsService hardOnlineCanteenStatsService; private final HardOrderSyncService hardOrderSyncService; private final HardCanteenTotalService canteenTotalService; + private final HardOrderStatisticsService hardOrderStatisticsService; /** 订单导出服务,负责异步任务、EasyExcel 分片写入和文件中心上传。 */ private final HardOrderExportService hardOrderExportService; @@ -598,6 +600,28 @@ public class HardOrderController { @PostMapping("/page-join") @PreAuthorize("@pms.hasPermission('hard_hardwareOrder_view')") public R pageJoin(@ParameterObject Page page, @RequestBody HardOrderDto dto) { + DataScope dataScope = prepareAdminOrderQuery(dto); + return R.ok(hardwareOrderService.scopePageList(page, dto, dataScope)); + } + + /** + * 按订单列表当前完整筛选条件统计全部命中订单,不受分页大小影响。 + * + * @param dto 与订单分页请求相同的筛选条件 + * @return 范围金额总计,以及按预计使用日期、餐段拆分的商品份数和金额 + */ + @Operation(summary = "订单列表统计", description = "按订单列表完整筛选条件统计范围总计及日期餐段明细") + @PostMapping("/statistics") + @PreAuthorize("@pms.hasPermission('hard_hardwareOrder_view')") + public R statistics(@RequestBody HardOrderDto dto) { + DataScope dataScope = prepareAdminOrderQuery(dto); + return R.ok(hardOrderStatisticsService.statistics(dto, dataScope)); + } + + /** + * 为管理端订单分页和统计统一注入设备类型权限、单商户子级范围及商户部门数据权限。 + */ + private DataScope prepareAdminOrderQuery(HardOrderDto dto) { DataScope dataScope = DataScope.of().func(DataScopeFuncEnum.ALL); dataScope.setScopeDeptName("merchant_dept_id"); PjcloudUser user = SecurityUtils.getUser(); @@ -614,7 +638,7 @@ public class HardOrderController { dto.setMchIds(mchDtoIds); } } - return R.ok(hardwareOrderService.scopePageList(page, dto, dataScope)); + return dataScope; } @Operation(summary = "分页查询未识别订单", description = "分页查询未识别订单") diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/mapper/order/HardOrderMapper.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/mapper/order/HardOrderMapper.java index f7e54249d..6c05ef4da 100644 --- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/mapper/order/HardOrderMapper.java +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/mapper/order/HardOrderMapper.java @@ -16,6 +16,7 @@ import com.bsd.pjcloud.hard.api.entity.order.HardOrderEntity; import com.bsd.pjcloud.hard.api.entity.order.HardParentCarEntity; import com.bsd.pjcloud.hard.api.vo.hard.PanelPorductVo; import com.bsd.pjcloud.hard.api.vo.order.*; +import com.bsd.pjcloud.hard.mapper.order.model.HardOrderStatisticsAggregate; import com.github.yulichang.wrapper.MPJLambdaWrapper; import org.apache.ibatis.annotations.Mapper; @@ -169,6 +170,15 @@ public interface HardOrderMapper extends PjcloudBaseMapper { List scopeList(@Param("dto") HardOrderDto dto, DataScope dataScope); + /** + * 按订单列表完整筛选条件统计订单金额、商品份数及日期餐段明细。 + * + * @param dto 与管理端订单分页相同的查询条件 + * @param dataScope 与管理端订单分页相同的商户部门数据范围 + * @return 按日期、餐段和商户部门预聚合的统计行 + */ + List listStatistics(@Param("dto") HardOrderDto dto, DataScope dataScope); + /** * 按订单筛选条件查询去重后的预约用户主键。 * diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/mapper/order/model/HardOrderStatisticsAggregate.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/mapper/order/model/HardOrderStatisticsAggregate.java new file mode 100644 index 000000000..286f830d4 --- /dev/null +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/mapper/order/model/HardOrderStatisticsAggregate.java @@ -0,0 +1,51 @@ +package com.bsd.pjcloud.hard.mapper.order.model; + +import lombok.Data; + +import java.time.LocalDate; + +/** + * 订单列表统计的数据库聚合行。 + * + *

数据库先按统计日期、餐段和商户部门聚合,保留商户部门是为了让统一数据权限拦截器 + * 与订单分页查询应用完全相同的可见范围;服务层会把权限过滤后的商户部门行再次合并。

+ */ +@Data +public class HardOrderStatisticsAggregate { + + /** 预计使用日期;历史空值由 SQL 回退到订单创建日期。 */ + private LocalDate statisticsDate; + + /** 订单餐段协议编码;历史空值允许为空。 */ + private String hourType; + + /** 商户所属部门,供统一数据权限拦截器过滤。 */ + private Long merchantDeptId; + + /** 订单创建人账号,供“仅本人”及“本人 + 部门”数据权限过滤。 */ + private String createBy; + + /** 命中筛选条件的订单数。 */ + private Long orderCount; + + /** 订单购物车中的原始商品份数。 */ + private Long productCount; + + /** 商品总价,单位为分。 */ + private Long totalPrice; + + /** 额外支付金额,单位为分。 */ + private Long extraPrice; + + /** 配送费,单位为分。 */ + private Long deliveryPrice; + + /** 实际支付金额,单位为分。 */ + private Long payPrice; + + /** 已退款金额,单位为分。 */ + private Long refundPrice; + + /** 订单实际金额,即实际支付减已退款,单位为分。 */ + private Long actualPrice; +} diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardOrderStatisticsService.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardOrderStatisticsService.java new file mode 100644 index 000000000..fcde2a07a --- /dev/null +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardOrderStatisticsService.java @@ -0,0 +1,20 @@ +package com.bsd.pjcloud.hard.service; + +import com.bsd.pjcloud.common.data.datascope.DataScope; +import com.bsd.pjcloud.hard.api.dto.order.HardOrderDto; +import com.bsd.pjcloud.hard.api.vo.order.HardOrderStatisticsVo; + +/** + * 订单管理列表统计服务。 + */ +public interface HardOrderStatisticsService { + + /** + * 按订单列表完整筛选条件和当前操作者数据范围统计全部命中订单。 + * + * @param query 订单列表查询条件 + * @param dataScope 与订单分页一致的商户部门数据范围 + * @return 范围总计及按日期、餐段拆分的统计 + */ + HardOrderStatisticsVo statistics(HardOrderDto query, DataScope dataScope); +} diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardTerminalFaceService.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardTerminalFaceService.java new file mode 100644 index 000000000..9c4e72744 --- /dev/null +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardTerminalFaceService.java @@ -0,0 +1,19 @@ +package com.bsd.pjcloud.hard.service; + +import com.bsd.pjcloud.admin.api.vo.SysUserFaceTerminalDeleteVo; +import com.bsd.pjcloud.hard.api.dto.hard.HardTerminalFaceDeleteDto; + +/** + * 终端在库人脸维护服务。 + */ +public interface HardTerminalFaceService { + + /** + * 以 DEVICE-ID 解析真实商户部门后请求用户服务删除人脸。 + * + * @param hardId 当前设备主键 + * @param dto 样本编号集合 + * @return 实际删除及已不存在的样本编号 + */ + SysUserFaceTerminalDeleteVo delete(Long hardId, HardTerminalFaceDeleteDto dto); +} diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/manager/HardTerminalFaceServiceImpl.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/manager/HardTerminalFaceServiceImpl.java new file mode 100644 index 000000000..652e2e9b2 --- /dev/null +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/manager/HardTerminalFaceServiceImpl.java @@ -0,0 +1,75 @@ +package com.bsd.pjcloud.hard.service.impl.manager; + +import cn.hutool.core.util.StrUtil; +import com.bsd.pjcloud.admin.api.dto.SysUserFaceTerminalDeleteDto; +import com.bsd.pjcloud.admin.api.feign.RemoteUserFaceService; +import com.bsd.pjcloud.admin.api.vo.SysUserFaceTerminalDeleteVo; +import com.bsd.pjcloud.common.core.util.R; +import com.bsd.pjcloud.hard.api.dto.hard.HardTerminalFaceDeleteDto; +import com.bsd.pjcloud.hard.api.entity.manager.HardManagerEntity; +import com.bsd.pjcloud.hard.api.vo.manager.HardManagerVo; +import com.bsd.pjcloud.hard.mapper.manager.HardManagerMapper; +import com.bsd.pjcloud.hard.service.HardManagerService; +import com.bsd.pjcloud.hard.service.HardTerminalFaceService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +/** + * 终端在库人脸维护服务实现。 + */ +@Service +@RequiredArgsConstructor +public class HardTerminalFaceServiceImpl implements HardTerminalFaceService { + + /** + * 设备 Mapper,用于在当前租户内验证 DEVICE-ID。 + */ + private final HardManagerMapper hardManagerMapper; + + /** + * 设备服务,用于解析与终端人脸同步一致的商户部门。 + */ + private final HardManagerService hardManagerService; + + /** + * UPMS 人脸远程服务,负责最终部门树范围校验和事务删除。 + */ + private final RemoteUserFaceService remoteUserFaceService; + + /** + * 根据当前设备真实绑定关系删除部门范围内人脸。 + * + * @param hardId 当前设备主键 + * @param dto 样本编号集合 + * @return 实际删除及已不存在的样本编号 + */ + @Override + public SysUserFaceTerminalDeleteVo delete(Long hardId, HardTerminalFaceDeleteDto dto) { + if (hardId == null) { + throw new RuntimeException("DEVICE-ID不能为空"); + } + HardManagerEntity hard = hardManagerMapper.selectById(hardId); + if (hard == null) { + throw new RuntimeException("当前租户下设备不存在"); + } + if (hard.getMerchantId() == null) { + throw new RuntimeException("设备尚未绑定商户"); + } + HardManagerVo hardDetail = hardManagerService.getByHardId(hardId); + if (hardDetail == null || hardDetail.getMerchantDeptId() == null) { + throw new RuntimeException("设备绑定商户缺少部门"); + } + + SysUserFaceTerminalDeleteDto request = new SysUserFaceTerminalDeleteDto(); + request.setMerchantDeptId(hardDetail.getMerchantDeptId()); + request.setSampleIds(dto.getSampleIds()); + R response = remoteUserFaceService.terminalDelete(request); + if (response == null || !response.isOk() || response.getData() == null) { + String message = response == null || StrUtil.isBlank(response.getMsg()) + ? "用户服务删除人脸失败" + : response.getMsg(); + throw new RuntimeException(message); + } + return response.getData(); + } +} diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardOrderStatisticsServiceImpl.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardOrderStatisticsServiceImpl.java new file mode 100644 index 000000000..7fba45924 --- /dev/null +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardOrderStatisticsServiceImpl.java @@ -0,0 +1,126 @@ +package com.bsd.pjcloud.hard.service.impl.order; + +import com.bsd.pjcloud.common.data.datascope.DataScope; +import com.bsd.pjcloud.hard.api.dto.order.HardOrderDto; +import com.bsd.pjcloud.hard.api.vo.order.HardOrderDailyStatisticsVo; +import com.bsd.pjcloud.hard.api.vo.order.HardOrderMealStatisticsVo; +import com.bsd.pjcloud.hard.api.vo.order.HardOrderStatisticsSummaryVo; +import com.bsd.pjcloud.hard.api.vo.order.HardOrderStatisticsVo; +import com.bsd.pjcloud.hard.mapper.order.HardOrderMapper; +import com.bsd.pjcloud.hard.mapper.order.model.HardOrderStatisticsAggregate; +import com.bsd.pjcloud.hard.service.HardOrderStatisticsService; +import com.bsd.pjcloud.hard.support.HardUserIdentitySupport; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * 订单列表统计实现;数据库负责筛选和初次聚合,服务层合并权限范围内的商户部门行。 + */ +@Service +@RequiredArgsConstructor +public class HardOrderStatisticsServiceImpl implements HardOrderStatisticsService { + + private final HardOrderMapper hardOrderMapper; + private final HardUserIdentitySupport userIdentitySupport; + + @Override + public HardOrderStatisticsVo statistics(HardOrderDto query, DataScope dataScope) { + applyIdentityQuery(query); + List rows = hardOrderMapper.listStatistics(query, dataScope); + return buildStatistics(rows == null ? List.of() : rows); + } + + /** + * 身份和人脸条件必须先转换成用户主键,与订单分页服务的筛选流程保持一致。 + */ + private void applyIdentityQuery(HardOrderDto query) { + if (!userIdentitySupport.hasIdentityQuery(query)) { + return; + } + query.setUserIds(userIdentitySupport.findIdentityUserIds(query, query.getUserId(), query.getUserIds())); + query.setUserId(null); + query.setIdentityTypes(null); + query.setStudentInfo(null); + query.setTeacherInfo(null); + query.setHasFace(null); + } + + /** + * 把按商户部门拆分的数据库结果合并为范围总计、每日统计和餐段统计。 + */ + private HardOrderStatisticsVo buildStatistics(List rows) { + HardOrderStatisticsVo result = new HardOrderStatisticsVo(); + Map dailyMap = new LinkedHashMap<>(); + + for (HardOrderStatisticsAggregate row : rows) { + addAggregate(result.getTotal(), row); + mergeMeal(result.getTotal(), row); + + HardOrderDailyStatisticsVo daily = dailyMap.computeIfAbsent(row.getStatisticsDate(), date -> { + HardOrderDailyStatisticsVo item = new HardOrderDailyStatisticsVo(); + item.setDate(date); + return item; + }); + addAggregate(daily, row); + mergeMeal(daily, row); + } + + sortMeals(result.getTotal()); + List daily = new ArrayList<>(dailyMap.values()); + daily.sort(Comparator.comparing(HardOrderDailyStatisticsVo::getDate, + Comparator.nullsLast(Comparator.reverseOrder()))); + daily.forEach(this::sortMeals); + result.setDaily(daily); + return result; + } + + /** 累加订单级金额和商品份数。 */ + private void addAggregate(HardOrderStatisticsSummaryVo target, HardOrderStatisticsAggregate source) { + target.setOrderCount(target.getOrderCount() + safe(source.getOrderCount())); + target.setProductCount(target.getProductCount() + safe(source.getProductCount())); + target.setTotalPrice(target.getTotalPrice() + safe(source.getTotalPrice())); + target.setExtraPrice(target.getExtraPrice() + safe(source.getExtraPrice())); + target.setDeliveryPrice(target.getDeliveryPrice() + safe(source.getDeliveryPrice())); + target.setPayPrice(target.getPayPrice() + safe(source.getPayPrice())); + target.setRefundPrice(target.getRefundPrice() + safe(source.getRefundPrice())); + target.setActualPrice(target.getActualPrice() + safe(source.getActualPrice())); + } + + /** 同一日期餐段可能被数据权限范围内的多个商户部门拆开,需要再次合并。 */ + private void mergeMeal(HardOrderStatisticsSummaryVo target, HardOrderStatisticsAggregate source) { + HardOrderMealStatisticsVo meal = target.getMeals().stream() + .filter(item -> java.util.Objects.equals(item.getHourType(), source.getHourType())) + .findFirst() + .orElseGet(() -> { + HardOrderMealStatisticsVo item = new HardOrderMealStatisticsVo(); + item.setHourType(source.getHourType()); + target.getMeals().add(item); + return item; + }); + meal.setOrderCount(meal.getOrderCount() + safe(source.getOrderCount())); + meal.setProductCount(meal.getProductCount() + safe(source.getProductCount())); + meal.setTotalPrice(meal.getTotalPrice() + safe(source.getTotalPrice())); + meal.setExtraPrice(meal.getExtraPrice() + safe(source.getExtraPrice())); + meal.setDeliveryPrice(meal.getDeliveryPrice() + safe(source.getDeliveryPrice())); + meal.setPayPrice(meal.getPayPrice() + safe(source.getPayPrice())); + meal.setRefundPrice(meal.getRefundPrice() + safe(source.getRefundPrice())); + meal.setActualPrice(meal.getActualPrice() + safe(source.getActualPrice())); + } + + /** 餐段编码稳定排序,空餐段放到最后;页面会再按当前字典顺序显示。 */ + private void sortMeals(HardOrderStatisticsSummaryVo summary) { + summary.getMeals().sort(Comparator.comparing(HardOrderMealStatisticsVo::getHourType, + Comparator.nullsLast(String::compareTo))); + } + + private long safe(Long value) { + return value == null ? 0L : value; + } +} diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/resources/mapper/order/HardOrderMapper.xml b/pjcloud-hardware/pjcloud-hardware-biz/src/main/resources/mapper/order/HardOrderMapper.xml index 978aed988..6692a5d70 100644 --- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/resources/mapper/order/HardOrderMapper.xml +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/resources/mapper/order/HardOrderMapper.xml @@ -468,6 +468,86 @@ + + +