孟苏慧

Merge branch 'qa/2025-04-10' into 'master'

Qa/2025 04 10



See merge request !17
......@@ -83,9 +83,7 @@ public interface ConfigConstants {
String MINI_PROGRAM_APP_SECRET = "${miniprogram.appSecret}";
String MINI_PROGRAM_ORDER_REMINDER_TEMPLATE_ID = "${miniprogram.orderReminderTemplateId}";
// 公众号配置(用于订阅消息推送)
String OFFICIAL_ACCOUNT_APP_ID = "${officialAccount.appId}";
String OFFICIAL_ACCOUNT_APP_SECRET = "${officialAccount.appSecret}";
String OFFICIAL_ACCOUNT_ORDER_REMINDER_TEMPLATE_ID = "${officialAccount.orderReminderTemplateId}";
// 公众号模板 ID(可选);appId/appSecret 见 WxOfficialAccountHttpClient 字段上的 @Value 默认值
String OFFICIAL_ACCOUNT_ORDER_REMINDER_TEMPLATE_ID = "${officialAccount.orderReminderTemplateId:}";
}
......
......@@ -89,6 +89,10 @@ public class SettlementDTO {
public static class SettlementOrderDetail {
private String mealTime;
private String code;
/** 支付完成时间,格式 yyyy-MM-dd HH:mm:ss,无则空 */
private String payTime;
/** 下单人:院区端为操作员名称;住院小程序为客户姓名;阳光厅为客户手机号 */
private String orderer;
private String mealName;
private String roomNo;
private String customerName;
......
......@@ -2,12 +2,16 @@ package com.infoloop.tianting.service.client;
import com.infoloop.tianting.COperatorServiceProtoRpcGrpc;
import com.infoloop.tianting.GetCOperatorByIdRpcRequest;
import com.infoloop.tianting.GetCOperatorByIdsRpcRequest;
import com.infoloop.tianting.GetCOperatorByMobileOrEmailRpcRequest;
import com.infoloop.tianting.SingleCOperatorRpcResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.List;
@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class OperatorServiceRpcClient {
......@@ -26,5 +30,15 @@ public class OperatorServiceRpcClient {
return cOperatorServiceProtoRpcBlockingStub.getCOperatorById(request).getResponse();
}
public List<SingleCOperatorRpcResponse> getCOperatorsByIds(List<Integer> ids) {
if (ids == null || ids.isEmpty()) {
return Collections.emptyList();
}
return cOperatorServiceProtoRpcBlockingStub.getCOperatorByIds(GetCOperatorByIdsRpcRequest.newBuilder()
.addAllIds(ids)
.setIncludeDeleted(false)
.build())
.getResponsesList();
}
}
......
......@@ -720,7 +720,7 @@ public class OrderServiceRpcClient {
if (menuById.getResponse().getOrderRuleJson().getModeOfPayment() == ModeOfPaymentEnum.ONLINE) {
final var autoCancelParam = new AutoCancelParam();
autoCancelParam.setOrderId(orderResponse.getId());
delayedQueue.addToQueue(OrderAutoCancelDelayTask.AUTO_CANCEL_ORDER_DELAY_QUEUE, autoCancelParam, 10, TimeUnit.MINUTES);
delayedQueue.addToQueue(OrderAutoCancelDelayTask.AUTO_CANCEL_ORDER_DELAY_QUEUE, autoCancelParam, 6, TimeUnit.HOURS);
}
}
return CreateOrderResponse.builder().id(orderResponse.getId()).isCreated(true).build();
......
......@@ -2,12 +2,13 @@ package com.infoloop.tianting.service.client;
import com.infoloop.tianting.model.dto.WxUserDto.SendSubscriptionMessageResponse;
import com.infoloop.tianting.model.dto.WxUserDto.WxUserTokenDto;
import org.springframework.lang.Nullable;
import com.infoloop.tianting.utils.JsonUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.env.Environment;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
......@@ -16,13 +17,12 @@ import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import javax.annotation.PostConstruct;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static com.infoloop.tianting.constant.ConfigConstants.OFFICIAL_ACCOUNT_APP_ID;
import static com.infoloop.tianting.constant.ConfigConstants.OFFICIAL_ACCOUNT_APP_SECRET;
/**
* 微信公众号 HTTP 客户端
* 用于发送公众号一次性订阅消息
......@@ -42,19 +42,36 @@ public class WxOfficialAccountHttpClient {
private final RestTemplate restTemplate;
private final StringRedisTemplate stringRedisTemplate;
private final Environment environment;
@Value(OFFICIAL_ACCOUNT_APP_ID)
private String appId;
/** 由 {@link #bindOfficialAccountConfig()} 从 Environment 读取;缺失配置项时为空白,不触发占位符解析 */
private String appId = "";
@Value(OFFICIAL_ACCOUNT_APP_SECRET)
private String appSecret;
private String appSecret = "";
private final Object lock = new Object();
@PostConstruct
void bindOfficialAccountConfig() {
this.appId = StringUtils.trimToEmpty(environment.getProperty("officialAccount.appId"));
this.appSecret = StringUtils.trimToEmpty(environment.getProperty("officialAccount.appSecret"));
}
private boolean isOfficialAccountConfigured() {
return StringUtils.isNotBlank(appId) && StringUtils.isNotBlank(appSecret);
}
/**
* 获取公众号 access_token(带缓存)
*
* @return token;未配置 appId/appSecret 时返回 null(不请求微信接口)
*/
@Nullable
public String fetchStableAccessToken() {
if (!isOfficialAccountConfigured()) {
log.warn("未配置 officialAccount.appId / officialAccount.appSecret,跳过获取公众号 access_token");
return null;
}
log.info("Fetching Official Account access token...");
String cachedToken = stringRedisTemplate.opsForValue().get(ACCESS_TOKEN_CACHE_KEY);
if (StringUtils.isNotEmpty(cachedToken)) {
......@@ -117,8 +134,15 @@ public class WxOfficialAccountHttpClient {
Map<String, Object> data,
String url,
Map<String, String> miniprogram) {
if (!isOfficialAccountConfigured()) {
log.warn("未配置公众号,跳过发送订阅消息 openId={}", openId);
return new SendSubscriptionMessageResponse(-1, "微信公众号未配置");
}
try {
String accessToken = fetchStableAccessToken();
if (accessToken == null) {
return new SendSubscriptionMessageResponse(-1, "未获取到公众号 access_token");
}
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
......