chen.yinxiang
Committed by Jiaqi Xia

ft:订阅消息创建接口

...@@ -4,7 +4,9 @@ import com.github.xiaoymin.knife4j.annotations.ApiSupport; ...@@ -4,7 +4,9 @@ import com.github.xiaoymin.knife4j.annotations.ApiSupport;
4 import com.infoloop.tianting.annotation.RepeatSubmit; 4 import com.infoloop.tianting.annotation.RepeatSubmit;
5 import com.infoloop.tianting.mealorderservice.SingleMealOrderRpcResponse; 5 import com.infoloop.tianting.mealorderservice.SingleMealOrderRpcResponse;
6 import com.infoloop.tianting.model.common.CreatedResult; 6 import com.infoloop.tianting.model.common.CreatedResult;
7 +import com.infoloop.tianting.model.common.ResponseResult;
7 import com.infoloop.tianting.model.dto.MealOrderDTO; 8 import com.infoloop.tianting.model.dto.MealOrderDTO;
9 +import com.infoloop.tianting.model.dto.OrderSubscriptionMessageDTO;
8 import com.infoloop.tianting.model.vo.DeliveryRuleVO; 10 import com.infoloop.tianting.model.vo.DeliveryRuleVO;
9 import com.infoloop.tianting.model.vo.DinerMealSuspensionRecordVO; 11 import com.infoloop.tianting.model.vo.DinerMealSuspensionRecordVO;
10 import com.infoloop.tianting.model.vo.MealOrderVO; 12 import com.infoloop.tianting.model.vo.MealOrderVO;
...@@ -75,4 +77,14 @@ public class MealOrderController { ...@@ -75,4 +77,14 @@ public class MealOrderController {
75 return mealOrderService.queryDinerMealSuspensionRecordsByCondition(); 77 return mealOrderService.queryDinerMealSuspensionRecordsByCondition();
76 } 78 }
77 79
80 + @ApiOperation(value = "小程序:创建订餐订阅消息授权记录")
81 + @PutMapping("/mealorder/subscriptionmessage")
82 + @ResponseStatus(HttpStatus.CREATED)
83 + public ResponseResult<Boolean> createOrderSubscriptionMessage(
84 + @RequestBody @Valid final OrderSubscriptionMessageDTO.CreateOrderSubscriptionMessageDTO dto
85 + ) {
86 + mealOrderService.createOrderSubscriptionMessage(dto);
87 + return ResponseResult.ok(true);
88 + }
89 +
78 } 90 }
......
...@@ -3,6 +3,7 @@ package com.infoloop.tianting.service; ...@@ -3,6 +3,7 @@ package com.infoloop.tianting.service;
3 import com.infoloop.tianting.mealorderservice.SingleMealOrderRpcResponse; 3 import com.infoloop.tianting.mealorderservice.SingleMealOrderRpcResponse;
4 import com.infoloop.tianting.model.common.CreatedResult; 4 import com.infoloop.tianting.model.common.CreatedResult;
5 import com.infoloop.tianting.model.dto.MealOrderDTO; 5 import com.infoloop.tianting.model.dto.MealOrderDTO;
6 +import com.infoloop.tianting.model.dto.OrderSubscriptionMessageDTO;
6 import com.infoloop.tianting.model.vo.DeliveryRuleVO; 7 import com.infoloop.tianting.model.vo.DeliveryRuleVO;
7 import com.infoloop.tianting.model.vo.DinerMealSuspensionRecordVO; 8 import com.infoloop.tianting.model.vo.DinerMealSuspensionRecordVO;
8 import com.infoloop.tianting.model.vo.MealOrderVO; 9 import com.infoloop.tianting.model.vo.MealOrderVO;
...@@ -30,4 +31,6 @@ public interface MealOrderService { ...@@ -30,4 +31,6 @@ public interface MealOrderService {
30 * @return 停餐信息列表 31 * @return 停餐信息列表
31 */ 32 */
32 List<DinerMealSuspensionRecordVO> queryDinerMealSuspensionRecordsByCondition(); 33 List<DinerMealSuspensionRecordVO> queryDinerMealSuspensionRecordsByCondition();
34 +
35 + void createOrderSubscriptionMessage(OrderSubscriptionMessageDTO.CreateOrderSubscriptionMessageDTO dto);
33 } 36 }
......
...@@ -12,6 +12,7 @@ import com.infoloop.tianting.mealorderservice.CreateMpAccountDinerRefRpcRequest; ...@@ -12,6 +12,7 @@ import com.infoloop.tianting.mealorderservice.CreateMpAccountDinerRefRpcRequest;
12 import com.infoloop.tianting.mealorderservice.CreateMpAccountDinerRefRpcResponse; 12 import com.infoloop.tianting.mealorderservice.CreateMpAccountDinerRefRpcResponse;
13 import com.infoloop.tianting.mealorderservice.CreateMpAccountRpcRequest; 13 import com.infoloop.tianting.mealorderservice.CreateMpAccountRpcRequest;
14 import com.infoloop.tianting.mealorderservice.CreateMpAccountRpcResponse; 14 import com.infoloop.tianting.mealorderservice.CreateMpAccountRpcResponse;
15 +import com.infoloop.tianting.mealorderservice.CreateOrderSubscriptionMessageRpcRequest;
15 import com.infoloop.tianting.mealorderservice.DeleteDinersByIdsRpcRequest; 16 import com.infoloop.tianting.mealorderservice.DeleteDinersByIdsRpcRequest;
16 import com.infoloop.tianting.mealorderservice.DeleteMpAccountDinerRefsByIdsRpcRequest; 17 import com.infoloop.tianting.mealorderservice.DeleteMpAccountDinerRefsByIdsRpcRequest;
17 import com.infoloop.tianting.mealorderservice.DinerCreation; 18 import com.infoloop.tianting.mealorderservice.DinerCreation;
...@@ -302,4 +303,29 @@ public class MealOrderServiceRpcClient { ...@@ -302,4 +303,29 @@ public class MealOrderServiceRpcClient {
302 return mealOrderServiceRpcBlockingStub.queryDinerMealSuspensionRecordsByCondition(request.build()) 303 return mealOrderServiceRpcBlockingStub.queryDinerMealSuspensionRecordsByCondition(request.build())
303 .getResponsesList(); 304 .getResponsesList();
304 } 305 }
306 +
307 + /**
308 + * 小程序订阅消息:创建一次性授权记录(待使用)
309 + */
310 + public long createOrderSubscriptionMessage(
311 + int enterpriseId,
312 + int dinerId,
313 + String openId,
314 + String templateId,
315 + long orderPeriodStartDate,
316 + @Nullable String jumpPath,
317 + @Nullable Long orderPeriodEndDate
318 + ) {
319 + final var req = CreateOrderSubscriptionMessageRpcRequest.newBuilder()
320 + .setEnterpriseId(enterpriseId)
321 + .setDinerId(dinerId)
322 + .setOpenId(openId)
323 + .setTemplateId(templateId)
324 + .setOrderPeriodStartDate(orderPeriodStartDate)
325 + .setJumpPath(jumpPath == null ? "" : jumpPath);
326 + if (orderPeriodEndDate != null) {
327 + req.setOrderPeriodEndDate(orderPeriodEndDate);
328 + }
329 + return mealOrderServiceRpcBlockingStub.createOrderSubscriptionMessage(req.build()).getId();
330 + }
305 } 331 }
......
...@@ -7,6 +7,7 @@ import com.infoloop.tianting.exception.ErrorCodeEnum; ...@@ -7,6 +7,7 @@ import com.infoloop.tianting.exception.ErrorCodeEnum;
7 import com.infoloop.tianting.mealorderservice.*; 7 import com.infoloop.tianting.mealorderservice.*;
8 import com.infoloop.tianting.model.common.CreatedResult; 8 import com.infoloop.tianting.model.common.CreatedResult;
9 import com.infoloop.tianting.model.dto.MealOrderDTO; 9 import com.infoloop.tianting.model.dto.MealOrderDTO;
10 +import com.infoloop.tianting.model.dto.OrderSubscriptionMessageDTO;
10 import com.infoloop.tianting.model.vo.DeliveryRuleVO; 11 import com.infoloop.tianting.model.vo.DeliveryRuleVO;
11 import com.infoloop.tianting.model.vo.DinerMealSuspensionRecordVO; 12 import com.infoloop.tianting.model.vo.DinerMealSuspensionRecordVO;
12 import com.infoloop.tianting.model.vo.MealOrderVO; 13 import com.infoloop.tianting.model.vo.MealOrderVO;
...@@ -19,12 +20,14 @@ import com.infoloop.tianting.service.SuspensionService; ...@@ -19,12 +20,14 @@ import com.infoloop.tianting.service.SuspensionService;
19 import lombok.RequiredArgsConstructor; 20 import lombok.RequiredArgsConstructor;
20 import lombok.extern.slf4j.Slf4j; 21 import lombok.extern.slf4j.Slf4j;
21 import org.springframework.beans.factory.annotation.Autowired; 22 import org.springframework.beans.factory.annotation.Autowired;
23 +import org.springframework.beans.factory.annotation.Value;
22 import org.springframework.stereotype.Service; 24 import org.springframework.stereotype.Service;
23 25
24 import java.time.DayOfWeek; 26 import java.time.DayOfWeek;
25 import java.time.LocalDate; 27 import java.time.LocalDate;
26 import java.time.LocalDateTime; 28 import java.time.LocalDateTime;
27 import java.time.LocalTime; 29 import java.time.LocalTime;
30 +import java.time.ZoneId;
28 import java.util.ArrayList; 31 import java.util.ArrayList;
29 import java.util.Collections; 32 import java.util.Collections;
30 import java.util.Date; 33 import java.util.Date;
...@@ -33,6 +36,8 @@ import java.util.Map; ...@@ -33,6 +36,8 @@ import java.util.Map;
33 import java.util.function.Function; 36 import java.util.function.Function;
34 import java.util.stream.Collectors; 37 import java.util.stream.Collectors;
35 38
39 +import static com.infoloop.tianting.constant.ConfigConstants.MINI_PROGRAM_ORDER_REMINDER_TEMPLATE_ID;
40 +
36 @Slf4j 41 @Slf4j
37 @Service 42 @Service
38 @RequiredArgsConstructor(onConstructor = @__(@Autowired)) 43 @RequiredArgsConstructor(onConstructor = @__(@Autowired))
...@@ -46,6 +51,9 @@ public class MealOrderServiceImpl implements MealOrderService { ...@@ -46,6 +51,9 @@ public class MealOrderServiceImpl implements MealOrderService {
46 51
47 private final SuspensionService suspensionService; 52 private final SuspensionService suspensionService;
48 53
54 + @Value(MINI_PROGRAM_ORDER_REMINDER_TEMPLATE_ID)
55 + private String templateId;
56 +
49 @Override 57 @Override
50 public List<MealOrderVO> queryMealOrders(MealOrderDTO.QueryMealOrderDTO queryMealOrderDTO) { 58 public List<MealOrderVO> queryMealOrders(MealOrderDTO.QueryMealOrderDTO queryMealOrderDTO) {
51 final var loginInfo = LoginContextHolder.getLoginInfo(); 59 final var loginInfo = LoginContextHolder.getLoginInfo();
...@@ -293,4 +301,152 @@ public class MealOrderServiceImpl implements MealOrderService { ...@@ -293,4 +301,152 @@ public class MealOrderServiceImpl implements MealOrderService {
293 * 将RPC响应转换为VO对象 301 * 将RPC响应转换为VO对象
294 */ 302 */
295 // 已改为统一从 SuspensionService 获取并集窗口,不再需要单条转换 303 // 已改为统一从 SuspensionService 获取并集窗口,不再需要单条转换
304 +
305 + @Override
306 + public void createOrderSubscriptionMessage(OrderSubscriptionMessageDTO.CreateOrderSubscriptionMessageDTO dto) {
307 + final var enterpriseId = LoginContextHolder.getEnterpriseId();
308 + final var openId = LoginContextHolder.getOpenId();
309 +
310 + if (openId == null || openId.isBlank()) {
311 + throw new IllegalStateException("当前登录用户 openId 为空,无法创建订阅提醒");
312 + }
313 + if (dto.getJumpPath() == null || dto.getJumpPath().isBlank()) {
314 + throw new IllegalArgumentException("jumpPath 不能为空");
315 + }
316 +
317 + // 1. 处理就餐人 ID:如果为空,从关联关系推断
318 + int dinerId = dto.getDinerId() != null ? dto.getDinerId() : getDefaultDinerId(enterpriseId, openId);
319 +
320 + // 2. 处理模板 ID:如果为空,使用配置的默认值
321 + String finalTemplateId = (dto.getTemplateId() != null && !dto.getTemplateId().isBlank())
322 + ? dto.getTemplateId()
323 + : templateId;
324 + if (finalTemplateId == null || finalTemplateId.isBlank()) {
325 + throw new IllegalStateException("订阅消息模板ID未配置,无法创建订阅提醒");
326 + }
327 +
328 + // 3. 先根据订餐规则配置,计算本次"订餐周期"的开始/结束时间
329 + final var period = resolveOrderPeriod(enterpriseId);
330 +
331 + // 打印所有参数,用于检查库服务版本是否匹配
332 + log.info("创建订阅消息 - 调用库服务参数: enterpriseId={}, dinerId={}, openId={}, templateId={}, orderPeriodStartDate={}, jumpPath={}, orderPeriodEndDate={}",
333 + enterpriseId,
334 + dinerId,
335 + openId,
336 + finalTemplateId,
337 + period.startAt,
338 + dto.getJumpPath(),
339 + period.endAt);
340 +
341 + mealOrderServiceRpcClient.createOrderSubscriptionMessage(
342 + enterpriseId,
343 + dinerId,
344 + openId,
345 + finalTemplateId,
346 + period.startAt,
347 + dto.getJumpPath(),
348 + period.endAt
349 + );
350 + }
351 +
352 + /**
353 + * 如果前端未传就餐人 ID,从当前用户的关联关系中推断默认值
354 + * 如果只有一个关联的就餐人,返回该就餐人 ID;如果有多个或没有,返回 0
355 + */
356 + private int getDefaultDinerId(int enterpriseId, String openId) {
357 + final var loginInfo = LoginContextHolder.getLoginInfo();
358 + final var mpAccountDinerRefs = mealOrderServiceRpcClient.queryMpAccountDinerRefsByCondition(
359 + enterpriseId,
360 + Collections.singletonList(loginInfo.getId()),
361 + Collections.emptyList()
362 + );
363 + if (mpAccountDinerRefs.size() == 1) {
364 + return mpAccountDinerRefs.get(0).getDinerId();
365 + }
366 + return 0;
367 + }
368 +
369 + private static final class Period {
370 + final long startAt;
371 + final Long endAt;
372 +
373 + private Period(long startAt, Long endAt) {
374 + this.startAt = startAt;
375 + this.endAt = endAt;
376 + }
377 + }
378 +
379 + /**
380 + * 根据订餐规则配置推导"最近的未来订餐周期"的开始/结束时间。
381 + *
382 + * 规则说明:
383 + * - 使用 DeliveryRuleServiceRpc.getDefaultDeliveryTemplateAndRule 的 configJson.rules[0]
384 + * - 该规则包含:startTime / endTime(HH:mm:ss)以及 monday~sunday 开关
385 + * - 从"当前时刻"开始,向未来查找最近的一个启用日 + startTime 作为周期开始时间
386 + * - 周期结束时间暂定为:该周最后一个启用日 + endTime(如果滚到了下一周,则用下一周的最后启用日)
387 + */
388 + private Period resolveOrderPeriod(int enterpriseId) {
389 + final var response = deliveryRuleServiceRpcClient.getDefaultDeliveryTemplateAndRule(enterpriseId);
390 + final var rule = response.getRule();
391 + if (rule == null || rule.getConfigJson().getRulesList().isEmpty()) {
392 + throw new IllegalStateException("订餐规则未配置,无法创建订阅提醒");
393 + }
394 +
395 + final var cfg = rule.getConfigJson().getRules(0);
396 + final String startTimeStr = cfg.getStartTime();
397 + final String endTimeStr = cfg.getEndTime();
398 + if (startTimeStr == null || endTimeStr == null) {
399 + throw new IllegalStateException("订餐规则时间配置不完整,无法创建订阅提醒");
400 + }
401 +
402 + final List<DayOfWeek> enabledDays = new ArrayList<>();
403 + if (cfg.getMonday()) enabledDays.add(DayOfWeek.MONDAY);
404 + if (cfg.getTuesday()) enabledDays.add(DayOfWeek.TUESDAY);
405 + if (cfg.getWednesday()) enabledDays.add(DayOfWeek.WEDNESDAY);
406 + if (cfg.getThursday()) enabledDays.add(DayOfWeek.THURSDAY);
407 + if (cfg.getFriday()) enabledDays.add(DayOfWeek.FRIDAY);
408 + if (cfg.getSaturday()) enabledDays.add(DayOfWeek.SATURDAY);
409 + if (cfg.getSunday()) enabledDays.add(DayOfWeek.SUNDAY);
410 + if (enabledDays.isEmpty()) {
411 + throw new IllegalStateException("订餐规则未启用任何星期,无法创建订阅提醒");
412 + }
413 +
414 + final var startTime = LocalTime.parse(startTimeStr, DateUtil.HH_MM_SS);
415 + final var endTime = LocalTime.parse(endTimeStr, DateUtil.HH_MM_SS);
416 +
417 + final ZoneId zone = DateUtil.CHINA_ZONE;
418 + final var now = java.time.ZonedDateTime.now(zone);
419 +
420 + // 从当前时间起,向后最多检查 14 天,找到最近的"启用日 + startTime"
421 + LocalDate periodStartDate = null;
422 + DayOfWeek lastEnabledDay = Collections.max(enabledDays);
423 +
424 + for (int i = 0; i < 14; i++) {
425 + LocalDate candidateDate = now.toLocalDate().plusDays(i);
426 + DayOfWeek dow = candidateDate.getDayOfWeek();
427 + if (!enabledDays.contains(dow)) {
428 + continue;
429 + }
430 + var candidateStart = candidateDate.atTime(startTime).atZone(zone);
431 + if (!candidateStart.isBefore(now)) {
432 + periodStartDate = candidateDate;
433 + break;
434 + }
435 + }
436 +
437 + if (periodStartDate == null) {
438 + throw new IllegalStateException("根据订餐规则无法找到未来的订餐周期开始时间");
439 + }
440 +
441 + // 周期结束时间:使用"同一周内最后一个启用日 + endTime"
442 + // 这里以 periodStartDate 所在周的周一为基准
443 + LocalDate monday = periodStartDate.with(DayOfWeek.MONDAY);
444 + LocalDate lastEnabledDate = monday.plusDays(lastEnabledDay.getValue() - 1L);
445 + var endDateTime = lastEnabledDate.atTime(endTime).atZone(zone);
446 +
447 + long startMs = periodStartDate.atTime(startTime).atZone(zone).toInstant().toEpochMilli();
448 + long endMs = endDateTime.toInstant().toEpochMilli();
449 +
450 + return new Period(startMs, endMs);
451 + }
296 } 452 }
......
...@@ -63,6 +63,12 @@ service MealOrderServiceRpc { ...@@ -63,6 +63,12 @@ service MealOrderServiceRpc {
63 rpc batchCreateDinerMealSuspensionRecords(BatchCreateDinerMealSuspensionRecordsRpcRequest) returns (BatchCreateDinerMealSuspensionRecordsRpcResponse); 63 rpc batchCreateDinerMealSuspensionRecords(BatchCreateDinerMealSuspensionRecordsRpcRequest) returns (BatchCreateDinerMealSuspensionRecordsRpcResponse);
64 rpc updateDinerMealSuspensionRecord(UpdateDinerMealSuspensionRecordRpcRequest) returns (UpdateDinerMealSuspensionRecordRpcResponse); 64 rpc updateDinerMealSuspensionRecord(UpdateDinerMealSuspensionRecordRpcRequest) returns (UpdateDinerMealSuspensionRecordRpcResponse);
65 rpc deleteDinerMealSuspensionRecordsByIds(DeleteDinerMealSuspensionRecordsByIdsRpcRequest) returns (DeleteDinerMealSuspensionRecordsRpcResponse); 65 rpc deleteDinerMealSuspensionRecordsByIds(DeleteDinerMealSuspensionRecordsByIdsRpcRequest) returns (DeleteDinerMealSuspensionRecordsRpcResponse);
66 +
67 + // 订餐订阅消息相关服务
68 + rpc CreateOrderSubscriptionMessage (CreateOrderSubscriptionMessageRpcRequest) returns (CreateOrderSubscriptionMessageRpcResponse) {}
69 + rpc GetUserOrderSubscriptionMessageHistory (GetUserOrderSubscriptionMessageHistoryRpcRequest) returns (GetUserOrderSubscriptionMessageHistoryRpcResponse) {}
70 + rpc BatchDeleteOrderSubscriptionMessagesByIds (BatchDeleteOrderSubscriptionMessagesByIdsRpcRequest) returns (BatchDeleteOrderSubscriptionMessagesByIdsRpcResponse) {}
71 + rpc BatchPhysicalDeleteOrderSubscriptionMessagesByIds (BatchPhysicalDeleteOrderSubscriptionMessagesByIdsRpcRequest) returns (BatchPhysicalDeleteOrderSubscriptionMessagesByIdsRpcResponse) {}
66 } 72 }
67 73
68 enum MealOrderOrderMethodEnum { 74 enum MealOrderOrderMethodEnum {
...@@ -955,3 +961,55 @@ message BatchCreateDinerMealSuspensionRecordsRpcResponse { ...@@ -955,3 +961,55 @@ message BatchCreateDinerMealSuspensionRecordsRpcResponse {
955 bool isCreated = 1; 961 bool isCreated = 1;
956 repeated int32 ids = 2; 962 repeated int32 ids = 2;
957 } 963 }
964 +
965 +// 订餐订阅消息相关消息定义
966 +message CreateOrderSubscriptionMessageRpcRequest {
967 + int32 enterpriseId = 1;
968 + int32 dinerId = 2;
969 + string openId = 3;
970 + string templateId = 4;
971 + int64 orderPeriodStartDate = 5; // Unix 时间戳(毫秒),精确到秒
972 + string jumpPath = 6; // 跳转路径(小程序 path),可选
973 + int64 orderPeriodEndDate = 7; // Unix 时间戳(毫秒),精确到秒,可选
974 +}
975 +
976 +message CreateOrderSubscriptionMessageRpcResponse {
977 + int64 id = 1;
978 +}
979 +
980 +message GetUserOrderSubscriptionMessageHistoryRpcRequest {
981 + int32 enterpriseId = 1;
982 + string openId = 2;
983 + int64 orderPeriodStartDate = 3; // Unix 时间戳(毫秒),精确到秒,可选,如果传入则查询大于等于该值的记录
984 +}
985 +
986 +message OrderSubscriptionMessageRpcResponse {
987 + int64 id = 1;
988 + int64 orderPeriodStartDate = 2; // Unix 时间戳(毫秒),精确到秒
989 + int64 createdAt = 3; // Unix 时间戳(毫秒)
990 + bool isDeleted = 4;
991 + string jumpPath = 5; // 跳转路径(小程序 path),可选
992 + int64 orderPeriodEndDate = 6; // Unix 时间戳(毫秒),精确到秒,可选
993 +}
994 +
995 +message GetUserOrderSubscriptionMessageHistoryRpcResponse {
996 + repeated OrderSubscriptionMessageRpcResponse response = 1;
997 +}
998 +
999 +message BatchDeleteOrderSubscriptionMessagesByIdsRpcRequest {
1000 + int32 enterpriseId = 1;
1001 + repeated int64 ids = 2; // 要删除的记录ID列表
1002 +}
1003 +
1004 +message BatchDeleteOrderSubscriptionMessagesByIdsRpcResponse {
1005 + int32 affectedRows = 1; // 受影响的行数
1006 +}
1007 +
1008 +message BatchPhysicalDeleteOrderSubscriptionMessagesByIdsRpcRequest {
1009 + int32 enterpriseId = 1;
1010 + repeated int64 ids = 2; // 要删除的记录ID列表
1011 +}
1012 +
1013 +message BatchPhysicalDeleteOrderSubscriptionMessagesByIdsRpcResponse {
1014 + int32 affectedRows = 1; // 受影响的行数
1015 +}
......