This commit is contained in:
2026-08-13 12:22:31 +08:00
parent 16dc03a2e2
commit e742abd1cf
49 changed files with 821 additions and 50 deletions
@@ -9,6 +9,7 @@ import com.baiyue.order.dto.BatchOrderReviewResponse;
import com.baiyue.order.dto.OrderDetailResponse;
import com.baiyue.order.dto.OrderRequests;
import com.baiyue.order.dto.TenantOrderCountResponse;
import com.baiyue.order.dto.TenantOrderStatisticsResponse;
import com.baiyue.order.entity.OrderEntity;
import com.baiyue.order.entity.OrderStatus;
import com.baiyue.order.service.OrderApplicationService;
@@ -134,6 +135,16 @@ public class PlatformFirstLegOrderController {
return ApiResponse.success(service.countByTenantIds(tenantIds));
}
@GetMapping("/tenant-statistics")
@PreAuthorize("hasAuthority('platform:order:list')")
@Operation(summary = "查询客户累计及本月运单数", description = "仅统计已提交的 MASTER 根运单;草稿和 CHILD 拆分单不计入,累计不受后续取消或驳回影响。月份按 Asia/Shanghai 半开区间计算。")
public ApiResponse<TenantOrderStatisticsResponse> tenantStatistics(
@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam long tenantId) {
long scopedTenant = OrderScopeGuard.platform(user, tenantId);
return ApiResponse.success(service.tenantSubmittedStatistics(scopedTenant));
}
@Operation(summary = "Tenant order detail / 租户订单详情", description = "Returns the complete order aggregate within the explicit target tenant. / 返回明确目标租户内的完整订单聚合。")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "Query succeeded / 查询成功"),
@@ -0,0 +1,14 @@
package com.baiyue.order.dto;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(name = "TenantOrderStatisticsResponse", description = "平台客户已提交主运单统计")
public record TenantOrderStatisticsResponse(
@Schema(description = "客户 ID") Long tenantId,
@Schema(description = "历史累计已提交主运单数") Long totalSubmittedOrders,
@Schema(description = "当前上海时区月份已提交主运单数") Long currentMonthSubmittedOrders,
@Schema(description = "本月起始时间(含),ISO-8601") String monthStart,
@Schema(description = "下月起始时间(不含),ISO-8601") String nextMonthStart,
@Schema(description = "统计时区") String zoneId,
@Schema(description = "统计快照时间,ISO-8601") String asOf
) { }
@@ -1,4 +1,25 @@
package com.baiyue.order.mapper; import com.baiyue.order.entity.OrderEntity; import com.baomidou.mybatisplus.core.mapper.BaseMapper; /**
package com.baiyue.order.mapper; import com.baiyue.order.entity.OrderEntity; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.time.LocalDateTime;
import java.util.Map;
/**
* 订单主表 Mapper,提供 {@code ord_order} 的 MyBatis-Plus 基础数据访问能力。
*/
public interface OrderMapper extends BaseMapper<OrderEntity>{}
public interface OrderMapper extends BaseMapper<OrderEntity>{
@Select("""
SELECT COUNT(*) AS total_submitted_orders,
COALESCE(SUM(CASE WHEN submitted_at >= #{monthStart}
AND submitted_at < #{nextMonthStart}
THEN 1 ELSE 0 END), 0) AS current_month_submitted_orders
FROM ord_order
WHERE tenant_id = #{tenantId}
AND deleted = 0
AND order_role = 'MASTER'
AND submitted_at IS NOT NULL
""")
Map<String, Object> selectTenantSubmittedStatistics(
@Param("tenantId") long tenantId,
@Param("monthStart") LocalDateTime monthStart,
@Param("nextMonthStart") LocalDateTime nextMonthStart);
}
@@ -258,6 +258,15 @@ public class OrderRepository {
));
}
/** 一次条件聚合客户已提交主运单,取消/驳回状态不会影响累计值。 */
public Map<String, Object> tenantSubmittedStatistics(long tenantId,
LocalDateTime monthStart,
LocalDateTime nextMonthStart) {
Map<String, Object> aggregate = orderMapper.selectTenantSubmittedStatistics(
tenantId, monthStart, nextMonthStart);
return aggregate == null ? Map.of() : aggregate;
}
public List<OrderCargoEntity> findCargo(long tenantId, long orderId) {
return cargoMapper.selectList(new LambdaQueryWrapper<OrderCargoEntity>()
.eq(OrderCargoEntity::getTenantId, tenantId).eq(OrderCargoEntity::getOrderId, orderId)
@@ -6,6 +6,7 @@ import com.baiyue.common.core.exception.BizException;
import com.baiyue.order.dto.OrderDetailResponse;
import com.baiyue.order.dto.OrderRequests;
import com.baiyue.order.dto.TenantOrderCountResponse;
import com.baiyue.order.dto.TenantOrderStatisticsResponse;
import com.baiyue.order.client.FirstlegInboundClient;
import com.baiyue.order.entity.*;
import com.baiyue.order.repository.OrderRepository;
@@ -23,6 +24,7 @@ import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import java.time.*;
import java.util.Locale;
import java.util.Map;
import java.util.stream.Collectors;
@@ -154,6 +156,30 @@ public class OrderApplicationService {
.map(tenantId -> new TenantOrderCountResponse(tenantId, counts.getOrDefault(tenantId, 0L)))
.toList();
}
private static final ZoneId STATISTICS_ZONE = ZoneId.of("Asia/Shanghai");
public TenantOrderStatisticsResponse tenantSubmittedStatistics(long tenantId) {
return tenantSubmittedStatistics(tenantId, Clock.system(STATISTICS_ZONE));
}
TenantOrderStatisticsResponse tenantSubmittedStatistics(long tenantId, Clock clock) {
ZonedDateTime asOf = ZonedDateTime.now(clock).withZoneSameInstant(STATISTICS_ZONE);
YearMonth month = YearMonth.from(asOf);
ZonedDateTime monthStart = month.atDay(1).atStartOfDay(STATISTICS_ZONE);
ZonedDateTime nextMonthStart = month.plusMonths(1).atDay(1).atStartOfDay(STATISTICS_ZONE);
var aggregate = repository.tenantSubmittedStatistics(tenantId,
monthStart.toLocalDateTime(), nextMonthStart.toLocalDateTime());
long total = number(aggregate.get("total_submitted_orders"));
long current = number(aggregate.get("current_month_submitted_orders"));
return new TenantOrderStatisticsResponse(tenantId, total, current,
monthStart.toOffsetDateTime().toString(), nextMonthStart.toOffsetDateTime().toString(),
STATISTICS_ZONE.getId(), asOf.toOffsetDateTime().toString());
}
private static long number(Object value) {
return value instanceof Number n ? n.longValue() : 0L;
}
public OrderDetailResponse detail(long tenantId, long id) {
OrderEntity order = require(tenantId, id);
return new OrderDetailResponse(order, repository.findCargo(tenantId, id), repository.findParties(tenantId, id),
+1
View File
@@ -55,6 +55,7 @@ CREATE TABLE IF NOT EXISTS ord_order (
UNIQUE KEY uk_order_tenant_no (tenant_id, order_no),
UNIQUE KEY uk_order_tenant_inbound_code (tenant_id, inbound_code),
KEY idx_order_tenant_status_time (tenant_id, status, created_at),
KEY idx_order_tenant_role_submitted (tenant_id, order_role, submitted_at),
KEY idx_order_tenant_customer (tenant_id, customer_id),
KEY idx_order_tenant_warehouse (tenant_id, destination_warehouse_id)
) ENGINE=InnoDB COMMENT='头程订单主表:保存商户委托、目的仓、运输方式和订单当前状态';
@@ -231,6 +231,25 @@ paths:
responses:
'200': {description: 成功或业务失败,按响应体 code 判断}
'403': {description: 需要 platform:order:create 权限}
/api/platform/v1/orders/first-leg/tenant-statistics:
get:
tags: [Platform first-leg orders]
summary: 查询客户累计及本月运单数
description: 仅统计 submitted_at 非空的 MASTER 根运单,草稿和 CHILD 拆分单不计入;累计不受取消或驳回影响;月份按 Asia/Shanghai 半开区间计算。
parameters:
- {$ref: '#/components/parameters/TenantId'}
responses:
'200':
description: 成功;需要 platform:order:list 权限
content:
application/json:
schema:
type: object
properties:
code: {type: string, example: '200'}
data: {$ref: '#/components/schemas/TenantOrderStatisticsResponse'}
'403': {description: 需要平台身份和 platform:order:list 权限}
/api/platform/v1/orders/first-leg/tenant-counts:
get:
summary: 批量查询客户运单数
@@ -327,6 +346,18 @@ paths:
'403': {description: 需要 merchant:order:view 权限}
components:
schemas:
TenantOrderStatisticsResponse:
type: object
required: [tenantId, totalSubmittedOrders, currentMonthSubmittedOrders, monthStart, nextMonthStart, zoneId, asOf]
properties:
tenantId: {type: string, pattern: '^[1-9][0-9]*$', description: Long 按字符串序列化以避免 JavaScript 精度丢失}
totalSubmittedOrders: {type: string, pattern: '^[0-9]+$', description: Long 按字符串序列化}
currentMonthSubmittedOrders: {type: string, pattern: '^[0-9]+$', description: Long 按字符串序列化}
monthStart: {type: string, format: date-time}
nextMonthStart: {type: string, format: date-time}
zoneId: {type: string, example: Asia/Shanghai}
asOf: {type: string, format: date-time}
securitySchemes:
bearerAuth:
type: http