chen.yinxiang

ft:订阅消息创建接口

......@@ -4,7 +4,9 @@ import com.github.xiaoymin.knife4j.annotations.ApiSupport;
import com.infoloop.tianting.annotation.RepeatSubmit;
import com.infoloop.tianting.mealorderservice.SingleMealOrderRpcResponse;
import com.infoloop.tianting.model.common.CreatedResult;
import com.infoloop.tianting.model.common.ResponseResult;
import com.infoloop.tianting.model.dto.MealOrderDTO;
import com.infoloop.tianting.model.dto.OrderSubscriptionMessageDTO;
import com.infoloop.tianting.model.vo.DeliveryRuleVO;
import com.infoloop.tianting.model.vo.DinerMealSuspensionRecordVO;
import com.infoloop.tianting.model.vo.MealOrderVO;
......@@ -75,4 +77,14 @@ public class MealOrderController {
return mealOrderService.queryDinerMealSuspensionRecordsByCondition();
}
@ApiOperation(value = "小程序:创建订餐订阅消息授权记录")
@PutMapping("/mealorder/subscriptionmessage")
@ResponseStatus(HttpStatus.CREATED)
public ResponseResult<Boolean> createOrderSubscriptionMessage(
@RequestBody @Valid final OrderSubscriptionMessageDTO.CreateOrderSubscriptionMessageDTO dto
) {
mealOrderService.createOrderSubscriptionMessage(dto);
return ResponseResult.ok(true);
}
}
......
......@@ -3,6 +3,7 @@ package com.infoloop.tianting.service;
import com.infoloop.tianting.mealorderservice.SingleMealOrderRpcResponse;
import com.infoloop.tianting.model.common.CreatedResult;
import com.infoloop.tianting.model.dto.MealOrderDTO;
import com.infoloop.tianting.model.dto.OrderSubscriptionMessageDTO;
import com.infoloop.tianting.model.vo.DeliveryRuleVO;
import com.infoloop.tianting.model.vo.DinerMealSuspensionRecordVO;
import com.infoloop.tianting.model.vo.MealOrderVO;
......@@ -30,4 +31,6 @@ public interface MealOrderService {
* @return 停餐信息列表
*/
List<DinerMealSuspensionRecordVO> queryDinerMealSuspensionRecordsByCondition();
void createOrderSubscriptionMessage(OrderSubscriptionMessageDTO.CreateOrderSubscriptionMessageDTO dto);
}
......
......@@ -12,6 +12,7 @@ import com.infoloop.tianting.mealorderservice.CreateMpAccountDinerRefRpcRequest;
import com.infoloop.tianting.mealorderservice.CreateMpAccountDinerRefRpcResponse;
import com.infoloop.tianting.mealorderservice.CreateMpAccountRpcRequest;
import com.infoloop.tianting.mealorderservice.CreateMpAccountRpcResponse;
import com.infoloop.tianting.mealorderservice.CreateOrderSubscriptionMessageRpcRequest;
import com.infoloop.tianting.mealorderservice.DeleteDinersByIdsRpcRequest;
import com.infoloop.tianting.mealorderservice.DeleteMpAccountDinerRefsByIdsRpcRequest;
import com.infoloop.tianting.mealorderservice.DinerCreation;
......@@ -302,4 +303,29 @@ public class MealOrderServiceRpcClient {
return mealOrderServiceRpcBlockingStub.queryDinerMealSuspensionRecordsByCondition(request.build())
.getResponsesList();
}
/**
* 小程序订阅消息:创建一次性授权记录(待使用)
*/
public long createOrderSubscriptionMessage(
int enterpriseId,
int dinerId,
String openId,
String templateId,
long orderPeriodStartDate,
@Nullable String jumpPath,
@Nullable Long orderPeriodEndDate
) {
final var req = CreateOrderSubscriptionMessageRpcRequest.newBuilder()
.setEnterpriseId(enterpriseId)
.setDinerId(dinerId)
.setOpenId(openId)
.setTemplateId(templateId)
.setOrderPeriodStartDate(orderPeriodStartDate)
.setJumpPath(jumpPath == null ? "" : jumpPath);
if (orderPeriodEndDate != null) {
req.setOrderPeriodEndDate(orderPeriodEndDate);
}
return mealOrderServiceRpcBlockingStub.createOrderSubscriptionMessage(req.build()).getId();
}
}
......
......@@ -7,6 +7,7 @@ import com.infoloop.tianting.exception.ErrorCodeEnum;
import com.infoloop.tianting.mealorderservice.*;
import com.infoloop.tianting.model.common.CreatedResult;
import com.infoloop.tianting.model.dto.MealOrderDTO;
import com.infoloop.tianting.model.dto.OrderSubscriptionMessageDTO;
import com.infoloop.tianting.model.vo.DeliveryRuleVO;
import com.infoloop.tianting.model.vo.DinerMealSuspensionRecordVO;
import com.infoloop.tianting.model.vo.MealOrderVO;
......@@ -19,12 +20,14 @@ import com.infoloop.tianting.service.SuspensionService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
......@@ -33,6 +36,8 @@ import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import static com.infoloop.tianting.constant.ConfigConstants.MINI_PROGRAM_ORDER_REMINDER_TEMPLATE_ID;
@Slf4j
@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
......@@ -46,6 +51,9 @@ public class MealOrderServiceImpl implements MealOrderService {
private final SuspensionService suspensionService;
@Value(MINI_PROGRAM_ORDER_REMINDER_TEMPLATE_ID)
private String templateId;
@Override
public List<MealOrderVO> queryMealOrders(MealOrderDTO.QueryMealOrderDTO queryMealOrderDTO) {
final var loginInfo = LoginContextHolder.getLoginInfo();
......@@ -293,4 +301,152 @@ public class MealOrderServiceImpl implements MealOrderService {
* 将RPC响应转换为VO对象
*/
// 已改为统一从 SuspensionService 获取并集窗口,不再需要单条转换
@Override
public void createOrderSubscriptionMessage(OrderSubscriptionMessageDTO.CreateOrderSubscriptionMessageDTO dto) {
final var enterpriseId = LoginContextHolder.getEnterpriseId();
final var openId = LoginContextHolder.getOpenId();
if (openId == null || openId.isBlank()) {
throw new IllegalStateException("当前登录用户 openId 为空,无法创建订阅提醒");
}
if (dto.getJumpPath() == null || dto.getJumpPath().isBlank()) {
throw new IllegalArgumentException("jumpPath 不能为空");
}
// 1. 处理就餐人 ID:如果为空,从关联关系推断
int dinerId = dto.getDinerId() != null ? dto.getDinerId() : getDefaultDinerId(enterpriseId, openId);
// 2. 处理模板 ID:如果为空,使用配置的默认值
String finalTemplateId = (dto.getTemplateId() != null && !dto.getTemplateId().isBlank())
? dto.getTemplateId()
: templateId;
if (finalTemplateId == null || finalTemplateId.isBlank()) {
throw new IllegalStateException("订阅消息模板ID未配置,无法创建订阅提醒");
}
// 3. 先根据订餐规则配置,计算本次"订餐周期"的开始/结束时间
final var period = resolveOrderPeriod(enterpriseId);
// 打印所有参数,用于检查库服务版本是否匹配
log.info("创建订阅消息 - 调用库服务参数: enterpriseId={}, dinerId={}, openId={}, templateId={}, orderPeriodStartDate={}, jumpPath={}, orderPeriodEndDate={}",
enterpriseId,
dinerId,
openId,
finalTemplateId,
period.startAt,
dto.getJumpPath(),
period.endAt);
mealOrderServiceRpcClient.createOrderSubscriptionMessage(
enterpriseId,
dinerId,
openId,
finalTemplateId,
period.startAt,
dto.getJumpPath(),
period.endAt
);
}
/**
* 如果前端未传就餐人 ID,从当前用户的关联关系中推断默认值
* 如果只有一个关联的就餐人,返回该就餐人 ID;如果有多个或没有,返回 0
*/
private int getDefaultDinerId(int enterpriseId, String openId) {
final var loginInfo = LoginContextHolder.getLoginInfo();
final var mpAccountDinerRefs = mealOrderServiceRpcClient.queryMpAccountDinerRefsByCondition(
enterpriseId,
Collections.singletonList(loginInfo.getId()),
Collections.emptyList()
);
if (mpAccountDinerRefs.size() == 1) {
return mpAccountDinerRefs.get(0).getDinerId();
}
return 0;
}
private static final class Period {
final long startAt;
final Long endAt;
private Period(long startAt, Long endAt) {
this.startAt = startAt;
this.endAt = endAt;
}
}
/**
* 根据订餐规则配置推导"最近的未来订餐周期"的开始/结束时间。
*
* 规则说明:
* - 使用 DeliveryRuleServiceRpc.getDefaultDeliveryTemplateAndRule 的 configJson.rules[0]
* - 该规则包含:startTime / endTime(HH:mm:ss)以及 monday~sunday 开关
* - 从"当前时刻"开始,向未来查找最近的一个启用日 + startTime 作为周期开始时间
* - 周期结束时间暂定为:该周最后一个启用日 + endTime(如果滚到了下一周,则用下一周的最后启用日)
*/
private Period resolveOrderPeriod(int enterpriseId) {
final var response = deliveryRuleServiceRpcClient.getDefaultDeliveryTemplateAndRule(enterpriseId);
final var rule = response.getRule();
if (rule == null || rule.getConfigJson().getRulesList().isEmpty()) {
throw new IllegalStateException("订餐规则未配置,无法创建订阅提醒");
}
final var cfg = rule.getConfigJson().getRules(0);
final String startTimeStr = cfg.getStartTime();
final String endTimeStr = cfg.getEndTime();
if (startTimeStr == null || endTimeStr == null) {
throw new IllegalStateException("订餐规则时间配置不完整,无法创建订阅提醒");
}
final List<DayOfWeek> enabledDays = new ArrayList<>();
if (cfg.getMonday()) enabledDays.add(DayOfWeek.MONDAY);
if (cfg.getTuesday()) enabledDays.add(DayOfWeek.TUESDAY);
if (cfg.getWednesday()) enabledDays.add(DayOfWeek.WEDNESDAY);
if (cfg.getThursday()) enabledDays.add(DayOfWeek.THURSDAY);
if (cfg.getFriday()) enabledDays.add(DayOfWeek.FRIDAY);
if (cfg.getSaturday()) enabledDays.add(DayOfWeek.SATURDAY);
if (cfg.getSunday()) enabledDays.add(DayOfWeek.SUNDAY);
if (enabledDays.isEmpty()) {
throw new IllegalStateException("订餐规则未启用任何星期,无法创建订阅提醒");
}
final var startTime = LocalTime.parse(startTimeStr, DateUtil.HH_MM_SS);
final var endTime = LocalTime.parse(endTimeStr, DateUtil.HH_MM_SS);
final ZoneId zone = DateUtil.CHINA_ZONE;
final var now = java.time.ZonedDateTime.now(zone);
// 从当前时间起,向后最多检查 14 天,找到最近的"启用日 + startTime"
LocalDate periodStartDate = null;
DayOfWeek lastEnabledDay = Collections.max(enabledDays);
for (int i = 0; i < 14; i++) {
LocalDate candidateDate = now.toLocalDate().plusDays(i);
DayOfWeek dow = candidateDate.getDayOfWeek();
if (!enabledDays.contains(dow)) {
continue;
}
var candidateStart = candidateDate.atTime(startTime).atZone(zone);
if (!candidateStart.isBefore(now)) {
periodStartDate = candidateDate;
break;
}
}
if (periodStartDate == null) {
throw new IllegalStateException("根据订餐规则无法找到未来的订餐周期开始时间");
}
// 周期结束时间:使用"同一周内最后一个启用日 + endTime"
// 这里以 periodStartDate 所在周的周一为基准
LocalDate monday = periodStartDate.with(DayOfWeek.MONDAY);
LocalDate lastEnabledDate = monday.plusDays(lastEnabledDay.getValue() - 1L);
var endDateTime = lastEnabledDate.atTime(endTime).atZone(zone);
long startMs = periodStartDate.atTime(startTime).atZone(zone).toInstant().toEpochMilli();
long endMs = endDateTime.toInstant().toEpochMilli();
return new Period(startMs, endMs);
}
}
......
......@@ -63,6 +63,12 @@ service MealOrderServiceRpc {
rpc batchCreateDinerMealSuspensionRecords(BatchCreateDinerMealSuspensionRecordsRpcRequest) returns (BatchCreateDinerMealSuspensionRecordsRpcResponse);
rpc updateDinerMealSuspensionRecord(UpdateDinerMealSuspensionRecordRpcRequest) returns (UpdateDinerMealSuspensionRecordRpcResponse);
rpc deleteDinerMealSuspensionRecordsByIds(DeleteDinerMealSuspensionRecordsByIdsRpcRequest) returns (DeleteDinerMealSuspensionRecordsRpcResponse);
// 订餐订阅消息相关服务
rpc CreateOrderSubscriptionMessage (CreateOrderSubscriptionMessageRpcRequest) returns (CreateOrderSubscriptionMessageRpcResponse) {}
rpc GetUserOrderSubscriptionMessageHistory (GetUserOrderSubscriptionMessageHistoryRpcRequest) returns (GetUserOrderSubscriptionMessageHistoryRpcResponse) {}
rpc BatchDeleteOrderSubscriptionMessagesByIds (BatchDeleteOrderSubscriptionMessagesByIdsRpcRequest) returns (BatchDeleteOrderSubscriptionMessagesByIdsRpcResponse) {}
rpc BatchPhysicalDeleteOrderSubscriptionMessagesByIds (BatchPhysicalDeleteOrderSubscriptionMessagesByIdsRpcRequest) returns (BatchPhysicalDeleteOrderSubscriptionMessagesByIdsRpcResponse) {}
}
enum MealOrderOrderMethodEnum {
......@@ -955,3 +961,55 @@ message BatchCreateDinerMealSuspensionRecordsRpcResponse {
bool isCreated = 1;
repeated int32 ids = 2;
}
// 订餐订阅消息相关消息定义
message CreateOrderSubscriptionMessageRpcRequest {
int32 enterpriseId = 1;
int32 dinerId = 2;
string openId = 3;
string templateId = 4;
int64 orderPeriodStartDate = 5; // Unix 时间戳(毫秒),精确到秒
string jumpPath = 6; // 跳转路径(小程序 path),可选
int64 orderPeriodEndDate = 7; // Unix 时间戳(毫秒),精确到秒,可选
}
message CreateOrderSubscriptionMessageRpcResponse {
int64 id = 1;
}
message GetUserOrderSubscriptionMessageHistoryRpcRequest {
int32 enterpriseId = 1;
string openId = 2;
int64 orderPeriodStartDate = 3; // Unix 时间戳(毫秒),精确到秒,可选,如果传入则查询大于等于该值的记录
}
message OrderSubscriptionMessageRpcResponse {
int64 id = 1;
int64 orderPeriodStartDate = 2; // Unix 时间戳(毫秒),精确到秒
int64 createdAt = 3; // Unix 时间戳(毫秒)
bool isDeleted = 4;
string jumpPath = 5; // 跳转路径(小程序 path),可选
int64 orderPeriodEndDate = 6; // Unix 时间戳(毫秒),精确到秒,可选
}
message GetUserOrderSubscriptionMessageHistoryRpcResponse {
repeated OrderSubscriptionMessageRpcResponse response = 1;
}
message BatchDeleteOrderSubscriptionMessagesByIdsRpcRequest {
int32 enterpriseId = 1;
repeated int64 ids = 2; // 要删除的记录ID列表
}
message BatchDeleteOrderSubscriptionMessagesByIdsRpcResponse {
int32 affectedRows = 1; // 受影响的行数
}
message BatchPhysicalDeleteOrderSubscriptionMessagesByIdsRpcRequest {
int32 enterpriseId = 1;
repeated int64 ids = 2; // 要删除的记录ID列表
}
message BatchPhysicalDeleteOrderSubscriptionMessagesByIdsRpcResponse {
int32 affectedRows = 1; // 受影响的行数
}
......