孟苏慧

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

Qa/2025 04 10



See merge request !17
...@@ -83,9 +83,7 @@ public interface ConfigConstants { ...@@ -83,9 +83,7 @@ public interface ConfigConstants {
83 String MINI_PROGRAM_APP_SECRET = "${miniprogram.appSecret}"; 83 String MINI_PROGRAM_APP_SECRET = "${miniprogram.appSecret}";
84 String MINI_PROGRAM_ORDER_REMINDER_TEMPLATE_ID = "${miniprogram.orderReminderTemplateId}"; 84 String MINI_PROGRAM_ORDER_REMINDER_TEMPLATE_ID = "${miniprogram.orderReminderTemplateId}";
85 85
86 - // 公众号配置(用于订阅消息推送) 86 + // 公众号模板 ID(可选);appId/appSecret 见 WxOfficialAccountHttpClient 字段上的 @Value 默认值
87 - String OFFICIAL_ACCOUNT_APP_ID = "${officialAccount.appId}"; 87 + String OFFICIAL_ACCOUNT_ORDER_REMINDER_TEMPLATE_ID = "${officialAccount.orderReminderTemplateId:}";
88 - String OFFICIAL_ACCOUNT_APP_SECRET = "${officialAccount.appSecret}";
89 - String OFFICIAL_ACCOUNT_ORDER_REMINDER_TEMPLATE_ID = "${officialAccount.orderReminderTemplateId}";
90 88
91 } 89 }
......
...@@ -89,6 +89,10 @@ public class SettlementDTO { ...@@ -89,6 +89,10 @@ public class SettlementDTO {
89 public static class SettlementOrderDetail { 89 public static class SettlementOrderDetail {
90 private String mealTime; 90 private String mealTime;
91 private String code; 91 private String code;
92 + /** 支付完成时间,格式 yyyy-MM-dd HH:mm:ss,无则空 */
93 + private String payTime;
94 + /** 下单人:院区端为操作员名称;住院小程序为客户姓名;阳光厅为客户手机号 */
95 + private String orderer;
92 private String mealName; 96 private String mealName;
93 private String roomNo; 97 private String roomNo;
94 private String customerName; 98 private String customerName;
......
...@@ -2,12 +2,16 @@ package com.infoloop.tianting.service.client; ...@@ -2,12 +2,16 @@ package com.infoloop.tianting.service.client;
2 2
3 import com.infoloop.tianting.COperatorServiceProtoRpcGrpc; 3 import com.infoloop.tianting.COperatorServiceProtoRpcGrpc;
4 import com.infoloop.tianting.GetCOperatorByIdRpcRequest; 4 import com.infoloop.tianting.GetCOperatorByIdRpcRequest;
5 +import com.infoloop.tianting.GetCOperatorByIdsRpcRequest;
5 import com.infoloop.tianting.GetCOperatorByMobileOrEmailRpcRequest; 6 import com.infoloop.tianting.GetCOperatorByMobileOrEmailRpcRequest;
6 import com.infoloop.tianting.SingleCOperatorRpcResponse; 7 import com.infoloop.tianting.SingleCOperatorRpcResponse;
7 import lombok.RequiredArgsConstructor; 8 import lombok.RequiredArgsConstructor;
8 import org.springframework.beans.factory.annotation.Autowired; 9 import org.springframework.beans.factory.annotation.Autowired;
9 import org.springframework.stereotype.Service; 10 import org.springframework.stereotype.Service;
10 11
12 +import java.util.Collections;
13 +import java.util.List;
14 +
11 @Service 15 @Service
12 @RequiredArgsConstructor(onConstructor = @__(@Autowired)) 16 @RequiredArgsConstructor(onConstructor = @__(@Autowired))
13 public class OperatorServiceRpcClient { 17 public class OperatorServiceRpcClient {
...@@ -26,5 +30,15 @@ public class OperatorServiceRpcClient { ...@@ -26,5 +30,15 @@ public class OperatorServiceRpcClient {
26 return cOperatorServiceProtoRpcBlockingStub.getCOperatorById(request).getResponse(); 30 return cOperatorServiceProtoRpcBlockingStub.getCOperatorById(request).getResponse();
27 } 31 }
28 32
33 + public List<SingleCOperatorRpcResponse> getCOperatorsByIds(List<Integer> ids) {
34 + if (ids == null || ids.isEmpty()) {
35 + return Collections.emptyList();
36 + }
37 + return cOperatorServiceProtoRpcBlockingStub.getCOperatorByIds(GetCOperatorByIdsRpcRequest.newBuilder()
38 + .addAllIds(ids)
39 + .setIncludeDeleted(false)
40 + .build())
41 + .getResponsesList();
42 + }
29 43
30 } 44 }
......
...@@ -720,7 +720,7 @@ public class OrderServiceRpcClient { ...@@ -720,7 +720,7 @@ public class OrderServiceRpcClient {
720 if (menuById.getResponse().getOrderRuleJson().getModeOfPayment() == ModeOfPaymentEnum.ONLINE) { 720 if (menuById.getResponse().getOrderRuleJson().getModeOfPayment() == ModeOfPaymentEnum.ONLINE) {
721 final var autoCancelParam = new AutoCancelParam(); 721 final var autoCancelParam = new AutoCancelParam();
722 autoCancelParam.setOrderId(orderResponse.getId()); 722 autoCancelParam.setOrderId(orderResponse.getId());
723 - delayedQueue.addToQueue(OrderAutoCancelDelayTask.AUTO_CANCEL_ORDER_DELAY_QUEUE, autoCancelParam, 10, TimeUnit.MINUTES); 723 + delayedQueue.addToQueue(OrderAutoCancelDelayTask.AUTO_CANCEL_ORDER_DELAY_QUEUE, autoCancelParam, 6, TimeUnit.HOURS);
724 } 724 }
725 } 725 }
726 return CreateOrderResponse.builder().id(orderResponse.getId()).isCreated(true).build(); 726 return CreateOrderResponse.builder().id(orderResponse.getId()).isCreated(true).build();
......
...@@ -2,12 +2,13 @@ package com.infoloop.tianting.service.client; ...@@ -2,12 +2,13 @@ package com.infoloop.tianting.service.client;
2 2
3 import com.infoloop.tianting.model.dto.WxUserDto.SendSubscriptionMessageResponse; 3 import com.infoloop.tianting.model.dto.WxUserDto.SendSubscriptionMessageResponse;
4 import com.infoloop.tianting.model.dto.WxUserDto.WxUserTokenDto; 4 import com.infoloop.tianting.model.dto.WxUserDto.WxUserTokenDto;
5 +import org.springframework.lang.Nullable;
5 import com.infoloop.tianting.utils.JsonUtil; 6 import com.infoloop.tianting.utils.JsonUtil;
6 import lombok.RequiredArgsConstructor; 7 import lombok.RequiredArgsConstructor;
7 import lombok.extern.slf4j.Slf4j; 8 import lombok.extern.slf4j.Slf4j;
8 import org.apache.commons.lang3.StringUtils; 9 import org.apache.commons.lang3.StringUtils;
9 import org.springframework.beans.factory.annotation.Autowired; 10 import org.springframework.beans.factory.annotation.Autowired;
10 -import org.springframework.beans.factory.annotation.Value; 11 +import org.springframework.core.env.Environment;
11 import org.springframework.data.redis.core.StringRedisTemplate; 12 import org.springframework.data.redis.core.StringRedisTemplate;
12 import org.springframework.http.HttpEntity; 13 import org.springframework.http.HttpEntity;
13 import org.springframework.http.HttpHeaders; 14 import org.springframework.http.HttpHeaders;
...@@ -16,13 +17,12 @@ import org.springframework.http.ResponseEntity; ...@@ -16,13 +17,12 @@ import org.springframework.http.ResponseEntity;
16 import org.springframework.stereotype.Service; 17 import org.springframework.stereotype.Service;
17 import org.springframework.web.client.RestTemplate; 18 import org.springframework.web.client.RestTemplate;
18 19
20 +import javax.annotation.PostConstruct;
21 +
19 import java.util.HashMap; 22 import java.util.HashMap;
20 import java.util.Map; 23 import java.util.Map;
21 import java.util.concurrent.TimeUnit; 24 import java.util.concurrent.TimeUnit;
22 25
23 -import static com.infoloop.tianting.constant.ConfigConstants.OFFICIAL_ACCOUNT_APP_ID;
24 -import static com.infoloop.tianting.constant.ConfigConstants.OFFICIAL_ACCOUNT_APP_SECRET;
25 -
26 /** 26 /**
27 * 微信公众号 HTTP 客户端 27 * 微信公众号 HTTP 客户端
28 * 用于发送公众号一次性订阅消息 28 * 用于发送公众号一次性订阅消息
...@@ -42,19 +42,36 @@ public class WxOfficialAccountHttpClient { ...@@ -42,19 +42,36 @@ public class WxOfficialAccountHttpClient {
42 42
43 private final RestTemplate restTemplate; 43 private final RestTemplate restTemplate;
44 private final StringRedisTemplate stringRedisTemplate; 44 private final StringRedisTemplate stringRedisTemplate;
45 + private final Environment environment;
45 46
46 - @Value(OFFICIAL_ACCOUNT_APP_ID) 47 + /** 由 {@link #bindOfficialAccountConfig()} 从 Environment 读取;缺失配置项时为空白,不触发占位符解析 */
47 - private String appId; 48 + private String appId = "";
48 49
49 - @Value(OFFICIAL_ACCOUNT_APP_SECRET) 50 + private String appSecret = "";
50 - private String appSecret;
51 51
52 private final Object lock = new Object(); 52 private final Object lock = new Object();
53 53
54 + @PostConstruct
55 + void bindOfficialAccountConfig() {
56 + this.appId = StringUtils.trimToEmpty(environment.getProperty("officialAccount.appId"));
57 + this.appSecret = StringUtils.trimToEmpty(environment.getProperty("officialAccount.appSecret"));
58 + }
59 +
60 + private boolean isOfficialAccountConfigured() {
61 + return StringUtils.isNotBlank(appId) && StringUtils.isNotBlank(appSecret);
62 + }
63 +
54 /** 64 /**
55 * 获取公众号 access_token(带缓存) 65 * 获取公众号 access_token(带缓存)
66 + *
67 + * @return token;未配置 appId/appSecret 时返回 null(不请求微信接口)
56 */ 68 */
69 + @Nullable
57 public String fetchStableAccessToken() { 70 public String fetchStableAccessToken() {
71 + if (!isOfficialAccountConfigured()) {
72 + log.warn("未配置 officialAccount.appId / officialAccount.appSecret,跳过获取公众号 access_token");
73 + return null;
74 + }
58 log.info("Fetching Official Account access token..."); 75 log.info("Fetching Official Account access token...");
59 String cachedToken = stringRedisTemplate.opsForValue().get(ACCESS_TOKEN_CACHE_KEY); 76 String cachedToken = stringRedisTemplate.opsForValue().get(ACCESS_TOKEN_CACHE_KEY);
60 if (StringUtils.isNotEmpty(cachedToken)) { 77 if (StringUtils.isNotEmpty(cachedToken)) {
...@@ -117,8 +134,15 @@ public class WxOfficialAccountHttpClient { ...@@ -117,8 +134,15 @@ public class WxOfficialAccountHttpClient {
117 Map<String, Object> data, 134 Map<String, Object> data,
118 String url, 135 String url,
119 Map<String, String> miniprogram) { 136 Map<String, String> miniprogram) {
137 + if (!isOfficialAccountConfigured()) {
138 + log.warn("未配置公众号,跳过发送订阅消息 openId={}", openId);
139 + return new SendSubscriptionMessageResponse(-1, "微信公众号未配置");
140 + }
120 try { 141 try {
121 String accessToken = fetchStableAccessToken(); 142 String accessToken = fetchStableAccessToken();
143 + if (accessToken == null) {
144 + return new SendSubscriptionMessageResponse(-1, "未获取到公众号 access_token");
145 + }
122 HttpHeaders headers = new HttpHeaders(); 146 HttpHeaders headers = new HttpHeaders();
123 headers.setContentType(MediaType.APPLICATION_JSON); 147 headers.setContentType(MediaType.APPLICATION_JSON);
124 148
......